修复就绪屏障位被消费致FTP永不监听;新增栈可观测(水位探针+静态调用图报告)并补FTP实测

This commit is contained in:
2026-09-21 04:26:52 +08:00
parent 03f175e7ba
commit bbdd059e25
15 changed files with 707 additions and 167 deletions
@@ -131,46 +131,38 @@ void vApplicationMallocFailedHook(void)
成本:每次上下文切换几十周期,不占 RAM。
#### 0.2 新增 `app_mem_stats` 模块(每任务消耗 + 堆余量报告)
#### 0.2 运行时栈水位(已落地:`stack_probe_poll`,2026-09-21)
新文件 `App/util/app_mem_stats.h/.c`(**需手动加入 Keil 工程**,见 AGENTS.md;命名/头文件保护宏遵守代码规范 V1.0):
实现位置:`Src/freertos.c` 的 USER CODE 区(**不新增文件**,无 Keil/CMake 双端注册成本)。
数据源是本版 CMSIS/FreeRTOS **已有**的 API:`osThreadGetStackSpace()`(剩余字节,内部走
`uxTaskGetStackHighWaterMark`,特征值法与 `configCHECK_FOR_STACK_OVERFLOW=2` 同源)。
本版**没有** `osThreadGetStackSize` 实现,也**没有** `ulTaskGetStackDepth` /
`xPortGetTotalHeapSize`,因此配置栈统一取各任务 `*_attributes.stack_size`(唯一定义处,不重复常量)。
```c
/* 核心逻辑(骨架):枚举全部任务 → 逐个取栈配置/高水位 → 报告堆 */
void app_mem_stats_report(const char *tag)
{
osThreadId_t a_ids[16];
uint32_t n = osThreadEnumerate(NULL, a_ids, 16); /* configUSE_OS2_THREAD_ENUMERATE=1 已开 */
DBG_INFO("[MEM][%s] ---- task ram ----", tag);
for (uint32_t i = 0; i < n; i++) {
TaskHandle_t th = xTaskGetHandle(a_ids[i]);
if (th == NULL) continue;
uint32_t total = ulTaskGetStackDepth(th); /* 分配栈,单位 word */
uint32_t hwm = uxTaskGetStackHighWaterMark(th); /* 历史最小剩余,word */
DBG_INFO("[MEM] %-14s stack=%5uB used=%5uB free_min=%5uB",
pcTaskGetName(th), total * 4U,
(total - hwm) * 4U, hwm * 4U);
}
DBG_INFO("[MEM] heap free=%uB total=%uB",
(unsigned)xPortGetFreeHeapSize(), (unsigned)xPortGetTotalHeapSize());
}
行为:挂在 defaultTask 主循环——首次 5s 后报告一次(bring-up 深链 mount/mkfs/探针/cfg/syslog
均已跑过),此后每 2s 采样,**只在出现更深峰值时打印**:既不破坏串口静默窗口,也不漏掉
之后才走到的更深分支(卷修复、配置保存等)。输出纯 ASCII,便于 `@test` 采集:
```
[STACK] <task> peak_used=<B>/<cfg> free_min=<B> (<pct>%)
[STACK] heap free=<B> min_ever=<B>
```
要点:
- `uxTaskGetStackHighWaterMark` 返回**任务创建以来**的最小剩余栈(单位 word=4B),`used = (栈配置 − 高水位)×4` 即该任务历史峰值栈占用。API 已在 `FreeRTOSConfig.h:116` 启用。
- 报告时机:① 启动完成后一次;② 每 5 分钟周期性一次(挂在 defaultTask 的 LED 循环里);③ 关键动作后(FTP 连接/大文件传输、f_mkfs、菜单遍历到最深层级)。
- **测量必须覆盖全功能路径**(高水位是历史最小值,只反映跑过的路径):全菜单遍历、FTP 上传/下载大文件、CH395F 断链重连、NAND 格式化、时间同步。建议跑一轮 `@test` 全回归 + 1 小时浸泡。
- `uxTaskGetStackHighWaterMark` 返回**任务创建以来**的最小剩余栈(单位 word=4B);`used = 配置 − 剩余` 即该任务历史峰值栈占用。
- **测量必须覆盖全功能路径**(高水位是历史最小值,只反映跑过的路径):全菜单遍历、FTP 上传/下载大文件、CH395F 断链重连、NAND 格式化、时间同步。建议 `@test` 全回归 + 1 小时浸泡。
- 未覆盖 idle/timer(内核静态小栈、纯循环,未取句柄);构建期静态侧见 §0.6。
#### 0.3 map 静态占用报告脚本
新增 `scripts/ram_report.ps1`:
脚本 `scripts/ram_report.py`(Python,已随仓库提供):
- 解析 `MDK-ARM/STM32F407-Demo/STM32F407-Demo.map`:
- RW_IRAM1 Region 的 `Size` / `Max` → 主 SRAM 静态水位
- "Image component sizes" 表 → 每 .o 的 ZI/RW 排序 Top20
- 与基线 `scripts/ram_baseline.tsv` 做 diff,任何对象 ZI 增量 >1KB 即高亮
- 用法:每次 `@build` 后运行 `powershell scripts/ram_report.ps1`,把 diff 贴进改动说明
- 用法:`& '.venv\Scripts\python.exe' scripts/ram_report.py -o docs/内存管理/data/ram_report_<日期>.md`,把结果贴进改动说明
- (待补)与基线 TSV 做 diff,任何对象 ZI 增量 >1KB 即高亮
#### 0.4 (可选)运行时间统计
@@ -180,11 +172,75 @@ void app_mem_stats_report(const char *tag)
启动期跑在 startup 的 1KB C 栈上(CubeMX 初始化 + 各驱动 init,含 BBT 全扫描)。ARMCC 可用 `__current_stack_usage()` 在 `app_main_init` 末尾打点,确认 1KB 够用;不够则调 scatter 的 `STACK` 节(与任务栈无关,单独预算)。
#### 0.6 静态栈分析 + 汇总工具(构建期,零目标依赖;2026-09-21 落地)
**UV4 每次构建已自动生成静态调用图** `MDK-ARM/STM32F407-Demo/STM32F407-Demo.htm`
(标题 "Static Call Graph",随 `@build` 刷新,**无需**给 armlink 加 `--callgraph`)。每个函数
条目自带 `Stack size N bytes`(该函数自身帧)与 `Max Depth = N`(其子树最坏累计深度)
以及完整调用链——即"不烧板子也能看到最坏链有多深、多宽"。
**用法:`scripts/stack_report.py`**(纯标准库,任意 python3 均可;解析上述 HTM +
`Src/freertos.c` 的栈配置,带 `-s` 时叠加串口 `[STACK]` 运行时水位):
```powershell
# 最小用法(只看静态:每任务最坏链 + 余量判定)
& '.venv\Scripts\python.exe' scripts/stack_report.py
# 推荐用法(静态 + 实测一起判,判定取两者更坏者;报告归档到 data/)
& '.venv\Scripts\python.exe' scripts/stack_report.py -s tools\serial_tool\state\serial.log `
-o 'docs\内存管理\data\stack_report_<日期>.md'
```
| 参数 | 默认 | 说明 |
|---|---|---|
| `-i/--html` | `MDK-ARM/STM32F407-Demo/STM32F407-Demo.htm` | 静态调用图(Keil 构建产物) |
| `-s/--serial` | 无 | 串口日志,解析其中 `[STACK]` 行(daemon 全量历史:`tools/serial_tool/state/serial.log`) |
| `-o/--output` | 无(打印到 stdout) | 报告输出文件(Markdown) |
| `--isr-margin` | `1024` | ISR 嵌套预留(字节) |
| `--min-free-pct` | `30` | 要求的最小余量百分比 |
- **判定规则**:`余量 = 配置 − max(静态最坏深度, 实测峰值)`,须 `≥ max(1024 B, 配置×30%)`;任一任务不达标 → 脚本退出码 **1**(可挂门禁),全达标 → 0,HTM 缺失 → 2。
- **何时跑**:① 改栈或加大局部变量前后;② 改 FatFs/lftpd/LVGL 等第三方配置后;③ 任务职责变化时。串口日志覆盖的路径越全越准(路径覆盖要求见 §0.2)。
- **报告内容**:每任务预算表(配置/静态/实测/最坏/余量/判定)、各任务最坏调用链、帧宽 Top15(找"宽度大户")、堆 free/min_ever。
首份基线(2026-09-21,产品构建 + FTP 会话实测;产物 `docs/内存管理/data/stack_report_20260921.md`):
| 任务 | 配置 (B) | 静态最坏 (B) | 实测峰值 (B) | 最坏 (B) | 余量 | 判定 |
|---|---|---|---|---|---|---|
| defaultTask | 4096 | **2776** | 2136 | 2776 | 1320 (32%) | PASS |
| **ftpTask** | 4096 | 2528 | **3480** | **3480** | **616 (15%)** | **FAIL** |
| netTask | 4096 | 440 | 600 | 600 | 3496 (85%) | PASS |
| uiTask | 16384 | 912 | 3804 | 3804 | 12580 (76%) | PASS |
| timeSyncTask | 2048 | 336 | 420 | 420 | 1628 (79%) | PASS |
| testTask(产品构建) | 8192 | 168 | 348 | 348 | 7844 (95%) | PASS |
实测堆:`free=8784 B / min_ever=4848 B`(48KB 池)。
**待办(由本次实测直接得出)**:ftpTask 余量仅 616 B(15%),低于 1024 B / 30% 底线。两条路:
① 按 §1.3 定栈规则 `峰值×1.5 + 1KB ≈ 6.2KB` → ftpTask 提到 **1600 words (6400 B)**(最省事);
② 先做 §1.4(lftpd 大局部变量移静态:`handle_control_channel` 1104 B、`send_file`/`receive_file` 1088 B、`send_response` 800 B)再复测,栈只需小步上调。建议 **②→①** 顺序:先砍宽度,避免长期背 6.4KB。
**静态与实测是互补的两套数据(必须都留)**:
- defaultTask:静态 2776 > 实测 2136——静态是**上界**(最深分支未必走到),实测是**已走路径**。**判栈够不够看两者更坏者,做定栈优化看实测。**
- netTask / uiTask / testTask / timeSyncTask / **ftpTask**:实测 > 静态——静态是**下界**(互递归、间接调用、ISR 上下文保存 ~100B 未计入)。ftpTask 最典型:静态 2528 B(控制通道握手链)**低估了 950 B**,真实峰值 3480 B 只在 128KB STOR 传输时暴露 → **不跑真实业务就定栈 = 拍脑袋。**
- 本次可观测性的第一个产出不是数字而是**缺陷**:ftpTask 因就绪位被其它等待者消费而**从未启动**(21 端口一直无监听),修好后才有了上面这组数据——详见 `docs/测试说明/FTP_Test_Guide.md` §8.8。
要点与边界:
- defaultTask 的静态最坏链是**配置保存/旧档迁移**路径,不是首启探针路径:
`net_cfg_load → net_cfg_legacy_migrate → net_cfg_save → cfg_file_save → s_write_template → s_write_doc → cfg_store_write_atomic → f_rename → follow_path → dir_next → create_chain → put_fat → move_window → sync_window → disk_write → ftl_flush_cache → dhara_map_write → prepare_write → auto_gc → dhara_map_gc → ... → trace_path → dhara_journal_read_meta → dhara_nand_read → gd5f_read_from_cache → HAL_SPI_Receive → ... → SPI_WaitFlagStateUntilTimeout`。
**2776 B > 2048 B** → 512 words 栈必然溢出,与 2026-09-21 实测完全一致。
- 帧宽榜直接点名宽度大户(自身帧):`handle_control_channel` 1104 B、`receive_file`/`send_file` 1088 B、`send_response` 800 B、**`s_append` 704 B**(FIL 512 + 记录 64 + 时间戳)、**`StartDefaultTask` 560 B**(探针 `FIL __chk`)、`cfg_store_read`/`cfg_store_write_atomic` 576 B——与 §1.3/§1.4 的整改项一一对得上。
- **静态分析的天然盲区**:递归/互递归(LVGL widget 树、`layout_update_core`、`apply_theme` 等)与间接调用只能按 "Cycle" 近似,armlink 明确标注 `+ Unknown(Cycles, Untraceable Function Pointers)`。因此 uiTask 的 912 B 是**下限**(实测 3804 B),必须靠运行时水位。
- testTask 在产品构建里只是 `osDelay` 空循环(168 B);2048 words 是给**存储套件构建**(`TEST_SUITE_STORAGE`)预留的,测套件时应重跑本脚本看该构建的静态值。
- **测 ftpTask 的复现配方**:设备就绪后跑 `& '.venv\Scripts\python.exe' test/ftp_speed_test.py --size 128`(STOR 128KB + RETR + 逐字节比对),再按上面命令叠加串口日志出报告。**不要用裸 TCP 工具探测 21 端口**——CH395F 单客户端复用,探测会占掉唯一会话(见 FTP 指南 §8.9)。
#### 阶段 0 验收
- 串口可见 `[MEM]` 报告:每任务 `stack/used/free_min` + 堆 free/total
- 串口可见 `[STACK]` 报告:每任务 `peak_used/free_min` + 堆 `free/min_ever`
- 人为制造栈溢出(临时把某任务栈改到 64 字节)能触发 `STACK OVERFLOW` 钩子
- `ram_report.ps1` 能输出 Top20 与基线 diff
- `scripts/ram_report.py` 输出 .map 静态占用 Top20;`scripts/stack_report.py` 输出每任务静态余量(可叠加运行时水位)
### 阶段 1:静态大项削减(收益最大,3~5 天)
@@ -0,0 +1,79 @@
# Stack budget report
- Source: `MDK-ARM\STM32F407-Demo\STM32F407-Demo.htm` (Keil static call graph)
- Linker run: Mon Sep 21 04:08:58 2026
- Global maximum stack usage: **2776 B** (any call tree)
- Runtime watermark: 6 task(s) from `tools\serial_tool\state\serial.log`
Rule: `margin = cfg - max(static_max_depth, runtime_peak)` must be >= max(1024 B ISR, 30% of cfg).
| Task | Entry | Cfg (B) | Static max (B) | Runtime peak (B) | Worst (B) | Margin (B) | Margin % | Verdict |
|---|---|---|---|---|---|---|---|---|
| defaultTask | `StartDefaultTask` | 4096 | 2776 | 2136 | 2776 | 1320 | 32% | PASS |
| netTask | `StartNetTask` | 4096 | 440 | 600 | 600 | 3496 | 85% | PASS |
| ftpTask | `StartFtpTask` | 4096 | 2528 | 3480 | 3480 | 616 | 15% | FAIL |
| uiTask | `StartUiTask` | 16384 | 912 | 3804 | 3804 | 12580 | 76% | PASS |
| testTask | `start_test_task` | 8192 | 168 | 348 | 348 | 7844 | 95% | PASS |
| timeSyncTask | `time_sync_task` | 2048 | 336 | 420 | 420 | 1628 | 79% | 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.
Runtime heap: free=8784 B, min_ever=4848 B
## Worst call chain per task
### defaultTask: 2776 B (own frame 560 B)
```
StartDefaultTask -> net_cfg_load -> net_cfg_legacy_migrate -> net_cfg_save -> cfg_file_save -> s_write_template -> s_write_doc -> cfg_store_write_atomic -> f_rename -> follow_path -> dir_next -> create_chain -> put_fat -> move_window -> sync_window -> disk_write -> ftl_flush_cache -> dhara_map_write -> prepare_write -> auto_gc -> dhara_map_gc -> try_recover -> raw_gc -> trace_path -> dhara_journal_read_meta -> dhara_nand_read -> gd5f_read_from_cache -> HAL_SPI_Receive -> HAL_SPI_TransmitReceive -> SPI_EndRxTxTransaction -> SPI_WaitFlagStateUntilTimeout
```
### netTask: 440 B (own frame 0 B)
```
StartNetTask -> net_task_func -> net_stack_bring_up -> net_init -> ch395f_reset -> sys_clock_get_str -> unix_to_sd2506 -> __aeabi_uldivmod
```
### ftpTask: 2528 B (own frame 24 B)
```
StartFtpTask -> lftpd_start -> handle_control_channel -> send_response -> lftpd_inet_write_string -> net_send -> osMessageQueuePut -> xQueueGenericSend -> xTaskResumeAll -> xTaskIncrementTick
```
### uiTask: 912 B (own frame 0 B)
```
StartUiTask -> ui_task_func -> ui_menu_init -> ui_menu_view_frame_build -> view_apply_sel -> lv_obj_scroll_to_y -> lv_obj_scroll_by_bounded -> lv_obj_update_layout -> layout_update_core -> layout_update_core (Cycle)
```
### testTask: 168 B (own frame 32 B)
```
start_test_task -> sys_clock_get_str -> unix_to_sd2506 -> __aeabi_uldivmod
```
### timeSyncTask: 336 B (own frame 32 B)
```
time_sync_task -> time_sync_handle_conn -> sys_clock_set_unix -> sys_clock_set -> sd2506_set_time -> sd2506_write_enable -> sd2506_write_reg -> HAL_I2C_Mem_Write -> I2C_RequestMemoryWrite -> I2C_WaitOnMasterAddressFlagUntilTimeout
```
## Frame-size ranking (own frame, top 15)
| Bytes | Function |
|---|---|
| 1104 | `handle_control_channel` |
| 1088 | `receive_file` |
| 1088 | `send_file` |
| 800 | `send_response` |
| 704 | `s_append` |
| 624 | `lv_draw_sw_arc` |
| 624 | `draw_indic` |
| 576 | `cfg_store_read` |
| 576 | `cfg_store_write_atomic` |
| 576 | `s_read_raw` |
| 560 | `StartDefaultTask` |
| 560 | `draw_main` |
| 400 | `draw_line_skew` |
| 352 | `lv_draw_sw_triangle` |
| 336 | `lftpd_start` |
+27 -8
View File
@@ -23,7 +23,9 @@
- CH395F 网口经以太网连接到 PC 或同一台交换机。
- PC 网卡与固件 IP **同网段**。固件默认静态 IP **192.168.1.100**(掩码 255.255.255.0);PC 可设 192.168.1.x(如 192.168.1.77)。
- 启动时 `netTask` 完成 `net_init()` + PHY 稳定(约 >2s),`ftpTask` 另有 `osDelay(7000)` 等待后才 `listen`;若 `open_socket` 失败会 `vTaskDelay(3000)` 重试(最多 3 次)。**客户端应在上电约 7s 后、串口出现 `[FTP] waiting for connection...` 再连接。**
- **启动时序(2026-09-21 现行实现,取代旧文档的 `osDelay(7000)`)**:`netTask` 完成 `net_init()` + PHY 稳定后置 `SYS_EVT_NET_READY`;`defaultTask` 挂载卷 + 加载配置后置 `SYS_EVT_STORAGE_READY`;`ftpTask` 以 `osEventFlagsWait(两位, osFlagsWaitAll | osFlagsNoClear)` 等**两位同时就绪**再 `lftpd_start`。串口出现 `[FTP] waiting for connection...` 即可连接(实测:上电后约 0.5~1s,取决于存储挂载耗时)。
- ⚠ **就绪位等待必须带 `osFlagsNoClear`**(2026-09-21 实证,详见 §8.6):`osEventFlagsWait` 默认**消费**所等待的位,若被其它等待者清掉,`ftpTask` 的 `WaitAll(两位)` 永不成立 → FTP 静默不监听。
- **单客户端**:监听 socket 与数据通道复用,同一时刻只服务 1 个会话;上一个会话未 `QUIT` 就断开时,服务端约 30s 后才丢弃旧会话并重新 listen,期间新连接被拒(`WinError 10061`)。脚本已内置连接重试;**不要用裸 TCP 工具探测 21 端口**(探测本身会占掉唯一会话,见 §8.7)。
### 2.2 软件
@@ -46,7 +48,8 @@
## 4. PC 端脚本手测(主要手段)
所有脚本支持 `--host`(默认 `192.168.1.100`)、`--port`(默认 21),在 `test/` 目录运行。
所有脚本支持 `--host`(默认 `192.168.1.100`)、`--port`(默认 21),在 `test/` 目录运行;`ftp_up_dl_test.py` / `ftp_speed_test.py` 内置**连接重试**(单客户端:旧会话未释放时自动等待重试,默认 6 次 × 3s)。
推荐入口:`& '.venv\Scripts\python.exe' test/ftp_up_dl_test.py`(功能闭环:登录/TYPE I/PASV/STOR/LIST/RETR/比对/DELE/QUIT)与 `& '.venv\Scripts\python.exe' test/ftp_speed_test.py --size 128`(大文件 + 吞吐);后者同时是 **ftpTask 栈水位测量**的负载(见 `docs/内存管理/RAM使用分析与优化方案.md` §0.6)。
### 4.1 冒烟测试:`ftp_test.py`
@@ -103,10 +106,10 @@
## 7. 测试清单(Checklist)
- [ ] 默认构建烧录,串口见 `[FTP] waiting for connection`。
- [ ] 默认构建烧录,串口见 `[FTP] waiting for connection`(缺此行 → 见 §8.6)。
- [ ] `ftp_test.py` 冒烟 PASS(连接 / 登录 / PASV / LIST)。
- [ ] `ftp_up_dl_test.py` 上传下载比对 `MATCH`。
- [ ] `ftp_speed_test.py` 大文件 `MATCH` + 记录速度基线。
- [ ] `ftp_up_dl_test.py` 上传下载比对 `MATCH`(10 项全 PASS)。
- [ ] `ftp_speed_test.py` 大文件 `MATCH` + 记录速度基线;同时用 `scripts/stack_report.py` 复核 ftpTask 栈余量(内存方案 §0.6)。
- [ ] (可选)GUI 客户端手动上传/下载成功。
- [ ] (可选)阶段 8 自动化 `TC-NET-801/802/803` PASS。
@@ -139,11 +142,27 @@
- **TRAP-12**:FatFS 根路径 `/` 前缀由 `to_fatfs_path()` 统一处理;客户端用绝对路径(如 `/UPLOAD.DAT`)即可。
### 8.6 单客户端限制
### 8.6 FTP 从不监听(21 端口一直拒绝)
- FTP 仅接受 1 个并发客户端;上一个会话未 `QUIT` 前,新连接会被拒。每次测完务必 `QUIT` / 断开。
- 现象:串口无 `[FTP] waiting for connection...`、无 `[LFT_INET]` 报错,`[STACK]` 行里 ftpTask 峰值极低(实测 156 B,仅为任务入口),21 端口始终 `connection refused`。
- 根因(2026-09-21 定位并修复):`osEventFlagsWait` 默认**消费**所等待的位,而 `SYS_EVT_NET_READY` / `SYS_EVT_STORAGE_READY` 是**状态位**。`netTask` 等 `STORAGE_READY` 时清掉 bit1,`uiTask` / `timeSyncTask` 等 `NET_READY` 时清掉 bit0 → `ftpTask` 的 `WaitAll(bit0|bit1)` 条件永不成立,任务永久阻塞在内核等待队列(无日志、无报错、静默失效)。
- 修复:**所有就绪位等待加 `osFlagsNoClear`**(`Src/freertos.c` 的 ftpTask、`App/task/ui_task.c`、`App/time_sync.c`、`App/task/net_task.c`、`test/ch395f_test_task.c`);命令位(`SYS_EVT_CFG_SAVE` / `SYS_EVT_NET_RECONFIG`)保持消费型等待不变。
- 验证:修后串口出现 `[FTP] waiting for connection...`、21 端口可连;ftpTask 的 `[STACK]` 峰值握手后升到 ~2.5KB、128KB 传输后升到 ~3.4KB(见内存方案 §0.6,余量告警)。
### 8.7 LIST/STOR/RETR 全部 550:卷未挂载 / 无 FAT 文件系统
### 8.7 单客户端限制:端口"通"但随后连接被拒(旧会话未释放)
- **FTP 仅接受 1 个并发客户端**:上一个会话未 `QUIT` 前,新连接会被拒;每次测完务必 `QUIT` / 断开。
- 现象:用 `Test-NetConnection` / `nc` 探测 21 端口显示可达,紧接着 FTP 脚本却 `10061 connection refused`。
- 根因:探测本身完成了一次 TCP 握手 → 占用了 CH395F 唯一的会话 socket;服务端进入控制通道等待,直到读超时(约 30s)才打印 `[ERR] [FTP] error reading next command` 并回到 `waiting for connection...`。
- 处理:**别用裸 TCP 工具探测 21 端口**;用 FTP 脚本(已内置重试:默认 6 次 × 3s)。若已被占用,等约 30s 或复位设备。
### 8.8 吞吐远低于基线(本次实测 ~50~60 KB/s @128KB)
- 本次 128KB STOR/RETR 实测上/下行约 49 / 60 KB/s,明显低于阶段 5 记录的 300~450 KB/s(`CH395F_Test_Guide.md`)。
- 已知差异:FTP 路径每次读 512B 经 FatFs→FTL→NAND 写/读,而阶段 5 基线是裸 socket 吞吐;小文件(128KB)还叠加了会话建立/关闭开销。
- 待办:如需吞吐结论,用 `ftp_speed_test.py --size 512`~`2048` 复测并与阶段 5 基线对齐;必要时再查 `WRITE_SEND_BUF` 分帧与 `net_poll` 周期。
### 8.9 LIST/STOR/RETR 全部 550:卷未挂载 / 无 FAT 文件系统
- 根因:FTP 服务的是 NAND 上的 FatFS 卷。固件在 `StartDefaultTask` 启动期执行 `f_mount(&fs, "", 1)`(会触发 `disk_initialize` → `nand_ftl_init`),使卷可用。
- **首启自动建卷**:若 `f_mount` 返回 `FR_NO_FILESYSTEM`(如坏块重建/恢复测试后卷被清空),固件会自动 `f_mkfs`(FM_FAT32、无分区表,参数与存储测试一致)再挂载。该过程上电会多花数秒(格式化),串口可见 `[FS] no FAT volume, formatting NAND...` → `[FS] FATFS mounted`。
+3 -1
View File
@@ -329,7 +329,9 @@ PC 每轮发送固定 8 字节帧:
`timeSyncTask` 启动后先等待 `SYS_EVT_NET_READY` 事件标志(取代旧版 8s 魔法延时;网络不就绪则一直等待),进入循环:
```c
osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY, osFlagsWaitAny, osWaitForever);
/* osFlagsNoClear:就绪位是"状态"不是"事件",任何消费者都不得把它清掉
* (否则其它等待者永不唤醒——2026-09-21 FTP 永不监听事故的根因) */
osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY, osFlagsWaitAny | osFlagsNoClear, osWaitForever);
for (;;) {
ls = time_sync_open_listener(); /* net_socket + net_bind + net_listen,失败重试 */
+29 -16
View File
@@ -11,9 +11,9 @@
|------|------|--------------|
| `defaultTask` | FTL + FatFS 存储挂载/测试(一次性) | 无(最先跑) |
| `netTask` | `net_init()` → `net_poll()` 周期轮询 | 无 |
| `ftpTask` | `lftpd_start("/", 21, &ftp)` | 等 `SYS_EVT_NET_READY | SYS_EVT_STORAGE_READY`(`osFlagsWaitAll`) |
| `start_test_task`(testTask) | 统一测试入口,按 `TEST_SUITE_*` 分发 | NET 模式下 `osEventFlagsWait(SYS_EVT_NET_READY, osFlagsWaitAny, osWaitForever)` 阻塞等待 |
| `timeSyncTask` | 墙钟时间同步 | `osEventFlagsWait(SYS_EVT_NET_READY, …)` 后开始同步 |
| `ftpTask` | `lftpd_start("/", 21, &ftp)` | 等 `SYS_EVT_NET_READY + SYS_EVT_STORAGE_READY`(`osFlagsWaitAll \| osFlagsNoClear`) |
| `start_test_task`(testTask) | 统一测试入口,按 `TEST_SUITE_*` 分发 | NET 模式下 `osEventFlagsWait(SYS_EVT_NET_READY, osFlagsWaitAny \| osFlagsNoClear, osWaitForever)` 阻塞等待 |
| `timeSyncTask` | 墙钟时间同步 | `osEventFlagsWait(SYS_EVT_NET_READY, … \| osFlagsNoClear)` 后开始同步 |
### 1.2 改造前(已废弃)的依赖管理手段
> 以下为引入 Event Flags 之前的历史做法,仅作背景记录,当前代码已不再使用。
@@ -36,6 +36,7 @@
- 每个子系统(网络、存储、FTP 服务等)在**真正准备好对外服务**的那一刻,广播一个"我好了"的事件标志。
- 任何依赖该子系统的任务,在入口处**阻塞等待**对应事件标志,收到才继续;此前不占 CPU、不被调度唤醒。
- 生产者与消费者**互相不知道对方存在**,只通过事件组解耦——新增任务只需在入口 `wait` 它需要的 bit,无需改动任何已有任务的代码。
- **就绪位是"状态",不是"事件"**:消费者必须带 `osFlagsNoClear` 等待,否则先到的消费者会把位清掉,后面的消费者(尤其 `osFlagsWaitAll` 组合)永远等不到——2026-09-21 实测即由此导致 `ftpTask` 永久阻塞、FTP 21 端口从未监听(详见 §3.2 注与 §6 第 7 条)。
这与 CubeMX"一次性创建全部任务"的模式完全兼容:任务仍由 `MX_FREERTOS_Init` 统一创建,但各自在入口 `wait`,相当于"创建即挂起、就绪才放行"。
@@ -50,7 +51,7 @@
| 等待方式 | 忙等轮询(浪费 tick) | 阻塞等待(零占用) |
| 多依赖组合 | 不支持 | `osFlagsWaitAll` / `osFlagsWaitAny` |
| 类型/作用域 | 裸 `volatile`,易误用 | 内核对象,API 受控 |
| 清空语义 | 手动维护 | `osFlagsNoClear` 可配 |
| 清空语义 | 手动维护 | 默认**消费**(唤醒即清);**就绪位必须 `osFlagsNoClear`**,只有命令位才用消费语义 |
| 调试可见性 | 不可观测 | 可在 IDE 观测事件组值 |
---
@@ -85,6 +86,11 @@ extern osEventFlagsId_t sys_evtHandle;
| `SYS_EVT_NET_RECONFIG` | 配置保存成功后(配置引擎属主 `defaultTask` 写盘后经 `net_cfg` 应用回调置位) | `netTask` 主循环:消费后 `net_stack_bring_up()` 重建网络(新网口参数立即生效),并递增网络代次号 |
| `SYS_EVT_CFG_SAVE` | UI 网口设置页保存行(经 `net_cfg` 保存交接回调置位) | `defaultTask` 主循环:立即唤醒执行 `net_cfg_save_run()`(写盘;成功后置 `SYS_EVT_NET_RECONFIG` 触发应用) |
> **各消费点的等待选项(2026-09-21 修正,务必照此写)**:
> - **状态位**(`NET_READY` / `STORAGE_READY`):`osFlagsWaitAny | osFlagsNoClear`(单依赖)或 `osFlagsWaitAll | osFlagsNoClear`(组合依赖,如 `ftpTask`)——**任何消费者都不得消费它**;
> - **命令位**(`NET_RECONFIG` / `CFG_SAVE`):消费型等待(不加 `osFlagsNoClear`)——必须恰好消费一次,否则会被反复执行(重复写盘 / 反复重建网络)。
> 判据:这个位表达的是"环境已具备"(状态)还是"请做一件事"(事件)。
### 3.3 事件组创建时机
`sys_evtHandle` 必须在**任何任务 `wait` 之前**存在。`main.c` 调用顺序为:
```
@@ -144,11 +150,13 @@ lftpd_start("/", 21, &ftp);
```
改为(网络 **且** 存储都就绪才 listen):
```c
/* 等网络 + 存储都真正就绪后再 listen,取代拍脑袋的 7s 魔法延时 */
/* 等网络 + 存储都真正就绪后再 listen,取代拍脑袋的 7s 魔法延时。
* osFlagsNoClear 必须带:就绪位是状态,被别的消费者清掉后本任务的
* WaitAll(两位) 将永不成立(2026-09-21 实测:FTP 21 端口从未监听) */
if (sys_evtHandle != NULL) {
(void)osEventFlagsWait(sys_evtHandle,
SYS_EVT_NET_READY | SYS_EVT_STORAGE_READY,
osFlagsWaitAll, osWaitForever);
osFlagsWaitAll | osFlagsNoClear, osWaitForever);
}
lftpd_start("/", 21, &ftp);
osEventFlagsSet(sys_evtHandle, SYS_EVT_FTP_READY); /* 自身就绪也广播(可选) */
@@ -163,21 +171,21 @@ while (!g_net_ready) { /* 旧:忙等轮询 */
```
改为:
```c
(void)osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY, osFlagsWaitAny, osWaitForever);
(void)osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY, osFlagsWaitAny | osFlagsNoClear, osWaitForever);
```
若测试套件同时需要文件系统,可组合等待:
```c
uint32_t need = SYS_EVT_NET_READY | SYS_EVT_STORAGE_READY;
osEventFlagsWait(sys_evtHandle, need, osFlagsWaitAll, osWaitForever);
osEventFlagsWait(sys_evtHandle, need, osFlagsWaitAll | osFlagsNoClear, osWaitForever);
```
### Step 6 timeSyncTask 必须等网络就绪(已实现)
`timeSyncTask` **依赖网络**,`App/time_sync.c` 中已在入口 `wait SYS_EVT_NET_READY`(`osFlagsWaitAny, osWaitForever`)后才开始时间同步,取代原先 8s 魔法延时。代码已落地:
`timeSyncTask` **依赖网络**,`App/time_sync.c` 中已在入口 `wait SYS_EVT_NET_READY`(`osFlagsWaitAny | osFlagsNoClear, osWaitForever`)后才开始时间同步,取代原先 8s 魔法延时。代码已落地:
```c
/* 等网络栈真正就绪,确保 netTask 已开始处理消息 */
/* 等网络栈真正就绪,确保 netTask 已开始处理消息(NoClear:状态位不得被消费) */
if (sys_evtHandle != NULL) {
(void)osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY,
osFlagsWaitAny, osWaitForever);
osFlagsWaitAny | osFlagsNoClear, osWaitForever);
}
```
> 注:`SYS_EVT_TIMESYNC_READY` 目前仅定义、**暂无生产者置位**(预留给对时精度敏感的业务),故其生产者列为"可选/预留",消费者亦暂不存在。
@@ -188,9 +196,9 @@ if (sys_evtHandle != NULL) {
| 场景 | 改造前 | 改造后 |
|------|------|--------|
| `ftpTask` 等网络 | `osDelay(7000)` 硬等 | `osEventFlagsWait(NET_READY, …, osWaitForever)` 就绪即放行 |
| `start_test_task` 等网络 | `while(!g_net_ready) osDelay(100)` 每 100ms 空转 | `osEventFlagsWait(NET_READY, …, osWaitForever)` 零占用阻塞 |
| "网络且存储都就绪" | 无法实现 | `osEventFlagsWait(NET_READY|STORAGE_READY, osFlagsWaitAll, …)` |
| `ftpTask` 等网络 | `osDelay(7000)` 硬等 | `osEventFlagsWait(NET_READY+STORAGE_READY, osFlagsWaitAll+osFlagsNoClear, …)` 就绪即放行 |
| `start_test_task` 等网络 | `while(!g_net_ready) osDelay(100)` 每 100ms 空转 | `osEventFlagsWait(NET_READY, osFlagsWaitAny+osFlagsNoClear, …)` 零占用阻塞 |
| "网络且存储都就绪" | 无法实现 | `osEventFlagsWait(NET_READY+STORAGE_READY, osFlagsWaitAll+osFlagsNoClear, …)` |
| 新增子系统 X | 手动在依赖任务里加延时/轮询 | 定义 `SYS_EVT_X`,生产者置位、消费者 `wait`,互不改动 |
---
@@ -198,10 +206,12 @@ if (sys_evtHandle != NULL) {
## 6. 注意事项 / 潜在坑
1. **事件组必须先建后用**:`sys_evtHandle` 由 CubeMX(FREERTOS Events 对象 `sys_evt`)在 `MX_FREERTOS_Init` 的任务创建之前自动创建,因此早于所有 `wait`。若句柄异常为 `NULL`,`osEventFlagsSet/Wait` 会返回 `osError` 且不阻塞——消费者会直接越过等待往下跑,重新引入竞态;各等待点已用 `if (sys_evtHandle != NULL)` 防御。
2. **`osWaitForever` 死锁风险**:若某生产者永远不置位(如初始化失败提前 `return`),消费者会永久阻塞。建议给关键 `wait` 加有限超时 + 错误日志,便于定位:
2. **`osWaitForever` 死锁风险**:若某生产者永远不置位(如初始化失败提前 `return`),消费者会永久阻塞。建议给关键 `wait` 加有限超时 + 错误日志,便于定位(另见第 7 条:**位被别的消费者吃掉**同样造成永久阻塞,而且更隐蔽):
```c
/* 状态位必须带 osFlagsNoClear,否则这条带超时的探测会把 NET_READY 吃掉,
* 后面的消费者(含 ftpTask 的 WaitAll)就再也等不到了 */
if (osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY,
osFlagsWaitAny, 30000U) == 0) {
osFlagsWaitAny | osFlagsNoClear, 30000U) == 0) {
DBG_ERROR("wait NET_READY timeout"); /* 后续可走降级/出错处理 */
}
```
@@ -209,6 +219,9 @@ if (sys_evtHandle != NULL) {
4. **bit 上限**:事件标志为 32 位,最高位(1<<31)被 CMSIS-RTOS 保留,可用 bit 0~30。
5. **测试构建(HW 层模式)的特殊性**:`TEST_SUITE_CH395F` 硬件层模式下 `netTask`(基础线程)入口自挂起(不 bring_up、不轮询、不广播 `SYS_EVT_NET_READY`),`start_test_task` 自己 `net_stack_bring_up()` 独占 CH395F。此时**无人置 `SYS_EVT_NET_READY`**(置位点只在 `net_task_func` 的未挂起路径内)。结果:`ftpTask` / `timeSyncTask` 会一直阻塞在 NET_READY 上——这正符合"硬件层模式不需要 FTP / 网络时间同步、CH395F 被 testTask 独占"的设计,属预期行为,不构成冲突。若将来需要在硬件层模式也起 FTP,可让 `start_test_task` 在 bring-up 成功后自行 `osEventFlagsSet(NET_READY)`。
6. **`g_net_ready` 去留**:**已完成淘汰**。`g_net_ready` 原全局标志已从代码中彻底移除(定义、置位、所有消费方均改为 Event Flags 等待),仅保留事件标志表达就绪。
7. **⚠ 就绪位被消费 → 永久阻塞(2026-09-21 实测,最隐蔽的一类)**:`osEventFlagsWait` 默认 `xClearOnExit = pdTRUE`(CMSIS 包装见 `cmsis_os2.c:1243-1249`),**唤醒时就把满足条件的位清掉**。修前 `netTask` 等 `STORAGE_READY`(Any,消费 bit1)、`uiTask`/`timeSyncTask` 等 `NET_READY`(Any,消费 bit0),于是 `ftpTask` 的 `WaitAll(bit0|bit1)` 需要"两位同时为 1"的条件**永不可能成立**(bit1 在 netTask 醒来时消失、bit0 在 uiTask/timeSync 醒来时消失)→ 任务永久阻塞,**无日志、无错误码、无超时**(`osWaitForever` + `(void)` 丢返回值 = 完全静默),FTP 21 端口从未监听。
- **修法**:所有**状态位**等待加 `osFlagsNoClear`(已改:`Src/freertos.c` 的 ftpTask、`App/task/ui_task.c`、`App/time_sync.c`、`App/task/net_task.c`、`test/ch395f_test_task.c`);**命令位**(`CFG_SAVE`/`NET_RECONFIG`)保持消费型等待。
- **工程化防线**:① 关键消费者加"启动超时告警"(如 ftpTask 10s 内未到 listen 就 `DBG_ERROR`);② 用 `scripts/stack_report.py` 看任务栈水位——**某任务水位异常低 = 它根本没干活**(本次正是靠 ftpTask 只有 156B 才发现)。
---
+34 -13
View File
@@ -52,14 +52,17 @@
| 任务 | 栈 (字节) | 优先级 | 职责 | 入口 |
|---|---|---|---|---|
| `defaultTask` | 2048 | Normal | FatFS 挂载(无卷 `f_mkfs`)→ `net_cfg_load`(配置引擎属主)→ 置 `STORAGE_READY`(= 存储+配置就绪)→ 日志落盘(唯一写者,`syslog_drain`)+ 配置保存执行/2s poll(配置引擎属主)+ LED 闪烁 | `StartDefaultTask` |
| `netTask` | 4096 | Normal | 等存储(配置已就绪)→ `net_stack_bring_up()`(只读 RAM 副本,不触碰配置文件引擎)→ 置 `NET_READY` → `net_poll` 轮询(CH395F 硬件层测试模式下入口自挂起) | `StartNetTask` |
| `ftpTask` | 4096 | Normal | 等网络+存储 → `lftpd_start("/", 21)` | `StartFtpTask` |
| `testTask` | 4096 | BelowNormal | 测试套件入口(`test/test_config.h` 选择;产品构建空转) | `start_test_task` |
| `defaultTask` | 4096 | Normal | FatFS 挂载(无卷 `f_mkfs`)→ 卷健康探针/自愈 → `net_cfg_load`(配置引擎属主)→ 置 `STORAGE_READY`(= 存储+配置就绪)→ 日志落盘(唯一写者,`syslog_drain`)+ 配置保存执行/2s poll + LED 闪烁 + **栈水位观测**(`stack_probe_poll`:首次 5s 后报告,此后仅在出现更深峰值时打印) | `StartDefaultTask` |
| `netTask` | 4096 | Normal | 等存储(配置已由 defaultTask 就绪,本任务只读 RAM 副本)→ `net_stack_bring_up()`(不触碰配置文件引擎)→ 置 `NET_READY` → `net_poll` 轮询(CH395F 硬件层测试模式下入口自挂起) | `StartNetTask` |
| `ftpTask` | 4096 | Normal | 等网络**且**存储(`WaitAll`)→ `lftpd_start("/", 21)` → 单客户端会话循环(断开后重开监听) | `StartFtpTask` |
| `testTask` | 8192 | BelowNormal | 测试套件入口(`test/test_config.h` 选择;产品构建空转) | `start_test_task` |
| `uiTask` | 16384 | BelowNormal | 等网络 → `ui_port_init`/`ui_menu_init`/`ui_standby_init`(空闲 60s 切待机屏)/`ui_net_sink` → 10ms 刷新 | `ui_task_func` |
| `timeSyncTask` | 2048 | Normal | 等网络 → TCP :8888 收 PC 推送时间 → 写 RTC | `time_sync_task` |
> `idle`(128 words)/ `timer`(256 words)由 FreeRTOS 内核创建。所有任务栈从 48KB heap_4 堆分配。
> **栈实测与预算**(2026-09-21,工具 `scripts/stack_report.py` = Keil 静态调用图 + 串口 `[STACK]` 水位;详见 `docs/内存管理/RAM使用分析与优化方案.md` §0.6):
> defaultTask 静态最坏 2776B/4096B(余量 32%)、**ftpTask 实测 3480B/4096B(余量仅 616B = 15%,低于 1024B 底线,待处置)**、uiTask 实测 3804B/16384B(缩栈空间大,内存方案 §1.3 目标 6KB)、netTask 600B、timeSyncTask 420B、testTask(产品构建)348B。
> ⚠ 改栈或加大栈内局部变量前后**必须**重跑该脚本——静态分析低估含递归/间接调用的链(ftpTask 静态 2528B vs 实测 3480B)。
### 2.2 就绪屏障(Event Flags,取代魔法延时)
@@ -72,6 +75,15 @@
**设计原则**:生产者与消费者互不知晓,只经事件组解耦;新增子系统只需定义新 bit + 各自 wait/set,不改已有任务。
(详见 `docs/项目启动顺序管理.md`)
**⚠ 就绪位必须 `osFlagsNoClear`(2026-09-21 实证,FTP 永不监听事故)**:`osEventFlagsWait` **默认消费**所等位(CMSIS 包装 → `xEventGroupWaitBits(xClearOnExit=pdTRUE)`,`cmsis_os2.c:1243-1249`),而就绪位表达的是**持久状态**——多人等待时必须人人可读。修前 `netTask` 等 `STORAGE_READY` 时清掉 bit1、`uiTask`/`timeSyncTask` 等 `NET_READY` 时清掉 bit0,导致 `ftpTask` 的 `WaitAll(bit0|bit1)` 两位永不同时为 1 → **任务永久阻塞、零日志零报错、21 端口从未监听**(靠栈水位异常才定位到)。
| 位类型 | 例子 | 等待方式 |
|---|---|---|
| **状态位**("环境已就绪") | `SYS_EVT_NET_READY` / `SYS_EVT_STORAGE_READY` | `osFlagsWaitAny\|osFlagsNoClear`、`osFlagsWaitAll\|osFlagsNoClear`——**任何消费者都不许消费它** |
| **命令位**("请做一件事") | `SYS_EVT_CFG_SAVE` / `SYS_EVT_NET_RECONFIG` | 消费型等待(默认,不加 NoClear)——必须恰好被消费一次,否则会反复写盘/重建网络 |
本节图中 `ftpTask ← bit0 + bit1(All 汇合点)`依赖的正是"状态位常驻"这一前提。
### 2.3 线程间通信
| 机制 | 对象 | 用途 |
@@ -106,17 +118,19 @@ sequenceDiagram
Note over K: MX_FREERTOS_Init 已创建 6 任务 + 事件组 + 队列
K->>D: 调度
D->>D: f_mount(无卷则 f_mkfs FAT32)→ LED 循环
D->>D: f_mount(无卷则 f_mkfs FAT32)→ 卷健康探针/自愈 → net_cfg_load()(配置引擎属主)
D-->>N: set SYS_EVT_STORAGE_READY
D->>D: 进入 LED / syslog / 配置 poll 循环(含 stack_probe_poll 栈水位采样)
N->>N: wait STORAGE_READY(≤10s,超时回退工厂默认)
N->>N: net_cfg_load() → net_init(ip,mask,gw,mac)<br/>→ PHY 强制 100M 全双工 → 等链路 ≤3s → MAC 回写影子
N->>N: wait STORAGE_READY(Any|NoClear,≤10s,超时回退工厂默认)
N->>N: 读 RAM 配置副本 → net_init(ip,mask,gw,mac)<br/>→ PHY 强制 100M 全双工 → 等链路 ≤3s → MAC 回写影子
N-->>F: set SYS_EVT_NET_READY
N->>N: 循环 net_poll() + net_process_messages() + osDelay(1)
F->>F: wait NET_READY|STORAGE_READY(All)→ lftpd_start("/", 21)
U->>U: wait NET_READY → ui_port_init → ui_menu_init → ui_net_sink_start(7003)
T->>T: wait NET_READY → TCP listen :8888
F->>F: wait NET_READY|STORAGE_READY(All|NoClear)→ lftpd_start("/", 21)<br/>→ 串口输出:waiting for connection...
U->>U: wait NET_READY(Any|NoClear)→ ui_port_init → ui_menu_init → ui_net_sink_start(7003)
T->>T: wait NET_READY(Any|NoClear)→ TCP listen :8888
Note over D,N,F,U,T: 就绪位一律 osFlagsNoClear(状态常驻、可被多任务重复读取);<br/>仅命令位 CFG_SAVE / NET_RECONFIG 用消费型等待
```
---
@@ -175,6 +189,10 @@ flowchart TB
**单连接约束**:CH395F 配置为多 Socket + TCP Server 单连接(`set_fun_para(0x09)`),Socket 0 复用监听与数据通道;FTP / 对时 / 远程显示三个 TCP 服务**串行**占用连接,连接断开后自动回到 LISTEN。
> **FTP 启动门(2026-09-21 修复)**:`ftpTask` 必须等 `NET_READY` **且** `STORAGE_READY`(两位均 `NoClear`)才 `lftpd_start`——此前就绪位被其它等待者消费,FTP **静默不监听**(详见 §2.2 与 `docs/测试说明/FTP_Test_Guide.md` §8.6)。
> **单客户端实测注意**:同一时刻只服务 1 个会话,旧会话异常断开后约 30s 才恢复监听。测试脚本必须内置连接重试(`test/ftp_up_dl_test.py`、`test/ftp_speed_test.py` 已内置),且**不要用裸 TCP 工具探测 21 端口**(探测会占掉唯一会话,见该指南 §8.7)。
> 2026-09-21 实测:登录/TYPE I/PASV/STOR/RETR/LIST/DELE/QUIT 全通,128KB 上传下载逐字节一致(约 49/60 KB/s,低于阶段 5 裸 socket 基线,待查)。
---
## 6. UI 层架构(框架 + 页面插件)
@@ -231,12 +249,12 @@ flowchart TB
```
STM32F4-Base/
├── Src/ Inc/ CubeMX 生成:main.c / freertos.c / HAL 外设 / 中断 / 时基
│ ├── freertos.c 6 任务 + netMsgQueue + sys_evt 创建(USER CODE 区自定义)
│ ├── freertos.c 6 任务 + netMsgQueue + sys_evt 创建(USER CODE 区自定义,含栈水位探针 stack_probe_poll)
│ └── main.c 入口 → app_main_init → osKernelStart
├── App/ 应用层(业务编排,非 CubeMX)
│ ├── app_main.c 启动编排(时间子系统优先)
│ ├── time_sync.c TCP 对时服务
│ ├── sys_events.h 就绪屏障 bit 定义
│ ├── sys_events.h 就绪屏障 bit 定义(就绪位等待须 osFlagsNoClear,见 §2.2)
│ ├── app_cfg.h 功能编译开关(APP_NET/ADC/RS485_ENABLE)
│ ├── task/ net_task / ui_task / adc_task* / rs485_task*
│ ├── log/ sys_log 记录引擎(定长 64B;MCU 后端落盘 /log/*.log)
@@ -257,7 +275,7 @@ STM32F4-Base/
├── simulator/ PC 模拟器(CMake + SDL2,共用 UI 源码 + 无头回归测试)
├── test/ 测试套件(test_config.h 互斥选择,产品构建空转)
├── tools/ font_gen(字体生成)/ menu_gen(菜单表生成)
├── scripts/ 代码规范检查、RAM 报告等
├── scripts/ 代码规范/乱码检查、RAM 报告(ram_report.py)、栈报告(stack_report.py)
└── docs/ 设计/测试/问题记录文档
```
@@ -291,6 +309,8 @@ flowchart LR
| 存储架构(NAND/FTL/FatFs) | `docs/STM32F4-Base存储架构说明.md` |
| 墙钟时间维护 | `docs/系统墙钟时间维护说明.md` |
| RAM 预算与优化 | `docs/内存管理/RAM使用分析与优化方案.md` |
| 栈/内存观测工具 | `scripts/stack_report.py`(Keil 静态调用图 × 串口 `[STACK]` 水位,判定取两者更坏者)· `scripts/ram_report.py`(.map 静态占用)—— 用法与基线见内存方案 §0.6 |
| FTP 功能与陷阱 | `docs/测试说明/FTP_Test_Guide.md`(启动门 §8.6 / 单客户端 §8.7)· 脚本 `test/ftp_up_dl_test.py`、`test/ftp_speed_test.py` |
| UI 设计架构 / 内容页 / 菜单 | `docs/UI 设计/UI_设计架构.md` · `内容页架构.md` · `菜单页面设计.md` |
| UI 自动化测试 | `docs/UI 设计/UI_自动化测试流程.md` |
| 驱动已知陷阱 | `docs/问题记录/{CH395F,GD5F2GQ5UE,SD2506}_Trap_Records.md` |
@@ -316,3 +336,4 @@ flowchart LR
| 2026-09-13 | `sys_clock` 由 ③基础服务层改归 **④中间件层**(通用平台时间服务,日志/文件/对时共用;文件移入 `App/util/` 与 crc/ringbuf 同目录,include 路径不变):`FatFs` → `sys_clock` 成为**层内引用**,工程反向依赖消除;同日新增的 §1.2 "反向依赖例外" 条目撤销 |
| 2026-09-14 | 通用配置文件(P3):网口配置存储由 `/cfg/netcfg.bin`(NCFG 二进制)迁移至 `/config.ini`(INI `[network] 段`,`App/cfg_file` 引擎 + `cfg_file_store` 双端后端);`net_cfg` 对外 API 不变;旧档首启自动迁移;`cfg_file` 列为 MCU/模拟器共享源码 |
| 2026-09-14 | 运行/自检记录:新增 `App/log/sys_log`(定长 64B 文本记录引擎,MCU 后端 `/log/run.log`·`selftest.log`,defaultTask 唯一写者)+ UI 页面插件 `ui_page_runlog`/`ui_page_selftest`(动态列表)+ `ui_standby` 待机屏(空闲超时切入);密码页错误提示;字体 111→122 字(217 字形);详见 `docs/运行与自检记录设计说明.md` |
| 2026-09-21 | **§2.2 屏障语义修正 + FTP 永不监听缺陷修复**:`osEventFlagsWait` 默认消费所等位,就绪位(`NET_READY`/`STORAGE_READY`)被 netTask/uiTask/timeSync 先消费后,`ftpTask` 的 `WaitAll(两位)` 永不成立 → 任务永久阻塞、21 端口从未监听(静默);5 处就绪位等待统一加 `osFlagsNoClear`(命令位 `CFG_SAVE`/`NET_RECONFIG` 保持消费型)。同轮新增**栈可观测性**:`Src/freertos.c` 的 `stack_probe_poll`(串口 `[STACK]` 水位)+ `scripts/stack_report.py`(Keil 静态调用图 × 运行时水位,判定取两者更坏者);§2.1 任务表补齐实测栈数据(ftpTask 3480B/4096B 余量 15% 待处置)并修正过期的 defaultTask/testTask 栈配置,§3 时序图补 `NoClear` 与 FTP 启动日志,§5 补 FTP 启动门/单客户端实测注意 |