用 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() 确保指令同步。

通过本文方法,你可以无侵入地获取精确的中断延迟数据,为实时性优化提供可靠依据。