星标公众号,让嵌入式知识 “投喂” 不停歇!
嵌入式开发中,大概率遇到过这些问题:
-
这个优化到底值不值:改了一堆代码,肉眼看不出有没有快。
-
控制算法一拍用了多少时间:能扛得住 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()这个格式化输出函数。
-
vConfigureTimerForRunTimeStats():初始化“高精度计数器”。 -
ulGetRunTimeCounterValue():返回当前“运行时间计数值”(一个不断递增的无符号数)。
示例:
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 统计,从“函数级 → 任务级 → 系统级”多个层次观测时间。