I will show how good object-oriented design of embedded software can result in many benefits – smaller code and improved ease of source-code maintenance – but with only a slight trade off in terms of performance.
Using an example as the starting point (a standard timer module written in C that services timer units that are provided by nearly every MCU) I will describe a six step process by which to convert C code into object oriented C++ code classes.
Several steps in this process will illustrate a safe path for migrating this source code into an object oriented class. An instance of this class then is a HW- Timer-Peripheral with a much nicer SW-Interface than registers and interrupts. Then it is a matter of simply instantiating the timer class once per HW-Timer available on the particular MCU.
This process can be applied to any situation where more than a single instance of one type of HW-peripheral is to be used in an embedded project. Measurements of performance and code size are provided and topics like OO-in embedded systems and the improved SW-Architecture will be discussed more generally.
The examples we will work through use two timers on an ARM7. They can be completely worked through without any real hardware, using Keil’s ARM7 simulator and the Keil Realview Compiler only. Because many other MCUs have at least 2 on-chip-timers these examples can be ported to other platforms, too.
Starting point: the standard Timer-Module in C
A hardware-timer is a counter/comparator with reset- and interrupt-logic. This timer « wakes up » an interrupt service routine at regular programmed intervals. But many embedded applications need more timers than HW-timers that are available on a given MCU.
We can duplicate the timer, by e.g. letting the HW-timer interrupt at 1 ms intervals. The ISR can then call function A every 3 ms and call another function B every 5 ms. This way we have used one HW-timer to realize several SW-timers, each with it’s own time base. This method is fairly common practice.
For reasons out of the scope of this article the project’s various timer-functions are registered with the timer-module and will then be called back when their time has come. The timer module (Figure 1 below) is based on a single HW-timer and each registered timer-callback-function has its own time interval. This is very similar to how a cyclic-task-scheduler works.
|Figure 1. Module and process-flow overview. Registration of timer callbacks (1 & 2) and their cyclic execution (x, a and b)|
The time base of the timer module depends on the cycle-times requested by the various application modules. If app. A wants 30 ms and B wants 50 ms, then the HW-timer can realize this using an overall time base of 10 ms. If A wants 9 and B wants 3 ms, then the main time base needs to interrupt every 3 ms.
Raising the interrupt no more often than absolutely necessary saves processor performance. This main-time-base is automatically adjusted (by reprogramming the counter register) when timer-callbacks are registered by means of a « greatest common divider » function.
Listing 1 above and Listing 2 below show excerpts of the interface and implementation which realize this concept of a timer module.
The result is a module that represents a single HW-timer and provides many SW-timers.
Let us now return to the main topic and start converting it into a class. Afterwards we will be able the instantiate a class for each HW-timer, where each provides many SW-timers.
|Listing 2. Implementation of timer module|
Step #1: Use the C++ compiler
Very often in SW-development it is a good idea to take small steps. This eases tracking down the mistakes later on. So let’s simply try to switch to another compiler. In the Keil IDE we do this by simply renaming the files from *.c to *.cpp. When we change timer.c to timer.cpp and compile it we will see the stricter type checking of the CPP compiler, which we resolve by doing type casts.
if ((g_pOnTimer[timerIdx] != NULL))
if ((g_pOnTimer[timerIdx] != (TimerCallbackPtr)NULL))
But on rebuilding the changed project a few more linker errors do appear, too:
.\Simulator\Test.axf: Error: L6218E: Undefined symbol TimerCreate (referred from hello.o).
hello.o refers to TimerCreate but timer.o exports _Z11TimerCreatejPFvvE. The CPP-compiler allows function overloading (same function name with different parameter lists). So it has to be able to distinguish between functions that bear the same name. It does it by using name-decoration.
I guess that « _Z11 » means something like CPP function that returns an int and « jPFvvE » represents the parameter list. Unfortunately every CPP compiler I have seen does this name-decoration in a different way. So while calling into a library created by another compiler was possible using pure C this is very rarely so using CPP.
Anyway, this name-decoration explains why a C-module cannot call CPP functions. (Calling C functions from CPP-modules is possible though.) While a C-module expects to find a simple function « CreateTimer » as declared the function provided has a decorated name.
This explains why the files will compile all right but linking fails. Hence, any module that calls functions with decorated names must also be a CPP-Module. In this example we’re forced to convert main.c to main.cpp.
Step #2: Create a simple class » classes, code-reuse, object allocation
During this and the following steps I will work with a mixture of object-oriented and plain C-code. This will work as long as I ensure that I have only a single instance of the object. The reason for this is » again » that I want to proceed in small steps.
Object orientation is analogous to the relationship between a cake and its recipe. I can bake many cakes but I use the same recipe for them all. The recipe is an analogy for code and I need it only once and can instantiate it many times. The ingredients and state of each cake that I bake are the properties of each instance of a cake-object. These are the variables that describe the state of each instance of the class.
One of the advantages of OO is code reuse. We have only one chunk of code for all objects that there are. The distinct objects differ by their individual state. So each object has its own data, but they all share the same code.
From this follows that in every function the code has to know which object it is working on. If you bake 5 cakes in parallel you should know which one to add butter to. When programming this « which one » is the hidden (this) pointer to the object to work on, that gets passed into every function of a class.
In order to understand object-oriented programming it helps to ask: How could I do OO in C? First, I would create a data structure that contains all variables of the module. I would declare a variable of that structure and delete former global variables. Then I equip all functions with an additional first parameter » the pointer to this new variable.
Finally I would change all these functions to access their data via the new « this » pointer. (This is » by the way » exactly how an early version of Keil’s OO compiler worked. It even generated intermediate C-files one could inspect.)
So we need object data. Just as ordinary variables can be declared at compile-time or allocated dynamically, there are both options for class objects, too. This is important because in many embedded systems dynamic data allocation is not allowed or only in certain limits, violates programming guidelines etc.
The reason is that safety critical systems that have to execute for long periods cannot afford memory leaks or fragmentation. However, if such rules apply, simply use static object instantiation as done in this example.
|Listing 3. Initial class interface|
Let’s start by turning the timer into a class. As mentioned above, during the intermediate steps of the C-to C++ migration part of the implementation will be OO, while the rest remains « ordinary C ». We can do that because the OO implementation can still access global variables. I start by creating a constructor and converting the « Init » function. Listing 3 above shows the new class-interface.
Lets declare a single instance of the class (global var in main.cpp):
Timer myTimer = Timer(0);
This declares the object myTimer and will cause the constructor Timer::Timer() to be called. But when? Somewhere in the startup code i.e. before the first line of your own code is being executed! This means that we cannot control exactly when the constructor of a statically allocated class-object is being called.
If it is important to adhere to an initialization order during system-init (e.g. reset hardware, then init) you need to split the initialization into two parts and provide an extra Init() function (see Listing 4 below.)
|Listing 4. Constructor and init function|
The initialization of the former TimerInit() function will now have to be separated. The time independent initialization goes into the constructor. The HW-Initialization is realized within the Init() function of the new class, which I call at the right moment.
Step #3 : Convert all Functions to class methods (except ISRs)
As shown in Listing 5 below, all old forward declarations become protected member functions.
Just move them into the private section of the header file. All so far published functions become public members. In the implementation just prefix all these functions with Timer.This step is quite simple.
So far we have a class with methods but no data. All class methods use global data.
|Listing 5. Additional methods.|
Step #4: Turn the ISR into a class member
Interrupt-Service-Functions differ from ordinary functions in that they are being called by the hardware which simply jumps to the address saved in the interrupt vector table.
Of course, the hardware does not supply a « this » pointer. So an Interrupt-Vector is a pointer to the ISR and a unique resource. This is why the class function has to be static (« static function in OO » means « a function that doesn’t have a this pointer »).
But how does the function know which object it should refer to? (Congratulations if you were just asking this yourself!) Well, it doesn’t, and I’ll start worrying about this when it comes to having more than one instance of the timer. Let’s just get on for now.
1. Remove the forward declaration in timer.cpp
void TimerTx_ISR (void) __irq;
and declarestatic void Tx_ISR (void) __irq;
in the protected section of the class declaration in the header instead.
2. Change the function name in the .cpp file accordingly
void Timer::Tx_ISR (void) __irq
3. Then adjust the interrupt vector to point to the new function
Step #5: Global variables become protected member variables
All right. Do what it says and see what happens. Cut and paste the global vars from timer.cpp to the protected section of class Timer. Try to compile: All works fine. It is just the ISR that complains:
« nonstatic reference must be relative to a specific object« .
I admit, this « let’s worry later » of Step 4 didn’t last long here. This is because the ISR uses member-variables now (instead of global vars). Consequently it wants a this-pointer and cannot be static. So, here I want the ISR both to be non-static (so it knows the object’s state) and static (so it can be an ISR) at the same time.
|Listing 6. Static and non-static ISR|
Really I need two functions:
1. The static real ISR. It has to somehow know the object instance and call.
2. the non-static member ISR-Handler function.
This means that I have to save the object instance pointer globally somewhere so that the ISR can refer to it. The best place to do this saving is the constructor. The additional changes required are listed in Listing 6 above and Listing 7 below.
In this step I also changed the variable prefixes g_ (for global var) to m_ (for member var).
|Listing 7. Store object pointer globally and call « class ISR »|
Step #6: Dealing with Multiple Object Instances
If we were using ordinary classes we would be ready now. But here each instance of the timer class represents a physical HW-timer-peripheral. Each HW-Timer has its own interrupt and register set to control it.
|Listing 8. Header additions for multiple objects|
So that each object instance knows which registers to address, I added pointers to the registers which the class now uses instead of the fixed special-function-registers.
These pointers need to be set up in the constructor, depending on which HW-timer is being used. The interrupt specific information (priority, interrupt-channel) is used and set in the same code section. This is shown in Listing 8 above and Listing 9 below.
Just as I saved the object pointer globally (to be used in static ISR) we now need to do this for each instance and add the static ISRs for all HW-Timers, that are to be supported.
|Listing 9. Initialize correct HW, pointers to SFRs, second ISR|
Finally we create a second instance of the timer (in main.c) and use the second timer. The test shows how well these two timers work.
Compare and contrast
Now it’s the time to compare the initial and final projects with respect to performance code size. For performance measurements I decided to measure the time spent in the ISR (including calling the callbacks). This is the time that reflects the amount of processor time used by the implementations. These results are shown in Table 1 below.
The differences in terms of SW-Design, ease of source code maintenance can only be discussed qualitatively and individual preferences will lead to different results. Personally, I prefer the OO solution when I have more than one instance because I have only « one instance » of source code and do not need to worry about keeping it in sync.
|Table 1. Comparison of code size and performance.|
These results are not surprising. The OO-solution needs an additional this-pointer. Dereferencing member variables and passing the additional parameter takes a little more time and consumes RAM. The code size is also a little more but advantageous for the second instance.
How useful are objects for real applications? Of course, the use of two instances representing two HW-timers is not so obvious. But think of applying this model to other applications. How about several independent stepper-motor-drives that run entirely in HW. They run synchronously due to the common oscillator clock.
I can also imagine applications where it would be very useful to have instances of a class that represent a group of identical or very similar peripherals. Think of Bus-Couplers. Here you may have some Bus-HW, that receives data on one end and copies (possibly after filtering) it to another Bus-HW of the same kind (maybe at another bus speed). Many MCUs have several identical communication peripherals on board that could well be implemented in this way.
Dirk Braun graduated from King’s College, University of London. His background ranges from software design and development to the electronics of embedded systems. He has recently developed a data centric RTOS and can be contacted at email@example.com.