用 DWT 的 CYCCNT 做无侵入式代码耗时测量:精度、溢出与多任务下的正确用法

在嵌入式开发中,我们经常需要测量一段代码的执行时间,以评估算法效率、优化热点函数或验证实时性。传统方法如 GPIO 翻转 + 示波器虽然直观,但需要额外引脚且难以测量短时间;使用定时器中断计数则可能引入中断开销。Cortex-M 内核自带的 DWT(Data Watchpoint and Trace) 单元中的 CYCCNT 计数器,可以让我们以 CPU 周期为精度,无侵入地测量代码耗时。本文将深入探讨其原理、配置、精度、溢出处理以及多任务环境下的正确用法。

1. DWT 与 CYCCNT 原理

DWT 是 ARM Cortex-M 内核中用于调试和跟踪的组件,其中包含一个 32 位的周期计数器 CYCCNT。当使能后,CYCCNT 在每个内核时钟周期递增(部分型号可配置为每 64 周期递增)。由于它直接由硬件维护,读取它不会干扰 CPU 流水线,因此是无侵入的。

  • 精度:理论上为 1 个 CPU 周期。例如 72MHz 的 STM32F103,分辨率约 13.9ns。
  • 范围:32 位计数器,在 72MHz 下约 59.6 秒溢出;在 168MHz 下约 25.6 秒溢出。
  • 访问:通过内核调试寄存器,需先使能 DWT 和 CYCCNT。

2. 配置步骤

以 STM32(Cortex-M3/M4/M7)为例,配置步骤如下:

  1. 使能 DWT 和 CYCCNT:设置 DEMCRTRCENA 位,然后设置 DWT_CTRLCYCCNTENA 位。
  2. 清零 CYCCNT:向 DWT_CYCCNT 写入 0。
  3. 读取 CYCCNT:直接读取 DWT_CYCCNT 寄存器。

注意:某些 Cortex-M0/M0+ 没有 DWT,需确认内核型号。

3. 完整代码示例

以下代码适用于 STM32 HAL 库环境,包含初始化、读取和测量宏。

#include "stm32f4xx.h"  // 根据型号修改

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

// 获取当前周期数
static inline uint32_t DWT_GetCycles(void)
{
    return DWT->CYCCNT;
}

// 测量代码块耗时(单位:周期)
#define DWT_MEASURE_BEGIN()  uint32_t _start = DWT_GetCycles()
#define DWT_MEASURE_END()    (DWT_GetCycles() - _start)

// 示例:测量一个函数的执行周期
void test_function(void)
{
    for (volatile int i = 0; i < 1000; i++);
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    DWT_Init();

    DWT_MEASURE_BEGIN();
    test_function();
    uint32_t cycles = DWT_MEASURE_END();

    // cycles 即为 test_function 消耗的 CPU 周期数
    // 可转换为时间:cycles / SystemCoreClock
    float time_us = (float)cycles / (SystemCoreClock / 1000000.0f);

    while (1);
}

4. 精度分析

  • 理论精度:1 个 CPU 周期。但实际测量包含调用 DWT_GetCycles() 的开销(约几个周期),对于极短代码块需扣除。
  • 影响因素
    • 中断:测量期间若发生中断,中断服务程序的时间会被计入。
    • 流水线:Cortex-M 流水线可能导致读取 CYCCNT 的指令本身有延迟,但影响很小。
    • 时钟频率:CYCCNT 随内核时钟变化,若使用低功耗模式或时钟切换,需重新校准。

5. 溢出处理

CYCCNT 是 32 位,溢出后回绕。直接相减 end - start 在无符号运算下仍能得到正确差值,前提是测量时间不超过溢出周期。例如 168MHz 下最大可测约 25.6 秒。若需测量更长时间,可:

  • 使用定时器中断累加溢出次数。
  • 降低 CYCCNT 递增频率(如每 64 周期),但会牺牲精度。
// 安全测量长耗时:结合溢出计数
volatile uint32_t overflow_count = 0;

void DWT_Overflow_Handler(void)  // 需在 CYCCNT 溢出中断中调用
{
    overflow_count++;
}

uint64_t DWT_GetCycles64(void)
{
    uint32_t primask = __get_PRIMASK();
    __disable_irq();
    uint32_t ov = overflow_count;
    uint32_t cyc = DWT->CYCCNT;
    // 若读取时发生溢出,需调整
    if (DWT->CTRL & DWT_CTRL_CYCCNTENA_Msk && (DWT->CYCCNT < cyc)) {
        ov++;
    }
    __set_PRIMASK(primask);
    return ((uint64_t)ov << 32) | cyc;
}

6. 多任务下的正确用法

在 RTOS(如 FreeRTOS)中,任务可能被调度切换,导致测量结果包含其他任务的时间。正确做法:

  • 测量单次执行:在任务中测量时,确保测量期间不发生任务切换。可临时挂起调度器 vTaskSuspendAll() / xTaskResumeAll()
  • 测量整体耗时:若需测量任务从开始到结束的 CPU 时间,应使用 RTOS 提供的运行时间统计(如 vTaskGetRunTimeStats()),而非 CYCCNT。
  • 中断中测量:在中断服务程序中测量时,注意中断嵌套可能影响结果。
// FreeRTOS 中测量任务内代码块
vTaskSuspendAll();
DWT_MEASURE_BEGIN();
// 被测代码
uint32_t cycles = DWT_MEASURE_END();
xTaskResumeAll();

7. 注意事项

  • 内核支持:Cortex-M0/M0+ 无 DWT,Cortex-M3/M4/M7 支持。
  • 调试器影响:连接调试器时,DWT 可能被调试器占用,导致无法使能。
  • 低功耗模式:进入睡眠或停止模式时,CYCCNT 可能停止,唤醒后需重新初始化。
  • 多核:多核系统中每个核有独立 DWT,需分别处理。
  • 编译器优化:使用 volatile 防止读取被优化,测量代码避免被优化掉。

通过合理使用 DWT CYCCNT,你可以获得高精度、低开销的代码耗时数据,为嵌入式性能优化提供有力支撑。