Introduction: Do You Know About ESP32 ADC Adjustment?
Today, I'm going to talk about a more technical issue, but one I think everyone who works with ESP32 should know: the issue of ADC (analog-to-digital converter) read adjustment. I find this important because when doing a "measurement," especially with an instrument that has an analog output, you need to be absolutely certain that the reading is being performed correctly.
In the video today, therefore, we will carry out measurements using the "analog-digital converter" of the ESP32, observe the conversion discrepancies, and apply an ADC adjustment / calibration method.
Step 1: What Is an AD Converter?
An AD converter is a circuit capable of translating an analog (continuous) quantity into digital (discrete) values. What does that mean? It means that while digital values can only assume discrete values formed by the combination of zeros and ones, an analog quantity can assume any value within a range. For example, if we measured the voltage of an ideal AA cell, we could find any value between 0V and 1.5V, since this is analog quantity. The output state of an ideal lamp must assume only two states (off or on), which is a discrete magnitude. Since microcontrollers work using this discrete logic, we need a circuit capable of translating an analog quantity into digital (or discrete).
Step 2: Resources Used
• One Lolin32 Lite card v1.0.0
• A Tektronix TDS1001C oscilloscope for capturing
• One USB cable for the ESP32
• A Hantek DSO4102C oscilloscope as a signal generator
Step 3: ESP32 ADC
According to Espressif data, the ESP32 chips may present a +/- 6% difference from one chip to another in the measured results.
In addition, the conversion does NOT have a linear answer for every available range for reading. Espressif provides a method for calibration and suggests that users implement other methods if they deem it necessary to achieve the desired accuracy.
We will perform a data acquisition, and from this, we will show the ADC responses and an example of applying a mathematical process to read the adjustment.
There are several (simpler or more complex) ways to accomplish these fixes. It is up to you to evaluate the most appropriate for your project.
The one shown here will have an illustrative purpose and try to address interesting points that can be observed during adjustments.
Step 4: Circuit Used
I used an oscilloscope with a signal generator that goes up to 25 MHz, the Hantek DSO4102C. We generated a wave that was read by the ESP A / D and the oscilloscope. The data collected was recorded in csv and in a spreadsheet, which I will leave at the end of the article for download.
Step 5: Sign Used
We chose a low-frequency trapezoidal signal which allows access to the ramps that run through the entire conversion range. This allows for a large number of samples on these ramps.
Step 6: Data Obtained by the Oscilloscope
The image of the capture was performed by the oscilloscope. The data was stored in a csv file. Note the slight curvature on the rising and falling ramps of the signal.
Step 7: Data Obtained by the Oscilloscope (csv File in Excel)
We have the samplings here.
Step 8: Data Obtained by the ADC
By changing the transfer rate of the serial, we can view the data captured by the ADC. Observe the deformation of the trapezoidal signal.
Data observed on Arduino IDE serial plotter
Step 9: Data Obtained by ADC - Excel
Using a higher rate and the serial terminal, we can capture the values and apply them in Excel for our comparisons.
Step 10: Comparison of Climb Ramps
We compare the two climbing ramps of the two catches.
Note the curvature that occurs on both ramps.
Note also that for the same ramp, we have many more samples of the ESP32 than from the oscilloscope.
Step 11: Equating the Number of Samples
Because the ESP32 provided a larger number of samples than the oscilloscope, we need to equate these values, as they will serve as an index to compare the two curves.
For this, we will make a direct comparison.
We have 305 samples for the oscilloscope ramp and 2365 samples for the ADC ramp.
Since the ramps are of the same range, we can say that we have approximately 7.75 samples of the ADC for each oscilloscope.
Multiplying the index of each oscilloscope sample has the same curve, but with indices equivalent to the ADC and the redistributed data.
To fill in the missing data for the new positions, we will apply a curve that statistically fits the known data.
Step 12: Filling the Gaps - Trend Line
Selecting the known data (blue dots), by clicking and then clicking with the right button, we select: "Add Trend line ..."
In the window that appears, we select the Polynomial type (order 2 will be enough).
We also checked the options "View Equation in the chart" and "Display R-squared value in the chart".
We click "Close".
Step 13: Filling in the Gaps - Grade 2 Polynomial Curve
Excel gives us two new pieces of information; the second-order equation that best fits the data, and the R-squared equation that quantifies this adequacy.
Just remember that the closer to 1, the more appropriate the equation.
Let's not delve into the math involved, let's just use it as a tool.
Step 14: Filling the Gaps - Evaluating the Function
Let's fill in the sampling gaps with the data generated by the equation. And then, compare them point by point.
y = -9E-08x2 + 0,0014x + 0,1505
R² = 0,9999
Oscilloscope voltage = -9E-08 * index2 + 0,0014 * index + 0,1505
Step 15: Converting the Oscilloscope Voltage to an Equivalent Value to Compare With the ADC
Let's take advantage of this to also transform the value of the oscilloscope voltage into an equivalent ADC value.
As the highest value obtained in the ADP of the ESP32 was 4095, which is equivalent to the reading of 2.958V for the same index, we can say that:
Each volt in the measurements of the oscilloscope equals approximately 1384.4 units of the AD. Therefore, we can multiply all measurements of the oscilloscope by this value.
Step 16: Comparing the Two Ramps Obtained
Visualizing the differences obtained in the two readings.
Step 17: Behavior of the ADC Reading Difference (ERROR)
The curve below shows how the difference in the ADC reading behaves as a function of the measurement. This collection of data will allow us to find a correction function.
To find this curve, we simply plot the difference found in each measure as a function of each possible AD position (0 to 4095).
Step 18: ADC Reading Difference Behavior - Finding a Correction Function
We can determine in Excel a correction function by adding a Trend Line, now of a higher degree, until it fits sufficiently with our data.
Step 19: Using Other Software
Other interesting software for determining curves is PolySolve, which can be used directly at the link: https://arachnoid.com/polysolve/ or downloaded as a Java application.
It allows the application of higher-degree polynomial regressions and delivery of the formatted function, as well as other functionalities.
To use it, simply enter the data in the first text box. The data must follow the order X, Y separated by a comma, or tab. Exercise caution in using the dot correctly as a decimal point.
A chart will appear in the next box if the entered data is correctly formatted.
Here's how our ADC error curve went.
This window will present the result of the regression, including function adequacy data, which in turn can have its output formatted in several ways: as a C / C ++ function, a list of coefficients, a function written in Java, etc.
Note: Pay attention to decimal separators
Step 20: Constants and Setup ()
I point out here the GPIO used for analog capture. I initialize the serial port, as well as the pin determined for analog capture.
const int pin_leitura = 36; //GPIO usado para captura analógica
void setup() { Serial.begin(1000000); //Iniciciando a porta serial somente para debug pinMode(pin_leitura, INPUT); //Pino utilizado para captura analógica }
Step 21: Loop () and the Correction Function
We make the capture of the adjusted voltage, and we print the values with or without the correct corrections.
void loop() {
int valor_analogico = analogRead(pin_leitura); //realiza a captura da tensão ajustada //Serial.print(valor_analogico + f(valor_analogico)); //imprime os valores para debug (COM CORREÇÃO) Serial.print(valor_analogico); //imprimime os valores para debug (SEM CORREÇÃO) Serial.print(","); Serial.print(4095);//cria uma linha para marcar o valor máximo de 4095 Serial.print(","); Serial.println(0); //cria uma linha para marcar o valor mínimo de 0 }
Notice in line 12 that we have the option of printing the data with the addition of the difference function f (analog_value).
Step 22: Using the PolySolve Correction Function
Here, we use the PolySolve function inside the Arduino IDE.
/*
Mode: normal Polynomial degree 6, 2365 x,y data pairs Correlation coefficient (r^2) = 9,907187626418e-01 Standard error = 1,353761109831e+01 Output form: C/C++ function: Copyright © 2012, P. Lutus -- http://www.arachnoid.com. All Rights Reserved. */ double f(double x) { return 2.202196968876e+02 + 3.561383996027e-01 * x + 1.276218788985e-04 * pow(x, 2) + -3.470360275448e-07 * pow(x, 3) + 2.082790802069e-10 * pow(x, 4) + -5.306931174991e-14 * pow(x, 5) + 4.787659214703e-18 * pow(x, 6); }
Note the comma-by-dot change as the decimal separator.
Step 23: Capture With Correction - Plotter Serial
Step 24: Computational Cost
To perform polynomial calculations, it is necessary for the processor to handle this task. This can lead to delays in execution, depending on the source code and available computing power.
Here, we see a result table of a test using multi-degree polynomials. Notice the difference between the times when the pow () function was used and when it was not.
Step 25: Test Code - Setup () and Loop Start ()
Here, we have the code used in our test.
void setup() {
Serial.begin(1000000); //Iniciando a porta serial somente para debug } void loop() { float valor_analogico = 500.0; //um valor arbtrario float quantidade = 10000.0; //quantidade de chamadas float contador = 0.0; //contador de chamadas
Step 26: Test Code - Loop () and Processing
I used the micros () function to get the value in microseconds.
//============= inicia o processo
float agora = micros(); //marca o instante inicial while (contador < quantidade) { //v(valor_analogico); //função vazia //r(valor_analogico); //função com retorno //f0(valor_analogico); //grau 0 //f1(valor_analogico); //grau 1 //f2(valor_analogico); //grau 2 //f3(valor_analogico); //grau 3 //f4(valor_analogico); //grau 4 //f5(valor_analogico); //grau 5 //f6(valor_analogico); //grau 6 //f13_semPow(valor_analogico); //grau 13º SEM a função POW //f13_comPow(valor_analogico); //grau 13º COM a função POW contador++; } agora = (micros() - agora) / quantidade; //determina o intervalo que se passou para cada iteração //============= finaliza o processo
Step 27: Test Code - Loop () - Results
We print the value returned from the grade 13 function with and without POW for comparison, as well as the processing interval.
//imprime o valor retornado da função de grau 13 com e sem POW para comparação
Serial.print(f13_semPow(valor_analogico)); //grau 13º SEM a função POW Serial.print(" - "); Serial.print(f13_comPow(valor_analogico)); //grau 13º COM a função POW Serial.print(" - "); //imprime o intervalo do processamento Serial.println(agora, 6); }
Step 28: Test Code - Used Functions
Empty functions (only with return) of degree 0 and 1.
//FUNÇÃO VAZIA
double v(double x) { } //FUNÇÃO SOMENTE COM RETORNO double r(double x) { return x; } //FUNÇÃO DE GRAU 0 double f0(double x) { return 2.202196968876e+02; } //FUNÇÃO DE GRAU 1 double f1(double x) { return 2.202196968876e+02 + 3.561383996027e-01 * x; }
Grade 2, 3, and 4 functions.
//FUNÇÃO DE GRAU 2
double f2(double x) { return 2.202196968876e+02 + 3.561383996027e-01 * x + 1.276218788985e-04 * pow(x, 2); } //FUNÇÃO DE GRAU 3 double f3(double x) { return 2.202196968876e+02 + 3.561383996027e-01 * x + 1.276218788985e-04 * pow(x, 2) + -3.470360275448e-07 * pow(x, 3); } //FUNÇÃO DE GRAU 4 double f4(double x) { return 2.202196968876e+02 + 3.561383996027e-01 * x + 1.276218788985e-04 * pow(x, 2) + -3.470360275448e-07 * pow(x, 3) + 2.082790802069e-10 * pow(x, 4); }
Grade 5 and 6 functions.
//FUNÇÃO DE GRAU 5
double f5(double x) { return 2.202196968876e+02 + 3.561383996027e-01 * x + 1.276218788985e-04 * pow(x, 2) + -3.470360275448e-07 * pow(x, 3) + 2.082790802069e-10 * pow(x, 4) + -5.306931174991e-14 * pow(x, 5); } //FUNÇÃO DE GRAU 6 double f6(double x) { return 2.202196968876e+02 + 3.561383996027e-01 * x + 1.276218788985e-04 * pow(x, 2) + -3.470360275448e-07 * pow(x, 3) + 2.082790802069e-10 * pow(x, 4) + -5.306931174991e-14 * pow(x, 5) + 4.787659214703e-18 * pow(x, 6); }
Grade 13 function using the POW.
//FUNÇÃO DE GRAU 13 USANDO O POW
double f13_comPow(double x) { return 2, 161282383460e+02 + 3, 944594843419e-01 * x + 5, 395439724295e-04 * pow(x, 2) + -3, 968558178426e-06 * pow(x, 3) + 1, 047910519933e-08 * pow(x, 4) + -1, 479271312313e-11 * pow(x, 5) + 1, 220894795714e-14 * pow(x, 6) + -6, 136200785076e-18 * pow(x, 7) + 1, 910015248179e-21 * pow(x, 8) + -3, 566607830903e-25 * pow(x, 9) + 5, 000280815521e-30 * pow(x, 10) + 3, 434515045670e-32 * pow(x, 11) + -1, 407635444704e-35 * pow(x, 12) + 9,871816383223e-40 * pow(x,13); }
Grade 13 function without using POW.
//FUNÇÃO DE GRAU SEM USAR O POW
double f13_semPow(double x) { return 2, 161282383460e+02 + 3, 944594843419e-01 * x + 5, 395439724295e-04 * x * x + -3, 968558178426e-06 * x * x * x + 1, 047910519933e-08 * x * x * x * x + -1, 479271312313e-11 * x * x * x * x * x + 1, 220894795714e-14 * x * x * x * x * x * x + -6, 136200785076e-18 * x * x * x * x * x * x * x + 1, 910015248179e-21 * x * x * x * x * x * x * x * x + -3, 566607830903e-25 * x * x * x * x * x * x * x * x * x + 5, 000280815521e-30 * x * x * x * x * x * x * x * x * x * x + 3, 434515045670e-32 * x * x * x * x * x * x * x * x * x * x * x + -1, 407635444704e-35 * x * x * x * x * x * x * x * x * x * x * x * x + 9, 871816383223e-40 * x * x * x * x * x * x * x * x * x * x * x * x * x; }