Files
STM32F4-Base/docs/架构设计/项目架构总览.md
T
wandering1 4a4ad6bc5f chore(全仓): @build/@test 旧文档别名清退为技能调用名
现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名,
不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体
run_boot_test.py 的真实调用。

- AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改
  「编译(/keil-build)」「串口回归(/serial-boot-test)」
- keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree
  方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围)
- 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入
  等 41 行历史证据保留旧名
- gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑
- 顺手补并行方案同行漏网计数 51→52 钩子

验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all
rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52
钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
2026-10-30 13:03:12 +08:00

402 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# STM32F4-Base 项目架构总览
> 版本:v1.2 日期:2026-10-30
> 平台:STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2)+ FreeRTOS V10.3.1(CMSIS-RTOS2)
> 用途:一页看清项目的分层、任务、数据流与目录组织。现状能力与功能缺口的权威口径在 AGENTS.md「项目概况」,细节以各模块专项文档为准(见 §9)。
---
## 1. 全景分层架构
![STM32F4-Base 项目分层架构](../images/项目架构总览.svg)
> 上图由 `tools/arch_diagram/gen_arch_svg.py` 生成(**手绘 SVG**,布局确定、零连线交叉);改架构后编辑脚本顶部 `L_*` / `ROWS` 数据重跑即可,源数据与 §1.1 表一致(脚本与产物同一笔提交,`generated-fresh` 门禁逐字节幂等)。
> 需要 PNG 预览(PPT/聊天分享):用 Edge 无头渲染 `docs/images/项目架构总览.svg` 即可(PNG 不入库)。
> 分层原则:**上层只依赖相邻下层(单向)**——这条方向已有机械检查(`layer-deps` 钩子 / `scripts/check_layer_deps.py`:`Drivers/BSP/**` 不得 include `App/**` 的头,存量 3 条反向边钉成基线、只减不增,三个横切叶子头 `console_log.h`/`sys_clock.h`/`sha256.h` 显式豁免);**① 业务层为顶层**(仅 `net_cfg`,被 UI 与基础服务共用),**② UI 层与 ③ 基础服务层同级并列**。跨层的具体调用关系见 §1.2 表。
### 1.1 各层职责与关键模块
> ① 业务层(顶层,仅 `net_cfg`);② UI 层与 ③ 基础服务层**同级并列**(均使用 `net_cfg`);④ 中间件层 → ⑤ 驱动层 → ⑥ 内核/HAL → ⑦ 硬件 依次向下。
| 层 | 目录 | 职责 | 关键模块 |
|---|---|---|---|
| ① 业务层 | `Drivers/BSP/NETCFG/` | 顶层共享配置(被 UI 与基础服务共用) | `net_cfg`(`/config.ini` `[network]` 段) |
| ② UI 层 | `Drivers/BSP/UI/` | LVGL 160×160 单色界面(与 PC 模拟器、屏板、远程显示**共用同一份显存**) | `ui_port` `ui_menu` `ui_menu_view` `ui_menu_tab`(生成物) `ui_content_page` `ui_page_registry`(生成物) `ui_page_*`(20+ 页插件) `ui_data`(数据供给发布 API) `ui_net_sink` `ui_standby` `ui_font_cn_12` `ui_deco` |
| ③ 基础服务层 | `App/`、`Drivers/BSP/NET/lftpd/` | 应用侧服务与编排 | `app_main` `sys_events` `ui_data_feed` `task/*`(9 任务) `algo/*`(语义取数与有效性~保护判据 + 遥测快照 + 录波数据面 + 屏板灯位映射) `protocol/iec104` `sys_log`·`soe_*`·`console_log` `cfg_file`·`prot_set_store` `vol_lock` `time_sync` `lftpd` |
| ④ 中间件层 | `Drivers/BSP/NET/`、`Lib/`、`App/util/` | 通用库 / 协议栈 | `net_socket` `net_select` `net_events` `net_listener` `net_pending` `FatFs` `nand_ftl(dhara)` `LVGL v9.2` `sys_clock` `task_watchdog` |
| ⑤ 驱动层 | `Drivers/BSP/{CH395F,GD5F2GQ5UE,TPAFE5160,SD2506,RS485,ScreenLink}/` | 芯片寄存器 / 时序 / 中断 | `ch395f` `gd5f2gq5ue` `tpafe5160` `sd2506` `rs485` `sb_link`·`sb_proto` |
| ⑥ 内核 / HAL | `Middlewares/`、`Drivers/STM32F4xx_HAL_Driver/`、`Drivers/CMSIS/` | 任务调度、外设 HAL、启动 | FreeRTOS V10.3.1 · STM32 HAL · CMSIS |
| ⑦ 硬件 | — | STM32F407ZGTx @168MHz | SPI1 / SPI2 / I2C1 / UART4 / UART5 / USART1 / ADC1 / TIM3 / DMA / GPIO |
### 1.2 关键跨层依赖(对应上图未画出的连线)
| 依赖 | 说明 |
|---|---|
| `ui_page_net_cfg` → `net_cfg` | 网口设置页显示/编辑 RAM 副本(不直接触 CH395F SPI) |
| `netTask` → `net_cfg` → `net_socket` | 上电只读 RAM 副本(defaultTask 已在屏障前 `net_cfg_load`)→ `net_init(ip,mask,gw,mac)`;配置保存后经 `SYS_EVT_NET_RECONFIG` 按**网络代次号**重建(新 IP 免重启生效) |
| `net_socket` → `net_events` / `net_listener` / `net_pending` | socket 宿主调三个内部模块:芯片边沿事件 + 100ms 周期对账 / 监听生命周期(重建请求登记 + 唯一执行)/ 阻塞 recv·send 挂起请求表 |
| `App/algo/*` → `adc_task` / `protect_task` | 采集与算法层为纯计算:adcTask 消费 语义取数与有效性/标定折算/全周傅氏相量/测频/录波环(TIM3 1kHz 节拍)并发布遥测快照,protectTask 消费 保护判据(20ms 判据节拍) |
| `App/algo/meas`·`prot_elm` → `iec104_pts` | 104 遥测 = 遥测快照 `meas_get()`(双区无锁、带品质、频率点绑定),遥信 = `prot_elm` 元件状态位图;点表与 `[comm]`/`[run_par]` 配置(运行参数段的 104 地址映射为真源) |
| `App/app/ui_data_feed` → `Drivers/BSP/UI/ui_data` | UI 数据通道的写/读两侧:装配层按节拍从 采集/算法/保护/协议/时钟 取数组三帧(实时量 rt / 参数 par / 装置信息 info),经 `ui_data.h` 发布 API 供页面只读;`ui_data_feed` 是快照唯一填充者(装配层可同时 include 两侧头,方向仍是上层→下层,`check_layer_deps.py` 只拦反向) |
| `uiTask` → `ScreenLink`(`sb_link`) | uiTask 10ms 节拍驱动:`sb_link_frame_update()` 活显存 memcmp 判变组帧 B(4208 B)、`sb_link_key_take()` 出队屏板按键、`sb_link_poll()` 离线判定(≥10s 无召唤);灯位映射在 `App/algo/sb_led_map`(104 RUN 观测口)。⚠ HAL 三个 UART 回调(`RxEvent`/`Error`/`TxCplt`)的**唯一实现处在 `rs485.c`**、逐行转发 `sb_link`——改任一 UART 回调必须两边一起看 |
| 录波链路(三任务交接) | adcTask `wave_ring_push()/tick()`(与相量主线同拍**并行**、互不阻塞)→ 触发冻结、后触发计满封段 → defaultTask 见 `wave_ring_sealed()` 游标分次搬进 `/wave` → uiTask 单通道回放页;落盘回执才解冻。取回 = FTP :21 |
| SOE 链路 | protectTask 动作/复归边沿 `soe_store_post()`(纯 RAM:取时标 + 组 64B 记录 + 队列,**零文件系统**)→ defaultTask(`/log` 唯一写者,`syslog_drain` 尾部挂接)落 `/log/soe.log` |
| `lftpd` / `net_cfg` / 测试 → `FatFs` → `nand_ftl` → `gd5f2gq5ue` | 文件、配置、录波段与 SOE 落 NAND |
| `ui_net_sink` / `time_sync` → `net_socket` | 远程显示 :7003 / 对时 :8888 共用同一 TCP 栈;屏板链路走 USART1 串口(不经 TCP),与 :7003 是同一份显存的两个出口(**串口为主**) |
| `ui_menu` → `ui_content_page` → `ui_page_*` | 框架 + 页面插件(描述符解耦;节点→页面绑定由注册表 `ui_page_registry.c` 生成,见 §6) |
| `time_sync` / `sys_clock` → `sd2506` | 墙钟基准与写回 RTC |
| `ui_port` / `ui_page_*` → `LVGL` | UI 全部绘制走 LVGL(1bpp 离屏渲染到内存帧缓冲) |
> `*` `rs485_task.c` 任务体已实现,但**当前未在 `freertos.c` 创建**(B2 暂缓,见 `docs/任务规划/暂缓-RS485.md`);`adc_task.c` 已于 2026-10-10 接入创建(`adcTask`)。
> 串口三分(2026-10-29 屏板批 0 后的定态):**USART1 = 屏板链路 700 kbaud**、**UART4 = RS-485(115200,未接线)**、**UART5 = 控制台日志 115200**——日志口原在 USART1,屏板占用后迁至 UART5(RS232#1 台架线改接日志口)。
---
## 2. 运行时任务与就绪屏障
### 2.1 任务一览(`Src/freertos.c` 创建)
| 任务 | 栈 (字节) | 优先级 | 职责 | 入口 |
|---|---|---|---|---|
| `defaultTask` | 5120 | Normal | FatFS 挂载(无卷 `f_mkfs`)→ 卷健康探针/自愈 → `net_cfg_load`(配置引擎属主)→ 置 `STORAGE_READY`(= 存储+配置就绪)→ **`/log` 唯一写者**(`syslog_drain` 尾部统一排空 syslog / SOE 两条队列 → run/selftest/soe 三本日志 + 控制台排空)+ **录波落盘道**(已封存段游标分次搬 `/wave`,轮转号启动扫盘重建)+ 配置保存执行/2s poll + LED 闪烁 + **栈水位观测**(`stack_probe_poll`) | `StartDefaultTask` |
| `netTask` | 3840 | Normal | 等存储(配置已由 defaultTask 就绪,本任务只读 RAM 副本)→ `net_stack_bring_up()`(不触碰配置文件引擎)→ 置 `NET_READY` → 循环 `net_poll` + `net_process_messages`;消费 `SYS_EVT_NET_RECONFIG` → 按网络代次号重建监听(CH395F 硬件层测试模式下入口自挂起) | `StartNetTask` |
| `ftpTask` | 4096 | Normal | 等网络**且**存储(`WaitAll`)→ `lftpd_start("/", 21)` → 单客户端会话循环(断开后经 `net_listener` 重建监听);录波段文件的取回通道 | `StartFtpTask` |
| `testTask` | 8192 | BelowNormal | 测试套件入口(`test/board/test_config.h` 选择)。**产品构建不创建**:CubeMX 生成区无条件建后,`MX_FREERTOS_Init()` 在未选套件时(`!TEST_SUITE_SELECTED`)立即 `vTaskDelete` 回收其 8KB CCM 栈(2026-10-04) | `start_test_task` |
| `uiTask` | 16384 | BelowNormal | 等网络 → `ui_port_init`/`ui_menu_init`/`ui_standby_init`(空闲 60s 切待机屏)/`ui_net_sink` → 10ms 刷新:`ui_data_feed_tick()` 三帧装配 + 屏板链路驱动(组帧 B / 出键 / 离线判定 / 状态灯位)+ 录波回放页 | `ui_task_func` |
| `timeSyncTask` | 2560 | Normal | 等网络 → TCP :8888 收 PC 推送时间 → 写 RTC | `time_sync_task` |
| `adcTask` | 2560 | AboveNormal | 纯消费者 + **唯一发布者**:`osThreadFlagsWait` 等 TIM3 1kHz 唤醒(ISR 内 `adc_tick_irq_notify` 置位)→ `adc_input_poll()`(TPAFE 原子快照 + STM32 ADC1 DMA 双缓冲)→ 标定折算 → 相量 → 测频;录波支路 `wave_ring_push()/tick()`;**每 200ms `meas_tick()` 发布遥测快照**(双区无锁,104 与 UI 共用同一份上游) | `adc_task_func` |
| `iec104Task` | 2560 | Normal | 等网络 → IEC 60870-5-104 上行(:2404 监听 + link 状态机;遥测读 meas 快照、遥信读 `prot_elm` 元件态,COV 死区/变位上送;`[comm]`/`[run_par]` 配置;B4/批 2)。任何构建都创建,纳入 IWDG `required_mask` | `iec104_serv_task` |
| `protectTask` | 2048 | Normal | 保护判据 20ms 节拍:`prot_elm_run()` + 「自检失败闭锁」采样故障闭锁 + 定值校验和巡检 + 动作/复归边沿 SOE 投递(B5);⚠ **当前处于「未标定闭锁」态**——`adc_cal_flags()` 无 VALID 时判据停走,两点标定写入 `[adc_cal]` 后自动解卡(用户裁定 2026-10-25:保持闭锁,不用万用表走捷径);纳入 IWDG `required_mask`(产品 `0x1F7`) | `protect_task` |
> `idle`(128 words)/ `timer`(256 words)由 FreeRTOS 内核创建。所有任务栈从 FreeRTOS `heap_4` 堆分配(**已迁 CCM**,2026-10-12 B5 由 48KB 扩容至 56KB,见 `docs/内存管理/RAM使用分析与优化方案.md` §4.1.2/§2.3)。
> **栈台账**:权威逐任务数字在内存方案 §2.3;`stack_report.py` 9/9 PASS(2026-10-22 静态最坏链 + 同日真机 `[STACK]` 水位;defaultTask 最坏 3024B/5120B 余 40%、protectTask 静态 872B/2048B 余 57%)。⚠ 改栈或加大栈内局部变量前后**必须**重跑该脚本——静态分析低估含递归/间接调用的链(优化前 ftpTask:静态 2528B vs 实测 3480B)。
### 2.2 就绪屏障(Event Flags,取代魔法延时)
![就绪屏障(Event Flags)](../images/就绪屏障.svg)
> 三栏式:**左 = 生产者(set)· 中 = 事件组 `sys_evtHandle` · 右 = 消费者(wait)**。
> 任务卡与事件位对齐同一行,箭头 = `set` / `wait` 关系;`bit1 → ftpTask` 的额外依赖用 **All 汇合点**表达(`osFlagsWaitAll`)。
> 源码 `tools/arch_diagram/gen_barrier_svg.py`(改数据重跑即可);PNG 预览本地渲染、不入库。
**设计原则**:生产者与消费者互不知晓,只经事件组解耦;新增子系统只需定义新 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 汇合点)`依赖的正是"状态位常驻"这一前提。录波、SOE、屏板链路**不新增事件位**:录波/SOE 靠"唯一写者轮询封存队列/封存环"衔接,屏板靠中断与 uiTask 节拍,均不进启动屏障。
### 2.3 线程间通信
| 机制 | 对象 | 用途 |
|---|---|---|
| Event Flags | `sys_evtHandle` | 启动就绪屏障(§2.2) |
| Message Queue | `netMsgQueue`(深度 8,`net_msg_t`) | 所有 CH395F 操作经队列**串行化**到 netTask 执行,保证 SPI 单线程访问 |
| Message Queue | syslog 条目队列 + SOE 投递队列(内核后**惰性创建**) | 各任务投递 → defaultTask 排空落 `/log/run.log`·`selftest.log`·`soe.log`(定长 64B) |
| 双区无锁快照 | `phasor` / `meas`(`App/algo`) | adcTask 唯一发布者,双区原子切换;protectTask(20ms)、iec104Task、uiTask 无锁读 |
| 冻结环交棒 | `wave_ring` | adcTask 写入;触发即冻结写指针、计满封段;defaultTask 只读已封存区游标搬运;落盘回执才解冻,冷却期再触发只加溢出计数 |
| 环形缓冲 + 行级单一所有权 | `console_log` | 各任务 append 日志行进环形缓冲,defaultTask 唯一发送者(UART5);裸发窗口用**非阻塞**所有权防半行交错(2026-10-26 O1 实证) |
| 按键环形队列 | `sb_link` | USART1 空闲中断入队(按键骑在帧 A 里上来)→ uiTask 出队映射 `UI_KEY_*` |
---
## 3. 启动时序
```mermaid
sequenceDiagram
autonumber
participant M as main
participant A as app_main_init
participant K as osKernelStart
participant D as defaultTask
participant N as netTask
participant F as ftpTask
participant U as uiTask
participant T as timeSyncTask
participant I as iec104Task
participant P as protectTask
participant C as adcTask
M->>M: HAL_Init + CubeMX 外设初始化<br/>(SPI1/SPI2/I2C1/UART4/UART5/USART1/ADC1/TIM3/DMA/GPIO/IWDG)
M->>A: app_main_init()
Note over A: fault_handler_enable() 第一行(四 Fault 全采集,否则被升级成 HardFault 看不出根因)
A->>A: syslog_mcu_init() → sd2506_init() → boot_stats_init() → sys_clock_init()(墙钟基准,时间优先使后续日志带时间戳)
A->>A: rs485_init(UART4) → sb_link_init(USART1 屏板:波特率交叉核对 + 挂首轮 IDLE 接收)
A->>A: gd5f2gq5ue_init()(BBT 扫描)→ tpafe5160_init()
A->>A: adc_input_init()(语义映射 + ADC1 DMA)→ phasor_init() → freq_init()<br/>→ adc_cal_init()(标定折算 [adc_cal] 注册)→ run_par_init()([run_par] + 104 地址映射真源)<br/>→ iec104_comm_init()([comm] ca/端口)→ prot_set_init() + prot_elm_init()(保护判据内核前封存)
A->>A: SD2506 诊断(ID/时间/温度/电池)→ HAL_TIM_Base_Start_IT(&htim3)(1kHz 节拍)
A-->>M: "App init OK"
M->>K: osKernelStart()
Note over K: MX_FREERTOS_Init 已创建 9 任务 + 事件组 + netMsgQueue<br/>(产品构建 testTask 创建即回收,实跑 8 任务)
K->>D: 调度
D->>D: f_mount(无卷则 f_mkfs FAT32)→ 卷健康探针/自愈 → net_cfg_load()(配置引擎属主)
D-->>N: set SYS_EVT_STORAGE_READY
D->>D: 进入 LED / syslog·SOE 排空 / 录波落盘道 / 配置 poll 循环(含 stack_probe_poll 栈水位采样)
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()<br/>(消费 NET_RECONFIG → 按代次号重建监听)
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)<br/>→ 10ms 循环:ui_data_feed_tick + 屏板组帧/出键/灯位
T->>T: wait NET_READY(Any|NoClear)→ TCP listen :8888
I->>I: wait NET_READY(Any|NoClear)→ IEC104 listen :2404(遥测读 meas 快照 / 遥信读 prot_elm)
P->>P: 20ms 判据节拍 prot_elm_run()(不依赖网络;未标定即闭锁停走)+ SOE 边沿投递
C->>C: wait TIM3 1kHz 唤醒(ISR adc_tick_irq_notify)→ adc_input_poll() 纯消费<br/>→ 标定→相量→测频 → wave_ring_push/tick → 每 200ms meas_tick() 发布快照
Note over D,N,F,U,T,I: 就绪位一律 osFlagsNoClear(状态常驻、可被多任务重复读取);<br/>仅命令位 CFG_SAVE / NET_RECONFIG 用消费型等待
```
---
## 4. 存储栈
```mermaid
flowchart LR
FS["FatFs (Lib/FatFs)<br/>f_mount / f_open / f_mkfs"]
DIO["diskio.c<br/>BSP 适配层"]
FTL["nand_ftl.c<br/>dhara FTL(磨损均衡 + 逻辑块映射)"]
NAND["gd5f2gq5ue.c<br/>页读写 / ECC / BBT 坏块表"]
CHIP[("GD5F2GQ5UE<br/>NAND 2Gbit")]
subgraph USERS["使用方"]
NC["net_cfg / cfg_file 引擎<br/>/config.ini 多段 INI"]
SLG["sys_log + soe_store<br/>/log/run.log · selftest.log · soe.log"]
LF["lftpd<br/>FTP 文件读写(录波取回同通道)"]
WS["录波落盘道(defaultTask)<br/>/wave 段文件(8.3 短名 WAVE 轮转)"]
PS["prot_set_store<br/>/prot_set.bin(B7 原子写)"]
ST["测试套件<br/>STORAGE/GD5F"]
end
NC --> FS
SLG --> FS
LF --> FS
WS --> FS
PS --> FS
ST --> FS
FS --> DIO --> FTL --> NAND --> CHIP
subgraph RTC["独立时间源"]
SD["SD2506 RTC (I2C1)"] --> SC["sys_clock<br/>墙钟 epoch"]
end
```
- **配置文件**:通用 `/config.ini`(cfg_file 引擎,静态态单任务收敛于 defaultTask,外部变更 4s 沉降后自动重载),**多段注册制**:`[network]`(net_cfg)/ `[ftp]`(鉴权)/ `[adc_cal]`(标定系数与零点)/ `[run_par]`(运行参数 + 104 地址映射真源)/ `[comm]`(104 ca/端口);新模块加段=注册 `cfg_file` 段处理器,唯一落盘者不变。旧版 NCFG 二进制(`/cfg/netcfg.bin`)已不再读取(2026-10-10 删迁移),存在则忽略。→ `docs/子系统设计/配置文件设计说明.md`
- **记录三本**:`sys_log` 引擎写定长 64B 文本记录到 `/log/run.log` 与 `/log/selftest.log`;`soe_store` 同先例落 `/log/soe.log`(64B 定长二进制,容量满删除重建)。defaultTask 是 `/log` 唯一写者(两条队列在 `syslog_drain` 尾部统一排空),FTP 可直下。→ `docs/子系统设计/运行与自检记录设计说明.md`
- **录波段文件**:`/wave`(首 boot `mkdir`,8.3 短名 WAVE 前缀轮转、启动扫盘重建下标),段头 + 通道表 + 两处 CRC-16/MODBUS + 整段自检;defaultTask 游标**分次搬运**(每次 64B 批)不独占 FatFs,落盘成功回执才解冻录波环。→ `docs/子系统设计/故障录波设计说明.md`
- **实时调试流** = 控制台单写者日志(**UART5**/RS232#1,4KB 环形缓冲 + defaultTask 唯一发送;2026-10-29 因屏板占用 USART1 自 UART5 定态),详见 `docs/子系统设计/控制台单写者日志设计说明.md`。
- NAND 坏块表(BBT)持久化在保留块池,不交给 dhara 管理。
---
## 5. 网络栈与 TCP 消费者
```mermaid
flowchart TB
CHIP[("CH395F<br/>MAC+PHY · SPI2 + DMA")] --> DRV["ch395f.c<br/>寄存器/命令 + 中断集中读取"]
DRV --> NS["net_socket.c<br/>BSD Socket API(单连接模式)"]
NS --> EVT["net_events.c<br/>边沿事件处理 + 100ms 周期对账"]
NS --> LIS["net_listener.c<br/>监听生命周期(请求登记 + 唯一执行)"]
NS --> PND["net_pending.c<br/>阻塞 recv/send 挂起请求表"]
DRV --> NSEL["net_select.c<br/>select/poll 多路复用"]
NS --> Q["netMsgQueue<br/>(串行化到 netTask)"]
Q --> NT["netTask<br/>net_poll() + net_process_messages()"]
NS --> C1["lftpd<br/>FTP :21"]
NS --> C2["time_sync<br/>对时 :8888"]
NS --> C3["ui_net_sink<br/>RemoDispBus :7003"]
NS --> C4["iec104_serv<br/>104 上行 :2404"]
C3 -.->|"回送 1bpp 帧 + 接收远端按键"| CLIENT["DTU-RemoteLCD 客户端"]
NC["net_cfg<br/>IP/掩码/网关/MAC"] --> NS
style NT fill:#efe,stroke:#484
```
**单连接约束**:CH395F 配置为 8 Socket(索引寻址)+ TCP Server 单连接(`set_fun_para(0x08)`,bit3 SOCKET_CLOSE;bit0 保留必须为 0),Socket 0 复用监听与数据通道;FTP / 对时 / 远程显示 / IEC104 四个 TCP 服务**串行**占用连接,连接断开后自动回到 LISTEN。人机显存的另一路出口(屏板)走 USART1 串口、不经 TCP——显存**一份三出**:屏板(主)/ :7003(网络镜像)/ 模拟器(同源像素)。
> **FTP 启动门(2026-09-21 修复)**:`ftpTask` 必须等 `NET_READY` **且** `STORAGE_READY`(两位均 `NoClear`)才 `lftpd_start`——此前就绪位被其它等待者消费,FTP **静默不监听**(详见 §2.2 与 `docs/测试说明/板级测试/FTP_Test_Guide.md` §8.6)。
> **单客户端实测注意**:同一时刻只服务 1 个会话,旧会话异常断开后经 `net_listener` 重建监听(实测恢复窗 p50 **208 ms**,n=30;口径见 `docs/网络/网络自愈架构设计.md`)。测试脚本必须内置连接重试(`test/host/ftp_up_dl_test.py`、`test/host/ftp_regression.py` 已内置),且**不要用裸 TCP 工具探测 21 端口**(探测会占掉唯一会话,见 `docs/测试说明/板级测试/FTP_Test_Guide.md` §8.7)。
> **阻塞模型(2026-10-14 SEND 二期落地)**:阻塞 recv/send 经 `net_pending` 挂起请求表续作,调用方铁律 `sent < len`(末块写完即返回、不得登记挂起);架构与五路语义见 `docs/网络/Net层阻塞模型架构设计.md`,监听重建/沉降隔离/周期对账见 `docs/网络/网络自愈架构设计.md`。
> 2026-10-14 实测:FTP 回归连续 19 次 11/11;`ftp_integrity_test` 20 轮 × 64 KB STOR/RETR SHA-256 20/20 一致,吞吐稳定 ≈80 KB/s。
---
## 6. UI 层架构(框架 + 注册表驱动的页面插件)
```mermaid
flowchart TB
subgraph LOGIC["逻辑层"]
UM["ui_menu.c<br/>级联框状态机 + 按键分发"]
MT["ui_menu_tab.c<br/>菜单树生成物(ui_menu_tree.txt)"]
RG["ui_page_registry.c<br/>节点→页面注册表(生成物)"]
end
subgraph VIEW["显示层"]
MV["ui_menu_view.c<br/>框体/行/装饰自绘"]
DECO["ui_deco.c<br/>浅 V 装饰"]
end
subgraph FRAME["内容页框架"]
CP["ui_content_page.c<br/>屏生命周期 + 描述符驱动绘制/按键"]
end
subgraph DATA["数据供给(写读分离)"]
FDR["App/app/ui_data_feed.c<br/>唯一填充者(uiTask 节拍)"]
DSC["ui_data.h<br/>发布 API:rt/par/info 三帧快照"]
end
subgraph PLUGIN["页面插件(20+ 页,每页一文件)"]
G1["实时量:ac / dc / yx / device_info"]
G2["参数定值:view_param / view_set /<br/>view_word / view_plate(整定·查看双入口复用)"]
G3["三遥设置:deadband / calib …"]
G4["装置维护:clock / reset / wave_manual / clear"]
G5["通讯参数:comm / net_cfg"]
G6["记录查询 ×10:soe / accident / oper / alarm /<br/>start / yk / selftest / runlog / runreport / wave_play"]
end
subgraph PORT["显示端口"]
UP["ui_port.c<br/>160x160 1bpp 内存帧缓冲"]
SINK["ui_net_sink.c<br/>远程推流 :7003(显存镜像)"]
STBY["ui_standby.c<br/>待机屏(空闲 60s 切入)"]
end
SB["屏板链路 ScreenLink(USART1 700 kbaud)<br/>帧 B 上屏(主出口)· 按键 → ui_port_queue_key"]
WR["录波数据<br/>wave_ring 封存区 / 搬运快照"]
UM --> MT
UM --> MV
UM --> CP
UM --> STBY
MV --> DECO
RG --> CP
CP --> G1 & G2 & G3 & G4 & G5 & G6
FDR --> DSC
DSC -.->|"页面只读快照"| G1 & G2 & G6
G6 -.->|"回放取数"| WR
MV --> UP
SINK --> UP
UP --> SB
SB -.->|"召唤帧带按键"| UM
G5 -.->|"net_cfg RAM 副本"| CFG["net_cfg"]
PASSWORD["password 页(#P 叶子统一入口)<br/>ui_pwd_def.h 编译期:查看 000 / 修改 123"] -.-> UM
```
- **单一事实源 = 仓根 `ui_menu_tree.txt`**(61 行,16 个受保护叶 ` #P`):缩进即层级,`#page=<模块>` 把叶子绑到 `ui_page_<模块>.c` 的描述符;重跑 `tools/menu_gen/gen_menu.py` 生成 `ui_menu_tab.*` 与 `ui_page_registry.*`,并**自动把新页写进双端注册文件**(`uvprojx` + `CMakeLists.txt`)。写了却不绑定的 `ui_page_*.c` 生成器直接报错(不留死代码)。
- 内容页为**独立全屏屏**,由框架一次性自绘(**零 LVGL 子对象**),ESC 返回菜单屏。
- 菜单受保护叶子(行尾 ` #P`):先弹密码页,授权后 `menu_password_ok` **先关页再续进入动作**。
- **数据供给**:页面不直接伸手进业务模块,一律读 `ui_data.h` 三帧快照(实时量/参数/装置信息);写侧唯一填充者 `ui_data_feed`(装配层,可双向 include 不违分层)。契约属主 `docs/UI设计/UI_数据供给设计说明.md`。
- **显存一份三出**(§5):屏板串口为主出口(4208 B 帧 B 点阵,memcmp 判变才组帧),:7003 为网络镜像,模拟器与固件同一份 UI 源码;屏板按键经 uiTask 映射 `UI_KEY_*` 回注。→ `docs/子系统设计/屏板链路设计说明.md`
- 录波两页:`wave_manual`(装置上手动触发录波)与 `wave_play`(现场单通道回放)——判据与显示契约见 `docs/子系统设计/故障录波设计说明.md` §8。
- 待机屏:空闲超时(默认 60s)自动切入,仅确认键回菜单;数据源同实时量页。
- 字体 `ui_font_cn_12.c` 由 `tools/font_gen/` 生成(宋体 12px 1bpp 子集,**224 字/319 字形**,2026-10-29 实测;字形数 = 字符数 + ASCII 95 个)。
---
## 7. 目录组织
```
STM32F4-Base/
├── Src/ Inc/ CubeMX 生成:main.c / freertos.c / HAL 外设 / 中断 / 时基
│ ├── freertos.c 9 任务 + netMsgQueue + sys_evt 创建(CubeMX 接线 + 崩溃钩子;任务体在 App/task/)
│ └── main.c 入口 → app_main_init → osKernelStart
├── App/ 应用层(业务编排,非 CubeMX)
│ ├── app/ 启动编排与全局契约:app_main.c(时间子系统优先)、app_cfg.h、sys_events.h、
│ │ ui_data_feed(App→UI 数据装配,快照唯一填充者)
│ ├── algo/ 采集与算法层(除 adc_input.c 外零平台依赖 → 主机单测免桩):
│ │ adc_input / adc_health / adc_cal / sample_ring / phasor / freq / meas /
│ │ prot_set / prot_elm / run_par / wave_ring / wave_store / sb_led_map
│ ├── task/ default_task / net_task / ui_task / adc_task / iec104_serv / protect_task /
│ │ rs485_task*(*未创建,B2 暂缓)+ task_handles.h
│ ├── protocol/iec104/ IEC 60870-5-104:iec104_asdu / iec104_link / iec104_pts(点表:遥测读 meas、遥信读 prot_elm)/ iec104_comm([comm] 段)
│ ├── log/ sys_log 记录引擎(定长 64B,/log 唯一写者=defaultTask)、
│ │ console_log(UART5 控制台单写者)、soe_rec(64B SOE 纯编解码)+ soe_store(投递/落盘胶水)
│ ├── cfg/ 配置引擎:cfg_file(静态态单任务收敛于 defaultTask)、store(MCU=FreeRTOS 堆)、
│ │ prot_set_store(/prot_set.bin 原子写,B7)
│ ├── fs/ 卷锁 vol_lock(syslog / cfg / lftpd 三条路径 try 语义)、vol_idle_sync
│ ├── sync/ time_sync(TCP 对时服务)
│ └── util/ sys_clock(墙钟)、task_watchdog(IWDG 判定状态机)、boot_stats、sha256、task_prof
├── Drivers/BSP/ 纯手工驱动/服务(不受 CubeMX 保护)
│ ├── CH395F/ 以太网 MAC+PHY(SPI2 + DMA)
│ ├── GD5F2GQ5UE/ NAND + dhara FTL
│ ├── TPAFE5160/ ADC(并行 16 位)
│ ├── SD2506/ RTC
│ ├── RS485/ 半双工串口(UART4 + DE;兼全局 UART 回调唯一实现处)
│ ├── ScreenLink/ 屏板链路(USART1 700 kbaud):sb_proto(纯协议,主机单测)+ sb_link(HAL/中断传输层)
│ ├── NET/ net_socket / net_select / net_events / net_listener / net_pending / lftpd
│ ├── NETCFG/ 网口配置(INI [network] 段)
│ ├── UI/ LVGL UI(与模拟器共用):ui_port / ui_menu* / ui_content_page / ui_page_*(20+ 页)/
│ │ ui_page_registry(生成)/ ui_data / ui_net_sink / ui_standby / ui_font_cn_12 / ui_pwd_def.h
│ └── dbg_log.h/dbg_cfg.h/app_assert.h/bsp_err.h 分级日志与 BSP 契约
├── Lib/ 第三方:LVGL v9.2、FatFs、dhara + lv_conf.h(MCU 端)
├── Middlewares/ FreeRTOS V10.3.1(ST 官方中间件)
├── Drivers/STM32F4xx_HAL_Driver/ + Drivers/CMSIS/ ST HAL / CMSIS(裁剪版)
├── MDK-ARM/ Keil 工程(uvprojx + scatter + build/download 脚本)
├── simulator/ PC 模拟器(CMake + SDL2,共用 UI 源码 + tests/ref 像素黄金参考回归)
├── test/ 测试套件(test_config.h 互斥选择,产品构建空转)
│ ├── board/ 板端套件(.c/.h:CH395F / 存储 / RTC / ADC / RS485 / UI)
│ └── host/ 主机侧单测与脚本(44 套带 run_unit_test.py,覆盖率总闸全量真跑)
├── ui_menu_tree.txt 菜单树声明源(生成器输入,仓根)
├── ui_cn_chars.txt 中文字库输入(生成器输入,仓根)
├── tools/ font_gen / menu_gen / arch_diagram(文档配图生成器)/ refgraph(引用图)/
│ setview_check / netlist_semantics(网表语义)
├── scripts/ 门禁与报告(check_* / gen_* / ci_local.py / stack_report / ram_report /
│ rename_ref / rename_ids / bom…)
└── docs/ 设计/测试/规范文档(人类索引 docs/README.md)
```
---
## 8. 构建与双端
```mermaid
flowchart LR
SRC["共享源码<br/>Drivers/BSP/UI/*<br/>Drivers/BSP/NETCFG/net_cfg.c<br/>App/cfg/cfg_file.c"]
SRC --> MCU["MCU 端<br/>Keil MDK-ARM (ARMCC v5)<br/>/keil-build → BUILD_OK"]
SRC --> SIM["PC 模拟器<br/>CMake + SDL2 (MSVC)<br/>lvgl_sim.exe --keys"]
MCU --> TEST["/serial-boot-test:串口采集 + 烧录 + 日志校验"]
SIM --> REG["UI 回归:<br/>check_menu_rows.py<br/>check_content_pages.py"]
```
| 目标 | 工具链 | 验证 |
|---|---|---|
| MCU 固件 | Keil MDK-ARM v5(`/keil-build` = `keil-build` skill) | `/keil-build` 0 错 0 警 → `/serial-boot-test` 串口闭环 |
| PC 模拟器 | CMake + SDL2 + MSVC(`simulator/build/`) | `check_menu_rows.py` + `check_content_pages.py`(文本断言 + 像素黄金参考) |
**纯逻辑模块第三端**:录波(`wave_ring`/`wave_store`)、保护判据(`prot_set`/`prot_elm`)、遥测快照(`meas`)、SOE 编解码(`soe_rec`)、屏板协议层(`sb_proto`)等零平台依赖,被 `test/host/` 原样 `#include` 免桩直跑(覆盖率总闸全量真跑)——主机侧就是它们的"第三端"。
**功能编译开关**:当前**不存在**——`App/app/app_cfg.h` 自 2026-10-22 起不含任何宏(原 `APP_NET/ADC/RS485_ENABLE` 三个零消费点占位宏已删,接回方法记在该文件 v1.2),各模块一律编译进产品;裁剪测试套件的唯一开关是 `test/board/test_config.h` 互斥选择(产品构建全注释)。
---
## 9. 相关专项文档
| 主题 | 文档 |
|---|---|
| 启动顺序 / 就绪屏障 | `docs/架构设计/项目启动顺序管理.md` |
| 存储架构(NAND/FTL/FatFs) | `docs/架构设计/STM32F4-Base存储架构说明.md` |
| NET 阻塞模型(挂起请求表五路语义) | `docs/网络/Net层阻塞模型架构设计.md` |
| 网络自愈(监听重建 / 沉降隔离 / 周期对账) | `docs/网络/网络自愈架构设计.md` |
| 采集与算法层 语义取数与有效性~保护判据(**设计与判据**) | `docs/算法/采集与算法层设计说明.md` |
| 采集与算法层目录索引(文件清单 / 构建测试) | `App/algo/README.md` |
| 保护功能复现规划(分层归属 / 功能项状态) | `docs/算法/继电保护功能复现规划.md` |
| ADC 采样节拍(TIM3 1kHz 同源驱动) | `docs/子系统设计/ADC采样节拍设计说明.md` |
| ADC 通道映射(网名/板级/通道号 13 路置换) | `docs/硬件平台/ADC通道映射说明.md` |
| 故障录波(数据面/段格式/回放契约) | `docs/子系统设计/故障录波设计说明.md` |
| 屏板链路(USART1 主出口 / 帧 A 帧 B / 实测证据) | `docs/子系统设计/屏板链路设计说明.md` |
| IEC 60870-5-104 上行(:2404) | `docs/子系统设计/IEC104上行设计说明.md` |
| 配置文件引擎(/config.ini 多段注册制) | `docs/子系统设计/配置文件设计说明.md` |
| 运行与自检记录(64B 记录 + SOE) | `docs/子系统设计/运行与自检记录设计说明.md` |
| 墙钟时间维护 | `docs/子系统设计/系统墙钟时间维护说明.md` |
| 控制台单写者日志(UART5 调试流) | `docs/子系统设计/控制台单写者日志设计说明.md` |
| 看门狗 / CubeMX 重生成存活方法论 | `docs/子系统设计/看门狗设计说明.md` §10 |
| RAM 预算与优化 | `docs/内存管理/RAM使用分析与优化方案.md` |
| 栈/内存观测工具 | `scripts/stack_report.py`(Keil 静态调用图 × 串口 `[STACK]` 水位,判定取两者更坏者)· `scripts/ram_report.py`(.map 静态占用)—— 用法与基线见内存方案 §4.0.6 |
| FTP 功能与陷阱 | `docs/测试说明/板级测试/FTP_Test_Guide.md` §8.6(启动门)/ §8.7(单客户端)· 脚本 `test/host/ftp_up_dl_test.py`、`test/host/ftp_regression.py`、`test/host/ftp_integrity_test.py` |
| UI 设计架构 / 内容页 / 菜单 | `docs/UI设计/UI_设计架构.md` · `内容页架构.md` · `菜单页面设计.md` |
| UI 数据供给契约(rt/par/info 三帧) | `docs/UI设计/UI_数据供给设计说明.md` |
| RemoDispBus 远程显示协议(:7003) | `docs/UI设计/RemoDispBus协议与接收设计说明.md` |
| UI 自动化测试 | `docs/UI设计/UI_自动化测试流程.md` |
| 驱动已知陷阱 | `docs/问题记录/{CH395F,GD5F2GQ5UE,SD2506}_Trap_Records.md` |
| 各模块测试指南 | `docs/测试说明/` |
| 代码规范 | `docs/代码规范/` |