เป้าหมายของหัวข้อนี้
จุดเข้าใช้งาน 5 จุดของ bento_storage เหตุผลที่ geometry (รูปทรงข้อมูลบนแฟลช) เป็นข้อกำหนดเชิงตัวเลขร่วมกับ MicroPython port ความแตกต่างเรื่องไม่ format เมื่อเกิด error ซึ่งเป็นการตัดสินใจโดยเจตนา และวิธีที่ฝั่ง C จัดหาชื่อทั้งหกของ lfs_wifi_creds_* ให้ใหม่ เพื่อให้บอร์ดที่ย้ายไปมาระหว่าง variant ยังคงเครือข่ายที่บันทึกไว้ เมื่อจบบทนี้จะอ่านและเขียนไฟล์บนวอลุมของ mtb-only ได้ โดยไม่ทำให้บอร์ด mtb-mpy ที่จะมาอ่านต่อพัง
ลำดับการทำงานจริงของเฟิร์มแวร์
ทุกอย่างในบทนี้ส่งมอบเป็นซอร์สอยู่ในเทมเพลต ได้แก่ bento_libs/claw/common/storage_c/bento_storage.{h,c}, storage_c/lfs_wifi_creds_c.c และบล็อก I/O สองบล็อกที่อยู่หลัง #if !BENTO_HAS_MPY ใน bento_libs/claw/common/modules/tesaiot_config/tesaiot_config_store.c
จุดเข้าใช้งาน 5 จุด
bento_storage.h:31-48:
| ฟังก์ชัน | ข้อกำหนดการเรียกใช้ (จาก 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() มีการเรียกที่ proj_cm33_ns/main.c:306 ในสาขา mtb-only (บท B1 ขั้นที่ 7)
ข้อกำหนดเรื่อง geometry
bento_storage.c:5-14 — ตัวเลขแต่ละตัวมีหมายเหตุกำกับไว้ว่าต้องเท่ากับค่าใดในซอร์ส MicroPython:
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." ฝั่ง mpy คือ mpy_main.c:485-492 (os.VfsLfs2(bdev, progsize=0x200, readsize=0x200)) และ 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."
การเมานต์ — และการเลือกไม่ format โดยเจตนา
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) {
printf("storage: mount failed (%d); volume left untouched\r\n", err);
return false;
}
s_mounted = true;
return true;
}
ความแตกต่างนี้ระบุไว้ในไฟล์ 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." ฝั่ง mpy คือ mpy_main.c:489-490 และ variants/mtb-only.mk:22-26 ย้ำการตัดสินใจเดียวกันไว้
การเขียนแบบอะตอมมิก — tmp, remove, rename
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);
ok = (lfs2_rename(&s_lfs, tmp, path) == LFS2_ERR_OK);
}
}
if (!ok) lfs2_remove(&s_lfs, tmp);
bs_unlock();
return ok;
คำมั่นในไฟล์ header (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() ที่ครอบอยู่เป็น lock ระดับไฟล์เพียงตัวเดียวใน variant ทั้งสอง
ที่เก็บข้อมูลรับรองฝั่ง C — ชื่อเดิม ไบต์เดิม
lfs_wifi_creds_c.c จัดหาชื่อทั้งหกที่ wifi_init.c ลิงก์ถึง (lfs_wifi_creds_ready/read/write/init/deinit/needs_resave) เพื่อให้ผู้เรียกเหมือนกันทั้งสอง variant รูปแบบข้อมูลบนดิสก์ (: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)
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)) {
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));
for (int i = 0; i < out; i++) {
entries[i].ssid[32] = '\0';
entries[i].password[64] = '\0';
}
return out;
}
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));
}
หมายเหตุการจับคู่: lfs_wifi_creds_init เป็น no-op ที่นี่ (bento_storage_init() เป็นเจ้าของการ bring-up) lfs_wifi_creds_ready คือ bento_storage_ready() lfs_wifi_creds_needs_resave คืนค่า false เสมอ ฝั่งอ่านยอมรับค่าตรวจสอบ (checksum) แบบ XOR-32 รุ่นเก่าเป็นทางสำรอง เช่นเดียวกับฝั่งอ่านของ mtb-mpy ส่วนฝั่งเขียนออกเป็น CRC32 เสมอ การอ่านตอนบูตอยู่ที่ sensor_auto_task.c:960-962 การบันทึกอยู่ที่ sensor_auto_task.c:844-849 — ทันที จากตัวทำงาน WiFi เพราะตัว flush ที่รอ REPL ว่างซึ่ง variant mpy ใช้นั้นไม่มีอยู่ที่นี่ (variants/mtb-only.mk:6-10) รูปแบบข้ามงานคือถ่าย snapshot ใต้ lock แล้วเขียนนอก lock เพราะการเขียนแฟลชกินเวลาระดับมิลลิวินาที
ชั้น config ที่วางอยู่บนนั้น
มีเพียงบล็อก I/O สองบล็อกเท่านั้นที่ขึ้นกับ variant ส่วน parser และ serialiser ใช้ร่วมกัน การโหลด:
#if !BENTO_HAS_MPY
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;
}
การบันทึก:
#if !BENTO_HAS_MPY
bool ok = bento_storage_write_file(TESAIOT_CONFIG_FILE_PATH,
save_buf, (size_t)pos);
จุดเข้าใช้งานสาธารณะ แบบโหลดหรือเขียนค่าเริ่มต้น:
bool tesaiot_config_init(void)
{
if (s_initialized) return true;
s_mutex = xSemaphoreCreateMutexStatic(&s_mutex_buf);
config_set_defaults(&s_config);
s_initialized = true;
if (!tesaiot_config_load()) {
printf("[TESAIOT_CFG] no config file, creating defaults\r\n");
tesaiot_config_save();
}
return true;
}
ทีละขั้น
ขั้นที่ 1 — บูตแล้วอ่านความเงียบ
แฟลช mtb-only (บท A2) ตัดไฟแล้วจ่ายไฟใหม่ และเฝ้าดูคอนโซล
- สิ่งที่ควรสังเกต
- ไม่มีบรรทัด storage: ความล้มเหลวจะดังเสมอ: storage: SMIF setup failed 0x%08lx (bento_storage.c:108) หรือ storage: mount failed (d); volume left untouched (:131) ตามด้วย ERROR: storage unavailable — config and WiFi credentials will use defaults (main.c:306-308) จากนั้นต้องไม่มี ERROR: tesaiot_config_init failed (main.c:310-312) — การที่บรรทัดนี้ไม่ปรากฏหมายความว่า config โหลดสำเร็จ (หรือเขียนค่าเริ่มต้นลงไปในการบูตครั้งแรก) แล้วจึงเป็น [HB] t=lus tasks=u ทุก 10 s — บอร์ดกำลังจัดตารางงานอยู่ ความเงียบ 3 จุดกับสัญญาณชีพ (heartbeat) หนึ่งเส้นคือการบูตของที่เก็บข้อมูลที่ปกติ ส่วนบรรทัด [TESAIOT_CFG] loaded: … แบบเดิมนั้นปิดเสียงไว้โดย tesaiot_config_store.c:26 และใช้อ้างอิงไม่ได้
ขั้นที่ 2 — เขียนไฟล์แล้วอ่านกลับจากฝั่ง C
ใน task ของ CM33_NS ที่เขียนเอง (หลัง scheduler ทำงานแล้ว และหลัง bento_storage_ready() เป็น 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));
(void)ok; (void)n;
}
- สิ่งที่ควรสังเกต
- ok เป็น true และ n เท่ากับความยาวที่เขียนไป ไม่มีอะไรพิมพ์ออกมา ความสำเร็จเงียบเสมอ หากถอดสาย USB ระหว่างการเขียน คำมั่นในไฟล์ header ยังคงเป็นจริง: ในการบูตครั้งถัดไปไฟล์จะเป็นของเดิมหรือของใหม่อย่างใดอย่างหนึ่ง
ขั้นที่ 3 — ย้ายบอร์ดข้ามระหว่าง variant
บันทึกเครือข่าย WiFi บน mtb-only (เส้นทาง UI ของบท C2) แฟลช mtb-mpy ตัดไฟแล้วจ่ายไฟใหม่
- สิ่งที่ควรสังเกต
- เครือข่ายที่บันทึกไว้ยังอยู่ในรายการ และการเชื่อมต่ออัตโนมัติตอนบูตใช้ค่านั้น สัญญาณที่ใช้ดูคือชุดเดียวกับบท C1 ขั้นที่ 3: [MPY] GC heap … พิสูจน์ว่า VM ขึ้นแล้วจากนั้น glyph WiFi บนแถบบนปรากฏ (บรรทัด [WiFiIPC] ของตัวทำงานเองปิดเสียงไว้) และไม่มีบรรทัด [wifi-glue] ปรากฏ เพราะแท็กนั้นพิมพ์เฉพาะภายใน app_wifi_connect_direct() (wifi_init.c:225-242) ซึ่งมีผู้เรียกรายเดียวคือตัวจัดคิววิทยุ BLE ที่เก็บอยู่ใน archive ภายใต้ ENABLE_PAGE_BENTO_BUDDY=1 (บท I1) — ไม่มีเส้นทางของ build ปริยายใดไปถึงมัน แฟลช mtb-only กลับอีกครั้งข้อมูลก็ยังอยู่ นี่คือความเข้ากันได้ระดับไบต์ที่ lfs_wifi_creds_c.c:18 ให้คำมั่นไว้และเป็นสิ่งที่ผลการตรวจรับใน README ของ dist ฝั่ง mtb-only บันทึกไว้ว่า "storage read data the mtb-mpy variant had written."
ขั้นที่ 4 — แผงเปรียบเทียบ: mtb-mpy ทำอย่างไรแทน
- สิ่งที่ควรสังเกต — mtb-mpy
- ไม่มี bento_storage เลย: VM เมานต์แฟลชก้อนเดียวกันจากฝั่ง Python (mpy_main.c:485-492) และ lfs_wifi_creds_* มาจาก libbento_mpy.a ซึ่งทำ I/O ไฟล์ด้วยการรันโค้ด Python — จึงเป็นเหตุผลที่ lfs_wifi_creds_init ในฝั่งนั้นต้องทำงานจาก MicroPython task หลังการเมานต์ VFS และต้องมี gc_collect() นำหน้า (mpy_main.c:625-650) การเมานต์ที่ล้มเหลวในฝั่งนั้น format วอลุมทิ้ง (mpy_main.c:489-490) การเขียนข้อมูลรับรองเลื่อนไปให้ตัว flush ที่รอ REPL ว่าง (mpy_main.c:169-190) ซึ่ง /main.py ที่วนไม่รู้จบไม่มีวันไปถึง (ภาคผนวก X #11)
กับดัก
- lfs2 อีกอินสแตนซ์หนึ่งบนแฟลชก้อนเดียวกันทำให้ข้อมูลเสียหาย (bento_storage.h:11-12) ห้ามลิงก์ storage_c เข้ากับ build ของ mtb-mpy
- การแก้ตัวเลข geometry ตัวใดก็ตาม ทำให้การอ่านข้ามระหว่าง variant พังอย่างเงียบ ๆ วอลุมจะเมานต์ได้ฝั่งหนึ่งและไม่ได้อีกฝั่ง (bento_storage.c:5-14)
- การเรียก bento_storage_format() เพื่อ "recover" ทำลาย /main.py และ config ทิ้ง — เป็นผลลัพธ์ที่นโยบายไม่ format มีไว้เพื่อหลีกเลี่ยงพอดี
- ภาคผนวก X #11 — การ flush ข้อมูลรับรองของ mtb-mpy ไม่มีวันทำงานเมื่อ /main.py วนไม่รู้จบ ส่วน mtb-only เขียนทันที ไฟล์เดียวกัน จังหวะเวลาต่างกัน
- ภาคผนวก X #18 — ผู้เขียนข้อมูลรับรอง 2 รายที่ไม่จับ lock modwifi.c:261-285 (mtb-mpy) และ wifi_init.c:261-302 (ทั้งสอง variant) ทำ read-modify-write ลงไฟล์ LFS โดยตรงโดยไม่จับ wifi_creds_lock จึงชิงกับ lfs_wifi_creds_write ที่ทำงานพร้อมกันได้ bs_lock() ใน bento_storage.c เป็น lock ระดับไฟล์เพียงตัวเดียว และมีเฉพาะบน mtb-only
- การคาดหวังบรรทัด [TESAIOT_CFG] บนคอนโซล บรรทัดนั้นปิดเสียงไว้ (tesaiot_config_store.c:26) สัญญาณที่ใช้คือการไม่มี ERROR: tesaiot_config_init failed
- ค่า port ใน config เก็บไว้แต่ MQTT ไม่ได้ใช้ (ภาคผนวก X #15, mqtt_client_config.c:105-118) การแก้ค่านั้นใน /.tesaiot_config ไม่เปลี่ยนอะไรเลย
ขอบเขตการใช้กับ variant
- Variant
- mtb-only — พร้อมแผงเปรียบเทียบสำหรับ mtb-mpy (ขั้นที่ 4) bento_storage คอมไพล์เฉพาะเมื่อ BENTO_HAS_MPY=0 ชื่อทั้งหกของ lfs_wifi_creds_* มีอยู่ในทั้งสอง variant ส่วนการนำไปใช้ฝั่ง mtb-only คือ lfs_wifi_creds_c.c ของบทนี้และคู่มืออ้างอิงชุดย่อเรื่องที่เก็บข้อมูลและข้อมูลรับรองสำหรับ variant นี้จัดวางไว้กับบทนี้