目录
一:vTaskGetRunTimeStats() — 任务运行时间统计
1:函数原型
2:需要的三个宏(缺一不可)
3:输出格式
4:跟 uxTaskGetSystemState() 的关系
5:时基定时器的要求
6:跟昨天学的 API 对比:
二:最终改动清单 — rtos\8\ 任务运行时间统计实验
TIM6.h
TIM6.c
stm32f4xx_it.c
FreeRTOSConfig.h
main.c
freertos_demo.c — 整个文件重写
Key.c
完整中断链路
跟正点代码的对比
三:rtos\8 任务运行时间轴
启动阶段
start_task(优先级 1,活不过 1ms)
稳态运行(无限循环)
CPU 占用占比(为什么 IDLE 是 99%)
按下 KEY1 那一刻
一:vTaskGetRunTimeStats()— 任务运行时间统计
1:函数原型
void vTaskGetRunTimeStats( char *pcWriteBuffer );
| 形参 | 含义 |
|---|---|
pcWriteBuffer | 输出缓冲区指针,写入格式化后的表格字符串。每个任务约需 40 字节 |
返值:无。数据写入pcWriteBuffer。
2:需要的三个宏(缺一不可)
| 宏 | 在哪设 | 干什么 |
|---|---|---|
configGENERATE_RUN_TIME_STATS = 1 | FreeRTOSConfig.h | 开运行时间统计功能 |
configUSE_STATS_FORMATTING_FUNCTIONS = 1 | FreeRTOSConfig.h | 开格式化输出(昨天已开 ✅) |
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() | 我们自己在 FreeRTOSConfig.h 实现 | 初始化一个高精度时基定时器 |
portGET_RUN_TIME_COUNTER_VALUE() | 我们自己在 FreeRTOSConfig.h 实现 | 读该定时器的当前计数值 |
3:输出格式
Task Abs Time % Time task2 12345 48% task1 10000 39% IDLE 3333 13% start_task 0 <1%
| 列 | 含义 |
|---|---|
| Task | 任务名 |
| Abs Time | 该任务自启动以来累计跑了多久(计时器 tick 数) |
| % Time | 占总 CPU 时间的百分比 |
4:跟uxTaskGetSystemState()的关系
vTaskGetRunTimeStats内部就是调了uxTaskGetSystemState(),然后把它返回的ulRunTimeCounter字段格式化成表格。核心逻辑(源码 tasks.c):
// 简化版内部实现 void vTaskGetRunTimeStats(char *pcWriteBuffer) { TaskStatus_t *pxTaskStatusArray; UBaseType_t uxArraySize; uint32_t ulTotalRunTime; uxArraySize = uxTaskGetNumberOfTasks(); pxTaskStatusArray = pvPortMalloc(uxArraySize * sizeof(TaskStatus_t)); if (pxTaskStatusArray != NULL) { // ★ uxTaskGetSystemState 第三个参数传 &ulTotalRunTime(不是 NULL 了!) uxArraySize = uxTaskGetSystemState(pxTaskStatusArray, uxArraySize, &ulTotalRunTime); // 用 sprintf 格式化每个任务的百分比 for (x = 0; x < uxArraySize; x++) { ulStatsAsPercentage = pxTaskStatusArray[x].ulRunTimeCounter / ulTotalRunTime; sprintf(pcWriteBuffer, "%s\t\t%lu\t\t%lu%%\r\n", ...); } vPortFree(pxTaskStatusArray); } }5:时基定时器的要求
计时精度需高于系统时钟节拍精度的10 至 100 倍
我们 SysTick 是 1ms(1KHz),所以时基定时器至少10KHz ~ 100KHz。比如用 TIM6 配 10KHz = 0.1ms 精度。
6:跟昨天学的 API 对比:
vTaskList | vTaskGetRunTimeStats | |
|---|---|---|
| 输出内容 | 状态、优先级、栈剩余 | 运行时间、CPU 占比 |
| 内部调用 | uxTaskGetSystemState | uxTaskGetSystemState |
| 额外宏 | configUSE_STATS_FORMATTING_FUNCTIONS | 还要configGENERATE_RUN_TIME_STATS+ 两个 port 宏 |
| 需要外设 | 无 | 需要一个硬件定时器(不能用 SysTick,它已经是心跳了) |
二:最终改动清单 — rtos\8\ 任务运行时间统计实验
TIM6.h
| 行号 | 内容 |
|---|---|
| 12-13 | 新增RT_STATS_PSC(89)/RT_STATS_ARR(9)— 100KHz 时基宏 |
| 17 | 新增void ConfigureTimeForRunTimeStats(void)声明 |
| 18 | 新增extern uint32_t FreeRTOSRunTimeTicks全局计数器声明 |
TIM6.c
| 行号 | 内容 |
|---|---|
| 7 | 新增uint32_t FreeRTOSRunTimeTicks全局计数器定义 |
| 29-49 | 新增ConfigureTimeForRunTimeStats()— 配 TIM6 为 100KHz(PSC=89, ARR=9) |
| 56-60 | HAL_TIM_PeriodElapsedCallback里 TIM6 分支:FreeRTOSRunTimeTicks++ |
stm32f4xx_it.c
| 行号 | 内容 |
|---|---|
| 172-178 | 新增TIM6_DAC_IRQHandler()— ISR 入口,调HAL_TIM_IRQHandler(&htim6) |
FreeRTOSConfig.h
| 行号 | 内容 |
|---|---|
| 172 | configGENERATE_RUN_TIME_STATS0→1 |
| 173 | 新增#include "../BSP/TIM6/TIM6.h"— 引入 ConfigureTimeForRunTimeStats 声明 |
| 174 | 新增portCONFIGURE_TIMER_FOR_RUN_TIME_STATS()→ConfigureTimeForRunTimeStats() |
| 175 | 新增portGET_RUN_TIME_COUNTER_VALUE()→FreeRTOSRunTimeTicks |
portCONFIGURE_TIMER_FOR_RUN_TIME_STATS() 这是一个 初始化函数宏。 当 FreeRTOS 启动调度器(vTaskStartScheduler())时,会自动调用这个宏。 它的职责是: 👉 初始化一个高精度定时器(这里是 TIM6),为任务时间统计做准备。 你需要在 ConfigureTimeForRunTimeStats() 里做什么? 通常需要完成以下几件事: 开启 TIM6 时钟 配置 TIM6 的预分频(PSC),让计数频率 远高于系统节拍 配置自动重装值(ARR),决定溢出周期 开启 TIM6 更新中断(可选,但推荐) 启动 TIM6 void ConfigureTimeForRunTimeStats(void) { FreeRTOSRunTimeTicks = 0; /* 计数器归零 */ TIM6_CLK_ENABLE(); /* 使能 TIM6 时钟 */ htim6.Instance = TIM6; htim6.Init.Prescaler = RT_STATS_PSC; /* 89 */ htim6.Init.CounterMode = TIM_COUNTERMODE_UP; htim6.Init.Period = RT_STATS_ARR; /* 9 */ HAL_TIM_Base_Init(&htim6); HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 6, 0); /* 抢占 6,低于关键中断 */ HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn); HAL_TIM_Base_Start_IT(&htim6); }#define portGET_RUN_TIME_COUNTER_VALUE() FreeRTOSRunTimeTicks 这是 获取当前“系统运行时间基准值”的宏。 FreeRTOS 在以下时刻会调用它: 任务切换时 系统节拍中断中 调用 vTaskGetRunTimeStats() 时 👉 它的返回值,代表“从系统启动到现在,一共经过了多少个高精度时间单位”。
main.c
| 行号 | 内容 |
|---|---|
| 5 | 新增#include "./Key/Key.h"— 按键模块 |
| 7 | 新增#include "./TIM6/TIM6.h"— TIM6 运行时间统计 |
| 18 | 新增ConfigureTimeForRunTimeStats()— 调度器启动前配好 TIM6 |
| 20 | 新增Key_Init()— 初始化按键 GPIO |
| 28 | printf 标题 →"FreeRTOS Run Time Stats Test!" |
freertos_demo.c — 整个文件重写
| 行号 | 内容 |
|---|---|
| 37 | task2 优先级=3(比 task1 高,方便中断响应) |
| 46 | static char task_buff[512]—vTaskGetRunTimeStats输出缓冲区 |
| 88-97 | task1:LED 红 500ms 交替闪烁 |
| 109-129 | task2:扫描 KEY1(PA0) → 按键触发vTaskGetRunTimeStats(task_buff)→ printf |
| 132 | vTaskDelay(10)— 10ms 扫一次按键 |
Key.c
| 行号 | 内容 |
|---|---|
| 2-3 | 新增#include "FreeRTOS.h"/#include "task.h"— RTOS API |
| 30 | HAL_Delay(10)→vTaskDelay(10 / portTICK_PERIOD_MS)— 消抖不卡 CPU |
| 33-34 | while(...);死循环 →while(...) vTaskDelay(...)— 等松开时让出 CPU |
完整中断链路
TIM6 硬件 (100KHz=10μs) → 向量表 TIM6_DAC_IRQHandler [stm32f4xx_it.c:175] → HAL_TIM_IRQHandler(&htim6) → HAL_TIM_PeriodElapsedCallback [TIM6.c:54] → FreeRTOSRunTimeTicks++ [TIM6.c:58] ↓ FreeRTOS 内核 portGET_RUN_TIME_COUNTER_VALUE() 读数 ↓ vTaskGetRunTimeStats() → sprintf 格式化 → printf 输出
跟正点代码的对比
| 正点 | 我们 | |
|---|---|---|
| 时基 | TIM6,btim.c/btim.h专用模块 | TIM6,复用BSP/TIM6/加功能 |
| 时基初始化 | port宏在调度器里调 | main 里主动调 + port 宏双重保险 |
| ISR 位置 | btim.c里BTIM_TIMX_INT_IRQHandler | stm32f4xx_it.c里TIM6_DAC_IRQHandler |
| 按键触发 | key_scan(0)+ KEY0 | Key_Scan(PA0)+ KEY1,RTOS 版消抖 |
| LED | LED0_TOGGLE() | LED_R(0/1)红灯交替 |
| 临界区 | taskENTER/EXIT_CRITICAL包创建 | 无临界区(之前验证过不需要) |
三:rtos\8 任务运行时间轴
启动阶段
上电 → HAL_Init → SysTick_Init(180MHz) → TIM7_Init(HAL时基) → ConfigureTimeForRunTimeStats(TIM6 100KHz 开始计数) → LED_Init / Key_Init / USART1_Init → printf("FreeRTOS Run Time Stats Test!") → freertos_demo() ├── xTaskCreate(start_task, prio=1) └── vTaskStartScheduler() └── xPortStartScheduler → prvStartFirstTask → svc 0 → SVC → 第一个任务 start_task 开始跑!start_task(优先级 1,活不过 1ms)
start_task 跑: ├── xTaskCreate(task1, prio=2) → task1 就绪 ├── xTaskCreate(task2, prio=3) → task2 就绪(比 task1 高!) └── vTaskDelete(NULL) → 自杀,触发 PendSV → 找到最高优先级 task2(prio=3) → 🎉 task2 开始跑
稳态运行(无限循环)
时间轴 → task2 (prio=3, 最高): Key_Scan(PA0) → 没按下 → vTaskDelay(10) → 睡 10ms ├─ 10ms 后醒来 ──→ Key_Scan → vTaskDelay(10) → 睡 ├─ 10ms 后醒来 ──→ ...(循环)... └─ 某次醒来 → KEY1 按下!→ vTaskGetRunTimeStats → printf(表格) → vTaskDelay(10) → 睡 task1 (prio=2): LED_R(0) 亮 → vTaskDelay(500) → 睡 500ms ├─ 500ms 后醒来 → LED_R(1) 灭 → vTaskDelay(500) → 睡 ├─ 500ms 后醒来 → LED_R(0) 亮 → vTaskDelay(500) → 睡 └─ ...(永远红灯亮 500ms / 灭 500ms)... IDLE (prio=0): 当 task1 和 task2 都在睡的时候跑 → 回收被删任务的尸体 → 空转省电
CPU 占用占比(为什么 IDLE 是 99%)
每 1 秒内 ──────────────────────────────────────────────── task2:每 10ms 醒来一次,读 GPIO + vTaskDelay,醒着时间 ≈ 几十 μs → 每 1 秒内实际干活 ≈ 0.01 秒 → 1% task1:每 500ms 醒来一次,LED 翻转 + vTaskDelay → 每 1 秒实际干活 ≈ 0.001 秒 → <1% IDLE:以上两个任务都睡了的 99% 时间 → IDLE 在空转
按下 KEY1 那一刻
task2 扫到按键按下 → delay_ms(10) 消抖(CPU 轮询,不切换任务) → 确认按下 → while(按下) 等松开(CPU 轮询) → 松开 → 返回 KEY_ON → vTaskGetRunTimeStats(task_buff) ├── pvPortMalloc → uxTaskGetSystemState(&ulTotalTime) │ └── ulTotalTime = FreeRTOSRunTimeTicks(TIM6 100KHz 累计值) ├── 算出每个任务占比 → sprintf 格式化 └── 写入 task_buff → printf("%s", task_buff) → 串口输出表格 → vTaskDelay(10) → 继续扫按键核心就是:两个任务都在睡 → CPU 交给 IDLE → 99% 空闲率。这也证明了 FreeRTOS 的调度没有额外开销——不干活的时候 CPU 就真闲着。