Making things do stuff – Part 3

Previously, we’ve looked at the basics of hardware manipulation in C++ and applied that to a simple GPIO implementation.

In this article we’re going to have a look at encapsulating our code in a class and look at some of the design choices we have to make.

If you’re not familiar with hardware manipulation in C++ I’d highly recommend reading the previous articles in this series before continuing.

Even if you’re familiar with the concepts of hardware manipulation, if you haven’t read the second article I’d highly recommend reviewing it so your familiar with the problem we’re tackling.

An object-based approach

Hardware devices lend themselves nicely to an object-based approach.  Each hardware device has an equivalent software object for accessing it; there is a one-to-one mapping between devices and objects.

Encapsulating device access within a class gives us a number of other benefits:

  • The member functions decouple the actual access from the application. We can easier separate Application Programming Interface (API) from implementation
  • Constructors can be used to initialise the device, removing the need for the client to explicitly do so. Destructors can be used to place the device back into a ‘safe’ state.
  • We can support multiple devices.
  • We can use specialisation (inheritance) to provide ‘families’ of devices.

Interface design

Before we look at implementation it’s worth taking some time to explore our client interface (API). We (generally) want to provide an interface that is simpler than the underlying implementation (otherwise, what would be the point?).  This means not only encapsulating implementation details, but also restricting the allowable behaviour of the device.

For our GPIO class we have a couple of options:

Bitmask-based member functions

In this interface design the client provides bit masks that specify which bits are to be set / cleared.  This interface allows multiple bits to be set / cleared in one call.

Clients can perform bitwise operations on the GPIO device as if it were a single abstract memory location; the fact that there are multiple registers being manipulated is hidden from the client.

class GPIO
{
public:
  void direction(std::uint32_t bitmask);     // 0 => input
                                             // 1 => output

  GPIO& operator=(std::uint32_t bitmask);
  operator uint32_t();

  GPIO& operator &= (std::uint32_t bitmask);
  GPIO& operator |= (std::uint32_t bitmask);
};

From a client perspective using the GPIO class looks a lot like using a pointer-based technique.

int main()
{
  // Create a GPIO object, port_D (see later
  // for a discussion on object construction)

  port_D.direction(1 << 15);

  while(true)
  {
    port_D |= (1 << 15);
    sleep(1000);
    port_D &= ~(1 << 15);
    sleep(1000);
  }
}

Pin-based member functions

Here we are restricting clients to manipulating one GPIO pin at a time.  We could perhaps restrict clients even more by using an enumeration for the pins.  For example:

class GPIO
{
public:
  enum Pin
  {
    Pin00, Pin01, Pin02, Pin03,
    Pin04, Pin05, Pin06, Pin07,
    Pin08, Pin09, Pin10, Pin11,
    Pin12, Pin13, Pin14, Pin15,
  };

  void set_as_output(Pin pin);
  void set_as_input (Pin pin);

  void set    (Pin pin);
  void clear  (Pin pin);
  bool is_set (Pin pin);
};

This interface design removes the need for bit calculations from the client.

int main()
{
  // Create the GPIO object, port_D...

  port_D.set_as_output(Pin15);

  while(true)
  {
    port_D.set(Pin15);
    sleep(1000);
    port_D.clear(Pin15);
    sleep(1000);
  } 
}

The Pin class

This idea could be taken further by defining a GPIO Pin class that represents a single pin on a port.

class Pin
{
public:
  enum Direction { INPUT, OUTPUT };

  void direction(Direction dir);

  void set();
  void clear();
  bool is_set();

  Pin& operator=(unsigned int val);
  operator unsigned int();
};

This is the most ‘abstract’ API, treating each physical pin as a one-bit integer.

int main()
{
  // Create a Pin object, blue_LED...

  blue_LED.direction(Pin::OUTPUT);

  while(true)
  {
    blue_LED = 1;
    sleep(1000);
    blue_LED = 0;
    sleep(1000);
  }
}

Choosing an interface

Firstly, there’s no such thing as a ‘perfect’ API.  All interface designs will make assumptions and compromises; design is the act of balancing these things.

As a rule-of-thumb, the more fine-grained control you grant clients the less portable your API is.  That is, allowing fine-grain control generally requires exposing more of the underlying implementation, which is the non-portable part.

For individual bit-manipulation a pin-based (or Pin class) interface is generally easier to use.  However, if your hardware requires multiple bits to be set a bitmask-based interface is more useful.  Often, you can’t know in advance which mechanism will be most valuable to the client so you provide a ‘combination’ interface; for example adding functions for both pin-based and bitmask-based calls.

For this article I’m going to use the pin-based GPIO API since the member function implementations are very close to the code we have seen in previous articles.  I’ll leave it as a perennial ‘exercise for the reader’ to implement the other APIs.

Construction and destruction

There are two problems we need to solve with our GPIO class constructor

  • Identifying a unique hardware port
  • Configuring the hardware for use

GPIO objects map explicitly (and, ideally, exclusively) onto a single hardware device.  We could use the hardware address for the device.  This is certainly unique but requires the client correctly remember a set of arcane, and target-specific, numbers in order to use the device.

Better, perhaps, would be to use an abstract identifier – an enumeration – for the device.

namespace STM32F407
{
  enum device
  {
    GPIO_A, GPIO_B, GPIO_C,
    GPIO_D, GPIO_E, GPIO_F,
    GPIO_G, GPIO_H, GPIO_I
  };
}

I’m putting device-specific elements (there will be more, shortly) in a namespace to isolate it.

We can now use this enumeration as a constructor parameter:

class GPIO
{
public:
  explicit GPIO(STM32F407::device id);
 
  // Other API...
};

Note the constructor is marked explicit.  This is to stop implicit conversion of (random!) integers to GPIO objects.

Inside the GPIO constructor, we need to enable the clock to the particular GPIO hardware device.  A simple function will suffice for this.

#include <cstdint>

// inline function to help declutter code
//
using std::uint32_t;

inline
volatile uint32_t* reg32_ptr(uint32_t addr)
{
  return reinterpret_cast<volatile uint32_t*>(addr);
}

namespace STM32F407
{
  enum device
  {
    GPIO_A, GPIO_B, GPIO_C,
    GPIO_D, GPIO_E, GPIO_F,
    GPIO_G, GPIO_H, GPIO_I
  };

  constexpr std::uint32_t peripheral_base { 0x40020000 };

  inline void enable_device(device dev)
  {
    auto const rcc_register { reg32_ptr(peripheral_base + 0x3830) };

    *rcc_register |= (1 << dev);
  }
}

The call to this function can be made within the GPIO constructor.

GPIO::GPIO(STM32F407::device dev)
{
  STM32F407::enable_device(dev);
}

The destructor on the GPIO could do nothing (the default); or it place the device back into a safe / quiescent state.  In our system one option would be to disable the clock to the hardware device.  A complementary function to the enable_device() function above could be simply written.  Note, however, the GPIO class has to store its device ID as a member in order to call the function.

class GPIO
{
public:
  explicit GPIO(STM32F407::device dev);
  ~GPIO();

  // Other API...

private:
  STM32F407::device ID;
};


GPIO::GPIO(STM32F407::device dev) :
 ID { dev }
{
  STM32F407::enable_device(ID);
}

GPIO::~GPIO()
{
  STM32F407::disable_device(ID);
}

Copy and move policy

Since there is a one-to-one mapping between objects and the physical hardware we have to think carefully about copying (and, by extension, moving) objects.

What does it mean to ‘copy’ a hardware device?  It could mean:

  • Two GPIO objects both refer to the same hardware device (the default copy behaviour)
  • All the configuration of one hardware port is copied to another
  • All the configuration settings and current output values are copied

None of these options is particularly desirable (or safe), so our ‘best’ option is to disable copying; and moving, for similar reasons.

class GPIO
{
public:
  explicit GPIO(STM32F407::device dev);
  ~GPIO();

  // Copy and move policy
  //
  GPIO(const GPIO&)            = delete;
  GPIO(GPIO&&)                 = delete;
  GPIO& operator=(const GPIO&) = delete;
  GPIO& operator=(GPIO&&)      = delete;

  // Other API...
};

When things go wrong

A crucial part of a class’s interface design is what happens when things go wrong.  That is, what is the mechanism by which you report success / failure on an object’s behaviour.  Within an operation there are generally two places you will apply error checking

  • Pre-condition validation – The input parameters to the operation are not acceptable
  • Post-condition validation – The resulting behaviour of the operation is outside some acceptable bounds

When an error condition occurs you have four options:

  • Do nothing
  • Return an error
  • Throw an exception
  • Terminate

Each of these options has a different level of consequence.

Do nothing

Silently ignoring failures when manipulating hardware could be potentially catastrophic to your system.  However, if it is known that the exception does not affect system operation it can be safely ignored without consequence.  In other cases the error condition may be transient (for example, during a system mode change) and it is understood that the error condition will not persist.

Return an error

Returning an error code gives a mechanism for our hardware manipulation code to report back its status.

The design of a function’s interface can affect the explicitness of error handling.  For example, consider the two operations on our GPIO class

class GPIO
{
public:
  error_code set_pin(Pin pin);
  void clear_pin(Pin pin, error_code& err);
};

For set_pin() the error code, being the return value from the function, can be implicitly ignored.  This gives the potential to miss error conditions, which could lead to failures, hazardous system conditions and all sorts of undesirable consequences.

C++17 gives us a mechanism to prevent the ignoring of error return codes.  Marking our function with the [[nodiscard]] attribute encourages the compiler to emit a warning if the return value is not used.

class GPIO
{
public:
  [[nodiscard]] error_code set_pin(Pin pin);
}


int main()
{
  GPIO port_D { STM32F407::GPIO_D };

  port_D.set_pin(GPIO::Pin15);      // WARNING: error_code ignored
}

 

For clear_pin() the  error condition, passed as an input-output parameter, must be supplied by the client (caller) and the compiler will give a warning if the error code is not subsequently read; unless it is explicitly ‘ignored’ by the caller.

Up to C++17 to ignore the unused error_code we would explicitly cast it to void to prevent a compiler warning.  From C++17 we can hint to the compiler that we may not be checking the error_code.

class GPIO
{
public:
  void clear_pin(Pin pin, error_code& err);
}


int main()
{
  [[maybe_unused]] error_code err { };  // We have to supply an
                                         // error_code object, but
                                         // we aren't interested
                                         // in it.

  GPIO port_D { STM32F407::GPIO_D };
  port_D.clear_pin(GPIO::Pin15, err);
  ... 
}

 

Notice, in the case of operator overloads there is no way of returning error codes (without breaking the semantics of the operator overload).

Global error objects

An alternative to error codes, then, is to use some global error condition object that can be updated and checked by clients.

Use of global error objects (and global objects in general) is frowned upon in modern programming, so we’ll say no more of it here.

Throw an exception

Throwing an exception provides a couple of benefits over error codes.  We can exploit the exception handling mechanism of the language to route error conditions to where they can be most appropriately handled.   Client code becomes more explicit, separating behaviour code from failure code.  Also, the behavioural API does not need to be cluttered with error handling

However, the cost of this is an increase in program size; and exception handling is non-deterministic and slow (when exceptions are thrown; there should be no little, or no, run-time cost if exceptions are not thrown).  For this reason many embedded systems explicitly disable exception handling.

More subtly, but arguably far more important, exception handling strategies must be built into your code from the ground up.  Simply adding ad-hoc exceptions to pre-existing code is likely to wreak havoc on the performance, maintainability and extensibility of your system.

Terminate

This is, of course, the extreme option.  Use assert-like code to terminate code on error conditions.  In application code, terminate-on-error will likely lead to code that is frustratingly cumbersome for clients to use.  However, hardware is far more pernickety.  There is little tolerance or error – the hardware is either working or it isn’t.   Terminating code is a ‘reasonable’ option with hardware manipulation; particularly with respect to pre-condition validation

Summary

This time we’ve looked at some of the design choices we have to make when building classes to encapsulate hardware devices.

Notice there are at least four design aspects we have to consider for any new class:

  • The client – behavioural – API
  • The construction /initialisation and destruction interface
  • The copy and move policy
  • Error condition reporting

It’s very easy to get absorbed with the functional API of a class and forget that the other three interfaces can also have far-reaching consequences for your system.

In the next article we’ll delve inside the class and have a look at the implementation techniques and their consequences.

Glennan Carnie

Glennan Carnie

Technical Consultant at Feabhas Ltd
Glennan is an embedded systems and software engineer with over 20 years experience, mostly in high-integrity systems for the defence and aerospace industry.

He specialises in C++, UML, software modelling, Systems Engineering and process development.
Glennan Carnie

Latest posts by Glennan Carnie (see all)

Dislike (0)

About Glennan Carnie

Glennan is an embedded systems and software engineer with over 20 years experience, mostly in high-integrity systems for the defence and aerospace industry. He specialises in C++, UML, software modelling, Systems Engineering and process development.
This entry was posted in C/C++ Programming, Cortex, Design Issues and tagged , , , , , , , , . Bookmark the permalink.

Leave a Reply