Tag Archives: asf

Using SAMD Emulated EEPROM ASF driver coming from the 8-bit micro world

Introduction

In a typical 8-bit microcontroller application with real EEPROM, I used to have to read EEPROM byte by byte to obtain values I want and store them into variables in the world of PIC16/18s (how awful they are). Or in the case of AVRs, I had the nice helper functions provided by <avr/eeprom.h>

Now the SAMD with it’s emulated EEPROM ASF driver, things suddenly get more interesting. Now when you read the “EEPROM”, you get 60 bytes back. Wow, all that in one read and now I have to step through bytes to create my variables?

Hell no, now I can use the power of structs, memcpy and magic!

My new usage of emulated EEPROM is to store structs directly to/from the emulated EEPROM pages. The only risk is if we create a struct greater than the page size but there’s an fix for that from the Linux kernel 😀

Implementation

Structs themselves define a physically grouped set of data, so all the bytes we want to store are roughly next to each other. (see Struct Padding below)
Now to start, say this is my EEPROM variable storage struct

struct settings
{
	uint8_t device_name[10];
	uint16_t serial_number;
	uint16_t voltage_output;
	uint16_t date_of_manufacture;
}

How do we store this struct into the emulated EEPROM?
Simple!

uint8_t page_data[EEPROM_PAGE_SIZE];
memcpy(page_data,&settings,sizeof(settings));

eeprom_emulator_write_page(0, page_data);
eeprom_emulator_commit_page_buffer();

This copies the settings struct as raw bytes into the page_data byte buffer and then saves it into page 0.

How do we read it back from emulated EEPROM?
Also simple!

	uint8_t page_data[EEPROM_PAGE_SIZE];
	eeprom_emulator_read_page(0, page_data);
	memcpy(&settings,page_data,sizeof(settings));

 Struct Padding

Because of compiler optimization, the variables will be padded to match the default size of the chip’s memory, i.e. 32-bits. Meaning there will be 2 “padding” bytes between device_name and serial_number to make 4 bytes. When your struct isn’t big and its being used internally for your firmware only then it isn’t a big deal to just leave.

Here’s an example of how it would turn out for the previous struct:

struct settings
{
	uint8_t device_name[10];  //10 bytes means 2 + 1/2 dword
	uint8_t padding1[2];        //1/2 dword
	uint16_t serial_number;    //16bit means 1/2 dword
	uint8_t padding2[2];         //1/2 dword
	uint16_t voltage_output;    //16bit means 1/2 dword
	uint8_t padding3[2];        //1/2 dword
	uint16_t date_of_manufacture;    //16bit means 1/2 dword
	uint8_t padding4[2];        //1/2 dword
}

However, when you start to exceed the 60 bytes limit you may want to force packing. Packing simply tells the compiler to smash all the bytes next to each other. The downside of this is optimization. The compiler will now have to recreate shorts, words, etc when you access them from the struct and they are split memory words. i.e. Memory word 1 and memory 2 might have the LSB and MSB of the serial_number struct define and the compiler must insert assembly to convert it to a single memory word 3 before usage. Without packing, it would already be stored as a single word.

So how do you pack?

 

struct settings
{
	uint8_t device_name[10];
	uint16_t serial_number;
	uint16_t voltage_output;
	uint16_t date_of_manufacture;
} __attribute__((__packed__));

Simply apply the gcc attribute informing it to do so like above.

I personally have not noticed any real performance deficiencies by using packed structs but its all application dependent on how often you access struct members, how the compiler packs struct members by chance (i.e. device_name gets split between words but voltage_output doesn’t), etc.

Struct Size Validation

This reads the page into the byte buffer and then we dump the byte buffer into the settings struct.
Why do I copy to a buffer instead of the struct directly? Because the driver will always try and return 60 bytes and there struct is not necessarily that big.

Now you may wonder, what happens if our struct is bigger than 60 bytes? Well, bad things will happen because memcpy is blind to that problem and other variables could be affected.

The worst case is maybe your struct is initially fine but then you later add onto it and cause it to grow past the page size unknowingly. It may then keep working or fail horribly in different conditions, the worse of which isn’t noticeable until some edge case. This is definitely something you do not want.

But there’s a solution!
The sizeof function is a operator that’ll normally compute the size of data types but it only works at compile-time. Sadly the pre-processor won’t be able to evaluate it and stop with a #error or #warning.
But,the Linux kernel has this useful macro in “include/linux/kernel.h”

#define BUILD_BUG_ON(condition) ((void)sizeof(char[1 - 2*!!(condition)]))

This beautiful macro is a bit of magic that will let us do a sizeof check that will stop compiling. Incidentally, the macro uses a sizeof check that causes the build error.

So the beautiful solution is to shove
BUILD_BUG_ON( sizeof(settings) > EEPROM_PAGE_SIZE); somewhere the compiler will come across, when the sizeof the settings struct is greater than EEPROM_PAGE_SIZE, the compiler will emit an unrelated error but will point to the line that BUILD_BUG_ON is on.

This is the protection to make this implementation safe to use in your firmware. You can then fix the error by trimming your struct by adjusting the data type sizes of variables or splitting it into a new struct. You can ideally store a different struct per page.

I used this trick because I had around 90 end user configurable settings, I easily split them off into four different structs that could be saved into four different pages. This reduced the work for me from having to explicitly assign data from EEPROM to RAM variables on start-up and doing the reverse when I needed to save them. Now I just blindly store and read the struct.

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.