Files
STM32F4-Base/docs/网络/BSD_Socket_API_函数调用关系.md
T
super_agent_bot a91640dc6e
CI / 静态门禁 (pre-commit) (push) Failing after 6m12s
CI / 主机侧单测 (MSVC) (push) Failing after 1m14s
CI / 静态门禁 (pre-commit) (pull_request) Failing after 45s
CI / 主机侧单测 (MSVC) (pull_request) Failing after 44s
feat(net/udp): UDP 数据面并入 netMsgQueue 串行执行(#73)
前置来自 #57 SNTP 调研:旧 net_sendto / net_recvfrom 由调用方直调
SPI 且无限自旋收包,既违约「netTask 是芯片唯一访问者」,又把 IWDG
预算吃在自旋里。

改造面
- 新增 Drivers/BSP/NET/net_udp.c/.h(真机专属,模拟器不编译):
  bind/set_peer/sendto/recvfrom 四条执行体 + 挂起收包表 + tick;
  net_msg_type_t 加四型,公开 API 一律「入队 → 睡在通知上」。
- 源地址出参不搭 net_msg_t(按值拷贝,AGENTS 禁加回传字段):改住
  net_udp.c 的 per-fd 槽,netTask 就地写回调用方内存,通知值只带
  结果码;每条出口都清槽,泄漏 = 该 fd 永久 BUSY。
- 发送复用 net_send_locked,不写第二份分块逻辑;「会话是否已死」
  收成一条判据 net_send_state_down()(TCP 看 ESTABLISHED、UDP 看
  UDP_OPEN),原先 4 处各写一遍。
- 顺带修掉真缺陷(新条 TRAP-NET-22):旧 sendto 把 DesIP 写成具体
  IP,芯片静默切 UDP Client,而旧 recvfrom 无条件剥 8 字节 → SNTP
  这类 48 B 应答丢载荷前 8 字节并交出垃圾源地址。加 udp_mode(塞进
  原有 padding,sizeof 不变)、bind 恒 SERVER、显式对端 sendto 切
  CLIENT、set_peer(fd, NULL) 轻量回 SERVER,仅 SERVER 且 avail >= 8
  才剥头。
- 截断定案:一次调用交付一个数据报,buf 装不下的尾巴用 64 B 栈上
  scratch 排空(不用 clear_recv_buf——它连后面已到达的包一起丢)。
- 取消点接齐:net_init 重建 cancel_all、net_socket 分配 fd clear、
  net_close settle_close + clear、重 bind 前先结算等包者,
  net_process_messages 第 5 个 tick 位挂 net_udp_tick。
- 收紧两处契约(原实现无生产调用者):UDP 未 bind 一律 -5;
  net_recv / _nb / _to 拒 DGRAM(绕过剥头就是静默吃 8 字节);
  NET_ERR_NOT_IN_NETTASK(-12) 退役为历史常量并写明原因。

验证
- 主机单测 test/host/net_socket 全套 616/616 绿(改造前 470):新
  分片 ns_s7_udp.c 20 组,改写 440 / 555 / 560 组;覆盖率
  net_udp.c 234/236(99.2%)、net_socket.c 956/969(98.7%),
  scripts/test_coverage_baseline.json 随本提交登记新文件。
- 测试抓到 4 个被测码真 bug:自等守卫误用调用侧谓词、芯片长度被读
  两次、槽指针取在 fd 合法性检查之前、重 bind 静默作废等待者。
- Keil ARMCC 0 Error(s) 0 Warning(s);net_udp.o Code 1,358 / ZI 224
  (8 fd × 28 B,其余模块零增长),全仓 RW_IRAM1 72.2%;内存台账
  受管区按门禁提示用 --write / --update 机器回填,未手改数字。
- pre-commit 全量钩子(含 doc-links、coding-standard、mem-budget、
  generated-fresh、bsp-err、registration、nested-comment)全绿。

限度
- 本轮无板子:真机回归 5 条(两端互通、set_peer(NULL) 能否恢复收
  广播、截断排空后下一包完整性、挂起 recv 期间 IWDG 余量、bind 写
  DesIP 与 open 先后的真芯片效果)已写进覆盖率口径「台架必验」。
- scripts/stack_report.py 退出码 1 是 10-26 起挂在账上的 iec104Task
  栈债(margin 1,016 < 1,024,见 docs/代码规范/pre-commit使用说明.md:
  122);本轮只让 net_bind 的 UDP 分支多一层静态帧(1,544 → 1,600),
  netTask margin 1,888 与 timeSyncTask 1,152 不受影响。抬栈要过
  mem-budget 台账,不在本分支自行处理。
- 登记簿销项按仓库约定留给主工作树,本分支不动 docs/任务规划/**。
2026-11-03 18:11:20 +08:00

61 KiB
Raw Blame History

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

flowchart TB
    subgraph APP["应用任务(非 netTask)"]
        PUB["经队列公共 API<br/>netListen · netAccept · netConnect · netSend<br/>netRecv · netRecvTo · netClose · netDropConn<br/>UDP:netBind · netSendto · netRecvFrom · netRecvFromTo · netUdpSetPeer"]
        RAM["直调 · 纯 RAM<br/>netSocket · netSockState · netGetSock<br/>netGetErrno · netSetEventCb · netGetSockname<br/>字节序(netHtons / netInetAddr …)"]
        SPI["板级硬件层测试 · 直调 BSP(须芯片访问者唯一)<br/>ch395f_*(该模式下 netTask 自挂起)"]
        CB["用户事件回调<br/>(在 netTask 上下文执行!)"]
    end

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

    subgraph NT["netTask 主循环 — 1ms/tick;芯片操作唯一执行者"]
        NPM["netProcessMessages<br/>先跑 4 个 tick,再排空队列分派"]
        NP["netPoll<br/>INT# GPIO 轮询 → 全局中断 → 各 Socket SINT → 状态缓存全量刷新 + sock_reset 步进 → 每轮一批"]
        RetryTick["① netListenerRetryTick<br/>D10 重监听重试:30ms / 2s 时限"]
        PendTick["② netPendingTick<br/>挂起 recv:①数据 ②到点(仅显式预算) ③断连 ④tripwire 60s 只留痕"]
        PendShimTick["③ netSendPendingTick<br/>挂起 send:①断连 ②推进(每轮≤1块) ③写完 ④无进展超时"]
        ReconcileTick["⑤ netReconcileTick<br/>TRAP-NET-17 对账 + 监听重建合一:100ms / 分级去抖 / 冷却<br/>读驱动状态缓存(①②③④⑤ + U11)"]
        UdpTick["④ netUdpTick<br/>UDP 挂起 recv:①数据到达(读一个完整数据报) ②预算到点 -2 ③会话已死"]
        DISP["队列分派 switch<br/>SEND · RECV · CLOSE<br/>CONNECT · LISTEN · ACCEPT · DROP_CONN<br/>UDP_BIND · UDP_SENDTO · UDP_RECVFROM · UDP_PEER"]
        subgraph LK["_locked / 内部分派目标(仅 netTask 上下文)"]
            LL["netListenLocked<br/>初次监听(open 重试 3× + sock_reset 排渣)"]
            LN["netConnectLocked<br/>open 重试 3×(sock_reset 排渣)+ tcpConnect"]
            LS["netSendLocked<br/>分块 ≤1KB + sendReady 门控;超 NET_SEND_SPIN_MS 登记挂起 send"]
            LR["netRecvLocked<br/>快路径 / DONTWAIT / 登记挂起"]
            LA["netAcceptLocked<br/>先 netPoll + 状态判定"]
            LC["netCloseLocked<br/>结束挂起 + cmd_done 快路径 / sock_reset(GRACEFUL)"]
            DC["netDropConnLocked<br/>拆会话保监听:sock_reset(QUARANTINE) + 登记重监听"]
            RLS["netListenerTry → netRelistenLocked<br/>单次尝试 + TRAP-NET-18 排渣隔离"]
            UB["netUdpBindExec<br/>开芯片侧 Socket(恒服务器模式,DesIP=FFH 在 open 前写)"]
            US["netUdpSendtoExec<br/>带地址则先写 DesIP/DesPort(转客户端模式)"]
            UR["netUdpRecvfromExec<br/>快路径 / DONTWAIT / 登记挂起 recv"]
            UP["netUdpSetPeerExec<br/>只改 DesIP/DesPort,不重开 Socket"]
        end
        EV["事件处理器<br/>handleConnect · handleDisconnect<br/>handleRecv · handleSend · handleTimeout"]
        ST["netListenerSettle<br/>只置沉降/隔离事实(100ms / 400ms)"]
        FE["fireEvent → 用户回调"]
        PR["s_net_pending<br/>(每 fd 至多一个挂起 recv)"]
        SR["s_net_send<br/>(每 fd 至多一个挂起 send)"]
        SU["s_net_udp<br/>(每 fd 至多一个接收槽:调用方出参指针 + 挂起 recv)"]
        CH[("ch395f.* 驱动<br/>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 发送(队列路径)

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

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 沉降)

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 + 回调<br/>+ 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)

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 轮)<br/>③④ 本层 LISTENING 丢监听 / CLOSED 孤儿 → 本 tick 形态③④ 重建(唯一重建入口,去抖 2 轮)<br/>⑤ 静默回收 → RESYNC 只修本层;U11 芯片在听 → 只重同步

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(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<br/>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
新增一个公共 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)