W1 功能主线完成:内存台账三件套(W1-0) + IEC104 上行(B4) + protectTask 板端接线(B5)
W1-0 台账三件套:scripts/ram_baseline.py(四模式)+ ram_baseline.tsv 趋势 + docs/内存管理/RAM预算台账.md + pre-commit mem-budget 钩子(RW_IRAM1/RW_CCM >92% 即 FAIL + Top-10 附表);审查报告 §5.5 三条闭环。 B4 IEC 60870-5-104 上行(被控站,TCP 2404):App/protocol/iec104/ 三模块 (asdu 编解码 / pts 点表+数据源 vtable / link APCI+FSM,纯逻辑零 I/O)+ App/task/iec104_serv.c(iec104Task,NET_READY 屏障 100ms 分片喂位)。 82 断言 L0.5 全绿 + iec104-unit-test 钩子(第 31 钩子); 设计说明 docs/子系统设计/IEC104上行设计说明.md 全量回填。 B5 protectTask(L3 板端接线):App/task/protect_task.c——20ms 判据节拍 prot_elm_run + L5-4 采样故障闭锁(adc_health 电平位→元件复位)+ L5-1 定值 校验和巡检;新增 prot_elm_request_reset() 收口跨上下文复归(104 YK11 落请求、 eval 头部消费);定值/元件初始化移入 app_main_init(内核前)。TC-PE-010 (prot_elm 28 用例)。 门禁与内存:TASK_WD_PROTECT=8 入 required_mask(产品 0x77→0x1F7、HW 层 0x1F5,看门狗单测 39 断言同步);FreeRTOS 堆 48→56KB(.ioc L1 参数 + FreeRTOSConfig.h 同步,RAM预算台账 §6 登记配额 48→56KB)。 ZI 144,008→152,204(+8,196 B = 堆 +8KB,B5 扩容);Code=204,348。 stack_report:protectTask 512 words 静态链 872 B/余 57%、iec104Task 640 words 1456 B/余 43% 均 PASS;RW_IRAM1 73.1% / RW_CCM 87.9%(<92%)。 验证:@build 0 错 0 警;ci_local 全绿(31 钩子 + 13 主机侧单测); doc-links 135 文档 PASS。登记簿 B4/B5 销项、W1 波次完成;保护规划/看门狗 说明/AGENTS(9 任务)/RAM 三文档全部同步。 遗留(验收窗口,非代码空洞):台架第三方 104 主站往返(送检窗口)、 标准源注入动作判据、@test PROT 锚点 + protect 栈水位 + 新堆 min_ever 复测。
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
"stack-overflow-hook-impl": "a006edbd30d2c4d7",
|
||||
"malloc-failed-hook-impl": "fcd7250e5cd6403b",
|
||||
"stack-overflow-hook-helper": "cacd9fd028be9b5b",
|
||||
"watchdog-module": "3d908285b092c402",
|
||||
"watchdog-module": "663f05caf22cf7d0",
|
||||
"reset-cause-latch": "52fc71acbb0f1aed",
|
||||
"iwdg-hal-driver": "4b902875a84554e9",
|
||||
"hal-iwdg-enabled": "1a8ed6cff34f7ba6",
|
||||
|
||||
@@ -378,6 +378,19 @@ repos:
|
||||
files: ^(Drivers/BSP/NET/lftpd/lftpd_auth\.(c|h)|App/util/sha256\.(c|h)|test/host/ftp_auth/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7m) IEC 60870-5-104 被控站协议栈主机侧单元测试(2026-10-12 加,B4)
|
||||
# APCI 重组/STARTDT/总召分片/COV/k-w-t1~t3/遥控定值两段式/C_CS/错误
|
||||
# 应答——link 层 tick 注入 + pts 数据源 vtable 抽象就是为 L0.5 全离线
|
||||
# 穷举设计(A-4 约束 1);板端复现这些分支要真主站+真等待。
|
||||
# 触发面含数据源依赖(prot_set/prot_elm/phasor/sample_ring)——测试
|
||||
# 直接 #include 这些生产源,签名变了要立刻在这里暴露。
|
||||
- id: iec104-unit-test
|
||||
name: IEC104 协议栈单测(主机侧)
|
||||
entry: python test/host/iec104/run_unit_test.py
|
||||
language: system
|
||||
files: ^(App/protocol/iec104/iec104_(asdu|pts|link)\.(c|h)|App/algo/(prot_set|prot_elm|phasor|sample_ring|adc_input)\.(c|h)|test/host/iec104/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 8) 测试套件开关门禁(2026-10-01 加)
|
||||
# 背景:test_config.h 的 TEST_SUITE_* 决定固件里编入哪个测试套件,
|
||||
# 全部注释 = 产品构建。但此前**无任何门禁检查**——实测已提交的 HEAD 里
|
||||
@@ -409,3 +422,17 @@ repos:
|
||||
language: system
|
||||
files: (\.c$|\.uvprojx$|CMakeLists\.txt$|^scripts/check_registration\.py$)
|
||||
pass_filenames: false
|
||||
# 10) RAM 预算 92% 阈值门禁(2026-10-12 加,登记簿 N-27 前半 / 审查报告 §5.5 三件套)
|
||||
# 背景:报告 §5.5 第 1 条要求"超阈值即失败 + Top-10 附表"——此前 mem-numbers 只防
|
||||
# "文档数字漂移",**没有任何机械检查拦"占用超阈值"**;台账三件套(ram_baseline.py +
|
||||
# ram_baseline.tsv + docs/内存管理/RAM预算台账.md)落地后,本钩子把 92% 红线机械化。
|
||||
# 逻辑:RW_IRAM1/RW_CCM 任一 >92% → FAIL + Top-10 RW+ZI 对象表;台账受管区漂移 → FAIL
|
||||
# (提示 --write);无 .map → 自适应跳过(同 mem-numbers 在 CI 的行为,盲区登记 A-5 残余)。
|
||||
# ⚠ pre-commit 4.x 无 `always: true` → files 覆盖所有能改变 RAM 的触发面:
|
||||
# 全部自研代码目录 + lv_conf.h + 测试板配置 + 脚本/tsv/台账自身 + 本配置文件。
|
||||
- id: mem-budget
|
||||
name: RAM 预算 92% 阈值门禁 (N-27)
|
||||
entry: python scripts/ram_baseline.py --gate
|
||||
language: system
|
||||
files: ^(App/|Drivers/BSP/|Src/|Inc/|Lib/lv_conf\.h|test/board/|scripts/ram_baseline\.|docs/内存管理/|\.pre-commit-config\.yaml$)
|
||||
pass_filenames: false
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
|
||||
## 项目概况
|
||||
|
||||
**现状一句话**:平台能力(网络 / 存储 / 时间 / UI / 日志 / 看门狗 / FTP / 配置)**已实现并台架验收**;**ADC / RS-485 这两个 DTU 核心采集职责尚未接线到业务消费者**——当前最大功能缺口,接线节奏受 E-59 门禁约束(见「平台约束」)。
|
||||
**现状一句话**:平台能力(网络 / 存储 / 时间 / UI / 日志 / 看门狗 / FTP / 配置)**已实现并台架验收**;采集主线已闭环到业务消费者(L1→L2 相量→L3 保护判据 protectTask→104 遥信,B5);**RS-485 采集尚未接线**(B2 按用户决策冻结,见登记簿 §2.4)——当前最大功能缺口,接线节奏受 E-59 门禁约束(见「平台约束」)。
|
||||
|
||||
**数据主线**(能力视角;分层图与跨层依赖表 → `docs/架构设计/项目架构总览.md`):
|
||||
|
||||
- **采集**:TPAFE5160(计量级 8 路,并行总线)+ 片内 ADC1(9 路,DMA)→ TIM3 1 kHz 节拍 + 原子快照 → adcTask → L1 有效性校验 → L2 相量 → L3 保护(软件侧已落地,待接任务)
|
||||
- **采集**:TPAFE5160(计量级 8 路,并行总线)+ 片内 ADC1(9 路,DMA)→ TIM3 1 kHz 节拍 + 原子快照 → adcTask → L1 有效性校验 → L2 相量 → L3 保护(protectTask 20 ms 判据节拍,B5)→ 104 遥信上送
|
||||
- **存储**:GD5F SPI NAND → dhara FTL → FatFs → `/log` 运行与自检记录、`/config.ini` 配置、FTP 根目录
|
||||
- **网络**:CH395F → netTask 串行 socket 栈 → FTP(21) / 对时(8888) / RemoDispBus 远程显示(7003)
|
||||
- **网络**:CH395F → netTask 串行 socket 栈 → FTP(21) / 对时(8888) / RemoDispBus 远程显示(7003) / IEC 60870-5-104 上行(2404)
|
||||
- **人机**:LVGL 160×160 单色菜单(uiTask),**与 PC 模拟器同一份 UI 源码**,远端可看图 + 注键
|
||||
|
||||
**目录地图**(只列顶层——稳定边界;细结构自己 `ls`,别背路径):
|
||||
@@ -39,14 +39,14 @@
|
||||
|
||||
## 架构要点
|
||||
|
||||
`Src/main.c`(CubeMX 初始化)→ `app_main_init` → `osKernelStart` → 7 任务(`osKernelStart` 之前全是内核启动前代码:禁调任何内核 API,见「平台约束」)
|
||||
`Src/main.c`(CubeMX 初始化)→ `app_main_init` → `osKernelStart` → 9 任务(`osKernelStart` 之前全是内核启动前代码:禁调任何内核 API,见「平台约束」)
|
||||
|
||||
- `app_main_init`(时间子系统优先,使后续日志带时间戳):`sd2506_init` → `sys_clock_init` → `rs485_init` → `gd5f2gq5ue_init` → `tpafe5160_init` → `adc_input_init`(L1 采集层)→ SD2506 诊断 → **启动 TIM3 1 kHz 采样节拍**
|
||||
- **就绪屏障**(`App/app/sys_events.h` Event Flags,取代魔法延时):`SYS_EVT_NET_READY` / `STORAGE_READY` / `FTP_READY` / `TIMESYNC_READY`;消费者用 `osEventFlagsWait` 阻塞等待(就绪位 `osFlagsNoClear`)
|
||||
- netTask:`net_stack_bring_up`(配置已由 defaultTask 在屏障前加载,本任务只读 RAM 副本)→ 置 NET_READY → 循环 `net_poll` + `net_process_messages`;消费 `SYS_EVT_NET_RECONFIG` → 按网络代次号重建监听(新 IP 免重启生效)
|
||||
- defaultTask:`f_mount` forced(无卷自动 `f_mkfs`;挂载后卷健康探针,FAT 区陈旧时自愈重格式化)→ 加载网络配置 → STORAGE_READY → LED 闪烁;同时是 syslog(`/log/run.log`·`selftest.log`,定长 64B 记录)的**唯一写者**与**配置引擎属主**(netTask/uiTask 不得直接触碰配置引擎)
|
||||
- adcTask:**纯消费者**——每拍 `adc_input_poll()`(TPAFE 原子快照 + 消费一帧 STM32 ADC1 DMA 双缓冲),只取数不做保护运算;**节拍与唤醒同源**(TIM3 ISR 内触发 CONVST + 同一更新事件经 TRGO 触发 ADC1,并调 `adc_tick_irq_notify()` 置位,任务 `osThreadFlagsWait` 等唤醒)。⚠ ISR 侧**只能**走 `adc_tick_irq_notify()`(内含"消费者就绪"门,TIM3 比调度器早 ≈100 ms 启动);手写于 `Src/freertos.c` 的 `USER CODE` 区,纳入 IWDG `required_mask`(`TASK_WD_ADC`)
|
||||
- **采集与算法层(L1/L2/L3)落在 `App/algo/`**:`adc_input`/`adc_health` = L1,`sample_ring`/`phasor` = L2,`prot_set`/`prot_elm` = L3 软件侧(**尚无任务调用 `prot_elm_run()`**——链接器移除、零足迹,板端接线随 protectTask 立项走 E-59 同款门禁)。→ `App/algo/README.md`
|
||||
- **采集与算法层(L1/L2/L3)落在 `App/algo/`**:`adc_input`/`adc_health` = L1,`sample_ring`/`phasor` = L2,`prot_set`/`prot_elm` = L3。板端消费者 = **protectTask**(B5,2026-10-12):20 ms 节拍 `prot_elm_run()` + L5-4 采样故障闭锁 + L5-1 定值校验和巡检;跨上下文复归(104 YK11)经 `prot_elm_request_reset()` 落请求。纳入 IWDG `required_mask`(`TASK_WD_PROTECT`,产品 `0x1F7`)。→ `App/algo/README.md`
|
||||
- 全景 → `docs/架构设计/项目架构总览.md`;启动时序与内核前模式 → `docs/架构设计/项目启动顺序管理.md`
|
||||
|
||||
<!-- TASK_TABLE:BEGIN generated by scripts/gen_task_table.py -->
|
||||
@@ -59,6 +59,8 @@
|
||||
| uiTask | 4096 | BelowNormal |
|
||||
| timeSyncTask | 512 | Normal |
|
||||
| adcTask | 640 | AboveNormal |
|
||||
| iec104Task | 640 | Normal |
|
||||
| protectTask | 512 | Normal |
|
||||
<!-- TASK_TABLE:END -->
|
||||
|
||||
(实际值以 `Src/freertos.c` 为准,本表由 `scripts/gen_task_table.py` 生成、`task-table` 钩子校验;testTask 仅在启用测试套件时创建。FreeRTOS 堆已迁 CCM,任务栈不占 SRAM;**每次抬栈的实测最坏链与余量台账** → `docs/内存管理/RAM使用分析与优化方案.md` §1.4 与 `docs/内存管理/内存内容分配策略.md` §6)
|
||||
@@ -87,7 +89,7 @@
|
||||
|
||||
## 提交门禁(pre-commit)
|
||||
|
||||
- 任一失败即拦截,共 **29 个钩子**(逐个详解 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 12 个(`-unit-test` 后缀)+ `test-suite-switch` + `registration-check`(双端注册反向校验,N-23)
|
||||
- 任一失败即拦截,共 **31 个钩子**(逐个详解 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 13 个(`-unit-test` 后缀)+ `test-suite-switch` + `registration-check`(双端注册反向校验,N-23)+ `mem-budget`(RAM 92% 预算门禁,N-27)
|
||||
- **范围 = 项目代码**:第三方(`Lib/`、`Middlewares/`、CMSIS、HAL、`lftpd`、`dhara`、`MDK-ARM`)排除;`cppcheck`/`coding-standard` 另排除 `test/`,规范检查再排除 CubeMX 生成的 `Src/`/`Inc/`。**`simulator/` 已纳入全部钩子**(2026-10-11)——生成物只靠 `.gitignore` 挡,**不在 `exclude` 里写目录名**(教训见 `docs/代码规范/pre-commit使用说明.md` §5.5)
|
||||
- ⚠ **pre-commit 4.x 已移除 `always: true`**(实测对 `local` 钩子只告警 `Unexpected key(s)` 并跳过)→ 想"总是跑"的钩子**必须让 `files:` 覆盖到所有会触发它的文件**,没有兜底开关
|
||||
- **改写型钩子(`trailing-whitespace` + `end-of-file-fixer`)豁免 CubeMX/Keil 生成文件**(配置里 YAML 锚点 `&cubemx-generated`);⚠ **只读检查不受影响**:`cppcheck` / 乱码 / `cubemx-survival` / `task-table` / `mem-numbers` 仍覆盖 `Src/`、`Inc/`
|
||||
@@ -108,7 +110,7 @@
|
||||
- **`osKernelStart` 之前禁止任何 FreeRTOS/CMSIS-RTOS 创建类 API**(`osMessageQueueNew` 等会杀死启动,2026-09-20 实证根因)→ 内核前标准模式(纯寄存器/HAL 轮询 + **无锁静态缓冲** + 内核启动后**惰性创建**真队列)见 `docs/架构设计/项目启动顺序管理.md`
|
||||
- **就绪屏障位必须 `osFlagsNoClear`**:`osEventFlagsWait` **默认消费**所等位,就绪位被先消费掉 → `ftpTask` 的 `WaitAll` 永不成立 → **FTP 21 端口静默不监听**(无日志无报错)。命令位(`SYS_EVT_CFG_SAVE`/`NET_RECONFIG`)保持消费型
|
||||
- **自研代码禁用 C 库堆分配**(`malloc/calloc/realloc/strdup/free`):MicroLIB 的 C 堆由 `startup_stm32f407xx.s` 的 `Heap_Size EQU 0x200` 限定为 **512 B**,KB 级分配必然返回 NULL,而**模拟器(宿主 CRT 堆)完全正常、照不出来** → 只用 `pvPortMalloc/vPortFree`,或把分配收进平台后端 → `docs/内存管理/RAM使用分析与优化方案.md`
|
||||
- **内存预算紧张**:新增任何任务/缓冲/栈前先核算 RAM 余量;改栈或加大局部变量**前后各跑一次** `scripts/stack_report.py`(静态最坏链 + 串口 `[STACK]` 水位)与 `scripts/ram_report.py`(.map 静态占用)→ `docs/内存管理/`
|
||||
- **内存预算紧张**:新增任何任务/缓冲/栈前先核算 RAM 余量;改栈或加大局部变量**前后各跑一次** `scripts/stack_report.py`(静态最坏链 + 串口 `[STACK]` 水位)与 `scripts/ram_report.py`(.map 静态占用);**总量红线已机械化**:`mem-budget` 钩子拦 RW_IRAM1/RW_CCM >92%,子系统配额 / CCM 台账 / 趋势见 `docs/内存管理/RAM预算台账.md`(**每次 `@build` 后跑 `scripts/ram_baseline.py --update`**)→ `docs/内存管理/`
|
||||
- FreeRTOS V10.3.1(CMSIS-RTOS V2);HAL 时基 = **TIM7**(非 SysTick),tick = 1ms;NVIC 4 位优先级分组,**外设中断优先级 ≥ 5**
|
||||
|
||||
**采集链(ADC / RS-485)**
|
||||
@@ -170,7 +172,7 @@
|
||||
| ADC 采样节拍 / 通道映射 / V0.1 硬件缺陷 | `docs/子系统设计/ADC采样节拍设计说明.md`、`docs/硬件平台/ADC通道映射说明.md` |
|
||||
| 存储栈(FatFs / dhara / lftpd / 两条 FatFs 陷阱) | `docs/架构设计/STM32F4-Base存储架构说明.md` |
|
||||
| RAM / 栈预算 / 容量台账 | `docs/内存管理/RAM使用分析与优化方案.md`、`内存内容分配策略.md` |
|
||||
| pre-commit 29 个钩子逐个详解 | `docs/代码规范/pre-commit使用说明.md` |
|
||||
| pre-commit 31 个钩子逐个详解 | `docs/代码规范/pre-commit使用说明.md` |
|
||||
| 代码规范 V1.0 / cppcheck 口径 | `docs/代码规范/嵌入式C语言代码规范_V1.0.md`、`cppcheck使用说明.md` |
|
||||
| 自动化测试 / 实机测试 / 板级套件 | `docs/测试说明/自动化测试流程.md`、`实机测试实施指南.md`、`board-test-suite` skill |
|
||||
| 烧录与复位语义 | `docs/测试说明/ST-LINK_CLI_复位使用说明.md` |
|
||||
|
||||
+4
-2
@@ -196,8 +196,10 @@ flowchart TD
|
||||
- **闭锁联动**:`prot_set_locked()`(L5-1)或投退=0 → 该元件状态整体复位、不判据。
|
||||
- **V0.1 实例**(N-18):`OC1/OC2/OC3/OL`(IA)、`OV/UV`(UA)、`3U0`(UN 外部通道)。
|
||||
零序过流(IN 走 STM32)与过频/低频(待频率算法)**故意不在表中**。
|
||||
- **尚未接线**:无任务调用 `prot_elm_run()`(链接器整对象移除,零足迹);
|
||||
板端接线随 protectTask 立项(栈预算 + IWDG 心跳门禁,AGENTS E-59 同款流程)。
|
||||
- **板端接线已落地(B5,2026-10-12)**:`App/task/protect_task.c`(protectTask,
|
||||
20 ms 节拍)消费 `prot_elm_run()` + L5-4 采样故障闭锁 + L5-1 校验和巡检;
|
||||
跨上下文复归走 `prot_elm_request_reset()`(104 YK11 落请求、eval 头部消费)。
|
||||
判据结果经 104 遥信上送(`iec104_pts` YX15~23/31)。→ `docs/算法/继电保护功能复现规划.md` §7
|
||||
|
||||
## 4. 关键约定
|
||||
|
||||
|
||||
@@ -43,6 +43,9 @@ typedef struct {
|
||||
|
||||
static pe_rt_t s_rt[PE_COUNT];
|
||||
|
||||
/* 跨上下文复归请求(单字标志,volatile;B5:104 YK11 在 iec104Task 落请求) */
|
||||
static volatile uint8_t s_reset_req;
|
||||
|
||||
/*
|
||||
* 函数功能:按名前缀在 phasor 实例表里找索引(开机解析一次)
|
||||
* 入口参数:prefix - 相量实例名前缀(如 "C01")
|
||||
@@ -90,6 +93,16 @@ void prot_elm_init(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:登记"复归元件状态"请求(任意上下文安全,见 prot_elm.h)
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 函数说明:只置单字标志;真正的状态复位在下一拍 eval 头部(消费者上下文)执行。
|
||||
*/
|
||||
void prot_elm_request_reset(void) {
|
||||
s_reset_req = 1u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:纯判据步进——按当前定值 + 给定测量值推进所有元件
|
||||
* 入口参数:now_ms - 单调时基(tick ms,差值法回绕安全)
|
||||
@@ -103,6 +116,13 @@ void prot_elm_eval(uint32_t now_ms, const float meas[PE_COUNT]) {
|
||||
uint8_t locked = prot_set_locked();
|
||||
uint32_t i;
|
||||
|
||||
/* 跨上下文复归请求在此消费(YK11 经 request_reset,见 prot_elm.h) */
|
||||
if (s_reset_req != 0u) {
|
||||
s_reset_req = 0u;
|
||||
prot_elm_init();
|
||||
locked = prot_set_locked();
|
||||
}
|
||||
|
||||
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
|
||||
const pe_cfg_t *cfg = &k_pe[i];
|
||||
uint8_t cond;
|
||||
|
||||
+13
-2
@@ -18,8 +18,10 @@
|
||||
* - 复压/方向/无压无流闭锁(dwSetFyCtrl 类)随 P2/P3 元件扩展;
|
||||
* - 工程量↔码值换算系数暂为 1(L1-5 标定未实现,同 phasor 层口径)。
|
||||
*
|
||||
* 线程约定:init 内核启动前;eval/run 仅保护消费者任务上下文(P1 阶段 = 单测;
|
||||
* 板端接线随 protectTask 立项,走栈预算 + IWDG 心跳门禁)。
|
||||
* 线程约定:init 内核启动前或保护消费者任务上下文;eval/run 仅保护消费者任务
|
||||
* 上下文(B5 起 = protectTask);request_reset 任意上下文(跨上下文复归请求,
|
||||
* 如 104 遥控 YK11 在 iec104Task 落请求、下一拍 eval 头部消费)。板端接线
|
||||
* 走栈预算 + IWDG 心跳门禁。
|
||||
*
|
||||
* 创建日期:2026-10-11
|
||||
*/
|
||||
@@ -50,9 +52,18 @@ typedef struct {
|
||||
|
||||
/*
|
||||
* 函数功能:复位全部元件状态(定值不动;幂等,内核启动前可调)
|
||||
* 限定条件:内核启动前,或保护消费者任务上下文(跨上下文请用 request_reset)
|
||||
*/
|
||||
void prot_elm_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:登记"复归元件状态"请求(任意上下文安全,单字标志)
|
||||
* 函数说明:下一拍 prot_elm_eval() 头部消费并执行 prot_elm_init()。
|
||||
* 104 遥控 YK11(iec104Task 上下文)必须走本接口,不得直接调
|
||||
* prot_elm_init()(会与 protectTask 的 eval 竞态)。
|
||||
*/
|
||||
void prot_elm_request_reset(void);
|
||||
|
||||
/*
|
||||
* 函数功能:纯判据步进——按当前定值(prot_set RAM)+ 给定测量值推进所有元件
|
||||
* 入口参数:now_ms - 单调时基(tick ms,回绕安全:差值法)
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
#include "tpafe5160.h"
|
||||
#include "adc_input.h" /* L1 采集层:adc_input_init()(语义映射 + STM32 ADC1 扫描) */
|
||||
#include "phasor.h" /* L2 相量层:phasor_init()(系数表 + 实例 + 双区交换) */
|
||||
#include "prot_set.h" /* L3 定值模型:prot_set_init()(B5,内核前封存) */
|
||||
#include "prot_elm.h" /* L3 元件框架:prot_elm_init()(B5,解析相量索引) */
|
||||
#include "sys_log.h"
|
||||
#include "task_watchdog.h" /* task_wd_seg_report():round 8 复位不丢"进段打点"上报 */
|
||||
#include "test_config.h" /* 套件开关:身份戳的构建变体串(FW_BUILD_VARIANT)依赖本头,
|
||||
@@ -240,6 +242,14 @@ int app_main_init(void) {
|
||||
(void)phasor_init();
|
||||
DBG_INFO("phasor layer init: OK (inst=%u)", (unsigned)phasor_inst_count());
|
||||
|
||||
/* 4d) 保护定值 + 元件层初始化(B5):出厂默认定值封存校验和 + 元件状态
|
||||
* 复位并解析相量索引(须在 phasor_init 之后)。内核前完成 → iec104
|
||||
* 定值读/protectTask 首拍前定值必已装载。判据消费在 protectTask。 */
|
||||
prot_set_init();
|
||||
prot_elm_init();
|
||||
DBG_INFO("protection init: OK (set=%u elm=%u)",
|
||||
(unsigned)PS_ID_COUNT, (unsigned)PE_COUNT);
|
||||
|
||||
/* 5) SD2506 RTC 诊断信息 */
|
||||
{
|
||||
/* 读取 ID */
|
||||
|
||||
+11
-4
@@ -1,11 +1,18 @@
|
||||
# App/protocol/ — 通信协议(预留)
|
||||
# App/protocol/ — 通信协议(纯逻辑层)
|
||||
|
||||
> **占位目录**:规划用于存放自定义通信协议实现(解析 / 组帧 / 状态机)。
|
||||
> 当前为空(仅 `.gitkeep`),协议相关实现暂分散在:
|
||||
> - RS-485 侧:`App/task/rs485_task.c` + `App/util/crc.c` + `App/util/ringbuf.c`
|
||||
> 存放**跨接口复用的协议逻辑**(解析 / 组帧 / 状态机),零 I/O、依赖注入,
|
||||
> 可被任意任务/接口承载;板端 I/O 胶水在对应 task(如 `App/task/iec104_serv.c`)。
|
||||
> 其余协议实现暂分散在:
|
||||
> - RS-485 侧:`App/task/rs485_task.c`(CRC 在驱动内)
|
||||
> - 网络侧:`Drivers/BSP/NET/`
|
||||
> - 远端显示:`docs/UI设计/RemoDispBus协议与接收设计说明.md`
|
||||
|
||||
## 已落地协议
|
||||
|
||||
| 子目录 | 协议 | 说明 |
|
||||
|---|---|---|
|
||||
| `iec104/` | IEC 60870-5-104 被控站(TCP 2404 上行) | **纯逻辑**三件套(asdu/pts/link,零 I/O,数据源 vtable 注入)→ 全栈 L0.5 主机单测;板端胶水与点表决策 → `docs/子系统设计/IEC104上行设计说明.md` |
|
||||
|
||||
## 何时使用本目录
|
||||
|
||||
当出现**跨接口复用的协议逻辑**(如统一的帧编解码器、协议状态机)时,落到此处,
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 ASDU 编解码(应用层,纯逻辑)
|
||||
* 模块功能:实现 iec104_asdu.h——ASDU 头解析/组装、IOA 3B 小端、IEEE754
|
||||
* 短浮点小端、CP56Time2a ↔ epoch 毫秒(UTC 分解,Hinnant civil
|
||||
* 日历算法,不依赖 time.h → ARMCC/MSVC 双端可移植)。
|
||||
* 线程约定:全部纯函数,无状态 → 任意上下文可调。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#include "iec104_asdu.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* COT 首字节:bit0..5 = 值,bit6 = PN,bit7 = T;RBT 复用 bit4(C_IC 语义) */
|
||||
#define COT_VAL_MASK 0x3FU
|
||||
#define COT_FLAG_MASK 0xC0U
|
||||
|
||||
/*
|
||||
* 函数功能:解析 ASDU 头(6B),信息体起点经 obj/obj_len 指出
|
||||
* 入口参数:buf/len = ASDU 首字节与总长;hdr = 输出
|
||||
* 返 回 值:0 成功 / -1 空指针或长度不足 6B
|
||||
*/
|
||||
int iec104_asdu_parse_hdr(const uint8_t *buf, uint16_t len, iec104_asdu_hdr_t *hdr) {
|
||||
if ((buf == NULL) || (hdr == NULL) || (len < 6u)) {
|
||||
return -1;
|
||||
}
|
||||
hdr->type = buf[0];
|
||||
hdr->vsq = buf[1];
|
||||
hdr->cot = (uint8_t)(buf[2] & COT_VAL_MASK);
|
||||
hdr->cot_hi = (uint8_t)(buf[2] & 0xC0u);
|
||||
hdr->oa = buf[3];
|
||||
hdr->ca = (uint16_t)(buf[4] | ((uint16_t)buf[5] << 8));
|
||||
hdr->obj = &buf[6];
|
||||
hdr->obj_len = (uint16_t)(len - 6u);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:组装 ASDU 头(OA 恒 0,EVI 由调用方置 buf[3];RBT 位透传)
|
||||
* 入口参数:cot = 6bit 原因值;cot_hi = 标志位(T/PN/RBT);ca = 公共地址
|
||||
* 返 回 值:固定 6(头字节数)
|
||||
*/
|
||||
uint16_t iec104_asdu_put_hdr(uint8_t *buf, uint8_t type, uint8_t vsq,
|
||||
uint8_t cot, uint8_t cot_hi, uint16_t ca) {
|
||||
buf[0] = type;
|
||||
buf[1] = vsq;
|
||||
buf[2] = (uint8_t)(((uint8_t)(cot & COT_VAL_MASK))
|
||||
| (uint8_t)(cot_hi & COT_FLAG_MASK)
|
||||
| (uint8_t)(cot_hi & 0x10u)); /* RBT 位透传 */
|
||||
buf[3] = 0u; /* OA = 0(EVI 由调用方按需置位) */
|
||||
buf[4] = (uint8_t)(ca & 0xFFu);
|
||||
buf[5] = (uint8_t)(ca >> 8);
|
||||
return 6u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:IOA 3 字节小端写入
|
||||
*/
|
||||
void iec104_put_ioa(uint8_t *buf, uint32_t ioa) {
|
||||
buf[0] = (uint8_t)(ioa & 0xFFu);
|
||||
buf[1] = (uint8_t)((ioa >> 8) & 0xFFu);
|
||||
buf[2] = (uint8_t)((ioa >> 16) & 0xFFu);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:IOA 3 字节小端读取
|
||||
*/
|
||||
uint32_t iec104_get_ioa(const uint8_t *buf) {
|
||||
return (uint32_t)buf[0] | ((uint32_t)buf[1] << 8) | ((uint32_t)buf[2] << 16);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:IEEE754 短浮点 → 小端字节序(104 恒小端,宿主字节序无关)
|
||||
*/
|
||||
void iec104_put_float(uint8_t *buf, float v) {
|
||||
/* 位模式小端:memcpy 不假设宿主字节序,逐字节拆给目标(104 恒小端) */
|
||||
uint8_t tmp[sizeof(float)];
|
||||
uint32_t i;
|
||||
|
||||
(void)memcpy(tmp, &v, sizeof(tmp));
|
||||
for (i = 0u; i < sizeof(tmp); i++) {
|
||||
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
|
||||
buf[i] = tmp[sizeof(tmp) - 1u - i];
|
||||
#else
|
||||
buf[i] = tmp[i];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:小端字节序 → IEEE754 短浮点
|
||||
*/
|
||||
float iec104_get_float(const uint8_t *buf) {
|
||||
uint8_t tmp[sizeof(float)];
|
||||
uint32_t i;
|
||||
float v;
|
||||
|
||||
for (i = 0u; i < sizeof(tmp); i++) {
|
||||
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
|
||||
tmp[i] = buf[sizeof(tmp) - 1u - i];
|
||||
#else
|
||||
tmp[i] = buf[i];
|
||||
#endif
|
||||
}
|
||||
(void)memcpy(&v, tmp, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
|
||||
/* ---------------- CP56Time2a ↔ epoch ---------------- */
|
||||
|
||||
/* 2000-01-01 00:00:00 UTC 的 epoch 毫秒(钳位下限) */
|
||||
#define EPOCH_2000_MS 946684800000ULL
|
||||
|
||||
/*
|
||||
* 函数功能:公历(y>=1)→ 距 1970-01-01 天数(Hinnant civil_from_days 正向)
|
||||
* 说明:纯整数算法;m 取 1..12。
|
||||
*/
|
||||
static uint32_t s_days_from_civil(uint32_t y, uint32_t m, uint32_t d) {
|
||||
uint32_t yy = y - ((m <= 2u) ? 1u : 0u);
|
||||
uint32_t era = yy / 400u;
|
||||
uint32_t yoe = yy - (era * 400u);
|
||||
uint32_t mp = (m > 2u) ? (m - 3u) : (m + 9u); /* 3 月 = 月序 0 */
|
||||
uint32_t doy = ((153u * mp) + 2u) / 5u + d - 1u;
|
||||
uint32_t doe = (yoe * 365u) + (yoe / 4u) - (yoe / 100u) + doy;
|
||||
|
||||
return (era * 146097u) + doe - 719468u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:距 1970-01-01 天数 → 公历(Hinnant 反向)
|
||||
* 入口参数:z_in = 天数;y/m/d = 输出
|
||||
*/
|
||||
static void s_civil_from_days(uint32_t z_in, uint32_t *y, uint32_t *m, uint32_t *d) {
|
||||
uint32_t z = z_in + 719468u;
|
||||
uint32_t era = z / 146097u;
|
||||
uint32_t doe = z - (era * 146097u);
|
||||
uint32_t yoe = (doe - (doe / 1460u) + (doe / 36524u) - (doe / 146096u)) / 365u;
|
||||
uint32_t yy = yoe + (era * 400u);
|
||||
uint32_t doy = doe - ((365u * yoe) + (yoe / 4u) - (yoe / 100u));
|
||||
uint32_t mp = ((5u * doy) + 2u) / 153u;
|
||||
uint32_t mon = mp + ((mp < 10u) ? 3u : 0u);
|
||||
|
||||
*d = doy - (((153u * mp) + 2u) / 5u) + 1u;
|
||||
if (mp >= 10u) {
|
||||
mon = mp - 9u;
|
||||
}
|
||||
*m = mon;
|
||||
*y = yy + ((mon <= 2u) ? 1u : 0u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:CP56Time2a → epoch 毫秒(UTC 分解)
|
||||
* 入口参数:buf = 7B;epoch_ms = 输出
|
||||
* 返 回 值:0 成功 / -2 IV(时间无效,调用方应拒绝)/ -1 字段越界
|
||||
*/
|
||||
int iec104_cp56_to_epoch_ms(const uint8_t *buf, uint64_t *epoch_ms) {
|
||||
uint32_t ms_min, minute, hour, day, mon, year;
|
||||
|
||||
if ((buf == NULL) || (epoch_ms == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
if ((buf[2] & 0x80u) != 0u) {
|
||||
return -2; /* IV:时间无效,调用方应拒绝 */
|
||||
}
|
||||
ms_min = (uint32_t)buf[0] | ((uint32_t)buf[1] << 8); /* 0..59999 */
|
||||
minute = (uint32_t)buf[2] & 0x3Fu;
|
||||
hour = (uint32_t)buf[3] & 0x1Fu;
|
||||
day = (uint32_t)buf[4] & 0x1Fu;
|
||||
mon = (uint32_t)buf[5] & 0x0Fu;
|
||||
year = 2000u + ((uint32_t)buf[6] & 0x7Fu);
|
||||
if ((day == 0u) || (day > 31u) || (mon == 0u) || (mon > 12u)
|
||||
|| (hour > 23u) || (minute > 59u) || (ms_min > 59999u)) {
|
||||
return -1;
|
||||
}
|
||||
*epoch_ms = ((uint64_t)s_days_from_civil(year, mon, day) * 86400000ULL)
|
||||
+ ((uint64_t)hour * 3600000ULL)
|
||||
+ ((uint64_t)minute * 60000ULL)
|
||||
+ (uint64_t)ms_min;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:epoch 毫秒 → CP56Time2a(钳位 [2000-01-01, 2127-12-31],IV 恒 0)
|
||||
* 返 回 值:固定 7(字节数)
|
||||
*/
|
||||
uint16_t iec104_epoch_ms_to_cp56(uint64_t epoch_ms, uint8_t *buf) {
|
||||
uint64_t days, rem, clamped;
|
||||
uint32_t y, m, d, ms_min;
|
||||
|
||||
clamped = (epoch_ms < EPOCH_2000_MS) ? EPOCH_2000_MS : epoch_ms;
|
||||
days = clamped / 86400000ULL;
|
||||
rem = clamped % 86400000ULL;
|
||||
s_civil_from_days((uint32_t)days, &y, &m, &d);
|
||||
if (y > 2127u) { /* 年字段 7bit 上限(2000+127) */
|
||||
y = 2127u;
|
||||
m = 12u;
|
||||
d = 31u;
|
||||
rem = 86399999ULL;
|
||||
}
|
||||
ms_min = (uint32_t)(rem % 60000ULL);
|
||||
buf[0] = (uint8_t)(ms_min & 0xFFu);
|
||||
buf[1] = (uint8_t)(ms_min >> 8);
|
||||
buf[2] = (uint8_t)((rem / 60000ULL) % 60ULL); /* min,IV=0 */
|
||||
buf[3] = (uint8_t)((rem / 3600000ULL) % 24ULL); /* hour */
|
||||
buf[4] = (uint8_t)(d & 0x1Fu); /* day */
|
||||
buf[5] = (uint8_t)(m & 0x0Fu); /* month */
|
||||
buf[6] = (uint8_t)((y - 2000u) & 0x7Fu); /* year */
|
||||
return 7u;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 ASDU 编解码(应用层,纯逻辑)
|
||||
* 模块功能:104 规约应用服务数据单元(ASDU)的类型/传送原因常量、组帧与解析。
|
||||
* **零 I/O、零平台依赖**(仅 stdint.h)→ 主机侧 L0.5 可离线单测
|
||||
* (登记簿 A-4 约束 1"协议解析与硬件 I/O 分离")。
|
||||
* 口径来源:docs/算法/继电保护功能复现规划.md §1.5(B0 产品定义)——
|
||||
* 公共地址 2 字节、传送原因 2 字节、信息体地址 IOA **3 字节**(101 为 2 字节);
|
||||
* 遥测上送**短浮点** M_ME_NC_1(系数 = 1);遥信双点上双点、其余单点;
|
||||
* 遥控**双点命令**(选择 82H / 执行 02H,即 QDC 的 S 位 + 值 2)。
|
||||
* 帧结构(本层只管 ASDU,APCI 6 字节头归 iec104_link):
|
||||
* [type 1B][vsq 1B][cot 2B][ca 2B]( [ioa 3B][元素...] )*n
|
||||
* 多信息体地址对象(SQ=0)每对象带独立 IOA;顺序对象(SQ=1)单 IOA + n 元素。
|
||||
* 创建日期:2026-10-12(B4,登记簿 §2.1 W1)
|
||||
*/
|
||||
#ifndef IEC104_ASDU_H
|
||||
#define IEC104_ASDU_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* ---------------- 类型标识(DL/T 634.5104,仅定义本期子集) ---------------- */
|
||||
#define IEC104_T_M_SP_NA_1 1U /* 单点遥信 SIQ */
|
||||
#define IEC104_T_M_DP_NA_1 3U /* 双点遥信 DIQ */
|
||||
#define IEC104_T_M_ME_NC_1 13U /* 遥测短浮点 float+QDS(§1.5 口径) */
|
||||
#define IEC104_T_C_SE_NC_1 50U /* 定值写(短浮点)float+QDS */
|
||||
#define IEC104_T_C_DC_NA_1 52U /* 双点遥控 QDC(82H 选 / 02H 执行) */
|
||||
#define IEC104_T_C_IC_NA_1 100U /* 总召 RQT */
|
||||
#define IEC104_T_C_RD_NA_1 102U /* 读单个测量值 IOA only */
|
||||
#define IEC104_T_C_CS_NA_1 103U /* 时钟同步 CP56Time2a */
|
||||
#define IEC104_T_C_TS_NA_1 104U /* 试验 FBP 2B */
|
||||
#define IEC104_T_C_RP_NA_1 105U /* 进程复位 RPA 1B */
|
||||
|
||||
/* ---------------- 传送原因 COT(低字节 6bit;高字节 T|PN|RBT/…) ---------- */
|
||||
#define IEC104_COT_PERIODIC 1U /* 周期/循环 */
|
||||
#define IEC104_COT_BG_SCAN 2U /* 背景扫描(总召响应) */
|
||||
#define IEC104_COT_SPONT 3U /* 突发(cos) */
|
||||
#define IEC104_COT_INIT 4U /* 初始化(总召 + RBT=1 时用) */
|
||||
#define IEC104_COT_REQ 5U /* 请求(C_RD 响应) */
|
||||
#define IEC104_COT_ACT 6U /* 激活 */
|
||||
#define IEC104_COT_ACTCON 7U /* 激活确认 */
|
||||
#define IEC104_COT_ACTTERM 8U /* 激活终止 */
|
||||
#define IEC104_COT_DEACT 9U /* deactivate */
|
||||
#define IEC104_COT_DEACTCON 10U /* 停止激活确认 */
|
||||
#define IEC104_COT_UNK_COT 12U /* 未知传送原因 */
|
||||
#define IEC104_COT_UNK_CA 13U /* 未知公共地址 */
|
||||
#define IEC104_COT_UNK_IOA 14U /* 未知信息体地址(+EVI) */
|
||||
#define IEC104_COT_BIT_T 0x80U /* COT 高字节 bit7:试验 */
|
||||
#define IEC104_COT_BIT_PN 0x40U /* COT 高字节 bit6:否定确认 */
|
||||
#define IEC104_COT_BIT_RBT 0x10U /* COT 高字节 bit4:请求链路仿真(总召初始化) */
|
||||
|
||||
/* ---------------- VSQ ---------------- */
|
||||
#define IEC104_VSQ_SQ 0x80U /* 1 = 顺序地址(单 IOA + n 元素) */
|
||||
#define IEC104_VSQ_N(n) ((uint8_t)((n) & 0x7FU))
|
||||
|
||||
/* ---------------- 遥信/遥测品质描述位(本期只用溢出/异常/非当前) --------- */
|
||||
#define IEC104_QDS_OV 0x01U
|
||||
#define IEC104_QDS_BL 0x10U /* blocked(闭锁) */
|
||||
#define IEC104_QDS_NT 0x20U /* not top(暂态品质,遥测未就绪用) */
|
||||
#define IEC104_QDS_IV 0x80U /* invalid(无效,通道不可用用) */
|
||||
#define IEC104_DIQ_IV 0x80U
|
||||
#define IEC104_SIQ_IV 0x80U
|
||||
|
||||
/* ---------------- 双点值(DIQ/QDC 低 2bit) ---------------- */
|
||||
#define IEC104_DPC_DONTCARE 0U
|
||||
#define IEC104_DPC_OFF 1U
|
||||
#define IEC104_DPC_ON 2U
|
||||
#define IEC104_QDC_S 0x80U /* 1 = 选择,0 = 执行(§1.5:82H/02H) */
|
||||
|
||||
/* ---------------- 总召 RQT ---------------- */
|
||||
#define IEC104_RQT_GROUP1 20U /* 总查询(组 1) */
|
||||
|
||||
/* ---------------- ASDU 头(解析产物) ---------------- */
|
||||
typedef struct {
|
||||
uint8_t type; /* 类型标识 */
|
||||
uint8_t vsq; /* 可变结构限定词 */
|
||||
uint8_t cot; /* 传送原因低 6bit */
|
||||
uint8_t cot_hi; /* COT 首字节的高位标志(T=0x80 / PN=0x40 / RBT=0x10 原样保留) */
|
||||
uint8_t oa; /* 源发站地址(第 2 COT 字节;bit7 = EVI) */
|
||||
uint16_t ca; /* 公共地址(链路地址 = ASDU 公共地址,2B) */
|
||||
const uint8_t *obj; /* 信息体区首址(指向调用方缓冲,生命周期同 on_rx) */
|
||||
uint16_t obj_len; /* 信息体区长度 */
|
||||
} iec104_asdu_hdr_t;
|
||||
|
||||
/*
|
||||
* 函数功能:解析 ASDU 头(不解析信息体——各命令的体解析在 iec104_link 内做)
|
||||
* 入口参数:buf - ASDU 首址(不含 APCI);len - ASDU 长度
|
||||
* hdr - 输出头
|
||||
* 返 回 值:0 成功 / -1 参数或长度错(len < 6)
|
||||
* 函数说明:纯只读,不改动 buf。
|
||||
*/
|
||||
int iec104_asdu_parse_hdr(const uint8_t *buf, uint16_t len, iec104_asdu_hdr_t *hdr);
|
||||
|
||||
/*
|
||||
* 函数功能:写 ASDU 头
|
||||
* 入口参数:buf - 目标缓冲(容量须 ≥ 6);type/vsq/cot/cot_hi/ca 见常量
|
||||
* 返 回 值:固定 6(已写字节数)
|
||||
*/
|
||||
uint16_t iec104_asdu_put_hdr(uint8_t *buf, uint8_t type, uint8_t vsq,
|
||||
uint8_t cot, uint8_t cot_hi, uint16_t ca);
|
||||
|
||||
/* ---------------- 小工具(IOA 3B 小端 / IEEE 短浮点小端) ---------------- */
|
||||
|
||||
/* IOA(3 字节小端)写入 */
|
||||
void iec104_put_ioa(uint8_t *buf, uint32_t ioa);
|
||||
/* IOA(3 字节小端)读取 */
|
||||
uint32_t iec104_get_ioa(const uint8_t *buf);
|
||||
/* IEEE754 float 按 104 小端写入(memcpy 位模式,不做字节序假设) */
|
||||
void iec104_put_float(uint8_t *buf, float v);
|
||||
/* IEEE754 float 按 104 小端读取 */
|
||||
float iec104_get_float(const uint8_t *buf);
|
||||
|
||||
/* ---------------- CP56Time2a(C_CS 时钟同步用) ---------------- */
|
||||
|
||||
/*
|
||||
* 函数功能:解析 CP56Time2a(7 字节)为 epoch 毫秒
|
||||
* 入口参数:buf - 7B 首址;epoch_ms - 输出(UTC 毫秒)
|
||||
* 返 回 值:0 成功 / -1 参数错 / -2 IV 位置起(时间无效,调用方应拒绝)
|
||||
* 函数说明:年字段仅 7bit(0..127,基准 2000);月/日/时/分按 1.. 语义;
|
||||
* 不处理 SU(夏令时)位——忽略。
|
||||
*/
|
||||
int iec104_cp56_to_epoch_ms(const uint8_t *buf, uint64_t *epoch_ms);
|
||||
|
||||
/*
|
||||
* 函数功能:epoch 毫秒 → CP56Time2a(7 字节,UTC 分解)
|
||||
* 入口参数:epoch_ms - UTC 毫秒;buf - 目标 7B
|
||||
* 返 回 值:固定 7
|
||||
* 函数说明:epoch_ms < 2000-01-01 时钳到 2000(年字段下限);IV 清 0。
|
||||
*/
|
||||
uint16_t iec104_epoch_ms_to_cp56(uint64_t epoch_ms, uint8_t *buf);
|
||||
|
||||
#endif /* IEC104_ASDU_H */
|
||||
@@ -0,0 +1,777 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 链路层(实现)
|
||||
* 模块功能:实现 iec104_link.h——APCI 流重组与 U/S/I 编解码、STARTDT/STOPDT/
|
||||
* TESTFR、k/w 窗与 t1/t2/t3(差值法,tick 回绕安全)、总召分片、
|
||||
* COV 突发(k 窗满暂存不丢)、周期上送、遥控/定值两段式、C_RD、
|
||||
* C_CS 对时、错误应答(COT 12/13/14 + EVI)。
|
||||
* 失败语义:坏帧/序号错/N(R) 越窗/缓冲溢出 → 实例置 IDLE(链路故障),
|
||||
* 由胶水层检测 IDLE 跳变断开 socket;本层不感知 I/O。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#include "iec104_link.h"
|
||||
|
||||
#include "iec104_asdu.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* ---------------- APCI 常量 ---------------- */
|
||||
#define APCI_START 0x68u
|
||||
#define APCI_CTRL_LEN 4u /* len 字段最小值(纯控制域) */
|
||||
#define APCI_LEN_MIN 4u
|
||||
#define APCI_LEN_MAX 252u
|
||||
#define SEQ_MOD 32768u /* 序号 15bit */
|
||||
|
||||
/* U 帧控制字节(DL/T 634.5104 表:低 2 位恒 11) */
|
||||
#define U_STARTDT_ACT 0x07u
|
||||
#define U_STARTDT_CON 0x0Bu
|
||||
#define U_STOPDT_ACT 0x13u
|
||||
#define U_STOPDT_CON 0x23u
|
||||
#define U_TESTFR_ACT 0x43u
|
||||
#define U_TESTFR_CON 0x83u
|
||||
|
||||
/* 本实现单 ASDU 信息体区上限(背景 YC 组 8×5=40 B 最大) */
|
||||
#define BODY_MAX 64u
|
||||
/* 周期上送周期(COT=1) */
|
||||
#define PERIOD_MS 60000UL
|
||||
|
||||
/*
|
||||
* 函数功能:超时判定(now 到达/越过 deadline,tick 回绕安全)
|
||||
* 返 回 值:1 已到期 / 0 未到期
|
||||
*/
|
||||
static int s_expired(uint32_t now, uint32_t deadline) {
|
||||
return (((int32_t)(now - deadline)) >= 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ---------------- 发送原语 ---------------- */
|
||||
|
||||
/*
|
||||
* 函数功能:发 U 帧(STARTDT/STOPDT/TESTFR act|con)
|
||||
* 说明:t1 只约束"需确认的发送"(标准语义)——TESTFR act 置挂起并起 t1;
|
||||
* STARTDT/STOPDT con 不启动 t1(主站可合法静默,靠 t3 探活)。
|
||||
*/
|
||||
static void s_send_u(iec104_link_t *lk, uint8_t q) {
|
||||
uint8_t buf[6];
|
||||
|
||||
if (lk->cfg->send_apdu == NULL) {
|
||||
return;
|
||||
}
|
||||
buf[0] = APCI_START;
|
||||
buf[1] = APCI_CTRL_LEN;
|
||||
buf[2] = q;
|
||||
buf[3] = 0u;
|
||||
buf[4] = 0u;
|
||||
buf[5] = 0u;
|
||||
lk->cfg->send_apdu(lk->ctx, buf, 6u);
|
||||
/* t1 只约束"需确认的发送"(标准语义):TESTFR act 等 con;
|
||||
* STARTDT/STOPDT con 不启动 t1——主站可合法静默,靠 t3 探活 */
|
||||
if (q == U_TESTFR_ACT) {
|
||||
lk->testfr_pending = 1u;
|
||||
lk->t1_deadline = lk->now_ms + IEC104_T1_MS;
|
||||
}
|
||||
lk->t3_deadline = lk->now_ms + IEC104_T3_MS;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:发 S 帧(确认已收序号,rx_unacked 清零)
|
||||
* 说明:S 不需对端确认 → 不起 t1,只刷 t3。
|
||||
*/
|
||||
static void s_send_s(iec104_link_t *lk) {
|
||||
uint8_t buf[6];
|
||||
uint16_t r = (uint16_t)(lk->rx_seq << 1);
|
||||
|
||||
if (lk->cfg->send_apdu == NULL) {
|
||||
return;
|
||||
}
|
||||
buf[0] = APCI_START;
|
||||
buf[1] = APCI_CTRL_LEN;
|
||||
buf[2] = 0x01u;
|
||||
buf[3] = 0u;
|
||||
buf[4] = (uint8_t)(r & 0xFFu);
|
||||
buf[5] = (uint8_t)(r >> 8);
|
||||
lk->cfg->send_apdu(lk->ctx, buf, 6u);
|
||||
lk->rx_unacked = 0u;
|
||||
lk->t3_deadline = lk->now_ms + IEC104_T3_MS; /* S 不需确认 → 不起 t1 */
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:发一个 I 帧(asdu 由调用方组好,N(S)/N(R) 由本层填)
|
||||
* 返 回 值:0 已发出 / -1 k 窗满或超长(调用方保留数据下次重试)
|
||||
*/
|
||||
static int s_send_i(iec104_link_t *lk, const uint8_t *asdu, uint16_t len) {
|
||||
uint8_t buf[6u + BODY_MAX];
|
||||
uint16_t s16;
|
||||
uint16_t r16;
|
||||
|
||||
if ((lk->cfg->send_apdu == NULL) || (len > BODY_MAX)) {
|
||||
return -1;
|
||||
}
|
||||
if (lk->tx_unacked >= IEC104_K_MAX) {
|
||||
return -1;
|
||||
}
|
||||
buf[0] = APCI_START;
|
||||
buf[1] = (uint8_t)(APCI_CTRL_LEN + len);
|
||||
s16 = (uint16_t)(lk->tx_seq << 1);
|
||||
r16 = (uint16_t)(lk->rx_seq << 1);
|
||||
buf[2] = (uint8_t)(s16 & 0xFFu);
|
||||
buf[3] = (uint8_t)(s16 >> 8);
|
||||
buf[4] = (uint8_t)(r16 & 0xFFu);
|
||||
buf[5] = (uint8_t)(r16 >> 8);
|
||||
(void)memcpy(&buf[6], asdu, len);
|
||||
lk->tx_seq = (uint16_t)((lk->tx_seq + 1u) % SEQ_MOD);
|
||||
lk->tx_unacked++;
|
||||
lk->cfg->send_apdu(lk->ctx, buf, (uint16_t)(6u + len));
|
||||
lk->t1_deadline = lk->now_ms + IEC104_T1_MS;
|
||||
lk->t3_deadline = lk->now_ms + IEC104_T3_MS;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:组 ASDU(hdr [+ ioa] + body)并发 I 帧
|
||||
* 说明:evi=1 → OA bit7(EVI,未知 IOA/类型/地址类应答)
|
||||
* 返 回 值:同 s_send_i
|
||||
*/
|
||||
static int s_send_asdu(iec104_link_t *lk, uint8_t type, uint8_t vsq,
|
||||
uint8_t cot, uint8_t cot_hi,
|
||||
uint8_t with_ioa, uint32_t ioa,
|
||||
const uint8_t *body, uint16_t body_len, uint8_t evi) {
|
||||
uint8_t buf[6u + BODY_MAX];
|
||||
uint16_t n;
|
||||
|
||||
n = iec104_asdu_put_hdr(buf, type, vsq, cot, cot_hi, lk->cfg->ca);
|
||||
if (evi != 0u) {
|
||||
buf[3] |= 0x80u; /* OA bit7 = EVI */
|
||||
}
|
||||
if (with_ioa != 0u) {
|
||||
iec104_put_ioa(&buf[n], ioa);
|
||||
n += 3u;
|
||||
}
|
||||
if (body_len > 0u) {
|
||||
if ((uint32_t)n + (uint32_t)body_len > (uint32_t)sizeof(buf)) {
|
||||
return -1;
|
||||
}
|
||||
(void)memcpy(&buf[n], body, body_len);
|
||||
n += body_len;
|
||||
}
|
||||
return s_send_i(lk, buf, n);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:命令应答(actcon/deactcon 等)——原样回显信息体(ioa+限定词)
|
||||
* 入口参数:pn = 1 置否定确认位
|
||||
*/
|
||||
static void s_cmd_con(iec104_link_t *lk, uint8_t type, const uint8_t *obj,
|
||||
uint16_t obj_len, uint8_t cot, uint8_t pn) {
|
||||
uint8_t body[BODY_MAX];
|
||||
uint16_t n = obj_len;
|
||||
|
||||
if (n > BODY_MAX) {
|
||||
n = 0u;
|
||||
}
|
||||
if (n > 0u) {
|
||||
(void)memcpy(body, obj, n);
|
||||
}
|
||||
(void)s_send_asdu(lk, type, 1u, cot,
|
||||
(pn != 0u) ? IEC104_COT_BIT_PN : 0u,
|
||||
0u, 0u, body, n, 0u);
|
||||
}
|
||||
|
||||
/* ---------------- 数据上送 ---------------- */
|
||||
|
||||
/*
|
||||
* 函数功能:单点上送遥测(M_ME_NC_1 SQ=0,值+QDS)
|
||||
* 返 回 值:同 s_send_i
|
||||
*/
|
||||
static int s_send_yc_single(iec104_link_t *lk, uint8_t idx, uint8_t cot) {
|
||||
uint8_t body[5];
|
||||
float v;
|
||||
uint8_t qds;
|
||||
|
||||
(void)iec104_pts_yc_read(idx, &v, &qds);
|
||||
iec104_put_float(body, v);
|
||||
body[4] = qds;
|
||||
return s_send_asdu(lk, IEC104_T_M_ME_NC_1, 1u, cot, 0u, 1u,
|
||||
iec104_pts_yc_ioa(idx), body, 5u, 0u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:总召/周期扫描一步(phase 游标 = interp_yx_idx:
|
||||
* 0 双点组→1 单点组→2 YC 组→3 频率→4 actterm)
|
||||
* 入口参数:budget = 本 poll 发送预算(成功一步减一);periodic = 周期扫描
|
||||
* 返 回 值:0 本步已发出 / -1 k 窗满原地保留(poll 循环必须就此停步,
|
||||
* 否则死循环——2026-10-12 单测抓到)
|
||||
*/
|
||||
static int s_scan_step(iec104_link_t *lk, uint8_t *budget, uint8_t periodic) {
|
||||
uint8_t body[BODY_MAX];
|
||||
uint8_t n = 0u;
|
||||
uint8_t i;
|
||||
uint8_t d;
|
||||
uint8_t qds;
|
||||
float v;
|
||||
int rc = -1;
|
||||
|
||||
switch (lk->interp_yx_idx) {
|
||||
case 0u: /* YX 双点组 SQ=1,IOA 1..4 */
|
||||
for (i = 0u; i < 4u; i++) {
|
||||
iec104_pts_yx_read(i, &d);
|
||||
body[n++] = d;
|
||||
}
|
||||
rc = s_send_asdu(lk, IEC104_T_M_DP_NA_1, (uint8_t)(IEC104_VSQ_SQ | 4u),
|
||||
lk->interp_cot, 0u, 1u, 1u, body, n, 0u);
|
||||
break;
|
||||
case 1u: /* YX 单点组 SQ=1,IOA 5..33 */
|
||||
for (i = 4u; i < 33u; i++) {
|
||||
iec104_pts_yx_read(i, &d);
|
||||
body[n++] = d;
|
||||
}
|
||||
rc = s_send_asdu(lk, IEC104_T_M_SP_NA_1, (uint8_t)(IEC104_VSQ_SQ | 29u),
|
||||
lk->interp_cot, 0u, 1u, 5u, body, n, 0u);
|
||||
break;
|
||||
case 2u: /* YC 组 SQ=1,IOA 0x4011..0x4018 */
|
||||
for (i = 0u; i < 8u; i++) {
|
||||
(void)iec104_pts_yc_read(i, &v, &qds);
|
||||
iec104_put_float(&body[n], v);
|
||||
n += 4u;
|
||||
body[n++] = qds;
|
||||
}
|
||||
rc = s_send_asdu(lk, IEC104_T_M_ME_NC_1, (uint8_t)(IEC104_VSQ_SQ | 8u),
|
||||
lk->interp_cot, 0u, 1u, 0x4011u, body, n, 0u);
|
||||
break;
|
||||
case 3u: /* 频率单点 SQ=0,IOA 0x401D */
|
||||
(void)iec104_pts_yc_read(8u, &v, &qds);
|
||||
iec104_put_float(&body[n], v);
|
||||
n += 4u;
|
||||
body[n++] = qds;
|
||||
rc = s_send_asdu(lk, IEC104_T_M_ME_NC_1, 1u, lk->interp_cot, 0u,
|
||||
1u, 0x401Du, body, n, 0u);
|
||||
break;
|
||||
default: /* actterm(仅总召;周期扫描不走这步) */
|
||||
iec104_put_ioa(&body[n], 0u);
|
||||
n += 3u;
|
||||
body[n++] = IEC104_RQT_GROUP1;
|
||||
rc = s_send_asdu(lk, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACTTERM, 0u,
|
||||
0u, 0u, body, n, 0u);
|
||||
break;
|
||||
}
|
||||
if (rc != 0) {
|
||||
return -1; /* k 窗满:下次 poll 重试本 phase */
|
||||
}
|
||||
(*budget)--;
|
||||
lk->interp_yx_idx++;
|
||||
if (periodic != 0u) {
|
||||
if (lk->interp_yx_idx >= 4u) {
|
||||
lk->interp_step = 0u; /* 周期只发 YC(phase 2..3),无 actterm */
|
||||
}
|
||||
} else if (lk->interp_yx_idx >= 5u) {
|
||||
lk->interp_step = 0u;
|
||||
lk->init_pending = 0u;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:突发(COV)上送——pend 暂存逐点 SQ=0 COT=3,发成功才清位
|
||||
*/
|
||||
static void s_spont_step(iec104_link_t *lk, uint8_t *budget) {
|
||||
uint8_t body[5];
|
||||
uint8_t i;
|
||||
uint8_t d;
|
||||
int rc;
|
||||
|
||||
while ((*budget > 0u) && ((lk->pend_yx != 0u) || (lk->pend_yc != 0u))) {
|
||||
if (lk->pend_yx != 0u) {
|
||||
i = 0u;
|
||||
while (i < 33u) {
|
||||
if ((lk->pend_yx & (1uLL << i)) != 0u) {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
iec104_pts_yx_read(i, &d);
|
||||
body[0] = d;
|
||||
rc = s_send_asdu(lk,
|
||||
(i < 4u) ? IEC104_T_M_DP_NA_1
|
||||
: IEC104_T_M_SP_NA_1,
|
||||
1u, IEC104_COT_SPONT, 0u, 1u,
|
||||
(uint32_t)i + 1u, body, 1u, 0u);
|
||||
if (rc != 0) {
|
||||
return; /* k 窗满:pend 位保留,下次续发 */
|
||||
}
|
||||
lk->pend_yx &= ~(1uLL << i);
|
||||
} else {
|
||||
i = 0u;
|
||||
while (i < 16u) {
|
||||
if ((lk->pend_yc & (uint16_t)(1u << i)) != 0u) {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
rc = s_send_yc_single(lk, i, IEC104_COT_SPONT);
|
||||
if (rc != 0) {
|
||||
return;
|
||||
}
|
||||
lk->pend_yc &= (uint16_t)~(1u << i);
|
||||
}
|
||||
(*budget)--;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 命令处理 ---------------- */
|
||||
|
||||
/*
|
||||
* 函数功能:C_IC_NA_1 总召处理(ACT → actcon + 启动扫描;DEACT → deactcon)
|
||||
* 说明:RBT(bit4)落在 COT 6bit 值域内,ACT+RBT 上收为 22——先剥再判;
|
||||
* 首召/RBT → COT=4(init),后续 COT=2(bg)
|
||||
*/
|
||||
static void s_h_ic(iec104_link_t *lk, const iec104_asdu_hdr_t *h) {
|
||||
/* RBT(bit4)落在 COT 6bit 值域内:ACT+RBT 上收为 22——先剥 RBT 再判值 */
|
||||
uint8_t cot_val = (uint8_t)(h->cot & 0xEFu);
|
||||
|
||||
if (cot_val == IEC104_COT_DEACT) {
|
||||
lk->interp_step = 0u;
|
||||
s_cmd_con(lk, IEC104_T_C_IC_NA_1, h->obj, h->obj_len,
|
||||
IEC104_COT_DEACTCON, 0u);
|
||||
return;
|
||||
}
|
||||
if (cot_val != IEC104_COT_ACT) {
|
||||
return;
|
||||
}
|
||||
if ((h->obj_len < 4u) || (h->obj[3] != IEC104_RQT_GROUP1)) {
|
||||
s_cmd_con(lk, IEC104_T_C_IC_NA_1, h->obj, h->obj_len,
|
||||
IEC104_COT_ACTCON, 1u);
|
||||
return;
|
||||
}
|
||||
s_cmd_con(lk, IEC104_T_C_IC_NA_1, h->obj, h->obj_len, IEC104_COT_ACTCON, 0u);
|
||||
lk->interp_step = 1u;
|
||||
lk->interp_yx_idx = 0u;
|
||||
lk->interp_cot = ((lk->init_pending != 0u)
|
||||
|| ((h->cot & IEC104_COT_BIT_RBT) != 0u))
|
||||
? (uint8_t)IEC104_COT_INIT : (uint8_t)IEC104_COT_BG_SCAN;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:C_CS_NA_1 对时(CP56 → epoch → cfg->clock_sync)
|
||||
* 说明:IV/字段坏/系统侧拒绝 → actcon + PN
|
||||
*/
|
||||
static void s_h_cs(iec104_link_t *lk, const iec104_asdu_hdr_t *h) {
|
||||
uint64_t epoch = 0u;
|
||||
int rc = -1;
|
||||
|
||||
if (h->obj_len >= 10u) {
|
||||
rc = iec104_cp56_to_epoch_ms(&h->obj[3], &epoch);
|
||||
if ((rc == 0) && (lk->cfg->clock_sync != NULL)) {
|
||||
rc = lk->cfg->clock_sync(lk->ctx, epoch);
|
||||
}
|
||||
}
|
||||
s_cmd_con(lk, IEC104_T_C_CS_NA_1, h->obj, h->obj_len, IEC104_COT_ACTCON,
|
||||
(rc == 0) ? 0u : 1u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:C_DC_NA_1 遥控两段式(选择记 ioa+dpc;执行须匹配,否则 PN)
|
||||
*/
|
||||
static void s_h_dc(iec104_link_t *lk, const iec104_asdu_hdr_t *h) {
|
||||
uint32_t ioa;
|
||||
uint8_t qdc;
|
||||
uint8_t sel;
|
||||
uint8_t dpc;
|
||||
int rc;
|
||||
|
||||
if (h->obj_len < 4u) {
|
||||
return;
|
||||
}
|
||||
ioa = iec104_get_ioa(h->obj);
|
||||
qdc = h->obj[3];
|
||||
sel = (uint8_t)(((qdc & IEC104_QDC_S) != 0u) ? 1u : 0u);
|
||||
dpc = (uint8_t)(qdc & 0x03u);
|
||||
if (sel != 0u) {
|
||||
rc = iec104_pts_yk_exec(ioa, dpc, 1u);
|
||||
if (rc == 0) {
|
||||
lk->yk_sel_ioa = ioa;
|
||||
lk->yk_sel_dpc = dpc;
|
||||
}
|
||||
} else {
|
||||
if ((lk->yk_sel_ioa == ioa) && (lk->yk_sel_dpc == dpc)) {
|
||||
rc = iec104_pts_yk_exec(ioa, dpc, 0u);
|
||||
} else {
|
||||
rc = -1; /* 无选择/失配 → 否定确认 */
|
||||
}
|
||||
lk->yk_sel_ioa = 0u;
|
||||
lk->yk_sel_dpc = 0u;
|
||||
}
|
||||
s_cmd_con(lk, IEC104_T_C_DC_NA_1, h->obj, h->obj_len, IEC104_COT_ACTCON,
|
||||
(rc == 0) ? 0u : 1u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:C_SE_NC_1 定值两段式写(选择段只校验;执行段落盘)
|
||||
* 说明:越界/未知地址 → actcon + PN
|
||||
*/
|
||||
static void s_h_se(iec104_link_t *lk, const iec104_asdu_hdr_t *h) {
|
||||
uint32_t addr;
|
||||
float v;
|
||||
uint8_t sel;
|
||||
int rc;
|
||||
|
||||
if (h->obj_len < 8u) {
|
||||
return;
|
||||
}
|
||||
addr = iec104_get_ioa(h->obj);
|
||||
v = iec104_get_float(&h->obj[3]);
|
||||
sel = (uint8_t)(((h->obj[7] & IEC104_QDC_S) != 0u) ? 1u : 0u);
|
||||
rc = iec104_pts_set_write(addr, v, sel);
|
||||
s_cmd_con(lk, IEC104_T_C_SE_NC_1, h->obj, h->obj_len, IEC104_COT_ACTCON,
|
||||
(rc == 0) ? 0u : 1u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:C_RD_NA_1 读单个信息体(YC 命中 → M_ME COT5;定值命中 → 同;
|
||||
* 未知 IOA → 原样回 C_RD + COT14 + EVI)
|
||||
*/
|
||||
static void s_h_rd(iec104_link_t *lk, const iec104_asdu_hdr_t *h) {
|
||||
uint32_t ioa;
|
||||
uint8_t i;
|
||||
float v;
|
||||
|
||||
if (h->obj_len < 3u) {
|
||||
return;
|
||||
}
|
||||
ioa = iec104_get_ioa(h->obj);
|
||||
for (i = 0u; i < iec104_pts_yc_count(); i++) {
|
||||
if (iec104_pts_yc_ioa(i) == ioa) {
|
||||
s_send_yc_single(lk, i, IEC104_COT_REQ);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (iec104_pts_set_read(ioa, &v) == 0) {
|
||||
uint8_t body[5];
|
||||
|
||||
iec104_put_float(body, v);
|
||||
body[4] = 0u;
|
||||
(void)s_send_asdu(lk, IEC104_T_M_ME_NC_1, 1u, IEC104_COT_REQ, 0u,
|
||||
1u, ioa, body, 5u, 0u);
|
||||
return;
|
||||
}
|
||||
(void)s_send_asdu(lk, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_UNK_IOA, 0u,
|
||||
1u, ioa, NULL, 0u, 1u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:ASDU 分派(公共地址校验 → 类型路由)
|
||||
* 说明:错 ca → COT13+EVI;未知类型 → COT12+EVI(原类型回)
|
||||
*/
|
||||
static void s_h_asdu(iec104_link_t *lk, const uint8_t *asdu, uint16_t len) {
|
||||
iec104_asdu_hdr_t h;
|
||||
|
||||
if (iec104_asdu_parse_hdr(asdu, len, &h) != 0) {
|
||||
return;
|
||||
}
|
||||
if (h.ca != lk->cfg->ca) {
|
||||
(void)s_send_asdu(lk, h.type, 1u, IEC104_COT_UNK_CA, 0u,
|
||||
0u, 0u, NULL, 0u, 1u);
|
||||
return;
|
||||
}
|
||||
switch (h.type) {
|
||||
case IEC104_T_C_IC_NA_1:
|
||||
s_h_ic(lk, &h);
|
||||
break;
|
||||
case IEC104_T_C_CS_NA_1:
|
||||
s_h_cs(lk, &h);
|
||||
break;
|
||||
case IEC104_T_C_DC_NA_1:
|
||||
s_h_dc(lk, &h);
|
||||
break;
|
||||
case IEC104_T_C_SE_NC_1:
|
||||
s_h_se(lk, &h);
|
||||
break;
|
||||
case IEC104_T_C_RD_NA_1:
|
||||
s_h_rd(lk, &h);
|
||||
break;
|
||||
case IEC104_T_C_TS_NA_1:
|
||||
s_cmd_con(lk, IEC104_T_C_TS_NA_1, h.obj, h.obj_len,
|
||||
IEC104_COT_ACTCON, 0u);
|
||||
break;
|
||||
case IEC104_T_C_RP_NA_1:
|
||||
iec104_pts_cov_check(NULL); /* 进程复位:重建快照(清历史) */
|
||||
s_cmd_con(lk, IEC104_T_C_RP_NA_1, h.obj, h.obj_len,
|
||||
IEC104_COT_ACTCON, 0u);
|
||||
break;
|
||||
default:
|
||||
(void)s_send_asdu(lk, h.type, 1u, IEC104_COT_UNK_COT, 0u,
|
||||
0u, 0u, NULL, 0u, 1u);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- APDU 分派 ---------------- */
|
||||
|
||||
/*
|
||||
* 函数功能:U 帧处理(STARTDT/STOPDT act → con + 态迁移;TESTFR act → con;
|
||||
* TESTFR con → 清挂起)
|
||||
*/
|
||||
static void s_h_u(iec104_link_t *lk, uint8_t q) {
|
||||
switch (q) {
|
||||
case U_STARTDT_ACT:
|
||||
s_send_u(lk, U_STARTDT_CON);
|
||||
lk->state = IEC104_ST_RUN;
|
||||
lk->interp_step = 0u;
|
||||
lk->yk_sel_ioa = 0u;
|
||||
break;
|
||||
case U_STOPDT_ACT:
|
||||
s_send_u(lk, U_STOPDT_CON);
|
||||
lk->state = IEC104_ST_STOPPED;
|
||||
lk->interp_step = 0u;
|
||||
break;
|
||||
case U_TESTFR_ACT:
|
||||
s_send_u(lk, U_TESTFR_CON);
|
||||
break;
|
||||
case U_TESTFR_CON:
|
||||
lk->testfr_pending = 0u;
|
||||
break;
|
||||
default:
|
||||
break; /* con/未知 U:t1/t3 已由外层刷新 */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:N(R) 校验并更新 tx_unacked(V(A) ≤ V(A)' ≤ V(S),即 0 ≤ d ≤ 窗内)
|
||||
* 说明:N(R) 倒退(早于上次确认)= 协议违例 → 链路故障(标准语义);
|
||||
* tx_unacked 恒 ≤ k(发送侧门控),故 d ≤ tx_unacked 已蕴含窗校验
|
||||
* 返 回 值:0 / -1 违例
|
||||
*/
|
||||
static int s_apply_nr(iec104_link_t *lk, uint16_t nr) {
|
||||
uint16_t d = (uint16_t)(((uint32_t)lk->tx_seq + SEQ_MOD - (uint32_t)nr)
|
||||
% SEQ_MOD);
|
||||
|
||||
if ((uint32_t)d > (uint32_t)lk->tx_unacked) {
|
||||
return -1;
|
||||
}
|
||||
lk->tx_unacked = d;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:单个完整 APDU 分派(U/S/I);任何接收只刷 t3(不刷 t1)
|
||||
* 说明:OPEN 态只认 U;I 帧 N(S) 不连续 → IDLE;RUN 态才处理 ASDU;
|
||||
* 收满 w 个未确认 → 回 S
|
||||
*/
|
||||
static void s_h_apdu(iec104_link_t *lk, const uint8_t *p, uint16_t total) {
|
||||
const uint8_t *c = &p[2]; /* 控制域 4B */
|
||||
uint16_t ns;
|
||||
uint16_t nr;
|
||||
|
||||
lk->t3_deadline = lk->now_ms + IEC104_T3_MS;
|
||||
if ((c[0] & 0x03u) == 0x03u) {
|
||||
s_h_u(lk, c[0]);
|
||||
return;
|
||||
}
|
||||
if (lk->state == IEC104_ST_OPEN) {
|
||||
return; /* OPEN 态只认 U 帧 */
|
||||
}
|
||||
if ((c[0] & 0x01u) != 0u) { /* S 格式 */
|
||||
nr = (uint16_t)(((uint16_t)c[2] | ((uint16_t)c[3] << 8)) >> 1);
|
||||
if (s_apply_nr(lk, nr) != 0) {
|
||||
lk->state = IEC104_ST_IDLE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
/* I 格式 */
|
||||
ns = (uint16_t)(((uint16_t)c[0] | ((uint16_t)c[1] << 8)) >> 1);
|
||||
nr = (uint16_t)(((uint16_t)c[2] | ((uint16_t)c[3] << 8)) >> 1);
|
||||
if (ns != lk->rx_seq) {
|
||||
lk->state = IEC104_ST_IDLE; /* 序号错 → 链路故障 */
|
||||
return;
|
||||
}
|
||||
if (s_apply_nr(lk, nr) != 0) {
|
||||
lk->state = IEC104_ST_IDLE;
|
||||
return;
|
||||
}
|
||||
lk->rx_seq = (uint16_t)((ns + 1u) % SEQ_MOD);
|
||||
lk->rx_unacked++;
|
||||
if (lk->rx_unacked == 1u) {
|
||||
lk->t2_deadline = lk->now_ms + IEC104_T2_MS;
|
||||
}
|
||||
if (lk->state == IEC104_ST_RUN) {
|
||||
s_h_asdu(lk, &p[6], (uint16_t)(total - 6u));
|
||||
}
|
||||
if (lk->rx_unacked >= IEC104_W_MAX) {
|
||||
s_send_s(lk);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 公开接口 ---------------- */
|
||||
|
||||
/*
|
||||
* 函数功能:链路实例初始化(全清零 + 绑配置与上下文,态 = IDLE)
|
||||
*/
|
||||
void iec104_link_init(iec104_link_t *lk, const iec104_link_cfg_t *cfg,
|
||||
void *ctx) {
|
||||
(void)memset(lk, 0, sizeof(*lk));
|
||||
lk->cfg = cfg;
|
||||
lk->ctx = ctx;
|
||||
lk->state = IEC104_ST_IDLE;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:TCP 连接建立 → OPEN(序号/窗口/定时/暂存全部复位,init_pending=1)
|
||||
*/
|
||||
void iec104_link_open(iec104_link_t *lk, uint32_t now_ms) {
|
||||
lk->state = IEC104_ST_OPEN;
|
||||
lk->tx_seq = 0u;
|
||||
lk->rx_seq = 0u;
|
||||
lk->tx_unacked = 0u;
|
||||
lk->rx_unacked = 0u;
|
||||
lk->testfr_pending = 0u;
|
||||
lk->init_pending = 1u;
|
||||
lk->rx_len = 0u;
|
||||
lk->yk_sel_ioa = 0u;
|
||||
lk->yk_sel_dpc = 0u;
|
||||
lk->interp_step = 0u;
|
||||
lk->interp_yx_idx = 0u;
|
||||
lk->pend_yx = 0u;
|
||||
lk->pend_yc = 0u;
|
||||
lk->now_ms = now_ms;
|
||||
lk->t1_deadline = now_ms + IEC104_T1_MS;
|
||||
lk->t2_deadline = now_ms + IEC104_T2_MS;
|
||||
lk->t3_deadline = now_ms + IEC104_T3_MS;
|
||||
lk->period_next = now_ms + PERIOD_MS;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:链路断开/故障 → IDLE(清重组缓冲与所有暂存)
|
||||
*/
|
||||
void iec104_link_close(iec104_link_t *lk) {
|
||||
lk->state = IEC104_ST_IDLE;
|
||||
lk->rx_len = 0u;
|
||||
lk->interp_step = 0u;
|
||||
lk->pend_yx = 0u;
|
||||
lk->pend_yc = 0u;
|
||||
lk->yk_sel_ioa = 0u;
|
||||
lk->yk_sel_dpc = 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:喂入收到的原始字节(APCI 流重组:半帧/粘帧逐处)
|
||||
* 说明:坏起始符/坏长度/缓冲溢出 → IDLE(胶水层检测后断开 socket)
|
||||
*/
|
||||
void iec104_link_on_rx(iec104_link_t *lk, const uint8_t *data, uint16_t len,
|
||||
uint32_t now_ms) {
|
||||
lk->now_ms = now_ms;
|
||||
if ((lk->state == IEC104_ST_IDLE) || (data == NULL)) {
|
||||
return;
|
||||
}
|
||||
if ((uint32_t)len > (uint32_t)(IEC104_RXBUF_SZ - lk->rx_len)) {
|
||||
lk->state = IEC104_ST_IDLE; /* 重组缓冲溢出 = 坏流 */
|
||||
lk->rx_len = 0u;
|
||||
return;
|
||||
}
|
||||
(void)memcpy(&lk->rx_buf[lk->rx_len], data, len);
|
||||
lk->rx_len = (uint16_t)(lk->rx_len + len);
|
||||
|
||||
while (lk->rx_len >= 2u) {
|
||||
uint16_t lf;
|
||||
uint16_t need;
|
||||
|
||||
if (lk->rx_buf[0] != APCI_START) {
|
||||
lk->state = IEC104_ST_IDLE;
|
||||
break;
|
||||
}
|
||||
lf = lk->rx_buf[1];
|
||||
if ((lf < APCI_LEN_MIN) || (lf > APCI_LEN_MAX)) {
|
||||
lk->state = IEC104_ST_IDLE;
|
||||
break;
|
||||
}
|
||||
need = (uint16_t)(lf + 2u);
|
||||
if (lk->rx_len < need) {
|
||||
break; /* 半帧:等下次 on_rx */
|
||||
}
|
||||
s_h_apdu(lk, lk->rx_buf, need);
|
||||
if (lk->state == IEC104_ST_IDLE) {
|
||||
break;
|
||||
}
|
||||
if (need < lk->rx_len) {
|
||||
(void)memmove(lk->rx_buf, &lk->rx_buf[need],
|
||||
(uint16_t)(lk->rx_len - need));
|
||||
}
|
||||
lk->rx_len = (uint16_t)(lk->rx_len - need);
|
||||
}
|
||||
if (lk->state == IEC104_ST_IDLE) {
|
||||
lk->rx_len = 0u;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:周期节拍(胶水层每次服务循环调用一次,时基由调用方注入)
|
||||
* 说明:t1 超时 → close;t3 到期 → TESTFR act;t2 到期 → S;
|
||||
* RUN 态按优先级:总召续扫 > 周期续扫 > COV 突发 > 周期触发
|
||||
*/
|
||||
void iec104_link_poll(iec104_link_t *lk, uint32_t now_ms) {
|
||||
uint8_t budget = IEC104_POLL_BUDGET;
|
||||
|
||||
lk->now_ms = now_ms;
|
||||
if ((lk->state != IEC104_ST_RUN) && (lk->state != IEC104_ST_STOPPED)) {
|
||||
return;
|
||||
}
|
||||
if (((lk->tx_unacked > 0u) || (lk->testfr_pending != 0u))
|
||||
&& (s_expired(now_ms, lk->t1_deadline) != 0)) {
|
||||
iec104_link_close(lk); /* t1:未确认 I/未答 TESTFR 超时 = 故障 */
|
||||
return;
|
||||
}
|
||||
if (s_expired(now_ms, lk->t3_deadline) != 0) {
|
||||
s_send_u(lk, U_TESTFR_ACT);
|
||||
}
|
||||
if ((lk->rx_unacked > 0u) && (s_expired(now_ms, lk->t2_deadline) != 0)) {
|
||||
s_send_s(lk);
|
||||
}
|
||||
if (lk->state != IEC104_ST_RUN) {
|
||||
return;
|
||||
}
|
||||
if (lk->interp_step == 1u) {
|
||||
while ((budget > 0u) && (lk->interp_step == 1u)) {
|
||||
if (s_scan_step(lk, &budget, 0u) != 0) {
|
||||
break; /* k 窗满:本 poll 停步(防死循环) */
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (lk->interp_step == 3u) {
|
||||
while ((budget > 0u) && (lk->interp_step == 3u)) {
|
||||
if (s_scan_step(lk, &budget, 1u) != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ((lk->pend_yx != 0u) || (lk->pend_yc != 0u)) {
|
||||
s_spont_step(lk, &budget);
|
||||
if (budget == 0u) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (s_expired(now_ms, lk->period_next) != 0) {
|
||||
lk->interp_step = 3u;
|
||||
lk->interp_yx_idx = 2u; /* 周期只扫 YC(phase 2..3) */
|
||||
lk->interp_cot = IEC104_COT_PERIODIC;
|
||||
lk->period_next = now_ms + PERIOD_MS;
|
||||
while ((budget > 0u) && (lk->interp_step == 3u)) {
|
||||
if (s_scan_step(lk, &budget, 1u) != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
{
|
||||
iec104_pts_cov_t cov;
|
||||
|
||||
iec104_pts_cov_check(&cov);
|
||||
lk->pend_yx |= cov.yx_changed;
|
||||
lk->pend_yc |= cov.yc_changed;
|
||||
s_spont_step(lk, &budget);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:查询链路状态(胶水层检测 IDLE 跳变以断开/重建 socket)
|
||||
*/
|
||||
iec104_state_t iec104_link_state(const iec104_link_t *lk) {
|
||||
return lk->state;
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 链路层(APCI + 状态机,纯逻辑)
|
||||
* 模块功能:TCP 流 → APDU 重组、U/S/I 帧编解码、STARTDT/STOPDT/TESTFR、
|
||||
* k/w 流量窗与 t1/t2/t3 超时、发送序号 N(S)/N(R) 管理,以及应用层
|
||||
* 流程编排:总召(act→背景扫描→actterm,首召 RBT 用 COT=4)、
|
||||
* 突发上送(COV,COT=3)、周期上送(COT=1)、读单个(C_RD→COT=5)、
|
||||
* 双点遥控两段式(选择/执行,失配否定确认 PN)、定值读(C_RD)/
|
||||
* 定值写(C_SE_NC 选择=范围校验 / 执行=写入+封存)、时钟同步(C_CS
|
||||
* → clock_sync 回调)、试验(C_TS)与进程复位(C_RP,清快照)。
|
||||
* **零 I/O**:收 = on_rx 喂 TCP 流片段,发 = send_apdu 回调,
|
||||
* 时基 = 调用方注入 now_ms(tick ms,回绕安全:差值法)
|
||||
* → 全栈(asdu+pts+link)主机侧 L0.5 可离线跑(登记簿 A-4 约束 1)。
|
||||
* 参数(DL/T 634.5104 标准默认,§1.5 未另定):k=12 w=8 t1=15s t2=10s t3=20s。
|
||||
* 端口:TCP **2404**(§1.5 B0 定义;本 DTU = 被控站/服务器,主站连入)。
|
||||
* 线程约定:单实例单任务上下文(板端 = 104 服务在 netTask 消息循环内驱动,
|
||||
* 见 iec104_serv 胶水层);非线程安全,禁止多任务共享一实例。
|
||||
* 失败语义:send_apdu 内**不得阻塞**;发送失败由胶水层断链(主站会重连),
|
||||
* 本层不感知发送失败。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#ifndef IEC104_LINK_H
|
||||
#define IEC104_LINK_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "iec104_pts.h"
|
||||
|
||||
/* 服务端口(§1.5:以太网 TCP 2404) */
|
||||
#define IEC104_PORT 2404U
|
||||
|
||||
/* 链路参数(标准默认值) */
|
||||
#define IEC104_K_MAX 12U /* 未确认 I 帧上限 */
|
||||
#define IEC104_W_MAX 8U /* 收到 I 帧未发确认的强制 S 阈值 */
|
||||
#define IEC104_T1_MS 15000UL /* 发送/确认超时(超时 = 链路故障) */
|
||||
#define IEC104_T2_MS 10000UL /* 无 I 发送时强制 S 确认超时(< t1) */
|
||||
#define IEC104_T3_MS 20000UL /* 空闲发 TESTFR 周期(< t1) */
|
||||
|
||||
/* TCP 流重组缓冲:APDU 上限 253 + 半帧余量 */
|
||||
#define IEC104_RXBUF_SZ 512U
|
||||
|
||||
/* 单次 poll 发送预算(ASDU 数)——防总召大扫描饿死宿主任务 */
|
||||
#define IEC104_POLL_BUDGET 4U
|
||||
|
||||
/* 链路状态 */
|
||||
typedef enum {
|
||||
IEC104_ST_IDLE = 0, /* TCP 未建立(或已断开) */
|
||||
IEC104_ST_OPEN, /* TCP 已建立,等 STARTDT act */
|
||||
IEC104_ST_RUN, /* STARTDT 已确认,数据服务开 */
|
||||
IEC104_ST_STOPPED /* STOPDT 已确认:停数据,保 S/TESTFR */
|
||||
} iec104_state_t;
|
||||
|
||||
/* 配置与回调(ctx 原样回传给回调) */
|
||||
typedef struct {
|
||||
uint16_t ca; /* 公共地址 = 链路地址(2B,§1.5) */
|
||||
/* 发一个完整 APDU(含 6B APCI 头)。不得阻塞;生命周期仅回调内有效 */
|
||||
void (*send_apdu)(void *ctx, const uint8_t *apdu, uint16_t len);
|
||||
/* C_CS 时钟同步:epoch 毫秒;NULL 或返回 -1 → 否定确认 */
|
||||
int (*clock_sync)(void *ctx, uint64_t epoch_ms);
|
||||
} iec104_link_cfg_t;
|
||||
|
||||
/* 链路实例(胶水层静态持有,零堆) */
|
||||
typedef struct {
|
||||
const iec104_link_cfg_t *cfg;
|
||||
void *ctx;
|
||||
iec104_state_t state;
|
||||
uint8_t init_pending; /* 1 = 下次总召按初始化(COT=4 + RBT) */
|
||||
/* 序号(0..32767,帧内 ×2) */
|
||||
uint16_t tx_seq;
|
||||
uint16_t rx_seq;
|
||||
uint16_t tx_unacked; /* 已发 I 未获认(k 窗) */
|
||||
uint16_t rx_unacked; /* 已收 I 未确认(w 阈) */
|
||||
uint8_t testfr_pending; /* 1 = TESTFR act 未获 con(t1 同样约束它) */
|
||||
/* 定时器到期时刻(now_ms 域;差值法判超时,回绕安全) */
|
||||
uint32_t t1_deadline;
|
||||
uint32_t t2_deadline;
|
||||
uint32_t t3_deadline;
|
||||
uint32_t now_ms;
|
||||
/* TCP 流重组 */
|
||||
uint8_t rx_buf[IEC104_RXBUF_SZ];
|
||||
uint16_t rx_len;
|
||||
/* 总召进度:0 无 / 1 背景扫描中 / 2 待发 actterm */
|
||||
uint8_t interp_step;
|
||||
uint8_t interp_yc_idx; /* 背景扫描 YC 游标 */
|
||||
uint8_t interp_yx_idx; /* 背景扫描 YX 游标 */
|
||||
uint8_t interp_cot; /* 本次总召用的 COT(BG_SCAN 或 INIT) */
|
||||
/* 遥控选择暂存(执行段比对) */
|
||||
uint32_t yk_sel_ioa; /* 0 = 无选择 */
|
||||
uint8_t yk_sel_dpc;
|
||||
/* 周期上送(COT=1)下次触发时刻 */
|
||||
uint32_t period_next;
|
||||
/* COV 待发暂存(k 窗满时保留到下次 poll,变化不许丢) */
|
||||
uint64_t pend_yx;
|
||||
uint16_t pend_yc;
|
||||
} iec104_link_t;
|
||||
|
||||
/*
|
||||
* 函数功能:初始化实例(置 IDLE、清序号;不发送任何帧)
|
||||
* 入口参数:lk - 实例;cfg/ctx - 配置与回调上下文
|
||||
*/
|
||||
void iec104_link_init(iec104_link_t *lk, const iec104_link_cfg_t *cfg, void *ctx);
|
||||
|
||||
/*
|
||||
* 函数功能:TCP 连接建立(新连接或重连后调用)
|
||||
* 入口参数:now_ms - 当前时基
|
||||
* 函数说明:序号清零、init_pending 置位(首召 = 初始化总召)、进 OPEN。
|
||||
*/
|
||||
void iec104_link_open(iec104_link_t *lk, uint32_t now_ms);
|
||||
|
||||
/*
|
||||
* 函数功能:TCP 断开(对端断开或胶水层发送失败主动断链时调用)
|
||||
* 函数说明:回 IDLE、清重组缓冲与选择暂存;不发送任何帧。
|
||||
*/
|
||||
void iec104_link_close(iec104_link_t *lk);
|
||||
|
||||
/*
|
||||
* 函数功能:喂入 TCP 收到的流片段(任意切分/粘帧)
|
||||
* 入口参数:data/len - 流片段;now_ms - 当前时基
|
||||
* 函数说明:内部重组,凑齐 APDU 即处理;坏帧(起始符/长度非法)→ 视同链路
|
||||
* 故障,实例回 IDLE(胶水层检测到 IDLE 跳变应断开 socket)。
|
||||
* 返回后实例仍为 RUN 不代表对端合法——仅表示未触发断链条件。
|
||||
*/
|
||||
void iec104_link_on_rx(iec104_link_t *lk, const uint8_t *data, uint16_t len,
|
||||
uint32_t now_ms);
|
||||
|
||||
/*
|
||||
* 函数功能:周期驱动(定时器 + COV + 总召分片 + 周期上送)
|
||||
* 入口参数:now_ms - 当前时基(建议 ≤100 ms 调一次)
|
||||
* 函数说明:仅 RUN 态产生数据帧;STOPPED 态只处理 t2/t3(S/TESTFR)。
|
||||
* 单次调用发送量 ≤ IEC104_POLL_BUDGET 个 ASDU。
|
||||
*/
|
||||
void iec104_link_poll(iec104_link_t *lk, uint32_t now_ms);
|
||||
|
||||
/* 当前状态(日志/看门狗用) */
|
||||
iec104_state_t iec104_link_state(const iec104_link_t *lk);
|
||||
|
||||
#endif /* IEC104_LINK_H */
|
||||
@@ -0,0 +1,426 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 点表与数据源适配(实现)
|
||||
* 模块功能:实现 iec104_pts.h——YC/YX/YK/定值点表(ROM)、板端默认源
|
||||
* (phasor / prot_elm / prot_set)、COV 快照比对(遥信变位 +
|
||||
* 遥测死区)。点表口径见 iec104_pts.h 文件头与设计文档 §3。
|
||||
* 线程约定:init/bind 建服务前;其余仅 104 服务任务上下文(快照非原子)。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#include "iec104_pts.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "iec104_asdu.h" /* QDS/DIQ 品质位常量 */
|
||||
#include "phasor.h" /* 板端源:遥测 = 基波有效值 */
|
||||
#include "prot_elm.h" /* 板端源:遥信 = 元件状态 */
|
||||
#include "prot_set.h" /* 板端源:定值 + 闭锁 */
|
||||
#include "adc_input.h" /* adc_semantic_t(phasor 实例按语义绑定) */
|
||||
|
||||
/* ---------------- YC 表(§1.5 点号 4011+;死区 = 拟定码值,L1-5 后复核) -- */
|
||||
typedef struct {
|
||||
uint32_t ioa;
|
||||
int8_t sem; /* 绑定的 phasor 语义通道(-1 = 无 phasor 源,恒无效) */
|
||||
float deadband; /* 死区(工程量绝对值) */
|
||||
} pts_yc_def_t;
|
||||
|
||||
static const pts_yc_def_t k_yc[] = {
|
||||
{ 0x4011u, (int8_t)ADC_SEM_UA, 10.0f }, /* 1Ua */
|
||||
{ 0x4012u, -1, 10.0f }, /* 1Ub:V0.1 削顶 → IV */
|
||||
{ 0x4013u, -1, 10.0f }, /* 1Uc:同上 */
|
||||
{ 0x4014u, (int8_t)ADC_SEM_UN, 10.0f }, /* 1U0(外部零序专用通道) */
|
||||
{ 0x4015u, (int8_t)ADC_SEM_IA, 10.0f }, /* 1Ia */
|
||||
{ 0x4016u, -1, 10.0f }, /* 1Ib */
|
||||
{ 0x4017u, -1, 10.0f }, /* 1Ic */
|
||||
{ 0x4018u, -1, 10.0f }, /* 1I0 */
|
||||
{ 0x401Du, -1, 0.5f }, /* 1f:L1-7 未实现 → IV */
|
||||
};
|
||||
#define YC_N ((uint8_t)(sizeof(k_yc) / sizeof(k_yc[0])))
|
||||
|
||||
/* phasor 实例下标缓存(init 时按 sem+harm==1 查表;-1 = 未找到) */
|
||||
static int8_t s_yc_phasor_idx[YC_N];
|
||||
|
||||
/* ---------------- YX 表(IOA = 1..33;双点 = 开关位置类) ------------------ */
|
||||
#define YX_N 33u
|
||||
#define YX_DP_N 4u /* idx 0..3 = 双点(开关位置,V0.1 无硬件 → IV) */
|
||||
|
||||
/* 有效位图:YX15~18(idx14~17)+ YX23(idx22)+ YX31(idx30) */
|
||||
#define YX_VALID_MASK (((1uLL << 14u) | (1uLL << 15u) | (1uLL << 16u) \
|
||||
| (1uLL << 17u) | (1uLL << 22u) | (1uLL << 30u)))
|
||||
|
||||
/* ---------------- 数据源(默认 = 板端;bind_src 可整体替换) -------------- */
|
||||
static int s_src_yc_read(uint8_t idx, float *out);
|
||||
static uint64_t s_src_yx_bits(void);
|
||||
static uint64_t s_src_yx_valid(void);
|
||||
static int s_src_yk_exec(uint32_t ioa, uint8_t dpc, uint8_t select);
|
||||
static int s_src_set_read(uint32_t addr, float *out);
|
||||
static int s_src_set_write(uint32_t addr, float v, uint8_t select);
|
||||
|
||||
static const iec104_pts_src_t k_default_src = {
|
||||
s_src_yc_read, s_src_yx_bits, s_src_yx_valid,
|
||||
s_src_yk_exec, s_src_set_read, s_src_set_write
|
||||
};
|
||||
static const iec104_pts_src_t *s_src;
|
||||
|
||||
/* ---------------- COV 快照 ---------------- */
|
||||
static float s_yc_last[YC_N];
|
||||
static uint16_t s_yc_have; /* bit i = 已有上次快照 */
|
||||
static uint64_t s_yx_last;
|
||||
static uint8_t s_yx_snap_ok;
|
||||
|
||||
/* ============================ 公开接口 ================================== */
|
||||
|
||||
/*
|
||||
* 函数功能:初始化点表模块(扫 phasor 基波实例映射、恢复默认源、清快照)
|
||||
*/
|
||||
void iec104_pts_init(void) {
|
||||
uint8_t i;
|
||||
uint8_t n;
|
||||
|
||||
(void)memset(s_yc_phasor_idx, -1, sizeof(s_yc_phasor_idx));
|
||||
n = phasor_inst_count();
|
||||
for (i = 0u; i < YC_N; i++) {
|
||||
uint8_t j;
|
||||
|
||||
if (k_yc[i].sem < 0) {
|
||||
continue;
|
||||
}
|
||||
for (j = 0u; j < n; j++) {
|
||||
const phasor_inst_t *inst = phasor_inst(j);
|
||||
|
||||
if ((inst != NULL) && ((uint8_t)inst->sem == (uint8_t)k_yc[i].sem)
|
||||
&& (inst->harm == 1u)) {
|
||||
s_yc_phasor_idx[i] = (int8_t)j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
s_src = &k_default_src;
|
||||
s_yc_have = 0u;
|
||||
s_yx_snap_ok = 0u;
|
||||
s_yx_last = 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:替换数据源(NULL = 恢复板端默认源;单测注入假源)
|
||||
*/
|
||||
void iec104_pts_bind_src(const iec104_pts_src_t *src) {
|
||||
s_src = (src != NULL) ? src : &k_default_src;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:遥测点数
|
||||
*/
|
||||
uint8_t iec104_pts_yc_count(void) {
|
||||
return YC_N;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:遥测点 IOA(按下标;越界 → 0)
|
||||
*/
|
||||
uint32_t iec104_pts_yc_ioa(uint8_t idx) {
|
||||
return (idx < YC_N) ? k_yc[idx].ioa : 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读遥测(值 + 品质 descriptor;源 -2 → NT,其余失败 → IV)
|
||||
* 返 回 值:0 有效 / -1 无效
|
||||
*/
|
||||
int iec104_pts_yc_read(uint8_t idx, float *v, uint8_t *qds) {
|
||||
int rc;
|
||||
|
||||
if (v != NULL) {
|
||||
*v = 0.0f;
|
||||
}
|
||||
if ((idx >= YC_N) || (s_src == NULL) || (s_src->yc_read == NULL)) {
|
||||
if (qds != NULL) {
|
||||
*qds = IEC104_QDS_IV;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
rc = s_src->yc_read(idx, v);
|
||||
if (rc == 0) {
|
||||
if (qds != NULL) {
|
||||
*qds = 0u;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (v != NULL) {
|
||||
*v = 0.0f;
|
||||
}
|
||||
if (qds != NULL) {
|
||||
*qds = (rc == -2) ? IEC104_QDS_NT : IEC104_QDS_IV;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:遥信点数
|
||||
*/
|
||||
uint8_t iec104_pts_yx_count(void) {
|
||||
return (uint8_t)YX_N;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:遥信点 IOA(idx + 1;越界 → 0)
|
||||
*/
|
||||
uint32_t iec104_pts_yx_ioa(uint8_t idx) {
|
||||
return ((uint8_t)idx < YX_N) ? ((uint32_t)idx + 1u) : 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:该遥信是否双点型(idx < 4 = 开关位置类)
|
||||
*/
|
||||
uint8_t iec104_pts_yx_is_dp(uint8_t idx) {
|
||||
return ((uint8_t)idx < YX_DP_N) ? 1u : 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读遥信(无效通道 → DIQ_IV;双点 → DPC 编码)
|
||||
*/
|
||||
void iec104_pts_yx_read(uint8_t idx, uint8_t *out) {
|
||||
uint64_t bits;
|
||||
uint64_t valid;
|
||||
uint8_t on;
|
||||
|
||||
if (out == NULL) {
|
||||
return;
|
||||
}
|
||||
if ((idx >= YX_N) || (s_src == NULL) || (s_src->yx_bits == NULL)
|
||||
|| (s_src->yx_valid == NULL)) {
|
||||
*out = IEC104_DIQ_IV;
|
||||
return;
|
||||
}
|
||||
valid = s_src->yx_valid();
|
||||
if (((valid >> idx) & 1u) == 0u) {
|
||||
*out = IEC104_DIQ_IV;
|
||||
return;
|
||||
}
|
||||
bits = s_src->yx_bits();
|
||||
on = (uint8_t)((bits >> idx) & 1u);
|
||||
if (iec104_pts_yx_is_dp(idx) != 0u) {
|
||||
*out = (on != 0u) ? (uint8_t)IEC104_DPC_ON : (uint8_t)IEC104_DPC_OFF;
|
||||
} else {
|
||||
*out = on;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:COV 快照比对(遥信 = 有效位翻转;遥测 = 死区)
|
||||
* 入口参数:cov = 输出变位位图(NULL = 只刷快照)
|
||||
*/
|
||||
void iec104_pts_cov_check(iec104_pts_cov_t *cov) {
|
||||
iec104_pts_cov_t local;
|
||||
|
||||
(void)memset(&local, 0, sizeof(local));
|
||||
|
||||
/* YX:变位 = 有效位上的位翻转(无效位不参与,避免 IV 通道抖动成"变位") */
|
||||
if ((s_src != NULL) && (s_src->yx_bits != NULL)
|
||||
&& (s_src->yx_valid != NULL)) {
|
||||
uint64_t bits = s_src->yx_bits() & s_src->yx_valid();
|
||||
|
||||
if (s_yx_snap_ok != 0u) {
|
||||
local.yx_changed = bits ^ s_yx_last;
|
||||
}
|
||||
s_yx_last = bits;
|
||||
s_yx_snap_ok = 1u;
|
||||
}
|
||||
|
||||
/* YC:死区比对(仅对"本次有效 且 上次有快照"的点判定) */
|
||||
if ((s_src != NULL) && (s_src->yc_read != NULL)) {
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0u; i < YC_N; i++) {
|
||||
float v;
|
||||
|
||||
if (s_src->yc_read(i, &v) != 0) {
|
||||
continue;
|
||||
}
|
||||
if (((s_yc_have >> i) & 1u) != 0u) {
|
||||
float d = v - s_yc_last[i];
|
||||
|
||||
if (d < 0.0f) {
|
||||
d = -d;
|
||||
}
|
||||
if (d > k_yc[i].deadband) {
|
||||
local.yc_changed |= (uint16_t)(1u << i);
|
||||
}
|
||||
}
|
||||
s_yc_last[i] = v;
|
||||
s_yc_have |= (uint16_t)(1u << i);
|
||||
}
|
||||
}
|
||||
|
||||
if (cov != NULL) {
|
||||
*cov = local;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:遥控两段式(转发数据源;select=1 选择段 / 0 执行段)
|
||||
* 返 回 值:0 接受 / -1 拒绝(调用方回 PN)
|
||||
*/
|
||||
int iec104_pts_yk_exec(uint32_t ioa, uint8_t dpc, uint8_t select) {
|
||||
if ((s_src == NULL) || (s_src->yk_exec == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
return s_src->yk_exec(ioa, dpc, select);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按通讯地址读定值(工程量浮点)
|
||||
* 返 回 值:0 / -1 未知地址
|
||||
*/
|
||||
int iec104_pts_set_read(uint32_t addr, float *out) {
|
||||
if ((s_src == NULL) || (s_src->set_read == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
return s_src->set_read(addr, out);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按通讯地址写定值(select=1 只校验;0 执行写入)
|
||||
* 返 回 值:0 / -1 越界或未知地址
|
||||
*/
|
||||
int iec104_pts_set_write(uint32_t addr, float v, uint8_t select) {
|
||||
if ((s_src == NULL) || (s_src->set_write == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
return s_src->set_write(addr, v, select);
|
||||
}
|
||||
|
||||
/* ============================ 板端默认源 ================================= */
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 遥测 = 基波相量有效值(fAmSQ)
|
||||
* 返 回 值:0 / -1 无 phasor 源(削顶通道/频率未实现)/ -2 未满一周波(NT)
|
||||
*/
|
||||
static int s_src_yc_read(uint8_t idx, float *out) {
|
||||
phasor_t p;
|
||||
int8_t pi;
|
||||
|
||||
if ((idx >= YC_N) || (out == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
pi = s_yc_phasor_idx[idx];
|
||||
if (pi < 0) {
|
||||
return -1; /* 无 phasor 源(削顶通道/频率未实现) */
|
||||
}
|
||||
if (phasor_get((uint8_t)pi, &p) != 0) {
|
||||
return -2; /* 未满一周波:未就绪(NT) */
|
||||
}
|
||||
*out = p.fAmSQ; /* 有效值口径(系数已折算) */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 遥信位图(保护元件动作态 + 定值闭锁)
|
||||
*/
|
||||
static uint64_t s_src_yx_bits(void) {
|
||||
prot_elm_state_t oc1;
|
||||
prot_elm_state_t oc2;
|
||||
prot_elm_state_t oc3;
|
||||
prot_elm_state_t ol;
|
||||
prot_elm_state_t u0;
|
||||
uint64_t bits = 0u;
|
||||
|
||||
prot_elm_state(PE_OC1, &oc1);
|
||||
prot_elm_state(PE_OC2, &oc2);
|
||||
prot_elm_state(PE_OC3, &oc3);
|
||||
prot_elm_state(PE_OL, &ol);
|
||||
prot_elm_state(PE_3U0, &u0);
|
||||
if ((oc1.asserted != 0u) || (oc2.asserted != 0u) || (oc3.asserted != 0u)
|
||||
|| (u0.asserted != 0u)) {
|
||||
bits |= (1uLL << 14u); /* YX15 故障总 */
|
||||
}
|
||||
if (oc1.asserted != 0u) {
|
||||
bits |= (1uLL << 15u); /* YX16 过流 I 段 */
|
||||
}
|
||||
if (oc2.asserted != 0u) {
|
||||
bits |= (1uLL << 16u); /* YX17 过流 II 段 */
|
||||
}
|
||||
if (oc3.asserted != 0u) {
|
||||
bits |= (1uLL << 17u); /* YX18 过流 III 段 */
|
||||
}
|
||||
if (ol.asserted != 0u) {
|
||||
bits |= (1uLL << 22u); /* YX23 过载 */
|
||||
}
|
||||
if (prot_set_locked() != 0u) {
|
||||
bits |= (1uLL << 30u); /* YX31 保护闭锁 */
|
||||
}
|
||||
return bits;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 遥信有效位图(V0.1 = YX15~18/23/31)
|
||||
*/
|
||||
static uint64_t s_src_yx_valid(void) {
|
||||
return YX_VALID_MASK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 遥控(V0.1 仅 YK11 复归 → prot_elm_request_reset)
|
||||
* 返 回 值:0 / -1 无出口硬件或未知 IOA(YK1~4/12~14 负向确认)
|
||||
* 函数说明:本回调跑在 iec104Task 上下文,**不得**直接 prot_elm_init()
|
||||
* (与 protectTask 的 eval 竞态)→ 落请求,下一拍 eval 头部消费(B5)。
|
||||
*/
|
||||
static int s_src_yk_exec(uint32_t ioa, uint8_t dpc, uint8_t select) {
|
||||
(void)dpc; /* 本期仅 YK11 与双点值无关(复归) */
|
||||
switch (ioa) {
|
||||
case 0x600Au: /* YK11 信号复归 → 元件状态复位(异步一拍) */
|
||||
if (select == 0u) {
|
||||
prot_elm_request_reset();
|
||||
}
|
||||
return 0;
|
||||
default:
|
||||
return -1; /* 其余 YK:无出口硬件/无对应能力 → 拒绝 */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 定值读(扫 g_prot_set_meta 通讯地址映射)
|
||||
* 返 回 值:0 / -1 未知地址
|
||||
*/
|
||||
static int s_src_set_read(uint32_t addr, float *out) {
|
||||
uint32_t i;
|
||||
|
||||
if (out == NULL) {
|
||||
return -1;
|
||||
}
|
||||
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
|
||||
if (g_prot_set_meta[i].comm_addr == (uint16_t)addr) {
|
||||
*out = prot_set_get_f((prot_set_id_t)i);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 定值写(Q10 范围校验;执行段 prot_set_set + seal)
|
||||
* 返 回 值:0 / -1 越界或未知地址
|
||||
*/
|
||||
static int s_src_set_write(uint32_t addr, float v, uint8_t select) {
|
||||
uint32_t i;
|
||||
int32_t q10;
|
||||
|
||||
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
|
||||
const prot_set_meta_t *meta = &g_prot_set_meta[i];
|
||||
|
||||
if (meta->comm_addr != (uint16_t)addr) {
|
||||
continue;
|
||||
}
|
||||
q10 = PROT_SET_Q10(v);
|
||||
if ((q10 < meta->min) || (q10 > meta->max)) {
|
||||
return -1; /* 越界:选择段即拒(执行段同样拒) */
|
||||
}
|
||||
if (select != 0u) {
|
||||
return 0; /* 选择段 = 只做范围校验 */
|
||||
}
|
||||
if (prot_set_set((prot_set_id_t)i, q10) != 0) {
|
||||
return -1;
|
||||
}
|
||||
prot_set_seal(); /* 执行段:写入 + 封存校验和(L5-1) */
|
||||
return 0;
|
||||
}
|
||||
return -1; /* 未知地址 */
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 点表与数据源适配(纯逻辑 + 源注入)
|
||||
* 模块功能:104 点表(遥测 YC / 遥信 YX / 遥控 YK / 定值)的定义、读取与变化检测
|
||||
* (COV:遥信变位 + 遥测死区)。**数据源经 vtable 注入**:板端绑
|
||||
* phasor/prot_elm/prot_set 真实源;主机单测绑假源 → 协议栈全链
|
||||
* (asdu+pts+link)可离线跑(登记簿 A-4 约束 1)。
|
||||
* 点表口径(docs/算法/继电保护功能复现规划.md §1.5,验收 = 省电科院点表):
|
||||
* - YC:IOA 0x4011~0x4018 + 0x401d(频率),短浮点、系数 1;
|
||||
* ⚠ V0.1 有效通道仅 UA/UN/IA(TPAFE 三路,§1.6)——其余点**存在但置 IV**
|
||||
* (无效位),频率随 L1-7 落地前同样置 IV;
|
||||
* - YX:IOA 1~33(§1.5 表(3));双点 = 开关位置类(YX1~4,本期无硬件 → IV),
|
||||
* 保护动作类 YX15~23 映射 prot_elm 状态,闭锁类 YX31~33 映射 prot_set;
|
||||
* - YK:IOA 0x6000+(§1.5 表(3));V0.1 无出口硬件(规划 R7)——除
|
||||
* YK11 信号复归(→ prot_elm_init)外一律否定确认(COT|PN),如实回;
|
||||
* - 定值:IOA = prot_set 元数据 comm_addr(0x8820 基址 + 告警段 0x8027~0x802A),
|
||||
* 读经 C_RD、写经 C_SE_NC(选择/执行两段,执行完成统一 seal 校验和);
|
||||
* - 固有参数 8001H~800AH:本期**不实现**(字符串类无法用短浮点上送),
|
||||
* 按未知 IOA(COT 14 + EVI)应答,登记为待办(设计文档)。
|
||||
* 线程约定:init/bind 内核启动前或建服务前;cov_check/read 仅 104 服务任务上下文。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#ifndef IEC104_PTS_H
|
||||
#define IEC104_PTS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* ---------------- 数据源 vtable(NULL 项 = 该能力不可用 → 按无效/拒绝处理) -- */
|
||||
typedef struct {
|
||||
/* 遥测读:idx = YC 表下标;out = 工程量浮点。
|
||||
* 返回 0 有效 / -1 无效(发 IV)/ -2 数据未就绪(发 NT,如相量未满一周波) */
|
||||
int (*yc_read)(uint8_t idx, float *out);
|
||||
/* 遥信位图:bit i = YX 表下标 i 的"合/动作"态(双点按 ON 计) */
|
||||
uint64_t (*yx_bits)(void);
|
||||
/* 遥信有效位图:bit i = 1 该点有效;0 → 发 IV(无硬件/未实现) */
|
||||
uint64_t (*yx_valid)(void);
|
||||
/* 遥控:select=1 选择段 / 0 执行段(dpc 已剥 S 位)。返回 0 接受 / -1 拒绝 */
|
||||
int (*yk_exec)(uint32_t ioa, uint8_t dpc, uint8_t select);
|
||||
/* 定值读:addr = 通讯地址;返回 0 存在 / -1 未知地址 */
|
||||
int (*set_read)(uint32_t addr, float *out);
|
||||
/* 定值写:select=1 预置校验(范围)/ 0 执行写入。返回 0 / -1 越界或未知 */
|
||||
int (*set_write)(uint32_t addr, float v, uint8_t select);
|
||||
} iec104_pts_src_t;
|
||||
|
||||
/* ---------------- 变化检测结果(cov_check 输出) ---------------- */
|
||||
typedef struct {
|
||||
uint64_t yx_changed; /* bit i = YX 下标 i 变位(相对上次快照) */
|
||||
uint16_t yc_changed; /* bit i = YC 下标 i 越过死区(相对上次快照) */
|
||||
} iec104_pts_cov_t;
|
||||
|
||||
/*
|
||||
* 函数功能:点表层初始化(清快照、绑板端默认源)
|
||||
* 限定条件:内核启动前或 104 服务启动前,调一次
|
||||
*/
|
||||
void iec104_pts_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:覆盖数据源(主机单测注假源)
|
||||
* 入口参数:src - vtable(浅拷贝,调用方保生命周期);NULL = 恢复板端默认源
|
||||
*/
|
||||
void iec104_pts_bind_src(const iec104_pts_src_t *src);
|
||||
|
||||
/* ---------------- YC ---------------- */
|
||||
uint8_t iec104_pts_yc_count(void);
|
||||
uint32_t iec104_pts_yc_ioa(uint8_t idx);
|
||||
/* 读 YC:v 有效值(无效时为 0);qds 输出品质位(IV/NT 等)。返回 0/无效-1 */
|
||||
int iec104_pts_yc_read(uint8_t idx, float *v, uint8_t *qds);
|
||||
|
||||
/* ---------------- YX ---------------- */
|
||||
uint8_t iec104_pts_yx_count(void);
|
||||
uint32_t iec104_pts_yx_ioa(uint8_t idx);
|
||||
uint8_t iec104_pts_yx_is_dp(uint8_t idx); /* 1 = 双点(M_DP) */
|
||||
/* 读 YX:out = DIQ/SIQ 值(含 IV)。 */
|
||||
void iec104_pts_yx_read(uint8_t idx, uint8_t *out);
|
||||
|
||||
/* ---------------- COV(每 poll 一次) ---------------- */
|
||||
/*
|
||||
* 函数功能:读全表快照并与上次比对(变位 + 死区),输出变化集
|
||||
* 入口参数:cov - 输出(可为 NULL = 只刷快照不取变化)
|
||||
* 函数说明:首次调用只建快照、变化集全 0(突发由初始化/总召覆盖)。
|
||||
* 死区 = 点表内置绝对值(§1.5 运行参数 8020H 段本期取内置默认)。
|
||||
*/
|
||||
void iec104_pts_cov_check(iec104_pts_cov_t *cov);
|
||||
|
||||
/* ---------------- 遥控 ---------------- */
|
||||
/* 遥控两段式(转发 src->yk_exec;无源 = 一律拒绝)。返回 0 接受 / -1 拒绝 */
|
||||
int iec104_pts_yk_exec(uint32_t ioa, uint8_t dpc, uint8_t select);
|
||||
|
||||
/* ---------------- 定值 ---------------- */
|
||||
/* 按通讯地址读定值(转发 src->set_read)。返回 0 / -1 未知地址 */
|
||||
int iec104_pts_set_read(uint32_t addr, float *out);
|
||||
/* 按通讯地址写定值(两段)。返回 0 / -1 */
|
||||
int iec104_pts_set_write(uint32_t addr, float v, uint8_t select);
|
||||
|
||||
#endif /* IEC104_PTS_H */
|
||||
+10
-2
@@ -62,6 +62,10 @@ extern osThreadId_t timeSyncTaskHandle;
|
||||
extern const osThreadAttr_t timeSyncTask_attributes;
|
||||
extern osThreadId_t adcTaskHandle;
|
||||
extern const osThreadAttr_t adcTask_attributes;
|
||||
extern osThreadId_t iec104TaskHandle;
|
||||
extern const osThreadAttr_t iec104Task_attributes;
|
||||
extern osThreadId_t protectTaskHandle;
|
||||
extern const osThreadAttr_t protectTask_attributes;
|
||||
|
||||
/* Private defines -----------------------------------------------------------*/
|
||||
/* 栈水位观测节奏:首次报告须晚于 bring-up 深链(mount/mkfs/探针/cfg/syslog),
|
||||
@@ -69,9 +73,11 @@ extern const osThreadAttr_t adcTask_attributes;
|
||||
#define STACK_PROBE_FIRST_MS 5000u
|
||||
#define STACK_PROBE_PERIOD_MS 2000u
|
||||
/* S11(2026-10-03):堆低水位告警阈值(B)。
|
||||
* 依据:2026-10-03 上板两次开机的 min_ever 实测下限 3,712 B(configTOTAL_HEAP_SIZE
|
||||
* 依据:2026-10-03 上板两次开机的 min_ever 实测下限 3,712 B(当时 configTOTAL_HEAP_SIZE
|
||||
* = 48KB)——低于此值说明堆余量 <8%,新增任务/缓冲随时可能分配失败(隐性风险)。
|
||||
* 阈值取 3,072 B 留出运行间 ~600B 波动余量,避免把正常波动误报为故障。 */
|
||||
* 阈值取 3,072 B 留出运行间 ~600B 波动余量,避免把正常波动误报为故障。
|
||||
* 2026-10-12(B5):堆扩至 56KB(.ioc configTOTAL_HEAP_SIZE=57344,CCM 腾给
|
||||
* protectTask/iec104Task 栈),阈值**不随堆缩放**——它是绝对字节安全线。 */
|
||||
#define HEAP_LOW_WATER_B 3072u
|
||||
/* E-31 自愈策略(2026-09-21 审计 P0 修复):启动期存储挂载失败后,
|
||||
* 主循环按 2s 周期重跑完整 bring-up 的次数上限;耗尽则维持无存储降级
|
||||
@@ -93,6 +99,8 @@ static const stack_probe_task_t s_stack_probe[] = {
|
||||
{ "ui", &uiTaskHandle, &uiTask_attributes },
|
||||
{ "timesync", &timeSyncTaskHandle, &timeSyncTask_attributes },
|
||||
{ "adc", &adcTaskHandle, &adcTask_attributes },
|
||||
{ "iec104", &iec104TaskHandle, &iec104Task_attributes },
|
||||
{ "protect", &protectTaskHandle, &protectTask_attributes },
|
||||
};
|
||||
|
||||
#define STACK_PROBE_COUNT (sizeof(s_stack_probe) / sizeof(s_stack_probe[0]))
|
||||
|
||||
@@ -0,0 +1,295 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 被控站服务(板端胶水任务)
|
||||
* 模块功能:2404 监听 + 单连接 socket 收发 + 把字节流喂给 iec104_link 纯逻辑
|
||||
* 层;对时经 sys_clock_set_unix 落墙钟;数据源走 iec104_pts 默认源
|
||||
* (phasor / prot_elm / prot_set)。协议语义全部在 App/protocol/iec104,
|
||||
* 本文件只做 I/O 编排(A-4 约束 1)。
|
||||
* 线程约定:iec104Task 独占(NET_READY 屏障后循环,100ms 节拍 + 看门狗心跳)。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#include "iec104_serv.h"
|
||||
|
||||
#define DBG_LINE_BUF_MAX 128U /* 深链模块日志缓冲收紧(time_sync 先例) */
|
||||
#define DBG_TAG "[I104]"
|
||||
|
||||
#include "iec104_link.h"
|
||||
#include "iec104_pts.h"
|
||||
#include "net_socket.h"
|
||||
#include "sys_clock.h"
|
||||
#include "sys_events.h"
|
||||
#include "task_watchdog.h"
|
||||
#include "dbg_log.h"
|
||||
|
||||
#include "cmsis_os2.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* 服务周期:COV 延迟 ≤100ms(远小于 SOE 5000ms 验收线);心跳间隔同值 */
|
||||
#define IEC104_POLL_MS 100u
|
||||
/* 接收搬运缓冲(静态,不入栈;link 层内部另有 512B 重组缓冲) */
|
||||
#define IEC104_RX_SZ 128u
|
||||
/* 兜底节拍:门控判"无数据"时每 32 轮无条件收一次(ui_net_sink RM 先例,
|
||||
* 防 RECV_OK 中断丢失把数据永久藏住);须为 2 的幂 */
|
||||
#define IEC104_LIVENESS_DIV 32u
|
||||
/* 连续发送失败阈值:同 ui_net_sink E-04(短写=对端收坏帧) */
|
||||
#define IEC104_SEND_FAIL_MAX 3u
|
||||
|
||||
/* ---------------- 服务状态(iec104Task 独占) ---------------- */
|
||||
static iec104_link_t s_link;
|
||||
static int s_listen = -1;
|
||||
static int s_client = -1; /* 单连接模型:MCU 上即 s_listen */
|
||||
static uint32_t s_gen; /* 网络代次(重建自愈,ui_net_sink 先例) */
|
||||
static uint8_t s_rx[IEC104_RX_SZ];
|
||||
static uint8_t s_liveness_ctr;
|
||||
static uint8_t s_send_fail_run;
|
||||
static uint8_t s_conn_dead; /* 发送连败 → 本轮 link 调用返回后拆链 */
|
||||
|
||||
/* 前向声明(cfg 初始化引用) */
|
||||
static void s_send_apdu(void *ctx, const uint8_t *apdu, uint16_t len);
|
||||
static int s_clock_sync(void *ctx, uint64_t epoch_ms);
|
||||
|
||||
/* V0.1 公共地址固定 1(点表口径;可配置项挂二期) */
|
||||
static const iec104_link_cfg_t s_cfg = { 1u, s_send_apdu, s_clock_sync };
|
||||
|
||||
/*
|
||||
* 函数功能:link 层发送回调——经 net_send 串行 RPC 发出一个 APDU
|
||||
* 入口参数:ctx 未用;apdu/len = 完整 APCI 帧
|
||||
* 返回值:无
|
||||
* 限定条件:s_client >= 0(在 link 调用栈内执行,即本任务上下文)
|
||||
* 函数说明:短写/失败累计(E-04 先例);连续 3 次 → 置 s_conn_dead,
|
||||
* 由 s_service_link 在 link 调用返回后统一拆链(严禁在回调里拆)
|
||||
*/
|
||||
static void s_send_apdu(void *ctx, const uint8_t *apdu, uint16_t len) {
|
||||
int ret;
|
||||
|
||||
(void)ctx;
|
||||
if (s_client < 0) {
|
||||
return;
|
||||
}
|
||||
ret = net_send(s_client, apdu, (int)len, 0);
|
||||
if (ret == (int)len) {
|
||||
s_send_fail_run = 0u;
|
||||
return;
|
||||
}
|
||||
if (s_send_fail_run < 255u) {
|
||||
s_send_fail_run++;
|
||||
}
|
||||
DBG_ERROR("i104 send short: ret=%d/%u run=%u", ret, (unsigned)len,
|
||||
(unsigned)s_send_fail_run);
|
||||
if (s_send_fail_run >= IEC104_SEND_FAIL_MAX) {
|
||||
s_conn_dead = 1u;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:link 层对时回调——epoch 毫秒落系统墙钟(含 SD2506 RTC 写回)
|
||||
* 入口参数:ctx 未用;epoch_ms = C_CS 解析出的 UTC 时刻
|
||||
* 返回值:0 成功 / -1 失败(link 层回 actcon+PN)
|
||||
* 函数说明:uint32 秒域上限 2106-02-07,越界显式拒绝(CP56 域到 2127)
|
||||
*/
|
||||
static int s_clock_sync(void *ctx, uint64_t epoch_ms) {
|
||||
uint64_t secs = epoch_ms / 1000ULL;
|
||||
|
||||
(void)ctx;
|
||||
if (secs > 0xFFFFFFFFULL) {
|
||||
return -1;
|
||||
}
|
||||
return sys_clock_set_unix((uint32_t)secs);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:创建并监听 2404(socket+bind+listen,抄 ui_net_sink bind_listen)
|
||||
* 返回值:0 成功(s_listen 写回)/ -1 失败
|
||||
* 限定条件:net 子系统可用(NET_READY 屏障后)
|
||||
*/
|
||||
static int s_bind_listen(void) {
|
||||
int ls;
|
||||
struct net_sockaddr_in addr;
|
||||
|
||||
ls = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
if (ls < 0) {
|
||||
return -1;
|
||||
}
|
||||
(void)memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = NET_AF_INET;
|
||||
addr.sin_port = net_htons(IEC104_SERV_PORT);
|
||||
addr.sin_addr.s_addr = NET_INADDR_ANY;
|
||||
if (net_bind(ls, (struct net_sockaddr *)&addr, (int)sizeof(addr)) < 0) {
|
||||
net_close(ls);
|
||||
return -1;
|
||||
}
|
||||
if (net_listen(ls) < 0) {
|
||||
net_close(ls);
|
||||
return -1;
|
||||
}
|
||||
s_listen = ls;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:主动断开当前 104 连接并重建监听(协议故障/t1 超时/发送连败)
|
||||
* 入口参数:why = 日志原因串(ASCII)
|
||||
* 返回值:无
|
||||
* 函数说明:与 EOF 路径不同——协议违例时主站不会自行断开,必须本端 close
|
||||
* 促其重连;单连接模型下 client 即 listen,close 后统一重建
|
||||
* (time_sync 会话收尾先例,非芯片侧 auto_relisten 风暴路径)
|
||||
*/
|
||||
static void s_drop_conn(const char *why) {
|
||||
DBG_ERROR("i104 drop: %s", why);
|
||||
if (s_client >= 0) {
|
||||
net_close(s_client);
|
||||
}
|
||||
s_client = -1;
|
||||
s_listen = -1;
|
||||
iec104_link_close(&s_link);
|
||||
s_send_fail_run = 0u;
|
||||
s_conn_dead = 0u;
|
||||
s_liveness_ctr = 0u;
|
||||
if (s_bind_listen() < 0) {
|
||||
s_listen = -1; /* 主循环下轮重试 */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:link 层节拍服务 + 故障检测(poll 后查 IDLE 跳变/发送连败)
|
||||
* 入口参数:now = 注入时基(tick ms)
|
||||
* 返回值:无
|
||||
*/
|
||||
static void s_service_link(uint32_t now) {
|
||||
iec104_link_poll(&s_link, now);
|
||||
if (s_conn_dead != 0u) {
|
||||
s_drop_conn("send fail");
|
||||
return;
|
||||
}
|
||||
if (iec104_link_state(&s_link) == IEC104_ST_IDLE) {
|
||||
s_drop_conn("protocol fault");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:单次服务循环(代次自愈 → accept → 门控收 → 喂 link → 节拍)
|
||||
* 返回值:无
|
||||
* 限定条件:NET_READY 已置位;s_cfg/s_link 已 init
|
||||
* 函数说明:门控(net_can_accept/net_can_read 纯 RAM 读)+ 兜底节拍照
|
||||
* ui_net_sink I-1/I-2;EOF 时 client==listen 严禁 close(net 层
|
||||
* 已 auto_relisten,close 会连监听一起毁)
|
||||
*/
|
||||
static void s_serv_poll(void) {
|
||||
uint32_t now = osKernelGetTickCount();
|
||||
int n;
|
||||
|
||||
/* 网络重建(代次变化):旧监听/客户端随芯片复位失效 → 重建 */
|
||||
if (net_get_generation() != s_gen) {
|
||||
if ((s_client >= 0) && (s_client != s_listen)) {
|
||||
net_close(s_client);
|
||||
}
|
||||
if (s_listen >= 0) {
|
||||
net_close(s_listen);
|
||||
s_listen = -1;
|
||||
}
|
||||
s_client = -1;
|
||||
iec104_link_close(&s_link);
|
||||
s_send_fail_run = 0u;
|
||||
s_conn_dead = 0u;
|
||||
s_liveness_ctr = 0u;
|
||||
s_gen = net_get_generation();
|
||||
(void)s_bind_listen();
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_listen < 0) {
|
||||
(void)s_bind_listen(); /* 启动失败/被毁:重试 */
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_client < 0) {
|
||||
int c;
|
||||
|
||||
if (net_can_accept(s_listen) == 0) {
|
||||
if (net_get_sock(s_listen) == NULL) {
|
||||
s_listen = -1; /* 控制块消失:交回上方重建 */
|
||||
}
|
||||
return;
|
||||
}
|
||||
c = net_accept(s_listen, NULL, NULL);
|
||||
if (c >= 0) {
|
||||
s_client = c;
|
||||
s_send_fail_run = 0u;
|
||||
s_conn_dead = 0u;
|
||||
s_liveness_ctr = 0u;
|
||||
iec104_link_open(&s_link, now);
|
||||
DBG_INFO("i104: accepted fd=%d", c);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* 门控 + 兜底:无数据多数轮直接走 link 节拍;每 32 轮无条件收一次 */
|
||||
if (net_can_read(s_client) == 0) {
|
||||
if (((++s_liveness_ctr) & (IEC104_LIVENESS_DIV - 1u)) != 0u) {
|
||||
s_service_link(now);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
s_liveness_ctr = 0u;
|
||||
}
|
||||
|
||||
n = net_recv(s_client, s_rx, (int)sizeof(s_rx), NET_MSG_DONTWAIT);
|
||||
if (n > 0) {
|
||||
iec104_link_on_rx(&s_link, s_rx, (uint16_t)n, now);
|
||||
} else if (n == 0) {
|
||||
/* 对端关闭:net 层已 auto_relisten;client==listen 时严禁 close */
|
||||
if (s_client != s_listen) {
|
||||
net_close(s_client); /* 模拟器多连接后端:数据通道独立 */
|
||||
}
|
||||
s_client = -1;
|
||||
s_liveness_ctr = 0u;
|
||||
iec104_link_close(&s_link);
|
||||
if ((net_get_sock(s_listen) == NULL) && (s_bind_listen() < 0)) {
|
||||
s_listen = -1;
|
||||
}
|
||||
return;
|
||||
} else if (net_get_errno() != NET_ERR_WOULDBLOCK) {
|
||||
s_drop_conn("recv err");
|
||||
return;
|
||||
}
|
||||
|
||||
s_service_link(now);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:iec104Task 任务体(freertos.c USER CODE 区创建)
|
||||
* 入口参数:argument 未用
|
||||
* 返回值:无(不返回)
|
||||
* 函数说明:NET_READY 屏障(100ms 分片 + 每片喂心跳,timeSyncTask 先例)→
|
||||
* 点表/链路 init + 建监听 → 100ms 服务循环(每轮喂看门狗)
|
||||
*/
|
||||
void iec104_serv_task(void *argument) {
|
||||
(void)argument;
|
||||
|
||||
if (sys_evtHandle != NULL) {
|
||||
for (;;) {
|
||||
uint32_t r = osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY,
|
||||
osFlagsWaitAny | osFlagsNoClear, 100u);
|
||||
if ((r & osFlagsError) == 0u) {
|
||||
break; /* 就绪位是状态位(NoClear),不消费 */
|
||||
}
|
||||
task_wd_beat(TASK_WD_IEC104);
|
||||
}
|
||||
}
|
||||
|
||||
iec104_pts_init();
|
||||
iec104_link_init(&s_link, &s_cfg, NULL);
|
||||
s_gen = net_get_generation();
|
||||
if (s_bind_listen() < 0) {
|
||||
DBG_ERROR("i104: bind port %u FAIL, retry in loop",
|
||||
(unsigned)IEC104_SERV_PORT);
|
||||
} else {
|
||||
DBG_INFO("i104: listening on %u", (unsigned)IEC104_SERV_PORT);
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
task_wd_beat(TASK_WD_IEC104);
|
||||
s_serv_poll();
|
||||
osDelay(IEC104_POLL_MS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 被控站服务(对外接口)
|
||||
* 模块功能:声明 iec104Task 任务体与监听端口;协议实现在 App/protocol/iec104/,
|
||||
* 本接口仅供 Src/freertos.c 创建任务使用。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#ifndef IEC104_SERV_H
|
||||
#define IEC104_SERV_H
|
||||
|
||||
/* IEC 60870-5-104 被控站监听端口(DL/T 634.5104 标准端口) */
|
||||
#define IEC104_SERV_PORT 2404u
|
||||
|
||||
/*
|
||||
* 函数功能:iec104Task 任务体(在 Src/freertos.c USER CODE 区 osThreadNew 创建)
|
||||
* 入口参数:argument - 未用
|
||||
* 返回值:无(任务不返回)
|
||||
*/
|
||||
void iec104_serv_task(void *argument);
|
||||
|
||||
#endif /* IEC104_SERV_H */
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* 模块名称:保护任务(L3 板端接线,B5)——实现
|
||||
* 模块功能:见 protect_task.h。每拍三件事:
|
||||
* ① L5-4 采样故障闭锁:adc_health_flags() 电平位命中 → 元件状态
|
||||
* 复位(防陈旧判据)+ 本拍不 eval;
|
||||
* ② prot_set_selfcheck()(L5-1 定值完整性巡检,31 项 sum32,开销
|
||||
* 可忽略;失配 → prot_set 内部闭锁,eval 自动复位全元件);
|
||||
* ③ prot_elm_run()(phasor rms → 判据步进)+ 动作位图边沿日志。
|
||||
* 定值/元件初始化在 app_main_init(内核前,B5);本任务只消费。
|
||||
* 线程约定:prot_elm_run / prot_set_selfcheck 仅本任务上下文(prot_elm.h);
|
||||
* 跨上下文复归(104 YK11)经 prot_elm_request_reset 落请求。
|
||||
* 创建日期:2026-10-12(B5)
|
||||
*/
|
||||
#include "protect_task.h"
|
||||
|
||||
#include "cmsis_os2.h"
|
||||
|
||||
#define DBG_TAG "[PROT]"
|
||||
#define DBG_LINE_BUF_MAX 128U
|
||||
#include "dbg_log.h"
|
||||
|
||||
#include "adc_health.h"
|
||||
#include "prot_elm.h"
|
||||
#include "prot_set.h"
|
||||
#include "task_watchdog.h"
|
||||
|
||||
/* L5-4 采样故障电平闭锁位:stall(可自愈)+ backward(锁存)都须停判据。
|
||||
* 锁存类(FRSTDATA/HW_ERR 等)不在此列——它们是一次性事件痕迹,不构成
|
||||
* "当前测量不可信";持续 stall/backward 才是。 */
|
||||
#define PROT_HF_LOCK_MASK (ADC_HF_TPAFE_STALL | ADC_HF_STM32_STALL | ADC_HF_SEQ_BACKWARD)
|
||||
|
||||
static uint32_t s_prev_active; /* 上拍动作位图(边沿日志用) */
|
||||
static uint8_t s_prev_hf_lock; /* 上拍采样闭锁态(边沿日志用) */
|
||||
|
||||
/*
|
||||
* 函数功能:动作位图变化沿日志(逐变化元件打名字与状态)
|
||||
* 入口参数:active - 本拍 prot_elm_active_mask()
|
||||
* 函数说明:V0.1 无出口硬件(规划 R7),动作 = 遥信 + 日志;SOE 落盘随 L4。
|
||||
*/
|
||||
static void s_log_active_edge(uint32_t active) {
|
||||
uint32_t chg = active ^ s_prev_active;
|
||||
uint32_t i;
|
||||
|
||||
for (i = 0u; (i < (uint32_t)PE_COUNT) && (chg != 0u); i++) {
|
||||
const uint32_t bit = 1u << i;
|
||||
|
||||
if ((chg & bit) != 0u) {
|
||||
prot_elm_state_t st;
|
||||
|
||||
chg &= ~bit;
|
||||
prot_elm_state((prot_elm_id_t)i, &st);
|
||||
DBG_INFO("elm %s asserted=%u picked=%u",
|
||||
prot_elm_name((prot_elm_id_t)i),
|
||||
(unsigned)st.asserted, (unsigned)st.picked);
|
||||
}
|
||||
}
|
||||
s_prev_active = active;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:protectTask 线程入口
|
||||
* 入口参数:argument - 未用
|
||||
* 返 回 值:无(不返回)
|
||||
* 函数说明:无就绪屏障(判据只依赖 phasor 使用区,未就绪时 prot_elm_run
|
||||
* 整拍自跳;采样闭锁另有 adc_health 电平位兜底)→ 上电即跑。
|
||||
* 每拍喂看门狗(TASK_WD_PROTECT,产品 required_mask 0x1F7)。
|
||||
*/
|
||||
void protect_task(void *argument) {
|
||||
(void)argument;
|
||||
|
||||
DBG_INFO("protect task start: cycle=%ums elm=%u",
|
||||
(unsigned)PROTECT_TASK_CYCLE_MS, (unsigned)PE_COUNT);
|
||||
|
||||
for (;;) {
|
||||
uint32_t now = osKernelGetTickCount();
|
||||
uint32_t hf = adc_health_flags();
|
||||
|
||||
if ((hf & PROT_HF_LOCK_MASK) != 0u) {
|
||||
/* L5-4:测量链不可信 → 复位元件(本任务上下文,直接 init 合法)
|
||||
* 并跳过判据;stall 自愈后下一拍自动恢复 */
|
||||
if (s_prev_hf_lock == 0u) {
|
||||
DBG_ERROR("sampling FAULT -> protection blocked (hf=0x%08lX)",
|
||||
(unsigned long)(hf & PROT_HF_LOCK_MASK));
|
||||
}
|
||||
s_prev_hf_lock = 1u;
|
||||
prot_elm_init();
|
||||
} else {
|
||||
if (s_prev_hf_lock != 0u) {
|
||||
DBG_INFO("sampling recovered -> protection resumed");
|
||||
s_prev_hf_lock = 0u;
|
||||
}
|
||||
/* L5-1 巡检:定值区与封存校验和不符 → prot_set 闭锁(下一拍起
|
||||
* eval 自动复位全元件)。⚠ 与 104 定值写(iec104Task set+seal)
|
||||
* 的竞态窗口为 µs 级:selfcheck 误锁后同一次写的 seal 即清锁
|
||||
* (prot_set.c),V0.1 如实登记,P4 定值持久化时收口。 */
|
||||
(void)prot_set_selfcheck();
|
||||
prot_elm_run(now);
|
||||
}
|
||||
|
||||
s_log_active_edge(prot_elm_active_mask());
|
||||
|
||||
task_wd_beat(TASK_WD_PROTECT);
|
||||
osDelay(PROTECT_TASK_CYCLE_MS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 模块名称:保护任务(L3 板端接线,B5)
|
||||
* 模块功能:protectTask 线程入口——以固定节拍驱动保护判据
|
||||
* (`prot_elm_run()`:phasor rms → 元件步进),并落实 L5-4 采样
|
||||
* 故障闭锁(`adc_health_flags()` 电平位 → 元件状态复位 + 本拍不判据)。
|
||||
* 判据本体在 `App/algo/prot_elm.c`(纯逻辑,L0.5 已覆盖),本文件
|
||||
* 只做消费/编排(AGENTS 目录地图:task 不装算法)。
|
||||
* 线程约定:仅本任务调 prot_elm_run / prot_set_selfcheck(prot_elm.h 线程约定);
|
||||
* prot_set 写在 P4 前无第二写者(104 定值写经 pts→prot_set,见注)。
|
||||
* 创建日期:2026-10-12(B5)
|
||||
*/
|
||||
#ifndef PROTECT_TASK_H
|
||||
#define PROTECT_TASK_H
|
||||
|
||||
/* 判据节拍(ms):V0.1 元件吃 50 Hz 基波 rms(phasor 每工频周更新)→ 20 ms
|
||||
* = 一个工频周;延时精度由 prot_elm 的 now_ms 差值法保证,与节拍无关。
|
||||
* 规划 §4 的"1 ms 高优先级"是参考系统瞬时值元件口径,随 P2 瞬时值元件再评。 */
|
||||
#define PROTECT_TASK_CYCLE_MS 20u
|
||||
|
||||
/*
|
||||
* 函数功能:protectTask 线程入口(freertos.c USER CODE 区创建)
|
||||
*/
|
||||
void protect_task(void *argument);
|
||||
|
||||
#endif /* PROTECT_TASK_H */
|
||||
@@ -191,7 +191,9 @@ static uint32_t wd_required_mask(void) {
|
||||
| (1u << (uint32_t)TASK_WD_FTP)
|
||||
| (1u << (uint32_t)TASK_WD_UI)
|
||||
| (1u << (uint32_t)TASK_WD_TIMESYNC)
|
||||
| (1u << (uint32_t)TASK_WD_ADC); /* adcTask 不依赖任何屏障,任何构建都置位 */
|
||||
| (1u << (uint32_t)TASK_WD_ADC) /* adcTask 不依赖任何屏障,任何构建都置位 */
|
||||
| (1u << (uint32_t)TASK_WD_IEC104) /* iec104Task 同 adcTask:任何构建都创建 */
|
||||
| (1u << (uint32_t)TASK_WD_PROTECT);/* protectTask 同:判据节拍必须心跳 */
|
||||
|
||||
#if defined(ENABLE_HW_LAYER_TESTS)
|
||||
/* 见函数说明:CH395F 硬件层测试构建 = default + 屏障等待者(等待分片喂位)
|
||||
@@ -200,7 +202,9 @@ static uint32_t wd_required_mask(void) {
|
||||
| (1u << (uint32_t)TASK_WD_FTP)
|
||||
| (1u << (uint32_t)TASK_WD_UI)
|
||||
| (1u << (uint32_t)TASK_WD_TIMESYNC)
|
||||
| (1u << (uint32_t)TASK_WD_ADC);
|
||||
| (1u << (uint32_t)TASK_WD_ADC)
|
||||
| (1u << (uint32_t)TASK_WD_IEC104)
|
||||
| (1u << (uint32_t)TASK_WD_PROTECT);
|
||||
#endif
|
||||
|
||||
return m;
|
||||
|
||||
@@ -31,6 +31,8 @@ typedef enum {
|
||||
TASK_WD_UI = 4,
|
||||
TASK_WD_TIMESYNC = 5,
|
||||
TASK_WD_ADC = 6, /* adcTask:1ms 采样节拍消费者(B1,2026-10-10) */
|
||||
TASK_WD_IEC104 = 7, /* iec104Task:104 上行服务(B4,2026-10-12) */
|
||||
TASK_WD_PROTECT = 8, /* protectTask:保护判据节拍(B5,2026-10-12) */
|
||||
TASK_WD_ID_MAX
|
||||
} task_wd_id_t;
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
#define configTICK_RATE_HZ ((TickType_t)1000)
|
||||
#define configMAX_PRIORITIES ( 56 )
|
||||
#define configMINIMAL_STACK_SIZE ((uint16_t)128)
|
||||
#define configTOTAL_HEAP_SIZE ((size_t)49152)
|
||||
#define configTOTAL_HEAP_SIZE ((size_t)57344)
|
||||
#define configMAX_TASK_NAME_LEN ( 16 )
|
||||
#define configUSE_TRACE_FACILITY 1
|
||||
#define configUSE_16_BIT_TICKS 0
|
||||
|
||||
@@ -337,7 +337,7 @@
|
||||
<MiscControls>--diag_suppress=1,68,111,188,191,546,1293</MiscControls>
|
||||
<Define>USE_HAL_DRIVER,STM32F407xx,NDEBUG</Define>
|
||||
<Undefine />
|
||||
<IncludePath>../Inc;../Drivers/STM32F4xx_HAL_Driver/Inc;../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy;../Drivers/CMSIS/Device/ST/STM32F4xx/Include;../Drivers/CMSIS/Include;../Drivers/BSP;../Drivers/BSP/CH395F;../Drivers/BSP/GD5F2GQ5UE;../Drivers/BSP/TPAFE5160;../Drivers/BSP/SD2506;../Drivers/BSP/RS485;../Drivers/BSP/NET;../Drivers/BSP/NET/lftpd;../Drivers/BSP/NETCFG;../App;../App/app;../App/algo;../App/cfg;../App/fs;../App/log;../App/sync;../App/task;../App/util;../Middlewares/Third_Party/FreeRTOS/Source/include;../Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2;../Middlewares/Third_Party/FreeRTOS/Source/portable/RVDS/ARM_CM4F;../Lib/dhara;../Lib/FatFs;../test/board;../Lib/lvgl;../Drivers/BSP/UI;../Lib</IncludePath>
|
||||
<IncludePath>../Inc;../Drivers/STM32F4xx_HAL_Driver/Inc;../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy;../Drivers/CMSIS/Device/ST/STM32F4xx/Include;../Drivers/CMSIS/Include;../Drivers/BSP;../Drivers/BSP/CH395F;../Drivers/BSP/GD5F2GQ5UE;../Drivers/BSP/TPAFE5160;../Drivers/BSP/SD2506;../Drivers/BSP/RS485;../Drivers/BSP/NET;../Drivers/BSP/NET/lftpd;../Drivers/BSP/NETCFG;../App;../App/app;../App/algo;../App/cfg;../App/fs;../App/log;../App/sync;../App/task;../App/protocol/iec104;../App/util;../Middlewares/Third_Party/FreeRTOS/Source/include;../Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2;../Middlewares/Third_Party/FreeRTOS/Source/portable/RVDS/ARM_CM4F;../Lib/dhara;../Lib/FatFs;../test/board;../Lib/lvgl;../Drivers/BSP/UI;../Lib</IncludePath>
|
||||
</VariousControls>
|
||||
</Cads>
|
||||
<Aads>
|
||||
@@ -947,6 +947,31 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\task\adc_task.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>iec104_serv.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\task\iec104_serv.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>protect_task.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\task\protect_task.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>iec104_asdu.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\protocol\iec104\iec104_asdu.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>iec104_pts.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\protocol\iec104\iec104_pts.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>iec104_link.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\protocol\iec104\iec104_link.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>adc_input.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
|
||||
@@ -76,6 +76,8 @@ main.c
|
||||
| uiTask | 4096 words | BelowNormal | LVGL UI 界面 |
|
||||
| timeSyncTask | 512 words | Normal | 墙钟时间同步 |
|
||||
| adcTask | 640 words | AboveNormal | TPAFE5160 采样(TIM3 1kHz 事件驱动 + 原子快照 + STM32 ADC DMA) |
|
||||
| iec104Task | 640 words | Normal | |
|
||||
| protectTask | 512 words | Normal | |
|
||||
<!-- TASK_TABLE:END -->
|
||||
|
||||
> 上表与 `Src/freertos.c` 的 `osThreadAttr_t` 一致(2026-09-25 校准);改动栈大小前先读
|
||||
|
||||
+1
-1
@@ -122,7 +122,7 @@ FREERTOS.Queues01=netMsgQueue,8,net_msg_t,0,Dynamic,NULL,NULL
|
||||
FREERTOS.Tasks01=defaultTask,24,1024,StartDefaultTask,Default,NULL,Dynamic,NULL,NULL;netTask,24,960,StartNetTask,Default,NULL,Dynamic,NULL,NULL;ftpTask,24,1024,StartFtpTask,As weak,NULL,Dynamic,NULL,NULL;testTask,16,2048,start_test_task,As weak,NULL,Dynamic,NULL,NULL;uiTask,16,4096,StartUiTask,Default,NULL,Dynamic,NULL,NULL
|
||||
FREERTOS.configCHECK_FOR_STACK_OVERFLOW=2
|
||||
FREERTOS.configENABLE_FPU=1
|
||||
FREERTOS.configTOTAL_HEAP_SIZE=49152
|
||||
FREERTOS.configTOTAL_HEAP_SIZE=57344
|
||||
FREERTOS.configUSE_MALLOC_FAILED_HOOK=1
|
||||
File.Version=6
|
||||
GPIO.groupedBy=Group By Peripherals
|
||||
|
||||
@@ -39,6 +39,8 @@
|
||||
#include "sys_events.h"
|
||||
#include "default_task.h"
|
||||
#include "adc_task.h" /* B1(2026-10-10):ADC 采集消费者(手写任务,见 Variables 区) */
|
||||
#include "iec104_serv.h" /* B4(2026-10-12):IEC 104 上行服务(手写任务,见 Variables 区) */
|
||||
#include "protect_task.h" /* B5(2026-10-12):保护判据节拍任务(手写任务,同上) */
|
||||
/* USER CODE END Includes */
|
||||
|
||||
/* Private typedef -----------------------------------------------------------*/
|
||||
@@ -119,6 +121,13 @@ static int hook_ptr_ok(const void *p)
|
||||
* syslog_post 回显尾)。静态最坏链 1080 B,512 words 时余 968 B < 1024 B
|
||||
* ISR 底线 → 抬至 640 words(2.5 KB,堆余量 min_ever 7152→≈6640 B,可承受)。
|
||||
* 栈值以 `scripts/stack_report.py` 实测最坏链为准,改动前后各跑一次。
|
||||
* iec104Task 640 words:B4(2026-10-12)104 上行服务(2404 监听 + link 状态
|
||||
* 机)。link/pts 缓冲全静态;stack_report 实测静态最坏链 1456 B(拆链
|
||||
* 重建监听 → net_bind → SPI 故障 → 日志尾,日志主导)。512 words 时余
|
||||
* 592 B < 1024 B ISR 底线 → 抬至 640(2.5 KB)。
|
||||
* protectTask 512 words:B5(2026-10-12)保护判据节拍(prot_elm_run 消费者
|
||||
* + L5-4 采样闭锁)。stack_report 实测静态最坏链 872 B(float 判据 +
|
||||
* phasor 快照 + 日志尾),余 1176 B / 57% ≥ ISR 底线 → 512 保持。
|
||||
*/
|
||||
/* 存储测试变量(g_storage_stats / s_perf_buf)已迁至 test/board/storage_test_task.c */
|
||||
osThreadId_t timeSyncTaskHandle;
|
||||
@@ -140,6 +149,28 @@ const osThreadAttr_t adcTask_attributes = {
|
||||
.stack_size = 640 * 4,
|
||||
.priority = (osPriority_t) osPriorityAboveNormal,
|
||||
};
|
||||
/* B4(2026-10-12):IEC 60870-5-104 被控站服务(2404 监听 + 链路状态机消费)。
|
||||
* 手写于此而非 .ioc(同 timeSyncTask/adcTask:不在 Tasks01 里 → 重生成会丢,
|
||||
* 靠审计 WARN 提示)。640 words:stack_report 实测静态最坏链 1456 B(拆链→
|
||||
* 重建监听→net_bind→SPI 故障→日志尾,日志主导);512 words 时余 592 B
|
||||
* < 1024 B ISR 底线 → 抬至 640(2.5 KB,CCM 堆 min_ever 再降 512 B,可承受)。 */
|
||||
osThreadId_t iec104TaskHandle;
|
||||
const osThreadAttr_t iec104Task_attributes = {
|
||||
.name = "iec104Task",
|
||||
.stack_size = 640 * 4,
|
||||
.priority = (osPriority_t) osPriorityNormal,
|
||||
};
|
||||
/* B5(2026-10-12):保护判据节拍(prot_elm_run 消费者 + L5-4 采样闭锁)。
|
||||
* 手写于此而非 .ioc(同上)。512 words:stack_report 实测静态最坏链 872 B
|
||||
* (float 判据 + phasor 快照 + 日志尾),余 1176 B / 57% ≥ ISR 底线 → 保持。
|
||||
* 优先级 Normal:V0.1 无出口硬件(规划 R7),判据是 20ms 软实时,不需要
|
||||
* adcTask 的 AboveNormal;规划 §4 的"1ms/高"随 P2 瞬时值元件再评。 */
|
||||
osThreadId_t protectTaskHandle;
|
||||
const osThreadAttr_t protectTask_attributes = {
|
||||
.name = "protectTask",
|
||||
.stack_size = 512 * 4,
|
||||
.priority = (osPriority_t) osPriorityNormal,
|
||||
};
|
||||
/* USER CODE END Variables */
|
||||
/* Definitions for defaultTask */
|
||||
osThreadId_t defaultTaskHandle;
|
||||
@@ -320,6 +351,12 @@ void MX_FREERTOS_Init(void) {
|
||||
* (内核启动前)完成;本任务只做运行期采样。 */
|
||||
adcTaskHandle = osThreadNew(adc_task_func, NULL, &adcTask_attributes);
|
||||
|
||||
/* B4(2026-10-12):IEC 60870-5-104 上行服务任务。任务体内先等 NET_READY
|
||||
* 屏障(100ms 分片 + 每片喂心跳,timeSyncTask 先例),再建 2404 监听并
|
||||
* 100ms 循环服务;纳入 IWDG required_mask(TASK_WD_IEC104,任何构建都创建)。 */
|
||||
iec104TaskHandle = osThreadNew(iec104_serv_task, NULL, &iec104Task_attributes);
|
||||
protectTaskHandle = osThreadNew(protect_task, NULL, &protectTask_attributes);
|
||||
|
||||
/* P2-13(2026-10-04):产品构建(未选任何 TEST_SUITE_*)不需要 testTask。
|
||||
* 上方生成区按 .ioc FREERTOS.Tasks01 无条件创建它,其 2048 words(8KB) 栈 + TCB
|
||||
* 取自 CCM 堆;而产品构建下 start_test_task() 仅打印一行后 osDelay(1000) 永久空转,
|
||||
|
||||
+3
-1
@@ -90,6 +90,7 @@ docs/
|
||||
| [`运行与自检记录设计说明.md`](子系统设计/运行与自检记录设计说明.md) | `/log/run.log`·`selftest.log` 定长记录与待机显示 |
|
||||
| [`看门狗设计说明.md`](子系统设计/看门狗设计说明.md) | IWDG(CubeMX 配置)+ 任务心跳位图:选型与超时推导、掩码与埋点、取证机制、验证记录;已台架闭环(V2/V4/V5)。**§11 = 任务级饥饿的诊断方法与 `[PROF]` 实测基线**(Task #32 沉淀:判读口径、量化归因、三条脆弱点、明确不做的四条) |
|
||||
| [`ADC采样节拍设计说明.md`](子系统设计/ADC采样节拍设计说明.md) | **ADC 采集链路采样节拍**:TIM3 1 kHz 更新事件同时触发 TPAFE CONVST 与(经 TRGO)STM32 ADC1 + DMA 乒乓缓冲;方案选型、CubeMX 配置规格、代码改造清单、时序/优先级分析、验收判据与实测剖面;**已落地并上板闭环** |
|
||||
| [`IEC104上行设计说明.md`](子系统设计/IEC104上行设计说明.md) | **B4 上行规约**:IEC 60870-5-104 被控站(TCP 2404)范围/点表(V0.1 有效子集)/链路流程(k/w/t1~t3、总召、COV、遥控两段式、定值)/netTask 接入决策/预算与测试方案;协议核心纯逻辑 + L0.5 全栈单测 |
|
||||
| [`网络/BSD_Socket_API_使用指南.md`](网络/BSD_Socket_API_使用指南.md) | **上层视图**:BSD Socket API 公开语义、示例、常见问题 |
|
||||
| [`网络/BSD_Socket_API_函数调用关系.md`](网络/BSD_Socket_API_函数调用关系.md) | **内部视图**:netTask / 消息队列 / L3 挂起 / 重试对账的调用关系与设计意图 |
|
||||
| [`网络/Net层阻塞模型架构设计.md`](网络/Net层阻塞模型架构设计.md) | **等待模型**:pending 请求架构、五路完成语义、设计边界与性能特征 |
|
||||
@@ -176,6 +177,7 @@ docs/
|
||||
| [`RAM使用分析与优化方案.md`](内存管理/RAM使用分析与优化方案.md) | **内存主文档**:占用分析、优化方案、定栈规则 |
|
||||
| [`内存内容分配策略.md`](内存管理/内存内容分配策略.md) | 各内存区(SRAM/CCM)的分配策略 |
|
||||
| [`存储栈RAM优化调研.md`](内存管理/存储栈RAM优化调研.md) | 存储栈 RAM 优化调研与结论 |
|
||||
| [`RAM预算台账.md`](内存管理/RAM预算台账.md) | **预算台账**:子系统配额、CCM 分配、趋势基线与 92% 门禁(N-27 三件套,2026-10-12) |
|
||||
| `data/` | `ram_report_*.md` / `stack_report_*.md` 快照数据(由 `scripts/` 生成) |
|
||||
|
||||
### 7.3 问题记录(**改驱动前必读**)
|
||||
@@ -254,7 +256,7 @@ docs/
|
||||
| 问题 | 位置 | 说明 |
|
||||
|---|---|---|
|
||||
| 指向未实现工具 | `UI_界面制作流程.md`、`UI_自动化测试流程.md` → `tools/ui_mock/` | 该目录**始终不存在**;golden 回归实际落在 `simulator/tests/`(四脚本)。引用待改指,见 `docs/问题记录/项目审查与优化建议报告.md` §6.1 |
|
||||
| 产物缺失 | `内存管理/RAM使用分析与优化方案.md` → `scripts/ram_baseline.tsv` | 方案写了、工具未建(引用已加 `[计划]` 标记) |
|
||||
| ~~产物缺失~~ | ~~`内存管理/RAM使用分析与优化方案.md` → `scripts/ram_baseline.tsv`~~ | ✅ 已建(2026-10-12,N-27 前半):`ram_baseline.py` + `ram_baseline.tsv` + [`RAM预算台账.md`](内存管理/RAM预算台账.md) + `mem-budget` 钩子 |
|
||||
| ~~目录名含空格~~ | ~~`UI 设计/`~~ | ✅ 已修复(2026-09-25,D-6):改名为 `UI设计/`,全仓库 67 处引用同步;命名约定见 [`文档规范.md`](文档规范.md) §1 |
|
||||
| ~~缺一致性门禁~~ | — | ✅ 已落地(2026-09-25/26,D-7 三档齐):`scripts/check_doc_links.py`(存在性)+ `scripts/gen_task_table.py`(数值)+ `scripts/check_mem_numbers.py`(体积),接入 pre-commit `doc-links` / `task-table` / `mem-numbers` 钩子 |
|
||||
| ~~`docs/文档规范.md` 未创建~~ | — | ✅ 已创建(2026-09-25):[`文档规范.md`](文档规范.md) |
|
||||
|
||||
@@ -468,6 +468,7 @@ sequenceDiagram
|
||||
| FTP | TCP **21** | 文件上传/下载(根目录 `/`,NAND 存储) | 登录后任意 FTP 客户端列目录/读写(鉴权见 §7.6) |
|
||||
| 墙钟对时 | TCP **8888** | PC 主动推送时间到设备 | 对时后核对设备时间 / 日志时间戳 |
|
||||
| 远程显示(RemoDispBus) | TCP **7003** | 远端看图(160×160 1bpp)+ 远端导航按键 | DTU-RemoteLCD 或任意 TCP 客户端连 7003 收帧 |
|
||||
| IEC 60870-5-104 上行 | TCP **2404** | 被控站接入:遥测/遥信上送、总召、遥控/定值下发(B4) | 任意 104 主站连 2404,STARTDT+总召往返(→ `docs/子系统设计/IEC104上行设计说明.md`) |
|
||||
|
||||
> **现场组网要求**:设备与客户端**同网段**(默认 .100/24、网关 .1);跨网段先改 IP 再上线。
|
||||
|
||||
@@ -779,7 +780,8 @@ cfg-file-unit-test、test-suite-switch、gitlint。
|
||||
|
||||
### 9.2 阶段路线图
|
||||
|
||||
> **顺序唯一权威 → [`../待处理内容.md`](../待处理内容.md) §2(统一推进顺序,波次 W1~W8)**。
|
||||
> **顺序唯一权威 → [`../待处理内容.md`](../待处理内容.md) §2(统一推进顺序,波次 W1~W7;
|
||||
> RS-485/B2 整簇暂缓见其 §2.4,2026-10-12 用户决定)**。
|
||||
> 本节自 2026-10-11 起不再维护路线图(原阶段图删除,避免与登记簿漂移;原 C4"defaultTask 栈余量 10%"
|
||||
> 已于 2026-10-10 削链闭环销项,见 `../内存管理/RAM使用分析与优化方案.md`)。
|
||||
> 功能缺口口径见 §9.1;B0 产品定义结论见 `../算法/继电保护功能复现规划.md` §1.5。
|
||||
|
||||
@@ -25,7 +25,8 @@ git commit
|
||||
├─ cubemx-survival CubeMX 重生成存活审计(易失定制清单 + 基线漂移)
|
||||
├─ watchdog-unit-test 看门狗判定状态机主机侧单测(L0.5)
|
||||
├─ test-suite-switch 测试套件开关=产品构建(防测试固件静默提交)
|
||||
└─ registration-check 双端注册反向校验(uvprojx + CMakeLists,N-23)
|
||||
├─ registration-check 双端注册反向校验(uvprojx + CMakeLists,N-23)
|
||||
└─ mem-budget RAM 预算 92% 阈值门禁(N-27 三件套,2026-10-12)
|
||||
└─ commit-msg(.git/hooks/commit-msg)
|
||||
└─ gitlint 校验 commit message 格式
|
||||
```
|
||||
@@ -50,6 +51,7 @@ git commit
|
||||
| scripts/check_cubemx_survival.py | 随仓库提交 | CubeMX 重生成存活审计(纯 Python,无依赖) |
|
||||
| scripts/check_test_suite.py | 随仓库提交 | 测试套件开关门禁(纯 Python,无依赖) |
|
||||
| scripts/check_registration.py | 随仓库提交 | 双端注册反向校验(纯 Python,无依赖) |
|
||||
| scripts/ram_baseline.py | 随仓库提交 | RAM 预算 92% 门禁 + 趋势台账(纯 Python,无依赖) |
|
||||
|
||||
> **PATH 要求**:cppcheck 安装后需在 PATH 中可找到(winget 自动写入 User PATH,但当前终端不会立即生效,需**重开终端**);装在非 PATH 位置时可用 `DTU_CPPCHECK` 环境变量指向 `cppcheck.exe` 完整路径(显式设置但无效 → 硬报错,不静默回退)。
|
||||
|
||||
@@ -371,6 +373,29 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法
|
||||
| **为什么需要** | 新 `.c` 必须手动注册两处才会被编译;历史上 `rs485_task.c`/`lftpd_log.c` **两处都没注册** = 写了却从不参与任何一端编译的**死代码**,误导读者以为功能已生效(出处审查报告 N-06 残余) |
|
||||
| **运行器** | `scripts/check_registration.py`(纯标准库;退出码 0=一致 / 1=有缺口 / 2=环境错误) |
|
||||
|
||||
### 2.19 mem-budget — RAM 预算 92% 阈值门禁(2026-10-12 加,N-27 三件套)
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| **触发条件** | 全部自研代码目录(`App/` `Drivers/BSP/` `Src/` `Inc/`)+ `Lib/lv_conf.h` + `test/board/` + `scripts/ram_baseline.*` + `docs/内存管理/` + 本配置文件 |
|
||||
| **处理方式** | **阻止提交**(RW_IRAM1 / RW_CCM 任一区占比 >92%,或台账受管区与 `.map` 漂移);无 `.map` 自适应跳过(同 `mem-numbers` 在 CI 的行为,盲区登记 `docs/待处理内容.md` A-5 残余) |
|
||||
| **检查逻辑** | 解析 `.map` 执行区占比 → 超阈值即 FAIL 并附 **Top-10 RW+ZI 对象表**(定位增量元凶);同时校验 `docs/内存管理/RAM预算台账.md` 受管区与 `.map` 一致(漂移 → 提示 `--write`) |
|
||||
| **软提醒**(不拦) | tsv 末行落后于 `.map`(忘跑 `--update`);`.map` 早于最新源码(忘 `@build`) |
|
||||
| **配套三件套** | `scripts/ram_baseline.py`(门禁/趋势/受管区生成)+ `scripts/ram_baseline.tsv`(每次 `@build` 后 `--update` 追加的趋势基线,`--trend` 高亮 RW+ZI 增长 >1 KB 的对象)+ `docs/内存管理/RAM预算台账.md`(子系统配额 + CCM 台账 + 超配额流程) |
|
||||
| **为什么需要** | `docs/问题记录/项目审查与优化建议报告.md` §5.5 第 1 条"超阈值即失败 + Top-10 附表"此前**无任何机械检查**——`mem-numbers` 只防"文档数字漂移"不防"占用超阈值";B4/B5 要新建任务与缓冲,红线必须先机械化(登记簿 N-27 前半 / W1-0) |
|
||||
| **运行器** | `scripts/ram_baseline.py --gate`(纯标准库,复用 `ram_report.py` 的 `.map` 解析口径;退出码 0=通过/跳过 / 1=超阈值或漂移 / 2=解析失败) |
|
||||
|
||||
### 2.20 iec104-unit-test — IEC 60870-5-104 协议栈主机侧单测(2026-10-12 加,B4)
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| **触发条件** | `App/protocol/iec104/iec104_(asdu|pts|link).*`、数据源依赖 `App/algo/(prot_set|prot_elm|phasor|sample_ring|adc_input).*`、`test/host/iec104/**` 或 `test/host/common/unit_test_harness.py` 变动 |
|
||||
| **处理方式** | **阻止提交**(单测失败即拦截) |
|
||||
| **被测对象** | `App/protocol/iec104/` 三模块(ASDU 编解码 / 点表+数据源 vtable / APCI+链路 FSM)——纯逻辑、tick 注入、发送经回调注入,零 I/O(A-4 约束 1) |
|
||||
| **用例** | 82 例:CP56 往返 / APCI 半帧粘帧坏帧 / STARTDT-STOPDT / 总召分片+RBT / COV 死区 / k=12 窗满 / w=8 触发 S / t1-t2-t3 / N(R) 倒退判故障 / 遥控两段式 / 定值越界 PN / C_CS 对时 / 未知类型-地址错误应答 / 周期上送 |
|
||||
| **为什么需要** | 板端复现这些分支要真主站 + 真等待(t1=15 s、k 窗满要发满 12 帧);link 层 tick 注入 + pts vtable 抽象就是为 L0.5 毫秒级穷举设计;数据源依赖进触发面——测试直接 `#include` 这些生产源,签名变了立刻暴露 |
|
||||
| **运行器** | `test/host/iec104/run_unit_test.py`(MSVC `cl.exe`,L0.5) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 日常工作流
|
||||
@@ -498,7 +523,7 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env
|
||||
|
||||
| 文件 | 位置 | 说明 |
|
||||
|---|---|---|
|
||||
| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**29 个钩子**,2026-10-12 清点;本文 §2 详解非单测类——N-19 新增的两个单测钩子见 §2.16/§2.17,N-23 双端注册反向校验见 §2.18,其余主机侧单测见 AGENTS.md 门禁条;主机侧单测共 12 个) |
|
||||
| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**31 个钩子**,2026-10-12 清点;本文 §2 详解非单测类——N-19 新增的两个单测钩子见 §2.16/§2.17,N-23 双端注册反向校验见 §2.18,N-27 RAM 预算门禁见 §2.19,B4 IEC104 协议栈单测见 §2.20,其余主机侧单测见 AGENTS.md 门禁条;主机侧单测共 13 个) |
|
||||
| `.gitlint` | 仓库根 | gitlint 规则(宽松,兼容中文) |
|
||||
| `.cubemx_survival_baseline.json` | 仓库根 | CubeMX 易失定制基线(哈希;`--write` 重建) |
|
||||
| `scripts/check_mojibake.py` | scripts/ | U+FFFD 检测脚本 |
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 |
|
||||
|---|---|---|---|
|
||||
| ER_IROM1 | 214,088 B (209.07 KB) | 1,048,576 B (1024 KB) | 20.4% |
|
||||
| RW_IRAM1 | 94,768 B (92.55 KB) | 131,072 B (128 KB) | 72.3% |
|
||||
| RW_CCM | 49,184 B (48.03 KB) | 65,280 B (64 KB) | 75.3% |
|
||||
| **Flash (ROM 合计)** | **215,056 B (210.0 KB)** | 1,048,576 B (1024 KB) | **20.5%** |
|
||||
| ER_IROM1 | 222,376 B (217.16 KB) | 1,048,576 B (1024 KB) | 21.2% |
|
||||
| RW_IRAM1 | 95,816 B (93.57 KB) | 131,072 B (128 KB) | 73.1% |
|
||||
| RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% |
|
||||
| **Flash (ROM 合计)** | **223,412 B (218.2 KB)** | 1,048,576 B (1024 KB) | **21.3%** |
|
||||
<!-- MEM_SNAPSHOT:END -->
|
||||
|
||||
各区的容量与常量来源:
|
||||
@@ -48,7 +48,7 @@
|
||||
|---|---|---|
|
||||
| 主 SRAM (0x20000000) | 128 KB(RW_IRAM1 Max `0x20000`) | scatter `STM32F407-Demo.sct:22` |
|
||||
| CCM SRAM (0x10000000) | 64 KB;FreeRTOS 堆于 2026-09-21 迁入(§1.2) | scatter `STM32F407-Demo.sct:25`;RM0090 §3.1.1 |
|
||||
| FreeRTOS 堆 (heap_4) | 48 KB(49,152 B),**位于 RW_CCM**;实测 `free=8,784 / min_ever=4,848` B | `Inc/FreeRTOSConfig.h:71` `configTOTAL_HEAP_SIZE` |
|
||||
| FreeRTOS 堆 (heap_4) | **56 KB(57,344 B)**,**位于 RW_CCM**;2026-10-12(B5)由 48 KB 扩容(protectTask/iec104Task 栈入堆,CCM 水位 87.9% < 92% 门禁);旧 48KB 池实测 `free=8,784 / min_ever=4,848` B(2026-10-03),新池 min_ever 待上板复测 | `Inc/FreeRTOSConfig.h:71` `configTOTAL_HEAP_SIZE`(L1:`.ioc` `FREERTOS.configTOTAL_HEAP_SIZE`) |
|
||||
| C 库堆/栈 (startup) | 512 B / 1 KB,启动期 main() 用 | map `startup_stm32f407xx.o` HEAP/STACK 节 |
|
||||
|
||||
### 2.2 静态大项排行(2026-10-10 当前,完整台账 `data/ram_report_20261010.md`)
|
||||
@@ -85,7 +85,7 @@
|
||||
> **DMA 可达性**:第 4/6/7 项(NAND FTL、CH395F SPI2、GD5F BBT)为 DMA 缓冲,**必须留在主 SRAM**、禁止迁 CCM/SRAM3——权威清点与规则见 `内存内容分配策略.md`(本文不重复)。
|
||||
> 2026-09-11 历史排行(`heap_4.o` 49,152 B 尚在 SRAM1、静态合计 122.7 KB 的旧布局)见 `data/ram_report_20260911.md`,仅供回溯。
|
||||
|
||||
### 2.3 任务栈(从 48KB 堆动态分配)
|
||||
### 2.3 任务栈(从 56KB 堆动态分配,2026-10-12 起;此前 48KB)
|
||||
|
||||

|
||||
|
||||
@@ -110,6 +110,8 @@
|
||||
| defaultTask | `1024*4` | 4KB | FatFS 挂载/探针 + 配置引擎 + syslog + 栈水位探针 | 2152 B(余 47%)/ 静态 2776 B |
|
||||
| timeSyncTask | `512*4` | 2KB | TCP 时间同步 | 616 B(余 69%)/ 静态 **1024 B**(= ISR 底线,无再加深空间) |
|
||||
| adcTask | `640*4` | 2.5KB | TPAFE5160 采样消费 + L2 相量 + L1-6 有效性校验(TIM3 1 kHz 节拍,B1→L2→L1-6 2026-10-11) | **952 / 2560**(余 1608 B / 62%,台架 `@test` 实测 2026-10-11);静态 1080 B |
|
||||
| iec104Task | `640*4` | 2.5KB | IEC104 上行服务(2404 监听 + link 状态机,B4 2026-10-12) | 未上板 / 静态 **1456 B**(拆链→重建监听→net_bind→SPI 故障→日志尾,日志主导;512 words 时余 592 B < ISR 底线 → 抬至 640,余 1104 B / 43%) |
|
||||
| protectTask | `512*4` | 2KB | 保护判据节拍(prot_elm_run 消费者 + L5-4 采样闭锁,B5 2026-10-12) | 未上板 / 静态 **872 B**(float 判据 + phasor 快照 + 日志尾,余 1176 B / 57%) |
|
||||
| idle(内核) | `configMINIMAL_STACK_SIZE=128` | 0.5KB | 空闲 | 未观测(未取句柄) |
|
||||
| timer(内核) | `configTIMER_TASK_STACK_DEPTH=256` | 1KB | 软定时器 | 未观测 |
|
||||
|
||||
@@ -417,7 +419,7 @@ FreeRTOS 堆不变(`free=17248 / min_ever=9312`)。验证链:`@build` 0/0
|
||||
|
||||
依据 RM0090 §3.1.1:F407 的 64KB CCM @0x10000000 仅 CPU 可访问(DMA 不可达、无 cache)。
|
||||
|
||||
**进度(2026-09-21)**:第一阶段已落地——scatter 新增 `RW_CCM` 区(`sct:25`,注释 "FreeRTOS heap only")并把 **FreeRTOS 堆整体迁入**(49,184 B,CCM 占用 75.3%)。下表缓冲迁入清单为**后续剩余项**(LVGL 池/UI 缓冲现仍在主 SRAM);`APP_CCM_ENABLE` 门禁宏**尚未实现**(scatter 无开关,见下"风险门禁")。
|
||||
**进度(2026-09-21)**:第一阶段已落地——scatter 新增 `RW_CCM` 区(`sct:25`,注释 "FreeRTOS heap only")并把 **FreeRTOS 堆整体迁入**(当时历史快照:49,184 B,CCM 占用 75.3%;现值见 §2.1——2026-10-12 B5 堆已扩至 56 KB)。下表缓冲迁入清单为**后续剩余项**(LVGL 池/UI 缓冲现仍在主 SRAM);`APP_CCM_ENABLE` 门禁宏**尚未实现**(scatter 无开关,见下"风险门禁")。
|
||||
|
||||
**迁入 CCM 的缓冲(全部纯 CPU 读写):**
|
||||
|
||||
@@ -523,7 +525,7 @@ ISR 余量:该任务运行期间可能被中断打断的最深中断链(SPI/
|
||||
|---|---|---|
|
||||
| 主 SRAM 静态占用 | 87,040 B / 128KB(66.4%) | **~27KB / 128KB**(LVGL 池 + UI 缓冲迁 CCM、`ui_net_sink` 关) |
|
||||
| 主 SRAM 余量 | 44,032 B(43.0KB) | **~101KB** |
|
||||
| CCM 使用 | 49,184 B / 64KB(75.0%,FreeRTOS 堆) | ~56KB / 64KB(堆 32K + 池 16K + UI 缓冲 + BBT) |
|
||||
| CCM 使用 | 49,184 B / 64KB(75.0%,FreeRTOS 堆,历史当时值。2026-10-12 B5 后堆 56 KB / 87.9%) | ~56KB / 64KB(堆 32K + 池 16K + UI 缓冲 + BBT) |
|
||||
| 任务栈合计 | 38,912 B(38.0KB) | ~21.5KB(§1.3 修正目标) |
|
||||
| FreeRTOS 堆 | 49,152 B(`free=8,784 / min_ever=4,848` B) | 32,768 B(余 ≥8KB,全负载验证) |
|
||||
| **总余量** | **~68KB**(47.9 + 16.0 + 4.8) | **~120KB** |
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
# RAM 预算台账(配额 · 趋势 · 门禁)
|
||||
|
||||
> **定位**:`docs/内存管理/RAM使用分析与优化方案.md` §8.2 模板与
|
||||
> `docs/问题记录/项目审查与优化建议报告.md` §5.5 三件套的**落地件**(2026-10-12,登记簿 N-27 前半 / W1-0)。
|
||||
> 回答三个问题:**每个子系统还能用多少 RAM**(配额)、**总量趋势往哪走**(基线 TSV)、
|
||||
> **超了怎么拦**(92% 门禁)。
|
||||
>
|
||||
> **数字不手抄**:下方受管区由 `scripts/ram_baseline.py --write` 从 `.map` 生成,
|
||||
> pre-commit `mem-budget` 钩子校验一致性(同 `mem-numbers` 的"事实源唯一"思路)。
|
||||
> 配额是**规划值**(`docs/问题记录/项目审查与优化建议报告.md` §5.5),改动 = 先登记本文件 §6 变更记录,再改 `ram_baseline.py` 的 `QUOTAS`。
|
||||
> 区域放置与 DMA 可达性规则 → `内存内容分配策略.md`(本文件不重复)。
|
||||
|
||||
## 1. 区域预算与门禁(受管区)
|
||||
|
||||
<!-- MEM_BUDGET:BEGIN generated by scripts/ram_baseline.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) |
|
||||
|---|---|---|---|---|
|
||||
| ER_IROM1 (Flash) | 222,376 B | 1,048,576 B | 21.2% | 参考 |
|
||||
| RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS |
|
||||
| UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 |
|
||||
| RW_IRAM1 | 95,816 B | 131,072 B | 73.1% | PASS |
|
||||
|
||||
| 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 |
|
||||
|---|---|---|---|
|
||||
| 网络 | 13,366 B | 12 KB | ⚠ 超 1.1 KB |
|
||||
| 存储 | 7,056 B | 12 KB | OK |
|
||||
| UI | 54,988 B | 16 KB | ⚠ 超 37.7 KB |
|
||||
| RTOS 堆 | 57,376 B | 56 KB | 堆=配额 |
|
||||
| 应用 | 12,843 B | 8 KB | ⚠ 超 4.5 KB |
|
||||
| 其它 | 7,596 B | — | 参考 |
|
||||
<!-- MEM_BUDGET:END -->
|
||||
|
||||
- **门禁**:pre-commit `mem-budget` 钩子(`scripts/ram_baseline.py --gate`)——
|
||||
RW_IRAM1 / RW_CCM 任一 >92% 即拦提交,并打印 Top-10 RW+ZI 对象定位增量;
|
||||
无 `.map` 时自适应跳过(CI 盲区登记在 `docs/待处理内容.md` A-5 残余)。
|
||||
- **阈值依据**:92% = `docs/问题记录/项目审查与优化建议报告.md` §5.5 第 1 条——留 ~8% 给突发缓冲与栈抬升,
|
||||
触线即触发排期,而不是静默逼近溢出。
|
||||
- **软提醒**(不拦提交):tsv 末行落后于 `.map`(忘跑 `--update`)、`.map` 早于最新源码(忘 `@build`)。
|
||||
- **子系统归集为参考值**:按对象名前缀归集(口径见 §2),合计与区域总量可差几十字节
|
||||
(链接器填充行与 `UNINIT_CCM` 等小区分段所致);区域行才是门禁事实源。
|
||||
|
||||
## 2. 子系统配额口径
|
||||
|
||||
配额值(`ram_baseline.py` 的 `QUOTAS`,与 `docs/问题记录/项目审查与优化建议报告.md` §5.5 第 3 条一致;
|
||||
RTOS 堆 2026-10-12 B5 扩容 48→56 KB 见 §6):**网络 12 KB / 存储 12 KB /
|
||||
UI 16 KB / RTOS 堆 56 KB / 应用 8 KB**。归集口径(按 `.map` 对象名,首匹配):
|
||||
|
||||
| 子系统 | 计入对象(前缀/精确名) | 不计入 |
|
||||
|---|---|---|
|
||||
| 网络 | `ch395f*` `net_*` `lftpd*` `ftp_task*` | `ui_net_sink`(算 UI) |
|
||||
| 存储 | `gd5f*` `nand_*` `ff*` `diskio*` `vol_lock*` + dhara 裸名(`map/journal/nand/fctl/era.o`) | `cfg_*`(算应用) |
|
||||
| UI | `lv_*` `ui_*`(含 `ui_net_sink`、`ui_task`) | — |
|
||||
| RTOS 堆 | `heap_4.o`(CCM)——配额 = `configTOTAL_HEAP_SIZE` 本身 | 内核对象(`tasks/queue/timers/...` 归"其它") |
|
||||
| 应用 | `App/` 其余:采集 `adc_*`、保护 `prot_*`、相量 `phasor/sample_ring`、配置 `cfg_*`、日志 `sys_/dbg_/console_log`、对时 `time_sync*`、`sys_clock`、`task_watchdog`、`sha256`、`tpafe*`、`sd2506`、`default_task` 等 | — |
|
||||
| 其它 | 内核对象、HAL、CMSIS、启动文件、C 库、链接器填充 | — |
|
||||
|
||||
**现状判读(2026-10-12 建档)**:
|
||||
|
||||
- **UI ⚠ 超配额 = W3 的既有债务,不是新增违规**:LVGL 池 40 KB(R-9 目标 16 KB)+ 帧/远显缓冲
|
||||
≈12.8 KB(R-2 目标迁 CCM)——配额 16 KB 描述的是 **R-9/R-2 落地后的目标态**(排期见
|
||||
`docs/待处理内容.md` §2 W3)。
|
||||
- **RTOS 堆 = 配额本身**:56 KB 即 `configTOTAL_HEAP_SIZE`(2026-10-12 B5 由 48 KB 扩容,见 §6;
|
||||
`heap_4.o` 另含 32 B 元数据,不计超);**改堆 = 改配额**,必须先过本文件 §6 登记。
|
||||
- **应用 ⚠ 小幅超**:8 KB 为报告规划值;2026-10-10 栈整改把 3 处 `FIL` 从栈搬进 BSS
|
||||
(静态 +4,128 B,见 `docs/内存管理/RAM使用分析与优化方案.md` §4"2026-10-10 复测")——
|
||||
属已评审的"以空间换栈安全",差额在此如实体现。
|
||||
|
||||
## 3. CCM 分配台账(谁能用 CCM)
|
||||
|
||||
| 项 | 值 | 说明 |
|
||||
|---|---|---|
|
||||
| CCM 容量 | `RW_CCM` Max 见受管区 | scatter `RW_CCM`;**仅 CPU 可达,DMA 不可达**(RM0090 §3.1.1) |
|
||||
| 现占用 | 仅 FreeRTOS 堆 `heap_4`(56 KB,2026-09-21 迁入;2026-10-12 B5 扩容 48→56 KB) | 其余候选全部仍在 SRAM1 |
|
||||
| 现余量 | 受管区 RW_CCM 容量 − 占用(≈8 KB) | **不够** R-2 后半的 UI 侧待迁体量(R-9 后仍 ≈30 KB) |
|
||||
| R-2 后半解锁链 | ① R-9:`LV_MEM_SIZE` 40→16 KB(待迁体量降到 ~30 KB)→ ② 堆 56→32 KB(`docs/内存管理/RAM使用分析与优化方案.md` §2.2,再腾 24 KB;⚠ 2026-10-12 B5 反向扩容至 56 KB——9 任务栈 + 队列实测需要,缩回 32 KB 前必须重跑 `stack_report.py` + 上板 min_ever 复测)→ ③ CCM 余 ~32 KB ≥ 30 KB → 迁入 | **顺序不可颠倒**;每步前后跑 `stack_report.py` / `ram_report.py`(铁律) |
|
||||
| 迁入前置 | 目标缓冲必须过 DMA 可达性清单(`内存内容分配策略.md`) | 违例 = 静默传错(N-11 同类坑);克隆芯片 CCM 可靠性风险见 `docs/内存管理/RAM使用分析与优化方案.md` §7 |
|
||||
|
||||
## 4. 趋势基线(`scripts/ram_baseline.tsv`)
|
||||
|
||||
- **每次 `@build` 后**:`& '.venv\Scripts\python.exe' scripts/ram_baseline.py --update`
|
||||
追加一行(Code/RO/RW/ZI + 三区水位 + Top-10 RW+ZI 对象;同日重跑覆盖当日行)。
|
||||
- **看趋势 / 找增量**:`scripts/ram_baseline.py --trend`——高亮较上一行 RW+ZI 增长 >1 KB 的对象,
|
||||
即 `docs/内存管理/RAM使用分析与优化方案.md` §0.3 "ZI 增量 >1KB 必须在提交说明中解释"的机械化;提交说明直接抄 `--trend` 输出行。
|
||||
|
||||
## 5. 超配额流程
|
||||
|
||||
新增/扩大静态缓冲导致子系统越过配额:**先登记再动手**——本文件 §6 加一行
|
||||
(日期 / 子系统 / 现值→新值 / 原因 / 决定人),提交说明引用该行。
|
||||
92% 门禁拦的是**总量**,配额管的是**结构**(谁挤占了谁),两者不可互替。
|
||||
|
||||
## 6. 变更记录
|
||||
|
||||
| 日期 | 变更 | 原因 / 出处 |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | 建档:三件套落地(本台账 + `scripts/ram_baseline.py` + `scripts/ram_baseline.tsv` + pre-commit `mem-budget` 钩子) | 登记簿 N-27 前半(W1-0);`docs/问题记录/项目审查与优化建议报告.md` §5.5 |
|
||||
| 2026-10-12 | **RTOS 堆配额 48 KB → 56 KB**(`QUOTAS` 同步) | B5 protectTask 立项:iec104Task(2.5KB)+protectTask(2KB)+TCB 入堆后 48KB 池 min_ever 预估跌破 3,072 B 告警线 → `.ioc` L1 参数 `configTOTAL_HEAP_SIZE` 49152→57344(CCM 水位 75.3%→87.9%,仍 <92% 门禁;CCM 空闲降至 ≈8 KB)。连带影响见 §3 R-2 后半链注记 |
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
## 4. 内容分配规则
|
||||
|
||||
- **R1(CCM 驻留)**:CCM 只放"**CPU-only 且损坏即致命、无自愈**"的内存。当前唯一驻留者 = **FreeRTOS 堆**(`heap_4.o`,48KB:全部任务栈/队列/定时器)。scatter 的 RW_CCM 节**不得**再列其他对象;任何新增必须经 §5 式裁决并走 §7 完整验证阶梯。
|
||||
- **R1(CCM 驻留)**:CCM 只放"**CPU-only 且损坏即致命、无自愈**"的内存。当前唯一驻留者 = **FreeRTOS 堆**(`heap_4.o`,56KB——2026-10-12 B5 由 48KB 扩容,`.ioc` L1 参数 `configTOTAL_HEAP_SIZE=57344`;全部任务栈/队列/定时器)。scatter 的 RW_CCM 节**不得**再列其他对象;任何新增必须经 §5 式裁决并走 §7 完整验证阶梯。
|
||||
- **R2(DMA 缓冲)**:只允许文件作用域静态缓冲,必须落 SRAM。两条防线缺一不可:
|
||||
- **尺寸防线**:GD5F DMA 阈值 `GD5F_DMA_THRESHOLD=1024`(>1024B 才走 DMA;阈值以下一律 CPU 轮询)。取值须 ≥ 存储路径上可能出现在任务栈的最大缓冲(dhara meta 132B、`dhara_nand_is_free` 64B)、且 < 页大小(2048,保住页级热路径的 DMA)。
|
||||
- **地址防线(2026-09-21 加固)**:`gd5f_dma_buf_ok()` 运行时校验**整段**缓冲是否落在 SRAM 区间(0x20000000~0x2001FFFF),越界者拒走 DMA、回落 CPU 轮询,并打印 `[ERR][NAND] RX/TX DMA buffer not DMA-reachable (p=… size=…) fallback poll`。
|
||||
@@ -108,12 +108,12 @@ CCM 64KB 只能容纳 {FreeRTOS 堆 48KB} 与 {LVGL 集合 ~52.5KB} 之一(两
|
||||
触发条件:测试构建 SRAM 余量不足,**且**已耗尽 `RAM使用分析与优化方案.md` 的 R-4 帧副本去重(−6.4~9.6KB)与 LVGL 池缩减等低代价手段。
|
||||
流程:改 scatter → 走 §7 完整验证阶梯 → 同步更新本文档、`AGENTS.md`、`.sct` 头注释。在此之前,方案 A 是唯一有效布局。
|
||||
|
||||
## 6. 当前容量台账(2026-09-21,最坏构建 = 存储套件)
|
||||
## 6. 当前容量台账(2026-10-12,产品构建;2026-09-21 最坏构建=存储套件的历史基线见 `docs/内存管理/RAM预算台账.md` 趋势)
|
||||
|
||||
| 区域 | 总量 | 已用 | 空闲 | 驻留内容 |
|
||||
|---|---|---|---|---|
|
||||
| RW_IRAM1(SRAM) | 131,072B (128KB) | 78,188B (~76.4KB) | 52,884B (~51.7KB) | 全部静态(除堆),含 LVGL 集合 53,805B |
|
||||
| RW_CCM | 65,536B (64KB) | 49,152B (48KB) | 16,384B (16KB) | **仅** `heap_4.o`(ucHeap 48KB) |
|
||||
| RW_IRAM1(SRAM) | 131,072B (128KB) | 95,816B (~93.6KB, 73.1%) | 35,256B (~34.4KB) | 全部静态(除堆),含 LVGL 集合 53,805B |
|
||||
| RW_CCM | 65,536B (64KB) | 57,376B (56KB, 87.9%) | 8,160B (~8KB) | **仅** `heap_4.o`(ucHeap 56KB,2026-10-12 B5 扩容) |
|
||||
|
||||
LVGL 集合明细(均在 SRAM):LVGL 池 40,960B(`lv_mem_core_builtin.o`,`LV_MEM_ADR=0` 内部静态)+ `g_ui_draw_buf` 3,232B(3200+32 保护带)+ `g_ui_fbuf` 3,200B + `g_tx` 3,209B + `g_lcdmem_data` 3,204B。
|
||||
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
# IEC 60870-5-104 上行设计说明(B4)
|
||||
|
||||
> **定位**:DTU 上行规约 IEC 60870-5-104(DL/T 634.5104-2009)的设计基线——
|
||||
> 范围、点表、链路流程、板端接入、预算与测试。规格来源
|
||||
> `docs/算法/继电保护功能复现规划.md` §1.5(B0 产品定义,验收 = 省电科院点表);
|
||||
> 代码落点 `App/protocol/iec104/`;登记项 `docs/待处理内容.md` §2.1(W1 / B4)。
|
||||
> **角色**:本 DTU = **被控站(受控端)**,104 **TCP 服务器,端口 2404**;主站
|
||||
> (控制端,点表参考 `192.168.1.199`)主动连入。
|
||||
|
||||
## 1. 范围(V0.1 子集)
|
||||
|
||||
| 面 | 本期做 | 本期不做(登记 §8) |
|
||||
|---|---|---|
|
||||
| 链路 | APCI U/S/I、STARTDT/STOPDT/TESTFR、k/w 窗、t1/t2/t3 | 多主站并发(单连接语义,见 §5) |
|
||||
| 遥测 YC | 短浮点 M_ME_NC_1(系数 1):背景/周期/死区上送 | 归一化/标度、谐波轨(L2-8) |
|
||||
| 遥信 YX | 单点 M_SP + 双点 M_DP:背景 + 突发(COT=3) | SOE 带时标 CP56(M_SP_TB,随 L4-4 SOE 落地) |
|
||||
| 遥控 YK | 双点 C_DC 两段式(选择 82H/执行 02H) | 带时标、升降/步位 |
|
||||
| 定值 | C_RD 读 + C_SE_NC 两段写(地址 = prot_set `comm_addr`) | 定值区切换/下装文件 |
|
||||
| 其他 | C_CS 对时(→ `sys_clock`)、C_TS、C_RP | 固有参数 8001H~800AH(字符串类)、C_CI 电度、文件传输(录波) |
|
||||
|
||||
## 2. 分层与模块(A-4 约束 1:协议解析与硬件 I/O 分离)
|
||||
|
||||
| 模块 | 文件 | 职责 | I/O |
|
||||
|---|---|---|---|
|
||||
| ASDU 编解码 | `App/protocol/iec104/iec104_asdu.c/h` | 类型/COT 常量、ASDU 头、IOA 3B 小端、短浮点、CP56Time2a↔epoch | 零 |
|
||||
| 点表/数据源 | `App/protocol/iec104/iec104_pts.c/h` | YC/YX/YK/定值表、COV(变位+死区)、**数据源 vtable**(板端绑 phasor/prot_*,单测绑假源) | 零(源注入) |
|
||||
| 链路状态机 | `App/protocol/iec104/iec104_link.c/h` | TCP 流重组、U/S/I、k/w/t1/t2/t3、总召/突发/遥控/定值/对时编排 | 零(收=on_rx 喂流,发=回调,时基注入) |
|
||||
| 板端胶水 | `App/task/iec104_serv.c/h`(随实现定名) | 2404 监听、数据通道收发、断链重听、netTask 驱动 | 全部(net_* API) |
|
||||
|
||||
> 三个纯逻辑模块 + 真实数据源(`App/algo/` 全纯逻辑)可整体在主机侧编译 →
|
||||
> L0.5 单测覆盖**全协议栈**(`test/host/iec104/`),与 `test/host/prot_elm/` 同模式。
|
||||
|
||||
## 3. 点表(V0.1)
|
||||
|
||||
### 3.1 遥测 YC(IOA 0x4011+,短浮点,系数 1)
|
||||
|
||||
| IOA | 点 | 来源 | V0.1 状态 |
|
||||
|---|---|---|---|
|
||||
| 0x4011 | 1Ua | phasor `C02-UA-F1` fAmSQ | ✅ 有效 |
|
||||
| 0x4012/0x4013 | 1Ub/1Uc | — | ❌ 发 IV(STM32 侧削顶,`docs/算法/继电保护功能复现规划.md` §1.6) |
|
||||
| 0x4014 | 1U0 | phasor `C03-UN-F1` | ✅ 有效(外部零序专用通道) |
|
||||
| 0x4015 | 1Ia | phasor `C01-IA-F1` | ✅ 有效 |
|
||||
| 0x4016~0x4018 | 1Ib/1Ic/1I0 | — | ❌ 发 IV(同上) |
|
||||
| 0x401D | 1f 频率 | 软件测频 | ❌ 发 IV(L1-7 未实现) |
|
||||
|
||||
- 现读数为**原始码值**(L1-5 标定未实现,系数=1 口径与之一致);死区本期内置
|
||||
10.0 码值/点,标定落地后复核。
|
||||
- 相量未就绪(未满一周波)→ QDS 置 NT(0x20),与 IV(无效通道)语义区分。
|
||||
|
||||
### 3.2 遥信 YX(IOA 1~33)
|
||||
|
||||
| 段 | 点 | 来源 | V0.1 状态 |
|
||||
|---|---|---|---|
|
||||
| 1~4 | 开关位置(双点) | 无出口/位置硬件 | ❌ IV |
|
||||
| 5~14 | 预留 | — | ❌ IV |
|
||||
| 15 | 故障总 | OC1∨OC2∨OC3∨3U0(`prot_elm`) | ✅ |
|
||||
| 16~18 | 过流 Ⅰ/Ⅱ/Ⅲ 段 | `PE_OC1/2/3` asserted | ✅ |
|
||||
| 19~20 | 零序过流 Ⅰ/Ⅱ | L3-2 未实现 | ❌ IV |
|
||||
| 21~22 | 接地故障/重载 | 未实现 | ❌ IV |
|
||||
| 23 | 过载 | `PE_OL` asserted | ✅ |
|
||||
| 24~30 | 外部遥信(RS-485) | B2 暂缓(`docs/待处理内容.md` §2.4) | ❌ IV |
|
||||
| 31 | 保护闭锁 | `prot_set_locked()` | ✅ |
|
||||
| 32~33 | 预留 | — | ❌ IV |
|
||||
|
||||
> **点存在性优先**:无效点照常入总召/周期表并置 IV——点表是验收口径,缺行比
|
||||
> 置 IV 更糟(主站侧无法区分"没做"与"接线错")。
|
||||
>
|
||||
> **保护类 YX(15/16~18/23/31)自 B5(2026-10-12)起为活数据**:protectTask
|
||||
> 20 ms 判据节拍驱动 `prot_elm` 状态 → pts COV 变位上送(≤120 ms 端到端)。
|
||||
|
||||
### 3.3 遥控 YK(IOA 0x6000+,双点两段式)
|
||||
|
||||
| IOA | 点 | V0.1 行为 |
|
||||
|---|---|---|
|
||||
| 0x6000~0x6003 | YK1~4 开关 | **否定确认**(无出口硬件,`docs/算法/继电保护功能复现规划.md` R7) |
|
||||
| 0x600A | YK11 信号复归 | ✅ 执行段 → `prot_elm_request_reset()`(B5 起跨上下文安全:请求由 protectTask 下一拍 eval 头部消费,不当拍直接复位) |
|
||||
| 0x600B | YK12 遥控软压板 | 否定确认(无全局压板项,随 P4 定值集成) |
|
||||
| 0x600C | YK13 电池活化 | 否定确认(无对应硬件) |
|
||||
| 0x600D | YK14 解除闭锁 | 否定确认(闭锁源于定值校验和,只能重写定值解除) |
|
||||
|
||||
### 3.4 定值(IOA = 通讯地址)
|
||||
|
||||
- 地址 = `App/algo/prot_set.h` 元数据 `comm_addr`(0x8820 基址 + 告警段 0x8027~0x802A,
|
||||
逐项对照参考项目 `Const_CommSetPara.c`,见 `docs/算法/继电保护功能复现规划.md` §5.4)——
|
||||
**点表不另立地址表**,prot_set 即单一事实源。
|
||||
- 写两段式:选择 = 范围校验;执行 = `prot_set_set` + `prot_set_seal`(校验和封存,
|
||||
L5-1 闭环:主站改错值 → 自检闭锁链自动生效)。
|
||||
|
||||
## 4. 链路与流程
|
||||
|
||||
- 参数:k=12 / w=8 / t1=15s / t2=10s / t3=20s(标准默认);序号 15bit 回绕,
|
||||
超时判定一律差值法(tick 回绕安全,同 `App/algo/` 惯例)。
|
||||
- 状态机:IDLE→(TCP 建立)OPEN→(STARTDT act/con)RUN⇄STOPPED;t1 超时或坏帧
|
||||
→ 实例回 IDLE,**由胶水层负责断 socket**(纯逻辑层不感知 I/O)。
|
||||
- 总召(C_IC 组 1):actcon → 分片背景扫描(YX 双点组 SQ=1 → YX 单点组 SQ=1 →
|
||||
YC 组 SQ=1 → 频率单点)→ actterm;首召/ RBT 位 → COT=4(初始化),否则 COT=2。
|
||||
单次 poll 发送预算 4 个 ASDU(防大扫描饿死宿主任务)。
|
||||
- 突发:COV = 遥信变位 + 遥测死区(`iec104_pts_cov_check`),COT=3 逐点 SQ=0。
|
||||
- 周期:RUN 态 60 s 全 YC 上送 COT=1。
|
||||
- 遥控/定值写:两段式,失配/越界 → COT|PN 否定确认;未知 IOA → COT=14+EVI、
|
||||
错公共地址 → COT=13+EVI、未知类型 → COT=12+EVI。
|
||||
- 对时 C_CS:CP56 → epoch ms → 胶水层 `sys_clock` 设置(IV 位置起即 PN)。
|
||||
|
||||
## 5. 板端接入(iec104Task 胶水,2026-10-12 实现回填)
|
||||
|
||||
- **承载任务 = 专用 iec104Task(640 words / Normal)**,非 netTask:netTask 是
|
||||
socket RPC 执行者,`net_recv/net_send` 在 netTask 内调用 = 自己等自己回执(死锁)。
|
||||
手写于 `Src/freertos.c` USER CODE 区(timeSyncTask/adcTask 同款先例),文件
|
||||
`App/task/iec104_serv.c`(纯编排,协议语义全在 `App/protocol/iec104/`)。
|
||||
- **看门狗**:`TASK_WD_IEC104 = 7` 纳入 `wd_required_mask()` 两分支(产品 0x1F7 /
|
||||
HW 测试层 0x1F5,含 B5 protect 位 bit8);主循环 100ms 节拍喂位,屏障等待按
|
||||
100ms 分片喂位(timeSyncTask 2026-10-02 台架先例)。
|
||||
- **就绪屏障**:等 `SYS_EVT_NET_READY`(`osFlagsNoClear` + 分片)后才建监听。
|
||||
- **单连接语义**(`AGENTS.md` 模块约定,抄 `ui_net_sink.c` 模板):2404 监听与
|
||||
数据通道复用同一 socket(accept 返回即 listen fd);EOF 时 net 层 auto_relisten,
|
||||
应用层**不得**关 fd(`client == listen` 分支);仅模拟器多连接后端(fd 不同)显式关。
|
||||
- **门控 + 兜底**:`net_can_accept`/`net_can_read`(纯 RAM 读)省无谓 RPC 往返;
|
||||
每 32 轮无条件 recv 一次防 RECV_OK 中断丢失(ui_net_sink I-1/I-2 先例)。
|
||||
- **网络代次自愈**:`net_get_generation()` 变化 → 关旧 fd 重建监听(netTask
|
||||
NET_RECONFIG 重建路径的既有约定)。
|
||||
- **主动拆链**(与 EOF 路径相对):协议故障(坏帧/N(R) 倒退/t1 超时)或发送连续
|
||||
3 次失败(E-04 短写先例)→ 本端 `net_close` + 重建监听,促主站重连——违例时
|
||||
主站不会自行断开,必须本端断(time_sync 会话收尾先例,非芯片 auto_relisten 风暴路径)。
|
||||
- **抢占策略修正(V0.1 如实登记)**:建档时定"后连优先",实现后确认单连接
|
||||
socket 在连接态无法 accept(监听即数据通道),**V0.1 实为首到优先**——旧会话
|
||||
经 EOF/t1/拆链结束后新主站才能接入;后连优先需 net 层支持,见 §8 第 7 条。
|
||||
- **k/w 与 RPC 时延**:`net_send` 经 netTask 串行 RPC(最坏往返无界 ≈4 s);
|
||||
t1/t2/t3 全部用注入 tick 判定,单次 RPC 停顿不会误触 t1(15 s),连续停顿由
|
||||
"发送 3 连败拆链"兜底。COV 突发延迟 ≤100ms(服务周期),远小于 SOE 5000ms 线。
|
||||
- **对时**:C_CS → `sys_clock_set_unix()`(含 SD2506 RTC 写回);epoch 秒 >2^32
|
||||
(2106-02-07 后)显式拒绝回 PN。公共地址 V0.1 固定 1。
|
||||
|
||||
## 6. RAM / 栈预算(2026-10-12 实测)
|
||||
|
||||
| 项 | 实测 | 依据 |
|
||||
|---|---|---|
|
||||
| 静态(新增) | link 实例 ~560 B(含 512 B 流重组缓冲)+ pts 快照 ~120 B + 胶水 rx 128 B | `iec104_link_t` 字段和 |
|
||||
| iec104Task 栈 | 640 words(2560 B):静态最坏链 1456 B(拆链→重建监听→net_bind→SPI 故障→日志尾),余 1104 B / 43% **PASS** | `stack_report.py` 2026-10-12;512 words 时余 592 B < ISR 底线 → 抬栈 |
|
||||
| CCM 堆 | 任务栈 2.5 KB + TCB,min_ever 6,640 → ≈6,100 B(> 3,072 告警线) | adcTask 抬栈同口径台账 |
|
||||
| 总量门禁 | RW_IRAM1 73.1% / RW_CCM 75.3%(<92%) | `ram_baseline.py --update` 2026-10-12 |
|
||||
|
||||
## 7. 测试方案
|
||||
|
||||
| 层 | 内容 |
|
||||
|---|---|
|
||||
| L0.5 | `test/host/iec104/`:APCI 重组/坏帧、STARTDT 握手、总召全流程(含 RBT/分片/预算)、COV、k/w 窗、t1/t2/t3(假时基)、遥控两段式与 PN、定值两段写 + seal 后 selfcheck、C_RD、C_CS(含闰年)、错地址/错类型 EVI |
|
||||
| L0 | `iec104-unit-test` 钩子 + cppcheck/coding-standard |
|
||||
| 台架 | `@test` 锚点(104 服务启动/监听行);主站工具(lib60870 或等效)总召+遥测+遥控往返,随 B4 收尾 |
|
||||
|
||||
## 8. V0.1 限制与待办(如实登记)
|
||||
|
||||
1. 固有参数 8001H~800AH 未实现(字符串类无法短浮点上送)→ 未知 IOA 应答;随送检需求再设计。
|
||||
2. SOE 带时标(M_SP_TB_1)随 L4-4 SOE 记录落地;现突发只发 COT=3。
|
||||
3. YK1~4/12/13/14 否定确认——出口硬件确认后(R7)转真执行。
|
||||
4. 遥测死区 10.0 码值为拟定值,L1-5 标定后按工程量死区(点表 8020H 段)复核。
|
||||
5. 多主站/双链路冗余不做(单连接语义)。
|
||||
6. 主站侧验证工具与送检环境未就绪——台架只做到"第三方 104 主站可总召"级。
|
||||
7. **后连优先未实现**(V0.1 = 首到优先):单连接 socket 连接态无法 accept,
|
||||
旧会话结束前新主站接入被拒;后连优先需 net 层(CH395F 单连接模型)支持,
|
||||
真需要冗余切换时随 net 扩展立项。
|
||||
8. 公共地址固定 1、监听端口固定 2404:可配置化(config.ini)随二期。
|
||||
|
||||
## 变更记录
|
||||
|
||||
| 日期 | 变更 |
|
||||
|---|---|
|
||||
| 2026-10-12 | 建档(B4):范围/分层/点表/链路流程/接入决策/预算/测试方案;§5 接线细节随实现回填 |
|
||||
| 2026-10-12 | B4 落地回填:承载改**专用 iec104Task**(netTask 承载会 RPC 自死锁);§5 全量回填(门控/代次/拆链/对时/看门狗位 7);抢占策略修正为**首到优先**(§8 第 7 条);§6 实测(640 words,链 1456 B,余 43% PASS) |
|
||||
@@ -24,7 +24,7 @@
|
||||
| 超时 | 标称 **2498 ms**,最坏(LSI 47 kHz)**≈1701 ms** | `WD_IWDG_PRESCALER=4` / `WD_IWDG_RELOAD=1249`(`task_watchdog.c`) |
|
||||
| 抽样周期 | **100 ms**(`WD_POLL_PERIOD_MS`) | 同上 |
|
||||
| 缺失容忍 | **连续 8 轮 = 800 ms** 后停喂(`WD_MISS_TOLERANCE`) | 同上 |
|
||||
| 必需掩码 | 产品构建 **`0x77`**(default/net/ftp/ui/timesync/adc);`ENABLE_HW_LAYER_TESTS` 下 **`0x75`**(剔 net) | `wd_required_mask()` |
|
||||
| 必需掩码 | 产品构建 **`0x1F7`**(default/net/ftp/ui/timesync/adc/iec104/protect);`ENABLE_HW_LAYER_TESTS` 下 **`0x1F5`**(剔 net) | `wd_required_mask()` |
|
||||
| 喂狗者 | **仅 defaultTask**(`task_wd_poll`,主循环每轮调用、内部节流) | `App/task/default_task.c` |
|
||||
| 启动时刻 | **defaultTask 存储 bring-up 之后**(`task_wd_start()`);`MX_IWDG_Init()` 只填参数不启动 | `default_task.c:175`、`Src/iwdg.c` 的 `USER CODE BEGIN 0` |
|
||||
| 复位归因 | 停喂 → IWDG 自然溢出 → `RCC_CSR.IWDGRSTF`(不用 `NVIC_SystemReset`) | `task_wd_poll()` |
|
||||
@@ -678,3 +678,4 @@ enter/exit 段还能读出"此刻仍在段内已多久"——**永久阻塞的
|
||||
| 2026-10-10 | **B1 `@test` 实机闭环:adcTask 掩码真机验证** | 台架闭环(`.dsh/skills/serial-boot-test/scripts/run_boot_test.py`)确认 `adcTask` 纳入掩码后**无 adc 相关误复位**:`[STACK] adc peak_used=496/2048 free_min=1552 (75%)`、`frames/seq` 每 10 s 严格 +10000(=1000 Hz)、`idle=0`/`frst_err=0`、全片回读 `No difference found`。启动早期两次 `MISS start snap=0x67 req=0x77` 仅缺 bit4(`timeSync` 未启动,属正常就绪窗;adc 的 bit6 自始置位),掩码随后收敛、全程无复位。同轮另修 `dbg_console_writer_ready()` 门控(adcTask 周期日志在写者注册前抑制,见 `控制台单写者日志设计说明.md`) |
|
||||
| 2026-10-10 | **新增 §11:任务级饥饿的诊断方法与实测基线** | 承接 Task #32(`task stall: mask=0x67 req=0x77`,缺 `TASK_WD_UI`)结案后的沉淀——原分析文档 `docs/问题记录/UI任务看门狗停摆分析.md` 按"完成即销项"删除,可复用部分并入本文:**§11.1** 三条易误读的判读口径(启动期 `MISS start 0x67` 良性、`miss=` 是累计不是连续、`reset cause` 不能区分 IWDG 与台架);**§11.2** 诊断四步(先看掩码缺几位 → 量化占空比 → 用计数速率反证空转 → 用 `[PROF]` 指认占用者;本例否掉"CPU 饱和",实测 10%~13%);**§11.3** 三条结构性脆弱点(uiTask 唯一 BelowNormal/心跳依赖 netTask 往返/自定截止期不可恢复)与两个顺带修掉的**正确性**缺陷(无新帧仍压环 ≈3% 频率失配、失败拍静默沿用旧值);**§11.4** `[PROF]`/`[PROF2]` 全字段语义 + v2.8 基线(含 `netp=0` 是真实值、`adcm` 峰值来自心跳日志、`uip` 地板 = LVGL 帧周期)与三机理判读口诀;**§11.5** 三条教训(门控须留低频兜底、门控会消灭暴露路径、事件驱动要核对节拍源与消费者创建时刻);**§11.6** 明确不做的四条及理由。§9.1 的 R7 行改指本文 §11.3 |
|
||||
| 2026-10-11 | **新增 §10.4:ARMCC `__asm` 的三条语法约束** | AGENTS.md 瘦身:「ARMCC 内联汇编语法」条目的完整细节(GCC 语法报 `#18`、块内联把寄存器名当 C 标识符报 `#20`、`__asm` 取不到 C 数据符号 `A1516E`、可用写法 = 整函数 `__asm void f(void)` + `B` 到 C 入口、`#define f f_Gen` 让位机理)迁入本文,AGENTS.md 只留一行禁令与指针 |
|
||||
| 2026-10-12 | **iec104Task / protectTask 纳入监视(B4/B5):必需掩码 `0x77`→`0xF7`→`0x1F7`** | 两行合并记录(B4 落地时漏补本文,B5 一并纠偏):**B4** 新增 `TASK_WD_IEC104 = 7`(iec104Task 100 ms 服务循环喂位,NET_READY 屏障等待按 100 ms 分片喂位),产品 `0x77`→`0xF7`、HW 层 `0x75`→`0xF5`;**B5** 新增 `TASK_WD_PROTECT = 8`(protectTask 20 ms 判据节拍喂位,见 `docs/算法/继电保护功能复现规划.md` §7),产品 `0xF7`→`0x1F7`、HW 层 `0xF5`→`0x1F5`。两任务均**不依赖存储/网络之外的屏障**(protectTask 无任何屏障)→ 任何构建都置位;均不做 NAND I/O → 不登记 FTL 心跳路由。主机侧单测 `beat_all()` 与 `tc_003`/`tc_005`/`tc_011` 掩码断言同步至 `0x1F7`,39 断言全绿。⚠ 注意 `0x1F7` 不含 bit3(testTask 任何构建不入掩码,口径不变) |
|
||||
|
||||
+110
-85
@@ -1,58 +1,64 @@
|
||||
# 待处理内容(统一待办登记簿)
|
||||
|
||||
> **用途**:**全项目唯一的未闭环事项登记簿**——汇总"审计发现 / 台架观察 / 测试充分性与架构
|
||||
> 专项 / 任务排序"四类待办。原 `审查记录/项目审计改进清单.md`(P-x)、`审查记录/下一步任务规划.md`
|
||||
> (阶段与基线)、`审查记录/测试与架构改进建议.md`(S1x / A-x)、`docs/待处理内容.md`(N-x / S-x / T-x)
|
||||
> 于 2026-10-04 合并入本文件,**编号一律沿用,不回收**。
|
||||
> **这个文件是什么**:全项目唯一的未闭环事项清单 + 全项目唯一权威的工作先后顺序。
|
||||
>
|
||||
> **只保留待办**:**已完成条目直接删除,不留变更记录**(历史见 `git log` 与各模块文档);
|
||||
> **怎么用**:想知道"现在该做什么、按什么顺序" → 只看 **§2**;想知道"某条待办的来龙去脉" → 按编号查 **§3**;
|
||||
> 想知道"还差哪些验证" → §2.3。其余章节:编号规则(§0/§8)、待确认(§4)、设计与诊断基线(§5~§7)。
|
||||
>
|
||||
> **只保留待办**:已完成条目直接删除,不留变更记录(历史见 `git log` 与各模块文档);
|
||||
> 已闭环项的**结论与证据沉淀进对应模块文档/问题记录**,本文件不再重复。
|
||||
> **消化即销项**:事项修复/关闭后,直接删除对应条目,并同步清理其它文档对本编号的引用。
|
||||
>
|
||||
> **不在本文件范围**:`问题记录/错误码传播审计.md`(E-xx,审计报告,非待办池)、
|
||||
> `问题记录/项目审查与优化建议报告.md`(体系层评审报告)、`测试说明/实机测试实施指南.md`(执行流程)。
|
||||
>
|
||||
> **创建**:2026-09-22(L3 pending 台架收尾);**合并重写**:2026-10-04;**统一排序 + 审查报告并轨**:2026-10-11。
|
||||
> **修订**:2026-10-04 四档合并;2026-10-11 统一排序 + 审查报告并轨;
|
||||
> **2026-10-12 按用户决定 RS-485(B2)整簇暂缓(§2.4),波次重排为 W1~W7,清除已销项残留**;
|
||||
> **2026-10-12 W1-0 台账三件套落地(N-27 前半闭环,`mem-budget` 钩子),N-23 钩子落地残余并入 C5**;
|
||||
> **2026-10-12 B4/B5 销项——W1 功能主线代码侧全部完成**(验收窗口见 §2.1)。
|
||||
|
||||
---
|
||||
|
||||
## 0. 编号空间与来源
|
||||
|
||||
| 编号 | 含义 | 原出处 | 备注 |
|
||||
|---|---|---|---|
|
||||
| `P0-x` / `P1-x` / `P2-x` / `P3-x` | 审计缺陷/改进项 | `项目审计改进清单.md`(2026-09-15/09-21 审计) | P0=功能 bug、P1=疑似 bug/健壮性、P2=改进、P3=卫生 |
|
||||
| `N-x` | 跨文档观察/登记项 | `待处理内容.md` 登记簿 | 台架/审计发现、暂不在修复范围;**含硬件改版项**(需下一版硬件解决,固件侧只记口径,如 N-18) |
|
||||
| `S-x` | 存储/日志管线待办 | 同上 | 存储侧健壮性 |
|
||||
| `T-x` | 测试/验证基础设施待办 | 同上 | 工具链、CI |
|
||||
| `S1~S14` / `A-1~A-7` | 测试充分性与架构改进专项 | `测试与架构改进建议.md`(2026-10-02 复审) | **编号不随条目删除而回收** |
|
||||
| `E-xx` | 错误码传播审计发现 | `错误码传播审计.md` | **不并入本文件**,交叉引用 |
|
||||
| 阶段 `A/B/C/D` + `B0~B3`/`C1~C6`/`D2~D6` | 任务排序分组 | `下一步任务规划.md` | 见 §2 |
|
||||
| 报告 `N-x`/`R-x`/`F-x`/`S-x`/`U-x`/`D-x` | **审查报告自有编号** | `项目审查与优化建议报告.md` | ⚠ **与本登记簿 N-x/S-x 同名不同义**(报告 N-16 = 日志逐条开关,登记簿 N-16 = A2/A4 负向台架;报告 S-3 = check_registration.py,登记簿 S-3 = USART1 TX-only)→ **引用一律带文档名**。2026-10-11 其未闭环项已并入本登记簿,新编 `N-19`~`N-29`(出处列保留报告编号) |
|
||||
| 编号 | 含义 | 备注 |
|
||||
|---|---|---|
|
||||
| `P0-x`~`P3-x` | 审计缺陷/改进项(2026-09-15/09-21 审计) | P0=功能 bug、P1=疑似 bug/健壮性、P2=改进、P3=卫生 |
|
||||
| `N-x` / `S-x` / `T-x` | 台架/审计观察登记项 / 存储·日志管线 / 测试与工具链 | 编号**不回收**;`N-x` 含硬件改版项(需下一版硬件,固件侧只记口径,如 N-18) |
|
||||
| `S1~S14` / `A-1~A-7` | 2026-10-02 复审的测试充分性与架构专项 | 编号**不回收** |
|
||||
| `E-xx` | 错误码传播审计发现 | 独立维护于 `错误码传播审计.md`,**不并入本文件**,只交叉引用 |
|
||||
| `B`/`C`/`D` 前缀 | §2 排序条目编号:B=功能主线、C=结构债、D=验证窗口 | 编号不回收,见 §2 |
|
||||
| 报告 `N-x`/`R-x`/`F-x`/`S-x`/`U-x`/`D-x` | `项目审查与优化建议报告.md` **自有编号** | ⚠ **与本登记簿同名不同义**(报告 N-16 = 日志逐条开关,登记簿 N-16 = A2/A4 负向台架)→ **引用一律带文档名**。其未闭环项 2026-10-11 并入本登记簿(新编 `N-19`~`N-29` + U 簇,出处列保留报告编号) |
|
||||
|
||||
**严重度定义**(沿用审计口径):P0 = 确认功能 bug|P1 = 疑似 bug 或健壮性风险|P2 = 改进|P3 = 卫生。
|
||||
**置信度**:high = 代码级确证|medium = 依赖外部条件(手册/硬件/产品定义)。
|
||||
|
||||
---
|
||||
|
||||
## 1. 未闭环一览(按严重度)
|
||||
## 1. 未闭环一览(按类别)
|
||||
|
||||
| 级别 | 数量 | 关键项 |
|
||||
| 类别 | 数量 | 条目 |
|
||||
|---|---|---|
|
||||
| **安全** | 0 | — |
|
||||
| **P1** | 0 (+1 收尾) | P1-10 测试固件构建实测闭环(主体已闭环,见 §3.1);~~N-30~~ 已销项(2026-10-11 三连定案修复,回归 4 轮 11/11,见 `问题记录/CH395F_Trap_Records.md` Trap 25) |
|
||||
| 登记簿 | 20 | 网络:N-2(49s 日志静默,**低风险观察项**)、N-3(L5 基线重验)、N-4(pending 二期)、N-16(A2/A4 专项负向验证)、N-17(探针窗口心跳缺口);存储/日志:**S-3(USART1 TX-only 根治)**、S-4(dhara 重试自旋)、S-5(可选全序列卷锁)、N-22(写回 flush)、N-24(日志逐条开关);数据正确性:**N-21(dhara is_free/copy,报告 N-12/13)**;结构:**N-20(Error_Handler 死循环)、N-23(注册反向校验)、N-26(NAND DMA 断言)**;预算:**N-27(内存优化簇)、N-28(Flash 死代码簇)**;仓库:**N-29(.git 149 MB,需用户决策)**;硬件:**N-18(V0.2 改版项)** |
|
||||
| 测试/基础设施 | 6 | T-7(CI runner 未就绪 + 推送后两 job 闭环 + 第二机交叉验证)+ **S12**(断言宏)、**S1-c**(可选单测)、**A-4**(采集链路测试约束)、**A-5 残余**(CI 体积门禁盲区)、**D7**(串口助手 A6/A7 待 CH340) |
|
||||
| P1 收尾 | 1 | P1-10 测试固件构建实测(§3.1) |
|
||||
| 网络 | 5 | N-2(49s 日志静默,**低风险观察项**)· N-3(L5 基线重验)· N-4(pending 二期)· N-16(A2/A4 负向台架)· N-17(探针窗口心跳缺口) |
|
||||
| 存储 / 日志 | 3 | **S-3(USART1 TX-only 根治)** · S-4(dhara 重试自旋)· S-5(可选全序列卷锁) |
|
||||
| 数据正确性 | 1 | **N-21(dhara `is_free`/`copy`,全簿最高风险)** |
|
||||
| 结构 / 健壮性 | 8 | N-20(Error_Handler 死循环)· N-22(写回 flush)· N-23(注册校验残余白名单)· N-24(日志逐条开关)· N-25(性能/语义簇)· N-26(NAND DMA 断言)· N-27(内存优化簇,三件套 ✅ 前半落地)· N-28(Flash 死代码簇) |
|
||||
| 仓库 | 1 | N-29(`.git` 149 MB,**已决策暂不处理、登记观察**) |
|
||||
| 硬件 | 1 | N-18(V0.2 改版项) |
|
||||
| 测试 / 基础设施 | 6 | T-7(CI runner + 推送闭环 + 第二机交叉验证)· S12(断言宏)· S1-c(可选单测)· A-4(采集链路测试约束,RS485 侧随 B2 暂缓)· A-5 残余(CI 体积门禁盲区)· D7(串口助手 A6/A7 待 CH340) |
|
||||
| UI | 1 簇 | U 簇(27 页标准化,**本轮不排期**) |
|
||||
| **暂缓** | 1 簇 | **B2 RS-485 整簇(2026-10-12 用户决定,§2.4)** |
|
||||
|
||||
> **网络侧另有 E-xx 未闭环项**(E-12/15/16/23/24/25/26/27/28/30/47/48/49/50/58/59/61,共 17 项)
|
||||
> 与 1 项回归监督(E-62),详见 `问题记录/错误码传播审计.md` §3。本文件不重复登记。
|
||||
> **UI 标准化簇**(报告 U1~U5,27 页未落地)与**覆盖率缺口**(lftpd/dhara 移植层无主机测试)
|
||||
> 为整簇排期项,见 §2 W7 / N-21,明细指报告 §6/§7。
|
||||
> 与 1 项回归监督(E-62),详见 `问题记录/错误码传播审计.md` §3——其中 **E-49/E-50 随 RS-485 暂缓**。
|
||||
> 本文件不重复登记。UI 标准化簇与覆盖率缺口(lftpd/dhara 移植层无主机测试)为整簇排期项,见 §2 W6 / N-21。
|
||||
|
||||
### 1.1 结构性判断(排序依据)
|
||||
|
||||
1. **产品功能主线只剩"采集与转发"两环**:ADC 侧 ✅(B1 台架闭环);**RS-485 侧被 E-50/E-49 挡着(B2)**;**上行 IEC104(B4)与 protectTask 立项(B5)此前只写在产品需求/保护规划,未进本登记簿——2026-10-11 补登**。B4 数据源已具备,可与 B2 并行。
|
||||
2. **数据正确性最高风险 = N-21**(dhara `is_free` 只查页首 64 B / `copy` 不处理哨兵):低概率、后果重(数据丢失),且**至今只能靠人工读代码发现**——修复须配主机侧移植层测试(一并补报告"覆盖率"缺口)。
|
||||
3. **内存优化被预算卡死**:CCM 只剩 16,304 B,54 KB 的 LVGL 池+UI 缓冲迁不进去(R-2 后半段堵死)→ 必须先做 §5.5 台账三件套(`ram_baseline.tsv` + 预算台账 + 92% 门禁)再排期 R-9→R-2。
|
||||
1. **产品功能主线"转发 + 保护"两环代码已落地(2026-10-12)**:采集侧 ADC(B1)已台架闭环;上行 IEC104(B4)与保护判据 protectTask(B5)代码均已落地(`App/protocol/iec104/` + iec104Task;`App/task/protect_task.c` 20 ms 判据节拍→104 遥信),**遗留为验收窗口**(台架主站往返随送检、标准源注入、`@test` 水位)。**RS-485 对下采集(B2)按用户决定暂缓(2026-10-12,§2.4),主线不再等它**。
|
||||
2. **数据正确性最高风险 = N-21**(dhara `is_free` 只查页首 64 B / `copy` 不处理哨兵):低概率、后果重(数据丢失),且至今只能靠人工读代码发现——修复须配主机侧移植层测试(一并补报告"覆盖率"缺口)。
|
||||
3. **内存预算红线已机械化(2026-10-12,W1-0)**:`mem-budget` 钩子拦 RW_IRAM1/RW_CCM >92%,子系统配额 / CCM 台账 / 趋势基线 → `docs/内存管理/RAM预算台账.md`;实际优化 R-9→R-2 排 W3(**B5 堆扩容 48→56 KB 后 CCM 只剩 ≈8,160 B**,解锁链见 `docs/内存管理/RAM预算台账.md` §3)。
|
||||
4. **验证债仅剩窗口性事项**(D2/D5/D6/D7 + P1-10 收尾),不单独排期、逢窗口顺手做。
|
||||
|
||||
---
|
||||
@@ -61,44 +67,49 @@
|
||||
|
||||
> **本节是全项目所有工作流唯一的权威顺序**:`产品需求设计文档.md` §9.2、`算法/继电保护功能复现规划.md`、
|
||||
> `项目审查与优化建议报告.md` §0.3 中的路线图均为局部视图,**顺序变更只改本节,其它文档只留指针**。
|
||||
> 排序原则:**安全风险 > 产品功能主线 > 数据正确性 > 预算/结构债 > 验证常态 > UI 扩展 > 仓库决策**。
|
||||
> 排序原则:**安全风险 > 产品功能主线 > 数据正确性 > 预算/结构债 > 验证常态 > UI 扩展 > 仓库决策**(当前无未闭环安全项)。
|
||||
|
||||
### 2.0 波次总览
|
||||
|
||||
| 波次 | 内容 | 为什么在这 |
|
||||
|---|---|---|
|
||||
| **W2 功能主线** | **修 E-50/E-49 → B2** RS-485(ModBus RTU)∥ **B4** IEC104 上行 → **B5** protectTask 立项 | DTU 核心职责"采集与转发"仅剩两环:RS-485 侧被 E-59 挡、上行侧未动工;B4 数据源(ADC ✅)已具备,**可与 B2 并行**。**已定(2026-10-11):B2 先行(先修 E-50/E-49 驱动契约)、B4 并行开工** |
|
||||
| **W3 数据正确性** | **N-21** dhara `is_free`/`copy`(报告 N-12/13) | 低概率、后果重(数据丢失);修复须配主机侧移植层测试,一并补报告"覆盖率"缺口 |
|
||||
| **W4 预算与内存** | **N-27**:`docs/问题记录/项目审查与优化建议报告.md` §5.5 台账三件套 → R-9 池 40→16 KB → R-2 后半(54 KB→CCM) | CCM 只剩 16,304 B,R-2 后半被堵死;三件套是排期前提且工作量小 |
|
||||
| **W5 结构债** | C1 pending 二期 · C3 日志管线(N-2 + N-24)· C5 收口 · C6 USART1(S-3)· N-20 `Error_Handler` · N-23 注册反向校验 · N-22/N-25/N-26/N-28 | 二期/健壮性/评估类,无紧急;N-23 是唯一"1 小时工作量"的一条;~~N-30~~ 已销项(Trap 25 定案修复,2026-10-11 回归 4 轮 11/11);~~N-31~~ 已销项(FUN_PARA=0x08 定案 + 0x08 下多 Socket 实测 Phase 7 34/34,见 `问题记录/CH395F_Trap_Records.md` Trap 18「定案更正」) |
|
||||
| **W6 验证常态** | P1-10 收尾 · T-7 CI · D2 N-3 · D5 N-16 · D6 N-17 · **D7 串口助手 A6/A7** · S12 · S1-c · A-5 | 窗口性,逢机顺带,不单独排期 |
|
||||
| **W7 UI 扩展** | U4 残余(golden + 字符集差集挂门禁)→ U1 令牌层 → U2/U3 → **27 页**落地 | 产品驱动、体量大,需单独立项(顺序按 `docs/问题记录/项目审查与优化建议报告.md` §6.5) |
|
||||
| **W8 仓库决策** | **N-29** `.git` 149 MB + 图片 36.3 MB | `git filter-repo` 历史重写影响未推送提交与协作者 → **需用户决策** |
|
||||
| **W1 功能主线** | **B4 IEC104 上行 ✅ 2026-10-12 → B5 protectTask ✅ 2026-10-12(W1 代码侧全部完成)**(W1-0 台账三件套 ✅ 2026-10-12 落地,见 N-27) | DTU 核心职责"转发 + 保护"两环代码均已落地:B4(`App/protocol/iec104/` 三件套 + iec104Task 2404,82 断言 L0.5 + `iec104-unit-test` 钩子);B5(`App/task/protect_task.c` 20 ms 判据节拍 + L5-4 采样闭锁 + IWDG bit8 + 栈 512 words 静态链 872 B/余 57% + 堆 48→56 KB,见 `docs/算法/继电保护功能复现规划.md` §7 变更记录)。**遗留 = 验收窗口**:台架第三方主站往返(`docs/子系统设计/IEC104上行设计说明.md` §7/§8)+ 标准源注入动作判据 + `@test` PROT 锚点/水位 |
|
||||
| **W2 数据正确性** | **N-21** dhara `is_free`/`copy`(报告 N-12/13) | 低概率、后果重(数据丢失);修复须配主机侧移植层测试,一并补报告"覆盖率"缺口 |
|
||||
| **W3 预算与内存** | **N-27 后半**:R-9 池 40→16 KB → R-2 后半(54 KB→CCM)→ R-3~R-6 逐项评估 | 依赖 W1 三件套产出的台账数据;**B5 堆扩容后 CCM 只剩 ≈8 KB**(56 KB 堆),R-2 后半更被堵死(解锁链见 `docs/内存管理/RAM预算台账.md` §3) |
|
||||
| **W4 结构债** | **N-20 → C6(S-3)→ C3(N-2+N-24)→ C1 pending 二期 → C5(含 N-23 残余)→ N-22/N-25/N-26/N-28** | 无紧急,按性价比排:C6 灭 IWDG 误复位机制根源;C1 体量大、验证重,放后(N-23 钩子已落地 2026-10-12,残余并入 C5) |
|
||||
| **W5 验证常态** | P1-10 收尾 · T-7 CI · D2 N-3 · D5 N-16 · D6 N-17 · D7 串口助手 · S12 · S1-c · A-5 | 窗口性,逢机顺带,不单独排期 |
|
||||
| **W6 UI 扩展** | U4 残余(golden + 字符集差集挂门禁)→ U1 令牌层 → U2/U3 → **27 页**落地 | 产品驱动、体量大,需单独立项(顺序按 `docs/问题记录/项目审查与优化建议报告.md` §6.5);**本轮不排期(2026-10-11 已确认)** |
|
||||
| **W7 仓库决策** | **N-29** `.git` 149 MB + 图片 36.3 MB | **已决策(2026-10-11):暂不处理、登记观察**——推送/协作前重评(`git filter-repo` 历史重写影响未推送提交与协作者) |
|
||||
| **暂缓** | **B2 RS-485 整簇**(§2.4) | **用户决定(2026-10-12):当前不处理 RS-485** |
|
||||
| **硬件改版** | N-18 8 路电气量全接 TPAFE(V0.2) | 下一版硬件;固件侧已用语义映射隔离,改版只动映射表 |
|
||||
|
||||
### 2.1 W2 功能主线详述
|
||||
### 2.1 W1 功能主线详述
|
||||
|
||||
> **B0 产品定义已于 2026-10-10 确认并销项**:上行 **IEC 60870-5-104**(以太网 TCP 2404,遥测短浮点系数 1)/
|
||||
> 持续采集 / **1000 Hz(20 点/周波)** / RS-485 **对下 ModBus RTU 采集**;结论详见
|
||||
> `docs/算法/继电保护功能复现规划.md` §1.5,并已同步 `docs/产品需求/产品需求设计文档.md` §5.9/§5.10/§9。
|
||||
> **B3(纪律门禁)已固化进 `AGENTS.md`**;**B1(ADC 采集链路)已 `@test` 实机闭环销项**(2026-10-10)。
|
||||
> **背景(已闭环,只留指针)**:B0 产品定义 2026-10-10 确认——上行 **IEC 60870-5-104**(以太网 TCP 2404,
|
||||
> 遥测短浮点系数 1)/ 持续采集 / **1000 Hz(20 点/周波)** / RS-485 对下 ModBus RTU,结论详见
|
||||
> `docs/算法/继电保护功能复现规划.md` §1.5 与 `docs/产品需求/产品需求设计文档.md` §5.9/§5.10/§9;
|
||||
> B1 ADC 采集链路已 `@test` 实机闭环;B3 纪律门禁已固化进 `AGENTS.md`。
|
||||
>
|
||||
> **W1 全部销项(2026-10-12)**:B4 证据见 `docs/子系统设计/IEC104上行设计说明.md`(82 断言 + 钩子 + `@build` 0/0);
|
||||
> B5 证据见 `docs/算法/继电保护功能复现规划.md` §7 变更记录(protectTask 20 ms 判据节拍 + L5-4 采样闭锁 +
|
||||
> IWDG `TASK_WD_PROTECT` bit8 + 栈 512 words 静态链 872 B/余 57% + 堆 48→56 KB + prot_elm 28 用例)。
|
||||
> **遗留验收窗口**(非代码空洞):台架第三方 104 主站往返(随送检窗口)、标准源注入动作判据、
|
||||
> `@test` PROT 锚点 + `[STACK] protect` 水位 + 新堆 min_ever 复测。
|
||||
|
||||
| 序 | 任务 | 前置 | 说明 |
|
||||
|---|---|---|---|
|
||||
| **B2** | **RS-485 采集链路实现**(消化 E-50、E-49) | **E-59 门禁尚未满足**(E-50 / E-49 未修,先修驱动契约) | 双缓冲 + 序号(防包撕裂)+ DE 收尾(超时路径等 TC)+ ModBus RTU 状态机(`App/protocol/` 正是为此预留);设计约束见 A-4 |
|
||||
| **B4** | **IEC 60870-5-104 上行**(TCP 2404,遥测/遥信/遥控上送) | B0 ✅ + 数据源(ADC ✅,**可与 B2 并行**) | 协议解析与 I/O 分离(A-4 约束 1)→ 天然适合 L0.5 主机单测;注意 CH395F 单连接约束(104 客户端与 FTP/远显/对时抢同一 socket 池,接入前定抢占策略,关联报告 N-09) |
|
||||
| **B5** | **protectTask 立项**(L3 板端接线) | B2/B4 推进中;E-59 同款门禁 | `prot_set`/`prot_elm` 软件侧已落地(链接器移除、零足迹);建任务前后各跑 `stack_report.py`;定值/回路接线按保护规划路线图走 |
|
||||
### 2.2 W4 结构债详述
|
||||
|
||||
### 2.2 W5 结构债详述
|
||||
> 波内顺序 = 性价比:先可靠性根治,后大体量重构;评估簇随窗口。
|
||||
|
||||
| 序 | 任务 | 说明 | 出处 |
|
||||
|---|---|---|---|
|
||||
| **C1** | **pending 二期(ACCEPT + SEND)** | 沿一期机制扩展;同期消化 E-25(net_select 语义)、E-24/E-27/E-28(跨上下文收口);基线重锁 + 探针族扩充 + 看门狗接 selftest。**设计基线见 §5.1** | 第二档 #2 / N-4 |
|
||||
| **C3** | **存储侧日志管线**(消化 N-2) | 丢弃可观测性**代码已具备**(`console_log.c` `s_con_drop`+`[ERR] CONSOLE dropped`、`sys_log_mcu.c` 队列满 `DBG_ERROR`、TX 僵尸通道自愈);V5 30 min 日志**未复现 49s 静默**(运行期 >10s 静默全为 ~28s、均在 `e02_probe` 窗口内=N-17)。剩余:与 S8(非阻塞日志)合并评估 + **S-3(USART1 TX-only 结构根治)** | N-2 / S-3 |
|
||||
| **C5** | 一档剩余 | E-49(RS485 DE 收尾)、E-15(ringbuf 定去留——**已确认零调用者,倾向删除**)、E-61(SD2506 注释订正)、E-16(cfg_file 段名长度)、E-26(UDP 定案,倾向移除)、E-23、E-30 | 一档/二档 |
|
||||
| **C6** | **USART1 TX-only 结构根治**(S-3) | 灭 ORE 僵尸机制根源:改 `.ioc` 令 USART1 不使能 RX / PA10 不复用为 `USART1_RX` → CubeMX 重生成 → 跑 `check_cubemx_survival.py`。属结构性改动,需单开一轮 + 复跑台架闭环 | S-3 |
|
||||
| **N-20** | **`Error_Handler` 统一降级** | 一处初始化失败 = 整机挂死(只剩 IWDG 兜底)→ 记自检 + 禁用该子系统 + 其余照常。详见 §3.2 | 报告 N-03c |
|
||||
| **C6** | **USART1 TX-only 结构根治(S-3)** | 灭 ORE 僵尸机制根源(2026-10-02 台架 IWDG 误复位的机制根源):改 `.ioc` 令 USART1 不使能 RX / PA10 不复用为 `USART1_RX` → CubeMX 重生成 → 跑 `check_cubemx_survival.py`。结构性改动,需单开一轮 + 复跑台架闭环 | S-3 |
|
||||
| **C3** | **存储侧日志管线(N-2 + N-24)** | 丢弃可观测性**代码已具备**(`console_log.c` `s_con_drop`+`[ERR] CONSOLE dropped`、`sys_log_mcu.c` 队列满 `DBG_ERROR`、TX 僵尸通道自愈);V5 30 min 日志**未复现 49s 静默**(运行期 >10s 静默全为 ~28s、均在 `e02_probe` 窗口内=N-17)。剩余:与 S8(非阻塞日志)合并评估 + 日志逐条 `f_open/f_write/f_close` 改造评估(N-24) | N-2 / N-24 |
|
||||
| **C1** | **pending 二期(ACCEPT + SEND)** | 沿一期机制扩展;同期消化 E-25(net_select 语义)、E-24/E-27/E-28(跨上下文收口);基线重锁 + 探针族扩充 + 看门狗接 selftest。**设计基线见 §5.1** | N-4 |
|
||||
| **C5** | 一档剩余 | E-15(ringbuf 定去留——**已确认零调用者,倾向删除**)、E-61(SD2506 注释订正)、E-16(cfg_file 段名长度)、E-26(UDP 定案,倾向移除)、E-23、E-30;**N-23 残余**(`KNOWN_GAPS` 白名单清理 + `app_cfg.h` 未接线开关,见 §3.2)。**E-49(RS485 DE 收尾)已移入暂缓(§2.4)** | 一档/二档 |
|
||||
| **N-22/25/26/28** | 评估簇 | 写回 flush / 性能与语义一致性 / NAND DMA 断言 / Flash 死代码——逐项见 §3.2 对应行,随窗口评估 | 报告各条 |
|
||||
|
||||
### 2.3 W6 验证常态详述
|
||||
### 2.3 W5 验证常态详述
|
||||
|
||||
| 序 | 任务 | 说明 |
|
||||
|---|---|---|
|
||||
@@ -107,21 +118,34 @@
|
||||
| **D6** | **N-17 探针窗口心跳缺口** | 定位 `time_sync` 监听重建为何阻塞 NET/FTP/UI(~1s 缺口,余量仅 ~0.7s);评估缩短重建重试或让掉线路径不阻塞其它任务 |
|
||||
| **D7** | **串口助手 A6/A7 正路径补验** | 需 CH340 硬件接入后补验(`串口助手设计说明.md` 状态行 A1–A5/A8 已过) |
|
||||
|
||||
### 2.4 顺序一图
|
||||
### 2.4 暂缓:RS-485 整簇(用户决定,2026-10-12)
|
||||
|
||||
> **当前没有处理 RS-485 的打算**,以下条目整体冻结、不占波次;**重启 B2 时先回本节评估前置,勿直接开工**。
|
||||
|
||||
| 项 | 内容 | 重启时的前置 |
|
||||
|---|---|---|
|
||||
| **B2** | **RS-485 采集链路实现**(消化 E-50、E-49):双缓冲 + 序号(防包撕裂)+ DE 收尾(超时路径等 TC)+ ModBus RTU 状态机(`App/protocol/` 正是为此预留) | **E-59 门禁尚未满足**:先修 E-50(单缓冲包撕裂)、E-49(TX 超时拉 DE 未等 TC),修完驱动契约才允许接消费者 |
|
||||
| E-49 | RS485 DE 收尾(原 C5 一档剩余,已移出) | 同上 |
|
||||
| A-4 RS485 侧 | 采集链路测试可验证性约束(双缓冲 + 序号、DE 超时路径覆盖,§3.3.3) | 随 B2 设计文档落地 |
|
||||
| §4-3 | RS-485 PD0 DE 半双工方向语义硬件实测确认 | 需硬件实测窗口 |
|
||||
| N-25 中 DE 项 | `HAL_Delay(1)` 阻塞 DE | 随 B2 |
|
||||
|
||||
### 2.5 顺序一图
|
||||
|
||||
```
|
||||
W2 功能 E-50/E-49 → B2 RS485(ModBus) ∥ B4 IEC104 上行 → B5 protectTask
|
||||
W1 功能 B4 IEC104 上行 ✅ 2026-10-12 → B5 protectTask ✅ 2026-10-12(W1 完成;台账三件套 ✅)
|
||||
↓
|
||||
W3 数据 N-21 dhara is_free/copy(配主机侧移植层测试)
|
||||
W2 数据 N-21 dhara is_free/copy(配主机侧移植层测试)
|
||||
↓
|
||||
W4 预算 内存台账三件套 → R-9 池瘦身 → R-2 后半(54KB→CCM)
|
||||
W3 预算 R-9 池瘦身 → R-2 后半(54KB→CCM)→ R-3~R-6 评估
|
||||
↓
|
||||
W5 结构 C1 pending 二期 · C3 日志(N-2/N-24) · C5 · C6(S-3) · N-20 · N-23 · N-22/25/26/28
|
||||
W4 结构 N-20 → C6(S-3) → C3(N-2/N-24) → C1 pending 二期 → C5(含 N-23 残余) → N-22/25/26/28
|
||||
↓
|
||||
W6 验证 P1-10 · T-7 CI · D2/D5/D6/D7 · S12 · S1-c · A-5
|
||||
W5 验证 P1-10 · T-7 CI · D2/D5/D6/D7 · S12 · S1-c · A-5
|
||||
↓
|
||||
W7 UI U4 残余 → U1 令牌层 → U2/U3 → 27 页(单独立项)
|
||||
W8 仓库 N-29 .git 149MB(用户决策) 硬件:N-18 V0.2
|
||||
W6 UI U4 残余 → U1 令牌层 → U2/U3 → 27 页(单独立项,本轮不排期)
|
||||
W7 仓库 N-29 .git 149MB(已决策暂不处理) 硬件:N-18 V0.2
|
||||
暂缓 B2 RS-485 整簇(2026-10-12 用户决定,§2.4)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -143,11 +167,12 @@ W8 仓库 N-29 .git 149MB(用户决策) 硬件:N-18 V0.2
|
||||
|
||||
| # | 事项 | 发现 | 证据与现状 | 出处 |
|
||||
|---|---|---|---|---|
|
||||
| N-2 | **49s 全系统日志静默(defaultTask/NAND syslog 排空停滞)**——串口日志 49s 无行,期间 net 栈照常处理外部连接 → 日志管线停滞,队列满后各任务日志行被丢;系统功能不受损 | 2026-09-22(L3 台架 F2,gen 181) | **A/B 归因:既有存储侧现象**。**2026-10-03 降级为低风险观察项**:① 丢弃可观测性**代码已具备**(`console_log.c` `s_con_drop` + `[ERR] CONSOLE dropped`;`sys_log_mcu.c` 队列满 `DBG_ERROR`;UART TX 僵尸通道自愈);② V5 30 min **未复现 49s**(>10s 静默全为 ~28s、均在 `e02_probe` 窗口内=N-17)。**残余待办**:与 S8(非阻塞日志)合并评估 | `错误码传播审计.md` E-02 阶段二 |
|
||||
| N-2 | **49s 全系统日志静默(defaultTask/NAND syslog 排空停滞)**——串口日志 49s 无行,期间 net 栈照常处理外部连接 → 日志管线停滞,队列满后各任务日志行被丢;系统功能不受损 | 2026-09-22(L3 台架 F2,gen 181) | **A/B 归因:既有存储侧现象**。**2026-10-03 降级为低风险观察项**:① 丢弃可观测性**代码已具备**(`console_log.c` `s_con_drop` + `[ERR] CONSOLE dropped`;`sys_log_mcu.c` 队列满 `DBG_ERROR`;UART TX 僵尸通道自愈);② V5 30 min **未复现 49s**(>10s 静默全为 ~28s、均在 `e02_probe` 窗口内=N-17)。**残余待办**:与 S8(非阻塞日志)合并评估(C3) | `错误码传播审计.md` E-02 阶段二 |
|
||||
| N-3 | **L5 队列往返基线(1223/988µs)的 L3 后重验**——L5 测 DONTWAIT 队列全程;L3 对该路径逐位不变 → 机制上无影响,但基线数值属分片时代,严格重锁需跑一轮 Phase 13(需 PC peer) | 2026-09-22(L3 收尾留档) | 低风险:10ms/100ms 阈值余量 >8×;下次 Phase 13 窗口顺带重锁即可 | `链路延迟测试说明.md` §6.4 L5 注¹ |
|
||||
| N-4 | **pending 机制二期:ACCEPT + SEND 阻塞路径 pending 化**——E-06、E-20、E-21 剩余分量共用该机制扩展;届时 L3/L5 基线再重锁、探针族扩充、看门狗接 selftest 留痕、模拟器差距清单 +1 | — | 一期(RECV)已落地并入主线;架构现状见 `Net层阻塞模型架构设计.md`,二期设计基线见 §5.1 | `错误码传播审计.md` 第二档 #2 |
|
||||
| N-16 | **A2/A4 专项场景台架验证待办**(正常路径已实测零回归):① PHY 拔线负向测试——断链需断 **DTU 侧**线缆或关**受管交换机端口**;② A2 芯片级 `net_init` 失败分支(需 SPI 故障注入);③ A4 短写压测(需远显客户端 + UI 推流压力) | 2026-10-03(台架拓扑 = CH395↔交换机,拔线操作受限) | 待专项台架窗口 | `实机测试实施指南.md` §10 |
|
||||
| N-17 | **`time_sync` 30s 挂起客户端掉线 + 监听重建期间,网络任务心跳缺口达 800ms~1s(声明 stall 但未复位)。**待办**:定位监听重建为何阻塞 NET/FTP/UI;评估缩短重建重试(5×2s)或让掉线路径不阻塞其它任务 | 2026-10-03(V5 台架) | V5 长跑 44 次 `task stall: mask=0x00000001`,**100% 与 `e02_probe` 的 30s 挂起客户端触发 `time_sync: no TIME packet in 30s, drop conn` → 监听重建(约 4s)重合**;隔离实验(仅 FTP,120s)**0 次 stall** → 探针场景专属。缺口(~1s)< IWDG 最坏超时(1701ms),余量仅 ~0.7s。 | `实机测试实施指南.md` §4;`子系统设计/看门狗设计说明.md` |
|
||||
| N-32 | **CH395F 监听生命周期根治(B10 残余)**——现仅靠对账④兜底(提交 `4f2cc8b`)。根治两步:**P1 把"会话已结束"从边沿改成电平**(`net_sock_t` 增 `session_dead`;`net_recv_locked` DONTWAIT 判据改 `state==CLOSED \|\| session_dead`;`ui_net_sink.c:563` 补 `g_client != g_listen` 护栏;accept 门控换 `net_listener_armed`);**P2 单一所有者**(新增 `net_listener_pump`,删 `net_accept_locked:1346`/`net_recv_locked:1884`/`net_pending_tick:2451` 散点 relisten;消灭 netTask 内 500 ms 忙等 `:2597-2616`/`:1115-1126`)。**已定案不走芯片多连接模式(A′ 出局:不支持多监听)** | 2026-10-12(B10 定案;⚠ ④ 有副作用——抢先 relisten 会把慢消费者的 EOF 永久吞掉,新客户端数据被旧会话静默吸收,待 P1 消除) | 原理 + 证据附录(实测数据 / 官方例程所有权 / 状态机 15 迁移点 / 三服务契约不一致表)+ **决策记录 D1~D7** + 可勾选执行清单 → `子系统设计/网络Socket监听生命周期设计说明.md` §7~§9;现象与回归见 `问题记录/CH395F_Trap_Records.md` B10 行 |
|
||||
|
||||
**存储 / 日志管线**
|
||||
|
||||
@@ -173,17 +198,17 @@ W8 仓库 N-29 .git 149MB(用户决策) 硬件:N-18 V0.2
|
||||
|
||||
| # | 事项 | 影响 / 现状 | 处置方向 | 波次 | 出处 |
|
||||
|---|---|---|---|---|---|
|
||||
| N-20 | `Error_Handler` 仍 `__disable_irq(); while(1){}`(`Src/main.c:274-282`) | 一处初始化失败 = 整机挂死(只剩 IWDG 兜底),无自检日志 | 统一降级策略:记自检 + 禁用该子系统 + 其余照常 | W5 | 报告 N-03c |
|
||||
| N-21 | **dhara 两处**:`dhara_nand_is_free()` 只查页首 64 B;`dhara_nand_copy()` 不处理 `DHARA_NO_PAGE` 哨兵 | 映射恢复误判 → **数据丢失风险**(低概率、后果重);哨兵页号被当真实页 → 未定义行为;至今只能靠人工读代码发现 | 整页(或首/中/尾)扫描 + 返回 `DHARA_E_UNSUPPORTED`;**必须配主机侧移植层单测**(一并补报告"覆盖率"缺口) | **W3** | 报告 N-12/N-13 + §7 |
|
||||
| N-22 | 写回缓存无空闲/掉电强制 flush(整页写与换页已 flush) | 写回窗口内掉电丢最近日志/配置写 | defaultTask 空闲 N ms flush | W5 | 报告 N-14 |
|
||||
| N-23 | **双端注册无反向校验**:`rs485_task.c`、`lftpd_log.c` 既不在 uvprojx 也不在 simulator | "写了不编译"= 死代码 + 误导 | `check_registration.py`(报告中唯一"1 小时工作量")+ 挂 pre-commit;`app_cfg.h` 未接线开关一并处置(C5) | W5 | 报告 N-06 残余 + 报告 S-3 |
|
||||
| N-24 | 日志每条 `f_open+f_write+f_close`(`sys_log_mcu.c:123-140`) | 每条日志 2–4 次 NAND 页读写;UI 翻记录逐条 open/seek/close | 与 C3 日志管线合并评估(现仅 `FIL` static 化省 528 B 栈) | W5 | 报告 N-16(**≠ 本登记簿 N-16**) |
|
||||
| N-25 | 性能/资源建议簇:PASV 每次新建监听;`NET_RECV_TIMEOUT_MS 30000`;PHY 强制 100M 无自协商回退;`NET_MAX_SOCKETS 8` 与单连接语义不符;`HAL_Delay(1)` 阻塞 DE | 次级性能/语义一致性 | 逐项见 `docs/问题记录/项目审查与优化建议报告.md` §4.2/§4.5/§4.7,随窗口评估 | W5 | 报告 N-09/P2-8/PHY/P3-x/rs485 |
|
||||
| N-26 | NAND 直 DMA 调用方缓冲(已加运行时守卫,阈值 1024) | CCM 缓冲被 DMA = 静默传错(与 ADC DMA 不可达 CCM 同类坑) | 补编译期/启动期地址断言 + **新铁律:堆缓冲不可被 DMA 直接寻址** | W5 | 报告 N-11 |
|
||||
| N-27 | **内存优化簇**:LVGL 池 + UI 两份缓冲 54,146 B 仍在 SRAM1,CCM 只剩 16,304 B;`ram_baseline.tsv` / `RAM预算台账.md` / 92% 阈值门禁**均未建** | R-2 后半段(迁 CCM)被堵死 → B2/B4/B5 的功能预算看不清 | **前置 §5.5 台账三件套** → R-9(`LV_MEM_SIZE` 40→16 KB,池水位门禁数据已具备)→ R-2 后半;R-3/R-4/R-5/R-6 逐项评估(R-6 前置已满足) | **W4** | 报告 R-2 剩余/R-3~R-6/R-9/§5.5 |
|
||||
| N-28 | Flash 死代码评估簇:剩余 ≈10 KB(`box_shadow` 3,246 / `draw_sw_img` 2,960 / `bin_decoder` 1,972 / `image_decoder` 952 / `triangle` 930)+ 双精度 `dadd` 等 ≈1 KB;uvprojx 仍挂 201 个空 LVGL TU(F-1/F-2/F-3 已闭环,省的是编译时长) | 编译时长 + 文档噪音 | 逐项实测再关;⚠ `mask`/`mask_rect`/`grad` 是文字与矩形绘制必经路径,**不可盲关** | W5 | 报告 F-4/F-5/F-6 |
|
||||
| N-29 | **仓库膨胀**:`.git` 149 MB + 图片 36.3 MB | clone/备份慢,且持续恶化 | 降采样 / 拆 submodule / LFS 三选一 **+ 一次 `git filter-repo` 历史重写**。**已决策(2026-10-11):暂不处理、登记观察**——推送/协作前重评(届时未推送提交更少、重写代价最低) | **W8** | 报告 D-1 |
|
||||
| U 簇 | UI 标准化:无令牌层/组件库/页注册表(手写 `case` 仍在),**27 页**未落地;golden 未挂 pre-commit、字符集差集检查仍是约定 | 新增一页成本仍是 8 处手工改动 | 按 `docs/问题记录/项目审查与优化建议报告.md` §6.5:U4 残余(两脚本入门禁,可逢窗口顺带)→ U1 令牌层 → U2/U3 → 页面落地;U4 golden 可直接当 U1 验收工具。**已确认(2026-10-11):本轮不排期**,等功能主线出结果后按产品需求启动 | W7 | 报告 U1~U5 |
|
||||
| N-20 | `Error_Handler` 仍 `__disable_irq(); while(1){}`(`Src/main.c:274-282`) | 一处初始化失败 = 整机挂死(只剩 IWDG 兜底),无自检日志 | 统一降级策略:记自检 + 禁用该子系统 + 其余照常 | W4 | 报告 N-03c |
|
||||
| N-21 | **dhara 两处**:`dhara_nand_is_free()` 只查页首 64 B;`dhara_nand_copy()` 不处理 `DHARA_NO_PAGE` 哨兵 | 映射恢复误判 → **数据丢失风险**(低概率、后果重);哨兵页号被当真实页 → 未定义行为;至今只能靠人工读代码发现 | 整页(或首/中/尾)扫描 + 返回 `DHARA_E_UNSUPPORTED`;**必须配主机侧移植层单测**(一并补报告"覆盖率"缺口) | **W2** | 报告 N-12/N-13 + §7 |
|
||||
| N-22 | 写回缓存无空闲/掉电强制 flush(整页写与换页已 flush) | 写回窗口内掉电丢最近日志/配置写 | defaultTask 空闲 N ms flush | W4 | 报告 N-14 |
|
||||
| N-23 残余 | **钩子已落地**(2026-10-12 `registration-check` 挂 pre-commit,存量缺口走 `KNOWN_GAPS` 白名单只 WARN);**残余** = 白名单清理:`rs485_task.c`(随 B2 暂缓一并处置:删除或注册)、`lftpd_log.c`;`app_cfg.h` 未接线开关(C5) | 白名单不清 = "写了不编译"盲区仍在 | 逐项清白名单(`--strict` 供审计复跑);开关随 C5 处置 | W4(C5) | 报告 N-06 残余 + 报告 S-3 |
|
||||
| N-24 | 日志每条 `f_open+f_write+f_close`(`sys_log_mcu.c:123-140`) | 每条日志 2–4 次 NAND 页读写;UI 翻记录逐条 open/seek/close | 与 C3 日志管线合并评估(现仅 `FIL` static 化省 528 B 栈) | W4 | 报告 N-16(**≠ 本登记簿 N-16**) |
|
||||
| N-25 | 性能/资源建议簇:PASV 每次新建监听;`NET_RECV_TIMEOUT_MS 30000`;PHY 强制 100M 无自协商回退;`NET_MAX_SOCKETS 8` 与单连接语义不符;`HAL_Delay(1)` 阻塞 DE(**DE 项随 B2 暂缓,§2.4**) | 次级性能/语义一致性 | 逐项见 `docs/问题记录/项目审查与优化建议报告.md` §4.2/§4.5/§4.7,随窗口评估 | W4 | 报告 N-09/P2-8/PHY/P3-x/rs485 |
|
||||
| N-26 | NAND 直 DMA 调用方缓冲(已加运行时守卫,阈值 1024) | CCM 缓冲被 DMA = 静默传错(与 ADC DMA 不可达 CCM 同类坑) | 补编译期/启动期地址断言 + **新铁律:堆缓冲不可被 DMA 直接寻址** | W4 | 报告 N-11 |
|
||||
| N-27 | **内存优化簇**:LVGL 池 + UI 两份缓冲 54,146 B 仍在 SRAM1,CCM 只剩 ≈8,160 B(2026-10-12 B5 堆扩容 48→56 KB 后) | R-2 后半段(迁 CCM)被堵死 → 后续功能预算看不清 | **三件套 ✅ 2026-10-12 落地**(`ram_baseline.py` 四模式 + `ram_baseline.tsv` 趋势 + `docs/内存管理/RAM预算台账.md` + `mem-budget` 钩子 92% 门禁)→ **W3** 再做 R-9(`LV_MEM_SIZE` 40→16 KB,池水位门禁数据已具备)→ R-2 后半;R-3/R-4/R-5/R-6 逐项评估(R-6 前置已满足) | **W3** | 报告 R-2 剩余/R-3~R-6/R-9/§5.5 |
|
||||
| N-28 | Flash 死代码评估簇:剩余 ≈10 KB(`box_shadow` 3,246 / `draw_sw_img` 2,960 / `bin_decoder` 1,972 / `image_decoder` 952 / `triangle` 930)+ 双精度 `dadd` 等 ≈1 KB;uvprojx 仍挂 201 个空 LVGL TU(F-1/F-2/F-3 已闭环,省的是编译时长) | 编译时长 + 文档噪音 | 逐项实测再关;⚠ `mask`/`mask_rect`/`grad` 是文字与矩形绘制必经路径,**不可盲关** | W4 | 报告 F-4/F-5/F-6 |
|
||||
| N-29 | **仓库膨胀**:`.git` 149 MB + 图片 36.3 MB | clone/备份慢,且持续恶化 | 降采样 / 拆 submodule / LFS 三选一 **+ 一次 `git filter-repo` 历史重写**。**已决策(2026-10-11):暂不处理、登记观察**——推送/协作前重评(届时未推送提交更少、重写代价最低) | **W7** | 报告 D-1 |
|
||||
| U 簇 | UI 标准化:无令牌层/组件库/页注册表(手写 `case` 仍在),**27 页**未落地;golden 未挂 pre-commit、字符集差集检查仍是约定 | 新增一页成本仍是 8 处手工改动 | 按 `docs/问题记录/项目审查与优化建议报告.md` §6.5:U4 残余(两脚本入门禁,可逢窗口顺带)→ U1 令牌层 → U2/U3 → 页面落地;U4 golden 可直接当 U1 验收工具。**已确认(2026-10-11):本轮不排期**,等功能主线出结果后按产品需求启动 | W6 | 报告 U1~U5 |
|
||||
|
||||
### 3.3 测试与架构改进专项(S1x / A-x)
|
||||
|
||||
@@ -199,7 +224,7 @@ W8 仓库 N-29 .git 149MB(用户决策) 硬件:N-18 V0.2
|
||||
|---|---|---|---|
|
||||
| **S12** | 模块级轻量断言宏 | ⬜ 待做(第 4 波按需) | 3.3.1 |
|
||||
| **S1-c** | `sys_log` / `time_sync` 主机侧单测 | ⬜ 可选(价值递减) | 3.3.2 |
|
||||
| **A-4** | ADC / RS485 采集链路的**测试可验证性约束** | ⏸ ADC 侧随 B1 闭环;RS485 待 B2 | 3.3.3 |
|
||||
| **A-4** | ADC / RS485 采集链路的**测试可验证性约束** | ⏸ ADC 侧随 B1 闭环;**RS485 侧随 B2 暂缓(§2.4)** | 3.3.3 |
|
||||
| **A-5 残余** | CI 体积门禁盲区(`mem-numbers` 在 CI 被跳过) | ⬜ 未消除 | 3.3.4 |
|
||||
|
||||
#### 3.3.1 S12:模块级轻量断言宏 ⬜
|
||||
@@ -242,9 +267,9 @@ W8 仓库 N-29 .git 149MB(用户决策) 硬件:N-18 V0.2
|
||||
#### 3.3.3 A-4:ADC / RS485 采集链路的测试可验证性约束 ⏸
|
||||
|
||||
**背景**:`adc_task.c` 已实现(B1,2026-10-10),`rs485_task.c` 仍是空桩,`App/protocol/` 只有 README 与 `.gitkeep`。
|
||||
本项不是"缺陷",而是**对上游 B2(B1 已完成)的设计约束**——应在动手前明确,否则后续无法补测。
|
||||
本项不是"缺陷",而是**对上游 B2 的设计约束**(B1 已完成;**B2 暂缓中,§2.4——重启 B2 动手前必须先落实本约束**,否则后续无法补测)。
|
||||
|
||||
**约束**(建议纳入 B1 / B2 设计):
|
||||
**约束**(建议纳入 B2 设计):
|
||||
|
||||
1. **协议解析必须与硬件 I/O 分离**——`App/protocol/` 的解析函数应接受
|
||||
`(const uint8_t *buf, size_t len)` 并返回解析结果,使其成为 L0.5 的**天然适格对象**
|
||||
@@ -267,16 +292,16 @@ S14 已在 workflow 显式提示"体积门禁未执行"(`::notice::`)并在
|
||||
|
||||
**完成判据**:存在**强制的**体积校验路径(CI 侧或本地侧),且文档明确其边界。
|
||||
|
||||
> **注**:本项与栈预算是同一话题的两面——**具体栈风险(原 S-1)已于 2026-10-10 削链闭环销项**
|
||||
> (defaultTask 静态最坏链 5312 → 2776 B、实测 3392 → 2152 B,**未抬栈**),本项仍是**门禁缺口**。
|
||||
> **注**:本项与栈预算是同一话题的两面——**具体栈风险(原 S-1)已于 2026-10-10 削链闭环**
|
||||
> (核算见 `docs/内存管理/RAM使用分析与优化方案.md`),本项仍是**门禁缺口**。
|
||||
|
||||
---
|
||||
|
||||
## 4. 待确认(需手册/硬件/产品定义,暂不直接改码)
|
||||
|
||||
1. TPAFE5160 是否有自动扫描模式(**已不影响**:1 kHz 硬件定时器触发已于 2026-10-10 由 **TIM3 更新中断**方案落地,不依赖自动扫描,见 `docs/子系统设计/ADC采样节拍设计说明.md`)。
|
||||
2. `APP_ADC_ENABLE` / `APP_RS485_ENABLE` 是否应在产品构建真实门控模块(当前宏已如实标注为"未接线占位",此处确认的是**是否接入真实门控**;B1 取数不依赖该宏)。
|
||||
3. RS-485 PD0 DE 的半双工方向语义需硬件实测确认(本次仅静态审查)。
|
||||
2. `APP_ADC_ENABLE` / `APP_RS485_ENABLE` 是否应在产品构建真实门控模块(当前宏已如实标注为"未接线占位",此处确认的是**是否接入真实门控**;B1 取数不依赖该宏。**RS-485 侧门控随 B2 暂缓,§2.4**)。
|
||||
3. RS-485 PD0 DE 的半双工方向语义需硬件实测确认(本次仅静态审查)——**随 B2 暂缓(§2.4)**。
|
||||
|
||||
---
|
||||
|
||||
@@ -336,9 +361,8 @@ S14 已在 workflow 显式提示"体积门禁未执行"(`::notice::`)并在
|
||||
|
||||
> **2026-10-10 重采(栈)/ 2026-10-04 重采(堆)**:栈 = **运行时高水位 + armlink 静态最坏链双口径**
|
||||
> (产品构建 `suite=NONE`,开机 + 空闲采样,串口 `[STACK]` + `stack_report.py`);静态区 = `.map` 受管快照。
|
||||
> ✅ **栈水位已回落**:2026-10-04 曾升到 defaultTask **3664 B**(登记 S-1);2026-10-10 削链
|
||||
> (删旧档迁移 + `FIL` 静态化 + 日志行缓冲按模块收紧 + 钩子脱离 HAL)后实测 **2152 B**、
|
||||
> 静态最坏链 **2776 B**,**未抬栈**即达标。完整核算见 `docs/内存管理/RAM使用分析与优化方案.md`。
|
||||
> 栈水位口径:2026-10-10 削链后 defaultTask 实测 **2152 B**、静态最坏链 **2776 B**,**未抬栈**即达标
|
||||
> (完整核算见 `docs/内存管理/RAM使用分析与优化方案.md`)。
|
||||
|
||||
| 项 | 当前值(栈:2026-10-10;其余:2026-10-04) | 出处 |
|
||||
|---|---|---|
|
||||
@@ -374,11 +398,12 @@ S14 已在 workflow 显式提示"体积门禁未执行"(`::notice::`)并在
|
||||
- **任一项完成后直接删除对应条目**(先销本文件条目,再清理其它文档对本编号的引用);
|
||||
**不留变更记录**,历史见 `git log` 与各模块文档。
|
||||
- 已闭环条目的**结论与证据沉淀进模块文档**,本文件不再重复。
|
||||
- 编号一律**沿用且不回收**(P-x / N-x / S-x / T-x / S1x / A-x);`错误码传播审计.md` 的 E-xx 独立维护。
|
||||
- 编号一律**沿用且不回收**(P-x / N-x / S-x / T-x / S1x / A-x / B·C·D 前缀);`错误码传播审计.md` 的 E-xx 独立维护。
|
||||
- **引用审查报告编号一律带文档名**(报告 N-16 ≠ 登记簿 N-16、报告 S-3 ≠ 登记簿 S-3);
|
||||
报告并轨项用本登记簿新编号(N-19~N-29 + U 簇),出处列保留报告编号。
|
||||
- **§2 是全项目唯一的推进顺序权威**:`产品需求设计文档.md` §9.2 等只留指针,顺序变更只改 §2。
|
||||
- 阶段 B 完成后重写 §1(届时 ADC/RS485 应从未闭环消失)。
|
||||
- 新增发现按既有惯例分流:能立即修的进 §2(阶段 C5);需排期的进 §3 对应编号空间;跨版本观察的进 §3.2(登记簿)。
|
||||
- **暂缓块(§2.4)不是销项**:条目仍在簿,只是不占波次;重启或关闭时更新 §2.4 与 §1。
|
||||
- 功能主线(B4/B5)完成后重写 §1。
|
||||
- 新增发现按既有惯例分流:能立即修的进 §2(W4 C5);需排期的进 §3 对应编号空间;跨版本观察的进 §3.2(登记簿)。
|
||||
- 上游引用关系:`问题记录/项目审查与优化建议报告.md`(体系层评审)、`问题记录/错误码传播审计.md`(E-xx)、
|
||||
`测试说明/实机测试实施指南.md`(执行流程)——三者均不在本文件合并范围。
|
||||
|
||||
@@ -329,7 +329,7 @@ static void phaseN_run(void) {
|
||||
| `App/log/sys_log_mcu.c` | — | ⬜ 候选 | 记录编解码(定长 64B) | 编解码纯逻辑;写入端依赖 FatFS + 队列,测编解码即可 |
|
||||
| `App/util/time_sync.c` | — | ⬜ 候选 | 协议帧编解码 | 报文组装/解析纯逻辑;网络收发不测 |
|
||||
| `Drivers/BSP/NETCFG/net_cfg_legacy.c` | — | ⛔ 已删除(2026-10-10) | — | 旧档 NCFG 迁移整体移除(旧档 `/cfg/netcfg.bin` 不读、不删、不迁移),测试候选随之撤销 |
|
||||
| `App/net/*`(网络栈上层) | — | ⬜ 待评估 | 依赖 socket/CH395F | 纯协议状态机部分或可测,需先确认抽象质量 |
|
||||
| `App/protocol/iec104/*`(104 协议栈) | — | ✅ 已覆盖(82 断言,2026-10-12 B4) | 数据源 vtable + tick 注入 + 发送回调注入 | APCI 重组/总召分片/COV/k-w-t1~t3/两段式全离线穷举;`iec104-unit-test` 钩子 |
|
||||
| `App/util/rs485.c` | — | ⏸ 待功能实现 | — | 上游 B2;当前为空桩 |
|
||||
| `Drivers/BSP/TPAFE5160/*.c` | — | ❌ 不适格 | — | 寄存器序列为主,需真机时序 |
|
||||
| 其余驱动(CH395F/GD5F2GQ5/SD2506) | — | ❌ 不适格 | — | 需硬件时序 / SPI / I2C 总线 |
|
||||
|
||||
+91
-72
@@ -25,16 +25,18 @@ Python 环境、以及 `docs/待处理内容.md` 这类**全局唯一登记簿**
|
||||
|
||||
## 2. 方案选型
|
||||
|
||||
| 方案 | 为什么不用 |
|
||||
|---|---|
|
||||
| 同一目录切分支 | 构建产物 / `.venv` / 日志目录 / `.workbuddy-ai` 全共享,两任务互相踩;且本仓库明令禁止用 `git checkout` 丢弃未提交工作 |
|
||||
| 每任务 `git clone` | 对象库 234 MB × N 份、Git LFS 全量重复;克隆之间无法安全共享分支,推送关系也更绕 |
|
||||
| `git stash` / 打补丁倒换 | 不具备并发安全性,冲突只能靠人工记忆 |
|
||||
| **`git worktree`(采用)** | 唯一同时满足「共享对象库 + 独立工作目录 + 独立 HEAD/index」的机制;git 原生,本仓库已有先例 |
|
||||
| 方案 | 为什么不用 |
|
||||
| ---------------------- | ----------------------------------------------------------------------------------- |
|
||||
| 同一目录切分支 | 构建产物 / `.venv` / 日志目录 / `.workbuddy-ai` 全共享,两任务互相踩;且本仓库明令禁止用 `git checkout` 丢弃未提交工作 |
|
||||
| 每任务 `git clone` | 对象库 234 MB × N 份、Git LFS 全量重复;克隆之间无法安全共享分支,推送关系也更绕 |
|
||||
| `git stash` / 打补丁倒换 | 不具备并发安全性,冲突只能靠人工记忆 |
|
||||
| **`git worktree`(采用)** | 唯一同时满足「共享对象库 + 独立工作目录 + 独立 HEAD/index」的机制;git 原生,本仓库已有先例 |
|
||||
|
||||
**既有先例**:`D:/Code/DTU Code/_v1pin-build`、`_v2oui-build` 就是遗留 worktree(detached HEAD,
|
||||
2026-09-19 建、已过期)。本方案沿用其「仓库同级 + `_` 前缀」的命名惯例,但把散落的目录收敛到统一的
|
||||
`_wt/` 容器下,并在落地时清理这两个遗留目录。
|
||||
**命名惯例来源**:`D:/Code/DTU Code/_v1pin-build`、`_v2oui-build` 曾是遗留 worktree(detached HEAD,
|
||||
2026-09-19 建、已过期),本方案沿用其「仓库同级 + `_` 前缀」命名,但把散落的目录收敛到统一的
|
||||
`_wt/` 容器下。**两者已于 2026-10-12 清理**:`_v1pin-build` 干净直接移除;`_v2oui-build` 有 2 个
|
||||
未提交改动(uvprojx / freertos.c),已按字节留档于 `_wt/.archive/2026-10-12-legacy-worktrees/`
|
||||
后才移除(两提交由 `wip/trap22-fix` / `wip/v2-pending-recv` 持有,无提交丢失)。
|
||||
|
||||
---
|
||||
|
||||
@@ -65,15 +67,15 @@ D:/Code/DTU Code/
|
||||
|
||||
### 3.2 共享 vs 独占
|
||||
|
||||
| 资源 | 归属 | 原因 |
|
||||
|---|---|---|
|
||||
| `.git/` 对象库 + hooks | **共享**(git 机制自动) | 工作树共享 common dir,提交门禁无需在新空间重装 |
|
||||
| `.venv/` | **共享**(junction) | 体积大、内容与任务无关;每任务重建一份纯属浪费 |
|
||||
| `simulator/SDL2/` | **共享**(junction) | 下载型 SDK,与任务无关 |
|
||||
| `.workbuddy-ai/` | **独占**(刻意不共享) | ① 避免并发写同一份日志丢更新;② 不给联接递归删除的机会——它是本项目最不可再生的目录。任务侧记忆由清场前归并回主工作树 |
|
||||
| `MDK-ARM/` 构建产物、`simulator/build/` | **独占** | 正是需要隔离的部分:两个任务可各自构建互不干扰 |
|
||||
| `.serialtest/config.json`、`.dsh/skills/`、`.pre-commit-config.yaml` | **独占但内容一致** | 受版本控制,随检出白拿,无需 bootstrap |
|
||||
| `.mnemon/`、`ci-artifacts/` | **独占** | 本地运行态,gitignored |
|
||||
| 资源 | 归属 | 原因 |
|
||||
| ------------------------------------------------------------------ | ---------------- | ------------------------------------------------------------- |
|
||||
| `.git/` 对象库 + hooks | **共享**(git 机制自动) | 工作树共享 common dir,提交门禁无需在新空间重装 |
|
||||
| `.venv/` | **共享**(junction) | 体积大、内容与任务无关;每任务重建一份纯属浪费 |
|
||||
| `simulator/SDL2/` | **共享**(junction) | 下载型 SDK,与任务无关 |
|
||||
| `.workbuddy-ai/` | **独占**(刻意不共享) | ① 避免并发写同一份日志丢更新;② 不给联接递归删除的机会——它是本项目最不可再生的目录。任务侧记忆由清场前归并回主工作树 |
|
||||
| `MDK-ARM/` 构建产物、`simulator/build/` | **独占** | 正是需要隔离的部分:两个任务可各自构建互不干扰 |
|
||||
| `.serialtest/config.json`、`.dsh/skills/`、`.pre-commit-config.yaml` | **独占但内容一致** | 受版本控制,随检出白拿,无需 bootstrap |
|
||||
| `.mnemon/`、`ci-artifacts/` | **独占** | 本地运行态,gitignored |
|
||||
|
||||
### 3.3 junction 规则(**不能用 symlink**)
|
||||
|
||||
@@ -98,13 +100,13 @@ D:/Code/DTU Code/
|
||||
|
||||
## 4. 分支模型
|
||||
|
||||
| 项 | 取值 |
|
||||
|---|---|
|
||||
| 集成分支(主干) | `v2-on-ui` |
|
||||
| 任务分支 | `task/<ID>-<slug>`,从主干当前 HEAD 切出 |
|
||||
| `<ID>` | **沿用 `docs/待处理内容.md` 的编号空间**(`P0-x`/`N-x`/`S-x`/`T-x`/`B-x`/`C-x`/`D-x`);无对应条目的临时任务用 `X-<日期>-<slug>` |
|
||||
| 合并方式 | `git merge --no-ff` 合入主干 |
|
||||
| 并发上限 | **同时最多 3 棵工作树** |
|
||||
| 项 | 取值 |
|
||||
| -------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| 集成分支(主干) | `v2-on-ui` |
|
||||
| 任务分支 | `task/<ID>-<slug>`,从主干当前 HEAD 切出 |
|
||||
| `<ID>` | **沿用 `docs/待处理内容.md` 的编号空间**(`P0-x`/`N-x`/`S-x`/`T-x`/`B-x`/`C-x`/`D-x`);无对应条目的临时任务用 `X-<日期>-<slug>` |
|
||||
| 合并方式 | `git merge --no-ff` 合入主干 |
|
||||
| 并发上限 | **同时最多 3 棵工作树** |
|
||||
|
||||
**`<ID>` 复用登记簿编号**是为了可追溯:任务分支名直接对应待办条目,回收时销项有据可查。
|
||||
|
||||
@@ -115,17 +117,17 @@ D:/Code/DTU Code/
|
||||
|
||||
## 5. 任务生命周期
|
||||
|
||||
| 阶段 | 动作 | 并行性 |
|
||||
|---|---|---|
|
||||
| `new` | 建分支 + 工作树 + junction + 台账 | 可并行 |
|
||||
| 编码 | 在新空间里改代码 | 可并行 |
|
||||
| 门禁 | `scripts/ci_local.py`(L0 + L0.5,纯软件) | **可并行** |
|
||||
| 构建 | `@build`(UV4 增量,需持 `build` 锁) | **必须串行** |
|
||||
| 台架 | `@test` / 板级套件(需先持台架锁,见 §7) | **必须串行** |
|
||||
| 提交 | 本地 `git commit`(多行正文用 `-F <UTF-8 无 BOM 文件>`) | 可并行 |
|
||||
| `sync` | `rebase` 追平主干,把冲突控制在任务分支上 | 可并行 |
|
||||
| `done` | 预演 → 合入主干 → 残留审计 → 重生成产物 → 门禁 → 清理工作树 | **必须串行** |
|
||||
| `push` | 推送主干/任务分支 | **需用户明确许可** |
|
||||
| 阶段 | 动作 | 并行性 |
|
||||
| ------ | -------------------------------------------- | ----------- |
|
||||
| `new` | 建分支 + 工作树 + junction + 台账 | 可并行 |
|
||||
| 编码 | 在新空间里改代码 | 可并行 |
|
||||
| 门禁 | `scripts/ci_local.py`(L0 + L0.5,纯软件) | **可并行** |
|
||||
| 构建 | `@build`(UV4 增量,需持 `build` 锁) | **必须串行** |
|
||||
| 台架 | `@test` / 板级套件(需先持台架锁,见 §7) | **必须串行** |
|
||||
| 提交 | 本地 `git commit`(多行正文用 `-F <UTF-8 无 BOM 文件>`) | 可并行 |
|
||||
| `sync` | `rebase` 追平主干,把冲突控制在任务分支上 | 可并行 |
|
||||
| `done` | 预演 → 合入主干 → 残留审计 → 重生成产物 → 门禁 → 清理工作树 | **必须串行** |
|
||||
| `push` | 推送主干/任务分支 | **需用户明确许可** |
|
||||
|
||||
**为什么 `done` 必须串行**:合并动作会移动主干引用,两处同时合入会互相覆盖;且主干只在主工作树检出。
|
||||
|
||||
@@ -138,14 +140,14 @@ D:/Code/DTU Code/
|
||||
并行失败通常不是代码冲突,而是**多个任务同时改同一个「登记簿」**。以下文件在任务期间
|
||||
**禁止在任务工作树里修改**,一律由 `done` 回收阶段在主工作树统一处理:
|
||||
|
||||
| 文件 | 冲突原因 | 回收时的处理 |
|
||||
|---|---|---|
|
||||
| `docs/待处理内容.md` | 全项目唯一登记簿,每个任务都要销项 | 由主工作树统一改,任务分支不碰 |
|
||||
| `AGENTS.md`、`docs/README.md`(索引/登记类) | 每个任务都要在「按主题查哪份文档」表、`docs/README.md` §9.2 环境表、变更记录里**各加一行** → 两任务必冲突(2026-10-12 实测:主工作树里本任务的行与他任务的行已并排混在**同一 hunk**) | 由主工作树统一追加;提交时**只 `git add` 本任务的行**(`git apply --cached` 选择性入库),勿整文件 `git add` |
|
||||
| `AGENTS.md` 的 `TASK_TABLE` 块 | 生成物,两个任务都抬栈就冲突 | 合并后重跑 `scripts/gen_task_table.py` **重新生成**,不手工并 |
|
||||
| `MDK-ARM/STM32F407-Demo.uvprojx` | 新源文件注册 + UV4 自动改写 | 手工并 `<File>` 条目 → 重跑 `registration-check` → `@build` |
|
||||
| `.pre-commit-config.yaml` | 新增主机单测要加钩子 | 并集处理 |
|
||||
| `docs/内存管理/RAM使用分析与优化方案.md`(及其 `mem-numbers` 快照) | `mem-numbers` 快照 | **绝不手工合数字**,合并后 `scripts/check_mem_numbers.py --write` 重生成 |
|
||||
| 文件 | 冲突原因 | 回收时的处理 |
|
||||
| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| `docs/待处理内容.md` | 全项目唯一登记簿,每个任务都要销项 | 由主工作树统一改,任务分支不碰 |
|
||||
| `AGENTS.md`、`docs/README.md`(索引/登记类) | 每个任务都要在「按主题查哪份文档」表、`docs/README.md` §9.2 环境表、变更记录里**各加一行** → 两任务必冲突(2026-10-12 实测:主工作树里本任务的行与他任务的行已并排混在**同一 hunk**) | 由主工作树统一追加;提交时**只 `git add` 本任务的行**(`git apply --cached` 选择性入库),勿整文件 `git add` |
|
||||
| `AGENTS.md` 的 `TASK_TABLE` 块 | 生成物,两个任务都抬栈就冲突 | 合并后重跑 `scripts/gen_task_table.py` **重新生成**,不手工并 |
|
||||
| `MDK-ARM/STM32F407-Demo.uvprojx` | 新源文件注册 + UV4 自动改写 | 手工并 `<File>` 条目 → 重跑 `registration-check` → `@build` |
|
||||
| `.pre-commit-config.yaml` | 新增主机单测要加钩子 | 并集处理 |
|
||||
| `docs/内存管理/RAM使用分析与优化方案.md`(及其 `mem-numbers` 快照) | `mem-numbers` 快照 | **绝不手工合数字**,合并后 `scripts/check_mem_numbers.py --write` 重生成 |
|
||||
|
||||
### 6.2 合并残留审计(**必做**)
|
||||
|
||||
@@ -160,6 +162,7 @@ D:/Code/DTU Code/
|
||||
### 6.3 冲突时的解决位置
|
||||
|
||||
`done` 先做**非破坏性预演**:`git merge-tree --write-tree v2-on-ui task/<ID>-<slug>`。
|
||||
|
||||
- `rc=0`(无冲突)→ 继续合并
|
||||
- `rc≠0` → **在任务分支上** `git merge v2-on-ui` 解决冲突并提交,再重跑 `done`
|
||||
|
||||
@@ -205,15 +208,15 @@ D:/Code/DTU Code/
|
||||
**执行体**:`.dsh/skills/task-worktree/scripts/task_worktree.py`(纯标准库,
|
||||
照 `keil_build.py` 的写法,整目录可拷到别的项目复用)。**已落地并端到端实测**(见 §11)。
|
||||
|
||||
| 子命令 | 作用 |
|
||||
|---|---|
|
||||
| 子命令 | 作用 |
|
||||
| ----------------------------------------------------- | ----------------------------------------------------------- |
|
||||
| `new <ID> <slug> [--base B] [--lean] [--needs-bench]` | 建分支 `task/<ID>-<slug>` + `_wt/<ID>-<slug>/` 工作树 + 共享联接 + 台账 |
|
||||
| `list` | 所有任务一览:分支 / 落后领先 / 脏文件数 / 台架状态 + 两个锁的归属 |
|
||||
| `status <ID>` | 单任务详情:未提交清单、相对基线漂移、台架记录、已建联接 |
|
||||
| `sync <ID>` | `rebase` 追平主干,把冲突控制在任务分支上 |
|
||||
| `lock acquire\|release\|status <build\|bench>` | 独占资源锁(`--task` / `--wait` / `--force` / `--bench-pass`) |
|
||||
| `done <ID> [--dry-run] [--skip-build] [--skip-gates]` | 预演 → 合入主干 → 残留审计 → 重生成产物 → 门禁 → 清场 |
|
||||
| `abort <ID> [--force]` | 放弃任务(有未提交/未合并内容时不加 `--force` 会拒绝) |
|
||||
| `list` | 所有任务一览:分支 / 落后领先 / 脏文件数 / 台架状态 + 两个锁的归属 |
|
||||
| `status <ID>` | 单任务详情:未提交清单、相对基线漂移、台架记录、已建联接 |
|
||||
| `sync <ID>` | `rebase` 追平主干,把冲突控制在任务分支上 |
|
||||
| `lock acquire\|release\|status <build\|bench>` | 独占资源锁(`--task` / `--wait` / `--force` / `--bench-pass`) |
|
||||
| `done <ID> [--dry-run] [--skip-build] [--skip-gates]` | 预演 → 合入主干 → 残留审计 → 重生成产物 → 门禁 → 清场 |
|
||||
| `abort <ID> [--force]` | 放弃任务(有未提交/未合并内容时不加 `--force` 会拒绝) |
|
||||
|
||||
**环境变量**:`DTU_WT_ROOT`(任务容器位置)· `DTU_TASK_BASE`(默认基线)·
|
||||
`DTU_INTEGRATION_WORKTREE`(集成点,默认主工作树;用于隔离演练/测试)。
|
||||
@@ -231,35 +234,37 @@ D:/Code/DTU Code/
|
||||
|
||||
## 9. 门禁与验收
|
||||
|
||||
| 层级 | 内容 | 时机 |
|
||||
|---|---|---|
|
||||
| 任务期 | `scripts/ci_local.py`(L0 + L0.5,可并行) | 每个任务各自跑 |
|
||||
| 任务期 | `@build`(0 错 0 警,**持 `build` 锁串行**) | 每个任务各自跑 |
|
||||
| 任务期(可选) | `@test` / 板级套件(持 `bench` 锁) | 需真机验证的任务 |
|
||||
| 回收期 | 重生成产物 → `ci_local.py` → `@build` | `done` 内强制(`--skip-gates` 可跳过) |
|
||||
| 人工 | push 许可 | 回收后 |
|
||||
| 层级 | 内容 | 时机 |
|
||||
| ------- | ------------------------------------ | ------------------------------ |
|
||||
| 任务期 | `scripts/ci_local.py`(L0 + L0.5,可并行) | 每个任务各自跑 |
|
||||
| 任务期 | `@build`(0 错 0 警,**持 `build` 锁串行**) | 每个任务各自跑 |
|
||||
| 任务期(可选) | `@test` / 板级套件(持 `bench` 锁) | 需真机验证的任务 |
|
||||
| 回收期 | 重生成产物 → `ci_local.py` → `@build` | `done` 内强制(`--skip-gates` 可跳过) |
|
||||
| 人工 | push 许可 | 回收后 |
|
||||
|
||||
**`done` 的完成判据**:主干上 `git merge --no-ff` 成功 + `ci_local.py` 全绿 + `@build` 0 错 0 警
|
||||
+ 残留审计无死规则/重复条目 + 工作树已移除。
|
||||
|
||||
- 残留审计无死规则/重复条目 + 工作树已移除。
|
||||
|
||||
---
|
||||
|
||||
## 10. 已知风险与待验证项
|
||||
|
||||
| 项 | 状态 | 处置 |
|
||||
|---|---|---|
|
||||
| UV4 能否并发构建 | **已确认不可**(2026-10-12) | `build` 与 `bench` 一并纳入串行锁 |
|
||||
| 跳过 Git LFS smudge 是否影响门禁 | **已实测**(2026-10-12) | 见下表:只省磁盘、不省时间,两种方式工作区都干净 → 默认全量,`--lean` 备选 |
|
||||
| 主工作树当前不干净 | 已知 | `done` 的前置检查会拦下 |
|
||||
| 遗留 worktree(`_v1pin-build` / `_v2oui-build`) | 已知 | 落地时 `git worktree remove` 清理,避免新旧两套并存 |
|
||||
| **「主工作树绿、干净工作树红」的隐藏依赖** | **已实证**(2026-10-12) | 见 10.2。属**原任务**漏提交,非本 skill 缺陷 |
|
||||
| 项 | 状态 | 处置 |
|
||||
| -------------------------------------------- | --------------------- | ------------------------------------------- |
|
||||
| UV4 能否并发构建 | **已确认不可**(2026-10-12) | `build` 与 `bench` 一并纳入串行锁 |
|
||||
| 跳过 Git LFS smudge 是否影响门禁 | **已实测**(2026-10-12) | 见下表:只省磁盘、不省时间,两种方式工作区都干净 → 默认全量,`--lean` 备选 |
|
||||
| 主工作树当前不干净 | 已知 | `done` 的前置检查会拦下 |
|
||||
| 遗留 worktree(`_v1pin-build` / `_v2oui-build`) | 已知 | 落地时 `git worktree remove` 清理,避免新旧两套并存 |
|
||||
| **「主工作树绿、干净工作树红」的隐藏依赖** | **已实证**(2026-10-12) | 见 10.2。属**原任务**漏提交,非本 skill 缺陷 |
|
||||
| `git worktree remove` 中途被中断 → 残留「裸目录」 | **已实证**(2026-10-12) | 见下方 10.3 |
|
||||
|
||||
### 10.1 LFS 实测数据(2026-10-12,2396 个受控文件 / 707 个 LFS `*.jpg`)
|
||||
|
||||
| 方式 | 建树耗时 | 检出体积 | `git status` | 参考图 |
|
||||
|---|---|---|---|---|
|
||||
| 全量 smudge(默认) | 7.25 s | 83 MB | 干净 | 真实 JPEG,可看 |
|
||||
| `--lean`(`GIT_LFS_SKIP_SMUDGE=1`) | 7.05 s | 45 MB | 干净 | 指针文本 |
|
||||
| 方式 | 建树耗时 | 检出体积 | `git status` | 参考图 |
|
||||
| --------------------------------- | ------ | ----- | ------------ | ---------- |
|
||||
| 全量 smudge(默认) | 7.25 s | 83 MB | 干净 | 真实 JPEG,可看 |
|
||||
| `--lean`(`GIT_LFS_SKIP_SMUDGE=1`) | 7.05 s | 45 MB | 干净 | 指针文本 |
|
||||
|
||||
**结论**:LFS 对象已在本地,smudge 几乎不耗时,跳过**只省约 38 MB 磁盘**。
|
||||
故默认走全量(保留参考图可读性),磁盘紧张时才用 `--lean`。
|
||||
@@ -286,6 +291,20 @@ docs\内存管理\RAM使用分析与优化方案.md:585 [路径引用] docs/内
|
||||
**副作用提醒**:这同时说明**共享主工作树里的绿门禁不可尽信**——只要有未提交文件躺在磁盘上,
|
||||
门禁就可能对着「未提交的现状」放行。多任务并行期更该用工作树 + `done` 复核。
|
||||
|
||||
### 10.3 `git worktree remove` 中断会留下「裸目录」(2026-10-12 实证)
|
||||
|
||||
清理遗留 worktree 时踩到:`git worktree remove --force` 被中断后,`_v2oui-build` 的 `.git` **已被删除**
|
||||
(于是 `git worktree list` 显示 `prunable`),但目录里**还躺着 2774 个条目**没删完。
|
||||
|
||||
**判据**:看到 `prunable` + 目录仍存在 = git 已删掉 gitfile、删除未完成。此时那棵树**已不再是 worktree**
|
||||
(git 不认它了),`git worktree remove` 无法再用,只能按普通目录删除。
|
||||
|
||||
|
||||
**处置**:`git worktree prune` 清台账 → 手工删目录。删之前必须**重新扫一遍 reparse**——
|
||||
理由它有 `.git` 时没有联接,不等于现在没有(尤其别让他人趁乱建了联接)。
|
||||
清除手段用 Python 自底向上删并顺带 `chmod +w`(MDK 产物有 2 个只读文件),
|
||||
**切忌 `rm -rf`**(MSYS 会递归进联接,实测见 3.3)。
|
||||
|
||||
**已知不做**:不引入第二套并行机制(如 git submodule / 多克隆),
|
||||
保持「同一能力只有一个实现」的收敛原则。
|
||||
|
||||
@@ -300,7 +319,7 @@ docs\内存管理\RAM使用分析与优化方案.md:585 [路径引用] docs/内
|
||||
| 新建 | `.dsh/skills/task-worktree/.gitignore` | ✅ |
|
||||
| 修改 | `AGENTS.md`「按主题查哪份文档」加一行指向本文档 | ✅ |
|
||||
| 修改 | `docs/README.md` §9.2 登记本文档 | ✅ |
|
||||
| 清理 | 移除遗留 worktree `_v1pin-build` / `_v2oui-build` | ⏳ 待用户确认(含未提交内容则先留档) |
|
||||
| 清理 | 移除遗留 worktree `_v1pin-build` / `_v2oui-build` | ✅ 2026-10-12 已清理(脏改动先留档,见 §2) |
|
||||
| 验证 | §10 的两个待验证项 | ✅ 见 §10 / §10.1 |
|
||||
|
||||
### 11.1 端到端实测记录(2026-10-12,隔离集成点演练)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
> **参考来源**:`D:\Code\DTU\参考资料\huaiyuan\huaiyuan\Scatter_P_HV03.01_SV01.01\项目分析文档\04-算法与采样专题\ADC采集至数据使用全流程详解`(同专题另有《项目详细分析-交流采样与有效值计算》《傅氏算法详解》《项目详细分析-阶段6-继电器保护单元分析》)。
|
||||
> **阅读前提**:先读 `docs/算法/傅氏算法详解.md`(算法原理)与 `docs/硬件平台/ADC通道映射说明.md`(采样通道权威口径)。
|
||||
> **相关文档**:`docs/产品需求/产品需求设计文档.md`、`docs/待处理内容.md`、`docs/内存管理/RAM使用分析与优化方案.md`。
|
||||
> **状态**:**P0 采样与相量底座已台架闭环(2026-10-11)**——L1 采集链路(B1,2026-10-10)、L1-6 采样有效性校验与 L2 相量层(`sample_ring` + `phasor`,57 项主机单测 + 台架 `@test` 含 `phasor(first)/phasor(hb)` 标记闭环)均已落地;**P1/P2 保护元件与定值模型软件侧完成(2026-10-11,`prot_set` + `prot_elm`,57 项主机单测)**,板端接线(protectTask)与出口记录(L4)为下一步(见 §7)。
|
||||
> **状态**:**P0 采样与相量底座已台架闭环(2026-10-11)**——L1 采集链路(B1,2026-10-10)、L1-6 采样有效性校验与 L2 相量层(`sample_ring` + `phasor`,57 项主机单测 + 台架 `@test` 含 `phasor(first)/phasor(hb)` 标记闭环)均已落地;**P1/P2 保护元件与定值模型软件侧完成(2026-10-11,`prot_set` + `prot_elm`,57 项主机单测)**;**B5 板端接线完成(2026-10-12)**——`protectTask`(20 ms 判据节拍 + L5-4 采样故障闭锁 + L5-1 定值校验和巡检,IWDG `TASK_WD_PROTECT` bit8,栈 512 words 静态链 872 B / 余 57%),判据结果经 B4 IEC104 遥信(YX15~23/31)上送;**出口记录(L4)与标准源注入验收为下一步**(见 §7)。
|
||||
|
||||
---
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
| **B0 产品定义** | 采集是否持续 / 采样率与通道 / 转发目标 / 保护元件清单——决定 B1 与保护层的**规模** | ✅ 已确认(2026-10-10,见 §1.5) |
|
||||
| **B1 ADC 采集链路** | 周期 CONVST + `adc_task` 消费 `tpafe5160_snapshot()` + 建任务 + 栈预算 | ✅ 已实现并 `@test` 实机闭环(2026-10-10;`adcTask` 512 words,台架 1000 Hz) |
|
||||
| **N-18 硬件缺陷** | V0.1 跨转换器无同一时刻性 → 限定 V0.1 可实现的保护范围 | ⚠️ 硬件改版项(V0.2) |
|
||||
| **栈预算** | 保护任务栈需入 `Src/freertos.c` 并经 `scripts/stack_report.py` 核算 | 🔶 `adcTask` 已核算(**L1-6 后:栈抬至 640 words,静态 1080 B / 台架实测峰值 952/2560 / 余 57%**);`protectTask` 为后续 |
|
||||
| **栈预算** | 保护任务栈需入 `Src/freertos.c` 并经 `scripts/stack_report.py` 核算 | ✅ 两任务均已核算:`adcTask`(**L1-6 后:栈抬至 640 words,静态 1080 B / 台架实测峰值 952/2560 / 余 57%**);`protectTask`(B5,2026-10-12:**512 words,静态链 872 B / 余 1176 B / 57%**,台架水位待 `@test`) |
|
||||
|
||||
### 1.5 B0 产品定义(2026-10-10 确认)
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
| 采样节拍 | 定时器中断(TIMx) | **1 ms** | 最高(ISR) | 产生 CONVST 脉冲;ISR 内只置事件/计数,不做重运算 |
|
||||
| 采集与预处理 | `adcTask` | 1 ms | 高 | `tpafe5160_snapshot()` 取数 → 标定 → 存入采样环 |
|
||||
| 相量计算 | `adcTask` 内联 或 `algoTask` | 1 ms | 高 | 全周傅氏 → 相量双区交换 |
|
||||
| 保护判据 | `protectTask` | 1 ms | 高 | 各保护元件 Init/Main → 出口 |
|
||||
| 保护判据 | `protectTask` | 1 ms | 高 | 各保护元件 Init/Main → 出口。**✅ B5 已落地(2026-10-12):V0.1 实为 20 ms / Normal**——元件吃 50 Hz 基波 rms(phasor 每工频周更新),延时精度由 `prot_elm` 的 now_ms 差值法保证、与节拍无关;1 ms/高是参考系统瞬时值元件口径,随 P2 瞬时值元件再评 |
|
||||
| 遥测与显示 | 复用 `uiTask` / 新增 `measTask` | 200 ms | 中 | 谐波方和根、死区/平滑、UI 与上送 |
|
||||
| 记录 | `defaultTask`(已有) | 事件驱动 | 低 | SOE/动作记录落 `sys_log` |
|
||||
|
||||
@@ -277,9 +277,9 @@
|
||||
|
||||
| # | 元件 | 判据要点 | 关键定值 | 状态 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| L3-1 | 三段式相间过流 | 相电流基波 > 定值,经延时出口;返回系数滞回 | Ⅰ/Ⅱ/Ⅲ 段电流 + 延时 | 🔶 2026-10-11 主机实现:`App/algo/prot_elm.c` 通用"滞回+定时限"核 + `prot_set.c` 定值(Ⅰ/Ⅱ/Ⅲ 段,A 相 `IA`),单测 27 用例含 1.01× 启动/100 ms 到点/滞回保持/返回释放;**板端接线与出口待 protectTask 立项** |
|
||||
| L3-1 | 三段式相间过流 | 相电流基波 > 定值,经延时出口;返回系数滞回 | Ⅰ/Ⅱ/Ⅲ 段电流 + 延时 | 🔶 2026-10-11 主机实现:`App/algo/prot_elm.c` 通用"滞回+定时限"核 + `prot_set.c` 定值(Ⅰ/Ⅱ/Ⅲ 段,A 相 `IA`),单测 27 用例含 1.01× 启动/100 ms 到点/滞回保持/返回释放;**板端接线已落地(B5,2026-10-12:protectTask 20 ms 节拍,动作经 104 YX16~18 上送)**;出口(L4)待做 |
|
||||
| L3-2 | 三段式零序过流 | 零序电流 > 定值,经延时出口 | Ⅰ/Ⅱ/Ⅲ 段零序电流 + 延时 | ⬜(N-18:`IN` 走 STM32,V0.2 使能;框架已就绪,加实例表一行即可) |
|
||||
| L3-3 | 过负荷 | 相电流 > 定值,告警/跳闸可整定 | 过负荷电流 + 延时 | 🔶 同 L3-1(`OL-IA` 实例,默认告警不跳闸) |
|
||||
| L3-3 | 过负荷 | 相电流 > 定值,告警/跳闸可整定 | 过负荷电流 + 延时 | 🔶 同 L3-1(`OL-IA` 实例,默认告警不跳闸;板端接线随 B5 落地,YX23 上送) |
|
||||
| L3-4 | 过压 / 欠压 | 相/线电压越限,经延时出口 | 过压定值、失压定值 + 延时 | 🔶 同 L3-1(`OV-UA`/`UV-UA`,欠量方向单测覆盖;沿用参考告警段 0x8027/0x8029) |
|
||||
| L3-5 | 过频 / 低频 | 频率越限,经延时出口 | 过频、低频定值 + 延时 | ⬜(随频率测量算法 P2;框架已就绪) |
|
||||
| L3-6 | 零序过压 | 零序电压 > 定值 | 零序过压定值 + 延时 | 🔶 同 L3-1(`3U0-UN` 实例,外部零序电压通道,不受 N-18 限制) |
|
||||
@@ -323,7 +323,7 @@
|
||||
| L5-1 | 定值校验和 | `tagPRS_XXX` 的 `dwTagSum` 自检,定值非法闭锁 | 🔶 2026-10-11:`prot_set.c` 已实现 sum32 正/反码封存 + selfcheck 失配置闭锁(参考 `dwTagSum/dwTagSumN` 模式);元件级 tagPRS 常量表校验随板端接线并入 |
|
||||
| L5-2 | 采样通道自检 | 通道码值越界/静默检出 | 🔶 静默/丢帧/序号倒退/错误增量已由 L1-6 `adc_health` 检出(2026-10-11);码值越界待做 |
|
||||
| L5-3 | 硬件自检 | 出口回路、电源、时钟 | ⬜ |
|
||||
| L5-4 | 自检失败闭锁 | 任一项失败 → 闭锁相关保护并告警 | 🔶 定值闭锁 → 全元件复位已联动(`prot_elm_eval` 读 `prot_set_locked()`,单测覆盖);采样故障闭锁(`adc_health_flags` 电平位 → 元件使锁)随板端接线 |
|
||||
| L5-4 | 自检失败闭锁 | 任一项失败 → 闭锁相关保护并告警 | 🔶 定值闭锁 → 全元件复位已联动(`prot_elm_eval` 读 `prot_set_locked()`,单测覆盖);**采样故障闭锁已落地(B5,2026-10-12)**:protectTask 每拍读 `adc_health_flags()` 电平位(TPAFE/STM32 stall + 序号倒退)→ 命中即 `prot_elm_init()` 复位 + 本拍不判据,自愈后自动恢复;锁存类事件位(FRSTDATA/HW_ERR)不构成"当前测量不可信"→ 不闭锁,如实登记 |
|
||||
|
||||
---
|
||||
|
||||
@@ -360,7 +360,7 @@
|
||||
| 相量区 RAM | `tVectorRly[2][N] × sizeof(相量)` | 双区 + **5 实例**(本项目;参考项目 4 实例) |
|
||||
| 采样缓冲 RAM | 40 点/通道 × 通道数 × 字宽 | 双缓冲 2 周波 |
|
||||
| 定值区 RAM | 元件数 × 定值项 × 字宽 | 可由 cfg_file 载入 |
|
||||
| 任务栈 | `adcTask` / `protectTask` 待核算 | 建任务前后各跑 `scripts/stack_report.py` |
|
||||
| 任务栈 | `adcTask` ✅ / `protectTask` ✅(B5:512 words,静态链 872 B / 余 57%) | 建任务前后各跑 `scripts/stack_report.py` |
|
||||
|
||||
> ⚠️ **改内存相关代码后必须刷新快照**:`@build` 出 `.map` 后跑 `scripts/check_mem_numbers.py --write`,同步 `docs/内存管理/RAM使用分析与优化方案.md` §2.1(`mem-numbers` 钩子会因体积变化 FAIL)。
|
||||
|
||||
@@ -466,7 +466,7 @@
|
||||
| 阶段 | 名称 | 内容 | 前置 | 里程碑 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **P0** | 采样与相量底座 | B1(周期 CONVST + `adcTask` 消费 + 建任务 + 栈预算)+ L1 + L2 | B0 + B1 ✅ | 每 1 ms 产出正确的基波相量 —— **✅ 2026-10-11 台架闭环**(L1-6 同日落地 ✅;L1-5 标定 / L1-7 等周波为 P0 遗留项,精度复核依赖标准源) |
|
||||
| **P1** | 基础保护元件 | **V0.1 有效子集**:过流(A 相,`IA`)、过负荷(A 相)+ 定值模型;B/C 相过流**建接口不使能** | P0 | 标准源注入 A 相可正确动作 —— 🔶 **软件侧 2026-10-11 完成**(`prot_set` 定值模型 30 用例 + `prot_elm` 元件框架/过流三段/过负荷 27 用例,主机全绿);板端接线(protectTask 立项)与标准源注入待做 |
|
||||
| **P1** | 基础保护元件 | **V0.1 有效子集**:过流(A 相,`IA`)、过负荷(A 相)+ 定值模型;B/C 相过流**建接口不使能** | P0 | 标准源注入 A 相可正确动作 —— 🔶 **软件侧 2026-10-11 完成**(`prot_set` 定值模型 30 用例 + `prot_elm` 元件框架/过流三段/过负荷 27 用例,主机全绿);**板端接线已落地(B5,2026-10-12:protectTask + IWDG bit8 + 栈核算 + 104 YX 上送)**;标准源注入验收待做 |
|
||||
| **P2** | 电压/频率类保护 | 过压/欠压(`UA`)、过频/低频、零序过压(`UN`) | P0 | 电压/频率越限正确动作 —— 🔶 过压/欠压/零序过压随 P1 同批主机完成(同框架实例);**过频/低频待频率测量算法** |
|
||||
| **P3** | 异常检测与闭锁 | L5 自检闭锁、采样通道自检;三相类(涌流/PT·CT 断线/母线接地)**建接口不使能** | P1/P2 | 自检失败闭锁;未使能元件不误动 |
|
||||
| **P4** | 出口、记录与定值集成 | L4 出口与记录、§5 定值模型落 cfg_file、UI 显示、上送 | P1 | 动作出口 + SOE 记录 + UI 显示闭环 |
|
||||
@@ -585,7 +585,6 @@ B0 产品定义 ✅(2026-10-10 已确认,见 §1.5)
|
||||
2. 出口硬件形态(继电器/回读)——依硬件(R7)。
|
||||
3. 等周波调频范围与精度目标——依产品定义(R4)。
|
||||
4. 是否保留遥测谐波轨(k=1~9)——依 UI/上送需求。
|
||||
5. 遥测/遥信/遥控的 104 上送实现范围与排期——依产品定义。
|
||||
|
||||
---
|
||||
|
||||
@@ -606,3 +605,4 @@ B0 产品定义 ✅(2026-10-10 已确认,见 §1.5)
|
||||
| 2026-10-11 | **L2 台架闭环:P0 收尾** | `@build` 0 错 0 警 → `@test` 闭环把 `phasor(first)`/`phasor(hb)` 列为 Required 并校验顺序 `App init OK>phasor(first)>phasor(hb)`(⚠ `net init: OK` **不在**该序内——adcTask 与 netTask 并行,`phasor(first)` ≈+500 ms 早于 `net init: OK` ≈+574 ms 属正常)→ **PASS**:5 实例相量开机即产出、心跳 5 实例持续刷新、`frames` 每 10 s +10000(1000.0 Hz)、`idle=0`、`frst_err=0`、`adc1_err=0`、`adc1_hw=0`、`fw build` 身份戳匹配、回读一致、锚点 ×1、零 `[ERR]`/HardFault;证据 `boot_log_data/20261011_prod_phasor_boot_log.txt`。栈复测:adcTask 静态 832 B / 实测峰值 **928/2048(余 54%)**,7 任务全 PASS(`docs/内存管理/RAM使用分析与优化方案.md` §2.3 已同步)。§3.1 L1-9 置 ✅(`C04-IA-F2`/`C05-IA-F5` 台架产出);§3.2 L2-1~L2-4/L2-6/L2-7 置 ✅(精度判据保留"待标准源"注),**L2-5 更正为"V0.1 不适用、随 V0.2"**(跨转换器合成被 N-18 禁止、零序走外部专用通道——原表下注"L2-1~L2-7 实现完成"对 L2-5 系误记);§7.1 P0 里程碑置 ✅(遗留 L1-5/L1-6/L1-7)。下一步 = **P1**(A 相过流/过负荷 + 定值模型)与 **L1-6 采样有效性校验** |
|
||||
| 2026-10-11 | **L1-6 采样有效性校验落地** | 新增 `App/algo/adc_health.c/h`(**纯逻辑判定层**,零平台依赖免桩单测):① 静默——两侧帧序号连续 ≥100 ms 无前进(电平位,可自愈;`adc_health_flags()` 为 L5-4 闭锁预留);② 丢帧——TPAFE 序号一次前进 >1 计 `tgap`(与 STM32 侧 `adc1_gap` 对称,边沿事件不告警——消费者超拍是已知现象,告警会淹没真故障);③ 单调性——序号倒退锁存位;④ 错误增量——FRSTDATA / HAL 错误回调 / STM32 停摆计数增量(事件位与锁存位**编号对齐**,消费者按位图"每类每次开机留痕一次")。`adc_task.c` 只做告警编排:严重事件/上升沿 `DBG_ERROR` + `SYSLOG_SELFTEST`("adc sampling fault"),下降沿 `DBG_INFO`,心跳行增 `tgap`。主机单测 `test/host/adc_health/` **31 用例全绿**(100 ms 门限两侧、tick 回绕、边沿语义、NULL 守卫、init 幂等;`adc-health-unit-test` 钩子登记)。台架 `@test` PASS:零 `[ERR]`(健康不误报)、`tgap=1→2` 与 `adc1_gap` 同步可见、`frames` 1000.0 Hz;证据 `boot_log_data/20261011_prod_adc_health_boot_log.txt`。⚠ **抬栈**:`adc_health_report`→`syslog_post` 控制台回显尾使 adcTask 静态最坏链 832→**1080 B**(512 words 余 968 B < 1024 B 底线)→ **512→640 words**,复测余 **1480 B / 57%**、堆 min_ever 7152→6640 B(AGENTS 任务表 / `docs/内存管理/RAM使用分析与优化方案.md` §2.3 已同步)。§3.1 L1-6 置 ✅ |
|
||||
| 2026-10-11 | **P1/P2 软件侧:定值模型 + 保护元件框架** | 新增 `App/algo/prot_set.c/h`(**定值模型**:Q10 定点与参考 2^10 口径一致;31 项元数据表**通讯地址逐项对照参考 `Comm/Pub/Const_CommSetPara.c`**——过流 Ⅰ/Ⅱ/Ⅲ = 0x8856~0x8861、过负荷 = 0x8871~0x8874、零序过压 = 0x888E~0x8890、过压/欠压沿用告警段 0x8027~0x802A;范围校验 + 整表装载 all-or-nothing + **sum32 正/反码封存自检闭锁**(参考 `dwTagSum/dwTagSumN` 模式,L5-1);⚠ 默认值/范围为**拟定**,参考 EEPROM 出厂表不在本仓库,待对照修订)+ `App/algo/prot_elm.c/h`(**元件框架**:通用"越限 + 返回系数滞回 + 定时限"核,数据驱动实例表 7 行 = OC1/OC2/OC3/OL(IA)+ OV/UV(UA)+ 3U0(UN);`prot_elm_eval()` 纯判据可主机单测,`prot_elm_run()` 为 phasor→测量薄适配(相量未就绪整拍跳过);定值闭锁 → 全元件复位)。主机单测 `test/host/prot_set/`(30 用例)+ `test/host/prot_elm/`(27 用例:1.01× 启动 / 100 ms 到点 / 滞回保持与释放 / 欠量方向 / 投退 / 闭锁联动 / 时基回绕)全绿,钩子 `prot-set-unit-test` / `prot-elm-unit-test` 登记。`@build` 0 错 0 警(**未接线 → 链接器整对象移除,零 Flash/RAM 足迹**);板端接线随 protectTask 立项(栈预算 + IWDG 门禁),标准源注入验收随其后。§3.3 L3-1/3/4/6 置 🔶、L3-2/5 注明框架就绪待通道/频率算法;§3.5 L5-1/2/4 置 🔶;§5.4 落实进度注 |
|
||||
| 2026-10-12 | **B5 板端接线:protectTask 立项落地** | 新增 `App/task/protect_task.c/h`(**纯编排**,判据仍在 `App/algo/`):20 ms 节拍三件事——① **L5-4 采样故障闭锁**:`adc_health_flags()` 电平位(TPAFE/STM32 stall + 序号倒退)命中 → `prot_elm_init()` 复位 + 本拍不 eval(stall 自愈自动恢复;锁存类事件位不闭锁——非"当前测量不可信");② **L5-1 定值校验和巡检**(每拍 `prot_set_selfcheck()`,31 项 sum32 开销可忽略;⚠ 与 104 定值写的竞态窗口 µs 级、同次写 seal 即清锁,如实登记,P4 定值持久化收口);③ `prot_elm_run()` + 动作位图边沿日志(`[PROT]`,V0.1 无出口 → 动作 = 遥信 + 日志)。**线程契约收口**:`prot_elm_request_reset()`(单字标志,任意上下文)——104 YK11 在 iec104Task 落请求、下一拍 eval 头部消费,杜绝与 protectTask eval 竞态(`iec104_pts.c` 改走此口);定值/元件初始化移入 `app_main_init`(内核前,phasor_init 之后)。**门禁(E-59 同款)**:`TASK_WD_PROTECT = 8` 入 `wd_required_mask()` 两分支(产品 `0xF7`→`0x1F7`、HW 层 `0xF5`→`0x1F5`,看门狗单测 39 断言同步);栈 **512 words**,`stack_report.py` 实测静态最坏链 **872 B / 余 1176 B / 57%**;**FreeRTOS 堆 48→56 KB**(`.ioc` L1 参数 + `FreeRTOSConfig.h` 同步,CCM 75.3%→87.9% < 92% 门禁,`RAM预算台账.md` §6 登记配额 48→56 KB)。任务手写于 `Src/freertos.c` USER CODE 区(Normal,V0.1 无出口不需 AboveNormal;§2.2 表已注"1 ms/高"为 P2 瞬时值口径)。单测 +1(TC-PE-010 复归请求:不当拍生效、下一拍消费,滞回保持区可区分复位与否)→ prot_elm 28 用例全绿。`@build` 0 错 0 警(Code=204348,ZI +8,196 = 堆 +8 KB);台架 `@test`(PROT 锚点 + `[STACK] protect` 水位 + 104 YX 联动)待做。§3.3 L3-1/3 注板端落地、§3.5 L5-4 置 ✅、§7 P1 里程碑更新 |
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
| **内存 RAM** | **R-2 剩余** 🟡 | LVGL 池 + UI 两份缓冲 54,146 B 仍在 SRAM1;CCM 只剩 16,304 B | 先做 R-9 或 R-4 腾 CCM,再迁 |
|
||||
| 内存 RAM | R-3 / R-4 / R-5 / R-6 / R-9 ❌ | 页缓冲未合并 / UI 帧 4 副本 / `configMAX_PRIORITIES` 仍 56 / 栈仍动态 / `LV_MEM_SIZE` 仍 40 KB | R-9 最省力(池水位门禁数据已具备);R-6 前置条件已满足 |
|
||||
| 内存 RAM | N-11 🟡 | NAND 仍直 DMA 调用方缓冲(已加运行时守卫、阈值 1024) | 补编译期/启动期地址断言 + **新铁律:堆缓冲不可被 DMA 直接寻址** |
|
||||
| 内存 RAM | §5.5 ❌ | `ram_baseline.tsv`、`RAM预算台账.md`、92% 阈值门禁均未建 | 三条一起补,否则 R-2 后半段无法排期 |
|
||||
| 内存 RAM | §5.5 ✅ | 三件套已建(2026-10-12):`scripts/ram_baseline.tsv` + `docs/内存管理/RAM预算台账.md` + `mem-budget` 钩子(92% + Top-10 附表) | 已闭环;R-9/R-2 排期解锁见登记簿 N-27(W3) |
|
||||
| **Flash** | **F-4 / F-5 / F-6** ❌ | 剩余可评估 ≈10 KB(`box_shadow` 3,246 / `draw_sw_img` 2,960 / `bin_decoder` 1,972 / `image_decoder` 952 / `triangle` 930)+ 双精度 `dadd` 等 ≈1 KB;uvprojx 仍挂 201 个 LVGL `.c`(F-2 后均为空 TU,省的是编译时长) | 逐项实测再关;⚠ `mask`/`mask_rect`/`grad` 是文字与矩形绘制的必经路径,**不可盲关** |
|
||||
| **性能/资源** | N-09 / N-16 / P2-8 / PHY / P3-x / rs485 ❌ | PASV 每次新建监听;日志每条 open/write/close;`NET_RECV_TIMEOUT_MS 30000`;PHY 强制 100M 无自协商回退;`NET_MAX_SOCKETS 8` 与单连接语义不符;`HAL_Delay(1)` 阻塞 DE | 见 §4.2 / §4.5 / §4.7 各行建议 |
|
||||
| **结构** | S-1 / S-2 / S-3 / S-4 / N-06 残余 ❌ | `NETCFG`/`UI` 语义层错位;4 处裸 `extern`;**无注册反向校验**;`app_cfg.h` 开关未接线未删;`rs485_task.c`/`lftpd_log.c` 两边不编 | `check_registration.py` 是其中唯一"1 小时工作量"的一条 |
|
||||
@@ -471,10 +471,13 @@ Select-String -Path 'MDK-ARM\STM32F407-Demo\STM32F407-Demo.map' -Pattern '__heap
|
||||
|
||||
1. **`scripts/ram_report.py` 升级为门禁**:超阈值(如 SRAM1 > 92%)即失败,并在 PR 描述里自动附 Top-10 对象表。
|
||||
→ **🟡 部分实现**:`scripts/check_mem_numbers.py` 已挂 pre-commit(以 `.map` 为**唯一事实源**,校验 `docs/内存管理/RAM使用分析与优化方案.md` 的 `MEM_SNAPSHOT:BEGIN/END` 受管区 + 散句陈旧数字扫描)——**防的是"文档数字漂移",不是"占用超阈值"**。⚠ 无 92% 阈值失败逻辑;且 CI 上无 `.map` 时该钩子自适应跳过(登记簿 **A-5 残余**)。
|
||||
→ ✅ **2026-10-12 闭环**:新增 `mem-budget` 钩子(`scripts/ram_baseline.py --gate`)——RW_IRAM1/RW_CCM >92% 即 FAIL + Top-10 RW+ZI 附表;无 `.map` 自适应跳过(A-5 残余口径不变)。
|
||||
2. **建立 `[计划] scripts/ram_baseline.tsv`**(`docs/内存管理/RAM使用分析与优化方案.md:172` 已承诺但未创建):每次构建记录 Code/RO/RW/ZI/Top 对象,形成趋势。
|
||||
→ **❌ 未建**;🟡 等效物已存在:`docs/内存管理/data/` 下 5 份 `ram_report`/`stack_report` 快照留档(人工触发,非 tsv 趋势表)。
|
||||
→ ✅ **2026-10-12 闭环**:`scripts/ram_baseline.tsv` 已建(`--update` 每次 @build 后追加;`--trend` 高亮 RW+ZI 增长 >1 KB 对象)。
|
||||
3. **RAM 预算台账**(同上,文档已承诺 `[计划] docs/内存管理/RAM预算台账.md`):按子系统配额(网络 12 KB / 存储 12 KB / UI 16 KB / RTOS 48 KB / 应用 8 KB),超配额必须先申请。
|
||||
→ **❌ 未建**。⚠ 复核:这条的价值在上升——堆已进 CCM 且只剩 16,304 B CCM 余量,"谁还能用 CCM"必须有台账,否则 R-2 后半段无法排期。
|
||||
→ ✅ **2026-10-12 闭环**:`docs/内存管理/RAM预算台账.md` 已建——子系统配额(网络 12/存储 12/UI 16/RTOS 堆 48/应用 8 KB,现状超配 = W3 债务如实展示)+ **CCM 分配台账**(谁能用 CCM、R-2 后半解锁链)+ 超配额流程 + 受管区自动生成。
|
||||
4. **DMA 可达性清单**(新增,源于 N-11):文档化"哪些缓冲必须 SRAM1",并在 CCM 迁移时对该清单加 `_Static_assert`/地址断言(`assert((addr & 0xFFF00000) == 0x20000000)`)。
|
||||
→ **🟡 半落地**:独立清单文档未建,但**铁律已进 AGENTS.md**("DMA 目的缓冲必须在 SRAM,DMA2 不可达 CCM 0x10000000"、"改 ADC 流后回 NVIC 核对");代码侧另有**运行时**守卫(`gd5f2gq5ue.c:110-136` 不可达回落轮询)。⚠ 仍缺**编译期/启动期断言**,且需补一条新铁律:**从 FreeRTOS 堆分配的缓冲不可被 DMA 直接寻址**(堆已在 CCM)。
|
||||
5. **禁止新增 C 库 `malloc`**:写进 `docs/代码规范/`,并在 `check_coding_standard.py` 加正则检查(`App/`、`Drivers/` 内出现 `malloc(` 即告警)。
|
||||
@@ -647,7 +650,7 @@ UI 剩余页面批量落地(U5)、目录级层次迁移(S-1 的物理搬
|
||||
| 交付物 | 类型 | 来源条目 | **2026-10-11 状态** |
|
||||
|--------|------|---------|---------------------|
|
||||
| `[计划] scripts/check_registration.py` | 新脚本 + pre-commit hook | S-3 / N-06 | ❌ **仍未建**(`scripts/` 11 个脚本里没有它)——这是本报告里**唯一一条"1 小时工作量、至今未做"**的机械检查 |
|
||||
| `[计划] scripts/ram_baseline.tsv` + `[计划] docs/内存管理/RAM预算台账.md` | 文档承诺落地 | §5.5 | ❌ **未建**;🟡 等效物 = `docs/内存管理/data/` 5 份快照 + `MEM_SNAPSHOT` 受管区(无趋势、无配额) |
|
||||
| `scripts/ram_baseline.tsv` + `docs/内存管理/RAM预算台账.md` | 文档承诺落地 | §5.5 | ✅ **2026-10-12 已建**(`ram_baseline.py` 三模式:`--gate`/`--update`/`--trend`/`--write`;另有 `mem-budget` 钩子) |
|
||||
| `Drivers/BSP/UI/ui_tokens.h` | 令牌层 | U1 | ❌ 未建 |
|
||||
| `Drivers/BSP/UI/ui_w_{list,kv,records,form,msgbox,title}.{c,h}` | 组件库(需双端注册) | U2/U3 | ❌ 未建 |
|
||||
| `tools/menu_gen/gen_menu.py` 扩展 + `ui_page_registry.c` 生成 | 生成器 | U3 | ❌ 未建(`gen_menu.py` 未扩展,注册表不存在) |
|
||||
@@ -690,7 +693,7 @@ UI 剩余页面批量落地(U5)、目录级层次迁移(S-1 的物理搬
|
||||
> **2026-10-11 复核收尾**:
|
||||
> - ⚠ **台账漏登两条**:本报告 **N-02(FTP 无鉴权)** 与 **N-12/N-13(dhara 移植缺陷)** 既未修复也未登记在 `../待处理内容.md` → 建议补登。
|
||||
> - ⚠ **编号撞车**:本报告曾用 N-18 指"无 CI"(该条已销项删除),而登记簿的 N-18 = "8 路电气量分属两片转换器(V0.2 改版项)"。引用时一律带文档名。
|
||||
> - `[计划] docs/内存管理/RAM预算台账.md` 与 `scripts/ram_baseline.tsv` **仍未创建**(见 §5.5)。
|
||||
> - ✅ `docs/内存管理/RAM预算台账.md` 与 `scripts/ram_baseline.tsv` **已于 2026-10-12 创建**(见 §5.5 各条闭环标注)。
|
||||
> - 本报告按 `docs/README.md` 的定位是**体系层评审报告,不是待办池**——待办一律以 `../待处理内容.md` 为准;本报告只保留**未闭环问题的发现 + 证据**,修复即销项。
|
||||
|
||||
---
|
||||
|
||||
@@ -88,9 +88,14 @@ SIM_EXEMPT = [
|
||||
("App/sync/time_sync.c", "对时任务(net+RTOS),MCU 专属"),
|
||||
("App/task/adc_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"),
|
||||
("App/task/default_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"),
|
||||
("App/task/iec104_serv.c", "固件 FreeRTOS 任务(104 上行,B4),模拟器不跑固件任务"),
|
||||
("App/task/protect_task.c", "固件 FreeRTOS 任务(保护判据节拍,B5),模拟器不跑固件任务"),
|
||||
("App/task/net_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"),
|
||||
("App/task/rs485_task.c", "固件 RS-485 任务,MCU 专属(另:uvprojx 缺口见 KNOWN_GAPS)"),
|
||||
("App/task/ui_task.c", "固件 UI 任务,模拟器另有 main.c 驱动 UI"),
|
||||
("App/protocol/iec104/iec104_asdu.c", "104 ASDU 编解码纯逻辑,L0.5 主机单测覆盖,模拟器无消费路径"),
|
||||
("App/protocol/iec104/iec104_link.c", "104 链路状态机纯逻辑(I/O 注入),L0.5 覆盖,模拟器无消费路径"),
|
||||
("App/protocol/iec104/iec104_pts.c", "104 点表/数据源适配(phasor/prot 依赖),模拟器无消费路径"),
|
||||
("App/util/sha256.c", "纯工具(FTP 鉴权在 MCU 端),模拟器无消费路径"),
|
||||
("App/util/sys_clock.c", "RTC/SD2506 依赖墙钟,MCU 专属"),
|
||||
("App/util/task_prof.c", "任务性能探针,MCU 专属"),
|
||||
|
||||
+10
-7
@@ -13,8 +13,8 @@
|
||||
|
||||
刻意**不含**(与 CI 同理由,见 workflow 文件头):
|
||||
- `@build`(需 Keil 授权;CI 也不跑,真编译是开发者本地红线)
|
||||
- `mem-numbers` 钩子(事实源 `.map` 是 Keil 产物——**本地若存在 .map 就照常跑**,
|
||||
与 CI 的自适应逻辑一致)
|
||||
- `mem-numbers` / `mem-budget` 钩子(事实源 `.map` 是 Keil 产物——**本地若存在 .map
|
||||
就照常跑**,与 CI 的自适应逻辑一致)
|
||||
|
||||
退出码:
|
||||
0 = 全部通过(或不适用项已按规则跳过)
|
||||
@@ -147,17 +147,20 @@ def run_l0():
|
||||
print("=" * 64)
|
||||
|
||||
env = dict(os.environ)
|
||||
# 与 CI 相同的自适应:无 .map(未做 Keil 构建)=> mem-numbers 不适用 => 跳过
|
||||
# 与 CI 相同的自适应:无 .map(未做 Keil 构建)=> mem-numbers / mem-budget
|
||||
# 不适用 => 跳过(两者事实源都是 Keil 产物 .map)
|
||||
map_path = _find_map()
|
||||
if map_path:
|
||||
print("[INFO] 找到 .map:{}".format(map_path.relative_to(REPO_ROOT)))
|
||||
print("[INFO] mem-numbers 照常运行")
|
||||
print("[INFO] mem-numbers / mem-budget 照常运行")
|
||||
else:
|
||||
print("[INFO] 未找到 .map(未做 Keil 构建)-> 跳过 mem-numbers(与 CI 一致)")
|
||||
print("[INFO] 未找到 .map(未做 Keil 构建)-> 跳过 mem-numbers / "
|
||||
"mem-budget(与 CI 一致)")
|
||||
print("[INFO] :warning: 体积门禁未执行——改代码后请本地 @build + "
|
||||
"check_mem_numbers.py --scan 兜底")
|
||||
"check_mem_numbers.py --scan + ram_baseline.py --gate 兜底")
|
||||
skip = env.get("SKIP", "")
|
||||
env["SKIP"] = (skip + "," if skip else "") + "mem-numbers"
|
||||
add = "mem-numbers,mem-budget"
|
||||
env["SKIP"] = (skip + "," if skip else "") + add
|
||||
|
||||
rc, text = _run_capture([PY, "-m", "pre_commit", "run", "--all-files"], env=env)
|
||||
# 汇总行(对齐 CI 的 ::notice::):统计钩子 Passed/Failed/Skipped
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
RAM 预算门禁 + 趋势台账(登记簿 N-27 前半 / 审查报告 §5.5 三件套,2026-10-12)。
|
||||
|
||||
三件套的机械部分收在本脚本(事实源 = Keil `.map`,与 `check_mem_numbers.py` 同口径):
|
||||
|
||||
1. **92% 阈值门禁**(`--gate`,pre-commit `mem-budget` 钩子入口):
|
||||
RW_IRAM1 / RW_CCM 任一区占比 > 92% → FAIL,并附 Top-10 RW+ZI 对象表;
|
||||
无 `.map`(未做 Keil 构建)→ 自适应跳过(与 `mem-numbers` 同口径,
|
||||
CI 盲区登记在 `docs/待处理内容.md` A-5 残余)。
|
||||
`--gate` 同时校验台账受管区与 `.map` 一致(防漂移,同 `mem-numbers` 思路)。
|
||||
2. **趋势基线** `scripts/ram_baseline.tsv`(`--update` 每次 @build 后追加一行;
|
||||
`--trend` 看趋势并高亮 RW+ZI 较上一行增长 >1 KB 的对象——方案 §0.3
|
||||
"ZI 增量 >1KB 必须解释"的机械化)。
|
||||
3. **台账受管区**(`--write` 同步 `docs/内存管理/RAM预算台账.md` 的
|
||||
`MEM_BUDGET:BEGIN/END` 区:区域水位 + 子系统配额对照)。
|
||||
|
||||
子系统配额(报告 §5.5 第 3 条的规划值;RTOS 堆 2026-10-12 B5 48→56 KB):
|
||||
网络 12 KB / 存储 12 KB / UI 16 KB / RTOS 堆 56 KB / 应用 8 KB
|
||||
——UI 配额是 R-9/R-2 落地后的**目标态**,现状超配 = W3 债务,台账如实展示;
|
||||
配额改动 = 先登记 `docs/内存管理/RAM预算台账.md` 变更记录,再改本脚本 `QUOTAS`。
|
||||
|
||||
用法(仓库根):
|
||||
python scripts/ram_baseline.py --gate
|
||||
python scripts/ram_baseline.py --write
|
||||
python scripts/ram_baseline.py --update
|
||||
python scripts/ram_baseline.py --trend [-n 10]
|
||||
|
||||
退出码:0 通过/跳过;1 门禁失败或受管区漂移;2 事实源解析失败。
|
||||
创建日期:2026-10-12(W1-0,登记簿 N-27 前半)
|
||||
"""
|
||||
import argparse
|
||||
import pathlib
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
|
||||
import ram_report # noqa: E402 同目录:复用 .map 解析(regions + components,口径一致)
|
||||
from check_mem_numbers import read_doc, write_doc # noqa: E402 保真读写(BOM/CRLF)
|
||||
|
||||
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
||||
MAP_DEFAULT = REPO_ROOT / "MDK-ARM" / "STM32F407-Demo" / "STM32F407-Demo.map"
|
||||
TSV_PATH = REPO_ROOT / "scripts" / "ram_baseline.tsv"
|
||||
LEDGER = REPO_ROOT / "docs" / "内存管理" / "RAM预算台账.md"
|
||||
|
||||
BEGIN = "<!-- MEM_BUDGET:BEGIN generated by scripts/ram_baseline.py -->"
|
||||
END = "<!-- MEM_BUDGET:END -->"
|
||||
|
||||
THRESHOLD_PCT = 92.0 # 报告 §5.5 第 1 条
|
||||
GATED_REGIONS = ("RW_IRAM1", "RW_CCM") # 门禁约束区;其余区仅展示
|
||||
GROWTH_ALERT_B = 1024 # 方案 §0.3:增量 >1 KB 必须解释
|
||||
TOP_N = 10 # tsv 记录 / 失败附表的对象数
|
||||
|
||||
RTOS_HEAP = "RTOS 堆"
|
||||
SUBSYSTEMS = ("网络", "存储", "UI", RTOS_HEAP, "应用", "其它")
|
||||
QUOTAS = {"网络": 12 * 1024, "存储": 12 * 1024, "UI": 16 * 1024,
|
||||
RTOS_HEAP: 56 * 1024, "应用": 8 * 1024} # RTOS 堆 48→56 KB:2026-10-12 B5(台账 §6)
|
||||
|
||||
# dhara 是裸对象名(nand.c/map.c/...),必须精确匹配,勿用前缀
|
||||
_DHARA = {"map.o", "journal.o", "nand.o", "fctl.o", "era.o"}
|
||||
# RTOS 内核静态对象:归"其它"(RTOS 配额只约束 CCM 堆本体,见台账 §2 口径表)
|
||||
_KERNEL = {"tasks.o", "queue.o", "timers.o", "event_groups.o",
|
||||
"list.o", "port.o", "cmsis_os2.o", "freertos.o"}
|
||||
|
||||
|
||||
def subsystem_of(obj):
|
||||
"""对象名 → 子系统(前缀首匹配;口径表见 RAM预算台账.md §2)。"""
|
||||
n = obj.strip().lower()
|
||||
if n.startswith(("lv_", "ui_")):
|
||||
return "UI"
|
||||
if n.startswith(("ch395f", "net_", "lftpd", "ftp_task")):
|
||||
return "网络"
|
||||
if (n.startswith(("gd5f", "nand_", "ff", "diskio", "vol_lock"))
|
||||
or n in _DHARA):
|
||||
return "存储"
|
||||
if n == "heap_4.o":
|
||||
return RTOS_HEAP
|
||||
if n in _KERNEL:
|
||||
return "其它"
|
||||
if n.startswith(("app_", "adc_", "prot_", "phasor", "sample_ring",
|
||||
"sys_", "cfg_", "dbg_", "console_log", "task_watchdog",
|
||||
"sha256", "tpafe", "sd2506", "test_task", "time_sync",
|
||||
"default_task")):
|
||||
return "应用"
|
||||
return "其它"
|
||||
|
||||
|
||||
def collect(map_path):
|
||||
"""解析 .map → {'regions': {name: r}, 'subsys': {name: bytes}, 'top': [(obj, B)]}。"""
|
||||
data = ram_report.parse_map(str(map_path))
|
||||
regions = {r["name"]: r for r in data["regions"]}
|
||||
comps = [c for c in data["components"]
|
||||
if not ram_report._is_aggregate(c["object"])
|
||||
and not c["object"].startswith("(incl.")]
|
||||
subsys = {s: 0 for s in SUBSYSTEMS}
|
||||
for c in comps:
|
||||
subsys[subsystem_of(c["object"])] += c["rw_data"] + c["zi_data"]
|
||||
top = sorted(((c["object"], c["rw_data"] + c["zi_data"]) for c in comps),
|
||||
key=lambda t: (-t[1], t[0]))
|
||||
top = [t for t in top if t[1] > 0][:TOP_N]
|
||||
return {"regions": regions, "region_list": data["regions"],
|
||||
"subsys": subsys, "top": top}
|
||||
|
||||
|
||||
def _util(r):
|
||||
return r["size"] / r["max"] * 100 if r["max"] else 0.0
|
||||
|
||||
|
||||
def _quota_status(name, used):
|
||||
if name not in QUOTAS:
|
||||
return "参考"
|
||||
if name == RTOS_HEAP and used - QUOTAS[name] <= 64:
|
||||
# heap_4.o = 堆本体 49,152 B(= 配额)+ 32 B RW 元数据,不算超
|
||||
return "堆=配额"
|
||||
if used <= QUOTAS[name]:
|
||||
return "OK"
|
||||
return f"⚠ 超 {(used - QUOTAS[name]) / 1024:.1f} KB"
|
||||
|
||||
|
||||
def render_budget(info):
|
||||
"""渲染台账受管区(按 .map 区出现顺序动态渲染,新增区自动入表;
|
||||
门禁列只约束 GATED_REGIONS,其余区标"参考")。"""
|
||||
L = []
|
||||
L.append("| 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) |")
|
||||
L.append("|---|---|---|---|---|")
|
||||
for r in info["region_list"]:
|
||||
gated = r["name"] in GATED_REGIONS
|
||||
label = f"{r['name']} (Flash)" if r["name"] == "ER_IROM1" else r["name"]
|
||||
u = _util(r)
|
||||
mark = ("PASS" if u <= THRESHOLD_PCT else "FAIL") if gated else "参考"
|
||||
L.append(f"| {label} | {r['size']:,} B | {r['max']:,} B | "
|
||||
f"{u:.1f}% | {mark} |")
|
||||
L.append("")
|
||||
L.append("| 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 |")
|
||||
L.append("|---|---|---|---|")
|
||||
for s in SUBSYSTEMS:
|
||||
used = info["subsys"][s]
|
||||
quota = f"{QUOTAS[s] // 1024} KB" if s in QUOTAS else "—"
|
||||
L.append(f"| {s} | {used:,} B | {quota} | {_quota_status(s, used)} |")
|
||||
return "\n".join(L)
|
||||
|
||||
|
||||
# --- 受管区读写(锚点语义同 check_mem_numbers.py) ---------------------------
|
||||
|
||||
def extract_managed(text):
|
||||
if BEGIN not in text or END not in text:
|
||||
return None
|
||||
s = text.index(BEGIN) + len(BEGIN)
|
||||
e = text.index(END)
|
||||
return text[s:e].strip()
|
||||
|
||||
|
||||
def replace_managed(text, block):
|
||||
s = text.index(BEGIN) + len(BEGIN)
|
||||
e = text.index(END)
|
||||
new = "\n" + block + "\n"
|
||||
if text[s:e] == new:
|
||||
return text, False
|
||||
return text[:s] + new + text[e:], True
|
||||
|
||||
|
||||
# --- 子命令 ------------------------------------------------------------------
|
||||
|
||||
def _map_or_skip(args):
|
||||
map_path = pathlib.Path(args.map)
|
||||
if not map_path.exists():
|
||||
return None
|
||||
return map_path
|
||||
|
||||
|
||||
def cmd_gate(args):
|
||||
map_path = _map_or_skip(args)
|
||||
if map_path is None:
|
||||
print("[SKIP] 无 .map(未做 Keil 构建)→ RAM 预算门禁不适用")
|
||||
print(" (CI 盲区已登记 docs/待处理内容.md A-5 残余;"
|
||||
"本地改过代码请先 @build)")
|
||||
return 0
|
||||
info = collect(map_path)
|
||||
if not info["regions"]:
|
||||
print(f"[FAIL] 未能从 {map_path.name} 解析出执行区", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
fails = []
|
||||
for name in ("RW_IRAM1", "RW_CCM"):
|
||||
r = info["regions"].get(name)
|
||||
if not r:
|
||||
fails.append(f"缺执行区 {name}(scatter 变更?)")
|
||||
continue
|
||||
u = _util(r)
|
||||
if u > THRESHOLD_PCT:
|
||||
fails.append(f"{name} 占用 {u:.1f}% > {THRESHOLD_PCT:.0f}%"
|
||||
f"({r['size']:,}/{r['max']:,} B)")
|
||||
if fails:
|
||||
print("[FAIL] RAM 预算超阈值:", file=sys.stderr)
|
||||
for f in fails:
|
||||
print(f" - {f}", file=sys.stderr)
|
||||
print(f"\nTop-{TOP_N} 静态对象(RW+ZI,找增量元凶):", file=sys.stderr)
|
||||
for obj, b in info["top"]:
|
||||
print(f" {b:>7,} B {obj}", file=sys.stderr)
|
||||
print("\n处置:削峰(静态化回退/缓冲复用)或按 docs/内存管理/RAM预算台账.md"
|
||||
" §5 走超配额流程;勿盲目抬 scatter。", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# 台账受管区一致性(防漂移;同 mem-numbers 的"事实源唯一"思路)
|
||||
if not LEDGER.exists():
|
||||
print(f"[FAIL] 台账缺失:{LEDGER.relative_to(REPO_ROOT)}", file=sys.stderr)
|
||||
return 1
|
||||
text, newline, bom = read_doc(LEDGER)
|
||||
expected = render_budget(info)
|
||||
got = extract_managed(text)
|
||||
if got is None:
|
||||
print(f"[FAIL] 台账缺受管锚点:{LEDGER.relative_to(REPO_ROOT)}",
|
||||
file=sys.stderr)
|
||||
return 1
|
||||
if got != expected:
|
||||
print("[FAIL] 台账受管区与 .map 不一致(区域水位或子系统配额表漂移)",
|
||||
file=sys.stderr)
|
||||
print(f"事实源:{map_path.relative_to(REPO_ROOT)}", file=sys.stderr)
|
||||
print("修复:python scripts/ram_baseline.py --write", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# 软提醒(不拦):tsv 落后 / .map 比源码旧
|
||||
if TSV_PATH.exists():
|
||||
rows = _read_tsv()
|
||||
ir = info["regions"].get("RW_IRAM1")
|
||||
if rows and ir and int(rows[-1]["iram1"]) != ir["size"]:
|
||||
print("[WARN] ram_baseline.tsv 末行落后于当前 .map —— "
|
||||
"@build 后请跑 `python scripts/ram_baseline.py --update`")
|
||||
else:
|
||||
print("[WARN] ram_baseline.tsv 不存在 —— 跑 "
|
||||
"`python scripts/ram_baseline.py --update` 建档")
|
||||
newest = _newest_source_mtime()
|
||||
if newest and newest > map_path.stat().st_mtime:
|
||||
print("[WARN] .map 早于最新源码——数字来自旧构建,建议先 @build 再提交")
|
||||
|
||||
ir, cc = info["regions"].get("RW_IRAM1"), info["regions"].get("RW_CCM")
|
||||
print(f"RAM 预算门禁 PASS:RW_IRAM1 {_util(ir):.1f}% / RW_CCM {_util(cc):.1f}%"
|
||||
f"(阈值 {THRESHOLD_PCT:.0f}%)")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_write(args):
|
||||
map_path = _map_or_skip(args)
|
||||
if map_path is None:
|
||||
print("[FAIL] 找不到 .map 事实源,先 @build", file=sys.stderr)
|
||||
return 1
|
||||
info = collect(map_path)
|
||||
if not LEDGER.exists():
|
||||
print(f"[FAIL] 台账不存在:{LEDGER.relative_to(REPO_ROOT)}", file=sys.stderr)
|
||||
return 1
|
||||
text, newline, bom = read_doc(LEDGER)
|
||||
if BEGIN not in text or END not in text:
|
||||
print("[FAIL] 台账缺受管锚点", file=sys.stderr)
|
||||
return 1
|
||||
new_text, did = replace_managed(text, render_budget(info))
|
||||
if did:
|
||||
write_doc(LEDGER, new_text, newline, bom)
|
||||
print("已同步台账受管区。")
|
||||
else:
|
||||
print("台账受管区已是最新。")
|
||||
return 0
|
||||
|
||||
|
||||
# --- tsv 趋势基线 ------------------------------------------------------------
|
||||
|
||||
TSV_HEADER = ("date\tcode\tro\trw\tzi\ter_irom1\ter_pct\tiram1\tiram1_pct"
|
||||
"\tccm\tccm_pct\ttop_rwzi")
|
||||
|
||||
|
||||
def _read_tsv():
|
||||
rows = []
|
||||
lines = TSV_PATH.read_text(encoding="utf-8").splitlines()
|
||||
if len(lines) < 2:
|
||||
return rows
|
||||
keys = lines[0].split("\t")
|
||||
for ln in lines[1:]:
|
||||
parts = ln.split("\t")
|
||||
if len(parts) == len(keys):
|
||||
rows.append(dict(zip(keys, parts)))
|
||||
return rows
|
||||
|
||||
|
||||
def _top_field(top):
|
||||
return ";".join(f"{obj}={b}" for obj, b in top)
|
||||
|
||||
|
||||
def cmd_update(args):
|
||||
map_path = _map_or_skip(args)
|
||||
if map_path is None:
|
||||
print("[FAIL] 找不到 .map 事实源,先 @build", file=sys.stderr)
|
||||
return 1
|
||||
info = collect(map_path)
|
||||
gt = ram_report.parse_map(str(map_path))["grand_totals"]
|
||||
if not gt:
|
||||
print("[FAIL] .map 缺 Grand Totals", file=sys.stderr)
|
||||
return 2
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
reg = info["regions"]
|
||||
row = [
|
||||
today, str(gt["code"]), str(gt["ro_data"]), str(gt["rw_data"]),
|
||||
str(gt["zi_data"]),
|
||||
str(reg["ER_IROM1"]["size"]), f"{_util(reg['ER_IROM1']):.1f}",
|
||||
str(reg["RW_IRAM1"]["size"]), f"{_util(reg['RW_IRAM1']):.1f}",
|
||||
str(reg["RW_CCM"]["size"]), f"{_util(reg['RW_CCM']):.1f}",
|
||||
_top_field(info["top"]),
|
||||
]
|
||||
lines = (TSV_PATH.read_text(encoding="utf-8").splitlines()
|
||||
if TSV_PATH.exists() else [])
|
||||
if not lines:
|
||||
lines = TSV_HEADER.split("\t")
|
||||
lines = [TSV_HEADER]
|
||||
body = [ln for ln in lines[1:] if ln.strip()
|
||||
and not ln.split("\t")[0] == today]
|
||||
body.append("\t".join(row))
|
||||
# newline="\n":Windows 文本模式默认 \n→\r\n,违反 .gitattributes eol=lf
|
||||
TSV_PATH.write_text("\n".join([lines[0]] + body) + "\n",
|
||||
encoding="utf-8", newline="\n")
|
||||
print(f"已记录 {today}:RW_IRAM1 {reg['RW_IRAM1']['size']:,} B "
|
||||
f"({_util(reg['RW_IRAM1']):.1f}%) / RW_CCM {reg['RW_CCM']['size']:,} B "
|
||||
f"({_util(reg['RW_CCM']):.1f}%) → {TSV_PATH.relative_to(REPO_ROOT)}")
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_trend(args):
|
||||
if not TSV_PATH.exists():
|
||||
print("[WARN] tsv 不存在——先跑 --update 建档")
|
||||
return 0
|
||||
rows = _read_tsv()
|
||||
if not rows:
|
||||
print("[WARN] tsv 为空")
|
||||
return 0
|
||||
print(f"趋势(最近 {min(args.n, len(rows))} 行,共 {len(rows)} 行):")
|
||||
print(f"{'日期':<12} {'RW_IRAM1':>12} {'RW_CCM':>12} {'Flash':>12}")
|
||||
for r in rows[-args.n:]:
|
||||
print(f"{r['date']:<12} {int(r['iram1']):>9,} B ({r['iram1_pct']:>4}%)"
|
||||
f" {int(r['ccm']):>7,} B ({r['ccm_pct']:>4}%)"
|
||||
f" {int(r['er_irom1']):>7,} B ({r['er_pct']:>4}%)")
|
||||
if len(rows) >= 2:
|
||||
prev = dict()
|
||||
for kv in rows[-2]["top_rwzi"].split(";"):
|
||||
if "=" in kv:
|
||||
obj, b = kv.rsplit("=", 1)
|
||||
prev[obj] = int(b)
|
||||
grew = []
|
||||
for kv in rows[-1]["top_rwzi"].split(";"):
|
||||
if "=" not in kv:
|
||||
continue
|
||||
obj, b = kv.rsplit("=", 1)
|
||||
delta = int(b) - prev.get(obj, 0)
|
||||
if delta > GROWTH_ALERT_B:
|
||||
grew.append((obj, prev.get(obj, 0), int(b), delta))
|
||||
if grew:
|
||||
print(f"\n⚠ 较上一行 RW+ZI 增长 >{GROWTH_ALERT_B} B 的对象"
|
||||
f"(提交说明须解释,方案 §0.3):")
|
||||
for obj, old, new, d in sorted(grew, key=lambda t: -t[3]):
|
||||
print(f" +{d:>6,} B {obj} ({old:,} → {new:,})")
|
||||
else:
|
||||
print("\nTop 对象无 >1 KB 的异常增长。")
|
||||
return 0
|
||||
|
||||
|
||||
# --- 工具 --------------------------------------------------------------------
|
||||
|
||||
def _newest_source_mtime():
|
||||
newest = 0
|
||||
for base in ("App", "Drivers/BSP", "Src", "Inc"):
|
||||
d = REPO_ROOT / base
|
||||
if not d.is_dir():
|
||||
continue
|
||||
for p in d.rglob("*"):
|
||||
if p.suffix in (".c", ".h") and p.is_file():
|
||||
newest = max(newest, p.stat().st_mtime)
|
||||
lv = REPO_ROOT / "Lib" / "lv_conf.h"
|
||||
if lv.is_file():
|
||||
newest = max(newest, lv.stat().st_mtime)
|
||||
return newest
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
for stream in (sys.stdout, sys.stderr):
|
||||
try:
|
||||
stream.reconfigure(encoding="utf-8", errors="replace")
|
||||
except Exception:
|
||||
pass
|
||||
ap = argparse.ArgumentParser(
|
||||
description="RAM 预算门禁 + 趋势台账(N-27 前半 / 报告 §5.5 三件套)")
|
||||
g = ap.add_mutually_exclusive_group()
|
||||
g.add_argument("--gate", action="store_true", help="92% 阈值门禁(pre-commit 入口)")
|
||||
g.add_argument("--write", action="store_true", help="同步台账受管区")
|
||||
g.add_argument("--update", action="store_true", help="追加 tsv 趋势行(@build 后跑)")
|
||||
g.add_argument("--trend", action="store_true", help="打印趋势 + 高亮 >1 KB 增长")
|
||||
ap.add_argument("-n", type=int, default=10, help="--trend 显示行数")
|
||||
ap.add_argument("--map", default=str(MAP_DEFAULT), help=".map 路径")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
if args.write:
|
||||
return cmd_write(args)
|
||||
if args.update:
|
||||
return cmd_update(args)
|
||||
if args.trend:
|
||||
return cmd_trend(args)
|
||||
return cmd_gate(args) # 默认 = --gate
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,2 @@
|
||||
date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi
|
||||
2026-10-12 204348 18028 1036 152204 222376 21.2 95816 73.1 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;console_log.o=4120;ch395f.o=4106;default_task.o=2736;sys_log_mcu.o=1924
|
||||
|
@@ -46,6 +46,8 @@ ROOTS = {
|
||||
"start_test_task": "testTask",
|
||||
"time_sync_task": "timeSyncTask",
|
||||
"adc_task_func": "adcTask",
|
||||
"iec104_serv_task": "iec104Task",
|
||||
"protect_task": "protectTask",
|
||||
}
|
||||
|
||||
# Runtime lines use the short labels emitted by stack_probe_poll() in freertos.c.
|
||||
@@ -57,6 +59,8 @@ RUNTIME_ALIAS = {
|
||||
"testTask": "test",
|
||||
"timeSyncTask": "timesync",
|
||||
"adcTask": "adc",
|
||||
"iec104Task": "iec104",
|
||||
"protectTask": "protect",
|
||||
}
|
||||
|
||||
FUNC_RE = re.compile(
|
||||
|
||||
@@ -55,6 +55,7 @@
|
||||
| `time_sync/run_unit_test.py` | **时间同步单测**:离线编译真实 `App/sync/time_sync.c`,80 断言点覆盖分片喂狗延时 / 监听退避 / 时间包重组(半包·粘连·EOF·真错·预算耗尽·tick 回绕)/ 任务主循环(**N-17 的全部逻辑贡献者**) |
|
||||
| `time_sync/time_sync_test.c` | 测试单元本体(`#include` 生产源码 + 网络 / 事件 / RTOS 可脚本化桩 + `longjmp` 跳出 `for(;;)` 任务循环) |
|
||||
| `time_sync/stub/` | 桩头文件(`FreeRTOS.h` / `task.h` / `cmsis_os2.h` / `net_socket.h` / `sys_clock.h` / `sys_events.h` / `dbg_log.h`) |
|
||||
| `iec104/run_unit_test.py` | **IEC 60870-5-104 协议栈单测(B4)**:离线编译真实 `App/protocol/iec104/` 三模块 + 数据源(prot_set/prot_elm/phasor/sample_ring),**82** 用例覆盖 CP56 往返 / APCI 半帧·粘帧·坏帧 / STARTDT 握手 / 总召分片+RBT / COV 死区 / k=12 窗满 / w=8 触发 S / t1·t2·t3 / N(R) 倒退判故障 / 遥控·定值两段式与 PN / C_CS / 未知类型·地址错误应答 |
|
||||
|
||||
> 详见 `docs/测试说明/看门狗单元测试说明.md`、`docs/测试说明/系统时钟单元测试说明.md`、
|
||||
> `docs/测试说明/配置文件单元测试说明.md`、`docs/测试说明/ADC输入层单元测试说明.md`、
|
||||
|
||||
@@ -0,0 +1,868 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 被控站协议栈主机侧单元测试
|
||||
* 模块功能:在 PC 上对 App/protocol/iec104 全栈(asdu + pts + link)做离线验证。
|
||||
*
|
||||
* 被测对象:真实源文件(#include 原样纳入):
|
||||
* iec104_asdu.c + iec104_pts.c + iec104_link.c(B4 协议核心)
|
||||
* prot_set.c + prot_elm.c + phasor.c + sample_ring.c(板端默认源
|
||||
* 的链接依赖;定值两段写走**真实 prot_set**)
|
||||
* 数据源经 iec104_pts_bind_src 注入假源(YC/YX/YK 可控),
|
||||
* 发送经 send_apdu 回调全量捕获,时基由测试注入。
|
||||
*
|
||||
* 覆盖点(docs/子系统设计/IEC104上行设计说明.md §7):
|
||||
* TC-104-001 CP56Time2a 往返 / 闰年 / IV / 钳位
|
||||
* TC-104-002 IOA 3B 与短浮点小端往返
|
||||
* TC-104-003 APCI 重组:半帧 / 粘帧 / 坏起始符 / 坏长度
|
||||
* TC-104-004 STARTDT 握手 → RUN
|
||||
* TC-104-005 OPEN 态 I 帧忽略(序号不消费)
|
||||
* TC-104-006 总召全流程:actcon→4 分片(首召 COT=4)→actterm;二召 COT=2;RBT→COT=4
|
||||
* TC-104-007 遥信变位 COV(COT=3);无效位不触发
|
||||
* TC-104-008 遥测死区 COV:死区内不发 / 越死区发
|
||||
* TC-104-009 k 窗:12 未确认停发;S 确认后恢复
|
||||
* TC-104-010 w 阈:收满 8 发 S
|
||||
* TC-104-011 t2 到期发 S
|
||||
* TC-104-012 t3 空闲发 TESTFR act;未答 t1 超时 → IDLE;con 清挂起不误杀
|
||||
* TC-104-013 遥控两段式:选择/执行成功;失配 PN;拒绝类选择段即 PN
|
||||
* TC-104-014 定值两段写(真实 prot_set):越界 PN;执行后 get_f + selfcheck
|
||||
* TC-104-015 C_RD:YC 命中 / 定值命中 / 未知 IOA COT14+EVI
|
||||
* TC-104-016 C_CS:clock_sync 收正确 epoch;IV → PN;clock_sync 失败 → PN
|
||||
* TC-104-017 未知类型 COT12+EVI;错公共地址 COT13+EVI
|
||||
* TC-104-018 STOPDT:数据停发,TESTFR 仍应答
|
||||
* TC-104-019 序号错 → IDLE(链路故障)
|
||||
* TC-104-020 pts:无效通道 IV / 未就绪 NT / 无效遥信 IV / is_dp / 表尺寸
|
||||
* TC-104-021 C_TS / C_RP actcon
|
||||
* TC-104-022 周期上送 COT=1(YC 组 + 频率)
|
||||
* TC-104-023 k 窗满 COV 不丢事件(pend 暂存,窗开后补发)
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static int s_total = 0, s_passed = 0, s_failed = 0;
|
||||
|
||||
#define CHECK(cond, name) do { \
|
||||
s_total++; \
|
||||
if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \
|
||||
else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \
|
||||
} while (0)
|
||||
|
||||
#include "iec104_asdu.c"
|
||||
#include "prot_set.c"
|
||||
#include "sample_ring.c"
|
||||
#include "phasor.c"
|
||||
#include "prot_elm.c"
|
||||
#include "iec104_pts.c"
|
||||
#include "iec104_link.c"
|
||||
|
||||
/* ---------------- 假数据源(定值转发真实 prot_set) ---------------- */
|
||||
static float s_f_yc[9];
|
||||
static int s_f_yc_rc[9];
|
||||
static uint64_t s_f_bits;
|
||||
static uint64_t s_f_valid;
|
||||
static uint32_t s_f_yk_ioa;
|
||||
static uint8_t s_f_yk_dpc;
|
||||
static uint8_t s_f_yk_sel;
|
||||
|
||||
static int f_yc(uint8_t idx, float *out)
|
||||
{
|
||||
if (idx >= 9u) {
|
||||
return -1;
|
||||
}
|
||||
if (s_f_yc_rc[idx] != 0) {
|
||||
return s_f_yc_rc[idx];
|
||||
}
|
||||
*out = s_f_yc[idx];
|
||||
return 0;
|
||||
}
|
||||
static uint64_t f_bits(void) { return s_f_bits; }
|
||||
static uint64_t f_valid(void) { return s_f_valid; }
|
||||
static int f_yk(uint32_t ioa, uint8_t dpc, uint8_t sel)
|
||||
{
|
||||
s_f_yk_ioa = ioa;
|
||||
s_f_yk_dpc = dpc;
|
||||
s_f_yk_sel = sel;
|
||||
return (ioa == 0x600Au) ? 0 : -1;
|
||||
}
|
||||
static int f_set_read(uint32_t addr, float *out)
|
||||
{
|
||||
return s_src_set_read(addr, out);
|
||||
}
|
||||
static int f_set_write(uint32_t addr, float v, uint8_t sel)
|
||||
{
|
||||
return s_src_set_write(addr, v, sel);
|
||||
}
|
||||
static const iec104_pts_src_t g_fake = {
|
||||
f_yc, f_bits, f_valid, f_yk, f_set_read, f_set_write
|
||||
};
|
||||
|
||||
/* ---------------- 发送捕获 + 对时假回调 ---------------- */
|
||||
#define CAP_MAX 48
|
||||
#define CAP_SZ 260
|
||||
static uint8_t s_cap[CAP_MAX][CAP_SZ];
|
||||
static int s_cap_n;
|
||||
static uint64_t s_clock_ms;
|
||||
static int s_clock_rc;
|
||||
|
||||
static void cap_send(void *ctx, const uint8_t *apdu, uint16_t len)
|
||||
{
|
||||
(void)ctx;
|
||||
if ((s_cap_n < CAP_MAX) && (len <= CAP_SZ)) {
|
||||
(void)memcpy(s_cap[s_cap_n], apdu, len);
|
||||
s_cap_n++;
|
||||
}
|
||||
}
|
||||
static int f_clock(void *ctx, uint64_t ms)
|
||||
{
|
||||
(void)ctx;
|
||||
s_clock_ms = ms;
|
||||
return s_clock_rc;
|
||||
}
|
||||
|
||||
static iec104_link_t g_lk;
|
||||
static const iec104_link_cfg_t g_cfg = { 1u, cap_send, f_clock };
|
||||
|
||||
/* ---------------- 主站侧模拟(N(R) 单调,照真实主站) ---------------- */
|
||||
static uint16_t m_ns;
|
||||
static uint16_t m_nr;
|
||||
|
||||
static void m_reset(void)
|
||||
{
|
||||
m_ns = 0u;
|
||||
m_nr = 0u;
|
||||
s_cap_n = 0;
|
||||
}
|
||||
static void feed_u(uint8_t q, uint32_t now)
|
||||
{
|
||||
uint8_t b[6];
|
||||
|
||||
b[0] = 0x68u; b[1] = 4u; b[2] = q; b[3] = 0u; b[4] = 0u; b[5] = 0u;
|
||||
iec104_link_on_rx(&g_lk, b, 6u, now);
|
||||
}
|
||||
static void feed_i(const uint8_t *asdu, uint16_t len, uint32_t now)
|
||||
{
|
||||
uint8_t b[6u + 250u];
|
||||
uint16_t fs = (uint16_t)(m_ns << 1);
|
||||
uint16_t fr = (uint16_t)(m_nr << 1); /* N(R) 单调(feed_s 推进) */
|
||||
|
||||
b[0] = 0x68u;
|
||||
b[1] = (uint8_t)(4u + len);
|
||||
b[2] = (uint8_t)(fs & 0xFFu);
|
||||
b[3] = (uint8_t)(fs >> 8);
|
||||
b[4] = (uint8_t)(fr & 0xFFu);
|
||||
b[5] = (uint8_t)(fr >> 8);
|
||||
(void)memcpy(&b[6], asdu, len);
|
||||
iec104_link_on_rx(&g_lk, b, (uint16_t)(6u + len), now);
|
||||
m_ns = (uint16_t)((m_ns + 1u) % 32768u);
|
||||
}
|
||||
static void feed_s(uint16_t nr, uint32_t now)
|
||||
{
|
||||
uint8_t b[6];
|
||||
uint16_t f = (uint16_t)(nr << 1);
|
||||
|
||||
b[0] = 0x68u; b[1] = 4u; b[2] = 1u; b[3] = 0u;
|
||||
b[4] = (uint8_t)(f & 0xFFu);
|
||||
b[5] = (uint8_t)(f >> 8);
|
||||
m_nr = nr; /* 主站确认推进(单调) */
|
||||
iec104_link_on_rx(&g_lk, b, 6u, now);
|
||||
}
|
||||
static uint16_t mk_asdu(uint8_t *b, uint8_t type, uint8_t vsq, uint8_t cot,
|
||||
uint8_t cot_hi, uint8_t with_ioa, uint32_t ioa,
|
||||
const uint8_t *body, uint16_t blen)
|
||||
{
|
||||
uint16_t n = iec104_asdu_put_hdr(b, type, vsq, cot, cot_hi, 1u);
|
||||
|
||||
if (with_ioa != 0u) {
|
||||
iec104_put_ioa(&b[n], ioa);
|
||||
n += 3u;
|
||||
}
|
||||
if (blen > 0u) {
|
||||
(void)memcpy(&b[n], body, blen);
|
||||
n += blen;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* ---------------- 复位 / 握手 ---------------- */
|
||||
static void reset_all(uint32_t now)
|
||||
{
|
||||
prot_set_init();
|
||||
prot_elm_init();
|
||||
iec104_pts_init();
|
||||
iec104_pts_bind_src(&g_fake);
|
||||
(void)memset(s_f_yc, 0, sizeof(s_f_yc));
|
||||
(void)memset(s_f_yc_rc, 0, sizeof(s_f_yc_rc));
|
||||
s_f_bits = 0u;
|
||||
s_f_valid = (1uLL << 14) | (1uLL << 15) | (1uLL << 16) | (1uLL << 17)
|
||||
| (1uLL << 22) | (1uLL << 30);
|
||||
s_clock_rc = 0;
|
||||
s_clock_ms = 0u;
|
||||
iec104_link_init(&g_lk, &g_cfg, NULL);
|
||||
iec104_link_open(&g_lk, now);
|
||||
m_reset();
|
||||
}
|
||||
static void handshake(uint32_t now)
|
||||
{
|
||||
feed_u(0x07u, now); /* STARTDT act */
|
||||
}
|
||||
|
||||
/* ---------------- 捕获检查 ---------------- */
|
||||
typedef struct {
|
||||
uint8_t type, vsq, cot, cot_hi, oa;
|
||||
uint32_t ca, ioa;
|
||||
const uint8_t *obj; /* ioa 之后的信息体 */
|
||||
uint16_t obj_len;
|
||||
} cap_t;
|
||||
|
||||
static int cap_i_count(void)
|
||||
{
|
||||
int k, n = 0;
|
||||
|
||||
for (k = 0; k < s_cap_n; k++) {
|
||||
if ((s_cap[k][2] & 0x01u) == 0u) {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
static int cap_u_count(uint8_t q)
|
||||
{
|
||||
int k, n = 0;
|
||||
|
||||
for (k = 0; k < s_cap_n; k++) {
|
||||
if ((s_cap[k][2] & 0x03u) == 0x03u && s_cap[k][2] == q) {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
static int cap_has_s(uint16_t *nr_out)
|
||||
{
|
||||
int k;
|
||||
|
||||
for (k = s_cap_n - 1; k >= 0; k--) {
|
||||
const uint8_t *p = s_cap[k];
|
||||
|
||||
if ((p[2] & 0x03u) == 0x01u) {
|
||||
*nr_out = (uint16_t)(((uint16_t)p[4] | ((uint16_t)p[5] << 8)) >> 1);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static int cap_i(int n, cap_t *o)
|
||||
{
|
||||
int k, seen = 0;
|
||||
|
||||
for (k = 0; k < s_cap_n; k++) {
|
||||
const uint8_t *p = s_cap[k];
|
||||
uint16_t lf = p[1];
|
||||
iec104_asdu_hdr_t h;
|
||||
|
||||
if ((p[2] & 0x01u) != 0u) {
|
||||
continue;
|
||||
}
|
||||
if (seen == n) {
|
||||
if (iec104_asdu_parse_hdr(&p[6], (uint16_t)(lf - 4u), &h) != 0) {
|
||||
return -1;
|
||||
}
|
||||
o->type = h.type;
|
||||
o->vsq = h.vsq;
|
||||
o->cot = h.cot;
|
||||
o->cot_hi = h.cot_hi;
|
||||
o->oa = h.oa;
|
||||
o->ca = h.ca;
|
||||
if (h.obj_len >= 3u) {
|
||||
o->ioa = iec104_get_ioa(h.obj);
|
||||
o->obj = h.obj + 3u;
|
||||
o->obj_len = (uint16_t)(h.obj_len - 3u);
|
||||
} else {
|
||||
o->ioa = 0u;
|
||||
o->obj = h.obj;
|
||||
o->obj_len = 0u;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
seen++;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* ============================ 测试用例 ============================ */
|
||||
|
||||
static void tc_cp56(void)
|
||||
{
|
||||
uint8_t b[7];
|
||||
uint64_t e1 = 946684800000ULL; /* 2000-01-01 00:00:00.000 */
|
||||
uint64_t e2 = 1709188496789ULL; /* 2024-02-29 06:34:56.789(闰日) */
|
||||
uint64_t back = 0u;
|
||||
int rc;
|
||||
|
||||
iec104_epoch_ms_to_cp56(e1, b);
|
||||
rc = iec104_cp56_to_epoch_ms(b, &back);
|
||||
CHECK((rc == 0) && (back == e1), "TC-001 CP56 往返 2000-01-01");
|
||||
iec104_epoch_ms_to_cp56(e2, b);
|
||||
back = 0u;
|
||||
rc = iec104_cp56_to_epoch_ms(b, &back);
|
||||
CHECK((rc == 0) && (back == e2), "TC-001 CP56 往返闰日 2024-02-29");
|
||||
b[2] |= 0x80u;
|
||||
CHECK(iec104_cp56_to_epoch_ms(b, &back) == -2, "TC-001 IV → -2");
|
||||
iec104_epoch_ms_to_cp56(0u, b); /* 早于 2000 → 钳位 */
|
||||
back = 0u;
|
||||
(void)iec104_cp56_to_epoch_ms(b, &back);
|
||||
CHECK(back == e1, "TC-001 早于 2000 钳位");
|
||||
}
|
||||
|
||||
static void tc_ioa_float(void)
|
||||
{
|
||||
uint8_t b[8];
|
||||
|
||||
iec104_put_ioa(b, 0x123456u);
|
||||
CHECK((iec104_get_ioa(b) == 0x123456u) && (b[0] == 0x56u) && (b[1] == 0x34u)
|
||||
&& (b[2] == 0x12u), "TC-002 IOA 3B 小端");
|
||||
iec104_put_float(b, 1.0f);
|
||||
CHECK((b[0] == 0x00u) && (b[1] == 0x00u) && (b[2] == 0x80u)
|
||||
&& (b[3] == 0x3Fu), "TC-002 float IEEE754 小端");
|
||||
iec104_put_float(b, -12.5f);
|
||||
CHECK(iec104_get_float(b) == -12.5f, "TC-002 float 往返");
|
||||
}
|
||||
|
||||
static void tc_apci(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
|
||||
reset_all(now);
|
||||
{
|
||||
static const uint8_t b[6] = { 0x68u, 4u, 0x07u, 0u, 0u, 0u };
|
||||
|
||||
iec104_link_on_rx(&g_lk, b, 3u, now);
|
||||
CHECK(cap_u_count(0x0Bu) == 0, "TC-003 半帧不触发");
|
||||
iec104_link_on_rx(&g_lk, &b[3], 3u, now);
|
||||
CHECK(cap_u_count(0x0Bu) == 1, "TC-003 半帧重组 → con");
|
||||
}
|
||||
{
|
||||
static const uint8_t b[12] = {
|
||||
0x68u, 4u, 0x43u, 0u, 0u, 0u, /* TESTFR act */
|
||||
0x68u, 4u, 0x13u, 0u, 0u, 0u /* STOPDT act */
|
||||
};
|
||||
|
||||
iec104_link_on_rx(&g_lk, b, 12u, now);
|
||||
CHECK((cap_u_count(0x83u) == 1)
|
||||
&& (iec104_link_state(&g_lk) == IEC104_ST_STOPPED),
|
||||
"TC-003 粘帧逐处 + STOPDT 生效");
|
||||
}
|
||||
reset_all(now);
|
||||
{
|
||||
static const uint8_t b[6] = { 0x69u, 4u, 0x07u, 0u, 0u, 0u };
|
||||
|
||||
iec104_link_on_rx(&g_lk, b, 6u, now);
|
||||
CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-003 坏起始符 → IDLE");
|
||||
}
|
||||
reset_all(now);
|
||||
{
|
||||
static const uint8_t b[6] = { 0x68u, 3u, 0x07u, 0u, 0u, 0u };
|
||||
|
||||
iec104_link_on_rx(&g_lk, b, 6u, now);
|
||||
CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-003 坏长度 → IDLE");
|
||||
}
|
||||
}
|
||||
|
||||
static void tc_startdt(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
|
||||
reset_all(now);
|
||||
CHECK(iec104_link_state(&g_lk) == IEC104_ST_OPEN, "TC-004 open → OPEN");
|
||||
handshake(now);
|
||||
CHECK((cap_u_count(0x0Bu) == 1)
|
||||
&& (iec104_link_state(&g_lk) == IEC104_ST_RUN), "TC-004 STARTDT → RUN");
|
||||
}
|
||||
|
||||
static void tc_open_ignore(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
uint8_t a[16];
|
||||
uint16_t n;
|
||||
|
||||
reset_all(now);
|
||||
n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x4011u, NULL, 0u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i_count() == 0) && (iec104_link_state(&g_lk) == IEC104_ST_OPEN),
|
||||
"TC-005 OPEN 态 I 帧忽略");
|
||||
handshake(now);
|
||||
m_ns = 0u; /* OPEN 忽略不消费序号 → 主站仍从 0 起 */
|
||||
feed_i(a, n, now); /* ns=0 仍有效(序号未被消费) */
|
||||
CHECK(cap_i_count() == 1, "TC-005 RUN 后同 ns 正常应答");
|
||||
}
|
||||
|
||||
static void tc_interrogation(void)
|
||||
{
|
||||
uint32_t now = 5000u;
|
||||
uint8_t a[32];
|
||||
uint8_t body[1];
|
||||
uint16_t n;
|
||||
cap_t c;
|
||||
|
||||
reset_all(now);
|
||||
s_f_yc[0] = 111.0f;
|
||||
s_f_yc_rc[1] = -1; /* idx1(1Ub)无效通道 */
|
||||
handshake(now);
|
||||
body[0] = IEC104_RQT_GROUP1;
|
||||
n = mk_asdu(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i_count() == 1) && (cap_i(0, &c) == 0) && (c.type == 100u)
|
||||
&& (c.cot == IEC104_COT_ACTCON), "TC-006 actcon 即发 COT7");
|
||||
iec104_link_poll(&g_lk, now);
|
||||
CHECK(cap_i_count() == 5, "TC-006 首 poll 发满预算 4 分片");
|
||||
CHECK((cap_i(1, &c) == 0) && (c.type == 3u) && (c.cot == IEC104_COT_INIT)
|
||||
&& (c.vsq == (uint8_t)(IEC104_VSQ_SQ | 4u)) && (c.ioa == 1u),
|
||||
"TC-006 YX 双点组 首召 COT=4");
|
||||
CHECK((cap_i(2, &c) == 0) && (c.type == 1u)
|
||||
&& (c.vsq == (uint8_t)(IEC104_VSQ_SQ | 29u)) && (c.ioa == 5u),
|
||||
"TC-006 YX 单点组 29 点");
|
||||
CHECK((cap_i(3, &c) == 0) && (c.type == 13u)
|
||||
&& (c.vsq == (uint8_t)(IEC104_VSQ_SQ | 8u)) && (c.ioa == 0x4011u)
|
||||
&& (c.obj_len == 40u), "TC-006 YC 组 8 点 40B");
|
||||
if (cap_i(3, &c) == 0) {
|
||||
CHECK((iec104_get_float(c.obj) == 111.0f) && (c.obj[4] == 0u),
|
||||
"TC-006 YC 值 111.0 QDS=0");
|
||||
CHECK(c.obj[9] == IEC104_QDS_IV, "TC-006 无效通道 QDS=IV");
|
||||
}
|
||||
CHECK((cap_i(4, &c) == 0) && (c.ioa == 0x401Du) && (c.vsq == 1u),
|
||||
"TC-006 频率单点 SQ=0");
|
||||
iec104_link_poll(&g_lk, now + 100u);
|
||||
CHECK((cap_i_count() == 6) && (cap_i(5, &c) == 0) && (c.type == 100u)
|
||||
&& (c.cot == IEC104_COT_ACTTERM), "TC-006 actterm 收尾");
|
||||
/* 二召:init 已清 → COT=2 */
|
||||
n = mk_asdu(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u);
|
||||
feed_i(a, n, now + 200u);
|
||||
iec104_link_poll(&g_lk, now + 200u);
|
||||
CHECK((cap_i(7, &c) == 0) && (c.type == 3u) && (c.cot == IEC104_COT_BG_SCAN),
|
||||
"TC-006 二召 COT=2");
|
||||
/* RBT 位 → COT=4(actcon3 是第 12 个未确认帧 → 先 S 确认窗才开) */
|
||||
n = mk_asdu(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, IEC104_COT_BIT_RBT,
|
||||
1u, 0u, body, 1u);
|
||||
feed_i(a, n, now + 300u);
|
||||
feed_s(12u, now + 300u);
|
||||
iec104_link_poll(&g_lk, now + 300u);
|
||||
CHECK((cap_i(12, &c) == 0) && (c.type == 3u) && (c.cot == IEC104_COT_INIT),
|
||||
"TC-006 RBT → COT=4");
|
||||
}
|
||||
|
||||
static void tc_cov_yx(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
cap_t c;
|
||||
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
iec104_link_poll(&g_lk, now); /* 建快照 */
|
||||
CHECK(cap_i_count() == 0, "TC-007 首 poll 无自发");
|
||||
s_f_bits |= (1uLL << 15); /* YX16(idx15)动作 */
|
||||
iec104_link_poll(&g_lk, now + 100u);
|
||||
CHECK((cap_i_count() == 1) && (cap_i(0, &c) == 0) && (c.type == 1u)
|
||||
&& (c.cot == IEC104_COT_SPONT) && (c.ioa == 16u)
|
||||
&& (c.obj_len == 1u) && (c.obj[0] == 1u), "TC-007 YX16 变位 COT3");
|
||||
s_f_bits &= ~(1uLL << 15); /* 复归 */
|
||||
iec104_link_poll(&g_lk, now + 200u);
|
||||
CHECK((cap_i_count() == 2) && (cap_i(1, &c) == 0) && (c.ioa == 16u)
|
||||
&& (c.obj[0] == 0u), "TC-007 YX16 复归再变");
|
||||
s_f_bits |= (1uLL << 0); /* idx0 无效位 → 不触发 */
|
||||
iec104_link_poll(&g_lk, now + 300u);
|
||||
CHECK(cap_i_count() == 2, "TC-007 无效位变位不触发");
|
||||
}
|
||||
|
||||
static void tc_cov_yc(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
cap_t c;
|
||||
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
s_f_yc[0] = 100.0f;
|
||||
iec104_link_poll(&g_lk, now); /* 快照 = 100 */
|
||||
s_f_yc[0] = 105.0f; /* 死区 10 内 */
|
||||
iec104_link_poll(&g_lk, now + 100u);
|
||||
CHECK(cap_i_count() == 0, "TC-008 死区内不上送");
|
||||
s_f_yc[0] = 130.0f; /* 130-105=25 > 10 */
|
||||
iec104_link_poll(&g_lk, now + 200u);
|
||||
CHECK((cap_i_count() == 1) && (cap_i(0, &c) == 0) && (c.type == 13u)
|
||||
&& (c.cot == IEC104_COT_SPONT) && (c.ioa == 0x4011u)
|
||||
&& (iec104_get_float(c.obj) == 130.0f), "TC-008 越死区上送");
|
||||
}
|
||||
|
||||
static void feed_rd(uint32_t now)
|
||||
{
|
||||
uint8_t a[16];
|
||||
uint16_t n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u,
|
||||
1u, 0x4011u, NULL, 0u);
|
||||
|
||||
feed_i(a, n, now);
|
||||
}
|
||||
|
||||
static void tc_k_window(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
int i;
|
||||
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
for (i = 0; i < 12; i++) {
|
||||
feed_rd(now);
|
||||
}
|
||||
CHECK(cap_i_count() == 12, "TC-009 k=12 封顶");
|
||||
feed_rd(now);
|
||||
CHECK(cap_i_count() == 12, "TC-009 第 13 个被 k 窗抑制");
|
||||
feed_s(12u, now); /* 全确认 → 窗开 */
|
||||
feed_rd(now);
|
||||
CHECK(cap_i_count() == 13, "TC-009 S 确认后恢复发送");
|
||||
}
|
||||
|
||||
static void tc_w_thresh(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
uint16_t nr = 0xFFFFu;
|
||||
int i;
|
||||
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
for (i = 0; i < 7; i++) {
|
||||
feed_rd(now);
|
||||
}
|
||||
CHECK(cap_has_s(&nr) == 0, "TC-010 收 7 未发 S");
|
||||
feed_rd(now); /* 第 8 个 → w 阈 */
|
||||
CHECK((cap_has_s(&nr) == 1) && (nr == 8u), "TC-010 w=8 → S nr=8");
|
||||
}
|
||||
|
||||
static void tc_t2(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
uint16_t nr = 0xFFFFu;
|
||||
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
feed_rd(now); /* rx_unacked=1,t2 起爆 */
|
||||
iec104_link_poll(&g_lk, now + 9999u);
|
||||
CHECK(cap_has_s(&nr) == 0, "TC-011 t2 未到期不发 S");
|
||||
iec104_link_poll(&g_lk, now + 10001u);
|
||||
CHECK((cap_has_s(&nr) == 1) && (nr == 1u), "TC-011 t2 到期发 S");
|
||||
}
|
||||
|
||||
static void tc_t3_t1(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
iec104_link_poll(&g_lk, now + 19999u);
|
||||
CHECK(cap_u_count(0x43u) == 0, "TC-012 t3 未到期无 TESTFR");
|
||||
iec104_link_poll(&g_lk, now + 20001u);
|
||||
CHECK(cap_u_count(0x43u) == 1, "TC-012 t3 到期发 TESTFR act");
|
||||
iec104_link_poll(&g_lk, now + 20001u + 15001u);
|
||||
CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-012 TESTFR 未答 t1 → IDLE");
|
||||
/* con 清挂起 → 不误杀 */
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
iec104_link_poll(&g_lk, now + 20001u);
|
||||
CHECK(cap_u_count(0x43u) == 1, "TC-012 重连 t3 再触发");
|
||||
feed_u(0x83u, now + 20100u); /* TESTFR con */
|
||||
iec104_link_poll(&g_lk, now + 35000u);
|
||||
CHECK(iec104_link_state(&g_lk) == IEC104_ST_RUN, "TC-012 con 清挂起后 t1 不误杀");
|
||||
}
|
||||
|
||||
static void tc_dc(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
uint8_t a[16];
|
||||
uint8_t body[1];
|
||||
uint16_t n;
|
||||
cap_t c;
|
||||
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
body[0] = 0x82u; /* S=1, DPC=ON */
|
||||
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Au, body, 1u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(0, &c) == 0) && (c.type == 52u) && (c.cot == IEC104_COT_ACTCON)
|
||||
&& ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-013 选择段 actcon");
|
||||
CHECK((s_f_yk_sel == 1u) && (s_f_yk_ioa == 0x600Au), "TC-013 选择段转发源");
|
||||
body[0] = 0x02u; /* 执行 ON */
|
||||
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Au, body, 1u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(1, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u)
|
||||
&& (s_f_yk_sel == 0u), "TC-013 执行段 actcon");
|
||||
/* 失配:选 ON 执 OFF */
|
||||
body[0] = 0x82u;
|
||||
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Au, body, 1u);
|
||||
feed_i(a, n, now);
|
||||
body[0] = 0x01u;
|
||||
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Au, body, 1u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(3, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u),
|
||||
"TC-013 执行失配 PN");
|
||||
/* 拒绝类(YK1):选择段即 PN */
|
||||
body[0] = 0x82u;
|
||||
n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x6000u, body, 1u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(4, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u),
|
||||
"TC-013 无出口硬件 YK 选择段 PN");
|
||||
}
|
||||
|
||||
static void tc_se(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
uint8_t a[24];
|
||||
uint8_t body[5];
|
||||
uint16_t n;
|
||||
uint32_t addr = g_prot_set_meta[PS_OC1_CUR].comm_addr; /* 0x8858 */
|
||||
cap_t c;
|
||||
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
CHECK(addr == 0x8858u, "TC-014 OC1 电流定值通讯地址");
|
||||
iec104_put_float(body, 5.0f);
|
||||
body[4] = IEC104_QDC_S; /* 选择段 */
|
||||
n = mk_asdu(a, IEC104_T_C_SE_NC_1, 1u, IEC104_COT_ACT, 0u, 1u, addr, body, 5u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(0, &c) == 0) && (c.type == 50u)
|
||||
&& ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-014 定值选择 actcon");
|
||||
body[4] = 0u; /* 执行段 */
|
||||
n = mk_asdu(a, IEC104_T_C_SE_NC_1, 1u, IEC104_COT_ACT, 0u, 1u, addr, body, 5u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(1, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u),
|
||||
"TC-014 定值执行 actcon");
|
||||
CHECK(prot_set_get_f(PS_OC1_CUR) == 5.0f, "TC-014 定值已写入(真实 prot_set)");
|
||||
CHECK(prot_set_selfcheck() == 0, "TC-014 执行后 seal → selfcheck 通过");
|
||||
iec104_put_float(body, 100000.0f); /* 越上限 */
|
||||
body[4] = IEC104_QDC_S;
|
||||
n = mk_asdu(a, IEC104_T_C_SE_NC_1, 1u, IEC104_COT_ACT, 0u, 1u, addr, body, 5u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(2, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u),
|
||||
"TC-014 越界选择段 PN");
|
||||
}
|
||||
|
||||
static void tc_rd(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
uint8_t a[16];
|
||||
uint16_t n;
|
||||
cap_t c;
|
||||
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
s_f_yc[0] = 222.0f;
|
||||
n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x4011u, NULL, 0u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(0, &c) == 0) && (c.type == 13u) && (c.cot == IEC104_COT_REQ)
|
||||
&& (iec104_get_float(c.obj) == 222.0f), "TC-015 C_RD YC 命中");
|
||||
n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x8858u, NULL, 0u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(1, &c) == 0) && (c.type == 13u) && (c.ioa == 0x8858u),
|
||||
"TC-015 C_RD 定值命中");
|
||||
n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x9999u, NULL, 0u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(2, &c) == 0) && (c.type == 102u)
|
||||
&& (c.cot == IEC104_COT_UNK_IOA) && ((c.oa & 0x80u) != 0u),
|
||||
"TC-015 未知 IOA COT14+EVI");
|
||||
}
|
||||
|
||||
static void tc_cs(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
uint8_t a[24];
|
||||
uint8_t cp[7];
|
||||
uint16_t n;
|
||||
cap_t c;
|
||||
uint64_t e = 1709188496789ULL;
|
||||
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
(void)iec104_epoch_ms_to_cp56(e, cp);
|
||||
n = mk_asdu(a, IEC104_T_C_CS_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, cp, 7u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(0, &c) == 0) && (c.type == 103u)
|
||||
&& ((c.cot_hi & IEC104_COT_BIT_PN) == 0u) && (s_clock_ms == e),
|
||||
"TC-016 C_CS 对时成功 epoch 精确");
|
||||
cp[2] |= 0x80u; /* IV */
|
||||
n = mk_asdu(a, IEC104_T_C_CS_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, cp, 7u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(1, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u),
|
||||
"TC-016 CP56 IV → PN");
|
||||
(void)iec104_epoch_ms_to_cp56(e, cp);
|
||||
s_clock_rc = -1; /* 系统侧拒绝 */
|
||||
n = mk_asdu(a, IEC104_T_C_CS_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, cp, 7u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(2, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u),
|
||||
"TC-016 clock_sync 失败 → PN");
|
||||
}
|
||||
|
||||
static void tc_err_resp(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
uint8_t a[16];
|
||||
uint16_t n, m;
|
||||
cap_t c;
|
||||
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
n = mk_asdu(a, 200u, 1u, IEC104_COT_ACT, 0u, 1u, 0u, NULL, 0u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(0, &c) == 0) && (c.type == 200u) && (c.cot == IEC104_COT_UNK_COT)
|
||||
&& ((c.oa & 0x80u) != 0u), "TC-017 未知类型 COT12+EVI");
|
||||
m = iec104_asdu_put_hdr(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, 0u, 2u);
|
||||
a[m] = IEC104_RQT_GROUP1;
|
||||
m++;
|
||||
feed_i(a, m, now); /* ca=2 ≠ 1 */
|
||||
CHECK((cap_i(1, &c) == 0) && (c.cot == IEC104_COT_UNK_CA)
|
||||
&& ((c.oa & 0x80u) != 0u), "TC-017 错公共地址 COT13+EVI");
|
||||
}
|
||||
|
||||
static void tc_stopdt(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
uint8_t a[16];
|
||||
uint8_t body[1];
|
||||
uint16_t n;
|
||||
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
feed_u(0x13u, now); /* STOPDT act */
|
||||
CHECK((cap_u_count(0x23u) == 1)
|
||||
&& (iec104_link_state(&g_lk) == IEC104_ST_STOPPED), "TC-018 STOPDT con");
|
||||
body[0] = IEC104_RQT_GROUP1;
|
||||
n = mk_asdu(a, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u);
|
||||
feed_i(a, n, now);
|
||||
CHECK(cap_i_count() == 0, "TC-018 STOPPED 态数据服务关");
|
||||
feed_u(0x43u, now);
|
||||
CHECK(cap_u_count(0x83u) == 1, "TC-018 STOPPED 态 TESTFR 仍应答");
|
||||
}
|
||||
|
||||
static void tc_seq_err(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
uint8_t a[16];
|
||||
uint16_t n;
|
||||
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
m_ns = 5u; /* 期望 0,投 5 */
|
||||
n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0x4011u, NULL, 0u);
|
||||
feed_i(a, n, now);
|
||||
CHECK(iec104_link_state(&g_lk) == IEC104_ST_IDLE, "TC-019 序号错 → IDLE");
|
||||
}
|
||||
|
||||
static void tc_pts(void)
|
||||
{
|
||||
float v;
|
||||
uint8_t q;
|
||||
uint8_t d;
|
||||
|
||||
reset_all(1000u);
|
||||
CHECK((iec104_pts_yc_count() == 9u) && (iec104_pts_yx_count() == 33u)
|
||||
&& (iec104_pts_yc_ioa(8u) == 0x401Du), "TC-020 表尺寸/IOA");
|
||||
s_f_yc[0] = 7.5f;
|
||||
CHECK((iec104_pts_yc_read(0u, &v, &q) == 0) && (v == 7.5f) && (q == 0u),
|
||||
"TC-020 有效 YC");
|
||||
s_f_yc_rc[1] = -1;
|
||||
(void)iec104_pts_yc_read(1u, &v, &q);
|
||||
CHECK((v == 0.0f) && (q == IEC104_QDS_IV), "TC-020 无效通道 IV");
|
||||
s_f_yc_rc[2] = -2;
|
||||
(void)iec104_pts_yc_read(2u, &v, &q);
|
||||
CHECK(q == IEC104_QDS_NT, "TC-020 未就绪 NT");
|
||||
iec104_pts_yx_read(0u, &d);
|
||||
CHECK(d == IEC104_DIQ_IV, "TC-020 无效遥信 IV");
|
||||
CHECK((iec104_pts_yx_is_dp(0u) == 1u) && (iec104_pts_yx_is_dp(4u) == 0u),
|
||||
"TC-020 is_dp 分界");
|
||||
s_f_bits = (1uLL << 15);
|
||||
iec104_pts_yx_read(15u, &d);
|
||||
CHECK(d == 1u, "TC-020 有效遥信 ON");
|
||||
}
|
||||
|
||||
static void tc_ts_rp(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
uint8_t a[16];
|
||||
uint8_t body[2];
|
||||
uint16_t n;
|
||||
cap_t c;
|
||||
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
body[0] = 0u;
|
||||
n = mk_asdu(a, IEC104_T_C_TS_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(0, &c) == 0) && (c.type == 104u) && (c.cot == IEC104_COT_ACTCON),
|
||||
"TC-021 C_TS actcon");
|
||||
n = mk_asdu(a, IEC104_T_C_RP_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0u, body, 1u);
|
||||
feed_i(a, n, now);
|
||||
CHECK((cap_i(1, &c) == 0) && (c.type == 105u) && (c.cot == IEC104_COT_ACTCON),
|
||||
"TC-021 C_RP actcon");
|
||||
}
|
||||
|
||||
static void tc_periodic(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
cap_t c;
|
||||
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
iec104_link_poll(&g_lk, now + 60001u);
|
||||
CHECK(cap_i_count() == 2, "TC-022 周期上送 2 分片");
|
||||
CHECK((cap_i(0, &c) == 0) && (c.type == 13u) && (c.cot == IEC104_COT_PERIODIC)
|
||||
&& (c.ioa == 0x4011u), "TC-022 周期 YC 组 COT=1");
|
||||
CHECK((cap_i(1, &c) == 0) && (c.ioa == 0x401Du), "TC-022 周期频率点");
|
||||
}
|
||||
|
||||
static void tc_cov_kfull(void)
|
||||
{
|
||||
uint32_t now = 1000u;
|
||||
int i;
|
||||
cap_t c;
|
||||
|
||||
reset_all(now);
|
||||
handshake(now);
|
||||
iec104_link_poll(&g_lk, now); /* 先建快照(首 poll 只建不算变位) */
|
||||
for (i = 0; i < 12; i++) {
|
||||
feed_rd(now); /* k 窗占满 */
|
||||
}
|
||||
s_f_bits |= (1uLL << 14); /* YX15 变位 */
|
||||
iec104_link_poll(&g_lk, now + 100u);
|
||||
CHECK(cap_i_count() == 12, "TC-023 k 满时自发发不出");
|
||||
feed_s(12u, now + 200u); /* 窗开 */
|
||||
iec104_link_poll(&g_lk, now + 300u);
|
||||
CHECK((cap_i_count() == 13) && (cap_i(12, &c) == 0) && (c.ioa == 15u)
|
||||
&& (c.cot == IEC104_COT_SPONT), "TC-023 窗开后补发不丢事件");
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
printf("=== IEC104 Protocol Stack Host Tests ===\n");
|
||||
tc_cp56();
|
||||
tc_ioa_float();
|
||||
tc_apci();
|
||||
tc_startdt();
|
||||
tc_open_ignore();
|
||||
tc_interrogation();
|
||||
tc_cov_yx();
|
||||
tc_cov_yc();
|
||||
tc_k_window();
|
||||
tc_w_thresh();
|
||||
tc_t2();
|
||||
tc_t3_t1();
|
||||
tc_dc();
|
||||
tc_se();
|
||||
tc_rd();
|
||||
tc_cs();
|
||||
tc_err_resp();
|
||||
tc_stopdt();
|
||||
tc_seq_err();
|
||||
tc_pts();
|
||||
tc_ts_rp();
|
||||
tc_periodic();
|
||||
tc_cov_kfull();
|
||||
printf("=== IEC104 Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
return (s_failed == 0) ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""IEC 60870-5-104 被控站协议栈(B4)主机侧单元测试运行器(L0.5,纯标准库)。
|
||||
|
||||
做什么:用本机 MSVC 离线编译 test/host/iec104/iec104_test.c(#include 纳入
|
||||
生产源文件 iec104_asdu.c + iec104_pts.c + iec104_link.c,及数据源链接
|
||||
依赖 prot_set.c + prot_elm.c + phasor.c + sample_ring.c),验证
|
||||
APCI 重组 / STARTDT 握手 / 总召分片 / COV / k-w-t1~t3 / 遥控与定值两段式 /
|
||||
C_RD / C_CS / 错误应答(EVI)。
|
||||
|
||||
用法(仓库根目录):
|
||||
& '.venv\\Scripts\\python.exe' test/host/iec104/run_unit_test.py
|
||||
|
||||
退出码:0=PASS / 1=测试失败 / 2=环境错误。
|
||||
|
||||
设计依据:docs/子系统设计/IEC104上行设计说明.md §7(测试方案)。
|
||||
创建日期:2026-10-12
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
|
||||
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
|
||||
|
||||
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
|
||||
|
||||
BUILD = TestBuild(
|
||||
name="IEC104 Tests",
|
||||
src=os.path.join(HERE, "iec104_test.c"),
|
||||
include_dirs=[
|
||||
os.path.join(ROOT, "App", "algo"),
|
||||
os.path.join(ROOT, "App", "protocol", "iec104"),
|
||||
HERE,
|
||||
],
|
||||
summary_marker="=== IEC104 Tests:",
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(run_host_unit_test(BUILD))
|
||||
@@ -18,6 +18,7 @@
|
||||
* TC-PE-007 时基回绕:跨 0xFFFFFFFF 的延时判定仍正确
|
||||
* TC-PE-008 动作位图聚合
|
||||
* TC-PE-009 守卫:state(NULL)/越界编号、name 越界
|
||||
* TC-PE-010 跨上下文复归请求(B5):request 不当拍生效、下一拍 eval 头部消费
|
||||
*
|
||||
* 通过:全部 TC PASS(main 返回 0)。
|
||||
* 创建日期:2026-10-11
|
||||
@@ -257,6 +258,31 @@ static void tc_guards(void) {
|
||||
CHECK(1, "TC-PE-009 eval(NULL meas) survives");
|
||||
}
|
||||
|
||||
static void tc_request_reset(void) {
|
||||
float m[PE_COUNT];
|
||||
prot_elm_state_t st;
|
||||
|
||||
prot_set_init();
|
||||
prot_elm_init();
|
||||
mk_base(m);
|
||||
m[PE_OC1] = 999.0f; /* OC1 默认 dly=0 → 启动同拍动作 */
|
||||
prot_elm_eval(1000u, m);
|
||||
prot_elm_state(PE_OC1, &st);
|
||||
CHECK(st.asserted != 0u, "TC-PE-010 pre: OC1 asserted (default dly=0)");
|
||||
|
||||
prot_elm_request_reset(); /* 跨上下文请求:不当拍生效 */
|
||||
prot_elm_state(PE_OC1, &st);
|
||||
CHECK(st.asserted != 0u, "TC-PE-010 request alone does not reset");
|
||||
|
||||
/* 下一拍头部消费请求 → 复位;测量落在滞回保持区(9.6 ∈ [返回9.5, 动作10))
|
||||
* ——不复位则 picked/asserted 保持,复位后保持清零,可区分 */
|
||||
m[PE_OC1] = 9.6f;
|
||||
prot_elm_eval(1100u, m);
|
||||
prot_elm_state(PE_OC1, &st);
|
||||
CHECK((st.picked == 0u) && (st.asserted == 0u),
|
||||
"TC-PE-010 next eval consumes request -> cleared in hysteresis band");
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
printf("=== Prot Elm Tests ===\n");
|
||||
tc_init_clear();
|
||||
@@ -268,6 +294,7 @@ int main(void) {
|
||||
tc_tick_wrap();
|
||||
tc_active_mask();
|
||||
tc_guards();
|
||||
tc_request_reset();
|
||||
|
||||
printf("\n=== Prot Elm Tests: %d/%d PASSED (failed=%d) ===\n",
|
||||
s_passed, s_total, s_failed);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/*
|
||||
/*
|
||||
* 模块名称:Task Watchdog 主机侧单元测试
|
||||
* 模块功能:在 PC 上对 App/util/task_watchdog.c 的**判定状态机**做穷举验证。
|
||||
*
|
||||
@@ -116,7 +116,7 @@ static void start_and_clear(void) {
|
||||
s_feed_count = 0;
|
||||
}
|
||||
|
||||
/* 全员置位(默认必需掩码 0x77 = bit0|1|2|4|5|6,含 adcTask) */
|
||||
/* 全员置位(默认必需掩码 0x1F7 = bit0|1|2|4|5|6|7|8,含 adc/iec104/protect) */
|
||||
static void beat_all(void) {
|
||||
task_wd_beat(TASK_WD_DEFAULT);
|
||||
task_wd_beat(TASK_WD_NET);
|
||||
@@ -124,6 +124,8 @@ static void beat_all(void) {
|
||||
task_wd_beat(TASK_WD_UI);
|
||||
task_wd_beat(TASK_WD_TIMESYNC);
|
||||
task_wd_beat(TASK_WD_ADC);
|
||||
task_wd_beat(TASK_WD_IEC104);
|
||||
task_wd_beat(TASK_WD_PROTECT);
|
||||
}
|
||||
|
||||
/* ===================== 用例 ===================== */
|
||||
@@ -152,7 +154,7 @@ static void tc_003_all_beat_feeds(void) {
|
||||
beat_all();
|
||||
poll_advance(100);
|
||||
CHECK(s_feed_count == 1, "全员到位喂狗 1 次");
|
||||
CHECK(task_wd_last_mask() == 0x77u, "last_mask == 0x77(bit0|1|2|4|5|6)");
|
||||
CHECK(task_wd_last_mask() == 0x1F7u, "last_mask == 0x1F7(bit0|1|2|4|5|6|7|8)");
|
||||
}
|
||||
|
||||
static void tc_004_take_and_clear(void) {
|
||||
@@ -174,9 +176,11 @@ static void tc_005_mask_required(void) {
|
||||
task_wd_beat(TASK_WD_FTP);
|
||||
task_wd_beat(TASK_WD_TIMESYNC);
|
||||
task_wd_beat(TASK_WD_ADC);
|
||||
task_wd_beat(TASK_WD_IEC104);
|
||||
task_wd_beat(TASK_WD_PROTECT);
|
||||
poll_advance(100);
|
||||
CHECK(s_feed_count == 0, "缺 UI 位 → 不喂狗");
|
||||
CHECK(task_wd_last_mask() == (0x77u & ~(1u << TASK_WD_UI)),
|
||||
CHECK(task_wd_last_mask() == (0x1F7u & ~(1u << TASK_WD_UI)),
|
||||
"last_mask 精确记录缺失位");
|
||||
}
|
||||
|
||||
@@ -252,7 +256,7 @@ static void tc_011_observers(void) {
|
||||
CHECK(task_wd_is_armed() == 1, "is_armed == 1(已启动)");
|
||||
beat_all();
|
||||
poll_advance(100);
|
||||
CHECK(task_wd_last_mask() == 0x77u, "last_mask 反映最近一次抽样");
|
||||
CHECK(task_wd_last_mask() == 0x1F7u, "last_mask 反映最近一次抽样");
|
||||
CHECK(task_wd_miss_count() == 0u, "全程达标 → miss_count == 0");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user