The Short Answer: Bridging lfs_dir_read to Arduino C++

If you are searching for how to call lfs_dir_read in Arduino, you are likely porting a pure C embedded library that relies on the raw LittleFS C API. Here is the direct answer: Standard Arduino cores do not expose the raw lfs_dir_read() C function directly to the sketch level. Instead, the Arduino FS.h abstraction wraps it in the C++ method Dir::openNextFile().

If you strictly need the C API signature to satisfy a ported C library, you must write a C-linkage shim that translates lfs_dir_read into the Arduino C++ equivalent. If you are just trying to iterate through a directory in a standard sketch, you should use openNextFile().

Board Variant Targeted: This guide and the code below specifically target the ESP32-WROOM-32 DevKit V1 running the official esp32-arduino core (v2.0.14 or v3.x) with the built-in LittleFS library.

Hardware Spec Sheet & Pin Mapping

While the ESP32 has internal flash, high-reliability datalogging requires an external SPI flash chip to avoid wear-leveling conflicts with the firmware partition. We use the VSPI bus for the filesystem to leave HSPI free for displays or radios.

External SPI Flash Pin Mapping (ESP32 DevKit V1 to W25Q128JVSIQ)
ESP32 Pin (VSPI) W25Q128 Pin Function Notes / Bench Tips
GPIO 5 1 (CS) Chip Select Add a 10kΩ pull-up to 3.3V to prevent boot glitches.
GPIO 18 6 (CLK) SPI Clock Keep trace under 5cm for 80MHz SPI stability.
GPIO 19 2 (DO / MISO) Master In Slave Out Direct connect; ESP32 is natively 3.3V logic.
GPIO 23 5 (DI / MOSI) Master Out Slave In Direct connect.
3V3 8 (VCC), 3 (WP), 7 (HOLD) Power & Control Tie WP and HOLD to 3.3V to disable hardware write protection.
GND 4 (GND) Ground Common ground required.

Complete Compilable Code: The C-Linkage Shim

Below is a complete, compilable sketch. It initializes LittleFS on the ESP32, creates dummy files, and demonstrates both the standard C++ directory iteration and a extern "C" shim function. This shim allows ported C code to call a function named shim_lfs_dir_read without breaking the Arduino abstraction layer.

/*
 * Target: ESP32-WROOM-32 DevKit V1
 * Core: esp32-arduino v2.0.14+ or v3.x
 * Library: LittleFS (Built-in)
 */

#include 
#include 
#include 

// --- Pin Definitions for External SPI Flash (VSPI) ---
#define FLASH_CS_PIN   5
#define FLASH_CLK_PIN  18
#define FLASH_MISO_PIN 19
#define FLASH_MOSI_PIN 23

// Mocked C-structs for the shim (normally pulled from lfs.h in pure C environments)
struct lfs_info_mock {
    uint8_t type;
    uint32_t size;
    char name[64];
};

// Global C++ Dir object to maintain state across C-shim calls
static File currentDir;
static File currentFile;

// --- C-Linkage Shim for Ported Libraries ---
extern "C" {
    /**
     * Shim to satisfy C libraries expecting lfs_dir_read.
     * Returns 1 on success (file found), 0 on end of directory, negative on error.
     */
    int shim_lfs_dir_read(void* lfs_ptr, void* dir_ptr, struct lfs_info_mock* info) {
        if (!currentDir) return -1; // LFS_ERR_IO equivalent
        
        currentFile = currentDir.openNextFile();
        if (!currentFile) {
            return 0; // End of directory
        }

        // Map C++ File properties to C lfs_info struct
        info->type = currentFile.isDirectory() ? 2 : 1; // 2=dir, 1=file
        info->size = currentFile.size();
        strncpy(info->name, currentFile.name(), sizeof(info->name) - 1);
        info->name[sizeof(info->name) - 1] = '\0';
        
        currentFile.close();
        return 1; // Success
    }
}

void setup() {
    Serial.begin(115200);
    delay(1000);
    Serial.println("\n--- LittleFS Directory Read Test ---");

    // 1. Initialize SPI bus for external flash
    SPI.begin(FLASH_CLK_PIN, FLASH_MISO_PIN, FLASH_MOSI_PIN, FLASH_CS_PIN);

    // 2. Mount LittleFS (format if mount fails)
    if (!LittleFS.begin(true, "/littlefs", 10, "part0")) {
        Serial.println("[FATAL] LittleFS Mount Failed. Check SPI wiring and partition table.");
        while (1) { delay(1000); }
    }
    Serial.println("LittleFS Mounted Successfully.");

    // 3. Create dummy files for testing
    File f1 = LittleFS.open("/log_001.csv", "w");
    if (f1) { f1.println("timestamp,value"); f1.close(); }
    
    File f2 = LittleFS.open("/log_002.csv", "w");
    if (f2) { f2.println("timestamp,value"); f2.close(); }

    // 4. Test the C-Linkage Shim
    Serial.println("\n--- Iterating via shim_lfs_dir_read (C-API style) ---");
    currentDir = LittleFS.open("/");
    if (!currentDir || !currentDir.isDirectory()) {
        Serial.println("Failed to open root directory.");
        return;
    }

    struct lfs_info_mock info;
    int result;
    while ((result = shim_lfs_dir_read(NULL, NULL, &info)) > 0) {
        Serial.printf("Found: %s (Type: %s, Size: %u bytes)\n", 
                      info.name, 
                      info.type == 2 ? "DIR" : "FILE", 
                      info.size);
    }
    currentDir.close();
    
    if (result < 0) {
        Serial.println("[ERROR] IO error during directory read.");
    }
}

void loop() {
    // Idle
    delay(10000);
}

Troubleshooting: Exact Errors and Ranked Causes

When working at the intersection of C APIs and Arduino wrappers, the compiler and the runtime will throw specific errors. Here are the first three things to check when your directory read fails, mapped to their exact error strings.

⚠️ Safety & Data Caveat: LittleFS is power-loss resilient, but repeatedly formatting a corrupted partition in a while(1) loop without a hardware watchdog can lead to flash wear-out if the SPI bus is floating. Always verify CS pin pull-ups.

1. Exact Error: fatal error: lfs.h: No such file or directory

  • Root Cause: You are trying to #include <lfs.h> directly in an ESP32 Arduino sketch. The ESP32 core uses esp_littlefs via the VFS (Virtual File System) layer, which intentionally hides the raw C headers from the sketch namespace to prevent pointer conflicts.
  • The Fix: Remove #include <lfs.h>. Use #include <LittleFS.h> and rely on the C++ wrapper, or use the shim provided above. If you absolutely need the raw C headers, you must migrate to the ESP-IDF native environment rather than the Arduino IDE.

2. Exact Error: E (1234) vfs_littlefs: lfs_dir_read failed with -84 (or LFS_ERR_CORRUPT)

  • Root Cause: The filesystem metadata is corrupted. This happens when the ESP32 browned out during a lfs_file_write operation, or you are trying to mount a partition that was previously formatted as SPIFFS or FATFS.
  • The Fix: Pass true as the first argument to LittleFS.begin(true) to force a format on mount failure. If it persists, use the ESP32 Flash Download Tool to completely erase the flash chip (Erase Flash button in Arduino IDE Tools menu) and re-upload.

3. Exact Error: assertion "lfs->cfg->read_size % lfs->cfg->prog_size == 0" failed

  • Root Cause: You are using a custom partition table where the LittleFS partition size is not a perfect multiple of the flash chip's block size (usually 4096 bytes for W25Q series).
  • The Fix: Open your partitions.csv file. Ensure the size column for your LittleFS partition is a multiple of 0x1000 (4KB). For example, use 0x100000 (1MB) instead of an arbitrary decimal value.

Decision Tree: Raw C API vs. Arduino C++ Wrapper

Use this decision matrix to determine which implementation path to take for your specific project constraints.

Project Constraint Choose Raw C API (ESP-IDF / Shim) Choose Arduino C++ Wrapper (FS.h)
Codebase Origin Porting existing bare-metal C firmware (e.g., Zephyr, custom RTOS). Writing a new sketch from scratch in the Arduino IDE.
Memory Overhead Requires manual allocation of lfs_file_t buffers (lower RAM overhead if tuned). Uses std::string and C++ objects (adds ~2-4KB RAM overhead).
Wear-Leveling Control Direct access to lfs_stat and block wear metrics. Metrics are hidden; relies on VFS defaults.
Development Speed Slow (requires writing C-shims and managing pointers). Fast (LittleFS.open() just works).
✅ The Default Pick: Unless you are actively porting a legacy C library that strictly requires lfs_t pointers, always choose the Arduino C++ Wrapper. The performance penalty is negligible on the ESP32's 240MHz dual-core processor, and it prevents catastrophic pointer misalignment bugs.

Extending and Simplifying the Build

How to Extend the Build (Advanced Datalogging)

If you are using LittleFS for high-frequency sensor logging, extend the build by implementing a write-ahead log (WAL) pattern. Instead of opening and closing a file for every sensor reading (which triggers LittleFS block erases), write raw bytes to a fixed-size RAM buffer (e.g., 4096 bytes). Only call file.write() and file.close() when the buffer is full. This aligns perfectly with the W25Q128's 4KB sector size, reducing flash wear by up to 90% and extending the chip's lifespan from 100,000 cycles to well over 1,000,000 effective cycles.

How to Simplify the Build (Beginner Path)

If the C-linkage shim and external SPI flash feel like overkill, simplify the build immediately:

  1. Drop the external flash: Use the ESP32's internal flash partition. Remove the SPI.begin() lines and change LittleFS.begin(true) to target the internal spiffs or littlefs partition label.
  2. Drop the C-shim: Replace the shim_lfs_dir_read logic entirely with the standard Arduino iteration pattern:
    Dir dir = LittleFS.openDir("/");
    while (dir.next()) {
        Serial.println(dir.fileName());
    }

By understanding what lfs_dir_read is actually doing under the hood—advancing a directory pointer and populating an lfs_info struct—you can confidently map any embedded C filesystem requirement to the Arduino environment without fighting the compiler.