docs(design): 运行/自检记录与 ADC 节拍两篇改为现状设计并去掉档名"说明"

两篇规划期文档改成对当前系统的描述,档名统一去掉"设计说明"的"说明"。

运行与自检记录:
- 旧档重写为现状版(10 节:概览 / 记录布局 / 写入路径 / 读取与呈现 /
  事件分类 / 控制台日志 / 双端差异 / 内存 / 异常恢复 / 自测要点),
  删掉需求表、调研摘要、模块清单动作列、实施步骤与开放问题。
- 按代码纠正:七类记录齐备(原文只写了两类)、自检描述串换成实际
  落盘的 28 条、待机阈值以 ui_tokens.h 为准、记录页已走 ui_w_records
  共享组件、容量满只删该文件而非整卷清空。
- 待机屏移出本篇:它不属于日志,归 UI 侧;内容页架构补"待机屏
  (覆盖层)"一节,产品需求 FR-LOG-3 拆成单纯的上电/重启分类,
  待机显示归口已有的 FR-UI-3。

ADC 采样节拍:
- §4.5 从 45 行压到 24 行,只留规则 / 违反的代价 / 为什么 / 兜底与
  档案指针;九路置换实测表、A/B 对照、探针计数等取证归
  ADC_Trap_Records.md 的 TRAP-ADC-01(那里已有完整档案)。
- adc1_gap 字段去掉"2026-10-21 起、此前固件为次数"的修订记录式括号。
- app_main.c 的注释改为指向 TRAP-ADC-01 取证、§4.5 设计口径。

引用同步:两个旧档名全仓替换(含代码注释、角色档、归档 id 表),
旧名残留 0;对 ADC 篇的 § 引用逐条复核落在真实章节上。

验证:48 个提交期钩子全通过,check_doc_links --sections-strict 通过。
This commit is contained in:
2026-11-15 21:30:07 +08:00
parent de6d5d863a
commit f5902bcc21
47 changed files with 413 additions and 469 deletions
+2 -2
View File
@@ -91,9 +91,9 @@ docs/
| [`网络配置说明.md`](子系统设计/网络配置说明.md) | 网口设置页:IP/掩码/网关,UI 保存即时生效 |
| [`系统墙钟时间维护说明.md`](子系统设计/系统墙钟时间维护说明.md) | `sys_clock`:RTC 基准、tick 回绕防护、30 天惰性再基准化 |
| [`控制台单写者日志设计说明.md`](子系统设计/控制台单写者日志设计说明.md) | 控制台日志单写者模型(避免多任务交错输出) |
| [`运行与自检记录设计说明.md`](子系统设计/运行与自检记录设计说明.md) | `/log/run.log`·`selftest.log` 定长记录与待机显示 |
| [`运行与自检记录设计.md`](子系统设计/运行与自检记录设计.md) | `/log/*.log` 七类 64B 定长记录、SOE 二进制记录、控制台日志、待机屏与七个记录页 |
| [`看门狗设计说明.md`](子系统设计/看门狗设计说明.md) | IWDG(CubeMX 配置)+ 任务心跳位图:选型与超时推导、掩码与埋点、取证机制、验证记录;已台架闭环(V2/V4/V5)。**§11 = 任务级饥饿的诊断方法与 `[PROF]` 实测基线**(诊断沉淀:判读口径、量化归因、三条脆弱点、明确不做的四条) |
| [`ADC采样节拍设计说明.md`](子系统设计/ADC采样节拍设计说明.md) | **ADC 采集链路采样节拍(现状说明)**:TIM3 1 kHz 更新事件同时触发 TPAFE CONVST 与(经 TRGO)STM32 ADC1 + DMA 乒乓缓冲;节拍源为什么是 TIM3、DMA 硬约束、事件驱动消费与就绪门、实现落点、观测计数、性能特征与设计边界 |
| [`ADC采样节拍设计.md`](子系统设计/ADC采样节拍设计.md) | **ADC 采集链路采样节拍(现状说明)**:TIM3 1 kHz 更新事件同时触发 TPAFE CONVST 与(经 TRGO)STM32 ADC1 + DMA 乒乓缓冲;节拍源为什么是 TIM3、DMA 硬约束、事件驱动消费与就绪门、实现落点、观测计数、性能特征与设计边界 |
| [docs/子系统设计/故障录波设计说明.md](子系统设计/故障录波设计说明.md) | **故障录波设计与口径属主**:1 kHz 起步与 80 点口径的偏离登记、存原始码值的理由、四段所有权与四条并发陷阱、段文件字节布局、现场单通道回放显示契约、预算与 R6 前置 |
| [`IEC104上行设计说明.md`](子系统设计/IEC104上行设计说明.md) | **IEC 104 上行规约**:IEC 60870-5-104 被控站(TCP 工厂默认 2404)范围/点表(V0.1 有效子集)/链路流程(k/w/t1~t3、总召、COV、遥控两段式、定值)/netTask 接入决策/预算与测试方案;协议核心纯逻辑 + 主机侧纯逻辑单测全栈单测 |
| [`屏板链路设计说明.md`](子系统设计/屏板链路设计说明.md) | **屏板串口链路(主机侧,`Drivers/BSP/ScreenLink/`)设计**:链路与角色、USART1 700 kbaud 配置与中断收发、物理接线(含台架日志口与 PC 顶替屏板)、协议实现口径(CRC 字节序 / 整帧 DMA / 时序)、键值与状态灯映射表、资源与耗时、对外接口表、验证状态与未闭环项、坑账本 |
+28 -1
View File
@@ -480,7 +480,34 @@ extern const content_page_def_t ui_page_device_info_desc; /* 描述符(Flash
先肉眼确认新效果正确,再 `check_content_pages.py --update` 刷参考(`ref/*.bin` 已入库,刷了会进 diff,必须一并提交并说明)
- 新增页面后同步在 `check_content_pages.py` 增加该页的按键序列、文本断言与参考图
## 九、变更记录
## 九、待机屏(覆盖层)
待机屏不是内容页,而是盖在活动屏之上的**覆盖层**(`Drivers/BSP/UI/ui_standby.c`,与内容页框架平级的显示端口模块)。它由 `uiTask` 在 `ui_menu_init` 之后调 `ui_standby_init(UI_STANDBY_IDLE_MS, ui_task_time_cb)` 装好。
**进入条件**:60 s 内无任何按键(`UI_STANDBY_IDLE_MS = 60000`,定义在 `Drivers/BSP/UI/ui_tokens.h`)→ 切待机屏。空闲检测 = 1 s 周期的 `lv_timer`(MCU 与模拟器都经 `lv_timer_handler` 驱动,无 OS 依赖);活动通知挂在 `menu_key_cb` / `cp_key_cb` 上,每收到一个键调 `ui_standby_notify_activity()` 重置计时。
屏上是 UA / UB / UC / IA / IB / IC 六路实时值与底部时钟:
```
UA = 0.341 ← 六路实时值,取自 ui_data 通道一的 yc[],下标 = 语义序
UB = 0.338 UA/UB/UC = 0..2,IA/IB/IC = 4..6;UN/IN 不上屏
UC = 0.339
IA = 0.021 ← 现在的显示值是引脚电压档:数字照显、不挂单位
IB = 0.020 (挂单位等于替用户完成没做的标定换算)
IC = 0.021 ← 升到二次/一次值档后,电压挂 V、电流挂 A
────────────────── ← 分隔线
YYYY-MM-DD HH:MM:SS ← 底部实时时钟(每秒重绘)
```
- 数值为空或品质带 `IV|NT|RAW|SB|BL|OV` 时显示 `--`;3 位小数定点格式化复用 `ui_w_editor_fmt_scaled`,不用 `%f`。口径的唯一真源是交流量页 `ui_page_ac.c`。
- 帧读取按 `ui_data_rt_seq()` 门控整帧拷贝;1 s 定时器整屏 invalidate 就是刷新节拍,没有新增定时器。
- **键语义**:只有 ENTER 进菜单,其余按键忽略(但同时重置空闲计时)。
- **覆盖层做法**:进入时保存当前活动屏 + 组内焦点对象,加载待机屏并聚焦其容器;ENTER 时还原。因此内容页打开期间超时同样切待机,返回后原样恢复,编辑态不丢。
- 布局常量(`ui_standby.c` 头部):首行 `SB_ROW_Y0=28`、行距 `SB_PITCH=18`、分隔线 `SB_LINE_Y=132`、时钟 `SB_TIME_Y=140`。
- 无头回归:模拟器 `--standby` 注键后经 `ui_standby_force_enter()` 直接切入并截图;窗口模式 `--idle <秒>` 走真超时路径。
- 真机串口标记:Required `UI standby: ready (idle=60s)`。
## 十、变更记录
| 日期 | 变更 |
|---|---|
+2 -2
View File
@@ -446,7 +446,7 @@ int run_par_set_deadband(uint8_t yc_idx, float v); /* 范围校验 */
> **本表只是页面侧的需求清单**(记录页要消费哪几类),**不是记录层的登记表**:
> 5 个 ID 的定名、记录格式、条数与字节上限、写入时机、以及是否真的落地,一律以
> `App/log` 侧为准,权威口径 `docs/子系统设计/运行与自检记录设计说明.md`;
> `App/log` 侧为准,权威口径 `docs/子系统设计/运行与自检记录设计.md`;
> 实施进度由记录侧任务回报后回填本文(本道不代记,免得同一件事在两份文档里各说一遍——
> 同一事实只在一处详述,见 `docs/文档规范.md`)。
@@ -482,7 +482,7 @@ void ui_w_soe_refresh(ui_w_soe_t *inst, uint16_t row, char *name, char *valu
**运行报告(最小可用版)**:3 行——`最近动作`(元件名)、`动作时间`、`当前状态`。数据取最后一条 `SOE_T_ELM_ASSERT`。完整版(故障前后有效值 + 波形摘要)依赖录波,不在本批。
**记录容量**:新增 5 类日志需各自定上限(参考现有:运行 1024 条/64 KB、自检 512 条/32 KB)。按"事故/告警/启动各 512 条 + 操作/遥控各 256 条"计 ≈ **128 KB**(64 B/条),NAND 余量需按 `docs/子系统设计/运行与自检记录设计说明.md` 的容量口径核对。
**记录容量**:新增 5 类日志需各自定上限(参考现有:运行 1024 条/64 KB、自检 512 条/32 KB)。按"事故/告警/启动各 512 条 + 操作/遥控各 256 条"计 ≈ **128 KB**(64 B/条),NAND 余量需按 `docs/子系统设计/运行与自检记录设计.md` 的容量口径核对。
### 3.7 装置信息(回炉)
@@ -374,9 +374,9 @@ flowchart LR
- 实现:4 KB 环形缓冲 + `defaultTask` 唯一发送(行级原子);内核前/ISR/异常钩子有界直发回退。
- 判据:日志无交错;崩溃时能输出归因日志。
**FR-LOG-3 上电/重启分类与待机显示**
- 需求:区分上电与重启;空闲切待机屏。
- 实现:`run.log` 末条 epoch 时间差推断;硬件复位原因(`RCC_CSR`)优先;`ui_standby` 空闲 60 s 切入。
**FR-LOG-3 上电/重启分类**
- 需求:区分上电与重启。
- 实现:`run.log` 末条 epoch 时间差推断;硬件复位原因(`RCC_CSR`)优先。
### 5.6 UI 与远程显示(FR-UI)✅
@@ -1808,7 +1808,7 @@ pre-commit run --all-files
| 配置引擎 | [`docs/子系统设计/配置文件设计.md`](../../docs/子系统设计/配置文件设计.md) |
| 网络配置 | [`docs/子系统设计/网络配置说明.md`](../../docs/子系统设计/网络配置说明.md) |
| 墙钟时间 | [`docs/子系统设计/系统墙钟时间维护说明.md`](../../docs/子系统设计/系统墙钟时间维护说明.md) |
| 运行/自检记录 | [`docs/子系统设计/运行与自检记录设计说明.md`](../../docs/子系统设计/运行与自检记录设计说明.md) |
| 运行/自检记录 | [`docs/子系统设计/运行与自检记录设计.md`](../../docs/子系统设计/运行与自检记录设计.md) |
| 控制台单写者日志 | [`docs/子系统设计/控制台单写者日志设计说明.md`](../../docs/子系统设计/控制台单写者日志设计说明.md) |
| 看门狗 | [`docs/子系统设计/看门狗设计说明.md`](../../docs/子系统设计/看门狗设计说明.md) |
| BSD Socket API | [`docs/网络/BSD_Socket_API_使用指南.md`](../../docs/网络/BSD_Socket_API_使用指南.md) |
@@ -1821,7 +1821,7 @@ pre-commit run --all-files
| 保护功能复现规划(产品定义 / 定值 / 路线) | [`docs/算法/继电保护功能复现规划.md`](../../docs/算法/继电保护功能复现规划.md) |
| 全周傅氏算法原理 | [`docs/算法/傅氏算法详解.md`](../../docs/算法/傅氏算法详解.md) |
| 采样有效性校验 | [`docs/算法/采样有效性校验设计说明.md`](../../docs/算法/采样有效性校验设计说明.md) |
| ADC 采样节拍与 DMA | [`docs/子系统设计/ADC采样节拍设计说明.md`](../../docs/子系统设计/ADC采样节拍设计说明.md) |
| ADC 采样节拍与 DMA | [`docs/子系统设计/ADC采样节拍设计.md`](../../docs/子系统设计/ADC采样节拍设计.md) |
| ADC 通道映射与实测 | [`docs/硬件平台/ADC通道映射说明.md`](../../docs/硬件平台/ADC通道映射说明.md) |
| ADC 前端注入验证流程 | [`docs/测试说明/专项验证/ADC前端注入验证流程.md`](../测试说明/专项验证/ADC前端注入验证流程.md) |
| IEC 60870-5-104 上行 | [`docs/子系统设计/IEC104上行设计说明.md`](../../docs/子系统设计/IEC104上行设计说明.md) |
@@ -200,50 +200,30 @@ const uint16_t *src = &s_stm32_dma[half * ADC_INPUT_STM32_CH_NUM];
- 取帧状态码:`ADC_INPUT_RC_OK` = 0 / `ADC_INPUT_RC_START` = -1 挂载失败 /
`ADC_INPUT_RC_STALL` = -2 本拍无新帧 / `ADC_INPUT_RC_NOT_RUN` = -3 尚未取过帧。
### 4.5 使能时机与 DMA 缓冲相位(+2 格旋转,2026-10-27 实测)
### 4.5 使能时机:挂 DMA 那一瞬 ADON 必须为 0
**这条是本节最反直觉的一条:DMA 缓冲会整体错开 2 格,而寄存器配置全对、拷贝循环也全对。**
**规则**:`app_main_init()` 步骤 4b-pre 先调 `adc1_disable()` 把 ADC 关掉,再调 `adc_input_init()`
挂 DMA;使能动作交给 `HAL_ADC_Start_DMA()` 内部完成——它自己 `__HAL_ADC_ENABLE` + 等 Tstab
(约 20 µs),然后紧接着开 `CR2.DMA` 并启流。
**现象。** 台架把某个端子接地,读数却出现在**另一个通道**上——例如接地 `AIN2`(`UB` / PC4 / CH14),
`UB` 读出 784 码(浮空值),而 `IB` 读出 0。表象与"映射表写错"完全一致,但
`SQR1/SQR2/SQR3` 实测为 CH0..CH7 + CH14、`L=8`,`k_stm32_rank_ch[]` 与拷贝循环也逐条核过,全对。
⚠ **顺序不可调换**:`adc1_disable()` 必须严格早于 `adc_input_init()`。
**证据。** 同一次开机内,先用无 DMA 单通道方式(逐路关扫描、软件触发、直接读 `DR`)取真值,
再与 DMA 路径的同一批输入逐路对照,得到 9/9 完美置换:
**违反的代价**:不报任何错,只是 9 路通道按 rank 序**整体错开 2 格**(槽 `i` 装的是
rank `((i+1) mod 9)+1`)。表象与"映射表写错""注入点标签不对"**完全一样**,
而 `s_stm32_raw[k_stm32_rank_ch[i]] = src[i]` 的恒等性**照常成立**——旋转后的缓冲被原样拷贝,
看起来完美自洽,所以拿它判映射会得出相反结论。
| 槽(rank) | 通道 | DMA 值 | 实际装的是 | 真值 | 差 |
|---|---|---|---|---|---|
| 1 | CH0 | 850 | CH2 | 850 | 0 |
| 2 | CH1 | 932 | CH3 | 931 | 1 |
| 3 | CH2 | 853 | CH4 | 852 | 1 |
| 4 | CH3 | 819 | CH5 | 817 | 2 |
| 5 | CH4 | 946 | CH6 | 945 | 1 |
| 6 | CH5 | 954 | CH7 | 953 | 1 |
| 7 | CH6 | 0 | CH14 | 0 | 0 |
| 8 | CH7 | 690 | CH0 | 691 | 1 |
| 9 | CH14 | 790 | CH1 | 785 | 5 |
**为什么**:触发条件是「ADC 被使能之后闲置了数秒才挂 DMA」。本工程 `MX_ADC1_Init()` 在 `main()`
里就调用(使能 ADC),而挂 DMA 在 `app_main_init()` 步骤 4b,中间隔着 GD5F 全片 BBT 扫描(数秒),
ADC 在这段时间里处于 `ADON=1` 的空转态。不需要整个闲置期 `ADON` 都为 0,只要**使能与搬运之间不留
可观测间隔**,旋转就不出现。微架构层面为何恰好错开 2 格尚未证出——不要为"解释清楚"去猜,
也不要据此改动 ADC 配置。
即**槽 i 装的是 rank ((i+1) mod 9)+1 —— 整个缓冲从 rank 3 开始搬,固定错开 2 格**。
差值全在 5 码以内(两种读法的噪声),不是随机抖动。
**兜底**:改动被删时由审计项 `adc1-calibration-impl` 的基线漂移拦下(它整段守护 `Src/adc.c`
的 `USER CODE BEGIN 1`)。
**判据上的陷阱。** `s_stm32_raw[k_stm32_rank_ch[i]] = src[i]` 的恒等性**不能**用来证明映射正确:
DMA 从序列中途开始搬时,缓冲整体旋转,拷贝照样是恒等的。判映射只能靠
**按通道号索引的独立真值**(无 DMA 单通道读),或按 §4.5 的修法消除旋转后再比对。
**根因。** 触发条件是「ADC 被使能之后**闲置了数秒**才挂 DMA」。本工程 `MX_ADC1_Init()` 在 `main()`
里调用,而挂 DMA 在 `app_main_init()` 步骤 4b,中间隔着 GD5F 全片 BBT 扫描。台架对照:
"使能后闲置数秒才挂" → 7/7 次都是 +2 旋转;"使能与挂 DMA 在同一时刻" → 6/6 次旋转为 0
(探针固件 5/5 + 同一次开机内重新挂 DMA 1/1)。
**微架构层面的确切原因(为何恰好丢 2 拍)尚未证出**,但工程结论是确定的:使能与搬运不该有可观测间隔。
**修法。** `app_main_init()` 步骤 4b-pre 调 `adc1_disable()`(实现见 `Src/adc.c` 的 `USER CODE` 区)
先把 ADC 关掉,使能动作交给 `HAL_ADC_Start_DMA()` 内部完成——它自己 `__HAL_ADC_ENABLE` +
等 Tstab(约 20 µs),然后紧接着开 `CR2.DMA` 并启流。台架验证:在 `MX_ADC1_Init()` 末尾直接关闭
4/4 次、在步骤 4b-pre 调本 API 3/3 次,均 `UB=0`,同开机对照差值 ≤4 码。
⚠ **顺序不可调换**:`adc1_disable()` 必须严格早于 `adc_input_init()`;若 ADC 在挂 DMA 时仍处于使能态,
旋转会重新出现。改动被删时会由审计项 `adc1-calibration-impl` 的基线漂移拦下
(它整段守护 `Src/adc.c` 的 `USER CODE BEGIN 1`)。
踩坑经过、判读陷阱与回归判据另见 `docs/问题记录/ADC_Trap_Records.md` 的 `TRAP-ADC-01`。
完整档案(九路置换实测表、A/B 对照、判读陷阱、回归判据)→ `docs/问题记录/ADC_Trap_Records.md`
的 `TRAP-ADC-01`。
---
## 5. TPAFE 侧:CONVST 脉冲与 BUSY 读帧
@@ -327,7 +307,7 @@ t = 1000 µs 下一个更新事件
| `ring` | `sample_ring_frames()` | `ring == frames` | 不等 → 有 `idle` / `skip` 拍被压进环(回归恒等式) |
| `adc1_rc` | 最近一拍取帧状态 | 0(`ADC_INPUT_RC_OK`) | -2 本拍无新帧;-1 DMA 挂载失败 |
| `adc1_seq` | `s_dma_seq`(半传输 + 整传输共用) | ≈ `seq`,**恒定小偏移** | 偏移持续增大 → STM32 侧在丢 |
| `adc1_gap` | Σ(`seq` 每次前进的超出部分) = **丢掉的帧点数**(2026-10-21 起;此前固件为"次数") | 每 10 s 心跳 +1 点 | 快速上涨 → 本任务单拍 > 1 ms(看 `adcm`) |
| `adc1_gap` | Σ(`seq` 每次前进的超出部分) = **丢掉的帧点数** | 每 10 s 心跳 +1 点 | 快速上涨 → 本任务单拍 > 1 ms(看 `adcm`) |
| `adc1_err` | **连续 ≥ 2 拍**无新帧 | 0 | > 0 → 节拍源 / DMA 真停 |
| `adc1_hw` | `HAL_ADC_ErrorCallback()` 次数 | 0 | > 0 → ADC / DMA 硬件报错 |
| `tgap` | `adc_health_tpafe_gaps()`(「采样有效性校验」) | 极少 | TPAFE 侧丢帧,与 `adc1_gap` **同单位(帧点数)**,可直接对比 |
@@ -3,7 +3,7 @@
> **版本**:v1.8(已实施;修订史见文末 §11「实施时间线」)
> **日期**:2026-10-30(v1.8:控制台端口由 USART1 改为 **UART5/RS232#1**,全文端口口径同步——物理口与台架端子见 `屏板链路设计说明.md` §2 / §3.2;v1.7 于 2026-10-26:崩溃上报归一进单写者 + 多段裸发收口,道G task-26;v1.6 同日:未注册期裸发加「行级单一所有者」;v1.5 于 2026-10-24;初版 v1.0 于 2026-09-25)
> **状态**:已实施并验证——`/keil-build` 0 错误 0 警告、`/serial-boot-test` 硬件闭环 PASS(2026-09-25,见 §10);设计 2026-09-24 加固实施(当时位于 `Src/usart.c` USER CODE 区),2026-09-25 实现迁出至 `App/log/`(HAL 生成文件不含应用代码);同日写者任务体自 `Src/freertos.c` 迁至 `App/task/default_task.c`(与其余 5 任务文件同址归位)
> **关联文档**:`项目架构总览.md` · `运行与自检记录设计说明.md`(姊妹模块:持久化日志,同一写者任务) · `docs/测试说明/专项验证/链路延迟测试说明.md`(D6 日志税 / 步骤 8 非阻塞日志,需求出处) · AGENTS.md「平台约束」
> **关联文档**:`项目架构总览.md` · `运行与自检记录设计.md`(姊妹模块:持久化日志,同一写者任务) · `docs/测试说明/专项验证/链路延迟测试说明.md`(D6 日志税 / 步骤 8 非阻塞日志,需求出处) · AGENTS.md「平台约束」
## 1. 背景:实测故障与根因
@@ -64,7 +64,7 @@ HAL 并发语义(本仓库实现实测,`stm32f4xx_hal_uart.c:1135`):轮
| 子系统 | 通道 | 写者 | 持久化 |
|--------|------|------|--------|
| 控制台单写者(本文档) | UART5 串口(RS232#1) | defaultTask | 无(易失,复位丢失)——实时调试流 |
| sys_log 运行/自检记录 | FatFS `/log/*.log` | defaultTask(同一任务) | 是(定长 64B 记录,`运行与自检记录设计说明.md`) |
| sys_log 运行/自检记录 | FatFS `/log/*.log` | defaultTask(同一任务) | 是(定长 64B 记录,`运行与自检记录设计.md`) |
| 配置引擎 | FatFS `/config.ini` | defaultTask(同一任务) | 是 |
defaultTask 主循环按序处理:控制台排空 → 配置保存执行 → 2s 配置轮询 → LED 心跳 → syslog 落盘 → 栈水位观测——同一任务串行化三类 I/O,任务间零竞争。
@@ -408,7 +408,7 @@ sequenceDiagram
## 14. 相关文档
- `项目架构总览.md` —— 六层分层 / 启动时序 / 6 任务(写者任务与就绪屏障位置)
- `运行与自检记录设计说明.md` —— sys_log 持久化日志(姊妹模块:同写者任务、不同通道)
- `运行与自检记录设计.md` —— sys_log 持久化日志(姊妹模块:同写者任务、不同通道)
- `docs/测试说明/专项验证/链路延迟测试说明.md` —— D6 日志税 / 步骤 8 非阻塞日志(需求出处与 09-22 丢行实证)
- `docs/测试说明/流程与规范/自动化测试流程.md` —— `/serial-boot-test` 标记体系(Required / Forbidden 模式,`[ERR]` 语义)
- AGENTS.md —— 平台约束(内核前禁内核对象 / C 堆禁令 / 事件位语义 / 内存预算)
@@ -4,7 +4,7 @@
> **文档定位**:本波的**设计属主**——口径、通道集、数据形态、触发模型、文件格式、预算与所有权边界、显示契约、二期不返工的结构约束在此定稿。**板级判据与本波执行顺序的属主即本文 §9 与 §10**——原任务规划下的《故障录波》卡片登记簿已于 2026-10-30 随销项并入本文后删除(沿革见 §14);排期顺位在 `docs/任务规划/README.md` §2.0「独立」行。
> **状态**:设计已定稿,**数据面 + 触发与落盘接线 + UI 两个页面(手动录波、录波回放)的代码面已交付(2026-10-29)**——主机单测 113 / 170 / 110 全 rc=0、双端编译 0 错误 0 警告、板端编码器与 PC 侧解码器两份独立实现字节级互验通过。**没做完的只有板级判据那一轮**(注量对表、FTP 拉回、与 `meas:` 对表、CRC 不符必须拒收的实测、回放页放一段真波、两个待台架核数值),逐条列在本文 §10 板级各行。三条已裁定口径见 §1.2。**2026-10-31 远程半轮**已把其中不需要到场与注入的六样跑过(落盘道、触发时序、冷却恢复、轮转、PC 侧坏段拒收、104 并发)——结果与回放页新缺陷「段表枚举恒空」全在 §10「板级远程半轮记录」小节;**该缺陷已于 2026-11-02 远程修复并板级闭环**(根因 = 读侧 `wave_store_seg_path()` 返回值判错 + 键处理缺失效,证据与前后对拍见 §10「回放页枚举恒空缺陷修复记录」)。
> **相关文档**:
> `docs/子系统设计/ADC采样节拍设计说明.md`(节拍与时序账的属主;本文 §3 的数据源就是它)
> `docs/子系统设计/ADC采样节拍设计.md`(节拍与时序账的属主;本文 §3 的数据源就是它)
> `docs/硬件平台/ADC通道映射说明.md`(网名 `AINn` 不等于板级 `ADCn`,也不等于转换器通道号;链路分段与削顶归属)
> `docs/算法/采集与算法层设计说明.md §13.10`(动态波形验证判据;录波落地后它的读数通路升级,见 §11)
> `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1(FatFs 两条实证陷阱与 8.3 短名限制)
@@ -54,7 +54,7 @@
| 项 | 内容 |
|---|---|
| 为什么先做 1 kHz | 抬到 4 kHz 的第一块骨头不在录波,而在采集主线:一拍时序账要重算(现在 TPAFE 转换约 23 µs、EXTI 读 8 通道约 0.5 µs、ADC1 第 9 点约 67 µs、读窗与下次 CONVST 相隔约 977 µs,见 `docs/子系统设计/ADC采样节拍设计说明.md` 的时序账),半区语义与「恒定小偏移」都要重推;算法层还有两处按 1 kHz 写死的假设(`App/algo/phasor.h` 的 `PHASOR_N 20U`、`App/algo/freq.h` 的 `FREQ_FS_HZ 1000.0f`)会被一起牵动 |
| 为什么先做 1 kHz | 抬到 4 kHz 的第一块骨头不在录波,而在采集主线:一拍时序账要重算(现在 TPAFE 转换约 23 µs、EXTI 读 8 通道约 0.5 µs、ADC1 第 9 点约 67 µs、读窗与下次 CONVST 相隔约 977 µs,见 `docs/子系统设计/ADC采样节拍设计.md` 的时序账),半区语义与「恒定小偏移」都要重推;算法层还有两处按 1 kHz 写死的假设(`App/algo/phasor.h` 的 `PHASOR_N 20U`、`App/algo/freq.h` 的 `FREQ_FS_HZ 1000.0f`)会被一起牵动 |
| 芯片支持吗 | **支持**:TPAFE5160 是 16 bit / 350 kSPS 全通道同时采样(`docs/芯片手册/TPAFE5160.md` 首行)→ 4 kHz 是纯固件与算法的工程账,不是硬件上限 |
| 1 kHz 能看到什么 | 基波形状、削顶、直流偏置、明显畸变、频率(数过零)。**看不到**:2/5 次谐波的形状细节(每周期只剩 10 / 4 个点)、暂态前沿 |
| 偏离登记 | 落码时在产品需求那一行补一句指向本文;「看清波形」这一目标在 1 kHz 下**只算部分达成**,不许拿 1 kHz 的图当满足 80 点口径的证据 |
@@ -678,7 +678,7 @@ Before/After 表与串口原文。
|---|---|
| 原任务规划《故障录波》卡片(2026-10-30 销项删除) | 卡片判据与顺序图已并入本文 §9 与 §10;排期顺位归 `docs/任务规划/README.md` §2.0「独立」行——判据不再另立文件 |
| `docs/算法/采集与算法层设计说明.md §13` | 上游:静态与动态信度、削顶归属由它定义;本文 §11 记录因录波而变的三处口径 |
| `docs/子系统设计/ADC采样节拍设计说明.md` | 数据源与节拍属主;本波**不改**它,二期改节拍时必须回头改它 |
| `docs/子系统设计/ADC采样节拍设计.md` | 数据源与节拍属主;本波**不改**它,二期改节拍时必须回头改它 |
| `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 | FatFs 陷阱与 8.3 短名的属主;本文 §5.2 只是消费者 |
| `docs/UI设计/页面补齐规划.md` | 「手动录波」叶子旧状态是「不做(无录波模块)」;P2 与 P3 落地时该处的叶数记录要同步(**两叶已绑,现值 43 节点 / 34 叶 / 已绑 31 / 未绑 3**,数法见 `docs/UI设计/菜单页面设计.md` §3.9) |
| `docs/问题记录/采集与数据处理短板评测.md` | 记录链缺口(波形数据面 / COMTRADE / 104 文件服务)的登记处;P1 落地后该行改指本文 |
@@ -6,7 +6,7 @@
> **日期**:2026-09-29 创建;**2026-10-10 按当前代码重写正文**(旧正文含大量立项前的
> 描述,已清理;实施史与逐次决策见 §10 与文末变更记录,未删)。
> **相关文档**:
> [`运行与自检记录设计说明.md`](运行与自检记录设计说明.md)(复位记录与 `selftest.log`)、
> [`运行与自检记录设计.md`](运行与自检记录设计.md)(复位记录与 `selftest.log`)、
> [`../测试说明/单元测试/看门狗单元测试说明.md`](../测试说明/单元测试/看门狗单元测试说明.md)(主机侧纯逻辑单测,判定状态机)、
> [`../测试说明/板级测试/实机测试实施指南.md`](../测试说明/板级测试/实机测试实施指南.md)(故障注入台架流程)、
> [`../架构设计/项目启动顺序管理.md`](../架构设计/项目启动顺序管理.md)(启动时序与就绪屏障)、
@@ -560,7 +560,7 @@ python scripts/check_cubemx_survival.py --write # 确认改动无误后重建
> **来源**:`[ERR][WD] task stall: mask=0x00000067 req=0x00000077`(缺 bit4 = `TASK_WD_UI`)
> 的完整归因与处置。故障本身已结案——**累计 38 次上板启动 0 复现**(主机侧回归的早期基线 3/10),
> 原始分析文档按"完成即销项"规则删除。本节只留**可复用的判读口径、诊断方法、
> 实测基线与教训**;ADC 侧的处置细节在 [`ADC采样节拍设计说明.md`](ADC采样节拍设计说明.md) §6 与 §9,
> 实测基线与教训**;ADC 侧的处置细节在 [`ADC采样节拍设计.md`](ADC采样节拍设计.md) §6 与 §9,
> UI 推流门控在 `../UI设计/RemoDispBus协议与接收设计说明.md` §3,就绪门控 API 在
> `../网络/BSD_Socket_API_使用指南.md` §10.1。
@@ -592,7 +592,7 @@ python scripts/check_cubemx_survival.py --write # 确认改动无误后重建
|---|---|---|
| 1 | **uiTask 是 `required_mask` 里唯一的 `osPriorityBelowNormal`**(其余五个全 `Normal`)。FreeRTOS 数值越大优先级越高 → 任一 Normal 任务持续就绪,uiTask 就**完全拿不到 CPU**;`configUSE_TIME_SLICING=1` 只保护**同优先级**公平 | **至今未变**(刻意不提优先级,理由见 §11.6)。风险由"心跳自产 + 往返门控 + 采集零忙等"三面压住 |
| 2 | **UI 心跳曾依赖 netTask 往返**:`ui_net_sink_poll()` → `net_accept()` → `osMessageQueuePut(osWaitForever)` + `xTaskNotifyWait(portMAX_DELAY)`,**之后**才 `task_wd_beat(TASK_WD_UI)` → netTask 慢一拍、UI 心跳晚一拍。这正是"**只有 UI 位缺失**"的根因(别的任务没这条依赖) | ✅ 已断两处:心跳提到 poll **之前**(`App/task/ui_task.c`);无客户端时门控让往返 = **0 次**(`uis` 实测 1 µs) |
| 3 | **自定截止期一旦错过就不可恢复**:`next += 1; osDelayUntil(next)` 且**丢弃返回值** → 单拍 >1 ms 后 `next` 相对当前时刻永久落后 → 该任务从此不再阻塞、以 Normal 优先级空转(**连续**慢拍才锁死,单次慢拍会自己追平)。返回值本是现成的重同步钩子 | ✅ 已消:adcTask 改**事件驱动唤醒**(`osThreadFlagsWait`),"落后"这个概念不存在了(`ADC采样节拍设计说明.md` §6) |
| 3 | **自定截止期一旦错过就不可恢复**:`next += 1; osDelayUntil(next)` 且**丢弃返回值** → 单拍 >1 ms 后 `next` 相对当前时刻永久落后 → 该任务从此不再阻塞、以 Normal 优先级空转(**连续**慢拍才锁死,单次慢拍会自己追平)。返回值本是现成的重同步钩子 | ✅ 已消:adcTask 改**事件驱动唤醒**(`osThreadFlagsWait`),"落后"这个概念不存在了(`ADC采样节拍设计.md` §6) |
⚠ 顺带揪出的两个**独立正确性**缺陷(与停摆无关,但同源发现,别只当性能问题):
@@ -600,7 +600,7 @@ python scripts/check_cubemx_survival.py --write # 确认改动无误后重建
等效采样间隔被拉长。当时 `idle` 占 3.2% → 窗长从 20.0 ms 漂到 ≈20.6 ms ≈ **3% 频率失配**
→ 幅值/相位**系统性误差**(不是随机噪声)。现仅 `r == 1` 压帧,上板印证 `ring == frames`。
- **失败拍静默沿用上一拍值**:失败支路不清 `s_stm32_valid` → 照样压环、`skip` 抓不到。
现失败即清标志走整帧跳过(`ADC采样节拍设计说明.md` §4.4;单测 TC-ADCIN-070..078 钉死)。
现失败即清标志走整帧跳过(`ADC采样节拍设计.md` §4.4;单测 TC-ADCIN-070..078 钉死)。
### 11.4 `[PROF]` / `[PROF2]` 字段语义与当前基线
@@ -0,0 +1,287 @@
# 运行与自检记录设计
> **读者**:要改日志记录、加自检检查点、调记录页,或排查"设备上为什么没记下来"的人。
> **读这一篇能得到什么**:`/log` 下那七个文件与 SOE、控制台日志的**现状设计**——记录长什么样、谁在什么时候写、满了怎么办、屏上怎么呈现、出问题时退到哪儿。
> **相关文档**:`docs/架构设计/STM32F4-Base存储架构说明.md`(卷与 FTL)· `docs/子系统设计/系统墙钟时间维护说明.md`(时标来源)· `docs/子系统设计/控制台单写者日志设计说明.md`(串口侧)· `docs/UI设计/内容页架构.md`(记录页框架)
本文只描述**现在**的系统。
## 1. 概览
装置有两类日志、三条落盘通道,都遵守**单写者**约定(谁写盘只有一个任务,别的任务只投递):
| 通道 | 载体 | 写者 | 内容 |
|---|---|---|---|
| 运行日志 | `/log` 下七个文本文件 | defaultTask(经 `syslog_drain()`) | 上电/重启、自检故障、事故、告警、启动、操作、遥控 |
| SOE 事件顺序记录 | `/log/soe.log`(二进制 64B) | defaultTask(`syslog_drain()` 尾部) | 保护元件的动作/复归边沿 |
| 控制台日志 | 串口(RS232#1 / UART5) | defaultTask(`dbg_console_drain()`) | `DBG_*` 调试行,不落盘 |
三条通道各有自己的模块,互不借用:
| 模块 | 端 | 职责 |
|---|---|---|
| `App/log/sys_log.h` | 双端共享 | 运行日志 API、64B 记录布局常量、行拆装 |
| `App/log/sys_log_mcu.c` | MCU | 待发队列、defaultTask 落盘、容量轮转、上电/重启分类、取证文件 `wdev.log` |
| `simulator/sim/sim_syslog.c` | 模拟器 | 宿主文件后端(七份 `sim_*.log`),无队列无丢弃 |
| `App/log/soe_rec.c/h` | 双端共享 | SOE 记录编解码(纯逻辑,零 I/O) |
| `App/log/soe_store.c/h` | MCU | SOE 投递(protectTask)与排空落盘(defaultTask) |
| `App/log/console_log.c/h` | MCU | 控制台环形缓冲 + 唯一发送者 |
| `Drivers/BSP/UI/ui_page_*.c`、`ui_w_records.c` | 双端共享 | 七个记录页 + 记录列表共享组件 |
**一条记录的诞生**:任意任务 `syslog_post()` / `syslog_post_quiet()` → 条目进 queue(内核未启动时进静态数组)→ defaultTask 在 `syslog_drain()` 里取出、按文件合并成一次追加写 → 落 NAND。业务任务**从不碰文件系统**。
## 2. 一份记录长什么样
### 2.1 文本记录:定长 64 B,一行一条
七个运行日志文件用同一套布局,可以直接 `RETR` 下载后按固定偏移解析:
```
偏移 长度 内容
[0..18] 19 "YYYY-MM-DD HH:MM:SS"(墙钟无效时 = "1970-01-01 08:00:00")
[19] 1 ' '
[20..29] 10 epoch 秒,ASCII "%010u"(墙钟无效 = "0000000000")
[30] 1 ' '
[31..58] 28 描述 UTF-8(≤28 B = 9 个汉字),不足补 '\0'
[59..63] 5 '\0' 填充
```
一行实际内容只到约 59 字节,不足 64 补零,所以 ftp 下载看到的是一串带空白的定长记录。示例(描述之后是 NUL 填充):
`2026-09-14 16:48:34 1789465714 装置上电记录`
选定长的三个理由:条数 = `文件大小 / 64`(O(1),不用扫描);第 k 条偏移 = `k×64`(翻页零解析);掉电写了半条,`floor(size/64)` 会自然把它排除掉。
epoch 定宽 10 位是因为 2001-09-23 起 Unix 秒已是 10 位;uint32 到 2106 年才溢出。描述限制 28 B 是被内容页框架的行缓冲 `CP_ROW_BUF_LEN = 32`(含 NUL)卡住的,超长在投递侧就截断。
### 2.2 七类文本记录与容量
**记录类型 = 日志 ID = 哪个文件**。记录体内没有类型字段——见 [App/log/README.md](../../App/log/README.md)「记录类型 = 日志 ID」,那里有完整的判据与逐类写入点清单;本节只说有哪些文件、各多大。
| 文件 | 日志 ID | 记录什么 | 上限 | 满时 |
|---|---|---|---|---|
| `/log/run.log` | `SYSLOG_RUN` | 上电、重启 | 1024 条(64 KB) | 删除重建 |
| `/log/selftest.log` | `SYSLOG_SELFTEST` | 自检故障(有问题才记) | 512 条(32 KB) | 同上 |
| `/log/accid.log` | `SYSLOG_ACCIDENT` | 事故(跳闸型元件动作) | 512 条(32 KB) | 同上 |
| `/log/alarm.log` | `SYSLOG_ALARM` | 保护告警(告警型元件动作) | 1024 条(64 KB) | 同上 |
| `/log/start.log` | `SYSLOG_START` | 保护启动(越限电平上升沿) | 1024 条(64 KB) | 同上 |
| `/log/oper.log` | `SYSLOG_OPER` | 操作(改动了装置状态的运维动作) | 512 条(32 KB) | 同上 |
| `/log/yk.log` | `SYSLOG_YK` | 遥控(104 命令的选择/执行/拒绝) | 256 条(16 KB) | 同上 |
- **上限是逻辑上限,不是预分配**:文件按追加写自然增长,一条不写就不占簇;到顶才删除重建(串口打 `cap full, recreate`)。七类全满的名义峰值 = 4864 条 × 64 B = **304 KB**,相对 NAND 容量可忽略,且这些只在事件边沿写。
- 告警/启动给到 1024 条,是因为一次故障会连片触发多个元件,别让最能说明故障过程的两类最先被截掉。
- 名字全部是严格 8.3(`FF_USE_LFN=0`:主名 ≤8、扩展 ≤3);`/log` 目录在首次落盘前 `f_mkdir`(已存在则忽略)。
- **`syslog_id_t` 只准尾部追加**:枚举序就是两端 `s_paths[]` / `s_caps[]` 的下标,中间插值会让某类记录安静地写进别的文件——不报错,屏上照常翻页。
- 记录只增不改;删除只有两条路径——容量满时重建,以及 UI「清除记录」(§4.5)。
### 2.3 SOE 记录:二进制 64 B
`/log/soe.log` 与上面七个文件**不是同一套格式**,也不共用读者:它是结构化的二进制记录,给程序解析用。
```
[0] 版本号
[1] 类型(soe_type_t)
[2] 元件号(prot_elm_id_t;0xFF = 非元件事件)
[3] 标志位(bit7 = 记录时时钟无效)
[4..7] 单调序号 seq(uint32 小端,同一 boot 内递增)
[8..14] CP56Time2a 时标(7 B,IEC 60870-5-4 §5.1.6)
[15..18] epoch 秒(uint32 小端;时钟无效时置 0,供主机交叉校验)
[19..22] 值 value(uint32 小端,语义随类型)
[23..62] 保留,恒 0
[63] 校验和 = sum8([0..62]),解析侧拒绝不匹配(撕裂/半写检测)
```
所有多字节字段小端。容量 1024 条,满则删除重建,**重建不重置 seq**(只有设备复位才重置,所以主机校验单调性要按"同一 boot 段"分段看)。
## 3. 写入路径
### 3.1 投递:任意任务只把条目丢给队列
```
任意上下文 syslog_post(id, desc)
├─ 内核未运行(app_main_init 期间)→ 静态待发数组(8 槽,满则丢弃并计数)
└─ 调度器已运行 → 消息队列(8 槽,满则丢弃并计数;日志永远不阻塞业务)
```
`syslog_entry_t` 内嵌定长缓冲 `char desc[SYSLOG_DESC_MAX + 1]` 并**拷贝**描述串,调用方返回后即可复用或销毁自己的缓冲。条目里不存指针。
两个投递口,按调用者的栈预算选:
- `syslog_post()`:通用。队列满/启动前数组满时,就地打一条留痕日志——那两条分支各内联约 216 B 的行缓冲。
- `syslog_post_quiet()`:静态最坏链吃紧的任务用(**protectTask、iec104Task 必须用它**)。同样丢弃,但只累加 `syslog_post_dropped()` 计数、不打任何日志,由 defaultTask 在 `syslog_drain()` 里按差值补一条留痕。收/丢判据两个口完全一致。
### 3.2 落盘:defaultTask 是唯一写者
```
defaultTask(挂载成功之后):
1) f_mkdir("/log")(幂等)
2) 排空启动前数组 + 队列 → 按文件合并落盘(一批 ≤8 条,一次
f_open(FA_OPEN_APPEND) → f_write(n×64) → f_close)
3) 排空 SOE 队列 → /log/soe.log
4) 执行挂起的"清除记录"请求
5) 主循环:osMessageQueueGet(阻塞, 10 s 超时) → 落盘;超时空转继续喂狗与 LED
```
**一次排空 = 每文件一次 open/write/close**。逐条写的话每条都要回写目录项、对目标扇区读-改-写,即每条日志 2~4 次 NAND 页读写;同一文件的一批(≤8 条 = 512 B 静态缓冲)合并成一次追加,掉电语义不变——批次仍在同一次 `syslog_drain()` 调用内写完并 `f_close`,而 `f_close` 内部 `sync_fs` → `disk_ioctl(CTRL_SYNC)` 会把 FTL 的写回缓存刷进 NAND。**批次不跨调用驻留 RAM**。
失败与回投的判据:
- 卷忙(卷锁 try 取不到)→ 返回 -2,整批**原样回投队列**,静默顺延到下一轮。这是硬要求:defaultTask 是喂狗点,阻塞等锁等于停喂看门狗。
- 写失败 / 容量不足 → 按需退回逐条慢路径("满了删除重建"只在逐条路径上实现,避免一批跨多轮重建)。
- 启动前数组整批成功才删;队列条目落盘失败时回投,**丢一整个批次是设计上禁止的**。
- **自愈重建只认介质/结构类错误码**(`FR_DISK_ERR` / `FR_INT_ERR` / `FR_NO_FILESYSTEM` / `FR_DENIED`):只有"删了这个文件真能修好"的才删。名字/参数类错误码(`FR_INVALID_NAME` / `FR_NO_PATH` / `FR_NO_FILE` / `FR_INVALID_OBJECT`)与 `FR_NOT_READY` / `FR_NOT_ENABLED` / `FR_TIMEOUT` 一律不删——同因下删了也照样失败,只会把历史日志一轮轮删光。判定入口是 `s_is_selfheal_code()`。
- 追加写必须 `FA_WRITE | FA_OPEN_APPEND`(本 FatFs 移植的 `FA_OPEN_APPEND` 不含写位);`f_write` 必须校验写回字节数(簇分配失败时返回 `FR_OK` + 短写,没有错误码)。
- 存储没就绪或挂载失败时,条目滞留 RAM,下次上电丢失,串口打告警——此时本就无法持久化。
### 3.3 留痕节流
避免"日志系统自己把日志刷爆":
- 排空的整批回投只在**正常→故障**那一次报(落盘成功一轮复位),文案 `write fail, requeue N entries`。
- 投递侧队列满、启动前数组满各有一个独立抑制位,只压留痕次数,不改变丢弃行为。
- 节流之后不统计丢弃条数(那属于新增对外可见信息)。
### 3.4 卷锁 fail-open 边沿留痕
卷锁层只自增计数、绝不产日志——它唯一能落盘的通道自己也要取那把非递归锁,被卷忙挡住时既写不出也不许在失败分支里再取同一把锁(机理见 `App/fs/vol_lock.h`「可观测性」段)。
所以留痕由唯一写者在 `syslog_drain()` 尾部按**边沿**补投:判据是"计数比上一轮读到的变大了",不是"计数大于零"(后者是电平,每轮一条 = 每轮一次 NAND 写,诊断自己变成负载)。**一个 episode(= 一次上电)只投一条**,重新上电复位 latch 后故障复发才会再投。文本形如 `vollock failopen=3`,落 `selftest.log`。
三条约束:① 不在失败调用点就地打日志;② 不打 DBG 错级、不打 `[ERR]`——`[ERR]` 是"设备出故障"的唯一信号通道,把可观测性事件塞进去就同时不再是故障、也不再是禁令;③ 记录自带的时间戳是**被观测到的时刻**(粒度 = 一轮 `syslog_drain()`),不是发生时刻,因此不另造更"精确"的字段。
### 3.5 取证文件 `/log/wdev.log`
看门狗停喂现场证据走独立通道 `sys_log_evidence()`:**变长行**、不走 64B 定长格式,写完立刻诱导一次同步把脏页刷进 NAND,防止复位丢现场。超过 8 KB 先删除重建,防长跑无界增长。它不在 `syslog_request_clear()` 的作用域内。
## 4. 读取与呈现
### 4.1 读接口
```c
uint32_t syslog_count(syslog_id_t id); /* = 文件大小 / 64 */
int syslog_get_record(syslog_id_t id, uint32_t idx,
char *ts, size_t ts_len,
char *desc, size_t desc_len); /* 成功 0,越界/读失败/缓冲不足 -1 */
int syslog_last_epoch(syslog_id_t id, uint32_t *p_epoch);
```
**容量契约**:`ts_len > SYSLOG_TS_LEN`、`desc_len > SYSLOG_DESC_MAX` 必须成立;不足一律返回 -1,既不静默截断、也不写调用方一个字节。描述在写入侧已截到 28 B,按最小容量给就一定装得下,读侧不需要第二道截断。UI 用 32 B 列缓冲,走允许路径。
### 4.2 读放大收口:8 条滑动窗口
MCU 端有 512 B 静态读窗口:`s_read_raw()` 先查窗口(同 id + 代数未变 + 落在区间内 → 零 I/O 直接返回),未命中才 `f_open → f_lseek(窗口首条×64) → f_read(≤8×64B) → f_close` 一次取回连续 8 条再切。写者每次落盘后把 `s_rd_gen[id]` 自增,读者比对填充时记录的代数,不匹配就重填。对齐 32 位在 ARM 上原子,跨任务(uiTask 填、defaultTask 增)无需临界区,最坏多读一轮、不会读错。
### 4.3 屏上怎么看到
菜单「记录查询」下挂九个叶子,其中七个是运行日志页、一个是 SOE 页:
| 菜单叶子 | 数据源 | 每记录几行 |
|---|---|---|
| 运行记录 / 自检记录 / 事故记录 / 操作记录 / 保护告警 / 保护启动 / 遥控记录 | 对应 `/log/*.log` | 2 行(时间戳 + 描述) |
| SOE 记录 | `/log/soe.log` | 另一套(二进制解码) |
七个文本页共用 `ui_w_records` 组件:页面模块只提供描述符与一个日志 ID,缓存与读盘都在组件内。每页 `rows_per_page = 8`(= 4 条记录/页),↑↓按行移动,页脚自动算"第 X 页,共 Y 页"。空日志时 `count_cb` 返回 0,页面以单条伪行 `无记录` 呈现(避免"共 0 页")。
UI 页面**不再 include `sys_log.h`**:日志 ID 在两端绑定处(`App/task/ui_task.c` 与模拟器)映射成不透明的 `UI_W_REC_LOG_*`,并有编译期断言把 `UI_W_REC_LOG_* == syslog_id_t` 逐项钉死。
这些记录页都是**内容页**——框架怎么建屏、怎么分页、怎么写一个新页,属 `docs/UI设计/内容页架构.md`;本节只说记录这一侧的口径。
### 4.4 清除记录
非写者上下文(uiTask 等)只能 `syslog_request_clear(id)` 置一个标志(零盘触点、零卷锁),真正的 `f_unlink` 在 defaultTask 的 `syslog_drain()` **尾部**执行。顺序是判据的一部分:排在条目排空之后,否则本轮刚排空的旧记录会被写回去,`syslog_count()` 不归零。
- 结果读 `syslog_clear_state(id)`:`NONE` / `OK` / `WAIT` / `FAIL`;事实复核读 `syslog_count()`。
- 落地函数 `syslog_clear_apply(id)` 是 MCU 后端专属,模拟器不实现——共享代码误调会在链接期报错,这是故意的。
- 卷锁一律 try,卷占用返回 -1 且不动原文件;幂等只认 `FR_NO_FILE`,`FR_NO_PATH` 如实返回 -1 且不做任何卷级动作(不补目录、不重格式化)。
- 清除成功会往操作记录里落一条 `clear <文件>`;**但清 `oper.log` 自己时刻意不投**,否则"我清除了记录"这条会把刚删空的文件又写成 1 条。
## 5. 事件分类
### 5.1 上电还是重启
defaultTask 挂载成功、排空待发队列之后写一条运行记录,靠 `run.log` 末条的 epoch 判断:
```
读 run.log 末条记录的 epoch 字段 E:
墙钟有效 且 (now - E) < 600 s → "装置重启记录"(近期刚运行过 = 软复位/看门狗)
其余(无文件 / 首条 / 间隔 ≥10 min / 墙钟无效)→ "装置上电记录"
```
阈值 `SYSLOG_REBOOT_WINDOW_S = 600`。断电不到 10 分钟会被误判成重启——没有掉电检测硬件时这个歧义消不掉,取工程折中。串口同步打一条 `runlog boot entry: 装置上电记录 / 装置重启记录`,供 `/serial-boot-test` 判读。
### 5.2 自检故障:只记故障
全部通过不写记录;下面每条都是**一个检查点对应一条固定描述串**,串即落盘内容,也是屏上显示的那行:
| 检查点 | 位置 | 描述串 |
|---|---|---|
| 看门狗复位 | app_main 启动早期读 `RCC_CSR` | `watchdog reset` |
| 墙钟/RTC 建立失败 | app_main:`sys_clock_init()` | `clock chip fault` |
| NAND 初始化失败 | app_main:`gd5f2gq5ue_init()` | `NAND fault` |
| ADC 转换器初始化失败 | app_main:`tpafe5160_init()` | `ADC fault` |
| ADC 通道映射失败 | app_main:`adc_init()` 返回 -1 | `ADC map fault` |
| ADC DMA 挂载失败 | app_main:`adc_init()` 其他负值 | `ADC dma fault` |
| 采样心跳看护 | adc_task | `adc sampling fault` / `adc code rail fault` |
| 文件系统故障 | default_task:挂载/重试耗尽、卷探测失败 | `filesystem fault` / `storage fault` |
| 存储容量耗尽 | default_task:卷探测报 `BSP_ERR_NO_SPACE` | `storage full` |
| 堆低水位 | default_task | `heap low water` |
| 定值区损坏 | default_task | `prot set corrupt` |
| 通讯链路异常 | net_task:bring_up 超时 | `comms fault` |
| 网络初始化/重建失败 | net_task | `net init fail` / `net reinit fail` |
| 配置文件引擎故障 | cfg_file(分配/模板/加载/保存/视图守卫/poll) | `cfg heap alloc fail` / `cfg template write fail` / `cfg load read fail` / `cfg load oversized` / `cfg save read fail` / `cfg save write fail` / `cfg save overflow` / `cfg save line too long` / `cfg save view stale` / `cfg poll view stale` / `cfg poll oversized` |
| 卷锁 fail-open | sys_log(边沿补投,§3.4) | `vollock failopen=N` |
FatFS 挂载/建卷失败时存储本身不可写,这条只能走串口:`[SELFTEST] fs: FAIL`。
不做"已复归 / 持续监测 / 恢复记录"——那是保护元件语义,不属于自检。
## 6. 控制台日志(串口)
与落盘日志是两条独立通道,共用"单写者"思想:
- 生产者(任何任务,含 `printf`)只把**已格式化好的整行**写进环形缓冲 `dbg_console_write_line`;格式化在 `DBG_*` 宏内用库 `snprintf` 完成,不自研可变参数函数。
- 写者 = defaultTask,`dbg_console_writer_attach()` 注册、主循环里 `dbg_console_drain()` 排空,是唯一的 `HAL_UART_Transmit` 调用点(行级原子)。
- 异常/早期路径 `dbg_console_write_raw()`:拿不到整行独占就把原文转入环形缓冲延后发,与当场直发逐字节相同。
- 成本:每条 `DBG_*` 约 90 B Flash + 216 B 栈;深调用链模块须预定义 `DBG_LINE_BUF_MAX`。
- 模拟器**不编译** `console_log.c`,`DBG_*` 直接映射 `printf`——所以环形缓冲溢出/丢弃逻辑在模拟器上测不到。
## 7. 双端差异
| 项 | MCU | 模拟器 |
|---|---|---|
| 后端 | `sys_log_mcu.c`(NAND + FatFS) | `sim_syslog.c`(工作目录下七份 `sim_*.log`) |
| 投递 | 队列 + 启动前静态数组,会丢、会回投 | 直接写宿主文件,**无队列、无丢弃**,`syslog_post_dropped()` 恒 0 |
| `syslog_post_quiet()` | 与 `syslog_post()` 判据同、栈成本不同 | 等价于 `syslog_post()` |
| 清除记录 | 请求与落地分离,`WAIT` 态真实存在 | 调用内同步删完,`WAIT` 永不出现 |
| SOE / 控制台 | 有 | 不编译 |
完整清单与"模拟器测不到什么"见 `docs/测试说明/专项验证/模拟器差距清单.md` §2.2。
## 8. 内存占用
| 项 | 大小 | 位置 |
|---|---|---|
| sys_log MCU:启动前待发数组 8 条 + 队列 8 条 + 状态 | 约 300 B 静态 + 约 150 B FreeRTOS 堆 | .bss / heap_4 |
| 读写窗口:写批 512 B + 读窗 512 B | 1 KB | .bss(常驻) |
| 记录页模块 | 0(描述符驱动,零 LVGL 对象,只有栈缓冲) | — |
不新增任务、不动任务栈。平台总量台账 → `docs/内存管理/RAM预算台账.md`。
## 9. 异常与恢复
| 场景 | 行为 |
|---|---|
| 卷忙(FTP 传输占锁) | 本轮整批不落盘、条目回投队列,下一轮再试;业务任务完全不阻塞 |
| 写失败 / 容量不足 | 退回逐条路径;满则删除重建该文件并串口告警 |
| 宿主/介质给回损坏文件 | 仅对介质类错误码删除重建并重试一次;错误只报第一次 |
| 掉电在写批中途 | `floor(size/64)` 排除半条;已 `f_close` 的批次均已同步进 NAND |
| 存储未就绪 / 挂载失败 | 条目滞留 RAM(下次上电丢失),串口告警;存储自愈重试由 default_task 负责 |
| 墙钟无效 | 时间戳落 `1970-01-01 08:00:00` + epoch 0,上电/重启分类因此退化为"上电" |
| 记录的条目被静默丢弃 | 投递侧计数,defaultTask 按差值补一条留痕 |
## 10. 自测要点
- 模拟器无头:`--logtest`(投递 N 条 → 读回断言 + 宿主文件字节级校验:64 B 定长、ISO 时间戳、NUL 填充)、`--keys` 导航到记录页做像素回归。
- 真机串口标记:Required `App init OK` / `Network Init` / `net init: OK` / `runlog boot entry:`;Forbidden `FAIL` / `ERROR` / `HardFault`。
- 主机侧单测覆盖队列回投、容量轮转、读窗口代数失效、清除请求与落地分离等只能在真机或纯逻辑层验证的判据(见 `test/host/sys_log_mcu/`)。
- 人工目检:FTP 下载 `/log/*.log` 看文本可读性;记录页翻页。
@@ -1,351 +0,0 @@
# 运行/自检记录与待机显示设计说明
> **版本**:v1.4(2026-11-13:§5.2 待机屏从 6 路占位电流改为 UA/UB/UC/IA/IB/IC 六路实时值,§1 R2 按新口径改写、不再引 mockup,§9 对应开放问题销项删行;v1.3 为 §4.4 新增"卷锁 fail-open 边沿补投自检记录"一条,道G task-40;v1.2 为 §4.3 随七类记录更新,v1.1 的原始设计口径不变)
> **日期**:2026-09-14(v1.2 更新 2026-10-26,v1.4 更新 2026-11-13)
> **状态**:步骤 1–9 全部完成——双端构建 0/0、`--logtest` PASS、ui-verify 五门禁全绿;真机 /serial-boot-test 待上电回归(§8.2)
> **关联文档**:`项目架构总览.md` · `STM32F4-Base存储架构说明.md` · `系统墙钟时间维护说明.md` · `UI设计/内容页架构.md`(如存在)· `docs/UI设计/mockups/`(待机显示页面.jpg / 运行记录页.jpg / 自检记录页.jpg / 密码页面实际机器的示例图片.jpg)
## 1. 背景与需求
| # | 需求 | 说明 |
|---|------|------|
| R1 | 密码错误提示 | 用户密钥页输错密码时提示"密码输入错误",并允许重新输入(现状:仅清空重输、无提示) |
| R2 | 待机显示页 | 长时间无操作时默认显示的界面:UA/UB/UC/IA/IB/IC 六路电量的实时值 + 底部时钟;**只有按确认键才进入菜单** |
| R3 | 运行记录页 | 记录装置上电时间、重启时间(参考 `mockups/运行记录页.jpg`:标题 + 逐条"时间戳 + 事件" + 页脚分页) |
| R4 | 自检记录页 | 记录开机自检结果,有问题才记录(参考 `mockups/自检记录页.jpg`) |
| R5 | FTP 可读存储 | 记录以**纯文本文件**落盘,FTP(根目录 = FatFS 卷根)可直接下载读取 |
| R6 | 先调研后设计 | 网络调研存储方式选型 → 写入本设计文档 → 按文档实施 |
## 2. 现状分析
### 2.1 UI 框架(`Drivers/BSP/UI/`,MCU/模拟器共用)
- **内容页 = 描述符驱动**:`content_page_def_t`(标题/几何/行数据源/4 钩子),框架 `ui_content_page.c` 统一建屏、切屏、焦点、按键分发与绘制;页面模块零 LVGL 对象。
- **分页已内建**:`selectable=1` + `rows_per_page`(↑↓移选、窗口跟随)+ 页脚"第 X 页,共 Y 页"自动计算;动态行经 `refresh(row, name, value)` 逐行取数(栈缓冲 **32B/列**),行数经 `count_cb()`。
- **菜单树已含目标叶子**:`ui_menu_tree.txt` "记录查询"下已有 **自检记录 = node 39、运行记录 = node 40**(当前为空白终点框,无内容页映射);无需改树。
- **密码页** `ui_page_password.c`:3 位数字框 + ↑↓←→/ENTER;输错仅清空重输(R1 缺口)。
- **按键流**:物理/远端键 → LVGL keypad → 默认输入组 → 焦点对象事件回调(菜单容器 `menu_key_cb` / 内容页容器 `cp_key_cb`)——空闲检测可在此统一挂钩。
- **字体**:子集位图字体 `ui_font_cn_12`,字符集 `ui_cn_chars.txt`(当前 224 汉字/符号 + ASCII 0x20~0x7E,共 319 字形;2026-10-29 实测。122/217 是 2026-09-14 的历史快照,见 §6.1)。
### 2.2 存储与时间
| 项 | 现状 |
|---|------|
| 卷 | GD5F2GQ5UE SPI NAND **256MB**(2048 块 × 64 页 × 2KB),dhara FTL(磨损均衡/坏块管理),FatFS FAT32,`FF_USE_LFN=0` → **文件名必须 8.3** |
| FTP | lftpd 根目录 = 卷根 `/`;现有文件 `/config.ini`(8.3) |
| 并发 | `FF_FS_REENTRANT=1` + 系统锁(`ffsystem.c` osMutex):单次 `f_*` 调用受保护,**多调用序列(open→seek→write→close)需单写者纪律** |
| 墙钟 | `sys_clock_get_str(buf,20)` → `"YYYY-MM-DD HH:MM:SS"`;`sys_clock_get()` → Unix 秒(失败 = `UINT32_MAX`);SD2506 RTC + timeSyncTask TCP 校时 |
| 挂载 | defaultTask:`f_mount(forced=1)`,空卷自动 `f_mkfs` FAT32,成功后置 `SYS_EVT_STORAGE_READY` |
### 2.3 现有自检检查点(v1 埋点位置)
| 检查点 | 位置 | 失败表现 |
|---|------|---------|
| RTC 读取/墙钟建立 | `app_main_init()`:`sd2506_init()` + `sys_clock_init()`(FreeRTOS 启动前) | 时间戳无效,后续日志无意义时间 |
| NAND 初始化 | `app_main_init()`:`gd5f2gq5ue_init()` 返回非 `GD5F_OK` | 存储不可用 |
| ADC 初始化 | `app_main_init()`:`tpafe5160_init()` 超时 | 采样不可用 |
| FatFS 挂载/建卷 | defaultTask:`f_mount`/`f_mkfs` 失败(置位 STORAGE_READY 前) | FTP/配置/记录全部不可写 |
| 网络链路 | net_task `net_stack_bring_up()`:PHY 等待 ≤3s 后仍 DISCONNECT | 组网可用但无链路 |
### 2.4 RAM/LVGL 池预算(依据 `docs/内存管理/RAM使用分析与优化方案.md`)
- FreeRTOS 堆真实余量 ≈ **11~12KB**;LVGL 池 16KB 静态,菜单常驻占用 ≈5KB。
- 本设计新增 RAM:**全部 < 1KB**(§7),不新增任务、不动任务栈。
## 3. 网络调研摘要(R6)
| 主题 | 结论 | 来源 |
|---|------|------|
| 日志行格式与时间戳 | syslog(RFC 5424):一行一条记录;TIMESTAMP 采用 **ISO 8601/RFC 3339**(`YYYY-MM-DDTHH:MM:SS[.frac][Z]`,机器可读、字典序可排序);MSG 用 UTF-8。本设计取空格变体 `YYYY-MM-DD HH:MM:SS`(与既有 `sys_clock_get_str` 输出一致),另附 epoch 秒字段供程序化解析 | RFC 5424 §6.2.3/§6.4(datatracker.ietf.org/doc/html/rfc5424) |
| 写入模型 | Linux printk:消息先进 **RAM 环形缓冲**,由**专用线程**异步刷持久存储;热路径禁止同步 I/O。本设计对应"RAM 待发队列 + defaultTask 单写者落盘" | kernel.org/doc/html/latest/core-api/printk-basics.html |
| FatFs 追加语义 | `FA_OPEN_APPEND`(= FA_WRITE\|FA_SEEKEND):打开即定位 EOF,文件不存在则创建;配合 `f_lseek(fp, FILE_SIZE)` 可取文件大小。本设计每条记录一次 `f_open→f_write(64B)→f_close` | 工程内 `Lib/FatFs/ff.h:399`、`ff.c:3871`(与官方文档一致) |
| NAND 写入代价 | dhara FTL 已做磨损均衡;FAT32 下每次小追加约消耗 1 个簇(4KB)。本场景**写频率极低**(每上电 1~N 条、故障才写),簇开销与磨损可接受;以文件容量上限约束增长 | `docs/架构设计/STM32F4-Base存储架构说明.md` §6.3 + GD5F 驱动参数 |
**选型结论**:纯文本、**定长 64B 记录、一行一条**(ISO 时间戳 + epoch + UTF-8 描述);追加写 + 容量上限截断;RAM 队列 + 单写者任务落盘。
不选变长行(需扫描计数/解析,掉电半行难界定);不选二进制格式(违背 R5 FTP 直读)。
## 4. 总体设计
### 4.1 模块与文件清单
| 文件 | 端 | 说明 |
|---|---|------|
| `App/log/sys_log.h` | 共享 | 公共 API + 记录格式常量 + 行拆分内联函数(无 OS 依赖) |
| `App/log/sys_log_mcu.c` | MCU | FatFS 后端:待发队列(启动前静态数组 + FreeRTOS 消息队列)、defaultTask 落盘、容量截断、上电/重启分类 |
| `sim/sim_syslog.c` | 模拟器 | 宿主文件后端(CWD 下与七类一一对应的 `sim_*.log` 七份),直接追加,`time()` 时间戳;**没有队列与丢弃**,`syslog_post_quiet()` 在本端等价于 `syslog_post()`(两端差异登记在 `docs/测试说明/专项验证/模拟器差距清单.md` §2.2) |
| `Drivers/BSP/UI/ui_standby.h/.c` | 共享 | 空闲检测 + 待机屏(屏幕覆盖层:保存/还原前屏与焦点) |
| `Drivers/BSP/UI/ui_page_runlog.h/.c` | 共享 | 运行记录内容页描述符(动态行,数据源 = sys_log API) |
| `Drivers/BSP/UI/ui_page_selftest.h/.c` | 共享 | 自检记录内容页描述符(同上) |
| `App/app/app_main.c` | MCU | 埋点:RTC/NAND/ADC 失败 → `syslog_post(SYSLOG_SELFTEST, …)` |
| `Src/freertos.c`(defaultTask USER CODE) | MCU | 挂载失败埋点 + **上电/重启分类记录** + 队列排空落盘循环 |
| `App/task/net_task.c` | MCU | 埋点:链路超时 → `syslog_post(SYSLOG_SELFTEST, "通讯异常")` |
| `Drivers/BSP/UI/ui_page_password.c` | 共享 | R1:错误提示 + 重输 |
| `Drivers/BSP/UI/ui_content_page.h/.c`、`ui_menu.c` | 共享 | 新增 2 个内容页 ID/描述符表项/node 39·40 映射;键事件挂钩空闲通知 |
| `ui_cn_chars.txt` + `tools/font_gen/` | — | 补 10 字重生成字体(§6) |
| `MDK-ARM/*.uvprojx`、`simulator/CMakeLists.txt` | — | 新文件双端注册(new-source-file skill) |
| `simulator/main.c` | 模拟器 | `--logtest` / `--standby` / `--idle <s>` 无头自检与测试钩子 |
### 4.2 日志记录格式(定长 64B,一行一条)
```
偏移 长度 内容
[0..18] 19 "YYYY-MM-DD HH:MM:SS"(ISO 8601 空格变体;墙钟无效时 = "1970-01-01 08:00:00")
[19] 1 ' '
[20..29] 10 epoch 秒 ASCII("%010u",机器解析/上电重启分类用;墙钟无效 = "0000000000")
[30] 1 ' '
[31..58] 28 描述 UTF-8(≤28B = 9 个汉字),不足补 '\0'
[59..63] 5 '\0' 填充
```
示例行:`2026-09-14 16:48:34 1789465714 装置上电记录`(后随 NUL 填充至 64B)。
**定长收益**:条数 = `filesize / 64`(O(1),免扫描);第 k 条偏移 = `k*64`(随机读,UI 翻页零解析成本);掉电半行被 `floor(size/64)` 自然排除。
**epoch 10 位约束**:2026 年 Unix 秒已达 10 位(2001-09-23 起),字段按 `%010u` 定宽;uint32 上限 4294967295(2106 年)内不溢出。
**描述 ≤28B 约束**:受框架动态行栈缓冲 `CP_ROW_BUF_LEN=32`(含 NUL)限制,不改框架。
### 4.3 文件与容量
**记录类型 = 日志 ID 本身,一类记录 = `/log` 下一个文件**(记录体内没有类型字段位,
理由与逐类判据、写入点清单见 `App/log/README.md`「五类记录与『记录类型 = 日志 ID』」——
本节只登记"有哪些文件、各多大"这一层,判据表**不在这里抄第二份**)。
| 文件 | 记录类型(= `syslog_id_t`) | 上限 | 满时策略 |
|---|------|------|---------|
| `/log/run.log` | `SYSLOG_RUN` 上电/重启与运行事件 | **1024 条(64KB)** | 删除重建(整卷清空,串口告警);DTU 上电频率低,容量充裕 |
| `/log/selftest.log` | `SYSLOG_SELFTEST` 自检故障(有问题才记;来源含**卷锁 fail-open 边沿留痕**,见 §4.4 最后一条) | **512 条(32KB)** | 同上 |
| `/log/accid.log` | `SYSLOG_ACCIDENT` 事故(跳闸型动作) | **512 条(32KB)** | 同上 |
| `/log/alarm.log` | `SYSLOG_ALARM` 保护告警 | **1024 条(64KB)** | 同上 |
| `/log/start.log` | `SYSLOG_START` 保护启动(越限电平) | **1024 条(64KB)** | 同上 |
| `/log/oper.log` | `SYSLOG_OPER` 操作(改动了装置状态的运维动作) | **512 条(32KB)** | 同上 |
| `/log/yk.log` | `SYSLOG_YK` 遥控(104 命令的选择/执行/拒绝) | **256 条(16KB)** | 同上 |
- 表内上限是**逻辑上限,不是预分配**:文件按追加写自然增长,一条不写就不占簇,到顶才
删除重建;七类全满的名义峰值 = 4864 条 × 64 B = **304 KB**(NAND 2 GB,且这些都只在
事件边沿写,不构成容量或磨损瓶颈)。
- 告警/启动给到 1024 条:一次系统故障会连片触发多个元件,别让最能说明故障过程的两类
记录最先被截掉。
- `id` 只准**尾部追加**:枚举序 = 两端 `s_paths[]`/`s_caps[]` 的下标,中间插值会让
某类记录安静地写进别的文件(不报错、屏上照看照翻),机械防线是
`TC-SYSLOG-130`/`131`/`131b`(逐 id 验"哪类落哪个文件、各用各的上限")。
- 另有两类**不属于本表**的文件各有各的属主与通道:`/log/wdev.log`(停喂取证,
`sys_log_evidence()`,§6)、`/log/soe.log`(二进制 SOE,64 B 定长但**另一套
布局与读者**,见本节末尾与 `App/log/README.md` soe 小节)。
- 目录 `/log/` 首次落盘前 `f_mkdir`(已存在则忽略);文件名均**严格 8.3**
(`FF_USE_LFN=0`:主名 ≤8、扩展 ≤3、不以点开头),逐条机器核
`TC-SYSLOG-081`/`130c`。
- FTP 视角:卷根下 `LOG` 目录内上述记录文件均为**同一格式**的 64 B 定长文本记录,
可直接下载后按固定偏移解析(R5)。
- 记录**只增不改不删**;删除只有两条既有途径——满时重建,以及 UI「清除记录」
(`syslog_request_clear(id)` 请求 / defaultTask `syslog_drain()` 尾部落地,
语义与判据见 `App/log/README.md`,O3 已由此关闭)。
### 4.4 写路径(单写者,调研结论落地)
**待发条目存副本而非指针(B 方案整改,2026-10-24,台架实证驱动)**:`syslog_entry_t` 原为
`{ uint8_t id; const char *desc; }`——只存调用方指针,隐含「desc 必须活到落盘」的契约,
但该契约既没写进 `sys_log.h`、也没有任何机械检查。一次可控台架实验把它照了出来:临时在
testTask 挂投递钩子,复用**同一块缓冲**连投 12 条带序号记录,并与一次 8 MB FTP STOR(占住
卷锁)重叠——落盘的 12 条变成「1~5 + 第 12 条七份」,即所有滞留条目别名到该缓冲的最后一次
写入。危险不止于内容错:卷忙整批回投让条目最长滞留到卷空闲,生产者若投栈缓冲,函数返回后
就是悬垂读。现改为条目内嵌定长缓冲 `char desc[SYSLOG_DESC_MAX + 1u]`,`syslog_post` 投递时按
`SYSLOG_DESC_MAX` 截断拷贝并补终止符,调用方缓冲区可立即复用;`sys_log.h` 的参数说明同步
改写。RAM 代价(`scripts/ram_report.py` 同口径前后各跑一次):`ZI 131,500 → 131,852 B`
(+352 B,`RW_IRAM1` 57.4% → 57.7%)、`Code +60 B`、`RW` 不变;`RW_CCM` 静态不变,队列
存储多要的 192 B 取自 FreeRTOS 堆池,烧录后串口水位行 `[STACK] heap free=15624
min_ever=8464` 说明余量充足。回归由 **TC-SYSLOG-108**(投出去之后改写调用方缓冲,落盘必须
仍是投递时的原文)与 **TC-SYSLOG-108b**(超长在条目内钳位)钉住。**为什么主机侧此前照不出
来**:用例传的都是字符串字面量、静态存储期无限,正好绕开生命周期维度——要照出这类问题必须
专门构造可变缓冲。
```
任意上下文 syslog_post(id, desc)
├─ FreeRTOS 未运行(app_main_init 期)→ 静态待发数组(8 槽,满则丢弃+计数)
└─ 调度器已运行 → osMessageQueueSend(8 槽,满则丢弃+计数;日志不得阻塞业务)
defaultTask(唯一写者,挂载成功后):
1) f_mkdir("/log")(幂等)
2) 排空启动前数组 + 队列 → 按文件合并落盘(一批最多 8 条:一次
f_open FA_OPEN_APPEND → f_write(n×64B) → f_close,见下方「日志读写放大收口」说明)
3) 写"上电/重启"分类记录(§5.3,基于 run.log 末条 epoch)
4) 主循环:osMessageQueueGet(阻塞, 10s 超时) → 落盘;超时空转继续 LED 闪烁
```
- **日志读写放大收口(2026-10-15,写侧):一次排空合并成"每文件一次 open/write/close"**。旧实现每条记录一轮,而 `f_close` 要回写目录项、`f_write` 要对目标扇区做读-改-写 → **每条日志 2~4 次 NAND 页读写**。现在同一文件的一批(≤8 条 = 512 B 静态缓冲 `s_batch`)合并成一次追加写:
- **掉电语义不变**:批次仍在**同一次 `syslog_drain()` 调用内**写完并 `f_close`;`f_close` 内部 `sync_fs` → `disk_ioctl(CTRL_SYNC)` 会 `ftl_flush_cache()` 把 FTL 的单页写回缓存刷进 NAND(`Drivers/BSP/GD5F2GQ5UE/nand_ftl.c` 的 `CTRL_SYNC` 分支)。落盘时机与旧实现每条 close 等价,**批次不跨调用驻留 RAM**:"写完即关"的路径关文件即落盘,残余只到"单次写事务进行中掉电/复位"为止(无空闲/掉电前周期 flush,2026-10-22 用户裁定暂不做)。
- **条目不丢**:启动前数组按下标"看而不删",整批成功才删;队列条目在落盘失败(-2 卷忙 / -1 写失败 / -3 容量不足)时**回投队列**。
⚠ **2026-10-24 更正本条**:旧实现里 -2 只在**按条慢路径**被正确回投,**批量路径根本没有 -2 分支**(`done` 保持 `n` → 回投循环空转)→ 卷忙时一批(≤8 条)既不落盘也不回投、静默丢失,而本节长期写着"条目不丢"替它背书。现已补 `rc == -2` 分支整批回投(不计故障、不打日志、保序),由 `test/host/sys_log_mcu/` 的 TC-SYSLOG-035 组逐条钉住。**真机必验**:FTP 传输期间持续投日志,传输结束后条数 == 投递条数、顺序不乱。
- **容量轮转仍只在按条慢路径做**:批量路径发现剩余容量装不下一批时返回 -3,退回逐条 `s_append()`("满了 unlink 重建"只保留一处实现,避免一批跨多轮重建)。
- 自愈重试(L1 丢弃重建 + 重试一次)与错误去重(L2 只报第一次)判据与单条路径一致。
- **自愈触发集分类(2026-10-24,代码侧唯一入口 `s_is_selfheal_code()`)**:只有"删了这个文件真能修好"的介质/结构类码才触发 unlink 重建——`FR_DISK_ERR` / `FR_INT_ERR` / `FR_NO_FILESYSTEM` / `FR_DENIED`;**名字/参数类严禁归入**(`FR_INVALID_NAME` / `FR_NO_PATH` / `FR_NO_FILE` / `FR_INVALID_OBJECT`,另显式排除 `FR_NOT_READY` / `FR_NOT_ENABLED` / `FR_TIMEOUT`)。同名同因下 unlink 必然同样失败,旧集合的真实后果是**每轮排空删一次自己的历史日志且永远修不好**,违反 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 第 10 条。`f_write` 之后的"失败/短写 → unlink 重试一次"**不接**本判定(能走到那里说明已打开成功;而簇分配失败是 `FR_OK` + 短写、根本没有码可判,必须无条件重试)。钉住:TC-SYSLOG-101 / 101b / 102 / 103;`App/log/soe_store.c` 侧同源但取**更保守子集**(TC-SOESTORE-022 / 022b)。
- **卷锁 fail-open(零互斥那一态)边沿补投一条自检记录(2026-10-27 道G task-40)**:锁层只自增计数、
绝不产日志——唯一能落盘的通道(`/log/wdev.log` 等)自己也要取这把**非递归**卷锁,被卷忙
挡住时既写不出、也不许在失败分支里再取同一把锁(机理与四态语义的唯一出处是
`App/fs/vol_lock.h` 的「可观测性」段,本文不复述)。因此留痕由唯一写者在 `syslog_drain()`
尾部按**边沿**补投:判据是"计数比上一轮读到的大了",不是"计数大于零"(后者是电平,每轮一条
= 每轮一次 NAND 写,诊断自己变成负载);**一个 episode(= 一次上电)只投一条**,重新上电
复位 latch 后故障复发仍会再投(否则第二次开机起永久静默)。文本形如
`vollock failopen=3`(累计值就是证据,不带态号),落 `selftest.log`。
⚠ 三条约束(与 task-23「日志让路不计 `[ERR]`」同一根判据):① 不在失败调用点就地打日志;
② 不打 DBG 错级、不打 `[ERR]`——`[ERR]` 是"设备出故障"的唯一信号通道(`/serial-boot-test` 的 Forbidden
也依赖它),塞进可观测性事件就同时不再是故障、不再是禁令;③ 记录自带的时间戳是
**被观测到的时刻**(粒度 = 一轮 `syslog_drain()`),不是发生时刻,所以本模块刻意不再另造
`obs_first_ms`/`obs_last_ms` 字段去"看起来更精确"。主机判据:`TC-SYSLOG-137` 至
`TC-SYSLOG-137d`(首投一条并带累计值 / 同 boot 不再投 / 计数不变不投 / 重新上电可再投),
其中 `g_sys_err_count == 0` 就是"不打错级"的机器证据。
- **留痕节流分两层、各自独立抑制位**:drain 侧"整批回投"只在正常→故障那一次报(`write fail, requeue %lu entries`,落盘成功一轮复位;旧文案写作 "drop N entries" 与实际行为相反——整批其实一条没丢);post 侧 `queue full, drop` / `preboot full, drop` 各一位(**丢弃语义不变,满则丢照旧**,只压留痕次数)。节流之后**没有丢弃计数**——要计数属新增对外可见信息,另列后续项。
- 读路径与写路径并发安全:UI 页在 uiTask 经 `syslog_get_record()` 读记录(内部走 `s_read_raw()`,见 §4.5);读写两侧都取卷锁 try 语义,FatFS 系统锁保护单次调用。
- **存储未就绪/挂载失败**:待发条目滞留 RAM(下次上电丢失),串口打印告警——存储故障时本就无法持久化,属预期行为。
### 4.5 读路径(UI / 模拟器)
```c
uint32_t syslog_count(syslog_id_t id); /* = filesize/64 */
int syslog_get_record(syslog_id_t id, uint32_t idx, char *ts, size_t ts_len,
char *desc, size_t desc_len); /* 越界 / 读失败 / 缓冲容量不足 均返回 -1 */
```
- MCU:**8 条滑动读窗口**(日志读写放大收口,读侧)——`s_read_raw()` 先查窗口(同 id + 代数未变 + 落在区间内 → **零 I/O** 直接从 512 B 静态窗口返回);未命中才 `f_open(FA_READ) → f_lseek(窗口首条×64) → f_read(≤8×64B) → f_close` **一次取回连续 8 条**,再从中切出请求的那条。旧实现每读一条都要 open/lseek/read/close 一轮,UI 翻 20 条记录 = 20 次目录项/FAT 往返。
- 失效机制:写者每次落盘后把 `s_rd_gen[id]` 自增,读者比对填充时记录的代数,不匹配就重填。对齐 32 位在 ARM 上原子,跨任务(uiTask 填、defaultTask 增)无需临界区;最坏情况多读一轮,不会读错。
- **API 与调用方一律不变**(`syslog_get_record()` 签名照旧,`ui_page_runlog` / `ui_page_selftest` 零改动)。
- **容量契约(2026-10-24 写死在 `App/log/sys_log.h`)**:`ts_len > SYSLOG_TS_LEN`、`desc_len > SYSLOG_DESC_MAX`,**不足一律 -1,既不静默截断也不写调用方一个字节**(描述在写入侧已截到 `SYSLOG_DESC_MAX`,按最小容量给就一定装得下,读侧不需要第二道截断)。旧实现里那句 `if (dl >= desc_len) dl = desc_len - 1` 由上面的守卫保证**永不可达**,已删除;它的存在还会误导调用方"给 28 字节缓冲能读到 27 字符"——实际是整条读不到。UI 侧现用 32 B 列缓冲,走允许路径、行为不变。钉住:TC-SYSLOG-057、057b(0x7A 哨兵逐字节回比)、098。
- 模拟器:宿主文件同语义(UI 页双端零差异)。
- UI 页每帧仅读当前窗口 ≤8 条记录(每次重绘 8×64B SPI 读,用户翻页触发,频率极低)。
## 5. 功能设计
### 5.1 R1 密码错误提示(`ui_page_password.c`)
- ENTER 比对失败 → `s_err=1`:3 框清零、光标回首框,**数字框下方居中显示"密码输入错误"**(y≈104)。
- 错误态下按 ↑↓←→:**先清除提示**再正常执行该键(即"让用户重新输入");再按 ENTER = 重新比对。
- 比对成功路径不变(回调授权/关页)。布局常量与绘制沿用现有 culling 模式,零 LVGL 对象。
### 5.2 R2 待机显示页(`ui_standby.c/h`)
**进入条件**:UI 初始化完成后,**60s(默认,可配)内无任何按键** → 切待机屏。
- 空闲检测 = `lv_timer`(1s 周期,MCU/模拟器均经 `lv_timer_handler` 驱动,无 OS 依赖);
- 活动通知挂钩:`menu_key_cb`、`cp_key_cb` 每收到一个键调 `ui_standby_notify_activity()`(重置计时)。
**待机屏内容**(v1.1 起显示实时值,2026-11-13;全 ASCII 无需新字体字):
```
UA = 0.341 ← 6 行实时值 = ui_data 通道一 yc[](下标 = adc_semantic_t 语义序:
UB = 0.338 UA/UB/UC = 0..2,IA/IB/IC = 4..6;UN/IN 不在待机屏——用户点名
UC = 0.339 六路,且 UN 本台架永久无数据,摆一排假通道不如不摆)
IA = 0.021 ← 示例为现状 ENG_PIN 引脚电压档:数字照显、不挂单位(挂单位 =
IB = 0.020 替用户完成没做的标定换算,同交流量页引脚档不造第二口径)
IC = 0.021 ← 升二次/一次值档后单位随 eng:UA/UB/UC 挂 V、IA/IB/IC 挂 A
────────────────────── ← 分隔线(y≈132)
YYYY-MM-DD HH:MM:SS ← 底部实时时钟(每秒重绘,lv_timer 驱动 invalidate)
```
- 空值/坏质口径与格式化的唯一真源 = 交流量页(`ui_page_ac.c`):无首帧或品质带
`IV|NT|RAW|SB|BL|OV` → `--`;3 位小数定点格式化复用 `ui_w_editor_fmt_scaled`(不引 %f)。
- 帧读取按 `ui_data_rt_seq()` 门控整帧拷(`ui_data.h` 头注);待机 1s 定时器整屏
invalidate 即数值刷新节拍,未新增定时器。模拟器 `main.c` 自带固定仿真 rt 帧:待机截图
(`--standby`)即双分支实证——UA/IA 显引脚档数字(0.812 / 0.046,不挂单位)、
UB/UC/IB/IC 带 `Q_IV|Q_OV` 坏质显 `--`。
**键语义(用户定案)**:待机屏上**只有 ENTER(确认键)进入菜单**;其余按键忽略(同时重置空闲计时)。
- 实现为**屏幕覆盖层**:进入时保存当前活动屏 + 组内焦点对象,加载待机屏并聚焦其容器;ENTER 时还原前屏与焦点。
- 内容页打开期间超时 → 同样切待机(页面对象保留在底层,ENTER 返回后原样恢复,编辑态不丢)。
### 5.3 R3 运行记录(上电/重启分类)
defaultTask 挂载成功、排空待发队列**之后**写一条:
```
读 run.log 末条记录的 epoch 字段 E:
墙钟有效 且 (now - E) < 600s → "装置重启记录"(近期刚运行过 = 软复位/看门狗)
其余(无文件/首条/间隔≥10min/墙钟无效)→ "装置上电记录"
```
- 阈值 `SYSLOG_REBOOT_WINDOW_S = 600`:断电 <10min 会被误判为重启——无掉电检测硬件时该歧义不可消除,取工程折中并在此声明。
- 串口标记(供 /serial-boot-test):`runlog boot entry: 装置上电记录 / 装置重启记录`(`sys_log_mcu.c` `DBG_INFO`,与落盘描述串同源)。
### 5.4 R4 自检记录(v1 项表)
| 检查点 | 触发时机 | 描述串(落盘 + UI 显示同源) |
|---|------|------|
| RTC/墙钟建立失败 | app_main_init | `时钟芯片异常` |
| NAND 初始化失败 | app_main_init | `NAND异常` |
| ADC 初始化超时 | app_main_init | `ADC异常` |
| FatFS 挂载/建卷失败 | defaultTask(仅串口,存储不可写) | (无法落盘,串口 `[SELFTEST] fs: FAIL`) |
| 网络链路 3s 未建立 | net_task bring_up 后 | `通讯异常` |
- **只记故障**;全部通过不写记录(与 mockup"有问题进行记录"一致)。
- v1 不做"已复归/动作/标志字"(mockup 中为保护继电器专有语义)——列入开放问题 O2。
- UI 页空态:`count_cb` 返回 0 时以单条伪行 `无记录` 呈现(页脚"共 1 页",避免"共 0 页")。
## 6. UI 布局与字体
### 6.1 新增字符集(11 字)→ 重生成 `ui_font_cn_12.c`
| 用途 | 新字 |
|---|------|
| 密码错误提示"**密码输入错误**" | 输、入、错、误 |
| 运行记录"装置**上电**记录 / 装置**重**启记录" | 上、电、重 |
| 自检故障名"时**钟芯片异常**"等(异/常为公共后缀) | 芯、片、异、常 |
流程按 `font-regen` skill:改 `ui_cn_chars.txt` → extract_cn → make_font → diff。注意:**extract_cn 只扫 UI 源码**,故障描述串在非 UI 源码(sys_log_mcu.c/app_main.c/net_task.c)中,须手动补入字符集;其余文案(运行记录/自检记录/无记录/第 X 页共 Y 页)均已在字符集内;待机页全 ASCII。
### 6.2 页面几何(160×160,沿用框架常量体系)
| 页 | 参数 |
|---|------|
| 运行记录 / 自检记录 | `selectable=1, rows_per_page=8, top_y=24, pitch=14, band_h=13`(同 AC/DC 约定;**偏离 mockup**:实机照片无选中带,但框架 selectable 页选中行恒黑字、band_h=0 时黑字黑底不可见,故保留标准白带),`left_x=7`;**每条记录 = 2 行**:偶数行 = 时间戳(`YYYY-MM-DD HH:MM:SS`,19 ASCII ≈114px 可容)、奇数行 = 描述(≤9 汉字);↑↓按行移动(8 行/页 = 4 条/页),页脚自动分页 |
| 待机屏 | 6 数据行 y=28 起 pitch=18(label x≈24 / `=` x≈72 / 值 x≈96);分隔线 y≈132;时钟 y≈140 居中 |
| 密码页错误提示 | "密码输入错误" 居中,y≈104(数字框行 y=78..92 之下) |
## 7. 内存预算
| 项 | 大小 | 位置 |
|---|------|------|
| sys_log MCU:启动前待发数组 8×12B + 队列 8×12B + 状态 | ≈300B(静态)+ ≈150B(FreeRTOS 堆,消息队列) | .bss / heap_4 |
| ui_standby:待机屏 + 容器 LVGL 对象(常驻) | ≈300B | LVGL 池(16KB,当前占用 ≈5KB) |
| 记录页模块 | 0(描述符驱动,零 LVGL 对象;仅栈缓冲) | — |
| 字体 +10 字形 | ≈250B Flash | .rodata |
**结论:不新增任务、不动任务栈、FreeRTOS 堆增量 <200B,LVGL 池余量充足。**
## 8. 测试计划
### 8.1 模拟器(无头自动化)
| 用例 | 手段 |
|---|------|
| 日志引擎闭环 `--logtest` | post N 条 → count/get 回读断言 + 宿主文件字节级校验(64B 定长、ISO 时间戳、NUL 填充),0=PASS(仿 `--cfgtest`) |
| 待机屏渲染 `--standby` | 无头注键后直接调测试钩子强制进入待机 → 截图比对(布局/时钟行);窗口模式 `--idle <s>` 可人工验证真实超时路径 |
| 记录页像素回归 | `--keys` 导航至 node 39/40 → 截图 + 文本断言(时间戳行/描述行/页脚分页),纳入 ui-verify 门禁 |
| 密码错误提示 | `--keys` 输错密码序列 → 截图断言"密码输入错误"可见;再按方向键 → 提示消失 |
### 8.2 真机(/serial-boot-test 串口标记)
Required:`App init OK`、`Network Init`、`net init: OK`、**`runlog boot entry:`**(装置上电记录 / 装置重启记录)、**`UI standby: ready (idle=60s)`**;
Forbidden:`FAIL`/`ERROR`/`HardFault`。
人工目检:待机屏(静置 60s)、记录页翻页、FTP 下载 `/log/*.log` 文本可读性。
## 9. 风险与开放问题
| # | 项 | 策略 |
|---|----|------|
| O1 | 断电 <10min 的"上电/重启"歧义 | §5.3 阈值折中,文档声明;如需精确可后续加掉电标志(PVD) |
| O2 | 故障"已复归"/持续监测(mockup 语义) | v2:周期自检 + 活动故障状态机 + 恢复记录 |
| O3 | ~~"清除记录"菜单项~~ **已实施(2026-10-26 道F task-4)** | 数据面 = `syslog_request_clear(id)` + `syslog_clear_state(id)`,落地在 defaultTask(唯一写者)的 `syslog_drain()` 尾部;UI 侧页面归记录/维护页批次。细口径见 `App/log/README.md` |
| — | FAT32 小追加簇开销 | §3 调研结论:写频率极低可接受;容量上限约束增长 |
| — | UI 读与落盘并发 | 只读既有内容 + EOF 追加 + FatFS 系统锁,无互斥需求(§4.4) |
## 10. 实施步骤(按序执行,每步双端构建 0/0)
> ✅ **步骤 1–9 已于 2026-09-14 全部完成**:Keil `@build` 0 错 0 警 + 模拟器 0 警告;步骤 8 = `--logtest` PASS(期间修复 ts 19 字符 / epoch 10 位两处格式 bug)+ ui-verify 五门禁全绿(池基线 5440/5520B、pwc 参考更新)+ 记录页/待机屏/密码错误提示截图目检通过;步骤 9 = 文档同步。
| # | 步骤 | 产物 |
|---|------|------|
| 步骤 1 | 字体补字 + 重生成 | `ui_cn_chars.txt`、`ui_font_cn_12.c` |
| 步骤 2 | 日志引擎:`sys_log.h`(共享 API/格式)+ `sys_log_mcu.c` + `sim/sim_syslog.c` | 双端可 post/count/get |
| 步骤 3 | 自检埋点 + defaultTask 落盘循环 + 上电/重启分类 | app_main/freertos/net_task 接线 |
| 步骤 4 | 密码错误提示 | ui_page_password.c |
| 步骤 5 | 待机屏 `ui_standby`(空闲检测 + 覆盖层)+ 键挂钩 | ui_menu/ui_content_page 各一行通知 |
| 步骤 6 | 记录页 ×2 + 内容页框架接线(枚举/描述符表/node 映射) | ui_page_runlog/selftest、ui_content_page.h/.c、ui_menu.c |
| 步骤 7 | 双端注册新文件(uvprojx + CMakeLists)+ 构建 0/0 | new-source-file skill |
| 步骤 8 | 模拟器无头回归(--logtest/--standby/截图断言)+ 真机 /serial-boot-test | 测试通过记录 |
| 步骤 9 | 文档同步(项目架构总览、AGENTS.md、本文档状态) | docs |
@@ -56,7 +56,7 @@
> 并在 ISR 内唤醒 `adcTask`——采集节拍与任务唤醒**不允许**各自计时。
> ⚠ **ADC1 必须在挂 DMA 的瞬间处于关闭态**:`app_main_init()` 步骤 4b-pre 的 `adc1_disable()` 不可删、不可后移,
> 否则 DMA 乒乓缓冲整体旋转 2 格(表象与"映射表写错"一致)。证据见
> [`docs/子系统设计/ADC采样节拍设计说明.md`](../子系统设计/ADC采样节拍设计说明.md) §4.5。
> [`docs/子系统设计/ADC采样节拍设计.md`](../子系统设计/ADC采样节拍设计.md) §4.5。
## 3. 就绪屏障(Event Flags)
+3 -3
View File
@@ -197,7 +197,7 @@ flowchart LR
```
- **配置文件**:通用 `/config.ini`(cfg_file 引擎,静态态单任务收敛于 defaultTask,外部变更 4s 沉降后自动重载),**多段注册制**:`[network]`(net_cfg)/ `[ftp]`(鉴权)/ `[adc_cal]`(标定系数与零点)/ `[run_par]`(运行参数 + 104 地址映射真源)/ `[comm]`(104 ca/端口);新模块加段=注册 `cfg_file` 段处理器,唯一落盘者不变。→ `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`
- **记录九件**:`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 管理。
@@ -388,13 +388,13 @@ flowchart LR
| 采集与算法层 语义取数与有效性~保护判据(**设计与判据**) | `docs/算法/采集与算法层设计说明.md` |
| 采集与算法层目录索引(文件清单 / 构建测试) | `App/algo/README.md` |
| 保护功能复现规划(分层归属 / 功能项状态) | `docs/算法/继电保护功能复现规划.md` |
| ADC 采样节拍(TIM3 1kHz 同源驱动) | `docs/子系统设计/ADC采样节拍设计说明.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` |
| 运行与自检记录(64B 记录 + SOE) | `docs/子系统设计/运行与自检记录设计.md` |
| 墙钟时间维护 | `docs/子系统设计/系统墙钟时间维护说明.md` |
| 控制台单写者日志(UART5 调试流) | `docs/子系统设计/控制台单写者日志设计说明.md` |
| 看门狗 / CubeMX 重生成存活方法论 | `docs/子系统设计/看门狗设计说明.md` §10 |
@@ -423,8 +423,8 @@ uint16_t ch395f_link_change_count(void);
(`net_msg_t` 是按值拷入队列的,改副本调用方看不到)。桩 = 新建假 CH395F。
该文件 80 次提交、15 次 fix/refactor,值;但**一次做完必翻车**(先求把状态机进分母,别求百分比)。
2. **`sys_log_mcu.c`(458) + `cfg_file_store_mcu.c`(129) + `soe_store.c`(123) = 710 行** —— 一套假 FatFs
吃三块,性价比最高的第二批。判据现成:`docs/子系统设计/运行与自检记录设计说明.md` §4.4(写侧批量合并 + **掉电语义不变** +
失败回投不丢条目 + 容量轮转只在慢路径)与 `docs/子系统设计/运行与自检记录设计说明.md` §4.5
吃三块,性价比最高的第二批。判据现成:`docs/子系统设计/运行与自检记录设计.md` §3.2(写侧批量合并 + **掉电语义不变** +
失败回投不丢条目 + 容量轮转只在慢路径)与 `docs/子系统设计/运行与自检记录设计.md` §4.5
(读侧 8 条窗口 + 写者代数失效 + API 不变)。
这块的历史教训就是"日志读写放大"与"`mkdir` 失败静默不再重试"。
3. **`console_log.c`(265)** —— 单写者契约 K1/K2 已经写成表,照抄成用例最省力:
@@ -499,7 +499,7 @@ uint16_t ch395f_link_change_count(void);
| # | 位置 | 现象与影响 | 钉住它的用例 |
|---|---|---|---|
| 1 | `App/log/sys_log_mcu.c:509~537` | **疑似数据丢失(建议按 P1 定级)**:批量路径 `done = n` 只有 `rc==-3`/`rc==-1` 两个分支会改它,**`rc==-2`(卷忙)没有分支** → 紧随其后的 `for (i = done; i < n; …)` 回投循环一次都不执行 → 最多 8 条日志既不落盘也不回投、不计故障、不打日志。同段 `:531` 注释与 `docs/子系统设计/运行与自检记录设计说明.md` §4.4 都写"条目不丢(旧实现 -2 丢那一条)"→ **现状比旧实现更糟**(旧丢 1 条、现一次丢 8 条)。对照:**逐条慢路径遇 `-2` 会正确回投尾部**,同一错误码在两条路径行为相反。触发面常见:卷锁 v1.2 之后 ftpTask 传文件期间 defaultTask 每 2 s 排空一次 | TC-SYSLOG-035 / 035b / 035c |
| 1 | `App/log/sys_log_mcu.c:509~537` | **疑似数据丢失(建议按 P1 定级)**:批量路径 `done = n` 只有 `rc==-3`/`rc==-1` 两个分支会改它,**`rc==-2`(卷忙)没有分支** → 紧随其后的 `for (i = done; i < n; …)` 回投循环一次都不执行 → 最多 8 条日志既不落盘也不回投、不计故障、不打日志。同段 `:531` 注释与 `docs/子系统设计/运行与自检记录设计.md` §3.2 都写"条目不丢(旧实现 -2 丢那一条)"→ **现状比旧实现更糟**(旧丢 1 条、现一次丢 8 条)。对照:**逐条慢路径遇 `-2` 会正确回投尾部**,同一错误码在两条路径行为相反。触发面常见:卷锁 v1.2 之后 ftpTask 传文件期间 defaultTask 每 2 s 排空一次 | TC-SYSLOG-035 / 035b / 035c |
| 2 | `App/log/sys_log_mcu.c:527~529` | `rc==-1` 打 `DBG_ERROR("drop %lu entries …")`,但该分支 `done=0` → 整批其实**全部回投**、一条没丢 → 文案与行为相反,且持续写故障时每轮 drain 都重投重试一遍 | TC-SYSLOG-036 |
| 3 | `App/log/sys_log_mcu.c:616` vs `627~628` | 守卫写成 `desc_len <= SYSLOG_DESC_MAX` 就 `-1`,而 `dl` 恒 ≤ 28 → 截断分支**永不可达**。无害(宁可拒绝不越界),但"传小缓冲能拿到截断内容"的直觉是错的 | TC-SYSLOG-098 / 057 |
| 4 | `Drivers/BSP/NET/net_socket.c:2825` / `:2869` / `:3080` | **同一文件两套 IP 字节序约定,`inet_addr` 与 `ntoa`/`getsockname` 不互逆**:`net_inet_addr` 小端组装后又 `NET_HTONL` → `0xC0A80164`;`net_inet_ntoa` 交换后高段在前 → 只能正确还原 `getsockname` 那族(`0x6401A8C0`);`net_connect_locked:1571` 用 `>>24` 取首段 → 只配 `inet_addr` 那族。**为什么长期不被看见**:`ntoa` 自己 `:2865~2867` 那段 E13 修复注释里的例子「192.168.1.100 → 0xC0A80164 → "192.168.1.100"」取的是**另一族**的值(交换宏下实算 `"100.1.168.192"`)。前提已核清:`Drivers/BSP/NET/net_types.h`:56 是 `#ifdef __BIG_ENDIAN` 才恒等,STM32 与 x86/MSVC 都走交换分支 → **不存在"主机语义≠目标语义"**。当前无调用者跨族(`lftpd.c`:864 走 `getpeername→ntoa` 同族恰好正确),属埋在库 API 面上的地雷;统一必须跨 `inet_addr`/`ntoa`/`getsockname`/`connect` 解析/`lftpd.c`:548 一起过 | TC-NETSOCK-100b / 100c / 100d、107f、430h(钉的是**具体数值**,统一时会五条一起红并互相指认,不会被误读成单点回归) |
@@ -704,7 +704,7 @@ uint16_t ch395f_link_change_count(void);
第 2 轮交付汇报里,「卷忙整批回投不丢条目」被列进上台架必验项。本轮把它跑成,并在跑的过程中改掉一个真隐患。
- **正/负双向实证(台架,全程非破坏性)**:临时钩子从 testTask 以 2.5 s 间隔投 12 条带序号记录(远低于 8 槽队列容量,避开投递侧「满则丢」),窗口中段起一次 8 MB FTP STOR 抢卷,回读 `LOG/SELFTEST.LOG` 按 64 B 定长解析——**修复在位 12/12 全落、序号严格递增**;把 `-2` 支路临时改成 `rc == -2 && 0u` 之后**只落 5/12、6~12 全丢**(两轮独立复现完全一致,且这一轮没有任何 `queue full` 留痕,说明丢的路径确实是「卷忙整批丢」而不是投递侧溢出)。**负向对照是必要的**:若卷其实没被占住,「零丢失」就是空结论。
- **顺手照出的隐患**:待发条目只存 `const char *`,复用栈缓冲投递时整批别名到最后一次写入(实测落盘「1~5 + 12 七份」)。已改为条目内嵌 `desc[SYSLOG_DESC_MAX + 1u]` 并在投递时截断拷贝,头文件参数契约同步改写;代价 `+352 B ZI / +60 B Code`。理由与实测细节 → `docs/子系统设计/运行与自检记录设计说明.md` §4.4。
- **顺手照出的隐患**:待发条目只存 `const char *`,复用栈缓冲投递时整批别名到最后一次写入(实测落盘「1~5 + 12 七份」)。已改为条目内嵌 `desc[SYSLOG_DESC_MAX + 1u]` 并在投递时截断拷贝,头文件参数契约同步改写;代价 `+352 B ZI / +60 B Code`。理由与实测细节 → `docs/子系统设计/运行与自检记录设计.md` §3.2。
- **两次测法自误(教训,别重复)**:① 先按 700 ms 快投递测,测到的 34 条丢失其实是投递侧队列满(设计如此),根本没走到被整改的支路——**测错了对象**;② 钩子投完后 `return` 使 testTask 退出,约 10 s 后被看门狗复位,把还压在 RAM 队列里的条目一起带走——**污染了自己的计数**。两者都不是固件问题,根因都是没有先验证测量手段本身有效。
- **假件护栏的一次正向记录**:条目由 8 B 变 32 B 后,`test/host/sys_log_mcu` 的假消息队列因 `MQ_MAXMSG`(原 24)拒收一切 `osMessageQueuePut`,从既有用例起整片失败——这种「尺寸变了就响」的断言比任何评审都可靠;该宏已抬到 40 并把现象写进注释。
- 本轮改动面:`App/log/sys_log_mcu.c`(条目结构 + 投递拷贝)、`App/log/sys_log.h`(参数契约)、`test/host/sys_log_mcu/sys_log_mcu_test.c`(新增 2 条断言 + `MQ_MAXMSG`)、`scripts/archive/id_gate/id_registry.json`(游标 `TC-SYSLOG-108`)。
+1 -1
View File
@@ -118,7 +118,7 @@ STM32(U17)为 LQFP144 脚号。
| ADC1 上电自校准 | `Src/adc.c` 的 `USER CODE BEGIN ADC1_Init 2` 调 `adc1_calibrate()`(实现同文件 `USER CODE BEGIN 1`,按 RM0090 §13.3.6:清 ADON→等生效→置 ADON→置 CAL→等 CAL 由硬件清零)。**CubeMX 不生成这一步**,裁剪版 HAL 没有 `HAL_ADCEx_Calibration_Start`、裁剪版 CMSIS 头没有 `ADC_CR2_CAL`,代码里按 RM 自建 `ADC1_CAL_BIT`;审计项 `adc1-calibration-call` / `adc1-calibration-impl` 兜底 | 缺这一步**不报错,但读数带失调**(实测校准后 STM32 侧六路同向移动 +10~+63 LSB,TPAFE 侧不动) |
| ADC 时钟 | ADCCLK = 21 MHz(PCLK2/4)。CubeMX 里 DIV2 候选被 DB 约束 `FadcConstraint Max=36000000` 禁掉(APB2 84 MHz → 42 MHz 超限),`.ioc` 不保存 `ADC1.ClockPrescaler` 键,重生成只会回落到约束允许的值 | 不额外设防护 |
| `adc_task` | `App/task/adc_task.c` 每拍调 `adc_input_poll()` = TPAFE `tpafe5160_snapshot()` 原子快照(8 通道)+ 消费一帧 STM32 ADC1 DMA 乒乓缓冲,**经 `adc_input_raw()` 按语义取数**;消费唤醒由 TIM3 ISR 置位 `ADC_WAKE_FLAG`;任务在 `Src/freertos.c` 的 `USER CODE` 区创建(640 words / **AboveNormal**,`Src/freertos.c:159 .stack_size = 640 * 4`) | 台架实测 `frames` 严格 1000 Hz 递增、`idle=0`、`adc1_rc=0`、`adc1_err=0` |
| TPAFE 采样触发 | `Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 **TIM3 更新事件(1 kHz)** 内调 `tpafe5160_start_conv_irq()`;`app_main_init()` 用 `HAL_TIM_Base_Start_IT(&htim3)` 启动节拍(在 `tpafe5160_init()` 之后)。同一 TIM3 更新事件经 **TRGO** 触发 ADC1 一轮 9 通道,并调 `adc_tick_irq_notify()` **唤醒消费者**(内含"消费者就绪"门) | 两片转换器由同一硬件事件同源对齐,连续波形 1 kHz;详见 `docs/子系统设计/ADC采样节拍设计说明.md` §4(DMA 乒乓搬运)与 §6(事件驱动唤醒) |
| TPAFE 采样触发 | `Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 **TIM3 更新事件(1 kHz)** 内调 `tpafe5160_start_conv_irq()`;`app_main_init()` 用 `HAL_TIM_Base_Start_IT(&htim3)` 启动节拍(在 `tpafe5160_init()` 之后)。同一 TIM3 更新事件经 **TRGO** 触发 ADC1 一轮 9 通道,并调 `adc_tick_irq_notify()` **唤醒消费者**(内含"消费者就绪"门) | 两片转换器由同一硬件事件同源对齐,连续波形 1 kHz;详见 `docs/子系统设计/ADC采样节拍设计.md` §4(DMA 乒乓搬运)与 §6(事件驱动唤醒) |
| 前端偏置 | 每路 1k 串联 + 滤波 C 跨接差分输入;差分负端(`*_GND` 网)经 1k 拴 AGND,**不是对地分压**(拓扑图见 §7.1),**无中点偏置** | TPAFE 输入阻抗 **1 MΩ 恒定**(`../芯片手册/TPAFE5160.md` "Analog Input Impedance" 节),端子对 AGND 的直流通路 = 1k + 1 MΩ + 1k ≈ **1.002 MΩ**,不是 1 kΩ;两侧各 1 kΩ 是手册要求的**源阻抗匹配**,不是分压。STM32 侧只能测 **0~3.3 V 单极性**,交流量负半周被削顶;主板侧只做监视可接受,若算 RMS/功率需硬件补偏置或另定算法口径 |
| TPAFE 量程 | 板上 RANGE 接 GND → **±5 V 双极性量程**(芯片手册输入量程表:RANGE = 0 → −5 ~ +5 V),16 位二进制补码,**±5 V 满量程 = 10 V 跨度**,LSB = 10 V / 65536 ≈ **152.59 µV/码(@ 转换器引脚)**。RANGE = HIGH 的 ±10 V 档(跨度 20 V、LSB ≈ 305.18 µV)是芯片另一可选档,**本板未用**;305.18 只属该档的 LSB,**不是本板系数** | 用 `tpafe5160_to_voltage()` 换算;端到端口径见 §7.1 |
| 取数契约 | 消费者**只能**用 **`tpafe5160_snapshot(dst, count)`**(临界区原子拷贝,返回 1=有新帧 / 0=无新帧 / -1=参数错,**仅任务上下文**)+ **`tpafe5160_frame_seq()`**(帧序号只增不清)+ **`tpafe5160_frstdata_err()`**(无跳变帧计数)。`tpafe5160_init()` 含**首帧自检**:一帧读下来 FRSTDATA 无跳变(芯片不驱动并行总线,板上 R24 4.7k 上拉会把它钉死为高)→ 返回 `-3` + `[ERR]` | `AGENTS.md` 已立为铁律;开机 `[ADC] Init OK` 只在自检帧通过后才打印 |
@@ -238,7 +238,7 @@
| 功能名 | 关键参数 | 完成判据 | 状态 |
| --- | --- | --- | --- |
| 周期 CONVST 重触发 | 1000 Hz(与保护节拍同源) | 示波器见 U3 `BUSY` 连续脉冲;`tpafe5160_frame_seq()` 持续递增 | ✅ 2026-10-10:**TIM3 更新事件(1 kHz)硬件触发**(ISR 内 `tpafe5160_start_conv_irq()`;方案 A,零引脚/零驱动改动。⚠ 节拍源**不能**是 TIM6/TIM7——F4 上 ADC1 收不到它们的 TRGO,见 `docs/子系统设计/ADC采样节拍设计说明.md` §3.2);台架 `/serial-boot-test` PASS——`seq` 每 10 s 精确 +10000 = 1000.0 Hz、`frst_err=0`(早期证据 `docs/测试说明/测试数据/boot_log_data/20261010_prod_adc_tim6_boot_log.txt`,节拍源当时为 TIM6) |
| 周期 CONVST 重触发 | 1000 Hz(与保护节拍同源) | 示波器见 U3 `BUSY` 连续脉冲;`tpafe5160_frame_seq()` 持续递增 | ✅ 2026-10-10:**TIM3 更新事件(1 kHz)硬件触发**(ISR 内 `tpafe5160_start_conv_irq()`;方案 A,零引脚/零驱动改动。⚠ 节拍源**不能**是 TIM6/TIM7——F4 上 ADC1 收不到它们的 TRGO,见 `docs/子系统设计/ADC采样节拍设计.md` §3.2);台架 `/serial-boot-test` PASS——`seq` 每 10 s 精确 +10000 = 1000.0 Hz、`frst_err=0`(早期证据 `docs/测试说明/测试数据/boot_log_data/20261010_prod_adc_tim6_boot_log.txt`,节拍源当时为 TIM6) |
| TPAFE 快照消费 | 8 通道 / 帧 | `adc_task` 每拍调 `tpafe5160_snapshot()`,返回 1/0/-1 有处理 | ✅ ADC 采集链路 已实现并 `/serial-boot-test` 闭环(`adc_task.c`;台架 `idle=0`、`frst_err=0`) |
| STM32 ADC1 启动与扫描 | 9 通道、144 cycles、**TIM3 TRGO 硬件触发 + DMA** | 全库出现 `HAL_ADC_Start_DMA`;9 通道码值随注入变化 | ✅ 2026-10-10(v2.0):`App/algo/adc_input.c` 的 `stm32_take()`——ADC1 由 TIM3 更新事件经 TRGO 触发一轮 9 通道,`HAL_ADC_Start_DMA(&hadc1, buf, 18)` + DMA2_Stream4 循环模式把码值自动搬进**乒乓双缓冲**,任务侧只消费刚完成的半区(**零忙等**,实测单拍 `adcp` 122 µs → **8 µs**);台架 `/serial-boot-test` PASS——`adc1_seq` 每拍 +1、`adc1_err=0`、`adc1_hw=0`。⚠ 「9 通道码值随注入变化」已由 2026-10-27 台架已知电压注入直接证实(六项判据全 PASS,实测记录 `docs/硬件平台/ADC通道映射说明.md` §8.1);多点标定与端子/子板变比确认仍待做 |
| 通道语义映射 | 见 `docs/硬件平台/ADC通道映射说明.md`(**按语义取数,不写死下标**) | `UA→buf[6]`、`UN→buf[5]`、`IA→buf[2]`;STM32 侧 `UB→ADC_CHANNEL_14` | ✅ 2026-10-10:`App/algo/adc_input.c` 的 `k_adc_map[]` 语义表(`UA/UN/IA`→TPAFE `buf[6]/[5]/[2]`,`UB/UC/IB/IC/IN/TEMP`→ADC1 `CH14/3/6/1/2/5`)+ `adc_input_raw()` 按语义取数;开机**映射自检**(`adc_input_init()` 校验 STM32 映射通道必落在扫描序列内 → 挡住"映射到不扫描的通道"静默错)通过(`ADC input layer init: OK`)。台架首帧日志 8 路 + 温度全部读出:`UA=11429 UN=11454 IA=11452`(与 TIM3 轮次一致)、`UB=1018 UC=1044 IB=943 IC=1069 IN=1052 T=850`(互异、非饱和;**两次开机读数不同** → 确为实时转换而非常量) |
@@ -2,7 +2,7 @@
> **本文回答的问题**:`App/algo/adc_health.c` 这 200 行为什么长成现在这样——判据是怎么定下来的、
> 每个设计选择换到了什么、代价与盲区在哪里、改它之前必须一起动什么。
> **阅读前提**:采样节拍与乒乓双缓冲 → `docs/子系统设计/ADC采样节拍设计说明.md`;
> **阅读前提**:采样节拍与乒乓双缓冲 → `docs/子系统设计/ADC采样节拍设计.md`;
> 8 路电气量为何分属两片转换器 → `docs/硬件平台/ADC通道映射说明.md`。
> **不在本文范围**:接口签名与全层约定速览在 `docs/算法/采集与算法层设计说明.md`,本文不重复其接口表;
> 需求与验收口径登记在 `docs/算法/继电保护功能复现规划.md` §3.1(功能项「采样有效性校验」)与同文档 §3.5(动作留痕自检与闭锁)。
@@ -92,7 +92,7 @@
两条边界是刻意的,破坏它们的代价写在右边:
1. **ISR 侧只加计数,不在中断里打日志**——FRSTDATA 异常只累计,帧序号只自增。
中断里做重活会直接推后下一拍采样,见 `docs/子系统设计/ADC采样节拍设计说明.md` §5。
中断里做重活会直接推后下一拍采样,见 `docs/子系统设计/ADC采样节拍设计.md` §5。
2. **判定层不许出现 HAL / 日志 / 取数**——`test/host/adc_health/adc_health_test.c` 用 `#include`
原样纳入生产源文件、**零桩**跑全套。判定层里多一个 `DBG_ERROR`,主机侧就要为它写一份桩,
「离线穷举故障」这条唯一的验证路径随之失效(§11)。
@@ -249,7 +249,7 @@ else { 清电平位 } /* 只有 stall 类有 else 分支,SEQ_BACKWARD 没有
两条车道**同单位(帧点数)**,心跳里 `tgap` 与 `adc1_gap` 可以直接对比(2026-10-21 之前
固件的 `adc1_gap` 是"次数"口径,跨版本对比要留意:单拍超一帧时两者数值相同,只有
一次跨过 ≥2 帧才会分出差值)。字段口径见 `docs/子系统设计/ADC采样节拍设计说明.md` §8。
一次跨过 ≥2 帧才会分出差值)。字段口径见 `docs/子系统设计/ADC采样节拍设计.md` §8。
为什么刻意不告警、不锁存、不闭锁:消费者超拍是**常态**。同一批台架日志里,
启动日志突发期 `adc1_gap` 已累计 23 次,稳态每 10 s 心跳 +1~2 次(`tgap` 1 → 2)。
@@ -542,7 +542,7 @@ stateDiagram-v2
| 剧本 | 判定路径 | 结论 |
|---|---|---|
| 现场报「保护退出」 | 查 `[PROT] sampling FAULT` 的 `hf` → 回到 `flags` 上升沿行看 `stall_ms` | `hf=0x4` → 计数器被动过,查软件;`hf=0x1/0x2` → 采样侧真停,查节拍与转换器供电 |
| 心跳里 `idle` / `skip` 在涨但无 `health` 行 | 那不是本层的判据(本层只看序号) | 属 语义取数与有效性相位伪影或通道首次未就绪,见 `docs/子系统设计/ADC采样节拍设计说明.md` §8 |
| 心跳里 `idle` / `skip` 在涨但无 `health` 行 | 那不是本层的判据(本层只看序号) | 属 语义取数与有效性相位伪影或通道首次未就绪,见 `docs/子系统设计/ADC采样节拍设计.md` §8 |
| 偶发一条 `health` + 一条 `recovered` | 一次瞬态静默(几十 ms 到几百 ms) | 查同窗口的日志突发、NAND 擦写、FTP 传输等长耗时窗口(`docs/子系统设计/看门狗设计说明.md` §4.3) |
---
@@ -632,7 +632,7 @@ stateDiagram-v2
**边界与残余**:TIM3 停 → ADC1 无触发 → 无 DMA 请求 → `s_dma_seq` 也停(节拍源是唯一上游,别
以为「DMA 还在循环所以计数会涨」)。开机不误锁的前提是 TIM3 在 `app_main_init()` 就启动、远早于
`osKernelStart()`(`docs/子系统设计/ADC采样节拍设计说明.md` §3.4),protectTask 首拍时计数器已在
`osKernelStart()`(`docs/子系统设计/ADC采样节拍设计.md` §3.4),protectTask 首拍时计数器已在
前进。残余风险:`protectTask` 自己被饿死时无人判老,此时退回 IWDG(它在 `required_mask = 0x1F7` 内)。
---
@@ -640,7 +640,7 @@ stateDiagram-v2
## 16. 相关文档
- `docs/算法/采集与算法层设计说明.md` §4 —— 本模块在全层中的位置与接口速览(本文不重复)
- `docs/子系统设计/ADC采样节拍设计说明.md` §8 —— 心跳字段全表与判读(`tgap` 所在行)
- `docs/子系统设计/ADC采样节拍设计.md` §8 —— 心跳字段全表与判读(`tgap` 所在行)
- `docs/子系统设计/看门狗设计说明.md` §4.3 —— 长耗时窗口的三类处置(瞬态静默的候选来源)
- `docs/子系统设计/看门狗设计说明.md` §11.4 —— `[PROF]` 探针字段与耗时基线(判「超拍」要看它)
- `docs/硬件平台/ADC通道映射说明.md` —— 语义映射唯一权威口径(与「哪一侧静默」直接相关)
@@ -648,4 +648,4 @@ stateDiagram-v2
- `docs/算法/继电保护功能复现规划.md` §3.5 —— 动作留痕自检与闭锁(本层供数的「自检失败闭锁」在此登记)
- `docs/内存管理/RAM使用分析与优化方案.md` §2.3 与 §4.0.6 —— `adcTask` 栈配置、最坏链与余量台账
- `App/algo/README.md` —— 目录级文件清单与构建测试入口
- `docs/子系统设计/ADC采样节拍设计说明.md` §3.4 —— TIM3 启动位置与顺序(兜底门开机不误锁的前提)
- `docs/子系统设计/ADC采样节拍设计.md` §3.4 —— TIM3 启动位置与顺序(兜底门开机不误锁的前提)
@@ -11,7 +11,7 @@
> **相关文档**:
> 分层与功能项(语义取数与有效性~动作留痕)的唯一属主登记处:`docs/算法/继电保护功能复现规划.md` §2.1(分层)、§3(功能清单)、§4.2(精度)、§7(路线图)
> `docs/硬件平台/ADC通道映射说明.md`(**语义映射唯一权威口径**)
> `docs/子系统设计/ADC采样节拍设计说明.md`(TIM3 1 kHz 同源节拍、ADC1+DMA 乒乓缓冲、`adcTask` 唤醒)
> `docs/子系统设计/ADC采样节拍设计.md`(TIM3 1 kHz 同源节拍、ADC1+DMA 乒乓缓冲、`adcTask` 唤醒)
> `docs/算法/傅氏算法详解.md` §2.3(有效值口径怎么来的)
> `docs/测试说明/单元测试/相量层单元测试说明.md`、`docs/测试说明/单元测试/ADC输入层单元测试说明.md`(主机单测细则)
@@ -53,7 +53,7 @@
## 2. 一拍的数据流与初始化落点
一拍 = TIM3 的一个 1 kHz 更新事件(节拍与消费者唤醒同源,见
`docs/子系统设计/ADC采样节拍设计说明.md` §1)。
`docs/子系统设计/ADC采样节拍设计.md` §1)。
```mermaid
flowchart TD
@@ -98,7 +98,7 @@ flowchart TD
| `adc_input_poll()` | 采一拍:`tpafe5160_snapshot()` 原子快照(8 通道)+ `stm32_take()` 消费**刚完成的那半区**(按完成计数奇偶推半区,零忙等) | **仅任务上下文**(快照是临界区);在 ISR 里调会破坏 `tpafe5160_snapshot()` 的"仅任务上下文"约定 |
| `adc_input_raw(ch, &out)` | 按语义读原始码值(TPAFE 16 位补码 / STM32 `0..4095`);`-2` = 该来源尚无有效数据 | 消费侧取数**唯一入口**;须先成功 `poll()` |
| `adc_input_desc(ch)` | 语义 → 物理通道描述(来源 + 下标 / 通道号),供自检注入与 UI 通道序 | 返回静态地址,**勿改** |
| `adc_input_stm32_rc()` / `_seq()` / `_errs()` / `_gaps()` / `_hw_errs()` | 最近一拍状态码 / DMA 整帧完成计数 / 停摆(连续 ≥2 拍无新帧)/ 丢帧**点数**(Σ 每次前进的超出部分,与 `tgap` 同单位)/ HAL 错误回调 | **四类成因不同,勿混读**:对应心跳字段 `adc1_rc` / `adc1_seq` / `adc1_err` / `adc1_gap` / `adc1_hw`(判读见 `docs/子系统设计/ADC采样节拍设计说明.md` §8) |
| `adc_input_stm32_rc()` / `_seq()` / `_errs()` / `_gaps()` / `_hw_errs()` | 最近一拍状态码 / DMA 整帧完成计数 / 停摆(连续 ≥2 拍无新帧)/ 丢帧**点数**(Σ 每次前进的超出部分,与 `tgap` 同单位)/ HAL 错误回调 | **四类成因不同,勿混读**:对应心跳字段 `adc1_rc` / `adc1_seq` / `adc1_err` / `adc1_gap` / `adc1_hw`(判读见 `docs/子系统设计/ADC采样节拍设计.md` §8) |
**要点**
@@ -107,7 +107,7 @@ flowchart TD
**V0.2 改版只改这张表的内容**,本层 API 与上层语义不动。
- **STM32 侧是顺序扫描**(Rank1..8 = CH0..CH7、Rank9 = CH14,与 `MX_ADC1_Init()` 严格一致),
9 个通道**不是同一时刻**(一轮 ≈67 µs);由 TIM3 TRGO 硬件触发 + DMA 自动搬运(
`docs/子系统设计/ADC采样节拍设计说明.md` §4)。STM32 侧数据**固定滞后 1 拍(1 ms)**——与 TPAFE
`docs/子系统设计/ADC采样节拍设计.md` §4)。STM32 侧数据**固定滞后 1 拍(1 ms)**——与 TPAFE
侧本来就没有同刻性(双转换器无共同采样触发),无新增误差。
- **开机映射自检**挡住"映射到一个从不扫描的通道"这类静默错(启动日志 `ADC input layer init: OK`)。
- 本文件是本层**唯一**允许出现 HAL 调用的地方;新增模块若不需要外设,请保持 `sample_ring` /
@@ -501,7 +501,7 @@ UA 无幅值落 IA → 0.0020 Hz。
| `prot_set.c` `s_val[2][40]` + 封存 / `uint64_t` 位图 / 闭锁 | 336 B | 项数 32 → 40(测频与遥测快照)后的量 |
| `prot_elm.c` `s_rt[9]`(16 B / 实例) | 144 B | 实例 7 → 9(测频与遥测快照)后的量 |
| `adc_input.c` `s_stm32_raw[16]` + `s_tpafe_raw[8]` | 48 B | — |
| `adc_input.c` `s_stm32_dma[2×9]`(`uint16_t`) | 36 B | 必须在 `RW_IRAM1`(DMA2 不可达 CCM)→ `docs/子系统设计/ADC采样节拍设计说明.md` §4.3 |
| `adc_input.c` `s_stm32_dma[2×9]`(`uint16_t`) | 36 B | 必须在 `RW_IRAM1`(DMA2 不可达 CCM)→ `docs/子系统设计/ADC采样节拍设计.md` §4.3 |
| `adc_health.c` 内部状态 | ≈ 40 B | — |
| `adc_cal.c` 三个观测计数(`volatile uint32_t`) | **12 B** | 只增不清零、不参与判定;定义与读法见 §10.1 |
| 运行期栈 `phasor_run()` 的 `win[20]` | 80 B / 帧 | 抬高 `PHASOR_N` 直接抬栈 |
@@ -892,7 +892,7 @@ typedef struct {
- 「采样有效性校验」判定层深设计(判据推导 / 盲区 / 验证分工):`docs/算法/采样有效性校验设计说明.md`
- 分层与功能项清单(语义取数与有效性~动作留痕的属主登记处):`docs/算法/继电保护功能复现规划.md`
- 通道映射权威口径:`docs/硬件平台/ADC通道映射说明.md`
- 采样节拍与 DMA 搬运:`docs/子系统设计/ADC采样节拍设计说明.md`
- 采样节拍与 DMA 搬运:`docs/子系统设计/ADC采样节拍设计.md`
- 傅氏算法与有效值口径:`docs/算法/傅氏算法详解.md` §2.3
- 主机单测细则:`docs/测试说明/单元测试/相量层单元测试说明.md`、`docs/测试说明/单元测试/ADC输入层单元测试说明.md`
- RAM 与栈台账:`docs/内存管理/RAM使用分析与优化方案.md` §2.3
+3 -3
View File
@@ -7,7 +7,7 @@ ADC1 采样链路(TIM3 1 kHz 节拍 → ADC1 九通道扫描 → DMA2_Stream4
> 编号口径:本域既有条目按 `TRAP-ADC-<nn>` 自 01 起连续编号,**只读引用**;2026-10-29 起**不再新增 TRAP 号**(`docs/文档规范.md` §7.1 一律停发),新踩坑在本文写语义行名。编号不复用、不重排。
> 状态图例:✅ 已修复定案 · ⏳ 待修。
> 设计侧口径(采样节拍、寄存器配置、铁律)不在本文,见
> `docs/子系统设计/ADC采样节拍设计说明.md`;本文只记「踩过的坑」与「怎么判断踩到了」。
> `docs/子系统设计/ADC采样节拍设计.md`;本文只记「踩过的坑」与「怎么判断踩到了」。
## 索引
@@ -92,7 +92,7 @@ DMA 从序列中途开始搬运时,整个缓冲一起旋转,拷贝照样"恒
⚠ **顺序不可调换**:`adc1_disable()` 必须严格早于 `adc_input_init()`。若 ADC 在挂 DMA 时
仍处于使能态,旋转会重新出现。该顺序已写进
`docs/子系统设计/ADC采样节拍设计说明.md` 的设计铁律第 8 条,并由 CubeMX 存活审计项
`docs/子系统设计/ADC采样节拍设计.md` 的设计铁律第 8 条,并由 CubeMX 存活审计项
`adc1-calibration-impl`(整段守护 `USER CODE BEGIN 1`)的基线漂移兜底。
**回归判据**:
@@ -139,7 +139,7 @@ DMA 从序列中途开始搬运时,整个缓冲一起旋转,拷贝照样"恒
## 相关文档
- 设计侧口径、寄存器配置、9 行证据表的原始记录与复现方法:
`docs/子系统设计/ADC采样节拍设计说明.md` §4.5(设计铁律第 8 条)。
`docs/子系统设计/ADC采样节拍设计.md` §4.5(设计铁律第 8 条)。
- 通道映射与引脚对应:`docs/硬件平台/ADC通道映射说明.md`。
- 前端拓扑(③ 段变比 = 1:1,非分压):`docs/硬件平台/V0.0_网表详细分析报告.md` §4.7。
- 零点/线性/映射的现场验证步骤与判据:`docs/测试说明/专项验证/ADC前端注入验证流程.md`。