The standard ADC conversion formula is V_in = (ADC_val * V_ref) / (2^n - 1). For a 10-bit Arduino Uno (ATmega328P) with a 5.0V reference, an ADC reading of 512 yields exactly 2.502V. While the math is simple, the hardware realities of sample-and-hold circuits, reference voltage droop, and quantization fencepost errors frequently break the theoretical model. This guide derives the formula, tracks units through solved problems, and exposes the embedded assumptions that separate textbook math from bench-verified firmware.

The Core ADC Conversion Formula and Symbol Definitions

An Analog-to-Digital Converter (ADC) maps a continuous analog voltage to a discrete digital integer. The fundamental mapping equation is:

V_in = (ADC_val × V_ref) / (2^n - 1)

Every symbol in this equation represents a specific physical or architectural constraint of your microcontroller. Below is the definitive spec sheet for these variables.

SymbolDescriptionUnitAVR (ATmega328P)ESP32-WROOM-32
V_inThe actual analog voltage present at the ADC pinVolts (V)0 to 5.0V0 to 3.3V (with attenuation)
ADC_valThe raw digital integer output by the ADC registerCounts (unitless)0 to 10230 to 4095
V_refThe reference voltage defining the full-scale rangeVolts (V)5.0V (default) or 1.1V2.5V (internal) or 3.3V
nThe resolution of the ADC in bitsBits1012
2^n - 1The maximum possible digital output value (Full Scale)Counts10234095

The term V_ref / (2^n - 1) is known as the quantization step size (or LSB voltage). It represents the smallest voltage change the ADC can theoretically resolve. For a 10-bit ADC on a 5V reference, one count equals 4.887 mV.

Rearranged Forms: Solving for Any Variable

In firmware development, you rarely just solve for V_in. You often need to calculate the expected register value for a comparator threshold, or deduce the actual V_ref if your VCC is sagging under load. Here are the algebraic rearrangements:

  • Solving for Digital Output (ADC_val):
    ADC_val = (V_in × (2^n - 1)) / V_ref
    Use case: Setting an analog watchdog threshold in firmware.
  • Solving for Reference Voltage (V_ref):
    V_ref = (ADC_val × V_ref_nominal) / ADC_val_measured (when measuring a known bandgap reference)
    Use case: Calibrating out VCC sag on an Arduino powered by a draining 9V battery.
  • Solving for Resolution (n):
    n = log2((ADC_val × V_ref / V_in) + 1)
    Use case: Determining the minimum bit-depth required for a specific sensor resolution.

Worked Examples with Unit Tracking

Abstract math hides errors. By tracking units through the calculation, we ensure the dimensional analysis holds up, preventing the classic mistake of multiplying volts by volts.

Problem 1: ESP32-S3 Battery Monitoring

Scenario: You are using an ESP32-S3 (12-bit ADC, n = 12) with an internal reference of V_ref = 2.5V. You are measuring a LiFePO4 battery through a voltage divider. The ADC register reads 2048 counts. What is the voltage at the ADC pin?

  1. Identify knowns: ADC_val = 2048 counts, V_ref = 2.5 V, 2^n - 1 = 4095 counts.
  2. Substitute into formula:
    V_in = (2048 counts × 2.5 V) / 4095 counts
  3. Multiply numerator (track units):
    V_in = 5120 (counts × V) / 4095 counts
  4. Divide and cancel units:
    V_in = 1.2503 V

Sanity Check: 2048 is exactly half of 4096. Half of 2.5V is 1.25V. The math holds.

Problem 2: Arduino Nano Threshold Trigger

Scenario: An ATmega328P (10-bit, n = 10) running at V_ref = 5.0V needs to trigger a relay when a thermistor voltage drops below 1.85V. What integer value should you use in your if() statement?

  1. Identify knowns: V_in = 1.85 V, V_ref = 5.0 V, 2^n - 1 = 1023 counts.
  2. Use rearranged formula:
    ADC_val = (V_in × (2^n - 1)) / V_ref
  3. Substitute and track units:
    ADC_val = (1.85 V × 1023 counts) / 5.0 V
  4. Calculate:
    ADC_val = 1892.55 (V × counts) / 5.0 V = 378.51 counts
  5. Round to nearest integer:
    ADC_val = 378.

Firmware implementation: if (analogRead(A0) < 378) { triggerRelay(); }

Assumptions, Unit Traps, and Realistic Magnitudes

When the Formula Applies (and When It Doesn't)
This formula assumes an ideal ADC. It assumes zero Integral Non-Linearity (INL), zero Differential Non-Linearity (DNL), and a perfectly stable V_ref. In reality, the ESP32's internal ADC is notoriously non-linear above 2.5V and suffers from severe DNL errors near the rails. For precision work on the ESP32, you must use the esp_adc_cal library to apply two-point calibration, or use an external I2C ADC like the ADS1115. The formula applies strictly to the raw, uncalibrated register mapping.

Unit Mistakes That Break the Math

  • The Fencepost Error (2^n vs 2^n - 1): Many beginners divide by 1024 instead of 1023 for a 10-bit ADC. The ADC has 1024 states (0 through 1023), but only 1023 intervals between them. Dividing by 1024 introduces a systematic scaling error of ~0.1%. (Note: Microchip application notes sometimes suggest dividing by 1024 when averaging noisy signals to account for the 0.5 LSB transition offset, but theoretically, the full-scale mapping uses 1023).
  • Mixing Millivolts and Volts: If your V_ref is defined as 5000 (mV) but you treat the output as Volts, your calculated V_in will be 1000x too large. Always normalize to base SI units (Volts) before calculating, then multiply by 1000 if your UI requires millivolts.

What a Realistic Answer Magnitude Looks Like

If your calculated V_in exceeds V_ref, your math or your hardware is wrong. A 10-bit ADC cannot physically read a voltage higher than its reference without clipping at 1023. If you calculate V_in = 5.2V on a 5V reference system, you have either ignored a voltage divider ratio in your math, or your VCC rail is experiencing a massive transient spike.

For deeper architectural context on how quantization steps are defined at the silicon level, refer to Texas Instruments' SLAA013 Application Note on Understanding Data Converters. For specific ESP32 hardware attenuation behaviors, consult the Espressif ESP-IDF ADC Oneshot Documentation.

Frequently Asked Questions

How does the ADC conversion formula change for signed vs unsigned outputs?

The standard formula assumes an unsigned, unipolar ADC (0V to V_ref). If you are using a bipolar, signed ADC (like the ADS1115 configured for ±2.048V), the digital output uses two's complement. The formula shifts to:
V_in = (ADC_val_signed / 2^(n-1)) × V_ref.
For a 16-bit signed ADC, the denominator becomes 32768, not 65535, because the MSB is reserved for the sign bit.

Why does my ESP32 ADC formula give the wrong voltage above 2.5V?

The ESP32's internal 12-bit ADC saturates and becomes highly non-linear as the input approaches the 3.3V rail. The raw formula V_in = (ADC_val × 3.3) / 4095 will yield significant errors above 2.5V. To fix this, you must either configure the internal 11dB attenuation pad (which shifts the measurable range up to ~3.9V but compresses the lower end) and use Espressif's eFuse calibration data, or bypass the internal ADC entirely and use an external SPI/I2C ADC.

What is the difference between using 1023 and 1024 in the 10-bit ADC conversion formula?

Theoretically, the maximum digital output is 1023, meaning there are 1023 steps between 0V and V_ref. Therefore, 1023 is the mathematically correct denominator for mapping full scale. However, the actual transition from code 0 to code 1 happens at 0.5 LSB (Least Significant Bit). Because of this 0.5 LSB offset and the presence of noise, some silicon vendors (including Microchip for the ATmega328P) suggest dividing by 1024 (or 2^n) in practical firmware when oversampling and averaging, as it simplifies bitwise shifting (>> 10) and statistically centers the quantization error. For strict, single-sample theoretical mapping, stick to 1023.

How do I account for a voltage divider in the ADC conversion formula?

The ADC formula only calculates the voltage at the microcontroller pin. If you are measuring a 12V battery through a voltage divider (e.g., R1 = 10kΩ, R2 = 3.3kΩ), you must multiply the calculated V_in by the divider's inverse ratio. The expanded formula becomes:
V_source = V_in × ((R1 + R2) / R2).
Always calculate the pin voltage first, verify it is within the safe V_ref limits, and then apply the multiplier.