ASF: SAMD20 ADC fine tuning

While working with the SAMD20 I noticed my ADC measurements were pretty noisy. In fact I was measuring thermistors and my end result was jumping around 1 or 2 degrees when I expected ~0.1C resolution at room temperature in that section of the thermistor curve.

Software Averaging (which didn’t work)

I started off initially by calling adc_read_buffer_job with a value of 1 for 1 sample only before it interrupts.

uint16_t adc_buffer[1];
adc_read_buffer_job(&adc_instance, adc_buffer, 1);

The immediate thing I did to “fix” the noise was to set the number of samples to 4 and then averaging the result in the interrupt.

uint16_t adc_buffer[NUMBER_SAMPLES];

adc_read_buffer_job(&adc_instance, adc_buffer, NUMBER_SAMPLES);

void adc_complete_callback(const struct adc_module *const module)
	uint16_t avg = 0;
	for(size_t i = 0; i < NUMBER_SAMPLES; i++)
		avg += adc_buffer[i];

This however only slightly improved the problem, I would receive results such as
in the adc_buffer which could average out to an stable value but it differed between channels too much, which is especially a problem when the channels had identical input voltages.

Increasing the number of buffered samples had no effect. Even with 16 samples it more or less disturbances in the result.

Hardware Averaging

The SAMD20 has the ability to average in hardware, this makes life easier as you don’t have take multiple samples via software and average. It can also be more accurate since your micro can take the samples in a continuous time span rather than in software interrupting, storing the result and then deciding on what to do (i.e. continue or run callback).

struct adc_config config;

config.resolution         = ADC_RESOLUTION_CUSTOM;
config_adc.divide_result = ADC_DIVIDE_RESULT_16;
config_adc.accumulate_samples = ADC_ACCUMULATE_SAMPLES_16;

To enable averaging you must adjust the adc_config struct. With the way Atmel setup the driver. adc_resolution primarily configures the hardware averaging functionality because it is how you extend resolution as well. Atmel predefined the divide and accumulation settings for different resolutions in the driver. This is why you need to set it to custom to override those settings.

Now after the resolution is set, we are able to change divide_result and accumulate_samples to anything that is supported by the chip.

Such as accumulate 16 and divide by 16 setting which will still give 12 bits.

This almost entirely eliminated my measurement problems by making the chip average in hardware instead of software. I can not explain it entirely but it works great

External vs. Internal Ground

The ADC configuration struct defaults to using the internal device ground as the negative reference. There is also an option to use external ground from the pins as the negative reference.
I have found through testing that using the internal ground results in an 10mV offset while my firmware is running. The offset is applied uniformly to all my channels.

The SAMD20 datasheet in Table 32-18 and Table 32-19 specs a -5.0mV to 5.0mV offset error for the ADC.

This is generally not a problem but the extra 5mV was curious. But the solution was rather easy. I switched to the IO gnd which reduced the ADC reading’s offset down to an acceptable 2-5mV range.

This is done by simply doing:

config_adc.negative_input = ADC_NEGATIVE_INPUT_IOGND;

In theory the offset of the internal device ground depends on the current the chip is consuming. If the SAMD20 is doing measurements in sleep, this may be an non-issue.

Switching to the IO/PAD/PIN GND also severely helps XMEGAE microcontrollers with their ADC readings as well. This is how I thought to change the ground selection. They have similar ADC peripherals.