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`,
与改造前的产品基线同尺寸。判据文字与真机证据见同批的测试、文档提交。
This commit is contained in:
2026-11-04 09:15:17 +08:00
parent 1b5e5d424c
commit b5671e8c02
4 changed files with 116 additions and 76 deletions
+20 -12
View File
@@ -2629,14 +2629,17 @@ int net_drop_conn(int sockfd) {
* 取代 2026-10-24「先把危险面关掉」的临时守卫 NET_ERR_NOT_IN_NETTASK)。 * 取代 2026-10-24「先把危险面关掉」的临时守卫 NET_ERR_NOT_IN_NETTASK)。
* 执行任务(netTask,或硬件层的 testTask)上下文直接执行不投自己—— * 执行任务(netTask,或硬件层的 testTask)上下文直接执行不投自己——
* 板级用例仍可在自己任务里裸调本接口。 * 板级用例仍可在自己任务里裸调本接口。
* 3. 带 dest_addr 会把芯片切成 **UDP 客户端模式**(DesIP=具体 IP):此后芯片 * 3. 带 dest_addr 会把芯片的对端约束切成 **UDP 客户端形态**(DesIP=具体 IP):
* 只收该 IP 的包、且不再附带 8 字节信息头。本层按 p_sock->udp_mode 记账, * 此后芯片只收该 IP 的包、发送也固定走它。⚠ 8 字节信息头并不因此消失:
* net_recvfrom 据此决定剥不剥头——改造前恒剥头,客户端模式下会吃掉 * 头在不在取决于**开 Socket 那一刻**的 DesIP(本层一律 0xFFFFFFFF),
* 载荷前 8 字节并回填一个假源地址(SNTP 这类"请求-应答"用法必踩)。 * 2026-11-04 08:16 真机 TC-NET-1603 实测 48 B 载荷可读 56 B、首字节 0x38。
* 本层按 p_sock->udp_mode 只记这份对端账,net_recvfrom 一律剥头、一律从
* 头里取源地址(为什么不能按 udp_mode 决定剥不剥,见 net_types.h UDP 形态
* 注释与 TRAP-NET-22 的 2026-11-04 追加段)。
* 4. 分块粒度、停滞超时、挂起 send 续传与 TCP 共用 net_send_locked, * 4. 分块粒度、停滞超时、挂起 send 续传与 TCP 共用 net_send_locked,
* 不留第二份自旋。单包超过 NET_SEND_CHUNK_MAX(1024) 会被写成多个数据报 * 不留第二份自旋。单包超过 NET_SEND_CHUNK_MAX(1024) 会被写成多个数据报
* (改造前后一致);一个应用数据报请控制在 1024 B 内。 * (改造前后一致);一个应用数据报请控制在 1024 B 内。
* 5. dest_addr == NULL = 不改 DesIP,沿用上次的对端(与改造前一致)。 * 5. dest_addr == NULL = 不改 DesIP,沿用上一次写进芯片的对端(与改造前一致)。
* 6. flags 目前不被本层使用(与改造前一致,发送不接受 DONTWAIT 语义)。 * 6. flags 目前不被本层使用(与改造前一致,发送不接受 DONTWAIT 语义)。
* 7. 直接路径(执行任务上下文)不做挂起续传:芯片发送缓冲超 2 ms 未腾出时 * 7. 直接路径(执行任务上下文)不做挂起续传:芯片发送缓冲超 2 ms 未腾出时
* 返回 NET_ERR_BUSY,而不是像改造前那样自旋到 1000 ms 后短写。 * 返回 NET_ERR_BUSY,而不是像改造前那样自旋到 1000 ms 后短写。
@@ -2733,11 +2736,14 @@ int net_sendto(int sockfd, const void *buf, int len, int flags,
* 返回值:实际接收字节数 int > 0 || -1 * 返回值:实际接收字节数 int > 0 || -1
* -1 - 接收失败(精确码见本任务 net_get_errno()) int * -1 - 接收失败(精确码见本任务 net_get_errno()) int
* 限定条件:Socket 已创建且为 UDP 类型(SOCK_DGRAM)**并已 net_bind**;net_init() 已调用 * 限定条件:Socket 已创建且为 UDP 类型(SOCK_DGRAM)**并已 net_bind**;net_init() 已调用
* 函数说明:1. 一次调用交付**一个完整数据报**:服务器模式剥掉芯片的 8 字节信息头并 * 函数说明:1. 一次调用交付**一个完整数据报**:剥掉芯片的 8 字节信息头并据此填
* 据此填 src_addr;客户端模式整包都是载荷、src_addr 取本 Socket 的固定 * src_addr。剥头**不看 udp_mode**——本层 Socket 一律在 DesIP=0xFFFFFFFF
* 对端(芯片在该模式下不加头,见 net_types.h UDP 模式宏注释)。 * 的形态下开出,芯片就给每个收到的数据报加头,运行时把 DesIP 改成具体
* 剥不剥头按 p_sock->udp_mode 判定,不再按"字节数 >= 8"猜——猜错即吃掉 * 对端(显式 dest_addr 的 net_sendto / net_udp_set_peer)并不会让头消失
* 载荷前 8 字节(本接口改造前的缺陷,SNTP 应答 48 B 会只剩 40 B)。 * (2026-11-04 08:16 真机 TC-NET-1603 实测:客户端形态下 48 B 载荷可读
* 56 B、首字节 0x38 = 信息头长度字段;详见 net_types.h UDP 形态注释与
* TRAP-NET-22 的 2026-11-04 追加段)。只有不足 8 字节的残包无从判头,
* 整包按载荷交付、源地址退到本 Socket 的对端记账。
* 2. 载荷超出 len 时交付前 len 字节并把剩余部分**读空丢弃**(手册 §9.2.4 * 2. 载荷超出 len 时交付前 len 字节并把剩余部分**读空丢弃**(手册 §9.2.4
* 要求服务器模式一次性读完;留半包会让下一个数据报从尾巴开始解析)。 * 要求服务器模式一次性读完;留半包会让下一个数据报从尾巴开始解析)。
* 3. 无数据时(与 net_recv 同构):DONTWAIT / 非阻塞属性立即返回 -1 * 3. 无数据时(与 net_recv 同构):DONTWAIT / 非阻塞属性立即返回 -1
@@ -2858,8 +2864,10 @@ int net_udp_set_peer(int sockfd, const struct net_sockaddr_in *peer) {
* 出口参数:无 * 出口参数:无
* 返回值:NET_UDP_MODE_SERVER / NET_UDP_MODE_CLIENT;-1 = 描述符非法 int * 返回值:NET_UDP_MODE_SERVER / NET_UDP_MODE_CLIENT;-1 = 描述符非法 int
* 限定条件:无(纯读控制块,任意上下文) * 限定条件:无(纯读控制块,任意上下文)
* 函数说明:给调用方与自测用:sendto(带目的地址) 之后模式是 CLIENT,此时 * 函数说明:给调用方与自测用:它报的是**对端形态**(下一次不带地址的 sendto 发往
* recvfrom 不剥信息头;bind 之后是 SERVER。 * 谁)——sendto(带目的地址) 之后是 CLIENT,bind / set_peer(NULL) 之后是
* SERVER。收包帧格式与之无关:本层 Socket 恒在全 F 形态下 open,芯片恒
* 前置 8 字节信息头,recvfrom 两种形态都剥(TRAP-NET-22 追加段)。
*/ */
int net_udp_get_mode(int sockfd) { int net_udp_get_mode(int sockfd) {
const net_sock_t *p_sock = net_get_sock(sockfd); const net_sock_t *p_sock = net_get_sock(sockfd);
+41 -28
View File
@@ -367,11 +367,15 @@ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms);
* 函数说明:1. 目的地址是**入参**,按值拷进 net_msg_t.addr 随消息进 netTask, * 函数说明:1. 目的地址是**入参**,按值拷进 net_msg_t.addr 随消息进 netTask,
* 与 net_connect 传地址同一条通道;结果(已发字节数/错误码) * 与 net_connect 传地址同一条通道;结果(已发字节数/错误码)
* 搭任务通知值回来(AGENTS「勿给 net_msg_t 加回传字段」)。 * 搭任务通知值回来(AGENTS「勿给 net_msg_t 加回传字段」)。
* 2. 带显式目的地址 = 芯片 DesIP 切到该 IP → 本 Socket 进入 * 2. 带显式目的地址 = 芯片 DesIP/DesPort 切到该对端 → 本 Socket 记账进入
* **UDP 客户端模式**(p_sock->udp_mode = NET_UDP_MODE_CLIENT): * **UDP 客户端形态**(p_sock->udp_mode = NET_UDP_MODE_CLIENT):此后
* 此后芯片只收该 IP 的包且**不再添加 8 字节信息头**, * `dest_addr = NULL` 的发送就发往它。**收包帧格式不受影响**——信息头是
* net_recvfrom 相应地不剥头、源地址回填该固定对端。 * OPEN_SOCKET 那一刻按当时的 DesIP 定死的,本层 Socket 一律由 net_bind
* 要回服务器模式(任意源 + 带信息头)调 net_udp_set_peer(sockfd, NULL)。 * 在全 F 形态下开出,所以恒带 8 字节信息头、net_recvfrom 恒剥(真机实测
* 见 TRAP-NET-22 的 2026-11-04 追加段)。运行时改窄 DesIP 会不会让芯片
* 按源过滤,台架未测出结论,多源服务别依赖它。
* 要回服务器形态(任意源可发、下一次 NULL 发送不再沿用旧对端)调
* net_udp_set_peer(sockfd, NULL)。
* 3. dest_addr=NULL 时不改 DesIP:服务器模式下 DesIP=0xFFFFFFFF 即 * 3. dest_addr=NULL 时不改 DesIP:服务器模式下 DesIP=0xFFFFFFFF 即
* 向 255.255.255.255 广播(板级 UDP 广播测试的用法),模式不变。 * 向 255.255.255.255 广播(板级 UDP 广播测试的用法),模式不变。
* 4. 分块上限 NET_SEND_CHUNK_MAX(1KB 发送 FIFO,TRAP-NET-16), * 4. 分块上限 NET_SEND_CHUNK_MAX(1KB 发送 FIFO,TRAP-NET-16),
@@ -404,15 +408,19 @@ int net_sendto(int sockfd, const void *buf, int len, int flags,
* 函数说明:1. 与 net_recv 同一条路径(入队 → netTask 串行执行芯片事务 → * 函数说明:1. 与 net_recv 同一条路径(入队 → netTask 串行执行芯片事务 →
* 结果搭通知值回来),**不再是**"仅限执行任务、自旋 net_poll"的 * 结果搭通知值回来),**不再是**"仅限执行任务、自旋 net_poll"的
* 历史形态;非 netTask 调用不再回 NET_ERR_NOT_IN_NETTASK。 * 历史形态;非 netTask 调用不再回 NET_ERR_NOT_IN_NETTASK。
* 2. 一次调用 = 一个数据报:芯片缓冲内该包**全部读空**(服务器模式 * 2. 一次调用 = 一个数据报:芯片缓冲内该包**全部读空**(信息头形态下
* 手册要求一次性读出),载荷超出 len 的部分按 BSD 语义截断丢弃, * 手册要求一次性读出),载荷超出 len 的部分按 BSD 语义截断丢弃,
* 不会留下半包污染下一次解析。 * 不会留下半包污染下一次解析。
* 3. 源地址按 udp_mode 取:服务器模式剥 8 字节信息头 * 3. **恒剥 8 字节信息头,源地址恒取自头部**([2-3] 源端口芯片小端、
* ([2-3] 源端口芯片小端、[4-7] 源 IP 线序字节 → 本层 s_addr 网络序 * [4-7] 源 IP 线序字节 → 本层 s_addr 网络序约定,见 net_inet_addr 注释,
* 约定,见 net_inet_addr 注释,2026-10-24 定案同族); * 2026-10-24 定案同族),**与 udp_mode 无关**:头是 OPEN_SOCKET 那一刻
* 客户端模式不剥头、源地址=本 Socket 固定对端。 * 按当时的 DesIP 定死的,本层 Socket 一律由 net_bind 在全 F 形态下开出,
* ⚠ 客户端模式若仍按服务器模式剥头,等于吃掉载荷前 8 字节 * 之后把 DesIP 改成具体对端并不会让头消失(2026-11-04 08:16 真机
* (SNTP 应答 48 字节会前 8 字节丢失)——这是本字段存在的理由。 * TC-NET-1603 实测 48 B 载荷可读 56 B、首字节 0x38 = 头里的长度字段;
* 定案见 TRAP-NET-22 追加段)。只有不足 8 字节的残包无从判头,整包按
* 载荷交付、源地址退到本 Socket 的对端记账。
* ⚠ 曾按"客户端形态不剥头"实现过一版,真机第一轮就把 8 字节头当载荷
* 交给了应用——不要再把剥头条件改回跟形态挂钩。
* 4. 阻塞语义与 net_recv 同构:无数据 + 阻塞 → **登记挂起、netTask * 4. 阻塞语义与 net_recv 同构:无数据 + 阻塞 → **登记挂起、netTask
* 零占用返回**,数据到点断连由 net_udp_tick 按 1ms 节拍结束; * 零占用返回**,数据到点断连由 net_udp_tick 按 1ms 节拍结束;
* 不再是自旋(旧形态会吃满 IWDG 预算并违反 netTask 契约)。 * 不再是自旋(旧形态会吃满 IWDG 预算并违反 netTask 契约)。
@@ -443,27 +451,32 @@ int net_recvfrom_to(int sockfd, void *buf, int len, uint32_t timeout_ms,
struct net_sockaddr *src_addr, int *addrlen); struct net_sockaddr *src_addr, int *addrlen);
/* /*
* 函数功能:设置/查询 UDP Socket 的芯片模式(客户端 ↔ 服务器,见 net_types.h * 函数功能:设置/查询 UDP Socket 的芯片对端形态(客户端 ↔ 服务器,见 net_types.h
* NET_UDP_MODE_* 与手册 §9.2.4) * NET_UDP_MODE_* 与手册 §9.2.4)——**只管发送去向与对端记账,不改收包帧格式**
* 入口参数:sockfd - socket 描述符(须为 DGRAM) * 入口参数:sockfd - socket 描述符(须为 DGRAM)
* peer - 非 NULL = 客户端模式:DesIP/DesPort 设为该对端,芯片只收 * peer - 非 NULL = 客户端形态:DesIP/DesPort 设为该对端,此后
* 它的包且**不加** 8 字节信息头; * `dest_addr = NULL` 的 net_sendto 就发往它;
* NULL = 服务器模式:DesIP=0xFFFFFFFF、DesPort=0,收任意来源 * NULL = 服务器形态:DesIP=0xFFFFFFFF、DesPort=0,不带地址的
* 且每个包带信息头(net_bind 的默认形态) * 发送不再沿用旧对端(net_bind 的默认形态)
* 返回值:0 - 成功 int * 返回值:0 - 成功 int
* -1 - 失败(NET_ERR_BADF / NET_ERR_INVAL / NET_ERR_BUSY,errno 可查) * -1 - 失败(NET_ERR_BADF / NET_ERR_INVAL / NET_ERR_BUSY,errno 可查)
* 限定条件:socket 已创建且为 DGRAM 且已 net_bind(芯片侧 Socket 已打开); * 限定条件:socket 已创建且为 DGRAM 且已 net_bind(芯片侧 Socket 已打开);
* 任意任务上下文(走消息队列,netTask 内直接执行) * 任意任务上下文(走消息队列,netTask 内直接执行)
* 函数说明:1. 存在的理由:模式由"最近一次写 DesIP"决定,net_sendto 带目的地址 * 函数说明:1. 存在的理由:带目的地址的 net_sendto 发一次就把 DesIP 静默改到了
* 发一次就把芯片静默切到客户端模式。要给多个对端收发(TFTP 式 * 那个对端(TFTP 式"收到谁的包回给谁"必须这么回),回完就得显式切回
* "收到谁的包回给谁、然后继续听所有人")必须在每次带地址发送后 * 服务器形态,否则下一次不带地址的发送会悄悄发给上一个对端。
* 显式回服务器模式,否则回程包会被当成带头的包错解析。 * 本接口是**轻切换**:只写 DesIP/DesPort 两个寄存器、不重开 Socket,
* 2. 只改 DesIP/DesPort 两个寄存器,不重开 Socket(不丢接收缓冲内 * 因此接收缓冲里已经到的应答不会被清掉(SNTP 这类请求-应答靠它)。
* 已到的数据);本层 udp_mode 同步更新,recvfrom 剥头判定据此走。 * 2. **两种形态的收包帧格式相同**:本层 Socket 恒在全 F 形态下 open,
* 3. 客户端 → 服务器切换后**先前**收到的包(无头)无法补上源地址, * 芯片就恒前置 8 字节信息头,运行时改 DesIP 不改变这一点(真机
* 模式切换时刻之后的包才按新模式解析——协议栈请"先切模式再等包"。 * TC-NET-1603 实测,见 TRAP-NET-22 追加段),net_recvfrom 因此恒剥头、
* 4. 切模式不改动 local_port,也不影响已登记的挂起 recv;挂起期间 * 恒从头部取源地址。udp_mode 只用于发送去向与诊断判读。
* 切模式由 tick 在结算时读当前模式解析(同一包不会两解)。 * 3. ⚠ 手册把"只收该 IP 的包"写在客户端形态名下,但**运行时**把 DesIP
* 改窄之后芯片还过不过滤别的源,台架没有测出结论(阶段 16 的 1604 只
* 验到"切回服务器形态后换源端口的新包能收到")。要做多源服务就保持
* 服务器形态、每次发送都带显式目的地址,别把过滤当已证事实。
* 4. 切换不改 local_port,也不影响已登记的挂起 recv;挂起期间切模式由
* tick 在结算时按当前寄存器状态处理(同一包不会两解)。
*/ */
int net_udp_set_peer(int sockfd, const struct net_sockaddr_in *peer); int net_udp_set_peer(int sockfd, const struct net_sockaddr_in *peer);
+27 -17
View File
@@ -51,23 +51,29 @@ extern "C" {
* 具名入口见 net_recv_nb() / net_setblocking()) */ * 具名入口见 net_recv_nb() / net_setblocking()) */
/* /*
* UDP 芯片模式(仅 type == NET_SOCK_DGRAM 有意义;手册 §9.2.4 步骤②: * UDP 芯片对端形态(仅 type == NET_SOCK_DGRAM 有意义;手册 §9.2.4:目的 IP 决定
* **目的 IP 决定模式**,两种模式的收包格式不同,见下) * 芯片的收/发对端约束。两种形态的差别**只在过滤与发送去向**,收包帧格式不在其中,
* 见下第 3 条——这一条是 2026-11-04 真机量出来的,不是推的。)
* *
* 服务器模式 DesIP = 0xFFFFFFFF:可与任意远端通讯,CH395F 在**每个**收到的 * 服务器形态 DesIP = 0xFFFFFFFF(net_bind 的 UDP 分支一律开在这个形态):
* 数据报前添加 8 字节信息头([0-1] 数据长度、[2-3] 源端口(芯片小端)、 * 芯片不按源过滤,可与任意远端通讯;CH395F 在**每个**收到的数据报前添加
* [4-7] 源 IP(线序字节,首段在低地址)),且手册要求一次性把该包全部读出。 * 8 字节信息头([0-1] 数据长度、[2-3] 源端口(芯片小端)、[4-7] 源 IP
* 客户端模式 DesIP = 具体 IP:芯片只收该 IP 的包,且**不加**信息头—— * (线序字节,首段在低地址)),且手册要求一次性把该包全部读出。
* 数据流直接落到接收缓冲区(可整读也可部分读)。 * 客户端形态 DesIP = 具体 IP:芯片只收该 IP 的包,发送也固定走它。
* *
* 为什么本层要存这个状态:模式是芯片属性、由"最近一次写 DesIP"决定,而软件 * ⚠ 信息头是否在,取决于**开 Socket 那一刻**的 DesIP,不取决于之后的 DesIP:
* 原先看不见它——net_sendto 带目的地址发一次就把芯片静默切到客户端模式,之后 * 运行时把 DesIP 从 0xFFFFFFFF 改成具体 IP,头并不会消失。2026-11-04 08:16
* 的收包仍按服务器模式剥 8 字节 = **吃掉载荷前 8 字节**并把垃圾字节当源地址 * 真机 TC-NET-1603 实测——客户端形态下对端发来 48 B,芯片报可读 56 B、首字节
* (SNTP 48 字节应答会丢掉前 8 字节,2026-11-03 定性,见 * 0x38(=56,正是信息头的长度字段)。所以本层 recvfrom **恒剥 8 字节**、
* docs/问题记录/CH395F_Trap_Records.md「UDP 模式」条)。剥不剥头必须由本字段决定。 * 恒从信息头取源地址;曾经"按 udp_mode 决定剥不剥"的写法会把 8 字节头当载荷
* 交给应用(SNTP 48 字节应答只剩 40 字节且前 8 字节是头),那正是
* docs/问题记录/CH395F_Trap_Records.md「TRAP-NET-22」记录的老缺陷的镜像面:
* 改造前恒剥头会在真客户端 Socket 上吃 8 字节,改造初版按运行时形态判剥不剥
* 则在服务器形态开出的 Socket 上多交 8 字节。两头都要靠"开 Socket 时定形态"
* 这一条事实来对齐,本字段只记账发送去向与源过滤。
*/ */
#define NET_UDP_MODE_SERVER 0U /* DesIP=0xFFFFFFFF:收包含 8 字节信息头(net_bind 的默认模式) */ #define NET_UDP_MODE_SERVER 0U /* DesIP=0xFFFFFFFF:芯片不按源过滤收包,发送走信息头/显式目的 */
#define NET_UDP_MODE_CLIENT 1U /* DesIP=具体 IP:收包无信息头,源地址=本 Socket 固定对端 */ #define NET_UDP_MODE_CLIENT 1U /* DesIP=具体 IP:芯片只收该对端、发送也走它;信息头仍在(帧格式看 udp_mode 字段注释) */
/* /*
* 网络字节序转换宏 * 网络字节序转换宏
@@ -186,13 +192,17 @@ typedef struct {
net_sock_state_t state; /* Socket 状态 */ net_sock_state_t state; /* Socket 状态 */
uint8_t type; /* SOCK_STREAM / SOCK_DGRAM */ uint8_t type; /* SOCK_STREAM / SOCK_DGRAM */
uint8_t in_use; /* 是否在用标志 */ uint8_t in_use; /* 是否在用标志 */
/* UDP 芯片模式(NET_UDP_MODE_SERVER / _CLIENT,宏注释见本文件 UDP 模式定义处)。 /* UDP 芯片对端形态(NET_UDP_MODE_SERVER / _CLIENT,宏注释见本文件 UDP 模式定义处)。
* 仅 type == NET_SOCK_DGRAM 有意义;置位/清零路径:net_bind 的 UDP 分支 → * 仅 type == NET_SOCK_DGRAM 有意义;置位/清零路径:net_bind 的 UDP 分支 →
* SERVER(DesIP=0xFFFFFFFF);net_sendto 带显式目的地址、net_udp_set_peer(非 * SERVER(DesIP=0xFFFFFFFF);net_sendto 带显式目的地址、net_udp_set_peer(非
* NULL) → CLIENT(DesIP=该 IP);net_udp_set_peer(NULL) → 回 SERVER。 * NULL) → CLIENT(DesIP=该 IP);net_udp_set_peer(NULL) → 回 SERVER。
* 新 fd / close 由整块 memset 归零 = SERVER,与 bind 默认同值,故不得继承上一条。 * 新 fd / close 由整块 memset 归零 = SERVER,与 bind 默认同值,故不得继承上一条。
* CLIENT 模式的对端簿记复用 remote_ip_arr[4] + remote_port(与 TCP 同一对字段, * ⚠ 这个字段**只描述芯片的对端过滤与发送去向,不描述收包帧格式**:本层 Socket
* 语义同为"本会话对端"),recvfrom 据此回填 src_addr。 * 一律在 DesIP=0xFFFFFFFF 时开出,芯片就固定给每个收到的数据报前置 8 字节信息
* 头,运行时改 DesIP 不会让头消失(2026-11-04 08:16 真机 TC-NET-1603 实测
* n=56 / 首字节 0x38,见 TRAP-NET-22 追加段)。因此 recvfrom 一律剥头、一律从
* 头里取源地址;此处 CLIENT 的对端簿记(复用 remote_ip_arr[4] + remote_port,
* 与 TCP 同一对字段)只用于发送去向与不足 8 字节残包的源地址兜底。
* 位置选择:紧接 in_use 之后,落在 type/in_use 之后原有的 2 字节填充里 * 位置选择:紧接 in_use 之后,落在 type/in_use 之后原有的 2 字节填充里
* (本结构 8 个实例,净增 RAM = 0,见 AGENTS RAM 红线)。 */ * (本结构 8 个实例,净增 RAM = 0,见 AGENTS RAM 红线)。 */
uint8_t udp_mode; uint8_t udp_mode;
+28 -19
View File
@@ -598,14 +598,16 @@ static void net_udp_req_complete(int sockfd, int result) {
} }
/* /*
* 函数功能:从 8 字节信息头回填源地址(服务器模式) * 函数功能:从 8 字节信息头回填源地址(本层收包的正常路径)
* 入口参数:p_sock - 控制块(本函数不用,保留与 peer 版同形签名) net_sock_t * * 入口参数:p_sock - 控制块(本函数不用,保留与 peer 版同形签名) net_sock_t *
* hdr - 已读出的 8 字节信息头 const uint8_t * * hdr - 已读出的 8 字节信息头 const uint8_t *
* src_addr - 调用方出参(可为 NULL) struct net_sockaddr * * src_addr - 调用方出参(可为 NULL) struct net_sockaddr *
* addrlen - 调用方出参长度(可为 NULL) int * * addrlen - 调用方出参长度(可为 NULL) int *
* 出口参数:src_addr - 填充发送方地址;*addrlen = sizeof(sockaddr_in) * 出口参数:src_addr - 填充发送方地址;*addrlen = sizeof(sockaddr_in)
* 返回值:无 * 返回值:无
* 限定条件:仅由 net_udp_datagram_read 在服务器模式分支调用 * 限定条件:由 net_udp_datagram_read 在剥头分支(avail >= 8)调用——本层 Socket
* 一律开在服务器形态,信息头与 udp_mode 无关,故 CLIENT 形态同样走这里,
* 头里的源 IP/端口就是本包的真实来源
* 函数说明:头布局(手册 §9.2.4)[0-1] 数据长度、[2-3] 源端口(芯片小端)、 * 函数说明:头布局(手册 §9.2.4)[0-1] 数据长度、[2-3] 源端口(芯片小端)、
* [4-7] 源 IP(线序字节,[4] = 第一段)。s_addr 直接按"首段落在低字节" * [4-7] 源 IP(线序字节,[4] = 第一段)。s_addr 直接按"首段落在低字节"
* 组装即得本层网络序约定(2026-10-24 定案:全仓 s_addr 只此一族, * 组装即得本层网络序约定(2026-10-24 定案:全仓 s_addr 只此一族,
@@ -642,15 +644,16 @@ static void net_udp_src_from_header(net_sock_t *p_sock, const uint8_t *hdr,
} }
/* /*
* 函数功能:从控制块固定对端回填源地址(客户端模式:芯片不加信息头) * 函数功能:从控制块固定对端回填源地址(不足 8 字节残包的兜底路径)
* 入口参数:p_sock - 控制块(remote_ip_arr/remote_port = 本 Socket 的对端) net_sock_t * * 入口参数:p_sock - 控制块(remote_ip_arr/remote_port = 本 Socket 的对端) net_sock_t *
* src_addr - 调用方出参(可为 NULL) struct net_sockaddr * * src_addr - 调用方出参(可为 NULL) struct net_sockaddr *
* addrlen - 调用方出参长度(可为 NULL) int * * addrlen - 调用方出参长度(可为 NULL) int *
* 出口参数:src_addr - 填充对端地址;*addrlen = sizeof(sockaddr_in) * 出口参数:src_addr - 填充对端地址;*addrlen = sizeof(sockaddr_in)
* 返回值:无 * 返回值:无
* 限定条件:仅由 net_udp_datagram_read 在客户端模式分支调用 * 限定条件:仅由 net_udp_datagram_read 在残包分支(avail < 8)调用
* 函数说明:客户端模式下芯片只收这个 IP 的包,源地址不需要从头里取—— * 函数说明:本层 Socket 一律开在服务器形态、每个数据报都带信息头,正常包都从
* 反过来说它也**没有**头可取,本函数是唯一的源地址来源。 * 头里取源地址;只有无从判头的残包才退到"源地址=本 Socket 的对端记账",
* 至少比报 NULL 或报上一个包的源更接近真相(TC-NETSOCK-440o 定案)。
*/ */
static void net_udp_src_from_peer(net_sock_t *p_sock, struct net_sockaddr *src_addr, static void net_udp_src_from_peer(net_sock_t *p_sock, struct net_sockaddr *src_addr,
int *addrlen) { int *addrlen) {
@@ -678,7 +681,7 @@ static void net_udp_src_from_peer(net_sock_t *p_sock, struct net_sockaddr *src_a
/* /*
* 函数功能:从芯片缓冲读出**一个完整数据报**(剥头 + 截断排空 + 清可读标志) * 函数功能:从芯片缓冲读出**一个完整数据报**(剥头 + 截断排空 + 清可读标志)
* 入口参数:sockfd - Socket 描述符 (0~7) int * 入口参数:sockfd - Socket 描述符 (0~7) int
* p_sock - 控制块(读 udp_mode) net_sock_t * * p_sock - 控制块(只在不足 8 字节的残包上用来取对端记账当源地址) net_sock_t *
* buf - 调用方缓冲 void * * buf - 调用方缓冲 void *
* len - 缓冲容量(> 0) int * len - 缓冲容量(> 0) int
* src_addr - 源地址出参(可为 NULL) struct net_sockaddr * * src_addr - 源地址出参(可为 NULL) struct net_sockaddr *
@@ -687,14 +690,21 @@ static void net_udp_src_from_peer(net_sock_t *p_sock, struct net_sockaddr *src_a
* 出口参数:buf - 载荷(最多 len 字节,多余部分丢弃);src_addr - 发送方地址 * 出口参数:buf - 载荷(最多 len 字节,多余部分丢弃);src_addr - 发送方地址
* 返回值:交付调用方的字节数(>= 0) int * 返回值:交付调用方的字节数(>= 0) int
* 限定条件:netTask 上下文;avail 必须是本函数调用前刚读到的 ch395f_get_recv_len() * 限定条件:netTask 上下文;avail 必须是本函数调用前刚读到的 ch395f_get_recv_len()
* 函数说明:1. 服务器模式剥 8 字节信息头并从中取源地址;客户端模式整包都是 * 函数说明:1. **剥头与 udp_mode 无关,只看开 Socket 时的形态**:本层所有 DGRAM
* 载荷、源地址取固定对端。模式判错 = 吃掉载荷前 8 字节(本函数 * Socket 都由 net_bind 在 `DesIP = 0xFFFFFFFF`(服务器形态)下开出来,
* 存在的理由,见 net_types.h UDP 模式注释)。 * 芯片据此给**每一个**收到的数据报前置 8 字节信息头,此后无论把 DesIP
* 2. 服务器模式手册要求"一次性将数据全部读出";且不论哪种模式, * 改成具体对端(显式 dest_addr 的 net_sendto / net_udp_set_peer)都不
* 载荷超出 len 时必须把剩余部分读空——留半包在芯片缓冲里, * 再改变这一帧格式。2026-11-04 08:16 真机 TC-NET-1603 实测:客户端
* 下一次收包会从上一包的尾巴开始解析,源地址与载荷从此全部错位 * 形态下发来 48 B,芯片给出 n=56、首字节 0x38(=56,正是信息头的长度
* (旧实现只读 min(可用, len),正是这个缺陷)。 * 字段)——运行时改 DesIP 不会让头消失。按 udp_mode 决定剥不剥,就会
* 3. 服务器模式收到不足 8 字节的残包:无从判头,整包按载荷交付 * 把 8 字节头当载荷交给应用(SNTP 48 B 应答只剩 40 B 且首 8 字节是头),
* 这正是 TRAP-NET-22 记录的老缺陷;定案见该条 2026-11-04 追加段。
* 源地址同理一律从头里取:头里的源 IP/端口是**本包**的真实来源,比
* 对端记账更准(对端记账只用于发送与芯片收包过滤)。
* 2. 手册要求"一次性将数据全部读出";且载荷超出 len 时必须把剩余部分
* 读空——留半包在芯片缓冲里,下一次收包会从上一包的尾巴开始解析,
* 源地址与载荷从此全部错位(旧实现只读 min(可用, len),正是这个缺陷)。
* 3. 不足 8 字节的残包:无从判头,整包按载荷交付、源地址取对端记账
* (保持既有定案 TC-NETSOCK-440o,不猜芯片行为)。 * (保持既有定案 TC-NETSOCK-440o,不猜芯片行为)。
* 4. 长度由调用方传入、本函数**不再自己读第二次**:一次收包的剥头量、 * 4. 长度由调用方传入、本函数**不再自己读第二次**:一次收包的剥头量、
* 载荷量、排空量必须由同一个数字算出。读两次等于多一次 SPI,而且 * 载荷量、排空量必须由同一个数字算出。读两次等于多一次 SPI,而且
@@ -715,9 +725,8 @@ static int net_udp_datagram_read(int sockfd, net_sock_t *p_sock, void *buf, int
return 0; return 0;
} }
if ((p_sock->udp_mode == NET_UDP_MODE_SERVER) && if ((uint32_t)avail >= NET_UDP_HDR_LEN) {
((uint32_t)avail >= NET_UDP_HDR_LEN)) { /* 信息头暂存(只在剥头这一支用得到,放在本分支栈上) */
/* 信息头暂存(只在服务器形态用得到,放在本分支栈上) */
uint8_t hdr[NET_UDP_HDR_LEN]; uint8_t hdr[NET_UDP_HDR_LEN];
/* 剥头:读即消费芯片缓冲 */ /* 剥头:读即消费芯片缓冲 */
@@ -725,7 +734,7 @@ static int net_udp_datagram_read(int sockfd, net_sock_t *p_sock, void *buf, int
net_udp_src_from_header(p_sock, hdr, src_addr, addrlen); net_udp_src_from_header(p_sock, hdr, src_addr, addrlen);
payload = avail - (int)NET_UDP_HDR_LEN; payload = avail - (int)NET_UDP_HDR_LEN;
} else { } else {
/* 客户端模式(或服务器模式残包):无头可剥,源地址取固定对端 */ /* 不足 8 字节的残包:无头可剥,源地址取固定对端 */
net_udp_src_from_peer(p_sock, src_addr, addrlen); net_udp_src_from_peer(p_sock, src_addr, addrlen);
payload = avail; payload = avail;
} }