Friday, February 19, 2010

The Value of Embedded Device Drivers

Today's embedded processors feature scores of easy-to-use peripherals that can usually be accessed directly through a series of memory-mapped registers, which are easily read and written through C/C++ or assembly code.

Flip a few bits here and there and all of a sudden your processor is sending data, toggling I/O lines, counting time, and making all sorts of logic dance! It's an elegant, flexible, and powerful way to control a wide variety of hardware, but it certainly doesn't help developers produce elegant, portable, or maintainable code.

To use a cliche - "with great power comes great responsibility."

What I'm referring to is the practice of separating peripheral I/O code from application code using device drivers.

And just like a teenager getting behind the wheel of Dad's car for the first time, many embedded developers take the "keys to the register set" and run directly into the proverbial garbage can before making it out of the driveway.

Chanting the mantra of code space overhead and the inefficiencies of "needless" layers of abstraction, we tell ourselves that sprinkling platform-specific I/O operations in the middle of applications is a good practice - because using functions to perform those operations would result in larger, less efficient code. And for a while it does make sense - especially on tiny microcontrollers where code space is at an absolute premium.

But as applications grow, features get added, and routines start fighting for resources, this approach falls apart. Our "streamlined" code becomes increasingly difficult to read and maintain, portability goes out the window, and reentrancy issues start to pop up making it difficult to guarantee the reliability of the system. Developers then resort to all sorts of trickery and work-arounds to plug the holes on a sinking ship.

It's at this point that programmers typically start looking at grouping related peripheral functions into modules to offer some level of abstraction. The result is a series of modules containing all of the I/O operations for the various peripherals written as functions - and this is often sufficient to get the application back into a usable shape.

While it's a good start, ad-hoc libraries (AHLs) are not device drivers.

The difference between ad-hoc libraries and a device driver is in how they are used at the application level:

While a board support package provides functions to control peripherals, each will typically have a separate API, and are generally specific to the peripheral. As a result, applications written to use AHLs are tied to a given platform, making portability difficult to achieve. Applications attempting to be portable using AHLs are typically easy to identify by the unreasonable amount of #ifdefs scattered throughout; a hallmark of unreadable, unmaintainable code.

These problems can all be avoided by abstracting AHLs through a device driver framework. While it might intuitively sound like there would be a lot of overhead involved in implementing a driver "framework", it really doesn't take much effort at all (and I'll demonstrate it in this article). In fact, most capable RTOS's include a formalized device driver framework which saves you the work.

A device driver needs to be able to perform the following operations:
  • Initialize a peripheral
  • Start/stop a peripheral
  • Handle I/O control operations
  • Perform various read/write operations

At the end of the day, that's pretty much all a device driver has to do, and all of the functionality that needs to be presented to the developer.

As a result, we need an API that contains the following functions:
  • Initialize
  • Start
  • Stop
  • Control
  • Read
  • Write
A basic driver framework and API can thus be implemented in six function calls - that's it! You could even reduce that further by handling the initialize, start, and stop operations inside the "control" operation.

In C, we can implement this as a data structure to abstract these event handlers, and a series of wrapper functions to call them, described below.

First, we define function pointer types for the different handlers in the framework:

typedef BOOL (*DRIVER_INIT)(void *pstThis_);
typedef BOOL (*DRIVER_START)(void *pstThis_);
typedef BOOL (*DRIVER_STOP)(void *pstThis_);
typedef USHORT (*DRIVER_CONTROL)(void *pstThis_, USHORT usID_, void *pvData_);
typedef USHORT (*DRIVER_READ)(void *pstThis_, UCHAR *pucData_, USHORT usLen_);
typedef USHORT (*DRIVER_WRITE)(void *pstThis_, UCHAR *pucData_, USHORT usLen_);

Which allows us to design a generic device driver "base" structure as follows:

typedef struct
{
// Function pointers to driver event handlers
DRIVER_INIT pfInit;
DRIVER_START pfStart;
DRIVER_STOP pfStop;
DRIVER_CONTROL pfControl;
DRIVER_READ pfRead;
DRIVER_WRITE pfWrite;

// Variables common to all drivers go below (driver name, type, state)
} DEVICE_DRIVER;

The API functions to operate on this driver structure can then be declared:

BOOL Driver_Init(DEVICE_DRIVER *pstDriver_);
BOOL Driver_Start(DEVICE_DRIVER *pstDriver_);
BOOL Driver_Stop(DEVICE_DRIVER *pstDriver_);
USHORT Driver_Control(DEVICE_DRIVER *pstDriver_, USHORT usEvent_, void *pvData_);
USHORT Driver_Read(DEVICE_DRIVER *pstDriver_, UCHAR *pucData_, USHORT usLen_);
USHORT Driver_Write(DEVICE_DRIVER *pstDriver_, UCHAR *pucData_, USHORT usLen_);


These functions simply act as wrappers for the handler functions set in the DEVICE_DRIVER struct. For example, the Driver_Init function might look something like this:

BOOL Driver_Init(DEVICE_DRIVER *pstDriver_)
{
return pstDriver_->pfInit((void*)pstDriver_);
}

Now, in this example we aren't checking that pstDriver_ is valid (which you would want to do in a *real* implementation), but it does illustrate the concept of wrapper functions.

You'll notice that the handlers function types each have a void pointer as the first argument, where the wrapper API calls use DEVICE_DRIVER pointers. There's a good reason for this, and it's related to inheritance.

In this framework, each individual device driver is built on top of the the basic device driver type. Since C doesn't have built-in inheritance found in C++ or Java, we must implement this feature using the knowledge of how data is aligned in structures. Implementing a device driver based on the "base" driver type is shown in the following example:

typedef
{
DEVICE_DRIVER stDriver; //!! Must be first

// data unique to this kind of device driver goes below...
USHORT usMyData;
} MY_PERIPHERAL_STRUCT;

BOOL MyPeripheral_Init(void *pstThis_);
BOOL
MyPeripheral_Start(void *pstThis_);
BOOL
MyPeripheral_Stop(void *pstThis_);
USHORT
MyPeripheral_Control(*DRIVER_CONTROL)(void *pstThis_, USHORT usID_, void *pvData_);
USHORT
MyPeripheral_Read(void *pstThis_, UCHAR *pucData_, USHORT usLen_);
USHORT
MyPeripheral_Write(void *pstThis_, UCHAR *pucData_, USHORT usLen_);

Specified handler functions for the driver are assigned to the function pointers when declaring the structure as follows:

MY_PERIPHERAL_STRUCT stMyPeripheral =
{
{ // set the handler functions for the base driver struct
MyPeripheral_Init,
MyPeripheral_Start,
MyPeripheral_Stop,
MyPeripheral_Control,
MyPeripheral_Read,
MyPeripheral_Write
},
0xBEEF // initialize driver pecific data
};

This looks simple- and it is, but something very interesting happens when we create a structure this way. In C, the first element in a struct will always have the same address as the struct itself, so if we have a struct of type MY_PERIPHERAL_STRUCT, it is completely valid to re-cast the struct to type (DEVICE_DRIVER*). This re-casting to a parent type is the essential element of inheritance which gives us the ability to create a series of device drivers that are all accessed through a consistent interface.

As a result, any specialized device driver type can be cast back to the DEVICE_DRIVER type and used with the API functions we have already defined! The driver's handler functions are responsible for re-casting these pointers back to the appropriate type when used internally (i.e. to MY_PERIPHERAL_STRUCT).

Consider a system with drivers for I2C, SPI, and UART peripherals - under our driver framework, an application can initialize all of these peripherals and write a greeting to each using the same simple API functions for all drivers:

Driver_Init((DEVICE_DRIVER*)stMyI2C);
Driver_Init((DEVICE_DRIVER*)stMyUART);
Driver_Init((DEVICE_DRIVER*)stMySPI);

Driver_Start((DEVICE_DRIVER*)stMyI2C);
Driver_Start((DEVICE_DRIVER*)stMyUART);
Driver_Start((DEVICE_DRIVER*)stMySPI);


Driver_Write((DEVICE_DRIVER*)stMyI2C, "Hello World!", 12);
Driver_Write((DEVICE_DRIVER*)stMyUART
, "Hello World!", 12);
Driver_Write((DEVICE_DRIVER*)stMySPI
, "Hello World!", 12);

To me, the benefits of using this kind of device driver framework are obvious:
  • It provides a consistent interfaces for controlling and accessing peripherals. All a developer needs is to learn a single driver API, with a few specific control events for each peripheral type.
  • The internals of the device driver are completely obscured from the user - all that needs to be provided to the application is a pointer to each driver.
  • Creates an automatic hardware abstraction layer, leading to better code reuse for the drivers and portability of application code.
  • Reduced module coupling in application code, as the app doesn't need to call AHL functions directly.
  • The wrapper functions can incorporate resource protection mechanisms to prevent deadlock in a multi-threaded system.
This approach was used in the Mini-Markade project to successfully abstract the display and joystick drivers to allow the games and frontend to compile under Borland C++ Builder on windows as well as the Atmel atmega328p platform without requiring any modifications or #defines to the application code. The overhead on the embedded side was roughly 100 bytes of code space, and in the context of a 32kB application, the benefit greatly outweighs the cost.

See the latest release of FunkOS for more information on how formal device drivers can be used in embedded systems applications.

Monday, February 15, 2010

The Mini-Markade Lives!

It's the first post on the blog, and I thought I'd start off by sharing a project of mine.

I've been working on a tiny arcade machine based on an AVR atmega328p microcontroller in an attempt to win a Guiness record for the category of "world's smallest arcade machine". Well, it turns out that Guiness doesn't give awards for "smallest" anything anymore, but that hasn't stopped me from building it all the same.

Originally, I got the idea after seeing a number of "Paper Arcade" models popping up on the 'tubes. Given the dimensions from those cabinets, I figured that it wouldn't be too hard to shoehorn an AVR board and an OLED display into that form factor - and while it was a challenge, I've been surprised at how well everything has come together.

The end result is what you see below - a fully functional version of the "Paper Arcade" capable of playing my own interpretations of classic arcade games.

My arcade machine (dubbed the "Mini-Markade") currently has versions of Tetris, Space Invaders, and Breakout - all selectable from a frontend application. Smooth 60fps graphics are displayed on a 1.5 inch OLED display (by 4D Systems), while controls are input through a custom Atari-compatible joystick made from random parts off digikey. Powered by 2xAAA batteries, the system will run for 12-15 hours between changes.

The Markade also runs FunkOS, my very own RTOS for low-resource microcontrollers, which you can download from my sourceforge page (http://funkos.sourceforge.net).

Between my RTOS, my own implementations of Tetris/Invaders/Breakout, and all the device driver and frontend code, the system uses about 1800 bytes of RAM and all but 80 bytes of the available 32KB of flash. Not bad!

The pics below show the cabinet almost fully assembled - all that's left to do is tack the back panel on, add an external power switch and give it some paint. Standing around 3.5 inches tall, the mini-markade replicates the design of an old Defender cabinet - with all dimensions implemented to scale.

More to come in the coming days and weeks!