SDK for TESAIoT Dev Kit
API reference & tutorials (ModusToolbox)
Loading...
Searching...
No Matches
G1 — bento_storage and the C credential store

Learning goal

The five entry points of bento_storage; why the on-flash geometry is a numeric contract with the MicroPython port; the deliberate no-format-on-error divergence; and how the six lfs_wifi_creds_* names are re-provided in C so that a board moved between variants keeps its saved networks. By the end you can read and write a file on the mtb-only volume without breaking the mtb-mpy board that will read it next.

The real firmware sequence

Everything in this chapter ships as source in the template: bento_libs/claw/common/storage_c/bento_storage.{h,c}, storage_c/lfs_wifi_creds_c.c, and the two #if !BENTO_HAS_MPY I/O blocks in bento_libs/claw/common/modules/tesaiot_config/tesaiot_config_store.c.

The five entry points

bento_storage.h:31-48:

Function Contract (from the header)
bool bento_storage_init(void) "Bring up SMIF serial memory and mount the LittleFS volume. Call once from main() before anything reads config or credentials. Returns true when the volume is mounted. Safe to call twice."
bool bento_storage_ready(void) "Mounted and usable."
bool bento_storage_format(void) "mkfs + mount. Destroys every file on the volume. Never called by init."
int bento_storage_read_file(const char*, void*, size_t) "Read a whole file into buf. Returns bytes read, or -1. Paths are as the Python side writes them, e.g. "/.tesaiot_config"."
bool bento_storage_write_file(const char*, const void*, size_t) "Atomically replace a file: write path.tmp, remove path, rename."

bento_storage_init() is called at proj_cm33_ns/main.c:306 in the mtb-only branch (Chapter B1, step 7).

The geometry contract

bento_storage.c:5-14 — each number annotated with the MicroPython source it must equal:

read/prog size   0x200      vfs_mount_script kwargs
block size       0x40000    EXT_FLASH_SECTOR_SIZE (KIT_PSE84_AI)
block count      208        EXT_FLASH_SIZE / block size
block_cycles     100        MicroPython extmod/vfs_lfsx.c
cache size       2048       MIN(block, 4*MAX(read,prog)) — same file
lookahead        32         MicroPython extmod/vfs_lfs.c default
flash base       0x00C00000 EXT_FLASH_BASE — above every XIP region

"These numbers must equal what the MicroPython port passes to VfsLfs2, or the two variants stop reading each other's volumes." The mpy side is mpy_main.c:485-492 (os.VfsLfs2(bdev, progsize=0x200, readsize=0x200)). And bento_storage.h:10-12: "Compiled ONLY when BENTO_HAS_MPY=0. Under mtb-mpy the VM owns the mount, and a second lfs2 instance on the same flash would corrupt it."

Mount — and the deliberate no-format

/* ...context: inside bento_storage_init() ... */
cy_rslt_t r = mtb_serial_memory_setup(&s_serial_mem,
MTB_SERIAL_MEMORY_CHIP_SELECT_1,
CYBSP_SMIF_CORE_0_XSPI_FLASH_hal_config.base,
CYBSP_SMIF_CORE_0_XSPI_FLASH_hal_config.clock,
&s_smif_ctx, &s_smif_info, &smif0BlockConfig);
if (r != CY_RSLT_SUCCESS) {
printf("storage: SMIF setup failed 0x%08lx\r\n", (unsigned long)r);
return false;
}
memset(&s_cfg, 0, sizeof(s_cfg));
s_cfg.read = bs_read;
s_cfg.prog = bs_prog;
s_cfg.erase = bs_erase;
s_cfg.sync = bs_sync;
s_cfg.read_size = BS_RW_SIZE;
s_cfg.prog_size = BS_RW_SIZE;
s_cfg.block_size = BS_BLOCK_SIZE;
s_cfg.block_count = BS_BLOCK_COUNT;
s_cfg.block_cycles = BS_BLOCK_CYCLES;
s_cfg.cache_size = BS_CACHE_SIZE;
s_cfg.lookahead_size = BS_LOOKAHEAD;
s_cfg.read_buffer = s_read_buf;
s_cfg.prog_buffer = s_prog_buf;
s_cfg.lookahead_buffer = s_lookahead_buf;
int err = lfs2_mount(&s_lfs, &s_cfg);
if (err != LFS2_ERR_OK) {
/* NOT formatted here on purpose — see the header. */
printf("storage: mount failed (%d); volume left untouched\r\n", err);
return false;
}
s_mounted = true;
return true;
}

The divergence, stated by the header (bento_storage.h:13-17): "the Python path formats the volume on ANY mount failure (bare except: → mkfs). Here a failed mount is an error and the volume is left alone — a wiped /main.py and config is a worse outcome than a boot with storage marked unavailable. bento_storage_format() exists for when erasing is what the caller actually means." The mpy side is mpy_main.c:489-490; variants/mtb-only.mk:22-26 repeats the decision.

Atomic write — tmp, remove, rename

/* ...context: inside bento_storage_write_file() ... */
bs_lock();
bool ok = false;
lfs2_file_t f;
struct lfs2_file_config fcfg = { .buffer = s_file_cache };
int err = lfs2_file_opencfg(&s_lfs, &f, tmp,
LFS2_O_WRONLY | LFS2_O_CREAT | LFS2_O_TRUNC,
&fcfg);
if (err == LFS2_ERR_OK) {
lfs2_ssize_t n = lfs2_file_write(&s_lfs, &f, buf, len);
err = lfs2_file_close(&s_lfs, &f);
if (n == (lfs2_ssize_t)len && err == LFS2_ERR_OK) {
lfs2_remove(&s_lfs, path); /* may not exist; ignore */
ok = (lfs2_rename(&s_lfs, tmp, path) == LFS2_ERR_OK);
}
}
if (!ok) lfs2_remove(&s_lfs, tmp);
bs_unlock();
return ok;

The header's promise (bento_storage.h:45-47): "The same sequence the Python path uses, so a power cut mid-save leaves either the old file or the new one, never a torn half." bs_lock()/bs_unlock() around it is the only file-level lock in either variant.

The C credential store — same names, same bytes

lfs_wifi_creds_c.c provides the six names wifi_init.c links against (lfs_wifi_creds_ready/read/write/init/deinit/needs_resave) so that callers are identical in both variants. On-disk format (:10-18):

0    4   magic    0x57494649 "WIFI"
4    2   version  1
6    2   count    0..6
8  600   entries  6 x sizeof(qspi_wifi_entry_t) = 100
608  4   CRC32 (poly 0xEDB88320) of bytes 0..607

"so a board can move between variants and keep its saved networks." (:18)

void lfs_wifi_creds_init(void) { /* bento_storage_init() owns bring-up */ }
void lfs_wifi_creds_deinit(void) { }
bool lfs_wifi_creds_ready(void) { return bento_storage_ready(); }
/* The mtb-mpy store can inherit a pre-LFS sector image that wants rewriting;
* this store writes only the current format, so never. */
bool lfs_wifi_creds_needs_resave(void) { return false; }
int lfs_wifi_creds_read(qspi_wifi_entry_t *entries, int max_entries) {
if (entries == NULL || max_entries <= 0) return 0;
if (!bento_storage_ready()) return 0;
int n = bento_storage_read_file(CREDS_FILE, s_buf, sizeof(s_buf));
if (n < CREDS_TOTAL_SIZE) return 0;
uint32_t magic; memcpy(&magic, &s_buf[0], 4);
uint16_t version; memcpy(&version, &s_buf[4], 2);
uint16_t count; memcpy(&count, &s_buf[6], 2);
if (magic != QSPI_WIFI_CREDS_MAGIC) return 0;
if (version != QSPI_WIFI_CREDS_VERSION) return 0;
if (count > QSPI_WIFI_CREDS_MAX) return 0;
uint32_t stored; memcpy(&stored, &s_buf[CREDS_HEADER_SIZE + CREDS_ENTRIES_SIZE], 4);
if (stored != calc_crc32(s_buf, CREDS_HEADER_SIZE + CREDS_ENTRIES_SIZE)) {
/* Legacy XOR-32 fallback, exactly as the mtb-mpy reader does. */
if (stored != calc_xor32(s_buf, CREDS_HEADER_SIZE + CREDS_ENTRIES_SIZE)) return 0;
}
int out = (count < (uint16_t)max_entries) ? count : max_entries;
memcpy(entries, &s_buf[CREDS_HEADER_SIZE], (size_t)out * sizeof(qspi_wifi_entry_t));
/* Defensive termination, same as the mtb-mpy reader. */
for (int i = 0; i < out; i++) {
entries[i].ssid[32] = '\0';
entries[i].password[64] = '\0';
}
return out;
}
bool lfs_wifi_creds_write(const qspi_wifi_entry_t *entries, int count) {
if (count < 0 || count > QSPI_WIFI_CREDS_MAX) return false;
if (count > 0 && entries == NULL) return false;
if (!bento_storage_ready()) return false;
memset(s_buf, 0, sizeof(s_buf));
uint32_t magic = QSPI_WIFI_CREDS_MAGIC;
uint16_t version = QSPI_WIFI_CREDS_VERSION;
uint16_t cnt = (uint16_t)count;
memcpy(&s_buf[0], &magic, 4);
memcpy(&s_buf[4], &version, 2);
memcpy(&s_buf[6], &cnt, 2);
if (count > 0) {
memcpy(&s_buf[CREDS_HEADER_SIZE], entries,
(size_t)count * sizeof(qspi_wifi_entry_t));
}
uint32_t crc = calc_crc32(s_buf, CREDS_HEADER_SIZE + CREDS_ENTRIES_SIZE);
memcpy(&s_buf[CREDS_HEADER_SIZE + CREDS_ENTRIES_SIZE], &crc, 4);
return bento_storage_write_file(CREDS_FILE, s_buf, sizeof(s_buf));
}

Mapping notes: lfs_wifi_creds_init is a no-op here (bento_storage_init() owns bring-up); lfs_wifi_creds_ready is bento_storage_ready(); lfs_wifi_creds_needs_resave always returns false; the read side accepts the legacy XOR-32 checksum as a fallback exactly as the mtb-mpy reader does; the write side always emits CRC32. Boot read: sensor_auto_task.c:960-962. Save: sensor_auto_task.c:844-849 — immediate, from the WiFi worker, because the REPL-idle flusher the mpy variant uses does not exist here (variants/mtb-only.mk:6-10). The cross-task shape is snapshot-under-lock, write-outside-lock, because the flash write takes milliseconds.

Config on top of it

Only the two I/O blocks are variant-conditional; the parser and serialiser are shared. Load:

/* ...context: inside tesaiot_config_load() ... */
#if !BENTO_HAS_MPY
/* Read the file through the C lfs2 mount. Same file, same parse. */
static char read_buf[2048];
int n = bento_storage_read_file(TESAIOT_CONFIG_FILE_PATH,
read_buf, sizeof(read_buf));
bool loaded = false;
if (n > 0) {
xSemaphoreTake(s_mutex, portMAX_DELAY);
config_set_defaults(&s_config);
config_parse_content(&s_config, read_buf, (size_t)n);
s_config._version = TESAIOT_CONFIG_VERSION;
xSemaphoreGive(s_mutex);
loaded = true;
}

Save:

/* ...context: inside tesaiot_config_save() ... */
#if !BENTO_HAS_MPY
/* Write through the C lfs2 mount. bento_storage_write_file() is the same
* .tmp + remove + rename sequence the Python block performs. */
bool ok = bento_storage_write_file(TESAIOT_CONFIG_FILE_PATH,
save_buf, (size_t)pos);

Public entry, load-or-write-defaults:

bool tesaiot_config_init(void)
{
if (s_initialized) return true;
s_mutex = xSemaphoreCreateMutexStatic(&s_mutex_buf);
config_set_defaults(&s_config);
s_initialized = true;
/* Try to load from LittleFS; if missing, create default file */
if (!tesaiot_config_load()) {
printf("[TESAIOT_CFG] no config file, creating defaults\r\n");
tesaiot_config_save();
}
return true;
}

Step-by-step

Step 1 — Boot and read the silence

Flash mtb-only (Chapter A2), power-cycle, watch the console.

What you should observe
No storage: line. Failure is loud: storage: SMIF setup failed 0x%08lx (bento_storage.c:108) or storage: mount failed (d); volume left untouched (:131), followed by ERROR: storage unavailable — config and WiFi credentials will use defaults (main.c:306-308). Then no ERROR: tesaiot_config_init failed (main.c:310-312) — its absence means the config loaded (or defaults were written on first boot). Then [HB] t=lus tasks=u every 10 s — the board is scheduling. Three silences and a heartbeat is a healthy storage boot. The former [TESAIOT_CFG] loaded: … line is muted by tesaiot_config_store.c:26 and cannot be used.

Step 2 — Write a file and read it back from C

In your own CM33_NS task (after the scheduler, after bento_storage_ready() is true):

static const char msg[] = "hello from mtb-only\n";
if (bento_storage_ready()) {
bool ok = bento_storage_write_file("/hello.txt", msg, sizeof(msg) - 1);
char buf[64];
int n = bento_storage_read_file("/hello.txt", buf, sizeof(buf));
/* ok == true, n == (int)(sizeof(msg) - 1) */
(void)ok; (void)n;
}
What you should observe
ok true and n equal to the length written. Nothing prints; success is silent. Pull the USB during the write and the header's promise holds: on the next boot the file is either the old one or the new one.

Step 3 — Move the board between variants

Save a WiFi network on mtb-only (Chapter C2's UI path). Flash mtb-mpy. Power-cycle.

What you should observe
The saved network is still listed and the boot auto-connect uses it — the signals are C1 Step 3's: [MPY] GC heap … proves the VM is up, then the topbar WiFi glyph appears (the worker's own [WiFiIPC] lines are muted). No [wifi-glue] line appears: that tag prints only inside app_wifi_connect_direct() (wifi_init.c:225-242), whose sole caller is the archived BLE radio scheduler under ENABLE_PAGE_BENTO_BUDDY=1 (Chapter I1) — no default-build path reaches it. Flash mtb-only again: still there. This is the byte compatibility lfs_wifi_creds_c.c:18 promises, and it is what the mtb-only dist README's acceptance recorded: "storage read data the mtb-mpy variant had written."

Step 4 — Contrast panel: what mtb-mpy does instead

What you should observe — mtb-mpy
No bento_storage at all: the VM mounts the same flash from Python (mpy_main.c:485-492), and lfs_wifi_creds_* come from libbento_mpy.a doing its file I/O by executing Python — which is why lfs_wifi_creds_init there must run from the MicroPython task after the VFS mount, with a gc_collect() before it (mpy_main.c:625-650). A mount failure there formats the volume (mpy_main.c:489-490). Credential writes are deferred to a REPL-idle flusher (mpy_main.c:169-190), which a looping /main.py never reaches (Appendix X #11).

Traps

  • A second lfs2 instance on the same flash corrupts it (bento_storage.h:11-12). Never link storage_c into an mtb-mpy build.
  • Changing any geometry number breaks cross-variant reads silently; the volume mounts on one side and not the other (bento_storage.c:5-14).
  • Calling bento_storage_format() to "recover" destroys /main.py and the config — the exact outcome the no-format policy exists to avoid.
  • Appendix X #11 — mtb-mpy credential flush never runs under a looping /main.py; mtb-only writes immediately. Same file, different timing.
  • Appendix X #18 — two unlocked credential writers. modwifi.c:261-285 (mtb-mpy) and wifi_init.c:261-302 (both variants) read-modify-write the LFS file directly without wifi_creds_lock; they can race a concurrent lfs_wifi_creds_write on the file. bs_lock() in bento_storage.c is the only file-level lock and it is mtb-only.
  • Expecting [TESAIOT_CFG] on the console. Muted (tesaiot_config_store.c:26). The signal is the absence of ERROR: tesaiot_config_init failed.
  • Config port is stored but unused for MQTT (Appendix X #15, mqtt_client_config.c:105-118); editing it in /.tesaiot_config changes nothing.

Variant applicability

Variant
mtb-only — with contrast panels for mtb-mpy (Step 4). bento_storage is compiled only when BENTO_HAS_MPY=0. The six lfs_wifi_creds_* names exist in both variants; their mtb-only implementation is this chapter's lfs_wifi_creds_c.c, and the reduced storage/credentials reference set for this variant is homed with this chapter.