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"),