a91640dc6e
前置来自 #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/任务规划/**。
757 lines
34 KiB
C
757 lines
34 KiB
C
/*
|
||
* 模块名称: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)。
|
||
*/
|
||
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 的入口 */
|
||
return NET_ERR_RETURN_1(NET_ERR_INVAL);
|
||
}
|
||
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);
|
||
}
|
||
|
||
/* 刷新状态机与可读/可写标志(与 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;
|
||
}
|