【FreeRTOS】调试与优化
【FreeRTOS】调试与优化一、调试1.1 打印1.2 断言1.3 Trace1.4 Malloc Hook 函数1.5 栈溢出 Hook 函数二、优化2.1 栈使用情况2.2 任务运行时间统计2.3 涉及的代码2.4 获得统计信息一、调试FreeRTOS 提供了很多调试手段打印断言configASSERTTraceHook 函数(回调函数)1.1 打印printfFreeRTOS 工程里使用了 microlib里面实现了 printf 函数。我们只需实现一下函数即可使用printf#includestm32f10x.h// 你的芯片库#includestdio.h// 必须加不然FILE不认识// 重定向 fputc让 printf 从串口1输出intfputc(intch,FILE*f){// 把一个字节 ch 发给串口USART_SendData(USART1,(uint8_t)ch);// 等待发送完成必须等不然会丢数据while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)RESET);returnch;// 标准要求返回发送的字符}1. 为什么不能直接用printf电脑上的printf最终会把字符输出到屏幕 / 终端这部分是操作系统帮你做好的。但嵌入式单片机printf只是一个 “半成品”它能把你写的printf(“Hello”)拆成一个个字符H、e、l、l、o但不知道把这些字符发到哪里去。microlib微库就是给单片机用的精简版 C 标准库它只提供了printf的 “拆字符” 功能不负责 “发字符”。2. fputc到底是干嘛的fputc就是那个“发字符” 的桥梁printf每拆出一个字符就会调用一次fputc把这个字符传给它。你写的fputc函数要做的就是把这个字符通过串口USART发送到电脑的串口助手这样你就能在电脑上看到打印内容了。说白了printf负责 “组织语言”fputc负责 “张嘴说话”你得自己写 “嘴” 的功能。自己写一个fputc这个函数之后再用printf它会自动调用fputc就可以直接发送给串口助手了。1.2 断言1. 先搞懂「断言」的本质断言就是一个「程序自检工具」用来强制检查代码里的「必须成立的条件」标准 C 库的 assert(表达式)如果表达式为真啥也不做程序正常跑如果为假直接终止程序提醒你这里出问题了。举个例子assert(指针 ! NULL)如果指针是空的说明代码逻辑错了直接停在这里避免后续崩溃。2. FreeRTOS 里的 configASSERT 是什么FreeRTOS 不用标准 C 的 assert而是用自己的宏 configASSERT(x)本质和断言完全一样只是更适合嵌入式、可自定义。① 最基础的实现#defineconfigASSERT(x)if(!(x))while(1);逻辑如果 x 为假!(x) 为真就进入死循环 while(1)程序卡死在这里。作用嵌入式里不能直接「终止程序」用死循环让程序停在错误点方便你调试比如用仿真器看卡死在哪一行。② 带调试信息的增强版#defineconfigASSERT(x)\if(!(x))\{\printf(%s %s %d\r\n,__FILE__,__FUNCTION__,__LINE__);\while(1);\}比基础版多了打印错误位置的功能FILE出错的源文件名比如 main.cFUNCTION出错的函数名LINE出错的代码行号作用不用仿真器直接在串口助手就能看到「哪里出了错」调试效率直接拉满。一句话总结configASSERT 就是 FreeRTOS 版的「程序保镖」检查条件错了就停还能告诉你错在哪帮你快速找 bug。1.3 TraceFreeRTOS 的 Trace 宏追踪宏是嵌入式开发中调试 FreeRTOS 内核行为的神器。核心原理FreeRTOS 在系统的关键节点任务切换、时钟节拍、阻塞等预埋了大量 trace 开头的空宏默认不做任何事。空宏的优势完全不影响代码体积和运行效率开发时可以自定义宏实现调试功能发布时直接还原为空零开销。用途自定义宏来打印日志、统计性能、触发断点、记录任务状态等深度监控内核运行。trace 宏通俗解读典型用法traceTASK_INCREMENT_TICK(xTickCount)系统时钟节拍SysTick中断里在 xTickCount 加 1 之前触发1. 统计系统运行总时长2. 监控节拍中断是否正常触发3. 实现高精度定时/超时校验traceTASK_SWITCHED_OUT()任务切换时旧任务被换出 CPU 之前触发1. 记录任务的 CPU 占用时间2. 统计任务切换次数3. 调试任务调度异常/死锁traceTASK_SWITCHED_IN()任务切换时新任务拿到 CPU 之后触发1. 记录任务的运行起始时间2. 监控高优先级任务是否正常抢占3. 排查任务饿死/调度延迟问题traceBLOCKING_ON_QUEUE_RECEIVE(pxQueue)任务读空队列/信号量/互斥量即将进入阻塞态的瞬间触发1. 调试任务阻塞原因2. 统计任务阻塞时长3. 排查队列/信号量同步逻辑错误traceBLOCKING_ON_QUEUE_SEND(pxQueue)任务往满队列/信号量/互斥量写数据即将进入阻塞态的瞬间触发1. 调试任务阻塞原因2. 统计队列满时的阻塞时长3. 排查队列发送同步问题traceQUEUE_SEND(pxQueue)队列/信号量发送成功时触发覆盖所有任务级发送API1. 统计队列发送成功率2. 监控任务间通信频率3. 调试发送逻辑异常traceQUEUE_SEND_FAILED(pxQueue)队列/信号量发送失败时触发覆盖所有任务级发送API1. 排查发送失败根因队列满、超时等2. 统计发送失败次数3. 优化队列容量设计traceQUEUE_RECEIVE(pxQueue)队列/信号量读取成功时触发覆盖所有任务级读取API1. 统计队列读取成功率2. 监控任务间通信频率3. 调试读取逻辑异常traceQUEUE_RECEIVE_FAILED(pxQueue)队列/信号量读取失败时触发覆盖所有任务级读取API1. 排查读取失败根因队列空、超时等2. 统计读取失败次数3. 优化任务同步逻辑traceQUEUE_SEND_FROM_ISR(pxQueue)中断中发送队列成功时触发1. 监控中断与任务间通信2. 统计中断发送成功率3. 调试中断发送逻辑traceQUEUE_SEND_FROM_ISR_FAILED(pxQueue)中断中发送队列失败时触发1. 排查中断发送失败根因2. 统计中断发送失败次数3. 优化中断处理流程traceQUEUE_RECEIVE_FROM_ISR(pxQueue)中断中读取队列成功时触发1. 监控中断与任务间通信2. 统计中断读取成功率3. 调试中断读取逻辑traceQUEUE_RECEIVE_FROM_ISR_FAILED(pxQueue)中断中读取队列失败时触发1. 排查中断读取失败根因2. 统计中断读取失败次数3. 优化中断处理流程traceTASK_DELAY_UNTIL()任务调用vTaskDelayUntil()即将进入阻塞态的瞬间触发1. 统计任务定时阻塞时长2. 监控任务周期执行情况3. 调试定时任务异常traceTASK_DELAY()任务调用vTaskDelay()即将进入阻塞态的瞬间触发1. 统计任务延时阻塞时长2. 监控任务执行节奏3. 调试延时任务异常1.4 Malloc Hook 函数这是 FreeRTOS 中用于堆内存分配失败兜底的核心钩子函数是嵌入式系统内存故障排查的关键工具。一、核心概念1. 设计背景嵌入式开发中内存越界、栈溢出、堆耗尽是最难排查的硬故障堆内存通过pvPortMalloc分配的动态内存是内存越界的高发区常规逻辑错误可通过调试定位但堆内存耗尽 / 越界往往直接导致系统死机无明确报错2. 钩子函数触发条件当 FreeRTOS 的内存分配函数pvPortMalloc分配失败堆空间不足 时若在FreeRTOSConfig.h中配置#define configUSE_MALLOC_FAILED_HOOK 1系统会自动调用用户实现的回调函数void vApplicationMallocFailedHook( void );二、使用方法与实现示例1. 配置步骤在 FreeRTOSConfig.h 中开启宏#defineconfigUSE_MALLOC_FAILED_HOOK12. 在用户代码中实现钩子函数FreeRTOS 仅声明需用户自定义逻辑voidvApplicationMallocFailedHook(void){// 1. 打印错误日志串口/调试器printf(Malloc failed! Heap exhausted!\r\n);// 2. 记录系统状态任务列表、堆剩余空间vTaskList((char*)pcWriteBuffer);vTaskGetRunTimeStats((char*)pcWriteBuffer);// 3. 安全兜底系统复位/进入死循环等待调试while(1){// 可添加LED闪烁、蜂鸣器报警等调试提示}// 或直接复位NVIC_SystemReset();}1.5 栈溢出 Hook 函数一、核心概念总览栈溢出是嵌入式 RTOS 开发中最致命的问题之一会导致程序跑飞、HardFault、数据篡改等严重故障。FreeRTOS 提供了专门的栈溢出检测机制和 Hook 回调函数用于在开发阶段快速定位栈溢出问题。1. 栈溢出 Hook 函数FreeRTOS 在任务切换时会调用taskCHECK_FOR_STACK_OVERFLOW检测栈是否溢出一旦检测到溢出会自动调用vApplicationStackOverflowHookvoidvApplicationStackOverflowHook(TaskHandle_t xTask,char*pcTaskName);参数说明xTask发生栈溢出的任务句柄pcTaskName发生栈溢出的任务名称作用用户可在该函数中实现自定义处理如打印错误信息、记录任务名、触发断点、重启系统等用于快速定位问题。使用前提必须在FreeRTOSConfig.h中开启configCHECK_FOR_STACK_OVERFLOW配置项。二、两种栈溢出检测方法对比FreeRTOS 提供了两种栈溢出检测方案分别对应configCHECK_FOR_STACK_OVERFLOW的不同配置值特性方法 1configCHECK_FOR_STACK_OVERFLOW 1方法 2configCHECK_FOR_STACK_OVERFLOW 2检测原理任务切换出时检查栈指针是否超出栈底边界创建任务时用 0xa5 填充整个栈检测栈末尾 16 字节是否被篡改性能极高几乎无额外开销较高比方法 1 慢但仍足够快准确性高效但不精确存在漏检风险极高能捕获几乎所有栈溢出适用场景对性能要求极高、资源极度受限的场景开发调试阶段、追求高可靠性的产品漏检场景任务运行中在调用某个函数时栈达到峰值但函数结束栈返还任务还没有运行完任务结束后才被调度无法检测仅当栈末尾被巧合写入 0xa5 时漏检概率极低三、方法 1栈指针边界检测11. 原理任务被切换出 CPU 前会将运行现场压入栈此时栈使用量达到峰值。FreeRTOS 直接检查当前栈指针pxTopOfStack是否小于等于栈底pxStack栈向下增长的架构如 ARM Cortex-M若超出则判定为栈溢出。2. 核心代码#if((configCHECK_FOR_STACK_OVERFLOW1)(portSTACK_GROWTH0))#definetaskCHECK_FOR_STACK_OVERFLOW()\{\/* Is the currently saved stack pointer within the stack limit? */\if(pxCurrentTCB-pxTopOfStackpxCurrentTCB-pxStack)\{\vApplicationStackOverflowHook((TaskHandle_t)pxCurrentTCB,pxCurrentTCB-pcTaskName);\}\}#endif/* configCHECK_FOR_STACK_OVERFLOW 1 */3. 优缺点✅ 优点执行效率极高仅做一次指针比较对系统性能几乎无影响❌ 缺点存在漏检风险。任务运行中在调用某个函数时栈达到峰值但函数结束栈返还任务还没有运行完任务结束后才被调度无法检测到峰值溢出。四、方法 2栈填充检测21. 原理初始化阶段创建任务时用固定值0xa5a5a5a5对应字节0xa5填充整个任务栈检测阶段任务切换时检查栈末尾的 16 字节4 个 32 位字是否仍为0xa5。若被篡改说明栈空间已被耗尽判定为栈溢出。2. 核心代码#if((configCHECK_FOR_STACK_OVERFLOW1)(portSTACK_GROWTH0))#definetaskCHECK_FOR_STACK_OVERFLOW()\{\constuint32_t*constpulStack(uint32_t*)pxCurrentTCB-pxStack;\constuint32_tulCheckValue(uint32_t)0xa5a5a5a5;\\if((pulStack[0]!ulCheckValue)||\(pulStack[1]!ulCheckValue)||\(pulStack[2]!ulCheckValue)||\(pulStack[3]!ulCheckValue))\{\vApplicationStackOverflowHook((TaskHandle_t)pxCurrentTCB,pxCurrentTCB-pcTaskName);\}\}#endif/* #if( configCHECK_FOR_STACK_OVERFLOW 1 ) */3. 优缺点✅ 优点检测精度极高能捕获几乎所有栈溢出包括方法 1 无法检测的峰值溢出❌ 缺点性能略低于方法 1需要读取 4 个栈内存并比较但仍远快于其他调试手段注意:1. 开启检测在FreeRTOSConfig.h中配置// 方法1开启栈指针检测#defineconfigCHECK_FOR_STACK_OVERFLOW1// 方法2开启栈填充检测推荐开发阶段使用// #define configCHECK_FOR_STACK_OVERFLOW 22. 实现 Hook 函数在用户代码中实现vApplicationStackOverflowHook示例voidvApplicationStackOverflowHook(TaskHandle_t xTask,char*pcTaskName){// 打印溢出任务名通过串口输出printf(Stack Overflow! Task: %s\r\n,pcTaskName);// 触发断点方便调试定位__asm(BKPT #0);// 死循环便于仿真器捕获while(1);}二、优化在 Windows 中当系统卡顿时我们可以查看任务管理器找到最消耗 CPU 资源的程序。在FreeRTOS中我们也可以查看任务使用CPU的情况、使用栈的情况然后针对性地进行优化。这就是查看任务的统计信息。2.1 栈使用情况在创建任务时分配了栈可以全部填入固定的数值比如 0xa5以后可以使用以下函数查看栈的高水位也就是还有多少空余的栈空间UBaseType_tuxTaskGetStackHighWaterMark(TaskHandle_t xTask);原理是从栈底往栈顶逐个字节地判断它们的值持续是 0xa5 就表示它是空闲的。函数说明参数/返回值说明xTask哪个任务返回值任务运行时、任务被切换时都会用到栈。栈里原来值(0xa5)就会被覆盖。逐个函数从栈的尾部判断栈的值连续为 0xa5 的个数它就是任务运行过程中空闲内存容量的最小值。注意假设从栈尾开始连续为 0xa5 的栈空间是 N 字节返回值是 N/4。2.2 任务运行时间统计对于同优先级的任务它们按照时间片轮流运行你执行一个 Tick我执行一个 Tick。是否可以在Tick中断函数中统计当前任务的累计运行时间不行很不精确因为有更高优先级的任务就绪时当前任务还没运行一个完整的Tick就被抢占了。我们需要比Tick更快的时钟比如Tick周期时1ms我们可以使用另一个定时器让它发生中断的周期时0.1ms甚至更短。使用这个定时器来衡量一个任务的运行时间原理如下图所示切换到 Task1 时使用更快的定时器记录当前时间 T1Task1 被切换出去时使用更快的定时器记录当前时间 T4(T4-T1)就是它运行的时间累加起来关键点能在 vTaskSwitchContext 函数中使用更快的定时器统计运行时间2.3 涉及的代码配置#defineconfigGENERATE_RUN_TIME_STATS1#defineconfigUSE_TRACE_FACILITY1#defineconfigUSE_STATS_FORMATTING_FUNCTIONS1实现宏 portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()它用来初始化更快的定时器实现这两个宏之一它们用来返回当前时钟值(更快的定时器)portGET_RUN_TIME_COUNTER_VALUE()直接返回时钟值portALT_GET_RUN_TIME_COUNTER_VALUE(Time)设置 Time 变量等于时钟值时钟值 这个高精度定时器 每计数 1 所花的时间代码执行流程初始化更快的定时器启动调度器时在任务切换时统计运行时间2.4 获得统计信息获得统计信息可以使用下列函数1. uxTaskGetSystemState对于每个任务它的统计信息都放在一个 TaskStatus_t结构体里UBaseType_tuxTaskGetSystemState(TaskStatus_t*constpxTaskStatusArray,constUBaseType_t uxArraySize,uint32_t*constpulTotalRunTime);参数 / 返回值描述pxTaskStatusArray指向 TaskStatus_t 结构体数组用于存储所有任务的统计信息任务名、优先级、栈剩余、运行时间等。任务数量可通过 uxTaskGetNumberOfTasks() 获取。uxArraySize数组大小元素个数必须大于或等于 uxTaskGetNumberOfTasks()否则会因空间不足导致填充失败。pulTotalRunTime用于保存系统总运行时间来自高精度定时器可传入 NULL 表示不获取总时间。返回值成功填充的数组项数等于系统任务数若 uxArraySize 小于任务数返回 0表示填充失败。典型使用流程// 1. 获取系统任务数UBaseType_t uxNumTasksuxTaskGetNumberOfTasks();// 2. 分配任务状态数组TaskStatus_t*pxTaskStatusArraypvPortMalloc(uxNumTasks*sizeof(TaskStatus_t));// 3. 调用函数获取任务状态UBaseType_t uxReturnuxTaskGetSystemState(pxTaskStatusArray,uxNumTasks,NULL);// 4. 遍历数组打印任务信息for(inti0;iuxReturn;i){printf(Task: %s, Priority: %d, Stack Left: %d\r\n,pxTaskStatusArray[i].pcTaskName,pxTaskStatusArray[i].uxCurrentPriority,pxTaskStatusArray[i].usStackHighWaterMark);}// 5. 释放内存vPortFree(pxTaskStatusArray);2. vTaskList 获得任务的统计信息形式为可读的字符串。注意pcWriteBuffer 必须足够大。voidvTaskList(signedchar*pcWriteBuffer);典型使用流程// 1. 定义足够大的缓冲区根据任务数调整示例按10个任务预留#defineTASK_LIST_BUFFER_SIZE512signedcharpcWriteBuffer[TASK_LIST_BUFFER_SIZE];// 2. 调用vTaskList生成任务信息字符串vTaskList(pcWriteBuffer);// 3. 通过串口打印输出调试用printf(%s\r\n,pcWriteBuffer);可读信息格式如下3. vTaskGetRunTimeStats获得任务的运行信息形式为可读的字符串。注意pcWriteBuffer 必须足够大。voidvTaskGetRunTimeStats(signedchar*pcWriteBuffer);典型使用流程// 1. 定义足够大的缓冲区根据任务数调整示例按10个任务预留#defineRUN_TIME_STATS_BUFFER_SIZE1024signedcharpcWriteBuffer[RUN_TIME_STATS_BUFFER_SIZE];// 2. 调用vTaskGetRunTimeStats生成运行时间统计字符串vTaskGetRunTimeStats(pcWriteBuffer);// 3. 通过串口打印输出调试用printf(%s\r\n,pcWriteBuffer);可读信息格式如下这三个函数是 FreeRTOS 任务状态 / 运行时间统计的核心工具使用前必须在 FreeRTOSConfig.h 中开启对应宏否则函数无法编译或运行。// 基础依赖所有三个函数都需要#defineconfigUSE_TRACE_FACILITY1// 格式化字符串函数依赖vTaskList / vTaskGetRunTimeStats 需要#defineconfigUSE_STATS_FORMATTING_FUNCTIONS1// 运行时间统计依赖仅 vTaskGetRunTimeStats 需要#defineconfigGENERATE_RUN_TIME_STATS1// 必须手动实现的计时宏以STM32为例用DWT定时器做高精度计时#defineportCONFIGURE_TIMER_FOR_RUN_TIME_STATS()(CoreDebug-DEMCR|CoreDebug_DEMCR_TRCENA_Msk;\DWT-CYCCNT0;\DWT-CTRL|DWT_CTRL_CYCCNTENA_Msk)#defineportGET_RUN_TIME_COUNTER_VALUE()(DWT-CYCCNT)有关【FreeRTOS】调试与优化 内容就到这里希望对你有所帮助感谢观看码文不易留个赞再走吧~参考文章韦东山FreeRTOS入门与工程实践