OO is a mind-set, a design style. We can design OO firmware, as long as our style favours minimalism rather than the baroque. It’s desirable for our implementation language to offer native support for OO, but it isn’t essential. We can implement an OO firmware design in assembly language if necessary. OO is just a way of working.
The interesting thing about OO firmware is how we implement it. Our team chose not to use C++ because, at the time we were making these decisions, it was ten years since I’d written C++ professionally, and I was the only member of the team with any C++ experience at all.
C++ is the most complicated language I’ve ever encountered. It’s a high-ish level OO language that can still connect directly to the hardware, which is important in firmware. But it takes a lot of time and effort to learn to use C++ well. Read this book to see what I mean. [It’s a Very Good Book, by the way.]
We chose to stick with C, and I call our approach classes-in-C. Simplicity is a wonderful thing in firmware, and classes-in-C is nothing if not simple. The OO features we really need for firmware are easily implemented in C, so this simplicity comes quite cheaply.
Each .c file encodes a class, and usually the one and only instance too. Private class data is implemented as static class variables. [Static variables are persistent, so they continue to exist, and retain their values, in between invocation of the class task(s).] Private class methods are declared and defined within the .c file. The class API comprises the methods defined in the class header file, where we also include any necessary typedefs and symbolic constants.
Our RTOS used co-operative multi-tasking, which executes each task to completion before executing the next. This allows a task owned by one class to call the API methods of another class. Pre-emptive multi-tasking simulates continuous execution of all tasks, so a call to an API method could occur while a non-interruptible operation is taking place within the class/instance. This is not an insuperable problem, but it is a difficult one, and it can be completely avoided by using the simpler and quicker method of task-switching. We only need to ensure that no task runs for too long, closing out the others.
Identifying classes in firmware is often quite straightforward. We need a suite of peripheral driver classes, one for each device type. We made a rule that each peripheral device is owned by exactly one class. This has the happy side-effect of encapsulating interrupt processing inside the driver class, where the thread-safety aspects are handled, contained and hidden. [In the unusual event that two or more classes needed access to the same peripheral device, we just added a new node (class) to arbitrate incoming access requests, and share the peripheral device out as required.]
An API method and a table of function pointers serves to implement the Command pattern quite effectively. We have no real need of polymorphism. And so we found it for the other OO features that OO languages support natively. As simple as our implementation is, we found that we could use any software concept or convention that was truly appropriate in a firmware context. Using C did not seem to place any significant constraints on our designs, or on our choice to use an object-oriented design approach.
I have seen some quite clever schemes to emulate more native OO support in C than we had. They all seemed to rely heavily on macros, which I don’t like because a symbolic debugger can’t execute a macro. We all try to avoid debugging at this level, but sometimes we have to, and it isn’t good if you can’t follow the execution of the code properly. You can guarantee that the rogue write-operation you’re searching for will happen in the macro code, where we can’t see it! Have you had any success with schemes like this?
This post may be edited to correct it, or to expand on points made in comments. Please leave your own, and join our discussion!