认识 Embedded HAL:硬件抽象层的设计哲学
各位同学,今天我们来聊聊 Embedded HAL。说实话,这个概念刚接触时我也觉得有点虚——什么抽象层、trait体系,听起来像是理论课。但等你真正写过几个驱动,踩过几回坑,就会明白:HAL 不是学术概念,它是嵌入式开发的生存工具。
什么是 HAL?从一次惨痛经历说起
先讲个故事。几年前我做一款工业传感器驱动,芯片用的是 STM32F4。驱动写好了,跑得稳稳的。结果客户说要换 NXP 的芯片,因为供货问题。我当时就懵了——所有 GPIO 操作、定时器配置全是针对 STM32 的寄存器写的,几乎要重写一遍。
这就是没有 HAL 的代价。你写的驱动,和具体芯片绑死了。
HAL 的全称是 Hardware Abstraction Layer,硬件抽象层。它的核心思想很简单:把“我要做什么”和“具体怎么做”分开。
- 做什么:让 LED 亮、让蜂鸣器响、延时 100 毫秒
- 怎么做:写哪个寄存器、拉高哪个引脚、用哪个定时器
HAL 就是中间那层胶水。它定义了一套接口,驱动只跟接口打交道,不关心底层芯片是谁。
一句话总结:HAL 让驱动代码“一次编写,多平台运行”。
embedded-hal:Rust 生态的 HAL 标准
在 Rust 嵌入式世界里,embedded-hal 就是那个标准。它不是某个芯片厂商的库,而是社区共同维护的一组 trait 定义。
说白了,它只告诉你“接口长什么样”,不告诉你“怎么实现”。
核心 trait 一览
| trait 名称 | 所属模块 | 作用 |
|---|---|---|
OutputPin |
digital |
控制 GPIO 输出高低电平 |
InputPin |
digital |
读取 GPIO 输入电平 |
DelayMs |
blocking::delay |
阻塞式毫秒延时 |
DelayUs |
blocking::delay |
阻塞式微秒延时 |
I2C |
blocking::i2c |
I2C 总线读写操作 |
SpiDevice |
blocking::spi |
SPI 设备读写操作 |
这些 trait 就是 HAL 的“接口契约”。任何芯片厂商,只要实现了这些 trait,你的驱动就能直接跑。
代码实战:用 trait 写一个通用 LED 驱动
光说不练假把式。我们直接写代码看看。
use embedded_hal::digital::OutputPin;
/// 一个通用的 LED 驱动
pub struct Led<P: OutputPin> {
pin: P,
}
impl<P: OutputPin> Led<P> {
pub fn new(pin: P) -> Self {
Self { pin }
}
pub fn on(&mut self) {
self.pin.set_high().unwrap();
}
pub fn off(&mut self) {
self.pin.set_low().unwrap();
}
}
注意看,这个 Led 结构体不依赖任何具体芯片。它只要求传入一个实现了 OutputPin 的类型。
在 STM32 上使用时:
use stm32f4xx_hal::gpio::{gpioc, Output, PushPull};
use stm32f4xx_hal::pac;
let dp = pac::Peripherals::take().unwrap();
let gpioc = dp.GPIOC.split();
let led_pin = gpioc.pc13.into_push_pull_output();
let mut led = Led::new(led_pin);
led.on(); // 点亮 LED
换到 nRF52 上呢?
use nrf52840_hal::gpio::{p0, Level, Output, PushPull};
let board = nrf52840_hal::pac::Peripherals::take().unwrap();
let p0 = p0::Parts::new(board.P0);
let led_pin = p0.p0_13.into_push_pull_output(Level::Low);
let mut led = Led::new(led_pin);
led.on(); // 同样点亮 LED
看到没?Led 驱动一个字都没改。这就是 HAL 的威力。
我的习惯:写驱动时,所有对外接口都用 HAL trait 做泛型参数。这样驱动天然就是跨平台的。我后来做过的三个项目,驱动库基本是复用的,只换底层 HAL 实现。
为什么需要 HAL?三个核心理由
你可能会想:我项目就一个芯片,用 HAL 不是多此一举吗?
嗯,这个问题我当年也问过。后来想明白了,HAL 的价值不止于跨平台。
- 解耦业务逻辑和硬件细节
驱动代码只关心“拉高电平”,不关心“写哪个寄存器”。业务逻辑更清晰,也更容易测试。
- 代码复用性
一个写好的传感器驱动,可以在 STM32、nRF52、ESP32 上直接跑。换芯片就像换插座一样简单。
- 社区生态共享
Rust 嵌入式社区有大量基于
embedded-hal的驱动库。你写一个,别人能用;别人写的,你也能直接用。
我曾经踩过的坑:早期写驱动时,我直接在驱动里调用了 HAL 实现的具体类型(比如 stm32f4xx_hal::gpio::Pin)。结果换芯片时,所有驱动都要改。后来我强制自己只用 trait 做泛型,再也没出过这种问题。
HAL 的层次结构
为了让你更直观地理解,我画了一张图。
从这张图可以清楚看到:驱动层位于中间,上下都不粘连。上面是业务逻辑,下面是芯片实现,驱动只认 HAL trait。
HAL 的两种模式:阻塞 vs 非阻塞
embedded-hal 把操作分为两类:
- 阻塞模式(blocking):函数执行完才返回。比如
DelayMs::delay_ms(),延时期间 CPU 空转。 - 非阻塞模式(nb):函数立即返回,通过状态或回调通知完成。适合实时性要求高的场景。
我个人习惯:简单应用用阻塞模式,代码好写;复杂系统用非阻塞,配合 RTOS 或 async/await。
关键点:HAL 不规定你用哪种模式。它只提供接口,具体实现由芯片厂商决定。你作为驱动开发者,只需要选对 trait 就行。
小结
这一章我们聊了 HAL 是什么、为什么需要它、以及 embedded-hal 的核心 trait 体系。说白了,HAL 就是嵌入式世界的“标准化接口”。它让驱动开发从“面向寄存器”变成“面向接口”,代码更干净、更可复用。