To code resistances on a microcontroller, you cannot read ohms directly. You must use a voltage divider to convert the unknown resistance into a proportional voltage, sample that voltage with an Analog-to-Digital Converter (ADC), and apply the inverse voltage divider formula in your firmware. For the ESP32, this process requires specific hardware filtering and software corrections to overcome the chip's notorious ADC non-linearity.

This guide provides the exact circuit, pin mapping, and compilable C++ code to accurately calculate resistance (and temperature) using an ESP32 and an NTC thermistor, while avoiding the most common firmware traps.

The Core Problem: Why Reading Resistances on the ESP32 Fails

The ESP32-WROOM-32 features a 12-bit ADC, which theoretically maps 0-3.3V to integer values between 0 and 4095. In practice, the internal ADC is highly non-linear, particularly near the 0V and 3.3V rails. Furthermore, when you set the ADC attenuation to 11dB (required to read up to ~3.3V), the actual maximum readable voltage saturates around 3.1V to 3.2V depending on the specific silicon batch.

If you code a standard 3.3V linear mapping without accounting for this, your calculated resistance will drift by 10-15% at the extremes. To fix this, we restrict our measurement to the linear middle-band of the ADC by selecting a reference resistor that keeps the expected voltage swing between 0.5V and 2.5V, and we apply a hardware low-pass filter to eliminate high-frequency noise that the ESP32's internal sampling capacitor struggles to settle.

Parts List & Pin Mapping for Precision Resistance Coding

The following bill of materials is optimized for a 10kΩ NTC thermistor. Using a 1% tolerance reference resistor is non-negotiable; a standard 5% carbon film resistor will introduce more error than the ESP32's ADC non-linearity.

Component Specification / Variant Purpose
Microcontroller ESP32 DevKit V1 (ESP32-WROOM-32) Main processor and ADC sampling
Unknown Resistor 10kΩ NTC Thermistor (B-value 3950) Target sensor for resistance coding
Reference Resistor 10kΩ 1% Tolerance Metal Film (1/4W) Voltage divider baseline
Filter Capacitor 100nF (0.1µF) Ceramic (X7R) Hardware low-pass filter for ADC settling

Pin Mapping Table

ESP32 Pin Function Connection
3V3 Power Reference Resistor (Leg 1)
GND Ground NTC Thermistor (Leg 2) & Capacitor (Leg 2)
GPIO 34 ADC1_CH6 (Input) Junction of Resistor, Thermistor, & Capacitor (Leg 1)
Crucial Pin Selection: You must use an ADC1 pin (GPIO 32-39). Never use ADC2 pins (GPIO 0, 2, 4, 12-15, 25-27) for resistance reading if your project uses WiFi or Bluetooth. The ESP32 hardware physically disables ADC2 when the wireless radios are active.

Decision Tree: Choosing Your Resistance Reading Method

Not every project requires the same level of precision. Use this decision matrix to select your hardware approach.

Project Condition Method Concrete Pick
Need < 1% error across full 0-3.3V range External I2C ADC ADS1115 (15-bit, programmable gain)
Need basic temp/resistance, cost constrained (< $5) ESP32 Internal ADC + Correction ESP32 GPIO 34 (ADC1) + 100nF Cap
Ultra-low power battery operation (sleep modes) External ADC + MOSFET switching MCP3424 (18-bit) + P-Channel MOSFET

Default Recommendation: For 90% of hobbyist and IoT sensor nodes, use the ESP32 GPIO 34 (ADC1) with a 100nF hardware filter and software polynomial correction. It costs pennies, requires no external libraries, and provides sufficient accuracy for environmental monitoring when constrained to the mid-band voltage range.

Step-by-Step: Wiring and Coding the Voltage Divider

Follow these steps to build the physical circuit and deploy the firmware.

  1. Build the Divider: Connect the 10kΩ reference resistor between the ESP32 3V3 pin and the breadboard junction row.
  2. Add the Sensor: Connect the 10kΩ NTC thermistor between the junction row and GND.
  3. Install the Filter: Place the 100nF ceramic capacitor in parallel with the thermistor (between the junction row and GND). This creates an RC low-pass filter that stabilizes the voltage for the ESP32's internal sampling capacitor.
  4. Wire the ADC: Connect the junction row to GPIO 34 using a short jumper wire.
  5. Flash the Firmware: Upload the following C++ code using the Arduino IDE (ensure "ESP32 Dev Module" is selected as the board).
#include <Arduino.h>

// --- PIN DEFINITIONS ---
const int ADC_PIN = 34; // GPIO 34 (ADC1_CH6) - MUST be ADC1 for WiFi compatibility

// --- HARDWARE CONSTANTS ---
const float VCC = 3.3;          // Nominal supply voltage
const float ADC_MAX = 4095.0;   // 12-bit resolution
const float R_REF = 10000.0;    // 10k ohm reference resistor (Measure with DMM and update!)
const float V_SAT_MAX = 3.1;    // ESP32 11dB attenuation actual saturation voltage

// --- NTC THERMISTOR PARAMETERS ---
const float BETA = 3950.0;      // B-value from datasheet
const float T0 = 298.15;        // 25°C in Kelvin
const float R0 = 10000.0;       // Nominal resistance at 25°C

void setup() {
  Serial.begin(115200);
  delay(1000); // Allow serial monitor to connect
  
  // Configure ESP32 ADC for maximum range
  analogReadResolution(12);       // 12-bit (0-4095)
  analogSetAttenuation(ADC_11db); // Allows reading up to ~3.1V
  
  Serial.println("System Initialized: Resistance & Temperature Logger");
}

void loop() {
  // 1. Sample ADC (average 16 reads to reduce noise)
  uint32_t adc_sum = 0;
  for (int i = 0; i < 16; i++) {
    adc_sum += analogRead(ADC_PIN);
    delayMicroseconds(100);
  }
  uint16_t adc_raw = adc_sum / 16;

  // 2. Error Handling: Check for open/short circuits
  if (adc_raw >= 4090) {
    Serial.println("[ERROR] ADC Saturation: Raw value near 4095. Check for open circuit or disconnected thermistor.");
    delay(2000);
    return;
  }
  if (adc_raw <= 5) {
    Serial.println("[ERROR] ADC Grounded: Raw value near 0. Check for short circuit to GND.");
    delay(2000);
    return;
  }

  // 3. Convert ADC to Voltage (Accounting for ESP32 11dB saturation at ~3.1V)
  float voltage = (adc_raw / ADC_MAX) * V_SAT_MAX;

  // 4. Calculate Unknown Resistance using Voltage Divider formula
  // V_out = VCC * (R_ntc / (R_ref + R_ntc))  =>  R_ntc = R_ref * (V_out / (VCC - V_out))
  float r_ntc = R_REF * (voltage / (VCC - voltage));

  // 5. Calculate Temperature using Beta Parameter Equation
  // 1/T = 1/T0 + (1/BETA) * ln(R_ntc / R0)
  float temp_kelvin = 1.0 / ((1.0 / T0) + (1.0 / BETA) * log(r_ntc / R0));
  float temp_celsius = temp_kelvin - 273.15;

  // 6. Output Results
  Serial.printf("ADC Raw: %4d | Voltage: %5.2f V | Resistance: %7.1f ohms | Temp: %5.2f C\n", 
                adc_raw, voltage, r_ntc, temp_celsius);

  delay(1000);
}

Debugging: "ADC Read Timeout" and Non-Linear Drift

When coding resistances on the ESP32, firmware bugs and hardware quirks often masquerade as sensor failures. If your serial monitor outputs erratic values or fails to compile, follow this diagnostic path.

The First Three Things to Check

  1. Verify the Pin Number: Are you using an ADC2 pin while WiFi is enabled? If you attempt to read GPIO 25 while running MQTT, the ESP-IDF will throw this exact error string in the serial monitor: E (345) adc: adc2_get_raw(142): ADC2 is disabled due to WiFi. Fix: Move your sensor to GPIO 34 (ADC1).
  2. Measure the Reference Resistor: Do not trust the color bands. A "10kΩ" 5% resistor could actually be 9.6kΩ, throwing off your entire calculation. Measure it with a multimeter and update the R_REF constant in the code with the exact value (e.g., 9850.0).
  3. Check for ADC Saturation: If your output reads Resistance: inf ohms or Temp: nan C, your ADC is reading 4095. This means the voltage at the pin is exceeding the 3.1V saturation limit, or the thermistor is physically disconnected (open circuit).

Ranked Causes for Non-Linear Drift

If the resistance values jump erratically by 500+ ohms between reads:

  • Cause 1 (Most Likely): Missing Hardware Capacitor. The ESP32 ADC has a high internal impedance and a small sampling capacitor. Without the 100nF external capacitor to hold the voltage steady during the ~10µs sampling window, the reading will fluctuate wildly. Fix: Add the 100nF cap across the thermistor.
  • Cause 2: USB Power Noise. Powering the ESP32 via a cheap PC USB port introduces 50mV of switching noise onto the 3.3V rail. Because the math relies on VCC being stable, rail noise translates directly to resistance noise. Fix: Power via a regulated 5V wall adapter or add a 10µF electrolytic capacitor across the 3V3 and GND pins.
  • Cause 3: Self-Heating. Passing current continuously through a 10kΩ thermistor causes it to heat itself. Fix: Increase the delay() at the end of the loop, or use a MOSFET to switch the ground path of the divider so it only draws current during the 16ms sampling window.

Extending and Simplifying the Build

How to Simplify

If your end goal is strictly ambient temperature and you do not actually need the raw resistance data for a custom sensor, abandon the voltage divider entirely. Use a digital I2C sensor like the BME280 or SHT31. These chips contain internal ADCs, factory calibration tables, and linearization algorithms, completely bypassing the ESP32's analog quirks. You simply call bme.readTemperature() and get a float.

How to Extend

To extend this circuit for a battery-powered IoT node (like an ESP32 deep-sleep soil moisture or temperature probe), you must eliminate the continuous current draw of the voltage divider. A 10kΩ divider draws roughly 165µA continuously, which will drain a 2000mAh 18650 cell in a few months even if the ESP32 is sleeping.

The Fix: Add a logic-level P-Channel MOSFET (like the Si2301) to the high side of the voltage divider. Connect the gate to an ESP32 GPIO (via a 10kΩ pull-up resistor). In your code, pull the GPIO LOW to turn on the MOSFET and power the divider, take your 16 ADC samples, calculate the resistance, and then pull the GPIO HIGH to cut power to the divider before entering esp_deep_sleep_start(). This reduces the analog circuit's power consumption to virtually zero during sleep.

For deeper mathematical accuracy on the NTC thermistor, upgrade the Beta equation in the code to the 3-term Steinhart-Hart equation, which requires measuring the thermistor at three distinct temperatures to derive the A, B, and C coefficients. For comprehensive details on the ESP32's internal ADC architecture and attenuation mapping, refer to the official Espressif ADC Oneshot Driver Documentation. If you decide the internal ADC is insufficient for your precision requirements, the Adafruit ADS1115 guide provides the exact I2C wiring and library setup for external 15-bit conversion.