嵌入式开发中,大概率遇到过这些问题:
这个优化到底值不值:改了一堆代码,肉眼看不出有没有快。
控制算法一拍用了多少时间:能扛得住 1 kHz、4 kHz 的控制周期吗?
这篇文章我们来梳理下:在嵌入式环境下,靠谱地测一段代码的执行时间的方法。
1. 嵌入式里“测时间”容易踩坑
在 PC 上测时间很简单:std::chrono、gettimeofday、各种 profiler,甚至 Chrome DevTools,都能给你一个数字。
到 MCU/SoC 上,事情立刻复杂几个量级:
操作系统薄甚至没有
很多裸机/轻量 RTOS 环境没有成熟 API,你只能直接碰寄存器。
资源受限
频率才几十到几百 MHz,带宽有限,任何额外 printf 都可能改变原本的时序。
频率、功耗动态变化
有些芯片会变频、进低功耗、关时钟,导致“周期数 → 时间”的关系不再简单线性。
常见错误姿势:
用毫秒级系统滴答去测微秒级代码:比如用 1ms 的 SysTick tick 测一个 10us 的函数,结果永远是 0。
到处 printf 打时间:printf 本身可能比你要测的代码还慢好几倍。
2. 几种主流手段
CPU 周期计数器(如 DWT_CYCCNT):精度最高,侵入最小。
片上定时器 / SysTick:工程中最常用的折中方案。
GPIO 翻转 + 示波器 / 逻辑分析仪:最直观的波形方式。
RTOS Trace / 运行时间统计:做系统级优化时很好用。
2.1 用 CPU 周期计数器(精度王)
以 Cortex-M 为例,内核里有一个调试用的周期计数器(DWT_CYCCNT),从使能那一刻开始,每个 CPU cycle 自增 1。
开始测量:读一次 DWT_CYCCNT,记为 start。
结束测量:读一次 DWT_CYCCNT,记为 end。
周期差:delta = end - start。
时间:Δt = delta / fcpu。
Cortex-M4 上开启 DWT_CYCCNT
以下是一个典型的初始化过程(以 STM32 为例),代码压缩到最关键的几行:
实际测量某段代码:
从工程视角看,如果芯片支持 DWT/CYCCNT,这应该是首选方案。
2.2. 用片上定时器计时(工程折中)
当芯片没有暴露类似 DWT 的计数器,或者你想用一个“跟 CPU 频率解耦”的时间基准时,就需要用通用定时器或 SysTick。
配置一个 32 位(或 16 位)定时器为 自由运行模式(Auto-reload 设置为最大值)。选择合适的分频,让它以某个已知频率递增,比如 1 MHz(1 tick = 1 us)。测量时读定时器计数器值(CNT),前后做差,乘以 tick 时间就是时间差。
以 STM32 TIM2 为例的配置思路
假设计数频率目标是 1 MHz(精度 1 µs):
APB1 定时器时钟:假设为 84 MHz;预分频系数 PSC = 84 - 1 = 83;自动重装载 ARR = 0xFFFFFFFF(32 位计数器)。
代码实现如:
测量时:
对几十微秒~秒级的代码块,用这种定时器方案。
2.3 GPIO 翻转 + 示波器/逻辑分析仪(最直观)
这个方法的思路特别“土”,但在工程里非常管用,而且误差小。
代码块开始前,把某个 GPIO 拉高;代码块结束后,把 GPIO 拉低;用示波器或逻辑分析仪测量这段高电平的脉宽,就是执行时间。
代码示例:
调试阶段能用 GPIO + 示波器就用,做完确认再把这些宏关掉(宏空实现),避免污染最终固件。
2.4 RTOS 运行时间统计 / Trace(系统级)
FreeRTOS 提供了一个功能:每个任务都维护一个运行时间计数器,最终可以看到“每个任务占用了多少 CPU 时间”。
开启方式(简化):
在 FreeRTOSConfig.h 打开统计功能
-
- ——告诉 FreeRTOS “我要统计运行时间”:
configGENERATE_RUN_TIME_STATS 1:启用运行时间统计(依赖你提供计数器)。
configUSE_STATS_FORMATTING_FUNCTIONS 1:启用 vTaskGetRunTimeStats() 这个格式化输出函数。
提供 2 个钩子函数——FreeRTOS 会回调它们来获取“时间刻度”:
vConfigureTimerForRunTimeStats():初始化“高精度计数器”。
ulGetRunTimeCounterValue():返回当前“运行时间计数值”(一个不断递增的无符号数)。
在某个任务里周期性打印统计信息——方便观察每个任务的 CPU 占比。
示例:
FreeRTOSConfig.h 中相关配置:
钩子函数的实现:
定时器实现:
注意:vConfigureTimerForRunTimeStats() 会在调度器启动前由 FreeRTOS 调用一次,你不需要手动调用;ulGetRunTimeCounterValue() 则会被内核在每次任务切换时调用,用来给对应任务累加“运行时间”。
随后我们创建一个“监控任务”,每隔 1s 打一份统计:
这几个函数背后的逻辑可以这样理解:
FreeRTOS 不会自己搞定“时间”,而是一直向你要一个“递增计数值”;每次任务切换时,它记下“上一个任务最后离开时的计数值”,和上一次进入时的值做差,累加到这个任务名下;
vTaskGetRunTimeStats()
- 会把每个任务的累计计数换算成百分比(相对于总计数),生成一张“任务运行时间分布表”。
所以,你看到的 Run Time 列本质上就是“这个任务在你的计数器上累计占了多少刻度”,刻度单位由 ulGetRunTimeCounterValue() 决定(本例中是 1us)。
这也是为什么我们用 1 MHz 的定时器:既好算,又有足够分辨率。
你会得到类似这样的输出(示意):
Task Run Time Percentage
------------------------------------
ctrlTask 350000 35%
commTask 250000 25%
logTask 150000 15%
idle 250000 25%
这能很快告诉你:
- 谁是真正的 CPU 大户;哪个任务突然 CPU 占比飙升(可能出现 bug 或负载增加)。
3. 如何选方法?
我们按“精度 / 侵入性 / 实现复杂度 / 是否系统级”几个维度简单总结。
精度敏感(控制、DSP):DWT + GPIO(调试);
通用逻辑性能评估:定时器计数 + 简单统计;
任务调度/架构优化:RTOS 运行时间统计/Trace。
4. 测量时间的宏
实际项目里,可以使用一套非常轻量的宏,方便随手插测量点:
#if !defined(NDEBUG) || defined(ENABLE_PROFILING)
/* Debug 模式,或者手动定义 ENABLE_PROFILING 时启用 */
#define PROF_INIT() cycle_counter_init()
#define PROF_START(var) uint32_t var = cycle_counter_get()
#define PROF_END(var, label)
do {
uint32_t _end = cycle_counter_get();
uint32_t _delta = _end - (var);
printf("[PROF] %s: %lu cyclesrn", (label), (unsigned long)_delta);
} while (0)
#else
#define PROF_INIT()
#define PROF_START(var)
#define PROF_END(var, label) do { (void)(var); (void)(label); } while (0)
#endif
cycle_counter_init与cycle_counter_get的实现就是上面的几种方法。
使用时:
void control_step(void)
{
PROF_START(t0);
update_observer();
update_pid();
update_pwm();
PROF_END(t0, "control_step");
}
调试阶段开着,确认性能 OK 后,可以把 PROF_END 宏改成空实现,完全不影响生产固件。
5. 总结
嵌入式里测一段代码的执行时间,比 PC 环境复杂得多,简单的 printf 和毫秒级 tick 基本不靠谱。
核心手段就是用 CPU 周期计数器、片上定时器、GPIO + 示波器、RTOS 统计,从“函数级 → 任务级 → 系统级”多个层次观测时间。
1888