---

Wednesday, January 24, 2024

Lies, Damn Lies and Analog Inputs (comparing ADCs on ESP32, Pico and Arduino)

After some inconsistent and unreliable results reading an analog input from an ESP32 board, I decided to get all scientific and do some experimenting with an ESP32, a Raspberry Pi Pico and an Arduino Uno R3.

Method

My test setup was a bench power supply providing the reference voltage to be measured by the test board. The output of the bench PSU had a dummy load of a 470Ω resistor and a 100nF capacitor in parallel (the latter largely for superstitions reasons) as the voltage output looks extremely stable on a DMM voltmeter.

This output from the bench power supply was then applied directly to an analog input and GND of the board being tested.

I was particularly interested in three things:

* finding any dead-zones at each end of the analog input voltage range

* measuring the reproducibility of the readings

* linearity through the range

Another time, I'd like to look at the input impedance of the ADC (analog to digital convertor) and the effects of how rapidly you sample. But I'll leave that for another day.

To measure the reproducibility of the readings, each time the test voltage was changed, 100 readings would be taken, and the mean and standard deviation of that set recorded. That way, when it came to plotting the readings from the boards, I could add some error bars.

For the ESP32 and Pico, I used MicroPython and for the Arduino Uno, I used Arduino C. The Arduino readings were scaled up to 16bit unsigned values (max value 65536) to be consistent with the MicroPython version. In all cases, the default ADC settings were used.

ESP32

For this experiment, I used an ESP32 Lite (sometimes also called LOLIN32 Lite). These are a ubiquitous low-cost ESP32 board, with built-in WiFi and Bluetooth.

Analog input maximum voltage 1.0V



Here's the plot

The red error bars show +- 3 standard deviations (SDs) from 100 samples. Nearly all of the sample values would fall within 3 SDs and 60% would fall within 1 SD.

There is a sizeable dead zone until the voltage rises to about 0.05V and a lot of noise around the readings, evidenced by the large error bars. But it retains pretty good linearity once you get past that up to the 1V upper limit.

Raspberry Pi Pico

The Raspberry Pi Pico uses Raspberry Pi's RP2040 chip. It'a another popular, good value board.

It's maximum analog input voltage is the full 3.3V supply range.


Here's the plot for the Pico - 


This is considerably better than the ESP32, with smaller 3 x SD error bars, a small dead zone at the low voltage end and some slight tail-off in linearity at the 3.3V end.

Arduino Uno R3

Despite its age, the Arduino Uno R3 (not the fancy new one) is still my go-to board for any experimentation or early stage project work that doesn't need a specific microcontroller. I'll admit, it's partly out of familiarity and inertia on my part.



And here are the results.


And there we have it. Very little deviation between readings and great linearity across the whole range, right up to 5V. The Uno with it's ancient Atmega328 is streets ahead of the other two boards.

Conclusion

On looking at the documentation in MicroPython and learning that the analog readings for a Pico and ESP32 come at a massive 16 bit precision (a number between 0 and 65536) it's easy think that their analog inputs are much better than the paltry 10 bits of an Arduino (0 to 1023 reading range). But this is to confuse precision with accuracy. It's why pure megapixels is not the best way to judge a camera. So much depends on the lens.

So, if you are trying to get decent accuracy and reproducibility from your analog readings, then you probably want to take a set of readings and average them -- or use an Arduino Uno R3!

Test Programs

ESP32

from machine import ADC, Pin
from time import sleep
from math import sqrt

analog = ADC(12)

p = 0.05
n = 100

while True:
    readings = []
    for i in range(0, n):
        readings.append(analog.read_u16())
        sleep(p)
    total = 0
    for i in range(0, n):
        total += readings[i]
    mean = total / n
    dist_tot = 0
    for i in range(0, n):
        dist = readings[i] - mean
        dist_tot += dist * dist
    
    print(mean, sqrt(dist_tot / n))
    
    input('Press enter to read again')

Pico

from machine import ADC, Pin
from time import sleep
from math import sqrt

analog = ADC(28)

p = 0.05
n = 100


while True:
    readings = []
    for i in range(0, n):
        readings.append(analog.read_u16())
        sleep(p)
    total = 0
    for i in range(0, n):
        total += readings[i]
    mean = total / n
    dist_tot = 0
    for i in range(0, n):
        dist = readings[i] - mean
        dist_tot += dist * dist
    
    print(mean, sqrt(dist_tot / n))
    
    input('Press enter to read again')

Arduino

int p = 50;

const int n = 100;


unsigned int readings[n];


void setup() {

  Serial.begin(9600);

}


void loop() {

  if (Serial.available()) {

    Serial.read();

    Serial.println("measuring");

    for (int i = 0; i < n; i++) {

      readings[i] = analogRead(A0) * 64; // 16 bit not 10

      delay(p);

    }

    float total = 0.0;

    for (int i = 0; i < n; i++) {

      total += float(readings[i]);

    }

    float mean = total / n;

    float dist_total = 0.0;

    for (int i = 0; i < n; i++) {

      float dist = float(readings[i] - mean);

      dist_total += (dist * dist);

    }

    float sd = sqrt(dist_total / n);

    Serial.print(mean); Serial.print(' '); Serial.println(sd);

  }

}



13 comments:

Anonymous said...

Hey, there seems to be a typo, the dead zone is before 0.05V not 0.5V.

Cheers!

Jon Durrant said...

Interesting results. To correct one thing though the Pico ADC is 12bit not 16bit. I tend to work in C so not so familiar with Python but looks like it may be extrapolating 12 bits to 16 bits which would impact your test result a bit.

a said...

What was the relative timing of the three different systems capturing the signal?

Is there an alternative metric that would show obtained accuracy as a function of identical total acquisition time when using multiple sample averaging?

Simon Monk said...

Thank you. I've edited the post accordingly

a said...

The visual comparison of "error" between the three independent charts is significantly affected by the 5X voltage scaling range. While it may be possible in many instances to "map" the input to the full range of the ADC, which makes this visual comparison valid, in others it may not be viable and the absolute voltage would be measured.

So it might also be worth generating a chart with all three results (color coded) for the 0-1V range to make absolute error easier to compare at the low end of the range.

Ali said...

Would be interesting to study the effect of the board settings as the default settings might not be the optimum for the other boards.

Conor Stewart said...

There is a lack of data points on some of the graphs, a little more time would have been well spent taking consistently spaced data points rather than seemingly random ones with large gaps between data points.

As for the 16 bit ADC when using micro python, your reasoning for it is not really correct. The ADCs are not 16 bit at all, but micro python right shifts them to be 16 bit so that every micropython board returns a 16 bit number regardless of actual ADC resolution so that it is more consistent and portable. To use your megapixel analogy, the micropython results are effectively upscaled.

It would have been good to see a comparison of more boards too with more advanced MCUs, like STM32 (in particular the higher end ones like the G4 series), teensy 4, etc.

Anonymous said...

I'd love to see some data on more mainline. Microcontrollers. ESP32 and The Pico are both made by pretty niche companies. I'd love to see how comparable NXP,renesas, ti, and st chips stack up

Anonymous said...

If you normalize the 16bit readings to match the 10bit range of the UNO does the variance on the ESP32 and RP2040 reduce to a similar range to the Arduino?

Norm said...

Very interesting!! Which component or components accounts for the differences? If you were using these boards to take readings from an attached sensor, say for example a pH sensor, does it follow that your sensor readings would also reflect this variability, or would it depend on the specific mode of operation for the sensor?

Ian McKellar said...

Hmm, I'm about to use an ADS1115 i2c ADC for a project. Actually lots of them or perhaps one plus some analog muxes - I need to sample a lot of sensors. I might have to run your same experiment with the ADS1115 - at least to find out if there are dead zones or nonlinearity that I can try to bias my signals away from and filter out any systemic errors.

Anonymous said...

How can one scale this to controllers with multi channel ADCs?

Simon Monk said...

Just to clarify a few points that people have raised in the comments.
I use unsigned 16 bit values because that's what the MicroPython ADC class uses.
The actual hardware ADC resolution for the boards is 12-bit for ESP32 and Pico, 10-bit for an Uno R3.
As for timing - the test code (in the blog) leaves 50ms between each individual sample.
'a' makes a good point. If you assume a constant noise level, then the plot for the Arduino's 5V range is bound to be a lot better than that of the ESP32s 1V range. But that is the range of the ESP32 so the comparison is still useful. I assume the default out-of-the-box performance, without adding op-amps or even tweaking ADC settings.