According to Dominion Electronics , code maintenance is an important aspect of application development that is often ignored in favour of quick-to-market designs. For some applications, this may not pose a significant problem since the lifespan of those applications is short or the application is deployed and never touched again. However, embedded systems applications may have life-spans that measure in decades and some early mistakes can result in significant cost later.
When developing an embedded application that will likely have a long life, maintenance must be a consideration both in design and in implementation. Following are some of the common issues in embedded systems code maintenance:
1) Avoid assembly code:
There are a lot of platforms that use assembly code as a means to increase performance and reduce code size. However, simply choosing to use assembly code can derail the project.
Assembly code allows the user direct access to the machine’s functionality, but the performance benefit can easily be overridden by the difficulty in understanding what is happening in a programme. It is for this reason that higher-level languages like C and Java were invented. One should consider every piece of assembly code to be suspicious when debugging, since violating the safety features of the higher-level languages is easy.
In C or Java, comments can clutter the code, but in assembly the comments can save time. Users may choose to comment blocks of assembly, but they need to make sure that there are not more than five or six instructions in a block. Ideally, the algorithms used should be spelled out in pseudo-code directly in the comments.
2) Avoid comment creep:
This is a general programming tip, but one that becomes especially important in long-lifetime applications – manage the comments’ association with the code they document. It can be easy for comments to migrate as code is updated, and the result can be difficult to understand.
3) Do not optimise prematurely:
One of the errors done in programming is premature optimisation. However, the rule is often broken in practice due to time constraints, sloppy coding, or overzealous engineers. Any programme should start out as simple as it can be and still provide the desired functionality – if performance is a requirement, implement the programme simply, even if it does not match the performance. Once users have tested and debugged the complete unit (be it a programme or a component of a larger system), they need to go back and optimise. Haphazardly optimising code can lead to a maintenance nightmare since optimised code is harder to understand, and users may not get the performance results they need. Use a profiler (such as gprof, which works with GCC, or Intel’s VTune) to see where the bottlenecks are and focus on those areas.
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 difficult to debug than ‘regular’ programme code. Users should try to move any data processing out of their ISR and into their main programme – 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 programme that there is data to be processed.
5) Leave debugging code in the source files:
During development, the user will likely add a code that is designed for debugging – verbose output, assertions and LED blinking. 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, 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 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 do not link it into production builds. Initial development of the application should include time to document and clean up debugging code.
6) Separate low-level I/O routines from the higher-level programme logic through interfaces - Write wrappers for system calls:
A programme can be made 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 harder to update and debug. This is especially true with hardware interfaces. One 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 an own ’wrapper’ interface. Users usually do not have control over what the hardware does, and if they have to change platforms in the future, having hardware-specific code (API or direct manipulation) in their application will make it more difficult to port. If users create their own wrapper interface, which can be as simple as creating macros that are defined to the hardware API, their code can be consistent and all the updates needed for porting will be in a centralised location.
7) Break up functionality as much as you need to - not more:
Embedded applications will differ from PC applications, in that a lot of the functionality will be specialised to the hardware the user is 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 programme further will create a spider web of a call graph, making debugging and comprehension difficult.
8) Keep all documentation with the code and a copy of the hardware too:
When documenting application, users should try to put as much of the design and application model directly into the source code. If users have to keep it separate, then they need put it in a source file as a giant comment and link it into the programme. If users use a version control system (such as CVS or Microsoft Source Safe), then they need to check the documentation into the same directory as their source; it is easy to lose the documentation if it is not located with the source. Ideally, users should put all the documentation and source on a CD (or their choice of portable storage device), seal it in a bag with the hardware and development tools they are using, and put that bag in a safe place.
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 are a huge number of ways to solve a single problem. Templates, inheritance, goto, the ternary operator (the “?”), the list goes on.
Clever programmers can come up with 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 keep the use of esoteric language features to a minimum – for example, do not rely on short-circuit evaluation in C statements or use the ternary operator for programme control (use an if-statement instead).