Lightweight Objects in Embedded C

Although C doesn’t have full object-oriented language constructs, there is nothing keeping us from doing true OO implementation. Some attempts use heavy-weight patterns that abuse the preprocessor and take a significant hit on efficiency. All just for the sake of making C look something like C++… which is hardly a real benefit.

In our experiences of using straight C for embedded development, and applying TDD as well, we have used a fairly simple pattern that has minimal hits on efficiency, and requires no dynamic memory allocation.
 In order to provide encapsulation and testability of an object, it frequently makes sense to break them up into multiple source and header files. The basic pattern is as follows:

Package Foo:

  • Module Foo (Foo.h, Foo.c)
    • Public interface
  • Module Foo_Defines (Foo_Defines.h)
    • Shared common types, macros and constants
  • Module Foo_Data (Foo_Data.h, Foo_Data.c)
    • Optional internal module package datastore
  • Module Foo_State (Foo_State.h, Foo_State.c)
    • Optional module for managing complex state behaviors and transitions
  • Module Foo_Regs (Foo_Regs.h, Foo_Regs.c)
    • Optional module for interfacing to processor registers

When performing true Test-Driven Development, it is best to always start with the entry point Foo module, and add the other only as deemed necessary. This is not meant to be a hard restriction on modules. Other pertinent modules should be added when and if they make sense.

All external objects should only call in to module Foo via the public interface provided via module Foo. If access to any method and/or data in any internal module becomes necessary, an accessor should be added through Foo.

A very common instance of this is initialization. If any module requires initialization, its module _Init() method should be called from Foo. This results in very little refactoring of the larger system if new state elements need to be managed. For example, if Foo_State holds a state variable, Foo_Init() should call Foo_State_Init(). Any sequencing of initialization is then fully isolated an contained in Foo.

Finally, this is just a recommendation that comes from lots of real-world experience. If you are writing in C, you likely have some resource limitations. Otherwise, you would be writing in at Java, Ruby or some other high-level language. Use common sense, and develop your own patterns based on real needs.