Difficulty Rating: 2/5 (Beginner-Intermediate)
Time to Complete: 20 minutes
Target Board Variant: Arduino Uno R3 (ATmega328P) or Nano v3 (10-bit ADC, 5V logic)

An NTC (Negative Temperature Coefficient) thermistor is a variable resistor whose resistance drops predictably as temperature rises. While digital sensors like the DS18B20 are popular, analog thermistors remain the undisputed king for fast thermal response times, low BOM cost (under $0.20 in bulk), and high-resolution bench measurements. However, because they are non-linear and rely on the microcontroller's Analog-to-Digital Converter (ADC), they are notorious for producing noisy data, NaN serial outputs, and self-heating errors if wired incorrectly.

This guide provides the exact voltage divider topology, the Steinhart-Hart C++ implementation with built-in fault detection, and a debugging framework for when your serial monitor spits out garbage data.

The Verdict: Which Temperature Sensor Should You Actually Use?

Before soldering, confirm that an analog thermistor is actually the right tool for your specific thermal envelope. Use this decision matrix to lock in your component choice.

Criteria NTC 10k Thermistor DS18B20 (Digital) PT100 (RTD)
Temp Range -40°C to +125°C -55°C to +125°C -200°C to +850°C
Response Time Fast (1-5 seconds in liquid) Slow (10+ seconds, enclosed in steel) Medium (depends on probe sheath)
Wiring Complexity Medium (Requires voltage divider) Low (1-Wire, needs 4.7k pull-up) High (Requires MAX31865 amp)
Long Cable Runs Poor (Wire resistance skews ADC) Excellent (Digital signal) Good (3-wire/4-wire compensation)
The Concrete Pick: For 90% of indoor HVAC, 3D printer hotend monitoring, and bench power supply thermal protection projects, buy the Vishay NTCLE100E3103 (10kΩ, B=3977K) or a generic 10k B3950 glass bead thermistor. If your application requires submerging the sensor in water or running cables longer than 2 meters, abandon the thermistor and use a waterproof DS18B20.

Parts List & Pin Mapping for the NTC 10k Build

To get accurate readings, your passive components matter just as much as the microcontroller. A 5% tolerance pull-up resistor will introduce a ±2°C baseline error before you even write a line of code.

Bill of Materials

  • Microcontroller: Arduino Uno R3 (ATmega328P) or compatible 5V clone.
  • Thermistor: 10kΩ NTC, B-value 3950K or 3977K (e.g., Vishay NTCLE100E3103 or generic glass-encapsulated bead).
  • Pull-up Resistor: 10kΩ, 1% tolerance metal film (Crucial: do not use a standard 5% carbon film resistor).
  • Decoupling Capacitor: 100nF (0.1µF) ceramic capacitor (X7R dielectric).
  • Wiring: 22 AWG solid core hook-up wire.

Pin Mapping Table

Arduino Uno R3 Pin Component Node Function
5V Pull-up Resistor (Leg 1) Excitation voltage for the divider
GND Thermistor (Leg 1) Circuit common / reference ground
A0 Junction (Pull-up Leg 2 + Thermistor Leg 2) Analog voltage input to ADC

Wiring the Voltage Divider & ADC Considerations

A thermistor cannot be read directly by a microcontroller; the ADC measures voltage, not resistance. We must convert the changing resistance into a changing voltage using a voltage divider.

Step-by-Step Wiring Procedure

  1. Build the Divider: Connect one leg of the 10kΩ pull-up resistor to the Arduino 5V pin. Connect one leg of the NTC thermistor to the Arduino GND pin.
  2. Create the Junction: Twist or solder the remaining leg of the pull-up resistor and the remaining leg of the thermistor together. This node is your analog signal.
  3. Connect the ADC: Run a jumper wire from this junction to the Arduino A0 pin. (Note: Do not use digital pin D0, which is the hardware UART RX line and will cause serial communication conflicts).
  4. Add the Decoupling Cap (The Secret to Stable Reads): The ATmega328P ADC is highly susceptible to high-frequency EMI, which manifests as ±10 LSB jitter on the serial monitor. Place the 100nF ceramic capacitor directly between the A0 junction node and GND. This forms a low-pass RC filter that smooths out transient noise without delaying the thermal response time.
Callout Tip: The Self-Heating Trap
Current flowing through the thermistor generates heat ($P = I^2R$). If your pull-up resistor is too small (e.g., 1kΩ), the current will heat the glass bead, causing the sensor to read 1-2°C higher than ambient. A 10kΩ pull-up limits the maximum current to 250µA at 5V, keeping self-heating well below 0.1°C in still air.

Complete Arduino Code with Steinhart-Hart & Error Handling

The relationship between an NTC thermistor's resistance and temperature is highly non-linear. The Steinhart-Hart equation models this curve using three coefficients (A, B, and C). The coefficients below are calibrated for a standard 10kΩ B3950/B3977 NTC thermistor.

This code targets the Arduino Uno R3 (ATmega328P) 10-bit ADC and includes boundary checks to prevent math domain errors that crash the serial output.

#include <math.h>

// --- Pin Definitions ---
#define PIN_THERMISTOR A0

// --- Hardware Constants ---
#define R_PULLUP 10000.0  // 10k ohm pull-up resistor value
#define ADC_MAX 1023.0    // 10-bit ADC resolution for ATmega328P

// --- Steinhart-Hart Coefficients (Generic 10k B3950 NTC) ---
#define A_COEFF 1.009249522e-03
#define B_COEFF 2.378405444e-04
#define C_COEFF 2.019202697e-07

void setup() {
  Serial.begin(115200);
  analogReference(DEFAULT); // Uses 5V VCC as reference on Uno R3
  
  // Allow ADC capacitor to charge before first read
  analogRead(PIN_THERMISTOR);
  delay(10);
}

void loop() {
  // Read the raw ADC value
  int adcRaw = analogRead(PIN_THERMISTOR);

  // --- Error Handling: Prevent divide-by-zero and log(0) domain errors ---
  if (adcRaw <= 0) {
    Serial.println("Error: ADC reads 0. Thermistor likely shorted or wired to GND.");
    delay(1000);
    return;
  }
  if (adcRaw >= ADC_MAX) {
    Serial.println("Error: ADC reads 1023. Thermistor likely disconnected (open circuit).");
    delay(1000);
    return;
  }

  // Calculate Thermistor Resistance using voltage divider algebra
  // R_therm = R_pullup * (ADC / (ADC_MAX - ADC))
  float resistance = R_PULLUP * ((float)adcRaw / (ADC_MAX - (float)adcRaw));

  // Apply Steinhart-Hart Equation: 1/T = A + B*ln(R) + C*(ln(R))^3
  float logR = log(resistance);
  float tempK = 1.0 / (A_COEFF + (B_COEFF * logR) + (C_COEFF * logR * logR * logR));
  float tempC = tempK - 273.15;
  float tempF = (tempC * 9.0 / 5.0) + 32.0;

  // Output formatted data
  Serial.print("ADC: ");
  Serial.print(adcRaw);
  Serial.print(" | R: ");
  Serial.print(resistance, 1);
  Serial.print(" ohms | Temp: ");
  Serial.print(tempC, 2);
  Serial.print(" C (");
  Serial.print(tempF, 2);
  Serial.println(" F)");

  delay(500);
}

Debugging: "NaN" Readings, ADC Drift, and Self-Heating

When working with analog sensors, the serial monitor will inevitably throw garbage data at some point. If your output looks like Temp: nan C or inf, do not rewrite the math library. The issue is almost always physical.

The First Three Things to Check When It Fails

  1. Verify Baseline Resistance with a DMM: Disconnect the thermistor from the circuit. Set your multimeter to the 20kΩ range and measure across the thermistor legs. At room temperature (25°C), it must read between 9.5kΩ and 10.5kΩ. If it reads 0Ω, the glass bead is cracked and shorted. If it reads OL (open), the internal wire bond is broken.
  2. Measure the Actual 5V Rail: The Arduino analogRead() function assumes the reference voltage is exactly 5.00V. If you are powering the Uno via a cheap USB hub that droops to 4.6V, your ADC math will skew heavily. Measure the 5V pin with a DMM. If it is below 4.8V, power the board via the barrel jack with a 9V wall adapter to engage the onboard linear regulator.
  3. Check for Digital/Analog Pin Confusion: Ensure your code defines A0 and your wire is in the analog header. If you accidentally wire the junction to digital pin 0 (which maps to D0 or RX) and use analogRead(0), the ATmega328P will attempt to read the UART line, resulting in chaotic, floating ADC values.

Ranked Causes for Exact Error Strings

Exact Serial Output Root Cause The Fix
Temp: nan C Math domain error. The code attempted to calculate log(0) because the ADC read exactly 0. Check for a dead short between the A0 junction and GND. Verify the pull-up resistor is actually connected to 5V.
Temp: inf C Divide-by-zero in the resistance formula. The ADC read 1023, making the denominator (1023 - 1023) = 0. The thermistor is disconnected or wired backwards. Check for broken jumper wires or a cold solder joint at the junction.
Temp: -40.12 C (at room temp) Wrong Steinhart-Hart coefficients. The B-value of your physical thermistor does not match the A/B/C constants in the code. Check the datasheet for your specific thermistor. If using a B3950 instead of B3977, update the coefficients using a Steinhart-Hart calculator.

Extending or Simplifying the Build

Depending on your final application, you may need to optimize this circuit for either raw speed or extreme precision.

How to Simplify (For Fast, Low-Memory Applications)

If you are running this on an ATtiny85 or need to sample temperature at 1kHz for a PID control loop, the log() and floating-point math in the Steinhart-Hart equation will bottleneck your CPU. The fix: Replace the math with a Look-Up Table (LUT). Pre-calculate the ADC-to-Temperature mapping for every 10th step (0-1023) in Excel, store it in an array in PROGMEM, and use linear interpolation between the nearest two indices. This drops the calculation time from ~150µs to under 5µs.

How to Extend (For High-Precision Lab Use)

If you need ±0.1°C accuracy, the standard analogRead() is insufficient due to internal ATmega328P ADC non-linearity and VCC ripple. Implement these three upgrades:

  1. Use the Internal 1.1V Reference: Change analogReference(DEFAULT) to analogReference(INTERNAL). This switches the ADC reference to the stable internal 1.1V bandgap. Warning: You must change your pull-up resistor to 1.1V (using a voltage divider or an LM4040 shunt regulator) or the ADC will saturate at 1023 immediately.
  2. Implement Software Oversampling: Take 16 rapid readings, sum them, and divide by 4. This yields a 12-bit effective resolution (0-4095), smoothing out Gaussian noise without adding hardware.
  3. Calibrate the Pull-Up: Measure your "10k" 1% resistor with a 4.5-digit bench multimeter. It might actually be 9,982Ω. Hardcode that exact value into the #define R_PULLUP macro to eliminate the largest source of systemic error in the voltage divider.