3. 外部中断EXTI:边沿触发配置、中断服务函数编写、消抖处理实战

外部中断,说白了就是让单片机对外界的电平变化做出即时反应。你按一下按键,或者传感器输出一个跳变沿,CPU 就能立刻停下手里的事去处理。这节我们就把 CMS32 的 EXTI 彻底讲透。

3.1 边沿触发配置:上升沿、下降沿还是双沿?

CMS32 的每个 EXTI 通道都支持独立配置触发边沿。我个人习惯把 GPIO 的输入模式和 EXTI 的触发沿放在一起配,这样逻辑清晰,不容易漏。

触发方式 适用场景 注意事项
上升沿触发 按键松开检测、脉冲计数 按键按下时可能产生抖动,需消抖
下降沿触发 按键按下检测、边沿中断唤醒 低电平有效信号常用下降沿
双沿触发 编码器信号、频率测量 中断频率翻倍,注意处理速度

配置代码其实不复杂,关键是寄存器别写错。我贴一段我项目里常用的初始化函数:

void EXTI_Init(void)
{
    // 1. 开启 GPIO 时钟和 EXTI 时钟
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_AFIOEN;
    
    // 2. 配置 PA0 为输入模式(浮空输入)
    GPIOA->CRL &= ~(0x0F << 0);  // 清除 CNF0 和 MODE0
    GPIOA->CRL |= (0x04 << 0);   // 浮空输入模式
    
    // 3. 选择 PA0 作为 EXTI0 的输入源
    AFIO->EXTICR[0] &= ~AFIO_EXTICR1_EXTI0;
    AFIO->EXTICR[0] |= AFIO_EXTICR1_EXTI0_PA;
    
    // 4. 配置 EXTI 触发边沿(下降沿触发)
    EXTI->FTSR |= EXTI_FTSR_TR0;   // 使能下降沿触发
    EXTI->RTSR &= ~EXTI_RTSR_TR0;  // 关闭上升沿触发
    
    // 5. 取消屏蔽,使能中断
    EXTI->IMR |= EXTI_IMR_MR0;
    
    // 6. 配置 NVIC 优先级
    NVIC_SetPriority(EXTI0_IRQn, 2);
    NVIC_EnableIRQ(EXTI0_IRQn);
}
我的习惯:配置完 EXTI 后,我会先软件触发一次中断,测试中断线是否通。用 EXTI->SWIER 寄存器写 1 就能模拟硬件触发,调试时特别好用。

3.2 中断服务函数编写:别在里面干重活

中断服务函数(ISR)的核心原则就一条:快进快出。你想想看,CPU 正在处理一个紧急中断,结果你在 ISR 里跑了个延时函数或者 printf,那其他中断就只能排队等着,系统实时性就崩了。

我建议 ISR 里只做三件事:

  1. 读取中断标志位,确认中断源
  2. 清除中断标志位
  3. 设置一个全局标志或唤醒信号量

来看一个标准的 ISR 写法:

volatile uint8_t g_key_pressed = 0;  // 全局标志,主循环轮询

void EXTI0_IRQHandler(void)
{
    // 1. 检查中断标志
    if (EXTI->PR & EXTI_PR_PR0)
    {
        // 2. 清除标志位(写 1 清除)
        EXTI->PR = EXTI_PR_PR0;
        
        // 3. 设置标志,通知主循环处理
        g_key_pressed = 1;
        
        // 4. 绝对不要在这里做延时、打印、复杂计算!
    }
}
我曾经踩过的坑:有一次我在 ISR 里调用了 HAL_Delay(),结果系统直接卡死。因为 HAL_Delay 依赖 SysTick 中断,而 SysTick 优先级比 EXTI 低,EXTI 一直占着 CPU,SysTick 永远得不到执行——死锁了。记住:ISR 里不要调用任何可能依赖其他中断的函数。

3.3 消抖处理实战:硬件消抖 vs 软件消抖

机械按键按下时,触点会弹跳几次,产生一连串的脉冲。如果不做消抖,一次按键可能触发 3~5 次中断。嗯,这里要注意,消抖不是可选项,是必选项。

3.3.1 硬件消抖

最简单的办法是在按键两端并联一个 0.1μF 电容。电容能吸收弹跳产生的毛刺。我一般用 RC 滤波:10kΩ 电阻串联 + 0.1μF 电容对地。这样硬件上就把大部分抖动滤掉了,软件负担小很多。

3.3.2 软件消抖

如果硬件已经做了消抖,软件可以只做简单的延时确认。如果硬件没做,那就得靠软件来扛。我常用的软件消抖策略有两种:

消抖方式 原理 优点 缺点
延时消抖 检测到边沿后延时 10~20ms,再次读取电平确认 实现简单 阻塞 CPU,不适合实时性要求高的场景
状态机消抖 用定时器轮询,连续多次采样一致才确认 非阻塞,适合多按键 代码稍复杂

我个人更推荐状态机消抖,尤其是在项目里按键数量超过 2 个的时候。下面是一个基于定时器轮询的状态机消抖示例:

#define DEBOUNCE_TIME_MS  20   // 消抖时间 20ms
#define SAMPLE_INTERVAL_MS 5   // 采样间隔 5ms

typedef enum {
    KEY_STATE_IDLE,
    KEY_STATE_DEBOUNCE,
    KEY_STATE_PRESSED
} KeyState_t;

KeyState_t key_state = KEY_STATE_IDLE;
uint8_t debounce_count = 0;

void Key_Scan(void)
{
    uint8_t level = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0);
    
    switch (key_state)
    {
        case KEY_STATE_IDLE:
            if (level == 0)  // 检测到低电平(按键按下)
            {
                key_state = KEY_STATE_DEBOUNCE;
                debounce_count = 0;
            }
            break;
            
        case KEY_STATE_DEBOUNCE:
            if (level == 0)
            {
                debounce_count++;
                if (debounce_count >= (DEBOUNCE_TIME_MS / SAMPLE_INTERVAL_MS))
                {
                    key_state = KEY_STATE_PRESSED;
                    g_key_pressed = 1;  // 确认按键有效
                }
            }
            else
            {
                // 采样到高电平,说明是抖动,回到空闲状态
                key_state = KEY_STATE_IDLE;
            }
            break;
            
        case KEY_STATE_PRESSED:
            if (level == 1)  // 按键释放
            {
                key_state = KEY_STATE_IDLE;
            }
            break;
    }
}
关键点:状态机消抖的核心是“连续多次采样一致才确认”。20ms 内连续 4 次(每 5ms 一次)都读到低电平,才认为按键真的按下了。这样即使有毛刺,只要毛刺宽度小于 5ms,就会被过滤掉。

3.4 本章知识体系

我把 EXTI 的配置、ISR 编写和消抖处理串成一张图,方便你对照理解:

外部中断EXTI知识体系 外部中断EXTI知识体系 边沿触发配置 上升沿 / 下降沿 / 双沿 GPIO输入模式配置 AFIO映射选择 NVIC优先级设置 中断服务函数(ISR) 读取中断标志位 清除中断标志位 设置全局标志 ⚠ 禁止延时/打印 消抖处理 硬件消抖:RC滤波 软件消抖:延时法 软件消抖:状态机法 连续采样确认 核心原则:快进快出 + 消抖确认 + 标志传递 实战流程 ① 配置GPIO为输入 → ② 选择EXTI映射源 → ③ 设置触发边沿 ④ 编写ISR(读标志→清标志→设全局变量)→ ⑤ 实现消抖逻辑 ⑥ 主循环轮询全局标志,执行实际业务处理

3.5 避坑指南与经验总结

  • 中断标志位必须手动清除:CMS32 的 EXTI 标志位不会自动清零,不清除的话会一直进中断。我见过新手调试时发现程序卡在 ISR 里出不来,就是忘了清标志。
  • NVIC 优先级别设太高:如果 EXTI 优先级高于 SysTick,那延时函数、HAL_Delay 全都会失效。我一般把 EXTI 优先级设在 1~3 之间,留 0 给系统滴答定时器。
  • 消抖时间不是越长越好:20ms 是机械按键的典型弹跳时间。如果设到 100ms,按键手感会变得很迟钝,用户按下去要等很久才有反应。
  • 多按键共用一个 EXTI 通道?:不行。每个 EXTI 通道只能对应一个 GPIO 引脚。多个按键必须用不同的 EXTI 通道,或者用 GPIO 中断 + 软件轮询的方式。
我的调试小技巧:在 ISR 里加一个 GPIO 翻转,用示波器看这个引脚的波形。如果波形上有毛刺,说明消抖没做好;如果波形干净,说明消抖有效。这个方法比看代码日志直观多了。