Skip to content

Use DmaBuffer API for I2S#5603

Open
bjoernQ wants to merge 12 commits into
esp-rs:mainfrom
bjoernQ:i2s-dma
Open

Use DmaBuffer API for I2S#5603
bjoernQ wants to merge 12 commits into
esp-rs:mainfrom
bjoernQ:i2s-dma

Conversation

@bjoernQ
Copy link
Copy Markdown
Contributor

@bjoernQ bjoernQ commented May 26, 2026

Changelog

esp-hal

  • Added: I2sRx::read and I2sTx::write take ownership of the channel and a DmaRxBuffer / DmaTxBuffer, returning I2sRxDmaTransfer / I2sTxDmaTransfer.
  • Added: dma_tx_stream_buffer! macro for statically allocated streaming TX buffers (matching the existing dma_rx_stream_buffer!).
  • Changed: I2S DMA transfers now use the shared DmaBuffer API instead of manually managed descriptor chains. Streaming transfers use dma_rx_stream_buffer! / dma_tx_stream_buffer!; one-shot transfers use
    dma_rx_buffer! / dma_tx_buffer!.
  • Changed: I2sRxCreator::build and I2sTxCreator::build no longer take a descriptor slice.
  • Changed: On streaming transfers, available() is now available_bytes() and pop() / push() return usize instead of Result.
  • Removed: I2sRx::read_dma, read_dma_circular, read_dma_circular_async, read_words, and I2sTx::write_dma, write_dma_circular, write_dma_circular_async, write_words.
  • Removed: AcceptedWord trait.
  • Removed: Generic DMA transfer types DmaTransferRx, DmaTransferTx, DmaTransferRxCircular, DmaTransferTxCircular, and DescriptorChain.
  • Added: dma_tx_stream_buffer! macro.
  • Removed: DmaError::Late (overrun/underrun is now handled via buffer back-pressure; drain promptly using available_bytes() / pop()).

Migration guide

esp-hal/I2S driver

I2S DMA now uses the DmaBuffer API

I2S no longer uses manually passed DMA descriptors or the generic DmaTransfer* types. Buffers are created with the DMA buffer macros, channels are built without descriptors, and transfers are started by consuming the channel.

-use esp_hal::dma_buffers;
-let (mut rx_buffer, rx_descriptors, _, _) = dma_buffers!(4 * 4092, 0);
+use esp_hal::dma_rx_stream_buffer;
+let rx_buffer = dma_rx_stream_buffer!(4 * 4092, 1024);
 let i2s_rx = i2s
     .i2s_rx
     .with_bclk(peripherals.GPIO1)
     .with_ws(peripherals.GPIO2)
     .with_din(peripherals.GPIO5)
-    .build(rx_descriptors);
+    .build();

Use dma_rx_buffer! / dma_tx_buffer! instead of the *_stream_buffer! macros when you need a finite, one-shot transfer rather than continuous streaming.

Starting transfers

read / write take ownership of the channel. For async code, call .into_async() on I2s before building the RX/TX channel.

-let mut transfer = i2s_rx.read_dma_circular(&mut rx_buffer)?;
+let mut transfer = i2s_rx.read(rx_buffer)?;

On failure, read / write return Err((Error, I2sRx, BUF)) (or the TX equivalents), so you can recover both the channel and the buffer.

Transfer handles and streaming I/O

Generic DmaTransferRxCircular / DmaTransferTxCircular (and the old I2sReadDmaTransferAsync / I2sWriteDmaTransferAsync) are replaced by I2sRxDmaTransfer / I2sTxDmaTransfer. These deref to the buffer view, exposing available_bytes(), pop(), push(), and push_with().

 loop {
-    transfer.wait_for_data().await?;
-    let avail = transfer.available()?;
+    transfer.wait_for_available_async().await?;
+    let avail = transaction.available_bytes();
     if avail > 0 {
-        transfer.pop(&mut rcv[..avail])?;
+        transfer.pop(&mut rcv[..avail]);
     }
 }

Finishing a one-shot transfer

let transfer = i2s_rx.read(buffer)?;
let (i2s_rx, buffer) = transfer.wait()?;
// or, in async code:
let (i2s_rx, buffer) = transfer.wait_async().await?;

@bjoernQ bjoernQ marked this pull request as ready for review May 26, 2026 13:11
@bjoernQ bjoernQ marked this pull request as draft May 26, 2026 16:12
Comment thread esp-hal/src/i2s/master.rs
Comment thread esp-hal/src/i2s/master.rs
Buf: DmaTxBuffer,
{
/// Waits for the transfer to finish and returns the peripheral and buffer.
pub async fn wait_async(mut self) -> Result<(I2sTx<'d, Async>, Buf::Final), DmaError> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait_async's API isn't cancel friendly. There should be a separate method like this which can be cancelled without dropping the transfer object or peripheral/buffer.

Comment thread esp-hal/src/i2s/master.rs Outdated
{
/// Waits for the transfer to finish and returns the peripheral and buffer.
pub async fn wait_async(mut self) -> Result<(I2sTx<'d, Async>, Buf::Final), DmaError> {
DmaTxFuture::new(&mut self.i2s_tx.tx_channel).await?;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For TX, just because the DMA is done, it doesn't the peripheral is. I think you still need a while !self.is_done() {} here anyway.

Comment thread esp-hal/src/i2s/master.rs
Comment thread esp-hal/src/i2s/master.rs Outdated
Comment on lines +345 to +346
self.i2s_rx.rx_channel.stop_transfer();
self.i2s_rx.i2s.rx_stop();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For RX, the peripheral should be stopped before the DMA.

Comment thread esp-hal/src/i2s/master.rs
pub async fn wait_for_available_async(&mut self) -> Result<(), DmaError> {
DmaRxFuture::new_with_config(
&mut self.i2s_rx.rx_channel,
enum_set!(DmaRxInterrupt::SuccessfulEof | DmaRxInterrupt::Done),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
enum_set!(DmaRxInterrupt::SuccessfulEof | DmaRxInterrupt::Done),
enum_set!(DmaRxInterrupt::Done),

DmaRxInterrupt::SuccessfulEof will always be accompanied with a DmaRxInterrupt::Done anyway.

The ideal would be to have one method for DmaRxInterrupt::SuccessfulEof and another for DmaRxInterrupt::Done, but that doesn't have to be in this PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 this driver should see more improvements in future (since it's getting more and more popular)

Comment thread esp-hal/src/i2s/master.rs Outdated
Comment on lines +306 to +307
self.i2s_rx.rx_channel.stop_transfer();
self.i2s_rx.i2s.rx_stop();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For RX, stop the peripheral before the DMA.

Comment thread esp-hal/src/i2s/master.rs
Comment thread esp-hal/src/dma/buffers/mod.rs
Comment thread esp-hal/src/dma/buffers/mod.rs
@bjoernQ
Copy link
Copy Markdown
Contributor Author

bjoernQ commented May 27, 2026

/hil full --tests i2s, misc_non_drivers

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 27, 2026

Triggered full HIL run for #5603.

Run: https://github.com/esp-rs/esp-hal/actions/runs/26517161789

Status update: ❌ HIL (full) run failed (conclusion: failure).

@bjoernQ bjoernQ marked this pull request as ready for review May 27, 2026 14:51
Comment thread esp-hal/src/i2s/master.rs
let (i2s_tx, buf) = self.release();

if i2s_tx.tx_channel.has_error() {
Err(DmaError::DescriptorError)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The peripheral and buffer should still be returned when there is an error.

Comment thread esp-hal/src/i2s/master.rs
pub async fn wait_for_available_async(&mut self) -> Result<(), DmaError> {
DmaTxFuture::new_with_config(
&mut self.i2s_tx.tx_channel,
enum_set!(DmaTxInterrupt::Eof),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this interrupt isn't cleared/consumed. So it's only async the first time it's called and subsequent calls immediately return.

desc.next = next;
next = desc;

desc.set_owner(Owner::Cpu);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should be Owner::Dma, otherwise available_bytes returns a value that is too high in the first pass.

If I'm understanding this buffer type correctly, it sets up the descriptors such that they are all linked together and the first half of the linked list contains the prefilled data and the second half contains empty descriptors.

When the user pushes more data after the transfer starts, it takes descriptors from the start of the linked list, instead of the start of the 2nd half.

The push function checks the descriptor ownership to see if it's available for filling.
Once the dma gets through the first half of the linked list, the push function is going to think the DMA has gotten through the second half as well, even if the DMA hasn't, due to these descriptors being marked as owned by the CPU.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants