Tip #1—Follow the rules you’ve read a hundred times
There are three things you must do each time you start writing your code. You’ve read these rules many times before and resolved to do them the next time you started code development. This time, do them—they will help you to avoid many long hours of debugging:
- Initialize variables before use.
- Do not ignore compiler warnings.
- Check return values.
Accessing objects before they have a defined state can lead to strange effects. Not only does avoiding these effects require that you make sure you’ve set all your ints and floats to a defined state, you also have to make sure that your complex type functions, such as typedefed structs, are initialized first.
Tip #2—Use enums as error types
Every module should have a specific error return type that explains what the problem is in detail, at the time it occurs. Often you receive error codes like “-1″ or “an error occurred.” If there is a run-time error detected and you know exactly what it is, document this for your later reference and for those who maintain the software.
Tip #3—Expect to fail
Failures happen. Often. So plan for it and use it to your advantage. It’s good practice to set the default return value of an operation to something like UNNOWN_ERROR. Only in the case of a good result should you set it to SUCCESS
Tip #4—Check input values: never trust a stranger
If your modules expect input data from other modules, you should never trust a stranger. That is, at the outmost layer of your software architecture, check all input values for consistency. The check has to be at the outmost layer since it must be detected as soon as possible. Otherwise you could, for instance, dereference an invalid pointer given to you at one of your lower layers. The result: The crash dump reports that it was your software’s problem, but later, after many hours of debugging, you find out that someone has given you invalid input.
Tip #5—Write once, read many times
When we read other people’s code, we’re thankful for any good line of comment or more readable code; most of the time, however, the original coder hasn’t been so kind to us. If the variables are called i, j, and k, you’ll soon have a mental break down. Often the longest variable name is pbuf. What can happen when code is difficult to decipher is that even though the next programmer should only slightly change the software, he or she says, “I can’t understand this hacker’s code. It will be faster to rewrite it.” The rewrite results in extra work and possibly new bugs.
So what can you do? First of all, if you write code, write it to be as readable as a newspaper. Well-written code requires only a few lines of comments. Also consider that although code is nothing for compilers, it needs to be readable by human beings.
Don’t be lazy at typing new variable names and, if required, add the unit to the name. For example, do not call parameters Size, Length, Temperature, or Angle. Instead, since all those parameters have a unit, call them:
Tip #6—When in doubt, leave it out
If you design an API that is nothing other than the external interface of your modules, consider the need of an operation. If you are not sure anyone will ever need an operation, leave it out. If someone does use your API and you later remove an operation, you’ll break his code.
Tip #7—Use the right tools
Everyone has a favorite editor, debugger, and compiler. But sometimes it’s worth looking for something new since “the better is the enemy of the good.” Here is what I use (many of which you may already use):
- Eclipse: Has a good editor, is good at refactoring code, also good for prototyping architectures on the PC (for example, with Cygwin on Windows). www.eclipse.org.
- Astyle: Artistic Style 2.01 is a great code formatter that can be configured in many ways to beautify the code. http://astyle.sourceforge.net/.
- Cygwin: For PC-based prototypes and for architectural studies, you can use the GNU tool chain of cygwin. Make sure you install the make, binutils, and gcc from the development package. www.cygwin.com/.
- GNU tool suite: Many embedded systems tool chains use this set of tools. Even if you don’t have hardware at the beginning of the project (your hardware developers may not have finished their work), you can start writing prototypes for your architecture. Eclipse together with Cygwin using the GNU tools is worth trying. www.gnu.org.
- Tortoise SVN: This is a nice add-on for Windows Explorer to access the subversion versioning system. http://tortoisesvn.tigris.org/.
Tip #8—Define the software requirements first
Defining the requirements for the software you write is the first step for a successful product. I mean the software requirements for the final product, not those for the quick hacked throwaway prototype you’re working on as a first step to the final product. And this, of course, requires defining the goal to be reached.
If you don’t define the requirements, you can’t test your final software properly—you’ll have nothing to use to define a useful test case. In other words, how can you determine if you’ve finished the development?
Tip #9—During boot phase, dump all available versions
If you’re the one who implements the boot loader on new hardware, you would normally do the following:
- Initialize the hardware according to the required memory map.
- Execute a hardware self test.
- Start booting the application.
Nothing new here: That is what your PC typically does every time you boot up.
But in embedded systems development in the era of FPGAs and CPLDs, the hardware is as modifiable and subject to change as the software. Dump all programmable logic devices version registers onto a console window or file before starting the application. This step is important since hardware developers nowadays use programmable devices to quickly change the behavior of their hardware, with the result that VHDL code can be changed as fast as software code can be changed.
Tip #10—Use a software version string for every release
If you’ve finished development of a particular stage in a project in order to do tests on it or to release a software version, be sure you take the following steps in exactly the order written:
- Update the version string and date.
- Check the software version in to your versioning system.
- Update the version string right after check-in for the next version.
- Test the software.
- Fix the bugs.
- Continue developing the next version.
The most important step is 3.
Since the C99 language standard has defined the stdbool.h and stdint.h headers, things have become portable and there is absolutely no need to define your own int or boolean types.
Tip #12—Expose only what is needed
When I read other programmers’ code, I wonder if they’ve ever heard about “information hiding.” I find many externally declared variables that can be accessed from several modules. The practice is both pointless and sometimes dangerous.
Module internal operations and variables are often not declared static, which allows them to be accessible from other modules. This accessibility results in a design that is not modular because when operations and variables are not declared static, they’re interdependent and not modular (since one thing cannot live without the other).
Also C doesn’t have any syntax for anything like namespaces, common in other object-oriented languages. Or to be more precise, C knows only one, the global namespace. This means that all nonstatic operations or variables are visible globally unless you hide them. This global visibility could result—and often does—in a name clash detected at linking the software. As long as you have all the source code for the project, you can easily resolve this issue. If you have only a library in binary format and some function headers, the situation is more complicated.
Another related topic: Parameters have to be declared as const if the implementer of the interface doesn’t want this object to be changed. The difference between C and C++ is that in C++ const means constant, whereas C defines constant to be interpreted as read-only.
Tip #13—Make sure you’ve used “volatile” correctly
In embedded software development you sometimes have to do things that your host-based colleagues are often not concerned about. One of those things is declaring variables to be volatile, which keeps the compiler from optimizing read or write operations for this variable.
Tip #14—Don’t start with optimization as the goal
Some developers are intent on writing “fast code,” even though they cannot define what “fast” means in the context of their application. It sounds good as an objective, but what I’ve seen is that often under the cover of writing fast code, they want to move beyond the existing system definition and move to a nonexisting architecture.
To account for this and other system-redefining goals, you should think seriously about developing a flexible architecture capable of adapting to various exigencies. First, this means developing a set of software requirements for the product being planned. Then you should assume that once developed, your software architecture will no doubt be extended, so consider how you can design it to be flexible enough to incorporate new features into the existing platform without scrapping the code base you’ve already developed.
If you’re concerned about performance, wait until your project’s first integration phase, at which point you can determine how fast the system is. This doesn’t have to be the last milestone in the project. As proof of concept, you can plan to produce several interim “proof of concept” implementations and measure performance. Working from that known value, consider what you need to do to achieve the necessary performance goals.
If you then detect that your code is not fast enough, you have to check which parts are responsible for the main time consumption. Then you profile the software and determine what loops and routines are consuming all the time.
The rule with optimization is that you first have to know where you are before considering what you need to optimize in order to get where you want to be. And don’t forget that the software must still be maintainable.
Tip #15—Don’t write complex code
Complex code is error-prone code. I think most of you already know this. But the question is, what does complex really mean in the context of your particular design? You shouldn’t just think about writing code during the initial software development phase. You need to think about all the stages of the code’s life. The code of a product will be changed and extended many times during the product’s life cycle. And the people who have to do this are pretty often not the ones who have written the code initially. In other words, do not just think about your own needs: many others are coming after you.
Tip #16—Use a static code checker
If you write safety-critical code, you surely have a coding guideline. Even if your guideline contains only 10 rules, you must have a tool to help you check those rules. If your team doesn’t have a tool for checking, you can be sure that things won’t be checked.
Many tools, such as PC-Lint, are available to accomplish this task. You should check your code at every significant milestone in your project to be sure that the code quality is good.
Remember, software testing is a multistage process, of which static code checking is one part. The other stages include:
• Functional tests.
• Requirements-based tests.
• Coverage tests (such as MC/DC).
All these tests have one general purpose: to reduce the number of bugs in your code.
Tip #17—Myths and sagas
Many myths and sagas persist in the world of safety-critical embedded systems. One of the most common is that dynamic memory allocation is forbidden. This myth, however, is only half of the truth. Every application has an initialization phase. This phase is followed by the operational one. It’s no risk at all to do dynamic memory allocation during the initialization phase. But to avoid memory fragmentation, during the operational phase, you aren’t allowed to change those allocations.
Author – Thomas Honold [EE Times Design]