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:
[snippet slug=uart_hal lang=c_cpp]
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:
[snippet slug=uart_isr lang=c_cpp]
We would normally fill this interrupt with application code such as:
[snippet slug=isrexample lang=c_cpp]
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:
[snippet slug=uartisrfilled lang=c_cpp]
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:
[snippet slug=callback lang=c_cpp]
We can define a callback function for the application using the following as an example:
[snippet slug=callbackfilled lang=c_cpp]
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:
[snippet slug=callbacks lang=c_cpp]
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:
[snippet slug=staticcallback lang=c_cpp]
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.
Struggling to keep your development skills up to date or facing outdated processes that slow down your team, raise costs, and impact product quality?
Here are 4 ways I can help you:
- Embedded Software Academy: Enhance your skills, streamline your processes, and elevate your architecture. Join my academy for on-demand, hands-on workshops and cutting-edge development resources designed to transform your career and keep you ahead of the curve.
- Consulting Services: Get personalized, expert guidance to streamline your development processes, boost efficiency, and achieve your project goals faster. Partner with us to unlock your team's full potential and drive innovation, ensuring your projects success.
- Team Training and Development: Empower your team with the latest best practices in embedded software. Our expert-led training sessions will equip your team with the skills and knowledge to excel, innovate, and drive your projects to success.
- Customized Design Solutions: Get design and development assistance to enhance efficiency, ensure robust testing, and streamline your development pipeline, driving your projects success.
Take action today to upgrade your skills, optimize your team, and achieve success.
Could you, please, add run-time error handling to the API?
I leave that as an exercise for the reader. I can’t give you everything!
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.
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!
Thanks! This has now been fixed!
The two interrupt examples have the same content: Uart0_ISR
Shouldn’t the second one call the callback?
Thanks this has now been fixed!
thanks for this explanation Jacob, finally someone who know how to explain things in a clear way !
Thanks alot but why this is a security vuln?
Thanks for the question.
If I can change the value of the callback function to point to a function I’ve injected into RAM, then I can execute any code that I want to.