Electronic – STM32F303 ADC+DMA Averaging of measurements

adcdmastm32

What is the best method of taking multiple measurements using ADC+DMA interrupts and averaging them? Currently I have an STM32F303 with ADC2 initialized with channels 3 and 18 (Vrefint). My aim is to take 16 measurements and then average the result. I need to take these measurements with a relatively low frequency and for this test I've set up main to trigger the ADC DMA conversion every 250ms. The problems I have are:

  • What is the best method of sharing data between the interrupt handler and the main loop
  • How to ensure interrupt is not triggered after 16 times

Part of my code is below. The ADC/DMA callback function is supposed to trigger the next conversion 16 times and then signal to the main loop via a flag that the conversion is complete. I can see the callback being called and the ADC conversion performed; measurements are correct.

// ADC Data structure, also accessed by ADC callback
typedef struct ADC_Data {
  volatile uint32_t adc_value_channel_3;
  volatile uint32_t adc_value_vrefint_channel;
  volatile bool adc_conversion_complete;
  uint16_t adc_value_vrefint_register;
}
ADC_Data;

int main(void) {

  // Initialize peripherals
  HAL_Init();
  SystemClock_Config();
  MX_DMA_Init();
  MX_ADC2_Init();
  MX_OPAMP2_Init();
  MX_GPIO_Init();
  MX_USART2_UART_Init();
  HAL_OPAMP_Start( & hopamp2);

  adc_data.adc_conversion_complete = false;
  adc_data.adc_value_channel_3 = 0;
  adc_data.adc_value_vrefint_channel = 0;
  adc_data.adc_value_vrefint_register = * VREFINT_CAL_ADDR;

  HAL_ADC_Start_DMA( & hadc2, (uint32_t * ) adc_buffer, 2);

  while (1) {

    HAL_Delay(250);

    if (adc_data.adc_conversion_complete == true) {
        double adc_value = get_adc_value(&adc_data);
        HAL_ADC_Start_IT(&hadc2);
    }

  }
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef * hadc) {
  adc_data.adc_conversion_complete = false;
  static uint32_t conv_count = 0;
  static uint32_t temp_adc_value = 0;
  static uint32_t temp_vrefint_value = 0;

  temp_adc_value += adc_buffer[0];
  temp_vrefint_value += adc_buffer[1];

  if (++conv_count == 16) {
    conv_count = 0;
    adc_data.adc_value_channel_3 = temp_adc_value >> 4;
    adc_data.adc_value_vrefint_channel = temp_vrefint_value >> 4;
    adc_data.adc_conversion_complete = true;
    temp_adc_value = 0;
    temp_vrefint_value = 0;
  } else {
    HAL_ADC_Start_IT(hadc);
  }

}


double get_adc_value(ADC_Data * data) {
  return (3.3 * data - > adc_value_vrefint_register * data - > adc_value_channel_3) / (data - > adc_value_vrefint_channel * 4095);
}

***********************************EDIT*******************************

I've modified the ADC initialization to allow continuous conversion (for 3 channels) and trigger an interrupt at the end of sequence conversion:
static void MX_ADC2_Init(void)
{
ADC_ChannelConfTypeDef sConfig = {0};

  hadc2.Instance = ADC2;
  hadc2.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV1;
  hadc2.Init.Resolution = ADC_RESOLUTION_12B;
  hadc2.Init.ScanConvMode = ADC_SCAN_ENABLE;
  hadc2.Init.ContinuousConvMode = ENABLE;
  hadc2.Init.DiscontinuousConvMode = DISABLE;
  hadc2.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
  hadc2.Init.ExternalTrigConv = ADC_SOFTWARE_START;
  hadc2.Init.DataAlign = ADC_DATAALIGN_RIGHT;
  hadc2.Init.NbrOfConversion = 3;
  hadc2.Init.DMAContinuousRequests = DISABLE;
  hadc2.Init.EOCSelection = ADC_EOC_SEQ_CONV;
  hadc2.Init.LowPowerAutoWait = DISABLE;
  hadc2.Init.Overrun = ADC_OVR_DATA_OVERWRITTEN;

  if (HAL_ADC_Init(&hadc2) != HAL_OK)
  {
    Error_Handler();
  }

  sConfig.Channel = ADC_CHANNEL_3;
  sConfig.Rank = ADC_REGULAR_RANK_1;
  sConfig.SingleDiff = ADC_SINGLE_ENDED;
  sConfig.SamplingTime = ADC_SAMPLETIME_181CYCLES_5;
  sConfig.OffsetNumber = ADC_OFFSET_NONE;
  sConfig.Offset = 0;
  if (HAL_ADC_ConfigChannel(&hadc2, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }

  sConfig.Channel = ADC_CHANNEL_VREFINT;
  sConfig.Rank = ADC_REGULAR_RANK_2;
  sConfig.SamplingTime = ADC_SAMPLETIME_181CYCLES_5;
  sConfig.OffsetNumber = ADC_OFFSET_NONE;
  sConfig.Offset = 0;
  if (HAL_ADC_ConfigChannel(&hadc2, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }

  sConfig.Channel = ADC_CHANNEL_4;
  sConfig.Rank = ADC_REGULAR_RANK_3;
  sConfig.SamplingTime = ADC_SAMPLETIME_181CYCLES_5;
  sConfig.OffsetNumber = ADC_OFFSET_NONE;
  sConfig.Offset = 0;
  if (HAL_ADC_ConfigChannel(&hadc2, &sConfig) != HAL_OK)
  {
    Error_Handler();
  }

  // Calibration
  ADC2->CR &= ~ADC_CR_ADEN;         // Disable ADC
  ADC2->CR |= ADC_CR_ADCAL;         // Start calibration
  while ( (ADC2->CR & ADC_CR_ADCAL) != 0);
}

The conversion is started with HAL_ADC_Start_DMA(&hadc2, (uint32_t*)adc_buffer, 48);. The interrupt callback is called when the buffer is filled with 48 samples (3 samples from each channel), and it sets a flag, which is then polled and reset from another function:

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    adc_data.adc_conversion_complete = true;
}

Then in the polling function I loop through the filled buffer and average its contents for each of the three channels:

adc_data.adc_value_channel_3 = 0;       
adc_data.adc_vrefint_data = 0;              
adc_data.adc_value_channel4 = 0;
uint32_t *tempbuf = adc_buffer;             

for (int x = 0; x < 16; x++) {                          
    adc_data.adc_value_channel_3 += *tempbuf++;
    adc_data.adc_vrefint_data += *tempbuf++;
    adc_data.adc_value_channel4+= *tempbuf++;
}

// Averaging by shifting each value to the right by 4 places...

Best Answer

Since you already have DMA initialized, why don't you use that? Pass in a memory pointer, poll for conversion done or signal from your interrupt:

in main function:

while (data_count-- > 0) {
    HAL_ADC_Start_DMA( & hadc2, (uint32_t * ) adc_buffer, 2);
    while(!adc_done) {}
    <do your math here>
    adc_done = 0;
}

uint8_t adc_done = 0; is global, which gets assigned in DMA interrupt.