Individually read distinct inputs with STM32F0 ADC

11,315

Solution 1

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.

Solution 2

I had the similar problem. I was using STM32F091RC. On ADC_V_PIN I had external multiplexer. On ADC_T_PIN I had NTC reading. I was controlling external multiplexer with GPIOs (this is not seen in the code below). What I needed was multiple successive readings of ADC_V_PIN, after that single ADC_T_PIN reading. With default sequential reading of ADC I would get between each external multiplexed reading of ADC_V_PIN also ADC_T_PIN reading. Using HAL_ADC_ConfigChannel multiple times seemed to OR with each other, and I had problems with reading. So I didn't use HAL_ADC_ConfigChannel at all. Instead, I reconfigured CHSELR register before each software triggered AD conversion. This was the way to make internal and external multiplexer work together.

Here is initialization code:

    GPIO_InitStruct.Pin = ADC_V_PIN;
    GPIO_InitStruct.Mode  = GPIO_MODE_ANALOG;
    GPIO_InitStruct.Pull  = GPIO_NOPULL;
    HAL_GPIO_Init(ADC_V_PORT, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = ADC_T_PIN;
    GPIO_InitStruct.Mode  = GPIO_MODE_ANALOG;
    GPIO_InitStruct.Pull  = GPIO_NOPULL;
    HAL_GPIO_Init(ADC_T_PORT, &GPIO_InitStruct);

    g_AdcHandle.Instance = ADC1;

    if (HAL_ADC_DeInit(&g_AdcHandle) != HAL_OK)
    {
        /* ADC initialization error */
        Error_Handler();
    }

    g_AdcHandle.Init.ClockPrescaler = ADC_CLOCK_ASYNC_DIV1;
    g_AdcHandle.Init.Resolution = ADC_RESOLUTION_12B;
    g_AdcHandle.Init.DataAlign = ADC_DATAALIGN_RIGHT;
    g_AdcHandle.Init.ScanConvMode = ADC_SCAN_DIRECTION_FORWARD;;
    g_AdcHandle.Init.ContinuousConvMode = DISABLE;
    g_AdcHandle.Init.DiscontinuousConvMode = ENABLE;
    g_AdcHandle.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
    g_AdcHandle.Init.LowPowerAutoWait      = DISABLE;
    g_AdcHandle.Init.LowPowerAutoPowerOff  = DISABLE;

    g_AdcHandle.Init.ExternalTrigConv      = ADC_SOFTWARE_START;
    g_AdcHandle.Init.ExternalTrigConvEdge  = ADC_EXTERNALTRIGCONVEDGE_NONE;


    g_AdcHandle.Init.DMAContinuousRequests = DISABLE;
    g_AdcHandle.Init.Overrun               = ADC_OVR_DATA_OVERWRITTEN;
    g_AdcHandle.Init.SamplingTimeCommon    = ADC_SAMPLETIME_239CYCLES_5;

    if (HAL_ADC_Init(&g_AdcHandle) != HAL_OK)
    {
      /* ADC initialization error */
      Error_Handler();
    }

    if (HAL_ADCEx_Calibration_Start(&g_AdcHandle) != HAL_OK)
    {
      /* Calibration Error */
      Error_Handler();
    }

while(1){

        ADC1->CHSELR = ADC_CHSELR_CHSEL0;
        HAL_ADC_Start(&g_AdcHandle);
        HAL_ADC_PollForConversion(&g_AdcHandle, 10);
        V = HAL_ADC_GetValue(&g_AdcHandle);
        HAL_ADC_Stop(&g_AdcHandle);

        ADC1->CHSELR = ADC_CHSELR_CHSEL10;
        HAL_ADC_Start(&g_AdcHandle);
        HAL_ADC_PollForConversion(&g_AdcHandle, 10);
        T = HAL_ADC_GetValue(&g_AdcHandle);
        HAL_ADC_Stop(&g_AdcHandle);
    }
Share:
11,315
ctag
Author by

ctag

Updated on June 04, 2022

Comments

  • ctag
    ctag about 2 years

    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.