fix(lftpd): LIST/NLST 满块续发——大目录不再被静默截断并回 550

现象:`ftp_test.py` 的 `LIST 150->226` 断言失败——1512 B 数据全收到了,完成回包却是 **550**。

**根因**(`send_list`/`send_nlst` 各一处):定案三把"整份目录一次性批量发送"落进 static 1536 B
缓冲时,顺手留下了"装满就 `break` 并回 −1",`cmd_list` 据此回 550。于是目录一旦超过一块,客户端
拿到的是**静默截断**的清单**外加一个错误码**——而清单看起来完全像"目录里就这些"。这比单纯报错
更危险:没有人会去数条目个数。

**台架实测(同一台设备、同一个根目录,改动前后各测一次)**:
· 改前:1512 B / **24 条目** / 完成回包 **550**(停在 A0007.BIN,后面 6 个文件被吞)
· 改后:1880 B / **30 条目** / 完成回包 **226**

**修法:满块续发**,而不是把缓冲区加大。新增 `list_batch_flush()`——装满一块就整块发出、清空
继续装,收尾再发最后一块。为什么仍要"批量":定案三的本体是实测结论——同一连接上背靠背 ~60 B
小段写会毒化 CH395F 的 TX(芯片报 SEND_OK 但对端全丢,无 RST 无 DISCONNECT),而 RETR 的 1 KB
满块连发安全;所以这里只发大块,绝不退回"每行一发"。单行真装不下时**明确失败**而不是少发条目
(8.3 短名 + 定长格式远小于 1536,真发生说明格式被改坏)。两条出口的 `lftpd_io_closedir` 收敛到
循环后单点:卷锁持有至 closedir,任何提前 return 都会漏锁。

**防回退**(关键):修完之后,**目录清空的设备上这颗雷再也暴露不出来**,所以补了
`test/host/ftp_list_fill_test.py`——自造 26 个 8.3 短名文件把清单撑过一块,三条判据(STOR 全 226
/LIST 全文里每个名字都在/完成回包 226 而非 550,用 ftplib `retrlines` 让非 2xx 完成直接抛),
跑完逐个 DELE 还原。真机实测 `收到 56 行 / 3544 字节`(跨 3 块,满块续发被真正走到)→ 三条全
PASS。用法登记进 `docs/测试说明/FTP_Test_Guide.md` §4.5,机理与增补写进
`docs/问题记录/CH395F_Trap_Records.md` 定案三。

**顺带记录的已知偏差(未修)**:`cmd_list`/`cmd_nlst` 忽略命令参数、恒列会话 CWD
(`client->directory`)。主流客户端都是 `CWD` + 裸 `LIST`,实际影响小,先记在案。

体量:`Code` 213,264 → **213,436 B(+172)**、RO 不变 → `ER_IROM1` **232,088 B(22.1%)
  **、
`ROM 合计` 233,276 B;**RAM 一字未动**(`RW_IRAM1` 75,264 B 57.4% / `RW_CCM` 57,376 B
  87.9%)。
数字联动照做:`data/ram_report` 重跑、`ram_baseline --update/--write`、
  `check_mem_numbers --write`、
FLASH 配图常量重算重生成(桶合计 = 232,088 与 `Code+RO` 相符)、报告 §1.3/§5.1/§5.4 与部署 §3.3。

门禁:`@build` 0 错 0 警告;`@test` PASS(692 ms);`ftp_test.py` → **TEST
  PASSED**(含此前失败
的 `LIST 150->226`);`ftp_up_dl_test.py` → TEST PASSED;`ftp_list_fill_test.py`
  → TEST PASSED;
`doc-links --all` 146 文档 0 问题;`id-check --strict` 增量拦截通过(可判定 82.82%);
`mem-numbers` / `mem-budget` 通过;改动文件字节卫生 BOM 无 / CR 0 / U+FFFD 0 / 无行尾空格。未推送。
This commit is contained in:
2026-10-22 22:41:31 +08:00
parent 4c21cff753
commit 8b0ea17b71
12 changed files with 262 additions and 74 deletions
+80 -26
View File
@@ -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)
+23 -23
View File
@@ -8,34 +8,34 @@
</style>
</defs>
<text x="410.0" y="24" text-anchor="middle" class="title">Flash (1MB) 使用布局 — 2026-10-22 @build</text>
<rect class="bar-seg" x="60" y="50" width="150.5" height="40" fill="#2196F3" rx="2"/>
<text x="135.3" y="70.0" text-anchor="middle" dominant-baseline="central" class="label" fill="#000">代码段 (Code) 208.3KB</text>
<rect class="bar-seg" x="210.50445556640625" y="50" width="13.2" height="40" fill="#4CAF50" rx="2"/>
<text x="227.7" y="70.0" text-anchor="start" dominant-baseline="central" class="legend-text" fill="#333">只读数据 (RO) 18.2KB</text>
<rect class="bar-seg" x="223.6675262451172" y="50" width="575.5" height="40" fill="#E0E0E0" rx="2"/>
<text x="511.4" y="70.0" text-anchor="middle" dominant-baseline="central" class="label" fill="#000">Flash 空闲 796.4KB</text>
<text x="60" y="108" class="label" fill="#333">已用: 227.6KB (22.2%) 空闲: 796.4KB (77.8%) ER_IROM1(Code+RO): 226.5KB (22.1%) 总量: 1MB (= ROM 合计口径)</text>
<text x="60" y="124" class="label" fill="#555">Code + RO 分布(ER_IROM1 226.5KB,按 .map 对象归类)</text>
<rect class="bar-seg" x="60" y="130" width="279.9" height="24" fill="#4CAF50" rx="2"/>
<text x="199.9" y="142.0" text-anchor="middle" dominant-baseline="central" class="label" fill="#000">85.7KB</text>
<rect class="bar-seg" x="339.8532227185705" y="130" width="181.9" height="24" fill="#2196F3" rx="2"/>
<text x="430.8" y="142.0" text-anchor="middle" dominant-baseline="central" class="label" fill="#000">55.7KB</text>
<rect class="bar-seg" x="521.7645181876196" y="130" width="82.1" height="24" fill="#F44336" rx="2"/>
<text x="562.8" y="142.0" text-anchor="middle" dominant-baseline="central" class="label" fill="#000">25.1KB</text>
<rect class="bar-seg" x="603.9087428206764" y="130" width="57.0" height="24" fill="#607D8B" rx="2"/>
<text x="632.4" y="142.0" text-anchor="middle" dominant-baseline="central" class="label" fill="#000">17.5KB</text>
<rect class="bar-seg" x="660.9476707083599" y="130" width="34.9" height="24" fill="#FF9800" rx="2"/>
<rect class="bar-seg" x="695.8264199106573" y="130" width="34.3" height="24" fill="#00BCD4" rx="2"/>
<rect class="bar-seg" x="730.1659221442246" y="130" width="27.0" height="24" fill="#9C27B0" rx="2"/>
<rect class="bar-seg" x="757.1857051691129" y="130" width="17.5" height="24" fill="#795548" rx="2"/>
<rect class="bar-seg" x="774.6904913848117" y="130" width="1.5" height="24" fill="#B0BEC5" rx="2"/>
<rect class="bar-seg" x="776.1837906828334" y="130" width="23.8" height="24" fill="#E0E0E0" rx="2"/>
<rect class="bar-seg" x="60" y="50" width="150.6" height="40" fill="#2196F3" rx="2"/>
<text x="135.3" y="70.0" text-anchor="middle" dominant-baseline="central" class="label" fill="#000">代码段 (Code) 208.4KB</text>
<rect class="bar-seg" x="210.62583923339844" y="50" width="13.2" height="40" fill="#4CAF50" rx="2"/>
<text x="227.8" y="70.0" text-anchor="start" dominant-baseline="central" class="legend-text" fill="#333">只读数据 (RO) 18.2KB</text>
<rect class="bar-seg" x="223.78890991210938" y="50" width="575.4" height="40" fill="#E0E0E0" rx="2"/>
<text x="511.5" y="70.0" text-anchor="middle" dominant-baseline="central" class="label" fill="#000">Flash 空闲 796.2KB</text>
<text x="60" y="108" class="label" fill="#333">已用: 227.8KB (22.2%) 空闲: 796.2KB (77.8%) ER_IROM1(Code+RO): 226.6KB (22.1%) 总量: 1MB (= ROM 合计口径)</text>
<text x="60" y="124" class="label" fill="#555">Code + RO 分布(ER_IROM1 226.6KB,按 .map 对象归类)</text>
<rect class="bar-seg" x="60" y="130" width="279.6" height="24" fill="#4CAF50" rx="2"/>
<text x="199.8" y="142.0" text-anchor="middle" dominant-baseline="central" class="label" fill="#000">85.7KB</text>
<rect class="bar-seg" x="339.64582399779397" y="130" width="181.8" height="24" fill="#2196F3" rx="2"/>
<text x="430.5" y="142.0" text-anchor="middle" dominant-baseline="central" class="label" fill="#000">55.7KB</text>
<rect class="bar-seg" x="521.4223053324602" y="130" width="82.6" height="24" fill="#F44336" rx="2"/>
<text x="562.7" y="142.0" text-anchor="middle" dominant-baseline="central" class="label" fill="#000">25.3KB</text>
<rect class="bar-seg" x="604.0540656992176" y="130" width="57.0" height="24" fill="#607D8B" rx="2"/>
<text x="632.6" y="142.0" text-anchor="middle" dominant-baseline="central" class="label" fill="#000">17.5KB</text>
<rect class="bar-seg" x="661.0507221398781" y="130" width="34.9" height="24" fill="#FF9800" rx="2"/>
<rect class="bar-seg" x="695.9036227637794" y="130" width="34.3" height="24" fill="#00BCD4" rx="2"/>
<rect class="bar-seg" x="730.2176760539107" y="130" width="27.0" height="24" fill="#9C27B0" rx="2"/>
<rect class="bar-seg" x="757.2174347661233" y="130" width="17.5" height="24" fill="#795548" rx="2"/>
<rect class="bar-seg" x="774.709248216194" y="130" width="1.5" height="24" fill="#B0BEC5" rx="2"/>
<rect class="bar-seg" x="776.2014408327877" y="130" width="23.8" height="24" fill="#E0E0E0" rx="2"/>
<rect x="60" y="167" width="10" height="10" fill="#4CAF50" rx="2"/>
<text x="74" y="176" class="legend-text" fill="#333">LVGL 库 85.7KB</text>
<rect x="255" y="167" width="10" height="10" fill="#2196F3" rx="2"/>
<text x="269" y="176" class="legend-text" fill="#333">应用代码/UI 55.7KB</text>
<rect x="450" y="167" width="10" height="10" fill="#F44336" rx="2"/>
<text x="464" y="176" class="legend-text" fill="#333">CH395F+FTP 25.1KB</text>
<text x="464" y="176" class="legend-text" fill="#333">CH395F+FTP 25.3KB</text>
<rect x="645" y="167" width="10" height="10" fill="#607D8B" rx="2"/>
<text x="659" y="176" class="legend-text" fill="#333">HAL 驱动 17.5KB</text>
<rect x="60" y="183" width="10" height="10" fill="#FF9800" rx="2"/>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

@@ -38,10 +38,10 @@
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
| 区域 | 用量 | 容量 | 占比 |
|---|---|---|---|
| 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%** |
<!-- MEM_SNAPSHOT:END -->
各区的容量与常量来源:
+1 -1
View File
@@ -15,7 +15,7 @@
<!-- MEM_BUDGET:BEGIN generated by scripts/ram_baseline.py -->
| 区域 | 用量 | 容量 | 占比 | 门禁(≤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 |
+11 -11
View File
@@ -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**
+14
View File
@@ -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 客户端手测(可选)
@@ -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% 拦提交 |
+1 -1
View File
@@ -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`(主分支存档)。
@@ -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 的 `<FilePath>` 计数(今日 **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 内存治理长效机制(常开,不受暂缓影响)
+1 -1
View File
@@ -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
1 date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi
4 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
5 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
6 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
7 2026-10-22 213264 213436 18652 1188 131500 231916 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
+120
View File
@@ -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())
+4 -4
View File
@@ -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"),