Share:

Using UTFS in an Embedded System

In this article we are going to look at how to use the UTFS file system on a few microcontrollers. This should give developers a good starting point for integration with new or existing systems.
Example code can be found on the github repository under the Examples/ folder: https://github.com/clisystems/utfs/tree/main/Examples
NOTE: This is an overview of the UTFS system, and all examples are for highlighting the UTFS features. The source code provided is under the MIT license.

Overview

The UTFS file system is a lightweight storage system designed for use in embedded systems. UTFS is designed for flat address space, sequential memories such as EEPROM or Flash, and is based on concepts from the TAR archive format where data is written sequentially without gaps.

Structure

UTFS provides an interface and driver layer between the application data, and the low level read/write functions for the storage medium.
Because the storage medium is dependent on the hardware in the system, the low level read/write functions are outside of the scope of UTFS

 
 

Example: SAMD20 with CPU flash page storage


The ATSAMD20 microcontoller from Microchip is a 32-bit ARM Cortex-M0+. The example in the github SAMD20 folder is for the SAMD20J17 part, with with 128KB Flash and 16KB RAM. We will use the last 1KB of the flash as a datastore for UTFS.

Storage Medium

The SAMD20 microcotroller flash is divided in to Pages of 64bytes and Rows of 4 Pages = 256bytes. The smallest erasable unit is the Row of 256bytes.
The lib/datastore.c source file provides functions to read and write ‘Blocks’ of 256bytes. 4 blocks are defined in the header, and are the last 4 blocks of the CPU flash, providing 1024bytes (1KB) of storage.

read/write functions

The main.c file provides the sys_read() and sys_write() files as required by UTFS.
The sys_write() function verifies the address is within the bounds of the storage medium, loads the block if needed, updates the data block, and writes the data block back to the data store.
The sys_read() function verifies the address is within the bounds of the storage medium, loads the block if needed, and extracts the selected section of the block based on the address.

UTFS setup

The system is setup by first initializing the lowest level of hardware access, the sys_init() function for hardware configuration and the datastore_init() function to setup the storage medium access.
After the hardware has been initialized, the UTFS system is initialized with a call to utfs_init(). Now the UTFS system is ready to use.

// Default 'driver' init function
system_init();

// Local project inits
sys_init();
datastore_init();
utfs_init(true);

File setup

In the SAMD20 example, an appdata_t data structure is defined with LED period_ms and settings_flags variables. A single instance of this structure is created as ‘appdata’.
Also, an instance of a utfs_file_t is created ‘appfile’.

typedef struct{
    uint16_t led_time_period_ms;
    uint16_t settings_flags;
}app_data_t;

app_data_t appdata;

utfs_file_t appfile;
After the UTFS system is initialized, the appfile is configured to point to the appdata structure and use the file name ‘appdata’; this is done with the helper function utfs_set(...).
// Configure UTFS files
utfs_set(&appfile,"appdata",&appdata,sizeof(appdata));
utfs_register(&appfile, UTFS_NOFLAGS, UTFS_NOOPT);

Loading and checking result

Finally, the UTFS system is loaded with utfs_load() which attempts to load the data from the storage medium.

ures = utfs_load();
printf("UTFS result: %s\n",utfs_result_str(ures));

// Handle invalid data
if(appfile.signature!=0xABCD){
    printf("Defaulting appdata\n");
    memset(&appdata,0,sizeof(appdata));
    appfile.signature=0xABCD;
    appdata.led_time_period_ms = 100;
    appdata.settings_flags=0;
}

The results of the utfs_load can be checked in a few different ways:
  • First the result of the utfs_load() call can be used to determine if the file system on the storage medium was valid or incorrect.
  • Second, the appfile member ‘size_loaded’ can be used to compare the number of bytes read from UTFS against the size of the data requested. If the size_loaded is 0, then the file was not found on the medium
  • This example uses a third method, the file signature variable. The ‘signature’ variable within the UTFS file type is expected to be 0xABCD, if the variable is not that value, the data is incorrect and default values are written to the structure and the signature in the file is updated.

Updating and saving the data

In the SAMD20 example, a simple serial based terminal interface is provided to read in character bytes over the SERCOM3 UART interface. These commands are parsed in to strings on the newline (\n) character and actions are performed based on the strings.
The ‘time’ command allows for setting the LED blinking period in milliseconds.
The ‘save’ and ‘load’ commands call the utfs_save() and utfs_load() functions.

Conclusion

The SAMD20 example shows how to use part of the CPU code flash as a datastore and use UTFS file system to store application data persistently.
The data writen to the CPU flash in this example is: (24 byte appfile header) + (4 byte appdata data) = 28 bytes, leaving 996 bytes of the 1K datastore available.

 
 

Example: Arduino Uno EEPROM storage


The Arduino Uno Rev 3 uses a ATmega328 micrcontroller from Microchip (Atmel) which includes a 1024 byte non-volatile EEPROM. In this example, we will use the EEPROM for UTFS file system

Storage Medium

The ATmega328 EEPROM is accessible via the EEPROM.read() and EEPROM.write() functions, and can read and write at the single byte level.

#include <EEPROM.h>

read/write functions

The .ino file provides the sys_read() and sys_write() files as required by UTFS, and reads and writes bytes on the EEPROM

// UTFS required functions sys_write and sys_read
// Write data to flash, return number of written bytes
uint32_t sys_write(uint32_t address, void * ptr, uint32_t length)
{
    uint16_t x;
    uint8_t * data;
    data = (uint8_t*)ptr;
    if(!ptr) return 0;
    // Arduino UNO has 1024 bytes of EEPROM
    if(address>1023) return 0;
    if(address+length>1023) length=1023-address;
    for(x=0;x<length;x++)
    {
        if(ptr) EEPROM.write(address+x,data[x]);
    }
    return length;
}

// Read data from flash, return number of read bytes
uint32_t sys_read(uint32_t address, void * ptr, uint32_t length)
{
    uint16_t x;
    uint8_t * data;
    data = (uint8_t*)ptr;
    if(!ptr) return 0;
    // Arduino UNO has 1024 bytes of EEPROM
    if(address>1023) return 0;
    if(address+length>1023) length=1023-address;
    for(x=0;x<length;x++)
    {
        if(ptr) data[x] = EEPROM.read(address+x);
    }
    return length;
}

UTFS setup

in the Arduino setup() function, the UTFS system is initialized with a call to utfs_init(). Now the UTFS system is ready to use.

utfs_init(true);

File setup

In this example two structures, system_data and applicaiton_data, created. system_data has a single instance named 'sysdata', and application_data has a single instance named 'appdata'.
Also, two instances of utfs_file_t are created ‘sysfile’ and ‘appfile’.

// Data structures used in the system. This is the data
// that will be written to the UTFS files.
struct system_data{
    char serialnumber[12];
    char modelnumber[12];
};

struct application_data{
    uint8_t led_speed;
};

struct system_data sysdata;
struct application_data appdata;

utfs_file_t sysfile;
utfs_file_t appfile;
After the UTFS system is initialized, the appfile is configured to point to the appdata structure and use the file name ‘appdata’, and appfile is configured to point to the sysdata structure and use the file name ‘system’.
// Register the UTFS files
sprintf(sysfile.filename,"system");
sysfile.data = &(sysdata);
sysfile.size = sizeof(sysdata);
utfs_register(&sysfile, UTFS_NOFLAGS, UTFS_NOOPT);

sprintf(appfile.filename,"appdata");
appfile.data = &(appdata);
appfile.size = sizeof(appdata);
utfs_register(&appfile, UTFS_NOFLAGS, UTFS_NOOPT);

Loading and checking result

Finally, the UTFS system is loaded with utfs_load() which attempts to load the data from the storage medium.

res = utfs_load();

// Example: How to handle RES_INVALID_FS on first run
#if 0
if(res==RES_INVALID_FS){
    // default data
    // save?
}
#endif

// Example: Use the 'signature' variable in the file

// Check the signature after the data is loaded to see if
// they match an expected value. If they do not match, then the
// data is not correct and set the default values
if(sysfile.signature != 0xA1){
    printf("Default sysdata\n");
    memset(&sysdata,0,sizeof(sysdata));
    sysfile.signature=0xA1;
}
if(appfile.signature != 0xF2){

    if(appfile.signature==0xA2){
        // upgrade the data from an earlier structure!
    }else{
        printf("Default appdata\n");
        memset(&appdata,0,sizeof(appdata));
        appfile.signature=0xF2;
        appdata.led_speed = LED_SPEED_MED;
    }
}

The results of the utfs_load can be checked in a few different ways:
  • First the result of the utfs_load() call can be used to determine if the file system on the storage medium was valid or incorrect.
  • Second, the appfile member ‘size_loaded’ can be used to compare the number of bytes read from UTFS against the size of the data requested. If the size_loaded is 0, then the file was not found on the medium.
  • This example uses a third method, the file signature variable.
    • The ‘signature’ variable within the UTFS sysfile is expected to be 0xA1, if the variable is not that value, the data is incorrect and default values are written to the structure.
    • The ‘signature’ variable within the UTFS appfile structure is expected to be 0xF2. If the signature variable is 0xA2, then the data is in an older format and must be updated, the example of which is outside the scope of this article. If the signature is neither 0xF2 nor 0xA2, then the data is invalid and default values are written to the structure and the appfile signature is set.

Updating and saving the data

In this example, a simple serial based terminal interface is provided to read in character bytes over USB serial interface via serial.read() and print information using printf() which wraps calls to serial.write().
Input commands are parsed in to strings on the newline (\n) character and actions are performed based on the strings. The terminal_command() function parses command received by the system.
The ‘slow’, ‘medium’, ‘fast’, and ‘off’ commands set the appdata.led_speed variable which controls the speed of blinking the on board LED.
The ‘serial’ and ‘model’ commands allow setting the sysdata.serialnumber and sysdata.modelnumber variables.
The ‘save’ and ‘load’ commands call the utfs_save() and utfs_load() functions.

Conclusion

The Arduino example shows how to use the built in EEPROM of the ATmega328 as a datastore and use UTFS file system to store two different files persistently.
The data writen to the Arduino EEPROM in this example is: (24 byte sysfile header) + (24 byte sysdata data) + (24 bytes appfile header) + (4 byte appdata data) = 76 bytes, leaving 948 bytes of the EEPROM available.