用结构体建模 PL011 UART 多寄存器布局:Comprehensive Rust 裸机课程中的#[repr(C)]实践
【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust
在 Google Android 团队的 Rust 课程 comprehensive-rust 的裸机(bare-metal)部分,应用处理器(Application Processors)章节讲解如何为 QEMUvirt开发板上的 PL011 UART 编写驱动。本文聚焦其中 Multiple registers 小节:当 UART 拥有十多个寄存器时,逐一用偏移量构造指针既易错又难读,正确做法是用 Rust 结构体一次性描述整个寄存器文件(register file)的内存布局。读完本文,你将掌握#[repr(C)]结构体建模 MMIO 寄存器的完整方法、配套bitflags位域处理技巧,以及课程驱动源码中的安全访问模式。
为什么需要一个结构体来表示寄存器?
PL011 是 ARM 平台上最常见的串口控制器。课程文档 A better UART driver 给出了它的完整寄存器表:
| Offset | Register name | Width |
|---|---|---|
| 0x00 | DR | 12 |
| 0x04 | RSR | 4 |
| 0x18 | FR | 9 |
| 0x20 | ILPR | 8 |
| 0x24 | IBRD | 16 |
| 0x28 | FBRD | 6 |
| 0x2c | LCR_H | 8 |
| 0x30 | CR | 16 |
| 0x34 | IFLS | 6 |
| 0x38 | IMSC | 11 |
| 0x3c | RIS | 11 |
| 0x40 | MIS | 11 |
| 0x44 | ICR | 11 |
| 0x48 | DMACR | 3 |
注:表中省略了若干用于设备标识的 ID 寄存器,以保持示例聚焦(参见 better-uart.md 中的备注)。
如果不用结构体,读写每个寄存器都要手工计算偏移并做指针算术,例如addr.add(0x18)这种写法,既难以阅读,也容易在修订驱动时算错偏移。课程的原话是 "adding offsets to construct pointers to access them is error-prone and hard to read"(用偏移构造指针既易错又难读),这是引出结构体建模的直接动因。
#[repr(C)]:让结构体布局可预测
课程 registers.md 给出的核心代码片段如下:
#[repr(C, align(4))] pub struct Registers { dr: u16, _reserved0: [u8; 2], rsr: ReceiveStatus, _reserved1: [u8; 19], fr: Flags, _reserved2: [u8; 6], ilpr: u8, _reserved3: [u8; 3], ibrd: u16, _reserved4: [u8; 2], fbrd: u8, _reserved5: [u8; 3], lcr_h: u8, _reserved6: [u8; 3], cr: u16, _reserved7: [u8; 3], ifls: u8, _reserved8: [u8; 3], imsc: u16, _reserved9: [u8; 2], ris: u16, _reserved10: [u8; 2], mis: u16, _reserved11: [u8; 2], icr: u16, _reserved12: [u8; 2], dmacr: u8, _reserved13: [u8; 3], }完整实现位于仓库 src/bare-metal/aps/examples/src/pl011_struct.rs 的Registers段(ANCHOR: Registers)。
这段代码承载了几个关键设计决策:
#[repr(C)]:告诉编译器按 C 语言的规则顺序排列字段。默认的 Rust 表示(repr 未指定)允许编译器按自己的意愿重排字段,这会让字段的偏移地址不可预测;#[repr(C)]保证字段按声明顺序排列、遵循 C 的对齐规则,从而得到与硬件寄存器地址一一对应的确定布局。align(4):结构体整体按 4 字节对齐,进一步契合这些寄存器按 4 字节边界排布的事实。- 保留字段(
_reservedN):硬件寄存器地址空间并非连续,结构体中用[u8; N]数组精确填充跳过的地址段。这样后面每个字段的偏移就自动与寄存器表中的 Offset 严格对应。例如fr前面有dr(2 字节)+_reserved0(2 字节)+rsr(2 字节)+_reserved1(19 字节),正好落在 0x18 偏移处。 - 字段类型即宽度:
dr是 12 位但用u16承载;fr(9 位)、cr(16 位)等用u16;单字节寄存器用u8。字段类型直接决定了该寄存器读取/写入时的宽度。
这种"结构体 = 寄存器文件内存镜像"的建模方式,是 Rust 编写 MMIO 设备驱动的经典做法:只需一个指向结构体的指针,就能以具名方式访问任意寄存器。
位域处理:bitflagscrate 的配合
寄存器表中不少寄存器是位域(bit fields),例如 FR(标志寄存器)的每一位代表 UART 的一个状态。课程在 Bitflags 小节 引入bitflagscrate 来结构化地操作这些位。
bitflags!宏会生成一个类似struct Flags(u16)的新类型(newtype),并附带一组用于读写各标志位的方法。课程示例 pl011_struct.rs 中的定义:
use bitflags::bitflags; bitflags! { /// Flags from the UART flag register. #[repr(transparent)] #[derive(Copy, Clone, Debug, Eq, PartialEq)] struct Flags: u16 { /// Clear to send. const CTS = 1 << 0; /// Data set ready. const DSR = 1 << 1; /// Data carrier detect. const DCD = 1 << 2; /// UART busy transmitting data. const BUSY = 1 << 3; /// Receive FIFO is empty. const RXFE = 1 << 4; /// Transmit FIFO is full. const TXFF = 1 << 5; /// Receive FIFO is full. const RXFF = 1 << 6; /// Transmit FIFO is empty. const TXFE = 1 << 7; /// Ring indicator. const RI = 1 << 8; } }要点说明:
- 底层类型用
u16,与 FR 寄存器 9 位宽度匹配; #[repr(transparent)]保证该包装类型在内存中的表示与其内部u16完全一致,这是它能直接放进Registers结构体、与硬件寄存器地址对齐的前提;- 每个常量用
1 << n精确映射硬件手册中的位号; - 同一文件中还定义了
ReceiveStatus(对应 RSR 寄存器),包含 FE(帧错误)、PE(奇偶错误)、BE(断开错误)、OE(溢出错误)四个错误标志位。
bitflags类型自带contains、insert、remove、intersects等方法,后续驱动代码正是靠contains来判断 FIFO 状态。若不用该 crate,就得手写一堆value & (1 << n) != 0的样板代码,且容易写错位号。
驱动如何使用这个结构体
课程 Driver 小节 展示了如何把Registers结构体真正用进驱动。完整实现同样位于 pl011_struct.rs 的Uart段:
/// Driver for a PL011 UART. #[derive(Debug)] pub struct Uart { registers: *mut Registers, } impl Uart { /// Constructs a new instance of the UART driver for a PL011 device with the /// given set of registers. /// /// # Safety /// /// The given pointer must point to the 8 MMIO control registers of a PL011 /// device, which must be mapped into the address space of the process as /// device memory and not have any other aliases. pub unsafe fn new(registers: *mut Registers) -> Self { Self { registers } } /// Writes a single byte to the UART. pub fn write_byte(&mut self, byte: u8) { // Wait until there is room in the TX buffer. while self.read_flag_register().contains(Flags::TXFF) {} // SAFETY: We know that self.registers points to the control registers // of a PL011 device which is appropriately mapped. unsafe { // Write to the TX buffer. (&raw mut (*self.registers).dr).write_volatile(byte.into()); } // Wait until the UART is no longer busy. while self.read_flag_register().contains(Flags::BUSY) {} } /// Reads and returns a pending byte, or `None` if nothing has been /// received. pub fn read_byte(&mut self) -> Option<u8> { if self.read_flag_register().contains(Flags::RXFE) { None } else { // SAFETY: We know that self.registers points to the control // registers of a PL011 device which is appropriately mapped. let data = unsafe { (&raw const (*self.registers).dr).read_volatile() }; // TODO: Check for error conditions in bits 8-11. Some(data as u8) } } fn read_flag_register(&self) -> Flags { // SAFETY: We know that self.registers points to the control registers // of a PL011 device which is appropriately mapped. unsafe { (&raw const (*self.registers).fr).read_volatile() } } }这段驱动代码体现了课程强调的几个关键实践:
1. 用&raw获取字段指针,而非创建中间引用
(&raw mut (*self.registers).dr)与&mut (*self.registers).dr的区别至关重要:后者会创建一个 Rust 引用,而编译器假定"凡引用存在之处都可以随时解引用",于是可能任意插入对 MMIO 地址的读写——这与 MMIO 的 volatile 语义相冲突,是不健全(unsound)的。课程 mmio.md 明确写道:"Never hold a reference to a location being accessed with these methods"(绝不要对用这些方法访问的地址持有引用),并推荐使用&raw从结构体指针直接取字段的裸指针;在旧版 Rust 上可用addr_of!/addr_of_mut!宏替代。
2. volatile 读写
对 MMIO 寄存器必须使用read_volatile/write_volatile。普通读写会被编译器视为无副作用的纯内存操作,从而可能被重排、复制甚至消除;而设备寄存器(如 DR 的读会弹出接收 FIFO 的一个字节)是有副作用的,volatile 访问禁止这些优化,保证每条读写都真正发生。
3. 发送与接收的握手逻辑
write_byte:先轮询 FR 寄存器直到TXFF(发送 FIFO 满)被清除——即有空间可写;写入 DR 后再轮询直到BUSY位清除,确保字节真正发送完毕;read_byte:先检查RXFE(接收 FIFO 空),为空则返回None,否则从 DR 读出一个字节。源码中还留有 TODO,提示 DR 的高位(bit 8-11)包含错误状态,完整驱动应对其进行检查。
4.unsafe边界收窄到构造与访问点
Uart::new是unsafe fn,其 Safety 注释要求调用方保证指针确实指向已映射为设备内存的 PL011 控制寄存器、且没有其他别名。课程随后在 safe-mmio 小节 进一步演进:通过safe-mmiocrate 的UniqueMmioPointer把"指针有效且唯一"的承诺封装起来,使Uart::new变成安全函数,MMIO 读写也不再需要unsafe。该 crate 还提供ReadOnly、ReadPure、ReadWrite、WriteOnly等寄存器类型,区分"读有无副作用"(例如读 DR 会弹出 FIFO 字节,属副作用读,而读 RSR 是纯读),帮助在类型层面阻止误用。
运行与验证
- 该示例不包含在幻灯片正文中,因为它与后续的
safe-mmio示例非常相似;如需实际运行,可在src/bare-metal/aps/examples目录下用make qemu在 QEMU 中启动(参见 driver.md 的备注)。 - 课程源码 pl011_struct.rs 中为演示片段打了 ANCHOR 注释(
// ANCHOR: Registers、// ANCHOR: Flags、// ANCHOR: Uart),mdbook 通过{{#include ...:Registers}}之类的语法把对应片段嵌入各小节页面,因此幻灯片代码与实际可运行源码始终同步。 - 文件顶部的
#![allow(dead_code)]表明这是一个教学用示例;impl Write for Uart让驱动实现core::fmt::Write,从而可以直接write!格式化输出;unsafe impl Send for Uart则声明该驱动可以在任意上下文间传递(它只含一个指向设备内存的指针)。 - 课程将运行平台设定为 QEMU aarch64
virt开发板,按裸机方式(如同编写操作系统)访问设备,不依赖任何宿主操作系统(参见 aps.md 的说明)。
小结
从"用偏移拼指针"到"用#[repr(C)]结构体描述整个寄存器文件",再到用bitflags封装位域、用&raw+ volatile 安全访问字段——这条路径正是 Rust 编写 MMIO 设备驱动的标准范式。它让寄存器地址映射变得可读、可审查,把"算错偏移"这一类最隐蔽的硬件 bug 从根上消除,同时把unsafe限制在最小的边界内。Comprehensive Rust 课程通过 registers.md、bitflags.md、driver.md 三个小节,配上 pl011_struct.rs 这份可直接运行的可执行源码,为学习者提供了一套可复用的嵌入式驱动开发模板,并自然衔接到更安全的safe-mmio抽象。
【免费下载链接】comprehensive-rustThis is the Rust course used by the Android team at Google. It provides you the material to quickly teach Rust.项目地址: https://gitcode.com/GitHub_Trending/co/comprehensive-rust
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考