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
+25 -6
View File
@@ -518,13 +518,32 @@ int net_send_pending_register(int sockfd, const void *buf, int len, int sent,
return NET_PENDING_REGISTERED;
}
/*
* 函数功能:判"本 Socket 的发送会话是否已不可继续"(TCP/UDP 统一判据)
* 入口参数:p_sock - Socket 控制块指针 net_sock_t *
* 出口参数:无
* 返回值:1 - 不可继续(应报 CONNRESET/NOTCONN);0 - 可继续 int
* 限定条件:p_sock 非空;任意上下文(纯读控制块)
* 函数说明:TCP 的门是 ESTABLISHED,UDP 的门是 UDP_OPEN(芯片侧由 net_bind 打开)。
* 判据只写这一处:net_send_locked 的入口门与两处中途检查、
* net_send_pending_tick 的 ① 都用它——两条发送路径各自复述一遍
* "state != ESTABLISHED"正是 UDP 接入时最容易漏改的形态。
*/
int net_send_state_down(const net_sock_t *p_sock) {
if (p_sock->type == NET_SOCK_DGRAM) {
return (p_sock->state != NET_SOCK_STATE_UDP_OPEN) ? 1 : 0;
}
return (p_sock->state != NET_SOCK_STATE_ESTABLISHED) ? 1 : 0;
}
/*
* 函数功能:挂起 send 推进与完成检查(netTask 每轮 net_process_messages 调用一次,非阻塞)
* 入口参数:p_socks - Socket 控制块表首指针(net_socket.c 的 s_net_socks) net_sock_t *
* 出口参数:无
* 返回值:无
* 限定条件:netTask 上下文
* 函数说明:1. 逐 fd 按序:①断连 → 以已发字节数结束(剩余字节不可能送达)
* 函数说明:1. 逐 fd 按序:①会话不可继续(TCP 断连 / UDP 芯片侧被关)→ 以已发字节数结束
* (剩余字节不可能送达)
* 2. ②推进:发送缓冲空闲则写下一块(每轮至多一块——写完即清 send_ready,
* 下一块须等 SENBUF_FREE 中断重新置位,由 net_poll 刷新)
* 3. ③全部写完 → 以总长度结束;④无进展超时 → 以已发字节数结束
@@ -553,13 +572,13 @@ void net_send_pending_tick(net_sock_t *p_socks) {
continue;
}
/* ① 断连:本会话已结束 → 剩余字节不可能送达,按已发字节数结束。
* 判据 `state != ESTABLISHED`(同快速路径):state 离开 ESTABLISHED 后
* 即便再回到 ESTABLISHED,也是**新会话**(relisten + 新 CONNECT)——
* 旧会话的剩余字节绝不能投到新连接上。session_dead 一并覆盖
/* ① 会话已不可继续 → 剩余字节不可能送达,按已发字节数结束。
* 判据见 net_send_state_down(TCP:state 离开 ESTABLISHED 后即便再回到
* ESTABLISHED 也是**新会话**,旧会话剩余字节绝不能投到新连接上;
* UDP:芯片侧 Socket 被关/复位即不可继续)。session_dead 一并覆盖
* (net_types.h:TIMEOUT/DISCONNECT 两条路径都同时置 CLOSED,此处为
* 与 recv 侧判据对齐的冗余保险) */
if ((p_sock->state != NET_SOCK_STATE_ESTABLISHED) || (p_sock->session_dead != 0U)) {
if ((net_send_state_down(p_sock) != 0) || (p_sock->session_dead != 0U)) {
DBG_ERROR("sock%d send pending aborted (%d/%d): session down",
i, p->sent, p->len);
(void)net_set_errno(NET_ERR_CONNRESET);
+12 -1
View File
@@ -163,7 +163,8 @@ int net_send_pending_register(int sockfd, const void *buf, int len, int sent,
* 出口参数:无
* 返回值:无
* 限定条件:netTask 上下文
* 函数说明:逐 fd 按序判定:①断连(state 非 ESTABLISHED / session_dead)→ 以已发
* 函数说明:逐 fd 按序判定:①会话不可继续(net_send_state_down:TCP 看
* ESTABLISHED / UDP 看 UDP_OPEN;session_dead 一并覆盖)→ 以已发
* 字节数结束;②推进——发送缓冲空闲则写下一块(每轮至多一块,写完即清
* send_ready,下一块等 SENBUF_FREE 由 net_poll 重新置位);③全部写完 →
* 以总长度结束;④无进展超时 → 以已发字节数结束。结果均为**已发字节数**
@@ -171,6 +172,16 @@ int net_send_pending_register(int sockfd, const void *buf, int len, int sent,
*/
void net_send_pending_tick(net_sock_t *p_socks);
/*
* 函数功能:判"本 Socket 的发送会话是否已不可继续"(TCP/UDP 统一判据)
* 入口参数:p_sock - Socket 控制块指针 net_sock_t *
* 返回值:1 - 不可继续;0 - 可继续 int
* 限定条件:p_sock 非空;纯读控制块,任意上下文
* 函数说明:TCP 门 = ESTABLISHED,UDP 门 = UDP_OPEN。快速路径(net_send_locked)
* 与挂起路径(net_send_pending_tick)共用,判据只此一处。
*/
int net_send_state_down(const net_sock_t *p_sock);
/*
* 函数功能:清除本 fd 的挂起 send(不通知;"新 fd 不得继承"与 close 后的幂等清理)
* 入口参数:sockfd - Socket 描述符 (0~7) int
File diff suppressed because it is too large Load Diff
+168 -37
View File
@@ -39,11 +39,31 @@ typedef enum {
NET_MSG_LISTEN,
NET_MSG_ACCEPT,
NET_MSG_DROP_CONN, /* 主动拆会话、保监听(见 net_drop_conn) */
/* UDP 队列化改造(2026-11-03,工单前置 = SNTP 客户端 #57):UDP 三个碰芯片的
* 动作从"调用方上下文直调 SPI"改为交 netTask 串行执行,字段用法:
* NET_MSG_UDP_BIND - sockfd + len=本地端口(主机序,0 = 执行体内取动态号)
* 恒 DesIP=0xFFFFFFFF 服务器模式(bind 不选对端,
* 客户端模式见 NET_MSG_UDP_PEER / SENDTO 的 addr)
* NET_MSG_UDP_SENDTO - buf/len=载荷 + addr=本包目的地址(按值拷入,**入参**方向)
* NET_MSG_UDP_RECVFROM - buf/len=载荷 + timeout_ms=等待预算 + flags
* ⚠ 源地址出参(src_addr / addrlen 两个指针)**不走本结构**:结构按值拷入
* 队列,指针若在 int 字段里过桥,64 位主机单测构建会被截断,且违反
* AGENTS「勿给 net_msg_t 加回传用字段」。改走 net_udp.c 的 per-fd 挂起槽
* (调用方在入队前登记自己的指针,netTask 完成后写进调用方内存 + 通知值回码),
* 与 TCP 挂起 recv 把数据直接写进调用方 buf 是同一个模型。
* 详见 docs/网络/BSD_Socket_API_使用指南.md §6 */
NET_MSG_UDP_BIND,
NET_MSG_UDP_SENDTO,
NET_MSG_UDP_RECVFROM,
/* 静默回收认领 + 手动自愈 API 退役(docs/网络/网络自愈架构设计.md §4.1、§4.2):
* NET_MSG_RESYNC_LISTEN / NET_MSG_RELISTEN 已删——
* 静默回收由对账⑤自动认领(net_reconcile_tick),重建由 reconciler
* 形态③④/D10 单一
* 所有者负责;手动自愈 API 面退役(生产调用者本就为零) */
/* UDP 模式切换(追加在枚举末尾:既有成员编号是主机单测/板测的钉值对象,
* 不在中间插入)——addr 按值携带对端则切客户端模式,addrlen == 0 则回
* 服务器模式;不重开 Socket,只写 DesIP/DesPort,语义见 net_udp_set_peer */
NET_MSG_UDP_PEER
} net_msg_type_t;
typedef struct {
@@ -331,60 +351,132 @@ int net_setblocking(int sockfd, int on);
int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms);
/*
* 函数功能:发送 UDP 数据
* 函数功能:发送 UDP 数据报(**走消息队列**,任意任务可调用)
* 入口参数:sockfd - socket 描述符
* buf - 数据缓冲区
* len - 数据长度
* flags - 通常为 0
* dest_addr - 目标地址
* addrlen - 地址长度
* len - 数据长度(>0)
* flags - 目前不使用,传 0(与 net_send 同)
* dest_addr - 本包目的地址;NULL = 沿用本 Socket 当前 DesIP
* addrlen - 地址长度(NULL 目的时传 0 即可)
* 返回值:实际发送字节数 int > 0
* -1 失败,精确码经本任务错误码槽给出(net_get_errno()):
* NET_ERR_BADF / NET_ERR_INVAL / NET_ERR_TIMEDOUT /
* NET_ERR_NOT_IN_NETTASK(调用上下文不对,见「限定条件」)
* 限定条件:**仅限本层执行任务(net_init 的调用者,NET 层 = netTask)调用**;
* socket 已创建且为 DGRAM;指针非空
* 函数说明:1. 设置目标地址后调用 CH395F 发送
* 2. ⚠ 本接口**不走消息队列**:它在调用方上下文里直接 net_poll +
* 写芯片缓冲,与 net_send/net_recv/net_connect 那一族(入队交 netTask
* 串行执行)不是同一条路径。别的任务调用它 = 与本层的 net_poll 抢
* 同一根 SPI2(TRAP-NET-14 的根因形态),故加上下文守卫直接拒掉,
* 回 NET_ERR_NOT_IN_NETTASK。**该码重试永远无意义**(既非忙也非超时):
* 要么把调用挪进执行任务,要么改用走队列的 net_send——没有第三条。
* 3. 「把它改成走消息队列」是已知后续项,未做的原因:UDP 的目的地址
* 与源地址回传都要穿过 net_msg_t,而 net_msg_t 按值拷入队列、
* 错误码只搭任务通知值回来(AGENTS 明文「勿给 net_msg_t 加回传字段」),
* 真要串行化得先设计 per-task 结果槽并配真机回归(2026-10-24 定案:
* 本轮只把危险面关掉,串行化仍未做)。
* 4. 全仓当前**无生产调用者**(只有 test/board 与模拟器),正因如此
* 这条守卫要在有人开始用之前就在。
* NET_ERR_BADF / NET_ERR_INVAL / NET_ERR_TIMEDOUT / NET_ERR_BUSY
* 限定条件:socket 已创建(DGRAM 用法须先 net_bind 或 net_udp_set_peer 打开芯片侧
* Socket);net_init() 已执行;**任意任务上下文**(含 netTask 自身,
* netTask 内直接执行不入队——自投递给自身 = 死锁)
* 函数说明:1. 目的地址是**入参**,按值拷进 net_msg_t.addr 随消息进 netTask,
* 与 net_connect 传地址同一条通道;结果(已发字节数/错误码)
* 搭任务通知值回来(AGENTS「勿给 net_msg_t 加回传字段」)。
* 2. 带显式目的地址 = 芯片 DesIP 切到该 IP → 本 Socket 进入
* **UDP 客户端模式**(p_sock->udp_mode = NET_UDP_MODE_CLIENT):
* 此后芯片只收该 IP 的包且**不再添加 8 字节信息头**,
* net_recvfrom 相应地不剥头、源地址回填该固定对端。
* 要回服务器模式(任意源 + 带信息头)调 net_udp_set_peer(sockfd, NULL)。
* 3. dest_addr=NULL 时不改 DesIP:服务器模式下 DesIP=0xFFFFFFFF 即
* 向 255.255.255.255 广播(板级 UDP 广播测试的用法),模式不变。
* 4. 分块上限 NET_SEND_CHUNK_MAX(1KB 发送 FIFO,TRAP-NET-16),
* 门控 SENBUF_FREE(TRAP-NET-04);单块等待超 NET_SEND_SPIN_MS
* 登记挂起 send、由 net_send_pending_tick 续传 → **netTask 不被
* 一次大报发送占住**(与 TCP 发送同一套机制)。
* 5. 短写:返回值 < len 表示未全部交付(停滞预算 NET_SEND_TIMEOUT_MS
* 用尽),调用方须检查返回值——同 net_send 契约。
* 6. 手册 §9.2.4:UDP 一包最大 1472 字节;本层不做长度上限校验
* (超出只会分多块写、对端收不到完整包),调用方自负。
*/
int net_sendto(int sockfd, const void *buf, int len, int flags,
const struct net_sockaddr *dest_addr, int addrlen);
/*
* 函数功能:接收 UDP 数据
* 函数功能:接收 UDP 数据报(含源地址;**走消息队列**,任意任务可调用)
* 入口参数:sockfd - socket 描述符
* buf - 接收缓冲区
* len - 缓冲区大小
* flags - 通常为 0;NET_MSG_DONTWAIT 单次强制非阻塞
* src_addr - 输出:发送方地址
* addrlen - 输入输出:地址长度
* 返回值:实际接收字节数(0 = 无数据可报的边界形态见说明) int >= 0
* len - 缓冲区大小(>0)
* flags - 0 = 按 socket 的 blocking 属性等待;NET_MSG_DONTWAIT = 单次非阻塞
* src_addr - 输出:发送方地址(可为 NULL)
* addrlen - 输入输出:src_addr 结构容量/实际长度(可为 NULL)
* 返回值:实际接收字节数 int >= 0
* -1 失败,精确码经本任务错误码槽给出(net_get_errno()):
* NET_ERR_BADF / NET_ERR_INVAL / NET_ERR_WOULDBLOCK /
* NET_ERR_NOT_IN_NETTASK(调用上下文不对,见「限定条件」)
* 限定条件:**仅限本层执行任务(net_init 的调用者,NET 层 = netTask)调用**;
* socket 已创建且为 DGRAM;指针非空
* 函数说明:1. 读取 CH395F 接收缓冲区,获取对端地址
* 2. 阻塞语义与 net_recv 同:blocking 属性下无数据
* 无限等待,DONTWAIT / 非阻塞属性立即 NET_ERR_WOULDBLOCK;
* 本接口为轮询自旋实现(历史遗留,UDP 专用),受 IWDG 监控的
* 任务同样禁止对其无限等待
* NET_ERR_TIMEDOUT / NET_ERR_CONNRESET(close/重建取消)/ NET_ERR_BUSY
* 限定条件:socket 已创建且为 DGRAM;net_init() 已执行;**任意任务上下文**
* (netTask 内直接执行不入队;netTask 内的阻塞等待被登记守卫拒绝 →
* 须用 NET_MSG_DONTWAIT)
* 函数说明:1. 与 net_recv 同一条路径(入队 → netTask 串行执行芯片事务 →
* 结果搭通知值回来),**不再是**"仅限执行任务、自旋 net_poll"的
* 历史形态;非 netTask 调用不再回 NET_ERR_NOT_IN_NETTASK。
* 2. 一次调用 = 一个数据报:芯片缓冲内该包**全部读空**(服务器模式
* 手册要求一次性读出),载荷超出 len 的部分按 BSD 语义截断丢弃,
* 不会留下半包污染下一次解析。
* 3. 源地址按 udp_mode 取:服务器模式剥 8 字节信息头
* ([2-3] 源端口芯片小端、[4-7] 源 IP 线序字节 → 本层 s_addr 网络序
* 约定,见 net_inet_addr 注释,2026-10-24 定案同族);
* 客户端模式不剥头、源地址=本 Socket 固定对端。
* ⚠ 客户端模式若仍按服务器模式剥头,等于吃掉载荷前 8 字节
* (SNTP 应答 48 字节会前 8 字节丢失)——这是本字段存在的理由。
* 4. 阻塞语义与 net_recv 同构:无数据 + 阻塞 → **登记挂起、netTask
* 零占用返回**,数据到点断连由 net_udp_tick 按 1ms 节拍结束;
* 不再是自旋(旧形态会吃满 IWDG 预算并违反 netTask 契约)。
* 5. ⚠ IWDG required_mask 内任务禁止无限等待形态:用
* net_recvfrom_to() 显式预算或 net_select()(同 net_recv 契约)。
*/
int net_recvfrom(int sockfd, void *buf, int len, int flags,
struct net_sockaddr *src_addr, int *addrlen);
/*
* 函数功能:接收 UDP 数据报(**带调用方指定超时**,对应 net_recv_to 的 UDP 版)
* 入口参数:sockfd - socket 描述符
* buf/len - 接收缓冲与容量
* flags - 目前不使用,传 0(超时由 timeout_ms 表达)
* timeout_ms - 本次等待预算(毫秒,>0)
* src_addr - 输出:发送方地址(可为 NULL)
* addrlen - 输入输出:src_addr 容量/实际长度(可为 NULL)
* 返回值:>0 实际字节数;-1 = 真错误(errno 可查);**-2 = 超时无数据**
* 限定条件:非 netTask 上下文(netTask 内须用 net_recvfrom + NET_MSG_DONTWAIT)
* 函数说明:1. 与 net_recv_to 同一条路径与同一套超时收口:挂起登记带 deadline,
* 到点由 net_udp_tick 以超时结束,对外统一 -2(不靠 errno 区分,
* 无跨任务 errno 竞态)。
* 2. 受 IWDG 监控的任务(含未来的 SNTP/对时任务)**必须**用本接口或
* net_select,不得用 net_recvfrom 的无限等待形态。
* 3. timeout_ms = 0 视为无效参数(无限等待请用 net_recvfrom)。
*/
int net_recvfrom_to(int sockfd, void *buf, int len, uint32_t timeout_ms,
struct net_sockaddr *src_addr, int *addrlen);
/*
* 函数功能:设置/查询 UDP Socket 的芯片模式(客户端 ↔ 服务器,见 net_types.h
* NET_UDP_MODE_* 与手册 §9.2.4)
* 入口参数:sockfd - socket 描述符(须为 DGRAM)
* peer - 非 NULL = 客户端模式:DesIP/DesPort 设为该对端,芯片只收
* 它的包且**不加** 8 字节信息头;
* NULL = 服务器模式:DesIP=0xFFFFFFFF、DesPort=0,收任意来源
* 且每个包带信息头(net_bind 的默认形态)
* 返回值:0 - 成功 int
* -1 - 失败(NET_ERR_BADF / NET_ERR_INVAL / NET_ERR_BUSY,errno 可查)
* 限定条件:socket 已创建且为 DGRAM 且已 net_bind(芯片侧 Socket 已打开);
* 任意任务上下文(走消息队列,netTask 内直接执行)
* 函数说明:1. 存在的理由:模式由"最近一次写 DesIP"决定,net_sendto 带目的地址
* 发一次就把芯片静默切到客户端模式。要给多个对端收发(TFTP 式
* "收到谁的包回给谁、然后继续听所有人")必须在每次带地址发送后
* 显式回服务器模式,否则回程包会被当成带头的包错解析。
* 2. 只改 DesIP/DesPort 两个寄存器,不重开 Socket(不丢接收缓冲内
* 已到的数据);本层 udp_mode 同步更新,recvfrom 剥头判定据此走。
* 3. 客户端 → 服务器切换后**先前**收到的包(无头)无法补上源地址,
* 模式切换时刻之后的包才按新模式解析——协议栈请"先切模式再等包"。
* 4. 切模式不改动 local_port,也不影响已登记的挂起 recv;挂起期间
* 切模式由 tick 在结算时读当前模式解析(同一包不会两解)。
*/
int net_udp_set_peer(int sockfd, const struct net_sockaddr_in *peer);
/*
* 函数功能:查询 UDP Socket 当前芯片模式
* 入口参数:sockfd - socket 描述符
* 返回值:NET_UDP_MODE_SERVER / NET_UDP_MODE_CLIENT uint8_t
* -1 - 描述符无效或非 DGRAM int
* 限定条件:任意上下文(纯 RAM 读,不发起芯片事务)
* 函数说明:诊断/单测/上层"要不要切回服务器模式"的判读入口
*/
int net_udp_get_mode(int sockfd);
/*
* 函数功能:关闭 socket
* 入口参数:sockfd - socket 描述符
@@ -598,6 +690,18 @@ int net_getpeername(int sockfd, struct net_sockaddr *addr, int *addrlen);
* 内部 API(需在 netTask 上下文中调用,绕过消息队列)
*/
/*
* 函数功能:取下一个动态端口并推进游标(49152~65535 回绕,内部使用)
* 入口参数:无
* 返回值:分配到的端口(主机序) uint16_t
* 限定条件:单线程语义——TCP 绑定路径(调用方上下文)与 UDP 绑定执行体
* (netTask)都经此取号,簿记只住 net_socket.c 一处
* 函数说明:原逻辑内联在 net_bind 里;UDP 队列化后绑定动作挪进 netTask 执行,
* 两处都要用同一游标,故抽成函数(同一事实只在一处)。
* 不做"是否已被其他 Socket 占用"检查——与抽取前行为一致。
*/
uint16_t net_dynamic_port_next(void);
/*
* 函数功能:发送数据(内部版本,直接操作,不经过消息队列)
* 入口参数:p_sock - Socket 控制块指针
@@ -634,6 +738,33 @@ int net_close_sock(net_sock_t *p_sock);
* 限定条件:必须在 netTask 上下文中调用
*/
int net_listen_locked(int sockfd);
/*
* 函数功能:判当前上下文是否本层执行任务(netTask / 硬件层的 testTask)
* 入口参数:无
* 返回值:1 - 是本层执行任务(或 net_init 前未捕获);0 - 明确不是 uint8_t
* 限定条件:net_init 之后判据才有效;任意上下文可读
* 函数说明:队列化接口用本判据决定"直接执行"还是"投递给 netTask"——执行任务
* 自己不能往自己的队列里投(要等自己回来泵,队列满即自等死锁);
* 挂起登记前也用它确认"完成动作有人做"。判据与留痕见定义处注释。
*/
uint8_t net_ctx_is_owner_task(void);
/*
* 函数功能:发送数据(内部锁定版本,TCP 与 UDP 共用同一份分块实现)
* 入口参数:p_sock - Socket 控制块指针
* buf - 数据缓冲区
* len - 数据长度
* caller - 挂起 send 的完成通知目标(执行任务自身传 NULL 即拒登记)
* 返回值:实际发送字节数(>= 0);NET_PENDING_REGISTERED = 已挂起;-1 = 错误
* 限定条件:必须在 netTask 上下文中调用
* 函数说明:UDP 队列化后 net_udp_sendto_exec 也走本函数——分块粒度
* (NET_SEND_CHUNK_MAX)、SENBUF_FREE 门控、末块不等、停滞超时、
* 挂起 send 表全部只有一份(TRAP-NET-16 的复发形态就是"两条路径
* 各写一套分块自旋",故不复制)。状态门按 type 区分:TCP 须
* ESTABLISHED,UDP 须 UDP_OPEN。
*/
int net_send_locked(net_sock_t *p_sock, const void *buf, int len, TaskHandle_t caller);
#endif /* NET_NO_OS */
#ifdef __cplusplus
+29
View File
@@ -50,6 +50,25 @@ extern "C" {
#define NET_MSG_DONTWAIT 0x01 /* 非阻塞模式(单次覆盖 socket 的 blocking 属性;
* 具名入口见 net_recv_nb() / net_setblocking()) */
/*
* UDP 芯片模式(仅 type == NET_SOCK_DGRAM 有意义;手册 §9.2.4 步骤②:
* **目的 IP 决定模式**,两种模式的收包格式不同,见下)
*
* 服务器模式 DesIP = 0xFFFFFFFF:可与任意远端通讯,CH395F 在**每个**收到的
* 数据报前添加 8 字节信息头([0-1] 数据长度、[2-3] 源端口(芯片小端)、
* [4-7] 源 IP(线序字节,首段在低地址)),且手册要求一次性把该包全部读出。
* 客户端模式 DesIP = 具体 IP:芯片只收该 IP 的包,且**不加**信息头——
* 数据流直接落到接收缓冲区(可整读也可部分读)。
*
* 为什么本层要存这个状态:模式是芯片属性、由"最近一次写 DesIP"决定,而软件
* 原先看不见它——net_sendto 带目的地址发一次就把芯片静默切到客户端模式,之后
* 的收包仍按服务器模式剥 8 字节 = **吃掉载荷前 8 字节**并把垃圾字节当源地址
* (SNTP 48 字节应答会丢掉前 8 字节,2026-11-03 定性,见
* docs/问题记录/CH395F_Trap_Records.md「UDP 模式」条)。剥不剥头必须由本字段决定。
*/
#define NET_UDP_MODE_SERVER 0U /* DesIP=0xFFFFFFFF:收包含 8 字节信息头(net_bind 的默认模式) */
#define NET_UDP_MODE_CLIENT 1U /* DesIP=具体 IP:收包无信息头,源地址=本 Socket 固定对端 */
/*
* 网络字节序转换宏
*/
@@ -167,6 +186,16 @@ typedef struct {
net_sock_state_t state; /* Socket 状态 */
uint8_t type; /* SOCK_STREAM / SOCK_DGRAM */
uint8_t in_use; /* 是否在用标志 */
/* UDP 芯片模式(NET_UDP_MODE_SERVER / _CLIENT,宏注释见本文件 UDP 模式定义处)。
* 仅 type == NET_SOCK_DGRAM 有意义;置位/清零路径:net_bind 的 UDP 分支 →
* SERVER(DesIP=0xFFFFFFFF);net_sendto 带显式目的地址、net_udp_set_peer(非
* NULL) → CLIENT(DesIP=该 IP);net_udp_set_peer(NULL) → 回 SERVER。
* 新 fd / close 由整块 memset 归零 = SERVER,与 bind 默认同值,故不得继承上一条。
* CLIENT 模式的对端簿记复用 remote_ip_arr[4] + remote_port(与 TCP 同一对字段,
* 语义同为"本会话对端"),recvfrom 据此回填 src_addr。
* 位置选择:紧接 in_use 之后,落在 type/in_use 之后原有的 2 字节填充里
* (本结构 8 个实例,净增 RAM = 0,见 AGENTS RAM 红线)。 */
uint8_t udp_mode;
uint16_t local_port; /* 本地端口(主机序) */
uint16_t remote_port; /* 远端端口(主机序) */
uint32_t remote_ip; /* 远端 IP(网络字节序) */
+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;
}
+190
View File
@@ -0,0 +1,190 @@
/*
* 模块名称:UDP 数据面执行器与挂起槽(netTask 串行化)
* 文件说明:UDP 三个碰芯片的动作(bind 打开、sendto、recvfrom)在 netTask 上下文
* 里的实际执行体,加上 per-fd 的"一次接收请求"槽位(载荷缓冲 + 源地址
* 出参 + 等待预算)。公开 API(net_sendto / net_recvfrom /
* net_recvfrom_to / net_udp_set_peer / net_udp_get_mode)住在
* net_socket.h,那边只负责参数校验、装消息入队、等通知值回结果。
* 为什么单独成模块:源地址(src_addr/addrlen)是**出参指针**,不能塞进按值拷入
* 队列的 net_msg_t(AGENTS 明文「勿给 net_msg_t 加回传用字段」;
* 指针过 int 桥在 64 位主机单测构建还会被截断)。所以出参走本模块的
* per-fd 槽:调用方入队前登记自己的指针,netTask 结算时把源地址写进
* 调用方内存、错误码/字节数搭任务通知值回去——与 TCP 挂起 recv 把数据
* 直接写进调用方 buf 是同一个模型(docs/网络/Net层阻塞模型架构设计.md)。
* 依赖:net_types.h(net_sock_t / NET_UDP_MODE_*)、ch395f.h(芯片事务)、
* net_socket.c(net_get_sock / net_poll / 消息分派 / 钩子调用点)
* 限定条件:除 net_udp_outparam_arm / net_udp_outparam_disarm / net_udp_clear(
* 纯 RAM,任意上下文)外,全部函数须 netTask 上下文
* 关联文档:docs/网络/BSD_Socket_API_使用指南.md §6(UDP 两种芯片模式与队列化)
* 作者:super_agent_bot
* 创建日期:2026-11-03
* 修改记录:
* 2026-11-03 super_agent_bot 创建:UDP 队列化改造(工单前置 = SNTP 客户端)
*/
#ifndef NET_UDP_H
#define NET_UDP_H
#include <stdint.h>
#include "FreeRTOS.h"
#include "task.h"
#include "net_types.h"
/*
* 函数功能:登记本次接收的源地址出参(调用方上下文,入队**前**调用)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* src_addr - 源地址出参指针(可为 NULL = 不关心来源) struct net_sockaddr *
* addrlen - 出参长度指针(可为 NULL) int *
* 出口参数:无
* 返回值:0 - 登记成功 int
* -1 - 失败(NET_ERR_BADF / NET_ERR_BUSY:本 fd 已有在途接收)
* 限定条件:任意上下文(临界区内检查并占位,与 netTask 的读改不竞态)
* 函数说明:占位成功后本 fd 的第二次接收请求会被拒(BUSY)——同一 fd 并发
* recvfrom 在数据报语义上无法区分归属,拒绝比交错投递安全。
* 调用方若入队失败必须 net_udp_outparam_disarm 归还槽位。
*/
int net_udp_outparam_arm(int sockfd, struct net_sockaddr *src_addr, int *addrlen);
/*
* 函数功能:归还源地址出参槽(调用方上下文,入队失败/取消后的清理)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* 出口参数:无
* 返回值:无(越界或空槽静默返回)
* 限定条件:任意上下文;仅在**未收到 netTask 回执**的失败路径调用
* 函数说明:netTask 结算路径自己清槽(幂等),本函数只服务调用方侧的
* "消息没投递出去"这一条出口,防槽位泄漏导致该 fd 永久 BUSY。
*/
void net_udp_outparam_disarm(int sockfd);
/*
* 函数功能:UDP 绑定/打开芯片侧 Socket(netTask 上下文执行体,恒服务器模式)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* local_port - 本地(源)端口,主机序;0 = 由本函数分配动态端口 uint16_t
* 出口参数:无
* 返回值:0 - 成功 int
* -1 - 失败(NET_ERR_BADF 描述符非法 / NET_ERR_INVAL 非 DGRAM /
* NET_ERR_BUSY 芯片 open 被拒;errno 已记)
* 限定条件:netTask 上下文;sockfd 对应控制块在用且为 DGRAM
* 函数说明:1. 序列按手册 §9.2.4:Socket 4~7 兜底重设收发缓冲 → SET_PROTO_TYPE(UDP)
* → SET_SOUR_PORT → SET_DES_IP(0xFFFFFFFF) → SET_DES_PORT(0) → OPEN_SOCKET。
* 2. open 之后写端口/IP 会被 open 复位,故目的地址必须在 open 前下发
* (docs/网络/BSD_Socket_API_函数调用关系.md §4 调用链表)。
* 3. DesIP 恒取 0xFFFFFFFF = **服务器模式**:BSD 的 bind 只表达本地地址、
* 不选对端,客户端模式由 net_udp_set_peer_exec / sendto(带目的地址) 进入。
* 4. 动态端口分配在本函数内做(原在调用方上下文的 net_bind 里写
* s_dynamic_port——芯片事务既然挪进 netTask,端口簿记一并挪,
* 避免两个上下文改同一个静态量;取号游标经 net_dynamic_port_next 共用)。
* 5. 顺带本 fd 生命周期收口:清挂起接收槽、udp_mode 置 SERVER、清对端
* 簿记、清 readable(重开不得继承上一条连接的残留电平)。
*/
int net_udp_bind_exec(int sockfd, uint16_t local_port);
/*
* 函数功能:设置芯片 DesIP/DesPort 并切换 UDP 模式(不重开 Socket,netTask 执行体)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* peer - 非 NULL = 客户端模式(对端);NULL = 服务器模式 const struct net_sockaddr_in *
* 出口参数:无
* 返回值:0 - 成功 int
* -1 - 失败(NET_ERR_BADF / NET_ERR_INVAL,errno 已记)
* 限定条件:netTask 上下文;控制块在用、type == DGRAM、芯片侧 Socket 已打开
* 函数说明:公开语义见 net_socket.h net_udp_set_peer。只写两个寄存器
* (SET_DES_IP + SET_DES_PORT,约数十微秒),不 CLOSE/OPEN,
* 故接收缓冲里已到的数据不丢。
*/
int net_udp_set_peer_exec(int sockfd, const struct net_sockaddr_in *peer);
/*
* 函数功能:UDP 发送执行体(netTask 上下文,分块 + 有界自旋 + 挂起续传)
* 入口参数: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 = 已登记挂起 send;
* -1 - 失败(NET_ERR_INVAL / NET_ERR_BADF,errno 已记) int
* 限定条件:netTask 上下文
* 函数说明:1. dest 非 NULL → 先 SET_DES_IP/SET_DES_PORT,并把 udp_mode 置
* CLIENT、对端簿记写入 remote_ip_arr/remote_port(芯片静默进
* 客户端模式是硬件事实,本层必须如实记账,否则收包剥头判错)。
* 2. 分块与等待语义与 net_send_locked 完全同源(NET_SEND_CHUNK_MAX /
* NET_SEND_SPIN_MS / NET_SEND_TIMEOUT_MS / 末块不等 SENBUF_FREE /
* 短写留痕),复用挂起 send 表推进——**不复制一份 UDP 专用自旋**,
* 两条路径分块粒度漂移就是 TRAP-NET-16 的复发形态。
* 3. 载荷指针在挂起期间由调用方持有(调用方阻塞在 net_wait_result,
* 栈与静态缓冲均有效),与 TCP 挂起 send 同一条铁律。
*/
int net_udp_sendto_exec(int sockfd, const void *buf, int len,
const struct net_sockaddr_in *dest, TaskHandle_t caller);
/*
* 函数功能:UDP 接收执行体(netTask 上下文:即时读 / 登记挂起 / 立即拒)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* buf - 调用方缓冲 void *
* len - 缓冲容量(>0) int
* flags - NET_MSG_DONTWAIT = 单次非阻塞 int
* to_ms - 等待预算(0 = 无限等待,须 blocking 属性) uint32_t
* caller - 调用方任务句柄(挂起完成通知目标) TaskHandle_t
* 出口参数:无(源地址经 net_udp_outparam_arm 登记的指针直接写回调用方内存)
* 返回值:实际接收字节数(> 0);NET_ERR_RETURN_1(WOULDBLOCK) = 非阻塞无数据;
* NET_PENDING_REGISTERED = 已登记挂起;-1 = 失败 int
* 限定条件:netTask 上下文;控制块在用且为 DGRAM
* 函数说明:1. 判据 = ch395f_get_recv_len()(芯片事实),readable 仅作省 SPI 的
* 门(同 net_pending_tick:事件未收到时 net_poll 每轮重读电平型 SINT
* 会重新置位,U15 探针同源结论)。
* 2. 服务器模式(udp_mode == SERVER)且本包 >= 8 字节 → 先读 8 字节
* 信息头解析源地址再读载荷;客户端模式 → 整包都是载荷,源地址 =
* 本 Socket 固定对端(remote_ip_arr/remote_port)。
* 3. **一个数据报一次读完**:可用字节 > len 时读满 len 后把剩余部分
* 读空丢弃(BSD 截断语义;手册 §9.2.4 对服务器模式要求一次性全部
* 读出)。旧实现只读 min(可用, len) 把半包留在芯片缓冲里,下一次
* 收包会把上一包的尾巴当成新包头的起点——整条链路的源地址从此错位。
* 4. 读空后清 readable(旧实现从不清,net_select 会永久报可读)。
* 5. 无数据 + 阻塞 → 登记挂起(同 fd 已有挂起 / 执行任务自身 → 拒绝,
* BUSY),netTask 零占用返回;完成条件由 net_udp_tick 判定。
*/
int net_udp_recvfrom_exec(int sockfd, void *buf, int len, int flags,
uint32_t to_ms, TaskHandle_t caller);
/*
* 函数功能:挂起 UDP 接收完成检查(netTask 每轮 net_process_messages 调用一次,非阻塞)
* 入口参数:p_socks - Socket 控制块表首指针(net_socket.c 的 s_net_socks) net_sock_t *
* 出口参数:无
* 返回值:无
* 限定条件:netTask 上下文
* 函数说明:逐 fd 按序判定:①fd 已关闭/已非 UDP → 以 NET_ERR_CONNRESET 结束(必通知)
* ②数据到达 → 读包(含剥头/截断/清 readable)+ 回填源地址 + 以字节数结束
* ③到点(仅显式预算)→ 以 NET_ERR_TIMEDOUT 结束(调用方 net_recvfrom_to
* 映射为对外 -2)。三条出口都必须经统一出口通知,漏通知 = 调用方永久挂死。
*/
void net_udp_tick(net_sock_t *p_socks);
/*
* 函数功能:清除本 fd 的 UDP 接收槽(不通知;"新 fd 不得继承"与 close 后幂等清理)
* 入口参数:sockfd - Socket 描述符 (0~7) int
* 出口参数:无
* 返回值:无(越界静默返回)
* 限定条件:任意上下文(netTask 的 close/socket 分配路径调用)
*/
void net_udp_clear(int sockfd);
/*
* 函数功能:取消全部挂起 UDP 接收并逐一通知(网络重建/复位路径)
* 入口参数:err - 通知的错误码 int
* 出口参数:无
* 返回值:无
* 限定条件:netTask 上下文;须在控制块清零与芯片复位之前调用
*/
void net_udp_cancel_all(int err);
/*
* 函数功能:关闭前结算本 fd 的挂起 UDP 接收(有数据先送达,无数据则取消)
* 入口参数:sockfd - 将被关闭的描述符 (0~7) int
* p_sock - 将被关闭的控制块指针(与 sockfd 对应) net_sock_t *
* 出口参数:无
* 返回值:无(无在位挂起时静默返回)
* 限定条件:netTask 上下文;须在 ch395f 关闭/复位该 Socket 之前调用
* 函数说明:与 net_pending_settle_close 同一契约——关闭不丢已到达数据
*/
void net_udp_settle_close(int sockfd, net_sock_t *p_sock);
#endif /* NET_UDP_H */