cancel
Showing results for 
Search instead for 
Did you mean: 

STM MCUs: how to sync Audio IN and OUT?

tjaekel
Lead

This is more an experience report.
I want to get audio from an INput source and forward to an OUTput sink. And both should stay in sync "forever" (I do not want to have a clock drift, I do not want to have the condition to skip a sample or to add one, just because of a slightly different timing (different clock frequencies)).

My use case:
STM32U5A5 with ADF for a PDM MIC as INput and SAI2 as SPDIF OUTput.
I use 48KHz audio Fs (PDM MIC is with decimation 64, so 3.072 MHz PDM MIC clock).

ADF runs in DMA mode (cyclic double buffer) but SPDIF (SAI2) runs in INT mode (single buffer).
This means: the ADF fills seamlessly the result buffer, without any software overhead, but the SAI2 as SPDIF out needs all the time periodic interrupts and the callback called and restarting the next INT.

Example:

void HAL_SAI_TxCpltCallback(SAI_HandleTypeDef *hsai)
{
  HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
  if (hsai->Instance == SAI2_Block_B)
  {
#ifdef SPDIF_TEST
	  extern int32_t SPDIF_out_test[2 * 192];
	  HAL_SAI_Transmit_IT(&hsai_BlockB1, (uint8_t *)SPDIF_out_test, (uint16_t)(sizeof(SPDIF_out_test) / sizeof(uint32_t)));
#else
	  ////ModifySPDIFOut();
	  if (ADF_GetADFState())
		  ADF_GetToOutBuffer();			/* stay in sync with the ADF DMA */
	  HAL_SAI_Transmit_IT(&hsai_BlockB1, (uint8_t *)SAIRxBuf, (uint16_t)(sizeof(SAIRxBuf) / sizeof(uint32_t)));
#endif
  }
}

See: on every finished INT transfer - the CpltCallback is called which starts again the INT again with HAL_SAI_Transmit_IT(). This "takes time" (see later).

It was not properly working in the first run: I got a lot of distortions.

The debug was this:

  • toggle two GPIO output pins: one in the callback for the INput process and another one in the OUTput process
  • compare (display) both with a scope
  • and I got two pulse trains which where drifting: not the exact same frequency! "Not locked".

scope_timing.png

I saw:

  • the INput (ADF) was slightly faster as the OUTput
  • the pulses were permanently drifting in relation to each other ("not locked")

Next step: check and make sure, both instances (ADF and SAI2) use the same clock source. At best: use a PLL with the same clock input and grab the peripheral clocks for both device from the same PLL:

    PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_ADF1;
#if 1
    PeriphClkInit.Adf1ClockSelection = RCC_ADF1CLKSOURCE_PLL3;	//RCC_ADF1CLKSOURCE_MSIK;

    /* the same as in HAL_OSPI_MspInit(), actually done already */
    PeriphClkInit.PLL3.PLL3Source = RCC_PLLSOURCE_HSE;
    PeriphClkInit.PLL3.PLL3M = 1;

    /* 48 KHz SPDIF            with scope/debug 	CubeMX cfg      correct */
    PeriphClkInit.PLL3.PLL3N = 36;					//36;			36;
    PeriphClkInit.PLL3.PLL3P = 24;					//96; 			24;		//SAI2
    PeriphClkInit.PLL3.PLL3Q = 24;											//ADF1 - see: OutputClock.Divider
    PeriphClkInit.PLL3.PLL3R = 2;
    PeriphClkInit.PLL3.PLL3RGE = RCC_PLLVCIRANGE_1;
    PeriphClkInit.PLL3.PLL3FRACN = 7080;			//7080			7078; -->
    // it results in 12,288.004 KHz in FW - MCLKDIV should be 2 at the end - but it fails!
    /* but: above 12,288 KHz results in MCLKDIV +1, too large! - so, we have to get a bit below "nominal", resulting in a larger clock offset error:
     * -36Hz, compared to value 7078 with just 4 Hz error!
     * why I cannot use 7078 with 12,288.004 KHz which would be better? The MCLKDIV is wrong by +1!
     */
    PeriphClkInit.PLL3.PLL3ClockOut = RCC_PLL3_DIVQ;
#else
    PeriphClkInit.Adf1ClockSelection = RCC_ADF1CLKSOURCE_MSIK;
#endif

This raises one question:

Can I assume, when I take PLLxP and PLLxQ from the same PLL, feeding the PLL by the same input clock, that the output clocks are "in sync"?

OK, maybe not with the same phase, but with the "same" output frequency. So, I might have a phase offset between both audio devices, but I can handle. As long as they are not drifting anymore - all is fine.

Also not sure what happens inside an audio peripheral, e.g. the ADF: it can have "clock synchronizers" and they could also add a phase difference, potentially also a drift (? not sure, hopefully not!).

As long as there is a constant phase offset or a small amount of jitter (within tolerance when it comes to handle samples and sample buffers) - all fine. What I do not want to have is: a "clock drift".
And this happens for sure, when ADF for instance runs with clock from the MSIK (two different clock sources now).

OK, PLL3P for SAI2 (SPDIF out) and PPL3Q for ADF (PDM MIC in). But still drifting!

Next step: how large is the "drift" (frequency offset)? It was very small (not in the range of a divider). Just for a test I tried this:

Make the buffer for the ADF (PDM MIC input), which was a bit faster, a tiny bit larger (one sample more). And what a surprise: now they GPIO test signals were locked! Perfectly in sync, no drift anymore.
But: "why do I need one more sample for input to make IN and OUT in sync?" And it is not really nice, because I had to skip one sample from IN, the OUT is not really seamless anymore.

"Hmmmm, scratching my head..."
I came up with this suspicion: the OUT runs in INT mode: on every callback I have to kick off again the INT.
But this function call to do (see the HAL_SAI_Transmit_IT() done in the CpltCallback), takes time. There is maybe a gap in the seamless audio streaming, the next INT starts a bit too late. This causes my clock drift.

YES!!!! It does!

The drift in the debug signals comes from the SW overhead, the time needed to start the INT again.

How to fix?
"Hmmm, start this callback and next INT a bit earlier, to make a seamless audio transfer".
Et voila: there is the FiFoThreshold, when the callback should be triggered. If this is set for "FifoEmpty" - it is potentially "too late" to get a seamless audio flow.

So, I have changed this:

  hsai_BlockB1.Init.FIFOThreshold = SAI_FIFOTHRESHOLD_1QF;	//SAI_FIFOTHRESHOLD_EMPTY;

This triggers the callback for the SAI2 (SPDIF out) a bit earlier. The SAI2 can still send a bit from FIFO but it I have time to restart the INT.

If I wait for _EMPTY - the call to start INT again has to be done (which takes time) - it creates a gap in the audio stream.

So, this FIFOThreshold changed has fixed the problem. Now, all runs fine and the "trick" with the slightly larger INput buffer is not needed anymore. I was right at the edge, that the output was one sample too late and setting the trigger a bit earlier has solved the problem.

Have fun with audio, esp. how to synchronize IN and OUT. Be smart on debugging the audio timing, to think about the system (e.g. what are the clock sources and how they are "synchronized" with each other).

My project is here:
https://github.com/tjaekel/NUCLEO-U5A5JZ-Q_QSPI 

 

1 ACCEPTED SOLUTION

Accepted Solutions
tjaekel
Lead

Can I assume, when taking two outputs from same PLL, that these clocks are "in sync"? (at least a fix, constant clock frequency relation)

View solution in original post

1 REPLY 1
tjaekel
Lead

Can I assume, when taking two outputs from same PLL, that these clocks are "in sync"? (at least a fix, constant clock frequency relation)