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:
@@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1`
|
||||
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 |
|
||||
|---|---|---|---|
|
||||
| ER_IROM1 | 277,384 B (270.88 KB) | 1,048,576 B (1024 KB) | 26.5% |
|
||||
| RW_IRAM1 | 94,368 B (92.16 KB) | 131,072 B (128 KB) | 72.0% |
|
||||
| ER_IROM1 | 278,960 B (272.42 KB) | 1,048,576 B (1024 KB) | 26.6% |
|
||||
| RW_IRAM1 | 94,624 B (92.41 KB) | 131,072 B (128 KB) | 72.2% |
|
||||
| RW_CCM | 57,376 B (56.03 KB) | 65,280 B (64 KB) | 87.9% |
|
||||
| **Flash (ROM 合计)** | **279,436 B (272.9 KB)** | 1,048,576 B (1024 KB) | **26.6%** |
|
||||
| **Flash (ROM 合计)** | **281,012 B (274.4 KB)** | 1,048,576 B (1024 KB) | **26.8%** |
|
||||
<!-- MEM_SNAPSHOT:END -->
|
||||
|
||||
各区的容量与常量来源:
|
||||
|
||||
@@ -15,14 +15,14 @@
|
||||
<!-- MEM_BUDGET:BEGIN generated by scripts/ram_baseline.py -->
|
||||
| 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) |
|
||||
|---|---|---|---|---|
|
||||
| ER_IROM1 (Flash) | 277,384 B | 1,048,576 B | 26.5% | 参考 |
|
||||
| ER_IROM1 (Flash) | 278,960 B | 1,048,576 B | 26.6% | 参考 |
|
||||
| RW_CCM | 57,376 B | 65,280 B | 87.9% | PASS |
|
||||
| UNINIT_CCM | 48 B | 256 B | 18.8% | 参考 |
|
||||
| RW_IRAM1 | 94,368 B | 131,072 B | 72.0% | PASS |
|
||||
| RW_IRAM1 | 94,624 B | 131,072 B | 72.2% | PASS |
|
||||
|
||||
| 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 |
|
||||
|---|---|---|---|
|
||||
| 网络 | 13,702 B | 12 KB | ⚠ 超 1.4 KB |
|
||||
| 网络 | 13,958 B | 12 KB | ⚠ 超 1.6 KB |
|
||||
| 存储 | 7,072 B | 12 KB | OK |
|
||||
| UI | 36,473 B | 16 KB | ⚠ 超 19.6 KB |
|
||||
| RTOS 堆 | 57,376 B | 56 KB | 堆=配额 |
|
||||
|
||||
@@ -385,7 +385,7 @@ for (;;) {
|
||||
|
||||
- 所有 socket 操作(`net_socket` / `net_bind` / `net_listen` / `net_accept` / `net_recv` / `net_close`)均走 `net_socket` 的**线程安全 API**,内部经消息队列由 `netTask` 串行执行;
|
||||
- netTask 因此只需做 `net_poll()` + `net_process_messages()`,不掺杂任何应用层网络逻辑;
|
||||
- **TCP 的全部 API 已经是基于消息队列的**(已在 `net_socket.c` 实现),而 UDP 的 `net_bind` / `net_sendto` / `net_recvfrom` 绕过队列。选用 TCP 即可让时间同步作为独立任务运行,且**无需改动 `net_socket.c` 的 UDP 路径**。
|
||||
- **TCP 的全部 API 已经是基于消息队列的**(已在 `net_socket.c` 实现);UDP 的 `net_bind` / `net_sendto` / `net_recvfrom` **曾经**绕过队列,**2026-11-03 已并入队列**(改造事实见 `docs/网络/BSD_Socket_API_使用指南.md` §9.0)。也就是说:**"UDP 不安全"不再是本节选 TCP 的理由**——本节结论不变(现有 TCP:8888 时间同步通道继续有效),但若日后要换成 UDP(#57 SNTP 方向),不再需要先改 net 层。
|
||||
|
||||
### 15.5 Socket 分配与冲突
|
||||
|
||||
|
||||
@@ -503,7 +503,7 @@ uint16_t ch395f_link_change_count(void);
|
||||
| 2 | `App/log/sys_log_mcu.c:527~529` | `rc==-1` 打 `DBG_ERROR("drop %lu entries …")`,但该分支 `done=0` → 整批其实**全部回投**、一条没丢 → 文案与行为相反,且持续写故障时每轮 drain 都重投重试一遍 | TC-SYSLOG-036 |
|
||||
| 3 | `App/log/sys_log_mcu.c:616` vs `627~628` | 守卫写成 `desc_len <= SYSLOG_DESC_MAX` 就 `-1`,而 `dl` 恒 ≤ 28 → 截断分支**永不可达**。无害(宁可拒绝不越界),但"传小缓冲能拿到截断内容"的直觉是错的 | TC-SYSLOG-098 / 057 |
|
||||
| 4 | `Drivers/BSP/NET/net_socket.c:2825` / `:2869` / `:3080` | **同一文件两套 IP 字节序约定,`inet_addr` 与 `ntoa`/`getsockname` 不互逆**:`net_inet_addr` 小端组装后又 `NET_HTONL` → `0xC0A80164`;`net_inet_ntoa` 交换后高段在前 → 只能正确还原 `getsockname` 那族(`0x6401A8C0`);`net_connect_locked:1571` 用 `>>24` 取首段 → 只配 `inet_addr` 那族。**为什么长期不被看见**:`ntoa` 自己 `:2865~2867` 那段 E13 修复注释里的例子「192.168.1.100 → 0xC0A80164 → "192.168.1.100"」取的是**另一族**的值(交换宏下实算 `"100.1.168.192"`)。前提已核清:`Drivers/BSP/NET/net_types.h`:56 是 `#ifdef __BIG_ENDIAN` 才恒等,STM32 与 x86/MSVC 都走交换分支 → **不存在"主机语义≠目标语义"**。当前无调用者跨族(`lftpd.c`:864 走 `getpeername→ntoa` 同族恰好正确),属埋在库 API 面上的地雷;统一必须跨 `inet_addr`/`ntoa`/`getsockname`/`connect` 解析/`lftpd.c`:548 一起过 | TC-NETSOCK-100b / 100c / 100d、107f、430h(钉的是**具体数值**,统一时会五条一起红并互相指认,不会被误读成单点回归) |
|
||||
| 5 | `Drivers/BSP/NET/net_socket.c:2408` / `:2537` | **UDP 两个公开 API 绕过 netTask 串行化**:`net_sendto`/`net_recvfrom` 直接 `net_poll()`(`:2482`/`:2501`/`:2563`)+ `ch395f_write_send_buf`(`:2487`)/`ch395f_read_recv_buf`(`:2575`/`:2604`),**全程不经 `netMsgQueue`** → 与 AGENTS「所有 `net_*` 经 netTask 串行执行」及 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-14 正面冲突。全仓无生产调用者(只有 `test/board` 与模拟器)所以未爆。建议:改走消息队列,或在 `net_socket.h` 显式标注"未串行化,仅限 netTask" | TC-NETSOCK-440 |
|
||||
| 5 | `Drivers/BSP/NET/net_socket.c:2408` / `:2537` | **UDP 两个公开 API 绕过 netTask 串行化**:`net_sendto`/`net_recvfrom` 直接 `net_poll()`(`:2482`/`:2501`/`:2563`)+ `ch395f_write_send_buf`(`:2487`)/`ch395f_read_recv_buf`(`:2575`/`:2604`),**全程不经 `netMsgQueue`** → 与 AGENTS「所有 `net_*` 经 netTask 串行执行」及 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-14 正面冲突。全仓无生产调用者(只有 `test/board` 与模拟器)所以未爆。建议:改走消息队列,或在 `net_socket.h` 显式标注"未串行化,仅限 netTask"。**处置(2026-11-03):已按前者落地**——UDP 全面走 `netMsgQueue`,出参经 per-fd 槽回传,见本文「第 4 轮:UDP 数据面队列化(2026-11-03)」 | TC-NETSOCK-440 |
|
||||
| 6 | `App/log/soe_store.c` 函数头 ④ | 注释写"打开/写入结构性错误 → `unlink` 重试一次(同 run.log)",实现里 `f_open` 失败**直接 return -1、不重试**(只有 `f_write` 失败才重试);且 `run.log` 侧 `s_append_rec` 的重试码集合还含 `FR_INVALID_NAME`/`FR_NO_PATH` → 两套自愈集合不一致。属"注释夸大实现"还是"实现少一半"待裁 | TC-SOESTORE-022 |
|
||||
| 7 | `App/cfg/cfg_file_store_mcu.c:50` | 函数头写"未挂载时 `f_open` 返回 `FR_NO_SYSTEM`"——**`FRESULT` 里没有这个码**(`Lib/FatFs/ff.h` 共 20 个码,未挂载实际是 `FR_NOT_READY`/`FR_NO_FILESYSTEM`)。只在注释里不参与编译,危害是后人照它写判据会写出永不成立的分支 | —(注释面) |
|
||||
| 8 | `App/log/console_log.c:317` vs `:285` | 冷却起点用 `s_con_cool_until = (uint32_t)HAL_GetTick()`,而判据拿 `!= 0U` 当哨兵 → 故障恰好落在 `HAL_GetTick()==0` 那一拍(开机头 1 ms,或 49.7 天回绕到 0)时**冷却等于没记**,下一轮 drain 立刻再耗一次 100 ms 块超时。撞的正是控制台域那条已登记的"背靠背块超时拖长主循环 → 喂狗节奏饥饿"。建议:另用一个"在冷却"标志,别拿 tick 值当哨兵 | TC-CONLOG-081 / 082(按现状钉) |
|
||||
@@ -526,18 +526,18 @@ uint16_t ch395f_link_change_count(void);
|
||||
| 6 soe_store 注释夸大实现 | **已修(改注释,实现不动)**:打开失败**任何错误码都不 unlink** 是有意的——soe.log 是事件序列,「打开失败就删文件」等于销毁事件证据;两侧资料块各写一份理由并**互指对方文件与测试号**,要求同步修改 | TC-SOESTORE-022 |
|
||||
| 7 FR_NO_SYSTEM 不存在 | **已修**:改成实际会返回的 `FR_NOT_READY(3)` / `FR_NO_FILESYSTEM(13)`,并写明「本层对任何非 FR_OK 一律转 -1,别去枚举错误码集合」,唯一例外指向自愈触发集 | 注释面(无行为改动) |
|
||||
| 4 IP 两套字节序 | **已修(统一到 POSIX 网络序)**:改 `net_inet_addr`(`&s_addr[0] == 第一段`,小端上 192.168.1.100 = `0x6401A8C0`)+ `net_connect_locked`/`net_sendto` 的 IP 数组改按内存字节顺位拆装(**进与出同时换族 → 芯片侧数组净效果不变**,由 550h 直接钉住)+ 模拟器 `net_socket_sim.c` 逐字同语义;`net_inet_ntoa` **函数体一字未动**,只把它注释里那个取了废弃族值的例子改对(就是那条隐蔽原因)。lftpd 两处经算术核对**确认无需改**(PASV 227 的双重反转仍成立),只加「勿顺手改对」的注释 | TC-NETSOCK-100 组(新增往返不变量 100g/100h、边界 100i/100j、截断 100k)、550h |
|
||||
| 5 UDP 绕过 netTask | **危险面已结构性关掉,串行化改造仍未做**(按 Lead 定案,未自升级为改队列):两个 UDP 入口加**执行任务上下文守卫**,非 netTask 调用回新码 `NET_ERR_NOT_IN_NETTASK = -12`(住 `net_types.h`,与 BUSY/TIMEDOUT 的本质差别 = **重试永远无意义**,不参与任何可重试判定);守卫位于参数校验之后、任何芯片事务之前;模拟器**刻意不加**并标注为有意的不对称(单线程 + Winsock 自身线程安全、没有那根 SPI)。**附带关掉第二条更隐蔽的危险**:非 netTask 在 recvfrom 里自旋期间,netTask 可经 close 清零控制块并把同一 sockfd 分给新会话 → 旧自旋者消费新会话数据报,这条路现在断了 | TC-NETSOCK-560 组(含「零芯片事务」与「守卫不吞正常错误路径」) |
|
||||
| 5 UDP 绕过 netTask | **2026-10-24 那轮只关掉了危险面,串行化改造本身已于 2026-11-03 落地**:两个 UDP 入口不再"非 netTask 就拒",而是与 TCP 同构走 `netMsgQueue`(执行体拆到新文件 `Drivers/BSP/NET/net_udp.c`);`NET_ERR_NOT_IN_NETTASK = -12` 保留为**历史码**——全仓已无运行时返回点,只剩注释说明它为什么不再可能发生。**顺带修掉一条当年看不见的问题**:`net_sendto` 为发一个包把 DesIP 从 `0xFFFFFFFF` 改成具体 IP,芯片就此静默切到 UDP Client 形态(收包**不再带 8 字节信息头**),而 `net_recvfrom` 仍按服务器形态无条件剥 8 字节 → 请求-响应式协议(SNTP 48 字节应答)会**丢掉载荷前 8 字节**并交出垃圾源地址。现已按形态分路。当年守卫顺带关掉的"自旋期间 netTask 把同一 sockfd 分给新会话 → 旧自旋者消费新会话数据报",现由"只有 netTask 读芯片 + 每 fd 一个排他接收槽"结构性排除 | `ns_s7_udp.c` UDP-1~14 组 + 改写后的 TC-NETSOCK-440 / 555 / 560 组("非 netTask 调用零芯片事务"仍逐条钉住) |
|
||||
| 8 冷却哨兵与 tick 撞值 | **已修**:新增独立 `s_con_cooling` 判定「是否在冷却」,`s_con_cool_until` 只存截止时刻(无符号差跨回绕的写法保留) | TC-CONLOG-080~084、107、108 |
|
||||
| 9 环空位判据在临界区外 | **已修**:「复核空位 + 整行拷贝 + 推进 head」收进同一个临界区(最长 192 B、168 MHz 下亚微秒;`vTaskDelay` 与 DWT/tick 判据一律留区外,区内绝不睡眠)→ 多生产者结构性安全,写者仍唯一。**原「这条主机测不出来」的结论本轮作废**:桩里记临界区嵌套深度并把第二生产者精确塞进第一个生产者的临界区,再把环形缓冲的宏名成函数调用使每一次碰环都可断言 | TC-CONLOG-160~167(**167 是正控**,证明 160~166 不是空转) |
|
||||
| 10 未初始化被误当僵尸通道 | **已修**:drain 的失败处理先分诊——`Instance == NULL` 视为通道不可用(不 reinit、不计 tx_fault、不进冷却、只记 dropped);只有确认超时/僵尸通道才走软复位自愈 + 冷却 | TC-CONLOG-103~106 |
|
||||
| (撤销)非阻塞 recv 看不见 EOF | **误报,未改任何码**:`net_recv_locked:1885` 的非阻塞分支里 `CLOSED || session_dead` 已判 EOF(0),只有会话存活才 WOULDBLOCK。顺带核实 `net_recvfrom` **不存在** EOF 误判面(UDP 无连接,无数据只有 WOULDBLOCK 或继续自旋两条出口) | —(记一次「描述得很合理」的误报:修之前先读码,误报比漏报更贵) |
|
||||
| (撤销)非阻塞 recv 看不见 EOF | **误报,未改任何码**:`net_recv_locked:1885` 的非阻塞分支里 `CLOSED || session_dead` 已判 EOF(0),只有会话存活才 WOULDBLOCK。顺带核实 `net_recvfrom` **不存在** EOF 误判面(UDP 无连接,无数据只有 WOULDBLOCK 或继续自旋两条出口;2026-11-03 队列化后"继续自旋"改为"登记挂起等 tick 结算",本条结论不变) | —(记一次「描述得很合理」的误报:修之前先读码,误报比漏报更贵) |
|
||||
|
||||
**修复过程中顺带照出的两条(原表没有,一并修掉)**:
|
||||
|
||||
- `syslog_post` 的 `queue full, drop` / `preboot full, drop` **无节流**——1 号修好之后「整批回投 → 队列常驻满」成为必然场景,不修等于**用一个新刷屏换掉一个静默丢**。已按同一「正常→故障」纪律加两个独立抑制位(**丢弃语义一字未改**,满则丢照旧)。TC-SYSLOG-041b/041d/041e/041f、105、106、107、107b、107c。
|
||||
- 自愈触发集里混着 `FR_INVALID_NAME` / `FR_NO_PATH` / `FR_NO_FILE` / `FR_INVALID_OBJECT`——**比原报告的 6 号严重**:这类码下 unlink 自己的文件必然同样失败(同名同因),后果是**每轮排空删一次自己的历史日志且永远修不好**,正是 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 第 10 条严禁的归集。已新增代码侧唯一入口 `s_is_selfheal_code()`:触发集收敛为 `{FR_DISK_ERR, FR_INT_ERR, FR_NO_FILESYSTEM, FR_DENIED}`,逐码依据(含 `Lib/FatFs/ff.c` 行号)与显式排除清单写在资料块里;`soe_store.c` 侧同源但**取更保守子集**。TC-SYSLOG-101/101b/102/103/103b、TC-SOESTORE-022b。
|
||||
|
||||
**本轮刻意没做(后续项,含理由)**:① UDP 改走 `netMsgQueue`(需先设计 per-task 结果槽:`net_msg_t` 按值拷入队列、错误码只搭任务通知值回传,AGENTS 明文禁止给 `net_msg_t` 加回传字段,且必须配真机回归);
|
||||
**本轮刻意没做(后续项,含理由)**:① UDP 改走 `netMsgQueue`(当年列的前置约束——`net_msg_t` 按值拷入队列、错误码只搭任务通知值回传、AGENTS 明文禁止给 `net_msg_t` 加回传字段,**至今成立**)→ **已于 2026-11-03 落地**:出参槽放进新模块 `net_udp.c` 内部,`net_msg_t` 一字未加;**真机回归仍待做**,见本文「第 4 轮:UDP 数据面队列化(2026-11-03)」的台架清单;
|
||||
② `simulator/sim/sim_syslog.c:171` 那条与 3 号**同源的不可达截断支**(本轮编译验证不到它——模拟器走 CMake+VS,不在 `/keil-build` 覆盖内,不交没编译过的改动);③ `syslog_post` 的**丢弃计数**(属新增对外可见信息,本轮没有实测频率支撑阈值);
|
||||
④ `dbg_console_tx_timeout()` 的**读端接线**(现无生产调用者;候选位置 `task_watchdog.c` 的 `STALL snap=… req=… h=… mg=… cf=… seg=…` 是**被解析的证据行**,加字段要连解析端与台架判据一起改,本批不碰台架);⑤ 给 104 加来源地址日志(现网 `iec104_serv.c` 根本不打印来源 IP、无 `getpeername/ntoa`,**没有输出点可看**)。
|
||||
|
||||
@@ -657,7 +657,7 @@ uint16_t ch395f_link_change_count(void);
|
||||
⚠ 结构体成员访问本身(`u->SR`/`u->DR`)钩不到,别绕进去。
|
||||
3. **跑飞保险要挂在被测模块的天然计数点上**:`net_send_locked`/`net_sendto`/`net_recvfrom` 的等待环里
|
||||
**没有 `osDelay`**,只给 `osDelay` 加保险挡不住这些死循环(实测第一次跑就真卡死)。挂在 `net_poll()`
|
||||
每轮必读的 `HAL_GPIO_ReadPin`(INT#)上,附带让 `NET_SEND_SPIN_MS` 这类有界自旋预算**第一次可量测**。
|
||||
每轮必读的 `HAL_GPIO_ReadPin`(INT#)上,附带让 `NET_SEND_SPIN_MS` 这类有界自旋预算**第一次可量测**(2026-11-03 更新:UDP 收包已不再在调用方自旋,改为登记挂起、由 `net_udp_tick` 结算——同段那条"泵 50 轮仍无通知就 FATAL"的纪律因此成了主力把关:漏一次 `xTaskNotify` 当场红,而不是慢慢卡死)。
|
||||
同一纪律的另一半:假内核对 `xTaskNotifyWait(portMAX_DELAY)` 泵 50 轮仍无通知就 `exit(2)` 打 `FATAL` ——
|
||||
它当场抓出一处把 `net_close` 返回语义判错的用例,而不是让门禁卡死。
|
||||
4. **未插桩全绿、插桩后 `0xC0000005`** 基本都是测试自己的越界读:假 UART 的上线字节日志是二进制缓冲没补
|
||||
@@ -710,6 +710,23 @@ uint16_t ch395f_link_change_count(void);
|
||||
- 本轮改动面:`App/log/sys_log_mcu.c`(条目结构 + 投递拷贝)、`App/log/sys_log.h`(参数契约)、`test/host/sys_log_mcu/sys_log_mcu_test.c`(新增 2 条断言 + `MQ_MAXMSG`)、`scripts/archive/id_gate/id_registry.json`(游标 `TC-SYSLOG-108`)。
|
||||
- **台架与临时改动已全部还原**:钩子、负向对照、套件宏逐一反向删除(`git status` 归零),设备上 `BENCH.STS` / `BENCH2.STS` / `B5FILL.STS` 与含钩子记录的 `SELFTEST.LOG` 已删除,产品固件重建后重烧并回读校验通过。
|
||||
|
||||
### 第 4 轮:UDP 数据面队列化(2026-11-03)
|
||||
|
||||
前置背景:`#57` SNTP 调研把"UDP 先串行化"列为 SNTP 落地的第一步——`net_sendto` / `net_recvfrom` 直调 SPI 且自旋收包,既违反"netTask 唯一芯片访问者",又把 IWDG 预算吃在自旋上。
|
||||
|
||||
- **改造面**:新增 `Drivers/BSP/NET/net_udp.c/.h`(真机专属,模拟器不编译)承接 4 个执行体与挂起 recv;`net_msg_type_t` 加 `NET_MSG_UDP_BIND / UDP_SENDTO / UDP_RECVFROM / UDP_PEER` 四型;公开 API 一律"入队 → 等通知"。`net_init` 重建、`net_socket` 分配 fd、`net_close`、掉线与对账复位四处接上 `net_udp_cancel_all / clear / settle_close`,`net_process_messages` 的第 5 个 tick 位挂 `net_udp_tick`。
|
||||
- **源地址怎么回来(AGENTS 硬约束)**:`net_msg_t` 按值拷入队列、明文禁止加"回传用"字段 → 出参指针**不进消息**,改住 `net_udp.c` 的 per-fd 槽:调用方 `arm`(临界区只包"查占位 + 占位 + 记 owner"三步)、入队失败立刻 `disarm`、netTask 执行体经该指针就地写回调用方自己的内存,通知值只带结果码。互斥 + 泄漏 = 该 fd 永久 `BUSY`,所以执行体**每条出口**都清(UDP-7d / UDP-9e 钉住)。
|
||||
- **不写第二份分块逻辑**:UDP 发送复用 `net_send_locked`(1KB 分块 + 挂起 send 都在里面)。为此把"会话是否已死"收成一条判据 `net_send_state_down()`(`net_pending.c`):TCP 看 `ESTABLISHED`、UDP 看 `UDP_OPEN`;原先 `net_send_locked` 两处 + `net_send_pending_tick` + `net_pending_tick` 各写一遍,共 4 处。
|
||||
- **顺带修掉的形态缺陷**(真隐患):`net_sendto` 为发一个包把 DesIP 从 `0xFFFFFFFF` 写成具体 IP,芯片静默切到 UDP Client 形态(收包**不带** 8 字节信息头),而旧 `recvfrom` 无条件剥 8 字节 → 请求-响应式协议(SNTP 48 B 应答)会丢载荷前 8 字节并交出垃圾源地址。**修法**:`net_sock_t` 加 `udp_mode`(塞进原有 2 字节 padding,`sizeof(net_sock_t)` 不变)、`bind` 恒 SERVER、显式对端 `sendto` 切 CLIENT、`net_udp_set_peer(fd, NULL)` 轻量回 SERVER(不重开 Socket 因此不清芯片缓冲)、只在 SERVER 且 `avail >= 8` 时剥头(残包不猜头,TC-NETSOCK-440o 契约不变)。
|
||||
- **截断语义定案**:一次调用交付一个数据报,`buf` 装不下的尾巴**必须排空**——`ch395f_read_recv_buf` 是按长度推进读指针的消费式读,不排空则下一次 `recvfrom` 交出上一个包的残料。排空用 64 B 栈上 scratch 循环读,**不用** `ch395f_clear_recv_buf`:它清整个接收缓冲,会把后面已经到达的包一起丢。
|
||||
- **用例面**:新分片 `test/host/net_socket/ns_s7_udp.c` 共 23 组 UDP-1~23(bind 的 5 条芯片命令**顺序**、未 bind 一律 -5、服务器形态剥头逐字节、客户端形态不剥头、`sendto` 切形态、挂起收包在后续 poll 轮到达、预算超时按调用方 deadline 计、close 与重建结算不许静默丢通知、出参槽互斥与泄漏、截断排空、netTask 自等守卫、`net_init` 重建取消、流式 API 拒 `DGRAM`、非 netTask 调用零芯片事务、执行体与公开入口的参数守卫(越界 fd / `STREAM` / 未 bind / 零长 / 出参容量不足——拒时必须零芯片事务)、"队列未绑定"与"队列满"两条失败出口不许漏槽、`net_udp_set_peer` 轻/重两种回服务器模式(重开丢缓冲 vs 不重开)、挂起 send 由 `net_send_pending` 结算且登记时不许抢发第二份回执、`net_udp_tick` 的会话作废与 stale-readable 兜底、重 bind 必须把还在等包的调用方按 -8 结算、`bind` 遇满队列必须 BUSY 且不给 fd 投毒、**芯片 `open` 回 `ERR_BUSY` 时精确码必须沿通知值回到等待方**(真芯片会这么拒)、**队列里"别人的失败"排在我这条之前时不许污染我的回执**(同 545 在 TCP 侧的钉子,UDP bind 侧补一条)、`net_recvfrom_to` 的"真失败"与"超时"两条出口分开、**netTask 直调路径(板级用例走的那条)的 `set_peer` / `recvfrom` 正例与三种拒绝**;改写 TC-NETSOCK-440(补 bind 前置 + 新增 440p~440u)、555(表头逐字节按形态分路)、560 组("拒在 netTask 外"→"经队列后非 netTask 仍零 SPI")、500h-i / 506 / 545。全套 **647/647 绿**(改造前 470)。
|
||||
- **假件新增(一律可加不可改)**:`g_fx.wlen[]` + `g_fx.last_write[]` 捕获 `ch395f_write_send_buf` 的**真实字节**(此前假件忽略 `p_data`,UDP 截断与排空无从断言);`ch395f_fake_set_write_delay(fd, n)` 让 SENBUF_FREE 第 n 次才到(钉"排空必须在下一次写之前完成");`g_chip.read_calls` 供"读了几个 scratch 块"断言。`g_fake` / `g_chip` 既有字段一字未动。
|
||||
- **两条教训(都是被测码真 bug,不是测法自误)**:① **执行侧自等守卫不能复用调用侧谓词**——`net_udp_recvfrom_exec` 用 `net_ctx_is_owner_task()` 判自等,而它在 netTask 内恒真 → 每一条经队列投进来的正常 recvfrom 都被拒;正确判据是 `caller == xTaskGetCurrentTaskHandle()`(对齐 `net_pending.c` 的 `caller == s_owner_task`)。抓它的是 UDP-8a。② **芯片长度只能读一次并传参**——`net_udp_datagram_read` 内部又调一次 `ch395f_get_recv_len`,于是"剥几字节"与"读几字节"不同源;假件的 recv_len 队列**每次调用弹一个值**,这条设计当场把双读暴露出来。现由调用方读一次 `avail` 传入三个调用点。
|
||||
- **补测阶段又抓到两条(同一批缺口分析顺手挖的)**:③ **槽指针必须在 fd 合法性检查之后取**——`net_udp_recvfrom_exec` 原先先算 `&s_net_udp[sockfd]` 再判 `p_sock == NULL`,越界 fd 立刻得到一个越界槽指针(哪怕当下没解引用,后面登记就是野写)。④ **作废不等于可以不吭声**——`net_udp_bind_exec` 重开芯片 Socket 会丢掉缓冲里的包,却只 `net_udp_clear()`:A 任务正阻塞在 `net_recvfrom`、B 任务重 bind 同一个 fd,A 就永远睡死。现在重开之前先按 `NET_ERR_CONNRESET` 结算并通知(UDP-20 钉住)。教训同 `net_pending`:**任何"取消挂起"的动作都必须走 complete 那条路,clear 只是归零**。
|
||||
- **本轮覆盖率**(收尾那次全仓跑数,`--update-baseline` 已把数字连同本轮提交一起落盘):`net_udp.c` 237/239 行(99.2%)、函数 **14/14**;`net_socket.c` 966/969(99.7%)、函数 **59/59**;`net_pending.c` 173/199(86.9%);全仓 line 8372/9290(90.1%)、47/47 套绿,无回退。剩两处未覆盖:`net_udp.c` 的槽越界兜底(调用方已挡,属防御性代码)与 `net_socket.c` 错误码槽重查竞态(真并发才走得到,改造前后同为那 2 行)。
|
||||
- **台架必验(本轮无板子,主机测替代不了)**:① 两端真机互通,服务器形态收发 + 客户端形态往返,重点看 48 B 这类小应答**载荷完整**且源地址正确;② `sendto` 切形态后 `net_udp_set_peer(fd, NULL)` 能否真恢复收广播——手册只写 DesIP 语义,没写切换时机;③ 截断包排空后下一个包是否完整(scratch 循环在真芯片读指针上的行为);④ 挂起 recv 等待期间的 IWDG 余量;⑤ bind 在 open **之前**写 DesIP 的顺序在真芯片上的实际效果(TRAP-NET-14 的 UDP 侧对照)。
|
||||
- 本轮改动面:`Drivers/BSP/NET/net_udp.c` 与 `Drivers/BSP/NET/net_udp.h`(新)、`net_socket.c` / `net_socket.h`、`net_pending.c` / `net_pending.h`、`net_types.h`、`MDK-ARM/STM32F407-Demo.uvprojx`、`scripts/check_registration.py`、`test/host/common/ch395f_fake.c` 与 `test/host/common/ch395f_fake.h`、`test/host/net_socket/`(含新分片);文档 → `docs/网络/BSD_Socket_API_使用指南.md` §6.0 / §6.1 / §6.2 / §6.3 / §9.0、`docs/网络/BSD_Socket_API_函数调用关系.md` §4.11、`docs/问题记录/CH395F_Trap_Records.md`(TRAP-NET-14 UDP 段 + 新条)。
|
||||
|
||||
### P3|**明确不做**(把理由写死,免得下次又被列回待办:29 个 `.c` = 4,814 行)
|
||||
|
||||
| 类别 | 体量 | 为什么不做 | 那它靠什么把关 |
|
||||
|
||||
+130
-33
@@ -65,6 +65,7 @@
|
||||
| `net_socket.c` | API 核心实现:消息队列处理与线程安全封装、内部 `_sock()` 版本;另含挂起 recv 登记路径 |
|
||||
| `net_events.c`(**真机专属**) | **状态面收敛**(2026-10-14 从 `net_socket.c` 抽出,与 `net_listener.c` 同族):5 个 `handle_*_event`(`net_poll` 检测中断后的分派目标)+ `fire_event` + TRAP-NET-17 周期对账 + 监听重建**合一** reconciler(`net_reconcile_tick`,100ms,分级去抖+冷却,形态①②③④⑤/U11)+ `net_lost_connect_probe`;**只置事实与登记意图**,重建动作在 `net_listener_tick`;内部头 `net_events.h`,模拟器不编译 |
|
||||
| `net_listener.c`(**真机专属**) | **监听生命周期**(E17 单一所有者收口):`net_listener_request`(**只登记意图**,5 个登记点)/ `net_listener_tick`(**唯一执行者**,门集 + D10 重试 30ms/2s)/ `net_listener_clear` / `net_listener_settle`(沉降/隔离 100ms/400ms);"该不该重建"的周期判据在 `net_reconcile_tick` 形态③④(单一 100ms tick);内部头,模拟器不编译 |
|
||||
| `net_udp.c`(**真机专属**) | **UDP 数据面执行体**(2026-11-03 队列化改造新增):`net_udp_bind_exec` / `net_udp_set_peer_exec`(切芯片形态,不重开 Socket)/ `net_udp_sendto_exec`(**复用** `net_send_locked` 的 1KB 分块与挂起 send)/ `net_udp_recvfrom_exec` + `net_udp_tick`(挂起 recv 完成 / 预算超时)+ per-fd 出参槽(源地址写回调用方自己的内存,不占 `net_msg_t`)+ 截断排空;内部头 `net_udp.h`,模拟器不编译;用法与约束见 §6.0 / §6.3,调用结构见 `docs/网络/BSD_Socket_API_函数调用关系.md` §4.11 |
|
||||
| `net_select.h` | select/poll API 头文件 |
|
||||
| `net_select.c` | select/poll 实现(仅 MCU 端编译) |
|
||||
| `net_socket_sim.c`(`simulator/sim/`) | Winsock2 后端(模拟器端,与 `net_socket.c` 互斥编译),详见 §2.3 |
|
||||
@@ -156,10 +157,10 @@ sequenceDiagram
|
||||
|
||||
| 规则 | 说明 |
|
||||
|------|------|
|
||||
| **`netTask` 外(任意应用任务)** | 公共 API:`net_send()` / `net_recv()` / `net_recv_to()` / `net_accept()` / `net_connect()` / `net_close()` / `net_listen()` / `net_drop_conn()`——请求入队列,netTask 串行执行,调用方阻塞等回执。**自愈无显式 API**(`net_relisten()` / `net_resync_listen()` 已退役,静默回收/监听失位由对账⑤与 reconciler 形态③④自动认领,见 §3.6) |
|
||||
| **`netTask` 外(任意应用任务)** | 公共 API:`net_send()` / `net_recv()` / `net_recv_to()` / `net_accept()` / `net_connect()` / `net_close()` / `net_listen()` / `net_drop_conn()`——请求入队列,netTask 串行执行,调用方阻塞等回执。UDP 侧同一口径:`net_bind()`(UDP) / `net_sendto()` / `net_recvfrom()` / `net_recvfrom_to()` / `net_udp_set_peer()`(2026-11-03 队列化改造,见 §6)。**自愈无显式 API**(`net_relisten()` / `net_resync_listen()` 已退役,静默回收/监听失位由对账⑤与 reconciler 形态③④自动认领,见 §3.6) |
|
||||
| **`netTask` 内** | 内部 API `net_send_sock()` / `net_recv_sock()` / `net_close_sock()` / `net_listen_locked()` |
|
||||
| **`net_socket()` / `net_bind()`** | 直接调用,无需队列;实际在就绪屏障后的服务任务(timeSyncTask / ftpTask / uiTask)中创建监听 |
|
||||
| **`net_poll()` 和 `net_process_messages()`** | 必须在 `netTask` 主循环调用;`net_process_messages()` 每轮先跑 **4 个非阻塞 tick**(D10 重监听重试 → 挂起 recv 完成 → 挂起 send 推进 → TRAP-NET-17 对账+监听重建合一 reconciler 100ms),再处理队列 |
|
||||
| **`net_socket()` / `net_bind()`** | `net_socket()` 纯 RAM(分配控制块),任意任务直接调用;`net_bind()` 分两支——**TCP** 只记本地端口,仍是直接调用;**UDP** 要开芯片侧 Socket,走队列在 netTask 里执行(要发 5 条芯片命令,见 §6.0) |
|
||||
| **`net_poll()` 和 `net_process_messages()`** | 必须在 `netTask` 主循环调用;`net_process_messages()` 每轮先跑 **5 个非阻塞 tick**(D10 重监听重试 → 挂起 recv 完成 → 挂起 send 推进 → **UDP 挂起 recv 完成/超时** → TRAP-NET-17 对账+监听重建合一 reconciler 100ms),再处理队列 |
|
||||
|
||||
### 3.4 内部 API 说明
|
||||
|
||||
@@ -440,8 +441,29 @@ void tcp_client(void)
|
||||
|
||||
## 6. UDP 编程
|
||||
|
||||
> **本项目不使用 UDP(2026-10-15 定案)**:UDP API 保留为 BSD socket 完整性,仅**单任务独占期**可用
|
||||
> (`net_sendto` / `net_recvfrom` 直碰 SPI、不经队列,见 §2 三例外);本平台无 UDP 消费者。
|
||||
> **UDP 已于 2026-11-03 队列化**(原"本项目不使用 UDP(2026-10-15 定案)"作废):
|
||||
> `net_bind`(UDP) / `net_sendto` / `net_recvfrom` / `net_recvfrom_to` / `net_udp_set_peer` 与 TCP 同一口径
|
||||
> ——请求投 `netMsgQueue`,芯片事务**全部在 `netTask` 里串行执行**,调用方阻塞等回执(结果搭 32 位任务
|
||||
> 通知值回来)。任何任务都能安全调用,不再有"单任务独占期"这个前提。第一个真实消费者是 SNTP(请求-应答)。
|
||||
|
||||
### 6.0 先分清楚:芯片的 UDP 两种模式
|
||||
|
||||
CH395F 用 `DesIP` 一个寄存器区分两种 UDP 形态(手册 `docs/芯片手册/CH395F.md` §9.2.4),本层把它做成
|
||||
socket 的**模式属性**(`net_types.h` `NET_UDP_MODE_SERVER` / `NET_UDP_MODE_CLIENT`)。两种形态收上来的
|
||||
字节**不一样多**,这是 UDP 侧最容易踩的坑:
|
||||
|
||||
| 模式 | DesIP | 收包布局 | 谁能发给我 | 本层如何进入 |
|
||||
|---|---|---|---|---|
|
||||
| **服务器** | `0xFFFFFFFF` | **8 字节信息头** + 载荷(头 [0-1] 长度、[2-3] 源端口、[4-7] 源 IP 线序) | 任意主机 | `net_bind()`(恒此模式:BSD 的 bind 只定本地端口,不选对端) |
|
||||
| **客户端** | 具体 IP | **无信息头**,整段就是载荷 | 只有该 IP | `net_sendto()` 带目的地址,或 `net_udp_set_peer(fd, &peer)` |
|
||||
|
||||
- 收包时**按模式**决定是否剥头:客户端模式绝不剥。旧实现不分模式恒剥 8 字节,而 `net_sendto` 一旦给出
|
||||
目的地址芯片就已是客户端形态——于是 SNTP 这类应答的**前 8 字节载荷被吃掉**,源地址还是从载荷里当
|
||||
头读出来的假值(缺陷登记见 `docs/问题记录/CH395F_Trap_Records.md`,回归钉子在
|
||||
`test/host/net_socket/ns_s7_udp.c` UDP-3 / UDP-4)。
|
||||
- 从客户端模式回服务器形态:`net_udp_set_peer(fd, NULL)`,只改 DesIP/DesPort,**不重开 Socket**;
|
||||
再次 `net_bind()` 则是重开(顺带丢掉芯片缓冲里的残留包),两者用途不同。
|
||||
- 客户端模式下 `net_recvfrom` 回填的源地址就是那个固定对端——芯片已经替我按 IP 过滤过一遍,包上也没有头可取。
|
||||
|
||||
### 6.1 UDP Client
|
||||
|
||||
@@ -463,37 +485,43 @@ void udp_client(void)
|
||||
return;
|
||||
}
|
||||
|
||||
/* 2. 绑定本地端口(可选) */
|
||||
/* 2. 绑定本地端口:UDP 侧**必做**(芯片侧 Socket 要在这里 open,
|
||||
* 没 bind 就收发一律 NET_ERR_NOTCONN)。端口给 0 = 本层取动态端口 */
|
||||
local_addr.sin_family = AF_INET;
|
||||
local_addr.sin_port = net_htons(12345);
|
||||
local_addr.sin_addr.s_addr = net_inet_addr("0.0.0.0");
|
||||
|
||||
net_bind(sock, (struct net_sockaddr *)&local_addr, sizeof(local_addr));
|
||||
if (net_bind(sock, (struct net_sockaddr *)&local_addr, sizeof(local_addr)) != 0) {
|
||||
printf("绑定失败\r\n");
|
||||
net_close(sock);
|
||||
return;
|
||||
}
|
||||
|
||||
/* 3. 设置目标地址 */
|
||||
dest_addr.sin_family = AF_INET;
|
||||
dest_addr.sin_port = net_htons(8888);
|
||||
dest_addr.sin_addr.s_addr = net_inet_addr("192.168.31.200");
|
||||
|
||||
/* 4. 发送数据 */
|
||||
/* 4. 发送数据:带目的地址的 sendto 顺手把 DesIP 指向该地址,
|
||||
* 芯片从此是**客户端模式**(只收这个 IP 的包,包上不带信息头,见 §6.0) */
|
||||
const char *msg = "UDP Hello!";
|
||||
net_sendto(sock, msg, strlen(msg), 0,
|
||||
(struct net_sockaddr *)&dest_addr, sizeof(dest_addr));
|
||||
printf("发送: %s\r\n", msg);
|
||||
if (net_sendto(sock, msg, strlen(msg), 0,
|
||||
(struct net_sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) {
|
||||
printf("发送失败 %d\r\n", net_get_errno());
|
||||
net_close(sock);
|
||||
return;
|
||||
}
|
||||
|
||||
/* 5. 接收数据 */
|
||||
struct net_sockaddr_in src_addr;
|
||||
int src_addr_len = sizeof(src_addr);
|
||||
|
||||
len = net_recvfrom(sock, buf, sizeof(buf) - 1, 0,
|
||||
(struct net_sockaddr *)&src_addr, &src_addr_len);
|
||||
/* 5. 接收应答:受 IWDG 监管的任务用带预算的 net_recvfrom_to,
|
||||
* 超时返回 -2(不置 errno),不会把喂狗窗口耗在等待上(§10.1) */
|
||||
len = net_recvfrom_to(sock, buf, sizeof(buf) - 1, 2000U, NULL, NULL);
|
||||
|
||||
if (len > 0) {
|
||||
buf[len] = '\0';
|
||||
char ip_str[16];
|
||||
printf("收到: %s (from %s:%d)\r\n", buf,
|
||||
net_inet_ntoa(src_addr.sin_addr.s_addr, ip_str),
|
||||
net_ntohs(src_addr.sin_port));
|
||||
/* 客户端模式的源地址就是这个固定对端,包上没有信息头 */
|
||||
printf("收到: %s (from server)\r\n", buf);
|
||||
} else if (len == -2) {
|
||||
printf("应答超时\r\n");
|
||||
}
|
||||
|
||||
net_close(sock);
|
||||
@@ -531,15 +559,15 @@ void udp_server(void)
|
||||
|
||||
printf("UDP Server 启动,监听端口 8888\r\n");
|
||||
|
||||
/* 3. 循环接收数据 */
|
||||
/* 3. 循环接收数据:一次调用 = 一个完整数据报(服务器模式自动剥 8 字节信息头) */
|
||||
while (1)
|
||||
{
|
||||
net_poll();
|
||||
|
||||
struct net_sockaddr_in src_addr;
|
||||
int src_addr_len = sizeof(src_addr);
|
||||
|
||||
int len = net_recvfrom(sock, buf, sizeof(buf) - 1, 0,
|
||||
/* 用带预算的收包当循环节拍:-2 = 本轮没人发包,不是错误;
|
||||
* 换 net_recvfrom(...) 就是无限等,受 IWDG 监管的任务里别这么写(§10.1) */
|
||||
int len = net_recvfrom_to(sock, buf, sizeof(buf) - 1, 500U,
|
||||
(struct net_sockaddr *)&src_addr, &src_addr_len);
|
||||
|
||||
if (len > 0) {
|
||||
@@ -549,14 +577,29 @@ void udp_server(void)
|
||||
net_inet_ntoa(src_addr.sin_addr.s_addr, ip_str),
|
||||
net_ntohs(src_addr.sin_port));
|
||||
|
||||
/* 回显数据 */
|
||||
net_sendto(sock, buf, len, 0,
|
||||
/* 回显数据。⚠ 带目的地址的 sendto 会把芯片切成**客户端模式**
|
||||
* (DesIP = 这个客户端),此后只有它一家能再发包进来。要长期服务
|
||||
* 多个客户端,回完就把它切回来: */
|
||||
(void)net_sendto(sock, buf, len, 0,
|
||||
(struct net_sockaddr *)&src_addr, src_addr_len);
|
||||
(void)net_udp_set_peer(sock, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 UDP 调用约束(2026-11-03 队列化之后)
|
||||
|
||||
| 约束 | 行为与理由 |
|
||||
|---|---|
|
||||
| 未 `net_bind()` 就 `sendto` / `recvfrom` / `set_peer` | 一律 `NET_ERR_NOTCONN`(-5):芯片侧 Socket 还没 open,此时写 DesIP 也会被随后的 open 复位 |
|
||||
| 一次收包 = 一个完整数据报 | 缓冲装不下时交付 `len` 字节并把剩余部分**读空丢弃**(留半包在芯片缓冲里,下一次收包会从上一包的尾巴开始解析,源地址与载荷从此全部错位);短交付靠返回值判读 |
|
||||
| 同一 fd 并发收包 | 第二个接收者拿 `NET_ERR_BUSY`(-11):数据报语义下无法把包归属到两个等待者,拒绝比交错投递安全(与 TCP 挂起 recv 同一守卫) |
|
||||
| 阻塞预算 | 受 IWDG 监管的任务禁用无限等待的 `net_recvfrom`,改 `net_recvfrom_to(fd, buf, len, to_ms, ...)`:超时返回 **-2** 且不置 errno(与 `net_recv_to` 同口径);或先 `net_select()` / `net_can_read()` 门控 |
|
||||
| 单包载荷 | ≤ 1472 字节(手册 §9.2.4);更长由本层按 1KB 分块、复用 TCP 的挂起 send 在 netTask 里推进,短写返回已发送字节数,调用方须检查返回值 |
|
||||
| fd 额度 | 与 TCP 共用 `NET_MAX_SOCKETS`(8),一个 UDP fd 占一个名额 |
|
||||
| 收包不占 netTask | 没数据时登记挂起(本 fd 一个槽)立即让出 netTask,完成条件由 `net_udp_tick` 每轮判定——与挂起 recv/send 同一套账,netTask 不会被一次 UDP 等待钉住 |
|
||||
|
||||
---
|
||||
|
||||
## 7. I/O 多路复用
|
||||
@@ -738,10 +781,13 @@ void event_callback_example(void)
|
||||
的 `s_addr` **同族、可直接相等比较**。历史背景(同一文件曾并存两套约定、`ntoa` 注释里的例子取了废弃族的值)
|
||||
见 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-21 的 2026-10-24 后续条目。
|
||||
|
||||
**UDP 直调限制**:`net_sendto` / `net_recvfrom` **不走消息队列**——它们在调用方上下文里直接 `net_poll()` + 读写芯片缓冲,
|
||||
因此**仅限本层执行任务(NET 层 = netTask)调用**;别的任务调用回 `NET_ERR_NOT_IN_NETTASK`(-12)。该码**不是运行时故障,
|
||||
重试永远无意义**:要么把调用挪进执行任务,要么改用走队列的 `net_send` / `net_recv`,没有第三条路。
|
||||
「把这两个接口改成经 `netMsgQueue` 串行化」是已知后续项,未做的理由见 `Drivers/BSP/NET/net_socket.h` 同名函数头。
|
||||
**UDP 也走队列(2026-11-03 改造)**:`net_bind`(UDP) / `net_sendto` / `net_recvfrom` / `net_recvfrom_to` /
|
||||
`net_udp_set_peer` 与 TCP 同一口径——投 `netMsgQueue`、在 `netTask` 里碰芯片、结果搭 32 位任务通知值回来。
|
||||
改造前有、现在作废的两件事:①"仅限本层执行任务(netTask)调用"——`NET_ERR_NOT_IN_NETTASK`(-12)自本
|
||||
改造起**没有任何返回路径**,常量保留只为老代码与历史文档对得上号,新代码不要拿它当判据;②"UDP 只在单任务
|
||||
独占期可用"。源地址回传不占 `net_msg_t`(该结构按值拷入队列,AGENTS 禁止加"回传用"字段),而是经
|
||||
`Drivers/BSP/NET/net_udp.c` 的 per-fd 出参槽写回调用方自己的内存,代价是**同一 fd 不允许两个任务同时收包**
|
||||
(第二个拿 `NET_ERR_BUSY`)。芯片形态(服务器 / 客户端)与剥头规则见 §6.0,调用约束见 §6.3。
|
||||
|
||||
### 9.1 端口号转换
|
||||
|
||||
@@ -1007,20 +1053,71 @@ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms);
|
||||
|
||||
调用方累计自身总预算:见到 -2 发起下一片,直到收齐 / EOF / 总预算耗尽。生产用途:lftpd 数据行读、timeSync 帧读(队头阻塞修复,完整例 §11.2);受 IWDG 监控任务的长等待必须走本接口(或 `net_select`)+ 每片喂心跳。
|
||||
|
||||
#### net_sendto - 发送 UDP 数据
|
||||
#### net_sendto - 发送 UDP 数据报
|
||||
|
||||
```c
|
||||
int net_sendto(int sockfd, const void *buf, int len, int flags,
|
||||
const struct net_sockaddr *dest_addr, int addrlen);
|
||||
```
|
||||
|
||||
#### net_recvfrom - 接收 UDP 数据
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `sockfd` | **必须已 `net_bind()`**(未 bind 回 -1 / `NET_ERR_NOTCONN`:芯片侧 Socket 还没 open) |
|
||||
| `buf` / `len` | 一个数据报的载荷,单包 ≤ 1472 字节(手册 §9.2.4);更长由本层按 1KB 分块、复用 TCP 的挂起 send 在 netTask 里推进 |
|
||||
| `flags` | 目前只消费 `NET_MSG_DONTWAIT`:芯片发送缓冲不可用时立即回 `NET_ERR_WOULDBLOCK`,不登记挂起 |
|
||||
| `dest_addr` / `addrlen` | 非 NULL = 顺带把芯片 DesIP 指向它 → 芯片转**客户端模式**(此后只收该 IP 的包、收包不带信息头,见 §6.0);NULL = 不改目的地址,沿用当前形态 |
|
||||
| 返回值 | > 0 = 已发送字节数(**可能短写**,须检查);-1 = 失败(`net_get_errno()`) |
|
||||
|
||||
任意任务可调用:请求入 `netMsgQueue`,芯片事务在 `netTask` 串行执行(2026-11-03 起,旧"仅限 netTask 直调"形态作废)。
|
||||
|
||||
#### net_recvfrom - 接收 UDP 数据报(无限等待形态)
|
||||
|
||||
```c
|
||||
int net_recvfrom(int sockfd, void *buf, int len, int flags,
|
||||
struct net_sockaddr *src_addr, int *addrlen);
|
||||
```
|
||||
|
||||
- 一次调用 = 一个**完整数据报**:服务器模式自动剥 8 字节信息头并从中取源地址,客户端模式整段是载荷、源地址回填固定对端(§6.0)。
|
||||
- `buf` 装不下时交付 `len` 字节并把剩余部分**读空丢弃**——半包留在芯片缓冲里,下一次收包会从上一包的尾巴开始解析。
|
||||
- `src_addr` / `addrlen` 可为 NULL;非 NULL 时 `*addrlen` 给 `sizeof(struct net_sockaddr_in)`,回填的 `s_addr` 是网络序整数(§9.0)。
|
||||
- ⚠ **无数据即无限等待**:受 IWDG 监管的任务改 `net_recvfrom_to()`,或先 `net_select()` / `net_can_read()` 门控。
|
||||
- 同一 fd 已有别的任务在收 → 本调用回 -1 / `NET_ERR_BUSY`(数据报无法归属两个等待者)。
|
||||
|
||||
#### net_recvfrom_to - 接收 UDP 数据报(显式预算形态)
|
||||
|
||||
```c
|
||||
int net_recvfrom_to(int sockfd, void *buf, int len, uint32_t timeout_ms,
|
||||
struct net_sockaddr *src_addr, int *addrlen);
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `timeout_ms` | 等待预算(ms,**必须 > 0**;0 = 无限等待,与本接口"有界"的存在理由相反,直接拒 `NET_ERR_INVAL`——无限形态请用 `net_recvfrom`) |
|
||||
| 返回值 | > 0 = 本包交付字节数(小于芯片内包长 = 被截断,多余部分已丢弃);**-2 = 预算内没等到**(**不是 errno**,不得据此判错误);-1 = 真错误 |
|
||||
|
||||
与 `net_recv_to()` 同一口径,是受 IWDG 监控任务的合法 UDP 收包入口;等待期本 fd 登记一个挂起槽、netTask 立即让出(不占 CPU、不占 netTask)。
|
||||
|
||||
#### net_udp_set_peer - 切换 UDP 芯片形态(不重开 Socket)
|
||||
|
||||
```c
|
||||
int net_udp_set_peer(int sockfd, const struct net_sockaddr_in *peer);
|
||||
```
|
||||
|
||||
| 参数 | 说明 |
|
||||
|------|------|
|
||||
| `peer` | 非 NULL = 客户端模式,DesIP/DesPort 指向该对端(只收它的包,收包无信息头);NULL = 回服务器模式(DesIP = `0xFFFFFFFF`、DesPort = 0,任意主机可发,收包带 8 字节信息头) |
|
||||
| 返回值 | 0 成功;-1 失败(未 bind → `NET_ERR_NOTCONN`) |
|
||||
|
||||
只写两个寄存器、**不重开 Socket**,因此不清芯片缓冲;要连残留包一起丢掉就再 `net_bind()` 一次(重开形态)。
|
||||
|
||||
#### net_udp_get_mode - 读当前 UDP 芯片形态
|
||||
|
||||
```c
|
||||
int net_udp_get_mode(int sockfd);
|
||||
```
|
||||
|
||||
返回 `NET_UDP_MODE_SERVER` / `NET_UDP_MODE_CLIENT`,参数错回 -1。给诊断、单测和上层判读"要不要切回服务器模式"用;纯 RAM 读,不入队。
|
||||
|
||||
#### net_close - 关闭 Socket
|
||||
|
||||
```c
|
||||
|
||||
@@ -5,9 +5,10 @@
|
||||
>
|
||||
> **范围**:MCU 端 `Drivers/BSP/NET/net_socket.c` + 其拆出的
|
||||
> `Drivers/BSP/NET/net_listener.c`(**监听生命周期**)+
|
||||
> `Drivers/BSP/NET/net_pending.c`(**挂起 recv / send 请求表**)。
|
||||
> `Drivers/BSP/NET/net_pending.c`(**挂起 recv / send 请求表**)+
|
||||
> `Drivers/BSP/NET/net_udp.c`(**UDP 数据面执行体与挂起 recv 槽**,2026-11-03 队列化改造新增,§4.11)。
|
||||
> 模拟器端后端 `simulator/sim/net_socket_sim.c`(Winsock)是另一套实现,见 `docs/网络/BSD_Socket_API_使用指南.md` §2.3;`net_select.c` 不在此展开,见 `docs/网络/BSD_Socket_API_使用指南.md` §7 / §10.5。
|
||||
> `net_listener.c` / `net_pending.c` 真机专属(模拟器不编译);设计依据分别见
|
||||
> `net_listener.c` / `net_pending.c` / `net_udp.c` 真机专属(模拟器不编译);设计依据分别见
|
||||
> `docs/网络/网络自愈架构设计.md` §11 与
|
||||
> `docs/网络/Net层阻塞模型架构设计.md`。
|
||||
>
|
||||
@@ -25,11 +26,11 @@
|
||||
|
||||
| 执行域 | 运行位置 / 节奏 | 允许的操作 | 典型调用者 |
|
||||
|---|---|---|---|
|
||||
| **netTask 主循环** | 独立 FreeRTOS 任务,1ms/tick | **唯一**可碰芯片(SPI2)的地方;4 个非阻塞 tick(D10 重试 / 挂起 recv / 挂起 send / TRAP-NET-17 对账+监听重建合一 reconciler);队列分派;事件处理器;用户回调 | `net_poll` / `net_process_messages` 及其下游全部 `_locked` / `_tick` / `handle_*` |
|
||||
| **netTask 主循环** | 独立 FreeRTOS 任务,1ms/tick | **唯一**可碰芯片(SPI2)的地方;5 个非阻塞 tick(D10 重试 / 挂起 recv / 挂起 send / **UDP 挂起 recv**(2026-11-03 队列化) / TRAP-NET-17 对账+监听重建合一 reconciler);队列分派;事件处理器;用户回调 | `net_poll` / `net_process_messages` 及其下游全部 `_locked` / `_tick` / `handle_*` |
|
||||
| **应用任务侧** | ftpTask / uiTask / timeSyncTask / testTask 等 | 经队列的公共 API(入队 + `xTaskNotifyWait` 等回执);纯 RAM 直调 API;**禁止直碰芯片** | 各服务任务 |
|
||||
| **直调快路径** | 同步调用,不经过队列 | `net_socket` / 查询族 / 回调注册(纯 RAM);**直碰 SPI 的三例外**:`net_bind`(UDP) / `net_sendto` / `net_recvfrom` | 初始化阶段、netTask 上下文、测试套件(硬件层测试模式下 netTask 自挂起,testTask 独占芯片) |
|
||||
| **直调快路径** | 同步调用,不经过队列 | `net_socket` / 查询族 / 回调注册 / `net_bind`(TCP) 记账(纯 RAM);**UDP 侧不再是例外**:`net_bind`(UDP) / `net_sendto` / `net_recvfrom` / `net_recvfrom_to` / `net_udp_set_peer` 已于 2026-11-03 全部并入队列 | 初始化阶段、netTask 上下文、测试套件(硬件层测试模式下 netTask 自挂起,testTask 独占芯片) |
|
||||
|
||||
> ⚠ 直碰 SPI 的三例外**只能在"芯片访问者唯一"的上下文**调用:产品构建中应用任务一律走队列(TCP 全路径);三例外的实际消费者是测试套件(见 AGENTS.md"测试构建")。从其他任务直接调用会与 `net_poll` 的 SPI 事务竞争,破坏 CH395F 寄存器(TRAP-NET-14 实证:CONNECT 超时)。
|
||||
> ⚠ 2026-11-03 起,**公开 API 里已经没有直碰 SPI 的口子**——原"三例外"(`net_bind`(UDP) / `net_sendto` / `net_recvfrom`)已全部并入队列,产品构建中应用任务的每一条 `net_*` 都是"入队 + 等回执"。仍需独占芯片的只剩**板级硬件层测试**:它绕过本层直接调 `ch395f_*`,前提是该测试模式下 netTask 自挂起、testTask 独占芯片(见 AGENTS.md"测试构建")。任何情况下从别的任务直接驱动 CH395F 都会与 `net_poll` 的 SPI 事务竞争、破坏芯片寄存器(TRAP-NET-14 实证:CONNECT 超时)。
|
||||
|
||||
---
|
||||
|
||||
@@ -38,9 +39,9 @@
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph APP["应用任务(非 netTask)"]
|
||||
PUB["经队列公共 API<br/>netListen · netAccept · netConnect · netSend<br/>netRecv · netRecvTo · netClose · netDropConn"]
|
||||
PUB["经队列公共 API<br/>netListen · netAccept · netConnect · netSend<br/>netRecv · netRecvTo · netClose · netDropConn<br/>UDP:netBind · netSendto · netRecvFrom · netRecvFromTo · netUdpSetPeer"]
|
||||
RAM["直调 · 纯 RAM<br/>netSocket · netSockState · netGetSock<br/>netGetErrno · netSetEventCb · netGetSockname<br/>字节序(netHtons / netInetAddr …)"]
|
||||
SPI["直调 · 直碰 SPI(须芯片唯一上下文)<br/>netBind(UDP) · netSendto · netRecvFrom"]
|
||||
SPI["板级硬件层测试 · 直调 BSP(须芯片访问者唯一)<br/>ch395f_*(该模式下 netTask 自挂起)"]
|
||||
CB["用户事件回调<br/>(在 netTask 上下文执行!)"]
|
||||
end
|
||||
|
||||
@@ -53,8 +54,9 @@ flowchart TB
|
||||
RetryTick["① netListenerRetryTick<br/>D10 重监听重试:30ms / 2s 时限"]
|
||||
PendTick["② netPendingTick<br/>挂起 recv:①数据 ②到点(仅显式预算) ③断连 ④tripwire 60s 只留痕"]
|
||||
PendShimTick["③ netSendPendingTick<br/>挂起 send:①断连 ②推进(每轮≤1块) ③写完 ④无进展超时"]
|
||||
ReconcileTick["④ netReconcileTick<br/>TRAP-NET-17 对账 + 监听重建合一:100ms / 分级去抖 / 冷却<br/>读驱动状态缓存(①②③④⑤ + U11)"]
|
||||
DISP["队列分派 switch<br/>SEND · RECV · CLOSE<br/>CONNECT · LISTEN · ACCEPT · DROP_CONN"]
|
||||
ReconcileTick["⑤ netReconcileTick<br/>TRAP-NET-17 对账 + 监听重建合一:100ms / 分级去抖 / 冷却<br/>读驱动状态缓存(①②③④⑤ + U11)"]
|
||||
UdpTick["④ netUdpTick<br/>UDP 挂起 recv:①数据到达(读一个完整数据报) ②预算到点 -2 ③会话已死"]
|
||||
DISP["队列分派 switch<br/>SEND · RECV · CLOSE<br/>CONNECT · LISTEN · ACCEPT · DROP_CONN<br/>UDP_BIND · UDP_SENDTO · UDP_RECVFROM · UDP_PEER"]
|
||||
subgraph LK["_locked / 内部分派目标(仅 netTask 上下文)"]
|
||||
LL["netListenLocked<br/>初次监听(open 重试 3× + sock_reset 排渣)"]
|
||||
LN["netConnectLocked<br/>open 重试 3×(sock_reset 排渣)+ tcpConnect"]
|
||||
@@ -64,19 +66,30 @@ flowchart TB
|
||||
LC["netCloseLocked<br/>结束挂起 + cmd_done 快路径 / sock_reset(GRACEFUL)"]
|
||||
DC["netDropConnLocked<br/>拆会话保监听:sock_reset(QUARANTINE) + 登记重监听"]
|
||||
RLS["netListenerTry → netRelistenLocked<br/>单次尝试 + TRAP-NET-18 排渣隔离"]
|
||||
UB["netUdpBindExec<br/>开芯片侧 Socket(恒服务器模式,DesIP=FFH 在 open 前写)"]
|
||||
US["netUdpSendtoExec<br/>带地址则先写 DesIP/DesPort(转客户端模式)"]
|
||||
UR["netUdpRecvfromExec<br/>快路径 / DONTWAIT / 登记挂起 recv"]
|
||||
UP["netUdpSetPeerExec<br/>只改 DesIP/DesPort,不重开 Socket"]
|
||||
end
|
||||
EV["事件处理器<br/>handleConnect · handleDisconnect<br/>handleRecv · handleSend · handleTimeout"]
|
||||
ST["netListenerSettle<br/>只置沉降/隔离事实(100ms / 400ms)"]
|
||||
FE["fireEvent → 用户回调"]
|
||||
PR["s_net_pending<br/>(每 fd 至多一个挂起 recv)"]
|
||||
SR["s_net_send<br/>(每 fd 至多一个挂起 send)"]
|
||||
SU["s_net_udp<br/>(每 fd 至多一个接收槽:调用方出参指针 + 挂起 recv)"]
|
||||
CH[("ch395f.* 驱动<br/>SPI2 串行事务")]
|
||||
end
|
||||
|
||||
PUB -->|入队| MQ
|
||||
MQ --> DISP
|
||||
DISP --> LL & LN & LS & LR & LA & LC & DC & RLS
|
||||
NPM --> RetryTick & PendTick & PendShimTick & ReconcileTick & NP
|
||||
DISP --> LL & LN & LS & LR & LA & LC & DC & RLS & UB & US & UR & UP
|
||||
NPM --> RetryTick & PendTick & PendShimTick & UdpTick & ReconcileTick & NP
|
||||
US -->|共用同一条分块与挂起 send| LS
|
||||
UR -->|登记 / 结算| SU
|
||||
UdpTick -->|读出一个完整数据报| SU
|
||||
UdpTick -->|xTaskNotify 完成或超时| XN
|
||||
LC -->|结束 UDP 挂起 recv(settle_close)| SU
|
||||
UB & US & UR & UP & UdpTick --> CH
|
||||
RetryTick --> RLS
|
||||
ReconcileTick -->|"① 已断"| EV
|
||||
ReconcileTick -->|"② 补 accept"| EV
|
||||
@@ -97,14 +110,14 @@ flowchart TB
|
||||
XN -->|唤醒| PUB
|
||||
NP --> CH
|
||||
LL & LN & LS & LR & LA & LC & DC & RLS --> CH
|
||||
SPI -->|直碰 SPI| CH
|
||||
SPI -->|仅板级硬件层测试:netTask 自挂起时独占芯片| CH
|
||||
CH -->|INT# 低电平| NP
|
||||
```
|
||||
|
||||
**读图要点**:
|
||||
|
||||
1. 应用侧只有两种"等":① 入队后 `xTaskNotifyWait` 等回执(同步语义);② 挂起 recv / send 的等待(同一通道,只是"完成"由 `netPendingTick` / `netSendPendingTick` 判定,不由 netTask 立即处理)。
|
||||
2. `net_process_messages()` 开头的 **4 个 tick 是网络层自愈主干**:没有任何应用消息时,重试 / 挂起 recv 完成 / 挂起 send 推进 / 状态对账 + 监听重建照常运转——"断连后即使没人发消息,监听器也能自己活过来"靠的就是它们。
|
||||
1. 应用侧只有两种"等":① 入队后 `xTaskNotifyWait` 等回执(同步语义);② 挂起 recv / send 的等待(同一通道,只是"完成"由 `netPendingTick` / `netSendPendingTick` / `netUdpTick` 判定,不由 netTask 立即处理)。
|
||||
2. `net_process_messages()` 开头的 **5 个 tick 是网络层自愈与等待结算主干**:没有任何应用消息时,重试 / 挂起 recv 完成 / 挂起 send 推进 / UDP 挂起 recv 完成与超时 / 状态对账 + 监听重建照常运转——"断连后即使没人发消息,监听器也能自己活过来"靠的就是它们。
|
||||
3. 事件处理器**只由 `net_poll` 驱动**,例外是 `net_reconcile_tick`(分级去抖 + 1s 冷却)可再入 `handle_disconnect_event` / `handle_connect_event`——对账是事件未收到的自愈通道(TRAP-NET-17)。**注意职责边界(对账与重建合并后)**:对账与事件处理器**只置事实**(置 CLOSED / 登记沉降),**重建监听的动作统一由 `net_reconcile_tick` 形态③④ 做**(本层唯一重建入口)——"谁先看到 DISCONNECT 就谁重建"的竞争在结构上消失。
|
||||
4. 回调 `event_cb` 在 **netTask 上下文**执行:回调内不得阻塞、不得发网络操作。
|
||||
5. **模块归属**:`RetryTick`(D10 重试)/ `RLS` 的 `net_listener_request` / `ST`(settle)在 `net_listener.c`(真机专属、模拟器不编译);挂起 recv 完成检查(`net_pending_tick`)、挂起 send 推进(`net_send_pending_tick`)与两张挂起表(`PR` / `SR`)在 `net_pending.c`(真机专属);其余 tick(含合并后的 `ReconcileTick` reconciler)与 `_locked` 在 `net_socket.c`。
|
||||
@@ -126,7 +139,7 @@ flowchart TB
|
||||
| 函数 | 设计作用 | 主要调用 | 意图 / 注意 |
|
||||
|---|---|---|---|
|
||||
| `net_poll()` | 事件泵:GPIO 轮询 INT#(低=有待处理)→ `GET_GLOB_INT_STATUS_ALL`(读=清)→ 按位图逐 Socket 读 SINT → 分派 5 类事件;**每轮末尾**对在用 Socket 全量刷新**驱动状态缓存** + 步进 `ch395f_sock_reset_poll` | `HAL_GPIO_ReadPin` → `ch395f_get_glob_int_status_all` → `ch395f_get_sock_int_status` ×n → `handle_connect/disconnect/recv/send/timeout_event` → `ch395f_get_sock_state` ×n(刷新缓存)+ `ch395f_sock_reset_poll` ×n | **RECV 中断是电平触发**:每次调用**只处理一批**(do-while(0)),处理完立即返回,否则主循环(echo/应用逻辑)饿死(TRAP-NET-08);SENBUF_FREE 只置 `send_ready` 不记事件;状态缓存**唯一刷新点**在此(reconciler 只读缓存,E2 实测单次回读 18.1µs);`sock_reset_poll` 步进使同步包装 600ms 放弃后的复位机不搁浅(IDLE 槽纯 RAM 判零 SPI) |
|
||||
| `net_process_messages()` | 队列入口 + 自愈 tick 宿主:每轮**先**跑 4 个非阻塞 tick,再排空队列逐条分派,结果 `xTaskNotify` 回传 | `net_listener_tick` → `net_pending_tick` → `net_send_pending_tick` → `net_reconcile_tick`(对账+重建合一)→ `osMessageQueueGet`(0 超时) ×n → switch 分派 → `xTaskNotify` | ① tick 放在队列**之前**:断连后即使没有任何应用消息,监听器也能自主恢复、挂起 recv / send 也能完成;② RECV 若返回 `NET_PENDING_REGISTERED`(哨兵 0x70000000)则**不发即时回执**(`do_notify=0`),完成时才通知;③ 失败结果把 `s_net_errno`(具体负错误码)压入通知值——调用方恢复 errno 后对外统一 -1(BSD 语义);④ 通知值**整个 32 位**都是结果(不压位域标签,否则 32768 这类大字节数会溢出为负);⑤ **对账①置 CLOSED 后形态③④同轮即可重建**(合并后,原"pump 置于对账之后"的顺序依赖在单 tick 内自然成立) |
|
||||
| `net_process_messages()` | 队列入口 + 自愈 tick 宿主:每轮**先**跑 5 个非阻塞 tick,再排空队列逐条分派,结果 `xTaskNotify` 回传 | `net_listener_tick` → `net_pending_tick` → `net_send_pending_tick` → `net_udp_tick`(UDP 挂起 recv 完成/超时)→ `net_reconcile_tick`(对账+重建合一)→ `osMessageQueueGet`(0 超时) ×n → switch 分派 → `xTaskNotify` | ① tick 放在队列**之前**:断连后即使没有任何应用消息,监听器也能自主恢复、挂起 recv / send 也能完成;② RECV 若返回 `NET_PENDING_REGISTERED`(哨兵 0x70000000)则**不发即时回执**(`do_notify=0`),完成时才通知;③ 失败结果把 `s_net_errno`(具体负错误码)压入通知值——调用方恢复 errno 后对外统一 -1(BSD 语义);④ 通知值**整个 32 位**都是结果(不压位域标签,否则 32768 这类大字节数会溢出为负);⑤ **对账①置 CLOSED 后形态③④同轮即可重建**(合并后,原"pump 置于对账之后"的顺序依赖在单 tick 内自然成立) |
|
||||
|
||||
### 4.3 经队列的公共 API 及其 netTask 侧实现
|
||||
|
||||
@@ -145,14 +158,17 @@ flowchart TB
|
||||
| `net_drop_conn` | `NET_MSG_DROP_CONN` | `net_drop_conn_locked` | `net_pending_settle_close` + `net_sock_reset_sync(QUARANTINE)` +(auto_relisten)`net_listener_request` | 0(含幂等)/ -1 | **拆会话保监听**(队头阻塞收口):应用放弃会话(对时 30s 预算耗尽 / 协议超时)而单连接监听被挂起对端永久占住时的应用侧出口;EOF 路径**不得**调(`auto_relisten` 已接管);非 ESTABLISHED/CONNECTING 直接返 0 不动监听;本层收口 CLOSED 不清 fd(保留 local_port/auto_relisten),监听型 fd 登记重监听、失败交 D10 + reconciler 形态④ |
|
||||
| ~~`net_relisten`~~ | ~~`NET_MSG_RELISTEN`~~ | **已删除**(消息类型同删) | — | — | 原"无条件芯片侧重建"与 pump/D10 完全重叠,且对健康监听器是 footgun(open→USE→close 自毁 ~1s);生产零调用。历史实现见 `net_socket.c` git 历史 |
|
||||
| ~~`net_resync_listen`~~ | ~~`NET_MSG_RESYNC_LISTEN`~~ | **已删除**(语义并入对账⑤) | — | — | D14 静默回收(芯片自行回监听、不发 DISCONNECT)现由 `net_reconcile_tick` 判据⑤自动认领:本层 ESTABLISHED 且芯片 0x05/0x01 → 只修本层(state→LISTENING + 转登 `last_peer_port` + 清 `lost_conn_seen` + 取消挂起 recv),**不发芯片命令**(纪律原样保留,run8 实证 170s 拒绝);去抖/冷却沿用对账框架 |
|
||||
| `net_bind`(**UDP 支**) | `NET_MSG_UDP_BIND`(端口按 `ntohs` 后的值传,0 = 由执行体取动态号) | `net_udp_bind_exec` | Socket 4~7 兜底 `set_send_buf` / `set_recv_buf` + `set_proto_type(UDP)` + `set_sour_port` + `set_des_ip(0xFFFFFFFF)` + `set_des_port(0)` + `open_socket` | 0 / -1 | **bind 恒服务器模式**(BSD 的 bind 只定本地端口,不选对端);DesIP/端口写在 open **之前**(open 复位寄存器,写晚了等于没写);动态端口游标在 netTask 内推进(旧形态在调用方上下文改静态量 = 跨任务竞争) |
|
||||
| `net_sendto` | `NET_MSG_UDP_SENDTO`(目的地址按值拷进 `msg.addr`,`addrlen > 0` 才改 DesIP) | `net_udp_sendto_exec` → **共用** `net_send_locked` | `set_des_ip` / `set_des_port`(带地址时)+ `write_send_buf` 分块 ≤ 1KB | 实际字节数(**可能短写**)/ -1 | 带目的地址 = 切**客户端模式**并记账 `remote_*`;发送与 TCP 走同一条分块 + 挂起 send(`net_send_pending`)通路,不为 UDP 复制第二份 1KB 分块逻辑;未 bind → NOTCONN(芯片侧还没 open) |
|
||||
| `net_recvfrom` / `net_recvfrom_to` | `NET_MSG_UDP_RECVFROM`(**出参指针不入队**:`net_udp_outparam_arm` 存进本 fd 槽,netTask 直接写回调用方内存) | `net_udp_recvfrom_exec`(即时读 / 登记挂起)+ `net_udp_tick`(完成 / 超时结算) | `net_poll` + `get_recv_len` + `read_recv_buf`(剥头 + 截断排空) | >0 / -1 / **-2(`_to` 预算内无数据,不占 errno)** | 一次调用 = 一个完整数据报;**按 `udp_mode` 决定剥不剥 8 字节信息头**(服务器剥、客户端不剥——旧形态不分模式恒剥,客户端模式等于吃掉载荷前 8 字节并回填假源地址);无数据 + 阻塞 → 登记挂起立即让出 netTask;缓冲装不下时剩余部分**读空丢弃**;同 fd 第二个接收者 → BUSY |
|
||||
| `net_udp_set_peer` | `NET_MSG_UDP_PEER`(`addrlen == 0` = 回服务器模式) | `net_udp_set_peer_exec` | `set_des_ip` + `set_des_port`(**不重开 Socket**) | 0 / -1 | 模式切换的轻量入口,给"上一轮当客户端问、这一轮要听所有人"的收包方;要连芯片缓冲残留一起丢掉就再 `net_bind()` 一次(重开形态) |
|
||||
|
||||
### 4.4 直调 API(不经队列)
|
||||
|
||||
| 函数 | 设计作用 | 主要调用 | 意图 / 注意 |
|
||||
|---|---|---|---|
|
||||
| `net_socket(domain,type)` | 分配空闲控制块(`alloc_socket` 线性扫描 `in_use=0`)→ CREATED | `alloc_socket` + `net_listener_clear` + `net_pending_clear` | 新 fd 不得继承上一次的待重试/挂起状态(复用描述符的经典坑);domain 仅 `NET_AF_INET` |
|
||||
| `net_bind(sockfd,addr,addrlen)` | TCP:仅记录本地端口(port=0 → 动态端口 49152 起递增)→ BOUND,**无芯片命令**;UDP:完整开 Socket → UDP_OPEN | UDP 路径:`set_proto_type(UDP)` + `set_sour_port` + `set_des_ip(0xFFFFFFFF)` + `set_des_port(0)` + `open_socket`(Socket 4~7 兜底重设缓冲) | TCP 的 bind 只是"记账",真正的芯片动作在 listen/connect;UDP 直碰 SPI(§2 三例外之一);动态端口回绕 65535→49152 |
|
||||
| `net_sendto` / `net_recvfrom` | UDP 收发(直碰 SPI,不经队列) | `set_des_ip` / `set_des_port` + `write_send_buf`(分块 1KB + `net_poll` 门控);`net_poll` + `get_recv_len` + `read_recv_buf` | 同 §2 上下文约束;recv 时 UDP Server 模式前 8 字节是固定头(源 IP + 端口 + 长度),先剥头填 `src_addr` 再读数据;recvfrom 阻塞语义与 net_recv 同构(blocking 属性 / DONTWAIT,无默认预算;轮询自旋实现,生产零调用) |
|
||||
| `net_bind(sockfd,addr,addrlen)` | **TCP 支**:仅记录本地端口(port=0 → 动态端口 49152 起递增)→ BOUND,**无芯片命令** | 写 `s_net_socks[fd].local_port` / `state` | TCP 的 bind 只是"记账",真正的芯片动作在 listen/connect;**UDP 支自 2026-11-03 起入队**(要发 5 条芯片命令,见 §4.3 `net_bind`(UDP 支)行);动态端口回绕 65535→49152 |
|
||||
| `net_setblocking(sockfd,on)` | 设置阻塞**模式属性**(直写控制块,不经队列) | 写 `s_net_socks[fd].blocking` | 对齐 BSD `O_NONBLOCK` / Python `setblocking`:"模式归使用者、超时归调用方";单字节原子写(先例 `net_set_event_cb`),最坏一次在途 recv 用旧模式;`net_socket()` 默认置 1(close 的 memset 清零后新 fd 必须显式重置) |
|
||||
| `net_wait_result(kind,sockfd)` | 调用侧两段式等待:**零等待拾取** → 阻塞等回执 | `xTaskNotifyWait` ×2 | ① 零等待段兜住"入队→等待入口"间隙落地的通知(2026-09-22 台架实证丢通知);② **清除掩码必须全 1**(传 0 = 不清 → 上次结果残留被重读,run3~11 挂死/错位的真根因);③ 必须判 **pdTRUE** 而非 `v!=0`(合法结果 0 = EOF 会被误判"无通知"→ 永久挂死);④ 32 位整体作结果(32768 大字节数不截断);⑤ 本路径**禁止**加逐调用日志(曾占日志 98.6%、冲掉 [LAT] 证据行,run14 实证) |
|
||||
| `net_sock_state(sockfd)` | 跨任务只读状态快照(单字节读,无锁) | 无 | 描述符无效/未占用 → -1;值可能滞后 netTask 最新状态——用于**轮询**(客户端等 ESTABLISHED),不替代 accept/connect |
|
||||
@@ -257,8 +273,31 @@ flowchart TB
|
||||
|
||||
---
|
||||
|
||||
### 4.11 UDP 数据面(`net_udp.c`,2026-11-03 队列化改造)
|
||||
|
||||
> **模块归属**:`Drivers/BSP/NET/net_udp.c`(真机专属,模拟器不编译——模拟器端 UDP 由 `simulator/sim/net_socket_sim.c` 的 Winsock 形态承担)。
|
||||
> **使用侧**契约(芯片两种模式、调用约束、超时口径)权威在 `docs/网络/BSD_Socket_API_使用指南.md` §6.0 / §6.3;
|
||||
> 本节只讲调用结构与"为什么这么切"。
|
||||
|
||||
| 函数 | 上下文 | 设计作用 | 关键决策 |
|
||||
|---|---|---|---|
|
||||
| `net_udp_outparam_arm` / `net_udp_outparam_disarm` | 调用方任务(临界区只包"查占位"三步) | 登记"这次收包的源地址写回哪块调用方内存" | **源地址不搭 `net_msg_t`**:该结构按值拷入队列,AGENTS / TRAP-NET-10 明令禁止给它加"回传用"字段;出参指针进 per-fd 槽,netTask 直接写回调用方自己的内存,通知值只带结果码 |
|
||||
| `net_udp_clear` | 两侧 | 清本 fd 槽(`net_socket` 分配新 fd、入队失败、执行体每条出口都要清) | 槽位泄漏 = 该 fd 从此永久 `BUSY`;出口必清(ns_s7_udp.c UDP-7d / UDP-9e 钉住) |
|
||||
| `net_udp_bind_exec` | netTask | 服务器模式开芯片侧 Socket(5 条命令 + Socket 4~7 缓冲兜底) | DesIP/端口写在 open **之前**(手册 §9.2.4;open 复位寄存器);bind **恒服务器模式**——BSD 语义里 bind 不选对端;动态端口游标在 netTask 内推进(消掉跨任务写静态量) |
|
||||
| `net_udp_set_peer_exec` | netTask | 只写 DesIP/DesPort 切形态;`peer == NULL` 回服务器模式 | 不重开 Socket,因此**不清**芯片缓冲;要连残留包一起丢就再 `net_bind()`(重开形态) |
|
||||
| `net_udp_sendto_exec` | netTask | 记对端账 + **复用 `net_send_locked`** 发送 | 不为 UDP 复制第二份 1KB 分块 / 挂起 send 逻辑;"会话是否已死"由 `net_send_state_down()`(`net_pending.c`)按 `type` 分流——TCP 看 ESTABLISHED、UDP 看 UDP_OPEN,一条判据两侧共用(`net_send_locked` / `net_send_pending_tick` / `net_pending_tick` 三处原先各写一遍) |
|
||||
| `net_udp_recvfrom_exec` | netTask | 三出口:数据已在 → 读一包;DONTWAIT / `blocking=0` → WOULDBLOCK;阻塞 → 登记挂起并立即让出 netTask | 自等守卫判 `caller == xTaskGetCurrentTaskHandle()`,**不是** `net_ctx_is_owner_task()`:执行体本身就跑在 netTask 里,用后者等于把每一条经队列投进来的正常请求都当自等拒掉(UDP-8a 当场抓到过这个错) |
|
||||
| `net_udp_tick` | netTask 1ms | 挂起 recv 的完成 / 预算超时 / 会话已死三类结算 + `xTaskNotify` | 与挂起 recv、挂起 send 同一节奏;`readable` 只当"省一次 SPI"的门,电平型 RECV_OK 每轮重读可自愈(事件漏了也不永久瞎) |
|
||||
| `net_udp_datagram_read` | netTask(private) | 一个完整数据报:按 `udp_mode` 决定剥不剥 8 字节信息头 → 截断排空 → 清 `readable` | `avail` 由调用方读一次**传进来**:剥头量、载荷量、排空量必须由同一个数字算出(读两次 = 决策与内容可能不同源);服务器模式不足 8 字节的残包不猜头,整包按载荷交付(TC-NETSOCK-440o 定案) |
|
||||
| `net_udp_settle_close` / `net_udp_cancel_all` | netTask | close / 网络重建时结算等待者 | **任何结算都必须通知**:close 时有数据先交付、无数据回 CONNRESET;`net_init` 重建走 `net_udp_cancel_all`(UDP-8 / UDP-12 钉住"不许静默丢通知")。⚠ **没有** per-fd `net_udp_cancel`:唯一的 per-fd 取消点在 `net_events.c` 的 reconcile 形态⑤,而形态①⑤的判据门是 `state == ESTABLISHED`(TCP 专属,UDP 在 `net_events.c` 里被"其余状态不归本 tick 认领"挡掉),UDP 挂起收包的"会话已死"结算由 `net_udp_tick` 第①条按状态门当轮完成(UDP-19 钉住),不留没人调的 API |
|
||||
|
||||
改造前后的差别一句话:**改造前 UDP 在调用方上下文自旋碰 SPI**(既违反"netTask 唯一芯片访问者",也把
|
||||
IWDG 预算吃在自旋上,且源地址无法回传);**改造后 UDP 与 TCP 同构**——入队、netTask 串行执行、挂起态
|
||||
交 tick 结算、结果搭 32 位通知值回来。
|
||||
|
||||
## 5. 典型端到端路径
|
||||
|
||||
|
||||
### 5.1 发送(队列路径)
|
||||
|
||||
```mermaid
|
||||
@@ -409,15 +448,15 @@ sequenceDiagram
|
||||
| `ch395f_set_mac_addr` | `net_init`(**须在 `ch395f_init` 之前**) | 写芯片 EEPROM;INIT_CH395 加载 MAC(`docs/芯片手册/CH395F.md` §9.2.1 步骤①) |
|
||||
| `ch395f_set_ip_addr` / `set_gwip_addr` / `set_mask_addr` / `set_dhcp` | `net_init`(**均须在 `ch395f_init` 之前**) | 固定 IP 或 DHCP |
|
||||
| `ch395f_init` / `ch395f_ping_enable` | `net_init` | 协议栈初始化(INIT_CH395)/ PING(须在 init 之后) |
|
||||
| `ch395f_set_send_buf` / `ch395f_set_recv_buf` | `net_init`(8 Socket 布局);`net_bind`(UDP)(Socket 4~7 兜底) | 每 Socket 收 4 块 + 发 2 块 |
|
||||
| `ch395f_set_send_buf` / `ch395f_set_recv_buf` | `net_init`(8 Socket 布局);`net_udp_bind_exec`(Socket 4~7 兜底,2026-11-03 起在 netTask 内执行) | 每 Socket 收 4 块 + 发 2 块 |
|
||||
| `ch395f_get_glob_int_status_all` | **仅 `net_poll`**(唯一读者红线:全局寄存器唯一读者,见 `docs/网络/网络自愈架构设计.md` §4.10;`net_listen_locked` / `net_connect_locked` / `net_close_locked` / `net_relisten_locked` 的原全局读清已全部删除) | **读 = 清**(中断清除机制,无按位手段)——其他位置读清会焊死 net_poll 的按 Socket 门控 |
|
||||
| `ch395f_get_sock_int_status` | `net_poll`(逐 Socket SINT);各 open 路径后清残留;事件处理器清 TIMEOUT | **读 = 清** |
|
||||
| `ch395f_sock_events` | `net_connect_locked`(open 后清残留,唯一读者红线的护栏原语,见 `docs/网络/网络自愈架构设计.md` §4.10);`net_close_locked`(芯片已 CLOSED 快路径只清本 Socket 残留事件) | 只碰**本 Socket** SINT 的护栏原语 |
|
||||
| `ch395f_open_socket` / `ch395f_close_socket` | `net_bind`(UDP)、`net_listen_locked`、`net_connect_locked`、`net_relisten_locked`(含沉降期主动 close);`net_close_locked` 的 CLOSE 已改经 `ch395f_cmd_done` / `ch395f_sock_reset_*` | TRAP-NET-09:close 后需沉降才能重开 |
|
||||
| `ch395f_open_socket` / `ch395f_close_socket` | `net_udp_bind_exec`(UDP 开 Socket)、`net_listen_locked`、`net_connect_locked`、`net_relisten_locked`(含沉降期主动 close);`net_close_locked` 的 CLOSE 已改经 `ch395f_cmd_done` / `ch395f_sock_reset_*` | TRAP-NET-09:close 后需沉降才能重开 |
|
||||
| `ch395f_tcp_listen` / `ch395f_tcp_connect` | `net_listen_locked` / `net_relisten_locked`;`net_connect_locked` | — |
|
||||
| `ch395f_set_proto_type` / `set_sour_port` / `set_des_ip` / `set_des_port` | `net_bind`(UDP)、`net_listen_locked`、`net_connect_locked`、`net_relisten_locked`、`net_sendto` | open 之后写端口/IP(open 会复位寄存器);set_des_ip 期望**大端数组** |
|
||||
| `ch395f_write_send_buf` | `net_send_locked` / `net_send_pending_tick`(挂起 send 推进)/ `net_sendto` | 单次 ≤1KB(硬件 FIFO 上限,TRAP-NET-16) |
|
||||
| `ch395f_get_recv_len` / `ch395f_read_recv_buf` | `net_recv_locked`、`net_pending_tick`、`net_pending_settle_close`、`net_recvfrom`、`net_drain_chip_rx`、`handle_disconnect_event`(M3 可疑门 RX 非空判据) | `handle_recv_event` **故意不读**(防竞争,旧机理未证实) |
|
||||
| `ch395f_set_proto_type` / `set_sour_port` / `set_des_ip` / `set_des_port` | `net_udp_bind_exec`、`net_udp_set_peer_exec`、`net_udp_sendto_exec`(2026-11-03 起 UDP 三条都只在 netTask 内跑)、`net_listen_locked`、`net_connect_locked`、`net_relisten_locked` | **两种顺序都存在,别当成一条**:TCP 各路径在 open **之后**写(open 会复位寄存器,TRAP-NET-14);UDP bind 按手册 §9.2.4 在 open **之前**写完 5 条——两侧都有钉子(TCP 见 TC-NETSOCK-410 组,UDP 见 ns_s7_udp.c UDP-1f 的命令顺序断言);set_des_ip 期望**线序数组**(`arr[0]` = 点分第一段) |
|
||||
| `ch395f_write_send_buf` | `net_send_locked`(TCP 与 **UDP sendto 共用**,2026-11-03)/ `net_send_pending_tick`(挂起 send 推进,同样两侧共用) | 单次 ≤1KB(硬件 FIFO 上限,TRAP-NET-16) |
|
||||
| `ch395f_get_recv_len` / `ch395f_read_recv_buf` | `net_recv_locked`、`net_pending_tick`、`net_pending_settle_close`、`net_udp_recvfrom_exec`、`net_udp_tick`、`net_udp_settle_close`(UDP 剥头与截断排空都在这三处)、`net_drain_chip_rx`、`handle_disconnect_event`(M3 可疑门 RX 非空判据) | `handle_recv_event` **故意不读**(防竞争,旧机理未证实);一次收包**只读一次长度**——剥头量/载荷量/排空量必须由同一个数字算出(`net_udp_datagram_read` 的 `avail` 参数) |
|
||||
| `ch395f_get_socket_status` | `handle_disconnect_event`(两处甄别——事件路径**主动 SPI**,缓存必然滞后于刚到达的中断)、`net_relisten_locked`(沉降门)、`net_connect_locked`(诊断回读)。⚠ **周期对账不再直接调**——改读缓存(下行) | 输出 **2 字节**([0]=Socket 状态 [1]=TCP 状态),缓冲必须给 2 字节 |
|
||||
| `ch395f_get_sock_state` / `ch395f_sock_state_cached` | 刷新**仅 `net_poll`** 每轮对在用 Socket 调 `get_sock_state`(唯一刷新点);`sock_state_cached` 读缓存 = `net_reconcile_tick`(形态①②③④⑤/U11 判据) | 芯片状态唯一事实源;E2 实测单次回读 18.1µs,缓存新鲜度 ≤1ms |
|
||||
| `ch395f_sock_reset_begin` / `ch395f_sock_reset_poll` | `net_sock_reset_sync`(`net_listen_locked` / `net_connect_locked` open 重试、`net_close_locked`、`net_drop_conn_locked`);`net_poll` 每轮对在用 Socket **步进**(IDLE 槽纯 RAM 判零 SPI) | 驱动复位状态机:读空 RX → CLOSE → 排空事件(quiescent) → 等 byte0=00H;内置 LISTEN 态二段 CLOSE 补发(E2 补充发现);GRACEFUL 100ms 底 / QUARANTINE 400ms 底 |
|
||||
|
||||
@@ -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