Chapter 03 · Rust 嵌入式开发

PAC 与 svd2rust

Peripheral Access Crate 如何由 SVD 文件自动生成,如何用 read/write/modify 闭包类型安全地操作寄存器位段,与 C 语言裸指针 + 位运算的本质区别。

课程进度25%

关键概念

PAC (Peripheral Access Crate)
对应 C 世界里 CMSIS 头文件的 Rust 版本。它把芯片所有外设的寄存器映射成类型安全的 Rust API:每个外设是一个结构体,每个寄存器是一个字段,每个位段是一个可读写的方法。如 stm32f4nrf52840-pacrp2040-pac。PAC 是 HAL 的底座,通常不直接用它写业务,但理解它是理解 HAL 的前提。
SVD (System View Description)
芯片厂商提供的 XML 文件,用标准格式(CMSIS-SVD)完整描述一颗 MCU 的所有外设、寄存器、位段、地址、复位值、访问权限。调试器(如 Keil/OpenOCD)用它显示寄存器视图,svd2rust 则用它生成 Rust PAC。SVD 是"芯片寄存器的机器可读说明书"。
svd2rust
一个命令行工具,读入 SVD 文件,自动生成对应的 Rust PAC 源码。生成的代码用类型系统编码了每个寄存器的读写权限、每个位段的合法取值,从而把"往寄存器写非法值""读只写寄存器"这类错误变成编译错误。绝大多数芯片的 PAC 都是由社区跑 svd2rust 生成后发布到 crates.io 的。
read / write / modify 三种访问
PAC 对每个寄存器提供三种方法:read() 返回一个只读代理,可用 .bits() 或位段方法取值;write(|w| ...) 用闭包构造新值并整体写入(未指定的位取复位值);modify(|r, w| ...) 先读出当前值、在闭包里修改部分位、再写回(读-改-写),是最常用的方式。
位段代理与枚举 (Field Writer / Enum)
svd2rust 为多取值的位段生成枚举,如 GPIO 模式位段会有 Input/Output/Alternate/Analog 四个变体。写寄存器时只能传这些合法枚举值,无法写入越界数字。对于单 bit 位段则生成 set_bit()/clear_bit()/bit(bool) 方法。
volatile 与内存屏障
寄存器访问必须是 volatile(易失),即每次读写都真实访问硬件、不能被编译器优化掉或重排。C 里靠 volatile 关键字,Rust PAC 内部用 core::ptr::read_volatile / write_volatile 保证。svd2rust 生成的代码已正确处理 volatile,用户无需关心底层指针。
外设单例与所有权
pac::Peripherals::take() 返回包含所有外设的结构体,且全局只能取一次(内部原子标志保证)。取出后,每个外设字段(如 dp.GPIOA)是一个拥有所有权的值——想在函数间使用就得移动或借用它。这从根本上防止了 C 里"两处代码同时配置同一个外设"导致的冲突。
复位值 (Reset Value)
每个寄存器上电/复位后的默认值,SVD 中有记录。write(|w| ...) 会以复位值为基础、只覆盖闭包中显式设置的位——这意味着用 write 时要清楚其余位会回到复位值;若只想改个别位而保留其他位,应使用 modify

从 SVD 到 PAC 的生成链路

你日常 cargo add stm32f4 拉到的 PAC,其实是这样被造出来的:

PAC 生成流水线 ═══════════════════════════════════════════════════ ┌────────────────────┐ │ STM32F411.svd │ 厂商提供的 XML │ (CMSIS-SVD 格式) │ 描述所有外设/寄存器/位段 └─────────┬──────────┘ │ svd2rust 读取解析 ▼ ┌────────────────────┐ │ 生成的 Rust 源码 │ 每个外设 → struct │ │ 每个寄存器 → 字段 + 方法 │ │ 每个位段 → 枚举/位方法 └─────────┬──────────┘ │ form + rustfmt 拆分/格式化 ▼ ┌────────────────────┐ │ stm32f4 PAC crate │ 发布到 crates.io │ cargo add stm32f4 │ 你直接依赖它 └────────────────────┘
你几乎不会自己跑 svd2rust

主流芯片(STM32 全系、nRF、RP2040、GD32、ESP32 等)的 PAC 都已由社区生成并发布,直接 cargo add 即可。只有在使用极冷门或全新芯片、且厂商仅提供 SVD 时,你才需要自己跑 svd2rust。理解这条链路的意义在于:当你看 HAL 源码时,能明白它底层调用的那些 modify(|_, w| ...) 是从哪来的。

C 裸指针 vs Rust PAC:点亮 PC13

同样是"开 GPIOC 时钟、设 PC13 为输出、输出高电平",对比两种写法的安全性差异:

C:裸指针 + 位运算

  • 地址靠宏,写错编译不报错
  • 位移/掩码手算,易错
  • 可往只读寄存器写
  • 可写入越界的位段值
  • 忘记 volatile 会被优化掉

Rust:PAC 类型安全 API

  • 寄存器是方法,拼错编译报错
  • 位段用命名方法,无需手算
  • 只读寄存器无 write 方法
  • 位段只接受合法枚举值
  • volatile 由生成代码保证

C 写法(对照)

/* 裸指针操作,一切靠程序员小心 */
#define RCC_AHB1ENR  (*(volatile uint32_t*)0x40023830)
#define GPIOC_MODER  (*(volatile uint32_t*)0x40020800)
#define GPIOC_ODR    (*(volatile uint32_t*)0x40020814)

RCC_AHB1ENR |= (1 << 2);              /* GPIOCEN,位号写错也不报错 */
GPIOC_MODER &= ~(0x3 << (13*2));      /* 手算位移,易错 */
GPIOC_MODER |=  (0x1 << (13*2));      /* 01 = 输出 */
GPIOC_ODR   |=  (1 << 13);             /* 输出高 */

Rust PAC 写法

use stm32f4::stm32f411 as pac;

let dp = pac::Peripherals::take().unwrap();

// ① 开 GPIOC 时钟:modify = 读-改-写,只动 gpiocen 位
dp.RCC.ahb1enr.modify(|_, w| w.gpiocen().set_bit());

// ② 设 PC13 为通用输出模式:位段方法 + 命名枚举,无需手算位移
dp.GPIOC.moder.modify(|_, w| w.moder13().output());

// ③ 输出高电平:置位数据寄存器 bit13
dp.GPIOC.odr.modify(|_, w| w.odr13().set_bit());

// 读取寄存器:read() 返回只读代理
let is_high = dp.GPIOC.idr.read().idr13().bit_is_set();
为什么 modify 用两个闭包参数 |r, w|

modify(|r, w| ...) 的闭包里,r 是"当前寄存器已读出的值"(可用来做条件判断),w 是"待写回的构造器"。方法链最终返回 w。它先 read 出旧值填进 w、执行你的闭包修改部分位、再 write_volatile 写回。相比 write(未设置的位归复位值),modify 保留了未触及的位,是操作"寄存器里只想改某几位"场景的正确选择。

write vs modify 的关键区别

方法行为未设置的位适用场景
read()只读,返回代理查询状态位、当前配置
write(|w| ..)整体写入取复位值一次性配置整个寄存器
modify(|r,w| ..)读-改-写保持原值只改部分位、最常用
reset()写入复位值全部复位把寄存器恢复默认
write 会清掉你以为还在的位

常见坑:先 write 配置了 A 位,之后又 write 配置 B 位,结果 A 位被复位值覆盖了。因为 write 是整体写入、未显式设置的位一律取复位值。凡是"在已有配置基础上追加设置",都应该用 modify。只有初始化时"这个寄存器我要完全掌控"才用 write

PAC 的类型安全防护图

PAC 如何用类型系统挡住错误 错误尝试 PAC 的反应 ───────────────────────────────────────────────────── 写只读寄存器 IDR → 编译错误:无 write 方法 往 2-bit 位段写值 5 → 编译错误:只接受枚举变体 拼错寄存器名 modr → 编译错误:无此字段 忘记 volatile → 不可能,生成代码内建 两处代码同时拥有 GPIOC → 编译错误:所有权已移动 ───────────────────────────────────────────────────── 结果:一整类寄存器操作 bug 在编译期消失
本章小结

PAC(Peripheral Access Crate)是 Rust 版的 CMSIS,由 svd2rust 读取厂商的 SVD(CMSIS-SVD 格式 XML,描述所有外设/寄存器/位段/地址/复位值)自动生成,主流芯片的 PAC 已发布到 crates.io 可直接依赖。PAC 提供三种访问:read() 只读代理取状态、write(|w| ..) 整体写入(未设置的位取复位值)、modify(|r, w| ..) 读-改-写保留其余位(最常用)。位段被编码为命名方法与枚举,写非法值、写只读寄存器、拼错寄存器名都会变成编译错误,volatile 由生成代码保证,无需手写。外设通过 Peripherals::take() 取得且全局唯一,每个外设是拥有所有权的值,杜绝多处并发配置。相比 C 裸指针 + 手算位移,PAC 把一整类寄存器 bug 挡在编译期——这是 HAL 得以安全构建的底座。