From 4a5d0313ca0b050ab005f89ae1883c662aec8f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Sat, 25 Jul 2026 21:54:29 +0300 Subject: [PATCH] libs/kaptan-stm32f7: prepare high-rate board runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Onur Özkan --- libs/kaptan-runtime/src/imu.rs | 13 +++- libs/kaptan-stm32f7/src/stm32f722/clock.rs | 87 +++++++++++++++------- libs/kaptan-stm32f7/src/stm32f722/mod.rs | 2 +- libs/kaptan-stm32f7/src/stm32f722/spi1.rs | 58 +++++++++++++-- 4 files changed, 126 insertions(+), 34 deletions(-) diff --git a/libs/kaptan-runtime/src/imu.rs b/libs/kaptan-runtime/src/imu.rs index 8f09567..0c5ff74 100644 --- a/libs/kaptan-runtime/src/imu.rs +++ b/libs/kaptan-runtime/src/imu.rs @@ -46,6 +46,13 @@ where /// Advances the reset-settling state on a board periodic event. pub fn on_periodic_tick(&mut self) -> ImuEvent { + if self.imu.is_ready() { + return match self.imu.request_sample(self.clock.now()) { + Ok(()) | Err(Mpu6000Error::NotReady) => ImuEvent::Idle, + Err(error) => ImuEvent::Fault(error), + }; + } + let _ = self.clock.now(); match self.imu.advance_initialization() { Ok(()) => ImuEvent::Idle, @@ -132,6 +139,10 @@ mod tests { service.on_transfer_complete(SpiCompletion::Complete), ImuEvent::Initialized )); - assert!(matches!(service.on_data_ready(), ImuEvent::Idle)); + assert!(matches!(service.on_periodic_tick(), ImuEvent::Idle)); + assert!(matches!( + service.on_transfer_complete(SpiCompletion::Complete), + ImuEvent::Sample(_) + )); } } diff --git a/libs/kaptan-stm32f7/src/stm32f722/clock.rs b/libs/kaptan-stm32f7/src/stm32f722/clock.rs index 7a3918d..19ca06a 100644 --- a/libs/kaptan-stm32f7/src/stm32f722/clock.rs +++ b/libs/kaptan-stm32f7/src/stm32f722/clock.rs @@ -1,54 +1,89 @@ -//! Deterministic STM32F722 clock-source selection. +//! Deterministic STM32F722 PLL clock-source selection. use stm32f7::stm32f722; const CLOCK_SWITCH_POLL_LIMIT: u16 = 1_024; -/// Failure to establish the board's HSI16 clock profile. +/// Failure to establish the 216 MHz PLL clock profile. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ClockProfileError { - /// HSI did not become ready within the bounded polling budget. HsiNotReady, - /// The system clock did not switch to HSI within the bounded polling budget. SystemClockNotHsi, + PllNotStopped, + PllNotReady, + OverDriveNotReady, + OverDriveSwitchNotReady, + SystemClockNotPll, } -/// Selects 16 MHz HSI as SYSCLK with undivided AHB, APB1, and APB2 clocks. +/// Selects the board's HSI PLL profile. /// -/// This profile is deliberately selected even when an earlier boot stage left -/// the device running from a PLL. It establishes the clock assumptions used by -/// DWT timestamps, USART1 baud calculation, and TIM2 heartbeat scheduling. -pub fn select_hsi16_profile(rcc: &stm32f722::RCC) -> Result<(), ClockProfileError> { +/// SYSCLK/HCLK run at 216 MHz; APB1 is 54 MHz (timers run at 108 MHz), and +/// APB2 is 108 MHz. HSI is the PLL source, so no external-crystal assumption +/// is introduced. +pub fn select_pll216_profile( + rcc: &stm32f722::RCC, + pwr: &stm32f722::PWR, + flash: &stm32f722::FLASH, +) -> Result<(), ClockProfileError> { rcc.cr().modify(|_, write| write.hsion().on()); - if !wait_for_hsi(rcc) { + if !wait_until(|| rcc.cr().read().hsirdy().is_ready()) { return Err(ClockProfileError::HsiNotReady); } - rcc.cfgr() - .modify(|_, write| write.sw().hsi().hpre().div1().ppre1().div1().ppre2().div1()); - if !wait_for_system_clock_hsi(rcc) { + // Return to HSI before changing a PLL left configured by an earlier boot stage. + rcc.cfgr().modify(|_, write| write.sw().hsi()); + if !wait_until(|| rcc.cfgr().read().sws().is_hsi()) { return Err(ClockProfileError::SystemClockNotHsi); } + rcc.cr().modify(|_, write| write.pllon().off()); + if !wait_until(|| !rcc.cr().read().pllrdy().is_ready()) { + return Err(ClockProfileError::PllNotStopped); + } + // Scale 1, over-drive and seven flash wait states are required at 216 MHz. + rcc.apb1enr().modify(|_, write| write.pwren().set_bit()); + pwr.cr1().modify(|read, write| unsafe { + write.bits((read.bits() & !(0b11 << 14)) | (0b11 << 14) | (1 << 16)) + }); + if !wait_until(|| pwr.csr1().read().odrdy().bit_is_set()) { + return Err(ClockProfileError::OverDriveNotReady); + } + pwr.cr1().modify(|_, write| write.odswen().set_bit()); + if !wait_until(|| pwr.csr1().read().odswrdy().bit_is_set()) { + return Err(ClockProfileError::OverDriveSwitchNotReady); + } + flash.acr().modify(|read, write| unsafe { + write.bits((read.bits() & !0x0f) | 7 | (1 << 8) | (1 << 9)) + }); + + // HSI / 16 * 432 / 2 = 216 MHz; Q=9 supplies a 48 MHz domain. + rcc.pllcfgr() + .write(|write| unsafe { write.bits(16 | (432 << 6) | (9 << 24)) }); + rcc.cr().modify(|_, write| write.pllon().on()); + if !wait_until(|| rcc.cr().read().pllrdy().is_ready()) { + return Err(ClockProfileError::PllNotReady); + } + rcc.cfgr().modify(|read, write| unsafe { + // AHB /1, APB1 /4, APB2 /2, SYSCLK = PLL. + write.bits( + (read.bits() & !((0x0f << 4) | (0x07 << 10) | (0x07 << 13) | 0x03)) + | (0b101 << 10) + | (0b100 << 13) + | 0b10, + ) + }); + if !wait_until(|| rcc.cfgr().read().sws().is_pll()) { + return Err(ClockProfileError::SystemClockNotPll); + } Ok(()) } -fn wait_for_hsi(rcc: &stm32f722::RCC) -> bool { +fn wait_until(mut ready: impl FnMut() -> bool) -> bool { for _ in 0..CLOCK_SWITCH_POLL_LIMIT { - if rcc.cr().read().hsirdy().is_ready() { + if ready() { return true; } } - - false -} - -fn wait_for_system_clock_hsi(rcc: &stm32f722::RCC) -> bool { - for _ in 0..CLOCK_SWITCH_POLL_LIMIT { - if rcc.cfgr().read().sws().is_hsi() { - return true; - } - } - false } diff --git a/libs/kaptan-stm32f7/src/stm32f722/mod.rs b/libs/kaptan-stm32f7/src/stm32f722/mod.rs index 318d154..af5f000 100644 --- a/libs/kaptan-stm32f7/src/stm32f722/mod.rs +++ b/libs/kaptan-stm32f7/src/stm32f722/mod.rs @@ -12,7 +12,7 @@ mod time; mod usart; mod usart1_tx_dma2_stream7; -pub use clock::{ClockProfileError, select_hsi16_profile}; +pub use clock::{ClockProfileError, select_pll216_profile}; pub use dma_buffer::DcacheDisabledDmaMemory; pub use gpio::{ ActiveLowLedPd2, configure_exti4_pin_pa4, configure_spi1_pins_pa5_pa6_pa7, diff --git a/libs/kaptan-stm32f7/src/stm32f722/spi1.rs b/libs/kaptan-stm32f7/src/stm32f722/spi1.rs index deb7718..af5840f 100644 --- a/libs/kaptan-stm32f7/src/stm32f722/spi1.rs +++ b/libs/kaptan-stm32f7/src/stm32f722/spi1.rs @@ -27,7 +27,10 @@ pub struct Spi1 { } impl Spi1 { - /// Configures SPI1 mode 0 master operation at APB2/64 and reserves DMA2. + /// Configures SPI1 mode 0 master operation at APB2/128 and reserves DMA2. + /// + /// This keeps the MPU6000 route below 1 MHz with the board's 108 MHz + /// APB2 clock while retaining substantial margin for board-level wiring. pub fn new( rcc: &stm32f722::RCC, peripheral: stm32f722::SPI1, @@ -44,7 +47,7 @@ impl Spi1 { .mstr() .master() .br() - .div64() + .div128() .ssm() .set_bit() .ssi() @@ -52,8 +55,7 @@ impl Spi1 { }); peripheral .cr2() - .write(|write| write.ds().eight_bit().frxth().set_bit()); - peripheral.cr1().modify(|_, write| write.spe().set_bit()); + .write(|write| write.ds().eight_bit().frxth().set_bit().errie().set_bit()); Self { peripheral, @@ -77,10 +79,27 @@ impl Spi1 { return Err(Spi1Error::Busy); } + // Arm the complete DMA receive path before SPI starts generating + // clocks. This avoids an RX FIFO overrun between transactions. + self.peripheral + .cr1() + .modify(|_, write| write.spe().clear_bit()); + self.clear_spi_overrun(); self.clear_rx_flags(); self.clear_tx_flags(); self.configure_rx(receive); self.configure_tx(transmit); + self.peripheral.cr2().modify(|_, write| { + let write = write.rxdmaen().set_bit().txdmaen().set_bit(); + if transmit.len() & 1 == 0 { + write.ldma_rx().even().ldma_tx().even() + } else { + // With 8-bit frames, F7 SPI needs this for the final + // byte of an odd-length DMA transaction. MPU samples are + // one command byte plus fourteen data bytes. + write.ldma_rx().odd().ldma_tx().odd() + } + }); self.rx_stream() .cr() .modify(|_, write| write.en().enabled()); @@ -88,20 +107,40 @@ impl Spi1 { .cr() .modify(|_, write| write.en().enabled()); self.peripheral - .cr2() - .modify(|_, write| write.rxdmaen().set_bit().txdmaen().set_bit()); + .cr1() + .modify(|_, write| write.spe().set_bit()); self.active = true; Ok(()) } /// Releases controller-side ownership after a DMA terminal event. pub fn complete_transfer(&mut self, _: SpiCompletion) { + // TODO: Abort both DMA streams on peripheral faults. self.peripheral .cr2() .modify(|_, write| write.rxdmaen().clear_bit().txdmaen().clear_bit()); + self.peripheral + .cr1() + .modify(|_, write| write.spe().clear_bit()); self.active = false; } + /// Acknowledges an SPI peripheral overrun interrupt. + /// + /// An overrun is not a DMA-stream error, so it must be observed through + /// SPI1's own IRQ. The reference manual clears OVR by reading DR then SR. + pub fn acknowledge_error_interrupt() -> bool { + // SAFETY: SPI1 is initialized once by `Spi1::new`; this ISR only + // clears its peripheral OVR condition. + let peripheral = unsafe { &*stm32f722::SPI1::ptr() }; + if !peripheral.sr().read().ovr().is_overrun() { + return false; + } + let _ = peripheral.dr().read().bits(); + let _ = peripheral.sr().read().bits(); + true + } + /// Acknowledges the RX terminal IRQ. Invoke only from DMA2 Stream0. pub fn acknowledge_rx_interrupt() -> SpiCompletion { // SAFETY: DMA2 is initialized once by `Spi1::new`; this ISR touches @@ -286,4 +325,11 @@ impl Spi1 { .clear() }); } + + fn clear_spi_overrun(&self) { + if self.peripheral.sr().read().ovr().is_overrun() { + let _ = self.peripheral.dr().read().bits(); + let _ = self.peripheral.sr().read().bits(); + } + } }