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 配置表列清楚,每个引脚的模式、上下拉、初始电平都写明白。磨刀不误砍柴工,真的。