feat(net/udp): UDP 数据面并入 netMsgQueue 串行执行(#73)
CI / 静态门禁 (pre-commit) (push) Failing after 49s
CI / 主机侧单测 (MSVC) (push) Failing after 30s
CI / 静态门禁 (pre-commit) (pull_request) Failing after 45s
CI / 主机侧单测 (MSVC) (pull_request) Failing after 44s

前置来自 #57 SNTP 调研:旧 net_sendto / net_recvfrom 由调用方直调
SPI 且无限自旋收包,既违约「netTask 是芯片唯一访问者」,又把 IWDG
预算吃在自旋里。

改造面
- 新增 Drivers/BSP/NET/net_udp.c/.h(真机专属,模拟器不编译):
  bind/set_peer/sendto/recvfrom 四条执行体 + 挂起收包表 + tick;
  net_msg_type_t 加四型,公开 API 一律「入队 → 睡在通知上」。
- 源地址出参不搭 net_msg_t(按值拷贝,AGENTS 禁加回传字段):改住
  net_udp.c 的 per-fd 槽,netTask 就地写回调用方内存,通知值只带
  结果码;每条出口都清槽,泄漏 = 该 fd 永久 BUSY。
- 发送复用 net_send_locked,不写第二份分块逻辑;「会话是否已死」
  收成一条判据 net_send_state_down()(TCP 看 ESTABLISHED、UDP 看
  UDP_OPEN),原先 4 处各写一遍。
- 顺带修掉真缺陷(新条 TRAP-NET-22):旧 sendto 把 DesIP 写成具体
  IP,芯片静默切 UDP Client,而旧 recvfrom 无条件剥 8 字节 → SNTP
  这类 48 B 应答丢载荷前 8 字节并交出垃圾源地址。加 udp_mode(塞进
  原有 padding,sizeof 不变)、bind 恒 SERVER、显式对端 sendto 切
  CLIENT、set_peer(fd, NULL) 轻量回 SERVER,仅 SERVER 且 avail >= 8
  才剥头。
- 截断定案:一次调用交付一个数据报,buf 装不下的尾巴用 64 B 栈上
  scratch 排空(不用 clear_recv_buf——它连后面已到达的包一起丢)。
- 取消点接齐:net_init 重建 cancel_all、net_socket 分配 fd clear、
  net_close settle_close + clear、重 bind 前先结算等包者,
  net_process_messages 第 5 个 tick 位挂 net_udp_tick。
- 收紧两处契约(原实现无生产调用者):UDP 未 bind 一律 -5;
  net_recv / _nb / _to 拒 DGRAM(绕过剥头就是静默吃 8 字节);
  NET_ERR_NOT_IN_NETTASK(-12) 退役为历史常量并写明原因。

验证
- 主机单测 test/host/net_socket 全套 616/616 绿(改造前 470):新
  分片 ns_s7_udp.c 20 组,改写 440 / 555 / 560 组;覆盖率
  net_udp.c 234/236(99.2%)、net_socket.c 956/969(98.7%),
  scripts/test_coverage_baseline.json 随本提交登记新文件。
- 测试抓到 4 个被测码真 bug:自等守卫误用调用侧谓词、芯片长度被读
  两次、槽指针取在 fd 合法性检查之前、重 bind 静默作废等待者。
- Keil ARMCC 0 Error(s) 0 Warning(s);net_udp.o Code 1,358 / ZI 224
  (8 fd × 28 B,其余模块零增长),全仓 RW_IRAM1 72.2%;内存台账
  受管区按门禁提示用 --write / --update 机器回填,未手改数字。
- pre-commit 全量钩子(含 doc-links、coding-standard、mem-budget、
  generated-fresh、bsp-err、registration、nested-comment)全绿。

限度
- 本轮无板子:真机回归 5 条(两端互通、set_peer(NULL) 能否恢复收
  广播、截断排空后下一包完整性、挂起 recv 期间 IWDG 余量、bind 写
  DesIP 与 open 先后的真芯片效果)已写进覆盖率口径「台架必验」。
- scripts/stack_report.py 退出码 1 是 10-26 起挂在账上的 iec104Task
  栈债(margin 1,016 < 1,024,见 docs/代码规范/pre-commit使用说明.md:
  122);本轮只让 net_bind 的 UDP 分支多一层静态帧(1,544 → 1,600),
  netTask margin 1,888 与 timeSyncTask 1,152 不受影响。抬栈要过
  mem-budget 台账,不在本分支自行处理。
- 登记簿销项按仓库约定留给主工作树,本分支不动 docs/任务规划/**。
This commit is contained in:
2026-11-03 18:08:27 +08:00
parent 2b5eb1c0db
commit fff961d803
26 changed files with 3550 additions and 557 deletions
+765
View File
@@ -0,0 +1,765 @@
/*
* 模块名称: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 <stddef.h>
#include <string.h>
#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;
}