Using a Thermistor with Arduino and Unexpected ESP32 ADC Non-linearity

5 pieces NTC Thermistor

I recently accidentally bought 5 pieces of thermistor, so I tried it out on both of my Arduino and ESP32 modules, and I have surprise findings on both Arduino and enchanted unexpected ESP32’s ADC linearity issue.

NTC Thermistor

What I means that I “accidentally” bought 5 pieces of thermistor for SGD5.40 (approximately a little less than USD4.00) is that I thought those were DS18B20 temperature sensor when I placed order and I didn’t pay too much attention about it because I know DB18B20 well from my previous projects and there are similar form factor for DS18B20 available in the market. It is not until I received the goods and decided to put it in use that I realised that it has only two pins instead of three pins as DS18B20 is. I quickly look at the product description, silly me, it was all written there that these are NTC thermistor. NTC thermistor is really easy to use, so I decided to apply it to my project.

NTC Thermistor Product Description
NTC Thermistor Product Description

Negative Temperature Coefficient (NTC) thermistor is the most common type of thermistors, and it is very easy to use. The one that I purchased has a resistance of 10k at temperature of 25 degree Celsius and the resistance go up when temperature go down. So in order to measure the temperature, what you need is a voltage divider circuits consists of a known resistor (R1) connected in serial with the thermistor (Rt), apply a voltage reference (Vs) to one end of the resistor, and connect the other end of the thermistor to ground, the voltage across the thermistor (Vo) is read to determine the temperature based on the voltage reading. As the resistance of the thermistor changes with temperature, the voltage Vo will varies accordingly, this can be easily measured with an Analog In on an Arduino to get the reading.

Vout/Rt = Vs/(R1 + Rt)
Vout = Vs * Rt/(R1 + Rt)

or

Rt = R1 * Vout/(Vs - Vout)

These formulas help us to measure the resistance Rt of the thermistor indirectly by measuring the Vout, as most of the microcontroller such as Arduino provided input for getting the reading of an analog voltage via an ADC (Analog-Digital Converter). But what we really want is the temperature reading, not just a resistance! According to the Steinhart-Hart equation, the resistance of a semiconductor at different temperatures follow an equation that consists of a series of coefficients. These coefficients can be obtained through some calibration processes. Luckily, most of the NTC thermistor manufacturers publish a coefficient called B parameter (which shown on the product description that I purchased as NTC 10k+/-1% 3950, the 3950 is the B parameter) which allows to simply the formula as:

    1/T = 1/To + 1/B * ln(Rt/Ro), so
    T = 1 / (1/To + 1/B * ln(Rt/Ro))

Where:
T is the temperature to be measured in Kelvin;
To is the reference temperature in Kelvin for 25 degree Celsius;
Ro is the thermistor resistance at To;
B is Beta or B parameter, provided by manufacturer in their specification.

Using Thermistor with Arduino

As we have all the formulas that we need for programming the Arduino, we can therefore read the voltage Vout via A0 pin on Arduino.

    // NTC B3950 Thermistor
    // the formula for temp in kelvin is
    //                 1
    // T = ----------------------------
    //     1/To + (1/beta) * ln(Rt/Ro)
    //
    // https://en.wikipedia.org/wiki/Thermistor


    int ThermistorPin;
    double adcMax, Vs;

    double R1 = 10000.0;   // voltage divider resistor value
    double Beta = 3950.0;  // Beta value
    double To = 298.15;    // Temperature in Kelvin for 25 degree Celsius
    double Ro = 10000.0;   // Resistance of Thermistor at 25 degree Celsius


    void setup() {
      Serial.begin(9600);

      ThermistorPin = A0;
      adcMax = 1023.0;   // ADC resolution 10-bit (0-1023)
      Vs = 5.0;          // supply voltage

    }

    void loop() {
      double Vout, Rt = 0;
      double T, Tc, Tf = 0;

      Vout = analogRead(ThermistorPin) * Vs/adcMax;
      Rt = R1 * Vout / (Vs - Vout);
      T = 1/(1/To + log(Rt/Ro)/Beta);  // Temperature in Kelvin
      Tc = T - 273.15;                 // Celsius
      Tf = Tc * 9 / 5 + 32;            // Fahrenheit
      Serial.println(Tc);

      delay(2000);
    }

The Arduino program is quite straightforward once you understand the formulas. The Arduino ADC provided a 10-bit resolution, what this means is that giving a voltage input of 5v, the reading will be 1023, and a reading of 512 would be 512 * 5v/1023=2.5v and so on so for. The temperature value derived from the B parameter formula is in Kelvin, but it is easy to convert it to Celsius by subtracting it with 273.15, I add the formula for converting Celsius to Fahrenheit, but only print out the Celsius value to the Serial Console.

The output of the reading is quite stable as you can see from the screen capture of the Arduino Serial Plotter.

Arduino ADC reading from thermistor
Arduino ADC Reading From Thermistor

The Unexpected ESP32 ADC Non-Linearity

I’m using more often to use ESP32 than Arduino for my project nowadays as it has a compact form factor, faster CPU with a lot of more memory, but more importantly for me is the built-in wireless connectivity with WiFi and Bluetooth. Particular for this project, I would need more than one ADC inputs eventually, a typical ESP32 would have 6 user-accessible ADC (the actual number of user-accessible ADC varies by modules, but 6 is quite common). The ESP32 ADC can also be configured with 9 to 12-bit resolution, for input voltage up to 3.3v. I was expecting 3.3v would be less noisy than 5v as it is further regulated from the supply voltage of 5v from USB, and 3.3/4095 at 12-bit resolution would provide better precision than Arduino as well. So ESP32 seems to be perfect for my project.

For the programming, I only need to change the settings in the setup(), the rest is the same as Arduino sketch:

    ThermistorPin = 34;
    adcMax = 4095.0; // ADC resolution 12-bit (0-4095)
    Vs = 3.3;        // supply voltage

Pin 34 refered to ESP32 GPIO 34 according the the ESP32 Dev Board Pin Map which can be configured as ADC6.

But when I run the program on ESP32, the reading of the temperature is almost 10% off compare to the reading from Arduino, this translate into about 2.3 – 2.4 degree Celsius differences in my case, where does it come from?

After digging through various websites, I realised that the delta was caused by the non-linearity of ESP32 ADC. There are lengthy discussions on Expressif github and forum about the issue. Basically, ESP32’s ADC is non-linear as shown in this chart below:

ESP32 ADC Linearity
ESP32 ADC Linearity

Although Espressif (ESP32 chip manufacturer) claims that it has fixed the issue by implementing a calibration algorithm during the manufacturing process for chips that are shipped on and after 1st week of 2018, the ESP32 WROOM-32 modules that I bought in Feb 2019 from Shenzhen clearly were produced before that. The fix described on the website seems to be a little troublesome and risky to do and will take me much longer time so I decided to look for alternative to fix the issue.

Fix ESP32 Non-linarity with a Lookup Table

Bury deep in an Expressif Forum discussion, Helmut Weber describes a way to generate a lookup table by feed a Digtial-to-Analog (DAC) output generated by ESP32 into the ADC input to compare the known value of DAT output with ADC reading, and use the lookup table to correct the raw ADC reading. He generates a whole table consists of 4096 values covering the entire 12-bit ADC range, and uses it to get the corrected value. The lookup table values varies from device to device due to the internal reference voltage of each ESP32 varies, so Weber’s pre-generated lookup table that he used doesn’t work for my device. So I connect the DAC output pin 25 (DAC_CHANNEL_1) to ADC 34 (this is the pin that I’d want to use for my thermistor reading), and run Weber’s program to generate the lookup table for my ESP32.

The ESP32 offers two DAC outputs (on GPIO25 and GPIO26) with 8-bit resolution, but the ADC is 12-bit resolution, so there will be some loss due to interpolation between 8-bit to 12-bit, but it is not significant compare with the noise level of ESP32 (more on that later).

Copy and paste the generated lookup table into my program as the declaration of the lookup table, and change the loop() so that instead of using the the data read from ADC directly, it uses the value get from analogRead(34) as the reference index to get the corrected value from the lookup table.

    float ADC_LUT[4096] = { 0,
    17.0000,18.4000,19.8000,21.2000,22.8000,24.0000,25.8000,27.0000,28.8000,30.0000,31.6000,32.8000,34.0000,35.2000,36.6000,37.8000,
    . . . . . .
    3990.6001,3991.0000,3991.8000,3992.6001,3993.0000,3993.8000,3994.6001,3995.0000,3995.8000,3996.6001,3997.0000,3997.8000,3998.6001,3999.0000,3999.80
    };

    void loop() {
      double Vout, Rt = 0;
      double T, Tc, Tf = 0;

      double adc = 0;
      if (esp32) {
        adc = analogRead(ThermistorPin);
        adc = ADC_LUT[int(adc)];
      }
      else {
        adc = analogRead(ThermistorPin);
      }
      Vout = adc * Vs/adcMax;
      Rt = R1 * Vout / (Vs - Vout);

      T = 1/(1/To + log(Rt/Ro)/Beta);    // Temperature in Kelvin
      Tc = T - 273.15;                   // Celsius
      Tf = Tc * 9 / 5 + 32;              // Fahrenheit
      if (Tc > 0) Serial.println(Tc);

      delay(2000);
    }

Please noted that instead of paste the long lookup table here, I only show partial of the code here, you can see the complete code from my Github repository.

The noise of ESP32 ADC

Non-linearity is not the only problem of ESP32 ADCs, the reading from ESP32 is also much noisier than the output from Arduino.

ESP32 ADC reading from thermistor
ESP32 ADC Reading From Thermistor

The noise is probably due to the higher CPU clock and small form factor of ESP32 module. The noise can be smooth out a little bit with some filtering algorithm or by adding 0.1uF capacitor to both the ADC input and the 3.3v pin close to the thermistor.

Conclusion

Thermistor is easy to use and quite stable with an Arduino, I’m impressed by the Arduino ADC performance despite it only offers 10-bit resolution.

I like ESP32 in general as it offers many good features for IoT projects, such as wireless connectivity and deep sleep, but I’m quite disappointed on the ADC linearity issue and this was something quite unexpected when I start this project. Personally I feel it is more important to offer one good ADC than offer a bunch of mediocre ADCs. The lookup table solution offers a decent solution for correcting the linearity issue, especially if what you are measuring fall within a narrow spectrum, this should be okay for most of hobby projects, but it is still quite troublesome as the calibration need to be carry out on each ESP32 you used. For production projects, I would consider to use a separate ADC integrated circuit rather than using ESP32’s ADC.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.