35 KiB
BSD Socket API 使用指南
目录
1. 概述
本项目基于 CH395F 以太网协议栈芯片,提供了一套兼容 Linux BSD Socket API 的网络编程接口。这套接口屏蔽了底层硬件细节,让嵌入式网络编程与标准 Linux 网络编程保持一致。
1.1 支持的功能
| 功能 | 说明 |
|---|---|
| TCP Client | 主动连接远程服务器 |
| TCP Server(单连接) | 支持 1 个客户端连接 |
| UDP Client | 发送 UDP 数据报 |
| UDP Server | 接收 UDP 数据报 |
| select/poll | I/O 多路复用 |
| 事件回调 | 异步事件通知 |
1.2 与 Linux Socket 的差异
| 特性 | Linux Socket | 本实现 |
|---|---|---|
| Socket 数量 | 无限制 | 最多 8 个 |
| 非阻塞模式 | 支持 | 通过 net_poll() 实现 |
| 错误码 | errno | net_get_errno() |
| 头文件 | <sys/socket.h> |
"net_socket.h" |
2. 架构说明
2.1 分层架构
┌──────────────────────────────────────────────────────────────┐
│ 应用层任务 (多个 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.4 Socket 状态机
net_socket()
│
▼
┌──────────── CREATED ────────────┐
│ │ │
│ net_bind() │
│ │ │
│ ▼ │
│ BOUND ─────────────────┤
│ │ │ │
│ net_listen() net_connect() │
│ │ │ │
│ ▼ ▼ │
│ LISTENING CONNECTING │
│ │ │ │
│ net_accept() │ (连接成功) │
│ │ │ │
│ ▼ ▼ │
│ ESTABLISHED ◄──────────────┘
│ │ │
│ net_accept() 返回监听 Socket 自身 │
│ │ │
│ net_send/recv() │
│ │ │
│ 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 中的网络业务逻辑,避免向自身发送消息造成死锁:
/* 在 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 包含头文件
#include "net_socket.h"
#include "net_select.h"
4.2 初始化网络
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)
- 触发事件回调
- 处理数据接收
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 既做监听又做数据通信:
#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. 开始监听 */
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));
/* 接收和发送数据 */
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 Client
#include "net_socket.h"
void tcp_client(void)
{
int sock;
struct net_sockaddr_in addr;
char buf[256];
int len;
/* 1. 创建 Socket */
sock = net_socket(AF_INET, SOCK_STREAM, 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
#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
#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 的读写事件:
#include "net_select.h"
void select_example(void)
{
int sock1, sock2;
net_fd_set readfds;
struct net_timeval timeout;
/* 创建并连接两个 Socket ... */
while (1)
{
NET_FD_ZERO(&readfds);
NET_FD_SET(sock1, &readfds);
NET_FD_SET(sock2, &readfds);
timeout.tv_sec = 0;
timeout.tv_usec = 500000; /* 500ms 超时 */
int ret = net_select(sock2 + 1, &readfds, NULL, NULL, &timeout);
if (ret > 0) {
if (NET_FD_ISSET(sock1, &readfds)) {
char buf[128];
int len = net_recv(sock1, buf, sizeof(buf), 0);
/* 处理数据... */
}
if (NET_FD_ISSET(sock2, &readfds)) {
char buf[128];
int len = net_recv(sock2, buf, sizeof(buf), 0);
/* 处理数据... */
}
} else if (ret == 0) {
/* 超时 */
}
}
}
7.2 poll 使用
net_poll_events() 使用 pollfd 结构体数组:
#include "net_select.h"
void poll_example(void)
{
net_pollfd fds[2];
fds[0].fd = sock1;
fds[0].events = NET_POLLIN;
fds[1].fd = sock2;
fds[1].events = NET_POLLIN;
while (1)
{
int ret = net_poll_events(fds, 2, 500); /* 500ms 超时 */
if (ret > 0) {
if (fds[0].revents & NET_POLLIN) {
char buf[128];
int len = net_recv(fds[0].fd, buf, sizeof(buf), 0);
/* 处理数据... */
}
if (fds[1].revents & NET_POLLIN) {
char buf[128];
int len = net_recv(fds[1].fd, buf, sizeof(buf), 0);
/* 处理数据... */
}
}
}
}
7.3 事件掩码说明
| 事件 | 说明 |
|---|---|
NET_POLLIN |
有数据可读,或有新连接到达(监听 Socket) |
NET_POLLOUT |
发送缓冲区有空间,可以写入数据 |
NET_POLLERR |
发生错误 |
NET_POLLHUP |
连接挂起(对端关闭) |
NET_POLLNVAL |
无效的文件描述符 |
8. 事件回调机制
可以通过注册回调函数,异步获取 Socket 事件通知:
8.1 定义回调函数
#include "net_socket.h"
void my_event_callback(int sockfd, net_event_t event, void *arg)
{
switch (event)
{
case NET_EVENT_CONNECTED:
printf("Socket %d: 连接建立\r\n", sockfd);
break;
case NET_EVENT_DISCONNECTED:
printf("Socket %d: 连接断开\r\n", sockfd);
break;
case NET_EVENT_DATA_RECEIVED:
printf("Socket %d: 收到数据\r\n", sockfd);
/* 注意:这里只是通知,需要调用 net_recv() 读取 */
break;
case NET_EVENT_SEND_COMPLETE:
printf("Socket %d: 发送完成\r\n", sockfd);
break;
case NET_EVENT_TIMEOUT:
printf("Socket %d: 操作超时\r\n", sockfd);
break;
case NET_EVENT_ERROR:
printf("Socket %d: 发生错误\r\n", sockfd);
break;
}
}
8.2 注册回调
void event_callback_example(void)
{
int sock;
/* 创建 Socket ... */
/* 注册事件回调 */
net_set_event_cb(sock, my_event_callback, NULL);
/* 主循环 */
while (1)
{
net_poll();
}
}
9. 字节序处理
网络协议使用大端序(网络字节序),而 STM32 使用小端序(主机字节序)。发送和接收数据时需要进行字节序转换。
9.1 端口号转换
uint16_t port = 8080;
/* 主机序 -> 网络序(发送前转换) */
uint16_t net_port = net_htons(port);
/* 网络序 -> 主机序(接收后转换) */
uint16_t host_port = net_ntohs(net_port);
9.2 IP 地址转换
/* 字符串转网络序 IP */
uint32_t ip = net_inet_addr("192.168.1.100");
/* 网络序 IP 转字符串 */
char ip_str[16];
net_inet_ntoa(ip, ip_str); /* ip_str = "192.168.1.100" */
9.3 填充地址结构体
struct net_sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = net_htons(8080); /* 端口转换 */
addr.sin_addr.s_addr = net_inet_addr("192.168.1.100"); /* IP 转换 */
/* 如果要绑定任意地址 */
addr.sin_addr.s_addr = net_inet_addr("0.0.0.0");
/* 或者直接 */
addr.sin_addr.s_addr = INADDR_ANY;
9.4 读取地址信息
struct net_sockaddr_in client_addr;
int addr_len = sizeof(client_addr);
int client_sock = net_accept(listen_sock,
(struct net_sockaddr *)&client_addr, &addr_len);
if (client_sock >= 0)
{
/* 转换端口到主机序 */
uint16_t port = net_ntohs(client_addr.sin_port);
/* 转换 IP 到字符串 */
char ip_str[16];
net_inet_ntoa(client_addr.sin_addr.s_addr, ip_str);
printf("客户端: %s:%d\r\n", ip_str, port);
}
10. API 参考
10.1 核心 API
net_init - 网络初始化
int net_init(const char *ip, const char *mask, const char *gateway);
| 参数 | 说明 |
|---|---|
ip |
IP 地址字符串,NULL 使用 DHCP |
mask |
子网掩码字符串,NULL 使用默认 |
gateway |
网关地址字符串,NULL 使用默认 |
| 返回值 | 0 成功,-1 失败 |
net_poll - 状态轮询
int net_poll(void);
| 参数 | 说明 |
|---|---|
| 返回值 | 有事件发生的 socket 数量 |
注意:必须在主循环中定期调用。
net_socket - 创建 Socket
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 - 绑定地址
int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen);
| 参数 | 说明 |
|---|---|
sockfd |
socket 描述符 |
addr |
地址结构体指针 |
addrlen |
地址结构体长度 |
| 返回值 | 0 成功,-1 失败 |
net_listen - 开始监听
int net_listen(int sockfd);
| 参数 | 说明 |
|---|---|
sockfd |
socket 描述符 |
| 返回值 | 0 成功,-1 失败 |
net_accept - 接受连接
int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen);
| 参数 | 说明 |
|---|---|
sockfd |
监听 socket 描述符 |
addr |
输出:客户端地址 |
addrlen |
输入输出:地址长度 |
| 返回值 | 新 socket 描述符,失败返回 -1 |
net_connect - 连接服务器
int net_connect(int sockfd, const struct net_sockaddr *addr, int addrlen);
| 参数 | 说明 |
|---|---|
sockfd |
socket 描述符 |
addr |
服务器地址 |
addrlen |
地址长度 |
| 返回值 | 0 成功,-1 失败 |
net_send - 发送数据
int net_send(int sockfd, const void *buf, int len, int flags);
| 参数 | 说明 |
|---|---|
sockfd |
socket 描述符 |
buf |
数据缓冲区 |
len |
数据长度 |
flags |
通常为 0 |
| 返回值 | 实际发送字节数,失败返回 -1 |
net_recv - 接收数据
int net_recv(int sockfd, void *buf, int len, int flags);
| 参数 | 说明 |
|---|---|
sockfd |
socket 描述符 |
buf |
接收缓冲区 |
len |
缓冲区大小 |
flags |
0=阻塞,NET_MSG_DONTWAIT=非阻塞 |
| 返回值 | 实际接收字节数,0=对端关闭,-1=错误 |
net_sendto - 发送 UDP 数据
int net_sendto(int sockfd, const void *buf, int len, int flags,
const struct net_sockaddr *dest_addr, int addrlen);
net_recvfrom - 接收 UDP 数据
int net_recvfrom(int sockfd, void *buf, int len, int flags,
struct net_sockaddr *src_addr, int *addrlen);
net_close - 关闭 Socket
int net_close(int sockfd);
10.2 消息队列 API
net_process_messages - 处理消息队列
void net_process_messages(void);
处理其他任务通过 net_send() / net_recv() / net_close() 发送到消息队列的请求。必须在 netTask 主循环中调用。
net_send_sock - 发送数据(内部版本)
int net_send_sock(net_sock_t *p_sock, const void *buf, int len);
直接操作 Socket 控制块进行发送,不经过消息队列。只能在 netTask 中调用,用于 netTask 内的网络业务逻辑。
net_recv_sock - 接收数据(内部版本)
int net_recv_sock(net_sock_t *p_sock, void *buf, int len, int flags);
直接操作 Socket 控制块进行接收,不经过消息队列。只能在 netTask 中调用。
net_close_sock - 关闭 Socket(内部版本)
int net_close_sock(net_sock_t *p_sock);
直接操作 Socket 控制块关闭连接,不经过消息队列。只能在 netTask 中调用。
10.3 辅助 API
net_set_event_cb - 注册事件回调
int net_set_event_cb(int sockfd, net_event_cb_t cb, void *arg);
net_get_sock - 获取 Socket 控制块
net_sock_t *net_get_sock(int sockfd);
net_get_errno - 获取错误码
int net_get_errno(void);
10.4 字节序转换
uint16_t net_htons(uint16_t hostshort); /* 主机序 -> 网络序 */
uint16_t net_ntohs(uint16_t netshort); /* 网络序 -> 主机序 */
uint32_t net_htonl(uint32_t hostlong); /* 主机序 -> 网络序 */
uint32_t net_ntohl(uint32_t netlong); /* 网络序 -> 主机序 */
uint32_t net_inet_addr(const char *cp); /* 字符串 -> 网络序 IP */
char *net_inet_ntoa(uint32_t addr, char *buf); /* 网络序 IP -> 字符串 */
10.5 select/poll
int net_select(int nfds, net_fd_set *readfds, net_fd_set *writefds,
net_fd_set *exceptfds, net_timeval *timeout);
int net_poll_events(net_pollfd *fds, int nfds, int timeout);
10.6 fd_set 操作宏
NET_FD_ZERO(fdset) /* 清空集合 */
NET_FD_SET(fd, fdset) /* 添加 fd */
NET_FD_CLR(fd, fdset) /* 移除 fd */
NET_FD_ISSET(fd, fdset) /* 检查 fd 是否在集合中 */
11. 完整示例
11.1 TCP Client 自动重连
#include "net_socket.h"
#include <stdio.h>
#include <string.h>
#define SERVER_IP "192.168.1.200"
#define SERVER_PORT 8080
void tcp_client_reconnect(void)
{
int sock = -1;
struct net_sockaddr_in addr;
uint32_t last_try = 0;
uint32_t reconnect_interval = 5000; /* 5 秒重连间隔 */
/* 初始化网络 */
net_init("192.168.1.100", "255.255.255.0", "192.168.1.1");
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_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 如何处理连接断开?
方式一:阻塞接收
int len = net_recv(sock, buf, sizeof(buf), 0);
if (len <= 0) {
/* 连接断开或错误 */
net_close(sock);
}
方式二:非阻塞接收
int len = net_recv(sock, buf, sizeof(buf), NET_MSG_DONTWAIT);
if (len < 0 && net_get_errno() == NET_ERR_WOULDBLOCK) {
/* 没有数据 */
} else if (len <= 0) {
/* 连接断开 */
}
方式三:事件回调
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 内核已就绪。