From 4c21cff753c7039aa60bddd3f5b389438ba126c8 Mon Sep 17 00:00:00 2001
From: Wanderingss <1624155937@qq.com>
Date: Thu, 22 Oct 2026 22:31:09 +0800
Subject: [PATCH] =?UTF-8?q?fix(lftpd):=20=E5=AF=B9=E7=AB=AF=E8=A1=8C?=
=?UTF-8?q?=E4=B8=BA=E4=B8=8D=E5=86=8D=E6=89=93=20[ERR]=E2=80=94=E2=80=94?=
=?UTF-8?q?=E6=96=B0=E5=A2=9E=20lftpd=5Fpeer=20=E7=BA=A7=E5=88=AB=E5=B9=B6?=
=?UTF-8?q?=E5=88=92=E6=B8=85=E7=BA=A7=E5=88=AB=E8=BE=B9=E7=95=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
现象:抓自检台账基线那次 FTP 会话一结束,设备就打 `[ERR] [FTP] error reading next command`,
本次 `@test` 直接 FAIL(Forbidden 含 `[ERR]`)。查下去发现 lftpd 把**全部**异常路径一律按
error 级打,其中九处其实由外部输入决定:客户端关窗口、连上就走、PASV 后不建数据连接、
运维改 IP 触发监听重建。两条代价:① 任何人"连 21 端口一下就断开"就能把一次启动测试打成
FAIL;② 同样的动作可以**无限制造固件级 ERROR 日志**——日志面与门禁面都可被外部驱动,
工业现场过不了审。
**做法**:`private/lftpd_log.h` 新增 `lftpd_log_peer()`(info 级,带 `[FTP]` 前缀),而不是
就地改成 `DBG_INFO`——让"谁才算真故障"在读代码时一目了然,也防止后来者顺手把对端行为升回
error。降级的九处:控制通道被对端关闭(原 `error reading next command`,改为
`control channel closed by peer (ret=%d)`,措辞原先把正常收尾写成故障、误导过排查方向)、
发送 220 前对端已走、`listener rebuilt`(网络重建,原因由 accept 层自己记)、四处
`data accept failed`(PASV 后没来数据连接)、`accept timeout`。
**刻意留在 error 的**:create/bind/listen/取端口失败、写通道失败、文件打开失败、
`DOWNLOAD/UPLOAD FAILED`(对端中途退出确实让一次传输没走完 = 数据完整性事件,
`../问题记录/CH395F_Trap_Records.md` 的中止家族要靠这些锚点定位)、
`data listener disarmed`(控制连接还活着而我方数据监听已不在服役 = 本侧异常,就地注明)。
**实测划清的边界**:客户端**干净收尾**(发 `QUIT` 或 FIN 半关)现在整条链路没有任何 `[ERR]`;
但 **RST 硬拆**(`SO_LINGER` 清零 / 进程被杀 / 拔线)仍会留一条 `[ERR][NET] sockN TIMEOUT`——
那是 net 层的套接字超时锚点,网络自愈架构判据要读它,不在本次降级范围。所以"采集窗口内别跑
`data listener disarmed`(控制连接还活着而我方数据监听已不在服役 = 本侧异常,就地注明)。
**真机验证**:`@build` 0 错 0 警告;`@test --require-selftest-pass --device-ip` →
**PASS**
(基线 29 条 → 本次无新增;`net init: OK` 726 ms);专门写了 RST 复现脚本(登录后 `SO_LINGER`
清零直接关)→ 串口得到 `[FTP] control channel closed by peer (ret=-1)`,**不再是 `[ERR]` 行**;
同一份日志里 QUIT 收尾的那次会话完全没有错误行。
体量:`Code` 213,236 → **213,264 B(+28)**、`RO` 18,580 → **18,652 B(+72)**(措辞变长),
同一份日志里 QUIT 收尾的那次会话完全没有错误行。
`ZI` 131,500、`RW_IRAM1` 75,264 B 57.4% / `RW_CCM` 57,376 B 87.9% 全不变)。数字联动照做:
两份 `data/` 快照、`ram_baseline --update/--write`、`check_mem_numbers --write`、FLASH
配图
常量重算重生成(桶合计 = 231,916 与 `Code+RO` 相符;越界复查 0)、报告 §1.3/§5.1/§5.4 与
部署文档 §3.3 同步。
口径落文:新增 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.3「FTP 日志级别划分:对端行为
vs 固件故障」(含两类的判据表与边界),并更新 `docs/测试说明/自动化测试流程.md` 的采集窗口警示。
顺手把 `private/lftpd_log.h` 工作区的 CRLF 混用归一回 LF(仓库里 blob 本来就是 LF,diff 无影响)。
门禁:`doc-links --all --sections-strict` 146 文档 0 问题、`id-check --strict` 增量拦截通过
(可判定 82.82%)、`mem-numbers` / `mem-budget` / `layer-deps` 全绿;改动文件字节卫生 BOM 无 /
CR 0 / U+FFFD 0 / 无行尾空格。未推送。
---
Drivers/BSP/NET/lftpd/lftpd.c | 20 ++++++----
Drivers/BSP/NET/lftpd/lftpd_inet.c | 8 +++-
Drivers/BSP/NET/lftpd/private/lftpd_log.h | 15 ++++++++
docs/images/内存布局_FLASH.svg | 42 ++++++++++-----------
docs/内存管理/RAM使用分析与优化方案.md | 4 +-
docs/内存管理/RAM预算台账.md | 2 +-
docs/内存管理/data/ram_report_20261022.md | 22 +++++------
docs/内存管理/data/stack_report_20261022.md | 8 ++--
docs/架构设计/STM32F4-Base存储架构说明.md | 28 ++++++++++++++
docs/测试说明/自动化测试流程.md | 10 +++--
docs/环境与部署/项目部署环境要求.md | 2 +-
docs/问题记录/项目审查与优化建议报告.md | 8 ++--
scripts/ram_baseline.tsv | 2 +-
tools/arch_diagram/gen_memory_svg.py | 10 ++---
14 files changed, 118 insertions(+), 63 deletions(-)
diff --git a/Drivers/BSP/NET/lftpd/lftpd.c b/Drivers/BSP/NET/lftpd/lftpd.c
index d3719416..32e8d446 100644
--- a/Drivers/BSP/NET/lftpd/lftpd.c
+++ b/Drivers/BSP/NET/lftpd/lftpd.c
@@ -368,7 +368,8 @@ static int cmd_list(lftpd_client_t *client, const char *arg)
send_simple_response(client->socket, 150, STATUS_150);
int data_sock = lftpd_inet_accept_timeout(client->data_socket, 5000);
if (data_sock < 0) {
- lftpd_log_error("%s accept failed, sk=%d", __func__, client->data_socket);
+ lftpd_log_peer("%s data accept failed (peer gone?), sk=%d",
+ __func__, client->data_socket);
send_simple_response(client->socket, 425, STATUS_425);
lftpd_inet_close(client->data_socket);
client->data_socket = -1;
@@ -400,7 +401,8 @@ static int cmd_nlst(lftpd_client_t *client, const char *arg)
send_simple_response(client->socket, 150, STATUS_150);
int data_sock = lftpd_inet_accept_timeout(client->data_socket, 5000);
if (data_sock < 0) {
- lftpd_log_error("%s accept failed, sk=%d", __func__, client->data_socket);
+ lftpd_log_peer("%s data accept failed (peer gone?), sk=%d",
+ __func__, client->data_socket);
send_simple_response(client->socket, 425, STATUS_425);
lftpd_inet_close(client->data_socket);
client->data_socket = -1;
@@ -526,7 +528,8 @@ static int cmd_retr(lftpd_client_t *client, const char *arg)
send_simple_response(client->socket, 150, STATUS_150);
int data_sock = lftpd_inet_accept_timeout(client->data_socket, 5000);
if (data_sock < 0) {
- lftpd_log_error("%s accept failed, sk=%d", __func__, client->data_socket);
+ lftpd_log_peer("%s data accept failed (peer gone?), sk=%d",
+ __func__, client->data_socket);
send_simple_response(client->socket, 425, STATUS_425);
lftpd_inet_close(client->data_socket);
client->data_socket = -1;
@@ -593,7 +596,8 @@ static int cmd_stor(lftpd_client_t *client, const char *arg)
send_simple_response(client->socket, 150, STATUS_150);
int data_sock = lftpd_inet_accept_timeout(client->data_socket, 5000);
if (data_sock < 0) {
- lftpd_log_error("%s accept failed, sk=%d", __func__, client->data_socket);
+ lftpd_log_peer("%s data accept failed (peer gone?), sk=%d",
+ __func__, client->data_socket);
send_simple_response(client->socket, 425, STATUS_425);
lftpd_inet_close(client->data_socket);
client->data_socket = -1;
@@ -658,7 +662,7 @@ static int handle_control_channel(lftpd_client_t *client)
{
int err = send_simple_response(client->socket, 220, STATUS_220);
if (err != 0) {
- lftpd_log_error("error sending welcome message");
+ lftpd_log_peer("peer gone before welcome acked");
goto cleanup;
}
@@ -668,7 +672,9 @@ static int handle_control_channel(lftpd_client_t *client)
int line_len = lftpd_inet_read_line(client->socket, read_buffer,
sizeof(read_buffer));
if (line_len != 0) {
- lftpd_log_error("error reading next command");
+ /* 绝大多数是客户端正常收尾(关窗口 / QUIT 前先断了 TCP);
+ * 真读错也由返回值区分得出来,留 ret=%d 便于排障 */
+ lftpd_log_peer("control channel closed by peer (ret=%d)", line_len);
goto cleanup;
}
@@ -786,7 +792,7 @@ int lftpd_start(const char *directory, int port, lftpd_t *lftpd)
int client_socket = lftpd_inet_accept(lftpd->server_socket);
if (client_socket < 0) {
- lftpd_log_error("error accepting client socket");
+ lftpd_log_peer("listener rebuilt (reason logged by accept layer)");
lftpd_inet_close(lftpd->server_socket);
lftpd->server_socket = -1; /* 监听器已不可用(网络重建):下轮重建 */
continue;
diff --git a/Drivers/BSP/NET/lftpd/lftpd_inet.c b/Drivers/BSP/NET/lftpd/lftpd_inet.c
index 7a073ad0..e14a269d 100644
--- a/Drivers/BSP/NET/lftpd/lftpd_inet.c
+++ b/Drivers/BSP/NET/lftpd/lftpd_inet.c
@@ -88,7 +88,8 @@ int lftpd_inet_accept(int socket)
return s;
}
if (net_get_generation() != gen) {
- DBG_ERROR("network re-init: abort accept, relisten");
+ /* 运维改 IP 的正常后果,不是缺陷 → info(级别划分见存储架构说明 §10.3) */
+ DBG_INFO("network re-init: abort accept, relisten");
return -1;
}
lftpd_task_wd_beat(); /* 看门狗:自旋轮次喂心跳(无限等待,可能长时间无客户端) */
@@ -110,6 +111,8 @@ int lftpd_inet_accept_timeout(int socket, int timeout_ms)
* 认"监听器在服役(LISTENING)或待重建且 RX 有残留(closed-with-data 救回窗)",
* 与 :7003 的 N-32 P1 同型修复 */
if (net_listener_armed(socket) == 0) {
+ /* **故意留 error**:控制连接还活着、我方数据监听却已不在服役 →
+ * 这是本侧异常,不是对端行为 */
DBG_ERROR("data listener disarmed, accept aborted");
return -1;
}
@@ -118,7 +121,8 @@ int lftpd_inet_accept_timeout(int socket, int timeout_ms)
elapsed += 100;
}
- DBG_ERROR("accept timeout (%dms)", timeout_ms);
+ /* 客户端连上控制通道却没来建数据连接(中止/被防火墙拦)= 对端行为 → info */
+ DBG_INFO("accept timeout (%dms) - peer never opened data channel", timeout_ms);
return -1;
}
diff --git a/Drivers/BSP/NET/lftpd/private/lftpd_log.h b/Drivers/BSP/NET/lftpd/private/lftpd_log.h
index 07572a96..30d6bf3e 100644
--- a/Drivers/BSP/NET/lftpd/private/lftpd_log.h
+++ b/Drivers/BSP/NET/lftpd/private/lftpd_log.h
@@ -4,4 +4,19 @@
#define lftpd_log_error(format, ...) DBG_ERROR("[FTP] " format, ##__VA_ARGS__)
#define lftpd_log_info(format, ...) DBG_INFO("[FTP] " format, ##__VA_ARGS__)
+/*
+ * 对端行为级别(既不是缺陷,也不该长得像缺陷)——
+ * 控制连接被对端关闭 / 客户端连上就走 / PASV 之后不来数据连接 / 网络重建导致
+ * 监听重建,这些都是**外部输入决定的正常收尾**。它们原先记 error 级,代价实测:
+ * ① 本仓 @test 的 Forbidden 标记含 `[ERR]`(它同时是四个 Fault 崩溃日志的前缀),
+ * 于是"FileZilla 关了个窗口"就能把一次启动测试判 FAIL;② 任何人对着 21 端口
+ * 连一下就走,就能无限制造固件级 ERROR 日志(日志面 + 门禁面都能被外部驱动)。
+ * 所以这一类降为 info,并单独起一个名字,让"谁是真故障"在读代码时就一目了然。
+ * 完整划分表 → docs/架构设计/STM32F4-Base存储架构说明.md §10.3。
+ *
+ * ⚠ **传输未完成不在这一级**:`DOWNLOAD/UPLOAD FAILED` 仍记 error——对端中途退出
+ * 确实让一次传输没走完,那是数据完整性事件,值得留痕。
+ */
+#define lftpd_log_peer(format, ...) DBG_INFO("[FTP] " format, ##__VA_ARGS__)
+
#define lftpd_log_debug(format, ...)
diff --git a/docs/images/内存布局_FLASH.svg b/docs/images/内存布局_FLASH.svg
index cbf7e988..cb1afc26 100644
--- a/docs/images/内存布局_FLASH.svg
+++ b/docs/images/内存布局_FLASH.svg
@@ -9,33 +9,33 @@
Flash (1MB) 使用布局 — 2026-10-22 @build
- 代码段 (Code) 208.2KB
-
- 只读数据 (RO) 18.1KB
-
- Flash 空闲 796.5KB
-已用: 227.5KB (22.2%) 空闲: 796.5KB (77.8%) ER_IROM1(Code+RO): 226.4KB (22.1%) 总量: 1MB (= ROM 合计口径)
-Code + RO 分布(ER_IROM1 226.4KB,按 .map 对象归类)
-
- 85.7KB
-
- 55.7KB
-
- 25.0KB
-
+ 代码段 (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
-
-
-
-
-
-
+
+
+
+
+
+
LVGL 库 85.7KB
应用代码/UI 55.7KB
- CH395F+FTP 25.0KB
+ CH395F+FTP 25.1KB
HAL 驱动 17.5KB
diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md
index 337f22fc..9d829319 100644
--- a/docs/内存管理/RAM使用分析与优化方案.md
+++ b/docs/内存管理/RAM使用分析与优化方案.md
@@ -38,10 +38,10 @@
| 区域 | 用量 | 容量 | 占比 |
|---|---|---|---|
-| ER_IROM1 | 231,816 B (226.38 KB) | 1,048,576 B (1024 KB) | 22.1% |
+| ER_IROM1 | 231,916 B (226.48 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,004 B (227.5 KB)** | 1,048,576 B (1024 KB) | **22.2%** |
+| **Flash (ROM 合计)** | **233,104 B (227.6 KB)** | 1,048,576 B (1024 KB) | **22.2%** |
各区的容量与常量来源:
diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md
index ef2c17ce..95aef43c 100644
--- a/docs/内存管理/RAM预算台账.md
+++ b/docs/内存管理/RAM预算台账.md
@@ -15,7 +15,7 @@
| 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) |
|---|---|---|---|---|
-| ER_IROM1 (Flash) | 231,816 B | 1,048,576 B | 22.1% | 参考 |
+| ER_IROM1 (Flash) | 231,916 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 5fd008e2..d8df039c 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 19:56 Map: `STM32F407-Demo.map`
+> Generated: 2026-10-22 22:26 Map: `STM32F407-Demo.map`
## 1. Grand Totals (全局总量)
| Metric | Value |
|---|---|
-| Code (inc. data) | 213,236 B (208.2 KB) |
-| RO Data | 18,580 B (18.1 KB) |
+| Code (inc. data) | 213,264 B (208.3 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,816 B (226.4 KB)** |
+| **Total RO (Code + RO)** | **231,916 B (226.5 KB)** |
| **Total RW (RW + ZI) = SRAM used** | **132,688 B (129.58 KB)** |
-| **Total ROM (Code + RO + RW init)** | **233,004 B (227.5 KB)** |
+| **Total ROM (Code + RO + RW init)** | **233,104 B (227.6 KB)** |
## 2. 执行区水位 (Execution Regions)
| Region | Base | Size | Max | Util% |
|---|---|---|---|---|
-| ER_IROM1 | 0x08000000 | 231,816 B (226.38 KB) | 1,048,576 B (1024 KB) | 22.1% |
+| ER_IROM1 | 0x08000000 | 231,916 B (226.48 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,108 | 509 | 160 | 7,724 | 7,884 |
+| 3 | lftpd.o | 6,180 | 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,7 +204,7 @@
| 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,236** | **18,580** | **1,188** | **131,500** | **132,688** |
+| | **All .o files** | **213,264** | **18,652** | **1,188** | **131,500** | **132,688** |
## 5. Flash (Code + RO Data) Top-30(按大小降序)
@@ -212,7 +212,7 @@
|---|---|---|---|---|
| 1 | ff.o | 7,962 | 282 | 8,244 |
| 2 | cfg_file.o | 5,970 | 810 | 6,780 |
-| 3 | lftpd.o | 6,108 | 509 | 6,617 |
+| 3 | lftpd.o | 6,180 | 509 | 6,689 |
| 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,236** | **18,580** | **231,816** |
+| | **All .o files** | **213,264** | **18,652** | **231,916** |
-> 自洽校验:per-file 213,236/18,580/1,188/131,500 vs Grand Totals 213,236/18,580/1,188/131,500(CodeΔ=+0 / ROΔ=+0 / RWΔ=+0 / ZIΔ=+0;精确一致(含 '('incl. Generated)'-'(incl. Padding)' 台账行))→ **PASS**
+> 自洽校验: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**
diff --git a/docs/内存管理/data/stack_report_20261022.md b/docs/内存管理/data/stack_report_20261022.md
index c2012bfa..eb6dc567 100644
--- a/docs/内存管理/data/stack_report_20261022.md
+++ b/docs/内存管理/data/stack_report_20261022.md
@@ -1,7 +1,7 @@
# Stack budget report
- Source: `MDK-ARM\STM32F407-Demo\STM32F407-Demo.htm` (Keil static call graph)
-- Linker run: Thu Oct 22 19:53:08 2026
+- Linker run: Thu Oct 22 22:25:25 2026
- Global maximum stack usage: **3024 B** (any call tree)
- Runtime watermark: 8 task(s) from 串口采集日志 `.dsh\skills\serial-assistant\scripts\state\serial.log`
@@ -11,12 +11,12 @@ Rule: `margin = cfg - max(static_max_depth, runtime_peak)` must be >= max(1024 B
|---|---|---|---|---|---|---|---|---|
| defaultTask | `StartDefaultTask` | 5120 | 3024 | 2152 | 3024 | 2096 | 40% | PASS |
| netTask | `StartNetTask` | 3840 | 1912 | 1116 | 1912 | 1928 | 50% | PASS |
-| ftpTask | `StartFtpTask` | 4096 | 1456 | 1404 | 1456 | 2640 | 64% | PASS |
+| ftpTask | `StartFtpTask` | 4096 | 1456 | 740 | 1456 | 2640 | 64% | PASS |
| uiTask | `StartUiTask` | 16384 | 1336 | 4052 | 4052 | 12332 | 75% | PASS |
| testTask | `start_test_task` | 8192 | 736 | - | 736 | 7456 | 91% | PASS |
-| timeSyncTask | `time_sync_task` | 2560 | 1264 | 648 | 1264 | 1296 | 50% | PASS |
+| timeSyncTask | `time_sync_task` | 2560 | 1264 | 616 | 1264 | 1296 | 50% | PASS |
| adcTask | `adc_task_func` | 2560 | 1320 | 952 | 1320 | 1240 | 48% | PASS |
-| iec104Task | `iec104_serv_task` | 2560 | 1456 | 540 | 1456 | 1104 | 43% | PASS |
+| iec104Task | `iec104_serv_task` | 2560 | 1456 | 580 | 1456 | 1104 | 43% | PASS |
| protectTask | `protect_task` | 2048 | 872 | 828 | 872 | 1176 | 57% | PASS |
`Static max` = armlink worst-case call-tree depth; `Runtime peak` = measured high-water mark (boot + whatever workload ran before the sample); `Worst` = max of the two, which is what the verdict is computed from.
diff --git a/docs/架构设计/STM32F4-Base存储架构说明.md b/docs/架构设计/STM32F4-Base存储架构说明.md
index 09137307..3cb5a955 100644
--- a/docs/架构设计/STM32F4-Base存储架构说明.md
+++ b/docs/架构设计/STM32F4-Base存储架构说明.md
@@ -593,3 +593,31 @@ FTP 根目录 = 卷根,lftpd 是卷的网络文件入口(`Drivers/BSP/NET/lf
- **STOR/DELE 写门** — `[ftp] write=0` 时写命令一律 `550` 拒,卷对 FTP 侧只读(`/log`、`/config.ini` 仍可下载不可改写)。
- **全局失败锁定** — 连续 5 次鉴权失败 → 锁定 60 s(跨会话全局、易失、重启清零);纯 lftpd 层状态,不触存储。
- **默认口令告警锚点** — 出厂 `pwd_hash` 在用期间每次启动打 INFO `[FTP] using factory default password`。
+
+### 10.3 FTP 日志级别划分:对端行为 vs 固件故障(2026-10-22)
+
+lftpd 原先把全部异常路径一律按 error 级打(串口上带 `[ERR]`),其中**九处其实由外部输入决
+定**:客户端关窗口、连上就走、PASV 之后不建数据连接、运维改 IP 触发监听重建。代价实测两条:
+
+1. `@test` 的 Forbidden 标记含 `[ERR]`(它同时是四个 Fault 崩溃日志的前缀)——"FileZilla 关了
+ 个窗口"就能把一次启动测试判 FAIL。实测就是这样踩到的:抓自检台账基线那次 FTP 会话结束后,
+ 设备打 `[ERR] [FTP] error reading next command`,本次 `@test` 直接 FAIL。
+2. 任何对 21 端口"连一下就断开"的外部行为,都能**无限制造固件级 ERROR 日志**——日志面与门禁
+ 面都可被外部输入驱动,这在工业现场是过不了审的。
+
+| 级别 | 串口形态 | 覆盖的事件 | 判据 |
+|---|---|---|---|
+| error(`lftpd_log_error` / `DBG_ERROR`) | `[ERR] …` | 建 socket / bind / listen 失败、取端口失败、写通道失败、文件打开失败(读/写/重复打开)、`DOWNLOAD FAILED` / `UPLOAD FAILED`(传输未完成)、`data listener disarmed`(控制连接还活着而我方数据监听已不在服役) | 本侧缺陷,或**数据完整性事件** |
+| peer(`lftpd_log_peer` → info 级) | 普通 INFO 行 | 控制通道被对端关闭(原 `error reading next command`)、发送 220 之前对端已走、`listener rebuilt`(网络重建,原因由 accept 层自己记)、四处 `data accept failed`(PASV 后没来数据连接)、`accept timeout` | **对端决定的正常收尾** |
+
+- **新增专门的 `lftpd_log_peer()` 而不是就地改成 `DBG_INFO`**:让"谁才算真故障"在读代码时
+ 一目了然,也防止后来者顺手把对端行为又升回 error。
+- **"传输未完成"仍留 error**:对端中途退出确实让一次 STOR/RETR 没走完,那是需要留痕的完整性
+ 事件(`../问题记录/CH395F_Trap_Records.md` 的中止家族就靠这些锚点定位)。
+- **边界(实测划清)**:客户端**干净收尾**(发 `QUIT`,或 FIN 半关)现在整条链路上没有任何 `[ERR]`;
+ 但用 **RST 硬拆**(`SO_LINGER` 清零、进程被杀、网线拔了)仍会留一条 `[ERR][NET] sockN TIMEOUT`——
+ 那是 net 层的套接字超时锚点,网络自愈架构的判据要读它,**不在本次降级范围**。所以"采集窗口内
+ 不要跑 FTP"这条规避口径仍然有效,只是不再因为一次正常下载而中弹。
+- 排障信息量不减:peer 级照旧打印,且比原来更准(带 `ret=` / `sk=` / `timeout(ms)`,措辞从
+ "error reading next command"改成"control channel closed by peer"——原先那句把正常收尾
+ 写成了故障,误导过排查方向。
diff --git a/docs/测试说明/自动化测试流程.md b/docs/测试说明/自动化测试流程.md
index fb6b2e01..8739548a 100644
--- a/docs/测试说明/自动化测试流程.md
+++ b/docs/测试说明/自动化测试流程.md
@@ -68,10 +68,12 @@
| 既没有 `device_ip` 也没有 `--selftest-log` | 要求判定却没有数据源 | **FAIL**(同上,不再 WARN 放行) |
| 基线最后一条在台账里找不到 | 512 条环已覆盖基线(更新量 ≥ 环容),无法证明 | **FAIL**(不拿"看不到"当"没有") |
-- ⚠ **采集窗口内不要跑 FTP**:设备会把每条 FTP 命令打到串口,客户端退出时还会多打一行
- `[ERR] [FTP] error reading next command` —— 默认 Forbidden 含 `[ERR]`,于是**任何**人边跑 `@test`
- 边用 FileZilla 传文件,都会让本次莫名 FAIL(本次改造实测踩到:抓基线原先排在采集武装之后,
- 开机耗时被自己的 FTP 会话拉到 ~8 s 并命中 `[ERR]`)。基线抓取因此固定在 open/clear **之前**。
+- ⚠ **采集窗口内仍不要跑 FTP**:每条 FTP 命令都会打到串口上(日志变长、开机耗时指标被污染——
+ 实测抓基线排在采集武装之后时,`net init: OK` 从 612 ms 被拉到 7997 ms)。基线抓取因此固定在
+ `open/clear` **之前**。(原先更要命的是客户端退出会打 `[ERR] [FTP] error reading next command`
+ → 直接命中 Forbidden 里的 `[ERR]`;该措辞与级别已在 2026-10-22 归入"对端行为"级,见
+ `../架构设计/STM32F4-Base存储架构说明.md` §10.3。注意:RST 硬拆仍会留一条 `[ERR][NET] sockN`
+ `TIMEOUT`——那是网络自愈架构要读的套接字超时锚点,不在降级范围内。
- `--selftest-scope all` = 保留旧的累计口径(任一记录即 FAIL),用于**审计**或第一次建基线。
- 给了 `--selftest-log`(本地副本)就没有基线可比 → 自动降级为累计口径并打印一行说明。
- 抓基线同样要 FTP 凭据:`--ftp-user` / `--ftp-pass`(config `ftp_user` / `ftp_pass`,默认出厂
diff --git a/docs/环境与部署/项目部署环境要求.md b/docs/环境与部署/项目部署环境要求.md
index 602b2802..c649bc88 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,816 B(226.4 KB) | 1 MB | 22.1% | ≈804 KB | **充裕**:产品需求无 OTA / 双镜像预算,Flash 不是卡点;死代码裁剪簇已收口 → `docs/问题记录/项目审查与优化建议报告.md` §5.4 |
+| Flash(`ER_IROM1` = Code + RO) | 231,916 B(226.5 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/问题记录/项目审查与优化建议报告.md b/docs/问题记录/项目审查与优化建议报告.md
index 1e6bf7eb..26fa8a1e 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,816 B(22.1%)**;`ROM 合计` 再加 `RW` 初始化表 = 233,004 B(22.2%) |
+| Flash 占用 | `Code + RO-data` = `ER_IROM1` = **231,916 B(22.1%)**;`ROM 合计` 再加 `RW` 初始化表 = 233,104 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,236 B | 213,520 B | 当日累计:F-6 −332 B、SPI 回调归属 +16 B、消息队列收口 +32 B |
-| `ER_IROM1`(Code+RO) | **231,816 B / 22.1%** | 232,280 B / 22.2% | 余 ≈804 KB |
+| `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 |
| `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,816 B)——这是"用 LVGL 画单色菜单"的结构性代价,F-1/F-2/F-3 已省掉 64,576 B。
+(87,706 / 231,916 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 44c05c1e..3e73537e 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 213236 18580 1188 131500 231816 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 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
diff --git a/tools/arch_diagram/gen_memory_svg.py b/tools/arch_diagram/gen_memory_svg.py
index 8d6b3ff9..5b4cb294 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 = 213236
-FLASH_RO = 18580
+FLASH_CODE = 213264
+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,816 B(22.1%)
+FLASH_IROM = FLASH_CODE + FLASH_RO # ER_IROM1 区用量 = 231,916 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,816 B;2026-10-22 按 .map 对象表逐对象归类,
+# Flash Code+RO 分布(= ER_IROM1 231,916 B;2026-10-22 按 .map 对象表逐对象归类,
# 口径同 data/ram_report_20261022.md;"其他"含链接器填充)
FLASH_CODE_SUB = [
("LVGL 库", 87706, "#4CAF50"),
("应用代码/UI", 57011, "#2196F3"),
- ("CH395F+FTP", 25644, "#F44336"),
+ ("CH395F+FTP", 25744, "#F44336"),
("HAL 驱动", 17876, "#607D8B"),
("dhara / FTL", 10931, "#FF9800"),
("FreeRTOS 内核", 10762, "#00BCD4"),