Search code examples
cstm32microcontrolleradcstm32f0

Individually read distinct inputs with STM32F0 ADC


STM32F072CBU microcontroller.

I have multiple inputs to the ADC and would like to read them individually and separately. STMcubeMX produces boilerplate code which assumes I wish to read all of the inputs sequentially, and I have not been able to figure out how to correct this.

This blog post expresses the same problem I am having, but the solution given doesn't seem to work. Turning the ADC on and off for each conversion correlates with error in the returned value. Only when I configure a single ADC input in STMcubeMX and then poll without de-initializing the ADC are accurate readings returned.

cubeMX's adc_init function:

/* ADC init function */
static void MX_ADC_Init(void)
{

  ADC_ChannelConfTypeDef sConfig;

    /**Configure the global features of the ADC (Clock, Resolution, Data Alignment and number of conversion) 
    */
  hadc.Instance = ADC1;
  hadc.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
  hadc.Init.Resolution = ADC_RESOLUTION_12B;
  hadc.Init.DataAlign = ADC_DATAALIGN_RIGHT;
  hadc.Init.ScanConvMode = ADC_SCAN_DIRECTION_FORWARD;
  hadc.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
  hadc.Init.LowPowerAutoWait = DISABLE;
  hadc.Init.LowPowerAutoPowerOff = DISABLE;
  hadc.Init.ContinuousConvMode = DISABLE;
  hadc.Init.DiscontinuousConvMode = DISABLE;
  hadc.Init.ExternalTrigConv = ADC_SOFTWARE_START;
  hadc.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
  hadc.Init.DMAContinuousRequests = DISABLE;
  hadc.Init.Overrun = ADC_OVR_DATA_PRESERVED;
  if (HAL_ADC_Init(&hadc) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

    /**Configure for the selected ADC regular channel to be converted. 
    */
  sConfig.Channel = ADC_CHANNEL_0;
  sConfig.Rank = ADC_RANK_CHANNEL_NUMBER;
  sConfig.SamplingTime = ADC_SAMPLETIME_41CYCLES_5;
  if (HAL_ADC_ConfigChannel(&hadc, &sConfig) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

    /**Configure for the selected ADC regular channel to be converted. 
    */
  sConfig.Channel = ADC_CHANNEL_1;
  if (HAL_ADC_ConfigChannel(&hadc, &sConfig) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

    /**Configure for the selected ADC regular channel to be converted. 
    */
  sConfig.Channel = ADC_CHANNEL_2;
  if (HAL_ADC_ConfigChannel(&hadc, &sConfig) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

    /**Configure for the selected ADC regular channel to be converted. 
    */
  sConfig.Channel = ADC_CHANNEL_3;
  if (HAL_ADC_ConfigChannel(&hadc, &sConfig) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

    /**Configure for the selected ADC regular channel to be converted. 
    */
  sConfig.Channel = ADC_CHANNEL_4;
  if (HAL_ADC_ConfigChannel(&hadc, &sConfig) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

    /**Configure for the selected ADC regular channel to be converted. 
    */
  sConfig.Channel = ADC_CHANNEL_TEMPSENSOR;
  if (HAL_ADC_ConfigChannel(&hadc, &sConfig) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

    /**Configure for the selected ADC regular channel to be converted. 
    */
  sConfig.Channel = ADC_CHANNEL_VREFINT;
  if (HAL_ADC_ConfigChannel(&hadc, &sConfig) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

}

main.c

int main(void)
{

  /* USER CODE BEGIN 1 */

  /* USER CODE END 1 */

  /* MCU Configuration----------------------------------------------------------*/

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* USER CODE BEGIN Init */

  /* USER CODE END Init */

  /* Configure the system clock */
  SystemClock_Config();

  /* USER CODE BEGIN SysInit */

  /* USER CODE END SysInit */

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_ADC_Init();
  MX_USART1_UART_Init();

  /* USER CODE BEGIN 2 */
  //HAL_TIM_Base_Start_IT(&htim3);
  init_printf(NULL, putc_wrangler);
  HAL_ADCEx_Calibration_Start(&hadc);
  HAL_ADC_DeInit(&hadc); // ADC is initialized for every channel change
  schedule_initial_events();
  /* USER CODE END 2 */

  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  event_loop();
  /* USER CODE END WHILE */

  /* USER CODE BEGIN 3 */

  /* USER CODE END 3 */

}

My process right now for turning the ADC off and reinitializing to change channels:

// Set up
  ADC_ChannelConfTypeDef channelConfig;

  channelConfig.SamplingTime = samplingT;
  channelConfig.Channel = sensorChannel;
  channelConfig.Rank = ADC_RANK_CHANNEL_NUMBER;

  if (HAL_ADC_Init(&hadc) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

  if (HAL_ADC_ConfigChannel(&hadc, &channelConfig) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

// Convert
  uint16_t retval;

  if (HAL_ADC_Start(&hadc) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

  if (HAL_ADC_PollForConversion(&hadc, 1) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

  if (HAL_ADC_GetError(&hadc) != HAL_ADC_ERROR_NONE)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

  retval = (uint16_t) HAL_ADC_GetValue(&hadc);

  if (HAL_ADC_Stop(&hadc) != HAL_OK)
  {
    _Error_Handler(__FILE__, __LINE__);
  }

// Close
  HAL_ADC_DeInit(&hadc);

At this point I'm not really sure that there's a way to accomplish what I want, STM32 seems dead set on active ADC lines being in the regular group and being converted in order.


Solution

  • If you want to read several ADC channels in single conversion mode then you have to change the channel setting before each reading, but you do not have to reinit the ADC. Simply do as below, select the new channel (you can change sampling time too if it must be different for the channels but generally it can be the same), select the channel rank and then call the HAL_ADC_ConfigChannel function. After this you can perform a conversion.

    void config_ext_channel_ADC(uint32_t channel, boolean_t val)
    {
      ADC_ChannelConfTypeDef sConfig;
    
      sConfig.Channel = channel;
      sConfig.SamplingTime = ADC_SAMPLETIME_71CYCLES_5;
    
      if(True == val)
      {
        sConfig.Rank = ADC_RANK_CHANNEL_NUMBER;
      }
      else
      {
        sConfig.Rank = ADC_RANK_NONE;
      }
    
      HAL_ADC_ConfigChannel(&hadc, &sConfig);
    }
    
    uint32_t r_single_ext_channel_ADC(uint32_t channel)
    {
      uint32_t digital_result;
    
      config_ext_channel_ADC(channel, True);
    
      HAL_ADCEx_Calibration_Start(&hadc);
    
      HAL_ADC_Start(&hadc);
      HAL_ADC_PollForConversion(&hadc, 1000);
      digital_result = HAL_ADC_GetValue(&hadc);
      HAL_ADC_Stop(&hadc);
    
      config_ext_channel_ADC(channel, False);
    
      return digital_result;
    }
    

    An example for usage:

    #define SUPPLY_CURRENT  ADC_CHANNEL_5
    #define BATTERY_VOLTAGE ADC_CHANNEL_6
    
    uint16_t r_battery_voltage(uint16_t mcu_vcc)
    {
      float vbat;
      uint16_t digital_val;
    
      digital_val = r_single_ext_channel_ADC(BATTERY_VOLTAGE);
      vbat = (mcu_vcc/4095.0) * digital_val;
      vbat = vbat * 2;         // 1/2 voltage divider
    
      return vbat;
    }
    
    uint16_t r_supply_current(uint16_t mcu_vcc)
    {
      float v_sense, current;
      uint16_t digital_val;
    
      digital_val = r_single_ext_channel_ADC(SUPPLY_CURRENT);
      v_sense = (mcu_vcc/4095.0) * digital_val;
      current = v_sense * I_SENSE_GAIN;
    
      return current;
    }
    

    This code was used on an STM32F030. For reading the internal temperature sensor and reference voltage a slightly different version of the above seen functions needed as additional enable bits must be set.

    void config_int_channel_ADC(uint32_t channel, boolean_t val)
    {
      ADC_ChannelConfTypeDef sConfig;
      sConfig.Channel = channel;
    
      if(val == True)
      {
        if(channel == ADC_CHANNEL_VREFINT)
        {
          ADC->CCR |= ADC_CCR_VREFEN;
          hadc.Instance->CHSELR = (uint32_t)(ADC_CHSELR_CHSEL17);
        }
        else if(channel == ADC_CHANNEL_TEMPSENSOR)
        {
          ADC->CCR |= ADC_CCR_TSEN;
          hadc.Instance->CHSELR = (uint32_t)(ADC_CHSELR_CHSEL16);
        }
    
        sConfig.Rank          = ADC_RANK_CHANNEL_NUMBER;
        sConfig.SamplingTime  = ADC_SAMPLETIME_239CYCLES_5;
      }
      else if(val == False)
      {
        if(channel == ADC_CHANNEL_VREFINT)
        {
          ADC->CCR &= ~ADC_CCR_VREFEN;
          hadc.Instance->CHSELR = 0;
        }
        else if(channel == ADC_CHANNEL_TEMPSENSOR)
        {
          ADC->CCR &= ~ADC_CCR_TSEN;
          hadc.Instance->CHSELR = 0;
        }
    
        sConfig.Rank          = ADC_RANK_NONE;
        sConfig.SamplingTime  = ADC_SAMPLETIME_239CYCLES_5;
      }
    
      HAL_ADC_ConfigChannel(&hadc,&sConfig);
    }
    
    uint32_t r_single_int_channel_ADC(uint32_t channel)
    {
      uint32_t digital_result;
    
      config_int_channel_ADC(channel, True);
    
      HAL_ADCEx_Calibration_Start(&hadc);
    
      HAL_ADC_Start(&hadc);
      HAL_ADC_PollForConversion(&hadc, 1000);
      digital_result = HAL_ADC_GetValue(&hadc);
      HAL_ADC_Stop(&hadc);
    
      config_int_channel_ADC(channel, False);
    
      return digital_result;
    }
    

    Example usage internal voltage reference for MCU VDD calculation:

    #define VREFINT_CAL_ADDR   ((uint16_t*) ((uint32_t) 0x1FFFF7BA))
    
    static float FACTORY_CALIB_VDD = 3.31;
    
    uint16_t calculate_MCU_vcc()
    {
      float analog_Vdd;
      uint16_t val_Vref_int = r_single_int_channel_ADC(ADC_CHANNEL_VREFINT);
    
      analog_Vdd = (FACTORY_CALIB_VDD * (*VREFINT_CAL_ADDR))/val_Vref_int;
    
      return analog_Vdd * 1000;
    }
    

    Internal temperature sensor reading:

    #define TEMP30_CAL_ADDR  ((uint16_t*) ((uint32_t) 0x1FFFF7B8))
    #define TEMP110_CAL_ADDR ((uint16_t*) ((uint32_t) 0x1FFFF7C2))
    
    static float FACTORY_CALIB_VDD = 3.31;
    
    float r_MCU_temp(uint16_t mcu_vcc)
    {
      float temp;
      float slope = ((110.0 - 30.0)/((*TEMP110_CAL_ADDR) - (*TEMP30_CAL_ADDR)));
    
      uint16_t ts_data = r_single_int_channel_ADC(ADC_CHANNEL_TEMPSENSOR);
    
      temp = ((mcu_vcc/FACTORY_CALIB_VDD) * ts_data)/1000;
      temp = slope * (temp - (*TEMP30_CAL_ADDR)) + 30;
    
      return round_to(temp, 0);
    }
    

    Note that calibration data addresses might be different for your MCU, check the datasheet for more information.