3、GPIO基础与按键输入:学习STM32 GPIO的推挽输出与上拉输入模式,用按键控制LED,理解电平与消抖

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

上一章我们点亮了LED,算是跟STM32打了个招呼。但那个LED是自己亮的,有点「自嗨」的感觉。这一章,我们要让LED听我们的话——按一下按键,它就亮;再按一下,它就灭。说白了,就是让芯片感知外部世界,再做出反应。

嗯,这里涉及两个核心概念:GPIO的输入模式按键消抖。我当年刚学的时候,就栽在消抖上——按键按下去,LED乱跳,我还以为是芯片坏了。后来才明白,不是芯片的问题,是物理世界的「抖动」在捣鬼。

3.1 GPIO的推挽输出模式(复习与深化)

上一章我们用了推挽输出。这里再啰嗦两句,因为后面要跟输入配合。

推挽输出,说白了就是芯片内部有两个管子:一个负责「推」(输出高电平),一个负责「挽」(输出低电平)。你让GPIO输出1,它就推上去,直接怼到VCC;你让输出0,它就拉下来,接到GND。所以推挽输出的驱动能力很强,可以直接驱动LED、蜂鸣器这些小负载。

我个人习惯,凡是驱动外部器件,优先用推挽输出。但要注意一点:推挽输出不能把两个引脚直接短接,否则一个输出1一个输出0,那就是短路,芯片会发热甚至烧掉。我在项目中见过有人这么干,结果芯片冒烟了……嗯,大家引以为戒。

3.2 GPIO的上拉输入模式

按键怎么接?最简单的办法:一端接GND,另一端接GPIO引脚。然后GPIO内部配置成上拉输入

什么叫上拉输入?就是芯片内部有一个电阻(通常是40kΩ左右),把引脚默认拉到高电平(3.3V)。当按键没按下时,引脚读到的是高电平(1)。当按键按下时,引脚被直接接到GND,读到的是低电平(0)。

你想想看,如果没有这个上拉电阻,按键没按下时引脚就是悬空的,电平不确定,芯片会读到乱七八糟的值。所以,上拉输入是处理按键的标准姿势

关键点:

  • 上拉输入:默认高电平,按键按下变低电平
  • 下拉输入:默认低电平,按键按下变高电平(不常用,因为外部需要接VCC)
  • 浮空输入:电平不确定,千万别用在这种场景

配置代码很简单,在HAL库中这样写:

// 假设按键接在PA0引脚
GPIO_InitTypeDef GPIO_InitStruct = {0};

__HAL_RCC_GPIOA_CLK_ENABLE();   // 使能GPIOA时钟

GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;      // 输入模式
GPIO_InitStruct.Pull = GPIO_PULLUP;          // 上拉
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

注意那个 GPIO_PULLUP,就是告诉芯片:「我要用内部上拉电阻」。如果你用的是外部上拉,这里可以设成 GPIO_NOPULL。但我建议新手直接用内部上拉,省一个电阻。

3.3 按键消抖——为什么需要?

好,现在我们把按键接好了,代码也写了。然后你按下按键,想控制LED翻转。结果发现:按一次,LED有时候亮,有时候不亮,有时候还闪两下。

为什么会这样?

因为按键是机械结构。你按下去的那一瞬间,金属触点不是「啪」一下就稳定接触的,它会弹跳几次——就像你扔一个球到地上,它会弹几下才停住。这个过程叫抖动,持续时间大概5~20ms。

在这段时间里,芯片读到的是:0、1、0、1、0、1……你以为按了一次,芯片以为你按了十几次。所以LED就乱跳了。

警告:千万不要忽略消抖!我曾经在一个产品中偷懒没做消抖,结果用户反馈按键「反应迟钝」「有时没反应」。其实不是反应迟钝,是抖动导致误判。后来被客户骂了一顿,老老实实加了消抖代码。

3.4 消抖的两种常用方法

消抖的本质就一句话:等抖动过去,再读电平

方法一:软件延时消抖(最简单)

检测到按键按下(电平变低),先等20ms,再读一次。如果还是低,说明真的按下了。否则认为是抖动,忽略。

if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) // 检测到按下
{
    HAL_Delay(20);  // 等待20ms,避开抖动
    if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET) // 再次确认
    {
        // 确实是按键按下,执行操作
        HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0); // 翻转LED
    }
}

这个方法简单粗暴,适合初学者。但有个缺点:HAL_Delay(20) 会阻塞CPU,如果系统里还有其他任务,这20ms就浪费了。不过对于咱们这个空气监测项目,20ms完全能接受。

方法二:状态机消抖(更专业)

如果你不想阻塞,可以用状态机。原理是:不断读取按键状态,连续读到N次相同的值,才认为状态稳定了。这个方法不阻塞,但代码稍微复杂一点。

// 简单的状态机消抖示例
uint8_t key_state = 1;  // 假设默认高电平(未按下)
uint8_t key_debounced = 1;
uint8_t counter = 0;

void Key_Scan(void)
{
    uint8_t current = HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
    
    if (current == key_state)
    {
        counter = 0;  // 状态没变,计数器清零
    }
    else
    {
        counter++;
        if (counter >= 5)  // 连续5次(约5ms)都不同,认为状态变化
        {
            key_state = current;
            key_debounced = current;
            counter = 0;
        }
    }
}

我个人习惯,在简单项目里用延时消抖,在RTOS或者复杂项目里用状态机。没有绝对的好坏,看场景。

3.5 完整示例:按键控制LED

把上面所有东西串起来,写一个完整的按键控制LED程序。功能:按一下按键,LED状态翻转。

// main.c 核心代码
int main(void)
{
    HAL_Init();
    SystemClock_Config();  // 配置系统时钟
    
    // 初始化LED引脚(PB0,推挽输出)
    __HAL_RCC_GPIOB_CLK_ENABLE();
    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(GPIOB, &GPIO_InitStruct);
    
    // 初始化按键引脚(PA0,上拉输入)
    __HAL_RCC_GPIOA_CLK_ENABLE();
    GPIO_InitStruct.Pin = GPIO_PIN_0;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    while (1)
    {
        // 检测按键按下(低电平有效)
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
        {
            HAL_Delay(20);  // 消抖
            if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
            {
                // 翻转LED
                HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_0);
                
                // 等待按键释放,防止一次按下触发多次
                while (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET);
            }
        }
    }
}

小提示:代码最后那个 while 循环是等待按键释放。如果不加,你按住按键不放,LED会一直翻转,看起来就像在闪烁。加了之后,只有松开再按下才会触发下一次。这个细节很多新手会忽略。

3.6 电平逻辑总结

这一章的核心就是理解电平。我画个表,大家一目了然:

按键状态 引脚电平 读取值 说明
未按下 高电平(3.3V) 1(GPIO_PIN_SET) 内部上拉电阻拉高
按下(抖动中) 高低跳变 不稳定 需要消抖处理
按下(稳定) 低电平(0V) 0(GPIO_PIN_RESET) 按键接通GND

记住这个逻辑,后面我们做空气颗粒物传感器读取时,本质上也是在读电平——只不过传感器输出的不是简单的0或1,而是PWM波或者串口数据。但基础都是GPIO。

3.7 避坑指南

最后,分享几个我踩过的坑:

  • 忘记使能时钟:GPIO不工作,90%是时钟没开。我刚开始学的时候,至少在这上面浪费了2小时。
  • 上拉/下拉搞反:按键接GND就用上拉,接VCC就用下拉。别搞混,否则逻辑是反的。
  • 消抖时间太短:有些劣质按键抖动时间长达30ms,你只等5ms可能不够。建议至少10~20ms。
  • 没有等待按键释放:这个前面说过了,会导致一次按下触发多次。

好了,这一章就到这里。下一章我们会用定时器来做更精确的消抖,顺便学学定时器的基本用法。先把这一章的代码跑通,按键控制LED成功之后,你就算正式入门STM32的输入输出了。

加油,动手试试看。