cancel
Showing results for 
Search instead for 
Did you mean: 

STM32H5 ADC DMA HAL_ADC_ConvCpltCallback Called Early

ricky-fleet
Associate II

I used the STM32H533 Nucleo board example project ADC_MultiChannelSingleConversion as a reference for this code. The actual project is running on an STM32H523.

I am attempting to have a software-triggered array of ADC reads across two channels using the GPDMA. [Edit: for clarification, multiple conversions across a sequence of 2 channels, not just a single conversion]. The plan is to be alerted via interrupt callback that the sequence (triggered by HAL_ADC_Start_DMA) is complete. I do not want the ADC to continue to sample or overwrite the data in the buffer, so I am turning off the ADC in the callback. 

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
  HAL_ADC_Stop_DMA(hadc);
  ...
}

The issue is that this callback is being triggered after every two-channel sequence is completed, instead of once after the DMA buffer is filled.

The system does not generate any DMA complete or half complete callbacks in this configuration.

Here's what appears to be working:

  • ADC conversions are triggered correctly
  • DMA is used to move data from ADC peripheral to memory correctly
  • Continuous conversions work as intended, only requiring one software start

Here is the relevant code with most boilerplate CubeMX comments removed for brevity:

void MX_ADC1_Init(void) {
  ADC_ChannelConfTypeDef sConfig = {0};

  hadc1.Instance = ADC1;
  hadc1.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV1;
  hadc1.Init.Resolution = ADC_RESOLUTION_12B;
  hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
  hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE;
  hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV;
  hadc1.Init.LowPowerAutoWait = DISABLE;
  hadc1.Init.ContinuousConvMode = ENABLE;
  hadc1.Init.NbrOfConversion = 2;
  hadc1.Init.DiscontinuousConvMode = DISABLE;
  hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
  hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
  hadc1.Init.DMAContinuousRequests = ENABLE;
  hadc1.Init.SamplingMode = ADC_SAMPLING_MODE_NORMAL;
  hadc1.Init.Overrun = ADC_OVR_DATA_PRESERVED;
  hadc1.Init.OversamplingMode = DISABLE;
  if (HAL_ADC_Init(&hadc1) != HAL_OK) {
    Error_Handler();
  }

  __HAL_ADC_ENABLE_IT(&hadc1, ADC_IT_EOC | ADC_IT_OVR);

  sConfig.Channel = ADC_CHANNEL_0;
  sConfig.Rank = ADC_REGULAR_RANK_1;
  sConfig.SamplingTime = ADC_SAMPLETIME_92CYCLES_5;  // TODO: make shorter
  sConfig.SingleDiff = ADC_SINGLE_ENDED;
  sConfig.OffsetNumber = ADC_OFFSET_NONE;
  sConfig.Offset = 0;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) {
    Error_Handler();
  }

  sConfig.Channel = ADC_CHANNEL_1;
  sConfig.Rank = ADC_REGULAR_RANK_2;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) {
    Error_Handler();
  }
}
void HAL_ADC_MspInit(ADC_HandleTypeDef* adcHandle) {
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  DMA_NodeConfTypeDef DMA_NodeConfig;
  if (adcHandle->Instance == ADC1) {
    LL_RCC_SetADCDACClockSource(LL_RCC_ADCDAC_CLKSOURCE_HCLK);

    HAL_RCC_ADC_CLK_ENABLED++;
    if (HAL_RCC_ADC_CLK_ENABLED == 1) {
      __HAL_RCC_ADC_CLK_ENABLE();
    }

    __HAL_RCC_GPIOA_CLK_ENABLE();
    /**ADC1 GPIO Configuration
    PA0     ------> ADC1_INP0
    PA1     ------> ADC1_INP1
    */
    GPIO_InitStruct.Pin = GPIO_PIN_0 | GPIO_PIN_1;
    GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    DMA_NodeConfig.NodeType = DMA_GPDMA_LINEAR_NODE;
    DMA_NodeConfig.Init.Request = GPDMA2_REQUEST_ADC1;
    DMA_NodeConfig.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST;
    DMA_NodeConfig.Init.Direction = DMA_PERIPH_TO_MEMORY;
    DMA_NodeConfig.Init.SrcInc = DMA_SINC_FIXED;
    DMA_NodeConfig.Init.DestInc = DMA_DINC_INCREMENTED;
    DMA_NodeConfig.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_HALFWORD;
    DMA_NodeConfig.Init.DestDataWidth = DMA_DEST_DATAWIDTH_HALFWORD;
    DMA_NodeConfig.Init.SrcBurstLength = 1;
    DMA_NodeConfig.Init.DestBurstLength = 1;
    DMA_NodeConfig.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0 | DMA_DEST_ALLOCATED_PORT0;
    DMA_NodeConfig.Init.TransferEventMode = DMA_TCEM_BLOCK_TRANSFER;
    DMA_NodeConfig.Init.Mode = DMA_NORMAL;
    DMA_NodeConfig.TriggerConfig.TriggerPolarity = DMA_TRIG_POLARITY_MASKED;
    DMA_NodeConfig.DataHandlingConfig.DataExchange = DMA_EXCHANGE_NONE;
    DMA_NodeConfig.DataHandlingConfig.DataAlignment = DMA_DATA_RIGHTALIGN_ZEROPADDED;
    if (HAL_DMAEx_List_BuildNode(&DMA_NodeConfig, &Node_GPDMA2_Channel0) != HAL_OK) {
      Error_Handler();
    }

    if (HAL_DMAEx_List_InsertNode(&List_GPDMA2_Channel0, NULL, &Node_GPDMA2_Channel0) != HAL_OK) {
      Error_Handler();
    }

    if (HAL_DMAEx_List_SetCircularMode(&List_GPDMA2_Channel0) != HAL_OK) {
      Error_Handler();
    }

    handle_GPDMA2_Channel0.Instance = GPDMA2_Channel0;
    handle_GPDMA2_Channel0.InitLinkedList.Priority = DMA_LOW_PRIORITY_HIGH_WEIGHT;
    handle_GPDMA2_Channel0.InitLinkedList.LinkStepMode = DMA_LSM_FULL_EXECUTION;
    handle_GPDMA2_Channel0.InitLinkedList.LinkAllocatedPort = DMA_LINK_ALLOCATED_PORT0;
    handle_GPDMA2_Channel0.InitLinkedList.TransferEventMode = DMA_TCEM_BLOCK_TRANSFER;
    handle_GPDMA2_Channel0.InitLinkedList.LinkedListMode = DMA_LINKEDLIST_CIRCULAR;
    if (HAL_DMAEx_List_Init(&handle_GPDMA2_Channel0) != HAL_OK) {
      Error_Handler();
    }

    if (HAL_DMAEx_List_LinkQ(&handle_GPDMA2_Channel0, &List_GPDMA2_Channel0) != HAL_OK) {
      Error_Handler();
    }

    __HAL_LINKDMA(adcHandle, DMA_Handle, handle_GPDMA2_Channel0);

    if (HAL_DMA_ConfigChannelAttributes(&handle_GPDMA2_Channel0, DMA_CHANNEL_NPRIV) != HAL_OK) {
      Error_Handler();
    }

    HAL_NVIC_SetPriority(ADC1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(ADC1_IRQn);
  } // ... ADC 2 not used with DMA
}

  

int main(void) {
  // ... Other init

  MX_GPDMA2_Init();

  MX_ADC1_Init();
  MX_ADC2_Init();

  // ... Other init
}

 Called to start with:

HAL_ADC_Start_DMA(&hadc1, dma_buffer, dma_buffer_size);

 

Any assistance or advice on this would be greatly appreciated! I will be as responsive as possible for any follow-up questions.

1 ACCEPTED SOLUTION

Accepted Solutions

Ok, so it sounds like you want multiple samples of each channel?

 

You'll need a queue to have the DMA copy to each queue. On each callback, you increment the queue pointer and call HAL_ADC_Start_DMA which will now point to the next queue index. Below is an example code that i just tested.

 

// main.h
typedef struct
{
	uint16_t data[2]; // the 2 channels
}ADC_Data_t;

typedef struct
{
	ADC_HandleTypeDef *hadc;
	ADC_Data_t queue[3];
	RING_BUFF_STRUCT ptr;
	bool rdy;
}ADC_t;


// main.c

ADC_t adc_buf =
{
	.hadc = &hadc1,
	.ptr.SkipOverFlow = true
};


int main(void)
{
    HAL_ADCEx_Calibration_Start(&hadc1);
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf.queue[adc_buf.ptr.index_IN].data, 2);

    while(1)
    {
        if(adc_buf.rdy)
        {
            adc_buf.rdy = false;

            // do something with data.
        }
    }
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
	int status = 0;

	if(hadc == adc_buf.hadc)
	{
		status = RingBuff_Ptr_Input(&adc_buf.ptr, 3); // sample 3 times
		if(status)
		{
			adc_buf.rdy = true;
			RingBuff_Ptr_Reset(&adc_buf.ptr);
		}
		else
		{
			HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf.queue[adc_buf.ptr.index_IN].data, 2);
		}
	}
}

 

this is what the debug window will look like. The HAL_ADC_ConvCpltCallback will increment a pointer. If it doesn't rollover (status =0) then i call HAL_ADC_Start_DMA again which is pointing to the next queue index. If it does roll over, it finished it's 3 samples and so I set a flag.

KarlYamashita_0-1752539470641.png

 

DMA is in Normal mode. Continuous Conversion mode is disabled.

I attached the RingBuffer driver.

 

 

 

If you FIFO doesn't work, then it's called GIGO.
TimerCallback tutorial! | UART and DMA Idle with multiple UART instances tutorial!

If you find my solution useful, please click the Accept as Solution so others see the solution.

View solution in original post

8 REPLIES 8
Karl Yamashita
Principal

Disable ContinuousConvMode

Make sure DMA is not in circular mode. You won't have to disable the DMA in the interrupt as it'll only run once. You will need to call HAL_ADC_Start_DMA when needed again.

If you FIFO doesn't work, then it's called GIGO.
TimerCallback tutorial! | UART and DMA Idle with multiple UART instances tutorial!

If you find my solution useful, please click the Accept as Solution so others see the solution.

Hi Karl, thanks for the reply! Perhaps something I did not clarify well was that I'm trying to do more than one ADC conversion at a time using DMA peripheral to memory. When I set it up this way, it appears to only perform one conversion then stop. If I disable the linked list configuration but keep continuous conversion enabled, I do have the option to use HAL_ADC_ErrorCallback() to halt the ADC, but that doesn't seem like an ideal solution.

Saket_Om
ST Employee

Hello @ricky-fleet 

Please refer to the workshop below about ADC conversion with dma linked list.

STM32H5 workshop - 05 More advanced GPDMA - Linked Lists (intermediate)

To give better visibility on the answered topics, please click on "Accept as Solution" on the reply which solved your issue or answered your question.
Saket_Om

 


@ricky-fleet wrote:

Hi Karl, thanks for the reply! Perhaps something I did not clarify well was that I'm trying to do more than one ADC conversion at a time using DMA peripheral to memory. When I set it up this way, it appears to only perform one conversion then stop. If I disable the linked list configuration but keep continuous conversion enabled, I do have the option to use HAL_ADC_ErrorCallback() to halt the ADC, but that doesn't seem like an ideal solution.


So this reply seems opposite of your original post.

Maybe a flowchart would clear up what you really want to happen?

If you FIFO doesn't work, then it's called GIGO.
TimerCallback tutorial! | UART and DMA Idle with multiple UART instances tutorial!

If you find my solution useful, please click the Accept as Solution so others see the solution.
ricky-fleet
Associate II

Thanks @Karl Yamashita and @Saket_Om, both pieces of information were helpful! I reviewed both responses and got the DMA configured correctly for linear mode (not circular mode) with continuous conversions and DMA requests. The issue remained that HAL_ADC_ConvCpltCallback() was being called far too early, pre-empting the ADC and DMA from completing the set of conversions and transfers. It turns out that the callback was occurring after every conversion via the HAL_ADC_IRQHandler(), as well as after completing the DMA transfer via ADC_DMAConvCplt() (which is automatically mapped as the hadc->DMA_Handle->XferCpltCallback in the HAL_ADC_Start_DMA() call). The use of HAL_ADC_Stop_DMA() in the callback was still necessary to halt the ADC. The offending line was MX_ADC1_Init():24 above, ADC_IT_EOC should not be enabled here:

  __HAL_ADC_ENABLE_IT(&hadc1, ADC_IT_EOC | ADC_IT_OVR);

Here is the resulting code:

void MX_ADC1_Init(void) {
  ADC_ChannelConfTypeDef sConfig = {0};

  /** Common config
   */
  hadc1.Instance = ADC1;
  hadc1.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV1;
  hadc1.Init.Resolution = ADC_RESOLUTION_12B;
  hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
  hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE;
  hadc1.Init.EOCSelection = ADC_EOC_SEQ_CONV;
  hadc1.Init.LowPowerAutoWait = DISABLE;
  hadc1.Init.ContinuousConvMode = ENABLE;
  hadc1.Init.NbrOfConversion = 2;
  hadc1.Init.DiscontinuousConvMode = DISABLE;
  hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
  hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
  hadc1.Init.DMAContinuousRequests = ENABLE;
  hadc1.Init.SamplingMode = ADC_SAMPLING_MODE_NORMAL;
  hadc1.Init.Overrun = ADC_OVR_DATA_PRESERVED;
  hadc1.Init.OversamplingMode = DISABLE;
  if (HAL_ADC_Init(&hadc1) != HAL_OK) {
    Error_Handler();
  }
  /** Configure Regular Channel
   */
  sConfig.Channel = ADC_CHANNEL_0;
  sConfig.Rank = ADC_REGULAR_RANK_1;
  sConfig.SamplingTime = ADC_SAMPLETIME_92CYCLES_5;  // TODO: make shorter
  sConfig.SingleDiff = ADC_SINGLE_ENDED;
  sConfig.OffsetNumber = ADC_OFFSET_NONE;
  sConfig.Offset = 0;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) {
    Error_Handler();
  }

  /** Configure Regular Channel
   */
  sConfig.Channel = ADC_CHANNEL_1;
  sConfig.Rank = ADC_REGULAR_RANK_2;
  if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) {
    Error_Handler();
  }
}
void MX_GPDMA2_Init(void) {
  /* Peripheral clock enable */
  __HAL_RCC_GPDMA2_CLK_ENABLE();

  handle_GPDMA2_Channel0.Instance = GPDMA2_Channel0;
  handle_GPDMA2_Channel0.Init.Request = GPDMA2_REQUEST_ADC1;
  handle_GPDMA2_Channel0.Init.BlkHWRequest = DMA_BREQ_SINGLE_BURST;
  handle_GPDMA2_Channel0.Init.Direction = DMA_PERIPH_TO_MEMORY;
  handle_GPDMA2_Channel0.Init.SrcInc = DMA_SINC_FIXED;
  handle_GPDMA2_Channel0.Init.DestInc = DMA_DINC_INCREMENTED;
  handle_GPDMA2_Channel0.Init.SrcDataWidth = DMA_SRC_DATAWIDTH_HALFWORD;
  handle_GPDMA2_Channel0.Init.DestDataWidth = DMA_DEST_DATAWIDTH_HALFWORD;
  handle_GPDMA2_Channel0.Init.Priority = DMA_LOW_PRIORITY_LOW_WEIGHT;
  handle_GPDMA2_Channel0.Init.SrcBurstLength = 1;
  handle_GPDMA2_Channel0.Init.DestBurstLength = 1;
  handle_GPDMA2_Channel0.Init.TransferAllocatedPort = DMA_SRC_ALLOCATED_PORT0 | DMA_DEST_ALLOCATED_PORT0;
  handle_GPDMA2_Channel0.Init.TransferEventMode = DMA_TCEM_BLOCK_TRANSFER;
  handle_GPDMA2_Channel0.Init.Mode = DMA_NORMAL;
  if (HAL_DMA_Init(&handle_GPDMA2_Channel0) != HAL_OK) {
    Error_Handler();
  }

  /* GPDMA2 interrupt Init */
  HAL_NVIC_SetPriority(GPDMA2_Channel0_IRQn, 0, 0);
  HAL_NVIC_EnableIRQ(GPDMA2_Channel0_IRQn);
}
// Code to start ADC
...
__HAL_ADC_DISABLE_IT(ADCx_, ADC_IT_EOC | ADC_IT_EOS); // May not be necessary
HAL_ADC_Start_DMA(&hadc1, dma_buffer, dma_buffer_size);
...

// Callback
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
  if (hadc->Instance == ADC1) {
    HAL_ADC_Stop_DMA(hadc);
  }
}

 

 


@Karl Yamashita wrote:

 


@ricky-fleet wrote:

Hi Karl, thanks for the reply! Perhaps something I did not clarify well was that I'm trying to do more than one ADC conversion at a time using DMA peripheral to memory. When I set it up this way, it appears to only perform one conversion then stop. If I disable the linked list configuration but keep continuous conversion enabled, I do have the option to use HAL_ADC_ErrorCallback() to halt the ADC, but that doesn't seem like an ideal solution.


So this reply seems opposite of your original post.

Maybe a flowchart would clear up what you really want to happen?


Hi @Karl Yamashita ! I think I have a solution but to make sure I'm understanding it correctly and not creating issues later, here is a quick flow chart to describe what I was trying to do (I know it is a little simplified with the actual ADC and DMA interactions). In my case, the DMA buffer can handle many conversions and I only want it to stop once the buffer is full (as described by the size in the HAL_ADC_Start_DMA() call).

adc_dma_flow.drawio.png

Ok, so it sounds like you want multiple samples of each channel?

 

You'll need a queue to have the DMA copy to each queue. On each callback, you increment the queue pointer and call HAL_ADC_Start_DMA which will now point to the next queue index. Below is an example code that i just tested.

 

// main.h
typedef struct
{
	uint16_t data[2]; // the 2 channels
}ADC_Data_t;

typedef struct
{
	ADC_HandleTypeDef *hadc;
	ADC_Data_t queue[3];
	RING_BUFF_STRUCT ptr;
	bool rdy;
}ADC_t;


// main.c

ADC_t adc_buf =
{
	.hadc = &hadc1,
	.ptr.SkipOverFlow = true
};


int main(void)
{
    HAL_ADCEx_Calibration_Start(&hadc1);
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf.queue[adc_buf.ptr.index_IN].data, 2);

    while(1)
    {
        if(adc_buf.rdy)
        {
            adc_buf.rdy = false;

            // do something with data.
        }
    }
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
	int status = 0;

	if(hadc == adc_buf.hadc)
	{
		status = RingBuff_Ptr_Input(&adc_buf.ptr, 3); // sample 3 times
		if(status)
		{
			adc_buf.rdy = true;
			RingBuff_Ptr_Reset(&adc_buf.ptr);
		}
		else
		{
			HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf.queue[adc_buf.ptr.index_IN].data, 2);
		}
	}
}

 

this is what the debug window will look like. The HAL_ADC_ConvCpltCallback will increment a pointer. If it doesn't rollover (status =0) then i call HAL_ADC_Start_DMA again which is pointing to the next queue index. If it does roll over, it finished it's 3 samples and so I set a flag.

KarlYamashita_0-1752539470641.png

 

DMA is in Normal mode. Continuous Conversion mode is disabled.

I attached the RingBuffer driver.

 

 

 

If you FIFO doesn't work, then it's called GIGO.
TimerCallback tutorial! | UART and DMA Idle with multiple UART instances tutorial!

If you find my solution useful, please click the Accept as Solution so others see the solution.

Thanks @Karl Yamashita ! Your solution performs as intended but did increase our interrupt overhead by a fair bit. I will fall back to my solution which appears to work so far if we run into any performance issues with very fast ADC conversions.