星标公众号,让嵌入式知识 “投喂” 不停歇!

嵌入式开发中,大概率遇到过这些问题:

  • 这个优化到底值不值:改了一堆代码,肉眼看不出有没有快。

  • 控制算法一拍用了多少时间:能扛得住 1 kHz、4 kHz 的控制周期吗?

这篇文章我们来梳理下:在嵌入式环境下,靠谱地测一段代码的执行时间的方法

1. 嵌入式里“测时间”容易踩坑

在 PC 上测时间很简单:std::chronogettimeofday、各种 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 时间”。

开启方式(简化):

  1. 在 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 统计,从“函数级 → 任务级 → 系统级”多个层次观测时间。