From 728d60675f29bab0af51f6cb500f2ebcf20323cc Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Tue, 27 Oct 2026 11:29:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(UI/=E9=87=87=E9=9B=86/104):=20=E9=80=9A?= =?UTF-8?q?=E8=AE=AF=E8=AE=BE=E7=BD=AE=E9=A1=B5=20+=20=E5=BC=80=E6=9C=BA?= =?UTF-8?q?=E8=B4=A6=E6=9C=AC=20+=20adc=5Fcal=20=E5=8F=AF=E8=AF=8A?= =?UTF-8?q?=E6=96=AD=E5=8C=96=E2=80=94=E2=80=94=E4=B8=89=E8=BD=A6=E9=81=93?= =?UTF-8?q?=E5=90=8C=E6=A0=91=E4=B8=80=E6=AC=A1=E6=9E=84=E5=BB=BA=E5=85=A5?= =?UTF-8?q?=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 三条道在同一棵树上交付,故合并成**一次权威构建**(@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,交台架道补。 --- App/algo/adc_cal.c | 39 +++ App/algo/adc_cal.h | 68 ++++- App/algo/run_par.c | 4 + App/algo/run_par.h | 23 +- App/app/app_main.c | 34 +-- App/app/ui_data_feed.c | 77 ++++- App/protocol/iec104/iec104_comm.c | 52 +++- App/protocol/iec104/iec104_comm.h | 37 ++- App/task/default_task.c | 4 + App/task/iec104_serv.c | 69 +++++ App/task/iec104_serv.h | 42 ++- App/util/boot_stats.c | 240 +++++++++++++++ App/util/boot_stats.h | 133 ++++++++ Drivers/BSP/UI/ui_data.c | 61 ++++ Drivers/BSP/UI/ui_data.h | 76 +++++ Drivers/BSP/UI/ui_font_cn_12.c | 140 +++++---- Drivers/BSP/UI/ui_page_comm.c | 278 +++++++++++++++++ Drivers/BSP/UI/ui_page_comm.h | 25 ++ Drivers/BSP/UI/ui_page_registry.c | 2 + Drivers/BSP/UI/ui_page_registry.h | 2 +- MDK-ARM/STM32F407-Demo.uvprojx | 10 + docs/README.md | 1 + docs/内存管理/RAM使用分析与优化方案.md | 20 +- docs/内存管理/RAM预算台账.md | 10 +- docs/子系统设计/IEC104上行设计说明.md | 24 ++ docs/测试说明/开机账本单元测试说明.md | 128 ++++++++ docs/算法/采集与算法层设计说明.md | 34 +++ scripts/check_registration.py | 1 + scripts/id_registry.json | 30 +- scripts/ram_baseline.tsv | 2 +- simulator/CMakeLists.txt | 1 + simulator/main.c | 91 +++++- simulator/tests/check_content_pages.py | 54 ++++ simulator/tests/ref/shot_comm.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_comm_edit.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_comm_ok.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_comm_reject.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_comm_ro.bin | Bin 0 -> 3200 bytes simulator/tests/ref/shot_comm_wait.bin | Bin 0 -> 3200 bytes test/host/adc_cal/adc_cal_test.c | 70 +++++ test/host/boot_stats/boot_stats_test.c | 374 +++++++++++++++++++++++ test/host/boot_stats/run_unit_test.py | 69 +++++ test/host/boot_stats/stub/main.h | 34 +++ test/host/iec104/iec104_test.c | 22 ++ test/host/ui_data/ui_data_test.c | 102 +++++++ ui_cn_chars.txt | 2 +- ui_menu_tree.txt | 2 +- 47 files changed, 2364 insertions(+), 123 deletions(-) create mode 100644 App/util/boot_stats.c create mode 100644 App/util/boot_stats.h create mode 100644 Drivers/BSP/UI/ui_page_comm.c create mode 100644 Drivers/BSP/UI/ui_page_comm.h create mode 100644 docs/测试说明/开机账本单元测试说明.md create mode 100644 simulator/tests/ref/shot_comm.bin create mode 100644 simulator/tests/ref/shot_comm_edit.bin create mode 100644 simulator/tests/ref/shot_comm_ok.bin create mode 100644 simulator/tests/ref/shot_comm_reject.bin create mode 100644 simulator/tests/ref/shot_comm_ro.bin create mode 100644 simulator/tests/ref/shot_comm_wait.bin create mode 100644 test/host/boot_stats/boot_stats_test.c create mode 100644 test/host/boot_stats/run_unit_test.py create mode 100644 test/host/boot_stats/stub/main.h diff --git a/App/algo/adc_cal.c b/App/algo/adc_cal.c index dc42dc67..7ef135c6 100644 --- a/App/algo/adc_cal.c +++ b/App/algo/adc_cal.c @@ -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 同名声明 diff --git a/App/algo/adc_cal.h b/App/algo/adc_cal.h index 6aaa4e8c..f61549b7 100644 --- a/App/algo/adc_cal.h +++ b/App/algo/adc_cal.h @@ -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 diff --git a/App/algo/run_par.c b/App/algo/run_par.c index 3f4e26ab..67b2a55e 100644 --- a/App/algo/run_par.c +++ b/App/algo/run_par.c @@ -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; } diff --git a/App/algo/run_par.h b/App/algo/run_par.h index 5e539617..b797b2a9 100644 --- a/App/algo/run_par.h +++ b/App/algo/run_par.h @@ -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 常量段)。 * 成功即置脏(落盘请求,见文件头持久化契约)。 diff --git a/App/app/app_main.c b/App/app/app_main.c index bcb4f2e1..592b1752 100644 --- a/App/app/app_main.c +++ b/App/app/app_main.c @@ -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"); diff --git a/App/app/ui_data_feed.c b/App/app/ui_data_feed.c index cbd5f9b7..c7387470 100644 --- a/App/app/ui_data_feed.c +++ b/App/app/ui_data_feed.c @@ -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(); diff --git a/App/protocol/iec104/iec104_comm.c b/App/protocol/iec104/iec104_comm.c index a8f9e52e..8502144b 100644 --- a/App/protocol/iec104/iec104_comm.c +++ b/App/protocol/iec104/iec104_comm.c @@ -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(®, 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; } diff --git a/App/protocol/iec104/iec104_comm.h b/App/protocol/iec104/iec104_comm.h index 090764a0..f6cd8084 100644 --- a/App/protocol/iec104/iec104_comm.h +++ b/App/protocol/iec104/iec104_comm.h @@ -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 diff --git a/App/task/default_task.c b/App/task/default_task.c index 301dd9a3..ab379e5c 100644 --- a/App/task/default_task.c +++ b/App/task/default_task.c @@ -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) { diff --git a/App/task/iec104_serv.c b/App/task/iec104_serv.c index 2e9aba98..fed87360 100644 --- a/App/task/iec104_serv.c +++ b/App/task/iec104_serv.c @@ -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 */ diff --git a/App/task/iec104_serv.h b/App/task/iec104_serv.h index 671ed70a..c8a5309b 100644 --- a/App/task/iec104_serv.h +++ b/App/task/iec104_serv.h @@ -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 /* 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 */ diff --git a/App/util/boot_stats.c b/App/util/boot_stats.c new file mode 100644 index 00000000..656c196f --- /dev/null +++ b/App/util/boot_stats.c @@ -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 + +#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)); +} diff --git a/App/util/boot_stats.h b/App/util/boot_stats.h new file mode 100644 index 00000000..cef06a6b --- /dev/null +++ b/App/util/boot_stats.h @@ -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 + +/* 复位原因编码。判定优先级(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 */ diff --git a/Drivers/BSP/UI/ui_data.c b/Drivers/BSP/UI/ui_data.c index 33d2d239..0763602e 100644 --- a/Drivers/BSP/UI/ui_data.c +++ b/Drivers/BSP/UI/ui_data.c @@ -28,6 +28,7 @@ * 作者:王建锋 * 创建日期:2026-10-25 * 修改记录: + * v1.x 2026-10-27 DTU 项目组 道G task-9:通道六通讯参数转发实现。 * v1.0 2026-10-25 王建锋 创建(道E task-14) * v1.1 2026-10-26 DTU 项目组 道F task-1:新增定值写路径环形队列(深度 4、满返 -1) * 与状态机(IDLE/WAIT/OK/ERR_RANGE/ERR_LOCKED/ERR_BUSY), @@ -1026,3 +1027,63 @@ uint8_t ui_data_yt_pending(void) { } return s_yt->pending(); } + +/* ============================================================ + * 通道六:通讯参数([comm])整定注入(道G task-9,通讯设置页) + * + * 与通道五同一组存在理由(ui_data.h 通道六表注:分层 / 模拟器 / 原子性); + * 本层同样**不持有任何 [comm] 值**——真源在 App/protocol/iec104/iec104_comm.c + * 的 RAM 副本,落盘动力在 defaultTask 每轮无条件 iec104_comm_store_save()。 + * 未注入语义按"能力不可用"退化(row/set → -1、pending → 0),页面不崩不猜。 + * ============================================================ */ +static const ui_data_comm_ops_t *s_comm; + +/* + * 函数功能:注入通讯参数执行侧函数表(启动期一次) + * 入口参数:ops - 三件齐;NULL = 撤销注入 + * 返回值:0 - 已注入/已撤销;-1 - 残表(存量不动,理由逐字同 yt/maint bind) int + * 限定条件:uiTask 启动期,先于 ui_menu_init() + */ +int ui_data_comm_bind(const ui_data_comm_ops_t *a_ops) { + if (a_ops == NULL) { + s_comm = NULL; + return 0; + } + if ((a_ops->row == NULL) || (a_ops->set == NULL) || + (a_ops->pending == NULL)) { + return -1; + } + s_comm = a_ops; + return 0; +} + +/* + * 函数功能:读第 idx 行通讯参数现状 + * 返回值:0 - 已整份写出;-1 - 未注入 / 指针空(出参不预清,返 0 才可读—— + * 通道五同一条契约) int + */ +int ui_data_comm_row(uint8_t a_idx, ui_data_comm_row_t *a_out) { + if ((s_comm == NULL) || (a_out == NULL)) { + return -1; + } + return s_comm->row(a_idx, a_out); +} + +/* + * 函数功能:写第 idx 行通讯参数——通讯设置页唯一写出口 + * 返回值:0 - 副本已写且已置脏;-1 - 未注入或带外拒(同码口径同通道五) int + */ +int ui_data_comm_set(uint8_t a_idx, uint32_t a_val) { + if (s_comm == NULL) { + return -1; + } + return s_comm->set(a_idx, a_val); +} + +/* 函数功能:[comm] 段落盘脏位(保存行三态判据;不加边沿记忆,锁存归页面) uint8_t */ +uint8_t ui_data_comm_pending(void) { + if (s_comm == NULL) { + return 0u; + } + return s_comm->pending(); +} diff --git a/Drivers/BSP/UI/ui_data.h b/Drivers/BSP/UI/ui_data.h index b5dddc42..db4cac48 100644 --- a/Drivers/BSP/UI/ui_data.h +++ b/Drivers/BSP/UI/ui_data.h @@ -22,6 +22,8 @@ * 作者:王建锋 * 创建日期:2026-10-25 * 修改记录: + * v1.x 2026-10-27 DTU 项目组 道G task-9:通道六通讯参数注入表 + * (ui_data_comm_ops_t,通讯设置页消费)。 * v1.0 2026-10-25 王建锋 创建(道E task-14:契约 §4 快照层落地;相对契约 * 两通道新增第三条 info,划分理由见本头"通道划分"注 * 与契约 §4.4) @@ -834,6 +836,80 @@ uint8_t ui_data_yt_cal_blocked(void); */ uint8_t ui_data_yt_pending(void); +/* ── 通道六:通讯参数([comm])整定注入(道G task-9,通讯设置页)─────── + * + * 理由与通道五逐字同构(分层方向 layer-deps / 模拟器不链 App/protocol / + * "写值+置脏"原子性归执行侧),此处只记差异: + * ① 行数不进表——[comm] 就两个键(ca/port),行名与行数是**页面形状**, + * 不是数据源;lo/hi 与值由 row() 现给(合法带唯一出口 = iec104_comm.h + * 的带宏与 getter 守卫),页面不得抄数(通道五"第二真源"条同口径)。 + * ② 生效语义与通道五不同档:port 下次重 bind 监听生效、ca **改后重启** + * 生效(页脚与保存行不承诺"立即生效",措辞口径见 ui_page_comm.c 头)。 + * ③ 值域 u16 级,但通道口径走 uint32——编辑器值通道是 int32 定点, + * set() 收到 >65535 由执行侧带外拒(副本不动),页面不做二次守卫。 + */ + +/* 行下标(行序 = 页面行序,装配与页面各自钉死,TC 双向锁) */ +#define UI_DATA_COMM_ROW_CA 0u /* 公共地址 1..65535(改后重启生效) */ +#define UI_DATA_COMM_ROW_PORT 1u /* 监听端口 1024..65535(下次重 bind 生效) */ +#define UI_DATA_COMM_ROW_N 2u + +/* 一行通讯参数的现状(执行侧现给;无浮点,uint32 直达) */ +typedef struct { + uint32_t val; /* 当前值(RAM 副本 getter) */ + uint32_t lo; /* 合法带下限(含)——iec104_comm.h 带宏现给 */ + uint32_t hi; /* 合法带上限(含)——同上 */ +} ui_data_comm_row_t; + +/* 执行侧函数表(常驻注入;整表校验、拒残表——通道四/五同款纪律) */ +typedef struct { + /* + * 函数功能:读第 idx 行现状(值 + 合法带) + * 返回值:0 - 已整份写出;-1 - idx 越界 / 指针空 / 未注入侧无此行 int + */ + int (*row)(uint8_t a_idx, ui_data_comm_row_t *a_out); + /* + * 函数功能:写第 idx 行(真机 = iec104_comm_ca_set / iec104_comm_port_set) + * 返回值:0 - 副本已写且已置脏;-1 - 带外拒(副本未动、不置脏) int + */ + int (*set)(uint8_t a_idx, uint32_t a_val); + /* + * 函数功能:段级落盘脏位探查——真机 = iec104_comm_dirty_peek() + * 返回值:1 - 有已改未落盘;0 - 干净。落盘动力 = defaultTask 每轮无条件 + * iec104_comm_store_save()(通道五注释里查实的那条链同款)。 + * 页面按「结果锁存」画 WAIT/OK(他人 104 侧无自动写者,比通道五 + * 更干净,但锁存纪律不变——同一条口径别做两套) + */ + uint8_t (*pending)(void); +} ui_data_comm_ops_t; + +/* + * 函数功能:注入通讯参数执行侧函数表(启动期一次;形状对齐 ui_data_yt_bind) + * 入口参数:ops - 函数表(三件都不得为 NULL);NULL = 撤销注入(宿主测试用) + * 返回值:0 - 已注入/已撤销;-1 - 残表(存量不动;页面按"不可用"渲染) int + * 限定条件:uiTask 启动期、先于 ui_menu_init()(真机 = 装配层 tick 首轮懒绑, + * 与通道五同点位相邻) + */ +int ui_data_comm_bind(const ui_data_comm_ops_t *a_ops); + +/* + * 函数功能:读第 idx 行通讯参数现状 + * 返回值:0 - 已写出;-1 - 未注入 / 参数空 / idx 越界(退化显示,不猜) int + */ +int ui_data_comm_row(uint8_t a_idx, ui_data_comm_row_t *a_out); + +/* + * 函数功能:写第 idx 行通讯参数 + * 返回值:0 - 已写并置脏;-1 - 未注入或带外拒(未注入与拒绝同码,同通道五) int + */ +int ui_data_comm_set(uint8_t a_idx, uint32_t a_val); + +/* + * 函数功能:[comm] 段落盘脏位探查(保存行三态判据) + * 返回值:1 - 有已改未落盘;0 - 干净或未注入 uint8_t + */ +uint8_t ui_data_comm_pending(void); + #ifdef __cplusplus } #endif diff --git a/Drivers/BSP/UI/ui_font_cn_12.c b/Drivers/BSP/UI/ui_font_cn_12.c index 0fcdf358..24b63387 100644 --- a/Drivers/BSP/UI/ui_font_cn_12.c +++ b/Drivers/BSP/UI/ui_font_cn_12.c @@ -463,6 +463,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x0, 0x3, 0xfe, 0x40, 0x48, 0x9, 0xff, 0x20, 0x20, + /* U+542C "听" */ + 0x0, 0xde, 0xe2, 0x50, 0x4a, 0x9, 0x7f, 0x29, + 0x25, 0x24, 0xa4, 0xf4, 0x92, 0x90, 0x12, 0x4, + 0x40, + /* U+542F "启" */ 0x4, 0x0, 0x40, 0xff, 0x90, 0x12, 0x2, 0x7f, 0xc8, 0x1, 0xff, 0x30, 0x2a, 0x5, 0x7f, 0xc8, @@ -848,6 +853,11 @@ static LV_ATTRIBUTE_LARGE_CONST const uint8_t glyph_bitmap[] = { 0x6, 0xc7, 0x7, 0x11, 0x2, 0x20, 0x84, 0x20, 0x80, + /* U+76D1 "监" */ + 0x9, 0x9, 0x21, 0x27, 0xa5, 0x4, 0xc8, 0x10, + 0x80, 0x3, 0xfe, 0x4a, 0x49, 0x49, 0x29, 0x7f, + 0xf0, + /* U+76F4 "直" */ 0x8, 0x3f, 0xf0, 0x81, 0xfe, 0x40, 0x9f, 0xe4, 0x9, 0xfe, 0x40, 0x9f, 0xe4, 0xb, 0xff, @@ -1323,20 +1333,20 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 990, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1007, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1024, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1041, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1055, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1041, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1058, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, {.bitmap_index = 1072, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1089, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1106, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1123, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1140, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1156, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1140, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1157, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1173, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1190, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1207, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1224, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1241, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1255, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1241, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1258, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, {.bitmap_index = 1272, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1289, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1306, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, @@ -1345,8 +1355,8 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 1357, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1374, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1391, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1408, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1422, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1408, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1425, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, {.bitmap_index = 1439, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1456, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1473, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, @@ -1366,13 +1376,13 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 1711, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1728, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1745, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1762, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1778, .adv_w = 192, .box_w = 9, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 1791, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1808, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1824, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1841, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 1855, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1762, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1779, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1795, .adv_w = 192, .box_w = 9, .box_h = 11, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 1808, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1825, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1841, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 1858, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1872, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1889, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 1906, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, @@ -1384,8 +1394,8 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 2008, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2025, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2042, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2059, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2075, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2059, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2076, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2092, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2109, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2126, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, @@ -1397,17 +1407,17 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 2228, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2245, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2262, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2279, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2293, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2279, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2296, .adv_w = 192, .box_w = 10, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2310, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2327, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 2342, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2359, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2376, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2392, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2409, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2425, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2442, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2327, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2344, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2361, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 2376, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2393, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2410, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2426, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2443, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2459, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2476, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2493, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, @@ -1417,9 +1427,9 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 2561, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2578, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2595, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2612, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 2626, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 2643, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2612, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2629, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 2646, .adv_w = 192, .box_w = 9, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, {.bitmap_index = 2660, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2677, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 2694, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, @@ -1445,31 +1455,33 @@ static const lv_font_fmt_txt_glyph_dsc_t glyph_dsc[] = { {.bitmap_index = 3034, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3051, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3068, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3085, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3101, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3118, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3085, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3102, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3119, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3135, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3152, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3169, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3186, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3203, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3220, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3237, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 3252, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, - {.bitmap_index = 3267, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3284, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3237, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3254, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3271, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, + {.bitmap_index = 3286, .adv_w = 192, .box_w = 10, .box_h = 12, .ofs_x = 1, .ofs_y = -2}, {.bitmap_index = 3301, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3318, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3335, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3352, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3368, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3385, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3352, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3369, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3386, .adv_w = 192, .box_w = 11, .box_h = 11, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3402, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3419, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, {.bitmap_index = 3436, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, - {.bitmap_index = 3453, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 3454, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0}, - {.bitmap_index = 3456, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 */ + {.bitmap_index = 3453, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3470, .adv_w = 192, .box_w = 11, .box_h = 12, .ofs_x = 0, .ofs_y = -2}, + {.bitmap_index = 3487, .adv_w = 192, .box_w = 2, .box_h = 4, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 3488, .adv_w = 192, .box_w = 2, .box_h = 6, .ofs_x = 2, .ofs_y = 0}, + {.bitmap_index = 3490, .adv_w = 165, .box_w = 8, .box_h = 10, .ofs_x = 1, .ofs_y = -1} /* U+25B6 */ }; /*--------------------- @@ -1481,25 +1493,25 @@ static const uint16_t unicode_list_1[] = { 0x2d44, 0x2d96, 0x2ded, 0x2dee, 0x2dfc, 0x2e7d, 0x2e81, 0x2edc, 0x3005, 0x300c, 0x3011, 0x3013, 0x309a, 0x30a6, 0x30d6, 0x30ed, 0x3148, 0x31b6, 0x31da, 0x322b, 0x3262, 0x326c, 0x3276, 0x3283, - 0x3297, 0x32a8, 0x32cf, 0x32ea, 0x357e, 0x35d0, 0x35e0, 0x362b, - 0x37ad, 0x37b6, 0x39f0, 0x39f7, 0x39f8, 0x3a3a, 0x3a3e, 0x3a66, - 0x3c92, 0x3cd8, 0x3d14, 0x3d2f, 0x3d46, 0x3d96, 0x3da2, 0x3daf, - 0x3dda, 0x3df2, 0x3df3, 0x3df5, 0x3ea1, 0x3edb, 0x3f02, 0x3f0f, - 0x40d7, 0x40eb, 0x4135, 0x4136, 0x4144, 0x4145, 0x420e, 0x4245, - 0x4247, 0x4249, 0x436d, 0x43e5, 0x43e8, 0x4410, 0x4414, 0x4480, - 0x4485, 0x4496, 0x44de, 0x45a0, 0x45a8, 0x45bf, 0x45ca, 0x45cc, - 0x461f, 0x4685, 0x46a7, 0x46c1, 0x46d7, 0x4760, 0x48c1, 0x49c0, - 0x49c1, 0x4a1b, 0x4a55, 0x4ae2, 0x4b82, 0x4be1, 0x4beb, 0x4c28, - 0x4ca5, 0x4cc9, 0x4f59, 0x50e7, 0x50e8, 0x5156, 0x53c8, 0x53d5, - 0x53ec, 0x5594, 0x55ab, 0x56a1, 0x570c, 0x570e, 0x57da, 0x5872, - 0x58ab, 0x598f, 0x59cc, 0x5b1b, 0x5b9b, 0x5d5f, 0x5d68, 0x5d94, - 0x5df1, 0x5e0e, 0x608a, 0x614f, 0x6217, 0x66ec, 0x6765, 0x6a06, - 0x6a44, 0x6a4f, 0x6a50, 0x6a5e, 0x6a82, 0x6a8f, 0x6a97, 0x6bbf, - 0x6c2a, 0x6c93, 0x6e0f, 0x6e1d, 0x6e33, 0x6e67, 0x6e70, 0x6e71, - 0x6e74, 0x6ea0, 0x6eba, 0x6ef3, 0x6f05, 0x6f88, 0x6fed, 0x7067, - 0x706d, 0x706f, 0x733f, 0x7345, 0x73a1, 0x73b9, 0x741f, 0x748d, - 0x7494, 0x7498, 0x74d2, 0x7504, 0x753c, 0x7596, 0x7602, 0x7715, - 0x7731, 0x7828, 0x792c, 0x7d78, 0xddac, 0xddba + 0x3297, 0x32a8, 0x32cc, 0x32cf, 0x32ea, 0x357e, 0x35d0, 0x35e0, + 0x362b, 0x37ad, 0x37b6, 0x39f0, 0x39f7, 0x39f8, 0x3a3a, 0x3a3e, + 0x3a66, 0x3c92, 0x3cd8, 0x3d14, 0x3d2f, 0x3d46, 0x3d96, 0x3da2, + 0x3daf, 0x3dda, 0x3df2, 0x3df3, 0x3df5, 0x3ea1, 0x3edb, 0x3f02, + 0x3f0f, 0x40d7, 0x40eb, 0x4135, 0x4136, 0x4144, 0x4145, 0x420e, + 0x4245, 0x4247, 0x4249, 0x436d, 0x43e5, 0x43e8, 0x4410, 0x4414, + 0x4480, 0x4485, 0x4496, 0x44de, 0x45a0, 0x45a8, 0x45bf, 0x45ca, + 0x45cc, 0x461f, 0x4685, 0x46a7, 0x46c1, 0x46d7, 0x4760, 0x48c1, + 0x49c0, 0x49c1, 0x4a1b, 0x4a55, 0x4ae2, 0x4b82, 0x4be1, 0x4beb, + 0x4c28, 0x4ca5, 0x4cc9, 0x4f59, 0x50e7, 0x50e8, 0x5156, 0x53c8, + 0x53d5, 0x53ec, 0x5571, 0x5594, 0x55ab, 0x56a1, 0x570c, 0x570e, + 0x57da, 0x5872, 0x58ab, 0x598f, 0x59cc, 0x5b1b, 0x5b9b, 0x5d5f, + 0x5d68, 0x5d94, 0x5df1, 0x5e0e, 0x608a, 0x614f, 0x6217, 0x66ec, + 0x6765, 0x6a06, 0x6a44, 0x6a4f, 0x6a50, 0x6a5e, 0x6a82, 0x6a8f, + 0x6a97, 0x6bbf, 0x6c2a, 0x6c93, 0x6e0f, 0x6e1d, 0x6e33, 0x6e67, + 0x6e70, 0x6e71, 0x6e74, 0x6ea0, 0x6eba, 0x6ef3, 0x6f05, 0x6f88, + 0x6fed, 0x7067, 0x706d, 0x706f, 0x733f, 0x7345, 0x73a1, 0x73b9, + 0x741f, 0x748d, 0x7494, 0x7498, 0x74d2, 0x7504, 0x753c, 0x7596, + 0x7602, 0x7715, 0x7731, 0x7828, 0x792c, 0x7d78, 0xddac, 0xddba }; /*Collect the unicode lists and glyph_id offsets*/ @@ -1510,12 +1522,12 @@ static const lv_font_fmt_txt_cmap_t cmaps[] = .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY }, { - .range_start = 9654, .range_length = 1, .glyph_id_start = 278, + .range_start = 9654, .range_length = 1, .glyph_id_start = 280, .unicode_list = NULL, .glyph_id_ofs_list = NULL, .list_length = 0, .type = LV_FONT_FMT_TXT_CMAP_FORMAT0_TINY }, { .range_start = 8544, .range_length = 56763, .glyph_id_start = 96, - .unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 182, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY + .unicode_list = unicode_list_1, .glyph_id_ofs_list = NULL, .list_length = 184, .type = LV_FONT_FMT_TXT_CMAP_SPARSE_TINY } }; diff --git a/Drivers/BSP/UI/ui_page_comm.c b/Drivers/BSP/UI/ui_page_comm.c new file mode 100644 index 00000000..78b5cb87 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_comm.c @@ -0,0 +1,278 @@ +/* + * 模块名称:UI Page Comm (通讯设置内容页) + * 模块功能:通讯参数 → 通讯设置 叶子(#P 受密码保护)的内容页——两行数值表 + * (公共地址 CA / 监听端口)+ 保存行;本文件零 App include:行现状 + * (值/合法带)经 ui_data.h 通道六注入表现取,写与落盘确认全走 + * ui_w_setlist 组件 + 本页自带的 60 行胶水(写记账/落盘锁存—— + * ui_w_ytview 是 [run_par] 档专用形态(float 域 + 徽标),两档语义 + * 不同不硬并) + * 两态一页:123 修改权限 → 可编辑(编辑器 + 保存行);000 查看权限 → 只读 + * (attach 内不绑 commit → 无保存行,key_cb 整页交回框架——死区页 + * 同一条分派纪律) + * 生效语义(屏上必须诚实的地方):保存行 OK 只证明 **落盘完成**,不证明 + * "已在新值上运行"——端口 = 下一次重 bind 监听生效(拆链/网络代次 + * 重建自然到位,不必整机重启),公共地址 = **改后重启生效** + * (iec104Task 启动时取一次 getter;docs/子系统设计/ + * IEC104上行设计说明.md §8)。页面上不加"立即生效"字样,也不为 + * ca 画第二枚徽标——生效时刻的完整口径归上面那份设计说明。 + * 徽标说明:本页不画"未标定"类徽标(与两点标定无关,稀释真告警——死区页 + * 同条纪律) + * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码) + * 作者:DTU 项目组 + * 创建日期:2026-10-27 + * 修改记录: + * v1.0 2026-10-27 DTU 项目组 创建(道G task-9;判据 = docs/任务规划/ + * UI补齐.md §2 第 1 行「重启即生效 + 104 侧读回 + * 真值 + 段数键数上限核对」,台架判据见该卡与 + * IEC104上行设计说明.md §8) + */ +/* 头文件包含区 */ +#include + +#include "ui_page_comm.h" + +#include "ui_data.h" /* 通道六:行现状/写入口/脏位(本层唯一的数与守卫的来源) */ +#include "ui_tokens.h" +#include "ui_w_setlist.h" + +/* 私有宏定义区 */ + +/* 保存行显示态(值与 ui_w_ytview 的 YT_ST_* 同序同义,胶水面独立一份) */ +#define CM_ST_ENTER 0u /* 空闲:保存行显示 ENTER */ +#define CM_ST_WAIT 1u /* 按过保存、脏位仍在:WAIT */ +#define CM_ST_OK 2u /* 脏位归零(或按时无脏):OK,锁存不回退 */ +#define CM_ST_ERR 3u /* 最近一次写被执行侧拒:粘滞 ERR */ +#define CM_BAD_ALL 0x03u /* 两行反显位图全掩码 */ + +/* 静态变量定义区 */ + +/* 行表在 RAM 现建(合法带唯一出口在执行侧,本文件零范围字面量——通道六 + * 表注第①条;行名是本机界面文案归本页,行序即 UI_DATA_COMM_ROW_* 下标) */ +static const char *const k_comm_name[UI_DATA_COMM_ROW_N] = { + "公共地址", + "监听端口" +}; +static ui_w_setlist_row_t s_rows[UI_DATA_COMM_ROW_N]; +static uint8_t s_n; /* 实际建出行数(半表纪律:读失败即停) */ +static uint8_t s_st; /* 保存行显示态 CM_ST_* */ +static uint8_t s_badmap; /* 写被拒行反显位图(bit0=CA bit1=PORT) */ + +/* 本页定值表实例(组件状态独占一份,勿跨页共用) */ +static ui_w_setlist_t s_sl; + +/* 函数定义区 */ + +/* + * 函数功能:写结果记账(成功/被拒共一个入口——死区页 yt_note_write 同款) + * 入口参数:a_id - 行下标 uint8_t;a_rc - 注入口返回值 int + * 返回值:无 + * 限定条件:set 回调链内(uiTask 按键) + * 函数说明:拒绝 → 该行反显 + 粘滞 ERR;成功 → 只清这一行反显 + 作废上一次 + * 保存结果(改过任何一位,上一轮的 OK/ERR 都不该再挂屏) + */ +static void cm_note_write(uint8_t a_id, int a_rc) { + if (a_rc != 0) { + s_badmap = (uint8_t)(s_badmap | (uint8_t)(1u << a_id)); + s_st = CM_ST_ERR; + } else { + s_badmap = (uint8_t)(s_badmap & (uint8_t)~(1u << a_id)); + s_st = CM_ST_ENTER; + } +} + +/* + * 函数功能:保存行按键(值早在逐行 ENTER 时写入副本并置脏;这里只是落盘 + * 确认的起点) + * 入口参数:无 + * 返回值:无 + * 限定条件:可编辑态、保存行 ENTER;uiTask 按键链 + * 函数说明:先清反显再评估;脏位仍在 → WAIT 锁存观察,无脏 → 直接 OK。 + * 真机落盘 = defaultTask 每轮无条件 iec104_comm_store_save(), + * WAIT 窗口 ≤50 ms;模拟器假件刻意不自动清脏(由无头 pump 清), + * 否则 WAIT 态永远不会被像素回归覆盖(道F 裁定④同一条理由) + */ +static void cm_commit(void) { + s_badmap = 0u; + s_st = (ui_data_comm_pending() != 0u) ? CM_ST_WAIT : CM_ST_OK; +} + +/* + * 函数功能:保存行值列回读(WAIT → OK 边沿在此收尾) + * 入口参数:无 + * 返回值:组件态枚举 ui_w_setlist_st_t + * 限定条件:绘制链内 + * 函数说明:锁存 = 只前进不回退("我这次动作的结果"与"档位此刻的状态"分开; + * 通道六里 ca/port 没有 104 侧自动写者,比通道五更干净,但锁存 + * 纪律不做两套) + */ +static ui_w_setlist_st_t cm_state(void) { + if ((s_st == CM_ST_WAIT) && (ui_data_comm_pending() == 0u)) { + s_st = CM_ST_OK; + } + switch (s_st) { + case CM_ST_WAIT: + return UI_W_SETLIST_ST_WAIT; + case CM_ST_OK: + return UI_W_SETLIST_ST_OK; + case CM_ST_ERR: + return UI_W_SETLIST_ST_FAIL; + default: + return UI_W_SETLIST_ST_IDLE; + } +} + +/* 函数功能:某行是否需越界反显(写被拒的账挂在这行值上) 返回值:反显 bool */ +static bool cm_bad(uint16_t a_row) { + if (a_row >= 8u) { + return false; + } + return ((s_badmap >> a_row) & 0x1u) != 0u; +} + +/* + * 函数功能:读某行当前值(int32 定点通道,decimals 恒 0 = u16 域直达) + * 入口参数:a_id - 行下标 uint8_t + * 返回值:当前值 int32_t + * 函数说明:读失败回 0 不回旧缓存——组件零缓存,0 是唯一诚实的退化值 + * (ui_data 通道五同口径) + */ +static int32_t cm_get(uint8_t a_id) { + ui_data_comm_row_t r; + + if ((a_id >= s_n) || (ui_data_comm_row(a_id, &r) != 0)) { + return 0; + } + return (int32_t)r.val; +} + +/* + * 函数功能:写某行(经通道六 → iec104_comm_*_set;守卫与置脏在带内执行侧) + * 入口参数:a_id - 行下标 uint8_t;a_val - 编辑器提交的整数 int32_t + * 返回值:无(结果经 cm_note_write 记账) + * 限定条件:set 回调链内 + * 函数说明:负数(编辑器理论不产)原样送 setter 拒掉,本页不做第二次守卫 + * ——合法带唯一出口在 iec104_comm.h,抄一份就是第二真源 + */ +static void cm_write(uint8_t a_id, int32_t a_val) { + if (a_id >= s_n) { + return; + } + cm_note_write(a_id, ui_data_comm_set(a_id, (uint32_t)a_val)); +} + +/* + * 函数功能:开页建表 + 绑视图(接 init_cb) + * 入口参数:无 + * 返回值:无 + * 限定条件:框架开页链内(本次进入权限已装载——先装权限再调 init_cb 的 + * 时序依据见 ui_page_view_param.c 同位置注释) + * 函数说明:两行逐行现取(lo/hi/值全现给);读到第 i 行失败停在 i 行—— + * 半张表比空表危险的反面论证同 ui_w_ytview.yt_fill;未注入 = + * 0 行空表页,一个像素都不画假数据 + */ +static void pg_init(void) { + uint8_t i; + + s_st = CM_ST_ENTER; + s_badmap = 0u; + s_n = 0u; + for (i = 0u; i < (uint8_t)UI_DATA_COMM_ROW_N; i++) { + ui_data_comm_row_t r; + + if (ui_data_comm_row(i, &r) != 0) { + break; + } + s_rows[i].name = k_comm_name[i]; + s_rows[i].id = i; + s_rows[i].kind = UI_W_SETLIST_KIND_NUM; + s_rows[i].decimals = 0u; + s_rows[i].min = (int32_t)r.lo; + s_rows[i].max = (int32_t)r.hi; + s_rows[i].opts = NULL; + s_rows[i].unit = NULL; /* CA 与端口都不是物理量,不挂单位 */ + s_n++; + } + (void)memset(&s_sl, 0, sizeof(s_sl)); + s_sl.rows = s_rows; + s_sl.count = s_n; + s_sl.get = cm_get; + s_sl.set = cm_write; + s_sl.bad = cm_bad; + if (ui_content_page_is_editable() != false) { + s_sl.commit = cm_commit; + s_sl.state = cm_state; + } + ui_w_setlist_reset(&s_sl); +} + +/* + * 函数功能:总行数(数据行实给 + 可编辑态才有保存行;接 count_cb) + * 入口参数:无 + * 返回值:总行数 uint16_t + * 限定条件:框架绘制/翻页链内 + */ +static uint16_t pg_count(void) { + return ui_w_setlist_count(&s_sl); +} + +/* + * 函数功能:逐行取数(行现状在组件与注入表里现取,本层无缓存) + * 入口参数:a_row / a_name / a_value - 契约见 ui_content_page.h + * 返回值:无 + * 限定条件:框架绘制链内 + */ +static void pg_refresh(uint16_t a_row, char *a_name, char *a_value) { + ui_w_setlist_refresh(&s_sl, a_row, a_name, a_value); +} + +/* + * 函数功能:自绘层(编辑光标反显 + 写被拒行反显) + * 入口参数:a_layer / a_clip - 契约见 ui_content_page.h + * 返回值:无 + * 限定条件:框架绘制链内,内置区块画完之后 + */ +static void pg_draw(lv_layer_t *a_layer, const lv_area_t *a_clip) { + ui_w_setlist_draw(&s_sl, a_layer, a_clip); +} + +/* + * 函数功能:按键转发(接 key_cb) + * 入口参数:a_key - LVGL 键值 lv_key_t + * 返回值:true - 本页已消费;false - 交回框架(↑↓ 移选、← 关页) + * 限定条件:描述符 edit_capable=1 + key_intercept=1(编辑中 ←→ 移数字光标, + * 必须先给页面——整定四页同款实证) + * 函数说明:第一条门 = 只读进入(未绑 commit)整页交回框架,与"本页没有 + * key_cb"逐字等价(黄金只读态为证) + */ +static bool pg_key(lv_key_t a_key) { + if (s_sl.commit == NULL) { + return false; + } + return (ui_w_setlist_key(&s_sl, a_key) != 0u); +} + +/* + * 页面描述符(外部链接:生成物 ui_page_registry.c 按菜单树 #page=comm 绑定引用) + * 列表页骨架 + 可编辑 + ← 拦截;row_count 给静态上限(2 数据行 + 保存行 = 3), + * 运行时以注入表现报为准;band_h>0 选中反显(死区页同款几何令牌) + */ +const content_page_def_t ui_page_comm_desc = { + .id = "comm", + .title = "通讯设置", + .selectable = 1u, + .edit_capable = 1u, + .key_intercept = 1u, + .top_y = UI_LIST_TOP, + .pitch = UI_ROW_PITCH, + .band_h = UI_ROW_BAND_H, + .text_dy = 0, + .left_x = UI_LIST_LEFT_X, + .right_x = UI_LIST_RIGHT_X, + .rows_per_page = UI_LIST_ROWS, + .row_count = 3u, + .init_cb = pg_init, + .count_cb = pg_count, + .refresh = pg_refresh, + .draw_cb = pg_draw, + .key_cb = pg_key, +}; diff --git a/Drivers/BSP/UI/ui_page_comm.h b/Drivers/BSP/UI/ui_page_comm.h new file mode 100644 index 00000000..ab2f2562 --- /dev/null +++ b/Drivers/BSP/UI/ui_page_comm.h @@ -0,0 +1,25 @@ +/* + * 模块名称:UI Page Comm (通讯设置内容页对外声明) + * 模块功能:供生成物 ui_page_registry.c 引用的页面描述符声明 + * 适用平台:STM32F407ZGTx / PC 模拟器(共用源码) + * 作者:DTU 项目组 + * 创建日期:2026-10-27 + * 修改记录: + * v1.0 2026-10-27 DTU 项目组 创建(道G task-9) + */ +#ifndef UI_PAGE_COMM_H +#define UI_PAGE_COMM_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include "ui_content_page.h" + +extern const content_page_def_t ui_page_comm_desc; + +#ifdef __cplusplus +} +#endif + +#endif /* UI_PAGE_COMM_H */ diff --git a/Drivers/BSP/UI/ui_page_registry.c b/Drivers/BSP/UI/ui_page_registry.c index 7de2fe40..001df90a 100644 --- a/Drivers/BSP/UI/ui_page_registry.c +++ b/Drivers/BSP/UI/ui_page_registry.c @@ -13,6 +13,7 @@ #include "ui_page_calib.h" #include "ui_page_clear.h" #include "ui_page_clock.h" +#include "ui_page_comm.h" #include "ui_page_dc.h" #include "ui_page_deadband.h" #include "ui_page_device_info.h" @@ -46,6 +47,7 @@ const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT] = { { 24u, &ui_page_clock_desc }, /* node 24 时钟设置 */ { 25u, &ui_page_reset_desc }, /* node 25 强制复归 */ { 27u, &ui_page_clear_desc }, /* node 27 清除记录 */ + { 29u, &ui_page_comm_desc }, /* node 29 通讯设置 */ { 30u, &ui_page_net_cfg_desc }, /* node 30 网口设置 */ { 33u, &ui_page_soe_desc }, /* node 33 SOE记录 */ { 39u, &ui_page_selftest_desc }, /* node 39 自检记录 */ diff --git a/Drivers/BSP/UI/ui_page_registry.h b/Drivers/BSP/UI/ui_page_registry.h index 1d2addfd..b70f2a12 100644 --- a/Drivers/BSP/UI/ui_page_registry.h +++ b/Drivers/BSP/UI/ui_page_registry.h @@ -25,7 +25,7 @@ typedef struct { const content_page_def_t *desc; /* 页面描述符(Flash const) */ } ui_page_entry_t; -#define UI_PAGE_MAP_COUNT 21u /* 绑定了内容页的叶子数 */ +#define UI_PAGE_MAP_COUNT 22u /* 绑定了内容页的叶子数 */ extern const ui_page_entry_t g_ui_page_map[UI_PAGE_MAP_COUNT]; diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index 2be9583e..22c5a228 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -932,6 +932,11 @@ 1 ..\App\util\sha256.c + + boot_stats.c + 1 + ..\App\util\boot_stats.c + vol_lock.c 1 @@ -3636,6 +3641,11 @@ 1 ..\Drivers\BSP\UI\ui_page_deadband.c + + ui_page_comm.c + 1 + ..\Drivers\BSP\UI\ui_page_comm.c + diff --git a/docs/README.md b/docs/README.md index e9bb3bbb..b5a64d64 100644 --- a/docs/README.md +++ b/docs/README.md @@ -129,6 +129,7 @@ docs/ |---|---| | [`看门狗单元测试说明.md`](测试说明/看门狗单元测试说明.md) | Task Watchdog 判定状态机(位图 / 掩码 / 节流 / tick 回绕),39 用例 | | [`系统时钟单元测试说明.md`](测试说明/系统时钟单元测试说明.md) | sys_clock 时间换算 / 星期 Zeller / 30 天惰性再基准化 / RTC 故障降级,100 断言点 | +| [`开机账本单元测试说明.md`](测试说明/开机账本单元测试说明.md) | boot_stats 上电次数四态(不可信 / 首次启用 / 延续 / 电池域掉过电)/ SRAM 记录校验与格式版本 / I2C 失败分支 / 复位原因优先级链,21 断言点 | | [`配置文件单元测试说明.md`](测试说明/配置文件单元测试说明.md) | cfg_file INI 解析 / 值编解码 / load-save-poll 契约 / 超长守卫,149 断言点(含抓出的外科手术保存死码 P0) | | [`ADC输入层单元测试说明.md`](测试说明/ADC输入层单元测试说明.md) | adc_input 语义通道映射 / ADC1 顺序扫描(**Rank↔通道判据**)/ poll 契约 / HAL 故障路径,70 用例 | | [`相量层单元测试说明.md`](测试说明/相量层单元测试说明.md) | phasor 全周傅氏系数表 / 幅值·相位 / 谐波抑制与提取 / 频偏 / 采样环 / 双区交换,57 用例 | diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index fc049d7a..72edc512 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1` | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 260,004 B (253.91 KB) | 1,048,576 B (1024 KB) | 24.8% | -| RW_IRAM1 | 80,920 B (79.02 KB) | 131,072 B (128 KB) | 61.7% | +| ER_IROM1 | 261,544 B (255.41 KB) | 1,048,576 B (1024 KB) | 24.9% | +| RW_IRAM1 | 81,072 B (79.17 KB) | 131,072 B (128 KB) | 61.9% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **261,540 B (255.4 KB)** | 1,048,576 B (1024 KB) | **24.9%** | +| **Flash (ROM 合计)** | **263,116 B (256.9 KB)** | 1,048,576 B (1024 KB) | **25.1%** | 各区的容量与常量来源: @@ -512,9 +512,17 @@ vApplicationAssertFailed` 共 103 行),而 `vApplicationAssertFailed` 钩子 记账 208 = D360)与 `[ERR]→NVIC_SystemReset` 这类终点路径,真实运行不会把环走满;它 也不算 ISR 上下文保存(约 100 B)与间接调用(§4.0.6 盲区条同源)。所以静态链适合当 **硬线**(可复现、无噪声、机器可判:margin ≥ max(1024 B, cfg×30%)),不适合当实际余量; -实际余量看 §4.0.2 的串口 `[STACK]` 水位。台架判据「栈水位不劣于上一轮」必须加容差或 -改拿本节的静态硬线(「栈水位判据加容差或改静态最坏链当硬线」卡 = task-32,与本节和 -§4.0.2 互指)。 +实际余量看 §4.0.2 的串口 `[STACK]` 水位。**台架栈判据已定稿(task-32,2026-10-27)——三档分权**: +静态 margin 是**唯一拦提交项**(提交时由 `scripts/stack_report.py` 守,见 §4.0.6);运行时水位只做 +**趋势告警**:同镜像 ≥3 圈取最大值与基线轮最大值互比,容差 **tol = 160 B**(来源 = 实测同镜像最大摆动 +80 B × 2,再向上取整到 16 B 格;登记与改数规则见 `scripts/stack_tolerance.tsv`——**只有台架重测后才能 +回填,被这把尺考核的人不得当场决定尺子的松紧**);运行时唯一能升级成 FAIL 的条件是绝对底线 +`free_min < 25% cfg`;**采集健康门不过 = VOID(exit 2)**,不红不绿也不进台账——按码聚合的人不会把 +「端口没开」记成「又崩了一次栈」。判据与门都可执行:`scripts/stack_gate.py`(`--selftest` 14 条),它是 +**台架侧门禁**(每轮台架跑完必跑;Lead 裁定 2026-10-27:不接 pre-commit——提交时既无设备也无当轮日志, +挂上去只会得到「永远 SKIP」或「拿上一轮日志判这一轮的码」,后者正是体积门禁那条假凭证的同族)。 +口径正文:`docs/测试说明/启动日志原子性复验流程.md` §5(三档表与门的细节都在那节)。 +本节口径与本文的运行时水位小节、静态硬线小节互指(同文件内引用,不再另标编号,避免跨文档串号)。 **取证纪律(本节方法的可复用模板)**:凡跨树/跨构建引用数字,必须同时钉**产物路径 + Linker run 时刻**——commit hash 不是稳定身份(重写正文即换 hash),tree hash + 父提交 diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index 3abe1469..869e3703 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,19 +15,19 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 260,004 B | 1,048,576 B | 24.8% | 参考 | +| ER_IROM1 (Flash) | 261,544 B | 1,048,576 B | 24.9% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | -| RW_IRAM1 | 80,920 B | 131,072 B | 61.7% | PASS | +| RW_IRAM1 | 81,072 B | 131,072 B | 61.9% | PASS | | 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 | |---|---|---|---| | 网络 | 13,702 B | 12 KB | ⚠ 超 1.4 KB | | 存储 | 7,072 B | 12 KB | OK | -| UI | 34,085 B | 16 KB | ⚠ 超 17.3 KB | +| UI | 34,188 B | 16 KB | ⚠ 超 17.4 KB | | RTOS 堆 | 57,376 B | 56 KB | 堆=配额 | -| 应用 | 15,537 B | 8 KB | ⚠ 超 7.2 KB | -| 其它 | 10,523 B | — | 参考 | +| 应用 | 15,552 B | 8 KB | ⚠ 超 7.2 KB | +| 其它 | 10,553 B | — | 参考 | - **门禁**:pre-commit `mem-budget` 钩子(`scripts/ram_baseline.py --gate`)—— diff --git a/docs/子系统设计/IEC104上行设计说明.md b/docs/子系统设计/IEC104上行设计说明.md index 9c699b83..20858acb 100644 --- a/docs/子系统设计/IEC104上行设计说明.md +++ b/docs/子系统设计/IEC104上行设计说明.md @@ -191,6 +191,30 @@ getter 合法带 ca∈1..65535 / port∈1024..65535,带外自愈回工厂默认 1/2404 (adc_cal 合法带哨兵同款;getter 不打日志,省 iec104Task 深链栈)。不挂段回调、 不新增事件位、不走 SYS_EVT_NET_RECONFIG(热切换代价见 §8,原「V0.1 固定 1」口径作废)。 +- **面板编辑(道G task-9,2026-10-27)**:「通讯参数 → 通讯设置」页(`#page=comm`, + `Drivers/BSP/UI/ui_page_comm.c`)经**通道六注入**(`ui_data_comm_ops_t`,理由与 + 通道五同构:分层 / 模拟器 / 「写值+置脏」原子性)编辑 ca/port;合法带由执行侧 + 现给(面板粗守与 setter 守卫同一出口,抄数即第二真源)。保存行 ENTER → + WAIT/OK 锁存判定 = `iec104_comm_dirty_peek()`,落盘 = defaultTask 每轮无条件 + `iec104_comm_store_save()`(run_par/prot_set 同款 B7 脏位形态,**无事件位**, + 延续上条 D5 口径)。屏上 OK 只证明**落盘完成**,不证明"已在新值上运行"—— + 生效时刻仍按上条(ca 重启 / port 重 bind)。容量核对:生产占 **6/8 段、 + [comm] 2/16 键**,超限现象 = 注册返 -1 → `comm init: FAIL` 启动锚 + getter + 回 ROM 默认,语义由 cfg_file 宿主套 TC-CONFIG-056/057 钉住,不抬宏。 + +- **观测计数(只读 getter,2026-10-27 道G task-12)**:`App/task/iec104_serv.c` 另有四枚 + **只增不清**的计数,经 `iec104_serv_get_stats()`(声明在 `App/task/iec104_serv.h`)对外只读: + `conn_up`(主站接入次数)/ `apdu_tx`(成功发出的 APDU 帧数)/ `tx_fail`(发送短写或失败次数)/ + `rx_bytes`(收到的**字节**数,不是帧数——帧边界在 link 层重组,serv 层看不见)。口径是 + **上电以来、跨重连不清零**,给页面用时那一栏要写「本次开机以来」,与运行时长同一类。 + ⚠ 与本节三枚**控制量**(`s_liveness_ctr` / `s_send_fail_run` / `s_conn_dead`,为"要不要拆链" + 服务、会被复位、只有 8 位)无关,拿它们当统计数字必错。为什么留在 serv.c 而不并进通用 + 统计模块、也不放进 `iec104_link_t`(实例是文件级 static,喂数据侧拿不到指针,且 + `link_open/link_close` 会把"上电以来"洗成"本次连接"):104 的账本归 104。getter + **任意上下文可读**(不取锁、不读时刻、不碰 socket;四枚 u32 单字原子,跨字段不保证同一 + 瞬间,四枚之间没有"合起来才成立"的判据),到顶饱和不回绕。**「总召次数 / 上送 I 帧数」 + 不在这里**:那是 link 层事件,serv 照不到,需要它得去 + `App/protocol/iec104/iec104_link.c` 加——本卡未做,别在页面上摆一个数却没有产生它的地方。 ## 6. RAM / 栈预算(2026-10-12 实测) diff --git a/docs/测试说明/开机账本单元测试说明.md b/docs/测试说明/开机账本单元测试说明.md new file mode 100644 index 00000000..2301e350 --- /dev/null +++ b/docs/测试说明/开机账本单元测试说明.md @@ -0,0 +1,128 @@ +# 开机账本单元测试说明(主机侧) + +> **模块**:Boot Stats(`App/util/boot_stats.c` + `App/util/boot_stats.h`) +> **测试类型**:**主机侧纯逻辑单元测试**(L0.5 层)——在 PC 上离线验证"上电次数"这块 +> RTC 用户 SRAM 账本的**四态判定、记录校验、I2C 失败分支、幂等与饱和**,以及复位原因 +> 优先级链的唯一出处。零硬件、零烧录、毫秒级。 +> **状态**:✅ 已落地(21 断言点,MSVC `/W4` 与 cppcheck 均 0 告警) +> **日期**:2026-10-27(道G task-12) +> **关联文档**: +> [`../../AGENTS.md`](../../AGENTS.md)(RTOS 与内存铁律、`[ERR]` 通道口径)、 +> [`测试开发标准流程.md`](测试开发标准流程.md)(测试分层与门禁总览)、 +> [`主机单测覆盖率口径.md`](主机单测覆盖率口径.md)(覆盖率总闸如何收编本套)、 +> [`../子系统设计/系统墙钟时间维护说明.md`](../子系统设计/系统墙钟时间维护说明.md) +> §13.1(同一块 70 B 用户 SRAM 的**占用分配表**,本模块占 0..7)、 +> [`../子系统设计/IEC104上行设计说明.md`](../子系统设计/IEC104上行设计说明.md) +> (同一张运行报告页的另一半数据源:104 观测计数) + +--- + +## 1. 为什么要单独做这一套 + +被测量的用途是回答"这台装置上了多少次电"。这类账本**错的时候不像错**:屏幕上永远 +有一个数字,格式正常、量级正常,没有任何一行日志会说它是假的。四条只能靠构造才能 +复现的失效路径: + +1. **I2C 读失败被当成"没有记录"**:一次总线抖动就把计数当 0 重来,从此从 1 往上数。 + 更坏的是读失败还顺手回写——那会把可能完好无损的旧记录**覆盖掉**,故障从"这次读不到" + 升级成"永久少一笔账"。 +2. **"本模块首次启用"与"RTC 电池域掉过电"被合并成一态**:这两件事的文案完全不同 + (前者是"账本从这一版固件开始",后者是"换过电池,历史没了")。合并就是拿前者 + 冒充后者。 +3. **只信 magic、不信校验和**:电池欠压造成的坏区常是整片全 0 或整片全 FF,magic + 那一关自然过不去;但更常见的失效是**个别字节翻转**——那时 magic 可能仍然是好的, + 而次数会错 256 倍。这种数字最容易被人当成真的。 +4. **到顶回绕而不是饱和**:回绕成个位数会被读成"刚上电几次";停在上限则一眼能看出 + 到头了。 + +真机只能覆盖"一切正常"那一条:SRAM 完好、I2C 通、计数往下加。上面四条在板端**构造不 +出来**(要真的把 I2C 拉死、真的换电池、真的翻转一个字节),而主机侧只要给假件注入一个 +返回值。这是本套存在的全部理由。 + +--- + +## 2. 用例清单(本文是这些号的**定义位**) + +跑法见 §4。断言全部只盯结构体字段与假 SRAM 的字节,不依赖打印。 + +| 号 | 判据 | 为什么是这一条 | +|---|---|---| +| `TC-BOOTSTAT-001` | SRAM 成片全 1(magic 过不去)且 RTC 未报欠压 ⇒ 判 FRESH、计数从 1 起、落盘一笔 | 首次启用与掉电必须分开;第一格是 1 不是 0 | +| `TC-BOOTSTAT-002` | 合法记录 count=7 ⇒ 判 CONTINUED、count=8,且回写后介质里也是 8 | 账要真的写回去,不是只写在 RAM | +| `TC-BOOTSTAT-002b` | 介质不动、只重开被测模块 ⇒ 继续往上数(9) | 这就是"跨掉电保持"的本体 | +| `TC-BOOTSTAT-003` | 记录无效且 BLF 置位 ⇒ 判 RESTARTED,绝不判 FRESH | 换过电池要说换过电池 | +| `TC-BOOTSTAT-004` | 读失败 ⇒ 判 UNKNOWN、计数归零显示、**零次回写**,假 SRAM 逐字节不变 | 读不到不许覆盖账本 | +| `TC-BOOTSTAT-004b` | 同一状态里 `ready=1` 且 `seq=UNKNOWN` | "不可信"必须与"零次""还没初始化"三种都能分辨 | +| `TC-BOOTSTAT-005` | 写失败 ⇒ 本次仍报数但 `write_failed=1` | 这条账只会少 1,不会重复计 | +| `TC-BOOTSTAT-006` | 同一次开机连调两次 init ⇒ 只 +1、只写一笔 | bring-up 重试不能把一次上电数成两次 | +| `TC-BOOTSTAT-007` | magic 与版本都好、中段漂一位 ⇒ 被校验和挡下,报 FRESH 而不是错账 | 挡住"256 倍的像真数字" | +| `TC-BOOTSTAT-008` | 格式版本不是当前值 ⇒ 整条作废并按当前版本重写(校验同步重算) | 将来改布局时旧记录不能错位读 | +| `TC-BOOTSTAT-009` | 计数已达上限 ⇒ 不再自增、不再写回 | 饱和而不是回绕 | +| `TC-BOOTSTAT-010` | BLF 读不到但记录有效 ⇒ 仍判 CONTINUED、`rtc_blf=0` | 读不到佐证不该把好账判坏 | +| `TC-BOOTSTAT-011` | SRAM 整片全 0(另一种擦除形态)⇒ FRESH 且第一格是 1 | 出厂态与掉电态的字节形态不同,两态都要认 | +| `TC-BOOTSTAT-020` | `IWDG \| SFT` ⇒ IWDG | 崩溃钩子常以软件复位收尾,两态同置时报看门狗才有价值 | +| `TC-BOOTSTAT-020b` | `WWDG \| SFT` ⇒ WWDG;单独 SFT ⇒ SFT | 同上,且不能把纯软件复位误判成狗 | +| `TC-BOOTSTAT-020c` | `POR \| PIN` ⇒ POR;`PIN \| BOR` ⇒ PIN;只有 BOR ⇒ BOR;全零 ⇒ UNKNOWN | 全零必须承认"不知道",不许猜一个 | +| `TC-BOOTSTAT-020d` | 每个编码都有可读串、全纯 ASCII、未知编码回落 `unknown` | 无 BOM 文件里字符串必须 ASCII;开机极早期不能因编码挡启动 | +| `TC-BOOTSTAT-020e` | 快照暴露的就是 `g_rcc_csr_latched` 里那次复位原因 | 串口与屏幕不可能各说一套(链已收成一份真源) | +| `TC-BOOTSTAT-030` | 未 init ⇒ 整表零且 `ready=0` | 页面该显示占位,而不是"上电 0 次" | +| `TC-BOOTSTAT-030b` | `boot_stats_get(NULL)` 无害 | 观测口不该因调用者手滑而炸 | +| `TC-BOOTSTAT-030c` | init 后连读 100 次,三类 I2C 调用计数**纹丝不动** | "任意上下文可读"不是口头承诺 | + +> ⚠ 这些号是**主机侧用例号**,不是板端用例号,也不是需求号;族口径沿用 +> `docs/编号规范.md` 的 TC 族(默认 `check_dangling`),所以**本文就是定义位**: +> 新增/删除用例必须同步改本表,否则 `id-check` 校验 3 会报悬空、校验 7 会报孤儿。 + +--- + +## 3. 桩的边界 + +| 头 | 取真实还是桩 | 原因 | +|---|---|---| +| `stub/main.h` | **桩** | 只给六个 `RCC_CSR_*RSTF` 位掩码;位号照抄 CMSIS(BOR25 / PIN26 / POR27 / SFT28 / IWDG29 / WWDG30)。真实 `main.h` 会拉整棵 HAL 头树,PC 上编不动。缺宏 ⇒ 编译直接失败,不会出现"桩悄悄放过"。 | +| `sd2506.h` / `bsp_err.h` / `boot_stats.h` / `app_main.h` | **真实头** | 都不依赖 HAL 类型,用真头才不会出现"假件与真契约漂移" | +| `sd2506_read_sram` / `sd2506_write_sram` / `sd2506_read_ctr1` | **假件(本套实现)** | 一片 70 B 假 SRAM + 三个可注入的错误码;地址越界按真驱动契约返 `BSP_ERR_PARAM` | +| `g_rcc_csr_latched` | 本套给出定义 | 真定义在 `App/app/app_main.c`,本套不编译它 | + +--- + +## 4. 怎么跑 + +```powershell +& '.venv\Scripts\python.exe' test/host/boot_stats/run_unit_test.py +``` + +退出码 0=PASS / 1=测试失败 / 2=环境错误。汇总行是 `s_passed/s_total`——**分子必须是 +真过了几条**;曾经写成 `s_total/s_total`,那种情况下失败时会打印 +"21/21 PASSED (failed=1)" 这种自相矛盾的串,等于把门禁的失败显示成成功。 + +--- + +## 5. 注入反证台账(这套断言有没有牙) + +逐个注入、跑套件、逐字节还原(还原后复跑全绿): + +| 注入的退化写法 | 变红的号 | 说明 | +|---|---|---| +| 读失败改成"当成读出一片空白"继续往下 | `TC-BOOTSTAT-004`、`TC-BOOTSTAT-004b` | 会回写、把完好账本覆盖掉 | +| 合并 FRESH 与 RESTARTED(不看 BLF) | `TC-BOOTSTAT-003` | 拿首次上电冒充 RTC 掉电 | +| 只判 magic、丢掉校验和 | `TC-BOOTSTAT-007` | 256 倍错账照发 | +| 去掉饱和判据(改成回绕) | `TC-BOOTSTAT-009` | 到顶变小数字 | +| BLF 读失败就早退并把记录判坏 | `TC-BOOTSTAT-010` | 判定顺序写反的代价 | + +> ⚠ 第一条**最初是一处假变异**:注入的是未声明标识符,套件 `exit 2`(编译错), +> 看着也"红"。那不算证据——**断言变红才是**。上表全部用能编过、能跑起来的退化写法重做过。 + +--- + +## 6. 主机侧测不出、必须上板的部分 + +如实登记,别在文档里当成已验证: + +- **真掉电保持**:断电 ≥5 min 再上电,`boot_count` 应 +1 且旧值保持(同时留证 BLF + 与电池电压)——主机侧的"假 SRAM 不动"只是模拟,不能证明电池域真的保住了。 +- **换电池**:应落 `RESTARTED` 而不是 `FRESH`。 +- **复位原因对照**:IWDG 触发 → 快照 = IWDG;软复位 → SFT;掉电上电 → POR/BOR。 + 三者与串口那条 `reset cause:` 必须同源(同一份判定链,见 `TC-BOOTSTAT-020e`)。 +- **写点不落在喂狗点**:`boot_stats_init()` 在内核启动前执行,主机侧无法验证"不占 + 喂狗预算",需板端结合 `[STACK]`/`task_wd` 水位看。 diff --git a/docs/算法/采集与算法层设计说明.md b/docs/算法/采集与算法层设计说明.md index 38be3793..da7c04b7 100644 --- a/docs/算法/采集与算法层设计说明.md +++ b/docs/算法/采集与算法层设计说明.md @@ -351,6 +351,38 @@ UA 无幅值落 IA → 0.0020 Hz。 6. **表只许尾部追加**:`k_inst[]`(相量实例)、`prot_elm` 实例表、定值元数据表—— 枚举序 = 数组序 = 校验和覆盖序,插入或重排等于改校验和与 104 点表。 +### 10.1 标定折算 `adc_cal`:谁写 RAM、谁落盘、什么时机(道G task-43) + +这一小节存在的理由很具体:**此前全仓没有任何一处写清"k/b 改完为什么会丢"**,连写卡片的人都猜错过一次 +(以为 `cfg_file_poll()` 的 2 s 轮询能兜住未落盘的改动——方向恰好相反)。三条事实链,每条可复算: + +| 问题 | 唯一答案 | 代码落点 | +|---|---|---| +| 谁是 k/b 的真源 | `[adc_cal]` / `[adc_cal_ofs]` 两段的引擎 RAM 副本(`App/algo/adc_cal.c`),别处不得再存一份系数 | `s_kcfg` / `s_bcfg`;`App/algo/run_par.h` 头注释「不另存一份系数」 | +| 谁能写 RAM | **只有 run_par 的两支语义 setter** `run_par_cal_k_set()` / `run_par_cal_b_set()`——"写值 + 置脏"成对发生(task-7 落地,lead 裁②消除两份置脏逻辑);`adc_cal_set()` 本身**只写 RAM、不置脏、不落盘、不返回"稍后会存"** | `App/algo/run_par.c` 语义 setter 内 `adc_cal_set()` 之后才 `s_dirty = 1u` | +| 谁落盘、什么时机 | 只有 defaultTask(配置引擎属主)的 `run_par_store_save()`:iec104Task 主循环 `run_par_dirty_peek()` → 置 `SYS_EVT_RUN_PAR_SAVE` → 属主 `cfg_file_save()` 整档外科保存 → 成功清脏,失败留脏下轮重试 | `App/algo/run_par.c` 落盘函数;本模块自身零文件 IO | + +**`cfg_file_poll()` 不是保存路径**:它的方向是**盘 → RAM 的重载**(外部改档 → 沉降去抖 → 重载,见 +`App/cfg/cfg_file.h` 同族契约与 `docs/子系统设计/配置文件设计说明.md`)。拿它"兜住"未落盘的改值, +等于把 FTP 侧的旧档倒灌回 RAM,比不兜更坏。**任何"改了就等于存了"的意图式陈述一律按本表改写** +(`docs/代码规范/pre-commit使用说明.md` §2.5 第二条)。 + +**"改了没落盘"现在是可诊断事实**(第②项;先例是 `App/fs/vol_lock.h` 的观测计数设计:只计数、 +不取锁、不在调用点打日志、不打 `[ERR]`)。三个观测计数的**定义只在 `App/algo/adc_cal.h` 一处**, +本文只讲怎么读: + +| `set_ok − persisted_set_ok` | `run_par_dirty_peek()` | 定性 | +|---|---|---| +| 0 | 0 | 盘与 RAM 齐(或压根没改过)——健康 | +| >0 | 1 | 改了、还没到落盘时机——正常在途 | +| >0 | 0 | **改了且永远不会落盘**:有写者绕过语义 setter 直调 `adc_cal_set()` | + +`persisted_set_ok` 是**水位**而不是计数:`run_par_store_save()` 成功路径上盖写为当时的 `set_ok` +(一次 save 覆盖整档、可能对应多次逐通道整定,自增会留下"假未落盘";回归锁 = TC-ADCCAL-021)。 +两个盲区写在这里,别当成"看得见全部":① 本层刻意不挂 cfg_file 回调,**盘 → RAM 的覆盖不产生任何计数**, +未落盘的改值被一次重载无声吞掉照不到;② 三个数之间没有共同时刻,定性只按上表**成对读**, +需要时刻请在消费者侧自盖并冠 `obs_` 前缀(先例:喂狗点单写者 latch)。 + --- ## 11. 资源足迹(改容量前先看这里) @@ -365,6 +397,7 @@ UA 无幅值落 IA → 0.0020 Hz。 | `adc_input.c` `s_stm32_raw[16]` + `s_tpafe_raw[8]` | 48 B | — | | `adc_input.c` `s_stm32_dma[2×9]`(`uint16_t`) | 36 B | 必须在 `RW_IRAM1`(DMA2 不可达 CCM)→ `docs/子系统设计/ADC采样节拍设计说明.md` §4.3 | | `adc_health.c` 内部状态 | ≈ 40 B | — | +| `adc_cal.c` 三个观测计数(`volatile uint32_t`) | **12 B** | 只增不清零、不参与判定;定义与读法见 §10.1 | | 运行期栈 `phasor_run()` 的 `win[20]` | 80 B / 帧 | 抬高 `PHASOR_N` 直接抬栈 | | 运行期栈 `freq_run()` 局部 | ≈ 30 B / 帧 | 无大局部数组 | @@ -386,6 +419,7 @@ adcTask 静态最坏链与余量的**台账在** `docs/内存管理/RAM使用分 | `prot_set` | 范围校验、all-or-nothing 装载、封存-自检-闭锁、Q10 往返、元数据表自检、blob 往返与损坏拒绝、换代拒绝、`uint64_t` bad_mask | `test/host/prot_set/` | | `prot_elm` | 滞回与定时限、欠量方向、投退、闭锁联动、时基回绕、位图聚合、测频接线与"必不启动值" | `test/host/prot_elm/` | | `freq` | J1~J6(§7.2)+ 直流与谐波抑制 + UA→IA 源回退 + 守卫与幂等 | `test/host/freq/` | +| `adc_cal` / `run_par` | Q16 无舍入与合法带含端点、坏文件/坏段/坏零点回退、真 cfg 引擎往返、未标定闭锁单点,以及 §10.1 那三态观测计数(含"一次 save 覆盖多次整定"的回归锁) | `test/host/adc_cal/`、`test/host/run_par/`(用例清单在两个套件文件头,本文不复制条数) | 命令与对应 pre-commit 钩子名见 `App/algo/README.md` §4(钩子清单的属主是 `.pre-commit-config.yaml`,用例清单的属主是各 `test/host/<模块>/` 与上表细则文档——本文不复制数字, diff --git a/scripts/check_registration.py b/scripts/check_registration.py index 32206fd5..f9bd7198 100644 --- a/scripts/check_registration.py +++ b/scripts/check_registration.py @@ -88,6 +88,7 @@ SIM_EXEMPT = [ ("App/algo/adc_input.c", "依赖 HAL/TPAFE 取数,MCU 专属"), ("App/algo/phasor.c", "L2 相量纯算法,模拟器无消费路径"), ("App/algo/prot_elm.c", "L3 保护元件纯算法,模拟器无消费路径"), + ("App/util/boot_stats.c", "开机账本(SD2506 用户 SRAM + RCC_CSR 快照,依赖 I2C/HAL 位掩码),MCU 专属;L0.5 覆盖于 test/host/boot_stats"), ("App/algo/prot_set.c", "L5 保护定值纯算法,模拟器无消费路径"), ("App/algo/freq.c", "L2 派生测频纯算法(输入=phasor 使用区),模拟器无消费路径"), ("App/cfg/prot_set_store.c", "定值持久化 MCU 胶水(依赖 prot_set 纯算法),模拟器无消费路径"), diff --git a/scripts/id_registry.json b/scripts/id_registry.json index 23623e4c..13d25aad 100644 --- a/scripts/id_registry.json +++ b/scripts/id_registry.json @@ -1693,6 +1693,28 @@ "owner_note": "B 层口径(用户裁定 2026-10-21):这些号实测在代码目录外零消费者(唯一外部引用是 id_aliases.json 自己),故属主登记处 = 单测代码本身,不逐号补文档;class 仍为 TC → 跨模块同号由校验 4 抓", "cursor_note": "游标按实测最大号收录(2026-10-21):越界即视为未登记的冒号牌" }, + { + "name": "TC-HOST-BOOTSTAT", + "class": "TC", + "title": "主机侧单测用例号:开机账本(上电次数与复位原因快照,test/host/boot_stats,道F task-12)", + "pattern": "\\bTC-BOOTSTAT-[0-9]{1,4}[a-z]?\\b", + "digit_width": [ + 2, + 4 + ], + "owner_docs_glob": [ + "test/host/boot_stats/*" + ], + "scope": "global", + "frozen": false, + "cursor": "TC-BOOTSTAT-030", + "cursor_owner_only": true, + "check_dangling": true, + "allow_orphans": true, + "subitem_ok": true, + "owner_note": "B 层口径(用户裁定 2026-10-21,同 TC-HOST-ADCCAL 先例;2026-10-27 道F task-12 建族):属主登记处 = 单测代码目录,不逐号补文档", + "cursor_note": "游标按建族时实测最大号收录(2026-10-27,实测最大 30):越界即视为未登记的冒号牌" + }, { "name": "TC-HOST-ADCCAL", "class": "TC", @@ -1729,13 +1751,13 @@ ], "scope": "global", "frozen": false, - "cursor": "TC-UIDATA-026", + "cursor": "TC-UIDATA-029", "cursor_owner_only": true, "check_dangling": true, "allow_orphans": true, "subitem_ok": true, "owner_note": "B 层口径(用户裁定 2026-10-21,同 TC-HOST-ADCCAL 先例;2026-10-25 道E task-14 建族):属主登记处 = 单测代码目录,不逐号补文档;本族号同时是 UI 不透明编号与 App 枚举的现场核对锚(SOE 类型/元件号)", - "cursor_note": "游标按建族时实测最大母号收录(2026-10-25):越界即视为未登记的冒号牌" + "cursor_note": "游标按建族时实测最大母号收录(2026-10-25):越界即视为未登记的冒号牌;2026-10-27 Lead 按交付号实抬" }, { "name": "TC-HOST-PROTDIM", @@ -1839,13 +1861,13 @@ ], "scope": "global", "frozen": false, - "cursor": "TC-IEC104-033", + "cursor": "TC-IEC104-034", "cursor_owner_only": true, "check_dangling": true, "allow_orphans": true, "subitem_ok": true, "owner_note": "B 层口径同其余 TC-HOST 族;本族是 2026-10-21 补漏——上批模块码改名前它是纯数字 `TC-104-*`,未进当时的悬空普查,故漏建族", - "cursor_note": "游标按实测最大号收录(2026-10-21)" + "cursor_note": "游标按实测最大号收录(2026-10-21);2026-10-27 Lead 按交付号实抬" }, { "name": "TC-HOST-NETCFG", diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index 0d73867f..53697183 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -9,4 +9,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-24 213780 18860 1204 131852 232640 22.2 75632 57.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736 2026-10-25 224860 24640 1332 135100 249500 23.8 79008 60.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4128;sys_log_mcu.o=3944;default_task.o=2736 2026-10-26 231712 25560 1520 136216 257272 24.5 80312 61.3 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4003;default_task.o=2736 -2026-10-27 233900 26104 1536 136808 260004 24.8 80920 61.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 +2026-10-27 235236 26308 1572 136924 261544 24.9 81072 61.9 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055;default_task.o=2736 diff --git a/simulator/CMakeLists.txt b/simulator/CMakeLists.txt index 74ee6d99..593a023a 100644 --- a/simulator/CMakeLists.txt +++ b/simulator/CMakeLists.txt @@ -117,6 +117,7 @@ set(UI_SOURCES ${UI_DIR}/ui_page_reset.c ${UI_DIR}/ui_page_calib.c ${UI_DIR}/ui_page_deadband.c + ${UI_DIR}/ui_page_comm.c ) # 运行/自检记录引擎(双端共享核心 + 平台存储后端;MCU 端编 sys_log_mcu.c,见 Keil 工程) diff --git a/simulator/main.c b/simulator/main.c index 7b365780..0db74c7b 100644 --- a/simulator/main.c +++ b/simulator/main.c @@ -291,6 +291,15 @@ static float s_yt_k[UI_DATA_YT_CH_N] = { static uint8_t s_yt_dirty; /* 镜像 run_par 的整档脏位(页面上 WAIT 的唯一来源) */ static int s_yt_nopump; /* --yt-nopump:拍 WAIT 态黄金专用(截图前不清脏) */ +/* 道G task-9 通讯参数假件:镜像 App/protocol/iec104/iec104_comm.c 的 RAM 副本 + * 与脏位。模拟器不链 iec104_comm.c(与 run_par 假件同一条理由,见上方镜像 + * 纪律注):值与合法带是镜像,初值 = IEC104_COMM_DEF_CA/_PORT,变更纪律同 + * s_yt_* 三件套 */ +static uint32_t s_comm_ca = 1u; /* 镜像 [comm] ca(工厂默认 1) */ +static uint32_t s_comm_port = 2404u; /* 镜像 [comm] port(工厂默认 2404) */ +static uint8_t s_comm_dirty; /* 镜像 iec104_comm 脏位 */ +static int s_comm_nopump; /* --comm-nopump:拍通讯页 WAIT 态黄金专用 */ + /* 死区粗守上限(工程量):镜像 RUN_PAR_DB_MAX_U/I/F 三档;分档与 feed 的 unit * 分档必须同序(同一条对表纪律) */ /* 函数功能:假死区粗守上限(工程量档,镜像 RUN_PAR_DB_MAX_U/I/F) 返回值:上限 float */ @@ -404,6 +413,75 @@ static void sim_yt_pump(void) { } s_yt_dirty = 0u; } + +/* ── 道G task-9:通讯参数假件(通道六的注入侧镜像) ────────────────────── */ + +/* + * 函数功能:假 [comm] 读行(值 + 合法带;带 = iec104_comm.h 带宏的镜像) + * 入口参数:a_idx - UI_DATA_COMM_ROW_CA/_PORT uint8_t + * a_out - 行现状 ui_data_comm_row_t * + * 返回值:0 - 已整份写出;-1 - idx 越界 int + */ +static int sim_comm_row(uint8_t a_idx, ui_data_comm_row_t *a_out) { + if (a_idx == UI_DATA_COMM_ROW_CA) { + a_out->val = s_comm_ca; + a_out->lo = 1u; + a_out->hi = 65535u; + return 0; + } + if (a_idx == UI_DATA_COMM_ROW_PORT) { + a_out->val = s_comm_port; + a_out->lo = 1024u; + a_out->hi = 65535u; + return 0; + } + return -1; +} + +/* + * 函数功能:假 [comm] 写行(带外拒、带内落副本并置脏——与真 setter 同形) + * 返回值:0 - 已写并置脏;-1 - 越界或带外拒 int + */ +static int sim_comm_set(uint8_t a_idx, uint32_t a_val) { + ui_data_comm_row_t r; + + if (sim_comm_row(a_idx, &r) != 0) { + return -1; + } + if ((a_val < r.lo) || (a_val > r.hi)) { + return -1; + } + if (a_idx == UI_DATA_COMM_ROW_CA) { + s_comm_ca = a_val; + } else { + s_comm_port = a_val; + } + s_comm_dirty = 1u; + return 0; +} + +/* 函数功能:假 [comm] 落盘脏位 返回值:1 有未落盘;0 干净 uint8_t */ +static uint8_t sim_comm_pending(void) { + return s_comm_dirty; +} + +static const ui_data_comm_ops_t s_comm_ops = { + sim_comm_row, + sim_comm_set, + sim_comm_pending +}; + +/* + * 函数功能:假 [comm] 落盘 pump(镜像 defaultTask 每轮无条件 + * iec104_comm_store_save;模拟器无失败面,pump 即落盘) + * 限定条件:与 sim_yt_pump 同两个调用点;--comm-nopump 时停转拍 WAIT + */ +static void sim_comm_pump(void) { + if (s_comm_nopump != 0) { + return; + } + s_comm_dirty = 0u; +} /* * 函数功能:SOE 记录假源——固定 3 条(镜像 App/task/ui_task.c 真机绑定的形态) * 入口参数:无 @@ -584,7 +662,7 @@ static void sim_publish_data_frames(void) { (void)memset(&info, 0, sizeof(info)); info.valid = 1u; info.uptime_s = 0u; - info.port_104 = 2404u; /* App/task/iec104_serv.h 的 IEC104_SERV_PORT */ + info.port_104 = (uint16_t)s_comm_port; /* 镜像 [comm] getter(真机 feed 同款:宏出口已删,道G task-9) */ info.port_disp = (uint16_t)UI_NET_SINK_PORT; /* 同源常量(BSP 头,模拟器可见) */ (void)snprintf(info.model, sizeof(info.model), "%s", "F30"); (void)snprintf(info.sw_ver, sizeof(info.sw_ver), "%s", "SV0.010"); @@ -725,6 +803,9 @@ static int run_headless(const char * keys, const char * path, int standby) { if (ui_data_yt_bind(&s_yt_ops) != 0) { printf("[sim] UI yt bind: FAIL\n"); } + if (ui_data_comm_bind(&s_comm_ops) != 0) { /* 道G task-9 通讯参数假表(两处同文 replace_all) */ + printf("[sim] UI comm bind: FAIL\n"); + } /* task-14 数据快照:镜像 ui_task.c 的 ui_data_init + 首帧发布(先于建页, * 页面任何时刻打开都拿得到帧,不必等装配节拍) */ sim_publish_data_frames(); @@ -759,6 +840,7 @@ static int run_headless(const char * keys, const char * path, int standby) { * OK/ERR/LOCK 三条判据的像素证据也就全都不成立) */ sim_param_pump(); sim_yt_pump(); /* 道F task-7:截图前清脏 = 拍 OK 黄金;--yt-nopump 停转 = 拍 WAIT 黄金(Lead 裁定④:两态都要有像素证据) */ + sim_comm_pump(); /* 道G task-9:通讯页同款两态;--comm-nopump 停转拍 WAIT */ /* --standby:注键后强制进入待机屏(截图前) */ if (standby != 0) { @@ -1154,6 +1236,9 @@ int main(int argc, char **argv) { if (strcmp(argv[i], "--yt-nopump") == 0) { s_yt_nopump = 1; /* 落盘 pump 停转:拍"已受理未落盘"(WAIT)黄金专用 */ } + if (strcmp(argv[i], "--comm-nopump") == 0) { + s_comm_nopump = 1; /* 道G task-9:通讯页 WAIT 态黄金专用(同款语义独立开关) */ + } } /* 截图/自检模式:--screenshot / --keys / --standby / --cfgtest / --logtest */ @@ -1228,6 +1313,9 @@ int main(int argc, char **argv) { if (ui_data_yt_bind(&s_yt_ops) != 0) { printf("[sim] UI yt bind: FAIL\n"); } + if (ui_data_comm_bind(&s_comm_ops) != 0) { /* 道G task-9 通讯参数假表(两处同文 replace_all) */ + printf("[sim] UI comm bind: FAIL\n"); + } /* task-14 数据快照:镜像 ui_task.c 的 ui_data_init + 首帧发布(先于建页, * 页面任何时刻打开都拿得到帧,不必等装配节拍) */ sim_publish_data_frames(); @@ -1277,6 +1365,7 @@ int main(int argc, char **argv) { * 出队 → prot_set 落值 → 重发参数帧);队列空时只是一次计数判断 */ sim_param_pump(); sim_yt_pump(); /* 道F task-7:窗口模式同样每轮无条件清脏(真机 50 ms 落盘的镜像) */ + sim_comm_pump(); /* 道G task-9:通讯页脏位同款镜像 */ /* 配置文件轮询(2s 周期:外部文件变更 4s 沉降后自动重载) */ uint32_t now_ms = (uint32_t)SDL_GetTicks(); diff --git a/simulator/tests/check_content_pages.py b/simulator/tests/check_content_pages.py index cb885c51..3b7a656f 100644 --- a/simulator/tests/check_content_pages.py +++ b/simulator/tests/check_content_pages.py @@ -121,6 +121,16 @@ PAGES = [ # 拍在首屏的图目检后改出来的键序);不按保存是因为按键会重新评估、ERR 会被 # 新一轮 WAIT/OK 覆盖——ERR 的像素证据只有这条窗口是真的 ("calib_reject", "ddd e de uruuruuue e ddd l ddddd l d e dddddddd"), + # ── 道G task-9:通讯设置页(comm,#P 叶;[comm] 工厂默认 CA=1 / 端口 2404)── + # 导航:ddddd 落到"通讯参数"(主界面第 6 个节点)→ e 进(首叶通讯设置被选中) + # → e 进密码页;123 = "uruuruuue" 可编辑,000(直接 e)只读。共 3 行不分页, + # 保存行恒在第 3 槽(y 带 52..66)。 + ("comm", "ddddd ee uruuruuue"), # 首屏:1 / 2404 / 保存行 ENTER + ("comm_ro", "ddddd ee e"), # 000 只读:无保存行 + ("comm_edit", "ddddd ee uruuruuue e"), # CA 行进编辑(个位光标反显) + ("comm_reject", "ddddd ee uruuruuue e d e"), # CA 编到 0(< min=1)→ ENTER 拒退 + ("comm_wait", "ddddd ee uruuruuue e u e dd e"), # CA 1→2 提交 → 下移到保存行 ENTER:WAIT(+--comm-nopump) + ("comm_ok", "ddddd ee uruuruuue e u e dd e"), # 同键序、pump 正常 → OK(脏位真被清) ] # 文本级断言表:页名 -> [(名称, 期望串, y 带, 是否反显)] @@ -538,6 +548,48 @@ TEXTS = { ("r3名", "MAC地址", (66, 80), False), ("r4名", "保存", (80, 95), True), ("r4值", "OK", (80, 95), True), ], + # ── 道G task-9:通讯设置页文本级断言(值 = [comm] 工厂默认的执行侧镜像)── + # 编辑/保存行的反显标记跟"选中在哪一行"走:初始与只读选中 r0,按保存后选中 + # 停在保存行——inv 口径与 ui_w_setlist 视觉一致,别照抄 netcfg 的行号 + "comm": [ + ("标题", "通讯设置", (0, 24), False), + ("r0名", "公共地址", (24, 38), True), ("r0值", "1", (24, 38), True), + ("r1名", "监听端口", (38, 52), False), ("r1值", "2404", (38, 52), False), + ("r2名", "保存", (52, 66), False), ("r2值", "ENTER", (52, 66), False), + ("页脚", "第 1 页,共 1 页", (140, 160), False), + ], + "comm_ro": [ + ("标题", "通讯设置", (0, 24), False), + ("r0名", "公共地址", (24, 38), True), ("r0值", "1", (24, 38), True), + ("r1名", "监听端口", (38, 52), False), ("r1值", "2404", (38, 52), False), + ("页脚", "第 1 页,共 1 页", (140, 160), False), + ], + "comm_edit": [ + ("标题", "通讯设置", (0, 24), False), + ("r0名", "公共地址", (24, 38), True), # 编辑中值列 = 裸数字串(本页无单位可挂) + ("r2名", "保存", (52, 66), False), + ], + "comm_reject": [ + # 实测帧(2026-10-27 目检 shot_comm_reject):拒退**留在编辑态**、 + # r0 值挂"越界"提示并反显(sparam_oob 同款),保存行仍是 ENTER—— + # 本行没按过保存,粘滞 ERR 不可达(comm 合法带编辑器=执行侧同带, + # 执行侧拒只有宿主 034 那条腿能进,屏幕不假拍,calib_reject 同理) + ("标题", "通讯设置", (0, 24), False), + ("r0名", "公共地址", (24, 38), True), + ("越界提示", "越界", (24, 38), True), + ("r2名", "保存", (52, 66), False), ("r2值", "ENTER", (52, 66), False), + ("页脚", "第 1 页,共 1 页", (140, 160), False), + ], + "comm_wait": [ + ("标题", "通讯设置", (0, 24), False), + ("r0名", "公共地址", (24, 38), False), ("r0值", "2", (24, 38), False), # CA 副本已 1→2(已受理;选中不在 r0,反显=False) + ("保存名", "保存", (52, 66), True), ("保存值", "WAIT", (52, 66), True), + ], + "comm_ok": [ + ("标题", "通讯设置", (0, 24), False), + ("r0名", "公共地址", (24, 38), False), ("r0值", "2", (24, 38), False), + ("保存名", "保存", (52, 66), True), ("保存值", "OK", (52, 66), True), # 脏位被 pump 清 = 已落盘 + ], } # 反向文本断言表:页名 -> [(名称, 期望**不出现**的串, y 带, 是否反显态)] @@ -638,6 +690,8 @@ EXTRA = { # OK 态同键序不加旗标,两图唯一差别就是"脏有没有被清"——对照即凭证 "deadband_wait": ["--yt-nopump"], "calib_wait": ["--yt-nopump"], + # 道G task-9:通讯页 WAIT 态 = 落盘 pump 停转(同款裁定,独立开关) + "comm_wait": ["--comm-nopump"], } diff --git a/simulator/tests/ref/shot_comm.bin b/simulator/tests/ref/shot_comm.bin new file mode 100644 index 0000000000000000000000000000000000000000..7f85a5ade4254fb916d21ed3832bc6a82297e0a5 GIT binary patch literal 3200 zcmeH}&ubGw6vyApE~dg_HxWUh*fm5vHl8cpWeIl8K}*1M3jP7@ErN*CC1AXzF(O_p zisIFS5kwl(_97w(z1PHB4`K=)3}%ycer=N7q9}@|eT1F&KJ%S0RGxG%CLCb z3vZKidztc+FjK%$qq^CaqY_K%5aG`5WoLh7D42#?+$nDN46p>d0D={4XKtq_*eWb@ zr?~B8>?y%IkmU~5rCq`3ZYKrX$)Cp#cM|{_cPxA{?Fx1b;0R}LI<_WjnE{~Euw4gG z{D^g^B!YYrUiKd&Gg^x8d*LI%zB05mqx0#zyCmg^FAAT@2v$$6wckp1F1^Zi4^kdX2?l4=OPuMo z^`g|lrF2!KORd|1?v7;sScj$`ivsEotM?q40|j|&)vK~T<&&(hPSyjR*dGA-Te{u~ zqCXrBba=ts+K4Hin|;C}XJgcBtW{i#GnYNNjo62>cppk}zEZR3h-_8oBKlC?RlX7y z6yFO-z^ptat0ehW~0RGxG%CLCb z3vZKidztc+FjK%$qq^CaqY_K%5aG`5WoLh7D42#?+$nDN46p>d0D={4XKtq_*eWb@ zr?~B8>?y%IkmU~5rCq`3ZYKrX$)Cp#cM|{_cPxA{?Fx1b;0R}LI<_WjnE{~Euw4gG z{D^g^B!YYrUiKd&Gg^x8d*LI%zB05mqx0#zyCmg^FAAT@2v$$6wckp1F1^Zi4^kdX2?l4=OPuMo z^`g|lrF2!KORd|1?v7;sScj$`ivsEotM?q40|j|&)vK~T<&&(hPSyjR*dGA-Te{u~ zqCXrBba=ts+K4Hin|;C}XJgcBtW{i#GnYNNjo62>cppk}zEZR3h-_8oBKlC?RlX7y z6yFO-z^ptat0ehW~c)ovL2W`V4hJy#%4Q;Y`b)tI6O=Mqz+Y$TceX%j4@Gj0LDr?=n-A$^XdMDVvTn~wn%2bUwe##^r{v8 zs_^84Q>d+%By&**n)6CKIG-XA7yuzV2mWKsjDCKH|5Zj5gJ zI?BQ^5;q#hdyPxD%aqOx<+mT~x>*Nz6q(H)t_pI(HPCe_x?U0S;c<5QLSHge8a8t5dp*Q>_n7NjaaQG3~xz=DWWYT+jrn`@| zzC~+%#Y;!GSqq#c)^sE;8FDTo15q0nojRbD=@9Vv;xx# z{C^ct*y}hw0)-s{EPP3`ckP}F?93Ce@JK_J*=f`bSbO}4K@%RZPa*e1lt%G^YFgkG zC>xJ0v!8LVXa-(JdaOgWqRG-3W4_B+U_=a-TnaDsU@U2iC7|YlngYUpPRbNaJ;FK! OSUO;eQ;f9#+Vv;=hJg71 literal 0 HcmV?d00001 diff --git a/simulator/tests/ref/shot_comm_reject.bin b/simulator/tests/ref/shot_comm_reject.bin new file mode 100644 index 0000000000000000000000000000000000000000..1a4cbb12c3463e277ec89b7f0fb4552fd54eaa20 GIT binary patch literal 3200 zcmeH}&ubGw6vyBGz*Jc5CMqZtyM~Cz#*>F?vMj-_IcO<(PQgE*y+u%Q8Un^!5+mY4 zgTL@v!~~>do3qo%cTToq2Cx7G?nc+Nyb& zyWjV3lgf2t`B#1>fhmvcrkkdWbgmN+b+)gWJ4+5@iqND^X0xk-&e$bjY{4*Rwz`Zh z!yR??o#~9x26k}WI3!+Kg5P(7*UAz*_F?I~#2xaSSL*cbd0br@$ zZUIQ{n6_0}y@lss3pZqeg8@&#nu~tK4%t=~%SdGhKbq3O~Q;F}7d!uKn86wN9+x!}}#V4ZG$s`|X`7$$J(=K0U zUzxBmrPAq#+bCkPFG@TcW2_Qcu|IHjKDtbGPa=JoU<}Sh=P6Umr3}}>)o78WiVW$HGrUg3Kq448({I5yv<= z%5k)iqijyr$P_WrW9hs`0e(n3gJIS}W!7*GAr F#^0+mY^MMK literal 0 HcmV?d00001 diff --git a/simulator/tests/ref/shot_comm_ro.bin b/simulator/tests/ref/shot_comm_ro.bin new file mode 100644 index 0000000000000000000000000000000000000000..a02ab9b86b0e696c150c97a6ead49bb51f6c031e GIT binary patch literal 3200 zcmeH}L2J}N6vy8r!R;Z~*?3VXwuTjt#&e|!%c9YPmV)OM`~dB31rf)kAl`PjE8;~_ z6t5m!L8LWpFP?j^i8oJfZ!R>u<7D21ZbtM{@suHuH^2GMdoM2o^8k)LUaZ5_M`_Vb zqdlsvzb-NpLQB`pN1??6r6WuldpE-Gs}p24iKNlo87)A7>@px*sfQPLN66OTwltbM zVXZ!cECEj%Lg%)T32?OwlRgwm`4*wP4$D{dRvNq`fQeNO7O%4H5fNQ>hxfXTIy z2#JO`D{lLbGha`I^VHuT$D4=4>6JgS!6ND=52QTxw+<@rKds{4oP8L7t-icdWUc%0 z8%?(~+>8er^M{E!Sk(OOb3QoK*w#3H`cY$W??>ycVi-O3dF$tBsqj;@qxqNOJ8;I( zmwRWE-c3AZWP6Uv<&8b&gm|Kib2Vgr=VAOsu?y~+)ID=jm_Y{T-DSyWuiI2QxaM{+ zoYSr6y2py~BORH2g$eYZcbRzx?-G8Rp@7_^dtc!K!@B_tfE zWo5d6iv2jMzcZ<8c$u!6>%@dw4Ns&q#W)izQyei^W)$8nWP6QBg@6tN(*{F)j_oOC So)QxSL^|N9E+$w1v-dB!OkFSl literal 0 HcmV?d00001 diff --git a/simulator/tests/ref/shot_comm_wait.bin b/simulator/tests/ref/shot_comm_wait.bin new file mode 100644 index 0000000000000000000000000000000000000000..8e444171e87804f9d758a2f2a9dd34b06d1dda12 GIT binary patch literal 3200 zcmeH}&1(}u6u{rkE+&UyCh?+B%yx-*bVa<0n=YYlBWNiU`~cIVH+vL{vJC;_Es60U zh!ydw=)n(=VAA&BQTh)g(u;Vo-V9`ub#~^>CRwQm6$Hr;lJ|c5+nJY_2|ECPJT}U3 z?v6J(O}^C2mY+^C6&x+ zYen>6!KjMm(luLpcaixcPf0!z1vDO2?>o4LBG;`~m$5$e6V}&oYtj?*11Nr*(w5Bj z|Foi8*Y`cEGmKKt`gl8JgR8qSmUz~imGIv%xct2%*sYPbjLcjMd_D%q&Nlmdl6j}m zdwi-R+3+Q@=9h4d;cH}X`y=l;vR7+kY&VEK>)E@Iu{;=_)$DKZX!Q1*n`i%?POnTm zFzvv!1Je%te;rWR<2X44g&hJc@sMWE+C3N8nI~Z3k%la>)2JJ;*7y;F#ynuJT;?0L zM)84aTHxgU_mUPa0aX{&1Q7Oh V5+*YB5K9QKa=^q>jI{sS=MNd9eQ5vy literal 0 HcmV?d00001 diff --git a/test/host/adc_cal/adc_cal_test.c b/test/host/adc_cal/adc_cal_test.c index dea17125..0f5db2cb 100644 --- a/test/host/adc_cal/adc_cal_test.c +++ b/test/host/adc_cal/adc_cal_test.c @@ -36,6 +36,14 @@ * 闭锁 / 两路齐第三路缺仍闭锁(全或无)/ 三路齐放行且 UB 仍无 VALID * (证明它是"三路"而不是 feed_eng 那个"8 路")/ 复位后立刻回闭锁 * (现查、无自有状态可残留) + * —— 以下四项是"改了没落盘"的可诊断化(道G task-43 第②项;四态**定义只在 + * App/algo/adc_cal.h 一处**,本表只描述断言、不重定义)—— + * TC-ADCCAL-019 观测件不得反过来制造故障:get_stats(NULL) 静默返回、随时可读 + * TC-ADCCAL-020 一次成功整定 → set_ok +1、水位不动 ⇒ 未落盘差额 = 1(改盘上没跟上,看得见) + * TC-ADCCAL-021 多次整定 + 一次落盘回执 → 差额归 0(水位是**盖写**不是自增:一次 save + * 覆盖整档,若自增会留下"假未落盘"——这条就是那个坑的回归锁) + * TC-ADCCAL-022 被拒整定(非法通道 / 越出合法带)只加 set_reject,不污染 set_ok 与差额 + * TC-ADCCAL-023 空转回执不倒挂:水位永不越过 set_ok(uint32 相减一绕回就是巨大的假差额) * * 通过:全部 TC PASS(main 返回 0)。 * 创建日期:2026-10-25(道A 卡 2/卡 4) @@ -524,6 +532,67 @@ static void tc_018_prot_blocked(void) { CHECK(adc_cal_prot_blocked() == 1u, "repeat call stable (idempotent pure read)"); } +/* ============ 观测计数(改了没落盘 → 可诊断,道G task-43 第②项) ============ */ +/* + * 四态定义只在 App/algo/adc_cal.h 一处;本用例**全部按增量断言**——静态计数跨用例 + * 累计,任何"绝对值等于几"的写法都会因为前面 TC 调过多少次 adc_cal_set() 而假失败 + * (也与 vol_lock 套件同一套写法先例)。 + */ +static void tc_019_023_persist_obs(void) { + adc_cal_stats_t s; + uint32_t ok0; + uint32_t rj0; + + printf("-- TC-ADCCAL-019..023 persist observability --\n"); + + /* TC-ADCCAL-019:观测件不得反过来制造故障 */ + adc_cal_get_stats(NULL); + CHECK(1, "get_stats(NULL) returns silently (an observer must not create a fault)"); + + /* 先立基线:把"到目前为止的改写"记为已落盘,后面全部按增量看 */ + adc_cal_note_persisted(); + adc_cal_get_stats(&s); + CHECK(s.set_ok == s.persisted_set_ok, "after a sync point water catches count (gap == 0)"); + + /* TC-ADCCAL-020:一次成功整定 → 差额 1,水位不动 */ + ok0 = s.set_ok; + rj0 = s.set_reject; + CHECK(adc_cal_set(ADC_SEM_UA, ADC_CAL_K_Q16_TPAFE * 2, 100L) == 0, "in-band set accepted"); + adc_cal_get_stats(&s); + CHECK(s.set_ok == ok0 + 1u, "set_ok incremented by exactly one"); + CHECK(s.set_reject == rj0, "set_reject untouched by a successful set"); + CHECK(s.persisted_set_ok == ok0, "water still at the old level (nothing saved yet)"); + CHECK(s.set_ok - s.persisted_set_ok == 1u, "gap == 1: RAM changed, disk not (the diagnosable fact)"); + + /* TC-ADCCAL-021:多次整定 + 一次落盘回执 → 差额归 0(水位是盖写不是自增) */ + CHECK(adc_cal_set(ADC_SEM_UN, ADC_CAL_K_Q16_TPAFE, -50L) == 0, "second channel set accepted"); + /* 合法带是 ROM 系数的 [0.5x, 2x](含端点,TC-ADCCAL-008 已钉)——这里必须留在带内, + * 否则第二次改写被拒,差额就不是我声称的 3 了(写用例时踩过一次,改成 2x 上界) */ + CHECK(adc_cal_set(ADC_SEM_IA, ADC_CAL_K_Q16_TPAFE * 2, 7L) == 0, "third channel set accepted (band upper end)"); + adc_cal_get_stats(&s); + CHECK(s.set_ok - s.persisted_set_ok == 3u, "three pending sets before the single save"); + adc_cal_note_persisted(); /* 一次 save 覆盖整档:等价 run_par_store_save() 成功路径 */ + adc_cal_get_stats(&s); + CHECK(s.set_ok - s.persisted_set_ok == 0u, "one save clears all three pending sets (water is a snapshot, not a tick)"); + + /* TC-ADCCAL-022:被拒的两类整定只加 set_reject,不污染差额 */ + ok0 = s.set_ok; + rj0 = s.set_reject; + CHECK(adc_cal_set(ADC_SEM_TEMP, ADC_CAL_K_Q16_TPAFE, 0L) == -1, "TEMP rejected"); + CHECK(adc_cal_set(ADC_SEM_UA, ADC_CAL_K_Q16_TPAFE * 100, 0L) == -1, "out-of-band k rejected (RAM untouched)"); + adc_cal_get_stats(&s); + CHECK(s.set_reject == rj0 + 2u, "set_reject counts both reject reasons"); + CHECK(s.set_ok == ok0, "rejected sets never inflate set_ok"); + CHECK(s.set_ok == s.persisted_set_ok, "gap stays zero across rejected attempts (no phantom pending write)"); + + /* TC-ADCCAL-023:空转回执不倒挂 */ + adc_cal_note_persisted(); + adc_cal_note_persisted(); + adc_cal_get_stats(&s); + CHECK(s.persisted_set_ok == s.set_ok, "idle acks keep water exactly at the level (never overrun)"); + CHECK(s.set_ok >= s.persisted_set_ok, "subtraction can never wrap (gap stays non-negative)"); +} + /* ===================== main ===================== */ int main(void) { printf("=== ADC Cal Layer Tests ===\n"); @@ -544,6 +613,7 @@ int main(void) { tc_012_file_corrupt(); tc_013_section_missing(); tc_018_prot_blocked(); + tc_019_023_persist_obs(); printf("\n=== ADC Cal Layer Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); diff --git a/test/host/boot_stats/boot_stats_test.c b/test/host/boot_stats/boot_stats_test.c new file mode 100644 index 00000000..e76923c6 --- /dev/null +++ b/test/host/boot_stats/boot_stats_test.c @@ -0,0 +1,374 @@ +/* + * 模块名称:开机账本(boot_stats)主机侧单元测试 + * 模块功能:在 PC 上离线验证 App/util/boot_stats.c 的**上电次数四态判定、记录校验、 + * I2C 失败分支(读失败绝不回写 / 写失败只少一笔账)、幂等、饱和**, + * 以及复位原因优先级链的唯一出处。 + * + * 被测对象:真实源文件 App/util/boot_stats.c(#include 原样纳入)。桩: + * - stub/main.h:六个 RCC_CSR 复位标志位掩码(位号照抄 CMSIS); + * - sd2506.h / bsp_err.h / boot_stats.h / app_main.h 用**真实头**; + * - sd2506_read_sram / sd2506_write_sram / sd2506_read_ctr1 由本文件实现 + * 为可注入错误的假件(含 70 B 假 SRAM,越界返 BSP_ERR_PARAM,与真驱动 + * 的契约一致);g_rcc_csr_latched 也由本文件给出。 + * + * 为什么这些判据必须在这儿测而不在真机上测(真机只覆盖"一切正常"那一条): + * 1. **读失败 ≠ 没有记录**:I2C 抖动一次就把计数当 0 重来,屏幕上是个很像样的小 + * 数字、日志里一个字都没有;读失败还顺手回写的话,故障就从"这次读不到"升级成 + * "永久少账"(004 用"假 SRAM 内容一字不许变"把这条钉住)。 + * 2. **首次上电 ≠ RTC 掉过电**:合并成一态,文案就把前者冒充后者(001 vs 003)。 + * 3. **只信 magic 不信校验**:坏区常是整片全 0/全 FF,但"个别字节翻转"时 magic + * 可能仍然好、次数错 256 倍(007)。 + * 4. **饱和 vs 回绕**:停在 42 亿一眼看出到头;回绕成个位数会被读成"刚上电"(009)。 + * 5. BLF 读不到不该把好记录判坏(012)——判定顺序写反的代价是"RTC 抖一下, + * 装置变成第一次上电"。 + * + * 编号:TC-BOOTSTAT-001 起(小写子项 = 同一判据的另一面)。族需已登记进 + * scripts/id_registry.json;未登记时 check_ids 校验 1 会报"未登记前缀"。 + * 创建日期:2026-10-27(道G task-12) + */ +#include +#include + +#include "boot_stats.h" +#include "sd2506.h" /* 真实头:bsp_err_t / SD2506_CTR1_BLF / 三个原型 */ +#include "app_main.h" /* extern volatile uint32_t g_rcc_csr_latched */ + +/* 被测生产源(照仓内既有套件:include_dirs 里有 App/util) */ +#include "boot_stats.c" + +/* ---------------- g_rcc_csr_latched(真实定义在未编译的 app_main.c) ---------------- */ +volatile uint32_t g_rcc_csr_latched; + +/* ---------------- SD2506 假件 ---------------- */ +#define FAKE_SRAM_SIZE 70u + +static uint8_t s_sram[FAKE_SRAM_SIZE]; +static uint8_t s_ctr1_val; +static int s_read_err; /* != BSP_OK ⇒ 注入读失败 */ +static int s_write_err; /* != BSP_OK ⇒ 注入写失败 */ +static int s_ctr1_err; +static int s_read_calls; +static int s_write_calls; +static int s_ctr1_calls; + +static void fake_reset(void) +{ + unsigned int i; + + for (i = 0u; i < FAKE_SRAM_SIZE; i++) { + s_sram[i] = 0xFFu; /* 芯片出厂/掉电后的常见形态:成片全 1 */ + } + s_ctr1_val = 0u; + s_read_err = BSP_OK; + s_write_err = BSP_OK; + s_ctr1_err = BSP_OK; + s_read_calls = 0; + s_write_calls = 0; + s_ctr1_calls = 0; + g_rcc_csr_latched = 0u; + /* 被测模块的静态状态也要归零——它在真实机器上"每 boot 一次", + * 测试里靠这条模拟"又开了一次机"(复位 SRAM 侧但不清假 SRAM = 跨掉电保持)。 */ + (void)memset(&s_bs, 0, sizeof(s_bs)); +} + +/* 造一条合法记录(校验按生产同一算法算,别把算法抄第二份在这里) */ +static void fake_put_count(uint32_t cnt, uint8_t ver) +{ + uint8_t i; + uint8_t sum = 0u; + + s_sram[0] = 0xB5u; + s_sram[1] = 0x72u; + s_sram[2] = (uint8_t)(cnt & 0xFFu); + s_sram[3] = (uint8_t)((cnt >> 8) & 0xFFu); + s_sram[4] = (uint8_t)((cnt >> 16) & 0xFFu); + s_sram[5] = (uint8_t)((cnt >> 24) & 0xFFu); + s_sram[6] = ver; + for (i = 0u; i < BS_REC_LEN - 1u; i++) { + sum = (uint8_t)(sum ^ s_sram[i]); + } + s_sram[7] = sum; +} + +static uint32_t fake_get_count(void) +{ + return ((uint32_t)s_sram[2]) | ((uint32_t)s_sram[3] << 8) | + ((uint32_t)s_sram[4] << 16) | ((uint32_t)s_sram[5] << 24); +} + +bsp_err_t sd2506_read_sram(uint8_t addr, uint8_t *p_buf, uint8_t len) +{ + uint8_t i; + + s_read_calls++; + if (s_read_err != BSP_OK) { + return (bsp_err_t)s_read_err; + } + if ((p_buf == 0) || (len == 0u) || (len > FAKE_SRAM_SIZE) || + ((uint16_t)addr + (uint16_t)len > FAKE_SRAM_SIZE)) { + return BSP_ERR_PARAM; + } + for (i = 0u; i < len; i++) { + p_buf[i] = s_sram[addr + i]; + } + return BSP_OK; +} + +bsp_err_t sd2506_write_sram(uint8_t addr, const uint8_t *p_buf, uint8_t len) +{ + uint8_t i; + + s_write_calls++; + if (s_write_err != BSP_OK) { + return (bsp_err_t)s_write_err; + } + if ((p_buf == 0) || (len == 0u) || (len > FAKE_SRAM_SIZE) || + ((uint16_t)addr + (uint16_t)len > FAKE_SRAM_SIZE)) { + return BSP_ERR_PARAM; + } + for (i = 0u; i < len; i++) { + s_sram[addr + i] = p_buf[i]; + } + return BSP_OK; +} + +bsp_err_t sd2506_read_ctr1(uint8_t *p_val) +{ + s_ctr1_calls++; + if (s_ctr1_err != BSP_OK) { + return (bsp_err_t)s_ctr1_err; + } + if (p_val == 0) { + return BSP_ERR_PARAM; + } + *p_val = s_ctr1_val; + return BSP_OK; +} + +/* sd2506_init 不测(属 SD2506 自己的套件),给个链接期够用的定义 */ +bsp_err_t sd2506_init(void) { return BSP_OK; } + +/* ---------------- 断言骨架 ---------------- */ +static int s_total; +static int s_passed; +static int s_failed; + +#define CHECK(cond, name) \ + do { \ + s_total++; \ + if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \ + else { s_failed++; printf(" [FAIL] %s\n", name); } \ + } while (0) + +static boot_stats_t snap(void) +{ + boot_stats_t b; + + boot_stats_get(&b); + return b; +} + +/* ===================== 上电次数四态 ===================== */ +static void tc_seq_states(void) +{ + boot_stats_t b; + uint8_t before[BS_REC_LEN]; + + /* 001 首次启用:SRAM 成片全 1(magic 过不去)+ RTC 没报欠压 ⇒ FRESH,不是 RESTARTED */ + fake_reset(); + boot_stats_init(); + b = snap(); + CHECK((b.ready == 1u) && (b.seq_state == BOOT_STATS_SEQ_FRESH) && (b.boot_count == 1u) + && (b.write_failed == 0u) && (s_write_calls == 1) && (fake_get_count() == 1u), + "TC-BOOTSTAT-001 blank SRAM without BLF starts the ledger at 1 and marks it FRESH, not RESTARTED"); + + /* 002 延续:合法记录 count=7 ⇒ count=8,且回写后介质里也是 8 */ + fake_reset(); + fake_put_count(7u, BS_REC_VER); + boot_stats_init(); + b = snap(); + CHECK((b.seq_state == BOOT_STATS_SEQ_CONTINUED) && (b.boot_count == 8u) + && (fake_get_count() == 8u), + "TC-BOOTSTAT-002 a valid record continues the count and persists the incremented value"); + + /* 002b 跨掉电保持的本体:假 SRAM 不清、只重开被测模块(等价于"又上电一次") */ + (void)memset(&s_bs, 0, sizeof(s_bs)); + boot_stats_init(); + b = snap(); + CHECK((b.seq_state == BOOT_STATS_SEQ_CONTINUED) && (b.boot_count == 9u), + "TC-BOOTSTAT-002b re-initialising over an intact medium keeps counting up rather than restarting at 1"); + + /* 003 电池域掉过电:记录无效 + BLF 置位 ⇒ RESTARTED(与 001 是两件事) */ + fake_reset(); + s_ctr1_val = SD2506_CTR1_BLF; + boot_stats_init(); + b = snap(); + CHECK((b.seq_state == BOOT_STATS_SEQ_RESTARTED) && (b.boot_count == 1u) && (b.rtc_blf == 1u), + "TC-BOOTSTAT-003 an invalid record with the battery-low flag set is reported as RESTARTED, never as first power-on"); + + /* 004 I2C 读失败 ⇒ 不可信,且**绝不回写**(把可能完好的记录覆盖掉是不可接受的) */ + fake_reset(); + fake_put_count(41u, BS_REC_VER); + (void)memcpy(before, s_sram, BS_REC_LEN); + s_read_err = BSP_ERR_TIMEOUT; + boot_stats_init(); + b = snap(); + CHECK((b.seq_state == BOOT_STATS_SEQ_UNKNOWN) && (b.boot_count == 0u) + && (s_write_calls == 0) && (memcmp(s_sram, before, BS_REC_LEN) == 0), + "TC-BOOTSTAT-004 an I2C read failure marks the ledger untrusted and writes nothing back, leaving the stored record byte-identical"); + + /* 004b 读不到时页面要能分辨"不可信"与"零次":同一张表里 ready=1 且 seq=UNKNOWN */ + CHECK((b.ready == 1u) && (b.seq_state == BOOT_STATS_SEQ_UNKNOWN), + "TC-BOOTSTAT-004b the untrusted state is distinguishable from both a real count and an uninitialised table"); + + /* 005 写失败 ⇒ 本次仍报数,但打上"少一笔账"的记号(下次开机读旧值,只会少不会重) */ + fake_reset(); + s_write_err = BSP_ERR_IO; + boot_stats_init(); + b = snap(); + CHECK((b.boot_count == 1u) && (b.write_failed == 1u) && (s_write_calls == 1), + "TC-BOOTSTAT-005 a failed write-back still reports this boot but flags it, so the ledger can only lose one count"); + + /* 006 幂等:bring-up 重试不得重复计数(真实机器上 sd2506 之后可能重进一次) */ + fake_reset(); + boot_stats_init(); + boot_stats_init(); + b = snap(); + CHECK((b.boot_count == 1u) && (s_write_calls == 1), + "TC-BOOTSTAT-006 a second init in the same boot neither counts twice nor writes twice"); + + /* 007 校验和不符(magic 与版本都好,中段漂了一位)⇒ 不当真账读 */ + fake_reset(); + fake_put_count(9u, BS_REC_VER); + s_sram[4] = (uint8_t)(s_sram[4] ^ 0x01u); /* 高位字节翻转:256 倍的错账 */ + boot_stats_init(); + b = snap(); + CHECK((b.seq_state == BOOT_STATS_SEQ_FRESH) && (b.boot_count == 1u), + "TC-BOOTSTAT-007 a single flipped byte that leaves the magic intact is caught by the checksum instead of reporting a count off by 256"); + + /* 008 格式版本不符(将来改了布局)⇒ 整条作废,且回写时落回当前版本 */ + fake_reset(); + fake_put_count(1234u, 2u); + boot_stats_init(); + b = snap(); + CHECK((b.seq_state == BOOT_STATS_SEQ_FRESH) && (b.boot_count == 1u) + && (s_sram[6] == BS_REC_VER) && (s_sram[7] == s_rec_sum(s_sram)), + "TC-BOOTSTAT-008 a record from a future layout is rejected outright and rewritten under the current version with a fresh checksum"); + + /* 009 饱和:到顶不再自增也不再写(回绕成个位数会被读成"刚上电") */ + fake_reset(); + fake_put_count(0xFFFFFFF0u, BS_REC_VER); + boot_stats_init(); + b = snap(); + CHECK((b.boot_count == 0xFFFFFFF0u) && (s_write_calls == 0) && (fake_get_count() == 0xFFFFFFF0u), + "TC-BOOTSTAT-009 the counter saturates at the ceiling and stops writing instead of wrapping to a small number"); + + /* 010 BLF 读不到但记录有效 ⇒ 仍按延续判(读不到佐证不该把好账判坏) */ + fake_reset(); + fake_put_count(20u, BS_REC_VER); + s_ctr1_err = BSP_ERR_IO; + boot_stats_init(); + b = snap(); + CHECK((b.seq_state == BOOT_STATS_SEQ_CONTINUED) && (b.boot_count == 21u) && (b.rtc_blf == 0u), + "TC-BOOTSTAT-010 an unreadable battery flag never invalidates an otherwise sound record"); + + /* 011 出厂全 0 的 SRAM(另一形态:不是全 FF)⇒ FRESH 且第一格就是 1,不报 0 次 */ + fake_reset(); + (void)memset(s_sram, 0, sizeof(s_sram)); + boot_stats_init(); + b = snap(); + CHECK((b.seq_state == BOOT_STATS_SEQ_FRESH) && (b.boot_count == 1u), + "TC-BOOTSTAT-011 an all-zero SRAM (the other erase pattern) starts the ledger at one rather than showing zero boots"); +} + +/* ===================== 复位原因链 ===================== */ +static uint8_t is_ascii(const char *p) +{ + while (*p != '\0') { + if (((unsigned char)*p) > 127u) { + return 0u; + } + p++; + } + return 1u; +} + +static void tc_cause_chain(void) +{ + /* 020 优先级唯一出处:IWDG 压过软件复位(崩溃钩子常以 NVIC_SystemReset 收尾, + * 两个标志同时置位时报看门狗才是我们要看的自动恢复事件) */ + CHECK(boot_stats_cause_from_csr(RCC_CSR_IWDGRSTF | RCC_CSR_SFTRSTF) == BOOT_STATS_CAUSE_IWDG, + "TC-BOOTSTAT-020 the independent watchdog outranks a software reset when both flags are set"); + + /* 020b WWDG 压过软件复位;单独的软件复位仍归软件 */ + CHECK((boot_stats_cause_from_csr(RCC_CSR_WWDGRSTF | RCC_CSR_SFTRSTF) == BOOT_STATS_CAUSE_WWDG) + && (boot_stats_cause_from_csr(RCC_CSR_SFTRSTF) == BOOT_STATS_CAUSE_SFT), + "TC-BOOTSTAT-020b the window watchdog likewise outranks a software reset while a lone software reset stays itself"); + + /* 020c POR 压过 PIN、PIN 压过 BOR;全零 ⇒ UNKNOWN */ + CHECK((boot_stats_cause_from_csr(RCC_CSR_PORRSTF | RCC_CSR_PINRSTF) == BOOT_STATS_CAUSE_POR) + && (boot_stats_cause_from_csr(RCC_CSR_PINRSTF | RCC_CSR_BORRSTF) == BOOT_STATS_CAUSE_PIN) + && (boot_stats_cause_from_csr(RCC_CSR_BORRSTF) == BOOT_STATS_CAUSE_BOR) + && (boot_stats_cause_from_csr(0u) == BOOT_STATS_CAUSE_UNKNOWN), + "TC-BOOTSTAT-020c power-on beats pin which beats brown-out, and no flag at all stays UNKNOWN rather than guessing"); + + /* 020d 每一格编码都有可读串、全是纯 ASCII(无 BOM 文件里字符串必须 ASCII), + * 且未知编码回落 unknown 而不是断言——这条链跑在开机极早期,不能挡启动 */ + CHECK((boot_stats_cause_str(BOOT_STATS_CAUSE_UNKNOWN) != 0) + && (boot_stats_cause_str(BOOT_STATS_CAUSE_BOR) != 0) + && (strcmp(boot_stats_cause_str(BOOT_STATS_CAUSE_IWDG), "IWDG (independent watchdog)") == 0) + && (strcmp(boot_stats_cause_str(200u), "unknown") == 0) + && is_ascii(boot_stats_cause_str(BOOT_STATS_CAUSE_SFT)), + "TC-BOOTSTAT-020d every code maps to a pure-ASCII description and an out-of-range code falls back to unknown instead of blocking startup"); + + /* 020e 快照:开机那次读的就是锁存影子里的复位原因(与串口那条同源同序) */ + fake_reset(); + g_rcc_csr_latched = RCC_CSR_IWDGRSTF; + boot_stats_init(); + CHECK(snap().rst_cause == BOOT_STATS_CAUSE_IWDG, + "TC-BOOTSTAT-020e the boot snapshot exposes the latched RCC_CSR cause so the screen and the serial line can never disagree"); +} + +/* ===================== 读口契约 ===================== */ +static void tc_get_contract(void) +{ + /* 030 未 init:整表零、ready=0(页面该显示占位而不是"0 次") */ + fake_reset(); + CHECK((snap().ready == 0u) && (snap().boot_count == 0u), + "TC-BOOTSTAT-030 before init the table reads zero with ready clear so a page can show a placeholder instead of zero boots"); + + /* 030b NULL 无害(观测口在任何上下文都不该因为调用者手滑而炸) */ + boot_stats_get(0); + CHECK(1, "TC-BOOTSTAT-030b a null output pointer is a harmless no-op"); + + /* 030c 读口不碰总线:init 之后再读 100 次,I2C 调用数纹丝不动 + * ——「任意上下文可读」不是口头承诺,是这条计数断言 */ + fake_reset(); + boot_stats_init(); + s_read_calls = 0; + s_write_calls = 0; + s_ctr1_calls = 0; + { + int i; + for (i = 0; i < 100; i++) { + (void)snap(); + } + } + CHECK((s_read_calls == 0) && (s_write_calls == 0) && (s_ctr1_calls == 0), + "TC-BOOTSTAT-030c the getter touches neither I2C nor the clock, so reading the ledger cannot become part of a fault"); +} + +int main(void) +{ + printf("=== Boot Stats Tests ===\n"); + printf("-- TC-BOOTSTAT-001..011 power-on ledger (four states, checksum, I2C failures, idempotence, saturation) --\n"); + tc_seq_states(); + printf("-- TC-BOOTSTAT-020..020e reset cause chain (single source of the priority order) --\n"); + tc_cause_chain(); + printf("-- TC-BOOTSTAT-030..030c read-only contract --\n"); + tc_get_contract(); + /* 分子必须是"真过了几条":写成 s_total/s_total 会出现 + * 「21/21 PASSED (failed=1)」这种自相矛盾的汇总行——人只看前半就放行了。 */ + printf("\n=== Boot Stats Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +} diff --git a/test/host/boot_stats/run_unit_test.py b/test/host/boot_stats/run_unit_test.py new file mode 100644 index 00000000..30f17dc1 --- /dev/null +++ b/test/host/boot_stats/run_unit_test.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""开机账本(boot_stats)的主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/boot_stats/boot_stats_test.c(以 #include 纳入**生产源** + App/util/boot_stats.c),在 PC 上离线验证上电次数的**四态判定**(不可信 / 首次启用 / + 延续 / 电池域掉过电)、记录校验与格式版本、I2C 读失败时**绝不回写**、写失败时 + "少一笔账"的记号、幂等、饱和,以及复位原因优先级链的唯一出处。 + 工具链与判定委托 test/host/common/unit_test_harness.py。 + +为什么要主机侧单测(而不是只靠真机): + 这一格数字的唯一用途是告诉人"这台装置上了多少次电",而它错的时候**永远不像错**: + 1. I2C 读失败若当成"没有记录",计数会在某次通信抖动后悄悄从 1 重来——屏幕上 + 是个很像样的小数字,没有任何一行日志会说它假; + 2. 读失败若还顺手回写,就把可能完好的旧记录**覆盖掉**,故障从"读不到"升级成 + "永久少账"; + 3. "第一次上电"与"RTC 掉过电"合并成一个状态,就等于拿前者冒充后者(文案会写错 + 整整一格的口径); + 4. 只信 magic 不信校验:电池坏区常是整片全 0 / 全 FF(magic 那关自然过不去), + 但更常见的"个别字节翻转"里 magic 可能仍然好,次数会错 256 倍。 + 这些都要能把 SRAM 内容、BLF、I2C 错误码逐个构造出来才测得到;真机上跑一次 + 只覆盖"一切正常"那一条。桩边界见下面的注释;上板另有一组判据(断电 ≥5 min 再 + 上电 +1、换电池后落 RESTARTED),见 task-12 卡片,主机测不出来。 + +桩的边界: + - stub/main.h:只给 RCC_CSR 六个复位标志位掩码(位号照抄 CMSIS,见该文件注释); + 真实 main.h 拉整棵 HAL 头树,PC 上编不动。 + - sd2506.h / bsp_err.h / boot_stats.h / app_main.h 一律用**真实头**(它们不依赖 + HAL 类型);被测源用到的三个 SD2506 函数 + sd2506_read_sram / sd2506_write_sram / sd2506_read_ctr1 由本测试单元实现为 + 可注入失败的假件(含一片 70 B 假 SRAM,地址越界按真驱动语义返 BSP_ERR_PARAM)。 + - g_rcc_csr_latched 的真实定义在 App/app/app_main.c(本套件不编译它), + 由本测试单元给出。 + +用法(仓库根目录): + python test/host/boot_stats/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误。 + +设计依据:App/util/boot_stats.h、docs/任务规划/UI补齐.md §2(运行报告四个数的口径) +创建日期:2026-10-27(道G task-12) +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 顺序敏感:stub 必须排在最前,才能用桩 main.h 覆盖真实 main.h。 +BUILD = TestBuild( + name="Boot Stats Tests", + src=os.path.join(HERE, "boot_stats_test.c"), + include_dirs=[ + os.path.join(HERE, "stub"), + os.path.join(ROOT, "App", "util"), + os.path.join(ROOT, "App", "app"), + os.path.join(ROOT, "Drivers", "BSP"), + os.path.join(ROOT, "Drivers", "BSP", "SD2506"), + HERE, + ], + summary_marker="=== Boot Stats Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/boot_stats/stub/main.h b/test/host/boot_stats/stub/main.h new file mode 100644 index 00000000..74eadb70 --- /dev/null +++ b/test/host/boot_stats/stub/main.h @@ -0,0 +1,34 @@ +/* + * 模块名称:main.h 桩(boot_stats 主机单测专用) + * 模块功能:只提供 App/util/boot_stats.c 真正用到的 **RCC_CSR 复位标志位掩码**, + * 让被测源在 PC 上编得动(真实 main.h 会拉整棵 HAL/CMSIS 头树)。 + * 创建日期:2026-10-27(道G task-12) + * + * ⚠ 位号与宏名必须与 CMSIS 一致(本仓真值取自 + * Drivers/CMSIS/Device/ST/STM32F4xx/Include/stm32f407xx.h 的 RCC_CSR_xxx_Pos): + * BOR=25 / PIN=26 / POR=27 / SFT=28 / IWDG=29 / WWDG=30。 + * 改了被测代码而这里缺宏 → 编译直接失败,不会出现"桩悄悄放过"的假绿; + * 改了位号(不会发生,芯片固定)则断言会红——优先级链是本项目唯一出处, + * 见 App/util/boot_stats.h 的 boot_stats_cause_from_csr 函数头。 + */ +#ifndef MAIN_H +#define MAIN_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +#define RCC_CSR_BORRSTF ((uint32_t)0x02000000u) /* bit25 掉电复位 */ +#define RCC_CSR_PINRSTF ((uint32_t)0x04000000u) /* bit26 引脚复位 */ +#define RCC_CSR_PORRSTF ((uint32_t)0x08000000u) /* bit27 上电复位 */ +#define RCC_CSR_SFTRSTF ((uint32_t)0x10000000u) /* bit28 软件复位 */ +#define RCC_CSR_IWDGRSTF ((uint32_t)0x20000000u) /* bit29 独立看门狗 */ +#define RCC_CSR_WWDGRSTF ((uint32_t)0x40000000u) /* bit30 窗口看门狗 */ + +#ifdef __cplusplus +} +#endif + +#endif /* MAIN_H */ diff --git a/test/host/iec104/iec104_test.c b/test/host/iec104/iec104_test.c index 99cf603d..f31d7554 100644 --- a/test/host/iec104/iec104_test.c +++ b/test/host/iec104/iec104_test.c @@ -38,6 +38,8 @@ * setter 守卫(0 拒 / 1023 拒 / 65535 收 / 带内提交) * TC-IEC104-033 ca 端到端:ca=comm 值时 C_RD 正答 COT5 且 TX 头回显同值; * 旧写死值 ca=1 变错地址 → COT13+EVI(证明过滤真源已迁移) + * TC-IEC104-034 [comm] 落盘脏位:置脏/清脏/空转/带外拒不置脏(道G task-9, + * 通讯设置页保存行 WAIT/OK 的判据源) * TC-IEC104-014 定值两段写(真实 prot_set):越界 PN;执行后 get_f + selfcheck * TC-IEC104-015 C_RD:YC 命中 / 定值命中 / 未知 IOA COT14+EVI * TC-IEC104-016 C_CS:clock_sync 收正确 epoch;IV → PN;clock_sync 失败 → PN @@ -1553,6 +1555,26 @@ static void tc_comm(void) "TC-IEC104-033 the retired hardwired ca=1 now answers COT13+EVI"); (void)iec104_comm_ca_set(IEC104_COMM_DEF_CA); /* 收尾:复位出厂 ca */ (void)iec104_comm_port_set(IEC104_COMM_DEF_PORT); + + /* ===== TC-IEC104-034 [comm] 落盘脏位与 store_save(道G task-9,页面链前置)===== + * 契约 = iec104_comm.h 的两个新出口:setter 成功置脏 / store_save 成功清脏 / + * 无脏空转返 0 / 带外拒不置脏。defaultTask 每轮无条件调 store_save(挂载点 + * 在 App/task/default_task.c,run_par_store_save 同点位),屏上 WAIT/OK 全看 + * 这枚脏位——脏位错 = 通讯页保存行说谎,这是本条用例钉的东西。 + * 编号 034 = 向 Lead 申请发号(cursor_owner_only),cursor 现 033。 */ + printf("[TC-IEC104-034] comm dirty bit drives the store_save cycle\n"); + CHECK(iec104_comm_dirty_peek() == 1u, + "TC-IEC104-034 successful setters leave the section dirty"); + CHECK((iec104_comm_store_save() == 0) && (iec104_comm_dirty_peek() == 0u), + "TC-IEC104-034 store_save clears dirty only on write success"); + CHECK((strstr(comm_vfs_head(rp_s_vfs_len), "ca=1") != NULL) && + (strstr(comm_vfs_head(rp_s_vfs_len), "port=2404") != NULL), + "TC-IEC104-034 the saved file carries the committed values"); + CHECK((iec104_comm_store_save() == 0) && (iec104_comm_dirty_peek() == 0u), + "TC-IEC104-034 a save with no dirty is a silent no-op (poll cost)"); + CHECK((iec104_comm_ca_set(0u) == -1) && (iec104_comm_dirty_peek() == 0u) && + (iec104_comm_ca() == IEC104_COMM_DEF_CA), + "TC-IEC104-034 out-of-band reject neither dirties nor moves the copy"); } diff --git a/test/host/ui_data/ui_data_test.c b/test/host/ui_data/ui_data_test.c index b81dee19..ba4a62a4 100644 --- a/test/host/ui_data/ui_data_test.c +++ b/test/host/ui_data/ui_data_test.c @@ -76,6 +76,8 @@ * TC-UIDATA-026 注入后转发言行:epoch 0 哨兵不下发、reset_req 自行投操作记录=0 次 * TC-UIDATA-027 遥测整定通道未注入防御:八个口一律安全返回、不碰出参; * 残表整表拒绝且不覆盖好表(与 025 同一条纪律的第五张表复刻) + * TC-UIDATA-029 通讯参数通道六(道G task-9):未注入防御 + 注入后转发 + * (行现状逐字段 / 带外拒不置脏 / 脏位零缓存跟随 / 撤销脱离) * TC-UIDATA-028 遥测整定注入后转发言行:行现状逐字段原样上抛、写口的 idx 与 * 工程量原样下传、返回码直通、pending 无状态直读(本层零缓存—— * 页面 OK/WAIT 的唯一依据必须是执行侧此刻的脏位) @@ -1480,6 +1482,103 @@ static void tc_yt_forwarding(void) CHECK(s_yt_pending_calls == 2, "TC-UIDATA-028q unbind stops reaching executor entirely"); } +/* ── 道G task-9 通道六:通讯参数注入(027/028 的第五张表复刻到第六张表)── + * 编号 029 = 向 Lead 申请发号(cursor_owner_only),cursor 现 026。 + * 假件语义 = simulator/main.c 同形(带内写副本、带外拒、写成功置脏)。 */ +static uint32_t s_cf_val[2] = { 1u, 2404u }; +static uint32_t s_cf_lo[2] = { 1u, 1024u }; +static uint32_t s_cf_hi[2] = { 65535u, 65535u }; +static uint8_t s_cf_dirty; +static uint8_t s_cf_last_idx = 0xFFu; +static uint32_t s_cf_last_val; +static int s_cf_calls; + +static int cf_stub_row(uint8_t a_idx, ui_data_comm_row_t *a_out) +{ + s_cf_calls++; + if (a_idx >= UI_DATA_COMM_ROW_N) { + return -1; + } + a_out->val = s_cf_val[a_idx]; + a_out->lo = s_cf_lo[a_idx]; + a_out->hi = s_cf_hi[a_idx]; + return 0; +} + +static int cf_stub_set(uint8_t a_idx, uint32_t a_val) +{ + s_cf_calls++; + s_cf_last_idx = a_idx; + s_cf_last_val = a_val; + if ((a_idx >= UI_DATA_COMM_ROW_N) || (a_val < s_cf_lo[a_idx]) || + (a_val > s_cf_hi[a_idx])) { + return -1; + } + s_cf_val[a_idx] = a_val; + s_cf_dirty = 1u; + return 0; +} + +static uint8_t cf_stub_pending(void) +{ + s_cf_calls++; + return s_cf_dirty; +} + +static const ui_data_comm_ops_t s_cf_stub = { + cf_stub_row, cf_stub_set, cf_stub_pending +}; + +/* 残表:只缺 pending——三口里最容易被"顺手少注一件"漏掉的那个(027l 同法) */ +static const ui_data_comm_ops_t s_cf_broken = { + cf_stub_row, cf_stub_set, NULL +}; + +/* TC-UIDATA-029a-e 未注入防御:三个口一律安全返回、不碰出参、不假报脏 */ +static void tc_comm_ch6_not_bound(void) +{ + ui_data_comm_row_t r; + const uint32_t sentinel = 0xA5A5A5A5uL; + + (void)ui_data_comm_bind(NULL); + r.val = sentinel; + CHECK(ui_data_comm_row(0u, &r) == -1, "TC-UIDATA-029a row not bound is -1"); + CHECK(r.val == sentinel, "TC-UIDATA-029b row not bound wrote nothing"); + CHECK(ui_data_comm_set(0u, 2u) == -1, "TC-UIDATA-029c set not bound is -1"); + CHECK(ui_data_comm_pending() == 0u, "TC-UIDATA-029d pending not bound is 0 (never fabricate WAIT)"); + CHECK(ui_data_comm_bind(&s_cf_broken) == -1, "TC-UIDATA-029e incomplete table refused"); +} + +/* TC-UIDATA-029f-o 注入后转发言行:现状逐字段原样上抛、写口 idx/值精确送达、 + * 脏位零缓存跟随(页面 WAIT/OK 的正确性全系于这两条) */ +static void tc_comm_ch6_forwarding(void) +{ + ui_data_comm_row_t r; + + CHECK(ui_data_comm_bind(&s_cf_stub) == 0, "TC-UIDATA-029f complete table accepted"); + CHECK(ui_data_comm_row(UI_DATA_COMM_ROW_PORT, &r) == 0, "TC-UIDATA-029g row forwarded"); + CHECK((r.val == 2404u) && (r.lo == 1024u) && (r.hi == 65535u), + "TC-UIDATA-029h row fields copied verbatim (band = executor's, not a copy)"); + CHECK(ui_data_comm_row(UI_DATA_COMM_ROW_N, &r) == -1, + "TC-UIDATA-029i out-of-range idx rejected at the port too"); + CHECK(ui_data_comm_row(0u, NULL) == -1, "TC-UIDATA-029j NULL out refused even bound"); + CHECK(ui_data_comm_set(UI_DATA_COMM_ROW_CA, 0u) == -1, + "TC-UIDATA-029k out-of-band write rejected (band owned by executor)"); + CHECK(s_cf_dirty == 0u, "TC-UIDATA-029l rejection left the section clean"); + CHECK(ui_data_comm_set(UI_DATA_COMM_ROW_CA, 7u) == 0, "TC-UIDATA-029m in-band write accepted"); + CHECK((s_cf_last_idx == UI_DATA_COMM_ROW_CA) && (s_cf_last_val == 7u), + "TC-UIDATA-029n executor got exact idx and value"); + CHECK(ui_data_comm_pending() == 1u, "TC-UIDATA-029o dirty forwarded (WAIT leg)"); + s_cf_dirty = 0u; + CHECK(ui_data_comm_pending() == 0u, "TC-UIDATA-029p clean follows immediately (OK leg, zero cache)"); + (void)ui_data_comm_bind(NULL); + CHECK(ui_data_comm_pending() == 0u, "TC-UIDATA-029q unbind restores safe defaults"); + s_cf_dirty = 1u; /* 桩此刻仍脏:撤销后连问都不该问到桩 */ + CHECK(ui_data_comm_pending() == 0u, "TC-UIDATA-029r unbind stops reaching executor entirely"); + s_cf_dirty = 0u; + s_cf_val[0] = 1u; /* 复位假件,别给后面的用例留串台状态 */ +} + /* ===================== main ===================== */ int main(void) @@ -1539,6 +1638,9 @@ int main(void) printf("-- yt channel: forwarding --\n"); tc_yt_forwarding(); + tc_comm_ch6_not_bound(); /* 道G task-9 通道六(TC-UIDATA-029) */ + tc_comm_ch6_forwarding(); + printf("\n=== UI Data Snapshot Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); return (s_failed == 0) ? 0 : 1; diff --git a/ui_cn_chars.txt b/ui_cn_chars.txt index e9c8f79e..81861b87 100644 --- a/ui_cn_chars.txt +++ b/ui_cn_chars.txt @@ -1 +1 @@ -ⅠⅡⅢ▶一三主事二交件位低作保信值入公共关出分制前动化区压参双取口号合启告回地址型复外子字存定实密已常年序度延异式强归当录态总恢息户手投抖护报据接控掩操故效数整无日时显最月期未本板查标校样检模欠次死段求波流测消清温点片版状用电界直看码硬确示秒程端第类系线终维网置自芯荷行装警认讯记设询误请负越跳软载输过运近返退通道遥部配采重量钟钥锁错长闭间闸防除障零面页频馈验默,: +ⅠⅡⅢ▶一三主事二交件位低作保信值入公共关出分制前动化区压参双取口号合听启告回地址型复外子字存定实密已年序度延式强归当录总恢息户手投抖护报据接控掩操故效数整无日时月期未本板查标校样检模欠次死段求波流测消清温点版用电界监直看码硬确秒程端第类系线终维网置自荷行装警认讯记设询误请负越跳软载输过运返退通道遥部配采重量钟钥锁错长闭闸防除障零面页频馈验默,:异常芯片态显最状示近间 diff --git a/ui_menu_tree.txt b/ui_menu_tree.txt index 155f1a28..757bb16b 100644 --- a/ui_menu_tree.txt +++ b/ui_menu_tree.txt @@ -45,7 +45,7 @@ 手动录波 #P 清除记录 #P #page=clear 通讯参数 - 通讯设置 #P + 通讯设置 #P #page=comm 网口设置 #P #page=net_cfg SNTP设置 #P 记录查询