10 tips for writing more maintainable embedded software code

Code maintenance is an important aspect of application development,one that is often ignored in favour of a faster time-to-market. Forsome applications, this may not pose a significant problem, since thelife span of those applications is short or the application is deployedand never touched again.

However, embedded systems applications may have life spans that aremeasured in decades, which means some early mistakes can result insignificant costs later.

When developing an embedded application that will likely have a longlife, maintenance must be a consideration both in design and inimplementation. The following tips by no means constitute a completelist, but they address some common issues that can give the maintainersof your application cause to curse your name ” and don't forget thatyou 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 ARMyou probably don't need it, but between these two extremes there are alot of platforms that use assembly code as a means to increaseperformance and reduce code size. However, the problem is that simplychoosing to use assembly code can derail your project and set you backmonths.

While assembly code allows you direct access to the machine'sfunctionality, the performance benefit can easily be overridden by thedifficulty in understanding just what is happening in a program. It isfor precisely this reason that higher level languages, like C and Java,were conceived.

Consider every piece of assembly code to be suspicious whendebugging, since violation of a high level language's safety featuresis extremely easy. If you must use assembly, try to be wordy whencommenting. In C or Java, comments can clutter the code, but inassembly the comments can save a lot of time and frustration.

You may choose to comment blocks of assembly, but make sure therearen't more than 5 or 6 instructions in a block. Ideally, thealgorithms used should be spelled out in pseudo-code directly in thecomments (see Tip #8 ).

Tip #2: Avoid comment creep
┬áThis is a general programming tip, but one that becomesespecially important in long-lifetime applications ” manage yourcomments' association with the code they document. It can be very easyfor comments to migrate as code is updated, and the result can bedifficult to understand. The following example shows just how easilycomment creep can happen over time:

// Thisfunction adds two numbers and returns the result #if __DEBUG voidprintNumber(int num) { printf(“Output:%dn”, num);
} #endif
// This functionmultiplies two numbers and returns the result int multiply(inta, int b) {
return a*b; }
int add(int a, intb) {
#if __DEBUG // Debugging output printNumber(a+b); #endif return a+b; }

Notice that the comment for the function 'add' is at the top of thelisting while the actual function is further down. This can happen overtime if there is a space between the comment and the function.

The likely cause was the addition of the printNumber functionbetween 'add' and its comment description. Later, someone saw thatthere was an addition function and it seemed logical to put multiplynext to it ” the comment creep has resulted in disjointed documentationwithin the code. To fight this problem, try keeping code inside thefunction it documents, or make the comment block very obvious byputting 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 shouldstart out as simple as it can possibly be and still provide the desiredfunctionality ” if performance is a requirement, try to implement theprogram simply, even if it does not match the performance.

Once you have tested and debugged your complete unit (be it a programor a component of a larger system), then go back and optimise.Haphazardly optimising code can lead to a maintenance nightmare sinceoptimised code is usually harder to understand, and you might not getthe performance results you need. Ideally, use a profiler (such asgprof, which works with GCC, or Intel's VTune) to see where yourbottlenecks are and focus on those areas ” what really is slow maysurprise you.

Tip #4: ISRs should be simple
Interrupt Service Routines (ISRs) should be as simple as possible, bothfor performance and maintenance reasons. ISRs, being asynchronous innature, are inherently more difficult to debug than “regular” programcode, so keeping their responsibility to a minimum is important for theoverall maintainability of your application. Try to move any dataprocessing out of your ISR and into your main program ” the ISR canthen be responsible only for grabbing the data (from hardware, forexample) and placing it in a buffer for later use. A simple flag can beused to signal the main program that there is data to be processed.

Tip #5: Leave debugging code in thesource files
During development, you will likely add a great deal of codethat is designed for debugging ” verbose output, assertions, LEDblinking, etc. When a project is over, it may be tempting to removethose sections of code to clean up the overall application, especiallyif the debugging code was haphazardly added.

While cleaning up the application is a noble pursuit, taking out thedebugging code creates problems later. Anyone attempting to maintainthat code will likely reproduce many of the steps created in theoriginal development ” if the code is already there it makesmaintenance a whole lot easier. If the code needs to be removed inproduction builds, use conditional compilation or put the debuggingcode in a central module or library and don't link it into productionbuilds. Initial development of the application should include time todocument and clean up debugging code; the extra time spent will be wellworth it.

Tip #6: Write wrappers for system calls
Try to separate low-level I/O routines from the higher-levelprogram logic through interfaces, because a program can be made verydifficult to manage by developing monolithically. Putting all of thefunctionality of an application into a few large functions makes codedifficult to understand and much harder to update and debug. This isespecially true with hardware interfaces. You may have direct access tohardware registers or I/O, or even an API provided by the platform'svendor, 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 ifyou have to change platforms in the future, having hardware-specificcode in your application (API or direct manipulation, it doesn't matterwhich) will make it much more difficult to port.

If you create your own wrapper interface, which can be as simple ascreating macros that are defined to the hardware API, your code can beconsistent and all the updates needed for porting will be in acentralised location.

Tip #7: Break up functionality only asneeded
Embedded applications will differ from PC applications in that a lot ofthe functionality will be specialised to the hardware you are workingwith. Splitting up functional units into the smallest pieces possibleis not advisable – keep the number of function calls in a single scope(function) to less than 5 or 6, and make functional units of thehardware correspond to functional units in the software.

Breaking up the program any further will create a spider web of acall graph, making debugging and comprehension difficult.

Tip #8: Documentation
Keep all documentation with the code and, ideally, a copy of thehardware too. When documenting your application, try to put as much ofthe design and application model directly into the source code. If youhave to keep it separate, put it in a source file as a giant commentand link it into the program.

At the very least, if you use a version control system (such as CVSor Microsoft Source Safe), check the documentation into the samedirectory as your source ” it is really easy to lose the documentationif it is not located with the source.

Ideally, put all the documentation and source on a CD (or yourchoice of portable storage device) seal it in a bag with the hardwareand 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 bigtrouble. Since C and C++ are still dominant languages in the embeddedworld, there is a huge number of ways to solve a single problem.Templates, inheritance, goto, the ternary operator (the “?”), the listgoes on and on.

Really clever programmers can come up with extremely compact andelegant ways to use these tools to solve problems. The problem is thatusually only the programmer understands the clever solution (and willlikely forget how it worked later).

The only solution is to avoidcleverness and keep the use of esoteric language features to a minimum” for example, don't rely on short-circuit evaluation in C statementsor use the ternary operator for program control (use an if-statementinstead).

Tip #10: Put all definitions in oneplace
This one is easy; if you have a lot of constant definitions orconditional defines, keep them in a central location. This could be asingle file or a source code directory, but if you bury a definitiondeep 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 theauthor of “Practical Embedded Security.”

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.