From ac54c8308d6a6172eecbb49bb5bfc721e91794c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Sat, 1 Aug 2026 20:54:21 +0300 Subject: [PATCH] platforms/stm32f7: add reusable F722 firmware --- libs/kaptan-dma/src/lib.rs | 2 +- libs/kaptan-i2c/src/lib.rs | 2 +- libs/kaptan-stm32f7/Cargo.toml | 22 - libs/kaptan-stm32f7/src/lib.rs | 14 - libs/kaptan-stm32f7/src/stm32f722/mod.rs | 27 - libs/kaptan-stm32f7/src/stm32f722/usart.rs | 148 ----- platforms/stm32f7/Cargo.toml | 32 + platforms/stm32f7/src/actuator/mod.rs | 135 +++++ platforms/stm32f7/src/actuator/tim3_dshot.rs | 466 +++++++++++++++ platforms/stm32f7/src/actuator/tim3_pwm.rs | 206 +++++++ platforms/stm32f7/src/f722/board.rs | 558 ++++++++++++++++++ platforms/stm32f7/src/f722/clock.rs | 25 + platforms/stm32f7/src/f722/data_channel.rs | 159 +++++ platforms/stm32f7/src/f722/diagnostics.rs | 59 ++ platforms/stm32f7/src/f722/firmware.rs | 328 ++++++++++ platforms/stm32f7/src/f722/interrupts.rs | 132 +++++ platforms/stm32f7/src/f722/mod.rs | 104 ++++ .../stm32f7/src/f722/peripherals}/clock.rs | 39 +- .../src/f722/peripherals}/dma_buffer.rs | 10 +- .../stm32f7/src/f722/peripherals}/gpio.rs | 91 ++- .../stm32f7/src/f722/peripherals}/i2c1.rs | 6 +- platforms/stm32f7/src/f722/peripherals/mod.rs | 33 ++ .../stm32f7/src/f722/peripherals}/spi1.rs | 0 .../stm32f7/src/f722/peripherals/spi3.rs | 335 +++++++++++ .../stm32f7/src/f722/peripherals}/tim2.rs | 18 +- .../stm32f7/src/f722/peripherals}/time.rs | 6 +- .../stm32f7/src/f722/peripherals/usart.rs | 285 +++++++++ .../peripherals}/usart1_tx_dma2_stream7.rs | 4 +- .../peripherals/usart2_tx_dma1_stream6.rs | 219 +++++++ platforms/stm32f7/src/f722/routes.rs | 142 +++++ platforms/stm32f7/src/f722/sensors.rs | 163 +++++ platforms/stm32f7/src/lib.rs | 484 +++++++++++++++ 32 files changed, 4006 insertions(+), 248 deletions(-) delete mode 100644 libs/kaptan-stm32f7/Cargo.toml delete mode 100644 libs/kaptan-stm32f7/src/lib.rs delete mode 100644 libs/kaptan-stm32f7/src/stm32f722/mod.rs delete mode 100644 libs/kaptan-stm32f7/src/stm32f722/usart.rs create mode 100644 platforms/stm32f7/Cargo.toml create mode 100644 platforms/stm32f7/src/actuator/mod.rs create mode 100644 platforms/stm32f7/src/actuator/tim3_dshot.rs create mode 100644 platforms/stm32f7/src/actuator/tim3_pwm.rs create mode 100644 platforms/stm32f7/src/f722/board.rs create mode 100644 platforms/stm32f7/src/f722/clock.rs create mode 100644 platforms/stm32f7/src/f722/data_channel.rs create mode 100644 platforms/stm32f7/src/f722/diagnostics.rs create mode 100644 platforms/stm32f7/src/f722/firmware.rs create mode 100644 platforms/stm32f7/src/f722/interrupts.rs create mode 100644 platforms/stm32f7/src/f722/mod.rs rename {libs/kaptan-stm32f7/src/stm32f722 => platforms/stm32f7/src/f722/peripherals}/clock.rs (63%) rename {libs/kaptan-stm32f7/src/stm32f722 => platforms/stm32f7/src/f722/peripherals}/dma_buffer.rs (71%) rename {libs/kaptan-stm32f7/src/stm32f722 => platforms/stm32f7/src/f722/peripherals}/gpio.rs (59%) rename {libs/kaptan-stm32f7/src/stm32f722 => platforms/stm32f7/src/f722/peripherals}/i2c1.rs (98%) create mode 100644 platforms/stm32f7/src/f722/peripherals/mod.rs rename {libs/kaptan-stm32f7/src/stm32f722 => platforms/stm32f7/src/f722/peripherals}/spi1.rs (100%) create mode 100644 platforms/stm32f7/src/f722/peripherals/spi3.rs rename {libs/kaptan-stm32f7/src/stm32f722 => platforms/stm32f7/src/f722/peripherals}/tim2.rs (74%) rename {libs/kaptan-stm32f7/src/stm32f722 => platforms/stm32f7/src/f722/peripherals}/time.rs (93%) create mode 100644 platforms/stm32f7/src/f722/peripherals/usart.rs rename {libs/kaptan-stm32f7/src/stm32f722 => platforms/stm32f7/src/f722/peripherals}/usart1_tx_dma2_stream7.rs (97%) create mode 100644 platforms/stm32f7/src/f722/peripherals/usart2_tx_dma1_stream6.rs create mode 100644 platforms/stm32f7/src/f722/routes.rs create mode 100644 platforms/stm32f7/src/f722/sensors.rs create mode 100644 platforms/stm32f7/src/lib.rs diff --git a/libs/kaptan-dma/src/lib.rs b/libs/kaptan-dma/src/lib.rs index 8757bc8..80361e9 100644 --- a/libs/kaptan-dma/src/lib.rs +++ b/libs/kaptan-dma/src/lib.rs @@ -8,7 +8,7 @@ /// A borrowed byte range prepared for a device-to-read DMA transfer. /// /// This is a capability rather than a general byte-slice wrapper. It exists -/// only after a board-selected policy proves DMA reachability and CPU-to-device +/// only after a platform-selected policy proves DMA reachability and CPU-to-device /// visibility for the selected controller. pub struct DmaReadableBuffer<'a> { bytes: &'a [u8], diff --git a/libs/kaptan-i2c/src/lib.rs b/libs/kaptan-i2c/src/lib.rs index ba3a20f..3b1b9de 100644 --- a/libs/kaptan-i2c/src/lib.rs +++ b/libs/kaptan-i2c/src/lib.rs @@ -42,7 +42,7 @@ pub trait I2cBus { /// [`Self::start_read`] for the completed read transaction. fn complete_transaction(&mut self, completion: I2cCompletion, read: Option<&mut [u8]>); - /// Cancels an in-flight transaction after a board-selected timeout. + /// Cancels an in-flight transaction after a platform-selected timeout. /// /// Implementations must release controller-owned transaction state and /// arrange for any required STOP or peripheral recovery without waiting. diff --git a/libs/kaptan-stm32f7/Cargo.toml b/libs/kaptan-stm32f7/Cargo.toml deleted file mode 100644 index dccbec2..0000000 --- a/libs/kaptan-stm32f7/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "kaptan-stm32f7" -edition.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true -version.workspace = true - -[lints] -workspace = true - -[features] -default = [] -stm32f722 = ["stm32f7/stm32f722"] - -[dependencies] -cortex-m = "0.7" -kaptan-dma = { path = "../kaptan-dma" } -kaptan-i2c = { path = "../kaptan-i2c" } -kaptan-spi = { path = "../kaptan-spi" } -kaptan-time = { path = "../kaptan-time" } -stm32f7 = { version = "0.16", default-features = false } diff --git a/libs/kaptan-stm32f7/src/lib.rs b/libs/kaptan-stm32f7/src/lib.rs deleted file mode 100644 index 4e422ea..0000000 --- a/libs/kaptan-stm32f7/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -//! STM32F7 peripheral drivers used by Kaptan boards. -//! -//! This crate is organized by MCU variant and peripheral route. A board chooses -//! an MCU feature, owns its pin muxing and RTIC interrupt graph, then composes -//! the typed peripheral drivers from the selected module. Generic telemetry -//! policy and frame ownership deliberately do not belong here. - -#![no_std] - -#[cfg(not(feature = "stm32f722"))] -compile_error!("select one supported STM32F7 variant feature, such as `stm32f722`"); - -#[cfg(feature = "stm32f722")] -pub mod stm32f722; diff --git a/libs/kaptan-stm32f7/src/stm32f722/mod.rs b/libs/kaptan-stm32f7/src/stm32f722/mod.rs deleted file mode 100644 index 55036d3..0000000 --- a/libs/kaptan-stm32f7/src/stm32f722/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -//! STM32F722 peripheral implementations selected by supported boards. -//! -//! This module exports only typed peripheral building blocks. A board selects -//! pins, clocks, interrupt priorities, and static ownership. - -mod clock; -mod dma_buffer; -mod gpio; -mod i2c1; -mod spi1; -mod tim2; -mod time; -mod usart; -mod usart1_tx_dma2_stream7; - -pub use clock::{ClockProfileError, select_pll216_profile}; -pub use dma_buffer::DcacheDisabledDmaMemory; -pub use gpio::{ - ActiveLowLedPd2, configure_exti4_pin_pa4, configure_i2c1_pins_pb8_pb9, - configure_spi1_pins_pa5_pa6_pa7, configure_usart1_rx_pin_pa10, configure_usart1_tx_pin_pa9, -}; -pub use i2c1::{I2c1, I2c1Config, I2c1Error}; -pub use spi1::{Spi1, Spi1Error}; -pub use tim2::{Tim2Periodic, Tim2PeriodicConfig}; -pub use time::{DwtClockStartError, DwtMicrosecondClock}; -pub use usart::{BaudRateError, ReceiveInterrupt, Usart1Tx}; -pub use usart1_tx_dma2_stream7::{StartError, Usart1TxDma}; diff --git a/libs/kaptan-stm32f7/src/stm32f722/usart.rs b/libs/kaptan-stm32f7/src/stm32f722/usart.rs deleted file mode 100644 index 48bd90a..0000000 --- a/libs/kaptan-stm32f7/src/stm32f722/usart.rs +++ /dev/null @@ -1,148 +0,0 @@ -//! USART1 transmit configuration for STM32F722. - -use stm32f7::stm32f722; - -/// UART baud-rate configuration error. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum BaudRateError { - /// Zero is not a valid baud rate. - Zero, - /// The requested divisor cannot fit USART BRR. - OutOfRange, -} - -/// Terminal result from acknowledging the USART1 receive interrupt. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum ReceiveInterrupt { - /// One received byte was removed from the UART data register. - Byte(u8), - /// A serial transport fault invalidated the current receive stream. - Fault, - /// This USART1 interrupt did not have a receive event pending. - NotPending, -} - -/// Owns configured full-duplex USART1 hardware. -/// -/// Pin alternate-function setup remains board-owned because it depends on the -/// schematic. Configure pins before constructing this type. -pub struct Usart1Tx { - peripheral: stm32f722::USART1, -} - -impl Usart1Tx { - /// Enables USART1 and configures 8N1 transmit and receive operation. - pub fn new( - rcc: &stm32f722::RCC, - peripheral: stm32f722::USART1, - peripheral_clock_hz: u32, - baud: u32, - ) -> Result { - let brr = calculate_brr(peripheral_clock_hz, baud)?; - - rcc.apb2rstr() - .modify(|_, write| write.usart1rst().set_bit()); - rcc.apb2rstr() - .modify(|_, write| write.usart1rst().clear_bit()); - rcc.apb2enr().modify(|_, write| write.usart1en().set_bit()); - peripheral.brr().write(|write| { - // SAFETY: `brr` is validated to fit the complete BRR register. - unsafe { write.bits(u32::from(brr)) } - }); - peripheral.cr1().write(|write| { - write - .te() - .set_bit() - .re() - .set_bit() - .rxneie() - .set_bit() - .ue() - .set_bit() - }); - peripheral.cr2().write(|write| write.stop().stop1()); - - Ok(Self { peripheral }) - } - - pub(crate) fn enable_dma_transmit(&self) { - self.peripheral.cr3().write(|write| write.dmat().set_bit()); - } - - pub(crate) fn transmit_data_register_address(&self) -> u32 { - self.peripheral.tdr().as_ptr() as u32 - } - - /// Acknowledges one USART1 receive interrupt without accessing TX state. - /// - /// The board invokes this only from the `USART1` interrupt. USART1 TX DMA - /// owns its disjoint data register and DMA stream, so this static route may - /// safely acknowledge RX while that transfer is active. - pub fn acknowledge_receive_interrupt() -> ReceiveInterrupt { - // SAFETY: the board binds USART1 exactly once and this function touches - // only its receive/error registers, never the TX DMA configuration. - let peripheral = unsafe { &*stm32f722::USART1::ptr() }; - let status = peripheral.isr().read(); - let fault = status.pe().is_error() - || status.fe().is_error() - || status.nf().is_noise() - || status.ore().is_overrun(); - - if status.rxne().is_data_ready() { - let byte = peripheral.rdr().read().rdr().bits() as u8; - if fault { - clear_receive_errors(peripheral); - ReceiveInterrupt::Fault - } else { - ReceiveInterrupt::Byte(byte) - } - } else if fault { - clear_receive_errors(peripheral); - ReceiveInterrupt::Fault - } else { - ReceiveInterrupt::NotPending - } - } -} - -fn clear_receive_errors(peripheral: &stm32f722::usart1::RegisterBlock) { - peripheral.icr().write(|write| { - write - .pecf() - .clear() - .fecf() - .clear() - .ncf() - .clear() - .orecf() - .clear() - }); -} - -const fn calculate_brr(peripheral_clock_hz: u32, baud: u32) -> Result { - if baud == 0 { - return Err(BaudRateError::Zero); - } - - let divisor = (peripheral_clock_hz + (baud / 2)) / baud; - if divisor == 0 || divisor > u16::MAX as u32 { - Err(BaudRateError::OutOfRange) - } else { - Ok(divisor as u16) - } -} - -#[cfg(test)] -mod tests { - use super::{BaudRateError, calculate_brr}; - - #[test] - fn calculates_reset_clock_brr() { - assert_eq!(calculate_brr(16_000_000, 115_200), Ok(139)); - } - - #[test] - fn rejects_zero_baud() { - assert_eq!(calculate_brr(16_000_000, 0), Err(BaudRateError::Zero)); - } -} diff --git a/platforms/stm32f7/Cargo.toml b/platforms/stm32f7/Cargo.toml new file mode 100644 index 0000000..ae8843a --- /dev/null +++ b/platforms/stm32f7/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "platform-stm32f7" +edition.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true +version.workspace = true + +[lints] +workspace = true + +[features] +default = [] + +[dependencies] +kaptan-actuator = { path = "../../libs/kaptan-actuator" } +kaptan-board = { path = "../../libs/kaptan-board" } +kaptan-data-channel = { path = "../../libs/kaptan-data-channel" } +kaptan-dma = { path = "../../libs/kaptan-dma" } +kaptan-drivers = { path = "../../libs/kaptan-drivers" } +kaptan-i2c = { path = "../../libs/kaptan-i2c" } +kaptan-mavlink = { path = "../../libs/kaptan-mavlink" } +kaptan-sensor = { path = "../../libs/kaptan-sensor" } +kaptan-spi = { path = "../../libs/kaptan-spi" } +kaptan-time = { path = "../../libs/kaptan-time" } +cortex-m = "0.7" +cortex-m-rt = "0.7" +rtic = { version = "2.3", default-features = false, features = ["thumbv7-backend"] } +stm32f7 = { version = "0.16", default-features = false, features = ["rt", "stm32f722"] } + +[target.'cfg(target_arch = "arm")'.dependencies] +panic-halt = "1" diff --git a/platforms/stm32f7/src/actuator/mod.rs b/platforms/stm32f7/src/actuator/mod.rs new file mode 100644 index 0000000..cce5227 --- /dev/null +++ b/platforms/stm32f7/src/actuator/mod.rs @@ -0,0 +1,135 @@ +#![cfg_attr(not(target_arch = "arm"), allow(dead_code))] + +mod tim3_dshot; +mod tim3_pwm; + +use kaptan_actuator::{MotorOutput, MotorProtocol}; +use stm32f7::stm32f722; + +use self::{tim3_dshot::Tim3Dshot, tim3_pwm::Tim3Pwm}; +use crate::f722::MotorRoute; + +/// STM32F722 TIM3/TIM5 protocol-neutral M1–M4 output route. +/// +/// TIM3 channels 1–4 use PB4, PB5, PB0, and PB1 respectively. Kakute's +/// M1–M4 connector order is PB0, PB1, PB4, PB5, while GEPRC's is PB4, PB5, +/// PB0, PB1. The route descriptor therefore supplies the logical-to-channel +/// map; no board inherits another board's motor numbering. +/// Opaque owner of the F722 motor peripherals used by the RTIC application. +/// +/// The type is public only because RTIC exposes resource metadata in generated +/// public items. Its fields and construction remain platform-private. +pub struct F722MotorOutput { + transport: Transport, +} + +enum Transport { + Pwm(Tim3Pwm), + Dshot(Tim3Dshot), +} + +impl F722MotorOutput { + pub(crate) fn new( + rcc: &stm32f722::RCC, + tim3: stm32f722::TIM3, + tim5: stm32f722::TIM5, + protocol: MotorProtocol, + route: MotorRoute, + ) -> Self { + let logical_to_tim3_channel = logical_to_tim3_channel(route); + let transport = match protocol { + MotorProtocol::Pwm(config) => { + let _ = tim5; + Transport::Pwm(Tim3Pwm::new(rcc, tim3, config)) + } + MotorProtocol::Dshot(config) => Transport::Dshot(Tim3Dshot::new( + rcc, + tim3, + tim5, + config, + logical_to_tim3_channel, + )), + }; + Self { transport } + } + + /// Services TIM3 while RTIC holds this motor owner's resource lock. + pub(crate) fn on_tim3_interrupt(&mut self) { + if matches!(self.transport, Transport::Pwm(_)) { + Tim3Pwm::on_tim3_update(); + } + } + + /// Services TIM5 while RTIC holds this motor owner's resource lock. + pub(crate) fn on_tim5_interrupt(&mut self) { + if matches!(self.transport, Transport::Dshot(_)) { + Tim3Dshot::on_tim5_update(); + } + } + + /// Begins the configured arming sequence. + pub(crate) fn arm(&mut self) { + ::arm(self); + } + + /// Immediately returns the outputs to their disarmed state. + pub(crate) fn disarm(&mut self) { + ::disarm(self); + } + + /// Returns the arming state of this uniquely owned output route. + pub(crate) fn is_armed(&self) -> bool { + ::is_armed(self) + } +} + +/// Maps logical M1–M4 indices to zero-based TIM3 channel indices. +fn logical_to_tim3_channel(route: MotorRoute) -> [usize; 4] { + match route { + // Kakute: M1=PB0/CH3, M2=PB1/CH4, M3=PB4/CH1, M4=PB5/CH2. + MotorRoute::KakuteTim3Tim5 => [2, 3, 0, 1], + // GEPRC: M1=PB4/CH1, M2=PB5/CH2, M3=PB0/CH3, M4=PB1/CH4. + MotorRoute::GeprcTim3Tim5 => [0, 1, 2, 3], + } +} + +impl MotorOutput for F722MotorOutput { + fn arm(&mut self) { + match &mut self.transport { + Transport::Pwm(output) => output.arm(), + Transport::Dshot(output) => output.arm(), + } + } + + fn disarm(&mut self) { + match &mut self.transport { + Transport::Pwm(output) => output.disarm(), + Transport::Dshot(output) => output.disarm(), + } + } + + fn is_armed(&self) -> bool { + match &self.transport { + Transport::Pwm(output) => output.is_armed(), + Transport::Dshot(output) => output.is_armed(), + } + } +} + +#[cfg(test)] +mod tests { + use super::logical_to_tim3_channel; + use crate::f722::MotorRoute; + + #[test] + fn keeps_each_supported_board_motor_order_explicit() { + assert_eq!( + logical_to_tim3_channel(MotorRoute::KakuteTim3Tim5), + [2, 3, 0, 1] + ); + assert_eq!( + logical_to_tim3_channel(MotorRoute::GeprcTim3Tim5), + [0, 1, 2, 3] + ); + } +} diff --git a/platforms/stm32f7/src/actuator/tim3_dshot.rs b/platforms/stm32f7/src/actuator/tim3_dshot.rs new file mode 100644 index 0000000..7984a01 --- /dev/null +++ b/platforms/stm32f7/src/actuator/tim3_dshot.rs @@ -0,0 +1,466 @@ +use core::sync::atomic::{AtomicBool, Ordering}; + +use kaptan_actuator::dshot::{ + ArmSequence, Config, FRAME_SYMBOLS, FrameValue, Throttle, Timing, symbols, +}; +use stm32f7::stm32f722; + +const APB1_TIMER_CLOCK_HZ: u32 = 108_000_000; +const DSHOT_CHANNELS: usize = 4; +const DSHOT_BUFFER_LENGTH: usize = FRAME_SYMBOLS * DSHOT_CHANNELS; +const TIM3_CHANNEL_ENABLE: u32 = 0x1111; +const PWM_MODE_1_WITH_PRELOAD: u32 = 0x6868; +const TIM3_DMAR_CCR1_OFFSET: u8 = 13; +const DEFAULT_ARMED_IDLE_THROTTLE: Throttle = match Throttle::new(48) { + Some(throttle) => throttle, + None => panic!("valid DShot throttle"), +}; + +// These static buffers live in DMA-visible SRAM. They are initialized before +// DMA is enabled and are subsequently read-only to the DMA controller. +static mut DSHOT_ZERO_FRAME: [u32; DSHOT_BUFFER_LENGTH] = [0; DSHOT_BUFFER_LENGTH]; +static mut DSHOT_ARMED_IDLE_FRAME: [u32; DSHOT_BUFFER_LENGTH] = [0; DSHOT_BUFFER_LENGTH]; +static ARM_SEQUENCE: ArmSequence = ArmSequence::new(DEFAULT_ARMED_IDLE_THROTTLE, 0); +static DMA_RUNNING: AtomicBool = AtomicBool::new(false); + +pub(super) struct Tim3Dshot { + timer: stm32f722::TIM3, + dma_available: bool, +} + +impl Tim3Dshot { + pub(super) fn new( + rcc: &stm32f722::RCC, + timer: stm32f722::TIM3, + scheduler: stm32f722::TIM5, + config: Config, + logical_to_tim3_channel: [usize; DSHOT_CHANNELS], + ) -> Self { + let rate = config.rate(); + let dshot_timer_hz = rate.timer_base_hz(); + let timing = match Timing::from_timer_clock(dshot_timer_hz, rate) { + Some(timing) => timing, + None => unreachable!(), + }; + let timer_prescaler = APB1_TIMER_CLOCK_HZ / dshot_timer_hz; + assert!(APB1_TIMER_CLOCK_HZ.is_multiple_of(dshot_timer_hz)); + ARM_SEQUENCE + .configure_zero_frames(rate.frames_for_millis(config.arming_duration().milliseconds())); + ARM_SEQUENCE.configure_armed_throttle(config.armed_idle_throttle()); + + rcc.ahb1enr().modify(|_, write| write.gpioben().set_bit()); + rcc.ahb1enr().modify(|_, write| write.dma1en().set_bit()); + rcc.apb1rstr().modify(|_, write| write.tim3rst().set_bit()); + rcc.apb1rstr() + .modify(|_, write| write.tim3rst().clear_bit()); + rcc.apb1enr().modify(|_, write| write.tim3en().set_bit()); + + timer + .cr1() + .modify(|_, write| write.cen().clear_bit().arpe().enabled()); + timer.dier().modify(|_, write| write.ude().disabled()); + timer.ccer().write(|write| { + // SAFETY: zero is the documented all-channels-disabled CCER encoding while TIM3 is stopped. + unsafe { write.bits(0) } + }); + timer.psc().write(|write| { + // SAFETY: `timer_prescaler` divides the fixed APB1 timer clock and fits the PSC field. + unsafe { write.psc().bits((timer_prescaler - 1) as u16) } + }); + timer.arr().write(|write| { + // SAFETY: `Timing` validates a non-zero ARR value representable by TIM3. + unsafe { write.bits(u32::from(timing.bit_ticks() - 1)) } + }); + timer.ccmr1_output().write(|write| { + // SAFETY: this constant selects documented PWM mode 1 with preload for channels 1 and 2. + unsafe { write.bits(PWM_MODE_1_WITH_PRELOAD) } + }); + timer.ccmr2_output().write(|write| { + // SAFETY: this constant selects documented PWM mode 1 with preload for channels 3 and 4. + unsafe { write.bits(PWM_MODE_1_WITH_PRELOAD) } + }); + timer.dcr().write(|write| { + // SAFETY: the DMA burst starts at CCR1 and covers exactly four contiguous compare registers. + unsafe { + write + .dba() + .bits(TIM3_DMAR_CCR1_OFFSET) + .dbl() + .bits((DSHOT_CHANNELS - 1) as u8) + } + }); + timer.egr().write(|write| write.ug().set_bit()); + let dma_available = disable_dma_stream(); + initialize_dma_frames( + timing, + config.armed_idle_throttle(), + logical_to_tim3_channel, + ); + + rcc.apb1rstr().modify(|_, write| write.tim5rst().set_bit()); + rcc.apb1rstr() + .modify(|_, write| write.tim5rst().clear_bit()); + rcc.apb1enr().modify(|_, write| write.tim5en().set_bit()); + scheduler.psc().write(|write| { + // SAFETY: zero is a valid TIM5 prescaler and keeps the scheduler at the APB1 timer clock. + unsafe { write.psc().bits(0) } + }); + scheduler.arr().write(|write| { + // SAFETY: each supported DShot rate divides APB1_TIMER_CLOCK_HZ, yielding a valid ARR value. + unsafe { write.bits(APB1_TIMER_CLOCK_HZ / rate.output_rate_hz() - 1) } + }); + scheduler.egr().write(|write| write.ug().set_bit()); + scheduler.sr().modify(|_, write| write.uif().clear()); + scheduler.dier().write(|write| write.uie().disabled()); + scheduler.cr1().write(|write| write.cen().clear_bit()); + configure_gpio_low(); + + Self { + timer, + dma_available, + } + } + + pub(super) fn arm(&mut self) { + if ARM_SEQUENCE.is_armed() { + return; + } + if !self.dma_available { + self.dma_available = disable_dma_stream(); + if !self.dma_available { + return; + } + } + ARM_SEQUENCE.arm(); + if !self.start_stream(zero_frame()) { + ARM_SEQUENCE.disarm(); + return; + } + // SAFETY: TIM5 was uniquely taken during initialization and remains owned by this + // platform resource; RTIC serializes this method with `on_tim5_update`. + let scheduler = unsafe { &*stm32f722::TIM5::ptr() }; + scheduler.sr().modify(|_, write| write.uif().clear()); + scheduler.dier().modify(|_, write| write.uie().enabled()); + scheduler.cr1().modify(|_, write| write.cen().set_bit()); + } + + pub(super) fn disarm(&mut self) { + ARM_SEQUENCE.disarm(); + // SAFETY: TIM5 is owned by this motor resource and cannot be accessed concurrently + // because RTIC locks that resource for both the service IRQ and this method. + let scheduler = unsafe { &*stm32f722::TIM5::ptr() }; + scheduler.cr1().modify(|_, write| write.cen().clear_bit()); + scheduler.dier().modify(|_, write| write.uie().disabled()); + self.stop_stream(); + self.timer.ccer().write(|write| { + // SAFETY: zero disables all TIM3 channels after its stream and counter have stopped. + unsafe { write.bits(0) } + }); + configure_gpio_low(); + } + + pub(super) fn is_armed(&self) -> bool { + ARM_SEQUENCE.is_armed() + } + + fn start_stream(&mut self, frame: *const u32) -> bool { + self.timer.cr1().modify(|_, write| write.cen().clear_bit()); + DMA_RUNNING.store(false, Ordering::Release); + if !configure_dma_stream(frame) { + return false; + } + self.timer.ccer().write(|write| { + // SAFETY: this enables exactly TIM3 channels 1 through 4 in output mode. + unsafe { write.bits(TIM3_CHANNEL_ENABLE) } + }); + configure_gpio_alternate_function(); + self.timer.egr().write(|write| write.ug().set_bit()); + DMA_RUNNING.store(true, Ordering::Release); + dma_stream().cr().modify(|_, write| write.en().enabled()); + self.timer.dier().modify(|_, write| write.ude().enabled()); + self.timer.cr1().modify(|_, write| write.cen().set_bit()); + true + } + + fn stop_stream(&mut self) { + self.timer.cr1().modify(|_, write| write.cen().clear_bit()); + DMA_RUNNING.store(false, Ordering::Release); + self.timer.dier().modify(|_, write| write.ude().disabled()); + let _ = disable_dma_stream(); + } + + pub(super) fn on_tim5_update() { + // SAFETY: TIM5 is owned by the motor resource; its RTIC lock excludes arm/disarm while + // the update handler clears its flag. + let scheduler = unsafe { &*stm32f722::TIM5::ptr() }; + scheduler.sr().modify(|_, write| write.uif().clear()); + // SAFETY: TIM3 is uniquely owned by the same platform resource and not otherwise stolen. + let timer = unsafe { &*stm32f722::TIM3::ptr() }; + timer.dier().modify(|_, write| write.ude().disabled()); + if !disable_dma_stream() { + return; + } + if !DMA_RUNNING.load(Ordering::Acquire) { + return; + } + let frame = match ARM_SEQUENCE.next_frame() { + Some(FrameValue::Stop) => zero_frame(), + Some(FrameValue::Throttle(_)) => armed_idle_frame(), + None => return, + }; + dma_stream().ndtr().write(|write| { + // SAFETY: the fixed frame length is the exact initialized DMA buffer length. + unsafe { write.bits(DSHOT_BUFFER_LENGTH as u32) } + }); + dma_stream().m0ar().write(|write| { + // SAFETY: `frame` is one of the static, aligned DMA-visible frame buffers. + unsafe { write.bits(frame as u32) } + }); + dma_stream().cr().modify(|_, write| write.en().enabled()); + timer.dier().modify(|_, write| write.ude().enabled()); + } +} + +fn initialize_dma_frames( + timing: Timing, + armed_idle: Throttle, + logical_to_tim3_channel: [usize; DSHOT_CHANNELS], +) { + write_dma_frame( + core::ptr::addr_of_mut!(DSHOT_ZERO_FRAME).cast(), + FrameValue::Stop, + timing, + logical_to_tim3_channel, + ); + write_dma_frame( + core::ptr::addr_of_mut!(DSHOT_ARMED_IDLE_FRAME).cast(), + FrameValue::Throttle(armed_idle), + timing, + logical_to_tim3_channel, + ); +} + +fn write_dma_frame( + destination: *mut u32, + value: FrameValue, + timing: Timing, + logical_to_tim3_channel: [usize; DSHOT_CHANNELS], +) { + let symbols = symbols( + value.packet(), + timing.zero_high_ticks(), + timing.one_high_ticks(), + ); + let mut bit = 0; + while bit < FRAME_SYMBOLS { + let mut logical_motor = 0; + while logical_motor < DSHOT_CHANNELS { + let channel = logical_to_tim3_channel[logical_motor]; + // SAFETY: `destination` identifies one of the exclusive static frame buffers during + // initialization. The selected platform route is a permutation of + // the four TIM3 channels, so the bounded indices stay within + // DSHOT_BUFFER_LENGTH and each DMA word is initialized once. + unsafe { + destination + .add(bit * DSHOT_CHANNELS + channel) + .write(symbols[bit] as u32) + }; + logical_motor += 1; + } + bit += 1; + } +} + +fn zero_frame() -> *const u32 { + core::ptr::addr_of!(DSHOT_ZERO_FRAME).cast() +} +fn armed_idle_frame() -> *const u32 { + core::ptr::addr_of!(DSHOT_ARMED_IDLE_FRAME).cast() +} + +fn dma() -> &'static stm32f722::dma2::RegisterBlock { + // SAFETY: DMA1 stream 2 is exclusively claimed by Tim3Dshot; all access to this register + // block is serialized through the platform's RTIC-owned motor state. + unsafe { &*stm32f722::DMA1::ptr() } +} +fn dma_stream() -> &'static stm32f722::dma2::ST { + dma().st(2) +} + +fn disable_dma_stream() -> bool { + dma_stream().cr().modify(|_, write| write.en().disabled()); + let mut spins = 1_024; + while dma_stream().cr().read().en().is_enabled() && spins != 0 { + spins -= 1; + } + if dma_stream().cr().read().en().is_enabled() { + return false; + } + dma().lifcr().write(|write| { + write + .cfeif2() + .clear() + .cdmeif2() + .clear() + .cteif2() + .clear() + .chtif2() + .clear() + .ctcif2() + .clear() + }); + true +} + +fn configure_dma_stream(frame: *const u32) -> bool { + if !disable_dma_stream() { + return false; + } + dma_stream().cr().write(|write| { + // SAFETY: stream 2 is disabled above; channel 5 and the width/direction settings are + // the documented DMA1/TIM3_UP route for this fixed u32 frame buffer. + unsafe { + write + .chsel() + .bits(5) + .dir() + .memory_to_peripheral() + .pl() + .very_high() + .minc() + .incremented() + .pinc() + .fixed() + .msize() + .bits32() + .psize() + .bits32() + .circ() + .disabled() + .tcie() + .disabled() + .teie() + .disabled() + .dmeie() + .disabled() + } + }); + dma_stream().ndtr().write(|write| { + // SAFETY: transfer count equals the fixed initialized DMA frame length. + unsafe { write.bits(DSHOT_BUFFER_LENGTH as u32) } + }); + dma_stream().par().write(|write| { + // SAFETY: the address is TIM3's DMA-burst register, the fixed peripheral endpoint. + unsafe { write.bits(tim3_dmar_address()) } + }); + dma_stream().m0ar().write(|write| { + // SAFETY: `frame` is a static aligned buffer retained for the entire DMA transfer. + unsafe { write.bits(frame as u32) } + }); + dma_stream().fcr().write(|write| { + // SAFETY: FIFO threshold 3 and direct-mode disable are documented legal DMA settings. + unsafe { write.fth().bits(3).dmdis().set_bit() } + }); + true +} + +fn tim3_dmar_address() -> u32 { + // SAFETY: TIM3 is uniquely owned by Tim3Dshot; this only obtains the stable MMIO address of + // its DMAR register and does not create mutable Rust access to the peripheral. + unsafe { (&*stm32f722::TIM3::ptr()).dmar().as_ptr() as u32 } +} + +fn port() -> &'static stm32f722::gpiob::RegisterBlock { + // SAFETY: PB0/PB1/PB4/PB5 are exclusively configured as TIM3 motor pins by this platform + // resource; no board code receives GPIOB or this raw register block. + unsafe { &*stm32f722::GPIOB::ptr() } +} + +fn configure_gpio_low() { + let port = port(); + port.moder().modify(|_, write| { + write + .moder0() + .output() + .moder1() + .output() + .moder4() + .output() + .moder5() + .output() + }); + port.otyper().modify(|_, write| { + write + .ot0() + .push_pull() + .ot1() + .push_pull() + .ot4() + .push_pull() + .ot5() + .push_pull() + }); + port.pupdr().modify(|_, write| { + write + .pupdr0() + .floating() + .pupdr1() + .floating() + .pupdr4() + .floating() + .pupdr5() + .floating() + }); + port.bsrr().write(|write| { + write + .br0() + .set_bit() + .br1() + .set_bit() + .br4() + .set_bit() + .br5() + .set_bit() + }); +} + +fn configure_gpio_alternate_function() { + let port = port(); + port.afrl().modify(|_, write| { + // SAFETY: AF2 is TIM3 on PB0/PB1/PB4/PB5; these fields are disjoint from board-owned + // pins and have been exclusively reserved for motor output by the route profile. + unsafe { + write + .afrl0() + .bits(2) + .afrl1() + .bits(2) + .afrl4() + .bits(2) + .afrl5() + .bits(2) + } + }); + port.moder().modify(|_, write| { + write + .moder0() + .alternate() + .moder1() + .alternate() + .moder4() + .alternate() + .moder5() + .alternate() + }); + port.ospeedr().modify(|_, write| { + write + .ospeedr0() + .high_speed() + .ospeedr1() + .high_speed() + .ospeedr4() + .high_speed() + .ospeedr5() + .high_speed() + }); +} diff --git a/platforms/stm32f7/src/actuator/tim3_pwm.rs b/platforms/stm32f7/src/actuator/tim3_pwm.rs new file mode 100644 index 0000000..6985712 --- /dev/null +++ b/platforms/stm32f7/src/actuator/tim3_pwm.rs @@ -0,0 +1,206 @@ +use core::sync::atomic::{AtomicU16, Ordering}; + +use kaptan_actuator::pwm::{ArmSequence, Config, FrameValue, PulseWidth}; +use stm32f7::stm32f722; + +const APB1_TIMER_CLOCK_HZ: u32 = 108_000_000; +const TIMER_TICK_HZ: u32 = 1_000_000; +const PWM_CHANNEL_ENABLE: u32 = 0x1111; +const PWM_MODE_1_WITH_PRELOAD: u32 = 0x6868; +const DEFAULT_MINIMUM_PULSE: PulseWidth = match PulseWidth::new(1_000) { + Some(pulse) => pulse, + None => panic!("valid PWM pulse"), +}; +const DEFAULT_ARMED_IDLE_PULSE: PulseWidth = match PulseWidth::new(1_000) { + Some(pulse) => pulse, + None => panic!("valid PWM pulse"), +}; + +static ARM_SEQUENCE: ArmSequence = ArmSequence::new(DEFAULT_ARMED_IDLE_PULSE, 0); +static MINIMUM_PULSE: AtomicU16 = AtomicU16::new(DEFAULT_MINIMUM_PULSE.microseconds()); + +pub(super) struct Tim3Pwm { + timer: stm32f722::TIM3, + minimum_pulse: PulseWidth, +} + +impl Tim3Pwm { + pub(super) fn new(rcc: &stm32f722::RCC, timer: stm32f722::TIM3, config: Config) -> Self { + let period_us = match config.frame_rate().period_microseconds() { + Some(period) => period, + None => unreachable!(), + }; + ARM_SEQUENCE.configure_minimum_frames( + config + .frame_rate() + .frames_for_millis(config.arming_duration().milliseconds()), + ); + ARM_SEQUENCE.configure_armed_idle(config.armed_idle_pulse()); + MINIMUM_PULSE.store(config.minimum_pulse().microseconds(), Ordering::Release); + + rcc.ahb1enr().modify(|_, write| write.gpioben().set_bit()); + rcc.apb1rstr().modify(|_, write| write.tim3rst().set_bit()); + rcc.apb1rstr() + .modify(|_, write| write.tim3rst().clear_bit()); + rcc.apb1enr().modify(|_, write| write.tim3en().set_bit()); + + configure_gpio_alternate_function(); + timer + .cr1() + .write(|write| write.cen().clear_bit().arpe().enabled()); + timer.psc().write(|write| { + // SAFETY: the fixed APB1 clock divides exactly to the documented 1 MHz timer tick. + unsafe { + write + .psc() + .bits((APB1_TIMER_CLOCK_HZ / TIMER_TICK_HZ - 1) as u16) + } + }); + timer.arr().write(|write| { + // SAFETY: `frame_rate` guarantees a non-zero period fitting TIM3's ARR register. + unsafe { write.bits(u32::from(period_us - 1)) } + }); + timer.ccmr1_output().write(|write| { + // SAFETY: this is the documented PWM mode 1 plus preload encoding for channels 1 and 2. + unsafe { write.bits(PWM_MODE_1_WITH_PRELOAD) } + }); + timer.ccmr2_output().write(|write| { + // SAFETY: this is the documented PWM mode 1 plus preload encoding for channels 3 and 4. + unsafe { write.bits(PWM_MODE_1_WITH_PRELOAD) } + }); + write_pulse(&timer, config.minimum_pulse()); + timer.ccer().write(|write| { + // SAFETY: this enables only the four TIM3 output channels configured above. + unsafe { write.bits(PWM_CHANNEL_ENABLE) } + }); + timer.egr().write(|write| write.ug().set_bit()); + timer.sr().modify(|_, write| write.uif().clear()); + timer.dier().write(|write| write.uie().enabled()); + timer.cr1().modify(|_, write| write.cen().set_bit()); + + Self { + timer, + minimum_pulse: config.minimum_pulse(), + } + } + + pub(super) fn arm(&mut self) { + ARM_SEQUENCE.arm(); + } + + pub(super) fn disarm(&mut self) { + ARM_SEQUENCE.disarm(); + write_pulse(&self.timer, self.minimum_pulse); + } + + pub(super) fn is_armed(&self) -> bool { + ARM_SEQUENCE.is_armed() + } + + pub(super) fn on_tim3_update() { + // SAFETY: TIM3 is exclusively owned by the platform motor resource; RTIC locks that + // resource across the service task and this update handler. + let timer = unsafe { &*stm32f722::TIM3::ptr() }; + timer.sr().modify(|_, write| write.uif().clear()); + let pulse = match ARM_SEQUENCE.next_frame() { + Some(FrameValue::ArmedIdle(pulse)) => pulse, + Some(FrameValue::Minimum) | None => configured_minimum_pulse(), + }; + write_pulse(timer, pulse); + } +} + +fn configured_minimum_pulse() -> PulseWidth { + match PulseWidth::new(MINIMUM_PULSE.load(Ordering::Acquire)) { + Some(pulse) => pulse, + None => unreachable!(), + } +} + +fn write_pulse(timer: &stm32f722::tim3::RegisterBlock, pulse: PulseWidth) { + let ticks = u32::from(pulse.microseconds()); + timer.ccr1().write(|write| { + // SAFETY: `PulseWidth` bounds ticks to the configured PWM frame interval. + unsafe { write.bits(ticks) } + }); + timer.ccr2().write(|write| { + // SAFETY: `PulseWidth` bounds ticks to the configured PWM frame interval. + unsafe { write.bits(ticks) } + }); + timer.ccr3().write(|write| { + // SAFETY: `PulseWidth` bounds ticks to the configured PWM frame interval. + unsafe { write.bits(ticks) } + }); + timer.ccr4().write(|write| { + // SAFETY: `PulseWidth` bounds ticks to the configured PWM frame interval. + unsafe { write.bits(ticks) } + }); +} + +fn port() -> &'static stm32f722::gpiob::RegisterBlock { + // SAFETY: PB0/PB1/PB4/PB5 are exclusively claimed by the selected TIM3 motor route; board + // code never receives GPIOB and RTIC serializes platform writes to these registers. + unsafe { &*stm32f722::GPIOB::ptr() } +} + +fn configure_gpio_alternate_function() { + let port = port(); + port.afrl().modify(|_, write| { + // SAFETY: AF2 selects TIM3 on these four reserved GPIOB fields, leaving all other pins intact. + unsafe { + write + .afrl0() + .bits(2) + .afrl1() + .bits(2) + .afrl4() + .bits(2) + .afrl5() + .bits(2) + } + }); + port.moder().modify(|_, write| { + write + .moder0() + .alternate() + .moder1() + .alternate() + .moder4() + .alternate() + .moder5() + .alternate() + }); + port.otyper().modify(|_, write| { + write + .ot0() + .push_pull() + .ot1() + .push_pull() + .ot4() + .push_pull() + .ot5() + .push_pull() + }); + port.ospeedr().modify(|_, write| { + write + .ospeedr0() + .high_speed() + .ospeedr1() + .high_speed() + .ospeedr4() + .high_speed() + .ospeedr5() + .high_speed() + }); + port.pupdr().modify(|_, write| { + write + .pupdr0() + .floating() + .pupdr1() + .floating() + .pupdr4() + .floating() + .pupdr5() + .floating() + }); +} diff --git a/platforms/stm32f7/src/f722/board.rs b/platforms/stm32f7/src/f722/board.rs new file mode 100644 index 0000000..c556f00 --- /dev/null +++ b/platforms/stm32f7/src/f722/board.rs @@ -0,0 +1,558 @@ +//! Profile-independent STM32F722 flight state and ownership graph. + +use core::sync::atomic::{AtomicBool, AtomicU8, Ordering}; + +use super::peripherals::{ + DwtMicrosecondClock, I2c1, I2c1Config, ReceiveInterrupt, Tim2Periodic, Uart4, + configure_exti3_pin_pc3, configure_exti4_pin_pa4, configure_i2c1_pins_pb8_pb9, + configure_spi1_pins_pa5_pa6_pa7, configure_spi3_pins_pc10_pc11_pc12, + configure_uart4_pins_pa0_pa1, configure_usart1_rx_pin_pa10, configure_usart1_tx_pin_pa9, + configure_usart2_pins_pa2_pa3, +}; +use kaptan_actuator::{ArmingDuration, MotorProtocol}; +use kaptan_board::{BoardConfig, VerticalSensingKind}; +use kaptan_data_channel::{ + DataChannel, DataSources, LatestImuMailbox, LatestOpticalFlowMailbox, LatestRangeMailbox, +}; +use kaptan_drivers::{ + lidar_lite_v3::{AcquisitionMode, LidarLiteV3, LidarLiteV3Service, LidarLiteV3ServiceEvent}, + mtf01::Mtf01Service, +}; +use kaptan_mavlink::{ + commands::arm::{ArmRequest, decode_arm_request}, + receiver::MavlinkReceiver, +}; +use kaptan_spi::SpiCompletion; +use stm32f7::stm32f722; + +use crate::{BoardError, F722BoardRoutes, F722Profile}; + +use super::{ + MotorRoute, VerticalRoute, clock, + data_channel::{ + F722MavlinkTransmitter, F722StatusLed, GeprcStatusLed, SERVICE_HZ, SERVICE_TIMER_CONFIG, + vertical_data_config, + }, + interrupts, + sensors::{F722Imu, ImuEvent}, +}; + +const UART4_CLOCK_HZ: u32 = 54_000_000; +const MTF01_BAUD: u32 = 115_200; +const I2C1_CONFIG: I2c1Config = I2c1Config::from_timing_register(0x2040_4768); +const LIDAR_MODE: AcquisitionMode = AcquisitionMode::WithReceiverBiasCorrection; +const LIDAR_STARTUP_TICKS: u8 = 3; +const MOTOR_PROTOCOL: MotorProtocol = MotorProtocol::Dshot( + match kaptan_actuator::dshot::Config::new( + kaptan_actuator::dshot::Rate::Dshot600, + match kaptan_actuator::dshot::Throttle::new(300) { + Some(throttle) => throttle, + None => panic!("armed idle throttle must be valid"), + }, + match ArmingDuration::from_millis(2_000) { + Some(duration) => duration, + None => panic!("arming duration must be valid"), + }, + ) { + Some(config) => config, + None => panic!("DShot motor protocol must be valid"), + }, +); + +static IMU_HEALTHY: AtomicBool = AtomicBool::new(false); +static UART_COMPLETION: AtomicU8 = AtomicU8::new(0); +static SAMPLES: LatestImuMailbox = LatestImuMailbox::new(); +static RANGES: LatestRangeMailbox = LatestRangeMailbox::new(); +static OPTICAL_FLOW: LatestOpticalFlowMailbox = LatestOpticalFlowMailbox::new(); + +type F722Range = LidarLiteV3Service; +/// Opaque RTIC-local MTF-01 service type; its route is platform-owned. +pub type F722Mtf01 = Mtf01Service; +/// Opaque RTIC-local UART4 owner; its construction is platform-owned. +pub type F722Mtf01Uart = Uart4; +/// Opaque RTIC-local service timer; its construction is platform-owned. +pub type F722ServiceTimer = Tim2Periodic; + +/// RTIC-shared MAVLink state. +/// +/// Only USART receive and the MAVLink worker access this state, so its +/// priority ceiling is 9 rather than the IMU or vertical-sensor IRQ ceiling. +pub struct F722MavlinkState { + receiver: MavlinkReceiver<4>, + data_channel: DataChannel<'static, F722MavlinkTransmitter, SERVICE_HZ>, + led: F722StatusLed, + config: BoardConfig, +} + +/// RTIC-shared LIDAR state. +/// +/// I2C event/error IRQs and the service timer are its only users. MTF-01 is a +/// separate UART-only local resource, so it cannot raise this ceiling. +pub struct F722LidarState { + range: Option, + startup_ticks: u8, +} + +/// Explicit handoff from platform initialization to the RTIC resource graph. +/// +/// This is not runtime state indirection: each field is moved exactly once +/// into the single RTIC owner that accesses it. +pub struct F722InitResources { + pub(crate) imu: F722Imu, + pub(crate) mtf01: Option, + pub(crate) mtf01_uart: Option, + pub(crate) timer: F722ServiceTimer, + pub(crate) lidar: F722LidarState, + pub(crate) mavlink: F722MavlinkState, + pub(crate) motors: crate::actuator::F722MotorOutput, +} + +/// Validates the relationship between portable metadata and selected sensor +/// route before platform initialization claims a peripheral. +pub(crate) fn validate_vertical_route( + config: BoardConfig, + routes: F722BoardRoutes, +) -> Result<(), BoardError> { + if config.data_channel.service_hz != SERVICE_HZ { + return Err(BoardError::UnsupportedServiceFrequency); + } + let expected = match routes.vertical { + VerticalRoute::None => VerticalSensingKind::None, + VerticalRoute::Mtf01 => VerticalSensingKind::RangeAndOpticalFlow, + VerticalRoute::LidarLiteV3 => VerticalSensingKind::Range, + }; + if config.vertical_sensing != expected { + return Err(BoardError::VerticalSensorCapabilityMismatch); + } + Ok(()) +} + +pub(crate) fn initialize( + mut core: cortex_m::Peripherals, + device: stm32f722::Peripherals, + config: BoardConfig, + routes: F722BoardRoutes, +) -> Result { + let clock = clock::initialize(&mut core, &device).map_err(|_| BoardError::Initialization)?; + + // The profile is written once before any interrupt is enabled. ISR helpers + // read it with Acquire ordering and only access a route initialized by the + // matching branch below. + interrupts::activate_profile(routes.profile()); + + match routes.profile() { + F722Profile::Mpu6000Spi1Usart1 => { + initialize_mpu6000(device, clock, config, routes.motors, routes.vertical) + } + F722Profile::Icm42688Spi3Usart2 => { + initialize_icm42688(device, clock, config, routes.motors, routes.vertical) + } + } +} + +fn initialize_mpu6000( + device: stm32f722::Peripherals, + clock: DwtMicrosecondClock, + config: BoardConfig, + motors: MotorRoute, + vertical: VerticalRoute, +) -> Result { + configure_spi1_pins_pa5_pa6_pa7(&device.RCC, &device.GPIOA); + configure_exti4_pin_pa4(&device.GPIOA); + configure_exti4(&device.RCC, device.SYSCFG, device.EXTI); + configure_usart1_tx_pin_pa9(&device.GPIOA); + configure_usart1_rx_pin_pa10(&device.GPIOA); + configure_vertical_pins(&device.RCC, &device.GPIOA, &device.GPIOB, vertical); + + let (range, mtf01, mtf01_uart) = initialize_vertical( + &device.RCC, + device.I2C1, + device.UART4, + clock.fork(), + clock.fork(), + vertical, + )?; + let imu = F722Imu::mpu6000(&device.RCC, device.SPI1, device.DMA2, device.GPIOB, clock); + let transmitter = F722MavlinkTransmitter::usart1(&device.RCC, device.USART1) + .map_err(|_| BoardError::Initialization)?; + let led = F722StatusLed::Kakute(super::peripherals::ActiveLowLedPd2::new( + &device.RCC, + device.GPIOD, + )); + finish( + &device.RCC, + device.TIM2, + device.TIM3, + device.TIM5, + config, + motors, + vertical, + imu, + range, + mtf01, + mtf01_uart, + transmitter, + led, + ) +} + +fn initialize_icm42688( + device: stm32f722::Peripherals, + clock: DwtMicrosecondClock, + config: BoardConfig, + motors: MotorRoute, + vertical: VerticalRoute, +) -> Result { + configure_spi3_pins_pc10_pc11_pc12(&device.RCC, &device.GPIOC); + configure_exti3_pin_pc3(&device.GPIOC); + configure_exti3(&device.RCC, device.SYSCFG, device.EXTI); + configure_usart2_pins_pa2_pa3(&device.GPIOA); + configure_vertical_pins(&device.RCC, &device.GPIOA, &device.GPIOB, vertical); + + let (range, mtf01, mtf01_uart) = initialize_vertical( + &device.RCC, + device.I2C1, + device.UART4, + clock.fork(), + clock.fork(), + vertical, + )?; + let imu = F722Imu::icm42688(&device.RCC, device.SPI3, device.DMA1, &device.GPIOA, clock); + let transmitter = F722MavlinkTransmitter::usart2(&device.RCC, device.USART2) + .map_err(|_| BoardError::Initialization)?; + let led = F722StatusLed::Geprc(GeprcStatusLed::new(&device.RCC, device.GPIOA)); + finish( + &device.RCC, + device.TIM2, + device.TIM3, + device.TIM5, + config, + motors, + vertical, + imu, + range, + mtf01, + mtf01_uart, + transmitter, + led, + ) +} + +#[allow(clippy::too_many_arguments)] +fn finish( + rcc: &stm32f722::RCC, + tim2: stm32f722::TIM2, + tim3: stm32f722::TIM3, + tim5: stm32f722::TIM5, + config: BoardConfig, + motors: MotorRoute, + vertical: VerticalRoute, + imu: F722Imu, + range: Option, + mtf01: Option, + mtf01_uart: Option, + transmitter: F722MavlinkTransmitter, + led: F722StatusLed, +) -> Result { + let data_channel = DataChannel::new( + transmitter, + config, + vertical_data_config(vertical), + DataSources { + imu: &SAMPLES, + range: &RANGES, + optical_flow: &OPTICAL_FLOW, + }, + ) + .map_err(|_| BoardError::Initialization)?; + Ok(F722InitResources { + imu, + mtf01, + mtf01_uart, + timer: Tim2Periodic::new(rcc, tim2, SERVICE_TIMER_CONFIG), + lidar: F722LidarState { + range, + startup_ticks: 0, + }, + mavlink: F722MavlinkState { + receiver: MavlinkReceiver::new(), + data_channel, + led, + config, + }, + motors: crate::actuator::F722MotorOutput::new(rcc, tim3, tim5, MOTOR_PROTOCOL, motors), + }) +} + +fn configure_vertical_pins( + rcc: &stm32f722::RCC, + gpioa: &stm32f722::GPIOA, + gpiob: &stm32f722::GPIOB, + vertical: VerticalRoute, +) { + match vertical { + VerticalRoute::None => {} + VerticalRoute::Mtf01 => configure_uart4_pins_pa0_pa1(gpioa), + VerticalRoute::LidarLiteV3 => configure_i2c1_pins_pb8_pb9(rcc, gpiob), + } +} + +type VerticalResources = (Option, Option, Option); + +fn initialize_vertical( + rcc: &stm32f722::RCC, + i2c: stm32f722::I2C1, + uart4: stm32f722::UART4, + mtf01_clock: DwtMicrosecondClock, + range_clock: DwtMicrosecondClock, + vertical: VerticalRoute, +) -> Result { + match vertical { + VerticalRoute::None => Ok((None, None, None)), + VerticalRoute::Mtf01 => Ok(( + None, + Some(Mtf01Service::new(mtf01_clock)), + Some( + Uart4::new(rcc, uart4, UART4_CLOCK_HZ, MTF01_BAUD) + .map_err(|_| BoardError::Initialization)?, + ), + )), + VerticalRoute::LidarLiteV3 => Ok(( + Some(LidarLiteV3Service::new( + range_clock, + LidarLiteV3::new(I2c1::new(rcc, i2c, I2C1_CONFIG)), + LIDAR_MODE, + )), + None, + None, + )), + } +} + +fn configure_exti4(rcc: &stm32f722::RCC, syscfg: stm32f722::SYSCFG, exti: stm32f722::EXTI) { + rcc.apb2enr().modify(|_, write| write.syscfgen().set_bit()); + syscfg.exticr2().modify(|_, write| { + // SAFETY: selector zero is GPIOA for EXTI4 on STM32F722. + unsafe { write.exti4().bits(0) } + }); + exti.imr().modify(|_, write| write.im4().set_bit()); + exti.rtsr().modify(|_, write| write.tr4().set_bit()); + exti.ftsr().modify(|_, write| write.tr4().clear_bit()); + exti.pr().write(|write| write.pr4().clear()); +} + +fn configure_exti3(rcc: &stm32f722::RCC, syscfg: stm32f722::SYSCFG, exti: stm32f722::EXTI) { + rcc.apb2enr().modify(|_, write| write.syscfgen().set_bit()); + syscfg.exticr1().modify(|_, write| { + // SAFETY: selector two is GPIOC for EXTI3 on STM32F722. + unsafe { write.exti3().bits(2) } + }); + exti.imr().modify(|_, write| write.im3().set_bit()); + exti.rtsr().modify(|_, write| write.tr3().set_bit()); + exti.ftsr().modify(|_, write| write.tr3().clear_bit()); + exti.pr().write(|write| write.pr3().clear()); +} + +/// Services the MTF-01 UART route. It is a task-local resource, so it needs +/// no RTIC lock and cannot extend another resource's priority ceiling. +pub(crate) fn on_uart4_interrupt(mtf01: &mut Option) { + let Some(mtf01) = mtf01.as_mut() else { + return; + }; + match Uart4::acknowledge_receive_interrupt() { + ReceiveInterrupt::Byte(byte) => { + if let Some(report) = mtf01.on_received_byte(byte) { + if let Some(range) = report.range_snapshot() { + RANGES.publish(range); + } + if let Some(flow) = report.optical_flow_snapshot() { + OPTICAL_FLOW.publish(flow); + } + } + } + ReceiveInterrupt::Fault => mtf01.reset(), + ReceiveInterrupt::NotPending => {} + } +} + +impl F722LidarState { + pub(crate) fn on_i2c_event(&mut self, error: bool) { + let Some(range) = self.range.as_mut() else { + return; + }; + let event = range.on_controller_interrupt(|i2c| { + if error { + i2c.acknowledge_error_interrupt() + } else { + i2c.acknowledge_event_interrupt() + } + }); + publish_lidar_event(event); + } + + pub(crate) fn on_timer(&mut self) { + if let Some(range) = self.range.as_mut() { + if self.startup_ticks < LIDAR_STARTUP_TICKS { + self.startup_ticks += 1; + if self.startup_ticks == LIDAR_STARTUP_TICKS { + let _ = range.finish_startup(); + } + } else { + publish_lidar_event(range.on_periodic_tick()); + } + } + } +} + +/// Advances the IMU state machine in its dedicated worker-local resource. +pub(crate) fn process_imu(imu: &mut F722Imu, events: u8) { + const START: u8 = 1 << 4; + const TRANSFER_FAULT: u8 = 1 << 2; + const TRANSFER_COMPLETE: u8 = 1 << 1; + const TIMER: u8 = 1 << 3; + const DRDY: u8 = 1 << 0; + + if events & START != 0 { + accept_imu_event(imu.begin()); + } + if events & TRANSFER_FAULT != 0 { + accept_imu_event(imu.on_transfer_complete(SpiCompletion::TransferFault)); + } + if events & TRANSFER_COMPLETE != 0 { + accept_imu_event(imu.on_transfer_complete(SpiCompletion::Complete)); + } + if events & TIMER != 0 { + accept_imu_event(imu.on_periodic_tick()); + } + if events & DRDY != 0 { + accept_imu_event(imu.on_data_ready()); + } +} + +fn accept_imu_event(event: ImuEvent) { + match event { + ImuEvent::Sample(sample) => { + IMU_HEALTHY.store(true, Ordering::Release); + SAMPLES.publish(sample); + } + ImuEvent::Fault => IMU_HEALTHY.store(false, Ordering::Release), + ImuEvent::Idle | ImuEvent::Initialized => {} + } +} + +pub(crate) fn imu_is_healthy() -> bool { + IMU_HEALTHY.load(Ordering::Acquire) +} + +impl F722MavlinkState { + pub(crate) fn on_mavlink_byte(&mut self, receive: ReceiveInterrupt) -> bool { + match receive { + ReceiveInterrupt::Byte(byte) => self.receiver.receive_byte(byte), + ReceiveInterrupt::Fault => { + self.receiver.reset(); + false + } + ReceiveInterrupt::NotPending => false, + } + } + + /// Handles the bounded DMA and timer work that does not need the motor + /// resource. `motors_armed` is sampled under its separate RTIC lock. + pub(crate) fn process_worker_events(&mut self, events: u8, motors_armed: bool) { + const TIMER: u8 = 1 << 0; + const DMA: u8 = 1 << 2; + + if events & DMA != 0 { + let completion = match UART_COMPLETION.swap(0, Ordering::AcqRel) { + 1 => kaptan_dma::TxCompletion::Complete, + 2 => kaptan_dma::TxCompletion::TransferFault, + _ => kaptan_dma::TxCompletion::NotPending, + }; + self.data_channel.on_dma_complete(completion); + if completion == kaptan_dma::TxCompletion::Complete { + self.led.toggle(); + } + } + + if events & TIMER != 0 { + self.data_channel + .on_service_tick(IMU_HEALTHY.load(Ordering::Acquire), motors_armed); + } + } + + /// Removes already-decoded non-arm frames and returns one arm command at a + /// time. The worker releases this resource before taking the motor lock. + pub(crate) fn next_arm_request(&mut self) -> Option { + while let Some(frame) = self.receiver.take_received() { + if let Some(request) = decode_arm_request( + &frame, + self.config.mavlink.system_id, + self.config.mavlink.component_id, + ) { + return Some(request); + } + } + None + } + + pub(crate) fn acknowledge_arm_request(&mut self, result: kaptan_mavlink::message::MavResult) { + self.data_channel.acknowledge_command( + kaptan_mavlink::message::MavCmd::MAV_CMD_COMPONENT_ARM_DISARM, + result, + ); + } +} + +pub fn record_mavlink_completion(completion: kaptan_dma::TxCompletion) { + let code = match completion { + kaptan_dma::TxCompletion::Complete => 1, + kaptan_dma::TxCompletion::TransferFault | kaptan_dma::TxCompletion::Halted => 2, + kaptan_dma::TxCompletion::NotPending => 0, + }; + if code != 0 { + UART_COMPLETION.store(code, Ordering::Release); + } +} + +fn publish_lidar_event(event: LidarLiteV3ServiceEvent) { + if let LidarLiteV3ServiceEvent::Measurement(snapshot) = event { + RANGES.publish(snapshot); + } +} + +#[cfg(test)] +mod tests { + use super::validate_vertical_route; + use crate::{F7BoardBuilder, Mavlink, MotorOutputs, Mpu6000, Pa4, VerticalSensor}; + use kaptan_board::{BoardConfig, DataChannelRates, MavlinkEndpoint, VerticalSensingKind}; + + #[test] + fn rejects_metadata_that_claims_flow_for_a_lidar_route() { + let routes = F7BoardBuilder::new() + .imu(Mpu6000::on_spi1().with_drdy(Pa4)) + .motors(MotorOutputs::kakute_tim3_tim5().dshot600()) + .mavlink(Mavlink::on_usart1()) + .vertical_sensor(VerticalSensor::lidar_lite_v3_on_i2c1()) + .build() + .unwrap(); + let config = BoardConfig { + name: "invalid", + mavlink: MavlinkEndpoint { + system_id: 1, + component_id: 1, + }, + vertical_sensing: VerticalSensingKind::RangeAndOpticalFlow, + data_channel: DataChannelRates { + service_hz: 100, + raw_imu_hz: 20, + range_hz: Some(20), + optical_flow_hz: Some(20), + heartbeat_hz: 2, + }, + }; + assert_eq!( + validate_vertical_route(config, routes), + Err(crate::BoardError::VerticalSensorCapabilityMismatch) + ); + } +} diff --git a/platforms/stm32f7/src/f722/clock.rs b/platforms/stm32f7/src/f722/clock.rs new file mode 100644 index 0000000..8a199c1 --- /dev/null +++ b/platforms/stm32f7/src/f722/clock.rs @@ -0,0 +1,25 @@ +//! STM32F722 Pll216 clock-source assembly. + +use super::peripherals::{DwtMicrosecondClock, select_pll216_profile}; +use stm32f7::stm32f722; + +/// Core frequency in whole megahertz after selecting the PLL216 board profile. +const CORE_CLOCK_MHZ: u32 = 216; + +/// A failure while establishing the board's clock tree or monotonic clock. +pub(crate) enum ClockInitializationError { + Profile, + Monotonic, +} + +/// Selects the F722 Pll216 profile and starts its monotonic microsecond clock. +pub(crate) fn initialize( + core: &mut cortex_m::Peripherals, + device: &stm32f722::Peripherals, +) -> Result { + select_pll216_profile(&device.RCC, &device.PWR, &device.FLASH) + .map_err(|_| ClockInitializationError::Profile)?; + core.SCB.disable_dcache(&mut core.CPUID); + DwtMicrosecondClock::start::(&mut core.DCB, &mut core.DWT) + .map_err(|_| ClockInitializationError::Monotonic) +} diff --git a/platforms/stm32f7/src/f722/data_channel.rs b/platforms/stm32f7/src/f722/data_channel.rs new file mode 100644 index 0000000..5976dfc --- /dev/null +++ b/platforms/stm32f7/src/f722/data_channel.rs @@ -0,0 +1,159 @@ +//! F722 transport and vertical-route adapters for the portable data channel. + +use super::{VerticalRoute, peripherals}; +use kaptan_data_channel::{FrameSink, VerticalDataConfig}; +use kaptan_dma::TxCompletion; +use kaptan_mavlink::{ + MavlinkFrame, + encoder::DistanceSensorConfig, + transmitter::{FrameClass, MavlinkTransmitter}, +}; + +pub(crate) const UART1_CLOCK_HZ: u32 = 108_000_000; +pub(crate) const UART2_CLOCK_HZ: u32 = 54_000_000; +pub(crate) const MAVLINK_BAUD: u32 = 115_200; +pub(crate) const SERVICE_HZ: usize = 100; + +/// Fixed service-timer configuration for the validated Pll216 profile. +pub(crate) const SERVICE_TIMER_CONFIG: peripherals::Tim2PeriodicConfig = + peripherals::Tim2PeriodicConfig { + prescaler: 10_799, + auto_reload: 99, + }; + +/// Maps a selected vertical electrical route to complete data-channel metadata. +pub(crate) fn vertical_data_config(route: VerticalRoute) -> VerticalDataConfig { + let distance = + |minimum_distance_centimetres, maximum_distance_centimetres| DistanceSensorConfig { + sensor_id: 0, + orientation: + kaptan_mavlink::message::MavSensorOrientation::MAV_SENSOR_ROTATION_PITCH_270, + minimum_distance_centimetres, + maximum_distance_centimetres, + }; + + match route { + VerticalRoute::None => VerticalDataConfig { + distance: None, + optical_flow_sensor_id: None, + maximum_age_ticks: 10, + }, + VerticalRoute::Mtf01 => VerticalDataConfig { + distance: Some(distance(2, 800)), + optical_flow_sensor_id: Some(0), + maximum_age_ticks: 10, + }, + VerticalRoute::LidarLiteV3 => VerticalDataConfig { + distance: Some(distance(20, 4_000)), + optical_flow_sensor_id: None, + maximum_age_ticks: 10, + }, + } +} + +/// The two F722 DMA engines behind the portable data-channel transport. +pub(crate) enum F722MavlinkTransmitter { + Usart1(MavlinkTransmitter), + Usart2(MavlinkTransmitter), +} + +impl F722MavlinkTransmitter { + pub(crate) fn usart1( + rcc: &stm32f7::stm32f722::RCC, + uart: stm32f7::stm32f722::USART1, + ) -> Result { + let uart = + peripherals::Usart1Tx::new(rcc, uart, UART1_CLOCK_HZ, MAVLINK_BAUD).map_err(|_| ())?; + // SAFETY: platform initialization disables D-cache before this object + // is created, and the active MAVLink frame remains queue-owned until + // this platform receives a terminal DMA completion notification. + let memory = unsafe { peripherals::DcacheDisabledDmaMemory::new() }; + Ok(Self::Usart1(MavlinkTransmitter::new( + memory, + peripherals::Usart1TxDma::new(rcc, uart), + ))) + } + + pub(crate) fn usart2( + rcc: &stm32f7::stm32f722::RCC, + uart: stm32f7::stm32f722::USART2, + ) -> Result { + let uart = + peripherals::Usart2Tx::new(rcc, uart, UART2_CLOCK_HZ, MAVLINK_BAUD).map_err(|_| ())?; + // SAFETY: identical DMA-memory invariant to the USART1 route above; + // only the fixed platform DMA endpoint differs. + let memory = unsafe { peripherals::DcacheDisabledDmaMemory::new() }; + Ok(Self::Usart2(MavlinkTransmitter::new( + memory, + peripherals::Usart2TxDma::new(rcc, uart), + ))) + } +} + +impl FrameSink for F722MavlinkTransmitter { + fn send(&mut self, class: FrameClass, frame: MavlinkFrame) { + match self { + Self::Usart1(transmitter) => { + let _ = transmitter.send_frame(class, frame); + } + Self::Usart2(transmitter) => { + let _ = transmitter.send_frame(class, frame); + } + } + } + + fn on_dma_complete(&mut self, completion: TxCompletion) { + match self { + Self::Usart1(transmitter) => transmitter.on_dma_complete(completion), + Self::Usart2(transmitter) => transmitter.on_dma_complete(completion), + } + } +} + +/// Board-route status light; it is not part of a board declaration API. +pub(crate) enum F722StatusLed { + Kakute(peripherals::ActiveLowLedPd2), + Geprc(GeprcStatusLed), +} + +impl F722StatusLed { + pub(crate) fn toggle(&mut self) { + match self { + Self::Kakute(led) => led.toggle(), + Self::Geprc(led) => led.toggle(), + } + } +} + +/// GEPRC GEP-F722-HD V2 blue status LED on PA13. +pub(crate) struct GeprcStatusLed { + port: stm32f7::stm32f722::GPIOA, + is_high: bool, +} + +impl GeprcStatusLed { + pub(crate) fn new(rcc: &stm32f7::stm32f722::RCC, port: stm32f7::stm32f722::GPIOA) -> Self { + rcc.ahb1enr().modify(|_, write| write.gpioaen().set_bit()); + port.moder().modify(|_, write| write.moder13().output()); + port.otyper().modify(|_, write| write.ot13().push_pull()); + port.ospeedr() + .modify(|_, write| write.ospeedr13().low_speed()); + port.pupdr().modify(|_, write| write.pupdr13().floating()); + port.bsrr().write(|write| write.br13().set_bit()); + Self { + port, + is_high: false, + } + } + + fn toggle(&mut self) { + self.is_high = !self.is_high; + self.port.bsrr().write(|write| { + if self.is_high { + write.bs13().set_bit() + } else { + write.br13().set_bit() + } + }); + } +} diff --git a/platforms/stm32f7/src/f722/diagnostics.rs b/platforms/stm32f7/src/f722/diagnostics.rs new file mode 100644 index 0000000..4831d26 --- /dev/null +++ b/platforms/stm32f7/src/f722/diagnostics.rs @@ -0,0 +1,59 @@ +//! Focused physical-route diagnostics for supported F722 boards. + +use super::peripherals::{Usart2Tx, configure_usart2_pins_pa2_pa3, select_pll216_profile}; +use cortex_m_rt as _; +use panic_halt as _; +use stm32f7::stm32f722; + +const UART2_CLOCK_HZ: u32 = 54_000_000; +const UART2_BAUD: u32 = 115_200; +const SQUARE_WAVE_BYTE: u8 = 0x55; +const LED_TOGGLE_INTERVAL_BYTES: u16 = 11_520; + +/// Continuously transmits `0x55` on the GEP-F722-HD V2's T2 pad. +/// +/// This intentionally initializes only the clock tree, PA2/PA3, and USART2. +/// It does not enable RTIC, DMA, MAVLink, sensors, timers, or motor outputs. +/// The PA13 blue LED also changes state roughly once per second, proving the +/// test passed clock and UART initialization. A host at 115200 8N1 must +/// receive a continuous sequence of `55` bytes. +pub fn geprc_uart2_wire_test() -> ! { + // SAFETY: this diagnostic installs the sole reset entry point and takes no + // interrupt or RTIC path, so no other code can own these peripherals. + let device = unsafe { stm32f722::Peripherals::steal() }; + + select_pll216_profile(&device.RCC, &device.PWR, &device.FLASH) + .expect("could not select F722 PLL216 clock profile"); + device + .RCC + .ahb1enr() + .modify(|_, write| write.gpioaen().set_bit()); + configure_usart2_pins_pa2_pa3(&device.GPIOA); + device + .GPIOA + .moder() + .modify(|_, write| write.moder13().output()); + // The V2's blue LED is active low. Start it off so each following toggle + // is visible; PA13 is otherwise unused by this standalone diagnostic. + device.GPIOA.bsrr().write(|write| write.bs13().set_bit()); + let uart = Usart2Tx::new(&device.RCC, device.USART2, UART2_CLOCK_HZ, UART2_BAUD) + .expect("could not initialize USART2"); + + let mut bytes_since_led_toggle = 0; + let mut led_is_on = false; + loop { + uart.write_blocking(SQUARE_WAVE_BYTE); + bytes_since_led_toggle += 1; + if bytes_since_led_toggle == LED_TOGGLE_INTERVAL_BYTES { + bytes_since_led_toggle = 0; + led_is_on = !led_is_on; + device.GPIOA.bsrr().write(|write| { + if led_is_on { + write.br13().set_bit() + } else { + write.bs13().set_bit() + } + }); + } + } +} diff --git a/platforms/stm32f7/src/f722/firmware.rs b/platforms/stm32f7/src/f722/firmware.rs new file mode 100644 index 0000000..28e6d48 --- /dev/null +++ b/platforms/stm32f7/src/f722/firmware.rs @@ -0,0 +1,328 @@ +//! The one platform-owned RTIC task graph for all supported F722 profiles. + +/// Defines the F722 application in the platform crate. All interrupt bindings +/// are the finite platform-owned superset. Profile activation determines which +/// acknowledged route has initialized hardware. +#[cfg(target_arch = "arm")] +macro_rules! f722_flight_application { + ($routes:path, $config:path) => { + #[allow(unused_imports)] + use $crate::__platform_internal::cmrt as _; + #[allow(unused_imports)] + use $crate::__platform_internal::panic as _; + + // RTIC 2.3 expands hardware-vector shims that call unsafe PAC token + // constructors inside generated unsafe functions. Those shims are + // generated by RTIC, not hand-written platform accesses; all platform + // unsafe register operations remain documented at their source. + #[allow(unsafe_op_in_unsafe_fn)] + #[$crate::__platform_internal::rtic_app::app( + device = $crate::__platform_internal::pac::stm32f722, + peripherals = true, + dispatchers = [TIM6_DAC, TIM7] + )] + mod kaptan_f722_platform_app { + use core::sync::atomic::{AtomicBool, AtomicU8, Ordering}; + + use $crate::f722::{ + F722Imu, F722InitResources, F722LidarState, F722MavlinkState, F722Mtf01, + F722Mtf01Uart, F722ServiceTimer, acknowledge_exti3, acknowledge_exti4, + acknowledge_imu_rx_dma1, acknowledge_imu_rx_dma2, acknowledge_imu_spi1_error, + acknowledge_imu_spi3_error, acknowledge_imu_tx_dma1, acknowledge_imu_tx_dma2, + acknowledge_mavlink_dma1, acknowledge_mavlink_dma2, acknowledge_mavlink_rx_usart1, + acknowledge_mavlink_rx_usart2, imu_is_healthy, initialize, on_uart4_interrupt, + process_imu, record_mavlink_completion, + }; + use $crate::actuator::F722MotorOutput; + use $crate::__platform_internal::spi::SpiCompletion; + #[allow(unused_imports)] + use $crate::__platform_internal::rtic_app::Mutex; + + const IMU_EVENT_DRDY: u8 = 1 << 0; + const IMU_EVENT_TRANSFER_COMPLETE: u8 = 1 << 1; + const IMU_EVENT_TRANSFER_FAULT: u8 = 1 << 2; + const IMU_EVENT_TIMER: u8 = 1 << 3; + const IMU_EVENT_START: u8 = 1 << 4; + const MAVLINK_EVENT_TIMER: u8 = 1 << 0; + const MAVLINK_EVENT_DMA: u8 = 1 << 2; + const MAVLINK_EVENT_RECEIVED: u8 = 1 << 3; + + // Hardware IRQs and both software workers coalesce event bits. + // Release/Acquire pairs make each terminal acknowledgement visible + // to the worker that consumes it; the queued bit prevents unbounded + // software-task backlog while preserving the latest pending work. + static IMU_EVENTS: AtomicU8 = AtomicU8::new(0); + static MAVLINK_EVENTS: AtomicU8 = AtomicU8::new(0); + static IMU_QUEUED: AtomicBool = AtomicBool::new(false); + static MAVLINK_QUEUED: AtomicBool = AtomicBool::new(false); + + #[shared] + struct Shared { + mavlink: F722MavlinkState, + motors: F722MotorOutput, + lidar: F722LidarState, + } + + #[local] + struct Local { + imu: F722Imu, + mtf01: Option, + _mtf01_uart: Option, + timer: F722ServiceTimer, + } + + #[init] + fn init(context: init::Context) -> (Shared, Local) { + let routes = $routes().unwrap_or_else(|_| safe_idle()); + let F722InitResources { + imu, + mtf01, + mtf01_uart, + timer, + lidar, + mavlink, + motors, + } = initialize(context.core, context.device, $config(), routes) + .unwrap_or_else(|_| safe_idle()); + schedule_imu(IMU_EVENT_START); + ( + Shared { + mavlink, + motors, + lidar, + }, + Local { + imu, + mtf01, + _mtf01_uart: mtf01_uart, + timer, + }, + ) + } + + #[idle] + fn idle(_: idle::Context) -> ! { + loop { + $crate::__platform_internal::cm::asm::wfi(); + } + } + + #[task(binds = EXTI4, priority = 15)] + fn exti4(_: exti4::Context) { + if acknowledge_exti4() { + schedule_imu(IMU_EVENT_DRDY); + } + } + + #[task(binds = EXTI3, priority = 15)] + fn exti3(_: exti3::Context) { + if acknowledge_exti3() { + schedule_imu(IMU_EVENT_DRDY); + } + } + + #[task(binds = DMA2_STREAM0, priority = 14)] + fn imu_rx_dma2(_: imu_rx_dma2::Context) { + match acknowledge_imu_rx_dma2() { + SpiCompletion::Complete => schedule_imu(IMU_EVENT_TRANSFER_COMPLETE), + SpiCompletion::TransferFault => schedule_imu(IMU_EVENT_TRANSFER_FAULT), + SpiCompletion::NotPending => {} + } + } + + #[task(binds = DMA2_STREAM3, priority = 14)] + fn imu_tx_dma2(_: imu_tx_dma2::Context) { + if acknowledge_imu_tx_dma2() == SpiCompletion::TransferFault { + schedule_imu(IMU_EVENT_TRANSFER_FAULT); + } + } + + #[task(binds = SPI1, priority = 14)] + fn imu_spi1_error(_: imu_spi1_error::Context) { + if acknowledge_imu_spi1_error() { + schedule_imu(IMU_EVENT_TRANSFER_FAULT); + } + } + + #[task(binds = DMA1_STREAM0, priority = 14)] + fn imu_rx_dma1(_: imu_rx_dma1::Context) { + match acknowledge_imu_rx_dma1() { + SpiCompletion::Complete => schedule_imu(IMU_EVENT_TRANSFER_COMPLETE), + SpiCompletion::TransferFault => schedule_imu(IMU_EVENT_TRANSFER_FAULT), + SpiCompletion::NotPending => {} + } + } + + #[task(binds = DMA1_STREAM5, priority = 14)] + fn imu_tx_dma1(_: imu_tx_dma1::Context) { + if acknowledge_imu_tx_dma1() == SpiCompletion::TransferFault { + schedule_imu(IMU_EVENT_TRANSFER_FAULT); + } + } + + #[task(binds = SPI3, priority = 14)] + fn imu_spi3_error(_: imu_spi3_error::Context) { + if acknowledge_imu_spi3_error() { + schedule_imu(IMU_EVENT_TRANSFER_FAULT); + } + } + + #[task(binds = UART4, priority = 14, local = [mtf01])] + fn uart4(context: uart4::Context) { + on_uart4_interrupt(context.local.mtf01); + } + + #[task(binds = TIM3, priority = 12, shared = [motors])] + fn motor_update(mut context: motor_update::Context) { + context.shared.motors.lock(F722MotorOutput::on_tim3_interrupt); + } + + #[task(binds = TIM5, priority = 12, shared = [motors])] + fn motor_scheduler(mut context: motor_scheduler::Context) { + context.shared.motors.lock(F722MotorOutput::on_tim5_interrupt); + } + + #[task(binds = I2C1_EV, priority = 11, shared = [lidar])] + fn i2c1_event(mut context: i2c1_event::Context) { + context + .shared + .lidar + .lock(|lidar| lidar.on_i2c_event(false)); + } + + #[task(binds = I2C1_ER, priority = 11, shared = [lidar])] + fn i2c1_error(mut context: i2c1_error::Context) { + context + .shared + .lidar + .lock(|lidar| lidar.on_i2c_event(true)); + } + + #[task(binds = DMA2_STREAM7, priority = 10)] + fn mavlink_dma2(_: mavlink_dma2::Context) { + let completion = acknowledge_mavlink_dma2(); + if completion != $crate::__platform_internal::dma::TxCompletion::NotPending { + record_mavlink_completion(completion); + schedule_mavlink(MAVLINK_EVENT_DMA); + } + } + + #[task(binds = DMA1_STREAM6, priority = 10)] + fn mavlink_dma1(_: mavlink_dma1::Context) { + let completion = acknowledge_mavlink_dma1(); + if completion != $crate::__platform_internal::dma::TxCompletion::NotPending { + record_mavlink_completion(completion); + schedule_mavlink(MAVLINK_EVENT_DMA); + } + } + + #[task(binds = USART1, priority = 8, shared = [mavlink])] + fn mavlink_rx_usart1(mut context: mavlink_rx_usart1::Context) { + let admitted = context + .shared + .mavlink + .lock(|mavlink| mavlink.on_mavlink_byte(acknowledge_mavlink_rx_usart1())); + if admitted { + schedule_mavlink(MAVLINK_EVENT_RECEIVED); + } + } + + #[task(binds = USART2, priority = 8, shared = [mavlink])] + fn mavlink_rx_usart2(mut context: mavlink_rx_usart2::Context) { + let admitted = context + .shared + .mavlink + .lock(|mavlink| mavlink.on_mavlink_byte(acknowledge_mavlink_rx_usart2())); + if admitted { + schedule_mavlink(MAVLINK_EVENT_RECEIVED); + } + } + + #[task(binds = TIM2, priority = 4, local = [timer], shared = [lidar])] + fn tim2(mut context: tim2::Context) { + context.local.timer.acknowledge_update(); + context.shared.lidar.lock(F722LidarState::on_timer); + schedule_imu(IMU_EVENT_TIMER); + schedule_mavlink(MAVLINK_EVENT_TIMER); + } + + #[task(priority = 13, local = [imu])] + async fn imu_worker(context: imu_worker::Context) { + let events = IMU_EVENTS.swap(0, Ordering::AcqRel); + process_imu(context.local.imu, events); + IMU_QUEUED.store(false, Ordering::Release); + if IMU_EVENTS.load(Ordering::Acquire) != 0 + && !IMU_QUEUED.swap(true, Ordering::AcqRel) + && imu_worker::spawn().is_err() + { + IMU_QUEUED.store(false, Ordering::Release); + } + } + + #[task(priority = 9, shared = [mavlink, motors])] + async fn mavlink_worker(mut context: mavlink_worker::Context) { + let events = MAVLINK_EVENTS.swap(0, Ordering::AcqRel); + let motors_armed = context.shared.motors.lock(|motors| motors.is_armed()); + context + .shared + .mavlink + .lock(|mavlink| mavlink.process_worker_events(events, motors_armed)); + if events & MAVLINK_EVENT_RECEIVED != 0 { + while let Some(request) = + context.shared.mavlink.lock(F722MavlinkState::next_arm_request) + { + let result = match request { + $crate::__platform_internal::mavlink::commands::arm::ArmRequest::Arm if !imu_is_healthy() => + { + $crate::__platform_internal::mavlink::message::MavResult::MAV_RESULT_DENIED + } + $crate::__platform_internal::mavlink::commands::arm::ArmRequest::Arm => { + context.shared.motors.lock(F722MotorOutput::arm); + $crate::__platform_internal::mavlink::message::MavResult::MAV_RESULT_ACCEPTED + } + $crate::__platform_internal::mavlink::commands::arm::ArmRequest::Disarm => { + context.shared.motors.lock(F722MotorOutput::disarm); + $crate::__platform_internal::mavlink::message::MavResult::MAV_RESULT_ACCEPTED + } + }; + context + .shared + .mavlink + .lock(|mavlink| mavlink.acknowledge_arm_request(result)); + } + } + MAVLINK_QUEUED.store(false, Ordering::Release); + if MAVLINK_EVENTS.load(Ordering::Acquire) != 0 + && !MAVLINK_QUEUED.swap(true, Ordering::AcqRel) + && mavlink_worker::spawn().is_err() + { + MAVLINK_QUEUED.store(false, Ordering::Release); + } + } + + fn schedule_imu(event: u8) { + IMU_EVENTS.fetch_or(event, Ordering::Release); + if !IMU_QUEUED.swap(true, Ordering::AcqRel) && imu_worker::spawn().is_err() { + IMU_QUEUED.store(false, Ordering::Release); + } + } + + fn schedule_mavlink(event: u8) { + MAVLINK_EVENTS.fetch_or(event, Ordering::Release); + if !MAVLINK_QUEUED.swap(true, Ordering::AcqRel) && mavlink_worker::spawn().is_err() + { + MAVLINK_QUEUED.store(false, Ordering::Release); + } + } + + fn safe_idle() -> ! { + loop { + $crate::__platform_internal::cm::asm::wfi(); + } + } + } + }; +} + +#[cfg(target_arch = "arm")] +pub(crate) use f722_flight_application; diff --git a/platforms/stm32f7/src/f722/interrupts.rs b/platforms/stm32f7/src/f722/interrupts.rs new file mode 100644 index 0000000..003ec79 --- /dev/null +++ b/platforms/stm32f7/src/f722/interrupts.rs @@ -0,0 +1,132 @@ +//! Platform-owned interrupt acknowledgement for all supported F722 profiles. + +use core::sync::atomic::{AtomicU8, Ordering}; + +use super::peripherals::{ + ReceiveInterrupt, Spi1, Spi3, Usart1Tx, Usart1TxDma, Usart2Tx, Usart2TxDma, +}; +use kaptan_dma::TxCompletion; +use kaptan_spi::SpiCompletion; +use stm32f7::stm32f722; + +use crate::F722Profile; + +const PROFILE_NONE: u8 = 0; +const PROFILE_MPU6000_SPI1_USART1: u8 = 1; +const PROFILE_ICM42688_SPI3_USART2: u8 = 2; + +static ACTIVE_PROFILE: AtomicU8 = AtomicU8::new(PROFILE_NONE); + +pub(crate) fn activate_profile(profile: F722Profile) { + let encoded = match profile { + F722Profile::Mpu6000Spi1Usart1 => PROFILE_MPU6000_SPI1_USART1, + F722Profile::Icm42688Spi3Usart2 => PROFILE_ICM42688_SPI3_USART2, + }; + ACTIVE_PROFILE.store(encoded, Ordering::Release); +} + +fn active_profile() -> u8 { + ACTIVE_PROFILE.load(Ordering::Acquire) +} + +pub fn acknowledge_exti4() -> bool { + if active_profile() != PROFILE_MPU6000_SPI1_USART1 { + return false; + } + // SAFETY: the selected profile configured EXTI4 once before interrupts. + // This ISR writes only the W1C pending bit for PA4 and does not alias the + // worker-owned SPI or DMA registers. + unsafe { + (&*stm32f722::EXTI::ptr()) + .pr() + .write(|write| write.pr4().clear()); + } + true +} + +pub fn acknowledge_exti3() -> bool { + if active_profile() != PROFILE_ICM42688_SPI3_USART2 { + return false; + } + // SAFETY: the selected profile configured EXTI3 once before interrupts. + // This ISR writes only the W1C pending bit for PC3, disjoint from every + // peripheral state owned by the deferred IMU worker. + unsafe { + (&*stm32f722::EXTI::ptr()) + .pr() + .write(|write| write.pr3().clear()); + } + true +} + +pub fn acknowledge_imu_rx_dma2() -> SpiCompletion { + if active_profile() == PROFILE_MPU6000_SPI1_USART1 { + Spi1::acknowledge_rx_interrupt() + } else { + SpiCompletion::NotPending + } +} + +pub fn acknowledge_imu_tx_dma2() -> SpiCompletion { + if active_profile() == PROFILE_MPU6000_SPI1_USART1 { + Spi1::acknowledge_tx_interrupt() + } else { + SpiCompletion::NotPending + } +} + +pub fn acknowledge_imu_spi1_error() -> bool { + active_profile() == PROFILE_MPU6000_SPI1_USART1 && Spi1::acknowledge_error_interrupt() +} + +pub fn acknowledge_imu_rx_dma1() -> SpiCompletion { + if active_profile() == PROFILE_ICM42688_SPI3_USART2 { + Spi3::acknowledge_rx_interrupt() + } else { + SpiCompletion::NotPending + } +} + +pub fn acknowledge_imu_tx_dma1() -> SpiCompletion { + if active_profile() == PROFILE_ICM42688_SPI3_USART2 { + Spi3::acknowledge_tx_interrupt() + } else { + SpiCompletion::NotPending + } +} + +pub fn acknowledge_imu_spi3_error() -> bool { + active_profile() == PROFILE_ICM42688_SPI3_USART2 && Spi3::acknowledge_error_interrupt() +} + +pub fn acknowledge_mavlink_dma2() -> TxCompletion { + if active_profile() == PROFILE_MPU6000_SPI1_USART1 { + Usart1TxDma::acknowledge_interrupt() + } else { + TxCompletion::NotPending + } +} + +pub fn acknowledge_mavlink_dma1() -> TxCompletion { + if active_profile() == PROFILE_ICM42688_SPI3_USART2 { + Usart2TxDma::acknowledge_interrupt() + } else { + TxCompletion::NotPending + } +} + +pub fn acknowledge_mavlink_rx_usart1() -> ReceiveInterrupt { + if active_profile() == PROFILE_MPU6000_SPI1_USART1 { + Usart1Tx::acknowledge_receive_interrupt() + } else { + ReceiveInterrupt::NotPending + } +} + +pub fn acknowledge_mavlink_rx_usart2() -> ReceiveInterrupt { + if active_profile() == PROFILE_ICM42688_SPI3_USART2 { + Usart2Tx::acknowledge_receive_interrupt() + } else { + ReceiveInterrupt::NotPending + } +} diff --git a/platforms/stm32f7/src/f722/mod.rs b/platforms/stm32f7/src/f722/mod.rs new file mode 100644 index 0000000..d918167 --- /dev/null +++ b/platforms/stm32f7/src/f722/mod.rs @@ -0,0 +1,104 @@ +//! Finite STM32F722 execution profiles. +//! +//! This module is the sole owner of STM32F722 register routes and execution +//! lifecycle. Its public-in-crate profile names describe useful electrical +//! meaning; the RTIC bindings, PAC names, DMA streams and synchronization are +//! intentionally contained here and in the macro template. + +#![cfg_attr(not(target_arch = "arm"), allow(dead_code, unused_imports))] + +mod board; +mod clock; +mod data_channel; +#[cfg(target_arch = "arm")] +mod diagnostics; +mod firmware; +mod interrupts; +mod peripherals; +mod routes; +mod sensors; + +use crate::{BoardError, F722BoardRoutes}; +use kaptan_board::BoardConfig; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum VerticalRoute { + None, + Mtf01, + LidarLiteV3, +} + +/// Logical-M1–M4 ordering for the fixed TIM3 physical channels. +/// +/// Both supported boards use PB4/PB5/PB0/PB1 electrically, but their ESC +/// connectors assign those pins to logical motors differently. This remains a +/// platform-private route choice rather than a board-side execution detail. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum MotorRoute { + KakuteTim3Tim5, + GeprcTim3Tim5, +} + +pub(crate) use board::{ + F722InitResources, F722LidarState, F722MavlinkState, F722Mtf01, F722Mtf01Uart, + F722ServiceTimer, imu_is_healthy, on_uart4_interrupt, process_imu, record_mavlink_completion, +}; +#[cfg(target_arch = "arm")] +pub use diagnostics::geprc_uart2_wire_test; +#[cfg(not(target_arch = "arm"))] +pub fn geprc_uart2_wire_test() -> ! { + loop { + core::hint::spin_loop(); + } +} +pub(crate) use interrupts::{ + acknowledge_exti3, acknowledge_exti4, acknowledge_imu_rx_dma1, acknowledge_imu_rx_dma2, + acknowledge_imu_spi1_error, acknowledge_imu_spi3_error, acknowledge_imu_tx_dma1, + acknowledge_imu_tx_dma2, acknowledge_mavlink_dma1, acknowledge_mavlink_dma2, + acknowledge_mavlink_rx_usart1, acknowledge_mavlink_rx_usart2, +}; +pub(crate) use sensors::F722Imu; + +/// Materializes a board declaration after RTIC has exclusively taken the PAC +/// peripherals. The route/config check happens before any pin or peripheral +/// is initialized, so a malformed board declaration cannot drive hardware. +pub fn initialize( + core: cortex_m::Peripherals, + device: stm32f7::stm32f722::Peripherals, + config: BoardConfig, + routes: F722BoardRoutes, +) -> Result { + config.validate().map_err(BoardError::InvalidConfig)?; + board::validate_vertical_route(config, routes)?; + board::initialize(core, device, config, routes) +} + +#[cfg(target_arch = "arm")] +unsafe extern "Rust" { + static KAPTAN_F722_BOARD_CONFIG: BoardConfig; + fn kaptan_f722_board_routes() -> Result; +} + +#[cfg(target_arch = "arm")] +fn registered_config() -> BoardConfig { + // SAFETY: `flight_firmware!` emits this immutable symbol exactly once in + // the selected board binary. The platform reads it only during RTIC init, + // before any interrupt or worker can observe configuration state. + unsafe { KAPTAN_F722_BOARD_CONFIG } +} + +#[cfg(target_arch = "arm")] +fn registered_routes() -> Result { + // SAFETY: `flight_firmware!` emits this function exactly once in the board + // binary. It only constructs a safe descriptor; it cannot touch PAC state + // because the board builder contains no peripherals. + unsafe { kaptan_f722_board_routes() } +} + +// This is one source-level task graph. Its compile-time bindings are the +// platform-owned finite interrupt superset, independent of the board crate. +#[cfg(target_arch = "arm")] +firmware::f722_flight_application!( + crate::f722::registered_routes, + crate::f722::registered_config +); diff --git a/libs/kaptan-stm32f7/src/stm32f722/clock.rs b/platforms/stm32f7/src/f722/peripherals/clock.rs similarity index 63% rename from libs/kaptan-stm32f7/src/stm32f722/clock.rs rename to platforms/stm32f7/src/f722/peripherals/clock.rs index 19ca06a..47b6f23 100644 --- a/libs/kaptan-stm32f7/src/stm32f722/clock.rs +++ b/platforms/stm32f7/src/f722/peripherals/clock.rs @@ -16,7 +16,7 @@ pub enum ClockProfileError { SystemClockNotPll, } -/// Selects the board's HSI PLL profile. +/// Selects the F722 platform's HSI PLL profile. /// /// 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 @@ -43,8 +43,10 @@ pub fn select_pll216_profile( // 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)) + pwr.cr1().modify(|read, write| { + // SAFETY: this read-modify-write changes only the documented voltage-scale and + // over-drive-enable fields; it preserves every other PWR_CR1 bit owned by boot. + 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); @@ -53,25 +55,34 @@ pub fn select_pll216_profile( 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)) + flash.acr().modify(|read, write| { + // SAFETY: these are the STM32F7-documented latency, instruction-cache and + // data-cache fields for 216 MHz; the read-modify-write retains other ACR bits. + 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.pllcfgr().write(|write| { + // SAFETY: 16, 432, P=/2 (the reset encoding), and Q=9 are the valid PLL + // divisors described above; the PLL is disabled before this full-register 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 { + rcc.cfgr().modify(|read, write| { // AHB /1, APB1 /4, APB2 /2, SYSCLK = PLL. - write.bits( - (read.bits() & !((0x0f << 4) | (0x07 << 10) | (0x07 << 13) | 0x03)) - | (0b101 << 10) - | (0b100 << 13) - | 0b10, - ) + // SAFETY: this preserves every CFGR field except the documented prescaler and + // system-clock-source fields, which are written with encodings valid for F722. + unsafe { + 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); diff --git a/libs/kaptan-stm32f7/src/stm32f722/dma_buffer.rs b/platforms/stm32f7/src/f722/peripherals/dma_buffer.rs similarity index 71% rename from libs/kaptan-stm32f7/src/stm32f722/dma_buffer.rs rename to platforms/stm32f7/src/f722/peripherals/dma_buffer.rs index f6743a4..517399d 100644 --- a/libs/kaptan-stm32f7/src/stm32f722/dma_buffer.rs +++ b/platforms/stm32f7/src/f722/peripherals/dma_buffer.rs @@ -2,17 +2,17 @@ use kaptan_dma::{DmaReadableBuffer, DmaTxBufferFactory}; /// -/// DMA transmit memory policy for a board with D-cache disabled. +/// DMA transmit memory policy for a platform with D-cache disabled. /// -/// The board's linker script must also place transfer frames in SRAM reachable +/// The platform linker policy must also place transfer frames in SRAM reachable /// by the selected DMA controller. This policy does not support a cached board; -/// such a board requires a distinct policy that performs cache maintenance. +/// such a platform requires a distinct policy that performs cache maintenance. pub struct DcacheDisabledDmaMemory { _private: (), } impl DcacheDisabledDmaMemory { - /// Declares that the selected board disabled D-cache before DMA setup. + /// Declares that the selected platform disabled D-cache before DMA setup. /// /// # Safety /// @@ -27,7 +27,7 @@ impl DmaTxBufferFactory for DcacheDisabledDmaMemory { type Error = core::convert::Infallible; fn prepare<'a>(&mut self, bytes: &'a [u8]) -> Result, Self::Error> { - // SAFETY: `Self` can exist only after the board proved its D-cache and + // SAFETY: `Self` can exist only after the platform proved its D-cache and // linker-memory policy. The runtime retains frame ownership until a // terminal DMA outcome or successful recovery. Ok(unsafe { DmaReadableBuffer::new_unchecked(bytes) }) diff --git a/libs/kaptan-stm32f7/src/stm32f722/gpio.rs b/platforms/stm32f7/src/f722/peripherals/gpio.rs similarity index 59% rename from libs/kaptan-stm32f7/src/stm32f722/gpio.rs rename to platforms/stm32f7/src/f722/peripherals/gpio.rs index 66375de..09e459c 100644 --- a/libs/kaptan-stm32f7/src/stm32f722/gpio.rs +++ b/platforms/stm32f7/src/f722/peripherals/gpio.rs @@ -48,6 +48,59 @@ pub fn configure_spi1_pins_pa5_pa6_pa7(rcc: &stm32f722::RCC, port: &stm32f722::G }); } +/// Configures PC10, PC11, and PC12 for SPI3 alternate function 6. +pub fn configure_spi3_pins_pc10_pc11_pc12(rcc: &stm32f722::RCC, port: &stm32f722::GPIOC) { + rcc.ahb1enr().modify(|_, write| write.gpiocen().set_bit()); + port.moder().modify(|_, write| { + write + .moder10() + .alternate() + .moder11() + .alternate() + .moder12() + .alternate() + }); + port.afrh().modify(|_, write| { + // SAFETY: AF6 selects SPI3 on PC10, PC11, and PC12 on STM32F722. + unsafe { + write + .afrh10() + .bits(0b0110) + .afrh11() + .bits(0b0110) + .afrh12() + .bits(0b0110) + } + }); + port.otyper().modify(|_, write| { + write + .ot10() + .push_pull() + .ot11() + .push_pull() + .ot12() + .push_pull() + }); + port.ospeedr().modify(|_, write| { + write + .ospeedr10() + .high_speed() + .ospeedr11() + .high_speed() + .ospeedr12() + .high_speed() + }); + port.pupdr().modify(|_, write| { + write + .pupdr10() + .floating() + .pupdr11() + .floating() + .pupdr12() + .floating() + }); +} + /// Configures PB8/PB9 for I²C1 alternate function 4. pub fn configure_i2c1_pins_pb8_pb9(rcc: &stm32f722::RCC, port: &stm32f722::GPIOB) { rcc.ahb1enr().modify(|_, w| w.gpioben().set_bit()); @@ -66,13 +119,19 @@ pub fn configure_i2c1_pins_pb8_pb9(rcc: &stm32f722::RCC, port: &stm32f722::GPIOB /// Configures PA4 as the input pin for an EXTI4 route. /// -/// A board must select this explicitly rather than inheriting the pin mode +/// A platform route must select this explicitly rather than inheriting the pin mode /// left by a bootloader or an earlier firmware image. pub fn configure_exti4_pin_pa4(port: &stm32f722::GPIOA) { port.moder().modify(|_, write| write.moder4().input()); port.pupdr().modify(|_, write| write.pupdr4().floating()); } +/// Configures PC3 as the input pin for an EXTI3 route. +pub fn configure_exti3_pin_pc3(port: &stm32f722::GPIOC) { + port.moder().modify(|_, write| write.moder3().input()); + port.pupdr().modify(|_, write| write.pupdr3().floating()); +} + /// Configures PA9 for USART1 transmit alternate function 7. pub fn configure_usart1_tx_pin_pa9(port: &stm32f722::GPIOA) { port.moder().modify(|_, write| write.moder9().alternate()); @@ -96,6 +155,36 @@ pub fn configure_usart1_rx_pin_pa10(port: &stm32f722::GPIOA) { port.pupdr().modify(|_, write| write.pupdr10().floating()); } +/// Configures PA2/PA3 for USART2 transmit/receive alternate function 7. +pub fn configure_usart2_pins_pa2_pa3(port: &stm32f722::GPIOA) { + port.moder() + .modify(|_, write| write.moder2().alternate().moder3().alternate()); + port.afrl().modify(|_, write| { + // SAFETY: AF7 selects USART2 TX/RX on PA2/PA3 on STM32F722. + unsafe { write.afrl2().bits(0b0111).afrl3().bits(0b0111) } + }); + port.otyper().modify(|_, write| write.ot2().push_pull()); + port.ospeedr() + .modify(|_, write| write.ospeedr2().high_speed()); + port.pupdr() + .modify(|_, write| write.pupdr2().floating().pupdr3().floating()); +} + +/// Configures PA0/PA1 for UART4 alternate function 8. +pub fn configure_uart4_pins_pa0_pa1(port: &stm32f722::GPIOA) { + port.moder() + .modify(|_, write| write.moder0().alternate().moder1().alternate()); + port.afrl().modify(|_, write| { + // SAFETY: AF8 selects UART4 TX/RX on PA0/PA1 on STM32F722. + unsafe { write.afrl0().bits(0b1000).afrl1().bits(0b1000) } + }); + port.otyper().modify(|_, write| write.ot0().push_pull()); + port.ospeedr() + .modify(|_, write| write.ospeedr0().high_speed()); + port.pupdr() + .modify(|_, write| write.pupdr0().floating().pupdr1().floating()); +} + /// Active-low status LED connected to PD2. pub struct ActiveLowLedPd2 { port: stm32f722::GPIOD, diff --git a/libs/kaptan-stm32f7/src/stm32f722/i2c1.rs b/platforms/stm32f7/src/f722/peripherals/i2c1.rs similarity index 98% rename from libs/kaptan-stm32f7/src/stm32f722/i2c1.rs rename to platforms/stm32f7/src/f722/peripherals/i2c1.rs index fe2f6f3..4564ccd 100644 --- a/libs/kaptan-stm32f7/src/stm32f722/i2c1.rs +++ b/platforms/stm32f7/src/f722/peripherals/i2c1.rs @@ -27,7 +27,7 @@ pub enum I2c1Error { Busy, } -/// Board-selected I²C1 electrical timing. +/// Platform-selected I²C1 electrical timing. #[derive(Clone, Copy)] pub struct I2c1Config { timing: u32, @@ -102,14 +102,14 @@ pub struct I2c1 { } impl I2c1 { - /// Configures I²C1 using board-selected pin electrical timing. + /// Configures I²C1 using platform-selected pin electrical timing. pub fn new(rcc: &stm32f722::RCC, peripheral: stm32f722::I2C1, config: I2c1Config) -> Self { rcc.apb1rstr().modify(|_, w| w.i2c1rst().set_bit()); rcc.apb1rstr().modify(|_, w| w.i2c1rst().clear_bit()); rcc.apb1enr().modify(|_, w| w.i2c1en().set_bit()); // SAFETY: zero is the documented reset configuration for CR1. peripheral.cr1().write(|w| unsafe { w.bits(0) }); - // SAFETY: `timing` is a board-selected I2C_TIMINGR bit pattern. + // SAFETY: `timing` is a platform-selected I2C_TIMINGR bit pattern. peripheral .timingr() .write(|w| unsafe { w.bits(config.timing) }); diff --git a/platforms/stm32f7/src/f722/peripherals/mod.rs b/platforms/stm32f7/src/f722/peripherals/mod.rs new file mode 100644 index 0000000..722c8b2 --- /dev/null +++ b/platforms/stm32f7/src/f722/peripherals/mod.rs @@ -0,0 +1,33 @@ +//! STM32F722 peripheral implementations selected by supported boards. +//! +//! This module exports only typed peripheral building blocks. The family +//! platform selects pins, clocks, interrupt priorities, and static ownership. + +mod clock; +mod dma_buffer; +mod gpio; +mod i2c1; +mod spi1; +mod spi3; +mod tim2; +mod time; +mod usart; +mod usart1_tx_dma2_stream7; +mod usart2_tx_dma1_stream6; + +pub use clock::select_pll216_profile; +pub use dma_buffer::DcacheDisabledDmaMemory; +pub use gpio::{ + ActiveLowLedPd2, configure_exti3_pin_pc3, configure_exti4_pin_pa4, configure_i2c1_pins_pb8_pb9, + configure_spi1_pins_pa5_pa6_pa7, configure_spi3_pins_pc10_pc11_pc12, + configure_uart4_pins_pa0_pa1, configure_usart1_rx_pin_pa10, configure_usart1_tx_pin_pa9, + configure_usart2_pins_pa2_pa3, +}; +pub use i2c1::{I2c1, I2c1Config}; +pub use spi1::{Spi1, Spi1Error}; +pub use spi3::{Spi3, Spi3Error}; +pub use tim2::{Tim2Periodic, Tim2PeriodicConfig}; +pub use time::DwtMicrosecondClock; +pub use usart::{ReceiveInterrupt, Uart4, Usart1Tx, Usart2Tx}; +pub use usart1_tx_dma2_stream7::Usart1TxDma; +pub use usart2_tx_dma1_stream6::Usart2TxDma; diff --git a/libs/kaptan-stm32f7/src/stm32f722/spi1.rs b/platforms/stm32f7/src/f722/peripherals/spi1.rs similarity index 100% rename from libs/kaptan-stm32f7/src/stm32f722/spi1.rs rename to platforms/stm32f7/src/f722/peripherals/spi1.rs diff --git a/platforms/stm32f7/src/f722/peripherals/spi3.rs b/platforms/stm32f7/src/f722/peripherals/spi3.rs new file mode 100644 index 0000000..2cd96b2 --- /dev/null +++ b/platforms/stm32f7/src/f722/peripherals/spi3.rs @@ -0,0 +1,335 @@ +//! STM32F722 SPI3 full-duplex DMA controller. +//! +//! SPI3 uses DMA1 Stream0/Channel0 for receive and DMA1 Stream5/Channel0 for +//! transmit. The receive terminal event is the transaction boundary because it +//! is generated after the last serial frame; transmit completion can occur +//! while the final serial clock is still in progress. + +use kaptan_spi::SpiCompletion; +use stm32f7::stm32f722; + +const MAX_TRANSFER_LENGTH: usize = u16::MAX as usize; + +/// Failure while submitting one SPI3 DMA transaction. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Spi3Error { + /// The transaction is malformed or exceeds the DMA counter range. + InvalidLength, + /// A previous DMA transaction still owns SPI3. + Busy, +} + +/// Owns SPI3 and its fixed full-duplex DMA1 request route. +pub struct Spi3 { + peripheral: stm32f722::SPI3, + controller: stm32f722::DMA1, + active: bool, +} + +impl Spi3 { + /// Configures SPI3 mode 0 master operation at APB2/128 and reserves DMA1. + /// + /// This keeps the ICM-42688-P 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::SPI3, + controller: stm32f722::DMA1, + ) -> Self { + rcc.apb1rstr().modify(|_, write| write.spi3rst().set_bit()); + rcc.apb1rstr() + .modify(|_, write| write.spi3rst().clear_bit()); + rcc.apb1enr().modify(|_, write| write.spi3en().set_bit()); + rcc.ahb1enr().modify(|_, write| write.dma1en().set_bit()); + + peripheral.cr1().write(|write| { + write + .mstr() + .master() + .br() + .div128() + .ssm() + .set_bit() + .ssi() + .set_bit() + }); + peripheral + .cr2() + .write(|write| write.ds().eight_bit().frxth().set_bit().errie().set_bit()); + + Self { + peripheral, + controller, + active: false, + } + } + + /// Starts a full-duplex DMA transaction without polling a peripheral flag. + pub fn start_transfer(&mut self, transmit: &[u8], receive: &mut [u8]) -> Result<(), Spi3Error> { + if transmit.is_empty() + || transmit.len() != receive.len() + || transmit.len() > MAX_TRANSFER_LENGTH + { + return Err(Spi3Error::InvalidLength); + } + if self.active + || self.rx_stream().cr().read().en().is_enabled() + || self.tx_stream().cr().read().en().is_enabled() + { + return Err(Spi3Error::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. IMU 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()); + self.tx_stream() + .cr() + .modify(|_, write| write.en().enabled()); + self.peripheral + .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 + /// SPI3's own IRQ. The reference manual clears OVR by reading DR then SR. + pub fn acknowledge_error_interrupt() -> bool { + // SAFETY: SPI3 is initialized once by `Spi3::new`; this ISR only + // clears its peripheral OVR condition. + let peripheral = unsafe { &*stm32f722::SPI3::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 DMA1 Stream0. + pub fn acknowledge_rx_interrupt() -> SpiCompletion { + // SAFETY: DMA1 is initialized once by `Spi3::new`; this ISR touches + // only Stream0's status and clear registers, never worker state. + let controller = unsafe { &*stm32f722::DMA1::ptr() }; + let status = controller.lisr().read(); + let fault = + status.teif0().is_error() || status.dmeif0().is_error() || status.feif0().is_error(); + let complete = status.tcif0().is_complete(); + controller.lifcr().write(|write| { + write + .cfeif0() + .clear() + .cdmeif0() + .clear() + .cteif0() + .clear() + .chtif0() + .clear() + .ctcif0() + .clear() + }); + if fault { + SpiCompletion::TransferFault + } else if complete { + SpiCompletion::Complete + } else { + SpiCompletion::NotPending + } + } + + /// Acknowledges TX errors; TX completion alone is not a transaction event. + pub fn acknowledge_tx_interrupt() -> SpiCompletion { + // SAFETY: DMA1 Stream5 is initialized once by `Spi3::new`; this ISR + // touches only Stream5's status and clear registers. + let controller = unsafe { &*stm32f722::DMA1::ptr() }; + let status = controller.hisr().read(); + let fault = + status.teif5().is_error() || status.dmeif5().is_error() || status.feif5().is_error(); + controller.hifcr().write(|write| { + write + .cfeif5() + .clear() + .cdmeif5() + .clear() + .cteif5() + .clear() + .chtif5() + .clear() + .ctcif5() + .clear() + }); + if fault { + SpiCompletion::TransferFault + } else { + SpiCompletion::NotPending + } + } + + fn configure_rx(&self, receive: &mut [u8]) { + self.rx_stream().cr().write(|write| { + // SAFETY: Channel 0 is SPI3_RX on DMA1 Stream0; checked slice + // length fits NDTR, and all values map directly to documented bits. + unsafe { + write + .chsel() + .bits(0) + .dir() + .peripheral_to_memory() + .pl() + .very_high() + .minc() + .incremented() + .pinc() + .fixed() + .msize() + .bits8() + .psize() + .bits8() + .circ() + .disabled() + .tcie() + .enabled() + .teie() + .enabled() + .dmeie() + .enabled() + } + }); + self.rx_stream() + .ndtr() + // SAFETY: `start_transfer` bounds the length to the 16-bit NDTR field. + .write(|write| unsafe { write.bits(receive.len() as u32) }); + self.rx_stream() + .par() + // SAFETY: `DR` is the memory-mapped SPI3 DMA peripheral endpoint. + .write(|write| unsafe { write.bits(self.peripheral.dr().as_ptr() as u32) }); + self.rx_stream() + .m0ar() + // SAFETY: the caller retains `receive` until terminal completion. + .write(|write| unsafe { write.bits(receive.as_mut_ptr() as u32) }); + } + + fn configure_tx(&self, transmit: &[u8]) { + self.tx_stream().cr().write(|write| { + // SAFETY: Channel 0 is SPI3_TX on DMA1 Stream5; checked slice + // length fits NDTR, and all values map directly to documented bits. + unsafe { + write + .chsel() + .bits(0) + .dir() + .memory_to_peripheral() + .pl() + .very_high() + .minc() + .incremented() + .pinc() + .fixed() + .msize() + .bits8() + .psize() + .bits8() + .circ() + .disabled() + .teie() + .enabled() + .dmeie() + .enabled() + } + }); + self.tx_stream() + .ndtr() + // SAFETY: `start_transfer` bounds the length to the 16-bit NDTR field. + .write(|write| unsafe { write.bits(transmit.len() as u32) }); + self.tx_stream() + .par() + // SAFETY: `DR` is the memory-mapped SPI3 DMA peripheral endpoint. + .write(|write| unsafe { write.bits(self.peripheral.dr().as_ptr() as u32) }); + self.tx_stream() + .m0ar() + // SAFETY: the caller retains `transmit` until terminal completion. + .write(|write| unsafe { write.bits(transmit.as_ptr() as u32) }); + } + + fn rx_stream(&self) -> &stm32f722::dma1::ST { + self.controller.st(0) + } + + fn tx_stream(&self) -> &stm32f722::dma1::ST { + self.controller.st(5) + } + + fn clear_rx_flags(&self) { + self.controller.lifcr().write(|write| { + write + .cfeif0() + .clear() + .cdmeif0() + .clear() + .cteif0() + .clear() + .chtif0() + .clear() + .ctcif0() + .clear() + }); + } + + fn clear_tx_flags(&self) { + self.controller.hifcr().write(|write| { + write + .cfeif5() + .clear() + .cdmeif5() + .clear() + .cteif5() + .clear() + .chtif5() + .clear() + .ctcif5() + .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(); + } + } +} diff --git a/libs/kaptan-stm32f7/src/stm32f722/tim2.rs b/platforms/stm32f7/src/f722/peripherals/tim2.rs similarity index 74% rename from libs/kaptan-stm32f7/src/stm32f722/tim2.rs rename to platforms/stm32f7/src/f722/peripherals/tim2.rs index 300c641..250855e 100644 --- a/libs/kaptan-stm32f7/src/stm32f722/tim2.rs +++ b/platforms/stm32f7/src/f722/peripherals/tim2.rs @@ -13,11 +13,11 @@ pub struct Tim2PeriodicConfig { /// TIM2 configured to issue periodic update interrupts. pub struct Tim2Periodic { - peripheral: stm32f722::TIM2, + _peripheral: stm32f722::TIM2, } impl Tim2Periodic { - /// Enables TIM2 with the supplied board-derived period configuration. + /// Enables TIM2 with the supplied platform-derived period configuration. pub fn new( rcc: &stm32f722::RCC, peripheral: stm32f722::TIM2, @@ -37,17 +37,21 @@ impl Tim2Periodic { unsafe { write.arr().bits(config.auto_reload) } }); peripheral.egr().write(|write| write.ug().set_bit()); - peripheral.sr().modify(|_, write| write.uif().clear_bit()); + // TIM2 status flags are write-zero-to-clear. Use the PAC's explicit + // W0C operation rather than a read-modify-write after `EGR.UG`. + peripheral.sr().write(|write| write.uif().clear()); peripheral.dier().modify(|_, write| write.uie().set_bit()); peripheral.cr1().modify(|_, write| write.cen().set_bit()); - Self { peripheral } + Self { + _peripheral: peripheral, + } } /// Acknowledges a TIM2 update interrupt. + #[cfg(target_arch = "arm")] pub fn acknowledge_update(&mut self) { - self.peripheral - .sr() - .modify(|_, write| write.uif().clear_bit()); + // `UIF` is write-zero-to-clear (W0C). + self._peripheral.sr().write(|write| write.uif().clear()); } } diff --git a/libs/kaptan-stm32f7/src/stm32f722/time.rs b/platforms/stm32f7/src/f722/peripherals/time.rs similarity index 93% rename from libs/kaptan-stm32f7/src/stm32f722/time.rs rename to platforms/stm32f7/src/f722/peripherals/time.rs index 6fed23f..ead4a87 100644 --- a/libs/kaptan-stm32f7/src/stm32f722/time.rs +++ b/platforms/stm32f7/src/f722/peripherals/time.rs @@ -7,7 +7,7 @@ use kaptan_time::{MonotonicClock, Timestamp}; /// /// `now` must be called at least once during every 32-bit cycle-counter wrap /// interval. At the Kakute reset clock this is approximately 268 seconds; the -/// board periodic service calls it much more frequently. +/// platform periodic service calls it much more frequently. pub struct DwtMicrosecondClock { cycles_per_microsecond: u32, previous_cycles: u32, @@ -24,8 +24,8 @@ pub enum DwtClockStartError { impl DwtMicrosecondClock { /// Enables the DWT cycle counter for a fixed integer-MHz core clock. /// - /// `MEGAHERTZ` must match the board's core clock. This function is called - /// once during board initialization before interrupts are enabled. + /// `MEGAHERTZ` must match the platform's core clock. This function is + /// called once during platform initialization before interrupts are enabled. pub fn start( dcb: &mut DCB, dwt: &mut DWT, diff --git a/platforms/stm32f7/src/f722/peripherals/usart.rs b/platforms/stm32f7/src/f722/peripherals/usart.rs new file mode 100644 index 0000000..7bdd69b --- /dev/null +++ b/platforms/stm32f7/src/f722/peripherals/usart.rs @@ -0,0 +1,285 @@ +//! STM32F722 USART routes used by supported boards. + +use stm32f7::stm32f722; + +/// UART baud-rate configuration error. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum BaudRateError { + /// Zero is not a valid baud rate. + Zero, + /// The requested divisor cannot fit USART BRR. + OutOfRange, +} + +/// Terminal result from acknowledging a USART receive interrupt. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ReceiveInterrupt { + /// One received byte was removed from the UART data register. + Byte(u8), + /// A serial transport fault invalidated the current receive stream. + Fault, + /// This USART interrupt did not have a receive event pending. + NotPending, +} + +/// Owns configured full-duplex USART1 hardware. +/// +/// The family platform owns schematic-selected pin setup. Configure pins +/// before constructing this type. +pub struct Usart1Tx { + peripheral: stm32f722::USART1, +} + +/// Owns configured full-duplex USART2 hardware. +/// +/// The family platform owns schematic-selected pin setup. Configure pins +/// before constructing this type. +pub struct Usart2Tx { + peripheral: stm32f722::USART2, +} + +impl Usart2Tx { + /// Enables USART2 and configures 8N1 transmit and receive operation. + pub fn new( + rcc: &stm32f722::RCC, + peripheral: stm32f722::USART2, + peripheral_clock_hz: u32, + baud: u32, + ) -> Result { + let brr = calculate_brr(peripheral_clock_hz, baud)?; + + // Do not inherit a bootloader's USART2 kernel-clock selection. The + // F722 flight clock profile supplies `peripheral_clock_hz` from PCLK1, + // so select PCLK1 before programming BRR. + rcc.dckcfgr2().modify(|_, write| write.usart2sel().apb1()); + rcc.apb1rstr() + .modify(|_, write| write.usart2rst().set_bit()); + rcc.apb1rstr() + .modify(|_, write| write.usart2rst().clear_bit()); + rcc.apb1enr().modify(|_, write| write.usart2en().set_bit()); + peripheral.brr().write(|write| { + // SAFETY: `brr` is validated to fit the complete BRR register. + unsafe { write.bits(u32::from(brr)) } + }); + peripheral.cr1().write(|write| { + write + .te() + .set_bit() + .re() + .set_bit() + .rxneie() + .set_bit() + .ue() + .set_bit() + }); + peripheral.cr2().write(|write| write.stop().stop1()); + + Ok(Self { peripheral }) + } + + pub(crate) fn enable_dma_transmit(&self) { + self.peripheral.cr3().write(|write| write.dmat().set_bit()); + } + + pub(crate) fn transmit_data_register_address(&self) -> u32 { + self.peripheral.tdr().as_ptr() as u32 + } + + /// Blocks until USART2 accepts one byte for transmission. + /// + /// This is used by the platform's UART wire diagnostic. Flight data-channel traffic + /// uses [`Usart2TxDma`](super::Usart2TxDma) instead. + #[cfg(target_arch = "arm")] + pub fn write_blocking(&self, byte: u8) { + while !self.peripheral.isr().read().txe().is_not_full() {} + self.peripheral.tdr().write(|write| { + // SAFETY: `byte` is eight bits and therefore fits the USART's + // documented nine-bit TDR data field. + unsafe { write.tdr().bits(u16::from(byte)) } + }); + } + + /// Acknowledges one USART2 receive interrupt without accessing TX state. + pub fn acknowledge_receive_interrupt() -> ReceiveInterrupt { + // SAFETY: the platform binds USART2 exactly once and this function touches + // only its receive/error registers, never the TX DMA configuration. + let peripheral = unsafe { &*stm32f722::USART2::ptr() }; + acknowledge_receive_interrupt(peripheral) + } +} + +impl Usart1Tx { + /// Enables USART1 and configures 8N1 transmit and receive operation. + pub fn new( + rcc: &stm32f722::RCC, + peripheral: stm32f722::USART1, + peripheral_clock_hz: u32, + baud: u32, + ) -> Result { + let brr = calculate_brr(peripheral_clock_hz, baud)?; + + rcc.apb2rstr() + .modify(|_, write| write.usart1rst().set_bit()); + rcc.apb2rstr() + .modify(|_, write| write.usart1rst().clear_bit()); + rcc.apb2enr().modify(|_, write| write.usart1en().set_bit()); + peripheral.brr().write(|write| { + // SAFETY: `brr` is validated to fit the complete BRR register. + unsafe { write.bits(u32::from(brr)) } + }); + peripheral.cr1().write(|write| { + write + .te() + .set_bit() + .re() + .set_bit() + .rxneie() + .set_bit() + .ue() + .set_bit() + }); + peripheral.cr2().write(|write| write.stop().stop1()); + + Ok(Self { peripheral }) + } + + pub(crate) fn enable_dma_transmit(&self) { + self.peripheral.cr3().write(|write| write.dmat().set_bit()); + } + + pub(crate) fn transmit_data_register_address(&self) -> u32 { + self.peripheral.tdr().as_ptr() as u32 + } + + /// Acknowledges one USART1 receive interrupt without accessing TX state. + /// + /// The platform invokes this only from the `USART1` interrupt. USART1 TX DMA + /// owns its disjoint data register and DMA stream, so this static route may + /// safely acknowledge RX while that transfer is active. + pub fn acknowledge_receive_interrupt() -> ReceiveInterrupt { + // SAFETY: the platform binds USART1 exactly once and this function touches + // only its receive/error registers, never the TX DMA configuration. + let peripheral = unsafe { &*stm32f722::USART1::ptr() }; + acknowledge_receive_interrupt(peripheral) + } +} + +/// Owns configured UART4 hardware. +/// +/// This route is intended for peripherals that continuously stream data, such +/// as the MTF-01. It does not enable DMA state. +pub struct Uart4 { + _peripheral: stm32f722::UART4, +} + +impl Uart4 { + /// Enables UART4 and configures 8N1 transmit and receive operation. + pub fn new( + rcc: &stm32f722::RCC, + peripheral: stm32f722::UART4, + peripheral_clock_hz: u32, + baud: u32, + ) -> Result { + let brr = calculate_brr(peripheral_clock_hz, baud)?; + + rcc.apb1rstr().modify(|_, write| write.uart4rst().set_bit()); + rcc.apb1rstr() + .modify(|_, write| write.uart4rst().clear_bit()); + rcc.apb1enr().modify(|_, write| write.uart4en().set_bit()); + peripheral.brr().write(|write| { + // SAFETY: `brr` is validated to fit the complete BRR register. + unsafe { write.bits(u32::from(brr)) } + }); + peripheral.cr1().write(|write| { + write + .te() + .set_bit() + .re() + .set_bit() + .rxneie() + .set_bit() + .ue() + .set_bit() + }); + peripheral.cr2().write(|write| write.stop().stop1()); + + Ok(Self { + _peripheral: peripheral, + }) + } + + /// Acknowledges one UART4 receive interrupt. + pub fn acknowledge_receive_interrupt() -> ReceiveInterrupt { + // SAFETY: the platform binds UART4 exactly once and this function + // accesses only its receive and error registers. + let peripheral = unsafe { &*stm32f722::UART4::ptr() }; + acknowledge_receive_interrupt(peripheral) + } +} + +fn acknowledge_receive_interrupt( + peripheral: &stm32f722::usart1::RegisterBlock, +) -> ReceiveInterrupt { + let status = peripheral.isr().read(); + let fault = status.pe().is_error() + || status.fe().is_error() + || status.nf().is_noise() + || status.ore().is_overrun(); + + if status.rxne().is_data_ready() { + let byte = peripheral.rdr().read().rdr().bits() as u8; + if fault { + clear_receive_errors(peripheral); + ReceiveInterrupt::Fault + } else { + ReceiveInterrupt::Byte(byte) + } + } else if fault { + clear_receive_errors(peripheral); + ReceiveInterrupt::Fault + } else { + ReceiveInterrupt::NotPending + } +} + +fn clear_receive_errors(peripheral: &stm32f722::usart1::RegisterBlock) { + peripheral.icr().write(|write| { + write + .pecf() + .clear() + .fecf() + .clear() + .ncf() + .clear() + .orecf() + .clear() + }); +} + +const fn calculate_brr(peripheral_clock_hz: u32, baud: u32) -> Result { + if baud == 0 { + return Err(BaudRateError::Zero); + } + + let divisor = (peripheral_clock_hz + (baud / 2)) / baud; + if divisor == 0 || divisor > u16::MAX as u32 { + Err(BaudRateError::OutOfRange) + } else { + Ok(divisor as u16) + } +} + +#[cfg(test)] +mod tests { + use super::{BaudRateError, calculate_brr}; + + #[test] + fn calculates_reset_clock_brr() { + assert_eq!(calculate_brr(16_000_000, 115_200), Ok(139)); + } + + #[test] + fn rejects_zero_baud() { + assert_eq!(calculate_brr(16_000_000, 0), Err(BaudRateError::Zero)); + } +} diff --git a/libs/kaptan-stm32f7/src/stm32f722/usart1_tx_dma2_stream7.rs b/platforms/stm32f7/src/f722/peripherals/usart1_tx_dma2_stream7.rs similarity index 97% rename from libs/kaptan-stm32f7/src/stm32f722/usart1_tx_dma2_stream7.rs rename to platforms/stm32f7/src/f722/peripherals/usart1_tx_dma2_stream7.rs index 15a3ede..a8f5a46 100644 --- a/libs/kaptan-stm32f7/src/stm32f722/usart1_tx_dma2_stream7.rs +++ b/platforms/stm32f7/src/f722/peripherals/usart1_tx_dma2_stream7.rs @@ -1,7 +1,7 @@ //! Fixed STM32F722 DMA route for USART1 transmit. //! //! USART1 TX uses DMA2 stream 7, channel 4 on STM32F722. The route is a type, -//! rather than board-provided integer parameters, so invalid request mappings +//! rather than platform-provided integer parameters, so invalid request mappings //! cannot be selected at construction. use kaptan_dma::{DmaReadableBuffer, DmaTxEngine, TxCompletion}; @@ -38,7 +38,7 @@ impl Usart1TxDma { /// /// The caller must retain `buffer`'s backing storage until /// [`Self::acknowledge_interrupt`] returns a terminal result. - /// - The board must bind `DMA2_STREAM7`, call `acknowledge_interrupt` from + /// - The platform must bind `DMA2_STREAM7`, call `acknowledge_interrupt` from /// that interrupt, and defer lifecycle handling to its single owner. pub fn start(&mut self, buffer: DmaReadableBuffer<'_>) -> Result<(), StartError> { let bytes = buffer.bytes(); diff --git a/platforms/stm32f7/src/f722/peripherals/usart2_tx_dma1_stream6.rs b/platforms/stm32f7/src/f722/peripherals/usart2_tx_dma1_stream6.rs new file mode 100644 index 0000000..ceb4922 --- /dev/null +++ b/platforms/stm32f7/src/f722/peripherals/usart2_tx_dma1_stream6.rs @@ -0,0 +1,219 @@ +//! Fixed STM32F722 DMA route for USART2 transmit. +//! +//! USART2 TX uses DMA1 Stream6, channel 4 on STM32F722. The route is a type, +//! rather than platform-provided integer parameters, so invalid request mappings +//! cannot be selected at construction. + +use kaptan_dma::{DmaReadableBuffer, DmaTxEngine, TxCompletion}; +use stm32f7::stm32f722; + +use super::Usart2Tx; + +/// Owns the fixed USART2 TX DMA1 Stream6 route. +pub struct Usart2TxDma { + usart: Usart2Tx, +} + +/// Failure to start one DMA frame transfer on USART2. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Usart2TxDmaStartError { + /// DMA cannot transfer an empty buffer or one exceeding the NDTR limit. + InvalidBufferLength, + /// DMA1 stream 6 still owns an earlier transfer. + Busy, +} + +impl Usart2TxDma { + /// Enables DMA1 and connects USART2 TX to its fixed request route. + pub fn new(rcc: &stm32f722::RCC, usart: Usart2Tx) -> Self { + rcc.ahb1enr().modify(|_, write| write.dma1en().set_bit()); + Self { usart } + } + + /// Starts one memory-to-USART DMA transfer. + /// + /// The caller retains the backing storage until + /// [`Self::acknowledge_interrupt`] reports a terminal result. + pub fn start(&mut self, buffer: DmaReadableBuffer<'_>) -> Result<(), Usart2TxDmaStartError> { + let bytes = buffer.bytes(); + if bytes.is_empty() || bytes.len() > u16::MAX as usize { + return Err(Usart2TxDmaStartError::InvalidBufferLength); + } + if self.is_active() { + return Err(Usart2TxDmaStartError::Busy); + } + + self.clear_interrupt_flags(); + // Idle USART2 has TXE set. Enabling DMAT *after* the stream is armed + // creates the initial request for byte zero without manually writing + // TDR, so the whole frame is owned by DMA. + self.configure_stream(bytes); + self.stream().cr().modify(|_, write| write.en().enabled()); + self.usart.enable_dma_transmit(); + Ok(()) + } + + /// Acknowledges a DMA1 Stream6 terminal IRQ without accessing engine state. + pub fn acknowledge_interrupt() -> TxCompletion { + // SAFETY: this handler accesses only the fixed DMA1 Stream6 route. + let controller = unsafe { &*stm32f722::DMA1::ptr() }; + let status = controller.hisr().read(); + let completion = DmaTerminalStatus { + transfer_complete: status.tcif6().is_complete(), + transfer_error: status.teif6().is_error(), + direct_mode_error: status.dmeif6().is_error(), + fifo_error: status.feif6().is_error(), + } + .classify(); + if completion == TxCompletion::NotPending { + return completion; + } + + controller + .st(6) + .cr() + .modify(|_, write| write.en().disabled()); + // SAFETY: USART2 is initialized once by `Usart2Tx::new`; this ISR + // clears only its DMA transmit-request enable after the stream's + // terminal status has been observed. + unsafe { + (&*stm32f722::USART2::ptr()) + .cr3() + .modify(|_, write| write.dmat().clear_bit()) + }; + controller.hifcr().write(|write| { + write + .ctcif6() + .clear() + .chtif6() + .clear() + .cteif6() + .clear() + .cdmeif6() + .clear() + .cfeif6() + .clear() + }); + completion + } + + /// Returns whether DMA1 Stream6 owns a transfer. + pub fn is_active(&self) -> bool { + self.stream().cr().read().en().is_enabled() + } + + fn configure_stream(&mut self, buffer: &[u8]) { + self.stream().cr().write(|write| { + // SAFETY: Channel 4 and these transfer fields are the documented + // USART2 TX mapping for DMA1 Stream6 on STM32F722. + unsafe { + write + .chsel() + .bits(4) + .dir() + .memory_to_peripheral() + .pl() + .very_high() + .minc() + .incremented() + .pinc() + .fixed() + .msize() + .bits8() + .psize() + .bits8() + .circ() + .disabled() + .tcie() + .enabled() + .teie() + .enabled() + .dmeie() + .enabled() + } + }); + self.stream().ndtr().write(|write| { + // SAFETY: `start` checked the slice length against the NDTR limit. + unsafe { write.bits(buffer.len() as u32) } + }); + self.stream().par().write(|write| { + // SAFETY: This is the memory-mapped USART2 transmit-data register. + unsafe { write.bits(self.usart.transmit_data_register_address()) } + }); + self.stream().m0ar().write(|write| { + // SAFETY: The caller of `start` upholds the buffer lifetime contract. + unsafe { write.bits(buffer.as_ptr() as u32) } + }); + } + + fn stream(&self) -> &stm32f722::dma2::ST { + self.controller().st(6) + } + + fn clear_interrupt_flags(&self) { + self.controller().hifcr().write(|write| { + write + .ctcif6() + .clear() + .chtif6() + .clear() + .cteif6() + .clear() + .cdmeif6() + .clear() + .cfeif6() + .clear() + }); + } + + fn controller(&self) -> &stm32f722::dma2::RegisterBlock { + // SAFETY: SPI3 owns DMA1 initialization; USART2 TX is permanently + // assigned to disjoint Stream6 and accesses no other stream. + unsafe { &*stm32f722::DMA1::ptr() } + } +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +struct DmaTerminalStatus { + transfer_complete: bool, + transfer_error: bool, + direct_mode_error: bool, + fifo_error: bool, +} + +impl DmaTerminalStatus { + const fn classify(self) -> TxCompletion { + if self.transfer_error || self.direct_mode_error || self.fifo_error { + TxCompletion::TransferFault + } else if self.transfer_complete { + TxCompletion::Complete + } else { + TxCompletion::NotPending + } + } +} + +impl DmaTxEngine for Usart2TxDma { + type StartError = Usart2TxDmaStartError; + + fn start(&mut self, buffer: DmaReadableBuffer<'_>) -> Result<(), Self::StartError> { + Self::start(self, buffer) + } +} + +#[cfg(test)] +mod tests { + use super::DmaTerminalStatus; + use kaptan_dma::TxCompletion; + + #[test] + fn terminal_error_has_precedence_over_completion() { + let status = DmaTerminalStatus { + transfer_complete: true, + transfer_error: false, + direct_mode_error: false, + fifo_error: true, + }; + assert_eq!(status.classify(), TxCompletion::TransferFault); + } +} diff --git a/platforms/stm32f7/src/f722/routes.rs b/platforms/stm32f7/src/f722/routes.rs new file mode 100644 index 0000000..10077f9 --- /dev/null +++ b/platforms/stm32f7/src/f722/routes.rs @@ -0,0 +1,142 @@ +//! Platform-owned STM32F722 electrical route implementations. + +use super::peripherals::{Spi1, Spi1Error, Spi3, Spi3Error}; +use kaptan_spi::{SpiCompletion, SpiDevice}; +use stm32f7::stm32f722; + +/// Owns the MPU6000 SPI1 transaction path and its PB2 chip select. +pub(crate) struct Mpu6000Spi1Route { + spi: Spi1, + chip_select: stm32f722::GPIOB, +} + +impl Mpu6000Spi1Route { + pub(crate) fn new(rcc: &stm32f722::RCC, spi: Spi1, chip_select: stm32f722::GPIOB) -> Self { + rcc.ahb1enr().modify(|_, write| write.gpioben().set_bit()); + chip_select + .moder() + .modify(|_, write| write.moder2().output()); + chip_select + .otyper() + .modify(|_, write| write.ot2().push_pull()); + chip_select + .ospeedr() + .modify(|_, write| write.ospeedr2().low_speed()); + chip_select + .pupdr() + .modify(|_, write| write.pupdr2().floating()); + chip_select.bsrr().write(|write| write.bs2().set_bit()); + Self { spi, chip_select } + } + + fn select(&self) { + self.chip_select.bsrr().write(|write| write.br2().set_bit()); + } + + fn deselect(&self) { + self.chip_select.bsrr().write(|write| write.bs2().set_bit()); + } +} + +impl SpiDevice for Mpu6000Spi1Route { + type StartError = Spi1Error; + + fn start_transfer( + &mut self, + transmit: &[u8], + receive: &mut [u8], + ) -> Result<(), Self::StartError> { + self.select(); + if let Err(error) = self.spi.start_transfer(transmit, receive) { + self.deselect(); + return Err(error); + } + Ok(()) + } + + fn complete_transfer(&mut self, completion: SpiCompletion) { + self.spi.complete_transfer(completion); + if matches!( + completion, + SpiCompletion::Complete | SpiCompletion::TransferFault + ) { + self.deselect(); + } + } +} + +/// Owns the ICM-42688-P SPI3 transaction path. PA15 is its chip select. +/// +/// GPIOA itself stays owned by the platform data-channel/LED state. The route +/// uses only PA15 BSRR writes, disjoint from USART2 (PA2/PA3) and the blue +/// LED (PA13), so no read-modify-write alias exists between these owners. +pub(crate) struct Icm42688Spi3Route { + spi: Spi3, +} + +impl Icm42688Spi3Route { + pub(crate) fn new(rcc: &stm32f722::RCC, spi: Spi3, chip_select: &stm32f722::GPIOA) -> Self { + rcc.ahb1enr().modify(|_, write| write.gpioaen().set_bit()); + chip_select + .moder() + .modify(|_, write| write.moder15().output()); + chip_select + .otyper() + .modify(|_, write| write.ot15().push_pull()); + chip_select + .ospeedr() + .modify(|_, write| write.ospeedr15().low_speed()); + chip_select + .pupdr() + .modify(|_, write| write.pupdr15().floating()); + chip_select.bsrr().write(|write| write.bs15().set_bit()); + Self { spi } + } + + fn chip_select_port() -> &'static stm32f722::gpioa::RegisterBlock { + // SAFETY: RTIC initialization configures GPIOA once before interrupts + // are enabled. This route subsequently writes only PA15 through BSRR; + // USART2 uses PA2/PA3 and the LED uses PA13 through disjoint BSRR bits. + // No task performs a GPIOA read-modify-write on those shared outputs. + unsafe { &*stm32f722::GPIOA::ptr() } + } + + fn select(&self) { + Self::chip_select_port() + .bsrr() + .write(|write| write.br15().set_bit()); + } + + fn deselect(&self) { + Self::chip_select_port() + .bsrr() + .write(|write| write.bs15().set_bit()); + } +} + +impl SpiDevice for Icm42688Spi3Route { + type StartError = Spi3Error; + + fn start_transfer( + &mut self, + transmit: &[u8], + receive: &mut [u8], + ) -> Result<(), Self::StartError> { + self.select(); + if let Err(error) = self.spi.start_transfer(transmit, receive) { + self.deselect(); + return Err(error); + } + Ok(()) + } + + fn complete_transfer(&mut self, completion: SpiCompletion) { + self.spi.complete_transfer(completion); + if matches!( + completion, + SpiCompletion::Complete | SpiCompletion::TransferFault + ) { + self.deselect(); + } + } +} diff --git a/platforms/stm32f7/src/f722/sensors.rs b/platforms/stm32f7/src/f722/sensors.rs new file mode 100644 index 0000000..eec4d23 --- /dev/null +++ b/platforms/stm32f7/src/f722/sensors.rs @@ -0,0 +1,163 @@ +//! Platform-owned F722 sensor-service assembly. + +use super::peripherals::{DwtMicrosecondClock, Spi1, Spi3}; +use kaptan_drivers::{ + icm42688::{Icm42688, Icm42688Service, Icm42688ServiceEvent}, + mpu6000::{ + AccelerometerRange, DigitalLowPassFilter, GyroscopeRange, Mpu6000, Mpu6000Config, + Mpu6000Service, Mpu6000ServiceEvent, + }, +}; +use kaptan_sensor::imu::ImuSnapshot; +use kaptan_spi::SpiCompletion; +use stm32f7::stm32f722; + +use super::routes::{Icm42688Spi3Route, Mpu6000Spi1Route}; + +const MPU6000_CONFIG: Mpu6000Config = Mpu6000Config { + accelerometer: AccelerometerRange::PlusMinus4G, + gyroscope: GyroscopeRange::PlusMinus500DegreesPerSecond, + digital_low_pass_filter: DigitalLowPassFilter::Bandwidth42Hz, + sample_rate_divider: 9, +}; + +/// A profile-neutral IMU worker result. Device protocol errors are consumed +/// here because flight policy only needs the terminal healthy/fault outcome. +pub(crate) enum ImuEvent { + Idle, + Initialized, + Sample(ImuSnapshot), + Fault, +} + +/// Opaque owner of the selected F722 IMU service. +/// +/// RTIC places local resource types in generated public metadata, so this +/// wrapper is public. The device services and routes remain private to the +/// platform, preserving the board-facing abstraction boundary. +pub struct F722Imu { + inner: F722ImuKind, +} + +/// The two concrete IMU services supported by the F722 platform. +enum F722ImuKind { + Mpu6000(Mpu6000Service), + Icm42688(Icm42688Service), +} + +impl F722Imu { + pub(crate) fn mpu6000( + rcc: &stm32f722::RCC, + spi: stm32f722::SPI1, + dma: stm32f722::DMA2, + chip_select: stm32f722::GPIOB, + clock: DwtMicrosecondClock, + ) -> Self { + Self { + inner: F722ImuKind::Mpu6000(Mpu6000Service::new( + clock, + Mpu6000::new( + Mpu6000Spi1Route::new(rcc, Spi1::new(rcc, spi, dma), chip_select), + MPU6000_CONFIG, + ), + )), + } + } + + pub(crate) fn icm42688( + rcc: &stm32f722::RCC, + spi: stm32f722::SPI3, + dma: stm32f722::DMA1, + chip_select: &stm32f722::GPIOA, + clock: DwtMicrosecondClock, + ) -> Self { + Self { + inner: F722ImuKind::Icm42688(Icm42688Service::new( + clock, + Icm42688::new(Icm42688Spi3Route::new( + rcc, + Spi3::new(rcc, spi, dma), + chip_select, + )), + )), + } + } + + pub(crate) fn begin(&mut self) -> ImuEvent { + let ok = match &mut self.inner { + F722ImuKind::Mpu6000(imu) => imu.begin().is_ok(), + F722ImuKind::Icm42688(imu) => imu.begin().is_ok(), + }; + if ok { ImuEvent::Idle } else { ImuEvent::Fault } + } + + pub(crate) fn on_periodic_tick(&mut self) -> ImuEvent { + match &mut self.inner { + F722ImuKind::Mpu6000(imu) => mpu_event(imu.on_periodic_tick()), + F722ImuKind::Icm42688(imu) => icm_event(imu.on_periodic_tick()), + } + } + + pub(crate) fn on_data_ready(&mut self) -> ImuEvent { + match &mut self.inner { + F722ImuKind::Mpu6000(imu) => mpu_event(imu.on_data_ready()), + F722ImuKind::Icm42688(imu) => icm_event(imu.on_data_ready()), + } + } + + pub(crate) fn on_transfer_complete(&mut self, completion: SpiCompletion) -> ImuEvent { + match &mut self.inner { + F722ImuKind::Mpu6000(imu) => mpu_event(imu.on_transfer_complete(completion)), + F722ImuKind::Icm42688(imu) => icm_event(imu.on_transfer_complete(completion)), + } + } +} + +fn mpu_event(event: Mpu6000ServiceEvent) -> ImuEvent { + match event { + Mpu6000ServiceEvent::Idle => ImuEvent::Idle, + Mpu6000ServiceEvent::Initialized => ImuEvent::Initialized, + Mpu6000ServiceEvent::Sample(sample) => ImuEvent::Sample(sample), + Mpu6000ServiceEvent::Fault(_) => ImuEvent::Fault, + } +} + +fn icm_event(event: Icm42688ServiceEvent) -> ImuEvent { + match event { + Icm42688ServiceEvent::Idle => ImuEvent::Idle, + Icm42688ServiceEvent::Initialized => ImuEvent::Initialized, + Icm42688ServiceEvent::Sample(sample) => ImuEvent::Sample(align_icm42688(sample)), + Icm42688ServiceEvent::Fault(_) => ImuEvent::Fault, + } +} + +/// Aligns the verified GEP-F722-HD V2 ICM-42688-P yaw-180° mounting to the +/// flight-controller body frame. The MPU6000 profile is already aligned. +fn align_icm42688(mut sample: ImuSnapshot) -> ImuSnapshot { + sample.acceleration_milligravity[0] = sample.acceleration_milligravity[0].saturating_neg(); + sample.acceleration_milligravity[1] = sample.acceleration_milligravity[1].saturating_neg(); + sample.angular_rate_milliradians_per_second[0] = + sample.angular_rate_milliradians_per_second[0].saturating_neg(); + sample.angular_rate_milliradians_per_second[1] = + sample.angular_rate_milliradians_per_second[1].saturating_neg(); + sample +} + +#[cfg(test)] +mod tests { + use super::align_icm42688; + use kaptan_sensor::imu::ImuSnapshot; + use kaptan_time::Timestamp; + + #[test] + fn geprc_profile_applies_its_documented_yaw_alignment() { + let sample = align_icm42688(ImuSnapshot::new( + Timestamp::from_microseconds(0), + [10, 20, 30], + [40, 50, 60], + 0, + )); + assert_eq!(sample.acceleration_milligravity, [-10, -20, 30]); + assert_eq!(sample.angular_rate_milliradians_per_second, [-40, -50, 60]); + } +} diff --git a/platforms/stm32f7/src/lib.rs b/platforms/stm32f7/src/lib.rs new file mode 100644 index 0000000..8058d0d --- /dev/null +++ b/platforms/stm32f7/src/lib.rs @@ -0,0 +1,484 @@ +//! STM32F7 execution platform for declarative Kaptan boards. +//! +//! A board crate selects the safe route descriptors below and invokes +//! [`flight_firmware!`]. The macro emits only a declarative link-time board +//! registration; the one RTIC application for every supported STM32F722 +//! profile is defined and compiled in this crate. Consequently RTIC, PAC +//! ownership, interrupt acknowledgement, DMA and atomics remain platform +//! implementation details. + +#![no_std] + +use core::marker::PhantomData; + +pub use kaptan_board::BoardConfig; +use kaptan_board::BoardConfigError; + +mod actuator; +mod f722; + +/// Dependencies used only while the platform expands its private RTIC macro. +/// They stay crate-private: board binaries never name them. +#[cfg(target_arch = "arm")] +pub(crate) mod __platform_internal { + pub(crate) use cortex_m as cm; + pub(crate) use cortex_m_rt as cmrt; + pub(crate) use kaptan_dma as dma; + pub(crate) use kaptan_mavlink as mavlink; + pub(crate) use kaptan_spi as spi; + pub(crate) use panic_halt as panic; + pub(crate) use rtic as rtic_app; + pub(crate) use stm32f7 as pac; +} + +/// The isolated diagnostic macro expands an entry attribute in a board binary. +/// Rust macro visibility requires this one dependency to be reachable; it is +/// not a platform configuration or execution API. +#[cfg(target_arch = "arm")] +#[doc(hidden)] +pub mod __macro_support { + pub use cortex_m_rt; +} + +/// Construction or validation failure for a declarative F722 board. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum BoardError { + /// Portable board metadata is self-inconsistent. + InvalidConfig(BoardConfigError), + /// Metadata and the selected vertical-sensor route disagree. + VerticalSensorCapabilityMismatch, + /// The declaration requests a service-timer frequency not implemented by + /// the selected F722 platform profile. + UnsupportedServiceFrequency, + /// The platform could not establish a clock, peripheral, or DMA owner. + Initialization, +} + +/// MPU6000 component selector. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Mpu6000; + +/// The supported MPU6000 SPI1 electrical route before selecting DRDY. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Mpu6000Spi1; + +/// The complete MPU6000 SPI1 / PA4 data-ready route. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Mpu6000Spi1Exti4; + +impl Mpu6000 { + /// Selects the supported SPI1 MPU6000 connection. + pub const fn on_spi1() -> Mpu6000Spi1 { + Mpu6000Spi1 + } +} + +impl Mpu6000Spi1 { + /// Completes the supported PA4 data-ready connection. + pub const fn with_drdy(self, _: Pa4) -> Mpu6000Spi1Exti4 { + Mpu6000Spi1Exti4 + } +} + +/// ICM-42688-P component selector. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Icm42688; + +/// The supported ICM-42688-P SPI3 electrical route before selecting DRDY. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Icm42688Spi3; + +/// The complete ICM-42688-P SPI3 / PC3 data-ready route. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Icm42688Spi3Exti3; + +impl Icm42688 { + /// Selects the supported SPI3 ICM-42688-P connection. + pub const fn on_spi3() -> Icm42688Spi3 { + Icm42688Spi3 + } +} + +impl Icm42688Spi3 { + /// Completes the supported PC3 data-ready connection. + pub const fn with_drdy(self, _: Pc3) -> Icm42688Spi3Exti3 { + Icm42688Spi3Exti3 + } +} + +/// MPU6000 data-ready pin selector. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Pa4; +/// ICM-42688-P data-ready pin selector. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Pc3; + +/// Motor-output component selector. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct MotorOutputs; +/// Kakute TIM3/TIM5 motor route before selecting a waveform protocol. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct KakuteTim3Tim5Motors; +/// GEPRC TIM3/TIM5 motor route before selecting a waveform protocol. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct GeprcTim3Tim5Motors; +/// Validated Kakute TIM3/TIM5 DShot600 motor route. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct KakuteTim3Tim5Dshot600; +/// Validated GEPRC TIM3/TIM5 DShot600 motor route. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct GeprcTim3Tim5Dshot600; + +impl MotorOutputs { + /// Selects Kakute's M1–M4 mapping: PB0, PB1, PB4, PB5. + pub const fn kakute_tim3_tim5() -> KakuteTim3Tim5Motors { + KakuteTim3Tim5Motors + } + + /// Selects GEPRC's M1–M4 mapping: PB4, PB5, PB0, PB1. + pub const fn geprc_tim3_tim5() -> GeprcTim3Tim5Motors { + GeprcTim3Tim5Motors + } +} + +impl KakuteTim3Tim5Motors { + /// Selects the platform-validated DShot600 waveform configuration. + pub const fn dshot600(self) -> KakuteTim3Tim5Dshot600 { + KakuteTim3Tim5Dshot600 + } +} + +impl GeprcTim3Tim5Motors { + /// Selects the platform-validated DShot600 waveform configuration. + pub const fn dshot600(self) -> GeprcTim3Tim5Dshot600 { + GeprcTim3Tim5Dshot600 + } +} + +/// MAVLink component selector. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Mavlink; +/// Validated MAVLink route using USART1. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct MavlinkUsart1; +/// Validated MAVLink route using USART2. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct MavlinkUsart2; + +impl Mavlink { + /// Selects the supported USART1 MAVLink route. + pub const fn on_usart1() -> MavlinkUsart1 { + MavlinkUsart1 + } + + /// Selects the supported USART2 MAVLink route. + pub const fn on_usart2() -> MavlinkUsart2 { + MavlinkUsart2 + } +} + +/// Vertical-sensor component selector. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct VerticalSensor; +/// No vertical-sensor connection is selected. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct NoVerticalSensor; +/// Validated MTF-01 Microlink route. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Mtf01Uart4; +/// Validated LIDAR-Lite v3 route. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct LidarLiteV3I2c1; + +impl VerticalSensor { + /// Declares no vertical-sensor peripheral. + pub const fn none() -> NoVerticalSensor { + NoVerticalSensor + } + + /// Selects the supported MTF-01 Microlink connection. + pub const fn mtf01_on_uart4() -> Mtf01Uart4 { + Mtf01Uart4 + } + + /// Selects the supported LIDAR-Lite v3 connection. + pub const fn lidar_lite_v3_on_i2c1() -> LidarLiteV3I2c1 { + LidarLiteV3I2c1 + } +} + +/// Unselected typestate used only while a board declaration is incomplete. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct Missing; +/// Safe typestate builder for a declarative F722 board. +/// +/// This type has no peripherals, PAC types, interrupt names, DMA identifiers, +/// atomics, or unsafe operations. The F722 platform has one fixed validated +/// clock tree, so only board-selectable electrical routes participate in +/// typestate. A final `build()` exists only for complete supported route +/// combinations, so omitted or cross-wired essential routes fail to compile at +/// the board declaration. +pub struct F7BoardBuilder< + Imu = Missing, + Motors = Missing, + MavlinkRoute = Missing, + Vertical = Missing, +> { + _state: PhantomData<(Imu, Motors, MavlinkRoute, Vertical)>, +} + +impl F7BoardBuilder { + /// Starts an empty board declaration. + pub const fn new() -> Self { + Self { + _state: PhantomData, + } + } +} + +impl Default for F7BoardBuilder { + fn default() -> Self { + Self::new() + } +} + +impl F7BoardBuilder { + /// Selects an IMU route descriptor. + /// + /// Only a complete, profile-compatible descriptor has a final + /// [`F7BoardBuilder::build`] implementation. Keeping this one selection + /// operation uniform avoids teaching a board author platform profile + /// mechanics merely because the selected IMU model differs. + pub fn imu(self, _: Selected) -> F7BoardBuilder { + F7BoardBuilder { + _state: PhantomData, + } + } + + /// Selects the TIM3/TIM5 DShot600 output route. + pub fn motors(self, _: Selected) -> F7BoardBuilder { + F7BoardBuilder { + _state: PhantomData, + } + } + + /// Selects a validated MAVLink route. Only profile-compatible route types + /// have a final [`F7BoardBuilder::build`] implementation. + pub fn mavlink(self, _: Selected) -> F7BoardBuilder { + F7BoardBuilder { + _state: PhantomData, + } + } + + /// Selects a validated vertical-sensor route. Its capability is checked + /// against [`F7Board::CONFIG`] before any peripheral is initialized. + pub fn vertical_sensor(self, _: Selected) -> F7BoardBuilder { + F7BoardBuilder { + _state: PhantomData, + } + } +} + +/// Finite STM32F722 execution profile selected from safe route descriptors. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum F722Profile { + Mpu6000Spi1Usart1, + Icm42688Spi3Usart2, +} + +/// Fully validated, non-executing F722 board declaration. +/// +/// It is deliberately only a compact route description. Peripheral tokens +/// are taken later by the platform's RTIC initializer and never reach a board +/// crate. +#[doc(hidden)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct F722BoardRoutes { + profile: F722Profile, + motors: f722::MotorRoute, + vertical: f722::VerticalRoute, +} + +impl F722BoardRoutes { + pub(crate) const fn profile(self) -> F722Profile { + self.profile + } +} + +macro_rules! impl_mpu6000_routes { + ($vertical:ty, $route:expr) => { + impl F7BoardBuilder { + /// Finishes a complete supported MPU6000 F722 declaration. + pub const fn build(self) -> Result { + Ok(F722BoardRoutes { + profile: F722Profile::Mpu6000Spi1Usart1, + motors: f722::MotorRoute::KakuteTim3Tim5, + vertical: $route, + }) + } + } + }; +} + +impl_mpu6000_routes!(Mtf01Uart4, f722::VerticalRoute::Mtf01); +impl_mpu6000_routes!(LidarLiteV3I2c1, f722::VerticalRoute::LidarLiteV3); +impl_mpu6000_routes!(NoVerticalSensor, f722::VerticalRoute::None); + +macro_rules! impl_icm42688_routes { + ($vertical:ty, $route:expr) => { + impl F7BoardBuilder { + /// Finishes a complete supported ICM-42688-P F722 declaration. + pub const fn build(self) -> Result { + Ok(F722BoardRoutes { + profile: F722Profile::Icm42688Spi3Usart2, + motors: f722::MotorRoute::GeprcTim3Tim5, + vertical: $route, + }) + } + } + }; +} + +impl_icm42688_routes!(NoVerticalSensor, f722::VerticalRoute::None); +impl_icm42688_routes!(Mtf01Uart4, f722::VerticalRoute::Mtf01); +impl_icm42688_routes!(LidarLiteV3I2c1, f722::VerticalRoute::LidarLiteV3); + +/// Missing and cross-profile route selections are rejected before firmware is +/// linked, because no matching `build` implementation exists. +/// +/// ```compile_fail +/// use platform_stm32f7::{ +/// F7BoardBuilder, Mavlink, MotorOutputs, Mpu6000, Pa4, +/// }; +/// +/// let _ = F7BoardBuilder::new() +/// .imu(Mpu6000::on_spi1().with_drdy(Pa4)) +/// .motors(MotorOutputs::kakute_tim3_tim5().dshot600()) +/// .mavlink(Mavlink::on_usart1()) +/// .build(); // vertical route is missing +/// ``` +/// +/// ```compile_fail +/// use platform_stm32f7::{ +/// F7BoardBuilder, Icm42688, Mavlink, MotorOutputs, Pc3, +/// VerticalSensor, +/// }; +/// +/// let _ = F7BoardBuilder::new() +/// .imu(Icm42688::on_spi3().with_drdy(Pc3)) +/// .motors(MotorOutputs::geprc_tim3_tim5().dshot600()) +/// .mavlink(Mavlink::on_usart1()) // ICM route requires USART2 +/// .vertical_sensor(VerticalSensor::none()) +/// .build(); +/// ``` +/// +/// ```compile_fail +/// use platform_stm32f7::{ +/// F7BoardBuilder, Icm42688, Mavlink, MotorOutputs, Pc3, VerticalSensor, +/// }; +/// +/// let _ = F7BoardBuilder::new() +/// .imu(Icm42688::on_spi3().with_drdy(Pc3)) +/// .motors(MotorOutputs::kakute_tim3_tim5().dshot600()) +/// .mavlink(Mavlink::on_usart2()) +/// .vertical_sensor(VerticalSensor::none()) +/// .build(); // ICM/USART2 uses the GEPRC logical motor order. +/// ``` +/// Contract implemented by a declarative STM32F722 board crate. +pub trait F7Board { + /// Portable identity, capabilities and outbound data-channel policy. + const CONFIG: BoardConfig; + + /// Selects one complete supported F722 route graph. + fn build(platform: F7BoardBuilder) -> Result; +} + +/// Checks portable metadata at compile time. +#[macro_export] +macro_rules! enforce_f7_board_contract { + ($board:ty) => { + const _: () = match <$board as $crate::F7Board>::CONFIG.validate() { + Ok(()) => (), + Err(_) => panic!("invalid STM32F7 board configuration"), + }; + }; +} + +/// Installs the platform-owned RTIC flight firmware for a declarative board. +/// +/// The application binds the finite superset of interrupt roles needed by the +/// currently supported F722 profiles. The route declaration selects which +/// roles are initialized; inactive bindings acknowledge nothing. Adding an +/// interrupt role is therefore a platform-only change, not a board task graph. +#[macro_export] +macro_rules! flight_firmware { + ($board:ty) => { + $crate::enforce_f7_board_contract!($board); + // The platform RTIC application calls these symbols during `init`. + // They carry only the declarative board contract; RTIC, PAC, DMA, + // atomics, and unsafe operations remain inside this platform crate. + #[unsafe(no_mangle)] + pub static KAPTAN_F722_BOARD_CONFIG: $crate::BoardConfig = + <$board as $crate::F7Board>::CONFIG; + #[unsafe(no_mangle)] + pub extern "Rust" fn kaptan_f722_board_routes() + -> Result<$crate::F722BoardRoutes, $crate::BoardError> { + <$board as $crate::F7Board>::build($crate::F7BoardBuilder::new()) + } + #[used] + static KAPTAN_F722_PLATFORM_FIRMWARE: fn() = $crate::firmware_anchor; + }; +} + +/// Installs the isolated GEPRC UART2 physical-route diagnostic image. +/// +/// This diagnostic never installs the flight application. It remains a +/// platform-owned image and is intentionally separate from board declarations. +#[macro_export] +macro_rules! geprc_uart2_wire_test { + () => { + #[$crate::__macro_support::cortex_m_rt::entry] + fn main() -> ! { + $crate::__private::geprc_uart2_wire_test() + } + }; +} + +/// Implementation support consumed only by the exported firmware macros. +#[doc(hidden)] +pub mod __private { + pub use crate::f722::geprc_uart2_wire_test; +} + +/// Link anchor for the platform-owned F722 RTIC application. +#[doc(hidden)] +pub fn firmware_anchor() {} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn route_descriptors_select_the_mpu6000_profile() { + let routes = F7BoardBuilder::new() + .imu(Mpu6000::on_spi1().with_drdy(Pa4)) + .motors(MotorOutputs::kakute_tim3_tim5().dshot600()) + .mavlink(Mavlink::on_usart1()) + .vertical_sensor(VerticalSensor::mtf01_on_uart4()) + .build() + .unwrap(); + assert_eq!(routes.profile(), F722Profile::Mpu6000Spi1Usart1); + } + + #[test] + fn route_descriptors_select_the_icm42688_profile() { + let routes = F7BoardBuilder::new() + .imu(Icm42688::on_spi3().with_drdy(Pc3)) + .motors(MotorOutputs::geprc_tim3_tim5().dshot600()) + .mavlink(Mavlink::on_usart2()) + .vertical_sensor(VerticalSensor::none()) + .build() + .unwrap(); + assert_eq!(routes.profile(), F722Profile::Icm42688Spi3Usart2); + } +}