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

在嵌入式开发中,定位性能瓶颈、验证优化效果都离不开函数耗时数据。常见做法有两种:

  • GPIO翻转 + 示波器:需要额外硬件,且翻转本身有几十纳秒开销。
  • 通用定时器打点:读取定时器寄存器有总线访问延迟,分辨率也受定时器时钟限制。

Cortex-M内核自带一个 DWT(Data Watchpoint and Trace) 单元,其中的 CYCCNT 寄存器是一个随内核时钟自增的32位周期计数器。读取它只需一条 MRS 指令,开销约1~2个周期,对被测代码几乎无干扰,分辨率达到单周期级别。

DWT周期计数器原理

DWT是ARM CoreSight调试组件的一部分,CYCCNT 在每个处理器时钟周期加1。以168MHz主频为例,计数器每约5.95ns加1,32位计满约25.6秒回绕。

使用前需要满足三个条件:

  1. 使能DWT(DEMCR 的 TRCENA 位)。
  2. 清零并使能 CYCCNT(DWT_CTRL 的 CYCCNTENA 位)。
  3. 部分芯片需要先解锁调试寄存器(如STM32需写 0xC5ACCE55 到 LAR)。

注意:CYCCNT 依赖调试时钟域,在深度睡眠模式下会停止计数,因此只适合测量运行态代码。

配置步骤

步骤1:使能DWT

#include "core_cm4.h"  /* 按内核型号选择 core_cm3/4/7.h */

static void DWT_Init(void)
{
    /* 1. 解锁ITM/DWT访问(部分Cortex-M3/M4需要) */
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;

    /* 2. 清零周期计数器 */
    DWT->CYCCNT = 0;

    /* 3. 使能周期计数器 */
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}

若使用STM32且上述代码无效,可先执行 ITM->LAR = 0xC5ACCE55; 解锁。

步骤2:封装读取与差值宏

#define DWT_CYCCNT  (DWT->CYCCNT)

/* 读取当前周期计数 */
static inline uint32_t DWT_GetCycles(void)
{
    return DWT_CYCCNT;
}

/* 计算两次采样之间的周期数,自动处理32位回绕 */
static inline uint32_t DWT_Elapsed(uint32_t start, uint32_t end)
{
    return end - start;   /* 无符号减法天然处理回绕 */
}

步骤3:换算为时间

/* 周期数转微秒,SystemCoreClock为内核主频 */
static inline float DWT_CyclesToUs(uint32_t cycles)
{
    return (float)cycles * 1000000.0f / (float)SystemCoreClock;
}

完整示例:统计一个函数的耗时

#include <stdio.h>
#include "core_cm4.h"

/* 被测函数:软件CRC32,模拟耗时业务 */
static uint32_t crc32_sw(const uint8_t *data, uint32_t len)
{
    uint32_t crc = 0xFFFFFFFF;
    for (uint32_t i = 0; i < len; i++) {
        crc ^= data[i];
        for (int b = 0; b < 8; b++)
            crc = (crc >> 1) ^ (0xEDB88320 & -(crc & 1));
    }
    return ~crc;
}

int main(void)
{
    SystemInit();
    DWT_Init();

    static uint8_t buf[1024];
    for (int i = 0; i < sizeof(buf); i++) buf[i] = (uint8_t)i;

    /* 测量开始 */
    uint32_t t0 = DWT_GetCycles();
    volatile uint32_t result = crc32_sw(buf, sizeof(buf));
    uint32_t t1 = DWT_GetCycles();

    uint32_t cycles = DWT_Elapsed(t0, t1);
    printf("CRC32 result=%08X, cycles=%lu, time=%.2f us\r\n",
           result, cycles, DWT_CyclesToUs(cycles));

    while (1) { }
}

实测在168MHz的STM32F4上,1024字节软件CRC32约消耗数十万周期,换算后可直接对比硬件CRC外设的加速比。

实战注意事项

  • 防止编译器优化掉被测代码:用 volatile 接收返回值,或把结果写入全局变量,否则空函数可能被整体优化。
  • 中断干扰:测量期间若发生中断,中断服务程序的时间会被计入。对短函数建议关中断测量,或多次采样取最小值。
  • 计数器回绕:32位计数器约25秒回绕一次,单次测量远小于该值,无符号减法即可正确处理;但不要跨回绕点做多次累加。
  • 多核/多任务:CYCCNT 是全局的,RTOS下测量会被任务切换污染,建议在临界区或单任务基准测试中使用。
  • 低功耗模式:睡眠时计数器停走,测得的“时间”是纯运行周期,反而适合评估CPU占用。
  • 调试器影响:连接调试器单步时计数行为可能异常,性能数据应在全速运行下采集。

小结

DWT周期计数器把函数耗时测量简化为“读两次寄存器、做一次减法”,开销低、分辨率高、无需外设资源,是Cortex-M平台上做性能剖析的利器。配合宏封装,可以进一步做成 PROFILE_BEGIN/PROFILE_END 的即插即用工具,在优化热点函数、对比算法实现时非常实用。