
用 Rust 为 QEMU virt 编写 PL011 UART 裸机驱动comprehensive-rust 实战解析【免费下载链接】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 课程中 bare-metal/APS 章节的 UART 驱动专题为核心讲解如何为 QEMUvirt虚拟机的 PL011 串口控制器编写 Rust 驱动程序。你将掌握使用read_volatile/write_volatile进行 MMIO 访问、用构造不安全、方法安全的封装模式收敛 unsafe 证明负担以及通过实现core::fmt::Write与unsafe impl Send把裸指针设备封装成可格式化输出的串口驱动并在 QEMU 中实际运行验证。PL011 与 QEMU virt驱动的目标设备PL011 是 ARM PrimeCell 家族中的通用异步收发器UARTIP广泛用于各类 ARM 平台作为串行控制台。在裸机开发中最常用的验证环境 QEMU 的virt机器中虚拟平台同样暴露了一个 PL011 实例因此课程选择它为编写驱动的教学对象——uart.md 开篇即指出The QEMU virt machine has a PL011 UART, so lets write a driver for that.在课程对应的实际示例工程中PL011 的基地址被定义为/// Base address of the primary PL011 UART. const PL011_BASE_ADDRESS: *mut u8 0x900_0000 as _;见 main_minimal.rs。该地址是 QEMUvirt机器给主串口分配的内存映射地址驱动需要把这块区域当作**设备内存device memory**来访问——即读取和写入都可能产生副作用编译器不得优化掉或随意重排这些访问。第一步一个最小可用的 PL011 驱动课程给出的第一个版本是极简实现只做逐字节写串口这一件事完整源码位于 pl011_minimal.rs。其核心结构如下const FLAG_REGISTER_OFFSET: usize 0x18; const FR_BUSY: u8 1 3; const FR_TXFF: u8 1 5; /// Minimal driver for a PL011 UART. #[derive(Debug)] pub struct Uart { base_address: *mut u8, } impl Uart { pub unsafe fn new(base_address: *mut u8) - Self { Self { base_address } } /// Writes a single byte to the UART. pub fn write_byte(self, byte: u8) { // Wait until there is room in the TX buffer. while self.read_flag_register() FR_TXFF ! 0 {} // SAFETY: We know that the base address points to the control // registers of a PL011 device which is appropriately mapped. unsafe { // Write to the TX buffer. 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 { // SAFETY: We know that the base address points to the control // registers of a PL011 device which is appropriately mapped. unsafe { self.base_address.add(FLAG_REGISTER_OFFSET).read_volatile() } } }这段代码浓缩了三个关键知识点1. MMIO 必须使用 volatile 访问设备寄存器与普通内存不同读取FRFlag Register这样的寄存器可能清空中断状态写DRData Register则会真正把字节送入硬件发送。课程在 mmio.md 中明确要求使用pointer::read_volatile和pointer::write_volatile访问设备寄存器永远不要持有指向这些位置的引用reference——编译器可能在任何时刻解引用/mut从而擅自重排、合并甚至删除访问需要通过结构体指针取字段时用raw旧版本 Rust 可用addr_of!宏避免创建中间引用。正因如此write_byte中写数据寄存器用的是self.base_address.write_volatile(byte)读标志寄存器用的是self.base_address.add(FLAG_REGISTER_OFFSET).read_volatile()。2. 发送流程轮询 TX FIFO 与忙标志write_byte的发送逻辑分两步等待发送 FIFO 有空位循环读取FR寄存器只要FR_TXFFTransmit FIFO Fullbit 5为 1 就继续等待写数据并等待发送完成向基地址即DR寄存器偏移 0x00写入一个字节后继续轮询FR_BUSYUART Busybit 3直到硬件完成移位发送。这就是典型的无中断轮询式串口发送代码量最小适合引导阶段boot 早期中断尚未使能时使用。3. 寄存器偏移以常量表达最小实现里没有引入结构体而是直接用偏移量FLAG_REGISTER_OFFSET: usize 0x18。这与 PL011 的寄存器布局一致——FR标志寄存器位于偏移0x18而DR数据寄存器位于偏移0x00即基地址本身。课程 better-uart.md 给出了 PL011 完整的寄存器总览OffsetRegister nameWidth0x00DR120x04RSR40x18FR90x20ILPR80x24IBRD160x28FBRD60x2cLCR_H80x30CR160x34IFLS60x38IMSC110x3cRIS110x40MIS110x44ICR110x48DMACR3另有若干 ID 寄存器为简洁起见被省略。核心设计模式把 unsafe 收敛到构造点这段代码最值得学习的设计思想是new是 unsafe其余方法都是 safe的封装策略课程在 uart.md 的演讲者备注中专门解释了这一点为什么Uart::new是 unsafe因为调用者必须保证两条前置条件对于一个给定的 UART全局只能存在一个驱动实例且没有任何其他代码别名alias这块地址空间。为什么write_byte却是 safe一旦new的前置条件被满足后续每次调用write_byte时调用方都可以信任驱动内部已经持有的指针指向合法的 PL011 设备内存因此无需在每个调用点重复做安全推理。为什么不反过来做如果让new安全而write_byteunsafe那么所有调用write_byte的地方都要各自证明安全性——调用点成百上千安全性证明的负担被摊薄到无数处极易出错。这正是课程强调的为 unsafe 代码编写安全包装的通用模式把健全性soundness的证明负担从大量调用点集中迁移到少数构造点。读者在 pl011_minimal.rs 中可以看到new的文档注释用# Safety段落明确写下了调用者必须满足的安全契约。更多 trait让驱动能直接用write!宏仅有write_byte还不够方便。课程紧接着在 traits.md 中演示了如何让驱动融入 Rust 标准库的格式化体系——实现core::fmt::Writetraituse core::fmt::{self, Write}; impl Write for Uart { fn write_str(mut self, s: str) - fmt::Result { for c in s.as_bytes() { self.write_byte(*c); } Ok(()) } }实现Write之后Uart就可以直接配合write!与writeln!宏使用例如在 main_minimal.rs 中writeln!(uart, main({x0:#x}, {x1:#x}, {x2:#x}, {x3:#x})).unwrap();这相当于用 20 行代码给裸机串口接上了 Rust 全套格式化字符串能力{:x}、{}、对齐、填充等代价仅是逐个字节转发。为什么要unsafe impl SendUart内部只有一个*mut u8裸指针。Send是一个 auto-trait但指针类型不会自动实现Send——编译器无法证明指针指向的数据在线程间转移是安全的。对于设备内存则不同PL011 的寄存器可以被任何上下文访问中断上下文、不同 CPU 核课程因此手动声明// SAFETY: Uart just contains a pointer to device memory, which can be // accessed from any context. unsafe impl Send for Uart {}这个例子很好地说明了一个原则unsafe impl的安全性同样需要由开发者声明并保证而这里的保证依据是指向的是设备内存而非普通数据与常规 Rust 内存模型中的Send推理路径不同。运行起来在 QEMU 中打印第一行输出驱动写好后课程在 using.md 中给出了配套的调用程序main_minimal.rs。这是一个#![no_main]、#![no_std]的裸机二进制main由汇编入口entry.S在引导阶段调用参见 inline-assembly.md 的演讲者备注整体流程如下用unsafe { Uart::new(PL011_BASE_ADDRESS) }构造驱动用writeln!(uart, ...)把收到的四个参数寄存器值以十六进制打印到串口通过 PSCI 的system_off::Hvc()请求关闭系统见 main_minimal.rspanic_handler同样把 panic 信息打到串口后关机。在src/bare-metal/aps/examples目录下运行make qemu_minimal即可启动 QEMU 观察输出。该 target 在 Makefile 中的定义是qemu_minimal: minimal.bin qemu-system-aarch64 -machine virt -cpu max -serial mon:stdio -display none -kernel $ -s其中-machine virt指定 QEMU 虚拟平台-serial mon:stdio把串口接到终端标准输入输出-kernel minimal.bin加载裸机二进制。minimal.bin由cargo objcopy --bin minimal -- -O binary从 ELF 转换而来见 Makefile。演进一用结构体布局表达寄存器最小实现里用base_address.add(FLAG_REGISTER_OFFSET)这种基地址 手工偏移的方式访问寄存器。课程在 better-uart.md 中指出这种做法error-prone and hard to read易错且难读随后给出结构体版本。仓库中的 pl011_struct.rs 展示了把整个寄存器堆映射为 C 风格结构体的方案#[repr(C, align(4))] pub struct Registers { dr: u16, _reserved0: [u8; 2], rsr: ReceiveStatus, _reserved1: [u8; 19], fr: Flags, // ... 其余寄存器及保留字段 }#[repr(C)]保证字段按 C 布局排列保留字段_reservedN用于占位以对齐真实硬件偏移。驱动持有*mut Registers通过(raw mut (*self.registers).dr).write_volatile(...)访问具体寄存器——这里正是 mmio.md 强调的用raw从结构体指针取字段而不是创建中间引用。字段级访问比手写偏移更可读、更不易出错。该版本还引入了bitflags!宏把FR的各个位命名化Flags::TXFF、Flags::BUSY等并用contains()判断代码因此变为while self.read_flag_register().contains(Flags::TXFF) {}见 pl011_struct.rs。bitflags 宏完整定义了 PL011 标志位的语义CTS/DSR/DCD/BUSY/RXFE/TXFF/RXFF/TXFE/RI以及接收状态寄存器的错误位FE/PE/BE/OE见 pl011_struct.rs。演进二safe_mmio 与回显程序课程示例仓库中还有进一步的演进版本 pl011.rs它使用safe_mmiocrate 的UniqueMmioPointer把独占设备内存这一安全前提编码进类型系统使Uart::new变为安全函数pub struct Uarta { registers: UniqueMmioPointera, Registers, } impla Uarta { pub fn new(registers: UniqueMmioPointera, Registers) - Self { Self { registers } } }这一版同时补上了接收能力read_byte先检查FR_RXFEReceive FIFO Empty标志非空才从DR读取数据见 pl011.rs。配套的 main_safemmio.rs 演示了完整的回显echo程序——把从串口读到的每个字节原样写回遇到回车补一个换行按q退出loop { if let Some(byte) uart.read_byte() { uart.write_byte(byte); match byte { b\r uart.write_byte(b\n), bq break, _ continue, } } }在src/bare-metal/aps/examples下运行make qemu_safemmio即可在 QEMU 中体验这个可交互的串口终端见 Makefile。小结一条从裸指针到类型安全的驱动演进路线综合 uart.md、traits.md、using.md 三篇课程文档以及示例仓库可以提炼出一条清晰的 PL011 驱动演进路线最小实现pl011_minimal.rs裸*mut u8 偏移常量 volatile 读写unsafe 全部收敛在newtrait 集成实现core::fmt::Write获得write!/writeln!能力unsafe impl Send让驱动可跨上下文使用结构体布局pl011_struct.rs#[repr(C)]结构体 bitflags!消灭手工偏移错误类型系统封装pl011.rssafe_mmio的UniqueMmioPointer把安全前提编码进类型new变成安全函数同时补齐接收与回显能力。每一步都在减少 unsafe 的暴露面、提升可读性与可维护性而始终不变的核心是课程反复强调的准则设备内存必须用 volatile 访问、不能对 MMIO 持有引用、以及把健全性证明负担集中到尽可能少的边界点上。对于任何需要编写裸机外设驱动的 Rust 开发者这套方法论都值得直接借鉴。【免费下载链接】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),仅供参考