diff --git a/Drivers/BSP/NET/lftpd/lftpd.c b/Drivers/BSP/NET/lftpd/lftpd.c index 32e8d446..02124f0b 100644 --- a/Drivers/BSP/NET/lftpd/lftpd.c +++ b/Drivers/BSP/NET/lftpd/lftpd.c @@ -104,6 +104,32 @@ static int send_response(int socket, int code, bool include_code, #define send_multiline_response_end(socket, code, format, ...) \ send_response(socket, code, true, false, format, ##__VA_ARGS__) +/* 一块装满就整块发出、清空继续装。 + * 为什么仍要"批量":定案三(../问题记录/CH395F_Trap_Records.md,docs 里那条 LIST 吞包 + * 定案)实测同一连接上**背靠背 ~60 B 小段写**会毒化 CH395F 的 TX(芯片报 SEND_OK 但对端 + * 全丢,无 RST 无 DISCONNECT),而 RETR 的 1 KB 满块连发安全——所以这里只发大块, + * 绝不退回"每行一发"。 + * 为什么必须"续发"而不是截断:原先装满就 break 并回 -1,cmd_list 据此回 550,客户端 + * 既拿到**不完整**的目录、又收到错误码(实测 24 条目/1512 B 之后全部被吞)。目录大 + * 小不该由一个缓冲区上限决定。 + * 返回:0 = 已发出(或本就空),-1 = 写失败 */ +static int list_batch_flush(int socket, char *batch, int *p_blen) +{ + int len = *p_blen; + + if (len <= 0) { + return 0; + } + lftpd_task_wd_beat(); /* 看门狗:目录列表分块发送处 */ + if (lftpd_inet_write(socket, batch, len) < 0) { + return -1; + } + *p_blen = 0; + return 0; +} + + + static int send_list(int socket, const char *path) { static const char *directory_format = "drw-rw-rw- 1 owner group %13lu Jan 01 1970 %s\r\n"; @@ -118,11 +144,9 @@ static int send_list(int socket, const char *path) uint32_t size; int is_dir; - /* N30-PROBE(临时实验):整份目录一次性发送(验证"背靠背多小段 - * 毒化芯片 TX"假设——每行一个 net_send 时 LIST#4 必挂) */ static char batch[1536]; /* static: off-stack */ int blen = 0; - int trunc = 0; + int fail = 0; while (lftpd_io_readdir(dp, name, sizeof(name), &size, &is_dir) == 0) { int n; @@ -133,25 +157,45 @@ static int send_list(int socket, const char *path) n = snprintf(batch + blen, sizeof(batch) - blen, file_format, (unsigned long)size, name); } - if (n < 0 || (size_t)n >= sizeof(batch) - blen) { - trunc = 1; + if (n < 0) { + fail = 1; break; } + if ((size_t)n >= sizeof(batch) - blen) { + if (list_batch_flush(socket, batch, &blen) != 0) { + fail = 1; + break; + } + /* 单行就超过整块 → 明确失败,绝不静默少发条目 + * (8.3 短名 + 定长格式远小于 1536,真发生说明格式被改坏) */ + if ((size_t)n >= sizeof(batch)) { + fail = 1; + break; + } + if (is_dir) { + n = snprintf(batch, sizeof(batch), directory_format, + (unsigned long)size, name); + } else { + n = snprintf(batch, sizeof(batch), file_format, + (unsigned long)size, name); + } + if (n < 0 || (size_t)n >= sizeof(batch)) { + fail = 1; + break; + } + } blen += n; } - lftpd_io_closedir(dp); + lftpd_io_closedir(dp); /* 所有出口都必须走到:卷锁持有至 closedir */ - /* lftpd_inet_write 成功返回字节数、失败返回 -1(非 0=成功语义) */ - if (blen > 0) { + if (!fail && blen > 0) { + /* lftpd_inet_write 成功返回字节数、失败返回 -1(非 0=成功语义) */ if (lftpd_inet_write(socket, batch, blen) < 0) { - return -1; + fail = 1; } } - if (trunc) { - return -1; - } - return 0; + return fail ? -1 : 0; } static int send_nlst(int socket, const char *path) @@ -165,34 +209,44 @@ static int send_nlst(int socket, const char *path) uint32_t size; int is_dir; - /* N30-PROBE(临时实验):同 send_list,一次性批量发送 */ static char batch[1536]; /* static: off-stack */ int blen = 0; - int trunc = 0; + int fail = 0; while (lftpd_io_readdir(dp, name, sizeof(name), &size, &is_dir) == 0) { if (!is_dir) { int n = snprintf(batch + blen, sizeof(batch) - blen, "%s\r\n", name); - if (n < 0 || (size_t)n >= sizeof(batch) - blen) { - trunc = 1; + if (n < 0) { + fail = 1; break; } + if ((size_t)n >= sizeof(batch) - blen) { + if (list_batch_flush(socket, batch, &blen) != 0) { /* 同 send_list:满块续发 */ + fail = 1; + break; + } + if ((size_t)n >= sizeof(batch)) { + fail = 1; + break; + } + n = snprintf(batch, sizeof(batch), "%s\r\n", name); + if (n < 0 || (size_t)n >= sizeof(batch)) { + fail = 1; + break; + } + } blen += n; } } - lftpd_io_closedir(dp); + lftpd_io_closedir(dp); /* 所有出口都必须走到:卷锁持有至 closedir */ - /* lftpd_inet_write 成功返回字节数、失败返回 -1(非 0=成功语义) */ - if (blen > 0) { - if (lftpd_inet_write(socket, batch, blen) < 0) { - return -1; + if (!fail && blen > 0) { + if (lftpd_inet_write(socket, batch, blen) < 0) { /* 成功返回字节数,非 0=成功 */ + fail = 1; } } - if (trunc) { - return -1; - } - return 0; + return fail ? -1 : 0; } static int send_file(int socket, const char *path) diff --git a/docs/images/内存布局_FLASH.svg b/docs/images/内存布局_FLASH.svg index cb1afc26..7b32ae2f 100644 --- a/docs/images/内存布局_FLASH.svg +++ b/docs/images/内存布局_FLASH.svg @@ -8,34 +8,34 @@ Flash (1MB) 使用布局 — 2026-10-22 @build - - 代码段 (Code) 208.3KB - - 只读数据 (RO) 18.2KB - - Flash 空闲 796.4KB -已用: 227.6KB (22.2%) 空闲: 796.4KB (77.8%) ER_IROM1(Code+RO): 226.5KB (22.1%) 总量: 1MB (= ROM 合计口径) -Code + RO 分布(ER_IROM1 226.5KB,按 .map 对象归类) - - 85.7KB - - 55.7KB - - 25.1KB - - 17.5KB - - - - - - + + 代码段 (Code) 208.4KB + + 只读数据 (RO) 18.2KB + + Flash 空闲 796.2KB +已用: 227.8KB (22.2%) 空闲: 796.2KB (77.8%) ER_IROM1(Code+RO): 226.6KB (22.1%) 总量: 1MB (= ROM 合计口径) +Code + RO 分布(ER_IROM1 226.6KB,按 .map 对象归类) + + 85.7KB + + 55.7KB + + 25.3KB + + 17.5KB + + + + + + LVGL 库 85.7KB 应用代码/UI 55.7KB - CH395F+FTP 25.1KB + CH395F+FTP 25.3KB HAL 驱动 17.5KB diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 9d829319..e5fb0cb9 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -38,10 +38,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 231,916 B (226.48 KB) | 1,048,576 B (1024 KB) | 22.1% | +| ER_IROM1 | 232,088 B (226.65 KB) | 1,048,576 B (1024 KB) | 22.1% | | RW_IRAM1 | 75,264 B (73.50 KB) | 131,072 B (128 KB) | 57.4% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **233,104 B (227.6 KB)** | 1,048,576 B (1024 KB) | **22.2%** | +| **Flash (ROM 合计)** | **233,276 B (227.8 KB)** | 1,048,576 B (1024 KB) | **22.2%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index 95aef43c..a69e0afa 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,7 +15,7 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 231,916 B | 1,048,576 B | 22.1% | 参考 | +| ER_IROM1 (Flash) | 232,088 B | 1,048,576 B | 22.1% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | | RW_IRAM1 | 75,264 B | 131,072 B | 57.4% | PASS | diff --git a/docs/内存管理/data/ram_report_20261022.md b/docs/内存管理/data/ram_report_20261022.md index d8df039c..035b08aa 100644 --- a/docs/内存管理/data/ram_report_20261022.md +++ b/docs/内存管理/data/ram_report_20261022.md @@ -1,24 +1,24 @@ # STM32F4-Base RAM/Flash Usage Report -> Generated: 2026-10-22 22:26 Map: `STM32F407-Demo.map` +> Generated: 2026-10-22 22:36 Map: `STM32F407-Demo.map` ## 1. Grand Totals (全局总量) | Metric | Value | |---|---| -| Code (inc. data) | 213,264 B (208.3 KB) | +| Code (inc. data) | 213,436 B (208.4 KB) | | RO Data | 18,652 B (18.2 KB) | | RW Data (initialized) | 1,188 B (1.16 KB) | | ZI Data (zero-init) | 131,500 B (128.4 KB) | -| **Total RO (Code + RO)** | **231,916 B (226.5 KB)** | +| **Total RO (Code + RO)** | **232,088 B (226.6 KB)** | | **Total RW (RW + ZI) = SRAM used** | **132,688 B (129.58 KB)** | -| **Total ROM (Code + RO + RW init)** | **233,104 B (227.6 KB)** | +| **Total ROM (Code + RO + RW init)** | **233,276 B (227.8 KB)** | ## 2. 执行区水位 (Execution Regions) | Region | Base | Size | Max | Util% | |---|---|---|---|---| -| ER_IROM1 | 0x08000000 | 231,916 B (226.48 KB) | 1,048,576 B (1024 KB) | 22.1% | +| ER_IROM1 | 0x08000000 | 232,088 B (226.65 KB) | 1,048,576 B (1024 KB) | 22.1% | | RW_CCM | 0x10000000 | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | | UNINIT_CCM | 0x1000FF00 | 48 B (0.05 KB) | 256 B (0 KB) | 18.8% | | RW_IRAM1 | 0x20000000 | 75,264 B (73.50 KB) | 131,072 B (128 KB) | 57.4% | @@ -136,7 +136,7 @@ |---|---|---|---|---|---|---| | 1 | heap_4.o | 512 | 69 | 32 | 57,344 | 57,376 | | 2 | lv_mem_core_builtin.o | 410 | 0 | 0 | 16,384 | 16,384 | -| 3 | lftpd.o | 6,180 | 509 | 160 | 7,724 | 7,884 | +| 3 | lftpd.o | 6,352 | 509 | 160 | 7,724 | 7,884 | | 4 | ui_net_sink.o | 1,126 | 67 | 20 | 6,746 | 6,766 | | 5 | ui_port.o | 418 | 0 | 12 | 6,408 | 6,420 | | 6 | nand_ftl.o | 2,352 | 175 | 16 | 6,200 | 6,216 | @@ -204,15 +204,15 @@ | 68 | vol_lock.o | 92 | 0 | 4 | 0 | 4 | | 69 | errno.o | 20 | 0 | 4 | 0 | 4 | | 70 | ui_page_net_cfg.o | 1,004 | 105 | 2 | 0 | 2 | -| | **All .o files** | **213,264** | **18,652** | **1,188** | **131,500** | **132,688** | +| | **All .o files** | **213,436** | **18,652** | **1,188** | **131,500** | **132,688** | ## 5. Flash (Code + RO Data) Top-30(按大小降序) | # | Object | Code | RO Data | Code+RO | |---|---|---|---|---| | 1 | ff.o | 7,962 | 282 | 8,244 | -| 2 | cfg_file.o | 5,970 | 810 | 6,780 | -| 3 | lftpd.o | 6,180 | 509 | 6,689 | +| 2 | lftpd.o | 6,352 | 509 | 6,861 | +| 3 | cfg_file.o | 5,970 | 810 | 6,780 | | 4 | net_socket.o | 4,944 | 443 | 5,387 | | 5 | lv_indev.o | 4,928 | 0 | 4,928 | | 6 | ch395f.o | 3,978 | 603 | 4,581 | @@ -240,6 +240,6 @@ | 28 | printfa.o | 2,312 | 0 | 2,312 | | 29 | stm32f4xx_hal_i2c.o | 2,294 | 0 | 2,294 | | 30 | lv_display.o | 2,284 | 0 | 2,284 | -| | **All .o files** | **213,264** | **18,652** | **231,916** | +| | **All .o files** | **213,436** | **18,652** | **232,088** | -> 自洽校验:per-file 213,264/18,652/1,188/131,500 vs Grand Totals 213,264/18,652/1,188/131,500(CodeΔ=+0 / ROΔ=+0 / RWΔ=+0 / ZIΔ=+0;精确一致(含 '('incl. Generated)'-'(incl. Padding)' 台账行))→ **PASS** +> 自洽校验:per-file 213,436/18,652/1,188/131,500 vs Grand Totals 213,436/18,652/1,188/131,500(CodeΔ=+0 / ROΔ=+0 / RWΔ=+0 / ZIΔ=+0;精确一致(含 '('incl. Generated)'-'(incl. Padding)' 台账行))→ **PASS** diff --git a/docs/测试说明/FTP_Test_Guide.md b/docs/测试说明/FTP_Test_Guide.md index d839b0ad..a99618bc 100644 --- a/docs/测试说明/FTP_Test_Guide.md +++ b/docs/测试说明/FTP_Test_Guide.md @@ -88,6 +88,20 @@ - 用途:连接/传输过程中抓包,分析 PASV 端口、命令/数据通道交互,用于疑难排查。 - 用法参见脚本内 `--help` / 注释。 +### 4.5 大目录完整性:`ftp_list_fill_test.py`(2026-10-22 起常驻) + +- **为什么单独立一个脚本**:lftpd 的 `send_list` 曾把目录装进一块 static 1536 B 缓冲, + 装满就 break 并回 −1,`cmd_list` 据此回 **550** —— 目录一大,客户端拿到的是**静默截断** + 的清单外加一个错误码,而清单看起来完全像"目录里就这些"。台架实测 30 条目的根目录只回 + 24 条目(1512 B),6 个文件凭空消失。修完之后,**目录清空的设备上这颗雷再也暴露不出来**, + 所以需要一个自己把目录撑过一块的回归。机理与修复见 `../问题记录/CH395F_Trap_Records.md` + 的 LIST 吞包定案(定案三)增补。 +- 命令:`& '.venv\Scripts\python.exe' test/host/ftp_list_fill_test.py`(`--files 26` 默认) +- 做什么:造 26 个 8.3 短名小文件(`ZZFTnn.TXT`)→ `LIST` → 三条判据 → 逐个 `DELE` 还原 +- 判据:① 26 个 STOR 全 226;② LIST 全文里每个名字都在(少一个 = 截断回归);③ LIST 的 + **完成回包是 226 而不是 550**(用 ftplib 的 `retrlines`,非 2xx 完成会直接抛异常)。 +- 实测:`收到 56 行 / 3544 字节`(跨 3 块,满块续发被真正走到)→ 三条全 PASS。 +- 注意:会在设备上写删 26 个文件,需要 `[ftp] write=1`(出厂默认);失败时按 WARN 提示手清残留。 --- ## 5. GUI 客户端手测(可选) diff --git a/docs/环境与部署/项目部署环境要求.md b/docs/环境与部署/项目部署环境要求.md index c649bc88..cd4e67a2 100644 --- a/docs/环境与部署/项目部署环境要求.md +++ b/docs/环境与部署/项目部署环境要求.md @@ -504,7 +504,7 @@ $env:DTU_KEIL_UV4 = 'D:\NotExist\UV4.exe' # 仅当前会话,不动用户级 | 资源 | 已用 | 总量 | 利用率 | 余量 | 红线说明 | |---|---|---|---|---|---| -| Flash(`ER_IROM1` = Code + RO) | 231,916 B(226.5 KB) | 1 MB | 22.1% | ≈804 KB | **充裕**:产品需求无 OTA / 双镜像预算,Flash 不是卡点;死代码裁剪簇已收口 → `docs/问题记录/项目审查与优化建议报告.md` §5.4 | +| Flash(`ER_IROM1` = Code + RO) | 232,088 B(226.6 KB) | 1 MB | 22.1% | ≈804 KB | **充裕**:产品需求无 OTA / 双镜像预算,Flash 不是卡点;死代码裁剪簇已收口 → `docs/问题记录/项目审查与优化建议报告.md` §5.4 | | 主 SRAM1(静态区;FreeRTOS 堆已迁 CCM) | 75,264 B(73.5 KB) | 128 KB | 57.4% | 55,808 B | 已不紧张——2026-10-11 堆迁 CCM 与 2026-10-15 `LV_MEM_SIZE` 40→16 KB 两步的结果 | | CCM(`RW_CCM` 段) | 57,376 B(56.0 KB) | 65,280 B | **87.9%** | **7,904 B** | **当前真正的红线**:加任务 / 抬栈 / 加堆会先在这里撞墙;`mem-budget` 钩子在 92% 拦提交 | diff --git a/docs/问题记录/CH395F_Trap_Records.md b/docs/问题记录/CH395F_Trap_Records.md index 0ce7e7e9..7b74f375 100644 --- a/docs/问题记录/CH395F_Trap_Records.md +++ b/docs/问题记录/CH395F_Trap_Records.md @@ -132,7 +132,7 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修 2. **幽灵态甄别**:补 accept 前回读对端端口(`GET_REMOT_IPP`),**= 上一条连接端口(`last_peer_port`)→ 幽灵,不 accept**; 3. 对账②同步加幽灵甄别;`last_peer_port` 登记点:`handle_disconnect_event` / `handle_timeout_event` / `net_relisten_try`(清零 remote_port 前转登)/ `net_close_locked`(跨 memset 保留——幽灵属芯片 Socket 级,与 fd 复用无关)。 - **定案二:closed-with-data accept(425 真根因=accept 竞态自毁缓冲)**:`FUN_PARA` bit3 `SOCKET_CLOSE=1`(§5.52 ②)下产生 DISCONNECT 后芯片**不关 Socket、不清缓冲**,RX 缓冲里 STOR 载荷一个不少(探针实证 `recv_len=1027 re-read st=0x05/0x00`);那个 DISCONNECT 是**客户端正常 FIN**(ftplib `storbinary` 发完即关,局域网小文件 ~5ms)。旧 `net_accept_locked` 把 CLOSED 一律当"无连接" → `net_relisten_try`(close+open **清空芯片 RX 缓冲**)→ 数据被**我方亲手销毁** → 425。**修复**:CLOSED + auto_relisten + **芯片 RX 有数据**(`GET_RECV_LEN_SN`>0)→ 接受这条"已关但可读"连接(日志 `closed-with-data accept (drain pending)`),应用快路径排空,排空后 EOF 自然触发 relisten——对齐 §5.52 ② 契约。 -- **定案三:LIST 吞包 = 背靠背小段写毒化芯片 TX**:决定性对照——同一目录,LIST #1(行间天然间隔)通过;LIST #4/#7(每行一次 `net_send` ~60B 背靠背小段)必挂。芯片对同一连接的**背靠背多段小写入**会毒化 TX(芯片报 SEND_OK 但对端全部丢弃,无 RST 无 DISCONNECT);RETR 1KB 满块连发不受影响。**修复**:`send_list`/`send_nlst` 整份目录**一次性批量发送**(static 1536B 缓冲;注意 `lftpd_inet_write` 成功返回**字节数**非 0)。 +- **定案三:LIST 吞包 = 背靠背小段写毒化芯片 TX**:决定性对照——同一目录,LIST #1(行间天然间隔)通过;LIST #4/#7(每行一次 `net_send` ~60B 背靠背小段)必挂。芯片对同一连接的**背靠背多段小写入**会毒化 TX(芯片报 SEND_OK 但对端全部丢弃,无 RST 无 DISCONNECT);RETR 1KB 满块连发不受影响。**修复**:`send_list`/`send_nlst` 整份目录**一次性批量发送**(static 1536B 缓冲;注意 `lftpd_inet_write` 成功返回**字节数**非 0)。 **2026-10-22 增补(同一处缓冲埋的第二颗雷)**:批量发送把"装满就 `break` + 回 −1"留在了路径上,`cmd_list` 据此回 **550**——目录一旦超过 1536 B,客户端拿到的是**静默截断**的清单**外加一个错误码**,而清单看起来完全像"目录里就这些"。实测台架根目录 30 条目:修复前 1512 B / 24 条目 / 完成回包 550,修复后 1880 B / 30 条目 / 226,**6 个文件凭空消失**。改成**满块续发**(装满一块整块发出、清空再装):既保住"只发大块"这条实测结论(定案三的本体),又不让清单完整性由一个缓冲区上限决定;单行真装不下时**明确失败**而不是少发条目。同一函数还有个已知偏差:`cmd_list`/`cmd_nlst` 忽略命令参数、恒列 `client->directory`(会话 CWD)——主流客户端都是 `CWD` + 裸 `LIST`,暂未修,登记在案。 **回归**:`test/host/ftp_list_fill_test.py`(自造 26 个 8.3 短名文件把清单撑过一块,判"全列出 + 完成回包 226",跑完自动删干净;用法见 `../测试说明/FTP_Test_Guide.md` §4.5)——设备目录清空后,只有这种自己造规模的脚本才拦得住这颗雷。 - **与 TRAP-NET-17 的关系**:周期对账对常驻监听器有效;FTP 数据通道是"活 1 秒就死"的短命监听器,对账②无幽灵甄别时既救不全又帮凶。本条把"看状态补 accept"内联进 accept 热路径(短去抖、无冷却)+ 端口甄别,对账②保留为兜底网。 - **教训(总纲)**:**CH395F 的"成功"(中断位/状态码)只代表芯片本地动作完成,不代表对端收到;判定必须回到对端可见证据(主机侧收发)+ 手册语义(FUN_PARA §5.52、状态码 §5.22)**。 - **对应文件**:`net_events.c`(`net_lost_connect_probe` / `net_reconcile_tick`②;原在 `net_socket.c`,2026-10-14 抽出)、`net_socket.c`(closed-with-data accept)、`net_types.h`(`last_peer_port`)、`lftpd.c`(批量发送);台架 `test/board/ch395f_reuse_bench.{c,h}` + `test/host/ch395f_reuse_host.py`(主分支存档)。 diff --git a/docs/问题记录/项目审查与优化建议报告.md b/docs/问题记录/项目审查与优化建议报告.md index 26fa8a1e..a7f09d47 100644 --- a/docs/问题记录/项目审查与优化建议报告.md +++ b/docs/问题记录/项目审查与优化建议报告.md @@ -93,7 +93,7 @@ git ls-files -z | xargs -0 stat -c '%s' | 名词 | 口径 | |---|---| -| Flash 占用 | `Code + RO-data` = `ER_IROM1` = **231,916 B(22.1%)**;`ROM 合计` 再加 `RW` 初始化表 = 233,104 B(22.2%) | +| Flash 占用 | `Code + RO-data` = `ER_IROM1` = **232,088 B(22.1%)**;`ROM 合计` 再加 `RW` 初始化表 = 233,276 B(22.2%) | | SRAM 占用 | `RW_IRAM1`(DMA 可达区)与 `RW_CCM`(仅 CPU)分开计,二者不可互换(规则 R2/R3) | | 最坏栈 | `max(armlink 静态调用图深度, 真机 [STACK] 高水位)`;判定线 `余量 ≥ max(1024 B ISR, 配置×30%)` | | 编译单元 | uvprojx 的 `` 计数(今日 **326 个 `.c`**,其中 LVGL 201);"自研 `.c`" 另按目录口径 114 个 | @@ -230,8 +230,8 @@ defaultTask 单属主;原子写经卷锁;旧档迁移已删除。**仍列为 | 指标 | 2026-10-22 | 2026-10-15 | 说明 | |---|---|---|---| -| `Code` | 213,264 B | 213,520 B | 当日累计:F-6 −332 B、SPI 回调归属 +16 B、消息队列收口 +32 B、FTP 日志级别改造 +28 B(RO 另 +72 B) | -| `ER_IROM1`(Code+RO) | **231,916 B / 22.1%** | 232,280 B / 22.2% | 余 ≈804 KB | +| `Code` | 213,436 B | 213,520 B | 当日累计:F-6 −332、SPI 回调 +16、消息队列 +32、FTP 日志级别 +28(RO 另 +72)、**LIST 满块续发 +172** | +| `ER_IROM1`(Code+RO) | **232,088 B / 22.1%** | 232,280 B / 22.2% | 余 ≈803 KB | | `RW_IRAM1` | **75,264 B / 57.4%**(余 55,808) | 73,568 B / 56.1% | 增量来自 B5/B8 功能码 | | `RW_CCM` | **57,376 B / 87.9%**(余 7,904) | 57,376 B / 87.9% | **当前唯一红线** | | FreeRTOS 堆 | 56 KB(57,344 B)@CCM;实测 `free=15,800` / `min_ever=8,464` | 同 | 2026-10-22 真机 | @@ -271,7 +271,7 @@ defaultTask 单属主;原子写经卷锁;旧档迁移已删除。**仍列为 | **F-6** 双精度软浮点 | ≈1 KB | **归因错**:全库 0 处 `%f`;剩余 `dadd/dmul/ddiv/depilogue/dfixul` 1,018 B 锁在 MicroLIB `printfa.o` 的 `_printf_core → _fp_digits`,只要用 `snprintf` 就删不掉。但**根因侧确有可修点**:`App/algo/prot_set.h` 两个 Q10 宏写成 `double` | ✅ **已落地 2026-10-22**:改单精度 → `@build` −332 B、5 个 helper(300 B)消失;数值逐位等价(穷举 42 个字面量 + int32 全域 ÷1024 往返 4.49×10⁶ 例零差异);真机 `@test` PASS | **当前可回收 Flash = 0 B**(F-4/F-6 已定论,图片链同理不打补丁)。LVGL 仍占镜像 **37.8%** -(87,706 / 231,916 B)——这是"用 LVGL 画单色菜单"的结构性代价,F-1/F-2/F-3 已省掉 64,576 B。 +(87,706 / 232,088 B)——这是"用 LVGL 画单色菜单"的结构性代价,F-1/F-2/F-3 已省掉 64,576 B。 ### 5.5 内存治理长效机制(常开,不受暂缓影响) diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index 3e73537e..73e89db2 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -4,4 +4,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-14 212296 18740 1112 154456 231036 22.0 98144 74.9 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924 2026-10-15 213540 18740 1136 131520 232280 22.2 75232 57.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;sys_log_mcu.o=3588;default_task.o=2736 2026-10-20 213356 18580 1184 131480 231936 22.1 75240 57.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;sys_log_mcu.o=3588;default_task.o=2736 -2026-10-22 213264 18652 1188 131500 231916 22.1 75264 57.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;sys_log_mcu.o=3588;default_task.o=2736 +2026-10-22 213436 18652 1188 131500 232088 22.1 75264 57.4 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;sys_log_mcu.o=3588;default_task.o=2736 diff --git a/test/host/ftp_list_fill_test.py b/test/host/ftp_list_fill_test.py new file mode 100644 index 00000000..175ef533 --- /dev/null +++ b/test/host/ftp_list_fill_test.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +LIST 完整性回归 —— 2026-10-22 起常驻 + +为什么需要:lftpd 的 send_list/send_nlst 原先把目录装进一块 static 1536 B 缓冲,装满就 +break 并回 -1,cmd_list 据此回 **550**——目录一大,客户端拿到的是**静默截断**的清单外加 +一个错误码,而清单看起来完全像"目录里就这些"。台架实测:30 条目的根目录只回了 24 条目 +(1512 B),6 个文件凭空消失。设备目录清空后这个雷**再也暴露不出来**,所以需要本脚本 +主动把目录撑过一块:造 N 个 8.3 短名小文件 → LIST 必须**全部列出**且完成回包是 226 → +逐个删掉还原。 + +判据(三条全过才 PASS): + 1. N 个文件全部 STOR 成功(226) + 2. LIST 全文里每个名字都在(少一个 = 截断回归) + 3. LIST 的完成回包是 226 而不是 550(ftplib 在非 2xx 完成时会抛异常,正是这条) +清理:无论成败都逐个 DELE;删除失败会打 WARN(不影响判定,但会留残留文件要手清)。 + +用法:python ftp_list_fill_test.py [--host 192.168.1.100] [--files 26] + [--user admin] [--password admin] + 注:--files 26 是按每行约 63 B 反推的下限(26×63 ≈ 1638 > 1536);目录本来就有 + 条目时更小也能触发,取 26 保证空目录也能越过一块。 +""" + +import argparse +import io +import sys +from ftplib import FTP, all_errors + +HOST = "192.168.1.100" +PORT = 21 +# 设备无匿名通道:账号取 /config.ini 的 [ftp],出厂默认 admin/admin +USER = "admin" +PASSWORD = "admin" +PREFIX = "ZZFT" +PAYLOAD = b"filler\r\n" + + +def names_of(count): + # FatFs 只支持严格 8.3(FF_USE_LFN=0):8 字符主名 + 3 字符扩展名 + return ["%s%02d.TXT" % (PREFIX, i) for i in range(count)] + + +def main(): + ap = argparse.ArgumentParser(description="FTP LIST completeness regression") + ap.add_argument("--host", default=HOST) + ap.add_argument("--port", type=int, default=PORT) + ap.add_argument("--files", type=int, default=26, + help="how many probe files to create (default 26, see docstring)") + ap.add_argument("--user", default=USER) + ap.add_argument("--password", default=PASSWORD) + args = ap.parse_args() + + names = names_of(args.files) + stored = [] + checks = [] + ftp = FTP() + try: + ftp.connect(args.host, args.port, timeout=15) + # connect() 已经把 220 欢迎读走了(ftplib 语义),这里不能再 getresp() + print("login:", ftp.login(args.user, args.password)) + ftp.voidcmd("TYPE I") + + print("\n[1] STOR %d 个探针文件" % args.files) + for name in names: + try: + ftp.storbinary("STOR " + name, io.BytesIO(PAYLOAD)) + stored.append(name) + except all_errors as exc: + checks.append(("STOR " + name, False, str(exc)[:60])) + break + if len(stored) == args.files: + checks.append(("STOR all %d files" % args.files, True, "ok")) + + print("[2] LIST 全文校验") + lines = [] + trunc_err = None + try: + ftp.retrlines("LIST", lines.append) + except all_errors as exc: + # 完成回包不是 2xx(老 bug 的 550 就走这里) + trunc_err = str(exc) + listed = " ".join(lines).upper() + print(" 收到 %d 行 / %d 字节" % (len(lines), sum(len(x) + 2 for x in lines))) + checks.append(("LIST 完成回包 226(非 550)", trunc_err is None, + trunc_err or "ok")) + missing = [n for n in stored if n.upper() not in listed] + checks.append(("LIST 列出全部 %d 个新文件" % len(stored), not missing, + "缺 " + ",".join(missing) if missing else "ok")) + except all_errors as exc: + checks.append(("会话", False, str(exc)[:80])) + finally: + print("[3] 清理") + left = 0 + for name in stored: + try: + ftp.delete(name) + except all_errors as exc: + left += 1 + print(" WARN: DELE %s 失败: %s" % (name, str(exc)[:50])) + if left: + print(" WARN: %d 个残留文件需手工清" % left) + try: + ftp.quit() + except all_errors: + pass + + print("\n" + "=" * 44) + ok_all = True + for label, ok, detail in checks: + ok_all = ok_all and ok + print(" [%s] %s%s" % ("PASS" if ok else "FAIL", label, + "" if ok or not detail else " - " + detail)) + print("=" * 44) + print(">>> TEST PASSED <<<" if ok_all else ">>> TEST FAILED <<<") + return 0 if ok_all else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/arch_diagram/gen_memory_svg.py b/tools/arch_diagram/gen_memory_svg.py index 5b4cb294..b40cecf6 100644 --- a/tools/arch_diagram/gen_memory_svg.py +++ b/tools/arch_diagram/gen_memory_svg.py @@ -47,11 +47,11 @@ RAM2_CATEGORIES = [ # ── Flash breakdown (1MB = 1,048,576 B; 2026-10-22 @build) ────────────────── FLASH_TOTAL = 1048576 -FLASH_CODE = 213264 +FLASH_CODE = 213436 FLASH_RO = 18652 FLASH_RW = 1188 FLASH_USED = FLASH_CODE + FLASH_RO + FLASH_RW # ROM 合计(含 RW 初始化表) -FLASH_IROM = FLASH_CODE + FLASH_RO # ER_IROM1 区用量 = 231,916 B(22.1%) +FLASH_IROM = FLASH_CODE + FLASH_RO # ER_IROM1 区用量 = 232,088 B(22.1%) FLASH_FREE = FLASH_TOTAL - FLASH_USED FLASH_CATEGORIES = [ @@ -61,12 +61,12 @@ FLASH_CATEGORIES = [ ("Flash 空闲", FLASH_FREE, "#E0E0E0"), ] -# Flash Code+RO 分布(= ER_IROM1 231,916 B;2026-10-22 按 .map 对象表逐对象归类, +# Flash Code+RO 分布(= ER_IROM1 232,088 B;2026-10-22 按 .map 对象表逐对象归类, # 口径同 data/ram_report_20261022.md;"其他"含链接器填充) FLASH_CODE_SUB = [ ("LVGL 库", 87706, "#4CAF50"), ("应用代码/UI", 57011, "#2196F3"), - ("CH395F+FTP", 25744, "#F44336"), + ("CH395F+FTP", 25916, "#F44336"), ("HAL 驱动", 17876, "#607D8B"), ("dhara / FTL", 10931, "#FF9800"), ("FreeRTOS 内核", 10762, "#00BCD4"),