How to Make and Test a Better DAC With ESP32
by the2ndTierney in Circuits > Microcontrollers
8654 Views, 7 Favorites, 0 Comments
How to Make and Test a Better DAC With ESP32
The ESP32 has 2 8-bit Digital to Analogue Converters (DACs). These DACs allow us to produce arbitrary voltages within a certain range (0-3.3V) with 8 bits of resolution. In this Instructable, I will show you how to build a DAC and characterise its performance as well as compare it to the ESP32 DAC. The performance indices I will look at include
- Noise Level
- Bandwidth
- Integral nonlinearity
- Differential nonlinearity
To test these indices I will use the ADS1115.
It's important to note that your assessment of all these indices will only be as accurate as your reference device (in this case the ADS115). For instance, the ADS115 does not have 16-bit precision when it comes to its voltage offset and gain. These errors may be as large as 0.1%. For many systems, these errors can be ignored when absolute accuracy is of limited concern.
Supplies
- ADS1115
- ESP32 Board
- breadboard
- jumper wires
- 5 kOhm Resistor
- 1 micro-Farad ceramic capacitor
Laying Out the Breadboard
Wire the following pins
Between the ESP32 and the ADS1115
3v3 --> VDD
GND -->GND
GPIO22 -->SCL
GPIO21 -->SDA
At the ADS1115
ADDR -->GND (ADS115)
Making the DAC
There are many ways to make a DAC. The simplest is to low-pass filter a PWM signal with a resistor and a capacitor. I could have added an op-amp in here as a buffer but wanted to keep things simple. This design is simple and cheap to implement with any microcontroller that supports PWM. I'm not going to go through the theory of the design here (google PWM DAC).
Just connect GPIO25-->5 KOhm resistor --> 1 microFarad Capacitor -->gnd
Now connect a jumper wire from the point where the resistor meets the capacitor to A0 on the ADS115.
Assess Signal to Noise Level
To assess the noise level simply run the script below. To assess this we simply leave the DAC at a fixed value and measure how the voltage oscillates over time.
Due to the design of the DAC, the noise will be greatest when the PWM signal is at 50% duty cycle. Therefore this is where we will assess it. We will also assess the ESP32 at this same signal level. We will also filter the ESP32 DAC with the same low pass filter so as to make the measurement comparable.
For me the output was clear. The PWM design had >6dB better SNR (that's 2 times better). A clear win for new DAC. One slight confound is that there are filters built into the ADC that are most definitely enhancing the SNR. So the absolute values may be difficult to interpret. If I had used a second-order filter this would not be the case.
Anyway code is below
#include <Wire.h> #include <Adafruit_ADS1015.h> Adafruit_ADS1115 ads; // adafruit library for adc int16_t adc0; // void setup(void) { Serial.begin(115200); // Start serial ads.setGain(GAIN_TWO); // 2x gain +/- 2.048V 1 bit =0.0625mV ads.begin(); // begin adc float M = 0; // initial mean float Mp = 0; // previouos mean float S = 0; // initial Variance float Sp = 0; // previous variance const int reps = 500; // number of repitions int n = 256; // number of samples ledcSetup(0, 25000, 8); // set pwm frequecny =25000 Hz at 8 bits resolution ledcAttachPin(25, 0); // set pwm on pin 25 ledcWrite(0, 128); // set it to half duty cycle(biggest noise) delay(3000); // wait for settling time float snrPWM[reps]; // array of snrs for PWM float snrDAC[reps]; // array of snrs for DAC for (int i = 0; i < reps; i++) { // loope over repititions for (int k = 1; k < (n + 1); k++) { // loope over samples adc0 = ads.readADC_SingleEnded(0); // get reading M = Mp + (adc0 - Mp) / k; // compute rolling mean Mp = M; // set previous mean S = Sp + (adc0 - Mp) * (adc0 - M); // compute rolling variance Sp = S; // set previous variance } // snr in dB snrPWM[i] = 20 * log10(3.3 / (sqrt(S / n) * .0625 * .001)); //reset values M = 0; Mp = 0; S = 0; Sp = 0; } ledcDetachPin(25); // detach PWM from pin 25 dacWrite(25, 128); // write to DAC delay(3000); // wait to settle for (int i = 0; i < reps; i++) { // same as PWM loop for (int k = 1; k < (n + 1); k++) { adc0 = ads.readADC_SingleEnded(0); M = Mp + (adc0 - Mp) / k; Mp = M; S = Sp + (adc0 - Mp) * (adc0 - M); Sp = S; } snrDAC[i] = 20 * log10(3.3 / (sqrt(S / n) * .0625 * .001)); M = 0; Mp = 0; S = 0; Sp = 0; } // plot SNRs on one graph for (int i = 1; i < reps; i++) { Serial.print("PWM_SNR(dB):"); Serial.print(snrPWM[i]); Serial.print(", "); Serial.print("ESP32_SNR(dB):"); Serial.println(snrDAC[i]); } } void loop(void) { }<br>
Integral Nonlinearity and Differential Nonlinearity
The integral nonlinearity is a measure of roughly how much deviation there is between your DAC output voltage and a straight line. The bigger this is the worse it is...
The differential nonlinearity is a measure of roughly how much the observed change in the voltage (from one code to the next) deviates from what would be expected from a straight line.
The results here were really interesting. First of all, both have less than 0.5lsb error (at 8-bit resolution) which is good but the PWM has much better integral linearity. Both have comparable differential nonlinearity but the ESP32 DAC has some very weird spikes. What is more, the PWM method has some structure to the errors. Essentially it overshoots and undershoots the correct voltage in an alternating fashion.
My suspicion is this is some weird rounding error in how an 8-bit PWM signal is produced on the ESP32.
One way to correct for this is to rapidly cycle between two adjacent codes (e.g 128,129) with the PWM. With an analogue lowpass filter, the resulting errors will average to zero. I simulated this in software and indeed all the errors disappeared. Now the PWM method has linearity that is accurate to 16-bits!
Anywho the code to generate the data is below. The output will be on the serial monitor in .csv format. Just copy it to a text file for further processing.
#include <Wire.h> #include <Adafruit_ADS1015.h> Adafruit_ADS1115 ads; /* Use this for the 16-bit version */ int16_t adc0; void setup(void) { Serial.begin(115200); ads.setGain(GAIN_ONE); // 2x gain +/- 2.048V 1 bit = 1mV 0.0625mV ads.begin(); ledcSetup(0, 25000, 8); ledcAttachPin(25, 0); Serial.println("Expected,Observed "); ledcWrite(0, 2); delay(3000); for (int i = 2; i < 255; i++) { ledcWrite(0, i ); delay(100); adc0 = ads.readADC_SingleEnded(0); float expected = (i / 256.0 * 3.3) / 4.096 * 32767; Serial.print(expected); Serial.print(","); Serial.println(adc0); } } void loop(void) { }<br>
Bandwidth
I'm going to define bandwidth as here as the frequency at which the output of the DAC drops by 3dB. This is a convention and, to a degree, arbitrary. For instance, at the 6dB point, the DAC will still output a signal it will just be ~50% amplitude.
To measure this we simply pass sine waves at an increasing frequency from the DAC to the ADC and measure their standard deviation. Unsurprisingly, the 3dB point is at 30Hz (1/(2*pi*5000*1e-6)).
The ESP32 can do 1 Mega sample per second. This is a hands-down win for the ESP32. Its amplitude does not decay at all in the 100Hz bandwidth test region.
The code below can test the PWM DAC bandwidth.
#include <Wire.h> #include <Adafruit_ADS1015.h> Adafruit_ADS1115 ads; /* Use this for the 16-bit version */ int16_t adc0; int16_t adc1; void setup(void) { float M; float Mp = 0; float S = 0; float Sp = 0; Serial.begin(115200); ads.setGain(GAIN_ONE); // 1x gain +/- 4.096V 1 bit = 2mV 0.125mV ads.begin(); ledcSetup(0, 25000, 8); ledcAttachPin(25, 0); delay(5000); Serial.println("Frequency,Amplitude "); for (int i = 1; i < 100; i++) { unsigned long start = millis(); unsigned long T = millis(); Sp = 0; S = 0; M = 0; Mp = 0; int k = 1; float norm; while ((T - start) < 1000) { int out = 24 * sin(2 * PI * i * (T - start) / 1000.0) + 128; ledcWrite(0, out); adc0 = ads.readADC_SingleEnded(0); M = Mp + (adc0 - Mp) / k; Mp = M; S = Sp + (adc0 - Mp) * (adc0 - M); Sp = S; T = millis(); k++; } if (i == 1) { norm = sqrt(S / k); } Serial.print(i); Serial.print(","); Serial.println(sqrt(S / k) / norm,3); k = 0; } } void loop(void) { }
And this code will test the ESP32 bandwidth. Make sure to remove the capacitor or the results will be the same for both methods.
#include <Wire.h> #include <Adafruit_ADS1015.h> Adafruit_ADS1115 ads; /* Use this for the 16-bit version */ int16_t adc0; int16_t adc1; void setup(void) { float M; float Mp = 0; float S = 0; float Sp = 0; Serial.begin(115200); ads.setGain(GAIN_ONE); // 1x gain +/- 4.096V 1 bit = 2mV 0.125mV ads.begin(); delay(5000); Serial.println("Frequency,Amplitude "); for (int i = 1; i < 100; i++) { unsigned long start = millis(); unsigned long T = millis(); Sp = 0; S = 0; M = 0; Mp = 0; int k = 1; float norm; while ((T - start) < 1000) { int out = 24 * sin(2 * PI * i * (T - start) / 1000.0) + 128; dacWrite(25, out); adc0 = ads.readADC_SingleEnded(0); M = Mp + (adc0 - Mp) / k; Mp = M; S = Sp + (adc0 - Mp) * (adc0 - M); Sp = S; T = millis(); k++; } if (i == 1) { norm = sqrt(S / k); } Serial.print(i); Serial.print(","); Serial.println(sqrt(S / k) / norm,3); k = 0; } } void loop(void) { }
Closing Thoughts
The new DAC design wins on linearity and noise but loses on bandwidth. Depending on your application one of these indices may be more important than the other. With these testing procedures, you should be able to objectively make that decision!
Also, I think it's worth pointing out here that because PWM output is low noise, with exceptional linearity it should be possible to construct a much higher resolution DAC with the PWM output (maybe even 16-bit precision). That's gonna take some work. Until then, I bid you adieu!