diff --git a/libs/kaptan-stm32f7/src/stm32f722/spi1.rs b/libs/kaptan-stm32f7/src/stm32f722/spi1.rs new file mode 100644 index 0000000..deb7718 --- /dev/null +++ b/libs/kaptan-stm32f7/src/stm32f722/spi1.rs @@ -0,0 +1,289 @@ +//! STM32F722 SPI1 full-duplex DMA controller. +//! +//! SPI1 uses DMA2 Stream0/Channel3 for receive and DMA2 Stream3/Channel3 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 SPI1 DMA transaction. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Spi1Error { + /// The transaction is malformed or exceeds the DMA counter range. + InvalidLength, + /// A previous DMA transaction still owns SPI1. + Busy, +} + +/// Owns SPI1 and its fixed full-duplex DMA2 request route. +pub struct Spi1 { + peripheral: stm32f722::SPI1, + controller: stm32f722::DMA2, + active: bool, +} + +impl Spi1 { + /// Configures SPI1 mode 0 master operation at APB2/64 and reserves DMA2. + pub fn new( + rcc: &stm32f722::RCC, + peripheral: stm32f722::SPI1, + controller: stm32f722::DMA2, + ) -> Self { + rcc.apb2rstr().modify(|_, write| write.spi1rst().set_bit()); + rcc.apb2rstr() + .modify(|_, write| write.spi1rst().clear_bit()); + rcc.apb2enr().modify(|_, write| write.spi1en().set_bit()); + rcc.ahb1enr().modify(|_, write| write.dma2en().set_bit()); + + peripheral.cr1().write(|write| { + write + .mstr() + .master() + .br() + .div64() + .ssm() + .set_bit() + .ssi() + .set_bit() + }); + peripheral + .cr2() + .write(|write| write.ds().eight_bit().frxth().set_bit()); + peripheral.cr1().modify(|_, write| write.spe().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<(), Spi1Error> { + if transmit.is_empty() + || transmit.len() != receive.len() + || transmit.len() > MAX_TRANSFER_LENGTH + { + return Err(Spi1Error::InvalidLength); + } + if self.active + || self.rx_stream().cr().read().en().is_enabled() + || self.tx_stream().cr().read().en().is_enabled() + { + return Err(Spi1Error::Busy); + } + + self.clear_rx_flags(); + self.clear_tx_flags(); + self.configure_rx(receive); + self.configure_tx(transmit); + self.rx_stream() + .cr() + .modify(|_, write| write.en().enabled()); + self.tx_stream() + .cr() + .modify(|_, write| write.en().enabled()); + self.peripheral + .cr2() + .modify(|_, write| write.rxdmaen().set_bit().txdmaen().set_bit()); + self.active = true; + Ok(()) + } + + /// Releases controller-side ownership after a DMA terminal event. + pub fn complete_transfer(&mut self, _: SpiCompletion) { + self.peripheral + .cr2() + .modify(|_, write| write.rxdmaen().clear_bit().txdmaen().clear_bit()); + self.active = false; + } + + /// Acknowledges the RX terminal IRQ. Invoke only from DMA2 Stream0. + pub fn acknowledge_rx_interrupt() -> SpiCompletion { + // SAFETY: DMA2 is initialized once by `Spi1::new`; this ISR touches + // only Stream0's status and clear registers, never worker state. + let controller = unsafe { &*stm32f722::DMA2::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: DMA2 Stream3 is initialized once by `Spi1::new`; this ISR + // touches only Stream3's status and clear registers. + let controller = unsafe { &*stm32f722::DMA2::ptr() }; + let status = controller.lisr().read(); + let fault = + status.teif3().is_error() || status.dmeif3().is_error() || status.feif3().is_error(); + controller.lifcr().write(|write| { + write + .cfeif3() + .clear() + .cdmeif3() + .clear() + .cteif3() + .clear() + .chtif3() + .clear() + .ctcif3() + .clear() + }); + if fault { + SpiCompletion::TransferFault + } else { + SpiCompletion::NotPending + } + } + + fn configure_rx(&self, receive: &mut [u8]) { + self.rx_stream().cr().write(|write| { + // SAFETY: Channel 3 is SPI1_RX on DMA2 Stream0; checked slice + // length fits NDTR, and all values map directly to documented bits. + unsafe { + write + .chsel() + .bits(3) + .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 SPI1 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 3 is SPI1_TX on DMA2 Stream3; checked slice + // length fits NDTR, and all values map directly to documented bits. + unsafe { + write + .chsel() + .bits(3) + .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 SPI1 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::dma2::ST { + self.controller.st(0) + } + + fn tx_stream(&self) -> &stm32f722::dma2::ST { + self.controller.st(3) + } + + 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.lifcr().write(|write| { + write + .cfeif3() + .clear() + .cdmeif3() + .clear() + .cteif3() + .clear() + .chtif3() + .clear() + .ctcif3() + .clear() + }); + } +}