4. GPIO与外部事件的实时响应

在实时控制系统中,GPIO 不只是“读电平、写电平”的普通外设,它往往是外部事件进入 MCU 的第一道入口。按键按下、限位开关触发、霍尔传感器翻转、编码器脉冲到来,这些事件都通过 GPIO 引脚进入芯片内部,再经由 EXTI(外部中断/事件控制器)或定时器输入捕获通道,转化为可被调度器处理的事件。本节围绕“事件如何被及时、可靠地捕获并转化为确定响应”这一核心问题展开。

一、输入捕获:把边沿变成时间戳

输入捕获(Input Capture)是定时器的重要功能。当指定引脚上出现上升沿、下降沿或双边沿时,定时器硬件自动把当前计数器 CNT 的值锁存到捕获寄存器 CCR 中,并可触发捕获中断或 DMA 请求。它最大的价值在于:时间戳由硬件记录,不受软件中断延迟影响。因此测量脉宽、周期、频率、占空比时,输入捕获比“中断里读系统滴答”精度高得多。

以 STM32F4 为例,假设 TIM2 挂载在 APB1,定时器时钟 84 MHz,预分频 PSC=83,则计数频率为 84 MHz/(83+1)=1 MHz,即每个计数代表 1 μs。若捕获到上升沿时 CCR=1200,下降沿时 CCR=4500,则高电平脉宽为 (4500−1200)×1 μs=3300 μs。若计数器在两次捕获之间发生溢出,还需结合溢出次数计算总时间,这就是“溢出补偿”。

配置要点:将 GPIO 设为复用功能(AF)并选择对应定时器通道;定时器设为输入捕获模式,选择边沿极性;使能捕获中断;在中断中读取 CCR 并记录溢出计数。对于高频脉冲,建议使用 DMA 搬运捕获值,避免中断过于频繁。

二、外部中断与消抖:让事件“干净”地进入系统

机械按键和部分传感器在状态切换瞬间会产生 5~20 ms 的抖动,表现为电平快速跳变。如果直接把 EXTI 中断接到按键,一次按下可能触发多次中断,导致计数错误、状态机误跳转。消抖的本质是:在时间维度上确认电平稳定后再承认事件。

常见做法有三类:

  • 硬件消抖:RC 低通 + 施密特触发器,成本低但占用板面,时间常数需与按键特性匹配。
  • 定时器扫描消抖:用 1 ms 或 5 ms 周期定时器轮询按键,连续 N 次读到相同电平才确认。适合按键数量多、实时性要求不极端的场景。
  • 中断 + 定时器延时确认:EXTI 触发后启动一个 10 ms 单次定时器,到期再读引脚,若仍为有效电平则确认事件。响应快且 CPU 占用低。

下面给出一个“中断触发 + 定时确认”的消抖实现示例(基于 HAL 库,参数为示例值):

// 按键接 PA0,下降沿触发,内部上拉
void HAL_GPIO_EXTI_Callback(uint16_t pin) {
    if (pin == GPIO_PIN_0) {
        // 启动 10ms 单次定时器用于确认
        HAL_TIM_Base_Start_IT(&htim6);
    }
}

// TIM6 周期 10ms,在更新中断中确认
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) {
    if (htim->Instance == TIM6) {
        HAL_TIM_Base_Stop_IT(&htim6);
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
            key_event = 1;   // 确认有效按下
        }
    }
}

这里 10 ms 是示例值,实际应根据按键手册的抖动时间选取,通常 5~20 ms。若按键抖动较大,可延长到 20 ms;若追求快速响应,可在确认后立即置事件标志,由主循环或任务处理。

三、按键与传感器事件触发:从“电平”到“事件”

在实时系统中,GPIO 输入应尽量转化为“事件”而非“状态查询”。事件触发方式包括:EXTI 中断、定时器输入捕获、DMA 请求。对于按键,推荐“中断 + 消抖确认 + 事件队列”;对于霍尔/光电传感器,若输出为脉冲,优先用输入捕获或 EXTI 计数;对于模拟传感器,则通过 ADC + 阈值比较或窗口看门狗产生事件。

事件进入系统后,不应在中断里做耗时处理。正确做法是:中断中只做“记录时间戳、置标志、发消息队列”,把具体业务交给任务。例如:

// 中断中仅发送事件,不处理业务
void HAL_GPIO_EXTI_Callback(uint16_t pin) {
    if (pin == SENSOR_PIN) {
        BaseType_t hpw = pdFALSE;
        Event_t ev = { .id = EV_SENSOR, .tick = HAL_GetTick() };
        xQueueSendFromISR(sensor_queue, &ev, &hpw);
        portYIELD_FROM_ISR(hpw);
    }
}

这样,中断延迟被压缩到最小,业务处理由任务按优先级调度,系统实时性显著提升。

四、响应时间优化:从引脚到任务的路径

外部事件从引脚到任务响应的总延迟,可拆解为:硬件传播延迟 + EXTI 同步延迟 + 中断入口延迟 + 中断服务执行时间 + 任务调度延迟。优化方向包括:

  • 提高 EXTI 中断优先级,但不要高于系统关键中断(如 SysTick、PendSV 需合理配置)。
  • 中断服务函数尽量短,只做标志、时间戳、队列发送。
  • 使用 DMA 搬运高频捕获数据,减少中断次数。
  • 避免在中断中调用阻塞式 API 或浮点运算。
  • 若使用 RTOS,确保事件队列和任务优先级匹配,避免高优先级任务长期占用 CPU。

以 STM32F407 @168 MHz 为例,若 EXTI 中断优先级为 5,中断入口约 12 个周期,服务函数约 30 个周期,则中断响应约 42 周期 ≈ 0.25 μs(不含硬件同步)。若任务调度再增加 1~2 μs,总延迟通常在几微秒量级。实际值需用示波器测量。

五、示波器验证中断延迟

验证方法:在中断服务函数入口翻转一个空闲 GPIO(如 PB0),用示波器同时观察触发信号(接 PA0)和 PB0。触发信号上升沿到 PB0 翻转的时间差,即为“引脚到中断入口”的延迟。若再在任务中翻转另一个 GPIO(如 PB1),则可测量“引脚到任务响应”的总延迟。

测量时注意:示波器探头地线要短,避免引入噪声;触发方式设为上升沿触发;时间基准设为 1 μs/div 或更小。若测得延迟远大于预期,应检查中断优先级、是否有高优先级中断长时间占用、是否在中断中执行了耗时操作。

外部事件从引脚到任务响应的信号链与延迟构成 外部事件 按键/传感器 GPIO 引脚 电平跳变 EXTI 边沿检测 同步/挂起 NVIC 中断入口 优先级仲裁 ISR 执行 置标志/发队列 总响应延迟 = 硬件传播 + 同步 + 中断入口 + ISR + 调度 示波器验证:触发信号 vs 中断翻转信号 PA0 触发 PB0 中断翻转 Δt ≈ 中断延迟 t

通过上述信号链,可以把“外部事件响应”从模糊的“很快”变成可测量、可优化的工程指标。实际项目中,建议把中断延迟、任务响应延迟纳入测试用例,在每次修改中断优先级或任务配置后复测,确保系统实时性始终满足要求。