4、GPIO与按键驱动:从按下到唤醒的完整链路
好,咱们进入第四章。这一章我打算聊点实在的——GPIO和按键驱动。
你可能会想:「按键嘛,不就是读个电平高低?」嗯,表面上看确实如此。但实际项目中,一个按键能让你调试到怀疑人生。我当年做第一款智能手表时,就因为在按键消抖上偷了懒,结果用户一抬手,屏幕就乱跳。那叫一个尴尬。
今天咱们就把这块彻底讲透。从GPIO模式配置,到外部中断,再到两种消抖算法,最后撸一个按键唤醒功能。走起。
4.1 GPIO模式配置:别上来就瞎设
很多新手拿到开发板,第一件事就是把GPIO设成输入,然后开始读电平。其实这里有个坑——你选的模式决定了你的信号质量。
以STM32为例,GPIO输入模式主要有这么几种:
| 模式 | 特点 | 适用场景 |
|---|---|---|
| 浮空输入 | 高阻态,电平不确定 | 外部有明确驱动源 |
| 上拉输入 | 内部接VCC,默认高电平 | 按键接地(常用) |
| 下拉输入 | 内部接GND,默认低电平 | 按键接VCC |
| 模拟输入 | 关闭数字功能 | ADC采样 |
我个人习惯,按键驱动一律用上拉输入。为什么?因为按键另一端接地,按下时读到低电平,松开时靠内部上拉电阻拉高。省一个外部电阻,还省心。
关键配置代码(HAL库风格):
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE(); // 别忘了开时钟
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP; // 上拉
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
小提示:时钟使能是新手最容易忘的。我见过有人调了一下午,结果发现GPIO没反应——时钟没开。先查时钟,再查模式,这俩搞定,80%的问题就解决了。
4.2 外部中断实现:别让CPU一直傻等
按键检测有两种方式:轮询和中断。
轮询就是CPU一直问「你按了吗?你按了吗?」——这在低功耗设备上简直是灾难。智能手表电池就那么点,你让CPU一直跑,续航直接崩。
所以,咱们用外部中断。按键按下时,CPU从休眠中被唤醒,处理完再睡回去。这才是正经做法。
配置外部中断的步骤其实不复杂:
- 配置GPIO为中断模式(不是普通输入)
- 选择触发边沿(上升沿、下降沿、双边沿)
- 配置NVIC(嵌套向量中断控制器)优先级
- 编写中断服务函数
这里我重点说一下触发边沿的选择。按键按下时,电平从高变低,所以用下降沿触发最直接。但注意——如果你按下后手抖了一下,可能会触发多次。这就是消抖要解决的问题。
// 配置外部中断
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿触发
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 配置NVIC
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
注意:中断服务函数里不要做耗时操作!我见过有人直接在中断里跑延时消抖,结果整个系统卡死。中断里只该做一件事——设置标志位。具体处理放到主循环或任务里。
4.3 按键消抖算法:硬件消抖 vs 软件消抖
按键按下和松开的瞬间,机械触点会弹跳。这个弹跳时间大概5-20ms。如果不处理,一次按下会被识别成好几次。
消抖有两种思路:硬件消抖和软件消抖。
4.3.1 硬件消抖
说白了就是加个RC滤波电路。电阻和电容组成低通滤波器,把毛刺滤掉。优点是CPU不用管,缺点是占PCB面积、增加BOM成本。
在智能手表这种寸土寸金的地方,我一般不推荐硬件消抖。除非你对功耗有极致要求,连CPU那几ms的消抖时间都不想浪费。
4.3.2 软件消抖
这才是主流做法。核心思想就一句话:检测到电平变化后,等一段时间再确认一次。
最简单的实现:
// 简易消抖:检测到按下后延时10ms再读一次
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
HAL_Delay(10); // 延时10ms
if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) {
// 确认按下,执行操作
}
}
但说实话,HAL_Delay在中断里用会出问题。更好的做法是用状态机+定时器:
typedef enum {
KEY_IDLE,
KEY_PRESS_DETECTED,
KEY_PRESS_CONFIRMED,
KEY_RELEASE_DETECTED
} KeyState;
KeyState keyState = KEY_IDLE;
uint32_t lastTick = 0;
void Key_Scan(void) {
uint8_t level = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
switch (keyState) {
case KEY_IDLE:
if (level == 0) {
keyState = KEY_PRESS_DETECTED;
lastTick = HAL_GetTick();
}
break;
case KEY_PRESS_DETECTED:
if (HAL_GetTick() - lastTick > 20) { // 20ms消抖
if (level == 0) {
keyState = KEY_PRESS_CONFIRMED;
// 触发按键事件
} else {
keyState = KEY_IDLE; // 抖动,复位
}
}
break;
case KEY_PRESS_CONFIRMED:
if (level == 1) {
keyState = KEY_RELEASE_DETECTED;
lastTick = HAL_GetTick();
}
break;
case KEY_RELEASE_DETECTED:
if (HAL_GetTick() - lastTick > 20) {
if (level == 1) {
keyState = KEY_IDLE; // 完全释放
} else {
keyState = KEY_PRESS_CONFIRMED; // 抖动,回到按下状态
}
}
break;
}
}
核心思想:状态机消抖不阻塞CPU,适合在低功耗系统中使用。你想想看,如果每次消抖都用delay,那CPU在这10ms里啥也干不了,多浪费。
4.4 实战:实现一个按键唤醒功能
好,终于到实战环节了。咱们要实现的功能是:手表休眠时,按一下按键就唤醒屏幕。
整体思路:
- 系统进入休眠模式(Stop模式或Standby模式)
- 配置按键GPIO为外部中断唤醒源
- 按键按下产生中断,CPU从休眠中醒来
- 在中断中设置唤醒标志,主循环检测到标志后点亮屏幕
代码实现:
// 进入休眠
void System_Enter_Sleep(void) {
// 关闭不需要的外设时钟
__HAL_RCC_GPIOB_CLK_DISABLE();
__HAL_RCC_GPIOC_CLK_DISABLE();
// 配置唤醒引脚(PA0)
HAL_GPIO_DeInit(GPIOA, GPIO_PIN_0);
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
// 使能唤醒中断
HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI0_IRQn);
// 进入Stop模式
HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI);
// 醒来后重新配置系统时钟
SystemClock_Config();
}
// 中断服务函数
void EXTI0_IRQHandler(void) {
if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET) {
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
wakeup_flag = 1; // 设置唤醒标志
}
}
// 主循环
int main(void) {
HAL_Init();
SystemClock_Config();
while (1) {
if (wakeup_flag) {
wakeup_flag = 0;
Display_WakeUp(); // 点亮屏幕
}
// 如果一段时间无操作,进入休眠
if (no_operation_timeout > 5000) {
System_Enter_Sleep();
}
HAL_Delay(10);
}
}
避坑指南:我曾经在唤醒后忘了重新配置系统时钟,结果屏幕刷得巨慢,像幻灯片一样。记住——从Stop模式醒来后,HSE(外部高速晶振)是关闭的,必须手动重新初始化时钟。
另外,关于功耗,我多说一句。进入休眠前,记得把不用的GPIO也处理一下。浮空的GPIO会漏电,虽然单个引脚只有几微安,但几十个引脚加起来,对电池续航的影响就很明显了。
好了,这一章的内容就到这里。按键驱动看似简单,但做好了能省很多事。下一章咱们聊定时器,那个更有意思——你可以用定时器做PWM调光、做呼吸灯、做精确延时。到时候见。