# 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 请求表**)+
> `Drivers/BSP/NET/net_udp.c`(**UDP 数据面执行体与挂起 recv 槽**,2026-11-03 队列化改造新增,§4.11)。
> 模拟器端后端 `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` / `net_udp.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)的地方;5 个非阻塞 tick(D10 重试 / 挂起 recv / 挂起 send / **UDP 挂起 recv**(2026-11-03 队列化) / TRAP-NET-17 对账+监听重建合一 reconciler);队列分派;事件处理器;用户回调 | `net_poll` / `net_process_messages` 及其下游全部 `_locked` / `_tick` / `handle_*` |
| **应用任务侧** | ftpTask / uiTask / timeSyncTask / testTask 等 | 经队列的公共 API(入队 + `xTaskNotifyWait` 等回执);纯 RAM 直调 API;**禁止直碰芯片** | 各服务任务 |
| **直调快路径** | 同步调用,不经过队列 | `net_socket` / 查询族 / 回调注册 / `net_bind`(TCP) 记账(纯 RAM);**UDP 侧不再是例外**:`net_bind`(UDP) / `net_sendto` / `net_recvfrom` / `net_recvfrom_to` / `net_udp_set_peer` 已于 2026-11-03 全部并入队列 | 初始化阶段、netTask 上下文、测试套件(硬件层测试模式下 netTask 自挂起,testTask 独占芯片) |
> ⚠ 2026-11-03 起,**公开 API 里已经没有直碰 SPI 的口子**——原"三例外"(`net_bind`(UDP) / `net_sendto` / `net_recvfrom`)已全部并入队列,产品构建中应用任务的每一条 `net_*` 都是"入队 + 等回执"。仍需独占芯片的只剩**板级硬件层测试**:它绕过本层直接调 `ch395f_*`,前提是该测试模式下 netTask 自挂起、testTask 独占芯片(见 AGENTS.md"测试构建")。任何情况下从别的任务直接驱动 CH395F 都会与 `net_poll` 的 SPI 事务竞争、破坏芯片寄存器(TRAP-NET-14 实证:CONNECT 超时)。
---
## 3. 总体调用关系图
```mermaid
flowchart TB
subgraph APP["应用任务(非 netTask)"]
PUB["经队列公共 API
netListen · netAccept · netConnect · netSend
netRecv · netRecvTo · netClose · netDropConn
UDP:netBind · netSendto · netRecvFrom · netRecvFromTo · netUdpSetPeer"]
RAM["直调 · 纯 RAM
netSocket · netSockState · netGetSock
netGetErrno · netSetEventCb · netGetSockname
字节序(netHtons / netInetAddr …)"]
SPI["板级硬件层测试 · 直调 BSP(须芯片访问者唯一)
ch395f_*(该模式下 netTask 自挂起)"]
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)"]
UdpTick["④ netUdpTick
UDP 挂起 recv:①数据到达(读一个完整数据报) ②预算到点 -2 ③会话已死"]
DISP["队列分派 switch
SEND · RECV · CLOSE
CONNECT · LISTEN · ACCEPT · DROP_CONN
UDP_BIND · UDP_SENDTO · UDP_RECVFROM · UDP_PEER"]
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 排渣隔离"]
UB["netUdpBindExec
开芯片侧 Socket(恒服务器模式,DesIP=FFH 在 open 前写)"]
US["netUdpSendtoExec
带地址则先写 DesIP/DesPort(转客户端模式)"]
UR["netUdpRecvfromExec
快路径 / DONTWAIT / 登记挂起 recv"]
UP["netUdpSetPeerExec
只改 DesIP/DesPort,不重开 Socket"]
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)"]
SU["s_net_udp
(每 fd 至多一个接收槽:调用方出参指针 + 挂起 recv)"]
CH[("ch395f.* 驱动
SPI2 串行事务")]
end
PUB -->|入队| MQ
MQ --> DISP
DISP --> LL & LN & LS & LR & LA & LC & DC & RLS & UB & US & UR & UP
NPM --> RetryTick & PendTick & PendShimTick & UdpTick & ReconcileTick & NP
US -->|共用同一条分块与挂起 send| LS
UR -->|登记 / 结算| SU
UdpTick -->|读出一个完整数据报| SU
UdpTick -->|xTaskNotify 完成或超时| XN
LC -->|结束 UDP 挂起 recv(settle_close)| SU
UB & US & UR & UP & UdpTick --> CH
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 -->|仅板级硬件层测试:netTask 自挂起时独占芯片| CH
CH -->|INT# 低电平| NP
```
**读图要点**:
1. 应用侧只有两种"等":① 入队后 `xTaskNotifyWait` 等回执(同步语义);② 挂起 recv / send 的等待(同一通道,只是"完成"由 `netPendingTick` / `netSendPendingTick` / `netUdpTick` 判定,不由 netTask 立即处理)。
2. `net_process_messages()` 开头的 **5 个 tick 是网络层自愈与等待结算主干**:没有任何应用消息时,重试 / 挂起 recv 完成 / 挂起 send 推进 / UDP 挂起 recv 完成与超时 / 状态对账 + 监听重建照常运转——"断连后即使没人发消息,监听器也能自己活过来"靠的就是它们。
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 宿主:每轮**先**跑 5 个非阻塞 tick,再排空队列逐条分派,结果 `xTaskNotify` 回传 | `net_listener_tick` → `net_pending_tick` → `net_send_pending_tick` → `net_udp_tick`(UDP 挂起 recv 完成/超时)→ `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 拒绝);去抖/冷却沿用对账框架 |
| `net_bind`(**UDP 支**) | `NET_MSG_UDP_BIND`(端口按 `ntohs` 后的值传,0 = 由执行体取动态号) | `net_udp_bind_exec` | Socket 4~7 兜底 `set_send_buf` / `set_recv_buf` + `set_proto_type(UDP)` + `set_sour_port` + `set_des_ip(0xFFFFFFFF)` + `set_des_port(0)` + `open_socket` | 0 / -1 | **bind 恒服务器模式**(BSD 的 bind 只定本地端口,不选对端);DesIP/端口写在 open **之前**(open 复位寄存器,写晚了等于没写);动态端口游标在 netTask 内推进(旧形态在调用方上下文改静态量 = 跨任务竞争) |
| `net_sendto` | `NET_MSG_UDP_SENDTO`(目的地址按值拷进 `msg.addr`,`addrlen > 0` 才改 DesIP) | `net_udp_sendto_exec` → **共用** `net_send_locked` | `set_des_ip` / `set_des_port`(带地址时)+ `write_send_buf` 分块 ≤ 1KB | 实际字节数(**可能短写**)/ -1 | 带目的地址 = 切**客户端模式**并记账 `remote_*`;发送与 TCP 走同一条分块 + 挂起 send(`net_send_pending`)通路,不为 UDP 复制第二份 1KB 分块逻辑;未 bind → NOTCONN(芯片侧还没 open) |
| `net_recvfrom` / `net_recvfrom_to` | `NET_MSG_UDP_RECVFROM`(**出参指针不入队**:`net_udp_outparam_arm` 存进本 fd 槽,netTask 直接写回调用方内存) | `net_udp_recvfrom_exec`(即时读 / 登记挂起)+ `net_udp_tick`(完成 / 超时结算) | `net_poll` + `get_recv_len` + `read_recv_buf`(剥头 + 截断排空) | >0 / -1 / **-2(`_to` 预算内无数据,不占 errno)** | 一次调用 = 一个完整数据报;**按 `udp_mode` 决定剥不剥 8 字节信息头**(服务器剥、客户端不剥——旧形态不分模式恒剥,客户端模式等于吃掉载荷前 8 字节并回填假源地址);无数据 + 阻塞 → 登记挂起立即让出 netTask;缓冲装不下时剩余部分**读空丢弃**;同 fd 第二个接收者 → BUSY |
| `net_udp_set_peer` | `NET_MSG_UDP_PEER`(`addrlen == 0` = 回服务器模式) | `net_udp_set_peer_exec` | `set_des_ip` + `set_des_port`(**不重开 Socket**) | 0 / -1 | 模式切换的轻量入口,给"上一轮当客户端问、这一轮要听所有人"的收包方;要连芯片缓冲残留一起丢掉就再 `net_bind()` 一次(重开形态) |
### 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,**无芯片命令** | 写 `s_net_socks[fd].local_port` / `state` | TCP 的 bind 只是"记账",真正的芯片动作在 listen/connect;**UDP 支自 2026-11-03 起入队**(要发 5 条芯片命令,见 §4.3 `net_bind`(UDP 支)行);动态端口回绕 65535→49152 |
| `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 命令点一行,排障用) |
---
### 4.11 UDP 数据面(`net_udp.c`,2026-11-03 队列化改造)
> **模块归属**:`Drivers/BSP/NET/net_udp.c`(真机专属,模拟器不编译——模拟器端 UDP 由 `simulator/sim/net_socket_sim.c` 的 Winsock 形态承担)。
> **使用侧**契约(芯片两种模式、调用约束、超时口径)权威在 `docs/网络/BSD_Socket_API_使用指南.md` §6.0 / §6.3;
> 本节只讲调用结构与"为什么这么切"。
| 函数 | 上下文 | 设计作用 | 关键决策 |
|---|---|---|---|
| `net_udp_outparam_arm` / `net_udp_outparam_disarm` | 调用方任务(临界区只包"查占位"三步) | 登记"这次收包的源地址写回哪块调用方内存" | **源地址不搭 `net_msg_t`**:该结构按值拷入队列,AGENTS / TRAP-NET-10 明令禁止给它加"回传用"字段;出参指针进 per-fd 槽,netTask 直接写回调用方自己的内存,通知值只带结果码 |
| `net_udp_clear` | 两侧 | 清本 fd 槽(`net_socket` 分配新 fd、入队失败、执行体每条出口都要清) | 槽位泄漏 = 该 fd 从此永久 `BUSY`;出口必清(ns_s7_udp.c UDP-7d / UDP-9e 钉住) |
| `net_udp_bind_exec` | netTask | 服务器模式开芯片侧 Socket(5 条命令 + Socket 4~7 缓冲兜底) | DesIP/端口写在 open **之前**(手册 §9.2.4;open 复位寄存器);bind **恒服务器模式**——BSD 语义里 bind 不选对端;动态端口游标在 netTask 内推进(消掉跨任务写静态量) |
| `net_udp_set_peer_exec` | netTask | 只写 DesIP/DesPort 切形态;`peer == NULL` 回服务器模式 | 不重开 Socket,因此**不清**芯片缓冲;要连残留包一起丢就再 `net_bind()`(重开形态) |
| `net_udp_sendto_exec` | netTask | 记对端账 + **复用 `net_send_locked`** 发送 | 不为 UDP 复制第二份 1KB 分块 / 挂起 send 逻辑;"会话是否已死"由 `net_send_state_down()`(`net_pending.c`)按 `type` 分流——TCP 看 ESTABLISHED、UDP 看 UDP_OPEN,一条判据两侧共用(`net_send_locked` / `net_send_pending_tick` / `net_pending_tick` 三处原先各写一遍) |
| `net_udp_recvfrom_exec` | netTask | 三出口:数据已在 → 读一包;DONTWAIT / `blocking=0` → WOULDBLOCK;阻塞 → 登记挂起并立即让出 netTask | 自等守卫判 `caller == xTaskGetCurrentTaskHandle()`,**不是** `net_ctx_is_owner_task()`:执行体本身就跑在 netTask 里,用后者等于把每一条经队列投进来的正常请求都当自等拒掉(UDP-8a 当场抓到过这个错) |
| `net_udp_tick` | netTask 1ms | 挂起 recv 的完成 / 预算超时 / 会话已死三类结算 + `xTaskNotify` | 与挂起 recv、挂起 send 同一节奏;`readable` 只当"省一次 SPI"的门,电平型 RECV_OK 每轮重读可自愈(事件漏了也不永久瞎) |
| `net_udp_datagram_read` | netTask(private) | 一个完整数据报:按 `udp_mode` 决定剥不剥 8 字节信息头 → 截断排空 → 清 `readable` | `avail` 由调用方读一次**传进来**:剥头量、载荷量、排空量必须由同一个数字算出(读两次 = 决策与内容可能不同源);服务器模式不足 8 字节的残包不猜头,整包按载荷交付(TC-NETSOCK-440o 定案) |
| `net_udp_settle_close` / `net_udp_cancel_all` | netTask | close / 网络重建时结算等待者 | **任何结算都必须通知**:close 时有数据先交付、无数据回 CONNRESET;`net_init` 重建走 `net_udp_cancel_all`(UDP-8 / UDP-12 钉住"不许静默丢通知")。⚠ **没有** per-fd `net_udp_cancel`:唯一的 per-fd 取消点在 `net_events.c` 的 reconcile 形态⑤,而形态①⑤的判据门是 `state == ESTABLISHED`(TCP 专属,UDP 在 `net_events.c` 里被"其余状态不归本 tick 认领"挡掉),UDP 挂起收包的"会话已死"结算由 `net_udp_tick` 第①条按状态门当轮完成(UDP-19 钉住),不留没人调的 API |
改造前后的差别一句话:**改造前 UDP 在调用方上下文自旋碰 SPI**(既违反"netTask 唯一芯片访问者",也把
IWDG 预算吃在自旋上,且源地址无法回传);**改造后 UDP 与 TCP 同构**——入队、netTask 串行执行、挂起态
交 tick 结算、结果搭 32 位通知值回来。
## 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_udp_bind_exec`(Socket 4~7 兜底,2026-11-03 起在 netTask 内执行) | 每 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_udp_bind_exec`(UDP 开 Socket)、`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_udp_bind_exec`、`net_udp_set_peer_exec`、`net_udp_sendto_exec`(2026-11-03 起 UDP 三条都只在 netTask 内跑)、`net_listen_locked`、`net_connect_locked`、`net_relisten_locked` | **两种顺序都存在,别当成一条**:TCP 各路径在 open **之后**写(open 会复位寄存器,TRAP-NET-14);UDP bind 按手册 §9.2.4 在 open **之前**写完 5 条——两侧都有钉子(TCP 见 TC-NETSOCK-410 组,UDP 见 ns_s7_udp.c UDP-1f 的命令顺序断言);set_des_ip 期望**线序数组**(`arr[0]` = 点分第一段) |
| `ch395f_write_send_buf` | `net_send_locked`(TCP 与 **UDP sendto 共用**,2026-11-03)/ `net_send_pending_tick`(挂起 send 推进,同样两侧共用) | 单次 ≤1KB(硬件 FIFO 上限,TRAP-NET-16) |
| `ch395f_get_recv_len` / `ch395f_read_recv_buf` | `net_recv_locked`、`net_pending_tick`、`net_pending_settle_close`、`net_udp_recvfrom_exec`、`net_udp_tick`、`net_udp_settle_close`(UDP 剥头与截断排空都在这三处)、`net_drain_chip_rx`、`handle_disconnect_event`(M3 可疑门 RX 非空判据) | `handle_recv_event` **故意不读**(防竞争,旧机理未证实);一次收包**只读一次长度**——剥头量/载荷量/排空量必须由同一个数字算出(`net_udp_datagram_read` 的 `avail` 参数) |
| `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)