Almost every embedded project I’ve built eventually needs to sense something from the physical world — a temperature, a light level, a battery voltage, a joystick position — and the physical world doesn’t speak in 1s and 0s. That’s where the ADC comes in. It’s the bridge between continuous, analog reality and the discrete, digital world microcontrollers operate in. In this article, I’ll walk through what an ADC actually does, how it works internally, how to configure and use one, and the practical gotchas that trip people up.
What Is an ADC?
An Analog-to-Digital Converter (ADC) is a peripheral (or standalone chip) that samples a continuously varying analog voltage and converts it into a discrete digital value that the CPU can read, store, and process. Nearly every microcontroller intended for sensor work — STM32, AVR, ESP32, PIC — has one or more ADC peripherals built in.
graph LR
A[Analog Signal - e.g. 0-3.3V] --> B[Sample & Hold]
B --> C[Quantizer]
C --> D[Digital Value - e.g. 0-4095]
D --> E[CPU / Memory]
Key ADC Concepts
Resolution
Resolution defines how many discrete steps the ADC can represent, expressed in bits. A 12-bit ADC divides its input voltage range into 2^12 = 4096 discrete steps (0 to 4095). A 10-bit ADC (common on classic AVR chips like the ATmega328) offers 1024 steps. Higher resolution means finer distinction between voltage levels, at the cost of conversion time and complexity.
Step size (LSB) = V_ref / 2^resolution
Example (12-bit, 3.3V reference): 3.3V / 4096 ≈ 0.8mV per step
Reference Voltage (V_ref)
The ADC measures the input voltage relative to a reference voltage. If V_ref is 3.3V and the ADC reads the maximum digital value, that corresponds to (approximately) 3.3V at the input pin. Using an accurate, stable, low-noise reference is critical — a noisy or drifting V_ref translates directly into noisy or drifting readings, even if the sensor itself is perfectly stable.
Sampling Rate
This is how many conversions per second the ADC can perform, governed by the Nyquist-Shannon sampling theorem: to accurately capture a signal, you must sample at least twice the highest frequency component present in that signal. For slow-changing signals like temperature, a few samples per second is plenty; for audio or vibration analysis, you might need tens of kHz of sampling rate.
Quantization Error
Because the ADC can only represent a finite number of discrete levels, there’s always some rounding — the actual analog value gets “snapped” to the nearest representable digital step. This introduces a small, unavoidable quantization error, typically ±0.5 LSB (least significant bit) for a well-designed ADC.
How a Successive Approximation ADC (SAR ADC) Works
Most microcontroller ADCs use the Successive Approximation Register (SAR) architecture, since it offers a good balance of speed, resolution, and simplicity.
sequenceDiagram
participant Input as Analog Input
participant SAR as SAR Logic
participant DAC as Internal DAC
participant Comp as Comparator
Input->>Comp: Sampled voltage held
SAR->>DAC: Set MSB = 1, rest 0
DAC->>Comp: Compare DAC output vs input
Comp-->>SAR: Input > DAC? Keep bit : Clear bit
SAR->>DAC: Move to next bit, repeat
Note over SAR: Repeats for each bit (12 cycles for 12-bit ADC)
SAR->>SAR: Final digital value ready
The SAR ADC works like a game of “guess the number” using a binary search: it starts by guessing the most significant bit is 1, compares that guess (via an internal DAC and comparator) against the actual input, keeps the bit if the guess was too low, clears it if too high, and moves to the next bit — repeating until every bit of the result has been resolved. This is why a SAR ADC conversion takes roughly N clock cycles for an N-bit result.
Configuring ADC on an STM32 (HAL Example)
#include "stm32f4xx_hal.h"
ADC_HandleTypeDef hadc1;
void ADC1_Init(void) {
ADC_ChannelConfTypeDef sConfig = {0};
hadc1.Instance = ADC1;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = DISABLE;
hadc1.Init.ContinuousConvMode = DISABLE;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
HAL_ADC_Init(&hadc1);
sConfig.Channel = ADC_CHANNEL_0; // PA0
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_84CYCLES;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}
uint16_t ADC1_Read(void) {
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, 100);
uint16_t value = HAL_ADC_GetValue(&hadc1);
HAL_ADC_Stop(&hadc1);
return value;
}
float ADC_ToVoltage(uint16_t raw) {
return (raw * 3.3f) / 4095.0f; // 12-bit, 3.3V reference
}
Configuring ADC on Arduino
void setup() {
Serial.begin(9600);
// analogReference(DEFAULT); // Uses 5V (or 3.3V on some boards)
}
void loop() {
int raw = analogRead(A0); // 10-bit result: 0-1023
float voltage = raw * (5.0 / 1023.0);
Serial.print("Raw: ");
Serial.print(raw);
Serial.print(" Voltage: ");
Serial.println(voltage);
delay(500);
}
Configuring ADC on ESP32 (Arduino Framework)
void setup() {
Serial.begin(115200);
analogReadResolution(12); // ESP32 supports up to 12-bit
analogSetAttenuation(ADC_11db); // Extends input range to ~3.3V
}
void loop() {
int raw = analogRead(34); // ADC1 channel on GPIO34
float voltage = (raw / 4095.0) * 3.3;
Serial.println(voltage);
delay(500);
}
Interrupt and DMA-Driven ADC Sampling
For continuous sensor monitoring (say, sampling an audio signal or a fast-changing sensor at a fixed rate), polling HAL_ADC_PollForConversion() repeatedly wastes CPU cycles. A far more efficient approach uses a timer to trigger conversions automatically and DMA to move each result into a buffer without CPU intervention:
#define BUFFER_SIZE 256
uint16_t adc_buffer[BUFFER_SIZE];
void ADC_DMA_Init(void) {
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, BUFFER_SIZE);
// Timer (e.g. TIM3) configured to trigger ADC conversions at a fixed rate
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
// Buffer is full - process adc_buffer[] here
process_samples(adc_buffer, BUFFER_SIZE);
}
graph LR
Timer[Hardware Timer] -->|Trigger| ADC[ADC Peripheral]
ADC -->|DMA Channel| Buffer[Memory Buffer]
Buffer -->|Interrupt on complete| CPU[CPU: Process batch]
Multi-Channel Scanning
Most microcontroller ADCs support scanning multiple input channels in sequence, useful when reading several sensors sharing one ADC peripheral:
ADC_ChannelConfTypeDef sConfig = {0};
hadc1.Init.ScanConvMode = ENABLE;
hadc1.Init.NbrOfConversion = 3;
sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = 1; HAL_ADC_ConfigChannel(&hadc1, &sConfig);
sConfig.Channel = ADC_CHANNEL_1; sConfig.Rank = 2; HAL_ADC_ConfigChannel(&hadc1, &sConfig);
sConfig.Channel = ADC_CHANNEL_4; sConfig.Rank = 3; HAL_ADC_ConfigChannel(&hadc1, &sConfig);
Noise Reduction Techniques
Analog signals are inherently susceptible to noise, and I’ve spent more debugging hours than I’d like chasing “jittery” ADC readings that turned out to be entirely normal electrical noise rather than a bug. A few techniques that consistently help:
- Averaging/oversampling: Take multiple samples and average them; this reduces random noise at the cost of a slower effective sample rate.
- Software low-pass filtering: A simple exponential moving average (
filtered = filtered*0.9 + new_sample*0.1) smooths out noisy readings cheaply. - Hardware filtering: A small RC (resistor-capacitor) low-pass filter on the analog input pin, physically on the PCB, can remove high-frequency noise before it even reaches the ADC.
- Separate analog and digital ground planes: On PCB designs, keeping noisy digital switching currents away from sensitive analog reference and input traces significantly improves ADC accuracy.
#define ALPHA 0.1f
float filtered_value = 0;
void update_filter(float new_sample) {
filtered_value = ALPHA * new_sample + (1 - ALPHA) * filtered_value;
}
Real-World Applications
- Battery voltage monitoring: Reading a divided-down battery voltage through an ADC to estimate remaining charge in a portable IoT device.
- Analog sensors: Potentiometers, LDRs (light sensors), thermistors, gas sensors, and many pressure sensors output a raw analog voltage proportional to the measured quantity.
- Audio sampling: Basic audio input (like for a simple voice-activity detector) can be captured through an ADC sampling at several kHz.
- Joystick/analog controls: Game controllers and industrial control panels commonly use potentiometer-based analog sticks read via ADC.
- Current sensing: Reading the voltage across a shunt resistor (often through an amplifier) to measure current draw in power management systems.
IoT Integration Example: Soil Moisture Monitor
#define MOISTURE_PIN 34
void setup() {
Serial.begin(115200);
analogReadResolution(12);
}
void loop() {
int raw = analogRead(MOISTURE_PIN);
// Calibrate: raw ~ 3000 (dry) to ~1200 (wet), adjust per sensor
int moisture_percent = map(raw, 3000, 1200, 0, 100);
moisture_percent = constrain(moisture_percent, 0, 100);
Serial.printf("Soil Moisture: %d%%\n", moisture_percent);
// publish_to_mqtt(moisture_percent); // send to cloud/broker
delay(60000); // check once per minute
}
Accuracy, Calibration, and Non-Idealities
Real ADCs aren’t perfect. Datasheets typically specify:
- Offset error: A constant deviation between the ideal and actual transfer function at zero input.
- Gain error: A deviation in the slope of the transfer function, growing with input voltage.
- INL/DNL (Integral/Differential Non-Linearity): Deviations from a perfectly straight, evenly-stepped conversion curve.
For applications needing higher accuracy than a raw ADC read provides, a two-point calibration against known reference voltages can correct for offset and gain error in software:
float calibrated_voltage(uint16_t raw) {
// Measured raw=100 at 0.1V, raw=4000 at 3.2V (example calibration)
float slope = (3.2f - 0.1f) / (4000 - 100);
return 0.1f + (raw - 100) * slope;
}
Performance and Power Considerations
Faster sampling and higher resolution both come at a power cost — internal reference circuits and comparators draw current whenever active. Many microcontrollers let you power down the ADC between readings, and some (like the ESP32 in deep sleep) can wake, take a single reading, and go back to sleep, which is essential for battery-powered sensor nodes that need to last months or years on a coin cell or small battery pack.
Frequently Asked Questions
Why do my ADC readings jump around even when the input is steady? This is typically noise — from the power supply, nearby switching circuits (like PWM signals or Wi-Fi radios), or an unstable reference voltage. Averaging multiple samples and adding a small hardware RC filter usually resolves it.
Can an ADC measure negative voltages? Standard microcontroller ADCs typically only measure voltages between 0V and V_ref (unipolar). Measuring negative or bipolar signals requires level-shifting circuitry to bring the signal into the ADC’s valid input range, or a differential/bipolar ADC designed for that purpose.
What’s the difference between ADC resolution and accuracy? Resolution is how many discrete steps the ADC can represent; accuracy is how close those readings are to the true analog value. A 16-bit ADC with poor reference stability or high noise can be less accurate in practice than a well-designed 12-bit ADC.
How do I choose the right sampling rate? Per the Nyquist theorem, sample at least twice as fast as the highest frequency component you care about in your signal — but in practice, sampling well beyond that (4x–10x) and filtering gives cleaner results for most sensor applications.
Summary
An ADC is the essential bridge between the analog physical world and digital microcontroller logic, converting a continuous voltage into a discrete numeric value through processes like successive approximation. Understanding resolution, reference voltage, sampling rate, and noise sources is what separates “it kind of works” ADC code from genuinely reliable sensor readings — and for anything beyond occasional single readings, offloading sampling to a timer-triggered, DMA-backed pipeline frees the CPU while giving you clean, consistent data.
References and Further Reading
- STMicroelectronics STM32 ADC Peripheral Reference Manual — st.com
- Espressif ESP32 ADC Calibration and Driver Documentation — docs.espressif.com
- Arduino analogRead() Reference — docs.arduino.cc/language-reference/en/functions/analog-io/analogread
- Microchip/Atmel AVR ADC Documentation — microchip.com
- Texas Instruments “Understanding SAR ADCs” Application Note — ti.com
- Analog Devices ADC Basics and Tutorials — analog.com
