用户裁定:文档只写现状,历史流水一律删掉,过程查 git log。 - 43 篇「变更记录 / 修订记录」整节删除:继电保护规划 12.6 KB、产品 需求 11.8 KB、项目部署环境 5.5 KB、BSD Socket 指南 5.3 KB、架构 总览 5.0 KB、实机测试实施指南 3.8 KB 等 - 23 篇别处仍有指向:只留节名 + 一行「本文件不再记流水账,过程查 git log」,节号与锚点不位移(doc-links 不因此断链) - 5 篇整表保留(被当唯一证据 / 活台账引用):看门狗设计说明、配置 文件设计说明、RAM预算台账、内容页架构、本地工具路径环境变量方案 - 规范同步:文档规范 §5.1 改新裁定;测试文档模板规范删示例节与 登记句;测试开发标准流程 / UI 两篇 / 实机测试实施指南 / 四篇单测 说明 / 并行任务工作树方案 / 编号规范 的「登记变更记录」改「写进 正文」 - 技能同步:stale-doc-count-sweep / ui-verify / id-family 三个 SKILL 正文不再要求追加流水行(顺带消掉 SKILL.md:121「产品需求 §12」 失效引用) - 修 5 处悬空指针:采样有效性校验目录锚点、BSD 指南「文末变更 记录」、docs/README 过期描述、网络自愈架构两处指向面 72 篇文档净减 600 余行;check_doc_links --all --sections-strict 可判 定比例 61% → 78%、失效引用 0;静态门禁 passed=50 failed=0;gitlint 10 条历史提交信息 0 failed。 未提交他人在飞改动:.dsh/skills/keil-build/*(%SystemDrive% 守卫)。
70 KiB
BSD Socket API 使用指南
恢复说明:本文档曾在提交
4e9ac0d(2026-08-28)中误删,2026-09-13 从 git 历史恢复。API 随代码多轮同步(2026-09-13:net_init4 参数 /net_socket2 参数 / 网络代次号 /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 排渣隔离)。历次同步的完整过程查 git log。伴读文档:本文回答"上层该怎么调";内部函数之间的调用关系、每个函数的设计作用与意图(netTask / 消息队列 / 挂起 recv / D10 重试 / TRAP-NET-17 对账 / TRAP-NET-18 排渣各子系统,含总体调用图与端到端时序)见
docs/网络/BSD_Socket_API_函数调用关系.md。
目录
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 分层架构
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 零占用(消除队头阻塞)。副作用:buf指针须保持有效至返回;close / 网络重建 / 陈旧重同步会显式取消并通知(NET_ERR_CONNRESET等),调用方永不挂死 - recv 语义主流化:对齐 POSIX/Python——模式归使用者、超时归调用方、API 不强加默认预算。
net_recv()默认无限阻塞(NET_RECV_TIMEOUT_MS30s 默认已删),唤醒源 = 数据 / EOF / close / 网络重建(CONNRESET),会话死亡由对账① ~400ms 判死兜底;非阻塞具名入口net_recv_nb()(原NET_MSG_DONTWAIT路径转正),模式属性net_setblocking(fd, on)挂 socket 控制块;超时首选net_select(...),net_recv_to(...)保留为显式预算便捷入口。内部看门狗降级为 tripwire:挂起超 60s 只[ERR]留痕一条(完成逻辑缺位探针),不再伪造 TIMEDOUT 返回值。⚠ 文档契约(嵌入式特有):IWDGrequired_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(幽灵甄别,属「relisten 沉降段原语化受阻」);③ 本层 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:21100ms/0ms/600ms 间隔各 50/50;零重建风暴)。 - 中止复连首上传不再被静默截断(TRAP-NET-18):RST/FIN 中止后首笔复连上传完整通过(RST 10/10、FIN 5/5,SHA-256 全匹配)。详见
docs/问题记录/CH395F_Trap_Records.mdTRAP-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 唤醒,队头阻塞修复)→ 挂起 send 推进(阻塞 send 唤醒)→ TRAP-NET-17 芯片/本层对账 + 监听重建合一 reconciler(100ms 周期,读驱动状态缓存,事件未收到/静默回收/监听失位自愈,§3.6)
- netTask:在任务函数中持续运行,每 1ms 调用一次
- 监听 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.0 s_addr 的唯一约定(2026-10-24 统一;全仓只此一族,勿再引入第二套)
定义:sin_addr.s_addr 一律存网络字节序整数,判据是 &s_addr 的第 0 个内存字节 == 点分第一段。
小端主机(STM32F407 与 PC 模拟器都是)上 "192.168.1.100" 的数值 = 0x6401A8C0(内存里是 C0 A8 01 64)。
| 方向 | 用哪个 API | 要点 |
|---|---|---|
字符串 → s_addr |
net_inet_addr() |
失败返回 0(与 BSD 一致),故 "0.0.0.0" 与非法串同值;别拿 NET_INADDR_NONE 当失败哨兵 |
s_addr → 字符串 |
net_inet_ntoa() |
内部先 NET_NTOHL 再高段在前打印(TRAP-NET-21 定案) |
不变量(有单测钉住):net_inet_ntoa(net_inet_addr(s), buf) 还原 s;net_inet_addr(net_inet_ntoa(v, buf)) 还原 v。
net_getsockname / net_getpeername / net_recvfrom 填出来的 s_addr,与喂进 net_bind / net_connect / net_sendto
的 s_addr 同族、可直接相等比较。历史背景(同一文件曾并存两套约定、ntoa 注释里的例子取了废弃族的值)
见 docs/问题记录/CH395F_Trap_Records.md TRAP-NET-21 的 2026-10-24 后续条目。
UDP 直调限制:net_sendto / net_recvfrom 不走消息队列——它们在调用方上下文里直接 net_poll() + 读写芯片缓冲,
因此仅限本层执行任务(NET 层 = netTask)调用;别的任务调用回 NET_ERR_NOT_IN_NETTASK(-12)。该码不是运行时故障,
重试永远无意义:要么把调用挪进执行任务,要么改用走队列的 net_send / net_recv,没有第三条路。
「把这两个接口改成经 netMsgQueue 串行化」是已知后续项,未做的理由见 Drivers/BSP/NET/net_socket.h 同名函数头。
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_MS30s 默认预算已删,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 全程零占用(消除队头阻塞)。内部 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 帧读(队头阻塞修复,完整例 §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 - 主动拆会话、保留监听(队头阻塞收口)
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_resetQUARANTINE(FIN + 排渣 + 排空事件 + 等 byte0=00H,TRAP-NET-18 异常释放语义,400ms 底);本层收口 CLOSED;auto_relistenfd 随即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):CH395F close 无 RST 语义——对端内核存活但不 close 时芯片停 FIN_WAIT_2 长尾,监听恢复受限于对端 close;对端配合则 TIME_WAIT 过渡
150ms → reconciler 形态④ relisten → 新连接接入。不配合场景兜底 = 芯片 TIMEOUT/keepalive 拆僵尸(E13 定案 3090s)。详见docs/问题记录/CH395F_Trap_Records.mdTRAP-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 超时出队):
- D10 重监听重试(
net_listener_tick):断连后监听器恢复的到点重试(30ms 间隔 / 2s 时限,§3.6) - 挂起 recv 完成检查(
net_pending_tick):按 ①数据 ②到点(仅显式预算) ③断连 检查每个挂起请求并通知调用方;④ 挂起超时留痕探针(编译期选项,默认关):挂起超阈值只[ERR]留痕一次,不强制结束 - 挂起 send 推进(
net_send_pending_tick):按 ①断连 ②推进(每轮至多一块NET_SEND_CHUNK_MAX) ③写完 ④无进展超时 推进挂起 send 并通知调用方(结果恒为已发字节数,短写由调用方检查) - 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_MS30s 默认预算——超时归调用方)。消息类型 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(队头阻塞)。挂起 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。