回调函数与钩子机制总结

张开发
2026/4/11 6:23:22 15 分钟阅读

分享文章

回调函数与钩子机制总结
一、什么是回调函数回调函数就是留一个空位让你把你的函数填进去系统在合适的时候调用它。想象你点了一份外卖你告诉外卖员你的电话号码。外卖员不会用你的号码做什么事但到了你家楼下他会打这个电话通知你。这个电话就是回调——系统在合适的时机调用你提供的函数。二、函数指针回调的基础c// 这行代码定义了一个函数指针类型 // 意思是声明一个指向参数是int返回void的函数的指针 typedef void (*Callback_t)(int value);可以理解为定义了一个合同模板规定回调函数必须长什么样。三、回调机制的四个步骤步骤1定义函数指针类型定合同ctypedef void (*ActionCallback_t)(uint8_t group, uint8_t index);步骤2声明函数指针变量留空位cstatic ActionCallback_t on_action_start NULL; // 存储开始回调 static ActionCallback_t on_action_complete NULL; // 存储完成回调步骤3提供注册函数让用户填内容cvoid Action_RegisterCallbacks(ActionCallback_t on_start, ActionCallback_t on_complete) { on_action_start on_start; // 用户把函数地址填进来 on_action_complete on_complete; }步骤4在合适时机调用系统触发cvoid Action_ExecuteTask(void) { if(on_action_start) on_action_start(group, index); // 调用用户函数 // 执行动作... if(on_action_complete) on_action_complete(group, index); }四、用户如何使用填内容c// 用户自己实现回调函数 void ui_on_action_start(uint8_t group, uint8_t index) { lv_label_set_text(guider_ui.screen_1_label_status, 正在执行); } void ui_on_action_complete(uint8_t group, uint8_t index) { lv_label_set_text(guider_ui.screen_1_label_status, 执行完成); } // 注册把函数地址告诉底层 Action_RegisterCallbacks(ui_on_action_start, ui_on_action_complete);五、FreeRTOS 中的钩子函数FreeRTOS 中有几个典型的钩子函数原理和回调完全一样。注意使用钩子函数前需要在 FreeRTOSConfig.h 中开启对应的宏。1. 空闲任务钩子Idle Hook作用当系统没有任务运行时空闲任务会执行这个钩子函数。开启宏configUSE_IDLE_HOOKc// FreeRTOSConfig.h 中开启 #define configUSE_IDLE_HOOK 1 // 1开启0关闭 // FreeRTOS 提供弱定义备胎 void vApplicationIdleHook(void) __attribute__((weak)) { // 默认什么都不做 } // 用户在自己的代码中重写强定义 void vApplicationIdleHook(void) { // 用户想空闲时做的事比如进入低功耗、喂狗等 EnterLowPowerMode(); // 注意空闲钩子中不能调用阻塞函数 }2. 滴答钩子Tick Hook作用每个系统时钟节拍Tick都会调用一次在定时器中断中执行。开启宏configUSE_TICK_HOOKc// FreeRTOSConfig.h 中开启 #define configUSE_TICK_HOOK 1 // 1开启0关闭 // 弱定义备胎 void vApplicationTickHook(void) __attribute__((weak)) { } // 用户重写 void vApplicationTickHook(void) { // 每 tick 执行一次比如喂狗、计时器更新 // 注意滴答钩子在中断中执行必须非常短 WatchDog_Feed(); software_timer_update(); }3. 栈溢出钩子Stack Overflow Hook作用当检测到任务栈溢出时调用用于错误处理。开启宏configCHECK_FOR_STACK_OVERFLOWc// FreeRTOSConfig.h 中开启 #define configCHECK_FOR_STACK_OVERFLOW 2 // 1或22更严格 // 弱定义 void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) __attribute__((weak)) { } // 用户重写处理栈溢出错误 void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { // 保存错误信息到Flash SaveErrorToFlash(pcTaskName); // 串口打印错误 printf(错误任务 %s 栈溢出了\r\n, pcTaskName); // 可选重启系统或进入安全模式 NVIC_SystemReset(); }4. 内存分配失败钩子Malloc Failed Hook作用当动态内存分配失败时调用。开启宏configUSE_MALLOC_FAILED_HOOKc// FreeRTOSConfig.h 中开启 #define configUSE_MALLOC_FAILED_HOOK 1 // 弱定义 void vApplicationMallocFailedHook(void) __attribute__((weak)) { } // 用户重写 void vApplicationMallocFailedHook(void) { printf(错误动态内存分配失败\r\n); // 尝试释放内存或重启 }5. 任务创建/删除钩子Task Creation/Deletion Hook作用任务创建和删除时调用用于调试跟踪。开启宏configUSE_TRACE_FACILITY和相关宏c// FreeRTOSConfig.h 中开启 #define configUSE_TRACE_FACILITY 1 // 弱定义 void vTaskDeleteHook(TaskHandle_t xTask) __attribute__((weak)) { } // 用户重写 void vTaskDeleteHook(TaskHandle_t xTask) { // 记录任务删除事件 printf(任务被删除句柄%p\r\n, xTask); }六、FreeRTOS钩子函数的宏控制汇总钩子函数控制宏说明vApplicationIdleHookconfigUSE_IDLE_HOOK空闲时执行vApplicationTickHookconfigUSE_TICK_HOOK每个Tick执行中断上下文vApplicationStackOverflowHookconfigCHECK_FOR_STACK_OVERFLOW栈溢出时执行vApplicationMallocFailedHookconfigUSE_MALLOC_FAILED_HOOK内存分配失败时执行vApplicationDaemonTaskStartupHookconfigUSE_DAEMON_TASK_STARTUP_HOOK守护任务启动时执行七、两种实现方式的对比方式实现优点缺点典型例子函数指针注册底层声明指针用户调用注册函数可动态改变灵活代码较多你的Action系统弱定义(__weak)底层提供弱定义用户直接重写简单直接编译时固定FreeRTOS钩子、HAL库回调FreeRTOS 用的是弱定义方式HAL库也用的是弱定义方式。两者本质相同只是写法不同。八、本质理解概念本质函数指针一个变量存储的是函数的地址回调函数通过函数指针调用的用户函数注册把用户函数地址赋值给函数指针弱定义编译器的备胎机制用户可覆盖钩子函数系统预留的扩展点用户可填充逻辑宏控制编译时开关控制钩子功能是否启用节省代码空间九、宏控制的本质c// FreeRTOSConfig.h #define configUSE_IDLE_HOOK 1 // 开启空闲钩子 // FreeRTOS 源码中 #if (configUSE_IDLE_HOOK 1) // 只有当宏开启时才会调用钩子函数 if(xTaskGetSchedulerState() taskSCHEDULER_RUNNING) { vApplicationIdleHook(); } #endif如果不开启宏钩子函数不会被编译和调用可以节省代码空间和CPU时间。十、对你的项目的建议场景推荐方式理由固定UI不换框架直接调用LVGL函数最简单代码可能被复用回调注册解耦底层通用想快速简单弱定义代码最少需要编译时裁剪加宏控制节省代码空间十一、一句话总结回调就是系统预留一个空位函数指针用户把自己的函数填进去注册系统在合适的时机执行它。FreeRTOS的钩子函数本质上就是回调只是用了弱定义的写法并通过宏控制是否启用以节省资源。

更多文章