3、GPIO 驱动开发:GPIO 模式配置(推挽、开漏、上拉/下拉)、LED 与按键驱动实战、中断触发与消抖处理
GPIO 驱动,说白了就是芯片跟外界打招呼的最基本方式。我做了这么多年嵌入式,发现很多新手一上来就急着写逻辑,结果连灯都点不亮——问题往往出在最基础的 GPIO 模式配置上。今天咱们就把这块彻底捋清楚。
3.1 GPIO 模式配置:推挽、开漏、上拉/下拉
先说说推挽输出。这是最常用的模式,内部有两个 MOS 管,一个推电流出去,一个拉电流回来。输出高电平时,上管导通,下管关断;输出低电平时反过来。驱动能力强,适合直接驱动 LED 这种负载。
开漏输出就有点意思了。它只有下管,上管是断开的。输出低电平没问题,但输出高电平?嗯,它只能靠外部上拉电阻拉上去。我曾在 I2C 总线上吃过亏,忘了加上拉电阻,结果总线一直低电平,设备死活不响应。
核心区别一句话:推挽能主动输出高低电平,开漏只能主动拉低,拉高要靠外部电阻。
上拉和下拉电阻呢?说白了就是给引脚一个默认状态。上拉让引脚默认高电平,下拉默认低电平。我习惯在按键输入引脚上加内部上拉,这样按键没按下时电平是确定的,不会乱跳。
| 模式 | 典型应用 | 注意事项 |
|---|---|---|
| 推挽输出 | LED、普通信号输出 | 两个引脚不能直接短接 |
| 开漏输出 | I2C、多设备共享总线 | 必须外接上拉电阻 |
| 上拉输入 | 按键检测 | 内部电阻约 40kΩ |
| 下拉输入 | 低电平有效信号 | 功耗略高于上拉 |
3.2 LED 与按键驱动实战
LED 驱动其实很简单,但有个坑——电流限制。直接推挽输出接 LED,不加限流电阻,LED 很快就会烧掉。我一般串一个 330Ω 电阻,电流控制在 10mA 左右。
// LED 驱动示例(STM32)
void LED_Init(void) {
GPIO_InitTypeDef GPIO_InitStruct = {0};
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_5;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP; // 推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}
void LED_Toggle(void) {
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}
按键驱动就复杂一些。你想想看,机械按键按下时,触点会弹跳几次,产生多个脉冲。如果不处理,一次按键可能被误判成十几次。
我的经验:硬件消抖加 RC 滤波最省事,但成本敏感的项目只能用软件消抖。软件消抖的核心就是延时再采样,一般 10-20ms 就够了。
// 按键扫描(带软件消抖)
uint8_t Key_Scan(void) {
static uint8_t last_state = 1;
uint8_t current_state = HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin);
if (last_state == 1 && current_state == 0) {
// 检测到下降沿,开始消抖
HAL_Delay(20); // 延时 20ms
current_state = HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin);
if (current_state == 0) {
last_state = 0;
return 1; // 有效按键
}
}
last_state = current_state;
return 0;
}
3.3 中断触发与消抖处理
轮询按键太浪费 CPU 了,中断才是正经做法。但中断里直接做消抖?千万别。中断服务函数要快进快出,延时 20ms 会阻塞整个系统。
我常用的方案是:中断里只设置一个标志位,主循环里检测标志位再做消抖处理。或者用定时器中断,每 10ms 扫描一次按键状态,这样既省资源又可靠。
我曾经踩过的坑:在中断里直接调用 HAL_Delay(),结果系统卡死。因为 HAL_Delay 依赖 SysTick 中断,而 SysTick 中断优先级可能低于当前中断,导致死锁。切记:中断里别用延时函数!
// 中断服务函数(只设标志)
void EXTI0_IRQHandler(void) {
if (__HAL_GPIO_EXTI_GET_IT(KEY_Pin) != RESET) {
key_press_flag = 1; // 设置标志
__HAL_GPIO_EXTI_CLEAR_IT(KEY_Pin);
}
}
// 主循环处理
while (1) {
if (key_press_flag) {
key_press_flag = 0;
HAL_Delay(20); // 主循环里做消抖
if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == 0) {
// 有效按键,执行动作
LED_Toggle();
}
}
}
还有一种更优雅的方式——状态机消抖。用定时器每 5ms 采样一次,连续采样 4 次都相同才认为有效。这样既不用阻塞延时,又能滤掉毛刺。
总结一下:GPIO 驱动看似简单,但模式选错、消抖没做好,后面调试起来很痛苦。我建议你在项目初期就把 GPIO 配置表列清楚,每个引脚的模式、上下拉、初始电平都写明白。磨刀不误砍柴工,真的。