Arduino(TM) Projects

Dive into various aspects of Arduino.

Analog performance, particularly of Due and ATSAMD21 12 bit ADC

The Blog for this project


Due and ARM Cortex M3
The Due is a pretty cool processor and shows what ARM Cortex 32 bit processors have done for the market. At least some of the cost reduction must be because instead of designing a processor architecture from scratch, a manufacturer licenses it from ARM for a few pennies per chip. So the manufacturer doesn't need to recoup development costs, and the net cost to the end user is generally lower than proprietary devices. And ARM is a good architecture. It uses the "Thumb" instruction set optimization to minimize program memory size. It uses caching and pipelining to increase execution speed, and high performance peripherals with DMA to increase I/O performance. And when you learn one ARM processor, you have learned something about all ARMs and about all manufacturer's ARMs. With 8/16 bit bit low-end processors, there are only proprietary architectures. With embedded 32 bits, the ARMs are winning and I think most proprietary architectures will  fade into irrelevance. ARM Cortex M0's, the low end devices, are currently giving the 8 bit market a run for the money.

ADC Testing
As an analog type, one of the first things I do with a new processor is to read the analog data sheet and measure the performance of its analog peripherals. The things I'm looking for:

The answers to these questions tell me if the ADC is good enough to do a certain job. Do I need to improve it? Do I need to calibrate each system? Can I squeeze a bit more performance out of it? Or do I need another processor or an external ADC to do the job?

Measuring real bits
An LSB is one count of the ADC. At 5V and 10 bits,  an LSB is 5V / (2^10 -1) = about 5mV. In a perfect universe you would execute analogRead(A0) and it would return a nice stable value equal to Vin * 1023  / 5.0. But in this universe, you get a whole bunch of errors. The ADC has offset and gain errors, the reference for the ADC has errors and noise, the ADC has noise, and worst of all it has non-linearity. And the next ADC you measure, even of the same type,  will have a set of different errors.

Depending on what your accuracy requirements are, you can either improve or correct for some of these.

Minimum industry standards dictate that an ADC has no missing codes. Meaning that as you slowly increase the voltage, all 2^N output values codes will eventually show up. It's a pretty low bar. But since marketing weasels write the first page of a data sheet, you need to dig deep into the numbers to get the whole truth. Marketing says "12 Bits", but the engineers that design and test these parts know the real truth, and usually publish it somewhere on pages 2 through N.  

Resolution
and Accuracy
Accuracy is probably your ultimate goal. You want to know that when you apply 1.000 volts to your 5.0V ADC you'll get 1/5 of the full scale number. For a perfect ADC, you would get 1.00 * 1023 / 5.00 = 204.6,  +/- 0.5. But to get that, you need offset and gain accuracy of less than 1/2 LSB, a reference accurate to better than 1/2 LSB (.05%), and an ADC with less than 1/2 LSB of INL, DNL and noise.   Even with these near perfect 1/2LSB specs, these 6 error sources can and will all add up, and you can get errors approaching 6 * 1/2LSB = 3LSBs. Ouch. Then if any of these specs drift with temperature (Note* "Everything drifts with temperature." ) then when the temperature changes, things typically get worse. This is why an error budget is an important part of engineering.

* Erickson's Law of Temperature Drift

Calibration
One technique to correct for some errors is to calibrate. Gain, offset, (and reference)  errors can often be calibrated out with a simple linear correction. Higher order errors are generally impractical to compensate for. Remember the equation of a line from high school:  Y=MX+B? If you know M, the gain correction, and B, the offset correction of your ADC, then you can correct for these. If you know M and B at various temperatures, you could even compensate for temperature drift but setting temperature and building a table of calibration vs. temperature is usually too expensive. INL, DNL and noise are harder to compensate for. INL requires that you know the errors at every possible input value and build a table of corrections. It can be done, but again, expensive (time consuming)  and uses lots of memory. Few real systems go to this level of complexity. Engineers usually just buy a better ADC and reference. It is the rare ADC that has gain and offset error specs as low as1/2 LSB.

In the good-old-days, trim-pots were used to correct for offset and gain errors and to provide calibration. But trim-pots require manual labor to adjust and are prone to their own thermal and mechanical drifts. Good trim-pots are fairly expensive. A 10 turn trim-pot costs about the same as an Arduino processor chip, thank you, Gordon Moore. When writable non-volatile memory such as EEPROM became widely available, trim-pots for calibration became rare.  In the case of most Arduinos, calibration factors can be stored in either EEPROM or Flash program memory. If you don't mind re-calibrating every time you power up, store them in RAM. That requires one or more precision sources that can be measured. This is how modern digital oscilloscopes and waveform generators do it.

So what are the real error numbers?

ADC Voltage Reference
Most Arduinos default to using VCC as the ADC reference. If the VCC is 5V, then it either comes from the USB connector or from the voltage regulator. USB 5V as a reference is pretty bad, since it comes from a PC with about 3-5% basic accuracy. Then there are voltage and ground drops across the cable, depending on the host, the length of the USB cable, the other devices plugged into USB, and the USB current load. Even turning on a handful of LEDs  on your Arduino can vary the supply voltage. Forget about powering 1/2 Amp of relays, servos or motors.

But.... there is one case where using a crappy reference works. That is the case where the sensor is ratiometric and is powered from the reference. When the reference voltage drops, the ADC gain increases. As long as the sensor output also drops, the reference errors cancel out. Examples of ratiometric sensors are thermistors and other resistive sensors with a pull-up resistor, and bridge sensors (pressure and strain).

Adding a more precise voltage reference to an Arduino isn't hard. Pay a few dollars, and connect a 2 to 4.5V (3.3V max on the Due) reference source to the reference pin, and set the Arduino to use external reference with analogReference(type). Compared to VCC, this will provide better accuracy, noise, and stability.

The Due and some other 3.3V Arduinos use the 3.3V power supply for a reference. It can still be noisy and have a few percent of error, but is better than USB +5V since it is locally regulated. What is the difference between a reference and a linear voltage regulator?  The big difference is that a reference is always specified for temperature drift but a linear regulator generally is not. A linear regulator is basically a high power, not too accurate reference. Good references are also better than 1%, specified for noise, low power, etc.

Measuring ADC noise
ADC noise is one limit of ADC accuracy. All you need for a quick ADC measurement is an example program that measures the ADC outputs the data to a serial port. The Arduino Example "AnalogReadSerial" does this nicely. The steps are simple:

Here is the noise plot and measurements for a Due 12 bit (4096 count) ADC. I measure 6 counts p-p and 1.00 counts RMS noise. For a Gaussian response, the ratio of p-p to RMS is about 5 to 1. Notice int the test data that there is only one point at value 2277. Without that sample the p-p would be 5, not 6.

1.00 counts of ADC noise basically takes 1 bit away from the specified ("Marketing") 12 bits, making it an 11 bit ADC. Look a the SAM3S processor ADC specs and you will see that the ENOB (equivalent Number of Bits) is  INL (Integral Non-Linearity) spec is +/- 1.2LSB max. over full temperature. (Equivalent number of Bits) is 10 typ. 11 max. ENOB is a measure of AC (dynamic) accuracy, not DC noise.

Due Noise

Also look for any repeating pulses or patterns in the noise data. These artifacts generally indicate some periodic noise or interference pattern. You should dig in with a 'scope to see what is causing it.

Another good noise analysis tool is the histogram. By displaying a histogram of the noise readings, you can see if the distribution is roughly gaussian or has some other distribution.

Increasing Resolution with Averaging
If the ADC noise is gaussian and is 1/2 LSB or more, then you can use this to your advantage by averaging the ADC values. Noise decreases by 1/(N^2). So averaging 4 values reduces noise by 1/2, providing another bit of resolution.  Averaging 64 values improves noise by 1/8, providing nearly 3 extra bits of resolution. This method  is particularly useful when you measure DC or slowly varying readings such as temperature. Summing N readings with integer math is easy. Then divide the sum by the number of samples collected. The easy way is to do a floating point divide which will produce a floating point result with the fractional part indicating fractions of an ADC LSB.

On 10 bit Arduino ADCs, the noise is typically less than 1/2 LSB and the distribution is typically not Gaussian. So averaging does not work. And don't forget that additional resolution is no additional accuracy. Ultimate accuracy of the ADC is dependent on INL. The INL spec for the Due are +/- 1.2LSBs over the full temperature range. I'd guess you would get  about +/- .75 LSB over a narrow room temperature range. Its just a guess.

Measuring and correcting Gain and Offset errors
Write a program to read and average 100 ADC readings, scale the data from Counts to Volts, and output the average value in floating point format, say 5 digits. Here is the program for a Due, with 12 bits of ADC resolution. 

Averaging program for gain and offset

Then apply a few known voltages spread out over the ADC input range. Use a multimeter with more resolution than your ADC to measure the voltages. Record the applied voltages and the measured voltages. Enter the numbers into 2 columns in Excel. Plot the measured voltages vs. the known voltages. That plot will give you the gain and offset errors. I like to just plot the errors: measured value minus the expected value (Vin * 1023/ 5.0).  You can use Excel's trendline feature (right click on the plot) and a linear regression to calculate your gain and offset calibration values. Then display the trend line equation on the plot. You may need to display more digits for the M (slope) and B (intercept). 

This is the 12 bit ADC on a Due board, with the default +3.3V reference. It has -20.9mV of offset error and +1.29 % gain error. To correct for this, add 20.9mV to your measurement and multiply by 1 / 1.0129.

The gain and offset specs for the Due ADC are gain error of typically 0.56% and offset of 11.5 codes (9.2mV). This part had 20.9mV or more than 2x the typical offset, and 12.% or more than 2X the gain error. Don't trust typical specs.

gain and offset error

The down-side to gain calibration is that you introduce missing codes. By multiplying the ADC counts by a non-integer number, some codes disappear, causing some 2 code jumps in your readings. Averaging first means that you have a bit more resolution than one LSB, and the jumps are obscured.

Due ADC Speed
The function analogRead() in a tight loop takes 39uS  per loop. Considering that the ADC can convert in 1uS and the processor can execute dozens of instruction in 1 uS. what's the beef? I dug into the analogRead() function to find out why. The first observation is that analogRead() does not initialize the ADC, it just sets the channel, starts a conversion and waits for the result. Initialization is done elsewhere. So basic code structure isn't the problem. Then I dug into the register settings. The register ADC_MR (Mode Register) contains a number of settings that affect ADC timing: TRACKTIM, SETTLING, STARTUP, and PRESCALE in addition to SLEEP. The following code allow you to display the 32 bit contents of the MR register via the serial port.

  unsigned long reg;
  reg = REG_ADC_MR;
  Serial.print("REG_ADC_MR = ");
  Serial.println(reg, HEX);


When run,  you get:

    REG_ADC_MR = 103C0100

The master clock, MCLK is 84MHz. The ADC prescaler is set to 0x01 which is divide by 4 so the ADC clock is 84MHz / 4 = 21MHz. That is as fast as it can go, so that's not the problem. The ADC clock period is 1 / 21MHz = 37.6nS. The 0xC sets STARTUP is 768. 768 / 21MHz = 36.6uS. Bingo! So I set STARTUP to a smaller number, 2, (value = 16 / 21M = .76uS) with this line of code:

    REG_ADC_MR = (REG_ADC_MR & 0xFFF0FFFF) | 0x00020000;

And the ADC loop now takes 4uS. Cool. To make it even faster, I also set the Settling number down from 3 to 1. The data sheet recommends settling and tracking numbers. It also says that if you want the ADC to convert faster than 500KHz, set the IBCTL field in ADC_ACR to 01.  I checked, and it was already is set to 01.

I found the Arduino line of code that initializes the ADC in \arduino-1.5.1r2\hardware\arduino\sam\variants\arduino_due_x\variants.cpp

    adc_init(ADC, SystemCoreClock, ADC_FREQ_MAX, ADC_STARTUP_FAST);

Sure, it's a hack to use hex numbers to set register fields, but until I find all the right .h files, it will do.  Here are the #defines for the STARTUP field from   
\arduino\sam\system\libsam\include\adc.h

    /* The normal adc startup time*/
    #define ADC_STARTUP_NORM     40
    /* The fast adc startup time*/
    #define ADC_STARTUP_FAST     12


This may be a bug. STARTUP is a four bit field so 12 (0xC) works, but 40 (0x28) won't work. And 12 sets the value to 768 which is why the ADC is so slow. So I suspect that the programmer confused the value with the hardware settings. The table of values vs. register settings is in the SAM3S processor manual.

Accessing Due Registers
I was unable to find a document showing how to directly access Due registers, but by doing text searches in the Arduino directories, I found that all of the SAM3X registers are defined simply as the register names preceded by REG_ such as REG_ADC_MR. Don't forget that these registers are generally 32 bits, so use unsigned long variables to read and write them.

Measurement of ATSAMD21 ADC (Mar 2024)

In 2023/2024 I was looking for a small module processor with a low-ish cost and decent 12b performance. My favorite Teensy processors have all dried up except for Teensy 4. I can't see using a 600MHz processor in places where an 8 bit ATMEGA would easily do the job. But a 48MHz ARM M0 for $12 would be nice. I wanted a reputable supplier and good Arduino support. I bought 2 ATSAMD21 modules from Adafruit, the ItsyBitsy M0 Express ($12) and the Feather M0 Express. These have 12b ADCs and specify INL of about 1.4LSB typical, 4.5LSB max. Noise is 1mV RMS. Not too bad for an on-chip ADC.

I tested the ADCs with my precision 18b DAC. I wrote a function to calculate the RMS noise (using standard deviation) of an array of readings. I measured the noise with different averaging values from 1 to 1000.

The ADC is set to 12 bits using analogReadResolution(12). Conversions in an averaging loop take about 28uS at 12b using analogRead(). Averaging 100 samples takes 2.8mS.

Peak-to-peak noise is about 5x the RMS values. So if you don't want your LSBs to bobble, more averaging.

Averages Noise RMS, mV
ENOB
(bits)
1
3.0 mV
10
10
1.0 mV
11.5
100
0.4 mV
12.5
1000
0.0015 mV
13

Got anything better??

For basic measurements, 10bits is decent, and 12b is better, if you can average. If you need better than 12 bits, the ADS1115 is a nice 4 channel, 16 bit part for $5. Cheap eval boards are available. It can do ADCs from +/- .256V to +/- 2.5V, single -ended (4 channels) or differential (3 channels. I2C, supports up to 4 devices on I2C., and in a VSSOP-10 package. Convert times are 8 to 860 SPS. Good for DC and low frequency signals.

I2C is pretty easy to isolate using  parts like the Siliconix SI860x or TI ISO154x.
If yo need faster ADCs. there are sampling SPI ADCs at 12 to 16 bits.


Of course there are plenty of 20 and 24 bit ADCs.


Back to Dave's Home Page

This page was last updated 3/3/2024