为什么需要无侵入耗时统计

在嵌入式开发中,定位性能瓶颈常需要测量函数执行时间。常见做法是用GPIO翻转配合示波器,或启动一个硬件定时器。但这些方案有明显缺陷:

  • GPIO翻转需要额外引脚,且翻转本身有开销,测量结果包含仪器误差
  • 硬件定时器资源有限,频繁启停影响其他功能
  • 中断上下文切换会引入抖动,难以测量短小函数

Cortex-M内核自带的DWT(Data Watchpoint and Trace) 单元中的周期计数器(CYCCNT)可以完美解决这些问题:它直接对内核时钟周期计数,精度达到单个CPU周期,且读写操作仅需几条指令,对被测代码几乎零侵入。

DWT周期计数器工作原理

DWT是ARM CoreSight调试组件的一部分,CYCCNT是一个32位向上计数器,每个内核时钟周期加1。假设主频为168MHz,则计数分辨率为1/168MHz ≈ 5.95ns,32位计数器溢出周期约25.6秒,足以覆盖绝大多数函数测量场景。

关键寄存器:

  • DWT_CTRL(地址0xE0001000):bit0为使能位CYCCNTENA
  • DWT_CYCCNT(地址0xE0001004):当前周期计数值
  • CoreDebug_DEMCR(地址0xE000EDFC):bit24为TRCENA,必须置1才能访问DWT

注意:CYCCNT在部分老旧Cortex-M0/M0+上不可用,M3/M4/M7/M33均支持。使用前需确认芯片参考手册。

配置步骤

  1. 使能全局调试跟踪:置位CoreDebug->DEMCR的TRCENA位
  2. 清零CYCCNT:写0到DWT->CYCCNT
  3. 使能周期计数器:置位DWT->CTRL的CYCCNTENA位
  4. 封装读取函数,注意使用__DSB()保证指令同步

完整代码示例

以下代码可直接用于STM32(Cortex-M3/M4/M7),其他厂商芯片只需包含对应CMSIS头文件。

#include "stm32f4xx.h"  // 根据实际芯片替换

/* 初始化DWT周期计数器 */
void DWT_Init(void)
{
    /* 使能调试跟踪 */
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    /* 清零计数器 */
    DWT->CYCCNT = 0;
    /* 使能周期计数器 */
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}

/* 读取当前周期数(内联以减少开销) */
static inline uint32_t DWT_GetCycles(void)
{
    return DWT->CYCCNT;
}

/* 计算两次计数的差值,处理32位溢出 */
static inline uint32_t DWT_Diff(uint32_t start, uint32_t end)
{
    return end - start;  // 无符号减法自动处理溢出
}

/* 将周期数转换为微秒(假设SystemCoreClock已正确配置) */
static inline float DWT_CyclesToUs(uint32_t cycles)
{
    return (float)cycles * 1000000.0f / (float)SystemCoreClock;
}

/* 使用示例:测量一个函数的耗时 */
void Measure_Function(void)
{
    uint32_t start, end, cycles;
    
    start = DWT_GetCycles();
    /* 被测函数 */
    TargetFunction();
    end = DWT_GetCycles();
    
    cycles = DWT_Diff(start, end);
    printf("耗时: %lu 周期, %.3f us\n", cycles, DWT_CyclesToUs(cycles));
}

实测数据与精度分析

在STM32F407(168MHz)上实测:

  • 空函数调用:约12个周期(含测量开销)
  • 浮点乘法:约5个周期
  • 100次循环累加:约320个周期
  • 测量开销本身:DWT_GetCycles()约3条指令,约6个周期

对于耗时超过100周期的函数,测量误差小于6%,完全满足工程需求。若需更高精度,可将测量代码用汇编内联,或多次测量取平均。

注意事项

  • 调试器影响:连接调试器时CYCCNT正常工作;脱离调试器后,部分芯片会关闭DWT时钟,需在初始化时确保TRCENA置位。
  • 低功耗模式:进入Sleep/Stop模式后CYCCNT停止计数,测量跨低功耗代码时结果无效。
  • 中断干扰:测量期间若发生中断,中断服务程序耗时会被计入。对时间敏感的场景,可在测量前后关中断,但会引入额外开销。
  • 多核/多主频:若系统动态调频,需在测量时记录当前主频,换算时间时使用对应频率。
  • 编译器优化:确保被测函数不被优化掉,可使用__attribute__((noinline))或volatile变量。
  • Cortex-M0限制:M0/M0+无DWT CYCCNT,需改用SysTick或其他方案。

扩展:构建轻量级性能剖析框架

基于上述基础,可进一步封装成宏,实现自动记录:

#define PROFILE_START()  uint32_t __t0 = DWT_GetCycles()
#define PROFILE_END(name) do { \
    uint32_t __t1 = DWT_GetCycles(); \
    printf("[PROFILE] %s: %lu cycles\n", name, __t1 - __t0); \
} while(0)

配合串口输出或SEGGER RTT,即可在不影响实时性的前提下,快速定位热点函数。