diff --git a/docs/BSD_Socket_API_使用指南.md b/docs/BSD_Socket_API_使用指南.md index 6cb0b211..53f062f3 100644 --- a/docs/BSD_Socket_API_使用指南.md +++ b/docs/BSD_Socket_API_使用指南.md @@ -1,6 +1,8 @@ # BSD Socket API 使用指南 > **恢复说明**:本文档曾在提交 `4e9ac0d`(2026-08-28)中误删,2026-09-13 从 git 历史恢复。API 随代码多轮同步(2026-09-13:`net_init` 4 参数 / `net_socket` 2 参数 / 网络代次号 / `NET_NO_OS` 双端构建;2026-09-23:`net_recv_to` / `net_sock_state` / `net_resync_listen` / `net_relisten`、L3 pending 化、D10 非阻塞重监听、Trap 22 周期对账、Trap 23 排渣隔离)。详见文末变更记录。 +> +> **伴读文档**:本文回答"上层该怎么调";**内部函数之间的调用关系、每个函数的设计作用与意图**(netTask / 消息队列 / L3 挂起 / D10 重试 / Trap 22 对账 / Trap 23 排渣各子系统,含总体调用图与端到端时序)见 `docs/BSD_Socket_API_函数调用关系.md`。 ## 目录 diff --git a/docs/BSD_Socket_API_函数调用关系.md b/docs/BSD_Socket_API_函数调用关系.md new file mode 100644 index 00000000..701bb9fe --- /dev/null +++ b/docs/BSD_Socket_API_函数调用关系.md @@ -0,0 +1,390 @@ +# BSD Socket API 函数调用关系与设计说明 + +> **定位**:本文档是 `docs/BSD_Socket_API_使用指南.md`(下称"使用指南")的**内部视图**伴读文档。 +> 使用指南回答"**上层该怎么调**"(公开语义 / 示例 / 常见问题);本文回答"**内部函数各自做什么、互相怎么调用、为什么这么设计**"(调用关系 / 设计意图 / 踩坑)。 +> +> **范围**:MCU 端 `Drivers/BSP/NET/net_socket.c`(约 2870 行,全部函数)。 +> 模拟器端后端 `simulator/sim/net_socket_sim.c`(Winsock)是另一套实现,见使用指南 §2.3;`net_select.c` 不在此展开,见使用指南 §7 / §10.5。 +> +> **交叉引用约定**:机制标 Trap 号 → `docs/问题记录/CH395F_Trap_Records.md`;用法语义引用使用指南 §;关键参数值与 `net_config.h` / `net_socket.c` 内宏一致。 + +--- + +## 1. 一句话架构 + +**一切芯片(CH395F)操作收敛到唯一执行者 netTask 串行完成**;其他任务通过"入队消息 + 任务通知"表达请求,结果只经**任务通知值**回传;阻塞等待**不独占 netTask**(挂起登记 + 主循环节拍完成);监听器**常驻**(`auto_relisten` + D10 非阻塞重试 + Trap 22 周期对账 + Trap 23 排渣隔离,四层自愈)。 + +--- + +## 2. 三个执行域 + +| 执行域 | 运行位置 / 节奏 | 允许的操作 | 典型调用者 | +|---|---|---|---| +| **netTask 主循环** | 独立 FreeRTOS 任务,1ms/tick | **唯一**可碰芯片(SPI2)的地方;3 个非阻塞 tick;队列分派;事件处理器;用户回调 | `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 16 实证:CONNECT 超时)。 + +--- + +## 3. 总体调用关系图 + +```mermaid +flowchart TB + subgraph APP["应用任务(非 netTask)"] + PUB["经队列公共 API
netListen · netAccept · netConnect · netSend
netRecv · netRecvTo · netClose
netRelisten · netResyncListen"] + 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
先跑 3 个 tick,再排空队列分派"] + NP["netPoll
INT# GPIO 轮询 → 全局中断 → 各 Socket SINT → 每轮一批"] + T1["① netRelistenRetryTick
D10 重监听重试:30ms / 2s 时限"] + T2["② netPendingTick
L3 挂起 recv:①数据 ②到点 ③断连 ④看门狗 1s"] + T3["③ netReconcileTick
Trap 22 对账:200ms / 去抖 2 次 / 冷却 1s"] + DISP["队列分派 switch
SEND · RECV · CLOSE · CONNECT · LISTEN
ACCEPT · RESYNC_LISTEN · RELISTEN"] + subgraph LK["_locked / 内部分派目标(仅 netTask 上下文)"] + LL["netListenLocked
初次监听(3×500ms 阻塞重试)"] + LN["netConnectLocked
3×500ms 重试 + tcpConnect"] + LS["netSendLocked
分块 ≤1KB + sendReady 门控"] + LR["netRecvLocked
快路径 / DONTWAIT / 登记挂起"] + LA["netAcceptLocked
先 netPoll + 状态判定"] + LC["netCloseLocked
了结挂起 + close + 等 CLOSED + 排空中断"] + RLS["netRelistenTry → netRelistenLocked
单次尝试 + Trap 23 排渣隔离"] + RSY["RESYNC 分支:改状态 + 取消挂起
(不发芯片命令)"] + end + EV["事件处理器
handleConnect · handleDisconnect
handleRecv · handleSend · handleTimeout"] + FE["fireEvent → 用户回调"] + PR["s_net_pending
(每 fd 至多一个挂起)"] + CH[("ch395f.* 驱动
SPI2 串行事务")] + end + + PUB -->|入队| MQ + MQ --> DISP + DISP --> LL & LN & LS & LR & LA & LC & RLS & RSY + NPM --> T1 & T2 & T3 & NP + T1 --> RLS + T3 -->|"① 已断"| EV + T3 -->|"② 补 accept"| EV + T3 -->|"③ 重建监听"| RLS + NP --> EV + EV --> FE + FE -.-> CB + EV -->|"LISTENING:监听丢失"| RLS + EV -->|"TIMEOUT:置隔离"| RLS + LR -->|无数据+阻塞| PR + PR --> T2 + T2 -->|xTaskNotify| XN + XN -->|唤醒| PUB + NP --> CH + LL & LN & LS & LR & LA & LC & RLS --> CH + SPI -->|直碰 SPI| CH + CH -->|INT# 低电平| NP +``` + +**读图要点**: + +1. 应用侧只有两种"等":① 入队后 `xTaskNotifyWait` 等回执(同步语义);② 挂起 recv 的等待(同一通道,只是"完成"由 `netPendingTick` 判定,不由 netTask 立即处理)。 +2. `net_process_messages()` 开头的 **3 个 tick 是网络层自愈主干**:没有任何应用消息时,监听恢复 / 挂起完成 / 状态对账照常运转——"断连后即使没人发消息,监听器也能自己活过来"靠的就是它们。 +3. 事件处理器**只由 `net_poll` 驱动**,例外是 `net_reconcile_tick`(带 2 次去抖 + 1s 冷却)可再入 `handle_disconnect_event` / `handle_connect_event` / `net_relisten_try`——对账是丢中断的唯一自愈通道(Trap 22)。 +4. 回调 `event_cb` 在 **netTask 上下文**执行:回调内不得阻塞、不得发网络操作。 + +--- + +## 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(0x09)` / `set_tcp_mss` / `set_retrans_count+period` / `set_keepalive_idle+intvl+cnt` → `parse_ip_string` + `set_ip_addr` / `set_gwip_addr` / `set_mask_addr`(或 `set_dhcp`)→ `ch395f_init` → `set_mac_addr` → `set_send_buf` / `set_recv_buf` ×8 → `ping_enable` | ① **重建前**先取消全部挂起 recv 并逐一通知(不通知 = 调用方永久挂死),且必须早于控制块清零与芯片复位;② 重传/KeepAlive 参数**必须**在 `ch395f_init` 之前下发;③ `fun_para=0x09`:多 Socket 模式 + TCP Server 单连接 + DISCONNECT/TIMEOUT 不清缓冲(软件排空后手动关);④ 缓冲布局每 Socket 6 块(收 4 + 发 2)×8 = 48 块恰好用尽,Socket 0~7 必须显式重分配(Trap 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 类事件 | `HAL_GPIO_ReadPin` → `ch395f_get_glob_int_status_all` → `ch395f_get_sock_int_status` ×n → `handle_connect/disconnect/recv/send/timeout_event` | **RECV 中断是电平触发**:每次调用**只处理一批**(do-while(0)),处理完立即返回,否则主循环(echo/应用逻辑)饿死(Trap 08);SENBUF_FREE 只置 `send_ready` 不记事件 | +| `net_process_messages()` | 队列入口 + 自愈 tick 宿主:每轮**先**跑 3 个非阻塞 tick,再排空队列逐条分派,结果 `xTaskNotify` 回传 | `net_relisten_retry_tick` → `net_pending_tick` → `net_reconcile_tick` → `osMessageQueueGet`(0 超时) ×n → switch 分派 → `xTaskNotify` | ① tick 放在队列**之前**:断连后即使没有任何应用消息,监听器也能自主恢复、挂起 recv 也能完成;② RECV 若返回 `NET_PENDING_REGISTERED`(哨兵 0x70000000)则**不发即时回执**(`do_notify=0`),完成时才通知;③ 失败结果把 `s_net_errno`(具体负错误码)压入通知值——调用方恢复 errno 后对外统一 -1(BSD 语义);④ 通知值**整个 32 位**都是结果(不压位域标签,否则 32768 这类大字节数会溢出为负) | + +### 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`(**3×500ms 阻塞重试**)+ `tcp_listen` + 清全部中断状态 | 0 / -1 | **初次**监听允许阻塞重试(启动期无竞争);成功后置 `auto_relisten=1`——监听常驻的源头;启动前清空各 Socket/全局中断状态,防历史残留误触发事件 | +| `net_accept` | `NET_MSG_ACCEPT` | `net_accept_locked` | 无(先 `net_poll` 泵一次 + 状态判定) | 监听 fd 自身(单连接)/ -1(WOULDBLOCK) | 单连接:监听 Socket 在 CONNECT 后**自身即数据通道**,返回的就是它;CLOSED + auto_relisten 时触发**非阻塞** `net_relisten_try`;**accept 路径不做自愈**——单次状态读会误判"刚 listen 未生效"致重建风暴(2026-09-23 实测),统一交给对账 | +| `net_connect` | `NET_MSG_CONNECT` | `net_connect_locked` | `set_proto_type` + `open_socket`(3×500ms 重试)+ 清残留中断 + `set_sour_port` / `set_des_ip`(大端数组)/ `set_des_port` + `tcp_connect` | 0 = 请求已发出(**异步**)/ -1 | open **先于**寄存器写入(open 会复位寄存器);open 后清残留 DISCONNECT,防新连接秒断(Trap 14/17);目的 IP 字节序写反 = CONNECT TIMEOUT(Trap 16);返回 0 ≠ 已建立——调用方以 `net_sock_state` 轮询等 ESTABLISHED | +| `net_send` | `NET_MSG_SEND` | `net_send_locked` | `ch395f_write_send_buf`(分块)+ 等 SENBUF_FREE(`net_poll` 轮询) | 实际字节数 / -1 | 分块 ≤ `NET_SEND_CHUNK_MAX` = 1KB(**硬件发送 FIFO 硬上限**,Trap 20:写穿 FIFO = 数据错位);`send_ready` 门控用 `net_poll` 轮询而非 osDelay;短写**必须**告警——静默截断会让对端永久等待剩余字节(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 / TIMEDOUT / CONNRESET) | 快路径:数据已在 → 立即读出返回(允许部分读);无数据 + 阻塞 → **登记挂起后立即返回**(L3,netTask 零占用);DONTWAIT + CLOSED → EOF + 触发重监听 | +| `net_recv_to` | `NET_MSG_RECV`(timeout>0) | `net_recv_locked` | 同上 | >0 / 0 / -1 / **-2(本片超时无数据)** | E-02 头阻塞修复配套:调用方把长等待切成有界片(如 5ms/剩余预算),**总超时语义不变、单次占用有界**;`timeout_ms=0` 直接拒(会被解释成默认 30s,恰是长阻塞);超时**只凭 -2 判定、不得依赖 errno**(`s_net_errno` 是跨任务共享全局量,随时被别的任务覆盖) | +| `net_close` | `NET_MSG_CLOSE` | `net_close_locked` | `close_socket` + 等 CLOSED(5ms 节奏 / ≤500ms)+ 排空 Socket/全局中断 | 0 / -1 | **先**了结本 fd 挂起 recv(排空/取消 + 必通知,关不得让等待者悬挂);关后排空残留 DISCONNECT(Trap 14/17:否则下一连接被"刚建立就断开"秒断);控制块整体清零 | +| `net_relisten` | `NET_MSG_RELISTEN` | `net_relisten_try` → `net_relisten_locked` | `set_proto_type` + `set_sour_port` + `open_socket` + `tcp_listen`(单次尝试;隔离期另有 `close` + `get_socket_status` + 排空) | 0 / -1 | 无条件芯片侧重建:芯片侧已在监听 → 首次 open 失败 → close 重建(短暂中断 ~1s);失败**不阻塞**,登记 D10 重试;用途 = 代次变化后显式重建 / 判死后确定性重建;**-1 = "恢复中"**(重试轮询接管),不是永久失败 | +| `net_resync_listen` | `NET_MSG_RESYNC_LISTEN` | 分派内直接处理(**无芯片命令**) | 无 | 0 / -1 | D14 静默回收场景:芯片已自行把 Socket 回收回监听且**不发 DISCONNECT 中断** → 本层滞留 ESTABLISHED。只修 MCU 侧(state → LISTENING + 取消该 fd 挂起 recv);**严禁**发芯片 close/重监听——会摧毁芯片侧健康监听器 → 连接永拒(run8 实证 170s 拒绝)。非 ESTABLISHED 时视为已一致,返回 0 | + +### 4.4 直调 API(不经队列) + +| 函数 | 设计作用 | 主要调用 | 意图 / 注意 | +|---|---|---|---| +| `net_socket(domain,type)` | 分配空闲控制块(`alloc_socket` 线性扫描 `in_use=0`)→ CREATED | `alloc_socket` + `net_relisten_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` 再读数据 | +| `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 L3 pending recv 子系统(阻塞等待不独占 netTask) + +**模型**:阻塞等待 = **挂起请求登记**(每 fd 至多一个,禁止覆盖)+ **主循环节拍完成检查** + **通知唤醒**(LwIP netconn 模型)。消除 E-02 头阻塞(旧实现: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_relisten_try` ④ `net_pending_complete` | 按 **①数据 ②到点 ③断连** 顺序检查(逐项对齐旧自旋语义);CONNECTING/LISTENING 等状态**保持等待**(CONNECT→ESTABLISHED 是合法迁移,不做状态推断取消,防误杀正在建链的等待);**看门狗**:到点后 1s 仍未了结 = 完成逻辑缺位 → 强制了结 + 错误日志 | +| `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 非阻塞重监听重试子系统(监听器自愈) + +**模型**:重开失败 = **登记待重试**(30ms 节奏 / 2s 时限),`net_relisten_retry_tick` 到点重试——任何时刻不阻塞任务。旧实现 `osDelay(500)` 忙等:netTask 冻结 500ms(全网停摆)、实测恢复 588ms、期间重连被拒(M3 台架)。 + +| 函数 | 设计作用 | 主要调用 | 意图 / 注意 | +|---|---|---|---| +| `net_relisten_try` | 尝试一次;失败登记重试(**不阻塞**) | `net_relisten_locked` → 成功 `net_relisten_done` / 失败置 `s_relisten_wait/next/dead` | 已在重试队列 → 直接返回 -1(**不重置重试时刻**):否则 10ms 一次的 accept 轮询会不断把重试时刻往后推 → 永不重试(饥饿) | +| `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 23 隔离门**:带隔离标记的 socket(异常释放 TIMEOUT,如中止上传)→ 持续排空 RX 残渣 + 主动 close + 排空中断 + 等芯片回 CLOSED/TCP_CLOSED,隔离满 400ms 才放行;**800ms 上限强制放行**(必须 < 2s 重试时限,否则隔离耗尽重试预算 → `relisten give up` 监听被弃——2026-09-23 实测修正);③ 成功只置 state=LISTENING,**不 memset**(fd 与 `auto_relisten` 保留——BSD 风格监听常驻) | +| `net_relisten_retry_tick` | 每轮到点重试 | `net_relisten_locked` → `net_relisten_done` / 顺延 / 放弃 | **同 fd 有在途挂起 recv 时跳过**(重开 Socket 会摧毁在途连接,L3 保险);到 2s 时限 → 清状态 + `relisten give up` 错误日志(应用可见) | +| `net_relisten_done` | 标记完成 | `net_relisten_clear` +(探针构建)恢复计时样本 | 恢复时延 = 首次尝试 → 完成(L3 探针 `relisten` 段) | +| `net_relisten_clear` | 取消该 fd 的待重试 | 清 wait/next/dead + **沉降/隔离状态** | 触发点:`net_socket`(新 fd 不继承)/ `net_close_locked`(关闭即取消);隔离状态必须一并清,否则残留标记会污染下一次复监听 | +| `net_drain_chip_rx` | 排空芯片 RX 残留(256B 分块,≤16 次 = 4KB 上限) | `get_recv_len` + `read_recv_buf` | Trap 23:中止/异常断开时芯片 RX 缓冲残留上一条连接的数据(实测 ~2048B),不排空会被下一条连接当自己的数据读走(静默截断);分块读避免新增静态缓冲(内存预算紧张) | + +### 4.7 Trap 22 周期对账(丢中断自愈) + +`net_reconcile_tick()`(`net_process_messages` 每轮调用;周期 200ms): + +**问题**:芯片在快速复连场景会**丢 CONNECT/DISCONNECT 中断**(或静默回收 Socket 不发中断);丢事件后纯事件驱动无法自愈——典型:对端连接一直占着单连接监听的"后备"(芯片 TCP 仍 ESTABLISHED),后续 SYN 全被拒,而应用因未收到 DISCONNECT 仍以为有客户端(永不调 accept)。 + +**机制**:对每个在用 Socket(ESTABLISHED / LISTENING)回读 `GET_SOCKET_STATUS`,判定三类失同步并修正: + +| # | 失同步 | 动作 | 复用的实现 | +|---|---|---|---| +| ① | 本层 ESTABLISHED 而芯片已无连接(sock/tcp = CLOSED) | 按断开处理 → CLOSED + 回调(应用读到 EOF 后自动重监听) | `handle_disconnect_event`(内置陈旧事件甄别) | +| ② | 本层 LISTENING 而芯片已完成握手(tcp = ESTABLISHED) | 补做 standalone accept 簿记 | `handle_connect_event` | +| ③ | 本层 LISTENING 而芯片监听已消失(异常关闭) | 重建监听 | `net_relisten_try`(D10 非阻塞重试) | + +**去抖 + 冷却(缺一不可)**:单次状态读可能落在"刚 open/listen 尚未生效"的瞬态——**2026-09-23 实测**:无去抖的 accept 路径自愈把监听反复重建直至 `relisten give up`(三服务口全死、89 行 ERROR)。故:同一失配需**连续 2 次**(≈400ms)才动手;同一 Socket 动手后**冷却 1s**。开销:每 200ms 每在用 Socket 一条状态命令,可忽略。 + +### 4.8 事件处理器(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 的大端相反)——解析别抄反 | +| `handle_disconnect_event` | 状态机迁移 + 陈旧事件甄别 + 监听丢失重建 | `ch395f_get_socket_status` →(LISTENING:芯片已不可监听 → `net_relisten_try`,仍在监听 → 判陈旧丢弃)/(ESTABLISHED:芯片仍 ESTABLISHED → 判陈旧丢弃) | ① **陈旧 DISCONNECT 甄别**(D20):同一 fd"关闭→重监听→再连接"循环复用,上一条连接的 FIN/ACK 可能在 fd 已复用时才上报 → 不甄别则新连接秒断(实测:FTP 同会话第二个数据连接必 425);② LISTENING 分支**已不可监听即重建**(Trap 22 修复的一半);③ 置 CLOSED 后**不立即关芯片 Socket**——留给 `net_recv_locked` 排空剩余数据(FIN 可能先于数据到达) | +| `handle_recv_event` | 置 `readable` + 回调;**不读 recv_len** | `fire_event` | 不读长度是为了避免与 `net_recv_locked` 的 `ch395f_get_recv_len` 竞争(Trap 13:分批读会归零);`readable` 供 `net_select` 无 SPI 访问判读 | +| `handle_send_event` | 发送缓冲已空:触发回调(`send_ready` 由 SENBUF_FREE 前置位) | `fire_event` | — | +| `handle_timeout_event` | CONNECTING → CREATED(连接超时);**ESTABLISHED → 置重监听隔离 + CLOSED**(Trap 23);LISTENING → 仅记日志 | (隔离)写 `s_relisten_quarantine[fd]` + `fire_event` | TIMEOUT = 异常释放(如客户端中止上传):对端在途数据随后落进 RX 缓冲 → 必须隔离后才许复监听;正常 FIN 释放走 disconnect 路径,**不走**隔离(零改动) | +| `fire_event` | 调用已注册回调 | `p_sock->event_cb` | **回调上下文 = netTask**:回调内不得阻塞、不得发网络操作(会自等死锁) | + +### 4.9 工具函数 + +| 函数 | 设计作用 | +|---|---| +| `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/块) + loop 每块 + N->>C: 等 SENBUF_FREE(net_poll 轮询,不睡) + end + C-->>N: SENBUF_FREE / SEND_OK(net_poll 置 send_ready) + N->>A: xTaskNotify(实际字节数 / 负错误码) + A->>A: 返回字节数 / -1(errno 已恢复) +``` + +### 5.2 阻塞接收无数据(L3 pending 路径) + +```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 23 隔离) + +```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 + 回调 + else TIMEOUT(异常,如中止上传) + N->>N: handle_timeout_event:置重监听隔离 + CLOSED + end + A->>N: 下一次 net_recv(无数据)/ net_accept + N->>C: net_relisten_try → net_relisten_locked + opt 隔离期内(Trap 23) + 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 09 沉降) + N->>N: 登记重试(30ms 节奏 / 2s 时限)→ 每轮到点重试 + else 成功 + N->>N: state = LISTENING(auto_relisten 保留) + N-->>A: xTaskNotify(accept 返回 / recv 返回 0) + end + Note over N: 2s 超限时 → 清状态 + "relisten give up"(错误日志,应用可感知) +``` + +### 5.4 丢中断自愈(Trap 22 对账) + +```mermaid +sequenceDiagram + participant N as netTask + participant C as CH395F + participant A as appTask + + Note over C: 快速复连:芯片完成握手但 CONNECT 中断丢失 + Note over N: 本层滞留 LISTENING → 应用以为无客户端(永不调 accept) + loop 每 200ms + N->>C: net_reconcile_tick:get_socket_status(每个在用 socket) + C-->>N: 失配(本层 LISTENING,芯片 ESTABLISHED) + end + Note over N: 连续 2 次失配(去抖 ≈400ms)且不在冷却期 → 动手 + N->>N: handle_connect_event:补 accept 簿记 → ESTABLISHED + N-->>A: 下一次 net_accept 返回客户端(连接不再"吞掉") + Note over N: 另两类:① 本层 ESTABLISHED 而芯片已断 → handle_disconnect_event
③ 本层 LISTENING 而芯片监听消失 → net_relisten_try +``` + +### 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(0x09)/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 16:跨任务 SPI 竞争破坏芯片寄存器 → 连接必超时 | +| 2 | **结果只经任务通知值回传**;消息按值入队,调用方看不到 netTask 对消息的写入;勿给消息加"回传用"字段 | Trap 10:6 个 API 永远返回 -1;"加 msg.err 字段"的修法曾致 `ui_net_sink` 每次空轮询误判真错误 → 连上即断 | +| 3 | **netTask 不为任何人自旋等待**:阻塞 recv = 挂起登记(L3);重开失败 = 重试登记(D10);初次 listen/connect 的 3×500ms 是唯一豁免(启动期无竞争) | E-02:一次空闲 30s recv 冻住全网 17.7s;旧重监听 osDelay(500) → 恢复 588ms、期间重连被拒 | +| 4 | **netTask 不向自身发消息 / 不自我挂起**(挂起登记对执行任务自身发起直接拒绝) | 自等自通知 = 死锁 | +| 5 | **基于单次状态读的重建动作必须去抖**(连续 2 次)+ 冷却(1s) | 2026-09-23:无去抖 → 监听反复重建直至 `relisten give up`,三服务口全死 | +| 6 | **异常释放(TIMEOUT)后复监听前必须隔离**:排空 RX 残渣 + 等 CLOSED/TCP_CLOSED;隔离上限 < 重试放弃时限 | Trap 23:~2KB 残渣被下条连接当自己的数据 → 首笔上传截断 2048B 并误报 226 成功;上限=2s 时隔离耗尽重试预算 → 监听被弃 | +| 7 | **RECV 电平触发中断每轮只处理一批** | Trap 08:连续循环 → 主循环饿死 | +| 8 | **close 前先了结挂起 recv + 排空残留 DISCONNECT** | Trap 14/17/19:锁存的陈旧 DISCONNECT 秒断下一连接 | +| 9 | **两段式等待防丢失通知**:零等待拾取 + 阻塞;清除掩码全 1;以 pdTRUE 判定 | run3~11:丢失通知 → 挂死 / 结果错位;0 结果误判"无通知" → 永久挂死 | +| 10 | **网络重建先取消并通知全部挂起,再复位芯片** | 取消不通知 = 调用方永久挂死 | + +--- + +## 7. ch395f 驱动接口使用图(net_socket.c → 芯片命令) + +| 芯片命令 | 使用处 | 说明 | +|---|---|---| +| `ch395f_check_exist` / `ch395f_reset` | `net_init` | 上电探测与复位 | +| `ch395f_set_fun_para(0x09)` | `net_init` | 多 Socket + TCP 单连接 + 手动关缓冲 | +| `ch395f_set_tcp_mss` / `set_retrans_count` / `set_retrans_period` / `set_keepalive_idle+intvl+cnt` | `net_init`(**均须在 `ch395f_init` 之前**) | 值见 `net_config.h` | +| `ch395f_set_ip_addr` / `set_gwip_addr` / `set_mask_addr` / `set_dhcp` | `net_init` | 固定 IP 或 DHCP | +| `ch395f_init` / `ch395f_set_mac_addr` / `ch395f_ping_enable` | `net_init` | 协议栈初始化 / MAC(EEPROM,幂等)/ PING | +| `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`;`net_listen_locked` / `net_connect_locked` / `net_close_locked` / `net_relisten_locked`(清残留) | **读 = 清**(中断清除机制) | +| `ch395f_get_sock_int_status` | `net_poll`(逐 Socket SINT);各 open 路径后清残留;事件处理器清 TIMEOUT | **读 = 清** | +| `ch395f_open_socket` / `ch395f_close_socket` | `net_bind`(UDP)、`net_listen_locked`、`net_connect_locked`、`net_close_locked`、`net_relisten_locked`(含隔离期主动 close) | Trap 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_sendto` | 单次 ≤1KB(硬件 FIFO 上限,Trap 20) | +| `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_recv_event` **故意不读**(防竞争,Trap 13) | +| `ch395f_get_socket_status` | `net_reconcile_tick`、`handle_disconnect_event`(两处甄别)、`net_close_locked`(等 CLOSED)、`net_relisten_locked`(隔离门)、`net_connect_locked`(诊断回读) | 输出 **2 字节**([0]=Socket 状态 [1]=TCP 状态),缓冲必须给 2 字节 | +| `ch395f_get_remot_ipp` | `handle_connect_event`、`net_connect_locked`(诊断) | **小端**输出(与 set_des_ip 相反) | +| `ch395f_set_keepalive_enable` | `handle_connect_event`(client 连接成功时) | — | + +--- + +## 8. 改动意图速查 + +| 想改什么 | 先看哪些函数 | +|---|---| +| 监听恢复策略(节奏/时限/隔离) | `net_relisten_try` / `net_relisten_locked` / `net_relisten_retry_tick`;常量 `NET_RELISTEN_*_MS`(`net_socket.c` 内) | +| 阻塞接收等待语义 | `net_pending_register` / `net_pending_tick` / `net_pending_complete`;常量 `NET_PENDING_WATCHDOG_MS` | +| 丢中断自愈策略 | `net_reconcile_tick`(周期/去抖/冷却三常量)+ `handle_disconnect_event` LISTENING 分支 | +| 新增一个公共 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_socket.c` 内 `NET_RELISTEN_*` / `NET_RECONCILE_*` / `NET_PENDING_WATCHDOG_MS` | + +--- + +## 9. 相关文档 + +- `docs/BSD_Socket_API_使用指南.md` —— 公开 API 语义、示例、常见问题(本文的上半部) +- `docs/问题记录/CH395F_Trap_Records.md` —— Trap 01~23 机制细节(本文引用处) +- `docs/问题记录/Net层阻塞模型业界对照与演进方案.md` —— L3 pending 化的设计推演 +- `docs/问题记录/错误码传播审计.md` —— E-02 头阻塞与错误码回传全审计 +- `docs/测试说明/链路延迟测试说明.md` —— D1/D10/L3/D14 等时延探针与自愈路径的台架证据 +- `docs/网络配置说明.md` §4 —— 代次号与网络重建时序 +- `docs/测试说明/CH395F_Test_Guide.md` —— 单连接模型测试规则(TC-NET-6xx)