Developing embedded software that is flexible and scalable has become an important aspect to product development. More and more, companies aren’t developing a single monolithic product, but instead, developing a core platform from which several different product lines will be created. In the core code, there will be times where the desired software behavior will be unknown and must be implemented for the specific product needs. In order to achieve code flexibility, developers can use callback functions.
A callback is a function that is executed through a pointer where the function being pointed to contains application specific code. Callbacks are normally passed into a function and provides the developer with the flexibility to change the callback code without having to modify the calling function. For example, callbacks are often used in low-level drivers to access application specific behaviors in the application layer. The driver may not know what the timer interrupt should do so it makes a call through a callback to application level code that defines what the interrupt should do. This allows the driver code to stay the same and for the application level code to define its behavior.
An easy way to spot a callback function is to look for functions that are passing function pointers into a function. When that function pointer is dereferenced, and the function executed, this is where the function gets its “callback” designation from. In general, developers will find that callbacks require three things to be used in an application:
- Defining the callback function
- Registering the callback function
- Executing the callback function
Defining a callback is simple. It’s the same as any other function. An example callback function for a watchdog timer might look something like the following:
Registering the callback could be done in several ways. The first, and most obvious method, is to pass the callback into a function. The function would be defined like:
void Watchdog_Expired(void (*Callback)(void))
And called like:
The second method, which works for interrupt callbacks, is to define a function pointer and then a function that is used to register the callback. For example, we would define the function pointer as:
typedef Callback_function void (*Callback)(void);
static Callback_function WatchdogExpired = NULL;
We can then use a registration function that is defined as:
WatchdogExpired = Callback;
Registering the Watchdog_ExpiredCallback that we used earlier would result in the following code:
This code would be executed during the system initialization, replacing the NULL pointer with a pointer to our desired function. The watchdog interrupt, would then look like the following:
if(WatchdogExpired != NULL)
Now you may look at this code for a moment and comment that it’s not a good idea to call a function from an interrupt! This is violating best practices. However, if a developer keeps their callback function short and sets their callback function to be in-lined by the compiler, no golden rules are violated, and we have a flexible and scalable solution using callbacks.
As you can imagine, we can easily compile the watchdog module into a library that we distribute or keep locked down. We still have the flexibility to adjust how the watchdog behaves from the higher-level application code by changing what the Watchdog_ExpiredCallback function does. This is why callbacks can help us improve our code flexibility. The low-level code stays the same and is untouched, but the higher-level code can change with the application needs.
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 email@example.com, at his website www.beningo.com, and sign-up for his monthly Embedded Bytes Newsletter here.