diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e90158eb..e9c65cfd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -396,6 +396,16 @@ repos: files: ^(App/algo/meas\.(c|h)|App/algo/(phasor|sample_ring)\.(c|h)|App/task/adc_task\.c|test/host/meas/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false + # 7h5) 运行参数段(「数据可信」卡 10,道D)主机侧单元测试(2026-10-25 加) + # 死区把守突发上报的量、104 整定段是现场唯一远程写入口:注册契约、 + # 分档量程守卫、两段式、系数直通的合法带与生效值回显必须离线钉死。 + - id: run-par-unit-test + name: 运行参数段单测(主机侧) + entry: python test/host/run_par/run_unit_test.py + language: system + files: ^(App/algo/run_par\.(c|h)|App/algo/adc_cal\.(c|h)|App/cfg/cfg_file(_store)?\.(c|h)|test/host/run_par/.*|test/host/common/unit_test_harness\.py)$ + pass_filenames: false + # 7i) 保护定值模型(P1 / L5-1 载体)主机侧单元测试(2026-10-11 加) # 定值是保护动作的"输入宪法":范围校验、整表装载 all-or-nothing、 @@ -463,7 +473,8 @@ repos: 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|freq)\.(c|h)|test/host/iec104/.*|test/host/common/unit_test_harness\.py)$ + # 卡 10 起本 TU 链入真实 cfg_file/adc_cal/run_par(板端真源链),触发面同步扩 + files: ^(App/protocol/iec104/iec104_(asdu|pts|link)\.(c|h)|App/algo/(prot_set|prot_elm|phasor|sample_ring|adc_input|freq|meas|adc_cal|run_par)\.(c|h)|App/cfg/cfg_file(_store)?\.(c|h)|test/host/iec104/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false # 7m2) SOE 动作记录编解码主机侧单元测试(2026-10-12 加,B9) diff --git a/App/algo/adc_cal.c b/App/algo/adc_cal.c index aa430667..b3e7fce2 100644 --- a/App/algo/adc_cal.c +++ b/App/algo/adc_cal.c @@ -18,6 +18,9 @@ * 创建日期:2026-10-25(道A 卡 2/卡 4) * 修改记录: * v1.0 2026-10-25 DTU 项目组 创建:Q16 系数/零点两段落盘 + 折算 + 三态 flags + * v1.1 2026-10-25 DTU 项目组 卡 10(道D):新增 adc_cal_b_codes() 生效零点 + * 读取口;set() 授权写者扩至 iec104Task 直通路径 + * (run_par 0x8040~/0x8048~,契约同 §4.4) * + 合法带哨兵回退 + 两点标定公式(台架注入待跑) */ @@ -295,11 +298,30 @@ float adc_cal_k_uv(adc_semantic_t sem) { return ((float)k) / 65536.0f; } +/* + * 函数功能:读通道当前生效零点偏移(码值,浮点)——adc_cal_k_uv 孪生入口 + * 入口参数:sem - 语义通道 adc_semantic_t + * 返 回 值:b[码值](越带时 s_eff_kb 已回 0);TEMP/越界返 0 float + * 限定条件:无(任意上下文,纯读) + * 函数说明:卡 10 的 104 b 覆盖回显 + k 单侧改写 RMW 读侧(App/algo/run_par.c) + */ +float adc_cal_b_codes(adc_semantic_t sem) { + int32_t k; + int32_t b; + + if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) { + return 0.0f; + } + (void)s_eff_kb((unsigned)sem, &k, &b); + return ((float)b); +} + /* * 函数功能:整定入口——写通道系数与零点,并置位 VALID(「数据可信」卡 4) * 入口参数:sem - 语义通道;k_q16 - 系数(µV/码×65536);b_codes - 零点(码值) * 返 回 值:0 - 成功;-1 - 参数非法/越出合法带(RAM 不动,[ERR] 留痕) int - * 限定条件:属主上下文(defaultTask 整定/标定链);落盘由属主经 cfg_file_save + * 限定条件:整定路径上下文(defaultTask 整定/标定链;iec104Task 经 run_par + * 直通为卡 10 授权写者,单字原子契约同 §4.4);落盘由属主经 cfg_file_save * 函数说明:见 adc_cal.h 同名声明 */ int adc_cal_set(adc_semantic_t sem, int32_t k_q16, int32_t b_codes) { diff --git a/App/algo/adc_cal.h b/App/algo/adc_cal.h index 4b83978c..207ce373 100644 --- a/App/algo/adc_cal.h +++ b/App/algo/adc_cal.h @@ -23,6 +23,7 @@ * 创建日期:2026-10-25(道A 卡 2/卡 4) * 修改记录: * v1.0 2026-10-25 DTU 项目组 创建:Q16 系数/零点 + 折算/flags/集 + * v1.1 2026-10-25 DTU 项目组 卡 10:+adc_cal_b_codes()(run_par 回显/RMW 用) */ #ifndef ADC_CAL_H @@ -99,13 +100,25 @@ uint32_t adc_cal_flags(adc_semantic_t sem); */ float adc_cal_k_uv(adc_semantic_t sem); +/* + * 函数功能:读通道当前生效零点偏移(码值,浮点)——adc_cal_k_uv 的孪生入口 + * 入口参数:sem - 语义通道 adc_semantic_t + * 返 回 值:b[码值](越带时为 0,与折算同视图);TEMP/越界返 0 float + * 限定条件:无 + * 函数说明:104 运行参数段(「数据可信」卡 10)b 覆盖回显与 k 单侧改写的 RMW + * 读侧用(App/algo/run_par.c);勿据其反推二次值(看 flags)。 + */ +float adc_cal_b_codes(adc_semantic_t sem); + /* * 函数功能:整定入口——写通道系数与零点,并置位 VALID(「数据可信」卡 4) * 入口参数:sem - 语义通道(电气 8 路) adc_semantic_t * k_q16 - 系数,µV/码 × 65536 内 int32_t * b_codes - 零点偏移,码值 内 int32_t * 返 回 值:0 - 成功;-1 - 参数非法/越出合法带(RAM 不动,[ERR] 留痕) int - * 限定条件:**属主上下文**(defaultTask 的标定流程/整定链),勿在 adcTask 调用 + * 限定条件:**整定路径上下文**(defaultTask 标定流程/整定链;卡 10 起 iec104Task + * 经 App/algo/run_par.c 的 0x8040~/0x8048~ 直通写亦为授权写者——写的仍是 + * 引擎对齐 int32 单字,µs 级混窗契约同「数据可信」§4.4)。勿在 adcTask 调用 * 函数说明:只写 RAM 副本;落盘由属主经 cfg_file_save 完成(卡 10 前整定链 * 未接,台架两点标定(卡 12)是首个消费者) */ diff --git a/App/algo/meas.c b/App/algo/meas.c index 1059d315..eaf8e35f 100644 --- a/App/algo/meas.c +++ b/App/algo/meas.c @@ -23,10 +23,10 @@ #include "sample_ring.h" /* sample_ring_window():以本拍为终止的一周波窗 */ /* ===== 双区(计算区 / 使用区),口径同 App/algo/phasor.c 的 L2-7 ===== */ -static meas_snap_t s_snap[2]; -static volatile uint8_t s_snap_use; -static uint8_t s_snap_ready; /* 使用区是否已产出过有效结果 */ -static uint32_t s_snap_seq; /* 只增帧号(写者单写者,读者原子单字读) */ +static meas_snap_t s_ms_snap[2]; +static volatile uint8_t s_ms_use; +static uint8_t s_ms_ready; /* 使用区是否已产出过有效结果 */ +static uint32_t s_ms_seq; /* 只增帧号(写者单写者,读者原子单字读) */ /* * 函数功能:清零一份快照(含 rsv 与全部数组元素) @@ -61,11 +61,11 @@ static void meas_zero(meas_snap_t *p) { * 返 -2(out 清零)、meas_seq() 恒返 0。 */ int meas_init(void) { - meas_zero(&s_snap[0]); - meas_zero(&s_snap[1]); - s_snap_use = 0u; - s_snap_ready = 0u; - s_snap_seq = 0u; + meas_zero(&s_ms_snap[0]); + meas_zero(&s_ms_snap[1]); + s_ms_use = 0u; + s_ms_ready = 0u; + s_ms_seq = 0u; return 0; } @@ -88,14 +88,14 @@ int meas_init(void) { */ void meas_tick(uint32_t tick_ms, uint8_t freq_valid_in, float freq_hz_in, uint32_t clip_mask) { - meas_snap_t *z = &s_snap[1u - s_snap_use]; /* 计算区 = 非使用区 */ + meas_snap_t *z = &s_ms_snap[1u - s_ms_use]; /* 计算区 = 非使用区 */ float win[PHASOR_N]; phasor_t p; uint8_t all_ok = 1u; uint8_t s; meas_zero(z); - z->seq = s_snap_seq + 1u; + z->seq = s_ms_seq + 1u; z->tick_ms = tick_ms; for (s = 0u; s < (uint8_t)ADC_SEM_TEMP; s++) { @@ -130,9 +130,9 @@ void meas_tick(uint32_t tick_ms, uint8_t freq_valid_in, float freq_hz_in, z->freq_hz = freq_hz_in; /* 全部字段就绪后才翻索引 + 置就绪(顺序即协议,勿交换) */ - s_snap_seq = z->seq; - s_snap_use = (uint8_t)(1u - s_snap_use); - s_snap_ready = 1u; + s_ms_seq = z->seq; + s_ms_use = (uint8_t)(1u - s_ms_use); + s_ms_ready = 1u; } /* @@ -149,12 +149,12 @@ int meas_get(meas_snap_t *out) { if (out == NULL) { return -1; } - if (s_snap_ready == 0u) { + if (s_ms_ready == 0u) { meas_zero(out); return -2; } - z = s_snap_use; - *out = s_snap[z]; + z = s_ms_use; + *out = s_ms_snap[z]; return 0; } @@ -166,5 +166,5 @@ int meas_get(meas_snap_t *out) { * 函数说明:轻量"是否换帧"探针,正式取数以 meas_get() 为准。 */ uint32_t meas_seq(void) { - return s_snap_seq; + return s_ms_seq; } diff --git a/App/algo/run_par.c b/App/algo/run_par.c new file mode 100644 index 00000000..de64e3dd --- /dev/null +++ b/App/algo/run_par.c @@ -0,0 +1,289 @@ +/* + * 模块名称:运行参数段(L2/L3 整定层,「数据可信」卡 10)——实现 + * 模块功能:见 run_par.h。工厂默认 = 「数据可信」卡 6 额定 0.5 % 档(电压 500 + * mV / 电流 5 mA / 频率 500 mHz,毫单位 int32 定点落盘);[run_par] + * 段 9 键(db_ua…db_freq,均 ≤15 字符)≤ CFG_FILE_MAX_KEYS 不拆段 + * (通道系数覆盖**不落本段**——直通 adc_cal 的 [adc_cal]/[adc_cal_ofs], + * 系数唯一真源在那两段,lead 裁②a)。 + * 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU) + * 作者:DTU 项目组 + * 创建日期:2026-10-25(道D 卡 10) + * 修改记录: + * v1.0 2026-10-25 DTU 项目组 创建:[run_par] 段 + 104 映射 + 直通 adc_cal + * + B7 式脏标志落盘请求 + */ + +/* 头文件包含区 */ +#include +#include +#include + +#include "run_par.h" + +#include "adc_cal.h" +#include "cfg_file.h" + +#define DBG_TAG "[PAR]" +#include "dbg_log.h" + +/* 常量定义区 */ + +/* cfg 段名(≤ CFG_FILE_MAX_NAME_LEN);键名 6~7 字符小写 */ +#define RUN_PAR_SEC "run_par" + +/* 104 运行参数地址(复现规划 §5.1 类内落实;权威逐项表不在仓,送检点表到位 + * 后以送检为准替换本常量段——lead 裁①。0x8027~0x802A = prot_set 告警段,永不让位) */ +#define RUN_PAR_IOA_DB0 0x8030u /* 9 点:0x8030~0x8038 */ +#define RUN_PAR_IOA_K0 0x8040u /* k 覆盖 8 路:0x8040~0x8047 */ +#define RUN_PAR_IOA_B0 0x8048u /* b 覆盖 8 路:0x8048~0x804F */ + +/* 死区合法域上限(毫单位):电压 50 V / 电流 1 A / 频率 5 Hz——守卫挡误写, + * 不限工况;下限统一 0(负死区 = 配置错误) */ +#define RUN_PAR_DB_MAX_U 50000L +#define RUN_PAR_DB_MAX_I 1000L +#define RUN_PAR_DB_MAX_F 5000L + +/* 静态变量定义区 */ + +/* [run_par] 段 RAM 副本(引擎原地更新;属主契约与 µs 级混窗见「数据可信」§4.4, + * 同 adc_cal——本层不挂回调,副本即生效值,消费侧实时读) */ +typedef struct { + int32_t db[RUN_PAR_DB_N]; +} run_par_db_t; + +static const run_par_db_t s_db_rom = { + { 500L, 500L, 500L, 500L, /* UA/UB/UC/UN:0.5 V(卡 6 额定档) */ + 5L, 5L, 5L, 5L, /* IA/IB/IC/IN:0.005 A */ + 500L } /* 频率:0.5 Hz(绝对档,非额定百分比) */ +}; +static run_par_db_t s_db; + +/* 键表 offset 与 run_par_db_t 字段一一对应(sizeof(int32_t) 步进) */ +static const cfg_key_def_t s_db_keys[] = { + { "db_ua", CFG_VAL_INT, 0u, 4u }, + { "db_ub", CFG_VAL_INT, 4u, 4u }, + { "db_uc", CFG_VAL_INT, 8u, 4u }, + { "db_un", CFG_VAL_INT, 12u, 4u }, + { "db_ia", CFG_VAL_INT, 16u, 4u }, + { "db_ib", CFG_VAL_INT, 20u, 4u }, + { "db_ic", CFG_VAL_INT, 24u, 4u }, + { "db_in", CFG_VAL_INT, 28u, 4u }, + { "db_freq", CFG_VAL_INT, 32u, 4u } +}; + +/* 落盘脏标志:104 写成功置位,run_par_store_save 成功清零(B7 prot_set 同款, + * 只置不清 = 写盘失败天然重试);单字节,跨任务 peek 无害 */ +static uint8_t s_dirty; + +/* 内部函数实现区 */ + +/* + * 函数功能:死区点量程上限(毫单位) + * 入口参数:idx - 0..8 uint8_t + * 返 回 值:上限;idx 越界返 0 长(一切值皆拒) 内 int32_t + */ +static int32_t s_db_cap_milli(uint8_t idx) { + if (idx < 4u) { + return RUN_PAR_DB_MAX_U; + } + if (idx < RUN_PAR_DB_N - 1u) { + return RUN_PAR_DB_MAX_I; + } + if (idx == (uint8_t)(RUN_PAR_DB_N - 1u)) { + return RUN_PAR_DB_MAX_F; + } + return 0L; +} + +/* + * 函数功能:工程量浮点 → 毫单位定点(只舍不入的截断由调用侧范围守卫兜底) + * 入口参数:v - 工程量(须已判有限) float + * 返 回 值:四舍五入最近毫单位 内 int32_t + */ +static int32_t s_milli_of(float v) { + return (int32_t)lrintf(v * 1000.0f); +} + +/* + * 函数功能:死区值校验(不提交),set/104 选择段共用同一守卫 = 两路语义同源 + * 入口参数:idx - 0..8 uint8_t;db - 工程量 float + * 返 回 值:合法毫单位 ≥0;-1 非法(越界/NaN/负/越程) 内 int32_t + */ +static int32_t s_db_check(uint8_t idx, float db) { + int32_t milli; + + if ((idx >= RUN_PAR_DB_N) || !isfinite(db) || !(db >= 0.0f)) { + return -1L; + } + milli = s_milli_of(db); + if ((milli < 0L) || ((long long)milli > (long long)s_db_cap_milli(idx))) { + return -1L; + } + return milli; +} + +/* 公共函数实现区 */ + +/* + * 函数功能:初始化——副本置工厂默认 + 注册 [run_par] 段(幂等,内核前) + * 返 回 值:无 + */ +void run_par_init(void) { + cfg_section_reg_t reg; + + (void)memcpy(&s_db, &s_db_rom, sizeof(s_db)); + s_dirty = 0u; + + (void)memset(®, 0, sizeof(reg)); + reg.name = RUN_PAR_SEC; + reg.p_data = &s_db; + reg.p_defaults = &s_db_rom; + reg.data_size = sizeof(s_db); + reg.p_keys = s_db_keys; + reg.key_count = (uint8_t)(sizeof(s_db_keys) / sizeof(s_db_keys[0])); + reg.p_cb = NULL; /* 不挂回调:RAM 副本即生效值(同 adc_cal) */ + if (cfg_file_register(®) != 0) { + DBG_ERROR("register %s FAIL (dup/full?) - file values inactive", + RUN_PAR_SEC); + } +} + +/* + * 函数功能:读第 idx 路死区(工程量) + * 返 回 值:V / A / Hz;越界返 0.0(消费侧按"无死区"保守处理) float + */ +float run_par_deadband(uint8_t idx) { + if (idx >= RUN_PAR_DB_N) { + return 0.0f; + } + return ((float)s_db.db[idx]) / 1000.0f; /* 毫→工程唯一换算点 */ +} + +/* + * 函数功能:写第 idx 路死区(守卫通过才提交;成功置脏) + * 返 回 值:0 成功;-1 非法(副本不动) int + */ +int run_par_deadband_set(uint8_t idx, float db) { + int32_t milli = s_db_check(idx, db); + + if (milli < 0L) { + return -1; + } + s_db.db[idx] = milli; + s_dirty = 1u; + return 0; +} + +/* + * 函数功能:104 运行参数地址读(命中集精确匹配,见 run_par.h 地址表) + * 返 回 值:0 命中;-1 非本段(调用方落 prot_set) int + */ +int run_par_104_read(uint32_t addr, float *out) { + if (out == NULL) { + return -1; + } + if ((addr >= RUN_PAR_IOA_DB0) && (addr < RUN_PAR_IOA_DB0 + RUN_PAR_DB_N)) { + *out = run_par_deadband((uint8_t)(addr - RUN_PAR_IOA_DB0)); + return 0; + } + if ((addr >= RUN_PAR_IOA_K0) && (addr < RUN_PAR_IOA_K0 + RUN_PAR_CH_N)) { + *out = adc_cal_k_uv((adc_semantic_t)(addr - RUN_PAR_IOA_K0)); + return 0; + } + if ((addr >= RUN_PAR_IOA_B0) && (addr < RUN_PAR_IOA_B0 + RUN_PAR_CH_N)) { + *out = adc_cal_b_codes((adc_semantic_t)(addr - RUN_PAR_IOA_B0)); + return 0; + } + return -1; +} + +/* + * 函数功能:104 运行参数地址写(两段式;k/b 直通 adc_cal_set——**run_par 不存 + * 系数**,系数唯一真源 = [adc_cal]/[adc_cal_ofs],lead 裁②a) + * 返 回 值:0 接受 / -1 拒绝 / -2 非本段(调用方落 prot_set) int + * 函数说明:选择段 = 校验不提交(死区走与 set 同源守卫;k/b 的 adc_cal 合法带 + * 由执行段裁决——IEC104 允许执行段 PN,两段结果可不一致,注释在此 + * 明说防"选择过了怎么还 PN"的现场困惑);k/b 单侧写 RMW 用**当前 + * 生效值**做另一侧(生效值经合法带哨兵,天然满足再写校验)。 + */ +int run_par_104_write(uint32_t addr, float v, uint8_t select) { + uint8_t slot; /* 每个命中分支先赋值再使用(无未init读路径) */ + int32_t k_q16; + int32_t b_codes; + int rc; + + if ((addr >= RUN_PAR_IOA_DB0) && (addr < RUN_PAR_IOA_DB0 + RUN_PAR_DB_N)) { + int32_t milli; + + slot = (uint8_t)(addr - RUN_PAR_IOA_DB0); + milli = s_db_check(slot, v); + if (milli < 0L) { + return -1; + } + if (select == 0u) { + s_db.db[slot] = milli; + s_dirty = 1u; + } + return 0; + } + if ((addr >= RUN_PAR_IOA_K0) && (addr < RUN_PAR_IOA_K0 + RUN_PAR_CH_N)) { + slot = (uint8_t)(addr - RUN_PAR_IOA_K0); + if (!isfinite(v) || !(v > 0.0f) || (v > 1.0e9f)) { + return -1; + } + if (select != 0u) { + return 0; /* 粗守过了 ≠ 执行必过:合法带裁决在执行段(见函数头) */ + } + k_q16 = (int32_t)lrintf(v * 65536.0f); + b_codes = (int32_t)lrintf(adc_cal_b_codes((adc_semantic_t)slot)); + rc = adc_cal_set((adc_semantic_t)slot, k_q16, b_codes); + if (rc == 0) { + s_dirty = 1u; + } + return rc; + } + if ((addr >= RUN_PAR_IOA_B0) && (addr < RUN_PAR_IOA_B0 + RUN_PAR_CH_N)) { + slot = (uint8_t)(addr - RUN_PAR_IOA_B0); + if (!isfinite(v) || (v < -1000000.0f) || (v > 1000000.0f)) { + return -1; /* 粗守 ±1e6 码;±8192 真带由 adc_cal 执行段裁 */ + } + if (select != 0u) { + return 0; + } + b_codes = (int32_t)lrintf(v); + k_q16 = (int32_t)lrintf(adc_cal_k_uv((adc_semantic_t)slot) * 65536.0f); + rc = adc_cal_set((adc_semantic_t)slot, k_q16, b_codes); + if (rc == 0) { + s_dirty = 1u; + } + return rc; + } + return -2; +} + +/* + * 函数功能:落盘脏探查(非消费) + * 返 回 值:1 有未落盘整定;0 干净 uint8_t + */ +uint8_t run_par_dirty_peek(void) { + return s_dirty; +} + +/* + * 函数功能:执行落盘(整档外科保存成功后清脏;失败脏保持 = 下轮重试) + * 返 回 值:0 成功/无脏;-1 写盘失败 int + * 限定条件:defaultTask(配置引擎属主)上下文,SYS_EVT_RUN_PAR_SAVE 驱动 + * 函数说明:一次 save 同时覆盖 [run_par] 9 键与刚被 adc_cal_set 改写的 + * [adc_cal]/[adc_cal_ofs] 副本(lead 裁②:一条事件位一次 save)。 + */ +int run_par_store_save(void) { + if (s_dirty == 0u) { + return 0; + } + if (cfg_file_save() != 0) { + DBG_ERROR("cfg save FAIL - keep dirty, retry next round"); + return -1; + } + s_dirty = 0u; + return 0; +} diff --git a/App/algo/run_par.h b/App/algo/run_par.h new file mode 100644 index 00000000..8a77548b --- /dev/null +++ b/App/algo/run_par.h @@ -0,0 +1,130 @@ +/* + * 模块名称:运行参数段(L2/L3 整定层,「数据可信」卡 10) + * 模块功能:遥测死区(9 点)与通道标定系数覆盖(k/b 各 8 路)的**单一真源**: + * - 死区 = 本模块 int32 毫单位 RAM 副本(注册进 cfg 引擎 [run_par] 段, + * 工厂默认 = 「数据可信」卡 6 额定 0.5 % 档),104 消费侧 + * (App/protocol/iec104/iec104_pts.c)每拍实时读取——无投递、无缓存 + * 副本(卡 9 曾在 pts 侧留运行时副本 s_yc_db,卡 10 落地后收编于此, + * 双源必打架); + * - 系数覆盖 = **直通 adc_cal_set()**([adc_cal]/[adc_cal_ofs] 是系数 + * 唯一真源,卡 4 已定;本模块只加 104 地址映射 + 落盘请求,不另存 + * 一份系数——同 D1 裁"两处判断必打架"逻辑)。 + * 104 地址映射(docs/算法/继电保护功能复现规划.md §5.1 运行参数类 + * 0x8020H~0x8058H;逐项表原文"待补"(参考项目 CommPara_ConfigTable + * 不在仓),本表即卡 10 的落实,送检点表若另给权威地址只改常量段: + * 0x8030~0x8038 = 死区 UA/UB/UC/UN/IA/IB/IC/IN/FREQ(工程量 V/A/Hz); + * 0x8040~0x8047 = k 覆盖(µV/码);0x8048~0x804F = b 覆盖(码值,可负); + * 其余留白(未启用类别),**0x8027~0x802A 属 prot_set 告警段**,本表 + * 永不让位(两表按精确地址分流,见 iec104_pts.c set 路由)。 + * 持久化(B7 prot_set 先例同款):任一写成功 → 脏标志只置不清; + * iec104Task 主循环 peek → 置 SYS_EVT_RUN_PAR_SAVE → defaultTask + * (配置引擎属主)run_par_store_save() 调 cfg_file_save() 整档外科 + * 保存(含 [run_par] 与刚被 adc_cal_set 改写的 [adc_cal]/[adc_cal_ofs] + * 副本)→ 成功清脏;失败留脏 = 下轮天然重试。本模块自身零文件 IO + * (属主纪律)。 + * 并发契约:引擎副本 int32 对齐字读写在 Cortex-M4 上原子;poll 重载 µs 级窗口 + * 为「数据可信」§4.4 明载设计(adc_cal 同款)。写路径 = 104 执行段 + * (iec104Task)与引擎读路径互不加锁,勿改成"读侧缓存"(缓存 = 新真源)。 + * 纯算法:只用 stdint/stddef/math + cfg_file.h/adc_cal.h 头(注册/直通均为 + * 接口级)→ 主机侧可单测(test/host/run_par,链真 cfg 引擎 + 真 adc_cal)。 + * 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU) + * 作者:DTU 项目组 + * 创建日期:2026-10-25(道D 卡 10) + * 修改记录: + * v1.0 2026-10-25 DTU 项目组 创建:[run_par] 段 + 104 地址映射 + 直通 + * adc_cal + B7 式脏标志落盘请求 + */ + +#ifndef RUN_PAR_H +#define RUN_PAR_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* 死区/覆盖点数(死区 = YC 表行数:电气 8 + 频率;覆盖 = 语义电气 8 路) */ +#define RUN_PAR_DB_N 9u +#define RUN_PAR_CH_N 8u + +/* + * 函数功能:运行参数初始化——副本置工厂默认 + 注册 [run_par] 段进 cfg 引擎 + * 入口参数:无 + * 返 回 值:无 + * 限定条件:**内核启动前**调用(app_main_init,adc_cal_init 之后);注册必须先 + * 于 defaultTask 的 cfg_file_load(N-19 契约,同 adc_cal 先例);幂等 + * 函数说明:注册失败(段名重复/段数超限)打 [ERR] 后继续——工厂默认照常生效, + * 仅文件整定失效(adc_cal_init 同款降级)。 + */ +void run_par_init(void); + +/* + * 函数功能:读第 idx 路遥测死区(工程量) + * 入口参数:idx - 0..7 = 语义通道序(UA..IN),8 = 频率 uint8_t + * 返 回 值:死区 V / A / Hz;idx 越界返 0.0(调用方按"无死区"保守处理) float + * 限定条件:任意上下文(int32 单字读原子;104 COV 每拍调用) + * 函数说明:毫单位 → 浮点的换算点(除以 1000 唯一在此,勿在消费侧另写)。 + */ +float run_par_deadband(uint8_t idx); + +/* + * 函数功能:写第 idx 路死区(工程量,含范围守卫 + 脏置位) + * 入口参数:idx - 同上 uint8_t;db - 死区工程量(V/A/Hz,≥0) float + * 返 回 值:0 成功;-1 越界/越程/NaN(副本不动) int + * 限定条件:**iec104Task 上下文**(104 执行段 / 卡 11 后的整定路径;UI 若整定 + * 需另行走配置引擎属主路径,勿跨任务直写) + * 函数说明:合法域按点型分档——电压 0~50 V、电流 0~1 A、频率 0~5 Hz(上限 = + * 额定 × 二档裕量,守卫目的是挡误写不是限工况;见 .c 常量段)。 + * 成功即置脏(落盘请求,见文件头持久化契约)。 + */ +int run_par_deadband_set(uint8_t idx, float db); + +/* + * 函数功能:104 运行参数地址读(C_RD 路由,先于 prot_set 扫描) + * 入口参数:addr - 通讯地址(0x8020~0x8058 段内) uint32_t;out - 工程量浮点 + * (死区 V/A/Hz;k 覆盖 µV/码;b 覆盖 码值) float * + * 返 回 值:0 命中并写出;-1 地址不归本段(调用方继续走 prot_set) int + * 限定条件:iec104Task 上下文 + * 函数说明:命中集 = 0x8030~0x8038 / 0x8040~0x8047 / 0x8048~0x804F 精确匹配; + * k/b 读回 = adc_cal **当前生效值**(与折算同视图:文件坏值被合法带 + * 哨兵回退 ROM 时,回显的就是 ROM——主站看到的即下游实际吃到的)。 + */ +int run_par_104_read(uint32_t addr, float *out); + +/* + * 函数功能:104 运行参数地址写(C_SE 两段式路由,先于 prot_set 扫描) + * 入口参数:addr - 通讯地址 uint32_t;v - 工程量浮点 float; + * select - 1 = 选择段(只校验不落副本)/ 0 = 执行段 uint8_t + * 返 回 值:0 接受;-1 拒绝(越程/NaN/合法带外/非本段可写地址命中守卫); + * -2 地址不归本段(调用方继续走 prot_set) int + * 限定条件:iec104Task 上下文;执行段成功即置脏 + * 函数说明:k/b 覆盖经 adc_cal_set() 合法带哨兵(µV/码 ×[0.5,2.0]、|b|≤8192, + * 越带 -1 → 104 侧 PN 否定确认,adc_cal 不留痕由**其自身**越带 [ERR] + * 机制负责——本模块直通不重复留痕);两段式与 prot_set 定值写同构。 + */ +int run_par_104_write(uint32_t addr, float v, uint8_t select); + +/* + * 函数功能:落盘脏标志探查(非消费) + * 入口参数:无 + * 返 回 值:1 = 有已整定未落盘的改动;0 = 干净 uint8_t + * 限定条件:任意上下文(单字节);iec104_serv 主循环 peek 后投递事件(B7 同款) + */ +uint8_t run_par_dirty_peek(void); + +/* + * 函数功能:执行落盘请求(cfg_file_save 整档外科保存 + 成功清脏) + * 入口参数:无 + * 返 回 值:0 成功(或无脏可存);-1 写盘失败(脏保持,下轮重试) int + * 限定条件:**defaultTask(配置引擎属主)上下文**,由 SYS_EVT_RUN_PAR_SAVE 驱动 + * 函数说明:save 走引擎唯一写闸(超长拒写/原子改名,E-01 契约);本函数不清 + * 引擎以外模块的脏语义([adc_cal] 副本值已由写路径就位)。 + */ +int run_par_store_save(void); + +#ifdef __cplusplus +} +#endif + +#endif /* RUN_PAR_H */ diff --git a/App/app/app_main.c b/App/app/app_main.c index 79f56f25..ec4b3894 100644 --- a/App/app/app_main.c +++ b/App/app/app_main.c @@ -24,6 +24,7 @@ #include "tpafe5160.h" #include "adc_input.h" /* L1 采集层:adc_input_init()(语义映射 + STM32 ADC1 扫描) */ #include "adc_cal.h" /* L1.5 标定层:adc_cal_init()(ROM 系数 + cfg 两段注册,道A 卡2/4) */ +#include "run_par.h" /* 运行参数段:run_par_init()([run_par] 注册 + 104 地址映射真源,道D 卡10) */ #include "phasor.h" /* L2 相量层:phasor_init()(系数表 + 实例 + 双区交换) */ #include "freq.h" /* L2 派生量:freq_init()(相位解缠绕测频,B8) */ #include "prot_set.h" /* L3 定值模型:prot_set_init()(B5,内核前封存) */ @@ -261,6 +262,12 @@ int app_main_init(void) { DBG_INFO("adc cal init: OK (k_q16 tpafe=%ld stm32=%ld)", (long)ADC_CAL_K_Q16_TPAFE, (long)ADC_CAL_K_Q16_STM32); + /* 4c2) 运行参数段初始化(「数据可信」卡 10):死区 9 点工厂默认 + 注册 + * [run_par] 段。同 adc_cal 的「先注册后 load」契约 → 必须内核前; + * 104 消费(COV 实时读 / 0x8020 段读写映射)在 iec104Task 启动后进行。 */ + run_par_init(); + DBG_INFO("run par init: OK (db9 k/b via adc_cal, evt=1<<8)"); + /* 4d) 保护定值 + 元件层初始化(B5):出厂默认定值封存校验和 + 元件状态 * 复位并解析相量索引(须在 phasor_init 之后)。内核前完成 → iec104 * 定值读/protectTask 首拍前定值必已装载。判据消费在 protectTask。 */ diff --git a/App/app/sys_events.h b/App/app/sys_events.h index f2d63d8b..088e372d 100644 --- a/App/app/sys_events.h +++ b/App/app/sys_events.h @@ -34,6 +34,10 @@ extern osEventFlagsId_t sys_evtHandle; #define SYS_EVT_PROT_SET_SAVE (1u << 7) /* 保护定值落盘请求(B7):iec104Task 检测到定值脏置位, * defaultTask(配置引擎属主)消费并写 /prot_set.bin * (消费型命令位;写盘失败脏标志保留 → 下轮再通知重试) */ +#define SYS_EVT_RUN_PAR_SAVE (1u << 8) /* 运行参数段落盘请求(「数据可信」卡 10):iec104Task + * 检测到 run_par 脏(死区 9 键 / 系数覆盖联动 [adc_cal]) + * 置位,defaultTask 消费并 cfg_file_save 整档外科保存 + * (消费型命令位;失败脏保留 → 下轮重试,B7 同款) */ #ifdef __cplusplus } diff --git a/App/protocol/iec104/iec104_pts.c b/App/protocol/iec104/iec104_pts.c index dfe91a78..d264e2ac 100644 --- a/App/protocol/iec104/iec104_pts.c +++ b/App/protocol/iec104/iec104_pts.c @@ -18,13 +18,18 @@ * 死区改运行时副本 + set/get 入口;COV 遥测腿 = 死区 * 连续 2 点确认(板端按快照 seq 去重)+ 品质翻转即报。 * 单测 = test/host/iec104 TC-IEC104-025~028。 + * 卡 10 2026-10-25 DTU 项目组 死区真源收编 App/algo/run_par.c(本层 s_yc_db + * 副本删除,set/get 转转发壳,壳保留清确认计数语义); + * 定值读写路由 = run_par 表先、prot_set 后(0x8020~ + * 0x8058 运行参数段 C_RD/C_SE,系数覆盖直通 adc_cal)。 */ #include "iec104_pts.h" #include #include "iec104_asdu.h" /* QDS/DIQ 品质位常量 */ -#include "meas.h" /* 板端源:遥测 = L2 遥测快照(卡 9;品质域位同源) */ +#include "meas.h" /* 板端源:遥测 = L2 遥测快照(卡 9;品质域位同源) */ +#include "run_par.h" /* 卡 10:死区真源 + 运行参数段 104 读写路由 */ #include "prot_elm.h" /* 板端源:遥信 = 元件状态 */ #include "prot_set.h" /* 板端源:定值 + 闭锁 */ #include "adc_input.h" /* adc_semantic_t(phasor 实例按语义绑定) */ @@ -37,7 +42,7 @@ typedef struct { uint32_t ioa; int8_t sem; /* 快照 yc 槽 = 语义通道(-1 = 非测量量点,当前仅频率) */ - float db_default; /* 死区 ROM 默认(运行时生效值 = s_yc_db,卡 10 覆盖) */ + float db_default; /* 死区工厂默认(生效真源 = run_par [run_par] 段,卡 10) */ uint8_t src; /* PTS_SRC_* 源类型 */ } pts_yc_def_t; @@ -45,8 +50,8 @@ typedef struct { * 额定基准 100 V / 1 A 与 App/algo/prot_set.c 头部同源,勿在此另立口径): * 电压类 0.5 % × 100 V = 0.5 V 电流类 0.5 % × 1 A = 0.005 A * 频率点不适用"额定百分比"(额定附近 0.5 Hz 与旧值同量级,维持 0.5 并随 - * 卡 10 运行参数段可调)。自卡 9 起本表**只作 ROM 默认载体**:运行时死区 = - * s_yc_db 副本;削顶五路不再 sem=-1 绑死,而是绑快照槽、由品质 CLIPPED + * 卡 10 运行参数段可调)。自卡 9 起本表**只作工厂默认载体**:生效死区 = + * App/algo/run_par.c 的 [run_par] 引擎副本(卡 10 真源收编,本层不再存副本);削顶五路不再 sem=-1 绑死,而是绑快照槽、由品质 CLIPPED * 驱动线上 IV——V0.2(N-18)削顶消失后点位零改动自动启用(docs/任务规划/ * 数据可信.md §10 的 N-18 行)。 * ⚠ 过渡期(eng=0 引脚电压域,卡 12 标定前):上送值本身非二次值,死区 @@ -70,7 +75,6 @@ static const pts_yc_def_t k_yc[] = { * 判据链:新测量点 → |v − 上次上送值| > 死区?→ 连续 2 点确认 → 置变化位。 * 基准 = **上次上送值**(非上次读值——否则慢漂移永不出死区,参考系统同款); * 品质(线上 qds)翻转 = 事件:不参与 2 点计数、当拍直报。 */ -static float s_yc_db[YC_N]; /* 死区运行时副本(ROM 默认 = k_yc 列) */ static float s_yc_rep[YC_N]; /* 上次上送值(建线/确认上送时更新) */ static uint8_t s_yc_repq[YC_N]; /* 上次上送的线上 qds */ static uint8_t s_yc_cnf[YC_N]; /* 越死区连续计数(1 = 待确认;2 = 确认) */ @@ -115,11 +119,8 @@ static uint8_t s_yx_snap_ok; * 直接可定);重复调用 = 全状态复位(单测 reset 依赖此语义)。 */ void iec104_pts_init(void) { - uint8_t i; - - for (i = 0u; i < YC_N; i++) { - s_yc_db[i] = k_yc[i].db_default; - } + /* 死区不在本层复位——真源 = run_par(cfg 引擎副本),复位入口唯一 + * run_par_init();本函数只复位 COV 状态(单测 reset 依赖此语义) */ (void)memset(s_yc_rep, 0, sizeof(s_yc_rep)); (void)memset(s_yc_repq, 0, sizeof(s_yc_repq)); (void)memset(s_yc_cnf, 0, sizeof(s_yc_cnf)); @@ -235,32 +236,35 @@ int iec104_pts_yc_read(uint8_t idx, float *v, uint8_t *qds) { } /* - * 函数功能:写死区运行时值(「数据可信」卡 9;卡 10 运行参数段的落点入口) - * 入口参数:idx - YC 表下标 db - 工程量绝对死区(≥0) float - * 返 回 值:0 成功;-1 越界/负值/NaN(RAM 不动) int - * 限定条件:**iec104Task 上下文**(配置属主 = defaultTask,卡 10 接线时经 - * 104 任务落地本调用,勿跨任务直调——本表状态无锁) + * 函数功能:写死区(卡 9 API 壳,转发 run_par 真源 + 清本层确认计数) + * 入口参数:idx - YC 表下标 db - 工程量绝对死区(≥0,分档上限见 run_par) + * 返 回 值:0 成功;-1 越界/负值/NaN/越程(run_par 副本不动) int + * 限定条件:**iec104Task 上下文**(run_par 副本写 = 引擎对齐 int32 单字, + * 104 执行段之外调用需自行避免与 104 写并发) * 函数说明:成功时清零该点待确认计数——跨死区档位不保留半截确认序列; - * 不隐式重建基线(上送基准延续,新死区即刻生效)。 + * 不隐式重建基线(上送基准延续,新死区即刻生效)。落盘由 run_par + * 脏标志 + SYS_EVT_RUN_PAR_SAVE 走 defaultTask(卡 10 持久化链)。 */ int iec104_pts_yc_set_deadband(uint8_t idx, float db) { - if ((idx >= YC_N) || !(db >= 0.0f) || !(db < 1000000.0f)) { - return -1; /* NaN 被 !(db>=0) 捕获(比较式写法,防 lint 报恒真) */ + if (idx >= YC_N) { + return -1; } - s_yc_db[idx] = db; - s_yc_cnf[idx] = 0u; + if (run_par_deadband_set(idx, db) != 0) { + return -1; /* 守卫(NaN/负/越程分档上限)单一实现,在 run_par */ + } + s_yc_cnf[idx] = 0u; /* 壳语义:跨死区档位不保留半截确认序列 */ return 0; } /* - * 函数功能:读当前生效死区 + * 函数功能:读当前生效死区(转发 run_par 真源) * 入口参数:idx - YC 表下标 - * 返 回 值:运行时死区;idx 越界返 0.0 float - * 限定条件:任意上下文(单字 float 读,Cortex-M 原子) - * 函数说明:UI 回显 / 卡 10 读写往返校验用;ROM 默认查点表,勿在此反推。 + * 返 回 值:生效死区;idx 越界返 0.0 float + * 限定条件:任意上下文(引擎副本 int32 单字读,Cortex-M 原子) + * 函数说明:UI 回显 / 读写往返校验用;工厂默认查点表 db_default 列。 */ float iec104_pts_yc_deadband(uint8_t idx) { - return (idx < YC_N) ? s_yc_db[idx] : 0.0f; + return (idx < YC_N) ? run_par_deadband(idx) : 0.0f; } /* @@ -389,7 +393,7 @@ void iec104_pts_cov_check(iec104_pts_cov_t *cov) { if (d < 0.0f) { d = -d; } - if (d > s_yc_db[i]) { + if (d > run_par_deadband(i)) { s_yc_cnf[i]++; if (s_yc_cnf[i] >= 2u) { /* 连续 2 越 → 确认:报 + 移基准 */ local.yc_changed |= (uint16_t)(1u << i); @@ -564,7 +568,10 @@ static int s_src_yk_exec(uint32_t ioa, uint8_t dpc, uint8_t select) { } /* - * 函数功能:板端默认源 - 定值读(扫 g_prot_set_meta 通讯地址映射) + * 函数功能:板端默认源 - 定值读(**路由顺序 = run_par 表先、未命中落 prot_set**: + * 0x8027~0x802A 属 prot_set 告警段,run_par 命中集为精确地址匹配、 + * 永不覆盖之;两表共存靠「先专属表、后通用扫」分流,勿反序——反序 + * 当前无冲突但失去 run_par 守卫先行留痕的意义) * 返 回 值:0 / -1 未知地址(本地项 comm_addr=0 不可寻址,B7 起显式排除) */ static int s_src_set_read(uint32_t addr, float *out) { @@ -573,6 +580,9 @@ static int s_src_set_read(uint32_t addr, float *out) { if (out == NULL) { return -1; } + if (run_par_104_read(addr, out) == 0) { + return 0; /* 运行参数段命中(卡 10) */ + } for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) { if ((g_prot_set_meta[i].comm_addr != 0u) && (g_prot_set_meta[i].comm_addr == (uint16_t)addr)) { @@ -584,13 +594,22 @@ static int s_src_set_read(uint32_t addr, float *out) { } /* - * 函数功能:板端默认源 - 定值写(Q10 范围校验;执行段 prot_set_set_sealed - * 原子写+封存,B7 收口 B5 竞态窗口;置脏 → 104 任务通知落盘) - * 返 回 值:0 / -1 越界或未知地址(本地项 comm_addr=0 不可寻址) + * 函数功能:板端默认源 - 定值写(**先 run_par 后 prot_set**,分流理由见 + * s_src_set_read 头注;prot_set 腿 = Q10 范围校验 + 执行段 + * prot_set_set_sealed 原子写+封存,B7 收口 B5 竞态窗口) + * 返 回 值:0 / -1 越界或未知地址(本地项 comm_addr=0 不可寻址);两条落盘 + * 链独立:run_par 腿脏 → SYS_EVT_RUN_PAR_SAVE(cfg_file), + * prot_set 腿脏 → SYS_EVT_PROT_SET_SAVE(/prot_set.bin) */ static int s_src_set_write(uint32_t addr, float v, uint8_t select) { uint32_t i; int32_t q10; + int rp; + + rp = run_par_104_write(addr, v, select); + if (rp != -2) { + return rp; /* 本段地址:0 接受 / -1 拒绝,不再下探 */ + } for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) { const prot_set_meta_t *meta = &g_prot_set_meta[i]; diff --git a/App/protocol/iec104/iec104_pts.h b/App/protocol/iec104/iec104_pts.h index d11a13b6..a8fb1871 100644 --- a/App/protocol/iec104/iec104_pts.h +++ b/App/protocol/iec104/iec104_pts.h @@ -29,6 +29,8 @@ * 快照(削顶路由品质驱动 IV);死区改运行时副本 + * set/get 入口;COV 遥测腿 = 死区 2 点确认(seq 去重) * + 品质变化即报;0x401d 绑频率源(freq_valid → IV)。 + * 2026-10-25 DTU 项目组 卡 10:死区真源迁 run_par([run_par] cfg 段 + + * 0x8020~0x8058 读写映射;本层 set/get 为转发壳)。 */ #ifndef IEC104_PTS_H #define IEC104_PTS_H @@ -81,9 +83,11 @@ uint32_t iec104_pts_yc_ioa(uint8_t idx); /* 读 YC:v 有效值(无效时为 0);qds 输出线上品质位(IV/NT/OV,CLIPPED 已映射 * 为 IV)。返回 0/无效-1(NT 亦无效,品质位区分) */ int iec104_pts_yc_read(uint8_t idx, float *v, uint8_t *qds); -/* 死区运行时写(「数据可信」卡 9;卡 10 运行参数段的落点入口): - * db = 工程量绝对值 ≥0;越界/负值/NaN → -1 且 RAM 不动;成功时清零该点 - * 待确认计数(跨死区基线不残留半截确认)。仅 104 服务任务上下文调用。 */ +/* 死区写(卡 9 API;卡 10 起 = 转发 App/algo/run_par.c 真源 + 清本层确认计数): + * db = 工程量绝对值 ≥0(电压 50 V / 电流 1 A / 频率 5 Hz 分档上限);越界/负值/ + * NaN → -1 且真源不动;成功时清零该点待确认计数(跨档位不残留半截确认——此壳 + * 行为有单测钉住)。仅 104 服务任务上下文调用。落盘 = run_par 脏 → 事件 → + * defaultTask(卡 10 持久化链,docs/子系统设计/IEC104上行设计说明.md §3.4)。 */ int iec104_pts_yc_set_deadband(uint8_t idx, float db); /* 读当前生效死区(越界 idx 返 0) */ float iec104_pts_yc_deadband(uint8_t idx); diff --git a/App/task/default_task.c b/App/task/default_task.c index 30f69d6b..5be4142b 100644 --- a/App/task/default_task.c +++ b/App/task/default_task.c @@ -38,6 +38,7 @@ #include "nand_ftl.h" #include "net_cfg.h" #include "cfg_file.h" +#include "run_par.h" /* 运行参数段落盘(卡 10,配置引擎属主 = 本任务) */ #include "prot_set_store.h" #include "sys_log.h" #include "sys_events.h" @@ -207,7 +208,7 @@ void default_task_func(void *argument) { if (sys_evtHandle != NULL) { (void)osEventFlagsWait(sys_evtHandle, SYS_EVT_CFG_SAVE | SYS_EVT_CONSOLE_TX - | SYS_EVT_PROT_SET_SAVE, + | SYS_EVT_PROT_SET_SAVE | SYS_EVT_RUN_PAR_SAVE, osFlagsWaitAny, 50u); } /* 控制台唯一发送点:把环形缓冲中的日志按块发出(无待发时立即返回) */ @@ -231,6 +232,9 @@ void default_task_func(void *argument) { /* 保护定值落盘(B7):内部消费脏标志,无改动 = 空操作; * 写失败脏回置 → iec104Task 下轮 poll 再投递事件 = 天然重试 */ (void)prot_set_store_save(); + /* 运行参数段落盘(卡 10):同款脏语义;save = 引擎整档外科写 + * ([run_par] 9 键 + 被 adc_cal_set 联动改写的 [adc_cal] 两段一趟落) */ + (void)run_par_store_save(); #endif /* 配置文件变更轮询(2s 周期;沉降去抖在引擎内) */ if ((cfg_poll_cnt++ % 40u) == 0u) { diff --git a/App/task/iec104_serv.c b/App/task/iec104_serv.c index 6205a0a8..2497f103 100644 --- a/App/task/iec104_serv.c +++ b/App/task/iec104_serv.c @@ -16,6 +16,7 @@ #include "iec104_pts.h" #include "net_socket.h" #include "prot_set.h" /* 定值脏标志 peek(B7 落盘通知) */ +#include "run_par.h" /* 运行参数脏 peek(卡 10 落盘通知,同款语义) */ #include "sys_clock.h" #include "sys_events.h" #include "task_watchdog.h" @@ -324,6 +325,11 @@ void iec104_serv_task(void *argument) { if ((prot_set_dirty_peek() != 0u) && (sys_evtHandle != NULL)) { (void)osEventFlagsSet(sys_evtHandle, SYS_EVT_PROT_SET_SAVE); } + /* 运行参数整定(C_SE 0x8020~0x8058 段)→ 通知属主落盘 config.ini + * (卡 10;peek 非消费 = 写盘失败天然重试,与上块 B7 语义完全同款) */ + if ((run_par_dirty_peek() != 0u) && (sys_evtHandle != NULL)) { + (void)osEventFlagsSet(sys_evtHandle, SYS_EVT_RUN_PAR_SAVE); + } osDelay(IEC104_POLL_MS); } } diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index 3ff74875..836bbf2a 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -1022,6 +1022,11 @@ 1 ..\App\algo\meas.c + + run_par.c + 1 + ..\App\algo\run_par.c + prot_set.c 1 diff --git a/docs/子系统设计/IEC104上行设计说明.md b/docs/子系统设计/IEC104上行设计说明.md index 295ee810..75972421 100644 --- a/docs/子系统设计/IEC104上行设计说明.md +++ b/docs/子系统设计/IEC104上行设计说明.md @@ -112,6 +112,31 @@ 开机在 STORAGE_READY 屏障前装载,损坏/越界/版本换代 = 整表拒绝保持出厂默认 (all-or-nothing)+ 自检留痕 `prot set corrupt`;写盘失败脏标志保留 = 下轮重试。 +**运行参数段 `0x8020H~0x8058H`(「数据可信」卡 10,2026-10-25 落实)**: + +> ⚠ 逐项地址为**本仓自定**(`docs/算法/继电保护功能复现规划.md` §5.4 的权威表不在仓—— + 参考项目 `CommPara_ConfigTable` 拿不到,送检点表到位后**以送检为准替换常量表**, + 映射表只落在 `App/algo/run_par.c` 顶部常量段一处)。 + +| 地址 | 项 | 域(C_SE/C_RD 浮点工程量) | 工厂默认 | 真源与守卫 | +|---|---|---|---|---| +| 0x8030~0x8038 | 遥测死区 UA/UB/UC/UN/IA/IB/IC/IN/FREQ | V / A / Hz,分档上限 50/1/5 | 卡 6 额定 0.5 % 档(0.5 V ×4 / 0.005 A ×4 / 0.5 Hz) | `run_par` [run_par] 段 9 键 int32 毫单位;落盘键表 → `docs/子系统设计/网络配置说明.md` §3.5 | +| 0x8040~0x8047 | 通道 k 覆盖(µV/码) | 正浮点;adc_cal 合法带 ROM×[0.5,2.0] 执行段裁决 | 回显生效值(ROM = 152.587890625 / 805.6640625) | **写 = `adc_cal_set` 代理,系数唯一真源在 [adc_cal] 段,run_par 不存系数**(防后人拿 run_par 表当真源表) | +| 0x8048~0x804F | 通道 b 覆盖(码值,可负) | ±8192(adc_cal 带) | 0(回显) | 同上;单侧写 RMW 用另一侧**当前生效值** | +| 其余 | 留白(PT/CT 变比等未启用类别) | — | — | C_RD → COT14+EVI / C_SE → actcon+PN | + +- `0x8027~0x802A` 是 prot_set 告警段(过压/欠压),**run_par 命中集精确匹配、永不覆盖**; + 两表共存的路由顺序 = **run_par 表先、未命中落 prot_set**(`iec104_pts.c` set 路由, + 证据钉在 TC-IEC104-029)。 +- 现行门控 = 两段式 + 范围/合法带校验(与保护定值同款),**无「仅暂停/测试态可写」 + 门控**——文档现行如此,不造新语义;若送检点表要求该类门控,接线点在 + `run_par_104_write` 执行段入口,加一处即可。 +- 持久化(卡 10):任一写成功置 `run_par` 脏 → iec104Task 同款 peek 投递 + `SYS_EVT_RUN_PAR_SAVE`(1<<8) → defaultTask `run_par_store_save()` 调 `cfg_file_save()` + 整档外科保存(一趟覆盖 [run_par] 9 键 + 被代理改写的 [adc_cal]/[adc_cal_ofs], + lead 裁②一条事件位一趟够用);失败脏保留 = 下轮重试。消费侧(COV 判定)**实时读 + 引擎副本**,无缓存无投递(卡 9 的 pts 层 s_yc_db 副本已收编进 run_par)。 + ## 4. 链路与流程 - 参数:k=12 / w=8 / t1=15s / t2=10s / t3=20s(标准默认);序号 15bit 回绕, @@ -172,7 +197,7 @@ | 层 | 内容 | |---|---| -| 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;**板端默认源真实链**(ring→phasor→freq→meas 快照,非桩):值/品质透传、CLIPPED→IV、0x401D 绑频(收敛 ±0.02 Hz / 未新鲜 IV)、死区运行时化 + 2 点确认/回摆撤销、seq 去重(卡 9/11,TC-IEC104-025~028);`test/host/prot_set/`:blob 往返 + 损坏整表拒绝(all-or-nothing)| +| 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;**板端默认源真实链**(ring→phasor→freq→meas 快照,非桩):值/品质透传、CLIPPED→IV、0x401D 绑频(收敛 ±0.02 Hz / 未新鲜 IV)、死区运行时化 + 2 点确认/回摆撤销、seq 去重(卡 9/11,TC-IEC104-025~028)+ 运行参数段路由/两段式/RMW(卡 10,TC-IEC104-029);`test/host/run_par/`:[run_par] 注册/落盘往返/守卫/系数直通合法带(TC-RUNPAR-001~008);`test/host/prot_set/`:blob 往返 + 损坏整表拒绝(all-or-nothing)| | L0 | `iec104-unit-test` 钩子 + cppcheck/coding-standard | | 台架 | `@test` 锚点(104 服务启动/监听行);**PC 主站客户端 `test/host/iec104_master.py`(纯标准库,B6 2026-10-12 就绪)**:STARTDT/总召四组+actterm/C_SE 定值两段写→YX15·16 变位(COT=3)/恢复复归/YK11 两段正答/C_RD 读回,台架往返 **18/18 PASS**(证据 `../测试说明/boot_log_data/20261012_B6_iec104_roundtrip_log.txt`);**B7 持久化模式 `--b7-pre/--b7-post/--b7-mark/--b7-check`**(中间 ST-LINK 复位):YK12 两段投退→保护退出/联动、**定值与压板态跨复位保持**(含非默认 7.5 往返),**21/21 + 13/13 + 4/4+4/4 PASS**(证据 `../测试说明/boot_log_data/20261012_B7_iec104_persist_log.txt`);**B9 SOE 腿**(同工具同轮回归):自发 YX 断言 **M_SP_TB_1 带 ts**(工具解析 type 30/1 双型)+ FTP 下载 `/log/soe.log` 经 `test/host/soe/decode_soe.py` 解码断言(校验和/seq 单调/CP56↔epoch UTC 交叉),pre 21/21 + post 13/13 不回归;lib60870 级第三方全流程归 B10 | @@ -183,8 +208,10 @@ 双点带时标不做(V0.1 双点恒 IV,无意义)。 3. YK1~4/13/14 否定确认——出口硬件确认后(R7)转真执行(YK12 已于 B7 转真)。 4. ~~遥测死区 10.0 码值为拟定值~~——工程量死区已于 2026-10-25 卡 6 落地(额定 0.5%); - 卡 9(2026-10-25)已改**运行时副本 + set/get 入口 + 2 点确认**;主站可读写(8020H 段 - run_par 接线 `iec104_pts_yc_set_deadband`)仍归「数据可信」卡 10。 + 卡 9 已加 2 点确认;卡 10(2026-10-25)已落地 **0x8020~0x8058 读写映射 + [run_par] + 持久化 + 系数覆盖直通 adc_cal**——死区系数唯一真源 = cfg 引擎副本(run_par 存死区、 + **不存系数**,系数真源在 [adc_cal] 段,见 §3.4 表注),`iec104_pts_yc_set_deadband` + 降级为转发壳。 5. 多主站/双链路冗余不做(单连接语义)。 6. 主站侧验证工具 = `test/host/iec104_master.py`(B6 2026-10-12 就绪,台架往返 18/18,覆盖总召/变位/遥控/定值);**第三方送检环境仍未就绪**——lib60870 级第三方全流程归 B10。 7. **后连优先未实现**(V0.1 = 首到优先):单连接 socket 连接态无法 accept, @@ -201,5 +228,6 @@ | 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 断言 | | 2026-10-12 | **B7 台架腿闭环**(§7 台架行回填):主站工具加 `--b7-pre/--b7-post/--b7-mark/--b7-check` 四模式(中间 ST-LINK 复位);pre 21/21(YK12 OFF→低定值不动作 / ON→YX15·16 即动作)、post 13/13(**压板退出态跨复位保持** + 重启后联动在位)、mark/check 4/4+4/4(**非默认定值 7.5 跨复位保持**);开机日志 `[CFG] prot set loaded (/prot_set.bin)`;证据 `../测试说明/boot_log_data/20261012_B7_*` | | 2026-10-25 | **标定折算落地联动(卡 6)**:遥测读数由原始码值改工程量(§3.1 注已重写);死区按额定 0.5% 逐点重定(0.5 V / 0.005 A / 0.5 Hz),§8 第 4 条改「已落地,8020H 段仍归卡 10」 | +| 2026-10-25 | **卡 10(道D,「数据可信」M6-①)**:新增 `App/algo/run_par.c/h`——遥测死区 9 点真源收编([run_par] 段 9 键 int32 毫单位落盘,工厂默认 = 卡 6 档;pts 层 s_yc_db 副本删除、set/get 转转发壳并保留清确认计数语义);104 **0x8020~0x8058 读写映射**(§3.4 新表:逐项地址本仓自定、送检点表到位后替换常量段;0x8027~0x802A 永不覆盖,路由 run_par 先 prot_set 后);**k/b 系数覆盖 = adc_cal_set 代理**(合法带/越带留痕复用,run_par 不存系数);持久化走新事件位 `SYS_EVT_RUN_PAR_SAVE`(1<<8) 整档外科 save;单测 test/host/run_par 新建(TC-RUNPAR-001~008,67 断言)+ iec104 套 +TC-IEC104-029 路由钉(141 断言);台架/送检腿待排窗 | | 2026-10-25 | **卡 9/卡 11(道D,「数据可信」M5/M6)**:YC 板端源改读 L2 遥测快照(削顶五路从点表绑死 -1 改为**槽位绑定 + 快照品质 CLIPPED→IV**,V0.2/N-18 零改动启用);`0x401D` 绑频率源(`freq_valid()==0` → IV);死区改运行时副本 + `iec104_pts_yc_set_deadband()`(卡 10 接线点);COV 遥测腿 = 死区连续 2 点确认(快照 `seq` 去重)+ 品质翻转直报;`iec104_pts_src_t` 表尾追加 `yc_read_ex`(旧源退路语义不变);单测 97→127(TC-IEC104-025~028 走真实 L2 链);台架腿(总召读回一致 / 频率 ±0.01 Hz)待排窗——主站工具已记 val+qds,无需改工具 | | 2026-10-12 | **B9 时标 SOE**:自发单点 YX 换 **M_SP_TB_1(30)**(SIQ+CP56,§3.2 注;总召/周期保持 NA 型);link cfg 增 `epoch_ms` 时标源(NULL/无效 → IV 如实);serv 绑 `sys_clock`(秒域 → ms 域=秒×1000);主站工具解析 type 30/1 双型;单测 iec104 94→97(TB 时标 + IV 路径,两种类型都断言);台架腿见 §7 B9 行;落盘侧见 `App/log/README.md` soe 节 | diff --git a/docs/子系统设计/网络配置说明.md b/docs/子系统设计/网络配置说明.md index b1a643fe..ea3bc0df 100644 --- a/docs/子系统设计/网络配置说明.md +++ b/docs/子系统设计/网络配置说明.md @@ -150,6 +150,27 @@ write=1 > **诚实边界**:FTP 协议**明文传输口令**——本方案挡的是**未授权访问**,不挡**网络嗅探**(CH395F 上实现 TLS 不现实)。`pwd_hash` 只防 `/config.ini` 泄露后的**离线口令还原**,盐 = 用户名(无每机随机盐,局域网威胁模型下够用)。 +### 3.5 `[run_par]` 段:运行参数(2026-10-25 新增,「数据可信」卡 10) + +遥测死区 9 点的持久载体(引擎属主 defaultTask;**现场远程写入口 = 104 段 +`0x8030~0x8038`**,本段键供 FTP 手改 / 排障直读,逐项语义与地址表 → +`docs/子系统设计/IEC104上行设计说明.md` §3.4)。**值 = 工程量毫单位 int32 有符号** +(`db_ua=500` 即 0.5 V;电流域 `db_ia=5` 即 0.005 A;频率域 `db_freq=500` 即 0.5 Hz)。 + +| 键 | 通道 | 工厂默认 | 104 写路径合法域(毫单位) | +|---|---|---|---| +| `db_ua` `db_ub` `db_uc` `db_un` | 电压死区 | 500 | 0 ~ 50000(50 V) | +| `db_ia` `db_ib` `db_ic` `db_in` | 电流死区 | 5 | 0 ~ 1000(1 A) | +| `db_freq` | 频率死区 | 500 | 0 ~ 5000(5 Hz) | + +- 键数 9 ≤ 16,**通道系数覆盖不落本段**——系数唯一真源 = `[adc_cal]` / `[adc_cal_ofs]` + 两段(卡 4 布局,docs/任务规划/数据可信.md §4.4;104 `0x8040~0x804F` 写 = + `adc_cal_set` 代理,lead 裁②a)。 +- 守卫在**写路径**(104/壳 API 拒 NaN/负/越程);**文件通路引擎照收**(同 adc_cal 的 + 「人工编辑 + 沉降去抖」契约)——死区超程只让上送变钝、不毁量纲,无合法带哨兵。 +- 生效链路:load/poll 原地改副本 → 104 COV 每拍实时读(无回调,卡 9/10 契约); + 104 写成功 → `SYS_EVT_RUN_PAR_SAVE` → 本任务 `cfg_file_save()` 整档外科落盘。 + --- ## 4. 生效链路 @@ -308,6 +329,7 @@ ENTER 进入选中地址行 → `↑/↓` 调活动字节(0..255 **环绕**) | 日期 | 变更 | |---|---| +| 2026-10-25 | v2.4:**新增 §3.5 `[run_par]` 段(「数据可信」卡 10)**——遥测死区 9 键毫单位布局、104 写路径合法域、系数覆盖不落本段(真源 [adc_cal])、守卫仅在写路径、事件化落盘 | | 2026-10-11 | v2.3:**新增 §3.4 `[ftp]` 段(N-19 FTP 鉴权)**——`user` / `pwd_hash`(SHA-256(user‖password))/ `write` 字段表、鉴权流程(USER 恒 331 / 恒定时间比较 / 未登录命令门 / 5 败锁 60 s)、改口令步骤与忘口令恢复路径、出厂默认口令启动告警、明文嗅探边界 | | 2026-10-10 | v2.2:**删除旧档 NCFG 迁移**——`net_cfg_legacy.c/h` 整体移除、`cfg_store_legacy_path()` 撤销;旧档 `/cfg/netcfg.bin` 若存在则被忽略(不读、不删、不迁移),配置回落工厂默认。理由:该一次性路径把整条配置写链挂进启动期调用图,defaultTask 静态最坏栈链 +600 B,且本仓库无携带旧档的设备 | | 2026-09-14 | v2.1:存储迁移至**通用配置文件** `/config.ini`(INI `[network]` 段,cfg_file 引擎:逐键校验 / 外科手术式保存 / 原子写 / 旧档首启迁移 / 沉降轮询);`net_cfg_store.*` 弃用(由 `cfg_file_store.*` 取代);对外 API 不变 | diff --git a/docs/算法/继电保护功能复现规划.md b/docs/算法/继电保护功能复现规划.md index a8b0e4a3..3973154e 100644 --- a/docs/算法/继电保护功能复现规划.md +++ b/docs/算法/继电保护功能复现规划.md @@ -409,6 +409,11 @@ > ⚠️ **待补**:逐项**取值范围 / 步长 / 默认值**取自参考项目 `APP/EquipCfg/Const_CommPara.c` 的 `CommPara_ConfigTable`(`0x8020~0x8882`);本表在实现各保护元件时逐项落实(见 §10.3)。 > +> **2026-10-25 运行参数段落实进度**:卡 10 已落 `App/algo/run_par.c`——本仓自定逐项地址 +> 0x8030~0x8038(死区 9 点)/ 0x8040~0x804F(k/b 覆盖直通 adc_cal),表与理由见 +> `docs/子系统设计/IEC104上行设计说明.md` §3.4;**送检点表到位后以送检为准替换常量段**。 +> 变比/报警/录波/功率置负等其余运行参数类项目前无消费者,留白不预埋。 +> > **2026-10-11 落实进度**:`App/algo/prot_set.c` 已建 RAM 模型(Q10 定点,31 项),**通讯地址逐项对照参考 `Comm/Pub/Const_CommSetPara.c` `g_tCommSetParaTab`**(基址 0x8820:过流 Ⅰ/Ⅱ/Ⅲ = 0x8856~0x8861、过负荷 = 0x8871~0x8874、零序过压 = 0x888E~0x8890;过压/欠压沿用告警段 0x8027~0x802A)。⚠ **默认值/范围为拟定值**(参考 EEPROM 出厂默认表不在本仓库,按 DTU 馈线典型量级拟定),待对照参考出厂表逐项修订。 --- diff --git a/scripts/check_registration.py b/scripts/check_registration.py index 3589179a..886a3f2a 100644 --- a/scripts/check_registration.py +++ b/scripts/check_registration.py @@ -96,6 +96,7 @@ SIM_EXEMPT = [ ("App/algo/sample_ring.c", "L2 环形缓冲纯算法,模拟器无消费路径"), ("App/algo/adc_cal.c", "L1.5 标定折算纯算法 + cfg 段注册(消费者 = MCU 侧 adcTask 每拍;模拟器不跑采样任务),模拟器无消费路径"), ("App/algo/meas.c", "L2 遥测快照发布纯算法(唯一发布者 = MCU 侧 adcTask 200 ms 分频;104/UI 侧消费属卡 9/UI 波次另行接线,模拟器无消费路径;L0.5 单测 test/host/meas 覆盖),模拟器无消费路径"), + ("App/algo/run_par.c", "运行参数段纯算法 + [run_par] cfg 段注册 + 104 地址映射(写者 = iec104Task 直通、属主落盘 defaultTask;模拟器不跑 104 协议,模拟器无消费路径;L0.5 单测 test/host/run_par 覆盖),模拟器无消费路径"), ("App/app/app_main.c", "固件启动/就绪屏障,MCU 专属"), ("App/app/ui_data_feed.c", "UI 快照装配器(依赖 phasor/prot_set/iec104 端口常量等固件侧数据源);模拟器不跑装配链,快照由 simulator/main.c 直接发布固定帧,模拟器无消费路径"), ("App/fs/vol_lock.c", "FreeRTOS 互斥量卷锁,MCU 专属"), diff --git a/scripts/id_registry.json b/scripts/id_registry.json index a1a85a06..9b1ffce3 100644 --- a/scripts/id_registry.json +++ b/scripts/id_registry.json @@ -1796,6 +1796,29 @@ "owner_note": "B 层口径(用户裁定 2026-10-21,同 TC-HOST-ADCHEALTH/TC-HOST-ADCCAL 先例;2026-10-25 道D 卡8 建族):属主登记处 = 单测代码目录,不逐号补文档", "cursor_note": "游标按建族时实测最大号收录(2026-10-25):越界即视为未登记的冒号牌" }, + { + "name": "TC-HOST-RUNPAR", + "class": "TC", + "title": "主机侧单测用例号:运行参数段(test/host/run_par,道D 卡10)", + "pattern": "\\bTC-RUNPAR-[0-9]{1,4}[a-z]?\\b", + "digit_width": [ + 2, + 4 + ], + "owner_docs_glob": [ + "test/host/run_par/*" + ], + "scope": "global", + "frozen": false, + "cursor": "TC-RUNPAR-008", + "cursor_owner_only": true, + "check_dangling": true, + "allow_orphans": true, + "subitem_ok": true, + "owner_note": "B 层口径(用户裁定 2026-10-21,同 TC-HOST-MEAS/TC-HOST-ADCCAL 先例;2026-10-25 道D 卡 10 建族):属主登记处 = 单测代码目录,不逐号补文档;路由顺序证据链在 TC-IEC104-029(test/host/iec104)", + "cursor_note": "游标按建族时实测最大号收录(2026-10-25):越界即视为未登记的冒号牌" + }, + { "name": "TC-HOST-IEC104", "class": "TC", @@ -1810,7 +1833,7 @@ ], "scope": "global", "frozen": false, - "cursor": "TC-IEC104-028", + "cursor": "TC-IEC104-029", "cursor_owner_only": true, "check_dangling": true, "allow_orphans": true, diff --git a/test/host/iec104/iec104_test.c b/test/host/iec104/iec104_test.c index a8452a15..447ad2b1 100644 --- a/test/host/iec104/iec104_test.c +++ b/test/host/iec104/iec104_test.c @@ -41,10 +41,12 @@ * →线上 IV 清零 / 干净路品质 0 / 频率未新鲜 IV * TC-IEC104-026 0x401D 绑频:收敛后 ±0.02 Hz / freq_valid==0 → IV 清零 * (卡 11 DoD 原句) - * TC-IEC104-027 死区运行时化:set/get + 非法拒 + 运行档压住旧档步长 + - * 2 点确认/回摆撤销/init 复位 ROM 默认 + * TC-IEC104-027 死区运行时化(卡 10 后 = run_par 真源 + 转发壳):set/get + * + 非法拒 + 运行档压旧档 + 2 点确认/回摆撤销 + 壳清半截确认计数 * TC-IEC104-028 品质翻转直报(假源)+ 板端 seq 去重(同帧不计两点)+ * 换帧只变品质时按集合直报 + * TC-IEC104-029 卡 10 路由:0x8030/0x8038 命中 / 0x8027 仍归 prot_set / + * 空隙 COT14 / C_SE 两段式 / k 带拒 / b 单侧 RMW 保 k * * 通过:全部 TC PASS(main 返回 0)。 * 创建日期:2026-10-12(B4) @@ -67,6 +69,115 @@ static int s_total = 0, s_passed = 0, s_failed = 0; #include "phasor.c" #include "freq.c" #include "meas.c" /* 卡 9:板端默认 YC 源 = L2 遥测快照(真实链,非桩) */ + +/* ===================== 卡 10 真链桩(同 adc_cal 套件先例) ===================== + * run_par 依赖真实 cfg 引擎与 adc_cal:dbg_log 走 stub/ 计数宏,文件/堆走 + * 内存虚拟盘(cfg_store_* 强符号),adc_input_desc 按 V0.1 权威映射给桩。 + * 顺序敏感:桩头与强符号先于被纳入的 .c。 */ +#include "dbg_log.h" /* stub/ 优先解析:DBG_* → 计数 */ +#include +#include "cfg_file_store.h" + +#define RP_VFS_CAP 8192u + +static uint8_t rp_s_vfs[RP_VFS_CAP]; +static size_t rp_s_vfs_len = 0u; +static uint32_t rp_s_vfs_mtime = 100u; +static int rp_s_vfs_exists = 0; + +void *cfg_store_alloc(size_t size) { return malloc(size); } +void cfg_store_free(void *p_buf) { free(p_buf); } +uint32_t cfg_store_heap_free(void) { return 0u; } + +int cfg_store_read(const char *p_path, uint8_t *p_buf, size_t cap, size_t *p_nread) { + size_t n; + + (void)p_path; + if (!rp_s_vfs_exists) { + return -1; + } + n = rp_s_vfs_len; + if (n > cap) { + n = cap; + } + (void)memcpy(p_buf, rp_s_vfs, n); + *p_nread = n; + return 0; +} + +int cfg_store_write_atomic(const char *p_path, const uint8_t *p_buf, size_t len) { + (void)p_path; + if (len > RP_VFS_CAP) { + return -1; + } + (void)memcpy(rp_s_vfs, p_buf, len); + rp_s_vfs_len = len; + rp_s_vfs_exists = 1; + rp_s_vfs_mtime++; + return 0; +} + +int cfg_store_stat(const char *p_path, cfg_file_stat_t *p_st) { + (void)p_path; + if (!rp_s_vfs_exists) { + return -1; + } + p_st->size = (uint32_t)rp_s_vfs_len; + p_st->mtime = rp_s_vfs_mtime; + return 0; +} + +int cfg_store_remove(const char *p_path) { + (void)p_path; + rp_s_vfs_exists = 0; + rp_s_vfs_len = 0u; + return 0; +} + +const char *cfg_store_config_path(void) { return "/config.ini"; } +const char *cfg_store_tmp_path(void) { return "/config.tmp"; } + +/* ---- sys_log 桩:只计数自检留痕 ---- */ +#include "sys_log.h" + +static int rp_s_syslog_count = 0; + +void syslog_post(syslog_id_t id, const char *desc) { + (void)id; + (void)desc; + rp_s_syslog_count++; +} + +/* ---- adc_input_desc 桩:V0.1 权威映射(docs/硬件平台/ADC通道映射说明.md) ---- */ +#include "adc_input.h" + +static const adc_channel_desc_t rp_s_desc[ADC_SEM_COUNT] = { + { ADC_SRC_TPAFE, 6u }, /* UA = buf[6] */ + { ADC_SRC_STM32, 14u }, /* UB = CH14(PC4) */ + { ADC_SRC_STM32, 3u }, /* UC = CH3 */ + { ADC_SRC_TPAFE, 5u }, /* UN = buf[5] */ + { ADC_SRC_TPAFE, 2u }, /* IA = buf[2] */ + { ADC_SRC_STM32, 6u }, /* IB = CH6 */ + { ADC_SRC_STM32, 1u }, /* IC = CH1 */ + { ADC_SRC_STM32, 2u }, /* IN = CH2 */ + { ADC_SRC_STM32, 5u }, /* TEMP = CH5 */ +}; + +const adc_channel_desc_t *adc_input_desc(adc_semantic_t ch) { + if ((int)ch < 0 || (unsigned)ch >= (unsigned)ADC_SEM_COUNT) { + return NULL; + } + return &rp_s_desc[(unsigned)ch]; +} + +/* dbg 计数定义(stub/dbg_log.h 声明 extern) */ +int g_dbgstub104_err_count = 0; +int g_dbgstub104_info_count = 0; +int g_dbgstub104_debug_count = 0; + +#include "cfg_file.c" +#include "adc_cal.c" +#include "run_par.c" #include "prot_elm.c" #include "iec104_pts.c" #include "iec104_link.c" @@ -215,6 +326,7 @@ static void reset_all(uint32_t now) { prot_set_init(); prot_elm_init(); + run_par_init(); /* 卡 10:死区/脏复位到工厂默认(重复注册 -1 走计数桩,无害) */ iec104_pts_init(); iec104_pts_bind_src(&g_fake); (void)memset(s_f_yc, 0, sizeof(s_f_yc)); @@ -1038,7 +1150,8 @@ static void tc_db_runtime(void) iec104_pts_cov_check(&cov); CHECK((cov.yc_changed & 0x0001u) == 0u, "TC-IEC104-027 2nd point under 2.0 still silent"); - iec104_pts_init(); /* 恢复 ROM 默认档(0.5) */ + run_par_init(); /* 恢复工厂默认档(0.5)——卡 10 复位入口 */ + iec104_pts_init(); iec104_pts_bind_src(&g_fake); s_f_yc[0] = 100.0f; iec104_pts_cov_check(&cov); /* 建线 */ @@ -1057,6 +1170,18 @@ static void tc_db_runtime(void) CHECK((cov.yc_changed & 0x0001u) == 0u, "TC-IEC104-027 pullback cleared pending count"); iec104_pts_cov_check(&cov); CHECK((cov.yc_changed & 0x0001u) != 0u, "TC-IEC104-027 two fresh crossings confirm again"); + + /* 壳语义钉(lead 裁⑤):iec104_pts_yc_set_deadband 成功须清零该点半截确认 + * 计数——若不清,下面第一拍就凑满 2 点直发,与"跨档位重新起算"矛盾 */ + s_f_yc[0] = 100.0f; /* rep=101.4 起 d=1.4 > 0.5:第 1 越挂起 */ + iec104_pts_cov_check(&cov); + CHECK((cov.yc_changed & 0x0001u) == 0u, "TC-IEC104-027 shell pin arms first"); + iec104_pts_yc_set_deadband(0u, 0.4f); + s_f_yc[0] = 100.5f; /* d=0.9 > 0.4:计数已清则仍只是第 1 越 */ + iec104_pts_cov_check(&cov); + CHECK((cov.yc_changed & 0x0001u) == 0u, "TC-IEC104-027 shell clears pending count"); + iec104_pts_cov_check(&cov); /* 新档连续第 2 越 → 确认 */ + CHECK((cov.yc_changed & 0x0001u) != 0u, "TC-IEC104-027 reconfirms after shell clear"); } static void tc_cov_qds_seq(void) @@ -1100,6 +1225,47 @@ static void tc_cov_qds_seq(void) "TC-IEC104-028 clipped flip per frame reported as a set"); } +/* 卡 10:0x8020~0x8058 读写路由(板端默认源,run_par 表先 / prot_set 后) */ +static void tc_pts_rpar_route(void) +{ + float v; + + reset_all(1000u); + iec104_pts_bind_src(NULL); + CHECK((iec104_pts_set_read(0x8030u, &v) == 0) && (fabsf(v - 0.5f) < 1e-4f), + "TC-IEC104-029 0x8030 deadband UA reads factory default"); + CHECK((iec104_pts_set_read(0x8038u, &v) == 0) && (fabsf(v - 0.5f) < 1e-4f), + "TC-IEC104-029 0x8038 deadband freq reads factory 0.5 Hz"); + /* 0x8027~0x802A = prot_set 告警段(欠压默认 20 V)——run_par 精确匹配 + * 永不覆盖;路由顺序的直接证据 */ + CHECK((iec104_pts_set_read(0x8027u, &v) == 0) && (fabsf(v - 20.0f) < 0.01f), + "TC-IEC104-029 0x8027 still served by prot_set table"); + CHECK(iec104_pts_set_read(0x8039u, &v) == -1, "TC-IEC104-029 gap address unknown"); + /* C_SE 两段式:选择段校验不提交,执行段落真源 */ + CHECK((iec104_pts_set_write(0x8031u, 0.7f, 1u) == 0) + && (fabsf(iec104_pts_yc_deadband(1u) - 0.5f) < 1e-4f), + "TC-IEC104-029 select phase validates without commit"); + CHECK((iec104_pts_set_write(0x8031u, 0.7f, 0u) == 0) + && (fabsf(iec104_pts_yc_deadband(1u) - 0.7f) < 1e-4f) + && (run_par_dirty_peek() != 0u), + "TC-IEC104-029 execute commits and marks dirty"); + CHECK(iec104_pts_set_write(0x8031u, 51.0f, 1u) == -1, + "TC-IEC104-029 over-range rejected in select"); + /* k/b 覆盖直通 adc_cal(UA 合法带 = ROM x[0.5,2.0] = [76.29,305.17]) */ + CHECK((iec104_pts_set_write(0x8040u, 300.0f, 0u) == 0) + && (fabsf(adc_cal_k_uv(ADC_SEM_UA) - 300.0f) < 0.01f), + "TC-IEC104-029 k override lands in adc_cal"); + CHECK(iec104_pts_set_write(0x8040u, 400.0f, 0u) == -1, + "TC-IEC104-029 k band reject (via adc_cal)"); + CHECK((iec104_pts_set_write(0x8048u, -5.0f, 0u) == 0) + && (fabsf(adc_cal_b_codes(ADC_SEM_UA) + 5.0f) < 1e-3f) + && (fabsf(adc_cal_k_uv(ADC_SEM_UA) - 300.0f) < 0.01f), + "TC-IEC104-029 b single-side write keeps k (RMW)"); + CHECK((iec104_pts_set_read(0x8048u, &v) == 0) && (fabsf(v + 5.0f) < 1e-3f), + "TC-IEC104-029 b echo via run_par read path"); + run_par_init(); /* 收尾:把 k/b 脏与副本复位留给后续 TC 的干净出厂态 */ +} + int main(void) { printf("=== IEC104 Protocol Stack Host Tests ===\n"); @@ -1131,6 +1297,7 @@ int main(void) tc_pts_freq(); tc_db_runtime(); tc_cov_qds_seq(); + tc_pts_rpar_route(); printf("=== IEC104 Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); return (s_failed == 0) ? 0 : 1; diff --git a/test/host/iec104/run_unit_test.py b/test/host/iec104/run_unit_test.py index cf0979b7..1f9f3d8d 100644 --- a/test/host/iec104/run_unit_test.py +++ b/test/host/iec104/run_unit_test.py @@ -4,7 +4,8 @@ 做什么:用本机 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),验证 + 依赖 prot_set.c + prot_elm.c + phasor.c + sample_ring.c + freq.c + + meas.c,卡 10 起再 + cfg_file.c + adc_cal.c + run_par.c 真链),验证 APCI 重组 / STARTDT 握手 / 总召分片 / COV / k-w-t1~t3 / 遥控与定值两段式 / C_RD / C_CS / 错误应答(EVI)。 @@ -28,8 +29,14 @@ 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"), + # 卡 10 起 stub 先于生产目录(dbg_log.h → 计数宏桩);App/cfg + App/log + # 真头供 cfg_file.h / cfg_file_store.h / sys_log.h;adc_input_desc / + # syslog_post / cfg_store_* 由测试单元给强符号桩(同 adc_cal 套件先例)。 include_dirs=[ + os.path.join(HERE, "stub"), os.path.join(ROOT, "App", "algo"), + os.path.join(ROOT, "App", "cfg"), + os.path.join(ROOT, "App", "log"), os.path.join(ROOT, "App", "protocol", "iec104"), HERE, ], diff --git a/test/host/iec104/stub/dbg_log.h b/test/host/iec104/stub/dbg_log.h new file mode 100644 index 00000000..8ac1cfb7 --- /dev/null +++ b/test/host/iec104/stub/dbg_log.h @@ -0,0 +1,20 @@ +/* + * 主机侧单测桩:dbg_log.h(IEC104 套件专用) + * 模块功能:为卡 10 起纳入本 TU 的 cfg_file.c / adc_cal.c / run_par.c 提供 + * 主机可编译的日志宏(映射为计数)。⚠ 仅供 test/host/iec104/ 使用, + * 不参与 MCU 构建。模式同 test/host/adc_cal/stub/ 先例。 + * 创建日期:2026-10-25(道D 卡 10) + */ + +#ifndef DBG_LOG_H +#define DBG_LOG_H + +extern int g_dbgstub104_err_count; +extern int g_dbgstub104_info_count; +extern int g_dbgstub104_debug_count; + +#define DBG_INFO(fmt, ...) do { g_dbgstub104_info_count++; } while (0) +#define DBG_DEBUG(fmt, ...) do { g_dbgstub104_debug_count++; } while (0) +#define DBG_ERROR(fmt, ...) do { g_dbgstub104_err_count++; } while (0) + +#endif /* DBG_LOG_H */ diff --git a/test/host/iec104_serv/iec104_serv_test.c b/test/host/iec104_serv/iec104_serv_test.c index b23bcc5d..235be207 100644 --- a/test/host/iec104_serv/iec104_serv_test.c +++ b/test/host/iec104_serv/iec104_serv_test.c @@ -202,6 +202,9 @@ int sys_clock_set_unix(uint32_t unix_secs) return g_clock_set_rc; } uint8_t prot_set_dirty_peek(void) { return (uint8_t)g_dirty; } +/* run_par 脏 peek 假件(卡 10):任务主循环在单测中不跑,脏恒 0 = + * 不投递 SYS_EVT_RUN_PAR_SAVE;落盘链真身覆盖在 test/host/run_par。 */ +uint8_t run_par_dirty_peek(void) { return 0u; } uint32_t osEventFlagsSet(osEventFlagsId_t ef_id, uint32_t flags) { (void)ef_id; diff --git a/test/host/run_par/run_par_test.c b/test/host/run_par/run_par_test.c new file mode 100644 index 00000000..934769af --- /dev/null +++ b/test/host/run_par/run_par_test.c @@ -0,0 +1,393 @@ +/* + * 模块名称:运行参数段(run_par)主机侧单元测试 + * 模块功能:离线钉死「数据可信」卡 10 的三层契约——① [run_par] cfg 段: + * 注册/工厂默认/落盘往返/文件改值生效(真实 cfg 引擎 + 内存虚拟盘, + * 同 adc_cal 套件先例);② 104 地址映射:命中集/空隙/两段式/分档 + * 量程守卫;③ k/b 覆盖直通 adc_cal_set(真实标定层):合法带拒绝、 + * 单侧 RMW 保另一侧、回显 = 生效值视图。 + * 被测对象:真实源文件(#include 纳入):run_par.c + cfg_file.c + adc_cal.c; + * dbg 计数桩(stub/dbg_log.h)+ cfg_store_* 内存虚拟盘 + adc_input_desc + * V0.1 权威映射桩。**路由顺序证据(run_par 表先 / prot_set 后)在 + * test/host/iec104 TC-IEC104-029 钉(那边才有全协议栈)**。 + * + * 覆盖点(TC-RUNPAR-xxx;属主登记处 = 本目录,族 TC-HOST-RUNPAR): + * TC-RUNPAR-001 init 工厂默认 = 卡 6 额定档 + idx 守卫 + * TC-RUNPAR-002 死区写往返 + 分档上限(V/A/Hz)+ NaN/负值拒 + 定点量化 + * TC-RUNPAR-003 落盘链:set→脏→store_save 成功清脏 + 文件含 9 键 + + * load 回读往返 + 外部改文件值生效 + 无脏 save 空操作 + * TC-RUNPAR-004 104 读映射:0x8030~0x8038 命中 / 空隙与邻段 -1 / k·b 回显 + * TC-RUNPAR-005 104 两段式:选择段不提交不置脏,执行段提交置脏 + * TC-RUNPAR-006 k/b 直通:合法带接受与拒绝 / 非正 k 拒 / b 单侧 RMW 保 k / + * 非法带值经 load 注入后回显 = 生效值(哨兵视图) + * TC-RUNPAR-007 init 幂等:重复调用复位默认 + 清脏;重注册被引擎拒(留痕) + * TC-RUNPAR-008 引擎副本 = 生效值(无回调):load 原地改副本即时可见 + * + * 通过:全部 TC PASS(main 返回 0)。 + * 创建日期:2026-10-25(道D 卡 10) + */ + +#include +#include +#include +#include + +/* 先桩头后生产源码(顺序敏感,同 adc_cal 套件) */ +#include "dbg_log.h" + +/* ===================== 内存虚拟文件系统(cfg_store_* 强符号) ===================== */ +#include "cfg_file_store.h" + +#define VFS_CAP 8192u + +static uint8_t s_vfs[VFS_CAP]; +static size_t s_vfs_len = 0u; +static uint32_t s_vfs_mtime = 100u; +static int s_vfs_exists = 0; +static int s_vfs_writes = 0; + +void *cfg_store_alloc(size_t size) { return malloc(size); } +void cfg_store_free(void *p_buf) { free(p_buf); } +uint32_t cfg_store_heap_free(void) { return 0u; } + +int cfg_store_read(const char *p_path, uint8_t *p_buf, size_t cap, size_t *p_nread) { + size_t n; + + (void)p_path; + if (!s_vfs_exists) { + return -1; + } + n = s_vfs_len; + if (n > cap) { + n = cap; + } + (void)memcpy(p_buf, s_vfs, n); + *p_nread = n; + return 0; +} + +int cfg_store_write_atomic(const char *p_path, const uint8_t *p_buf, size_t len) { + (void)p_path; + if (len > VFS_CAP) { + return -1; + } + (void)memcpy(s_vfs, p_buf, len); + s_vfs_len = len; + s_vfs_exists = 1; + s_vfs_mtime++; + s_vfs_writes++; + return 0; +} + +int cfg_store_stat(const char *p_path, cfg_file_stat_t *p_st) { + (void)p_path; + if (!s_vfs_exists) { + return -1; + } + p_st->size = (uint32_t)s_vfs_len; + p_st->mtime = s_vfs_mtime; + return 0; +} + +int cfg_store_remove(const char *p_path) { + (void)p_path; + s_vfs_exists = 0; + s_vfs_len = 0u; + return 0; +} + +const char *cfg_store_config_path(void) { return "/config.ini"; } +const char *cfg_store_tmp_path(void) { return "/config.tmp"; } + +/* ---- sys_log 桩 ---- */ +#include "sys_log.h" + +static int s_syslog_count = 0; + +void syslog_post(syslog_id_t id, const char *desc) { + (void)id; + (void)desc; + s_syslog_count++; +} + +/* ---- adc_input_desc 桩:V0.1 权威映射(docs/硬件平台/ADC通道映射说明.md) ---- */ +#include "adc_input.h" + +static const adc_channel_desc_t s_desc_stub[ADC_SEM_COUNT] = { + { ADC_SRC_TPAFE, 6u }, /* UA = buf[6] */ + { ADC_SRC_STM32, 14u }, /* UB = CH14(PC4) */ + { ADC_SRC_STM32, 3u }, /* UC = CH3 */ + { ADC_SRC_TPAFE, 5u }, /* UN = buf[5] */ + { ADC_SRC_TPAFE, 2u }, /* IA = buf[2] */ + { ADC_SRC_STM32, 6u }, /* IB = CH6 */ + { ADC_SRC_STM32, 1u }, /* IC = CH1 */ + { ADC_SRC_STM32, 2u }, /* IN = CH2 */ + { ADC_SRC_STM32, 5u }, /* TEMP = CH5 */ +}; + +const adc_channel_desc_t *adc_input_desc(adc_semantic_t ch) { + if ((int)ch < 0 || (unsigned)ch >= (unsigned)ADC_SEM_COUNT) { + return NULL; + } + return &s_desc_stub[(unsigned)ch]; +} + +/* dbg 计数定义(stub/dbg_log.h extern 声明) */ +int g_rptest_err_count = 0; +int g_rptest_info_count = 0; +int g_rptest_debug_count = 0; + +/* ===================== 测试框架(极简,同 adc_cal 套件) ===================== */ +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) + +#define ASSERT_NEAR(a, b, eps, name) CHECK(fabsf((float)(a) - (float)(b)) <= (eps), name) + +/* ===================== 被测代码 ===================== */ +#include "cfg_file.c" +#include "adc_cal.c" +#include "run_par.c" + +static void vfs_reset(void) { + s_vfs_len = 0u; + s_vfs_mtime = 100u; + s_vfs_exists = 0; +} + +/* 全复位:引擎段复位 + run_par 复位 + 标定复位(重注册被拒 = 预期,清计数) */ +static void reset_all_rp(void) { + run_par_init(); + adc_cal_init(); + g_rptest_err_count = 0; +} +/* ===================== 测试用例 ===================== */ + +static void tc_defaults(void) +{ + uint8_t i; + + vfs_reset(); + reset_all_rp(); + for (i = 0u; i < 4u; i++) { + CHECK(fabsf(run_par_deadband(i) - 0.5f) <= 1e-6f, "TC-RUNPAR-001 voltage ch factory 0.5 V"); + } + for (i = 4u; i < 8u; i++) { + CHECK(fabsf(run_par_deadband(i) - 0.005f) <= 1e-9f, "TC-RUNPAR-001 current ch factory 0.005 A"); + } + CHECK(fabsf(run_par_deadband(8u) - 0.5f) <= 1e-6f, "TC-RUNPAR-001 freq factory 0.5 Hz"); + CHECK(run_par_deadband(9u) == 0.0f, "TC-RUNPAR-001 idx guard returns zero"); + CHECK(run_par_dirty_peek() == 0u, "TC-RUNPAR-001 init leaves clean"); +} + +static void tc_set_range(void) +{ + volatile float fnan; + + vfs_reset(); + reset_all_rp(); + CHECK((run_par_deadband_set(0u, 0.7f) == 0) + && (fabsf(run_par_deadband(0u) - 0.7f) <= 1e-6f), + "TC-RUNPAR-002 write round trip voltage domain"); + CHECK((run_par_deadband_set(4u, 0.02f) == 0) + && (fabsf(run_par_deadband(4u) - 0.02f) <= 1e-6f), + "TC-RUNPAR-002 write round trip current domain"); + CHECK(run_par_deadband_set(0u, 50.0f) == 0, "TC-RUNPAR-002 voltage cap endpoint inclusive"); + CHECK((run_par_deadband_set(0u, 50.001f) == -1) + && (fabsf(run_par_deadband(0u) - 50.0f) <= 1e-6f), + "TC-RUNPAR-002 over voltage cap rejected"); + CHECK(run_par_deadband_set(4u, 1.001f) == -1, "TC-RUNPAR-002 current cap 1 A enforced"); + CHECK(run_par_deadband_set(8u, 5.001f) == -1, "TC-RUNPAR-002 freq cap 5 Hz enforced"); + CHECK(run_par_deadband_set(0u, -0.001f) == -1, "TC-RUNPAR-002 negative rejected"); + fnan = 0.0f; + fnan = fnan / fnan; + CHECK(run_par_deadband_set(0u, fnan) == -1, "TC-RUNPAR-002 NaN rejected"); + CHECK(run_par_deadband_set(9u, 1.0f) == -1, "TC-RUNPAR-002 idx guard rejected"); + CHECK((run_par_deadband_set(2u, 0.1234f) == 0) + && (fabsf(run_par_deadband(2u) - 0.123f) <= 1e-6f), + "TC-RUNPAR-002 milli quantization rounds to nearest"); +} + +static void tc_persist(void) +{ + char probe[24]; + + vfs_reset(); + reset_all_rp(); + CHECK(run_par_store_save() == 0, "TC-RUNPAR-003 clean save is no-op ok"); + CHECK(run_par_deadband_set(2u, 0.9f) == 0, "TC-RUNPAR-003 seed write ok"); + CHECK(run_par_dirty_peek() == 1u, "TC-RUNPAR-003 write marks dirty"); + CHECK((run_par_store_save() == 0) && (run_par_dirty_peek() == 0u), + "TC-RUNPAR-003 save clears dirty on success"); + (void)memset(probe, 0, sizeof(probe)); + (void)memcpy(probe, "db_uc=", 6u); + CHECK((s_vfs_exists == 1) && (strstr((const char *)s_vfs, "db_uc=900") != NULL), + "TC-RUNPAR-003 file carries milli value"); + CHECK(strstr((const char *)s_vfs, "db_freq=500") != NULL, + "TC-RUNPAR-003 all nine keys present (freq spot check)"); + /* 往返:改内存 → 落盘 → 引擎 load 回读 */ + CHECK(run_par_deadband_set(4u, 0.02f) == 0, "TC-RUNPAR-003 second seed"); + (void)run_par_store_save(); + run_par_deadband_set(4u, 0.5f); /* 覆盖内存态 */ + CHECK(cfg_file_load() == 0, "TC-RUNPAR-003 engine reload ok"); + CHECK(fabsf(run_par_deadband(4u) - 0.02f) <= 1e-9f, + "TC-RUNPAR-003 reload restores last saved value"); + /* set(4,0.5) 是一次未落盘的本地整定 → 脏必须活过 reload(reload 不是 + * 清脏事件;只有 save 成功才清) */ + CHECK(run_par_dirty_peek() == 1u, "TC-RUNPAR-003 unsaved local edit survives reload"); + /* 外部改档(FTP 手改 config.ini 的现场形态):load 后副本即生效值 */ + { + static const char s_edited[] = + "[run_par]\ndb_ua=250\ndb_ub=500\ndb_uc=500\ndb_un=500\n" + "db_ia=10\ndb_ib=5\ndb_ic=5\ndb_in=5\ndb_freq=800\n"; + (void)memcpy(s_vfs, s_edited, sizeof(s_edited) - 1u); + s_vfs_len = sizeof(s_edited) - 1u; + s_vfs_exists = 1; + s_vfs_mtime++; + } + CHECK(cfg_file_load() == 0, "TC-RUNPAR-003 edited file accepted"); + CHECK((fabsf(run_par_deadband(0u) - 0.25f) <= 1e-6f) + && (fabsf(run_par_deadband(4u) - 0.01f) <= 1e-9f) + && (fabsf(run_par_deadband(8u) - 0.8f) <= 1e-6f), + "TC-RUNPAR-003 edited values live in copy immediately"); + /* 越界文件值:引擎照收(int32 域),104/COV 读到超程值 —— 守卫在**写路径**, + * 文件通路的防线是"人工编辑 + 沉降去抖 + 可再改回";与 adc_cal 合法带不同, + * 死区超程不毁量纲(只是上送变钝),此处钉住"引擎不吞值"即可 */ + CHECK(fabsf(run_par_deadband(0u) - 0.25f) <= 1e-6f, "TC-RUNPAR-003 value stable re-read"); +} + +static void tc_104_read(void) +{ + float v; + + vfs_reset(); + reset_all_rp(); + (void)run_par_deadband_set(0u, 0.6f); + CHECK((run_par_104_read(0x8030u, &v) == 0) && (fabsf(v - 0.6f) <= 1e-6f), + "TC-RUNPAR-004 db first slot hit"); + CHECK((run_par_104_read(0x8038u, &v) == 0) && (fabsf(v - 0.5f) <= 1e-6f), + "TC-RUNPAR-004 db last slot (freq) hit"); + CHECK(run_par_104_read(0x8039u, &v) == -1, "TC-RUNPAR-004 gap address not owned"); + CHECK(run_par_104_read(0x8027u, &v) == -1, "TC-RUNPAR-004 prot_set alarm addr untouched"); + CHECK(run_par_104_read(0x802Fu, &v) == -1, "TC-RUNPAR-004 below base not claimed"); + CHECK((run_par_104_read(0x8040u, &v) == 0) && (fabsf(v - 152.587890625f) <= 1e-4f), + "TC-RUNPAR-004 k echo = adc_cal effective UA"); + CHECK((run_par_104_read(0x8047u, &v) == 0) && (fabsf(v - 805.6640625f) <= 1e-4f), + "TC-RUNPAR-004 k echo last channel (IN, STM32 ROM)"); + CHECK((run_par_104_read(0x8048u, &v) == 0) && (v == 0.0f), + "TC-RUNPAR-004 b echo factory zero"); + CHECK(run_par_104_read(0x8050u, &v) == -1, "TC-RUNPAR-004 above b block not owned"); +} + +static void tc_104_two_step(void) +{ + float v; + + vfs_reset(); + reset_all_rp(); + CHECK((run_par_104_write(0x8031u, 0.8f, 1u) == 0) + && (fabsf(run_par_deadband(1u) - 0.5f) <= 1e-6f) + && (run_par_dirty_peek() == 0u), + "TC-RUNPAR-005 select validates without commit or dirty"); + CHECK((run_par_104_write(0x8031u, 0.8f, 0u) == 0) + && (fabsf(run_par_deadband(1u) - 0.8f) <= 1e-6f) + && (run_par_dirty_peek() == 1u), + "TC-RUNPAR-005 execute commits and marks dirty"); + CHECK(run_par_104_write(0x8031u, 60.0f, 1u) == -1, "TC-RUNPAR-005 range reject in select"); + CHECK(run_par_104_write(0x8039u, 1.0f, 0u) == -2, "TC-RUNPAR-005 foreign addr passes through"); + CHECK(run_par_104_write(0x8027u, 1.0f, 0u) == -2, "TC-RUNPAR-005 alarm addr passes through"); + CHECK((run_par_104_read(0x8031u, &v) == 0) && (fabsf(v - 0.8f) <= 1e-6f), + "TC-RUNPAR-005 committed value readable at same addr"); +} + +static void tc_104_cal_proxy(void) +{ + float v; + + vfs_reset(); + reset_all_rp(); + CHECK(run_par_104_write(0x8040u, 300.0f, 0u) == 0, "TC-RUNPAR-006 k in-band accepted"); + CHECK((run_par_104_read(0x8040u, &v) == 0) && (fabsf(v - 300.0f) <= 0.01f), + "TC-RUNPAR-006 k echo follows adc_cal_set"); + CHECK((run_par_104_write(0x8040u, 400.0f, 0u) == -1) + && (fabsf(adc_cal_k_uv(ADC_SEM_UA) - 300.0f) <= 0.01f), + "TC-RUNPAR-006 k over band rejected by adc_cal, RAM intact"); + CHECK((run_par_104_write(0x8040u, 70.0f, 0u) == -1), + "TC-RUNPAR-006 k under band rejected (0.5x ROM)"); + CHECK(run_par_104_write(0x8040u, -1.0f, 0u) == -1, "TC-RUNPAR-006 non-positive k rejected"); + CHECK((run_par_104_write(0x8048u, -5.0f, 0u) == 0) + && (fabsf(adc_cal_b_codes(ADC_SEM_UA) + 5.0f) <= 1e-3f) + && (fabsf(adc_cal_k_uv(ADC_SEM_UA) - 300.0f) <= 0.01f), + "TC-RUNPAR-006 b single-side RMW keeps k"); + CHECK(run_par_104_write(0x8048u, -9000.0f, 0u) == -1, "TC-RUNPAR-006 b over +-8192 rejected"); + CHECK((run_par_104_write(0x8040u, 200.0f, 1u) == 0) + && (fabsf(adc_cal_k_uv(ADC_SEM_UA) - 300.0f) <= 0.01f), + "TC-RUNPAR-006 k select does not touch adc_cal"); + /* 生效值视图回显:文件塞越带 k → 哨兵回退 ROM → 104 回显 ROM */ + { + static const char s_badk[] = + "[adc_cal]\nk_ua=99011483\nvalid=1\n"; + (void)memcpy(s_vfs, s_badk, sizeof(s_badk) - 1u); + s_vfs_len = sizeof(s_badk) - 1u; + s_vfs_exists = 1; + s_vfs_mtime++; + } + CHECK(cfg_file_load() == 0, "TC-RUNPAR-006 poisoned file loaded by engine"); + CHECK((run_par_104_read(0x8040u, &v) == 0) && (fabsf(v - 152.587890625f) <= 1e-4f), + "TC-RUNPAR-006 echo = effective (ROM fallback), not raw file value"); + reset_all_rp(); + CHECK(cfg_file_save() == 0, "TC-RUNPAR-006 reset file rewritten"); +} + +static void tc_init_idempotent(void) +{ + int err_before; + + vfs_reset(); + reset_all_rp(); + (void)run_par_deadband_set(1u, 0.9f); + (void)run_par_104_write(0x8040u, 200.0f, 0u); + err_before = g_rptest_err_count; + run_par_init(); + CHECK(fabsf(run_par_deadband(1u) - 0.5f) <= 1e-6f, "TC-RUNPAR-007 re-init restores defaults"); + CHECK(run_par_dirty_peek() == 0u, "TC-RUNPAR-007 re-init clears dirty"); + CHECK(g_rptest_err_count > err_before, "TC-RUNPAR-007 dup register leaves trace"); + CHECK(fabsf(run_par_deadband(0u) - 0.5f) <= 1e-6f, "TC-RUNPAR-007 values intact after dup reg"); +} + +static void tc_copy_is_effective(void) +{ + static const char s_file[] = + "[run_par]\ndb_ua=650\ndb_ub=500\ndb_uc=500\ndb_un=500\n" + "db_ia=5\ndb_ib=5\ndb_ic=5\ndb_in=5\ndb_freq=500\n"; + + vfs_reset(); + reset_all_rp(); + (void)memcpy(s_vfs, s_file, sizeof(s_file) - 1u); + s_vfs_len = sizeof(s_file) - 1u; + s_vfs_exists = 1; + s_vfs_mtime++; + CHECK(cfg_file_load() == 0, "TC-RUNPAR-008 load ok"); + CHECK(fabsf(run_par_deadband(0u) - 0.65f) <= 1e-6f, + "TC-RUNPAR-008 no cache layer: copy read is effective value"); + CHECK(run_par_dirty_peek() == 0u, "TC-RUNPAR-008 load itself is not a local edit"); +} + +int main(void) +{ + printf("=== RunPar Layer Host Tests ===\n"); + tc_defaults(); + tc_set_range(); + tc_persist(); + tc_104_read(); + tc_104_two_step(); + tc_104_cal_proxy(); + tc_init_idempotent(); + tc_copy_is_effective(); + printf("=== RunPar Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/run_par/run_unit_test.py b/test/host/run_par/run_unit_test.py new file mode 100644 index 00000000..ba86f60e --- /dev/null +++ b/test/host/run_par/run_unit_test.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""运行参数段(run_par,「数据可信」卡 10)主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么:用本机 MSVC 离线编译 test/host/run_par/run_par_test.c(#include 纳入 + run_par.c + cfg_file.c + adc_cal.c 真实链 + 内存虚拟盘桩),钉死 [run_par] + 注册/工厂默认/落盘往返、104 地址映射两段式、k/b 覆盖直通 adc_cal 合法带、 + 生效值视图回显与 init 幂等。 + +用法(仓库根目录): + & '.venv\\Scripts\\python.exe' test/host/run_par/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误。 + +设计依据:docs/任务规划/数据可信.md(卡 10)+ docs/子系统设计/IEC104上行设计说明.md §3.4。 +创建日期:2026-10-25(道D 卡 10) +""" +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 + +# 顺序敏感:stub 先于生产目录(dbg_log.h → 计数宏桩);App/cfg + App/log 真头 +# 供 cfg_file.h / cfg_file_store.h / sys_log.h;adc_input_desc / syslog_post / +# cfg_store_* 内存虚拟盘由测试单元给强符号桩(同 adc_cal 套件先例)。 +BUILD = TestBuild( + name="RunPar Layer Tests", + src=os.path.join(HERE, "run_par_test.c"), + include_dirs=[ + os.path.join(HERE, "stub"), + os.path.join(ROOT, "App", "algo"), + os.path.join(ROOT, "App", "cfg"), + os.path.join(ROOT, "App", "log"), + HERE, + ], + summary_marker="=== RunPar Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/run_par/stub/dbg_log.h b/test/host/run_par/stub/dbg_log.h new file mode 100644 index 00000000..53496f6e --- /dev/null +++ b/test/host/run_par/stub/dbg_log.h @@ -0,0 +1,20 @@ +/* + * 主机侧单测桩:dbg_log.h(run_par 套件专用) + * 模块功能:为 cfg_file.c / adc_cal.c / run_par.c 提供主机可编译的日志宏 + * (映射为计数)。⚠ 仅供 test/host/run_par/ 使用,不参与 MCU 构建。 + * 模式同 test/host/adc_cal/stub/ 先例。 + * 创建日期:2026-10-25(道D 卡 10) + */ + +#ifndef DBG_LOG_H +#define DBG_LOG_H + +extern int g_rptest_err_count; +extern int g_rptest_info_count; +extern int g_rptest_debug_count; + +#define DBG_INFO(fmt, ...) do { g_rptest_info_count++; } while (0) +#define DBG_DEBUG(fmt, ...) do { g_rptest_debug_count++; } while (0) +#define DBG_ERROR(fmt, ...) do { g_rptest_err_count++; } while (0) + +#endif /* DBG_LOG_H */