3. 按键输入与中断:GPIO输入模式、按键消抖与EXTI配置
按键,是嵌入式系统里最基础、也最容易出幺蛾子的外设。你想想看,一个简单的开关动作,到了单片机眼里却可能变成一连串的“抖动”信号。这一章,我们就来彻底搞定按键输入——从GPIO怎么配,到消抖怎么处理,再到中断怎么用。
本章核心脉络:GPIO输入模式配置 → 硬件/软件消抖 → EXTI外部中断 → 中断优先级管理。说白了,就是让单片机“看懂”按键按下这个动作,并且能及时响应。
3.1 GPIO输入模式:不只是“读引脚”那么简单
很多人觉得GPIO输入就是配个模式、读个电平。其实没那么简单。我刚开始做项目时,就因为在输入模式上偷懒,吃了不少苦头。
STM32的GPIO输入模式,主要有这么几种配置:
| 配置项 | 选项 | 说明 |
|---|---|---|
| GPIO_Mode | GPIO_Mode_IN_FLOATING | 浮空输入,电平由外部决定,不稳定 |
| GPIO_Mode | GPIO_Mode_IPU | 上拉输入,默认高电平,按键按下拉低 |
| GPIO_Mode | GPIO_Mode_IPD | 下拉输入,默认低电平,按键按下拉高 |
| GPIO_Speed | 10MHz / 50MHz | 输入模式下速度影响不大,但建议统一配置 |
我个人习惯:按键电路如果外部已经接了上拉电阻,就用浮空输入;如果外部没接,就用内部上拉。千万别用下拉——你想想看,按键按下拉高,松开后引脚悬空,电平飘忽不定,那画面太美我不敢看。
小技巧:配置GPIO输入时,记得开启对应的AFIO时钟(如果需要复用功能),但普通输入不需要。我见过有人把输入口配成推挽输出,结果按键按下直接短路——嗯,芯片没烧算运气好。
3.2 按键消抖:硬件与软件两手抓
按键按下和松开的瞬间,机械触点会弹跳,产生一连串的脉冲。这个时间大概5~20ms。如果不处理,单片机可能认为你按了十几次。
消抖有两种方式:硬件消抖和软件消抖。
3.2.1 硬件消抖
硬件消抖,说白了就是用电容把毛刺滤掉。典型电路如下:
VCC → 10kΩ上拉电阻 → 按键 → GND
|
100nF电容 → GND
电容的作用是吸收弹跳产生的高频分量。RC时间常数τ = R × C,一般取1~10ms。我建议用10kΩ + 100nF,τ ≈ 1ms,效果不错。
注意:硬件消抖虽然省事,但会增加BOM成本和PCB面积。量产项目里,如果按键数量多,成本压力就上来了。我曾经在一个项目里用了8个按键,每个都加电容,结果被硬件经理骂了一顿——后来全改成软件消抖了。
3.2.2 软件消抖
软件消抖更灵活,也省钱。核心思路就是:检测到电平变化后,延时一段时间再读一次,确认状态稳定。
我常用的软件消抖流程:
- 检测到按键按下(电平变化)
- 延时10~20ms(用定时器或简单循环)
- 再次读取按键电平
- 如果状态一致,确认按键动作
- 等待按键释放(可选,防止重复触发)
代码示例:
uint8_t Key_Scan(void)
{
static uint8_t last_state = 1; // 假设上拉,默认高电平
uint8_t current_state;
uint8_t key_press = 0;
current_state = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0);
if (current_state == 0 && last_state == 1) // 检测到下降沿
{
Delay_ms(20); // 消抖延时
current_state = GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0);
if (current_state == 0) // 确认按下
{
key_press = 1;
}
}
last_state = current_state;
return key_press;
}
避坑指南:Delay_ms(20) 如果用的是SysTick延时,注意不要在中断里调用。我曾经在中断服务函数里直接调延时,结果系统卡死——因为SysTick中断优先级比当前中断低,永远等不到延时结束。嗯,这个坑我踩过,你们别踩。
3.3 外部中断(EXTI):让CPU不再“傻等”
轮询按键有个问题:CPU得一直跑循环去读引脚,干不了别的。用中断就不一样了——按键按下时,CPU自动跳去处理,处理完回来继续干正事。
STM32的EXTI(External Interrupt)支持最多16个外部中断线(EXTI0~EXTI15),每个中断线可以映射到多个GPIO引脚,但同一时间只能选一个。
3.3.1 EXTI配置步骤
- 使能GPIO时钟和AFIO时钟
- 配置GPIO为输入模式(上拉或浮空)
- 配置GPIO到EXTI的映射(GPIO_EXTILineConfig)
- 配置EXTI中断线(触发方式、使能)
- 配置NVIC中断优先级
- 编写中断服务函数
代码示例(基于标准库):
void EXTI_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
EXTI_InitTypeDef EXTI_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
// 1. 使能时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
// 2. 配置GPIO
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; // 上拉输入
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 3. 映射到EXTI0
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0);
// 4. 配置EXTI
EXTI_InitStructure.EXTI_Line = EXTI_Line0;
EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt;
EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; // 下降沿触发
EXTI_InitStructure.EXTI_LineCmd = ENABLE;
EXTI_Init(&EXTI_InitStructure);
// 5. 配置NVIC
NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
}
// 6. 中断服务函数
void EXTI0_IRQHandler(void)
{
if (EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 延时消抖(注意:这里不能用阻塞延时!)
// 建议用定时器或状态机处理
// 处理按键逻辑
EXTI_ClearITPendingBit(EXTI_Line0);
}
}
重要提醒:中断服务函数里不要做复杂操作,更不要用阻塞延时。我见过有人在中断里调printf,结果串口还没发完,下一个中断又来了——直接死机。中断里只做标志位设置,具体逻辑放主循环处理。
3.4 中断优先级管理:谁先谁后,得有个规矩
STM32的NVIC支持最多16级抢占优先级和16级子优先级(具体取决于芯片型号)。优先级管理说白了就是:当多个中断同时发生时,谁先执行。
NVIC优先级分组:
| 分组 | 抢占优先级位数 | 子优先级位数 | 说明 |
|---|---|---|---|
| NVIC_PriorityGroup_0 | 0 | 4 | 只有子优先级,无抢占 |
| NVIC_PriorityGroup_1 | 1 | 3 | 2级抢占,8级子优先级 |
| NVIC_PriorityGroup_2 | 2 | 2 | 4级抢占,4级子优先级 |
| NVIC_PriorityGroup_3 | 3 | 1 | 8级抢占,2级子优先级 |
| NVIC_PriorityGroup_4 | 4 | 0 | 16级抢占,无子优先级 |
我个人习惯:用NVIC_PriorityGroup_2,4级抢占够用,4级子优先级也灵活。按键中断一般给个中等优先级(比如抢占2,子优先级0),别太高也别太低。
经验之谈:中断优先级分配有个原则——实时性要求高的给高优先级,比如定时器、通信中断;按键这种可以稍微低一点。我曾经在一个项目里把按键中断优先级设得比系统滴答定时器还高,结果按键按下去,系统时钟乱了,整个调度都崩了。嗯,从那以后我再也不敢乱设优先级了。
3.5 本章知识体系
下面这张图,把按键输入与中断的整个知识结构串起来了。你可以把它当作一个快速索引,哪里不会看哪里。
这张图把本章的核心知识点都串起来了。从GPIO输入模式开始,到消抖处理,再到EXTI配置和优先级管理,每一步都有对应的子项。你可以把它当作一个快速索引,哪里不会看哪里。
本章小结:按键输入看似简单,但涉及的知识点不少。GPIO输入模式要选对,消抖要处理好,中断配置要规范,优先级要合理。把这些都搞定了,按键这块就算真正掌握了。