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都会轻松很多。