3. 按键输入与中断:从抖动到精准控制
按键,看起来最简单的外设,其实坑不少。我刚开始做项目时,总觉得按键不就是个开关嘛,结果被“抖动”折磨得够呛。这一章,咱们就把按键消抖、外部中断、NVIC优先级这些硬骨头啃下来,最后用按键控制LED做个实战。
3.1 按键消抖原理
机械按键按下和松开时,触点不会瞬间稳定。你想想看,金属片碰撞会产生一连串的“弹跳”,持续时间大概5~20ms。如果不处理,一次按下可能被识别成好几次。
消抖的本质:等待信号稳定后再读取状态。
消抖方式主要有两种:
| 方式 | 原理 | 适用场景 |
|---|---|---|
| 硬件消抖 | RC低通滤波 + 施密特触发器 | 对成本不敏感、要求高可靠 |
| 软件消抖 | 延时20ms后再次读取 | 绝大多数嵌入式项目 |
我个人习惯用软件消抖,成本低、灵活。核心思路就一句话:检测到电平变化后,等20ms再读一次,确认还是那个电平,才算有效。
避坑指南:我曾经在一个项目中,按键接的是长线,干扰很大。20ms延时不够,最后加到了50ms才稳定。具体延时值,建议用示波器实测你的按键波形。
3.2 外部中断配置
轮询按键太浪费CPU了。你想,主循环一直在查按键状态,啥正事都干不了。外部中断就是解决方案——按键按下时,CPU才响应。
STM32的外部中断,说白了就是GPIO引脚检测到电平变化,触发一个中断请求。配置步骤我总结为四步:
- 使能GPIO时钟和SYSCFG时钟
- 配置GPIO为输入模式(上拉或下拉看电路)
- 将GPIO连接到EXTI线(通过SYSCFG)
- 配置EXTI触发方式(上升沿、下降沿或双边沿)
这里有个关键点:EXTI线0~15对应GPIO的Pin0~Pin15。比如PA0和PB0不能同时用EXTI0,因为它们是共享一条中断线的。
注意:同一个EXTI线只能映射一个GPIO引脚。如果你需要多个按键,必须用不同的Pin,比如PA0用EXTI0,PA1用EXTI1。
代码示例(以PA0为例,下降沿触发):
// 1. 使能时钟
RCC->APB2ENR |= RCC_APB2ENR_SYSCFGEN;
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
// 2. 配置PA0为输入,上拉
GPIOA->MODER &= ~(3 << 0); // 输入模式
GPIOA->PUPDR |= (1 << 0); // 上拉
// 3. 连接EXTI0到PA0
SYSCFG->EXTICR[0] &= ~(0xF << 0);
SYSCFG->EXTICR[0] |= (0 << 0); // PA口
// 4. 配置EXTI
EXTI->IMR |= (1 << 0); // 不屏蔽
EXTI->FTSR |= (1 << 0); // 下降沿触发
3.3 中断优先级与NVIC设置
中断优先级,说白了就是“谁更紧急”。STM32的NVIC支持最多16级抢占优先级和16级子优先级(具体看芯片型号)。
我见过不少新手,所有中断都设成一样的优先级,结果高频率中断把低优先级任务饿死了。嗯,这里要讲清楚两个概念:
- 抢占优先级:数值越小,优先级越高。高抢占优先级可以打断低抢占优先级的中断。
- 子优先级:当抢占优先级相同时,子优先级决定谁先执行,但不能互相打断。
配置NVIC的步骤:
- 调用
NVIC_PriorityGroupConfig()设置优先级分组 - 设置每个中断的抢占优先级和子优先级
- 使能中断
举个例子,按键中断优先级设得比定时器高,这样按键响应更及时:
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); // 2位抢占,2位子优先级
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // 最高抢占
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
我的经验:中断服务函数里尽量少做事。我习惯只设一个标志位,主循环里再处理。这样中断占用时间短,系统响应更快。
3.4 按键控制LED实战
理论讲完了,咱们动手。目标:按下按键,LED状态翻转。
整体流程我用一张图来说明:
完整代码实现:
volatile uint8_t key_flag = 0; // 按键标志位
void EXTI0_IRQHandler(void)
{
if(EXTI->PR & (1 << 0))
{
// 软件消抖:延时20ms
delay_ms(20);
// 再次确认按键是否真的按下
if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0)
{
key_flag = 1; // 设置标志位
}
EXTI->PR = (1 << 0); // 清除中断标志
}
}
int main(void)
{
// 初始化...
LED_Init();
KEY_Init();
NVIC_Configuration();
while(1)
{
if(key_flag)
{
key_flag = 0; // 清除标志
LED_Toggle(); // 翻转LED
}
// 其他任务...
}
}
重要提醒:中断服务函数里用delay_ms(20)会阻塞CPU。如果系统里还有其他中断,这个延时会影响响应。更好的做法是用定时器做延时,或者用状态机。不过对于入门学习,这样写最直观。
实战中还有几个细节要注意:
- 按键电路:外部加上拉电阻(10kΩ),防止浮空电平误触发
- 中断标志:一定要在中断退出前清除,否则会一直触发
- 优先级:按键中断优先级别设太高,否则可能影响系统关键中断
好了,这一章的内容就这些。按键消抖、外部中断、NVIC优先级,这三个知识点串起来,你就能做出一个响应灵敏、稳定可靠的按键控制系统。下次遇到按键问题,记得先查抖动,再查中断配置。