In the last post, we examined how in a modern embedded application a developer should create an interface that decouples the low-level driver implementation details from the application code. This interface provides an architectural abstraction that increases the scalability and portability for the application code by making it less dependent on the hardware.
Now we are going to start to look at several different ways that a developer can implement an ADC driver based on the techniques that we discussed in 3 driver design techniques for microcontrollers. In this article, we will examine in more detail how we can use the polling technique and discuss the difference between blocking and non-blocking drivers.
To Block or Not to Block, that is the Question
When developing any driver for a microcontroller, a developer has to decide whether their driver will be blocking or non-blocking. A blocking driver essentially stalls code execution until the driver has completed its task. For example, the typical implementation for printf that is mapped to a UART is blocking.
When you make a call like:
A developer knows that whatever line of code follows that statement will not execute until the entire “Hello World!” statement has been printed out the UART. “Hello World!” contains twelve bytes, 96 bits, but the amount of time the statement will block depends on the UART baud rate. For a UART configured at 1 Mbps, you would expect about 96 microseconds. For a UART configured at 9600 bps, you would expect about 10,000 microseconds! That’s a big difference depending on how the hardware is configured and it can affect the program execution dramatically with the UART driver being configured as a blocking driver.
A non-blocking driver is one that does not stall program execution while the driver is completing its task. For example, printf and the UART driver from the previous example could be configured so that it does not block and instead allows the application to continue executing while each byte is transmitted out the UART. This can make for a more efficient application under the right circumstances but requires additional setup such as using interrupts, DMA or at least a transmit buffer.
Deciding which way to design your driver depends on your application and hardware. For example, if the UART is configured for 1 Mbps, writing a non-blocking driver probably won’t gain much from an efficiency standpoint and could actually cause more problems than it fixes through additional program complexity. However, if the application calls for 9600 bps, where the application code is blocked for 10 milliseconds, having a non-blocking driver can dramatically improve program efficiency and the risk for additional timing complexity issues is much less and more manageable.
An Embedded ADC Driver Overview
It’s important to note that in a single blog I can’t walk through all the steps necessary to write a full ADC driver. I could easily write a twenty-page paper on it or give an entire webinar and it probably wouldn’t still cover all the details, but we can at least look at some of the core pieces.
There are several ways that we could organize an ADC driver, but the way that I like to organize them requires three components:
- The low-level driver
- The application code
- A configuration module
The low-level driver takes the configuration module during initialization and sets up the hardware based on the configuration. The low-level driver provides a common hardware abstraction layer (HAL) that the application code can then use. The ADC HAL calls should be generic so that the high-level application can configure the hardware in any way that is necessary and so that it can be reusable and scalable. For example, a few ADC HAL calls that I’ve used in the past include:
- AdcError_t Adc_Init(const AdcConfig_t * Config);
- AdcError_t Adc_StartConversion(void);
- bool Adc_ConversionComplete(void);
- void Adc_RegisterWrite(uint32_t const Address, uint32_t const Value);
- uint32_t Adc_RegisterRead(uint32_t Address);
- void Adc_CallbackRegister(AdcCallback_t const Function, TYPE (*CallbackFunction)(type));
The first three API’s provide the capability to initialize the ADC hardware, start a conversion and then check on the conversion status. The last three functions are designed to allow scalability to the low-level hardware. For example, if the HAL doesn’t provide an option that is needed by the application such as converting a single ADC channel, the HAL can be extended using the Adc_RegisterRead and Adc_RegisterWrite functions. This provides flexibility based on the application needs without creating an overwhelming API.
Writing a Simple Blocking ADC Driver
We can write a really simple ADC driver that is above the hardware layer. For example, we can create a simple function named Adc_Sample that kicks off the ADC hardware and then stores all the results in a buffer that can then be accessed by the application. The buffer that stores the analog values count values doesn’t necessarily need to store just a single value but can store multiple values that could later be averaged or filtered based on the application need. The blocking version for the sampling function might look something like the following:
As you can see in this code, the while loop blocks execution until the ADC hardware has completed its conversion and then it stores the values in the application buffer.
Writing a Simple Non-Blocking ADC Driver
Converting the blocking driver to non-blocking code is quite simple, but it would require changes to the higher-level application code. For example, right now, if the application wants to sample the sensors, a developer calls:
In the non-blocking version, a developer has to check the return value from Adc_Sample to see if the samples are completed and ready for use. This allows the samples to run in the background, application code to continue running with the following updates to our driver code:
As we have seen in this post, there are multiple ways to write an ADC and the implementation can be blocking, or non-blocking based on our needs. Blocking drivers tend to be simpler and less complete than non-blocking drivers but they can be inefficient. Non-blocking drivers allow other code to run while the driver works, but the application code still needs to check-in on the status which in itself is inefficient in a polled implementation.
In the next article in this series, we will examine how we can write an application that samples a sensor through an ADC peripheral that uses interrupts.
Jacob Beningo is an embedded software consultant, advisor and educator who currently works with clients in more than a dozen countries to dramatically transform their software, systems and processes. Feel free to contact him at firstname.lastname@example.org, at his website www.beningo.com, and sign-up for his monthly Embedded Bytes Newsletter.