cancel
Showing results for 
Search instead for 
Did you mean: 

Channel results of ADC in scan mode with DMA sometimes switch places. (STM32F103)

Lupin
Associate II

I want to calculate the resistance/power of an AC heating element by measuring voltage (via a voltage divider) and current (using an ACS71240 current sensor). The hardware setup is fine. I get the expected values. I checked this when using Arduino Framework's analogRead(). But that call is so slow (because it does the whole ADC initialization and deinitialization every time), that there's too much time between the voltage and current measurement.

So while I'm still using the Arduino Framework for most of the code, I've used CubeMX to configure the ADC with DMA and integrated that code into mine. I initialize the ADC/DMA, run one ADC calibration, then in a loop call HAL_ADC_Start_DMA(), wait on a dma_complete variable, that is set in the HAL_ADC_ConvCpltCallback(). This works ... almost.

At some point the values of the channels end up in the wrong order in the DMA buffer. So the second channel's conversion result is first in buffer and the first's in the second place. This seems to happen randomly, but with a low chance. Like 100 results in the correct order, then switches and gives 100 results in the wrong order (the 100 is random; I've even seen single "switches"). The result when printed afterwards the looks something like this:

V/A:    514   2087
V/A:    573   2095
V/A:    635   2095
V/A:    691   2099
V/A:    747   2102
V/A:    797   2105
V/A:    840   2107
V/A:    874   2110
V/A:    904   2111
V/A:    929   2113
V/A:    943   2114
V/A:    953   2114
V/A:    962   2114
V/A:    964   2115
V/A:    967   2115
V/A:    968   2114
V/A:    969   2115
V/A:    968   2118
V/A:    968   2118
V/A:   2116    959
V/A:   2117    951
V/A:   2115    931
V/A:   2115    909
V/A:   2114    879
V/A:   2112    841
V/A:   2110    795
V/A:   2106    745
V/A:   2106    690
V/A:   2102    631
V/A:   2098    563
V/A:   2097    497
V/A:   2092    432

The ADC results are exactly what I expect to see (shows the sine on the voltage channel - the one that is less than 1000 - and the current channel, which has an offset of ideally 2048), it's just that they suddenly appear in the wrong order.

Here's the (abbreviated) code I'm using. Ignore the parts that come from Arduino. I'm not using it for analog reads.

The main code:

ADC_HandleTypeDef hadc;
DMA_HandleTypeDef hdma_adc;
uint8_t dma_complete = 0;
v_c_measurement_t measurements_raw_dbg[1 << OVER_SAMPLING_EXP];

void setup() {
	hal_specific_setup::ADC_DMA_Init(&hadc);
	HAL_ADCEx_Calibration_Start(&hadc);
}
void loop() {
	v_c_measurement_t measurement_raw;
	measure_U_I(measurement_raw);
	// printing results happens here
}

void measure_U_I(v_c_measurement_t &result) {
	v_c_measurement_t adc_dma_result = {0, 0};

	for (uint16_t i = 0; i < (1 << OVER_SAMPLING_EXP); i++) {
		dma_complete = 0;
		HAL_ADC_Start_DMA(&hadc, (uint32_t*) &adc_dma_result, 2);
		while(dma_complete == 0) {
			__NOP();
		}
		measurements_raw_dbg[i].voltage = adc_dma_result.voltage;
		measurements_raw_dbg[i].current = adc_dma_result.current;
		delayMicroseconds(200);
	}
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
	dma_complete = 1;
}

The initialization functions:

void hal_specific_setup::ADC_DMA_Init(ADC_HandleTypeDef* hadc) {
	GPIO_InitTypeDef GPIO_InitStruct = {0};
	__HAL_RCC_ADC1_CLK_ENABLE();
	__HAL_RCC_GPIOA_CLK_ENABLE();

	GPIO_InitStruct.Pin = analogInputToPinName(_ADC_VOLTAGE_PIN) | analogInputToPinName(_ADC_CURRENT_PIN);
	GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;
	HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
	
	hadc->Instance = ADC1;
	hadc->Init.ScanConvMode = ADC_SCAN_ENABLE;
	hadc->Init.ContinuousConvMode = DISABLE;
	hadc->Init.DiscontinuousConvMode = DISABLE;
	hadc->Init.ExternalTrigConv = ADC_SOFTWARE_START;
	hadc->Init.DataAlign = ADC_DATAALIGN_RIGHT;
	hadc->Init.NbrOfConversion = 2;
	if (HAL_ADC_Init(hadc) != HAL_OK)
	{
		Error_Handler();
	}
	
	ADC_ChannelConfTypeDef channel_config = {0};
	uint32_t rbank = 0;
	channel_config.Channel = get_adc_channel(analogInputToPinName(_ADC_VOLTAGE_PIN), &rbank);
	channel_config.Rank = ADC_REGULAR_RANK_1;
	channel_config.SamplingTime = ADC_SAMPLETIME_13CYCLES_5;
	if (HAL_ADC_ConfigChannel(hadc, &channel_config) != HAL_OK) {
		Error_Handler();
	}

	channel_config.Channel = get_adc_channel(analogInputToPinName(_ADC_CURRENT_PIN), &rbank);
	channel_config.Rank = ADC_REGULAR_RANK_2;
	if (HAL_ADC_ConfigChannel(hadc, &channel_config) != HAL_OK) {
		Error_Handler();
	}

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

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

	/* ADC1 DMA Init */
	hdma_adc.Instance = DMA1_Channel1;
	hdma_adc.Init.Direction = DMA_PERIPH_TO_MEMORY;
	hdma_adc.Init.PeriphInc = DMA_PINC_DISABLE;
	hdma_adc.Init.MemInc = DMA_MINC_ENABLE;
	hdma_adc.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
	hdma_adc.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
	hdma_adc.Init.Mode = DMA_NORMAL;
	hdma_adc.Init.Priority = DMA_PRIORITY_LOW;
	if (HAL_DMA_Init(&hdma_adc) != HAL_OK) {
		Error_Handler();
	}

	__HAL_LINKDMA(hadc, DMA_Handle, hdma_adc);
}

/**
  * @brief This function handles DMA1 channel1 global interrupt.
  */
void DMA1_Channel1_IRQHandler(void)
{
  /* USER CODE BEGIN DMA1_Channel1_IRQn 0 */

  /* USER CODE END DMA1_Channel1_IRQn 0 */
  HAL_DMA_IRQHandler(&hdma_adc);
  /* USER CODE BEGIN DMA1_Channel1_IRQn 1 */

  /* USER CODE END DMA1_Channel1_IRQn 1 */
}

Do you have any idea, what is happening here?

11 REPLIES 11
waclawek.jan
Super User

> Is calling HAL_ADC_Stop_DMA(...); necessary before calling HAL_ADC_Start_DMA(...) again?

What does Cube/HAL documentation say on this?

I don't use Cube/HAL.

JW

 


@waclawek.jan wrote:

What does Cube/HAL documentation say on this?


Nothing about having to call Stop before calling start again (also nothing about it in the notes of the respective functions):

HAL_ADC_Start_DMA
Enables ADC, starts conversion of regular group and transfers result through DMA.

 

HAL_ADC_Stop_DMA
Stop ADC conversion of regular group (and injected group in case of auto_injection mode), disable ADC DMA
transfer, disable ADC peripheral.

With a new word to search for (HAL_ADC_Start_DMA/HAL_ADC_Stop_DMA), I found other threads having a similar problem. E. g.: Is there a HAL_ADC_Stop_DMA() alternative to be able to start second transfer using HAL_ADC_Start_DMA() for single conversion? 

But I also found threads with an example where repeated calls to Start without Stop apparently are fine: STM32H5 ADC DMA HAL_ADC_ConvCpltCallback Called Early 

The threads are for different STM32 families and didn't contain a solution for my problem, but it made me rewrite my code again, pulling the Start and Stop out of the loop (as I don't want to have to disable the ADC every loop iteration). I took the line in HAL_ADC_Start_DMA(...) that actually starts the ADC conversion (SET_BIT(hadc.Instance->CR2, (ADC_CR2_SWSTART));) and put it in my loop instead. That (and clearing the ADC Start flag) seems to be the only thing necessary to trigger a new conversion with the results ending up in the right spot in memory. I also set the DMA mode to circular for that to work, so I don't have to reset its registers after every single conversion.

	hdma_adc.Init.Mode = DMA_CIRCULAR;
void measure_U_I(v_c_measurement_t &result) {
	v_c_measurement_t adc_dma_result = {0, 0};

	adc_complete = 0;
	HAL_ADC_Start_DMA(&hadc, (uint32_t*) &adc_dma_result, 2);
	
	for (uint16_t i = 0; i < 128; i++) {
		if (!(hadc.Instance->SR & ADC_FLAG_STRT)) {
			// start ADC conversion, if it isn't already running (which it is on first loop iteration)
			adc_complete = 0;
			SET_BIT(hadc.Instance->CR2, (ADC_CR2_SWSTART));
		}

		// wait for conversions to finish
		while(adc_complete == 0) {
			__NOP();
		}

		// clear start flag (set by hardware on conversion start)
		CLEAR_BIT(hadc.Instance->SR, ADC_FLAG_STRT);

		measurements_raw_dbg[i].voltage = adc_dma_result.voltage;
		measurements_raw_dbg[i].current = adc_dma_result.current;

		// delay for next loop
		time_var = 100; // DEBUG
		delayMicroseconds(time_var);
	}
	
	HAL_ADC_Stop_DMA(&hadc);
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
	adc_complete = 1;
}

So this seems to work now. I think I'd have enough time to call Start and Stop every loop iteration (not sure which sampling rate I want in the end), it feels a lot more "right" to do it with two lines of register accesses. I'd have liked to stay away from direct register access though, so it's easier to port to another MCU family (which as I mentioned I intend to do at some point, to an L031). But I'll cross that bridge when I get there.

 

I'd REALLY like to know though why the ADC and DMA can go "out of sync" in the original version in the first place.