Category Archives: SAMD20

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.

#define NUMBER_SAMPLES 4
#define RESULT_DIVIDE 2
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];
	}
	
	avg >>= RESULT_DIVIDE;
}

This however only slightly improved the problem, I would receive results such as
2802
2780
2802
2815
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;
adc_get_config_defaults(&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.
samd20-adc-offset

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.

ASF: SAMD20/SAMD21 External interrupts in standby and the side effect of debugging

The EIC driver in the ASF for the SAMD20 defaults to using GCLK_GENERATOR_0. Unforunately I overlooked this little configuration when I was trying to use EIC in standby. So when I debugged, I could get external interrupts in standby, if I didn’t debug, my interrupts would not fire at all (while my RTC was fine). Fixing this was simple.

Cause

The root cause of the problem is in standby, all clocks are turned off and only the GCLKs are that are configured to remain on still run. The debugger appears to not allow clocks to turn off while debugging and thus peripherals will work even with clock configuration issues. However without the debugger it will fail.

Also, the EIC needs a running clock in standby if you want edge detection based interrupts to occur or if set to logic level interrupt with filtering. Purely logic level interrupts do not require a clock.

Solution

So my solution was because I was running GCLK2 in standby for the RTC, I configured the EIC to use it. This brings two benefits

1. My signals are not time critical, the 32khz clock has no effect on my application’s performance

2. Less power consumption from slower clocking and less gclks

Power saving in my application was critical so every microamp I saved is great.

So the simplest place to “fix” the configuration is to go into conf/conf_extint.c and change the GCLK define to GCLK_GENERATOR_1 in case of using the RTC.

Otherwise you can either

  1. Enable the GCLK_GENERATOR_0 to run in standby
  2. Configure another GCLK that will run in standby by editing conf_clocks.h. You can consider running it from the 32khz clock if you don’t have response time requirements.

And tada, you get external interrupts in standby outside of debug.

Additional Note

In conf_clocks.h

Check to make sure your 8mhz clock or 32khz clock that you are using to run your gclk is also enabled to run in standby or else the gclk won’t run.

# define CONF_CLOCK_OSC8M_RUN_IN_STANDBY true
or
# define CONF_CLOCK_OSC32K_RUN_IN_STANDBY true
also make sure your OSC32K is enabled in the first place(that’s not default)
# define CONF_CLOCK_OSC32K_ENABLE true