
Function pointers are a powerful feature in the C programming language that enable dynamic behavior, modularity, and abstraction in embedded systems. By storing the address of a function in a pointer variable, developers can create flexible designs where the function to be called is determined at runtime. This capability is particularly valuable in resource-constrained environments where adaptability and code reuse are essential.
In embedded systems, function pointers find extensive use in implementing callback mechanisms for interrupt service routines, hardware abstraction layers, event-driven architectures, and state machines. They allow developers to decouple the invariant parts of an algorithm from the variable parts, leading to cleaner, more maintainable code. Understanding how to declare, initialize, and use function pointers is fundamental for any embedded programmer aiming to write efficient and scalable firmware.
A function pointer declaration specifies the return type and parameter types of the functions it can point to. The syntax can be intimidating at first, but breaking it down helps clarify its meaning.
// Declaration of a function pointer that points to a function// returning void and taking an integer parametervoid (*func_ptr)(int);
To make function pointers easier to use, especially when dealing with complex signatures, the typedef keyword is commonly employed to create an alias for the function pointer type.
// Using typedef to create a function pointer type aliastypedef void (*callback_func_t)(int);// Now we can declare variables of this type more intuitivelycallback_func_t my_callback;
Assigning a function to a function pointer is straightforward: simply use the function name (without parentheses) as the address.
// Example function that matches the callback_func_t signaturevoid timer_expired_handler(int timer_id) {// Handle timer expiration}// Assign the function to the function pointermy_callback = timer_expired_handler;// Calling the function through the pointermy_callback(42); // Calls timer_expired_handler(42)
It’s important to note that the function pointer must point to a function with a matching signature; otherwise, the behavior is undefined. Especially critical on embedded systems where calling convention mismatches can corrupt registers or stack frames.
One of the most common uses of function pointers in embedded systems is in configuring interrupt service routines. Instead of hardcoding the ISR function, a function pointer allows the ISR to be dynamically assigned within a dispatcher, rather than changing the actual hardware ISR.
// In a device driver headertypedef void (*isr_func_t)(void);// Function to set the ISR for a particular interruptvoid set_interrupt_handler(int interrupt_number, isr_func_t handler);// In the application codevoid uart_rx_isr(void) {// Process received byte}// During initializationset_interrupt_handler(UART_RX_IRQ, uart_rx_isr);
This approach enables the same driver code to work with different ISR implementations, enhancing flexibility and reusability.
Function pointers are instrumental in creating hardware abstraction layers where the same API can be used with different underlying hardware implementations.
// HAL structure containing function pointers for pin operationstypedef struct {void (*set_pin_high)(uint8_t pin);void (*set_pin_low)(uint8_t pin);void (*toggle_pin)(uint8_t pin);uint8_t (*read_pin)(uint8_t pin);} gpio_hal_t;// Implementation for a specific microcontrollervoid stm32_set_pin_high(uint8_t pin) { /* STM32-specific code */ }void stm32_set_pin_low(uint8_t pin) { /* STM32-specific code */ }// ... other functions// Initialize the HAL with the specific implementationsgpio_hal_t gpio_hal = {.set_pin_high = stm32_set_pin_high,.set_pin_low = stm32_set_pin_low,.toggle_pin = stm32_toggle_pin,.read_pin = stm32_read_pin};// Application code uses the HAL without knowing the underlying MCUgpio_hal.set_pin_high(5);
This pattern allows the application layer to remain unchanged when switching between different microcontrollers or peripheral implementations.
Function pointers provide an elegant way to implement state machines, where each state is represented by a function pointer, and transitions are achieved by changing the current state pointer.
// Forward declarationtypedef struct state_machine_t state_machine_t;// State function typetypedef void (*state_func_t)(state_machine_t *);// State machine contextstruct state_machine_t {state_func_t current_state;// Other state machine data};// State functionsvoid state_idle(state_machine_t *sm) {// Idle state behaviorif (condition_a) {sm->current_state = state_processing;}}void state_processing(state_machine_t *sm) {// Processing state behaviorif (condition_b) {sm->current_state = state_complete;}}// State machine update functionvoid state_machine_update(state_machine_t *sm) {sm->current_state(sm); // Call the current state function}
This technique results in state machines that are easy to extend and modify, as each state is encapsulated in its own function.
Many embedded libraries and frameworks use function pointers to allow users to customize behavior without modifying the library code. This is particularly useful for event-driven architectures where specific actions need to be taken in response to events like button presses, sensor thresholds, or communication timeouts.
// Event typestypedef enum {EVENT_BUTTON_PRESS,EVENT_SENSOR_THRESHOLD,EVENT_COMM_TIMEOUT} event_t;// Callback function typetypedef void (*event_callback_t)(event_t event, void *context);// Event manager structuretypedef struct {event_callback_t callback;void *context;} event_manager_t;// Register a callbackvoid event_manager_register_callback(event_manager_t *em, event_callback_t cb, void *ctx) {em->callback = cb;em->context = ctx;}// Trigger an eventvoid event_manager_trigger_event(event_manager_t *em, event_t event) {if (em->callback) {em->callback(event, em->context);}}// Application callbackvoid my_event_handler(event_t event, void *context) {switch (event) {case EVENT_BUTTON_PRESS:// Handle button pressbreak;case EVENT_SENSOR_THRESHOLD:// Handle sensor eventbreak;// ... other cases}}// During initializationevent_manager_t em;event_manager_register_callback(&em, my_event_handler, NULL);
This pattern enables the application to extend the functionality of the event manager without altering its source code.
Always initialize function pointers to a known value, preferably NULL if they are not immediately assigned a valid function. Before calling a function through a pointer, verify that the pointer is not NULL to avoid undefined behavior.
callback_func_t cb = NULL;// ... laterif (cb != NULL) {cb(parameter);}
Ensure that the function pointer type exactly matches the signature of the functions it points to. Mismatched signatures can lead to stack corruption or unexpected behavior, especially on architectures with strict calling conventions.
Function pointers themselves typically consume only a few bytes (the size of a pointer on the target architecture). However, be mindful of placing large numbers of function pointers in memory-constrained sections like the stack or small RAM banks.
For scenarios involving multiple related functions (e.g., a set of driver operations or state handlers), consider using arrays of function pointers. This can simplify dispatch logic and improve performance.
// Array of function pointers for different operationsvoid (*operation_table[OP_COUNT])(void) = {op_init,op_read,op_write,op_deinit};// Call operation by indexoperation_table[op_index]();
While function pointers are useful in ISRs for flexibility, they introduce an indirect function call which may add a few cycles of overhead. In time-critical ISRs, direct function calls might be preferable. However, the flexibility often outweighs the minimal performance cost, especially when the ISR is not extremely time-sensitive.
Function pointers are a versatile tool in the embedded C programmer’s arsenal, enabling dynamic behavior, code reuse, and modular design. They are particularly valuable in:
By mastering function pointers, embedded developers can create firmware that is not only functional but also adaptable, maintainable, and scalable. Key takeaways include:
typedef to simplify complex function pointer syntaxAs embedded systems continue to grow in complexity, the ability to write flexible and reusable code becomes increasingly important. Function pointers provide a mechanism to achieve this flexibility without sacrificing the efficiency and control that C offers in the embedded domain.
Quick Links
Legal Stuff





