Embedded Basics – Callback Functions

Callback functions are an essential and often critical concept that developers need to create drivers or custom libraries. 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(10). A callback allows a driver or library developer to specify a behavior at a lower layer but leave the implementation definition to the application layer.

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
Figure 46 shows how these three pieces work together in a typical callback implementation.

Figure 46 – Callback Example Usage

First, a developer creates the library or module that will have an implementation element that is determined by the application developer. An example might be that a developer creates a GPIO driver that has an interrupt service routine whose code is specified by the application developer. The interrupt could handle a button press or some other functionality. The driver doesn’t care about the functionality but only that at run-time it knows what function should be called when the interrupt fires. The code that will invoke the callback function within the module is often called the signal handler.

Next, there needs to be some way to tell the lower level code what function should be executed. There are many ways that this can be done but for a driver module, a recommended practice is to create a function within the module that is specifically designed to register a function as a callback. Having a separate function to register the callback function makes it very clear to the developer that the callback function is being registered to a specific signal handler. When the register function is called, the desired function that will be called is passed as a parameter into the module and that functions address is stored.

Finally, the application developer writes their application which includes creating the implementation for the callback and initialization code that registers that function with the library or module. When the application is executed, the low-level code has the callback function address stored and when the feature needs to execute, it dereferences the callback function and executes it.

There are two primary examples that a developer can consider for using callbacks. First, in drivers, a developer will not know how any interrupt service routine might need to be used by the end application. If the developer is creating a library for some microcontrollers peripherals, a callback could be used to specify all the interrupts behaviors. Using the callback would allow the developer to make sure that every interrupt had a default service routine in the event that the application developer did not register a custom callback function. When callbacks are used with interrupts, developers need to keep in mind that the best practices for interrupts need to be followed.

Second, callbacks can be used whenever there is common behavior in an application that might have implementation specific behaviors. For example, initializing an array is a very common task that needs to be performed within an application. What if for some applications, a developer wants to initialize array elements to all zero’s where in another application they want the array elements initialized to random numbers? In this case, they could use a callback to initialize the arrays.

Examine Figure 47. The ArrayInit function takes a pointer to an array with elements size and then it also takes a pointer to a function that returns integers. The function at this point is not defined but can be defined by the application code. When ArrayInit is called the developer passes in whatever function they choose to initialize the array elements. A few example functions that could be passed into ArrayInit can be seen in Figure 48 and Figure 49.

void ArrayInit(int * Array, size_t size, int (*Function)(void))
{
for(size_t i = 0; i < size; i++)
{
Array[i] = Function();
}
}

Figure 47 – Function with Callback

int Zeros(void)
{
return 0;
}

Figure 48 – Initialize Elements to 0

int Random(void)
{
return rand();
}

Figure 49 – Initialize Elements to Random Numbers

The functions Zeros or Random are passed into ArrayInit depending on how the application developer wants to initialize the array.

Note: To learn more portable and reusable firmware techniques, check out “Designing Reusable Firmware

Share >

13 thoughts on “Embedded Basics – Callback Functions

  1. Awesome article. Callbacks are super useful.
    For simplicity and code readability, i generally tend to use typedef to define function pointers.

  2. Jacob,

    Thanks for the article! I am a seasoned C/C++ programmer who is new to embedded programming; I am learning to program a PIC18 for use with my hobbyist project to build a MIDI controlled synthesizer. I am currently writing a MIDI protocol handling library which I plan to open source, and I have a question about how embedded programmers deal with callback registration.

    My view is that in a system with limited resources, there is a strong desire to handle callbacks as quickly as possible. Thus, I was wondering if you believe the overhead of invoking callbacks via pointers is something I need to worry about? My prototype uses a PIC18F4620 with an 8MHZ external oscillator, but I am planning to potentially use a 16MHZ crystal and a PIC1846K80 which supports 64MHZ in PLL mode.

    Thus, I don’t think I have to worry, but was wondering if there are other strategies used in resource constrained situations. One idea that I had was to use #define statements in the header that by default invoke a dummy callback, and instruct users of the library to modify the #define to call their own function if they wish to handle the event. Since anyone using my library will likely be rebuilding the entire system along with my C code, this would allow things to work without a callback registration mechanism involving pointers.

    Something like this:

    // Dummy function that accepts two bytes.
    void dummy2(char k, char v) { }

    // User would modify the header to call their function
    #define ON_MIDI_NOTE_ON(k,v) my_handle_note_on((k),(v))

    // User would leave unused events alone to call the library defined dummy.
    #define ON_MIDI_NOTE_OFF (k,v) dummy2((k), (v))

    Warm Regards, Tom Dial

    • Thanks for the comment. In the system that you are describing, I would probably not be worried about the overhead from the function pointer or calling a function. The overhead as far as clock cycles go are minimal.

      You can use macros as you have described as a possible solution. My preference would be to just register a function with a function pointer but using macros would allow the substitution to occur at compile time and allow the compiler to optimize the code.

      • Thank you for the comment!

        Shortly after posting, I thought more and realized that I was likely committing the sin of premature optimization. The PIC18 that I will end up using can run at 64Mhz in PLL mode. A few extra cycles to handle a pointer dereference can’t possibly cause a problem for a library whose purpose is to handle incoming serial data at a rate of 31,250 bits per second!

        I’ll be open-sourcing my MIDI library soon; it will be available on Github. At that time, I will post back on the thread, and welcome anyone with embedded programming experience to submit PRs if you see potential for improvements.

  3. Really Good!!! I searched several websites but no where i found the real time explanation like this Jocab.
    Its really a good work…..
    Thanks….

  4. Hello,

    As promised, I’ve put my MIDI code on Github. The library only supports MIDI receive at this time, and even then is not feature complete, but it does support most common MIDI messages.

    https://github.com/mikromodular/libmidi/blob/master/README.md

    While you may not have any interest in MIDI per se, I am posting this for those who may be unfamiliar with how callback-based APIs work; please see the README for more details.

    Also, I would welcome suggestions. Although this code contains nothing specific to a particular microcontroller, I would be interested to hear feedback from “embedded veterans” on whether I’m doing anything that would be unusual in an embedded environment. (All my my professional programming experience has been on Windows and Linux).

    TD

  5. Why do you need a callback? You could have called the function directly, without using a function pointer right?

    void ArrayInit(int * Array, size_t size)
    {
    for(size_t i = 0; i < size; i++)
    {
    Array[i] = rand();
    }
    }

    • The callback function allows scalability of the application. For different applications, different rand() functions may be used. In C, we can’t overload or do any cool modern techniques. Instead, we can use callback functions to assign what function we want to be executed instead.

      Thanks for the question!

  6. Yes, great explanation. But I have doubt that why we should use callback in event occur or interrupt service routine?? Instead can’t we call function directly??

    • You can call it directly … but if you want to write code that you can compile into an object and just reuse in different applications, we use the callback to add flexibility to the design and make it easier to reuse and maintain.

Leave a Reply to Timothy 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.