feat(屏板): 状态灯位映射 sb_led_map + 104 RUN 观测口(批 3);附 4-1 并发压测脚本

批 3(LOGO 按用户裁定出局,不实现、sb_proto 的 LOGO 组帧挂待删标注):
- 新增 App/algo/sb_led_map.c/h:业务状态 → 屏板 LED 位掩码的唯一映射表
  (纯逻辑,主机单测可跑)。ID1 运行心跳 500ms 半周期闪(与主板 LED 同拍)、
  ID2 保护动作、ID3 104 链路 RUN、ID4 采样电平故障(不选 sticky——灯要能灭)、
  ID5 保护启动 picked;bit0 与 ID 6~19 恒 0。这张表参考源码里没有,属自定,
  出处 docs/产品需求/产品需求设计文档.md §5.16.3 ①。
- iec104_serv 新增 iec104_serv_link_run()(透传 link 状态机,不另存镜像量)。
- ui_task.c 帧组装块每拍现采四路状态源(均为任意上下文可读观测口)折算后
  传 sb_link_frame_update——替换批 2 的恒 0 占位;掩码变了帧就重组,
  复用 busy/dirty 机制,无新通路。
- 主机单测 test/host/sb_led_map 17/17(位语义/恒零位/闪烁相位/tick 回绕/
  NULL 契约);注册豁免走 SIM_EXEMPT(模拟器无屏板串口)。

批 4 的 4-1 双通道并发回归(真屏板在线实测):
- 新增 test/host/remolcd_pull_test.py:RemoDispBus 拉帧压测客户端
  (INIT→LCDMEM 10 Hz、逐帧 XOR+长度校验、往返延迟统计、--dump PBM)。
- 实测:网络侧两轮 2200 拉帧零错零超时(min 35/avg 63/max 131 ms);
  屏板侧 60 s 隔离窗口 rx +233 全收、drop 零新增、bad=0、off=0、无
  OFFLINE——两侧互不拖。观察项 len=7(断帧尺寸≠6,累计 7 次)出现窗口
  无法归因压测、隔离窗口零新增、有效帧零损失,已挂设计说明 §5.7 待查。

附带:sb_link.h frame_update 注释回填实测耗时(509/688 µs 取代旧估算)。
Keil 0 错误 0 警告;ci_local 全绿(SKIP=nested-comment,主树脚本另查 0 新发);
Code=239,064 B / RW_IRAM1 65.5 % / RW_CCM 87.9 %;覆盖率 90.1 % / 44 套件。
This commit is contained in:
2026-10-30 00:39:57 +08:00
parent 9c6d150261
commit 0b84c43d2e
17 changed files with 768 additions and 39 deletions
+58
View File
@@ -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 <stddef.h> /* 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;
}
+101
View File
@@ -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 <stdint.h>
/* ============================================================================
* 一、位定义(屏板物理口径: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 */
+15
View File
@@ -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 → 节拍)
* 返回值:无
+13
View File
@@ -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 */
+19 -3
View File
@@ -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 的说明)。 */