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 里只做三件事:
- 读取中断标志位,确认中断源
- 清除中断标志位
- 设置一个全局标志或唤醒信号量
来看一个标准的 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 编写和消抖处理串成一张图,方便你对照理解:
3.5 避坑指南与经验总结
- 中断标志位必须手动清除:CMS32 的 EXTI 标志位不会自动清零,不清除的话会一直进中断。我见过新手调试时发现程序卡在 ISR 里出不来,就是忘了清标志。
- NVIC 优先级别设太高:如果 EXTI 优先级高于 SysTick,那延时函数、HAL_Delay 全都会失效。我一般把 EXTI 优先级设在 1~3 之间,留 0 给系统滴答定时器。
- 消抖时间不是越长越好:20ms 是机械按键的典型弹跳时间。如果设到 100ms,按键手感会变得很迟钝,用户按下去要等很久才有反应。
- 多按键共用一个 EXTI 通道?:不行。每个 EXTI 通道只能对应一个 GPIO 引脚。多个按键必须用不同的 EXTI 通道,或者用 GPIO 中断 + 软件轮询的方式。
我的调试小技巧:在 ISR 里加一个 GPIO 翻转,用示波器看这个引脚的波形。如果波形上有毛刺,说明消抖没做好;如果波形干净,说明消抖有效。这个方法比看代码日志直观多了。