4、STM32 GPIO编程:GPIO模式配置、推挽输出与开漏输出、点亮LED、按键输入检测

好,咱们今天聊聊GPIO。说实话,GPIO是嵌入式开发里最基础、也最绕不开的东西。你想想看,一个单片机要跟外界打交道,靠的就是这些引脚。我刚开始学STM32的时候,觉得GPIO不就是个高低电平嘛,有啥好学的?结果后来做项目,被一个开漏输出折腾了一整天……嗯,从那以后我再也不敢小看GPIO了。

4.1 GPIO模式到底有几种?

STM32的GPIO模式,官方文档里列了一大堆。我帮你梳理一下,其实就四大类:

  • 输入模式:读取引脚电平。比如检测按键按下。
  • 输出模式:控制引脚输出高低电平。比如点亮LED。
  • 复用功能模式:引脚交给片上外设使用,比如USART的TX/RX。
  • 模拟模式:用于ADC采集,引脚直接连到模拟电路。

每种模式下面还有细分,比如输出模式又分推挽和开漏。别急,咱们一个一个说。

重点提醒:配置GPIO时,别忘了开启对应端口的时钟。我见过太多新手(包括当年的我)忘了开时钟,结果怎么配置都没反应。RCC_APB2PeriphClockCmd() 这个函数,一定要先调用。

4.2 推挽输出 vs 开漏输出

这两个概念,我当年学的时候也迷糊过。说白了:

  • 推挽输出:引脚能主动输出高电平,也能主动输出低电平。就像两个开关,一个推上去接VCC,一个拉下来接GND。输出高电平时,电流从芯片流出;输出低电平时,电流流入芯片。
  • 开漏输出:引脚只能主动输出低电平。想输出高电平?得靠外部上拉电阻。就像只有一个开关,只能拉到GND,高电平靠电阻拉上去。
特性 推挽输出 开漏输出
输出高电平 芯片内部驱动 需外部上拉电阻
输出低电平 芯片内部驱动 芯片内部驱动
驱动能力 弱(取决于上拉电阻)
多引脚线与 不支持 支持
电平转换 不支持 支持(如3.3V驱动5V)

我在项目中遇到过一个问题:用推挽输出驱动一个I2C总线,结果死活通信不上。后来才发现,I2C协议要求开漏输出,因为多个设备要共用一条线,推挽输出会打架。你想想看,一个设备想拉高,另一个想拉低,那不就短路了吗?

我的习惯:驱动LED、蜂鸣器这类简单负载,用推挽输出。涉及总线协议(I2C、SMBus)或者需要电平转换的场合,用开漏输出加外部上拉。

4.3 点亮LED——第一个实战

好,咱们动手写代码。点亮一个LED,说白了就是让GPIO输出高电平。我习惯用库函数,因为可读性好。当然,直接操作寄存器也行,但初学者还是先走库函数路线吧。

// 第一步:开启GPIO时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);

// 第二步:配置GPIO结构体
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;      // PB0
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; // 推挽输出
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; // 速度50MHz
GPIO_Init(GPIOB, &GPIO_InitStructure);

// 第三步:输出高电平,点亮LED
GPIO_SetBits(GPIOB, GPIO_Pin_0);

// 或者用这个也行
// GPIO_WriteBit(GPIOB, GPIO_Pin_0, Bit_SET);

这里有个细节:GPIO_Speed 到底设多少?我一般设50MHz,因为够用。但如果你驱动的是低频信号,设10MHz甚至2MHz也行,还能省点电。嗯,这里要注意,速度设高了,信号边沿会更陡,EMI也会大一些。不过点亮个LED,无所谓。

我曾经踩过的坑:LED的正极接VCC,负极接GPIO,然后配置推挽输出高电平——结果灯不亮。为什么?因为电流方向反了。LED需要电流从正极流向负极,GPIO输出高电平时,电流是从芯片流出的,所以LED应该正极接GPIO,负极接GND。或者用低电平驱动:正极接VCC,负极接GPIO,输出低电平。

4.4 按键输入检测

按键检测,比LED稍微复杂一点。核心问题是:机械按键有抖动。你按下去的时候,电平会跳来跳去,持续几毫秒到十几毫秒。如果不做消抖,一次按下可能被误判成好几次。

消抖有两种方法:

  • 硬件消抖:加RC滤波电路,或者用施密特触发器。
  • 软件消抖:检测到电平变化后,延时10-20ms再读一次。

我一般用软件消抖,因为省成本。来看代码:

// 配置按键引脚为输入模式
GPIO_InitTypeDef GPIO_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;      // PA0
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;   // 上拉输入
GPIO_Init(GPIOA, &GPIO_InitStructure);

// 按键检测函数
uint8_t Key_Scan(void)
{
    if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0) // 检测到按下
    {
        Delay_ms(20);  // 消抖延时
        if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0) // 再次确认
        {
            // 等待按键释放
            while(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_0) == 0);
            return 1;  // 返回按键按下
        }
    }
    return 0;
}

这里我用了上拉输入模式。为什么?因为按键通常一端接GND,另一端接GPIO。上拉输入模式下,没按下时引脚被拉到高电平,按下后被拉到低电平。这样检测低电平就是按下。

你可能会问:为什么不用下拉输入?也可以,但需要外部下拉电阻。STM32内部只有上拉电阻,没有下拉电阻(部分型号有,但大部分没有)。所以上拉输入是最省事的。

我的经验:延时消抖的20ms不是死数。有些按键质量差,抖动时间可能更长。我一般先设20ms,如果发现误触发,就加到30ms。另外,while等待释放的写法会阻塞程序,不适合复杂系统。实际项目中我一般用状态机或者定时器轮询。

4.5 综合一下:按键控制LED

把上面两个例子合起来,就是按键控制LED亮灭。这个例子虽然简单,但包含了输入和输出的完整流程。

int main(void)
{
    // 初始化LED和按键
    LED_Init();   // PB0 推挽输出
    Key_Init();   // PA0 上拉输入
    
    while(1)
    {
        if(Key_Scan() == 1)
        {
            // 翻转LED状态
            GPIO_WriteBit(GPIOB, GPIO_Pin_0, 
                (BitAction)(1 - GPIO_ReadOutputDataBit(GPIOB, GPIO_Pin_0)));
        }
    }
}

这里有个小技巧:用 GPIO_ReadOutputDataBit 读取当前输出状态,然后取反。这样每次按键按下,LED就翻转一次。比直接设置高或低更灵活。

好了,GPIO的基础就这些。说白了,就是配置模式、读写电平。但别小看它,我见过很多复杂的项目,底层都是这些基础操作堆起来的。你把这些搞透了,后面学定时器、中断、PWM都会轻松很多。