GPIO基础与LED控制:用embedded-hal点亮第一盏灯
做嵌入式开发,GPIO 是最基础也最绕不开的外设。说白了,它就是芯片伸出来的那些引脚,你可以让它们输出高电平或低电平,也可以读取外部信号是 0 还是 1。今天我们就从 GPIO 入手,用 Rust 和 embedded-hal 来操作 LED,先让它闪烁,再玩出呼吸灯的效果。
1. embedded-hal 的 GPIO 抽象
embedded-hal 是 Rust 嵌入式生态里的硬件抽象层。它定义了一组 trait,比如 OutputPin、InputPin、PwmPin。不同的芯片厂商(STM32、nRF、ESP32 等)只要实现了这些 trait,你的代码就能跨平台跑。
我个人习惯把 GPIO 操作分成三层来看:
| 层次 | 说明 | 示例 |
|---|---|---|
| 硬件层 | 芯片寄存器,控制引脚模式、上下拉、速度 | GPIOx_MODER, GPIOx_ODR |
| HAL 层 | embedded-hal trait 的具体实现 | stm32f4xx_hal::gpio::PB5 |
| 应用层 | 你的业务逻辑,只调用 trait 方法 | pin.set_high() / pin.set_low() |
你想想看,如果直接操作寄存器,换个芯片就得重写。但用 embedded-hal,应用层代码几乎不用改。我在项目中遇到过好几次芯片换型,只要 HAL 实现没变,LED 闪烁的代码一行都没动。
2. 点亮 LED:从闪烁开始
先来一个最简单的例子。假设我们有一个 STM32F4 开发板,LED 接在 PB5 上,低电平点亮。
use embedded_hal::digital::v2::OutputPin;
use stm32f4xx_hal::{
pac,
prelude::*,
gpio::GpioExt,
};
fn main() -> ! {
let dp = pac::Peripherals::take().unwrap();
let rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.freeze();
let gpiob = dp.GPIOB.split();
let mut led = gpiob.pb5.into_push_pull_output();
loop {
led.set_low().unwrap(); // 点亮
cortex_m::asm::delay(8_000_000);
led.set_high().unwrap(); // 熄灭
cortex_m::asm::delay(8_000_000);
}
}
嗯,这里要注意:set_low() 点亮是因为硬件上 LED 的阴极接 GPIO,阳极接 VCC。如果你的板子是高电平点亮,那就反过来。我曾经因为没看原理图,调了半小时才发现是电平反了。
3. 从闪烁到呼吸灯:PWM 原理
闪烁太简单了,呼吸灯才有点意思。呼吸灯的本质是让 LED 的亮度随时间缓慢变化。怎么控制亮度?靠 PWM(脉冲宽度调制)。
PWM 的核心思想:在一个固定周期内,高电平占的比例(占空比)决定了平均电压。占空比 0% 就是全灭,100% 就是全亮,中间值对应不同亮度。
人眼有视觉暂留效应。只要 PWM 频率足够高(通常 1kHz 以上),你就不会看到闪烁,只会感觉到亮度的变化。呼吸灯就是让占空比从 0% 慢慢升到 100%,再慢慢降回来。
4. 用 embedded-hal 实现 PWM 呼吸灯
embedded-hal 里定义了 PwmPin trait,它提供了 set_duty() 方法来设置占空比。不同的芯片,PWM 的时钟源、定时器配置都不一样,但应用层的接口是统一的。
下面是一个基于 STM32F4 的呼吸灯示例:
use embedded_hal::PwmPin;
use stm32f4xx_hal::{
pac,
prelude::*,
timer::Timer,
};
fn main() -> ! {
let dp = pac::Peripherals::take().unwrap();
let rcc = dp.RCC.constrain();
let clocks = rcc.cfgr.freeze();
let gpiob = dp.GPIOB.split();
let led = gpiob.pb5.into_alternate();
let timer = Timer::new(dp.TIM3, &clocks);
let mut pwm = timer.pwm(led, 1.khz());
let max_duty = pwm.get_max_duty();
pwm.enable();
loop {
// 从暗到亮
for duty in 0..=max_duty {
pwm.set_duty(duty);
delay_ms(1);
}
// 从亮到暗
for duty in (0..=max_duty).rev() {
pwm.set_duty(duty);
delay_ms(1);
}
}
}
这里 get_max_duty() 返回的是 PWM 计数器的最大值。比如 16 位定时器,最大值就是 65535。你传 0 就是全灭,传 max_duty 就是全亮。
5. 调试经验:LED 不亮怎么办?
做硬件调试,LED 不亮是最常见的问题。我总结了一个排查清单:
- 检查引脚模式:是 push-pull 还是 open-drain?LED 一般用 push-pull。
- 检查电平极性:set_high 是亮还是灭?看原理图。
- 检查时钟:GPIO 外设的时钟开了吗?RCC 里要 enable。
- 检查限流电阻:LED 没串电阻?那可能已经烧了。
- 用逻辑分析仪:看引脚波形,确认 PWM 频率和占空比对不对。
我曾经有一次调呼吸灯,LED 死活不亮。折腾了半天,发现是定时器的时钟源没配好,PWM 根本没输出。用逻辑分析仪一看,引脚一直是低电平。嗯,从那以后我每次都会先看波形。
6. 本章知识结构
这一章我们只用了 OutputPin 和 PwmPin。下一章会用到 InputPin 来做按键检测。但别急,先把 LED 玩熟了再说。