为什么需要更少侵入的耗时统计

在STM32等Cortex-M单片机开发中,我们常需要回答:这个函数执行了多久?中断延迟多大?优化后性能提升多少?

常见方法有两种:

  • GPIO翻转 + 示波器/逻辑分析仪:需要占用引脚,每次测量要改硬件连接,且受限于仪器采样率(通常几十MHz),对百纳秒级函数误差大。
  • 系统滴答定时器(SysTick):分辨率通常为1ms,只能测长耗时函数。

而Cortex-M内核内置的DWT(Data Watchpoint and Trace) 单元中的周期计数器(CYCCNT),能以CPU时钟周期为分辨率(例如168MHz下约5.95ns),且不占用任何外部引脚,只需几条指令即可读取,侵入性极低。

DWT周期计数器原理

DWT是ARM CoreSight调试组件的一部分,其中CYCCNT寄存器是一个32位向上计数器,每个内核时钟周期递增1。当计数溢出(2^32个周期)后自动回绕。

关键点:

  • 需要使能DWT的CYCCNT功能(通过DEMCR寄存器中的TRCENA位和DWT->CTRL的CYCCNTENA位)。
  • 读取DWT->CYCCNT仅需一条LDR指令,开销约1~2个周期。
  • 计数器在调试器暂停时通常也停止,不影响测量。
  • 不同Cortex-M内核(M3/M4/M7/M33等)均支持,但M0/M0+无DWT。

配置步骤

以STM32 HAL库环境为例(标准外设库同理):

  1. 使能DWT访问:设置CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
  2. 清零并启动周期计数器:DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
  3. 读取计数:直接读DWT->CYCCNT。
  4. 换算时间:周期数 / SystemCoreClock 得到秒数。

注意:某些芯片需要先解锁DWT(如STM32F7/H7需设置DWT->LAR = 0xC5ACCE55),但多数F1/F4系列不需要。

完整代码示例

以下代码提供可移植的接口,支持嵌套测量(通过保存进入时的计数值)。

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

// 初始化DWT周期计数器
void DWT_Init(void)
{
    // 使能DWT
    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;
}

// 计算两次计数之间的周期差(处理回绕)
static inline uint32_t DWT_CycleDiff(uint32_t start, uint32_t end)
{
    return (end >= start) ? (end - start) : (0xFFFFFFFF - start + end + 1);
}

// 将周期数转换为微秒(浮点)
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();
    // 被测函数
    SomeFunction();
    end = DWT_GetCycles();
    
    cycles = DWT_CycleDiff(start, end);
    float us = DWT_CyclesToUs(cycles);
    printf("SomeFunction took %lu cycles, %.2f us\n", cycles, us);
}

对于需要多次测量取平均的场景,可封装成宏:

#define DWT_MEASURE_BEGIN()  uint32_t _dwt_start = DWT_GetCycles()
#define DWT_MEASURE_END()    (DWT_CycleDiff(_dwt_start, DWT_GetCycles()))

注意事项与进阶技巧

  • 中断干扰:测量期间若发生中断,中断服务程序的时间会被计入。建议在测量关键函数时临时关闭全局中断(__disable_irq()),但会改变实时性,需权衡。
  • 编译器优化:确保被测函数不会被优化掉(可加__attribute__((noinline))或使用volatile变量)。
  • 多任务环境:在RTOS中,任务切换会导致测量值偏大。可在测量前后记录任务切换计数,或仅在单任务上下文中使用。
  • 计数器回绕:32位计数器在168MHz下约25.5秒回绕一次,短时间测量无需担心,但长时间测量需用差值法处理。
  • 调试器影响:连接调试器时,DWT可能被调试器占用或复位,建议在独立运行模式下验证。
  • 低功耗模式:进入睡眠/停止模式后,DWT计数器可能停止,测量低功耗唤醒时间需改用其他方法。
  • 精度验证:可用GPIO翻转配合示波器对比,确认DWT测量误差在1%以内。

总结

DWT周期计数器为嵌入式开发者提供了一种近乎零成本、高分辨率、无引脚占用的函数级耗时统计手段。通过简单的初始化和几行读取代码,即可在开发阶段快速定位性能瓶颈。结合本文的代码和注意事项,你可以将其集成到自己的调试框架中,实现更高效的性能分析。