In some ways, writing embedded software is much like writing software for any other kind of application. But in other ways it differs widely. This means that embedded software developers can learn lessons from their desktop-bound counterparts, but they must harvest that wisdom selectively.
Embedded software technology appears to lag behind – it's often a little late in picking up on the latest trends and fashions. This is largely because embedded developers have a cautious, conservative attitude, which is borne out of years of experience contending with limited resources: memory that cannot be treated as unlimited and CPUs that are just powerful enough to get the job done.
The choice of programming language echoes this pragmatic conservatism. Embedded software engineers were slow to pick up on high-level languages years ago, but eventually C became accepted.
It took the arrival of very high quality code generation and flexible, transparent debugging technology for the industry to accept such innovative ideas, and that was only under the relentless pressure to become more productive.
The obvious step forward from C is C++. Even though its use for embedded software has steadily increased in recent years, it is far from universal. Why is that?
It is not stubborn resistance to change. It's simply continued worries about resource utilization. Memory may be bigger, but embedded software developers cannot just add an extra gigabyte when needed. Processors are much faster, but cost and power limitations mean they are way behind the devices found in even the more modest PCs.
Five good ideas for using C++ in embedded apps
Is this concern about the suitability of C++ for embedded software really justified? Not really. Like any tool, the language needs to be handled correctly to yield good results. The cautious (embedded) programmer needs guidelines.
Good Idea #1: Understand how C++ actually works. To get the best from any programming language, you need to fully understand what the language constructs mean and what kind of code the compiler might generate.
Look at the code produced and try to understand why it's done the way it is. Pay attention to details.
For example, if you think the two statements i++; and ++i; are always identical in C++, think again. (They yield the same result, but the postfix operator probably requires intermediate storage, so the prefix version may be more efficient.)
Good Idea #2: Start by applying C++ incrementally. If you're using C, it may be a big step to change to C++. However, since C++ is not an object-oriented language (it's a procedural language with some object oriented features ” and is basically a super-set of C) you can start taking advantage of the extra functionality one bit at a time.
As you learn new features and see how they can improve your code, you can start to apply them.
Good Idea #3: Apply language features in appropriate ways. Like C, C++ was not designed specifically for embedded applications, so it is not 100% suited to the task. Some features of the language carry absolutely no cost in resources like overloaded functions, for example, which are just a compiler/linker symbol issue; these may be used freely as required.
Other features can cause significant overhead and should be used with care or avoided all together. A good example is the exception handling system (EHS). This facility enables the construction of very robust code, with very little effort required by the programmer to accommodate a wide range of exceptional events smoothly.
The downside is that, behind the scenes, a lot of code has been silently generated to accommodate this functionality. Although avoiding the EHS entirely may not be necessary, using it in a simple way minimizes unwelcome surprises.
It's worth noting that the EHS overhead code may be generated, even if EHS is not being deployed; EHS support is switched on, by default, in many compilers.
Good Idea #4: Encapsulate expertise in objects. An embedded software team has a diverse set of skills. Different members of the team have different areas of expertise. Endeavor to encapsulate that expertise into classes so that it can be utilized safely by other members of the team.
For example, a hardware device may have some very precise requirements associated with its access; representing the device by an object means that not every user needs to understand its eccentricities. A similar approach can be used to hide the specifics of a real-time operating system.
Good Idea #5: Use tools that are optimized for embedded. It's a known fact that embedded software development is different from desktop programming. This is widely recognized and a great many companies offer tools for embedded programming, but tread carefully here. Some tools are much more oriented towards the needs of embedded developers than others; some vendors simply adapt desktop development tools.
Five things to avoid
#1: Writing “clever” code is never smart. C++ gives you the opportunity to write very clear, concise code. It also provides the means to write very convoluted, arcane code which does nothing except illustrate how clever you think you are. Do not give in to this temptation.
For example, in C++ you can define your own operators. Always ensure that their functionality will not surprise a reader of the code. For example, you could define a new class and include a definition of the + operator, such that an expression may be written like this:
a + b;
The above example adds the value of the object b to the object a. This is unwise, as the + operator does not normally function in this way. Redefining the += operator would be much better.
If they do not serve to clarify the logic of the software, do not use overloaded operators; just use member functions with meaningful names instead. Neither language feature introduces any hidden overheads into the resulting code.
#2: Do not treat an embedded system like a desktop computer. If you're programming a PC, you can assume that memory is infinite (and free) and that you will always have enough CPU power.
With embedded software, you need to be more circumspect as both memory and CPU power are limited. The best approach is to write some code, then measure its size and its execution performance. Only when you are satisfied that these measurements are within reasonable bounds should you move on.
#3: A purely top down implementation may be dangerous. It may be attractive to build a software application by starting at the high level and including just stubs for lower level functions. Then, when that structure is debugged, you can start implementing the next level down.
The drawback to this strategy is that a nasty surprise may be waiting just around the corner. You can finish coding and find that the whole program is too big and/or too slow. And you have no idea where you acquired the additional overhead.
A better approach is to start at the other end. Encapsulate the lower level functionality in objects and build the application up from there ” following the previous advice of measuring as you go. With this approach you will not encounter any surprises.
#4: Avoid the use of deeply nested inheritance. Part of the point behind object-oriented programming is the ability to define classes in terms of others (base classes) that have been previously created (by you or by another developer) without the need to fully understand the inner workings of the base classes.
This is inheritance. The downside is that objects instantiated from such derived classes may carry some overhead ” not much, but some. And this overhead may mount up as the depth of inheritance increases. So, for an embedded application, where overheads may really count, you need to be vigilant.
#5: It is not logical to avoid language features just because your tools do not handle them well. Some language features can be very useful, but will carry an overhead if not handled in an appropriate way for embedded applications. An obvious example is function templates which if badly implemented, can cause an enormous code bloat.
A common implementation of templates results in instantiations which are local to a module. This can lead to numerous copies of identical code that could waste an enormous amount of memory. If your tools do not handle such constructs well, do not eschew good programming practice. Get some tools that will enable you to program productively.
As code size and complexity increases relentlessly, C++ is becoming an excellent programming language of choice for embedded applications. Past failures have deterred many developers from adopting the language choosing instead to continue using C. The key to success with C++ is to use it intelligently, focus on the problems that it addresses, and pay attention to its implementation on an embedded system.
Colin Walls has over twenty-five years experience in the electronics industry, largely involved with embedded software. A frequent presenter at conferences and seminars including the Embedded Systems Conference he is a member of the marketing team of the Embedded Systems Division of Mentor Graphics.