/* * 模块名称:Network Events & Reconcile * 模块功能:芯片边沿事件处理 + 100ms 周期对账(真机专属)—— 本层状态镜像与芯片状态面的 * **唯一收敛实现**:5 个 handle_*_event(net_poll 的分派目标)、 * net_reconcile_tick(形态①②③④⑤/U11 判据)、net_lost_connect_probe * (accept 路径补 accept)。**只置事实与登记意图**:重建监听的**动作**在 * net_listener_tick(net_listener.c),本模块只调 net_listener_request。 * 适用平台:STM32F407ZGTx + CH395F 以太网芯片 * 作者:王建锋 * 创建日期:2026-10-14 * 修改记录: * 2026-10-14 王建锋 从 net_socket.c 抽出(与 net_listener.c 同族模块化):事件处理与 * 周期对账同属"状态面收敛"一个职责,且对账直接调用事件处理器 2 处 * → 合为一个模块,跨模块导出为零。搬移时 4 处 fd 取法替换(其余 * 代码逐行未改):fd 反推 `p_sock - s_net_socks` → `p_sock->ch395_sock` * (net_socket() 里 `ch395_sock = sockfd`,恒等,模拟器同构造); * 对账循环 `&s_net_socks[i]` → `net_sock_raw(i)`。 * ⚠ 随之**收窄**两处沉降登记的范围门 `(qfd >= 0) && (qfd < MAX)` → * `qfd < MAX`:`ch395_sock` 是 uint8_t,下界在类型上不可能越界, * 留着会被 cppcheck 判 `knownConditionTrueFalse` 拦提交。**勿加回**。 * * 设计依据 → docs/网络/网络自愈架构设计.md §4(形态表 §4.1 / 单一所有者 §4.2 / 沉降 §4.3) * 依赖(单向,无反向 include):net_socket.h(net_sock_raw / net_set_errno / NET_ERR_*) * net_events.h(本模块声明)/ net_listener.h(settle / request)/ net_pending.h(形态⑤ * 取消挂起)/ ch395f.h(驱动)/ dbg_log.h / main.h(HAL_GetTick) */ #include #include #include "FreeRTOS.h" #include "task.h" #include "cmsis_os.h" #include "net_socket.h" #include "net_events.h" #include "net_listener.h" #include "net_pending.h" #include "ch395f.h" #include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */ #include "net_latency_test.h" #include "main.h" /* 深调用链模块:与 net_socket.c 同口径收 128 B(最长行渲染后 ≈100 B < 128 → 不截断) */ #define DBG_LINE_BUF_MAX 128U #define DBG_TAG "[NET]" #include "dbg_log.h" /* 模块内前向声明 */ static void fire_event(net_sock_t *p_sock, net_event_t event); #if defined(NET_BENCH_CMD_LOG) /* * 函数功能:台架 Bench 芯片命令序列日志(每条真实 SPI 命令记一行,排障用) * 入口参数:tag - 命令类别 (P=poll全局中断 R=recv S=send C=close * A=accept L=listen N=connect U=relisten M=其他) char * sock - CH395F socket 号 int * detail - 结果/中断状态 int * 出口参数:无 * 返回值:无 * 限定条件:NET_BENCH_CMD_LOG 启用(2026-09-22 排障台架工具,默认关);netTask 上下文 * 函数说明:格式 netcmd: fd= t= d=<距上条命令 tick 间隔> * x=;台架专用,实验后回退 */ static uint32_t s_bench_prev_tick = 0U; static uint8_t s_bench_prev_valid = 0U; void net_bench_cmd_log(char tag, int sock, int detail) { /* 当前 tick(命令时间戳) */ uint32_t now = (uint32_t)xTaskGetTickCount(); /* 距上一条命令的 tick 间隔(首条命令记 0) */ uint32_t delta = (s_bench_prev_valid != 0U) ? (now - s_bench_prev_tick) : 0U; /* 更新"上一条命令"记录,供下次计算间隔 */ s_bench_prev_tick = now; s_bench_prev_valid = 1U; /* 台架日志:命令类别 + fd + tick + 间隔 + 结果/中断状态 */ DBG_INFO("netcmd: %c fd=%d t=%u d=%u x=%d", tag, sock, now, delta, detail); } #endif /* 对账与监听重建合一周期(原 200ms 对账 / 100ms 重建合一) */ #define NET_RECONCILE_PERIOD_MS 100U /* 同一 socket 两次动手的最小间隔(防抖成风暴) */ #define NET_RECONCILE_COOLDOWN_MS 1000U /* 分级去抖门槛(单位=轮) */ #define NET_RECON_STABLE_ACCEPT 1U /* 形态② CONNECT 事件未收到补 accept(幽灵甄别兜底假阳性) */ #define NET_RECON_STABLE_RELISTEN 2U /* 形态③④ 监听失位重建 / ⑤ 静默回收 RESYNC */ #define NET_RECON_STABLE_DEATH 4U /* 形态① 连接已失(M3 让位窗 400ms,勿改小) */ /* * 函数功能:处理 CH395F CONNECT 中断事件(单连接到达 / Client 连接成功) * 入口参数:p_sock - 发生连接事件的 Socket 控制块指针 net_sock_t * * 出口参数:无 * 返回值:无 * 限定条件:net_poll() 检测到 CONNECT 中断后调用;netTask 上下文 * 函数说明:1. 单连接:自身转 ESTABLISHED 并读取对端地址 * 2. TCP Client(CONNECTING):连接成功,读取对端地址并置 ESTABLISHED * 3. 连接成功后使能 KeepAlive 并触发 NET_EVENT_CONNECTED 回调 */ void handle_connect_event(net_sock_t *p_sock) { #if defined(NET_BENCH_CMD_LOG) /* Bench:记 'a'(accept)命令 */ net_bench_cmd_log('a', (int)p_sock->ch395_sock, 0); #endif /* 监听态:有连接到达 */ if (p_sock->state == NET_SOCK_STATE_LISTENING) { /* 单连接:收到 CONNECT 中断 = 自身有连接到达,转为 ESTABLISHED */ DBG_INFO("sock%d standalone accept", p_sock->ch395_sock); /* 对端 IP/端口(6 字节:4B IP + 2B 端口,小端) */ uint8_t remote[6]; /* 回读对端 IP + 端口 */ ch395f_get_remot_ipp(p_sock->ch395_sock, remote); /* 对端 IP 数组(前 4 字节,大端) */ memcpy(p_sock->remote_ip_arr, remote, 4); /* 小端 IP 组装为 32 位值 */ p_sock->remote_ip = ((uint32_t)remote[0]) | ((uint32_t)remote[1] << 8) | ((uint32_t)remote[2] << 16) | ((uint32_t)remote[3] << 24); /* 对端端口(小端 2 字节) */ p_sock->remote_port = (uint16_t)(remote[4] | (remote[5] << 8)); /* 状态 → ESTABLISHED(监听自身转数据通道) */ p_sock->state = NET_SOCK_STATE_ESTABLISHED; /* N-32(P1):新会话开始 —— 清"上一条会话已结束"电平。必须在置 * ESTABLISHED 之后立刻做:否则抢先 relisten 后到新连接的窗口内 * recv 仍会误得 EOF(把新客户端踢开) */ p_sock->session_dead = 0U; /* F-NET-17 M3 前置:清可疑电平——探针(LISTENING 期)残留计数会带进新会话, * 不补此清零则 M3 矛盾判据首轮即被旁路(T6 实测 grep:connect 原不清此字段) */ p_sock->lost_conn_seen = 0U; /* 新开 Socket 发送缓冲按空闲计 */ p_sock->send_ready = 1; /* 清除残留的 TIMEOUT 中断标志 */ /* 读即清:本 Socket 中断状态寄存器 */ (void)ch395f_get_sock_int_status(p_sock->ch395_sock); /* §5.56:TCP 服务器须在 CONNECT 后开启 KeepAlive。2026-10-12 曾加后 * 撤回(当时仅证明"物理断链"已被 +2s TIM_OUT 快速覆盖,其余场景 * 无法注入、未证明);E11(2026-10-13,《网络自愈架构设计》)用"禁用 PC 网卡" * 注入"链路在、对端静默"(NAT/黑洞)补上证据链:生产参数 60/5/3 * 实测 ≈30 s 判死 → TIM_OUT → QUARANTINE → reconciler 重建;对端存活时 * 探测被 ACK、空闲会话不误杀。消费链路现成,按手册位置转正。 */ ch395f_set_keepalive_enable(p_sock->ch395_sock, 1); /* 触发连接建立事件回调 */ fire_event(p_sock, NET_EVENT_CONNECTED); } else if (p_sock->state == NET_SOCK_STATE_CONNECTING) { /* TCP Client 连接成功 */ /* 对端 IP/端口(6 字节:4B IP + 2B 端口,小端) */ uint8_t remote[6]; /* 回读对端 IP + 端口 */ ch395f_get_remot_ipp(p_sock->ch395_sock, remote); /* 对端 IP 数组(前 4 字节) */ memcpy(p_sock->remote_ip_arr, remote, 4); /* 小端 IP 组装为 32 位值 */ p_sock->remote_ip = ((uint32_t)remote[0]) | ((uint32_t)remote[1] << 8) | ((uint32_t)remote[2] << 16) | ((uint32_t)remote[3] << 24); /* 对端端口(小端 2 字节) */ p_sock->remote_port = (uint16_t)(remote[4] | (remote[5] << 8)); /* 状态 → ESTABLISHED */ p_sock->state = NET_SOCK_STATE_ESTABLISHED; /* N-32(P1):新会话开始 —— 清"上一条会话已结束"电平(同 LISTENING * 分支:必须在转 ESTABLISHED 后立刻做,防抢先 relisten 窗口误 EOF) */ p_sock->session_dead = 0U; /* F-NET-17 M3 前置:清可疑电平(同 LISTENING 分支) */ p_sock->lost_conn_seen = 0U; /* 使能 KeepAlive(客户端连接保活) */ ch395f_set_keepalive_enable(p_sock->ch395_sock, 1); /* 触发连接建立事件回调 */ fire_event(p_sock, NET_EVENT_CONNECTED); } } /* * 函数功能:处理 Socket 断开(DISCONNECT)事件,更新 Socket 状态机 * 入口参数:p_sock - 发生断开事件的 Socket 控制块指针 net_sock_t * * 出口参数:无 * 返回值:无 * 限定条件:net_poll() 检测到 DISCONNECT 中断后调用;netTask 上下文 * 函数说明:1. ⚠ FUN_PARA bit3=1(本仓 net_init 置 0x08):DISCONNECT/TIM_OUT 后芯片 * **不**关 Socket、**不清**缓冲(停 0x05/0x00)——残留数据须由本层读出后,再由 * 本层 ch395f_close_socket 关闭;芯片不代劳(§5.52② / §5.23),不得跳过 * 显式 close(U12 成因之一)。之后需重新 OPEN + LISTEN * 2. 单连接:置 CLOSED,交由 net_recv_locked 排空剩余数据后再关 * 3. 触发 NET_EVENT_DISCONNECTED 回调 */ void handle_disconnect_event(net_sock_t *p_sock) { #if defined(NET_BENCH_CMD_LOG) /* Bench:记 'd'(disconnect)命令 */ net_bench_cmd_log('d', (int)p_sock->ch395_sock, 0); #endif /* 由控制块指针反推描述符 */ int sockfd = (int)p_sock->ch395_sock; /* 监听态:DISCONNECT 落在监听器上 */ if (p_sock->state == NET_SOCK_STATE_LISTENING) { /* TRAP-NET-17 / N-6:监听态收到 DISCONNECT——bit3=1(0x08)下芯片**不**关该 Socket(停 0x05/0x00,芯片 TCP 侧已 CLOSED);不重开则此后所有 SYN 全被拒 = 监听永久死亡; * 触发 = 断连后 <~1s 快速复连时 CONNECT 中断未上报(控制块滞留 LISTENING)后该连接断开(2026-09-23 生产固件复现); * N-32 P2:本分支**只回读留痕、不再 relisten** —— 重建统一交 * net_reconcile_tick 形态③(每 100 ms 读驱动缓存,监听失位即重建)。原处直接 * relisten 是 12 个所有者之一,且位于 DISCONNECT 事件路径,可能"抢先" * 于消费者 recv(P1 已消除其后果,P2 消除其可能)。 * ⚠ 本留痕回读保持**主动 SPI**(不用驱动缓存):事件路径上缓存必然滞后 * 于刚到达的中断(刷新在 net_poll 末尾、事件处理之后),仅周期对账可用缓存 */ /* 回读缓冲(芯片状态 2 字节) */ uint8_t st[2] = {0, 0}; /* 回读芯片状态(仅为留痕:重建与否由 reconciler 裁决) */ ch395f_get_socket_status((uint8_t)sockfd, st); /* 监听已失位(芯片已关闭) */ if ((st[0] == CH395F_SOCKET_CLOSED) || (st[1] != CH395F_TCP_LISTEN)) { DBG_INFO("sock%d listen lost (chip sock=0x%02X tcp=0x%02X), reconciler will relisten", sockfd, st[0], st[1]); } else { /* 仍在监听:判陈旧事件丢弃 */ DBG_INFO("sock%d stale listen DISCONNECT ignored (chip tcp=0x%02X)", sockfd, st[1]); } } else if (p_sock->state == NET_SOCK_STATE_ESTABLISHED) { /* 陈旧 DISCONNECT 甄别(D20 修复,2026-09-22 台架实证):同一 fd 被"关闭 → 重新监听 → 再连接"循环复用,上一条连接的 FIN/ACK * 可能在 fd 已复用为新连接**之后**才上报(net_close_locked 排空的中断里还没有它)→ 新连接被误判为断开(实测:FTP 同一会话第二个数据连接必失败 425); * 处置:回读芯片状态,TCP 仍 ESTABLISHED 判陈旧丢弃(已读清不重复上报);真断开时芯片处于 CLOSE_WAIT(0x07)/CLOSED(0x00) 等状态照常处理;超时事件走 handle_timeout_event,不受影响 */ /* 芯片状态回读缓冲(2 字节) */ uint8_t st[2]; /* 回读芯片状态(Socket + TCP) */ ch395f_get_socket_status((uint8_t)sockfd, st); /* 芯片仍 ESTABLISHED:陈旧事件 */ if (st[1] == CH395F_TCP_ESTABLISHED) { /* 留痕:陈旧事件丢弃 */ DBG_ERROR("sock%d stale DISCONNECT ignored (chip TCP still ESTABLISHED)", sockfd); /* 直接返回(中断已读清,不会重复上报) */ return; } /* F-NET-17 M3(自愈专项实验 2026-10-13):矛盾态可疑门——芯片 Socket 仍 OPEN(0x05) * 而 TCP 槽 0x00 且本层 ESTABLISHED = 逻辑矛盾(活连接应 0x04):陈旧 DISCONNECT * (拆链完成滞后锁存,TRAP-NET-12)与"真断开恰被旧拆链覆写槽"在 D20 上不可分辨 * (结构性失明,F-NET-15 Q1)。处置:不吃边沿,置可疑电平返回,交对账① * (ESTABLISHED + 芯片 TCP==CLOSED)电平去抖再入本函数(对账与重建合一 * 后=4×100ms,NET_RECON_STABLE_DEATH,让位租约 400ms 不变 + 冷却): * flag==1 旁路本门 → 正常释放;连接真活则租约内 * 会话正常完成(风暴链掐断,E9a n30 0/50 实证)。 * 零额外 SPI 于常规路径(st[] 已读;recv_len 仅可疑分支多读一条)。 * ⚠ E9a/E9b 实测两轮收窄:①必须含 recv_bytes==0——已读数据的会话遇 * 0x05/0x00 多为真死(探针控制通道教训);②必须含芯片 RX>0——延迟保护的 * 本质是"在途投递不可毁"(closed-with-data 救回同源红线:RX 有数据=消费者 * 资产)。@0ms 快复连(连上即关、零数据,E9b :21 46/50 教训)RX 空 → 立即 * 释放,误杀也无数据损失,后到数据由救回路径接住。边界:滞后锁存杀 * "已读数据或 RX 空"会话的形态不在本门保护域(E8 窗口未出现该受害形态)。 * 零误伤:健康 FIN=0x07、静默回收=0x00/0x00 均不命中(E5 20/20 样本) */ if ((st[0] == CH395F_SOCKET_OPEN) && (st[1] == CH395F_TCP_CLOSED) && (p_sock->lost_conn_seen == 0U) && (p_sock->recv_bytes == 0U) && (ch395f_get_recv_len((uint8_t)sockfd) > 0U)) { /* 置可疑电平(复用探针字段,零 RAM;connect/relisten 清零) */ p_sock->lost_conn_seen = 1U; /* 留痕(INFO 级:这是预期自愈路径,非错误,勿污染 [ERR] 台架判据) */ DBG_INFO("sock%d suspect DISCONNECT on live conn (chip 0x%02X/0x%02X), defer to reconcile", sockfd, st[0], st[1]); /* 不释放、不登记沉降、不动 session_dead——保留 400ms 租约窗口 */ return; } /* 单连接:先不关 CH395F Socket, * 留给 net_recv_locked 把剩余数据读出来再关,置 CLOSED 等待应用重新监听 */ DBG_INFO("sock%d DISCONNECTED, releasing (chip st=0x%02X/0x%02X)", p_sock->ch395_sock, st[0], st[1]); /* TRAP-NET-18 推广(2026-09-24 台架实证):正常 DISCONNECT 释放留下的芯片收尾状态 * 会在复用同一 fd 的下一条连接上以"伪 DISCONNECT"形式投递(实测 accept 后约 10~20ms)→ 该连接被提前释放,应用把已到的 2048B 当整份文件并回报 226; * 仅**本连接收过数据**时登记沉降(实测触发源 = 收过数据的数据通道被关闭),纯连接即关/只发不收的连接免沉降,保住 TRAP-NET-17 的快速复连时序(:21 @0ms ×50 全通过) */ /* F-NET-15 M1(自愈专项实验 2026-10-12):沉降**无条件**登记。原 recv_bytes!=0 门的 * 受害者由事件时序定义(accept 后 ~20ms 被陈旧 DISCONNECT 杀 → recv_bytes==0 是 * 构造性必然),门对"残渣循环自持"形态失效(n30_repro 背靠背 9/10 失败,E1 实测); * 门原保护目标(:21 @0ms 快复连)已被 N-32 单一重建入口延迟化(恢复窗实测 0.3~1s)架空, * +100ms 沉降落在窗内不可见。沉降窗口同时让残渣 DISCONNECT 在 state==CLOSED * (无害、仅留痕)时被消费,断开"杀下一条 → 新拆链 → 再杀"的繁殖链 */ { /* fd 取自 uint8_t ch395_sock → 恒非负,只需上界门 */ int qfd = (int)p_sock->ch395_sock; if (qfd < NET_MAX_SOCKETS) { /* 登记优雅沉降(NET_RELISTEN_SETTLE_GRACEFUL) */ net_listener_settle(qfd, NET_LISTENER_SETTLE_GRACEFUL); } } /* 正常释放(对端 FIN):断连时挂起项以 EOF(0) 结束 */ p_sock->released_abnormal = 0U; /* N-32(P1 不变量 I):置"本会话已结束"电平。须在置 CLOSED **之前**, * 使 session_dead 覆盖"CLOSED 等待 recv 排空"整个窗口——排空期间 * recv 的 DONTWAIT 快路径先返数据(recv_len>0 优先),数据尽后判据 * 落到本标志 → 仍得 EOF,不因排空慢而漏报 */ p_sock->session_dead = 1U; /* N-30(TRAP-NET-20):登记本连接对端端口(幽灵态甄别基准,须在状态 * 迁移前记录——remote_port 此刻仍有效,relisten 清零) */ p_sock->last_peer_port = p_sock->remote_port; /* 状态 → CLOSED */ p_sock->state = NET_SOCK_STATE_CLOSED; /* 触发断连事件回调 */ /* 触发断连事件回调 */ fire_event(p_sock, NET_EVENT_DISCONNECTED); /* 其他状态:未预期的断开 */ } else if (p_sock->state != NET_SOCK_STATE_CLOSED && p_sock->state != NET_SOCK_STATE_BOUND && p_sock->state != NET_SOCK_STATE_CREATED) { /* 未预期的断开:记错误日志 */ DBG_ERROR("sock%d disconnect in state %d", p_sock->ch395_sock, p_sock->state); /* 触发断连事件回调(统一) */ fire_event(p_sock, NET_EVENT_DISCONNECTED); } } /* * 函数功能:处理 Socket 数据接收(RECV_OK)事件 * 入口参数:p_sock - 发生接收事件的 Socket 控制块指针 net_sock_t * * 出口参数:无 * 返回值:无 * 限定条件:net_poll() 检测到 RECV_OK 中断后调用;netTask 上下文 * 函数说明:1. 仅触发 NET_EVENT_DATA_RECEIVED 回调通知上层 * 2. 不在此读取 recv_len:单一读取点归 net_recv_locked(省一次 SPI 查询) */ void handle_recv_event(net_sock_t *p_sock) { /* RECV_OK 中断已触发,直接通知上层。不在此读 recv_len, * 单一读取点在 net_recv_locked(省 SPI;原"部分读归零"顾虑已降级) */ /* 置可读标志,供 net_select 在无 SPI 访问下判读 */ p_sock->readable = 1; /* 触发数据到达事件回调 */ fire_event(p_sock, NET_EVENT_DATA_RECEIVED); } /* * 函数功能:处理 Socket 发送完成(SEND_OK)事件 * 入口参数:p_sock - 发生发送完成事件的 Socket 控制块指针 net_sock_t * * 出口参数:无 * 返回值:无 * 限定条件:net_poll() 检测到 SEND_OK 中断后调用;netTask 上下文 * 函数说明:触发 NET_EVENT_SEND_COMPLETE 回调通知上层 */ void handle_send_event(net_sock_t *p_sock) { /* 触发发送完成事件回调 */ fire_event(p_sock, NET_EVENT_SEND_COMPLETE); } /* * 函数功能:处理 Socket 超时(SOCK_TIMEOUT)事件,更新 Socket 状态机 * 入口参数:p_sock - 发生超时事件的 Socket 控制块指针 net_sock_t * * 出口参数:无 * 返回值:无 * 限定条件:net_poll() 检测到 SOCK_TIMEOUT 中断后调用;netTask 上下文 * 函数说明:1. TCP Client(CONNECTING):连接超时,回到 CREATED 状态 * 2. 已建立连接:回到 CLOSED(单连接) * 3. 监听态:异常超时,记录错误日志 * 4. 触发 NET_EVENT_TIMEOUT 回调 */ void handle_timeout_event(net_sock_t *p_sock) { #if defined(NET_BENCH_CMD_LOG) /* Bench:记 'x'(timeout)命令 */ net_bench_cmd_log('x', (int)p_sock->ch395_sock, 0); #endif /* 连接中状态:连接超时 */ if (p_sock->state == NET_SOCK_STATE_CONNECTING) { /* TCP Client 连接超时 */ DBG_ERROR("sock%d CONNECT TIMEOUT", p_sock->ch395_sock); /* 状态 → CREATED(客户端槽位回可复用) */ p_sock->state = NET_SOCK_STATE_CREATED; } else if (p_sock->state == NET_SOCK_STATE_ESTABLISHED) { /* 已建立连接超时 */ DBG_ERROR("sock%d TIMEOUT", p_sock->ch395_sock); /* TRAP-NET-18:异常释放(TIMEOUT,例如客户端中止上传)→ 复监听前隔离, * 等对端在途数据落尽并排空 RX 残渣(详见 net_relisten_locked 区块注释) */ { /* fd 取自 uint8_t ch395_sock → 恒非负,只需上界门 */ int qfd = (int)p_sock->ch395_sock; if (qfd < NET_MAX_SOCKETS) { /* 登记异常释放隔离(重监听前等待在途数据落尽 + RX 排空) */ net_listener_settle(qfd, NET_LISTENER_SETTLE_QUARANTINE); } } /* TRAP-NET-18 待办 #3:标记异常释放——挂起 recv 排空断连后以 * NET_ERR_CONNRESET 结束(而非 EOF),应用(lftpd STOR)回报 5xx * 而非误报 226(见 net_pending_tick 断连分支) */ p_sock->released_abnormal = 1U; /* N-30(TRAP-NET-20):异常释放同样登记对端端口(幽灵甄别基准) */ p_sock->last_peer_port = p_sock->remote_port; /* N-32(P1 不变量 I):异常结束同样是"本会话已结束"电平(TIMEOUT/RST * 已在下方 requarantine,本标志只表达"会话结束了"这一事实,与 * released_abnormal(结束得好不好看)正交) */ p_sock->session_dead = 1U; /* 状态 → CLOSED */ p_sock->state = NET_SOCK_STATE_CLOSED; } else if (p_sock->state == NET_SOCK_STATE_LISTENING) { /* 监听超时(异常情况) */ DBG_ERROR("sock%d LISTEN TIMEOUT", p_sock->ch395_sock); } /* 触发超时事件回调(统一) */ fire_event(p_sock, NET_EVENT_TIMEOUT); } /* * 函数功能:触发 Socket 事件回调(若已注册) * 入口参数:p_sock - 触发事件的 Socket 控制块指针 net_sock_t * * event - 事件类型 net_event_t * 出口参数:无 * 返回值:无 * 限定条件:net_init() 已调用 * 函数说明:1. 仅当 p_sock->event_cb 非空时调用回调 * 2. 回调参数为 ch395_sock、event、event_arg */ static void fire_event(net_sock_t *p_sock, net_event_t event) { /* 已注册回调 */ if (p_sock->event_cb != NULL) { /* 调用回调(ch395_sock、event、用户参数) */ p_sock->event_cb(p_sock->ch395_sock, event, p_sock->event_arg); } } /* * 函数功能:监听等待期的芯片状态轮询探针——芯片已 ESTABLISHED 而 CONNECT 事件 * 未收到时补做 accept(复用 CONNECT 事件簿记路径)。 * 入口参数:p_sock - 处于 LISTENING 的监听控制块 net_sock_t * * 出口参数:无(命中时 p_sock->state 经 handle_connect_event 转 ESTABLISHED) * 返回值:无 * 限定条件:netTask 上下文(net_accept_locked 内调用,其开头已 net_poll(), * 驱动缓存新鲜度有保证) * 函数说明:1. F-MS-9:判据改读**驱动状态缓存**(零 SPI;唯一 * 刷新点=net_poll),**探针去抖删除**——原去抖防的两类瞬态已 * 结构性消除:"刚 listen 尚未生效"显示 TCP=00 永不命中 ESTABLISHED * 门(E2:LISTEN 可见 100% <=10ms 无长尾);拆链窗假 ESTABLISHED * 由幽灵甄别拦截。NET_LOST_CONN_STABLE_N 随之下线 * 2. 幽灵态(对端端口 = last_peer_port)不动手,计数清零 */ void net_lost_connect_probe(net_sock_t *p_sock) { /* 对端 IP+端口回读缓冲(4B IP + 2B 端口小端) */ uint8_t remote[6] = {0, 0, 0, 0, 0, 0}; /* 驱动状态缓存(net_poll 每轮刷新;本探针入口前 accept 已 net_poll()) */ ch395f_sock_state_t c = ch395f_sock_state_cached(p_sock->ch395_sock); /* 芯片尚未完成握手:可疑电平计数清零,本轮无动作 */ if (c.tcp_state != CH395F_TCP_ESTABLISHED) { p_sock->lost_conn_seen = 0U; return; } /* 已 ESTABLISHED:回读对端端口做幽灵甄别(按需 SPI,仅命中态发) */ ch395f_get_remot_ipp(p_sock->ch395_sock, remote); /* 对端端口(小端 2 字节) */ uint16_t peer = (uint16_t)(remote[4] | (remote[5] << 8)); /* 幽灵甄别:仅"端口非零且 = 上一条连接端口"才判幽灵拦截; * peer==0(芯片对端端口提交滞后,TRAP-NET-13 同类)不拦截——宁可补 accept * 也不退回事件未收口的死等(2026-10-11 验证轮实测:peer==0 拦截把首连接 * 事件未收到的原有对账救援也挡死,LIST#1 卡满 20s,比修复前更糟) */ if ((peer != 0U) && (peer == p_sock->last_peer_port)) { p_sock->lost_conn_seen = 0U; return; } /* F-MS-9:去抖已删(判据读缓存,两类瞬态结构性消除, * 见函数头注释 1);lost_conn_seen 仅作 M3 可疑电平标志用,动手前清零 */ p_sock->lost_conn_seen = 0U; /* 留痕:事件未收到自愈命中 */ DBG_INFO("sock%d lost-CONNECT self-accept (peer=%u)", (int)p_sock->ch395_sock, peer); /* 复用 CONNECT 事件的簿记路径(转 ESTABLISHED + 记对端 + 事件回调) */ handle_connect_event(p_sock); } /* * 函数功能:芯片/本层 socket 状态周期对账(TRAP-NET-17 事件面未收到自愈,E14 复核:非芯片丢事件是被读清消费)+ 监听器重建 —— 合并为单一 100ms tick(docs/网络/网络自愈架构设计.md §4.1) * 入口参数:无 * 出口参数:无 * 返回值:无 * 限定条件:netTask 上下文(由 net_process_messages 每轮调用) * 函数说明:1. 芯片快速复连会收不到 CONNECT/DISCONNECT 事件(读清消费竞态,E14 复核:非芯片丢事件),本层无法靠事件自愈 * 2. 判据读**驱动状态缓存**(唯一刷新点=net_poll 每轮全量刷新,E2 实测单次回读 18.1us) * 3. 覆盖形态(原对账①②⑤ + 原 pump③④/U11): * ① 本层 ESTABLISHED 而芯片已无连接 → 按断开处理(只置事实) * ② 本层 LISTENING 而芯片已完成握手 → 补做 standalone accept(命中时 * 才回读对端端口做幽灵甄别——本 tick 唯一按需 SPI) * ⑤(F-NET-2)本层 ESTABLISHED 而芯片静默回收回 LISTEN(0x05/0x01 无 * DISCONNECT)→ RESYNC 只修本层 * ③ 本层 LISTENING(监听型 fd)而芯片监听失位 → 重建 * ④ 本层 CLOSED(监听型 fd)而芯片不在 LISTEN → 重建 * U11 本层 CLOSED(监听型 fd)而芯片在听 → 只重同步,**不发芯片命令** * (重下发因 USE 错误关掉正听着的 Socket,U12 同源事故) * 4. 去抖分级(100ms/轮;E2:LISTEN 可见 100% <=10ms 无长尾):②=1 轮 * (幽灵甄别挡拆链窗的旧连接 ESTABLISHED);③④⑤=2 轮(2026-09-23 实证: * 无去抖时监听被反复重建直至放弃);①=4 轮(**勿改小**:F-NET-17 M3 * 可疑电平让位再入,400ms 让位窗=在途投递不可毁的最后租约,E9a 实证) * 5. 动手后冷却 NET_RECONCILE_COOLDOWN_MS 防风暴(U11 被动重同步不占) * 6. 链路态订阅(F-DRV-13):驱动 link 变化计数变化 → 留痕(不自动重配) */ void net_reconcile_tick(void) { /* 上次对账轮的时刻(tick) */ static uint32_t s_last = 0U; /* 连续失配计数(去抖) */ static uint8_t s_bad[NET_MAX_SOCKETS]; /* 冷却到期时刻 */ static uint32_t s_cool[NET_MAX_SOCKETS]; /* 当前 tick */ uint32_t now = HAL_GetTick(); /* Socket 循环索引 */ int i; /* 未到对账周期(100ms):直接返回(缓存刷新在 net_poll,与本函数无关) */ if ((uint32_t)(now - s_last) < NET_RECONCILE_PERIOD_MS) { return; } /* 更新本轮起始时刻 */ s_last = now; /* 链路态订阅(F-DRV-13):net_poll 已喂驱动,这里只看计数变化 */ { /* 链路变化计数订阅基线(0xFFFF=未建档) */ static uint16_t s_link_last = 0xFFFFU; uint16_t lc = ch395f_link_change_count(); if (s_link_last != lc) { if (s_link_last != 0xFFFFU) { DBG_INFO("net: PHY link %s (changes=%u)", ch395f_link_up_cached() ? "up" : "down", (unsigned)lc); } s_link_last = lc; } } for (i = 0; i < NET_MAX_SOCKETS; i++) { /* 取控制块(直接数组下标,无需越界判断) */ net_sock_t *p_sock = net_sock_raw(i); /* 驱动状态缓存(net_poll 每轮刷新,本 tick 读到的新鲜度 <=1ms) */ ch395f_sock_state_t c = ch395f_sock_state_cached((uint8_t)i); /* 芯片当前是否在 LISTEN(二段关闭中间态 05/00 不算在听) */ int chip_listening = ((c.open != CH395F_SOCKET_CLOSED) && (c.tcp_state == CH395F_TCP_LISTEN)); /* 监听型 fd 门(原 pump 认领口径:auto_relisten 是决定性门——少了它, * "本层 CLOSED + 已绑端口"的出向连接会被误判为监听器并被 relisten) */ int listener_fd = ((p_sock->auto_relisten != 0U) && (p_sock->type == NET_SOCK_STREAM) && (p_sock->local_port != 0U)); /* 失同步类型:1=连接已失 2=握手已成事件未收到 3=监听失位 4=孤儿监听 * 5=芯片静默回收回监听(编号沿用历史口径,U11 重同步不设编号——被动零动作) */ int bad = 0; /* 本形态去抖门槛(默认按重建档) */ uint8_t stable_need = NET_RECON_STABLE_RELISTEN; if (p_sock->in_use == 0U) { s_bad[i] = 0U; continue; } if (p_sock->state == NET_SOCK_STATE_ESTABLISHED) { /* ① 本层 ESTABLISHED 而芯片已无连接 → 按断开处理 */ if ((c.open == CH395F_SOCKET_CLOSED) || (c.tcp_state == CH395F_TCP_CLOSED)) { bad = 1; stable_need = NET_RECON_STABLE_DEATH; } else if ((c.open != CH395F_SOCKET_CLOSED) && (c.tcp_state == CH395F_TCP_LISTEN)) { /* ⑤(F-NET-2 缺口):芯片静默回收——本层 ESTABLISHED * 而芯片已自行把 Socket 收回 LISTEN(0x05/0x01,**不发 DISCONNECT * 中断**,run8 台架实证)→ RESYNC 语义 */ bad = 5; } } else if (p_sock->state == NET_SOCK_STATE_LISTENING) { if (c.tcp_state == CH395F_TCP_ESTABLISHED) { /* ② 本层 LISTENING 而芯片已完成握手(CONNECT 事件未收到)→ 补做 accept * N-30(TRAP-NET-20)幽灵态甄别:拆链收尾窗口内旧连接未拆净,状态如实 * 显示 ESTABLISHED(E15:bit3=1 保留现场契约 + TCP 拆链状态机,非谎报), * 回读对端端口 = 上一条连接端口 → 不补 accept(补了会把数据发给 * 已死的旧连接 = 生产 LIST#4 数据被吞的直接成因),等拆净或新 SYN。 * 这是本 tick 唯一的按需 SPI(仅形态②命中时发) */ uint8_t remote[6] = {0, 0, 0, 0, 0, 0}; ch395f_get_remot_ipp((uint8_t)i, remote); uint16_t peer = (uint16_t)(remote[4] | (remote[5] << 8)); /* 仅"端口非零且 = 上一条连接端口"判幽灵拦截;peer==0 不拦截 * (对端端口提交滞后时不得挡掉事件未收口的救援,见 net_lost_connect_probe 注释) */ if (!((peer != 0U) && (peer == p_sock->last_peer_port))) { bad = 2; stable_need = NET_RECON_STABLE_ACCEPT; } } else if (!chip_listening && listener_fd) { /* ③ 本层 LISTENING 而芯片监听失位 → 重建(原 pump①) */ bad = 3; } } else if ((p_sock->state == NET_SOCK_STATE_CLOSED) && listener_fd) { if (chip_listening) { /* U11:本层 CLOSED 而芯片在听 = 本层状态滞后 → 只重同步。 * **不发任何芯片命令**(重下发 open+listen 会因 USE 错误走进 * 关闭分支、把正听着的 Socket 关掉,U12 同源事故)。 * 被动修正零风险:不去抖、不占冷却(原 pump 同口径) */ DBG_INFO("sock%d reconcile: chip listening, resync CLOSED->LISTENING (U11)", i); p_sock->state = NET_SOCK_STATE_LISTENING; /* N-30(TRAP-NET-20):死连接对端端口转登幽灵甄别基准 + 探针计数清零 * —— 芯片监听器未被重开,芯片侧对端信息可能仍是上一条连接的, * 必须转登为基准,否则形态② 的幽灵拦截会失效 */ p_sock->last_peer_port = p_sock->remote_port; p_sock->lost_conn_seen = 0U; s_bad[i] = 0U; continue; } /* ④ 本层 CLOSED(会话已结束)而芯片不在 LISTEN → 重建(原 pump②) */ bad = 4; } else { /* 其余状态(CREATED/BOUND/UDP_OPEN 等)不归本 tick 认领 */ s_bad[i] = 0U; continue; } /* 无失同步:清零计数,转下一 socket */ if (bad == 0) { s_bad[i] = 0U; continue; } /* 累加连续失配计数(封顶至本形态门槛) */ if (s_bad[i] < stable_need) { s_bad[i]++; } /* 去抖未满或处于冷却期:本轮不动手 */ if ((s_bad[i] < stable_need) || ((int32_t)(now - s_cool[i]) < 0)) { continue; } /* 动手前:清失配计数并置冷却到期时刻(1s) */ s_bad[i] = 0U; s_cool[i] = now + NET_RECONCILE_COOLDOWN_MS; if (bad == 1) { /* 类型①:按断开处理(应用读到 EOF 后由形态③④重建监听) */ DBG_INFO("sock%d reconcile: chip closed (0x%02X/0x%02X) -> disconnect", i, c.open, c.tcp_state); /* 复用断开状态机(含陈旧事件甄别 + 状态迁移 + 事件派发); * 只置事实,重建由本 tick 形态③④统一做(N-32 P2 单一所有者) */ handle_disconnect_event(p_sock); } else if (bad == 5) { /* 类型⑤:静默回收 → RESYNC 语义(原 NET_MSG_RESYNC_LISTEN 分派体 * 原样并入;纪律同 U11:**不发任何芯片命令**—— * 重下发 close/重监听会摧毁芯片侧健康监听器 → 端口永拒, * run8 实证 170s 拒绝) */ DBG_INFO("sock%d reconcile: chip re-listened w/o DISCONNECT (0x%02X/0x%02X) -> resync", i, c.open, c.tcp_state); /* 仅修 MCU 侧;新连接的 CONNECT 中断会把两侧重新同步 */ p_sock->state = NET_SOCK_STATE_LISTENING; /* N-30(TRAP-NET-20):死连接对端端口转登幽灵甄别基准 + 探针计数清零 */ p_sock->last_peer_port = p_sock->remote_port; p_sock->lost_conn_seen = 0U; /* 取消本 fd 在死连接上的挂起 recv(必通知,防调用方挂死) */ net_pending_cancel(i, NET_ERR_CONNRESET); /* 挂起 send 同样取消(会话已被芯片收走,剩余字节不可能送达) */ net_send_pending_cancel(i, NET_ERR_CONNRESET); } else if ((bad == 3) || (bad == 4)) { /* 类型③④:监听失位/孤儿监听 → **登记**重建请求(判据在此,动作在 * net_listener_tick 一处;非阻塞,30ms 节奏重试) */ DBG_INFO("sock%d reconcile: chip not listening (0x%02X/0x%02X, was state=%d) -> relisten", i, c.open, c.tcp_state, (int)p_sock->state); net_listener_request(i, NET_LISTENER_REQ_RECON); } else { /* 类型②:补做 standalone accept */ DBG_INFO("sock%d reconcile: connection completed w/o CONNECT irq -> accept", i); /* 复用 CONNECT 事件的簿记路径 */ handle_connect_event(p_sock); } } }