STM32 ADC Tutorial: Reading a Potentiometer with HAL Polling Mode

Most real sensors give an analog voltage: temperature sensors such as the LM35, light-dependent resistors in a divider, joysticks and potentiometers. The Analog to Digital Converter (ADC) turns that voltage into a number your program can use. STM32 chips have 12-bit successive-approximation ADCs that are fast and accurate enough for most hobby and student projects.

In this tutorial you will read a 10 k potentiometer on a Nucleo-F401RE, convert the raw value to millivolts with integer maths, smooth it with averaging, and print the result over UART. The Blue Pill differences, including the calibration step it needs, are covered too.

How the STM32 ADC Works

A 12-bit ADC divides the range from 0 V to the reference voltage into 4096 steps, so the result runs from 0 to 4095. On the Nucleo-F401RE and Blue Pill the reference is tied to the 3.3 V supply. The conversion formula is:

Vin = raw x Vref / 4095

Example: raw = 2048, Vref = 3300 mV
Vin = 2048 x 3300 / 4095 = 1650 mV (approximately)

One step (LSB) = 3300 mV / 4095 = about 0.8 mV

Each conversion has two parts. First, an internal sample-and-hold capacitor charges to the input voltage during the sampling time, which you choose in CubeMX in ADC clock cycles. Then the converter performs the 12-bit successive approximation. A longer sampling time gives more accurate readings from high-impedance sources such as a 10 k potentiometer or a resistor divider.

A conversion can be started by software, by a timer event, or by an external pin, and the result can be collected by polling, by interrupt, or by DMA. This tutorial uses the simplest combination: a software start with polling, where your code starts the conversion and waits for the end-of-conversion flag. It is easy to follow and perfectly adequate for reading a knob a few times per second.

The ADC has many input channels, each tied to a fixed pin. You cannot route any pin to the ADC; you must use a pin that the datasheet lists as ADC_INx.

Wiring the Potentiometer

Potentiometer pinNucleo-F401REBlue Pill
One outer pin3V33.3
Other outer pinGNDG
Wiper (middle)A0 = PA0 (ADC1_IN0)PA0 (ADC1_IN0)

CubeMX Configuration

  1. Click PA0 and select ADC1_IN0. Under Analog, ADC1, the IN0 checkbox is now ticked.
  2. In Parameter Settings keep Resolution at 12 bits, Data Alignment Right, Scan Conversion Mode Disabled, Continuous Conversion Mode Disabled.
  3. Under ADC_Regular_ConversionMode, Rank 1, set Channel 0 and Sampling Time to 84 cycles on the F4. On the Blue Pill, choose 71.5 cycles.
  4. On the Blue Pill only, open the Clock Configuration tab and set the ADC prescaler so the ADC clock does not exceed 14 MHz; with a 72 MHz APB2 clock, divide by 6 to get 12 MHz.
  5. Keep USART2 enabled for printing and generate the code.

Reading and Averaging the ADC

The function below performs one software-triggered conversion in polling mode. A second function takes 16 readings and averages them, which reduces random noise by roughly a factor of four. Output is in millivolts so that no floating point is needed.

main.c (Nucleo-F401RE): poll ADC1 channel 0 and print millivolts
/* USER CODE BEGIN Includes */
#include <stdio.h>
/* USER CODE END Includes */

/* USER CODE BEGIN PV */
#define VREF_MV     3300U
#define ADC_MAX     4095U
#define AVG_SAMPLES 16U
/* USER CODE END PV */

/* USER CODE BEGIN 0 */
int _write(int file, char *ptr, int len)
{
  (void)file;
  HAL_UART_Transmit(&huart2, (uint8_t *)ptr, (uint16_t)len, HAL_MAX_DELAY);
  return len;
}

/* One conversion: start, wait for end of conversion, read, stop */
static uint16_t adc_read_once(void)
{
  uint16_t value = 0;
  HAL_ADC_Start(&hadc1);
  if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK)
  {
    value = (uint16_t)HAL_ADC_GetValue(&hadc1);
  }
  HAL_ADC_Stop(&hadc1);
  return value;
}

/* Average several readings to reduce noise */
static uint16_t adc_read_average(void)
{
  uint32_t sum = 0;
  for (uint32_t i = 0; i < AVG_SAMPLES; i++)
  {
    sum += adc_read_once();
  }
  return (uint16_t)(sum / AVG_SAMPLES);
}
/* USER CODE END 0 */

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART2_UART_Init();
  MX_ADC1_Init();          /* PA0 = ADC1_IN0, 12-bit, single conversion */

  /* USER CODE BEGIN 2 */
  /* On STM32F1 (Blue Pill) calibrate once before the first conversion:
     HAL_ADCEx_Calibration_Start(&hadc1);
     The F4 ADC has no calibration function. */
  printf("\r\nADC potentiometer demo\r\n");
  /* USER CODE END 2 */

  while (1)
  {
    /* USER CODE BEGIN 3 */
    uint16_t raw = adc_read_average();

    /* Integer conversion: multiply first, then divide, in 32 bits */
    uint32_t mv = ((uint32_t)raw * VREF_MV) / ADC_MAX;

    /* Percentage of the potentiometer travel */
    uint32_t percent = ((uint32_t)raw * 100U) / ADC_MAX;

    printf("raw=%4u  voltage=%4lu mV  position=%3lu %%\r\n", raw, mv, percent);

    /* Use the reading: LED on above half travel */
    HAL_GPIO_WritePin(LD2_GPIO_Port, LD2_Pin,
                      (raw > ADC_MAX / 2) ? GPIO_PIN_SET : GPIO_PIN_RESET);

    HAL_Delay(200);
    /* USER CODE END 3 */
  }
}
Sample terminal output while turning the knob:
raw=   0  voltage=   0 mV  position=  0 %
raw=1210  voltage= 975 mV  position= 29 %
raw=2047  voltage=1649 mV  position= 49 %
raw=4095  voltage=3300 mV  position=100 %

Understanding the HAL ADC Calls

  • HAL_ADC_Start(&hadc1) enables the ADC if needed and starts a conversion by software trigger.
  • HAL_ADC_PollForConversion(&hadc1, 10) waits up to 10 ms for the end-of-conversion (EOC) flag. A single conversion takes only microseconds, so a timeout here usually means the ADC was not started or configured.
  • HAL_ADC_GetValue(&hadc1) reads the data register, which also clears the EOC flag.
  • HAL_ADC_Stop(&hadc1) stops the ADC. In single-conversion mode you could leave it running, but stopping keeps the pattern clear.
  • The multiplication raw * VREF_MV reaches at most 4095 x 3300, about 13.5 million, which fits easily in a 32-bit integer.

Because the reference is the 3.3 V supply, any error in the supply appears directly in your readings. If your board's 3V3 actually measures 3.28 V with a multimeter, change VREF_MV to 3280. On the F4 you can also measure the internal reference VREFINT and compute the true supply voltage, a technique explained in the reference manual.

Common Problems

  • Reading stuck at 0 or 4095: the wiper is not on PA0, or an outer pin is disconnected.
  • Value jumps by 20 to 50 counts: use more averaging, a longer sampling time, and a 100 nF capacitor from PA0 to GND.
  • Blue Pill readings offset by a few counts: you skipped HAL_ADCEx_Calibration_Start.
  • Readings slightly below 4095 at full rotation: normal; the potentiometer track and wiring add a little resistance.
  • %% prints a single percent sign in printf; a lone % before a space causes odd output.

Summary

You configured ADC1 channel 0 on PA0, performed software-triggered conversions with HAL_ADC_Start, HAL_ADC_PollForConversion and HAL_ADC_GetValue, converted the result to millivolts with safe integer maths, and reduced noise by averaging. Polling is fine for one slow channel. When you need several channels sampled continuously, DMA is the better tool, as shown later in this series. Next, you will use a timer to produce PWM and dim an LED.