Using Callbacks with Interrupts

On a weekly basis, I receive a fair number of emails with questions about how to design embedded systems. One question that seems to be asked more frequently than others is how to write a configurable driver that uses callbacks with interrupts. If a developer is writing a driver that will be reused in multiple applications, the interrupt is usually compiled into the driver so the only way to make the interrupt handler flexible is to use a callback but how exactly is that done? In today’s post, we are going to dive into exactly how to do this.

(Image Source: Reusable Firmware Development)

 

An Example UART Hardware Abstraction Layer (HAL)

In order to understand what we need to do with the callback and interrupt, it’s useful to look at an example. Let’s say that we have a UART or USART that will be reused in multiple applications. We define a hardware abstraction layer for the basic functions so that we can decouple the driver code from the application code. The interface might look something like the following:

void Uart_Init(UartConfig_t const * const Config);
void Uart_BaudRateSet(UartChannel_t const Channel, UartConfig_t const * const Config);
uint8_t Uart_CharGet(UartChannel_t const Channel);
void Uart_CharPut(UartChannel_t const Channel, char const Ch);
uint8_t Uart_IsDataPresent(UartChannel_t const Channel);
void Uart_RegisterWrite(uint32_t const Address, uint32_t const Value);
uint32_t Uart_RegisterRead(uint32_t const Address);
void Uart_CallbackRegister(UartCallback_t const Function, void (*CallbackFunction)(void));

Notice that the interface has a method that can be used to register a callback function. This registration allows the developer to take their callback function and assign it to the interrupt that they need to assign it to such an UART receive or transmit interrupt.

 

Building the Interrupt Code

Within the UART driver, there may be several different interrupts that are defined. For example, one interrupt handlers may be:

void Uart0_ISR(void);

We would normally fill this interrupt with application code such as:

void Uart0_ISR(void)
{
    HAL_UART_Transmit(&huart2, (uint8_t *)aRxBuffer, 1, 0xFFFF);

    CBUF_Push(RxDataBuffer, aRxBuffer[0]);

    HAL_UART_Receive_IT(&huart2, (uint8_t *)aRxBuffer, 1);
}

This code though is application specific and we want to assign what the interrupt does at runtime.

 

Instead, we can setup our interrupt handler as follows:

void Uart0_ISR(void)
{
    if(UART0_ISR->function != NULL)
    {
        (*UART0_ISR->function)();
    }   
}

The idea here is that we are going to use a function pointer to specify which function should be executed when the interrupt fires. If we have not assigned an interrupt, that is the function pointer is assign NULL, then we do nothing. If the function pointer is assigned, then we execute the function.

 

Assigning the Function Pointer

The function that is assigned to the function pointer is set at runtime using the following HAL function:

void Uart_CallbackRegister(UartCallback_t const Function, void (*CallbackFunction)(void));

We can define a callback function for the application using the following as an example:

void MyIsrFunction (void)
{
    HAL_UART_Transmit(&huart2, (uint8_t *)aRxBuffer, 1, 0xFFFF);
    CBUF_Push(RxDataBuffer, aRxBuffer[0]);
    HAL_UART_Receive_IT(&huart2, (uint8_t *)aRxBuffer, 1);
}

The system initialization code then makes the following call to assign the function to the function pointer that is executed in the interrupt service handler:

Uart_CallbackRegister(UART0_ISR, MyIsrFunction);

 

Removing Dynamic Callback Assignment

Having an API that can be called to change the function that is executed by the interrupt may seem dangerous or could be a security vulnerability. An alternative to having an API assignment is to instead use a configuration table to initialize the function pointer at compile time. You’ll notice that the Uart_Init function has the form:

void Uart_Init(UartConfig_t const * const Config);

A configuration table could be used to assign the function that is executed. The advantages here are multifold such as:

  • The function is assigned at compile time
  • The assignment is made through a const table
  • The function pointer assignment can be made so that it resides in ROM versus RAM which will make it unchangeable at runtime

There are certainly several different ways that this can be done, but the idea is to make it so that the driver code is constant, unchanging and could even be provided as a precompiled library. The application code can then still easily change the interrupt behavior without having to see the implementation details.

 

Conclusions

As we have seen in today’s post, callbacks can be used to easily create interrupt service routines that are flexible and scalable. We have seen that there are several methods that developers can use to employ callbacks in this way. My personal preference is to statically assign the callback so that it cannot be changed at runtime, but dynamic assignment can be useful for applications where the interrupt behavior may need to change during execution.

5 thoughts on “Using Callbacks with Interrupts”

  1. Hi Jacob, I have used a similar callback methods extensively for my drivers but I structure my HAL layer to only register client callbacks for execution at the task level. The technique assumes you have basic methods to signal or message a worker task from the ISR, but this allows having a very tight predictable ISR function. This is because the ISR’s only job on the receive side is to read a bytes and feed it into a buffer (usually a ring buffer) and signal the associated serial task to process the buffer. This also supports multiple client callbacks (a callback list) and in the case of serial drivers I keep the port information in a static array and its possible to route serial streams in and out different ports. This only works if the client driver code can tolerate sloppier timing and doesn’t have a hard real time requirement (such as measuring the inter-byte timing), but this is normally the case.
    Thanks for the article.

  2. Hey Jacob!

    I think the code box beneath the line “Instead, we can setup our interrupt handler as follows:” is not correct. It appears to be a duplication of the previous ISR code. Instead, I believe it should be where you call the callback function.

    Cheers!

  3. The two interrupt examples have the same content: Uart0_ISR
    Shouldn’t the second one call the callback?

Leave a Reply to Nick Newell Cancel reply

Your email address will not be published. Required fields are marked *

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