为什么GPIO翻转法不够准

很多开发者用“进中断拉高GPIO、退出拉低”配合示波器测中断延迟。这个方法直观,但存在几类系统误差:

  • 引脚驱动延迟:GPIO输出级从写寄存器到电平真正翻转有数纳秒到十几纳秒的延迟,且随负载电容变化。
  • 示波器与探头误差:探头带宽、接地弹簧长度都会引入振铃和延迟。
  • 编译器重排:GPIOA->BSRR = ... 可能被优化或与前后指令重排,导致测量点漂移。
  • 无法区分“进入ISR”与“执行到某条语句”:拉高动作本身就在ISR里,已经包含了函数序言。

要得到“从外设事件到ISR第一条指令”的真实耗时,最干净的办法是使用内核自带的DWT周期计数器(Data Watchpoint and Trace),它以CPU时钟为节拍,精度为一个周期。

DWT周期计数器原理

Cortex-M3/M4/M7/M33 的 DWT 单元包含一个32位自由运行计数器 DWT->CYCCNT,使能后每个内核时钟周期加1。相关寄存器:

  • CoreDebug->DEMCR 的 TRCENA 位:DWT总开关。
  • DWT->CTRL 的 CYCCNTENA 位:计数器使能。
  • DWT->CYCCNT:当前周期计数值。

在168MHz的STM32F4上,32位计数器约25.6秒回绕一次,单次测量完全够用。测量思路:在中断触发源处记录CYCCNT,在ISR入口再读一次,差值即延迟周期数。

难点在于“触发源处”如何记录。对于EXTI外部中断,可以在触发前由软件写时间戳;对于定时器中断,可在更新事件回调前读取。更通用的做法是:用另一个GPIO在触发瞬间翻转,同时用DWT记录,二者交叉验证。

配置步骤

  1. 使能DWT:置位DEMCR.TRCENA,再置位DWT->CTRL.CYCCNTENA。
  2. 配置EXTI中断源(以PA0下降沿为例),NVIC使能对应通道。
  3. 在主循环中产生触发信号(或用外部信号源),触发前读取t0 = DWT->CYCCNT。
  4. ISR第一条可执行语句读取t1 = DWT->CYCCNT,保存差值。
  5. 多次采样取最小值和平均值,排除流水线抖动。

完整代码示例

#include "stm32f4xx.h"

volatile uint32_t g_delay_cycles = 0;
volatile uint32_t g_t0 = 0;

static void dwt_init(void)
{
    CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
    DWT->CYCCNT = 0;
    DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
}

static void exti0_init(void)
{
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    GPIOA->MODER &= ~GPIO_MODER_MODER0;
    GPIOA->PUPDR |= GPIO_PUPDR_PUPDR0_0;   /* 上拉 */

    RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
    SYSCFG->EXTICR[0] &= ~SYSCFG_EXTICR1_EXTI0;
    EXTI->IMR  |= EXTI_IMR_MR0;
    EXTI->FTSR |= EXTI_FTSR_TR0;           /* 下降沿 */

    NVIC_SetPriority(EXTI0_IRQn, 0);
    NVIC_EnableIRQ(EXTI0_IRQn);
}

void EXTI0_IRQHandler(void)
{
    uint32_t t1 = DWT->CYCCNT;             /* ISR入口时间戳 */
    g_delay_cycles = t1 - g_t0;
    EXTI->PR = EXTI_PR_PR0;                /* 清挂起 */
}

int main(void)
{
    dwt_init();
    exti0_init();

    for (;;) {
        __disable_irq();
        g_t0 = DWT->CYCCNT;                /* 触发前时间戳 */
        __enable_irq();
        /* 此处由外部信号拉低PA0,或软件模拟触发 */
        for (volatile int i = 0; i < 100000; i++) { }
    }
}

注意:g_t0 的读取必须紧邻触发事件之前,且 __disable_irq/__enable_irq 之间的窗口要尽量短,否则会把关中断时间算进延迟。

结果解读与校准

在STM32F407@168MHz、EXTI0、无中断嵌套条件下,实测差值通常在 12~20个周期(约70~120ns),包含:

  • 中断采样与同步:约2~3周期;
  • 压栈(8个寄存器):约8周期(取决于总线等待);
  • 向量取指与流水线填充:约4~6周期。

若数值明显偏大,检查:

  • Flash等待周期:高主频下若未开ART/预取,向量表取指会多出等待周期。
  • 中断优先级与嵌套:更高优先级ISR正在执行时,延迟会叠加。
  • 编译优化:-O0 下ISR序言更长,建议用 -O2 并对比。
  • 计数器回绕:单次测量不会回绕,但若跨25秒需处理无符号减法。

注意事项

  • DWT在调试器连接时默认可能被复位,量产固件中需自行使能。
  • 部分低端Cortex-M0/M0+没有DWT CYCCNT,需改用SysTick或GPIO法。
  • 测量时关闭不必要的中断,避免测量窗口被抢占。
  • 多次采样取最小值更接近“纯硬件延迟”,平均值反映实际负载。
  • 若用GPIO交叉验证,记得扣除GPIO输出级延迟(可先用已知循环校准)。

用DWT替代GPIO翻转,能把中断延迟测量从“示波器上看个大概”提升到“周期级可复现”,对RTOS任务切换、电机控制环路和通信协议时序分析都很有价值。