From 427b188effd4fe6ac7274567c88b06bd2439fe7e Mon Sep 17 00:00:00 2001 From: James Munns Date: Fri, 19 Jun 2026 12:37:51 +0200 Subject: [PATCH 1/9] Initial attempt at timer based boot --- drv/stm32h7-startup/src/lib.rs | 60 +++++++++++++++++++++++++----- lib/measurement-handoff/src/lib.rs | 8 +++- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/drv/stm32h7-startup/src/lib.rs b/drv/stm32h7-startup/src/lib.rs index 3ea7247c3..916847772 100644 --- a/drv/stm32h7-startup/src/lib.rs +++ b/drv/stm32h7-startup/src/lib.rs @@ -15,6 +15,19 @@ use stm32h7::stm32h753 as device; #[cfg(any(feature = "h743", feature = "h753"))] #[pre_init] unsafe fn system_pre_init() { + // /!\ EXTREME DANGER WARNING /!\ + // + // We are running this function *before* the startup routine has completed, + // meaning that `static`s have NOT been initialized. This is extremely + // likely to be unsound in the general case, and should probably be + // rewritten in `global_asm!` some day, as the `pre_init` macro is now + // deprecated. + // + // Until that day, you MUST NOT read or write any `static` variables, as + // that would be IMMEDIATE Undefined Behavior. Tread carefully! + // + // /!\ EXTREME DANGER WARNING /!\ + // // Configure the power supply to latch the LDO on and prevent further // reconfiguration. // @@ -111,22 +124,49 @@ pub fn system_init_custom( cp.SCB.enable_icache(); cp.SCB.enable_dcache(&mut cp.CPUID); + // Hand-build TIM5 as a 32-bit rolling timer at 1 MHz. Start by enabling + // TIM5 is on APB1, which at boot is undivided from the default 64MHz + // HSI clock source. This *will* change when we reconfigure clocks later! + // + // Start by enabling the peripheral in RCC and toggling reset + p.RCC.apb1lenr.modify(|_r, w| w.tim5en().enabled()); + p.RCC.apb1lrstr.modify(|_r, w| w.tim5rst().set_bit()); + p.RCC.apb1lrstr.modify(|_r, w| w.tim5rst().clear_bit()); + + // Now, configure it for an upcounting rolling mode + // + // Disable counter + p.TIM5.cr1.modify(|_r, w| w.cen().disabled()); + // Set counter to zero + p.TIM5.cnt.modify(|_r, w| w.cnt().bits(0)); + // Set prescaler to (FREQ / 1M) - 1, (64M / 1M) - 1 = 63, as the counter + // resets to 0 AFTER counting this number. + p.TIM5.psc.write(|w| w.psc().bits(63)); + // Set auto-reload to u32::MAX + p.TIM5.arr.write(|w| w.arr().bits(u32::MAX)); + // Generate update (latch the PSC and ARR values) + p.TIM5.egr.write(|w| w.ug().set_bit()); + // Before doing anything else, check for a measurement handoff token #[cfg(feature = "measurement-handoff")] unsafe { - // After each delay, we'll wait roughly 200 ms. We double the naive - // cycle count because the STM32H7 may (under some circumstances) - // dual-issue instructions in the delay loop, which would make the loop - // run twice as fast as expected. We'd rather the loop sometimes run - // twice as *slow*, because that just slows down SP boot in cases where - // the RoT is not present; if the loop is twice as fast, the SP can time - // out before RoT comes up at all, which is a much worse failure mode. - const DELAY_CYCLES: u32 = 12860000 * 2; + // Start counting! + p.TIM5.cr1.modify(|_r, w| w.cen().enabled()); + + // After each delay, we'll wait roughly 200 ms. + // + // You might ask yourself, "how do we have a RETRY_COUNT if the closure + // diverges"? Well! `measurement_handoff::check` stores the iteration + // counter in a linker location that persists across soft-reboots. + const DELAY_MICROS: u32 = 200 * 1_000; const RETRY_COUNT: u32 = 20; - measurement_handoff::check(RETRY_COUNT, || { - cortex_m::asm::delay(DELAY_CYCLES); + measurement_handoff::check(RETRY_COUNT, &p, |p| { + while p.TIM5.cnt.read().bits() < DELAY_MICROS {} cortex_m::peripheral::SCB::sys_reset() }); + + // Disable timer, we will need to reconfigure it after setting up PLL + p.TIM5.cr1.modify(|_r, w| w.cen().disabled()); } // The H7 -- and perhaps the Cortex-M7 -- has the somewhat annoying diff --git a/lib/measurement-handoff/src/lib.rs b/lib/measurement-handoff/src/lib.rs index 300485f71..6ad3c70fe 100644 --- a/lib/measurement-handoff/src/lib.rs +++ b/lib/measurement-handoff/src/lib.rs @@ -75,7 +75,11 @@ pub static mut HUBRIS_MEASUREMENT_RESULT: Option = None; /// measurement is valid, or `false` if we exceeded `retry_count`. /// /// `delay_and_reset` should include a delay, to give the RoT time to boot. -pub unsafe fn check(retry_count: u32, delay_and_reset: fn() -> !) -> bool { +pub unsafe fn check( + retry_count: u32, + arg: T, + delay_and_reset: fn(T) -> !, +) -> bool { let ptr: *mut u32 = &raw mut __REGION_DTCM_BASE as *mut _; let end: *mut u32 = &raw mut __REGION_DTCM_END as *mut _; assert!(ptr == measurement_token::SP_ADDR); @@ -133,7 +137,7 @@ pub unsafe fn check(retry_count: u32, delay_and_reset: fn() -> !) -> bool { next ^ COUNTER_TAG, ); } - delay_and_reset(); + delay_and_reset(arg); } } } From b60cadc8f6f9fc162c49fdaf4834c083345542bd Mon Sep 17 00:00:00 2001 From: James Munns Date: Mon, 22 Jun 2026 12:02:22 +0200 Subject: [PATCH 2/9] Implement blocking timer helper functions --- drv/stm32h7-startup/src/lib.rs | 61 +++++++++++++++++++++++++----- lib/measurement-handoff/src/lib.rs | 8 +--- 2 files changed, 54 insertions(+), 15 deletions(-) diff --git a/drv/stm32h7-startup/src/lib.rs b/drv/stm32h7-startup/src/lib.rs index 916847772..42cb99782 100644 --- a/drv/stm32h7-startup/src/lib.rs +++ b/drv/stm32h7-startup/src/lib.rs @@ -11,6 +11,7 @@ use stm32h7::stm32h743 as device; #[cfg(feature = "h753")] use stm32h7::stm32h753 as device; +use stm32h7::stm32h753::TIM5; #[cfg(any(feature = "h743", feature = "h753"))] #[pre_init] @@ -137,19 +138,19 @@ pub fn system_init_custom( // // Disable counter p.TIM5.cr1.modify(|_r, w| w.cen().disabled()); - // Set counter to zero - p.TIM5.cnt.modify(|_r, w| w.cnt().bits(0)); - // Set prescaler to (FREQ / 1M) - 1, (64M / 1M) - 1 = 63, as the counter - // resets to 0 AFTER counting this number. - p.TIM5.psc.write(|w| w.psc().bits(63)); // Set auto-reload to u32::MAX p.TIM5.arr.write(|w| w.arr().bits(u32::MAX)); - // Generate update (latch the PSC and ARR values) - p.TIM5.egr.write(|w| w.ug().set_bit()); // Before doing anything else, check for a measurement handoff token #[cfg(feature = "measurement-handoff")] unsafe { + // Set counter to zero + p.TIM5.cnt.modify(|_r, w| w.cnt().bits(0)); + // Set prescaler to (FREQ / 1M) - 1, (64M / 1M) - 1 = 63, as the counter + // resets to 0 AFTER counting this number. + p.TIM5.psc.write(|w| w.psc().bits(63)); + // Generate update (latch the PSC and ARR values) + p.TIM5.egr.write(|w| w.ug().set_bit()); // Start counting! p.TIM5.cr1.modify(|_r, w| w.cen().enabled()); @@ -160,8 +161,8 @@ pub fn system_init_custom( // counter in a linker location that persists across soft-reboots. const DELAY_MICROS: u32 = 200 * 1_000; const RETRY_COUNT: u32 = 20; - measurement_handoff::check(RETRY_COUNT, &p, |p| { - while p.TIM5.cnt.read().bits() < DELAY_MICROS {} + measurement_handoff::check(RETRY_COUNT, || { + blocking_delay_micros(DELAY_MICROS); cortex_m::peripheral::SCB::sys_reset() }); @@ -387,8 +388,50 @@ pub fn system_init_custom( p.RCC.d2ccip2r.modify(|_, w| w.rngsel().pll1_q()); // Hello from target speed! + // + // Set up the clock for the new nominal APB1 frequency of 100MHz + // + // Set counter to zero + p.TIM5.cnt.modify(|_r, w| w.cnt().bits(0)); + // Set prescaler to (FREQ / 1M) - 1, (100M / 1M) - 1 = 99, as the counter + // resets to 0 AFTER counting this number. + p.TIM5.psc.write(|w| w.psc().bits(99)); + // Generate update (latch the PSC and ARR values) + p.TIM5.egr.write(|w| w.ug().set_bit()); + // Start counting! + p.TIM5.cr1.modify(|_r, w| w.cen().enabled()); // Hand the peripherals back in case the board-specific setup code needs to // do anything. p } + +/// Obtain the current count value of TIM5, which is a 32-bit timer that ticks +/// at a rate of 1MHz. +#[inline(always)] +pub fn get_rolling_micros_since_boot() -> u32 { + let tim5 = unsafe { &*TIM5::ptr() }; + tim5.cnt.read().bits() +} + +/// Perform a blocking delay for the given number of microseconds. +#[inline] +pub fn blocking_delay_micros(micros: u32) { + let start = get_rolling_micros_since_boot(); + loop { + let now = get_rolling_micros_since_boot(); + + // Since this is a rolling timer, we can perform a wrapping sub to + // obtain the elapsed amount of time, even if we have crossed the roll- + // over point, e.g.: + // + // start = 0xFFFF_FFFE + // now = 0x0000_0080 + // + // now.wrapping_sub(start) => 0x82 + let elapsed = now.wrapping_sub(start); + if elapsed >= micros { + break; + } + } +} diff --git a/lib/measurement-handoff/src/lib.rs b/lib/measurement-handoff/src/lib.rs index 6ad3c70fe..300485f71 100644 --- a/lib/measurement-handoff/src/lib.rs +++ b/lib/measurement-handoff/src/lib.rs @@ -75,11 +75,7 @@ pub static mut HUBRIS_MEASUREMENT_RESULT: Option = None; /// measurement is valid, or `false` if we exceeded `retry_count`. /// /// `delay_and_reset` should include a delay, to give the RoT time to boot. -pub unsafe fn check( - retry_count: u32, - arg: T, - delay_and_reset: fn(T) -> !, -) -> bool { +pub unsafe fn check(retry_count: u32, delay_and_reset: fn() -> !) -> bool { let ptr: *mut u32 = &raw mut __REGION_DTCM_BASE as *mut _; let end: *mut u32 = &raw mut __REGION_DTCM_END as *mut _; assert!(ptr == measurement_token::SP_ADDR); @@ -137,7 +133,7 @@ pub unsafe fn check( next ^ COUNTER_TAG, ); } - delay_and_reset(arg); + delay_and_reset(); } } } From 341c1c05b080ca6a178df0ef5ed7e6590500068b Mon Sep 17 00:00:00 2001 From: James Munns Date: Mon, 22 Jun 2026 15:24:25 +0200 Subject: [PATCH 3/9] Add unsafe configure helper, do configuration before `system_init_custom`. --- app/cosmo/src/main.rs | 9 ++ app/gimlet/src/main.rs | 33 +++---- app/grapefruit/src/main.rs | 9 ++ app/medusa/src/main.rs | 9 ++ app/minibar/src/main.rs | 15 ++- app/observer/src/main.rs | 9 ++ app/psc/src/main.rs | 17 +++- app/sidecar/src/main.rs | 13 ++- drv/stm32h7-startup/src/lib.rs | 163 +++++++++++++++++++++------------ 9 files changed, 195 insertions(+), 82 deletions(-) diff --git a/app/cosmo/src/main.rs b/app/cosmo/src/main.rs index ed9a3d989..ea6f4ba9d 100644 --- a/app/cosmo/src/main.rs +++ b/app/cosmo/src/main.rs @@ -34,6 +34,15 @@ fn system_init() { let cp = cortex_m::Peripherals::take().unwrap(); let p = device::Peripherals::take().unwrap(); + // Start the higher resolution timer with the default APB1 clock rate of + // 64MHz + // + // SAFETY: We do not carry any "instant" values across this point (as they + // would be invalidated here!), and we do not re-use TIM5 for anything. + unsafe { + drv_stm32h7_startup::rolling_timer::configure_tim5(&p, 64); + } + // Check the package we've been flashed on (Cosmo boards use BGA240) // // We need to turn the SYSCFG block on to do this. diff --git a/app/gimlet/src/main.rs b/app/gimlet/src/main.rs index 0d9ac3b6d..b516ff701 100644 --- a/app/gimlet/src/main.rs +++ b/app/gimlet/src/main.rs @@ -11,7 +11,7 @@ extern crate stm32h7; use stm32h7::stm32h753 as device; -use drv_stm32h7_startup::ClockConfig; +use drv_stm32h7_startup::{ClockConfig, rolling_timer::blocking_delay_micros}; use cortex_m_rt::entry; @@ -34,6 +34,15 @@ fn system_init() { let cp = cortex_m::Peripherals::take().unwrap(); let p = device::Peripherals::take().unwrap(); + // Start the higher resolution timer with the default APB1 clock rate of + // 64MHz + // + // SAFETY: We do not carry any "instant" values across this point (as they + // would be invalidated here!), and we do not re-use TIM5 for anything. + unsafe { + drv_stm32h7_startup::rolling_timer::configure_tim5(&p, 64); + } + // Check the package we've been flashed on. Gimlet boards use BGA240. // Gimletlet boards are very similar but use QFPs. This is designed to fail // a Gimlet firmware that was accidentally flashed onto a Gimletlet. @@ -91,20 +100,19 @@ fn system_init() { cortex_m::asm::dsb(); // Make PC6 (SEQ_REG_TO_SP_V3P3_PG) and PC7 (SEQ_REG_TO_SP_V1P2_PG) inputs, - // then wait for both of them to go high. We time out after 1M iterations - // (with 100 cycles each), which is roughly 1.5s. + // then wait for both of them to go high. We time out after roughly 1.5s. p.GPIOC.moder.modify(|_, w| { w.moder6().input(); w.moder7().input() }); const SEQ_PG: u32 = 0b11 << 6; let mut seq_pg_okay = false; - for _ in 0..1_000_000 { + for _ in 0..1_500_000 { if p.GPIOC.idr.read().bits() & SEQ_PG == SEQ_PG { seq_pg_okay = true; break; } else { - cortex_m::asm::delay(100); + blocking_delay_micros(1); } } if !seq_pg_okay { @@ -115,11 +123,11 @@ fn system_init() { // the FPGA bitstream. The minimum CRESET pulse is 200 ns, or 13 cycles, // but there's a 1µF capacitor on that line. Let's assume we're discharging // the capacitor at 5 mA from 3V3; in that case, it will take 0.66 ms, or - // 42K cycles. We'll be conservative and pad it to 100K cycles. + // 42K cycles. We'll be conservative and pad it to 2ms. p.GPIOD.bsrr.write(|w| w.bs5().set()); p.GPIOD.moder.modify(|_, w| w.moder5().output()); p.GPIOD.bsrr.write(|w| w.br5().reset()); - cortex_m::asm::delay(100_000); + blocking_delay_micros(2_000); p.GPIOG.moder.modify(|_, w| { w.moder0().input(); @@ -140,15 +148,8 @@ fn system_init() { // V(t) = 1 / 50 pF * 10 µA * t // Time to reach Vil of 2.31 V (0.7 VDD) = 11.55 µs // - // Maximum speed of 64MHz oscillator after ST manufacturing calibration, per - // the datasheet, is 64.3 MHz. - // - // 11.55 µs @ 64.3MHz ~= 743 cycles - // - // The cortex_m delay routine is written for single-issue simple cores and - // is simply wrong on the M7 (they know this). So, let's conservatively pad - // it by a factor of 10. - cortex_m::asm::delay(743 * 10); + // Conservatively, we will wait 100 µs. + blocking_delay_micros(100); // Okay! What does the fox^Wpins say? let rev = p.GPIOG.idr.read().bits() & 0b111; diff --git a/app/grapefruit/src/main.rs b/app/grapefruit/src/main.rs index 33dc8188d..04b7a6d28 100644 --- a/app/grapefruit/src/main.rs +++ b/app/grapefruit/src/main.rs @@ -30,6 +30,15 @@ fn system_init() { let cp = cortex_m::Peripherals::take().unwrap(); let p = device::Peripherals::take().unwrap(); + // Start the higher resolution timer with the default APB1 clock rate of + // 64MHz + // + // SAFETY: We do not carry any "instant" values across this point (as they + // would be invalidated here!), and we do not re-use TIM5 for anything. + unsafe { + rolling_timer::configure_tim5(&p, 64); + } + // Check the package we've been flashed on. Grapefruit boards use BGA240. // Gimletlet boards are very similar but use QFPs. This is designed to fail // a Grapefruit firmware that was accidentally flashed onto a Gimletlet. diff --git a/app/medusa/src/main.rs b/app/medusa/src/main.rs index 263515609..56643b170 100644 --- a/app/medusa/src/main.rs +++ b/app/medusa/src/main.rs @@ -34,6 +34,15 @@ fn system_init() { let cp = cortex_m::Peripherals::take().unwrap(); let p = device::Peripherals::take().unwrap(); + // Start the higher resolution timer with the default APB1 clock rate of + // 64MHz + // + // SAFETY: We do not carry any "instant" values across this point (as they + // would be invalidated here!), and we do not re-use TIM5 for anything. + unsafe { + rolling_timer::configure_tim5(&p, 64); + } + // Check the package we've been flashed on. Medusa boards use BGA240. // Gimletlet boards are very similar but use QFPs. This is designed to fail // a Medusa firmware that was accidentally flashed onto a Gimletlet. diff --git a/app/minibar/src/main.rs b/app/minibar/src/main.rs index cfa596112..c438fc1c2 100644 --- a/app/minibar/src/main.rs +++ b/app/minibar/src/main.rs @@ -11,7 +11,7 @@ extern crate stm32h7; use stm32h7::stm32h753 as device; -use drv_stm32h7_startup::ClockConfig; +use drv_stm32h7_startup::{ClockConfig, rolling_timer::blocking_delay_micros}; use cortex_m_rt::entry; @@ -28,6 +28,15 @@ fn system_init() { let cp = cortex_m::Peripherals::take().unwrap(); let p = device::Peripherals::take().unwrap(); + // Start the higher resolution timer with the default APB1 clock rate of + // 64MHz + // + // SAFETY: We do not carry any "instant" values across this point (as they + // would be invalidated here!), and we do not re-use TIM5 for anything. + unsafe { + rolling_timer::configure_tim5(&p, 64); + } + // Check the package we've been flashed on. Minibar boards use BGA240. // Gimletlet boards are very similar but use QFPs. This is designed to fail // a Minibar firmware that was accidentally flashed onto a Gimletlet. @@ -81,7 +90,9 @@ fn system_init() { .pupdr7().pull_up()); // TODO: fill in timing justification here based on Sidecar's schematic. - cortex_m::asm::delay(2000); + // The previous code here waited 2000 cycles at 64MHz, or an ideal time of + // 31.25µs. We'll conservatively wait 100µs. + blocking_delay_micros(100); // Build the full ID let rev = p.GPIOK.idr.read().bits(); diff --git a/app/observer/src/main.rs b/app/observer/src/main.rs index 00078e3fc..0fb2e8b45 100644 --- a/app/observer/src/main.rs +++ b/app/observer/src/main.rs @@ -28,6 +28,15 @@ fn system_init() { let cp = cortex_m::Peripherals::take().unwrap(); let p = device::Peripherals::take().unwrap(); + // Start the higher resolution timer with the default APB1 clock rate of + // 64MHz + // + // SAFETY: We do not carry any "instant" values across this point (as they + // would be invalidated here!), and we do not re-use TIM5 for anything. + unsafe { + rolling_timer::configure_tim5(&p, 64); + } + // Un-gate the clock to GPIO bank G. p.RCC.ahb4enr.modify(|_, w| w.gpiogen().set_bit()); cortex_m::asm::dsb(); diff --git a/app/psc/src/main.rs b/app/psc/src/main.rs index 872aa1ee8..64f42f148 100644 --- a/app/psc/src/main.rs +++ b/app/psc/src/main.rs @@ -11,7 +11,7 @@ extern crate stm32h7; use stm32h7::stm32h753 as device; -use drv_stm32h7_startup::ClockConfig; +use drv_stm32h7_startup::{ClockConfig, rolling_timer::blocking_delay_micros}; use cortex_m_rt::entry; @@ -28,6 +28,15 @@ fn system_init() { let cp = cortex_m::Peripherals::take().unwrap(); let p = device::Peripherals::take().unwrap(); + // Start the higher resolution timer with the default APB1 clock rate of + // 64MHz + // + // SAFETY: We do not carry any "instant" values across this point (as they + // would be invalidated here!), and we do not re-use TIM5 for anything. + unsafe { + rolling_timer::configure_tim5(&p, 64); + } + // We want to measure PG0-2 to determine if we're running on the correct // board. On rev A, these pins are left floating; on later revisions, they // are pulled either high or low. @@ -54,7 +63,11 @@ fn system_init() { // Wait for pins to charge / discharge (see comment in gimlet/src/main.rs // for the actual calculations). - cortex_m::asm::delay(155 * 2); + // + // Later note: as of 2026, gimlet's main calculated a necessary time of + // 11µs, and was conservatively waiting about 10x that. We will wait a + // similar amount of time. + blocking_delay_micros(100); let rev = p.GPIOG.idr.read().bits() & 0b111; cfg_if::cfg_if! { diff --git a/app/sidecar/src/main.rs b/app/sidecar/src/main.rs index c30b77e92..d7be74ba7 100644 --- a/app/sidecar/src/main.rs +++ b/app/sidecar/src/main.rs @@ -28,6 +28,15 @@ fn system_init() { let cp = cortex_m::Peripherals::take().unwrap(); let p = device::Peripherals::take().unwrap(); + // Start the higher resolution timer with the default APB1 clock rate of + // 64MHz + // + // SAFETY: We do not carry any "instant" values across this point (as they + // would be invalidated here!), and we do not re-use TIM5 for anything. + unsafe { + rolling_timer::configure_tim5(&p, 64); + } + // Check the package we've been flashed on. Sidecar boards use BGA240. // Gimletlet boards are very similar but use QFPs. This is designed to fail // a Sidecar firmware that was accidentally flashed onto a Gimletlet. @@ -83,7 +92,9 @@ fn system_init() { .pupdr13().pull_up()); // TODO: fill in timing justification here based on Sidecar's schematic. - cortex_m::asm::delay(2000); + // The previous code here waited 2000 cycles at 64MHz, or an ideal time of + // 31.25µs. We'll conservatively wait 100µs. + blocking_delay_micros(100); // Build the full ID let rev = p.GPIOC.idr.read().bits(); diff --git a/drv/stm32h7-startup/src/lib.rs b/drv/stm32h7-startup/src/lib.rs index 42cb99782..c9afd7266 100644 --- a/drv/stm32h7-startup/src/lib.rs +++ b/drv/stm32h7-startup/src/lib.rs @@ -11,7 +11,6 @@ use stm32h7::stm32h743 as device; #[cfg(feature = "h753")] use stm32h7::stm32h753 as device; -use stm32h7::stm32h753::TIM5; #[cfg(any(feature = "h743", feature = "h753"))] #[pre_init] @@ -107,6 +106,15 @@ pub fn system_init(config: ClockConfig) -> device::Peripherals { let cp = cortex_m::Peripherals::take().unwrap(); let p = device::Peripherals::take().unwrap(); + // Start the higher resolution timer with the default APB1 clock rate of + // 64MHz + // + // SAFETY: We do not carry any "instant" values across this point (as they + // would be invalidated here!), and we do not re-use TIM5 for anything. + unsafe { + rolling_timer::configure_tim5(&p, 64); + } + system_init_custom(cp, p, config) } @@ -125,35 +133,9 @@ pub fn system_init_custom( cp.SCB.enable_icache(); cp.SCB.enable_dcache(&mut cp.CPUID); - // Hand-build TIM5 as a 32-bit rolling timer at 1 MHz. Start by enabling - // TIM5 is on APB1, which at boot is undivided from the default 64MHz - // HSI clock source. This *will* change when we reconfigure clocks later! - // - // Start by enabling the peripheral in RCC and toggling reset - p.RCC.apb1lenr.modify(|_r, w| w.tim5en().enabled()); - p.RCC.apb1lrstr.modify(|_r, w| w.tim5rst().set_bit()); - p.RCC.apb1lrstr.modify(|_r, w| w.tim5rst().clear_bit()); - - // Now, configure it for an upcounting rolling mode - // - // Disable counter - p.TIM5.cr1.modify(|_r, w| w.cen().disabled()); - // Set auto-reload to u32::MAX - p.TIM5.arr.write(|w| w.arr().bits(u32::MAX)); - // Before doing anything else, check for a measurement handoff token #[cfg(feature = "measurement-handoff")] unsafe { - // Set counter to zero - p.TIM5.cnt.modify(|_r, w| w.cnt().bits(0)); - // Set prescaler to (FREQ / 1M) - 1, (64M / 1M) - 1 = 63, as the counter - // resets to 0 AFTER counting this number. - p.TIM5.psc.write(|w| w.psc().bits(63)); - // Generate update (latch the PSC and ARR values) - p.TIM5.egr.write(|w| w.ug().set_bit()); - // Start counting! - p.TIM5.cr1.modify(|_r, w| w.cen().enabled()); - // After each delay, we'll wait roughly 200 ms. // // You might ask yourself, "how do we have a RETRY_COUNT if the closure @@ -162,7 +144,7 @@ pub fn system_init_custom( const DELAY_MICROS: u32 = 200 * 1_000; const RETRY_COUNT: u32 = 20; measurement_handoff::check(RETRY_COUNT, || { - blocking_delay_micros(DELAY_MICROS); + rolling_timer::blocking_delay_micros(DELAY_MICROS); cortex_m::peripheral::SCB::sys_reset() }); @@ -347,6 +329,10 @@ pub fn system_init_custom( .variant(config.apb3_div) }); // Other APB buses at HCLK/2 = CPU/4 = 100MHz + // + // NOTE: until we call `configure_tim5` again below, our rolling timer will + // be inaccurate as we have changed the prescaler (and will shortly be + // changing the source clock fed to the prescaler!) p.RCC.d2cfgr.write(|w| { w.d2ppre1() .variant(config.apb1_div) @@ -390,48 +376,103 @@ pub fn system_init_custom( // Hello from target speed! // // Set up the clock for the new nominal APB1 frequency of 100MHz - // - // Set counter to zero - p.TIM5.cnt.modify(|_r, w| w.cnt().bits(0)); - // Set prescaler to (FREQ / 1M) - 1, (100M / 1M) - 1 = 99, as the counter - // resets to 0 AFTER counting this number. - p.TIM5.psc.write(|w| w.psc().bits(99)); - // Generate update (latch the PSC and ARR values) - p.TIM5.egr.write(|w| w.ug().set_bit()); - // Start counting! - p.TIM5.cr1.modify(|_r, w| w.cen().enabled()); + unsafe { + rolling_timer::configure_tim5(&p, 100); + } // Hand the peripherals back in case the board-specific setup code needs to // do anything. p } -/// Obtain the current count value of TIM5, which is a 32-bit timer that ticks -/// at a rate of 1MHz. -#[inline(always)] -pub fn get_rolling_micros_since_boot() -> u32 { - let tim5 = unsafe { &*TIM5::ptr() }; - tim5.cnt.read().bits() -} +pub mod rolling_timer { + use super::device; + + /// Enable TIM5 for use as a 32-bit rolling timer at a tick rate of 1MHz. + /// + /// TIM5 will be enabled at the RCC level, and the current count value will + /// be reset to zero. This function may be called multiple times, modulo the + /// safety concerns listed below. + /// + /// SAFETY: Calling this function will invalidate any current timer counts + /// obtained through `get_rolling_micros_since_boot()`, as it resets our + /// "epoch". If TIM5 is re-used for another purpose, you MUST NOT call + /// `get_rolling_micros_since_boot()` or `blocking_delay_micros()` after + /// reconfiguring or disabling TIM5. If APB1 is reconfigured to another + /// frequency after calling `configure_tim5`, you must call it again with + /// the new frequency for timing measurements to be accurate. + pub unsafe fn configure_tim5(p: &device::Peripherals, apb1_mhz: u16) { + // Hand-build TIM5 as a 32-bit rolling timer at 1 MHz. Start by enabling + // TIM5 is on APB1, which at boot is undivided from the default 64MHz + // HSI clock source. This *will* change when we reconfigure clocks + // later! + // + // Start by enabling the peripheral in RCC and toggling reset + p.RCC.apb1lenr.modify(|_r, w| w.tim5en().enabled()); + cortex_m::asm::dsb(); -/// Perform a blocking delay for the given number of microseconds. -#[inline] -pub fn blocking_delay_micros(micros: u32) { - let start = get_rolling_micros_since_boot(); - loop { - let now = get_rolling_micros_since_boot(); + p.RCC.apb1lrstr.modify(|_r, w| w.tim5rst().set_bit()); + p.RCC.apb1lrstr.modify(|_r, w| w.tim5rst().clear_bit()); - // Since this is a rolling timer, we can perform a wrapping sub to - // obtain the elapsed amount of time, even if we have crossed the roll- - // over point, e.g.: - // - // start = 0xFFFF_FFFE - // now = 0x0000_0080 + // Now, configure it for an upcounting rolling mode // - // now.wrapping_sub(start) => 0x82 - let elapsed = now.wrapping_sub(start); - if elapsed >= micros { - break; + // Disable counter + p.TIM5.cr1.modify(|_r, w| w.cen().disabled()); + // Set auto-reload to u32::MAX + p.TIM5.arr.write(|w| w.arr().bits(u32::MAX)); + // Set counter to zero + p.TIM5.cnt.modify(|_r, w| w.cnt().bits(0)); + // Set prescaler to (FREQ / 1M) - 1, as the counter resets to 0 AFTER + // counting this number. + p.TIM5.psc.write(|w| w.psc().bits(apb1_mhz - 1)); + // Generate update (latch the PSC and ARR values) + p.TIM5.egr.write(|w| w.ug().set_bit()); + // Start counting! + p.TIM5.cr1.modify(|_r, w| w.cen().enabled()); + } + + pub fn assert_tim5_running() { + let (rcc, tim5) = + unsafe { (&*device::RCC::ptr(), &*device::TIM5::ptr()) }; + let rcc_en = rcc.apb1lenr.read().tim5en().is_enabled(); + let tim_en = tim5.cr1.read().cen().is_enabled(); + assert!(rcc_en && tim_en); + } + + /// Obtain the current count value of TIM5, which is a 32-bit timer that + /// ticks at a rate of 1MHz. + /// + /// NOTE: This does *not* assert that the timer is currently running. If it + /// has not ever been started using `configure_tim5`, the returned value may + /// not ever change. + #[inline(always)] + pub fn get_rolling_micros_since_boot() -> u32 { + let tim5 = unsafe { &*device::TIM5::ptr() }; + tim5.cnt.read().bits() + } + + /// Perform a blocking delay for the given number of microseconds. + /// + /// If TIM5 is not already enabled and configured, this function will panic. + #[inline] + pub fn blocking_delay_micros(micros: u32) { + assert_tim5_running(); + let start = get_rolling_micros_since_boot(); + loop { + let now = get_rolling_micros_since_boot(); + + // Since this is a rolling timer, we can perform a wrapping sub to + // obtain the elapsed amount of time, even if we have crossed the + // rollover point, e.g.: + // + // start = 0xFFFF_FFFE + // now = 0x0000_0080 + // + // now.wrapping_sub(start) => 0x82 + let elapsed = now.wrapping_sub(start); + if elapsed >= micros { + break; + } } } } From 1ddcb6c6463113faceac5b1e1b218c93cbf5ef13 Mon Sep 17 00:00:00 2001 From: James Munns Date: Mon, 22 Jun 2026 15:28:56 +0200 Subject: [PATCH 4/9] Fix import for copy-paste --- app/cosmo/src/main.rs | 2 +- app/gimlet/src/main.rs | 2 +- app/grapefruit/src/main.rs | 4 ++-- app/medusa/src/main.rs | 4 ++-- app/minibar/src/main.rs | 4 ++-- app/observer/src/main.rs | 4 ++-- app/psc/src/main.rs | 4 ++-- app/sidecar/src/main.rs | 4 ++-- drv/stm32h7-startup/src/lib.rs | 4 ++-- 9 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/cosmo/src/main.rs b/app/cosmo/src/main.rs index ea6f4ba9d..31b4d6b7f 100644 --- a/app/cosmo/src/main.rs +++ b/app/cosmo/src/main.rs @@ -35,7 +35,7 @@ fn system_init() { let p = device::Peripherals::take().unwrap(); // Start the higher resolution timer with the default APB1 clock rate of - // 64MHz + // 64MHz. // // SAFETY: We do not carry any "instant" values across this point (as they // would be invalidated here!), and we do not re-use TIM5 for anything. diff --git a/app/gimlet/src/main.rs b/app/gimlet/src/main.rs index b516ff701..6e16e78b9 100644 --- a/app/gimlet/src/main.rs +++ b/app/gimlet/src/main.rs @@ -35,7 +35,7 @@ fn system_init() { let p = device::Peripherals::take().unwrap(); // Start the higher resolution timer with the default APB1 clock rate of - // 64MHz + // 64MHz. // // SAFETY: We do not carry any "instant" values across this point (as they // would be invalidated here!), and we do not re-use TIM5 for anything. diff --git a/app/grapefruit/src/main.rs b/app/grapefruit/src/main.rs index 04b7a6d28..6a2a25a49 100644 --- a/app/grapefruit/src/main.rs +++ b/app/grapefruit/src/main.rs @@ -31,12 +31,12 @@ fn system_init() { let p = device::Peripherals::take().unwrap(); // Start the higher resolution timer with the default APB1 clock rate of - // 64MHz + // 64MHz. // // SAFETY: We do not carry any "instant" values across this point (as they // would be invalidated here!), and we do not re-use TIM5 for anything. unsafe { - rolling_timer::configure_tim5(&p, 64); + drv_stm32h7_startup::rolling_timer::configure_tim5(&p, 64); } // Check the package we've been flashed on. Grapefruit boards use BGA240. diff --git a/app/medusa/src/main.rs b/app/medusa/src/main.rs index 56643b170..bb6471a77 100644 --- a/app/medusa/src/main.rs +++ b/app/medusa/src/main.rs @@ -35,12 +35,12 @@ fn system_init() { let p = device::Peripherals::take().unwrap(); // Start the higher resolution timer with the default APB1 clock rate of - // 64MHz + // 64MHz. // // SAFETY: We do not carry any "instant" values across this point (as they // would be invalidated here!), and we do not re-use TIM5 for anything. unsafe { - rolling_timer::configure_tim5(&p, 64); + drv_stm32h7_startup::rolling_timer::configure_tim5(&p, 64); } // Check the package we've been flashed on. Medusa boards use BGA240. diff --git a/app/minibar/src/main.rs b/app/minibar/src/main.rs index c438fc1c2..de1ee8544 100644 --- a/app/minibar/src/main.rs +++ b/app/minibar/src/main.rs @@ -29,12 +29,12 @@ fn system_init() { let p = device::Peripherals::take().unwrap(); // Start the higher resolution timer with the default APB1 clock rate of - // 64MHz + // 64MHz. // // SAFETY: We do not carry any "instant" values across this point (as they // would be invalidated here!), and we do not re-use TIM5 for anything. unsafe { - rolling_timer::configure_tim5(&p, 64); + drv_stm32h7_startup::rolling_timer::configure_tim5(&p, 64); } // Check the package we've been flashed on. Minibar boards use BGA240. diff --git a/app/observer/src/main.rs b/app/observer/src/main.rs index 0fb2e8b45..c5870ccec 100644 --- a/app/observer/src/main.rs +++ b/app/observer/src/main.rs @@ -29,12 +29,12 @@ fn system_init() { let p = device::Peripherals::take().unwrap(); // Start the higher resolution timer with the default APB1 clock rate of - // 64MHz + // 64MHz. // // SAFETY: We do not carry any "instant" values across this point (as they // would be invalidated here!), and we do not re-use TIM5 for anything. unsafe { - rolling_timer::configure_tim5(&p, 64); + drv_stm32h7_startup::rolling_timer::configure_tim5(&p, 64); } // Un-gate the clock to GPIO bank G. diff --git a/app/psc/src/main.rs b/app/psc/src/main.rs index 64f42f148..e980c59e4 100644 --- a/app/psc/src/main.rs +++ b/app/psc/src/main.rs @@ -29,12 +29,12 @@ fn system_init() { let p = device::Peripherals::take().unwrap(); // Start the higher resolution timer with the default APB1 clock rate of - // 64MHz + // 64MHz. // // SAFETY: We do not carry any "instant" values across this point (as they // would be invalidated here!), and we do not re-use TIM5 for anything. unsafe { - rolling_timer::configure_tim5(&p, 64); + drv_stm32h7_startup::rolling_timer::configure_tim5(&p, 64); } // We want to measure PG0-2 to determine if we're running on the correct diff --git a/app/sidecar/src/main.rs b/app/sidecar/src/main.rs index d7be74ba7..935a12673 100644 --- a/app/sidecar/src/main.rs +++ b/app/sidecar/src/main.rs @@ -29,12 +29,12 @@ fn system_init() { let p = device::Peripherals::take().unwrap(); // Start the higher resolution timer with the default APB1 clock rate of - // 64MHz + // 64MHz. // // SAFETY: We do not carry any "instant" values across this point (as they // would be invalidated here!), and we do not re-use TIM5 for anything. unsafe { - rolling_timer::configure_tim5(&p, 64); + drv_stm32h7_startup::rolling_timer::configure_tim5(&p, 64); } // Check the package we've been flashed on. Sidecar boards use BGA240. diff --git a/drv/stm32h7-startup/src/lib.rs b/drv/stm32h7-startup/src/lib.rs index c9afd7266..ae64b232c 100644 --- a/drv/stm32h7-startup/src/lib.rs +++ b/drv/stm32h7-startup/src/lib.rs @@ -107,12 +107,12 @@ pub fn system_init(config: ClockConfig) -> device::Peripherals { let p = device::Peripherals::take().unwrap(); // Start the higher resolution timer with the default APB1 clock rate of - // 64MHz + // 64MHz. // // SAFETY: We do not carry any "instant" values across this point (as they // would be invalidated here!), and we do not re-use TIM5 for anything. unsafe { - rolling_timer::configure_tim5(&p, 64); + drv_stm32h7_startup::rolling_timer::configure_tim5(&p, 64); } system_init_custom(cp, p, config) From 588ebf7ecb5c07e3e3988125ee98f6fd8ada4383 Mon Sep 17 00:00:00 2001 From: James Munns Date: Mon, 22 Jun 2026 15:33:35 +0200 Subject: [PATCH 5/9] Fix overzealous find and replace --- drv/stm32h7-startup/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drv/stm32h7-startup/src/lib.rs b/drv/stm32h7-startup/src/lib.rs index ae64b232c..aa4a12e23 100644 --- a/drv/stm32h7-startup/src/lib.rs +++ b/drv/stm32h7-startup/src/lib.rs @@ -112,7 +112,7 @@ pub fn system_init(config: ClockConfig) -> device::Peripherals { // SAFETY: We do not carry any "instant" values across this point (as they // would be invalidated here!), and we do not re-use TIM5 for anything. unsafe { - drv_stm32h7_startup::rolling_timer::configure_tim5(&p, 64); + rolling_timer::configure_tim5(&p, 64); } system_init_custom(cp, p, config) From 2533d2cf4d018d4713b9e1fd833ceb3d71d95671 Mon Sep 17 00:00:00 2001 From: James Munns Date: Mon, 22 Jun 2026 15:49:30 +0200 Subject: [PATCH 6/9] Consistent import structure --- app/gimlet/src/main.rs | 7 +++++-- app/minibar/src/main.rs | 7 +++++-- app/psc/src/main.rs | 7 +++++-- app/sidecar/src/main.rs | 7 +++++-- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/app/gimlet/src/main.rs b/app/gimlet/src/main.rs index 6e16e78b9..3d004692b 100644 --- a/app/gimlet/src/main.rs +++ b/app/gimlet/src/main.rs @@ -11,7 +11,10 @@ extern crate stm32h7; use stm32h7::stm32h753 as device; -use drv_stm32h7_startup::{ClockConfig, rolling_timer::blocking_delay_micros}; +use drv_stm32h7_startup::{ + ClockConfig, + rolling_timer::{blocking_delay_micros, configure_tim5}, +}; use cortex_m_rt::entry; @@ -40,7 +43,7 @@ fn system_init() { // SAFETY: We do not carry any "instant" values across this point (as they // would be invalidated here!), and we do not re-use TIM5 for anything. unsafe { - drv_stm32h7_startup::rolling_timer::configure_tim5(&p, 64); + configure_tim5(&p, 64); } // Check the package we've been flashed on. Gimlet boards use BGA240. diff --git a/app/minibar/src/main.rs b/app/minibar/src/main.rs index de1ee8544..e4e68b7ed 100644 --- a/app/minibar/src/main.rs +++ b/app/minibar/src/main.rs @@ -11,7 +11,10 @@ extern crate stm32h7; use stm32h7::stm32h753 as device; -use drv_stm32h7_startup::{ClockConfig, rolling_timer::blocking_delay_micros}; +use drv_stm32h7_startup::{ + ClockConfig, + rolling_timer::{blocking_delay_micros, configure_tim5}, +}; use cortex_m_rt::entry; @@ -34,7 +37,7 @@ fn system_init() { // SAFETY: We do not carry any "instant" values across this point (as they // would be invalidated here!), and we do not re-use TIM5 for anything. unsafe { - drv_stm32h7_startup::rolling_timer::configure_tim5(&p, 64); + configure_tim5(&p, 64); } // Check the package we've been flashed on. Minibar boards use BGA240. diff --git a/app/psc/src/main.rs b/app/psc/src/main.rs index e980c59e4..827cdd110 100644 --- a/app/psc/src/main.rs +++ b/app/psc/src/main.rs @@ -11,7 +11,10 @@ extern crate stm32h7; use stm32h7::stm32h753 as device; -use drv_stm32h7_startup::{ClockConfig, rolling_timer::blocking_delay_micros}; +use drv_stm32h7_startup::{ + ClockConfig, + rolling_timer::{blocking_delay_micros, configure_tim5}, +}; use cortex_m_rt::entry; @@ -34,7 +37,7 @@ fn system_init() { // SAFETY: We do not carry any "instant" values across this point (as they // would be invalidated here!), and we do not re-use TIM5 for anything. unsafe { - drv_stm32h7_startup::rolling_timer::configure_tim5(&p, 64); + configure_tim5(&p, 64); } // We want to measure PG0-2 to determine if we're running on the correct diff --git a/app/sidecar/src/main.rs b/app/sidecar/src/main.rs index 935a12673..404bc351c 100644 --- a/app/sidecar/src/main.rs +++ b/app/sidecar/src/main.rs @@ -11,7 +11,10 @@ extern crate stm32h7; use stm32h7::stm32h753 as device; -use drv_stm32h7_startup::ClockConfig; +use drv_stm32h7_startup::{ + ClockConfig, + rolling_timer::{blocking_delay_micros, configure_tim5}, +}; use cortex_m_rt::entry; @@ -34,7 +37,7 @@ fn system_init() { // SAFETY: We do not carry any "instant" values across this point (as they // would be invalidated here!), and we do not re-use TIM5 for anything. unsafe { - drv_stm32h7_startup::rolling_timer::configure_tim5(&p, 64); + configure_tim5(&p, 64); } // Check the package we've been flashed on. Sidecar boards use BGA240. From 3dc62a812281ba78f697ce8faa86915ea2269cb2 Mon Sep 17 00:00:00 2001 From: James Munns Date: Tue, 23 Jun 2026 10:03:06 +0200 Subject: [PATCH 7/9] Fix some self-review nits --- drv/stm32h7-startup/src/lib.rs | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/drv/stm32h7-startup/src/lib.rs b/drv/stm32h7-startup/src/lib.rs index aa4a12e23..d278f28dc 100644 --- a/drv/stm32h7-startup/src/lib.rs +++ b/drv/stm32h7-startup/src/lib.rs @@ -394,6 +394,10 @@ pub mod rolling_timer { /// be reset to zero. This function may be called multiple times, modulo the /// safety concerns listed below. /// + /// `apb1_mhz` should be the configured frequency in MHz of the APB1 clock, + /// which is used as an input to TIM5, and will be used to pre-scale this + /// input down to a tick rate of 1MHz. + /// /// SAFETY: Calling this function will invalidate any current timer counts /// obtained through `get_rolling_micros_since_boot()`, as it resets our /// "epoch". If TIM5 is re-used for another purpose, you MUST NOT call @@ -403,11 +407,7 @@ pub mod rolling_timer { /// the new frequency for timing measurements to be accurate. pub unsafe fn configure_tim5(p: &device::Peripherals, apb1_mhz: u16) { // Hand-build TIM5 as a 32-bit rolling timer at 1 MHz. Start by enabling - // TIM5 is on APB1, which at boot is undivided from the default 64MHz - // HSI clock source. This *will* change when we reconfigure clocks - // later! - // - // Start by enabling the peripheral in RCC and toggling reset + // TIM5 on APB1L in RCC and toggling reset p.RCC.apb1lenr.modify(|_r, w| w.tim5en().enabled()); cortex_m::asm::dsb(); @@ -431,17 +431,28 @@ pub mod rolling_timer { p.TIM5.cr1.modify(|_r, w| w.cen().enabled()); } + /// This function checks that TIM5 has been enabled in RCC, and is currently + /// counting. pub fn assert_tim5_running() { let (rcc, tim5) = unsafe { (&*device::RCC::ptr(), &*device::TIM5::ptr()) }; - let rcc_en = rcc.apb1lenr.read().tim5en().is_enabled(); - let tim_en = tim5.cr1.read().cen().is_enabled(); - assert!(rcc_en && tim_en); + + assert!( + rcc.apb1lenr.read().tim5en().is_enabled() + && tim5.cr1.read().cen().is_enabled() + ); } /// Obtain the current count value of TIM5, which is a 32-bit timer that /// ticks at a rate of 1MHz. /// + /// The value returned by this function "rolls over", or wraps around every + /// 71 minutes or so. Callers should be careful to handle potential wrapping + /// of the returned value when calculating elapsed time or using for delays. + /// + /// Consider using `blocking_delay_micros()`, which correctly handles this + /// calculation, for early boot-up delays. + /// /// NOTE: This does *not* assert that the timer is currently running. If it /// has not ever been started using `configure_tim5`, the returned value may /// not ever change. From fc0eddbfb535191d3cce192dcc9aa69f02b34c9a Mon Sep 17 00:00:00 2001 From: James Munns Date: Fri, 26 Jun 2026 14:57:27 +0200 Subject: [PATCH 8/9] Rework to a more ownership-based API that automatically handles teardown --- app/cosmo/src/main.rs | 9 -- app/gimlet/src/main.rs | 21 ++- app/grapefruit/src/main.rs | 9 -- app/medusa/src/main.rs | 9 -- app/minibar/src/main.rs | 17 +-- app/observer/src/main.rs | 9 -- app/psc/src/main.rs | 17 +-- app/sidecar/src/main.rs | 17 +-- drv/stm32h7-startup/src/lib.rs | 198 +++++++++++++---------------- lib/measurement-handoff/src/lib.rs | 21 ++- 10 files changed, 133 insertions(+), 194 deletions(-) diff --git a/app/cosmo/src/main.rs b/app/cosmo/src/main.rs index 31b4d6b7f..ed9a3d989 100644 --- a/app/cosmo/src/main.rs +++ b/app/cosmo/src/main.rs @@ -34,15 +34,6 @@ fn system_init() { let cp = cortex_m::Peripherals::take().unwrap(); let p = device::Peripherals::take().unwrap(); - // Start the higher resolution timer with the default APB1 clock rate of - // 64MHz. - // - // SAFETY: We do not carry any "instant" values across this point (as they - // would be invalidated here!), and we do not re-use TIM5 for anything. - unsafe { - drv_stm32h7_startup::rolling_timer::configure_tim5(&p, 64); - } - // Check the package we've been flashed on (Cosmo boards use BGA240) // // We need to turn the SYSCFG block on to do this. diff --git a/app/gimlet/src/main.rs b/app/gimlet/src/main.rs index 3d004692b..e6c5b8ae7 100644 --- a/app/gimlet/src/main.rs +++ b/app/gimlet/src/main.rs @@ -11,10 +11,7 @@ extern crate stm32h7; use stm32h7::stm32h753 as device; -use drv_stm32h7_startup::{ - ClockConfig, - rolling_timer::{blocking_delay_micros, configure_tim5}, -}; +use drv_stm32h7_startup::{ClockConfig, rolling_timer::RollingTimer}; use cortex_m_rt::entry; @@ -39,12 +36,7 @@ fn system_init() { // Start the higher resolution timer with the default APB1 clock rate of // 64MHz. - // - // SAFETY: We do not carry any "instant" values across this point (as they - // would be invalidated here!), and we do not re-use TIM5 for anything. - unsafe { - configure_tim5(&p, 64); - } + let timer = RollingTimer::new_tim5(&p, 64); // Check the package we've been flashed on. Gimlet boards use BGA240. // Gimletlet boards are very similar but use QFPs. This is designed to fail @@ -115,7 +107,7 @@ fn system_init() { seq_pg_okay = true; break; } else { - blocking_delay_micros(1); + timer.blocking_delay_micros(1); } } if !seq_pg_okay { @@ -130,7 +122,7 @@ fn system_init() { p.GPIOD.bsrr.write(|w| w.bs5().set()); p.GPIOD.moder.modify(|_, w| w.moder5().output()); p.GPIOD.bsrr.write(|w| w.br5().reset()); - blocking_delay_micros(2_000); + timer.blocking_delay_micros(2_000); p.GPIOG.moder.modify(|_, w| { w.moder0().input(); @@ -152,7 +144,7 @@ fn system_init() { // Time to reach Vil of 2.31 V (0.7 VDD) = 11.55 µs // // Conservatively, we will wait 100 µs. - blocking_delay_micros(100); + timer.blocking_delay_micros(100); // Okay! What does the fox^Wpins say? let rev = p.GPIOG.idr.read().bits() & 0b111; @@ -178,6 +170,9 @@ fn system_init() { assert_eq!(rev, expected_rev); + // Drop the timer since we're passing the peripherals by ownership here. + drop(timer); + // Do most of the setup with the common implementation. let p = drv_stm32h7_startup::system_init_custom( cp, diff --git a/app/grapefruit/src/main.rs b/app/grapefruit/src/main.rs index 6a2a25a49..33dc8188d 100644 --- a/app/grapefruit/src/main.rs +++ b/app/grapefruit/src/main.rs @@ -30,15 +30,6 @@ fn system_init() { let cp = cortex_m::Peripherals::take().unwrap(); let p = device::Peripherals::take().unwrap(); - // Start the higher resolution timer with the default APB1 clock rate of - // 64MHz. - // - // SAFETY: We do not carry any "instant" values across this point (as they - // would be invalidated here!), and we do not re-use TIM5 for anything. - unsafe { - drv_stm32h7_startup::rolling_timer::configure_tim5(&p, 64); - } - // Check the package we've been flashed on. Grapefruit boards use BGA240. // Gimletlet boards are very similar but use QFPs. This is designed to fail // a Grapefruit firmware that was accidentally flashed onto a Gimletlet. diff --git a/app/medusa/src/main.rs b/app/medusa/src/main.rs index bb6471a77..263515609 100644 --- a/app/medusa/src/main.rs +++ b/app/medusa/src/main.rs @@ -34,15 +34,6 @@ fn system_init() { let cp = cortex_m::Peripherals::take().unwrap(); let p = device::Peripherals::take().unwrap(); - // Start the higher resolution timer with the default APB1 clock rate of - // 64MHz. - // - // SAFETY: We do not carry any "instant" values across this point (as they - // would be invalidated here!), and we do not re-use TIM5 for anything. - unsafe { - drv_stm32h7_startup::rolling_timer::configure_tim5(&p, 64); - } - // Check the package we've been flashed on. Medusa boards use BGA240. // Gimletlet boards are very similar but use QFPs. This is designed to fail // a Medusa firmware that was accidentally flashed onto a Gimletlet. diff --git a/app/minibar/src/main.rs b/app/minibar/src/main.rs index e4e68b7ed..08e2ae9bc 100644 --- a/app/minibar/src/main.rs +++ b/app/minibar/src/main.rs @@ -11,10 +11,7 @@ extern crate stm32h7; use stm32h7::stm32h753 as device; -use drv_stm32h7_startup::{ - ClockConfig, - rolling_timer::{blocking_delay_micros, configure_tim5}, -}; +use drv_stm32h7_startup::{ClockConfig, rolling_timer::RollingTimer}; use cortex_m_rt::entry; @@ -33,12 +30,7 @@ fn system_init() { // Start the higher resolution timer with the default APB1 clock rate of // 64MHz. - // - // SAFETY: We do not carry any "instant" values across this point (as they - // would be invalidated here!), and we do not re-use TIM5 for anything. - unsafe { - configure_tim5(&p, 64); - } + let timer = RollingTimer::new_tim5(&p, 64); // Check the package we've been flashed on. Minibar boards use BGA240. // Gimletlet boards are very similar but use QFPs. This is designed to fail @@ -95,7 +87,7 @@ fn system_init() { // TODO: fill in timing justification here based on Sidecar's schematic. // The previous code here waited 2000 cycles at 64MHz, or an ideal time of // 31.25µs. We'll conservatively wait 100µs. - blocking_delay_micros(100); + timer.blocking_delay_micros(100); // Build the full ID let rev = p.GPIOK.idr.read().bits(); @@ -118,6 +110,9 @@ fn system_init() { assert_eq!(rev, expected_rev); + // Drop the timer since we're passing the peripherals by ownership here. + drop(timer); + drv_stm32h7_startup::system_init_custom( cp, p, diff --git a/app/observer/src/main.rs b/app/observer/src/main.rs index c5870ccec..00078e3fc 100644 --- a/app/observer/src/main.rs +++ b/app/observer/src/main.rs @@ -28,15 +28,6 @@ fn system_init() { let cp = cortex_m::Peripherals::take().unwrap(); let p = device::Peripherals::take().unwrap(); - // Start the higher resolution timer with the default APB1 clock rate of - // 64MHz. - // - // SAFETY: We do not carry any "instant" values across this point (as they - // would be invalidated here!), and we do not re-use TIM5 for anything. - unsafe { - drv_stm32h7_startup::rolling_timer::configure_tim5(&p, 64); - } - // Un-gate the clock to GPIO bank G. p.RCC.ahb4enr.modify(|_, w| w.gpiogen().set_bit()); cortex_m::asm::dsb(); diff --git a/app/psc/src/main.rs b/app/psc/src/main.rs index 827cdd110..33bd7f77f 100644 --- a/app/psc/src/main.rs +++ b/app/psc/src/main.rs @@ -11,10 +11,7 @@ extern crate stm32h7; use stm32h7::stm32h753 as device; -use drv_stm32h7_startup::{ - ClockConfig, - rolling_timer::{blocking_delay_micros, configure_tim5}, -}; +use drv_stm32h7_startup::{ClockConfig, rolling_timer::RollingTimer}; use cortex_m_rt::entry; @@ -33,12 +30,7 @@ fn system_init() { // Start the higher resolution timer with the default APB1 clock rate of // 64MHz. - // - // SAFETY: We do not carry any "instant" values across this point (as they - // would be invalidated here!), and we do not re-use TIM5 for anything. - unsafe { - configure_tim5(&p, 64); - } + let timer = RollingTimer::new_tim5(&p, 64); // We want to measure PG0-2 to determine if we're running on the correct // board. On rev A, these pins are left floating; on later revisions, they @@ -70,7 +62,7 @@ fn system_init() { // Later note: as of 2026, gimlet's main calculated a necessary time of // 11µs, and was conservatively waiting about 10x that. We will wait a // similar amount of time. - blocking_delay_micros(100); + timer.blocking_delay_micros(100); let rev = p.GPIOG.idr.read().bits() & 0b111; cfg_if::cfg_if! { @@ -84,6 +76,9 @@ fn system_init() { } assert_eq!(rev, expected_rev); + // Drop the timer since we're passing the peripherals by ownership here. + drop(timer); + drv_stm32h7_startup::system_init_custom( cp, p, diff --git a/app/sidecar/src/main.rs b/app/sidecar/src/main.rs index 404bc351c..e7ed51949 100644 --- a/app/sidecar/src/main.rs +++ b/app/sidecar/src/main.rs @@ -11,10 +11,7 @@ extern crate stm32h7; use stm32h7::stm32h753 as device; -use drv_stm32h7_startup::{ - ClockConfig, - rolling_timer::{blocking_delay_micros, configure_tim5}, -}; +use drv_stm32h7_startup::{ClockConfig, rolling_timer::RollingTimer}; use cortex_m_rt::entry; @@ -33,12 +30,7 @@ fn system_init() { // Start the higher resolution timer with the default APB1 clock rate of // 64MHz. - // - // SAFETY: We do not carry any "instant" values across this point (as they - // would be invalidated here!), and we do not re-use TIM5 for anything. - unsafe { - configure_tim5(&p, 64); - } + let timer = RollingTimer::new_tim5(&p, 64); // Check the package we've been flashed on. Sidecar boards use BGA240. // Gimletlet boards are very similar but use QFPs. This is designed to fail @@ -97,7 +89,7 @@ fn system_init() { // TODO: fill in timing justification here based on Sidecar's schematic. // The previous code here waited 2000 cycles at 64MHz, or an ideal time of // 31.25µs. We'll conservatively wait 100µs. - blocking_delay_micros(100); + timer.blocking_delay_micros(100); // Build the full ID let rev = p.GPIOC.idr.read().bits(); @@ -121,6 +113,9 @@ fn system_init() { assert_eq!(rev, expected_rev); + // Drop the timer since we're passing the peripherals by ownership here. + drop(timer); + drv_stm32h7_startup::system_init_custom( cp, p, diff --git a/drv/stm32h7-startup/src/lib.rs b/drv/stm32h7-startup/src/lib.rs index d278f28dc..11a157c7f 100644 --- a/drv/stm32h7-startup/src/lib.rs +++ b/drv/stm32h7-startup/src/lib.rs @@ -106,15 +106,6 @@ pub fn system_init(config: ClockConfig) -> device::Peripherals { let cp = cortex_m::Peripherals::take().unwrap(); let p = device::Peripherals::take().unwrap(); - // Start the higher resolution timer with the default APB1 clock rate of - // 64MHz. - // - // SAFETY: We do not carry any "instant" values across this point (as they - // would be invalidated here!), and we do not re-use TIM5 for anything. - unsafe { - rolling_timer::configure_tim5(&p, 64); - } - system_init_custom(cp, p, config) } @@ -143,13 +134,13 @@ pub fn system_init_custom( // counter in a linker location that persists across soft-reboots. const DELAY_MICROS: u32 = 200 * 1_000; const RETRY_COUNT: u32 = 20; + + // APB1 is currently 64MHz. Create a rolling timer we can use for now. + let timer = rolling_timer::RollingTimer::new_tim5(&p, 64); measurement_handoff::check(RETRY_COUNT, || { - rolling_timer::blocking_delay_micros(DELAY_MICROS); + timer.blocking_delay_micros(DELAY_MICROS); cortex_m::peripheral::SCB::sys_reset() }); - - // Disable timer, we will need to reconfigure it after setting up PLL - p.TIM5.cr1.modify(|_r, w| w.cen().disabled()); } // The H7 -- and perhaps the Cortex-M7 -- has the somewhat annoying @@ -373,13 +364,6 @@ pub fn system_init_custom( #[cfg(any(feature = "h743", feature = "h753"))] p.RCC.d2ccip2r.modify(|_, w| w.rngsel().pll1_q()); - // Hello from target speed! - // - // Set up the clock for the new nominal APB1 frequency of 100MHz - unsafe { - rolling_timer::configure_tim5(&p, 100); - } - // Hand the peripherals back in case the board-specific setup code needs to // do anything. p @@ -388,101 +372,95 @@ pub fn system_init_custom( pub mod rolling_timer { use super::device; - /// Enable TIM5 for use as a 32-bit rolling timer at a tick rate of 1MHz. - /// - /// TIM5 will be enabled at the RCC level, and the current count value will - /// be reset to zero. This function may be called multiple times, modulo the - /// safety concerns listed below. - /// - /// `apb1_mhz` should be the configured frequency in MHz of the APB1 clock, - /// which is used as an input to TIM5, and will be used to pre-scale this - /// input down to a tick rate of 1MHz. - /// - /// SAFETY: Calling this function will invalidate any current timer counts - /// obtained through `get_rolling_micros_since_boot()`, as it resets our - /// "epoch". If TIM5 is re-used for another purpose, you MUST NOT call - /// `get_rolling_micros_since_boot()` or `blocking_delay_micros()` after - /// reconfiguring or disabling TIM5. If APB1 is reconfigured to another - /// frequency after calling `configure_tim5`, you must call it again with - /// the new frequency for timing measurements to be accurate. - pub unsafe fn configure_tim5(p: &device::Peripherals, apb1_mhz: u16) { - // Hand-build TIM5 as a 32-bit rolling timer at 1 MHz. Start by enabling - // TIM5 on APB1L in RCC and toggling reset - p.RCC.apb1lenr.modify(|_r, w| w.tim5en().enabled()); - cortex_m::asm::dsb(); - - p.RCC.apb1lrstr.modify(|_r, w| w.tim5rst().set_bit()); - p.RCC.apb1lrstr.modify(|_r, w| w.tim5rst().clear_bit()); - - // Now, configure it for an upcounting rolling mode - // - // Disable counter - p.TIM5.cr1.modify(|_r, w| w.cen().disabled()); - // Set auto-reload to u32::MAX - p.TIM5.arr.write(|w| w.arr().bits(u32::MAX)); - // Set counter to zero - p.TIM5.cnt.modify(|_r, w| w.cnt().bits(0)); - // Set prescaler to (FREQ / 1M) - 1, as the counter resets to 0 AFTER - // counting this number. - p.TIM5.psc.write(|w| w.psc().bits(apb1_mhz - 1)); - // Generate update (latch the PSC and ARR values) - p.TIM5.egr.write(|w| w.ug().set_bit()); - // Start counting! - p.TIM5.cr1.modify(|_r, w| w.cen().enabled()); + /// A 32-bit rolling hardware timer, ticking at 1MHz. + pub struct RollingTimer<'a> { + tim: &'a device::TIM5, } - /// This function checks that TIM5 has been enabled in RCC, and is currently - /// counting. - pub fn assert_tim5_running() { - let (rcc, tim5) = - unsafe { (&*device::RCC::ptr(), &*device::TIM5::ptr()) }; - - assert!( - rcc.apb1lenr.read().tim5en().is_enabled() - && tim5.cr1.read().cen().is_enabled() - ); - } - - /// Obtain the current count value of TIM5, which is a 32-bit timer that - /// ticks at a rate of 1MHz. - /// - /// The value returned by this function "rolls over", or wraps around every - /// 71 minutes or so. Callers should be careful to handle potential wrapping - /// of the returned value when calculating elapsed time or using for delays. - /// - /// Consider using `blocking_delay_micros()`, which correctly handles this - /// calculation, for early boot-up delays. - /// - /// NOTE: This does *not* assert that the timer is currently running. If it - /// has not ever been started using `configure_tim5`, the returned value may - /// not ever change. - #[inline(always)] - pub fn get_rolling_micros_since_boot() -> u32 { - let tim5 = unsafe { &*device::TIM5::ptr() }; - tim5.cnt.read().bits() + /// Stop the rolling timer automatically when dropped. + impl Drop for RollingTimer<'_> { + fn drop(&mut self) { + self.tim.cr1.modify(|_r, w| w.cen().disabled()); + } } - /// Perform a blocking delay for the given number of microseconds. - /// - /// If TIM5 is not already enabled and configured, this function will panic. - #[inline] - pub fn blocking_delay_micros(micros: u32) { - assert_tim5_running(); - let start = get_rolling_micros_since_boot(); - loop { - let now = get_rolling_micros_since_boot(); - - // Since this is a rolling timer, we can perform a wrapping sub to - // obtain the elapsed amount of time, even if we have crossed the - // rollover point, e.g.: + impl<'a> RollingTimer<'a> { + /// Enable TIM5 for use as a 32-bit rolling timer at a tick rate of + /// 1MHz. + /// + /// TIM5 will be enabled at the RCC level, and the current count value + /// will be reset to zero. This function may be called multiple times, + /// modulo the safety concerns listed below. + /// + /// `apb1_mhz` should be the configured frequency in MHz of the APB1 + /// clock, which is used as an input to TIM5, and will be used to + /// pre-scale this input down to a tick rate of 1MHz. + pub fn new_tim5(p: &'a device::Peripherals, apb1_mhz: u16) -> Self { + // Hand-build TIM5 as a 32-bit rolling timer at 1 MHz. Start by + // enabling TIM5 on APB1L in RCC and toggling reset + p.RCC.apb1lenr.modify(|_r, w| w.tim5en().enabled()); + cortex_m::asm::dsb(); + + p.RCC.apb1lrstr.modify(|_r, w| w.tim5rst().set_bit()); + p.RCC.apb1lrstr.modify(|_r, w| w.tim5rst().clear_bit()); + + // Now, configure it for an upcounting rolling mode // - // start = 0xFFFF_FFFE - // now = 0x0000_0080 - // - // now.wrapping_sub(start) => 0x82 - let elapsed = now.wrapping_sub(start); - if elapsed >= micros { - break; + // Disable counter + p.TIM5.cr1.modify(|_r, w| w.cen().disabled()); + // Set auto-reload to u32::MAX + p.TIM5.arr.write(|w| w.arr().bits(u32::MAX)); + // Set counter to zero + p.TIM5.cnt.modify(|_r, w| w.cnt().bits(0)); + // Set prescaler to (FREQ / 1M) - 1, as the counter resets to 0 + // AFTER counting this number. + p.TIM5.psc.write(|w| w.psc().bits(apb1_mhz - 1)); + // Generate update (latch the PSC and ARR values) + p.TIM5.egr.write(|w| w.ug().set_bit()); + // Start counting! + p.TIM5.cr1.modify(|_r, w| w.cen().enabled()); + + Self { tim: &p.TIM5 } + } + + /// Obtain the current count value of TIM5, which is a 32-bit timer that + /// ticks at a rate of 1MHz. + /// + /// The value returned by this function "rolls over", or wraps around + /// every 71 minutes or so. Callers should be careful to handle + /// potential wrapping of the returned value when calculating elapsed + /// time or using for delays. + /// + /// Consider using `blocking_delay_micros()`, which correctly handles + /// this calculation, for early boot-up delays. + /// + /// NOTE: The returned value here is only valid while *this* instance of + /// `RollingTimer` is valid. If the timer is dropped and recreated, the + /// count will be reset to zero. + #[inline(always)] + pub fn get_rolling_micros(&self) -> u32 { + self.tim.cnt.read().bits() + } + + /// Perform a blocking delay for the given number of microseconds. + #[inline] + pub fn blocking_delay_micros(&self, micros: u32) { + let start = self.get_rolling_micros(); + loop { + let now = self.get_rolling_micros(); + + // Since this is a rolling timer, we can perform a wrapping sub + // to obtain the elapsed amount of time, even if we have crossed + // the rollover point, e.g.: + // + // start = 0xFFFF_FFFE + // now = 0x0000_0080 + // + // now.wrapping_sub(start) => 0x82 + let elapsed = now.wrapping_sub(start); + if elapsed >= micros { + break; + } } } } diff --git a/lib/measurement-handoff/src/lib.rs b/lib/measurement-handoff/src/lib.rs index 300485f71..ed730026d 100644 --- a/lib/measurement-handoff/src/lib.rs +++ b/lib/measurement-handoff/src/lib.rs @@ -75,7 +75,13 @@ pub static mut HUBRIS_MEASUREMENT_RESULT: Option = None; /// measurement is valid, or `false` if we exceeded `retry_count`. /// /// `delay_and_reset` should include a delay, to give the RoT time to boot. -pub unsafe fn check(retry_count: u32, delay_and_reset: fn() -> !) -> bool { +/// +// TODO: `core::convert::Infallible` is used as a stand-in for `!`, we should +// change this over when https://github.com/rust-lang/rust/pull/155499 lands. +pub unsafe fn check(retry_count: u32, delay_and_reset: F) -> bool +where + F: FnOnce() -> core::convert::Infallible, +{ let ptr: *mut u32 = &raw mut __REGION_DTCM_BASE as *mut _; let end: *mut u32 = &raw mut __REGION_DTCM_END as *mut _; assert!(ptr == measurement_token::SP_ADDR); @@ -133,7 +139,18 @@ pub unsafe fn check(retry_count: u32, delay_and_reset: fn() -> !) -> bool { next ^ COUNTER_TAG, ); } - delay_and_reset(); + + // NOTE(AJM), sadly, the compiler IS smart enough to figure out that + // core::convert::Infallible is uninhabited, and will warn us that + // code following `delay_and_reset` is unreachable, BUT, it isn't + // smart enough to enforce this wrt types, and if we DON'T include + // the `unreachable!()` macro, it will tell us that it expects us to + // return a bool here. + #[allow(unreachable_code)] + { + delay_and_reset(); + unreachable!(); + } } } } From 796377d58a84ed23578d4f812a5486c30a7a982d Mon Sep 17 00:00:00 2001 From: James Munns Date: Fri, 26 Jun 2026 15:00:55 +0200 Subject: [PATCH 9/9] Remove stale comment --- drv/stm32h7-startup/src/lib.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/drv/stm32h7-startup/src/lib.rs b/drv/stm32h7-startup/src/lib.rs index 11a157c7f..94a09ce22 100644 --- a/drv/stm32h7-startup/src/lib.rs +++ b/drv/stm32h7-startup/src/lib.rs @@ -320,10 +320,6 @@ pub fn system_init_custom( .variant(config.apb3_div) }); // Other APB buses at HCLK/2 = CPU/4 = 100MHz - // - // NOTE: until we call `configure_tim5` again below, our rolling timer will - // be inaccurate as we have changed the prescaler (and will shortly be - // changing the source clock fed to the prescaler!) p.RCC.d2cfgr.write(|w| { w.d2ppre1() .variant(config.apb1_div)