认识 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 的价值不止于跨平台。

  1. 解耦业务逻辑和硬件细节

    驱动代码只关心“拉高电平”,不关心“写哪个寄存器”。业务逻辑更清晰,也更容易测试。

  2. 代码复用性

    一个写好的传感器驱动,可以在 STM32、nRF52、ESP32 上直接跑。换芯片就像换插座一样简单。

  3. 社区生态共享

    Rust 嵌入式社区有大量基于 embedded-hal 的驱动库。你写一个,别人能用;别人写的,你也能直接用。

我曾经踩过的坑:早期写驱动时,我直接在驱动里调用了 HAL 实现的具体类型(比如 stm32f4xx_hal::gpio::Pin)。结果换芯片时,所有驱动都要改。后来我强制自己只用 trait 做泛型,再也没出过这种问题。

HAL 的层次结构

为了让你更直观地理解,我画了一张图。

Embedded HAL 层次结构 应用层(业务逻辑) 驱动层(基于 HAL trait) HAL 抽象层(embedded-hal trait) 芯片实现层(HAL 实现) 驱动层只依赖 HAL trait,不依赖具体芯片实现

从这张图可以清楚看到:驱动层位于中间,上下都不粘连。上面是业务逻辑,下面是芯片实现,驱动只认 HAL trait。

HAL 的两种模式:阻塞 vs 非阻塞

embedded-hal 把操作分为两类:

  • 阻塞模式(blocking):函数执行完才返回。比如 DelayMs::delay_ms(),延时期间 CPU 空转。
  • 非阻塞模式(nb):函数立即返回,通过状态或回调通知完成。适合实时性要求高的场景。

我个人习惯:简单应用用阻塞模式,代码好写;复杂系统用非阻塞,配合 RTOS 或 async/await。

关键点:HAL 不规定你用哪种模式。它只提供接口,具体实现由芯片厂商决定。你作为驱动开发者,只需要选对 trait 就行。

小结

这一章我们聊了 HAL 是什么、为什么需要它、以及 embedded-hal 的核心 trait 体系。说白了,HAL 就是嵌入式世界的“标准化接口”。它让驱动开发从“面向寄存器”变成“面向接口”,代码更干净、更可复用。