第 6 章:可移植性与生态层次
Part1 了解嵌入式rust
第 6 章:可移植性与生态层次#
本章要回答的问题:
embedded-hal是什么?Rust 嵌入式生态的层次结构是怎样的?有哪些并发框架可选?
6.1 问题:不同芯片的外设 API 不同,驱动怎么复用?#
C 的困境:每换一个平台就要重写#
假设你是一个传感器驱动开发者,你写了一个 BME280 温湿度气压传感器的驱动。在 STM32 上,你的代码长这样:
// C:STM32 平台上的 BME280 驱动
#include "stm32f4xx_hal.h"
// 依赖 ST 的 HAL 库
extern I2C_HandleTypeDef hi2c1;
int bme280_read_temperature(int32_t *temp) {
uint8_t reg = 0xFA; // 温度数据寄存器
uint8_t data[3];
// 使用 ST HAL 的 I2C API
HAL_I2C_Mem_Read(&hi2c1, BME280_ADDR << 1, reg,
I2C_MEMADD_SIZE_8BIT, data, 3, 100);
*temp = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4);
return 0;
}c现在,客户说:“我们下一款产品用 nRF52,你的驱动能直接用吗?”
// C:nRF52 平台上的 BME280 驱动——必须重写!
#include "nrf_drv_twi.h"
// Nordic 的 TWI(I2C)驱动 API 完全不同
static const nrf_drv_twi_t twi = NRF_DRV_TWI_INSTANCE(0);
int bme280_read_temperature(int32_t *temp) {
uint8_t reg = 0xFA;
uint8_t data[3];
// Nordic 的 API:函数名、参数、错误处理全部不同
nrf_drv_twi_tx(&twi, BME280_ADDR, ®, 1, false);
nrf_drv_twi_rx(&twi, BME280_ADDR, data, 3);
*temp = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4);
return 0;
}c再换到 ESP32?又是一套完全不同的 API:
// C:ESP32 平台——又要重写!
#include "driver/i2c.h"
int bme280_read_temperature(int32_t *temp) {
uint8_t reg = 0xFA;
uint8_t data[3];
// 乐鑫的 I2C API:又是完全不同的风格
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (BME280_ADDR << 1) | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, reg, true);
i2c_master_start(cmd);
i2c_master_write_byte(cmd, (BME280_ADDR << 1) | I2C_MASTER_READ, true);
i2c_master_read(cmd, data, 3, I2C_MASTER_LAST_NACK);
i2c_master_stop(cmd);
i2c_master_cmd_begin(I2C_NUM_0, cmd, 100 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
*temp = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4);
return 0;
}c问题的本质:
| 平台 | I2C 库 | API 风格 | 错误处理 |
|---|---|---|---|
| STM32 | stm32f4xx_hal.h | HAL_I2C_Mem_Read() | 返回 HAL_StatusTypeDef |
| nRF52 | nrf_drv_twi.h | nrf_drv_twi_tx() / nrf_drv_twi_rx() | 返回 ret_code_t |
| ESP32 | driver/i2c.h | i2c_master_cmd_begin() | 返回 esp_err_t |
| Linux | linux/i2c-dev.h | ioctl() / read() / write() | 返回 -errno |
驱动开发者的痛苦:
- 每支持一个新平台,就要重写所有 I2C/SPI/UART 交互代码
- 每个平台的错误处理方式不同,错误传播逻辑要重新设计
- 无法复用测试——在 STM32 上测试通过的驱动,换到 nRF52 可能行为不同
- 社区碎片化——STM32 的驱动库和 nRF52 的驱动库互不兼容
C 世界的”解决方案”及其局限#
C 世界并非没有尝试过解决这个问题:
| 方案 | 代表 | 局限 |
|---|---|---|
| 厂商统一 HAL | STM32 HAL、nRF SDK | 只覆盖自家芯片,跨厂商无效 |
| CMSIS | ARM CMSIS-Driver | 定义了接口,但实现质量参差不齐,采用率低 |
| 抽象层库 | Zephyr 的设备模型 | 绑定特定 RTOS,不能独立使用 |
| 函数指针表 | 自定义 i2c_ops 结构体 | 无类型安全,运行时开销,无编译期检查 |
// C 的"函数指针表"方案(最接近 embedded-hal 的思路)
typedef struct {
int (*read)(void *ctx, uint8_t addr, uint8_t reg, uint8_t *buf, uint16_t len);
int (*write)(void *ctx, uint8_t addr, uint8_t reg, const uint8_t *buf, uint16_t len);
} i2c_ops_t;
// BME280 驱动只依赖这个接口
typedef struct {
const i2c_ops_t *ops;
void *ctx;
} bme280_dev_t;
int bme280_read_temperature(bme280_dev_t *dev, int32_t *temp) {
uint8_t data[3];
int ret = dev->ops->read(dev->ctx, BME280_ADDR, 0xFA, data, 3);
if (ret != 0) return ret;
*temp = (data[0] << 12) | (data[1] << 4) | (data[2] >> 4);
return 0;
}c这个方案方向是对的——它定义了接口,让驱动与具体实现解耦。但它有严重缺陷:
- 无类型安全:
void *ctx可以是任何东西,传错了编译器不管 - 运行时开销:每次调用都通过函数指针间接跳转(无法内联)
- 无编译期检查:忘了实现某个函数?运行时才发现(空指针崩溃)
- 无泛型:
uint8_t *buf没有长度信息,缓冲区溢出靠程序员自觉
Rust 的 embedded-hal 就是”函数指针表”方案的类型安全、零开销版本。
6.2 embedded-hal 的设计哲学#
核心思想:定义 trait(接口),不定义实现#
embedded-hal 的设计可以用一句话概括:
它只定义”硬件应该能做什么”(trait),不定义”硬件怎么做”(实现)。
这就像 Java 的 interface 或 Go 的 interface——但 Rust 的 trait 更强大,因为:
- 它是零开销的(泛型单态化,无虚函数表)
- 它支持关联类型(错误类型由实现者定义)
- 它支持异步(
async fnin trait,Rust 1.75+ 稳定)
embedded-hal 1.0 的关键 trait#
embedded-hal 1.0 于 2023 年底正式发布,经过多年迭代,已成为 Rust 嵌入式生态的基石。以下是核心 trait 一览:
数字 I/O#
// embedded-hal 1.0:数字输出引脚
pub trait OutputPin {
type Error;
fn set_low(&mut self) -> Result<(), Self::Error>;
fn set_high(&mut self) -> Result<(), Self::Error>;
}
// 数字输入引脚
pub trait InputPin {
type Error;
fn is_low(&mut self) -> Result;
fn is_high(&mut self) -> Result;
}
// 可切换方向的引脚
pub trait StatefulOutputPin: OutputPin {
fn is_set_low(&mut self) -> Result;
fn is_set_high(&mut self) -> Result;
}rustSPI(分为总线和设备两层)#
// SPI 总线(物理层:SCK + MOSI + MISO)
pub trait SpiBus {
type Error;
fn read(&mut self, words: &mut [Word]) -> Result<(), Self::Error>;
fn write(&mut self, words: &[Word]) -> Result<(), Self::Error>;
fn transfer(&mut self, read: &mut [Word], write: &[Word]) -> Result<(), Self::Error>;
fn flush(&mut self) -> Result<(), Self::Error>;
}
// SPI 设备(逻辑层:总线 + 片选)
pub trait SpiDevice {
type Error;
fn transaction(
&mut self,
operations: &mut [Operation],
) -> Result<(), Self::Error>;
}
// SPI 操作类型
pub enum Operation<'a, Word: Copy = u8> {
Read(&'a mut [Word]),
Write(&'a [Word]),
Transfer(&'a mut [Word], &'a [Word]),
DelayNs(u32),
}rust为什么 SPI 要分为 SpiBus 和 SpiDevice?
物理层(SpiBus):
MCU ──── SCK ────┬──── 设备 A
MOSI ───┤
MISO ───┤
└──── 设备 B
逻辑层(SpiDevice):
SpiDevice A = SpiBus + CS_A(片选 A)
SpiDevice B = SpiBus + CS_B(片选 B)plaintextSpiBus 代表物理总线(多个设备共享),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>;
}rustUART(串行通信)#
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:
// embedded-hal-async:异步 I2C
pub trait I2c {
type Error;
async fn read(&mut self, address: u8, read: &mut [u8]) -> Result<(), Self::Error>;
async fn write(&mut self, address: u8, write: &[u8]) -> Result<(), Self::Error>;
async fn write_read(
&mut self,
address: u8,
write: &[u8],
read: &mut [u8],
) -> Result<(), Self::Error>;
}
// embedded-hal-async:异步 SPI 设备
pub trait SpiDevice {
type Error;
async fn transaction(
&mut self,
operations: &mut [Operation],
) -> Result<(), Self::Error>;
}rust异步 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 驱动,它不依赖任何特定平台:
// bme280_driver/src/lib.rs
// 这个驱动可以在 STM32、nRF52、ESP32、RP2040 上运行——无需修改!
#![no_std]
use embedded_hal::i2c::I2c;
const BME280_ADDR: u8 = 0x76;
const REG_TEMP_MSB: u8 = 0xFA;
pub struct Bme280 {
i2c: I2C,
}
pub enum Bme280Error {
I2c(I2cError),
InvalidData,
}
impl Bme280 {
pub fn new(i2c: I2C) -> Self {
Self { i2c }
}
pub fn read_temperature(&mut self) -> Result> {
let mut data = [0u8; 3];
// 只依赖 embedded_hal::i2c::I2c trait——不依赖任何具体 HAL!
self.i2c
.write_read(BME280_ADDR, &[REG_TEMP_MSB], &mut data)
.map_err(Bme280Error::I2c)?;
let raw = ((data[0] as i32) << 12)
| ((data[1] as i32) << 4)
| ((data[2] as i32) >> 4);
if raw == 0x80000 {
return Err(Bme280Error::InvalidData);
}
Ok(raw)
}
}rust这个驱动可以在任何实现了 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 | 总线共享工具 | RefCellDevice、ExclusiveDevice 等 |
embedded-io | I/O 流 trait | 类似 std::io::Read / Write |
embedded-io-async | 异步 I/O 流 trait | Embassy 的网络/串口使用 |
embedded-can | CAN 总线 trait | CAN 通信接口 |
embedded-dma | DMA trait | DMA 传输接口 |
6.3 抽象层次结构#
完整层次图#
Rust 嵌入式生态的抽象层次可以用以下金字塔表示:
┌─────────────────────────────────────────────────────────────┐
│ 应用层 │
│ │
│ 你的业务逻辑:读取传感器 → 处理数据 → 发送报告 │
│ 依赖:embedded-hal trait(不依赖具体 HAL) │
│ │
├─────────────────────────────────────────────────────────────┤
│ 第三方驱动层 │
│ │
│ BME280 驱动、SSD1306 OLED 驱动、MPU6050 驱动... │
│ 依赖:embedded-hal / embedded-hal-async trait │
│ 特点:跨平台,一次编写到处运行 │
│ │
├─────────────────────────────────────────────────────────────┤
│ embedded-hal(接口层) │
│ │
│ 定义 trait:I2c、SpiDevice、OutputPin、UartTx... │
│ 特点:只有接口定义,没有任何实现 │
│ 类比:Java 的 interface / Go 的 interface │
│ │
├─────────────────────────────────────────────────────────────┤
│ HAL 层(芯片厂商实现) │
│ │
│ embassy-stm32 / nrf-hal / esp-hal / rp-hal │
│ 特点:为具体芯片实现 embedded-hal trait │
│ 提供:类型状态 GPIO、时钟配置、DMA 管理 │
│ │
├─────────────────────────────────────────────────────────────┤
│ PAC 层(外设访问) │
│ │
│ stm32f4xx-pac / nrf52840-pac / esp32c3-pac │
│ 特点:由 svd2rust 从 SVD 文件自动生成 │
│ 提供:每个寄存器的类型化读写 API │
│ 类比:CMSIS 的设备头文件(stm32f407xx.h) │
│ │
├─────────────────────────────────────────────────────────────┤
│ 硬件(芯片) │
│ │
│ STM32F407 / nRF52840 / ESP32-C3 / RP2040 │
│ 物理寄存器、外设、总线 │
│ │
└─────────────────────────────────────────────────────────────┘plaintext各层的职责与边界#
| 层次 | 职责 | 谁维护 | 代码量 | 变化频率 |
|---|---|---|---|---|
| 硬件 | 物理寄存器 | 芯片厂商 | — | 不变 |
| 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 转换后:
// 自动生成的 PAC 代码(简化)
pub struct USART2 {
_marker: PhantomData,
}
impl USART2 {
pub const PTR: *const u32 = 0x40004400 as *const u32;
/// Status register
pub fn sr(&self) -> &SR {
unsafe { &*(Self::PTR as *const SR) }
}
/// Data register
pub fn dr(&self) -> &DR {
unsafe { &*(Self::PTR.add(1) as *const DR) }
}
}
// SR 寄存器的类型化访问
pub struct SR { /* ... */ }
impl SR {
pub fn read(&self) -> sr::R { /* ... */ }
}
pub mod sr {
pub struct R { bits: u32 }
impl R {
/// Transmit data register empty
pub fn txe(&self) -> bool { (self.bits >> 7) & 1 != 0 }
/// Read data register not empty
pub fn rxne(&self) -> bool { (self.bits >> 5) & 1 != 0 }
}
}rust与 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();
}rustHAL 层详解:从寄存器到人类可用的 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-hal | let mut led = gpioa.pa5.into_push_pull_output(); |
| 使用第三方驱动 | embedded-hal trait | let mut sensor = Bme280::new(i2c); |
| HAL 未覆盖的外设 | PAC | dp.TIM1.ccr1().write(...) |
| 极致性能优化 | PAC(直接寄存器) | 绕过 HAL 的抽象层 |
| 编写跨平台驱动 | embedded-hal trait | fn read<I2C: I2c>(i2c: &mut I2C) |
| 编写新芯片的 HAL | PAC + embedded-hal | 为 PAC 实现 embedded-hal trait |
6.5 并发框架选型:Embassy vs RTIC vs 传统 RTOS#
三种框架概览#
在 Rust 嵌入式生态中,有三种主流的并发框架:
Embassy:异步协作式#
// Embassy 风格:async/await
#[embassy_executor::task]
async fn sensor_task() {
let mut timer = Ticker::every(Duration::from_secs(1));
loop {
let temp = read_sensor().await; // 异步等待 DMA 完成
process(temp);
timer.next().await; // 异步等待 1 秒(不阻塞 CPU)
}
}
#[embassy_executor::task]
async fn display_task() {
loop {
let data = CHANNEL.receive().await; // 等待数据
update_display(data);
}
}rust核心特征:
- 基于 Rust 的
async/await语法 - 协作式调度:任务在
.await点让出 CPU - 无 RTOS 内核:没有上下文切换,没有独立任务栈
- 零开销:编译后等价于手写状态机
- 由 Rust Embedded 工作组维护,社区最活跃
RTIC:中断驱动抢占式#
// RTIC 风格:中断驱动 + 优先级
#[rtic::app(device = stm32f4xx_hal::pac, dispatchers = [USART1])]
mod app {
#[shared]
struct Shared {
counter: u32,
}
#[local]
struct Local {}
#[init]
fn init(cx: init::Context) -> (Shared, Local) {
// 初始化
(Shared { counter: 0 }, Local {})
}
// 硬件任务:绑定到具体中断,有优先级
#[task(binds = TIM2, priority = 2)]
fn timer_interrupt(cx: timer_interrupt::Context) {
// 高优先级:立即执行,可抢占低优先级任务
cx.shared.counter.lock(|c| *c += 1);
}
// 软件任务:由代码触发
#[task(priority = 1)]
fn process_data(cx: process_data::Context, data: u32) {
// 低优先级:可能被 timer_interrupt 抢占
send_to_display(data);
}
}rust核心特征:
- 基于中断优先级实现抢占式调度
- 编译期静态分析:无数据竞争、无死锁、无优先级反转
- 无运行时调度器:零 Flash/RAM 开销
- 适合硬实时场景(电机控制、通信协议)
- 由瑞典 Luleå 理工大学团队维护
传统 RTOS 绑定(FreeRTOS-rs 等)#
// FreeRTOS-rs 风格:传统 RTOS API
use freertos_rs::{Task, Queue, Duration};
fn main() {
let queue = Queue::create(4).unwrap();
Task::new()
.name("sensor")
.stack_size(512)
.priority(2)
.spawn(move || {
loop {
let temp = read_sensor();
queue.send(temp, Duration::MAX);
Task::delay(Duration::from_secs(1));
}
})
.unwrap();
// 启动调度器
FreeRTOS::start_scheduler();
}rust核心特征:
- 将 C 的 RTOS(FreeRTOS、Zephyr)封装为 Rust API
- 抢占式调度,与 C 版本行为一致
- 适合已有 RTOS 代码的迁移
- 运行时开销与 C 版本相同(上下文切换、独立栈)
三者对比表#
| 维度 | Embassy | RTIC | 传统 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 背书 |
各框架的适用场景#
你的项目需要什么?
│
├── I/O 密集型(传感器 + 通信 + 显示)
│ └── → Embassy ✅
│ 理由:async/await 天然适合等待 I/O,生态最完整
│
├── 硬实时(电机控制、通信协议、严格截止时间)
│ └── → RTIC ✅
│ 理由:编译期保证无优先级反转,WCET 可分析
│
├── 从 C RTOS 项目迁移
│ └── → 传统 RTOS 绑定 ✅
│ 理由:API 相似,迁移成本最低
│
├── 需要网络/USB/蓝牙协议栈
│ └── → Embassy ✅
│ 理由:embassy-net、embassy-usb、trouble 是唯一选择
│
├── 多核(RP2040 双核、ESP32 双核)
│ └── → Embassy ✅ 或 传统 RTOS ✅
│ 理由:Embassy 支持多核 Executor
│
├── 极简资源(< 32KB Flash,< 8KB RAM)
│ └── → RTIC ✅ 或 裸机
│ 理由:RTIC 零运行时开销
│
└── 安全关键(ISO 26262、IEC 61508)
└── → 目前都不成熟,关注 Ferrocene + Embassy 的进展plaintext本书选择 Embassy 的理由#
-
生态最完整:Embassy 是唯一提供完整网络栈(
embassy-net)、USB 栈(embassy-usb)、蓝牙栈(trouble)的 Rust 嵌入式框架。 -
厂商背书最强:乐鑫(Espressif)将 Embassy 作为
esp-rs生态的默认异步框架——这是芯片厂商对 Rust 嵌入式框架的最高级别认可。 -
社区最活跃:GitHub 上 Embassy 的 star 数、issue 活跃度、PR 频率均领先。2026 年的更新节奏约为每 1-2 周一次 release。
-
编程模型最直观:对于 C 工程师来说,
async/await的”顺序写法”比 RTIC 的”中断+优先级声明”更容易上手。 -
与
embedded-hal-async深度集成:Embassy 的 HAL 直接实现embedded-hal-asynctrait,第三方异步驱动可以无缝使用。 -
覆盖面最广:从简单的 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 总结#
本章要点#
-
embedded-hal是 Rust 嵌入式的”USB 接口”:它定义了硬件交互的标准接口(trait),让驱动可以跨平台复用。这是 C 生态一直想做但没做好的事情。 -
抽象层次清晰:芯片 → PAC → HAL → embedded-hal → 驱动 → 应用。每一层有明确的职责边界,你可以选择在任何一层工作。
-
三种并发框架各有适用场景:Embassy(I/O 密集)、RTIC(硬实时)、传统 RTOS(迁移)。本书选择 Embassy,因为它的生态最完整、编程模型最直观、厂商支持最强。
Part 1 知识图谱回顾#
Part 1:通用基础
│
├── 第 1 章:为什么是 Rust?
│ └── 内存安全、并发安全、零成本抽象、no_std
│
├── 第 2 章:Rust 速成
│ └── 所有权、Trait、枚举、泛型、Result、闭包、模块
│
├── 第 3 章:第一个裸机工程
│ └── 工程结构、Cargo.toml、target triple、probe-rs、RTT
│
├── 第 4 章:零成本抽象
│ └── 类型状态、单例模式、unsafe、反汇编验证
│
├── 第 5 章:并发基础
│ └── 超级循环、中断驱动、RTOS、抢占 vs 协作、临界区
│
└── 第 6 章:可移植性与生态层次(本章)
└── embedded-hal、PAC/HAL 层次、Embassy vs RTIC vs RTOSplaintext从 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 |
| 为什么选择 Embassy | Embassy 的完整生态(USB、网络、蓝牙、Bootloader) |
Part 2 的第一个问题(第 7 章):
“Embassy 的 async/await 到底是怎么工作的?它和你在第 5 章看到的手写状态机有什么关系?采用 Embassy 需要付出什么代价?”
Part 1 结束。下一章:第 7 章——Embassy 入门:思想、对比与代价。