Interfacing with modern sensors: Interrupt driven ADC drivers - Embedded.com

Interfacing with modern sensors: Interrupt driven ADC drivers

In the last article, we examined how we could write an analog-to-digital converter (ADC) driver that blocked and one that used a polling technique to not block application flow. Drivers that poll a peripheral are inefficient and can waste precious clock cycles that could otherwise have been used or energy if the system could have been placed into a low power state. An efficient way that a developer can implement an ADC driver is to use interrupts to notify the application that the conversion cycle has been completed. In this article, we are going to examine how this can be done.

Updating the ADC Driver Sample Function

There are several different ways that an ADC driver can be written to use interrupts. In this article, we will look at how we can modify the non-blocking ADC driver that we looked at in the last article. Just like we did in that article, the application can make a call to start an ADC conversion by calling the function Adc_Sample.

This is a good example why having a good hardware abstraction layer (HAL) can come in handy. Whether I’m blocking, not blocking, polling or interrupting, I make a call to the exact same function and the behavior simply changes based on the configuration settings for the driver or it may link in a different version of the Adc_Sample function based on the application need.

As you may recall from the last article, the Adc_Sample function for the non-blocking system looked like the following:

bool Adc_Sample(void)
{
    AdcPin_t AdcPin = AdcChannel0;
    static bool SampleInProgress = false;
    bool SampleComplete = false;
    if(SampleInProgress == false)
    {
        if(Adc_SampleIndex == ADC_SAMPLE_SIZE)
        {
            Adc_SampleIndex = 0;
        }
        SampleInProgress = true;
        Adc_StartConversion();
    }
    else
    {
        if(Adc_ConversionComplete == true)
        {
            for(AdcPin = AdcChannel0; AdcPin < NUM_ANALOG_PINS; AdcPin++)
            {
                Adc_SampleBuffer[i][Adc_SampleIndex] = *AD1BufPtr++;
            }
            
            Adc_SampleIndex++;
            SampleComplete = true;
        }
        return SampleComplete;
    }

In our new version, the Adc_Sample function might be implemented something like the following:

bool Adc_Sample(void)
{
    Adc_StartConversion();
    return true;          
}

Wow! What happened to all the code? Our non-blocking driver had all kinds of checks and accessing to buffers, etc. For our interrupt-based driver, all we want to do it kick-off the ADC channel conversions based on the peripheral was configured during initialization. So, for example, if we wanted to sample channels 0, 1 and 3, those would be the channels that were enabled during initialization. This driver is meant to sample all the channels specified at one time and not sample just one or two. As I’ve mentioned, there are many ways to do this and to help clarify the concepts, we are using the simplest solution possible.

At this point, if we called Adc_StartConversion, we’d expect the ADC peripheral to sample the channels, but when the interrupt fires nothing would happen at this point. We need to populate the ADC interrupt handler but doing this in the driver is problematic. Instead, we want to try and abstract the interrupt handler code if we can.

Abstracting the Interrupt

One problem that developers often encounter when writing drivers is that when they develop an interrupt driven solution, they often tightly couple their interrupt to the application code. Optimally the interrupt would reside with the driver code, which is in the driver layer, not in the application code which is at the highest layer in the architecture. Tightly coupling the interrupt to the application code can make it difficult to port the code and even scale it in some cases.

One solution that developers can use to keep the interrupt in the driver layer and still customize it for an application is to use a callback. A callback function is a reference to executable code that is passed as an argument to other code that allows a lower-level software layer to call a function defined in a higher-level layer[1]. A callback function at its simplest is just a function pointer that is passed to another function as a parameter. In most instances, a callback will contain three pieces:

  • The callback function
  • A callback registration
  • Callback execution

How these three pieces work together in a typical callback implementation can be seen in the image below:


Figure: Typical callback architecture. (Source: Reusable Firmware Development)

If you recall from the last blog, the ADC driver HAL included the following function:

void Adc_CallbackRegister(AdcCallback_t const Function, void (*CallbackFunction)(void));

If you look at this closer, this function is designed to register a callback function from the application code with the ADC driver. The first parameter specifies what interrupt it is that the callback will be assigned to while the second parameter assigns the function to be called by passing a function pointer into the function.

The low-level driver at this point would then assign the function pointer to the specified interrupt. This is extremely flexible since the developer can easily update and change what function is executed by the interrupt without having to go back and modify and recompile the adc driver. This helps to separate out the application code from the driver code, creating a scalable and flexible solution.

With this knowledge, we can implement out ADC interrupt handler to look something like the following:

void ADC_IRQHandler(void)
{
    (*ADC_Interrupt1)();   
}

The interrupt is nothing more than dereferencing the pointer that was assigned through the Adc_CallbackRegister() function. (Note that in production code I would also add in some checks to make sure that the function pointer had been assigned, but I think you get the idea).

Writing the Interrupt Handler

For a developer that uses this method, the interrupt handler will be written in their application layer and can have nearly any function name they like. Personally, I always name it something like Adc_InterruptCallback so that I know what the heck it is easily. The implementation for that callback can vary depending on the application. For example, in one application the callback may look like the following:

void Adc_CallbackRegister(void)
{
    tx_semaphore_put(&Adc1Semaphore);
}

In this example, the callback is simply putting a semaphore to notify a task that the ADC data is available. Another example might look like the following:

void Adc_CallbackRegister(void)
{
    AdcPin = AdcChannel0;
    // Loop through and store the buffer data
    for(AdcPin = AdcChannel0; AdcPin < NUM_ANALOG_PINS; AdcPin++)
    {
        Adc_SampleBuffer[i][Adc_SampleIndex] = *AD1BufPtr++;
    }
    
    Adc_SampleIndex++;
    if(Adc_SampleIndex == ADC_SAMPLE_SIZE)
    {
        Adc_SampleIndex = 0;
    }
}

As you can see, it’s up to the developer to decide how they want to handle the analog data in their interrupt handler, and it will vary greatly based on the application and its need.

It’s important to note that for these callback functions, which are really interrupt handlers, it’s important to follow interrupt handler best practices. This means minimizes the code and making them as fast as possible to minimize impact on the rest of the system performance.

Conclusion

As we have seen in this article, using an interrupt driven driver design pattern can dramatically improve the efficiency of the driver. The use of callbacks can keep the interrupt implementation in the application code and assigned to the interrupt through the drivers’ callback mechanism. This enables the solution and the code to be highly reusable, flexible and scalable.

In the next article, we will discuss how an ADC driver can be made even more efficient by using a direct memory access (DMA) controller.

References

 

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.