Bristlemouth Mote Filesystem

Hello Bristlemouth community!
I am embarking on a journey to add a filesystem to the Bristlemouth mote for use in Dev Kit applications as well as new product initiatives!

Why does this matter?
Well, imagine a future where Bristlemouth devices are able to transfer files to one another over an FTP like service.
This would unlock the door for so many interesting applications:

  • Collecting coredumps from nodes
  • Sending configuration files directly to a node rather than individual parameters
  • Passing along audio data from a hydrophone to a high bandwidth telemetry modem

The possibilities are endless1

How could the mote support this?
The mote has an 8MB flash chip available for use.
Currently only about 30kB of it is being used for the mote configuration partitions :duck:.
This means there is about 99.6% of the flash chip is available!
Sounds like there is enough space to throw a file system partition on the external flash chip.
A point to mention, the external flash is not even used to house DFU images (if it was we could have another 1MB of internal flash to use on the mote’s Microcontroller :laughing:).

So what filesystem should I implement you ask?
Well, there are a few filesystems that exist out there that are open source and geared towards embedded system (small computer) applications:

  • uC-FS
    • Apart of the uC-OS real time operating system by Micrium, looks cool, but looks very specific to their operating system
  • Little-FS
    • Could almost be considered industry standard, something I have personally used in the past and have had a pretty solid experience with
  • spiffs
    • Widely used, is the main file system on the ESP32’s ecosystem (super popular hobby Microcontroller), has not been maintained in the last 8 years

All of these are great options and with enough effort (some more than others) could be integrated into bm_protocol.
But let me introduce you to another filesystem: STORfs.
This is a filesystem that I wrote when I first began my career about 5 years ago.
It is messy.
It is inefficient.
It is ugly.
But, it is mine and open source (and for some reason gaining a lot of traction on Github, it is up to 37 starts at the time of this post).
I want to make it better and hopefully make it a worthwhile filesystem for all your ocean needs!

In order to get there, there are a few things that need to happen in STORfs:

  • Restructure of files/organization, a major refactor of the code
  • Improved searching for files
    • Currently a tree-structure is used for directories and a linked list for files in the directory, I plan to switch to hashtables with an inode like structure
  • Journaling and improved caching
  • Tests
    • Performance tests
    • Unit tests

I will be keeping this thread updated with my journey along the way, I hope something like this excites you all as much as it does me :slight_smile:

4 Likes

Awesome! After writing my last post, I’ve spoken this work into the world, which means I’m now committed to actually following through. Since then, I’ve already made some initial strides.

First, let me lay out a roadmap for how I plan to accomplish the improvements to STORfs.

What are those improvements again?

The most significant improvement will be how directories and files are searched for and organized within the filesystem. This new approach will improve both the speed of file access and the efficiency of flash block utilization.

Here are the steps to make this a production-ready library:

  • Perform an extraction refactor of the codebase. This encapsulates major operations into their own files, making the code more maintainable and digestible while enabling easy unit testing of modularized components.

  • Add unit testing infrastructure. I’ve used Ceedling in past projects and really enjoyed its flexibility and ease of setup. Check out how I used it in my polyglot project on GitHub.

  • Implement the new file access scheme. This will include:

    1. Using a hash table for organizing directory contents
    2. Using an inode-like structure (which I’ll rename to snode for STORfs) to store file metadata
  • Add atomic operations to prevent filesystem corruption in the event of power loss. This could range from a simple write-sync-read-validate operation to a full journaling implementation.

  • Integrate the new file access scheme and atomic operations into the existing codebase. This will deliver the improvements needed for faster filesystem operations.

  • Test on actual hardware:

    1. x86/ARM Mac
    2. A Bristlemouth mote
    3. Other PCBs I’ve designed for personal projects, to gather additional data points

    Once implemented on these platforms, the goal is to benchmark performance. I’ve looked for de-facto filesystem benchmark tools but haven’t found a standard yet—I’ll do more research when the time comes.

  • Proper wear leveling implementation. The current approach isn’t true wear leveling and has some flaws (which I’ll detail in a future blog post).

  • Better hooks for thread safety. Combined with caching of open files, this will allow files to be accessed from multiple threads quickly and safely.

Bonus features (not required, but nice to have):

  • Implement garbage collection to help with defragmentation and reclaim space from deleted files.
  • Filesystem check and repair utilities in case of filesystem corruption.

Progress Update:

At the time of writing, the modularization (first step) has been completed, and I’ve begun work on the unit test framework.

For each of these steps, I plan to write another post diving into more detail about the work done!

2 Likes

In this post, I’m going to discuss how I’m tracking the work being done, as well as the extraction refactor from the aforementioned first phase.

Within the STORfs repo I have created some issues that will be used to track the work described in my previous post. Issues on Github are not necessarily problems (bugs) in the repository, but can be used to capture a general unit of work or pertinent discussion as well.These issues are mainly being used to keep me on track as well as capture any thoughts that might come up during the implementation of said issues.

When an issue is being worked on, a pull request will be created to associate the issue with the actual code being written. These pull requests will be merged into the develop branch with the full implementation for the phase.

Onto the first order of business: the extraction refactor. This extraction refactor involves identifying and removing cohesive sections of code from a large complex file, to smaller more cohesive files. This makes the code more digestible and maintainable. I like to think of it as breaking down the file into smaller units (this becomes very important when conceptualizing the second phase : Add unit testing infrastructure). In an ideal world, this refactor can all happen without having to structurally change any of the code (i.e., change the logic)—essentially by copy-and-pasting directly to the new files!

I decided to break down the single source file storfs.c into the following file structure:

src/
├─ core.c
├─ crc.c
├─ fgets.c
├─ fopen.c
├─ fputs.c
├─ mkdir.c
├─ mount.c
├─ rewind.c
├─ rm.c
├─ touch.c
├─ wear.c

The refactor breaks down the original file, storfs.c as so:

  • Core Elements
    • Helper functions
    • Root directory operations
  • CRC Calculating Logic
  • Wear Leveling
  • Filesystem operations
    • fopen
    • fgets
    • fputs
    • mount
    • mkdir
    • rewind
    • rm
    • touch

When performing the refactor I would constantly run the test program located at Examples/test in order to make sure the output was consistent with every function moved from storfs.c to one of the newly created files.
This ensured that the changes were not breaking.

The work done can be seen at the following pull request #4. Check it out for a more detailed look at the file structure and layout.

2 Likes

Hello and happy holidays!
It has been a few weeks since my last post, but rest assured, I have been busy (I have been focused on a lot of coding, but have been too lazy to write a new blog post).
The next path on this journey leads us to the: unit testing infrastructure.
As I previously mentioned, I decided to use the testing framework Ceedling.
Throughout this post, I will explain what unit testing is, the benefits of unit testing as well as how its implemented in STORfs.

What is unit testing?
The following quote from this article (which serves as a solid introduction to unit testing) explains the concept of unit testing:

A unit test is a block of code that verifies the accuracy of a smaller, isolated block of application code, typically a function or method. The unit test is designed to check that the block of code runs as expected, according to the developer’s theoretical logic behind it. The unit test is only capable of interacting with the block of code via inputs and captured asserted (true or false) output.

This testing allows precise coverage of a module (file/class) to provide greater confidence in the code being integrated into the project, also called the SUT or System Under Test.
When testing a module it is important to validate a few metrics:

  1. Functional output, does the unit of code perform as expected
  2. Code coverage
  3. Branching conditions

With an emphasis on functional output, which is a qualitative metric. Ensuring the behavior of the function actually performs up to the expectations of the developer is the main reason to test! But many developers fall in the trap of trying to chase the other two metrics.
Code coverage and branching conditions are easy to quantify as percentages:

  • Code coverage equates to the lines of code that have been tested
  • Branching conditions determines if all logic control statements (if, else if, else) has been exercised

if (x == 10 || y == 11) has 3 branching conditions that can make this control statement true or false:

  • If x is 10, this is true, y is not considered
  • If x is not 10 but y is 11, this is true
  • if x is not 10 and y is not 11, this is false

Usually the more branching conditionals a function has the more complex it is.
Reducing that function’s complexity makes the code easier to test. This can be done by extracting some of the inner logic into smaller functions (i.e. an extraction refactor).

How is unit testing performed?
Usually a framework such as Ceedling makes unit testing easier to enroll into a code base.
Testing is done to validate the output of a function in accordance to provided inputs.
As a simple example consider the following C function:

int sum(int x, int y) {
  return x + y;
}

A unit test for this function would look as so:

void test_sum(void) {
  assert(sum(2, 2) == 4);
  assert(sum(5, 10) != 200);
}

Code can also be mocked, stubbed or faked in a unit test (there is also spies and dummies but I believe keeping unit testing simple with the three aforementioned methods are the way to go).
Mocking involves setting expectations for a function to be invoked in the SUT, and returning a value from that function.
This validates the behavior of the SUT.
The following C example shows how a mock is performed:

int system_self_test(void) {
  if (check_sensors() == false) {
    return 1;
  }

  if (check_actuators() == false) {
    return 1;
  }

  return 0;
}

The SUT, system_self_test invokes two functions:

  • check_sensors
  • check_actuators

The unit test with mocked functions would look like the following:

void test_system_self_test(void) {
  mock_expect_and_return_check_sensors(true);
  mock_expect_and_return_check_actuators(false);
  assert(system_self_test() == 1);
}

The mock function calls tell the unit testing framework to expect a function to be invoked within the test and returns a value, true or false in this example.

Stubbing is similar to mocking but does not hold the functions invoked in the SUT to any expectations. This provides simply a canned response. So the SUT in the previous example could look as so:

void test_system_self_test(void) {
  stub_return_check_sensors(false);
  stub_return_check_actuators(false); // Don't care if this is called
  assert(system_self_test() == 1);
}

Because check_sensors returns false in the SUT, check_actuators will not be invoked, but the unit test will still pass because no expectation is passed onto check_actuators. If this were the previous mock example where check_actuators is held to an expectation of being invoked, the unit test would fail!

Fakes are a used to implement a light weight version of a function that is implemented in the SUT. This gives the developer tighter control over what the function might return and is generally useful for controlling state driven decisions. An example of of a fake for the example we have been working with might look something as so:

bool check_sensors(void) {
  return true;
}

bool check_actuators(void) {
  return false;
}

void test_system_self_test(void) {
  assert(system_self_test() == 1);
}

Here the values of the fake functions are static and it would be more simple to mock or stub. But, with more complex functions which are state driven or need to perform some logic on a set of data, fakes can be incredibly useful.

Why unit tests for STORfs?
The refactor I completed in my last post broke storfs.c into smaller modules.
This modularization is perfect for unit testing because:

  • Each file has a focused responsibility
  • Functions can be tested in isolation
  • Edge cases (corruption, full disk) are easier to simulate

How are unit tests implemented on STORfs?
As previously mentioned, the Ceedling framework is being used for unit testing this library.
I decided on Ceedling because:

  • It targets the C language
  • It is easy to setup
  • Testing API is not incredibly complex
  • There are awesome plugins
  • Not much is necessary when adding new files to the project
  • Originally, ceedling was targeted towards embedded systems (although now just used as a generic C unit testing framework)
  • Easy to fake flash operations

In order to setup Ceedling and ensure consistency between multiple developer machines, I created an Ubuntu based Docker container to host all of the packages/tooling necessary.
If further interested in the contents of the Dockerfile, it is located here.
I also create a Makefile used to create the docker image as well as to run the unit tests. This setup makes it pretty easy to run the unit tests. The only tool requirements for a develop to have installed for this setup are Docker and GNU Make. In order to run the unit tests only two commands are necessary to be evoked from the command line:

  • make init
  • make

make init only has to be invoked once to set up the container and whenever an update is made to the Dockerfile, then make is used following that to invoke the unit tests!

Within the test directory of the project, files prepended with fake_ will be used for fakes, files prepended with tests_ are used to test specific files in the src directory, mocks and stubs are auto-generated by Ceedling when running the tests!
For the work covered in this post, I have only created a fake_flash file which mimics data storage from the filesystem in the local machines RAM.

The following PR#6 implements the work described above!

Until next time!

1 Like

I have the first portion of Integrate the new file access scheme and atomic operations completed and ready to be merged in at the following PR #19. This stage will be broken up into two following posts:

  • snode file information
  • hash map directory information

Reason being, this amount of work was pretty substantial with a lot of testing and planning involved.

The following explains the implementation of the bitmap which is used to indicate which pages are available to write data to in the filesystem (recommended to look at the PR as this explanation is located there with better formatting):

STORfs Bitmap

The bitmap in the filesystem is used to indicate whether or not specificpages in persistent memory have been allocated or are free. Each bit is defined as follows:
image

Each page in the filesystem is represented by a bit in the bitmap. The bitmap is stored within the filesystem so that it may be retrieved every time the system is booted.

Storage

A reserved number of pages in the filesystem are used to store this bitmap.
This looks like the following:

image

  • Filesystem Root:
    • Reserved page for the filesystem that is used to form information about the root directory
  • Bitmap Pages:
    • Pages used to indicate which Filesystem Pages are free and which ones are allocated
  • Filesystem Pages:
    • Pages used for files and directories

The number of reserved pages for the bitmap and root page is calculated as follows:

image

  • np: Number of bitmap pages
    • Number of reserved pages for the bitmap
  • pc: Page count
    • Total number of pages in filesystem
  • ps: Page size
    • Size of each page in bytes

Page count is divided by 8 in order to get the number of bytes necessary to represent the number of pages, which is then ceiling divided by the size of an individual page to get the total number of pages needed for the bitmap.

Initialization

Upon creating the filesystem, the bitmap is created and stored as described above. The initial pages used for the bitmap and root page are marked as allocated during this time:

image

  • ba: Pages allocated during filesystem creation
  • np: Number of bitmap pages
  • pr: Root page (holds information for the root directory)

When the filesystem is mounted, the next free page is found and stored into a hint variable described in the following section.

Hint

The hint effectively amortizes the cost of repeated allocations to O(1) (that is big O notation, for further reading on big O notation take a look at the following link). The bitmap hint is constantly updated whenever new pages are discovered, allocated or freed. This is done in the following manner:

  • discovery
    • Finds the first page available in the bitmap and sets the hint to that page
  • allocation
    • Uses the hint to find the first page available,
      marks all requested pages (single or contiguous) as allocated
      and then updates the hint equal to the end of the allocated pages
  • free
    • The first requested page freed (if successful) is set to the new hint

The search uses a circular scan starting from the hint, wrapping around using modulo arithmetic:

image

Where s is the step size (1 for bit-level, 8 for byte-level processing) and p_c* is the total page count. This ensures all pages are examined regardless of where the hint starts. If the search wraps fully back to the starting position without finding a free page,
the filesystem is full.

When comparing the hint system to a naive search from the beginning of the bitmap, where k is the number of allocated pages before the first free page and n is the total page count:

Operations

Four operations are available in order to access the bitmap:

  • Get
    • Used to determine if a page is free or allocated.
  • Discover
    • Find the next available page(s)
  • Alloc
    • Mark the next available page(s) as used
  • Free
    • Mark the indicated page(s) as unused

These operations are used for access to individual and contiguous pages. Contiguous operations help aid in allocating or freeing pages for data on the filesystem, while single page operations are available for precise operations that do not require the contiguous functions.

Optimizations

When accessing contiguous pages either for discovery, allocation, or freeing, slight optimizations have been made in order to speed up the searching. The following captures that optimization:

Byte Level Processing

As the bitmap is iterated through to find contiguous pages,
the algorithm can skip or modify full bytes (8 pages at a time) when byte-aligned.
Given a byte Bj representing pages 8j through 8j+7, the byte can be skipped when:

image

This requires the following preconditions:

image

Where i is the current page and r is the remaining pages to process.
When both conditions are met, the scan advances by 8 pages per iteration rather than 1.

When the action is alloc or free, matching full bytes are also bulk-modified:

image