fff961d803
前置来自 #57 SNTP 调研:旧 net_sendto / net_recvfrom 由调用方直调 SPI 且无限自旋收包,既违约「netTask 是芯片唯一访问者」,又把 IWDG 预算吃在自旋里。 改造面 - 新增 Drivers/BSP/NET/net_udp.c/.h(真机专属,模拟器不编译): bind/set_peer/sendto/recvfrom 四条执行体 + 挂起收包表 + tick; net_msg_type_t 加四型,公开 API 一律「入队 → 睡在通知上」。 - 源地址出参不搭 net_msg_t(按值拷贝,AGENTS 禁加回传字段):改住 net_udp.c 的 per-fd 槽,netTask 就地写回调用方内存,通知值只带 结果码;每条出口都清槽,泄漏 = 该 fd 永久 BUSY。 - 发送复用 net_send_locked,不写第二份分块逻辑;「会话是否已死」 收成一条判据 net_send_state_down()(TCP 看 ESTABLISHED、UDP 看 UDP_OPEN),原先 4 处各写一遍。 - 顺带修掉真缺陷(新条 TRAP-NET-22):旧 sendto 把 DesIP 写成具体 IP,芯片静默切 UDP Client,而旧 recvfrom 无条件剥 8 字节 → SNTP 这类 48 B 应答丢载荷前 8 字节并交出垃圾源地址。加 udp_mode(塞进 原有 padding,sizeof 不变)、bind 恒 SERVER、显式对端 sendto 切 CLIENT、set_peer(fd, NULL) 轻量回 SERVER,仅 SERVER 且 avail >= 8 才剥头。 - 截断定案:一次调用交付一个数据报,buf 装不下的尾巴用 64 B 栈上 scratch 排空(不用 clear_recv_buf——它连后面已到达的包一起丢)。 - 取消点接齐:net_init 重建 cancel_all、net_socket 分配 fd clear、 net_close settle_close + clear、重 bind 前先结算等包者, net_process_messages 第 5 个 tick 位挂 net_udp_tick。 - 收紧两处契约(原实现无生产调用者):UDP 未 bind 一律 -5; net_recv / _nb / _to 拒 DGRAM(绕过剥头就是静默吃 8 字节); NET_ERR_NOT_IN_NETTASK(-12) 退役为历史常量并写明原因。 验证 - 主机单测 test/host/net_socket 全套 616/616 绿(改造前 470):新 分片 ns_s7_udp.c 20 组,改写 440 / 555 / 560 组;覆盖率 net_udp.c 234/236(99.2%)、net_socket.c 956/969(98.7%), scripts/test_coverage_baseline.json 随本提交登记新文件。 - 测试抓到 4 个被测码真 bug:自等守卫误用调用侧谓词、芯片长度被读 两次、槽指针取在 fd 合法性检查之前、重 bind 静默作废等待者。 - Keil ARMCC 0 Error(s) 0 Warning(s);net_udp.o Code 1,358 / ZI 224 (8 fd × 28 B,其余模块零增长),全仓 RW_IRAM1 72.2%;内存台账 受管区按门禁提示用 --write / --update 机器回填,未手改数字。 - pre-commit 全量钩子(含 doc-links、coding-standard、mem-budget、 generated-fresh、bsp-err、registration、nested-comment)全绿。 限度 - 本轮无板子:真机回归 5 条(两端互通、set_peer(NULL) 能否恢复收 广播、截断排空后下一包完整性、挂起 recv 期间 IWDG 余量、bind 写 DesIP 与 open 先后的真芯片效果)已写进覆盖率口径「台架必验」。 - scripts/stack_report.py 退出码 1 是 10-26 起挂在账上的 iec104Task 栈债(margin 1,016 < 1,024,见 docs/代码规范/pre-commit使用说明.md: 122);本轮只让 net_bind 的 UDP 分支多一层静态帧(1,544 → 1,600), netTask margin 1,888 与 timeSyncTask 1,152 不受影响。抬栈要过 mem-budget 台账,不在本分支自行处理。 - 登记簿销项按仓库约定留给主工作树,本分支不动 docs/任务规划/**。
1577 lines
79 KiB
Markdown
1577 lines
79 KiB
Markdown
# 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 排渣隔离)。历次同步的完整过程查 git log。
|
||
>
|
||
> **伴读文档**:本文回答"上层该怎么调";**内部函数之间的调用关系、每个函数的设计作用与意图**(netTask / 消息队列 / 挂起 recv / D10 重试 / TRAP-NET-17 对账 / TRAP-NET-18 排渣各子系统,含总体调用图与端到端时序)见 `docs/网络/BSD_Socket_API_函数调用关系.md`。
|
||
|
||
## 目录
|
||
|
||
1. [概述](#1-概述)
|
||
2. [架构说明](#2-架构说明)
|
||
3. [线程安全设计](#3-线程安全设计)
|
||
4. [快速开始](#4-快速开始)
|
||
5. [TCP 编程](#5-tcp-编程)
|
||
6. [UDP 编程](#6-udp-编程)
|
||
7. [I/O 多路复用](#7-io-多路复用)
|
||
8. [事件回调机制](#8-事件回调机制)
|
||
9. [字节序处理](#9-字节序处理)
|
||
10. [API 参考](#10-api-参考)
|
||
11. [完整示例](#11-完整示例)
|
||
12. [常见问题](#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 分层架构
|
||
|
||

|
||
|
||
### 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_udp.c`(**真机专属**) | **UDP 数据面执行体**(2026-11-03 队列化改造新增):`net_udp_bind_exec` / `net_udp_set_peer_exec`(切芯片形态,不重开 Socket)/ `net_udp_sendto_exec`(**复用** `net_send_locked` 的 1KB 分块与挂起 send)/ `net_udp_recvfrom_exec` + `net_udp_tick`(挂起 recv 完成 / 预算超时)+ per-fd 出参槽(源地址写回调用方自己的内存,不占 `net_msg_t`)+ 截断排空;内部头 `net_udp.h`,模拟器不编译;用法与约束见 §6.0 / §6.3,调用结构见 `docs/网络/BSD_Socket_API_函数调用关系.md` §4.11 |
|
||
| `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 状态机
|
||
|
||
```mermaid
|
||
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 消息队列模式
|
||
|
||
```mermaid
|
||
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 串行执行,调用方阻塞等回执。UDP 侧同一口径:`net_bind()`(UDP) / `net_sendto()` / `net_recvfrom()` / `net_recvfrom_to()` / `net_udp_set_peer()`(2026-11-03 队列化改造,见 §6)。**自愈无显式 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()`** | `net_socket()` 纯 RAM(分配控制块),任意任务直接调用;`net_bind()` 分两支——**TCP** 只记本地端口,仍是直接调用;**UDP** 要开芯片侧 Socket,走队列在 netTask 里执行(要发 5 条芯片命令,见 §6.0) |
|
||
| **`net_poll()` 和 `net_process_messages()`** | 必须在 `netTask` 主循环调用;`net_process_messages()` 每轮先跑 **5 个非阻塞 tick**(D10 重监听重试 → 挂起 recv 完成 → 挂起 send 推进 → **UDP 挂起 recv 完成/超时** → TRAP-NET-17 对账+监听重建合一 reconciler 100ms),再处理队列 |
|
||
|
||
### 3.4 内部 API 说明
|
||
|
||
内部 API(带 `_sock` 后缀)直接操作 Socket 控制块和 CH395F,不经过消息队列。用于 `netTask` 中的网络业务逻辑,避免向自身发送消息造成死锁:
|
||
|
||
```c
|
||
/* 在 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_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(幽灵甄别,属「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 `: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 包含头文件
|
||
|
||
```c
|
||
#include "net_socket.h"
|
||
#include "net_select.h"
|
||
```
|
||
|
||
### 4.2 初始化网络
|
||
|
||
```c
|
||
int main(void)
|
||
{
|
||
/* 初始化网络子系统
|
||
* 参数:IP地址, 子网掩码, 网关, MAC(NULL 或全零 = 芯片内置 MAC)
|
||
* 传入 NULL 使用 DHCP 自动获取
|
||
* 本项目中由 netTask 调用(网口配置已由 defaultTask 在就绪屏障前
|
||
* 加载进 RAM 副本,netTask 只读副本组网)
|
||
*/
|
||
int ret = net_init("192.168.31.100", "255.255.255.0", "192.168.31.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)
|
||
- **监听 Socket 建立**:由就绪屏障后的服务任务(timeSyncTask / ftpTask / uiTask)经队列 API 建立(`net_socket` → `net_bind` → `net_listen`);断连后 net 层自动重监听(§3.6),服务层只需循环 `net_accept`,无需维护重监听逻辑
|
||
|
||
```c
|
||
int main(void)
|
||
{
|
||
/* 外设初始化... */
|
||
|
||
/* BSD Socket API 初始化 */
|
||
net_init("192.168.31.100", "255.255.255.0", "192.168.31.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 既做监听又做数据通信:
|
||
|
||
```c
|
||
#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
|
||
|
||
```c
|
||
#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.31.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-11-03 队列化**(原"本项目不使用 UDP(2026-10-15 定案)"作废):
|
||
> `net_bind`(UDP) / `net_sendto` / `net_recvfrom` / `net_recvfrom_to` / `net_udp_set_peer` 与 TCP 同一口径
|
||
> ——请求投 `netMsgQueue`,芯片事务**全部在 `netTask` 里串行执行**,调用方阻塞等回执(结果搭 32 位任务
|
||
> 通知值回来)。任何任务都能安全调用,不再有"单任务独占期"这个前提。第一个真实消费者是 SNTP(请求-应答)。
|
||
|
||
### 6.0 先分清楚:芯片的 UDP 两种模式
|
||
|
||
CH395F 用 `DesIP` 一个寄存器区分两种 UDP 形态(手册 `docs/芯片手册/CH395F.md` §9.2.4),本层把它做成
|
||
socket 的**模式属性**(`net_types.h` `NET_UDP_MODE_SERVER` / `NET_UDP_MODE_CLIENT`)。两种形态收上来的
|
||
字节**不一样多**,这是 UDP 侧最容易踩的坑:
|
||
|
||
| 模式 | DesIP | 收包布局 | 谁能发给我 | 本层如何进入 |
|
||
|---|---|---|---|---|
|
||
| **服务器** | `0xFFFFFFFF` | **8 字节信息头** + 载荷(头 [0-1] 长度、[2-3] 源端口、[4-7] 源 IP 线序) | 任意主机 | `net_bind()`(恒此模式:BSD 的 bind 只定本地端口,不选对端) |
|
||
| **客户端** | 具体 IP | **无信息头**,整段就是载荷 | 只有该 IP | `net_sendto()` 带目的地址,或 `net_udp_set_peer(fd, &peer)` |
|
||
|
||
- 收包时**按模式**决定是否剥头:客户端模式绝不剥。旧实现不分模式恒剥 8 字节,而 `net_sendto` 一旦给出
|
||
目的地址芯片就已是客户端形态——于是 SNTP 这类应答的**前 8 字节载荷被吃掉**,源地址还是从载荷里当
|
||
头读出来的假值(缺陷登记见 `docs/问题记录/CH395F_Trap_Records.md`,回归钉子在
|
||
`test/host/net_socket/ns_s7_udp.c` UDP-3 / UDP-4)。
|
||
- 从客户端模式回服务器形态:`net_udp_set_peer(fd, NULL)`,只改 DesIP/DesPort,**不重开 Socket**;
|
||
再次 `net_bind()` 则是重开(顺带丢掉芯片缓冲里的残留包),两者用途不同。
|
||
- 客户端模式下 `net_recvfrom` 回填的源地址就是那个固定对端——芯片已经替我按 IP 过滤过一遍,包上也没有头可取。
|
||
|
||
### 6.1 UDP Client
|
||
|
||
```c
|
||
#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. 绑定本地端口:UDP 侧**必做**(芯片侧 Socket 要在这里 open,
|
||
* 没 bind 就收发一律 NET_ERR_NOTCONN)。端口给 0 = 本层取动态端口 */
|
||
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");
|
||
|
||
if (net_bind(sock, (struct net_sockaddr *)&local_addr, sizeof(local_addr)) != 0) {
|
||
printf("绑定失败\r\n");
|
||
net_close(sock);
|
||
return;
|
||
}
|
||
|
||
/* 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.31.200");
|
||
|
||
/* 4. 发送数据:带目的地址的 sendto 顺手把 DesIP 指向该地址,
|
||
* 芯片从此是**客户端模式**(只收这个 IP 的包,包上不带信息头,见 §6.0) */
|
||
const char *msg = "UDP Hello!";
|
||
if (net_sendto(sock, msg, strlen(msg), 0,
|
||
(struct net_sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) {
|
||
printf("发送失败 %d\r\n", net_get_errno());
|
||
net_close(sock);
|
||
return;
|
||
}
|
||
|
||
/* 5. 接收应答:受 IWDG 监管的任务用带预算的 net_recvfrom_to,
|
||
* 超时返回 -2(不置 errno),不会把喂狗窗口耗在等待上(§10.1) */
|
||
len = net_recvfrom_to(sock, buf, sizeof(buf) - 1, 2000U, NULL, NULL);
|
||
|
||
if (len > 0) {
|
||
buf[len] = '\0';
|
||
/* 客户端模式的源地址就是这个固定对端,包上没有信息头 */
|
||
printf("收到: %s (from server)\r\n", buf);
|
||
} else if (len == -2) {
|
||
printf("应答超时\r\n");
|
||
}
|
||
|
||
net_close(sock);
|
||
}
|
||
```
|
||
|
||
### 6.2 UDP Server
|
||
|
||
```c
|
||
#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. 循环接收数据:一次调用 = 一个完整数据报(服务器模式自动剥 8 字节信息头) */
|
||
while (1)
|
||
{
|
||
struct net_sockaddr_in src_addr;
|
||
int src_addr_len = sizeof(src_addr);
|
||
|
||
/* 用带预算的收包当循环节拍:-2 = 本轮没人发包,不是错误;
|
||
* 换 net_recvfrom(...) 就是无限等,受 IWDG 监管的任务里别这么写(§10.1) */
|
||
int len = net_recvfrom_to(sock, buf, sizeof(buf) - 1, 500U,
|
||
(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));
|
||
|
||
/* 回显数据。⚠ 带目的地址的 sendto 会把芯片切成**客户端模式**
|
||
* (DesIP = 这个客户端),此后只有它一家能再发包进来。要长期服务
|
||
* 多个客户端,回完就把它切回来: */
|
||
(void)net_sendto(sock, buf, len, 0,
|
||
(struct net_sockaddr *)&src_addr, src_addr_len);
|
||
(void)net_udp_set_peer(sock, NULL);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 6.3 UDP 调用约束(2026-11-03 队列化之后)
|
||
|
||
| 约束 | 行为与理由 |
|
||
|---|---|
|
||
| 未 `net_bind()` 就 `sendto` / `recvfrom` / `set_peer` | 一律 `NET_ERR_NOTCONN`(-5):芯片侧 Socket 还没 open,此时写 DesIP 也会被随后的 open 复位 |
|
||
| 一次收包 = 一个完整数据报 | 缓冲装不下时交付 `len` 字节并把剩余部分**读空丢弃**(留半包在芯片缓冲里,下一次收包会从上一包的尾巴开始解析,源地址与载荷从此全部错位);短交付靠返回值判读 |
|
||
| 同一 fd 并发收包 | 第二个接收者拿 `NET_ERR_BUSY`(-11):数据报语义下无法把包归属到两个等待者,拒绝比交错投递安全(与 TCP 挂起 recv 同一守卫) |
|
||
| 阻塞预算 | 受 IWDG 监管的任务禁用无限等待的 `net_recvfrom`,改 `net_recvfrom_to(fd, buf, len, to_ms, ...)`:超时返回 **-2** 且不置 errno(与 `net_recv_to` 同口径);或先 `net_select()` / `net_can_read()` 门控 |
|
||
| 单包载荷 | ≤ 1472 字节(手册 §9.2.4);更长由本层按 1KB 分块、复用 TCP 的挂起 send 在 netTask 里推进,短写返回已发送字节数,调用方须检查返回值 |
|
||
| fd 额度 | 与 TCP 共用 `NET_MAX_SOCKETS`(8),一个 UDP fd 占一个名额 |
|
||
| 收包不占 netTask | 没数据时登记挂起(本 fd 一个槽)立即让出 netTask,完成条件由 `net_udp_tick` 每轮判定——与挂起 recv/send 同一套账,netTask 不会被一次 UDP 等待钉住 |
|
||
|
||
---
|
||
|
||
## 7. I/O 多路复用
|
||
|
||
### 7.1 select 使用
|
||
|
||
`net_select()` 可以同时监控多个 Socket 的读写事件:
|
||
|
||
```c
|
||
#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` 结构体数组:
|
||
|
||
```c
|
||
#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 定义回调函数
|
||
|
||
```c
|
||
#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 注册回调
|
||
|
||
```c
|
||
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.31.100"` 的数值 = **`0x641FA8C0`**(内存里是 `C0 A8 1F 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 也走队列(2026-11-03 改造)**:`net_bind`(UDP) / `net_sendto` / `net_recvfrom` / `net_recvfrom_to` /
|
||
`net_udp_set_peer` 与 TCP 同一口径——投 `netMsgQueue`、在 `netTask` 里碰芯片、结果搭 32 位任务通知值回来。
|
||
改造前有、现在作废的两件事:①"仅限本层执行任务(netTask)调用"——`NET_ERR_NOT_IN_NETTASK`(-12)自本
|
||
改造起**没有任何返回路径**,常量保留只为老代码与历史文档对得上号,新代码不要拿它当判据;②"UDP 只在单任务
|
||
独占期可用"。源地址回传不占 `net_msg_t`(该结构按值拷入队列,AGENTS 禁止加"回传用"字段),而是经
|
||
`Drivers/BSP/NET/net_udp.c` 的 per-fd 出参槽写回调用方自己的内存,代价是**同一 fd 不允许两个任务同时收包**
|
||
(第二个拿 `NET_ERR_BUSY`)。芯片形态(服务器 / 客户端)与剥头规则见 §6.0,调用约束见 §6.3。
|
||
|
||
### 9.1 端口号转换
|
||
|
||
```c
|
||
uint16_t port = 8080;
|
||
|
||
/* 主机序 -> 网络序(发送前转换) */
|
||
uint16_t net_port = net_htons(port);
|
||
|
||
/* 网络序 -> 主机序(接收后转换) */
|
||
uint16_t host_port = net_ntohs(net_port);
|
||
```
|
||
|
||
### 9.2 IP 地址转换
|
||
|
||
```c
|
||
/* 字符串转网络序 IP */
|
||
uint32_t ip = net_inet_addr("192.168.31.100");
|
||
|
||
/* 网络序 IP 转字符串 */
|
||
char ip_str[16];
|
||
net_inet_ntoa(ip, ip_str); /* ip_str = "192.168.31.100" */
|
||
```
|
||
|
||
### 9.3 填充地址结构体
|
||
|
||
```c
|
||
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.31.100"); /* IP 转换 */
|
||
|
||
/* 如果要绑定任意地址 */
|
||
addr.sin_addr.s_addr = net_inet_addr("0.0.0.0");
|
||
/* 或者直接 */
|
||
addr.sin_addr.s_addr = INADDR_ANY;
|
||
```
|
||
|
||
### 9.4 读取地址信息
|
||
|
||
```c
|
||
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 - 网络初始化
|
||
|
||
```c
|
||
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 - 网络代次号
|
||
|
||
```c
|
||
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 - 状态轮询
|
||
|
||
```c
|
||
int net_poll(void);
|
||
```
|
||
|
||
| 参数 | 说明 |
|
||
|------|------|
|
||
| 返回值 | 有事件发生的 socket 数量 |
|
||
|
||
**注意**:必须在主循环中定期调用。
|
||
|
||
#### net_socket - 创建 Socket
|
||
|
||
```c
|
||
int net_socket(int domain, int type);
|
||
```
|
||
|
||
| 参数 | 说明 |
|
||
|------|------|
|
||
| `domain` | 地址族,仅支持 `AF_INET` |
|
||
| `type` | `SOCK_STREAM`(TCP) 或 `SOCK_DGRAM`(UDP) |
|
||
| 返回值 | socket 描述符 (0~7),失败返回 -1 |
|
||
|
||
#### net_bind - 绑定地址
|
||
|
||
```c
|
||
int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen);
|
||
```
|
||
|
||
| 参数 | 说明 |
|
||
|------|------|
|
||
| `sockfd` | socket 描述符 |
|
||
| `addr` | 地址结构体指针 |
|
||
| `addrlen` | 地址结构体长度 |
|
||
| 返回值 | 0 成功,-1 失败 |
|
||
|
||
#### net_listen - 开始监听
|
||
|
||
```c
|
||
int net_listen(int sockfd);
|
||
```
|
||
|
||
| 参数 | 说明 |
|
||
|------|------|
|
||
| `sockfd` | socket 描述符 |
|
||
| 返回值 | 0 成功,-1 失败 |
|
||
|
||
> **自动重监听**:`net_listen()` 对 TCP 监听 Socket 置 `auto_relisten = 1`。连接结束后 net 层自动回到 `LISTENING`,应用层无需手动 `net_close()` + `net_listen()`。
|
||
|
||
#### net_accept - 接受连接
|
||
|
||
```c
|
||
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 读,不走消息队列**)
|
||
|
||
```c
|
||
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 - 连接服务器
|
||
|
||
```c
|
||
int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen);
|
||
```
|
||
|
||
| 参数 | 说明 |
|
||
|------|------|
|
||
| `sockfd` | socket 描述符 |
|
||
| `addr` | 服务器地址 |
|
||
| `addrlen` | 地址长度 |
|
||
| 返回值 | 0 成功,-1 失败 |
|
||
|
||
#### net_send - 发送数据
|
||
|
||
```c
|
||
int net_send(int sockfd, const void *buf, int len, int flags);
|
||
```
|
||
|
||
| 参数 | 说明 |
|
||
|------|------|
|
||
| `sockfd` | socket 描述符 |
|
||
| `buf` | 数据缓冲区 |
|
||
| `len` | 数据长度 |
|
||
| `flags` | 通常为 0 |
|
||
| 返回值 | 实际发送字节数,失败返回 -1 |
|
||
|
||
#### net_recv - 接收数据(默认无限阻塞)
|
||
|
||
```c
|
||
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 全程零占用(消除队头阻塞)。内部 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 - 接收数据(非阻塞具名入口)
|
||
|
||
```c
|
||
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 - 设置阻塞模式属性
|
||
|
||
```c
|
||
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 - 接收数据(调用方指定超时,显式预算分片版)
|
||
|
||
```c
|
||
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 数据报
|
||
|
||
```c
|
||
int net_sendto(int sockfd, const void *buf, int len, int flags,
|
||
const struct net_sockaddr *dest_addr, int addrlen);
|
||
```
|
||
|
||
| 参数 | 说明 |
|
||
|------|------|
|
||
| `sockfd` | **必须已 `net_bind()`**(未 bind 回 -1 / `NET_ERR_NOTCONN`:芯片侧 Socket 还没 open) |
|
||
| `buf` / `len` | 一个数据报的载荷,单包 ≤ 1472 字节(手册 §9.2.4);更长由本层按 1KB 分块、复用 TCP 的挂起 send 在 netTask 里推进 |
|
||
| `flags` | 目前只消费 `NET_MSG_DONTWAIT`:芯片发送缓冲不可用时立即回 `NET_ERR_WOULDBLOCK`,不登记挂起 |
|
||
| `dest_addr` / `addrlen` | 非 NULL = 顺带把芯片 DesIP 指向它 → 芯片转**客户端模式**(此后只收该 IP 的包、收包不带信息头,见 §6.0);NULL = 不改目的地址,沿用当前形态 |
|
||
| 返回值 | > 0 = 已发送字节数(**可能短写**,须检查);-1 = 失败(`net_get_errno()`) |
|
||
|
||
任意任务可调用:请求入 `netMsgQueue`,芯片事务在 `netTask` 串行执行(2026-11-03 起,旧"仅限 netTask 直调"形态作废)。
|
||
|
||
#### net_recvfrom - 接收 UDP 数据报(无限等待形态)
|
||
|
||
```c
|
||
int net_recvfrom(int sockfd, void *buf, int len, int flags,
|
||
struct net_sockaddr *src_addr, int *addrlen);
|
||
```
|
||
|
||
- 一次调用 = 一个**完整数据报**:服务器模式自动剥 8 字节信息头并从中取源地址,客户端模式整段是载荷、源地址回填固定对端(§6.0)。
|
||
- `buf` 装不下时交付 `len` 字节并把剩余部分**读空丢弃**——半包留在芯片缓冲里,下一次收包会从上一包的尾巴开始解析。
|
||
- `src_addr` / `addrlen` 可为 NULL;非 NULL 时 `*addrlen` 给 `sizeof(struct net_sockaddr_in)`,回填的 `s_addr` 是网络序整数(§9.0)。
|
||
- ⚠ **无数据即无限等待**:受 IWDG 监管的任务改 `net_recvfrom_to()`,或先 `net_select()` / `net_can_read()` 门控。
|
||
- 同一 fd 已有别的任务在收 → 本调用回 -1 / `NET_ERR_BUSY`(数据报无法归属两个等待者)。
|
||
|
||
#### net_recvfrom_to - 接收 UDP 数据报(显式预算形态)
|
||
|
||
```c
|
||
int net_recvfrom_to(int sockfd, void *buf, int len, uint32_t timeout_ms,
|
||
struct net_sockaddr *src_addr, int *addrlen);
|
||
```
|
||
|
||
| 参数 | 说明 |
|
||
|------|------|
|
||
| `timeout_ms` | 等待预算(ms,**必须 > 0**;0 = 无限等待,与本接口"有界"的存在理由相反,直接拒 `NET_ERR_INVAL`——无限形态请用 `net_recvfrom`) |
|
||
| 返回值 | > 0 = 本包交付字节数(小于芯片内包长 = 被截断,多余部分已丢弃);**-2 = 预算内没等到**(**不是 errno**,不得据此判错误);-1 = 真错误 |
|
||
|
||
与 `net_recv_to()` 同一口径,是受 IWDG 监控任务的合法 UDP 收包入口;等待期本 fd 登记一个挂起槽、netTask 立即让出(不占 CPU、不占 netTask)。
|
||
|
||
#### net_udp_set_peer - 切换 UDP 芯片形态(不重开 Socket)
|
||
|
||
```c
|
||
int net_udp_set_peer(int sockfd, const struct net_sockaddr_in *peer);
|
||
```
|
||
|
||
| 参数 | 说明 |
|
||
|------|------|
|
||
| `peer` | 非 NULL = 客户端模式,DesIP/DesPort 指向该对端(只收它的包,收包无信息头);NULL = 回服务器模式(DesIP = `0xFFFFFFFF`、DesPort = 0,任意主机可发,收包带 8 字节信息头) |
|
||
| 返回值 | 0 成功;-1 失败(未 bind → `NET_ERR_NOTCONN`) |
|
||
|
||
只写两个寄存器、**不重开 Socket**,因此不清芯片缓冲;要连残留包一起丢掉就再 `net_bind()` 一次(重开形态)。
|
||
|
||
#### net_udp_get_mode - 读当前 UDP 芯片形态
|
||
|
||
```c
|
||
int net_udp_get_mode(int sockfd);
|
||
```
|
||
|
||
返回 `NET_UDP_MODE_SERVER` / `NET_UDP_MODE_CLIENT`,参数错回 -1。给诊断、单测和上层判读"要不要切回服务器模式"用;纯 RAM 读,不入队。
|
||
|
||
#### net_close - 关闭 Socket
|
||
|
||
```c
|
||
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 - 主动拆会话、保留监听(队头阻塞收口)
|
||
|
||
```c
|
||
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):CH395F close **无 RST 语义**——对端内核存活但不 close 时芯片停 FIN_WAIT_2 长尾,监听恢复受限于对端 close;对端配合则 TIME_WAIT 过渡 ~150ms → reconciler 形态④ relisten → 新连接接入。不配合场景兜底 = 芯片 TIMEOUT/keepalive 拆僵尸(E13 定案 30~90s)。详见 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-07
|
||
|
||
#### 自愈与查询 API
|
||
|
||
**net_sock_state — 查询 Socket 状态(只读快照)**
|
||
|
||
```c
|
||
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+端口)**
|
||
|
||
```c
|
||
int net_getsockname(int sockfd, struct net_sockaddr *addr, int *addrlen);
|
||
```
|
||
|
||
Socket 已创建或已绑定时可用;port 为 0(动态分配)时返回已分配的动态端口(49152 起)。
|
||
|
||
### 10.2 消息队列 API
|
||
|
||
#### net_process_messages - 处理消息队列
|
||
|
||
```c
|
||
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 - 发送数据(内部版本)
|
||
|
||
```c
|
||
int net_send_sock(net_sock_t *p_sock, const void *buf, int len);
|
||
```
|
||
|
||
直接操作 Socket 控制块进行发送,不经过消息队列。**只能在 `netTask` 中调用**,用于 `netTask` 内的网络业务逻辑。
|
||
|
||
#### net_recv_sock - 接收数据(内部版本)
|
||
|
||
```c
|
||
int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags);
|
||
```
|
||
|
||
直接操作 Socket 控制块进行接收,不经过消息队列。**只能在 `netTask` 中调用**。
|
||
|
||
#### net_close_sock - 关闭 Socket(内部版本)
|
||
|
||
```c
|
||
int net_close_sock(net_sock_t *p_sock);
|
||
```
|
||
|
||
直接操作 Socket 控制块关闭连接,不经过消息队列。**只能在 `netTask` 中调用**。
|
||
|
||
#### net_listen_locked - 开始监听(内部版本)
|
||
|
||
```c
|
||
int net_listen_locked(net_sock_t *p_sock);
|
||
```
|
||
|
||
直接操作 Socket 控制块开始监听,不经过消息队列。**只能在 `netTask` 中调用**(供 TCP 服务的代次号重建逻辑使用)。
|
||
|
||
### 10.3 辅助 API
|
||
|
||
#### net_set_event_cb - 注册事件回调
|
||
|
||
```c
|
||
int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg);
|
||
```
|
||
|
||
#### net_get_sock - 获取 Socket 控制块
|
||
|
||
```c
|
||
net_sock_t *net_get_sock(int sockfd);
|
||
```
|
||
|
||
#### net_get_errno - 获取错误码
|
||
|
||
```c
|
||
int net_get_errno(void);
|
||
```
|
||
|
||
### 10.4 字节序转换
|
||
|
||
```c
|
||
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
|
||
|
||
```c
|
||
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 操作宏
|
||
|
||
```c
|
||
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 自动重连
|
||
|
||
```c
|
||
#include "net_socket.h"
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
#define SERVER_IP "192.168.31.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.31.100", "255.255.255.0", "192.168.31.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 零占用(挂起模型);调用方累计总预算,收齐协议帧再返回:
|
||
|
||
```c
|
||
#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 文档契约)**
|
||
```c
|
||
int len = net_recv(sock, buf, sizeof(buf), 0);
|
||
if (len <= 0) {
|
||
/* 连接断开或错误(EOF / CONNRESET / 真错误,net_get_errno 可查) */
|
||
net_close(sock);
|
||
}
|
||
```
|
||
|
||
**方式二:非阻塞接收(`net_recv_nb`)**
|
||
```c
|
||
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 时序限制)。正确写法:
|
||
|
||
```c
|
||
} 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)。
|
||
|
||
**方式三:事件回调**
|
||
```c
|
||
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`。
|