2
\$\begingroup\$

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...
\$\endgroup\$
  • 1
    \$\begingroup\$ Well don't do your averaging math in ISR. OTTOMH STM32 family push/pop 4 registers automatically, but you have code that probably requires more than 4, so for 15 of 16 interrupts extra registers are pushed/popped. Use your flag to do calculate average. \$\endgroup\$ – StainlessSteelRat Nov 4 at 13:11
  • 1
    \$\begingroup\$ As the answer suggests, use DMA to transfer data from ADC to memory buffer. DMA finishes, generate interrupt. Interrupt, do math, reprogram DMA. \$\endgroup\$ – StainlessSteelRat Nov 4 at 16:54
  • \$\begingroup\$ The best way is to use RTOS. It has all needed sync and inter process communications features built in \$\endgroup\$ – P__J__ Nov 5 at 11:55
0
\$\begingroup\$

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.