feat(N34): 阶段3 net层收敛——单一reconciler+忙等统一+镜像降级+E-02收口

实施方案 docs/网络/N34-自愈审计/实施方案.md §5 项1/2/3/5(项4 U15受E6暂缓):

- 3a 合并reconciler:net_listener_pump删除,与net_reconcile_tick合并为
  单一100ms tick;判据读驱动状态缓存(唯一刷新点=net_poll每轮全量刷新,
  E2背书18.1us);去抖分级②=1/③④⑤=2/①=4轮(M3让位窗400ms保留)+冷却1s;
  链路态订阅留痕(F-DRV-13);探针去抖删除(F-MS-9)
- 3b 忙等统一(P2b):listen/connect open重试与close排空循环改
  net_sock_reset_sync(驱动sock_reset:读空RX→CLOSE→排空事件→等
  byte0=00H,内置LISTEN态二段CLOSE补发);close不再读清全局中断寄存器
  (F-NET-7);net_poll每轮步进活跃复位状态机(驱动契约,防搁浅)
- 3c 镜像降级:net_sock_state_t声明为本层会话投影,芯片事实源=驱动缓存
- E-02收口:新API net_drop_conn(拆会话保监听,QUARANTINE语义,幂等;
  模拟器双端实现;timeSync 30s预算耗尽接入;主机单测TC-TS-027d)
  定案:CH395F close无RST,对内核存活不close的对端停FIN_WAIT_2,
  监听恢复受限于对端close(Trap 07补充),兜底=E13 keepalive/TIMEOUT

台架全绿(Code=211348 RW=1104 ZI=154232):@test PASS;[STACK] net
1116/3840;Trap22 :7003 50/50+:21三档各50/50;Trap23 RST10/10+FIN5/5;
Trap25 n30 11/11;E-02双PASS;iec104 18/18;对时跨会话连续;
ftp_regression 11/11;integrity 15/15 SHA;ci_local L0+L0.5全过。
This commit is contained in:
2026-10-13 15:09:58 +08:00
parent 83a70721bd
commit 6394853fef
14 changed files with 558 additions and 348 deletions
+6
View File
@@ -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,
+9 -5
View File
@@ -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 修复)
+17 -130
View File
@@ -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 形态③④ 调用)。 */
+7 -14
View File
@@ -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)。 */
+398 -171
View File
@@ -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
* (无害、仅留痕)时被消费,断开"杀下一条 → 新拆链 → 再杀"的繁殖链 */
{
+23 -5
View File
@@ -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 地址字符串转为网络字节序
+6 -1
View File
@@ -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, /* 未使用 */
@@ -36,10 +36,10 @@
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
| 区域 | 用量 | 容量 | 占比 |
|---|---|---|---|
| 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%** |
<!-- MEM_SNAPSHOT:END -->
各区的容量与常量来源:
@@ -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) |
+3 -3
View File
@@ -15,14 +15,14 @@
<!-- MEM_BUDGET:BEGIN generated by scripts/ram_baseline.py -->
| 区域 | 用量 | 容量 | 占比 | 门禁(≤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 | 堆=配额 |
+1 -1
View File
@@ -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
1 date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi
2 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
3 2026-10-13 209780 211348 18592 18756 1112 1104 154264 154232 228372 230104 21.8 21.9 97952 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
+34
View File
@@ -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 描述符
+34 -14
View File
@@ -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 <run-tag>
退出码: 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
+1
View File
@@ -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);
+15
View File
@@ -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();