# BSD Socket API 使用指南 ## 目录 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 个客户端连接 | | TCP Server(多连接) | 支持最多 7 个并发连接 | | UDP Client | 发送 UDP 数据报 | | UDP Server | 接收 UDP 数据报 | | select/poll | I/O 多路复用 | | 事件回调 | 异步事件通知 | ### 1.2 与 Linux Socket 的差异 | 特性 | Linux Socket | 本实现 | |------|--------------|--------| | Socket 数量 | 无限制 | 最多 8 个 | | TCP Server 多连接 | 无限制 | 最多 7 个并发连接 | | 非阻塞模式 | 支持 | 通过 `net_poll()` 实现 | | 错误码 | errno | `net_get_errno()` | | 头文件 | `` | `"net_socket.h"` | --- ## 2. 架构说明 ### 2.1 分层架构 ``` ┌──────────────────────────────────────────────────────────────┐ │ 应用层任务 (多个 FreeRTOS Task) │ │ net_send() / net_recv() / net_close() (线程安全封装) │ ├──────────────────────────────────────────────────────────────┤ │ 消息队列 (osMessageQueue) │ │ 由 CubeMX 生成,在 MX_FREERTOS_Init() 中创建 │ ├──────────────────────────────────────────────────────────────┤ │ netTask (网络任务线程) │ │ net_process_messages() + net_poll() — 串行化执行 │ │ net_send_sock() / net_recv_sock() / net_close_sock() │ ├──────────────────────────────────────────────────────────────┤ │ net_socket.c (BSD Socket API 核心) │ │ Socket 状态机 + CH395F 命令封装 │ ├──────────────────────────────────────────────────────────────┤ │ ch395f.c/h (底层 SPI 驱动) │ │ 所有 SPI 事务必须由 netTask 发起 │ ├──────────────────────────────────────────────────────────────┤ │ CH395F 硬件 (SPI2) │ └──────────────────────────────────────────────────────────────┘ ``` ### 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 实现 | ### 2.3 底层驱动改进(v2.1) ch395f.c/h 已完成以下优化: | 改进项 | 说明 | |--------|------| | 命令执行等待 | `open/close/listen/connect/disconnect` 所有长命令内部调用 `poll_cmd_status` 等待完成(20ms × 200 次 = 4s 超时) | | 全局中断 | 使用 `GET_GLOB_INT_STATUS_ALL`(2字节版,命令码 0x19)支持全部 8 个 Socket | | 错误码 | 与芯片手册完全对齐的 17 个状态码定义 | | FUN_PARA 标志 | `CH395F_FUN_PARA_TCP_SERVER` / `SOCKET_CLOSE` / `DISABLE_SEND_OK` 宏 | | 新增函数 | `ch395f_set_arp()` / `ch395f_set_ttl()` / `ch395f_get_unreach_info()` / `ch395f_poll_cmd_status()` | | KeepAlive | 参数单位为毫秒(非秒),必须为 500 的倍数 | ### 2.4 Socket 状态机 ``` net_socket() │ ▼ ┌──────────── CREATED ────────────┐ │ │ │ │ net_bind() │ │ │ │ │ ▼ │ │ BOUND ─────────────────┤ │ │ │ │ │ net_listen() net_connect() │ │ │ │ │ │ ▼ ▼ │ │ LISTENING CONNECTING │ │ │ │ │ │ net_accept() │ (连接成功) │ │ │ │ │ │ ▼ ▼ │ │ TCP_ACCEPT ESTABLISHED ◄──┤ │ │ │ │ │ (分配连接)│ net_send/recv() │ │ │ │ │ │ ▼ ▼ │ │ ESTABLISHED │ │ │ │ │ net_close() │ │ │ │ │ ▼ │ └────── CLOSED ──────────────────┘ ``` --- ## 3. 线程安全设计 ### 3.1 设计背景 NET 层(`net_socket.c` + `ch395f.c`)不是线程安全的: - Socket 控制块(`s_net_socks[]`)无互斥保护 - CH395F SPI 事务不重入(STM32 HAL SPI 无内部锁) - 状态机在多个任务中并发访问会崩溃 引入消息队列将所有的 CH395F 操作串行化到 `netTask` 中执行。 ### 3.2 消息队列模式 ``` App Task A App Task B netTask | | | net_send() net_recv() net_process_messages() | | + net_poll() ▼ ▼ │ ┌─────────┐ ┌─────────┐ │ │send msg │ │recv msg │ │ └────┬────┘ └────┬────┘ │ │ │ │ └─────────┬─────────┘ │ ▼ ▼ ┌──────────────────┐ ┌──────────────┐ │ 消息队列 │──────────│ 取出消息 │ │ osMessageQueue │ dequeue │ 执行操作 │ └──────────────────┘ │ 写结果 │ ▲ │ 通知调用方 │ │ └──────┬───────┘ │ ulTaskNotifyTake() │ │ (阻塞等待结果) │ xTaskNotifyGive() │ │ ┌─────┴──────┐ │ │ 返回结果给 │◄──────────────────────┘ │ 调用方 │ └────────────┘ ``` ### 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地址, 子网掩码, 网关 * 传入 NULL 使用 DHCP 自动获取 */ int ret = net_init("192.168.1.100", "255.255.255.0", "192.168.1.1"); if (ret != 0) { printf("网络初始化失败!\r\n"); return -1; } /* 其他初始化... */ while (1) { net_poll(); /* 必须在主循环中调用 */ /* 其他任务... */ } } ``` ### 4.3 主循环要求 **重要**:本项目使用 FreeRTOS 多任务架构,`net_poll()` 和 `net_process_messages()` 均在 `netTask`(FreeRTOS 线程)中定期调用,而非主循环。 - **主循环**:系统初始化完成后进入空循环,等待 `osKernelStart()` 启动调度器 - **netTask**:在任务函数中持续运行,每 10ms 调用一次 `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"); /* 创建 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(10); /* 10ms 周期 */ } } ``` --- ## 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, 0); 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. 开始监听(单连接模式 backlog=0) */ if (net_listen(listen_sock, 0) != 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)); /* 接收和发送数据 */ while (1) { len = net_recv(client_sock, buf, sizeof(buf) - 1, 0); if (len <= 0) { printf("连接断开\r\n"); break; } buf[len] = '\0'; printf("收到: %s\r\n", buf); /* 回显数据 */ net_send(client_sock, buf, len, 0); } net_close(client_sock); } } } ``` ### 5.2 TCP Server(多连接) 多连接模式使用 1 个监听 Socket + N 个数据 Socket,支持并发连接: ```c #include "net_socket.h" #include "net_select.h" #define MAX_CLIENTS 4 void tcp_server_multi(void) { int listen_sock; struct net_sockaddr_in addr; int i; /* 1. 创建监听 Socket */ listen_sock = net_socket(AF_INET, SOCK_STREAM, 0); if (listen_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("0.0.0.0"); if (net_bind(listen_sock, (struct net_sockaddr *)&addr, sizeof(addr)) != 0) { printf("绑定失败\r\n"); return; } /* 3. 启动监听(backlog 设为并发连接数) */ if (net_listen(listen_sock, MAX_CLIENTS) != 0) { printf("监听失败\r\n"); return; } printf("TCP Server 启动,支持 %d 个并发连接\r\n", MAX_CLIENTS); /* 4. 使用 select 等待事件 */ while (1) { net_fd_set readfds; struct net_timeval timeout; NET_FD_ZERO(&readfds); /* 监听 Socket 加入读集合 */ NET_FD_SET(listen_sock, &readfds); /* 数据 Socket(1~4)加入读集合 */ for (i = 1; i <= MAX_CLIENTS; i++) { NET_FD_SET(i, &readfds); } timeout.tv_sec = 0; timeout.tv_usec = 100000; /* 100ms 超时 */ int ret = net_select(listen_sock + MAX_CLIENTS + 1, &readfds, NULL, NULL, &timeout); if (ret > 0) { /* 检查数据 Socket */ for (i = 1; i <= MAX_CLIENTS; i++) { if (NET_FD_ISSET(i, &readfds)) { net_sock_t *sock = net_get_sock(i); if (sock != NULL && sock->state == NET_SOCK_STATE_ESTABLISHED) { char buf[256]; int len = net_recv(i, buf, sizeof(buf) - 1, 0); if (len > 0) { buf[len] = '\0'; printf("Socket %d: %s\r\n", i, buf); net_send(i, buf, len, 0); /* 回显 */ } else { printf("Socket %d 断开\r\n", i); } } } } } } } ``` ### 5.3 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, 0); 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, 0); 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, 0); 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); ``` | 参数 | 说明 | |------|------| | `ip` | IP 地址字符串,NULL 使用 DHCP | | `mask` | 子网掩码字符串,NULL 使用默认 | | `gateway` | 网关地址字符串,NULL 使用默认 | | 返回值 | 0 成功,-1 失败 | #### net_poll - 状态轮询 ```c int net_poll(void); ``` | 参数 | 说明 | |------|------| | 返回值 | 有事件发生的 socket 数量 | **注意**:必须在主循环中定期调用。 #### net_socket - 创建 Socket ```c int net_socket(int domain, int type, int protocol); ``` | 参数 | 说明 | |------|------| | `domain` | 地址族,仅支持 `AF_INET` | | `type` | `SOCK_STREAM`(TCP) 或 `SOCK_DGRAM`(UDP) | | `protocol` | 通常为 0 | | 返回值 | 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, int backlog); ``` | 参数 | 说明 | |------|------| | `sockfd` | socket 描述符 | | `backlog` | 最大并发连接数(0=单连接) | | 返回值 | 0 成功,-1 失败 | #### net_accept - 接受连接 ```c int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen); ``` | 参数 | 说明 | |------|------| | `sockfd` | 监听 socket 描述符 | | `addr` | 输出:客户端地址 | | `addrlen` | 输入输出:地址长度 | | 返回值 | 新 socket 描述符,失败返回 -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=对端关闭,-1=错误 | #### 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` 中调用**。 ### 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 Echo Server(多连接) ```c #include "net_socket.h" #include "net_select.h" #include #include #define SERVER_PORT 8080 #define MAX_CLIENTS 4 void tcp_echo_server(void) { int listen_sock; struct net_sockaddr_in addr; int i; /* 初始化网络 */ net_init("192.168.1.100", "255.255.255.0", "192.168.1.1"); /* 创建监听 Socket */ listen_sock = net_socket(AF_INET, SOCK_STREAM, 0); if (listen_sock < 0) { printf("创建 Socket 失败\r\n"); while(1); } /* 绑定 */ 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("0.0.0.0"); if (net_bind(listen_sock, (struct net_sockaddr *)&addr, sizeof(addr)) != 0) { printf("绑定失败\r\n"); while(1); } /* 监听 */ if (net_listen(listen_sock, MAX_CLIENTS) != 0) { printf("监听失败\r\n"); while(1); } printf("TCP Echo Server 启动,端口 %d\r\n", SERVER_PORT); /* 主循环 */ while (1) { net_fd_set readfds; struct net_timeval timeout; NET_FD_ZERO(&readfds); NET_FD_SET(listen_sock, &readfds); for (i = 1; i <= MAX_CLIENTS; i++) { NET_FD_SET(i, &readfds); } timeout.tv_sec = 0; timeout.tv_usec = 100000; int ret = net_select(listen_sock + MAX_CLIENTS + 1, &readfds, NULL, NULL, &timeout); if (ret > 0) { for (i = 1; i <= MAX_CLIENTS; i++) { if (NET_FD_ISSET(i, &readfds)) { net_sock_t *sock = net_get_sock(i); if (sock == NULL) continue; if (sock->state == NET_SOCK_STATE_ESTABLISHED) { char buf[256]; int len = net_recv(i, buf, sizeof(buf) - 1, 0); if (len > 0) { buf[len] = '\0'; printf("客户端 %d: %s\r\n", i, buf); net_send(i, buf, len, 0); } else { printf("客户端 %d 断开\r\n", i); net_close(i); } } } } } } } ``` ### 11.2 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"); while (1) { net_poll(); /* 尝试重连 */ if (sock < 0 && (HAL_GetTick() - last_try) >= reconnect_interval) { last_try = HAL_GetTick(); sock = net_socket(AF_INET, SOCK_STREAM, 0); 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_TCP_ACCEPT || 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 + 最多 7 个数据 Socket - TCP Client:最多 8 个 - UDP:最多 8 个 ### 12.3 TCP Server 多连接模式有什么要求? **模式工作原理:** - CH395F 多连接模式下,Socket 0 专职监听,Socket 1~7 由芯片自动分配 - 客户端连接时,CH395F 自动查找协议类型为 TCP、源端口与监听端口相同、处于关闭状态的 Socket - 找到后自动打开该 Socket 并分配连接,触发 CONNECT 中断 **硬件要求:** - CH395F 固件版本 >= 0x44(支持 Socket 4~7) ### 12.4 发送数据有什么限制? - 每次发送的数据长度不能超过 `NET_SEND_BUF_SIZE`(默认 4096 字节) - TCP 需要等待发送缓冲区空闲(`send_ready` 标志) - UDP 单次发送最大 1460 字节 ### 12.5 如何处理连接断开? **方式一:阻塞接收** ```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.6 select 和 poll 的区别? | 特性 | select | poll | |------|--------|------| | 描述符集合 | fd_set(位掩码) | pollfd 数组 | | 最大 fd 数 | 32 | 无限制 | | 使用方式 | NET_FD_SET 宏 | 直接操作结构体 | 两者功能相同,推荐使用 `net_select()`,更接近 Linux 编程习惯。 ### 12.7 错误码说明 | 错误码 | 宏定义 | 说明 | |--------|--------|------| | 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.8 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.9 故障排查 **问题:TCP Client 连接超时,无 CONNECT 中断** - 检查多连接模式下数据 Socket(1~7)是否设置了 `SET_PROTO_TCP` + `SET_SOUR_PORT`(与监听端口相同) - 如果不设置,CH395F 不知道哪些 Socket 可用于自动分配,TCP SYN 被静默丢弃 **问题:数据 Socket OPEN 返回 0x1B(已被使用)** - CH395F 多连接模式下,数据 Socket 由芯片自动管理,软件不应手动 `OPEN_SOCKET` - 只需 `SET_PROTO_TCP` + `SET_SOUR_PORT`,CH395F 在有连接时自动打开 **问题: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.10 线程安全问题 **问题:`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 内核已就绪。