/* * 模块名称: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 /* 服务周期: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); /* V0.1 公共地址固定 1(点表口径;可配置项挂二期) */ static const iec104_link_cfg_t s_cfg = { 1u, s_send_apdu, s_clock_sync }; /* * 函数功能: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); } /* * 函数功能:创建并监听 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_can_accept/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; if (net_can_accept(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); } }