Files
STM32F4-Base/App/task/iec104_serv.c
T
wandering1 b8c6a3dab0 refactor(net)!: N34 阶段1 API 面收敛(对账⑤ + 删 3 个手动自愈 API)
BREAKING CHANGE: 删除公开 API net_relisten / net_resync_listen /
net_can_accept 与消息类型 NET_MSG_RESYNC_LISTEN / NET_MSG_RELISTEN。
生产调用者为零(仅看门狗文档与 iec104 门控),应用侧无迁移成本;
如有外部消费者须改用对账/pump 自动机制(见下)。

1. 对账①扩展为①②⑤(F-NET-2 缺口,先补缺口再删 API)
   net_reconcile_tick 新增 bad=5:本层 ESTABLISHED 且芯片 0x05/0x01
   (Socket OPEN + TCP LISTEN,静默回收回监听、不发 DISCONNECT)
   → RESYNC 语义只修本层(state→LISTENING + last_peer_port 转登 +
   lost_conn_seen 清零 + 取消挂起 recv),**不发任何芯片命令**
   (纪律同 pump③ U11,run8 实证重下发会 170s 永拒)。
   原判据①要求 TCP==CLOSED、②要求本层 LISTENING,均不命中此形态
   ——此前生产无人认领(仅手动 API 能修)。
2. 删 net_relisten(net_socket.c 函数 + 消息分派 + 头文件声明)
   footgun:对健康监听器无条件 open→USE→close 自毁 ~1s(原注释自认);
   与 pump(100ms)/D10(30ms) 完全重叠。
3. 删 net_resync_listen(同上;语义并入第 1 条)
4. 删 net_can_accept(net_socket.c + net_socket.h + sim 三处)
   只认 ESTABLISHED 的旧门控把"监听器在服役、待 CONNECT"窗口全挡掉,
   :2404 丢 CONNECT 探针因此从不运行;iec104 门控迁 net_listener_armed。
5. timeSync :8888 会话收尾不再 net_close(同 fd 续 accept,监听跨会话
   常驻;原 close→重建每会话一个 Trap 09 沉降窗)。server 脚本注释同步
   新契约(PC 侧 2s 宽限后自行关闭)。
6. lftpd 应用层阻塞重试退役:lftpd_inet_listen 5 轮(500ms/2000ms,最坏
   10s 阻塞 ftpTask) → 单次 + 失败即 425;cmd_pasv 3×200ms 同删。
   就绪屏障已消除 Trap 09 启动形态,客户端自带传输重试语义。

台架实证修正(本提交内二次修复,务必留痕):
- net_listener_armed 补齐 **ESTABLISHED 放行**。首版迁移只放行
  LISTENING/CLOSED+残留,漏了 ESTABLISHED=单连接模型的"待 accept"电平
  → iec104 200ms 任务周期抢不过对账② 的 ~400ms 去抖,对账②先 standalone
  accept 转 ESTABLISHED 后 armed=0 → 应用整场不 accept,18/18 检查全 FAIL
  (设备日志证据:sock1 standalone accept 后无 i104 accepted,直至对端 FIN
  才由 closed-with-data 救回)。补放行后 18/18 PASS,且耗时 127s→8.3s。
  该态 accept 必返 fd,无 RPC 风暴风险。
- 期间顺带验证 M3 无辜:sock1 suspect DISCONNECT on live conn 命中后
  defer→对账①→closed-with-data accept 把未 accept 的 RX 数据救回,
  正是 M3 设计意图(资产门保护)。

验证(全绿):@build 0/0(Code=209580;含 CH395F 套件+Phase13 全量重编
0/0 的编译验证);@test PASS(回读无差异);iec104 18/18(8.3s);
n30 0/50;:21 @0ms 50/50 p50=512;:7003 50/50 p50=517;ftp_regression
11/11;Trap23 RST 5/5 + FIN 5/5;time_sync :8888 连续推送正常(RTC 更新、
无 [ERR])。文档同步:BSD 指南(§3.3/3.6/10.1/10.2/12 + 变更记录)、
函数调用关系、监听生命周期设计说明、IEC104 上行设计说明、模拟器差距
清单、链路延迟测试说明。

门禁说明:doc-links 工作树 junction 伪影按既定口径 SKIP;mem-budget
分支 .map 漂移同前;cppcheck/coding-standard 手跑 Passed。
2026-10-13 06:44:26 +08:00

327 lines
12 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 被控站服务(板端胶水任务)
* 模块功能:2404 监听 + 单连接 socket 收发 + 把字节流喂给 iec104_link 纯逻辑
* 层;对时经 sys_clock_set_unix 落墙钟;数据源走 iec104_pts 默认源
* (phasor / prot_elm / prot_set)。协议语义全部在 App/protocol/iec104,
* 本文件只做 I/O 编排(A-4 约束 1)。
* 线程约定:iec104Task 独占(NET_READY 屏障后循环,100ms 节拍 + 看门狗心跳)。
* 创建日期:2026-10-12(B4)
*/
#include "iec104_serv.h"
#define DBG_LINE_BUF_MAX 128U /* 深链模块日志缓冲收紧(time_sync 先例) */
#define DBG_TAG "[I104]"
#include "iec104_link.h"
#include "iec104_pts.h"
#include "net_socket.h"
#include "prot_set.h" /* 定值脏标志 peek(B7 落盘通知) */
#include "sys_clock.h"
#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 E-04(短写=对端收坏帧) */
#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 调用返回后拆链 */
/* 前向声明(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);
/* V0.1 公共地址固定 1(点表口径;可配置项挂二期) */
static const iec104_link_cfg_t s_cfg = { 1u, s_send_apdu, s_clock_sync, s_epoch_ms };
/*
* 函数功能:link 层发送回调——经 net_send 串行 RPC 发出一个 APDU
* 入口参数:ctx 未用;apdu/len = 完整 APCI 帧
* 返回值:无
* 限定条件:s_client >= 0(在 link 调用栈内执行,即本任务上下文)
* 函数说明:短写/失败累计(E-04 先例);连续 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;
return;
}
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 写回)
* 入口参数:ctx 未用;epoch_ms = C_CS 解析出的 UTC 时刻
* 返回值:0 成功 / -1 失败(link 层回 actcon+PN)
* 函数说明:uint32 秒域上限 2106-02-07,越界显式拒绝(CP56 域到 2127)
*/
static int s_clock_sync(void *ctx, uint64_t epoch_ms) {
uint64_t secs = epoch_ms / 1000ULL;
(void)ctx;
if (secs > 0xFFFFFFFFULL) {
return -1;
}
return sys_clock_set_unix((uint32_t)secs);
}
/*
* 函数功能: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;
}
/*
* 函数功能:创建并监听 2404(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_SERV_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");
}
}
/*
* 函数功能:单次服务循环(代次自愈 → accept → 门控收 → 喂 link → 节拍)
* 返回值:无
* 限定条件:NET_READY 已置位;s_cfg/s_link 已 init
* 函数说明:门控(net_listener_armed/net_can_read 纯 RAM 读)+ 兜底节拍照
* ui_net_sink I-1/I-2;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;
/* N34 阶段1(F-NET-4):门控从 net_can_accept(只认 ESTABLISHED,已退役)
* 迁 net_listener_armed——旧门控把 LISTENING 窗口全挡掉,:2404 的丢
* CONNECT 探针(net_accept_locked 内)因此从不运行(与已修的 :7003
* 同型);armed=ESTABLISHED 待 accept / LISTENING /
* CLOSED+auto_relisten+残留(ESTABLISHED 放行为 2026-10-13 台架补齐:
* 漏它则本任务 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);
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;
}
n = net_recv(s_client, s_rx, (int)sizeof(s_rx), NET_MSG_DONTWAIT);
if (n > 0) {
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();
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_SERV_PORT);
} else {
DBG_INFO("i104: listening on %u", (unsigned)IEC104_SERV_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);
}
osDelay(IEC104_POLL_MS);
}
}