LittleFS on nRF5340 + MX25L51245G (64MB QSPI NOR): -14 (EFAULT) after ~1600 files, filesystem wiped on reboot

Environment

  • Board: nRF5340 custom PCB
  • SDK: nRF Connect SDK 3.0.2
  • External flash: MX25L51245GZ2I-08G (64MB NOR, QSPI interface)
  • Filesystem: LittleFS via Zephyr fstab
  • IDE: VS Code with nRF Connect extension

What I am doing

I am writing 2500 to 3000 files into a single LittleFS directory on external QSPI flash. Each file is approximately 1KB in size and stores pin code data. Files are named by their pin code value for example /external/pin_codes/12345678.

For each pin received over BLE I follow this exact sequence:

  1. Write the pin file using fs_write_pin_code_data_to_file()
  2. Verify the written size using fs_read_pin_code_data_file_size()
  3. Write a 12-byte GUID/transaction record into a separate session file using fs_write_current_ble_session_command_guids_data_on_file()

Total data written is roughly 2500 x 1KB = ~2.5MB. This is less than 4% of the 64MB flash. Storage space is not the issue.


Problem

After approximately 1600 file writes the following errors start occurring:

  • fs_open() or fs_write() returns -14 (EFAULT)
  • Occasionally -2 (ENOENT) on file open even though the directory exists
  • On the next reboot all files are completely wiped — all 1600+ pin files are gone with no warning

Key observation: if I comment out the call fs_write_current_ble_session_command_guids_data_on_file() at step 3, I can write 2500 to 3000 pin files without any error. The moment that call is active errors begin at around 1600 writes. The function fs_write_current_ble_session_command_guids_data_on_file() does internally on every single call — it opens the session file, seeks to the position, writes 12 bytes of GUID/transaction data, then closes the file. This means across 1600 pin writes the same session file is being opened, seeked, written and closed 1600 times. 

Also observed: if I batch the session file writes — record every 20 records in a buffer and then write — the error does not appear for 3000 pins but it may appear after that.

The major concern is that the filesystem behaves very unpredictably — I am not confident about when it will randomly wipe out all files and data

DTS configuration

dts
&qspi {
    pinctrl-0 = <&qspi_default>;
    pinctrl-1 = <&qspi_sleep>;
    pinctrl-names = "default", "sleep";

    mx25l512: mx25l51245g@0 {
        compatible = "nordic,qspi-nor";
        reg = <0>;
        writeoc = "pp4io";
        readoc = "read4io";
        sck-frequency = <8000000>;
        jedec-id = [c2 20 1A];
        sfdp-bfp = [
            e5 20 fb ff  1f ff ff ff  44 eb 08 6b  08 3b 04 bb
            fe ff ff ff  ff ff 00 ff  ff ff 44 eb  0c 20 0f 52
            10 d8 00 ff  d6 49 c5 00  81 df 04 e3  44 03 67 38
            30 b0 30 b0  f7 bd d5 5c  4a 9e 29 ff  f0 50 f9 85
        ];
        size = <0x20000000>;
        has-dpd;
        t-enter-dpd = <10000>;
        t-exit-dpd = <30000>;
        external_partition: partition {
            label = "external";
        };
    };
};

fstab {
    compatible = "zephyr,fstab";
    lfs1: lfs1 {
        compatible = "zephyr,fstab,littlefs";
        mount-point = "/external";
        partition = <&external_partition>;
        read-size = <16>;
        prog-size = <256>;
        cache-size = <4096>;
        lookahead-size = <512>;
        block-cycles = <500>;
    };
};

Functions being used

Below is the outer flow that runs once per BLE pin command. It ties all three steps together:

c
// Called once per pin received over BLE.
// Writes pin file, verifies it, then writes the transaction GUID.
void process_single_pin(uint32_t pin_code, uint8_t pin_length,
                        const uint8_t *data, uint16_t start,
                        uint16_t length, int32_t command_guid,
                        int32_t time_stamp)
{
    // Step 1 — write the pin data file (~1KB) to flash
    int write_res = fs_write_pin_code_data_to_file(
                        pin_code, pin_length, data, start, length);
    sb_print_log(LOG_DEBUG, "Pin write result: %d", write_res);

    // Step 2 — verify written size matches expected
    int pin_size = fs_read_pin_code_data_file_size(pin_code);
    if (pin_size != length)
    {
        sb_print_log(LOG_ERR, "Size mismatch. Expected: %d Found: %d",
                     length, pin_size);
        fs_delete_pin_code_data(pin_code);
        return;
    }

    // Step 3 — write 12-byte GUID record into separate session file.
    // Internally this opens the session file, seeks to the current
    // counter offset, writes 12 bytes, then closes the file.
    // This open/seek/write/close happens once per pin — so across
    // 1600 pins this function opens and closes the same file 1600 times.
    fs_write_current_ble_session_command_guids_data_on_file(command_guid, len);
                                    
}

Step 1 — Write pin file

c
int fs_write_pin_code_data_to_file(uint32_t pin_code, uint8_t pin_length,
                                    const uint8_t *data, uint16_t start,
                                    uint16_t length)
{
    int write_count = 0;
    char pin_code_file_name[15];
    char full_file_name[MAX_PATH_LEN];

    snprintf(pin_code_file_name, sizeof(pin_code_file_name), "%" PRIu32, pin_code);
    snprintf(full_file_name, sizeof(full_file_name), "%s/%s",
             PIN_CODES_DIRECTORY, pin_code_file_name);

    for (int i = 0; i < 3; i++)
    {
        int result = __fs_write_bytes_to_file(fs_external_mount_point,
                                              full_file_name,
                                              data + start, length,
                                              &write_count);
        if (result < 0)
        {
            k_sleep(K_MSEC(100));
            result = __fs_write_bytes_to_file(fs_external_mount_point,
                                              full_file_name,
                                              data + start, length,
                                              &write_count);
        }
        if (result == APP_SUCCESS)
            break;
    }
    return write_count;
}

int __fs_write_bytes_to_file(struct fs_mount_t *fs_mount_point,
                              const char *abs_path,
                              const char *data, int len, int *write_count)
{
    int rc;
    struct fs_file_t file;
    char fname[MAX_PATH_LEN];

    fs_file_t_init(&file);
    snprintf(fname, sizeof(fname), "%s/%s",
             fs_mount_point->mnt_point, abs_path);

    rc = fs_open(&file, fname, FS_O_CREATE | FS_O_WRITE);
    if (rc < 0)
    {
        sb_print_log(LOG_DEBUG, "File Open Err: %d", rc);
        return rc;
    }

    rc = fs_write(&file, data, len);
    fs_sync(&file);
    fs_close(&file);

    *write_count = rc;
    return rc < 0;
}

Step 2 — Verify written file size

c
int fs_read_pin_code_data_file_size(uint32_t pin_code)
{
    char pin_code_file_name[45];
    char file_name[MAX_PATH_LEN];

    snprintf(pin_code_file_name, sizeof(pin_code_file_name), "%u", pin_code);
    snprintf(file_name, sizeof(file_name), "%s/%s/%s",
             fs_external_mount_point->mnt_point,
             PIN_CODES_DIRECTORY,
             pin_code_file_name);

    int rc;
    int size = 0;

    for (int i = 0; i < 3; i++)
    {
        struct fs_dirent dirent;
        dirent.type = FS_DIR_ENTRY_FILE;
        rc = fs_stat(file_name, &dirent);
        if (rc == 0)
        {
            size = dirent.size;
            break;
        }
    }

    if (rc < 0)
    {
        sb_print_log(LOG_DEBUG, "Error %d for Fs Stat (%s)", rc, file_name);
        return rc;
    }

    return size;
}


Step 3 — Write the 12 byte GUID data in session file

int8_t fs_write_current_ble_session_command_guids_data_on_file(uint8_t *data_to_write, uint8_t len)
{
     int8_t rc;
     struct fs_file_t file;
     fs_file_t_init(&file);
     char fname[MAX_PATH_LEN];
     snprintf(fname, sizeof(fname), "%s/%s",
     fs_external_mount_point->mnt_point,
     TRANSACTIONS_BLE_SESSION_DATA_FILE);
     rc = fs_open(&file, fname, FS_O_CREATE | FS_O_WRITE);
     if (rc < 0)
     {
        return -1;
     }

   /**
   * Each record is of 12 bytes.
   * Seek to the correct position for this record.
   */
    int32_t location_index = g_current_ble_session_transaction_counter * len;
    rc = fs_seek(&file, location_index, FS_SEEK_SET);
    if (rc != 0)
    {
       sb_print_log(LOG_DEBUG, "File Seek Err: %d", rc);
       return rc;
     }

   /**
   * Write the record data.
   */
   rc = fs_write(&file, data_to_write, len);
   if (rc != len)
   {
      sb_print_log(LOG_DEBUG, "Failed to Write Current Session GUIDs in file: %d", rc);
   }

   fs_close(&file);

   /**
   * Update the counter to enable circular usage of the file.
   */
    g_current_ble_session_transaction_counter++;
    if (g_current_ble_session_transaction_counter >= MAX_CURRENT_SESSION_TRANSACTIONS_COUNT)
    {
       g_current_ble_session_transaction_counter = 0;
    }

    return rc;
}






  • Try adding this in your dts for mx25l512 node.

    address-size-32;

    I think the qspi driver might be defaulting to a 24 bit addressing in the absense of this. and 24 bit addressing only covers 16MB. That could be a reason for it to roll over or something like that.

  • Hi Nordic Team,

    I am waiting for your reply on my previous post. In the meantime I have done an isolated stress test on a fresh LittleFS sample project to narrow down the issue.


    Environment

    • Board: nRF5340 custom PCB
    • SDK: nRF Connect SDK 3.0.2
    • External flash: MX25L51245GZ2I-08G (64MB NOR, QSPI)
    • Filesystem: LittleFS via Zephyr fstab

    What I am doing

    I am writing the same single file in a loop of 10,000 iterations — overwriting it every time. The file is always the same name and always the same size. Flash is fully erased before the test starts. Only DATA_SIZE changes between tests runs. Everything else is identical.


    Observation

    DATA_SIZE Result
    270 bytes -14 (EFAULT) at ~4000 writes
    800 bytes No error at 10000 writes
    1024 bytes No error at 10000 writes
    4000 bytes No error at 10000 writes

    Question:

    What is causing -14 (EFAULT) specifically when writing a 270 byte file repeatedly but not when writing the file with size 800, 1024 or 4000?


    &qspi {
    pinctrl-0 = <&qspi_default>;
    pinctrl-1 = <&qspi_sleep>;
    pinctrl-names = "default", "sleep";

    mx25l512: mx25l51245g@0 {
    compatible = "nordic,qspi-nor";
    reg = <0>;
    writeoc = "pp4io";
    readoc = "read4io";
    sck-frequency = <8000000>;
    jedec-id = [c2 20 1a];
    sfdp-bfp = [
    e5 20 fb ff ff ff ff 1f 44 eb 08 6b 08 3b 04 bb
    fe ff ff ff ff ff 00 ff ff ff 44 eb 0c 20 0f 52
    10 d8 00 ff d6 49 c5 00 81 df 04 e3 44 03 67 38
    30 b0 30 b0 f7 bd d5 5c 4a 9e 29 ff f0 50 f9 85
    ];
    size = <0x20000000>;
    has-dpd;
    t-enter-dpd = <10000>;
    t-exit-dpd = <30000>;
    external_partition: partition {
    label = "external";
    };
    };
    };

    fstab {
    compatible = "zephyr,fstab";
    lfs1: lfs1 {
    compatible = "zephyr,fstab,littlefs";
    mount-point = "/external";
    partition = <&external_partition>;
    read-size = <16>;
    prog-size = <256>;
    cache-size = <4096>;
    lookahead-size = <512>;
    block-cycles = <500>;
    };
    };


    Proj.conf

    CONFIG_MAIN_STACK_SIZE=10240

    CONFIG_HEAP_MEM_POOL_SIZE=20480
    CONFIG_MBEDTLS_HEAP_SIZE=18432
    CONFIG_FS_LITTLEFS_FC_HEAP_SIZE=16384

    # Debug
    CONFIG_DEBUG=y
    # Logging — use IMMEDIATE so logs aren't buffered/lost on quick exit
    CONFIG_LOG=y
    CONFIG_LOG_MODE_IMMEDIATE=y
    CONFIG_LOG_BACKEND_UART=y
    # Console/Serial
    CONFIG_SERIAL=y
    CONFIG_UART_CONSOLE=y
    CONFIG_PRINTK=y

    # Storage
    CONFIG_FLASH=y
    CONFIG_FLASH_MAP=y
    CONFIG_FILE_SYSTEM=y
    CONFIG_FILE_SYSTEM_LITTLEFS=y
    CONFIG_APP_LITTLEFS_STORAGE_FLASH=y
    CONFIG_CONSOLE=y
    CONFIG_REBOOT=y
    CONFIG_SPI=y
    CONFIG_FLASH_JESD216_API=y



    Refer the code below

    #define DATA_SIZE (270)

    int fs_write_bytes_to_file(struct fs_mount_t *fs_mount_point,
    const char *abs_path,
    const uint8_t *data,
    int len)
    {
    if (fs_mount_point == NULL || abs_path == NULL || data == NULL)
    {
    LOG_ERR("%s: NULL argument", __func__);
    return -EINVAL;
    }

    if (len <= 0)
    {
    LOG_ERR("%s: invalid length %d", __func__, len);
    return -EINVAL;
    }

    char fname[MAX_PATH_LEN];
    int path_len = snprintf(fname, sizeof(fname), "%s/%s",
    fs_mount_point->mnt_point, abs_path);

    if (path_len < 0 || path_len >= (int)sizeof(fname))
    {
    LOG_ERR("%s: path too long (%s/%s)",
    __func__, fs_mount_point->mnt_point, abs_path);
    return -ENAMETOOLONG;
    }

    struct fs_file_t file;
    fs_file_t_init(&file);

    int rc = fs_open(&file, fname, FS_O_CREATE | FS_O_RDWR);
    if (rc < 0)
    {
    LOG_ERR("%s: fs_open failed (%s) rc=%d", __func__, fname, rc);
    return rc;
    }

    rc = fs_write(&file, data, len);
    if (rc < 0)
    {
    LOG_ERR("%s: fs_write failed (%s) rc=%d", __func__, fname, rc);
    goto cleanup;
    }

    if (rc != len)
    {
    LOG_ERR("%s: partial write (%s) wrote=%d expected=%d",
    __func__, fname, rc, len);
    rc = -EIO;
    goto cleanup;
    }

    rc = fs_sync(&file);
    if (rc < 0)
    {
    LOG_ERR("%s: fs_sync failed (%s) rc=%d", __func__, fname, rc);
    goto cleanup;
    }

    rc = len;

    cleanup:
    int close_rc = fs_close(&file);
    if (close_rc < 0)
    {
    LOG_ERR("%s: fs_close failed (%s) rc=%d", __func__, fname, close_rc);
    if (rc >= 0)
    rc = close_rc;
    }

    return rc;
    }




    int fs_write_data_file(const uint8_t *data, int size)
    {
    return fs_write_bytes_to_file(fs_external_mount_point,
    LOCKER_RSA_PUB_KEY_FILE,
    data, size);
    }




    static int little_fs_stress_test1(void)
    {
    uint8_t dummy_data[DATA_SIZE];
    for (size_t i = 0; i < DATA_SIZE; i++)
    {
    dummy_data[i] = (uint8_t)(i & 0xFF);
    }

    for (int i = 0; i < 10000; i++)
    {
    LOG_INF("Writing file counter =:: %d", i);
    fs_write_data_file(dummy_data, DATA_SIZE);
    }

    return 0;
    }



    int main(void)
    {
    struct fs_statvfs sbuf;
    int rc;

    rc = littlefs_mount(fs_external_mount_point);
    if (rc < 0)
    {
    LOG_ERR("FAIL: mount external flash: %d", rc);
    return 0;
    }

    rc = fs_statvfs(fs_external_mount_point->mnt_point, &sbuf);
    if (rc < 0)
    {
    LOG_PRINTK("FAIL: statvfs: %d\n", rc);
    return 0;
    }

    LOG_PRINTK("%s: bsize=%lu frsize=%lu blocks=%lu bfree=%lu\n",
    fs_external_mount_point->mnt_point,
    sbuf.f_bsize, sbuf.f_frsize,
    sbuf.f_blocks, sbuf.f_bfree);

    fs_flash_area_erase(fs_external_mount_point);
    little_fs_stress_test1();

    return 0;
    }

     






  • Have you read my previous reply? Have you tried adding address-size-32 in your dts file for the mx25l512 node? I thought that if you add this to the node, then the addressing bit mode should change and should help with the -14 error, if it did not, then please help me reproduce this issue on the nRF5340 DK by giving me a minimalistic project.

Related