From 1fe163ba7b09e510649acda2e1271f125891013b Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Mon, 12 Oct 2026 15:01:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(protect):=20B7=20=E5=AE=9A=E5=80=BC?= =?UTF-8?q?=E6=8C=81=E4=B9=85=E5=8C=96=20+=20=E5=85=A8=E5=B1=80=E8=BD=AF?= =?UTF-8?q?=E5=8E=8B=E6=9D=BF=20+=20L5-1=20=E7=AB=9E=E6=80=81=E6=94=B6?= =?UTF-8?q?=E5=8F=A3=EF=BC=88W1=20=E4=BB=A3=E7=A0=81=E8=85=BF=E5=AE=8C?= =?UTF-8?q?=E6=88=90=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - prot_set:新增 PS_GLOBAL_EN(枚举尾追加);值区双缓冲 + 单字交换, prot_set_set_sealed 原子写+封存;selfcheck 改 seqlock 式复核 (提交撞见 = 本轮跳过)→ 收口 B5 登记的 µs 级误锁窗口 - 持久化:新增 App/cfg/prot_set_store.c/h,/prot_set.bin blob (magic+版本+计数+值区+sum32 正/反码对,144 B)走 cfg_store_write_atomic; uvprojx 注册 + 模拟器豁免点名(check_registration 同步) - 写路径:C_SE/YK12 执行段置脏 → iec104Task 投递 SYS_EVT_PROT_SET_SAVE (事件位 bit7)→ defaultTask(配置引擎属主)落盘,写失败脏回置 = 天然 重试;装载 = STORAGE_READY 屏障前整表原子 parse(损坏保出厂默认 + 自检留痕 prot set corrupt;文件缺失 = 首启静默) - YK12 转真:两段式(选择段校验 DPC / 执行段 set_sealed),protectTask 下一拍读退出态 → 复位元件 + 停判据(global plate OUT/IN 边沿日志); 本地定值项 comm_addr=0 显式不可寻址(防 IOA=0 误命中首个本地项) - 单测:prot_set 63 断言(+33:软压板/原子写/脏握手/blob 往返/损坏拒绝/ 伪造自洽越界对抗样本)、iec104 94 断言(+7:YK12 两段转真/置脏/本地项 C_RD EVI) - 文档:IEC104 设计说明 §3.3/§3.4/§7、保护规划 §3.5 L5-1/§7 P4/变更记录、 App/algo 与 App/cfg README、W1 任务卡 B7→🟦(台架腿 🔴 待窗口,随 B6 同窗) - 验证:@build 0 错 0 警(Code=205500,ZI +280 = 双缓冲+blob 缓冲); ci_local 全绿(L0 27 钩子 + L0.5 13 套件 826 断言);ram_baseline 台账 受管区与内存快照已同步;19 个改动文件无 BOM / LF / 零乱码 --- App/algo/README.md | 13 +- App/algo/prot_set.c | 247 +++++++++++++++++++++++-- App/algo/prot_set.h | 55 +++++- App/app/sys_events.h | 3 + App/cfg/README.md | 4 +- App/cfg/prot_set_store.c | 64 +++++++ App/cfg/prot_set_store.h | 31 ++++ App/protocol/iec104/iec104_pts.c | 37 ++-- App/protocol/iec104/iec104_pts.h | 11 +- App/task/default_task.c | 21 ++- App/task/iec104_serv.c | 7 + App/task/protect_task.c | 27 ++- MDK-ARM/STM32F407-Demo.uvprojx | 5 + docs/任务规划/W1-采集保护收尾.md | 4 +- docs/内存管理/RAM使用分析与优化方案.md | 6 +- docs/内存管理/RAM预算台账.md | 6 +- docs/子系统设计/IEC104上行设计说明.md | 20 +- docs/算法/继电保护功能复现规划.md | 7 +- scripts/check_registration.py | 1 + scripts/ram_baseline.tsv | 2 +- test/host/iec104/iec104_test.c | 52 ++++++ test/host/prot_set/prot_set_test.c | 103 +++++++++++ 22 files changed, 655 insertions(+), 71 deletions(-) create mode 100644 App/cfg/prot_set_store.c create mode 100644 App/cfg/prot_set_store.h diff --git a/App/algo/README.md b/App/algo/README.md index 32432413..ee3b12d1 100644 --- a/App/algo/README.md +++ b/App/algo/README.md @@ -154,16 +154,19 @@ flowchart TD ### 3.5 `prot_set.c` / `prot_set.h` — 保护定值模型(P1 / L5-1 载体) **解决的问题**:保护判据的"输入宪法"。参考系统把定值放 EEPROM + `tagPRS` 常量表 -校验和自检;本层先做 **RAM 模型**(cfg_file 持久化属 P4),把编号 / 通讯地址 / -范围 / 默认值 / 校验和闭环钉死。 +校验和自检;本层做 **RAM 模型 + 持久化纯逻辑层**(blob 序列化/解析;文件胶水在 +`App/cfg/prot_set_store.c`,B7),把编号 / 通讯地址 / 范围 / 默认值 / 校验和闭环钉死。 | API | 干什么 | 注意 | |---|---|---| | `prot_set_init()` | 装默认值 + 封存校验和(幂等) | 内核启动前可调 | -| `prot_set_load_raw(vals)` | 整表装载(P4 起 cfg_file 调用),逐项范围校验 | **all-or-nothing**:任一项越界整表不装载,`prot_set_bad_mask()` 指认越界项 | -| `prot_set_set(id, q10)` | 单项写入(范围校验) | **不自动重封存**——批量改完必须 `prot_set_seal()`,否则 selfcheck 判"非法改写" | +| `prot_set_load_raw(vals)` | 整表装载(B7 由 `prot_set_store_parse` 内部走同一路径),逐项范围校验 | **all-or-nothing**:任一项越界整表不装载,`prot_set_bad_mask()` 指认越界项;影子区 + 单字交换,读侧无中间态 | +| `prot_set_set(id, q10)` | 单项写入(范围校验,批处理路径) | **不自动重封存**——批量改完必须 `prot_set_seal()`,否则 selfcheck 判"非法改写";板端单项写一律用下行 | +| `prot_set_set_sealed(id, q10)` | **原子写+封存(B7)**:影子区改好重封后单字交换 | selfcheck 永不见中间态(收口 B5 竞态窗口);置脏标志 | +| `prot_set_dirty_peek/take/mark()` | 脏标志三件套(B7 落盘握手) | peek=iec104Task 通知;take=属主落盘前消费;mark=写失败回置重试 | +| `prot_set_store_serialize/parse(buf,len)` | 持久化 blob(`PSET`+版本+值区+sum32 正/反码对) | parse **整表原子**:magic/版本/长度/校验对/范围任一失败 = 一字不动 | | `prot_set_get_f()` / `get_ctl()` | 工程量浮点 / 投退读取 | 单字读原子 | -| `prot_set_selfcheck()` | 重算 sum32 与封存正/反码比对,失配 → 闭锁 | L5-1;`prot_set_seal()` 是解锁的唯一合法路径 | +| `prot_set_selfcheck()` | 重算 sum32 与封存正/反码比对,失配 → 闭锁 | L5-1;seqlock 式复核:提交撞见 = 本轮跳过不误锁;`prot_set_seal()` 是解锁的唯一合法路径 | **要点** diff --git a/App/algo/prot_set.c b/App/algo/prot_set.c index cfefead3..4b07260f 100644 --- a/App/algo/prot_set.c +++ b/App/algo/prot_set.c @@ -9,6 +9,8 @@ */ #include "prot_set.h" +#include /* memcpy(set_sealed 影子区拷贝,B7) */ + /* 元数据表:顺序必须与 prot_set_id_t 严格一致(逐行带地址注释,同 phasor * k_inst 的朴素行式风格) */ const prot_set_meta_t g_prot_set_meta[PS_ID_COUNT] = { @@ -44,25 +46,30 @@ const prot_set_meta_t g_prot_set_meta[PS_ID_COUNT] = { { 0x8821, 0u, PROT_SET_Q10(100.0), PROT_SET_Q10(1.0), PROT_SET_Q10(200.0) }, { 0x8825, 0u, PROT_SET_Q10(100.0), PROT_SET_Q10(1.0), PROT_SET_Q10(10000.0) }, { 0x8826, 0u, PROT_SET_Q10(5.0), PROT_SET_Q10(1.0), PROT_SET_Q10(10.0) }, + { 0x0000, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) }, /* 全局软压板投退(YK12,B7) */ }; -static uint32_t s_val[PS_ID_COUNT]; /* RAM 值区(uint32 承载 int32,校验和按字覆盖) */ +static uint32_t s_val[2][PS_ID_COUNT]; /* 双缓冲值区(uint32 承载 int32,校验和按字覆盖) + * B7:写者改影子区后单字交换 s_act, + * 读侧(protectTask selfcheck/get)永远看到一致快照 */ +static volatile uint8_t s_act; /* 活跃区下标(0/1);交换 = 单字存储 */ static uint32_t s_sum; /* 封存的正码 */ static uint32_t s_sumn; /* 封存的反码 */ -static uint32_t s_bad_mask; /* load_raw 越界位图 */ +static uint32_t s_bad_mask; /* load_raw/parse 越界位图 */ static uint8_t s_locked; /* 定值非法 → 闭锁(L5-1) */ +static uint8_t s_dirty; /* 未落盘定值改动(B7 持久化脏标志) */ /* - * 函数功能:sum32 覆盖 RAM 值区(参考 Fun_CalSum32 同款:32 位字累加自然回绕) - * 入口参数:无 + * 函数功能:sum32 覆盖指定值区(参考 Fun_CalSum32 同款:32 位字累加自然回绕) + * 入口参数:vals - PS_ID_COUNT 个字的值区 * 返 回 值:校验和正码 */ -static uint32_t prot_sum32(void) { +static uint32_t prot_sum_buf(const uint32_t *vals) { uint32_t sum = 0u; uint32_t i; for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) { - sum += s_val[i]; + sum += vals[i]; } return sum; } @@ -76,7 +83,7 @@ static void prot_load_defaults(void) { uint32_t i; for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) { - s_val[i] = (uint32_t)g_prot_set_meta[i].def; + s_val[s_act][i] = (uint32_t)g_prot_set_meta[i].def; } } @@ -89,6 +96,7 @@ void prot_set_init(void) { prot_load_defaults(); s_bad_mask = 0u; s_locked = 0u; + s_dirty = 0u; prot_set_seal(); } @@ -110,6 +118,8 @@ uint32_t prot_set_bad_mask(void) { */ int prot_set_load_raw(const int32_t *vals) { uint32_t mask = 0u; + uint32_t cur_act; + uint32_t *shadow; uint32_t i; if (vals == 0) { @@ -124,10 +134,16 @@ int prot_set_load_raw(const int32_t *vals) { if (mask != 0u) { return -1; } + /* 原子装载(B7):写影子区 → 封存 → 单字交换;读侧无中间态 */ + cur_act = s_act; + shadow = s_val[cur_act ^ 1u]; for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) { - s_val[i] = (uint32_t)vals[i]; + shadow[i] = (uint32_t)vals[i]; } - prot_set_seal(); + s_sum = prot_sum_buf(shadow); + s_sumn = ~s_sum; + s_locked = 0u; + s_act = (uint8_t)(cur_act ^ 1u); return 0; } @@ -145,10 +161,64 @@ int prot_set_set(prot_set_id_t id, int32_t q10) { if ((q10 < g_prot_set_meta[id].min) || (q10 > g_prot_set_meta[id].max)) { return -1; } - s_val[(uint32_t)id] = (uint32_t)q10; + s_val[s_act][(uint32_t)id] = (uint32_t)q10; return 0; } +/* + * 函数功能:单项**原子**写入+封存(B7,收口 B5 竞态窗口) + * 入口参数:id - 定值项编号;q10 - Q10 定点值 + * 返 回 值:0 / -1 越界或非法编号拒写(原值不动) + * 函数说明:影子区拷贝 → 改影子 → 按影子重封校验和 → 单字交换活跃区。 + * selfcheck 任何时刻看到的都是"值区与封存一致"的完整快照 + * (区别于 prot_set_set+seal 批处理路径;两路径勿在一次批处理中交错)。 + * 置脏标志 → 104 任务通知 defaultTask(配置引擎属主)落盘。 + */ +int prot_set_set_sealed(prot_set_id_t id, int32_t q10) { + uint32_t cur_act; + uint32_t *shadow; + + if (id >= PS_ID_COUNT) { + return -1; + } + if ((q10 < g_prot_set_meta[id].min) || (q10 > g_prot_set_meta[id].max)) { + return -1; + } + cur_act = s_act; + shadow = s_val[cur_act ^ 1u]; + (void)memcpy(shadow, s_val[cur_act], sizeof(s_val[0])); + shadow[(uint32_t)id] = (uint32_t)q10; + s_sum = prot_sum_buf(shadow); + s_sumn = ~s_sum; + s_locked = 0u; + s_act = (uint8_t)(cur_act ^ 1u); /* 单字交换:读侧原子切换 */ + s_dirty = 1u; + return 0; +} + +/* + * 函数功能:脏标志三件套(B7 持久化握手) + * 入口参数:无 + * 返 回 值:peek/take = 1 存在未落盘改动 / 0 干净;mark 无返回 + * 函数说明:peek 非消费(iec104Task 每轮通知,直到落盘成功); + * take 消费(defaultTask 落盘前);mark 写盘失败回置(等下轮重试)。 + * 单字节标志,多读者安全;写者只有 set_sealed(置)与属主任务(清/置)。 + */ +uint8_t prot_set_dirty_peek(void) { + return s_dirty; +} + +uint8_t prot_set_dirty_take(void) { + uint8_t d = s_dirty; + + s_dirty = 0u; + return d; +} + +void prot_set_dirty_mark(void) { + s_dirty = 1u; +} + /* * 函数功能:读定值 Q10 原值 * 入口参数:id - 定值项编号 @@ -158,7 +228,7 @@ int32_t prot_set_get(prot_set_id_t id) { if (id >= PS_ID_COUNT) { return 0; } - return (int32_t)s_val[(uint32_t)id]; + return (int32_t)s_val[s_act][(uint32_t)id]; } /* @@ -185,7 +255,7 @@ uint8_t prot_set_get_ctl(prot_set_id_t id) { * 返 回 值:sum32 正码 */ uint32_t prot_set_checksum(void) { - return prot_sum32(); + return prot_sum_buf(s_val[s_act]); } /* @@ -193,9 +263,12 @@ uint32_t prot_set_checksum(void) { * 入口参数:无 * 返 回 值:无 * 函数说明:封存同时解除闭锁——selfcheck 失配后的唯一合法解锁路径。 + * ⚠ 批处理路径(prot_set_set 多次 + 本函数收尾)在 set 与 seal + * 之间存在"已改值未封存"窗口,selfcheck 会如实判失配(防篡改语义); + * 板端单项写一律走 prot_set_set_sealed(原子,无此窗口)。 */ void prot_set_seal(void) { - s_sum = prot_sum32(); + s_sum = prot_sum_buf(s_val[s_act]); s_sumn = ~s_sum; s_locked = 0u; } @@ -206,16 +279,23 @@ void prot_set_seal(void) { * 返 回 值:0 通过 / -1 失配(已置闭锁) * 函数说明:失配 → s_locked 置位并保持,直到下一次 seal; * 元件层每拍读 prot_set_locked() 决定整体复位(联动闭锁)。 + * B7 双缓冲 + seqlock 式复核:先读封存和、再读值区、再读封存和, + * 两次不同 = 检查期间发生了原子提交 → 本轮不作数(下拍再查), + * 杜绝"提交瞬间被巡检撞见"的误锁(收口 B5 竞态窗口)。 */ int prot_set_selfcheck(void) { - if (prot_sum32() != s_sum) { + uint32_t sum_a = s_sum; + uint32_t calc = prot_sum_buf(s_val[s_act]); + uint32_t sum_b = s_sum; + + if (sum_a != sum_b) { + /* 检查期间有 set_sealed/load_raw 提交:快照不一致,本轮跳过 */ + return (s_locked != 0u) ? -1 : 0; + } + if ((calc != sum_a) || (sum_a != ~s_sumn)) { s_locked = 1u; /* 值区与封存不符:定值被非法改写 → 闭锁 */ } - if ((s_sum != (~s_sumn)) || (s_locked != 0u)) { - s_locked = 1u; - return -1; - } - return 0; + return (s_locked != 0u) ? -1 : 0; } /* @@ -226,3 +306,132 @@ int prot_set_selfcheck(void) { uint8_t prot_set_locked(void) { return s_locked; } + +/* ==================== 持久化 blob(B7,纯逻辑) ========================= */ + +/* 小端编解码(blob 跨端一致;主机单测与 MCU 同一份字节口径) */ + +/* + * 函数功能:16 位值按小端写入缓冲 + * 入口参数:p - 写位置(≥2 字节余量);v - 值 + * 返 回 值:无 + */ +static void s_put16(uint8_t *p, uint16_t v) { + p[0] = (uint8_t)(v & 0xFFu); + p[1] = (uint8_t)(v >> 8); +} + +/* + * 函数功能:32 位值按小端写入缓冲 + * 入口参数:p - 写位置(≥4 字节余量);v - 值 + * 返 回 值:无 + */ +static void s_put32(uint8_t *p, uint32_t v) { + p[0] = (uint8_t)(v & 0xFFu); + p[1] = (uint8_t)((v >> 8) & 0xFFu); + p[2] = (uint8_t)((v >> 16) & 0xFFu); + p[3] = (uint8_t)(v >> 24); +} + +/* + * 函数功能:从小端缓冲读 16 位值 + * 入口参数:p - 读位置 + * 返 回 值:值 uint16_t + */ +static uint16_t s_get16(const uint8_t *p) { + return (uint16_t)((uint32_t)p[0] | ((uint32_t)p[1] << 8)); +} + +/* + * 函数功能:从小端缓冲读 32 位值 + * 入口参数:p - 读位置 + * 返 回 值:值 uint32_t + */ +static uint32_t s_get32(const uint8_t *p) { + return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) + | ((uint32_t)p[3] << 24); +} + +/* + * 函数功能:把当前值区序列化为 blob(校验和按值区现算,不依赖封存值) + * 入口参数:p_buf - 输出缓冲;cap - 容量;p_len - 实际长度输出 + * 返 回 值:0 / -1 参数非法或容量不足 + */ +int prot_set_store_serialize(uint8_t *p_buf, uint32_t cap, uint32_t *p_len) { + const uint32_t *vals = s_val[s_act]; + uint32_t sum = 0u; + uint32_t i; + + if ((p_buf == 0) || (p_len == 0) || (cap < PROT_SET_BLOB_SZ)) { + return -1; + } + s_put32(p_buf, PROT_SET_BLOB_MAGIC); + s_put16(p_buf + 4u, (uint16_t)PROT_SET_BLOB_VER); + s_put16(p_buf + 6u, (uint16_t)PS_ID_COUNT); + for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) { + s_put32(p_buf + PROT_SET_BLOB_HDR_SZ + 4u * i, vals[i]); + sum += vals[i]; + } + s_put32(p_buf + PROT_SET_BLOB_SZ - 8u, sum); + s_put32(p_buf + PROT_SET_BLOB_SZ - 4u, ~sum); + *p_len = PROT_SET_BLOB_SZ; + return 0; +} + +/* + * 函数功能:解析 blob 并**整表原子**装载(all-or-nothing) + * 入口参数:p_buf/len - blob 字节流 + * 返 回 值:0 已装载并封存 / -1 拒绝(magic/版本/长度/校验对不符或任一项 + * 越界 → 活跃值区一字不动;越界项经 prot_set_bad_mask() 指认) + * 函数说明:解码/校验全在影子区进行,只有全部通过才交换活跃区—— + * 损坏 blob 不可能污染在用的定值(掉电/写坏/版本换代同一路径)。 + */ +int prot_set_store_parse(const uint8_t *p_buf, uint32_t len) { + uint32_t cur_act; + uint32_t *shadow; + uint32_t sum = 0u; + uint32_t mask = 0u; + uint32_t i; + + if ((p_buf == 0) || (len != PROT_SET_BLOB_SZ)) { + return -1; + } + if (s_get32(p_buf) != PROT_SET_BLOB_MAGIC) { + return -1; + } + if (s_get16(p_buf + 4u) != (uint16_t)PROT_SET_BLOB_VER) { + return -1; + } + if (s_get16(p_buf + 6u) != (uint16_t)PS_ID_COUNT) { + return -1; + } + cur_act = s_act; + shadow = s_val[cur_act ^ 1u]; + for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) { + uint32_t v = s_get32(p_buf + PROT_SET_BLOB_HDR_SZ + 4u * i); + + shadow[i] = v; + sum += v; + } + if (sum != s_get32(p_buf + PROT_SET_BLOB_SZ - 8u)) { + return -1; /* 校验和正码不符(影子区未交换前不可见,无害) */ + } + if (~sum != s_get32(p_buf + PROT_SET_BLOB_SZ - 4u)) { + return -1; /* 反码对不符 */ + } + for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) { + if (((int32_t)shadow[i] < g_prot_set_meta[i].min) + || ((int32_t)shadow[i] > g_prot_set_meta[i].max)) { + mask |= (1uL << i); + } + } + s_bad_mask = mask; + if (mask != 0u) { + return -1; /* 校验对一致但含越界项:同样整表拒绝 */ + } + s_sum = sum; + s_sumn = ~sum; + s_locked = 0u; + s_act = (uint8_t)(cur_act ^ 1u); + return 0; +} diff --git a/App/algo/prot_set.h b/App/algo/prot_set.h index a4cff982..f26c54ff 100644 --- a/App/algo/prot_set.h +++ b/App/algo/prot_set.h @@ -1,8 +1,8 @@ /* * 模块名称:保护定值模型(P1,L5-1 载体) - * 模块功能:保护定值的 RAM 模型——编号 / 通讯地址 / 范围 / 默认值 / 校验和自检。 - * 纯逻辑(仅 stdint.h),主机侧可离线单测;持久化(cfg_file 定值段) - * 属 P4,本层只提供 load_raw/validate 的装载接口。 + * 模块功能:保护定值的 RAM 模型——编号 / 通讯地址 / 范围 / 默认值 / 校验和自检, + * 以及 B7 定值持久化的**纯逻辑层**(blob 序列化/解析 + 原子装载)。 + * 文件读写胶水在 App/cfg/prot_set_store.c(MCU 端)。 * * 设计口径(docs/算法/继电保护功能复现规划.md §5): * - 定点存储:Q10(×1024),与参考系统定点系数 2^10 一致(§5.3); @@ -13,8 +13,11 @@ * - ⚠ 默认值/范围为**拟定值**:参考 EEPROM 出厂默认表未在本仓库, * 待对照后逐项修订(规划 §10.3 待补项);范围先按 DTU 馈线典型量级。 * - * 线程约定:装载(init/load_raw)在 defaultTask(P4 起);读在保护消费者任务。 - * 读接口返回快照值(单个 int32 读取原子);写与封存必须同一任务(P4 定)。 + * 线程约定(B7 定):装载(init/store_parse)在 defaultTask(存储 bring-up, + * STORAGE_READY 前);单项写在 iec104Task(C_SE/YK12,经 set_sealed 原子路径); + * 落盘在 defaultTask(配置引擎属主)。读在保护消费者任务(快照值原子)。 + * 双缓冲交换 = 单字写,读侧永远看到一致快照;写者之间靠启动时序串行 + * (装载先于 NET_READY,104 写在其后)。 * * 创建日期:2026-10-11 */ @@ -65,6 +68,8 @@ typedef enum { PS_PT_SEC, /* 0x8821 PT 二次额定(V) */ PS_CT_PRI, /* 0x8825 CT 一次额定(A) */ PS_CT_SEC, /* 0x8826 CT 二次额定(A) */ + /* --- 全局软压板(B7):104 YK12 两段式投退;0 = 保护退出服务 --- */ + PS_GLOBAL_EN, /* 本地项(无通讯地址,经 YK12 写、C_RD 不可读) */ PS_ID_COUNT } prot_set_id_t; @@ -113,4 +118,44 @@ void prot_set_seal(void); int prot_set_selfcheck(void); uint8_t prot_set_locked(void); +/* + * 函数功能:单项**原子**写入+封存(B7,收口 B5 竞态窗口) + * 入口参数:id - 定值项编号;q10 - Q10 定点值 + * 返 回 值:0 / -1 越界或非法编号拒写(原值不动) + * 函数说明:影子缓冲拷贝 → 改影子 → 重封校验和 → 单字交换活跃区。 + * selfcheck 永远看到"值区与封存一致"的完整快照,不存在 + * "已改值未封存"中间态(区别于 prot_set_set+seal 的批处理路径, + * 两条路径不得在一次批处理中途交错)。置脏标志(待持久化)。 + */ +int prot_set_set_sealed(prot_set_id_t id, int32_t q10); + +/* 脏标志(存在未落盘的定值改动):set_sealed 置位;load/init 不置 */ +uint8_t prot_set_dirty_peek(void); /* 非消费(生产者用它通知属主任务) */ +uint8_t prot_set_dirty_take(void); /* 消费(属主任务落盘前取) */ +void prot_set_dirty_mark(void); /* 写盘失败回置(等下轮重试) */ + +/* ---------------- 持久化 blob(B7;纯逻辑,主机可单测) ---------------- + * 格式(小端):magic 'PSET'(4B) + version u16 + count u16 + * + values u32×count + sum32 u32 + ~sum32 u32 + * 版本 1;count 必须等于 PS_ID_COUNT(定值表增项 → 旧 blob 整体作废, + * 编号不回收但格式换代,宁可用出厂默认也不半装载)。 */ +#define PROT_SET_BLOB_MAGIC 0x50534554uL /* 字节序 P,S,E,T */ +#define PROT_SET_BLOB_VER 1u +#define PROT_SET_BLOB_HDR_SZ 8u /* magic(4)+ver(2)+cnt(2) */ +#define PROT_SET_BLOB_SZ (PROT_SET_BLOB_HDR_SZ + 4u * (uint32_t)PS_ID_COUNT + 8u) + +/* + * 函数功能:把当前值区序列化为 blob(校验和按值区现算,不依赖封存值) + * 返 回 值:0 / -1 缓冲不足或参数非法;*p_len 输出实际长度 + */ +int prot_set_store_serialize(uint8_t *p_buf, uint32_t cap, uint32_t *p_len); + +/* + * 函数功能:解析 blob 并**整表原子**装载(all-or-nothing) + * 入口参数:p_buf/len - blob 字节流 + * 返 回 值:0 已装载并封存 / -1 拒绝(magic/版本/长度/校验对不符或 + * 任一项越界 → 值区一字不动;越界项经 prot_set_bad_mask() 指认) + */ +int prot_set_store_parse(const uint8_t *p_buf, uint32_t len); + #endif /* PROT_SET_H */ diff --git a/App/app/sys_events.h b/App/app/sys_events.h index b8f257c3..f2d63d8b 100644 --- a/App/app/sys_events.h +++ b/App/app/sys_events.h @@ -31,6 +31,9 @@ extern osEventFlagsId_t sys_evtHandle; #define SYS_EVT_CFG_SAVE (1u << 5) /* UI:网口配置保存请求(defaultTask = 配置引擎属主,消费并写盘) */ #define SYS_EVT_CONSOLE_TX (1u << 6) /* 控制台单写者:生产者入环形缓冲后置位,defaultTask 消费并发送 * (消费型命令位,非就绪位——就绪位必须 osFlagsNoClear) */ +#define SYS_EVT_PROT_SET_SAVE (1u << 7) /* 保护定值落盘请求(B7):iec104Task 检测到定值脏置位, + * defaultTask(配置引擎属主)消费并写 /prot_set.bin + * (消费型命令位;写盘失败脏标志保留 → 下轮再通知重试) */ #ifdef __cplusplus } diff --git a/App/cfg/README.md b/App/cfg/README.md index f09a590c..2b56fbe9 100644 --- a/App/cfg/README.md +++ b/App/cfg/README.md @@ -10,11 +10,13 @@ | `cfg_file.c/h` | **配置引擎核心**:解析 / 查询 / 保存 / 2s 周期 `cfg_file_poll` | | `cfg_file_store.h` | **存储后端抽象接口**(MCU=FreeRTOS 堆 / 模拟器=宿主堆,共享核心零分配符号) | | `cfg_file_store_mcu.c` | **MCU 端后端实现**:`/config.ini`(临时名由目标路径同目录推导 + `f_rename` 原子写) | +| `prot_set_store.c/h` | **保护定值持久化胶水(B7)**:`/prot_set.bin` blob 读写(blob 格式/校验/原子装载在 `App/algo/prot_set.c` 纯逻辑层);装载 = storage bring-up(屏障前),落盘 = defaultTask 消费 `SYS_EVT_PROT_SET_SAVE` | ## 设计要点 - **单任务收敛**:`cfg_file` 为静态态,只有 defaultTask 操作;UI 保存请求经 - `SYS_EVT_CFG_SAVE` 事件唤醒 defaultTask 执行(netTask/uiTask 不得直接调用) + `SYS_EVT_CFG_SAVE` 事件唤醒 defaultTask 执行(netTask/uiTask 不得直接调用); + 定值落盘同构——iec104Task 只置事件位(`SYS_EVT_PROT_SET_SAVE`),不碰文件系统 - **零分配共享核心**:核心逻辑不含任何分配符号,动态内存收进平台后端 (`cfg_store_alloc/free`:MCU→`pvPortMalloc`,模拟器→宿主堆) - **原子写**:写 `/config.TMP` 后 `f_rename` 替换,避免掉电损坏 diff --git a/App/cfg/prot_set_store.c b/App/cfg/prot_set_store.c new file mode 100644 index 00000000..4e0f3b85 --- /dev/null +++ b/App/cfg/prot_set_store.c @@ -0,0 +1,64 @@ +/* + * 模块名称:prot_set_store(B7 定值持久化)——实现 + * 模块功能:见 prot_set_store.h。blob 格式/校验/原子装载全在 prot_set.c + * (纯逻辑,主机单测覆盖),本文件只做"读文件→parse、 + * serialize→原子写"的平台胶水。 + * 文件口径:"/prot_set.bin"(卷根,8.3 合规,FF_USE_LFN=0 约束); + * 原子写临时名由 cfg_store_write_atomic 按目标目录推导 + * (固定 config.TMP),与 /config.ini 的保存同在 defaultTask + * 串行执行,无并发竞争。 + * 创建日期:2026-10-12(B7) + */ +#include "prot_set_store.h" + +#include + +#include "cfg_file_store.h" +#include "prot_set.h" + +#define PROT_SET_STORE_PATH "/prot_set.bin" + +/* blob 收发缓冲(静态:defaultTask 栈深已是全项目最深链,勿再压 144 B 局部) */ +static uint8_t s_blob[PROT_SET_BLOB_SZ]; + +/* + * 函数功能:开机装载定值 blob + * 入口参数:无 + * 返 回 值:0 已装载 / 1 文件缺失或读失败(保持默认)/ -1 损坏被拒 + * 函数说明:读失败与"文件不存在"同归 1——启动期卷锁无竞争(本任务独占 + * bring-up),try-lock 失败即真 I/O 故障,此时保持出厂默认并让 + * 调用方按"无档"处理是安全侧(定值仍封存自洽,L5-1 不误锁)。 + */ +int prot_set_store_load(void) { + size_t nread = 0u; + + if (cfg_store_read(PROT_SET_STORE_PATH, s_blob, sizeof(s_blob), &nread) != 0) { + return 1; + } + if (prot_set_store_parse(s_blob, (uint32_t)nread) != 0) { + return -1; + } + return 0; +} + +/* + * 函数功能:落盘当前定值(消费脏标志;失败回置等重试) + * 入口参数:无 + * 返 回 值:0 无待写或成功 / -1 写失败 + */ +int prot_set_store_save(void) { + uint32_t len = 0u; + + if (prot_set_dirty_take() == 0u) { + return 0; + } + if (prot_set_store_serialize(s_blob, (uint32_t)sizeof(s_blob), &len) != 0) { + prot_set_dirty_mark(); + return -1; + } + if (cfg_store_write_atomic(PROT_SET_STORE_PATH, s_blob, (size_t)len) != 0) { + prot_set_dirty_mark(); /* 卷忙/写失败:脏保留,iec104Task 下轮再通知 */ + return -1; + } + return 0; +} diff --git a/App/cfg/prot_set_store.h b/App/cfg/prot_set_store.h new file mode 100644 index 00000000..c4570709 --- /dev/null +++ b/App/cfg/prot_set_store.h @@ -0,0 +1,31 @@ +/* + * 模块名称:prot_set_store + * 模块功能:保护定值持久化胶水(B7)——把 prot_set 的纯逻辑 blob + * (prot_set_store_serialize/parse)落到 cfg 存储后端的 + * "/prot_set.bin"(MCU = NAND/FatFs 原子写,模拟器 = 宿主文件系统)。 + * 装载在 defaultTask 存储 bring-up(STORAGE_READY 前); + * 落盘在 defaultTask(配置引擎属主,iec104Task 经 + * SYS_EVT_PROT_SET_SAVE 投递请求,本模块只被属主调用)。 + * 适用平台:双端接口(MCU 与模拟器共用 cfg_file_store 抽象) + * 创建日期:2026-10-12(B7) + */ +#ifndef PROT_SET_STORE_H +#define PROT_SET_STORE_H + +/* + * 函数功能:开机装载定值 blob(整表原子,all-or-nothing) + * 返回值:0 - 已装载;1 - 文件缺失(首启正常,保持出厂默认); + * -1 - blob 损坏/越界被拒(保持出厂默认,调用方留痕) + * 限定条件:仅 defaultTask、卷已挂载(storage_bring_up 内调用) + */ +int prot_set_store_load(void); + +/* + * 函数功能:落盘当前定值(内部先消费脏标志;无改动 = 空操作返回 0) + * 返回值:0 - 无待写或写成功;-1 - 写失败(脏标志已回置,等下轮重试) + * 限定条件:仅 defaultTask 调用(配置引擎属主;原子写 = 临时文件 + rename, + * 与 /config.ini 共用同目录 config.TMP——两者都在本任务串行执行,无并发) + */ +int prot_set_store_save(void); + +#endif /* PROT_SET_STORE_H */ diff --git a/App/protocol/iec104/iec104_pts.c b/App/protocol/iec104/iec104_pts.c index 49560e6c..c3ffd6fd 100644 --- a/App/protocol/iec104/iec104_pts.c +++ b/App/protocol/iec104/iec104_pts.c @@ -358,19 +358,32 @@ static uint64_t s_src_yx_valid(void) { } /* - * 函数功能:板端默认源 - 遥控(V0.1 仅 YK11 复归 → prot_elm_request_reset) - * 返 回 值:0 / -1 无出口硬件或未知 IOA(YK1~4/12~14 负向确认) + * 函数功能:板端默认源 - 遥控(YK11 复归 → prot_elm_request_reset; + * YK12 全局软压板 → PS_GLOBAL_EN 两段式转真,B7) + * 返 回 值:0 / -1 无出口硬件或未知 IOA(YK1~4/13/14 负向确认) * 函数说明:本回调跑在 iec104Task 上下文,**不得**直接 prot_elm_init() * (与 protectTask 的 eval 竞态)→ 落请求,下一拍 eval 头部消费(B5)。 + * YK12 走 prot_set_set_sealed(原子写+封存,B7);protectTask 下一拍 + * 读到压板态即复位元件并停判据(protect_task.c)。 */ static int s_src_yk_exec(uint32_t ioa, uint8_t dpc, uint8_t select) { - (void)dpc; /* 本期仅 YK11 与双点值无关(复归) */ switch (ioa) { case 0x600Au: /* YK11 信号复归 → 元件状态复位(异步一拍) */ + (void)dpc; /* 复归与双点值无关 */ if (select == 0u) { prot_elm_request_reset(); } return 0; + case 0x600Bu: /* YK12 全局软压板(PS_GLOBAL_EN)两段式 */ + if ((dpc != (uint8_t)IEC104_DPC_ON) && (dpc != (uint8_t)IEC104_DPC_OFF)) { + return -1; /* 非法双点命令:选择段即拒 */ + } + if (select != 0u) { + return 0; /* 选择段 = 只校验 */ + } + return prot_set_set_sealed(PS_GLOBAL_EN, + (dpc == (uint8_t)IEC104_DPC_ON) + ? PROT_SET_Q10(1) : PROT_SET_Q10(0)); default: return -1; /* 其余 YK:无出口硬件/无对应能力 → 拒绝 */ } @@ -378,7 +391,7 @@ static int s_src_yk_exec(uint32_t ioa, uint8_t dpc, uint8_t select) { /* * 函数功能:板端默认源 - 定值读(扫 g_prot_set_meta 通讯地址映射) - * 返 回 值:0 / -1 未知地址 + * 返 回 值:0 / -1 未知地址(本地项 comm_addr=0 不可寻址,B7 起显式排除) */ static int s_src_set_read(uint32_t addr, float *out) { uint32_t i; @@ -387,7 +400,8 @@ static int s_src_set_read(uint32_t addr, float *out) { return -1; } for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) { - if (g_prot_set_meta[i].comm_addr == (uint16_t)addr) { + if ((g_prot_set_meta[i].comm_addr != 0u) + && (g_prot_set_meta[i].comm_addr == (uint16_t)addr)) { *out = prot_set_get_f((prot_set_id_t)i); return 0; } @@ -396,8 +410,9 @@ static int s_src_set_read(uint32_t addr, float *out) { } /* - * 函数功能:板端默认源 - 定值写(Q10 范围校验;执行段 prot_set_set + seal) - * 返 回 值:0 / -1 越界或未知地址 + * 函数功能:板端默认源 - 定值写(Q10 范围校验;执行段 prot_set_set_sealed + * 原子写+封存,B7 收口 B5 竞态窗口;置脏 → 104 任务通知落盘) + * 返 回 值:0 / -1 越界或未知地址(本地项 comm_addr=0 不可寻址) */ static int s_src_set_write(uint32_t addr, float v, uint8_t select) { uint32_t i; @@ -406,7 +421,7 @@ static int s_src_set_write(uint32_t addr, float v, uint8_t select) { 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) { + if ((meta->comm_addr == 0u) || (meta->comm_addr != (uint16_t)addr)) { continue; } q10 = PROT_SET_Q10(v); @@ -416,11 +431,7 @@ static int s_src_set_write(uint32_t addr, float v, uint8_t select) { 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 prot_set_set_sealed((prot_set_id_t)i, q10); } return -1; /* 未知地址 */ } diff --git a/App/protocol/iec104/iec104_pts.h b/App/protocol/iec104/iec104_pts.h index d488a41e..d2438bd8 100644 --- a/App/protocol/iec104/iec104_pts.h +++ b/App/protocol/iec104/iec104_pts.h @@ -10,10 +10,13 @@ * (无效位),频率随 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 校验和); + * - YK:IOA 0x6000+(§1.5 表(3));V0.1 无出口硬件(规划 R7)—— + * YK11 信号复归(→ prot_elm_request_reset)与 YK12 全局软压板 + * (→ PS_GLOBAL_EN 两段式转真,B7)转真执行,其余一律否定确认(COT|PN),如实回; + * - 定值:IOA = prot_set 元数据 comm_addr(0x8820 基址 + 告警段 0x8027~0x802A; + * comm_addr=0 本地项不可寻址), + * 读经 C_RD、写经 C_SE_NC(选择/执行两段,执行段原子写+封存并置脏, + * 持久化 /prot_set.bin 由 defaultTask 执行,B7); * - 固有参数 8001H~800AH:本期**不实现**(字符串类无法用短浮点上送), * 按未知 IOA(COT 14 + EVI)应答,登记为待办(设计文档)。 * 线程约定:init/bind 内核启动前或建服务前;cov_check/read 仅 104 服务任务上下文。 diff --git a/App/task/default_task.c b/App/task/default_task.c index 4439702d..68dd7a6a 100644 --- a/App/task/default_task.c +++ b/App/task/default_task.c @@ -35,6 +35,7 @@ #include "nand_ftl.h" #include "net_cfg.h" #include "cfg_file.h" +#include "prot_set_store.h" #include "sys_log.h" #include "sys_events.h" /* 深调用链模块(本任务 = 存储 bring-up + 配置引擎属主 + syslog 唯一写者, @@ -221,7 +222,8 @@ void default_task_func(void *argument) { task_wd_seg(TASK_WD_SEG_EVTWAIT); if (sys_evtHandle != NULL) { (void)osEventFlagsWait(sys_evtHandle, - SYS_EVT_CFG_SAVE | SYS_EVT_CONSOLE_TX, + SYS_EVT_CFG_SAVE | SYS_EVT_CONSOLE_TX + | SYS_EVT_PROT_SET_SAVE, osFlagsWaitAny, 50u); } /* 控制台唯一发送点:把环形缓冲中的日志按块发出(无待发时立即返回) */ @@ -242,6 +244,9 @@ void default_task_func(void *argument) { #if !defined(TEST_SUITE_STORAGE) task_wd_seg(TASK_WD_SEG_CFGSAVE); net_cfg_save_run(); + /* 保护定值落盘(B7):内部消费脏标志,无改动 = 空操作; + * 写失败脏回置 → iec104Task 下轮 poll 再投递事件 = 天然重试 */ + (void)prot_set_store_save(); #endif /* 配置文件变更轮询(2s 周期;沉降去抖在引擎内) */ if ((cfg_poll_cnt++ % 40u) == 0u) { @@ -527,6 +532,20 @@ static FRESULT storage_bring_up(FATFS *fs) { * 文件缺失/损坏 = 工厂默认,不阻塞组网)。须在置位 STORAGE_READY 前, * 使"存储就绪"屏障蕴含"配置已就绪",netTask 组网时配置恒有效 */ (void)net_cfg_load(); + /* 装载保护定值持久化档(B7):文件缺失 = 出厂默认(首启正常, + * 静默);blob 损坏/越界 = 整表拒绝保持默认 + 自检留痕 + * (all-or-nothing,prot_set_store_parse 保证一字不半装载)。 + * 须在 STORAGE_READY 前——netTask 等此屏障,104 定值写必在装载后 */ + { + int ps_rc = prot_set_store_load(); + + if (ps_rc == 0) { + DBG_INFO("[CFG] prot set loaded (/prot_set.bin)"); + } else if (ps_rc < 0) { + DBG_ERROR("[CFG] prot set blob CORRUPT, defaults in use"); + syslog_post(SYSLOG_SELFTEST, "prot set corrupt"); + } + } /* 广播"存储就绪"(= 卷挂载 + 配置加载完成),唤醒依赖文件系统的任务 */ if (sys_evtHandle != NULL) { osEventFlagsSet(sys_evtHandle, SYS_EVT_STORAGE_READY); diff --git a/App/task/iec104_serv.c b/App/task/iec104_serv.c index 35b18417..db6ba923 100644 --- a/App/task/iec104_serv.c +++ b/App/task/iec104_serv.c @@ -15,6 +15,7 @@ #include "iec104_link.h" #include "iec104_pts.h" #include "net_socket.h" +#include "prot_set.h" /* 定值脏标志 peek(B7 落盘通知) */ #include "sys_clock.h" #include "sys_events.h" #include "task_watchdog.h" @@ -290,6 +291,12 @@ void iec104_serv_task(void *argument) { for (;;) { task_wd_beat(TASK_WD_IEC104); s_serv_poll(); + /* 定值改动(C_SE 执行段 / YK12)→ 通知属主任务落盘 /prot_set.bin(B7)。 + * peek 非消费:写盘失败时脏标志仍在,下一轮再投递 = 天然重试; + * 本任务不碰文件系统(配置引擎属主 = defaultTask,模块约定) */ + if ((prot_set_dirty_peek() != 0u) && (sys_evtHandle != NULL)) { + (void)osEventFlagsSet(sys_evtHandle, SYS_EVT_PROT_SET_SAVE); + } osDelay(IEC104_POLL_MS); } } diff --git a/App/task/protect_task.c b/App/task/protect_task.c index b29b9dc6..22a22a98 100644 --- a/App/task/protect_task.c +++ b/App/task/protect_task.c @@ -5,7 +5,9 @@ * 复位(防陈旧判据)+ 本拍不 eval; * ② prot_set_selfcheck()(L5-1 定值完整性巡检,31 项 sum32,开销 * 可忽略;失配 → prot_set 内部闭锁,eval 自动复位全元件); - * ③ prot_elm_run()(phasor rms → 判据步进)+ 动作位图边沿日志。 + * ③ prot_elm_run()(phasor rms → 判据步进)+ 动作位图边沿日志; + * ④ 全局软压板 PS_GLOBAL_EN(104 YK12 两段式投退,B7):退出 = + * 复位元件 + 停判据(与采样闭锁同构,独立边沿日志)。 * 定值/元件初始化在 app_main_init(内核前,B5);本任务只消费。 * 线程约定:prot_elm_run / prot_set_selfcheck 仅本任务上下文(prot_elm.h); * 跨上下文复归(104 YK11)经 prot_elm_request_reset 落请求。 @@ -31,6 +33,7 @@ static uint32_t s_prev_active; /* 上拍动作位图(边沿日志用) */ static uint8_t s_prev_hf_lock; /* 上拍采样闭锁态(边沿日志用) */ +static uint8_t s_prev_plate_out; /* 上拍全局软压板退出态(边沿日志用,B7) */ /* * 函数功能:动作位图变化沿日志(逐变化元件打名字与状态) @@ -90,11 +93,25 @@ void protect_task(void *argument) { s_prev_hf_lock = 0u; } /* L5-1 巡检:定值区与封存校验和不符 → prot_set 闭锁(下一拍起 - * eval 自动复位全元件)。⚠ 与 104 定值写(iec104Task set+seal) - * 的竞态窗口为 µs 级:selfcheck 误锁后同一次写的 seal 即清锁 - * (prot_set.c),V0.1 如实登记,P4 定值持久化时收口。 */ + * eval 自动复位全元件)。B7 收口:104 定值写改走 + * prot_set_set_sealed(影子区写+封存后单字交换),selfcheck 另有 + * seqlock 式复核(提交撞见 = 本轮跳过),误锁窗口已消除。 */ (void)prot_set_selfcheck(); - prot_elm_run(now); + /* 全局软压板(PS_GLOBAL_EN,104 YK12 两段式投退,B7):退出 = + * 保护停止判据并复位元件状态(重新投入从干净快照起步) */ + if (prot_set_get_ctl(PS_GLOBAL_EN) == 0u) { + if (s_prev_plate_out == 0u) { + DBG_INFO("global plate OUT -> protection disabled"); + s_prev_plate_out = 1u; + } + prot_elm_init(); + } else { + if (s_prev_plate_out != 0u) { + DBG_INFO("global plate IN -> protection enabled"); + s_prev_plate_out = 0u; + } + prot_elm_run(now); + } } s_log_active_edge(prot_elm_active_mask()); diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index 7b575f6f..3b1975b6 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -887,6 +887,11 @@ 1 ..\App\cfg\cfg_file_store_mcu.c + + prot_set_store.c + 1 + ..\App\cfg\prot_set_store.c + sys_log_mcu.c 1 diff --git a/docs/任务规划/W1-采集保护收尾.md b/docs/任务规划/W1-采集保护收尾.md index ed70a73d..0df64fa8 100644 --- a/docs/任务规划/W1-采集保护收尾.md +++ b/docs/任务规划/W1-采集保护收尾.md @@ -52,9 +52,9 @@ graph LR | 序 | 任务 | DoD(可验证验收判据) | 硬依赖 | 资源 | 写范围 | 状态 | 出处 | |---|---|---|---|---|---|---|---| -| **B6** | **台架 `@test` 闭环复测(B4/B5 验收窗口)** | `@test` PASS:`protect task start` 锚点 ×1;`[STACK] protect`/`iec104` 水位行在且 free 达标;新堆 min_ever 记录、无 `HEAP_LOW_WATER_B` 告警;PC 客户端 2404 总召/变位往返日志;采样闭锁不误报 | — | 🔴 | `.serialtest/config.json`(无新固件代码) | ⬜ | 保护规划 §7 变更记录"遗留";`../测试说明/自动化测试流程.md` | +| **B6** | **台架 `@test` 闭环复测(B4/B5 验收窗口)** | `@test` PASS:`protect task start` 锚点 ×1;`[STACK] protect`/`iec104` 水位行在且 free 达标;新堆 min_ever 记录、无 `HEAP_LOW_WATER_B` 告警;PC 客户端 2404 总召/变位往返日志;采样闭锁不误报 | — | 🔴 | `.serialtest/config.json`(无新固件代码) | 🟦 | 保护规划 §7 变更记录"遗留";`../测试说明/自动化测试流程.md` | | **N-33** | **栈余量收口**(defaultTask 余 1080 B < 30% 线 / timeSyncTask 余 784 B < ISR 底线,2026-10-12 实测;**既有债非 B4/B5 引入**;链 = dhara GC→控制台回显尾→`(Cycle)`) | `stack_report.py` 9 任务 0 FAIL + `@build` 0/0;若抬栈:台账 §6 登记 + `@test` 复测 PASS | B6(min_ever 背书,AGENTS 抬栈门禁) | 🟢(瘦链调研)+ 🔴(复测腿) | `Src/freertos.c`(USER CODE)/ 瘦链目标文件 | ⬜ | 2026-10-12 `stack_report.py` 实测 | -| **B7** | **定值持久化 + 遥控软压板(P4)** | 主机单测:定值存/取往返、损坏→all-or-nothing 拒绝、YK12 两段式转真执行;`@build` 0/0;**台架腿**:104 C_SE 写 → 断电重启 → 定值保持;写+seal 原子化(B5 竞态窗口关闭,回填保护规划 §3.5) | —(代码);验证腿 🔴 | 🟢 + 🔴 | `App/algo/prot_set.*`、`App/cfg/`、`App/protocol/iec104/iec104_pts.c`(定值段)、`App/task/iec104_serv.c` | ⬜ | 保护规划 §3.5 L5-1、§5;`../子系统设计/IEC104上行设计说明.md` §3.3 | +| **B7** | **定值持久化 + 遥控软压板(P4)** | 主机单测:定值存/取往返、损坏→all-or-nothing 拒绝、YK12 两段式转真执行;`@build` 0/0;**台架腿**:104 C_SE 写 → 断电重启 → 定值保持;写+seal 原子化(B5 竞态窗口关闭,回填保护规划 §3.5) | —(代码);验证腿 🔴 | 🟢 + 🔴 | `App/algo/prot_set.*`、`App/cfg/`、`App/protocol/iec104/iec104_pts.c`(定值段)、`App/task/iec104_serv.c` | 🟦 代码腿完成(2026-10-12:prot_set 63 + iec104 94 断言全绿、`@build` 0/0;`/prot_set.bin` 原子持久化 + YK12 转真 + 竞态收口已落地);**台架腿待窗口 🔴**(与 B6 同窗跑:C_SE 写→断电重启→定值保持 + YK12→YX 联动) | 保护规划 §3.5 L5-1、§5;`../子系统设计/IEC104上行设计说明.md` §3.3 | | **B8** | **频率测量算法 + 过频/低频保护(P2 剩余)** | 频率测量主机单测(45~55 Hz 扫描,精度判据写入文档)+ OF/UF 实例单测;`@build` 0/0;心跳行显示频率值。OV/UV/3U0 实例 B5 起已在跑,注入判据归 B10 | — | 🟢 | `App/algo/phasor.*`(或新 `freq.*`)、`App/algo/prot_elm.*`、`App/algo/prot_set.*`(定值行) | ⬜ | 保护规划 §3.3 L3-5、保护规划 §7 P2 | | **B9** | **动作记录 + 时标 SOE(L4)** | SOE 记录编解码主机单测 + iec104 单测(M_SP_TB_1);`@build` 0/0;protectTask 静态链不变(动作路径无 NAND I/O,记录走队列异步);**台架腿**:动作边沿 → `/log` 记录可见。出口跳闸仍否定确认(R7 口径不变) | —(代码);验证腿 🔴 | 🟢 + 🔴 | `App/log/`、`App/protocol/iec104/iec104_asdu.*` + `iec104_pts.c`(YX 时标段)、`App/task/protect_task.c` | ⬜ | 保护规划 §3.4 L4、保护规划 §7 P4;`../子系统设计/IEC104上行设计说明.md` §3.2 注 | | **B10** | **标准源注入 + 第三方 104 主站往返(送检窗口)** | 标准源注入:L1-5 标定复核 / L1-7 等周波 / P1 过流·过负荷 / P2 电压 + 频率类判据全通过;第三方主站总召/周期/变位/遥控全流程(`../子系统设计/IEC104上行设计说明.md` §7/§8 核对单);证据归档 | M1 + M2 | 🟡 | —(窗口内缺陷修复走 Expedite 类) | ⬜ | 保护规划 §7 P1/P2 验收;`../子系统设计/IEC104上行设计说明.md` §7/§8 | diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 80bf930a..7c7d57ed 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -36,10 +36,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| 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% | +| ER_IROM1 | 223,544 B (218.30 KB) | 1,048,576 B (1024 KB) | 21.3% | +| RW_IRAM1 | 96,096 B (93.84 KB) | 131,072 B (128 KB) | 73.3% | | 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%** | +| **Flash (ROM 合计)** | **224,580 B (219.3 KB)** | 1,048,576 B (1024 KB) | **21.4%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index aa00c622..3a06ba13 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,10 +15,10 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 222,376 B | 1,048,576 B | 21.2% | 参考 | +| ER_IROM1 (Flash) | 223,544 B | 1,048,576 B | 21.3% | 参考 | | 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_IRAM1 | 96,096 B | 131,072 B | 73.3% | PASS | | 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 | |---|---|---|---| @@ -26,7 +26,7 @@ | 存储 | 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 | +| 应用 | 13,119 B | 8 KB | ⚠ 超 4.8 KB | | 其它 | 7,596 B | — | 参考 | diff --git a/docs/子系统设计/IEC104上行设计说明.md b/docs/子系统设计/IEC104上行设计说明.md index 8c0d995f..bb646a0b 100644 --- a/docs/子系统设计/IEC104上行设计说明.md +++ b/docs/子系统设计/IEC104上行设计说明.md @@ -74,7 +74,7 @@ |---|---|---| | 0x6000~0x6003 | YK1~4 开关 | **否定确认**(无出口硬件,`docs/算法/继电保护功能复现规划.md` R7) | | 0x600A | YK11 信号复归 | ✅ 执行段 → `prot_elm_request_reset()`(B5 起跨上下文安全:请求由 protectTask 下一拍 eval 头部消费,不当拍直接复位) | -| 0x600B | YK12 遥控软压板 | 否定确认(无全局压板项,随 P4 定值集成) | +| 0x600B | YK12 遥控软压板 | ✅ 两段式转真(B7):选择段校验 DPC(ON/OFF,非法即 PN);执行段 → `prot_set_set_sealed(PS_GLOBAL_EN)` 原子写+封存;protectTask 下一拍读到退出态即复位元件并停判据(`App/task/protect_task.c`) | | 0x600C | YK13 电池活化 | 否定确认(无对应硬件) | | 0x600D | YK14 解除闭锁 | 否定确认(闭锁源于定值校验和,只能重写定值解除) | @@ -82,9 +82,16 @@ - 地址 = `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 闭环:主站改错值 → 自检闭锁链自动生效)。 + **点表不另立地址表**,prot_set 即单一事实源。`comm_addr = 0` 的本地项(投退/返回系数/ + 全局软压板)不可经 C_RD/C_SE 寻址(B7 起显式排除,防 IOA=0 误命中首个本地项)。 +- 写两段式:选择 = 范围校验;执行 = `prot_set_set_sealed`(**原子写+封存**:影子区 + 写好后单字交换活跃区,protectTask 的 L5-1 巡检任何时刻都看到自洽快照, + B5 登记的 µs 级竞态窗口就此收口)。 +- 持久化(B7):执行段置 prot_set 脏标志 → iec104Task 每轮 poll `dirty_peek` 投递 + `SYS_EVT_PROT_SET_SAVE` → **defaultTask(配置引擎属主)** 经 `App/cfg/prot_set_store.c` + 原子写 `/prot_set.bin`(blob = magic+版本+计数+值区+sum32 正/反码对); + 开机在 STORAGE_READY 屏障前装载,损坏/越界/版本换代 = 整表拒绝保持出厂默认 + (all-or-nothing)+ 自检留痕 `prot set corrupt`;写盘失败脏标志保留 = 下轮重试。 ## 4. 链路与流程 @@ -143,7 +150,7 @@ | 层 | 内容 | |---|---| -| L0.5 | `test/host/iec104/`:APCI 重组/坏帧、STARTDT 握手、总召全流程(含 RBT/分片/预算)、COV、k/w 窗、t1/t2/t3(假时基)、遥控两段式与 PN、定值两段写 + seal 后 selfcheck、C_RD、C_CS(含闰年)、错地址/错类型 EVI | +| L0.5 | `test/host/iec104/`:APCI 重组/坏帧、STARTDT 握手、总召全流程(含 RBT/分片/预算)、COV、k/w 窗、t1/t2/t3(假时基)、遥控两段式与 PN、YK12 软压板两段转真 + 置脏(B7)、定值两段写 + set_sealed 后 selfcheck、C_RD(含本地项不可寻址)、C_CS(含闰年)、错地址/错类型 EVI;`test/host/prot_set/`:blob 往返 + 损坏整表拒绝(all-or-nothing)| | L0 | `iec104-unit-test` 钩子 + cppcheck/coding-standard | | 台架 | `@test` 锚点(104 服务启动/监听行);主站工具(lib60870 或等效)总召+遥测+遥控往返,随 B4 收尾 | @@ -151,7 +158,7 @@ 1. 固有参数 8001H~800AH 未实现(字符串类无法短浮点上送)→ 未知 IOA 应答;随送检需求再设计。 2. SOE 带时标(M_SP_TB_1)随 L4-4 SOE 记录落地;现突发只发 COT=3。 -3. YK1~4/12/13/14 否定确认——出口硬件确认后(R7)转真执行。 +3. YK1~4/13/14 否定确认——出口硬件确认后(R7)转真执行(YK12 已于 B7 转真)。 4. 遥测死区 10.0 码值为拟定值,L1-5 标定后按工程量死区(点表 8020H 段)复核。 5. 多主站/双链路冗余不做(单连接语义)。 6. 主站侧验证工具与送检环境未就绪——台架只做到"第三方 104 主站可总召"级。 @@ -166,3 +173,4 @@ |---|---| | 2026-10-12 | 建档(B4):范围/分层/点表/链路流程/接入决策/预算/测试方案;§5 接线细节随实现回填 | | 2026-10-12 | B4 落地回填:承载改**专用 iec104Task**(netTask 承载会 RPC 自死锁);§5 全量回填(门控/代次/拆链/对时/看门狗位 7);抢占策略修正为**首到优先**(§8 第 7 条);§6 实测(640 words,链 1456 B,余 43% PASS) | +| 2026-10-12 | **B7 定值持久化 + 软压板**:YK12 转真(§3.3,两段式 → `PS_GLOBAL_EN`);定值执行段改 `prot_set_set_sealed` 原子写+封存(§3.4,收口 B5 竞态窗口);本地项 `comm_addr=0` 不可寻址;持久化 = 脏标志 → `SYS_EVT_PROT_SET_SAVE` → defaultTask 原子写 `/prot_set.bin`,开机屏障前整表原子装载(损坏拒绝保默认 + 自检留痕);单测 +7(iec104 94 断言)+ prot_set blob 往返/损坏 33 断言 | diff --git a/docs/算法/继电保护功能复现规划.md b/docs/算法/继电保护功能复现规划.md index e0b66781..a29dce9a 100644 --- a/docs/算法/继电保护功能复现规划.md +++ b/docs/算法/继电保护功能复现规划.md @@ -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/任务规划/README.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 项主机单测)**;**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)。 +> **状态**:**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)上送;**B7 定值持久化 + 全局软压板 + 竞态收口完成(2026-10-12,代码腿;台架腿待窗口)**;**出口记录(L4)与标准源注入验收为下一步**(见 §7)。 --- @@ -320,7 +320,7 @@ | # | 功能点 | 说明 | 状态 | | --- | --- | --- | --- | -| L5-1 | 定值校验和 | `tagPRS_XXX` 的 `dwTagSum` 自检,定值非法闭锁 | 🔶 2026-10-11:`prot_set.c` 已实现 sum32 正/反码封存 + selfcheck 失配置闭锁(参考 `dwTagSum/dwTagSumN` 模式);元件级 tagPRS 常量表校验随板端接线并入 | +| L5-1 | 定值校验和 | `tagPRS_XXX` 的 `dwTagSum` 自检,定值非法闭锁 | 🔶 2026-10-11:`prot_set.c` 已实现 sum32 正/反码封存 + selfcheck 失配置闭锁(参考 `dwTagSum/dwTagSumN` 模式);**B7(2026-10-12)竞态收口**:104 定值写改 `prot_set_set_sealed`(影子区写+封存后单字交换)+ selfcheck seqlock 式复核(提交撞见 = 本轮跳过),B5 登记的 µs 级误锁窗口消除;定值持久化 `/prot_set.bin`(blob 带 sum32 正/反码对,整表原子装载);元件级 tagPRS 常量表校验仍待补 | | L5-2 | 采样通道自检 | 通道码值越界/静默检出 | 🔶 静默/丢帧/序号倒退/错误增量已由 L1-6 `adc_health` 检出(2026-10-11);码值越界待做 | | L5-3 | 硬件自检 | 出口回路、电源、时钟 | ⬜ | | 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)不构成"当前测量不可信"→ 不闭锁,如实登记 | @@ -469,7 +469,7 @@ | **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 显示闭环 | +| **P4** | 出口、记录与定值集成 | L4 出口与记录、§5 定值模型落 cfg_file、UI 显示、上送 | P1 | 动作出口 + SOE 记录 + UI 显示闭环(**定值持久化 + 软压板 + 竞态收口 = B7 已完成 2026-10-12**,台架腿待窗口 🔴) | | **P5** | 方向/功率类(延后) | L3-13 方向元件等 | V0.2 硬件 / 子板计量 | 跨转换器相位一致后实现 | > ⚠️ **V0.1 硬件裁剪**(2026-10-10,详见 §1.6):V0.1 上只有 TPAFE 三路(`UA`/`UN`/`IA`)测量有效。故 **P0 骨架全做**(与通道无关),**P1/P2 只做有效通道子集**——A 相过流、A 相过负荷、过压/欠压、零序过压、过频/低频;**三相过流(B/C 相)、零序过流、方向/功率类、PT/CT 断线**依赖 V0.2 硬件(N-18 修复 + 中点偏置),本期**建接口不使能**。 @@ -606,3 +606,4 @@ B0 产品定义 ✅(2026-10-10 已确认,见 §1.5) | 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 里程碑更新 | +| 2026-10-12 | **B7 定值持久化 + 全局软压板 + 竞态收口(W1 任务卡)** | **定值模型增项** `PS_GLOBAL_EN`(全局软压板,枚举尾追加,编号不回收);**原子写**:`prot_set_set_sealed()` 影子区拷贝→改→重封→**单字交换活跃区**(双缓冲 +124 B 静态),`prot_set_selfcheck()` 改 seqlock 式复核(封存和两读不一致 = 提交撞见 → 本轮跳过)——B5 登记的"104 写 vs 巡检"µs 级误锁窗口就此消除(§3.5 L5-1 注收口)。**持久化**:新增 `App/cfg/prot_set_store.c/h`(双端注册 + 模拟器豁免点名)——blob = magic`PSET`+版本+计数+值区+sum32 正/反码对(144 B),`/prot_set.bin` 走 `cfg_store_write_atomic`(临时文件+rename,掉电安全);**写路径** = C_SE/YK12 执行段置脏 → iec104Task `dirty_peek` 投递 `SYS_EVT_PROT_SET_SAVE`(事件位 bit7)→ defaultTask(配置引擎属主)落盘,写失败脏回置 = 下轮重试;**装载** = storage_bring_up 在 STORAGE_READY 前整表原子 parse(损坏/越界/版本换代 → 拒绝保出厂默认 + 自检留痕 `prot set corrupt`;文件缺失 = 首启正常静默)。**YK12 转真**:`iec104_pts.c` 两段式(选择段校验 DPC、执行段 set_sealed),protectTask 下一拍读退出态 → 复位元件 + 停判据(`global plate OUT/IN` 边沿日志);本地定值项 `comm_addr=0` 显式不可寻址(防 IOA=0 误命中)。单测:prot_set 63 断言(+33:软压板/原子写/脏握手/blob 往返/坏 magic/截断/坏版本/**伪造自洽越界**对抗样本)、iec104 94 断言(+7:YK12 两段转真/置脏/本地项 C_RD EVI)。`@build` 0 错 0 警(Code=205500 +1152 B,ZI +280 = 双缓冲+blob 缓冲,`ram_baseline` 已刷)。台架腿(104 C_SE 写→断电重启→定值保持 + YK12 联动 YX)待窗口 🔴 | diff --git a/scripts/check_registration.py b/scripts/check_registration.py index bf0afbf1..97d8e33b 100644 --- a/scripts/check_registration.py +++ b/scripts/check_registration.py @@ -81,6 +81,7 @@ SIM_EXEMPT = [ ("App/algo/phasor.c", "L2 相量纯算法,模拟器无消费路径"), ("App/algo/prot_elm.c", "L3 保护元件纯算法,模拟器无消费路径"), ("App/algo/prot_set.c", "L5 保护定值纯算法,模拟器无消费路径"), + ("App/cfg/prot_set_store.c", "定值持久化 MCU 胶水(依赖 prot_set 纯算法),模拟器无消费路径"), ("App/algo/sample_ring.c", "L2 环形缓冲纯算法,模拟器无消费路径"), ("App/app/app_main.c", "固件启动/就绪屏障,MCU 专属"), ("App/fs/vol_lock.c", "FreeRTOS 互斥量卷锁,MCU 专属"), diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index f8342b91..0fff2549 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -1,2 +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 +2026-10-12 205500 18044 1036 152484 223544 21.3 96096 73.3 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 diff --git a/test/host/iec104/iec104_test.c b/test/host/iec104/iec104_test.c index d7cb37e6..504f8f58 100644 --- a/test/host/iec104/iec104_test.c +++ b/test/host/iec104/iec104_test.c @@ -84,6 +84,10 @@ 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; + if (ioa == 0x600Bu) { + /* YK12 全局软压板(B7):转真实板端源(prot_set_set_sealed 原子路径) */ + return s_src_yk_exec(ioa, dpc, sel); + } return (ioa == 0x600Au) ? 0 : -1; } static int f_set_read(uint32_t addr, float *out) @@ -610,6 +614,48 @@ static void tc_dc(void) "TC-013 无出口硬件 YK 选择段 PN"); } +/* TC-024(B7):YK12 全局软压板两段式转真(经真实板端源 + 原子封存) */ +static void tc_dc_yk12(void) +{ + uint32_t now = 1000u; + uint8_t a[16]; + uint8_t body[1]; + uint16_t n; + cap_t c; + + reset_all(now); + handshake(now); + CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-024 软压板默认投入"); + body[0] = 0x81u; /* S=1, DPC=OFF:选择段 */ + n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u); + feed_i(a, n, now); + CHECK((cap_i(0, &c) == 0) && (c.cot == IEC104_COT_ACTCON) + && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), "TC-024 YK12 选择段 actcon"); + CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-024 选择段不动作"); + body[0] = 0x01u; /* 执行 OFF */ + n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u); + feed_i(a, n, now); + CHECK((cap_i(1, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), + "TC-024 YK12 执行段 actcon"); + CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-024 执行段软压板转真退出"); + CHECK(prot_set_selfcheck() == 0, "TC-024 set_sealed 原子封存 → selfcheck 通过"); + CHECK(prot_set_dirty_peek() == 1u, "TC-024 置脏待持久化"); + body[0] = 0x82u; /* 再投入:选择 ON */ + n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u); + feed_i(a, n, now); + body[0] = 0x02u; /* 执行 ON */ + n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u); + feed_i(a, n, now); + CHECK((cap_i(3, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) == 0u), + "TC-024 投入执行 actcon"); + CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-024 软压板转真投入"); + body[0] = 0x80u; /* DPC=00 非法:选择段即 PN */ + n = mk_asdu(a, IEC104_T_C_DC_NA_1, 1u, IEC104_COT_ACT, 0u, 1u, 0x600Bu, body, 1u); + feed_i(a, n, now); + CHECK((cap_i(4, &c) == 0) && ((c.cot_hi & IEC104_COT_BIT_PN) != 0u), + "TC-024 非法 DPC 选择段 PN"); +} + static void tc_se(void) { uint32_t now = 1000u; @@ -635,6 +681,7 @@ static void tc_se(void) "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 通过"); + CHECK(prot_set_dirty_peek() == 1u, "TC-014 执行段置脏(B7 待落盘)"); 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); @@ -666,6 +713,10 @@ static void tc_rd(void) CHECK((cap_i(2, &c) == 0) && (c.type == 102u) && (c.cot == IEC104_COT_UNK_IOA) && ((c.oa & 0x80u) != 0u), "TC-015 未知 IOA COT14+EVI"); + n = mk_asdu(a, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_REQ, 0u, 1u, 0u, NULL, 0u); + feed_i(a, n, now); + CHECK((cap_i(3, &c) == 0) && (c.cot == IEC104_COT_UNK_IOA), + "TC-015 本地定值项(comm_addr=0)不可寻址(B7)"); } static void tc_cs(void) @@ -852,6 +903,7 @@ int main(void) tc_t2(); tc_t3_t1(); tc_dc(); + tc_dc_yk12(); tc_se(); tc_rd(); tc_cs(); diff --git a/test/host/prot_set/prot_set_test.c b/test/host/prot_set/prot_set_test.c index f2c8cbe4..c40907ec 100644 --- a/test/host/prot_set/prot_set_test.c +++ b/test/host/prot_set/prot_set_test.c @@ -14,6 +14,12 @@ * TC-PS-005 Q10 往返精度(±1 LSB) * TC-PS-006 元数据表自检:min<=max、投退项 0..1、非零通讯地址不重复 * TC-PS-007 越界编号守卫(get/set) + * TC-PS-008 全局软压板 PS_GLOBAL_EN(B7):默认投入、set_sealed 原子写 + * 不破坏 selfcheck、脏标志三件套(peek/take/mark) + * TC-PS-009 持久化 blob 往返:serialize → init 清场 → parse 恢复 + * (位精确 + 封存自洽 + parse 不置脏) + * TC-PS-010 损坏 blob 整表拒绝(all-or-nothing):值区位翻转/坏 magic/ + * 截断/超长/坏版本/伪造自洽越界 → 全部拒绝且活跃值区一字不动 * * 通过:全部 TC PASS(main 返回 0)。 * 创建日期:2026-10-11 @@ -138,6 +144,100 @@ static void tc_id_guard(void) { prot_set_init(); CHECK(prot_set_get(PS_ID_COUNT) == 0, "TC-PS-007 get out-of-range id -> 0"); CHECK(prot_set_set(PS_ID_COUNT, 0) == -1, "TC-PS-007 set out-of-range id -> -1"); + CHECK(prot_set_set_sealed(PS_ID_COUNT, 0) == -1, "TC-PS-007 set_sealed id guard"); +} + +/* TC-PS-008(B7:全局软压板 + 原子写 + 脏标志) */ +static void tc_global_plate_dirty(void) { + prot_set_init(); + CHECK(prot_set_dirty_peek() == 0u, "TC-PS-008 init not dirty"); + CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 1u, "TC-PS-008 global plate default IN"); + CHECK(prot_set_set(PS_GLOBAL_EN, PROT_SET_Q10(2)) == -1, "TC-PS-008 plate rejects 2"); + CHECK(prot_set_set_sealed(PS_GLOBAL_EN, 0) == 0, "TC-PS-008 plate OUT via set_sealed"); + CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-PS-008 plate OUT readable"); + CHECK(prot_set_selfcheck() == 0, "TC-PS-008 set_sealed leaves selfcheck PASS (no unsealed window)"); + CHECK(prot_set_locked() == 0u, "TC-PS-008 set_sealed no lock"); + CHECK(prot_set_dirty_peek() == 1u, "TC-PS-008 set_sealed marks dirty"); + CHECK(prot_set_dirty_take() == 1u, "TC-PS-008 dirty take consumes"); + CHECK(prot_set_dirty_peek() == 0u, "TC-PS-008 dirty cleared after take"); + prot_set_dirty_mark(); + CHECK(prot_set_dirty_peek() == 1u, "TC-PS-008 dirty mark re-arms (write-fail retry)"); + CHECK(prot_set_set_sealed(PS_GLOBAL_EN, PROT_SET_Q10(2)) == -1, "TC-PS-008 set_sealed range check"); + CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-PS-008 rejected set_sealed keeps value"); +} + +/* TC-PS-009(B7:blob 往返) */ +static void tc_store_roundtrip(void) { + uint8_t blob[PROT_SET_BLOB_SZ]; + uint32_t len = 0u; + int32_t cur; + + prot_set_init(); + (void)prot_set_set_sealed(PS_OC1_CUR, PROT_SET_Q10(12.5)); + (void)prot_set_set_sealed(PS_GLOBAL_EN, 0); + (void)prot_set_dirty_take(); + cur = prot_set_get(PS_OC1_CUR); + CHECK(prot_set_store_serialize(blob, sizeof(blob), &len) == 0, "TC-PS-009 serialize ok"); + CHECK(len == (uint32_t)PROT_SET_BLOB_SZ, "TC-PS-009 blob exact size"); + prot_set_init(); + CHECK(prot_set_get(PS_OC1_CUR) != cur, "TC-PS-009 init wiped to defaults"); + CHECK(prot_set_store_parse(blob, len) == 0, "TC-PS-009 parse ok"); + CHECK(prot_set_get(PS_OC1_CUR) == cur, "TC-PS-009 value round-trip bit-exact"); + CHECK(prot_set_get_ctl(PS_GLOBAL_EN) == 0u, "TC-PS-009 plate state round-trip"); + CHECK(prot_set_selfcheck() == 0, "TC-PS-009 parsed table sealed"); + CHECK(prot_set_dirty_peek() == 0u, "TC-PS-009 parse does not mark dirty"); +} + +/* 伪造辅助:按 blob 值区重算尾部 sum/sumn(构造"自洽但越界"的对抗样本) */ +static void s_forge_tail(uint8_t *b, uint32_t len) { + uint32_t sum = 0u; + uint32_t inv; + uint32_t i; + + for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) { + sum += s_get32(b + PROT_SET_BLOB_HDR_SZ + 4u * i); + } + inv = ~sum; + s_put32(b + len - 8u, sum); + s_put32(b + len - 4u, inv); +} + +/* TC-PS-010(B7:损坏 blob 整表拒绝,all-or-nothing) */ +static void tc_store_corrupt(void) { + uint8_t blob[PROT_SET_BLOB_SZ]; + uint32_t len = 0u; + int32_t keep; + + prot_set_init(); + (void)prot_set_set_sealed(PS_OC2_CUR, PROT_SET_Q10(6.0)); + keep = prot_set_get(PS_OC2_CUR); + CHECK(prot_set_store_serialize(blob, sizeof(blob), &len) == 0, "TC-PS-010 serialize ok"); + + blob[PROT_SET_BLOB_HDR_SZ] ^= 0x01u; /* 值区一位翻转:sum 对不符 */ + CHECK(prot_set_store_parse(blob, len) == -1, "TC-PS-010 flipped value byte rejected (sum pair)"); + CHECK(prot_set_get(PS_OC2_CUR) == keep, "TC-PS-010 reject leaves values untouched"); + + (void)prot_set_store_serialize(blob, sizeof(blob), &len); + blob[0] ^= 0xFFu; /* magic 破坏 */ + CHECK(prot_set_store_parse(blob, len) == -1, "TC-PS-010 bad magic rejected"); + + (void)prot_set_store_serialize(blob, sizeof(blob), &len); + CHECK(prot_set_store_parse(blob, len - 4u) == -1, "TC-PS-010 truncated blob rejected"); + CHECK(prot_set_store_parse(blob, len + 4u) == -1, "TC-PS-010 oversized blob rejected"); + + (void)prot_set_store_serialize(blob, sizeof(blob), &len); + blob[5] ^= 0x02u; /* version 高字节:1 -> 0x200 */ + CHECK(prot_set_store_parse(blob, len) == -1, "TC-PS-010 version mismatch rejected"); + + /* 对抗样本:越限定值 + 重算 sum 对(校验全过、范围必拦) */ + (void)prot_set_store_serialize(blob, sizeof(blob), &len); + s_put32(blob + PROT_SET_BLOB_HDR_SZ + 4u * (uint32_t)PS_OC1_CUR, + (uint32_t)(g_prot_set_meta[PS_OC1_CUR].max + 1)); + s_forge_tail(blob, len); + CHECK(prot_set_store_parse(blob, len) == -1, "TC-PS-010 forged consistent out-of-range rejected"); + CHECK(prot_set_bad_mask() == (1uL << PS_OC1_CUR), "TC-PS-010 bad_mask points at item"); + CHECK(prot_set_get(PS_OC1_CUR) == PROT_SET_Q10(10.0), "TC-PS-010 defaults still intact"); + CHECK(prot_set_selfcheck() == 0, "TC-PS-010 active table still self-consistent after all rejects"); } int main(void) { @@ -149,6 +249,9 @@ int main(void) { tc_q10_roundtrip(); tc_meta_sanity(); tc_id_guard(); + tc_global_plate_dirty(); + tc_store_roundtrip(); + tc_store_corrupt(); printf("\n=== Prot Set Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed);