Introduction
Recently I had the opportunity to develop an electronic fuel injection system for a race car. I found it helpful to use an object-oriented approach when designing the embedded software. I wanted to use C++ as the programming language but, unfortunately, no compiler was available for my target processor. After finesseing a number of issues, I figured out a way around this problem. This article describes how C++ helped out the design and implementation of my embedded software and what I had to do to run my code on a processor for which no C++ compiler existed.
My fuel injection system was built to control the engine on a Formula-style race car developed at Rensselaer Polytechnic Institute. When I first considered the design problem I saw the system in terms of the mechanical control objects (fuel injectors, sensors, ignition coils, etc.) and how these objects interacted to produce a running engine. It seemed a natural step to create a software design in which each of the mechanical objects in the system was represented in code by a software object. I could then use my knowledge of electronic and non-electronic fuel injection systems to guide me in my design of the software. The software objects, their organization, and their communications would mimic that which I knew about existing physical systems.
I also used objects to represent virtual devices. For example, a "distributor" is a rotating mechanical switch found on most older automobile engines that determines the firing order of the spark plugs. It may perform other actions, such as determining the precise timing of the spark. The decisions about "what should happen next and when" are performed by mechanical gearing. Although my engine had no distributor, I found it convenient to have a software Distributor object that was responsible for determining "what should happen next and when."
The software object was "geared" to the engine by an electronic position signal generating an interrupt. By basing it on a physical device whose operation was well known to me it was clear what the software object's responsibilities were. Following this process, I found it easy to arrive at a set of classes amongst which I could partition the functionality of the system.
I extended the use of objects to represent features of the processor too. I used the Motorola 68HC16 microcontroller. It has a number of built-in hardware modules that make it useful for this type of application, such as A/D converters, timers, and counters. Rather than have higher-level objects deal directly with hardware registers, I chose to use objects as intermediaries. There was some benefit to this in that it created a less cryptic interface to the hardware. A major benefit for the future is that it allows for easy simulation of the running system. By replacing these objects with derived objects that simulate the hardware, the software could be recompiled on, say, a PC and run as a normal application independent of the target hardware. Encapsulating the hardware in this fashion also facilitates moving to a different processor.
The concept of inheritance was important for the future. I anticipated new mechanical devices replacing older devices, requiring changes to the software. For example, fuel injectors come in a variety of different flow rates and open/close timing characteristics. I knew that the injectors originally specified for this engine could change for cost or performance reasons. It made sense to have a software Injector class defining the standards for communicating with the generic object and to develop derived classes encapsulating the particulars about the injector models they represented.
Inheritance was also an important concept for many of the software classes not tied to physical devices. Several types of objects needed to be able to catch interrupts; another group would be involved in round-robin multitasking. This gave rise, respectively, to the InterruptHandler and Process classes.
I chose C++ as the programming language for a number of reasons. First, of course, was my desire to use a language I found effective at expressing object-oriented designs. I had successfully coded an earlier verision of this system in assembly language but I found writing object-oriented code without explicit language support awkward at best. The C++ class construct solved this problem.
Significantly, I needed a language familiar to future system maintainers. I knew the code would continue to exist in a college environment. It is increasingly rare to find students comfortable with assembly language, whereas C and C++ are taught in a number of classes. I also envisioned a time when mechanical engineering students with a minimal background in C would want to write simple programs to run on the system. Since C++ subsumes much of C, it would be reasonable for these students to write simple functions that could be inserted into the code by a computer engineering student. Finally, no other languge with object-oriented features was available for the 68HC16. Strictly speaking, neither was C++, but I figured out how to get around that with the help of a C++-to-C translator.
Using C++ Without a Compiler
Getting my C++ code into the processor wasn't easy. Introl Corporation generously donated a complete C development system for the 68HC16. I found the cfront C++-to-C translator as part of the Glockenspiel C++ development system for the PC. But it wasn't as simple as running cfront first, then compiling. These issues had to be addressed:
- heap management
- far and volatile variables, unions, and structs
- catching interrupts with objects through virtual functions
- calling the constructors for static objects
- source-level debugging
cfront translates programs written in C++ into C. The resulting C code can then be compiled by a C compiler to produce executable code. The C code can be somewhat abstruse, as the following example shows. Listing 1 shows the original C++ code, and Listing 2 shows it after being translated into C.
It's often fascinating to run cfront on a C++ program, because it can give you insight as to how your code is really implemented. For example, take a look at how the constructor of the class Frog gets translated into the function _Frog__ctor. Notice the this pointer, always implied as the invisible first parameter in any C++ member function, but here shown explicitly in the C translation. Now look at the first statement in the function, an if statement that was not in the C++ code. This if statement, inserted by cfront, takes care of object initialization when the object is dynamically created on the heap. If the this pointer is null, the object has not yet been created, so it calls _new to allocate storage for the new object.
cfront presumes the existence of _new and _delete functions for heap management. It also presumes the existence of the functions _vec_new and _vec_delete for creating and properly initializing vectors of objects on the heap and for deleting them. exit is another function cfront expects, which should cause the program to terminate. The required operation of _new and _delete is shown in Listing 3.
You must supply these functions; cfront does not. Although they can be based on standard library functions like malloc and free, I chose to write my own because of the unique requirements of my system. _vec_new and _vec_delete need not be unique, so I show them in Listing 4.
Using far Variables
As required by the bank-switched memory architecture of the 68HC16, the Introl C compiler supports the declaration of "far" variables. A variable representing a hardware register is the most common example of a far variable in my software. Unfortunately, cfront does not recognize the keyword far because it is not part of the AT&T C++ language. I had to circumvent cfront by using a special typedef in the C++ code and then fixing up the C code after it came out of cfront but before it was compiled. For example, many of my far variables were 16-bit words. I created the following typedef:
typedef unsigned short int WORDfar;Any far WORD variables were declared as WORDfar. The string WORDfar is preserved by cfront, for example:
typedef unsigned short int WORDfar; int main() { WORDfar w; ..... }becomes:
typedef unsigned short WORDfar ; int main () { { WORDfar _au1_w ; ..... } }I then used a search-and-replace utility to go through the .c file, looking for the string WORDfar and replacing it with WORD far. For far structs and unions I needed a slightly different technique. I prefixed all such variables with a unique string (like CPU16_), then did a search and replace (to far CPU16_) after cfront ran.
Volatile variables were handled similarly. Hardware registers should be declared volatile so they aren't affected by optimization, but the volatile keyword was not preserved across cfront. Again, by using a unique prefix on the name of each such variable I was able to perform a search and replace to insert the volatile keyword.
Capturing Interrupts
I wanted certain objects to be able to catch an interrupt as if the interrupt caused a virtual member function to be called. Introl C provides function-name qualifiers that will turn a normal C function into an interrupt service routine so that the processor state is properly saved and restored. However, these qualifiers would not get past cfront. A jump through a virtual table is also entirely unsupported by normal interrupt processing. To get around this, I created an InterruptHandlerClass, defined as shown in Listing 5.
The 68HC16 supports 252 interrupt vectors. For those vectors I wished to capture, such as vector 0xF1, I wrote the code shown in Listing 6. The address of function InterruptF1Func is placed at the appropriate memory location in the 68HC16 interrupt vector table by instructions to the linker. Then, when interrupt 0xF1 occurs, the processor vectors directly to the function InterruptF1Func. _InterruptHandlerClass_FirstHandle is the mangled name of function InterruptHandlerClass::FirstHandle. Notice that pInterruptF1HandlerObject is passed as the first (and only) parameter, thereby becoming the this pointer.
Listing 7 shows a simple class derived from InterruptHandlerClass, which responds to interrupt 0xF1. The base class function InterruptHandlerClass::FirstHandle calls InputCapture1Class::Handle via the virtual table when the interrupt occurs.
Constructing Static Objects
My software contained a number of static objects declared the file scope. These objects have their constructors called before main. It is the responsibility of the linker to generate startup code that calls all the constructors for static objects. Being C++-unaware, the Introl linker has no knowledge of this requirement and cannot do it automatically. cfront helps out as far as it can. For every module that contains static objects it creates a function called _STI that calls the constructor for each static object. For example, if the class Frog example above were changed to:
// a static "Frog" Frog Kermit(Green); int main() { }cfront would add the following to the C translation:
struct Frog Kermit ; static void _STI (){ _Frog__ctor ( & Kermit , 1) ; }This happens for each separately compiled module in which static objects are declared with file scope. A linker aware of this convention would add startup code that would call the _STI functions from all the modules in some order before calling main.
Since there were several _STI functions created in this fashion, I couldn't simply change the startup code to call _STI before main. I noticed certain Introl compiler qualifiers that would put variables in special sections in the object code modules. The Introl C compiler segregates code in a familiar fashion, putting executable code in a .text section, uninitialized variables in a .bss section, etc. During the linking process each of these sections is placed starting at a different physical address depending on the linking instructions. This allows control over exactly where things get placed in memory.
It turns out that if a given variable is preceded with the __mod1__ qualifier, it gets placed in a special section called .mod1. This solved the problem. For each _STI function I created a function pointer variable initialized to point to that function. All of these function pointer variables were declared with the __mod1__ qualifier. Since these were the only variables placed in the .mod1 section, I could have my startup code loop through all the entries in that section, calling each function in turn.
To illustrate, once I had translated a C++ module to C, if it contained static objects I appended the following code fragment:
typedef void (*pSTI_t)(); static __mod1__ pSTI_t _pSTI = _STI;I then compiled. When linking, the linker would coalesce all .mod1 sections from all object modules. Only the _pSTI pointers to the _STI functions would be found here, so the .mod1 section would end up being an array of these pointers. I changed the startup code, adding a simple loop that went through the .mod1 section calling the functions whose addresses it found there.
cfront also creates functions named _STD that call the destructors for static objects when main exits. The nature of my system is such that static destructors, if they existed, would never be called. I therefore ignored this case. If it were necessary, a similar technique could be used to call these _STD functions.
Debugging
True source-level debugging of the C++ code on the target was not possible. No information useful to the Introl C debugger was preserved across cfront relating C++ source lines to C source lines. Nonetheless, using Introl's source-level debugger on the translated C code was still productive. I found it convenient to run the C code through a pretty-print utility first. This literally put the code into a recognizable shape (coming out of cfront it looks like a bunch of run-on sentences). The name mangling may take a little getting used to, as you can see from the class Frog example above. Of course, if you want to look at the value of a variable, you have to specify the fully-mangled name, but it works. Even though the Introl debugger is not designed to understand objects, it still understands structs, so I could type print *this and see all of the data members of the current object. Once I had overcome these small hurdles, debugging was only slightly messier than usual.
Summary
All of the backflips just mentioned to get things to work were efficiently managed by batch files. An annotated sample batch file is provided on this month's code disk. It calls certain utility programs and filters that are easily written or found.
C++ turned out to be a good choice for this project. The object-oriented design has proven to be easily communicated to newcomers because it mimics a physical system with which they are already familiar. It seems to be a robust design, having required no high-level design changes despite a significant extension of functionality since inception. Although difficult for the complete neophyte, the code has shown itself to be maintainable by students with some C++ and C background. This is especially significant for embedded systems as it is increasingly hard to find assembly language programmers. I have shown how cfront and a C compiler can be used together to run C++ code on a target for which no C++ compiler exists. These techniques can get you going while waiting on a "real" development system or where the advantages of C++ overshadow some code-development awkwardness.
Ed Lansinger is a computer and systems engineer at General Motors working on exciting new features for the Cadillac Northstar powertrain. He has worked on embedded systems as well as PC application software for the past eleven years. Ed can be reach via the Internet at lansie@rpi.edu.