How to Write an Effective Driver Time-Out

Developing time-outs for your embedded software could help prevent a lock up.

Jacob Beningo

August 25, 2023

6 Min Read
time-out-GettyImages-985635618-2.jpg
Peter Dazeley/The Image Bank via Getty Images

Writing an effective driver time-out for an embedded system can be tricky. It’s tempting to assume that everything will work as expected, but that’s typically only the case in the lab. While transmitting data over a serial interface or reading a value from a sensor seems like it can never fail, there are situations where hardware can lock up or glitch. When something goes wrong, an expected bit isn’t flipped, or a peripheral doesn’t respond, you don’t want your system to lock up. You want your software to detect that too much time has passed and allow it to continue executing code. This post will explore a few simple but effective methods for developing time-outs for your embedded software.

The State of Industry Code

Writing a time-out for your driver or code isn’t rocket science. It’s a problem that was solved many years ago, yet if you examine open-source and vendor-provided code, you’ll discover that most software doesn’t include a time-out! In fact, if you look at how they write code that interacts with hardware and sensors, you’ll find code that looks like the following:

while(isConversionComplete == false){

   isConversionComplete = (REGISTER & REGISTER_BIT) >> REGISTER_SHIFT;

}

As you can see, the code has a while loop that expects something to happen in hardware. If that thing doesn’t happen, then the code is trapped! An unintentional infinite loop has been created! You might consider this okay if you expect your watchdog to swoop in and save the day. If you want to write reliable software that doesn’t just hang and reset but can detect the hang-up and recover, then this tactic of interacting with hardware will not suffice. Instead, you need to build a time-out into your code.

Tracking Time in Low-Level Drivers

When you think about tracking time at low levels within your embedded software, you’ll discover that it presents a few problems. First, tracking time will add a time dependency to your code. You need to figure out some mechanism that can be used to track time. Next, the mechanism you choose could waste clock cycles, or it could add additional complexity to your software.

There are several different mechanisms that you can use to track time and create an effective driver time-out, such as:

  • Loop until time expiration

  • Sleep until time-out

  • Sleep until event

The time-out mechanism you use will depend on your software architecture and whether you use a real-time operating system (RTOS). Let’s examine a few of these and some potential implementation ideas.

Loop until time expiration

When you first add a time-out to your hardware check loop, you’ll most likely end up writing some code that looks something like the following:

isConversionComplete = (REGISTER & REGISTER_BIT) >> REGISTER_SHIFT;

 

while((isConversionComplete == false) && (isTimeout == false)){

   isConversionComplete = (REGISTER & REGISTER_BIT) >> REGISTER_SHIFT;

 

   TimeNow = SystemTime_Get();

       

   if(TimeNow >= Timeout){

      isTimeout = true;

   }

}

At first glance, this seems okay, but this approach has several problems. First, we are most likely coupling SystemTime to our driver code. The code looks okay, but I often see that some external module is brought into the code base. When this is done, you end up with a bunch of modules dependent upon a single module, and the code coupling gets out of control. Second, we have some code duplication because we check the isConversionComplete status before the while loop, and then if it is false, we add another check.

The second issue can be solved quite easily by converting the while loop into a do . . . while loop. The change ensures that the code is executed at least once and helps us to refactor the code so that it looks cleaner, as shown below:

do{

   isConversionComplete = (REGISTER & REGISTER_BIT) >> REGISTER_SHIFT;

 

   TimeNow = SystemTime_Get();

       

   if(TimeNow >= Timeout){

      isTimeout = true;

   }

} while((isConversionComplete == false) && (isTimeout == false));

The first problem can be a little more interesting. You can include additional modules to increase your dependencies and code coupling or use dependency injection. Dependency injection is where we use the function's parameter to inject any dependencies, such as time dependency.

In our example, we need to be able to access the current system time. We would inject a pointer to the function in our driver functions to use dependency injection. There are two ways to do this. First, you can inject it into the driver initialization code and then manage the pointer in the driver. Second, you can pass the pointer as a constant pointer to every function in the driver. While the second one appears appealing because we can pass the function as a const, we may be forcing additional coupling and dependency to higher-level application code. So, if we go with the first option, our code might look something like the following:

Error_t MyDriver(uint32_t const (*Time_Get)(void));

It’s a simple technique to get time tracking into the driver or other code without adding a bunch of dependencies. While this is a simple technique, our implementation is a polled approach! That’s not very efficient. There are undoubtedly other approaches you could use. Let’s look at one other example that uses sleep until time-out. You’ll find the general mechanism we use is dependency injection, so you can easily tailor the technique for sleep until the event as well.

Sleep Until Time-Out and Sleep Until Event

If you use an RTOS, you may decide that while you wait for the hardware to respond, you do not want to sit and poll the clock but instead sleep the thread. An RTOS usually provides a mechanism to get the kernel time to check for a time-out and event flags or semaphores to wait for an event.

You can use dependency injection to inject a sleep function into the function. For example, you might find that MyDriver now looks like the following:

Error_t MyDriver(uint32_t const (*Time_Get)(void), uint32_t const (*Rtos_Sleep)(uint32_t));

The calling function, if it were to use ThreadX, might look like the following:

MyDriver(tx_time_get, tx_thread_sleep);

MyDriver has no clue what RTOS is being used, and it doesn’t care. It just needs a pointer to the time and sleep functions that are provided by the RTOS so that the code can sleep and keep track of time correctly.

Conclusions on Time-Outs

It’s common in an embedded system to wait for hardware to return a value or status before continuing code execution. Unfortunately, many code today assumes everything will work well, and that status will eventually come. That’s a poor assumption. Instead, you should write your code assuming there will be a problem. When this happens, you’ll need to include some sort of time-out to return an error to a higher-level code.

Time-outs are an effective mechanism to ensure you don’t end up with driver code that creates an infinite loop. A halting system or one that resets periodically is a nuisance. Instead, start with one of the time-out mechanisms discussed in this post and adapt it to work with your system requirements. You’ll find that it helps you write more reliable low-level code that doesn’t assume everything will work out okay.

About the Author(s)

Jacob Beningo

Jacob Beningo is an embedded software consultant who currently works with clients in more than a dozen countries to dramatically transform their businesses by improving product quality, cost and time to market. He has published more than 300 articles on embedded software development techniques, has published several books, is a sought-after speaker and technical trainer and holds three degrees which include a Masters of Engineering from the University of Michigan.

Sign up for the Design News Daily newsletter.

You May Also Like