CC Blog Quick Bits Resources

Writing Device Drivers

Written by Andrew Levido

One of the tasks we often have to do when creating the firmware for our projects is to write device drivers. These are the lowest-level bits of code that interface with specific hardware devices, usually via a set of device registers. You can sometimes find an existing device driver for more popular parts, which you can modify for your needs (license permitting). In other cases, you will have to write your own from scratch.

Whichever approach you use, it pays to follow a few self-imposed guidelines when writing device drivers. This consistency will pay dividends when de-bugging and maintaining your code (especially if a bit of time has passed since they were written) and it will promote re-use of code. Coding style is a very subjective and personal matter, but I am going to share a few key guidelines I use when writing drivers. I believe that having one approach and sticking to it is more important than looking for some mythical “perfect” way.

Use a consistent naming convention

I like to make sure that all of the drivers I write use a consistent naming convention for files and functions so that it is clear which functions belong to which driver. I use a short descriptive prefix for each function such as “lcd” for a display driver or “cdc” for a CODEC driver. The header and code files are also named with this prefix plus some more descriptive name describing the driver. For example, the driver files for an SGTL5000 audio CODEC would be named cdc_SGTL5000.c and cdc_SGTL5000.h. The initialisation function for the CODEC would be named cdc_init(), and that for the LCD display would be lcd_init().

Simple, Intuitive API

This leads on to the API. While it is impossible to be completely prescriptive, I like to use a consistent prefix-verb-noun-qualifier pattern for function names. The noun and qualifier being optional, depending on the function. I try to use consistent verbs such as set/get for setting or retrieving parameters and read/write for bulk transfers. Table 1 shows some example function names. This pattern means it is relatively easy to create consistent and descriptive function names.

Table 1

One other aspect of the API that is worth touching on is initialisation. Some drivers require quite a number of parameters be specified at initialisation. Rather than have a long list of function parameters, I prefer to use an initialisation structure which is populated and then passed as a parameter to the initialisation routine. This is shown conceptually in Code snippet 1. You can use this model to configure sub-sections of the driver as well for particularly complex drivers. For example, I have used this technique for an IMU driver where the accelerometer and gyroscope each have a configuration function in addition to the overall IMU initialisation.

/* Code Snippet 1 - Using a configuration structure in an LCD module driver 
   using an SPI interface and a gpio pin for backlight */

/* In driver lcd_module.h file */

...

typedef struct {
  spi_peripheral_t spi_instance;
  gpio_port_t backlight_pin_port;
  gpio_pin_t backlight_pin;
  lcd_orientation_t orientation;
  uint16_t background_colour;
  uint8_t backlight_level;
} lcd_init_struct_t;

...

driver_result_t lcd_init(lcd_init_struct_t * config);


/* In user code to initialise the driver */

#include "lcd_module.h"

...

lcd_init_struct_t lcd_config = {
  .spi_instance         = SPI_1,                /* SPI instance for LCD       */
  .backlight_pin_port   = PORT_C,               /* Backlight pin PORTC Pin12  */
  .backlight_pin        = 12,
  .orientation          = LCD_90_DEGREES,       /* Lanscape orientation       */
  .background_colour    = 0x0000,               /* Initially black            */
  .backlight_level      = 50,                   /* backlight at mid-level     */
};

...

if(lcd_init(&lcd_config) != DRIVER_RESULT_OK) {
  // Handle error
}

...

Code Snippet 1
In this example an initialisation structure type is defined in the driver header file, along with a prototype for the initialisation fuction. To initialise the driver the user instantates an initialisation structure an populates it with the configuration parameters. The initialisation fuction is then called with a pointer to the initialisation structure. In my opinion this is neater and easier than an initalisation function with many parameters.

— ADVERTISMENT—

Advertise Here

Separate Driver Implementation from Configuration (mostly)

To maximise re-use, it is a good idea to separate the driver implementation from the specific configuration required for a particular application. To take an example, the CODEC driver should allow the user to set the CODEC bitrate, which may vary from one application to another, rather than encoding it into the driver, even if the application you are building today will only ever use one rate.

This is a bit of a balancing act to be honest. Taken to its extremes, we would have to allow the user to configure the peripheral with every legal combination of settings. This could mean a lot of coding that will never be used, and no-one has the time for that. Under these circumstances you may elect to implement a subset of driver functions and have the rest of the settings default to a known state. If I choose to do this, I try to write the driver in a way that lets me extend the driver functionality in future if I need to.

Be Clear About What the Driver is For

You need to be very clear about just what the driver is supposed to be doing. Take for example the example of a TFT LCD display module which has a display driver chip embedded into it. You need to decide whether you are writing a driver for the display driver chip or for the display module itself? The decision will depend on your circumstances but taking the latter approach will mean that some driver chip settings will be dictated by the configuration of the display module and can be coded into the driver without breaking the separation requirement.

For example, the display driver chip may support a range of screen resolutions, but the TFT display will dictate that only one resolution is meaningful for the display module. In this case you can safely hard code the resolution into the display module driver. You would not do this if you were writing a generic driver for the chip which could be used with various TFTs.

Handle Errors

This should go without saying, but I have seen plenty of drivers that just assume everything will just work or that the user will never do something stupid. You should always handle error conditions and incorrect or out-of-range parameters.

Not all driver functions need to return an error code, however. It may be perfectly sufficient to do nothing in case of an error. If for example you try to write pixels to an LCD outside the display area it might be appropriate just to ignore those pixels and clip the image to the display boundaries.  In other cases you could limit out of range values to meaningful levels (for example a backlight brightness greater than the maximum could be interpreted as full brightness).

Watch the Sequence of Operations

You may need to pay attention to the sequence of operation. For example,  if a driver needs to be initialised before a driver function can be called, the function should test that the initialization has taken place and respond appropriately if it has not. If a driver should only be initialised once, make sure it cannot happen again, even if the initialisation function is called multiple times.

Keep Driver State Private

Code snippet 2 contains the skeleton of a simple data structure and driver function for a single-instance driver. This is provided just to outline how I generally to do it – no doubt you will have your own ideas and preferences.

/* Code Snippet 2 - Typical skeleton of a simple driver API function */

/* Driver state maintained in global structure */

static struct {
  bool init;                  /* true if driver initialised */
  spi_peripheral_t spi;       /* spi peripheral for lcd */
  pwm_peripheral_t pwm;       /* pwm peripheral for backlight */
  uint8_t backlight_level;    /* 0 to 100 for PWM duty cycle 0 t0 100% */
  ...

} lcd;

driver_result_t lcd_set_backlight(uint8_t level)
{
  /* Check sequencing - driver must be initialised, else error */
  if(lcd.init != true) { return(DRIVER_NOT_INITILAISED); }

  /* Validate parameters - maximum duty cycle is 100% */
  if(level > 100) { level = 100; }

  /* Store state - for lcd_get_backlight() */
  lcd.backlight_level = level;

  /* Implement driver action - write level to duty cycle register */
  lcd.pwm->duty_cycle = level;

  return(DRIVER_OK);

}

Code Snippet 2
Driver state is contained in a global data structure (declared static so its visibility is limited to this file). This state is therefore accessible only to the driver functions, and not to the user directly. The example API function first checks the driver is initialised then validates the function parameters. Only then does it implement the necessary driver functionality, using and updating the driver state as necessary.

— ADVERTISMENT—

Advertise Here

Usually, the driver will need to maintain a certain amount of state information (global variables). I like to put all of this in a structure to keep things neat and tidy. It goes without saying that the driver state is private to the driver and should never be accessible to user code. Using structures in this way is especially handy if there will be more than one instance of the device managed by the driver. Each instance gets its own data structure, and each API call includes a pointer to the relevant structure indicating which instance of the device it refers to.

The important thing is to find an approach that works for you and apply it consistently. This will definitely make your code more maintainable and reusable.

Keep up-to-date with our FREE Weekly Newsletter!

Don't miss out on upcoming issues of Circuit Cellar.


Note: We’ve made the May 2020 issue of Circuit Cellar available as a free sample issue. In it, you’ll find a rich variety of the kinds of articles and information that exemplify a typical issue of the current magazine.

Would you like to write for Circuit Cellar? We are always accepting articles/posts from the technical community. Get in touch with us and let's discuss your ideas.

Sponsor this Article
+ posts

Andrew Levido (andrew.levido@gmail.com) earned a bachelor’s degree in Electrical Engineering in Sydney, Australia, in 1986. He worked for several years in R&D for power electronics and telecommunication companies before moving into management roles. Andrew has maintained a hands-on interest in electronics, particularly embedded systems, power electronics, and control theory in his free time. Over the years he has written a number of articles for various electronics publications and occasionally provides consulting services as time allows.

Supporting Companies

Upcoming Events


Copyright © KCK Media Corp.
All Rights Reserved

Copyright © 2023 KCK Media Corp.

Writing Device Drivers

by Andrew Levido time to read: 6 min