在学习 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下面分别介绍。
四、空闲任务 Hook:vApplicationIdleHook
1. 开启方式
在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 Hook:vApplicationTickHook
1. 开启方式
在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 中应该只做短小、快速的操作。
六、内存申请失败 Hook:vApplicationMallocFailedHook
1. 开启方式
在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) { // 可以在这里打断点,方便调试 } }实际项目中也可以点亮错误 LED:
void vApplicationMallocFailedHook(void) { taskDISABLE_INTERRUPTS(); Error_LED_On(); while (1); }七、栈溢出 Hook:vApplicationStackOverflowHook
1. 开启方式
在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];八、守护任务启动 Hook:vApplicationDaemonTaskStartupHook
1. 开启方式
在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 在特定时机自动调用的用户函数,用来让用户在系统运行的关键位置插入自己的处理代码。