- D-6 目录规范化:docs 根目录散落文档归入 UI设计/子系统设计/架构设计/审查记录/ 环境与部署/网络 等子目录,去掉目录名空格,重命名 嵌入式C语言代码规范_V1.0.md - D-7 三档一致性门禁:新增 scripts/check_doc_links.py(链接存在性)、 scripts/gen_task_table.py(FreeRTOS 任务表数值)、scripts/check_mem_numbers.py (内存/Flash 体积),均支持 --check/--write/--diff,pre-commit 钩子同步注册 - D-5 文档索引:新建 docs/README.md,新建 docs/文档规范.md 约束目录命名与链接写法 - D-1 芯片手册瘦身:删除 1069 张无引用孤儿图片(77.06 MB), docs/芯片手册 120 MB → 41 MB,已引用图片零断链 - 同步 C 源码注释中的文档路径引用(D-6 重组后),构建 0 错误 0 警告
58 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、L3 pending 化、D10 非阻塞重监听、Trap 22 周期对账、Trap 23 排渣隔离)。详见文末变更记录。伴读文档:本文回答"上层该怎么调";内部函数之间的调用关系、每个函数的设计作用与意图(netTask / 消息队列 / L3 挂起 / D10 重试 / Trap 22 对账 / Trap 23 排渣各子系统,含总体调用图与端到端时序)见
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_poll() 实现 |
| 错误码 | errno | net_get_errno() |
| 头文件 | <sys/socket.h> |
"net_socket.h" |
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() 版本;另含 L3 pending 化 recv、D10 非阻塞重监听重试(30ms/2s)、Trap 22 周期对账(200ms,去抖+冷却)、Trap 23 排渣隔离(400ms 起/800ms 上限) |
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 串行化 |
单线程直调;所有 Socket 创建即非阻塞(FIONBIO),主循环直接轮询 |
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_relisten() / net_resync_listen()——请求入队列,netTask 串行执行,调用方阻塞等回执 |
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() 每轮先跑 D10 重监听重试 → L3 挂起 recv 完成 → Trap 22 对账(200ms)三个非阻塞 tick,再处理队列 |
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(L3 pending 化,2026-09-22):无数据时公共
net_recv()/net_recv_to()登记挂起请求后返回(每 fd 至多一个,禁止覆盖——覆盖即丢失前调用方通知 = 永久挂死);完成由net_pending_tick()按主循环节拍检查(①数据 ②到点 ③断连,看门狗 1s)并经任务通知唤醒调用方——等待期间调用方在自己上下文睡通知,netTask 零占用(消除 E-02 头阻塞)。副作用:buf指针须保持有效至返回;close / 网络重建 / 陈旧重同步会显式取消并通知(NET_ERR_CONNRESET等),调用方永不挂死 - 队列阻塞语义:只有
osMessageQueuePut失败(8 长度固定队列,极端情形)时公共 API 阻塞;recv 挂起等待不计入队列占用(完成前队列中无消息滞留) - 哪些操作要求 netTask 已运行:
net_listen()/net_accept()/net_connect()/net_send()/net_recv()/net_close()/net_relisten()/net_resync_listen()全经消息队列 → 只能在就绪屏障后的任务调用;net_socket()/net_bind()为直接调用,但同样建议屏障后
3.6 芯片/本层状态对账与重监听自愈(Trap 09 / 22 / 23)
单连接模型下"监听 Socket = 数据通道",多种芯片行为会造成芯片侧与本层状态失同步,纯事件(中断)驱动无法自愈。当前版本有四层自愈,全在 netTask 内、全非阻塞、不冻结主循环:
| 机制 | 触发 | 动作 | 关键参数 |
|---|---|---|---|
auto_relisten |
数据通道正常 EOF / DISCONNECT | 重开 + 重新 LISTEN,回 LISTENING;应用层无需 close/重 listen | net_listen() 置位;net_close() 清 |
| D10 非阻塞重监听重试 | 重开失败(Trap 09:芯片 close 后需沉降) | 单次尝试失败即登记待重试,每轮到点重试;超时限放弃并记 relisten give up |
重试间隔 30ms / 时限 2s(自首次尝试);同 fd 有在途挂起 recv 时跳过(L3 保险,防毁在途连接) |
| Trap 22 周期对账 | 芯片丢 CONNECT/DISCONNECT 中断(快速复连);或静默回收 Socket(peer FIN,无 DISCONNECT 中断) | 每 200ms 对在用 socket 回读芯片状态,修正三类失同步:① 本层 ESTABLISHED 而芯片已断 → 按断开处理;② 本层 LISTENING 而芯片已握手 → 补 accept;③ 本层 LISTENING 而芯片监听消失 → 重建 | 周期 200ms;连续 2 次失配才动手(去抖 ≈400ms)+ 动手后 1s 冷却(防"刚 open 未生效"瞬态重建风暴) |
| Trap 23 排渣隔离 | 异常释放(芯片 TIMEOUT,如客户端中止上传):在途数据落进 RX 缓冲,直接复监听 → 下条连接把 ~2048B 残渣当自己的数据读走(上传静默截断) | 复监听前隔离:持续排空 RX 残渣 + 主动 close + 排空中断,等芯片回 CLOSED/TCP_CLOSED 才 open+listen;到期强制放行 | 隔离 400ms 起 / 800ms 上限(必须 < 2s 时限,否则监听被放弃);仅异常路径生效,正常 FIN 路径零改动 |
行为结论:监听器恢复从"一次性事件"变为"登记 → 到点重试"过程:
- 正常断连:通常一次重开即成;失败时 ~30ms 量级恢复,最迟 ~1s(2s 后放弃,错误日志可见)。恢复窗内该端口拒绝新连接 → 调用方重连逻辑须容忍 ~1s 中断。
- 快速复连不再"监听死亡"(Trap 22):对账保证失同步 ≤200ms 内修正(2026-09-23 生产路径:待机后 UI
:7003快速复连 10/10;FTP:21100ms/0ms/600ms 间隔各 50/50;零重建风暴)。 - 中止复连首上传不再被静默截断(Trap 23):RST/FIN 中止后首笔复连上传完整通过(RST 10/10、FIN 5/5,SHA-256 全匹配)。详见
docs/问题记录/CH395F_Trap_Records.mdTrap 22/23。
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()每轮先跑 3 个非阻塞检查再取队列:D10 重监听重试(断连后监听器恢复)→ L3 挂起 recv 完成检查(阻塞 recv 唤醒,E-02 修复)→ Trap 22 芯片/本层对账(200ms 周期,丢中断/静默回收自愈)
- 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 编程
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)。中断丢失也不会让监听死亡——对账在 200ms 内修正"本层 LISTENING 而芯片监听消失"等失同步。
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=阻塞,NET_MSG_DONTWAIT=非阻塞 |
| 返回值 | 实际接收字节数 (>0);0=对端关闭(net 层随即自动重监听);-1=错误(非阻塞无数据=NET_ERR_WOULDBLOCK;阻塞预算内无数据=NET_ERR_TIMEDOUT,默认 30s NET_RECV_TIMEOUT_MS;close/网络重建/陈旧重同步取消=NET_ERR_CONNRESET) |
L3 pending 化(2026-09-22):无数据时阻塞接收不占 netTask——登记挂起请求(每 fd 至多一个,禁止覆盖:覆盖会丢失前调用方通知 = 永久挂死)后立即返回;完成按主循环节拍检查(数据/到点/断连,看门狗 1s)并经任务通知唤醒调用方。等待期间调用方在自己上下文睡通知,netTask 全程零占用(消除 E-02 头阻塞)。
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 会被解释为"默认 30s",恰是本接口要规避的长阻塞,直接拒 NET_ERR_INVAL) |
| 返回值 | >0=字节数(允许部分读,TCP 分段);0=对端关闭(排空残留后);-1=真错误(net_get_errno 可查);-2=本片预算内无数据(不是 errno,不得依赖 errno 判超时——s_net_errno 是跨任务共享全局量) |
调用方累计自身总预算:见到 -2 发起下一片,直到收齐 / EOF / 总预算耗尽。生产用途:lftpd 数据行读、timeSync 帧读(E-02 头阻塞修复,完整例 §11.2)。
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);
自愈与查询 API
net_sock_state — 查询 Socket 状态(只读快照)
int net_sock_state(int sockfd);
跨任务只读快照(单字节读);描述符无效/未占用返回 -1;值可能滞后 netTask 最新状态。用于状态轮询(客户端等 ESTABLISHED,§5.2),不替代 accept/connect。
net_resync_listen — 断连重同步(仅修 MCU 侧)
int net_resync_listen(int sockfd);
适用场景(D14):对端 FIN 后芯片静默回收 Socket 回监听且不发 DISCONNECT 中断 → 本层滞留 ESTABLISHED,新连接字节被旧会话吞掉。须确认连接已死(如控制通道超时中止后)再调;只把本层状态改回 LISTENING 并取消本 fd 挂起 recv,不发任何芯片命令(发 close/重监听会摧毁芯片侧健康监听器 → 连接永拒)。返回 0=已同步(或原本即一致),-1=失败。注:Trap 22 周期对账同样能自愈合此失同步(≤200ms),显式 API 用于"不愿等下一 tick"的场景。
net_relisten — 无条件重新监听
int net_relisten(int sockfd);
TCP 监听型 Socket:芯片侧 open+listen(D10 非阻塞重试)。芯片侧已在监听时:首次 open 失败 → close 重建 → 监听短暂中断 ~1s;两种情形都收敛到"监听器确定在位"。用途:代次号变化后显式重建(见 docs/子系统设计/网络配置说明.md §4)、应用判定连接已死需确定性监听。正常流无需(auto_relisten + 对账已覆盖)。返回 0 成功 / -1 失败。
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/resync-listen / relisten)。每次调用先跑 3 个非阻塞 tick,再取队列消息(0 超时出队):
- D10 重监听重试:断连后监听器恢复的到点重试(30ms 间隔 / 2s 时限,§3.6)
- L3 挂起 recv 完成检查:按 ①数据 ②到点 ③断连 检查每个挂起请求并通知调用方(看门狗 1s)
- Trap 22 芯片/本层对账:200ms 周期修正三类失同步(§3.6)
net_msg_t.timeout_ms:L3 分片预算(net_recv_to()),0=默认NET_RECV_TIMEOUT_MS(30s)。消息类型含NET_MSG_RESYNC_LISTEN(仅 MCU 侧状态)与NET_MSG_RELISTEN(芯片侧重监听)。
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);
}
/* 检查接收 */
char buf[128];
int len = net_recv(sock, buf, sizeof(buf) - 1, NET_MSG_DONTWAIT);
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 头阻塞)。L3 pending 化后:单次 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():除串行处理队列请求外,每轮先跑 3 个非阻塞检查——D10 重监听重试(断连后监听器恢复)→ L3 挂起 recv 完成检查(阻塞 recv 的唤醒)→ Trap 22 芯片/本层对账(200ms 周期,丢中断/静默回收自愈)。
建议主循环周期 ≤ 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 窗口填满是正常现象,不等于"冻结"(Trap 21 结案,见 §12.10)
12.4 如何处理连接断开?
方式一:阻塞接收
int len = net_recv(sock, buf, sizeof(buf), 0);
if (len <= 0) {
/* 连接断开或错误 */
net_close(sock);
}
方式二:非阻塞接收
int len = net_recv(sock, buf, sizeof(buf), NET_MSG_DONTWAIT);
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 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 答完一帧即断连、
端口随后"连接被拒"。)
芯片静默回收(D14,2026-09-22 台架实证):对端发 FIN 后,芯片可能自行把 Socket 回收回 LISTEN 且不发 DISCONNECT 中断 → 本层滞留 ESTABLISHED,应用继续往死连接上读写,新连接的字节被旧会话吞掉。两种处理:
- 等对账自愈(默认,推荐):§3.6 对账 ≤200ms 发现"本层 ESTABLISHED 而芯片已回收" → 按断开处理 → 自动重监听;等待期间该端口实际可接新连接(芯片侧已 LISTEN),新连接经对账补 accept 被本层感知。
- 显式重同步(需立即对齐时):确认会话已死(如控制通道超时中止)后调
net_resync_listen(fd)——只修 MCU 侧状态 + 取消挂起 recv,不发芯片命令(发 close/重监听会摧毁芯片侧健康监听器 → 连接永拒)。
⚠ 切勿用单次芯片状态读决定重建:单次读可能落在"刚 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(L3:挂起请求对执行任务自身发起时直接拒绝,防自等自通知死锁)。
问题:哪些操作要求 netTask 已运行?
net_listen() / net_accept() / net_connect() / net_send() / net_recv() / net_close() / net_relisten() / net_resync_listen() 全部经消息队列 → 只能在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 09、Trap 22、Trap 23)
默认行为(无需应用代码):断连 → auto_relisten 重开监听;重开失败 → 30ms 节奏重试(2s 时限);芯片丢中断/静默回收 → 200ms 对账自愈;中止复连首上传 → 排渣隔离。全在 netTask 内,应用只需保证 net_poll() + net_process_messages() 主循环在跑。
| 场景 | 修复前症状 | 机制 | 生产验证(2026-09-23) |
|---|---|---|---|
| Trap 22 快速复连丢中断 → 监听静默死亡 | 待机后 UI :7003 快速复连 2/10 且永久拒连;FTP :21 100ms 间隔自第 29 轮死亡;设备日志无报错(状态静默滞留) |
周期对账 net_reconcile_tick()(200ms,三类失同步)+ 去抖(连续 2 次)+ 冷却(1s);handle_disconnect_event LISTENING 分支改为"已不可监听即重建" |
:7003 10/10;:21 100ms/0ms/600ms 各 50/50;UI 20/20;FTP 正常 10/10(SHA-256);中止 RST/FIN 5/5 + 5/5;零重建风暴 |
| Trap 23 中止后首笔复连上传被静默截断 ≤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 全匹配 |
| Trap 21(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)、MDK-ARM/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-09-23 | 同步 L3 pending 化、D10 非阻塞重监听、Trap 22 对账、Trap 23 排渣隔离后的现状:新增 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(自愈行为数据与排障判据,含 Trap 21 结案) |
| 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 概览与基本用法 |