A six step process for migrating embedded C into a C++ object-oriented framework - Embedded.com

A six step process for migrating embedded C into a C++ object-oriented framework

Object oriented programmingin embedded systems is still not widely employed. Many programmers inthisarea either come from a hardware electronics background or are purecomputer scientists.

This article discusses one role that objectoriented programming canplay in embedded systems while maintaining requirements concerningsafety and reliability that are often imposed by standards like MISRA.

I will show how goodobject-oriented design of embedded software can result in manybenefits – smaller code and improved ease of source-code maintenance -butwith only a slight trade off in terms of performance.

Using an example as the starting point (a standard timer modulewritten in C that services timer units that are provided by nearlyevery MCU ) I will describe a six step process by which toconvert C code into object oriented C++ code classes .

Several steps in this process will illustrate a safe path formigrating this source code into an object oriented class. An instanceof this class then is a HW- Timer-Peripheral with a much nicerSW-Interface than registers and interrupts. Then it is a matter ofsimply instantiating the timer class once per HW-Timeravailable on the particular MCU.

This process can be applied to any situation where more than asingle instance of one type of HW-peripheral is to be used in anembedded project. Measurements of performance and code size areprovided and topics like OO-in embedded systems and the improvedSW-Architecture will be discussed more generally.

The examples we will work through use two timers on an ARM7.Theycan be completely worked through without any real hardware, usingKeil's ARM7 simulator andthe Keil Realview Compiler only.Because manyother MCUs have at least 2 on-chip-timers these examples can be portedto other platforms, too.

Starting point: the standardTimer-Module in C
A hardware-timer is a counter/comparator with reset- andinterrupt-logic. This timer “wakes up” an interrupt service routine atregular programmed intervals. But many embedded applications need moretimers than HW-timers that are available on a given MCU.

We can duplicate the timer, by e.g. letting the HW-timer interruptat 1 ms intervals. The ISR can then call function A every 3 ms and callanother function B every 5 ms. This way we have used one HW-timer torealize several SW-timers, each with it's own time base. This method isfairly common practice.

For reasons out of the scope of this article the project's varioustimer-functions are registered with the timer-module and will then becalled back when their time has come. The timer module (Figure 1 below ) is based on a singleHW-timer and each registered timer-callback-function has its own timeinterval. This is very similar to how a cyclic-task-scheduler works.

Figure1. 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-timesrequested by the various application modules. If app. A wants 30 ms andB wants 50 ms, then the HW-timer can realize this using an overall timebase of 10 ms. If A wants 9 and B wants 3 ms, then the main time baseneeds to interrupt every 3 ms.

Raising the interrupt no more often than absolutely necessary savesprocessor performance. This main-time-base is automatically adjusted(by reprogramming the counter register) when timer-callbacks areregistered by means of a “greatest common divider” function.

Listing1

Listing 1 above and Listing 2 below show excerpts ofthe interface and implementation which realize this concept of a timermodule.

The result is a module that represents a single HW-timer andprovides many SW-timers.

Let us now return to the main topic and start converting it into aclass. Afterwards we will be able the instantiate a class for eachHW-timer, where each provides many SW-timers.

Listing2. 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 toswitch to another compiler. In the Keil IDE we do this by simplyrenaming the files from *.c to *.cpp. When we change timer.c totimer.cpp and compile it we will see the stricter type checking of theCPP compiler, which we resolve by doing type casts.

if((g_pOnTimer[timerIdx] != NULL))
becomes
if((g_pOnTimer[timerIdx] != (TimerCallbackPtr)NULL))

But on rebuilding the changed project a few more linker errors doappear, too:

.SimulatorTest.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 differentparameter lists ). So it has to be able to distinguish betweenfunctions that bear the same name. It does it by using name-decoration.

I guess that “_Z11” means something like CPP function that returnsan int and “jPFvvE” represents the parameter list. Unfortunately everyCPP compiler I have seen does this name-decoration in a different way.So while calling into a library created by another compiler waspossible using pure C this is very rarely so using CPP.

Anyway, this name-decoration explains why a C-module cannot call CPPfunctions. (Calling C functions fromCPP-modules is possible though. ) While a C-module expects tofind a simple function “CreateTimer” as declared the function providedhas a decorated name.

This explains why the files will compile all right but linkingfails. Hence, any module that calls functions with decorated names mustalso be a CPP-Module. In this example we're forced to convert main.c tomain.cpp.

Step #2: Create a simple class “classes, code-reuse, object allocation
During this and the following steps I will work with a mixture ofobject-oriented and plain C-code. This will work as long as I ensurethat I have only a single instance of the object. The reason for thisis ” again ” that I want to proceed in small steps.

Object orientation is analogous to the relationship between a cakeand its recipe. I can bake many cakes but I use the same recipe forthem all. The recipe is an analogy for code and I need it only once andcan instantiate it many times. The ingredients and state of each cakethat I bake are the properties of each instance of a cake-object. Theseare the variables that describe the state of each instance of theclass.

One of the advantages of OO is code reuse. We have only one chunk ofcode for all objects that there are. The distinct objects differ bytheir individual state. So each object has its own data, but they allshare the same code.

From this follows that in every function the code has to know whichobject it is working on. If you bake 5 cakes in parallel you shouldknow which one to add butter to. When programming this “which one” isthe hidden (this) pointer to the object to work on, that gets passedinto 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 thatcontains all variables of the module. I would declare a variable ofthat structure and delete former global variables. Then I equip allfunctions with an additional first parameter ” the pointer to this newvariable.

Finally I would change all these functions to access their data viathe new “this” pointer. (This is ” bythe way ” exactly how an early version of Keil's OO compiler worked. Iteven generated intermediate C-files one could inspect. )

So we need object data. Just as ordinary variables can be declaredat compile-time or allocated dynamically, there are both options forclass objects, too. This is important because in many embedded systemsdynamic 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 forlong periods cannot afford memory leaks or fragmentation. However, ifsuch rules apply, simply use static object instantiation as done inthis example.

Listing3. 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 theimplementation will be OO, while the rest remains “ordinary C”. We cando that because the OO implementation can still access globalvariables. 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 ):

TimermyTimer = Timer(0);

This declares the object myTimer and will cause the constructorTimer::Timer() to be called. But when? Somewhere in the startup codei.e. before the first line of your own code is being executed! Thismeans that we cannot control exactly when the constructor of astatically allocated class-object is being called.

If it is important to adhere to an initialization order duringsystem-init (e.g. reset hardware, then init) you need to split theinitialization into two parts and provide an extra Init() function (see Listing 4 below.)

Listing4. Constructor and init function

The initialization of the former TimerInit() function will now haveto be separated. The time independent initialization goes into theconstructor. 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 toclass 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 sofar published functions become public members. In the implementationjust prefix all these functions with Timer.This step is quite simple.

So far we have a class with methods but no data. All class methodsuse global data.

Listing5. Additional methods.

Step #4: Turn the ISR into a classmember
Interrupt-Service-Functions differ from ordinary functions in that theyare being called by the hardware which simply jumps to the addresssaved in the interrupt vector table.

Of course, the hardware does not supply a “this” pointer. So anInterrupt-Vector is a pointer to the ISR and a unique resource. This iswhy 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, itdoesn't, and I'll start worrying about this when it comes to havingmore than one instance of the timer. Let's just get on for now.

1. Remove the forwarddeclaration in timer.cpp
void TimerTx_ISR(void) __irq;
and declarestaticvoidTx_ISR (void) __irq;
in the protected section of the classdeclaration in the headerinstead.

2. Change the function namein the .cpp file accordingly
voidTimer::Tx_ISR (void) __irq

3. Then adjust theinterrupt vector to point to the new function
SET_VIC_VECT_ADDR(TIMER_ILVL, Tx_ISR)

Step #5: Global variables becomeprotected member variables
All right. Do what it says and see what happens. Cut and paste theglobal vars from timer.cpp to the protected section of class Timer. Tryto compile: All works fine. It is just the ISR that complains:

nonstatic reference must berelative 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 globalvars ). Consequently it wants a this-pointer and cannot bestatic. So,here I want the ISR both to be non-static (so it knows the object'sstate ) and static (so it canbe an ISR ) at the same time.

Listing6. Static and non-static ISR

Really I need two functions:

1. The static real ISR. Ithas to somehow know the object instanceand call.
2. the non-static memberISR-Handler function.

This means that I have to save the object instance pointer globallysomewhere so that the ISR can refer to it. The best place to do thissaving is the constructor. The additional changes required are listedin Listing 6 above and Listing 7 below .

In this step I also changed the variable prefixes g_ (for globalvar) to m_ (for member var).

Listing7. Store object pointer globally and call “class ISR”

Step #6: Dealing with MultipleObject Instances
If we were using ordinary classes we would be ready now. But here eachinstance of the timer class represents a physical HW-timer-peripheral.Each HW-Timer has its own interrupt and register set to control it.

Listing8. Header additions for multiple objects

So that each object instance knows which registers to address, Iadded pointers to the registers which the class now uses instead of thefixed special-function-registers.

These pointers need to be set up in the constructor, depending onwhich 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 staticISR ) we now need to do this for each instance and add the staticISRsfor all HW-Timers, that are to be supported.

Listing9. Initialize correct HW, pointers to SFRs, second ISR

Finally we create a second instance of the timer (in main.c) and usethe 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 withrespect to performance code size. For performance measurements Idecided to measure the time spent in the ISR (including calling thecallbacks ). This is the time that reflects the amount ofprocessor timeused by the implementations. These results are shown in Table 1 below.

The differences in terms of SW-Design, ease of source codemaintenance can only be discussed qualitatively and individualpreferences will lead to different results. Personally, I prefer the OOsolution when I have more than one instance because I have only “oneinstance” of source code and do not need to worry about keeping it insync.

Table1. Comparison of code size and performance.

These results are not surprising. The OO-solution needs anadditional this-pointer. Dereferencing member variables and passing theadditional parameter takes a little more time and consumes RAM. Thecode size is also a little more but advantageous for the secondinstance.

How useful are objects for real applications? Of course, the use oftwo instances representing two HW-timers is not so obvious. But thinkof applying this model to other applications. How about severalindependent stepper-motor-drives that run entirely in HW. They runsynchronously due to the common oscillator clock.

I can also imagine applications where it would be very usefulto haveinstances of a class that represent a group of identical or verysimilar peripherals. Think of Bus-Couplers. Here you may have someBus-HW, that receives data on one end and copies (possibly afterfiltering ) it to another Bus-HW of the same kind (maybe at another busspeed ). Many MCUs have several identical communicationperipherals onboard that could well be implemented in this way.

Dirk Braun graduated from King'sCollege, University of London. His background ranges from softwaredesign and development to the electronics of embedded systems. He hasrecently developed a data centric RTOSand can be contacted at dbraun@cleversoftware.de.

Leave a Reply

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