A workshop on C to C++ migration and creation of classes that represent a type of On- Chip HW-Peripherals.



Similar documents
Freescale Semiconductor, I

Simple Cooperative Scheduler for Arduino ARM & AVR. Aka «SCoop»

Singletons. The Singleton Design Pattern using Rhapsody in,c

Using the TASKING Software Platform for AURIX

Keil C51 Cross Compiler

MPLAB Harmony System Service Libraries Help

Control III Programming in C (small PLC)

How To Port A Program To Dynamic C (C) (C-Based) (Program) (For A Non Portable Program) (Un Portable) (Permanent) (Non Portable) C-Based (Programs) (Powerpoint)

SKP16C62P Tutorial 1 Software Development Process using HEW. Renesas Technology America Inc.

EE8205: Embedded Computer System Electrical and Computer Engineering, Ryerson University. Multitasking ARM-Applications with uvision and RTX

MPLAB TM C30 Managed PSV Pointers. Beta support included with MPLAB C30 V3.00

An Introduction To Simple Scheduling (Primarily targeted at Arduino Platform)

APPLICATION. si32library. Callback CMSIS HARDWARE. Figure 1. Firmware Layer Block Diagram

AN1229. Class B Safety Software Library for PIC MCUs and dspic DSCs OVERVIEW OF THE IEC STANDARD INTRODUCTION

An Introduction to MPLAB Integrated Development Environment

XMOS Programming Guide

Glossary of Object Oriented Terms

The C Programming Language course syllabus associate level

Embedded Programming in C/C++: Lesson-1: Programming Elements and Programming in C

Embedded Component Based Programming with DAVE 3

Embedded Software development Process and Tools:

Serial Communications

C Programming. for Embedded Microcontrollers. Warwick A. Smith. Postbus 11. Elektor International Media BV. 6114ZG Susteren The Netherlands

Last Class: OS and Computer Architecture. Last Class: OS and Computer Architecture

C++ Programming Language

PC Base Adapter Daughter Card UART GPIO. Figure 1. ToolStick Development Platform Block Diagram

C++ INTERVIEW QUESTIONS

Digitale Signalverarbeitung mit FPGA (DSF) Soft Core Prozessor NIOS II Stand Mai Jens Onno Krah

Fast Arithmetic Coding (FastAC) Implementations

Object Oriented Software Design II

Molecular Dynamics Simulations with Applications in Soft Matter Handout 7 Memory Diagram of a Struct

Storage Classes CS 110B - Rule Storage Classes Page 18-1 \handouts\storclas

This tutorial material is part of a series to be published progressively by Doulos.

Freescale MQX USB Device User Guide

AN1754 APPLICATION NOTE

Freescale Variable Key Security Protocol Transmitter User s Guide by: Ioseph Martínez and Christian Michel Applications Engineering - RTAC Americas

An Implementation Of Multiprocessor Linux

Object Oriented Software Design II

Programing the Microprocessor in C Microprocessor System Design and Interfacing ECE 362

Early Hardware/Software Integration Using SystemC 2.0

Using the HCS12 Serial Monitor on Wytec Dragon-12 boards. Using Motorola s HCS12 Serial Monitor on Wytec s Dragon-12 boards

Operating System Manual. Realtime Communication System for netx. Kernel API Function Reference.

Moving from CS 61A Scheme to CS 61B Java

Java Interview Questions and Answers

Real-Time Systems Prof. Dr. Rajib Mall Department of Computer Science and Engineering Indian Institute of Technology, Kharagpur

Introduction to Embedded Systems. Software Update Problem

8-bit Microcontroller. Application Note. AVR134: Real-Time Clock (RTC) using the Asynchronous Timer. Features. Theory of Operation.

6.088 Intro to C/C++ Day 4: Object-oriented programming in C++ Eunsuk Kang and Jean Yang

Virtuozzo Virtualization SDK

Computer Organization and Components

(Cat. No SI) Product Data

Comparing RTOS to Infinite Loop Designs

Software design for self-sustaining embedded systems

The programming language C. sws1 1

Mutual Exclusion using Monitors

EMBEDDED C USING CODEWARRIOR Getting Started Manual

Embedded Systems. Review of ANSI C Topics. A Review of ANSI C and Considerations for Embedded C Programming. Basic features of C

Software development and debugging for NXP ARM7 MCUs

Variable Base Interface

Using C to Access Data Stored in Program Space Memory on the TMS320C24x DSP

MS Active Sync: Sync with External Memory Files

Chapter 13. PIC Family Microcontroller

AN3307 Application note

Operating Systems 4 th Class

Application Note: AN00141 xcore-xa - Application Development

Hello and welcome to this presentation of the STM32L4 Firewall. It covers the main features of this system IP used to secure sensitive code and data.

Designing a Home Alarm using the UML. And implementing it using C++ and VxWorks

Top 10 Bug-Killing Coding Standard Rules

Exception and Interrupt Handling in ARM

EE 472 Lab 2 (Group) Scheduling, Digital I/O, Analog Input, and Pulse Generation University of Washington - Department of Electrical Engineering

MODULE BOUSSOLE ÉLECTRONIQUE CMPS03 Référence :

N3458: Simple Database Integration in C++11

A C Test: The 0x10 Best Questions for Would-be Embedded Programmers

Von der Hardware zur Software in FPGAs mit Embedded Prozessoren. Alexander Hahn Senior Field Application Engineer Lattice Semiconductor

Automating with STEP7 in LAD and FBD

Automating witfi STEP7 in LAD and FBD

8051 MICROCONTROLLER COURSE

Volume I, Section 4 Table of Contents

Timer Value IRQ IACK

Microtronics technologies Mobile:

Helping you avoid stack overflow crashes!

Code Qualities and Coding Practices

TivaWare Utilities Library

No no-argument constructor. No default constructor found

ECU State Manager Module Development and Design for Automotive Platform Software Based on AUTOSAR 4.0

Tutorial - Creating Your Own Applications

An Incomplete C++ Primer. University of Wyoming MA 5310

An Introduction to the ARM 7 Architecture

Interfacing an HTML Form to the ez80f91 MCU

SYSTEM ecos Embedded Configurable Operating System

Help on the Embedded Software Block

Quick Start Tutorial. Presentation Tutorial for a Quick Start Handson Session: Creating a simple Project using PWM and Count Apps.

1 Abstract Data Types Information Hiding

C++ Overloading, Constructors, Assignment operator

Transcription:

C to C++ Migration for Embedded Systems A workshop on C to C++ migration and creation of classes that represent a type of On- Chip HW-Peripherals. Abstract Using object oriented programming in embedded systems is still not widely employed. Many programmers in this area either come from an electronical background or are pure computer scientists. This article focuses on one of the many drawbacks that result from this lack of integrated training of embedded software development. In particular this article discusses one role that OO programming can play in embedded systems while maintaining requirements concerning safety and reliability that are often imposed by standards like MISRA. Using an example I will show how good object-oriented design of embedded software can result in many benefits: smaller code, improved ease of source-code maintenance but a slight trade off in terms of performance. For each step of the process example code is provided. It consists of an initial timer module written in C, that services timer units that are provided by nearly every MCU. Several steps illustrate a safe path for migrating this source code into an object oriented class. An instance of this class then is the HW-Timer-Peripheral with a much nicer SW- Interface than registers and interrupts. Simply instantiate the timer class once per HW- Timer that is available on the particular MCU. This model 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. Content This article deals with an object oriented (OO) approach to controlling HW-peripherals. So, in fact, I am going to discuss two things: 1. OO-migration in general and 2. the more specific subject of how to deal with HW-registers and interrupt service routines in an OO approach (how to attach global resources to class instances). 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-chiptimers these examples can be ported to other platforms, too. Here are the steps to go through: Step 0 Presentation of the initial C-Project to start with

Step 1 Use the C++-compiler instead of the C compiler, use C++, but no object orientation yet. Step 2 Create a simple class with Constructor and Initialization functions, only. In this stage and the following ones - there will be a mix of C and C++. I will discuss the this-pointer and static object memory allocation. Step 3 Convert all functions (except the ISR) to class member functions. All previous forward declarations become private member functions. All functions so far published in the header-file become public members. Step 4 Convert the Interrupt-Service-Function to a class member-function. An ISR has to be static. So that it can refer to an object instance I ll illustrate how several static ISR-functions can call back into a common class-member-isr for separate classinstances. Step 5: Turn all previous global module-variables into protected member variables. Step 6: Create multiple class-instances that represent separate HW-entities. This step deals with keeping HW-instance specific registers separate in a generic way. At this stage we have fully converted the module into a class. We ll try it out by declaring separate objects and testing them. Finally Measurement and Discussion Step 0 a standard Timer-Module in C Let me shortly explain the functionality and idea of the timer-module before we come back to the topic. 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 projects various timer-functions are registered with the timer-module and will then be called back when their time has come. The timer module is based on a single HW-timer and each registered timer-callbackfunction has its own time interval. This is very similar to the work of a cyclic-taskscheduler.

App Module A Timer Module Timer Registration 1 a InitA TimerCallbackA HW-Timer Event x Timer ISR works like a scheduler 2 App Module B b InitB TimerCallbackB Fig 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. // timer.h // declare a type for Timer- Callbacks typedef void (*TimerCallbackPtr) (); void TimerInit(void); int TimerCreate(WORD ms_interval, TimerCallbackPtr pcallback); BOOL TimerDelete(int timerno, TimerCallbackPtr pcallback); Listing 1 essential timer module interface Listings 1 and 2 show excerpts of the interface and implementation which realize this concept of a timer module. Now there is a module that represents a single HW-timer and provides many SWtimers. 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.

//Timer.c // forward declarations WORD Gcd(WORD a, WORD b); // calculate greatest common divider void TimerTx_ISR (void) irq; // ISR // prepare lists or structures for callback & cycle information // declare parallel arrays for timer TimerCallbackPtr g_pontimer[max_callbacks_timer]; // callbacks DWORD g_timerinterval_ms[max_callbacks_timer]; // intervals WORD g_timergcd; // greatest common divider void TimerInit() // initialize arrays of function callback pointers for (timeridx=0; timeridx<max_callbacks_timer; timeridx++) g_pontimer[timeridx] = NULL; // set up timer HW T0MCR = 3; // Interrupt and Reset on MR0 T0TCR = 0; // Timer0 Enable // set up interrupt int TimerCreate(WORD ms_interval, TimerCallbackPtr pcallback) // adds a new callback pointer to internal array // remember this timers info g_pontimer[timeridx] = pcallback; g_timerinterval_ms[timeridx] = ms_interval; g_timercallbackcnt++; // count it // calculate new greatest common divider BOOL TimerDelete(int timeridx, TimerCallbackPtr pcallback) // remove timer from array // calculate new greatest common divider void TimerTx_ISR (void) irq // restart HW-timer // check which callbacks want to get called this time for (timeridx=0; timeridx<max_callbacks_timer; timeridx++) if (g_timercurloopcnt[timeridx] == g_timernoloopsggt[timeridx]) // call this timers callback g_pontimer[timeridx](); g_timercurloopcnt[timeridx] = 1; else // jus count the loop g_timercurloopcnt[timeridx]++; 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. becomes 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 like the cake and the 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? 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 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. 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 shows the new class-interface. Lets declare a single instance of the class (global var in main.cpp): class Timer protected: public: // constructor Timer(BYTE timerno); ; void Init(); Listing 3 initial class interface 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. Timer::Timer(BYTE timerno) // do time independent // initialization here void Timer::Init() // do time dependant // initialization here 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) 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 was quite simple. So far we have a class with methods but no data. All class methods use global data. class Timer protected: WORD Gcd(WORD a, WORD b); void IntEnable(void); void IntDisable(void); void Start(); void Stop(); void CalculateInternals(void); public: // constructor Timer(BYTE timerno); void Init(); ; int Create(WORD ms_interval, TimerCallbackPtr pcallback); BOOL Delete(int timerno, TimerCallbackPtr pcallback); 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 declare static 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 SET_VIC_VECT_ADDR(TIMER_ILVL, Tx_ISR) 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, just the ISR complains. nonstatic reference must be relative to a specific object. class Timer I admit, this let s worry later of step 4 protected: didn t last long here. This is because the ISR uses member-variables now static void T0_ISR (void) irq; void Tx_ISR (void); (instead of global vars). Consequently it wants a this-pointer and cannot be public: 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) Listing 6 static & non-static ISR at the same time. Timer* ptimer0; 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 listings 6 & 7. In this step I also changed the variable prefixes g_ (for global var) to m_ (for member var). Timer::Timer(BYTE timerno) switch (timerno) case 0: ptimer0 = this; break; default: // todo: show error break; SET_VIC_VECT_ADDR(TIMER_ILVL, T0_ISR) void Timer::T0_ISR (void) irq ptimer0->tx_isr(); void Timer::Tx_ISR (void) // as before Listing 7 store object pointer globally and call class ISR Step 6: 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. WORD m_timerchannel; volatile unsigned long* m_ptxmr0; volatile unsigned long* m_ptxtcr; volatile unsigned long* m_ptxir; static void T1_ISR (void) irq; 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 listings 8 and 9.

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. Finally we create a second instance of the timer (in main.c) and use the second timer. The test shows how these two timers work well. Conclusion & discussion 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. Timer* ptimer1; 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; void Timer::T1_ISR (void) irq ptimer1->tx_isr(); The differences in terms Listing 9 initialize correct HW, pointers to SFRs, 2 of SW-Design, ease of nd ISR 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. C-module 2 C-modules CPP class Code size 3548 5008 3772 Bytes Data size 1544 1792 1772 Bytes Time in ISR 7.22 6.79 9.12 µs 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 2 nd 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 at King s College - University of London. His background and experience ranges from SW- Design and development to electronics of embedded systems. He has recently developed a Data Centric RTOS for MCUs and can be contacted at dbraun@cleversoftware.de