Design Con 2015

Objects? No, thanks! (Using C++ effectively on small systems)

Wouter van Ooijen, Hogeschool Utrecht

February 15, 2014

Wouter van Ooijen, Hogeschool UtrechtFebruary 15, 2014

My field of interest is using C++ on small microcontrollers. Such chips are traditionally written in C or even assembler, but in my opinion C++ has much to offer, if used appropriately. In this article I will show how using static class templates can be used to get compile time flexibility without paying a price in size or speed.

I will use the blinking of an LED as an example, because it is the very first thing a microcontroller programmer does on a new chip, and it has two parts: the blinking, and the manipulation of the IO pin. These two aspects beg to be handled separately, otherwise writing N such algorithms for M chips would require N x M specific programs. Separating the two aspects reduces the effort to N algorithms plus M pieces of pin access code.

As an example, I’ll use an LPC1114FN28 chip with a Cortex M0 CPU, 4K RAM, 32K ROM. To blink an LED on pin 0 of port 1 of this chip a C programmer might write something like the following code. Various details, like configuring the pin for GPIO, setting its direction to output, and realizing the delay() function, have been omitted to concentrate on the blinking and the handling of the pin. The GPIOREG macro provides access to an IO pin related register; the PIN_SET macro does the actual setting of the pin. The main() calls these two macro’s in a loop, interspaced with delay() calls.

// typical blink-an-LED in C

   // return the pointer to an IO port related register
#define GPIOREG( port, offset ) \
   (*(volatile int *)( 0x50000000 + (port) * 0x10000 + (offset)))

   // set the pin port.pin to value   
#define PIN_SET( port, pin, value ) \
   { GPIOREG( (port), 0x04 << (pin) ) = (value) ? -1 : 0; }

int main(){
   for(;;){
      PIN_SET( 1, 0, 1 );
      delay();
      PIN_SET( 1, 0, 0 );
      delay();
   }
}


Both macros might seem pretty complicated, but the compiler can translate a call of PIN_SET with literal arguments into just a few machine instructions. Note in particular that the address arithmetic and the choice implied by the ? operator have been eliminated because the input values are constants.

// assembler listing fragment for the blink-an-LED for(;;){ . . . } loop

.L97:
    ldr    r4, .L98      // get the I/O address
    mov    r3, #1        // get -1 into r3
    neg    r0, r3
    str    r0, [r4]      // store -1 in the I/O register
    bl     delay
    mov    r1, #0
    str    r1, [r4]      // store 0 in the I/O register
    bl     delay
    b      .L97

Now consider how a C++ library might help programmers of such a chip by providing a nice OO abstraction for a pin of a microcontroller. A function could be provided that does essentially the same thing as the PIN_SET macro (and by providing it in a header, the same optimizations would be possible), but this does not make a pin something that can be treated as an abstraction. For instance, the fact that a pin on the LPC1114 is characterized by its port and pin number does not hold for all IO pins on all chips. Instead a pin is, in true OO fashion, represented by an object that implements an abstract interface. For this simple case the interface contains only one method: set( bool ), which makes the pin either high or low, depending on the value passed. A concrete class derives from this interface class and implements the method.

// an OO interface for a pin, and an LPC1114 implementation

struct pin_out {
   virtual void set( bool ) = 0;
};

volatile int & gpioreg( int port, int offset ){
   return * (volatile int *) ( 0x50000000 + port * 0x10000 + offset );
}

struct lpc1114_gpio : public pin_out {
   const int port, pin;
   
   lpc1114_gpio( int port, int pin )
      : port( port ), pin( pin ){}
   
   void set( bool x ){
      *gpioreg( port, 0x04 << pin ) = x ? -1 : 0;
   }
};


With this abstraction, code can be written that manipulates a pin without knowing the details of how this is done. The blink function below takes a pin_out as parameter. It is fully decoupled from the concrete pin and the actual operations needed to make the pin high or low. This nicely breaks the N x M problem. The blink application now only has to create the pin object and pass it to the blink function. This could of course be done in a single line, but it is good style to give the object a meaningful name.

// blinking the LED using a pin object

void blink( pin_out & pin ){
   for(;;){
      pin.set( 1 );
      delay();
      pin.set( 0 );
      delay();
   }
}   

int main(){
   lpc1114_gpio led( 1, 0 );
   blink( led );
}


If this code would be as efficient as the C code shown earlier, the decoupling of the pin access from the algorithm would probably be welcomed by microcontroller programmers. Or, at the very least, they would not discard this approach, and C++ with it, on first sight. But alas, there is a price to be paid for this abstraction in RAM use, code size, and speed.

Each concrete object that inherits from pin_out has at least a virtual function table pointer (4 bytes on a 32-bit CPU), and probably some more information that identifies the point. In the LPC1114 implementation, two integers are used (4 bytes each). This could be easily reduced to one or two bytes, but on a Cortex CPU objects must be 4-byte aligned, so the practical minimum size for a pin object is 8 bytes.

The LPC1114DFN28 chip has 4K RAM and 22 pins that can be used as output. When objects are created for all 22 pins, this would claim 8 x 22 = 176 bytes, which is 4% of the available RAM. To some this might seem insignificant, but it is a good argument for C programmers that C++ is wasteful of resources.

The use of RAM can be avoided by creating the pin objects in ROM, of which a microcontroller generally has more. A problem with this is that it forces all pin-using algorithms to work with const objects, which makes it difficult to do interesting things in a pin that require state - for instance optimizing writes by writing out only changes.

// OO pin abstraction and implementation with const objects

struct pin_out {
   virtual void set( bool ) const = 0;
};

struct lpc1114_gpio : public pin_out {
   int port, pin;
   
   constexpr lpc1114_gpio( int port, int pin )
      : port( port ), pin( pin ){}
   
   void set( bool x ) const {
      gpioreg( port, 0x04 << pin ) = x ? -1 : 0;
   }
};

void blink( pin_out const & pin ){
   . . .
}  

int main(){
   constexpr lpc1114_gpio led = lpc1114_gpio( 1, 0 );
   blink( led );
}

The example pin_out abstraction has only one method. A realistic one would have many more: for configuring the pin as GPIO, for setting the direction, for reading the level, for enabling and disabling the weak pull-ups, etc. These methods would all be virtual in the abstract pin class, and implemented (probably differently) in each concrete pin class. Current C++ compilers are not good at optimizing away unused virtual method implementations, so all methods for all pins are going to be present in the ROM image. For a desktop, code size is almost a non-issue these days, but for a small microcontroller it can be a killer. This issue would discourage a library author from providing a rich interface for a pin. Alternatively, it would force a microcontroller programmer not to use such a library, because the compiled code would not fit in his target chip.

The fact that led.set() is a virtual method call means that it is somewhat slower than a plain method or function call. The overhead is only a little arithmetic and an indirect call, but when the method is just a few assembler instructions this overhead is significant. Much worse, because the compiler in general can’t know which method is called, it can’t inline the method code, and hence can’t apply constant folding, dead code elimination, and other nice optimizations. The simple action of making a pin high or low now takes much more code and hence more CPU cycles than it ought to compared to the C version.

Now take a step back. WHY does the OO version have so much overhead compared to the C version? The crucial difference is that in the OO version blink() can be called with any pin object, whereas the C version is for a specific pin. This might sound like a big advantage to an OO programmer, but for a small microcontroller the use of its pins is generally fixed by the hardware it is part of. It would take at least a soldering iron to connect the LED to a different pin. If we are going to do that, we might as well re-compile the software for the new pin too.

< Previous
Page 1 of 2
Next >

Loading comments...

Parts Search Datasheets.com

KNOWLEDGE CENTER