A basic C function—e.g., one or two arguments and a return value—is not a complicated thing. However, C functions are quite flexible, and by going beyond the basics you can write code more easily and introduce some beneficial characteristics into your firmware.
1. Put Your Function Prototypes in a Header File
Sometimes I wonder if a person has to be a computer science major to fully understand C-language function prototypes. It really is a rather complicated issue. I think that firmware engineers can safely ignore the details and simply adopt a standard course of action such as the following: always include a prototype for your functions, and place these prototypes in a header file.
A function prototype is one line of code that gives the compiler the data types for a function’s arguments and return value. For example:
float ArithmeticMean(char Value1, char Value2, char Value3);
It actually is not necessary to include the name of the input parameter, because all the compiler wants to know at this point is the data type. Thus, you could also write the prototype as follows:
float ArithmeticMean(char, char, char);
I prefer to include the parameter names, probably because it allows me to create the function prototype by copying and pasting from the function definition.
Strange things can happen when function prototypes are omitted or misplaced, and in my experience the easiest way to eliminate these concerns is to place all of your function prototypes in a header file that is included in all of your project’s source files. This ensures that you can safely use any of your functions in any of your source files, because each function will always be declared (i.e., in the header file) before it is invoked.
For example, the following code snippet is taken from a file called “Project_DefsVarsFuncs.h”.
//function prototypes void Delay_us(unsigned int DelayCount); void Delay_10ms(unsigned int DelayCount); void Delay_seconds(unsigned int DelayCount); void Update_LCD(unsigned char SensorNumber, unsigned long DisplayValue); void LCD_Clear_All();
This header file is included in my source files as follows:
//----------------------------------------------------------------------------- // Includes //----------------------------------------------------------------------------- #include "Project_DefsVarsFuncs.h"
2. Declare Functions as Static to Avoid Naming Conflicts
As a firmware project becomes larger and more complicated, you might find that you want to reuse the same function name in different source files. You can accomplish this by using the “static” keyword. This is a situation in which you would not place the function prototype in a header file, as described above.
The “static” keyword restricts the scope of the function to one source file. This allows you to use the same function name for a different function in a different source file. In the example shown below, the project needs three different ProcessData() functions—one for analyzing ADC data, one for responding to UART commands, and one for handling I2C messages.
I would place the “static” keyword in front of the function prototype, and then the function prototype goes toward the top of the source file (i.e., before the “normal” code consisting of processor instructions inside function bodies).
The “static” keyword is also handy when multiple engineers will be working on the same firmware project. If two engineers are working in different source files, the use of static functions allows Engineer A to choose function names without worrying about the possibility that Engineer B will choose the same name for a different function.
3. Use a Pointer to Pass an Array to a Function
Embedded applications make frequent use of arrays—sequences of sensor readings, ADC values, short ASCII messages, and so forth. It might seem that functions are a bit awkward in this sort of development context, because you cannot pass an array to a C function. Well, it’s true that you cannot pass an array in the way that you pass an individual variable, but you can give a function access to an array’s data by using pointers.
If you include a pointer as one of the arguments and then pass the array identifier when you call the function, the statements in the function body can use that pointer to read and modify the contents of the array. If you find this concept a bit confusing, I recommend that you first read my article on arrays in C and then the first article on pointers.
4. You Can Call Functions Using a Pointer
A pointer is a variable that holds a memory address. This address often identifies the location of a variable or of the zeroth element of an array. However, pointers can also point to functions.
If you’ve read my first article on C functions, you know that functions are stored at specific locations in code memory just as variables are stored at specific locations in data memory. If the value stored in a pointer is the starting address of a function, you can call that function using the pointer. If you change the pointer’s value such that it equals the starting address of a different function, you can call that different function using the same pointer.
The following code snippet shows you how to declare a function pointer that can be used with functions that have three char arguments and a return type of float.
float (*Ptr_to_Function)(char, char, char);
An array identifier is essentially a pointer to the zeroth element of the array. Likewise, the name of a function is interpreted as the starting address (in code memory) of the function. Thus, you can assign a function to a function pointer as follows:
Ptr_to_Function = ArithmeticMean;
You can now invoke the function ArithmeticMean() using Ptr_to_Function. The following two statements are equivalent:
Average = ArithmeticMean(Temperature1, Temperature2, Temperature3); Average = (*Ptr_to_Function)(Temperature1, Temperature2, Temperature3);
5. Use Local Variables When Possible
I think that in many cases our first instinct is to define all variables at the top of the source file and leave it at that. However, it’s good to get in the habit of using local variables in your functions. The scope of a local variable is limited to the function in which it is defined; in other words, the variable is created when the function is called and disappears when the function has completed its task.
I recommend local variables for two reasons. First, they allow you to reuse the same variable name in different functions. A complicated project may include numerous functions that need basic variables such as a loop counter. I like to repeatedly use “n” for my loop counters instead of gradually cycling through all the letters in the alphabet.
Second, use of local variables might result in faster code. I’m not an expert on compilers, but I think that a local variable is likely to be placed in a register rather than a memory location, and access to registers is faster than access to memory.
Registers are a more integral part of the processor and allow for rapid reading and writing of data.
We’ve covered quite a few details here related to the use of functions in the C language, and I hope that this information will help you to make your firmware more efficient and functional (no pun intended). If you have any function-related tips, feel free to share them in the comments section below.
C Language Series
- Introduction to the C Programming Language for Embedded Applications
- Understanding Variables in C Programming
- Understanding Arrays in C Programming
- Pointers in C Programming: What Is a Pointer and What Does It Do?
- What Are Functions in C Programming?
- How to Incorporate Functions into Embedded Firmware
- How to Use Pointers in C-Language Firmware
- Five Tips for Using Functions in C-Language Firmware