Files
STM32F4-Base/Drivers/BSP/NET/net_udp.c
T
super_agent_bot b5671e8c02 fix(net/udp): 收包剥头只看芯片宣布的字节数,与 udp_mode 无关(#73)
真机第一轮(2026-11-04 08:16,阶段 16 台架)把本轮最大的一条设计错误照了出来:
TC-NET-1603 在客户端形态下发 48 字节应答,板子读到的是 `n=56`、首字节 `0x38`
——56 正是信息头的长度字段,也就是**头还在**,而我把它的 8 字节当成了载荷。

设计时按手册 §9.2.4 读出一句"客户端模式不带信息头",就以为运行时把 DesIP 从
全 F 改成具体对端会让头消失。真机给的定案是:**加不加 8 字节信息头,是在
OPEN_SOCKET 那一刻由当时的 DesIP 定死的**,之后再改 DesIP 只改发送去向与芯片
的收包源过滤,不改收包帧格式。本层 `net_bind` 一律以 DesIP=0xFFFFFFFF 开
Socket,所以对本层的 UDP socket 而言头恒在、恒剥。

- `net_udp_datagram_read` 的剥头条件由 `udp_mode == SERVER && avail >= 8` 改为
  **只看 `avail >= 8`**;源地址一律从信息头里取——头里的源 IP/端口是本包的真实
  来源,比"我把包发给了谁"这本账更准(NAT 后的时间服务器就是两种地址不一致的
  那种)。
- 不足 8 字节的残包保持既有定案(TC-NETSOCK-440o):无从判头,整包按载荷交付,
  源地址退到对端记账。这也是 `net_udp_src_from_peer()` 如今唯一的读者。
- `udp_mode` 语义收窄为"芯片的对端过滤 + 发送去向",**不描述收包帧格式**;
  `net_types.h` 的 UDP 形态块、两个模式宏的注释、`net_sendto` / `net_recvfrom`
  的函数说明一并按定案改写。字段本身没动,仍复用原有的 2 字节 padding,RAM 零增长。

产品构型 Keil 构建 0 Error 0 Warning,`Code=249120 RO=29836 RW=2052 ZI=149996`,
与改造前的产品基线同尺寸。判据文字与真机证据见同批的测试、文档提交。
2026-11-04 09:15:17 +08:00

775 lines
36 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 在剥头分支(avail >= 8)调用——本层 Socket
* 一律开在服务器形态,信息头与 udp_mode 无关,故 CLIENT 形态同样走这里,
* 头里的源 IP/端口就是本包的真实来源
* 函数说明:头布局(手册 §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);
}
}
/*
* 函数功能:从控制块固定对端回填源地址(不足 8 字节残包的兜底路径)
* 入口参数: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 在残包分支(avail < 8)调用
* 函数说明:本层 Socket 一律开在服务器形态、每个数据报都带信息头,正常包都从
* 头里取源地址;只有无从判头的残包才退到"源地址=本 Socket 的对端记账",
* 至少比报 NULL 或报上一个包的源更接近真相(TC-NETSOCK-440o 定案)。
*/
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 - 控制块(只在不足 8 字节的残包上用来取对端记账当源地址) 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. **剥头与 udp_mode 无关,只看开 Socket 时的形态**:本层所有 DGRAM
* Socket 都由 net_bind 在 `DesIP = 0xFFFFFFFF`(服务器形态)下开出来,
* 芯片据此给**每一个**收到的数据报前置 8 字节信息头,此后无论把 DesIP
* 改成具体对端(显式 dest_addr 的 net_sendto / net_udp_set_peer)都不
* 再改变这一帧格式。2026-11-04 08:16 真机 TC-NET-1603 实测:客户端
* 形态下发来 48 B,芯片给出 n=56、首字节 0x38(=56,正是信息头的长度
* 字段)——运行时改 DesIP 不会让头消失。按 udp_mode 决定剥不剥,就会
* 把 8 字节头当载荷交给应用(SNTP 48 B 应答只剩 40 B 且首 8 字节是头),
* 这正是 TRAP-NET-22 记录的老缺陷;定案见该条 2026-11-04 追加段。
* 源地址同理一律从头里取:头里的源 IP/端口是**本包**的真实来源,比
* 对端记账更准(对端记账只用于发送与芯片收包过滤)。
* 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 ((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 {
/* 不足 8 字节的残包:无头可剥,源地址取固定对端 */
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;
}