用 DWT 的 CYCCNT 做 Cortex-M 中断延迟与函数耗时测量
在嵌入式开发中,我们经常需要回答两个问题:中断从触发到执行第一条指令花了多少时间?某个关键函数到底跑了多少个周期?传统方法要么用 GPIO 翻转 + 示波器,要么用定时器,前者需要硬件,后者精度有限。其实 Cortex-M 内核自带了一个“隐形秒表”——DWT 的 CYCCNT 计数器,它能以单周期精度测量任何代码段的耗时。
原理:DWT 与 CYCCNT
DWT(Data Watchpoint and Trace)是 ARM CoreSight 调试组件的一部分,其中的 CYCCNT 是一个 32 位向上计数器,每个内核时钟周期加 1。它独立于 CPU 流水线,计数准确,且不占用任何外设。
- 时钟源:CYCCNT 直接对内核时钟(HCLK)计数,因此 1 个计数 = 1 个内核周期。
- 溢出:32 位计数器在 168MHz 下约 25.6 秒溢出一次,一般测量远小于此值。
- 访问:通过 DWT->CYCCNT 寄存器直接读取,无需特殊指令。
要使用 CYCCNT,需要使能 DWT 和 CYCCNT 本身,并确保调试接口时钟开启。
配置步骤
以 STM32F4(Cortex-M4)为例,配置流程如下:
- 使能调试外设时钟:在 RCC 中开启 DBGMCU 时钟(部分系列默认开启)。
- 解锁 DWT:向 DWT->LAR 写入 0xC5ACCE55(仅部分内核需要,Cortex-M3/M4 通常不需要)。
- 使能 CYCCNT:置位 DWT->CTRL 的 CYCCNTENA 位。
- 复位计数器:写 DWT->CYCCNT = 0。
代码实现:
#include "stm32f4xx.h"
void DWT_Init(void)
{
// 使能 DWT 和 CYCCNT
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;
}
测量函数耗时
测量函数耗时的基本思路:在函数调用前后读取 CYCCNT,差值即为周期数。注意要防止编译器优化掉空函数或重排指令。
uint32_t measure_func_cycles(void (*func)(void))
{
uint32_t start, end;
__DSB(); // 确保之前指令完成
start = DWT_GetCycles();
func();
__DSB(); // 确保函数内指令完成
end = DWT_GetCycles();
return end - start;
}
若需测量一段代码,可直接内联:
uint32_t t1 = DWT_GetCycles();
// 待测代码
uint32_t t2 = DWT_GetCycles();
uint32_t cycles = t2 - t1;
测量中断延迟
中断延迟定义为:从外部中断信号有效到中断服务程序(ISR)第一条指令执行的时间。用 CYCCNT 测量时,可以在中断触发前记录时间,在 ISR 入口读取时间,但需注意中断触发时刻无法精确获知。
一种实用方法:用 GPIO 产生一个脉冲作为中断源,同时在脉冲上升沿读取 CYCCNT(通过外部中断或轮询),然后在 ISR 入口再读一次。但更简单的是测量 ISR 执行时间 和 中断响应抖动。
下面示例测量从 EXTI 中断触发到 ISR 第一条指令的周期数(近似值,包含中断延迟):
volatile uint32_t isr_entry_cycles = 0;
volatile uint32_t isr_exit_cycles = 0;
void EXTI0_IRQHandler(void)
{
isr_entry_cycles = DWT_GetCycles();
// 清除中断标志
EXTI->PR = EXTI_PR_PR0;
// 用户代码
isr_exit_cycles = DWT_GetCycles();
}
// 在主循环中触发中断并记录时间
void trigger_and_measure(void)
{
uint32_t before = DWT_GetCycles();
// 软件触发 EXTI0(例如置位 SWIER)
EXTI->SWIER = EXTI_SWIER_SWIER0;
// 等待中断发生(实际中可用标志)
while (isr_entry_cycles == 0);
uint32_t latency = isr_entry_cycles - before;
uint32_t duration = isr_exit_cycles - isr_entry_cycles;
// latency 包含软件触发到 ISR 入口的延迟,duration 为 ISR 执行周期
}
注意:软件触发到 ISR 入口的延迟包含流水线、中断优先级等,实际硬件中断延迟需用示波器配合 GPIO 测量。
注意事项
- 调试器影响:连接调试器时 CYCCNT 正常工作,但单步调试会暂停计数。
- 低功耗模式:进入睡眠或停止模式后 CYCCNT 可能停止,唤醒后需重新初始化。
- 溢出处理:测量长时间间隔时,需处理计数器回绕(用无符号减法自动处理)。
-
编译器优化:使用
__DSB()或volatile防止指令重排;测量空函数时可能被优化,可加__NOP()。 - 多核/多任务:在 RTOS 中,任务切换可能影响测量,建议关中断或使用高优先级任务。
- 时钟频率:周期数转换为时间需知道内核频率,例如 168MHz 下 1 周期 ≈ 5.95ns。
总结
DWT CYCCNT 是 Cortex-M 上最轻量、最精确的代码耗时测量工具。只需几行初始化代码,就能获得单周期分辨率,非常适合优化中断服务程序、算法热点和实时性分析。结合 GPIO 翻转,还能进一步测量硬件中断延迟。掌握它,你的性能调优将如虎添翼。