用 DWT 的 CYCCNT 做无侵入式中断延迟测量:从配置到数据解读
在嵌入式实时系统中,中断延迟直接影响系统的响应能力和稳定性。传统测量方法通常需要在 ISR 中翻转 GPIO 并用示波器观察,或者插入时间戳代码,这些都会引入额外开销甚至改变系统行为。本文将介绍一种无侵入式的测量方法:利用 Cortex-M 内核的 DWT(Data Watchpoint and Trace)单元中的 CYCCNT 计数器,精确测量从中断触发到 ISR 第一条指令执行之间的周期数。
1. 原理:DWT 与 CYCCNT
DWT 是 ARM Cortex-M 内核提供的一个调试组件,其中包含一个 32 位的自由运行计数器 CYCCNT,它每个内核时钟周期递增 1。通过读取 CYCCNT,我们可以获得高精度的周期级时间戳。
中断延迟的定义:从硬件中断信号被内核采样,到 ISR 的第一条指令开始执行所经历的周期数。由于 CYCCNT 在后台自动运行,我们只需在 ISR 入口读取其值,并与中断触发时刻的 CYCCNT 值相减,即可得到延迟。但如何获取“中断触发时刻”的值?
一个巧妙的方法:利用另一个定时器(如 TIM)在固定周期产生中断,并在其中断标志置位时记录 CYCCNT。但这样仍会引入软件开销。更精确的做法是使用 GPIO 翻转 + 输入捕获,但本文聚焦于纯软件无侵入方案:在中断向量表中,将目标中断的 ISR 替换为一个极简的汇编包装函数,该函数仅读取 CYCCNT 并存储,然后跳转到原 ISR。这样,测量代码本身只增加几个周期,且不影响原 ISR 逻辑。
2. 配置步骤
2.1 使能 DWT 和 CYCCNT
DWT 默认可能未使能,需要操作以下寄存器:
-
CoreDebug->DEMCR:使能 TRCENA 位(位 24)。 -
DWT->CYCCNT:清零计数器。 -
DWT->CTRL:使能 CYCCNT(位 0)。
#define DWT_CTRL_CYCCNTENA_Msk (1UL << 0)
#define CoreDebug_DEMCR_TRCENA_Msk (1UL << 24)
void DWT_Init(void) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
2.2 替换中断向量
以 SysTick 中断为例,原 ISR 为 SysTick_Handler。我们创建一个包装函数 SysTick_Handler_Wrapper,在其中读取 CYCCNT 并保存到全局变量,然后调用原 SysTick_Handler。
volatile uint32_t isr_entry_cycle;
volatile uint32_t isr_latency;
void SysTick_Handler_Wrapper(void) {
isr_entry_cycle = DWT->CYCCNT;
// 计算延迟:需要知道中断触发时刻的 CYCCNT
// 这里假设我们在触发中断前记录了 trigger_cycle
isr_latency = isr_entry_cycle - trigger_cycle;
SysTick_Handler(); // 调用原处理函数
}
但 trigger_cycle 如何获取?对于周期性中断,我们可以在上一次中断结束时预测下一次触发时刻,但不够精确。更好的方法:使用另一个定时器在固定时刻触发软件中断,并在触发前读取 CYCCNT。例如,使用 TIM2 比较事件触发软件中断,在使能中断前读取 CYCCNT 作为 trigger_cycle。
void TIM2_IRQHandler(void) {
if (TIM2->SR & TIM_SR_CC1IF) {
TIM2->SR &= ~TIM_SR_CC1IF;
trigger_cycle = DWT->CYCCNT; // 记录触发时刻
NVIC_SetPendingIRQ(EXTI0_IRQn); // 触发目标中断
}
}
然后目标中断的包装函数中计算延迟。
2.3 完整代码示例
以下代码基于 STM32F4,使用 TIM2 触发 EXTI0 软件中断。
#include "stm32f4xx.h"
volatile uint32_t trigger_cycle = 0;
volatile uint32_t isr_entry_cycle = 0;
volatile uint32_t latency_cycles = 0;
void DWT_Init(void) {
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
void TIM2_Init(void) {
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
TIM2->PSC = 16000 - 1; // 1MHz 计数频率
TIM2->ARR = 1000 - 1; // 1ms 周期
TIM2->CCR1 = 500; // 比较值
TIM2->CCMR1 |= TIM_CCMR1_OC1M_1 | TIM_CCMR1_OC1M_2; // 比较模式
TIM2->CCER |= TIM_CCER_CC1E;
TIM2->DIER |= TIM_DIER_CC1IE;
TIM2->CR1 |= TIM_CR1_CEN;
NVIC_EnableIRQ(TIM2_IRQn);
}
void EXTI0_IRQHandler_Wrapper(void) {
isr_entry_cycle = DWT->CYCCNT;
latency_cycles = isr_entry_cycle - trigger_cycle;
// 原 EXTI0_IRQHandler 内容
EXTI->PR = EXTI_PR_PR0;
}
void TIM2_IRQHandler(void) {
if (TIM2->SR & TIM_SR_CC1IF) {
TIM2->SR &= ~TIM_SR_CC1IF;
trigger_cycle = DWT->CYCCNT;
NVIC_SetPendingIRQ(EXTI0_IRQn);
}
}
int main(void) {
DWT_Init();
TIM2_Init();
NVIC_EnableIRQ(EXTI0_IRQn);
while (1) {
// 主循环
}
}
注意:需要将 EXTI0_IRQHandler 替换为 EXTI0_IRQHandler_Wrapper,可通过修改启动文件或使用链接脚本重定向。
3. 数据解读
latency_cycles 即为从中断触发(NVIC_SetPendingIRQ 执行后)到 ISR 第一条指令执行之间的周期数。典型值包括:
- 内核响应中断的固定开销(约 12 周期)
- 流水线刷新、取向量等
- 若使能了中断嵌套,还需考虑优先级
在 STM32F4 @168MHz 下,测得延迟通常在 12~20 周期,即约 70~120ns。若数值异常大,可能原因:
- 中断被更高优先级中断抢占
- Flash 等待周期导致取指延迟
- 未使能 CYCCNT 或 DWT 时钟
4. 注意事项
- CYCCNT 是 32 位,在 168MHz 下约 25.6 秒溢出,长时间测量需处理溢出。
- 测量代码本身会引入少量延迟(读取 CYCCNT 约 2-3 周期),但这是可接受的。
- 确保 DWT 在低功耗模式下仍运行,否则测量失效。
- 不同 Cortex-M 内核(M0/M3/M4/M7)的 DWT 功能略有差异,M0 无 DWT。
- 使用
__DSB()和__ISB()确保指令同步。
通过本文方法,你可以无侵入地获取精确的中断延迟数据,为实时性优化提供可靠依据。