diff --git a/README.md b/README.md index d0e2b3f1..b9985ed4 100644 --- a/README.md +++ b/README.md @@ -379,6 +379,7 @@ pre-commit run --all-files | `docs/项目启动顺序管理.md` | Event Flags 就绪屏障设计 | | `docs/系统墙钟时间维护说明.md` | RTC + 墙钟时间子系统 | | `docs/网络配置说明.md` | 网口设置页配置(UI 可改的 IP/掩码/网关/MAC:持久化 + 立即生效 + 页面操作) | +| `docs/BSD_Socket_API_使用指南.md` | BSD Socket API 使用指南(`net_socket` / `net_select`:线程安全 / 快速开始 / TCP / UDP / I/O 多路复用 / 事件回调 / API 参考 / 常见问题) | | `docs/UI 设计/UI_设计架构.md` | LVGL UI 系统架构(菜单 / 内容页 / 内存预算) | | `docs/UI 设计/菜单页面设计.md` | 菜单导航行为、数据流与内容页设计 | | `docs/UI 设计/UI_界面制作流程.md` | 参考图 → 设计规格 → LVGL 代码管线(P0~P4 门禁) | @@ -390,7 +391,7 @@ pre-commit run --all-files | `docs/代码规范/cppcheck使用说明.md` | cppcheck 规则、配置与误报抑制 | | `docs/测试说明/自动化测试流程.md` | 构建 → 烧录 → 串口校验闭环 | | `docs/测试说明/` | 各模块测试指南(CH395F / NAND / FTP / RTC / 串口调试) | -| `docs/测试开发标准流程.md` / `docs/测试文档模板规范.md` | 测试开发流程与文档模板 | +| `docs/测试说明/测试开发标准流程.md` / `docs/测试说明/测试文档模板规范.md` | 测试开发流程与文档模板 | | `docs/问题记录/` | 已知陷阱与解决方案(CH395F / NAND / RTC) | | `docs/芯片手册/` | 硬件数据手册 | diff --git a/docs/BSD_Socket_API_使用指南.md b/docs/BSD_Socket_API_使用指南.md new file mode 100644 index 00000000..72ec4dd2 --- /dev/null +++ b/docs/BSD_Socket_API_使用指南.md @@ -0,0 +1,1238 @@ +# BSD Socket API 使用指南 + +> **恢复说明**:本文档曾在提交 `4e9ac0d`(2026-08-28)中误删,2026-09-13 从 git 历史恢复,并同步当前 API(`net_init` 4 参数 / `net_socket` 2 参数 / 网络代次号 / `NET_NO_OS` 双端构建)。详见文末变更记录。 + +## 目录 + +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_poll()` 实现 | +| 错误码 | errno | `net_get_errno()` | +| 头文件 | `` | `"net_socket.h"` | + +--- + +## 2. 架构说明 + +### 2.1 分层架构 + +![BSD 网络栈分层架构](images/BSD网络栈分层.svg) + +### 2.2 文件说明 + +| 文件 | 说明 | +|------|------| +| `net_config.h` | 配置宏定义(最大 Socket 数、超时时间等) | +| `net_types.h` | 类型定义(地址结构、Socket 控制块等) | +| `net_socket.h` | BSD Socket API 头文件,含 `net_msg_t` 消息类型定义 | +| `net_socket.c` | API 核心实现,含消息队列处理、线程安全封装、内部 `_sock()` 版本 | +| `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 端 netTask 上电流程,见 `simulator/main.c`):`WSAStartup` → `net_cfg_load()`(网口配置 RAM 副本)→ `ui_net_sink_start(7003)`(RemoDispBus TCP 监听,与 MCU 端同端口)→ 主循环 `ui_net_sink_poll()`(非阻塞协议收发)。不调用 `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()
CH395F 中断轮询 + 状态机更新 +``` + +### 3.3 关键规则 + +| 规则 | 说明 | +|------|------| +| **`netTask` 外调用** | 使用 `net_send()` / `net_recv()` / `net_close()` 公共 API | +| **`netTask` 内调用** | 使用 `net_send_sock()` / `net_recv_sock()` / `net_close_sock()` 内部 API | +| **创建/绑定/监听** | 在 `main()` 初始化阶段完成(`osKernelStart()` 之前),无需队列 | +| **`net_poll()` 和 `net_process_messages()`** | 必须在 `netTask` 主循环中调用 | + +### 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` 引用 +- **队列满时**:`net_send()` / `net_recv()` 会阻塞等待,直到 `netTask` 处理完消息腾出空间 +- **`netTask` 不可向自身发消息**:`netTask` 中直接调 `net_send()` 会死锁(等待自己处理消息),必须在 `netTask` 中使用 `net_send_sock()` 等内部版本 +- **`net_socket()` / `net_bind()` / `net_listen()` 无需线程安全**:这些仅在初始化阶段调用,消息队列仅保护数据收发和连接关闭 + +--- + +## 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 在加载 net_cfg(Flash 网口配置)后调用 + */ + 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) + - 触发事件回调 + - 处理数据接收 + +```c +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 既做监听又做数据通信: + +```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.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. 等待连接建立 */ + while (1) + { + net_poll(); + + net_sock_t *sock_info = net_get_sock(sock); + if (sock_info != NULL && sock_info->state == 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 + +```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. 绑定本地端口(可选) */ + 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 + +```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. 循环接收数据 */ + 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 的读写事件: + +```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.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.1.100"); + +/* 网络序 IP 转字符串 */ +char ip_str[16]; +net_inet_ntoa(ip, ip_str); /* ip_str = "192.168.1.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.1.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(可循环重试) | + +#### 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=阻塞,`NET_MSG_DONTWAIT`=非阻塞 | +| 返回值 | 实际接收字节数;`0`=对端已关闭(net 层随即自动重监听);`-1`=错误(非阻塞且无数据时返回 `NET_ERR_WOULDBLOCK`) | + +#### net_sendto - 发送 UDP 数据 + +```c +int net_sendto(int sockfd, const void *buf, int len, int flags, + const struct net_sockaddr *dest_addr, int addrlen); +``` + +#### net_recvfrom - 接收 UDP 数据 + +```c +int net_recvfrom(int sockfd, void *buf, int len, int flags, + struct net_sockaddr *src_addr, int *addrlen); +``` + +#### net_close - 关闭 Socket + +```c +int net_close(int sockfd); +``` + +### 10.2 消息队列 API + +#### net_process_messages - 处理消息队列 + +```c +void net_process_messages(void); +``` + +处理其他任务通过 `net_send()` / `net_recv()` / `net_close()` 发送到消息队列的请求。必须在 `netTask` 主循环中调用。 + +#### 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 +#include + +#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) + { + net_sock_t *sock_info = net_get_sock(sock); + + if (sock_info != NULL && sock_info->state == 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 (sock_info != NULL && + (sock_info->state == NET_SOCK_STATE_CLOSED)) + { + /* 连接失败或断开 */ + printf("连接失败,准备重连...\r\n"); + net_close(sock); + sock = -1; + } + } + } +} +``` + +--- + +## 12. 常见问题 + +### 12.1 net_poll() 必须调用吗? + +**是的**。`net_poll()` 负责轮询 CH395F 的中断状态并更新所有 Socket 的状态。如果不调用,连接状态不会更新,数据也无法接收。 + +建议至少每 10ms 调用一次。 + +### 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 包发送) + +### 12.4 如何处理连接断开? + +**方式一:阻塞接收** +```c +int len = net_recv(sock, buf, sizeof(buf), 0); +if (len <= 0) { + /* 连接断开或错误 */ + net_close(sock); +} +``` + +**方式二:非阻塞接收** +```c +int len = net_recv(sock, buf, sizeof(buf), NET_MSG_DONTWAIT); +if (len < 0 && net_get_errno() == NET_ERR_WOULDBLOCK) { + /* 没有数据 */ +} else if (len <= 0) { + /* 连接断开 */ +} +``` + +**方式三:事件回调** +```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()` 可以在任意任务中调用吗?** + +可以。这些公共 API 通过消息队列将请求委托给 `netTask` 串行执行,自身阻塞等待结果,是线程安全的。 + +**问题:`netTask` 内可以直接调用 `net_send()` / `net_recv()` 吗?** + +不可以。`netTask` 中必须使用 `net_send_sock()` / `net_recv_sock()` 等内部版本,否则会死锁(任务向自身发送消息,永远无人处理)。内部版本直接操作 Socket 控制块,不经过消息队列。 + +**问题:`net_socket()` / `net_bind()` / `net_listen()` 线程安全吗?** + +这些函数仅在系统初始化阶段(`osKernelStart()` 之前)调用,不存在并发访问。消息队列仅保护数据收发和连接关闭操作。 + +**问题:消息队列由谁创建?在哪里创建?** + +由 CubeMX 在 `MX_FREERTOS_Init()` 中创建(`osMessageQueueNew(8, sizeof(net_msg_t), NULL)`),在 `osKernelInitialize()` 之后执行,确保 FreeRTOS 内核已就绪。 + +--- + +## 变更记录 + +| 日期 | 变更 | +|------|------| +| 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 概览与基本用法 | diff --git a/docs/images/BSD网络栈分层.svg b/docs/images/BSD网络栈分层.svg new file mode 100644 index 00000000..83206708 --- /dev/null +++ b/docs/images/BSD网络栈分层.svg @@ -0,0 +1,66 @@ + + +BSD Socket API 分层架构(NET 网络栈) +CH395F 以太网协议栈芯片 · 消息队列串行化全部网络操作至 netTask · FreeRTOS V10.3.1 + +应用层任务 +任意 FreeRTOS 任务可调用公共 API(线程安全) + +net_send() / net_recv() / net_close()(线程安全封装) + +net_select() / net_poll_events() / net_set_event_cb() + +消费方:lftpd(FTP :21)· time_sync(对时 :8888)· ui_net_sink(远程显示 :7003) + +消息队列 +osMessageQueue · CubeMX 生成,在 MX_FREERTOS_Init() 中创建 + +net_msg_t:send / recv / close / listen 四类请求 + +调用方:入队请求 → 阻塞等待结果(ulTaskNotifyTake) + +netTask 侧:出队 → 执行 → 写结果 → xTaskNotifyGive 通知 + +netTask(网络任务) +1ms 周期 · 所有 CH395F 操作在此串行执行 + +net_poll() — CH395F 中断轮询 + Socket 状态机 + 事件回调 + +net_process_messages() — 消费消息队列请求 + +net_send_sock() / net_recv_sock() / net_close_sock() / net_listen_locked()(内部版本) + +net_socket.c(BSD Socket API 核心) +Socket 状态机 + CH395F 命令封装 + +8 个 Socket 控制块 s_net_socks[](状态机,无锁) + +CH395F 命令序列封装(OPEN / TCP_LISTEN / CONNECT / SEND ...) + +网络代次号(net_get_generation / net_generation_inc,服务自愈) + +ch395f.c/h(底层 SPI 驱动) +SPI2 · 不可重入 + +ch395f_spi_begin() / ch395f_spi_end() — 包裹每次 SPI 事务 + +所有 SPI 事务仅能由 netTask 发起(独占,无重入) + +CH395F 硬件 +以太网协议栈芯片 + +CH395F(SPI2:PB12 CS / PB13 SCK / PB14 SDO / PB15 SDI) + +INT# 中断输出(低电平有效 · GPIO 轮询,无 EXTI) + + + + + + + + + + +上层只依赖相邻下层(单向);消息队列 + netTask 串行化全部 CH395F 操作;模拟器端定义 NET_NO_OS 裁掉队列/回调(Winsock 后端) + diff --git a/tools/arch_diagram/gen_net_stack_svg.py b/tools/arch_diagram/gen_net_stack_svg.py new file mode 100644 index 00000000..acf8d634 --- /dev/null +++ b/tools/arch_diagram/gen_net_stack_svg.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""生成 docs/images/BSD网络栈分层.svg —— NET 网络栈分层架构图(纯标准库,无需第三方依赖)。 + +由 gen_arch_svg.py 复制改数据区:BSD Socket API 六层结构 +(docs/BSD_Socket_API_使用指南.md §2.1): +应用层任务 → 消息队列 → netTask → net_socket 核心 → ch395f 驱动 → CH395F 硬件。 + +用法(仓库根目录,任意 python3): + python tools/arch_diagram/gen_net_stack_svg.py +输出: + docs/images/BSD网络栈分层.svg +改架构时:编辑本文件顶部的 LAYERS / ROWS 数据后重跑即可。 +""" +import html +import os + +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +OUT = os.path.join(ROOT, "docs", "images", "BSD网络栈分层.svg") + +# ----------------------------- 画布与尺寸参数 ----------------------------- +W = 1200 +PAD = 26 +TITLE_FONT = 20 +SUB_FONT = 13 +BAND_TITLE_FONT = 17 +BAND_SUB_FONT = 12 +CHIP_FONT = 13 + +CHIP_H = 38 # 芯片盒高 +CHIP_GAP = 10 # 芯片水平间距 +CHIP_ROW_GAP = 10 # 芯片行间距 +CHIP_PAD = 16 # 芯片内左右留白 +BAND_PAD_X = 18 # 框内左右留白 +BAND_TITLE_H = 38 # 框内标题区高 +BAND_PAD_BOTTOM = 16 +BAND_H_GAP = 26 # 同一行内并列框的间距 +BAND_V_GAP = 44 # 行间距(放箭头) +TITLE_H = 78 # 顶部标题区高 + +FONT = "Microsoft YaHei, Segoe UI, PingFang SC, sans-serif" + +# ----------------------------- 架构数据(改这里) ----------------------------- +L_APP = dict( + title="应用层任务", sub="任意 FreeRTOS 任务可调用公共 API(线程安全)", color="#2563eb", bg="#eff6ff", + chips=["net_send() / net_recv() / net_close()(线程安全封装)", + "net_select() / net_poll_events() / net_set_event_cb()", + "消费方:lftpd(FTP :21)· time_sync(对时 :8888)· ui_net_sink(远程显示 :7003)"], +) +L_QUEUE = dict( + title="消息队列", sub="osMessageQueue · CubeMX 生成,在 MX_FREERTOS_Init() 中创建", color="#7c3aed", bg="#f5f3ff", + chips=["net_msg_t:send / recv / close / listen 四类请求", + "调用方:入队请求 → 阻塞等待结果(ulTaskNotifyTake)", + "netTask 侧:出队 → 执行 → 写结果 → xTaskNotifyGive 通知"], +) +L_NETTASK = dict( + title="netTask(网络任务)", sub="1ms 周期 · 所有 CH395F 操作在此串行执行", color="#0d9488", bg="#f0fdfa", + chips=["net_poll() — CH395F 中断轮询 + Socket 状态机 + 事件回调", + "net_process_messages() — 消费消息队列请求", + "net_send_sock() / net_recv_sock() / net_close_sock() / net_listen_locked()(内部版本)"], +) +L_CORE = dict( + title="net_socket.c(BSD Socket API 核心)", sub="Socket 状态机 + CH395F 命令封装", color="#16a34a", bg="#f0fdf4", + chips=["8 个 Socket 控制块 s_net_socks[](状态机,无锁)", + "CH395F 命令序列封装(OPEN / TCP_LISTEN / CONNECT / SEND ...)", + "网络代次号(net_get_generation / net_generation_inc,服务自愈)"], +) +L_DRV = dict( + title="ch395f.c/h(底层 SPI 驱动)", sub="SPI2 · 不可重入", color="#d97706", bg="#fffbeb", + chips=["ch395f_spi_begin() / ch395f_spi_end() — 包裹每次 SPI 事务", + "所有 SPI 事务仅能由 netTask 发起(独占,无重入)"], +) +L_HW = dict( + title="CH395F 硬件", sub="以太网协议栈芯片", color="#e11d48", bg="#fff1f2", + chips=["CH395F(SPI2:PB12 CS / PB13 SCK / PB14 SDO / PB15 SDI)", + "INT# 中断输出(低电平有效 · GPIO 轮询,无 EXTI)"], +) + +# 每一行可放多个"同级并列"的层(上层只依赖下方各层) +ROWS = [ + [L_APP], + [L_QUEUE], + [L_NETTASK], + [L_CORE], + [L_DRV], + [L_HW], +] + +FOOT = "上层只依赖相邻下层(单向);消息队列 + netTask 串行化全部 CH395F 操作;模拟器端定义 NET_NO_OS 裁掉队列/回调(Winsock 后端)" + + +# ----------------------------- 工具函数 ----------------------------- +def esc(s): + return html.escape(s, quote=True) + + +def text_w(s, fs): + """粗略估算文本像素宽:CJK/全角/圈码/箭头 ≈ 1.0em,其余 ≈ 0.58em。 + + 取偏大估计(宁可芯片略宽,不可文字溢出盒外)。 + """ + w = 0.0 + for ch in s: + w += fs * (1.0 if ord(ch) >= 0x2460 else 0.58) + return w + + +def chip_w(label): + return text_w(label, CHIP_FONT) + 2 * CHIP_PAD + + +def wrap_chips(chips, avail): + rows, cur, cur_w = [], [], 0.0 + for c in chips: + cw = chip_w(c) + need = cw if not cur else cur_w + CHIP_GAP + cw + if cur and need > avail: + rows.append(cur) + cur, cur_w = [c], cw + else: + cur.append(c) + cur_w = need + if cur: + rows.append(cur) + return rows + + +# ----------------------------- 生成 ----------------------------- +def build(): + out = [] + add = out.append + + # 布局:逐行排布(行内可并列多个框,行高取该行最高框) + placements = [] # (layer, chip_rows, x, y, w, h) + y = TITLE_H + for row in ROWS: + n = len(row) + bw = (W - 2 * PAD - BAND_H_GAP * (n - 1)) // n + bands = [] + for i, layer in enumerate(row): + bx = PAD + i * (bw + BAND_H_GAP) + avail = bw - 2 * BAND_PAD_X + rows = wrap_chips(layer["chips"], avail) + bh = BAND_TITLE_H + len(rows) * CHIP_H + (len(rows) - 1) * CHIP_ROW_GAP + BAND_PAD_BOTTOM + bands.append([layer, rows, bx, y, bw, bh]) + row_h = max(b[5] for b in bands) + for b in bands: + b[5] = row_h # 同行等高(视觉"同级") + placements.extend(bands) + y += row_h + BAND_V_GAP + total_h = y - BAND_V_GAP + 54 + + add('' % (W, total_h)) + add('%s' + % (PAD, 40, TITLE_FONT, esc("BSD Socket API 分层架构(NET 网络栈)"))) + add('%s' + % (PAD, 62, SUB_FONT, + esc("CH395F 以太网协议栈芯片 · 消息队列串行化全部网络操作至 netTask · FreeRTOS V10.3.1"))) + + for (layer, rows, bx, by, bw, bh) in placements: + # 框背景 + 边框 + add('' % (bx, by, bw, bh, layer["bg"], layer["color"])) + # 标题(层名 + 目录/职责说明) + add('%s' + % (bx + BAND_PAD_X, by + 26, BAND_TITLE_FONT, layer["color"], esc(layer["title"]))) + tw = text_w(layer["title"], BAND_TITLE_FONT) + add('%s' + % (bx + BAND_PAD_X + tw + 12, by + 26, BAND_SUB_FONT, esc(layer["sub"]))) + # 芯片 + cy = by + BAND_TITLE_H + for row in rows: + cx = bx + BAND_PAD_X + for c in row: + cw = chip_w(c) + add('' % (cx, cy, cw, CHIP_H, layer["color"])) + add('%s' + % (cx + cw / 2, cy + CHIP_H / 2 + CHIP_FONT * 0.35, CHIP_FONT, esc(c))) + cx += cw + CHIP_GAP + cy += CHIP_H + CHIP_ROW_GAP + + # 行间箭头:每个上层框向下引一条(顶层两框 → 两条,体现"都依赖下层") + row_first = [] # 每行首个 placement 的下标 + idx = 0 + for row in ROWS: + row_first.append(idx) + idx += len(row) + for r in range(len(ROWS) - 1): + y0 = placements[row_first[r]][3] + placements[row_first[r]][5] + 6 + y1 = placements[row_first[r + 1]][3] - 8 + for i in range(row_first[r], row_first[r] + len(ROWS[r])): + (_, _, bx, _, bw, _) = placements[i] + ax = bx + bw / 2 + add('' % (ax, y0, ax, y1 - 7)) + add('' % (ax, y1)) + + add('%s' + % (PAD, total_h - 18, esc(FOOT))) + + svg = ('\n%s\n\n' + % (W, total_h, W, total_h, FONT, "\n".join(out))) + return svg, total_h + + +def main(): + svg, h = build() + with open(OUT, "w", encoding="utf-8", newline="\n") as f: + f.write(svg) + import xml.etree.ElementTree as ET + ET.fromstring(svg) + print("OK %d bytes, canvas %dx%d -> %s" % (len(svg.encode("utf-8")), W, h, OUT)) + + +if __name__ == "__main__": + main()