The real world is analog. Temperature, pressure, light, sound, vibration — none of it arrives as clean digital ones and zeros. Every embedded system I’ve built that interacts with the physical world eventually comes down to the same core problem: how do I take a continuously varying voltage and turn it into a number a microcontroller can actually compute with, reliably and accurately? In this article I’ll walk through the full pipeline, from the physical sensor to the digital value sitting in a variable in my firmware.
The Analog-to-Digital Pipeline
graph LR
A[Physical Phenomenon] --> B[Sensor / Transducer]
B --> C[Signal Conditioning]
C --> D[Anti-Aliasing Filter]
D --> E[ADC - Analog to Digital Converter]
E --> F[Digital Value in MCU]
F --> G[Firmware Processing / Calibration]
G --> H[Application Logic]
Every stage in this chain exists because analog signals are messy: they’re noisy, they can be too weak or too strong for the ADC’s input range, and they can contain frequency content that will corrupt the digital result if not handled correctly.
Step 1: The Sensor Converts a Physical Quantity to a Voltage or Current
A thermistor changes resistance with temperature. A piezoelectric element generates a voltage under mechanical stress. A photodiode generates current proportional to light intensity. None of these produce a signal the ADC can directly digitize cleanly — which is where signal conditioning comes in (covered in depth in the next article of this series).
Step 2: Analog-to-Digital Conversion
The ADC is the actual bridge between analog and digital. I need to understand a few key parameters every time I select or configure one:
- Resolution (bits): a 12-bit ADC divides its reference voltage into 4096 discrete steps; a 16-bit ADC into 65536 steps. More bits mean finer granularity but not necessarily more accuracy.
- Sampling rate: how many conversions per second the ADC can perform.
- Reference voltage (Vref): defines the full-scale input range; a poorly chosen or noisy Vref directly corrupts every reading.
- Input impedance and settling time: the source driving the ADC input must be able to charge the internal sample-and-hold capacitor within the sampling window, or readings will be inaccurate.
ADC Architectures I Commonly Work With
| Type | How It Works | Typical Use Case |
|---|---|---|
| Successive Approximation Register (SAR) | Binary search comparing input to a DAC output | General-purpose MCU ADCs (most common) |
| Sigma-Delta (ΔΣ) | Oversampling + noise shaping + digital filtering | High-resolution, low-frequency signals (weigh scales, precision temperature) |
| Flash ADC | Bank of comparators, instantaneous conversion | Very high-speed, low-resolution (oscilloscopes) |
| Pipeline ADC | Multi-stage successive conversion | High-speed, moderate-to-high resolution (video, radar) |
Nyquist Theorem and Aliasing
One of the most common mistakes I see in embedded designs is ignoring the sampling theorem. The Nyquist-Shannon theorem states that to accurately reconstruct a signal, the sampling rate must be at least twice the highest frequency component in that signal. If I sample slower than that, higher frequencies “fold back” into my sampled data as false, lower-frequency signals — a phenomenon called aliasing.
graph TD
A[Real signal: 6kHz sine wave] --> B{Sampling Rate}
B -->|Sampled at 20kHz - correct| C[Accurately reconstructed 6kHz signal]
B -->|Sampled at 8kHz - undersampled| D[Aliased into false 2kHz signal]
This is exactly why an anti-aliasing filter (a low-pass analog filter placed before the ADC) is standard practice: it removes frequency content above the Nyquist frequency before the ADC ever sees it, so there’s nothing left to alias.
Configuring an ADC: A Practical STM32 Example
Here’s a typical single-channel ADC read on an STM32 using HAL, reading a temperature sensor connected to channel 0:
#include "stm32f4xx_hal.h"
ADC_HandleTypeDef hadc1;
void ADC_Config(void)
{
ADC_ChannelConfTypeDef sConfig = {0};
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = DISABLE;
hadc1.Init.ContinuousConvMode = DISABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
HAL_ADC_Init(&hadc1);
sConfig.Channel = ADC_CHANNEL_0;
sConfig.Rank = 1;
sConfig.SamplingTime = ADC_SAMPLETIME_84CYCLES; /* allow settling time */
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}
float Read_Temperature_C(void)
{
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
uint32_t raw = HAL_ADC_GetValue(&hadc1);
HAL_ADC_Stop(&hadc1);
/* Convert 12-bit raw value to voltage, Vref = 3.3V */
float voltage = (raw / 4095.0f) * 3.3f;
/* Example linear sensor: 10mV per degree C, 0V = 0C */
float temperature = voltage / 0.01f;
return temperature;
}
Note the ADC_SAMPLETIME_84CYCLES — this isn’t arbitrary. It’s set based on the source impedance of the sensor driving the ADC input, calculated so the internal sample-and-hold capacitor has enough time to charge to the true input voltage. Too short a sampling time with a high-impedance source is a very common source of noisy or inaccurate ADC readings in real projects.
Timing Diagram: SAR ADC Conversion
sequenceDiagram
participant CPU
participant ADC
participant SH as Sample & Hold
CPU->>ADC: Start Conversion
ADC->>SH: Sample input (settling time)
SH->>ADC: Hold value
loop Successive Approximation (per bit)
ADC->>ADC: Compare against internal DAC, set/clear bit
end
ADC->>CPU: Conversion complete interrupt / flag
CPU->>ADC: Read digital result register
Multi-Channel and DMA-Driven Acquisition
For real applications I rarely poll a single ADC channel in a blocking loop — that wastes CPU cycles. Instead I configure the ADC to scan multiple channels and use DMA to transfer results directly into a memory buffer without CPU intervention, freeing the processor for other work:
uint16_t adc_buffer[4]; /* 4 channels */
void ADC_DMA_Config(void)
{
hadc1.Init.ScanConvMode = ENABLE;
hadc1.Init.NbrOfConversion = 4;
/* configure channels 0-3 with increasing ranks, omitted for brevity */
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buffer, 4);
/* adc_buffer is now continuously refreshed by hardware via DMA */
}
Calibration and Accuracy
Raw ADC counts are rarely usable directly. I typically apply:
- Offset calibration — correcting for a non-zero reading when the true input is 0V
- Gain calibration — correcting for slope error using known reference points
- Linearization — for non-linear sensors (like thermistors or thermocouples), applying a lookup table or polynomial fit
- Averaging/oversampling — taking multiple samples and averaging to reduce random noise, and in sigma-delta style oversampling, even gaining effective bits of resolution
uint16_t Read_Averaged_ADC(uint8_t samples)
{
uint32_t sum = 0;
for (uint8_t i = 0; i < samples; i++) {
HAL_ADC_Start(&hadc1);
HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);
sum += HAL_ADC_GetValue(&hadc1);
}
return (uint16_t)(sum / samples);
}
Real-World Applications
- IoT environmental sensors reading temperature, humidity, and gas concentration via analog outputs feeding a low-power ADC
- Motor control reading current shunt voltages at high sample rates for closed-loop torque control
- Audio capture using sigma-delta ADCs to digitize microphone input for voice-activated IoT devices
- Battery monitoring measuring cell voltage and current for state-of-charge estimation in battery management systems
Performance and Reliability Considerations
ADC noise is a constant battle. I separate analog and digital ground planes on the PCB, add decoupling capacitors close to the Vref pin, and keep high-speed digital traces away from analog input traces. For reliability, I often add software range-checking on ADC readings (rejecting obviously impossible values, which usually indicates a sensor fault or wiring issue) before that data ever reaches control logic.
Differential vs Single-Ended Analog Inputs
Beyond single-ended signals (measured relative to a common ground), many precision embedded designs use differential signaling, where the quantity of interest is the voltage difference between two lines rather than either line’s absolute voltage relative to ground. This matters enormously for noise rejection: any noise coupled equally onto both lines (common-mode noise, like 50/60Hz mains hum) cancels out when the ADC (or the instrumentation amplifier ahead of it) computes the difference, whereas a single-ended measurement has no such protection.
graph TD
A[Signal+] --> C[Differential ADC Input]
B[Signal-] --> C
D[Common-mode noise couples equally onto both lines] --> A
D --> B
C --> E[Noise cancels in the differential measurement]
Many modern SAR ADCs support differential input pairs directly, and I reach for this mode whenever the sensor itself is inherently differential (bridge sensors, some industrial 4-20mA loop receivers) or when the signal path is long enough that common-mode noise pickup becomes a real concern.
/* STM32 example: configuring a differential ADC channel pair */
sConfig.Channel = ADC_CHANNEL_1;
sConfig.SingleDiff = ADC_DIFFERENTIAL_ENDED;
sConfig.OffsetNumber = ADC_OFFSET_NONE;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
Debugging Analog Signal Chains
When an ADC reading doesn’t match expectations, I work through a consistent debugging sequence rather than guessing:
- Verify with a multimeter/oscilloscope directly at the sensor output before the signal reaches any conditioning circuitry, to confirm the sensor itself is producing the expected voltage for a known physical input.
- Check the signal after each conditioning stage (after amplification, after filtering) to isolate exactly which stage is introducing an error, rather than assuming the whole chain is at fault.
- Verify the ADC reference voltage independently — a noisy or incorrect Vref silently scales every single reading, and it’s one of the most overlooked sources of “mysterious” ADC inaccuracy.
- Check grounding — a poor or shared ground return path between analog and digital circuitry is one of the most common causes of unexplained noise, especially when a design shares a ground plane between a switching power supply and sensitive analog circuitry.
- Compare firmware register configuration against the datasheet — an incorrect sampling time, wrong channel selection, or misconfigured resolution setting produces plausible-looking but wrong numbers that are easy to miss without a careful register-level review.
Optimizing ADC Performance and Throughput
For applications needing high sample rates (audio capture, vibration analysis, motor current sensing), I look at several optimization levers:
- DMA-driven continuous conversion to eliminate CPU polling overhead entirely, as shown earlier, freeing the CPU for other real-time work
- Oversampling and decimation — sampling faster than needed and averaging down, which both reduces noise and can increase effective resolution beyond the ADC’s native bit depth (each doubling of the number of averaged samples with proper dithering can add roughly half a bit of effective resolution)
- Injected vs. regular conversion channels (available on many STM32 parts) to let a high-priority analog measurement interrupt a lower-priority scan sequence without disrupting it
- Parallel ADC instances, when the microcontroller has multiple ADC peripherals, to sample several channels truly simultaneously rather than sequentially, which matters when phase relationships between channels are important (like three-phase motor current sensing)
Real-World Case Study: Motor Current Sensing
A pattern I’ve implemented many times: sensing motor phase current for closed-loop torque control requires the ADC to sample synchronously with the PWM switching cycle, specifically at the midpoint of the PWM “on” time where current ripple is at its average value, not at a random point in the switching cycle.
sequenceDiagram
participant Timer as PWM Timer
participant ADC
participant CPU
Timer->>Timer: Generate PWM waveform for motor phase
Timer->>ADC: Trigger ADC conversion at PWM midpoint (timer compare event)
ADC->>ADC: Sample current sense amplifier output
ADC->>CPU: Conversion complete interrupt with accurate average current
CPU->>CPU: Update PID current control loop
This hardware-triggered, timer-synchronized ADC sampling (rather than software-initiated sampling) is a technique that shows up constantly in professional motor control and power electronics firmware, precisely because software-triggered sampling has too much timing jitter to reliably hit the correct point in the switching cycle.
Analog Signal Handling in Low-Power IoT Designs
Battery-powered devices add another dimension to analog design decisions, tying directly back to the power management topic covered elsewhere in this series. Keeping an analog front end (op-amps, reference voltage sources) continuously powered can quietly dominate a device’s total current draw even when the MCU itself is asleep. Practical techniques I use:
- Power-gating the analog front end with a load switch, enabling it only briefly right before a measurement and disabling it immediately after, rather than leaving bias currents flowing continuously
- Choosing low-power op-amps and voltage references specifically rated for microamp-level quiescent current, even though they typically have less bandwidth than their higher-power counterparts
- Allowing settling time after power-up before triggering the ADC conversion, since a freshly powered analog circuit needs time for its bias points and reference voltage to stabilize — skipping this step is a common source of the first reading after wake-up being noticeably less accurate than subsequent ones
void Low_Power_ADC_Read(float *result)
{
Enable_Analog_Frontend(); /* power on sensor + conditioning circuit */
HAL_Delay(2); /* allow settling time before sampling */
uint16_t raw = Read_ADC_Channel();
*result = Convert_And_Calibrate(raw);
Disable_Analog_Frontend(); /* power off immediately after use */
}
This pattern of power-gate, settle, sample, power-down is exactly the kind of duty-cycled analog handling that lets battery-powered sensor nodes achieve years of operating life while still capturing accurate analog measurements.
The Reverse Path: Digital-to-Analog Conversion
Embedded systems often need to go the other direction too — producing an analog output from a digital value, whether to drive an actuator, generate an audio signal, or control a variable voltage reference. A Digital-to-Analog Converter (DAC) performs this conversion, and the same principles of resolution, reference voltage, and settling time apply in reverse.
/* STM32 example: outputting a computed analog voltage via DAC */
void DAC_Output_Voltage(float voltage, float vref)
{
uint32_t dac_value = (uint32_t)((voltage / vref) * 4095.0f); /* 12-bit DAC */
HAL_DAC_SetValue(&hdac1, DAC_CHANNEL_1, DAC_ALIGN_12B_R, dac_value);
HAL_DAC_Start(&hdac1, DAC_CHANNEL_1);
}
For many actuator control applications, rather than a true DAC, embedded systems use Pulse Width Modulation (PWM) combined with a simple RC low-pass filter to approximate an analog output — a technique that’s cheaper and more widely available across MCU families than a dedicated DAC peripheral, at the cost of some output ripple that the filter must adequately smooth.
graph LR
A[PWM Output - digital square wave, variable duty cycle] --> B[RC Low-Pass Filter]
B --> C[Approximated Analog Voltage - proportional to duty cycle]
Frequently Asked Questions
Why does my ADC reading look noisy even with a stable analog input? Common causes include insufficient sampling time relative to source impedance, digital switching noise coupling into the analog reference, or a missing/inadequate decoupling capacitor on Vref. Averaging multiple samples also helps mask residual noise.
Should I always use the highest-resolution ADC available? Not necessarily — a 16-bit ADC is wasted if the sensor’s own noise floor only supports 10 effective bits, and higher resolution ADCs generally cost more, draw more power, and convert more slowly.
What’s the difference between ADC resolution and ADC accuracy? Resolution is how finely the input range is divided (a count of possible output codes). Accuracy is how close a given digital output actually is to the true analog input value — a 16-bit ADC with poor calibration can be less accurate than a well-calibrated 12-bit ADC.
Do I need an anti-aliasing filter for slow-changing signals like temperature? Usually a simple RC low-pass filter is enough since temperature changes slowly, but any signal with fast transients or electrical noise coupled onto it benefits from proper anti-aliasing before the ADC.
Summary
Handling analog signals in an embedded system is a full pipeline problem, not a single ADC register write. It starts with understanding the sensor’s physical behavior, conditioning and filtering the signal appropriately, respecting the Nyquist sampling theorem to avoid aliasing, configuring the ADC’s sampling time correctly for the source impedance, and finally calibrating and validating the digital result in firmware. Getting any single stage wrong — even something as simple as an under-sized sampling time — can silently corrupt every measurement downstream, which is why I treat analog signal handling as one of the most detail-sensitive parts of embedded design.