Design Article
10 tips for writing more maintainable embedded software code
Timothy Stapko
12/3/2008 12:15 AM EST
However, embedded systems applications may have life spans that are measured in decades, which means some early mistakes can result in significant costs later.
When developing an embedded application that will likely have a long life, maintenance must be a consideration both in design and in implementation. The following tips by no means constitute a complete list, but they address some common issues that can give the maintainers of your application cause to curse your name " and don't forget that you may be one of them!
Tip #1: Avoid assembly code
Of course, on a low end PIC you have no choice and on a high end ARM
you probably don't need it, but between these two extremes there are a
lot of platforms that use assembly code as a means to increase
performance and reduce code size. However, the problem is that simply
choosing to use assembly code can derail your project and set you back
months.
While assembly code allows you direct access to the machine's functionality, the performance benefit can easily be overridden by the difficulty in understanding just what is happening in a program. It is for precisely this reason that higher level languages, like C and Java, were conceived.
Consider every piece of assembly code to be suspicious when debugging, since violation of a high level language's safety features is extremely easy. If you must use assembly, try to be wordy when commenting. In C or Java, comments can clutter the code, but in assembly the comments can save a lot of time and frustration.
You may choose to comment blocks of assembly, but make sure there aren't more than 5 or 6 instructions in a block. Ideally, the algorithms used should be spelled out in pseudo-code directly in the comments (see Tip #8).
Tip #2: Avoid comment creep
This is a general programming tip, but one that becomes
especially important in long-lifetime applications " manage your
comments' association with the code they document. It can be very easy
for comments to migrate as code is updated, and the result can be
difficult to understand. The following example shows just how easily
comment creep can happen over time:
// This
function adds two numbers and returns the result
#if __DEBUG
void
printNumber(int num) {
printf("Output:
%d\n", num);
}
#endif
// This function
multiplies two numbers and returns the result
int multiply(int
a, int b) {
return a*b;
}
int add(int a, int
b) {
#if __DEBUG
// Debugging output
printNumber(a+b);
#endif
return a+b;
}
Notice that the comment for the function 'add' is at the top of the
listing while the actual function is further down. This can happen over
time if there is a space between the comment and the function.
The likely cause was the addition of the printNumber function between 'add' and its comment description. Later, someone saw that there was an addition function and it seemed logical to put multiply next to it " the comment creep has resulted in disjointed documentation within the code. To fight this problem, try keeping code inside the function it documents, or make the comment block very obvious by putting lines above and below the comments.
Tip #3: Don't optimize prematurely.One of the cardinal sins in programming is premature optimisation. However, the rule is often broken in practice due to time constraints, sloppy coding, or overzealous engineers. Any program you write should start out as simple as it can possibly be and still provide the desired functionality " if performance is a requirement, try to implement the program simply, even if it does not match the performance.
Once you have tested and debugged your complete unit (be it a program or a component of a larger system), then go back and optimise. Haphazardly optimising code can lead to a maintenance nightmare since optimised code is usually harder to understand, and you might not get the performance results you need. Ideally, use a profiler (such as gprof, which works with GCC, or Intel's VTune) to see where your bottlenecks are and focus on those areas " what really is slow may surprise you.
Tip #4: ISRs should be simple
Interrupt Service Routines (ISRs) should be as simple as possible, both
for performance and maintenance reasons. ISRs, being asynchronous in
nature, are inherently more difficult to debug than "regular" program
code, so keeping their responsibility to a minimum is important for the
overall maintainability of your application. Try to move any data
processing out of your ISR and into your main program " the ISR can
then be responsible only for grabbing the data (from hardware, for
example) and placing it in a buffer for later use. A simple flag can be
used to signal the main program that there is data to be processed.
During development, you will likely add a great deal of code that is designed for debugging " verbose output, assertions, LED blinking, etc. When a project is over, it may be tempting to remove those sections of code to clean up the overall application, especially if the debugging code was haphazardly added.
While cleaning up the application is a noble pursuit, taking out the debugging code creates problems later. Anyone attempting to maintain that code will likely reproduce many of the steps created in the original development " if the code is already there it makes maintenance a whole lot easier. If the code needs to be removed in production builds, use conditional compilation or put the debugging code in a central module or library and don't link it into production builds. Initial development of the application should include time to document and clean up debugging code; the extra time spent will be well worth it.
Tip #6: Write wrappers for system callsTry to separate low-level I/O routines from the higher-level program logic through interfaces, because a program can be made very difficult to manage by developing monolithically. Putting all of the functionality of an application into a few large functions makes code difficult to understand and much harder to update and debug. This is especially true with hardware interfaces. You may have direct access to hardware registers or I/O, or even an API provided by the platform's vendor, but there is a lot of motivation to create your own 'wrapper' interface.
You usually do not have control over what the hardware does and if you have to change platforms in the future, having hardware-specific code in your application (API or direct manipulation, it doesn't matter which) will make it much more difficult to port.
If you create your own wrapper interface, which can be as simple as creating macros that are defined to the hardware API, your code can be consistent and all the updates needed for porting will be in a centralised location.
Tip #7: Break up functionality only as neededEmbedded applications will differ from PC applications in that a lot of the functionality will be specialised to the hardware you are working with. Splitting up functional units into the smallest pieces possible is not advisable - keep the number of function calls in a single scope (function) to less than 5 or 6, and make functional units of the hardware correspond to functional units in the software.
Breaking up the program any further will create a spider web of a
call graph, making debugging and comprehension difficult.
Keep all documentation with the code and, ideally, a copy of the hardware too. When documenting your application, try to put as much of the design and application model directly into the source code. If you have to keep it separate, put it in a source file as a giant comment and link it into the program.
At the very least, if you use a version control system (such as CVS
or Microsoft Source Safe), check the documentation into the same
directory as your source " it is really easy to lose the documentation
if it is not located with the source.
Ideally, put all the documentation and source on a CD (or your choice of portable storage device) seal it in a bag with the hardware and development tools you are using and put that bag in a safe place " your successors will thank you.
Tip #9: Don't be clever!
Similar to premature optimisation, clever coding can lead to big
trouble. Since C and C++ are still dominant languages in the embedded
world, there is a huge number of ways to solve a single problem.
Templates, inheritance, goto, the ternary operator (the "?"), the list
goes on and on.
Really clever programmers can come up with extremely compact and
elegant ways to use these tools to solve problems. The problem is that
usually only the programmer understands the clever solution (and will
likely forget how it worked later).
The only solution is to avoid cleverness and keep the use of esoteric language features to a minimum " for example, don't rely on short-circuit evaluation in C statements or use the ternary operator for program control (use an if-statement instead).
Tip #10: Put all definitions in one placeThis one is easy; if you have a lot of constant definitions or conditional defines, keep them in a central location. This could be a single file or a source code directory, but if you bury a definition deep within your implementation, it will come back to bite you.
Timothy Stapko is lead software engineer for Digi International with focus on the Rabbit line of embedded products. Stapko has more than 8 years software industry experience and is the author of "Practical Embedded Security."




vocaro
12/3/2008 3:02 PM EST
This is good advice except for #5, which I totally disagree with. Leaving ad hoc debugging code mixed in with the source file leads to clutter and "debug creep". Just as in tips #2 and #8, where comments and documentation can get out of hand, so too can debug statements. It is fine to put them in during development, but once most of the bugs are fixed, the debugging code should be cleaned out and replaced with unit tests that are kept separate from the source file.
Sign in to Reply
Double Double
12/4/2008 8:17 AM EST
I also agree with vocaro, but not to his degree. I think important and often used debug code should be left in the source but it must be managed the same way as comments are in item 2.
Sign in to Reply
deyyoung
12/4/2008 9:01 AM EST
Debug code is critical for long term projects, as well as projects that evolve into new products over the years. You MUST leave in good debug code! Manufacturing test, software QA, service, etc. all need good debug options. Removing debug code (unless of course it is bad debug code) is a huge no-no! If the code is not usable/relevant in an executable state, then compile it out, but do not remove it!
Sign in to Reply
Crous
12/4/2008 9:59 AM EST
At Micrium, we also look at debug code the same way. At some point, it has to be removed to make the source code a commercial product.
We developped a solution to this. Don't instrument your code at all, but still do run time debugging the same way. It is called µC/Probe.
Please take a look at it. It is a very neat tool that brings source code debugging to the 21st century! (http://www.micrium.com/products/probe/probe.html)
Sign in to Reply
SW Craftsman
12/4/2008 3:24 PM EST
About #3. Not all optimizations complicate code. The best kind of optimization simplifies it. That kind is good at any stage of development, especially where it affects something used pervasively like an API or data structure. The more these get used, the more expensive it becomes to simplify things later. It may (or may not) take longer to design and implement something simpler, such as when adding pieces and seeing a common pattern that simplifies the whole; there's a balance on when/where it is optimal to do it in that case.
Sign in to Reply
mkhs
12/4/2008 8:37 PM EST
About #3, developer need to develop a mindset of write code keeping in mind the optimization during design itself (even before starting coding). Also while coding always optimization should be running back in mind. This shall save lots of time (it can take months to optimize code which was written in very relaxed way) in the world where time to market is on high on pyramid.
Sign in to Reply
yabaud
12/8/2008 3:31 AM EST
#10: taken too literally this can be misleading. Our system consists in several embedded subsystems. Our habit of putting all the system constants definitions into one single file ended up being an outstanding issue, because it created far more dependencies across the projects than really needed. In fact, the scope of the definitions should be carefully analyzed.
Sign in to Reply
Larry Martin
12/8/2008 11:13 AM EST
Great article. Lots of not-so-commonsense pointers and plenty of fuel for debate.
One thing I appreciate is the lack of block-format function header comments. So many people think those are the key to good code maintenance, but I have never seen a maintenance-phase project where even half of the block comments were accurate. I once had a guy working for me on a "new construction" project who did maintain his block comments, even lining up the asterisks on the right of the block, but wrote code that stressed the processor severely with nested loops and wasted space with extra scratch buffers. Given a choice between good code and good comments, you have to take the code.
I also think #8 is especially well taken. At various companies, I've had "old" source archives deleted to save server-side disk space. The thing is, you don't know it's happened until it's way too late to recover. At my last software manager job, I used to back up the SCCS database to CD, just in case. I did that for three years and needed the data once. It was worth it.
Sign in to Reply
freezykid
12/11/2008 12:38 AM EST
Great article. I will recommend all my colleagues to go through this article for sure.
Sign in to Reply
sapnasudhi
12/29/2008 6:29 AM EST
Good advice
Sign in to Reply
Parashuram
12/29/2008 8:09 AM EST
It is a good insight for keeping good source code. Few more points can be added.
1. Always keep count of memory being used like stack or heap. This can be done by adding few advanced macros to print such status on need basis.
2. Don't mix Non-Paged and Page code. Maintain good way to differentiate them without really digging your MAP file to findout segment definitions. This would help in the long-run to allocate NAND and RAM memory.
Sign in to Reply