Skip to content

Filesystem Module

The filesystem module provides a unified, path-based storage abstraction for the ESP3D pendant firmware. It layers three tiers — a global path router, per-medium backend drivers, and platform-specific implementations — so every consumer (HTTP handlers, ESP commands, CNC UI screens, update service) works through a single, consistent POSIX-like API regardless of whether data lives on internal flash or an SD card.


  1. Architecture Overview
  2. Tier 1 — Global Router (ESP3DGlobalFileSystem)
  3. Tier 2 — Backend Drivers
  4. Tier 3 — Platform Implementations
  5. Path Conventions
  6. Feature Flags & Build Selection
  7. SD Card Configuration
  8. Access Control & Concurrency
  9. Shared SD (Optional)
  10. Data Flow Diagrams
  11. Global Instances
  12. API Reference
  13. Consumers
  14. Related Documentation

SD Backend (ESP3D_SD_CARD_FEATURE)

Flash Backend

Consumers

(compile-time selection)

(compile-time selection)

/fs/...

/sd/...

HTTP Service

Handlers

ESP Commands

720 / 740 / 780

CNC File Screens

grbl / fluidnc / grblhal

Update Service

Translation Service

globalFs

ESP3DGlobalFileSystem

(path router)

flashFs • ESP3DFlash

mount: /fs

partition: flashfs

esp_flash_fatFs.cpp

ESP3D_FATFS_FEATURE

esp_flash_littleFs.cpp

ESP3D_LITTLEFS_FEATURE

sd • ESP3DSd

mount: /sd

esp_sd_spi.cpp

SD_INTERFACE_TYPE == 0

esp_sd_sdio.cpp

SD_INTERFACE_TYPE == 1

The module follows a strict separation of concerns:

TierClassFileResponsibility
1ESP3DGlobalFileSystemesp3d_globalfs.h/.cppInspect path prefix, route to correct backend
2aESP3DFlashesp3d_flash.hFlash lifecycle, state, POSIX API
2bESP3DSdesp3d_sd.hSD lifecycle, state, watchdog, POSIX API
3a(FatFS impl)flash/esp_flash_fatFs.cppMount/unmount/ops via esp_vfs_fat + wear leveling
3a(LittleFS impl)flash/esp_flash_littleFs.cppMount/unmount/ops via esp_littlefs
3b(SPI impl)sd/esp_sd_spi.cppSD via sdspi + esp_vfs_fat
3b(SDIO impl)sd/esp_sd_sdio.cppSD via sdmmc + esp_vfs_fat

Tier 1 — Global Router (ESP3DGlobalFileSystem)

Section titled “Tier 1 — Global Router (ESP3DGlobalFileSystem)”

File: main/modules/filesystem/esp3d_globalfs.h / esp3d_globalfs.cpp
Global instance: globalFs

ESP3DGlobalFileSystem is a thin path-routing layer. It exposes the same POSIX-like API as the backend drivers but resolves each call to the correct backend by inspecting the path prefix.

/fs/...

/sd/...

/

other

path argument

Prefix?

flashFs

(strip /fs prefix)

sd

(strip /sd prefix)

virtual root

(in-memory dirent)

return NULL / false

getFSType(const char *path) implements this logic by comparing the path against rootDirsHeaders[]:

const char *rootDirsHeaders[] = { "/fs/", "/sd/" }; // ESP3D_FLASH_FS_HEADER, ESP3D_SD_FS_HEADER

When routing to a backend, the prefix is stripped — i.e. /fs/config.ini becomes /config.ini before being forwarded to flashFs.

The virtual root presents all mounted sub-filesystems as directory entries without touching real storage. The router holds a single static DIR _rootDir and dirent _rootEntry pair; it is not concurrency-safe for multiple simultaneous root opendir callers, which is acceptable because the firmware never lists the root from more than one task at a time.

/ (virtual root)
├── fs/ → flashFs (always present)
└── sd/ → sd (only when ESP3D_SD_CARD_FEATURE)

DIR entries are identified internally with unique marker constants so closedir / readdir / rewinddir know which backend to dispatch to:

ConstantValueBackend
GLOBAL_ROOT_DIR_ID8888virtual root
GLOBAL_FLASH_DIR_ID1111flashFs
GLOBAL_SD_DIR_ID2222sd

Both backend drivers share an identical public API surface; the difference lies in their lifecycle mechanics.

File: main/modules/filesystem/esp3d_flash.h
Global instance: flashFs
Mount point: /fs
Partition label: flashfs

begin() / mount() OK

accessFS()

releaseFS() (nesting = 0)

accessFS() nested

error

mount() retry

idle

busy

unknown

StateMeaning
idleMounted, no active accessor
busyOne or more callers inside accessFS() / releaseFS() block
unknownInitialization failed

_access_nesting is a uint16_t counter that permits nested accessFS() calls from the same subsystem. releaseFS() decrements it; the filesystem transitions back to idle only when the counter reaches zero.

MethodDescription
begin()Mounts and caches space info; idempotent
mount()Calls the compile-time-selected VFS backend
unmount()Calls onBeforeUnmount() then unregisters VFS
format()Unmounts, formats partition, remounts
accessFS()Increments _access_nesting; blocks concurrent unmount
releaseFS()Decrements _access_nesting
getSpaceInfo()Returns total/used/free bytes; caches result unless refreshStats=true

File: main/modules/filesystem/esp3d_sd.h
Global instance: sd
Mount point: /sd
Format: FAT (via esp_vfs_fat)

begin() - card not found

begin() + mount() OK

accessFS() + mount() success

accessFS()

releaseFS()

card removed / unmount()

critical error

not_present

idle

busy

unknown

StateMeaning
idleMounted, no active accessor
not_presentCard absent or unmounted
busyActive accessor holds the bus
unknownUnrecoverable error

ESP3DSd maintains an acquire timestamp (_acquireTimestamp). Any operation that holds a file open for an extended period — such as GCode streaming while a job is paused — calls refreshWatchdog() to re-arm the timer. If the watchdog expires without being refreshed the driver can force-release the SD to allow other subsystems access. This design is specifically accounted for in the GCode host streaming flow.

All state transitions — accessFS(), releaseFS(), watchdog resets, and shared-SD handoffs — are serialized by pthread_mutex_t _state_mutex. This makes ESP3DSd safe to call from different FreeRTOS tasks running on different cores.

MethodDescription
configure(config)Injects the board-specific esp3d_sd_config_t
begin()Initializes SPI bus or SDIO host; does not mount
getSPISpeedDivider()Returns the current SPI speed divider
setSPISpeedDivider()Adjusts SPI clock divisor at runtime
refreshWatchdog()Re-arms the access watchdog

Tier 3 files implement the ESP3DFlash or ESP3DSd methods for a specific hardware interface. Only one implementation per medium is compiled into the firmware, controlled by preprocessor guards.

Guard: #if ESP3D_FATFS_FEATURE

Uses ESP-IDF’s wear-levelling + FAT stack:

esp_vfs_fat_spiflash_mount_rw_wl() → wl_handle_t → standard VFS calls
  • Partition label: "flashfs" on the flashfs partition
  • Max path length: CONFIG_FATFS_MAX_LFN
  • Space info: via f_getfree("0:", ...)
  • Format: esp_vfs_fat_spiflash_format_rw_wl() followed by remount

Guard: #if ESP3D_LITTLEFS_FEATURE

Uses ESP-IDF’s LittleFS port:

esp_vfs_littlefs_register() → standard VFS calls
  • Same partition label "flashfs", same mount point /fs
  • grow_on_mount = true — expands to available partition space at first boot
  • Max path length: CONFIG_LITTLEFS_OBJ_NAME_LEN
  • Space info: via esp_littlefs_info()

Both flash implementations share a local flash_vfs_path() helper that normalises incoming paths, preventing double-prefix bugs when a caller passes a full VFS path like /fs/config.ini rather than a relative path.

Guard: #if SD_INTERFACE_TYPE == 0

spi_bus_initialize() → esp_vfs_fat_sdspi_mount() → standard VFS calls
  • SPI host selected from _config->spi.host (SPI2 / SPI3)
  • Speed: _config->freq / 1000 / speed_divider kHz — divider loaded at runtime from NVS via ESP3DSettings
  • disk_status_check_enable = true — helps detect cards removed without unmounting
  • _resetWatchdog() called on every open() and readdir()

Guard: #if SD_INTERFACE_TYPE == 1

sdmmc_host (SDMMC_HOST_DEFAULT) → esp_vfs_fat_sdmmc_mount() → standard VFS calls
  • On chips with GPIO matrix (e.g. ESP32-S3): CLK/CMD/D0–D3 are freely assignable via _config->sdio.*
  • 1-bit or 4-bit bus width via _config->sdio.bit_width
  • SDMMC_SLOT_FLAG_INTERNAL_PULLUP enabled by default

PathResolved toNotes
/Virtual rootLists fs/ and optionally sd/
/fsflashFs.mount_point()Flash root directory
/fs/config.iniFlash filePrefix stripped before backend call
/sdsd.mount_point()SD card root directory
/sd/job.ncSD filePrefix stripped before backend call

Prefix stripping detail: &path[strlen(ESP3D_FLASH_FS_HEADER) - 1] — the - 1 retains the leading / so the backend receives an absolute POSIX path like /config.ini rather than a bare config.ini.


CMakeLists.txt / sdkconfig

ON

ON

ON

0

1

ON

ESP3D_FATFS_FEATURE

ESP3D_LITTLEFS_FEATURE

ESP3D_SD_CARD_FEATURE

SD_INTERFACE_TYPE

ESP3D_SD_SHARED_FEATURE

FatFS flash impl

LittleFS flash impl

SD backend compiled in

SPI SD impl

SDIO SD impl

Shared-bus coordination

⚠️ ESP3D_FATFS_FEATURE and ESP3D_LITTLEFS_FEATURE are mutually exclusive — only one may be active per build. The build system enforces this via cmake/sanity_check.cmake.


esp3d_sd_config_t (defined in esp3d_sd_config.h) is a union-based structure that covers both interfaces in a single memory layout, saving RAM on the memory-constrained ESP32:

typedef struct {
uint8_t interface_type; // 0 = SPI, 1 = SDIO
gpio_num_t detect_pin; // GPIO_NUM_NC if unused
uint8_t detect_value; // 0 = active-low, 1 = active-high
uint32_t freq; // Hz (e.g. 20 000 000 for SPI)
union {
struct {
gpio_num_t mosi, miso, clk, cs;
uint8_t host; // SPI2_HOST / SPI3_HOST
uint8_t speed_divider; // runtime-adjustable via ESP3DSettings
int max_transfer_sz;
uint32_t allocation_size;
} spi;
struct {
gpio_num_t cmd, clk, d0, d1, d2, d3;
uint8_t bit_width; // 1 or 4
} sdio;
};
} esp3d_sd_config_t;

The BSP layer for each supported board (boards/<board>/components/bsp/board_init.c) populates this structure and calls sd.configure(&config) during board_init(). See bsp_board_initialization.md for board-specific pin assignments.


The two backends use different synchronisation strategies appropriate to their hardware characteristics.

flashFsTask B (e.g. HTTP upload)Task AflashFsTask B (e.g. HTTP upload)Task AaccessFS() → _access_nesting = 1accessFS() → _access_nesting = 2releaseFS() → _access_nesting = 1releaseFS() → _access_nesting = 0 (idle)

Flash uses a uint16_t _access_nesting counter rather than a mutex. The assumption is that flash VFS operations are individually atomic at the POSIX layer and that callers coordinate at a higher level.

sd (ESP3DSd)UI TaskGCode Host Tasksd (ESP3DSd)UI TaskGCode Host Taskloop[while job paused]accessFS() [mutex lock → state = busy]open("job.nc")refreshWatchdog()close(fd)releaseFS() [state = idle, mutex unlock]accessFS() [now succeeds]

pthread_mutex_t _state_mutex ensures that all state transitions, watchdog resets, and shared-SD handoffs are atomic even across FreeRTOS tasks on different cores.


Some boards share the SD bus between the ESP32 and an external MCU (e.g. a CNC controller board). When ESP3D_SD_SHARED_FEATURE is enabled, ESP3DSd adds a three-state ownership machine on top of the standard access model:

accessFS() requested

MCU confirmed bus released

releaseFS() - MCU remounted

MCU_OWNS

ACQUIRING

ESP_OWNS

Shared StateDescription
MCU_OWNSExternal MCU has bus ownership; ESP32 must wait
ACQUIRINGHandoff in progress; waiting for MCU acknowledgement
ESP_OWNSESP32 has exclusive SD access

The BSP registers three callbacks via setMCUBusyCallback(), setMCUReleaseCallback(), and setMCURemountCallback() so the driver can poll the MCU and coordinate the handoff before accessing the bus.

For the full protocol specification see the shared_sd_mechanism document referenced in docs/architecture/shared_sd_mechanism_V2.0.md.


File Read (e.g. streaming a GCode job from SD)

Section titled “File Read (e.g. streaming a GCode job from SD)”
ESP-IDF VFS / FATsd (ESP3DSd)globalFsGCode HostESP-IDF VFS / FATsd (ESP3DSd)globalFsGCode Hostloop[read lines]accessFS("/sd/job.nc")accessFS(sd, "/sd/job.nc")OK (mutex held, state = busy)open("/sd/job.nc", "r")open("/job.nc", "r")fopen("/sd/job.nc", "r")FILE*FILE*FILE*refreshWatchdog()fgets / freadclose(fd, "/sd/job.nc")close(fd)releaseFS("/sd/job.nc")releaseFS() [state = idle] ESP-IDF VFSflashFs (ESP3DFlash)app_main / ESP3DX::begin()ESP-IDF VFSflashFs (ESP3DFlash)app_main / ESP3DX::begin()alt[ESP3D_FATFS_FEATURE][ESP3D_LITTLEFS_FEATURE]begin()mount()esp_vfs_fat_spiflash_mount_rw_wl()\n"flashfs" partition → /fsOK + wl_handleesp_vfs_littlefs_register()\n"flashfs" partition → /fsOKgetSpaceInfo() - cache initial statstrue (_mounted = true, _started = true)
SymbolTypeHeaderDescription
flashFsESP3DFlashesp3d_flash.hInternal flash filesystem, always present
sdESP3DSdesp3d_sd.hSD card filesystem, compiled in when ESP3D_SD_CARD_FEATURE
globalFsESP3DGlobalFileSystemesp3d_globalfs.hPath-routing façade — preferred entry point for all callers

All three are statically allocated file-scope objects — no heap allocation at runtime.


All three objects (globalFs, flashFs, sd) expose the same core POSIX-like interface. Callers should always prefer globalFs so path routing is transparent.

MethodFlashSDNotes
begin()✅✅Initialize and mount
mount()✅✅(Re)mount the filesystem
unmount()✅✅Flush and detach VFS
isMounted()✅✅Returns _mounted flag
format()✅❌Low-level format then remount (flash only)
configure(cfg)❌✅SD only — inject board pin config before begin()
MethodDescription
accessFS(path)Claim exclusive-or-nested access; must always be paired with releaseFS
releaseFS(path)Release claim; transitions state to idle when nesting hits zero
getState()Returns ESP3DFsState or ESP3DSdState
refreshWatchdog()SD only — extend hold during long-lived file operations
MethodDescription
getSpaceInfo(total, used, free, refresh)Bytes available/used/free; cached unless refresh = true
getFileSystemName()Human-readable name: "LittleFS", "FatFS", "SDFat native"
maxPathLength()Maximum filename/path length for this FS
mount_point()Returns "/fs" or "/sd"
getFSType(path)Returns ESP3DFileSystemType enum value
MethodDescription
open(path, mode)fopen equivalent → FILE*
close(fd) / close(fd, path)fclose equivalent (globalFs requires path to route the call)
exists(path)stat + check → bool
remove(path)unlink → bool
rename(oldpath, newpath)Move/rename (same filesystem only; cross-FS rename is refused)
mkdir(path)Create directory
rmdir(path)Remove empty directory
stat(path, &st)POSIX stat → int
opendir(path)opendir → DIR*
closedir(dir)closedir → int
readdir(dir)readdir → dirent*
rewinddir(dir)Reset directory stream to beginning

Consumer moduleWhat it accessesPath prefix
Network_&_Web_Services.md — flash file handlerServe/upload/delete files on flash over HTTP/fs/
Network_&_Web_Services.md — SD file handlerServe/upload/delete GCode and config from SD/sd/
Network_&_Web_Services.md — WebDAV handlerPROPFIND directory listingsboth
esp3d_commands.md — ESP720Flash file listing and operations/fs/
esp3d_commands.md — ESP740SD file listing and operations/sd/
esp3d_commands.md — ESP780Global filesystem listing/
Storage_&_Configuration.md — Update ServiceRead firmware images and UI resources from SD/sd/
translations.md — Translation ServiceLoad .lng language pack blobs/fs/ or /sd/
gcode_host.md — GCode HostStream .nc job files/sd/
grbl_module.md, fluidnc_module.md, grblhal_module.md — File ScreensBrowse, launch, delete, rename jobs/sd/
cnc_shared.md — Macro ManagerPersist macro definitions/fs/
SD log backendWrite log entries to a file on SD/sd/