diff --git a/app/gimlet/src/main.rs b/app/gimlet/src/main.rs index 0d9ac3b6d0..e6c5b8ae72 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::RollingTimer}; use cortex_m_rt::entry; @@ -34,6 +34,10 @@ 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. + 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 // a Gimlet firmware that was accidentally flashed onto a Gimletlet. @@ -91,20 +95,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); + timer.blocking_delay_micros(1); } } if !seq_pg_okay { @@ -115,11 +118,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); + timer.blocking_delay_micros(2_000); p.GPIOG.moder.modify(|_, w| { w.moder0().input(); @@ -140,15 +143,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. + timer.blocking_delay_micros(100); // Okay! What does the fox^Wpins say? let rev = p.GPIOG.idr.read().bits() & 0b111; @@ -174,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/minibar/src/main.rs b/app/minibar/src/main.rs index cfa5961123..08e2ae9bca 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::RollingTimer}; use cortex_m_rt::entry; @@ -28,6 +28,10 @@ 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. + 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 // a Minibar firmware that was accidentally flashed onto a Gimletlet. @@ -81,7 +85,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. + timer.blocking_delay_micros(100); // Build the full ID let rev = p.GPIOK.idr.read().bits(); @@ -104,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/psc/src/main.rs b/app/psc/src/main.rs index 872aa1ee89..33bd7f77f1 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::RollingTimer}; use cortex_m_rt::entry; @@ -28,6 +28,10 @@ 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. + 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 // are pulled either high or low. @@ -54,7 +58,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. + timer.blocking_delay_micros(100); let rev = p.GPIOG.idr.read().bits() & 0b111; cfg_if::cfg_if! { @@ -68,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 c30b77e92e..e7ed519498 100644 --- a/app/sidecar/src/main.rs +++ b/app/sidecar/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::RollingTimer}; use cortex_m_rt::entry; @@ -28,6 +28,10 @@ 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. + 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 // a Sidecar firmware that was accidentally flashed onto a Gimletlet. @@ -83,7 +87,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. + timer.blocking_delay_micros(100); // Build the full ID let rev = p.GPIOC.idr.read().bits(); @@ -107,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 3ea7247c35..94a09ce22d 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. // @@ -114,17 +127,18 @@ pub fn system_init_custom( // 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; + // 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; + + // 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, || { - cortex_m::asm::delay(DELAY_CYCLES); + timer.blocking_delay_micros(DELAY_MICROS); cortex_m::peripheral::SCB::sys_reset() }); } @@ -346,9 +360,104 @@ pub fn system_init_custom( #[cfg(any(feature = "h743", feature = "h753"))] p.RCC.d2ccip2r.modify(|_, w| w.rngsel().pll1_q()); - // Hello from target speed! - // Hand the peripherals back in case the board-specific setup code needs to // do anything. p } + +pub mod rolling_timer { + use super::device; + + /// A 32-bit rolling hardware timer, ticking at 1MHz. + pub struct RollingTimer<'a> { + tim: &'a device::TIM5, + } + + /// Stop the rolling timer automatically when dropped. + impl Drop for RollingTimer<'_> { + fn drop(&mut self) { + self.tim.cr1.modify(|_r, w| w.cen().disabled()); + } + } + + 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 + // + // 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 300485f71c..ed730026d8 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!(); + } } } }