An ADC value is the discrete digital integer output by an Analog-to-Digital Converter that represents a specific sampled analog voltage within a defined reference range. This integer changes how a microcontroller interprets continuous real-world sensor signals—like a thermistor's resistance drop or a potentiometer's wiper position—into actionable digital logic for your code. Beginners most commonly confuse the raw ADC value (the unitless integer) with the actual analog voltage (measured in volts) or the reference voltage (VREF) used to scale it.
Think of an ADC as a physical ruler. The reference voltage is the total length of the ruler, and the ADC resolution (10-bit or 12-bit) dictates how many tick marks are printed on it. The ADC value is simply which tick mark the measured voltage lands on. If you don't know the ruler's total length (VREF) and how many tick marks it has (resolution), the raw tick mark number (ADC value) is meaningless.
Actual_Voltage = (ADC_Value * V_REF) / Max_ADC_Value
The Math Behind the ADC Value (Worked Examples)
Different microcontrollers use different hardware architectures, which directly dictates the maximum ADC value your code will return. The two most common maker boards illustrate this perfectly:
- Arduino Uno (ATmega328P): Features a 10-bit ADC. This means it divides the reference voltage into 1,024 discrete steps, yielding an ADC value range of 0 to 1023. The default VREF is tied to the 5V USB/VIN rail.
- ESP32 (ESP32-WROOM-32): Features a 12-bit ADC. This divides the reference voltage into 4,096 steps, yielding an ADC value range of 0 to 4095. The default VREF is internally tied to the 3.3V rail.
Worked Numeric Example: Battery Monitoring on an ESP32
Suppose you are building a solar charge controller and need to monitor a 12V lead-acid battery using an ESP32. Because the ESP32's GPIO pins will be destroyed by anything over 3.6V, you use a voltage divider (a 100kΩ and 22kΩ resistor) to scale the battery voltage down to a safe level.
At peak charge, your battery sits at 12.60V. Your voltage divider scales this down to exactly 2.50V at the ESP32's GPIO34 pin.
- Calculate the expected ADC value:
ADC_Value = (2.50V / 3.3V) * 4095 = 3102 - Read it in code: Your
analogRead(34)function returns3102. - Convert it back to voltage in code:
Voltage = (3102 * 3.3) / 4095 = 2.499V - Scale back to battery voltage: Multiply by your specific divider ratio (in this case, 5.04).
2.499V * 5.04 = 12.59V(accounting for minor resistor tolerance drift).
According to the official Arduino analogRead() documentation, this scaling math is universal across AVR, SAMD, and RP2040 architectures, provided you substitute the correct bit-depth and VREF variables.
Where You Meet This in Practice
You will rarely read a raw voltage directly. Instead, you use circuits that translate physical phenomena into a voltage between 0V and VREF. Here is a reference table of common sensor circuits and the ADC values you should expect to see on a standard 5V, 10-bit Arduino Uno.
| Sensor / Component | Typical Circuit | Expected ADC Value (10-bit) | Physical Meaning |
|---|---|---|---|
| NTC Thermistor (10kΩ) | Voltage divider with 10kΩ fixed resistor | ~850 (Cold) to ~250 (Hot) | Temperature drops, resistance drops, ADC value drops. |
| LDR (Photoresistor) | Voltage divider with 10kΩ fixed resistor | ~50 (Bright) to ~950 (Dark) | Light increases, resistance drops, ADC value drops. |
| Potentiometer (10kΩ) | Wiper to ADC, outer legs to 5V and GND | 0 to 1023 | Direct linear mapping of user dial position. |
| ACS712 (20A Module) | Direct to ADC (Output biased at 2.5V) | ~512 (0A) ± 100 per Amp | AC/DC current measurement centered around VCC/2. |
| MQ-2 Gas Sensor | Direct to ADC (Analog out pin) | ~100 (Clean air) to 900+ (Gas) | Gas concentration increases, output voltage rises. |
analogReference(INTERNAL) to use the 1.1V internal reference, which maps that 1V signal to nearly the full 0-1023 range.
Hardware Quirks: ESP32 Non-Linearity and Floating Pins
Reading an ADC value in a textbook is perfect; reading it on a workbench introduces hardware realities that will break your code if you aren't prepared.
The ESP32 ADC Non-Linearity Problem
The ESP32's internal ADC is notoriously non-linear at the top end of its range. Even with the attenuation set to 11dB (which theoretically allows reads up to 3.3V), the ADC value will often max out at 4095 when the actual input voltage is only 3.1V. If you are trying to measure a 3.2V lithium-ion cell directly on an ESP32 pin, the microcontroller will simply report 4095, blinding you to overvoltage conditions.
The Fix: Never trust the ESP32's internal ADC for precision voltage measurement above 3.0V. As detailed in this comprehensive ESP32 ADC guide by Random Nerd Tutorials, you should either use a voltage divider to scale your maximum expected voltage down to 2.8V, or bypass the internal hardware entirely and use an external I2C ADC like the 16-bit ADS1115 module.
Arduino Floating Pin Noise
If you upload a sketch that prints the ADC value of an unconnected analog pin on an Arduino Uno, you will see the serial monitor spit out random numbers (e.g., 124, 388, 12, 509). This is not a broken chip. The ADC input has an incredibly high impedance (roughly 100 megohms). It acts like an antenna, picking up the 50/60Hz electromagnetic hum from your room's AC wiring and the switching noise from the microcontroller's own digital clock.
The Fix: Never leave an analog pin floating. If a sensor is disconnected, the pin must be pulled to GND via a high-value resistor (e.g., 100kΩ), or you must handle the disconnected state in software by checking for erratic jitter.
Frequently Asked Questions
Why is my ESP32 ADC value maxing out at 4095 prematurely?
This happens because the ESP32's ADC attenuation defaults to 0dB on boot, which limits the measurable input voltage to roughly 1.0V. Any voltage above 1.0V will immediately return an ADC value of 4095. To fix this in the Arduino IDE, you must explicitly set the attenuation in your setup() block using analogSetPinAttenuation(34, ADC_11db); (for GPIO34). This expands the readable range up to ~3.1V. Note that the ESP32-C3 and ESP32-S3 variants have significantly improved ADC hardware and do not suffer from the severe non-linearity of the original ESP32-WROOM silicon.
How do I map an ADC value to a physical unit like temperature?
Do not use the Arduino map() function for non-linear sensors like NTC thermistors or LDRs. The map() function assumes a strictly linear relationship (like a potentiometer). For an NTC thermistor, the resistance changes exponentially with temperature. Instead, convert the ADC value to voltage, then to resistance using the voltage divider formula, and finally apply the Steinhart-Hart equation: 1/T = A + B*ln(R) + C*(ln(R))^3. You will need the specific A, B, and C coefficients from your thermistor's datasheet to get an accurate Celsius reading.
What causes a fluctuating ADC value and how do I filter it?
Jitter is caused by electrical noise on the power rail, long unshielded wires acting as antennas, or the internal sample-and-hold capacitor failing to charge fully due to high source impedance. Hardware fixes include placing a 0.1µF ceramic capacitor directly between the ADC pin and GND, and keeping analog traces away from digital switching lines.
On the software side, avoid simple delay-based averaging, which blocks your main loop. Instead, use an Exponential Moving Average (EMA) filter. It uses minimal memory and reacts quickly to real changes while smoothing out high-frequency noise:
float alpha = 0.1; // Smoothing factor (0.0 to 1.0). Lower = smoother but slower.
float filteredAdc = 0;
void setup() {
Serial.begin(115200);
// Initialize with a raw read to prevent startup lag
filteredAdc = analogRead(34);
}
void loop() {
int rawAdc = analogRead(34);
// EMA Formula
filteredAdc = (alpha * rawAdc) + ((1.0 - alpha) * filteredAdc);
Serial.print('Raw: '); Serial.print(rawAdc);
Serial.print(' | Filtered: '); Serial.println(filteredAdc);
delay(20);
}






