diff --git a/libs/kaptan-stm32f7/src/stm32f722/gpio.rs b/libs/kaptan-stm32f7/src/stm32f722/gpio.rs index d8301aa..66375de 100644 --- a/libs/kaptan-stm32f7/src/stm32f722/gpio.rs +++ b/libs/kaptan-stm32f7/src/stm32f722/gpio.rs @@ -48,6 +48,22 @@ pub fn configure_spi1_pins_pa5_pa6_pa7(rcc: &stm32f722::RCC, port: &stm32f722::G }); } +/// 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()); + port.moder() + .modify(|_, w| w.moder8().alternate().moder9().alternate()); + // SAFETY: AF4 is the documented I2C1 alternate-function selector for PB8/PB9. + port.afrh() + .modify(|_, w| unsafe { w.afrh8().bits(4).afrh9().bits(4) }); + port.otyper() + .modify(|_, w| w.ot8().open_drain().ot9().open_drain()); + port.ospeedr() + .modify(|_, w| w.ospeedr8().high_speed().ospeedr9().high_speed()); + port.pupdr() + .modify(|_, w| w.pupdr8().floating().pupdr9().floating()); +} + /// Configures PA4 as the input pin for an EXTI4 route. /// /// A board must select this explicitly rather than inheriting the pin mode diff --git a/libs/kaptan-stm32f7/src/stm32f722/i2c1.rs b/libs/kaptan-stm32f7/src/stm32f722/i2c1.rs new file mode 100644 index 0000000..fe2f6f3 --- /dev/null +++ b/libs/kaptan-stm32f7/src/stm32f722/i2c1.rs @@ -0,0 +1,361 @@ +//! STM32F722 I²C1 interrupt-driven controller. + +use kaptan_i2c::{I2cBus, I2cCompletion}; +use stm32f7::stm32f722; + +const MAX_BYTES: usize = 255; +const CR1_INTERRUPTS: u32 = (1 << 0) | (1 << 1) | (1 << 2) | (1 << 4) | (1 << 5) | (1 << 7); +const ISR_TXIS: u32 = 1 << 1; +const ISR_RXNE: u32 = 1 << 2; +const ISR_NACKF: u32 = 1 << 4; +const ISR_STOPF: u32 = 1 << 5; +const ISR_BERR: u32 = 1 << 8; +const ISR_ARLO: u32 = 1 << 9; +const ISR_OVR: u32 = 1 << 10; +const ISR_ERRORS: u32 = ISR_BERR | ISR_ARLO | ISR_OVR; +const ICR_NACKCF: u32 = 1 << 4; +const ICR_STOPCF: u32 = 1 << 5; +const ICR_ERRORS: u32 = (1 << 8) | (1 << 9) | (1 << 10); +const ICR_TERMINAL: u32 = ICR_NACKCF | ICR_STOPCF | ICR_ERRORS; +const ICR_FAULTS: u32 = ICR_NACKCF | ICR_ERRORS; +const CR2_STOP: u32 = 1 << 14; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum I2c1Error { + InvalidLength, + InvalidAddress, + Busy, +} + +/// Board-selected I²C1 electrical timing. +#[derive(Clone, Copy)] +pub struct I2c1Config { + timing: u32, +} + +impl I2c1Config { + /// Creates a configuration from an STM32 `I2C_TIMINGR` register value. + pub const fn from_timing_register(timing: u32) -> Self { + Self { timing } + } +} + +#[derive(Clone, Copy)] +enum Direction { + Write, + Read, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum TransferState { + Idle, + Active, + Stopping, +} + +fn read_index_after_event(direction: Direction, flags: u32, index: usize, length: usize) -> usize { + if matches!(direction, Direction::Read) && flags & ISR_RXNE != 0 && index < length { + index + 1 + } else { + index + } +} + +fn completion_after_stop(index: usize, length: usize) -> I2cCompletion { + if index == length { + I2cCompletion::Complete + } else { + I2cCompletion::TransferFault + } +} + +impl TransferState { + fn accepts_start(self, bus_busy: bool) -> bool { + self == Self::Idle && !bus_busy + } + + fn after_stop(self) -> Self { + if self == Self::Stopping { + Self::Idle + } else { + self + } + } + + fn after_driver_completion(self) -> Self { + if self == Self::Active { + Self::Idle + } else { + self + } + } +} + +/// Owns I²C1 while one interrupt-driven transaction is active. +pub struct I2c1 { + peripheral: stm32f722::I2C1, + buffer: [u8; MAX_BYTES], + length: usize, + index: usize, + direction: Direction, + state: TransferState, +} + +impl I2c1 { + /// Configures I²C1 using board-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. + peripheral + .timingr() + .write(|w| unsafe { w.bits(config.timing) }); + // SAFETY: these are only documented CR1 interrupt-enable and PE bits. + peripheral + .cr1() + .write(|w| unsafe { w.bits(CR1_INTERRUPTS) }); + Self { + peripheral, + buffer: [0; MAX_BYTES], + length: 0, + index: 0, + direction: Direction::Write, + state: TransferState::Idle, + } + } + + /// Advances the event IRQ; returns a terminal result only after STOPF. + pub fn acknowledge_event_interrupt(&mut self) -> I2cCompletion { + let flags = self.peripheral.isr().read().bits(); + if self.state != TransferState::Active { + self.clear_inactive_event_flags(flags); + return I2cCompletion::NotPending; + } + if flags & ISR_ERRORS != 0 { + self.abort(); + self.acknowledge_stop_if_present(flags); + return I2cCompletion::TransferFault; + } + if flags & ISR_NACKF != 0 { + self.abort(); + self.acknowledge_stop_if_present(flags); + return I2cCompletion::TransferFault; + } + // With AUTOEND a final RXNE and STOPF may be visible in one ISR + // entry. Drain RXDR before retiring the transfer: otherwise RXNE + // remains asserted after software advances its state, retriggering + // I2C1_EV indefinitely at priority 11. + if matches!(self.direction, Direction::Write) + && flags & ISR_TXIS != 0 + && self.index < self.length + { + // SAFETY: the value is an eight-bit TXDR payload. + self.peripheral + .txdr() + .write(|w| unsafe { w.bits(u32::from(self.buffer[self.index])) }); + self.index += 1; + } + let next_read_index = + read_index_after_event(self.direction, flags, self.index, self.length); + if next_read_index != self.index { + self.buffer[self.index] = self.peripheral.rxdr().read().bits() as u8; + self.index = next_read_index; + } + if flags & ISR_STOPF != 0 { + self.acknowledge_stop(); + return completion_after_stop(self.index, self.length); + } + I2cCompletion::NotPending + } + + pub fn acknowledge_error_interrupt(&mut self) -> I2cCompletion { + let flags = self.peripheral.isr().read().bits(); + if flags & ISR_ERRORS == 0 { + return I2cCompletion::NotPending; + } + if self.state == TransferState::Active { + self.abort(); + self.acknowledge_stop_if_present(flags); + I2cCompletion::TransferFault + } else { + // A STOP may have already retired the software transaction before + // the error IRQ is serviced. Its flags still must be acknowledged + // or this high-priority IRQ will continuously retrigger. + self.clear_inactive_event_flags(flags); + I2cCompletion::NotPending + } + } + + fn start(&mut self, address: u8, length: usize, direction: Direction) -> Result<(), I2c1Error> { + if length == 0 || length > MAX_BYTES { + return Err(I2c1Error::InvalidLength); + } + if address > 0x7f { + return Err(I2c1Error::InvalidAddress); + } + if !self.state.accepts_start(self.bus_is_busy()) { + return Err(I2c1Error::Busy); + } + // SAFETY: ICR_TERMINAL contains only documented write-one-to-clear flags. + self.peripheral + .icr() + .write(|w| unsafe { w.bits(ICR_TERMINAL) }); + self.length = length; + self.index = 0; + self.direction = direction; + self.state = TransferState::Active; + let direction_bit = if matches!(direction, Direction::Read) { + 1 << 10 + } else { + 0 + }; + let cr2 = (u32::from(address) << 1) + | direction_bit + | ((length as u32) << 16) + | (1 << 25) + | (1 << 13); + // SAFETY: `cr2` contains a validated 7-bit address and documented transfer fields. + self.peripheral.cr2().write(|w| unsafe { w.bits(cr2) }); + Ok(()) + } + + fn abort(&mut self) { + // Keep software ownership closed until STOPF is observed. A requested + // STOP may be delayed indefinitely by a held bus, so this state keeps + // a new START from being issued before the terminal STOPF arrives. + self.state = TransferState::Stopping; + // SAFETY: this only requests STOP while preserving the current CR2 fields. + self.peripheral + .cr2() + .modify(|read, write| unsafe { write.bits(read.bits() | CR2_STOP) }); + // SAFETY: ICR_FAULTS contains only documented fault write-one-to-clear flags. + self.peripheral + .icr() + .write(|w| unsafe { w.bits(ICR_FAULTS) }); + } + + fn bus_is_busy(&self) -> bool { + self.peripheral.isr().read().bits() & (1 << 15) != 0 + } + + fn acknowledge_stop(&mut self) { + // SAFETY: ICR_STOPCF is the documented STOP write-one-to-clear bit. + self.peripheral + .icr() + .write(|w| unsafe { w.bits(ICR_STOPCF) }); + self.state = self.state.after_stop(); + } + + fn acknowledge_stop_if_present(&mut self, flags: u32) { + if flags & ISR_STOPF != 0 { + self.acknowledge_stop(); + } + } + + fn clear_inactive_event_flags(&mut self, flags: u32) { + // An abort may request STOP after the driver has already released the + // transaction. Drain that terminal IRQ instead of leaving I2C1_EV + // pending forever. RXNE is normally impossible here, but draining it + // keeps a malformed terminal transfer from becoming an IRQ storm. + if flags & ISR_RXNE != 0 { + let _ = self.peripheral.rxdr().read().bits(); + } + if flags & (ISR_NACKF | ISR_ERRORS) != 0 { + // SAFETY: ICR_FAULTS contains only documented fault write-one-to-clear flags. + self.peripheral + .icr() + .write(|w| unsafe { w.bits(ICR_FAULTS) }); + } + self.acknowledge_stop_if_present(flags); + if flags & ISR_TXIS != 0 { + self.abort(); + } + } +} + +impl I2cBus for I2c1 { + type StartError = I2c1Error; + fn start_write(&mut self, address: u8, write: &[u8]) -> Result<(), Self::StartError> { + if write.is_empty() || write.len() > MAX_BYTES { + return Err(I2c1Error::InvalidLength); + } + if !self.state.accepts_start(self.bus_is_busy()) { + return Err(I2c1Error::Busy); + } + // Populate controller-owned storage before `start` makes the + // transaction visible to the higher-priority I²C event IRQ. + self.buffer[..write.len()].copy_from_slice(write); + self.start(address, write.len(), Direction::Write)?; + Ok(()) + } + fn start_read(&mut self, address: u8, length: usize) -> Result<(), Self::StartError> { + self.start(address, length, Direction::Read) + } + fn complete_transaction(&mut self, completion: I2cCompletion, read: Option<&mut [u8]>) { + if completion == I2cCompletion::Complete + && matches!(self.direction, Direction::Read) + && let Some(read) = read + { + debug_assert_eq!(read.len(), self.length); + read.copy_from_slice(&self.buffer[..self.length]); + } + self.state = self.state.after_driver_completion(); + self.length = 0; + self.index = 0; + } + + fn cancel_transaction(&mut self) { + if self.state == TransferState::Active { + self.abort(); + } + self.length = 0; + self.index = 0; + } +} + +#[cfg(test)] +mod tests { + use super::{ + Direction, I2cCompletion, ICR_ERRORS, ICR_FAULTS, ICR_NACKCF, ICR_STOPCF, ISR_RXNE, + ISR_STOPF, TransferState, completion_after_stop, read_index_after_event, + }; + + #[test] + fn rejects_starts_while_a_transaction_or_recovery_is_pending() { + assert!(TransferState::Idle.accepts_start(false)); + assert!(!TransferState::Idle.accepts_start(true)); + assert!(!TransferState::Active.accepts_start(false)); + assert!(!TransferState::Stopping.accepts_start(false)); + } + + #[test] + fn abort_stays_closed_until_stop_and_normal_completion_releases_ownership() { + assert_eq!(TransferState::Stopping.after_stop(), TransferState::Idle); + assert_eq!(TransferState::Active.after_stop(), TransferState::Active); + assert_eq!( + TransferState::Active.after_driver_completion(), + TransferState::Idle + ); + assert_eq!( + TransferState::Stopping.after_driver_completion(), + TransferState::Stopping + ); + } + + #[test] + fn fault_cleanup_does_not_acknowledge_stop_before_stop_is_observed() { + assert_eq!(ICR_FAULTS, ICR_NACKCF | ICR_ERRORS); + assert_eq!(ICR_FAULTS & ICR_STOPCF, 0); + } + + #[test] + fn rxne_and_stopf_complete_a_final_read_in_one_event() { + let index = read_index_after_event(Direction::Read, ISR_RXNE | ISR_STOPF, 1, 2); + + assert_eq!(index, 2); + assert_eq!(completion_after_stop(index, 2), I2cCompletion::Complete); + } +} diff --git a/libs/kaptan-stm32f7/src/stm32f722/mod.rs b/libs/kaptan-stm32f7/src/stm32f722/mod.rs index af5f000..55036d3 100644 --- a/libs/kaptan-stm32f7/src/stm32f722/mod.rs +++ b/libs/kaptan-stm32f7/src/stm32f722/mod.rs @@ -6,6 +6,7 @@ mod clock; mod dma_buffer; mod gpio; +mod i2c1; mod spi1; mod tim2; mod time; @@ -15,9 +16,10 @@ 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_spi1_pins_pa5_pa6_pa7, - configure_usart1_rx_pin_pa10, configure_usart1_tx_pin_pa9, + 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}; diff --git a/libs/kaptan-stm32f7/src/stm32f722/time.rs b/libs/kaptan-stm32f7/src/stm32f722/time.rs index d48e74e..6fed23f 100644 --- a/libs/kaptan-stm32f7/src/stm32f722/time.rs +++ b/libs/kaptan-stm32f7/src/stm32f722/time.rs @@ -51,6 +51,16 @@ impl DwtMicrosecondClock { self.advance_to(DWT::cycle_count()) } + /// Creates another independent reader for the same already-running DWT + /// counter. Each reader must be serviced often enough to observe wraps. + pub const fn fork(&self) -> Self { + Self { + cycles_per_microsecond: self.cycles_per_microsecond, + previous_cycles: self.previous_cycles, + elapsed_cycles: self.elapsed_cycles, + } + } + fn advance_to(&mut self, current_cycles: u32) -> Timestamp { let elapsed = current_cycles.wrapping_sub(self.previous_cycles); self.previous_cycles = current_cycles;