From e83377072247e417f14fb8a794fb1169e54ad5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?D=C3=A1niel=20Buga?= Date: Mon, 13 Apr 2026 13:04:14 +0200 Subject: [PATCH] Add min_async_transfer_size --- esp-hal/CHANGELOG.md | 1 + esp-hal/src/spi/master/dma.rs | 75 +++++++++++++++++++++++++++++++++++ esp-hal/src/spi/master/mod.rs | 54 +++++++++++++++++++++++++ 3 files changed, 130 insertions(+) diff --git a/esp-hal/CHANGELOG.md b/esp-hal/CHANGELOG.md index 20997c405f2..068d309d93b 100644 --- a/esp-hal/CHANGELOG.md +++ b/esp-hal/CHANGELOG.md @@ -101,6 +101,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - C61: Add SHA and ECC support (#5354) - A `PsramMode` option has been introduced for ESP32-S3. The default mode is `Auto` which will try to detect if PSRAM works via Octal or Quad SPI and configure it accordingly. (#5334) - Add I2S loopback logic to the peripheral driver. (#5349) +- SPI master: added `min_async_transfer_size` config option to force small transfers to use blocking/CPU driven mode. (#5350) ### Changed diff --git a/esp-hal/src/spi/master/dma.rs b/esp-hal/src/spi/master/dma.rs index a6f70041ed2..e0aacbf9959 100644 --- a/esp-hal/src/spi/master/dma.rs +++ b/esp-hal/src/spi/master/dma.rs @@ -414,6 +414,11 @@ impl<'d> SpiDma<'d, Async> { self.wait_for_idle_async().await; self.driver().setup_full_duplex()?; + if self.use_blocking_transfer(words.len()) { + self.dma_driver().disable_dma(); + return self.driver().read(words); + } + let mut descriptors = [DmaDescriptor::EMPTY; LINK_DESCRIPTOR_COUNT]; let mut maybe_copy_buffer = match DmaOperationKind::for_read(words) { DmaOperationKind::Copied => { @@ -454,6 +459,12 @@ impl<'d> SpiDma<'d, Async> { self.wait_for_idle_async().await; self.driver().setup_full_duplex()?; + if self.use_blocking_transfer(words.len()) { + self.dma_driver().disable_dma(); + self.driver().write(words)?; + return self.driver().flush(); + } + let mut descriptors = [DmaDescriptor::EMPTY; LINK_DESCRIPTOR_COUNT]; let mut maybe_copy_buffer = match DmaOperationKind::for_write(words) { DmaOperationKind::Copied => { @@ -489,6 +500,18 @@ impl<'d> SpiDma<'d, Async> { self.wait_for_idle_async().await; self.driver().setup_full_duplex()?; + if self.use_blocking_transfer(read.len().max(write.len())) { + self.dma_driver().disable_dma(); + if read.is_empty() { + self.driver().write(write)?; + return self.driver().flush(); + } else if write.is_empty() { + return self.driver().read(read); + } else { + return self.driver().transfer(read, write); + } + } + let mut rx_descriptors = [DmaDescriptor::EMPTY; LINK_DESCRIPTOR_COUNT]; let mut maybe_copy_rx_buffer = match DmaOperationKind::for_read(read) { DmaOperationKind::Copied => { @@ -557,6 +580,11 @@ impl<'d> SpiDma<'d, Async> { self.wait_for_idle_async().await; self.driver().setup_full_duplex()?; + if self.use_blocking_transfer(words.len()) { + self.dma_driver().disable_dma(); + return self.driver().transfer_in_place(words); + } + let mut rx_descriptors = [DmaDescriptor::EMPTY; LINK_DESCRIPTOR_COUNT]; let mut tx_descriptors = [DmaDescriptor::EMPTY; LINK_DESCRIPTOR_COUNT]; let (mut maybe_copy_rx_buffer, mut maybe_copy_tx_buffer) = @@ -768,6 +796,15 @@ impl<'d, Dm> SpiDma<'d, Dm> where Dm: DriverMode, { + fn use_blocking_transfer(&self, transfer_size: usize) -> bool { + let threshold = self + .spi + .state() + .min_async_transfer_size + .load(Ordering::Relaxed); + threshold > 0 && transfer_size < threshold + } + fn spi(&self) -> &SpiWrapper<'_> { &self.spi } @@ -1222,6 +1259,11 @@ where self.wait_for_idle(); self.driver().setup_full_duplex()?; + if self.use_blocking_transfer(words.len()) { + self.dma_driver().disable_dma(); + return self.driver().read(words); + } + let mut descriptors = [DmaDescriptor::EMPTY; LINK_DESCRIPTOR_COUNT]; let mut maybe_copy_buffer = match DmaOperationKind::for_read(words) { DmaOperationKind::Copied => { @@ -1257,6 +1299,12 @@ where self.wait_for_idle(); self.driver().setup_full_duplex()?; + if self.use_blocking_transfer(words.len()) { + self.dma_driver().disable_dma(); + self.driver().write(words)?; + return self.driver().flush(); + } + let mut descriptors = [DmaDescriptor::EMPTY; LINK_DESCRIPTOR_COUNT]; let mut maybe_copy_buffer = match DmaOperationKind::for_write(words) { DmaOperationKind::Copied => { @@ -1286,6 +1334,18 @@ where self.wait_for_idle(); self.driver().setup_full_duplex()?; + if self.use_blocking_transfer(read.len().max(write.len())) { + self.dma_driver().disable_dma(); + if read.is_empty() { + self.driver().write(write)?; + return self.driver().flush(); + } else if write.is_empty() { + return self.driver().read(read); + } else { + return self.driver().transfer(read, write); + } + } + let mut rx_descriptors = [DmaDescriptor::EMPTY; LINK_DESCRIPTOR_COUNT]; let mut maybe_copy_rx_buffer = match DmaOperationKind::for_read(read) { DmaOperationKind::Copied => { @@ -1348,6 +1408,11 @@ where self.wait_for_idle(); self.driver().setup_full_duplex()?; + if self.use_blocking_transfer(words.len()) { + self.dma_driver().disable_dma(); + return self.driver().transfer_in_place(words); + } + let mut rx_descriptors = [DmaDescriptor::EMPTY; LINK_DESCRIPTOR_COUNT]; let mut tx_descriptors = [DmaDescriptor::EMPTY; LINK_DESCRIPTOR_COUNT]; let (mut maybe_copy_rx_buffer, mut maybe_copy_tx_buffer) = @@ -1563,6 +1628,16 @@ impl DmaDriver { self.driver.update(); } + fn disable_dma(&self) { + #[cfg(dma_kind = "gdma")] + self.regs().dma_conf().modify(|_, w| { + w.dma_tx_ena().clear_bit(); + w.dma_rx_ena().clear_bit() + }); + + // PDMA: nothing to do + } + fn regs(&self) -> &RegisterBlock { self.driver.regs() } diff --git a/esp-hal/src/spi/master/mod.rs b/esp-hal/src/spi/master/mod.rs index 7e68365f074..967527cfcf8 100644 --- a/esp-hal/src/spi/master/mod.rs +++ b/esp-hal/src/spi/master/mod.rs @@ -42,6 +42,7 @@ use core::{ marker::PhantomData, mem::MaybeUninit, pin::Pin, + sync::atomic::{AtomicUsize, Ordering}, task::{Context, Poll}, }; @@ -472,6 +473,20 @@ pub struct Config { /// Bit order of the written data. write_bit_order: BitOrder, + + /// Minimum transfer size in bytes below which CPU-driven (blocking) I/O + /// is used instead of async or DMA transfers. + /// + /// This can reduce overhead for small transfers where DMA setup or + /// async context-switch cost exceeds the benefit. For + /// [`SpiDma`][crate::spi::master::dma::SpiDma], the threshold applies in + /// both blocking and async DMA modes: when met, DMA is disabled and the + /// transfer is performed by the CPU. + /// + /// A value of `0` (the default) disables the threshold — all transfers use + /// the driver's default method. + #[builder_lite(unstable)] + min_async_transfer_size: usize, } impl Default for Config { @@ -483,6 +498,7 @@ impl Default for Config { mode: Mode::_0, read_bit_order: BitOrder::MsbFirst, write_bit_order: BitOrder::MsbFirst, + min_async_transfer_size: 0, }; this.reg = this.recalculate(); @@ -921,6 +937,10 @@ impl<'d> Spi<'d, Async> { self.driver().flush_async().await; self.driver().setup_full_duplex()?; + if self.use_blocking_transfer(words.len()) { + return self.driver().transfer_in_place(words); + } + self.driver().transfer_in_place_async(words).await } @@ -932,6 +952,10 @@ impl<'d> Spi<'d, Async> { self.driver().flush_async().await; self.driver().setup_full_duplex()?; + if self.use_blocking_transfer(words.len()) { + return self.driver().read(words); + } + self.driver().read_async(words).await } @@ -941,6 +965,11 @@ impl<'d> Spi<'d, Async> { self.driver().flush_async().await; self.driver().setup_full_duplex()?; + if self.use_blocking_transfer(words.len()) { + self.driver().write(words)?; + return self.driver().flush(); + } + self.driver().write_async(words).await } } @@ -1393,6 +1422,15 @@ where self.driver().flush() } + fn use_blocking_transfer(&self, transfer_size: usize) -> bool { + let threshold = self + .spi + .state() + .min_async_transfer_size + .load(Ordering::Relaxed); + threshold > 0 && transfer_size < threshold + } + fn driver(&self) -> Driver { Driver { info: self.spi.info(), @@ -1478,6 +1516,17 @@ impl SpiBusAsync for Spi<'_, Async> { self.driver().flush_async().await; self.driver().setup_full_duplex()?; + if self.use_blocking_transfer(read.len().max(write.len())) { + if read.is_empty() { + self.driver().write(write)?; + return self.driver().flush(); + } else if write.is_empty() { + return self.driver().read(read); + } else { + return self.driver().transfer(read, write); + } + } + if read.is_empty() { self.driver().write_async(write).await } else if write.is_empty() { @@ -1867,6 +1916,9 @@ impl Driver { self.ch_bus_freq(config)?; self.set_bit_order(config.read_bit_order, config.write_bit_order); self.set_data_mode(config.mode); + self.state + .min_async_transfer_size + .store(config.min_async_transfer_size, Ordering::Relaxed); #[cfg(esp32)] self.calculate_half_duplex_values(config); @@ -2482,6 +2534,7 @@ for_each_spi_master! { static STATE: State = State { waker: AtomicWaker::new(), pins: UnsafeCell::new(MaybeUninit::uninit()), + min_async_transfer_size: AtomicUsize::new(0), #[cfg(esp32)] esp32_hack: Esp32Hack { @@ -2508,6 +2561,7 @@ impl QspiInstance for AnySpi<'_> {} pub struct State { waker: AtomicWaker, pins: UnsafeCell>, + min_async_transfer_size: AtomicUsize, #[cfg(esp32)] esp32_hack: Esp32Hack,