# BSD Socket API 函数调用关系与设计说明 > **定位**:本文档是 `docs/网络/BSD_Socket_API_使用指南.md`(下称"使用指南")的**内部视图**伴读文档。 > 使用指南回答"**上层该怎么调**"(公开语义 / 示例 / 常见问题);本文回答"**内部函数各自做什么、互相怎么调用、为什么这么设计**"(调用关系 / 设计意图 / 踩坑)。 > > **范围**:MCU 端 `Drivers/BSP/NET/net_socket.c` + 其拆出的 > `Drivers/BSP/NET/net_listener.c`(**监听生命周期**)+ > `Drivers/BSP/NET/net_pending.c`(**挂起 recv / send 请求表**)。 > 模拟器端后端 `simulator/sim/net_socket_sim.c`(Winsock)是另一套实现,见 `docs/网络/BSD_Socket_API_使用指南.md` §2.3;`net_select.c` 不在此展开,见 `docs/网络/BSD_Socket_API_使用指南.md` §7 / §10.5。 > `net_listener.c` / `net_pending.c` 真机专属(模拟器不编译);设计依据分别见 > `docs/网络/网络自愈架构设计.md` §11 与 > `docs/网络/Net层阻塞模型架构设计.md`。 > > **交叉引用约定**:机制标 Trap 号 → `docs/问题记录/CH395F_Trap_Records.md`;用法语义引用使用指南 §;关键参数值与 `net_config.h` / `net_socket.c` 内宏一致。 --- ## 1. 一句话架构 **一切芯片(CH395F)操作收敛到唯一执行者 netTask 串行完成**;其他任务通过"入队消息 + 任务通知"表达请求,结果只经**任务通知值**回传;阻塞等待**不独占 netTask**(挂起登记 + 主循环节拍完成);监听器**常驻**(`auto_relisten` + D10 非阻塞重试 + **TRAP-NET-17 周期对账 + 监听重建合一 `net_reconcile_tick`(唯一重建入口=形态③④,100ms)** + TRAP-NET-18 排渣隔离,四层自愈;原监听器 pump 已并入对账)。 --- ## 2. 三个执行域 | 执行域 | 运行位置 / 节奏 | 允许的操作 | 典型调用者 | |---|---|---|---| | **netTask 主循环** | 独立 FreeRTOS 任务,1ms/tick | **唯一**可碰芯片(SPI2)的地方;4 个非阻塞 tick(D10 重试 / 挂起 recv / 挂起 send / TRAP-NET-17 对账+监听重建合一 reconciler);队列分派;事件处理器;用户回调 | `net_poll` / `net_process_messages` 及其下游全部 `_locked` / `_tick` / `handle_*` | | **应用任务侧** | ftpTask / uiTask / timeSyncTask / testTask 等 | 经队列的公共 API(入队 + `xTaskNotifyWait` 等回执);纯 RAM 直调 API;**禁止直碰芯片** | 各服务任务 | | **直调快路径** | 同步调用,不经过队列 | `net_socket` / 查询族 / 回调注册(纯 RAM);**直碰 SPI 的三例外**:`net_bind`(UDP) / `net_sendto` / `net_recvfrom` | 初始化阶段、netTask 上下文、测试套件(硬件层测试模式下 netTask 自挂起,testTask 独占芯片) | > ⚠ 直碰 SPI 的三例外**只能在"芯片访问者唯一"的上下文**调用:产品构建中应用任务一律走队列(TCP 全路径);三例外的实际消费者是测试套件(见 AGENTS.md"测试构建")。从其他任务直接调用会与 `net_poll` 的 SPI 事务竞争,破坏 CH395F 寄存器(TRAP-NET-14 实证:CONNECT 超时)。 --- ## 3. 总体调用关系图 ```mermaid flowchart TB subgraph APP["应用任务(非 netTask)"] PUB["经队列公共 API
netListen · netAccept · netConnect · netSend
netRecv · netRecvTo · netClose · netDropConn"] RAM["直调 · 纯 RAM
netSocket · netSockState · netGetSock
netGetErrno · netSetEventCb · netGetSockname
字节序(netHtons / netInetAddr …)"] SPI["直调 · 直碰 SPI(须芯片唯一上下文)
netBind(UDP) · netSendto · netRecvFrom"] CB["用户事件回调
(在 netTask 上下文执行!)"] end MQ[("netMsgQueue
8 槽 · 按值拷贝")] XN["xTaskNotify / xTaskNotifyWait
(结果回传的唯一通道)"] subgraph NT["netTask 主循环 — 1ms/tick;芯片操作唯一执行者"] NPM["netProcessMessages
先跑 4 个 tick,再排空队列分派"] NP["netPoll
INT# GPIO 轮询 → 全局中断 → 各 Socket SINT → 状态缓存全量刷新 + sock_reset 步进 → 每轮一批"] RetryTick["① netListenerRetryTick
D10 重监听重试:30ms / 2s 时限"] PendTick["② netPendingTick
挂起 recv:①数据 ②到点(仅显式预算) ③断连 ④tripwire 60s 只留痕"] PendShimTick["③ netSendPendingTick
挂起 send:①断连 ②推进(每轮≤1块) ③写完 ④无进展超时"] ReconcileTick["④ netReconcileTick
TRAP-NET-17 对账 + 监听重建合一:100ms / 分级去抖 / 冷却
读驱动状态缓存(①②③④⑤ + U11)"] DISP["队列分派 switch
SEND · RECV · CLOSE
CONNECT · LISTEN · ACCEPT · DROP_CONN"] subgraph LK["_locked / 内部分派目标(仅 netTask 上下文)"] LL["netListenLocked
初次监听(open 重试 3× + sock_reset 排渣)"] LN["netConnectLocked
open 重试 3×(sock_reset 排渣)+ tcpConnect"] LS["netSendLocked
分块 ≤1KB + sendReady 门控;超 NET_SEND_SPIN_MS 登记挂起 send"] LR["netRecvLocked
快路径 / DONTWAIT / 登记挂起"] LA["netAcceptLocked
先 netPoll + 状态判定"] LC["netCloseLocked
结束挂起 + cmd_done 快路径 / sock_reset(GRACEFUL)"] DC["netDropConnLocked
拆会话保监听:sock_reset(QUARANTINE) + 登记重监听"] RLS["netListenerTry → netRelistenLocked
单次尝试 + TRAP-NET-18 排渣隔离"] end EV["事件处理器
handleConnect · handleDisconnect
handleRecv · handleSend · handleTimeout"] ST["netListenerSettle
只置沉降/隔离事实(100ms / 400ms)"] FE["fireEvent → 用户回调"] PR["s_net_pending
(每 fd 至多一个挂起 recv)"] SR["s_net_send
(每 fd 至多一个挂起 send)"] CH[("ch395f.* 驱动
SPI2 串行事务")] end PUB -->|入队| MQ MQ --> DISP DISP --> LL & LN & LS & LR & LA & LC & DC & RLS NPM --> RetryTick & PendTick & PendShimTick & ReconcileTick & NP RetryTick --> RLS ReconcileTick -->|"① 已断"| EV ReconcileTick -->|"② 补 accept"| EV ReconcileTick -->|"⑤ 静默回收:RESYNC 只修本层 + 取消挂起(不发芯片命令,吸收原 net_resync_listen 语义)"| RSY["reconcile⑤ RESYNC 动作"] ReconcileTick -->|"③④ 监听失位 / 孤儿监听即重建(原 pump)"| RLS DC -->|"拆会话后登记重监听"| RLS NP --> EV EV --> FE FE -.-> CB EV -->|"断连/超时:只置事实"| ST ST -.->|"事实 → 由 reconciler 形态③④ 统一重建"| ReconcileTick LR -->|无数据+阻塞| PR PR --> PendTick PendTick -->|xTaskNotify| XN LS -->|等发送缓冲超预算| SR SR --> PendShimTick PendShimTick -->|xTaskNotify| XN XN -->|唤醒| PUB NP --> CH LL & LN & LS & LR & LA & LC & DC & RLS --> CH SPI -->|直碰 SPI| CH CH -->|INT# 低电平| NP ``` **读图要点**: 1. 应用侧只有两种"等":① 入队后 `xTaskNotifyWait` 等回执(同步语义);② 挂起 recv / send 的等待(同一通道,只是"完成"由 `netPendingTick` / `netSendPendingTick` 判定,不由 netTask 立即处理)。 2. `net_process_messages()` 开头的 **4 个 tick 是网络层自愈主干**:没有任何应用消息时,重试 / 挂起 recv 完成 / 挂起 send 推进 / 状态对账 + 监听重建照常运转——"断连后即使没人发消息,监听器也能自己活过来"靠的就是它们。 3. 事件处理器**只由 `net_poll` 驱动**,例外是 `net_reconcile_tick`(分级去抖 + 1s 冷却)可再入 `handle_disconnect_event` / `handle_connect_event`——对账是事件未收到的自愈通道(TRAP-NET-17)。**注意职责边界(对账与重建合并后)**:对账与事件处理器**只置事实**(置 CLOSED / 登记沉降),**重建监听的动作统一由 `net_reconcile_tick` 形态③④ 做**(本层唯一重建入口)——"谁先看到 DISCONNECT 就谁重建"的竞争在结构上消失。 4. 回调 `event_cb` 在 **netTask 上下文**执行:回调内不得阻塞、不得发网络操作。 5. **模块归属**:`RetryTick`(D10 重试)/ `RLS` 的 `net_listener_request` / `ST`(settle)在 `net_listener.c`(真机专属、模拟器不编译);挂起 recv 完成检查(`net_pending_tick`)、挂起 send 推进(`net_send_pending_tick`)与两张挂起表(`PR` / `SR`)在 `net_pending.c`(真机专属);其余 tick(含合并后的 `ReconcileTick` reconciler)与 `_locked` 在 `net_socket.c`。 --- ## 4. 函数清单:设计作用与调用关系 ### 4.1 初始化与代次(直调;`net_init` 的调用者 = netTask) | 函数 | 设计作用 | 主要调用 | 意图 / 注意 | |---|---|---|---| | `net_init(ip,mask,gw,mac)` | 芯片一次性配置:探测/复位 → 参数 → MAC → 协议栈初始化 → 8 Socket 缓冲布局 → 使能 PING | `net_pending_cancel_all` → `ch395f_check_exist` / `ch395f_reset` / `set_fun_para(0x08)` / `set_tcp_mss` / `set_retrans_count+period` / `set_keepalive_idle+intvl+cnt` → `set_mac_addr` → `parse_ip_string` + `set_ip_addr` / `set_gwip_addr` / `set_mask_addr`(或 `set_dhcp`)→ `ch395f_init` → `set_send_buf` / `set_recv_buf` ×8 → `ping_enable` | ① **重建前**先取消全部挂起 recv 并逐一通知(不通知 = 调用方永久挂死),且必须早于控制块清零与芯片复位;② 重传/KeepAlive/**MAC**/IP/GW/MASK 参数**必须**在 `ch395f_init` 之前下发(`docs/芯片手册/CH395F.md` §9.2.1:SET_MAC 为步骤①,INIT_CH395 加载 MAC——顺序反了本次上电不生效);③ `fun_para=0x08`:bit3 SOCKET_CLOSE(DISCONNECT/TIMEOUT 后不清缓冲,软件排空后手动关);bit0/bit1 保持 0——8 个 Socket 由索引 `_SN` 寻址 + 缓冲显式分配实现,与 FUN_PARA 无关(bit0 是保留位,必须为 0);④ 缓冲布局每 Socket 6 块(收 4 + 发 2)×8 = 48 块恰好用尽,Socket 0~7 必须显式重分配(TRAP-NET-01/-02);⑤ 捕获 `xTaskGetCurrentTaskHandle` 存入 `s_net_task_handle`(防自通知死锁的判据) | | `net_get_generation()` | 读网络代次号(纯 RAM) | 无 | FTP/UI/对时服务据此感知"网络已重建"→ 放弃旧监听并重建 | | `net_generation_inc()` | 代次号 +1 | 无 | 唯一合法递增点 = `net_stack_bring_up()` 末尾(PHY/链路就绪后)——太早递增会让服务在网络不可用时抢跑重建;模拟器端恒为 0(无重建概念,上层自愈逻辑空转) | ### 4.2 主循环:事件泵与队列分派(仅 netTask) | 函数 | 设计作用 | 主要调用 | 意图 / 注意 | |---|---|---|---| | `net_poll()` | 事件泵:GPIO 轮询 INT#(低=有待处理)→ `GET_GLOB_INT_STATUS_ALL`(读=清)→ 按位图逐 Socket 读 SINT → 分派 5 类事件;**每轮末尾**对在用 Socket 全量刷新**驱动状态缓存** + 步进 `ch395f_sock_reset_poll` | `HAL_GPIO_ReadPin` → `ch395f_get_glob_int_status_all` → `ch395f_get_sock_int_status` ×n → `handle_connect/disconnect/recv/send/timeout_event` → `ch395f_get_sock_state` ×n(刷新缓存)+ `ch395f_sock_reset_poll` ×n | **RECV 中断是电平触发**:每次调用**只处理一批**(do-while(0)),处理完立即返回,否则主循环(echo/应用逻辑)饿死(TRAP-NET-08);SENBUF_FREE 只置 `send_ready` 不记事件;状态缓存**唯一刷新点**在此(reconciler 只读缓存,E2 实测单次回读 18.1µs);`sock_reset_poll` 步进使同步包装 600ms 放弃后的复位机不搁浅(IDLE 槽纯 RAM 判零 SPI) | | `net_process_messages()` | 队列入口 + 自愈 tick 宿主:每轮**先**跑 4 个非阻塞 tick,再排空队列逐条分派,结果 `xTaskNotify` 回传 | `net_listener_tick` → `net_pending_tick` → `net_send_pending_tick` → `net_reconcile_tick`(对账+重建合一)→ `osMessageQueueGet`(0 超时) ×n → switch 分派 → `xTaskNotify` | ① tick 放在队列**之前**:断连后即使没有任何应用消息,监听器也能自主恢复、挂起 recv / send 也能完成;② RECV 若返回 `NET_PENDING_REGISTERED`(哨兵 0x70000000)则**不发即时回执**(`do_notify=0`),完成时才通知;③ 失败结果把 `s_net_errno`(具体负错误码)压入通知值——调用方恢复 errno 后对外统一 -1(BSD 语义);④ 通知值**整个 32 位**都是结果(不压位域标签,否则 32768 这类大字节数会溢出为负);⑤ **对账①置 CLOSED 后形态③④同轮即可重建**(合并后,原"pump 置于对账之后"的顺序依赖在单 tick 内自然成立) | ### 4.3 经队列的公共 API 及其 netTask 侧实现 调用侧四件套:**参数检查 → `osMessageQueuePut`(osWaitForever) → `net_wait_result` → errno 恢复/返回值**。差异只在消息类型、`timeout_ms` 与结果映射。 | 公共 API | 消息 | netTask 侧实现 | 关键芯片命令 | 返回语义 | 设计意图 / 注意 | |---|---|---|---|---|---| | `net_listen` | `NET_MSG_LISTEN` | `net_listen_locked` | `set_proto_type` + `set_sour_port` + `open_socket`(**open 重试 3×,失败走 `net_sock_reset_sync` 确定性排渣**)+ `tcp_listen` + 清**本 Socket** 中断状态 | 0 / -1 | **初次**监听允许有界重试(启动期无竞争);成功后置 `auto_relisten=1`——监听常驻的源头;启动前只清本 Socket SINT(唯一读者红线:全局寄存器唯一读者 = `net_poll`,全量读清会焊死其它 Socket 事件位,见 `docs/网络/网络自愈架构设计.md` §4.10);open 失败重试原"close+盲等 500ms"已换 sock_reset(GRACEFUL 100ms 底,E4 定案复位后 open 必成) | | `net_accept` | `NET_MSG_ACCEPT` | `net_accept_locked` | 无(先 `net_poll` 泵一次 + 状态判定) | 监听 fd 自身(单连接)/ -1(WOULDBLOCK) | 单连接:监听 Socket 在 CONNECT 后**自身即数据通道**,返回的就是它;CLOSED + auto_relisten 时触发**非阻塞** `net_listener_request`;**accept 路径不做自愈**——单次状态读会误判"刚 listen 未生效"致重建风暴(2026-09-23 实测),统一交给 `net_reconcile_tick` 形态③④(合并后的单一重建入口) | | `net_connect` | `NET_MSG_CONNECT` | `net_connect_locked` | `set_proto_type` + `open_socket`(open 重试 3× + `net_sock_reset_sync` 排渣)+ 清残留中断(只清本 Socket SINT,`ch395f_sock_events`)+ `set_sour_port` / `set_des_ip`(大端数组)/ `set_des_port` + `tcp_connect` | 0 = 请求已发出(**异步**)/ -1 | open **先于**寄存器写入(open 会复位寄存器);open 后清残留 DISCONNECT,防新连接秒断(TRAP-NET-12);目的 IP 字节序写反 = CONNECT TIMEOUT(TRAP-NET-14);返回 0 ≠ 已建立——调用方以 `net_sock_state` 轮询等 ESTABLISHED | | `net_send` | `NET_MSG_SEND` | `net_send_locked` | `ch395f_write_send_buf`(分块)+ 等 SENBUF_FREE(`net_poll` 轮询,**有界自旋 ≤ `NET_SEND_SPIN_MS`**);超预算登记挂起 send(`net_send_pending_register`) | 实际字节数 / -1 | 分块 ≤ `NET_SEND_CHUNK_MAX` = 1KB(**硬件发送 FIFO 硬上限**,TRAP-NET-16:写穿 FIFO = 数据错位);`send_ready` 门控用 `net_poll` 轮询而非 osDelay,**自旋超 `NET_SEND_SPIN_MS`(2ms)即转挂起**(netTask 最坏停滞由 1s 压到 2ms);短写**必须**告警——静默截断会让对端永久等待剩余字节(2026-09-21 实证:UI 推流应答截断 → 客户端失联) | | `net_recv` | `NET_MSG_RECV`(timeout=0→**无限等待**,无 30s 默认预算) | `net_recv_locked` | `net_poll` + `get_recv_len` / `read_recv_buf` | >0 / 0(EOF) / -1(errno = WOULDBLOCK / CONNRESET;阻塞形态无 TIMEDOUT 一档) | 快路径:数据已在 → 立即读出返回(允许部分读);无数据 + 阻塞(`blocking` 属性=1 且未 DONTWAIT)→ **登记挂起后立即返回**(挂起登记,netTask 零占用),唤醒源=数据/EOF/close/网络重建;DONTWAIT / `blocking=0` + CLOSED/session_dead → EOF + 触发重监听;⚠ IWDG 受监控任务禁用无限形态(`net_socket.h` 文档契约) | | `net_recv_nb` | `NET_MSG_RECV`(flags=DONTWAIT) | `net_recv_locked` | 同上 | >0 / 0 / -1(WOULDBLOCK 等) | 原 `NET_MSG_DONTWAIT` 路径转正的具名入口;不改变 `blocking` 属性;受 IWDG 监控 / 有周期职责任务的推荐默认(ui_net_sink / iec104_serv 已迁) | | `net_recv_to` | `NET_MSG_RECV`(timeout>0) | `net_recv_locked` | 同上 | >0 / 0 / -1 / **-2(本片超时无数据)** | 队头阻塞修复配套 + **显式预算**入口(等价 Python settimeout,无隐藏默认值):调用方把长等待切成有界片(如剩余预算),**总超时语义不变、单次占用有界**;`timeout_ms=0` 直接拒(0 = 无限等待,与本接口"有界"存在理由相反);超时**只凭 -2 判定、不得依赖 errno**(`s_net_errno` 是跨任务共享全局量,随时被别的任务覆盖) | | `net_close` | `NET_MSG_CLOSE` | `net_close_locked` | `ch395f_cmd_done(CLOSE)` 快路径(芯片已 CLOSED → 只清本 Socket 残留事件)/ `net_sock_reset_sync(GRACEFUL)` 确定性排渣 | 0 / -1 | **先**结束本 fd 挂起 recv(排空/取消 + 必通知,关不得让等待者悬挂);排空只碰本 Socket SINT,**不再读清全局中断寄存器**(唯一读者红线:全局唯一读者 = `net_poll`,见 `docs/网络/网络自愈架构设计.md` §4.10);原"close + 500ms 盲等排空循环"已换 sock_reset(内置 LISTEN 态二段 CLOSE 补发,E2 补充发现);控制块整体清零(`last_peer_port` 幽灵基准跨 fd 保留,属「relisten 沉降段原语化受阻」) | | `net_drop_conn` | `NET_MSG_DROP_CONN` | `net_drop_conn_locked` | `net_pending_settle_close` + `net_sock_reset_sync(QUARANTINE)` +(auto_relisten)`net_listener_request` | 0(含幂等)/ -1 | **拆会话保监听**(队头阻塞收口):应用放弃会话(对时 30s 预算耗尽 / 协议超时)而单连接监听被挂起对端永久占住时的应用侧出口;EOF 路径**不得**调(`auto_relisten` 已接管);非 ESTABLISHED/CONNECTING 直接返 0 不动监听;本层收口 CLOSED 不清 fd(保留 local_port/auto_relisten),监听型 fd 登记重监听、失败交 D10 + reconciler 形态④ | | ~~`net_relisten`~~ | ~~`NET_MSG_RELISTEN`~~ | **已删除**(消息类型同删) | — | — | 原"无条件芯片侧重建"与 pump/D10 完全重叠,且对健康监听器是 footgun(open→USE→close 自毁 ~1s);生产零调用。历史实现见 `net_socket.c` git 历史 | | ~~`net_resync_listen`~~ | ~~`NET_MSG_RESYNC_LISTEN`~~ | **已删除**(语义并入对账⑤) | — | — | D14 静默回收(芯片自行回监听、不发 DISCONNECT)现由 `net_reconcile_tick` 判据⑤自动认领:本层 ESTABLISHED 且芯片 0x05/0x01 → 只修本层(state→LISTENING + 转登 `last_peer_port` + 清 `lost_conn_seen` + 取消挂起 recv),**不发芯片命令**(纪律原样保留,run8 实证 170s 拒绝);去抖/冷却沿用对账框架 | ### 4.4 直调 API(不经队列) | 函数 | 设计作用 | 主要调用 | 意图 / 注意 | |---|---|---|---| | `net_socket(domain,type)` | 分配空闲控制块(`alloc_socket` 线性扫描 `in_use=0`)→ CREATED | `alloc_socket` + `net_listener_clear` + `net_pending_clear` | 新 fd 不得继承上一次的待重试/挂起状态(复用描述符的经典坑);domain 仅 `NET_AF_INET` | | `net_bind(sockfd,addr,addrlen)` | TCP:仅记录本地端口(port=0 → 动态端口 49152 起递增)→ BOUND,**无芯片命令**;UDP:完整开 Socket → UDP_OPEN | UDP 路径:`set_proto_type(UDP)` + `set_sour_port` + `set_des_ip(0xFFFFFFFF)` + `set_des_port(0)` + `open_socket`(Socket 4~7 兜底重设缓冲) | TCP 的 bind 只是"记账",真正的芯片动作在 listen/connect;UDP 直碰 SPI(§2 三例外之一);动态端口回绕 65535→49152 | | `net_sendto` / `net_recvfrom` | UDP 收发(直碰 SPI,不经队列) | `set_des_ip` / `set_des_port` + `write_send_buf`(分块 1KB + `net_poll` 门控);`net_poll` + `get_recv_len` + `read_recv_buf` | 同 §2 上下文约束;recv 时 UDP Server 模式前 8 字节是固定头(源 IP + 端口 + 长度),先剥头填 `src_addr` 再读数据;recvfrom 阻塞语义与 net_recv 同构(blocking 属性 / DONTWAIT,无默认预算;轮询自旋实现,生产零调用) | | `net_setblocking(sockfd,on)` | 设置阻塞**模式属性**(直写控制块,不经队列) | 写 `s_net_socks[fd].blocking` | 对齐 BSD `O_NONBLOCK` / Python `setblocking`:"模式归使用者、超时归调用方";单字节原子写(先例 `net_set_event_cb`),最坏一次在途 recv 用旧模式;`net_socket()` 默认置 1(close 的 memset 清零后新 fd 必须显式重置) | | `net_wait_result(kind,sockfd)` | 调用侧两段式等待:**零等待拾取** → 阻塞等回执 | `xTaskNotifyWait` ×2 | ① 零等待段兜住"入队→等待入口"间隙落地的通知(2026-09-22 台架实证丢通知);② **清除掩码必须全 1**(传 0 = 不清 → 上次结果残留被重读,run3~11 挂死/错位的真根因);③ 必须判 **pdTRUE** 而非 `v!=0`(合法结果 0 = EOF 会被误判"无通知"→ 永久挂死);④ 32 位整体作结果(32768 大字节数不截断);⑤ 本路径**禁止**加逐调用日志(曾占日志 98.6%、冲掉 [LAT] 证据行,run14 实证) | | `net_sock_state(sockfd)` | 跨任务只读状态快照(单字节读,无锁) | 无 | 描述符无效/未占用 → -1;值可能滞后 netTask 最新状态——用于**轮询**(客户端等 ESTABLISHED),不替代 accept/connect | | `net_get_sock(sockfd)` | 返回控制块指针 | 无 | netTask 上下文内用;跨任务只可读单字节 `state`(即 `net_sock_state`),不得读结构体其他成员 | | `net_get_errno()` | 读最后错误码 | 无 | `s_net_errno` 是**跨任务共享**全局量:只作参考,判定以返回值为准(`net_recv_to` 的 -2 同理) | | `net_set_event_cb(sockfd,cb,arg)` | 注册/注销事件回调 | 无 | 回调由 `fire_event` 在 **netTask 上下文**调用 → 回调内不得阻塞/发网络操作;`NULL` 注销 | | `net_getsockname(sockfd,addr,addrlen)` | 本地地址(IP + 端口) | 读 `s_local_ip`(`net_init` 缓存)+ `local_port` | IP 是 `net_init` 时的值(DHCP 场景不更新——本构建固定 IP);port=0 时返回已分配动态端口 | | 字节序族 `net_htons/ntohs/htonl/ntohl/inet_addr/inet_ntoa` | 纯计算转换 | 无 | 无状态、任意上下文可调 | ### 4.5 挂起 recv 子系统(阻塞等待不独占 netTask) **模型**:阻塞等待 = **挂起请求登记**(每 fd 至多一个,禁止覆盖)+ **主循环节拍完成检查** + **通知唤醒**(LwIP netconn 模型)。消除队头阻塞(旧实现:netTask 自旋等数据,一次 30s 空闲 recv 冻住全网 17.7s)。 | 函数 | 设计作用 | 主要调用 | 意图 / 注意 | |---|---|---|---| | `net_pending_register` | 登记挂起(`net_recv_locked` 的"无数据 + 阻塞"路径) | 写 `s_net_pending[fd]` | 每 fd 至多一个:**覆盖会丢失前调用方通知 = 永久挂死**;执行任务自身(netTask)发起的等待直接拒绝(自等自通知 = 死锁,netTask 内须用 DONTWAIT) | | `net_pending_tick` | 完成检查(`net_process_messages` 每轮调用,非阻塞) | ① `get_recv_len` + `read_recv_buf` ② 到点(仅显式预算) ③ `get_recv_len` 排空 + `net_listener_request` ④ `net_pending_complete` | 按 **①数据 ②到点(仅 `net_recv_to` 显式预算;无限登记无 deadline)③断连** 顺序检查(逐项对齐旧自旋语义);CONNECTING/LISTENING 等状态**保持等待**(CONNECT→ESTABLISHED 是合法迁移,不做状态推断取消,防误杀正在建链的等待);**挂起超时留痕探针**(编译期选项 `NET_PENDING_TRIPWIRE`,默认关):挂起超阈值 = 完成逻辑缺位 → 只 `[ERR]` 留痕一次,**绝不强制结束、绝不伪造 TIMEDOUT**——③ 与显式 cancel 是无限 recv 的全部唤醒责任方 | | `net_pending_complete` | 所有完成/取消路径的公共出口 | `xTaskNotify(caller, result)` + 清 active | 通知值 = 结果原值(>0 字节 / 0 EOF / 负错误码,与消息分派同一回传通道);失败结果同步 `s_net_errno`(netTask 上下文一致性);无在位挂起时静默返回(防重复通知) | | `net_pending_settle_close` | close 前**结束**本 fd 挂起 | `get_recv_len` + `read_recv_buf` + `net_pending_complete` | 关闭不得丢已到达数据(芯片缓冲 close 前可读):有数据 → 按字节数通知;无数据 → 取消(CONNRESET);先于 `ch395f_close_socket` 执行 | | `net_pending_cancel` / `cancel_all` | 显式取消(**必通知**) | `net_pending_complete` | 取消路径:陈旧 resync(单 fd)/ 网络重建(全部,`net_init` 开头,早于控制块清零)——"取消不通知 = 调用方永久挂死" | | `net_pending_clear` | 清除**不通知** | 清 `active` | 仅用于"新 fd 不得继承"(`net_socket`)与 close 后幂等清理(`net_close_locked`)——这两处前驱通知已由 settle 路径发出 | ### 4.6 D10 非阻塞重监听重试子系统(`net_listener.c`) **模型**:重开失败 = **登记待重试**(30ms 节奏 / 2s 时限),`net_listener_tick` 到点重试——任何时刻不阻塞任务。旧实现 `osDelay(500)` 忙等:netTask 冻结 500ms(全网停摆)、实测恢复 588ms、期间重连被拒(M3 台架)。 > **模块归属**:本子系统的函数在 `net_listener.c`(真机专属,模拟器不编译);沉降等级只经 > `net_listener_settle` 登记,外部不得直接写 `s_relisten_quarantine[]`。 | 函数 | 设计作用 | 主要调用 | 意图 / 注意 | |---|---|---|---| | `net_listener_settle` | 登记"本 fd 会话已结束,重建前需沉降/隔离"(**只置事实**) | 写 `s_relisten_quarantine[fd]`(等级:GRACEFUL 100ms / QUARANTINE 400ms) | 由 `handle_disconnect_event`(GRACEFUL)/ `handle_timeout_event`(QUARANTINE)调用。**只置事实,动作只在 `net_listener_tick` 一处发生**;越界 fd 静默忽略 | | `net_listener_request` | **登记**"本 fd 期望回到 LISTEN"(只置意图,**不碰芯片**) | 写 `s_relisten_req[fd]`(来源 +1)+ `next=0`(下一轮 tick 即尝试) | 取代原 `net_listener_try`(当场动作 → 4 个调用点各自补 `state == CLOSED` 门,U12 漏一处即事故;E17 收口后门上收到 tick)。已在请求中 → **不重置重试时刻**:否则 10ms 一次的 accept 轮询会把重试时刻不断往后推 → 永不重试(饥饿);越界 fd 静默忽略 | | `net_relisten_locked` | 真正的 open + listen(**单次尝试**) | (沉降期)`net_drain_chip_rx` + `close_socket` + `get_sock_int_status` / `get_glob_int_status_all` + `get_socket_status` → `set_proto_type` + `set_sour_port` + `open_socket` + `tcp_listen` + 清中断 | ① **严禁 osDelay 忙等**:失败只清理并返回,沉降交给重试轮询;② **TRAP-NET-18 沉降门**:带沉降标记的 socket(异常释放 TIMEOUT → QUARANTINE 400ms;正常释放 DISCONNECT → GRACEFUL 100ms)→ 排空 RX 残渣 + 主动 close + 排空中断 + 等芯片回 CLOSED/TCP_CLOSED;**上限(800ms)只留痕、不强行放行**——芯片未回 CLOSED 时 open 注定吃 USE 错,兜底 = D10 预算到期 + 形态④ 重新登记;③ 成功只置 state=LISTENING,**不 memset**(fd 与 `auto_relisten` 保留——BSD 风格监听常驻) | | `net_listener_tick` | 监听重建的**唯一执行者**(netTask 每轮,非阻塞) | `net_relisten_locked` → `net_relisten_done` / 顺延 30ms / 超预算清登记 + `[ERR]` | **门集集中这一处**:控制块有效 + `in_use` + 状态 ∈ {LISTENING, CLOSED} + 监听型(`auto_relisten` / STREAM / `local_port≠0`)+ **芯片已在 LISTEN 即视为请求已达成、静默了结、绝不 open**(U12 结构性防护,读驱动状态缓存零 SPI)+ **同 fd 有在途挂起 recv / send 时跳过**(重开 Socket 会摧毁在途连接 / 截断在途传输,挂起保险)+ 30ms 节奏 / 2s 预算(D10;超预算清登记并留痕,之后由 reconciler 形态③④ 重新登记,监听不永久失效) | | `net_relisten_done` | 标记完成 | `net_listener_clear` +(探针构建)恢复计时样本 | 恢复时延 = 首次尝试 → 完成(延迟探针 `relisten` 段) | | `net_listener_clear` | 取消该 fd 的重建请求 / 沉降 | 清 `req/next/dead` + `settle_dead/settle_start` + `quarantine` | 触发点:`net_socket`(新 fd 不继承)/ `net_close_locked`(关闭即取消)/ tick 内"登记已陈旧或已达成";沉降/隔离状态必须一并清,否则残留标记会污染下一次复监听 | | `net_drain_chip_rx` | 排空芯片 RX 残留(256B 分块,≤16 次 = 4KB 上限) | `get_recv_len` + `read_recv_buf` | TRAP-NET-18:中止/异常断开时芯片 RX 缓冲残留上一条连接的数据(实测 ~2048B),不排空会被下一条连接当自己的数据读走(静默截断);分块读避免新增静态缓冲(内存预算紧张) | ### 4.7 监听重建(原监听器 pump;已并入 `net_reconcile_tick` 形态③④/U11) > **现状**:`net_listener_pump()` 已**删除**——其全部三种情形作为**形态③④ + U11** 并入 `net_reconcile_tick`(§4.8,单一 100ms tick,判据读驱动状态缓存)。本节保留"单一重建所有者"的设计口径与情形表;实现位置与参数以 §4.8 为准。 **设计意图(设计文档 §4 不变量 II)**:其它所有位置**只置事实**(会话结束 / 状态迁移 / 事件派发),**不做重建动作**——于是"谁先看到 DISCONNECT 就谁重建"这类**多所有者竞争在结构上消失**。`net_reconcile_tick` 形态③④是**本层唯一**的周期性重建决策者。 **覆盖三种情形**(认领门槛:在用 + `auto_relisten` + TCP + 已绑端口——原 pump 口径,少了它"本层 CLOSED + 已绑端口"的出向连接会被误判为监听器并被 relisten): | # | 本层状态 / 芯片状态 | 动作 | 来源 | |---|---|---|---| | ③ | 本层 LISTENING 而芯片监听**已失位** | 重建(`net_listener_request`) | 原对账③ / 原 pump① | | ④ | 本层 CLOSED(会话已结束)而芯片**不在** LISTEN | 重建(`net_listener_request`) | 原对账④ / 原 pump② | | U11 | 本层 CLOSED 而芯片**正在** LISTEN | **只做状态重同步**(CLOSED→LISTENING),**不发任何芯片命令** | U11 缺口 | 情形 U11 的理由:芯片监听器是**健康**的,重下发 open+listen 会因 `USE` 错误走进关闭分支、把正听着的 Socket 关掉(U12 同源事故);只把本层状态收回 LISTENING 即可(与对账⑤ RESYNC 同一纪律——原 `net_resync_listen` 已退役)。 **去抖 + 冷却**(③④ 沿用原实测结论,取值不变):同一失配需**连续 2 轮**(`NET_RECON_STABLE_RELISTEN`,≈200ms)才动手;同一 Socket 动手后**冷却 1s**(防抖成风暴,2026-09-23 实证);U11 被动重同步**不去抖、不占冷却**(零风险)。**tick 周期 100ms**(原 pump 周期;合并后对账也从 200ms 提到 100ms):比 200ms 快、又不至于像 50ms 那样频繁 close/open 与消费者收发撞车(2026-10-12 台架实测 50ms 会把 uiTask 最坏单轮从 172ms 推到 1034ms)。 > `session_dead` **不在此清零**(与 `net_relisten_locked` 同一契约:重建监听 ≠ 应用已确认会话结束;清零只发生在 `handle_connect_event`)。 ### 4.8 TRAP-NET-17 周期对账 + 监听重建(合一 `net_reconcile_tick`,事件未收到自愈,`net_events.c`) `net_reconcile_tick()`(`net_process_messages` 每轮调用;周期 **100ms**,`NET_RECONCILE_PERIOD_MS`;含原 pump 形态③④/U11): **问题**:芯片在快速复连场景会**收不到 CONNECT/DISCONNECT 事件**(读清消费竞态——E14 复核:非芯片侧丢失;或芯片静默回收 Socket 不发中断);事件未收到时纯事件驱动无法自愈——典型:对端连接一直占着单连接监听的"后备"(芯片 TCP 仍 ESTABLISHED),后续 SYN 全被拒,而应用因未收到 DISCONNECT 仍以为有客户端(永不调 accept)。 **机制**:对每个在用 Socket 读**驱动状态缓存**(`ch395f_sock_state_cached`,唯一刷新点 = `net_poll` 每轮全量刷新,E2 实测单次回读 18.1µs;**本 tick 不再自发状态 SPI**,唯一按需 SPI = 形态② 命中时回读对端端口做幽灵甄别),判定**五类**失同步 + 一类被动重同步并修正: | # | 失同步 | 动作 | 复用的实现 | |---|---|---|---| | ① | 本层 ESTABLISHED 而芯片已无连接(sock/tcp = CLOSED) | 按断开处理 → CLOSED + 回调(应用读到 EOF 后由形态③④重建监听) | `handle_disconnect_event`(内置陈旧事件甄别) | | ② | 本层 LISTENING 而芯片已完成握手(tcp = ESTABLISHED) | 补做 standalone accept 簿记 | `handle_connect_event` | | ③ | 本层 LISTENING(监听型 fd)而芯片监听失位 | 重建 | `net_listener_request`(原 pump①) | | ④ | 本层 CLOSED(监听型 fd)而芯片不在 LISTEN | 重建 | `net_listener_request`(原 pump②) | | ⑤ | 本层 ESTABLISHED 而芯片静默回收回 LISTEN(0x05/0x01,无 DISCONNECT 中断) | RESYNC 只修本层、不发芯片命令 | 原 `net_resync_listen` 语义(已并入) | | U11 | 本层 CLOSED(监听型 fd)而芯片**正在** LISTEN | 只重同步 CLOSED→LISTENING,不发芯片命令 | 原 pump③ | > ⚠ **单一所有者不变量**:重建决策**只此一处**(原 `net_listener_pump` 已删除,③④/U11 就在本 tick); > 认领门槛见 §4.7(在用 + `auto_relisten` + TCP + 已绑端口)——少了它"本层 CLOSED + 已绑端口"的 > 出向连接会被误判为监听器并 relisten。 **去抖 + 冷却(缺一不可)**:单次状态读可能落在"刚 open/listen 尚未生效"的瞬态——**2026-09-23 实测**:无去抖的 accept 路径自愈把监听反复重建直至 `relisten give up`(三服务口全死、89 行 ERROR)。故**分级去抖**(单位=轮,100ms/轮):②=1 轮(`NET_RECON_STABLE_ACCEPT`,幽灵甄别兜底假阳性);③④⑤=2 轮(`NET_RECON_STABLE_RELISTEN`);①=4 轮(`NET_RECON_STABLE_DEATH`,**勿改小**——风暴态三机制里 M3 可疑门的让位再入 400ms 租约,见 `docs/网络/网络自愈架构设计.md` §4.1.6,E9a 实证);同一 Socket 动手后**冷却 1s**(`NET_RECONCILE_COOLDOWN_MS`;U11 被动重同步不去抖不占冷却)。开销:每 100ms 每在用 Socket 的状态回读已收拢在 `net_poll`(≈100µs/轮),本 tick 本身零额外 SPI。 ### 4.9 事件处理器(netTask 上下文;由 `net_poll` 驱动,对账 tick 可再入) | 函数 | 设计作用 | 主要调用 | 意图 / 注意 | |---|---|---|---| | `handle_connect_event` | LISTENING → standalone accept(回读对端地址 → ESTABLISHED + `send_ready=1` + 清残留 TIMEOUT + 回调);CONNECTING → 客户端连接成功(回读对端 + 使能 KeepAlive + 回调) | `ch395f_get_remot_ipp` + `get_sock_int_status` +(client)`set_keepalive_enable` + `fire_event` | GET_REMOT_IPP 为**小端**输出(与 set_des_ip 的大端相反)——解析别抄反;`session_dead` 的**唯一清零点** | | `handle_disconnect_event` | 状态机迁移 + 陈旧事件甄别 + **登记沉降事实**(不再自己重建) | `ch395f_get_socket_status`(事件路径**主动 SPI**,不用缓存——缓存刷新在 net_poll 末尾,事件路径上必然滞后)→(LISTENING:只回读留痕,监听失位交 **reconciler 形态③** 重建;仍在监听 → 判陈旧丢弃)/(ESTABLISHED:芯片仍 ESTABLISHED → 判陈旧丢弃;M3 可疑门 → 置可疑电平让位对账①,见 `docs/网络/网络自愈架构设计.md` §4.1.6) | ① **陈旧 DISCONNECT 甄别**(D20):同一 fd"关闭→重监听→再连接"循环复用,上一条连接的 FIN/ACK 可能在 fd 已复用时才上报 → 不甄别则新连接秒断(实测:FTP 同会话第二个数据连接必 425);② 正常释放(FIN)→ **GRACEFUL 沉降 100ms**(只置事实,重建由 `net_reconcile_tick` 形态③④ 做);③ 置 CLOSED 后**不立即关芯片 Socket**——留给 `net_recv_locked` 排空剩余数据(FIN 可能先于数据到达) | | `handle_recv_event` | 置 `readable` + 回调;**不读 recv_len** | `fire_event` | 不读长度是为了避免与 `net_recv_locked` 的 `ch395f_get_recv_len` 竞争(旧"分批读归零"机理,未证实);`readable` 供 `net_select` 无 SPI 访问判读 | | `handle_send_event` | 发送缓冲已空:触发回调(`send_ready` 由 SENBUF_FREE 前置位) | `fire_event` | — | | `handle_timeout_event` | CONNECTING → CREATED(连接超时);**ESTABLISHED → 登记隔离 + CLOSED**(TRAP-NET-18);LISTENING → 仅记日志 | **`net_listener_settle(fd, NET_LISTENER_SETTLE_QUARANTINE)`** + `fire_event` | TIMEOUT = 异常释放(如客户端中止上传):对端在途数据随后落进 RX 缓冲 → 必须隔离(400ms)后才许复监听;正常 FIN 释放走 disconnect 路径(GRACEFUL 100ms),**不走**隔离 | | `fire_event` | 调用已注册回调 | `p_sock->event_cb` | **回调上下文 = netTask**:回调内不得阻塞、不得发网络操作(会自等死锁) | ### 4.10 工具函数 | 函数 | 设计作用 | |---|---| | `alloc_socket` | 线性扫描 `s_net_socks`,返回首个 `in_use=0` 的索引(-1 = 8 个全占) | | `parse_ip_string` | 点分十进制 → 4 字节数组(网络序,供 `ch395f_set_*_addr`) | | `s_mac_is_zero` | MAC 全零判定(NULL/全零 = 用芯片内置 MAC) | | `net_bench_cmd_log` | 台架命令序列日志(`NET_BENCH_CMD_LOG` 编译开关,默认关;每个真实 SPI 命令点一行,排障用) | --- ## 5. 典型端到端路径 ### 5.1 发送(队列路径) ```mermaid sequenceDiagram participant A as appTask(如 ftpTask) participant Q as netMsgQueue participant N as netTask participant C as CH395F A->>Q: net_send:入队(fd/buf 指针/len,按值拷贝) Note over A: net_wait_result:零等待拾取 → 无则阻塞 Q->>N: NET_MSG_SEND N->>C: net_send_locked:write_send_buf(≤1KB/块) alt 健康路径(块间等待 < NET_SEND_SPIN_MS=2ms) loop 每块 N->>C: 等 SENBUF_FREE(net_poll 有界自旋,不睡) end else 超预算(对端零窗口 / 不读) N->>N: net_send_pending_register(每 fd 至多 1;do_notify=0) Note over A,N: netTask 立即解放;A 在自己上下文睡通知 loop 每个主循环节拍(1ms) N->>C: net_send_pending_tick:①断连 ②推进 ③写完 ④无进展超时 end end C-->>N: SENBUF_FREE / SEND_OK(net_poll 置 send_ready) N->>A: xTaskNotify(实际字节数 / 负错误码) A->>A: 返回字节数 / -1(errno 已恢复;短写按 ret < len 处理) ``` ### 5.2 阻塞接收无数据(挂起 recv 路径) ```mermaid sequenceDiagram participant A as appTask participant Q as netMsgQueue participant N as netTask participant C as CH395F A->>Q: net_recv_to:入队(fd/buf/len/单片预算) Q->>N: NET_MSG_RECV N->>C: net_poll + get_recv_len C-->>N: 无数据 N->>N: net_pending_register(每 fd 至多 1;do_notify=0) Note over A,N: netTask 立即解放;A 在自己上下文睡任务通知 loop 每个主循环节拍(1ms) N->>C: net_pending_tick:①数据 ②到点 ③断连 end C-->>N: 数据到达 / 到点 / 断连 N->>C: read_recv_buf(断连时先排空残留) N->>A: xTaskNotify(字节数 / 0 / 负错误码) A->>A: >0 数据 · 0 EOF(自动重监听)· -1 真错误 · -2 本片超时 Note over N: close / 网络重建 / 陈旧 resync → 显式 settle/cancel(必通知) ``` ### 5.3 断连 → 自动重监听(含 TRAP-NET-18 沉降) ```mermaid sequenceDiagram participant N as netTask participant C as CH395F participant A as appTask Note over C: 客户端 FIN(正常)或 RST/无响应(异常) N->>C: net_poll 读 SINT alt DISCONNECT(正常) N->>N: handle_disconnect_event:回读甄别 → CLOSED + 回调
+ net_listener_settle(GRACEFUL 100ms) else TIMEOUT(异常,如中止上传) N->>N: handle_timeout_event:net_listener_settle(QUARANTINE 400ms) + CLOSED end Note over N: 只置事实 —— 重建统一由 net_reconcile_tick 形态③④(100ms)决定 N->>C: net_listener_request(登记)→ net_listener_tick → net_relisten_locked opt 沉降期内(TRAP-NET-18) N->>C: 排空 RX 残渣(256B 分块)+ close + 排空中断 C-->>N: 未到 CLOSED/TCP_CLOSED → 返回(30ms 后重试轮询复检) end N->>C: set_proto_type + set_sour_port + open_socket + tcp_listen alt open 失败(TRAP-NET-09 沉降) N->>N: 登记重试(30ms 节奏 / 2s 时限)→ net_listener_tick 到点重试 else 成功 N->>N: state = LISTENING(auto_relisten 保留) N-->>A: xTaskNotify(accept 返回 / recv 返回 0) end Note over N: 2s 超限时 → 清状态 + "relisten give up"(错误日志,应用可感知) ``` ### 5.4 事件未收到自愈(TRAP-NET-17 对账 + 监听重建合一 reconciler) ```mermaid sequenceDiagram participant N as netTask participant C as CH395F participant A as appTask Note over C: 快速复连:芯片完成握手但 CONNECT 事件未收到 Note over N: 本层滞留 LISTENING → 应用以为无客户端(永不调 accept) loop 每 100ms(net_poll 每轮刷新驱动状态缓存) N->>N: net_reconcile_tick:读 ch395f_sock_state_cached(每个在用 socket) C-->>N: 失配(本层 LISTENING,芯片 ESTABLISHED) end Note over N: 形态② 去抖=1 轮(幽灵甄别挡假阳性;仅命中时回读对端端口,本 tick 唯一按需 SPI) N->>N: handle_connect_event:补 accept 簿记 → ESTABLISHED N-->>A: 下一次 net_accept 返回客户端(连接不再"吞掉") Note over N: 其余形态:① 本层 ESTABLISHED 而芯片已断 → handle_disconnect_event(去抖 4 轮)
③④ 本层 LISTENING 丢监听 / CLOSED 孤儿 → 本 tick 形态③④ 重建(唯一重建入口,去抖 2 轮)
⑤ 静默回收 → RESYNC 只修本层;U11 芯片在听 → 只重同步 ``` ### 5.5 网络重建(IP 变更免重启) ```mermaid sequenceDiagram participant N as netTask participant C as CH395F participant A as 各应用任务 N->>A: net_init 开头:net_pending_cancel_all(CONNRESET)——逐一取消并通知 N->>N: 控制块全部清零 N->>C: check_exist → reset → fun_para(0x08)/MSS/重传/KeepAlive N->>C: IP·GW·掩码(或 DHCP)→ ch395f_init → MAC → 8 Socket 缓冲布局 Note over N,A: net_stack_bring_up:PHY/链路就绪 → net_generation_inc
FTP/UI/对时据**代次变化**放弃旧监听并重建(见 docs/子系统设计/网络配置说明.md §4) ``` --- ## 6. 设计铁律(为什么是这样) | # | 铁律 | 实证来源 | |---|---|---| | 1 | **一切芯片访问收敛 netTask**(唯一执行者);跨任务调用走队列 | TRAP-NET-14:跨任务 SPI 竞争破坏芯片寄存器 → 连接必超时 | | 2 | **结果只经任务通知值回传**;消息按值入队,调用方看不到 netTask 对消息的写入;勿给消息加"回传用"字段 | TRAP-NET-10:6 个 API 永远返回 -1;"加 msg.err 字段"的修法曾致 `ui_net_sink` 每次空轮询误判真错误 → 连上即断 | | 3 | **netTask 不为任何人自旋等待**:阻塞 recv = 挂起登记;重开失败 = 重试登记(D10);初次 listen/connect 的 open 重试环(`net_sock_reset_sync` 有界等待,GRACEFUL 100ms 底 / 600ms 封顶)是唯一豁免(启动期无竞争,原 3×500ms 盲等已换确定性排渣) | 队头阻塞整改:一次空闲 30s recv 冻住全网 17.7s;旧重监听 osDelay(500) → 恢复 588ms、期间重连被拒 | | 4 | **netTask 不向自身发消息 / 不自我挂起**(挂起登记对执行任务自身发起直接拒绝) | 自等自通知 = 死锁 | | 5 | **基于状态读的重建动作必须去抖 + 冷却**:分级门槛 ②=1 轮 / ③④⑤=2 轮 / ①=4 轮(100ms/轮)+ 冷却 1s | 2026-09-23:无去抖 → 监听反复重建直至 `relisten give up`,三服务口全死 | | 6 | **异常释放(TIMEOUT)后复监听前必须隔离**:排空 RX 残渣 + 等 CLOSED/TCP_CLOSED;隔离上限 < 重试放弃时限 | TRAP-NET-18:~2KB 残渣被下条连接当自己的数据 → 首笔上传截断 2048B 并误报 226 成功;上限=2s 时隔离耗尽重试预算 → 监听被弃 | | 7 | **RECV 电平触发中断每轮只处理一批** | TRAP-NET-08:连续循环 → 主循环饿死 | | 8 | **close 前先结束挂起 recv + 排空残留 DISCONNECT** | TRAP-NET-12:锁存的陈旧 DISCONNECT 秒断下一连接 | | 9 | **两段式等待防丢失通知**:零等待拾取 + 阻塞;清除掩码全 1;以 pdTRUE 判定 | run3~11:丢失通知 → 挂死 / 结果错位;0 结果误判"无通知" → 永久挂死 | | 10 | **网络重建先取消并通知全部挂起,再复位芯片** | 取消不通知 = 调用方永久挂死 | | 11 | **监听重建只有一个所有者**(`net_reconcile_tick` 形态③④;原 `net_listener_pump` 已删);其它位置**只置事实**(置 CLOSED / 登记沉降),不做重建动作 | 第三方送检验收:多所有者导致"谁先看到 DISCONNECT 就谁重建"的竞争与重建风暴 | --- ## 7. ch395f 驱动接口使用图(`net_socket.c` / `net_listener.c` → 芯片命令) | 芯片命令 | 使用处 | 说明 | |---|---|---| | `ch395f_check_exist` / `ch395f_reset` | `net_init` | 上电探测与复位 | | `ch395f_set_fun_para(0x08)` | `net_init` | bit3 手动关缓冲(SOCKET_CLOSE);bit0/1=0,Socket 个数与 FUN_PARA 无关 | | `ch395f_set_tcp_mss` / `set_retrans_count` / `set_retrans_period` / `set_keepalive_idle+intvl+cnt` | `net_init`(**均须在 `ch395f_init` 之前**) | 值见 `net_config.h` | | `ch395f_set_mac_addr` | `net_init`(**须在 `ch395f_init` 之前**) | 写芯片 EEPROM;INIT_CH395 加载 MAC(`docs/芯片手册/CH395F.md` §9.2.1 步骤①) | | `ch395f_set_ip_addr` / `set_gwip_addr` / `set_mask_addr` / `set_dhcp` | `net_init`(**均须在 `ch395f_init` 之前**) | 固定 IP 或 DHCP | | `ch395f_init` / `ch395f_ping_enable` | `net_init` | 协议栈初始化(INIT_CH395)/ PING(须在 init 之后) | | `ch395f_set_send_buf` / `ch395f_set_recv_buf` | `net_init`(8 Socket 布局);`net_bind`(UDP)(Socket 4~7 兜底) | 每 Socket 收 4 块 + 发 2 块 | | `ch395f_get_glob_int_status_all` | **仅 `net_poll`**(唯一读者红线:全局寄存器唯一读者,见 `docs/网络/网络自愈架构设计.md` §4.10;`net_listen_locked` / `net_connect_locked` / `net_close_locked` / `net_relisten_locked` 的原全局读清已全部删除) | **读 = 清**(中断清除机制,无按位手段)——其他位置读清会焊死 net_poll 的按 Socket 门控 | | `ch395f_get_sock_int_status` | `net_poll`(逐 Socket SINT);各 open 路径后清残留;事件处理器清 TIMEOUT | **读 = 清** | | `ch395f_sock_events` | `net_connect_locked`(open 后清残留,唯一读者红线的护栏原语,见 `docs/网络/网络自愈架构设计.md` §4.10);`net_close_locked`(芯片已 CLOSED 快路径只清本 Socket 残留事件) | 只碰**本 Socket** SINT 的护栏原语 | | `ch395f_open_socket` / `ch395f_close_socket` | `net_bind`(UDP)、`net_listen_locked`、`net_connect_locked`、`net_relisten_locked`(含沉降期主动 close);`net_close_locked` 的 CLOSE 已改经 `ch395f_cmd_done` / `ch395f_sock_reset_*` | TRAP-NET-09:close 后需沉降才能重开 | | `ch395f_tcp_listen` / `ch395f_tcp_connect` | `net_listen_locked` / `net_relisten_locked`;`net_connect_locked` | — | | `ch395f_set_proto_type` / `set_sour_port` / `set_des_ip` / `set_des_port` | `net_bind`(UDP)、`net_listen_locked`、`net_connect_locked`、`net_relisten_locked`、`net_sendto` | open 之后写端口/IP(open 会复位寄存器);set_des_ip 期望**大端数组** | | `ch395f_write_send_buf` | `net_send_locked` / `net_send_pending_tick`(挂起 send 推进)/ `net_sendto` | 单次 ≤1KB(硬件 FIFO 上限,TRAP-NET-16) | | `ch395f_get_recv_len` / `ch395f_read_recv_buf` | `net_recv_locked`、`net_pending_tick`、`net_pending_settle_close`、`net_recvfrom`、`net_drain_chip_rx`、`handle_disconnect_event`(M3 可疑门 RX 非空判据) | `handle_recv_event` **故意不读**(防竞争,旧机理未证实) | | `ch395f_get_socket_status` | `handle_disconnect_event`(两处甄别——事件路径**主动 SPI**,缓存必然滞后于刚到达的中断)、`net_relisten_locked`(沉降门)、`net_connect_locked`(诊断回读)。⚠ **周期对账不再直接调**——改读缓存(下行) | 输出 **2 字节**([0]=Socket 状态 [1]=TCP 状态),缓冲必须给 2 字节 | | `ch395f_get_sock_state` / `ch395f_sock_state_cached` | 刷新**仅 `net_poll`** 每轮对在用 Socket 调 `get_sock_state`(唯一刷新点);`sock_state_cached` 读缓存 = `net_reconcile_tick`(形态①②③④⑤/U11 判据) | 芯片状态唯一事实源;E2 实测单次回读 18.1µs,缓存新鲜度 ≤1ms | | `ch395f_sock_reset_begin` / `ch395f_sock_reset_poll` | `net_sock_reset_sync`(`net_listen_locked` / `net_connect_locked` open 重试、`net_close_locked`、`net_drop_conn_locked`);`net_poll` 每轮对在用 Socket **步进**(IDLE 槽纯 RAM 判零 SPI) | 驱动复位状态机:读空 RX → CLOSE → 排空事件(quiescent) → 等 byte0=00H;内置 LISTEN 态二段 CLOSE 补发(E2 补充发现);GRACEFUL 100ms 底 / QUARANTINE 400ms 底 | | `ch395f_cmd_done` | `net_close_locked`(CLOSED 快路径判据) | 命令完成判定(OPEN/LISTEN/CLOSE 三形态,读缓存不额外发 SPI) | | `ch395f_link_up_cached` / `ch395f_link_change_count` | `net_reconcile_tick`(链路态订阅留痕,见 `docs/网络/网络自愈架构设计.md` §4.4 链路态喂料缓存) | 驱动 link 变化计数,net_poll 喂 | | `ch395f_get_remot_ipp` | `handle_connect_event`、`net_reconcile_tick`(幽灵甄别,形态② 命中时唯一按需 SPI)、`net_connect_locked`(诊断) | **小端**输出(与 set_des_ip 相反) | | `ch395f_set_keepalive_enable` | `handle_connect_event`(client 连接成功时) | — | --- ## 8. 改动意图速查 | 想改什么 | 先看哪些函数 | |---|---| | 监听恢复策略(节奏/时限/沉降) | `net_listener_request` / `net_relisten_locked` / `net_listener_tick`;常量 `NET_RELISTEN_*_MS`(`net_listener.c` 内);重建决策在 `net_reconcile_tick` 形态③④(下行) | | 阻塞接收等待语义 | `net_pending_register` / `net_pending_tick` / `net_pending_complete`;常量 `NET_PENDING_WATCHDOG_MS` | | 阻塞发送等待语义(挂起 send) | `net_send_pending_register` / `net_send_pending_tick` / `net_send_pending_complete`;常量 `NET_SEND_SPIN_MS`(有界自旋预算)/ `NET_SEND_TIMEOUT_MS`(无进展超时)/ `NET_SEND_CHUNK_MAX`(分块上限,`net_pending.h` 唯一定义处) | | 事件未收到自愈 + 监听重建策略(合一 tick) | `net_reconcile_tick`(`NET_RECONCILE_PERIOD_MS` / `NET_RECONCILE_COOLDOWN_MS` / `NET_RECON_STABLE_ACCEPT|RELISTEN|DEATH`,①②③④⑤+U11,唯一重建入口=形态③④)+ `handle_disconnect_event` / `handle_connect_event` | | 新增一个公共 API | 模式:`net_socket.h` 声明(`NET_NO_OS` 条件编译)→ `net_msg_type_t` 加类型(如需要)→ `net_process_messages` 加 dispatch 分支 → netTask 侧 `_locked` 实现;调用侧复制"参数检查 → 入队 → `net_wait_result` → errno 恢复"骨架 | | 缓冲布局 / MSS / 重传 / KeepAlive 参数 | `net_init` + `net_config.h` + `ch395f.h`(`CH395F_RECV/SEND_BLOCKS`、`CH395F_TCP_MSS`);改完必须 0 错 0 警构建 + 全测试套件回归 | | 队列容量 / 消息结构 | CubeMX 生成的 `MX_FREERTOS_Init`(8 槽)+ `net_socket.h` 的 `net_msg_t`(**勿加回传字段**) | | 超时参数(收发/重试/对账) | `net_config.h`(收发/重传/KeepAlive);`net_listener.c` 内 `NET_RELISTEN_*`;`net_socket.c` 内 `NET_RECONCILE_*` / `NET_RECON_STABLE_*` / `NET_PENDING_WATCHDOG_MS` | --- ## 9. 相关文档 - `docs/网络/BSD_Socket_API_使用指南.md` —— 公开 API 语义、示例、常见问题(本文的上半部) - `docs/网络/网络自愈架构设计.md` —— **监听生命周期权威设计**(边沿→电平、单一所有者重建 = `net_reconcile_tick` 形态③④、`net_listener.c` / `net_events.c` 模块化边界 §11) - `docs/问题记录/CH395F_Trap_Records.md` —— TRAP-NET-01~23 机制细节(本文引用处) - `docs/网络/Net层阻塞模型架构设计.md` —— 阻塞等待模型的现状架构设计(pending 请求模型 / 五路完成语义) - `docs/测试说明/专项验证/链路延迟测试说明.md` —— 芯片 / 队列 / 挂起 recv 等分段时延基线与自愈路径的台架证据 - `docs/子系统设计/网络配置说明.md` §4 —— 代次号与网络重建时序 - `docs/测试说明/板级测试/CH395F_Test_Guide.md` —— 单连接模型测试规则(TC-NET-6xx)