Embedded C track

Lesson 20 · Part 3: STM32 (Keil MDK-ARM / STM32F103C8, direct register control)

ADC Analog Input

12-bit conversion and scaling

Part 3: STM32 (Keil MDK-ARM / STM32F103C8, direct register control)6 min readFree lesson

All the pins we've dealt with so far were either 0 or 1, that is, digital. However, most signals in the world are continuous analog signals, like temperature, brightness, or sound. ADC (Analog-to-Digital Converter) converts analog voltage to numbers, allowing the MCU to handle those values. Inputs such as potentiometers, light sensors, and temperature sensors all go through the ADC.

The ADC of STM32F103 is 12-bit, representing voltage as integers from 0 to 4095. In this chapter, we read the voltage on PA0 using ADC1 and convert that count into millivolts. This is the final chapter of the STM32 peripheral series (Chapters 13–20), where the clock, pin, and polling concepts we learned earlier are all used at once.

Simulator Note: The STM32F103 ADC model in μVision may have limitations on input voltage injection. In this chapter, check the conversion sequence (calibration, EOC, DR) via By-Register/Watch, and verify the count→voltage conversion formula itself on a PC using pc_test.c.

Learning Objectives

  • Understand the role of ADC, 12-bit resolution (0–4095), and the concept of reference voltage.
  • Set the ADC clock constraint (≤14MHz) and prescaler.
  • Configure channel, sample time, and conversion sequence.
  • Implement F1 ADC calibration (RSTCAL/CAL) and End-Of-Conversion (EOC) polling.
  • Convert ADC counts into millivolts.

Target Environment

  • Compiler: Keil MDK-ARM (Arm Compiler 6).
  • Device: STM32F103C8 (μVision simulator).
  • Verification: μVision simulator (ADC1 registers, Watch). Conversion formula is verified on PC gcc(pc_test.c).

Key Concepts

1) Resolution and Reference Voltage

A 12-bit ADC divides the input voltage range (0 ~ Vref, usually 3.3V) into 2¹² = 4096 steps. One step (LSB) is approximately 3300mV / 4095 ≈ 0.806mV.

mV = adc * 3300 / 4095
ADC Count Voltage
0 0 mV
2048 Approximately 1650 mV
4095 3300 mV

2) ADC Clock Constraints

The ADC clock (ADCCLK) is maximum 14MHz. Using PCLK2=72MHz directly is too fast, so it is divided by ADCPRE of RCC->CFGR. part/6 fits within the limit at 12MHz.

RCC->CFGR |= RCC_CFGR_ADCPRE_DIV6;   /* 72/6 = 12MHz */

3) Channels and Sample Time

  • Channel: PA0=Channel0, PA1=Channel1 … put the channel to be converted into the first slot (SQ1) of the sequence register SQR3.
  • Sample Time: Set with SMPR. High impedance sources need a longer time (e.g., 239.5 cycles) for accuracy.
ADC1->SQR3  = 0u;                 /*First conversion channel 0 (PA0)*/
ADC1->SMPR2 |= (7u << 0);         /*Channel 0 maximum sample time*/

4) Calibration and Conversion (F1 specific)

F1 ADC must perform calibration once after power-on to be accurate.

ADC1->CR2 |= ADC_CR2_ADON;             /*Power ON*/
/*tSTAB Standby*/
ADC1->CR2 |= ADC_CR2_RSTCAL; while (ADC1->CR2 & ADC_CR2_RSTCAL) { }
ADC1->CR2 |= ADC_CR2_CAL;    while (ADC1->CR2 & ADC_CR2_CAL)    { }

For conversion, reset ADON again to start, wait for EOC (End of Conversion) in SR, then read DR.

5) Reading Results

DR holds the 12-bit result (right-aligned by default). Reading it will clear the EOC.

ADC1->CR2 |= ADC_CR2_ADON;
while (!(ADC1->SR & ADC_SR_EOC)) { }
uint16_t v = ADC1->DR & 0x0FFF;

Creating a μVision Project

  1. Project → New µVision Project… → name it stm32_adc.
  2. Device: STM32F103C8.
  3. Manage Run-Time Environment → CMSIS:CORE, Device:Startup.
  4. After adding main.c, copy and paste.
  5. Apply STM32F10X_MD to C/C++ → Define.
  6. Debug → Use Simulator, -pSTM32F103C8.
  7. Rebuild(F7) → Debug(Ctrl+F5).

Examples

Example 1 — main.c: Reading Voltage of PA0

#include "stm32f10x.h"

/*
 *Lesson 20 Example — Single conversion of ADC1 Channel 0 (PA0).
 *Target: STM32F103C8, Compiler: Keil MDK-ARM.
 *
 *Read the voltage of PA0 with a 12-bit ADC (0~4095) and convert it to millivolts.
 *mV = adc * 3300 / 4095   (assuming reference voltage 3.3V)
 *For F1 ADC, after turning it on, calibration (RSTCAL → CAL) must be done once to be accurate.
 */

static void short_delay(void)
{
    volatile uint32_t i;
    for (i = 0; i < 10000u; i++) { }   /*tSTAB short wait for stabilization*/
}

static void adc1_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_ADC1EN;
    RCC->CFGR    |= RCC_CFGR_ADCPRE_DIV6;       /*ADC clock 72/6=12MHz (≤14MHz)*/

    /*PA0 = analog input (CNF=00, MODE=00 → 0x0)*/
    GPIOA->CRL &= ~(0xFu << (0u * 4u));

    ADC1->SQR3  = 0u;                            /*Primary conversion channel = 0 (PA0)*/
    ADC1->SMPR2 |= (7u << (3u * 0u));            /*Channel 0 sample time 239.5 cycle*/

    ADC1->CR2 |= ADC_CR2_ADON;                   /*ADC power ON*/
    short_delay();

    ADC1->CR2 |= ADC_CR2_RSTCAL;                 /*Reset calibration register*/
    while (ADC1->CR2 & ADC_CR2_RSTCAL) { }
    ADC1->CR2 |= ADC_CR2_CAL;                    /*Start calibration*/
    while (ADC1->CR2 & ADC_CR2_CAL) { }          /*Wait for calibration complete*/
}

static uint16_t adc1_read(void)
{
    ADC1->CR2 |= ADC_CR2_ADON;                   /*ADON reset = start conversion*/
    while (!(ADC1->SR & ADC_SR_EOC)) { }         /*Wait for conversion complete (EOC)*/
    return (uint16_t)(ADC1->DR & 0x0FFFu);       /*12-bit result*/
}

int main(void)
{
    volatile uint16_t adc_val = 0;
    volatile uint32_t mv = 0;

    adc1_init();

    while (1) {
        adc_val = adc1_read();
        mv = (uint32_t)adc_val * 3300u / 4095u;  /*Conversion to millivolts*/
    }
}

Example 2 — pc_test.c: Count → Voltage Conversion

#include <stdio.h>
#include <stdint.h>

/*
 *Lesson 20 PC Verification — Converting ADC counts (0~4095) to millivolts.
 *mV = adc * 3300 / 4095   (Reference voltage 3.3V, 12-bit)
 *Since it is integer arithmetic, multiply first then divide to reduce error.
 */

static uint32_t adc_to_mv(uint16_t adc)
{
    return (uint32_t)adc * 3300u / 4095u;
}

int main(void)
{
    uint16_t samples[5] = { 0u, 1024u, 2048u, 3072u, 4095u };
    int i;

    for (i = 0; i < 5; i++) {
        printf("ADC=%u -> %lu mV\n",
               samples[i], (unsigned long)adc_to_mv(samples[i]));
    }
    return 0;
}

Checking with Simulator

  • Add adc_val and mv to Watch and run.
  • If the input voltage can be set in Peripherals → A/D Converter → ADC1 (depends on the simulation version), change the value and observe the changes in DR and mv.
  • In ADC1's CR2, check that ADON=1, CAL=0 after calibration, and EOC=1 in SR after conversion.
  • Expected behavior: adc_val (04095) and mv (03300) are updated proportionally to the input voltage.

Verification on a PC

gcc pc_test.c -o pc_test && ./pc_test

Expected output (stdout):

ADC=0 -> 0 mV
ADC=1024 -> 825 mV
ADC=2048 -> 1650 mV
ADC=3072 -> 2475 mV
ADC=4095 -> 3300 mV

Common Mistakes

Q. It stops waiting for EOC without finishing the conversion.

A. Check if the ADC power (ADON) is turned on, and if the conversion has been started (ADON reset or SWSTART). Also, verify that the ADC clock (RCC_APB2ENR_ADC1EN) is active.

Q. The values are fluctuating and inaccurate.

A. ① F1 is inaccurate if calibration (RSTCAL/CAL) is skipped. ② If the sample time is short, high-impedance sources will cause large errors. ③ The ADC clock must not exceed 14MHz (check ADCPRE).

Q. The mV calculation results in 0.

A. If in adc/4095*3300 order, it will result in 0 in integers. Always multiply first, then divide: adc * 3300 / 4095. Also, multiply using 32-bit to avoid overflow ((uint32_t)adc * 3300).

Q. PA0 is set as input pull-up.

A. ADC input must be analog input (0x0). If digital input Buffer/Pull-up is enabled, measurements will be distorted.

Summary

  • ADC converts analog voltage to 12-bit integers (0~4095).
  • mV = adc × 3300 / 4095 (multiply then divide, calculated in 32-bit).
  • ADC clock ≤14MHz — divide with ADCPRE.
  • For F1, convert after calibration (RSTCAL/CAL), then read DR after EOC polling.
  • Set ADC pins as analog input (0x0).

Try it yourself

Solve the assignment below. The reference solution is shown below.

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/stm32_adc/main.cC
#include "stm32f10x.h"

/*
 *Lesson 20 Example — Single conversion of ADC1 Channel 0 (PA0).
 *Target: STM32F103C8, Compiler: Keil MDK-ARM.
 *
 *Read the voltage of PA0 with a 12-bit ADC (0~4095) and convert it to millivolts.
 *mV = adc * 3300 / 4095   (assuming reference voltage 3.3V)
 *For F1 ADC, after turning it on, calibration (RSTCAL → CAL) must be done once to be accurate.
 */

static void short_delay(void)
{
    volatile uint32_t i;
    for (i = 0; i < 10000u; i++) { }   /*tSTAB short wait for stabilization*/
}

static void adc1_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_ADC1EN;
    RCC->CFGR    |= RCC_CFGR_ADCPRE_DIV6;       /*ADC clock 72/6=12MHz (≤14MHz)*/

    /*PA0 = analog input (CNF=00, MODE=00 → 0x0)*/
    GPIOA->CRL &= ~(0xFu << (0u * 4u));

    ADC1->SQR3  = 0u;                            /*Primary conversion channel = 0 (PA0)*/
    ADC1->SMPR2 |= (7u << (3u * 0u));            /*Channel 0 sample time 239.5 cycle*/

    ADC1->CR2 |= ADC_CR2_ADON;                   /*ADC power ON*/
    short_delay();

    ADC1->CR2 |= ADC_CR2_RSTCAL;                 /*Reset calibration register*/
    while (ADC1->CR2 & ADC_CR2_RSTCAL) { }
    ADC1->CR2 |= ADC_CR2_CAL;                    /*Start calibration*/
    while (ADC1->CR2 & ADC_CR2_CAL) { }          /*Wait for calibration complete*/
}

static uint16_t adc1_read(void)
{
    ADC1->CR2 |= ADC_CR2_ADON;                   /*ADON reset = start conversion*/
    while (!(ADC1->SR & ADC_SR_EOC)) { }         /*Wait for conversion complete (EOC)*/
    return (uint16_t)(ADC1->DR & 0x0FFFu);       /*12-bit result*/
}

int main(void)
{
    volatile uint16_t adc_val = 0;
    volatile uint32_t mv = 0;

    adc1_init();

    while (1) {
        adc_val = adc1_read();
        mv = (uint32_t)adc_val * 3300u / 4095u;  /*Conversion to millivolts*/
    }
}
src/stm32_adc/pc_test.cC
#include <stdio.h>
#include <stdint.h>

/*
 *Lesson 20 PC Verification — Converting ADC counts (0~4095) to millivolts.
 *mV = adc * 3300 / 4095   (Reference voltage 3.3V, 12-bit)
 *Since it is integer arithmetic, multiply first then divide to reduce error.
 */

static uint32_t adc_to_mv(uint16_t adc)
{
    return (uint32_t)adc * 3300u / 4095u;
}

int main(void)
{
    uint16_t samples[5] = { 0u, 1024u, 2048u, 3072u, 4095u };
    int i;

    for (i = 0; i < 5; i++) {
        printf("ADC=%u -> %lu mV\n",
               samples[i], (unsigned long)adc_to_mv(samples[i]));
    }
    return 0;
}

Assignment

Problem 1 is pure logic (PC gcc), Problem 2 is MDK (STM32F103C8, μVision simulator).


Problem 1 — Convert ADC Counts to Percent (%)

  • File name: homework_01/main.c
  • Key Concept: Scale conversion, order of integer division
  • Platform: Common (PC gcc)

Requirements

  • Convert ADC counts (04095) to 0100% and output. pct = adc × 100 / 4095.
  • Input is {0, 1024, 2048, 3071, 4095}.
  • The format ADC=N -> P% must match the expected output exactly, character by character.

Expected output

ADC=0 -> 0%
ADC=1024 -> 25%
ADC=2048 -> 50%
ADC=3071 -> 74%
ADC=4095 -> 100%

Hint

  • pct = (uint32_t)adc * 100u / 4095u; (multiply then divide).
  • 3071 becomes 74 instead of 75 due to integer truncation (natural result of truncation).

Problem 2 — LED Control with ADC Threshold

  • File name: homework_02/main.c
  • Key Concept: ADC channel 1 (PA1), threshold comparison, LED control
  • Platform: MDK (STM32F103C8)

Requirements

  • Read ADC1 channel 1 (PA1), if the value is greater than 2048 (about half), turn on PC13 LED, otherwise turn it off (active low).
  • Perform calibration and conversion sequence the same as in the example.

Expected Operation (Simulator)

  • The first channel of SQR3 is 1, PA1 is analog input (0x0).
  • If ADC value > 2048 then PC13=0 (LED ON), else PC13=1 (OFF). Check adc_val and GPIOC ODR with Watch.

Hint

  • Change channel: ADC1->SQR3 = 1u; and set PA1 as analog input (clear bits [7:4] of GPIOA->CRL).
  • LED: if (v > 2048u) GPIOC->BRR = (1u<<13); else GPIOC->BSRR = (1u<<13);.

Check your answers

After solving it yourself, compare it with the reference solution below. The solution files include comments on key points and common mistakes.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

homework_01/main.cC
#include <stdio.h>
#include <stdint.h>

/*
 *Answer 1 — Convert ADC counts to percent (%)
 *
 *Key Points:
 *- pct = adc × 100 / 4095. Multiplying before dividing results in smaller integer errors.
 *- Full scale 4095 corresponds to 100% (2^12 - 1).
 *
 *Common mistake:
 *- Doing adc/4095 first can result in 0 (multiply first).
 *- Truncation can cause 3071 to be 74 instead of 75, which may be mistaken as a bug.
 */

int main(void)
{
    uint16_t samples[5] = { 0u, 1024u, 2048u, 3071u, 4095u };
    int i;

    for (i = 0; i < 5; i++) {
        uint32_t pct = (uint32_t)samples[i] * 100u / 4095u;
        printf("ADC=%u -> %lu%%\n", samples[i], (unsigned long)pct);
    }
    return 0;
}
homework_02/main.cC
#include "stm32f10x.h"

/*
 *Answer 2 (MDK, STM32F103C8) — Controlling LED with ADC threshold
 *
 *Key Points:
 *- Put channel 1 (PA1) in the first position of SQR3 and set PA1 as analog input (0x0).
 *- If the conversion result is greater than half (2048), turn ON LED on PC13 (active low = 0).
 *
 *Common mistake:
 *- Changing the channel to 1 but leaving PA0 as analog (pin/channel mismatch).
 *- Forgetting that it is active low, so ON is 0.
 *
 *Expected behavior (simulator): adc_val > 2048 → PC13=0 (LED ON), otherwise PC13=1 (OFF).
 */

#define LED_PIN   13u

static void short_delay(void)
{
    volatile uint32_t i;
    for (i = 0; i < 10000u; i++) { }
}

static void adc1_init(void)
{
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPCEN | RCC_APB2ENR_ADC1EN;
    RCC->CFGR    |= RCC_CFGR_ADCPRE_DIV6;

    GPIOA->CRL &= ~(0xFu << (1u * 4u));          /*PA1 analog input*/

    GPIOC->CRH &= ~(0xFu << ((LED_PIN - 8u) * 4u));
    GPIOC->CRH |=  (0x2u << ((LED_PIN - 8u) * 4u));

    ADC1->SQR3  = 1u;                            /*1st conversion channel = 1 (PA1)*/
    ADC1->SMPR2 |= (7u << (3u * 1u));            /*Channel 1 sample time 239.5 cycles*/

    ADC1->CR2 |= ADC_CR2_ADON;
    short_delay();
    ADC1->CR2 |= ADC_CR2_RSTCAL; while (ADC1->CR2 & ADC_CR2_RSTCAL) { }
    ADC1->CR2 |= ADC_CR2_CAL;    while (ADC1->CR2 & ADC_CR2_CAL)    { }
}

static uint16_t adc1_read(void)
{
    ADC1->CR2 |= ADC_CR2_ADON;
    while (!(ADC1->SR & ADC_SR_EOC)) { }
    return (uint16_t)(ADC1->DR & 0x0FFFu);
}

int main(void)
{
    volatile uint16_t adc_val = 0;

    adc1_init();

    while (1) {
        adc_val = adc1_read();
        if (adc_val > 2048u) {
            GPIOC->BRR  = (1u << LED_PIN);   /*LED ON (active low)*/
        } else {
            GPIOC->BSRR = (1u << LED_PIN);   /* LED OFF */
        }
    }
}

Learn this lesson with a mentor

Work through the Embedded C track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now