300 likes | 548 Views
C to C++ Migration for Embedded Systems. A Step by Step tutorial with Pro’s and Con’s. by Dirk Braun. General considerations What is OO? (very briefly, very quick) Why is OO in embedded systems a special subject?. Contents.
E N D
C to C++ Migration for Embedded Systems A Step by Step tutorial with Pro’s and Con’s by Dirk Braun
General considerations What is OO? (very briefly, very quick) Why is OO in embedded systems a special subject? Contents • Tutorial: Convert an example C module into a C++ class in 6 simple steps • Measurements & comparison of code-size and speed Not dealt with: Inheritance and advanced OO.
What is meant by Object Orientation? General Considerations Code is in classes, Objects are instances of classes. Analogy: Alarm clock. The generalization of the alarm clock is the class. Real alarm clocks are instances of the idea (the class). These would be called the objects or class instances. Uses for SW-projects: Code for all objects of the same class can be identical and exist only once. Benefit: save code size from the second instance The instances differ by their states. I.e. Each object has its own private copy of memory that completely describes its state. Benefit: object instances are independant of each other objects can be created at run time
Why is OO in embedded systems a special subject? General Considerations • Because assumption about available memory differ. Embedded Reality OO Dynamic object- creation at runtime (usually) • Limited memory • Coding standards • Because many developers haven’t learned it during their professional training. OO trained from mid 90s, many developers learned electronics rather than computing science. Combined studies emerging now.
General Considerations Why is OO linked to dynamic memory allocation? Compile Time Run-Time Object 1 with var Class: Code + Data Structure Object 2 with var Object 3 with var • Dynamic memory allocation has to be treated with care because: • We cannot easily test if the physically available memory will survive the worst case. • Memory can get fragmented and thereby seem to get used up. • Programmed Memory holes are not very easy to detect and may appear only after the system has been running for days or weeks. • When using object oriented programming: • Each object has its own variable (of class type). • When the number of required objects is unknown at compile-time, the objects have to be instantiated at run-time and allocated dyamically. • With limited RAM I avoid permament dynamic object creation (i.e. Dynamic memory allocation) at run-time but I think it‘s OK during startup or reconfiguration.
Tutorial: Convert a C-Timer Module to C++ in 6 simple steps 0. Get to know the initial C-Project • Switch from C to C++ compiler (no change of source code) • A simple class with Constructor and Initialization functions. What is the “this-pointer”? How to do static object memory allocation. • Convert all functions (except the ISR) to class member functions. • Convert the Interrupt-Service-Function to a class member-function. • Turn all global module-variables into protected member variables. • Create multiple class-instances that represent separate HW-entities.
Standard on Chip HW-Timer Provides SW-Timers for other modules Wake up at intervals, count ticks, check if any SW-timer needs being called. Similar to the work of a cyclic task scheduler Additional features: time base readjusts itself automatically to the greatest possible granularity to satisfy all SW timer cycles. Step 0: The initial C-Project
Working-Principle of Timer Module App Module A Timer Module InitA 1 Timer Registration TimerCallbackA a HW-Timer Event Timer ISR works like a scheduler x 2 App Module B InitB b TimerCallbackB • SW-Timers register their on cyclic functions (1 & 2) • Timer-Interrupts get counted (x) • SW-Timer-Callbacks get called at the right times (a & b)
Use & debug the Timer Demo Project 1. Initialize & Register SW-Timer-callbacks 2. Set breakpoints inside the callbacks. int main (void) { ... TimerInit(); // erster TestTimer TimerCreate(25, On1stTimer3); TimerCreate(30, On2ndTimer3); TimerCreate(35, On3rdTimer3); ... while (TRUE) { /* Loop forever */ ; } } 3. Observe the times at which the breakpoints are approached (watch the timing repoted by the simulator).
Rename timer.c to timer.cpp. This causes the µVision IDE to use the c++ compiler. Errors show stricter type checking -> do type casts New linker errors: Undefined symbol TimerCreate The reason for this is “name decoration” that is used in C++. Further examination on next page. Step 1: Switch from C to C++ compiler
Searching object files reveals: “hello.o” wants ”TimerCreate” but timer.o exports Decorated Names • _Z11TimerCreatejPFvvE • _Z11TimerDeleteiPFvvE • _Z15TimerIntDisablev • _Z11TimerTx_ISRv • _Z10TimerStartv • _Z9TimerInitv … • int TimerCreate(WORD,TimerCallbackPtr); • BOOL TimerDelete(int, TimerCallbackPtr); • void TimerIntDisable(void); • void TimerTx_ISR (void) __irq; • void TimerStart(void); • void TimerInit(void); … Reason for name decoration: C++ permits identical function names that differ only by their parameter lists. Name decoration codes the parameter lists into the exported function names. Different compilers very like decorate in different ways. So using a C++ library compiled wih a different compiler very likely won’t work. -> To us this means: all code module that use C++ functions have to be compiled with a C++ Compiler, too. -> Rename “hello.c” to “hello.cpp”
Now the linker reports undefinded symbols “ienable” and “idisable”. These functions are implemented in a module (utilities) that is still compiled by the C compiler (and shall remain so). The C++ compiler assumed these are also C++ functions and hence tells the linker to look for decorated function names. We have to tell the C++ compiler that these are C functions by use of “extern C”. Call C-functions from CPP works At the beginning and end of utilities.h insert the statements shown on the right. These conditionals ensure that the same file can freely be included by C and C++ modules. #ifdef __cplusplus extern "C" { #endif ... #ifdef __cplusplus } // extern "C" #endif Finally the project should compile. Debug and check that it still works.
Step 2 Create a simple class – classes, code-reuse, object allocation From Step 2 to step 6 use mixture of C and C++ using only a single instance (object) of the new timer class. Add a very simple class to the class.
1. Add a class declaration to the header Step 2 Create a simple class – classes, code-reuse, object allocation • class Timer • { • protected: • public: • Timer(BYTE timerNo); • void Init(); • }; void TimerInit() becomes Timer::Timer(BYTE timerNo) Add new (empty function) function void Timer::Init() 2. Change implementation in timer.cpp The static initialization creates the timer before main gets called. The intialization (for static object instantiation) is split into two parts. The constructor that gets called “automatically” and the self-made “Init”-function. 3. Declare a timer object statically in hello.c (similar to a global variable). To avoid dynamic memory allocation I used a static declaration. Space for the class gets reserved at compile time. Timer myTimer = Timer(0); 4.Compile and check for errors.
class Timer { protected: WORD Gcd(WORD a, WORD b); void IntEnable(void); void IntDisable(void); void Start(); void Stop(); void CalculateInternals(void); public: Timer(BYTE timerNo); void Init(); int Create(WORD ms_interval, TimerCallbackPtr pCallback); BOOL Delete(int timerNo, TimerCallbackPtr pCallback); }; Step 3 Convert all Functions to class methods (except ISRs) • Turn all forward declared functions (used in .c file only) into protected member functions (except ISR). • Turn all the remaining publicly declared functions (those in the header) into public members. • Change the implementation of these functions by prefixing the functions with „Timer::“. • Change all calls to these functions from outside of the class (i.e. in hello.c) to class-method calls. • Compile and check for errors. void TimerIntEnable(void) becomes void Timer::IntEnable(void) TimerCreate(25, On1stTimer3); becomes myTimer.Create(25, On1stTimer3);
Detecting the this pointer Step 3 Convert all Functions to class methods (except ISRs) • We have to get a better understanding of what happens in the background. • Set a breakpoint at the first call to „myTimer.Create“ and let the debugger run up to there. • Open the disassembly view. • See how 3 parameters (R0, R1 & R2) in the call to Timer::Create(). • Close the disassembly view and step into the function. • Add two variables „&myTimer“ (the address of a global variable) and „this“ to the watch window. The addresses conicide (also with the content of R0 – used for parameter passing into the function).
The purpose of the „this“ pointer Step 3 Convert all Functions to class methods (except ISRs) As mentioned earlier OO is about code reuse. Code exists only once, but we can have many instances of objects. Every method call gets a hidden first parameter: a pointer to the object the method shall be applied to. The object then is a variable containing the objects state. In our case – at this stage of conversion – the class does not have any properties (i.e. variables) yet, and hence „this“ points to en empty structure. This knowledge is important in order to understand the next step.
Step 4 Turn the ISR into a class member We just learned that every class method receives a hidden this pointer. So if we want to change the Interrupt Service Routine (ISR) into a class method we‘re going to run into trouble. An ISR is just an interrupt vector. The interrupt-controller won‘t be kind enough to provide a suitable this pointer. In C++ a method can be „static“. This simply means that the function doesn‘t receive a „this“ pointer and will not be able to know which object it‘s working on. For now we‘re dealing with one object only, so we should get along. Let‘s just convert it and see how far we get.
Step 4 Turn the ISR into a class member • Remove the forward declaration of the ISR in timer.cpp • In timer.h declare • static void Tx_ISR (void) __irq; • in the protected section of the class declaration. • 3. Change the function name in the .cpp file accordingly • void Timer::Tx_ISR (void) __irq • Then adjust the interrupt vector to point to the new function • SET_VIC_VECT_ADDR(TIMER_ILVL, Tx_ISR) • in the constructor. • 5. Compiler, Debug, Check. • Don‘t worry if you do not quite understand why this works. Step 5 makes it clear.
Step 5 The objects get a state – global variable become protected members • Cut & paste all globally declared variables in timer.c to the protected section in timer.h. • Try to compile. • Get loads of errors, but all from inside the ISR !? • In the previous step the ISR refered to global variables. These have just been moved into the class (or in C-talk: into a structure). All the other class functions reference into this structure by use of the hidden this pointer. The ISR doesn‘t have one.
Timer* pTimer0; // somehow globally stored void Timer::T0_ISR (void) __irq { pTimer0->Tx_ISR(); } void Timer::Tx_ISR (void) { ... // as before Idea: how to provide a this pointer for the static ISR Step 5 The objects get a state – global variable become protected members The ISR uses a global pointer to „call into“ a method of a real object instance. This method contains the original ISR code.
Timer* pTimer0; Timer::Timer(BYTE timerNo) { ... switch (timerNo) { case 0: pTimer0 = this; break; default: // todo: show error break; } ... } void Timer::T0_ISR (void) __irq { pTimer0->Tx_ISR(); } void Timer::Tx_ISR (void) { ... // as before Step 5 The objects get a state – global variable become protected members Use a global class-pointer to „call back into the class“ • Declare a global pointer variable „pTimer0“ of type „Timer*“ • In the constructor save the this pointer to the global pointer. • Create a new protected method (Tx_ISR) that does what the ISR did so far. • From the real static ISR call the new Tx_Isr with the new global class-pointer. • Compile, debug and check for errors. • Set a breakpoint in T0_ISR and see how the code calls back into the class.
Step 6 - Multiple Object Instances • An ordinary class would be finished now and could be instantiated many times. • This class is special because it is linked to HW-resources, namely Timer-Peripherals. As a consequence each object instance needs its own • set of pointers to registers • Interrupt Service Routine • Create pointer vars to timer registers • Create a second ISR for Timer 1 protected: ... WORD m_timerChannel; volatile unsigned long* m_pTxMR0; volatile unsigned long* m_pTxTCR; volatile unsigned long* m_pTxIR; ... static void T1_ISR (void) __irq;
Timer::Timer(BYTE timerNo) { ... switch (timerNo) { case 0: pTimer0 = this; T0MCR = 3; // Interrupt and Reset on MR0 T0TCR = 0; // Timer0 Enable m_pTxMR0 = &T0MR0; // set pointers to SFRs m_pTxTCR = &T0TCR; m_pTxIR = &T0IR; m_timerChannel = TIMER0_CHANNEL; SET_VIC_VECT_ADDR(TIMER0_ILVL, T0_ISR) SET_VIC_VECT_CNTL(TIMER0_ILVL, m_timerChannel) break; case 1: pTimer1 = this; T1MCR = 3; // Interrupt and Reset on MR1 T1TCR = 0; // Timer1 Enable m_pTxMR0 = &T1MR0; // set pointers to SFRs m_pTxTCR = &T1TCR; m_pTxIR = &T1IR; m_timerChannel = TIMER1_CHANNEL; SET_VIC_VECT_ADDR(TIMER1_ILVL, T1_ISR) SET_VIC_VECT_CNTL(TIMER1_ILVL, m_timerChannel) break; ... • In timer.cpp create global pointer to timer 1. • Store global object pointer to timer 1 in constructor. • Store register pointers in constructor. Step 6 - Multiple Object Instances Timer* pTimer1;
Access registers via these pointers. (Changes shown for one example) • Create ISR for Timer1 peripheral. • Instantiate & use the second HW-timer object in hello.c (note: no of HW-Timer passed in constructor) • Compile and check for errors. Step 6 - Multiple Object Instances void Timer::Stop() { *m_pTxTCR = 0; // Timer X Disable } void Timer::T1_ISR (void) __irq { pTimer1->Tx_ISR(); } Timer myOtherTimer = Timer(1); // static allocation of timer object int main (void) { ... myOtherTimer.Init(); ... myOtherTimer.Create(35, On3rdTimer3); // cycle time = 35 ms ... }
A B C Units Code size 3548 5008 3772 Bytes Data size 1544 1792 1772 Bytes Time in ISR 7.22 6.79 9.12 µs Comparison of two systems: A – code of step 0 (C-only) B – code of step 0 duplicated (C-only) C – equal to step 6 (OO) Measurements • Interpreting measurements: • Code size • C much less than B. Expected. • C slightly more than A. May be assigned to added indirection and storing of pointers etc. • RAM • B and C very similar. Expected. • Performance • C greater than A and B. Expected. Attributed to indirection and additional function call.
Summary • Pro‘s • Smaller Code for more than one object • „Automatic“ code maintenance as code exists only once. • Con‘s • Small performance overhead • „Automatic“ code maintenance as code exists only once. • Uses • Bus couplers • Time synchronous machines – e.g. multi axis control Contact Dirk Braun Email dbraun@cleversoftware.de