用DWT周期计数器测量中断响应延迟:从GPIO翻转看NVIC抢占的实际开销
1. 为什么需要实测中断延迟
在STM32等Cortex-M MCU上,中断响应延迟由硬件自动完成:保存上下文、取向量、跳转ISR。数据手册通常给出“12个周期”之类的理论值,但实际系统中还受以下因素影响:
- NVIC抢占优先级配置与当前执行状态
- Flash等待周期(WS)与预取缓冲区状态
- 总线矩阵仲裁与DMA活动
- 编译器生成的ISR入口代码
因此,实测比查手册更有意义。DWT(Data Watchpoint and Trace)中的CYCCNT计数器以CPU时钟为基准递增,精度达1个周期,是测量短延时的理想工具。
2. DWT周期计数器原理
Cortex-M3/M4/M7内核的DWT单元包含一个32位自由运行计数器CYCCNT,使能后每个内核时钟周期加1。关键寄存器:
-
DWT->CTRL:bit0使能CYCCNT -
DWT->CYCCNT:当前计数值 -
CoreDebug->DEMCR:bit24使能TRCENA(DWT总开关)
注意:CYCCNT在调试器连接时可能被复位,且部分低端型号(如Cortex-M0)不支持DWT。
3. 测量方案设计
思路:在外部中断引脚上产生一个边沿,同时在ISR入口翻转另一个GPIO,用逻辑分析仪或示波器观察两路信号的时间差。但更精确的方法是纯软件测量:
- 在主循环中等待中断标志(或使用EXTI触发)。
- 在ISR的第一条语句读取
DWT->CYCCNT并保存。 - 在触发中断前读取一次CYCCNT作为起点。
- 两者差值即为从触发到ISR执行的总周期数。
但“触发”时刻难以精确捕获。改进:用定时器输出一个脉冲到GPIO,同时触发EXTI;在ISR中翻转另一个GPIO并记录CYCCNT。用示波器测量两个GPIO边沿的物理延迟,再与CYCCNT差值对照,可分离出GPIO翻转本身的开销。
本文采用纯软件自测法:利用一个高优先级定时器中断作为“触发源”,在定时器ISR中翻转GPIO并记录CYCCNT,然后立即软件触发一个低优先级EXTI中断(通过NVIC_SetPendingIRQ),在EXTI ISR入口读取CYCCNT。两者差值包含:中断挂起、NVIC抢占、上下文保存、取向量、跳转等全部开销。
4. 配置步骤
4.1 使能DWT
static void DWT_Init(void)
{
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
DWT->CYCCNT = 0;
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}
4.2 配置GPIO与EXTI
以STM32F4为例,PA0作为EXTI0输入,PA1作为调试输出。
void GPIO_EXTI_Init(void)
{
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
GPIOA->MODER &= ~(3U << 0); // PA0 输入
GPIOA->MODER |= (1U << 2); // PA1 输出
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
SYSCFG->EXTICR[0] &= ~SYSCFG_EXTICR1_EXTI0;
EXTI->IMR |= EXTI_IMR_MR0;
EXTI->RTSR |= EXTI_RTSR_TR0;
NVIC_SetPriority(EXTI0_IRQn, 2);
NVIC_EnableIRQ(EXTI0_IRQn);
}
4.3 定时器触发源
使用TIM2更新中断,优先级设为0(高于EXTI0)。
void TIM2_Init(void)
{
RCC->APB1ENR |= RCC_APB1ENR_TIM2EN;
TIM2->PSC = 84 - 1; // 1MHz计数
TIM2->ARR = 1000 - 1; // 1ms周期
TIM2->DIER |= TIM_DIER_UIE;
TIM2->CR1 |= TIM_CR1_CEN;
NVIC_SetPriority(TIM2_IRQn, 0);
NVIC_EnableIRQ(TIM2_IRQn);
}
5. 测量代码
volatile uint32_t t_start, t_end, latency_cycles;
volatile uint32_t isr_entry_cycles;
void TIM2_IRQHandler(void)
{
TIM2->SR &= ~TIM_SR_UIF;
t_start = DWT->CYCCNT;
GPIOA->ODR ^= (1U << 1); // 调试翻转
NVIC_SetPendingIRQ(EXTI0_IRQn); // 软件触发EXTI0
}
void EXTI0_IRQHandler(void)
{
isr_entry_cycles = DWT->CYCCNT; // ISR第一条有效语句
EXTI->PR = EXTI_PR_PR0;
t_end = isr_entry_cycles;
latency_cycles = t_end - t_start;
GPIOA->ODR ^= (1U << 1);
}
主循环中打印latency_cycles。注意:NVIC_SetPendingIRQ本身有函数调用开销,应将其内联或直接写NVIC->STIR = EXTI0_IRQn。
NVIC->STIR = EXTI0_IRQn; // 直接触发,减少开销
6. 结果分析与注意事项
在STM32F407@168MHz、Flash 5WS、开启预取的情况下,实测latency_cycles约为18~24周期,对应约107~143ns。其中包含:
-
NVIC->STIR写入到中断挂起:约2~3周期 - NVIC抢占仲裁:约2周期
- 上下文保存(8个寄存器压栈):取决于总线,约6~8周期
- 取向量与跳转:约4~6周期
- ISR入口压栈与第一条指令:约4周期
注意事项:
- 测量时关闭编译器优化对
DWT->CYCCNT读取的重排,使用volatile或内存屏障。 - 确保
t_start读取后到NVIC->STIR之间没有其他中断插入,可将全局中断关闭再打开,但会引入额外开销。 - CYCCNT在调试暂停时可能停止,量产测试时不要连接调试器。
- 不同优先级配置会改变抢占延迟,建议测试多组优先级。
- Flash等待周期对取指影响显著,可将ISR放在RAM中运行对比。
7. 优化建议
- 将ISR放入RAM(
__attribute__((section(".ramfunc"))))可减少Flash等待。 - 使用
__attribute__((interrupt))或汇编入口减少编译器插入的额外指令。 - 避免在ISR入口调用函数,直接操作寄存器。
- 合理设置NVIC优先级分组,减少抢占仲裁层级。
通过DWT周期计数器,开发者可以量化中断延迟,为实时性设计提供数据支撑。
暂无评论,来抢沙发~