FreeRTOS 中的 Hook 回调函数详解
在学习 FreeRTOS 时经常会看到一些名字比较特殊的函数例如void vApplicationIdleHook(void); void vApplicationTickHook(void); void vApplicationMallocFailedHook(void); void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName);这些函数并不是普通任务函数而是 FreeRTOS 提供的Hook 回调函数。一、什么是 Hook 回调函数Hook可以理解为“钩子函数”。FreeRTOS 在系统运行的某些关键位置提前预留了一些函数接口。用户只要按照规定的函数名和参数格式实现这些函数FreeRTOS 在运行到对应位置时就会自动调用这些函数。简单理解Hook 函数 FreeRTOS 在特定时机自动调用的用户函数它和普通函数最大的区别是普通函数用户自己主动调用 Hook 函数FreeRTOS 内核在特定时机自动调用例如void vApplicationIdleHook(void) { // 用户自己写的代码 }这个函数不需要用户在main()中调用。当系统进入空闲任务时FreeRTOS 会自动调用它。二、Hook 函数的使用条件使用 FreeRTOS Hook 函数一般需要满足三个条件1. 在 FreeRTOSConfig.h 中打开对应宏 2. 按照固定函数名实现函数 3. 函数参数和返回值必须符合要求例如使用空闲任务 Hook需要在FreeRTOSConfig.h中配置#define configUSE_IDLE_HOOK 1然后在工程中实现void vApplicationIdleHook(void) { // 空闲任务运行时执行 }只要宏打开并且函数名正确FreeRTOS 就会自动调用这个函数。三、常见 Hook 函数FreeRTOS 中比较常见的 Hook 函数有vApplicationIdleHook() 空闲任务 Hook vApplicationTickHook() Tick 中断 Hook vApplicationMallocFailedHook() 内存申请失败 Hook vApplicationStackOverflowHook() 栈溢出 Hook vApplicationDaemonTaskStartupHook() 守护任务启动 Hook下面分别介绍。四、空闲任务 HookvApplicationIdleHook1. 开启方式在FreeRTOSConfig.h中打开#define configUSE_IDLE_HOOK 1然后实现函数void vApplicationIdleHook(void) { // 空闲任务运行时执行 }2. 什么时候执行当系统中没有其他就绪任务可以运行时FreeRTOS 会运行空闲任务。空闲任务运行过程中会调用vApplicationIdleHook();执行流程可以理解为其他任务都阻塞 / 延时 / 挂起 | v 系统运行空闲任务 | v 调用 vApplicationIdleHook()3. 适合做什么空闲任务 Hook 适合做一些低优先级、轻量级的事情例如低功耗处理 喂看门狗 简单状态统计 CPU 空闲率统计 后台清理工作例如void vApplicationIdleHook(void) { // 喂看门狗 // IWDG_ReloadCounter(); // 或者进入低功耗 // __WFI(); }4. 注意事项空闲任务 Hook 中不要写死循环。错误写法void vApplicationIdleHook(void) { while (1) { // 错误 } }因为vApplicationIdleHook()本身就是在空闲任务循环中被调用的。也不要在里面调用阻塞函数例如vTaskDelay(); xQueueReceive(..., portMAX_DELAY); xSemaphoreTake(..., portMAX_DELAY);五、Tick HookvApplicationTickHook1. 开启方式在FreeRTOSConfig.h中打开#define configUSE_TICK_HOOK 1然后实现void vApplicationTickHook(void) { // Tick 中断中执行 }2. 什么时候执行FreeRTOS 每产生一次系统节拍中断就会调用一次 Tick Hook。执行流程可以理解为SysTick 中断发生 | v FreeRTOS 处理系统节拍 | v 调用 vApplicationTickHook()也就是说vApplicationTickHook()是在中断中执行的。3. 适合做什么Tick Hook 适合做非常短、非常快的周期性处理例如简单计数 设置标志位 短周期状态检测 从中断中通知任务例如volatile uint32_t tick_count 0; void vApplicationTickHook(void) { tick_count; }也可以在 Tick Hook 中通知某个任务void vApplicationTickHook(void) { BaseType_t xHigherPriorityTaskWoken pdFALSE; vTaskNotifyGiveFromISR(TaskHandle, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); }4. 注意事项Tick Hook 在中断中执行所以不能写耗时代码。不建议这样写void vApplicationTickHook(void) { printf(tick\r\n); // 不建议 }也不能调用普通的阻塞 API。错误示例void vApplicationTickHook(void) { vTaskDelay(10); // 错误 }Tick Hook 中应该只做短小、快速的操作。六、内存申请失败 HookvApplicationMallocFailedHook1. 开启方式在FreeRTOSConfig.h中打开#define configUSE_MALLOC_FAILED_HOOK 1然后实现void vApplicationMallocFailedHook(void) { // 内存申请失败时执行 }2. 什么时候执行当 FreeRTOS 动态申请内存失败时会调用这个函数。例如下面这些操作可能会申请内存xTaskCreate(); xQueueCreate(); xSemaphoreCreateMutex(); xTimerCreate(); pvPortMalloc();如果堆空间不足就会进入vApplicationMallocFailedHook();3. 常见原因进入这个 Hook通常说明 FreeRTOS 的堆空间不够。常见原因有configTOTAL_HEAP_SIZE 配置太小 任务栈分配太大 创建了太多任务 创建了太多队列、信号量、定时器 重复申请内存但没有释放4. 示例代码void vApplicationMallocFailedHook(void) { taskDISABLE_INTERRUPTS(); while (1) { // 可以在这里打断点方便调试 } }实际项目中也可以点亮错误 LEDvoid vApplicationMallocFailedHook(void) { taskDISABLE_INTERRUPTS(); Error_LED_On(); while (1); }七、栈溢出 HookvApplicationStackOverflowHook1. 开启方式在FreeRTOSConfig.h中配置#define configCHECK_FOR_STACK_OVERFLOW 2然后实现void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { // 任务栈溢出时执行 }2. 什么时候执行当 FreeRTOS 检测到某个任务栈可能溢出时会调用vApplicationStackOverflowHook(xTask, pcTaskName);参数含义xTask 出问题的任务句柄 pcTaskName 出问题的任务名字3. 示例代码void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { taskDISABLE_INTERRUPTS(); printf(Stack overflow: %s\r\n, pcTaskName); while (1) { // 可以在这里打断点 } }如果不使用参数可以这样写避免编译警告void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { (void)xTask; (void)pcTaskName; taskDISABLE_INTERRUPTS(); while (1); }4. 常见原因进入栈溢出 Hook通常说明某个任务栈不够。常见原因有任务栈设置太小 任务中定义了很大的局部数组 函数调用层级太深 递归调用 printf 占用较多栈空间例如下面这种写法容易导致任务栈溢出void MyTask(void *argument) { uint8_t buffer[2048]; while (1) { // 任务代码 } }如果数组比较大可以改成全局变量或静态变量static uint8_t buffer[2048];八、守护任务启动 HookvApplicationDaemonTaskStartupHook1. 开启方式在FreeRTOSConfig.h中打开#define configUSE_TIMERS 1 #define configUSE_DAEMON_TASK_STARTUP_HOOK 1然后实现void vApplicationDaemonTaskStartupHook(void) { // 守护任务第一次运行时执行 }2. 什么时候执行FreeRTOS 中的软件定时器由定时器服务任务管理这个任务也叫守护任务。当调度器启动后守护任务第一次运行时会调用vApplicationDaemonTaskStartupHook();流程可以理解为vTaskStartScheduler() | v 调度器开始运行 | v 守护任务第一次运行 | v 调用 vApplicationDaemonTaskStartupHook()3. 适合做什么这个 Hook 适合做一些需要在调度器启动后才能执行的初始化操作。例如启动软件定时器 发送初始化事件 启动依赖 RTOS 的模块示例void vApplicationDaemonTaskStartupHook(void) { xTimerStart(xTimer1, 0); }九、Hook 函数和普通回调函数的区别普通回调函数一般需要用户主动注册。例如软件定时器回调函数xTimerCreate( Timer1, 1000, pdTRUE, NULL, TimerCallback );这里的TimerCallback是用户主动传进去的。而 Hook 函数不同。Hook 函数一般不需要主动注册而是依靠固定函数名 配置宏例如#define configUSE_IDLE_HOOK 1 void vApplicationIdleHook(void) { }只要宏打开并且函数名正确FreeRTOS 内核就会自动调用它。十、Hook 函数的内部机制以 Idle Hook 为例。用户在FreeRTOSConfig.h中打开#define configUSE_IDLE_HOOK 1然后实现void vApplicationIdleHook(void) { // 用户代码 }FreeRTOS 内部大概类似这样static void prvIdleTask(void *pvParameters) { for (;;) { #if (configUSE_IDLE_HOOK 1) { vApplicationIdleHook(); } #endif } }也就是说FreeRTOS 在空闲任务中预留了一个调用位置。如果用户打开了宏内核就会调用用户实现的 Hook 函数。Tick Hook 也是类似道理void xPortSysTickHandler(void) { // FreeRTOS 处理系统节拍 #if (configUSE_TICK_HOOK 1) { vApplicationTickHook(); } #endif }所以 Tick Hook 是在系统节拍中断中执行的。十一、Hook 函数一般写在哪里Hook 函数可以写在main.c freertos.c app_freertos.c freertos_hooks.c只要这个.c文件被加入工程编译即可。实际项目中可以单独新建一个文件freertos_hooks.c然后把 Hook 函数统一放进去#include FreeRTOS.h #include task.h void vApplicationMallocFailedHook(void) { taskDISABLE_INTERRUPTS(); while (1); } void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { (void)xTask; (void)pcTaskName; taskDISABLE_INTERRUPTS(); while (1); }这样工程结构会更清晰。十二、常见配置整理在FreeRTOSConfig.h中常见配置如下#define configUSE_IDLE_HOOK 1 #define configUSE_TICK_HOOK 1 #define configUSE_MALLOC_FAILED_HOOK 1 #define configCHECK_FOR_STACK_OVERFLOW 2 #define configUSE_DAEMON_TASK_STARTUP_HOOK 1对应需要实现的函数void vApplicationIdleHook(void); void vApplicationTickHook(void); void vApplicationMallocFailedHook(void); void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName); void vApplicationDaemonTaskStartupHook(void);十三、使用 Hook 函数的常见错误1. 只写函数没有打开宏错误情况void vApplicationIdleHook(void) { }但是没有配置#define configUSE_IDLE_HOOK 1这样 FreeRTOS 不会调用这个函数。2. 函数名写错错误写法void ApplicationIdleHook(void) { }正确写法void vApplicationIdleHook(void) { }Hook 函数的名字必须和 FreeRTOS 要求的一致。3. 函数参数写错错误写法void vApplicationStackOverflowHook(void) { }正确写法void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { }4. 在 Tick Hook 中写耗时代码错误写法void vApplicationTickHook(void) { printf(tick\r\n); }Tick Hook 在中断中执行不适合写耗时代码。5. 在 Idle Hook 中调用阻塞函数错误写法void vApplicationIdleHook(void) { vTaskDelay(100); }Idle Hook 是空闲任务的一部分不应该阻塞空闲任务。十四、实际项目推荐在学习和调试 FreeRTOS 时建议优先打开这两个 Hook#define configUSE_MALLOC_FAILED_HOOK 1 #define configCHECK_FOR_STACK_OVERFLOW 2并实现void vApplicationMallocFailedHook(void) { taskDISABLE_INTERRUPTS(); while (1) { // 内存申请失败打断点排查 } } void vApplicationStackOverflowHook(TaskHandle_t xTask, char *pcTaskName) { (void)xTask; (void)pcTaskName; taskDISABLE_INTERRUPTS(); while (1) { // 栈溢出打断点排查 } }这两个 Hook 对排查任务创建失败、系统异常、任务跑飞等问题非常有用。十五、总结FreeRTOS Hook 回调函数就是系统预留给用户的回调接口。它的核心特点是固定函数名 固定参数格式 需要打开配置宏 由 FreeRTOS 内核自动调用常见 Hook 函数如下vApplicationIdleHook() 空闲任务运行时调用 vApplicationTickHook() 系统 Tick 中断中调用 vApplicationMallocFailedHook() 动态内存申请失败时调用 vApplicationStackOverflowHook() 任务栈溢出时调用 vApplicationDaemonTaskStartupHook() 守护任务第一次运行时调用一句话总结FreeRTOS Hook 函数就是 FreeRTOS 在特定时机自动调用的用户函数用来让用户在系统运行的关键位置插入自己的处理代码。