PAC 与 svd2rust
Peripheral Access Crate 如何由 SVD 文件自动生成,如何用 read/write/modify 闭包类型安全地操作寄存器位段,与 C 语言裸指针 + 位运算的本质区别。
Peripheral Access Crate 如何由 SVD 文件自动生成,如何用 read/write/modify 闭包类型安全地操作寄存器位段,与 C 语言裸指针 + 位运算的本质区别。
stm32f4、nrf52840-pac、rp2040-pac。PAC 是 HAL 的底座,通常不直接用它写业务,但理解它是理解 HAL 的前提。read() 返回一个只读代理,可用 .bits() 或位段方法取值;write(|w| ...) 用闭包构造新值并整体写入(未指定的位取复位值);modify(|r, w| ...) 先读出当前值、在闭包里修改部分位、再写回(读-改-写),是最常用的方式。Input/Output/Alternate/Analog 四个变体。写寄存器时只能传这些合法枚举值,无法写入越界数字。对于单 bit 位段则生成 set_bit()/clear_bit()/bit(bool) 方法。volatile 关键字,Rust PAC 内部用 core::ptr::read_volatile / write_volatile 保证。svd2rust 生成的代码已正确处理 volatile,用户无需关心底层指针。pac::Peripherals::take() 返回包含所有外设的结构体,且全局只能取一次(内部原子标志保证)。取出后,每个外设字段(如 dp.GPIOA)是一个拥有所有权的值——想在函数间使用就得移动或借用它。这从根本上防止了 C 里"两处代码同时配置同一个外设"导致的冲突。write(|w| ...) 会以复位值为基础、只覆盖闭包中显式设置的位——这意味着用 write 时要清楚其余位会回到复位值;若只想改个别位而保留其他位,应使用 modify。你日常 cargo add stm32f4 拉到的 PAC,其实是这样被造出来的:
主流芯片(STM32 全系、nRF、RP2040、GD32、ESP32 等)的 PAC 都已由社区生成并发布,直接 cargo add 即可。只有在使用极冷门或全新芯片、且厂商仅提供 SVD 时,你才需要自己跑 svd2rust。理解这条链路的意义在于:当你看 HAL 源码时,能明白它底层调用的那些 modify(|_, w| ...) 是从哪来的。
同样是"开 GPIOC 时钟、设 PC13 为输出、输出高电平",对比两种写法的安全性差异:
/* 裸指针操作,一切靠程序员小心 */ #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); /* 输出高 */
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| ...) 的闭包里,r 是"当前寄存器已读出的值"(可用来做条件判断),w 是"待写回的构造器"。方法链最终返回 w。它先 read 出旧值填进 w、执行你的闭包修改部分位、再 write_volatile 写回。相比 write(未设置的位归复位值),modify 保留了未触及的位,是操作"寄存器里只想改某几位"场景的正确选择。
| 方法 | 行为 | 未设置的位 | 适用场景 |
|---|---|---|---|
read() | 只读,返回代理 | — | 查询状态位、当前配置 |
write(|w| ..) | 整体写入 | 取复位值 | 一次性配置整个寄存器 |
modify(|r,w| ..) | 读-改-写 | 保持原值 | 只改部分位、最常用 |
reset() | 写入复位值 | 全部复位 | 把寄存器恢复默认 |
常见坑:先 write 配置了 A 位,之后又 write 配置 B 位,结果 A 位被复位值覆盖了。因为 write 是整体写入、未显式设置的位一律取复位值。凡是"在已有配置基础上追加设置",都应该用 modify。只有初始化时"这个寄存器我要完全掌控"才用 write。
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 得以安全构建的底座。