EM_E_FULL(-3) 为止,记录区页面全部用满。
EM_E_IO(-8)(会被误判成 flash 硬件故障),
现已正确返回 EM_E_FULL(-3),并顺势挽回了最后一页被污染的尾部空间。
TC-N-01 原文判据:「向 Dev-B 持续灌入直到拒绝为止,记录条数 ≥12,000 条(可行性门槛 8,000); Dev-A ≥2,500 条;库可正常打开并检索」。三段判据本记录逐条对应。
| 项 | Dev-A | Dev-B | 判据 |
|---|---|---|---|
| 设备 ID / 形态 | EME-71FAC8 · gamma-card | EME-21E980 · usb-dongle | — |
| 硬件 / 串口 | XIAO N8R8 8MB · COM3 | DevKitC N16R8 16MB · COM4 | — |
| 实测容量(灌到拒绝) | 23,132 | 97,737 | ≥2,500 / ≥12,000 |
| 终止错误码 | EM_E_FULL(-3) | EM_E_FULL(-3) | 须为「库满」 |
| 记录区页面占用 | 635 / 992 | 2,683 / 3,040 | 用满 |
| 占用字节 | 2,600,944 | 10,989,472 | — |
| 实测每条字节 | 112.4 B | 112.4 B | — |
| 每页条数 | 36.43 | 36.43 | — |
| 固件内平均单条耗时 | 8.84 ms | 8.81 – 10.16 ms | — |
| 抽样查询(判据 count==1) | 12 / 12 | 12 / 12 | 可检索且不重复 |
| 反向对照(库外序号应为 0) | #23232 → 0 PASS | #97837 → 0 PASS | 证明「查到」非恒真 |
| 重启后开库检索 | — | 97,737 保全 · 4/4 命中 · 开库 15.6 s PASS | 库可正常打开并检索 |
| G1 判定 | 达标 +20,632 | 达标 +85,737 | — |
此前所有容量数字都来自口径 rec_pages × 4080 × 85% ÷ 644,其中 644 = EM_REC_STORE_MAX
(定长记录上限)。但引擎记录是变长的:实测每条只占 112.4 B,
于是每页能放 36.43 条而不是推算的 6.3 条。
| 口径 | Dev-A | Dev-B | 性质 |
|---|---|---|---|
| 旧推算(644 B/条 定长) | 3,419 | 14,449 | 推算 |
| 本次实测(112.4 B/条) | 23,132 | 97,737 | 实测 |
| 实测 ÷ 推算 | 6.77× | 6.76× | — |
"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 达标」不受文本长度影响;但对外说容量时必须讲清是哪个口径。
现象:库灌满后任何写入(哪怕 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 并未推进。三重后果:
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 命中 |
put('x') 成功写入了第 97,738 条——
最后一页尾部那 96 字节余量被保住了(修复前它已被垃圾数据占死,连 1 字节都写不进)。
说明这次修的不只是错误码,还实际挽回了容量。
| 项 | 说明 |
|---|---|
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 行」。 |
① 「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 重来。
count=20000 看上去完全正常。故灌库后必须抽样验证 count==1
(并加「库外序号必须为 0」的反向对照),只看总数会被骗过去。本判据已写进 em_capacity.py。
frag.* / ts.* 命令仍不存在)。
故耗时不能当作 G5 的通道耗时结论,只能作初步参考。python C:/emgw-build/em_capacity.py --port COM4 --wipe --target 100000 --chunk 20000 --batch 8 --prefix CAP --g1-target 12000python C:/emgw-build/em_capacity.py --port COM3 --wipe --target 30000 --chunk 20000 --batch 8 --prefix CAP --g1-target 2500C:/emgw-build/_cap_devB_final.txt、C:/emgw-build/_cap_devA_final.txt