From e91765060299ee452d361445116e80563e054567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Onur=20=C3=96zkan?= Date: Sun, 19 Jul 2026 22:40:04 +0300 Subject: [PATCH] libs/kaptan-stm32f7: add USART1 DMA transmit route --- .../src/stm32f722/dma_buffer.rs | 35 +++ .../src/stm32f722/usart1_tx_dma2_stream7.rs | 218 ++++++++++++++++++ 2 files changed, 253 insertions(+) create mode 100644 libs/kaptan-stm32f7/src/stm32f722/dma_buffer.rs create mode 100644 libs/kaptan-stm32f7/src/stm32f722/usart1_tx_dma2_stream7.rs diff --git a/libs/kaptan-stm32f7/src/stm32f722/dma_buffer.rs b/libs/kaptan-stm32f7/src/stm32f722/dma_buffer.rs new file mode 100644 index 0000000..f6743a4 --- /dev/null +++ b/libs/kaptan-stm32f7/src/stm32f722/dma_buffer.rs @@ -0,0 +1,35 @@ +//! D-cache-disabled DMA memory policy for STM32F722 boards. + +use kaptan_dma::{DmaReadableBuffer, DmaTxBufferFactory}; +/// +/// DMA transmit memory policy for a board with D-cache disabled. +/// +/// The board's linker script 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. +pub struct DcacheDisabledDmaMemory { + _private: (), +} + +impl DcacheDisabledDmaMemory { + /// Declares that the selected board disabled D-cache before DMA setup. + /// + /// # Safety + /// + /// - D-cache must remain disabled for every CPU access to DMA-owned frames. + /// - The linker policy must locate those frames in DMA-visible SRAM. + pub const unsafe fn new() -> Self { + Self { _private: () } + } +} + +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 + // 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/usart1_tx_dma2_stream7.rs b/libs/kaptan-stm32f7/src/stm32f722/usart1_tx_dma2_stream7.rs new file mode 100644 index 0000000..15a3ede --- /dev/null +++ b/libs/kaptan-stm32f7/src/stm32f722/usart1_tx_dma2_stream7.rs @@ -0,0 +1,218 @@ +//! 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 +//! cannot be selected at construction. + +use kaptan_dma::{DmaReadableBuffer, DmaTxEngine, TxCompletion}; +use stm32f7::stm32f722; + +use super::Usart1Tx; + +/// Owns the fixed STM32F722 USART1 TX DMA route. +pub struct Usart1TxDma { + usart: Usart1Tx, +} + +/// Failure to start one DMA frame transfer. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum StartError { + /// DMA cannot transfer an empty buffer or one exceeding the NDTR limit. + InvalidBufferLength, + /// DMA2 stream 7 still owns an earlier transfer. + Busy, +} + +impl Usart1TxDma { + /// Enables DMA2 and connects USART1 TX to its fixed request route. + pub fn new(rcc: &stm32f722::RCC, usart: Usart1Tx) -> Self { + rcc.ahb1enr().modify(|_, write| write.dma2en().set_bit()); + usart.enable_dma_transmit(); + + Self { usart } + } + + /// Starts one memory-to-USART DMA transfer. + /// + /// # Ownership + /// + /// 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 + /// that interrupt, and defer lifecycle handling to its single owner. + pub fn start(&mut self, buffer: DmaReadableBuffer<'_>) -> Result<(), StartError> { + let bytes = buffer.bytes(); + if bytes.is_empty() || bytes.len() > u16::MAX as usize { + return Err(StartError::InvalidBufferLength); + } + if self.is_active() { + return Err(StartError::Busy); + } + + self.clear_interrupt_flags(); + self.configure_stream(bytes); + self.stream().cr().modify(|_, write| write.en().enabled()); + + Ok(()) + } + + /// Acknowledges a DMA2 Stream7 terminal IRQ without accessing engine state. + pub fn acknowledge_interrupt() -> TxCompletion { + // SAFETY: this handler accesses only the fixed DMA2 Stream7 route. + let controller = unsafe { &*stm32f722::DMA2::ptr() }; + let status = controller.hisr().read(); + let completion = DmaTerminalStatus { + transfer_complete: status.tcif7().is_complete(), + transfer_error: status.teif7().is_error(), + direct_mode_error: status.dmeif7().is_error(), + fifo_error: status.feif7().is_error(), + } + .classify(); + if completion == TxCompletion::NotPending { + return completion; + } + + // A normal-mode STM32 DMA stream clears EN at terminal completion. + // Request disable without polling: a later start observes EN and + // refuses ownership rather than blocking an interrupt handler. + controller + .st(7) + .cr() + .modify(|_, write| write.en().disabled()); + controller.hifcr().write(|write| { + write + .ctcif7() + .clear() + .chtif7() + .clear() + .cteif7() + .clear() + .cdmeif7() + .clear() + .cfeif7() + .clear() + }); + completion + } + + /// Returns whether DMA2 stream 7 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 + // USART1 TX mapping for DMA2 stream 7 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 USART1 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(7) + } + + fn clear_interrupt_flags(&self) { + self.controller().hifcr().write(|write| { + write + .ctcif7() + .clear() + .chtif7() + .clear() + .cteif7() + .clear() + .cdmeif7() + .clear() + .cfeif7() + .clear() + }); + } + + fn controller(&self) -> &stm32f722::dma2::RegisterBlock { + // SAFETY: SPI1 owns DMA2 initialization; USART1 TX is permanently + // assigned to the disjoint Stream7 route and accesses no other stream. + unsafe { &*stm32f722::DMA2::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 Usart1TxDma { + type StartError = StartError; + + 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); + } +}