Skip to main content
Associate
Oct 13, 2026 1:59 PM UTC
Question

Syncing SAI and DMA for I2S

Hello,


I have a STM32U585. I’m trying to use SAI to transmit and receive I2S audio as a slave. The I2S master is an external codec that’s configured for stereo 16bit 16kHz audio. Mic/input from codec->MCU and speaker/output from MCU->codec. The first stereo sample received on the codec is routed to the left speaker and second to the right.
 
This works as expected for the mic. For the speakers, sometimes even samples will play on the right side and odd on the left. I’ve loaded the DMA buffer with dummy values 0xAAAA and 0x0000 to demonstrate. MISO is the signal of interest here. I’d like 0xAAAA to be on Ch 1 consistently.

Expected Samples
Unexpected Samples

This occurred about 50% of the time. It’s observable for the entire runtime and doesn’t correlate with how long the software has been running. I suspected a sync issue SAI/DMA so I implemented a quick hack to poll the GPIO to start the DMA transfer after the falling edge of WS to help sync. This improves reliability from 50% to 90%. I’ve included a simplified snippet of the code.

HAL_SAI_Abort(rx);
HAL_SAI_Abort(tx);

while (HAL_GPIO_ReadPin(WS_PORT, WS_PIN) == GPIO_PIN_RESET);
while (HAL_GPIO_ReadPin(WS_PORT, WS_PIN) == GPIO_PIN_SET);

HAL_SAI_Receive_DMA(rx, rx_data, sizeof(rx_data));
HAL_SAI_Transmit_DMA(tx, tx_data, sizeof(tx_data));

 

I am using SAI1 block A for async Tx and SAI1 block B for sync Rx both with DMA.

 

Appreciate your help, thanks.

8 replies

LCE
Principal III
Oct 13, 2026 4:11 PM UTC

​​​​​​What else is going on with in your code?

If possible, ​try to increase both DMA priorities. If the channel swap happens while everything is running, this might be the cause.

And the DMA buffers: big enough and same size?

And how’s the buffer handling? Does the U5 have DMA double buffer mode (DBM)? If yes, use that. If not, implement some smart DMA “transfer complete / half-complete” buffer handling.

AND - as I recently learned - after each stop (or before each start) of the SAIs / DMA, flush the FIFO.

 

JackRAuthor
Associate
Oct 16, 2026 3:02 PM UTC

Hi @LCE,

Thanks for your reply, I was able to run several experiments with your suggestions over the past few days.

If possible, ​try to increase both DMA priorities. If the channel swap happens while everything is running, this might be the cause.

The swap happens when I start the transfer or close enough that I can’t notice otherwise. I do have some other peripherals running but changing the SAI DMA priorities from 10 to 0 and the others from 0 to 1 doesn’t seem to make any improvements.

And the DMA buffers: big enough and same size?

If the audio frame is always 5ms. 5ms * 16 samples/ms * 2 bytes/sample *  2 ch = 320 per side. There are two for ping ponging.

 And how’s the buffer handling? Does the U5 have DMA double buffer mode (DBM)? If yes, use that. If not, implement some smart DMA “transfer complete / half-complete” buffer handling.

I wasn’t able to find anything that is specifically called double buffer mode. I am using circular mode for the DMA, which I have tested, and is ping ponging between the two buffers cleanly.

I had never implemented complete / half-complete callbacks for Tx to know which side of the buffer to fill. Sometimes my buffer would write part of one audio frame to one side of the buffer and part to the other side. If I fill the audio frame with 0x00, 0x01, 0x02…, the skipping is clear on the I2S analyzer. However, even with this fix, the samples still swap between channels on I2S.

AND - as I recently learned - after each stop (or before each start) of the SAIs / DMA, flush the FIFO.

I recognized this as the solution to your own thread from a few weeks ago. I call HAL_SAI_Abort which flushes the FIFO with SET_BIT(hsai->Instance->CR2, SAI_xCR2_FFLUSH), before starting the DMA transfer, or so I thought. I discovered yesterday that my call to HAL_SAI_Abort was in a conditional tree that wasn’t running before the first DMA transfer.

With this fixed, I am seeing “unexpected” behavior on every test, where the even samples appear on the right. Although I expected even->left, odd->right, this could be a solution as long as the behavior is predictable. This is promising, I will continue testing to see how consistent it is.

rich.g.williams
Senior
Oct 13, 2026 6:11 PM UTC

I can explain prerequisites in terms of SPI not SAI or I2C.

As I understand it WS is a framing signal and it differentiates between left ch and right ch.  I’m assuming WS timing is good enough to ensure differentiation.

Your DMA is circular meaning the linked list (with probably one entry) does not terminate it keeps reloading the destination rings and  source rings. This is simplest to do using four DMA channels

a) transfer from SAI (Left) to Ring_In_Left

b) transfer  from SAI (Right) to Ring_In_Right

c) transfer to SAI (Left) from Ring_Out_Left

d) transfer  to SAI (Right) from Ring_Out_Right

With SPI I set it to a word length of 32 bit and what’s in the FIFO gives me a trigger for the DMA so the input ring buffer is continuously filling.

Let’s assume your input ring buffers are say 4KB each and your output ring buffers are also 4KB each and lets assume your app reads the inputs processes (FIR filter FFT etc) and populates the output ring buffers.  What’s then vital is that you are reading the input rings behind the input DMA pointer and writing the output rings ahead of the output DMA pointer.

Also that all four rings are separate - no way they can overlap (that’s the advantage of 4 DMA channels doing the job). If you are using two DMA channels it can be done but the linked list and trigger event becomes more involved I think.

Summing up its likely that what you are seeing is a trigger event problem and if you are using WS for your trigger events WS has to be OK.  

For DMA there are a huge number of choices for trigger events to find out about.  

Hope that helps, I could give you working DMA examples of DMA running with SPI but they are in assembler (not popular on here).

 

 

Bill's Notes
JackRAuthor
Associate
Oct 16, 2026 4:19 PM UTC

Hi ​@rich.g.williams,

Thanks for your reply.

Your DMA is circular meaning the linked list (with probably one entry) does not terminate it keeps reloading the destination rings and  source rings. This is simplest to do using four DMA channels

I am only using two DMA channels because the stereo samples from left and right are interleaved. So instead of Rx L, Rx R, Tx L, Tx R it’s just Rx and Tx.

Also that all four rings are separate - no way they can overlap (that’s the advantage of 4 DMA channels doing the job). If you are using two DMA channels it can be done but the linked list and trigger event becomes more involved I think.

HAL SAI has HAL_SAI_TxHalfCpltCallback and HAL_SAI_TxCpltCallback to signal which side is ready. I had implemented these for Rx but not Tx. Sometimes my buffer would write part of one audio frame to one side of the buffer and part to the other side. If I fill the audio frame with 0x00, 0x01, 0x02…, the skipping is clear on the I2S analyzer. However, even with this fix, the samples still swap between channels on I2S.

So there was a trigger event bug as you predicted, along with another bug that is causing the original swapping issue.

ST Technical Moderator
Oct 15, 2026 7:56 AM UTC

Hello ​@JackR 

If the FIFO threshold is set to EMPTY, the DMA may provide only one data word to the FIFO before the SAI starts transmitting. After transmission begins, the DMA refills the FIFO only when it becomes empty again.

In this situation, it is possible that at startup:

  • SAI transmits the first word already present in the FIFO,
  • but the DMA does not refill the FIFO quickly enough before the next slot is transmitted,
  • so the SAI may repeat the previous word or shift the channel alignment.

This could explain why the left/right samples are sometimes swapped, and why the issue seems to occur only at the start of the stream.

A useful test would be to increase the FIFO threshold, for example to HALF FULL. In that case, the SAI would wait until several data words are already present in the FIFO before starting transmission. This gives more margin at startup and may prevent the initial channel misalignment.

In order to give better visibility on the answered topics, please click on 'Best answer' on the reply which solved your issue or answered your question. Saket_Om
LCE
Principal III
Oct 15, 2026 9:38 AM UTC

As I said:  flush the SAI FIFO.

rich.g.williams
Senior
Oct 16, 2026 5:37 PM UTC

​@JackR 

Thanks for your comments, I don't know your application directly but maybe what I am doing will give you context so here goes.

"When a Serial Audio Interface (SAI) is configured as an I2S Slave (transmitting or receiving data), it relies entirely on the Frame Synchronization (FS / WS / LRCLK) signal provided by the external I2S Master to distinguish between the Left and Right channels.
In standard I2S protocol, the protocol mandates that:
• Left Channel: FS line is LOW.
• Right Channel: FS line is HIGH.
• Channel Boundary: The actual data transmission for each channel starts 1 bit clock (BCLK) cycle after the FS edge transitions (this is the standard I2S 1-bit)"


So your DMA triggers must be rising edge of FS start reading Right Channel dropping edge of FS Line start reading Left Channel so set dma channels to:-

36  sai1_a_dma
37  sai1_b_dma
38  sai2_a_dma
39  sai2_b_dma  
or from FS Line on GPIO

as appropriate and trigger DMA on FS Line within the SAI registers

"The precise hardware triggers for sorting data into Slot A (Left) and Slot B (Right) are the electrical transitions (edges) of the Frame Synchronization (FS / WS) line, possibly combined with a bit-clock (BCLK) counter"

so there is need to choose the peripheral for DMA channel (maybe a Ch 0  and b Ch 1).

In STM32 a circular buffer is just a DMA transfer that never stops, it keeps reloading the base address from the linked list, increments as set, reaches the end address and reloads the  base again from the linked list.  The linked list is just a series of instructions to the DMA channel telling it what to reload. Unexpectedly to stop the DMA an extra item is needed in the linked list (simply 0x00).  You can see it here in my code :-

.word 0x8000; .word 0x00  //linked list 2

where  to transfer one 64KB block the Linked List has to reload 0x8000 (the DMA cant do 0x10000 bytes it does two lots of 0x8000) and at the end reloads 0x00 to stop the transfer.

Here are two images of my complete app it sets up four DMA channels as follows

a) DMA to read 4 x 32 bit words from RX FIFO 

b) DMA to write 4 x 32 bit words into TX FIFO 

c) DMA to read words from RX FIFO into a 64KB ring

d) DMA to write words into TX FIFO from another 64KB ring

FIFO holds 4 words and I rely on the RXD flag (RX FIFO full) to initialise and sync DMA 

Here are the two images 

 

The DMA control registers setup by using a lookup table. The label saying Hardware Triggers is Just selecting SPI1 

The Linked List can be told by settings in the GPDMA_CxLLR register what to do:-


Bit 31 UT1: Update GPDMA_CxTR1 from memory
This bit controls the update of GPDMA_CxTR1 from the memory during the link transfer.
0: no GPDMA_CxTR1 update
1: GPDMA_CxTR1 update

Bit 30 UT2: Update GPDMA_CxTR2 from memory
This bit controls the update of GPDMA_CxTR2 from the memory during the link transfer.
0: no GPDMA_CxTR2 update
1: GPDMA_CxTR2 update

Bit 29 UB1: Update GPDMA_CxBR1 from memory
This bit controls the update of GPDMA_CxBR1 from the memory during the link transfer.
If UB1 = 0 and if GPDMA_CxLLR ≠ 0, the linked-list is not completed.
GPDMA_CxBR1.BNDT[15:0] is then restored to the programmed value after data transfer is
completed and before the link transfer.
0: no GPDMA_CxBR1 update from memory (GPDMA_CxBR1.BNDT[15:0] restored if any
link transfer)
1: GPDMA_CxBR1 update

Bit 28 USA: update GPDMA_CxSAR from memory
This bit controls the update of GPDMA_CxSAR from the memory during the link transfer.
0: no GPDMA_CxSAR update
1: GPDMA_CxSAR update

Bit 27 UDA: Update GPDMA_CxDAR register from memory
This bit is used to control the update of GPDMA_CxDAR from the memory during the link
transfer.
0: no GPDMA_CxDAR update
1: GPDMA_CxDAR update

You have to look to the SAI registers to tell DMA channels to trigger on the FS Line.

If you still get swapping on startup check how you trigger the DMA channels on FS Line.

Hope the above helps to give you some low level context for what HAL is doing.  

“HAL SAI has HAL_SAI_TxHalfCpltCallback and HAL_SAI_TxCpltCallback to signal which side is ready” 

I would check very carefully what HAL is actually doing here - how it knows,  I didn’t look it up but I’m suspicious of this “TxHalf” because in SPI there is the concept of FIFO half full (sets a flag) and FIFO full (sets a different flag so check you have the right HAL abstractions for watching FS.


“When RXWNE = 0 and data size is set up to 16 bits, the value gives number of remaining
data frames persisting at RxFIFO”
“00: no next frame is available at RxFIFO
01: 1 frame is available
10: 2 frames are available
11: 3 frames are available”

 

 

PS  ​@LCE  ​@Saket_Om  I agree with flushing buffers if you mean SAI FIFO I would suggest the sequence would be:- 

  1. hold the Codek off
  2. Initialise the SAI and the DMA (from reset)
  3. Enable the DMA
  4. Enable the SAI 

My experience with SPI is that after reset , disable, enable the SPI FIFO’s are empty. Real transfers will flush the ring buffers as soon as it starts, DMA will start from its base address.  There is I suppose the possibility that some timing problem can occur as the first transfer takes place but if the system works properly data is always deterministically in the right place.

Good luck.

 

 

 

 

 

 

Bill's Notes