3、GPIO基础与按键输入:学习GPIO模式配置,用按键控制LED开关,理解上拉/下拉电阻

好,咱们进入第三节课。这节课我打算聊聊GPIO,也就是通用输入输出口。这东西是单片机跟外界打交道最基础的方式,说白了就是芯片的「手脚」。

你想想看,单片机要控制一个LED亮灭,或者检测一个按键有没有被按下,靠的就是GPIO。这节课我们就用按键控制LED开关这个经典实验,把GPIO的模式配置、上拉下拉电阻这些概念一次性讲透。

3.1 GPIO到底是什么?

GPIO的全称是General Purpose Input Output,通用输入输出。每个GPIO引脚都可以独立配置成输入模式或者输出模式。

  • 输出模式:单片机主动往外送高电平(3.3V或5V)或低电平(0V),用来驱动LED、蜂鸣器这类外设。
  • 输入模式:单片机读取外部信号是高还是低,比如检测按键是否按下。

嗯,这里要注意一点:同一个引脚,同一时间只能干一件事。要么输入,要么输出,不能又当爹又当妈。我见过不少新手把引脚同时配成输入和输出,结果逻辑混乱,查了半天才发现问题。

3.2 GPIO的几种工作模式

现在的单片机(比如STM32)GPIO模式远不止输入输出这么简单。我列个表,你一看就明白:

模式 说明 典型用途
推挽输出 可以输出高电平和低电平,驱动能力强 驱动LED、继电器
开漏输出 只能输出低电平,高电平需要外部上拉 I2C总线、多路信号线与
浮空输入 引脚电平不确定,容易受干扰 基本不用,除非你知道在干什么
上拉输入 内部接一个电阻到VCC,默认高电平 按键检测(按下为低)
下拉输入 内部接一个电阻到GND,默认低电平 按键检测(按下为高)
模拟输入 不读数字电平,直接读电压值 ADC采集

我个人习惯,做按键输入时首选上拉输入模式。为什么?因为大多数按键电路都是按下接地,上拉输入正好匹配,而且省掉一个外部电阻。

3.3 上拉电阻与下拉电阻——别让引脚「飘着」

这里有个关键概念:上拉电阻和下拉电阻。说白了,就是给引脚一个「默认状态」。

你想想看,如果引脚什么都不接,它处于什么电平?答案是——不确定。可能受周围电磁干扰乱跳,读出来的值一会儿0一会儿1。这就是所谓的「浮空」状态。

上拉电阻:在引脚和VCC之间接一个电阻(通常4.7kΩ~10kΩ)。这样引脚默认就是高电平。当按键按下接地时,引脚被拉低,变成低电平。

下拉电阻:在引脚和GND之间接一个电阻。默认低电平,按键按下接VCC时变成高电平。

重要提醒: 千万不要让引脚悬空输入!我在项目中遇到过,一个同事把按键引脚配成浮空输入,结果按键没按下时,LED自己在那闪。查了半天,发现是旁边电机产生的电磁干扰。加上内部上拉后,问题立刻解决。

3.4 实战:用按键控制LED开关

好,理论讲完了,咱们直接上代码。这个实验的目标是:按下按键,LED亮;松开按键,LED灭。

硬件连接很简单:

  • LED正极接GPIOA Pin0(推挽输出)
  • LED负极串联一个220Ω电阻到GND
  • 按键一端接GPIOA Pin1(上拉输入)
  • 按键另一端接GND

下面是基于STM32的HAL库代码:

// 初始化GPIO
void GPIO_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    // 使能GPIOA时钟
    __HAL_RCC_GPIOA_CLK_ENABLE();

    // 配置PA0为推挽输出
    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);

    // 配置PA1为上拉输入
    GPIO_InitStruct.Pin = GPIO_PIN_1;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;        // 输入模式
    GPIO_InitStruct.Pull = GPIO_PULLUP;            // 内部上拉
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
}

// 主循环
int main(void)
{
    HAL_Init();
    GPIO_Init();

    while (1)
    {
        // 读取按键状态
        if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET)
        {
            // 按键按下(低电平),点亮LED
            HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);
        }
        else
        {
            // 按键松开(高电平),熄灭LED
            HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
        }
    }
}
小技巧: 如果你用的是51单片机或者Arduino,原理完全一样。只是寄存器操作方式不同。比如51单片机,配置P1.0为输入时,直接写 P1_0 = 1 就是内部上拉。嗯,不同平台API不同,但底层逻辑是相通的。

3.5 按键消抖——别忽略这个细节

你实际测试时会发现一个问题:有时候按一下按键,LED会闪好几下。这不是代码写错了,而是按键的机械抖动。

按键内部是金属弹片,按下时不会瞬间稳定接触,会在几毫秒内反复弹跳。单片机速度很快,可能在这几毫秒内读到了多次电平变化。

解决办法很简单:延时消抖。检测到按键按下后,等10~20ms再读一次,确认确实是按下状态。

if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET)
{
    // 延时10ms消抖
    HAL_Delay(10);
    // 再次确认
    if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_1) == GPIO_PIN_RESET)
    {
        // 这才是真正的按键按下
        HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_0);  // 翻转LED状态
    }
}
避坑指南: 我曾经在一个项目中,按键控制电机启停。没做消抖,结果按一次按键,电机启动又立刻停止,反复好几次。后来加上10ms延时消抖,问题解决。记住:机械按键必须消抖,除非你用的是触摸按键。

3.6 总结一下这节课的核心

  • GPIO有输入和输出两种基本方向,同一时间只能选一种。
  • 上拉/下拉电阻给引脚一个确定的默认电平,避免浮空。
  • 按键输入建议用内部上拉,省外部元件。
  • 机械按键必须做消抖处理,10~20ms延时足够。

下节课咱们会用到定时器,让LED实现呼吸灯效果。到时候你会发现,GPIO配合定时器,能玩出很多花样。今天就到这儿,动手试试吧!