Files
STM32F4-Base/docs/架构设计/项目架构总览.md
T
wandering1 2525c2d948 docs(104): 端口口径统一成「工厂默认」,登记「改后立即重 bind」待办
用户裁定两条:① 剩余裸写端口的地方加「工厂默认」限定词,不改固件行为;
② 生效语义按需求口径「保存即生效」定,现实现(端口下次重 bind 才生效)是
要改的一侧 → 建任务登记,本笔不动代码行为。

限定词(24 处 / 17 文件):AGENTS 数据主线、App/protocol/README、根 README
文档索引、Src/freertos.c 三处任务注释、docs/README、项目启动顺序管理、存储
架构说明、软件架构与运行时规格(2)、RAM 使用分析、产品需求设计文档(5)、
项目架构总览(3)、test/host/README、iec104_master 流程注释、IEC104 上行设
计说明、继电保护功能复现规划。配图两张同批重跑:gen_arch_svg.py 与
gen_memory_svg.py(脚本与产物同一笔)。

两处陈旧计数(都做过双源核证):
- 测试开发标准流程 像素层「60 内容页」→ 内容页黄金参考 66 张(64 页);
  双源 = simulator/tests/ref/shot_*.bin 实数 66 与 check_content_pages.py
  的 PAGES 66 条(含两个 _p2)。
- .pre-commit-config.yaml 的 uinav-selftest 注释「18 张黄金参考」→ 写成
  「接线当时 18 张、现 66 张」:git log -S 定到 6a904c1b(2026-10-25),那
  天 ref 目录确实 18 张,写时为真,但它描述的是钩子现有行为,属当前态。

口径修正与登记:
- 页面扩充详细方案「通讯设置 = 无配置模型(端口与 CA 都是 #define)」→ 标
  「当时」,并写明现已在 /config.ini 的 [comm] 段可配、UI 通讯设置页可改。
- 产品需求 §通讯配置:删掉「改后需重启生效」这句把两件事混为一谈的话,改成
  需求要求「保存即生效」+ 现实现(公共地址重启、端口下次重 bind)+ 指向登记簿。
- docs/任务规划/登记簿.md §1 新增「104 端口改后立刻重 bind」:含隐性耦合
  —— .serialtest/config.json 的必检锚点写死 listening on 2404,端口一改串口
  回归必假失败;改行为时要么锚点只查 listening on,要么由脚本注入端口。

复扫(旧形态归零判据)剩 20 处未带限定词,逐条定性为刻意不改:默认值定义处、
出厂模板与断言、usage 与 argparse default、UI 黄金参考显示值、台架主站地址
(同数不同义)、串口锚点、叙事句、引用关系图坐标。逐条理由见一次性台账。

门禁:check_doc_links --sections-strict 对 16 篇改动 .md 逐个 rc=0;
check_mermaid --all 31 文件 62 围栏新发 0;check_nested_comment --all 449
文件新发 0;check_bom_drift --all 99 个名单内文件无漂移;
check_generated_fresh PASS(25 生成器 / 21 一致 / 4 只减不增基线);字节体检
无新增 BOM / CR / U+FFFD / 行尾空格(三连空行与 HEAD 逐项相同 = 存量)。
/keil-build 0 错 0 警,Program Size Code=246720 RO=29836 RW=2052 ZI=149708
与凭证同码:本笔只碰注释与配图文字,无固件行为变更。
2026-10-30 15:40:27 +08:00

36 KiB
Raw Blame History

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 项目分层架构

上图由 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.3 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 两条队列 → syslog 七本 + soe.log + 控制台排空)+ 录波落盘道(已封存段游标分次搬 /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)

三栏式:左 = 生产者(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. 启动时序

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. 存储栈

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 引擎按 App/log/sys_log_mcu.c 的 s_paths[] 写七本定长 64B 文本记录——/log/run.log、selftest.log、accid.log、alarm.log、start.log、oper.log、yk.log;soe_store 同先例落 /log/soe.log(64B 定长二进制,容量满删除重建);/log/wdev.log 是变长取证行(掉电不丢,App/fs/vol_lock 侧投,不走 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 消费者

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 层架构(框架 + 注册表驱动的页面插件)

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.3、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/              板端套件:CH395F(硬件层 + NET 层 Phase 6~11 + 链路时延探针)/ GD5F /
│   │                       存储性能 / SD2506 RTC(逐文件清单 = `test/board/README.md`;无 ADC / RS485 / UI 板端套件)
│   └── host/               主机侧单测与脚本(47 套带 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. 构建与双端

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,可经 [comm] 改) 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/代码规范/