Virtual filesystem component

Overview

Virtual filesystem (VFS) component provides a unified interface for drivers which can perform operations on file-like objects. This can be a real filesystems (FAT, SPIFFS, etc.), or device drivers which exposes file-like interface.

This component allows C library functions, such as fopen and fprintf, to work with FS drivers. At high level, each FS driver is associated with some path prefix. When one of C library functions needs to open a file, VFS component searches for the FS driver associated with the file’s path, and forwards the call to that driver. VFS also forwards read, write, and other calls for the given file to the same FS driver.

For example, one can register a FAT filesystem driver with /fat prefix, and call fopen("/fat/file.txt", "w"). VFS component will then call open function of FAT driver and pass /file.txt argument to it (and appropriate mode flags). All subsequent calls to C library functions for the returned FILE* stream will also be forwarded to the FAT driver.

FS registration

To register an FS driver, application needs to define in instance of esp_vfs_t structure and populate it with function pointers to FS APIs:

esp_vfs_t myfs = {
    .fd_offset = 0,
    .flags = ESP_VFS_FLAG_DEFAULT,
    .write = &myfs_write,
    .open = &myfs_open,
    .fstat = &myfs_fstat,
    .close = &myfs_close,
    .read = &myfs_read,
};

ESP_ERROR_CHECK(esp_vfs_register("/data", &myfs, NULL));

Depending on the way FS driver declares its APIs, either read, write, etc., or read_p, write_p, etc. should be used.

Case 1: API functions are declared without an extra context pointer (FS driver is a singleton):

size_t myfs_write(int fd, const void * data, size_t size);

// In definition of esp_vfs_t:
    .flags = ESP_VFS_FLAG_DEFAULT,
    .write = &myfs_write,
// ... other members initialized

// When registering FS, context pointer (third argument) is NULL:
ESP_ERROR_CHECK(esp_vfs_register("/data", &myfs, NULL));

Case 2: API functions are declared with an extra context pointer (FS driver supports multiple instances):

size_t myfs_write(myfs_t* fs, int fd, const void * data, size_t size);

// In definition of esp_vfs_t:
    .flags = ESP_VFS_FLAG_CONTEXT_PTR,
    .write_p = &myfs_write,
// ... other members initialized

// When registering FS, pass the FS context pointer into the third argument
// (hypothetical myfs_mount function is used for illustrative purposes)
myfs_t* myfs_inst1 = myfs_mount(partition1->offset, partition1->size);
ESP_ERROR_CHECK(esp_vfs_register("/data1", &myfs, myfs_inst1));

// Can register another instance:
myfs_t* myfs_inst2 = myfs_mount(partition2->offset, partition2->size);
ESP_ERROR_CHECK(esp_vfs_register("/data2", &myfs, myfs_inst2));

Paths

Each registered FS has a path prefix associated with it. This prefix may be considered a “mount point” of this partition.

Registering mount points which have another mount point as a prefix is not supported and results in undefined behavior. For instance, the following is correct and supported:

  • FS 1 on /data/fs1
  • FS 2 on /data/fs2

This will not work as expected:

  • FS 1 on /data
  • FS 2 on /data/fs2

When opening files, FS driver will only be given relative path to files. For example:

  • myfs driver is registered with /data as path prefix
  • and application calls fopen("/data/config.json", ...)
  • then VFS component will call myfs_open("/config.json", ...).
  • myfs driver will open /config.json file

VFS doesn’t impose a limit on total file path length, but it does limit FS path prefix to ESP_VFS_PATH_MAX characters. Individual FS drivers may have their own filename length limitations.

File descriptors

It is suggested that filesystem drivers should use small positive integers as file descriptors. VFS component assumes that CONFIG_MAX_FD_BITS bits (12 by default) are sufficient to represent a file descriptor.

If filesystem is configured with an option to offset all file descriptors by a constant value, such value should be passed to fd_offset field of esp_vfs_t structure. VFS component will then remove this offset when working with FDs of that specific FS, bringing them into the range of small positive integers.

While file descriptors returned by VFS component to newlib library are rarely seen by the application, the following details may be useful for debugging purposes. File descriptors returned by VFS component are composed of two parts: FS driver ID, and the actual file descriptor. Because newlib stores file descriptors as 16-bit integers, VFS component is also limited by 16 bits to store both parts.

Lower CONFIG_MAX_FD_BITS bits are used to store zero-based file descriptor. If FS driver has a non-zero fd_offset field, this fd_offset is subtracted FDs obtained from the FS open call, and the result is stored in the lower bits of the FD. Higher bits are used to save the index of FS in the internal table of registered filesystems.

When VFS component receives a call from newlib which has a file descriptor, this file descriptor is translated back to the FS-specific file descriptor. First, higher bits of FD are used to identify the FS. Then fd_offset field of the FS is added to the lower CONFIG_MAX_FD_BITS bits of the fd, and resulting FD is passed to the FS driver.

   FD as seen by newlib                                    FD as seen by FS driver
                                              +-----+
+-------+---------------+                     |     |    +------------------------+
| FS id | Zero—based FD |     +---------------> sum +---->                        |
+---+---+------+--------+     |               |     |    +------------------------+
    |          |              |               +--^--+
    |          +--------------+                  |
    |                                            |
    |       +-------------+                      |
    |       | Table of    |                      |
    |       | registered  |                      |
    |       | filesystems |                      |
    |       +-------------+    +-------------+   |
    +------->  entry      +----> esp_vfs_t   |   |
    index   +-------------+    | structure   |   |
            |             |    |             |   |
            |             |    | + fd_offset +---+
            +-------------+    |             |
                               +-------------+

Standard IO streams (stdin, stdout, stderr)

If “UART for console output” menuconfig option is not set to “None”, then stdin, stdout, and stderr are configured to read from, and write to, a UART. It is possible to use UART0 or UART1 for standard IO. By default, UART0 is used, with 115200 baud rate, TX pin is GPIO1 and RX pin is GPIO3. These parameters can be changed in menuconfig.

Writing to stdout or stderr will send characters to the UART transmit FIFO. Reading from stdin will retrieve characters from the UART receive FIFO.

Note that while writing to stdout or stderr will block until all characters are put into the FIFO, reading from stdin is non-blocking. The function which reads from UART will get all the characters present in the FIFO (if any), and return. I.e. doing fscanf("%d\n", &var); may not have desired results. This is a temporary limitation which will be removed once fcntl is added to the VFS interface.

Standard streams and FreeRTOS tasks

FILE objects for stdin, stdout, and stderr are shared between all FreeRTOS tasks, but the pointers to these objects are are stored in per-task struct _reent. The following code:

fprintf(stderr, "42\n");

actually is translated to to this (by the preprocessor):

fprintf(__getreent()->_stderr, “42n”);

where the __getreent() function returns a per-task pointer to struct _reent (newlib/include/sys/reent.h#L370-L417>). This structure is allocated on the TCB of each task. When a task is initialized, _stdin, _stdout and _stderr members of struct _reent are set to the values of _stdin, _stdout and _stderr of _GLOBAL_REENT (i.e. the structure which is used before FreeRTOS is started).

Such a design has the following consequences:

  • It is possible to set stdin, stdout, and stderr for any given task without affecting other tasks, e.g. by doing stdin = fopen("/dev/uart/1", "r").
  • Closing default stdin, stdout, or stderr using fclose will close the FILE stream object — this will affect all other tasks.
  • To change the default stdin, stdout, stderr streams for new tasks, modify _GLOBAL_REENT->_stdin (_stdout, _stderr) before creating the task.

Application Example

Instructions

API Reference

Macros

ESP_VFS_PATH_MAX 15

Maximum length of path prefix (not including zero terminator)

ESP_VFS_FLAG_DEFAULT 0

Default value of flags member in esp_vfs_t structure.

ESP_VFS_FLAG_CONTEXT_PTR 1

Flag which indicates that FS needs extra context pointer in syscalls.

Structures

struct esp_vfs_t

VFS definition structure.

This structure should be filled with pointers to corresponding FS driver functions.

If the FS implementation has an option to use certain offset for all file descriptors, this value should be passed into fd_offset field. Otherwise VFS component will translate all FDs to start at zero offset.

Some FS implementations expect some state (e.g. pointer to some structure) to be passed in as a first argument. For these implementations, populate the members of this structure which have _p suffix, set flags member to ESP_VFS_FLAG_CONTEXT_PTR and provide the context pointer to esp_vfs_register function. If the implementation doesn’t use this extra argument, populate the members without _p suffix and set flags member to ESP_VFS_FLAG_DEFAULT.

If the FS driver doesn’t provide some of the functions, set corresponding members to NULL.

Public Members

int fd_offset

file descriptor offset, determined by the FS driver

int flags

ESP_VFS_FLAG_CONTEXT_PTR or ESP_VFS_FLAG_DEFAULT

Functions

esp_err_t esp_vfs_register(const char *base_path, const esp_vfs_t *vfs, void *ctx)

Register a virtual filesystem for given path prefix.

Return
ESP_OK if successful, ESP_ERR_NO_MEM if too many VFSes are registered.
Parameters
  • base_path: file path prefix associated with the filesystem. Must be a zero-terminated C string, up to ESP_VFS_PATH_MAX characters long, and at least 2 characters long. Name must start with a “/” and must not end with “/”. For example, “/data” or “/dev/spi” are valid. These VFSes would then be called to handle file paths such as “/data/myfile.txt” or “/dev/spi/0”.
  • vfs: Pointer to esp_vfs_t, a structure which maps syscalls to the filesystem driver functions. VFS component doesn’t assume ownership of this pointer.
  • ctx: If vfs->flags has ESP_VFS_FLAG_CONTEXT_PTR set, a pointer which should be passed to VFS functions. Otherwise, NULL.

esp_err_t esp_vfs_unregister(const char *base_path)

Unregister a virtual filesystem for given path prefix

Return
ESP_OK if successful, ESP_ERR_INVALID_STATE if VFS for given prefix hasn’t been registered
Parameters
  • base_path: file prefix previously used in esp_vfs_register call

ssize_t esp_vfs_write(struct _reent *r, int fd, const void *data, size_t size)

These functions are to be used in newlib syscall table. They will be called by newlib when it needs to use any of the syscalls.

off_t esp_vfs_lseek(struct _reent *r, int fd, off_t size, int mode)
ssize_t esp_vfs_read(struct _reent *r, int fd, void *dst, size_t size)
int esp_vfs_open(struct _reent *r, const char *path, int flags, int mode)
int esp_vfs_close(struct _reent *r, int fd)
int esp_vfs_fstat(struct _reent *r, int fd, struct stat *st)
int esp_vfs_stat(struct _reent *r, const char *path, struct stat *st)
int esp_vfs_link(struct _reent *r, const char *n1, const char *n2)
int esp_vfs_unlink(struct _reent *r, const char *path)
int esp_vfs_rename(struct _reent *r, const char *src, const char *dst)
void esp_vfs_dev_uart_register()

add /dev/uart virtual filesystem driver

This function is called from startup code to enable serial output