Files
STM32F4-Base/App/task/iec104_serv.c
T
wandering1 0b84c43d2e 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 套件。
2026-10-30 00:39:57 +08:00

457 lines
19 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:IEC 60870-5-104 被控站服务(板端胶水任务)
* 模块功能:监听端口来自 [comm] 段(工厂默认 2404)的 TCP server + 单连接
* socket 收发 + 把字节流喂给 iec104_link 纯逻辑层;对时经
* sys_clock_set_unix 落墙钟;数据源走 iec104_pts 默认源
* (phasor / prot_elm / prot_set)。协议语义全部在 App/protocol/iec104,
* 本文件只做 I/O 编排(分层约束)。
* 线程约定:iec104Task 独占(NET_READY 屏障后循环,100ms 节拍 + 看门狗心跳)。
* 创建日期:2026-10-12(B4)
* 修改记录:
* 2026-10-26 DTU 项目组 道G task-8:公共地址与监听端口取值来源改
* [comm] 段 getter(iec104_comm.h,改后重启生效;
* 端口另在下次重 bind 自然生效)。只改取值来源,
* 链路编排零改动。
*/
#include "iec104_serv.h"
#define DBG_LINE_BUF_MAX 128U /* 深链模块日志缓冲收紧(time_sync 先例) */
#define DBG_TAG "[I104]"
#include "iec104_comm.h" /* [comm] 段 getter:ca / 监听端口(道G task-8) */
#include "iec104_link.h"
#include "iec104_pts.h"
#include "net_socket.h"
#include "prot_set.h" /* 定值脏标志 peek(B7 落盘通知) */
#include "run_par.h" /* 运行参数脏 peek(运行参数段落盘通知,同款语义) */
#include "sys_clock.h"
#include "sys_log.h" /* task-19:对时成功落一条操作记录(SYSLOG_OPER) */
#include "sys_events.h"
#include "task_watchdog.h"
#include "dbg_log.h"
#include "cmsis_os2.h"
#include <string.h>
/* 服务周期:COV 延迟 ≤100ms(远小于 SOE 5000ms 验收线);心跳间隔同值 */
#define IEC104_POLL_MS 100u
/* 接收搬运缓冲(静态,不入栈;link 层内部另有 512B 重组缓冲) */
#define IEC104_RX_SZ 128u
/* 兜底节拍:门控判"无数据"时每 32 轮无条件收一次(ui_net_sink RM 先例,
* 防 RECV_OK 事件未收到把数据永久藏住);须为 2 的幂 */
#define IEC104_LIVENESS_DIV 32u
/* 连续发送失败阈值:同 ui_net_sink 先例(短写=对端收坏帧) */
#define IEC104_SEND_FAIL_MAX 3u
/* ---------------- 服务状态(iec104Task 独占) ---------------- */
static iec104_link_t s_link;
static int s_listen = -1;
static int s_client = -1; /* 单连接模型:MCU 上即 s_listen */
static uint32_t s_gen; /* 网络代次(重建自愈,ui_net_sink 先例) */
static uint8_t s_rx[IEC104_RX_SZ];
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);
static uint64_t s_epoch_ms(void *ctx);
/* 链路 cfg:ca 占位工厂默认,任务启动经 s_apply_comm_ca() 从 [comm] 段取真值
* (道G task-8;点表口径默认 1);非 const 即此一处写、其余只读 */
static iec104_link_cfg_t s_cfg = { IEC104_COMM_DEF_CA,
s_send_apdu, s_clock_sync, s_epoch_ms };
/*
* 函数功能:link 层发送回调——经 net_send 串行 RPC 发出一个 APDU
* 入口参数:ctx 未用;apdu/len = 完整 APCI 帧
* 返回值:无
* 限定条件:s_client >= 0(在 link 调用栈内执行,即本任务上下文)
* 函数说明:短写/失败累计(ui_net_sink 先例);连续 3 次 → 置 s_conn_dead,
* 由 s_service_link 在 link 调用返回后统一拆链(严禁在回调里拆)
*/
static void s_send_apdu(void *ctx, const uint8_t *apdu, uint16_t len) {
int ret;
(void)ctx;
if (s_client < 0) {
return;
}
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++;
}
DBG_ERROR("i104 send short: ret=%d/%u run=%u", ret, (unsigned)len,
(unsigned)s_send_fail_run);
if (s_send_fail_run >= IEC104_SEND_FAIL_MAX) {
s_conn_dead = 1u;
}
}
/*
* 函数功能:link 层对时回调——epoch 毫秒落系统墙钟(含 SD2506 RTC 写回),
* 成功时顺带落一条操作记录(道F task-19:「时钟设置」是四处操作记录
* 写入点之一,另外三处见 App/log/README.md 的五类记录对照表)
* 入口参数:ctx 未用;epoch_ms = C_CS 解析出的 UTC 时刻
* 返回值:0 成功 / -1 失败(link 层回 actcon+PN)
* 函数说明:uint32 秒域上限 2106-02-07,越界显式拒绝(CP56 域到 2127)。
* **只在真正改成功之后才投记录**:越界拒绝与 sys_clock_set_unix 失败两条腿
* 一条都不投——操作记录是审计面,写进去一条没发生过的"对时"比少一条更坏
* (TC-I104SRV-050b / 051 分别钉这两条腿)。
* 投递用 syslog_post_quiet():iec104Task 是深链任务(本文件顶部已把
* DBG_LINE_BUF_MAX 收到 128 就是同一件事的证据),不许在它的调用链上
* 内联日志行缓冲;两个投递口的收/丢判据一致,差别只在留痕位置
* (口径与理由见 App/log/README.md「两个投递口」)。
* 文案是 ROM 常量、17 字节 < SYSLOG_DESC_MAX(28):iec104Task 里不 snprintf。
*/
static int s_clock_sync(void *ctx, uint64_t epoch_ms) {
uint64_t secs = epoch_ms / 1000ULL;
int rc;
(void)ctx;
if (secs > 0xFFFFFFFFULL) {
return -1;
}
rc = sys_clock_set_unix((uint32_t)secs);
if (rc == 0) {
syslog_post_quiet(SYSLOG_OPER, "clock set by 104");
}
return rc;
}
/*
* 函数功能:link 层时标源——当前墙钟 epoch 毫秒(B9:M_SP_TB_1 自发用)
* 入口参数:ctx 未用
* 返回值:epoch 毫秒;墙钟未建(UINT32_MAX)→ UINT64_MAX(link 置 CP56 IV)
* 函数说明:sys_clock 秒域时基 → ms 字段恒 0(不造假亚秒,B9 口径);
* 纯 RAM 推算,无 I/O、无副作用。
*/
static uint64_t s_epoch_ms(void *ctx) {
uint32_t s = sys_clock_get();
(void)ctx;
if (s == UINT32_MAX) {
return UINT64_MAX;
}
return (uint64_t)s * 1000ULL;
}
/*
* 函数功能:把 [comm] 段公共地址取进链路 cfg(道G task-8 取值来源改造)
* 返回值:无
* 限定条件:iec104_link_init 之前调用(任务启动一次)——ca 生效语义 =
* 改后重启;端口不同:s_bind_listen 每次现取 iec104_comm_port(),
* 下次重 bind(拆链重建/网络代次重建)即生效,不必整机重启
*/
static void s_apply_comm_ca(void) {
s_cfg.ca = iec104_comm_ca();
}
/*
* 函数功能:创建并监听 [comm] port(socket+bind+listen,抄 ui_net_sink bind_listen)
* 返回值:0 成功(s_listen 写回)/ -1 失败
* 限定条件:net 子系统可用(NET_READY 屏障后)
*/
static int s_bind_listen(void) {
int ls;
struct net_sockaddr_in addr;
ls = net_socket(NET_AF_INET, NET_SOCK_STREAM);
if (ls < 0) {
return -1;
}
(void)memset(&addr, 0, sizeof(addr));
addr.sin_family = NET_AF_INET;
addr.sin_port = net_htons(iec104_comm_port());
addr.sin_addr.s_addr = NET_INADDR_ANY;
if (net_bind(ls, (struct net_sockaddr *)&addr, (int)sizeof(addr)) < 0) {
net_close(ls);
return -1;
}
if (net_listen(ls) < 0) {
net_close(ls);
return -1;
}
s_listen = ls;
return 0;
}
/*
* 函数功能:主动断开当前 104 连接并重建监听(协议故障/t1 超时/发送连败)
* 入口参数:why = 日志原因串(ASCII)
* 返回值:无
* 函数说明:与 EOF 路径不同——协议违例时主站不会自行断开,必须本端 close
* 促其重连;单连接模型下 client 即 listen,close 后统一重建
* (time_sync 会话收尾先例,非芯片侧 auto_relisten 风暴路径)
*/
static void s_drop_conn(const char *why) {
DBG_ERROR("i104 drop: %s", why);
if (s_client >= 0) {
net_close(s_client);
}
s_client = -1;
s_listen = -1;
iec104_link_close(&s_link);
s_send_fail_run = 0u;
s_conn_dead = 0u;
s_liveness_ctr = 0u;
if (s_bind_listen() < 0) {
s_listen = -1; /* 主循环下轮重试 */
}
}
/*
* 函数功能:link 层节拍服务 + 故障检测(poll 后查 IDLE 跳变/发送连败)
* 入口参数:now = 注入时基(tick ms)
* 返回值:无
*/
static void s_service_link(uint32_t now) {
iec104_link_poll(&s_link, now);
if (s_conn_dead != 0u) {
s_drop_conn("send fail");
return;
}
if (iec104_link_state(&s_link) == IEC104_ST_IDLE) {
s_drop_conn("protocol fault");
}
}
/*
* 函数功能:读 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;
}
/*
* 函数功能:读 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 → 节拍)
* 返回值:无
* 限定条件:NET_READY 已置位;s_cfg/s_link 已 init
* 函数说明:门控(net_listener_armed/net_can_read 纯 RAM 读)+ 兜底节拍照
* ui_net_sink 先例;EOF 时 client==listen 严禁 close(net 层
* 已 auto_relisten,close 会连监听一起毁)
*/
static void s_serv_poll(void) {
uint32_t now = osKernelGetTickCount();
int n;
/* 网络重建(代次变化):旧监听/客户端随芯片复位失效 → 重建 */
if (net_get_generation() != s_gen) {
if ((s_client >= 0) && (s_client != s_listen)) {
net_close(s_client);
}
if (s_listen >= 0) {
net_close(s_listen);
s_listen = -1;
}
s_client = -1;
iec104_link_close(&s_link);
s_send_fail_run = 0u;
s_conn_dead = 0u;
s_liveness_ctr = 0u;
s_gen = net_get_generation();
(void)s_bind_listen();
return;
}
if (s_listen < 0) {
(void)s_bind_listen(); /* 启动失败/被毁:重试 */
return;
}
if (s_client < 0) {
int c;
/* accept 门控迁移(docs/网络/网络自愈架构设计.md §4.2):门控从 net_can_accept(只认 ESTABLISHED,已退役)
* 迁 net_listener_armed——旧门控把 LISTENING 窗口全挡掉,:2404 的丢
* CONNECT 探针(net_accept_locked 内)因此从不运行(与已修的 :7003
* 同型);armed=ESTABLISHED 待 accept / LISTENING /
* CLOSED+auto_relisten+残留(ESTABLISHED 放行由台架实证补齐:
* 漏它则本任务 200ms 周期抢不过对账② ~400ms 去抖、整场饿死) */
if (net_listener_armed(s_listen) == 0) {
if (net_get_sock(s_listen) == NULL) {
s_listen = -1; /* 控制块消失:交回上方重建 */
}
return;
}
c = net_accept(s_listen, NULL, NULL);
if (c >= 0) {
s_client = c;
s_send_fail_run = 0u;
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;
}
/* 门控 + 兜底:无数据多数轮直接走 link 节拍;每 32 轮无条件收一次 */
if (net_can_read(s_client) == 0) {
if (((++s_liveness_ctr) & (IEC104_LIVENESS_DIV - 1u)) != 0u) {
s_service_link(now);
return;
}
} else {
s_liveness_ctr = 0u;
}
/* DONTWAIT 已转正为具名 API net_recv_nb——本任务在 IWDG
* required_mask 内且有 100ms 周期职责(喂狗/链路节拍/上送),禁止无限
* 阻塞 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 */
if (s_client != s_listen) {
net_close(s_client); /* 模拟器多连接后端:数据通道独立 */
}
s_client = -1;
s_liveness_ctr = 0u;
iec104_link_close(&s_link);
if ((net_get_sock(s_listen) == NULL) && (s_bind_listen() < 0)) {
s_listen = -1;
}
return;
} else if (net_get_errno() != NET_ERR_WOULDBLOCK) {
s_drop_conn("recv err");
return;
}
s_service_link(now);
}
/*
* 函数功能:iec104Task 任务体(freertos.c USER CODE 区创建)
* 入口参数:argument 未用
* 返回值:无(不返回)
* 函数说明:NET_READY 屏障(100ms 分片 + 每片喂心跳,timeSyncTask 先例)→
* 点表/链路 init + 建监听 → 100ms 服务循环(每轮喂看门狗)
*/
void iec104_serv_task(void *argument) {
(void)argument;
if (sys_evtHandle != NULL) {
for (;;) {
uint32_t r = osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY,
osFlagsWaitAny | osFlagsNoClear, 100u);
if ((r & osFlagsError) == 0u) {
break; /* 就绪位是状态位(NoClear),不消费 */
}
task_wd_beat(TASK_WD_IEC104);
}
}
iec104_pts_init();
s_apply_comm_ca();
iec104_link_init(&s_link, &s_cfg, NULL);
s_gen = net_get_generation();
if (s_bind_listen() < 0) {
DBG_ERROR("i104: bind port %u FAIL, retry in loop",
(unsigned)iec104_comm_port());
} else {
DBG_INFO("i104: listening on %u", (unsigned)iec104_comm_port());
}
for (;;) {
task_wd_beat(TASK_WD_IEC104);
s_serv_poll();
/* 定值改动(C_SE 执行段 / YK12)→ 通知属主任务落盘 /prot_set.bin(B7)。
* peek 非消费:写盘失败时脏标志仍在,下一轮再投递 = 天然重试;
* 本任务不碰文件系统(配置引擎属主 = defaultTask,模块约定) */
if ((prot_set_dirty_peek() != 0u) && (sys_evtHandle != NULL)) {
(void)osEventFlagsSet(sys_evtHandle, SYS_EVT_PROT_SET_SAVE);
}
/* 运行参数整定(C_SE 0x8020~0x8058 段)→ 通知属主落盘 config.ini
* (运行参数段;peek 非消费 = 写盘失败天然重试,与上块 B7 语义完全同款) */
if ((run_par_dirty_peek() != 0u) && (sys_evtHandle != NULL)) {
(void)osEventFlagsSet(sys_evtHandle, SYS_EVT_RUN_PAR_SAVE);
}
osDelay(IEC104_POLL_MS);
}
}