3、GPIO基础与LED指示:GPIO模式配置(推挽/开漏)、寄存器操作与HAL库对比、LED流水灯实验、按键输入检测与消抖
各位同学,欢迎来到实战第三讲。今天咱们聊聊GPIO——嵌入式开发里最基础、也最绕不开的东西。
说实话,我刚开始学单片机那会儿,觉得GPIO不就是个高低电平嘛,有啥好研究的?后来做项目多了才发现,这里面的坑真不少。推挽和开漏选错了,LED不亮;按键没消抖,程序乱跳。今天咱们就把这些事彻底捋清楚。
3.1 GPIO模式:推挽输出与开漏输出
先问大家一个问题:你让单片机输出高电平,它真的能输出5V吗?
答案取决于你配置的模式。GPIO输出模式主要有两种:推挽输出和开漏输出。
3.1.1 推挽输出(Push-Pull)
推挽输出,说白了就是内部有两个管子:一个负责推(输出高电平),一个负责挽(输出低电平)。输出高时,上管导通,直接怼到VCC;输出低时,下管导通,拉到GND。
这种模式的好处是:驱动能力强,速度快。我做过一个LED灯带项目,直接用推挽输出驱动,效果很好。
适用场景:驱动LED、蜂鸣器、继电器(加驱动电路)等需要较强电流的场合。
3.1.2 开漏输出(Open-Drain)
开漏输出就有点意思了。它只有下管,没有上管。输出低电平时,管子导通,拉到GND;输出高电平时,管子断开,引脚处于高阻态——说白了就是悬空。
那高电平怎么办?需要外部接一个上拉电阻。我见过不少新手在这里翻车:配置了开漏输出,忘了接上拉电阻,结果引脚电压飘忽不定。
注意:开漏输出必须外接上拉电阻,否则无法输出高电平!电阻值一般选4.7kΩ~10kΩ。
开漏最大的好处是:可以实现"线与"功能——多个开漏输出可以直接连在一起,任何一个拉低,总线就是低电平。I2C总线就是典型应用。
| 特性 | 推挽输出 | 开漏输出 |
|---|---|---|
| 输出高电平 | 直接输出VCC | 需外接上拉电阻 |
| 输出低电平 | 直接拉到GND | 直接拉到GND |
| 驱动能力 | 强 | 弱(受上拉电阻限制) |
| 线与功能 | 不支持 | 支持 |
| 典型应用 | LED、普通IO | I2C、电平转换 |
3.2 寄存器操作 vs HAL库:两种风格
做嵌入式开发,你迟早要面对这个问题:到底用寄存器还是HAL库?
我个人习惯是:学习阶段用寄存器,项目阶段用HAL库。为什么?寄存器让你理解底层原理,HAL库让你提高开发效率。
3.2.1 寄存器操作:直接、高效
以STM32F103为例,点亮一个LED,寄存器写法是这样的:
// 开启GPIOB时钟
RCC->APB2ENR |= (1 << 3);
// 配置PB0为推挽输出,50MHz
GPIOB->CRL &= ~(0x0F << 0); // 先清零
GPIOB->CRL |= (0x03 << 0); // 设置模式
// 输出高电平
GPIOB->ODR |= (1 << 0);
你看,每一步都很清楚:开时钟、配模式、写数据。但缺点也很明显——可读性差,容易写错位。我曾经在CRL寄存器配置上吃过亏,位偏移算错一位,折腾了半天。
3.2.2 HAL库:封装、易用
同样的功能,用HAL库写:
// 初始化结构体
GPIO_InitTypeDef GPIO_InitStruct = {0};
// 开启时钟
__HAL_RCC_GPIOB_CLK_ENABLE();
// 配置GPIO
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);
// 输出高电平
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_SET);
代码长了,但清晰多了。你不需要记寄存器地址,只需要填结构体。HAL库帮你处理了各种边界情况。
我的建议:初学阶段,两种写法都练练。寄存器让你懂原理,HAL库让你能干活。面试时很多公司会问寄存器操作,项目里却用HAL库——都得会。
3.3 LED流水灯实验
好了,理论说完了,咱们动手做个实验。流水灯,嵌入式界的"Hello World"。
硬件连接:4个LED分别接PB0~PB3,阳极通过电阻接VCC,阴极接GPIO(低电平点亮)。
代码实现:
#include "stm32f1xx_hal.h"
// LED引脚定义
#define LED1_PIN GPIO_PIN_0
#define LED2_PIN GPIO_PIN_1
#define LED3_PIN GPIO_PIN_2
#define LED4_PIN GPIO_PIN_3
#define LED_PORT GPIOB
// 延时函数(简单粗暴版)
void delay_ms(uint32_t ms) {
for(uint32_t i = 0; i < ms * 4000; i++) {
__NOP();
}
}
int main(void) {
HAL_Init();
__HAL_RCC_GPIOB_CLK_ENABLE();
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = LED1_PIN | LED2_PIN | LED3_PIN | LED4_PIN;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(LED_PORT, &GPIO_InitStruct);
// 初始全部熄灭
HAL_GPIO_WritePin(LED_PORT, LED1_PIN | LED2_PIN | LED3_PIN | LED4_PIN, GPIO_PIN_SET);
while(1) {
// 逐个点亮
HAL_GPIO_WritePin(LED_PORT, LED1_PIN, GPIO_PIN_RESET);
delay_ms(200);
HAL_GPIO_WritePin(LED_PORT, LED1_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(LED_PORT, LED2_PIN, GPIO_PIN_RESET);
delay_ms(200);
HAL_GPIO_WritePin(LED_PORT, LED2_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(LED_PORT, LED3_PIN, GPIO_PIN_RESET);
delay_ms(200);
HAL_GPIO_WritePin(LED_PORT, LED3_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(LED_PORT, LED4_PIN, GPIO_PIN_RESET);
delay_ms(200);
HAL_GPIO_WritePin(LED_PORT, LED4_PIN, GPIO_PIN_SET);
}
}
这段代码很简单,但有个问题:延时函数用的是空循环,CPU一直在跑,浪费资源。实际项目中我会用定时器中断来做延时,后面章节会讲到。
3.4 按键输入检测与消抖
按键检测,看着简单,其实是个技术活。为什么?因为按键有抖动。
你按一下按键,机械触点会弹跳几次,产生多个脉冲。如果不处理,程序会误判为按了好多次。
3.4.1 硬件消抖
最简单的办法:在按键两端并联一个电容(0.1μF左右),利用电容的充放电特性滤除抖动。我早期做的一个遥控器项目,就是用硬件消抖,效果不错。
3.4.2 软件消抖
更常用的方法是软件消抖。原理很简单:检测到电平变化后,延时10~20ms再读一次,确认电平是否稳定。
// 按键引脚定义
#define KEY_PIN GPIO_PIN_0
#define KEY_PORT GPIOA
// 按键扫描函数
// 返回值:0-未按下,1-按下
uint8_t Key_Scan(void) {
// 检测按键是否按下(低电平有效)
if(HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) == GPIO_PIN_RESET) {
// 延时消抖
HAL_Delay(20);
// 再次确认
if(HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) == GPIO_PIN_RESET) {
// 等待按键释放
while(HAL_GPIO_ReadPin(KEY_PORT, KEY_PIN) == GPIO_PIN_RESET);
return 1; // 有效按下
}
}
return 0; // 未按下
}
我曾经踩过的坑:消抖延时太短(比如5ms),按键还是会误触发。延时太长(比如50ms),按键响应迟钝。经验值:机械按键用10~20ms,薄膜按键用5~10ms。
另外要注意:上面的代码用了while等待按键释放,这会阻塞程序。实际项目中,我一般用状态机来做按键检测,不阻塞主循环。这个后面会专门讲。
3.5 小结
今天的内容就这些。总结几个要点:
- 推挽输出:驱动能力强,适合LED等普通输出
- 开漏输出:需要上拉电阻,适合I2C等总线
- 寄存器操作:底层、高效,适合理解原理
- HAL库:封装好、易移植,适合项目开发
- 按键消抖:延时10~20ms,别太短也别太长
下节课咱们讲定时器,到时候会教大家用定时器中断做精确延时,彻底告别空循环延时。各位回去把流水灯实验跑通,有问题群里问。