Files
STM32F4-Base/Drivers/BSP/NET/net_udp.c
T
super_agent_bot fff961d803
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
feat(net/udp): UDP 数据面并入 netMsgQueue 串行执行(#73)
前置来自 #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/任务规划/**。
2026-11-03 18:44:01 +08:00

766 lines
35 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称: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;
}