How to Incorporate Functions into Embedded Firmware
This article explains why you should use functions in your C-language code and discusses situations in which functions are particularly helpful.
This article explains why you should use functions in your C-language code and discusses situations in which functions are particularly helpful.
Supporting Information
Like various other forms of organization, functions initially entail some additional effort and forethought. In the long run, though, we will save ourselves time and labor, not to mention stress, if we develop a habit of writing code that includes a generous use of functions.
There’s no doubt that it feels rather “easy” to take every document that I receive and place it on one of several piles scattered across my desk. Eventually, though, this easy organizational scheme will create all sorts of difficulties, even if the piles are neatly arranged and labeled with sticky notes. Likewise, when I’m thinking about a firmware project, it might seem that the most direct and painless path to a working prototype is a relatively “functionless” source file that does exactly what I need it to and nothing else. Every once in a while this approach makes sense, but in general I consider it a shortsighted solution.
The Benefits of Functions
Modularity
Code is modular if it consists primarily of blocks of code (or “modules”) that perform clearly defined tasks and that operate independently from one another. Modularity helps you to keep your code organized, repeatedly adjust implementation details, and incorporate identical functionality into various portions of your project.
In my experience with embedded firmware development, moderate (rather than obsessive) use of C functions provides just the right amount of modularity. A function makes a good module if you design it according to the “black box” mentality: You want to assume that the other portions of the code don’t know what’s happening inside the function. All they know is the task that the function performs and, if applicable, the function’s interface, i.e., the inputs and outputs.
The idea here is that if the code calls the correct function and provides a valid input, the required task will be correctly performed and the code will receive a valid output. Modularity also involves dealing with the opposite situation—if the wrong function is called, the program shouldn’t crash, and if an invalid input is supplied, the function should recognize this and handle the error appropriately.
Readability
I cannot overemphasize the importance of writing code that is easy to read and interpret. It is absolutely true that the processor could not care less about your intuitive identifiers, visually appealing capitalization, explanatory comments, indents, color coding, and so forth. But it is also true that the person writing the code is a human being, not a computer, and readable code makes firmware development faster, more accurate, more flexible, and more enjoyable.
Functions are a great way to make code more readable, because they encapsulate a potentially large number of complicated statements in one intuitive function call. This is especially important in embedded development, where C statements can be particularly incomprehensible because they are used to modify seemingly random bits in abstrusely named special function registers.
Reusability
This is related to modularity, though in this section I want to emphasize the ability to incorporate code from one project into a completely separate project.
If you tend to repeatedly use microcontrollers from the same manufacturer (a practice that I recommend if you want to increase productivity and reduce stress), you will almost certainly find that you regularly implement functionality that is almost identical to something that you used in the past.
If you consistently organize your firmware into self-contained functions, you will accumulate an abundant supply of proven code that can be transferred to new projects. Copying and pasting an entire function, especially one that was designed according to the black box model, is easier and less error-prone than copying and pasting individual lines of code that may be interspersed with statements that are not relevant to the functionality that you are trying to duplicate. Also, if the original code doesn’t clearly indicate which instructions are necessary for a given operation, you might omit an essential statement and thereby create serious malfunction or, perhaps even worse, an elusive bug.
Collaboration
This one is also closely related to modularity. If you design your firmware as a collection of self-contained modules, it will be easier for another engineer to modify or expand your code’s functionality.
Referring again to the black box mentality, we can see that code produced by someone else will be readily integrated into the rest of the project if it is written as a function that performs the required task, correctly handles inputs, and correctly generates an output.
Tips for Using Functions in Embedded C
- It’s very convenient to have portable, proven blocks of code for simple operations that might be needed in a wide variety of applications. I call these “utility” functions, and in this category I include such things as timers (either providing a fixed delay or accepting an input parameter for microseconds or milliseconds) as well as functions that can find the maximum or minimum value in a series, calculate the mean of a series, clear an array (i.e., set the value of all the elements to zero), or determine whether a value is within the range defined by a lower bound and an upper bound.
- Functions are a good place for complicated mathematical operations. Once you determine that a particular computation is implemented correctly, it’s better to improve readability by hiding all those mathematical details inside a function call.
- It’s helpful to define a function for each operational category in your firmware (usually there will be functions within these functions). This allows you to easily set up a rudimentary real-time operating system that responds to events by checking status flags and calling the corresponding function. The following diagram conveys the general idea, and a specific example is shown in the code snippet.
while(1)
{
if(RxData_FLAG)
{
RxDATA_FLAG = FALSE;
ProcessRxData();
}
if(ADCReady_FLAG)
{
ADCReady_FLAG = FALSE;
ProcessADCData();
}
if(LCDTimerDone_FLAG)
{
LCDTimerDone_FLAG = FALSE;
UpdateLCD();
}
}
Conclusion
I hope I’ve demonstrated that “functionless” code is not as quick and convenient as it might initially appear, and that a generous application of the C language’s function capabilities makes embedded firmware development not only more efficient but also more enjoyable. If you have any firmware topics that you would like to see covered in future articles, feel free to let us know 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
Absolutely great explanation of function usage in firmware development. Praise the Lord for this article. It should be shared among firmware developers.