diff --git a/App/algo/sb_led_map.c b/App/algo/sb_led_map.c new file mode 100644 index 00000000..e22da68e --- /dev/null +++ b/App/algo/sb_led_map.c @@ -0,0 +1,58 @@ +/* + * 模块名称:屏板状态灯位映射(业务状态 → LED 位掩码) + * 模块功能:sb_led_map_eval()——状态快照进、32 位 LED 掩码出。位定义与理由全在 + * sb_led_map.h(映射表唯一出处),本文件只做折算。 + * 适用平台:平台无关(纯标准 C) + * 作者:王建锋 + * 创建日期:2026-10-29 + * 修改记录: + * 2026-10-29 王建锋 创建初始版本(批 3 的 3-1) + */ + +#include /* NULL:纯逻辑不经 main.h,NULL 要自己带来 */ + +#include "sb_led_map.h" + +/* NULL 输入归一用的全零快照(static const:不占 RAM,语义就是"什么都没发生") */ +static const sb_led_in_t s_in_zero = { 0UL, 0UL, 0UL, 0U, { 0U, 0U, 0U } }; + +/* + * 函数功能:把状态快照折算成屏板 LED 32 位掩码(契约见 sb_led_map.h) + * 入口参数:p_in - 状态快照,允许 NULL(= 全零输入,只跑心跳) + * now_ms - 毫秒时基(只用于运行灯相位) + * 返回值:32 位 LED 掩码(bit i = 灯 ID i;本函数不触碰 bit0 与 ID 6~19) + * 限定条件:任意上下文,纯函数 + * 函数说明:心跳相位取 (now_ms / SB_LED_BLINK_HALF_MS) 的奇偶——用**除法取奇偶** + * 而不是"与上次比差值再翻转",因为后者要把相位存成静态量,纯函数就没了 + * (单测要能不开机就验完整个闪烁序列)。回绕只截短一个半周期,无害。 + */ +uint32_t sb_led_map_eval(const sb_led_in_t *p_in, uint32_t now_ms) +{ + const sb_led_in_t *q = (p_in != NULL) ? p_in : &s_in_zero; + uint32_t mask = 0UL; + + /* ID 1 运行心跳:偶数个 500 ms 段 = 亮 */ + if (((now_ms / SB_LED_BLINK_HALF_MS) & 1UL) == 0UL) + { + mask |= SB_LED_BIT_RUN; + } + + if (q->prot_active != 0UL) + { + mask |= SB_LED_BIT_ALARM; + } + if (q->iec104_run != 0U) + { + mask |= SB_LED_BIT_COMM; + } + if (q->adc_flags != 0UL) + { + mask |= SB_LED_BIT_ADC_FAULT; + } + if (q->prot_pick != 0UL) + { + mask |= SB_LED_BIT_PICKUP; + } + + return mask; +} diff --git a/App/algo/sb_led_map.h b/App/algo/sb_led_map.h new file mode 100644 index 00000000..4b3bb4ac --- /dev/null +++ b/App/algo/sb_led_map.h @@ -0,0 +1,101 @@ +/* + * 模块名称:屏板状态灯位映射(业务状态 → LED 位掩码) + * 模块功能:把装置的业务状态(保护动作 / 保护启动 / 采样故障 / 104 主站在线 / + * 运行心跳)折算成屏板帧 B `info[0..3]` 的 32 位 LED 掩码。 + * ⚠ 本层**纯逻辑**:不碰 HAL、不读全局、不打日志——输入是调用方 + * (uiTask)采好的状态快照,输出是一个 uint32_t → 主机侧可纯逻辑单测 + * (test/host/sb_led_map/)。与 Drivers/BSP/ScreenLink/sb_proto 同一分层理由。 + * 适用平台:平台无关(纯标准 C) + * 作者:王建锋 + * 创建日期:2026-10-29 + * 修改记录: + * 2026-10-29 王建锋 创建初始版本(批 3 的 3-1:映射表落纸,docs/任务规划/屏板链路.md §3) + * + * 这张表为什么必须"自定":屏板源码(DTU-LCD LED_Printf)只规定了"位 → 灯号"的 + * 物理口径(bit1~bit19 → ID 1~19,ID 0 电源灯硬件常亮、不看位,bit20~31 未使用), + * **"哪路状态占哪一位"在参考主机源码里不存在**(参考的 LCD_LED_Update 实现在 + * 未开源的屏板 BSP 库里)。出处 docs/产品需求/产品需求设计文档.md §5.16.3 ①。 + */ +#ifndef SB_LED_MAP_H +#define SB_LED_MAP_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include + +/* ============================================================================ + * 一、位定义(屏板物理口径:bit i = 灯 ID i;bit0 被屏板忽略,别指望用它) + * ==========================================================================*/ + +/* ID 1 = 运行灯:装置活着的心跳,500 ms 半周期闪烁(与主板 LED1~3 心跳节奏同源, + * docs/产品需求/产品需求设计文档.md §5.8)。它**不取决于任何输入状态**——只要这段 + * 代码还在被调度,灯就在闪;灯不闪 = uiTask 停了,这正是操作员要看的信号。 */ +#define SB_LED_BIT_RUN (1UL << 1) + +/* ID 2 = 告警灯:任一保护元件处于**动作**态(prot_elm_active_mask != 0)。 + * ⚠ 产品构建处于「未标定闭锁」(App/task/protect_task.c 的 VALID 门禁), + * 标定前恒灭——与屏上"保护动作 N"同一已知口径,不是本灯的缺陷。 */ +#define SB_LED_BIT_ALARM (1UL << 2) + +/* ID 3 = 通信灯:IEC 60870-5-104 链路处于 RUN(STARTDT 已确认 = 主站真的在线在 + * 轮询)。选 RUN 而不选"TCP 已建连":连上不发 STARTDT 的扫描器不该点亮它。 */ +#define SB_LED_BIT_COMM (1UL << 3) + +/* ID 4 = 采样故障灯:ADC 采样有效性**电平**故障位非零(adc_health_flags, + * 静默/倒退等,当前活跃、可自愈熄灭)。选电平不选 sticky:这盏灯要能熄灭, + * "曾经故障过"的账在自检记录里,不该用一盏长明灯喊给操作员。 */ +#define SB_LED_BIT_ADC_FAULT (1UL << 4) + +/* ID 5 = 保护启动灯:任一保护元件处于**启动**(picked)态(prot_elm_pick_mask)。 + * picked 早于 asserted,"越限启动了但没到动作点"这段过程只在这盏灯上可见 + * (与 104「保护启动」记录同一动机,App/algo/prot_elm.h 的 pick_mask 注释)。 + * 同样受未标定闭锁口径约束。 */ +#define SB_LED_BIT_PICKUP (1UL << 5) + +/* 已赋义位的并集(判据用:掩码里不得出现本表之外的置位)。 + * ID 6~19 保留未用,恒 0;将来扩展从 ID 6 顺序取,跳号留白不回收语义。 */ +#define SB_LED_BITS_DEFINED (SB_LED_BIT_RUN | SB_LED_BIT_ALARM | SB_LED_BIT_COMM | \ + SB_LED_BIT_ADC_FAULT | SB_LED_BIT_PICKUP) + +/* 运行灯闪烁半周期(ms):0 ~ SB_LED_BLINK_HALF_MS-1 亮,下一个 500 ms 灭,交替。 + * 取 500 是为与主板 LED 心跳(defaultTask 500 ms 翻转)同拍,两套灯闪烁同相。 */ +#define SB_LED_BLINK_HALF_MS (500U) + +/* ============================================================================ + * 二、状态输入快照(调用方每拍现采,本模块不缓存、不加锁) + * ==========================================================================*/ + +typedef struct +{ + uint32_t prot_active; /* prot_elm_active_mask():bit i = 元件 i 动作 */ + uint32_t prot_pick; /* prot_elm_pick_mask():bit i = 元件 i 启动 */ + uint32_t adc_flags; /* adc_health_flags():ADC_HF_* 电平故障位 */ + uint8_t iec104_run; /* 1 = IEC 104 链路 RUN(iec104_serv_link_run()) */ + uint8_t pad[3]; /* 显式补齐:结构体大小固定 16 B,主机/目标端一致 */ +} sb_led_in_t; + +/* ============================================================================ + * 三、求值入口 + * ==========================================================================*/ + +/* + * 函数功能:把状态快照折算成屏板 LED 32 位掩码(纯函数,映射表唯一出处见本头 §一) + * 入口参数:p_in - 状态快照;**允许 NULL**:等价于全零输入(只跑运行灯心跳—— + * 心跳证明的是"这段代码活着",与别的状态无关,不该被空输入连坐) + * now_ms - 当前毫秒时基(运行灯相位唯一依赖;osKernelGetTickCount 同域) + * 返回值:32 位 LED 掩码(bit i = 灯 ID i;bit0 与 ID 6~19 恒 0,直接进帧 B info[0..3]) + * 限定条件:任意上下文(纯计算:无分支副作用、不读写任何静态量、时基回绕安全—— + * 回绕瞬间至多把一个半周期截短或延长 1 拍,500 ms 的心跳灯看不出来) + * 函数说明:闪烁相位 = (now_ms / 500) 的奇偶,偶 = 亮。上电 first 500 ms 为亮, + * "刚上电灯就在闪"是最直观的自检信号。各状态位相互独立,任一非零即置位, + * 无优先级、无互斥(告警与通信可以同时亮,那是两件同时在真发生的事)。 + */ +uint32_t sb_led_map_eval(const sb_led_in_t *p_in, uint32_t now_ms); + +#ifdef __cplusplus +} +#endif + +#endif /* SB_LED_MAP_H */ diff --git a/App/task/iec104_serv.c b/App/task/iec104_serv.c index 20a4c71e..fa42f47c 100644 --- a/App/task/iec104_serv.c +++ b/App/task/iec104_serv.c @@ -282,6 +282,21 @@ void iec104_serv_get_stats(iec104_serv_stats_t *p_out) *p_out = s; } +/* + * 函数功能:读 104 链路是否处于 RUN 态(契约见 iec104_serv.h 的 iec104_serv_link_run) + * 入口参数:无 + * 返 回 值:1 = RUN;0 = 其它 uint8_t + * 限定条件:任意上下文;单枚举字读,原子 + * 函数说明:直接透传 iec104_link_state(),不另存镜像量——镜像量一旦和状态机 + * 分家(比如 open/close 路径漏更),屏灯就成第二本假账。link_state + * 本身只读实例里的 state 字段(App/protocol/iec104/iec104_link.c), + * 无副作用,跨任务读安全。 + */ +uint8_t iec104_serv_link_run(void) +{ + return (iec104_link_state(&s_link) == IEC104_ST_RUN) ? 1U : 0U; +} + /* * 函数功能:单次服务循环(代次自愈 → accept → 门控收 → 喂 link → 节拍) * 返回值:无 diff --git a/App/task/iec104_serv.h b/App/task/iec104_serv.h index c8a5309b..0403c0e0 100644 --- a/App/task/iec104_serv.h +++ b/App/task/iec104_serv.h @@ -52,4 +52,17 @@ typedef struct { */ void iec104_serv_get_stats(iec104_serv_stats_t *p_out); +/* + * 函数功能:读 104 链路是否处于 RUN 态(STARTDT 已确认 = 主站在线在轮询) + * 返 回 值:1 = RUN;0 = 其它(IDLE/OPEN/STOPPED 或未创建任务) uint8_t + * 限定条件:**任意上下文可读**——单枚举字读,不取锁、不碰 socket、不阻塞 + * (与 iec104_serv_get_stats 同一形态与同一读者群)。 + * 函数说明:写者 = iec104Task(link 状态机推进),读者按需采样;单字读写在 + * Cortex-M4 上原子,最多读到"晚一拍"的状态,无撕裂风险。 + * 消费点 = 屏板状态灯 ID 3(App/algo/sb_led_map.h 的通信灯)。 + * ⚠ 别拿它换算 conn_up:RUN 是电平,conn_up 是累计边沿,语义不可互换 + * (同文件统计注释里的"不得互相换算"纪律)。 + */ +uint8_t iec104_serv_link_run(void); + #endif /* IEC104_SERV_H */ diff --git a/App/task/ui_task.c b/App/task/ui_task.c index 486fc1b3..6d5e5034 100644 --- a/App/task/ui_task.c +++ b/App/task/ui_task.c @@ -54,6 +54,9 @@ #include "ui_tokens.h" /* UI 设计令牌:循环周期 UI_TASK_TICK_MS / 待机阈值 UI_STANDBY_IDLE_MS */ #include "sys_clock.h" #include "prot_elm.h" /* 道F task-3 装置维护:强制复归请求 + 动作态元件位图 */ +#include "adc_health.h" /* 屏板状态灯 ID 4:ADC 电平故障位(adc_health_flags) */ +#include "sb_led_map.h" /* 屏板状态灯位映射(批 3:业务状态 → info[0..3] 掩码) */ +#include "iec104_serv.h" /* 屏板状态灯 ID 3:104 链路 RUN 态(任意上下文可读) */ #include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */ #include "net_latency_test.h" /* M3 探针:ui_loop(生产循环实测周期) */ #include "task_watchdog.h" @@ -527,10 +530,23 @@ void ui_task_func(void *argument) { * 排在推流往返**之前**,是因为那个往返最坏能等 ≈4 s(LCDMEM 应答 3204 B), * 排在它后面会让屏板画面跟着网络一起卡(同一份显存、两个出口,出口不能互相拖)。 * 显存直接取活帧缓冲:不留 3200 B 影子副本,sb_link 内部与帧里的点阵段 memcmp。 - * LED 掩码暂传 0(批 3 才落状态灯映射表),故 info[0..3] 现恒为 0。 */ + * LED 掩码(批 3):每拍现采四路状态源经 sb_led_map_eval 折算——四个源 + * 都是"任意上下文可读"的观测口(不取锁不阻塞,见各自头文件契约), + * 折算本身纯计算;心跳灯相位跟着这一拍的 tick 走(uiTask 10 ms 节拍 + * 远高于 500 ms 半周期,闪烁边沿误差 ≤1 拍)。 */ { - bsp_err_t sb_ret = sb_link_frame_update(ui_port_get_framebuffer(), 0u); - uint8_t board_key; + sb_led_in_t led_in; + uint32_t led_mask; + bsp_err_t sb_ret; + uint8_t board_key; + + led_in.prot_active = prot_elm_active_mask(); + led_in.prot_pick = prot_elm_pick_mask(); + led_in.adc_flags = adc_health_flags(); + led_in.iec104_run = iec104_serv_link_run(); + led_mask = sb_led_map_eval(&led_in, osKernelGetTickCount()); + + sb_ret = sb_link_frame_update(ui_port_get_framebuffer(), led_mask); /* BSP_ERR_BUSY 是常态(上一帧还在 59.6 ms 的发送窗口里),不必留痕; * 其余失败打一行即可(见 s_sb_frame_err_logged 的说明)。 */ diff --git a/Drivers/BSP/ScreenLink/sb_link.h b/Drivers/BSP/ScreenLink/sb_link.h index e9678299..63bd1ea7 100644 --- a/Drivers/BSP/ScreenLink/sb_link.h +++ b/Drivers/BSP/ScreenLink/sb_link.h @@ -214,14 +214,16 @@ void sb_link_poll(uint32_t now_ms); /* * 函数功能:组一帧帧 B(任务侧,内容/掩码变化时才真组) * 入口参数:p_live_bmp - 当前 1bpp 显存首地址(>= SB_FRAME_B_BMP_LEN 字节,非空) - * led_mask - 状态灯 32 位掩码(小端填进 info[0..3];批 3 才填真值,此处传 0) + * led_mask - 状态灯 32 位掩码(小端填进 info[0..3];位的语义见 + * App/algo/sb_led_map.h——位变了帧就重组,灯随下一轮召唤出去) * 返回值:BSP_OK 帧已就绪(本次没变,或已重新组好) * BSP_ERR_PARAM p_live_bmp 为空 * BSP_ERR_STATE 尚未 sb_link_init * BSP_ERR_BUSY TX DMA 正在发这一帧,本拍跳过(TxCplt 之后的下一拍会补上) * BSP_ERR_CRC 组完自校验失败(已计数 frame_bad,帧不会发出去) * 限定条件:**任务上下文**,且必须与 ui_port_step() 同一个任务(显存是单写者)。 - * 单帧约 0.17 ms(3200 B 拷贝 + 4206 B CRC),只在变化时付一次。 + * 变化帧实测 ≈0.5 ms(典型)/ 0.8 ms(峰值),没变只付 memcmp(≈20 µs); + * 判据口径见 docs/任务规划/屏板链路.md §3 的 2-1。 * 函数说明:1. **不持有显存副本**——直接拿 p_live_bmp 与帧缓冲里的点阵段 memcmp, * 没变就立刻返回(约 20 µs)。省掉 3200 B 影子缓冲,也省掉每拍重算 CRC。 * 2. **TX 占用期间绝不写帧缓冲**(那会让 DMA 发出半新半旧的"脏帧"): diff --git a/Drivers/BSP/ScreenLink/sb_proto.h b/Drivers/BSP/ScreenLink/sb_proto.h index 0682f73e..2b1a1189 100644 --- a/Drivers/BSP/ScreenLink/sb_proto.h +++ b/Drivers/BSP/ScreenLink/sb_proto.h @@ -58,7 +58,9 @@ extern "C" { #define SB_CMD_KEY (0x00U) /* 命令类型:按键事件(立即召唤) */ #define SB_CMD_RT (0x01U) /* 命令类型:周期刷新 */ -/* 帧 B 的 LOGO 魔数(info[4] / info[5]) */ +/* 帧 B 的 LOGO 魔数(info[4] / info[5]) + * ⚠ 待删除(用户裁定 2026-10-29):LOGO 功能出局,不下发、不接线;本组 + * LOGO 常量与 sb_frame_b_build_logo() 保留仅为协议层完整性,**勿新增调用**。 */ #define SB_LOGO_MAGIC_HI (0x89U) #define SB_LOGO_MAGIC_LO (0x45U) diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index e1f92f36..e0fbed65 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -1052,6 +1052,11 @@ 1 ..\App\algo\freq.c + + sb_led_map.c + 1 + ..\App\algo\sb_led_map.c + ui_data_feed.c 1 diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 662a4f6a..6e180e12 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1` | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 268,076 B (261.79 KB) | 1,048,576 B (1024 KB) | 25.6% | +| ER_IROM1 | 268,220 B (261.93 KB) | 1,048,576 B (1024 KB) | 25.6% | | RW_IRAM1 | 85,904 B (83.89 KB) | 131,072 B (128 KB) | 65.5% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **270,008 B (263.7 KB)** | 1,048,576 B (1024 KB) | **25.7%** | +| **Flash (ROM 合计)** | **270,152 B (263.8 KB)** | 1,048,576 B (1024 KB) | **25.8%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index 16ee11e4..55112558 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,7 +15,7 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 268,076 B | 1,048,576 B | 25.6% | 参考 | +| ER_IROM1 (Flash) | 268,220 B | 1,048,576 B | 25.6% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | | RW_IRAM1 | 85,904 B | 131,072 B | 65.5% | PASS | diff --git a/scripts/check_registration.py b/scripts/check_registration.py index 222149dd..15613e78 100644 --- a/scripts/check_registration.py +++ b/scripts/check_registration.py @@ -98,6 +98,7 @@ SIM_EXEMPT = [ ("App/algo/adc_cal.c", "标定折算纯算法 + cfg 段注册(消费者 = MCU 侧 adcTask 每拍;模拟器不跑采样任务),模拟器无消费路径"), ("App/algo/meas.c", "测频与遥测快照发布纯算法(唯一发布者 = MCU 侧 adcTask 200 ms 分频;104 改读快照与 UI 侧消费属另一波次另行接线,模拟器无消费路径;主机侧纯逻辑单测 test/host/meas 覆盖),模拟器无消费路径"), ("App/algo/run_par.c", "运行参数段纯算法 + [run_par] cfg 段注册 + 104 地址映射(写者 = iec104Task 直通、属主落盘 defaultTask;模拟器不跑 104 协议,模拟器无消费路径;主机侧纯逻辑单测 test/host/run_par 覆盖),模拟器无消费路径"), + ("App/algo/sb_led_map.c", "屏板状态灯位映射纯算法(唯一消费者 = MCU 侧 uiTask 经 sb_link 组帧;模拟器无屏板串口,模拟器无消费路径;主机侧纯逻辑单测 test/host/sb_led_map 覆盖),模拟器无消费路径"), ("App/app/app_main.c", "固件启动/就绪屏障,MCU 专属"), ("App/app/ui_data_feed.c", "UI 快照装配器(依赖 phasor/prot_set/iec104 端口常量等固件侧数据源);模拟器不跑装配链,快照由 simulator/main.c 直接发布固定帧,模拟器无消费路径"), ("App/fs/vol_lock.c", "FreeRTOS 互斥量卷锁,MCU 专属"), diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index a7605711..bf48a619 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -12,3 +12,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-27 235372 26392 1572 136924 261764 25.0 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 2026-10-28 236456 28124 1876 137084 264580 25.2 81536 62.2 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-29 238936 29140 1932 141396 268076 25.6 85904 65.5 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;sb_link.o=4364;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055 +2026-10-30 239064 29156 1932 141396 268220 25.6 85904 65.5 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;sb_link.o=4364;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055 diff --git a/scripts/size_attestation.json b/scripts/size_attestation.json index 78c0bf59..4da0b49f 100644 --- a/scripts/size_attestation.json +++ b/scripts/size_attestation.json @@ -1,17 +1,17 @@ { - "built_at": "2026-10-29T20:54:02+08:00", - "fingerprint": "1297b04cfaed4c9b8c30a1fcc64608932d3d047a32ad687274a6eac40bc4ca45", - "fingerprint_files": 1456, + "built_at": "2026-10-30T00:21:57+08:00", + "fingerprint": "1cbc1bc0c030169d1851e6303e0d643ae1f01632f03883215557416e7f3eb4ee", + "fingerprint_files": 1458, "fingerprint_source": "git-index", "gated_regions": [ "RW_IRAM1", "RW_CCM" ], - "git_head": "e5b4182a9f02dfe9f1f06dd58d086f64a157cc5c", + "git_head": "9c6d150261caacd1a3ff835d2830f706e9de2bf7", "map": "MDK-ARM/STM32F407-Demo/STM32F407-Demo.map", - "map_mtime": 1793278440, + "map_mtime": 1793290914, "pct": { - "ER_IROM1": 25.57, + "ER_IROM1": 25.58, "RW_CCM": 87.89, "RW_IRAM1": 65.54, "UNINIT_CCM": 18.75 @@ -19,7 +19,7 @@ "regions": { "ER_IROM1": { "max": 1048576, - "size": 268076 + "size": 268220 }, "RW_CCM": { "max": 65280, @@ -37,8 +37,8 @@ "schema": 2, "scope_note": "\u540c\u7801\u51ed\u8bc1\uff1a\u6307\u7eb9\u8986\u76d6\u56fa\u4ef6\u4f53\u79ef\u76f8\u5173\u6e90\u7801\uff0c\u53e3\u5f84\u89c1\u672c\u811a\u672c docstring", "sizes": { - "code": 238936, - "ro_data": 29140, + "code": 239064, + "ro_data": 29156, "rw_data": 1932, "zi_data": 141396 }, diff --git a/scripts/test_coverage_baseline.json b/scripts/test_coverage_baseline.json index 620e576b..6c091b7d 100644 --- a/scripts/test_coverage_baseline.json +++ b/scripts/test_coverage_baseline.json @@ -80,6 +80,14 @@ "lines_covered": 41, "lines_valid": 41 }, + "App/algo/sb_led_map.c": { + "func_rate": 1.0, + "funcs_covered": 1, + "funcs_valid": 1, + "line_rate": 1.0, + "lines_covered": 15, + "lines_valid": 15 + }, "App/cfg/cfg_file.c": { "func_rate": 0.963, "funcs_covered": 26, @@ -193,12 +201,12 @@ "lines_valid": 100 }, "App/task/iec104_serv.c": { - "func_rate": 0.8182, + "func_rate": 0.75, "funcs_covered": 9, - "funcs_valid": 11, - "line_rate": 0.7706, + "funcs_valid": 12, + "line_rate": 0.7572, "lines_covered": 131, - "lines_valid": 170 + "lines_valid": 173 }, "App/util/boot_stats.c": { "func_rate": 1.0, @@ -482,9 +490,9 @@ }, "iec104_serv": { "files": { - "App/task/iec104_serv.c": 170 + "App/task/iec104_serv.c": 173 }, - "lines_total": 170, + "lines_total": 173, "ok": true }, "lftpd": { @@ -623,6 +631,13 @@ "lines_total": 961, "ok": true }, + "sb_led_map": { + "files": { + "App/algo/sb_led_map.c": 15 + }, + "lines_total": 15, + "ok": true + }, "sb_link": { "files": { "Drivers/BSP/ScreenLink/sb_link.c": 207, @@ -739,6 +754,7 @@ "App/algo/prot_set.c", "App/algo/run_par.c", "App/algo/sample_ring.c", + "App/algo/sb_led_map.c", "App/cfg/cfg_file.c", "App/cfg/cfg_file_store_mcu.c", "App/cfg/prot_set_store.c", @@ -780,16 +796,16 @@ ], "totals": { "files_generated_excluded": 5, - "files_tested": 48, - "files_untested": 165, - "func_rate": 0.9339, - "funcs_covered": 551, - "funcs_valid": 590, - "line_rate": 0.9011, - "lines_covered": 7334, - "lines_valid": 8139, - "modules_ok": 43, - "modules_total": 43 + "files_tested": 49, + "files_untested": 166, + "func_rate": 0.9324, + "funcs_covered": 552, + "funcs_valid": 592, + "line_rate": 0.9009, + "lines_covered": 7349, + "lines_valid": 8157, + "modules_ok": 44, + "modules_total": 44 }, "untested_sources": [ { @@ -833,7 +849,7 @@ "path": "App/task/adc_task.c" }, { - "code_lines": 245, + "code_lines": 253, "path": "App/task/ui_task.c" }, { @@ -1116,6 +1132,10 @@ "code_lines": 27, "path": "Drivers/BSP/UI/ui_w_setview.h" }, + { + "code_lines": 26, + "path": "App/algo/sb_led_map.h" + }, { "code_lines": 26, "path": "App/util/task_prof.h" @@ -1264,6 +1284,10 @@ "code_lines": 12, "path": "App/app/app_main.h" }, + { + "code_lines": 12, + "path": "App/task/iec104_serv.h" + }, { "code_lines": 12, "path": "Drivers/BSP/GD5F2GQ5UE/nand_ftl.h" @@ -1280,10 +1304,6 @@ "code_lines": 11, "path": "App/sync/time_sync.h" }, - { - "code_lines": 11, - "path": "App/task/iec104_serv.h" - }, { "code_lines": 11, "path": "App/task/net_task.h" diff --git a/test/host/remolcd_pull_test.py b/test/host/remolcd_pull_test.py new file mode 100644 index 00000000..72f23bce --- /dev/null +++ b/test/host/remolcd_pull_test.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""RemoDispBus(7003) 拉帧压力客户端:批 4 的 4-1「双通道并发回归」的网络侧激励。 + +做什么: + 连 192.168.1.100:7003,先发 INIT 查询屏幕参数(应得 8 B:宽u16/高u16/帧字节数u32, + 大端),随后按固定节拍持续发 LCDMEM(偏移 0) 拉整帧 3200 B 显存,逐帧校验 + XOR 校验和与长度,统计请求数 / 应答数 / CRC 错 / 超时 / 往返延迟 min-avg-max。 + 可 --dump last_frame.pbm 存最后一帧(1bpp 布局与屏板帧 B 点阵段逐字节同构)。 + +为什么它能当 4-1 的激励: + 7003 的每次 LCDMEM 往返在固件里是「uiTask 入队 + 等 netTask 回执」 + (osMessageQueuePut(osWaitForever) + xTaskNotifyWait(portMAX_DELAY),最坏 ≈4 s, + 见 ui_net_sink.h 的 ⚠),即**每一拍拉帧都在真实占用 uiTask**——这正是 + 「7003 拉帧期间屏板不超时」要压的条件。屏板侧应答起在 USART1 空闲中断里, + 理论上不被 uiTask 拖住;4-1 的判据就是拿两边的数交叉证明这件事: + 网络侧(本脚本):超时 ≈0、延迟分布正常; + 固件侧(日志口 grep SBLINK):tx 跟得上 rx(drop 无新增)、bad=0、kerr=0。 + +协议(Drivers/BSP/UI/ui_net_sink.c 实测口径): + 帧 = + 客户端 TAG=0xAA,设备 TAG=0xBB;CMD:0=KEEP 1=INIT 2=KEY 3=LCDMEM。 + LCDMEM 请求 payload = 偏移 u32 大端(仅支持 0);应答 payload = 偏移 u32 + 3200 B 位图。 + (设备对 CRC 只做异或累加——与屏板链路的 CRC-16 是两套,别混。) + +用法(仓库根目录,先清代理再跑——本机有 HTTP 代理会劫持内网连接): + & '.venv\\Scripts\\python.exe' test/host/remolcd_pull_test.py --seconds 30 + --hz 10 拉帧节拍(默认 10 = 每秒 10 次,重压;1 次/秒 ≈ DTU-RemoteLCD 观感) + --dump x.pbm 退出前把最后一帧写成 PBM(160x160 1bpp,可直接图片查看) + +退出码:0 = 全程无超时且 CRC 全对;1 = 出现超时/CRC 错/断连;2 = 用法错误。 + +设计依据:docs/任务规划/屏板链路.md §3 批 4 的 4-1。 +创建日期:2026-10-30 +""" +import argparse +import socket +import struct +import sys +import time + +DEFAULT_IP = "192.168.1.100" +DEFAULT_PORT = 7003 +TAG_CLIENT = 0xAA +TAG_DEVICE = 0xBB +CMD_KEEP = 0 +CMD_INIT = 1 +CMD_LCDMEM = 3 +FB_SIZE = 3200 +SCREEN_W = 160 +SCREEN_H = 160 + + +def build_frame(cmd, payload=b""): + """组一帧客户端请求:AA 。""" + body = bytes([TAG_CLIENT, cmd, (len(payload) >> 8) & 0xFF, len(payload) & 0xFF]) + xor = 0 + for b in payload: + xor ^= b + return body + payload + bytes([xor]) + + +class FrameReader: + """设备应答流重组(逐字节状态机,TAG 定界——与固件侧同思路,噪声字节自动重同步)。""" + + def __init__(self): + self.buf = bytearray() + + def pop(self): + """从缓冲里取一个完整 (cmd, payload);没有则 None。坏帧丢弃继续找下一个 TAG。""" + while True: + i = self.buf.find(bytes([TAG_DEVICE])) + if i < 0: + del self.buf[:] # 整段都不是帧头:全弃 + return None + if i > 0: + del self.buf[:i] # 丢帧头前的噪声 + if len(self.buf) < 4: + return None + cmd = self.buf[1] + ln = (self.buf[2] << 8) | self.buf[3] + total = 4 + ln + 1 + if len(self.buf) < total: + return None + payload = bytes(self.buf[4:4 + ln]) + crc = self.buf[4 + ln] + del self.buf[:total] + xor = 0 + for b in payload: + xor ^= b + if crc != xor: + return ("CRCERR", payload) + return (cmd, payload) + + def feed(self, data): + self.buf += data + + +def run(args): + try: + s = socket.create_connection((args.ip, args.port), timeout=5.0) + except OSError as exc: + print("[ERR] 连 %s:%d 失败:%s" % (args.ip, args.port, exc)) + print(" (若在内网却报超时/奇怪拒绝:先清 HTTP 代理再跑,见文件头用法行)") + return 1 + s.settimeout(5.0) + rd = FrameReader() + + def wait_frame(want_cmd): + """收一个 cmd==want_cmd 的应答;CRC 错返回 ('CRCERR', None);超时返回 None。""" + t0 = time.monotonic() + while time.monotonic() - t0 < 5.0: + f = rd.pop() + if f is not None: + cmd, payload = f + if cmd == "CRCERR": + return ("CRCERR", None) + if cmd == want_cmd: + return (cmd, payload) + # 其它命令的应答(如残留 KEEP 回显):跳过继续找 + continue + try: + data = s.recv(4096) + except socket.timeout: + return None + if not data: + return "CLOSED" + rd.feed(data) + return None + + # ---- INIT:查询屏幕参数,顺带把几何钉死 ---- + s.sendall(build_frame(CMD_INIT)) + r = wait_frame(CMD_INIT) + if r is None or r == "CLOSED" or r[0] == "CRCERR": + print("[ERR] INIT 无有效应答(%s)" % (r,)) + s.close() + return 1 + _, payload = r + if len(payload) >= 8: + w, h, nbytes = struct.unpack(">HHI", payload[:8]) + print("[INIT] %dx%d frame_bytes=%u" % (w, h, nbytes)) + if w != SCREEN_W or h != SCREEN_H or nbytes != FB_SIZE: + print("[WARN] INIT 几何与预期 160x160/3200 不符——继续,以设备为准") + else: + print("[WARN] INIT 应答长度 %d < 8,跳过几何核对" % len(payload)) + + # ---- LCDMEM 主循环 ---- + n_req = n_resp = n_crc = n_to = 0 + lat_sum = lat_min = lat_max = 0.0 + last_bmp = None + period = 1.0 / args.hz + t_end = time.monotonic() + args.seconds + next_at = time.monotonic() + while time.monotonic() < t_end: + now = time.monotonic() + if now < next_at: + time.sleep(min(next_at - now, 0.005)) + continue + t0 = time.monotonic() + s.sendall(build_frame(CMD_LCDMEM, struct.pack(">I", 0))) + n_req += 1 + r = wait_frame(CMD_LCDMEM) + if r == "CLOSED": + print("[ERR] 第 %d 次拉帧时对端断开" % n_req) + s.close() + break + if r is None: + n_to += 1 + print("[WARN] #%d LCDMEM 超时(5 s)" % n_req) + else: + cmd_got, payload = r + if cmd_got == "CRCERR": + n_crc += 1 + print("[WARN] #%d CRC 错" % n_req) + elif len(payload) != 4 + FB_SIZE: + n_crc += 1 + print("[WARN] #%d 应答长度 %d != %d" % (n_req, len(payload), 4 + FB_SIZE)) + else: + dt = time.monotonic() - t0 + n_resp += 1 + lat_sum += dt + lat_min = dt if lat_min == 0 else min(lat_min, dt) + lat_max = max(lat_max, dt) + last_bmp = payload[4:] + next_at += period + if next_at < time.monotonic() - 1.0: # 落后太多不追(避免突发风暴) + next_at = time.monotonic() + + s.close() + avg = (lat_sum / n_resp * 1000.0) if n_resp else 0.0 + print("[STAT] 请求 %d / 应答 %d / CRC错 %d / 超时 %d" % (n_req, n_resp, n_crc, n_to)) + print("[STAT] 往返延迟 min %.1f / avg %.1f / max %.1f ms" + % (lat_min * 1000.0, avg, lat_max * 1000.0)) + if args.dump and last_bmp: + with open(args.dump, "wb") as f: + f.write(b"P4\n%d %d\n" % (SCREEN_W, SCREEN_H)) + f.write(last_bmp) + print("[DUMP] 最后一帧已写 %s(P4 PBM,与屏板帧 B 点阵段同构)" % args.dump) + ok = (n_to == 0) and (n_crc == 0) and (n_resp > 0) + print("[VERDICT] %s" % ("PASS" if ok else "FAIL")) + return 0 if ok else 1 + + +def main(): + ap = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + ap.add_argument("--ip", default=DEFAULT_IP) + ap.add_argument("--port", type=int, default=DEFAULT_PORT) + ap.add_argument("--seconds", type=float, default=30.0) + ap.add_argument("--hz", type=float, default=10.0, help="拉帧节拍(次/秒,默认 10)") + ap.add_argument("--dump", default="", help="退出前把最后一帧写成的 PBM 路径") + args = ap.parse_args() + if args.seconds <= 0 or args.hz <= 0: + print("[ERR] --seconds / --hz 必须为正") + return 2 + return run(args) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/host/sb_led_map/run_unit_test.py b/test/host/sb_led_map/run_unit_test.py new file mode 100644 index 00000000..67aa8e7a --- /dev/null +++ b/test/host/sb_led_map/run_unit_test.py @@ -0,0 +1,42 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""屏板状态灯位映射(sb_led_map)主机侧纯逻辑单元测试运行器(纯标准库)。 + +做什么:用本机 MSVC 离线编译 test/host/sb_led_map/sb_led_map_test.c + (#include 纳入生产源文件 App/algo/sb_led_map.c),验证位语义 + (每路状态只点亮对应位、可叠加不遮蔽)、恒零位(bit0 / ID 6~19)、 + 心跳闪烁相位与 tick 回绕、NULL 输入契约。 + +为什么这套判据在真机上测不动:真机只能看到"哪盏灯亮了"(目视),看不到 + "掩码里多没多出一位保留位"——保留位被顺带点亮时屏板毫无反应,现场表现为 + "改了表没效果";主机侧用"掩码 & ~定义集 == 0"一条就把映射表钉死。 + +用法(仓库根目录): + & '.venv\\Scripts\\python.exe' test/host/sb_led_map/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误。 + +设计依据:docs/任务规划/屏板链路.md §3 批 3 的 3-1。 +创建日期:2026-10-29 +""" +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 + +BUILD = TestBuild( + name="SB LED Map Tests", + src=os.path.join(HERE, "sb_led_map_test.c"), + include_dirs=[ + os.path.join(ROOT, "App", "algo"), + HERE, + ], + summary_marker="=== SB LED Map Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/sb_led_map/sb_led_map_test.c b/test/host/sb_led_map/sb_led_map_test.c new file mode 100644 index 00000000..ed9325c2 --- /dev/null +++ b/test/host/sb_led_map/sb_led_map_test.c @@ -0,0 +1,234 @@ +/* + * 模块名称:屏板状态灯位映射(sb_led_map)主机侧单元测试 + * 模块功能:在 PC 上离线验证 App/algo/sb_led_map.c 的位语义、恒零位、心跳闪烁 + * 相位与时基回绕行为。 + * + * 被测对象:真实源文件 App/algo/sb_led_map.c(#include 原样纳入)。 + * 本层纯逻辑(仅 stdint.h),**无需任何桩**——与 adc_health 套件同形。 + * + * 为什么值得单测(真机上验的是"灯亮没亮",这里验的是"亮的是不是那一位"): + * 1. bit0 恒 0——屏板 ID 0 是硬件常亮电源灯,掩码 bit0 根本不被读;但一旦 + * 哪天有人往映射里塞 bit0,测试先拦住,而不是到现场发现"多一位没反应"。 + * 2. ID 6~19 恒 0——映射表之外的位不许被顺带点亮(掩码里出现未定义位 = 表被改 + * 而测试没跟上,这是这张"自定表"唯一的防漂移手段)。 + * 3. 心跳相位在 tick 回绕点不得连灭两个 500 ms 段(操作员眼里的"装置停了")。 + * 4. NULL 输入只跑心跳(心跳证明的是"代码活着",不该被空输入连坐熄灭)。 + * + * 编号:TC-SBLEDMAP-001 起(小写子项 = 同一判据的另一面)。 + * 创建日期:2026-10-29(批 3 · 3-1) + */ +#include +#include + +#include "sb_led_map.h" + +/* 被测生产源(include_dirs 里有 App/algo) */ +#include "sb_led_map.c" + +static int s_pass; +static int s_fail; + +#define CHECK(cond, name) \ + do { \ + if (cond) { \ + s_pass++; \ + } else { \ + s_fail++; \ + printf("FAIL: %s (line %d)\n", (name), __LINE__); \ + } \ + } while (0) + +/* 亮/灭心跳各取一个代表时刻(偶段 = 亮,见 sb_led_map.c) */ +#define T_ON (0U) /* [0,500) 偶段 → 亮 */ +#define T_OFF (500U) /* [500,1000) 奇段 → 灭 */ + +/* + * TC-SBLEDMAP-001 全零输入:只有心跳位,其余状态位全灭 + */ +static void tc_zero_input(void) +{ + sb_led_in_t in; + uint32_t m; + + in.prot_active = 0U; + in.prot_pick = 0U; + in.adc_flags = 0U; + in.iec104_run = 0U; + + m = sb_led_map_eval(&in, T_ON); + CHECK(m == SB_LED_BIT_RUN, "TC-SBLEDMAP-001 zero input lights only the run blink (on phase)"); + + m = sb_led_map_eval(&in, T_OFF); + CHECK(m == 0U, "TC-SBLEDMAP-001b zero input in the dark half-cycle is all-zero"); +} + +/* + * TC-SBLEDMAP-002 每一路状态各自点亮且只点亮对应位(非零即真,不看具体值) + */ +static void tc_one_source_each(void) +{ + sb_led_in_t in; + uint32_t m; + + in.prot_active = 0U; + in.prot_pick = 0U; + in.adc_flags = 0U; + in.iec104_run = 0U; + + in.prot_active = 0x00080000U; /* 随便哪个元件动作 */ + m = sb_led_map_eval(&in, T_OFF); + CHECK(m == SB_LED_BIT_ALARM, "TC-SBLEDMAP-002 prot_active -> ID 2 only"); + in.prot_active = 0U; + + in.prot_pick = 1U; + m = sb_led_map_eval(&in, T_OFF); + CHECK(m == SB_LED_BIT_PICKUP, "TC-SBLEDMAP-002b prot_pick -> ID 5 only"); + in.prot_pick = 0U; + + in.adc_flags = 0x80000000U; /* 高位故障位同样算非零 */ + m = sb_led_map_eval(&in, T_OFF); + CHECK(m == SB_LED_BIT_ADC_FAULT, "TC-SBLEDMAP-002c adc_flags -> ID 4 only"); + in.adc_flags = 0U; + + in.iec104_run = 1U; + m = sb_led_map_eval(&in, T_OFF); + CHECK(m == SB_LED_BIT_COMM, "TC-SBLEDMAP-002d iec104_run -> ID 3 only"); + in.iec104_run = 0U; +} + +/* + * TC-SBLEDMAP-003 多路同时:位可叠加、互不遮蔽(告警+通信同时真是合法工况) + */ +static void tc_stack(void) +{ + sb_led_in_t in; + uint32_t m; + + in.prot_active = 3U; + in.prot_pick = 3U; + in.adc_flags = 1U; + in.iec104_run = 1U; + + m = sb_led_map_eval(&in, T_ON); + CHECK(m == (SB_LED_BITS_DEFINED), "TC-SBLEDMAP-003 all sources + blink = exactly the defined set"); + + m = sb_led_map_eval(&in, T_OFF); + CHECK(m == (SB_LED_BITS_DEFINED & ~SB_LED_BIT_RUN), + "TC-SBLEDMAP-003b dark half-cycle drops only the run bit"); +} + +/* + * TC-SBLEDMAP-004 恒零位:bit0 与 ID 6~19 在任何输入下不得出现 + *(bit0 屏板根本不读;ID 6~19 是保留位——掩码出现表外位 = 表被改而没走测试) + */ +static void tc_reserved_bits(void) +{ + sb_led_in_t in; + uint32_t t; + int clean = 1; + + in.prot_active = 0xFFFFFFFFU; + in.prot_pick = 0xFFFFFFFFU; + in.adc_flags = 0xFFFFFFFFU; + in.iec104_run = 1U; + + /* 扫 3 秒时基(6 个半周期)+ 边界时刻,全取定义集做差 */ + for (t = 0U; t < 3000U; t += 499U) { + uint32_t m = sb_led_map_eval(&in, t); + if ((m & ~SB_LED_BITS_DEFINED) != 0U) { + clean = 0; + } + } + CHECK(clean == 1, "TC-SBLEDMAP-004 no bit outside the defined set ever appears"); + CHECK((sb_led_map_eval(&in, T_ON) & 1U) == 0U, "TC-SBLEDMAP-004b bit0 stays 0 (screen board ignores it)"); +} + +/* + * TC-SBLEDMAP-005 心跳相位:连续 8 个半周期亮灭交替,边界含等号(500 整 = 暗段起点) + */ +static void tc_blink_sequence(void) +{ + int i; + int prev = -1; + int alt = 1; + uint32_t m; + + for (i = 0; i < 8; i++) { + uint32_t t = (uint32_t)i * SB_LED_BLINK_HALF_MS; + int on; + + m = sb_led_map_eval(NULL, t); + on = ((m & SB_LED_BIT_RUN) != 0U) ? 1 : 0; + if ((prev >= 0) && (on == prev)) { + alt = 0; + } + prev = on; + } + CHECK(alt == 1, "TC-SBLEDMAP-005 run bit alternates every half-period"); + CHECK((sb_led_map_eval(NULL, 0U) & SB_LED_BIT_RUN) != 0U, + "TC-SBLEDMAP-005b power-on first half-period is LIT (blink visible from t=0)"); + CHECK((sb_led_map_eval(NULL, SB_LED_BLINK_HALF_MS) & SB_LED_BIT_RUN) == 0U, + "TC-SBLEDMAP-005c the boundary instant belongs to the dark half-period"); + CHECK((sb_led_map_eval(NULL, SB_LED_BLINK_HALF_MS - 1U) & SB_LED_BIT_RUN) != 0U, + "TC-SBLEDMAP-005d last ms before the boundary still lit"); +} + +/* + * TC-SBLEDMAP-006 tick 回绕:跨 0xFFFFFFFF 连续熄灭时长不得超过一个半周期 + *(相位由除法直接得出、不依赖"上次"状态。回绕把末段截短、下一段从"偶段=亮" + * 重新开始——最坏也只是某段被截短,不会出现操作员眼里的"连灭 1 s = 装置停了"。 + * 1 ms 步进扫回绕前后各 1.5 个周期,量最长暗段。) + */ +static void tc_wraparound(void) +{ + uint32_t t; + int run = 0; /* 当前连续暗 ms 数 */ + int worst = 0; + + /* now 从 0xFFFFFFFF-1500 走到 0xFFFFFFFF 再接 0..1500(模 2^32 连续) */ + for (t = 0xFFFFFFFEU - 1500U; t != 0xFFFFFFFFU; t++) { + int dark = ((sb_led_map_eval(NULL, t) & SB_LED_BIT_RUN) == 0U) ? 1 : 0; + + run = (dark != 0) ? (run + 1) : 0; + if (run > worst) { + worst = run; + } + } + for (t = 0U; t <= 1500U; t++) { + int dark = ((sb_led_map_eval(NULL, t) & SB_LED_BIT_RUN) == 0U) ? 1 : 0; + + run = (dark != 0) ? (run + 1) : 0; + if (run > worst) { + worst = run; + } + } + /* 半周期 500 ms + 1 ms 扫描步进余量 */ + CHECK(worst <= (int)SB_LED_BLINK_HALF_MS + 1, + "TC-SBLEDMAP-006 dark run never exceeds one half-period across tick wraparound"); +} + +/* + * TC-SBLEDMAP-007 NULL 输入 = 全零输入 + 心跳照常(注释契约:心跳不被连坐) + */ +static void tc_null_input(void) +{ + CHECK(sb_led_map_eval(NULL, T_ON) == SB_LED_BIT_RUN, + "TC-SBLEDMAP-007 NULL input still blinks (liveness signal must not depend on inputs)"); + CHECK(sb_led_map_eval(NULL, T_OFF) == 0U, + "TC-SBLEDMAP-007b NULL input lights nothing else"); +} + +int main(void) +{ + printf("-- sb_led_map unit tests --\n"); + tc_zero_input(); + tc_one_source_each(); + tc_stack(); + tc_reserved_bits(); + tc_blink_sequence(); + tc_wraparound(); + tc_null_input(); + + printf("=== SB LED Map Tests: %d/%d PASSED ===\n", s_pass, s_pass + s_fail); + return (s_fail == 0) ? 0 : 1; +}