Skip to content
Open
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1870275
feat: i2s move
AstrickHarren Sep 22, 2025
1192aab
fix: updating trait bounds
AstrickHarren Sep 22, 2025
6c40e56
feat: add macros for tx stream buf
AstrickHarren Sep 23, 2025
89356d8
refactor: use dma loop buf for sine sound test
AstrickHarren Sep 23, 2025
d82ebaa
refactor: remove i2s test late
AstrickHarren Sep 23, 2025
4bab025
fix: import issue in qa test
AstrickHarren Sep 23, 2025
0e1531f
fix: update trait bounds
AstrickHarren Sep 23, 2025
48309c0
feat: prefilling tx stream buf
AstrickHarren Sep 23, 2025
438f5d4
Merge branch 'main' into feat/i2s-move
AstrickHarren Sep 23, 2025
619bb19
Merge branch 'main' into feat/i2s-move
AstrickHarren Sep 24, 2025
6897081
refactor: read channel done to detect if an operation is done
AstrickHarren Sep 24, 2025
ba6748f
fix: make the new test work
AstrickHarren Sep 24, 2025
24478a4
feat: add i2s done for esp32 and esp32s2
AstrickHarren Sep 24, 2025
834bf3c
test: use macros for creating DMA tx stream buffers
AstrickHarren Sep 25, 2025
1951f08
test: use dma stream buffer macros instead
AstrickHarren Sep 25, 2025
751ce01
Merge branch 'main' into feat/i2s-move
AstrickHarren Sep 25, 2025
be30dda
fix: not listening to interrupts by default
AstrickHarren Sep 25, 2025
4f6564f
Merge branch 'main' into feat/i2s-move
AstrickHarren Sep 25, 2025
4b311b6
Merge branch 'main' into feat/i2s-move
AstrickHarren Oct 10, 2025
c6cdbd4
refactor: simplify tx streaming logic
AstrickHarren Oct 10, 2025
cef43b0
refactor: try to make use of in_desc_empty for rx_done
AstrickHarren Oct 10, 2025
3f82bc0
fix: minor code cleaning
AstrickHarren Oct 10, 2025
a9777d8
Merge branch 'main' into feat/i2s-move
AstrickHarren Oct 10, 2025
1ec9ecf
Merge branch 'main' into feat/i2s-move
AstrickHarren Oct 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions esp-hal/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `AnyPin::downcast`/`AnyPeripheral::downcast` to allow retrieving the original GPIO/peripheral type (#3783, #3784)
- Add `ESP_HAL_CONFIG_PLACE_RMT_DRIVER_IN_RAM` configuration option to pin the RMT driver in RAM (#3778)
- The `rt` feature (#3706)
- Added `DmaTxStreamBuf` (#3639)

### Changed

Expand All @@ -119,6 +120,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Adjusted ESP32-S2 deep-sleep to hibernate for the Ext1WakeupSource (#3785)
- Libraries depending on esp-hal should now disable default features, so that only the final binary crate enables the `rt` feature (#3706)
- Changed `interrupt::RESERVED_INTERRUPTS` from `&[usize]` to `&[u32]` (#3798)
- Migrate the I2S driver to a move based API (#3639)

### Fixed

Expand Down
215 changes: 215 additions & 0 deletions esp-hal/src/dma/buffers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1429,6 +1429,221 @@ impl DmaRxStreamBufView {
}
}

/// DMA Streaming Transmit Buffer.
///
/// This is symmetric implementation to [DmaTxStreamBuf], used for continuously
/// streaming data to a peripheral's FIFO.
///
/// The list starts out like so `A(full) -> B(full) -> C(full) -> D(full) -> NULL`.
///
/// As the DMA writes to FIFO, the list progresses like so:
/// - `A(full) -> B(full) -> C(full) -> D(full) -> NULL`
/// - `A(empty) -> B(full) -> C(full) -> D(full) -> NULL`
/// - `A(empty) -> B(empty) -> C(full) -> D(full) -> NULL`
/// - `A(empty) -> B(empty) -> C(empty) -> D(full) -> NULL`
///
/// As you call [DmaTxStreamBufView::push] the list (approximately) progresses like so:
/// - `A(empty) -> B(empty) -> C(empty) -> D(full) -> NULL`
/// - `B(empty) -> C(empty) -> D(full) -> A(full) -> NULL`
/// - `C(empty) -> D(full) -> A(full) -> B(full) -> NULL`
/// - `D(full) -> A(full) -> B(full) -> C(full) -> NULL`
///
/// If all the descriptors fill up, the [DmaTxInterrupt::TotalEof] interrupt will fire and DMA
/// will stop writing, at which point it is up to you to resume/restart the transfer.
pub struct DmaTxStreamBuf {
descriptors: &'static mut [DmaDescriptor],
buffer: &'static mut [u8],
burst: BurstConfig,
pre_filled: Option<usize>,
}

impl DmaTxStreamBuf {
/// Creates a new [DmaTxStreamBuf] evenly distributing the buffer between
/// the provided descriptors.
pub fn new(
descriptors: &'static mut [DmaDescriptor],
buffer: &'static mut [u8],
) -> Result<Self, DmaBufError> {
match () {
_ if !is_slice_in_dram(descriptors) => Err(DmaBufError::UnsupportedMemoryRegion)?,
_ if !is_slice_in_dram(buffer) => Err(DmaBufError::UnsupportedMemoryRegion)?,
_ if descriptors.is_empty() => Err(DmaBufError::InsufficientDescriptors)?,
_ => (),
};

// Evenly distribute the buffer between the descriptors.
let chunk_size = Some(buffer.len() / descriptors.len())
.filter(|x| *x <= 4095)
.ok_or(DmaBufError::InsufficientDescriptors)?;

let mut chunks = buffer.chunks_exact_mut(chunk_size);
for (desc, chunk) in descriptors.iter_mut().zip(chunks.by_ref()) {
desc.buffer = chunk.as_mut_ptr();
desc.set_size(chunk.len());
}
let remainder = chunks.into_remainder();

if !remainder.is_empty() {
// Append any excess to the last descriptor.
let last_descriptor = descriptors.last_mut().unwrap();
let size = last_descriptor.size() + remainder.len();
if size > 4095 {
Err(DmaBufError::InsufficientDescriptors)?;
}
last_descriptor.set_size(size);
}

Ok(Self {
descriptors,
buffer,
burst: Default::default(),
pre_filled: None,
})
}

/// Consume the buf, returning the descriptors and buffer.
pub fn split(self) -> (&'static mut [DmaDescriptor], &'static mut [u8]) {
(self.descriptors, self.buffer)
}

/// Fill the buffer with the given data before DMA transfer starts.
///
/// Otherwise the streaming buffer will transfer garbage data to
/// DMA so that CPU has enough time to fill the buffer after transfer starts.
pub fn fill(&mut self, data: &[u8]) -> usize {
let n = self.buffer.len().min(data.len());
self.buffer[..n].copy_from_slice(&data[..n]);
self.pre_filled = Some(n);
n
}
}

unsafe impl DmaTxBuffer for DmaTxStreamBuf {
type View = DmaTxStreamBufView;
type Final = Self;

fn prepare(&mut self) -> Preparation {
// Link up all the descriptors (but not in a circle).
let mut next = null_mut();
for desc in self.descriptors.iter_mut().rev() {
desc.next = next;
next = desc;

desc.set_suc_eof(false);
}
Preparation {
start: self.descriptors.as_mut_ptr(),
direction: TransferDirection::Out,
#[cfg(psram_dma)]
accesses_psram: false,
burst_transfer: self.burst,

// Whilst we give ownership of the descriptors the DMA, the correctness of this buffer
// implementation doesn't rely on the DMA checking for descriptor ownership.
// No descriptor is added back to the end of the stream before it's ready for the DMA
// to consume it.
check_owner: None,
auto_write_back: true,
}
}

fn into_view(self) -> Self::View {
// If the buffer is not pre-filled, treat it as fully filled
let pre_filled = self.pre_filled.unwrap_or(self.buffer.len());
let mut view = DmaTxStreamBufView {
buf: self,
descriptor_idx: 0,
descriptor_offset: 0,
};
view.update_state(pre_filled);
view
}

fn from_view(view: Self::View) -> Self {
view.buf
}
}

/// A view into a [DmaRxStreamBuf]
pub struct DmaTxStreamBufView {
buf: DmaTxStreamBuf,
descriptor_idx: usize,
descriptor_offset: usize,
}

impl DmaTxStreamBufView {
/// Returns the number of bytes available for writing.
pub fn available_bytes(&self) -> usize {
let (tail, head) = self.buf.descriptors.split_at(self.descriptor_idx);
head.iter()
.chain(tail)
.take_while(|d| d.owner() == Owner::Cpu)
.map(|d| d.size())
.sum::<usize>()
- self.descriptor_offset
}

/// Pushes a buffer into the stream buffer.
/// Returns the number of bytes pushed.
pub fn push_with(&mut self, f: impl FnOnce(&mut [u8]) -> usize) -> usize {
let chunk_size = self.buf.descriptors[0].size();
let dma_start = self.descriptor_idx * chunk_size + self.descriptor_offset;
let dma_end = (dma_start + self.available_bytes()).min(self.buf.buffer.len());
let bytes_pushed = f(&mut self.buf.buffer[dma_start..dma_end]);

self.update_state(bytes_pushed);
bytes_pushed
}

/// Updates the view state on pushed with `bytes_pushed` many bytes.
fn update_state(&mut self, bytes_pushed: usize) {
let mut bytes_filled = 0;
for d in (self.descriptor_idx..self.buf.descriptors.len()).chain(core::iter::once(0)) {
let desc = &mut self.buf.descriptors[d];
let bytes_in_d = desc.size() - self.descriptor_offset;
// There is at least one byte left in `desc`.
if bytes_in_d + bytes_filled > bytes_pushed {
self.descriptor_idx = d;
self.descriptor_offset = self.descriptor_offset + bytes_pushed - bytes_filled;
break;
}
bytes_filled += bytes_in_d;
self.descriptor_offset = 0;

// Put the current descriptor at the end of the list
desc.set_owner(Owner::Dma);
desc.set_length(desc.size());
desc.set_suc_eof(true);
let p = d.checked_sub(1).unwrap_or(self.buf.descriptors.len() - 1);
if p != d {
let [prev, desc] = self.buf.descriptors.get_disjoint_mut([p, d]).unwrap();
desc.next = null_mut();
prev.next = desc;
prev.set_suc_eof(false);
}
}
}

/// Pushes a buffer into the stream buffer.
/// Returns the number of bytes pushed.
pub fn push(&mut self, data: &[u8]) -> usize {
let mut remaining = data.len();
let mut offset = 0;

while self.available_bytes() >= remaining && remaining > 0 {
let written = self.push_with(|buffer| {
let len = usize::min(buffer.len(), data.len() - offset);
buffer[..len].copy_from_slice(&data[offset..][..len]);
len
});
offset += written;
remaining -= written;
}

offset
}
}

static mut EMPTY: [DmaDescriptor; 1] = [DmaDescriptor::EMPTY];

/// An empty buffer that can be used when you don't need to transfer any data.
Expand Down
Loading
Loading