G1 容量实测记录

v1.0 · 2026-10-28 · 判据来源 TC-N-01(06-MVP定稿/05-测试与验收计划)· 执行:板端真机 · 固件 v1.20261028-104652 前后两版 · 工具 C:/emgw-build/em_capacity.py
结论先行:G1 容量判据在两块板上实测达标—— Dev-B 97,737 条(判据 12,000,超 8.1 倍)、 Dev-A 23,132 条(判据 2,500,超 9.3 倍), 两者均灌到引擎返回 EM_E_FULL(-3) 为止,记录区页面全部用满。
附带修掉一个真缺陷:满库写入原本返回 EM_E_IO(-8)(会被误判成 flash 硬件故障), 现已正确返回 EM_E_FULL(-3),并顺势挽回了最后一页被污染的尾部空间。

1. 判据与实测值

TC-N-01 原文判据:「向 Dev-B 持续灌入直到拒绝为止,记录条数 ≥12,000 条(可行性门槛 8,000); Dev-A ≥2,500 条;库可正常打开并检索」。三段判据本记录逐条对应。

项Dev-ADev-B判据
设备 ID / 形态EME-71FAC8 · gamma-cardEME-21E980 · usb-dongle—
硬件 / 串口XIAO N8R8 8MB · COM3DevKitC N16R8 16MB · COM4—
实测容量(灌到拒绝)23,13297,737≥2,500 / ≥12,000
终止错误码EM_E_FULL(-3)EM_E_FULL(-3)须为「库满」
记录区页面占用635 / 9922,683 / 3,040用满
占用字节2,600,94410,989,472—
实测每条字节112.4 B112.4 B—
每页条数36.4336.43—
固件内平均单条耗时8.84 ms8.81 – 10.16 ms—
抽样查询(判据 count==1)12 / 1212 / 12可检索且不重复
反向对照(库外序号应为 0)#23232 → 0 PASS#97837 → 0 PASS证明「查到」非恒真
重启后开库检索—97,737 保全 · 4/4 命中 · 开库 15.6 s PASS库可正常打开并检索
G1 判定达标 +20,632达标 +85,737—

2. 推算口径被实测推翻(差 6.8 倍)

此前所有容量数字都来自口径 rec_pages × 4080 × 85% ÷ 644,其中 644 = EM_REC_STORE_MAX (定长记录上限)。但引擎记录是变长的:实测每条只占 112.4 B, 于是每页能放 36.43 条而不是推算的 6.3 条。

口径Dev-ADev-B性质
旧推算(644 B/条 定长)3,41914,449推算
本次实测(112.4 B/条)23,13297,737实测
实测 ÷ 推算6.77×6.76×—
别把 97,737 当成产品承诺。112.4 B/条 是合成短文本 ("CAP #00000 记忆条目合成内容用于容量实测")的实测值。 真实场景记录更长(text 上限 150 B + meta 64 B + 头 32 B ≈ 246 B,含 384 B 向量时达 644 B)。 按最坏 644 B/条 折算,Dev-B 约 14,449 条、Dev-A 约 3,419 条——两种口径都仍高于 G1 判据, 所以结论「G1 达标」不受文本长度影响;但对外说容量时必须讲清是哪个口径。

3. 顺带修掉的缺陷:满库写入报成了「flash 坏了」

现象:库灌满后任何写入(哪怕 1 字节文本、batch=1)都返回 -8 = EM_E_IO(介质 IO 失败),而灌库过程中同样是满却返回 -3。库本身没坏 (count 不变、检索正常)。上层因此无法区分「容量到了 → 淘汰旧数据」与「flash 故障 → 告警停机」,处置完全相反。

取证

读代码绕了六轮没定论,改为在失败路径加日志上板复现,一次命中:

em.diag: append: total=48 page=2813 off=4000 avail=96 rec_pages=2683 pages_used=2683
em.diag: tx_mark 相容性失败 page=2813 off=4000 len=48 i=8 cur=0x15 new=0x86

根因

em_mem.c: mem_append_raw() 的劈记录分支顺序错了: 先 em_tx_mark(page, off, rec, avail) 把前半段登记进 WAL 脏页表(成功), 然后才 page_advance(),无下一页时返回 EM_E_FULL。 此时脏页表里已躺着这半条记录,em.bulk 随即 em_tx_commit() → apply_all() 把这半条记录真的写进了 flash 数据页,而超级块 rec_cur_off 并未推进。三重后果:

  1. 数据区留下一条无法解析的半截记录(与 G7 断电语义同源的污染);
  2. 该页尾部从此不是 0xFF,NOR 上永久不可写,容量实际损失;
  3. 之后写入时 em_tx_mark 的相容性检查 (cur & new)==new 失败 → EM_E_IO(-8)。

修法与验证

劈记录之前先做无副作用的边界预检(不登记脏页、不擦页):

if (page + 1u >= db->geom.rec_first + db->geom.rec_pages) return EM_E_FULL;
验证项Dev-A(23,132 条满库)Dev-B(97,737 条满库)
再写入的返回码-3 PASS-3 PASS
count 是否被污染保持 23,132 PASS保持 97,737 PASS
满库仍可检索3/3 命中3/3 命中
额外收获:修复后 Dev-B 上 put('x') 成功写入了第 97,738 条—— 最后一页尾部那 96 字节余量被保住了(修复前它已被垃圾数据占死,连 1 字节都写不进)。 说明这次修的不只是错误码,还实际挽回了容量。

4. 本轮新增/调整的工具与参数

项说明
em.wipe(新命令)只擦引擎分区页 0..7(超级块两槽 + Region Table),下次开库自动整片重建。 不动 app / NVS / ota_data,与 esptool erase_flash 是两回事。必须带 confirm:1。 实测:800 条 → 0,rec 页 22 → 1,固件耗时 1.66 s。没有它,每次容量重测都要整片重烧(十几分钟)。
em.bulk 新增 start起始序号。不传就必然重复编号——见下方第 5 节。
em.bulk 默认 batch 64 → 8引擎 em_tx.h:EM_TX_DIRTY_MAX=8、EM_TX_RANGE_MAX=16; 同一页在一个事务里最多登记 16 次修改,故每事务最多约 8 条。batch=64 必然撞 EM_E_TXFULL(-7) 再一路减半到 8。这是 no-malloc 固定脏页表的设计上限,不是缺陷。
em.bulk 的 ok 判据改为 written == n。原先只要中途出现过任何错误码就报 ok=false,曾出现「写入 300/300 却报失败」的误导。
emb.py(新工具)板端通信助手,固化三个坑:开串口会复位板子;首次开大分区要整片格式化 (必须轮询等就绪,定时睡会误判成掉线);日志与 JSON 共用一路串口, 判据必须是「找 id 匹配的完整 JSON 行」。

5. 两个被证伪的「故障」(别再当成 bug)

① 「Dev-B 复位后 CLI 无响应」——误判,固件没问题。 sys.ping / em.stat / sys.info 全部正常回应,500 条数据重启后仍在。 误判原因:诊断脚本在打开串口的瞬间(DTR/RTS 复位板子)就发命令,打在引擎初始化之前; 又叠加 wifi 告警 10 行/秒刷屏干扰判读。

② 「库里每条存了两份」——不是固件缺陷。 决定性实验:全新前缀 T1 灌 300 条 → #00000..#00299 各存恰好 1 份、 #00300 不存在,1:1 完美。此前的重复是把 em.bulk 跑了两遍造成的—— 序号每轮从 0 重来。

由此固化的教训:写入成功 ≠ 编号唯一。 连灌 10 轮 ×2000 条后库里是「#00000..#01999 各 10 份」而不是「#00000..#19999 各 1 份」, 而 count=20000 看上去完全正常。故灌库后必须抽样验证 count==1 (并加「库外序号必须为 0」的反向对照),只看总数会被骗过去。本判据已写进 em_capacity.py。

6. 诚实边界(本记录未覆盖的部分)

复现命令:
python C:/emgw-build/em_capacity.py --port COM4 --wipe --target 100000 --chunk 20000 --batch 8 --prefix CAP --g1-target 12000
python C:/emgw-build/em_capacity.py --port COM3 --wipe --target 30000 --chunk 20000 --batch 8 --prefix CAP --g1-target 2500
原始输出:C:/emgw-build/_cap_devB_final.txt、C:/emgw-build/_cap_devA_final.txt