1. What is a Callback Function?
A Callback Function is a programming pattern implemented through function pointers, characterized by the following core features:
-
Passing Method: Function A (the callback function) is passed as a parameter to Function B.
-
Calling Relationship: Function B calls Function A under specific conditions, in contrast to the traditional top-down calling.
Key Point:
The callback function implements a division of labor model of “you define the logic, I call it,” which is a typical manifestation of Inversion of Control (IoC).
2. Classification of Callback Functions
Based on the timing of triggering, they can be divided into two categories: synchronous callbacks and asynchronous callbacks.
Characteristics in Embedded Scenarios: In MCUs, due to resource constraints and real-time requirements, asynchronous callbacks dominate, for example:
-
Triggering tasks after timer expiration
-
Handling serial port receive complete interrupts
-
ADC sampling complete events
3. Classic Applications of Callback Functions in MCUs
1. Timer Task Scheduling (Combined with Linked List)
Scenario: Implementing multi-task timing triggers (e.g., LED blinking, sensor polling).
Code Example:
// Define task structure (linked list node)typedef struct { uint32_t timer_count; // Timer void (*callback)(void); // Callback function pointer struct Task *next; // Linked list pointer} Task;
Task *task_list_head = NULL; // Task list head
// Register task to the listvoid Task_Register(uint32_t interval_ms, void (*cb)(void)) { Task *new_task = malloc(sizeof(Task)); new_task->timer_count = interval_ms; new_task->callback = cb; new_task->next = task_list_head; task_list_head = new_task;}
// Timer interrupt service function (hardware triggered)void TIM_IRQHandler() { Task *p = task_list_head; while (p != NULL) { if (--p->timer_count == 0) { p->callback(); // Trigger callback! p->timer_count = ...; // Reset timer } p = p->next; }}
In this code, a timer task scheduling system based on callback functions is implemented, which is very suitable for embedded systems.
Next, we will analyze the key parts of the above code block.
1. Data Structure Design: Task Linked List
typedef struct { uint32_t timer_count; // Countdown timer (unit: ms) void (*callback)(void); // Function pointer, pointing to user-defined callback function struct Task *next; // Pointer to the next task} Task;
Task *task_list_head = NULL; // Linked list head pointer
Core Idea:
-
Each task (Task) is a linked list node, containing:
-
timer_count: A dynamically decrementing timer that triggers the callback when it reaches zero.
-
callback: A user-registered function pointer that defines specific task logic (e.g., LED control, sensor reading).
-
next: A key pointer that forms a singly linked list.
-
Linked List Head:
<span>task_list_head</span>points to the first task, and subsequent tasks are linked through<span>next</span>.
2. Task Registration: <span>Task_Register</span> Function
void Task_Register(uint32_t interval_ms, void (*cb)(void)) { Task *new_task = malloc(sizeof(Task)); // Dynamically allocate memory new_task->timer_count = interval_ms; // Initialize timer new_task->callback = cb; // Bind callback function new_task->next = task_list_head; // Insert at head of the list task_list_head = new_task; // Update list head}
Overall Analysis of the Above Code Workflow:
-
The user calls
<span>Task_Register(1000, LED_Toggle)</span>, intending to toggle the LED state every 1000ms. -
The function dynamically creates a new task node and inserts it at the head of the list (an efficient operation with a time complexity of O(1)).
-
The final linked list structure is as follows:
task_list_head -> [Task1] -> [Task2] -> ... -> NULL
3. Task Scheduling: <span>TIM_IRQHandler</span> Interrupt Service Function
void TIM_IRQHandler() { Task *p = task_list_head; while (p != NULL) { // Traverse the list if (--p->timer_count == 0) { // Decrement timer and check p->callback(); // Trigger callback function p->timer_count = ...; // Reset timer (user-defined) } p = p->next; // Move to the next node }}
Key Mechanism:
-
Hardware Trigger: The timer hardware generates an interrupt every 1ms, calling
<span>TIM_IRQHandler</span>. -
List Traversal: Each interrupt traverses all tasks, decrementing their
<span>timer_count</span>. -
Callback Trigger: When a task’s
<span>timer_count</span>reaches zero:
-
Its callback function (e.g.,
<span>LED_Toggle()</span>) is executed. -
The timer is reset (based on business logic, such as fixed intervals or dynamic adjustments).
Performance Considerations:
-
The interrupt context should be kept short to avoid complex operations (e.g., blocking, floating-point calculations).
-
If there are many tasks, optimization can be done using a double-linked list or priority queue.
A double-linked list or priority queue: If supported by a double-linked list, task priority can be implemented, meaning that when there is a task that needs urgent processing in the task scheduler, it can be inserted at the front of the current list to handle urgent tasks.
Role of Callback Functions
-
User Layer: Registers custom logic through
<span>Task_Register</span>(e.g.,<span>LED_Toggle</span>). -
Framework Layer: Timer interrupts reverse call user functions, achieving Inversion of Control (IoC).
Manifestation of Asynchronous Callbacks
-
User code does not know when the callback will be executed, driven by hardware interrupt events.
-
Typical asynchronous scenarios: timed triggers, external interrupts, DMA completion.
Comparison with RTOS
-
Similarity: FreeRTOS’s
<span>xTimerCreate</span><span> also implements timed tasks through callback functions.</span> -
Difference: This example is a lightweight implementation under a bare-metal system, without task priority/preemption mechanisms.
Workflow:
-
The user calls
<span>Task_Register(1000, LED_Toggle)</span>to register a task. -
The hardware timer interrupt traverses the list and triggers the due tasks.
2. Callback Mechanism in RTOS
-
FreeRTOS: Binds callback functions to software timers through
<span>xTimerCreate</span>. -
RT-Thread: Uses
<span>rt_timer_init</span><span> to set timer callbacks.</span><strong><span>Advantages</span></strong><span>:</span> -
Low Coupling: Decouples task logic from the scheduler.
-
High Cohesion: Each callback function focuses solely on its functionality.
4. The Essence and Value of Callback Functions
-
Reverse Control Flow: Allows lower-level code (e.g., driver libraries) to call upper-level code (e.g., application logic), breaking traditional hierarchical constraints.
-
Event-Driven Programming: Transforms the “wait-response” model into an “event-callback” model, enhancing system response efficiency.
-
Engineering Significance:
-
Scalability: New functionalities can be added simply by registering callbacks without modifying framework code.
-
Modularity: Functional isolation facilitates team collaboration and maintenance.
5. Considerations and Challenges
-
Real-Time Performance:
-
Callback functions should be concise to avoid blocking interrupts or tasks.
Resource Sharing:
-
If callbacks access global data, semaphores/atomic operations should be used.
Debugging Difficulty:
-
The call stack of asynchronous callbacks can be deep, requiring logs or debugging tools for tracing.
Conclusion
Callback functions are the “Swiss Army Knife” in embedded systems, achieving:
-
Efficient response to hardware events (e.g., interrupts, timers)
-
Flexible decoupling of system architecture (e.g., RTOS task scheduling)
-
Code reuse and modular design
Mastering the callback mechanism is a key skill for writing efficient and maintainable embedded code!