ESP32 analog input linearity

ESP32(s) are fast and have a lot more analog input pins than the esp8266. However, the ESPs do not provide an external voltage reference pin, so you need to manually compensate for the non-linearity of the analog input readings. But first, let’s take a look at the test result.

Note that I added a 35nF ceramic capacitor parallel to the analog input, I am using GPIO 36 as the analog input. A linear voltage regulator HT7333 is used to provide a stable 3.308v power source for the ESP32.

Here is the test code modified from Arduino tutorial, I averaged 1000 test points. The result is quite stable.

#include <Arduino.h>
#define ADC_PIN 36

const int numReadings = 1000;

float readings[numReadings];      // the readings from the analog input
int readIndex = 0;              // the index of the current reading
float total = 0;                  // the running total
float average = 0;                // the average

void setup() {
  Serial.begin(115200);
  for (int thisReading = 0; thisReading < numReadings; thisReading++) {
    readings[thisReading] = 0;
  }
}

void loop() {
  total = total - readings[readIndex];
  readings[readIndex] = analogRead(ADC_PIN);
  total = total + readings[readIndex];
  readIndex = readIndex + 1;

  if (readIndex >= numReadings) {
    readIndex = 0;
  }
  average = total / numReadings;
  Serial.println(average * 3.3 / 4096 ,3);
  delay(1);        // delay in between reads for stability
}

And here is the result:

ESP32 linearity test

A linear region lies between 0.2v to2.5v, then I tried to add a compensation by shifting the readings by +0.153 and rotating the axis by a factor of 0.99. So the final readings should be: compensated result = (raw readings + 1.53) * 0.99.

compensated result

In the region of 0.2v to 2.5v, we could archive a result of +/- 7mv. By using a voltage divider, we could limit the voltage below 2.5v. For example, when measuring a li-ion, we use a 470k ohm and 680k ohm resistor to convert the maximum battery voltage 4.2v to 2.483v.

    \[{{battery voltage * 680k} \over {470k+680k}}=({{analogreadings * 3.3} \over 4096} + 1.53) *0.99\]

And here is the wiring diagram:

ESP32 with voltage divider

Note that I removed the smoothing capacitor because the readings are quite stable.



After all the massive work and statistics, I only calibrated one of my many ESPs. And this compensation may not be suitable for all other boards!! Because they vary from each other. According to Espressif, each ESP32 has its own ADC reference voltage VRef (1100mV nominal), but it may range from 1000mV to 1200mV. This value is measured and burned into eFuse BLOCK0 during factory calibration. We can take this number as a reference and use it to compensate for our measurements.

By using espefuse.py we can find out the Vref stored in eFuse $IDF_PATH/components/esptool_py/esptool/espefuse.py --port /dev/ttyUSB0 adc_info. Here I checked two of my ESP32 boards, the first one has a 1100mV VRef and the second board is 1114mV.

C:\Users\Developer\AppData\Local\Programs\Python\Python310\Lib\site-packages>python espefuse.py --port COM4 adc_info
Connecting...
Detecting chip type... Unsupported detection protocol, switching and trying again...
Connecting...
Detecting chip type... ESP32
espefuse.py v3.3

=== Run "adc_info" command ===
ADC VRef calibration: 1100mV

C:\Users\Developer\AppData\Local\Programs\Python\Python310\Lib\site-packages>python espefuse.py --port COM4 adc_info
Connecting...................
Detecting chip type... Unsupported detection protocol, switching and trying again...
Connecting...
Detecting chip type... ESP32
espefuse.py v3.3

=== Run "adc_info" command ===
ADC VRef calibration: 1114mV

Before continuing to our code, we need to first include the library esp_adc_cal.h. And use the following structure variable and function.

struct esp_adc_cal_characteristics_t

Is a structure to store the ADC’s characteristics, such as ADC unit and channel (GPIO), ADC’s attenuation, resolution, etc.

While the function esp_adc_cal_characterize()used to initialize the above structure.

  esp_adc_cal_characteristics_t adc_chars; //declare a structure named adc_chars
  esp_adc_cal_value_t val_type = esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 1100, &adc_chars); //Generate compensation curve of an ADC at a particular attenuation by VRef

ADC_UNIT_1: The ADC unit. Unit 1 includes 8 channels: GPIO32-GPIO39
ADC_ATTEN_DB_11: ADC Attenuation. 11dB, suitable rage 150mV ~ 2450mV
ADC_WIDTH_BIT_12: Bit capture width for ADC unit. 12 bit resolution
1100: Default VRef if eFuse value is not available
&adc_chars: The memory address of adc_chars

After we initialized the characteristic curve, we can take a look at the API Espressif has provided: esp_adc_cal_raw_to_voltage(). This function takes the compensation curve and raw ADC readings and returns voltage in mV. So let’s take out our ESP32 and check how accurate this function provided.

uint32_t esp_adc_cal_raw_to_voltage(uint32_t adc_reading, const esp_adc_cal_characteristics_t *chars)

Actually, the compensation was done in a similar manner on top:  [y = coeff_a * x + coeff_b]. We can even check the coeff_a and coeff_b, they are stored as a public member of esp_adc_cal_characteristics_t. Read the coeff_a by Serial.println(adc_chars.coeff_a) and Serial.println(adc_chars.coeff_b)

For my ESP32, the coeff_a is 53470, coeff_b is 142. Let’s varify the how ESP32 do the compensation. Take a single measurement as an example, the analogRead() is: 2586, esp_adc_cal_raw_to_voltage() is: 2252.

    \[compensatedVoltage = {{53470} \over {65536}} * 2586 +142\]

    \[compensatedVoltage = 2251.885\]

The result 2251.885 in integer is 2252 which matches esp_adc_cal_raw_to_voltage().

In the following, I used two ESP32 to check the accuracy of the esp_adc_cal_raw_to_voltage() function. One with a VRef of 1114mV and one with 1100mV

Actual voltage vs esp_adc_cal_raw_to_voltage

Here is the code I used for testing. The compensated result looks accurate and could generate a reading with +/-15mV , which already exceeded the precision of my power supply.

To conclude, the esp_adc_cal_raw_to_voltage() does a great job of compensating the ADC, as long as the voltage goes between 150 ~ 2450mV which it can be easily done by adding a voltage divider.

#include <Arduino.h>
#include "driver/adc.h"
#include "esp_adc_cal.h"
#define ADC_PIN 35

const int numReadings = 100; 
uint32_t cal_sumReadings = 0; 
int vref = 1100;

void setup(){
  Serial.begin(115200);
}

void loop(){
  cal_sumReadings =0;
  for (int i = 0; i < numReadings; i++){
    cal_sumReadings += analogRead(ADC_PIN);
    delay(1);
  }
  
  esp_adc_cal_characteristics_t adc_chars; //declare a structure named adc_chars
  esp_adc_cal_value_t val_type = esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, ADC_WIDTH_BIT_12, 1100, &adc_chars); //Define compensation of an ADC at a particular attenuation.
  if (val_type == ESP_ADC_CAL_VAL_EFUSE_VREF) {
    Serial.printf("eFuse Vref:%u mV", adc_chars.vref);
    Serial.println("");
    vref = adc_chars.vref;
    uint32_t averageReadingInt = cal_sumReadings/numReadings;
    Serial.print ("esp_adc_cal_raw_to_voltage: ");
    Serial.println(esp_adc_cal_raw_to_voltage(averageReadingInt, &adc_chars));
    Serial.print ("coeff_a: ");
    Serial.println(adc_chars.coeff_a);
    Serial.print ("coeff_b: ");
    Serial.println(adc_chars.coeff_b);
    Serial.print ("averageReadingInt: ");
    Serial.println(averageReadingInt);
  }
  Serial.println("~~~~~~~~~~~~~~~~~~~~~~~~");
  delay (1000);
}

Summary

In this post, 2 compensation methods are discussed. In the beginning, we measured the ADC output value with respect to the actual voltage, then we come up with a formula:

    \[{{battery voltage * 680k} \over {470k+680k}}=({{analogreadings * 3.3} \over 4096} + 1.53) *0.99\]

By applying this formula, we get a quite close result for a specific ESP32. This method is easy to implement to code and does not require additional libraries. However, the formula does not fit other ESP32, it could be troublesome to measure and record the ADC characteristics for each ESP32.

In the later part, we introduced the esp_adc_cal.h library, and discussed how the library does the compensation.

Finally, we used a handy function esp_adc_cal_raw_to_voltage() to compare the result with the actual voltage. It turns out to be quite accurate and could archive a +/-15mV result within the recommended voltage region. Such accuracy should be enough for a rough measurement without the need for external ADCs such as measuring battery voltage. If you want a more precise and accurate analog measurement, I still recommend going for an external ADC such as ADS1115.

Additional notes

The val_type variable above stores the method for generating the characteristics curve.

ESP_ADC_CAL_VAL_EFUSE_VREF: generated by reference voltage stored in eFuse
ESP_ADC_CAL_VAL_EFUSE_TP: generated by Two-point method
ESP_ADC_CAL_VAL_DEFAULT_VREF: generated by default VRef if eFuse value is not available
ESP_ADC_CAL_VAL_EFUSE_TP_FIT: generated by Two-point method and fitting curve coefficient

But all my ESP32 are using ESP_ADC_CAL_VAL_EFUSE_VREF, so I only discussed this type above. You can check Espressif’s documentation if you need more details.

By the way, with respect to the function, only Two-point method is supported, the parameter default_vref is unused. And only ADC_WIDTH_BIT_13 is supported.

Leave a Reply

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