2. embedded-hal 特质初探:从点亮一盏灯开始
各位同学,欢迎来到第二章。上一章我们搭好了开发环境,今天咱们来点真家伙——直接上手 embedded-hal 这个核心库。
说实话,我第一次接触嵌入式 HAL 时也有点懵。那么多 trait,到底怎么用?后来做项目多了才明白,这些 trait 就是 Rust 给硬件外设画的“接口蓝图”。你只要实现这些 trait,你的驱动就能跑在任何支持 embedded-hal 的芯片上。这就是生态的力量。
2.1 embedded-hal 是什么?
embedded-hal 是一个 trait 集合,它定义了嵌入式外设的通用接口。它不是库实现,而是一套规范。就像 USB 接口规范——不管你是 U 盘还是键盘,插上去就能用。
| 概念 | 说明 |
|---|---|
| trait 定义 | 只声明方法签名,不提供实现 |
| 芯片厂商实现 | STM32、ESP32、nRF52 等各自实现这些 trait |
| 驱动开发者 | 你只需要依赖 trait,不用关心底层寄存器 |
核心思想:写一次驱动,所有芯片通用。这就是 Rust 嵌入式生态的杀手锏。
2.2 数字输入输出:GPIO 的基本操作
咱们先从最简单的开始——控制一个 LED 亮灭。这背后就是 OutputPin 和 InputPin 两个 trait。
OutputPin:让引脚输出高低电平
看代码最直接:
use embedded_hal::digital::v2::OutputPin;
// 假设 led 已经由 HAL 初始化好了
let mut led: PA5 = ...; // 具体类型由芯片 HAL 提供
// 点亮 LED(输出高电平)
led.set_high().unwrap();
// 熄灭 LED(输出低电平)
led.set_low().unwrap();
// 翻转状态
led.toggle().unwrap();
这里有个细节——set_high() 返回的是 Result。为什么?因为硬件操作可能失败。比如引脚被复用成其他功能了,或者时钟没开。我在项目中就踩过这个坑——直接 unwrap() 一时爽,调试火葬场。
我的习惯:原型阶段用 unwrap() 没问题,但产品代码里一定要做错误处理。哪怕只是 let _ = pin.set_high(); 也比裸 unwrap 强。
InputPin:读取外部信号
读取按键状态是 InputPin 的典型场景:
use embedded_hal::digital::v2::InputPin;
let button: PB1 = ...;
loop {
if button.is_high().unwrap() {
// 按键未按下
} else {
// 按键按下(低电平有效)
}
}
你可能会问:为什么不用 is_low()?两个都有。我个人习惯用 is_high(),逻辑更直观——高电平就是 true,低电平就是 false。
避坑指南:引脚复用冲突
我曾经在一个项目里同时把同一个引脚配成了 OutputPin 和 PWM 输出,结果芯片直接死机。后来查手册才发现,一个引脚同一时间只能绑定一个功能。Rust 的所有权系统其实能帮你避免这个问题——如果你用对了 HAL 的 API。
2.3 阻塞式延时:DelayMs 和 DelayUs
做嵌入式开发,延时是家常便饭。LED 闪烁、按键消抖、传感器等待……都离不开延时。
embedded-hal 提供了两个 trait:
- DelayMs:毫秒级延时
- DelayUs:微秒级延时
用法极其简单:
use embedded_hal::blocking::delay::{DelayMs, DelayUs};
// 假设 delay 由 HAL 初始化
let mut delay: SysDelay = ...;
// 延时 500 毫秒
delay.delay_ms(500u16);
// 延时 100 微秒
delay.delay_us(100u32);
这里有个类型参数的问题——delay_ms 接受 u16,最大 65535 毫秒(约 65 秒)。delay_us 接受 u32,最大约 4294 秒。够用了吧?
注意:阻塞式延时意味着 CPU 在这段时间里啥也不干,就在那空转。对于简单应用没问题,但如果你要做多任务或低功耗设计,就得考虑非阻塞方案了。这个我们后面章节会讲。
2.4 串口通信:serial::Read 和 serial::Write
串口是嵌入式开发者的“眼睛”——调试信息、传感器数据、设备间通信,都靠它。
embedded-hal 把串口抽象成两个 trait:
| Trait | 方法 | 说明 |
|---|---|---|
serial::Write |
bwrite_all(&mut self, buffer: &[u8]) |
发送字节数据 |
serial::Read |
read(&mut self, buffer: &mut [u8]) |
接收字节数据 |
来看个实际例子——打印 "Hello, world!":
use embedded_hal::serial::Write;
use nb::block;
// 假设 serial 由 HAL 初始化
let mut serial: USART1 = ...;
// 发送字符串
let msg = b"Hello, embedded world!\r\n";
serial.bwrite_all(msg).unwrap();
// 注意:bwrite_all 是阻塞的,会等所有字节发送完成
接收数据稍微复杂一点——因为你不知道数据什么时候来:
use embedded_hal::serial::Read;
use nb::block;
let mut rx_buf = [0u8; 1];
// 阻塞等待一个字节
let byte = block!(serial.read()).unwrap();
// 或者用循环接收多个字节
for byte in rx_buf.iter_mut() {
*byte = block!(serial.read()).unwrap();
}
这里出现了 nb::block! 宏。为什么需要它?因为串口读取是非阻塞的——没有数据时返回 WouldBlock 错误。block! 宏帮我们循环等待直到数据到达。
我建议:刚开始学的时候,直接用 block! 配合 unwrap() 就好。等后面学到异步编程,再考虑更优雅的处理方式。
2.5 知识体系总览
这一章的内容其实就围绕一个核心——trait 抽象。下面这张图帮你理清关系:
2.6 本章小结
这一章我们干了三件事:
- 认识了 embedded-hal——它不是库,是接口规范
- 学会了 GPIO 操作——OutputPin 控制输出,InputPin 读取输入
- 掌握了延时和串口——DelayMs/Us 做时间控制,serial::Read/Write 做数据收发
这些 trait 看起来简单,但它们是整个嵌入式 Rust 生态的基石。你想想看——以后你写的传感器驱动,只要依赖这些 trait,就能跑在任何支持 embedded-hal 的芯片上。这就是抽象的力量。
一句话总结:embedded-hal 的 trait 就是 Rust 嵌入式世界的“通用插座”——你只管按规范写驱动,剩下的交给芯片厂商。