3、GPIO输入驱动:按键输入实验、机械按键消抖、外部中断EXTI、中断优先级管理
按键输入,是嵌入式开发里最基础、也最容易踩坑的环节。很多新手觉得按键不就是读个电平嘛,有什么难的?结果一上电,按键按下一次,灯闪了七八下——这就是典型的没做消抖。
我刚开始做项目时也吃过这个亏。那是一个工业控制面板,操作员按一次启动键,机器抖动了三次才启动。后来查了半天,发现就是按键消抖没处理好。从那以后,我对按键输入的处理就格外上心。
这一章,咱们就把GPIO输入驱动讲透。从最基础的按键电平读取,到机械按键消抖,再到外部中断EXTI,最后聊聊中断优先级管理。一步步来,别急。
3.1 按键输入的基本原理
按键的本质,就是一个机械开关。按下时导通,松开时断开。我们通过GPIO读取这个开关的电平状态,就能判断按键是否被按下。
但这里有个关键点:按键的电路连接方式。常见的有两种:
| 连接方式 | 电路结构 | 按下时电平 | 松开时电平 |
|---|---|---|---|
| 上拉输入 | 按键一端接GND,另一端接GPIO,GPIO内部上拉至VCC | 低电平(0) | 高电平(1) |
| 下拉输入 | 按键一端接VCC,另一端接GPIO,GPIO内部下拉至GND | 高电平(1) | 低电平(0) |
我个人习惯使用上拉输入。为什么?因为大多数MCU的GPIO默认就支持内部上拉,省一个外部电阻。而且GND比VCC更稳定,抗干扰能力更强。
3.2 机械按键消抖
机械按键有个天生的毛病——抖动。你按下一次,实际上触点在极短时间内会反复通断好几次,持续时间大约5~20ms。如果不处理,MCU会误判为按了多次。
消抖的方法主要有两种:
- 硬件消抖:在按键两端并联一个电容(通常0.1μF),利用电容的充放电特性滤除抖动。简单粗暴,但增加BOM成本。
- 软件消抖:检测到电平变化后,延时10~20ms再读一次。如果电平稳定,才确认按键状态。不花钱,但占用CPU时间。
我在项目中通常用软件消抖。原因很简单——省钱、灵活。而且RTOS环境下,可以用任务延时让出CPU,不影响其他任务。
下面是一个典型的软件消抖代码:
// 按键消抖函数
// 返回值:0-未按下,1-按下
uint8_t Key_Scan(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin)
{
if (HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET) // 第一次检测到按下
{
HAL_Delay(10); // 延时10ms,避开抖动区间
if (HAL_GPIO_ReadPin(GPIOx, GPIO_Pin) == GPIO_PIN_RESET) // 再次确认
{
return 1; // 确认按下
}
}
return 0; // 未按下或抖动误判
}
3.3 外部中断EXTI
轮询读按键太浪费CPU了。你想啊,CPU每秒几百万条指令,却要不停地去查一个按键有没有被按——这效率太低了。更好的方式是:让按键主动通知CPU。这就是外部中断EXTI干的事。
STM32的EXTI支持16个外部中断线(EXTI0~EXTI15),每个GPIO引脚都可以配置为中断源。配置流程大致如下:
- 使能GPIO时钟和SYSCFG时钟
- 配置GPIO为输入模式(上拉/下拉)
- 将GPIO引脚连接到EXTI线(通过SYSCFG)
- 配置EXTI触发方式(上升沿、下降沿、双边沿)
- 配置NVIC中断优先级并使能中断
- 编写中断服务函数
下面是一个完整的EXTI配置示例:
// 按键中断初始化(PA0,下降沿触发)
void Key_EXTI_Init(void)
{
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 1. 使能时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_SYSCFG_CLK_ENABLE();
// 2. 配置GPIO
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发
GPIO_InitStruct.Pull = GPIO_PULLUP; // 内部上拉
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 3. 配置NVIC中断优先级
HAL_NVIC_SetPriority(EXTI0_IRQn, 2, 0); // 抢占优先级2,子优先级0
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}
// 中断服务函数
void EXTI0_IRQHandler(void)
{
if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET)
{
// 这里放你的按键处理逻辑
// 注意:中断里不要做耗时操作!
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0); // 清除中断标志
}
}
3.4 中断优先级管理
当系统里有多个中断源时,谁先响应、谁可以打断谁,这就是中断优先级管理的事。STM32使用NVIC(嵌套向量中断控制器)来管理中断优先级。
STM32的中断优先级分为两部分:
- 抢占优先级:数值越小,优先级越高。高抢占优先级可以打断低抢占优先级的中断。
- 子优先级:当抢占优先级相同时,子优先级决定谁先执行。但子优先级不能互相打断。
优先级分组由SCB->AIRCR寄存器决定。STM32通常支持4位优先级,可以灵活分配抢占位和子位:
| 分组 | 抢占位 | 子位 | 说明 |
|---|---|---|---|
| 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级子优先级也够灵活。你想想看,一个典型的物联网项目里,中断优先级大概这么分配:
- 抢占0:系统时钟(SysTick)——最高,保证系统心跳
- 抢占1:通信中断(UART、SPI)——数据不能丢
- 抢占2:外部中断(按键、传感器)——响应要快
- 抢占3:普通外设(定时器、ADC)——优先级最低
3.5 知识体系总览
这一章的内容比较多,我画了一张结构图,帮你理清思路:
从这张图可以看得很清楚:GPIO输入驱动不是孤立的知识点。按键电平读取是基础,消抖是工程实践,EXTI是效率提升,中断优先级管理是系统整合。四者环环相扣,缺一不可。
嗯,这一章的内容就到这里。代码示例可以直接拿去用,但建议你动手改一改——比如把延时消抖改成定时器轮询,或者试试不同的优先级分组。只有亲手踩过坑,才能真正记住这些知识点。