Skip to main content
Zaher
Senior II
May 25, 2023
Question

I2S DMA in NORMAL mode for double-buffering, not halving a single one!

  • May 25, 2023
  • 11 replies
  • 7164 views

Hello everyone!

I was wondering what might go wrong with I2S DMA configured in normal mode for audio streaming with double-buffering technique. I'm not talking about circular buffer here (ping-pong) where a single buffer is used in halves in HC and TC interrupts.

I have spent quite some time trying to figure it out, but seems the codec doesn't output any audio when DMA is in 'NORMAL' mode. The init sequence of codec doesn't work, as well, which indicates the first 16 dummy bytes that must be sent for the codec to set/initialize the registers never make it to the codec.

The only thing that comes to my mind now are the HC/TC IRQs, their callbacks, and whether they were registered when DMA is configured in 'NORMAL' mode.

 This is for STM32F413 + WM8994

This topic has been closed for replies.

11 replies

LCE
Principal II
May 25, 2023

Wasn't there something that DBM only works in CIRCULAR mode? Which makes sense, because NORMAL mode means that the DMA stops when the first buffer + length given to it is done.

From RM0090:

"A double-buffer stream works as a regular (single buffer) stream with the difference that it

has two memory pointers. When the Double buffer mode is enabled, the Circular mode is

automatically enabled (CIRC bit in DMA_SxCR is don’t care) and at each end of transaction,

the memory pointers are swapped."

In case you are using a buffer chain, you could use the half-complete callback to set a new buffer address to the currently unused M0 / M1 register and so on.

Anyway, check that the init sequence is correct (old Cube error, basic DMA init / clock enable must happen before peripheral DMA init) and that interrupts are enabled.

And I'm not sure how current HAL version handles DBM, when I started using that it wasn't really working, so I built my own I2S / SAI DMA with DBM start function.

Zaher
ZaherAuthor
Senior II
May 25, 2023

I was just looking into that, I mean the DBM mode (DMA_SxCR_DBM). I didn't know this was supported in hardware or HAL, so I was trying to implement it in the program. My current HAL seems to have 'HAL_DMAEx_MultiBufferStart()' but not sure if this is the way to do it with I2S. I was trying dozens of scenarios, switching buffer pointers like you pointed out, using BSP_AUDIO_OUT_ChangeBuffer(). and so on, but none worked out for me.

Zaher
ZaherAuthor
Senior II
May 25, 2023

I found this in the HAL DMA driver:

 /* Disable the half transfer interrupt if the DMA mode is not CIRCULAR */
 if((hdma->Instance->CR & DMA_SxCR_CIRC) == RESET)
 {
 /* Disable the half transfer interrupt */
 hdma->Instance->CR &= ~(DMA_IT_HT);
 }
 
 /* Disable the transfer complete interrupt if the DMA mode is not CIRCULAR */
 else
 {
 if((hdma->Instance->CR & DMA_SxCR_CIRC) == RESET)
 {// Rest of the block has been omitted}
 

When DMA is configured in 'NORMAL' mode, these interrupts never fire. I know that in NORMAL mode, you will have to switch buffers and initiate the transaction over and over again. unlike the case with ping-pong buffer. But why HC/TC interrupts are disabled when DMA is in NORMAL mode?

LCE
Principal II
May 25, 2023

Well, just try circular mode.

And as I said below, start simpler, just one sine buffer output in CIRCULAR mode, maybe use the HC/TC interrupts to toggle a pin or count just to check.

LCE
Principal II
May 25, 2023

I would start with a simple sine buffer in circular mode, no DBM yet.

If you then get I2S data output, then start working on the DBM mode.

I just checked my code, I'm actually using "HAL_DMAEx_MultiBufferStart_IT()", I just build my own I2S / SAI start function around that.

And there were some manual DBM enable settings I did in DMA peripheral init, but I'm not sure if that is still needed with current HAL_DMAEx_MultiBufferStart_IT:

From my I2S init, as I said, that might be redundant...

DMA_Stream_TypeDef *pDmaStream;
uint32_t u32RegTemp = 0;
 
/* DBM - double buffer mode activation
		- AFTER LINK to DMA!
		- BEFORE DMA enable!
	 */
		pDmaStream = (DMA_Stream_TypeDef *)hDMA_I2S_1.Instance;
 
		u32RegTemp 		= pDmaStream->CR;
		pDmaStream->CR 	&= ~DMA_SxCR_EN;
		pDmaStream->CR 	|= DMA_SxCR_DBM;
		u32RegTemp 		&= DMA_SxCR_EN;
		pDmaStream->CR 	|= u32RegTemp;
		/* CT - current buffer reset */
		pDmaStream->CR 	&= ~DMA_SxCR_CT;

Zaher
ZaherAuthor
Senior II
May 25, 2023

Audio is working fine in circular mode. I have several players working, as well as, a sine wave example. I just wanted to switch to double-buffer (two separate buffers) because I believe this makes things better when you process one large buffer while the other one is sent to the codec using DMA.

I will try to do the same in my I2S DMA init and see what else needs to be changed. I believe your code does exactly what I'm looking for.

LCE
Principal II
May 25, 2023

Okay, at least something...

Here's my I2S Rxstarter:

  • DMA set to circular
  • for RX
  • same callback for M0 and M1 HC / TC
uint8_t I2sRxDmaDbmStart(I2S_HandleTypeDef *phI2S, uint8_t *pu8DataM0, uint8_t *pu8DataM1, uint16_t u16Size)
{
	uint8_t u8RetVal = HAL_OK;
 
	if( (pu8DataM0 == NULL) || (u16Size == 0) )
	{
		uart_printf("\n\r# ERR: I2sRxDmaDbmStart(): (pu8DataM0 == NULL) || (u16Size == 0)\n\r");
		return HAL_ERROR;
	}
 
	if( phI2S->State != HAL_I2S_STATE_READY )
	{
		uart_printf("\n\r# ERR: I2sRxDmaDbmStart(): State != READY\n\r");
		return HAL_BUSY;
	}
 
	/* set state and reset error code */
	phI2S->State 	= HAL_I2S_STATE_BUSY_RX;
	phI2S->ErrorCode 	= HAL_I2S_ERROR_NONE;
	phI2S->pRxBuffPtr 	= (uint16_t *)pu8DataM0;
	phI2S->RxXferSize 	= u16Size;
	phI2S->RxXferCount	= u16Size;
 
	/* init field not used in handle to zero */
	phI2S->pTxBuffPtr 	= NULL;
	phI2S->TxXferSize 	= 0;
	phI2S->TxXferCount	= 0;
 
/* set the I2S Rx DMA half/complete transfer callbacks - for first buffer M0 */
	phI2S->hdmarx->XferHalfCpltCallback 	= I2sDmaRxHalfCpltCB;
	phI2S->hdmarx->XferCpltCallback 		= I2sDmaRxCpltCB;
 
/* set I2S Rx DMA callbacks for DOUBLE buffer mode, DBM bit set in DMA->CR */
	/* -> same callbacks as for M0 */
	/* Set the M1 I2S Rx DMA half/complete transfer callbacks */
	phI2S->hdmarx->XferM1HalfCpltCallback 	= I2sDmaRxHalfCpltCB;
	phI2S->hdmarx->XferM1CpltCallback 		= I2sDmaRxCpltCB;
 
	/* set the DMA error callback */
	phI2S->hdmarx->XferErrorCallback 		= I2sDmaErrorCB;
	/* no DMA Rx abort callback */
	phI2S->hdmarx->XferAbortCallback 		= NULL;
 
/* enable the Rx DMA Stream: interrupt & double buffer mode */
	u8RetVal = HAL_DMAEx_MultiBufferStart_IT(phI2S->hdmarx,
												(uint32_t)&phI2S->Instance->RXDR,
												(uint32_t)pu8DataM0,
												(uint32_t)pu8DataM1,
												(uint32_t)u16Size);
	if( u8RetVal != HAL_OK )
	{
		/* set I2S error code */
		phI2S->ErrorCode |= HAL_I2S_ERROR_DMA;
		phI2S->State = HAL_I2S_STATE_READY;
		uart_printf("\n\r# ERR: I2S HAL_DMAEx_MultiBufferStart_IT(), ErrorCode = %08lX\n\r", phI2S->ErrorCode);
		return u8RetVal;
	}
 
	/* enable Rx DMA Request */
	phI2S->Instance->CFG1 |= SPI_CFG1_RXDMAEN;
 
	/* enable I2S peripheral */
	phI2S->Instance->CR1 |= SPI_CR1_SPE;
 
	/* start the transfer */
	phI2S->Instance->CR1 |= SPI_CR1_CSTART;
 
	return u8RetVal;
}

Zaher
ZaherAuthor
Senior II
May 25, 2023

Thank you very much for sharing this. So, even with DBM, the DMA is still configured in circular mode? Another issue is, I didn't find much info about DBM in the RM of my device. According to several resources, this feature is only available on the following series:

  1. STM32F427/437/429/439 (F4)
  2. STM32F746/756
  3. STM32F767/769/777/779
  4. STM32F446
  5. STM32F769/779
  6. STM32H745/755/747/757
  7. STM32H750/753/755/757
  8. STM32H7A3/7B3/7A1/7B1
  9. STM32G474/484/491/4A1
  10. STM32G4B1/4B2 series

Though not really sure about this list.

Zaher
ZaherAuthor
Senior II
May 25, 2023

Sorry for confusion. I was trying to quickly search RM with the wrong keyword, but when I read the entire section related to the DMA, I found the following:

Double-buffer mode

This mode is available for all the DMA1 and DMA2 streams. The double-buffer mode is enabled by setting the DBM bit in the DMA_SxCR register. A double-buffer stream works as a regular (single buffer) stream with the difference that it has two memory pointers. When the double-buffer mode is enabled, the circular mode is automatically enabled (CIRC bit in DMA_SxCR is not relevant) and at each end of transaction, the memory pointers are swapped.

So, if the circular mode is the default mode when DBM is enabled, the pointers circulate between beginning and end of a single buffer in a ping-pong fashion or between two distinct buffers?

Zaher
ZaherAuthor
Senior II
May 25, 2023

I was wondering if LL driver generation is available in Cube for the STM32F413. Although I get used to HAL for quite some years now, I want to start playing with this I2S DMA thing at register level. At least, this seems easier to understand what's going on. Or maybe a mix of HAL/LL to get the job done.

Zaher
ZaherAuthor
Senior II
May 27, 2023

I got it! It's working flawlessly. I can confirm now, DMA streaming for I2S is working in double-buffer mode using two separate buffers, waveform1/waveform2, on STM32F413ZHT. I have populated these buffers with sine waves of different frequencies, 440/220 HZ respectively, so I can distinguish them. The DMA controller is alternating between first buffer and second buffer very smoothly. I did not utilize any interrupts for now to keep it simple.

In case someone is interested in doing the same, here's my DMA setup code:

/*
 * Configure the DMA Stream for I2S TX to work in double-buffer mode
 */
void DMA_DBM_Setup()
{
	static DMA_Stream_TypeDef* DMA_Stream = DMA1_Stream4;
	// Disable the DMA stream first
	DMA_Stream->CR &= ~DMA_SxCR_EN;
	// Wait until the stream is disabled
	while(DMA_Stream->CR & DMA_SxCR_EN);
 
	// Assign I2S2 DR to DMA stream
	DMA_Stream->PAR = (uint32_t)&haudio_i2s.Instance->DR;
 
	// Configure source audio buffers used in DBM
	DMA_Stream->M0AR = (uint32_t)waveform1;
	DMA_Stream->M1AR = (uint32_t)waveform2;
 
	// Set the length/size of the DATA stream in
	DMA_Stream->NDTR = SINE_LENGTH;
 
	DMA_Stream->CR |= (0 << DMA_SxCR_CHSEL_Pos); // DMA channel selection
	DMA_Stream->CR &= ~DMA_SxCR_MBURST; // Set single memory burst
	DMA_Stream->CR &= ~DMA_SxCR_PBURST; // Set single peripheral burst
	DMA_Stream->CR &= ~DMA_SxCR_CT; 	// Set memory to peripheral mode
	DMA_Stream->CR &= ~DMA_SxCR_PL_1; 	// Set DMA priority to high
	DMA_Stream->CR &= ~DMA_SxCR_PINCOS; // Set peripheral increment offset size to 32-bit
	DMA_Stream->CR |= DMA_SxCR_MINC; 	// Enable memory increment mode
	DMA_Stream->CR &= ~(DMA_SxCR_PINC); // Disable peripheral increment mode
	DMA_Stream->CR |= DMA_SxCR_MSIZE_0; // Set memory data size to 16-bit
	DMA_Stream->CR |= DMA_SxCR_PSIZE_0; // Set peripheral data size to 16-bit
	DMA_Stream->CR |= DMA_SxCR_CIRC; 	// Enable circular mode
	DMA_Stream->CR |= (DMA_SxCR_DBM); // Enable double buffer mode
	DMA_Stream->CR |= (DMA_SxCR_TCIE | DMA_SxCR_HTIE); // Enable TC and HC interrupts
 
	/* Enable DMA stream */
	DMA_Stream->CR |= DMA_SxCR_EN;
}

Zaher