Files
STM32F4-Base/docs/网络/BSD_Socket_API_使用指南.md
T
wandering1 506eccc68f docs: 错误码传播审计收口并删除该文档(E-xx 转历史编号族)
本次会话先后销项与关闭:
- E-61 SD2506 清 INTAF:原实现只读取 CTR1,而按手册 ARST=0 时读操作
  不清标志,属空操作;改为读-改-写 CTR1,并按手册订正 CTR1 位宏
  (INTAF/INTDF/BLF 位置,ARST 归 CTR3)
- E-16 cfg_file:新增 CFG_FILE_MAX_NAME_LEN(段名/键名 ≤15),注册时
  拒绝超长键名;s_gen_document 两处 snprintf 改检查式;补 TC-CFG-024
  用例(主机单测 149→151 PASSED)
- E-23 TC704:PHASE7_MULTI_COUNT 8→5(并发上限 = 8 − 3 个常驻监听),
  accept 失败分支加网络代次检查(网络重建即重建监听)
- E-26(UDP)按“保留 API + 文档定案”关闭;E-30 评估为不必要后关闭;
  N-4(ACCEPT 决定不实施)关闭

审计文档剩余 5 项 P2 + 2 项监督,经用户裁定“都不重要”后整体删除:
- 删除 docs/问题记录/错误码传播审计.md(634 行)
- 九类缺陷模式表迁入 docs/编号规范.md §4.2.1(PAT-01~09)
- E-xx 转为纯历史编号族:编号规范 §4.1 属主改“无(原属主已删除)”,
  §4.3 保持冻结;全仓 26 处引用按“保留编号、去悬空文档名”处理
- 顺带修正 N-4 收口遗留:排期详述 C1 才是 E-25/E-27/E-28 的承载处
  (审计 §4 曾误写为登记簿);README 两处 C1 旧名改回“net 层跨上下文收口”

一并提交此前遗留的体系层评审报告 2026-10-14 复核 WIP(六维评分复核、
未闭环 Top 5→3、三区水位实测刷新)。

校验:Keil 构建 0 错误 0 警告(Code=212344);check_doc_links
--sections-strict 与 --all(142 文档)通过;内存双台账刷新后均 PASS。
2026-10-15 07:39:38 +08:00

76 KiB
Raw Blame History

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、挂起 recv 化、D10 非阻塞重监听、TRAP-NET-17 周期对账、TRAP-NET-18 排渣隔离)。详见文末变更记录。

伴读文档:本文回答"上层该怎么调";内部函数之间的调用关系、每个函数的设计作用与意图(netTask / 消息队列 / 挂起 recv / D10 重试 / TRAP-NET-17 对账 / TRAP-NET-18 排渣各子系统,含总体调用图与端到端时序)见 docs/网络/BSD_Socket_API_函数调用关系.md。

目录

  1. 概述
  2. 架构说明
  3. 线程安全设计
  4. 快速开始
  5. TCP 编程
  6. UDP 编程
  7. I/O 多路复用
  8. 事件回调机制
  9. 字节序处理
  10. API 参考
  11. 完整示例
  12. 常见问题

1. 概述

本项目基于 CH395F 以太网协议栈芯片,提供了一套兼容 Linux BSD Socket API 的网络编程接口。这套接口屏蔽了底层硬件细节,让嵌入式网络编程与标准 Linux 网络编程保持一致。

1.1 支持的功能

功能 说明
TCP Client 主动连接远程服务器
TCP Server(单连接) 支持 1 个客户端连接
UDP Client 发送 UDP 数据报
UDP Server 接收 UDP 数据报
select/poll I/O 多路复用
事件回调 异步事件通知

1.2 与 Linux Socket 的差异

特性 Linux Socket 本实现
Socket 数量 无限制 最多 8 个
阻塞默认 默认阻塞(无隐藏超时) 同(net_recv() 无限阻塞,超时归调用方——net_select / net_recv_to)
非阻塞模式 O_NONBLOCK / MSG_DONTWAIT net_setblocking(fd, 0)(模式属性)/ net_recv_nb() / NET_MSG_DONTWAIT(单次)
错误码 errno net_get_errno()
头文件 <sys/socket.h> "net_socket.h"
无限阻塞约束 无 ⚠ IWDG required_mask 内任务禁止(嵌入式特有契约,§10.1)

2. 架构说明

2.1 分层架构

BSD 网络栈分层架构

2.2 文件说明

文件 说明
net_config.h 配置宏定义(最大 Socket 数、超时时间等)
net_types.h 类型定义(地址结构、Socket 控制块等)
net_socket.h BSD Socket API 头文件,含 net_msg_t 消息类型定义
net_socket.c API 核心实现:消息队列处理与线程安全封装、内部 _sock() 版本;另含挂起 recv 登记路径
net_events.c(真机专属) 状态面收敛(2026-10-14 从 net_socket.c 抽出,与 net_listener.c 同族):5 个 handle_*_event(net_poll 检测中断后的分派目标)+ fire_event + TRAP-NET-17 周期对账 + 监听重建合一 reconciler(net_reconcile_tick,100ms,分级去抖+冷却,形态①②③④⑤/U11)+ net_lost_connect_probe;只置事实与登记意图,重建动作在 net_listener_tick;内部头 net_events.h,模拟器不编译
net_listener.c(真机专属) 监听生命周期(E17 单一所有者收口):net_listener_request(只登记意图,5 个登记点)/ net_listener_tick(唯一执行者,门集 + D10 重试 30ms/2s)/ net_listener_clear / net_listener_settle(沉降/隔离 100ms/400ms);"该不该重建"的周期判据在 net_reconcile_tick 形态③④(单一 100ms tick);内部头,模拟器不编译
net_select.h select/poll API 头文件
net_select.c select/poll 实现(仅 MCU 端编译)
net_socket_sim.c(simulator/sim/) Winsock2 后端(模拟器端,与 net_socket.c 互斥编译),详见 §2.3

双端构建:net_socket.h/.c、net_select.h/.c 同时参与 MCU 端与 PC 模拟器端编译;模拟器端定义 NET_NO_OS,裁掉消息队列 / 事件回调等 OS 相关接口,仅保留 BSD Socket 核心 API(模拟器网络后端为 Winsock,net_init 不初始化 CH395F,详见 §2.3)。

网口配置:IP / 掩码 / 网关 / MAC 由 Drivers/BSP/NETCFG/net_cfg 持久化(上电加载,UI 网口设置页可改并立即生效),见 docs/子系统设计/网络配置说明.md。

2.3 PC 模拟器端:net_socket_sim

PC 模拟器(simulator/,CMake + SDL2)与 MCU 端共用同一份上层网络源码(ui_net_sink.c、net_cfg.c 等),网络后端换成 simulator/sim/net_socket_sim.c(Winsock2 实现)。构建时定义 NET_NO_OS,裁掉 net_socket.h 中的 FreeRTOS 相关接口:消息队列类型 net_msg_t、net_process_messages() 与 net_send_sock() / net_recv_sock() / net_close_sock() / net_listen_locked() 四个内部 API。上层使用同一套 net_* BSD API,对后端差异无感。

项 MCU 端(net_socket.c) 模拟器端(net_socket_sim.c)
底层后端 CH395F(SPI2) Winsock2(链接 ws2_32)
net_init() 芯片初始化 / 复位 / 配置 IP·MAC / 分配 Socket 缓冲 仅确保 WSAStartup(幂等);ip/mask/gw/mac 四参数全部忽略
fd 模型 控制块索引 0~7 相同(并行数组 s_ws[] 存 Winsock 句柄,不侵入共享的 net_sock_t)
并发 / 阻塞 消息队列 + netTask 串行化 单线程直调;Winsock 句柄创建即非阻塞(FIONBIO),阻塞语义按控制块 blocking 属性在包装层模拟(net_recv 轮询等待 / net_recv_nb 立即返回,与 MCU 端对齐)
net_poll() CH395F 中断轮询 + 状态机更新 空操作(返回 0,非阻塞 I/O 直接反映事件)
net_accept() 单连接:监听 Socket 自身即数据通道(返回监听 Socket 自身) Winsock 语义:返回独立于监听 Socket 的新 fd(监听 Socket 保持 LISTENING)
事件回调 net_poll 检测后主动触发 仅存储,不主动触发(上层自行非阻塞轮询感知)
代次号 网络重建 +1(服务自愈依据) 恒为 0(无重建概念,上层自愈逻辑空转)
net_bind() — 先设 SO_REUSEADDR(断线重监听可复用端口)
错误码 CH395F 状态码映射 WSAGetLastError() 映射(WSAEWOULDBLOCK → NET_ERR_WOULDBLOCK)
select/poll net_select.c 参与编译 未编译(上层不依赖)

模拟器端网络启动(镜像 MCU 端上电流程:defaultTask 在就绪屏障前加载 net_cfg,见 simulator/main.c):WSAStartup → net_cfg_load()(网口配置 RAM 副本,失败回退工厂默认)→ ui_net_sink_start(7003)(RemoDispBus TCP 监听,与 MCU 端同端口)→ 主循环 ui_net_sink_poll()(非阻塞协议收发)+ net_cfg_save_run()(执行 UI 保存请求,主循环 = 配置引擎属主)。不调用 net_init()——WSA 初始化由 net_socket_sim.c 内部 ensure_wsa() 幂等保证,net_init() 仅为对齐 API 而保留(上层代码双端统一调用)。

2.4 Socket 状态机

stateDiagram-v2
    [*] --> CLOSED : 初始(in_use = 0)
    CLOSED --> CREATED : net_socket() 分配描述符
    CREATED --> BOUND : net_bind()(TCP)
    CREATED --> UDP_OPEN : net_bind()(UDP)
    CREATED --> LISTENING : net_listen()
    BOUND --> LISTENING : net_listen()
    BOUND --> CONNECTING : net_connect()
    CONNECTING --> ESTABLISHED : 连接建立(CONNECT 中断)
    CONNECTING --> CREATED : 连接超时
    LISTENING --> ESTABLISHED : 客户端连接(standalone accept)
    ESTABLISHED --> CLOSED : 对端断开 / 超时 / net_close()
    CLOSED --> LISTENING : auto_relisten 自动重监听
    UDP_OPEN --> CLOSED : net_close()

状态名与 net_types.h 的 net_sock_state_t 一一对应(共 8 态)。另有 TCP_CLIENT_RECONNECTING 状态:应用层用它标记 TCP Client 的重连阶段(非驱动自动迁移,故未画入上图)。

单连接 Server 自动重监听(auto_relisten):net_listen() 会为 TCP 监听 Socket 置 auto_relisten = 1。连接断开(net_recv 收到 EOF / 空闲 net_accept)时,net 层自动重新 OPEN + TCP_LISTEN,状态由 ESTABLISHED/CLOSED 自动回到 LISTENING,应用层无需调用 net_close() + net_listen()。这与 Linux BSD Socket“监听 Socket 常驻”语义一致——net_accept() 在每条新连接到达时返回,服务端循环不必维护重监听逻辑。


3. 线程安全设计

3.1 设计背景

NET 层(net_socket.c + ch395f.c)不是线程安全的:

  • Socket 控制块(s_net_socks[])无互斥保护
  • CH395F SPI 事务不重入(STM32 HAL SPI 无内部锁)
  • 状态机在多个任务中并发访问会崩溃

引入消息队列将所有的 CH395F 操作串行化到 netTask 中执行。

3.2 消息队列模式

sequenceDiagram
    participant A as App Task A
    participant B as App Task B
    participant Q as 消息队列 (osMessageQueue)
    participant N as netTask

    A->>Q: net_send() → 入队 net_msg_t
    B->>Q: net_recv() → 入队 net_msg_t
    A->>A: ulTaskNotifyTake() 阻塞等待结果
    B->>B: ulTaskNotifyTake() 阻塞等待结果
    N->>Q: net_process_messages() 出队
    N->>N: 执行请求(net_send_sock / net_recv_sock / net_close_sock)
    N->>Q: 写结果
    N-->>A: xTaskNotifyGive()
    N-->>B: xTaskNotifyGive()
    A-->>A: 返回结果给调用方
    B-->>B: 返回结果给调用方
    Note over N: 每周期还调用 net_poll()<br/>CH395F 中断轮询 + 状态机更新

3.3 关键规则

规则 说明
netTask 外(任意应用任务) 公共 API:net_send() / net_recv() / net_recv_to() / net_accept() / net_connect() / net_close() / net_listen() / net_drop_conn()——请求入队列,netTask 串行执行,调用方阻塞等回执。自愈无显式 API(net_relisten() / net_resync_listen() 已退役,静默回收/监听失位由对账⑤与 reconciler 形态③④自动认领,见 §3.6)
netTask 内 内部 API net_send_sock() / net_recv_sock() / net_close_sock() / net_listen_locked()
net_socket() / net_bind() 直接调用,无需队列;实际在就绪屏障后的服务任务(timeSyncTask / ftpTask / uiTask)中创建监听
net_poll() 和 net_process_messages() 必须在 netTask 主循环调用;net_process_messages() 每轮先跑 4 个非阻塞 tick(D10 重监听重试 → 挂起 recv 完成 → 挂起 send 推进 → TRAP-NET-17 对账+监听重建合一 reconciler 100ms),再处理队列

3.4 内部 API 说明

内部 API(带 _sock 后缀)直接操作 Socket 控制块和 CH395F,不经过消息队列。用于 netTask 中的网络业务逻辑,避免向自身发送消息造成死锁:

/* 在 netTask 中直接操作 Socket,绕过消息队列 */
int net_send_sock(net_sock_t *p_sock, const void *buf, int len);
int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags);
int net_close_sock(net_sock_t *p_sock);

3.5 注意事项

  • 消息队列由 CubeMX 创建:在 MX_FREERTOS_Init()(osKernelInitialize() 之后)中调用 osMessageQueueNew(8, sizeof(net_msg_t), NULL) 创建
  • 队列句柄全局可见:通过 extern osMessageQueueId_t netMsgQueueHandle 引用
  • netTask 不可向自身发消息:netTask 中直接调 net_send() 会死锁(等待自己处理消息),必须在 netTask 中使用 net_send_sock() 等内部版本
  • 阻塞 recv 不独占 netTask(挂起 recv 模型):无数据时公共 net_recv() / net_recv_to() 登记挂起请求后返回(每 fd 至多一个,禁止覆盖——覆盖即丢失前调用方通知 = 永久挂死);完成由 net_pending_tick() 按主循环节拍检查(①数据 ②到点[仅显式预算] ③断连)并经任务通知唤醒调用方——等待期间调用方在自己上下文睡通知,netTask 零占用(消除 E-02 头阻塞)。副作用:buf 指针须保持有效至返回;close / 网络重建 / 陈旧重同步会显式取消并通知(NET_ERR_CONNRESET 等),调用方永不挂死
  • recv 语义主流化:对齐 POSIX/Python——模式归使用者、超时归调用方、API 不强加默认预算。net_recv() 默认无限阻塞(NET_RECV_TIMEOUT_MS 30s 默认已删),唤醒源 = 数据 / EOF / close / 网络重建(CONNRESET),会话死亡由对账① ~400ms 判死兜底;非阻塞具名入口 net_recv_nb()(原 NET_MSG_DONTWAIT 路径转正),模式属性 net_setblocking(fd, on) 挂 socket 控制块;超时首选 net_select(...),net_recv_to(...) 保留为显式预算便捷入口。内部看门狗降级为 tripwire:挂起超 60s 只 [ERR] 留痕一条(完成逻辑缺位探针),不再伪造 TIMEDOUT 返回值。⚠ 文档契约(嵌入式特有):IWDG required_mask 内任务禁止无限阻塞 recv——必须 net_select 超时或 net_recv_to 显式预算("超时归使用者"对受监控任务是义务不是选项)
  • 阻塞 send 不独占 netTask(挂起 send 模型):net_send() 快路径有界自旋等发送缓冲腾出(预算 NET_SEND_SPIN_MS = 2ms;健康链路实测块间等待 med 219µs ≪ 2ms,故正常发送不会触发挂起),超预算即登记挂起 send 返回;推进由 net_send_pending_tick() 按主循环节拍分块完成(①断连 ②推进 ③写完 ④无进展超时),调用方睡任务通知——netTask 最坏停滞由 NET_SEND_TIMEOUT_MS(1s)压到 2ms。返回值恒为已写入字节数:= len 全部写入,< len 为短写,调用方必须检查并重试剩余(断连/超时/取消均以已发字节数结束,不会伪造全部成功)。停滞计时按无进展刷新(每次成功写块重置),大文件传输不会被误判超时
  • 队列阻塞语义:只有 osMessageQueuePut 失败(8 长度固定队列,极端情形)时公共 API 阻塞;recv / send 挂起等待不计入队列占用(完成前队列中无消息滞留)
  • 哪些操作要求 netTask 已运行:net_listen() / net_accept() / net_connect() / net_send() / net_recv() / net_close() 全经消息队列 → 只能在就绪屏障后的任务调用;net_socket() / net_bind() 为直接调用,但同样建议屏障后;net_listener_armed() / net_can_read() 是纯 RAM 读控制块(§10.1),不入队、不等回执,任意任务可安全周期调用(用途 = 门控,把"无谓的 RPC"降为 0)

3.6 芯片/本层状态对账与重监听自愈(TRAP-NET-09/-17/-18)

单连接模型下"监听 Socket = 数据通道",多种芯片行为会造成芯片侧与本层状态失同步,纯事件(中断)驱动无法自愈。当前有四层自愈(周期对账与监听重建合并在单一 100ms net_reconcile_tick),全在 netTask 内、全非阻塞、不冻结主循环:

机制 触发 动作 关键参数
auto_relisten 数据通道正常 EOF / DISCONNECT 重开 + 重新 LISTEN,回 LISTENING;应用层无需 close/重 listen net_listen() 置位;net_close() 清
D10 非阻塞重监听重试 重开失败(TRAP-NET-09:芯片 close 后需沉降) 单次尝试失败即登记待重试,每轮到点重试;超时限放弃并记 relisten give up 重试间隔 30ms / 时限 2s(自首次尝试);同 fd 有在途挂起 recv / send 时跳过(挂起保险,防毁在途连接 / 截断在途传输)
TRAP-NET-17 周期对账 + 监听重建(合一 net_reconcile_tick;对账①②⑤ + 原 pump③④/U11) 芯片 CONNECT/DISCONNECT 事件未收到(快速复连,读清消费竞态——E14 复核:非芯片侧丢失);静默回收 Socket(peer FIN,无 DISCONNECT 事件);监听型 fd 芯片监听失位 / 本层状态滞后 每 100ms 对在用 socket 读驱动状态缓存(ch395f_sock_state_cached,唯一刷新点 = net_poll 每轮全量刷新,E2 实测单次回读 18.1µs),修正五类失同步 + 一类被动重同步:① 本层 ESTABLISHED 而芯片已断 → 按断开处理(只置事实);② 本层 LISTENING 而芯片已握手 → 补 accept(N-30 幽灵甄别);③ 本层 LISTENING 而芯片监听失位 → 重建(net_listener_request);④ 本层 CLOSED(监听型 fd)而芯片不在 LISTEN → 重建;⑤ 本层 ESTABLISHED 而芯片已静默回收回 LISTEN(0x05/0x01)→ RESYNC 语义只修本层、不发芯片命令(原手动 API net_resync_listen 的语义并入);U11 本层 CLOSED 而芯片正在 LISTEN → 只做状态重同步、不发芯片命令 周期 100ms;分级去抖:②=1 轮、③④⑤=2 轮、①=4 轮(M3 让位窗 400ms,勿改小)+ 动手后 1s 冷却(防"刚 open 未生效"瞬态重建风暴;U11 被动重同步不去抖、不占冷却);其它位置只置事实、不做重建动作
TRAP-NET-18 排渣隔离 异常释放(芯片 TIMEOUT,如客户端中止上传):在途数据落进 RX 缓冲,直接复监听 → 下条连接把 ~2048B 残渣当自己的数据读走(上传静默截断);正常 FIN 释放亦有残留 DISCONNECT 收尾问题 复监听前沉降/隔离:持续排空 RX 残渣 + 主动 close + 排空中断,等芯片回 CLOSED/TCP_CLOSED 才 open+listen;到期强制放行 沉降等级(net_listener_settle):异常释放 QUARANTINE 400ms / 正常释放 GRACEFUL 100ms(2026-09-24 起正常 FIN 亦需沉降);上限 800ms 强制放行(必须 < 2s 时限,否则监听被放弃)

行为结论:监听器恢复从"一次性事件"变为"登记 → 到点重试"过程:

  • 正常断连:通常一次重开即成;失败时 ~30ms 量级恢复,最迟 ~1s(2s 后放弃,错误日志可见)。恢复窗内该端口拒绝新连接 → 调用方重连逻辑须容忍 ~1s 中断。
  • 快速复连不再"监听死亡"(TRAP-NET-17):对账保证失同步按分级去抖在 100~400ms 内修正(②1 轮 / ③④⑤2 轮 / ①4 轮,100ms/轮;2026-09-23 生产路径:待机后 UI :7003 快速复连 10/10;FTP :21 100ms/0ms/600ms 间隔各 50/50;零重建风暴)。
  • 中止复连首上传不再被静默截断(TRAP-NET-18):RST/FIN 中止后首笔复连上传完整通过(RST 10/10、FIN 5/5,SHA-256 全匹配)。详见 docs/问题记录/CH395F_Trap_Records.md TRAP-NET-17/-18。

4. 快速开始

4.1 包含头文件

#include "net_socket.h"
#include "net_select.h"

4.2 初始化网络

int main(void)
{
    /* 初始化网络子系统
     * 参数:IP地址, 子网掩码, 网关, MAC(NULL 或全零 = 芯片内置 MAC)
     * 传入 NULL 使用 DHCP 自动获取
      * 本项目中由 netTask 调用(网口配置已由 defaultTask 在就绪屏障前
      * 加载进 RAM 副本,netTask 只读副本组网)
     */
    int ret = net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL);
    if (ret != 0) {
        printf("网络初始化失败!\r\n");
        return -1;
    }

    /* 其他初始化... */

    while (1)
    {
        net_poll();  /* 必须在主循环中调用 */
        /* 其他任务... */
    }
}

4.3 主循环要求

重要:本项目使用 FreeRTOS 多任务架构,net_poll() 和 net_process_messages() 均在 netTask(FreeRTOS 线程)中定期调用,而非主循环。

  • 主循环:系统初始化完成后进入空循环,等待 osKernelStart() 启动调度器
    • netTask:在任务函数中持续运行,每 1ms 调用一次 net_poll() + net_process_messages(),负责:
    • 轮询 CH395F INT# 引脚电平(GPIO 轮询,无需 EXTI 中断)
    • 通过 GET_GLOB_INT_STATUS_ALL(2字节版)读取所有 8 个 Socket 的中断状态
    • 更新所有 Socket 的连接状态(CONNECT / DISCONNECT / RECV / TIMEOUT)
    • 触发事件回调
    • 处理数据接收
    • net_process_messages() 每轮先跑 4 个非阻塞检查再取队列:D10 重监听重试(断连后监听器恢复)→ 挂起 recv 完成检查(阻塞 recv 唤醒,E-02 修复)→ 挂起 send 推进(阻塞 send 唤醒)→ TRAP-NET-17 芯片/本层对账 + 监听重建合一 reconciler(100ms 周期,读驱动状态缓存,事件未收到/静默回收/监听失位自愈,§3.6)
  • 监听 Socket 建立:由就绪屏障后的服务任务(timeSyncTask / ftpTask / uiTask)经队列 API 建立(net_socket → net_bind → net_listen);断连后 net 层自动重监听(§3.6),服务层只需循环 net_accept,无需维护重监听逻辑
int main(void)
{
    /* 外设初始化... */

    /* BSD Socket API 初始化 */
    net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL);

    /* 创建 Socket、绑定、监听... */

    osKernelInitialize();
    MX_FREERTOS_Init();   /* 创建 netTask 等 FreeRTOS 对象 */
    osKernelStart();      /* 启动调度器 */

    while (1) { }         /* 空循环,控制流已交给调度器 */
}

/* netTask 中(freertos.c): */
void StartNetTask(void *argument)
{
    for (;;)
    {
        net_poll();           /* CH395F 中断轮询 + Socket 状态更新 */
        net_process_messages(); /* 处理其他任务发送的网络操作请求 */

        /* 应用层网络业务逻辑... */

        osDelay(1);   /* 1ms 周期(tick = 1ms) */
    }
}

5. TCP 编程

5.1 TCP Server(单连接)

单连接模式下,1 个 Socket 既做监听又做数据通信:

#include "net_socket.h"

void tcp_server_single(void)
{
    int listen_sock;
    struct net_sockaddr_in addr;
    struct net_sockaddr_in client_addr;
    int client_addr_len;
    char buf[256];
    int len;

    /* 1. 创建 Socket */
    listen_sock = net_socket(AF_INET, SOCK_STREAM);
    if (listen_sock < 0) {
        printf("创建 Socket 失败\r\n");
        return;
    }

    /* 2. 绑定地址和端口 */
    addr.sin_family = AF_INET;
    addr.sin_port = net_htons(8080);  /* 端口号 8080 */
    addr.sin_addr.s_addr = net_inet_addr("0.0.0.0");  /* 监听所有网卡 */

    if (net_bind(listen_sock, (struct net_sockaddr *)&addr, sizeof(addr)) != 0) {
        printf("绑定失败\r\n");
        net_close(listen_sock);
        return;
    }

    /* 3. 开始监听 */
    if (net_listen(listen_sock) != 0) {
        printf("监听失败\r\n");
        net_close(listen_sock);
        return;
    }

    printf("TCP Server 启动,监听端口 8080\r\n");

    /* 4. 主循环 */
    while (1)
    {
        net_poll();

        /* 尝试接受新连接 */
        client_addr_len = sizeof(client_addr);
        int client_sock = net_accept(listen_sock,
            (struct net_sockaddr *)&client_addr, &client_addr_len);

        if (client_sock >= 0)
        {
            char ip_str[16];
            printf("新连接: %s:%d\r\n",
                net_inet_ntoa(client_addr.sin_addr.s_addr, ip_str),
                net_ntohs(client_addr.sin_port));

            /* 接收和发送数据(单连接模式:client_sock 即 listen_sock 自身) */
            while (1)
            {
                len = net_recv(client_sock, buf, sizeof(buf) - 1, 0);
                if (len <= 0) {
                    /* 对端关闭:net 层已在 EOF 时自动重监听,
                       直接 break,外层 net_accept 会接下一个连接 */
                    printf("连接断开,net 层自动重监听\r\n");
                    break;
                }

                buf[len] = '\0';
                printf("收到: %s\r\n", buf);

                /* 回显数据 */
                net_send(client_sock, buf, len, 0);
            }

            /* 单连接模式不要在此 net_close(client_sock):它即监听 Socket,
               一旦关闭需重新 net_socket+net_bind+net_listen;重监听已由 net 层自动完成 */
        }
    }
}

5.2 TCP Client

#include "net_socket.h"

void tcp_client(void)
{
    int sock;
    struct net_sockaddr_in addr;
    char buf[256];
    int len;

    /* 1. 创建 Socket */
    sock = net_socket(AF_INET, SOCK_STREAM);
    if (sock < 0) {
        printf("创建 Socket 失败\r\n");
        return;
    }

    /* 2. 设置服务器地址 */
    addr.sin_family = AF_INET;
    addr.sin_port = net_htons(8080);
    addr.sin_addr.s_addr = net_inet_addr("192.168.1.200");

    /* 3. 连接服务器 */
    printf("正在连接服务器...\r\n");
    if (net_connect(sock, (struct net_sockaddr *)&addr, sizeof(addr)) != 0) {
        printf("连接失败\r\n");
        net_close(sock);
        return;
    }

    /* 4. 等待连接建立(跨任务只读快照:net_sock_state,
       描述符无效返回 -1) */
    while (1)
    {
        net_poll();

        if (net_sock_state(sock) == NET_SOCK_STATE_ESTABLISHED) {
            printf("连接成功!\r\n");
            break;
        }

        HAL_Delay(10);
    }

    /* 5. 发送数据 */
    const char *msg = "Hello, CH395F!";
    net_send(sock, msg, strlen(msg), 0);
    printf("发送: %s\r\n", msg);

    /* 6. 接收数据 */
    len = net_recv(sock, buf, sizeof(buf) - 1, 0);
    if (len > 0) {
        buf[len] = '\0';
        printf("收到: %s\r\n", buf);
    }

    /* 7. 关闭连接 */
    net_close(sock);
}

6. UDP 编程

本项目不使用 UDP(2026-10-15 定案):UDP API 保留为 BSD socket 完整性,仅单任务独占期可用 (net_sendto / net_recvfrom 直碰 SPI、不经队列,见 §2 三例外);本平台无 UDP 消费者。

6.1 UDP Client

#include "net_socket.h"

void udp_client(void)
{
    int sock;
    struct net_sockaddr_in local_addr;
    struct net_sockaddr_in dest_addr;
    char buf[256];
    int len;

    /* 1. 创建 UDP Socket */
    sock = net_socket(AF_INET, SOCK_DGRAM);
    if (sock < 0) {
        printf("创建 Socket 失败\r\n");
        return;
    }

    /* 2. 绑定本地端口(可选) */
    local_addr.sin_family = AF_INET;
    local_addr.sin_port = net_htons(12345);
    local_addr.sin_addr.s_addr = net_inet_addr("0.0.0.0");

    net_bind(sock, (struct net_sockaddr *)&local_addr, sizeof(local_addr));

    /* 3. 设置目标地址 */
    dest_addr.sin_family = AF_INET;
    dest_addr.sin_port = net_htons(8888);
    dest_addr.sin_addr.s_addr = net_inet_addr("192.168.1.200");

    /* 4. 发送数据 */
    const char *msg = "UDP Hello!";
    net_sendto(sock, msg, strlen(msg), 0,
        (struct net_sockaddr *)&dest_addr, sizeof(dest_addr));
    printf("发送: %s\r\n", msg);

    /* 5. 接收数据 */
    struct net_sockaddr_in src_addr;
    int src_addr_len = sizeof(src_addr);

    len = net_recvfrom(sock, buf, sizeof(buf) - 1, 0,
        (struct net_sockaddr *)&src_addr, &src_addr_len);

    if (len > 0) {
        buf[len] = '\0';
        char ip_str[16];
        printf("收到: %s (from %s:%d)\r\n", buf,
            net_inet_ntoa(src_addr.sin_addr.s_addr, ip_str),
            net_ntohs(src_addr.sin_port));
    }

    net_close(sock);
}

6.2 UDP Server

#include "net_socket.h"

void udp_server(void)
{
    int sock;
    struct net_sockaddr_in addr;
    char buf[256];

    /* 1. 创建 UDP Socket */
    sock = net_socket(AF_INET, SOCK_DGRAM);
    if (sock < 0) {
        printf("创建 Socket 失败\r\n");
        return;
    }

    /* 2. 绑定本地端口 */
    addr.sin_family = AF_INET;
    addr.sin_port = net_htons(8888);
    addr.sin_addr.s_addr = net_inet_addr("0.0.0.0");

    if (net_bind(sock, (struct net_sockaddr *)&addr, sizeof(addr)) != 0) {
        printf("绑定失败\r\n");
        net_close(sock);
        return;
    }

    printf("UDP Server 启动,监听端口 8888\r\n");

    /* 3. 循环接收数据 */
    while (1)
    {
        net_poll();

        struct net_sockaddr_in src_addr;
        int src_addr_len = sizeof(src_addr);

        int len = net_recvfrom(sock, buf, sizeof(buf) - 1, 0,
            (struct net_sockaddr *)&src_addr, &src_addr_len);

        if (len > 0) {
            buf[len] = '\0';
            char ip_str[16];
            printf("收到: %s (from %s:%d)\r\n", buf,
                net_inet_ntoa(src_addr.sin_addr.s_addr, ip_str),
                net_ntohs(src_addr.sin_port));

            /* 回显数据 */
            net_sendto(sock, buf, len, 0,
                (struct net_sockaddr *)&src_addr, src_addr_len);
        }
    }
}

7. I/O 多路复用

7.1 select 使用

net_select() 可以同时监控多个 Socket 的读写事件:

#include "net_select.h"

void select_example(void)
{
    int sock1, sock2;
    net_fd_set readfds;
    struct net_timeval timeout;

    /* 创建并连接两个 Socket ... */

    while (1)
    {
        NET_FD_ZERO(&readfds);
        NET_FD_SET(sock1, &readfds);
        NET_FD_SET(sock2, &readfds);

        timeout.tv_sec = 0;
        timeout.tv_usec = 500000;  /* 500ms 超时 */

        int ret = net_select(sock2 + 1, &readfds, NULL, NULL, &timeout);

        if (ret > 0) {
            if (NET_FD_ISSET(sock1, &readfds)) {
                char buf[128];
                int len = net_recv(sock1, buf, sizeof(buf), 0);
                /* 处理数据... */
            }

            if (NET_FD_ISSET(sock2, &readfds)) {
                char buf[128];
                int len = net_recv(sock2, buf, sizeof(buf), 0);
                /* 处理数据... */
            }
        } else if (ret == 0) {
            /* 超时 */
        }
    }
}

7.2 poll 使用

net_poll_events() 使用 pollfd 结构体数组:

#include "net_select.h"

void poll_example(void)
{
    net_pollfd fds[2];

    fds[0].fd = sock1;
    fds[0].events = NET_POLLIN;

    fds[1].fd = sock2;
    fds[1].events = NET_POLLIN;

    while (1)
    {
        int ret = net_poll_events(fds, 2, 500);  /* 500ms 超时 */

        if (ret > 0) {
            if (fds[0].revents & NET_POLLIN) {
                char buf[128];
                int len = net_recv(fds[0].fd, buf, sizeof(buf), 0);
                /* 处理数据... */
            }

            if (fds[1].revents & NET_POLLIN) {
                char buf[128];
                int len = net_recv(fds[1].fd, buf, sizeof(buf), 0);
                /* 处理数据... */
            }
        }
    }
}

7.3 事件掩码说明

事件 说明
NET_POLLIN 有数据可读,或有新连接到达(监听 Socket)
NET_POLLOUT 发送缓冲区有空间,可以写入数据
NET_POLLERR 发生错误
NET_POLLHUP 连接挂起(对端关闭)
NET_POLLNVAL 无效的文件描述符

8. 事件回调机制

可以通过注册回调函数,异步获取 Socket 事件通知:

8.1 定义回调函数

#include "net_socket.h"

void my_event_callback(int sockfd, net_event_t event, void *arg)
{
    switch (event)
    {
        case NET_EVENT_CONNECTED:
            printf("Socket %d: 连接建立\r\n", sockfd);
            break;

        case NET_EVENT_DISCONNECTED:
            printf("Socket %d: 连接断开\r\n", sockfd);
            break;

        case NET_EVENT_DATA_RECEIVED:
            printf("Socket %d: 收到数据\r\n", sockfd);
            /* 注意:这里只是通知,需要调用 net_recv() 读取 */
            break;

        case NET_EVENT_SEND_COMPLETE:
            printf("Socket %d: 发送完成\r\n", sockfd);
            break;

        case NET_EVENT_TIMEOUT:
            printf("Socket %d: 操作超时\r\n", sockfd);
            break;

        case NET_EVENT_ERROR:
            printf("Socket %d: 发生错误\r\n", sockfd);
            break;
    }
}

8.2 注册回调

void event_callback_example(void)
{
    int sock;

    /* 创建 Socket ... */

    /* 注册事件回调 */
    net_set_event_cb(sock, my_event_callback, NULL);

    /* 主循环 */
    while (1)
    {
        net_poll();
    }
}

9. 字节序处理

网络协议使用大端序(网络字节序),而 STM32 使用小端序(主机字节序)。发送和接收数据时需要进行字节序转换。

9.1 端口号转换

uint16_t port = 8080;

/* 主机序 -> 网络序(发送前转换) */
uint16_t net_port = net_htons(port);

/* 网络序 -> 主机序(接收后转换) */
uint16_t host_port = net_ntohs(net_port);

9.2 IP 地址转换

/* 字符串转网络序 IP */
uint32_t ip = net_inet_addr("192.168.1.100");

/* 网络序 IP 转字符串 */
char ip_str[16];
net_inet_ntoa(ip, ip_str);  /* ip_str = "192.168.1.100" */

9.3 填充地址结构体

struct net_sockaddr_in addr;

addr.sin_family = AF_INET;
addr.sin_port = net_htons(8080);                    /* 端口转换 */
addr.sin_addr.s_addr = net_inet_addr("192.168.1.100");  /* IP 转换 */

/* 如果要绑定任意地址 */
addr.sin_addr.s_addr = net_inet_addr("0.0.0.0");
/* 或者直接 */
addr.sin_addr.s_addr = INADDR_ANY;

9.4 读取地址信息

struct net_sockaddr_in client_addr;
int addr_len = sizeof(client_addr);

int client_sock = net_accept(listen_sock,
    (struct net_sockaddr *)&client_addr, &addr_len);

if (client_sock >= 0)
{
    /* 转换端口到主机序 */
    uint16_t port = net_ntohs(client_addr.sin_port);

    /* 转换 IP 到字符串 */
    char ip_str[16];
    net_inet_ntoa(client_addr.sin_addr.s_addr, ip_str);

    printf("客户端: %s:%d\r\n", ip_str, port);
}

10. API 参考

10.1 核心 API

net_init - 网络初始化

int net_init(const char *ip, const char *mask, const char *gateway, const uint8_t *mac);
参数 说明
ip IP 地址字符串,NULL 使用 DHCP
mask 子网掩码字符串,NULL 使用默认
gateway 网关地址字符串,NULL 使用默认
mac MAC 地址(6 字节);NULL 或全零 = 芯片内置 MAC(ch395f_init() 后设置)
返回值 0 成功,-1 失败

net_get_generation / net_generation_inc - 网络代次号

uint32_t net_get_generation(void);
void net_generation_inc(void);
函数 说明
net_get_generation 获取当前网络代次号(首次 net_init 成功前为 0,每次网络重建 +1)
net_generation_inc 代次号加 1(netTask 重建成功、网络已可用后调用)

用途:长驻 TCP 服务(FTP :21 / 对时 :8888 / 远程显示 :7003)在监听循环中检测代次号变化后主动重建监听 Socket,网口配置立即生效后可自动在新 IP 上恢复(重建会断开既有连接,属预期行为)。详见 docs/子系统设计/网络配置说明.md §4。

net_poll - 状态轮询

int net_poll(void);
参数 说明
返回值 有事件发生的 socket 数量

注意:必须在主循环中定期调用。

net_socket - 创建 Socket

int net_socket(int domain, int type);
参数 说明
domain 地址族,仅支持 AF_INET
type SOCK_STREAM(TCP) 或 SOCK_DGRAM(UDP)
返回值 socket 描述符 (0~7),失败返回 -1

net_bind - 绑定地址

int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen);
参数 说明
sockfd socket 描述符
addr 地址结构体指针
addrlen 地址结构体长度
返回值 0 成功,-1 失败

net_listen - 开始监听

int net_listen(int sockfd);
参数 说明
sockfd socket 描述符
返回值 0 成功,-1 失败

自动重监听:net_listen() 对 TCP 监听 Socket 置 auto_relisten = 1。连接结束后 net 层自动回到 LISTENING,应用层无需手动 net_close() + net_listen()。

net_accept - 接受连接

int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen);
参数 说明
sockfd 监听 socket 描述符
addr 输出:客户端地址
addrlen 输入输出:地址长度
返回值 新连接描述符(单连接模式下 == 监听 sockfd),无连接时返回 -1(可循环重试)

单连接模式:监听 Socket 在 CONNECT 后自身即数据通道(§5.1)。net_accept 失败时若状态为 CLOSED 且 auto_relisten 开启,内部触发非阻塞重监听(失败交重试轮询,§3.6)。事件未收到也不会让监听死亡——合一对账(100ms 周期,§3.6)在 ~200ms 内修正"本层 LISTENING 而芯片监听消失"等失同步。

net_listener_armed / net_can_read - 就绪门控(纯 RAM 读,不走消息队列)

int net_listener_armed(int sockfd); /* 1 = 监听器在服役,值得发起 accept;0 = 跳过本轮 */
int net_can_read(int sockfd);       /* 1 = 应发一次 recv;0 = 可跳过本轮 */
项 说明
代价 零 RPC:只读 Socket 控制块(net_get_sock()),不入队、不等 netTask 回执 → 任意任务上下文可安全周期调用,不被 netTask 忙碌程度拖住
net_listener_armed 判据 ESTABLISHED(CONNECT 已处理、待应用 accept——单连接模型的"连接已到"电平,漏放行则任务周期慢的消费者永不 accept),或 LISTENING,或 CLOSED + auto_relisten 且 RX 有残留(closed-with-data 救回窗)——表达"这个监听器还在服役、这一轮值不值得发起 accept",而非"一定有连接"。(前身 net_can_accept() 只认 ESTABLISHED,会把监听服役窗口全挡掉、丢 CONNECT 探针从不运行——已退役,其 ESTABLISHED 放行态并入本函数,勿再引入)
net_can_read 判据 ESTABLISHED → 看 readable;非 ESTABLISHED 一律返 1(必须让上层 recv 一次拿到 EOF/错误并复位自身状态,门掉它会把"连接已消失"永久藏住);描述符失效也返 1(让 recv 报 BADF 走既有错误分支)
用途 门控:无活时跳过 net_accept()/net_recv()——两者内部都是 osMessageQueuePut(osWaitForever) + xTaskNotifyWait(portMAX_DELAY),均无上界(队列深 8 且与 FTP/timeSync 共享)
⚠ 不可作为唯一判据 readable 只由 RECV_OK 中断置位,而 §3.6 的对账不含"芯片 RX 非空但 readable=0" → 事件未收到时 net_can_read() 恒返 0。调用方必须保留低频兜底 recv(ui_net_sink.c 每 32 轮无条件收一次,教训见 docs/子系统设计/看门狗设计说明.md §11.5 第 1 条)
竞态 netTask 是唯一写者,单字读;最坏"晚一轮发现"(netTask 每 1 ms 就 net_poll())或"扑空"(RPC 返回 NET_ERR_WOULDBLOCK),均无副作用
模拟器端 net_socket_sim.c 同名实现恒返回 1——Winsock 本就非阻塞、无 RPC 成本,恒 1 使门控在模拟器上逐位等价于改前行为

net_connect - 连接服务器

int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen);
参数 说明
sockfd socket 描述符
addr 服务器地址
addrlen 地址长度
返回值 0 成功,-1 失败

net_send - 发送数据

int net_send(int sockfd, const void *buf, int len, int flags);
参数 说明
sockfd socket 描述符
buf 数据缓冲区
len 数据长度
flags 通常为 0
返回值 实际发送字节数,失败返回 -1

net_recv - 接收数据(默认无限阻塞)

int net_recv(int sockfd, void *buf, int len, int flags);
参数 说明
sockfd socket 描述符
buf 接收缓冲区(须保持有效至调用返回)
len 缓冲区大小
flags 0=按 socket 阻塞属性等待,NET_MSG_DONTWAIT=单次强制非阻塞
返回值 实际接收字节数 (>0);0=对端关闭(net 层随即自动重监听);-1=错误(非阻塞无数据=NET_ERR_WOULDBLOCK;close/网络重建/陈旧重同步取消=NET_ERR_CONNRESET;阻塞形态无"预算内无数据"一档——默认无限等待,超时是调用方策略)

语义主流化:对齐 POSIX/Python——net_recv() 默认无限阻塞(旧 NET_RECV_TIMEOUT_MS 30s 默认预算已删,API 不再强加隐藏值)。唤醒源 = 数据 / EOF / close / 网络重建(NET_ERR_CONNRESET);会话死亡由对账①在 ~400ms 内判死兜底(§3.6),不存在"对端消失后永久挂死"窗口。需要"等但不无限等":首选 net_select(r,w,e,timeout_ms)(§7.1),或 net_recv_to() 显式预算。

挂起 recv 模型:无数据时阻塞接收不占 netTask——登记挂起请求(每 fd 至多一个,禁止覆盖:覆盖会丢失前调用方通知 = 永久挂死)后立即返回;完成按主循环节拍检查(①数据 ②到点[仅显式预算] ③断连)并经任务通知唤醒调用方。等待期间调用方在自己上下文睡通知,netTask 全程零占用(消除 E-02 头阻塞)。内部 tripwire(原看门狗降级):挂起超 60s 只 [ERR] 留痕一条("完成逻辑缺位"探针),绝不伪造 TIMEDOUT 返回值。

⚠ 文档契约(嵌入式特有,Python 没有):IWDG required_mask 内任务(当前产品掩码 0x1F7:default/net/ftp/ui/timeSync/adc/iec104/protect)禁止无限阻塞 recv——必须 net_select 超时或 net_recv_to 显式预算并周期喂心跳,否则"对端连上不发数据"这种完全正常的场景 = IWDG 复位。

net_recv_nb - 接收数据(非阻塞具名入口)

int net_recv_nb(int sockfd, void *buf, int len);
参数 说明
sockfd / buf / len 同 net_recv
返回值 >0 字节数;0=对端关闭;-1=无数据(NET_ERR_WOULDBLOCK)或真错误(net_get_errno 可查)

原 NET_MSG_DONTWAIT 路径转正:等价 net_recv(sockfd, buf, len, NET_MSG_DONTWAIT),单次强制非阻塞、不改变 socket 的 blocking 属性。受 IWDG 监控 / 有周期职责的任务的推荐默认入口(生产用户:ui_net_sink :7003、iec104_serv :2404)。

net_setblocking - 设置阻塞模式属性

int net_setblocking(int sockfd, int on);
参数 说明
sockfd socket 描述符
on 1=阻塞(net_socket() 默认);0=非阻塞
返回值 0 成功;-1 失败(描述符无效/未占用)

模式挂 socket 控制块属性(对齐 BSD O_NONBLOCK / Python setblocking):非阻塞模式下 net_recv() 无数据立即返回 NET_ERR_WOULDBLOCK(EOF 判定不变:CLOSED/session_dead 无数据仍返回 0 并触发自动重监听)。与超时预算正交——模式是粘滞属性,单次预算用 net_recv_to / net_select。直写控制块单字节(先例 net_set_event_cb),任意上下文可调。

net_recv_to - 接收数据(调用方指定超时,显式预算分片版)

int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms);
参数 说明
sockfd / buf / len 同 net_recv
timeout_ms 本片等待预算(ms,必须 >0;0 = net_recv 的无限等待语义,恰与本接口"有界"存在理由相反,直接拒 NET_ERR_INVAL——本接口是显式预算入口,没有"默认 30s"隐藏值)
返回值 >0=字节数(允许部分读,TCP 分段);0=对端关闭(排空残留后);-1=真错误(net_get_errno 可查);-2=本片预算内无数据(不是 errno,不得依赖 errno 判超时——s_net_errno 是跨任务共享全局量)

调用方累计自身总预算:见到 -2 发起下一片,直到收齐 / EOF / 总预算耗尽。生产用途:lftpd 数据行读、timeSync 帧读(E-02 头阻塞修复,完整例 §11.2);受 IWDG 监控任务的长等待必须走本接口(或 net_select)+ 每片喂心跳。

net_sendto - 发送 UDP 数据

int net_sendto(int sockfd, const void *buf, int len, int flags,
               const struct net_sockaddr *dest_addr, int addrlen);

net_recvfrom - 接收 UDP 数据

int net_recvfrom(int sockfd, void *buf, int len, int flags,
                 struct net_sockaddr *src_addr, int *addrlen);

net_close - 关闭 Socket

int net_close(int sockfd);

释放 Socket 控制块,TCP 先断开连接。⚠ 单连接模型(MCU 端 CH395F)不得对监听 fd 调本 API——连接与监听同一 fd,close 会连监听一起毁掉(§12.4);EOF 时 net 层已 auto_relisten,应用层不得在 EOF 时 close 该 fd。"放弃会话但保监听"用下面的 net_drop_conn()。

net_drop_conn - 主动拆会话、保留监听(E-02 收口)

int net_drop_conn(int sockfd);

应用放弃会话(对时 30s 预算耗尽、协议超时)时主动拆掉当前 TCP 会话并保留监听——单连接模型下挂起但 TCP 仍活的客户端会永久占住监听(PC 内核自动 ACK keepalive,芯片僵尸判不死,定案见 docs/网络/网络自愈架构设计.md §5.2 E13),而应用又不得 net_close 监听 fd(毁监听);本 API 补上"拆连接不拆监听"的应用侧形态。

  • netTask 侧:先结束本 fd 挂起 recv(排空/取消 + 必通知),再 sock_reset QUARANTINE(FIN + 排渣 + 排空事件 + 等 byte0=00H,TRAP-NET-18 异常释放语义,400ms 底);本层收口 CLOSED;auto_relisten fd 随即 net_listener_request 登记重监听(下一轮 net_listener_tick 执行,30ms 节奏重试 + reconciler 形态④兜底)
  • 幂等:无活动会话(非 ESTABLISHED/CONNECTING)直接返 0,不动监听
  • EOF 路径不要调:EOF 时 net 层 auto_relisten 已接管(§12.4),无需也不应再拆
  • 经消息队列委托 netTask(同其它 net_* API,非 netTask 上下文调用);等待期 netTask 有界占用 ~400-600ms
  • 模拟器(net_socket_sim.c,数据 fd ≠ 监听 fd)实现为关数据连接、监听常驻
  • 台架定案(2026-10-13,E-02):CH395F close 无 RST 语义——对端内核存活但不 close 时芯片停 FIN_WAIT_2 长尾,监听恢复受限于对端 close;对端配合则 TIME_WAIT 过渡 150ms → reconciler 形态④ relisten → 新连接接入。不配合场景兜底 = 芯片 TIMEOUT/keepalive 拆僵尸(E13 定案 3090s)。详见 docs/问题记录/CH395F_Trap_Records.md TRAP-NET-07

自愈与查询 API

net_sock_state — 查询 Socket 状态(只读快照)

int net_sock_state(int sockfd);

跨任务只读快照(单字节读);描述符无效/未占用返回 -1;值可能滞后 netTask 最新状态。用于状态轮询(客户端等 ESTABLISHED,§5.2),不替代 accept/connect。

net_resync_listen / net_relisten — 已退役(勿再引入)

两者曾是"应用层手动自愈"入口:net_resync_listen() 只修 MCU 侧状态(D14 静默回收),net_relisten() 无条件芯片侧 open+listen。现状:

  • 静默回收由对账⑤自动认领(§3.6:本层 ESTABLISHED 而芯片 0x05/0x01 → RESYNC 语义,≤400ms 去抖 + 1s 冷却,纪律"不发芯片命令"原样保留在认领动作内)——应用无需判定"连接是否已死",也就消除了"误判活连接调 resync 把活连接标成监听态"的 footgun。
  • 监听重建由 reconciler 形态③④(100ms 周期,唯一重建入口 = net_reconcile_tick)/ D10(30ms 重试)单一所有者负责——net_relisten() 对健康监听器反而是 footgun(无条件 open→USE→close 自毁 ~1s,原注释自认)。

生产调用者本就为零(仅板级测试 Phase 13,已改由自动机制接管)。历史实现见 net_socket.c git 历史。

net_getsockname — 获取 Socket 本地地址(IP+端口)

int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen);

Socket 已创建或已绑定时可用;port 为 0(动态分配)时返回已分配的动态端口(49152 起)。

10.2 消息队列 API

net_process_messages - 处理消息队列

void net_process_messages(void);

处理其他任务投递的请求(send/recv/connect/accept/close/drop_conn)。每次调用先跑 4 个非阻塞 tick,再取队列消息(0 超时出队):

  1. D10 重监听重试(net_listener_tick):断连后监听器恢复的到点重试(30ms 间隔 / 2s 时限,§3.6)
  2. 挂起 recv 完成检查(net_pending_tick):按 ①数据 ②到点(仅显式预算) ③断连 检查每个挂起请求并通知调用方;④ 挂起超时留痕探针(编译期选项,默认关):挂起超阈值只 [ERR] 留痕一次,不强制结束
  3. 挂起 send 推进(net_send_pending_tick):按 ①断连 ②推进(每轮至多一块 NET_SEND_CHUNK_MAX) ③写完 ④无进展超时 推进挂起 send 并通知调用方(结果恒为已发字节数,短写由调用方检查)
  4. TRAP-NET-17 芯片/本层对账 + 监听重建(合一 net_reconcile_tick):100ms 周期,判据读驱动状态缓存(ch395f_sock_state_cached),修正五类失同步(①②③④⑤)+ U11 被动重同步——原 200ms 对账与 100ms 监听器 pump 已合并为单一 tick,唯一重建入口=形态③④(§3.6)

net_msg_t.timeout_ms:显式预算(net_recv_to()),0=无限等待(无 NET_RECV_TIMEOUT_MS 30s 默认预算——超时归调用方)。消息类型 7 种(send/recv/close/connect/listen/accept/drop_conn)——自愈类消息 NET_MSG_RESYNC_LISTEN / NET_MSG_RELISTEN 已随 API 退役删除。

net_send_sock - 发送数据(内部版本)

int net_send_sock(net_sock_t *p_sock, const void *buf, int len);

直接操作 Socket 控制块进行发送,不经过消息队列。只能在 netTask 中调用,用于 netTask 内的网络业务逻辑。

net_recv_sock - 接收数据(内部版本)

int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags);

直接操作 Socket 控制块进行接收,不经过消息队列。只能在 netTask 中调用。

net_close_sock - 关闭 Socket(内部版本)

int net_close_sock(net_sock_t *p_sock);

直接操作 Socket 控制块关闭连接,不经过消息队列。只能在 netTask 中调用。

net_listen_locked - 开始监听(内部版本)

int net_listen_locked(net_sock_t *p_sock);

直接操作 Socket 控制块开始监听,不经过消息队列。只能在 netTask 中调用(供 TCP 服务的代次号重建逻辑使用)。

10.3 辅助 API

net_set_event_cb - 注册事件回调

int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg);

net_get_sock - 获取 Socket 控制块

net_sock_t *net_get_sock(int sockfd);

net_get_errno - 获取错误码

int net_get_errno(void);

10.4 字节序转换

uint16_t net_htons(uint16_t hostshort);  /* 主机序 -> 网络序 */
uint16_t net_ntohs(uint16_t netshort);  /* 网络序 -> 主机序 */
uint32_t net_htonl(uint32_t hostlong);  /* 主机序 -> 网络序 */
uint32_t net_ntohl(uint32_t netlong);   /* 网络序 -> 主机序 */
uint32_t net_inet_addr(const char *cp); /* 字符串 -> 网络序 IP */
char *net_inet_ntoa(uint32_t addr, char *buf); /* 网络序 IP -> 字符串 */

10.5 select/poll

int net_select(int nfds, net_fd_set *readfds, net_fd_set *writefds,
               net_fd_set *exceptfds, net_timeval *timeout);

int net_poll_events(net_pollfd *fds, int nfds, int timeout);

10.6 fd_set 操作宏

NET_FD_ZERO(fdset)           /* 清空集合 */
NET_FD_SET(fd, fdset)        /* 添加 fd */
NET_FD_CLR(fd, fdset)        /* 移除 fd */
NET_FD_ISSET(fd, fdset)      /* 检查 fd 是否在集合中 */

11. 完整示例

11.1 TCP Client 自动重连

#include "net_socket.h"
#include <stdio.h>
#include <string.h>

#define SERVER_IP   "192.168.1.200"
#define SERVER_PORT 8080

void tcp_client_reconnect(void)
{
    int sock = -1;
    struct net_sockaddr_in addr;
    uint32_t last_try = 0;
    uint32_t reconnect_interval = 5000;  /* 5 秒重连间隔 */

    /* 初始化网络 */
    net_init("192.168.1.100", "255.255.255.0", "192.168.1.1", NULL);

    while (1)
    {
        net_poll();

        /* 尝试重连 */
        if (sock < 0 && (HAL_GetTick() - last_try) >= reconnect_interval)
        {
            last_try = HAL_GetTick();

            sock = net_socket(AF_INET, SOCK_STREAM);
            if (sock >= 0) {
                memset(&addr, 0, sizeof(addr));
                addr.sin_family = AF_INET;
                addr.sin_port = net_htons(SERVER_PORT);
                addr.sin_addr.s_addr = net_inet_addr(SERVER_IP);

                if (net_connect(sock, (struct net_sockaddr *)&addr, sizeof(addr)) == 0) {
                    printf("正在连接服务器...\r\n");
                } else {
                    net_close(sock);
                    sock = -1;
                }
            }
        }

        /* 检查连接状态 */
        if (sock >= 0)
        {
            if (net_sock_state(sock) == NET_SOCK_STATE_ESTABLISHED)
            {
                /* 已连接,发送数据 */
                static uint32_t last_send = 0;
                if ((HAL_GetTick() - last_send) >= 1000) {
                    last_send = HAL_GetTick();
                    net_send(sock, "heartbeat", 9, 0);
                }

                /* 检查接收(非阻塞具名入口 net_recv_nb) */
                char buf[128];
                int len = net_recv_nb(sock, buf, sizeof(buf) - 1);
                if (len > 0) {
                    buf[len] = '\0';
                    printf("收到: %s\r\n", buf);
                } else if (len == 0) {
                    /* 连接断开 */
                    printf("连接断开\r\n");
                    net_close(sock);
                    sock = -1;
                }
            }
            else if (net_sock_state(sock) == NET_SOCK_STATE_CLOSED)
            {
                /* 连接失败或断开 */
                printf("连接失败,准备重连...\r\n");
                net_close(sock);
                sock = -1;
            }
        }
    }
}

11.2 分片阻塞读(net_recv_to,timeSync 模式)

长等待 recv 曾独占 netTask(E-02 头阻塞)。挂起 recv 模型下:单次 net_recv_to 单次占用有界(≤ 本片预算)且等待期间 netTask 零占用(挂起模型);调用方累计总预算,收齐协议帧再返回:

#define FRAME_LEN      8      /* "TIME" + uint32 大端 本地时间 */
#define TOTAL_BUDGET   30000U /* 总等待预算(ms) */

static int read_time_frame(int conn_sock, uint8_t buf[FRAME_LEN]) {
    int got = 0;
    TickType_t t0 = xTaskGetTickCount();

    while (got < FRAME_LEN) {
        TickType_t elapsed = (TickType_t)(xTaskGetTickCount() - t0);
        uint32_t rem = (elapsed >= pdMS_TO_TICKS(TOTAL_BUDGET)) ? 0U
                       : (uint32_t)(pdMS_TO_TICKS(TOTAL_BUDGET) - elapsed);
        if (rem == 0U) {
            return -1;                          /* 总预算耗尽 */
        }

        int n = net_recv_to(conn_sock, buf + got, FRAME_LEN - got, rem);
        if (n > 0) {
            got += n;                           /* 允许部分读(TCP 分段) */
        } else if (n == 0) {
            return (got >= FRAME_LEN) ? 0 : -1; /* EOF:收齐成功,否则失败 */
        } else if (n == -2) {
            continue;                           /* 本片超时:累计预算,下片继续 */
        } else {
            return -1;                           /* 真错误(net_get_errno 可查) */
        }
    }
    return 0;
}

约束:buf 须在整个读过程有效(挂起持指针,不拷数据);同一 fd 同一时刻至多一个挂起 recv(覆盖会挂死前调用方);close/网络重建/陈旧重同步会通知(NET_ERR_CONNRESET 等)→ 上述 n<0 分支统一当失败处理。


12. 常见问题

12.1 net_poll() 必须调用吗?

是的,且 net_poll() 与 net_process_messages() 缺一不可:

  • net_poll():GPIO 轮询 INT#,每轮批处理一批中断,更新各 Socket 状态、触发事件回调(RECV 为电平触发,每轮只处理一批,避免饿死主循环)。
  • net_process_messages():除串行处理队列请求外,每轮先跑 4 个非阻塞检查——D10 重监听重试(断连后监听器恢复)→ 挂起 recv 完成检查(阻塞 recv 的唤醒)→ 挂起 send 推进(阻塞 send 的唤醒)→ TRAP-NET-17 芯片/本层对账 + 监听重建合一 reconciler(100ms 周期,读驱动状态缓存,事件未收到/静默回收/监听失位自愈,§3.6)。

建议主循环周期 ≤ 10ms(本项目 1ms)。若 netTask 主循环停摆(断点、任务被挂起),监听器不会恢复、阻塞 recv 不会完成——这是软件侧"监听死机"的最常见原因。

12.2 Socket 数量有限制吗?

是的,CH395F 最多支持 8 个 Socket(索引 0~7)。其中:

  • TCP Server(单连接):1 个监听 Socket 即数据通道
  • TCP Client:最多 8 个
  • UDP:最多 8 个

12.3 发送数据有什么限制?

  • 每次发送的数据长度不能超过 NET_SEND_BUF_SIZE(默认 4096 字节)
  • TCP 需要等待发送缓冲区空闲(send_ready 标志)
  • UDP 单包载荷最大 1472 字节(写入超过此值,CH395F 会自动分片为多个 UDP 包发送)
  • 接收侧:每 Socket RX 缓冲 2KB(4×512B,即芯片接收窗口)、TX FIFO 1KB(2×512B)、MSS=1024。对端常态把 2KB 窗口填满是正常现象,不等于"冻结"(TC-NET-604 结案,见 §12.10)

12.4 如何处理连接断开?

方式一:阻塞接收(无限等待——⚠ 仅限不受 IWDG 监控且无周期职责的任务,§10.1 文档契约)

int len = net_recv(sock, buf, sizeof(buf), 0);
if (len <= 0) {
    /* 连接断开或错误(EOF / CONNRESET / 真错误,net_get_errno 可查) */
    net_close(sock);
}

方式二:非阻塞接收(net_recv_nb)

int len = net_recv_nb(sock, buf, sizeof(buf));
if (len < 0 && net_get_errno() == NET_ERR_WOULDBLOCK) {
    /* 没有数据 */
} else if (len <= 0) {
    /* 连接断开 */
}

⚠ 单连接模型(MCU 端 CH395F)的断开处理:监听 Socket 在 CONNECT 后自身即数据通道 (g_client == g_listen);net_recv() 返回 0(EOF)时 net 层已自动把该 Socket 放回 LISTEN (auto_relisten,BSD 风格"监听常驻")。此时不得再对它调 net_close()——那会把监听一起关掉, 后续连接直接被拒(连接与监听同一 fd,重建还受 TRAP-NET-09 时序限制)。正确写法:

} else if (len == 0) {
    if (client_fd != listen_fd) {
        net_close(client_fd);   /* 多连接后端(PC 模拟器):数据通道独立,需显式关闭 */
    }
    client_fd = -1;             /* 单连接后端:监听已由 net 层恢复,直接等下一次 accept */
}

(2026-09-21 台架实证:ui_net_sink 曾在 EOF 时无条件 net_close,表现为 7003 答完一帧即断连、 端口随后"连接被拒"。)

应用主动放弃会话(非 EOF):若会话是应用侧放弃的(对时 30s 预算耗尽、协议超时)而对端 TCP 仍活,EOF 路径不会发生、挂起客户端会永久占住单连接监听——此时用 net_drop_conn(fd)(§10.1)拆会话保监听,不要 net_close 监听 fd。EOF 路径不要调它(auto_relisten 已接管)。

芯片静默回收(D14,2026-09-22 台架实证):对端发 FIN 后,芯片可能自行把 Socket 回收回 LISTEN 且不发 DISCONNECT 中断 → 本层滞留 ESTABLISHED,应用继续往死连接上读写,新连接的字节被旧会话吞掉。

无需应用处理:对账⑤自动认领此形态(§3.6:2 轮去抖 ≈200ms + 1s 冷却)→ RESYNC 语义只修本层状态 + 取消本 fd 挂起 recv,不发任何芯片命令(发 close/重监听会摧毁芯片侧健康监听器 → 连接永拒,run8 实证 170s 拒绝)。等待期间该端口实际可接新连接(芯片侧已 LISTEN),新连接经对账补 accept 被本层感知。原显式 API net_resync_listen(fd) 已退役——"应用判定连接已死"这一步本身就是误判源(对活连接调用会把活连接标成监听态)。

⚠ 切勿用单次芯片状态读决定重建:单次读可能落在"刚 open 未生效"瞬态,曾致监听反复重建直至 relisten give up(2026-09-23 实测风暴,三服务口全死)——任何重建动作必须去抖(连续确认)+ 冷却(§3.6)。

方式三:事件回调

void callback(int sockfd, net_event_t event, void *arg)
{
    if (event == NET_EVENT_DISCONNECTED) {
        /* 连接断开 */
    }
}

12.5 select 和 poll 的区别?

特性 select poll
描述符集合 fd_set(位掩码) pollfd 数组
最大 fd 数 32 无限制
使用方式 NET_FD_SET 宏 直接操作结构体

两者功能相同,推荐使用 net_select(),更接近 Linux 编程习惯。

12.6 错误码说明

错误码 宏定义 说明
0 NET_OK 成功
-1 NET_ERR 通用错误
-2 NET_ERR_BADF 无效的 socket 描述符
-3 NET_ERR_INVAL 无效参数
-4 NET_ERR_NOMEM 内存不足
-5 NET_ERR_NOTCONN 未连接
-6 NET_ERR_ISCONN 已连接
-7 NET_ERR_WOULDBLOCK 非阻塞模式下操作未完成
-8 NET_ERR_CONNRESET 连接被重置
-9 NET_ERR_TIMEDOUT 操作超时
-10 NET_ERR_NOSPACE 发送缓冲区无空间
-11 NET_ERR_BUSY 操作未完成(BUSY)

12.7 CH395F 命令执行状态码

CH395F 底层驱动返回的状态码(ch395f_get_cmd_status() 或 ch395f_poll_cmd_status()):

代码 宏定义 说明
0x00 CH395F_ERR_SUCCESS 成功
0x10 CH395F_ERR_BUSY 忙,命令正在执行
0x11 CH395F_ERR_MEM 内存管理错误
0x12 CH395F_ERR_BUF 缓冲区错误
0x13 CH395F_ERR_TIMEOUT 超时
0x14 CH395F_ERR_RTE 路由错误
0x15 CH395F_ERR_ABRT 连接中止
0x16 CH395F_ERR_RST 连接复位
0x17 CH395F_ERR_CLSD 连接关闭
0x18 CH395F_ERR_CONN 无连接
0x19 CH395F_ERR_VAL 值错误
0x1A CH395F_ERR_ARG 参数错误
0x1B CH395F_ERR_USE 已被使用(常见于重复 OPEN 或冲突配置)
0x1C CH395F_ERR_IF MAC 错误
0x1D CH395F_ERR_ISCONN 已连接
0x20 CH395F_ERR_OPEN 已打开
0x5F CH395F_CMD_RET_ABORT 命令中止
0xFA CH395F_ERR_UNKNOW 未知错误

12.8 故障排查

问题:Socket 4~7 的 CONNECT/RECV 中断不触发

  • 需使用 GET_GLOB_INT_STATUS_ALL(命令码 0x19,返回 2 字节)才能检测 Socket 4~7 的中断
  • 1 字节版 GET_GLOB_INT_STATUS(命令码 0x29)仅支持 Socket 0~3

问题:CH395F 初始化超时

  • 芯片 INIT 需要约 200ms,轮询间隔应为 20ms(过短的查询可能干扰内部处理)
  • 超时值建议设为 4 秒以上

12.9 线程安全问题

问题:net_send() / net_recv() / net_close()(含 net_recv_to())可以在任意任务中调用吗?

可以。这些公共 API 通过消息队列将请求委托给 netTask 串行执行;注意阻塞 recv 的"阻塞"发生在调用方自己的上下文(挂起模型:登记请求后调用方睡任务通知,netTask 不等待,§3.6)——因此多个任务可同时持有 recv,netTask 只被数据路径短暂占用。

问题:netTask 内可以直接调用 net_send() / net_recv() 吗?

不可以。netTask 中必须使用 net_send_sock() / net_recv_sock() 等内部版本,否则会死锁(任务向自身发送消息,永远无人处理)。netTask 上下文的"立即看有无数据"需求用 NET_MSG_DONTWAIT(挂起请求对执行任务自身发起时直接拒绝,防自等自通知死锁)。

问题:哪些操作要求 netTask 已运行?

net_listen() / net_accept() / net_connect() / net_send() / net_recv() / net_close() 全部经消息队列 → 只能在netTask 已运行的上下文(就绪屏障后的服务任务)调用;内核启动前的代码禁调任何 FreeRTOS/CMSIS-RTOS API,网络监听建立天然在内核启动后。net_socket() / net_bind() 为直接调用,但同样建议在屏障后。

问题:消息队列由谁创建?在哪里创建?

由 CubeMX 在 MX_FREERTOS_Init() 中创建(osMessageQueueNew(8, sizeof(net_msg_t), NULL)),在 osKernelInitialize() 之后执行,确保 FreeRTOS 内核已就绪。

问题:net_recv() 返回 -1 后,net_get_errno() 的 NET_ERR_WOULDBLOCK 是谁设置的?

消息体是按值拷入队列的:osMessageQueuePut() 复制整个 net_msg_t,netTask 改的是它自己那份副本, 调用方看不到(极易踩坑)。因此实现上不靠消息字段回传错误码,而是把它搭在 xTaskNotify() 的通知值上: 失败时通知值 = 具体错误码(负值,如 NET_ERR_WOULDBLOCK),成功时 = 实际字节数/描述符; 公开 API 再把它恢复为 s_net_errno,并按 BSD 语义对外统一返回 -1。

推论:不要给 net_msg_t 加"回传用"字段——调用方读到的只会是自己的初值/栈垃圾; 需要回传请用调用方提供的指针,或复用通知值。(2026-09-21 实证:曾按"加 msg.err 字段"方式修错误码, 结果 ui_net_sink 每次空轮询都拿到栈垃圾 → 误判为"真错误" → 连上即 net_close。)

12.10 监听器自愈行为与沉降/隔离(D10 / TRAP-NET-09、TRAP-NET-17、TRAP-NET-18)

默认行为(无需应用代码):断连 → auto_relisten 重开监听;重开失败 → 30ms 节奏重试(2s 时限);芯片事件未收到/静默回收/监听失位 → 单一 reconciler 100ms 自愈(分级去抖,对账与重建合一);中止复连首上传 → 沉降/隔离。全在 netTask 内,应用只需保证 net_poll() + net_process_messages() 主循环在跑。

场景 修复前症状 机制 生产验证(2026-09-23)
TRAP-NET-17 快速复连事件面未收到 → 监听静默死亡(E14 复核:非芯片丢事件,是读清消费竞态) 待机后 UI :7003 快速复连 2/10 且永久拒连;FTP :21 100ms 间隔自第 29 轮死亡;设备日志无报错(状态静默滞留) 单一 reconciler net_reconcile_tick()(100ms,对账①②⑤ + 重建③④/U11 合一,唯一重建入口=形态③④;判据读驱动状态缓存)+ 分级去抖(②1 轮/③④⑤2 轮/①4 轮)+ 冷却(1s);handle_disconnect_event 只置沉降事实、不再自行重建 :7003 10/10;:21 100ms/0ms/600ms 各 50/50;UI 20/20;FTP 正常 10/10(SHA-256);中止 RST/FIN 5/5 + 5/5;零重建风暴(2026-10-13 合并后复测 :7003 50/50、:21 三档各 50/50 全绿)
TRAP-NET-18 中止后首笔复连上传被静默截断 ≤2048B 并误报 226 成功 RST/FIN 中止上传 → 紧接着的首笔上传"收"到 ~2KB 残留后判结束回 226 → 客户端以为成功 = 静默数据损坏(第二笔起正常) 异常释放(TIMEOUT)预重监听隔离:net_drain_chip_rx() 分块排空 RX 残渣 + 主动 close + 排空中断 + 等 CLOSED/TCP_CLOSED(400ms 起 / 800ms 上限 < 2s 时限) RST 10/10、FIN 5/5 全通过;正常路径 30/30 + 20/20,SHA-256 全匹配
TC-NET-604"接收冻结"虚警 旧判读脚本把"对端填满 2KB RX 窗口"误判为 STALL/WEDGE 结论:非缺陷(芯片 RX 缓冲即窗口,对端填满是常态);判据改为脚本层 STEP + 客户端内容校验(RET SHA-256),虚警工具归档(MDK-ARM/retracted_bench_20260922/,README 禁重跑) 真实 FTP 路径 227 轮全通过(8KB×10 … 1MB×2);并发 UI 流 600/600 CRC;设备零 FAIL/ERROR/WARN

排障判据(无 Wireshark 环境,纯生产路径):MDK-ARM/ftp_freeze_probe.py(真实 STOR + RETR SHA-256)、test/host/ftp_abort_probe.py(RST/FIN 中止复连)、MDK-ARM/tcp_reconnect_probe.py(快速复连存活甄别)、MDK-ARM/bench_verdict.py(脚本层 STEP 为真值;probe_classify.py A/C 判读不可信,见 retracted 归档 README)。详见 docs/问题记录/CH395F_Trap_Records.md。


变更记录

日期 变更
2026-10-14 二期 SEND pending 化(架构见 docs/网络/Net层阻塞模型架构设计.md §4.5/§5.5/§7):net_send 快路径改为有界自旋(NET_SEND_SPIN_MS = 2ms,健康链路块间等待 med 219µs ≪ 2ms,行为不变),超预算登记挂起 send,由新增 net_send_pending_tick 按主循环节拍分块推进(①断连 ②推进 ③写完 ④无进展超时),调用方睡任务通知——netTask 最坏停滞由 NET_SEND_TIMEOUT_MS(1s)压到 2ms。返回值恒为已写入字节数(短写须检查);NET_SEND_TIMEOUT_MS 降级为无进展超时(每次成功写块刷新)。主循环 tick 由 3 个增至 4 个(新增 net_send_pending_tick,与 net_pending_tick 并列);§3.3 / §3.5 / §3.6 / §10.2 / §12 同步
2026-10-13 自愈收敛阶段 3 自愈合并 + E-02 收口(实施规格现沉淀 docs/网络/网络自愈架构设计.md §4.1):net_listener_pump 删除,与 net_reconcile_tick 合并为单一 100ms tick(覆盖原对账①②⑤ + 原 pump③④/U11;判据读驱动状态缓存 ch395f_sock_state_cached,唯一刷新点 = net_poll 每轮全量刷新,E2 实测单次回读 18.1µs;分级去抖 ②=1 轮 / ③④⑤=2 轮 / ①=4 轮(M3 让位窗 400ms)+ 1s 冷却;§2.2 / §3.3 / §3.6(五层 → 四层)/ §10.2 / §12.1 / §12.10 同步,主循环自愈 tick 由 4 个回到 3 个;常量 NET_LISTENER_PUMP_PERIOD_MS / NET_LISTENER_STABLE_N / NET_LISTENER_COOLDOWN_MS 删除);忙等统一——net_listen_locked / net_connect_locked open 失败重试"close+盲等 500ms"与 net_close_locked 500ms 排空循环改走驱动 sock_reset 原语(net_sock_reset_sync,GRACEFUL 100ms 底),net_close_locked 不再读清全局中断寄存器(F-NET-7:全局寄存器唯一读者 = net_poll);新增 net_drop_conn(sockfd)(§10.1,E-02 收口:拆会话保监听、幂等、EOF 路径不得调;新消息类型 NET_MSG_DROP_CONN;timeSync 30s 预算耗尽路径接入;模拟器实现为关数据连接);net_poll 每轮对在用 Socket 步进 ch395f_sock_reset_poll(同步包装 600ms 放弃后复位机不搁浅);net_sock_state_t 注释声明为"本层会话投影"(芯片事实源 = 驱动缓存,net_types.h);台架回归全绿(最终构建 Code=211348,E-02 双 PASS,见 docs/网络/网络自愈架构设计.md §5.2 E-02)
2026-10-13 自愈收敛阶段 1.5 recv 语义主流化(规格现沉淀 docs/网络/网络自愈架构设计.md §4.5):net_recv() 默认无限阻塞(删 NET_RECV_TIMEOUT_MS 30s 默认预算,唤醒源=数据/EOF/close/网络重建 CONNRESET);新增 net_recv_nb()(NET_MSG_DONTWAIT 路径转正)与 net_setblocking(fd, on)(模式挂 socket 控制块属性,net_sock_t.blocking,默认阻塞);net_recv_to() 保留为显式预算便捷入口(timeout_ms=0 语义从"默认 30s"改为"无限"故仍拒 INVAL);内部 recv 看门狗降级为 tripwire(挂起超 60s 只 [ERR] 留痕一次,不再"到点 +1s 强制了结"伪造 TIMEDOUT);⚠ 新增文档契约:IWDG required_mask 内任务禁止无限阻塞 recv(必须 net_select 超时或 net_recv_to 显式预算);ui_net_sink / iec104_serv 迁移 net_recv_nb,lftpd / timeSync 维持显式预算;模拟器 net_socket_sim.c 镜像三 API(blocking 属性包装层模拟)
2026-10-13 自愈收敛阶段1 API 面收敛(审计过程已归档 docs/网络/网络自愈架构设计.md §12,现状见其 §4):删除手动自愈 API net_relisten() / net_resync_listen()(生产零调用;后者语义并入对账⑤——本层 ESTABLISHED 而芯片 0x05/0x01 静默回收 → RESYNC 只修本层,§3.6 对账由两类扩为三类①②⑤;前者与 pump/D10 重叠且对健康监听器是 footgun)与 net_can_accept()(只认 ESTABLISHED 的旧门控挡死丢 CONNECT 探针,监听门控统一 net_listener_armed(),§10.1 门控节改写);NET_MSG_RESYNC_LISTEN / NET_MSG_RELISTEN 消息类型删除;timeSync 会话收尾不再 net_close(同 fd 续 accept,监听跨会话常驻);lftpd 应用层阻塞重试退役(就绪屏障已消除 TRAP-NET-09 启动形态)
2026-10-12 N-32 监听生命周期模块化 + 单一所有者:监听生命周期抽为 net_listener.c(§2.2 文件表新增该行,真机专属);主循环自愈 tick 由 3 个增至 4 个(新增 net_listener_pump,100ms,唯一重建入口);net_reconcile_tick 对账收窄为 ①②,③④ 移交 pump;handle_disconnect_event / handle_timeout_event 改为 net_listener_settle(GRACEFUL 100ms / QUARANTINE 400ms) 只置事实、不再自行重建;§3.3 / §3.6(四层自愈 → 五层)/ §12.10 同步。权威设计见 docs/网络/网络自愈架构设计.md §11
2026-10-10 新增 net_can_accept() / net_can_read() 就绪门控 API(§10.1):纯 RAM 读控制块、不走消息队列、不等 netTask 回执,供实时性敏感任务在发 RPC 前判断"有没有活"(动因 = ui_net_sink_poll() 每 10 ms 一次 net_accept 把 uiTask 拖成最坏 ≈50 ms;原拟登记 N-19 / C7,当日结案不入登记簿;设计与实测见 docs/子系统设计/看门狗设计说明.md §11(原分析文档已并入该节));§3.5 补"netTask 运行要求"例外条;⚠ 记录两条使用约束——不可作为唯一判据(readable 只由 RECV_OK 中断置位,须保留低频兜底 recv)与模拟器后端恒返 1(net_socket_sim.c,Winsock 无 RPC 成本)
2026-09-23 同步 L3 pending 化、D10 非阻塞重监听、TRAP-NET-17 对账、TRAP-NET-18 排渣隔离后的现状:新增 net_recv_to / net_sock_state / net_resync_listen / net_relisten(§10.1)及 net_getsockname;§3 新增 3.6(对账与自愈四机制)、3.3/3.5 改写(recv 挂起模型、队列语义、netTask 运行要求);§4/§5 示例更新(监听由就绪屏障后服务任务建立、net_sock_state 轮询);§10.2 改述 net_process_messages() 每轮节奏(重监听重试 → 挂起 recv 完成 → 200ms 对账 → 队列),补 timeout_ms/新消息类型说明;新增 §11.2 分片阻塞读(net_recv_to);§12.1/12.3/12.4/12.9 更新,新增 §12.10(自愈行为数据与排障判据,含 TC-NET-604 结案)
2026-09-21 修正公开 API 的错误码回传:net_msg_t 按值拷入队列 → netTask 对消息的写入调用方不可见,改用 xTaskNotify() 通知值回传具体错误码(负值),公开 API 恢复 errno 后统一返回 -1(此前被覆盖成 NET_ERR,使 net_get_errno() != NET_ERR_WOULDBLOCK 的"空轮询"判断恒成立 → ui_net_sink 连上即 net_close);§12.4 补单连接模型断开处理(EOF 时不得 net_close 监听 socket);§12.9 补错误码回传约束
2026-09-15 同步配置引擎属主变更(P1-3):net_init 调用方说明改为"netTask 只读 RAM 副本"(网口配置由 defaultTask 在就绪屏障前加载);模拟器端启动序列补 net_cfg_save_run()(主循环 = 模拟器侧配置引擎属主,执行 UI 保存请求)
2026-09-13 从 git 历史恢复(4e9ac0d 2026-08-28 误删);同步当前代码:net_init 4 参数(新增 mac)、net_socket 2 参数(移除 protocol)、新增 net_get_generation / net_generation_inc / net_listen_locked、NET_NO_OS 双端构建、netTask 周期 10ms → 1ms
2026-09-13 配图:§2.1 分层架构改 SVG 生成器产物(tools/arch_diagram/gen_net_stack_svg.py → docs/images/BSD网络栈分层.svg);§2.4 状态机、§3.2 消息队列时序改 Mermaid(状态名对齐代码 net_sock_state_t,补 UDP_OPEN / auto_relisten 迁移,修正旧 ASCII 图不准处)
2026-09-13 新增 §2.3「PC 模拟器端:net_socket_sim」(Winsock 后端、NET_NO_OS 裁剪清单、MCU/模拟器行为差异表、模拟器端启动序列);§2.2 文件表补模拟器后端行
2026-08-28 原版最后更新(删除前):net_socket() / net_recv() / net_close() 等返回值语义统一(0 / -1,net_socket() 返回描述符)
2026-08-20 增加 8 Socket 架构、事件回调机制、select/poll、字节序处理
2026-08-18 增加消息队列模式、线程安全设计
2026-08-15 初版:BSD Socket API 概览与基本用法