feat(protect): B7 定值持久化 + 全局软压板 + L5-1 竞态收口(W1 代码腿完成)

- 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 / 零乱码
This commit is contained in:
2026-10-12 15:01:49 +08:00
parent d3107f374f
commit 1fe163ba7b
22 changed files with 655 additions and 71 deletions
+8 -5
View File
@@ -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()` 是解锁的唯一合法路径 |
**要点**
+228 -19
View File
@@ -9,6 +9,8 @@
*/
#include "prot_set.h"
#include <string.h> /* 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;
}
+50 -5
View File
@@ -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 */
+3
View File
@@ -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
}
+3 -1
View File
@@ -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` 替换,避免掉电损坏
+64
View File
@@ -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 <stddef.h>
#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;
}
+31
View File
@@ -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 */
+24 -13
View File
@@ -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; /* 未知地址 */
}
+7 -4
View File
@@ -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 服务任务上下文。
+20 -1
View File
@@ -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);
+7
View File
@@ -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);
}
}
+22 -5
View File
@@ -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());