/* * 模块名称:UDP 数据面执行器与挂起槽(netTask 串行化) * 文件说明:UDP 队列化改造(2026-11-03)的执行侧。公开 API 在 net_socket.c 里只做 * "校验参数 → 装 net_msg_t 入队 → 等任务通知值",真正的芯片事务全部 * 在本文件的 *_exec 函数里执行(netTask 上下文,与 TCP 同一根 SPI 的 * 唯一持有者)。源地址出参经本文件的 per-fd 槽位回到调用方内存。 * 架构位置:docs/网络/BSD_Socket_API_使用指南.md §6(UDP 两种芯片模式)+ * docs/网络/Net层阻塞模型架构设计.md(挂起模型与通知值回传) * 依赖:net_socket.h(net_get_sock / net_poll / net_set_errno / 动态端口)、 * net_pending.h(NET_PENDING_REGISTERED / NET_SEND_CHUNK_MAX / 挂起 send 表)、 * ch395f.h(芯片命令)、main.h(HAL_GetTick) * 作者:super_agent_bot * 创建日期:2026-11-03 * 修改记录: * 2026-11-03 super_agent_bot 创建:UDP 队列化改造(SNTP 客户端的前置) */ #include #include #include "FreeRTOS.h" #include "task.h" #include "net_types.h" #include "net_socket.h" #include "net_udp.h" #include "net_pending.h" #include "ch395f.h" #include "dbg_log.h" #include "main.h" /* * 私有常量定义区 */ /* 服务器模式信息头长度(手册 §9.2.4:[0-1] 数据长度、[2-3] 源端口、[4-7] 源 IP) */ #define NET_UDP_HDR_LEN 8U /* 源端口在信息头内的偏移 */ #define NET_UDP_HDR_PORT_OFF 2U /* 源 IP 在信息头内的偏移 */ #define NET_UDP_HDR_IP_OFF 4U /* 截断数据报的排空粒度(字节):读空不丢后续包,只在调用方缓冲偏小时走这条路 */ #define NET_UDP_DRAIN_CHUNK 64U /* * 私有类型定义区 */ /* * 一次 UDP 接收请求的槽位(每 fd 至多一个) * armed = 1:调用方已登记源地址出参(消息可能还在队列里,也可能正在执行) * pending = 1:已登记挂起等待(数据/到点/关闭三者之一将结束它) * 两个标志的差别是"谁欠调用方一个回执":armed 期间回执还没发;pending 期间 * 连即时回执都还没生成,结束动作由 net_udp_tick 负责。 */ typedef struct { uint8_t armed; /* 1 = 槽位被占用(禁止第二个接收请求入队) */ uint8_t pending; /* 1 = 挂起等待中 */ struct net_sockaddr *src_addr; /* 调用方源地址出参(可为 NULL) */ int *addrlen; /* 调用方长度出参(可为 NULL) */ void *buf; /* 载荷缓冲(调用方持有,须有效至回执) */ int len; /* 载荷缓冲容量 */ uint32_t deadline; /* 结束时刻(0 = 无限等待,同 net_pending 约定) */ TaskHandle_t caller; /* 回执目标 */ } net_udp_req_t; /* * 私有变量定义区 */ /* per-fd 接收槽位表:与 net_pending(TCP recv)/ net_send_pending 各自独立, * 同一 fd 上三者互不阻塞(UDP 只用本表 + send 表) */ static net_udp_req_t s_net_udp[NET_MAX_SOCKETS]; /* * 私有函数声明区 */ static void net_udp_src_from_header(net_sock_t *p_sock, const uint8_t *hdr, struct net_sockaddr *src_addr, int *addrlen); static void net_udp_src_from_peer(net_sock_t *p_sock, struct net_sockaddr *src_addr, int *addrlen); static int net_udp_datagram_read(int sockfd, net_sock_t *p_sock, void *buf, int len, struct net_sockaddr *src_addr, int *addrlen, int avail); static void net_udp_req_complete(int sockfd, int result); /* * 公共函数实现(调用方上下文) */ /* * 函数功能:登记本次接收的源地址出参(调用方上下文,入队前) * 入口参数:sockfd - Socket 描述符 (0~7) int * src_addr - 源地址出参指针(可为 NULL) struct net_sockaddr * * addrlen - 出参长度指针(可为 NULL) int * * 出口参数:无 * 返回值:0 - 成功;-1 - 失败(NET_ERR_BADF / NET_ERR_BUSY) int * 限定条件:任意上下文 * 函数说明:临界区只包"查占位"三步,不碰芯片;占位成功后本 fd 的第二个接收 * 请求被拒(BUSY)。同一 fd 的并发接收在数据报语义上无法区分归属, * 拒绝比交错投递安全(TCP 挂起 recv 同一守卫,见 net_pending_register)。 */ int net_udp_outparam_arm(int sockfd, struct net_sockaddr *src_addr, int *addrlen) { if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { return NET_ERR_RETURN_1(NET_ERR_BADF); } taskENTER_CRITICAL(); if (s_net_udp[sockfd].armed != 0U) { taskEXIT_CRITICAL(); return NET_ERR_RETURN_1(NET_ERR_BUSY); } s_net_udp[sockfd].armed = 1U; s_net_udp[sockfd].src_addr = src_addr; s_net_udp[sockfd].addrlen = addrlen; taskEXIT_CRITICAL(); return 0; } /* * 函数功能:归还源地址出参槽(调用方上下文,入队失败这条出口) * 入口参数:sockfd - Socket 描述符 (0~7) int * 出口参数:无 * 返回值:无 * 限定条件:任意上下文;仅在未拿到 netTask 回执的失败路径调用 * 函数说明:netTask 的结算路径自己清槽(net_udp_req_complete),本函数只服务 * 调用方侧"消息没投出去"的清理,防槽位泄漏把该 fd 永久钉成 BUSY。 */ void net_udp_outparam_disarm(int sockfd) { if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { return; } taskENTER_CRITICAL(); s_net_udp[sockfd].armed = 0U; s_net_udp[sockfd].pending = 0U; s_net_udp[sockfd].src_addr = NULL; s_net_udp[sockfd].addrlen = NULL; s_net_udp[sockfd].buf = NULL; s_net_udp[sockfd].len = 0; s_net_udp[sockfd].deadline = 0U; s_net_udp[sockfd].caller = NULL; taskEXIT_CRITICAL(); } /* * 函数功能:清除本 fd 的 UDP 接收槽(不通知) * 入口参数:sockfd - Socket 描述符 (0~7) int * 出口参数:无 * 返回值:无 * 限定条件:任意上下文(netTask 的 close/net_socket 分配路径调用) * 函数说明:与 net_pending_clear 同一契约——"新 fd 不得继承上一条连接的挂起", * 调用方侧通知已在各 settle/cancel 路径发过,这里只做幂等归零。 */ void net_udp_clear(int sockfd) { if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { return; } (void)memset(&s_net_udp[sockfd], 0, sizeof(s_net_udp[sockfd])); } /* * 公共函数实现(netTask 上下文) */ /* * 函数功能:UDP 绑定/打开芯片侧 Socket(服务器模式) * 入口参数:sockfd - Socket 描述符 (0~7) int * local_port - 本地(源)端口,主机序;0 = 分配动态端口 uint16_t * 出口参数:无 * 返回值:0 - 成功;-1 - 失败(NET_ERR_BADF/INVAL/BUSY,errno 已记) int * 限定条件:netTask 上下文;控制块在用且为 DGRAM * 函数说明:1. 命令顺序按手册 §9.2.4:SET_PROTO_TYPE → SET_SOUR_PORT → * SET_DES_IP(0xFFFFFFFF) → SET_DES_PORT(0) → OPEN_SOCKET。 * 目的地址必须在 OPEN 之前下发——open 会复位端口/IP 寄存器 * (docs/网络/BSD_Socket_API_函数调用关系.md §4)。 * 2. DesIP=0xFFFFFFFF 即服务器模式:收任意来源,芯片给每个包加 * 8 字节信息头(net_types.h UDP 模式宏注释)。 * 3. BSD 的 bind 只表达本地地址,**不选对端**,故本函数恒为服务器模式; * 客户端模式由 net_sendto(带目的地址) 或 net_udp_set_peer 进入。 * 4. 重绑(同一 fd 二次 bind)会重开芯片侧 Socket:这是"从客户端模式 * 回到服务器模式"的完整形态(比 net_udp_set_peer 更重,连缓冲与 * 端口一起重来),接收缓冲内未读数据随之作废——调用方自选。 * 作废不等于可以不吭声:重开之前先把本 fd 的挂起收包按 CONNRESET * 结算并通知,否则"另一个任务重绑 + 本任务无限等包"= 永久睡死。 * 5. 动态端口在 netTask 内取号(原在调用方上下文改 s_dynamic_port)。 */ int net_udp_bind_exec(int sockfd, uint16_t local_port) { /* 控制块 */ net_sock_t *p_sock = net_get_sock(sockfd); /* open_socket 返回状态 */ uint8_t status; /* 最终端口(主机序) */ uint16_t port = local_port; /* 服务器模式 DesIP(线序字节:首段在低地址) */ static const uint8_t s_des_ip_any[4] = {0xFFU, 0xFFU, 0xFFU, 0xFFU}; if (p_sock == NULL) { return NET_ERR_RETURN_1(NET_ERR_BADF); } if (p_sock->type != NET_SOCK_DGRAM) { return NET_ERR_RETURN_1(NET_ERR_INVAL); } /* 重开芯片侧会丢掉接收缓冲里已到的数据报:本 fd 若正有人在等包,必须在动手 * 之前先给一个明确的了断(CONNRESET + 通知)。少了这一步,"A 任务 net_recvfrom * 无限等 + B 任务重新 bind 同一 fd"就是永久睡死——本函数末尾的 net_udp_clear * 只归零状态、不通知,那是兜底,不是结算。 */ net_udp_req_complete(sockfd, NET_ERR_CONNRESET); /* net_init 已为全部 8 个 Socket 分配缓冲;此处仅对 Socket 4~7 兜底重设, * 布局与 net_init 一致(固定 6 块/Socket,与 MSS 解耦) */ if (sockfd >= 4) { /* 每 Socket 块数 = 接收 + 发送 */ uint8_t blocks_per_socket = (uint8_t)(CH395F_RECV_BLOCKS + CH395F_SEND_BLOCKS); /* 本 Socket 接收缓冲起始块 */ uint8_t recv_start = (uint8_t)(sockfd * blocks_per_socket); /* 发送缓冲起始块(接收块之后) */ uint8_t send_start = (uint8_t)(recv_start + CH395F_RECV_BLOCKS); ch395f_set_send_buf((uint8_t)sockfd, send_start, (uint8_t)CH395F_SEND_BLOCKS); ch395f_set_recv_buf((uint8_t)sockfd, recv_start, (uint8_t)CH395F_RECV_BLOCKS); } /* 协议类型 = UDP */ ch395f_set_proto_type((uint8_t)sockfd, CH395F_PROTO_TYPE_UDP); /* 端口 0 = 请本层分配 */ if (port == 0U) { port = net_dynamic_port_next(); } p_sock->local_port = port; ch395f_set_sour_port((uint8_t)sockfd, port); /* DesIP=0xFFFFFFFF + DesPort=0:服务器模式(不限对端) */ ch395f_set_des_ip((uint8_t)sockfd, (uint8_t *)s_des_ip_any); ch395f_set_des_port((uint8_t)sockfd, 0U); status = ch395f_open_socket((uint8_t)sockfd); if (status != CH395F_ERR_SUCCESS) { DBG_ERROR("udp sock%d open FAIL status=0x%02X", sockfd, status); return NET_ERR_RETURN_1(NET_ERR_BUSY); } /* 打开即可用(UDP 无监听态) */ p_sock->state = NET_SOCK_STATE_UDP_OPEN; /* 模式与对端簿记复位:重开后的芯片就是服务器模式,软件账本必须同步 */ p_sock->udp_mode = NET_UDP_MODE_SERVER; (void)memset(p_sock->remote_ip_arr, 0, sizeof(p_sock->remote_ip_arr)); p_sock->remote_port = 0U; p_sock->remote_ip = 0U; /* 重开不得继承上一条连接的电平 */ p_sock->readable = 0U; /* 本 fd 的接收槽兜底归零(上方的 req_complete 已把"重开前还在等包"的调用方结算掉) */ net_udp_clear(sockfd); return 0; } /* * 函数功能:切换 UDP 芯片模式(改 DesIP/DesPort,不重开 Socket) * 入口参数:sockfd - Socket 描述符 (0~7) int * peer - 非 NULL = 客户端模式对端;NULL = 回服务器模式 const struct net_sockaddr_in * * 出口参数:无 * 返回值:0 - 成功;-1 - 失败(NET_ERR_BADF/INVAL/NOTCONN,errno 已记) int * 限定条件:netTask 上下文;控制块在用、type == DGRAM、state == UDP_OPEN * 函数说明:语义见 net_socket.h net_udp_set_peer。open 之后改这两个寄存器 * 是否立即生效已有实证:net_sendto 一直是"发之前才写 DesIP", * 芯片据此就换模式(docs/问题记录/CH395F_Trap_Records.md「UDP 模式」条)。 */ int net_udp_set_peer_exec(int sockfd, const struct net_sockaddr_in *peer) { /* 控制块 */ net_sock_t *p_sock = net_get_sock(sockfd); /* 目的 IP 线序字节 */ uint8_t ip_arr[4]; if (p_sock == NULL) { return NET_ERR_RETURN_1(NET_ERR_BADF); } if (p_sock->type != NET_SOCK_DGRAM) { return NET_ERR_RETURN_1(NET_ERR_INVAL); } if (p_sock->state != NET_SOCK_STATE_UDP_OPEN) { /* 芯片侧 Socket 还没打开:先 net_bind(否则 open 会把这里设的值复位) */ return NET_ERR_RETURN_1(NET_ERR_NOTCONN); } if (peer != NULL) { /* 客户端模式:对端 IP 按内存顺位取(s_addr 已是网络序 = 首段在低地址) */ (void)memcpy(ip_arr, &peer->sin_addr.s_addr, sizeof(ip_arr)); ch395f_set_des_ip((uint8_t)sockfd, ip_arr); ch395f_set_des_port((uint8_t)sockfd, net_ntohs(peer->sin_port)); p_sock->udp_mode = NET_UDP_MODE_CLIENT; (void)memcpy(p_sock->remote_ip_arr, ip_arr, sizeof(ip_arr)); p_sock->remote_port = net_ntohs(peer->sin_port); p_sock->remote_ip = peer->sin_addr.s_addr; } else { /* 服务器模式:DesIP=0xFFFFFFFF、DesPort=0(与 bind 同值) */ ip_arr[0] = 0xFFU; ip_arr[1] = 0xFFU; ip_arr[2] = 0xFFU; ip_arr[3] = 0xFFU; ch395f_set_des_ip((uint8_t)sockfd, ip_arr); ch395f_set_des_port((uint8_t)sockfd, 0U); p_sock->udp_mode = NET_UDP_MODE_SERVER; (void)memset(p_sock->remote_ip_arr, 0, sizeof(p_sock->remote_ip_arr)); p_sock->remote_port = 0U; p_sock->remote_ip = 0U; } return 0; } /* * 函数功能:UDP 发送执行体(分块 + 有界自旋 + 挂起 send 续传) * 入口参数:sockfd - Socket 描述符 (0~7) int * buf - 载荷(调用方内存) const void * * len - 载荷长度(>0) int * dest - 本包目的地址;NULL = 不改 DesIP const struct net_sockaddr_in * * caller - 调用方任务句柄(挂起 send 通知目标) TaskHandle_t * 出口参数:无 * 返回值:实际发送字节数(>= 0);NET_PENDING_REGISTERED;-1 = 失败 int * 限定条件:netTask 上下文;控制块在用且已 bind(state == UDP_OPEN) * 函数说明:目的地址簿记 + 模式置位在本函数做完,之后把分块/等待/挂起全部 * 交给 net_send_locked——分块粒度、SENBUF_FREE 门控、末块不等、 * 短写与挂起 send 表都与 TCP 共用同一份实现,不留第二份自旋 * (TRAP-NET-16 的复发形态正是"两条路径各写一套分块")。 */ int net_udp_sendto_exec(int sockfd, const void *buf, int len, const struct net_sockaddr_in *dest, TaskHandle_t caller) { /* 控制块 */ const net_sock_t *p_sock = net_get_sock(sockfd); if (p_sock == NULL) { return NET_ERR_RETURN_1(NET_ERR_BADF); } if ((buf == NULL) || (len <= 0)) { return NET_ERR_RETURN_1(NET_ERR_INVAL); } if (p_sock->state != NET_SOCK_STATE_UDP_OPEN) { return NET_ERR_RETURN_1(NET_ERR_NOTCONN); } if (dest != NULL) { /* 带显式目的地址:芯片 DesIP 切到该 IP → 客户端模式(硬件行为,如实记账) */ struct net_sockaddr_in peer; int rc; (void)memset(&peer, 0, sizeof(peer)); peer.sin_family = NET_AF_INET; peer.sin_port = dest->sin_port; peer.sin_addr.s_addr = dest->sin_addr.s_addr; rc = net_udp_set_peer_exec(sockfd, &peer); if (rc != 0) { return rc; } } /* 分块发送(含挂起 send 登记)与 TCP 同一实现。 * 必须走 net_send_locked 而不是 net_send_sock:后者把 caller 写成自己 * (netTask),挂起登记会被"执行者自等待"守卫拒成 BUSY,真正的调用方 * 就永远等不到续传完成的通知。 */ return net_send_locked(net_get_sock(sockfd), buf, len, caller); } /* * 函数功能:UDP 接收执行体(即时读 / 登记挂起 / 立即拒) * 入口参数:sockfd - Socket 描述符 (0~7) int * buf - 调用方缓冲 void * * len - 缓冲容量(>0) int * flags - NET_MSG_DONTWAIT = 单次非阻塞 int * to_ms - 等待预算(0 = 无限等待) uint32_t * caller - 调用方任务句柄(挂起完成通知目标) TaskHandle_t * 出口参数:无(源地址经槽位里的调用方指针直接写回) * 返回值:实际接收字节数(> 0);NET_PENDING_REGISTERED;-1 = 失败(errno 已记) int * 限定条件:netTask 上下文;控制块在用、type == DGRAM、state == UDP_OPEN * 函数说明:1. 一次调用 = 一个完整数据报(剥头 + 截断排空),见 net_udp_datagram_read。 * 2. 无数据 + 阻塞 → 登记挂起(复用本 fd 已 arm 的槽位),netTask 零占用 * 返回;完成条件由 net_udp_tick 按 1ms 主循环节拍判定。 * 3. 执行任务自身(caller == netTask)的阻塞等待被登记守卫拒绝—— * tick 就跑在执行任务里,自等自通知 = 死锁(同 net_pending_register)。 * 4. 槽位是调用方在 arm 之后才把消息投进来的,所以本函数**每一条负向 * 出口都必须归还槽位**(含参数/形态拒绝):漏一条,这条 fd 就永久 * BUSY,且报错还是从下一条 recvfrom 才冒出来(2026-11-03 补测抓到, * 当时 NOTCONN 这条没清)。唯一清不掉的是 fd 越界(p_sock == NULL), * 那时连槽都不存在。 */ int net_udp_recvfrom_exec(int sockfd, void *buf, int len, int flags, uint32_t to_ms, TaskHandle_t caller) { /* 控制块 */ net_sock_t *p_sock = net_get_sock(sockfd); /* 本 fd 接收槽 */ net_udp_req_t *p; /* 芯片侧可用字节 */ int avail; if (p_sock == NULL) { return NET_ERR_RETURN_1(NET_ERR_BADF); } /* ⚠ 取槽指针必须排在 fd 合法性检查之后:s_net_udp[] 只有 NET_MAX_SOCKETS 项, * 在越界 fd 上先取地址 = 越界指针(哪怕还没解引用,后续登记就是野写)。 */ p = &s_net_udp[sockfd]; if (p_sock->type != NET_SOCK_DGRAM) { /* 面向流的 recv 才是 TCP 的入口 */ net_udp_clear(sockfd); return NET_ERR_RETURN_1(NET_ERR_INVAL); } if ((buf == NULL) || (len <= 0)) { net_udp_clear(sockfd); return NET_ERR_RETURN_1(NET_ERR_INVAL); } if (p_sock->state != NET_SOCK_STATE_UDP_OPEN) { /* 芯片侧 Socket 还没开:先 net_bind */ net_udp_clear(sockfd); return NET_ERR_RETURN_1(NET_ERR_NOTCONN); } /* 刷新状态机与可读/可写标志(与 net_recv_locked 同一入口动作) */ net_poll(); avail = (int)ch395f_get_recv_len((uint8_t)sockfd); if (avail > 0) { /* 数据已在:读出这一个数据报并立即回执 */ int rd = net_udp_datagram_read(sockfd, p_sock, buf, len, p->src_addr, p->addrlen, avail); net_udp_clear(sockfd); return rd; } /* 非阻塞:DONTWAIT 单次覆盖或 blocking 属性为 0(与 TCP recv 同判据) */ if (((flags & NET_MSG_DONTWAIT) != 0) || (p_sock->blocking == 0U)) { net_udp_clear(sockfd); return NET_ERR_RETURN_1(NET_ERR_WOULDBLOCK); } /* 执行任务自身:无他人可完成本挂起 → 拒绝(自等死锁守卫,同 * net_pending_register 的 `caller == s_owner_task` 判据)。 * ⚠ 这里**不能**用 net_ctx_is_owner_task():那个谓词是给"调用方选路"用的, * 而本函数按设计就跑在执行任务里——用它等于把每一条经队列投进来的正常 * 请求都当成自等拒掉(2026-11-03 主机单测 UDP-8a 当场抓到)。 * 执行侧要问的是"这条请求的调用方是不是正在执行的我",与"我在不在执行 * 任务里"是两个问题;直接路径由 net_recvfrom_impl 传进当前句柄,仍然命中。 */ if ((caller == NULL) || (caller == xTaskGetCurrentTaskHandle())) { net_udp_clear(sockfd); return NET_ERR_RETURN_1(NET_ERR_BUSY); } /* 登记挂起:载荷与出参已在槽内,补上预算与通知目标 */ p->pending = 1U; p->buf = buf; p->len = len; p->deadline = (to_ms == 0U) ? 0U : (HAL_GetTick() + to_ms); p->caller = caller; return NET_PENDING_REGISTERED; } /* * 函数功能:挂起 UDP 接收完成检查(netTask 每轮调用一次,非阻塞) * 入口参数:p_socks - Socket 控制块表首指针(net_socket.c 的 s_net_socks) net_sock_t * * 出口参数:无 * 返回值:无 * 限定条件:netTask 上下文 * 函数说明:逐 fd 按序 ①fd 失效/不再是已打开的 UDP → CONNRESET ②数据到达 → * 读包 + 回填源地址 + 字节数 ③到点(仅显式预算)→ TIMEDOUT。 * 三条出口共用 net_udp_req_complete(必通知,漏通知 = 调用方永久挂死)。 */ void net_udp_tick(net_sock_t *p_socks) { /* 当前 tick */ uint32_t now = HAL_GetTick(); /* 循环索引 */ int i; for (i = 0; i < NET_MAX_SOCKETS; i++) { /* 本 fd 接收槽 */ net_udp_req_t *p = &s_net_udp[i]; /* 本 fd 控制块 */ net_sock_t *p_sock = &p_socks[i]; if (p->pending == 0U) { continue; } /* ① fd 已失效或本 Socket 不再是打开的 UDP(close/重建/被复位) */ if ((p_sock->in_use == 0U) || (p_sock->type != NET_SOCK_DGRAM) || (p_sock->state != NET_SOCK_STATE_UDP_OPEN)) { net_udp_req_complete(i, NET_ERR_CONNRESET); continue; } /* ② 数据到达:芯片缓冲有包即完成(readable 只是省 SPI 的门, * 电平型 RECV_OK 每轮重读会重新置位,事件未收到也能自愈) */ if (p_sock->readable != 0U) { int avail = (int)ch395f_get_recv_len((uint8_t)i); if (avail > 0) { int rd = net_udp_datagram_read(i, p_sock, p->buf, p->len, p->src_addr, p->addrlen, avail); net_udp_req_complete(i, rd); continue; } /* 可读标志是陈旧电平(包已被别的路径读空):清掉,别每轮白问一次 SPI */ p_sock->readable = 0U; } /* ③ 到点(仅显式预算;deadline = 0 表示无限等待,不参与判定) */ if ((p->deadline != 0U) && ((int32_t)(now - p->deadline) >= 0)) { net_udp_req_complete(i, NET_ERR_TIMEDOUT); } } } /* * 函数功能:取消全部挂起 UDP 接收并逐一通知(网络重建/复位路径) * 入口参数:err - 通知的错误码 int * 出口参数:无 * 返回值:无 * 限定条件:netTask 上下文;须在控制块清零与芯片复位之前调用 */ void net_udp_cancel_all(int err) { int i; for (i = 0; i < NET_MAX_SOCKETS; i++) { net_udp_req_complete(i, err); } } /* * 函数功能:关闭前结算本 fd 的挂起 UDP 接收(有数据先送达,无数据则取消) * 入口参数:sockfd - 将被关闭的描述符 (0~7) int * p_sock - 将被关闭的控制块指针 net_sock_t * * 出口参数:无 * 返回值:无(无在位挂起时静默返回) * 限定条件:netTask 上下文;须在 ch395f 关闭/复位该 Socket 之前调用 * 函数说明:与 net_pending_settle_close 同一契约——关闭不丢已到达数据。 * 送达走同一条剥头/截断路径,源地址照常回填,调用方拿到的是一个 * 完整数据报而不是被关闭流程截走的半包。 */ void net_udp_settle_close(int sockfd, net_sock_t *p_sock) { /* 本 fd 接收槽 */ net_udp_req_t *p; /* 芯片侧可用字节 */ int avail; if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { return; } p = &s_net_udp[sockfd]; if (p->pending == 0U) { return; } avail = (int)ch395f_get_recv_len((uint8_t)sockfd); if (avail > 0) { int rd = net_udp_datagram_read(sockfd, p_sock, p->buf, p->len, p->src_addr, p->addrlen, avail); net_udp_req_complete(sockfd, rd); return; } net_udp_req_complete(sockfd, NET_ERR_CONNRESET); } /* * 私有函数实现 */ /* * 函数功能:统一出口——通知调用方并归还本 fd 接收槽 * 入口参数:sockfd - Socket 描述符 (0~7) int * result - 结果原值(> 0 字节数;负 = 错误码,搭通知值回传) int * 出口参数:无 * 返回值:无(槽位未在位时静默返回,防重复通知) * 限定条件:netTask 上下文 * 函数说明:与 net_pending_complete 同构:通知值 32 位整体即结果, * 错误码由调用方侧 NET_ERR_RETURN_1 记入**自己**的 errno 槽位, * 不存在跨任务 errno 竞态。 */ static void net_udp_req_complete(int sockfd, int result) { /* 本 fd 接收槽 */ net_udp_req_t *p; if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { return; } p = &s_net_udp[sockfd]; if (p->pending == 0U) { return; } xTaskNotify(p->caller, (uint32_t)result, eSetValueWithOverwrite); net_udp_clear(sockfd); } /* * 函数功能:从 8 字节信息头回填源地址(服务器模式) * 入口参数:p_sock - 控制块(本函数不用,保留与 peer 版同形签名) net_sock_t * * hdr - 已读出的 8 字节信息头 const uint8_t * * src_addr - 调用方出参(可为 NULL) struct net_sockaddr * * addrlen - 调用方出参长度(可为 NULL) int * * 出口参数:src_addr - 填充发送方地址;*addrlen = sizeof(sockaddr_in) * 返回值:无 * 限定条件:仅由 net_udp_datagram_read 在服务器模式分支调用 * 函数说明:头布局(手册 §9.2.4)[0-1] 数据长度、[2-3] 源端口(芯片小端)、 * [4-7] 源 IP(线序字节,[4] = 第一段)。s_addr 直接按"首段落在低字节" * 组装即得本层网络序约定(2026-10-24 定案:全仓 s_addr 只此一族, * 与 net_inet_addr / net_getpeername 同族,可直接相等比较), * **不能再加一次换端**。 */ static void net_udp_src_from_header(net_sock_t *p_sock, const uint8_t *hdr, struct net_sockaddr *src_addr, int *addrlen) { /* 可写判定:两个出参都到位且调用方给的容量足够 */ int writable; (void)p_sock; writable = ((src_addr != NULL) && (addrlen != NULL) && (*addrlen >= (int)sizeof(struct net_sockaddr_in))) ? 1 : 0; if (writable == 0) { return; } { /* IPv4 视图 */ struct net_sockaddr_in *p_addr_in = (struct net_sockaddr_in *)src_addr; p_addr_in->sin_family = NET_AF_INET; /* 源端口:芯片小端 → 主机序 → 本层网络序字段 */ p_addr_in->sin_port = net_htons((uint16_t)((uint16_t)hdr[NET_UDP_HDR_PORT_OFF] | ((uint16_t)hdr[NET_UDP_HDR_PORT_OFF + 1U] << 8))); p_addr_in->sin_addr.s_addr = ((uint32_t)hdr[NET_UDP_HDR_IP_OFF]) | ((uint32_t)hdr[NET_UDP_HDR_IP_OFF + 1U] << 8) | ((uint32_t)hdr[NET_UDP_HDR_IP_OFF + 2U] << 16) | ((uint32_t)hdr[NET_UDP_HDR_IP_OFF + 3U] << 24); *addrlen = (int)sizeof(struct net_sockaddr_in); } } /* * 函数功能:从控制块固定对端回填源地址(客户端模式:芯片不加信息头) * 入口参数:p_sock - 控制块(remote_ip_arr/remote_port = 本 Socket 的对端) net_sock_t * * src_addr - 调用方出参(可为 NULL) struct net_sockaddr * * addrlen - 调用方出参长度(可为 NULL) int * * 出口参数:src_addr - 填充对端地址;*addrlen = sizeof(sockaddr_in) * 返回值:无 * 限定条件:仅由 net_udp_datagram_read 在客户端模式分支调用 * 函数说明:客户端模式下芯片只收这个 IP 的包,源地址不需要从头里取—— * 反过来说它也**没有**头可取,本函数是唯一的源地址来源。 */ static void net_udp_src_from_peer(net_sock_t *p_sock, struct net_sockaddr *src_addr, int *addrlen) { int writable; writable = ((src_addr != NULL) && (addrlen != NULL) && (*addrlen >= (int)sizeof(struct net_sockaddr_in))) ? 1 : 0; if (writable == 0) { return; } { /* IPv4 视图 */ struct net_sockaddr_in *p_addr_in = (struct net_sockaddr_in *)src_addr; p_addr_in->sin_family = NET_AF_INET; p_addr_in->sin_port = net_htons(p_sock->remote_port); /* 对端字节已是线序数组,按内存顺位拷进 s_addr 即网络序整数 */ (void)memcpy(&p_addr_in->sin_addr.s_addr, p_sock->remote_ip_arr, sizeof(p_addr_in->sin_addr.s_addr)); *addrlen = (int)sizeof(struct net_sockaddr_in); } } /* * 函数功能:从芯片缓冲读出**一个完整数据报**(剥头 + 截断排空 + 清可读标志) * 入口参数:sockfd - Socket 描述符 (0~7) int * p_sock - 控制块(读 udp_mode) net_sock_t * * buf - 调用方缓冲 void * * len - 缓冲容量(> 0) int * src_addr - 源地址出参(可为 NULL) struct net_sockaddr * * addrlen - 源地址出参长度(可为 NULL) int * * avail - 调用方刚从芯片读到的可读字节数(> 0) int * 出口参数:buf - 载荷(最多 len 字节,多余部分丢弃);src_addr - 发送方地址 * 返回值:交付调用方的字节数(>= 0) int * 限定条件:netTask 上下文;avail 必须是本函数调用前刚读到的 ch395f_get_recv_len() * 函数说明:1. 服务器模式剥 8 字节信息头并从中取源地址;客户端模式整包都是 * 载荷、源地址取固定对端。模式判错 = 吃掉载荷前 8 字节(本函数 * 存在的理由,见 net_types.h UDP 模式注释)。 * 2. 服务器模式手册要求"一次性将数据全部读出";且不论哪种模式, * 载荷超出 len 时必须把剩余部分读空——留半包在芯片缓冲里, * 下一次收包会从上一包的尾巴开始解析,源地址与载荷从此全部错位 * (旧实现只读 min(可用, len),正是这个缺陷)。 * 3. 服务器模式收到不足 8 字节的残包:无从判头,整包按载荷交付 * (保持既有定案 TC-NETSOCK-440o,不猜芯片行为)。 * 4. 长度由调用方传入、本函数**不再自己读第二次**:一次收包的剥头量、 * 载荷量、排空量必须由同一个数字算出。读两次等于多一次 SPI,而且 * 两次之间芯片缓冲可能已经变了(下一包已到),拿后一次的小值去剥 * 前一次的包,头与载荷从此错位——正是本节第 2 条要避免的那类缺陷。 */ static int net_udp_datagram_read(int sockfd, net_sock_t *p_sock, void *buf, int len, struct net_sockaddr *src_addr, int *addrlen, int avail) { /* 本包载荷字节数(剥头后) */ int payload; /* 交付调用方的字节数 */ int copy; /* 需排空的剩余字节 */ int left; if (avail <= 0) { return 0; } if ((p_sock->udp_mode == NET_UDP_MODE_SERVER) && ((uint32_t)avail >= NET_UDP_HDR_LEN)) { /* 信息头暂存(只在服务器形态用得到,放在本分支栈上) */ uint8_t hdr[NET_UDP_HDR_LEN]; /* 剥头:读即消费芯片缓冲 */ ch395f_read_recv_buf((uint8_t)sockfd, hdr, (uint16_t)NET_UDP_HDR_LEN); net_udp_src_from_header(p_sock, hdr, src_addr, addrlen); payload = avail - (int)NET_UDP_HDR_LEN; } else { /* 客户端模式(或服务器模式残包):无头可剥,源地址取固定对端 */ net_udp_src_from_peer(p_sock, src_addr, addrlen); payload = avail; } copy = (payload > len) ? len : payload; if (copy > 0) { ch395f_read_recv_buf((uint8_t)sockfd, (uint8_t *)buf, (uint16_t)copy); } /* 截断剩余:读空丢弃,保证"一次调用 = 一个数据报"。 * 暂存放本函数栈上(仅 netTask 上下文使用,netTask 栈 960 字); * 不用 ch395f_clear_recv_buf——它清的是整个接收缓冲,会连带丢掉 * 后续已到、调用方还没读的数据报。 */ left = payload - copy; if (left > 0) { uint8_t scratch[NET_UDP_DRAIN_CHUNK]; /* 与 TCP 短写同一口径记 ERROR:静默丢字节最难查。 * ⚠ 本仓无 DBG_WARN 宏(dbg_log.h 只有 ERROR/INFO/DEBUG),勿写 DBG_WARN */ DBG_ERROR("sock%d udp datagram truncated %d->%d, %d bytes drained", sockfd, payload, copy, left); while (left > 0) { uint16_t n = (left > (int)NET_UDP_DRAIN_CHUNK) ? (uint16_t)NET_UDP_DRAIN_CHUNK : (uint16_t)left; ch395f_read_recv_buf((uint8_t)sockfd, scratch, n); left -= (int)n; } } /* 本包已消费完:清可读电平,否则 net_select 会永久报可读 */ p_sock->readable = 0U; p_sock->recv_bytes += (uint32_t)copy; return copy; }