In its classic form, a callback (aka callback function) is a function that is passed as an argument to another function. The function that accepts the callback as an argument is expected to call back on it (execute it) at a certain point in time.
The callback mechanism allows a lower-level software layer to call functions defined in an upper-level software layer.

In Fig.1 we have a simple block diagram showing an interaction between a user application code and a hardware driver. The HW driver (lower-level software layer) is a standalone reusable driver that has no knowledge of the layer above (in this case the user application). The HW driver provides API functions that allow the user application to register a function as a callback. This callback function is then called by the HW driver as part of the execution of Function_B
. Without the use of a callback, Function_B
would have been coded to call Function_A
directly. This would have made the HW driver specific to the particular upper-level software level and reduced its re-usability. Another benefit of the callback mechanism is that the callback function called by Function_B
can be dynamically changed during program execution.
Callbacks in C
Different programming languages have different ways of implementing callbacks. In this article, we will focus on the C programming language, as it is the most popular language used for embedded software development.
Callbacks in C are implemented using function pointers. А function pointer acts just like a regular pointer but instead of pointing at an address of a variable, it points at an address of a function. The same function pointer can be set to point to different functions during the runtime of the program.
In the code below we can see how a function pointer is used for passing a function as an argument to a function. The arithm_op
function takes as argument a function pointer calback_func
and two integer values a
and b
. The arithmetic operation that will be performed depends on the function that will be passed to the function pointer argument.
uint16_t sum(uint8_t a, uint8_t b) {
return a + b;
}
uint16_t mul(uint8_t a, uint8_t b) {
return a * b;
}
uint16_t arithm_op (uint16_t (*callback_func)(uint8_t, uint8_t),uint8_t a, uint8_t b) {
return callback_func(a,b);
}
void main() {
arithm_op(mul,4,10); /* Multiplication: 4x10*/
arithm_op(add,9,5); /* Addition: 9+5 */
}
Practical Usage of Callbacks
Callbacks can be employed in many types of situations and are widely used in embedded firmware development. They provide greater code flexibility and allows us to develop drivers that can be fined tuned by the end-user without the need for code changes.
The elements needed for having callback functionality in our code are:
- a function that will be called upon (callback function)
- a function pointer that will be used to access the callback function
- a function (“caller function”) that will call the callback function
A simple flow for using callback functions is presented next.
Declare a function pointer that will be used for accessing the callback function
We can simply declare a function pointer as:
uint8_t (*p_CallbackFunc)(void);
However for cleaner code it is better to define a function pointer type:
typedef uint8_t (*CallbackFunc_t) (void);
Define the callback function – It is important to note that a callback function is just a function. We referer to it as a callback due to the way it is used (accessed by a function pointer). So this step is just a definition of the function that our previously declared pointer will point to.
uint8_t Function_A(void) {
/* code of the function */
}
Register the callback function – This is the operation of assigning an address to the the function pointer. In our case the address should be that of the callback function.
There can be a dedicated function for registering the callback function as shown below.
static CallbackFunc_t TransferCompleted;
/* Function for registering the callback function */
void CallbackRegister (CallbackFunc_t callback_func) {
TransferCompleted = callback_func;
}
/* Register Function_A as a callback */
CallbackRegister(Function_A);
Code Examples
Example 1: Callback on an event
In this example, we show how a callback can be used for handling an event. The example code below is of a data communication protocol stack that is built upon a lower lever physical communication interface (e.g UART, SPI, I2C, etc.). The communication protocol stack implements two different types of frames – a standard communication frame and an enhanced communication frame. There are two different functions for handling an event of a received byte. In the initialization function Comm_Init
, the function pointer pNewByteReceived
is assigned the address of the function that should be used. for handling the event. This is the operation of registering a callback function.
/* File name: example_comm_stack.c */
/* function pointer to a callback function */
uint8_t ( *pNewByteReceived ) ( void );
/* Simplified Initialization function
* Here the function pointer is assigned an address of a function (registering the callback function) */
void Comm_Init( uint8_t op_mode) {
switch ( op_mode ) {
case STD_FRAME:
pNewByteReceived = StdRxFSM;
break;
case ENHANCED_FRAME:
pNewByteReceived = EnhancedRxFSM;
break;
default:
pNewByteReceived = EnhancedRxFSM;
}
}
/* These are functions (callbacks) implemented in the communication stack.
* They are not directly called anywhere, a function pointer is used to access them */
uint8_t StdRxFSM( void ) {
/* Do some stuff */
}
uint8_t EnhancedRxFSM( void ) {
/* Do some stuff */
}
The callback function in our example is called by the user application code when a new byte is received (the event) from the physical communication interface (e.g UART) .
/* File name: user_app.c */
extern uint8_t ( *pNewByteReceived ) ( void );
void new_byte() {
/* Do some stuff */
pNewByteReceived(); /* The user function indicates new byte has been received */
}
Example 2: Multiple callbacks in a register
This example shows how we can create a register for storing callback functions. It is implemented using an array of function_register_t
data type elements. The data type is a structure with a function_id
member and a p_callback_func
member. The function_id
is used for assigning an identification (unique number) for each callback function in the register. The function pointer p_callback_func
is assigned the address of the callback function that is to be associated with the unique function_id
. RegisterCallback
function is used for adding and removing callbacks from the register.
#define FUNC_REGISTER_SIZE (255)
#define FUNC_ID_MAX (127)
/* function pointer type */
typedef uint8_t ( *callback_func_t ) ( uint8_t * p_data, uint16_t len );
typedef struct {
uint8_t function_id;
callback_func_t p_callback_func;
} function_register_t;
/* An array of functions handlers each with an id.*/
static function_register_t func_register[FUNC_REGISTER_SIZE];
/* Register function callback (function codes + functions) */
uint8_t RegisterCallback (uint8_t function_id, callback_func_t p_callback_func ) {
uint8_t status;
if ((0 < function_id) && (function_id <= FUNC_ID_MAX)) {
/* Add function to the register */
if ( p_callback_func != NULL ) {
for (int i = 0; i < FUNC_REGISTER_SIZE; i++ ) {
if (( func_register[i].p_callback_func == NULL ) ||
( func_register[i].p_callback_func == p_callback_func )) {
func_register[i].function_id = function_id;
func_register[i].p_callback_func = p_callback_func;
break;
}
}
if (i != FUNC_REGISTER_SIZE) {
status = SUCESSFULL;
}
else {
status = FAILURE;
}
}
else { /*Remove function from the register */
for ( i = 0; i < FUNC_REGISTER_SIZE; i++ ) {
if ( func_register[i].function_id == function_id ) {
func_register[i].function_id = 0;
func_register[i].p_callback_func = NULL;
break;
}
}
status = SUCESSFULL;
}
}
else {
status = FAILURE; /* Invalid argument */
}
return status;
}
In the code below we can see an example of a function that can be used for calling a callback based on a function id.
/*An example of how a callback function with a specific function code is called*/
uint8_t execute_callback(uint8_t FuncCode, uint8_t * p_data_buf, uint16_t len) {
uint8_t status;
status = FAILURE;
for( i = 0; i < FUNC_REGISTER_SIZE; i++ ){
/* No more callbacks registered, exit. */
if( func_register[i].function_id == 0 ){
break;
}
else if( func_register[i].function_id == FuncCode) {
status = func_register[i].p_callback_func( p_data_buf, len );
break;
}
}
return status;
}
Conclusion
We can write programs that do not use callbacks, however by adding them to our arsenal of tools, they can make our code more efficient and easier to maintain. It is important to use them wisely, otherwise going overboard with the use of callbacks (function pointers) can make the code hard to review and debug. Another thing to consider is that using function pointers may prevent some of the optimizations (e.g function inlining) that the compiler performs.
Many Many Many thanks pretty simple explanation
Your effort is appreciated, Thanks for simple explanation
I finally understood the concept after looking at your explanations. Really appreciate it.
Great explanation. Congratulations