feat(UI/采集/104): 通讯设置页 + 开机账本 + adc_cal 可诊断化——三车道同树一次构建入库

三条道在同一棵树上交付,故合并成**一次权威构建**(@build 0 错 0 警,Code=235236 RO=26308 RW=1572
ZI=136924)后同笔入库;台账已按该构建回填(`ram_baseline.py --update` + `--write`、`chec
k_mem_numbers.py
 --write`),凭证与实际 .map 同源。归属如下:

· **通讯设置页**(道G task-9):新增 `Drivers/BSP/UI/ui_page_comm.{c,h}`(通道六注入,与死
区页同骨架同纪
  律),`#page=comm` 绑定使 bound **21 → 22**、内容页黄金 **47 → 53**(旧 47 页字节零变化,
新增六态含
  WAIT/OK 两张);`iec104_comm` 脏位 + store_save(**无事件位**)挂 defaultTask 同点位
轮询;`port_104` 由
  宏改走 getter、`IEC104_SERV_PORT` 删除;字体 +听/监(185 字全覆)。**实机链已闭环**:FTP 外改
[comm]
  的 ca/port → 复位 → 串口出现 `section 'comm' (2 keys)` 与 `i104: listening o
n 9004` → 还原 2404,全程无
  FAIL ⇒「重启即生效 + 端口真值」不是推断是实测。上限核对 6/8 段、2/16 键,超限现象由 TC-CONFIG-056/057
  钉死(注册 -1 + `comm init: FAIL` 锚 + getter 回 ROM),**不抬宏**。
· **开机账本**(道F task-12):新增 `App/util/boot_stats.{c,h}`——上电次数落 SD2506 用户
 SRAM(magic +
  小端计数 + 版本 + 异或校验),一次上电 = 1 读 + ≤1 写,**零 NAND、零卷锁、零配置段**;四态互斥
  UNKNOWN / FRESH / CONTINUED / RESTARTED(BLF=1 才算电池域掉过电),**读失败绝不回写**;
文案口径「自 RTC
  电池周期起」,**禁写终身次数**。复位原因优先级链从 `app_main.c` 的 if-else 收进一份真源(串口与屏幕同
  源);104 四个只增不清计数 + `iec104_serv_get_stats()` 留在 serv、未塞进 link(会被 open
/close 洗掉)。
  运行时长按用户裁定 = **只报本次开机以来**,零新增代码。新套件 `test/host/boot_stats` 21/21,五处注入
  **全部有牙**(含「漂一字节被校验和挡下而不是报 256 倍错账」)。
· **adc_cal 可诊断化**(道E task-43):三个 volatile 计数 + `adc_cal_get_stats()`
/ `adc_cal_note_persisted()`
  (水位**盖写非自增**,因为一次 save 覆盖整档,数 save 会留假未落盘);「差额 × 脏位」三态语义唯一落在
  `adc_cal.h`,其中「差额 > 0 且脏位恒 0」= 绕过语义 setter 的静默丢失;TC-ADCCAL-019~023。

三条**必须留档的自纠**(都是执行道自己报的,我不替他们润色):
① stdint 那条断链是**宿主套件抓到、通讯道先修**,采集道承认自己是重复添加者并删掉自己那份;
② 「exit 2 不是证据,断言变红才是」——采集道第一版反证注入了未声明标识符导致编译错,差点当成功;
③ 套件汇总行原写 `s_total/s_total`,失败时会打印「21/21 PASSED (failed=1)」这种自相矛盾的行,已改。

另修:`check_registration.py` 的 `SIM_EXEMPT` 加一行豁免 `App/util/boot_stats.c
`(依赖 I2C 与 RCC 位掩码,
MCU 专属;核过 `sys_clock.c`/`sha256.c` 同形态先例),注册反向校验 exit 1 → 0。发号 TC-IEC1
04-034、
TC-UIDATA-029 已实抬游标(`TC-IEC104-033→034`、`TC-UIDATA-026→029`)。

门禁:id-check --strict 与 plain 均 rc=0、doc-links --all rc=0、注册校验 0、cppche
ck / coding-standard /
bom-drift 0;iec104 179/179、ui_data 295/295、boot_stats 21/21、adc_cal 11
7/117、run_par 67/67;菜单 8 状态
池 5368 不变、内容页 53/53。**已知遗留**:内容页数 47 → 53 的全仓文本同步尚未做(另卡跟踪),本笔只保
证代码与黄金一致;面板注键真机那一腿缺 CLI,交台架道补。
This commit is contained in:
2026-10-27 11:29:23 +08:00
parent 0ae9c10f24
commit 728d60675f
47 changed files with 2364 additions and 123 deletions
+39
View File
@@ -92,6 +92,15 @@ static const adc_cal_bcfg_t s_bcfg_rom = { { 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L } };
* adc_cal_init 清零 = 新一轮观测窗) */
static uint8_t s_err_latched;
/*
* 观测计数(三态语义唯一落点在 adc_cal.h,这里只写"怎么自增"):与上面那个留痕位
* 不同,这三个数一个 [ERR] 都不打、也不参与任何判定,因此 **adc_cal_init() 刻意不清**
* ——清了水位与计数就失配,会凭空造出一段"假未落盘"差额。
*/
static volatile uint32_t s_st_set_ok; /* 成功改写 RAM 副本的次数 */
static volatile uint32_t s_st_set_reject; /* 被拒次数(非法通道 / 越出合法带) */
static volatile uint32_t s_st_persist_water; /* 落盘水位:盖写,绝不自增 */
/* 键表:offset 用 offsetof 取真值(k[0]@0 / k[1]@4 … valid@32) */
static const cfg_key_def_t s_k_keys_tbl[9] = {
{ "k_ua", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[0]), 4u },
@@ -355,19 +364,49 @@ float adc_cal_b_codes(adc_semantic_t sem) {
*/
int adc_cal_set(adc_semantic_t sem, int32_t k_q16, int32_t b_codes) {
if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) {
s_st_set_reject++; /* 观测:连通道号都非法,RAM 未动 */
return -1;
}
if (s_kband_ok((unsigned)sem, k_q16, b_codes) == 0) {
DBG_ERROR("set ch=%u rejected: k_q16=%ld b=%ld out of ROM band [0.5x,2x]/|b|<=%ld",
(unsigned)sem, (long)k_q16, (long)b_codes, (long)ADC_CAL_B_MAX);
s_st_set_reject++; /* 观测:越出合法带,RAM 未动([ERR] 由上一行打) */
return -1;
}
s_kcfg.k[sem] = k_q16;
s_bcfg.b[sem] = b_codes;
s_kcfg.valid |= (uint8_t)(1u << (unsigned)sem);
s_st_set_ok++; /* 观测:RAM 副本确实被改写了(与落盘无关) */
return 0;
}
/*
* 函数功能:读标定层观测计数(不取锁、不取时刻、任意上下文可读)
* 返 回 值:无 void
* 限定条件:无;adc_cal_init() 之前可读(全 0 = 本层还没被改写过)
* 函数说明:见 adc_cal.h 同名声明(三态语义与那张成对读表都在那里,本处只取数)
*/
void adc_cal_get_stats(adc_cal_stats_t *p_out) {
if (p_out == NULL) {
return; /* 观测件不得反过来制造故障 */
}
p_out->set_ok = s_st_set_ok;
p_out->set_reject = s_st_set_reject;
p_out->persisted_set_ok = s_st_persist_water;
}
/*
* 函数功能:落盘成功回执——把水位盖写为当前的 set_ok(不是自增)
* 返 回 值:无 void
* 限定条件:仅"整档保存已成功"那一条路径(当前唯一调用点 run_par_store_save())
* 函数说明:盖写而非自增的理由与回归用例见 adc_cal.h 同名声明。水位直接取 set_ok 的
* 当前值,因此它**在结构上不可能越过** set_ok——差额是 uint32 相减,一旦越过
* 就绕回成一个巨大的假未落盘数,这里不留那种可能
*/
void adc_cal_note_persisted(void) {
s_st_persist_water = s_st_set_ok; /* 水位只会追平,不会越过 */
}
/*
* 函数功能:两点标定公式(卡 12 的算术核,纯函数无状态)
* 入口参数:c_lo/c_hi、v_lo_uv/v_hi_uv、p_k_q16/p_b 见 adc_cal.h 同名声明
+66 -2
View File
@@ -158,8 +158,11 @@ float adc_cal_b_codes(adc_semantic_t sem);
* 限定条件:**整定路径上下文**(defaultTask 标定流程/整定链;卡 10 起 iec104Task
* 经 App/algo/run_par.c 的 0x8040~/0x8048~ 直通写亦为授权写者——写的仍是
* 引擎对齐 int32 单字,µs 级混窗契约同「数据可信」§4.4)。勿在 adcTask 调用
* 函数说明:只写 RAM 副本;落盘由属主经 cfg_file_save 完成(卡 10 前整定链
* 未接,台架两点标定(卡 12)是首个消费者)
* 函数说明:**只写 RAM 副本,不置脏、不落盘、不返回"稍后会存"的承诺**——落盘由属主经
* cfg_file_save 完成,而"要不要落盘"这件事只有一个真源:run_par 的脏标志。
* 因此授权写者是 run_par 的语义 setter(写值 + 置脏成对发生,lead 裁②);
* 绕过它直调本函数 = 值只活在 RAM 里,重启即丢且**没有错误返回**,
* 该形态现可被本层观测计数抓到(差分口径见下方 adc_cal_stats_t 那张表)
*/
int adc_cal_set(adc_semantic_t sem, int32_t k_q16, int32_t b_codes);
@@ -189,6 +192,67 @@ int adc_cal_two_point(int32_t c_lo, int32_t c_hi, int32_t v_lo_uv, int32_t v_hi_
*/
int adc_cal_rail_ths(adc_semantic_t sem, int32_t *p_lo_th, int32_t *p_hi_th);
/*
* 观测计数:把「改了没落盘」从意图式陈述变成可诊断事实(道G task-43 第②项;
* 先例 App/fs/vol_lock.h 的同族设计——只计数、不取锁、不在调用点打日志、不打 [ERR])
*
* 三态语义(**定义只在本头文件这一处**,消费者只描述取数与断言,不重定义):
* set_ok RAM 副本被本层 API 成功改写的累计次数
* set_reject 被本层拒掉的整定请求累计次数(参数非法 / 越出合法带)
* persisted_set_ok **水位**而不是计数:最近一次随整档保存落盘成功时的
* set_ok 快照(由 adc_cal_note_persisted() 盖写,永不越过 set_ok)
*
* 未落盘差额 = set_ok − persisted_set_ok,即"自上次成功落盘之后被改写的次数"。
* 单看差额不够定性,必须与 run_par_dirty_peek() 成对读(两列一起才只有一种解释):
*
* 差额 run_par_dirty_peek() 说的是什么事
* ------ ---------------------- ----------------------------------------------
* 0 0 盘与 RAM 齐(或压根没改过)——健康
* >0 1 改了、还没到落盘时机——正常在途
* >0 0 **改了且永远不会落盘**:写者绕过 run_par 的语义
* setter 直调 adc_cal_set()(本卡起的事)
*
* 消费者告诫(与 vol_lock_get_stats() 同一套,别重犯):
* ① 本组接口不取锁、不取时刻,任意上下文可读;三个数各自是 uint32 单字读写,
* Cortex-M4 上原子,但三个数之间**没有共同时刻**——别把它们拼成"一个瞬间的快照"
* 下结论,定性只按上面那张两列表成对读。
* ② 需要"被观测到的时刻"请在消费者侧自盖并冠 obs_ 前缀命名(先例:喂狗点单写者 latch),
* 本层不存时刻——存了就得连带管溢出与单位。
* ③ 这些计数**不参与任何判定**:能不能信只看 adc_cal_flags() /
* adc_cal_prot_blocked(),要不要落盘只看 run_par 的脏标志。别拿差额去改控制流,
* 那等于把观测升级成第二真源(本卡第①项已由 run_par 语义 setter 收口,不再加口)。
* ④ 写清盲区:本层刻意**不挂** cfg_file 回调,因此"盘 → RAM 的覆盖"(cfg_file_load
* 与外部变更重载)不产生任何计数;未落盘的改值被一次重载无声吞掉,正是这三个
* 数照不到的那一条。要照它得先给本层挂回调 = 行为改动,另卡处置。
*/
typedef struct {
uint32_t set_ok; /* 成功改写 RAM 副本次数(见上方唯一落点) */
uint32_t set_reject; /* 被拒次数(见上方唯一落点) */
uint32_t persisted_set_ok; /* 落盘水位,不是计数(见上方唯一落点) */
} adc_cal_stats_t;
/*
* 函数功能:读标定层观测计数(只读,不改任何状态)
* 入口参数:p_out - 输出:三个观测计数 adc_cal_stats_t *
* 返 回 值:无 void
* 限定条件:任意上下文可读(含 ISR 与内核启动前);adc_cal_init() 之前可读,得全 0
* 函数说明:p_out 为 NULL 时静默返回——观测件不得反过来制造故障
*/
void adc_cal_get_stats(adc_cal_stats_t *p_out);
/*
* 函数功能:落盘成功回执——把水位 persisted_set_ok 盖写为当前的 set_ok(**不是自增**)
* 入口参数:无 void
* 返 回 值:无 void
* 限定条件:只允许配置引擎属主侧"整档保存已成功"那一条路径调用;当前唯一调用点 =
* App/algo/run_par.c 的 run_par_store_save() 中 cfg_file_save() 返 0 之后
* 函数说明:为什么盖写不自增——一次 save 覆盖整档,可能对应多次整定改写(逐通道
* 标定 3 路 = 3 次 adc_cal_set + 1 次 save);自增会留下 2 次"假未落盘"。
* 水位语义让差额只反映"上次落盘之后的改写",回归锁见
* test/host/adc_cal/adc_cal_test.c 的 TC-ADCCAL-021
*/
void adc_cal_note_persisted(void);
#ifdef __cplusplus
}
#endif
+4
View File
@@ -354,6 +354,9 @@ uint8_t run_par_dirty_peek(void) {
* 限定条件:defaultTask(配置引擎属主)上下文,SYS_EVT_RUN_PAR_SAVE 驱动
* 函数说明:一次 save 同时覆盖 [run_par] 9 键与刚被 adc_cal_set 改写的
* [adc_cal]/[adc_cal_ofs] 副本(lead 裁②:一条事件位一次 save)。
* 成功路径上给标定层盖一次落盘水位(adc_cal_note_persisted())——它是
* **观测回执**,不参与本函数任何判定;失败路径不盖,于是"save 一直失败"
* 在标定层侧表现为差额长期不归零(道G task-43 第②项)。
*/
int run_par_store_save(void) {
if (s_dirty == 0u) {
@@ -364,5 +367,6 @@ int run_par_store_save(void) {
return -1;
}
s_dirty = 0u;
adc_cal_note_persisted(); /* 观测回执:水位追平,差额归零(不是判定输入) */
return 0;
}
+21 -2
View File
@@ -22,6 +22,18 @@
* 保存(含 [run_par] 与刚被 adc_cal_set 改写的 [adc_cal]/[adc_cal_ofs]
* 副本)→ 成功清脏;失败留脏 = 下轮天然重试。本模块自身零文件 IO
* (属主纪律)。
* ⚠ 三份职责别混(道G task-43 第③项,此前只有意图式陈述):
* - **谁写 RAM**:系数/零点的唯一真源是 adc_cal 的引擎副本,而本模块那两支
* 语义 setter(run_par_cal_k_set / run_par_cal_b_set,task-7 落地)是**唯一
* "写值 + 置脏"成对发生**的入口;任何消费者绕过它们直调 adc_cal_set(),
* 值只进 RAM、脏标志不动 ⇒ 屏上"当前生效"是新的、盘上仍是旧的、重启即丢,
* 且该路径**没有错误返回**。这条形态现在可观测:adc_cal 侧
* set_ok 与落盘水位的差额 > 0,而 run_par_dirty_peek() 恒为 0。
* - **谁落盘 / 什么时机**:只有 defaultTask 的 run_par_store_save()(由
* iec104Task peek 后置的 SYS_EVT_RUN_PAR_SAVE 驱动);本模块零文件 IO。
* - **cfg_file_poll() 不是保存路径**:它的方向是**盘 → RAM 的重载**(外部
* 改档 → 沉降去抖 → 重载),拿它"兜住"未落盘的改值会把 FTP 侧的旧档倒灌
* 回 RAM,比不兜更坏。设计依据见 docs/算法/采集与算法层设计说明.md §10.1。
* 并发契约:引擎副本 int32 对齐字读写在 Cortex-M4 上原子;poll 重载 µs 级窗口
* 为「数据可信」§4.4 明载设计(adc_cal 同款)。写路径 = 104 执行段
* (iec104Task)与引擎读路径互不加锁,勿改成"读侧缓存"(缓存 = 新真源)。
@@ -41,6 +53,10 @@
* RUN_PAR_K_MAX_UV;毫→工程换算收进模块内单点
* s_milli_to_eng()(顺带把 task-43 第①项所需的
* "写值必置脏"这一半满足掉)
* v1.2 2026-10-27 DTU 项目组 道G task-43 第②③项:run_par_store_save() 成功路径
* 给标定层盖一次落盘水位(纯观测回执,不参与判定);
* 本头把"谁写 RAM / 谁落盘 / 什么时机 / poll 不是保存
* 路径"从意图式陈述改成逐条契约
*/
#ifndef RUN_PAR_H
@@ -88,8 +104,11 @@ 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 若整定
* 需另行走配置引擎属主路径,勿跨任务直写)
* 限定条件:写者两门——**iec104Task**(104 C_SE 执行段)与 **uiTask**(遥测死区页
* 经 ui_data 通道五注入转达,道F task-7 起;ui_data.h 通道五表注:合法域
* 与"写值+置脏"必须原子发生在执行侧)。两门都只写 RAM 副本 + 置脏,
* 落盘统一收敛在 defaultTask(勿在 adcTask 调用;此句为 2026-10-27 措辞
* 收口——原写"UI 若整定需另行走属主路径"是 task-2 期遗留,task-7 已接线)
* 函数说明:合法域按点型分档——电压 0~50 V、电流 0~1 A、频率 0~5 Hz(上限 =
* 额定 × 二档裕量,守卫目的是挡误写不是限工况;见 .c 常量段)。
* 成功即置脏(落盘请求,见文件头持久化契约)。
+17 -17
View File
@@ -19,6 +19,7 @@
#include "tim.h" /* htim3:1 kHz 采样节拍(TIM3 更新中断触发 CONVST + TRGO 触发 ADC1) */
#include "rs485.h"
#include "sd2506.h"
#include "boot_stats.h" /* 开机账本:上电次数(RTC 用户 SRAM)+ 本次复位原因快照(道G task-12) */
#include "sys_clock.h"
#include "gd5f2gq5ue.h"
#include "tpafe5160.h"
@@ -76,23 +77,11 @@ volatile uint32_t g_rcc_csr_latched;
* 兜底,两者同时置位时报告看门狗更有价值)。纯 ASCII,防编码坑。
*/
static void reset_cause_str(uint32_t csr, char *buf, unsigned int len) {
const char *cause;
if ((csr & RCC_CSR_IWDGRSTF) != 0u) {
cause = "IWDG (independent watchdog)";
} else if ((csr & RCC_CSR_WWDGRSTF) != 0u) {
cause = "WWDG (window watchdog)";
} else if ((csr & RCC_CSR_SFTRSTF) != 0u) {
cause = "software (NVIC_SystemReset)";
} else if ((csr & RCC_CSR_PORRSTF) != 0u) {
cause = "power-on (POR)";
} else if ((csr & RCC_CSR_PINRSTF) != 0u) {
cause = "external pin (NRST)";
} else if ((csr & RCC_CSR_BORRSTF) != 0u) {
cause = "brown-out (BOR)";
} else {
cause = "unknown";
}
/* 优先级链**不在这里再写一遍**:改由 boot_stats 出一份(IWDG > WWDG > 软件 >
* POR > PIN > BOR)。原先这条 if-else 链与页面上要显示的编码各写一份,两份早晚
* 会漂——漂了的症状是串口说 "IWDG"、屏幕写 "software",两条却都"看着有据"。
* 顺序本身的理由见本函数上方原注释与 App/util/boot_stats.h 的函数头。 */
const char *cause = boot_stats_cause_str(boot_stats_cause_from_csr(csr));
/* 简单有界拼接(不引 snprintf,避免依赖与栈开销) */
if (buf == NULL || len == 0u) {
@@ -187,6 +176,17 @@ int app_main_init(void) {
bsp_err_t rtc_ret = sd2506_init();
DBG_INFO("SD2506 init: %s (ret=%d)",
(rtc_ret == BSP_OK) ? "OK" : "FAILED", rtc_ret);
/* 1b) 开机账本(道G task-12):紧接 sd2506_init 之后,是**唯一**的调用点。
* 为什么放这儿而不是 defaultTask 的存储 bring-up:
* ① 这一步只做 1 次 I2C 读 + 至多 1 次 I2C 写,落在内核启动前——
* 单线程、非 ISR、调度器还没跑(本函数不调任何创建类内核 API,
* AGENTS「内核前禁调创建类 API」那条与此无关);
* ② 保持介质是 RTC 用户 SRAM,不是文件系统:不必等 STORAGE_READY,
* 也就不必在喂狗点上碰卷锁(这条链上根本没有卷操作可言);
* ③ sd2506_init 失败时这里自然走"读不到"那一支:状态标成不可信、
* 且不回写,不会把可能还完好的记录覆盖掉。
* 失败一律不打日志(可观测性事件不进 [ERR]),状态留在 boot_stats_get()。 */
boot_stats_init();
}
if (sys_clock_init() != 0) {
syslog_post(SYSLOG_SELFTEST, "clock chip fault");
+75 -2
View File
@@ -25,6 +25,10 @@
* 见 s_param_ops);② 参数帧尾填 cal_block(未标定
* 闭锁徽标,三路口径同 protect_task.c,不用 eng)
* v1.3 2026-10-27 DTU 项目组 道F task-7:遥测整定注入表(ui_data_yt_ops_t)
* v1.4 2026-10-27 DTU 项目组 道G task-9:通讯参数注入表(ui_data_comm_ops_t,
* 通讯设置页);port_104 快照从 IEC104_SERV_PORT 宏
* 切 iec104_comm_port() 真源(宏同笔删除,交接口径见
* IEC104上行设计说明.md §8)
* 装配层实现 + 首轮懒绑(时序与通道四绑定等价,
* 见 feed_yt_bind_once 函数头);行数等值由编译期
* 守卫钉,量纲分档与合法域分档同源同序
@@ -66,7 +70,7 @@
#include "prot_elm.h" /* 元件动作态(遥信位图来源) */
#include "prot_set.h" /* 40 项定值 Q10 + 校验和 + 闭锁 + 越界位图 */
#include "sys_clock.h" /* 墙钟字符串 */
#include "iec104_serv.h" /* IEC104_SERV_PORT:实际监听端口(非协议层同名常量) */
#include "iec104_comm.h" /* 道G task-9:[comm] 真源 getter/setter/脏位;快照端口切 getter,IEC104_SERV_PORT 字面宏同笔删除 */
#include "iec104_pts.h" /* task-15 批4:遥信点数与单/双点性质(点表事实的唯一来源) */
/* 私有宏定义区 */
@@ -612,11 +616,74 @@ static const ui_data_yt_ops_t s_yt_ops = {
feed_yt_pending
};
/* ======================= 道G task-9:通讯参数注入表(ui_data_comm_ops_t)===
*
* 页面形状(两行的名/序)归 ui_page_comm.c;本层只转达值与合法带——
* lo/hi 现取 iec104_comm.h 带宏(唯一出口),值现取 getter(带内自愈),
* 装配层**不写第二份数值**(通道五同一条纪律)。
*/
/*
* 函数功能:读第 idx 行通讯参数现状(值 + 合法带)
* 入口参数:idx - UI_DATA_COMM_ROW_CA/_PORT uint8_t
* a_out - 行现状 ui_data_comm_row_t *
* 返回值:0 - 已整份写出;-1 - idx 越界(不猜、不顶替) int
* 限定条件:uiTask 页面绘制链(经 ui_data 通道六转发)
*/
static int feed_comm_row(uint8_t a_idx, ui_data_comm_row_t *a_out) {
if (a_idx == UI_DATA_COMM_ROW_CA) {
a_out->val = iec104_comm_ca();
a_out->lo = IEC104_COMM_CA_MIN;
a_out->hi = IEC104_COMM_CA_MAX;
return 0;
}
if (a_idx == UI_DATA_COMM_ROW_PORT) {
a_out->val = iec104_comm_port();
a_out->lo = IEC104_COMM_PORT_MIN;
a_out->hi = IEC104_COMM_PORT_MAX;
return 0;
}
return -1;
}
/*
* 函数功能:写第 idx 行通讯参数(守卫在 setter:带外拒、副本不动、不置脏)
* 返回值:0 - 已写并置脏;-1 - idx 越界或带外拒 int
* 限定条件:uiTask 按键链(编辑提交)
* 函数说明:a_val > 65535 不在此先裁——截断成 u16 会把 65536 洗成 0 再被
* setter 合法拒掉,等于替屏幕编了个假值;越界一律原样送 setter 拒
*/
static int feed_comm_set(uint8_t a_idx, uint32_t a_val) {
if (a_val > 65535uL) {
return -1;
}
if (a_idx == UI_DATA_COMM_ROW_CA) {
return iec104_comm_ca_set((uint16_t)a_val);
}
if (a_idx == UI_DATA_COMM_ROW_PORT) {
return iec104_comm_port_set((uint16_t)a_val);
}
return -1;
}
/* 函数功能:[comm] 段落盘脏位(与页面 WAIT/OK 同一枚 peek,不另发明) uint8_t */
static uint8_t feed_comm_pending(void) {
return iec104_comm_dirty_peek();
}
/* 通讯参数注入表(三件齐全;ui_data_comm_bind 拒残表) */
static const ui_data_comm_ops_t s_comm_ops = {
feed_comm_row,
feed_comm_set,
feed_comm_pending
};
/* 首轮懒绑一次:ui_data_feed_tick() 的首个调用点在 App/task/ui_task.c 里先于
* ui_menu_init()(首帧发布那次),与通道四 ui_data_maint_bind 的绑定时序等价;
* 不在 ui_task.c 直接绑是为了本批写域收在派发单列出的文件里。残表是真故障,
* 必须能在启动日志里看见,不能静默(通道四同口径) */
static uint8_t s_yt_bound;
static uint8_t s_comm_bound; /* 道G task-9 通讯参数表懒绑门(同款先置位防刷日志) */
/*
@@ -668,7 +735,7 @@ static void feed_fill_info(ui_data_info_t *p_snap, uint32_t tick_ms) {
p_snap->valid = 1u;
p_snap->uptime_s = tick_ms / 1000u;
p_snap->port_104 = (uint16_t)IEC104_SERV_PORT;
p_snap->port_104 = iec104_comm_port(); /* 真源 = [comm] RAM 副本(getter 带内恒成立,u16 读原子) */
p_snap->port_disp = (uint16_t)UI_NET_SINK_PORT;
feed_copy_str(p_snap->model, sizeof(p_snap->model), FEED_MODEL);
feed_copy_str(p_snap->sw_ver, sizeof(p_snap->sw_ver), FEED_SW_VER);
@@ -706,6 +773,12 @@ void ui_data_feed_tick(void) {
DBG_ERROR("UI yt bind: FAIL");
}
}
if (s_comm_bound == 0u) {
s_comm_bound = 1u; /* 道G task-9:同款一轮一绑;两表时序等价(先于 ui_menu_init) */
if (ui_data_comm_bind(&s_comm_ops) != 0) {
DBG_ERROR("UI comm bind: FAIL");
}
}
now = osKernelGetTickCount();
+48 -4
View File
@@ -40,6 +40,13 @@ static const iec104_comm_t s_comm_rom = {
IEC104_COMM_DEF_CA, IEC104_COMM_DEF_PORT
};
/* 落盘脏位(B7 形态,run_par/prot_set 同款):setter 成功置 1,
* iec104_comm_store_save() 写盘成功清 0。**不挂回调、不加事件位**(task-8
* D5 裁定的持久化面延续):defaultTask 每轮无条件调 store_save,无脏空转。
* 单字节标志:uiTask 写、defaultTask 读清——置 1 幂等、清零窗口最坏
* 多跑一轮 save,语义无损(run_par s_dirty 同一条竞态分析)。 */
static volatile uint8_t s_comm_dirty;
/* 键表(s_comm 布局内偏移,同 net_cfg/ftp_cfg 写法) */
static const cfg_key_def_t s_comm_keys[] = {
{ "ca", CFG_VAL_UINT, offsetof(iec104_comm_t, ca), 2u },
@@ -57,6 +64,7 @@ int iec104_comm_init(void) {
cfg_section_reg_t reg;
(void)memcpy(&s_comm, &s_comm_rom, sizeof(s_comm));
s_comm_dirty = 0u;
(void)memset(&reg, 0, sizeof(reg));
reg.name = IEC104_COMM_SEC;
@@ -100,27 +108,63 @@ uint16_t iec104_comm_port(void) {
/*
* 函数功能:写 ca 进 RAM 副本(守卫 = getter 同带;非法不提交)
* 入口参数:ca - 公共地址 uint16_t
* 返回值:0 提交;-1 带外(副本不动) int
* 限定条件:UI 保存交接路(task-9);落盘走 SYS_EVT_CFG_SAVE 整档外科 save
* 返回值:0 提交并置落盘脏;-1 带外(副本不动、不置脏) int
* 限定条件:UI 保存交接路(通讯设置页经 ui_data 通道六转进来)与宿主测试;
* 落盘 = defaultTask 每轮无条件 iec104_comm_store_save()(无事件位)
*/
int iec104_comm_ca_set(uint16_t ca) {
if ((ca < IEC104_COMM_CA_MIN) || (ca > IEC104_COMM_CA_MAX)) {
return -1;
}
s_comm.ca = ca;
s_comm_dirty = 1u;
return 0;
}
/*
* 函数功能:写 port 进 RAM 副本(守卫 = getter 同带;非法不提交)
* 入口参数:port - 监听端口 uint16_t
* 返回值:0 提交;-1 带外(副本不动) int
* 限定条件:同 iec104_comm_ca_set
* 返回值:0 提交并置落盘脏;-1 带外(副本不动、不置脏) int
* 限定条件:同 iec104_comm_ca_set(生效时刻 = 下一次重 bind 监听)
*/
int iec104_comm_port_set(uint16_t port) {
if ((port < IEC104_COMM_PORT_MIN) || (port > IEC104_COMM_PORT_MAX)) {
return -1;
}
s_comm.port = port;
s_comm_dirty = 1u;
return 0;
}
/*
* 函数功能:落盘脏探查(非消费)——页面保存行 WAIT/OK 的唯一判定源
* 返回值:1 有未落盘改动;0 干净 uint8_t
* 限定条件:无(任意上下文纯读)
* 函数说明:脏位是全段级(ca+port 一次 save 一起落):归零只证明
* 「这一档落盘了」,不证明「我这一项单独落盘」;104 侧无自动
* 写者(ca/port 不经 C_SE 整定),比 run_par 少一条「他人再改」
* 干扰,页面仍按「结果锁存」画(run_par 页同口径,别做两套)
*/
uint8_t iec104_comm_dirty_peek(void) {
return s_comm_dirty;
}
/*
* 函数功能:执行落盘(整档外科 save 成功后清脏;失败脏保持 = 下轮重试)
* 返回值:0 成功/无脏;-1 写盘失败 int
* 限定条件:**defaultTask(配置引擎属主)上下文**——run_par_store_save()
* 同一调用点旁挂,每轮无条件
* 函数说明:失败不打日志是刻意的(App/protocol 零日志惯例,本文件无 DBG):
* cfg_file_save 自身已有 DBG_ERROR 留痕 + 脏位保留 = 屏上 ERR 的
* 判定源,再打一条就是重复锚点(AGENTS 日志成本口径)
*/
int iec104_comm_store_save(void) {
if (s_comm_dirty == 0u) {
return 0;
}
if (cfg_file_save() != 0) {
return -1;
}
s_comm_dirty = 0u;
return 0;
}
+30 -7
View File
@@ -7,12 +7,15 @@
* 判定;初版曾寄身 iec104_pts.c 尾段,2026-10-26 lead 纠偏迁出)。
* 修改记录:
* 2026-10-26 DTU 项目组 创建(道G task-8);同日实现位置改配对拆分。
* 2026-10-27 DTU 项目组 道G task-9:脏位 + iec104_comm_dirty_peek() /
* store_save()(run_par B7 同款,通讯设置页与
* defaultTask 轮询的消费面)。
* 线程约定:注册/复位 = app_main_init 内核前(run_par_init / adc_cal_init 同款
* "先注册后 load"契约);load/save/poll = defaultTask(配置引擎属主,
* netTask/uiTask 不得直接触碰引擎);getter = 任意消费上下文(u16
* 半字对齐读天然原子,重载瞬间最坏 = 单字段新旧值之一);setter =
* UI 保存交接路(task-9,net_cfg_set_* 先例)。
* 生效语义:**改后重启生效**(docs/任务规划/UI补齐.md §2 第 5 行验收口径)——
* 生效语义:**改后重启生效**(通讯设置页验收口径,docs/任务规划/UI补齐.md §2)——
* iec104Task 启动时取一次 getter 填 link cfg;端口另在下次重 bind
* (拆链/网络代次重建)自然生效,不必整机重启。不挂段回调、不新增
* 事件位、不走 SYS_EVT_NET_RECONFIG(网络代次链语义不同;且 CH395F
@@ -34,9 +37,10 @@ extern "C" {
/* 段名(cfg_file 引擎注册用;≤ CFG_FILE_MAX_NAME_LEN) */
#define IEC104_COMM_SEC "comm"
/* 工厂默认:CA = 1(点表口径,V0.1 沿用);端口 = 2404(DL/T 634.5104 标准,
* 与 App/task/iec104_serv.h 的 IEC104_SERV_PORT 同值——该宏现仅作
* ui_data_feed 快照兼容出口保留,运行期真源 = 本段 getter) */
/* 工厂默认:CA = 1(点表口径,V0.1 沿用);端口 = 2404(DL/T 634.5104 标准)。
* 原同名宏 IEC104_SERV_PORT(App/task/iec104_serv.h)已随道G task-9 快照切
* getter 删除——本段 getter 自 task-8 起就是运行期唯一真源,本头文件是这两个
* 常量的唯一出口 */
#define IEC104_COMM_DEF_CA 1u
#define IEC104_COMM_DEF_PORT 2404u
@@ -79,8 +83,9 @@ uint16_t iec104_comm_port(void);
* 函数功能:写 ca 到 RAM 副本(守卫 = getter 合法带;非法值不提交)
* 入口参数:ca - 公共地址 1..65535 uint16_t
* 返回值:0 - 提交;-1 - 带外(副本不动) int
* 限定条件:调用方 = UI 保存交接路(task-9);落盘经 SYS_EVT_CFG_SAVE
* 整档外科 save(net_cfg_save_request 同款交接,本模块不写盘)
* 限定条件:调用方 = 通讯设置页(ui_data 通道六注入转 uiTask 上下文);
* 成功置落盘脏——写盘 = defaultTask 每轮无条件
* iec104_comm_store_save()(run_par B7 同款,无事件位、不写盘于本模块)
*/
int iec104_comm_ca_set(uint16_t ca);
@@ -88,10 +93,28 @@ int iec104_comm_ca_set(uint16_t ca);
* 函数功能:写 port 到 RAM 副本(守卫 = getter 合法带;非法值不提交)
* 入口参数:port - 监听端口 1024..65535 uint16_t
* 返回值:0 - 提交;-1 - 带外(副本不动) int
* 限定条件:同 iec104_comm_ca_set
* 限定条件:同 iec104_comm_ca_set(生效时刻 = 下一次重 bind 监听,改后不必重启)
*/
int iec104_comm_port_set(uint16_t port);
/*
* 函数功能:落盘脏探查(非消费)——页面保存行 ENTER/WAIT/OK 判定源
* 返回值:1 - 有已改未落盘;0 - 干净 uint8_t
* 限定条件:无(任意上下文纯读)
* 函数说明:脏位段级(ca+port 同档共落);页面按「结果锁存」画,与 run_par
* 页同一条纪律(ui_w_ytview 文件头)
*/
uint8_t iec104_comm_dirty_peek(void);
/*
* 函数功能:执行落盘(整档外科 save 成功清脏;失败脏保持下轮重试)
* 返回值:0 - 成功/无脏;-1 - 写盘失败(屏上 ERR 的判定源) int
* 限定条件:**defaultTask(配置引擎属主)上下文**;由 default_task.c 的
* run_par_store_save() 同点位每轮无条件调用(本模块不挂回调、
* 不设事件位——task-8 D5 口径的持久化面)
*/
int iec104_comm_store_save(void);
#ifdef __cplusplus
}
#endif
+4
View File
@@ -41,6 +41,7 @@
#include "run_par.h" /* 运行参数段落盘(卡 10,配置引擎属主 = 本任务) */
#include "prot_set_store.h"
#include "prot_set.h" /* task-10:prot_set_dirty_peek(区分"无待写"与"真落盘") */
#include "iec104_comm.h" /* 道G task-9:[comm] 段落盘(脏位轮询同款,通讯设置页) */
#include "sys_log.h"
#include "sys_events.h"
/* 深调用链模块(本任务 = 存储 bring-up + 配置引擎属主 + syslog 唯一写者,
@@ -244,6 +245,9 @@ void default_task_func(void *argument) {
/* 运行参数段落盘(卡 10):同款脏语义;save = 引擎整档外科写
* ([run_par] 9 键 + 被 adc_cal_set 联动改写的 [adc_cal] 两段一趟落) */
(void)run_par_store_save();
/* [comm] 段落盘(道G task-9 通讯设置页):同款脏位、每轮无条件,
* 无事件位(task-8 D5 口径——ca 改后重启生效,落盘只保断电不丢) */
(void)iec104_comm_store_save();
#endif
/* 配置文件变更轮询(2s 周期;沉降去抖在引擎内) */
if ((cfg_poll_cnt++ % 40u) == 0u) {
+69
View File
@@ -54,6 +54,40 @@ static uint8_t s_liveness_ctr;
static uint8_t s_send_fail_run;
static uint8_t s_conn_dead; /* 发送连败 → 本轮 link 调用返回后拆链 */
/* ---------------- 只读观测计数(道G task-12;上电以来,跨重连**不清零**)-----------
* 与上面三枚的分工要说清:s_liveness_ctr / s_send_fail_run / s_conn_dead 是**控制量**
* ——它们为"要不要拆链"服务,会被复位、也是 8 位,拿它们当统计数字必错(拆一次链
* 就把连败清零)。下面四枚只做一件事:被 iec104_serv_get_stats() 读出去显示。
* 为什么留在本文件而不是并进 App/util/boot_stats:104 的账本归 104(分层),
* boot_stats 只管开机账;两边各一张表比一张混表好查。
* 为什么**不放进 iec104_link_t**:实例是 serv.c:48 的文件级 static,feed 拿不到指针;
* 更要紧的是 link_open/link_close 的语义会把"上电以来"洗成"本次连接"。
* 写者 = iec104Task(本文件),读者 = uiTask/feed;u32 单字读写在 Cortex-M4 上原子,
* 跨字段不保证同一瞬间——四个数之间没有"合起来才成立"的判据,混窗对显示无害。
* 到顶**饱和**不回绕:停在 42 亿一眼能看出到头,回绕成个位数会被读成"刚上电"。 */
static uint32_t s_st_conn_up; /* 主站接入次数(accept 成功且已 open link) */
static uint32_t s_st_apdu_tx; /* 成功发出的 APDU 帧数(短写不计,那算 tx_fail) */
static uint32_t s_st_tx_fail; /* 发送短写/失败次数(与连败复位无关,只累计) */
static uint32_t s_st_rx_bytes; /* 收到的字节数(帧数在 link 层重组,serv 只看得见字节) */
/*
* 函数功能:单枚观测计数 +1,到顶饱和(四枚同式,收成一处,别在四个调用点各写一遍)
* 入口参数:p - 计数变量地址 uint32_t *
* 返 回 值:无 void
* 限定条件:iec104Task 上下文(唯一写者);NULL 时无害返回
* 函数说明:**只加不清**——观测计数一旦有清零路径,"上电以来"就退化成"上次谁清零
* 以来"。p 为 NULL 直接返回(与三个对外 getter 的 NULL 契约一致)。
*/
static void s_cnt_add(uint32_t *p)
{
if (p == 0) {
return;
}
if (*p < 0xFFFFFFF0u) {
*p = *p + 1u;
}
}
/* 前向声明(cfg 初始化引用) */
static void s_send_apdu(void *ctx, const uint8_t *apdu, uint16_t len);
static int s_clock_sync(void *ctx, uint64_t epoch_ms);
@@ -82,8 +116,10 @@ static void s_send_apdu(void *ctx, const uint8_t *apdu, uint16_t len) {
ret = net_send(s_client, apdu, (int)len, 0);
if (ret == (int)len) {
s_send_fail_run = 0u;
s_cnt_add(&s_st_apdu_tx); /* 只数真发出去的(道G task-12 观测计数) */
return;
}
s_cnt_add(&s_st_tx_fail); /* 短写/失败也进观测账,与下面的连败判定各管各的 */
if (s_send_fail_run < 255u) {
s_send_fail_run++;
}
@@ -222,6 +258,30 @@ static void s_service_link(uint32_t now) {
}
}
/*
* 函数功能:读 104 观测计数(契约见 iec104_serv.h 的 iec104_serv_get_stats)
* 入口参数:p_out - 输出缓冲;NULL 时无害返回 iec104_serv_stats_t *
* 返 回 值:无 void
* 限定条件:任意上下文;不取锁、不读时刻、不碰 socket、不阻塞
* 函数说明:先读进局部再整体赋出——四枚都读完后才发布,读者拿到的是"同一轮读取"的
* 四元组(中途若有新计数产生,那一枚可能偏新一格,但不会读到半改写的字)。
* 本函数**不清零、不复位任何计数**:统计口一旦能清零,"上电以来"就退化成
* "上次谁读过以来",那条账再没人敢信。
*/
void iec104_serv_get_stats(iec104_serv_stats_t *p_out)
{
iec104_serv_stats_t s;
if (p_out == NULL) {
return;
}
s.conn_up = s_st_conn_up;
s.apdu_tx = s_st_apdu_tx;
s.tx_fail = s_st_tx_fail;
s.rx_bytes = s_st_rx_bytes;
*p_out = s;
}
/*
* 函数功能:单次服务循环(代次自愈 → accept → 门控收 → 喂 link → 节拍)
* 返回值:无
@@ -280,6 +340,7 @@ static void s_serv_poll(void) {
s_conn_dead = 0u;
s_liveness_ctr = 0u;
iec104_link_open(&s_link, now);
s_cnt_add(&s_st_conn_up); /* 主站接入次数(上电以来;道G task-12) */
DBG_INFO("i104: accepted fd=%d", c);
}
return;
@@ -300,6 +361,14 @@ static void s_serv_poll(void) {
* 阻塞 recv(文档契约见 net_socket.h net_recv 注释),非阻塞轮询是义务 */
n = net_recv_nb(s_client, s_rx, (int)sizeof(s_rx));
if (n > 0) {
/* 收字节观测计数(道G task-12):这里数的是 recv 段返回的**字节数**,不是
* APDU 帧数——帧边界在 link 层重组,serv 这一层看不见;页面文案别写"接收帧数"。
* 加法也做饱和:n 最大 128,先留出余量再比,避免"加完才判"溢出成小数字。 */
if (s_st_rx_bytes < (0xFFFFFFF0u - (uint32_t)n)) {
s_st_rx_bytes = s_st_rx_bytes + (uint32_t)n;
} else {
s_st_rx_bytes = 0xFFFFFFF0u;
}
iec104_link_on_rx(&s_link, s_rx, (uint16_t)n, now);
} else if (n == 0) {
/* 对端关闭:net 层已 auto_relisten;client==listen 时严禁 close */
+37 -5
View File
@@ -7,11 +7,11 @@
#ifndef IEC104_SERV_H
#define IEC104_SERV_H
/* IEC 60870-5-104 被控站监听端口(DL/T 634.5104 标准端口)。
* 道G task-8 起运行期真源 = [comm] 段(App/protocol/iec104/iec104_comm.h,
* 工厂默认 IEC104_COMM_DEF_PORT 与本宏同值);本宏保留仅作快照兼容出口
* (App/app/ui_data_feed.c 的 port_104——该处切 getter 属 UI 道 task-9)。 */
#define IEC104_SERV_PORT 2404u
#include <stdint.h> /* uint32_t(观测计数声明):头文件自含——2026-10-27 @build 实测
* 本头此前全靠包含者先带 stdint,feed 换 include 即断链报 #20 */
/* 监听端口的宏出口已删除(道G task-9 收口 task-8 交接痕):运行期真源 =
* [comm] 段 getter(App/protocol/iec104/iec104_comm.h 的 iec104_comm_port(),
* 工厂默认 IEC104_COMM_DEF_PORT = 2404 = DL/T 634.5104 标准端口)。 */
/*
* 函数功能:iec104Task 任务体(在 Src/freertos.c USER CODE 区 osThreadNew 创建)
@@ -20,4 +20,36 @@
*/
void iec104_serv_task(void *argument);
/*
* 104 只读观测计数(道G task-12):四个数都是**上电以来**、跨重连不清零,语义各自
* 独立、不得互相换算:
* conn_up = 主站接入次数(accept 成功并建立协议层链路)
* apdu_tx = 成功发出的 APDU 帧数(短写算 tx_fail,不算这里)
* tx_fail = 发送短写/失败次数(含拆链前那几次连败)
* rx_bytes = 收到的**字节**数(不是帧数:帧边界在 link 层重组,serv 看不见)
* ⚠ 与 App/task/iec104_serv.c 里 s_liveness_ctr / s_send_fail_run / s_conn_dead 三枚
* **控制量**无关:那三枚会被复位、只有 8 位,为"要不要拆链"服务,拿来当统计必错。
* 到顶饱和在 0xFFFFFFF0,不回绕(回绕成个位数会被读成"刚上电")。
* 页面这一栏要写「本次开机以来」,与运行时长同一类,别让人读成终身值。
*/
typedef struct {
uint32_t conn_up;
uint32_t apdu_tx;
uint32_t tx_fail;
uint32_t rx_bytes;
} iec104_serv_stats_t;
/*
* 函数功能:读 104 观测计数(纯拷字段)
* 入口参数:p_out - 输出缓冲;NULL 时无害返回 iec104_serv_stats_t *
* 返 回 值:无 void
* 限定条件:**任意上下文可读**——不取锁、不读时刻、不碰 socket、不阻塞(照
* adc_cal_get_stats() / boot_stats_get() 的形态:观测口一旦去抢总线或等锁,
* 观测就成了故障的一部分)
* 函数说明:写者 = iec104Task,读者 = uiTask/喂数据侧;u32 单字读写原子,跨字段
* **不保证同一瞬间**——四枚之间没有"合起来才成立"的判据,混窗对显示无害。
* 本函数无任何清零能力(能清零的统计口不是统计口)。
*/
void iec104_serv_get_stats(iec104_serv_stats_t *p_out);
#endif /* IEC104_SERV_H */
+240
View File
@@ -0,0 +1,240 @@
/*
* 模块名称:Boot Stats(开机账本)实现
* 模块功能:见 boot_stats.h——上电次数落 SD2506 RTC 用户 SRAM,本次开机复位原因
* 在开机时从 RCC_CSR 锁存值翻成枚举驻留 RAM。
* 适用平台:STM32F407ZGTx
* 作者:道G log-maint-writer
* 创建日期:2026-10-27
* 修改记录:
* v1.0 2026-10-27 道G 创建(task-12)
*/
#include "boot_stats.h"
#include <string.h>
#include "main.h" /* RCC_CSR_IWDGRSTF 等位掩码(与 CMSIS 同名同值;主机单测走桩 main.h) */
#include "app_main.h" /* g_rcc_csr_latched:Src/main.c 在 HAL_Init() 后采得并清标志 */
#include "sd2506.h" /* sd2506_read_sram / sd2506_write_sram / sd2506_read_ctr1 + BLF 位 */
/* ---- RTC 用户 SRAM 记录布局(8 字节,偏移 0 = 寄存器 2CH)----------------------------
* [0] 0xB5 magic 低
* [1] 0x72 magic 高 ← 两个字节都对上才算本模块写过的记录
* [2..5] 上电次数,小端 u32
* [6] 0x01 记录格式版本 ← 以后改布局必须升它,否则旧记录会被当新格式读出错位
* [7] 校验 = [0]..[6] 逐字节异或
* 为什么带校验而不是只看 magic:电池欠压造成的坏区经常是**成片全 0 或成片全 FF**,
* magic 那关就已经过不去;但"漂了一位数据"更常见的是个别字节翻转,那时 magic 可能
* 仍然好、次数却错了 256 倍。这一格是给人看"装置上了多少次电"的账,宁可报
* "不可信/重开",也不要报一个看起来很像样的错数。
* 为什么放偏移 0:SD2506 有 70 B 用户 SRAM(2CH~71H),本模块只用前 8 B,后面
* 留着——日后要放"上次结束原因"之类的另起一段,别在同一段里挤着改。 */
#define BS_SRAM_OFS 0u
#define BS_REC_LEN 8u
#define BS_REC_MAGIC_LO 0xB5u
#define BS_REC_MAGIC_HI 0x72u
#define BS_REC_VER 1u
/* 计数饱和上限:到顶就不再自增也不再写回(宁可停在上限,也不许回绕成 1) */
#define BS_CNT_MAX 0xFFFFFFF0u
static boot_stats_t s_bs; /* .bss 零初值 = ready 0,天然表达"还没 init" */
/*
* 函数功能:把 RCC_CSR 锁存值翻成复位原因编码
* 入口参数:csr - RCC_CSR 锁存值 uint32_t
* 返 回 值:BOOT_STATS_CAUSE_* 之一 uint8_t
* 限定条件:任意上下文;纯位判定,不读外设、不写状态
* 函数说明:本函数是这条优先级链的**唯一一份**。App/app/app_main.c 的开机打印过去
* 自带一条同样顺序的 if-else 链,现在改成经本函数取编码再取串——两处各写
* 一条链,早晚有一天串口说"IWDG"而页面上写"software"。
*/
uint8_t boot_stats_cause_from_csr(uint32_t csr)
{
if ((csr & RCC_CSR_IWDGRSTF) != 0u) {
return BOOT_STATS_CAUSE_IWDG;
}
if ((csr & RCC_CSR_WWDGRSTF) != 0u) {
return BOOT_STATS_CAUSE_WWDG;
}
if ((csr & RCC_CSR_SFTRSTF) != 0u) {
return BOOT_STATS_CAUSE_SFT;
}
if ((csr & RCC_CSR_PORRSTF) != 0u) {
return BOOT_STATS_CAUSE_POR;
}
if ((csr & RCC_CSR_PINRSTF) != 0u) {
return BOOT_STATS_CAUSE_PIN;
}
if ((csr & RCC_CSR_BORRSTF) != 0u) {
return BOOT_STATS_CAUSE_BOR;
}
return BOOT_STATS_CAUSE_UNKNOWN;
}
/*
* 函数功能:取复位原因可读串
* 入口参数:cause - BOOT_STATS_CAUSE_* uint8_t
* 返 回 值:静态常量串(永不 NULL) const char *
* 限定条件:任意上下文;返回指针指向 .const,调用方不得写
* 函数说明:串内容与 App/app/app_main.c 过去打印的一致(纯 ASCII)。编码不认识时
* 回落 "unknown" 而不是断言——这条链跑在开机极早期,不能因为一个编码把
* 启动挡住。
*/
const char *boot_stats_cause_str(uint8_t cause)
{
switch (cause) {
case BOOT_STATS_CAUSE_IWDG:
return "IWDG (independent watchdog)";
case BOOT_STATS_CAUSE_WWDG:
return "WWDG (window watchdog)";
case BOOT_STATS_CAUSE_SFT:
return "software (NVIC_SystemReset)";
case BOOT_STATS_CAUSE_POR:
return "power-on (POR)";
case BOOT_STATS_CAUSE_PIN:
return "external pin (NRST)";
case BOOT_STATS_CAUSE_BOR:
return "brown-out (BOR)";
default:
return "unknown";
}
}
/*
* 函数功能:算记录校验([0] 到倒数第二字节逐字节异或)
* 入口参数:p_rec - 记录缓冲(长度恒为 BS_REC_LEN,不作出参长度参数) const uint8_t *
* 返 回 值:校验字节 uint8_t
* 限定条件:调用方保证 p_rec 非空且长度 BS_REC_LEN(本文件内私有,两处调用点都传数组)
* 函数说明:校验与写回**共用这同一个函数**——两边各写一份异或,改布局时必有一份忘改,
* 症状是"记录永远读不出来"(写进去的校验和读的时候算的不一样)。
*/
static uint8_t s_rec_sum(const uint8_t *p_rec)
{
uint8_t sum = 0u;
uint8_t i;
for (i = 0u; i < BS_REC_LEN - 1u; i++) {
sum = (uint8_t)(sum ^ p_rec[i]);
}
return sum;
}
/*
* 函数功能:判记录可信(magic 两字节 + 格式版本 + 校验,三者全中才算)
* 入口参数:p_rec - 记录缓冲 const uint8_t *
* 返 回 值:1 - 可信;0 - 不可信(按"无记录"另走首次/掉电两条分支) uint8_t
* 限定条件:调用方保证 p_rec 已填入 BS_REC_LEN 字节
* 函数说明:**只信 magic 不够**:电池坏区常见整片全 0/全 FF(magic 自然过不去),
* 但"个别字节翻转"时 magic 可能仍然好,次数会错 256 倍——那正是校验和
* 要挡的情形(机器判据 TC-BOOTSTAT-007)。
*/
static uint8_t s_rec_valid(const uint8_t *p_rec)
{
if ((p_rec[0] != BS_REC_MAGIC_LO) || (p_rec[1] != BS_REC_MAGIC_HI)) {
return 0u;
}
if (p_rec[6] != BS_REC_VER) {
return 0u;
}
return (p_rec[7] == s_rec_sum(p_rec)) ? 1u : 0u;
}
/*
* 函数功能:开机快照 + 上电次数自增(幂等)
* 入口参数:无 void
* 返 回 值:无
* 限定条件:SD2506 已 init 之后;唯一调用点在 App/app/app_main.c 的启动段(内核启动
* 前,单线程非 ISR)。本函数含 1 次 I2C 读 + 至多 1 次 I2C 写,I2C1 与 RTC
* 同总线,严禁搬进 iec104/adc 关键链或 ISR
* 函数说明:状态全留在 s_bs 里,一条日志都不打(可观测性事件不进 [ERR],理由见
* boot_stats.h 与 .c 里 s_bs 那几枚字段的注释)。
* 写失败的后果要算清楚:本次只在 RAM 里数上了,下次开机读回的还是旧值,
* 账本**少 1**,不会重复计——所以 write_failed 是"少账"的记号,不是故障。
*/
void boot_stats_init(void)
{
uint8_t rec[BS_REC_LEN];
uint8_t ctr1 = 0u;
uint32_t cnt;
bsp_err_t err;
if (s_bs.ready != 0u) {
return; /* 幂等:bring-up 重试不重复计数 */
}
/* ① 复位原因快照——**必须先做**:RCC_CSR 的标志在 Src/main.c 里读后即清,
* 本函数拿到的一直是那份锁存影子,与串口那条 "reset cause:" 同源同序。 */
s_bs.rst_cause = boot_stats_cause_from_csr(g_rcc_csr_latched);
s_bs.ready = 1u;
/* ② 读 RTC 用户 SRAM。I2C 失败 ≠ 没有记录:把这一格标成"不可信"就停手,
* **绝不写回**——在只读到一半脏数据的情况下去写,等于把可能还完好的记录
* 覆盖掉(芯片刚上电、总线还没稳的时候最容易走这条路)。 */
err = sd2506_read_sram(BS_SRAM_OFS, rec, BS_REC_LEN);
if (err != BSP_OK) {
s_bs.seq_state = BOOT_STATS_SEQ_UNKNOWN;
s_bs.boot_count = 0u;
return;
}
/* ③ BLF(电池欠压标志):只用来把"记录无效"分成「首次启用」与「电池域掉过电」
* 两件事;读失败按 0 处理,且不参与有效性判定(读不到 BLF 不该把好记录判坏)。
* 该位是滞后的:置位后不清就一直在那儿,所以只在记录无效时才看它——记录还有效
* 就说明电池域没把 SRAM 洗掉,滞后无害。 */
(void)sd2506_read_ctr1(&ctr1);
s_bs.rtc_blf = ((ctr1 & SD2506_CTR1_BLF) != 0u) ? 1u : 0u;
if (s_rec_valid(rec) != 0u) {
cnt = ((uint32_t)rec[2]) | ((uint32_t)rec[3] << 8) |
((uint32_t)rec[4] << 16) | ((uint32_t)rec[5] << 24);
s_bs.seq_state = BOOT_STATS_SEQ_CONTINUED;
} else {
/* 记录无效:BLF 置位 → 电池周期换了,计数是从重开的;BLF 未置位 → 本模块
* 从来没写过这块 SRAM(第一次上电/首次刷入带本功能的固件)。两者**不许
* 合并**,合并就等于把"第一次上电"说成"RTC 掉过电"。 */
cnt = 0u;
s_bs.seq_state = (s_bs.rtc_blf != 0u) ? BOOT_STATS_SEQ_RESTARTED
: BOOT_STATS_SEQ_FRESH;
}
s_bs.write_failed = 0u;
if (cnt >= BS_CNT_MAX) {
/* 饱和:停在上限,不再写回。回绕成小数字比"停在 42 亿"恶劣得多——前者会让
* 人以为装置刚上电几次,后者一眼就能看出到头了。 */
s_bs.boot_count = cnt;
return;
}
cnt = cnt + 1u;
s_bs.boot_count = cnt;
rec[0] = BS_REC_MAGIC_LO;
rec[1] = BS_REC_MAGIC_HI;
rec[2] = (uint8_t)(cnt & 0xFFu);
rec[3] = (uint8_t)((cnt >> 8) & 0xFFu);
rec[4] = (uint8_t)((cnt >> 16) & 0xFFu);
rec[5] = (uint8_t)((cnt >> 24) & 0xFFu);
rec[6] = BS_REC_VER;
rec[7] = s_rec_sum(rec);
err = sd2506_write_sram(BS_SRAM_OFS, rec, BS_REC_LEN);
if (err != BSP_OK) {
s_bs.write_failed = 1u; /* 少 1 笔账,不是故障:见函数头 */
}
}
/*
* 函数功能:读开机账本快照
* 入口参数:p_out - 输出缓冲;NULL 时无害返回 boot_stats_t *
* 返 回 值:无 void
* 限定条件:任意上下文可读——不取锁、不读时刻、不碰 I2C、不阻塞
* 函数说明:整体一次拷(memcpy 结构体),跨字段不保证同一瞬间;表内没有任何"两字段
* 合起来才成立"的判据,混窗对显示无害(详见头文件)。
*/
void boot_stats_get(boot_stats_t *p_out)
{
if (p_out == NULL) {
return;
}
(void)memcpy(p_out, &s_bs, sizeof(boot_stats_t));
}
+133
View File
@@ -0,0 +1,133 @@
/*
* 模块名称:Boot Stats(开机账本:上电次数 + 本次开机复位原因)
* 模块功能:给"运行报告"这一页提供两个**没有现成正源**的量——① 上电次数(掉电保持,
* 落 SD2506 RTC 用户 SRAM,零 NAND 磨损、零卷锁、零配置段);② 本次开机的
* 复位原因(RCC_CSR 标志在 Src/main.c 里读后即清,故本模块在开机时把
* 锁存值翻成枚举**驻留 RAM**,否则第二次读就什么都没有了)。
* 适用平台:STM32F407ZGTx(仅 MCU 端:依赖 SD2506 I2C 与 RCC_CSR;主机单测用桩 SD2506
* 与桩 main.h,见 test/host/boot_stats/;模拟器不编译本模块)
* 作者:道G log-maint-writer
* 创建日期:2026-10-27
* 修改记录:
* v1.0 2026-10-27 道G 创建(task-12「运行报告计数器」的数据面;UI 页面归 task-13)
*
* ┌── 四个数的分类台账(页面文案必须逐项挂类别,"不得含糊"是 UI 补齐清单的原文判据)──┐
* │ 上电次数 = **掉电保持**:本模块,落 RTC 用户 SRAM;口径见下面 BOOT_STATS_SEQ_* 一段 │
* │ 运行时长 = **本次开机以来**:现成值 `ui_data_info_t.uptime_s`(写侧 │
* │ App/app/ui_data_feed.c),**本模块不重复提供**——重复就是第二真源。 │
* │ 用户裁定 2026-10-27:累计时长不做(装置无正常关机流程,任何"累计"都只能 │
* │ 周期写近似值)。⚠ 连续开机 49.7 天后 tick 回绕归零,所以那一栏必须写 │
* │ 「本次开机以来」,不能让人读成累计值。 │
* │ 复位原因 = **本次开机**:本模块 `rst_cause`(跨启动的"上一次结束原因"本卡**不做**, │
* │ 它需要额外一次落盘写点,且口径未裁,别在页面上口头承诺) │
* │ 104 统计 = **本次开机以来**(跨重连不清零):计数在 App/task/iec104_serv.c, │
* │ getter 在 App/task/iec104_serv.h——账本归 104,不并进本模块(分层) │
* └──────────────────────────────────────────────────────────────────────────────┘
*
* ⚠ 「上电次数」这一格**不许写成终身次数**:保持介质是 RTC 芯片的电池域 SRAM,
* 换电池 / 电池欠压都会把它清掉。可读出来的只有"自 RTC 电池周期起"这一件事,
* 且必须能把「本计数器首次启用」和「电池域掉过电」分开——见 `seq_state`:
* UNKNOWN = SRAM 读失败(I2C 超时/芯片故障):既不知道计数,也不知道是否延续;
* FRESH = 记录无效但 RTC 没报欠压(BLF=0)= **本固件第一次写它**;
* CONTINUED = 记录有效 = 接着上一轮数;
* RESTARTED = 记录无效且 BLF=1 = 电池域掉过电,计数是从重开的。
* 把 FRESH 与 RESTARTED 混成一件事 = 拿"第一次上电"冒充"RTC 掉电"(或反过来);
* 把 UNKNOWN 当成 FRESH = I2C 故障那天,屏幕上的次数悄悄归 1。
*/
#ifndef BOOT_STATS_H
#define BOOT_STATS_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
/* 复位原因编码。判定优先级(IWDG > WWDG > 软件 > POR > PIN > BOR > 未知)的
* **唯一出处**是 boot_stats_cause_from_csr():App/app/app_main.c 的开机打印改成调用
* 它取串,所以这里不再另列一遍顺序,两处也不许各写一条链(两处各写就会漂)。 */
#define BOOT_STATS_CAUSE_UNKNOWN 0u /* 六个标志都没置位(锁存值为 0 等) */
#define BOOT_STATS_CAUSE_IWDG 1u /* 独立看门狗:上一轮有任务卡死,自动恢复事件 */
#define BOOT_STATS_CAUSE_WWDG 2u /* 窗口看门狗 */
#define BOOT_STATS_CAUSE_SFT 3u /* 软件复位(崩溃钩子兜底的 NVIC_SystemReset) */
#define BOOT_STATS_CAUSE_POR 4u /* 上电复位 */
#define BOOT_STATS_CAUSE_PIN 5u /* 外部复位引脚 NRST */
#define BOOT_STATS_CAUSE_BOR 6u /* 掉电(brown-out) */
/* SRAM 记录的可信度状态,见文件头那一段。 */
#define BOOT_STATS_SEQ_UNKNOWN 0u
#define BOOT_STATS_SEQ_FRESH 1u
#define BOOT_STATS_SEQ_CONTINUED 2u
#define BOOT_STATS_SEQ_RESTARTED 3u
typedef struct {
uint32_t boot_count; /* 上电次数;seq_state == UNKNOWN 时**不可信**(此时为 0) */
uint8_t rst_cause; /* BOOT_STATS_CAUSE_*:本次开机 */
uint8_t seq_state; /* BOOT_STATS_SEQ_*:上电次数这一格的可信度 */
uint8_t write_failed; /* 1 = 本次自增没能写回 SRAM:这次只在 RAM 里数上了,
* 下次开机读到的还是旧值 ⇒ 账本少 1(只会少不会重) */
uint8_t rtc_blf; /* RTC 电池欠压标志原值(0/1);读不到时按 0,不参与判定 */
uint8_t ready; /* 0 = boot_stats_init() 还没跑:整表都是初值,别当数据用 */
} boot_stats_t;
/*
* 函数功能:把 RCC_CSR 锁存值翻成复位原因编码(优先级链的唯一出处)
* 入口参数:csr - RCC_CSR 的锁存值(Src/main.c 在 HAL_Init() 后采并清标志)uint32_t
* 返 回 值:BOOT_STATS_CAUSE_* 之一 uint8_t
* 限定条件:任意上下文(纯位判定,无副作用、不读外设、不取锁)
* 函数说明:IWDG/WWDG 排在软件复位之前是刻意的:看门狗复位才是我们要看的自动恢复
* 事件,而崩溃钩子常以 NVIC_SystemReset 收尾——两类标志同时置位时,报
* 看门狗更有价值(理由与 App/app/app_main.c 原注释一致,此处只搬家不改编)。
*/
uint8_t boot_stats_cause_from_csr(uint32_t csr);
/*
* 函数功能:取复位原因的可读串(纯 ASCII,供开机日志与页面共用)
* 入口参数:cause - BOOT_STATS_CAUSE_* 之一 uint8_t
* 返 回 值:静态常量串指针(永不 NULL;未知编码回落 "unknown") const char *
* 限定条件:任意上下文(只读 .const,不写缓冲、不需要调用方提供空间)
*/
const char *boot_stats_cause_str(uint8_t cause);
/*
* 函数功能:开机快照 + 上电次数自增(一次上电调一次,幂等)
* 入口参数:无 void
* 返 回 值:无
* 限定条件:**唯一调用点 = App/app/app_main.c 紧接 sd2506_init 之后**(内核启动前:
* 单线程、非 ISR、调度器未跑;本函数不调任何创建类内核 API)。之所以不放在
* defaultTask 的存储 bring-up:保持介质是 RTC 用户 SRAM 而不是文件系统,
* 不必等 STORAGE_READY,也就把 I2C 彻底挪出了喂狗点与卷锁之外。
* 本函数做 1 次 I2C 读 + 至多 1 次 I2C 写:I2C1 与 RTC 同总线,严禁再把它
* 搬进 iec104/adc 任何关键链或 ISR。重复调用直接返回,不重复计数。
* 函数说明:① 全程**不打日志**:I2C 失败/电池换过都是可观测性事件,不是装置故障;
* `[ERR]` 在本仓是"设备出故障"的唯一信号通道(@test 的 Forbidden 依赖它),
* 把可观测性事件塞进去就变成第二个信号源——与"日志让路不计 [ERR]"、
* 卷锁 fail-open 只投自检记录是同一条口径。状态一律留在结构体里。
* ② 落盘只写 8 字节到 RTC 用户 SRAM:无 FTL 写放大、不取卷锁、不进 /log;
* 一次上电一笔 I2C 写,失败**不做周期补偿**(否则"零写放大"方案会被做成
* 周期 I2C 轮询),后果就是 write_failed 那一条。
*/
void boot_stats_init(void);
/*
* 函数功能:读开机账本快照(纯拷字段)
* 入口参数:p_out - 输出缓冲;NULL 时无害返回 boot_stats_t *
* 返 回 值:无 void
* 限定条件:**任意上下文可读**——不取锁、不读时刻、不碰 I2C、不阻塞(照
* adc_cal_get_stats() / vol_lock_get_stats() 的形态:观测口一旦要去抢
* 总线或等锁,观测就成了故障的一部分)
* 函数说明:① 各字段都是 32 位对齐量或单字节,Cortex-M4 上单字/单字节读写原子;
* 跨字段**不保证同一瞬间**——本表里没有任何"两个字段合起来才成立"的判据,
* 所以混窗对显示无害。seq_state 与 boot_count 要一起看时,按"是否可信"
* 读,不要拿差值当账。
* ② ready == 0 表示 boot_stats_init() 还没跑(开机极早期或 SD2506 未就绪),
* 此时整表为 0,页面应显示占位而不是 0 次。
*/
void boot_stats_get(boot_stats_t *p_out);
#ifdef __cplusplus
}
#endif
#endif /* BOOT_STATS_H */