Skip to content
Home » Embedded Systems » Basic Electronics » ADC in Microcontrollers: How It Works, Reading Patterns, and Common Pitfalls

ADC in Microcontrollers: How It Works, Reading Patterns, and Common Pitfalls

Key Takeaways

  • An ADC converts a continuous analog voltage to a discrete integer. Quality is set by resolution (bits) and sampling rate (samples per second).
  • The reference voltage (VREF) is the silent killer — a noisy VREF makes every reading noisy, regardless of bit count.
  • 10-bit vs 12-bit isn’t always worth it. Past ~10 effective bits, shielding and grounding beat resolution. Datasheet ENOB (effective number of bits) matters more than nominal bits.
  • Three read patterns: polling (simplest, blocks the CPU briefly), interrupt-driven (CPU does work during conversion), DMA (hardware streams without CPU).
  • Source impedance matters — the ADC’s sample-and-hold capacitor needs to charge to the input voltage during the sampling window. High-impedance sources (>10 kΩ) need an op-amp buffer.
  • For software cleanup of noisy readings — averaging, EMA, oversampling — see the companion post on filtering noisy ADC readings.

Every embedded engineer touches the ADC. It is the bridge between the analog world — sensors, voltages, temperatures, currents — and the digital world your microcontroller actually understands. And every embedded engineer eventually hits the same wall: the ADC reads something, but the numbers are wrong. They drift, they jitter, they don’t match the multimeter. This post explains what is actually happening inside the ADC, the five things that determine reading quality, the three ways to read it in firmware, and the pitfalls that cause those frustrating readings.

What an ADC actually does

An Analog-to-Digital Converter takes a continuous voltage and approximates it as an integer. With a 10-bit ADC and a 5 V reference:

  • 0 V → integer 0
  • 5 V → integer 1023 (210 − 1)
  • 2.5 V → integer ~511

integer = (Vin / VREF) × (2n − 1)

This step size — the smallest voltage difference the ADC can detect — is the resolution. On a 10-bit ADC with VREF = 5 V, each step is ~4.88 mV (5 V / 1024). On 12-bit with the same reference, ~1.22 mV per step. On 16-bit, ~76 μV.

But there is a catch: just because the ADC can report a value does not mean that value is meaningful. Internal noise, VREF noise, and source impedance all degrade the usable resolution. The datasheet term for this is ENOB (Effective Number Of Bits), and it is usually 1–2 bits below the nominal bit count. A “12-bit” ADC in a noisy environment may give you only 10 usable bits.

The five things that determine ADC quality

FactorWhat it controlsTypical embedded value
Resolutionsmallest detectable voltage step8–12 bits (MCU built-in), 16–24 bits (external)
Sampling ratehow often you can read100 kSPS to 2 MSPS (MCU built-in)
Reference voltagethe voltage that maps to integer maxVDD, internal 1.1 V, external precision VREF IC
Input impedancehow the ADC loads the sourcedatasheet 10–100 MΩ; sample window limits effective Z
Noise floorminimum noise level — sets ENOBtypically 1–3 LSBs

Three of these are set in hardware design (resolution, VREF source, input impedance handling). Two are tunable in firmware (sampling rate, sample-and-hold time). All five interact — a 12-bit ADC fed from a noisy 5 V rail through a 1 MΩ source is not really a 12-bit ADC.

Conversion methods

Most embedded ADCs use one of three techniques:

MethodSpeedAccuracyBest for
Successive Approximation (SAR)medium-fastmediumgeneral-purpose MCU ADCs (Arduino, STM32, ESP32)
Sigma-Deltaslowvery highprecision measurement, audio, weight scales
Flashvery fastlow-mediumhigh-frequency signals (oscilloscopes, RF)

The Arduino UNO and the vast majority of MCU ADCs are SAR — fast enough for sensors and slow signals, accurate enough for most real-world embedded work. When SAR is not enough (lab instrumentation, weight scales, load cells), an external sigma-delta like the ADS1115 (16-bit, I²C) or HX711 (24-bit, dedicated) is the next step.

Reading the ADC: three patterns

1. Polling

The simplest pattern. Trigger a conversion, wait for it to finish, read the result. Blocks the CPU for the conversion time — typically 1 to 100 microseconds depending on clock and ADC bit count.

// Arduino — blocks for ~104 μs per call on UNO
int reading = analogRead(A0);

Use when: occasional reads, simple main loop, no other time-critical work.

2. Interrupt-driven

Trigger a conversion, do other work, get an interrupt when it is done. Better CPU utilisation, especially if you sample regularly.

// STM32 HAL
HAL_ADC_Start_IT(&hadc1);    // start with interrupt enabled

// ISR fires this callback when the conversion completes
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
    uint32_t value = HAL_ADC_GetValue(hadc);
    // process …
}

Use when: regular sampling at a fixed rate, the CPU has other useful work to do during the conversion.

3. DMA (Direct Memory Access)

Configure the ADC and the DMA controller to fill a buffer in memory without CPU involvement. The CPU only wakes when the buffer is half-full or full. This is the right pattern for any streaming application — audio, oscilloscope-style logging, anything sampled at >1 kSPS.

// STM32 HAL — stream into a 256-sample buffer
#define BUF_SIZE 256
uint16_t adc_buffer[BUF_SIZE];

HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, BUF_SIZE);

// half-buffer ready: HAL_ADC_ConvHalfCpltCallback
// full buffer    : HAL_ADC_ConvCpltCallback
// process whichever half is stable while the other fills.

Use when: streaming sensor data, the CPU must remain responsive to other tasks, sampling rate is high enough that polling/IRQ overhead matters.

Arduino code example — averaging for cleaner reads

The most common embedded technique for cleaning up ADC noise: average several consecutive readings. With 16 samples averaged, the noise is reduced by a factor of 4 (square root of 16).

const int   SENSOR_PIN = A0;
const int   N_SAMPLES  = 16;     // averaging count
const float V_REF      = 5.0f;   // VCC (use a precision reference for accuracy)

void setup() {
    Serial.begin(115200);
    analogReference(DEFAULT);    // use VCC as reference
}

void loop() {
    long sum = 0;
    for (int i = 0; i < N_SAMPLES; i++) {
        sum += analogRead(SENSOR_PIN);
    }
    int   avg_raw = sum / N_SAMPLES;
    float voltage = (avg_raw * V_REF) / 1023.0f;

    Serial.print("Raw: ");     Serial.print(avg_raw);
    Serial.print("  Voltage: "); Serial.println(voltage, 3);

    delay(100);
}

This is the simplest form of filtering. For more advanced approaches (median, exponential moving average, oversampling), see the companion post on filtering noisy ADC readings.

STM32 HAL example

For more demanding applications, the STM32 ADC offers 12-bit (or higher on F3/H7), multiple channels, and tight timing control. Here is a single conversion in polling mode:

// Configured via CubeMX: ADC1, channel 0, 12-bit, polling
uint32_t adc_value = 0;
float    voltage   = 0.0f;
const float V_REF  = 3.3f;

HAL_ADC_Start(&hadc1);
if (HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY) == HAL_OK) {
    adc_value = HAL_ADC_GetValue(&hadc1);
    voltage   = (adc_value * V_REF) / 4095.0f;   // 12-bit: 2^12 − 1
}
HAL_ADC_Stop(&hadc1);

At register level (no HAL), the same single conversion is just:

ADC1->CR  |= ADC_CR_ADSTART;             // start
while (!(ADC1->ISR & ADC_ISR_EOC));      // wait for end-of-conversion
uint32_t value = ADC1->DR;               // read 12-bit value

Common pitfalls and how to avoid them

1. High-impedance source

ADCs have a tiny sample-and-hold capacitor (a few pF). During the sampling window, this capacitor must charge to the input voltage through the source impedance. If your source is high-impedance — for example, a voltage divider built from megohm-scale resistors, or a sensor with a high output Z — the capacitor never fully charges, and your reading is wrong.

Fix: buffer with a unity-gain op-amp. Output impedance of the op-amp is in single-digit ohms, the ADC’s S/H charges instantly. See the operational amplifier introduction for the standard buffer circuit.

2. Noisy VREF

If VREF wobbles, every ADC reading wobbles with it. Using VCC as VREF is convenient but VCC is rarely clean — especially if you have switching loads (motors, servos, LCDs, RF transmitters) on the same rail.

Fix: use the MCU’s internal voltage reference (often 1.1 V or 2.5 V — check your datasheet), or add an external precision VREF IC (LM4040, REF3030). Always decouple AVCC with 100 nF and 10 μF in parallel.

3. Multi-channel cross-talk

Switching from channel 5 to channel 2 does not instantly discharge the sample-and-hold capacitor. The previous channel’s residue can leak into the new reading — especially if the impedance of channel 2’s source is high.

Fix: insert a dummy conversion between channel switches and discard the result; or extend the sample time on the new channel. Most HAL implementations let you set per-channel sample time.

4. Missing decoupling and shared grounds

AVCC and AGND on the MCU need their own decoupling — typically a 100 nF ceramic and a 10 μF bulk cap, ideally with a ferrite bead or small inductor separating the analogue supply from the digital supply. Sharing one ground plane for digital switching currents and analogue measurement currents causes ground bounce that shows up directly in the ADC reading.

Fix: dedicated analogue ground plane connected to the digital ground at a single star-point at the regulator. See signal conditioning for sensors for board-layout patterns.

5. Quantisation noise / unstable readings

Even with a perfect hardware setup, the LSB jitters one count up and down — that is the fundamental limit of any ADC. The fix is in firmware: average several readings, run an exponential moving average, or oversample-and-downsample to gain effective bits.

Fix: covered in detail in the companion post on filtering noisy ADC readings.

Calibration: converting raw counts to engineering units

Raw ADC counts are not useful on their own. You need to convert to volts, then to the actual quantity you are measuring. For a linear sensor like the LM35 temperature sensor (10 mV/°C output):

float adc_to_celsius(int adc_raw) {
    float voltage       = (adc_raw * V_REF) / 1023.0f;   // raw → volts
    float temperature_c = voltage * 100.0f;              // 10 mV/°C inverted
    return temperature_c;
}

For non-linear sensors (NTC thermistors, FlexiForce, gas sensors, photodiodes) you need either a Steinhart-Hart equation, a polynomial fit, or a lookup table. The FlexiForce calibration guide shows a worked polynomial-fit example with real Instron-applied force data.

When you should use an external ADC instead

The built-in MCU ADC is fine for most use cases. Reach for an external part when:

NeedExternal part typeExample
>12 bits of effective resolutionsigma-delta over I²C/SPIADS1115 (16-bit, I²C)
Strain gauges, load cellsprecision delta-sigma, dedicatedHX711 (24-bit)
Audio capturededicated audio codecWM8731, PCM5102 (I²S)
Multi-channel simultaneous samplingSAR with sample-and-hold per channelADS8332
Isolation (mains-side measurement)isolated ADCAMC1306

Do not reach for an external ADC just because you can. The MCU’s built-in is usually good enough if you respect the five quality factors above — and external parts add cost, board space, and one more communication bus to debug.

Conclusion — getting it right

The ADC is hardware that respects physics. If your readings are bad, the cause is almost always one of:

  1. The reference voltage is dirty — fix with a stable VREF source plus proper decoupling.
  2. The source impedance is too high — fix with an op-amp buffer.
  3. The sample rate is wrong — fix by sampling at ≥2× the highest signal frequency you care about (Nyquist).
  4. The board layout is sloppy — fix with separated analogue / digital ground planes and star-point grounding.
  5. The readings are noisy at the LSB level — fix in firmware via averaging, filtering, or oversampling.

Items 1–4 are hardware. Item 5 is the easiest to fix and deserves its own post — see filtering noisy ADC readings for the practical software techniques: moving average, median filter, exponential moving average, oversampling, and hysteresis.

Related Reading

Frequently Asked Questions

What is the difference between 10-bit and 12-bit ADC?

A 10-bit ADC has 210 = 1024 possible output values; 12-bit has 212 = 4096. On the same reference voltage, the 12-bit can resolve 4× smaller voltage differences. In practice, the effective number of bits (ENOB) is usually 1–2 less than nominal due to internal noise, so a 12-bit ADC typically delivers about 10–11 usable bits in a real circuit.

How do I read an ADC value in Arduino?

Call analogRead(pin) on any analogue input (A0–A5 on UNO). It returns an integer 0–1023 representing 0 V to VCC (typically 5 V). Each step is about 4.88 mV. To convert to volts, multiply by 5.0 / 1023.

What is reference voltage in ADC?

The reference voltage (VREF) is the voltage the ADC equates to its maximum output value. With VREF = 5 V on a 10-bit ADC, the output 1023 means “input is at the reference voltage.” Using a noisy or drifting VREF directly degrades every measurement — for precision use an external voltage reference IC or the MCU’s internal reference.

Why are my ADC readings unstable?

The usual culprits, in order: (1) noisy VREF — decouple AVCC properly and consider an internal/external reference; (2) high-impedance source overloading the sample-and-hold capacitor — buffer with a unity-gain op-amp; (3) missing or shared analogue ground — separate AGND from DGND, star-ground at the regulator. Once hardware is solid, software filtering (averaging, EMA, oversampling) cleans up the residual quantisation noise.

What is the sampling rate of an Arduino ADC?

The Arduino UNO’s ADC takes about 104 μs per analogRead(), giving a maximum sampling rate of about 9.6 kSPS (kilo-samples per second). This is enough for slow signals (sensors, knobs, temperature) but too slow for audio. The ADC clock prescaler can be tuned via the ADCSRA register for faster sampling at the cost of accuracy.

Can I use a microcontroller ADC for audio?

Marginal at best. Audio needs ≥44.1 kSPS at 16-bit minimum. Most MCU ADCs (10–12 bit, up to ~1 MSPS) can technically capture audio but with poor dynamic range and audible noise. For audio applications, use a dedicated audio codec IC (WM8731, PCM5102) or an external 16–24 bit ADC over I²S.

📖 Related: LM35 Temperature Sensor: Working, Circuit, and Arduino Code

Leave a Reply

Your email address will not be published. Required fields are marked *