ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

FreeRTOS CPU使用率统计:原理、方法与STM32实战

2026/9/17 15:00:52 拓冰建站 浏览量
FreeRTOS CPU使用率统计:原理、方法与STM32实战 调试过FreeRTOS项目的工程师十有八九都遇到过这种场景系统跑着跑着偶发卡顿加了个新功能之后看门狗频繁复位或者明明任务都建好了设备却像“死机”一样没反应。排查来排查去最后才发现问题根源是CPU负载已经到了极限。FreeRTOS系统CPU使用率统计就是帮你在系统出症状之前把这层窗户纸捅破的关键工具。这篇文章我结合自己移植和调试过的STM32项目把FreeRTOS统计CPU使用率的原理、三种实现思路、完整代码和调试经验一次讲透。不绕弯子直接说人话适合刚把FreeRTOS跑起来的新手也适合项目已经出问题正在救火的工程师参考。1. 为什么实时系统也要盯CPU使用率1.1 从一次“莫名其妙的看门狗复位”说起先说个真实案例。之前做的一个采集设备逻辑很简单一个传感器采集任务一个数据处理任务一个通信上报任务再加一个喂狗任务。功能联调的时候一切正常一旦放到现场连续跑两天就会偶发复位。查了很久各种排查手段都试了最后把CPU使用率统计加进去一看——CPU负载长期在95%以上通信任务偶尔被其他任务饿死喂狗任务更是时灵时不灵看门狗超时复位。这种问题在裸机开发时代其实不明显因为while(1)大循环里代码跑多快、跑多少心里大致有数。但上了FreeRTOS之后任务调度是抢占式的高优先级任务会打断低优先级任务多个任务叠加在一起CPU资源到底被谁吃掉了光靠读代码很难看出来。1.2 CPU使用率过高会带来哪些连锁反应很多人对“CPU使用率过高”只有个模糊概念觉得可能有点卡但不知道具体后果。在实时操作系统里这个指标直接影响系统稳定性常见的影响包括低优先级任务被饿死。高优先级任务占用CPU时间过长低优先级任务长期得不到调度表现就是某个功能时而正常时而失灵。喂狗任务被延误。如果喂狗任务的优先级不够高或者系统负载过高导致任务切换延迟看门狗很容易超时复位。中断响应延迟增大。CPU长时间在某个任务里忙转关中断或临界区过长时外部中断得不到及时响应这对有严格时序要求的控制类项目是致命的。低功耗失效。电池供电的设备如果CPU长期高于80%系统几乎没有机会进入低功耗模式续航时间成倍缩短。任务堆栈溢出风险上升。高负载下任务上下文切换频繁调用路径更深更容易触发堆栈溢出检测。这也是为什么“FreeRTOS CPU使用率过高”会被很多面试官拿来当综合题——它考察的不只是统计方法本身还有对整个调度模型的理解。1.3 哪些项目最需要这个统计如果你做的是以下这几类项目建议尽早把CPU使用率统计加到系统里带多个通信协议栈的项目Modbus、MQTT、TCP/IP等协议栈本身吃CPU而且不同运行状态下负载差异很大。控制类项目比如电机控制、飞行器、机械臂这类任务对实时性要求极高一旦负载过高导致控制周期抖动后果很严重。低功耗电池设备需要量化每个功能模块的耗电影响。需要做容量评估的项目比如“下一个功能加进来之后当前主控还有没有余量”。在项目初期就把这套统计做好后面排查问题会轻松很多。2. 统计CPU使用率的三种实现思路2.1 核心原理空闲任务就是你的“空转计数器”讲具体方法之前先把最核心的原理说清楚。FreeRTOS是一个抢占式实时操作系统它会创建一个优先级最低的空闲任务IDLE Task。当系统里所有应用任务都在等待事件、处于阻塞或者挂起状态时CPU没有正经事可做调度器就会让空闲任务跑起来。因此CPU使用率可以这样算CPU使用率 (总时间 - 空闲任务运行时间) / 总时间换成人话就是CPU花在“干活”上的时间占总时间的比例。空闲任务跑得越多说明系统越闲空闲任务几乎不跑说明CPU快被榨干了。这个逻辑非常简单所有统计方案都围绕它展开。2.2 思路一空闲任务钩子计数法这是最常用、也是我推荐优先尝试的方案。FreeRTOS提供了一个配置项configUSE_IDLE_HOOK把它设为1之后每次空闲任务被调度执行时系统会自动调用vApplicationIdleHook()这个钩子函数。我们可以在这个钩子函数里做一件事情把一个计数器累加起来。假设系统节拍是1ms一次那么系统运行1000个tick的时间里空闲计数器如果只增加了200说明空闲任务只跑了20%的时间CPU使用率就是80%。这个方案优点是代码量极小、依赖极少只需要两个全局变量加一个统计任务就能跑起来。缺点是它只能统计“整体CPU使用率”看不到具体是哪个任务占用了CPU。不过对于“先判断系统是否超负荷”这个需求来说已经完全够用了。2.3 思路二官方Run Time Stats统计任务级占用如果需要知道“每个任务分别占了多少CPU”就要用FreeRTOS官方提供的Run Time Stats功能。这个方案需要两样东西把configGENERATE_RUN_TIME_STATS设为1让内核在任务切换时维护每个任务的累计运行时间。提供两个宏portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()初始化计时器和portGET_RUN_TIME_COUNTER_VALUE()返回当前计数值。简单说FreeRTOS在每次任务切换发生时会读取当前时间计数器的值累加到即将被切走的那个任务的“运行时间账户”里。所有任务运行时间的总和就等于统计周期内的CPU总时间。拿到数据之后每个任务占用CPU的百分比就是自己的累计运行时间除以总运行时间。用这个方案你能清楚看到是采集任务吃掉了60%的CPU还是通信任务吃掉了40%定位问题非常直接。代价是配置稍微多一点但也就两三个宏的事情。2.4 思路三任务内高精度计时与实测法还有一种更“土”但很实用的方法直接在任务代码里测量单个函数或一段逻辑的执行时间。比如在任务开头把某个GPIO拉高任务结尾拉低用示波器看脉宽或者在关键函数前后分别读取一个高精度定时器的计数值做差得到执行时间。这个方法不受FreeRTOS统计框架限制适用于判断“某个具体函数是不是性能瓶颈”的场景。这个思路在排查性能问题时特别有用我后面会详细展开。它和Run Time Stats是互补关系一个侧重任务维度一个侧重函数维度。2.5 三个方案怎么选我把常见选型维度整理成了下表维度空闲钩子计数法Run Time Stats函数级计时法统计粒度系统整体每个任务每个函数/代码段配置复杂度低中低对系统性能影响极小较小切换时增加几次读计数器操作取决于采样频率主要应用场景快速判断系统负载定位高占用任务分析函数耗时代码量少中少我的建议是先做空闲钩子方案把整体负载趋势看明白如果发现负载确实高再升级到Run Time Stats定位具体任务最后针对可疑任务里的可疑函数做函数级计时。三步走效率最高。3. 手把手实操从CubeMX到统计代码跑起来3.1 环境准备STM32 CubeMX FreeRTOS我这边以STM32F103C8T6为例这也是目前最普及的板子网上适配资料很多。开发环境用Keil MDK中间层通过STM32CubeMX生成工程。CubeMX里集成了FreeRTOS的中间件配置起来比手写移植省事很多而且不容易漏掉关键宏定义。在CubeMX的Middleware and Software Packs里选择FREERTOSInterface选CMSIS_V2新版CubeMX默认都是这个。然后添加一个用于统计的任务任务栈大小512字就够了优先级可以设成Normal。统计任务和业务任务互不影响单独给一个Task就够。有一点要提醒CubeMX生成的代码框架和用户代码用USER CODE BEGIN注释区域做了隔离自己加的代码必须放在这些区域内否则下次重新生成工程时会被覆盖掉。这个习惯一定要养成。3.2 关键配置开启IDLE Hook和Run Time Stats在CubeMX的FREERTOS配置页面里Config Parameters选项卡下有非常多的define参数。开启空闲钩子的方法是找到configUSE_IDLE_HOOK把Disable改成Enable。生成代码之后打开FreeRTOSConfig.h你会看到类似这样的内容#define configUSE_IDLE_HOOK 1 #define configUSE_TICK_HOOK 0 #define configUSE_RUN_TIME_STATS 0如果要用Run Time Stats还需要修改两处把configUSE_RUN_TIME_STATS改为1并添加configUSE_STATS_FORMATTING_FUNCTIONS为1。同时定义一个初始化计时器用的宏和一个读取计数的宏#define configGENERATE_RUN_TIME_STATS 1 #define configUSE_STATS_FORMATTING_FUNCTIONS 1 #define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() CPU_Time_Init() #define portGET_RUN_TIME_COUNTER_VALUE() CPU_Time_Get()这里我建议用Cortex-M3/M4内核自带的DWT模块来做高精度计时不需要额外占用一个定时器外设频率等于系统主频精度非常高。关于DWT的初始化代码我放在后面的3.4节。3.3 代码实现空闲钩子 统计任务先看最简单的空闲钩子计数法。在freertos.c的USER CODE区域添加三个东西一个空闲计数器变量、空闲钩子函数、统计任务函数。/* USER CODE BEGIN Variables */ volatile uint32_t idle_tick_count 0; /* USER CODE END Variables */ /* USER CODE BEGIN 4 */ void vApplicationIdleHook(void) { idle_tick_count; } /* USER CODE END 4 */注意这里的vApplicationIdleHook不能声明为static因为FreeRTOS内部会通过extern方式引用它。函数体里只做计数器累加绝对不要做延时、打印、关中断这类操作理由我放到踩坑部分讲。统计任务的核心逻辑是每隔固定周期读取一次空闲计数值和系统总tick数计算差值然后算出使用率。/* USER CODE BEGIN Header_CpuUsageTask */ void CpuUsageTask(void *argument) /* USER CODE END Header_CpuUsageTask */ { /* USER CODE BEGIN CpuUsageTask */ uint32_t last_idle 0; uint32_t last_total 0; for (;;) { osDelay(1000); // 每秒统计一次 uint32_t cur_idle idle_tick_count; uint32_t cur_total xTaskGetTickCount(); uint32_t idle_inc cur_idle - last_idle; uint32_t total_inc cur_total - last_total; uint32_t cpu_usage 0; if (total_inc 0) { cpu_usage (total_inc - idle_inc) * 100 / total_inc; } printf(CPU Usage: %lu%%\r\n, (unsigned long)cpu_usage); last_idle cur_idle; last_total cur_total; } /* USER CODE END CpuUsageTask */ }这段代码有两个关键细节。第一个是差值计算用当前值减上次值而不是直接用绝对计数这样即使计数器溢出也不怕uint32_t的无符号减法能自动处理回绕问题。第二个是统计周期我用的是1000个tick也就是1秒这个周期足够平滑也不会让统计任务本身占用太多CPU。如果周期太短比如100ms你会看到使用率数值跳来跳去那是因为任务调度本身就有随机性。3.4 进阶用DWT计时器提高统计精度空闲钩子计数法统计的是“空闲任务被调度了多少个tick”实际上在两次tick之间的时间片里空闲任务会不会跑满整个tick取决于中断和调度的时机所以它天然带有最多1个tick的量化误差。如果希望把统计粒度从tick级别提升到CPU周期级别可以用DWT的CYCCNT寄存器。它是个32位的自由运行计数器每一个CPU时钟周期加一。72MHz主频下它每秒钟计数7200万次统计分辨率高了好几个数量级。在main.c或者freertos.c里添加初始化函数void CPU_Time_Init(void) { CoreDebug-DEMCR | CoreDebug_DEMCR_TRCENA_Msk; DWT-CYCCNT 0; DWT-CTRL | DWT_CTRL_CYCCNTENA_Msk; } uint32_t CPU_Time_Get(void) { return DWT-CYCCNT; }然后把空闲钩子改成这样volatile uint32_t idle_cycle_count 0; void vApplicationIdleHook(void) { static uint32_t last_cc 0; uint32_t cc DWT-CYCCNT; idle_cycle_count cc - last_cc; last_cc cc; }统计任务里计算总运行时间时用CPU_Time_Get()的差值作为分母用idle_cycle_count的差值作为分子。这样CPU使用率的波动会小很多。不过要提醒一句DWT的CYCCNT是32位计数器72MHz下大约59.6秒回绕一次。由于我们用的是差值计算回绕不会造成错误但如果你在统计字符串里长期累加运行时间需要注意精度问题。3.5 实操中容易踩的5个坑第一个坑空闲钩子函数里做了阻塞操作。有人想通过空闲任务做低功耗休眠直接在钩子里调用延时函数结果系统调度直接乱了。空闲任务是优先级最低的任务它一旦阻塞调度器会去寻找下一个就绪任务而正常情况下所有任务都在等事件系统就会进入无任务可调度的错误状态。空闲钩子里的代码必须短小精悍不能有任何阻塞。第二个坑CubeMX生成代码后手动修改的FreeRTOSConfig.h被覆盖。很多人在CubeMX图形界面里找不到某个宏就手动加进FreeRTOSConfig.h下次在CubeMX里重新生成时就被回滚了。我的做法是所有自定义宏都放在USER CODE BEGIN区域内比如/* USER CODE BEGIN RTOS_Config */ #define portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() CPU_Time_Init() #define portGET_RUN_TIME_COUNTER_VALUE() CPU_Time_Get() /* USER CODE END RTOS_Config */第三个坑printf占用CPU时间太多。统计任务每秒调用一次printf输出结果有些人会把这个周期缩短到100ms甚至10ms结果统计任务自己就占用了大量CPU统计出来的使用率虚高。调试阶段可以频繁打印性能分析阶段建议把打印周期放长到3到5秒或者把结果存入环形缓冲区需要时再一次性输出。第四个坑统计任务的优先级设置不合适。统计任务建议用Normal优先级不要用最高优先级。如果统计任务的优先级太高它会频繁抢占业务任务导致统计结果里包含了它自己的大量占用同时也会影响系统实时性。第五个坑忘记启用configUSE_STATS_FORMATTING_FUNCTIONS。如果你调用了vTaskGetRunTimeStats()却没有启用这个宏链接时会报错找不到函数。这类问题属于配置遗漏排查起来很费时间。4. CPU使用率过高按这个顺序排查4.1 先确认是“整体高”还是“单个任务高”统计工具只负责告诉你“现在负载多高”不负责解释“为什么高”。排查的第一步是要区分两个方向如果整体CPU使用率一直很高但每个任务的单独占用都不高说明忙碌时间主要发生在空闲期之外的系统调用、中断处理或者存在频繁的上下文切换。如果某个具体任务的运行时间占比很高比如打印任务占了60%那重点盯住这个任务的代码逻辑。用Run Time Stats方案你可以每5秒打印一次所有任务的状态观察任务名、运行时间计数器和百分比。先看占据前两名的任务90%的CPU问题都集中在它们身上。4.2 常见元凶代码长什么样根据我的调试经验嵌入式系统的CPU使用率异常绝大多数来自下面这几类代码模式轮询等待典型写法是while (1) { if (HAL_GPIO_ReadPin(...) RESET) { break; } }这种忙等会占满整个时间片。如果等待的是一个短期事件问题不大如果等待的时间不确定应该改成信号量或事件标志组让任务在等待时进入阻塞态。过度的log输出算一个。代码里到处是printf尤其是通过串口逐字节发送的场景。一只115200bps的串口每秒大约只能传1.1万个字符printf如果没有任何缓冲机制一个几百字符的日志就能阻塞好几个毫秒。平时调试没问题生产环境必须隔离日志输出或者改用DMA发送。临界区和关中断过长的代码也比较常见。FreeRTOS的taskENTER_CRITICAL()和taskEXIT_CRITICAL()之间不要做复杂运算、不要调用打印函数。临界区之内所有中断被屏蔽系统时钟节拍也进不来这段时间不会算在任何任务的运行时间里但实际CPU就是在等待所以它会直接拉高整体使用率。无谓的延迟方式比如用for(j0;j10000;j);这样的软件延时在RTOS里是必须杜绝的所有延时都应该用osDelay让出CPU。中断服务函数里做过多工作也很常见。中断处理逻辑应该遵循“快进快出”的原则中断里只做标记和数据的快速搬运真正的处理放在任务里做。如果中断里做浮点运算或者字符串处理整个系统的可用CPU会被大量吞掉。4.3 从现象到根因一个看门狗复位的排查例子说一个我实际处理过的例子。现象是系统运行一段时间后看门狗复位表面看起来完全是随机故障。我第一步先开启了空闲钩子统计发现CPU使用率在启动瞬间冲到100%然后稳定在30%左右。如果只是整体高问题不会偶发所以又切换到了Run Time Stats。结果发现有一个通信任务在每次进行大量数据打包时运行时间占比从20%飙到80%而且持续时间长达几十毫秒。第二步我在通信任务的打包函数前后增加了GPIO翻转用示波器实测这段代码耗时发现最大耗时能达到40ms。拆开代码逐段排查后发现罪魁祸首是候用了一个低效的字符串拼接函数数据量一大就慢得离谱。第三步换成更高效的缓冲区和批量拷贝方式打包耗时降到了200微秒以内。CPU使用率整体降到了15%看门狗复位问题再也没出现过。这个例子说明统计工具给出的数字只能帮你缩小范围真正找到根因还得靠代码级别的深入分析。但如果没有第一步和第二步的量化数据我可能还要在几个可疑模块之间来回猜很久。5. 常见问题与面试问答实录5.1 统计不准的几种典型情况统计结果出现异常先不要怀疑FreeRTOS内核有bug大部分情况都是用法问题。下面是几种最常见的异常表现和对应原因。现象可能原因处理方式使用率一直是100%空闲钩子没开启或某高优先级任务忙等检查configUSE_IDLE_HOOK用Run Time Stats查哪个任务占用高使用率数值剧烈跳动统计周期太短把统计周期拉长到1秒以上使用率偏高但系统流畅printf干扰了统计任务本身降低打印频率或打印缓冲区加DMA使用率偏低但系统卡顿占用时间发生在了中断或临界区里使用DWT高精度计数检查中断里是否有较重的计算低功耗模式下数值异常进入睡眠后系统节拍停止空闲计数不准低功耗场景不使用tick级统计改用外部实时时钟还有一种情况容易被忽略统计任务本身被创建时使用了较高的优先级并且频繁打印它自己就成了个“吃CPU大户”统计出来的数值永远虚高。这类问题的本质不是统计算法错误而是统计行为会改变被统计的系统。尽量减少统计动作本身的开销是保证结果真实的前提。5.2 面试官爱问的4个问题FreeRTOS是嵌入式岗位面试的高频考点CPU使用率统计经常会作为追问出现。我整理了四个常见问题供正在准备面试的朋友参考。第一个问题FreeRTOS怎么统计CPU使用率推荐这样回答核心思路是通过空闲任务运行时间的比例反推CPU占比。可以在空闲任务钩子里累加空闲计数周期任务采样计算整体使用率如果要精确到每个任务则开启configGENERATE_RUN_TIME_STATS配合高分辨率计时器用uxTaskGetSystemState获取每个任务的ulRunTimeCounter再除以总运行时间。第二个问题空闲任务钩子函数里能调用vTaskDelay吗答案是不能。空闲任务优先级最低它在系统没有其他就绪任务时才会运行。如果在钩子里调用阻塞函数空闲任务进入阻塞态后系统将没有任务可调度这与RTOS的调度模型冲突会导致系统异常。第三个问题怎么判断某个任务占用CPU过高是代码问题还是优先级问题这个问题考察的是分析和排查能力。可以先统计所有任务的运行时间占比找到异常任务再通过函数级计时确认是不是某段代码耗时过长最后看这个任务的优先级是否合理是否存在高优先级任务频繁抢占导致低优先级任务饿死。要结合数据和调度模型一起分析不能只看单一因素。第四个问题CPU使用率100%一定有问题吗不一定是致命问题但需要关注。如果是事件驱动型系统空闲时间占比高才是常态对于控制类或实时采集类系统长期满载意味着没有设计余量一旦出现突发任务或者中断风暴系统响应就会延迟。关键看业务场景对实时性的要求。6. 一点个人心得做了这么多年嵌入式开发我的感受是CPU使用率统计这类工具就像体温计它不会直接给你治病但能告诉你是哪个环节出了问题。FreeRTOS里实现这个统计其实门槛很低核心就一句话“空闲任务跑了多少反推出CPU忙了多久”。但真正用好它靠的是排查经验的积累。几个实操建议送给大家。第一项目一初始化就把空闲钩子统计加上代码不到十行后期排查问题的成本能省一半。第二定性能问题时多用Run Time Stats配合稳定的统计周期不要频繁打印。第三遇到偶发性问题时GPIO翻转加示波器永远是最可靠的定位手段比任何软件工具都直接。第四统计代码本身要保持轻量不要在钩子里做复杂操作统计出来的结果才真实可信。最后再分享一个小技巧如果遇到“CPU看着不高但系统还是卡”的情况别急着怀疑统计不准建议先检查是不是某个中断处理时间过长或者系统里某个互斥锁导致任务频繁阻塞。这种问题光看使用率看不出来但把示波器探头夹到关键任务的GPIO翻转脚上波形会直接告诉你答案。