Files
STM32F4-Base/docs/网络/BSD_Socket_API_函数调用关系.md
T
wandering1 a6cdebde62 文档体系重组与 D-7 一致性门禁落地,清理芯片手册孤儿图片
- D-6 目录规范化:docs 根目录散落文档归入 UI设计/子系统设计/架构设计/审查记录/
  环境与部署/网络 等子目录,去掉目录名空格,重命名 嵌入式C语言代码规范_V1.0.md
- D-7 三档一致性门禁:新增 scripts/check_doc_links.py(链接存在性)、
  scripts/gen_task_table.py(FreeRTOS 任务表数值)、scripts/check_mem_numbers.py
  (内存/Flash 体积),均支持 --check/--write/--diff,pre-commit 钩子同步注册
- D-5 文档索引:新建 docs/README.md,新建 docs/文档规范.md 约束目录命名与链接写法
- D-1 芯片手册瘦身:删除 1069 张无引用孤儿图片(77.06 MB),
  docs/芯片手册 120 MB → 41 MB,已引用图片零断链
- 同步 C 源码注释中的文档路径引用(D-6 重组后),构建 0 错误 0 警告
2026-09-26 00:51:53 +08:00

37 KiB
Raw Blame History

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. 总体调用关系图

flowchart TB
    subgraph APP["应用任务(非 netTask)"]
        PUB["经队列公共 API<br/>netListen · netAccept · netConnect · netSend<br/>netRecv · netRecvTo · netClose<br/>netRelisten · netResyncListen"]
        RAM["直调 · 纯 RAM<br/>netSocket · netSockState · netGetSock<br/>netGetErrno · netSetEventCb · netGetSockname<br/>字节序(netHtons / netInetAddr …)"]
        SPI["直调 · 直碰 SPI(须芯片唯一上下文)<br/>netBind(UDP) · netSendto · netRecvFrom"]
        CB["用户事件回调<br/>(在 netTask 上下文执行!)"]
    end

    MQ[("netMsgQueue<br/>8 槽 · 按值拷贝")]
    XN["xTaskNotify / xTaskNotifyWait<br/>(结果回传的唯一通道)"]

    subgraph NT["netTask 主循环 — 1ms/tick;芯片操作唯一执行者"]
        NPM["netProcessMessages<br/>先跑 3 个 tick,再排空队列分派"]
        NP["netPoll<br/>INT# GPIO 轮询 → 全局中断 → 各 Socket SINT → 每轮一批"]
        T1["① netRelistenRetryTick<br/>D10 重监听重试:30ms / 2s 时限"]
        T2["② netPendingTick<br/>L3 挂起 recv:①数据 ②到点 ③断连 ④看门狗 1s"]
        T3["③ netReconcileTick<br/>Trap 22 对账:200ms / 去抖 2 次 / 冷却 1s"]
        DISP["队列分派 switch<br/>SEND · RECV · CLOSE · CONNECT · LISTEN<br/>ACCEPT · RESYNC_LISTEN · RELISTEN"]
        subgraph LK["_locked / 内部分派目标(仅 netTask 上下文)"]
            LL["netListenLocked<br/>初次监听(3×500ms 阻塞重试)"]
            LN["netConnectLocked<br/>3×500ms 重试 + tcpConnect"]
            LS["netSendLocked<br/>分块 ≤1KB + sendReady 门控"]
            LR["netRecvLocked<br/>快路径 / DONTWAIT / 登记挂起"]
            LA["netAcceptLocked<br/>先 netPoll + 状态判定"]
            LC["netCloseLocked<br/>了结挂起 + close + 等 CLOSED + 排空中断"]
            RLS["netRelistenTry → netRelistenLocked<br/>单次尝试 + Trap 23 排渣隔离"]
            RSY["RESYNC 分支:改状态 + 取消挂起<br/>(不发芯片命令)"]
        end
        EV["事件处理器<br/>handleConnect · handleDisconnect<br/>handleRecv · handleSend · handleTimeout"]
        FE["fireEvent → 用户回调"]
        PR["s_net_pending<br/>(每 fd 至多一个挂起)"]
        CH[("ch395f.* 驱动<br/>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 发送(队列路径)

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 路径)

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 隔离)

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 对账)

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<br/>③ 本层 LISTENING 而芯片监听消失 → net_relisten_try

5.5 网络重建(IP 变更免重启)

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<br/>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)