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
mƎALLEm
ST Employee

Hello,


@Lupin wrote:

 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.


I'm wondering why you don't use the "Dual regular simultaneous mode" described in the application note AN3116 "STM32™’s ADC modes and their applications" for your current and voltage measurement:

mALLEm_0-1770825325019.png

 

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.

I intend to port this to an STM32L031, which has only one ADC, so I don't can't use that feature. I know that I have to watch out for the differences between families. But it's easier to develop on the slightly bigger STM32F1, where you have more flash/RAM for debug purposes.

I think I wouldn't even need DMA for my purposes. Just doing single conversions using HAL instead of Arduino would be much faster already. But hey, if I'm leaving some Arduino things behind, I might as well make it harder, so I have more chances to learn something, when something doesn't work as expected ;) .

Please bear in mind we don't support Arduino environment here. Any Arduino related question need to be asked in their Community.

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.

Which is why I specifically stated that I do not use Arduino code for the ADC/DMA part (except for a function that translates pin names like "PA0" to "GPIO_PIN_0"), but code generated by CubeMX.

TDK
Super User
	hdma_adc.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
	hdma_adc.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;

This seems wrong. Alignment should match.

channel_config.Channel = get_adc_channel(analogInputToPinName(_ADC_VOLTAGE_PIN), &rbank)

 Who knows what this does? (You do, but we don't, which makes it impossible to debug.) Perhaps simplify the code while debugging.

If you feel a post has answered your question, please click "Accept as Solution".
Lupin
Associate II

@TDK wrote:
	hdma_adc.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
	hdma_adc.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;

This seems wrong. Alignment should match.

No, this is correct. There's even a table in the STM32F103xx reference manual (RM0008, table 76), describing what happens in this case. It's essentially like an uint_16 to uint_32 conversion (I want the latter data width for later calculations). 

 


@TDK wrote:
channel_config.Channel = get_adc_channel(analogInputToPinName(_ADC_VOLTAGE_PIN), &rbank)

 Who knows what this does? (You do, but we don't, which makes it impossible to debug.) Perhaps simplify the code while debugging.


You are right, it's not necessarily clear. It returns the ADC_CHANNEL_x value from stm32f1xx_hal_adc.h corresponding to a given pin. It makes it less error prone, should I change a pin around (which I set by a #define), because I don't have to look up (and thereby can't forget) to change the channel value in the ADC setup (of course making sure the pin is actually usable by the ADC). But the issue isn't here anyway. This part gets called only once right at the start (as it's the ADC init) and I get the expected values after that, at least for some time, until it suddenly switches around.

I don't think there are any other issues in the code you've presented. You say it is simplified code, so perhaps the bug was simplified away in the presentation.

> ADC_SAMPLETIME_13CYCLES_5

Perhaps increase this to max to see if the problem disappears. If it does, maybe you are running into bandwidth issues on the internal bus, though this should appear as an underrun error flag somewhere in DMA.

> STM32F103

Also perhaps counterfeit.

If you feel a post has answered your question, please click "Accept as Solution".
waclawek.jan
Super User

Without attempting to understand the real nature of the problem let me try a blind shot: you don't observe the ADC registers in debugger, or attempt to read out the ADC status register somewhere in the program, do you?

JW


@waclawek.jan wrote:

Without attempting to understand the real nature of the problem let me try a blind shot: you don't observe the ADC registers in debugger, or attempt to read out the ADC status register somewhere in the program, do you?

JW


No and no. There's debugger active when this happens. It is also the only place in the entire program that interacts with the ADC (except for the initialization right at startup).

But I found one thing that apparently stops the switch from happening. If I also call HAL_ADC_Stop_DMA(&hadc); in the loop, the values no longer switch places.

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

	for (uint16_t i = 0; i < 128; i++) {
		adc_complete = 0;
		HAL_ADC_Start_DMA(&hadc, (uint32_t*) &adc_dma_result, 2);
		while(adc_complete == 0) {
			__NOP();
		}
		HAL_ADC_Stop_DMA(&hadc);
		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) {
	adc_complete = 1;
}

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

My initial idea was to use HAL_ADC_Start_DMA(...) to trigger the ADC, which is in non-continuous scan mode (even though it's just two channels; but I want them to be sampled one after the other as quickly as possible). After the two conversions this then calls HAL_ADC_ConvCpltCallback(), after which I should have the values in memory. Then repeat this triggering every loop. As the ADC stops itself I didn't think I needed to stop anything. But without the Stop at some point the DMA (which is in circular mode) seems to "miss" an ADC-value and is in the "wrong" spot of it's circular buffer. As I only have two values, it looks like they switch around.