3、GPIO基础与LED驱动:GPIO模式配置(推挽、开漏)、点亮板载LED、实现呼吸灯效果、按键输入检测与消抖

各位同学,欢迎来到第三章。

这一章我们开始真正动手了。GPIO,说白了就是芯片的“手脚”。你要让芯片对外输出信号,或者读取外部电平,都得靠它。我当年刚入行时,觉得GPIO太简单了,不就是置1清0嘛。结果第一个项目就栽在开漏输出上,上拉电阻没加,通讯死活不通。嗯,从那以后我再也不敢小看GPIO了。

3.1 GPIO模式:推挽与开漏

先搞清楚两个最常用的输出模式。你想想看,芯片内部其实就是一个开关电路。

推挽输出(Push-Pull)

推挽输出,内部有两个MOS管。一个负责“推”(输出高电平),一个负责“挽”(输出低电平)。输出高电平时,上管导通,下管截止;输出低电平时,上管截止,下管导通。

特点:

  • 输出高低电平都有驱动能力
  • 输出速度快
  • 不能“线与”(多个输出直接连一起会打架)

我个人的习惯是,驱动LED、蜂鸣器这类负载,直接用推挽输出。简单粗暴,效率高。

开漏输出(Open-Drain)

开漏输出,只有下管。输出低电平时,下管导通;输出高电平时,下管截止,引脚处于高阻态。所以,要输出高电平,必须在外部加上拉电阻。

特点:

  • 可以“线与”(多个开漏输出直接连一起,实现“与”逻辑)
  • 电平转换方便(上拉到不同电压)
  • 输出高电平靠外部上拉,速度较慢

核心区别:推挽输出能主动输出高低电平;开漏输出只能主动输出低电平,高电平靠外部上拉。

我在项目中遇到过I2C总线,必须用开漏输出。当时有个同事用了推挽,结果多个设备一通信,总线直接短路。所以,总线类接口,老老实实用开漏。

3.2 点亮板载LED

好,理论讲完,咱们来点实际的。板载LED通常接在GPIO上,低电平点亮或高电平点亮,看原理图。

假设LED接在PA0,低电平点亮。配置步骤如下:

  1. 使能GPIOA时钟
  2. 配置PA0为推挽输出
  3. 设置PA0输出低电平
// 以STM32 HAL库为例
void LED_Init(void)
{
    __HAL_RCC_GPIOA_CLK_ENABLE();   // 使能时钟,这一步容易忘

    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = GPIO_PIN_0;
    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);

    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET); // 点亮
}

小技巧:我习惯把LED操作封装成宏,比如 #define LED_ON() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET),这样代码读起来像自然语言。

3.3 实现呼吸灯效果

呼吸灯,说白了就是让LED亮度渐变。怎么实现?用PWM(脉冲宽度调制)。

PWM的原理很简单:快速开关LED,通过调节占空比(高电平时间比例)来控制亮度。人眼有视觉暂留效应,只要频率够高(通常100Hz以上),就看不到闪烁,只看到亮度变化。

实现步骤:

  1. 配置定时器输出PWM(比如TIM2的CH1)
  2. 设置PWM频率(比如1kHz)
  3. 在主循环中逐渐改变占空比
// 呼吸灯主循环
void BreathingLight(void)
{
    for(int i = 0; i < 1000; i++)  // 渐亮
    {
        __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, i);
        HAL_Delay(1);
    }
    for(int i = 1000; i > 0; i--)  // 渐暗
    {
        __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, i);
        HAL_Delay(1);
    }
}

注意:呼吸效果要平滑,占空比变化步长要小,延时不能太长。我曾经为了省事,步长设了100,结果LED像在“跳霹雳舞”,一点不优雅。

3.4 按键输入检测与消抖

按键,最常用的输入设备。但机械按键有个毛病:按下和松开时,触点会弹跳,产生多个电平跳变。如果不处理,一次按下可能被误判成多次。

硬件消抖

最简单的方法:在按键两端并联一个电容(0.1μF左右),利用电容的充放电来平滑波形。我一般会在按键PCB布局时,把电容紧挨着按键放。

软件消抖

更灵活的方法。核心思想:检测到电平变化后,延时一段时间(通常10-20ms),再读取一次,确认电平是否稳定。

// 按键扫描函数,返回1表示按下
uint8_t Key_Scan(void)
{
    static uint8_t last_state = 1;  // 上次状态,默认高电平(未按下)
    uint8_t current_state;

    current_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0); // 读取当前电平

    if(last_state == 1 && current_state == 0) // 检测下降沿
    {
        HAL_Delay(20);  // 延时消抖
        current_state = HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_0);
        if(current_state == 0)  // 再次确认按下
        {
            last_state = 0;
            return 1;
        }
    }
    else if(last_state == 0 && current_state == 1) // 检测上升沿(松开)
    {
        last_state = 1;
    }

    return 0;
}

避坑指南:我曾经在消抖延时里用了 HAL_Delay(20),结果整个系统响应变慢。后来改用定时器轮询,每10ms扫描一次按键,既消抖又不阻塞主循环。记住,延时函数在实时系统中要慎用。

3.5 综合练习:按键控制呼吸灯

把上面两个知识点结合起来:按一下按键,呼吸灯开始;再按一下,呼吸灯停止。

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    LED_Init();
    Key_Init();
    PWM_Init();

    uint8_t breathing_enable = 0;

    while(1)
    {
        if(Key_Scan())  // 检测到按键按下
        {
            breathing_enable = !breathing_enable;  // 切换状态
        }

        if(breathing_enable)
        {
            BreathingLight();  // 执行呼吸效果
        }
        else
        {
            HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); // 熄灭LED
        }
    }
}

这个例子虽然简单,但包含了GPIO输入输出、PWM、状态机的基本思想。你想想看,很多复杂系统,底层无非就是这些东西的组合。

好了,这一章就到这里。下一章我们开始讲中断,那才是真正让MCU“活”起来的关键。到时候见。