embedded software boot camp

Configuring hardware – part 2.

Wednesday, December 15th, 2010 by Nigel Jones

This is the second in a series on configuring the hardware peripherals in a microcontroller. In the first part I talked about how to set / clear bits in a configuration register.  Now while setting bits is an essential part of the problem, it is by no means the most difficult task. Instead the real problem is this. You need to configure the peripheral but on examining the data sheet you discover that the peripheral has twenty registers, can operate in a huge number of modes and has multiple interrupt sources. To compound the difficulty, you may not fully understand the task the peripheral performs – and the data sheet appear to have been written by someone who has clearly never written a device driver in their life. If this sounds a lot like what you have experienced, then read on!

When I first started working in embedded systems, I used to dread having to write a device driver. I knew I was in for days, if not weeks of anguish trying to make the stupid thing work. Today I can usually get a peripheral to do what I want with almost no heartache – and in a fraction of the time it used to take me. I do this by following a standard approach that helps minimize various problems that seem to crop up all the time in device drivers. These problems are as follows:

  1. Setting the wrong bits in a register
  2. Failing to configure a register at all.
  3. Setting the correct configuration bits – but in the wrong temporal order.
  4. Interrupts incorrectly handled.

To help minimize these types of problems, this is what I do.

Step 0 – Document *what* the driver is supposed to do

This is a crucial step. If you can’t write in plain English (or French etc) what the driver is supposed to do then you stand no chance of making it work correctly.  This is a remarkably difficult thing to do. If you find that you can’t succinctly and unambiguously describe the driver’s functionality then attempting to write code is futile. I typically put this explanation in the module header block where future readers of the code can see it. An explanation may look something like this.

This is a serial port driver. It is intended to be used on an RS232 line at 38400 baud, 8 data bits, no parity, one stop bit. The driver supports CTS / RTS handshaking. It does not support Xon / Xoff handshaking.

Characters to be transmitted are buffered and sent out under interrupt. If the transmit buffer fills up then incoming characters are dropped.

Characters are received under interrupt and placed in a buffer. When the receive buffer is almost full, the CTS line is asserted. Once the receive buffer has dropped below the low threshold, CTS is negated. If the host ignores the CTS line and continues to transmit then characters received after the receive buffer is full are discarded.

As it stands, this description is incomplete; for example it doesn’t say what happens if a receiver overrun is detected. However you should get the idea.

Incidentally I can’t stress the importance of this step enough. This was the single biggest breakthrough I made in improving my driver writing. This is also the step that I see missing from almost all driver code.

Step 1 – Create standard function outlines

Nearly all drivers need the following functions:

  1. Open function. This function does the bulk of the peripheral configuration, but typically does not activate (enable) the peripheral.
  2. Close function. This is the opposite of the open function in that it returns a peripheral to its initial (usually reset) condition. Even if your application would never expect to close a peripheral it is often useful to write this function as it can deepen your understanding of the peripheral’s functionality.
  3. Start function. This function typically activates the peripheral. For peripherals such as timers, the start function is aptly and accurately named. For more complex peripherals, the start function may be more of an enable function. For example a CAN controller’s start function may start the CAN controller listening for packets.
  4. Stop function. This is the opposite of the start function. Its job is to stop the peripheral from running, while leaving it configured.
  5. Update function(s). These function(s) are highly application specific. For example an ADC peripheral may not need an update function. A PWM channel’s update function would be used to update the PWM depth. A UART’s update function would be the transmit function. In some cases you may need multiple update functions.
  6. Interrupt handler(s). Most peripheral’s need at least one interrupt handler. Even if you aren’t planning on using an interrupt source, I strongly recommend you put together a function outline for it. The reason will become clear!

At this stage, your driver looks something like this:

/*
 Detailed description of what the driver does goes here
*/

void driver_Open(void)
{
}

void driver_Close(void)
{
}

void driver_Start(void)
{
}

void driver_Stop(void)
{
}

void driver_Update(void)
{
}

__interrupt void driver_Interrupt1(void)
{
}

__interrupt void driver_Interrupt2(void)
{
}

Step 2 – Set up power, clocks, port pins

In most modern processors, a peripheral does not exist in isolation. Many times peripherals need to be powered up, clocks need to routed to the peripheral and port pins need to be configured. This step is separate from the configuration of the peripheral. Furthermore documentation on these requirements is often located in non-obvious places – and thus this step is often overlooked. This is an area where I must give a thumbs-up to NXP. At the start of each of their peripherals is a short clear write up documenting the ancillary registers that need to be configured for the peripheral to be used. An example is shown below:

Basic Configuration Steps for the SSP

Personally, I usually place the configuration of these registers in a central location which is thus outside the driver. However there is also a case for placing the configuration of these registers in the driver open function. I will address why I do it this way in a separate blog post.

Step 3 – Add all the peripheral registers to the open function

This step is crucial. In my experience a large number of driver problems come about because a register hasn’t been configured. The surest way to kill this potential problem is to open up the data sheet at the register list for the peripheral and simply add all the registers to the open function. For example, here is the register list for the SSP controller on an NXP ARM processor:

Ten registers are listed.  Even though one register is listed as read only, I still add it to the driver_Open function as I may need to read it in order to clear status flags. Thus my open function now becomes this:

void driver_Open(void)
{
 SSP0CR0 = 0;
 SSP0CR1 = 0;
 SSP0DR = 0;
 SSP0SR;            /* Status register - read and discard */
 SSP0CPSR = 0;
 SSP0IMSC = 0;
 SSP0RIS = 0;
 SSP0MIS = 0;
 SSP0ICR = 0;
 SSP0DMACR = 0;
}

At this stage all I have done is ensure that my code is at least aware of the requisite registers.

Step 4 – Arrange the registers in the correct order

For many peripherals, it is important that registers be configured in a specific order. In some cases a register must be partially configured, then other registers must be configured, and then the initial register must be completely configured. There is no way around this, other than to read the data sheet to determine if this ordering exists. I should note that the order that registers appear in the data sheet is rarely the order in which they should be configured. In my example, I will assume that the registers are correctly ordered.

Step 5 – Write the close function

While manufacturer’s often put a lot of effort into telling you how to configure a peripheral, it’s rare to see information on how to shut a peripheral down. In the absence of this information, I have found that a good starting point is to simply take the register list from the open function and reverse it. Thus the first pass close function looks like this:

void driver_Close(void)
{
 SSP0DMACR = 0;
 SSP0ICR = 0;
 SSP0MIS = 0;
 SSP0RIS = 0;
 SSP0IMSC = 0;
 SSP0CPSR = 0;
 SSP0DR = 0;
 SSP0CR1 = 0;    
 SSP0CR0 = 0;
}

Step 6 – Configure the bits in the open function

This is the step where you have to set and clear the bits in the registers. If you use the technique that I espoused in part 1 of this series, then your open function will now explicitly consider every bit in every register.  An example of a partially completed open function is shown below:

void driver_Open(void)
{
 SSP1CR0 = ((4 - 1) << 0) |    /* DSS = 4 bit transfer (min value allowed) */
            (0U << 4) |        /* SPI format */
            (1U << 6) |        /* CPOL = 1 => Clock idles high */
            (1U << 7) |        /* CPHA = 1 => Output data valid on rising edge */
            (5U << 8);         /* SCR = 5 to give a division by 6 */

 SSP1CR1 =  (0U << 0) |        /* LPM = 0 ==> no loopback mode */
            (1U << 1) |        /* SSE = 1 ==> SSP1 is enabled */
            (0U << 2) |        /* MS = 0 ==> Master mode */
            (0U << 3);         /* SOD = 0 (don't care as we are in master mode */

 SSP0DR = 0;
 SSP0SR;            /* Status register - read and discard */
 SSP0CPSR = 0;
 SSP0IMSC = 0;
 SSP0RIS = 0;
 SSP0MIS = 0;
 SSP0ICR = 0;
 SSP0DMACR = 0;
}

Clearly this is the toughest part of the exercise. However at least if you have followed these steps, then you are guaranteed not to have made an error of omission.

This blog posting has got long enough. In the next part of this series, I will address common misconfiguration issues, interrupts etc.

3 Responses to “Configuring hardware – part 2.”

  1. Lundin says:

    It is always interesting to read how others do this. Writing down a specification for what the driver should do is definitely a good idea, I think I should adopt it myself 🙂

    Otherwise, I’m using something quite similar to this approach myself, but somewhere around step 5, I always go off to check erratas, before I start to do any serious work. I think I have yet to work with a MCU peripheral completely free silicon bugs. The earlier you read the erratas, the more time you will save.

    I would take the abstraction level further though. A good driver in my opinion, is one you can pick up for another project without changing anything in the code. And a state of the art driver is one you can pick up for another project -no matter the MCU or compiler-. The latter is something I’ve recently started to practice myself, after I got fed up with writing my 99th-something SPI driver.

    For this to work, I will have a framework (API if you will) for a peripheral: CAN, SPI, EEPROM etc etc. The functionality I want out of those peripherals are common for all projects, though the ways of implementing them aren’t.

    For example, all CAN will have init(), send(), receive(), some filter handling, some ways to detect 11-bit versus 29-bit identifiers, some ways to detect special frames (RTR, errors etc) and so on. All this stuff comes from the CAN ISO standard, and is really the only things of interest. Everything MCU-specific like buffer sizes, interrupts etc are just ways to reach the standard CAN functionality.

    So I will have code like this:

    can.h // general CAN API
    HCS08_CAN.c // CAN driver for the HCS08 microcontroller, implements functions from can.h
    HCS12_CAN.c // CAN driver for the HCS12 microcontroller, implements functions from can.h

    The OO fanatics will recognize it a heritage, where can.h is the abstract base class. The advantages are obvious: code can be ported between projects and MCUs with minimum effort.

    For this to work however, you will need to include things like clock and pin routing etc as parameters to the driver. I don’t think this is a bad idea: you need this “clock etc” for the driver to work. There is no situation where I want to use the driver without the “clock etc”. And if the clock is a parameter to the driver rather than something hardcoded, you can have your driver calculate variable baudrates, duty cycles, prescalers etc based on the CPU clock. I will be curious to hear the reasons why “clock etc” things should not be in the driver.

    • Nigel Jones says:

      Thanks for your as always interesting comments Daniel.
      I think you make an good case on the errata. I’m much more likely to read the errata on a complex peripheral in a new micro than I am on a simple peripheral on an old micro. I’m also more likely to do it if I’m working with a Microchip processor. Notwithstanding this, I think this is an area where I can (and should) improve my procedures – so thanks for the tip.

      I like what you are saying about the abstraction. I have shied away from it for two reasons.
      1. The article is aimed at those who struggle to make drivers work. Adding abstraction makes the problem even tougher.
      2. As a consultant I face a rather interesting legal and ethical quandary. In general the work I do for a client is considered to be so called ‘work-for-hire’. In simple terms, once they have paid for it, they own it. If I take the time to write a highly abstracted reusable driver (and of course bill the client for the time), then reusing the driver on a different project for a different client is a legal and ethical mine field. Thus I find myself in the rather interesting situation whereby my job would really benefit from me developing a series of abstract reusable drivers – but I can’t use them! One solution of course would be for me to develop these drivers on my own time. However between work, family, hobbies and this blog I’m struggling to find enough time as it is! Interestingly, I don’t think I have ever had a client come to me and ask me to develop a library of reusable abstract drivers. I’m not sure what that says!

      Finally I’m glad you mentioned clocking. I was intending on talking about clocking in the next part of the article as in my experience it is an area where many problems arise. I will be addressing methods of ensuring that your clocking is correct.

    • GroovyD says:

      That is the way I write drivers as well, with a common interface file which all implementations try to satisfy. When I come across a new device which requires a fundamental difference I try to re-work the legacy devices to work with this new approach as well. Sometimes I end up with functions that are only #ifdef’ed in because of special features for example an accelerometer that can wake from a freefall event, or an rtc with a temperature sensor.

      I have adopted a naming scheme which though is post-fixed with the specific device name instead of pre-fixed as you indicate; so can.h, can_hcs08.c, can_xxxx.c for example so that alphabetically they all sort themselves out in the folder making it easy to find. Typically my core processor is the final postfix in the event that it matters such as: pic, msp, avr, arm, …

Leave a Reply to Nigel Jones

You must be logged in to post a comment.