用 DWT 的 CYCCNT 做无侵入式中断延迟测量:从 NVIC 优先级到实际响应周期

1. 为什么需要无侵入式中断延迟测量?

中断延迟定义为从中断请求信号有效到中断服务程序(ISR)第一条指令执行的时间。在实时系统中,它直接影响控制环路的抖动。传统测量方法有两种:

  • GPIO 翻转法:在 ISR 入口翻转引脚,用示波器观察。缺点是需要占用引脚,且翻转本身引入额外延迟。
  • 定时器捕获法:用另一个定时器记录中断触发时刻。缺点是需要额外定时器资源,且中断嵌套时难以区分。

DWT(Data Watchpoint and Trace) 是 Cortex-M 内核的调试组件,其中的 CYCCNT 是一个 32 位自由运行的周期计数器,计数频率等于内核时钟(HCLK)。它不占用任何外设,不修改 ISR 代码,仅需在中断触发前后读取计数值,即可精确到单个时钟周期。

2. DWT CYCCNT 工作原理

  • CYCCNT 位于 DWT 控制寄存器的第 0 位使能后,每个内核时钟周期加 1。
  • 溢出周期:对于 72MHz 的 STM32F103,约 59.6 秒溢出一次;对于 168MHz 的 F407,约 25.6 秒。
  • 读取 DWT->CYCCNT 是一条 LDR 指令,耗时 1~2 个周期,对测量影响可忽略。
  • 关键点:CYCCNT 在中断入口自动保存/恢复? 不,它只是自由计数器,中断前后读取的差值就是实际经过的周期数。

3. 配置步骤

3.1 使能 DWT 和 CYCCNT

// dwt.h
#ifndef __DWT_H
#define __DWT_H

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

static inline 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;
}

#endif

3.2 配置 NVIC 优先级

为了测量不同优先级下的延迟,需要配置两个中断:一个作为被测中断(如 EXTI),另一个作为触发中断(如 TIM)。这里以 TIM2 触发 EXTI0 为例。

// 配置 EXTI0 为被测中断,优先级可调
NVIC_SetPriority(EXTI0_IRQn, 1);  // 抢占优先级 1
NVIC_EnableIRQ(EXTI0_IRQn);

// 配置 TIM2 为触发中断,优先级更高,用于记录起始时刻
NVIC_SetPriority(TIM2_IRQn, 0);   // 抢占优先级 0
NVIC_EnableIRQ(TIM2_IRQn);

3.3 测量代码框架

在 TIM2 中断中触发 EXTI0(通过软件置位),并记录 t_start;在 EXTI0 中断中记录 t_end,差值即为中断延迟。

volatile uint32_t t_start = 0;
volatile uint32_t t_end = 0;
volatile uint32_t latency_cycles = 0;

void TIM2_IRQHandler(void)
{
    if (TIM2->SR & TIM_SR_UIF) {
        TIM2->SR &= ~TIM_SR_UIF;
        t_start = DWT_GetCycles();
        // 软件触发 EXTI0
        EXTI->SWIER |= EXTI_SWIER_SWIER0;
    }
}

void EXTI0_IRQHandler(void)
{
    if (EXTI->PR & EXTI_PR_PR0) {
        EXTI->PR |= EXTI_PR_PR0;  // 清除标志
        t_end = DWT_GetCycles();
        latency_cycles = t_end - t_start;
        // 此处可设置断点或通过串口输出
    }
}

4. 完整示例:测量不同优先级下的中断延迟

以下代码基于 STM32F407,系统时钟 168MHz。通过改变 EXTI0 的抢占优先级,观察延迟变化。

#include "stm32f4xx.h"
#include "dwt.h"

volatile uint32_t latency[4] = {0};
volatile uint8_t  idx = 0;

void TIM2_IRQHandler(void)
{
    if (TIM2->SR & TIM_SR_UIF) {
        TIM2->SR &= ~TIM_SR_UIF;
        // 记录起始周期
        uint32_t start = DWT_GetCycles();
        // 软件触发 EXTI0
        EXTI->SWIER |= EXTI_SWIER_SWIER0;
        // 等待 EXTI0 执行完毕(通过标志)
        while (EXTI->PR & EXTI_PR_PR0);
        // 计算延迟
        uint32_t end = DWT_GetCycles();
        latency[idx] = end - start;
        idx = (idx + 1) & 0x03;
    }
}

void EXTI0_IRQHandler(void)
{
    if (EXTI->PR & EXTI_PR_PR0) {
        EXTI->PR |= EXTI_PR_PR0;
        // 空操作,仅用于测量
    }
}

void EXTI0_Config(uint8_t preempt_prio)
{
    // 使能 SYSCFG 时钟
    RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
    // 将 PA0 连接到 EXTI0
    SYSCFG->EXTICR[0] &= ~SYSCFG_EXTICR1_EXTI0;
    SYSCFG->EXTICR[0] |= SYSCFG_EXTICR1_EXTI0_PA;
    // 配置 EXTI0 为上升沿触发
    EXTI->RTSR |= EXTI_RTSR_TR0;
    EXTI->IMR  |= EXTI_IMR_MR0;
    // 设置优先级
    NVIC_SetPriority(EXTI0_IRQn, preempt_prio);
    NVIC_EnableIRQ(EXTI0_IRQn);
}

void TIM2_Config(void)
{
    // 使能 TIM2 时钟
    RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
    // 设置预分频和自动重装载,产生周期中断
    TIM2->PSC = 16800 - 1;  // 10kHz 计数
    TIM2->ARR = 1000 - 1;   // 100ms 周期
    TIM2->DIER |= TIM_DIER_UIE;
    TIM2->CR1 |= TIM_CR1_CEN;
    // 优先级最高
    NVIC_SetPriority(TIM2_IRQn, 0);
    NVIC_EnableIRQ(TIM2_IRQn);
}

int main(void)
{
    DWT_Init();
    EXTI0_Config(1);  // 先测优先级 1
    TIM2_Config();
    while (1) {
        // 通过串口或调试器观察 latency 数组
        __NOP();
    }
}

5. 结果分析与注意事项

5.1 典型延迟值

  • 无嵌套、无等待:Cortex-M4 从 EXTI 触发到 ISR 第一条指令,通常为 12~16 个周期(包括压栈、取向量、流水线刷新)。
  • 优先级影响:若高优先级中断正在执行,低优先级中断需等待,延迟可能增加数百周期。
  • Flash 等待周期:若 ISR 位于 Flash 且未使能预取,取指延迟会增加。

5.2 注意事项

  • CYCCNT 溢出:测量时间差时,若两次读取跨越溢出点,需用无符号减法 end - start 自动处理。
  • 编译器优化:DWT_GetCycles 应使用 static inline 或 __STATIC_INLINE,避免函数调用开销。
  • 中断嵌套:测量时确保触发中断(TIM2)优先级高于被测中断(EXTI0),否则无法准确捕获起始时刻。
  • 调试器影响:连接调试器时,DWT 可能被复位或禁用,需在初始化中重新使能。
  • 低功耗模式:在 Sleep 或 Stop 模式下,CYCCNT 可能停止计数,测量前需唤醒。

6. 总结

利用 DWT 的 CYCCNT 进行中断延迟测量,无需额外硬件,不修改 ISR 逻辑,精度可达单个时钟周期。通过调整 NVIC 优先级,可以量化优先级对实时性的影响,为系统优化提供数据支撑。该方法适用于 Cortex-M3/M4/M7 等内核,是嵌入式开发者工具箱中的利器。