驱动)
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本篇技术指南以 comprehensive-rust 课程中src/exercises/bare-metal/rtc.md练习文档为核心完整讲解如何为 QEMU aarch64 virt 虚拟机上的 PL031 实时时钟Real-Time Clock设备编写裸机驱动。通过本练习你将掌握 MMIO 寄存器映射驱动的写法、基于chrono的时间格式化、利用 match 寄存器与 raw interrupt status 实现忙等待busy-wait以及进阶的 GIC 中断配置与wfi低功耗等待最终在 QEMU 中运行完整的三阶段 RTC 驱动。练习背景QEMU virt 机器上的 PL031Arm 的 PL031 是 PrimeCell 家族中一款成熟的实时时钟外设广泛出现在 Arm 参考平台上。QEMU 的aarch64virt 虚拟机在固定地址0x9010000处暴露了一个 PL031 实时时钟。裸机程序中没有任何操作系统帮忙管理时间因此要获得当前时间或实现定时功能就必须直接操作这块 MMIO 设备。本练习位于 comprehensive-rust 课程的裸机bare-metal章节是下午练习afternoon exercise的一部分与 RTC 练习入口文档、下午练习总览 以及 RTC 练习参考答案 相互呼应。练习要求依次完成三个递进目标使用驱动读取当前时间并通过chronocrate 格式化后打印到串口控制台使用 match 寄存器与 raw interrupt status忙等待到未来某个时刻例如 3 秒后循环内调用core::hint::spin_loop扩展任务可选启用并处理 RTC 匹配产生的中断借助arm-giccrate 配置 Arm Generic Interrupt ControllerGIC将 RTC 中断接到 GIC 的IntId::spi(2)开启后用arm_gic::wfi()让核心睡眠直到中断到来。练习工程结构你需要哪些文件课程的 RTC 练习采用no_stdno_main的裸机工程结构目标平台是 aarch64。仓库中src/exercises/bare-metal/rtc/目录提供了完整的工程包含参考答案其中各文件职责如下文件职责是否需要修改src/main.rs入口函数、UART/GIC 初始化、三阶段主流程是练习主体src/exceptions.rsaarch64 异常向量表实现仅第 3 部分需要src/logger.rs基于 PL011 UART 的log输出后端否src/pl031.rsPL031 寄存器布局与Rtc驱动封装是编写驱动核心Cargo.toml工程依赖声明否build.rs构建脚本否memory.ld链接脚本内存布局否Makefile构建与 QEMU 运行入口否.cargo/config.tomlCargo 目标与链接器配置否注意仓库中src/exercises/bare-metal/rtc/src/pl031.rs与main.rs已经包含完整答案通过// ANCHOR: solution注释标记可供对照学习练习时你可以先自行实现再参照仓库中的参考实现。Cargo.toml 依赖清单工程依赖见 rtc/Cargo.toml揭示了这次练习用到的全部关键库[package] name rtc version 0.1.0 edition 2024 publish false [dependencies] aarch64-paging { version 0.12.1, default-features false } aarch64-rt 0.4.3 arm-gic 0.8.1 arm-pl011-uart 0.5.0 bitflags 2.11.1 chrono { version 0.4.44, default-features false } log 0.4.30 safe-mmio 0.3.0 smccc 0.2.3 spin 0.12.0 zerocopy 0.8.50aarch64-rt提供异常向量表宏exception_handlers!、入口宏entry!以及初始页表initial_pagetable!aarch64-paging描述 EL1 页表属性El1Attributes用于建立初始恒等映射arm-gicGICv3 驱动负责中断控制器配置与wfi()arm-pl011-uartPL011 串口驱动用于日志输出chrono时间格式化注意default-features false裸机环境下不依赖标准库时钟safe-mmio提供UniqueMmioPointer与ReadPure/ReadPureWrite/WriteOnly等字段类型用类型安全的方式访问 MMIO 寄存器log日志门面配合logger.rs输出到 UARTsmccc通过 HVCHypervisor Call调用 PSCI 的system_off关闭系统。链接脚本与内存布局memory.ld 将镜像放在 QEMU virt 机器的 RAM 起始处MEMORY { image : ORIGIN 0x40080000, LENGTH 2M }0x40080000是 QEMU virt 机器上典型的内核加载地址2 MiB 的可加载空间足够容纳这个小型裸机镜像。第一步编写 PL031 寄存器映射驱动PL031 寄存器布局PL031 的寄存器全部以 32 位字对齐。参考实现 pl031.rs 使用#[repr(C, align(4))]结构体精确描述寄存器布局#[repr(C, align(4))] pub struct Registers { /// Data register当前 RTC 计数值 dr: ReadPureu32, /// Match register匹配值达到后产生中断 mr: ReadPureWriteu32, /// Load register加载初始计数值 lr: ReadPureWriteu32, /// Control register控制位 cr: ReadPureWriteu8, _reserved0: [u8; 3], /// Interrupt Mask Set or Clear register中断掩码 imsc: ReadPureWriteu8, _reserved1: [u8; 3], /// Raw Interrupt Status原始中断状态不受掩码影响 ris: ReadPureu8, _reserved2: [u8; 3], /// Masked Interrupt Status掩码后的中断状态 mis: ReadPureu8, _reserved3: [u8; 3], /// Interrupt Clear Register写 1 清除中断 icr: WriteOnlyu8, _reserved4: [u8; 3], }各寄存器位 0bit 0是核心状态位寄存器访问类型位 0 含义DRData只读当前 RTC 计数值Unix 时间戳形式MRMatch读写设定匹配时刻RTC 计数值与之相等时置位状态IMSCInterrupt Mask Set/Clear读写1 使能中断0 关闭中断RISRaw Interrupt Status只读匹配发生后置 1无论中断是否使能MISMasked Interrupt Status只读仅当匹配发生且中断使能时为 1ICRInterrupt Clear只写写 1 清除挂起的中断safe-mmio的字段类型让每个寄存器具备编译期检查的访问语义ReadPure只能读ReadPureWrite可读可写WriteOnly只能写。Rtc 驱动 APIRtc结构体封装了上述寄存器对外提供如下方法源码见 pl031.rspub fn new(registers: UniqueMmioPointera, Registers) - Self // 读取当前 RTC 计数值Unix 秒 pub fn read(self) - u32 // 写入匹配值当 RTC 计数值与之相等时会生成中断若已使能 pub fn set_match(mut self, value: u32) // 返回是否已匹配读 RIS 位 0与中断是否使能无关 pub fn matched(self) - bool // 返回是否有中断挂起读 MIS 位 0等价于 matched 且中断已使能 pub fn interrupt_pending(self) - bool // 设置/清除中断掩码true 使能中断false 关闭 pub fn enable_interrupt(mut self, mask: bool) // 清除挂起中断写 ICR 位 0 为 1 pub fn clear_interrupt(mut self)其中matched()与interrupt_pending()的区别正是练习第 2、3 部分的落脚点忙等待阶段只关心硬件是否到点读RIS而中断阶段关心是否产生了可响应的中断读MIS。第二步裸机入口与基础环境初始化初始页表设备内存映射main.rs 顶部用initial_pagetable!建立 1 GiB 粒度的初始恒等映射initial_pagetable!({ let mut idmap [0; 512]; // 1 GiB of device memory. idmap[0] DEVICE_ATTRIBUTES.bits(); // 1 GiB of normal memory. idmap[1] MEMORY_ATTRIBUTES.bits() | 0x40000000; // Another 1 GiB of device memory starting at 256 GiB. idmap[256] DEVICE_ATTRIBUTES.bits() | 0x4000000000; InitialPagetable(idmap) });idmap[0]映射0x0起的 1 GiB 设备内存覆盖 GIC0x800_0000附近的 distributor/redistributor、PL011 UART0x900_0000与 PL031 RTC0x901_0000等外设地址idmap[1]映射0x40000000起的 1 GiB 普通内存| 0x40000000表示页表项输出地址即镜像所在的 RAM 区域idmap[256]映射0x4000000000256 GiB起的另一段设备内存。设备内存使用DEVICE_ATTRIBUTESVALID | ATTRIBUTE_INDEX_0 | ACCESSED | UXN普通内存使用MEMORY_ATTRIBUTESVALID | ATTRIBUTE_INDEX_1 | INNER_SHAREABLE | ACCESSED | NON_GLOBAL。这对 MMIO 访问至关重要——设备寄存器绝不能被缓存必须按设备内存语义访问。地址常量与入口const PL011_BASE_ADDRESS: NonNullPL011Registers NonNull::new(0x900_0000 as _).unwrap(); const PL031_BASE_ADDRESS: NonNullpl031::Registers NonNull::new(0x901_0000 as _).unwrap(); const PL031_IRQ: IntId IntId::spi(2); entry!(main); fn main(x0: u64, x1: u64, x2: u64, x3: u64) - ! { let uart unsafe { Uart::new(UniqueMmioPointer::new(PL011_BASE_ADDRESS)) }; logger::init(uart, LevelFilter::Trace).unwrap(); info!(main({:#x}, {:#x}, {:#x}, {:#x}), x0, x1, x2, x3); ... }入口函数接收启动时来自固件的x0–x3四个参数首先初始化 PL011 UART 并挂载日志后端见 logger.rs其内部用SpinMutex保护 UART实现Logtrait 后逐行输出[级别] 消息随后打印入口参数。注意地址常量通过unsafe的NonNull::new(...).unwrap()构造并配合逐条// SAFETY:注释说明该地址确为对应设备基地址且无人同时访问该区间——这是 unsafe 代码中安全前置条件precondition的规范写法。GIC 初始化在进行中断相关操作前主流程先初始化 Arm Generic Interrupt Controller v3let mut gic unsafe { GicV3::new( UniqueMmioPointer::new(GICD_BASE_ADDRESS), GICR_BASE_ADDRESS, 1, // 核心数 false, // 非安全模式相关配置 ) }; gic.setup(0);其中GICD_BASE_ADDRESS 0x800_0000distributor、GICR_BASE_ADDRESS 0x80A_0000redistributor这两个地址同样来自 QEMU virt 机器的设备树布局。GICv3 是 aarch64 上中断路由与分发的中枢PL031 的匹配中断最终正是经由它送达 CPU。第三步目标 1读取并打印当前时间完成初始化后读取 RTC 计数值并用chrono格式化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()从 PL031DR寄存器读出当前计数值Unix 秒Utc.timestamp_opt(timestamp.into(), 0)将 u32 计数值转换为chrono的DateTimeUtc.into()把u32转成i64timestamp_opt返回LocalResult配合unwrap()处理边缘时刻如闰秒歧义。QEMU virt 机器的 PL031 默认从虚拟机启动时刻开始计数因此打印出的时间是从启动起的相对秒数。运行后你会在串口看到类似RTC: 2026-…的 UTC 时间行。第四步目标 2用 match 寄存器忙等待 3 秒接着代码演示了不依赖中断的定时等待——利用MR与RIS// Wait for 3 seconds, without interrupts. let target timestamp 3; rtc.set_match(target); info!(Waiting for {}, Utc.timestamp_opt(target.into(), 0).unwrap()); trace!( matched{}, interrupt_pending{}, rtc.matched(), rtc.interrupt_pending() ); while !rtc.matched() { spin_loop(); } trace!( matched{}, interrupt_pending{}, rtc.matched(), rtc.interrupt_pending() ); info!(Finished waiting);要点rtc.set_match(timestamp 3)把目标时刻写入MR循环条件读取rtc.matched()即 PL031RIS寄存器的位 0——该位不受中断掩码影响只要 RTC 计数值与匹配值相等就会置位因此即使中断未使能也能感知到点循环体内调用core::hint::spin_loop()该指令向 CPU 提示当前处于自旋等待spin-wait状态有助于降低功耗与提高多核公平性前后两次trace!输出对比matched与interrupt_pending状态在未使能中断时匹配发生后matched变为true而interrupt_pending读MIS仍为false直观验证了RIS与MIS的语义差异。第五步扩展中断驱动等待与 wfi 睡眠练习的第 3 部分是加分项把忙等待升级为真正的中断驱动让 CPU 在等待期间进入休眠由 GIC 转发 RTC 中断唤醒核心。配置 RTC 中断路径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();GicCpuInterface::set_priority_mask(0xff)将 CPU 接口优先级掩码设为 0xff在 arm-gic 中数值越大优先级越低确保任意优先级的中断都能通过set_interrupt_priority(PL031_IRQ, None, 0x80)为 SPI 2 设置优先级 0x80set_trigger(..., Trigger::Level)PL031 的中断是电平触发level-sensitive必须按电平触发配置irq_enable()开启当前核的 IRQ 总开关enable_interrupt(PL031_IRQ, None, true)在 GIC distributor 侧使能该中断。其中PL031_IRQ IntId::spi(2)对应练习文档说明的RTC 中断接到 GIC 的 SPI 2——SPIShared Peripheral Interrupt编号来自 QEMU virt 机器的中断路由表。等待下一个 3 秒并休眠// Wait another 3 seconds for an interrupt. let target timestamp 6; info!(Waiting for {}, Utc.timestamp_opt(target.into(), 0).unwrap()); rtc.set_match(target); rtc.clear_interrupt(); rtc.enable_interrupt(true); trace!(...); while !rtc.interrupt_pending() { wfi(); } trace!(...); info!(Finished waiting);set_match(target)设定新匹配时刻clear_interrupt()写ICR清除前一次可能残留的挂起状态enable_interrupt(true)置位IMSC使能 PL031 的中断输出到 GIC循环条件改为!rtc.interrupt_pending()即读MIS——只有匹配发生且中断使能时该位才为 1循环体内调用arm_gic::wfi()Wait For Interrupt核心进入低功耗睡眠直到收到中断才被唤醒比忙等待大幅省电。异常向量表与中断处理中断唤醒后由异常向量表接管。仓库中的 exceptions.rs 使用aarch64-rt的exception_handlers!宏定义了完整的 EL1 异常向量表struct Handlers; impl ExceptionHandlers for Handlers { extern C fn sync_current(_state: RegisterStateRef) { ... } extern C fn irq_current(_state: RegisterStateRef) { trace!(irq_current); let intid GicCpuInterface::get_and_acknowledge_interrupt(InterruptGroup::Group1) .expect(No pending interrupt); info!(IRQ {intid:?}); } extern C fn fiq_current(_state: RegisterStateRef) { ... } ... } exception_handlers!(Handlers);当 RTC 中断到来时CPU 进入irq_current处理函数通过GicCpuInterface::get_and_acknowledge_interrupt(InterruptGroup::Group1)读取并应答中断得到IntId应为IntId::spi(2)并打印。该函数返回Option用expect处理无挂起中断却进入 IRQ的异常情况——这正是练习文档中只应在第 3 部分修改 exceptions.rs的原因前两个目标根本不依赖中断向量。其余同步异常sync_current、FIQ、SError 以及来自低特权级lower EL的异常统一打印错误后通过 PSCIsystem_off关机保证失败可观测。构建与运行make qemu工程通过 Makefile 一站式完成编译、镜像生成与 QEMU 启动见 rtc/Makefile.PHONY: build qemu_minimal qemu qemu_logger all: rtc.bin build: cargo build rtc.bin: build cargo objcopy -- -O binary $ qemu: rtc.bin qemu-system-aarch64 -machine virt,gic-version3 -cpu max -serial mon:stdio -display none -kernel $ -s clean: cargo clean rm -f *.bin执行make qemu即可在 QEMU 中运行cargo build交叉编译.cargo/config.toml已指定 aarch64 目标与链接脚本cargo objcopy -- -O binary rtc.bin从 ELF 提取裸二进制镜像qemu-system-aarch64以-machine virt,gic-version3必须使用 GICv3与代码中GicV3驱动匹配、-cpu max、-serial mon:stdio将串口接到当前终端、-kernel rtc.bin加载镜像、-s开启 1234 端口 GDB stub 方便调试。运行成功后串口输出应依次呈现入口参数日志 → 初始 RTC 时间 → Waiting for … → 忙等待结束 → 中断等待 → 两次IRQ IntId::spi(2)相关日志 →system_off关机。整个流程完整演示了轮询polling→ 中断驱动interrupt-driven两种外设等待模式的迁移。小结本练习沉淀的裸机技能类型安全的 MMIO 驱动用#[repr(C, align(4))]结构体描述寄存器布局配合safe-mmio的读写权限类型让寄存器访问具备编译期检查时间处理裸机下用chronodefault-features falseUtc::timestamp_opt把硬件计数值格式化为可读时间两种等待范式基于RIS的忙等待配合spin_loop与基于 GIC MISwfi的中断驱动等待理解RAW status与MASKED status的本质区别GICv3 中断链路从IntId::spi(2)出发依次配置优先级、触发方式、总开关与 distributor 使能再在异常向量表中应答中断安全注释习惯所有 MMIO 地址构造均使用unsafe并附// SAFETY:前置条件说明这是 comprehensive-rust 全程强调的安全前置条件safety preconditions实践。如果想挑战自己可以先隐藏pl031.rs自行实现驱动再对照仓库中带ANCHOR: solution的参考代码main.rs、pl031.rs与 solutions-afternoon.md 检查差异。更进一步的延伸可以阅读课程中 bare-metal 章节 下的 uart 驱动、mmio 访问 与 GIC 相关材料将本练习的零散知识点串联成完整的裸机系统认知。【免费下载链接】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),仅供参考