源码解析)
在 Rust 裸机内核中实现架构计时器抽象rust-raspberrypi-OS-tutorials 教程 07时间戳源码解析【免费下载链接】rust-raspberrypi-OS-tutorials:books: Learn to write an embedded OS in Rust :crab:项目地址: https://gitcode.com/gh_mirrors/ru/rust-raspberrypi-OS-tutorials导读本教程07_timestamps为 rust-raspberrypi-OS-tutorials 系列的第 7 步核心任务是为裸机内核引入计时器硬件抽象层在通用层src/time.rs定义TimeManager接口并在架构层src/_arch/aarch64/time.rs基于 ARM 通用计时器Generic Timer完成实现。随后这套计时器能力被用于两处关键改造——为 UART 打印输出添加启动时间戳info!/warn!宏以及替换 GPIO 驱动中基于 CPU 周期数的粗糙延迟使延迟精度从猜测的周期数提升到精确到微秒。读完本文你将掌握 ARMv8-A 架构计时器的寄存器读取、频率获取、无符号运算转换等底层细节并理解如何在内核中设计通用接口 架构实现的抽象模式。一、为什么需要计时器抽象从周期延迟到时间戳在上一教程06_uart_chainloader中内核的计时手段非常原始GPIO 驱动在配置 PL011 UART 引脚上下拉电阻时依赖cpu::spin_for_cycles(n)这种基于 NOP 指令数量的循环延迟。开发者不得不根据 CPU 主频猜一个周期数代码注释里甚至写着educated guess有根据的猜测且延迟值随 CPU 频率变化而漂移精度无法保证。本教程引入的计时器抽象一次性解决了两个问题精确延迟用time::time_manager().spin_for(Duration::from_micros(1))替代猜测 2000 个周期约等于 1 微秒的写法延迟值与 CPU 频率解耦在任何主频下都精确。时间戳让所有内核日志输出自动携带[ 秒.微秒]前缀例如[ 0.143123] mingo version 0.7.0这对调试启动流程、测量各阶段耗时极具价值。这一改造的完整 diff 可见英文版文档 README.md中文版文档明确说明英文版本为最新 diff 来源。二、架构总览通用接口 架构实现的两层设计本项目采用一致的模块组织原则在 src/main.rs 的模块文档中有详细描述通用子系统代码放在src顶层架构相关代码放在src/_arch/aarch64/通过 Rust 的path属性按编译目标条件引入。计时器子系统正是这一模式的典型示例src/time.rs # 通用计时器接口TimeManager src/_arch/aarch64/time.rs # ARM 架构计时器实现以 crate::time::arch_time 模块路径被引入在 src/time.rs 中可以看到引入方式#[cfg(target_arch aarch64)] #[path _arch/aarch64/time.rs] mod arch_time;随后TimeManager的方法全部转发到arch_time模块调用方无需关心底层是哪个架构pub fn resolution(self) - Duration { arch_time::resolution() } pub fn uptime(self) - Duration { arch_time::uptime() } pub fn spin_for(self, duration: Duration) { arch_time::spin_for(duration) }TimeManager是一个零大小ZST类型通过static TIME_MANAGER: TimeManager TimeManager::new();构造全局单例并用time_manager()返回其引用见 src/time.rs。这是本项目后续章节反复使用的全局实例模式。从命名空间上看整个计时器子系统对外暴露为crate::time::*内核其他模块只需调用crate::time::time_manager()即可无需关心架构细节。三、ARM 架构计时器实现原理src/_arch/aarch64/time.rs3.1 核心寄存器与数据结构ARMv8-A 架构提供了系统级计时器Generic Timer本实现使用两个关键寄存器寄存器作用CNTFRQ_EL0计时器计数频率Hz由固件/硬件配置例如 Raspberry Pi 3 上典型值为 19.2 MHz19200000 HzCNTPCT_EL0物理计数器当前值自设备上电更准确地说自计数器使能以来单调递增代码中定义了一个包装类型GenericTimerCounterValue(u64)来表示原始计数器值src/_arch/aarch64/time.rs并为其实现了Add使用wrapping_add实现加法保证计数器回绕时也不产生 panicL58-L64FromGenericTimerCounterValue for Duration把原始计数值换算成标准库DurationTryFromDuration for GenericTimerCounterValue反向换算用于把要延迟的时长转换成需要等待的计数器增量。3.2 计数值与 Duration 的双向换算计数值 → DurationFrom实现L66-L89let secs counter_value.0.div(frequency); let sub_second_counter_value counter_value.0 % frequency; let nanos unsafe { sub_second_counter_value.unchecked_mul(u64::from(NANOSEC_PER_SEC)) } .div(frequency) as u32; Duration::new(secs, nanos)其中NANOSEC_PER_SEC 1_000_000_000。整秒部分由计数值 / 频率得到亚秒部分纳秒先取余数再乘以 10 亿后除以频率。注释中论证了这里使用unchecked_mul的安全性因为频率最大不超过u32::MAX余数必然小于u32::MAX因此余数 × 10⁹不会溢出u64且最终除法结果一定小于 10 亿可以安全地as u32转换。Duration → 计数值TryFrom实现L95-L118if duration resolution() { return Ok(GenericTimerCounterValue(0)); } if duration max_duration() { return Err(Conversion error. Duration too big); } let counter_value unsafe { duration.unchecked_mul(frequency) }.div(NonZeroU128::from(NANOSEC_PER_SEC));两个边界检查值得注意下限若请求的时长小于计时器分辨率即一个计数周期直接返回 0——这会导致spin_for变为立即返回实际上调用方会先收到一个warn!见下文上限若时长超过GenericTimerCounterValue::MAXu64::MAX个计数周期对应的时长返回错误Conversion error. Duration too big。换算在u128空间中进行以避免溢出时长纳秒× 频率 ≤ Duration::MAX.as_nanos() × u32::MAX u128::MAX同样论证了安全性。3.3 计时器分辨率pub fn resolution() - Duration { Duration::from(GenericTimerCounterValue(1)) }即一个计数器 tick 对应的时间。在启动日志中打印的Architectural timer resolution: 52 ns正是这一数值19.2 MHz 频率下1/19200000 秒 ≈ 52.08 ns。3.4 读取计数器ISB 屏障防止乱序#[inline(always)] fn read_cntpct() - GenericTimerCounterValue { // Prevent that the counter is read ahead of time due to out-of-order execution. barrier::isb(barrier::SY); let cnt CNTPCT_EL0.get(); GenericTimerCounterValue(cnt) }read_cntpct在读取前执行一条全同步的 ISBInstruction Synchronization Barrier指令防止处理器乱序执行导致计数器被提前读取从而保证读取时刻的准确性。isb(barrier::SY)来自aarch64_cpucrate依赖声明见 Cargo.toml。3.5 spin_for忙等实现pub fn spin_for(duration: Duration) { let curr_counter_value read_cntpct(); let counter_value_delta: GenericTimerCounterValue match duration.try_into() { Err(msg) { warn!(spin_for: {}. Skipping, msg); return; } Ok(val) val, }; let counter_value_target curr_counter_value counter_value_delta; // Busy wait. // Read CNTPCT_EL0 directly to avoid the ISB that is part of [read_cntpct]. while GenericTimerCounterValue(CNTPCT_EL0.get()) counter_value_target {} }逻辑清晰记录当前计数值 → 把时长换算成计数器增量 → 计算目标值 → 忙等直到CNTPCT_EL0超过目标。两个细节转换失败处理当请求时长小于分辨率或过大时try_into()返回错误spin_for会调用新增的warn!宏输出警告并跳过延迟。启动日志中的[W 0.145469] Spin duration smaller than architecturally supported, skipping正是内核在kernel_main里刻意测试spin_for(Duration::from_nanos(1))1 纳秒远小于 52 ns 分辨率的产物见 src/main.rs。忙等循环直接读寄存器为避免每次循环都执行 ISB 带来的开销循环体内直接调用CNTPCT_EL0.get()绕开read_cntpct中的屏障。四、计数器频率的获取汇编启动代码写入全局变量一个关键问题是arch_time里的换算依赖CNTFRQ_EL0的频率值但这个值如何进入 Rust 侧答案在启动汇编代码 src/_arch/aarch64/cpu/boot.s 中// Read the CPUs timer counter frequency and store it in ARCH_TIMER_COUNTER_FREQUENCY. // Abort if the frequency read back as 0. ADR_REL x1, ARCH_TIMER_COUNTER_FREQUENCY // provided by aarch64/time.rs mrs x2, CNTFRQ_EL0 cmp x2, xzr b.eq .L_parking_loop str w2, [x1]流程为_start完成 BSS 清零、栈指针设置后读取CNTFRQ_EL0若为 0异常情况则进入停车循环wfe挂起否则把频率值写入 Rust 侧声明的全局变量#[no_mangle] static ARCH_TIMER_COUNTER_FREQUENCY: NonZeroU32 NonZeroU32::MIN;Rust 侧通过core::ptr::read_volatile读取该变量src/_arch/aarch64/time.rs并注释说明使用 volatile 读取是为了防止编译器把这个从未被 Rust 代码写入的静态变量优化掉。同时本教程的启动代码还从绝对寻址ADR_ABS全面切换为 PC 相对寻址ADR_REL并删除了上一教程中的二进制重定位逻辑——这意味着07_timestamps的镜像可以像教程 05 之前那样直接从固件默认加载地址运行这正是它能作为 chainboot 目标二进制被直接加载的前提。五、带时间戳的打印宏info! 与新增的 warn!5.1 宏实现剖析本教程在 src/print.rs 中新增了两个带时间戳的打印宏info!与warn!。以info!为例#[macro_export] macro_rules! info { ($string:expr) ({ let timestamp $crate::time::time_manager().uptime(); $crate::print::_print(format_args_nl!( concat!([ {:3}.{:06}] , $string), timestamp.as_secs(), timestamp.subsec_micros(), )); }); ($format_string:expr, $($arg:tt)*) ({ let timestamp $crate::time::time_manager().uptime(); // ... 带格式化参数的版本追加 $($arg)* }) }关键点宏先调用time_manager().uptime()获取启动以来含固件与 bootloader 消耗的时间的时长使用concat!拼接格式前缀[ {:3}.{:06}] {:3}右对齐 3 位秒数{:06}补齐 6 位微秒从而得到[ 0.143123]这样的输出通过$crate::time::...路径调用保证宏在 crate 外部展开时也能正确定位模块。warn!宏与info!结构完全相同只是前缀改为[W {:3}.{:06}]用W标识警告级别。5.2 输出效果启动日志完美展示了两种宏的效果来自 README 的 chainboot 运行输出[ 0.143123] mingo version 0.7.0 [ 0.143323] Booting on: Raspberry Pi 3 [ 0.143778] Architectural timer resolution: 52 ns [ 0.144352] Drivers loaded: [ 0.144688] 1. BCM PL011 UART [ 0.145110] 2. BCM GPIO [W 0.145469] Spin duration smaller than architecturally supported, skipping [ 0.146313] Spinning for 1 second [ 1.146715] Spinning for 1 second [ 2.146938] Spinning for 1 second可以清晰看到info!输出以[开头warn!以[W开头kernel_main的主循环每隔 1 秒打印一次Spinning for 1 second时间戳严格递增约 1 秒直观验证了计时器的准确性src/main.rs。panic_wait.rs中的 panic 输出也同样加上了时间戳前缀见英文版 README 的 diff确保内核崩溃时也能定位到发生时刻。六、GPIO 驱动的精度改进本教程消除了上一教程中基于周期数的延迟。对比 src/bsp/device_driver/bcm/bcm2xxx_gpio.rs 的disable_pud_14_15_bcm2837之前的做法教程 06根据 Wikipedia 上 RPi 主频猜测延迟为 2000 个周期注释坦承这是 educated guess并假设 CPU 以 2 GHz 运行时约等于 1 微秒——主频变化就会失准。本教程的做法// The Linux 2837 GPIO driver waits 1 µs between the steps. const DELAY: Duration Duration::from_micros(1); self.registers.GPPUD.write(GPPUD::PUD::Off); time::time_manager().spin_for(DELAY); self.registers .GPPUDCLK0 .write(GPPUDCLK0::PUDCLK15::AssertClock GPPUDCLK0::PUDCLK14::AssertClock); time::time_manager().spin_for(DELAY);现在延迟直接声明为1 µs参考 Linux BCM2837 GPIO 驱动的步进间隔由硬件计时器精确保证与 CPU 频率无关——这正是 README 中所说的 boosts accuracy提升准确性。同时旧的cpu::spin_for_cycles函数及其在 src/_arch/aarch64/cpu.rs 与 src/cpu.rs 中的 re-export 被一并删除。此外UART 驱动bcm2xxx_pl011_uart.rs中read_char更名为read_char_converting新增了回车符\r到换行符\n的转换配合 chainboot 串口终端使用。七、运行与验证7.1 在真实硬件上测试chainboot本教程沿用了上一教程引入的 chainboot 流程通过minipush将编译好的内核镜像直接推送到树莓派并执行无需 SD 卡$ make chainbootMakefileMakefile中chainboot目标定义为chainboot: $(KERNEL_BIN) $(DOCKER_CHAINBOOT) $(EXEC_MINIPUSH) $(DEV_SERIAL) $(KERNEL_BIN)默认BSP rpi3、DEV_SERIAL /dev/ttyUSB0可通过命令行覆盖如BSPrpi4 make chainboot。整个交互流程为Minipush 等待串口设备 → 提示给目标板上电 → 请求并推送二进制日志显示Pushing 12 KiB→ 目标加载执行 → 串口输出带时间戳的内核日志即第五节展示的内容。7.2 自动化启动测试本教程同时加入了自动化启动测试。测试期望字符串定义在 tests/boot_test_string.rbEXPECTED_PRINT Spinning for 1 second即验证内核成功启动并进入 1 秒循环。运行方式$ make test # 等价于 make test_boottest_boot目标通过../common/tests/dispatch.rb调度脚本启动 QEMU-M raspi3并匹配输出见 Makefile。注意该 Makefile 相比上一教程引入了两个变化测试调度统一走common/tests/dispatch.rb并且chainboot现在直接推送编译出的kernel8.img而非预制的 demo payload。若选择rpi4则QEMU_MACHINE_TYPE为空make test会提示 This board is not yet supported for QEMU.。7.3 版本与依赖本教程的 crate 版本升级到0.7.0见 Cargo.toml并新增了若干实验性 feature 声明const_option、nonzero_min_max、unchecked_mathNonZeroU32::MIN、unchecked_mul等依赖这些 nightly feature见 src/main.rs。目标平台为aarch64-unknown-none-softfloatCPU 为cortex-a53rpi3或cortex-a72rpi4。八、小结教程 07 为内核建立了可复用的时间子系统抽象层TimeManager通用接口src/time.rs ARM Generic Timer 实现src/_arch/aarch64/time.rs通过CNTPCT_EL0计数值与Duration的精确双向换算提供uptime()、resolution()、spin_for()三个能力频率来源启动汇编src/_arch/aarch64/cpu/boot.s在跳转 Rust 前把CNTFRQ_EL0写入 Rust 静态变量日志升级info!/warn!宏为所有内核输出添加[ 秒.微秒]时间戳src/print.rs精度提升GPIO 驱动的微秒级延迟不再依赖 CPU 频率猜测src/bsp/device_driver/bcm/bcm2xxx_gpio.rs。这套通用接口 架构实现 全局单例的模式将在后续教程中被反复使用——从异常处理、MMU 到时钟回调timer callbacks计时器子系统始终是内核基础设施的核心组成部分。你可以通过make qemu在 QEMU 中快速观察带时间戳的启动日志或通过make chainboot在真实树莓派上体验。【免费下载链接】rust-raspberrypi-OS-tutorials:books: Learn to write an embedded OS in Rust :crab:项目地址: https://gitcode.com/gh_mirrors/ru/rust-raspberrypi-OS-tutorials创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考