Zephyr has a filesystem API that makes it pretty easy to read and write to device flash. We’ve found this useful when testing out certificate authentication. The Golioth Firmware SDK shows how to write credentials onto the filesystem via SMP, useful since credentials are different for each device. But what if you want to write the same files to all devices during production?
Whether it’s machine learning models or simple configuration data, you can preload a filesystem and flash it as part of your production process. Today we’ll cover generating, flashing, reading, and extracting LittleFS images using Zephyr. However, the process will be nearly identical for other RTOSes.
Why not just make it part of the firmware?
Why are we even doing this, can’t these files just be built into the firmware? The problem with hardcoding data like this is that you need to update the whole firmware image to change them.
Take the case of machine learning models. These models are commonly trained on specialized hardware, and only once the model has been refined is it presented to the constrained device (your microcontroller-based product). If a more performant model is available later on, you want to be able to update just the model. Firmware updates consume bandwidth (which for cellular has a cost) and battery budget. Golioth specializes in OTA updates of not just firmware, but assets like ML models. So you can send the new model as an OTA asset which gets written to the filesystem.
Separating this data into a filesystem makes a lot of sense. You can update the firmware without sending the machine learning model with it. The device is also capable of updating what’s on the filesystem without touching the firmware its currently running. So let’s take care of the chicken and egg issue by flashing a populated filesystem as devices roll off the line.
Running the Zephyr LFS Sample
Zephy has a built-in support for LFS, the “little fail-safe filesystem designed for microcontrollers”. The RTOS reserves a partition for the filesystem, and at first run it will format the partition if unable to a read valid filesystem configuration. We can build the Zephyr LFS sample to see the formatting happen, and the sample will also populate some files in the filesystem afterward.
I’m building for the NXP frdm_rw612
board. By default, this sample will build a filesystem that is ~57MB, so I’m going to use a devicetree overlay file to create a more manageable ~24k filesystem.
/delete-node/ &storage_partition; / { fstab { compatible = "zephyr,fstab"; lfs1: lfs1 { compatible = "zephyr,fstab,littlefs"; mount-point = "/lfs1"; partition = <&lfs1_part>; automount; read-size = <16>; prog-size = <16>; cache-size = <64>; lookahead-size = <32>; block-cycles = <512>; }; }; }; &w25q512jvfiq { partitions { compatible = "fixed-partitions"; #address-cells = <1>; #size-cells = <1>; lfs1_part: partition@623000 { label = "storage"; reg = <0x623000 0x6000>; }; }; };
Now let’s build and flash the application:
west build -p -b frdm_rw612 west flash
You should see the application format the filesystem, mount it, then write a file to it.
I: littlefs partition at /lfs1 I: LittleFS version 2.10, disk version 2.1 I: FS at w25q512jvfiq@0:0x623000 is 6 0x1000-byte blocks with 512 cycle I: partition sizes: rd 16 ; pr 16 ; ca 64 ; la 32 E: WEST_TOPDIR/modules/fs/littlefs/lfs.c:1389: Corrupted dir pair at {0x0, 0x1} W: can't mount (LFS -84); formatting I: /lfs1 mounted I: Automount /lfs1 succeeded *** Booting Zephyr OS build v4.1.0 *** Sample program to r/w files on littlefs Area 0 at 0x623000 on w25q512jvfiq@0 for 24576 bytes /lfs1 automounted /lfs1: bsize = 16 ; frsize = 4096 ; blocks = 6 ; bfree = 4 Listing dir /lfs1 ... /lfs1/boot_count read count:0 (bytes: 0) /lfs1/boot_count write new boot count 1: [wr:1] I: Test file: /lfs1/pattern.bin not found, create one! ------ FILE: /lfs1/pattern.bin ------ 01 55 55 55 55 55 55 55 02 55 55 55 55 55 55 55 03 55 55 55 55 55 55 55 04 55 55 55 55 55 55 55 --- snip --- 43 55 55 55 55 55 55 55 44 55 55 55 55 55 55 55 45 55 aa I: /lfs unmounted /lfs unmount: 0
Reading and Analyzing an LFS Partition
Now that we have a filesystem on the device, let’s read it from flash and analyze the contents. I’m using a J-Link to read the memory. For this particular chip, flash is locate at an offset of 0x8000000. Combine this with the partition address (0x623000) and size (0x6000) from our devicetree overlay file and we can read the LFS partition to a file.
JLinkExe -Device RW612 -if SWD -Speed 4000 connect SaveBin lfs.bin 0x8623000 0x6000
Now we have an lfs.bin file. There’s a handy tool called littlefs-python that we can use to view the files on the filesystem. Let’s install it and list the files. If you go back and look at the output of the sample program you’ll notice the filesystem is reported 6 blocks of size 0x1000 (4096 bytes) which we use when viewing the binary.
pip install littlefs-python littlefs-python list lfs.bin --block-size 4096 /boot_count /pattern.bin
Voila! We can see the pattern.bin file as well as a boot count file, both written by the sample application. If you want access to these files, you can extract them using the same tool.
littlefs-python extract --block-size 4096 lfs.bin output/
Creating an LFS Binary
Now for the main event. Let’s create our own filesystem binary and write it to the device. I’m going to start by setting up the desired filesystem on my computer. I’m going to do this with empty files but of course any files may be used here.
mkdir my-new-fs touch my-new-fs/hello-there.txt mkdir my-new-fs/models touch my-new-fs/models/yolo.bin
My filesystem now looks like this:
my-new-fs/ ├── hello-there.txt └── models └── yolo.bin
Now use the python tool to convert this to a binary. Make sure the block count matches the filesize you set up in Zephyr.
littlefs-python create my-new-fs/ lfs-new.bin --block-size 4096 --block-count 6
And then list the contents to make sure we have what we want:
littlefs-python list lfs-new.bin --block-size 4096 /models /hello-there.txt /models/yolo.bin
Now writing it back onto the device is very similar to the read step earlier in this post. Once again, the start address (0x623000) comes from the partition in build/zephyr/zephyr.dts which was set by the overlay file that I used. And this chip begins at a memory offset of 0x8000000.
JLinkExe -Device RW612 -if SWD -Speed 4000 connect LoadBin lfs-new.bin 0x8623000 reset
Our new filesystem is written to device flash and we rebooted the chip. However, it’s a bit difficult to tell so let’s use the filesystem shell so we can get a directory listing.
Using Zephyr’s Filesystem Shell
We can recompile and reflash our firmware to enable the shell. Remember, the filesystem is on its own partition, so this will not overwrite our data.
One change is required in the sample code. At the end of the application, the filesystem is unmounted. We want to leave it mounted so we can list the files. Before rebuilding, comment out these two lines at the end of main.c:
/*rc = fs_unmount(mountpoint);*/ /*LOG_PRINTK("%s unmount: %d\n", mountpoint->mnt_point, rc);*/
Now rebuild with additional Kconfig symbols to turn on the shell and the filesystem shell and flash the new firmware. These Kconfig can also go into your prj.conf if you want them to be permanently part of your project.
west build -p -b frdm_rw612 -- -DCONFIG_SHELL=y -DCONFIG_FILE_SYSTEM_SHELL=y west flash
We can now use shell commands to list files on the filesystem, confirming our generated binary is present on the device.
uart:~$ fs ls lfs1/ uart:~$ fs ls lfs1 boot_count hello-there.txt models/ pattern.bin uart:~$ fs ls lfs1/models yolo.bin
What will you use it for?
These are some nice filesystem tricks that may come in handy during a manufacturing run. While we’ve used Zephyr’s SMP subsystem to read from and write to the filesystem, that requires the SMP to be built into your application. A programmer is already present on the production line, so writing a filesystem at the same time as the firmware makes sense.
If it’s the same filesystem for every device, the binary may be bundled along with the firmware. But there are use cases where each device would have different data. Such is the case where certificate authentication is used. Your PKI should have the device itself generate a key, and the certificate signing request (CSR) may be written to the filesystem then read/signed and the resulting public cert written back onto the device.
Whatever your use case, we’d love to hear about why you need to read/write the filesystem. Let us know by posting to the Golioth forum!
No comments yet! Start the discussion at forum.golioth.io