platforms/stm32f7: add reusable F722 firmware

This commit is contained in:
Onur Özkan committed 2026-08-01 20:54:21 +03:00
1 parent 2ae66c49a8
commit ac54c8308d
32 files changed
+4006 -248

No files matched your search

+1 -1
View File
@@ -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],
+1 -1
View File
@@ -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.
-22
View File
@@ -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 }
-14
View File
@@ -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;
@@ -1,89 +0,0 @@
//! Deterministic STM32F722 PLL clock-source selection.
use stm32f7::stm32f722;
const CLOCK_SWITCH_POLL_LIMIT: u16 = 1_024;
/// Failure to establish the 216 MHz PLL clock profile.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ClockProfileError {
HsiNotReady,
SystemClockNotHsi,
PllNotStopped,
PllNotReady,
OverDriveNotReady,
OverDriveSwitchNotReady,
SystemClockNotPll,
}
/// Selects the board'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
/// is introduced.
pub fn select_pll216_profile(
rcc: &stm32f722::RCC,
pwr: &stm32f722::PWR,
flash: &stm32f722::FLASH,
) -> Result<(), ClockProfileError> {
rcc.cr().modify(|_, write| write.hsion().on());
if !wait_until(|| rcc.cr().read().hsirdy().is_ready()) {
return Err(ClockProfileError::HsiNotReady);
}
// Return to HSI before changing a PLL left configured by an earlier boot stage.
rcc.cfgr().modify(|_, write| write.sw().hsi());
if !wait_until(|| rcc.cfgr().read().sws().is_hsi()) {
return Err(ClockProfileError::SystemClockNotHsi);
}
rcc.cr().modify(|_, write| write.pllon().off());
if !wait_until(|| !rcc.cr().read().pllrdy().is_ready()) {
return Err(ClockProfileError::PllNotStopped);
}
// Scale 1, over-drive and seven flash wait states are required at 216 MHz.
rcc.apb1enr().modify(|_, write| write.pwren().set_bit());
pwr.cr1().modify(|read, write| unsafe {
write.bits((read.bits() & !(0b11 << 14)) | (0b11 << 14) | (1 << 16))
});
if !wait_until(|| pwr.csr1().read().odrdy().bit_is_set()) {
return Err(ClockProfileError::OverDriveNotReady);
}
pwr.cr1().modify(|_, write| write.odswen().set_bit());
if !wait_until(|| pwr.csr1().read().odswrdy().bit_is_set()) {
return Err(ClockProfileError::OverDriveSwitchNotReady);
}
flash.acr().modify(|read, write| unsafe {
write.bits((read.bits() & !0x0f) | 7 | (1 << 8) | (1 << 9))
});
// HSI / 16 * 432 / 2 = 216 MHz; Q=9 supplies a 48 MHz domain.
rcc.pllcfgr()
.write(|write| unsafe { write.bits(16 | (432 << 6) | (9 << 24)) });
rcc.cr().modify(|_, write| write.pllon().on());
if !wait_until(|| rcc.cr().read().pllrdy().is_ready()) {
return Err(ClockProfileError::PllNotReady);
}
rcc.cfgr().modify(|read, write| unsafe {
// AHB /1, APB1 /4, APB2 /2, SYSCLK = PLL.
write.bits(
(read.bits() & !((0x0f << 4) | (0x07 << 10) | (0x07 << 13) | 0x03))
| (0b101 << 10)
| (0b100 << 13)
| 0b10,
)
});
if !wait_until(|| rcc.cfgr().read().sws().is_pll()) {
return Err(ClockProfileError::SystemClockNotPll);
}
Ok(())
}
fn wait_until(mut ready: impl FnMut() -> bool) -> bool {
for _ in 0..CLOCK_SWITCH_POLL_LIMIT {
if ready() {
return true;
}
}
false
}
@@ -1,35 +0,0 @@
//! 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<DmaReadableBuffer<'a>, 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) })
}
}
-128
View File
@@ -1,128 +0,0 @@
//! Named STM32F722 GPIO routes used by supported board wiring.
use stm32f7::stm32f722;
/// Configures PA5, PA6, and PA7 for SPI1 alternate function 5.
pub fn configure_spi1_pins_pa5_pa6_pa7(rcc: &stm32f722::RCC, port: &stm32f722::GPIOA) {
rcc.ahb1enr().modify(|_, write| write.gpioaen().set_bit());
port.moder().modify(|_, write| {
write
.moder5()
.alternate()
.moder6()
.alternate()
.moder7()
.alternate()
});
port.afrl().modify(|_, write| {
// SAFETY: AF5 selects SPI1 on PA5, PA6, and PA7 on STM32F722.
unsafe {
write
.afrl5()
.bits(0b0101)
.afrl6()
.bits(0b0101)
.afrl7()
.bits(0b0101)
}
});
port.otyper()
.modify(|_, write| write.ot5().push_pull().ot6().push_pull().ot7().push_pull());
port.ospeedr().modify(|_, write| {
write
.ospeedr5()
.high_speed()
.ospeedr6()
.high_speed()
.ospeedr7()
.high_speed()
});
port.pupdr().modify(|_, write| {
write
.pupdr5()
.floating()
.pupdr6()
.floating()
.pupdr7()
.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());
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
/// 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 PA9 for USART1 transmit alternate function 7.
pub fn configure_usart1_tx_pin_pa9(port: &stm32f722::GPIOA) {
port.moder().modify(|_, write| write.moder9().alternate());
port.afrh().modify(|_, write| {
// SAFETY: AF7 selects USART1 TX on PA9 on STM32F722.
unsafe { write.afrh9().bits(0b0111) }
});
port.otyper().modify(|_, write| write.ot9().push_pull());
port.ospeedr()
.modify(|_, write| write.ospeedr9().high_speed());
port.pupdr().modify(|_, write| write.pupdr9().floating());
}
/// Configures PA10 for USART1 receive alternate function 7.
pub fn configure_usart1_rx_pin_pa10(port: &stm32f722::GPIOA) {
port.moder().modify(|_, write| write.moder10().alternate());
port.afrh().modify(|_, write| {
// SAFETY: AF7 selects USART1 RX on PA10 on STM32F722.
unsafe { write.afrh10().bits(0b0111) }
});
port.pupdr().modify(|_, write| write.pupdr10().floating());
}
/// Active-low status LED connected to PD2.
pub struct ActiveLowLedPd2 {
port: stm32f722::GPIOD,
is_on: bool,
}
impl ActiveLowLedPd2 {
/// Configures PD2 as an initially-off active-low output.
pub fn new(rcc: &stm32f722::RCC, port: stm32f722::GPIOD) -> Self {
rcc.ahb1enr().modify(|_, write| write.gpioden().set_bit());
port.moder().modify(|_, write| write.moder2().output());
port.otyper().modify(|_, write| write.ot2().push_pull());
port.ospeedr()
.modify(|_, write| write.ospeedr2().low_speed());
port.pupdr().modify(|_, write| write.pupdr2().floating());
port.bsrr().write(|write| write.bs2().set_bit());
Self { port, is_on: false }
}
/// Toggles the LED state.
pub fn toggle(&mut self) {
self.is_on = !self.is_on;
if self.is_on {
self.port.bsrr().write(|write| write.br2().set_bit());
} else {
self.port.bsrr().write(|write| write.bs2().set_bit());
}
}
}
-361
View File
@@ -1,361 +0,0 @@
//! 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);
}
}
-27
View File
@@ -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};
-335
View File
@@ -1,335 +0,0 @@
//! 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/128 and reserves DMA2.
///
/// This keeps the MPU6000 route below 1 MHz with the board's 108 MHz
/// APB2 clock while retaining substantial margin for board-level wiring.
pub fn new(
rcc: &stm32f722::RCC,
peripheral: stm32f722::SPI1,
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()
.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<(), 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);
}
// Arm the complete DMA receive path before SPI starts generating
// clocks. This avoids an RX FIFO overrun between transactions.
self.peripheral
.cr1()
.modify(|_, write| write.spe().clear_bit());
self.clear_spi_overrun();
self.clear_rx_flags();
self.clear_tx_flags();
self.configure_rx(receive);
self.configure_tx(transmit);
self.peripheral.cr2().modify(|_, write| {
let write = write.rxdmaen().set_bit().txdmaen().set_bit();
if transmit.len() & 1 == 0 {
write.ldma_rx().even().ldma_tx().even()
} else {
// With 8-bit frames, F7 SPI needs this for the final
// byte of an odd-length DMA transaction. MPU samples are
// one command byte plus fourteen data bytes.
write.ldma_rx().odd().ldma_tx().odd()
}
});
self.rx_stream()
.cr()
.modify(|_, write| write.en().enabled());
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
/// SPI1's own IRQ. The reference manual clears OVR by reading DR then SR.
pub fn acknowledge_error_interrupt() -> bool {
// SAFETY: SPI1 is initialized once by `Spi1::new`; this ISR only
// clears its peripheral OVR condition.
let peripheral = unsafe { &*stm32f722::SPI1::ptr() };
if !peripheral.sr().read().ovr().is_overrun() {
return false;
}
let _ = peripheral.dr().read().bits();
let _ = peripheral.sr().read().bits();
true
}
/// Acknowledges the RX terminal IRQ. Invoke only from DMA2 Stream0.
pub fn acknowledge_rx_interrupt() -> SpiCompletion {
// SAFETY: DMA2 is initialized once by `Spi1::new`; this ISR touches
// 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()
});
}
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();
}
}
}
-53
View File
@@ -1,53 +0,0 @@
//! STM32F722 TIM2 periodic interrupt configuration.
use stm32f7::stm32f722;
/// Precomputed TIM2 periodic-update configuration.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct Tim2PeriodicConfig {
/// Timer prescaler register value.
pub prescaler: u16,
/// Timer auto-reload register value.
pub auto_reload: u32,
}
/// TIM2 configured to issue periodic update interrupts.
pub struct Tim2Periodic {
peripheral: stm32f722::TIM2,
}
impl Tim2Periodic {
/// Enables TIM2 with the supplied board-derived period configuration.
pub fn new(
rcc: &stm32f722::RCC,
peripheral: stm32f722::TIM2,
config: Tim2PeriodicConfig,
) -> Self {
rcc.apb1rstr().modify(|_, write| write.tim2rst().set_bit());
rcc.apb1rstr()
.modify(|_, write| write.tim2rst().clear_bit());
rcc.apb1enr().modify(|_, write| write.tim2en().set_bit());
peripheral.cr1().modify(|_, write| write.cen().clear_bit());
peripheral.psc().write(|write| {
// SAFETY: `prescaler` is exactly the 16-bit PSC register width.
unsafe { write.psc().bits(config.prescaler) }
});
peripheral.arr().write(|write| {
// SAFETY: TIM2 ARR is a 32-bit register and accepts `auto_reload`.
unsafe { write.arr().bits(config.auto_reload) }
});
peripheral.egr().write(|write| write.ug().set_bit());
peripheral.sr().modify(|_, write| write.uif().clear_bit());
peripheral.dier().modify(|_, write| write.uie().set_bit());
peripheral.cr1().modify(|_, write| write.cen().set_bit());
Self { peripheral }
}
/// Acknowledges a TIM2 update interrupt.
pub fn acknowledge_update(&mut self) {
self.peripheral
.sr()
.modify(|_, write| write.uif().clear_bit());
}
}
-104
View File
@@ -1,104 +0,0 @@
//! DWT-backed monotonic microsecond clock for STM32F722.
use cortex_m::peripheral::{DCB, DWT};
use kaptan_time::{MonotonicClock, Timestamp};
/// A DWT cycle counter extended into monotonic microseconds.
///
/// `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.
pub struct DwtMicrosecondClock {
cycles_per_microsecond: u32,
previous_cycles: u32,
elapsed_cycles: u64,
}
/// Invalid fixed clock configuration for the DWT timestamp source.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DwtClockStartError {
/// A zero-megahertz core clock cannot produce microsecond timestamps.
ZeroFrequency,
}
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.
pub fn start<const MEGAHERTZ: u32>(
dcb: &mut DCB,
dwt: &mut DWT,
) -> Result<Self, DwtClockStartError> {
if MEGAHERTZ == 0 {
return Err(DwtClockStartError::ZeroFrequency);
}
dcb.enable_trace();
DWT::unlock();
dwt.set_cycle_count(0);
dwt.enable_cycle_counter();
Ok(Self {
cycles_per_microsecond: MEGAHERTZ,
previous_cycles: 0,
elapsed_cycles: 0,
})
}
/// Captures the current monotonic microsecond timestamp.
pub fn now(&mut self) -> Timestamp {
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;
self.elapsed_cycles = self.elapsed_cycles.saturating_add(u64::from(elapsed));
Timestamp::from_microseconds(self.elapsed_cycles / u64::from(self.cycles_per_microsecond))
}
}
impl MonotonicClock for DwtMicrosecondClock {
fn now(&mut self) -> Timestamp {
Self::now(self)
}
}
#[cfg(test)]
mod tests {
use super::DwtMicrosecondClock;
fn clock(previous_cycles: u32) -> DwtMicrosecondClock {
DwtMicrosecondClock {
cycles_per_microsecond: 16,
previous_cycles,
elapsed_cycles: 0,
}
}
#[test]
fn converts_cycles_to_microseconds() {
let mut clock = clock(0);
assert_eq!(clock.advance_to(160).as_microseconds(), 10);
}
#[test]
fn extends_a_single_cycle_counter_wrap() {
let mut clock = clock(u32::MAX - 15);
assert_eq!(clock.advance_to(16).as_microseconds(), 2);
}
}
-148
View File
@@ -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<Self, BaudRateError> {
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<u16, BaudRateError> {
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));
}
}
@@ -1,218 +0,0 @@
//! 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);
}
}