feat(ui+记录): 整定写路径与四类页落地,五类记录写入方补齐
UI 面:ui_data 增加深度 4 参数写队列(请求 168 B、队列 .bss 672 B)与状态机 OK/ERR_RANGE/ERR_LOCKED/ERR_BUSY,ui_data_feed 出队后按注入 ops 走「闭锁 → 整批 预检 → 逐项 prot_set_set_sealed」,只提交改动位图;par 帧尾追加 cal_block (口径 = UA/UN/IA 三路 ADC_CAL_F_VALID,与 protect_task 判据同构,不用 rt.eng 反推,V0.1 下 eng 恒 0)、UI_DATA_VER 升到 2。参数/定值/控制字/软压板四叶绑定 内容页(UI_PAGE_MAP_COUNT 12→16,复用四个查看模块,零新增 .c),编辑态按 is_editable 分派,LOCK(写不进去)与「未标定」(仍可改值)两源两通道两词。 记录面:syslog_request_clear/state/apply 三段式删除 API(UI 只发请求,落地在 defaultTask 的 drain 尾部且必须后置,卷锁一律 try,幂等只认 FR_NO_FILE); 事故/告警/启动/操作/遥控五类各有一个真实写入点,记录类型 = 日志 ID 本身 (一类一个 /log 严格 8.3 文件;64B 定长体没有类型字段位,塞字段会砸掉既有读者)。 为守 protectTask 872 B 静态最坏链另拆 syslog_post_quiet()(无 DBG 的投递核, 丢弃留痕挪到 drain 侧差值补发),syslog_post 对外语义逐字不变。 为什么两道合成一笔而不是各一笔(实测,不是省事):simulator/main.c 同时消费 两道新增 API(整定 pump 与 logtest 清除用例),按道切文件会让中间提交编不过; RAM 受管快照与体积同码凭证描述的是合并后的 .map,拆开必落在「台账与 .map 不同 码」这条红线上。pre-commit 跑钩子前会 stash 未 staged 部分,这类耦合没法绕。 实测:@build 0 错误 0 警告;Code 228,260 / RO 24,960 / RW 1,440 / ZI 135,976 B; RW_IRAM1 79,992 B = 61.0%、RW_CCM 87.9%(阈值 92%);九个任务静态最坏链全 PASS, protectTask 872 B / 57% 改前后逐字节不变(记录投递没垫栈)。主机单测 ui_data 82→ 164、sys_log_mcu 106→137、prot_elm 47→60、iec104 141→154、iec104_serv 28→34, 既有断言零删改;48 个钩子 --all-files 全绿;编号门禁可判定 86.34%(基线 86.0%)、 不可判定 1,153 处低于上限 1,166;doc-links 无法判定 0;UI 黄金 18 页扩到 27 页 连跑两次逐字节一致;字库 diff 空。 顺手修掉三处既有缺陷(都是本轮第一个消费者才暴露):sim_syslog.c 的 s_paths 行宽 16 而 "sim_selftest.log" 正好 16 字符没有 NUL 位(stat/remove 读越过数组尾,此前 靠后面 .rodata 碰巧为 0);ui_w_setlist 二态行 ENTER 不回绕(控制字与软压板出厂值 全在最后一项,按了像没反应而叠层里已是越界值);生成物 ui_menu_tab.h 与 gen_menu.py 模板漂移(上一笔改了模板没重跑生成器),现随生成器输出对齐。 未收口(不算完成):保存后重启值保持与主站 C_RD 读回两条只能上板,台架窗口执行; LOCK 屏显与越界反显的页面侧在模拟器造不出来(模拟器不跑定值自检、无 NAND), 已由生成侧单测与代码注释点名差距;同类页数与钩子数陈述残留另立待办。
This commit is contained in:
+68
-10
@@ -18,12 +18,20 @@ typedef struct {
|
||||
uint8_t over; /* 1 = 过量(越上限动作),0 = 欠量(越下限动作) */
|
||||
uint8_t src; /* 测量源:0 = phasor 基波有效值,1 = freq 测频 */
|
||||
prot_set_id_t en_id; /* 投退定值 */
|
||||
uint8_t trip_ctl; /* 跳闸投退定值项(PS_*_TRIP);PROT_ELM_NO_TRIP =
|
||||
* 该元件没有这一项(告警类)→ 动作只出告警记录。
|
||||
* 用 uint8_t 而不是 prot_set_id_t 就是因为要能表达
|
||||
* "无此项"这个第 41 种取值(PS_ID_COUNT 是数组长度,
|
||||
* 拿去喂 prot_set_get_ctl() 会越界读) */
|
||||
prot_set_id_t set_id; /* 动作定值 */
|
||||
prot_set_id_t dly_id; /* 延时定值(ms) */
|
||||
prot_set_id_t ret_id; /* 返回系数定值(RET_OVER / RET_UNDER) */
|
||||
const char *pprefix; /* 相量实例名前缀("C01"…);src=1 时为 0(不用) */
|
||||
} pe_cfg_t;
|
||||
|
||||
/* k_pe[].trip_ctl 的"无此定值项"取值(见上方字段注释) */
|
||||
#define PROT_ELM_NO_TRIP 0xFFu
|
||||
|
||||
/* 测量源枚举(pe_cfg_t.src) */
|
||||
#define PE_SRC_PHASOR 0u
|
||||
#define PE_SRC_FREQ 1u
|
||||
@@ -31,18 +39,24 @@ typedef struct {
|
||||
/* freq 无有效读数时给欠量元件的"必不启动"填充值(远大于任何欠量定值上限) */
|
||||
#define PE_MEAS_BIG 1e30f
|
||||
|
||||
static const pe_cfg_t k_pe[PE_COUNT] = {
|
||||
{ "OC1-IA", 1u, PE_SRC_PHASOR, PS_OC1_EN, PS_OC1_CUR, PS_OC1_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OC2-IA", 1u, PE_SRC_PHASOR, PS_OC2_EN, PS_OC2_CUR, PS_OC2_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OC3-IA", 1u, PE_SRC_PHASOR, PS_OC3_EN, PS_OC3_CUR, PS_OC3_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OL-IA", 1u, PE_SRC_PHASOR, PS_OL_EN, PS_OL_CUR, PS_OL_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OV-UA", 1u, PE_SRC_PHASOR, PS_OV_EN, PS_OV_VOL, PS_OV_DLY, PS_RET_OVER, "C02" },
|
||||
{ "UV-UA", 0u, PE_SRC_PHASOR, PS_UV_EN, PS_UV_VOL, PS_UV_DLY, PS_RET_UNDER, "C02" },
|
||||
{ "3U0-UN", 1u, PE_SRC_PHASOR, PS_3U0_EN, PS_3U0_VOL, PS_3U0_DLY, PS_RET_OVER, "C03" },
|
||||
/* 列序 = pe_cfg_t 字段序(name, over, src, en_id, trip_ctl, set_id, dly_id, ret_id,
|
||||
* pprefix)。⚠ trip_ctl 紧跟 en_id 插入,新增元件必须照这一列的口径填:有跳闸投退
|
||||
* 定值项的填 PS_*_TRIP,没有的填 PROT_ELM_NO_TRIP(= 该元件动作只出告警记录)。*/
|
||||
static const pe_cfg_t k_pe[PE_COUNT] = { /* ← 表头注释见上,行内列序与之一致 */
|
||||
{ "OC1-IA", 1u, PE_SRC_PHASOR, PS_OC1_EN, PS_OC1_TRIP, PS_OC1_CUR, PS_OC1_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OC2-IA", 1u, PE_SRC_PHASOR, PS_OC2_EN, PS_OC2_TRIP, PS_OC2_CUR, PS_OC2_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OC3-IA", 1u, PE_SRC_PHASOR, PS_OC3_EN, PS_OC3_TRIP, PS_OC3_CUR, PS_OC3_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OL-IA", 1u, PE_SRC_PHASOR, PS_OL_EN, PS_OL_TRIP, PS_OL_CUR, PS_OL_DLY, PS_RET_OVER, "C01" },
|
||||
/* ↓ 三项 + 下面两项频率元件:参考系统的告警段(0x8027~0x802A)本就没有「跳闸
|
||||
投退」这一档,g_prot_set_meta 也无对应项 → PROT_ELM_NO_TRIP,动作一律归告警。
|
||||
这不是兜底猜测,是照实反映("哪些元件算跳闸"的真源始终是这张表 + 定值表)。*/
|
||||
{ "OV-UA", 1u, PE_SRC_PHASOR, PS_OV_EN, PROT_ELM_NO_TRIP, PS_OV_VOL, PS_OV_DLY, PS_RET_OVER, "C02" },
|
||||
{ "UV-UA", 0u, PE_SRC_PHASOR, PS_UV_EN, PROT_ELM_NO_TRIP, PS_UV_VOL, PS_UV_DLY, PS_RET_UNDER, "C02" },
|
||||
{ "3U0-UN", 1u, PE_SRC_PHASOR, PS_3U0_EN, PROT_ELM_NO_TRIP, PS_3U0_VOL, PS_3U0_DLY, PS_RET_OVER, "C03" },
|
||||
/* 过频/低频(L3-5,B8):测量量 = freq 层测得频率(Hz),返回系数独立
|
||||
* (RET_OF/RET_UF——频率滞回带与电气量不同量级,勿共用 0.95/1.05) */
|
||||
{ "OF-F", 1u, PE_SRC_FREQ, PS_OF_EN, PS_OF_HZ, PS_OF_DLY, PS_RET_OF, 0 },
|
||||
{ "UF-F", 0u, PE_SRC_FREQ, PS_UF_EN, PS_UF_HZ, PS_UF_DLY, PS_RET_UF, 0 },
|
||||
{ "OF-F", 1u, PE_SRC_FREQ, PS_OF_EN, PROT_ELM_NO_TRIP, PS_OF_HZ, PS_OF_DLY, PS_RET_OF, 0 },
|
||||
{ "UF-F", 0u, PE_SRC_FREQ, PS_UF_EN, PROT_ELM_NO_TRIP, PS_UF_HZ, PS_UF_DLY, PS_RET_UF, 0 },
|
||||
};
|
||||
|
||||
/* 运行时状态(picked/asserted 的对外快照 + 延时起点 + 相量实例索引缓存) */
|
||||
@@ -259,6 +273,50 @@ uint32_t prot_elm_active_mask(void) {
|
||||
return mask;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:聚合启动位图
|
||||
* 入口参数:无
|
||||
* 返 回 值:bit i = 元件 i picked(越限电平,早于/等于 asserted)
|
||||
* 限定条件:任意上下文(只读快照,无锁;与 prot_elm_active_mask 同口径)
|
||||
* 函数说明:调用方是 protectTask 的「保护启动」记录边沿检测(task-10)。本函数
|
||||
* **只聚合电平**,不造边沿——边沿属观测者状态(s_prev_pick 在那一侧),
|
||||
* 判据核里再存一份"上一拍"会让 eval 不再幂等。
|
||||
*/
|
||||
uint32_t prot_elm_pick_mask(void) {
|
||||
uint32_t mask = 0u;
|
||||
uint32_t i;
|
||||
|
||||
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
|
||||
if (s_rt[i].picked != 0u) {
|
||||
mask |= (1uL << i);
|
||||
}
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:判定该元件动作上升沿应归哪一类记录(见 prot_elm.h 同号声明)
|
||||
* 入口参数:id - 元件编号 prot_elm_id_t
|
||||
* 返 回 值:PROT_REC_TRIP / PROT_REC_ALARM / PROT_REC_NONE(id 越界) prot_rec_kind_t
|
||||
* 限定条件:任意上下文;只读,不改任何状态
|
||||
* 函数说明:判据唯一来源 = k_pe[id].trip_ctl 指向的那枚 PS_*_TRIP 投退定值。
|
||||
* trip_ctl == PROT_ELM_NO_TRIP 时**先返回告警**,绝不把 0xFF 喂给
|
||||
* prot_set_get_ctl()(那是数组越界读,会读到定值区外的 RAM——
|
||||
* 与"未标定拿 ADC 引脚电压当二次值"同类的静默错数据形状)。
|
||||
*/
|
||||
prot_rec_kind_t prot_elm_record_kind(prot_elm_id_t id) {
|
||||
uint8_t trip_ctl;
|
||||
|
||||
if (id >= PE_COUNT) {
|
||||
return PROT_REC_NONE;
|
||||
}
|
||||
trip_ctl = k_pe[(uint32_t)id].trip_ctl;
|
||||
if (trip_ctl == (uint8_t)PROT_ELM_NO_TRIP) {
|
||||
return PROT_REC_ALARM;
|
||||
}
|
||||
return (prot_set_get_ctl((prot_set_id_t)trip_ctl) != 0u) ? PROT_REC_TRIP : PROT_REC_ALARM;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取实例名(日志用)
|
||||
* 入口参数:id - 元件编号
|
||||
|
||||
@@ -100,6 +100,43 @@ void prot_elm_state(prot_elm_id_t id, prot_elm_state_t *out);
|
||||
/* 动作位图(bit i = 元件 i asserted),供 L4 出口 / 心跳日志 */
|
||||
uint32_t prot_elm_active_mask(void);
|
||||
|
||||
/* 启动位图(bit i = 元件 i picked),供「保护启动」记录边沿(task-10,2026-10-26):
|
||||
* picked 是越限滞回后的电平,早于(或同拍于)asserted——只看 active_mask 会让
|
||||
* "启动了但没到点"这一整段在记录里彻底消失,而它正是"保护启动"页要看的东西。 */
|
||||
uint32_t prot_elm_pick_mask(void);
|
||||
|
||||
/*
|
||||
* 元件动作边沿的记录归属(task-10「五类记录」)。
|
||||
* ⚠ 本枚举刻意**不**用 syslog_id_t:那会把 App/log 的头拽进 algo 层,而
|
||||
* App/algo/prot_elm.c 同时被 test/host/prot_dim 编进测试单元(那个运行器的
|
||||
* include 路径里没有 App/log,且不在本道写域)→ 一改就在别人的单测上爆错。
|
||||
* "元件 → 哪一类"是判据域事实(属主在本文件),"哪一类 → 哪个日志文件"是
|
||||
* 记录域映射(属主在 App/task/protect_task.c 的一行三元),两边各自单源。
|
||||
*/
|
||||
typedef enum {
|
||||
PROT_REC_TRIP = 0u, /* 跳闸型动作 → 事故记录 */
|
||||
PROT_REC_ALARM, /* 告警型动作 → 保护告警记录 */
|
||||
PROT_REC_NONE /* id 越界 → 调用方一条都不许落 */
|
||||
} prot_rec_kind_t;
|
||||
|
||||
/*
|
||||
* 函数功能:判定"该元件若发生动作上升沿,应归哪一类记录"
|
||||
* 入口参数:id - 元件编号 prot_elm_id_t
|
||||
* 返回值:PROT_REC_TRIP / PROT_REC_ALARM / PROT_REC_NONE(越界) prot_rec_kind_t
|
||||
* 限定条件:任意上下文(只读 k_pe[] + prot_set RAM,不落任何状态)
|
||||
* 函数说明:判据**唯一来源** = 该元件的跳闸投退定值项 g_prot_set_meta 里的
|
||||
* PS_*_TRIP(其枚举注释即"0 告警 / 1 跳闸")。元件→投退项的对应关系
|
||||
* 放在实例表 k_pe[] 的 trip_ctl 列(与 en_id/set_id/dly_id 同一张表,
|
||||
* 新增元件只加一行);**本函数里没有第二份"哪些元件算跳闸"的名单**,
|
||||
* 所以它不会与 g_prot_set_meta / k_pe[] / 整定视图门禁互相打架。
|
||||
* 过压/欠压/零序过压/过频/低频五项本就没有 TRIP 投退项(告警类)→
|
||||
* trip_ctl = PROT_ELM_NO_TRIP → 恒 PROT_REC_ALARM,这是照实反映,
|
||||
* 不是兜底猜测。
|
||||
* 定值巡检闭锁(prot_set_locked)不影响本判定:闭锁态下元件会被复位、
|
||||
* 根本走不到动作上升沿,调用方无需在此重复判闭锁。
|
||||
*/
|
||||
prot_rec_kind_t prot_elm_record_kind(prot_elm_id_t id);
|
||||
|
||||
/* 实例名(ROM 常量,日志用) */
|
||||
const char *prot_elm_name(prot_elm_id_t id);
|
||||
|
||||
|
||||
+163
-17
@@ -5,6 +5,11 @@
|
||||
* (实时量 rt / 参数 par / 装置信息 info),经 Drivers/BSP/UI/ui_data.h
|
||||
* 的发布 API 推给 UI 层;本模块是快照的**唯一填充者**,不含任何布局、
|
||||
* 格式化与 LVGL 调用(显示规格全在页面行表,同文档 §8)
|
||||
* 写方向(道F task-1):同一条 tick 顺带把页面投递的定值整定请求落定——
|
||||
* ui_data_param_drain() 每轮取一条,执行动作经本文件的 s_param_ops
|
||||
* (locked = prot_set_locked / check = g_prot_set_meta 上下限 /
|
||||
* apply = prot_set_set_sealed)注入给 BSP;值域与落盘事实都留在 App
|
||||
* 侧,BSP 只有队列与状态机(契约 §6.1"UI 不能直写"的落地形态)
|
||||
* 量纲诚实性:eng 字段按**真值**填——docs/任务规划/数据可信.md §2.6 把承诺边界定在
|
||||
* "引脚电压",故未做两点标定前 eng = UI_DATA_ENG_PIN(值 = ADC 引脚电压 V,
|
||||
* 不是端子二次值);电气 8 路 adc_cal_flags() 的 VALID 全置位才升
|
||||
@@ -15,6 +20,10 @@
|
||||
* 创建日期:2026-10-25
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 王建锋 创建(道E task-14)
|
||||
* v1.1 2026-10-26 DTU 项目组 道F task-1:① tick 前置 ui_data_param_drain()
|
||||
* (定值写路径出队 → 闭锁/越界预检 → set_sealed,
|
||||
* 见 s_param_ops);② 参数帧尾填 cal_block(未标定
|
||||
* 闭锁徽标,三路口径同 protect_task.c,不用 eng)
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
@@ -93,11 +102,18 @@
|
||||
* (契约 docs/UI设计/UI_数据供给设计说明.md §9.4 第 1/2 条) */
|
||||
typedef char feed_chk_yc_n_t[(UI_DATA_YC_N == (uint32_t)ADC_SEM_COUNT) ? 1 : -1];
|
||||
typedef char feed_chk_set_n_t[(UI_DATA_SET_N == (uint32_t)PS_ID_COUNT) ? 1 : -1];
|
||||
/* 写路径注入表的 id 形参是 uint8_t(BSP 侧不透明编号宽度,位图只有 64 位):
|
||||
* 定值项数一旦破 64/255,request 的 mask 与 check/apply 的编号都会静默截断,
|
||||
* 故在写侧也钉一道(BSP 侧同一条守卫在 Drivers/BSP/UI/ui_data.c) */
|
||||
typedef char feed_chk_set_u8_t[((uint32_t)PS_ID_COUNT <= 64u) ? 1 : -1];
|
||||
typedef char feed_chk_fuser_t[(UI_DATA_FUSER_LEN >= FTP_CFG_USER_MAX) ? 1 : -1];
|
||||
|
||||
/* 静态变量定义区 */
|
||||
|
||||
/* 组装缓冲:三帧串行填充,共用一块存储(取最大者 = 参数帧),省两份 RAM */
|
||||
/* 组装缓冲:三帧串行填充,共用一块存储,省两份 RAM。
|
||||
* 实际最大者是**实时帧**(208 B > par 192 B > info),故给 par 尾追加字段
|
||||
* 只要不超过 rt 就一字节 RAM 都不多花(这条由主机单测 TC-UIDATA-021g/h 钉住,
|
||||
* 尺寸一变这里就是唯一会撒谎的地方) */
|
||||
static union {
|
||||
ui_data_rt_t rt;
|
||||
ui_data_par_t par;
|
||||
@@ -109,10 +125,17 @@ static union {
|
||||
static int8_t s_yc_phasor[ADC_SEM_COUNT];
|
||||
static uint8_t s_map_done;
|
||||
|
||||
/* 发布节拍与参数变化检测状态 */
|
||||
/* 发布节拍与参数变化检测状态
|
||||
* par 帧有**三个各自独立**的变化源,任一变了就要重发一帧(详见 tick 里的门控注释):
|
||||
* s_par_sum 定值改动(校验和)
|
||||
* s_par_locked 定值闭锁(L5-1 自检失配,校验和不动)
|
||||
* s_par_cal_block 未标定闭锁(卡 12 标定完成,校验和也不动)
|
||||
* 后两个各占一字节——省下来就是"屏上状态与真值脱拍"这笔债 */
|
||||
static uint32_t s_last_rt_ms;
|
||||
static uint32_t s_par_sum;
|
||||
static uint8_t s_par_sum_valid;
|
||||
static uint8_t s_par_locked;
|
||||
static uint8_t s_par_cal_block;
|
||||
static uint8_t s_first_done;
|
||||
|
||||
/* 函数定义区 */
|
||||
@@ -386,8 +409,96 @@ static void feed_fill_rt(ui_data_rt_t *p_snap, uint32_t tick_ms) {
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 写路径(契约 §6.2):把 prot_set 的三个动作包成注入给 BSP 的函数表 ────── */
|
||||
|
||||
/*
|
||||
* 函数功能:组装参数快照(40 项定值 Q10 + 校验和 + 封存态 + 越界位图)
|
||||
* 函数功能:写路径判定"定值闭锁中吗"(BSP 经 ui_data_param_ops_t 来问)
|
||||
* 入口参数:无
|
||||
* 返 回 值:1 - 闭锁(定值校验和自检失败态,禁一切定值写入);0 - 可写 uint8_t
|
||||
* 限定条件:uiTask 上下文(只读 prot_set 状态位)
|
||||
* 函数说明:这里的"闭锁"是**定值闭锁**(= par.locked = YX31 遥信),**不是**未标定
|
||||
* 闭锁(= par.cal_block):后者仍可改定值、只是判据停走,两者在屏上不得
|
||||
* 互认(三词三源口径 docs/任务规划/数据可信.md §4.3 + 落地简报 §7-6)
|
||||
*/
|
||||
static uint8_t feed_param_locked(void) {
|
||||
return prot_set_locked();
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写路径整批预检——单项是否落在 meta 上下限内
|
||||
* 入口参数:a_id - 定值项编号(位图位序;uint8_t 是 BSP 侧不透明编号的宽度)
|
||||
* a_q10 - 待写 Q10 定点值
|
||||
* 返 回 值:0 - 合法;-1 - 编号越界或值越界 int
|
||||
* 限定条件:只读 g_prot_set_meta(ROM 表),任意上下文安全
|
||||
* 函数说明:上下限**只此一处取**——BSP 行表(Drivers/BSP/UI/ui_w_setview.c)是它的
|
||||
* 换算镜像,由 setview-check 钩子核一致;本函数再抄一份数字就是第二真相
|
||||
* ("查看合法、整定越界"两套边界就是这么来的)
|
||||
*/
|
||||
static int feed_param_check(uint8_t a_id, int32_t a_q10) {
|
||||
if (a_id >= (uint8_t)PS_ID_COUNT) {
|
||||
return -1;
|
||||
}
|
||||
if ((a_q10 < g_prot_set_meta[a_id].min) || (a_q10 > g_prot_set_meta[a_id].max)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写路径落值——单项**原子**写入 + 重封校验和 + 置脏
|
||||
* 入口参数:a_id - 定值项编号;a_q10 - Q10 定点值
|
||||
* 返 回 值:0 - 已写入;-1 - 编号越界或 prot_set 拒写(原值不动) int
|
||||
* 限定条件:uiTask 上下文;调用前 BSP 已整批预检通过(两条件与本函数一致)
|
||||
* 函数说明:用 prot_set_set_sealed()(影子拷贝→改→重封→单字交换,**每项自带置脏**,
|
||||
* 落盘由 defaultTask 50 ms 圈扫脏写 /prot_set.bin,本模块不碰 FatFs)。
|
||||
* ⚠ 不得改成 prot_set_set() + 批末 prot_set_seal():两条路径之间有
|
||||
* "已改未封存"窗口,prot_set.c 明令不得交错,撞上就被 L5-1 自检判成
|
||||
* "定值被非法改写"→ 整台保护闭锁
|
||||
*/
|
||||
static int feed_param_apply(uint8_t a_id, int32_t a_q10) {
|
||||
if (a_id >= (uint8_t)PS_ID_COUNT) {
|
||||
return -1;
|
||||
}
|
||||
return prot_set_set_sealed((prot_set_id_t)a_id, a_q10);
|
||||
}
|
||||
|
||||
/* 注入给 ui_data_param_drain() 的执行侧实现(BSP 不保存副本,每次调用现传) */
|
||||
static const ui_data_param_ops_t s_param_ops = {
|
||||
feed_param_locked,
|
||||
feed_param_check,
|
||||
feed_param_apply
|
||||
};
|
||||
|
||||
/*
|
||||
* 函数功能:未标定闭锁判定(整定页"未标定"徽标的数据源)
|
||||
* 入口参数:无
|
||||
* 返 回 值:1 - 未标定闭锁(UA/UN/IA 三路至少一路标定 VALID 未置位);0 - 三路全
|
||||
* VALID(保护判据正在跑) uint8_t
|
||||
* 限定条件:uiTask 上下文(adc_cal_flags 纯读,每 200 ms 一次,代价可忽略)
|
||||
* 函数说明:判据与 App/task/protect_task.c 的 s_cal_locked() **逐字同构**——那是
|
||||
* 停走判据的真源,本函数只是把它搬到显示通道上;两处的例外通道也必须
|
||||
* 一起改(DTU_BENCH 台架旁路,仅本地台架自行定义、产品恒强制)。
|
||||
* ⚠ 已知重复:本仓没有导出该判据的公开 API(protect_task.h 只有线程
|
||||
* 入口),故这里镜像一份;将来 protect_task 导出访问器后应收敛成一处
|
||||
* (Lead 补充 A 明确本任务按此实现,不改 protect_task.*)
|
||||
* ⚠ 不得用 rt.eng 反推:eng 要求电气 8 路全 VALID,V0.1 双转换器下
|
||||
* UB/UC/IB/IC/IN 永不置位 → eng 恒为引脚档,照它画徽标就永不解锁
|
||||
*/
|
||||
static uint8_t feed_cal_block(void) {
|
||||
/* 形态与 protect_task.c 同:用 #ifndef 把产品默认路径留在主分支(而非 #ifdef
|
||||
* 双分支)——cppcheck 对未定义宏取真分支,双分支会把它引到旁路上报假信号 */
|
||||
#ifndef DTU_BENCH
|
||||
if (((adc_cal_flags(ADC_SEM_UA) & ADC_CAL_F_VALID) == 0u) ||
|
||||
((adc_cal_flags(ADC_SEM_UN) & ADC_CAL_F_VALID) == 0u) ||
|
||||
((adc_cal_flags(ADC_SEM_IA) & ADC_CAL_F_VALID) == 0u)) {
|
||||
return 1u;
|
||||
}
|
||||
#endif
|
||||
return 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:组装参数快照(40 项定值 Q10 + 校验和 + 封存态 + 越界位图 + 未标定闭锁)
|
||||
* 入口参数:p_snap - 目标帧(调用方已清零) ui_data_par_t *
|
||||
* sum - 本次发布的 prot_set_checksum() 值 uint32_t
|
||||
* 返回值:无
|
||||
@@ -406,6 +517,9 @@ static void feed_fill_par(ui_data_par_t *p_snap, uint32_t sum) {
|
||||
/* 越界位图非 0 = 上一次整表装载/提交有越界项(prot_set_bad_mask 的既有口径),
|
||||
* 页面据此反显;为 0 时无意义位图不参与显示 */
|
||||
p_snap->bad_valid = (p_snap->bad_mask != 0u) ? 1u : 0u;
|
||||
/* v2 尾追加:未标定闭锁徽标(三路口径,与判据停走同源;与上面的 locked 是
|
||||
* 两件事——locked 禁编辑,cal_block 只说明"改了也不会动作") */
|
||||
p_snap->cal_block = feed_cal_block();
|
||||
for (i = 0u; i < (uint8_t)PS_ID_COUNT; i++) {
|
||||
p_snap->q10[i] = prot_set_get((prot_set_id_t)i);
|
||||
}
|
||||
@@ -442,23 +556,40 @@ static void feed_fill_info(ui_data_info_t *p_snap, uint32_t tick_ms) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:装配节拍入口——实时帧与信息帧满 200 ms 发布一次,参数帧仅在校验和
|
||||
* 变化时发布;首轮立即发布(免启动初期出现一闪的 "--")
|
||||
* 函数功能:装配节拍入口——先落定一条定值整定请求(写方向),再发布实时帧与
|
||||
* 信息帧(满 200 ms 一次)、参数帧(仅校验和变化时);首轮立即发布
|
||||
* (免启动初期出现一闪的 "--")
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:uiTask 上下文,主循环每轮一次(UI 循环周期 10 ms)
|
||||
* 函数说明:每轮未到期时代价 = 一次 osKernelGetTickCount + 两次减法比较;
|
||||
* 差值法吸收 tick 回绕(同 App/algo/adc_health.h 的 now_ms 口径)
|
||||
* 函数说明:每轮未到期时代价 = 一次 osKernelGetTickCount + 两次减法比较 +
|
||||
* 一次空队列判断(drain 无请求时只是一次计数比较);
|
||||
* 差值法吸收 tick 回绕(同 App/algo/adc_health.h 的 now_ms 口径)。
|
||||
* 写路径失败态(ERR_RANGE/ERR_LOCKED/ERR_BUSY)走屏上通道,不打日志
|
||||
* (每条 DBG 约 90 B Flash + 216 B 栈,AGENTS「日志不是免费的」)
|
||||
*/
|
||||
void ui_data_feed_tick(void) {
|
||||
uint32_t now = osKernelGetTickCount();
|
||||
uint32_t sum = prot_set_checksum();
|
||||
uint32_t now;
|
||||
uint32_t sum;
|
||||
uint8_t due_rt;
|
||||
|
||||
now = osKernelGetTickCount();
|
||||
|
||||
if (s_map_done == 0u) {
|
||||
feed_map_phasor();
|
||||
}
|
||||
|
||||
/* 写路径:先落定整定请求,**再**取校验和——本轮刚写进 prot_set 的定值就进
|
||||
* 本轮"校验和变化才发"那道门,页面保存行的 OK 与新值同一帧回来,不必多等
|
||||
* 一轮(10 ms)。顺序反了不出错,只是慢一轮。
|
||||
* 每轮最多处理一条(ui_data_param_drain 的限定条件):整定是人工低频事件,
|
||||
* 一轮一条已经把最坏耗时钉住,不必排空整个队列
|
||||
* ⚠ 返回值必须显式丢弃:本函数不消费"处理了几条",但 bsp-err 的
|
||||
* consume-result 只认接住或 (void) 两种写法(实测编译器与 cppcheck 都不报) */
|
||||
(void)ui_data_param_drain(&s_param_ops);
|
||||
|
||||
sum = prot_set_checksum();
|
||||
|
||||
due_rt = 0u;
|
||||
if ((s_first_done == 0u) || ((uint32_t)(now - s_last_rt_ms) >= FEED_RT_PERIOD_MS)) {
|
||||
due_rt = 1u;
|
||||
@@ -475,13 +606,28 @@ void ui_data_feed_tick(void) {
|
||||
(void)ui_data_publish_info(&s_scr.info);
|
||||
}
|
||||
|
||||
/* 参数帧:校验和门控——定值变化是低频事件,不随 200 ms 白拷 184 B
|
||||
* (docs/UI设计/UI_数据供给设计说明.md §5.2) */
|
||||
if ((s_par_sum_valid == 0u) || (sum != s_par_sum)) {
|
||||
(void)memset(&s_scr, 0, sizeof(s_scr));
|
||||
feed_fill_par(&s_scr.par, sum);
|
||||
(void)ui_data_publish_par(&s_scr.par);
|
||||
s_par_sum = sum;
|
||||
s_par_sum_valid = 1u;
|
||||
/* 参数帧门控:定值变化是低频事件,不随 200 ms 白拷一帧(契约 §5.2)。
|
||||
* 但"校验和没变"不等于"页面该看的没变"——本帧里还有两个与校验和无关的事实:
|
||||
* · locked(定值闭锁):自检失配时置位,校验和一字不变,
|
||||
* 而整定页保存行要显示 LOCK 并禁编辑;
|
||||
* · cal_block(未标定闭锁):卡 12 两点标定写完 [adc_cal] 的 valid 就翻转,
|
||||
* 校验和同样不动,而标题上的"未标定"徽标要当场消失。
|
||||
* 只盯校验和,这两件事都到不了屏上(真机现象:标定做完了屏上徽标永远挂着,
|
||||
* 直到某次定值改动才被顺带刷掉——那是把两件不相干的事当同步点)。
|
||||
* 代价:每轮多两次字节比较(无变化时零拷贝),换来屏上状态与真值同拍 */
|
||||
{
|
||||
uint8_t locked_now = prot_set_locked();
|
||||
uint8_t cal_now = feed_cal_block();
|
||||
|
||||
if ((s_par_sum_valid == 0u) || (sum != s_par_sum)
|
||||
|| (locked_now != s_par_locked) || (cal_now != s_par_cal_block)) {
|
||||
(void)memset(&s_scr, 0, sizeof(s_scr));
|
||||
feed_fill_par(&s_scr.par, sum);
|
||||
(void)ui_data_publish_par(&s_scr.par);
|
||||
s_par_sum = sum;
|
||||
s_par_locked = locked_now;
|
||||
s_par_cal_block = cal_now;
|
||||
s_par_sum_valid = 1u;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+14
-6
@@ -17,6 +17,8 @@
|
||||
* 创建日期:2026-10-25
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 王建锋 创建(道E task-14)
|
||||
* v1.1 2026-10-26 DTU 项目组 道F task-1:tick 兼任定值写路径的落定节拍
|
||||
* (契约 §6.2;实现见 .c 的 s_param_ops)
|
||||
*/
|
||||
|
||||
#ifndef UI_DATA_FEED_H
|
||||
@@ -27,15 +29,21 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
* 函数功能:装配节拍入口——实时帧与信息帧满 200 ms 发布一次,参数帧仅在校验和
|
||||
* 变化时发布;首轮立即发布(免启动后出现一闪的 "--")
|
||||
* 函数功能:装配节拍入口——先落定一条定值整定请求(写方向),再发布实时帧与
|
||||
* 信息帧(满 200 ms 一次)、参数帧(仅校验和变化时);首轮立即发布
|
||||
* (免启动后出现一闪的 "--")
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:uiTask 上下文,主循环每轮一次(UI 循环周期 10 ms)
|
||||
* 函数说明:本函数每轮代价 = 一次 tick 差值比较 + 一次校验和比较;真正的取数
|
||||
* 与组装只在到期时发生(docs/UI设计/UI_数据供给设计说明.md §5.2)。
|
||||
* 限定条件:uiTask 上下文,主循环每轮一次(UI 循环周期 10 ms);页面"保存"按键
|
||||
* 与本页节拍同上下文,故写路径的投递/落定之间不需要任何锁
|
||||
* (Drivers/BSP/UI/ui_data.h 写路径节)
|
||||
* 函数说明:本函数每轮代价 = 一次 tick 差值比较 + 一次校验和比较 + 一次
|
||||
* ui_data_param_drain()(队列空时只是一次计数判断);真正的取数与组装
|
||||
* 只在到期时发生(docs/UI设计/UI_数据供给设计说明.md §5.2)。
|
||||
* ⚠ drain 排在取校验和**之前**:本轮写入的定值要在本轮就被"校验和变化
|
||||
* 才发"那道门看见,页面才能在同一帧里同时显示 OK 与新值
|
||||
* 内部零阻塞、零堆分配、零日志(日志每条约 90 B Flash + 216 B 栈,
|
||||
* 200 ms 周期打不起)
|
||||
* 200 ms 周期打不起);整定失败态走屏上通道,不打日志
|
||||
*/
|
||||
void ui_data_feed_tick(void);
|
||||
|
||||
|
||||
+62
-1
@@ -7,7 +7,7 @@
|
||||
|
||||
| 文件 | 作用 |
|
||||
|---|---|
|
||||
| `sys_log.h` | **日志接口(平台无关)**:定长 64B 记录写入 `/log/run.log`·`selftest.log` |
|
||||
| `sys_log.h` | **日志接口(平台无关)**:定长 64B 记录,七类各一个 `/log` 文件(run / selftest / accid / alarm / start / oper / yk)|
|
||||
| `sys_log_mcu.c` | `sys_log` 的 MCU 端后端实现(NAND + FatFs) |
|
||||
| `console_log.c/h` | **控制台单写者日志**:多任务日志经队列汇入唯一写者,避免串口输出交错 |
|
||||
| `soe_rec.c/h` | **SOE 记录纯编解码**(B9):64B 定长记录(CP56Time2a 时标 + 类型 + 元件号 + 单调序号 + 值 + sum8 撕裂校验),零 I/O 零 OS 依赖,主机单测覆盖 |
|
||||
@@ -23,6 +23,67 @@
|
||||
内核启动后由 `syslog_drain` 惰性创建真队列并排空
|
||||
- ⚠️ 写盘必须校验 `f_write` 返回的**字节数**(该 FatFs 移植在簇分配失败时返回 `FR_OK` + 短写,
|
||||
不报错误码);追加写必须 `FA_WRITE|FA_OPEN_APPEND`(`FA_OPEN_APPEND` 单独用**不含写位**)
|
||||
- **清除记录 = 请求 / 落地分离**:非写者上下文(uiTask 等)只能 `syslog_request_clear(id)`
|
||||
置一个标志(零盘触点、零卷锁),真正的 `f_unlink` 在 **defaultTask** 的 `syslog_drain()`
|
||||
**尾部**执行——顺序也是判据的一部分:排在条目排空之后,否则本轮刚排空的旧记录会被
|
||||
写回去、`syslog_count()` 不归零。结果读 `syslog_clear_state(id)`
|
||||
(`NONE/OK/WAIT/FAIL`),事实复核读 `syslog_count()`。
|
||||
落地函数 `syslog_clear_apply(id)` 是 MCU 后端专属(模拟器不实现 → 共享代码误调在
|
||||
链接期就报错):卷锁一律 try,卷占用返 -1 且**不动原文件**;幂等只认 `FR_NO_FILE`,
|
||||
`FR_NO_PATH` 如实返 -1 且**不做任何卷级动作**(不补目录、不重格式化)
|
||||
- 删除的作用域 = 上表七个记录文件(`syslog_request_clear(id)` 逐 id 独立请求);取证文件
|
||||
`wdev.log` 与 `soe.log` 各有各的属主与通道,不在本 API 作用域,想清它们要另开 API。
|
||||
清除成功会往**操作记录**里落一条 `clear <文件>`——但清 `oper.log` 自己时**刻意不投**,
|
||||
否则"我清除了记录"这条会把刚删空的文件又写成 1 条,直接砸掉「删除后 `syslog_count()`
|
||||
归零」这条判据(`TC-SYSLOG-133b` 钉住)
|
||||
- ⚠ 宿主后端(`simulator/sim/sim_syslog.c`)与 MCU 端**判据相同、时序不同**:模拟器单线程,
|
||||
七类对应工作目录下七份宿主文件(`sim_accid.log` 等),`syslog_post_quiet()` 在本端
|
||||
等价于 `syslog_post()`(宿主进程没有那条栈硬线,也没有队列与丢弃);
|
||||
`syslog_request_clear()` 调用内就删完了,因此 `WAIT` 态在模拟器上永不出现
|
||||
(同 `syslog_post` 在两端"有队列 / 直写无队列"的差异)
|
||||
|
||||
### 五类记录与「记录类型 = 日志 ID」(道F task-10,2026-10-26;task-11 记录 5 页按此做)
|
||||
|
||||
**决策一:记录类型就是 `syslog_id_t` 本身,一类记录 = `/log` 下一个严格 8.3 短名的文件。**
|
||||
64B 定长记录体里**没有类型字段位**:`[0..18]` 时标 / `[19]` 空格 / `[20..29]` epoch /
|
||||
`[30]` 空格 / `[31..58]` 描述 / 尾部补零,这个布局被 UI 记录页与主机解码工具按**固定偏移**
|
||||
读了了很久(run.log / selftest.log 是既有文件,既有读者)。塞类型字节要么挪走 epoch
|
||||
(上电/重启分类就断),要么挪走描述(屏上少字),且两端读者同批改——所以
|
||||
"这条记录是什么类型"由**它在哪个文件里**回答。`TC-SYSLOG-136` 把这条钉成机器断言:
|
||||
同一条内容在七类之间逐字节相同,记录体不编码类型。
|
||||
|
||||
**决策二:不动 `soe_type_t`(二进制 SOE 通道保持现有 8 型)。** 加 SOE 类型必须同步 UI 侧
|
||||
`ui_w_soe.c` 的类型名表(那是另一位队友的写域),而 SOE 页与这五页本就是两套读者
|
||||
(二进制解码器 vs 文本列表)。**别把新类型顺手加进 SOE**:那会同时造出
|
||||
"同一个事件两个真源"。
|
||||
|
||||
| 记录 | 日志 ID / 文件 | 容量上限 | 写入点 | 触发判据 |
|
||||
|---|---|---|---|---|
|
||||
| 事故 | `SYSLOG_ACCIDENT` / `accid.log` | 512 | `App/task/protect_task.c:181` | `asserted` 上升沿**且**该元件跳闸投退项 = 1 |
|
||||
| 保护告警 | `SYSLOG_ALARM` / `alarm.log` | 1024 | 同文件 `184` | `asserted` 上升沿**且**跳闸投退项 = 0 或该元件无此项 |
|
||||
| 保护启动 | `SYSLOG_START` / `start.log` | 1024 | 同文件 `176` | `picked` 上升沿(越限电平,早于/等于动作) |
|
||||
| 操作 | `SYSLOG_OPER` / `oper.log` | 512 | `App/protocol/iec104/iec104_pts.c:574/602`、`App/task/default_task.c:242`、`App/log/sys_log_mcu.c:646`(宿主 `sim_syslog.c:245`) | 强制复归受理 / 软压板投退落地 / 定值**真落盘成功** / 清除记录成功 |
|
||||
| 遥控 | `SYSLOG_YK` / `yk.log` | 256 | `App/protocol/iec104/iec104_pts.c:573/576/582/586/596/598/609` | 104 双点命令的选择段、执行段、非法 DPC、未知 IOA 各一条 |
|
||||
|
||||
- **"哪些元件算跳闸"只有一个真源**:定值表 `g_prot_set_meta` 里的 `PS_*_TRIP`(枚举注释
|
||||
即"0 告警 / 1 跳闸")+ 实例表 `k_pe[]` 的 `trip_ctl` 列。分类入口
|
||||
`prot_elm_record_kind()`(`App/algo/prot_elm.c`)里**没有第二份元件名单**,
|
||||
没有该投退项的五项(OV/UV/3U0/OF/UF)填 `PROT_ELM_NO_TRIP=0xFF` → 恒告警;
|
||||
`TC-PROTELEM-015` 逐元钉住"逐项独立 / 改定值即改类别 / 无此项者不受别人定值影响"。
|
||||
- **描述只用 ROM 常量或 `prot_elm_name()` 直传**:所有写入点零 `snprintf`、零拼接。
|
||||
最长一条 20 字节 < `SYSLOG_DESC_MAX`(28);`TC-IEC104-030` 逐条量长度(超 28 会被
|
||||
投递侧静默截断,屏上少几个字,没人会去数)。
|
||||
- ⚠️ **两个投递口,按调用者的栈预算选**:`syslog_post()` 的两个"满态丢弃留痕"分支
|
||||
内联着日志行缓冲(每处约 216 B 栈),因此 `syslog_post_quiet()` 给静态最坏链吃紧的
|
||||
任务用(**protectTask、iec104Task 必须用它**),丢弃只累加 `syslog_post_dropped()`、
|
||||
由唯一写者 defaultTask 在 `syslog_drain()` 里按差值补一条留痕——同 `soe_store_post`
|
||||
当年那条 `872 → 1192 B FAIL` 的实证纪律。收/丢判据两个口完全一致。
|
||||
- **读侧契约(给 task-11)**:`syslog_count(id)` + `syslog_get_record(id, idx, ts, ts_len,
|
||||
desc, desc_len)` 与运行/自检记录**同一枚 reader**,"最近 20 条"= 从 `count-20` 起读;
|
||||
`count()==0` 就是空态,页面显示"无记录"。**不要**在共享代码里调 MCU 专属的
|
||||
`syslog_clear_apply()`(模拟器不实现 → 链接期报错是故意的);清除一律走
|
||||
`syslog_request_clear()` + `syslog_clear_state()`,并把 `WAIT` 当正常态看(模拟器端
|
||||
永不出现 `WAIT`,见下条)。
|
||||
|
||||
### console_log(控制台单写者)
|
||||
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
* 创建日期:2026-09-14
|
||||
* 修改记录:
|
||||
* v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/子系统设计/运行与自检记录设计说明.md §4)
|
||||
* v1.2 2026-10-26 王建锋 新增「记录删除」API(UI 装置维护「清除记录」的前置,
|
||||
* 道F task-4;判据 docs/任务规划/UI补齐.md §2 第 3 行
|
||||
* 「不允许只做一个没有实际效果的按钮」):
|
||||
* 共享层 syslog_request_clear() / syslog_clear_state()
|
||||
* (双端都实现,UI 侧只准用这两个)+ MCU 后端专属
|
||||
* syslog_clear_apply()(落地半边,仅 defaultTask)。
|
||||
* 请求与落地分离的理由与执行点写在下面声明处
|
||||
* v1.1 2026-10-24 王建锋 仅契约注释更正,与 App/log/sys_log_mcu.c 的 C/F 两项
|
||||
* 整改同步(无签名、无行为改动):
|
||||
* syslog_get_record 写死容量要求(ts_len > SYSLOG_TS_LEN、
|
||||
@@ -28,10 +35,28 @@ extern "C" {
|
||||
|
||||
/*
|
||||
* 日志 ID:决定落盘文件与容量上限
|
||||
*
|
||||
* ⚠ **记录类型 = 日志 ID 本身**(一类记录 = /log 下一个严格 8.3 短名的文件),
|
||||
* 记录体内**没有**类型字段位(Lead 裁定 2026-10-26,task-10):64B 定长格式被
|
||||
* UI 记录页(文本读者)与主机解码工具同时按固定偏移读,塞类型字节等于当场砸掉
|
||||
* 这两批既有读者;而运行/自检记录已经在这份格式里跑了很久,不能为五类新记录
|
||||
* 改版本位。于是"这条记录是什么类型"由它在哪个文件里回答。
|
||||
* 同理二进制 SOE 通道的 soe_type_t 本轮**不动**:改它必须同步 UI 侧的类型名表
|
||||
* (类型名表在 UI 侧 <Drivers/BSP/UI/ui_w_soe.c>,不在本道写域),而且 SOE
|
||||
* 页与这五页是两套读者。
|
||||
* 口径与五个写入点已登记进 App/log/README.md(task-11「记录 5 页」按此做)。
|
||||
*/
|
||||
typedef enum {
|
||||
SYSLOG_RUN = 0u, /* 运行记录 /log/run.log:上电/重启事件 */
|
||||
SYSLOG_SELFTEST, /* 自检记录 /log/selftest.log:开机自检故障(仅故障才写) */
|
||||
/* ↓ 五类记录(docs/UI设计/页面扩充详细方案.md §3.6 的五行,task-10 落写入点)
|
||||
* ⚠ 只准**尾部追加**:id 值即枚举序 = 两端 s_paths[]/s_caps[] 的下标,
|
||||
* 中间插值会让老文件被新 id 读成别的类型(静默错类别,比崩更难查)。 */
|
||||
SYSLOG_ACCIDENT, /* 事故记录 /log/accid.log:跳闸投退=1 的元件动作 */
|
||||
SYSLOG_ALARM, /* 保护告警 /log/alarm.log:跳闸投退=0 或该项不存在的元件动作 */
|
||||
SYSLOG_START, /* 保护启动 /log/start.log:元件 picked 上升沿 */
|
||||
SYSLOG_OPER, /* 操作记录 /log/oper.log:强制复归 / 清除记录 / 对时 / 定值保存 */
|
||||
SYSLOG_YK, /* 遥控记录 /log/yk.log:104 遥控的选择 / 执行 / 拒绝 */
|
||||
SYSLOG_COUNT /* 哨兵值,非有效 ID */
|
||||
} syslog_id_t;
|
||||
|
||||
@@ -53,9 +78,21 @@ typedef enum {
|
||||
|
||||
/*
|
||||
* 容量上限(条数;满时删除重建,设计文档 §4.3)
|
||||
*
|
||||
* ⚠ **上限是逻辑上限,不是预分配**:文件按追加写自然增长,一条不写就不占簇;
|
||||
* 到顶才 unlink 重建(照 run.log 先例)。七类全满的名义峰值 =
|
||||
* (1024+512+512+1024+1024+512+256) × 64B = **304 KB**,NAND 2 GB、且这些
|
||||
* 记录只在事件边沿才写(判据动作/遥控/人工操作),不构成容量或磨损瓶颈。
|
||||
* 告警/启动给到 1024 是因为一次系统故障会连片触发多个元件,别让最想了解
|
||||
* 故障过程的两类记录最先被截掉。
|
||||
*/
|
||||
#define SYSLOG_RUN_MAX 1024u /* /log/run.log:64KB */
|
||||
#define SYSLOG_SELFTEST_MAX 512u /* /log/selftest.log:32KB */
|
||||
#define SYSLOG_ACCIDENT_MAX 512u /* /log/accid.log:32KB(跳闸稀有,换长历史) */
|
||||
#define SYSLOG_ALARM_MAX 1024u /* /log/alarm.log:64KB */
|
||||
#define SYSLOG_START_MAX 1024u /* /log/start.log:64KB */
|
||||
#define SYSLOG_OPER_MAX 512u /* /log/oper.log:32KB */
|
||||
#define SYSLOG_YK_MAX 256u /* /log/yk.log:16KB(低频,与事故同稀有) */
|
||||
|
||||
/*
|
||||
* 上电/重启分类窗口(秒):run.log 末条 epoch 距 now < 该值 → "reboot",否则 "power on"
|
||||
@@ -83,6 +120,34 @@ typedef enum {
|
||||
*/
|
||||
void syslog_post(syslog_id_t id, const char *desc);
|
||||
|
||||
/*
|
||||
* 函数功能:投递一条日志——**调用链上不带日志行缓冲**的那一个版本
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* desc - 描述串(NUL 结尾;超过 SYSLOG_DESC_MAX 截断),同 syslog_post
|
||||
* 返回值:无
|
||||
* 限定条件:任意上下文;MCU 端不碰 FatFs、不碰卷锁、不阻塞
|
||||
* 函数说明:为什么要有第二个投递口(不是洁癖):每个 DBG_* 调用点在调用者栈上
|
||||
* 内联约 216 B 行缓冲,而 syslog_post 的两个"满态丢弃留痕"分支就躺在
|
||||
* 它自己的帧里——于是"调一次 post"在静态最坏链上等于"带上这两处缓冲"。
|
||||
* protectTask 的静态最坏链是 B9 写进 DoD 的硬线(872 B / 57%,当年在
|
||||
* post 里加一条 DBG_* 实测垫到 1192 B = stack_report FAIL),iec104Task
|
||||
* 同样是深链任务,所以这五类记录的写入点一律走本函数。
|
||||
* 代价是丢弃时不当场留痕:只累加 syslog_post_dropped(),由唯一写者
|
||||
* defaultTask 在 syslog_drain() 里按差值补一条(照 soe_store 的先例)。
|
||||
* 与 syslog_post 的差别**只有留痕路径**,收/丢的判据逐条相同。
|
||||
* 模拟器后端没有队列也没有栈预算问题,本函数等价于 syslog_post。
|
||||
*/
|
||||
void syslog_post_quiet(syslog_id_t id, const char *desc);
|
||||
|
||||
/*
|
||||
* 函数功能:取 syslog_post_quiet() 通道的累计丢弃条数(可观测性)
|
||||
* 入口参数:无
|
||||
* 返回值:MCU 端 = 队列满 / 启动前数组满累计次数;模拟器端恒 0(直写不丢) uint32_t
|
||||
* 函数说明:与 soe_store_dropped() 同款定位。留痕由 drain 按差值上报,本函数是
|
||||
* 给单测与台架读数用的,不参与任何判定路径。
|
||||
*/
|
||||
uint32_t syslog_post_dropped(void);
|
||||
|
||||
/*
|
||||
* 函数功能:获取记录条数(= filesize / SYSLOG_REC_SIZE,O(1))
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
@@ -118,6 +183,58 @@ int syslog_get_record(syslog_id_t id, uint32_t idx, char *p_ts, size_t ts_len, c
|
||||
*/
|
||||
int syslog_last_epoch(syslog_id_t id, uint32_t *p_epoch);
|
||||
|
||||
/*
|
||||
* 记录删除(「清除记录」):请求 → 落地分离
|
||||
*
|
||||
* 为什么必须分两步:defaultTask 是本模块**唯一写者**(存储架构说明 §10.1 第 7 条),
|
||||
* uiTask 直接 f_unlink 会与写者的追加写/「容量满 unlink 重建」交错,而且删除要走
|
||||
* 卷锁 + FAT 回写——uiTask 在 IWDG required_mask 内、每 100 ms 喂一次心跳,
|
||||
* 给它一条最坏数百 ms 的存储路径就是拿误复位当代价。故 UI 侧只登记请求,
|
||||
* 真正的删除在 defaultTask 的 syslog_drain() 尾部执行
|
||||
* (范式同 App/algo/prot_elm.c 的 prot_elm_request_reset():单字节标志 + 消费者落地)。
|
||||
*
|
||||
* 状态码(syslog_clear_state 返回值;写成宏而不是枚举,与本头既有 SYSLOG_* 常量同族)
|
||||
*/
|
||||
#define SYSLOG_CLEAR_NONE 0 /* 从未请求过清除(上电初值) */
|
||||
#define SYSLOG_CLEAR_OK 1 /* 已清空:文件不在盘上,同拍 syslog_count() 必为 0 */
|
||||
#define SYSLOG_CLEAR_WAIT (-1) /* 请求排队中:等 defaultTask 下一轮 drain(含卷占用顺延、卷未就绪挂起) */
|
||||
#define SYSLOG_CLEAR_FAIL (-2) /* 最近一次落地失败:原文件未被删除,可再发一次请求 */
|
||||
|
||||
/*
|
||||
* 函数功能:登记「清除某日志全部记录」的请求(任意上下文;不碰存储、不阻塞)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* 返回值:0 - 请求已登记(同一 id 重复登记合并为一次,幂等)
|
||||
* -1 - id 越界(请求未登记,状态一个字节都没动) int
|
||||
* 限定条件:非唯一写者的上下文(uiTask / netTask / iec104Task)**只能**用本函数发起
|
||||
* 删除,不得直调 MCU 后端的 syslog_clear_apply()
|
||||
* 函数说明:本函数**不等于"已清除"**,只代表"已受理"——MCU 端的删除动作发生在
|
||||
* defaultTask 的下一次 syslog_drain()(50 ms 节拍),结果读
|
||||
* syslog_clear_state(),事实复核读 syslog_count()。
|
||||
* ⚠ 两端时序有别(同 syslog_post 的队列/直写差异,那边写在 post 的头注释里):
|
||||
* MCU 端返回后状态通常是 SYSLOG_CLEAR_WAIT;模拟器端单线程、本调用内
|
||||
* 同步落地,返回后立即是 OK/FAIL,**永远不会读到 WAIT**。UI 侧要按
|
||||
* "WAIT 也正常"写判断,别把 WAIT 当失败。
|
||||
* 刻意**不**在删除成功后补一条 "records cleared" 留痕:判据要求
|
||||
* syslog_count() 归零,写了立刻变 1;要留痕归操作记录/SOE
|
||||
* (docs/任务规划/UI补齐.md 第 6 项的写入方),不是本 API 的职责。
|
||||
*/
|
||||
int syslog_request_clear(syslog_id_t id);
|
||||
|
||||
/*
|
||||
* 函数功能:查「清除记录」的状态(给 UI 出真实反馈,不是"按了就算成功")
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* 返回值:SYSLOG_CLEAR_NONE(0) / SYSLOG_CLEAR_OK(1) / SYSLOG_CLEAR_WAIT(-1) /
|
||||
* SYSLOG_CLEAR_FAIL(-2);id 越界按 NONE 返回(等价"从没请求过") int
|
||||
* 限定条件:任意上下文(MCU 端只读两个 volatile 单字节,Cortex-M 上原子,无锁)
|
||||
* 函数说明:排队位优先于结果字节——否则连点两次会先把上一次的结果当成这次的,
|
||||
* 还在排队的请求被显示成"已清空"。
|
||||
* 无锁交接的最坏竞态只有两种,都不产生错误数据:① 写者已销账、尚未写
|
||||
* 结果的那几个指令周期里读 → 读到上一次的结果(同向,多为 OK);
|
||||
* ② 上一次失败被新请求清零 → 那次失败不再显示(用户已在重新发起,
|
||||
* 语义正确)。为它上临界区要引 FreeRTOS 头 + 宿主桩,不值。
|
||||
*/
|
||||
int syslog_clear_state(syslog_id_t id);
|
||||
|
||||
/*
|
||||
* MCU 后端专属 API(sys_log_mcu.c;模拟器端无对应实现,勿在共享代码调用)
|
||||
*/
|
||||
@@ -143,9 +260,34 @@ void syslog_mcu_mounted(void);
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:仅 defaultTask(唯一写者)调用;卷未就绪时直接返回(条目滞留待重试)
|
||||
* 本函数尾部同时消费「清除记录」请求(syslog_request_clear 的落地半边,
|
||||
* 排在条目排空之后——删除必须是本轮对这两个文件的最后一次盘上动作,
|
||||
* 先删后写会把刚排空的旧记录又落回去,"清除"就变成没效果的按钮)
|
||||
*/
|
||||
void syslog_drain(void);
|
||||
|
||||
/*
|
||||
* 函数功能:立即删除某日志的全部记录(「清除记录」的落地半边,MCU 后端专属)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* 返回值:0 - 记录已清空(含"文件本就不存在"的幂等成功,见下)
|
||||
* -1 - 未清空:卷被占用或删除失败,**原文件一个字节都没少** int
|
||||
* 限定条件:仅 defaultTask(syslog 唯一写者)上下文,且调用处**不得持卷锁**
|
||||
* (卷锁非递归锁);卷锁一律 try(vol_lock_wait(0),本任务 = 看门狗
|
||||
* 喂狗点,阻塞等锁 = 停喂 = IWDG 误复位)
|
||||
* 函数说明:其它上下文请调 syslog_request_clear()(本头文件上方);本函数在
|
||||
* MCU 后端之外**没有任何实现**,共享代码误调会在链接期报错,
|
||||
* 这比运行时断言硬。产品路径上唯一的调用者是 syslog_drain 尾部的
|
||||
* 请求消费循环,直接调用只用于板级套件与主机单测。
|
||||
* 幂等口径:只有 FR_NO_FILE 算"已经没有什么可删了"→ 返 0;
|
||||
* FR_NO_PATH(/log 目录不在)**不算**幂等——记录确实读不到,但成因是
|
||||
* 目录没了而不是"被清空",补目录归 syslog_mcu_mounted() 每 2s 的重试,
|
||||
* 这里如实返 -1,不把卷异常洗成"清除成功"。
|
||||
* 名字/参数类错误码(FR_INVALID_NAME 等)一律只返 -1 不做任何补救动作:
|
||||
* 本函数压根没有卷级动作(无 f_mkdir / 无 f_mkfs / 不重试 unlink),
|
||||
* 这正是存储架构说明 §10.1 第 10 条在删除路径上的落地形态。
|
||||
*/
|
||||
int syslog_clear_apply(syslog_id_t id);
|
||||
|
||||
/*
|
||||
* 函数功能:把一条取证行追加到 /log/wdev.log(掉电不丢;变长行,不走 64B 定长格式)
|
||||
* 入口参数:p_desc - 取证描述行(调用方已格式化;不可为 NULL) const char *
|
||||
|
||||
+372
-40
@@ -1,11 +1,33 @@
|
||||
/*
|
||||
* 模块名称:System Log (MCU 后端) — 运行/自检记录落盘
|
||||
* 模块功能:FatFS /log 目录日志文件(run.log/selftest.log)单写者落盘;启动前静态待发数组 + FreeRTOS 消息队列,
|
||||
* 模块功能:FatFS /log 目录**七类**定长 64B 记录(run/selftest/accid/alarm/start/oper/yk,
|
||||
* 一类记录 = 一个文件 = 一种记录类型)单写者落盘;启动前静态待发数组 + FreeRTOS 消息队列,
|
||||
* defaultTask(唯一写者)排空落盘;容量满删除重建;上电/重启分类
|
||||
* 适用平台:STM32F407ZGTx
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-09-14
|
||||
* 修改记录:
|
||||
* v1.7 2026-10-26 王建锋 「记录 5 页」的数据面(task-10):syslog_id_t 尾部追加
|
||||
* SYSLOG_ACCIDENT / ALARM / START / OPER / YK 五枚,本文件
|
||||
* s_paths[]/s_caps[] 同步到七项(**必须与枚举同序**,越界
|
||||
* 守卫只挡 id>=SYSLOG_COUNT,序错会静默把事故写进告警文件)。
|
||||
* 记录体内不加类型字段——文件即类型(决策与理由见 sys_log.h
|
||||
* 的 syslog_id_t 注释、App/log/README.md)。
|
||||
* 成本:.bss **+40 B**(s_rd_gen 4B×5 + s_requeue_reported/
|
||||
* s_clear_req/s_clear_st/s_fault_reported 各 1B×5),
|
||||
* .const **+111 B**(5 枚路径指针 20B + 5 项容量 20B +
|
||||
* 5 条 8.3 路径串 71B);零新增缓冲、零堆分配、零栈增量。
|
||||
* 写入点全部在别处(protectTask / iec104 / 配置保存 /
|
||||
* 本模块 request_clear),本文件只多七张表的五项。
|
||||
* v1.6 2026-10-26 王建锋 新增「记录删除」(道F task-4,UI 装置维护「清除记录」
|
||||
* 的前置):请求 syslog_request_clear() / 状态
|
||||
* syslog_clear_state() / 落地 syslog_clear_apply()。
|
||||
* 仍然只有 defaultTask 碰盘——非写者上下文只置一个
|
||||
* volatile 单字节,f_unlink 在 syslog_drain() 尾部执行
|
||||
* (范式同 prot_elm_request_reset);卷锁一律 try,
|
||||
* 本任务 = 看门狗喂狗点绝不阻塞。
|
||||
* 新增静态 4 B .bss(s_clear_req[2] + s_clear_st[2]),
|
||||
* 零新增缓冲、零堆分配。
|
||||
* v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/子系统设计/运行与自检记录设计说明.md §4.4)
|
||||
* v1.1 2026-09-21 王建锋 消息队列改为内核启动后由 syslog_drain 惰性创建
|
||||
* (修复 osKernelStart 前创建 FreeRTOS 内核对象导致的启动失败)
|
||||
@@ -58,8 +80,30 @@
|
||||
|
||||
/* 静态变量定义区 */
|
||||
|
||||
static const char *const s_paths[SYSLOG_COUNT] = { "/log/run.log", "/log/selftest.log" };
|
||||
static const uint32_t s_caps[SYSLOG_COUNT] = { SYSLOG_RUN_MAX, SYSLOG_SELFTEST_MAX };
|
||||
/* ⚠ 两张表的下标 = syslog_id_t 枚举序,**逐项同序追加**,中间不得插值(错序 = 静默
|
||||
* 把某类记录写进别的文件,越界守卫拦不住)。文件名一律严格 8.3(FF_USE_LFN=0:
|
||||
* 主名 ≤8 字符、扩展 ≤3、不得以点开头),由 TC-SYSLOG-081 逐条机器核。
|
||||
* 七项合计 .const ≈ 111 B(含 5 条路径串 71 B);容量是**逻辑上限非预分配**。 */
|
||||
static const char *const s_paths[SYSLOG_COUNT] = {
|
||||
"/log/run.log", "/log/selftest.log", "/log/accid.log", "/log/alarm.log",
|
||||
"/log/start.log", "/log/oper.log", "/log/yk.log"
|
||||
};
|
||||
static const uint32_t s_caps[SYSLOG_COUNT] = {
|
||||
SYSLOG_RUN_MAX, SYSLOG_SELFTEST_MAX, SYSLOG_ACCIDENT_MAX, SYSLOG_ALARM_MAX,
|
||||
SYSLOG_START_MAX, SYSLOG_OPER_MAX, SYSLOG_YK_MAX
|
||||
};
|
||||
|
||||
/* 「清除记录」成功后的操作记录文案(task-10 的第五个操作写入点)。一类一枚 ROM
|
||||
* 常量、零拼接:投递发生在 defaultTask 的 drain 尾部,那里不该为一条日志再放
|
||||
* 格式化缓冲;最长 20 字符 << SYSLOG_DESC_MAX(28)。
|
||||
* ⚠ SYSLOG_OPER 自己那一格**永远不会被用到**:清 oper.log 时不投递操作记录,
|
||||
* 否则"我清除了记录"这条会把刚删空的 oper.log 又写成 1 条,直接砸掉 task-4
|
||||
* 定死、UI 页面照它出反馈的判据「删除成功后 syslog_count() 归零」(与
|
||||
* sys_log.h 里刻意不写 "records cleared" 留痕是同一条理由的两个面)。 */
|
||||
static const char *const s_clear_rec[SYSLOG_COUNT] = {
|
||||
"clear run.log", "clear selftest.log", "clear accid.log", "clear alarm.log",
|
||||
"clear start.log", "clear oper.log", "clear yk.log"
|
||||
};
|
||||
|
||||
#define SYSLOG_QUEUE_MAX 8u
|
||||
|
||||
@@ -108,6 +152,16 @@ static uint8_t s_requeue_reported[SYSLOG_COUNT];
|
||||
static volatile uint8_t s_post_qfull_reported; /* 队列满丢弃:已留痕? */
|
||||
static volatile uint8_t s_post_pboot_full_reported;/* 启动前数组满丢弃:已留痕? */
|
||||
|
||||
/* syslog_post_quiet() 通道(task-10,2026-10-26):给"静态最坏链放不下日志内联
|
||||
* 行缓冲"的任务用的投递口(protectTask 872 B / 57% 是 B9 的 DoD 硬线,当年在
|
||||
* post 里加一条 DBG_* 实测把它垫到 1192 B = stack_report FAIL)。本通道丢弃时
|
||||
* **只累加计数**,留痕挪到 syslog_drain()(defaultTask)按差值上报——与
|
||||
* App/log/soe_store.c 的 s_drop_cnt + drain 侧差值上报完全同构。
|
||||
* RAM:两枚 uint32 = **8 B .bss**;volatile 单字自增在 Cortex-M 上原子,最坏
|
||||
* 少报一条,不会漏计到永久静默(水位只在本函数读到差值时才前进)。 */
|
||||
static volatile uint32_t s_quiet_drop; /* quiet 通道累计丢弃条数 */
|
||||
static uint32_t s_quiet_reported; /* drain 侧已上报水位(唯一写者私有) */
|
||||
|
||||
/* 日志读写放大收口(2026-10-15,读侧):8 条滑动读窗口 + 写者代数。
|
||||
* 窗口 512 B 走静态(不吃任务栈;defaultTask/uiTask 最坏链见 stack_report.py,
|
||||
* 预算余量见 docs/内存管理/RAM预算台账.md)。s_rd_gen 由写者自增、读者比对,
|
||||
@@ -120,6 +174,21 @@ static uint32_t s_rd_win_cnt; /* 窗口内有效条数 */
|
||||
static uint32_t s_rd_win_gen; /* 填充时看到的写者代数 */
|
||||
static volatile uint32_t s_rd_gen[SYSLOG_COUNT]; /* 每次成功落盘 / unlink 后自增 */
|
||||
|
||||
/* 「清除记录」的请求 / 落地交接(道F task-4)。两枚各 SYSLOG_COUNT 字节 = **共 4 B
|
||||
* .bss**(本模块唯一的新增静态;无缓冲、无堆,MicroLIB C 堆只有 512 B):
|
||||
* s_clear_req[id] —— 1 = 有未落地的删除请求。非写者上下文(uiTask)置 1,
|
||||
* defaultTask 落地时先清 0 再执行 → 期间到达的新请求会
|
||||
* 重新置 1,**下一次排空再删一次**,不会因为"正在删"而丢;
|
||||
* s_clear_st[id] —— 最近一次落地的结果,取值 SYSLOG_CLEAR_NONE/OK/FAIL
|
||||
* (WAIT 不落在这里,由 s_clear_req 现场推,见 syslog_clear_state)。
|
||||
* 为什么拆两枚而不是合成一个四态状态机:单字节状态机在"写者读完状态到写回结果"
|
||||
* 之间收到的新请求会被结果覆盖掉(用户的第二次点击被无声吞掉)。拆开后各自的写者
|
||||
* 分明(req:任意上下文置 1 / 写者清 0;st:只有写者写),单字节写在 Cortex-M 上
|
||||
* 原子,剩下的竞态只有"读到上一次结果"这一种同向偏差 → 无锁,代价换来 0 个临界区、
|
||||
* 0 个头文件依赖(本文件不 include FreeRTOS.h)。 */
|
||||
static volatile uint8_t s_clear_req[SYSLOG_COUNT]; /* 1 = 待落地的清除请求 */
|
||||
static volatile int8_t s_clear_st[SYSLOG_COUNT]; /* SYSLOG_CLEAR_NONE / OK / FAIL */
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
/*
|
||||
@@ -484,6 +553,102 @@ static int s_read_raw(syslog_id_t id, uint32_t idx, uint8_t *p_rec) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:落地执行"删掉本模块自己的日志文件"(记录删除的落地半边,内部三态版)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* 返回值:0 - 已删除,或文件本就不存在(FR_NO_FILE = 幂等成功);
|
||||
* -1 - 删除真失败(原文件一个字节都没少);
|
||||
* -2 - 卷被占用(本轮顺延,非故障,与 s_append_rec 的 -2 同一含义) int
|
||||
* 限定条件:仅 defaultTask(syslog 唯一写者)上下文;调用处不得持卷锁
|
||||
* (卷锁非递归锁,重入即自死锁,见 App/fs/vol_lock.h 使用约定)
|
||||
* 函数说明:本函数只做一件事——对自己那一个文件 f_unlink 一次。因此存储架构说明
|
||||
* §10.1 第 10 条(名字/参数类错误码严禁升级成卷操作)在这里的形态是
|
||||
* "压根没有卷级动作":不 f_mkdir、不 f_mkfs、不重开、不重试,
|
||||
* FR_INVALID_NAME / FR_NO_PATH / FR_DENIED 一律如实返 -1。
|
||||
* 幂等只认 FR_NO_FILE(文件不在 = 没有记录可删,目标已达成);
|
||||
* FR_NO_PATH 刻意不算幂等,理由写在 syslog_clear_apply() 的头注释里。
|
||||
* 只有真删成功(含幂等路径)才让 s_rd_gen[id] 自增:文件确实不在盘上了,
|
||||
* 读者的 8 条窗口必须作废;失败路径内容没变,窗口仍是有效数据,
|
||||
* 不去做无谓的失效(与 s_append_rec"失败也一律失效"不同——那边失败
|
||||
* 伴随过 unlink/重建,本函数失败时盘上什么都没动)。
|
||||
* 刻意不在删除成功后补一条 "records cleared":判据要求 syslog_count()
|
||||
* 归零,写了立刻变 1;要留痕归操作记录/SOE(UI补齐.md 第 6 项)。
|
||||
*/
|
||||
static int s_clear_try(syslog_id_t id) {
|
||||
FRESULT fr;
|
||||
int rc;
|
||||
|
||||
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) {
|
||||
return -1; /* 守卫在取锁之前:非法入参一个字节都不碰盘,也不占用卷 */
|
||||
}
|
||||
/* 卷锁 try:本任务 = 看门狗喂狗点,绝不阻塞等锁(与 s_append_rec 同一条铁律) */
|
||||
if (vol_lock_wait(0u) == 0u) {
|
||||
return -2;
|
||||
}
|
||||
fr = f_unlink(s_paths[id]);
|
||||
if ((fr == FR_OK) || (fr == FR_NO_FILE)) {
|
||||
s_rd_gen[id]++; /* 文件已不在盘上:读者的 8 条窗口作废重填 */
|
||||
rc = 0;
|
||||
} else {
|
||||
rc = -1;
|
||||
}
|
||||
vol_lock_release();
|
||||
if (rc != 0) {
|
||||
/* 一次用户动作至多一条留痕(失败不重试,见 s_clear_service),不会刷屏,
|
||||
* 所以这里不需要像 B/F 项那样另立节流位。
|
||||
* ⚠ 留这条的成本约 90 B Flash(AGENTS「日志与工具链」);留它的理由:
|
||||
* "清除失败但盘上还有记录"在串口之外没有任何可观测面(UI 只有一个整数状态)。 */
|
||||
DBG_ERROR("clear %s fail (%d)", s_paths[id], (int)fr);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:消费「清除记录」请求(syslog_request_clear 的请求 → 落地执行点)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:仅 syslog_drain() 调用 = defaultTask(唯一写者)上下文,且不持卷锁;
|
||||
* 卷未就绪 / 队列未建成时 syslog_drain 在本函数之前就已早退,
|
||||
* 请求原地保持挂起(与日志条目滞留同一处置),状态一直是 WAIT
|
||||
* 函数说明:两个决定都是刻意的:
|
||||
* 1) 排在条目排空**之后**(调用位置见 syslog_drain):删除必须是本轮对
|
||||
* run.log/selftest.log 的最后一次盘上动作,否则本轮刚排空的旧记录
|
||||
* 会在删除之后又落回去,count 不归零,"清除"就成了没实际效果的按钮
|
||||
* (判据出处 docs/任务规划/UI补齐.md §2 第 3 行)。
|
||||
* 副作用照实登记:**先排空再删** = 请求受理时仍在队列/preboot 里的
|
||||
* 条目也一并被删掉(用户按下的就是"把现在这些记录清掉"),
|
||||
* 只有删除之后新投递的记录才会留下。
|
||||
* 2) 卷占用(-2)把请求**挂回**而不是判失败:与 A 项整改后的条目
|
||||
* "整批回投、静默顺延"同一纪律(本函数每 50 ms 被排空调一次,
|
||||
* 一打日志就自己刷屏);真失败(-1)则销账 + 置 FAIL,绝不自动重试
|
||||
* ——名字/参数类错误码反复 unlink 自己的文件 = §10.1 第 10 条老账。
|
||||
*/
|
||||
static void s_clear_service(void) {
|
||||
uint32_t i;
|
||||
|
||||
for (i = 0u; i < (uint32_t)SYSLOG_COUNT; i++) {
|
||||
int rc;
|
||||
|
||||
if (s_clear_req[i] == 0u) {
|
||||
continue;
|
||||
}
|
||||
s_clear_req[i] = 0u; /* 先销账再执行:期间的新请求留到下轮,不丢 */
|
||||
rc = s_clear_try((syslog_id_t)i);
|
||||
if (rc == -2) {
|
||||
s_clear_req[i] = 1u; /* 卷占用:挂回请求,状态继续是 WAIT,静默 */
|
||||
} else {
|
||||
s_clear_st[i] = (rc == 0) ? (int8_t)SYSLOG_CLEAR_OK : (int8_t)SYSLOG_CLEAR_FAIL;
|
||||
/* 清除是一次真实的运维动作 → 操作记录(为何只在成功时记、为何
|
||||
* 独独跳过 SYSLOG_OPER,见上面 s_clear_rec 的表头注释)。
|
||||
* 失败不记:那一次什么都没改,记了反而让"操作记录"里出现
|
||||
* 一个没发生过的动作。 */
|
||||
if ((rc == 0) && (i != (uint32_t)SYSLOG_OPER)) {
|
||||
syslog_post_quiet(SYSLOG_OPER, s_clear_rec[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:日志引擎初始化(内核启动前安全空操作)
|
||||
* 入口参数:无
|
||||
@@ -671,12 +836,42 @@ void syslog_drain(void) {
|
||||
break; /* 本轮到此为止,下轮再试 */
|
||||
}
|
||||
}
|
||||
/* quiet 通道的丢弃留痕(task-10):投递侧不许打日志(见上面 s_quiet_drop 的
|
||||
* 存在理由),所以由唯一写者按差值补一条。与 soe_store 的 dropped 上报同款,
|
||||
* 也是"日志自己不许变成负载"的既有纪律:只在计数真的前进时打。 */
|
||||
if (s_quiet_drop != s_quiet_reported) {
|
||||
s_quiet_reported = s_quiet_drop;
|
||||
DBG_ERROR("quiet drop total=%lu", (unsigned long)s_quiet_drop);
|
||||
}
|
||||
/* 「清除记录」的落地半边——必须在上面的条目排空**之后**(顺序是判据的一部分:
|
||||
* 先删后写会把本轮刚排空的旧记录又落回去,count 不归零 = 没实际效果的按钮),
|
||||
* 也在 soe_store_drain() 之前:本函数自带卷锁 try,绝不在此持锁状态下调用。
|
||||
* 卷未就绪/队列未建时上面已经早退,请求保持挂起(状态 WAIT),不会丢。 */
|
||||
s_clear_service();
|
||||
/* B9:SOE 落盘挂本函数尾部——同一唯一写者(defaultTask)、同一卷就绪门
|
||||
(上方 s_fs_ready 早退蕴含 /log 已建)、同一卷锁 try 语义;protectTask
|
||||
只投递不写盘(动作路径零 NAND I/O,DoD 硬约束)。 */
|
||||
soe_store_drain();
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:立即删除某日志的全部记录(「清除记录」的落地半边,MCU 后端专属)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* 返回值:0 - 记录已清空(含"文件本就不存在"的幂等成功)
|
||||
* -1 - 未清空:卷被占用或删除失败,**原文件未被删除** int
|
||||
* 限定条件:仅 defaultTask(syslog 唯一写者)上下文,且调用处不得持卷锁;
|
||||
* 非写者上下文一律走 syslog_request_clear()(本文件不实现"跨上下文删除",
|
||||
* 那是把 uiTask 直接接上 FAT 回写的开始)
|
||||
* 函数说明:内部三态里"卷占用 = -2(顺延)"在本 API 折成 -1 —— 对调用方来说
|
||||
* "没删掉、记录还在"是同一个事实,该不该重试由调用方定;
|
||||
* 请求侧的顺延重试策略(-2 挂回请求、-1 判失败不重试)在 s_clear_service(),
|
||||
* 不外溢成第三种返回值。逐码依据与幂等口径见 sys_log.h 里本函数声明处的
|
||||
* 头注释,以及 s_clear_try() 的实现注释。
|
||||
*/
|
||||
int syslog_clear_apply(syslog_id_t id) {
|
||||
return (s_clear_try(id) == 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:投递一条日志(任意上下文;非阻塞,队列满则丢弃)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
@@ -690,55 +885,192 @@ void syslog_drain(void) {
|
||||
* 串口就按业务投递频率刷"queue full, drop",日志自己变成负载。
|
||||
* 抑制位是 volatile 单字节、由任意上下文读写:竞态最坏只多打/少打一行。
|
||||
*/
|
||||
void syslog_post(syslog_id_t id, const char *desc) {
|
||||
/* 投递结果编码(s_put 返回值)。分通道返回是**必须的**,不是讲究:F 项那两个节流
|
||||
* 抑制位按通道分开(队列 / 启动前数组),塌成一个"失败"就会把"数组满"报成"队列
|
||||
* 满",还会把另一通道的抑制位误复位(TC-SYSLOG-105..107c 逐条钉着这条)。 */
|
||||
#define SYSLOG_PUT_Q_OK 0u /* 队列收下 */
|
||||
#define SYSLOG_PUT_PB_OK 1u /* 启动前待发数组收下 */
|
||||
#define SYSLOG_PUT_Q_FULL 2u /* 队列满:本条丢弃 */
|
||||
#define SYSLOG_PUT_PB_FULL 3u /* 数组满:本条丢弃 */
|
||||
|
||||
/*
|
||||
* 函数功能:填一条待发条目——投递时就拷贝描述(post / post_quiet 共用,内部)
|
||||
* 入口参数:p_e - 输出条目(不可为 NULL) syslog_entry_t *
|
||||
* id - 日志 ID(调用方已核过界) syslog_id_t
|
||||
* desc - 描述串,NUL 结尾;静态串或调用方的临时缓冲都可以 const char *
|
||||
* 返 回 值:无
|
||||
* 函数说明:拷贝之后待发链不再引用调用方的缓冲区——这条纪律是 B 方案整改用真机
|
||||
* 实测换来的(旧实现只存指针,复用栈缓冲时一整批 12 条全部别名到最后一次
|
||||
* 写入的内容),见本文件上方 syslog_entry_t 的注释。
|
||||
*/
|
||||
static void s_entry_fill(syslog_entry_t *p_e, syslog_id_t id, const char *desc) {
|
||||
size_t dl = strlen(desc);
|
||||
|
||||
p_e->id = (uint8_t)id;
|
||||
if (dl > SYSLOG_DESC_MAX) {
|
||||
dl = SYSLOG_DESC_MAX;
|
||||
}
|
||||
memcpy(p_e->desc, desc, dl);
|
||||
p_e->desc[dl] = '\0';
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把条目送进消息队列;队列不存在或内核未跑则进启动前待发数组(内部)
|
||||
* 入口参数:p_e - 已填好的条目(不可为 NULL) const syslog_entry_t *
|
||||
* 返 回 值:SYSLOG_PUT_Q_OK / SYSLOG_PUT_PB_OK / SYSLOG_PUT_Q_FULL /
|
||||
* SYSLOG_PUT_PB_FULL uint8_t
|
||||
* 限定条件:⚠ **本函数内禁止出现任何 DBG_***(照 soe_store_post 的同款禁令)
|
||||
* 函数说明:它是 post 与 post_quiet 的共同落点,而 post_quiet 存在的唯一理由就是
|
||||
* "调用方任务的静态最坏链放不下日志的内联行缓冲"(每处约 216 B 栈)。
|
||||
* 这里一旦出现日志,quiet 就退化成 post 的别名,protectTask / iec104Task
|
||||
* 的记录投递又会把链垫回 stack_report 的 FAIL 区——B9 当年实测
|
||||
* 872 B -> 1192 B 就是这条,别用"这里只有一条"说服自己。
|
||||
* 留痕因此分两侧:post 的调用侧节流位 + drain 的 quiet 差值上报。
|
||||
*/
|
||||
static uint8_t s_put(const syslog_entry_t *p_e) {
|
||||
if (s_queue != NULL && osKernelGetState() == osKernelRunning) {
|
||||
return (osMessageQueuePut(s_queue, p_e, 0u, 0u) == osOK) ?
|
||||
(uint8_t)SYSLOG_PUT_Q_OK : (uint8_t)SYSLOG_PUT_Q_FULL;
|
||||
}
|
||||
if (s_preboot_cnt < SYSLOG_QUEUE_MAX) {
|
||||
s_preboot[s_preboot_cnt] = *p_e;
|
||||
s_preboot_cnt = (uint8_t)(s_preboot_cnt + 1u);
|
||||
return (uint8_t)SYSLOG_PUT_PB_OK;
|
||||
}
|
||||
return (uint8_t)SYSLOG_PUT_PB_FULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:投递一条日志(零日志缓冲版本;收/丢语义与 syslog_post 完全一致)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* desc - 描述串(NUL 结尾,静态/常量串;超长截断) const char *
|
||||
* 返 回 值:无
|
||||
* 限定条件:任意上下文;不碰 FatFs、不碰卷锁、不阻塞。**专为静态最坏链吃紧的
|
||||
* 任务准备**(本仓当前 = protectTask、iec104Task)
|
||||
* 函数说明:与 syslog_post 的差别只有两处:① 本调用链上不内联任何 DBG 行缓冲,
|
||||
* 丢弃时只累加 s_quiet_drop,由 defaultTask 在 syslog_drain() 里按差值
|
||||
* 补一条留痕;② 不参与 F 项那两个节流位——那两个位描述的是 syslog_post
|
||||
* 自己的历史,混用会让"post 已恢复"被 quiet 的成功投递误复位。
|
||||
* 丢弃计数读 syslog_post_dropped()。其它任务请继续用 syslog_post:
|
||||
* 即时留痕,可观测性更好,别为不存在的栈压力付两倍的认知成本。
|
||||
*/
|
||||
void syslog_post_quiet(syslog_id_t id, const char *desc) {
|
||||
syslog_entry_t e;
|
||||
size_t dl;
|
||||
uint8_t res;
|
||||
|
||||
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT || desc == NULL) {
|
||||
return;
|
||||
}
|
||||
e.id = (uint8_t)id;
|
||||
/* 投递即拷贝(含截断):待发条目不再引用调用方缓冲区,栈缓冲/临时格式化结果
|
||||
* 都能安全投递(见上方 syslog_entry_t 注释的台架实证) */
|
||||
dl = strlen(desc);
|
||||
if (dl > SYSLOG_DESC_MAX) {
|
||||
dl = SYSLOG_DESC_MAX;
|
||||
}
|
||||
memcpy(e.desc, desc, dl);
|
||||
e.desc[dl] = '\0';
|
||||
if (s_queue != NULL && osKernelGetState() == osKernelRunning) {
|
||||
if (osMessageQueuePut(s_queue, &e, 0u, 0u) != osOK) {
|
||||
/* F 项:只报"从正常转入故障"那一次;期间条目照常丢弃(丢弃语义不变,
|
||||
* 变的只是留痕次数)。下一次投递成功即复位。 */
|
||||
if (s_post_qfull_reported == 0u) {
|
||||
s_post_qfull_reported = 1u;
|
||||
DBG_ERROR("queue full, drop: %s", desc);
|
||||
}
|
||||
} else {
|
||||
/* 恢复正常:清抑制位,下一次故障重新留痕。这里刻意无条件写
|
||||
* (曾试成"仅置位时才清",被 cppcheck duplicateConditionalAssign
|
||||
* 判成等价赋值并拦提交闸;本函数上方已做过一次条目拷贝,多一条
|
||||
* STR 可以忽略,别为它加 inline-suppr)。 */
|
||||
s_post_qfull_reported = 0u;
|
||||
s_entry_fill(&e, id, desc);
|
||||
res = s_put(&e);
|
||||
if ((res == (uint8_t)SYSLOG_PUT_Q_FULL) || (res == (uint8_t)SYSLOG_PUT_PB_FULL)) {
|
||||
if (s_quiet_drop < UINT32_MAX) {
|
||||
s_quiet_drop = s_quiet_drop + 1u; /* 留痕在 drain 侧,理由见函数头 */
|
||||
}
|
||||
} else {
|
||||
uint8_t n = s_preboot_cnt;
|
||||
if (n < SYSLOG_QUEUE_MAX) {
|
||||
s_preboot[n] = e;
|
||||
s_preboot_cnt = n + 1u;
|
||||
s_post_pboot_full_reported = 0u; /* 恢复正常:同上,无条件复位 */
|
||||
} else if (s_post_pboot_full_reported == 0u) {
|
||||
/* 同一纪律的另一通道:启动前数组只有 SYSLOG_QUEUE_MAX 槽,挂载慢 +
|
||||
* 开机事件密集时会在整个"未就绪窗口"里每条投递打一行(比队列通道更早
|
||||
* 发生、也更密集)。数组重新收下第一条即视为恢复。 */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取 quiet 通道累计丢弃条数(可观测性;照 soe_store_dropped 同款)
|
||||
* 入口参数:无
|
||||
* 返 回 值:累计丢弃条数 uint32_t
|
||||
*/
|
||||
uint32_t syslog_post_dropped(void) {
|
||||
return s_quiet_drop;
|
||||
}
|
||||
|
||||
void syslog_post(syslog_id_t id, const char *desc) {
|
||||
syslog_entry_t e;
|
||||
uint8_t res;
|
||||
|
||||
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT || desc == NULL) {
|
||||
return;
|
||||
}
|
||||
s_entry_fill(&e, id, desc);
|
||||
res = s_put(&e);
|
||||
switch (res) {
|
||||
case SYSLOG_PUT_Q_OK:
|
||||
/* 恢复正常:清抑制位,下一次故障重新留痕。这里刻意无条件写
|
||||
* (曾试成"仅置位时才清",被 cppcheck duplicateConditionalAssign
|
||||
* 判成等价赋值并拦提交闸;本函数上方已做过一次条目拷贝,多一条
|
||||
* STR 可以忽略,别为它加 inline-suppr)。 */
|
||||
s_post_qfull_reported = 0u;
|
||||
break;
|
||||
case SYSLOG_PUT_PB_OK:
|
||||
s_post_pboot_full_reported = 0u; /* 恢复正常:同上,无条件复位 */
|
||||
break;
|
||||
case SYSLOG_PUT_Q_FULL:
|
||||
/* F 项:只报"从正常转入故障"那一次;期间条目照常丢弃(丢弃语义不变,
|
||||
* 变的只是留痕次数)。下一次投递成功即复位。 */
|
||||
if (s_post_qfull_reported == 0u) {
|
||||
s_post_qfull_reported = 1u;
|
||||
DBG_ERROR("queue full, drop: %s", desc);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
/* SYSLOG_PUT_PB_FULL:同一纪律的另一通道——启动前数组只有
|
||||
* SYSLOG_QUEUE_MAX 槽,挂载慢 + 开机事件密集时会在整个"未就绪窗口"里
|
||||
* 每条投递打一行(比队列通道更早发生、也更密集)。数组重新收下第一条
|
||||
* 即视为恢复(上面 PB_OK 已复位)。故障期间静默,条目照常丢弃。 */
|
||||
if (s_post_pboot_full_reported == 0u) {
|
||||
s_post_pboot_full_reported = 1u;
|
||||
DBG_ERROR("preboot full, drop: %s", desc);
|
||||
} else {
|
||||
/* 故障期间静默,条目照常丢弃 */
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:登记「清除某日志全部记录」的请求(任意上下文;不碰存储、不阻塞)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* 返回值:0 - 请求已登记(同一 id 重复登记合并为一次,幂等)
|
||||
* -1 - id 越界(该 id 的请求位与结果字节一个都没动) int
|
||||
* 限定条件:任意任务上下文(含 uiTask);零 FatFs 触点、零卷锁触点,
|
||||
* 因此**不会**把调用者的喂狗节拍拖进存储路径
|
||||
* 函数说明:MCU 端本函数只代表"已受理",真正的 f_unlink 在 defaultTask 的
|
||||
* syslog_drain() 尾部(s_clear_service)执行,结果读 syslog_clear_state()。
|
||||
* 先清结果再置请求,是为了让 UI 在"排队中"这一拍不会拿上一次的
|
||||
* OK/FAIL 当本次的结论(顺序反了就会出现"按了按钮仍显示上次失败")。
|
||||
* 竞态面(写在 sys_log.h 同一位置,勿只信这里):置请求与写者清请求都是
|
||||
* 单字节写,Cortex-M 上原子;最坏是写者已清账、本函数又置回 1 →
|
||||
* 下一轮多删一次(unlink 幂等),不会丢请求、也不会误清别人的记录。
|
||||
*/
|
||||
int syslog_request_clear(syslog_id_t id) {
|
||||
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) {
|
||||
return -1;
|
||||
}
|
||||
s_clear_st[id] = (int8_t)SYSLOG_CLEAR_NONE;
|
||||
s_clear_req[id] = 1u;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:查「清除记录」的状态(给 UI 出真实反馈,不是"按了就算成功")
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* 返回值:SYSLOG_CLEAR_NONE(0) / SYSLOG_CLEAR_OK(1) / SYSLOG_CLEAR_WAIT(-1) /
|
||||
* SYSLOG_CLEAR_FAIL(-2);id 越界按 NONE 返回 int
|
||||
* 限定条件:任意上下文(只读两个 volatile 单字节,无锁)
|
||||
* 函数说明:排队位优先于结果字节 —— 否则连点两次时会先把上一次的结果当成本次的
|
||||
* 结论,还在排队的请求被显示成"已清空"。两个字节都是 defaultTask 与
|
||||
* uiTask 之间的无锁交接,最坏竞态与为什么不加临界区,见 sys_log.h
|
||||
* syslog_clear_state() 的头注释(同一事实只在那一处详述)。
|
||||
*/
|
||||
int syslog_clear_state(syslog_id_t id) {
|
||||
if ((uint32_t)id >= (uint32_t)SYSLOG_COUNT) {
|
||||
return (int)SYSLOG_CLEAR_NONE;
|
||||
}
|
||||
if (s_clear_req[id] != 0u) {
|
||||
return (int)SYSLOG_CLEAR_WAIT;
|
||||
}
|
||||
if (s_clear_st[id] == (int8_t)SYSLOG_CLEAR_OK) {
|
||||
return (int)SYSLOG_CLEAR_OK;
|
||||
}
|
||||
if (s_clear_st[id] == (int8_t)SYSLOG_CLEAR_FAIL) {
|
||||
return (int)SYSLOG_CLEAR_FAIL;
|
||||
}
|
||||
return (int)SYSLOG_CLEAR_NONE;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:获取记录条数(= filesize / SYSLOG_REC_SIZE,O(1))
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
#include "run_par.h" /* 卡 10:死区真源 + 运行参数段 104 读写路由 */
|
||||
#include "prot_elm.h" /* 板端源:遥信 = 元件状态 */
|
||||
#include "prot_set.h" /* 板端源:定值 + 闭锁 */
|
||||
#include "sys_log.h" /* task-10:遥控记录(SYSLOG_YK)/ 操作记录(SYSLOG_OPER) */
|
||||
#include "adc_input.h" /* adc_semantic_t(phasor 实例按语义绑定) */
|
||||
|
||||
/* ---------------- YC 表(§1.5 点号 4011+;死区列 = ROM 默认值) ------------ */
|
||||
@@ -544,25 +545,68 @@ static uint64_t s_src_yx_valid(void) {
|
||||
* YK12 走 prot_set_set_sealed(原子写+封存,B7);protectTask 下一拍
|
||||
* 读到压板态即复位元件并停判据(protect_task.c)。
|
||||
*/
|
||||
/* 遥控记录的文案表(task-10)。**全部 ROM 常量、零拼接**:本回调跑在
|
||||
* iec104Task,而 iec104_serv.c 的日志缓冲已经因为"深链模块"被收到 128 B,
|
||||
* 这里再 snprintf 一条"YK ioa=0x600B dpc=2 sel=0 rc=0"就是往那条链上加缓冲;
|
||||
* 记录类型由 SYSLOG_YK 这个文件本身回答,屏上话术归 UI(App/log/README.md 口径)。
|
||||
* 最长一条 20 字符 < SYSLOG_DESC_MAX(28)。 */
|
||||
#define YK_REC_YK11_SEL "YK11 select"
|
||||
#define YK_REC_YK11_EXEC "YK11 exec"
|
||||
#define YK_REC_YK12_SEL "YK12 select"
|
||||
#define YK_REC_YK12_ON "YK12 exec on"
|
||||
#define YK_REC_YK12_OFF "YK12 exec off"
|
||||
#define YK_REC_YK12_FAIL "YK12 exec fail"
|
||||
#define YK_REC_DPC_BAD "YK12 bad dpc"
|
||||
#define YK_REC_UNKNOWN "YK unknown ioa"
|
||||
|
||||
static int s_src_yk_exec(uint32_t ioa, uint8_t dpc, uint8_t select) {
|
||||
switch (ioa) {
|
||||
case 0x600Au: /* YK11 信号复归 → 元件状态复位(异步一拍) */
|
||||
(void)dpc; /* 复归与双点值无关 */
|
||||
if (select == 0u) {
|
||||
prot_elm_request_reset();
|
||||
/* 遥控记录(执行段已受理)+ 操作记录(本地/远端强制复归)。
|
||||
* 两条不是重复留痕:前者回答"主站下了什么命令、结果如何",后者
|
||||
* 回答"装置被谁改动了状态"——UI 的遥控记录页与操作记录页各读一份。
|
||||
* YK11 没有可失败的下游(request_reset 只置单字节标志),所以它
|
||||
* 只有受理态,没有 fail 分支。 */
|
||||
syslog_post_quiet(SYSLOG_YK, YK_REC_YK11_EXEC);
|
||||
syslog_post_quiet(SYSLOG_OPER, "force reset");
|
||||
} else {
|
||||
syslog_post_quiet(SYSLOG_YK, YK_REC_YK11_SEL);
|
||||
}
|
||||
return 0;
|
||||
case 0x600Bu: /* YK12 全局软压板(PS_GLOBAL_EN)两段式 */
|
||||
case 0x600Bu: { /* YK12 全局软压板(PS_GLOBAL_EN)两段式 */
|
||||
int rc;
|
||||
if ((dpc != (uint8_t)IEC104_DPC_ON) && (dpc != (uint8_t)IEC104_DPC_OFF)) {
|
||||
syslog_post_quiet(SYSLOG_YK, YK_REC_DPC_BAD);
|
||||
return -1; /* 非法双点命令:选择段即拒 */
|
||||
}
|
||||
if (select != 0u) {
|
||||
syslog_post_quiet(SYSLOG_YK, YK_REC_YK12_SEL);
|
||||
return 0; /* 选择段 = 只校验 */
|
||||
}
|
||||
return prot_set_set_sealed(PS_GLOBAL_EN,
|
||||
(dpc == (uint8_t)IEC104_DPC_ON)
|
||||
? PROT_SET_Q10(1) : PROT_SET_Q10(0));
|
||||
/* 执行段:set_sealed 自带范围校验与封存,失败 = 值被拒、RAM 未改。
|
||||
* 这里**必须接住返回值再落记录**(bsp-err:错误码不得裸丢弃;
|
||||
* 原实现是 return 直出,现在多一条记录后仍然原值返回,语义不变)。 */
|
||||
rc = prot_set_set_sealed(PS_GLOBAL_EN,
|
||||
(dpc == (uint8_t)IEC104_DPC_ON)
|
||||
? PROT_SET_Q10(1) : PROT_SET_Q10(0));
|
||||
if (rc != 0) {
|
||||
syslog_post_quiet(SYSLOG_YK, YK_REC_YK12_FAIL);
|
||||
} else {
|
||||
syslog_post_quiet(SYSLOG_YK,
|
||||
(dpc == (uint8_t)IEC104_DPC_ON) ? YK_REC_YK12_ON
|
||||
: YK_REC_YK12_OFF);
|
||||
/* 压板投退是一次真实的运维动作(保护退出/恢复服务)→ 同时进操作记录 */
|
||||
syslog_post_quiet(SYSLOG_OPER,
|
||||
(dpc == (uint8_t)IEC104_DPC_ON) ? "plate in"
|
||||
: "plate out");
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
default:
|
||||
syslog_post_quiet(SYSLOG_YK, YK_REC_UNKNOWN);
|
||||
return -1; /* 其余 YK:无出口硬件/无对应能力 → 拒绝 */
|
||||
}
|
||||
}
|
||||
|
||||
+11
-2
@@ -40,6 +40,7 @@
|
||||
#include "cfg_file.h"
|
||||
#include "run_par.h" /* 运行参数段落盘(卡 10,配置引擎属主 = 本任务) */
|
||||
#include "prot_set_store.h"
|
||||
#include "prot_set.h" /* task-10:prot_set_dirty_peek(区分"无待写"与"真落盘") */
|
||||
#include "sys_log.h"
|
||||
#include "sys_events.h"
|
||||
/* 深调用链模块(本任务 = 存储 bring-up + 配置引擎属主 + syslog 唯一写者,
|
||||
@@ -230,8 +231,16 @@ void default_task_func(void *argument) {
|
||||
task_wd_seg(TASK_WD_SEG_CFGSAVE);
|
||||
net_cfg_save_run();
|
||||
/* 保护定值落盘(B7):内部消费脏标志,无改动 = 空操作;
|
||||
* 写失败脏回置 → iec104Task 下轮 poll 再投递事件 = 天然重试 */
|
||||
(void)prot_set_store_save();
|
||||
* 写失败脏回置 → iec104Task 下轮 poll 再投递事件 = 天然重试。
|
||||
* task-10「操作记录」写入点之一:⚠ 不能按 prot_set_store_save() 的返回值
|
||||
* 记——它对"无待写"同样返 0,照返回值记就是每 50 ms 刷一条**没发生过**的
|
||||
* 操作(正是"不许用静态演示数据充数"的反面)。先 peek 脏位(非消费,
|
||||
* iec104_serv.c 通知属主用的就是同一枚),只有"确实有待写且这趟写成功"
|
||||
* 才算一次整定操作。失败不记(判据 = 动作成功后写一条),脏位保留、
|
||||
* 下轮重试成功后才留痕。 */
|
||||
if ((prot_set_dirty_peek() != 0u) && (prot_set_store_save() == 0)) {
|
||||
syslog_post(SYSLOG_OPER, "settings saved");
|
||||
}
|
||||
/* 运行参数段落盘(卡 10):同款脏语义;save = 引擎整档外科写
|
||||
* ([run_par] 9 键 + 被 adc_cal_set 联动改写的 [adc_cal] 两段一趟落) */
|
||||
(void)run_par_store_save();
|
||||
|
||||
+19
-3
@@ -18,6 +18,7 @@
|
||||
#include "prot_set.h" /* 定值脏标志 peek(B7 落盘通知) */
|
||||
#include "run_par.h" /* 运行参数脏 peek(卡 10 落盘通知,同款语义) */
|
||||
#include "sys_clock.h"
|
||||
#include "sys_log.h" /* task-19:对时成功落一条操作记录(SYSLOG_OPER) */
|
||||
#include "sys_events.h"
|
||||
#include "task_watchdog.h"
|
||||
#include "dbg_log.h"
|
||||
@@ -85,19 +86,34 @@ static void s_send_apdu(void *ctx, const uint8_t *apdu, uint16_t len) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:link 层对时回调——epoch 毫秒落系统墙钟(含 SD2506 RTC 写回)
|
||||
* 函数功能:link 层对时回调——epoch 毫秒落系统墙钟(含 SD2506 RTC 写回),
|
||||
* 成功时顺带落一条操作记录(道F task-19:「时钟设置」是四处操作记录
|
||||
* 写入点之一,另外三处见 App/log/README.md 的五类记录对照表)
|
||||
* 入口参数:ctx 未用;epoch_ms = C_CS 解析出的 UTC 时刻
|
||||
* 返回值:0 成功 / -1 失败(link 层回 actcon+PN)
|
||||
* 函数说明:uint32 秒域上限 2106-02-07,越界显式拒绝(CP56 域到 2127)
|
||||
* 函数说明:uint32 秒域上限 2106-02-07,越界显式拒绝(CP56 域到 2127)。
|
||||
* **只在真正改成功之后才投记录**:越界拒绝与 sys_clock_set_unix 失败两条腿
|
||||
* 一条都不投——操作记录是审计面,写进去一条没发生过的"对时"比少一条更坏
|
||||
* (TC-I104SRV-050b / 051 分别钉这两条腿)。
|
||||
* 投递用 syslog_post_quiet():iec104Task 是深链任务(本文件顶部已把
|
||||
* DBG_LINE_BUF_MAX 收到 128 就是同一件事的证据),不许在它的调用链上
|
||||
* 内联日志行缓冲;两个投递口的收/丢判据一致,差别只在留痕位置
|
||||
* (口径与理由见 App/log/README.md「两个投递口」)。
|
||||
* 文案是 ROM 常量、17 字节 < SYSLOG_DESC_MAX(28):iec104Task 里不 snprintf。
|
||||
*/
|
||||
static int s_clock_sync(void *ctx, uint64_t epoch_ms) {
|
||||
uint64_t secs = epoch_ms / 1000ULL;
|
||||
int rc;
|
||||
|
||||
(void)ctx;
|
||||
if (secs > 0xFFFFFFFFULL) {
|
||||
return -1;
|
||||
}
|
||||
return sys_clock_set_unix((uint32_t)secs);
|
||||
rc = sys_clock_set_unix((uint32_t)secs);
|
||||
if (rc == 0) {
|
||||
syslog_post_quiet(SYSLOG_OPER, "clock set by 104");
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
#include "prot_elm.h"
|
||||
#include "prot_set.h"
|
||||
#include "soe_store.h" /* B9:动作/复归边沿 → SOE 投递(零 FS,队列异步) */
|
||||
#include "sys_log.h" /* task-10:事故 / 保护告警 / 保护启动 三类文本记录 */
|
||||
#include "task_watchdog.h"
|
||||
#include "tpafe5160.h" /* tpafe5160_frame_seq():帧序号(BUSY EXTI 写) */
|
||||
|
||||
@@ -74,6 +75,7 @@ static adc_health_age_t s_age_tp; /* TPAFE 车道 */
|
||||
static adc_health_age_t s_age_a1; /* STM32 车道 */
|
||||
|
||||
static uint32_t s_prev_active; /* 上拍动作位图(边沿日志用) */
|
||||
static uint32_t s_prev_pick; /* 上拍启动位图(「保护启动」记录边沿用,task-10) */
|
||||
static uint8_t s_prev_hf_lock; /* 上拍采样闭锁态(边沿日志用) */
|
||||
static uint8_t s_prev_plate_out; /* 上拍全局软压板退出态(边沿日志用,B7) */
|
||||
static uint8_t s_prev_cal_lock; /* 上拍未标定闭锁态(边沿日志用,卡 5) */
|
||||
@@ -138,6 +140,61 @@ static void s_log_active_edge(uint32_t active) {
|
||||
s_prev_active = active;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把元件状态边沿落成三类文本记录(事故 / 保护告警 / 保护启动)
|
||||
* 入口参数:active - 本拍 prot_elm_active_mask()(asserted 位图) uint32_t
|
||||
* pick - 本拍 prot_elm_pick_mask()(picked 位图) uint32_t
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅本任务上下文;**只投递、不碰盘**(唯一写者是 defaultTask),
|
||||
* 投递走 syslog_post_quiet()——本任务静态最坏链是 B9 写进 DoD 的硬线
|
||||
* (872 B / 57%),syslog_post 的两个满态留痕分支各内联约 216 B 行缓冲,
|
||||
* 在这里调它等于把链垫回 stack_report 的 FAIL 区(同本文件不用
|
||||
* snprintf/DBG 拼记录的纪律,见 s_log_active_edge 函数头)
|
||||
* 函数说明:三类记录的**类型就是目标文件**(一类一个 /log 文件,记录体内没有
|
||||
* 类型字段位;决策与理由见 App/log/README.md)。判据:
|
||||
* 1) picked 上升沿 → SYSLOG_START("保护启动"):越限电平一到就记,
|
||||
* 不等定时限到点——只记动作会让"启动了但没到点"那段在记录里消失;
|
||||
* 2) asserted上升沿 → 归 事故 还是 告警 由 prot_elm_record_kind() 判,
|
||||
* 其判据唯一来源是定值表里的 PS_*_TRIP(0 告警 / 1 跳闸);本函数
|
||||
* 不另立"哪些元件算跳闸"的名单,避免与 g_prot_set_meta / k_pe[] 打架。
|
||||
* 返回沿(asserted/picked 掉落)**不记**:复归留痕已由 s_log_active_edge
|
||||
* 的 SOE 通道承担,再写一份就是同一事件两个真源。
|
||||
* 同拍既启动又动作(延时段 0ms / 速断)→ 两条都记,顺序先启动后动作,
|
||||
* 与物理事件次序一致。
|
||||
* desc 直接用 prot_elm_name() 的 ROM 常量("OC1-IA" 等,最长 6 字节
|
||||
* << SYSLOG_DESC_MAX=28),零格式化、零栈缓冲。
|
||||
*/
|
||||
static void s_log_record_edge(uint32_t active, uint32_t pick) {
|
||||
uint32_t p_chg = pick ^ s_prev_pick;
|
||||
uint32_t a_chg = active ^ s_prev_active;
|
||||
uint32_t i;
|
||||
|
||||
for (i = 0u; (i < (uint32_t)PE_COUNT) && (((p_chg | a_chg) != 0u)); i++) {
|
||||
const uint32_t bit = 1u << i;
|
||||
|
||||
if (((p_chg & bit) != 0u) && ((pick & bit) != 0u)) {
|
||||
syslog_post_quiet(SYSLOG_START, prot_elm_name((prot_elm_id_t)i));
|
||||
}
|
||||
if (((a_chg & bit) != 0u) && ((active & bit) != 0u)) {
|
||||
switch (prot_elm_record_kind((prot_elm_id_t)i)) {
|
||||
case PROT_REC_TRIP:
|
||||
syslog_post_quiet(SYSLOG_ACCIDENT, prot_elm_name((prot_elm_id_t)i));
|
||||
break;
|
||||
case PROT_REC_ALARM:
|
||||
syslog_post_quiet(SYSLOG_ALARM, prot_elm_name((prot_elm_id_t)i));
|
||||
break;
|
||||
default:
|
||||
/* PROT_REC_NONE:元件号越界。位图来自 prot_elm 自己的表,正常
|
||||
* 走不到这里;真走到了就是表与位图不一致,一条都不能落——
|
||||
* 落错类别的审计记录比少一条记录更坏。 */
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
s_prev_pick = pick;
|
||||
s_prev_active = active;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:protectTask 线程入口
|
||||
* 入口参数:argument - 未用
|
||||
@@ -242,6 +299,8 @@ void protect_task(void *argument) {
|
||||
}
|
||||
|
||||
s_log_active_edge(prot_elm_active_mask());
|
||||
/* 三类文本记录(task-10):放在 SOE 边沿之后、喂狗之前,纯 RAM 投递 */
|
||||
s_log_record_edge(prot_elm_active_mask(), prot_elm_pick_mask());
|
||||
|
||||
task_wd_beat(TASK_WD_PROTECT);
|
||||
osDelay(PROTECT_TASK_CYCLE_MS);
|
||||
|
||||
Reference in New Issue
Block a user