Structure device drivers and error handling with sequence points

Randall Maas 5/21/2010 6:29:16 PM

In a previous post I discussed logical OR, sequence points, and mentioned that these are useful for writing driver code with failure handling. This is a description of just that.

The basic API

First, let's say we're talking about a device on the I2C bus. It could be a lot of other kinds of interfaces, but let's just say it's I2C. To talk a device on the I2C bus, let's say we've wrapped the relevant microcontroller IO and registers stuff into a few procedures that do specific things:

Each of these procedures is small and simple to understand. They don't block indefinitely, and they can be analyzed for their impact on the rest of the system, including on timing and stack depth. (They could return 0 on success, and an error code otherwise)

The first set of operations are built on the basic API

At this point I would build a procedure to send the command message to the I2C device. It checks for errors, but it does not perform any retries to recover from the error. Again this returns a 0 on error, otherwise it is an error.

byte I2C_SendCommand(byte* Buff, int Length)
{
   I2C_Start();

   // Send the address to select the device.  This could be a parameter
   if (  !I2C_Send(DeviceAddress1) 
      || !I2C_Send(DeviceAddress2))
     {
        (void) I2C_Stop();
        return 0;
     }

   // Send each of the bytes
   for (int I = 0; I < Length; I++)
     if (FAILURE == I2C_Send(Buff[I]))
       {
          (void) I2C_Stop();
          return 0;
       }

   return I2C_Stop();
}

Although the procedure isn't trivial, it is pretty straightforward to understand, and it is small. Notice that we always call the I2C_Stop() in the procedure, not at a higher layer. The sequence points provide a nice structure of the actions to be done, but breaks immediately and false into the error state.

In some cases the error state handling might be complex enough to warrant grouping it in one area, and using a goto to go back to it. (Fortunately this is rate on the smallest microcontrollers, and only shows up once you get to complex peripherals).

No matter what the error handling does not loop. It is important in embedded designs to not use unbounded loops. If you avoid them you can easily put together a pretty consistent, predictable system. (This is not the same as a strict real-time system, especially one with the rate monotonic properties, but that is a topic for another time).

Side note: The (void) before some of the I2C_Stop() calls is a construct a coworker taught me. In these cases the return value from I2C_Stop() is not meaningful: the overall transfer failed. However the compiler and other analysis tools don't know this. We are marking that ignoring this as intentional to the tools and other developers with the (void).

The sophisticated operations with retry handling

Although fail-fast approach has the operation working in most cases, it can fail. What do you do then? That depends on your design. The simplest case is to retry a few times:

byte SomeDeviceDoOperation()
{
   for (int I = 0; I < 5; I++)
    if (I2C_SendCommand(CmdString))
      return 1;

   return 0;
}

This code is very brief, simple to understand, limited. And for a lot embedded stuff you will probably be pretty happy with that. Sure you might want to log the information, flag the device as broken, and so forth. But that is it.

In other designs maybe not. If you have cooperative design, where you have broken down each task into small fixed chunks that run to completion (i.e., these are not pre-empted for other threads in any way). In this case, you will probably use some tracking variables indicating that you are trying to do the operation, you are in retry #, and so forth. Every cycle thru the main loop you give it another chance to do its subtask, then go onto the next chunk of work for something else. At some point these tracking variables indicate that the retries are over, and it enables some other chain of subtasks to interwoven on the main loop.

If you're not comfortable with that, that's ok. There are plenty of techniques you can choose from, (admittedly there are fewer the more you try to have safety/reliability/performance/other-quality).