Need an extra ADC? Add one for a few cents



When designing devices with microcontrollers (MCU), I like to use some of the analog-to-digital converter (ADC) inputs to measure onboard voltages along with all the required sensors inputs. This means I often run out of ADC inputs. So presented here is a way to add more ADCs without adding external chips, costing less than 5 cents, and taking up negligible space on a PCB!

There are two things in the MCU you are using: a pulse width modulator (PWM) output and an onboard analog comparator. Some MCU lines that have these are the PIC, AVR, and ATmega MCUs from Microchip. Also, TI’s Piccolo line and STMicroelectronics STM32L5 have both a PWM and comparator.

So, let’s look at how this is configured.

The basic concept

Figure 1 is a diagram showing the addition of a resistor and capacitor to your MCU project.

Figure 1 Basic concept of the circuit that uses an MCU with an onboard PWM and comparator, as well as an RC filter to create an ADC.

The resistor and capacitor form a single pole low-pass filter. So, the circuit concept takes the output of an onboard PWM, filters it to create a DC signal that is set by the PWM’s duty cycle. The DC level is then compared to the input signal using the on-board comparator. The circuit is very simple so let’s talk about the code used to create an ADC from this arrangement.

To get a sample reading of the input signal, we start by setting the PWM to a 50% duty cycle. This square-wave PWM signal will be filtered by the RC low-pass filter to create a voltage that is ½ of the MCU’s system voltage. The comparator output will go high (or output a digital 1) if the filtered DC level is greater than the instantaneous input signal voltage, otherwise the comparator output will go low (outputting a digital 0).

The code will now read the comparator output and execute a search to find a new level that forces the comparator to an opposite output. In other words, if the comparator is a 0 the code will adjust the PWM duty cycle up until the comparator outputs a 1. If the comparator is currently showing a 1 the PWM duty cycle will be reduced until the comparator outputs a 0. If the PWM is capable of something like 256 steps (or more) in duty cycle, this search could take some significant time. To mitigate this, we will do a binary search so if there are 256 steps available in the PWM, it will only take log2(256), or 8, steps to test the levels.

A quick description of the binary search is that after the first 50% level reading, the next test will be at a 25% or 75% level, depending on the state of the comparator output. The steps after this will again test the middle of the remaining levels.

An example of the circuit’s function

Let’s show a quick example and assume the following:

  • System voltage: 5 V
  • PWM available levels: 256
  • Instantaneous input signal: 1 V

The first test will be executed with the PWM at about 50% duty cycle (a setting of 128), creating a 2.50-V signal that is applied to the “+” input of the comparator. This means the comparator will output a high which implies that the PWM duty cycle is too high. So, we will cut the duty cycle in half giving a setting of 64, which creates 1.25 V on the “+” input. The comparator again will output a 1… to high so we drop the PWM duty cycle by half again to 32. This gives a “+” level of 0.625 V. Now the comparator will output a 0 so we know we went too low, and we increase the PWM duty cycle. We know 64 was too high and 32 was too low so we go to the center, or (64+32)/2 = 48, giving 0.9375 V. We’re still too low so we split the difference of 64 and 48 resulting in 56 or about 1.094 V…too high. This continues with (56+48)/2=52, giving 1.016 V…too high. Again, with a PWM setting of (52+48)/2=50, giving 0.9766 V. One last step, (52+50)/2=51 giving 0.9961 V.

This was 8 steps and got us as close as we can to the answer. So, our ADC setup would return an answer that the instantaneous input signal was 0.9961 V.

Sample circuit with Arduino Nano

Let’s take a look at a real-world example. This example uses an Arduino Nano which uses an ATmega328P which has a number of PWM outputs and one analog comparator. The PWM we will use can be clocked at various rates and we want to clock it fast as this will make the filtering easier. It will also speed up the time for the filter output to settle to its final level. We will select a PWM clocking rate of about 31.4 kHz. Figure 2 shows the schematic with a one pole RC low-pass filter.

Figure 2 Schematic of the sample circuit using an Arduino Nano and a one-pole RC low-pass filter.

In this schematic D11 is the PWM output, D6 is the comparator’s “+” input, while D7 is the comparator’s “-” input. The filter is composed of a 20kΩ resistor and a 0.1 µF capacitor. I arrived at these values by playing around in an LTspice simulation to try to minimize the remnants of the PWM pulse (ripple) while also maintaining a fairly fast settling time. A target for the ripple was the resolution of a 1-bit change in the PWM, or less. Using the 5 V of the system voltage and the information that the PWM has 8-bit (256 settings) adjustability we get 5 V/256 = ~20 mV. In the LTspice simulation I got 18 mV of ripple while the dc level settled in within a few millivolts of its final value at 15 ms. Therefore, when writing the code, I used 15 ms as the delay between samples (with a small twist you’ll see below). Since it takes 8 readings to get a final usable sample, it will take 8*15 ms = 120 ms, or 8.3 samples per second. As noted at the beginning, you won’t be sampling at audio rates, but you can certainly monitor DC voltages on the board or slow-moving analog signals.

[This may be a good place to note that the analog input does not have a sample-and-hold as most ADCs do, so readings are a moving target. Also, there is no anti-aliasing filter on the input signal. If needed, an anti-alias filter can remove noise and also act as a rough sample and hold.]

Sample code

Below is the code listing for use in an Arduino development environment. You can also download it here. It will read the input signal, do the binary search, convert it to a voltage, and then display the final 8-bit DAC value, corresponding voltage reading, and a slower moving filtered value.

The following gives a deeper description of the code:

  • Lines 1-8 define the pin we are using for the PWM and declares our variables. Note that line 3 sets the system voltage. This value should be measured on your MCU’s Vcc pin.
  • Lines 11 and 12 set up the PWM at the required frequency.
  • Lines 15 and 16 set up the on-board comparator we are using.
  • Line 18 initializes the serial port we will print the results on.
  • Line 22 is where the main code starts. First, we initialize some variables each time to begin a binary search.
  • Line 29 we begin the 8-step binary search and line 30 sets the duty cycle for the PWM. A 15-millisecond delay is then introduced to allow for the low-pass filter to settle.
  • Line 34 is the “small twist” hinted at above. This introduces a second, random, delay between 0 and 31 microseconds. This is included because the PWM ripple that is present, after the filter, is correlated to the 16-MHz MCU’s clock so, to assist in filtering this out of our final reading, we inject this delay to break up the correlation.
  • Lines 37 and 38 will check the comparator after the delay is implemented. Depending on the comparison check, the range for the next PWM duty cycle is adjusted.
  • Line 40 calculates the new PWM duty cycle within this new range. The code then loops 8 times to complete the binary search.
  • Lines 43 and 44 calculate the voltage for the current instantaneous voltage reading as well as a filtered average voltage reading. This voltage averaging is accomplished using a very simple IIR filter.
  • Lines 46-51 send the information to the Arduino serial monitor for display.
1  #define PWMpin 11 // pin 11 is D11
2  
3  float systemVoltage = 4.766; // Actual voltage powering the MCU for calibrating printedoutput voltage
4  float ADCvoltage = 0; // Final discovered voltage
5  float ADCvoltageAve = 0; // Final discovered voltage averaged
6  uint8_t currentPWMnum = 0; // Number sent to the PWM to generate the requested voltage
7  uint8_t minPWMnum = 0; 
8  uint8_t maxPWMnum = 255; 
9  
10 void setup() { 
11   pinMode(PWMpin, OUTPUT); // Set up PWM for output 
12   TCCR2B = (TCCR2B & B11111000) | B00000001;  // Set timer 1 to 31372.55 Hz which is now the D11 PWM frequency
13 
14   // Set up comparator
15   ADCSRB = 0b01000000; // (Disable) ACME: Analog Comparator Multiplexer disabled
16   ACSR = 0b00000000; //enable AIN0 and AIN1 comparison with interrupts disabled  
17 
18   Serial.begin(9600); // open the serial port at 9600 bps:
19 }
20 
21 
22 void loop() {
23 
24   currentPWMnum = 127; // Start binary search at the halfway point  
25   minPWMnum = 0; 
26   maxPWMnum = 255; 
27 
28   // Perform a binary search for matching comparator setting
29   for (int8_t i = 0; i < 8; i++) { // Loop 8 times   
30     analogWrite(PWMpin, currentPWMnum); // Adjust PWM to new dutycycle setting  
31       
32     // Now wait
33     delay(15); // Wait 15 ms to let the low-pass filter to settle.
34     delayMicroseconds(random(0,32)); // Delay a random number of microseconds (0 thru 31) to break possible correlation (dithering)
35   
36     // Check to see if comparator shows AIN0 > AIN1 ( if so ACO in ACSR is set to 1)
37     if (ACSR & (1<<ACO))  maxPWMnum = currentPWMnum; // (AIN0 > AIN1) Move max pointer
38     else minPWMnum = currentPWMnum; // Move min pointer  
39 
40     currentPWMnum= minPWMnum + ((maxPWMnum - minPWMnum) / 2); // Set new test number to the middle of PWMmin and PWMmax
41   } 
42 
43   ADCvoltage = systemVoltage * ((float)currentPWMnum/255); // Set the PWM for binary search of voltage (assumes 0 to 5v signal
44   ADCvoltageAve = (ADCvoltageAve * 0.95) + (ADCvoltage * 0.05); // Generate an average value to smooth reading
45 
46   Serial.print("PWM Setting = ");
47   Serial.print(currentPWMnum);
48   Serial.print("   ADC Voltage  = ");
49   Serial.print(ADCvoltage, 4);
50   Serial.print("   ADC Voltage Filtered = ");
51   Serial.println(ADCvoltageAve, 4);
52 }    

Test results

The first step was to measure the system voltage on the +5-V pin or the Arduino Nano. This value (4.766 V) was entered on line 3 of the code. I then ran the code on an Arduino Nano V3 and monitored the output on the Arduino serial monitor. To test the code and system, I first connected a 2.5-V reference voltage to the signal input. This reference was first warmed up and a voltage reading was taken on a calibrated 5 ½ digit DMM. The reference read 2.5001 V. The serial monitor showed an instantaneous voltage varying from 2.5232 to 2.4858 V and the average voltage varied from 2.5061 to 2.5074 V. This is around 0.9% error in the instantaneous voltage reading and about 0.3% on the averaged voltage reading. This shows we are getting a reading with about ±1 LSB error in the instantaneous voltage reading and a filtered reading of about ± 0.4 LSB. When inputting various other voltages I got similar accuracies.

I also tested with an input of Vcc (4.766 V) and viewed results of 4.7473 V which means it could work up very close to the upper rail. With the input grounded the instantaneous and filtered voltages showed 0.000 V.

These seem to be a very good result for an ADC created by adding two inexpensive parts.

So next time you’re short of ADCs give this a try. The cost is negligible, PCB space is very minimal, and the code is small and easy to understand.

Damian Bonicatto is a consulting engineer with decades of experience in embedded hardware, firmware, and system design. He holds over 30 patents.

Phoenix Bonicatto is a freelance writer.

Related Content

<!–
googletag.cmd.push(function() { googletag.display(‘div-gpt-ad-native’); });
–>

The post Need an extra ADC? Add one for a few cents appeared first on EDN.



Source link