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:
- Write the pin file using
fs_write_pin_code_data_to_file() - Verify the written size using
fs_read_pin_code_data_file_size() - 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()orfs_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
&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:
// 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
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
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;
}