用 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 等内核,是嵌入式开发者工具箱中的利器。