3、GPIO基础与LED控制:GPIO模式配置、推挽输出、点亮LED作为状态指示
各位同学,今天我们聊点实在的。嵌入式系统里,GPIO 是最基础、最常用的外设。说白了,它就是芯片和外界打交道的「手脚」。你想想看,一个单片机要控制设备,总得有个输出口吧?要读取传感器状态,总得有个输入口吧?这些就是 GPIO 干的活。
我个人习惯,拿到一个新板子,第一件事就是先点亮一个 LED。为什么?因为这是验证硬件、软件、调试链路是否正常的最快方法。灯亮了,说明你的代码跑起来了,GPIO 配置对了,硬件也没问题。灯不亮?那问题就大了,得从头排查。
3.1 GPIO 模式配置:别小看这一步
很多新手容易犯一个错误:以为 GPIO 就是简单的「输出高电平」或「输入低电平」。其实没那么简单。GPIO 的模式配置,直接决定了你的电路能不能正常工作。
常见的 GPIO 模式有这些:
| 模式 | 说明 | 典型应用 |
|---|---|---|
| 推挽输出 | 可以输出高电平或低电平,驱动能力强 | 驱动 LED、继电器、蜂鸣器 |
| 开漏输出 | 只能输出低电平,高电平需要外部上拉 | I2C 总线、多设备共享线路 |
| 浮空输入 | 输入阻抗高,电平不确定 | 按键检测(需外部上拉/下拉) |
| 上拉输入 | 内部上拉电阻,默认高电平 | 按键检测(节省外部电阻) |
| 下拉输入 | 内部下拉电阻,默认低电平 | 按键检测(节省外部电阻) |
| 模拟模式 | 关闭数字功能,用于 ADC 输入 | 采集模拟信号 |
嗯,这里要注意:不是所有引脚都支持所有模式。我在项目中遇到过,有人把 ADC 引脚配置成推挽输出,结果采集到的电压值一直不对。查了半天才发现,引脚模式没改回来。
3.2 推挽输出:最常用的输出模式
推挽输出,名字听起来挺唬人,其实原理很简单。它内部有两个 MOS 管:一个负责「推」(输出高电平),一个负责「挽」(输出低电平)。两个管子交替工作,所以叫推挽。
为什么推荐用推挽输出驱动 LED?因为它的驱动能力强。一般单片机的推挽输出能提供 5-20mA 的电流,驱动一个普通 LED 绰绰有余。你想想看,如果换成开漏输出,还得外加一个上拉电阻,多麻烦。
我曾经在一个项目中,用推挽输出直接驱动了一个小继电器。结果发现继电器吸合时,芯片电压被拉低,导致系统复位。后来一查,继电器线圈电流需要 50mA,远超 GPIO 的驱动能力。所以,推挽输出虽然强,但也不是万能的。驱动大负载时,一定要加三极管或 MOS 管做驱动。
3.3 点亮 LED:从原理到代码
好了,理论说完了,咱们动手写代码。点亮一个 LED,其实就三步:
- 使能 GPIO 时钟:别笑,很多人忘了这一步。STM32 这类芯片,外设的时钟默认是关闭的,你得先打开它。
- 配置 GPIO 模式:设置成推挽输出,速度选个合适的值(一般 10MHz 就够)。
- 设置输出电平:写 1 亮灯,还是写 0 亮灯?这取决于你的电路设计。
来看一个具体的例子。假设我们用 STM32F103,LED 接在 PA0 引脚,高电平点亮:
// 1. 使能 GPIOA 时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
// 2. 配置 PA0 为推挽输出,速度 10MHz
GPIOA->CRL &= ~(0x0F << 0); // 先清零
GPIOA->CRL |= (0x01 << 0); // 输出模式,速度 10MHz
// 3. 点亮 LED
GPIOA->BSRR = GPIO_BSRR_BS0; // 设置 PA0 为高电平
嗯,这里要注意:BSRR 寄存器是「只写」的。你写 1 到对应的位,引脚就输出高电平;写 1 到 BR 位,引脚就输出低电平。用 BSRR 的好处是,不需要关中断,也不会影响其他引脚的状态。
如果你用的是 HAL 库,代码会更简洁:
// 使能时钟
__HAL_RCC_GPIOA_CLK_ENABLE();
// 配置 GPIO
GPIO_InitTypeDef GPIO_InitStruct = {0};
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);
// 点亮 LED
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET);
3.4 避坑指南:我踩过的那些坑
做嵌入式这么多年,在 GPIO 上栽过的跟头不少。分享几个典型的,你们遇到了能少走弯路。
- 引脚复用冲突:我曾经在一个项目里,把 PA9 和 PA10 配置成普通 GPIO,结果串口死活不通。后来才发现,这两个引脚默认是 USART1 的 TX 和 RX。配置 GPIO 前,一定要先看芯片手册的「引脚功能表」。
- 电平不匹配:有一次我用 3.3V 的单片机去驱动一个 5V 的 LED 灯板。推挽输出高电平时,灯板不亮。后来一测,灯板需要 5V 才能正常工作。解决办法是加一个电平转换电路,或者用三极管驱动。
- 电流过大烧引脚:这个我印象最深。刚入行时,我直接用 GPIO 驱动了一个小电机,结果芯片冒烟了。后来才知道,电机启动电流能到几百毫安,而 GPIO 最多只能承受 20mA。从那以后,我驱动任何负载前,都会先算一下电流。
3.5 实战:用 LED 做状态指示
好了,回到我们的主题——剃须刀防水检测。LED 在这里的作用是什么?就是状态指示。比如:
- 检测通过:绿灯亮
- 检测失败:红灯亮
- 正在检测:黄灯闪烁
你看,三个 LED,三种状态,一目了然。这就是 GPIO 控制 LED 的典型应用。
我个人习惯,在写状态指示代码时,会先定义一个枚举类型:
typedef enum {
STATUS_PASS = 0, // 通过
STATUS_FAIL, // 失败
STATUS_TESTING, // 检测中
STATUS_IDLE // 空闲
} TestStatus_t;
然后写一个更新 LED 状态的函数:
void UpdateStatusLED(TestStatus_t status) {
switch(status) {
case STATUS_PASS:
HAL_GPIO_WritePin(GREEN_LED_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(RED_LED_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(YELLOW_LED_PIN, GPIO_PIN_RESET);
break;
case STATUS_FAIL:
HAL_GPIO_WritePin(GREEN_LED_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(RED_LED_PIN, GPIO_PIN_SET);
HAL_GPIO_WritePin(YELLOW_LED_PIN, GPIO_PIN_RESET);
break;
case STATUS_TESTING:
// 黄灯闪烁,用定时器控制
HAL_GPIO_TogglePin(YELLOW_LED_PIN);
break;
default:
// 全部熄灭
HAL_GPIO_WritePin(GREEN_LED_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(RED_LED_PIN, GPIO_PIN_RESET);
HAL_GPIO_WritePin(YELLOW_LED_PIN, GPIO_PIN_RESET);
break;
}
}
嗯,这里要注意:闪烁不能用 delay 实现。delay 会阻塞整个系统,导致其他任务无法执行。正确的做法是用定时器,每隔一定时间翻转一次引脚。这个我们后面会详细讲。
好了,这一节的内容就到这里。GPIO 看似简单,但用好它,能让你的嵌入式系统更稳定、更可靠。下一节,我们会讲定时器,到时候就能实现 LED 的闪烁效果了。