Skip to main content
Visitor II
June 13, 2026
Question

How to properly set dual ADC with one DMA stream for each?

  • June 13, 2026
  • 4 replies
  • 52 views

Hello everyone!

I will try to be short here… even though I will probably fail on this task.



Hardware: ST-Nucleo-STM32F446RE


I have a 96kHz loop, which happens inside a callback from TIM3. I also have 4 analog signals to be sampled.

 

Originally, I have set these signals into ADC1 (CH6 and CH7) and ADC2 (CH15 and CH9). Due to the ~10.4us period of time I have inside my callback, considering 168MHz system clock and 21MHz ADC clock (System Clock /2 /4), I need to act quick.

 

I have decided to use DMA for this application: DMA2 Stream 0 assigned to ADC1 and DMA Stream 2 assigned to ADC2. Then, inside the loop, I am simply assigning the buffer values to variables (or, if I need to squeeze a few more cycles, I can use them directly).

 

However, I noticed the variables assigned to ADC2 would not be sampled correctly: the first channel would be sampled just once, at the startup, and the second never, while the two channels on ADC1 would behave well. In my code, var3 and var4, respectively.

 

The most relevant pieces of my code are the following:

 

Before main():

/* Private variables ---------------------------------------------------------*/
ADC_HandleTypeDef hadc1;
ADC_HandleTypeDef hadc2;
DMA_HandleTypeDef hdma_adc1;
DMA_HandleTypeDef hdma_adc2;
[...]

 

/* USER CODE BEGIN PV */
__IO uint32_t adc1_buf[2];
__IO uint32_t adc2_buf[2];
[...]

 

Inside of main ():

  /* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART2_UART_Init();
MX_ADC1_Init();
MX_ADC2_Init();
MX_TIM3_Init();
/* USER CODE BEGIN 2 */
HAL_TIM_Base_Start_IT(&htim3);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_2);
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc1_buf, 2);
HAL_ADC_Start_DMA(&hadc2, (uint32_t*)adc2_buf, 2);
[...]

 

ADC1 and ADC2 definitions - a little long, sorry, but this is the one auto-generated by CubeMX:

/**
* @brief ADC1 Initialization Function
* @param None
* @retval None
*/
static void MX_ADC1_Init(void)
{

/* USER CODE BEGIN ADC1_Init 0 */

/* USER CODE END ADC1_Init 0 */

ADC_ChannelConfTypeDef sConfig = {0};

/* USER CODE BEGIN ADC1_Init 1 */

/* USER CODE END ADC1_Init 1 */

/** Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion)
*/
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = ENABLE;
hadc1.Init.ContinuousConvMode = DISABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING;
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 2;
hadc1.Init.DMAContinuousRequests = ENABLE;
hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
if (HAL_ADC_Init(&hadc1) != HAL_OK)
{
Error_Handler();
}

/** Configure for the selected ADC regular channel its corresponding rank in the sequencer and its sample time.
*/
sConfig.Channel = ADC_CHANNEL_6;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_15CYCLES;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}

/** Configure for the selected ADC regular channel its corresponding rank in the sequencer and its sample time.
*/
sConfig.Channel = ADC_CHANNEL_7;
sConfig.Rank = 2;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN ADC1_Init 2 */

/* USER CODE END ADC1_Init 2 */

}

/**
* @brief ADC2 Initialization Function
* @param None
* @retval None
*/
static void MX_ADC2_Init(void)
{

/* USER CODE BEGIN ADC2_Init 0 */

/* USER CODE END ADC2_Init 0 */

ADC_ChannelConfTypeDef sConfig = {0};

/* USER CODE BEGIN ADC2_Init 1 */

/* USER CODE END ADC2_Init 1 */

/** Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion)
*/
hadc2.Instance = ADC2;
hadc2.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc2.Init.Resolution = ADC_RESOLUTION_12B;
hadc2.Init.ScanConvMode = ENABLE;
hadc2.Init.ContinuousConvMode = DISABLE;
hadc2.Init.DiscontinuousConvMode = DISABLE;
hadc2.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING;
hadc2.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO;
hadc2.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc2.Init.NbrOfConversion = 2;
hadc2.Init.DMAContinuousRequests = ENABLE;
hadc2.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
if (HAL_ADC_Init(&hadc2) != HAL_OK)
{
Error_Handler();
}

/** Configure for the selected ADC regular channel its corresponding rank in the sequencer and its sample time.
*/
sConfig.Channel = ADC_CHANNEL_15;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_84CYCLES;
if (HAL_ADC_ConfigChannel(&hadc2, &sConfig) != HAL_OK)
{
Error_Handler();
}

/** Configure for the selected ADC regular channel its corresponding rank in the sequencer and its sample time.
*/
sConfig.Channel = ADC_CHANNEL_9;
sConfig.Rank = 2;
if (HAL_ADC_ConfigChannel(&hadc2, &sConfig) != HAL_OK)
{
Error_Handler();
}
/* USER CODE BEGIN ADC2_Init 2 */

/* USER CODE END ADC2_Init 2 */

}

 

DMA definition, also from CubeMX:

/**
* Enable DMA controller clock
*/
static void MX_DMA_Init(void)
{

/* DMA controller clock enable */
__HAL_RCC_DMA2_CLK_ENABLE();

/* DMA interrupt init */
/* DMA2_Stream0_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn);
/* DMA2_Stream2_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA2_Stream2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA2_Stream2_IRQn);

}

 

And then, in the callback from TIM3,

/* USER CODE BEGIN 4 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM3)
{

// Start measuring time
start_time = DWT->CYCCNT;

HAL_GPIO_WritePin(EN_A_GPIO_Port, LED_Pin, GPIO_PIN_SET);

start_time_ADC = DWT->CYCCNT;
// Read ADCs from DMA
var1 = adc1_buf[0]; // rank 1, IN6
var2 = adc1_buf[1]; // rank 2, IN7
var3 = adc2_buf[0]; // rank 1, IN15
var4 = adc2_buf[1]; // rank 2, IN9
//
elapsed_time_ADC = DWT->CYCCNT - start_time_ADC;

[...]

 

Hopefully the code is more readable than screenshots from CubeMX - at least it is in my opinion!

 

Some troubleshooting:

  • Disabling ADC1 and DMA2 Stream 0 (therefore having var1 and var2 stuck at 0) allows the two channels on ADC2 to behave normally.
  • Changing the ADC EOC flag from individual to all channels did not change anything.
  • Changing the DMA priorities (setting them the same) did not change anything.
  • Getting rid of the DMAs and doing the sampling inside the TIM3 callback: it works… ish. Sadly, along with the rest of the code inside the callback, there is simply not enough time to do everything. All 4 values are sampled, but the overrun is massive, and my loop drops to an erratic 65ishkHz. Due to the 12 bit nature of the sampling, I cannot take them down to 3 cycles, and even the minimum 15 cycles for each already causes a race condition. And since two of these signals have a high-ish impedance, they don’t behave well at 15 cycles, even though the values are updated.

 

My current workaround:

I have assigned all 4 analog signals to ADC1, however I cannot increase their sampling time. For now, my project works fine-ish, but as it evolves, I will need to solve this issue, since all 4 channels are already reaching about 9.4us - while my loop is at about 10.4us.

 

Problems I have discarded:

  • Hardware: since the ADC2 channels sample fine when ADC1 is removed, I have discarded the possibility some of my hardware is damaged. Also, when debugging, var4 is always at 0 with a black background, meaning it was never updated. var3 has a value that is correct to whatever the signal was when the program started, but it is completely static, not the typical “something is damaged noisy mess”.
  • TIM3 triggering before the ADCs can finish their thing: TIM3 triggers each 1/96kHz ≈ 10.4us (verified with an oscilloscope probing the on-board green LED signal) and the ADCs take 15+15+84+84 = 198 ADC cycles. At 168MHz/2/4 = 21MHz, this is 198/21MHz ≈ 9.4us. So a hair left still (please, correct me if I am wrong).

 

Current hypothesis:

Maybe both ADCs/DMAs are being triggered at the same time, by TIM3, and the MCU has to prioritise ADC1 and ignore ADC2?

 

As I said, having two ADCs (2 channels each) with one DMA stream for each does not work. Shoving all signals into a single ADC (4 channels) works, though, so I am convinced my monkey brain cannot figure out how to properly do this DMA stuff - if someone can shine a light here, I truly appreciate!

 

Also, since we are already here, I would like to ask a relevant question: is having my 4 signals split into 2 ADCs with a DMA stream for each quicker than one ADC and one DMA? Can the ADCs and DMA streams run in parallel, out of the CPU?

 

Best regards, and thank you for your time!

4 replies

Visitor II
June 14, 2026

The ADC sampling time is the cycles to charge up the sample and hold capacitor to the input voltage. 15 or so conversion cycles are then needed after that for each sample.

2 ADCs will convert faster than 1 if you run them in parallel.

Visitor II
June 14, 2026

Apparently the edit function disappears after a while, so add this to my comments:

For the sampling times you have, start ADC DMA on ADC2, then on ADC1. Test for DMA EOC on ADC1, and save these two values when complete. Then test for DMA EOC on ADC2 and save those two when complete. That is the fastest you will get. 8 bit conversions on ADC2 will run 8 cycles faster.

MasterT
Lead II
June 15, 2026

DMA2 Stream 0 assigned to ADC1 and DMA Stream 2 assigned to ADC2.

Do you mean  DMA-2 Stream 2 assigned to ADC2 ?

Post DMA settings, circular mode ?

You don’t have to activate DMA interrupt, since reading in the TIM interrupt (normally it’s done in Half Transfer and Complete Transfer of the DMA interrupt). Ether  way ‘d works, no need for both TIM and DMA interrupt. But if you have it active, check if its run properly driving GPIO like:

void DMA1_Stream6_IRQHandler(void)
{
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, GPIO_PIN_SET);
HAL_DMA_IRQHandler(DacHandle.DMA_Handle2);
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, GPIO_PIN_RESET);
}

Strange to see 168 MHz in F446, default 180.

168 is on G474re

question: is having my 4 signals split into 2 ADCs with a DMA stream for each quicker than one ADC and one DMA? Can the ADCs and DMA streams run in parallel, out of the CPU?

DMA was designed to run w/o CPU. It’d not be “faster”, same things. Dual ADC makes sense if max sampling rate required, 3-5 msps - not your case with 100-x2 -200 ksps only.

Richard Li
Senior
June 15, 2026

The DMA2 interrupt only initially and enable, looks no code handle interrupt, as 

MasterT gave the code, at least , you need clear the flag, otherwise, interrupt continual coming, system will crush.

Maybe this way your one channel got one time work, after interrupt fire flag, it totally wrong.