feat(net/udp): UDP 数据面并入 netMsgQueue 串行执行(#73)
前置来自 #57 SNTP 调研:旧 net_sendto / net_recvfrom 由调用方直调 SPI 且无限自旋收包,既违约「netTask 是芯片唯一访问者」,又把 IWDG 预算吃在自旋里。 改造面 - 新增 Drivers/BSP/NET/net_udp.c/.h(真机专属,模拟器不编译): bind/set_peer/sendto/recvfrom 四条执行体 + 挂起收包表 + tick; net_msg_type_t 加四型,公开 API 一律「入队 → 睡在通知上」。 - 源地址出参不搭 net_msg_t(按值拷贝,AGENTS 禁加回传字段):改住 net_udp.c 的 per-fd 槽,netTask 就地写回调用方内存,通知值只带 结果码;每条出口都清槽,泄漏 = 该 fd 永久 BUSY。 - 发送复用 net_send_locked,不写第二份分块逻辑;「会话是否已死」 收成一条判据 net_send_state_down()(TCP 看 ESTABLISHED、UDP 看 UDP_OPEN),原先 4 处各写一遍。 - 顺带修掉真缺陷(新条 TRAP-NET-22):旧 sendto 把 DesIP 写成具体 IP,芯片静默切 UDP Client,而旧 recvfrom 无条件剥 8 字节 → SNTP 这类 48 B 应答丢载荷前 8 字节并交出垃圾源地址。加 udp_mode(塞进 原有 padding,sizeof 不变)、bind 恒 SERVER、显式对端 sendto 切 CLIENT、set_peer(fd, NULL) 轻量回 SERVER,仅 SERVER 且 avail >= 8 才剥头。 - 截断定案:一次调用交付一个数据报,buf 装不下的尾巴用 64 B 栈上 scratch 排空(不用 clear_recv_buf——它连后面已到达的包一起丢)。 - 取消点接齐:net_init 重建 cancel_all、net_socket 分配 fd clear、 net_close settle_close + clear、重 bind 前先结算等包者, net_process_messages 第 5 个 tick 位挂 net_udp_tick。 - 收紧两处契约(原实现无生产调用者):UDP 未 bind 一律 -5; net_recv / _nb / _to 拒 DGRAM(绕过剥头就是静默吃 8 字节); NET_ERR_NOT_IN_NETTASK(-12) 退役为历史常量并写明原因。 验证 - 主机单测 test/host/net_socket 全套 616/616 绿(改造前 470):新 分片 ns_s7_udp.c 20 组,改写 440 / 555 / 560 组;覆盖率 net_udp.c 234/236(99.2%)、net_socket.c 956/969(98.7%), scripts/test_coverage_baseline.json 随本提交登记新文件。 - 测试抓到 4 个被测码真 bug:自等守卫误用调用侧谓词、芯片长度被读 两次、槽指针取在 fd 合法性检查之前、重 bind 静默作废等待者。 - Keil ARMCC 0 Error(s) 0 Warning(s);net_udp.o Code 1,358 / ZI 224 (8 fd × 28 B,其余模块零增长),全仓 RW_IRAM1 72.2%;内存台账 受管区按门禁提示用 --write / --update 机器回填,未手改数字。 - pre-commit 全量钩子(含 doc-links、coding-standard、mem-budget、 generated-fresh、bsp-err、registration、nested-comment)全绿。 限度 - 本轮无板子:真机回归 5 条(两端互通、set_peer(NULL) 能否恢复收 广播、截断排空后下一包完整性、挂起 recv 期间 IWDG 余量、bind 写 DesIP 与 open 先后的真芯片效果)已写进覆盖率口径「台架必验」。 - scripts/stack_report.py 退出码 1 是 10-26 起挂在账上的 iec104Task 栈债(margin 1,016 < 1,024,见 docs/代码规范/pre-commit使用说明.md: 122);本轮只让 net_bind 的 UDP 分支多一层静态帧(1,544 → 1,600), netTask margin 1,888 与 timeSyncTask 1,152 不受影响。抬栈要过 mem-budget 台账,不在本分支自行处理。 - 登记簿销项按仓库约定留给主工作树,本分支不动 docs/任务规划/**。
This commit is contained in:
@@ -33,6 +33,7 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修
|
||||
| TRAP-NET-19 | SET_MAC 顺序 | ✅ | §9.2.1:SET_MAC 必须在 `INIT_CH395` **之前**(INIT 从 EEPROM 加载) |
|
||||
| TRAP-NET-20 | FTP 数据连接三连故障 | ✅ | 三重定案:状态回读补 accept + 幽灵甄别 / closed-with-data / 背靠背小段毒化 TX("丢中断"归因已被 E14 复核推翻) |
|
||||
| TRAP-NET-21 | IP 显示段序与对端地址 | ✅ | `net_inet_ntoa` 段序 MSB 首;对端地址用 `net_getpeername`,勿用 `getsockname` |
|
||||
| TRAP-NET-22 | UDP 形态与剥头不一致 | ✅ | DesIP 决定有无 8 字节信息头;旧 `sendto` 静默把芯片切成 Client 形态而 `recvfrom` 仍剥头 → 应答丢前 8 字节 |
|
||||
|
||||
---
|
||||
|
||||
@@ -204,6 +205,7 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修
|
||||
- **现象**:UDP 发送后立即接收,接收长度恒 0 或数据错误。
|
||||
- **根因(定案)**:手册要求每次 `WRITE_SEND_BUF` 后必须等 `SINT_STAT_SENBUF_FREE`,否则下次写入被静默丢弃。
|
||||
- **修复**:每次 `ch395f_write_send_buf()` 后轮询 `ch395f_get_sock_int_status()` 检查该标志。
|
||||
- **现状(2026-11-03 UDP 队列化后)**:UDP 发送已改由 netTask 内的 `net_send_locked()` 执行(与 TCP 同一条分块 / 等 SENBUF_FREE 路径,见 `docs/网络/BSD_Socket_API_函数调用关系.md` §4.11),本条要求由该实现统一满足;调用方上下文不再直接碰 SPI,也不再自旋。
|
||||
|
||||
## TRAP-NET-14 · NET 层目的 IP 字节序写反 → CONNECT TIMEOUT
|
||||
|
||||
@@ -235,6 +237,17 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修
|
||||
- **教训**:显示层字节序 bug 不影响协议本身(FTP 功能全程正常),但会系统性误导排障——**日志里的地址也要当被测对象核对**。
|
||||
- **后续(2026-10-24):同一文件里的`s_addr` 两套约定已统一**。修完段序之后,本条只解决了显示侧——当时 `net_inet_addr` 仍属**另一族**(小端累加后又 `NET_HTONL`,返回 `0xC0A80164`),而 `net_inet_ntoa` / `net_getsockname` 属 POSIX 网络序那族(`0x6401A8C0`),两者**不互逆**;按前者写的消费者(`net_connect_locked` 用 `>>24` 取首段)恰好自洽,所以一直没人撞上。现已统一到 **POSIX 网络序一族**(判据 `&s_addr[0] == 第一段`;小端上 192.168.1.100 = `0x6401A8C0`):改 `net_inet_addr` + 出向 IP 数组改按内存字节顺位拆装(**进与出同时换族 → 芯片侧数组净效果不变**)+ 模拟器 `simulator/sim/net_socket_sim.c` 逐字同步;`net_inet_ntoa` 函数体一字未动。**为什么它能长期不被发现**:本条"修复"行下方 `ntoa` 自己注释里的例子(`192.168.1.100 → 0xC0A80164`)**取的是废弃族的值**——注释替错误背书,比没有注释更糟。规范定义见 `docs/网络/BSD_Socket_API_使用指南.md` §9.0;往返不变量由 TC-NETSOCK-100g / 100h 钉住。
|
||||
|
||||
## TRAP-NET-22 · UDP 形态(DesIP)与剥头不一致 → 应答丢前 8 字节
|
||||
|
||||
- **状态**:✅(2026-11-03 修复,UDP 队列化改造顺带照出)
|
||||
- **现象**(改前):`net_bind(UDP)` 后 `net_sendto()` 发请求、再 `net_recvfrom()` 收应答,**收到的 48 字节应答少了前 8 字节**(SNTP 用例里魔数 / 时间戳整体左移 8 字节),且 `src_addr` 是垃圾。
|
||||
- **根因(定案)**:CH395F 的 UDP 有两种形态,**由 DesIP 这一个寄存器决定,写一次即全局生效**(手册 §9.2.4):DesIP = `0xFFFFFFFF` → **UDP Server**,每个到达包**前置 8 字节信息头**(`[0-1]` 长度、`[2-3]` 源端口、`[4-7]` 源 IP);DesIP = 具体 IP → **UDP Client**,**不带**信息头,且只收该对端的包。旧实现 `net_bind()` 把 DesIP 写成 `0xFFFFFFFF`(服务器形态),但 `net_sendto()` 为了发出这个包把 DesIP 改成目的 IP —— **芯片就此静默切到 Client 形态**,而 `net_recvfrom()` 仍按服务器形态**无条件剥掉 8 字节**(判据只有 `recv_len >= 8`,没有形态条件)。
|
||||
- **为什么长期没人发现**:全仓无生产 UDP 调用者(只有 `test/board` 的硬件层用例,直接用 `ch395f_*`、不经这层 API),`docs/网络/BSD_Socket_API_使用指南.md` §6 甚至写着"本项目不使用 UDP";唯一会踩中它的是**请求-响应式单包应答**协议(SNTP / DHCP 类),本仓一个都没有。
|
||||
- **修复(定案)**:`net_sock_t` 新增 `udp_mode`(`NET_UDP_MODE_SERVER / CLIENT`,住 `net_types.h`,塞进原有 2 字节 padding → `sizeof(net_sock_t)` 不变);`net_bind()` 恒置 SERVER(BSD 语义里 bind 不选对端);显式对端的 `net_sendto()` 切 CLIENT;新增 `net_udp_set_peer(fd, NULL)` 回 SERVER(只写 DesIP/DesPort、**不重开 Socket**,因此不清芯片缓冲;要把残留包一起丢就再 `net_bind()`);剥头条件收紧为 `udp_mode == SERVER && avail >= 8`,CLIENT 形态源地址取对端记账。
|
||||
- **教训**:① **芯片的"形态"是隐式全局状态,API 里每一次"顺手写一个寄存器"都可能切换它**——写之前先问"这一下改变了芯片的什么模式、谁负责改回来";② **解析收包字节流必须有形态前提**,`if (len >= 8) 剥 8` 这种"看着像头就剥"的写法,在协议边界上迟早吃掉载荷;③ "没有生产调用者"不等于"被测过"——这层 API 的 UDP 路径是本轮第一次被端到端测到(`ns_s7_udp.c` UDP-1~14 + TC-NETSOCK-440 / 555)。
|
||||
- **关联**:`docs/网络/BSD_Socket_API_使用指南.md` §6.0(形态表 + 谁负责切换)/ §6.3;`docs/网络/BSD_Socket_API_函数调用关系.md` §4.11;`docs/测试说明/单元测试/主机单测覆盖率口径.md`「第 4 轮」;TRAP-NET-14(同一族字节序口径:信息头里源端口小端、源 IP 点分内存顺位);TRAP-NET-10(收包出参为何不能搭 `net_msg_t`)。
|
||||
- **未上台架**:本条差异在主机测里靠假件契约钉住(真芯片不校验我们写的值),真机复验项见"第 4 轮"台架清单。
|
||||
|
||||
---
|
||||
|
||||
# 第五章 上层集成与主机侧工具
|
||||
@@ -253,6 +266,8 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修
|
||||
- **根因(定案)**:`osMessageQueue` 传 `net_msg_t` 是**值拷贝**——netTask 修改的是队列副本,调用方栈变量的 `msg.result` 从未被更新。
|
||||
- **修复**:结果搭 `xTaskNotify(caller, (uint32_t)msg.result, eSetValueWithOverwrite)` 通知值回来,调用方 `xTaskNotifyWait` 取回。**勿给 `net_msg_t` 加"回传用"字段**(调用方只会读到栈垃圾)。
|
||||
- **注意**:指针成员(`msg.buf`)不受值拷贝影响;内核级 `_locked` API 直接操作不受影响。
|
||||
- **现状补充(2026-11-03)**:UDP 队列化时"要回传的不止结果码、还有源地址"这一类需求,按本条纪律**没有**往 `net_msg_t` 加字段,而是走 per-fd 出参槽(调用方 arm 自己的内存,netTask 就地写回,通知值只带结果码)——机制见 `docs/网络/BSD_Socket_API_函数调用关系.md` §4.11,坑见 TRAP-NET-22。
|
||||
- **后续(2026-11-03)· UDP 的出参怎么回来**:`net_recvfrom` 的 `src_addr` / `addrlen` 同样**不进** `net_msg_t`——既不能加"回传用"字段,也不建议把调用方指针塞进 `msg.buf` / `msg.len` 这两个已有语义的槽(`net_accept` 那么做是历史例外,它没有别的出参要带)。新模块 `net_udp.c` 改用 **per-fd 出参槽**:调用方 `net_udp_outparam_arm()` 登记指针 → netTask 执行体经该指针就地写回调用方自己的内存 → 通知值只带结果码。两条硬约束:**互斥**(两个任务收同一 fd 时,前者的源地址会被写进后者的缓冲)与**每条出口都清**(槽泄漏 = 该 fd 从此永久 `BUSY`)。
|
||||
|
||||
## TRAP-NET-13 · DHCP 成功后 IP 寄存器提交滞后
|
||||
|
||||
|
||||
Reference in New Issue
Block a user