知识门户

返回

第 6 章:可移植性与生态层次

Part1 了解嵌入式rust

第 6 章:可移植性与生态层次#

本章要回答的问题embedded-hal 是什么?Rust 嵌入式生态的层次结构是怎样的?有哪些并发框架可选?


6.1 问题:不同芯片的外设 API 不同,驱动怎么复用?#

C 的困境:每换一个平台就要重写#

假设你是一个传感器驱动开发者,你写了一个 BME280 温湿度气压传感器的驱动。在 STM32 上,你的代码长这样:

现在,客户说:“我们下一款产品用 nRF52,你的驱动能直接用吗?”

再换到 ESP32?又是一套完全不同的 API:

问题的本质

平台I2C 库API 风格错误处理
STM32stm32f4xx_hal.hHAL_I2C_Mem_Read()返回 HAL_StatusTypeDef
nRF52nrf_drv_twi.hnrf_drv_twi_tx() / nrf_drv_twi_rx()返回 ret_code_t
ESP32driver/i2c.hi2c_master_cmd_begin()返回 esp_err_t
Linuxlinux/i2c-dev.hioctl() / read() / write()返回 -errno

驱动开发者的痛苦

  • 每支持一个新平台,就要重写所有 I2C/SPI/UART 交互代码
  • 每个平台的错误处理方式不同,错误传播逻辑要重新设计
  • 无法复用测试——在 STM32 上测试通过的驱动,换到 nRF52 可能行为不同
  • 社区碎片化——STM32 的驱动库和 nRF52 的驱动库互不兼容

C 世界的”解决方案”及其局限#

C 世界并非没有尝试过解决这个问题:

方案代表局限
厂商统一 HALSTM32 HAL、nRF SDK只覆盖自家芯片,跨厂商无效
CMSISARM CMSIS-Driver定义了接口,但实现质量参差不齐,采用率低
抽象层库Zephyr 的设备模型绑定特定 RTOS,不能独立使用
函数指针表自定义 i2c_ops 结构体无类型安全,运行时开销,无编译期检查

这个方案方向是对的——它定义了接口,让驱动与具体实现解耦。但它有严重缺陷:

  1. 无类型安全void *ctx 可以是任何东西,传错了编译器不管
  2. 运行时开销:每次调用都通过函数指针间接跳转(无法内联)
  3. 无编译期检查:忘了实现某个函数?运行时才发现(空指针崩溃)
  4. 无泛型uint8_t *buf 没有长度信息,缓冲区溢出靠程序员自觉

Rust 的 embedded-hal 就是”函数指针表”方案的类型安全、零开销版本。


6.2 embedded-hal 的设计哲学#

核心思想:定义 trait(接口),不定义实现#

embedded-hal 的设计可以用一句话概括:

它只定义”硬件应该能做什么”(trait),不定义”硬件怎么做”(实现)。

这就像 Java 的 interface 或 Go 的 interface——但 Rust 的 trait 更强大,因为:

  • 它是零开销的(泛型单态化,无虚函数表)
  • 它支持关联类型(错误类型由实现者定义)
  • 它支持异步async fn in trait,Rust 1.75+ 稳定)

embedded-hal 1.0 的关键 trait#

embedded-hal 1.0 于 2023 年底正式发布,经过多年迭代,已成为 Rust 嵌入式生态的基石。以下是核心 trait 一览:

数字 I/O#

SPI(分为总线和设备两层)#

为什么 SPI 要分为 SpiBusSpiDevice

物理层(SpiBus):
    MCU ──── SCK ────┬──── 设备 A
              MOSI ───┤
              MISO ───┤
                      └──── 设备 B

逻辑层(SpiDevice):
    SpiDevice A = SpiBus + CS_A(片选 A)
    SpiDevice B = SpiBus + CS_B(片选 B)
plaintext

SpiBus 代表物理总线(多个设备共享),SpiDevice 代表总线上的一个具体设备(自动管理片选)。驱动开发者只需要依赖 SpiDevice——不需要关心片选管理。

I2C#

pub trait I2c {
    type Error;

    fn read(&mut self, address: u8, read: &mut [u8]) -> Result<(), Self::Error>;
    fn write(&mut self, address: u8, write: &[u8]) -> Result<(), Self::Error>;
    fn write_read(
        &mut self,
        address: u8,
        write: &[u8],
        read: &mut [u8],
    ) -> Result<(), Self::Error>;
}
rust

UART(串行通信)#

pub trait UartTx {
    type Error;

    fn write(&mut self, buffer: &[Word]) -> Result<(), Self::Error>;
    fn flush(&mut self) -> Result<(), Self::Error>;
}

pub trait UartRx {
    type Error;

    fn read(&mut self, buffer: &mut [Word]) -> Result<(), Self::Error>;
}

pub trait Uart: UartTx + UartRx {}
rust

异步版本(embedded-hal-async#

embedded-hal 1.0 的一个重大变化是引入了异步 trait:

异步 trait 的意义:在 Embassy 中,I2C/SPI 传输通过 DMA 完成,CPU 在等待传输完成时可以执行其他任务。异步 trait 让驱动可以 await 传输完成,而非忙等待。

与 C 的”函数指针表”类比#

C 的函数指针表Rust 的 trait优势
int (*read)(void *ctx, ...)fn read(&mut self, ...) -> Result<..., Self::Error>类型安全,错误类型明确
void *ctx(无类型上下文)&mut self(类型化的实现者)编译期类型检查
运行时通过指针调用编译期单态化(零开销)可内联,无间接跳转
忘了实现某个函数 → 空指针崩溃忘了实现某个方法 → 编译错误编译期保证完整性
无法表达”错误类型因实现而异”type Error(关联类型)每个 HAL 可以定义自己的错误枚举

实际例子:一个 embedded-hal 兼容的传感器驱动#

以下是一个简化版的 BME280 驱动,它不依赖任何特定平台

这个驱动可以在任何实现了 embedded_hal::i2c::I2c trait 的平台上使用

// 在 STM32 上使用
use stm32f4xx_hal::{pac, prelude::*, i2c::I2c};

let dp = pac::Peripherals::take().unwrap();
let gpiob = dp.GPIOB.split();
let i2c = I2c::new(dp.I2C1, (gpiob.pb6, gpiob.pb7), 400.kHz(), &clocks);

let mut sensor = Bme280::new(i2c);  // ✅ STM32 的 I2C 实现了 I2c trait
let temp = sensor.read_temperature().unwrap();
rust
// 在 nRF52 上使用——驱动代码零修改!
use nrf_hal::{pac, prelude::*, twim::Twim};

let pac = pac::Peripherals::take().unwrap();
let i2c = Twim::new(pac.TWIM0, pins, Frequency::K400);

let mut sensor = Bme280::new(i2c);  // ✅ nRF52 的 TWIM 也实现了 I2c trait
let temp = sensor.read_temperature().unwrap();
rust
// 在 ESP32 上使用——驱动代码零修改!
use esp_hal::i2c::master::I2c;

let i2c = I2c::new(peripherals.I2C0, config);

let mut sensor = Bme280::new(i2c);  // ✅ ESP32 的 I2C 也实现了 I2c trait
let temp = sensor.read_temperature().unwrap();
rust

这就是 embedded-hal 的价值:驱动写一次,到处运行。

embedded-hal 生态中的 crate 家族#

Crate功能说明
embedded-hal同步 trait 定义核心接口(阻塞式)
embedded-hal-async异步 trait 定义Embassy 使用的版本
embedded-hal-nb非阻塞 trait(nb 风格)旧版非阻塞方案,逐步被 async 替代
embedded-hal-bus总线共享工具RefCellDeviceExclusiveDevice
embedded-ioI/O 流 trait类似 std::io::Read / Write
embedded-io-async异步 I/O 流 traitEmbassy 的网络/串口使用
embedded-canCAN 总线 traitCAN 通信接口
embedded-dmaDMA traitDMA 传输接口

6.3 抽象层次结构#

完整层次图#

Rust 嵌入式生态的抽象层次可以用以下金字塔表示:

各层的职责与边界#

层次职责谁维护代码量变化频率
硬件物理寄存器芯片厂商不变
PAC寄存器的类型化访问自动生成(svd2rust)~50K 行/芯片极少变
HAL外设的高级 API + embedded-hal 实现社区 / 厂商~10-50K 行/芯片中等
embedded-hal跨平台接口定义Rust Embedded 工作组~2K 行极少变(1.0 已稳定)
第三方驱动传感器/显示器/通信模块驱动社区~500-5K 行/驱动中等
应用业务逻辑取决于项目频繁

与 C 生态的对应关系#

Rust 生态                          C 生态
─────────                          ─────
应用层                             应用层
    │                                  │
第三方驱动(embedded-hal)         第三方驱动(厂商 HAL API)
    │                                  │
embedded-hal(trait 接口)         CMSIS-Driver(接口规范)← 采用率低!
    │                                  │
HAL(embassy-stm32 等)           厂商 HAL(STM32 HAL / nRF SDK)
    │                                  │
PAC(svd2rust 生成)              CMSIS Core + 设备头文件
    │                                  │
硬件                               硬件
plaintext

关键区别

方面C 生态Rust 生态
接口标准CMSIS-Driver(采用率低,各厂商自行其是)embedded-hal(事实标准,几乎所有 HAL 都实现)
接口强制力无(“建议”遵循,不遵循也能编译)编译器强制(不实现 trait 就不能用)
驱动复用极难(每个平台重写)容易(依赖 trait,不依赖实现)
寄存器访问手动 #define + 结构体svd2rust 自动生成(类型安全)
新芯片支持等厂商出 SDK(可能等几年)社区可以从 SVD 文件生成 PAC(几天)

PAC 层详解:svd2rust 的魔法#

SVD(System View Description) 是 ARM 定义的 XML 格式,描述芯片的所有外设和寄存器。每个芯片厂商都会提供 SVD 文件。

svd2rust 工具将 SVD 文件转换为 Rust 代码:

<!-- SVD 文件中的 USART2 描述(简化) -->
xml

经过 svd2rust 转换后:

与 C 的对比

// C:手动定义的寄存器结构体(容易出错)
typedef struct {
    volatile uint32_t SR;   // 偏移 0x00
    volatile uint32_t DR;   // 偏移 0x04
    volatile uint32_t BRR;  // 偏移 0x08
    // ... 如果偏移写错了?编译器不会告诉你
} USART_TypeDef;

#define USART2 ((USART_TypeDef *)0x40004400)

// 读取 RXNE 位
if (USART2->SR & (1 << 5)) {  // 魔法数字 5——如果记错了呢?
    uint8_t data = USART2->DR;
}
c
// Rust:PAC 提供的类型化访问
if usart2.sr().read().rxne() {  // ✅ 字段名有含义,拼错编译不过
    let data = usart2.dr().read().bits();
}
rust

HAL 层详解:从寄存器到人类可用的 API#

HAL 层在 PAC 之上提供”人类可用”的 API:

// PAC 层:直接操作寄存器(繁琐,但类型安全)
// 配置 USART2 为 115200 波特率需要:
// 1. 使能时钟(RCC->APB1ENR |= bit17)
// 2. 配置 GPIO 复用功能(GPIOA->AFR[0])
// 3. 计算 BRR 值(取决于时钟频率)
// 4. 配置 CR1(使能、字长、校验)
// 5. 配置 CR2(停止位)
// 6. 使能 USART(CR1 |= UE)

// HAL 层:一行搞定
let mut usart = dp.USART2.serial(
    (gpioa.pa2, gpioa.pa3),  // TX, RX
    115200.bps(),             // 波特率
    &clocks,                  // 时钟配置(自动计算 BRR)
);
rust

各层的使用场景#

你想做什么使用哪一层示例
快速开发应用HAL + embedded-hallet mut led = gpioa.pa5.into_push_pull_output();
使用第三方驱动embedded-hal traitlet mut sensor = Bme280::new(i2c);
HAL 未覆盖的外设PACdp.TIM1.ccr1().write(...)
极致性能优化PAC(直接寄存器)绕过 HAL 的抽象层
编写跨平台驱动embedded-hal traitfn read<I2C: I2c>(i2c: &mut I2C)
编写新芯片的 HALPAC + embedded-hal为 PAC 实现 embedded-hal trait

6.5 并发框架选型:Embassy vs RTIC vs 传统 RTOS#

三种框架概览#

在 Rust 嵌入式生态中,有三种主流的并发框架:

Embassy:异步协作式#

核心特征

  • 基于 Rust 的 async/await 语法
  • 协作式调度:任务在 .await 点让出 CPU
  • 无 RTOS 内核:没有上下文切换,没有独立任务栈
  • 零开销:编译后等价于手写状态机
  • 由 Rust Embedded 工作组维护,社区最活跃

RTIC:中断驱动抢占式#

核心特征

  • 基于中断优先级实现抢占式调度
  • 编译期静态分析:无数据竞争、无死锁、无优先级反转
  • 无运行时调度器:零 Flash/RAM 开销
  • 适合硬实时场景(电机控制、通信协议)
  • 由瑞典 Luleå 理工大学团队维护

传统 RTOS 绑定(FreeRTOS-rs 等)#

核心特征

  • 将 C 的 RTOS(FreeRTOS、Zephyr)封装为 Rust API
  • 抢占式调度,与 C 版本行为一致
  • 适合已有 RTOS 代码的迁移
  • 运行时开销与 C 版本相同(上下文切换、独立栈)

三者对比表#

维度EmbassyRTIC传统 RTOS 绑定
调度模型协作式(async/await)抢占式(中断优先级)抢占式(RTOS 内核)
上下文切换❌ 无❌ 无✅ 有
每任务独立栈❌ 无(共享栈)❌ 无(静态分配)✅ 有
RAM 开销极小(~几百字节)极小(~几百字节)大(每任务 256-1024B)
Flash 开销小(~2-5 KB)极小(~0,纯代码生成)大(内核 5-20 KB)
实时性⭐⭐⭐ 好(协作式有饿死风险)⭐⭐⭐⭐⭐ 极好(编译期保证)⭐⭐⭐⭐ 好(取决于配置)
确定性⭐⭐⭐⭐ 高⭐⭐⭐⭐⭐ 完全确定⭐⭐⭐ 中等
编程模型顺序代码(async/await)中断处理函数 + 资源声明线程 + IPC
学习曲线⭐⭐⭐ 中等(需理解 async)⭐⭐⭐⭐ 较陡(宏 + 优先级)⭐⭐ 低(熟悉 RTOS 即可)
生态成熟度⭐⭐⭐⭐⭐ 最活跃⭐⭐⭐⭐ 成熟⭐⭐⭐ 依赖 C 生态
I/O 密集型✅ 最佳⭐⭐ 一般⭐⭐⭐ 可以
硬实时⭐⭐⭐ 需配合 InterruptExecutor✅ 最佳⭐⭐⭐⭐ 好
多核支持✅ 支持⭐⭐ 有限✅ 支持
网络/USB/蓝牙✅ 完整协议栈❌ 需自行集成⭐⭐ 依赖 C 库
社区活跃度⭐⭐⭐⭐⭐ 最活跃⭐⭐⭐⭐ 活跃⭐⭐⭐ 中等
厂商支持乐鑫官方默认无厂商背书FreeRTOS 有 AWS 背书

各框架的适用场景#

本书选择 Embassy 的理由#

  1. 生态最完整:Embassy 是唯一提供完整网络栈(embassy-net)、USB 栈(embassy-usb)、蓝牙栈(trouble)的 Rust 嵌入式框架。

  2. 厂商背书最强:乐鑫(Espressif)将 Embassy 作为 esp-rs 生态的默认异步框架——这是芯片厂商对 Rust 嵌入式框架的最高级别认可。

  3. 社区最活跃:GitHub 上 Embassy 的 star 数、issue 活跃度、PR 频率均领先。2026 年的更新节奏约为每 1-2 周一次 release。

  4. 编程模型最直观:对于 C 工程师来说,async/await 的”顺序写法”比 RTIC 的”中断+优先级声明”更容易上手。

  5. embedded-hal-async 深度集成:Embassy 的 HAL 直接实现 embedded-hal-async trait,第三方异步驱动可以无缝使用。

  6. 覆盖面最广:从简单的 LED 闪烁到复杂的 WiFi + BLE + OTA 产品,Embassy 都能覆盖。RTIC 更适合”纯控制”场景。

注意:这并不意味着 RTIC 不好——在硬实时场景中,RTIC 的编译期保证是 Embassy 无法替代的。但对于本书的目标读者(从 C 转向 Rust 的嵌入式工程师)和主力场景(IoT、传感器、通信),Embassy 是最佳起点。

Embassy 与 RTIC 可以共存吗?#

可以,但需要注意:

  • 两者都使用中断,需要协调中断优先级
  • 通常不建议在同一项目中混用(增加复杂度)
  • 如果确实需要:RTIC 处理硬实时任务,Embassy 处理 I/O 任务
  • 更常见的做法:在 Embassy 中使用 InterruptExecutor(第 9 章)实现类似 RTIC 的抢占

6.6 本章小结与 Part 1 总结#

本章要点#

  1. embedded-hal 是 Rust 嵌入式的”USB 接口”:它定义了硬件交互的标准接口(trait),让驱动可以跨平台复用。这是 C 生态一直想做但没做好的事情。

  2. 抽象层次清晰:芯片 → PAC → HAL → embedded-hal → 驱动 → 应用。每一层有明确的职责边界,你可以选择在任何一层工作。

  3. 三种并发框架各有适用场景:Embassy(I/O 密集)、RTIC(硬实时)、传统 RTOS(迁移)。本书选择 Embassy,因为它的生态最完整、编程模型最直观、厂商支持最强。

Part 1 知识图谱回顾#

从 Part 1 到 Part 2 的桥梁#

Part 1 建立了以下认知:

你已经知道的Part 2 将深入的
Rust 的所有权和 trait 系统Embassy 如何利用这些特性实现零开销并发
裸机工程的结构和工具链Embassy 工程的独特结构和配置
类型状态和零成本抽象Embassy HAL 的 GPIO/UART/SPI 异步 API
并发基础(抢占 vs 协作)Embassy Executor 的调度原理
embedded-hal 的接口设计embassy-hal 如何实现 embedded-hal-async
为什么选择 EmbassyEmbassy 的完整生态(USB、网络、蓝牙、Bootloader)

Part 2 的第一个问题(第 7 章):

“Embassy 的 async/await 到底是怎么工作的?它和你在第 5 章看到的手写状态机有什么关系?采用 Embassy 需要付出什么代价?”


Part 1 结束。下一章:第 7 章——Embassy 入门:思想、对比与代价。