Comprehensive Rust 裸机实战:在 QEMU aarch64 上编写 PL031 实时时钟(RTC)驱动

发布时间:2026/9/11 12:22:12
Comprehensive Rust 裸机实战:在 QEMU aarch64 上编写 PL031 实时时钟(RTC)驱动 Comprehensive Rust 裸机实战在 QEMU aarch64 上编写 PL031 实时时钟RTC驱动【免费下载链接】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裸机章节下午场Afternoon的核心实战指南主题是为 QEMU aarch64 virt 虚拟机上的 PL031 实时时钟Real-Time Clock编写设备驱动。你将掌握通过 MMIO 寄存器读取 RTC 当前时间并格式化输出、利用 Match 寄存器实现精确到秒的忙等待busy-wait、以及借助 Arm GIC 中断控制器完成睡眠等待的进阶中断编程。完成本练习后你将具备在no_std、no_main环境下从零编写裸机外设驱动的完整能力。本练习对应课程文件 afternoon.md其完整任务描述在 rtc.md参考解答在 solutions-afternoon.md。练习背景与硬件平台为什么选择 PL031PL031 是 Arm 官方提供的 PrimeCell 实时时钟外设广泛出现在 Arm 生态的参考平台与虚拟化环境如 QEMU中。在 QEMU aarch64 virt 机器上PL031 被固定映射在物理地址0x9010000紧邻主串口 PL011 UART0x900_0000。它的数据寄存器DR以秒为单位递增计数非常适合用来演示读取时间—设定闹钟—等待中断这一典型的嵌入式外设编程闭环。与上午场morning.md通过 I2C 罗盘读取方向并输出到串口相比下午场的 RTC 练习更侧重于时间戳处理、寄存器状态轮询与中断驱动三种编程模型的对比。练习的三大任务根据 rtc.md 的要求本练习分三步递进读取并打印当前时间编写驱动读取 PL031 的时间戳使用chronocrate 完成日期时间格式化输出到串口控制台匹配寄存器忙等待使用 Match 寄存器与原始中断状态寄存器Raw Interrupt Status忙等待到未来某个时刻例如 3 秒后循环体内调用core::hint::spin_loop扩展时间允许时中断驱动等待启用并处理 RTC Match 产生的中断借助arm-giccrate 配置 Arm 通用中断控制器GICRTC 中断被连接到 GIC 的IntId::spi(2)中断使能后通过arm_gic::wfi()让内核休眠直到收到中断为止。驱动器的寄存器级设计PL031 寄存器布局参考解答 pl031.rs 使用safe-mmiocrate 以#[repr(C, align(4))]结构体精确描述 PL031 的寄存器文件。根据结构体字段顺序可以推断出各寄存器相对基地址0x9010000的偏移偏移寄存器简称类型作用0x00Data Registerdr只读 u32当前 RTC 值秒级时间戳0x04Match Registermr读写 u32写入目标时间相等时触发匹配0x08Load Registerlr读写 u32写入以预设 RTC 值0x0CControl Registercr读写 u8控制使能等本例未直接使用0x10Interrupt Mask Set/Clearimsc读写 u8中断屏蔽位bit01 使能中断0x14Raw Interrupt Statusris只读 u8原始中断状态bit01 表示匹配发生0x18Masked Interrupt Statusmis只读 u8屏蔽后的中断状态即已提交到 GIC0x1CInterrupt Clear Registericr只写 u8写 0x01 清除挂起的中断代码中的寄存器类型选择本身即是裸机编程的良好示范ReadPure表示只读、ReadPureWrite表示可读可写、WriteOnly表示只写icr就是典型只写寄存器u8与_reserved: [u8; 3]的配合则保证 32 位对齐的寄存器间距。驱动 API 设计Rtca结构体持有UniqueMmioPointera, Registers来自safe-mmio对外暴露六个方法全部语义清晰、一一对应硬件行为new(registers)从NonNullRegisters构造驱动实例read() - u32读dr返回当前时间戳set_match(value)写mr设定匹配时刻RTC 值匹配时产生中断若已使能matched() - bool读ris的 bit0判断是否已匹配与中断是否使能无关interrupt_pending() - bool读mis的 bit0判断当前是否有中断挂起enable_interrupt(mask: bool)写imscbit0 置 1 使能中断、置 0 关闭clear_interrupt()向icr写 0x01 清除挂起中断。注意matched与interrupt_pending的区别前者反映硬件匹配事实Raw Status后者反映经过屏蔽后的最终中断信号Masked Status二者在中断掩码打开时应当一致——这正是 pl031.rs 注释中如果且仅如果matched为真且中断被屏蔽interrupt_pending才为真的含义。主程序的三段式实现完整可编译的解答见 main.rs解法片段用ANCHOR: solution标记其入口结构如下。启动与初始化裸机环境的必要铺垫#![no_main] #![no_std] mod exceptions; mod logger; mod pl031;程序没有标准库与 main运行时代理由aarch64-rt的entry!宏接管。入口函数main(x0, x1, x2, x3)接收引导加载程序传入的参数依次完成初始化串口日志Uart::new(UniqueMmioPointer::new(PL011_BASE_ADDRESS))其中PL011_BASE_ADDRESS 0x900_0000随后logger::init(uart, LevelFilter::Trace)。日志实现见 logger.rs——它用SpinMutexOptionUart包住串口实现log::Logtrait输出格式为[级别] 消息初始化 GICv3以GICD_BASE_ADDRESS 0x800_0000分发器与GICR_BASE_ADDRESS 0x80A_0000重分发器构造GicV3并调用gic.setup(0)为后续中断扩展做准备初始页表通过initial_pagetable!宏建立恒等映射其中idmap[0]为 1 GiB 设备内存0x0起、idmap[1]为 1 GiB 普通内存0x4000_0000起、idmap[256]为 256 GiB 处的另一段设备内存设备内存使用VALID | ATTRIBUTE_INDEX_0 | ACCESSED | UXN属性普通内存使用VALID | ATTRIBUTE_INDEX_1 | INNER_SHAREABLE | ACCESSED | NON_GLOBAL属性。const PL031_BASE_ADDRESS: NonNullpl031::Registers NonNull::new(0x901_0000 as _).unwrap(); const PL031_IRQ: IntId IntId::spi(2);RTC 基地址与中断号被定义为常量贯穿后续所有逻辑。第一步读取并打印当前时间let mut rtc unsafe { Rtc::new(UniqueMmioPointer::new(PL031_BASE_ADDRESS)) }; let timestamp rtc.read(); let time Utc.timestamp_opt(timestamp.into(), 0).unwrap(); info!(RTC: {time});rtc.read()返回自 1970-01-01 起的秒数u32经chrono::Utc::timestamp_opt转为可读的 UTC 时间后输出。chrono在 Cargo.toml 中以default-features false引入以适配no_std环境。第二步Match 寄存器 忙等待// Wait for 3 seconds, without interrupts. let target timestamp 3; rtc.set_match(target); info!(Waiting for {}, Utc.timestamp_opt(target.into(), 0).unwrap()); while !rtc.matched() { spin_loop(); } info!(Finished waiting);此段演示无中断的轮询式等待把目标时刻写入mr然后循环读取ris直到 bit0 置位。core::hint::spin_loop()向 CPU 提示当前处于自旋等待状态避免流水线过热。trace!日志会在等待前后分别打印matched与interrupt_pending便于在 QEMU 上观察两个状态位的演变。第三步扩展GIC 中断 WFI 睡眠等待这是本练习最富技巧的部分核心思路是把 CPU 从空转换成睡眠GicCpuInterface::set_priority_mask(0xff); gic.set_interrupt_priority(PL031_IRQ, None, 0x80).unwrap(); gic.set_trigger(PL031_IRQ, None, Trigger::Level).unwrap(); irq_enable(); gic.enable_interrupt(PL031_IRQ, None, true).unwrap(); // Wait another 3 seconds for an interrupt. let target timestamp 6; rtc.set_match(target); rtc.clear_interrupt(); rtc.enable_interrupt(true); while !rtc.interrupt_pending() { wfi(); } info!(Finished waiting);完整流程包含四层配合GIC 侧配置设置优先级掩码为0xff接收所有中断为PL031_IRQ分配优先级0x80触发方式设为电平触发Trigger::Level然后irq_enable()打开中断总开关最后enable_interrupt启用该中断源RTC 侧配置set_match(target)设定新目标clear_interrupt()清除上一次可能残留的挂起中断enable_interrupt(true)打开 PL031 的中断掩码睡眠等待wfi()Wait For Interrupt让核心进入低功耗等待状态直到收到中断被唤醒中断服务中断到来后由 exceptions.rs 中的irq_current处理——调用GicCpuInterface::get_and_acknowledge_interrupt(InterruptGroup::Group1)读取并确认中断号再打印IRQ {intid:?}。练习文档特别指出exceptions.rs只在第三部分才需要改动而logger.rs、Cargo.toml、build.rs、memory.ld、Makefile与.cargo/config.toml均无需修改——这是典型的脚手架给你、核心逻辑归你的课程设计。构建与运行在 QEMU 中验证驱动工程脚手架一览模板工程位于 rtc 目录关键文件与作用文件作用是否需要修改src/main.rs入口、初始化、三段任务逻辑需要补全 TODOsrc/pl031.rsPL031 驱动模板中为空需要核心src/exceptions.rs异常/中断向量仅扩展部分需要src/logger.rsUART 日志后端不需要Cargo.toml依赖清单不需要build.rs链接脚本参数不需要memory.ld内存布局image 段位于0x400800002 MiB不需要Makefile构建与运行入口不需要.cargo/config.toml默认目标aarch64-unknown-none不需要依赖方面见 Cargo.tomlaarch64-rt 0.4.3提供入口与异常向量宏arm-gic 0.8.1提供 GICv3 驱动arm-pl011-uart 0.5.0提供串口safe-mmio 0.3.0提供 MMIO 安全访问封装smccc 0.2.3提供 PSCI 系统关机调用system_off::Hvc()另有aarch64-paging、bitflags、chrono、log、spin、zerocopy等辅助 crate。所有依赖均关闭默认特性以保持no_std。一键运行qemu: rtc.bin qemu-system-aarch64 -machine virt,gic-version3 -cpu max -serial mon:stdio -display none -kernel $ -s执行make qemu即可在 QEMU 中运行完整命令见 Makefile。要点-machine virt,gic-version3选择带 GICv3 的 virt 平台与GicV3驱动匹配-cpu max启用尽可能完整的 CPU 特性-serial mon:stdio把串口接到当前终端可看到info!输出的 RTC 时间与等待日志-kernel rtc.bin加载经cargo objcopy转换的裸二进制镜像-s开启 GDB stub端口 1234方便后续单步调试。rtc.bin由make目标生成cargo build后通过cargo objcopy -- -O binary $得到纯二进制。链接脚本由 build.rs 通过cargo:rustc-link-arg-Timage.ld与-Tmemory.ld注入内存布局见 memory.ld——image 段位于0x40080000长度 2 MiB。运行成功后串口应当依次打印RTC: UTC 时间、Waiting for ...、Finished waiting并在中断阶段再次输出Finished waiting与IRQ ...日志最终通过system_off优雅关机。从练习到实战本练习传递的裸机编程要点结合完整解答源码这个练习浓缩了裸机驱动开发中最值得沉淀的四点方法论MMIO 寄存器映射用#[repr(C, align(4))]结构体按偏移描述寄存器文件配合safe-mmio的ReadPure/ReadPureWrite/WriteOnly区分访问权限让编译器替你检查只读寄存器不写入这类错误状态位轮询 vs 中断matched()Raw Status与interrupt_pending()Masked Status的对比直观展示了硬件事实与最终交付到 CPU 的信号之间的分层忙等待与 WFI 睡眠是低功耗场景下两种典型等待策略GIC 中断链路中断源PL031→ 中断控制器GICv3 分发器/重分发器→ CPU 接口 → 异常向量每一环的配置缺一不可——优先级掩码、触发方式、使能开关、irq_enable()总开关任何一环遗漏都可能导致中断石沉大海no_std 生态协作aarch64-rt运行时、aarch64-paging页表、arm-gic中断、arm-pl011-uart串口、smcccPSCI 电源管理各司其职配合spin实现无锁日志、chrono关闭默认特性保持无标准库构成了完整的裸机 Rust 开发范式。如果你希望继续深挖可以参考同目录下的罗盘练习morning.md 与 compass.md对比 I2C 与 MMIO 外设的驱动差异也可以回到裸机章节主页 bare-metal.md 了解课程完整的裸机知识图谱包括no_std、分配器、微控制器等主题。【免费下载链接】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),仅供参考