Modularizing C Code - Managing large projects.pdf

(92 KB) Pobierz
Modularizing C Code: Managing large projects
Modularizing C Code: Managing large projects
By Dean Camera
There comes a time when a project has outgrown the scope of a single file. Perhaps
it's the point at with you need to add more RAM to your computer just to open your
project, or when you realize you've just spent 45 minutes tracking down the timeout
function you swore you wrote two weeks ago. Whatever the reason, it's time to split
your project up into several manageable files, which you can then keep better track
of. This tutorial will give several pointers about the intricacies of how to accomplish
such a task.
Step one - identify common routines
First of all, it makes sense to group all the common routines into their own files - eg.
one for USART management, one for your protocol layer handler, one for your main
code. You should determine which functions can be moved into a separate files while
still retaining some connection with one another. Let's take the following example of
routines:
GetUSARTByte()
TurnOnStatusLed()
SetupSpeaker()
TurnOffStatusLed()
SetupUSART()
main()
CheckTempSensorVal()
ProcessByte()
Beep()
EnableUSART()
SleepMode()
CheckADCChanel()
We can split these routines up into several small files - one for the USART, one for
the main (and misc) routines, one for the speaker and one for the ADC (and related
processing).
Let's take a look at our proposed new structure:
USART.c:
SetupUSART()
EnableUSART()
GetUSARTByte()
ProcessByte()
Page 1
316730611.007.png 316730611.008.png 316730611.009.png
Speaker.c:
SetupSpeaker()
Beep()
ADC.c:
CheckADCChanel()
CheckTempSensorVal()
Main.c:
main()
SleepMode()
TurnOnStatusLed()
TurnOffStatusLed()
Ok, looks pretty good! We'll cut and paste those routines into the appropriate files in
the same project directory, so now we have a bunch of C files containing related
functions.
Adding the new files to your makefile
Even though you've made no functional changes to your code outside moving
certain routines to a different file, you still need to tell the compiler, linker and
associated GCC tools where everything is now located. Open up your makefile, and
you should find some lines similar to the following (if your makefile is based off the
WinAVR template):
# List C source files here. (C dependencies are automatically
generated.)
SRC = $(TARGET).c
What we now need to do is add the file names of the newly created files. We'll take
our above example here again for consistency. Our new extra files are called
"ADC.c", "Speaker.c" and "USART.c", so we need to add those to the SRC line of the
makefile.
# List C source files here. (C dependencies are automatically
generated.)
SRC = $(TARGET).c ADC.c USART.c Speaker.c
Page 2
316730611.010.png 316730611.001.png
Now, that'll work, but it's a real pain for future expansion. To make our life easier,
we'll place the filenames on their own line in alphabetical order. To indicate a line
continuation in a makefile, we need to place a "\" at the end of the continued lines:
# List C source files here. (C dependencies are automatically
generated.)
SRC = $(TARGET).c \
ADC.c \
Speaker.c \
USART.c
NB: Watch the file case! To GCC and its related utilities, "MAIN.C" is not the same
as "Main.c". Make sure you put down the file names in the exact same case as they
appear inside explorer, or you'll get build errors!
Naming the routines
One problem with our multi-file setup remains; routines are still hard to find across
the files. One might easily identify the file location of our mythical
"CheckADCChanel()" routine, but what about "SleepMode()"? Obviously a naming
convention becomes important.
Now, this is where it gets tricky. There's really no set standard for function names,
and every one prefers something different. It is important to choose which one you
prefer, but it is ten times more important to remain consistent in your scheme
across your entire project.
The first word, symbol or acronym in your function name should indicate the file it is
located in. For example, all the ADC functions will have "ADC" at their start, and all
the speaker-related functions in Speaker.c would have "Speaker" at their start. You
can eliminate repetition in a name as it becomes self-explanatory what the function
is referring to due to the prefix - thus "CheckADCChanel()" would become
"ADCCheckChannel()" (or similar), rather than the superfluous
"ADCCheckADCChanel()".
I'll use our example again and put here a possible function naming scheme. Note
that the "main()" function's name remains unchanged as it is mandated by the C
standard:
USART.c:
USARTSetup()
USARTEnable()
USARTGetByte()
USARTProcessByte()
Page 3
316730611.002.png 316730611.003.png
Speaker.c:
SpeakerSetup()
SpeakerBeep()
ADC.c:
ADCCheckChanel()
ADCCheckTempSensorVal()
Main.c:
main()
MainSleepMode()
MainTurnOnStatusLed()
MainTurnOffStatusLed()
This new scheme makes finding the location of a routine quick and easy, without the
need for a multi-file search utility. Don't forget to change the function prototypes in
your header file to the match your new function names!
Making functions static
Static functions is possibly something you've never heard of up to now. The process
of declaring a function to be static indicates to the compiler that its scope is limited
to the source file in which it is contained - in essence it makes the function private,
only accessible by other function in the same C file. This is good practice from two
standpoints; one, it prevents outside code from calling module-internal functions
(and reduces the number of functions exposed by the file) and two, it gives the
compiler a better chance to optimize the function in a better way that if it was
assumed to be able to be called by any other source file.
Identify which of your functions are module-internal, and add in the static keyword.
In our example, let's say that the "USARTEnable()" function was only called by
"USARTSetup()", and never by any other source file. Assume that the USARTEnable
function is declared as the following:
void USARTSetup(void)
We'll make the function static to reduce its scope to inside USART.c:
static void USARTEnable(void)
Page 4
316730611.004.png 316730611.005.png
Note that the static keyword should be added to both the prototype in the header
file, as well as to the function in the C source file.
Global variables
Your project probably has quite a few global variables declared at the top of your
main source file. You need to cut-and-paste them each into the most appropriate
source file. Don't worry if it's referenced by more than one file; dealing with that
case will be handled later in the tutorial. For now, just put it in the C file you deem
to be the best for it.
Just in case you've done this, remember the following golden rule: header files
should declare, not define. A header file should not itself result directly in generated
code - it is there to help the compiler and yourself link together your C code, located
in C files. If you've put any globals inside your header file, move them out now.
Splitting the header file
This is where the hard part begins. So far we've successfully split up the contents of
our project into several C files, but we're still stuck with one large header file
containing all our definitions. We need to break our header file up into separate files
for each of our C modules.
First, we'll create a bunch of .h header files of the same names as our .c files. Next,
we'll move the obvious things from the master header file, the function prototypes.
Copy each of the prototypes into the respective header files.
Next, the macros. Again, move the macros into the header file where they are used
the most. If you have any generic macros that are used in the majority of your
source files, create a new "GlobalDefinitions.h" header file and place them there. Do
the same for any typedefs you may have in your header file.
Let's assume our above example has the following original header file:
#define StatusLedRed (1 << 3)
#define StatusLedGreen (1 << 2)
#define ADCTempSensChannel 5
#define BitMask(x) (1 << x)
typedef unsigned char USARTByte;
USARTByte GetUSARTByte(void);
void TurnOnStatusLed(void);
void SetupSpeaker(void);
void TurnOffStatusLed(void);
void SetupUSART(void);
int main(void);
int CheckTempSensorVal(void);
void ProcessByte(USARTByte);
void Beep(void);
void EnableUSART(void);
void SleepMode(void);
int CheckADCChanel(char);
Page 5
316730611.006.png
Zgłoś jeśli naruszono regulamin