2026-02-26 01:40:46 +01:00
//! # TODO
//! * Get rid of leaking buffers, if possible.
//! * Add a parameter for the IRAM allocator.
//! * Customizable interrupt handler priority.
//! * Reorganize code into multiple modules.
//! * Make peripherals generic.
//! * Make the API actually safe to use.
//! Currently, multiple instances are prevented from being instantiated by the usage
//! of non-generic peripheral types which we hold onto.
//! * Add V-SYNC support.
//! * Add support for acyclic buffers and acyclic outbound transmissions (`Dpi::send(false, ..)`).
//! * Release an 0.1.0.
2026-02-26 00:35:57 +01:00
#![ no_std ]
2026-02-26 01:40:46 +01:00
#![ feature(allocator_api) ]
use core ::{
alloc ::Layout ,
2026-02-27 01:14:03 +01:00
cell ::RefCell ,
fmt ::Debug ,
2026-02-26 01:40:46 +01:00
ops ::{ Deref , DerefMut } ,
sync ::atomic ::{ self , AtomicBool } ,
} ;
use alloc ::{ alloc ::Allocator , boxed ::Box , sync ::Arc , vec } ;
2026-02-27 01:14:03 +01:00
use embassy_sync ::{ blocking_mutex ::raw ::CriticalSectionRawMutex , mutex ::Mutex } ;
2026-02-26 01:40:46 +01:00
use esp_hal ::{
Blocking ,
dma ::{
self , BurstConfig , DmaDescriptor , DmaTxBuffer , Mem2Mem , SimpleMem2Mem ,
SimpleMem2MemTransfer ,
} ,
handler ,
interrupt ::{ self , Priority } ,
lcd_cam ::lcd ::dpi ::{ Dpi , DpiTransfer } ,
peripherals ::{ DMA , DMA_CH0 , Interrupt } ,
ram ,
spi ::master ::AnySpi ,
} ;
use ouroboros ::self_referencing ;
2026-02-26 00:35:57 +01:00
extern crate alloc ;
2026-02-26 01:40:46 +01:00
const DMA_CHANNEL_OUTBOUND : usize = 2 ;
const INTERRUPT_OUTBOUND : Interrupt = Interrupt ::DMA_OUT_CH2 ;
2026-02-27 01:14:03 +01:00
static RUNNING_DMA_BOUNCE : Mutex < CriticalSectionRawMutex , RefCell < Option < DmaBounce > > > =
Mutex ::new ( RefCell ::new ( None ) ) ;
struct DmaPeripheralWithChannel < ' a > {
peripheral : AnySpi < ' a > ,
channel : DMA_CH0 < ' a > ,
}
mod transfer {
use super ::* ;
// This would be the ideal implementation, but it doesn't work, because
// I'm not sure how I could make lifetime `'d` in `SimpleMem2MemTransfer` invariant.
//
// #[self_referencing]
// struct ReceivingTransfer {
// peripheral: DmaPeripheralWithChannel<'static>,
// #[borrows(mut peripheral)]
// #[covariant]
// mem2mem: SimpleMem2Mem<'this, Blocking>,
// #[borrows(mut mem2mem)]
// #[covariant]
// transfer: Option<SimpleMem2MemTransfer<'this, 'this, Blocking>>,
// }
#[ self_referencing ]
struct ReceivingTransferInner {
// This peripheral simultaneously exists cloned in `mem2mem`.
// Care must be taken that it is not accessed before `mem2mem` is dropped.
//
// Implementation note:
// Ideally, this would be referenced by `mem2mem` via `#[borrows(mut peripheral)]`,
// but then ouroboros complains about the invariant lifetime `'d` on `transfer`.
peripheral : DmaPeripheralWithChannel < 'static > ,
mem2mem : SimpleMem2Mem < 'static , Blocking > ,
#[ borrows(mut mem2mem) ]
#[ covariant ]
transfer : Option < SimpleMem2MemTransfer < ' this , 'static , Blocking > > ,
}
pub struct ReceivingTransfer ( ReceivingTransferInner ) ;
impl ReceivingTransfer {
pub fn new (
peripheral : DmaPeripheralWithChannel < 'static > ,
mem2mem_builder : impl FnOnce (
DmaPeripheralWithChannel < 'static > ,
) -> SimpleMem2Mem < 'static , Blocking > ,
transfer_builder : impl for < ' a > FnOnce (
& ' a mut SimpleMem2Mem < 'static , Blocking > ,
)
-> SimpleMem2MemTransfer < ' a , 'static , Blocking > ,
) -> Self {
let inner = ReceivingTransferInnerBuilder {
// Safety:
// These peripherals are not used until `mem2mem` is dropped.
// This is ensured by making it a private field.
peripheral : unsafe {
DmaPeripheralWithChannel {
peripheral : peripheral . peripheral . clone_unchecked ( ) ,
channel : peripheral . channel . clone_unchecked ( ) ,
}
} ,
mem2mem : ( mem2mem_builder ) ( peripheral ) ,
transfer_builder : move | mem2mem | Some ( ( transfer_builder ) ( mem2mem ) ) ,
}
. build ( ) ;
Self ( inner )
}
pub fn with_transfer_mut < R > (
& mut self ,
callback : impl FnOnce ( & mut Option < SimpleMem2MemTransfer < '_ , 'static , Blocking > > ) -> R ,
) -> R {
self . 0. with_transfer_mut ( callback )
}
pub fn into_peripheral ( self ) -> DmaPeripheralWithChannel < 'static > {
self . 0. into_heads ( ) . peripheral
}
}
2026-02-26 01:40:46 +01:00
}
2026-02-27 01:14:03 +01:00
use transfer ::* ;
2026-02-26 01:40:46 +01:00
pub struct Swapchain {
pub framebuffers : [ & 'static mut [ u8 ] ; 2 ] ,
}
impl Swapchain {
pub fn into_reader_writer ( self ) -> ( SwapchainReader , SwapchainWriter ) {
assert_eq! (
self . framebuffers [ 0 ] . len ( ) ,
self . framebuffers [ 1 ] . len ( ) ,
" framebuffers in a swapchain must have an equal length "
) ;
let reader_index = Arc ::new ( AtomicBool ::new ( true ) ) ;
(
SwapchainReader {
framebuffers_rw : [
self . framebuffers [ 0 ] as * const [ u8 ] ,
self . framebuffers [ 1 ] as * const [ u8 ] ,
] ,
reader_index : reader_index . clone ( ) ,
} ,
SwapchainWriter {
framebuffers_wr : [
self . framebuffers [ 1 ] as * mut [ u8 ] ,
self . framebuffers [ 0 ] as * mut [ u8 ] ,
] ,
reader_index ,
} ,
)
}
}
// TODO: Don't need to store the framebuffer length twice. Use `*const u8` instead, and store length separately.
pub struct SwapchainReader {
/// These are in the opposite order to `SwapchainWriter`'s framebuffers.
framebuffers_rw : [ * const [ u8 ] ; 2 ] ,
reader_index : Arc < AtomicBool > ,
}
unsafe impl Send for SwapchainReader { }
impl SwapchainReader {
fn len ( & self ) -> usize {
self . framebuffers_rw [ 0 ] . len ( )
}
fn load_read_index ( & self ) -> usize {
self . reader_index . load ( atomic ::Ordering ::SeqCst ) as usize
}
fn get_latest_framebuffer ( & self ) -> & [ u8 ] {
unsafe { & * self . framebuffers_rw [ self . load_read_index ( ) ] }
}
}
// TODO: Don't need to store the framebuffer length twice. Use `*mut u8` instead, and store length separately.
pub struct SwapchainWriter {
/// These are in the opposite order to `SwapchainReader`'s framebuffers.
framebuffers_wr : [ * mut [ u8 ] ; 2 ] ,
reader_index : Arc < AtomicBool > ,
}
unsafe impl Send for SwapchainWriter { }
impl SwapchainWriter {
pub fn len ( & self ) -> usize {
self . framebuffers_wr [ 0 ] . len ( )
}
pub fn write ( & mut self ) -> SwapchainWriteGuard < '_ > {
let framebuffer_ptr =
self . framebuffers_wr [ self . reader_index . load ( atomic ::Ordering ::SeqCst ) as usize ] ;
SwapchainWriteGuard {
framebuffer : unsafe { & mut * framebuffer_ptr } ,
reader_index : & self . reader_index ,
}
}
}
pub struct SwapchainWriteGuard < ' a > {
framebuffer : & ' a mut [ u8 ] ,
reader_index : & ' a AtomicBool ,
}
impl Drop for SwapchainWriteGuard < '_ > {
fn drop ( & mut self ) {
self . reader_index . fetch_xor ( true , atomic ::Ordering ::SeqCst ) ;
}
}
impl < ' a > Deref for SwapchainWriteGuard < ' a > {
type Target = [ u8 ] ;
fn deref ( & self ) -> & Self ::Target {
self . framebuffer
}
}
impl < ' a > DerefMut for SwapchainWriteGuard < ' a > {
fn deref_mut ( & mut self ) -> & mut Self ::Target {
self . framebuffer
}
}
pub struct DmaBounce {
// TODO: Make these generic.
2026-02-27 01:14:03 +01:00
// These currently cannot be generic, because they lack a `reborrow` method.
peripheral_src : Option < DmaPeripheralWithChannel < 'static > > ,
2026-02-26 01:40:46 +01:00
// This can also be more generic, see `DmaEligible` in `Mem2Mem::new`.
peripheral_dst : Option < Dpi < 'static , Blocking > > ,
// TODO: Combine with peripheral_dst using an enum?
transfer_dst : Option < DpiTransfer < 'static , DmaTxBounceBuf , Blocking > > ,
// TODO: Consider having a separate burst config for the two transfers.
burst_config : BurstConfig ,
cyclic : bool ,
/// The size of each window.
window_size : usize ,
/// The number of windows.
windows_len : usize ,
swapchain_src : SwapchainReader ,
// Two buffers of size `window_size`,
// one of which is being written to, while the other is being read from.
bounce_buffer_dst : & 'static mut [ u8 ] ,
bounce_buffer_src : & 'static mut [ u8 ] ,
// A descriptor list that spans a buffer of size `window_size`.
// The buffer pointers need to be updated before each transmission to point to the correct window in the source buffer `src_buffer`.
src_descs : & 'static mut [ DmaDescriptor ] ,
// A descriptor list that spans a buffer of size `window_size`.
// The buffer pointers need to be updated before each transmission to point to the correct bounce buffer.
bounce_dst_descs : & 'static mut [ DmaDescriptor ] ,
// A cyclic descriptor list that spans the buffers `bounce_buffer_dst` and `bounce_buffer_src`.
bounce_src_descs : & 'static mut [ DmaDescriptor ] ,
descriptors_per_window : usize ,
// The index of the next window about to be received into the destination bounce buffer.
window_index_next : usize ,
frame_index_next : usize ,
receiving_transfer : Option < ReceivingTransfer > ,
}
2026-02-27 01:14:03 +01:00
unsafe impl Send for DmaBounce { }
2026-02-26 01:40:46 +01:00
impl DmaBounce {
/// * `allocator` - The allocator used to allocate the bounce buffers.
/// * `channel` - The DMA channel used to transfer data from the source buffer to the bounce buffers.
/// * `peripheral_src` - The peripheral to transfer data from the source buffer to the bounce buffers.
/// * `peripheral_dst` - The peripheral to transfer data to, from the bounce buffers.
/// * `buffer_src` - The source buffer, typically allocated in external memory.
/// * `row_front_porch_bytes` - The number of arbitrary-valued bytes to be sent in front of each row to the destination peripheral.
/// * `row_width_bytes` - The width of a row, in bytes.
/// * `window_size_rows` - The size of a single bounce buffer, in rows.
/// * `burst_config` - The burst config to use for memory transfers (both in and out). TODO: This could be split.
/// * `cyclic` - Experimental! Whether to use a cyclic descriptor list for transfer from the bounce buffers to the destination peripheral.
pub fn new (
allocator : impl Allocator + Copy + 'static ,
channel : DMA_CH0 < 'static > ,
peripheral_src : AnySpi < 'static > ,
peripheral_dst : Dpi < 'static , Blocking > ,
swapchain_src : SwapchainReader ,
row_front_porch_bytes : usize ,
row_width_bytes : usize ,
window_size_rows : usize ,
burst_config : BurstConfig ,
cyclic : bool ,
) -> Self {
assert_eq! (
cyclic , true ,
" acyclic outbound transmissions are not yet implemented "
) ;
let window_size = row_width_bytes * window_size_rows ;
assert_eq! (
swapchain_src . len ( ) % window_size ,
0 ,
" the size of a source buffer must be a multiple of the window size ({window_size} bytes), but it is {len} bytes large " ,
len = swapchain_src . len ( )
) ;
// Conservative alignment. Maxiumum of the cartesian product of [tx, rx] × [internal, external].
let alignment = burst_config . min_compatible_alignment ( ) ;
for & swapchain_ptr in & swapchain_src . framebuffers_rw {
assert_eq! (
unsafe { & * swapchain_ptr } . as_ptr ( ) as usize % alignment ,
0 ,
" the source buffer must be sufficiently aligned to {alignment} bytes for the burst config " ,
) ;
}
assert_eq! (
row_width_bytes % alignment ,
0 ,
" the size of a row in bytes must be sufficiently aligned to {alignment} bytes for the burst config " ,
) ;
assert_eq! (
row_front_porch_bytes % alignment ,
0 ,
" the size of a row's front porch in bytes must be sufficiently aligned to {alignment} bytes for the burst config " ,
) ;
// We need to make the destination peripheral read the front porch data from somewhere,
// and that somewhere is currently the bounce buffer.
// Therefore the front porch must be in bounds.
assert! (
row_front_porch_bytes < = window_size ,
" front porch too large "
) ;
let windows_len = swapchain_src . len ( ) / window_size ;
// TODO: Figure out a way to avoid `leak`ing memory.
// We probably want to store the `Box`es and then unsafely extend the lifetime at sites of usage.
let bounce_buffer_dst =
Box ::leak ( allocate_dma_buffer_in ( window_size , burst_config , allocator ) ) ;
let bounce_buffer_src =
Box ::leak ( allocate_dma_buffer_in ( window_size , burst_config , allocator ) ) ;
let src_descs = Self ::linear_descriptors_for_buffer ( window_size , burst_config , | desc | {
desc . reset_for_tx ( desc . next . is_null ( ) ) ;
// Length for TX buffers must be set in software.
// In RX buffers, it is set by hardware.
desc . set_length ( desc . size ( ) ) ;
} ) ;
let bounce_dst_descs =
Self ::linear_descriptors_for_buffer ( window_size , burst_config , | _ | { } ) ;
let ( bounce_src_descs , descriptors_per_window ) = Self ::bounce_descriptors_for_buffer (
windows_len ,
row_front_porch_bytes ,
row_width_bytes ,
window_size_rows ,
unsafe {
(
& mut * ( bounce_buffer_dst as * mut _ ) ,
& mut * ( bounce_buffer_src as * mut _ ) ,
)
} ,
burst_config ,
cyclic ,
) ;
Self {
2026-02-27 01:14:03 +01:00
peripheral_src : Some ( DmaPeripheralWithChannel {
channel ,
peripheral : peripheral_src ,
} ) ,
2026-02-26 01:40:46 +01:00
peripheral_dst : Some ( peripheral_dst ) ,
transfer_dst : None ,
burst_config ,
cyclic ,
window_size ,
windows_len ,
swapchain_src ,
bounce_buffer_dst ,
bounce_buffer_src ,
src_descs ,
bounce_dst_descs ,
bounce_src_descs ,
descriptors_per_window ,
window_index_next : 0 ,
frame_index_next : 0 ,
receiving_transfer : None ,
}
}
fn linear_descriptors_for_buffer (
buffer_len : usize ,
burst_config : BurstConfig ,
mut setup_desc : impl FnMut ( & mut DmaDescriptor ) ,
) -> & 'static mut [ DmaDescriptor ] {
let max_chunk_size = burst_config . max_compatible_chunk_size ( ) ;
let descriptors_len = dma ::descriptor_count ( buffer_len , max_chunk_size , false ) ;
// TODO: This leaks memory. Ensure it's only called during setup.
let descriptors = Box ::leak ( vec! [ DmaDescriptor ::EMPTY ; descriptors_len ] . into_boxed_slice ( ) ) ;
// Link up the descriptors.
let mut next = core ::ptr ::null_mut ( ) ;
for desc in descriptors . iter_mut ( ) . rev ( ) {
desc . next = next ;
next = desc ;
}
// Prepare each descriptor's buffer size.
let mut descriptors_it = descriptors . iter_mut ( ) ;
let mut remaining_len = buffer_len ;
while remaining_len > 0 {
let chunk_size = core ::cmp ::min ( max_chunk_size , remaining_len ) ;
let desc = descriptors_it . next ( ) . unwrap ( ) ;
desc . set_size ( chunk_size ) ;
( setup_desc ) ( desc ) ;
remaining_len - = chunk_size ;
}
descriptors
}
fn prepare_descriptors_window (
bounce_buffer : & mut [ u8 ] ,
descriptors_window : & mut [ DmaDescriptor ] ,
row_front_porch_bytes : usize ,
row_width_bytes : usize ,
window_size_rows : usize ,
max_chunk_size : usize ,
descriptors_per_row : usize ,
descriptors_per_row_front_porch : usize ,
) {
for ( row_index_in_window , descriptors_row ) in descriptors_window
. chunks_mut ( descriptors_per_row )
. enumerate ( )
{
// let row_index = row_index_in_window + window_index * window_size_rows;
let ( descriptors_row_front_porch , descriptors_row_stored ) =
descriptors_row . split_at_mut ( descriptors_per_row_front_porch ) ;
// Prepare front porch descriptors.
{
let mut descriptors_it = descriptors_row_front_porch . iter_mut ( ) ;
let mut remaining_front_porch = row_front_porch_bytes ;
while remaining_front_porch > 0 {
let desc = descriptors_it . next ( ) . unwrap ( ) ;
let chunk_size = core ::cmp ::min ( max_chunk_size , remaining_front_porch ) ;
remaining_front_porch - = chunk_size ;
// Just make it point at a bounce buffer.
// It is guaranteed to have enough bytes by `DmaBounce::new`.
desc . buffer = bounce_buffer . as_mut_ptr ( ) ;
desc . set_size ( chunk_size ) ;
desc . set_length ( chunk_size ) ;
desc . reset_for_tx ( false ) ;
}
assert! (
descriptors_it . next ( ) . is_none ( ) ,
" front porch descriptors must be used up "
) ;
assert_eq! (
descriptors_row_front_porch
. iter ( )
. map ( | desc | desc . size ( ) )
. sum ::< usize > ( ) ,
row_front_porch_bytes
) ;
}
// Prepare window descriptors.
{
let mut remaining_bounce_buffer =
& mut bounce_buffer [ row_index_in_window * row_width_bytes .. ] [ .. row_width_bytes ] ;
// if remaining_bounce_buffer.len() > row_width_bytes {
// remaining_bounce_buffer = &mut remaining_bounce_buffer[..row_width_bytes];
// }
for desc in & mut * descriptors_row_stored {
let chunk_size = core ::cmp ::min ( max_chunk_size , remaining_bounce_buffer . len ( ) ) ;
desc . buffer = remaining_bounce_buffer . as_mut_ptr ( ) ;
remaining_bounce_buffer = & mut remaining_bounce_buffer [ chunk_size .. ] ;
desc . set_size ( chunk_size ) ;
desc . set_length ( chunk_size ) ;
desc . reset_for_tx ( false ) ;
}
assert! (
remaining_bounce_buffer . is_empty ( ) ,
" bounce buffer must be used up "
) ;
assert_eq! (
descriptors_row_stored
. iter ( )
. map ( | desc | desc . size ( ) )
. sum ::< usize > ( ) ,
row_width_bytes
) ;
}
}
// Set EOF bit on the last descriptor of the window, to signal
// that the bounce buffer is done being read from.
if let Some ( last_desc ) = descriptors_window . last_mut ( ) {
last_desc . reset_for_tx ( true ) ;
}
assert_eq! (
descriptors_window
. iter ( )
. map ( | desc | desc . size ( ) )
. sum ::< usize > ( ) ,
window_size_rows * ( row_front_porch_bytes + row_width_bytes )
) ;
}
fn bounce_descriptors_for_buffer (
windows_len : usize ,
row_front_porch_bytes : usize ,
row_width_bytes : usize ,
window_size_rows : usize ,
bounce_buffers : ( & 'static mut [ u8 ] , & 'static mut [ u8 ] ) ,
burst_config : BurstConfig ,
cyclic : bool ,
) -> ( & 'static mut [ DmaDescriptor ] , usize ) {
assert_eq! (
bounce_buffers . 0. len ( ) ,
bounce_buffers . 1. len ( ) ,
" bounce buffers must be equal in size "
) ;
// If an odd number of windows were needed, two descriptor lists would be needed,
assert_eq! ( windows_len % 2 , 0 , " the number of windows must be even " ) ;
let buffer_len = bounce_buffers . 0. len ( ) ;
assert_eq! (
buffer_len ,
row_width_bytes * window_size_rows ,
" the provided bounce buffers have an invalid size "
) ;
// Implementation note:
// A cyclic descriptor could consist of just a set of descriptors per window,
// so two sets in total, because there are two bounce buffers.
// However, we can also access the pointer of the EOF descriptor within the
// EOF interrupt handler, which lets us compute which descriptor generated that
// interrupt.
// This is useful in the case when an interrupt is missed. Then the number of interrupts
// handled doesn't correspond to the number of windows sent to the destination peripheral.
// In that case, the number of windows sent can be computed from the address of the descriptor.
let max_chunk_size = burst_config . max_compatible_chunk_size ( ) ;
let descriptors_per_row_front_porch =
dma ::descriptor_count ( row_front_porch_bytes , max_chunk_size , false ) ;
let descriptors_per_row_stored =
dma ::descriptor_count ( row_width_bytes , max_chunk_size , false ) ;
let descriptors_per_row = descriptors_per_row_stored + descriptors_per_row_front_porch ;
let descriptors_per_window = window_size_rows * descriptors_per_row ;
let descriptors_per_frame = descriptors_per_window * windows_len ;
let descriptors_frame =
Box ::leak ( vec! [ DmaDescriptor ::EMPTY ; descriptors_per_frame ] . into_boxed_slice ( ) ) ;
// Link up the descriptors.
let mut next = if cyclic {
descriptors_frame . first_mut ( ) . unwrap ( ) as * mut _
} else {
core ::ptr ::null_mut ( )
} ;
for desc in descriptors_frame . iter_mut ( ) . rev ( ) {
desc . next = next ;
next = desc ;
}
// Prepare each descriptor's buffer size.
let bounce_buffers = [ bounce_buffers . 0 , bounce_buffers . 1 ] ;
for ( window_index , descriptors_window ) in descriptors_frame
. chunks_mut ( descriptors_per_window )
. enumerate ( )
{
let bounce_buffer_index = window_index % 2 ;
let bounce_buffer = & mut * bounce_buffers [ bounce_buffer_index ] ;
Self ::prepare_descriptors_window (
bounce_buffer ,
descriptors_window ,
row_front_porch_bytes ,
row_width_bytes ,
window_size_rows ,
max_chunk_size ,
descriptors_per_row ,
descriptors_per_row_front_porch ,
) ;
}
assert_eq! (
descriptors_frame
. iter ( )
. map ( | desc | desc . size ( ) )
. sum ::< usize > ( ) ,
windows_len * window_size_rows * ( row_front_porch_bytes + row_width_bytes )
) ;
( descriptors_frame , descriptors_per_window )
}
/// Safety:
/// TX descriptors require read access to the buffer.
/// RX descriptors require write access to the buffer.
unsafe fn linear_descriptors_prepare (
descriptors : & mut [ DmaDescriptor ] ,
mut buffer : Option < & [ u8 ] > ,
mut setup_desc : impl FnMut ( & mut DmaDescriptor ) ,
) {
for descriptor in descriptors . iter_mut ( ) {
if let Some ( inner_buffer ) = buffer {
descriptor . buffer = inner_buffer . as_ptr ( ) as * mut u8 ;
buffer = Some ( & inner_buffer [ descriptor . size ( ) .. ] ) ;
}
( setup_desc ) ( descriptor ) ;
}
if let Some ( buffer ) = buffer {
assert! (
buffer . is_empty ( ) ,
" a buffer of an incompatible length was assigned to a descriptor set "
) ;
}
}
fn enable_interrupts ( ) {
// Enable interrupts for the peripheral, pt. 1.
interrupt ::enable (
INTERRUPT_OUTBOUND ,
dma_outbound_interrupt_handler . priority ( ) ,
)
. unwrap ( ) ;
// Bind the interrupt handler.
unsafe {
interrupt ::bind_interrupt ( INTERRUPT_OUTBOUND , dma_outbound_interrupt_handler . handler ( ) ) ;
}
// Enable interrupts for the peripheral, pt. 2.
DMA ::regs ( )
. ch ( DMA_CHANNEL_OUTBOUND )
. out_int ( )
. ena ( )
. modify ( | _ , w | w . out_eof ( ) . bit ( true ) ) ;
}
/// Receive a window of bytes into the current dst bounce buffer.
/// Finally, swaps the bounce buffers.
///
/// # Safety:
/// TODO
unsafe fn receive_window_start ( & mut self ) -> ReceivingTransfer {
// Descriptors are initialized by `DmaTxBuf::new`.
let buffer_src_window = & self . swapchain_src . get_latest_framebuffer ( )
[ self . window_index_next * self . window_size .. ] [ .. self . window_size ] ;
unsafe {
Self ::linear_descriptors_prepare ( self . src_descs , Some ( buffer_src_window ) , | _desc | {
// No need to call `DmaDescriptor::reset_for_tx`, because
// 1. we don't rely on the ownership flag;
// 2. the EOF flag is already set during the construction of this buffer.
} ) ;
// TODO: Precompute a descriptor list for each buffer, then use `None` instead of `Some(&mut *self.bounce_buffer_dst)`.
Self ::linear_descriptors_prepare (
self . bounce_dst_descs ,
Some ( self . bounce_buffer_dst ) ,
| desc | {
desc . reset_for_rx ( ) ;
} ,
) ;
}
2026-02-27 01:14:03 +01:00
let peripheral = self
. peripheral_src
. take ( )
. expect ( " the source DMA peripheral should be available " ) ;
ReceivingTransfer ::new (
peripheral ,
| peripheral | {
// Extend the lifetime to 'static because it is required by Mem2Mem.
//
// Safety:
// Pointees are done being used by the driver before this scope ends,
// this is because we `SimpleMem2MemTransfer::wait()` on the transfer to finish.
let bounce_dst_descs : & 'static mut [ DmaDescriptor ] =
unsafe { & mut * ( self . bounce_dst_descs as * mut _ ) } ;
let src_descs : & 'static mut [ DmaDescriptor ] =
unsafe { & mut * ( self . src_descs as * mut _ ) } ;
Mem2Mem ::new ( peripheral . channel , peripheral . peripheral )
. with_descriptors ( bounce_dst_descs , src_descs , self . burst_config )
. unwrap ( )
2026-02-26 01:40:46 +01:00
} ,
2026-02-27 01:14:03 +01:00
| mem2mem | {
mem2mem
. start_transfer ( self . bounce_buffer_dst , buffer_src_window )
. unwrap ( )
} ,
)
}
fn receive_window_wait (
& mut self ,
mut receiving_transfer : ReceivingTransfer ,
increase_window_counter : bool ,
) {
receiving_transfer . with_transfer_mut ( | receiving_transfer | {
// TODO: Async
let receiving_transfer = receiving_transfer
. take ( )
. expect ( " no ongoing inner transfer to a bounce buffer present " ) ;
if ! receiving_transfer . is_done ( ) {
#[ cfg(feature = " log " ) ]
log ::debug! ( " Inbound transfer not yet finished, waiting via spinlock... " ) ;
receiving_transfer . wait ( ) . unwrap ( ) ;
#[ cfg(feature = " log " ) ]
log ::debug! ( " Inbound transfer not finished. " ) ;
}
} ) ;
if increase_window_counter {
self . increase_window_counter ( 1 ) ;
2026-02-26 01:40:46 +01:00
}
2026-02-27 01:14:03 +01:00
let existing_peripheral_src = self
. peripheral_src
. replace ( receiving_transfer . into_peripheral ( ) ) ;
assert! (
existing_peripheral_src . is_none ( ) ,
" no idle source DMA peripheral should be present, it should be receiving data instead "
) ;
}
fn receive_window ( & mut self , increase_window_counter : bool ) {
let receiving_transfer = unsafe { self . receive_window_start ( ) } ;
self . receive_window_wait ( receiving_transfer , increase_window_counter ) ;
2026-02-26 01:40:46 +01:00
}
fn increase_window_counter ( & mut self , windows : isize ) {
2026-02-27 01:14:03 +01:00
// TODO: Updating `self.frame_index_next` without calling this function is error prone.
// Index into an array with `self.window_index_next % 2` instead.
2026-02-26 01:40:46 +01:00
if windows . rem_euclid ( 2 ) = = 1 {
core ::mem ::swap ( & mut self . bounce_buffer_dst , & mut self . bounce_buffer_src ) ;
}
let window_index_next = self . window_index_next as isize + windows ;
self . frame_index_next = ( self . frame_index_next as isize
+ window_index_next / self . windows_len as isize )
as usize ;
self . window_index_next = window_index_next . rem_euclid ( self . windows_len as isize ) as usize ;
}
2026-02-27 01:14:03 +01:00
pub async fn launch_interrupt_driven_task ( mut self ) -> RunningDmaBounceHandle {
2026-02-26 01:40:46 +01:00
Self ::enable_interrupts ( ) ;
2026-02-27 01:14:03 +01:00
// Reset the existing transfer left over since the outbound transfer was paused.
if let Some ( receiving_transfer ) = self . receiving_transfer . take ( ) {
self . receive_window_wait ( receiving_transfer , false ) ;
}
2026-02-26 01:40:46 +01:00
2026-02-27 01:14:03 +01:00
// Reset window index.
if self . window_index_next ! = 0 {
self . increase_window_counter ( ( self . windows_len - self . window_index_next ) as isize ) ;
}
// Fully receive the first windows, so that the outbound transfer can read valid data.
self . receive_window ( true ) ;
#[ cfg(feature = " log " ) ]
log ::debug! ( " DmaBounce: Starting outbound transfer. " ) ;
2026-02-26 01:40:46 +01:00
let dma_tx_buffer = self . get_dma_tx_buffer ( ) ;
let transfer = self
. peripheral_dst
. take ( )
. unwrap ( )
. send ( self . cyclic /* Send perpetually */ , dma_tx_buffer )
. unwrap_or_else ( | ( error , _ , _ ) | {
panic! ( " failed to begin the transmission of the first frame: {error:?} " ) ;
} ) ;
self . transfer_dst = Some ( transfer ) ;
2026-02-27 01:14:03 +01:00
// Start receiving the second window.
2026-02-26 01:40:46 +01:00
self . receiving_transfer = Some ( unsafe { self . receive_window_start ( ) } ) ;
2026-02-27 01:14:03 +01:00
RunningDmaBounceHandle ::new ( self ) . await
2026-02-26 01:40:46 +01:00
}
fn get_dma_tx_buffer ( & mut self ) -> DmaTxBounceBuf {
DmaTxBounceBuf {
preparation : dma ::Preparation {
start : self . bounce_src_descs . first_mut ( ) . unwrap ( ) ,
direction : dma ::TransferDirection ::Out ,
accesses_psram : false ,
burst_transfer : self . burst_config ,
// We don't care about ownership.
// Just yeet whatever the descriptors point to to the destination peripheral.
check_owner : Some ( false ) ,
auto_write_back : false ,
} ,
}
}
2026-02-26 00:35:57 +01:00
}
2026-02-26 01:40:46 +01:00
pub struct DmaTxBounceBuf {
preparation : dma ::Preparation ,
}
unsafe impl DmaTxBuffer for DmaTxBounceBuf {
type View = Self ;
type Final = Self ;
fn prepare ( & mut self ) -> dma ::Preparation {
dma ::Preparation {
start : self . preparation . start ,
direction : self . preparation . direction ,
accesses_psram : self . preparation . accesses_psram ,
burst_transfer : self . preparation . burst_transfer ,
check_owner : self . preparation . check_owner ,
auto_write_back : self . preparation . auto_write_back ,
}
}
fn into_view ( self ) -> Self ::View {
self
}
fn from_view ( view : Self ::View ) -> Self ::Final {
view
}
}
2026-02-27 01:14:03 +01:00
#[ must_use = " the `Drop` implementation uses a spinlock, which can result in a deadlock; use `RunningDmaBounceHandle::stop` instead of letting it be dropped " ]
pub struct RunningDmaBounceHandle {
perform_drop : bool ,
// Prevent construction.
_marker : ( ) ,
}
impl RunningDmaBounceHandle {
async fn new ( dma_bounce : DmaBounce ) -> Self {
let dma_state = RUNNING_DMA_BOUNCE . lock ( ) . await ;
* dma_state . borrow_mut ( ) = Some ( dma_bounce ) ;
Self {
perform_drop : true ,
_marker : ( ) ,
}
}
pub async fn stop ( mut self ) -> DmaBounce {
#[ cfg(feature = " log " ) ]
log ::debug! ( " DmaBounce: Stopping outbound transfer due to `stop`. " ) ;
let mut dma_bounce = RUNNING_DMA_BOUNCE
. lock ( )
. await
. borrow_mut ( )
. take ( )
. expect ( " an instance of a running `DmaBounce` should be available " ) ;
let transfer_dst = dma_bounce
. transfer_dst
. take ( )
. expect ( " an instance of a transfer to the destination peripheral should be available " ) ;
let ( peripheral_dst , _dma_tx_buffer ) = transfer_dst . stop ( ) ;
let previous_peripheral_dst = dma_bounce . peripheral_dst . replace ( peripheral_dst ) ;
self . perform_drop = false ;
2026-02-26 01:40:46 +01:00
2026-02-27 01:14:03 +01:00
assert! (
previous_peripheral_dst . is_none ( ) ,
" there should be no existing destination peripheral in `DmaBounce` "
) ;
2026-02-26 01:40:46 +01:00
2026-02-27 01:14:03 +01:00
dma_bounce
}
}
impl Drop for RunningDmaBounceHandle {
fn drop ( & mut self ) {
if ! self . perform_drop {
return ;
}
#[ cfg(feature = " log " ) ]
log ::debug! ( " DmaBounce: Stopping outbound transfer due to `drop`. " ) ;
let dma_bounce = loop {
if let Ok ( dma_bounce ) = RUNNING_DMA_BOUNCE . try_lock ( ) {
break dma_bounce ;
}
} ;
let mut dma_bounce = dma_bounce
. borrow_mut ( )
. take ( )
. expect ( " an instance of a running `DmaBounce` should be available " ) ;
let transfer_dst = dma_bounce
. transfer_dst
. take ( )
. expect ( " an instance of a transfer to the destination peripheral should be available " ) ;
let ( peripheral_dst , _dma_tx_buffer ) = transfer_dst . stop ( ) ;
dma_bounce . peripheral_dst = Some ( peripheral_dst ) ;
drop ( dma_bounce ) ;
}
}
impl Debug for RunningDmaBounceHandle {
fn fmt ( & self , f : & mut core ::fmt ::Formatter < '_ > ) -> core ::fmt ::Result {
f . debug_struct ( " RunningDmaBounceHandle " )
. finish_non_exhaustive ( )
}
}
2026-02-26 01:40:46 +01:00
#[ handler(priority = Priority::Priority3) ]
#[ ram ] // Improves performance.
fn dma_outbound_interrupt_handler ( ) {
let interrupt = DMA ::regs ( ) . ch ( DMA_CHANNEL_OUTBOUND ) . out_int ( ) ;
let bounce_buffer_sent = interrupt . st ( ) . read ( ) . out_eof ( ) . bit_is_set ( ) ;
if ! bounce_buffer_sent {
2026-02-27 01:14:03 +01:00
// This should never happen.
#[ cfg(feature = " log " ) ]
log ::warn! ( " DMA interrupt handler invoked without `OUT_EOF` bit having been set. " ) ;
2026-02-26 01:40:46 +01:00
return ;
}
// Clear the bit by writing 1 to the clear bits.
interrupt . clr ( ) . write ( | w | w . out_eof ( ) . bit ( true ) ) ;
2026-02-27 01:14:03 +01:00
let Ok ( dma_state ) = RUNNING_DMA_BOUNCE . try_lock ( ) else {
// If we can't acquire a lock guard, just give up.
#[ cfg(feature = " log " ) ]
log ::trace! ( " Failed to lock `RUNNING_DMA_BOUNCE`. " ) ;
return ;
} ;
let mut dma_state = dma_state . borrow_mut ( ) ;
let Some ( dma_state ) = dma_state . as_mut ( ) else {
// The outbound transmission was stopped.
#[ cfg(feature = " log " ) ]
log ::trace! ( " `RUNNING_DMA_BOUNCE` is empty. " ) ;
return ;
2026-02-26 01:40:46 +01:00
} ;
// The descriptor of the buffer with an EOF flag that just finished being sent.
let descriptor_ptr = DMA ::regs ( )
. ch ( DMA_CHANNEL_OUTBOUND )
. out_eof_des_addr ( )
. read ( )
. out_eof_des_addr ( )
. bits ( ) as * const DmaDescriptor ;
// This is the index of the window that just finished being transmitted to the destination peripheral.
let window_sent_index =
unsafe { descriptor_ptr . offset_from_unsigned ( dma_state . bounce_src_descs . as_ptr ( ) ) }
/ dma_state . descriptors_per_window ;
// The next window to be sent is `(window_sent_index + 1) % dma_state.windows_len`.
// That is not the window we want to buffer, because the transmissions would race.
// We instead want to buffer the next window:
let window_index_next = ( window_sent_index + 2 ) % dma_state . windows_len ;
// Swap bounce buffers.
if ( dma_state . windows_len + window_index_next - dma_state . window_index_next ) % 2 = = 1 {
core ::mem ::swap (
& mut dma_state . bounce_buffer_dst ,
& mut dma_state . bounce_buffer_src ,
) ;
}
dma_state . window_index_next = window_index_next ;
2026-02-27 01:14:03 +01:00
let receiving_transfer = dma_state
2026-02-26 01:40:46 +01:00
. receiving_transfer
. take ( )
. expect ( " no ongoing transfer to a bounce buffer present " ) ;
2026-02-27 01:14:03 +01:00
dma_state . receive_window_wait (
receiving_transfer ,
// `window_index_next` is already updated above.
false ,
) ;
2026-02-26 01:40:46 +01:00
// If there is any ongoing transfer, cancel it and start a new one.
dma_state . receiving_transfer = Some ( unsafe { dma_state . receive_window_start ( ) } ) ;
}
pub fn allocate_dma_buffer_in < A : Allocator > (
len : usize ,
burst_config : BurstConfig ,
alloc : A ,
) -> Box < [ u8 ] , A > {
// Conservative alignment. Maxiumum of the cartesian product of [tx, rx] × [internal, external].
let alignment = burst_config . min_compatible_alignment ( ) ;
assert_eq! (
len % alignment ,
0 ,
" the size of a DMA buffer must be a multiple of {alignment} bytes, but it is {len} bytes large "
) ;
2026-02-26 00:35:57 +01:00
2026-02-26 01:40:46 +01:00
// ⚠️ Note: For chips that support DMA to/from PSRAM (ESP32-S3) DMA transfers to/from PSRAM
// have extra alignment requirements. The address and size of the buffer pointed to by each
// descriptor must be a multiple of the cache line (block) size. This is 32 bytes on ESP32-S3.
// That is ensured by the `assert_eq` preceding this block.
unsafe {
let raw = alloc
. allocate_zeroed ( Layout ::from_size_align ( len , alignment ) . unwrap ( ) )
. expect ( " failed to allocate a DMA buffer " ) ;
Box ::from_raw_in ( raw . as_ptr ( ) , alloc )
2026-02-26 00:35:57 +01:00
}
}