Wednesday, March3, 2021
Privacy Policy

ESP32 Low Noise Voltage Measurements

Solving the noise and linearity issues with the ESP32

Resource id #18

The ESP32 has many great features. One of them is multiple ADC inputs. The problem is that these inputs are not very good. They suffer from noise and non linearity. The software I will present overcomes these problems in what I think is a unique way. The first thing to deal with is the noise. Once we get quiet readings, we can linearize them for our application. The application I have developed reads battery voltage on a boat so the range of 12 to 14 volts is the most important. More on that later but first we need quiet readings.

There are three kinds of noise that disturb the reading. One is electrical noise so putting a .1 micro farad capacitor near the input pin is useful. The next is random spike noise. The problem with them is that you can't really get rid of this noise by averaging. These spikes are large and do not at all resemble what we think of as random noise. We need to reject them before averaging what is left. The technique I used is very simple to implement. I sum 8 readings and then subtract the largest and smallest and divide what is left by 6. That is a common technique for dealing with data that has outliers. I then take 100 of these combined readings and average them. Thus a single point represents 800 measurements of which 600 are used and 200 are ignored. This produces readings that have noise in the .01 volt range.

The choice of 8 readings is important. You would like to have a lot of readings but you don't want more than one spike in the readings. With 8, most samples do not have a spike and tossing out the high and low does not bias the result. More than 8 and you start to see more than one spike in a sample. So averaging 8 gives 6 samples that can then be averaged to get rid of the more traditional gaussian noise.

Once I have a quiet reading, I use a third order curve fit to linearize it. I picked readings in the region I cared about and then used Excel to curve fit the data. I extracted the coefficients using an obscure Excel function called LINEST. From there it was a simple matter of solving the equation.

The final thing is that each individual ESP32 has a different gain. I have three units that I rotate in as I make changes to the code. Each has it's own gain. I have a defined constant UNIT which tells the linearization function which gain correction to use.

The final thing that could be done is to have a stable voltage reference which is measured to take out any temperature effects. I did not do that. What you would do is have a separate ADC input on the same ADC channel to read the reference voltage and scale all the readings by that value. I have not found that necessary (yet).

Actually, there is one more thing that could be a problem but isn't. I used 1% resistors so worst case could have a 2% error between my two ADC channels. Luckily they are much closer and that is not a problem. I could also calibrate each channel independently but again, it is not a problem.

The code is below.

The ADC is very fast and 100 readings takes very little time given I am reading a DC voltage.
const int samples = 100; //there will be 8 times this many readings. Recommend at least 2

This is the main function called for reading voltages
double readVoltage(int bat1Pin){
  // Reading voltage
  double bat1Value = 0;
  int x = 0;
  for(x = 0; x < samples; x++){
    bat1Value += analogReadAverage(bat1Pin);
  bat1Value /= samples;
  double linValue = lin(bat1Value);
  Serial.println("Battery " + String(bat1Pin) + " is " + String(linValue) + " Volts");
  return linValue;
Third order curve fit linearization. Note that there is an offset term so this function will not read 0 volts. Not a problem in this application.

double lin(float batvalue){
  double gain = 1;
  switch (UNIT){
    case 1:
      gain = 999999;
    case 2:
      gain = .992;
    case 3:
      gain = .9874;
      gain = 1;
  double returnValue = gain * (-9.42459E-11 * pow(batvalue, 3) +2.79259E-07 * pow(batvalue, 2) + batvalue*0.004223291+0.478405313);
  return returnValue;
//Read 8 values and toss the high and low. Average the rest.

double analogReadAverage(int bat1Pin){
  int i;
  int maxValue = 0;
  int minValue = 5000;
  double sumValue = 0;
  double result;
  int reading;
  for(i = 0; i<8; i++){
    reading = analogRead(bat1Pin);
    sumValue += (double)reading; 
    if(reading > maxValue) maxValue = reading;
    if(reading < minValue) minValue = reading;
  result = (sumValue - (double)maxValue - (double)minValue) / 6;
  return result;

Please read website Cookie, Privacy, and Disclamers by clicking HERE.