diff --git a/App/sync/time_sync.c b/App/sync/time_sync.c index 2a043c3e..00351afd 100644 --- a/App/sync/time_sync.c +++ b/App/sync/time_sync.c @@ -179,6 +179,12 @@ static void time_sync_handle_conn(int conn_sock) { sys_clock_set_unix(epoch); DBG_INFO("time_sync: wall clock updated (epoch=%lu)", (unsigned long)epoch); } + } else { + /* N34 阶段 3(E-02 收口,实验方案 E13 §5):本层放弃会话 → 主动拆除。 + * 挂起但 TCP 仍活的客户端(PC 内核自动 ACK keepalive,芯片僵尸判不死) + * 会永久占住单连接监听;EOF 路径 drop 幂等返 0(net 层已 relisten)。 + * 模拟器多连接后端 conn≠ls,由 sim 实现按数据 fd 关闭。 */ + (void)net_drop_conn(conn_sock); } /* N34 阶段1-5:原 net_close(conn_sock) 已删——单连接模式 conn==监听 fd, diff --git a/Drivers/BSP/NET/README.md b/Drivers/BSP/NET/README.md index 90423db7..81692380 100644 --- a/Drivers/BSP/NET/README.md +++ b/Drivers/BSP/NET/README.md @@ -20,13 +20,17 @@ - 一期(RECV)已落地:`net_recv` 等支持带超时的阻塞(`pending` 挂起 + tick 唤醒) - 二期(ACCEPT + SEND)待做 → `docs/任务规划/登记簿.md` N-4 -### 周期对账自愈(Trap 22 修复) +### 周期对账自愈(Trap 22 修复;N34 阶段 3 合并 pump) -- `net_reconcile_tick()`(200ms)回读芯片状态修正三类失同步: +- `net_reconcile_tick()`(100ms,N34 阶段 3 起判据读**驱动状态缓存**——唯一 + 刷新点 `net_poll` 每轮全量刷新,E2 实测单次回读 18.1us)修正失同步: 本层 ESTABLISHED 而芯片已断 → 断开;本层 LISTENING 而芯片已握手 → 补 accept; - 本层 LISTENING 而芯片监听消失 → 重建 -- **去抖(连续 2 次)+ 冷却(1s)**——单次状态读的重建动作**必须去抖**, - 否则"刚 listen 未生效"瞬态会反复重建直至 `relisten give up`(三监听口全死) + 本层 LISTENING/CLOSED(监听型 fd)而芯片监听消失 → 重建(原 pump③④); + 本层 CLOSED 而芯片在听 → 只重同步(U11);芯片静默回收 → RESYNC(⑤) +- **分级去抖(②=1 轮 / ③④⑤=2 轮 / ①=4 轮)+ 冷却(1s)**——监听失位的重建 + 动作**必须去抖**,否则"刚 listen 未生效"瞬态会反复重建直至 + `relisten retry budget exhausted`(三监听口全死);①的 4 轮窗是 F-NET-17 M3 + 可疑电平的让位租约(勿改小) ### 异常复监听隔离(Trap 23 修复) diff --git a/Drivers/BSP/NET/net_listener.c b/Drivers/BSP/NET/net_listener.c index 0dbfd900..86925820 100644 --- a/Drivers/BSP/NET/net_listener.c +++ b/Drivers/BSP/NET/net_listener.c @@ -2,12 +2,14 @@ * 模块名称:Network Listener Lifecycle * 模块功能:监听器生命周期(真机专属)—— 维持 fd 处于 LISTEN 态的全部机制: * 重建(open+listen)、重建前的沉降/隔离(Trap 23)、排空 RX 残渣、 - * D10 非阻塞重试、周期性自愈 pump(唯一的周期性重建决策者)。 + * D10 非阻塞重试(周期性重建决策者=N34 阶段 3 合并后的 + * net_reconcile_tick,见 net_socket.c)。 * 适用平台:STM32F407ZGTx + CH395F 以太网芯片 * 作者:王建锋 * 创建日期:2026-10-12 * 修改记录: * 2026-10-12 王建锋 从 net_socket.c 整体抽出监听生命周期(N-32 §10 模块化) + * 2026-10-13 王建锋 N34 阶段 3:pump 并入 net_reconcile_tick(实施方案 §5 项 1) * * 职责与边界见 net_listener.h 头注释;设计依据见 * docs/子系统设计/网络Socket监听生命周期设计说明.md §10。 @@ -15,7 +17,7 @@ * 模块内分层: * ① 策略常量与私有状态(s_relisten_*) * ② 机制:net_drain_chip_rx(排空)/ net_relisten_locked(重建一次) - * ③ 对外接口:settle / try / clear / retry_tick / pump + * ③ 对外接口:settle / try / clear / retry_tick * * 依赖(全部为单向,无反向 include): * net_types.h(net_sock_t)/ net_socket.h(net_get_sock / NET_MAX_SOCKETS / @@ -285,7 +287,8 @@ static int net_relisten_locked(int sockfd) { * 出口参数:无 * 返回值:无 * 限定条件:netTask 上下文;由 handle_disconnect_event / handle_timeout_event 调用 - * 函数说明:1. 只置事实,不做动作 —— 重建由 net_listener_pump / net_listener_try 负责 + * 函数说明:1. 只置事实,不做动作 —— 重建由 net_reconcile_tick(形态③④)/ + * net_listener_try 负责 * 2. 越界 sockfd 静默忽略(与旧直接写 s_relisten_quarantine 行为一致) */ void net_listener_settle(int sockfd, net_listener_settle_t level) { @@ -293,7 +296,7 @@ void net_listener_settle(int sockfd, net_listener_settle_t level) { if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { return; } - /* 登记沉降/隔离等级 —— 只置事实,重建由 pump / try 负责 */ + /* 登记沉降/隔离等级 —— 只置事实,重建由 reconciler / try 负责 */ s_relisten_quarantine[sockfd] = (uint8_t)level; } @@ -399,7 +402,7 @@ int net_listener_try(int sockfd) { * 2. 本 fd 有在途挂起 recv 时跳过(net_pending_active,L3 保险: * 重开 Socket 会摧毁在途连接) * 3. 超过 NET_RELISTEN_DEADLINE_MS 停止 D10 快速重试并留痕 - * (监听不永久失效——pump 100ms 周期仍按"芯片不在听"认领重建) + * (监听不永久失效——reconciler 100ms 周期仍按"芯片不在听"认领重建) */ void net_listener_retry_tick(void) { /* 当前 tick */ @@ -414,7 +417,7 @@ void net_listener_retry_tick(void) { } /* 0-4(N34 阶段0)状态门:仅本层仍 LISTENING/CLOSED 才重试。若已转 * ESTABLISHED(新会话抢先到达),在途重试登记即陈旧——继续跑会 - * open→close 摧毁活连接(认领口径与 pump 一致,见 net_listener_pump) */ + * open→close 摧毁活连接(认领口径与 reconciler 形态③④ 一致) */ { /* 取控制块 */ const net_sock_t *p_sock = net_get_sock(i); @@ -438,9 +441,10 @@ void net_listener_retry_tick(void) { if ((int32_t)(now - s_relisten_dead[i]) >= 0) { /* 清重试状态 */ net_listener_clear(i); - /* D10 快速重试预算耗尽留痕:后续交 pump 周期接管(N-32 单一重建 - * 入口),监听不会永久失效——旧文案"give up"夸大了失效语义 */ - DBG_ERROR("sock%d relisten retry budget exhausted, pump takes over", i); + /* D10 快速重试预算耗尽留痕:后续交 reconciler 形态③④ 周期接管 + * (N-32 单一重建入口),监听不会永久失效——旧文案"give up" + * 夸大了失效语义 */ + DBG_ERROR("sock%d relisten retry budget exhausted, reconciler takes over", i); continue; } /* 重试成功 */ @@ -456,124 +460,7 @@ void net_listener_retry_tick(void) { } } -/* 监听器 pump 周期(对账 200ms 的 1/2:比它快、又不至于像 50ms 那样 - * 频繁 close/open 芯片 Socket 与消费者收发撞车——2026-10-12 台架实测 - * 50ms 会把 uiTask 最坏单轮从 172ms 推到 1034ms) */ -#define NET_LISTENER_PUMP_PERIOD_MS 100U -/* pump 去抖与冷却(沿用原对账③④ 的实测结论,取值不变): - * 2026-09-23 实证——无去抖时监听被反复重建直至放弃 */ -#define NET_LISTENER_STABLE_N 2U /* 连续失配次数(去抖,≈200ms) */ -#define NET_LISTENER_COOLDOWN_MS 1000U /* 同一 socket 两次动手的最小间隔(防抖成风暴) */ - -/* - * 函数功能:监听器 pump(N-32 P2「单一所有者」)—— 本层**唯一**的周期性 - * 重建监听决策者 - * 入口参数:无 - * 出口参数:无 - * 返回值:无 - * 限定条件:netTask 上下文(由 net_process_messages 每轮调用) - * 函数说明:1. 设计意图(设计文档 §4 不变量 II):其它所有位置只**置事实** - * (session_dead / 状态迁移 / 事件派发),不**做动作**——于是 - * "谁先看到 DISCONNECT 就谁重建"这类竞争在结构上消失 - * 2. 覆盖三种情形(前两种原属对账③④,第三种为 U11 缺口): - * ① 本层 LISTENING 而芯片监听已失位 → 重建(原对账③) - * ② 本层 CLOSED(会话已结束)而芯片不在 LISTEN → 重建(原对账④) - * ③ 本层 CLOSED 而芯片**正在** LISTEN → **只做状态重同步** - * (U11):芯片监听器是健康的,重下发 open+listen 会因 USE - * 错误走进关闭分支、把正听着的 Socket 关掉(U12 同源事故); - * 只把本层状态收回 LISTENING,**不发任何芯片命令** - * 3. 去抖/冷却沿用对账的实测结论(2026-09-23 实证:无去抖时监听被 - * 反复重建直至放弃) - * 4. session_dead **不在此清零**(与 net_relisten_locked 同一契约: - * 重建监听 ≠ 应用已确认会话结束;清零只发生在 handle_connect_event) - */ -void net_listener_pump(void) { - /* 上次轮次时刻 */ - 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; - - /* 未到 pump 周期:直接返回 */ - if ((uint32_t)(now - s_last) < NET_LISTENER_PUMP_PERIOD_MS) { - return; - } - s_last = now; - - for (i = 0; i < NET_MAX_SOCKETS; i++) { - /* 取控制块 */ - net_sock_t *p_sock = net_get_sock(i); - /* 芯片侧状态两字节 */ - uint8_t st[2] = {0, 0}; - /* 芯片当前是否在 LISTEN */ - int chip_listening; - - /* 控制块无效:跳过 */ - if (p_sock == NULL) { - s_bad[i] = 0U; - continue; - } - - /* 认领门槛:仅监听型 fd(在用 + 自动重监听 + TCP + 已绑端口)。 - * auto_relisten 是决定性门:少了它,"本层 CLOSED + 已绑端口"的出向 - * 连接(板级 Phase 7 TC701~703 用 net_connect 绑本地口)会被误判为 - * 监听器并被 relisten(同对账④的既定口径) */ - if ((p_sock->in_use == 0U) || - (p_sock->auto_relisten == 0U) || - (p_sock->type != NET_SOCK_STREAM) || - (p_sock->local_port == 0U)) { - s_bad[i] = 0U; - continue; - } - /* 只处理与监听生命周期相关的两个状态 */ - if ((p_sock->state != NET_SOCK_STATE_LISTENING) && - (p_sock->state != NET_SOCK_STATE_CLOSED)) { - s_bad[i] = 0U; - continue; - } - - /* 回读芯片状态(每监听型 fd 一条命令) */ - ch395f_get_socket_status((uint8_t)i, st); - chip_listening = ((st[0] != CH395F_SOCKET_CLOSED) && - (st[1] == CH395F_TCP_LISTEN)); - - /* 芯片在听 */ - if (chip_listening) { - s_bad[i] = 0U; - /* 本层 CLOSED 而芯片在听 = U11(本层状态滞后)→ 只重同步 */ - if (p_sock->state == NET_SOCK_STATE_CLOSED) { - DBG_INFO("sock%d pump: chip listening, resync CLOSED->LISTENING (U11)", i); - /* 只改本层状态,**不发任何芯片命令**(同 net_resync_listen 的 - * 既定纪律,理由见该函数注释)。session_dead 不清 */ - p_sock->state = NET_SOCK_STATE_LISTENING; - /* N-30(Trap 25):死连接对端端口转登幽灵甄别基准 + 去抖清零 - * —— 芯片监听器未被重开,芯片侧对端信息可能仍是上一条连接的, - * 必须转登为基准,否则对账② 的幽灵拦截会失效(同 net_resync_listen) */ - p_sock->last_peer_port = p_sock->remote_port; - p_sock->lost_conn_seen = 0U; - } - continue; - } - - /* 芯片不在听:情形①②同一处置——重建(本层唯一重建入口) */ - if (s_bad[i] < NET_LISTENER_STABLE_N) { - s_bad[i]++; - } - /* 去抖未满或处于冷却期:本轮不动手 */ - if ((s_bad[i] < NET_LISTENER_STABLE_N) || - ((int32_t)(now - s_cool[i]) < 0)) { - continue; - } - s_bad[i] = 0U; - s_cool[i] = now + NET_LISTENER_COOLDOWN_MS; - DBG_INFO("sock%d pump: chip not listening (0x%02X/0x%02X, was state=%d) -> relisten", - i, st[0], st[1], (int)p_sock->state); - /* 非阻塞:失败由 D10 重试轮询接管 */ - (void)net_listener_try(i); - } -} +/* N34 阶段 3(实施方案 §5 项 1):原"监听器 pump"(100ms 周期、去抖/冷却、 + * 情形①②③)已并入 net_socket.c 的 net_reconcile_tick 单一 100ms tick + * (判据读驱动状态缓存);本模块保留沉降/隔离、D10 重试、armed 查询与 + * net_listener_try(重建动作本身,由 reconciler 形态③④ 调用)。 */ diff --git a/Drivers/BSP/NET/net_listener.h b/Drivers/BSP/NET/net_listener.h index 597cd352..2929f2a1 100644 --- a/Drivers/BSP/NET/net_listener.h +++ b/Drivers/BSP/NET/net_listener.h @@ -2,7 +2,8 @@ * 模块名称:Network Listener Lifecycle (Header) * 模块功能:监听器生命周期模块的对外接口(**真机专属**)—— 维持 fd 处于 * LISTEN 态的全部机制:重建、重建前的沉降/隔离(Trap 23)、排空 RX - * 残渣、D10 非阻塞重试、周期性自愈 pump。 + * 残渣、D10 非阻塞重试(周期重建决策者=N34 阶段 3 合并后的 + * net_reconcile_tick,见 net_socket.c)。 * 适用平台:STM32F407ZGTx + CH395F 以太网芯片 * 作者:王建锋 * 创建日期:2026-10-12 @@ -46,7 +47,8 @@ typedef enum { * 出口参数:无 * 返回值:无 * 限定条件:netTask 上下文;由 handle_disconnect_event / handle_timeout_event 调用 - * 函数说明:1. 只置事实,不做动作 —— 重建由 net_listener_pump / net_listener_try 负责 + * 函数说明:1. 只置事实,不做动作 —— 重建由 net_reconcile_tick(形态③④)/ + * net_listener_try 负责 * 2. 越界 sockfd 静默忽略(不设错误码,与旧直接写数组行为一致) */ void net_listener_settle(int sockfd, net_listener_settle_t level); @@ -84,18 +86,9 @@ void net_listener_clear(int sockfd); */ void net_listener_retry_tick(void); -/* - * 函数功能:监听器 pump —— 本层**唯一**的周期性重建决策者 - * 入口参数:无 - * 出口参数:无 - * 返回值:无 - * 限定条件:netTask 上下文(由 net_process_messages 每轮调用) - * 函数说明:1. 覆盖三种情形:① LISTENING 而芯片失位 → 重建 - * ② CLOSED 而芯片不在 LISTEN → 重建 - * ③ CLOSED 而芯片**正在** LISTEN → 只做状态重同步(U11,不发芯片命令) - * 2. 100 ms 周期 + 去抖 + 冷却(详见实现处注释) - */ -void net_listener_pump(void); +/* N34 阶段 3:原 `net_listener_pump`(周期重建决策者)已并入 net_socket.c 的 + * net_reconcile_tick 单一 100ms tick(判据读驱动状态缓存,实施方案 §5 项 1)。 + * 本模块的 net_listener_try 由 reconciler 形态③④ 与 D10 重试轮询调用。 */ /* 注:本模块消费的宿主能力 `net_pending_active()` 声明在 net_socket.h * (它是 net_socket.c 的函数,须与配对头文件同处,见规范 S17 2.2)。 */ diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index 9581781b..78873b19 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -168,6 +168,7 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len); static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, uint32_t timeout_ms, TaskHandle_t caller); static int net_close_locked(net_sock_t *p_sock); +static int net_drop_conn_locked(net_sock_t *p_sock); static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen); static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int addrlen); @@ -455,6 +456,10 @@ int net_poll(void) { /* 读取全局中断状态(2 字节):读操作本身会清除 CH395F 中断并使其拉高 INT#, * 若此后仍有新中断,CH395F 会再次拉低 INT#,由下一次 net_poll() 处理 */ int_status = ch395f_get_glob_int_status_all(); + /* N34 阶段 3(F-DRV-13 接线):把全局位喂驱动——bit2 PHY_CHANGE 时驱动 + * 重读 PHY 更新 link_up 缓存 + 变化计数,net 层在 reconciler 订阅留痕 + * (运行期拔网线不再静默;不自动重配,重配仍走 UI/配置引擎路径) */ + ch395f_glob_event_feed(int_status); #if defined(NET_BENCH_CMD_LOG) /* 仅状态跳变时记(电平型 RECV_OK 会每 poll 重复,全记会冲爆日志) */ static uint16_t s_bench_last_glob = 0xFFFF; @@ -533,36 +538,62 @@ int net_poll(void) { /* 因此每次 net_poll() 只处理“一批”,剩余数据在后续轮询中读取 */ } while (0); + /* N34 阶段 3(§5 项 1 基础):每轮全量刷新在用 Socket 的驱动状态缓存 + * (唯一刷新点;E2 实测单次回读 18.1us,在用 5~6 个 ≈100us ≪ 1ms 轮询 + * 周期)。reconciler / 探针 / 镜像降级决策点一律读缓存( + * ch395f_sock_state_cached),不再各自发状态 SPI。放在中断处理之后: + * 事件处理(handle_*_event)内部若再读状态,拿到的也是本轮最新值。 */ + for (int i = 0; i < NET_MAX_SOCKETS; i++) { + if (s_net_socks[i].in_use != 0U) { + (void)ch395f_get_sock_state((uint8_t)i); + /* N34 阶段 3(E-02 收口):驱动 sock_reset 状态机在 net_poll 步进 + * (驱动头注释契约"在 net_poll 或测试循环里周期调用")。IDLE 槽位 + * 纯 RAM 判返回 0 无 SPI;仅活跃复位机做短 SPI。作用:同步包装 + * (net_sock_reset_sync)600ms 放弃后复位机不搁浅,芯片条件满足 + * (对端回 FIN 等)时自动走完,槽位可复用(begin 不再恒返 -1)。 */ + (void)ch395f_sock_reset_poll((uint8_t)i); + } + } + /* 返回本次累计发生的事件数量(0 表示无事件) */ return event_count; } +/* 对账+pump 合并周期(N34 阶段 3:原 200ms 对账 / 100ms pump 合一) */ +#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,勿改小) */ + /* - * 函数功能:芯片/本层 socket 状态周期对账(Trap 22 丢中断自愈) + * 函数功能:芯片/本层 socket 状态周期对账(Trap 22 丢中断自愈)+ 监听器 pump + * (N-32 P2)—— N34 阶段 3 合并为单一 100ms tick(docs/网络/N34-自愈审计/实施方案.md §5 项 1) * 入口参数:无 * 出口参数:无 * 返回值:无 * 限定条件:netTask 上下文(由 net_process_messages 每轮调用) - * 函数说明:1. 芯片在快速复连场景会丢 CONNECT/DISCONNECT 中断;丢事件后本层无法靠事件自愈 - * 2. 典型:对端连接占着单连接监听的"后备"(芯片 TCP 仍 ESTABLISHED),后续 SYN 全被拒 - * 3. 应用因未收到 DISCONNECT 仍以为有客户端,永不调用 accept,事件驱动自愈无路可走 - * 4. 故周期回读芯片状态,修正**三类**失同步: - * 5. ① 本层 ESTABLISHED 而芯片已无连接 → 按断开处理(只置事实;重建交 pump) - * 6. ② 本层 LISTENING 而芯片已完成握手 → 补做 standalone accept(复用 handle_connect_event) - * 6b.⑤(N34 阶段1,F-NET-2)本层 ESTABLISHED 而芯片已静默回收回 LISTEN - * (0x05/0x01 无 DISCONNECT)→ RESYNC 只修本层,纪律同 pump③ U11 - * 7. ⚠ N-32 P2(2026-10-12):原③(本层 LISTENING 丢监听)与④(孤儿监听) - * 已**整体移交** `net_listener_pump` —— 本函数内不再有任何重建动作 - * (单一所有者);认领门槛相应收窄为 ESTABLISHED/LISTENING - * 8. 单次状态读可能落在"刚 open/listen 尚未生效"的瞬态 - * 9. 同一失配需连续 NET_RECONCILE_STABLE_N 次才动手(2026-09-23 实测:无去抖时监听被反复重建直至放弃) - * 10. 同一 socket 动手后冷却 NET_RECONCILE_COOLDOWN_MS,防去抖成风暴 - * 11. 周期 NET_RECONCILE_PERIOD_MS;每次每在用 socket 一条状态命令,开销可忽略 + * 函数说明:1. 芯片快速复连会丢 CONNECT/DISCONNECT 中断,本层无法靠事件自愈 + * 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 变化计数变化 → 留痕(不自动重配) */ -#define NET_RECONCILE_PERIOD_MS 200U /* 对账周期 */ -#define NET_RECONCILE_STABLE_N 2U /* 连续失配次数(去抖,≈400ms) */ -#define NET_RECONCILE_COOLDOWN_MS 1000U /* 同一 socket 两次动手的最小间隔(防抖成风暴) */ - static void net_reconcile_tick(void) { /* 上次对账轮的时刻(tick) */ static uint32_t s_last = 0U; @@ -575,76 +606,118 @@ static void net_reconcile_tick(void) { /* Socket 循环索引 */ int i; - /* 未到对账周期(200ms):直接返回,省 SPI 命令 */ + /* 未到对账周期(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 = &s_net_socks[i]; - /* 芯片侧状态两字节:[0]=socket 状态,[1]=TCP 状态 */ - uint8_t st[2] = {0, 0}; - /* 失同步类型:1=连接已失 2=握手已成丢中断 5=芯片静默回收回监听 - *(原 3/4=监听失位已移交 pump,编号不复用以避与历史口径混淆) */ + /* 驱动状态缓存(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; - /* 对账认领范围:**仅 ESTABLISHED / LISTENING**。 - * ⚠ 原先还认领"孤儿监听"(CLOSED + auto_relisten + 已绑端口)以驱动对账④; - * N-32 P2 把③④整体移交给 net_listener_pump(唯一重建入口),本函数只剩 - * ①②——两者都以 ESTABLISHED/LISTENING 为前提,故认领门槛相应收窄。 */ - if ((p_sock->in_use == 0U) || - ((p_sock->state != NET_SOCK_STATE_ESTABLISHED) && - (p_sock->state != NET_SOCK_STATE_LISTENING))) { + if (p_sock->in_use == 0U) { s_bad[i] = 0U; continue; } - /* 读芯片侧两字节状态(每 socket 一条 SPI 命令) */ - ch395f_get_socket_status((uint8_t)i, st); - /* ① 本层 ESTABLISHED 而芯片已无连接 → 按断开处理 */ - if ((p_sock->state == NET_SOCK_STATE_ESTABLISHED) && - ((st[0] == CH395F_SOCKET_CLOSED) || (st[1] == CH395F_TCP_CLOSED))) { - bad = 1; - } else if ((p_sock->state == NET_SOCK_STATE_LISTENING) && - (st[1] == CH395F_TCP_ESTABLISHED)) { - /* ② 本层 LISTENING 而芯片已完成握手(丢 CONNECT 中断)→ 补做 accept - * N-30(Trap 25)幽灵态甄别:拆链收尾窗口内芯片假报 ESTABLISHED, - * 回读对端端口 = 上一条连接端口 → 不补 accept(补了会把数据发给 - * 已死的旧连接 = 生产 LIST#4 数据被吞的直接成因),等拆净或新 SYN */ - 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; + + 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 缺口,N34 阶段1):芯片静默回收——本层 ESTABLISHED + * 而芯片已自行把 Socket 收回 LISTEN(0x05/0x01,**不发 DISCONNECT + * 中断**,run8 台架实证)→ RESYNC 语义 */ + bad = 5; } - } else if ((p_sock->state == NET_SOCK_STATE_ESTABLISHED) && - (st[0] != CH395F_SOCKET_CLOSED) && - (st[1] == CH395F_TCP_LISTEN)) { - /* ⑤(F-NET-2 缺口,N34 阶段1):芯片静默回收——本层 ESTABLISHED - * 而芯片已自行把 Socket 收回 LISTEN(0x05/0x01,**不发 DISCONNECT - * 中断**,run8 台架实证)。判据①要求 TCP==CLOSED 不命中此形态、 - * 判据②要求本层 LISTENING 也不命中——此前生产无人认领(原仅手动 - * API net_resync_listen 能修且生产零调用)。→ bad=5 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 25)幽灵态甄别:拆链收尾窗口内芯片假报 ESTABLISHED, + * 回读对端端口 = 上一条连接端口 → 不补 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 25):死连接对端端口转登幽灵甄别基准 + 探针计数清零 + * —— 芯片监听器未被重开,芯片侧对端信息可能仍是上一条连接的, + * 必须转登为基准,否则形态② 的幽灵拦截会失效 */ + 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; } - /* ③④(本层 LISTENING 丢监听 / 本层 CLOSED 孤儿监听)已移交 - * net_listener_pump —— N-32 P2 单一所有者,本函数不再做重建动作 */ /* 无失同步:清零计数,转下一 socket */ if (bad == 0) { s_bad[i] = 0U; continue; } - /* 累加连续失配计数(封顶至去抖门槛) */ - if (s_bad[i] < NET_RECONCILE_STABLE_N) { + /* 累加连续失配计数(封顶至本形态门槛) */ + if (s_bad[i] < stable_need) { s_bad[i]++; } /* 去抖未满或处于冷却期:本轮不动手 */ - if ((s_bad[i] < NET_RECONCILE_STABLE_N) || + if ((s_bad[i] < stable_need) || ((int32_t)(now - s_cool[i]) < 0)) { continue; } @@ -653,26 +726,32 @@ static void net_reconcile_tick(void) { s_cool[i] = now + NET_RECONCILE_COOLDOWN_MS; if (bad == 1) { - /* 类型①:按断开处理(应用读到 EOF 后由 pump 重建监听) */ - DBG_INFO("sock%d reconcile: chip closed (sock=0x%02X tcp=0x%02X) -> disconnect", - i, st[0], st[1]); + /* 类型①:按断开处理(应用读到 EOF 后由形态③④重建监听) */ + DBG_INFO("sock%d reconcile: chip closed (0x%02X/0x%02X) -> disconnect", + i, c.open, c.tcp_state); /* 复用断开状态机(含陈旧事件甄别 + 状态迁移 + 事件派发); - * 只置事实,重建由 net_listener_pump 统一做(N-32 P2) */ + * 只置事实,重建由本 tick 形态③④统一做(N-32 P2 单一所有者) */ handle_disconnect_event(p_sock); } else if (bad == 5) { /* 类型⑤:静默回收 → RESYNC 语义(原 NET_MSG_RESYNC_LISTEN 分派体 - * 原样并入,N34 阶段1;纪律同 pump③ U11:**不发任何芯片命令**—— + * 原样并入,N34 阶段1;纪律同 U11:**不发任何芯片命令**—— * 重下发 close/重监听会摧毁芯片侧健康监听器 → 端口永拒, * run8 实证 170s 拒绝) */ DBG_INFO("sock%d reconcile: chip re-listened w/o DISCONNECT (0x%02X/0x%02X) -> resync", - i, st[0], st[1]); + i, c.open, c.tcp_state); /* 仅修 MCU 侧;新连接的 CONNECT 中断会把两侧重新同步 */ p_sock->state = NET_SOCK_STATE_LISTENING; - /* N-30(Trap 25):死连接对端端口转登幽灵甄别基准 + 去抖清零 */ + /* N-30(Trap 25):死连接对端端口转登幽灵甄别基准 + 探针计数清零 */ p_sock->last_peer_port = p_sock->remote_port; p_sock->lost_conn_seen = 0U; /* 取消本 fd 在死连接上的挂起 recv(必通知,防调用方挂死) */ net_pending_cancel(i, NET_ERR_CONNRESET); + } else if ((bad == 3) || (bad == 4)) { + /* 类型③④:监听失位/孤儿监听 → 重建(本层唯一重建入口; + * 非阻塞:失败由 D10 重试轮询接管) */ + 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); + (void)net_listener_try(i); } else { /* 类型②:补做 standalone accept */ DBG_INFO("sock%d reconcile: connection completed w/o CONNECT irq -> accept", i); @@ -694,20 +773,18 @@ static void net_reconcile_tick(void) { * 限定条件:必须在 netTask 上下文中调用(独占 CH395F 与 s_net_socks 访问权); * 由 net_task.c 在 net_stack_bring_up 之后每次 1ms 循环调用。 * 函数说明:1. 四个自愈 tick 均非阻塞(绝不 osDelay 忙等,netTask 永不冻结),先于消息分派执行 - * 2. net_listener_retry_tick(D10):断连后按 30ms - * (NET_RELISTEN_RETRY_MS)节拍重试 open+listen;2s(NET_RELISTEN_DEADLINE_MS) - * 总时限到期放弃留痕 - * 3. D10:本 fd 有在途挂起 recv 时跳过(L3 保险) - * 4. net_pending_tick(L3):按节拍了结挂起 recv(数据/到点[仅显式预算]/断连) - * 5. L3 tripwire(N34 阶段 1.5):挂起超 60s 只 DBG_ERROR 留痕一次,不再强制了结/伪造返回值 - * 6. net_reconcile_tick(Trap 22):每 200ms(NET_RECONCILE_PERIOD_MS)对账①② - * 7. 对账以连续 2 次失配去抖 + 1s 冷却,自愈两类失同步 - * 8. net_listener_pump(N-32 P2):每 100ms 监听器 pump,本层**唯一**重建入口 - * (原对账③④;本层 CLOSED 而芯片在听时只重同步)——置于对账之后 - * 9. net_msg_t 按值入队,netTask 侧副本调用方不可见 → 结果只能搭任务通知值回传 - * 10. 32 位整体即结果,不做位域压缩 - * 11. RECV 特例:无数据+阻塞 → 登记 pending 哨兵 NET_PENDING_REGISTERED(0x70000000) - * 12. RECV 特例 do_notify=0,回执推迟到 net_pending_tick 完成时 + * 2. net_listener_retry_tick(D10):断连后按 30ms(NET_RELISTEN_RETRY_MS) + * 节拍重试 open+listen,2s 总时限到期放弃留痕;本 fd 有在途挂起 recv 时跳过(L3 保险) + * 3. net_pending_tick(L3):按节拍了结挂起 recv(数据/到点[仅显式预算]/断连); + * L3 tripwire(N34 阶段 1.5):挂起超 60s 只 DBG_ERROR 留痕一次,不再强制了结/伪造返回值 + * 4. net_reconcile_tick(Trap 22 + pump 合并,N34 阶段 3):每 100ms 对账 + * ①②⑤ + ③④ 重建 + U11 重同步,判据读驱动状态缓存(net_poll 每轮 + * 全量刷新,E2 背书);去抖分级(②=1 轮/③④⑤=2 轮/①=4 轮,M3 让位窗) + * + 1s 冷却;重建失败进 D10 重试队列,reconciler 不重复催 + * 5. net_msg_t 按值入队,netTask 侧副本调用方不可见 → 结果只能搭任务通知值回传 + * 6. 32 位整体即结果,不做位域压缩 + * 7. RECV 特例:无数据+阻塞 → 登记 pending 哨兵 NET_PENDING_REGISTERED(0x70000000), + * do_notify=0,回执推迟到 net_pending_tick 完成时 */ void net_process_messages(void) { net_msg_t msg; @@ -722,11 +799,10 @@ void net_process_messages(void) { net_listener_retry_tick(); /* L3 挂起 recv 完成检查:数据到达 / 到点 / 断连时通知调用方 */ net_pending_tick(); - /* Trap 22 状态对账:周期回读芯片状态,自愈丢失的中断(①②;③④ 已归 pump) */ + /* Trap 22 状态对账 + 监听器 pump(N34 阶段 3 合并单一 100ms tick): + * 判据读驱动缓存(net_poll 每轮全量刷新),①②⑤ 失同步 + ③④ 重建 + + * U11 重同步全部在此(本层唯一周期重建入口,N-32 P2 单一所有者) */ net_reconcile_tick(); - /* N-32 P2:监听器 pump —— 本层唯一重建入口(置于对账之后:对账①② - * 可能刚把会话置 CLOSED,pump 紧接着就能看到并重建) */ - net_listener_pump(); /* 队列排空:超时 0 非阻塞取,每条处理一轮,直到本轮队列为空 */ while (osMessageQueueGet(netMsgQueueHandle, &msg, NULL, 0) == osOK) { @@ -813,9 +889,29 @@ void net_process_messages(void) { msg.result = net_accept_locked(msg.sockfd, p_addr, p_addrlen); break; } + case NET_MSG_DROP_CONN: { + /* N34 阶段 3(E-02 收口):应用放弃会话的主动拆除(拆连接、保监听) */ + net_sock_t *p_sock = net_get_sock(msg.sockfd); + if (p_sock == NULL) { + /* 描述符无效 → BADF */ + net_set_errno(NET_ERR_BADF); + msg.result = -1; + } else if (!p_sock->in_use || p_sock->type != NET_SOCK_STREAM) { + /* 不在用 / 非 TCP 流 → INVAL */ + net_set_errno(NET_ERR_INVAL); + msg.result = -1; + } else if (p_sock->state != NET_SOCK_STATE_ESTABLISHED && + p_sock->state != NET_SOCK_STATE_CONNECTING) { + /* 无活动会话:幂等成功(监听原样,不动芯片) */ + msg.result = 0; + } else { + msg.result = net_drop_conn_locked(p_sock); + } + break; + } /* N34 阶段1(F-NET-3):NET_MSG_RESYNC_LISTEN / NET_MSG_RELISTEN 两个 * 手动自愈消息已删——RESYNC 语义并入对账⑤(net_reconcile_tick), - * RELISTEN 与 pump/D10 完全重叠且对健康监听器是 footgun; + * RELISTEN 与 reconciler/D10 完全重叠且对健康监听器是 footgun; * 生产调用者本就为零(仅 test/board Phase 13,已改由自动机制接管) */ default: /* 未知消息类型(枚举闭合,正常不应触发):置通用错误码与结果 -1, @@ -1063,6 +1159,47 @@ int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) { return 0; } +/* + * 函数功能:驱动 sock_reset 原语的有界确定性排渣等待(P2b 收口,N34 阶段 3; + * 替代原"close + 盲等 500ms"式忙等) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * mode - CH395F_RESET_GRACEFUL(等 100ms)/ QUARANTINE(等 400ms) + * 出口参数:无 + * 返回值:0 - 复位完成(芯片 byte0=00H、事件已排空);-1 - 启动失败或 600ms 超时 int + * 限定条件:仅 netTask 上下文(SPI 串行契约);调用前本层已放弃该会话 + * 函数说明:1. 原语本身非阻塞(begin/poll 分步),本包装是**同步 API 路径** + * (listen open 重试 / close)用的有界等待:2ms 节奏 poll,600ms 封顶 + * 2. 内部序列=E4 定案:读空 RX -> CLOSE -> 排空事件(quiescent) -> + * 轮询 byte0=00H;WAIT_CLOSE 内置 LISTEN 态二段 CLOSE 补发 + * (E2 补充发现:一次 CLOSE 只拆监听 byte0 恒 05) + * 3. 超时留痕返 -1,调用方按原语义兜底(Trap 09 重试环 / close 放行) + */ +static int net_sock_reset_sync(int sockfd, ch395f_reset_mode_t mode) { + /* 起始时刻(600ms 上限基准) */ + uint32_t t0 = HAL_GetTick(); + /* 状态机步进返回值 */ + int rc; + + /* 启动状态机(本 fd 已在运行返 -1:netTask 串行上下文不应发生) */ + if (ch395f_sock_reset_begin((uint8_t)sockfd, mode) != 0) { + return -1; + } + /* 有界等待:2ms 节奏步进(每步仅短 SPI 事务,netTask 无长忙等) */ + for (;;) { + rc = ch395f_sock_reset_poll((uint8_t)sockfd); + /* 0=完成 / -1=失败(槽位已复位可重开):都退出等待 */ + if (rc != 1) { + break; + } + if ((uint32_t)(HAL_GetTick() - t0) > 600U) { + DBG_ERROR("sock%d sock_reset_sync timeout (600ms)", sockfd); + return -1; + } + osDelay(2); + } + return (rc == 0) ? 0 : -1; +} + /* * 函数功能:启动 TCP Server 监听(内部版本,必须在 netTask 上下文调用) * 入口参数:sockfd - Socket 描述符 (0~7) int @@ -1098,7 +1235,11 @@ int net_listen_locked(int sockfd) { /* 本地端口 = 控制块 local_port */ ch395f_set_sour_port(sockfd, p_sock->local_port); - /* CH395F 关闭 Socket 后需延时才能重新打开(Trap 09),故失败重试最多 3 次 */ + /* CH395F 关闭 Socket 后需延时才能重新打开(Trap 09),故失败重试最多 3 次。 + * P2b 收口(N34 阶段 3):原"close + 盲等 500ms"改**确定性排渣**—— + * sock_reset(读空 RX -> CLOSE -> 排空事件 -> 等 byte0=00H,内置 LISTEN + * 态二段 CLOSE 补发;E4 定案 60 轮实证复位后 open 必成)。GRACEFUL + * 最短等 100ms ≪ 原 500ms 盲等;复位失败/超时仍走下一轮重试,Trap 09 语义保留 */ for (int open_try = 0; open_try < 3; open_try++) { /* 尝试打开芯片侧 Socket */ status = ch395f_open_socket(sockfd); @@ -1106,10 +1247,8 @@ int net_listen_locked(int sockfd) { /* 打开成功,退出重试 */ break; } - /* 打开失败先关闭,再延时后重试 */ - ch395f_close_socket(sockfd); - /* 等 500ms 芯片沉降后再试(Trap 09) */ - osDelay(500); + /* 打开失败:确定性复位本 Socket(排渣/二段关闭/等 CLOSED 一体) */ + (void)net_sock_reset_sync(sockfd, CH395F_RESET_GRACEFUL); } /* 3 次仍失败,返回错误 */ if (status != CH395F_ERR_SUCCESS) { @@ -1224,34 +1363,34 @@ int net_listen(int sockfd) { * 幽灵态甄别:拆链窗口内芯片也会直接假报 ESTABLISHED(旧连接未拆净),此时回读 * 对端端口 = 上一条连接端口;新连接临时端口几乎必然不同 → 端口相同不 accept, * 等旧连接拆净 / 新 SYN 到达(新连接若也丢中断,后续轮询以新端口命中)。 */ -#define NET_LOST_CONN_STABLE_N 2U /* 连续确认轮次(accept 轮询 100ms/轮,≈200ms 去抖) */ /* * 函数功能:监听等待期的芯片状态轮询探针——芯片已 ESTABLISHED 而 CONNECT 中断 - * 未到(丢中断)时,去抖满后补做 accept(复用 CONNECT 事件簿记路径)。 + * 未到(丢中断)时补做 accept(复用 CONNECT 事件簿记路径)。 * 入口参数:p_sock - 处于 LISTENING 的监听控制块 net_sock_t * * 出口参数:无(命中时 p_sock->state 经 handle_connect_event 转 ESTABLISHED) * 返回值:无 - * 限定条件:netTask 上下文(net_accept_locked 内调用,SPI 访问安全) - * 函数说明:1. 每轮 accept 轮询(100ms)读一次芯片状态,兼作状态第二信源 - * 2. 幽灵态(对端端口 = last_peer_port)不动手,去抖清零 - * 3. 连续 NET_LOST_CONN_STABLE_N 轮确认后才补 accept(防"刚 listen - * 尚未生效"的瞬态误判——2026-09-23 实测教训) + * 限定条件:netTask 上下文(net_accept_locked 内调用,其开头已 net_poll(), + * 驱动缓存新鲜度有保证) + * 函数说明:1. N34 阶段 3(F-MS-9):判据改读**驱动状态缓存**(零 SPI;唯一 + * 刷新点=net_poll),**探针去抖删除**——原去抖防的两类瞬态已 + * 结构性消除:"刚 listen 尚未生效"显示 TCP=00 永不命中 ESTABLISHED + * 门(E2:LISTEN 可见 100% <=10ms 无长尾);拆链窗假 ESTABLISHED + * 由幽灵甄别拦截。NET_LOST_CONN_STABLE_N 随之下线 + * 2. 幽灵态(对端端口 = last_peer_port)不动手,计数清零 */ static void net_lost_connect_probe(net_sock_t *p_sock) { - /* 芯片侧状态两字节:[0]=socket 状态,[1]=TCP 状态 */ - uint8_t st[2] = {0, 0}; /* 对端 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); - /* 读芯片状态 */ - ch395f_get_socket_status(p_sock->ch395_sock, st); - /* 芯片尚未完成握手:去抖计数清零,本轮无动作 */ - if (st[1] != CH395F_TCP_ESTABLISHED) { + /* 芯片尚未完成握手:可疑电平计数清零,本轮无动作 */ + if (c.tcp_state != CH395F_TCP_ESTABLISHED) { p_sock->lost_conn_seen = 0U; return; } - /* 已 ESTABLISHED:回读对端端口做幽灵甄别 */ + /* 已 ESTABLISHED:回读对端端口做幽灵甄别(按需 SPI,仅命中态发) */ ch395f_get_remot_ipp(p_sock->ch395_sock, remote); /* 对端端口(小端 2 字节) */ uint16_t peer = (uint16_t)(remote[4] | (remote[5] << 8)); @@ -1263,14 +1402,8 @@ static void net_lost_connect_probe(net_sock_t *p_sock) { p_sock->lost_conn_seen = 0U; return; } - /* 去抖:连续确认满门槛才补 accept(封顶防溢出) */ - if (p_sock->lost_conn_seen < NET_LOST_CONN_STABLE_N) { - p_sock->lost_conn_seen++; - } - if (p_sock->lost_conn_seen < NET_LOST_CONN_STABLE_N) { - return; - } - /* 动手前清去抖计数 */ + /* N34 阶段 3(F-MS-9):去抖已删(判据读缓存,两类瞬态结构性消除, + * 见函数头注释 1);lost_conn_seen 仅作 M3 可疑电平标志用,动手前清零 */ p_sock->lost_conn_seen = 0U; /* 留痕:丢中断自愈命中 */ DBG_INFO("sock%d lost-CONNECT self-accept (peer=%u)", @@ -1481,9 +1614,9 @@ int net_listener_armed(int sockfd) { * ⚠ 无残留时必须返回 0 —— 否则调用方会在整个 CLOSED 窗口内每轮都发起 * net_accept(RPC:入队 + xTaskNotifyWait 回执**无上界**)→ 队列积压 → * 调用方任务喂不上狗。2026-10-12 P2a 台架实测:删掉即时 relisten 后 CLOSED - * 窗口被拉长到 pump 周期 + 隔离期(~100ms),:21 0ms×50 期间出现 48 条 + * 窗口被拉长到 reconciler 周期 + 隔离期(~100ms),:21 0ms×50 期间出现 48 条 * `[WD] task stall`(mask=0x67/req=0x77,缺 bit4=UI)。 - * 重建本身由 net_listener_pump 负责,调用方**无需**在此催促。 + * 重建本身由 net_reconcile_tick 形态③④ 负责,调用方**无需**在此催促。 * readable 由 net_poll 在 RECV_OK 置位,"对端发完即 FIN"场景下 CLOSED 之后 * 仍为 1(无人清除),可作"可能仍有残留"的纯 RAM 判据(不发起 SPI)。 */ if ((p_sock->state == NET_SOCK_STATE_CLOSED) && @@ -1600,7 +1733,8 @@ int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen) { * 返回值:0 - 连接请求已发出(异步,成功以 CONNECT 事件为准);-1 - 失败 int * 限定条件:net_init() 已调用;必须在 netTask 上下文中调用 * 函数说明:1. 校验控制块(in_use、TCP 类型、地址参数)并保存对端地址 - * 2. 芯片侧 OPEN(失败重试 3 次、间隔 500ms,见 Trap 09)+ 配置源/目的端口与目的 IP + * 2. 芯片侧 OPEN(失败重试 3 次,每次失败走 sock_reset 确定性排渣, + * 见 Trap 09 / P2b)+ 配置源/目的端口与目的 IP * 3. 清本 Socket 残留中断状态(丢弃上一条连接遗留的 DISCONNECT) * 4. 发起 TCP_CONNECT(驱动内已轮询 CMD_STATUS)并置 CONNECTING; * 命令级被拒立即关 Socket 返 -1,TCP 层成败仍由 CONNECT/TIMEOUT 裁定 @@ -1651,7 +1785,11 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a /* 设置协议类型为 TCP */ ch395f_set_proto_type(sockfd, CH395F_PROTO_TYPE_TCP); - /* CH395F 关闭 Socket 后需延时才能重新打开(Trap 09),故失败重试最多 3 次 */ + /* CH395F 关闭 Socket 后需延时才能重新打开(Trap 09),故失败重试最多 3 次。 + * P2b 收口(N34 阶段 3):原"close + 盲等 500ms"改**确定性排渣**—— + * sock_reset(读空 RX -> CLOSE -> 排空事件 -> 等 byte0=00H,内置 LISTEN + * 态二段 CLOSE 补发;E4 定案 60 轮实证复位后 open 必成)。GRACEFUL + * 最短等 100ms ≪ 原 500ms 盲等;复位失败/超时仍走下一轮重试,Trap 09 语义保留 */ for (int open_try = 0; open_try < 3; open_try++) { /* 尝试打开芯片侧 Socket */ status = ch395f_open_socket(sockfd); @@ -1659,10 +1797,8 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a /* 打开成功,退出重试 */ break; } - /* 打开失败先关闭,再延时后重试 */ - ch395f_close_socket(sockfd); - /* 等 500ms 芯片沉降后再试(Trap 09) */ - osDelay(500); + /* 打开失败:确定性复位本 Socket(排渣/二段关闭/等 CLOSED 一体) */ + (void)net_sock_reset_sync(sockfd, CH395F_RESET_GRACEFUL); } /* 3 次仍失败 → BUSY */ @@ -1670,11 +1806,10 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a return NET_ERR_RETURN_1(NET_ERR_BUSY); } - /* 清掉本 Socket 可能残留的中断状态,丢弃上一条连接遗留的 DISCONNECT */ - /* 读即清:本 Socket 中断状态寄存器 */ - (void)ch395f_get_sock_int_status((uint8_t)sockfd); - /* 读即清:全局中断状态寄存器 */ - (void)ch395f_get_glob_int_status_all(); + /* 清掉本 Socket 可能残留的中断状态,丢弃上一条连接遗留的 DISCONNECT。 + * F-NET-7(N34 阶段 3):只清**本 Socket** SINT(驱动护栏原语)——原 + * "再读清全局"会焊死其它在用 Socket 的事件位(全局寄存器唯一读者=net_poll) */ + (void)ch395f_sock_events((uint8_t)sockfd); /* 设置本地(源)端口 */ ch395f_set_sour_port(sockfd, p_sock->local_port); @@ -1943,7 +2078,7 @@ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, * 它是"消费者动作触发"(消费者正在等这条连接的结局),不是周期性 * "抢先";删掉它会把 CLOSED 窗口从 ~0 拉长到 pump 周期+隔离期 * (~100ms),期间调用方高频发起 net_accept(RPC 无上界)→ - * `[WD] task stall`(实测 uis 往返 1063ms)。周期性抢先由 pump 收敛即可。 */ + * `[WD] task stall`(实测 uis 往返 1063ms)。周期性抢先由 reconciler 收敛即可。 */ if ((p_sock->state == NET_SOCK_STATE_CLOSED) && (p_sock->auto_relisten != 0U)) { /* 非阻塞:失败由 D10 重试轮询接管 */ (void)net_listener_try(sockfd); @@ -2338,45 +2473,31 @@ static void net_pending_settle_close(net_sock_t *p_sock) { * 出口参数:无 * 返回值:0 - 关闭成功 int * 限定条件:必须在 netTask 上下文中调用;p_sock 有效 - * 函数说明:1. 关闭 CH395F Socket,排空上一条连接残留的 DISCONNECT 中断 - * 2. 等待芯片回到 CLOSED 状态(超时 500ms),再清零控制块 + * 函数说明:1. P2b 收口(N34 阶段 3):原"close + 500ms 盲等排空循环"改 + * **确定性 sock_reset**(读空 RX -> CLOSE(幂等) -> 排空本 Socket + * 事件(quiescent) -> 等 byte0=00H,内置 LISTEN 态二段 CLOSE 补发 + * ——E2 补充发现:一次 CLOSE 只拆监听 byte0 恒 05) + * 2. F-NET-7:排空只碰本 Socket SINT,**不再读清全局中断寄存器** + * (唯一读者=net_poll;原循环读全局会焊死其它 Socket 事件位) + * 3. 快路径:芯片已 CLOSED(byte0=00H)→ 跳过复位只清残留事件 + * 4. 复位超时照常放行(同原 500ms 超时容忍;后续 relisten 由 + * Trap 09 open 重试环吸收) */ static int net_close_locked(net_sock_t *p_sock) { /* 由控制块指针反推描述符 */ int sockfd = (int)(p_sock - s_net_socks); - /* 起始时刻(等芯片 CLOSED 的 500ms 上限基准) */ - uint32_t tick = HAL_GetTick(); /* L3:先了结本 fd 的挂起 recv(排空/取消 + 必通知),再关芯片 Socket * ——关闭不得让等待中的调用方永久悬挂 */ net_pending_settle_close(p_sock); - /* 关闭芯片侧 Socket */ - ch395f_close_socket(sockfd); - - /* 排空上一条连接残留的 DISCONNECT 中断(单连接模式仅 Socket 0;复用同一 Socket 时 - * 旧 DISCONNECT 在 FIN/ACK 完成后才上报,不清除会被下一连接误判为"刚建立就断开") - * 等待芯片回到 CLOSED 并清掉 Socket/全局中断状态,避免事件串扰 */ - do { - /* [0]=Socket 状态 [1]=TCP 状态(该 API 输出 2 字节) */ - uint8_t st[2] = {0, 0}; - /* 读芯片状态(两字节) */ - ch395f_get_socket_status((uint8_t)sockfd, st); - /* 读即清:本 Socket 中断状态 */ - (void)ch395f_get_sock_int_status((uint8_t)sockfd); - /* 读即清:全局中断状态 */ - (void)ch395f_get_glob_int_status_all(); - /* 芯片已回 CLOSED:结束 */ - if (st[0] == CH395F_SOCKET_CLOSED) { - break; - } - /* 等待超时(500ms):停止等待继续 */ - if (HAL_GetTick() - tick > 500) { - break; - } - /* 等 5ms 再查(循环受 500ms 上限约束) */ - osDelay(5); - } while (1); + /* 排渣 + 关闭 + 等 CLOSED 一体(见函数头注释 1); + * 已 CLOSED 的 fd 只清本 Socket 残留事件(防下一连接误判"刚建立就断开") */ + if (ch395f_cmd_done((uint8_t)sockfd, CH395F_DONE_CLOSE) == 1) { + (void)ch395f_sock_events((uint8_t)sockfd); + } else if (net_sock_reset_sync(sockfd, CH395F_RESET_GRACEFUL) != 0) { + DBG_ERROR("sock%d close: sock_reset not done, proceed anyway", sockfd); + } /* 清零控制块(memset 0 = 释放本 fd 全部状态) */ /* N-30(Trap 25):幽灵甄别基准跨 fd 释放保留(幽灵属芯片 Socket 级状态, @@ -2393,6 +2514,52 @@ static int net_close_locked(net_sock_t *p_sock) { return 0; } +/* + * 函数功能:主动拆掉当前 TCP 会话、保留监听(内部版本,netTask 上下文) + * 入口参数:p_sock - 会话所在 Socket 控制块(调用方已判 ESTABLISHED/CONNECTING) + * 出口参数:无 + * 返回值:0 - 拆除完成(监听型 fd 已重监听或交 D10) int + * 限定条件:netTask 上下文;p_sock 在用且为 TCP 流 + * 函数说明:1. E-02 收口(实验方案 E13 §5):应用放弃的会话若 TCP 仍活 + * (PC 内核自动 ACK keepalive,僵尸判不死),单连接监听被永久 + * 占住;应用又不得 net_close 监听 fd(毁监听)——本函数补上 + * "拆连接不拆监听"的应用侧形态 + * 2. 拆除=sock_reset QUARANTINE(Trap 23 异常释放语义:FIN + 排渣 + + * 排空事件(quiescent) + 等 byte0=00H,内置二段 CLOSE 补发) + * 3. auto_relisten(监听型 fd):收口 CLOSED 后即时 net_listener_try + * (芯片已 CLOSED 直接 open 必成,E4 定案;失败交 D10 重试 + + * reconciler 形态④);非 auto_relisten(client fd):收口 CLOSED, + * 是否重连由调用方决定 + * 4. 挂起 recv 先了结(settle_close:排空尝试或取消,必通知) + */ +static int net_drop_conn_locked(net_sock_t *p_sock) { + /* 由控制块指针反推描述符 */ + int sockfd = (int)(p_sock - s_net_socks); + + /* L3:先了结本 fd 挂起 recv(排空/取消 + 必通知),再拆芯片会话 */ + net_pending_settle_close(p_sock); + + /* 确定性拆除(异常释放 → QUARANTINE 400ms 底,Trap 23 语义) */ + if (net_sock_reset_sync(sockfd, CH395F_RESET_QUARANTINE) != 0) { + DBG_ERROR("sock%d drop_conn: sock_reset not done", sockfd); + /* 超时继续收口本层;芯片侧交 reconciler/pump 对账接管 */ + } + + /* 会话簿记收口(保留 fd:不 memset、不动 local_port/auto_relisten) */ + p_sock->state = NET_SOCK_STATE_CLOSED; + p_sock->readable = 0U; + /* N-30(Trap 25):新会话起点,丢中断去抖计数清零 */ + p_sock->lost_conn_seen = 0U; + + if (p_sock->auto_relisten) { + /* 监听型 fd:即时重建(非阻塞;失败 D10 重试轮询接管) */ + (void)net_listener_try(sockfd); + } + + /* 拆除完成 */ + return 0; +} + /* * 函数功能:发送数据(线程安全版本,通过消息队列委托给 netTask) * 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS @@ -2705,6 +2872,63 @@ int net_close(int sockfd) { return msg.result; } +/* + * 函数功能:主动拆掉当前 TCP 会话、保留监听(线程安全版本,经消息队列委托 netTask) + * 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS + * 出口参数:无 + * 返回值:0 - 成功(含幂等:无活动会话直接 0) int + * -1 - 失败(BADF/INVAL/入队失败) int + * 限定条件:1. 必须在【非 netTask】上下文调用(同其它 net_* API,netTask 内会自等死锁) + * 2. 场景=应用放弃会话(对时 30s 预算耗尽、协议超时);EOF 路径**不要**调 + * (net 层 auto_relisten 已接管,见 AGENTS 单连接断开语义) + * 函数说明:1. 单连接模型下挂起但 TCP 仍活的客户端会永久占住监听(keepalive + * 被 PC 内核自动 ACK 判不死,实验方案 E13 §5);应用不得 + * net_close 监听 fd → 本 API 拆连接不拆监听(E-02 收口) + * 2. netTask 侧执行 sock_reset QUARANTINE + 监听即时重建(见 + * net_drop_conn_locked);等待期 netTask 有界占用 ~400-600ms + */ +int net_drop_conn(int sockfd) { + /* 消息体(按值拷入队列,netTask 改的是其副本) */ + net_msg_t msg; + + /* 描述符越界 → BADF */ + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { + return NET_ERR_RETURN_1(NET_ERR_BADF); + } + + /* 消息队列未创建(网络层未初始化)→ 通用错误 */ + if (netMsgQueueHandle == NULL) { + return NET_ERR_RETURN_1(NET_ERR); + } + + /* 消息类型:拆会话保监听 */ + msg.type = NET_MSG_DROP_CONN; + /* 目标 Socket */ + msg.sockfd = sockfd; + /* 无超时 */ + msg.timeout_ms = 0U; + /* 调用方任务句柄(netTask 凭它回传回执) */ + msg.caller = xTaskGetCurrentTaskHandle(); + /* 结果占位 */ + msg.result = -1; + + /* 入队:队列满时阻塞调用方直至有空位 */ + if (osMessageQueuePut(netMsgQueueHandle, &msg, 0, osWaitForever) != osOK) { + /* 入队失败 → BUSY */ + return NET_ERR_RETURN_1(NET_ERR_BUSY); + } + + /* 阻塞等待 netTask 回执(两段式拾取,防丢失通知) */ + msg.result = net_wait_result("dcp", sockfd); + + if (msg.result < 0) { + /* 错误码经通知值回传;对外保持 BSD 语义失败一律 -1 */ + return NET_ERR_RETURN_1(msg.result); + } + /* 拆除成功(0,含幂等) */ + return msg.result; +} + /* * 函数功能:发送 UDP 数据报(含目标地址) * 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS @@ -3118,7 +3342,7 @@ int net_sock_state(int sockfd) { /* N34 阶段1(F-NET-3):手动自愈 API net_resync_listen / net_relisten 已删除—— * RESYNC 语义并入对账⑤(net_reconcile_tick 自动认领"芯片静默回收回 LISTEN"形态, - * 200ms×2 去抖 + 1s 冷却沿用);RELISTEN 与 pump/D10 完全重叠且对健康监听器是 + * 200ms×2 去抖 + 1s 冷却沿用);RELISTEN 与 reconciler/D10 完全重叠且对健康监听器是 * footgun(无条件 open→USE→close 自毁 ~1s,原注释自认)。生产调用者本就为零 * (仅 test/board Phase 13,已改由自动机制接管)。历史实现见本文件 git 历史。 */ @@ -3325,7 +3549,7 @@ static void handle_connect_event(net_sock_t *p_sock) { * 撤回(当时仅证明"物理断链"已被 +2s TIM_OUT 快速覆盖,其余场景 * 无法注入、未证明);E11(2026-10-13,实验方案.md)用"禁用 PC 网卡" * 注入"链路在、对端静默"(NAT/黑洞)补上证据链:生产参数 60/5/3 - * 实测 ≈30 s 判死 → TIM_OUT → QUARANTINE → pump 重建;对端存活时 + * 实测 ≈30 s 判死 → TIM_OUT → QUARANTINE → reconciler 重建;对端存活时 * 探测被 ACK、空闲会话不误杀。消费链路现成,按手册位置转正。 */ ch395f_set_keepalive_enable(p_sock->ch395_sock, 1); @@ -3389,17 +3613,19 @@ static void handle_disconnect_event(net_sock_t *p_sock) { /* Trap 22 / N-6:监听态收到 DISCONNECT——bit3=1(0x08)下芯片**不**关该 Socket(停 0x05/0x00,芯片 TCP 侧已 CLOSED);不重开则此后所有 SYN 全被拒 = 监听永久死亡; * 触发 = 断连后 <~1s 快速复连时 CONNECT 中断未上报(控制块滞留 LISTENING)后该连接断开(2026-09-23 生产固件复现); * N-32 P2:本分支**只回读留痕、不再 relisten** —— 重建统一交 - * net_listener_pump(每 100 ms 回读芯片状态,监听失位即重建)。原处直接 + * net_reconcile_tick 形态③(每 100 ms 读驱动缓存,监听失位即重建)。原处直接 * relisten 是 12 个所有者之一,且位于 DISCONNECT 事件路径,可能"抢先" - * 于消费者 recv(P1 已消除其后果,P2 消除其可能) */ + * 于消费者 recv(P1 已消除其后果,P2 消除其可能)。 + * ⚠ 本留痕回读保持**主动 SPI**(不用驱动缓存):事件路径上缓存必然滞后 + * 于刚到达的中断(刷新在 net_poll 末尾、事件处理之后),仅周期对账可用缓存 */ /* 回读缓冲(芯片状态 2 字节) */ uint8_t st[2] = {0, 0}; - /* 回读芯片状态(仅为留痕:重建与否由 pump 裁决) */ + /* 回读芯片状态(仅为留痕:重建与否由 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), pump will relisten", + DBG_INFO("sock%d listen lost (chip sock=0x%02X tcp=0x%02X), reconciler will relisten", sockfd, st[0], st[1]); } else { /* 仍在监听:判陈旧事件丢弃 */ @@ -3425,8 +3651,9 @@ static void handle_disconnect_event(net_sock_t *p_sock) { * 而 TCP 槽 0x00 且本层 ESTABLISHED = 逻辑矛盾(活连接应 0x04):陈旧 DISCONNECT * (拆链完成滞后锁存,Trap 14)与"真断开恰被旧拆链覆写槽"在 D20 上不可分辨 * (结构性失明,F-NET-15 Q1)。处置:不吃边沿,置可疑电平返回,交对账① - * (ESTABLISHED + 芯片 TCP==CLOSED,:588-589)电平去抖再入本函数(去抖 2×200ms - * + 冷却,实测延迟 ~1.2s):flag==1 旁路本门 → 正常释放;连接真活则租约内 + * (ESTABLISHED + 芯片 TCP==CLOSED)电平去抖再入本函数(N34 阶段 3 合并 + * 后=4×100ms,NET_RECON_STABLE_DEATH,让位租约 400ms 不变 + 冷却): + * flag==1 旁路本门 → 正常释放;连接真活则租约内 * 会话正常完成(风暴链掐断,E9a n30 0/50 实证)。 * 零额外 SPI 于常规路径(st[] 已读;recv_len 仅可疑分支多读一条)。 * ⚠ E9a/E9b 实测两轮收窄:①必须含 recv_bytes==0——已读数据的会话遇 @@ -3457,7 +3684,7 @@ static void handle_disconnect_event(net_sock_t *p_sock) { /* F-NET-15 M1(N34 实验 2026-10-12):沉降**无条件**登记。原 recv_bytes!=0 门的 * 受害者由事件时序定义(accept 后 ~20ms 被陈旧 DISCONNECT 杀 → recv_bytes==0 是 * 构造性必然),门对"残渣循环自持"形态失效(n30_repro 背靠背 9/10 失败,E1 实测); - * 门原保护目标(:21 @0ms 快复连)已被 N-32 pump 延迟化(恢复窗实测 0.3~1s)架空, + * 门原保护目标(:21 @0ms 快复连)已被 N-32 单一重建入口延迟化(恢复窗实测 0.3~1s)架空, * +100ms 沉降落在窗内不可见。沉降窗口同时让残渣 DISCONNECT 在 state==CLOSED * (无害、仅留痕)时被消费,断开"杀下一条 → 新拆链 → 再杀"的繁殖链 */ { diff --git a/Drivers/BSP/NET/net_socket.h b/Drivers/BSP/NET/net_socket.h index bbd4a319..a85a9159 100644 --- a/Drivers/BSP/NET/net_socket.h +++ b/Drivers/BSP/NET/net_socket.h @@ -37,8 +37,10 @@ typedef enum { NET_MSG_CONNECT, NET_MSG_LISTEN, NET_MSG_ACCEPT, + NET_MSG_DROP_CONN, /* N34 阶段 3(E-02 收口):主动拆会话、保监听(见 net_drop_conn) */ /* N34 阶段1(F-NET-2/3):NET_MSG_RESYNC_LISTEN / NET_MSG_RELISTEN 已删—— - * 静默回收由对账⑤自动认领(net_reconcile_tick),重建由 pump/D10 单一 + * 静默回收由对账⑤自动认领(net_reconcile_tick),重建由 reconciler + * 形态③④/D10 单一 * 所有者负责;手动自愈 API 面退役(生产调用者本就为零) */ } net_msg_type_t; @@ -179,7 +181,7 @@ int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen); * 保住 N-30 的 "closed-but-readable" 接受;**无残留时必须返回 0**, * 否则调用方会在整个 CLOSED 窗口内每轮发起 `net_accept`(RPC 无上界) * → 队列积压 → 喂不上狗(2026-10-12 P2a 台架实测 48 条 task stall)。 - * 重建由 `net_listener_pump` 负责,调用方无需催促 + * 重建由 `net_reconcile_tick` 形态③④ 负责,调用方无需催促 * 4. 与已退役 `net_can_accept` 的关系:本函数 = 其 ESTABLISHED 放行态 * ∪ LISTENING ∪ CLOSED+残留;回答"这个监听器这一轮**值不值得**发起 * 一次 accept",而非"一定有连接" @@ -341,6 +343,22 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags, */ int net_close(int sockfd); +/* + * 函数功能:主动拆掉当前 TCP 会话,**保留监听**(N34 阶段 3,E-02 收口) + * 入口参数:sockfd - socket 描述符 + * 返回值:0 成功(含幂等:无活动会话),-1 失败 + * 限定条件:非 netTask 上下文(经消息队列委托,同其它 net_* API) + * 函数说明:1. 适用场景=应用层放弃会话(对时 30s 预算耗尽、协议超时等): + * 单连接模型禁止对监听 fd net_close(连监听一起毁,AGENTS 约定), + * 而挂起但 TCP 仍活的客户端会永久占住单连接监听(PC 内核自动 + * ACK keepalive,芯片僵尸判不死——实验方案 E13 §5 定案缺口) + * 2. 实现=sock_reset QUARANTINE(FIN 关闭 + 排渣 + 排空事件 + 等 + * byte0=00H,Trap 23 异常释放语义);auto_relisten fd 随即重监听 + * (失败交 D10 重试),非 auto_relisten(client fd)收口 CLOSED + * 3. 幂等:非 ESTABLISHED/CONNECTING 直接返 0,不动监听 + */ +int net_drop_conn(int sockfd); + /* * 事件回调 API */ @@ -383,9 +401,9 @@ int net_get_errno(void); int net_sock_state(int sockfd); /* N34 阶段1(F-NET-3):net_resync_listen / net_relisten 声明已删—— - * 静默回收形态由对账⑤自动认领(≤400ms 去抖 + 1s 冷却,纪律"不发芯片命令" - * 原样保留在认领动作内);监听重建由 pump(100ms 周期)/D10(30ms 重试) - * 单一所有者。勿再引入"应用层手动自愈"入口。 */ + * 静默回收形态由对账⑤自动认领(2 轮去抖 + 1s 冷却,纪律"不发芯片命令" + * 原样保留在认领动作内);监听重建由 reconciler 形态③④(100ms 周期)/ + * D10(30ms 重试)单一所有者。勿再引入"应用层手动自愈"入口。 */ /* * 函数功能:将 IP 地址字符串转为网络字节序 diff --git a/Drivers/BSP/NET/net_types.h b/Drivers/BSP/NET/net_types.h index 35cfbd42..99e7380d 100644 --- a/Drivers/BSP/NET/net_types.h +++ b/Drivers/BSP/NET/net_types.h @@ -111,7 +111,12 @@ struct net_sockaddr_in { #define NET_ERR_BUSY -11 /* 操作未完成(CH395F BUSY) */ /* - * Socket 状态枚举 + * Socket 状态枚举(**本层会话投影**,非芯片状态事实源) + * N34 阶段 3(镜像降级,实施方案 docs/网络/N34-自愈审计/实施方案.md §5 项 5): + * 芯片侧状态唯一事实源=驱动状态缓存(ch395f_sock_state_cached,刷新点=net_poll); + * 本 8 态只表达**本层会话语义**(fd 生命周期/待 accept 电平/门控展示), + * 事件与对账把它与芯片事实对齐。新增"按芯片状态决策"的代码一律读驱动缓存, + * 勿再直读本投影判芯片(历史五层补丁的根因,见主流方案对照 F-MS-8)。 */ typedef enum { NET_SOCK_STATE_CLOSED = 0, /* 未使用 */ diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 68821deb..d66efbfc 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -36,10 +36,10 @@ | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 228,372 B (223.02 KB) | 1,048,576 B (1024 KB) | 21.8% | -| RW_IRAM1 | 97,952 B (95.66 KB) | 131,072 B (128 KB) | 74.7% | +| ER_IROM1 | 230,104 B (224.71 KB) | 1,048,576 B (1024 KB) | 21.9% | +| RW_IRAM1 | 97,912 B (95.62 KB) | 131,072 B (128 KB) | 74.7% | | RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% | -| **Flash (ROM 合计)** | **229,484 B (224.1 KB)** | 1,048,576 B (1024 KB) | **21.9%** | +| **Flash (ROM 合计)** | **231,208 B (225.8 KB)** | 1,048,576 B (1024 KB) | **22.0%** | 各区的容量与常量来源: @@ -110,7 +110,7 @@ | 任务 | 配置栈 | 字节 | 实际职责 | 2026-10-10 实测峰值 / 静态最坏链 | |---|---|---|---|---| | uiTask | `4096*4` | 16KB | LVGL 1bpp 离屏渲染(渲染缓冲在静态区,栈只走调用链) | 4016 B(余 75%)/ 静态 1336 B → 可大幅缩减 | -| netTask | `960*4` | 3.75KB | CH395F 轮询(DMA 搬运,栈压力小) | 920 B(余 76%)/ 静态 **1944 B**(2026-10-12 N32 监听模块化后复测,链尾经 net_listener_pump) | +| netTask | `960*4` | 3.75KB | CH395F 轮询(DMA 搬运,栈压力小) | 920 B(余 76%)/ 静态 **1944 B**(2026-10-12 N32 监听模块化后复测;N34 阶段 3 合并后复测 **1944 B** 不变,链尾经 net_listener_retry_tick→net_relisten_locked) | | ftpTask | `1024*4` | 4KB | lftpd FTP 服务 | 740 B(余 81%)/ 静态 1216 B | | testTask | `2048*4` | 8KB | 测试套件(产品构建不创建;存储套件深链需要) | 未创建 / 静态 496 B | | defaultTask | `1280*4` | 5KB | FatFS 挂载/探针 + 配置引擎 + syslog + 栈水位探针 | 2152 B(余 41%)/ 静态 **3016 B**(N-33 抬栈 1024→1280,2026-10-12) | diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index fe53fb96..7c131e20 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,14 +15,14 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 228,372 B | 1,048,576 B | 21.8% | 参考 | +| ER_IROM1 (Flash) | 230,104 B | 1,048,576 B | 21.9% | 参考 | | RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS | | UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 | -| RW_IRAM1 | 97,952 B | 131,072 B | 74.7% | PASS | +| RW_IRAM1 | 97,912 B | 131,072 B | 74.7% | PASS | | 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 | |---|---|---|---| -| 网络 | 13,498 B | 12 KB | ⚠ 超 1.2 KB | +| 网络 | 13,458 B | 12 KB | ⚠ 超 1.1 KB | | 存储 | 7,056 B | 12 KB | OK | | UI | 54,988 B | 16 KB | ⚠ 超 37.7 KB | | RTOS 堆 | 57,376 B | 56 KB | 堆=配额 | diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index 511cd685..f865b933 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -1,3 +1,3 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-12 209492 18332 1104 154184 227824 21.7 97864 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;console_log.o=4120;ch395f.o=4106;default_task.o=2736;sys_log_mcu.o=1924 -2026-10-13 209780 18592 1112 154264 228372 21.8 97952 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924 +2026-10-13 211348 18756 1104 154232 230104 21.9 97912 74.7 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=40960;lftpd.o=7884;ui_net_sink.o=6766;ui_port.o=6420;nand_ftl.o=6216;ch395f.o=4162;console_log.o=4120;default_task.o=2736;sys_log_mcu.o=1924 diff --git a/simulator/sim/net_socket_sim.c b/simulator/sim/net_socket_sim.c index 81eef003..ed64c237 100644 --- a/simulator/sim/net_socket_sim.c +++ b/simulator/sim/net_socket_sim.c @@ -708,6 +708,40 @@ int net_close(int sockfd) { return 0; } +/* + * 函数功能:主动拆掉当前 TCP 会话、保留监听(与 MCU 端同名 API——N34 阶段 3,E-02 收口) + * 入口参数:sockfd - socket 描述符 + * 出口参数:无 + * 返回值:0 成功(含幂等:无活动会话),-1 失败 + * 限定条件:socket 已打开(in_use == 1) + * 函数说明:模拟器为多连接后端(conn != 监听 fd):等价关闭该数据连接的 + * Winsock 句柄并收口 CLOSED(fd 槽不释放,外层 net_close 收尾); + * 监听 fd 不受影响。无活动会话幂等返 0。 + */ +int net_drop_conn(int sockfd) { + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { + s_net_errno = NET_ERR_BADF; + return -1; + } + if (s_socks[sockfd].in_use == 0) { + s_net_errno = NET_ERR_BADF; + return -1; + } + if (s_socks[sockfd].state != NET_SOCK_STATE_ESTABLISHED && + s_socks[sockfd].state != NET_SOCK_STATE_CONNECTING) { + /* 无活动会话:幂等成功 */ + s_net_errno = NET_OK; + return 0; + } + + closesocket(s_ws[sockfd]); + s_ws[sockfd] = INVALID_SOCKET; + s_socks[sockfd].state = NET_SOCK_STATE_CLOSED; + + s_net_errno = NET_OK; + return 0; +} + /* * 函数功能:注册事件回调 * 入口参数:sockfd - socket 描述符 diff --git a/test/host/e02_probe.py b/test/host/e02_probe.py index 19c49410..f6268f36 100644 --- a/test/host/e02_probe.py +++ b/test/host/e02_probe.py @@ -7,10 +7,18 @@ 全体消费者的队列消息停摆最长 30s; 2. 2s 后从 PC 发起 FTP(:21) 连接,测 banner 到达时延 = 队列响应性: 修复前 ≈ 25~30s(队头冻结),修复后 ≈ 数百 ms 内; - 3. 34s 时 30s 总预算应已耗尽:设备拆除该连接(修复版落 - "time_sync: no TIME packet in 30s, drop conn" 日志,旧版静默)、 - 监听重建,:8888 应能再接受新连接; - 4. 新连接发完整 8 字节 TIME 包 → 设备应落 "wall clock updated"。 + 3. 34s 时 30s 总预算应已耗尽:设备拆掉该会话(N34 阶段 3 net_drop_conn: + FIN 关闭 + 本层收口 + 重监听),随后**本探针模拟守规矩对端**: + close 自己的 socket; + 4. 芯片回 CLOSED(TC-NET-204 模式 A:TIME_WAIT 过渡 ~150ms)→ relisten, + :8888 应能再接受新连接(3s 重试窗内);新连接发完整 8 字节 TIME 包 → + 设备应落 "wall clock updated"。 + +⚠ 定案口径(Trap 07 定案更正 2026-10-11 + 实验方案 E13 §5):CH395F close +**无 RST 语义**。若对端内核存活(app 挂起但 socket 不关,内核自动 ACK FIN), +芯片停在 FIN_WAIT_2、监听恢复受限于对端 close——探针第 3 步的 close 正是 +模拟"对端配合"这一可恢复边界;不配合场景的兜底是芯片 TIMEOUT/keepalive +拆僵尸会话(E13 定案,30~90s)。 用法: python e02_probe.py 退出码: 0 = 队列响应性 PASS;1 = FAIL;2 = 探针自身异常 @@ -53,16 +61,32 @@ def main(): % ("PASS" if queue_ok else "FAIL", ("%.0f" % (dt * 1000)) if dt is not None else "n/a")) - # 3. 等 30s 总预算耗尽(设备应拆除挂起连接、重建监听) + # 3. 等 30s 总预算耗尽(设备应拆会话:FIN + 本层收口 + 重监听) remain = 34.0 - (time.time() - t0) if remain > 0: time.sleep(remain) - # 4. 监听应已重建:新连接可接入,且正常对时路径工作 + # 3b. 模拟守规矩对端:设备已发 FIN(drop_conn),本端 close 配合拆链。 + # CH395F close 无 RST 语义(Trap 07 定案更正):不配合的内核存活对端 + # 会把芯片停在 FIN_WAIT_2,监听恢复受限于对端 close——不在本探针判据内。 + try: + ts.close() + log("hung client socket closed (cooperate with device FIN)") + except OSError: + pass # 设备已先拆连,本地 close 报 ECONNRESET 属预期 + + # 4. 监听应已重建(芯片 TIME_WAIT 过渡 ~150ms + relisten):3s 重试窗 t2 = time.time() listener_ok = False - try: - ts2 = socket.create_connection((DEV, 8888), timeout=5) + ts2 = None + while time.time() - t2 < 3.0: + try: + ts2 = socket.create_connection((DEV, 8888), timeout=2) + break + except Exception as e: + log("second time-sync conn retry: %r" % (e,)) + time.sleep(0.3) + if ts2 is not None: dt2 = time.time() - t2 epoch = int(time.time()) log("new time-sync conn in %6.0f ms, sent full TIME packet (epoch=%d)" @@ -71,12 +95,8 @@ def main(): time.sleep(1.5) # 留时间给设备处理并打日志 ts2.close() listener_ok = True - except Exception as e: - log("second time-sync conn FAIL: %r" % (e,)) - try: - ts.close() - except OSError: - pass # 设备已先拆连,本地 close 报 ECONNRESET 属预期 + else: + log("second time-sync conn FAIL (3s window)") log("OVERALL: queue=%s listener=%s" % ("PASS" if queue_ok else "FAIL", "PASS" if listener_ok else "FAIL")) return 0 if (queue_ok and listener_ok) else 1 diff --git a/test/host/time_sync/stub/net_socket.h b/test/host/time_sync/stub/net_socket.h index 92b1df16..0cdd5ebb 100644 --- a/test/host/time_sync/stub/net_socket.h +++ b/test/host/time_sync/stub/net_socket.h @@ -60,6 +60,7 @@ int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen); int net_listen(int sockfd); int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen); int net_close(int sockfd); +int net_drop_conn(int sockfd); int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms); uint32_t net_get_generation(void); uint16_t net_htons(uint16_t hostshort); diff --git a/test/host/time_sync/time_sync_test.c b/test/host/time_sync/time_sync_test.c index 92278aa2..7aac8da2 100644 --- a/test/host/time_sync/time_sync_test.c +++ b/test/host/time_sync/time_sync_test.c @@ -235,6 +235,18 @@ int net_close(int sockfd) { return 0; } +/* ---- net_drop_conn(N34 阶段 3 E-02 收口):计数 + 记录 fd ---- */ +static int s_drop_calls; +static int s_drop_socks[32]; + +int net_drop_conn(int sockfd) { + if (s_drop_calls < 32) { + s_drop_socks[s_drop_calls] = sockfd; + } + s_drop_calls++; + return 0; +} + uint32_t net_get_generation(void) { int i = s_gen_calls++; @@ -351,6 +363,7 @@ static void fakes_reset(void) { s_accept_calls = 0; s_accept_seq_n = 0; s_close_calls = 0; + s_drop_calls = 0; s_gen_seq_n = 0; s_gen_default = 1u; s_gen_calls = 0; @@ -518,6 +531,7 @@ static void tc_handle_conn(void) { CHECK((s_recv_calls == 1) && (s_recv_len_log[0] == 16), "TC-TS-020c 首次请求 16 字节"); CHECK(s_recv_to_log[0] == 50U, "TC-TS-020d 单次等待片长 = 50ms"); CHECK(s_close_calls == 0, "TC-TS-020e 处理完**不**关闭连接(N34 阶段1:close 交外层 conn!=ls 分支)"); + CHECK(s_drop_calls == 0, "TC-TS-020g 成功会话不拆(drop 只用于放弃路径)"); CHECK(g_tstest_info_count == 1, "TC-TS-020f 留痕 wall clock updated"); /* 2) 分段送达:4 + 4 */ @@ -591,6 +605,7 @@ static void tc_handle_conn(void) { CHECK(s_recv_calls == 1, "TC-TS-027 预算耗尽后不再读(只读 1 次)"); CHECK(g_tstest_info_count == 1, "TC-TS-027b 留痕 no TIME packet in 30s, drop conn"); CHECK(s_close_calls == 0, "TC-TS-027c 预算耗尽也不关闭连接"); + CHECK(s_drop_calls == 1, "TC-TS-027d 预算耗尽主动拆会话(N34 阶段 3 E-02 收口:net_drop_conn 拆连接保监听)"); /* 9) 剩余预算不足一片 */ fakes_reset();