news 2026/9/11 13:41:03

Comprehensive Rust 裸机编程:为 QEMU virt 平台的 PL011 UART 编写结构化寄存器驱动

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Comprehensive Rust 裸机编程:为 QEMU virt 平台的 PL011 UART 编写结构化寄存器驱动

Comprehensive Rust 裸机编程:为 QEMU virt 平台的 PL011 UART 编写结构化寄存器驱动

【免费下载链接】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

导读

本文基于 Comprehensive Rust 课程中src/bare-metal/aps/better-uart.md一节,讲解如何把“直接用偏移量拼指针”的裸奔式 UART 驱动,升级为基于#[repr(C, align(4))]寄存器结构体与bitflags位域宏的结构化驱动。你将掌握 PL011 寄存器内存布局的建模方法、位域(bitfield)的声明式访问方式、&raw const/&raw mut裸指针取址的正确用法,以及如何在 QEMU 上运行并验证该驱动。

背景:为什么需要一个“更好的” UART 驱动

在课程的前一讲 Let's write a UART driver 中,我们针对 QEMUvirt机器上的 ARM PL011 UART 写了一个最小驱动。其核心逻辑用base_address.add(offset)的方式手工拼接寄存器指针:

const FLAG_REGISTER_OFFSET: usize = 0x18; const FR_BUSY: u8 = 1 << 3; const FR_TXFF: u8 = 1 << 5; pub fn write_byte(&self, byte: u8) { // Wait until there is room in the TX buffer. while self.read_flag_register() & FR_TXFF != 0 {} unsafe { self.base_address.write_volatile(byte); } // Wait until the UART is no longer busy. while self.read_flag_register() & FR_BUSY != 0 {} } fn read_flag_register(&self) -> u8 { unsafe { self.base_address.add(FLAG_REGISTER_OFFSET).read_volatile() } }

完整代码见 pl011_minimal.rs。这种写法有两个明显痛点:

  1. 偏移量拼指针易错且难读:PL011 实际有 14 个以上的控制寄存器(另有若干 ID 寄存器),每个都有不同的偏移、不同的位宽,靠base.add(0x18)这样的魔法数字极易写错,代码可读性也差;
  2. 位字段缺乏结构化访问:FR(Flag Register)的每一位代表 TX FIFO 满、BUSY 忙等不同含义,FR_TXFF = 1 << 5这类裸位掩码散落在代码里,无法做类型化的检查与组合。

因此这一节的目标是:用 Rust 结构体描述 PL011 的完整寄存器内存布局,用位域类型表达每一位的语义,并据此重写驱动

PL011 寄存器总览:一张必须完整掌握的地址表

PL011 编程模型中的核心寄存器由 better-uart.md 给出。下表是编写结构化驱动的地图,Offset 表示相对 UART 基地址(本示例中为0x900_0000)的偏移

OffsetRegister nameWidth
0x00DR12
0x04RSR4
0x18FR9
0x20ILPR8
0x24IBRD16
0x28FBRD6
0x2cLCR_H8
0x30CR16
0x34IFLS6
0x38IMSC11
0x3cRIS11
0x40MIS11
0x44ICR11
0x48DMACR3

为保持简洁,表中省略了若干 ID 寄存器(如 ID0–ID12、PERIPHID 等),它们不影响控制逻辑的编写。

注意各寄存器并非连续排列——例如 DR 在 0x00(12 位),RSR 却跳到了 0x04,FR 在 0x18,中间的地址段要么是保留区,要么是其他功能。这正是“用偏移拼指针”容易出错的根源:你不仅要记住偏移,还要记住每个寄存器之间隔着多长的保留区域。

第一步:用#[repr(C)]结构体建模寄存器布局

解决方案在 registers.md 中给出:用结构体表示 UART 寄存器的内存布局。

结构体定义

课程的实现位于 pl011_struct.rs:

#[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], }

设计要点解读

  • #[repr(C, align(4))]保证布局可预测:Rust 默认的内存表示允许编译器随意重排字段、按需填充 padding,这会让字段偏移与硬件地址对不上。repr(C)则强制编译器按 C 的规则、严格按字段声明顺序排布,确保dr落在偏移 0x00、rsr落在 0x04、fr落在 0x18……与硬件寄存器地址一一对应。align(4)则进一步对齐到 4 字节,适配 MMIO 访问的硬件约束。可参考 Rust Reference 中关于 C representation 的说明 理解其语义。
  • _reservedN: [u8; N]显式填充空洞:每个真实寄存器与下一个寄存器之间,用对应大小的字节数组“占位”,把不存在的地址段显式表达出来,保证后继字段落在正确偏移上。
  • 字段类型贴合寄存器位宽:12 位的 DR 用u16(位宽向上取整到 2 字节,并在其后补 2 字节保留区维持 4 字节对齐节奏),16 位的 IBRD、CR、IMSC、RIS、MIS、ICR 用u16,8 位的 ILPR、FBRD、LCR_H、IFLS、DMACR 用u8,4 位的 RSR 用自定义类型ReceiveStatus,9 位的 FR 用自定义类型Flags

第二步:用bitflags宏声明位域语义

FR 和 RSR 都是典型的位字段寄存器,课程推荐用bitflags)。bitflags!宏会生成一个类似struct Flags(u16)的 newtype,并附带一整套读写位的方法实现。

Flags:Flag Register 的 9 个位

实现见 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; } }

其中与本驱动强相关的三位是:

常量含义驱动用途
BUSY1 << 3UART 正在发送数据发送字节后等待其清零
RXFE1 << 4接收 FIFO 为空判断是否有字节可读
TXFF1 << 5发送 FIFO 已满发送前轮询等待空间

ReceiveStatus:RSR / 错误清除寄存器

4 位宽的错误状态位同样用位域建模(见 pl011_struct.rs):

bitflags! { /// Flags from the UART Receive Status Register / Error Clear Register. #[repr(transparent)] #[derive(Copy, Clone, Debug, Eq, PartialEq)] struct ReceiveStatus: u16 { /// Framing error. const FE = 1 << 0; /// Parity error. const PE = 1 << 1; /// Break error. const BE = 1 << 2; /// Overrun error. const OE = 1 << 3; } }

FE(帧错误)、PE(奇偶错误)、BE(中断错误)、OE(溢出错误)四位将来可用于读取路径的错误检测——课程示例的read_byte目前以// TODO标注了这一点(见下文)。

第三步:基于Registers结构体重写驱动

有了结构体与位域类型,就可以在 driver.md 的指导下重写驱动。完整实现见 pl011_struct.rs:

/// 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() } } }

与旧驱动相比的改进点

  • 字段访问取代魔法偏移(*self.registers).fr(*self.registers).dr直接按字段名访问,编译器根据repr(C)布局计算出地址,再也不用手写0x18之类的偏移常量。
  • 位域类型取代裸位掩码Flags::TXFFFlags::BUSY通过bitflags生成的contains()方法做类型化检查,语义一目了然。
  • &raw const/&raw mut直接对字段取裸指针:这是本示例最值得注意的 Rust 细节。对(*self.registers).dr&mut会先构造一个指向Registers的临时引用,而对 MMIO 寄存器取引用(尤其&mut)本身是未定义行为——设备寄存器在读取时可能有副作用(如清中断位),且 Rust 引用要求别名规则不被破坏。&raw mut/&raw const(自 Rust 1.82 起为稳定语法,此前是addr_of!/addr_of_mut!宏)在不创建中间引用的情况下直接得到字段的裸指针,是读写 MMIO 字段的正确姿势。
  • 所有“真读”都走read_volatile/write_volatile:防止编译器将重复的 MMIO 读优化掉,或把对设备的写当作普通内存写合并。
  • unsafe 边界集中在new一处Uart::newunsafe fn,其 Safety 契约要求传入的指针确实指向一个 PL011 设备的 MMIO 控制寄存器、已按设备内存映射且无其他别名;一旦契约满足,后续write_byte/read_byte就都是安全方法。这正是上一讲 uart.md 强调的设计模式:把证明 soundness 的责任从大量调用点集中到少数构造点,是安全封装 unsafe 代码的通用做法。

尚未完成的部分

read_byte中有两处留白:

  1. // TODO: Check for error conditions in bits 8-11:DR 寄存器 12 位宽中的高 4 位(bits 8–11)是错误标志,未来接入ReceiveStatus位域即可补全;
  2. 课程刻意没有把该示例加入幻灯片正文,因为其形态与下一讲的safe-mmio示例非常接近(见 driver.md 的备注)。

第四步:实现WriteSendtrait

要让驱动用起来顺手,还需实现core::fmt::Write,从而直接使用write!/writeln!宏;再手动实现Send,使驱动可以跨上下文(如中断处理)传递。实现见 pl011_struct.rs:

impl Write for Uart { fn write_str(&mut self, s: &str) -> fmt::Result { for c in s.as_bytes() { self.write_byte(*c); } Ok(()) } } // Safe because it just contains a pointer to device memory, which can be // accessed from any context. unsafe impl Send for Uart {}

两个 trait 的实现要点:

  • Writecore::fmt::Write:在#![no_std]裸机环境中没有标准库的std::io::Write,需导入core::fmt::{self, Write}。实现write_str后即可用writeln!(uart, "...")格式化输出(课程对同一模式在 pl011_minimal.rs 中也有注释说明)。
  • Send需要手动unsafe implSend是 auto trait,但不会为包含裸指针的类型自动实现(裸指针不是Send)。Uart内部只有一个指向设备内存的裸指针,设备内存可从任何上下文访问,因此手动unsafe impl Send for Uart {}是安全的。Sync则未实现——驱动方法都取&mut self,不允许多线程共享引用,这正符合 MMIO 驱动“同一时刻只能有一个访问者”的约束。

在 QEMU 上运行验证

课程提供了完整的可运行示例:入口main位于 main_minimal.rs:

#![no_main] #![no_std] mod asm; mod exceptions; mod pl011_minimal; use crate::pl011_minimal::Uart; use core::fmt::Write; use core::panic::PanicInfo; use log::error; use smccc::Hvc; use smccc::psci::system_off; /// Base address of the primary PL011 UART. const PL011_BASE_ADDRESS: *mut u8 = 0x900_0000 as _; #[unsafe(no_mangle)] extern "C" fn main(x0: u64, x1: u64, x2: u64, x3: u64) { // SAFETY: `PL011_BASE_ADDRESS` is the base address of a PL011 device, and // nothing else accesses that address range. let mut uart = unsafe { Uart::new(PL011_BASE_ADDRESS) }; writeln!(uart, "main({x0:#x}, {x1:#x}, {x2:#x}, {x3:#x})").unwrap(); system_off::<Hvc>().unwrap(); }

要点说明:

  • 基地址0x900_0000是 QEMUvirt机器上主 PL011 UART 的 MMIO 基地址;
  • main不是 Rust 入口,而是由entry.S中的启动代码在初始化完成后调用(同 inline assembly 一节的约定),因此标注#[no_mangle] extern "C"
  • writeln!借助刚实现的Writetrait 把x0–x3四个启动参数以十六进制打印到串口控制台,随后通过 PSCIsystem_off关闭虚拟机;
  • panic handler 同样会打印错误信息并关机。

运行命令

src/bare-metal/aps/examples目录下,使用 Makefile 提供的两个目标:

  • make qemu_minimal:运行基于 main_minimal.rs 的最小示例,串口输出main(...)启动参数(见 uart/using.md 的说明);
  • make qemu:运行完整示例集;本文的结构化Registers驱动若需单独验证,同样可在此目标下执行(见 driver.md 的备注)。

若用Uart::new时传入PL011_BASE_ADDRESS对应的*mut Registers指针,即可在 QEMU 串口上看到与最小示例一致的输出,从而验证结构化驱动的读写路径正确。

小结:从最小驱动到结构化驱动的演进路径

维度最小驱动(pl011_minimal.rs)结构化驱动(pl011_struct.rs)
寄存器访问base.add(offset)手拼偏移repr(C)结构体字段,编译器算地址
位字段裸常量FR_TXFF = 1 << 5bitflags!newtype +contains()
取字段指针无(基于u8指针)&raw const/&raw mut免引用取址
可读性/可维护性魔法数字多,易错字段名 + 类型语义,声明式表达

结构化驱动把 PL011 的寄存器表(偏移、位宽、位含义)直接翻译成了 Rust 类型系统:偏移映射为repr(C)结构体的字段顺序,位宽映射为字段类型,位含义映射为bitflags常量。这让驱动代码几乎成为硬件手册的可执行注释,也为课程后续的safe-mmio示例(在裸指针之上构建类型安全的 MMIO 抽象)铺平了道路。

延伸阅读(Comprehensive Rust 课程内):

  • 寄存器建模与repr(C)详解
  • 位域宏使用说明
  • 结构化驱动设计说明
  • 上一讲:最小 UART 驱动与 unsafe 边界设计
  • QEMU 运行入口与串口输出示例
  • 完整课程源码目录

【免费下载链接】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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 13:37:24

Agilent E4417A功率计的VISA控制与SCPI编程指南

1. Agilent E4417A功率计控制基础Agilent E4417A作为一款经典的高精度功率计&#xff0c;在射频测试领域已经服役超过20年。我手头这台设备虽然出厂于2003年&#xff0c;但配合现代计算机通过VISA控制&#xff0c;依然能完美胜任5G基站功率校准任务。不同于现在主流的USB接口设…

作者头像 李华