4. GPIO控制:从模式配置到LED实战
GPIO,说白了就是芯片跟外界打招呼的手脚。我刚开始做嵌入式那会儿,觉得GPIO不就是个高低电平嘛,有啥好研究的?直到有一次,一个开漏输出没配好,整个I²C总线卡死,排查了整整两天……嗯,从那以后我再也不敢小看这几个引脚了。
今天咱们就把GPIO的几种模式、推挽与开漏的区别、上下拉电阻怎么选,以及怎么用Rust的HAL库点亮一个LED,一次性讲透。
4.1 GPIO模式:输入、输出、复用
一个GPIO引脚,在不同场景下扮演不同角色。你可以把它想象成一个多功能的工具箱——关键是要选对工具头。
| 模式 | 典型用途 | 内部电路状态 |
|---|---|---|
| 输入模式 | 读取按键、检测电平 | 高阻态,引脚电平由外部驱动 |
| 输出模式 | 驱动LED、控制继电器 | 推挽或开漏,由寄存器驱动 |
| 复用功能 | UART、SPI、I²C等外设 | 引脚交给片上外设控制 |
我个人习惯,在初始化阶段就把每个引脚的模式想清楚。别偷懒——我见过太多人把本该配成复用的引脚配成输出,结果外设死活不工作。
核心原则:一个引脚同一时间只能处于一种模式。切换模式前,务必确保当前外设已经释放对该引脚的控制。
4.2 推挽输出 vs 开漏输出
这两个概念,很多初学者容易搞混。我简单解释一下:
- 推挽输出(Push-Pull):输出高电平时,内部PMOS导通,直接拉到VCC;输出低电平时,内部NMOS导通,直接拉到GND。说白了,就是既能主动推高,也能主动拉低。
- 开漏输出(Open-Drain):只有拉低的能力。输出高电平时,引脚处于高阻态,必须靠外部上拉电阻把电平拉高。
你想想看,为什么要有开漏这种“半残”模式?因为多个开漏输出可以接在同一条线上——谁拉低谁说话,这就是I²C总线的基础。
| 特性 | 推挽输出 | 开漏输出 |
|---|---|---|
| 驱动能力 | 强,可直接驱动LED | 弱,需外部上拉 |
| 电平转换 | 不支持 | 可通过上拉电阻实现 |
| 多设备共享 | 不能(会短路) | 可以(线与逻辑) |
| 典型应用 | 普通GPIO、SPI | I²C、中断信号 |
我曾经踩过的坑:把I²C的SCL和SDA配成了推挽输出。结果两个设备同时输出不同电平,直接短路,芯片发烫。记住:总线协议要求开漏,就别自作聪明。
4.3 上拉与下拉电阻
输入模式下,引脚如果悬空,电平是不确定的。这时候就需要上拉或下拉电阻来给一个确定的默认状态。
- 上拉电阻:默认高电平,外部拉低时读取到低电平。适合按键检测(按下为低)。
- 下拉电阻:默认低电平,外部拉高时读取到高电平。适合某些传感器输出高有效的场景。
现代MCU内部一般都集成了可编程的上拉/下拉电阻,阻值通常在30kΩ~50kΩ之间。我个人习惯,能用内部电阻就不用外部的——省一个电阻是一分钱,量产时你就知道这有多重要了。
小技巧:如果外部已经有上拉电阻(比如I²C总线),就别再使能内部上拉了。两个电阻并联,等效阻值会变小,功耗增加,信号质量反而下降。
4.4 使用HAL库控制LED
好了,理论说完了,咱们来点实际的。下面是用Rust的embassy-stm32 HAL库点亮一个LED的完整代码。
假设LED接在PA5引脚,高电平点亮。
use embassy_stm32::gpio::{Level, Output, Pin, Speed};
use embassy_stm32::Peripherals;
#[embassy_executor::main]
async fn main(_spawner: embassy_executor::Spawner) {
let p = Peripherals::take();
// 配置PA5为推挽输出,初始低电平
let mut led = Output::new(
p.PA5,
Level::Low,
Speed::VeryHigh,
);
// 点亮LED
led.set_high();
// 或者用toggle翻转
// led.toggle();
loop {
// 你的主循环
}
}
这段代码看起来简单,但有几个细节值得注意:
Level::Low是初始状态。我建议LED初始化为灭,避免上电瞬间闪一下。Speed::VeryHigh设置的是IO口的翻转速率。驱动LED用中速就够了,高速会增加功耗和噪声。set_high()和set_low()是HAL封装好的方法,内部操作的是BSRR寄存器——比直接写ODR更安全,不会影响其他引脚。
性能提示:如果你需要频繁翻转GPIO(比如软件PWM),直接用toggle()方法。它内部用的是ODR寄存器的异或操作,比先读再写快一个数量级。
4.5 知识结构总览
下面这张图,把GPIO配置的整个决策流程串起来了。我建议你把它存下来,以后配引脚时照着走一遍,基本不会出错。
这张图的逻辑其实很简单:先确定你要用这个引脚干什么,然后根据输入还是输出走不同的分支。复用功能本质上也是一种“输出交给外设”的模式,所以归在输出分支里。
4.6 实战:按键控制LED
光会点亮LED还不够,咱们来个完整的输入输出配合——用按键控制LED亮灭。
use embassy_stm32::gpio::{Input, Level, Output, Pull, Speed};
use embassy_stm32::Peripherals;
#[embassy_executor::main]
async fn main(_spawner: embassy_executor::Spawner) {
let p = Peripherals::take();
// PA5: 推挽输出,驱动LED
let mut led = Output::new(
p.PA5,
Level::Low,
Speed::Medium,
);
// PC13: 输入模式,内部上拉,读取按键
// 按键按下时,引脚被拉低
let button = Input::new(
p.PC13,
Pull::Up,
);
loop {
if button.is_low() {
led.set_high(); // 按键按下,点亮
} else {
led.set_low(); // 松开,熄灭
}
}
}
这里有个细节:按键用了内部上拉,所以默认是高电平。按下时引脚被拉到GND,读取到低电平。这种接法很常见,省一个外部电阻。
注意:实际项目中,按键需要做消抖处理。上面的代码只是为了演示GPIO的基本用法。消抖可以用硬件(RC滤波)或软件(延时去抖),咱们在后面的章节会专门讲。
好了,GPIO这部分就到这里。记住:模式选对,上下拉配好,输出类型搞清楚,你的引脚就不会出幺蛾子。下次咱们聊定时器,那个坑更多……到时候我慢慢给你讲。