4 Commits

Author SHA1 Message Date
super_agent_bot fc0d3e4b5c test(net/udp): 阶段 16 台架加固 + 真机首跑证据回填(#73)
CI / 静态门禁 (pre-commit) (push) Failing after 5m39s
CI / 主机侧单测 (MSVC) (push) Successful in 30s
CI / 静态门禁 (pre-commit) (pull_request) Failing after 4m15s
CI / 主机侧单测 (MSVC) (pull_request) Successful in 47s
第一次上真机把台架的两处软肋照出来了,都不是被测代码的问题:

- peer 死在 Windows 的 ICMP 10054:板子还没 bind 时那一下端口不可达会被记在套接句上,
  下一次 recvfrom 才抛出来。本机 CPython 3.14.7 既没有 socket.SIO_UDP_CONNRESET 也不接受
  它的数值形式,所以主力防线改成把 10054 当「这一轮没收到」继续重试,并在回路自检里加
  冷端口探针——探针走脚本真正在用的 _recv_maybe,另拿裸套接句证明这台机器确实会回 ICMP,
  否则那条判定就没有牙。
- 单步预算错位:板端每步等 25 s,peer 每步只等 8 s,一步超时后整条梯子全灭。现在 peer
  每步都重发、单步预算 --step-budget 默认 35 s(小于 26 s 打警告);板端 udp_bench_expect
  按载荷第 0 字节的步号自愈,比本步小的旧包丢掉继续等(上限 UDP_BENCH_STALE_MAX = 8),
  比本步大的直接报出来。DBG_WARN 在本工程不存在、字符串字面量必须纯 ASCII,两处已改。

真机已拿到的证据:TC-NET-1601a/b/c(建 socket、bind :6001、net_udp_get_mode 回 SERVER)与
TC-NET-1602(服务端形态收到 20 B、addrlen/sin_family/源端口全对、逐字节回显、形态翻成
CLIENT)全绿——剥 8 字节信息头与源地址回填在真芯片上成立;②③④⑤ 因板子当晚两次失电
(19:19、21:0x,SWD No target connected + ping 不通)未测,指南 §12.6 与覆盖率口径第 4 轮
的状态条据此改写。产品态(宏回默认)重新构建 0 Error 0 Warning,Code 249,124 / RO 29,836 /
RW 2,052 / ZI 149,996 与本阶段之前逐字节一致,阶段 16 在产品里被预处理干净。
2026-11-03 21:28:36 +08:00
super_agent_bot b0a6394aa1 fix(test/udp): peer 换源端口上报方式并补 PC 回路自检(#73)
CI / 静态门禁 (pre-commit) (push) Failing after 5m4s
CI / 主机侧单测 (MSVC) (push) Successful in 44s
CI / 静态门禁 (pre-commit) (pull_request) Failing after 4m48s
CI / 主机侧单测 (MSVC) (pull_request) Failing after 44s
- test/host/udp_queue_bench.py:Windows 上未 connect 的 UDP socket 调 getsockname()
  直接抛 WinError 10022(请求一个未连接套接句的名称),脚本在第一条用例就崩。
  源端口改为首次收到回显时打印——它本来就是「板端剥头/源地址判定对不对」的直接读数。
- 新增 test/host/udp_queue_bench_loopback.py:不用板子的 peer 自检,在 127.0.0.1 上放
  一个按板端阶段 16 同样三条规矩说话的假板子(原样回显、tag 0x04 只回 64 B、>1KB 按
  1024 拆片),先把步骤表 / 模式函数 / 分片预期验掉。2026-11-03 首跑 27/27 绿,
  实测拆片形状 [1024, 1] 与 [1024, 448]。
- CH395F_Test_Guide.md §12.6:运行方法补「先跑自检」这一步;>1024 B 的 UDP 拆片从
  「多收到一片就 FAIL」改成「实测观测量」(拼接后逐字节一致才算绿),并说清这是既有
  NET_SEND_CHUNK_MAX 行为、不是本轮引入;脚本表补一行。
- 板子自 19:19 起未再上电(ping 不通、SWD No target、串口静默),阶段 16 真机那一趟
  仍未跑,PR #74 的合并前提不变。
2026-11-03 20:46:15 +08:00
super_agent_bot 8a81fa9e41 test(net/udp): 阶段 16 UDP 数据面真机回归台架(板端 + PC peer),台账补实测数字(#73)
CI / 静态门禁 (pre-commit) (push) Failing after 5m27s
CI / 主机侧单测 (MSVC) (push) Successful in 33s
CI / 静态门禁 (pre-commit) (pull_request) Failing after 7m6s
CI / 主机侧单测 (MSVC) (pull_request) Successful in 48s
- 板端 net_layer_udp_bench_test_main:TC-NET-1601~1609,逐步覆盖主机测替代不了的
  5 条台架必验——形态切换与 bind 后首包、48 B 应答在客户端形态的载荷完整(TRAP-NET-22
  的真机对照)、截断包排空、挂起等待期间的 IWDG 余量、DesIP 先于 OPEN_SOCKET 的顺序。
  每步都有 25 s 预算,PC 缺席时报 [FAIL] 而不是把 testTask 钉死。
- PC peer test/host/udp_queue_bench.py:板端先 bind 当服务端,对端地址从收到的包里学
  (PC 是 DHCP,TEST_PC_PEER_IP 那个固定地址不在本机上);对 >1024 B 的回显额外监听
  第二片,把 net_send_locked 分块在 UDP 上的后果量出来。
- ch395f_test.h:阶段 16 宏默认关(与其它阶段一致),RAM 台账补 2026-11-03 实测——
  台架镜像 RW_IRAM1 96,232 B / 73.4%,阶段 16 自身代价 1,600 B;产品镜像 RW+ZI 与改造前逐字节相同。
- 文档:CH395F_Test_Guide §12.6 跑法与判据 + Python 脚本表加一行;覆盖率口径第 4 轮
  「台架必验」指向本阶段的可执行形态,并写明尚未在真机上跑过。
2026-11-03 20:18:24 +08:00
super_agent_bot fff961d803 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/任务规划/**。
2026-11-03 18:44:01 +08:00
13 changed files with 1185 additions and 16 deletions
+9
View File
@@ -383,6 +383,11 @@ int net_udp_sendto_exec(int sockfd, const void *buf, int len,
* 返回;完成条件由 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) {
@@ -401,12 +406,16 @@ int net_udp_recvfrom_exec(int sockfd, void *buf, int len, int flags,
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);
}
@@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1`
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
| 区域 | 用量 | 容量 | 占比 |
|---|---|---|---|
| ER_IROM1 | 278,948 B (272.41 KB) | 1,048,576 B (1024 KB) | 26.6% |
| 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 合计)** | **281,000 B (274.4 KB)** | 1,048,576 B (1024 KB) | **26.8%** |
| **Flash (ROM 合计)** | **281,012 B (274.4 KB)** | 1,048,576 B (1024 KB) | **26.8%** |
<!-- MEM_SNAPSHOT:END -->
各区的容量与常量来源:
+1 -1
View File
@@ -15,7 +15,7 @@
<!-- MEM_BUDGET:BEGIN generated by scripts/ram_baseline.py -->
| 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) |
|---|---|---|---|---|
| ER_IROM1 (Flash) | 278,948 B | 1,048,576 B | 26.6% | 参考 |
| 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,624 B | 131,072 B | 72.2% | PASS |
@@ -719,12 +719,13 @@ uint16_t ch395f_link_change_count(void);
- **不写第二份分块逻辑**: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` 共 20 组 UDP-1~20(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 结算);改写 TC-NETSOCK-440(补 bind 前置 + 新增 440p~440u)、555(表头逐字节按形态分路)、560 组("拒在 netTask 外"→"经队列后非 netTask 仍零 SPI")、500h-i / 506 / 545。全套 **616/616 绿**(改造前 470)。
- **用例面**:新分片 `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 只是归零**。
- **本轮覆盖率**:`net_udp.c` 234/236 行(99.2%)、`net_socket.c` 956/969(98.7%)、`net_pending.c` 173/199,全仓 line 8359/9287(90.0%)、47/47 套绿;`net_udp.c` 作为新文件随本轮 `--update-baseline` 登记,未覆盖的两处是 `net_udp.c` 的槽越界兜底(调用方已挡)与 `net_socket.c` 错误码槽重查竞态(真并发才走得到,改造前后同为那 2 行)。
- **本轮覆盖率**(收尾那次全仓跑数,`--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 侧对照)。
- **这 5 条已落成可执行的板级阶段**:阶段 16(`docs/测试说明/板级测试/CH395F_Test_Guide.md` §12.6,板端 `test/board/net_test_task.c` 的 `net_layer_udp_bench_test_main`,PC 侧 `test/host/udp_queue_bench.py`),TC-NET-1601~1609 与 ①~⑤ 的对应关系写在表里。**真机只跑了个开头**:2026-11-03 夜第一次上台架,① 的上半段已见证据——`TC-NET-1601` 建 socket/bind `:6001`/形态回 SERVER、`TC-NET-1602` 服务端形态收到 20 B 且 `addrlen`/`sin_family`/源端口全对、回显逐字节一致并把形态翻成 CLIENT(剥 8 字节信息头与源地址回填在真芯片上成立);②③④⑤ 未测,板子当晚两次失电(SWD `No target connected`、ping 不通,19:19 与 21:0x 各一次),两趟还各暴露一个**台架**缺陷(peer 被 Windows 的 ICMP 10054 打死;单步预算 8 s < 板端 25 s park 导致整条梯子错位),修法与守卫见上面那份指南。PR #74 的合并前提仍是这条回归跑完整;跑完请把结果回填本条并把上面「台架必验」改写成实测结论。
- 本轮改动面:`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 行)
@@ -1125,6 +1125,89 @@ WAIT_CLOSE 阶段已内置二段补发(只补一次),生产 net 层则靠
---
## 12.6 阶段 16:BSD UDP 数据面真机回归(NET 层,需 PC peer)
**动机**:PR #74 把 UDP 数据面并入 `netMsgQueue` 串行执行(见
`docs/网络/BSD_Socket_API_函数调用关系.md` §4.11),并顺手修掉「DesIP 形态与剥头不一致」
这个会让 48 字节应答丢掉前 8 字节的缺陷(`docs/问题记录/CH395F_Trap_Records.md` 的 TRAP-NET-22)。
主机单测(`test/host/net_socket/ns_s7_udp.c`,`docs/测试说明/单元测试/主机单测覆盖率口径.md` 第 4 轮)
把逻辑面钉死了,但芯片行为——DesIP 生效时机、UDP 的 SINT 触发、读指针上的残包排空——在桩里怎么编都"对"。
下面 5 条就是那轮登记簿留下的「台架必验」的可执行形态。
**架构**:板端一个 UDP socket,先 `net_bind(:6001)` 做服务端,再从收到的包里学对端地址回显。
**必须板端先听**:PC 侧是 DHCP(本轮实测 192.168.31.161),`net_test_task.h` 里那个
`TEST_PC_PEER_IP` 192.168.31.2 并不在本机上,让板端当客户端就连不上;板端当服务端则从不断言
"对端是谁"才收,对端从实测包文学来,PC 换 IP 也不用改固件。PC 侧跑
`test/host/udp_queue_bench.py` 逐步激励,用**三个不同源端口的 socket** 分批发("客户端形态会不会
把别的源过滤掉"那一步必须有区分度才有意义)。
| 用例 | 内容 | 通过标准 | 必验 |
|------|------|----------|------|
| TC-NET-1600 | 载荷样式自检(fill/match 自洽) | 篡改第 9 字节 → match 返回 10;换 tag → 返回 1 | — |
| TC-NET-1601 | socket + bind :6001 + bind 后首包 | `net_udp_get_mode` = SERVER;20 B 收到且长度/样式/`addrlen`/`sin_family` 全对、学到 PC 源端口;回显后 mode = CLIENT | ① ⑤ |
| TC-NET-1602 | **48 B 应答落在客户端形态**(SNTP 尺寸) | 必须读到 48 B 且 `buf[0]` 就是载荷首字节——判据写错会只剩 40 B 且前 8 字节被当信息头吃掉;回显走 `dest_addr = NULL`(用记账对端那条路) | ① |
| TC-NET-1603 | `net_udp_set_peer(fd, NULL)` 回服务器形态 | 返回 0、mode = SERVER,且**不重开**芯片 socket;随后**换新源端口**发来的包必须收到(客户端形态会被 DesIP 过滤),再回显给这个新端口 | ② |
| TC-NET-1604 / 1605 | 截断与残包排空 | 200 B 的包用 64 B 缓冲只读到 64;紧接着 30 B 的下一个包必须整包到达(残包没排干净,这里会读到上一条的尾巴) | ③ |
| TC-NET-1606 | 超时出口 | 空窗 `net_recvfrom_to(..., 1000)` 返回 **-2**(不是 -9),实耗 1000~4000 ms;下一步照常收发(槽没被毒化) | — |
| TC-NET-1607 | 挂起等待期间的 IWDG 余量 | PC 迟到约 12 s 才发;板端 `net_recvfrom_to(..., 30000)` 醒来看见 24 B 正确载荷,实测等待落在 7~25 s,`net_get_generation()` 不变(没重建没复位)。硬证据在串口日志:这段 `[ERR][WD] task stall` 零条、`[PROF]` 心跳连续 | ④ |
| TC-NET-1608 | 关旧 socket → 新开 → 重 bind :6002 | 新 socket 的首包必到(DesIP 要在 `OPEN_SOCKET` **之前**写才生效) | ⑤ |
| TC-NET-1609 | 分块边界 1 / 20 / 1024 / 1025 / 1472 | 每条往返字节完全一致 | — |
**运行方法**:
1. 先在 PC 上自检 peer 脚本(不用板子):`python test/host/udp_queue_bench_loopback.py`,绿了再往下走。
2. `test/board/test_config.h` 开 `TEST_SUITE_CH395F`;`test/board/ch395f_test.h` **只**开
`ENABLE_PHASE16_TESTS`,并把默认的 `ENABLE_PHASE11_TESTS` / `ENABLE_PHASE14_TESTS` 注掉——
它们会带进 `TEST_LATENCY_PROBE`,污染要证的 UDP 面。
3. `/keil-build` 后烧录(`.dsh/skills/stlink-flash/scripts/stlink_flash.py flash`,`-Rst -Run` 必带)。
4. PC 先起 peer:`python test/host/udp_queue_bench.py --board 192.168.31.100`,再给板子上电/复位;
peer 首步每 4 s 重发,能盖住板子启动那段时间。
5. 判据是**两侧都绿**:板端串口 `=== Phase 16: N/N PASSED (failed=0) ===`,PC 端
`=== UDP Bench PC Peer: N/N PASSED (failed=0) ===`。PC 绿板红 = 板端断言没对上激励;
板端绿 PC 红 = 回显内容或分片不对。Windows 防火墙静默丢包会让首步一直"无回显"(§14 已记)。
6. 跑完把这四行宏恢复成产品态重新构建 + 烧录(`test-suite-switch` 钩子也会拦住带测试宏的提交)。
**限度与观测点**:
- `net_send_locked` 的分块上限 `NET_SEND_CHUNK_MAX = 1024` 是为 TCP 流设计的;UDP 上它会把一条
超过 1024 B 的 `sendto` 拆成多次 `WRITE_SEND_BUF`,芯片就发成**多个数据报**(UDP 没有可拼的流语义)。
这是既有行为、不是本轮引入(改造前的直调 `net_sendto` 就是同一个分块循环,只是那时 UDP 根本没有多任务
调用者)。peer 把它当**观测量**而不是错误:TC-NET-1609 的 1025 / 1472 两条要求「所有片拼回来与发出的
载荷逐字节一致」,并打印实测分片形状;只有拼接后长度或内容不对才判 [FAIL]。
产品侧结论:**UDP 单包保持在 1024 B 以内**(#57 的 SNTP 是 48 B,碰不到这条)。
- peer 脚本自己有 PC 侧回路自检,不用板子:`python test/host/udp_queue_bench_loopback.py`——它在
127.0.0.1 上放一个按上面三条规矩说话的假板子(同样剥 tag 0x04 到 64 B、同样 1024 拆片),把步骤表、
模式函数、分片预期先验一遍。2026-11-03 首跑 `27/27 PASSED`(实测拆片形状 `[1024, 1]` / `[1024, 448]`)。
它只证明**对端脚本**可用,不证明板端行为——真机那一趟仍然一条都不能省。
- TC-NET-1607 用 30 s 预算版的挂起等待,不用无限版:同一套 park/唤醒机制,但无人值守的台架不该留一个
能把任务钉死的等待。无限版(`timeout_ms = 0` → deadline = 0)的语义由主机单测 UDP-6 钉住。
- 本阶段构建实测(2026-11-03 加固后,`MDK-ARM/STM32F407-Demo/STM32F407-Demo.map`):Code 257,388 /
RO 34,136 / RW 2,060 / ZI 151,596;RW_IRAM1 **96,232 B = 73.4%**(距 92% 红线余 24,354 B),
RW_CCM 57,376 B / 87.9% 与产品一致。产品态(宏回默认后重新构建)Code 249,124 / RW 2,052 /
ZI 149,996,与本阶段之前逐字节一致——阶段 16 的代码在产品里被预处理干净。台账数字以
`test/board/ch395f_test.h` 那段为准。
- **真机进度(2026-11-03 夜)**:第一次上台架已跑通**前半段**——板端
`TC-NET-1601a/b/c`(建 socket、bind :6001、`net_udp_get_mode` 回 SERVER)与
`TC-NET-1602`(服务端形态收到 20 B、`addrlen`/`sin_family`/源端口都对、逐字节一致,回显后模式
翻成 CLIENT)**全绿**,PC peer 侧同样收到那 20 B 回显;这就是队列化后的 UDP 收包路径在真芯片上
的第一份证据(剥 8 字节信息头 + 源地址回填 + 回显发送全部正确)。**1603 之后仍未测**:板子在
21:0x 再次失电(SWD `No target connected`、ping 不通、ST-LINK 探针仍在),与 19:19 那次同一现象。
- 首跑抓到两处**台架**缺陷(都不是被测代码的问题,已改掉并各自有守卫):
1. peer 死在 `WinError 10054`——板子还没 bind 时那一下 ICMP 端口不可达会被 Windows 记在套接句上,
下一次 `recvfrom` 才抛出来;本机 CPython 3.14.7 既没有 `socket.SIO_UDP_CONNRESET` 也不接受它的
数值,所以主力防线改成把 10054 当「这一轮没收到」继续重试(`udp_queue_bench.py` 的
`new_peer_socket` / `_recv_after` / `_recv_maybe`),并由
`test/host/udp_queue_bench_loopback.py` 的冷端口探针守着(用脚本真正在用的那条路判定,
同时拿裸套接句证明这台机器确实会回 ICMP,否则探针没有牙)。
2. **一步超时→整条梯子错位**:板端每步等 25 s,peer 每步只等 8 s,于是 1602 之后每一步都踩在板子
的上一步预算里,日志看起来像"全灭"。现在两侧各自加固:peer 每步都重发且单步预算 `--step-budget`
默认 35 s(小于 26 s 会打警告);板端 `udp_bench_expect` 认载荷第 0 字节的步号,比本步小的旧包
丢掉继续等(上限 `UDP_BENCH_STALE_MAX = 8`),比本步大的直接报出来。
3. 顺带一条纪律:跑台架期间**不要用 serial-assistant 守护进程取证**——它重启后不会重开 COM3
(`status` 里 `ready:false`、`rx_bytes:0`),会把整段板端日志丢光;直接 `pyserial` 落盘更稳。
---
## 13. Python 测试脚本参考
| 命令 | 模式 | 说明 |
@@ -1134,6 +1217,8 @@ WAIT_CLOSE 阶段已内置二段补发(只补一次),生产 net 层则靠
| `tcp_file --ip ... --port 8080 --size 20480` | PC 作文件接收 | 测试大文件传输(阶段 7) |
| `udp_client --ip ... --port 60000` | PC 作 UDP Client | 测试 UDP Echo(阶段 3) |
| `udp_server --port 8082` | PC 作 UDP Server | 接收 CH395F UDP 数据 |
| `python test/host/udp_queue_bench.py --board <板 IP>` | PC 作 UDP 对端(独立脚本,不在上面那个脚本的模式里) | 阶段 16 BSD UDP 数据面回归:逐步换源端口、切形态、截断、空窗、迟到包(§12.6) |
| `python test/host/udp_queue_bench_loopback.py` | 不用板子的 peer 回路自检(本机假板子) | 上阶段 16 台架前先验 peer 的步骤表与分片预期(§12.6 第 1 步) |
| `dhcp_server ...` | DHCP Server | 内置 DHCP(阶段 4,需管理员权限) |
### 通用参数
+10 -10
View File
@@ -348,17 +348,17 @@
"func_rate": 1.0,
"funcs_covered": 59,
"funcs_valid": 59,
"line_rate": 0.9866,
"lines_covered": 956,
"line_rate": 0.9969,
"lines_covered": 966,
"lines_valid": 969
},
"Drivers/BSP/NET/net_udp.c": {
"func_rate": 1.0,
"funcs_covered": 14,
"funcs_valid": 14,
"line_rate": 0.9915,
"lines_covered": 234,
"lines_valid": 236
"line_rate": 0.9916,
"lines_covered": 237,
"lines_valid": 239
},
"Drivers/BSP/NETCFG/ftp_cfg.c": {
"func_rate": 1.0,
@@ -596,9 +596,9 @@
"net_socket": {
"files": {
"Drivers/BSP/NET/net_socket.c": 969,
"Drivers/BSP/NET/net_udp.c": 236
"Drivers/BSP/NET/net_udp.c": 239
},
"lines_total": 1205,
"lines_total": 1208,
"ok": true
},
"phasor": {
@@ -852,9 +852,9 @@
"func_rate": 0.9266,
"funcs_covered": 619,
"funcs_valid": 668,
"line_rate": 0.9001,
"lines_covered": 8359,
"lines_valid": 9287,
"line_rate": 0.9012,
"lines_covered": 8372,
"lines_valid": 9290,
"modules_ok": 47,
"modules_total": 47
},
+19 -1
View File
@@ -26,6 +26,7 @@
* Phase 12 | 硬件层 | 独立运行:SPI 字节命令 / DMA 时延基线(SPI_CMD/SPI_DMA,无需 PC)
* Phase 13 | NET 层 | testTask:芯片 + 队列时延(CHUNK_WAIT~FRAME_RT/LOGTAX,需 PC peer,独立运行)
* Phase 14 | NET 层 | testTask:生产路径时延(PROD_CYCLE~UI_LOOP + 模拟器 A/B,需 PC peer)〔M3 规划〕
* Phase 16 | NET 层 | testTask:BSD UDP 数据面真机回归(形态切换/剥头/截断排空/挂起唤醒/分块边界,需 PC peer,独立运行)
* 时延分段定义/判定基线/执行流程:docs/测试说明/专项验证/链路延迟测试说明.md
*/
@@ -75,6 +76,10 @@ extern "C" {
* 与 Phase 11 共存安全:peer 缺席时有 LAT_P14_TIMEOUT_MS 整体超时,自动 done,
* 不永久挡 accept */
#define ENABLE_PHASE14_TESTS /* [网] 生产路径时延(PROD_CYCLE~UI_LOOP) */
/* [网] BSD UDP 数据面真机回归(阶段 16,PR #74 UDP 队列化的台架对照):需 PC 端
* `python test/host/udp_queue_bench.py --board <板 IP>`,**只开本宏**——阶段 7/11/14
* 会把 TEST_LATENCY_PROBE 带进产品路径,污染要证的 UDP 面。判据见 CH395F_Test_Guide 阶段 16。 */
//#define ENABLE_PHASE16_TESTS /* [网] BSD UDP 数据面真机回归(需 PC peer,独立运行) */
/*
* 开关行注释规范(2026-10-25 构建矩阵实测踩出来的,别改回去):
@@ -106,6 +111,18 @@ extern "C" {
* ch395f_test_task.o 数据段仍是 12,285 B)。上表 04 点那组的产品基线已前进,
* 作为当日历史记录保留、不追改(两组差值都在 +136~+152 B 量级,来自新增的
* adc_cal 数据段;结论"最坏态仍余约 29 KB、不必扩 scatter"不变)。
*
* 2026-11-03 阶段 16(BSD UDP 数据面真机回归)复核,本文件仍是数字唯一出处。
* 同一棵树的产品基线这几天已前进到 Code=249,124 / RO=29,836 / RW=2,052 / ZI=149,996,
* RW_IRAM1 94,624 B / 72.2%、RW_CCM 57,376 B / 87.9%——比 10-28 那组高约 19 KB,
* 是期间合入的东西,与阶段 16 无关,别把它算到本阶段头上。
* 配置(TEST_SUITE_CH395F + 只开阶段 16) Code ZI RW_IRAM1 距红线余量
* UDP 台架镜像 257,176 151,596 96,232 73.4% 24,354 B
* 阶段 16 自己的全部代价 = `net_test_task.o` 的 1,600 B .bss(一个 1600 B 收发缓冲,
* 装得下 1472 B 上限的数据报)。产品构建(四行宏全恢复默认)RW+ZI 与改造前逐字节相同;
* UDP 队列化本身在产品镜像里只多 `net_udp.o` 的 224 B .bss。跑法与判据见
* docs/测试说明/板级测试/CH395F_Test_Guide.md §12.6。
*
* 台架冒烟(2026-10-25,_wt 同树构建 → 主树烧录,boot log 20261025_050539):
* 镜像 ch395f_hw_nopc_v2.hex(戳 suite=CH395F dstr=off)单次开机跑完 1/12/15 三段,
* 汇总行 Phase 1: 9/9、Phase 12: 1/1、Phase 15: **21/21**(failed=0),全程无 Fault。
@@ -135,7 +152,8 @@ extern "C" {
#if defined(ENABLE_PHASE6_TESTS) || defined(ENABLE_PHASE7_TESTS) || \
defined(ENABLE_PHASE8_TESTS) || defined(ENABLE_PHASE9_TESTS) || \
defined(ENABLE_PHASE10_TESTS) || defined(ENABLE_PHASE11_TESTS) || \
defined(ENABLE_PHASE13_TESTS) || defined(ENABLE_PHASE14_TESTS)
defined(ENABLE_PHASE13_TESTS) || defined(ENABLE_PHASE14_TESTS) || \
defined(ENABLE_PHASE16_TESTS)
#define ENABLE_NET_LAYER_TESTS 1
#endif
+4
View File
@@ -1757,6 +1757,10 @@ void ch395f_test_run(void)
DBG_INFO("netTask ready");
#if defined(ENABLE_PHASE7_TESTS)
net_layer_client_test_main();
#elif defined(ENABLE_PHASE16_TESTS)
/* 阶段 16(BSD UDP 数据面真机回归,PR #74):独立入口,不占 TCP 监听 socket;
* 与阶段 7/11/14 互斥开法见 ch395f_test.h 里本宏上方的说明。 */
net_layer_udp_bench_test_main();
#else
net_layer_test_main();
#endif
+383
View File
@@ -986,3 +986,386 @@ void net_layer_test_main(void)
osDelay(10);
}
}
/* ===================================================================
* Phase 16 — BSD UDP 数据面真机回归(2026-11-03 UDP 队列化改造,PR #74)
*
* 测试目的:把 net_socket.c/net_udp.c 这条「UDP 收发并入 netMsgQueue、在
* netTask 内串行执行」的新链路放上真芯片跑一遍,逐条对上
* docs/测试说明/单元测试/主机单测覆盖率口径.md 第 4 轮登记的
* 「台架必验」五项:
* 1602 → ① server 形态首包(bind 时 DesIP 已写在全F)+ ⑤ 顺序
* 1603 → TRAP-NET-22 缺陷本体:48 字节小应答在 client 形态下
* 必须一字节不少(旧实现恒剥 8 字节 → 这里会变成 40)
* 1604 → ② sendto 切形态后能否真恢复收其它源地址
* 1605 → ③ 截断包排空后下一个包是否完整
* 1607 → ④ 挂起 recv 等待期间的 IWDG 余量(无 [ERR][WD] task stall)
* 1608 → ⑤ 重新 open 后第一个包就要能收到
* 1609 → 分块边界(发送缓冲 1KB)与 1472 字节满 MTU 数据报
*
* 板端角色:UDP **服务端**(先 bind,由收到的包学出对端地址再回显)。
* 这样 PC 侧不需要固定 IP——台架机器是 DHCP,与 net_test_task.h 里
* TEST_PC_PEER_IP 的 192.168.31.2 并不一致,写死会让本阶段只能在那台机器上跑。
*
* PC 端:python test/host/udp_queue_bench.py --board <板 IP>
* 两侧的步骤表(tag/长度/端口/源套接字)必须一一对应,改一边就改另一边。
*
* 缓冲:本阶段自带 1600 B 静态缓冲(>1472 上限,≤2KB 约定),不复用 32KB 的
* s_rx_buf——阶段 11 那条「产品 RAM 余量放不下 32KB」的门控说明同样适用于此。
* =================================================================== */
#ifdef ENABLE_PHASE16_TESTS
#define UDP_BENCH_PORT 6001U /* 阶段前半段的服务端端口 */
#define UDP_BENCH_PORT2 6002U /* 关闭重开后换的端口(证明首包不丢) */
#define UDP_BENCH_BUF_SIZE 1600 /* 单包上限 1472 + 余量 */
#define UDP_BENCH_WAIT_MS 25000U /* 每步等 PC 的预算:PC 缺席时报 FAIL 而不是挂死 */
#define UDP_BENCH_QUICK_MS 1000U /* 超时判据用的短预算 */
#define UDP_BENCH_LATE_BUDGET 30000U /* 迟发包那一步的等待预算(比 PC 迟发时刻宽得多) */
#define UDP_BENCH_LATE_MIN_MS 7000U /* 迟发包实测等待的下界 */
#define UDP_BENCH_LATE_MAX_MS 25000U /* 迟发包实测等待的上界 */
#define UDP_BENCH_STALE_MAX 8U /* 单步最多丢掉几个旧步数据报(步序自愈的上限) */
static uint8_t s_udp_buf[UDP_BENCH_BUF_SIZE];
/*
* 函数功能:填充测试载荷(自校验模式,PC 端同式)
* 入口参数:p - 缓冲区 uint8_t * n - 长度 int tag - 步号(首字节)
* 出口参数:p - p[0]=tag,p[i]=(i*7+0x11)&0xFF (i>=1)
* 返回值:无
* 限定条件:n>=0
* 函数说明:模式与步号一起写,接收方只凭收到的字节就能判断
* 「是不是这一步的包」以及「有没有被剥头/错位」——
* 少 8 字节会让 p[1] 不再是 0x18,错位一眼可见。
*/
static void udp_bench_fill(uint8_t *p, int n, uint8_t tag)
{
int i;
if (n <= 0) {
return;
}
p[0] = tag;
for (i = 1; i < n; i++) {
p[i] = (uint8_t)(((uint32_t)i * 7U + 0x11U) & 0xFFU);
}
}
/*
* 函数功能:校验载荷模式
* 入口参数:p - 缓冲区 const uint8_t * n - 长度 int tag - 期望步号
* 出口参数:无
* 返回值:0 = 完全一致;否则返回第一个不符的字节下标 + 1(0 号位错返回 1)
* 限定条件:n>=0
* 函数说明:返回值直接进日志,指到字节号就能区分「整体少 8 字节」
* 与「只有尾部缺」这两类完全不同的故障。
*/
static int udp_bench_match(const uint8_t *p, int n, uint8_t tag)
{
int i;
if (n <= 0) {
return 0;
}
if (p[0] != tag) {
return 1;
}
for (i = 1; i < n; i++) {
if (p[i] != (uint8_t)(((uint32_t)i * 7U + 0x11U) & 0xFFU)) {
return i + 1;
}
}
return 0;
}
/*
* 函数功能:等一个数据报并校验(步号 + 长度 + 模式 + 源地址元数据)
* 入口参数:fd - Socket int tag - 期望步号 uint8_t
* exp - 期望字节数(截断步骤填截断后的长度)int
* cap - 本步接收上限 int to_ms - 等待预算 uint32_t
* src - 源地址输出(可为 NULL) struct net_sockaddr_in *
* who - 用例号(进日志) const char *
* 出口参数:src - 学到的对端地址(回显要用)
* 返回值:实际收到的字节数(>=0);-1 = 失败(已打日志)
* 限定条件:已由调用方 bind 好;buf 用 s_udp_buf
* 函数说明:源地址元数据(addrlen/family)也在本函数判,因为「客户端形态
* 下源地址退化成垃圾」正是 TRAP-NET-22 的后半段现象,
* 只在 PC 侧判会让板端日志失去定位能力。
*/
static int udp_bench_expect(int fd, uint8_t tag, int exp, int cap, uint32_t to_ms,
struct net_sockaddr_in *src, const char *who)
{
struct net_sockaddr_in from;
int alen = (int)sizeof(from);
uint32_t t_start;
uint32_t left_ms;
int stale = 0;
int n;
int bad;
/*
* 函数说明(步序自愈):步号写在载荷第 0 字节,所以先认步号再认内容。比本步小的
* = 上一步超时后对端重发留下的旧包,丢掉继续等;比本步大的 = 对端抢跑(不该发生,
* 报出来好定位)。没有这层判断时,任何一步只要超时一次,后面每一步都会错位 25 秒
* ——2026-11-03 第一次上台架就是这样:1602 通了,1603 起整条梯子在 PC 侧 8 秒预算
* 里全灭,分不清是板子的问题还是纯粹的错位。只允许有限次丢弃,防止对端脚本写错时
* 把包无限吞掉。
*/
t_start = HAL_GetTick();
for (;;) {
if ((HAL_GetTick() - t_start) >= to_ms) {
/* to_ms==0 在 net_recvfrom_to 里是「非法参数」(0 = 无限等由另一个入口表达),
* 预算见底时不能再拿 0 去调它,否则报出来的是 INVAL 而不是超时。 */
DBG_ERROR("%s: budget %ums exhausted before tag 0x%02X (stale drops=%d)",
who, (unsigned)to_ms, (unsigned)tag, stale);
return -1;
}
left_ms = to_ms - (HAL_GetTick() - t_start);
(void)memset(&from, 0, sizeof(from));
alen = (int)sizeof(from);
n = net_recvfrom_to(fd, s_udp_buf, cap, left_ms,
(src != NULL) ? (struct net_sockaddr *)&from : NULL,
(src != NULL) ? &alen : NULL);
if (n < 0) {
DBG_ERROR("%s: recv tag 0x%02X -> %d (errno=%d)", who, (unsigned)tag, n, net_get_errno());
return -1;
}
if (n < 1 || s_udp_buf[0] != tag) {
if ((n >= 1) && (s_udp_buf[0] < tag) && ((unsigned)stale < UDP_BENCH_STALE_MAX)) {
stale++;
DBG_INFO("%s: drop stale tag 0x%02X (drop #%d), still waiting for 0x%02X",
who, (unsigned)s_udp_buf[0], stale, (unsigned)tag);
continue;
}
DBG_ERROR("%s: tag 0x%02X got 0x%02X (n=%d, stale drops=%d)",
who, (unsigned)tag, (n >= 1) ? (unsigned)s_udp_buf[0] : 0U, n, stale);
return -1;
}
if (n != exp) {
DBG_ERROR("%s: tag 0x%02X len=%d expect=%d", who, (unsigned)tag, n, exp);
return -1;
}
bad = udp_bench_match(s_udp_buf, n, tag);
if (bad != 0) {
DBG_ERROR("%s: tag 0x%02X pattern breaks at byte %d (got 0x%02X, want 0x%02X)",
who, (unsigned)tag, bad - 1, s_udp_buf[bad - 1],
(bad == 1) ? (unsigned)tag : (uint8_t)(((uint32_t)(bad - 1) * 7U + 0x11U) & 0xFFU));
return -1;
}
break;
}
if (src != NULL) {
TEST_CHECK(alen == (int)sizeof(struct net_sockaddr_in),
"%s: src addrlen=%d (expect %d)", who, alen, (int)sizeof(struct net_sockaddr_in));
TEST_CHECK(from.sin_family == NET_AF_INET, "%s: src family=%d (expect %d)",
who, (int)from.sin_family, (int)NET_AF_INET);
*src = from;
DBG_INFO("%s: tag 0x%02X %dB from port %u OK", who, (unsigned)tag, n,
(unsigned)net_ntohs(from.sin_port));
} else {
DBG_INFO("%s: tag 0x%02X %dB OK", who, (unsigned)tag, n);
}
return n;
}
/*
* 函数功能:把刚收到的数据报原样回显给对端
* 入口参数:fd - Socket int n - 长度 int
* dst - 目的地址;NULL = 走芯片 DesIP 记账(客户端形态)
* who - 用例号
* 出口参数:无
* 返回值:net_sendto 的返回值(= n 才算成功)
* 限定条件:s_udp_buf 里就是那 n 个字节
* 函数说明:dst 是否为 NULL 决定走「显式目的地址(会切形态)」还是
* 「已记账对端(不碰寄存器)」两条不同的发送路径,1603 特意用
* NULL 把后者也压上台架。
*/
static int udp_bench_echo(int fd, int n, const struct net_sockaddr_in *dst, const char *who)
{
int rc;
rc = net_sendto(fd, s_udp_buf, n, 0,
(dst != NULL) ? (const struct net_sockaddr *)dst : NULL,
(dst != NULL) ? (int)sizeof(struct net_sockaddr_in) : 0);
if (rc != n) {
DBG_ERROR("%s: echo %dB -> %d (errno=%d)", who, n, rc, net_get_errno());
}
return rc;
}
void net_layer_udp_bench_test_main(void)
{
struct net_sockaddr_in me;
struct net_sockaddr_in peer;
uint32_t gen0;
uint32_t t0;
uint32_t dt;
int fd;
int rc;
int n;
int alen;
int i;
/* 分块边界尺寸表:1024 = 发送缓冲整块,1025 = 刚跨块,1472 = 满 MTU 数据报 */
static const int s_sizes[5] = { 1, 20, 1024, 1025, 1472 };
DBG_INFO("=== CH395F Phase 16 Tests (BSD UDP Queue) ===");
DBG_INFO("PC peer: python test/host/udp_queue_bench.py --board <ip> --port %u",
(unsigned)UDP_BENCH_PORT);
/* ---------- TC-NET-1600:模式函数自检(先信校验收件,再信它判的包) ---------- */
DBG_INFO("--- TC-NET-1600: pattern self-check ---");
udp_bench_fill(s_udp_buf, 64, 0xAA);
TEST_CHECK(udp_bench_match(s_udp_buf, 64, 0xAA) == 0, "TC-NET-1600a: fill/match agree");
s_udp_buf[9] = (uint8_t)(s_udp_buf[9] ^ 0xFFU);
TEST_CHECK(udp_bench_match(s_udp_buf, 64, 0xAA) == 10, "TC-NET-1600b: matcher points at byte 9");
udp_bench_fill(s_udp_buf, 64, 0xAA);
s_udp_buf[0] = 0xBB;
TEST_CHECK(udp_bench_match(s_udp_buf, 64, 0xAA) == 1, "TC-NET-1600c: wrong tag caught at slot 1");
(void)memset(s_udp_buf, 0, UDP_BENCH_BUF_SIZE);
/* ---------- TC-NET-1601:socket + bind(服务端形态) ---------- */
DBG_INFO("--- TC-NET-1601: socket/bind -> server mode ---");
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
TEST_CHECK(fd >= 0, "TC-NET-1601a: udp socket fd=%d", fd);
if (fd < 0) {
TEST_REPORT("Phase 16");
return;
}
(void)memset(&me, 0, sizeof(me));
me.sin_family = NET_AF_INET;
me.sin_port = net_htons((uint16_t)UDP_BENCH_PORT);
me.sin_addr.s_addr = 0U;
rc = net_bind(fd, (const struct net_sockaddr *)&me, (int)sizeof(me));
TEST_CHECK(rc == 0, "TC-NET-1601b: bind :%u rc=%d", (unsigned)UDP_BENCH_PORT, rc);
TEST_CHECK(net_udp_get_mode(fd) == NET_UDP_MODE_SERVER,
"TC-NET-1601c: mode=SERVER after bind (%d)", net_udp_get_mode(fd));
if (rc != 0) {
net_close(fd);
TEST_REPORT("Phase 16");
return;
}
/* ---------- TC-NET-1602:服务端首包(必验①/⑤) ---------- */
DBG_INFO("--- TC-NET-1602: first datagram after bind (server mode) ---");
n = udp_bench_expect(fd, 0x01, 20, UDP_BENCH_BUF_SIZE, UDP_BENCH_WAIT_MS, &peer, "TC-NET-1602");
if (n < 0) {
DBG_ERROR("TC-NET-1602: peer absent? aborting phase 16");
net_close(fd);
TEST_REPORT("Phase 16");
return;
}
rc = udp_bench_echo(fd, n, &peer, "TC-NET-1602");
TEST_CHECK(rc == n, "TC-NET-1602d: echo %dB rc=%d", n, rc);
TEST_CHECK(net_udp_get_mode(fd) == NET_UDP_MODE_CLIENT,
"TC-NET-1602e: sendto switched mode to CLIENT (%d)", net_udp_get_mode(fd));
/* ---------- TC-NET-1603:48 字节小应答(TRAP-NET-22 本体) ---------- */
DBG_INFO("--- TC-NET-1603: 48-byte reply, no header strip in client mode ---");
n = udp_bench_expect(fd, 0x02, 48, UDP_BENCH_BUF_SIZE, UDP_BENCH_WAIT_MS, NULL, "TC-NET-1603");
TEST_CHECK(n == 48, "TC-NET-1603a: 48B intact in client mode (n=%d)", n);
if (n == 48) {
rc = udp_bench_echo(fd, n, NULL, "TC-NET-1603");
TEST_CHECK(rc == 48, "TC-NET-1603b: echo via booked peer (dest NULL) rc=%d", rc);
}
/* ---------- TC-NET-1604:切回服务端形态收另一个源端口(必验②) ---------- */
DBG_INFO("--- TC-NET-1604: set_peer(NULL) restores other-source receive ---");
rc = net_udp_set_peer(fd, NULL);
TEST_CHECK(rc == 0, "TC-NET-1604a: set_peer(NULL) rc=%d", rc);
TEST_CHECK(net_udp_get_mode(fd) == NET_UDP_MODE_SERVER,
"TC-NET-1604b: mode back to SERVER (%d)", net_udp_get_mode(fd));
n = udp_bench_expect(fd, 0x03, 20, UDP_BENCH_BUF_SIZE, UDP_BENCH_WAIT_MS, &peer, "TC-NET-1604");
TEST_CHECK(n == 20, "TC-NET-1604c: datagram from a NEW source port received (n=%d)", n);
if (n == 20) {
rc = udp_bench_echo(fd, n, &peer, "TC-NET-1604");
TEST_CHECK(rc == 20, "TC-NET-1604d: echo to the new source rc=%d", rc);
}
/* ---------- TC-NET-1605:截断 + 排空(必验③) ---------- */
DBG_INFO("--- TC-NET-1605: truncated datagram then a clean one ---");
n = udp_bench_expect(fd, 0x04, 64, 64, UDP_BENCH_WAIT_MS, &peer, "TC-NET-1605");
TEST_CHECK(n == 64, "TC-NET-1605a: 200B read as 64 (n=%d)", n);
if (n == 64) {
rc = udp_bench_echo(fd, n, &peer, "TC-NET-1605");
TEST_CHECK(rc == 64, "TC-NET-1605b: echo truncated 64B rc=%d", rc);
}
n = udp_bench_expect(fd, 0x05, 30, UDP_BENCH_BUF_SIZE, UDP_BENCH_WAIT_MS, &peer, "TC-NET-1605");
TEST_CHECK(n == 30, "TC-NET-1605c: next datagram clean, no leftovers (n=%d)", n);
if (n == 30) {
rc = udp_bench_echo(fd, n, &peer, "TC-NET-1605");
TEST_CHECK(rc == 30, "TC-NET-1605d: echo 30B rc=%d", rc);
}
/* ---------- TC-NET-1606:超时语义 -2 且不留脏槽 ---------- */
DBG_INFO("--- TC-NET-1606: recvfrom_to timeout returns -2 ---");
t0 = HAL_GetTick();
n = net_recvfrom_to(fd, s_udp_buf, UDP_BENCH_BUF_SIZE, UDP_BENCH_QUICK_MS, NULL, NULL);
dt = HAL_GetTick() - t0;
TEST_CHECK(n == -2, "TC-NET-1606a: timeout returns -2 not errno (n=%d)", n);
TEST_CHECK(dt >= UDP_BENCH_QUICK_MS && dt < UDP_BENCH_QUICK_MS + 3000U,
"TC-NET-1606b: waited %ums with a %ums budget", (unsigned)dt, (unsigned)UDP_BENCH_QUICK_MS);
/* ---------- TC-NET-1607:挂起等待被迟发包唤醒(必验④) ---------- */
DBG_INFO("--- TC-NET-1607: parked recv woken by a late datagram ---");
gen0 = net_get_generation();
t0 = HAL_GetTick();
alen = (int)sizeof(peer);
(void)memset(&peer, 0, sizeof(peer));
/* 用有界预算而不是无限等:等包机制两者同一条(登记挂起 + tick 结算),
* 但无限等在 PC 中途走开会把手工台架钉死在这里,判据却一分不多。 */
n = net_recvfrom_to(fd, s_udp_buf, UDP_BENCH_BUF_SIZE, UDP_BENCH_LATE_BUDGET,
(struct net_sockaddr *)&peer, &alen);
dt = HAL_GetTick() - t0;
TEST_CHECK(n == 24, "TC-NET-1607a: late datagram delivered (n=%d)", n);
TEST_CHECK(dt >= UDP_BENCH_LATE_MIN_MS && dt <= UDP_BENCH_LATE_MAX_MS,
"TC-NET-1607b: waited %ums (expect %u..%u) - IWDG margin lives in the boot log",
(unsigned)dt, (unsigned)UDP_BENCH_LATE_MIN_MS, (unsigned)UDP_BENCH_LATE_MAX_MS);
TEST_CHECK(gen0 == net_get_generation(), "TC-NET-1607c: no net rebuild while waiting (gen %lu -> %lu)",
(unsigned long)gen0, (unsigned long)net_get_generation());
if (n == 24) {
TEST_CHECK(udp_bench_match(s_udp_buf, n, 0x06) == 0, "TC-NET-1607d: payload pattern intact");
rc = udp_bench_echo(fd, n, &peer, "TC-NET-1607");
TEST_CHECK(rc == n, "TC-NET-1607e: echo late datagram rc=%d", rc);
}
/* ---------- TC-NET-1608:关闭重开后首包必达(必验⑤) ---------- */
DBG_INFO("--- TC-NET-1608: close/rebind on a new port, first packet lands ---");
net_close(fd);
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
TEST_CHECK(fd >= 0, "TC-NET-1608a: fresh udp socket fd=%d", fd);
if (fd >= 0) {
(void)memset(&me, 0, sizeof(me));
me.sin_family = NET_AF_INET;
me.sin_port = net_htons((uint16_t)UDP_BENCH_PORT2);
me.sin_addr.s_addr = 0U;
rc = net_bind(fd, (const struct net_sockaddr *)&me, (int)sizeof(me));
TEST_CHECK(rc == 0, "TC-NET-1608b: bind :%u rc=%d", (unsigned)UDP_BENCH_PORT2, rc);
TEST_CHECK(net_udp_get_mode(fd) == NET_UDP_MODE_SERVER,
"TC-NET-1608c: fresh socket starts in SERVER mode (%d)", net_udp_get_mode(fd));
n = udp_bench_expect(fd, 0x07, 20, UDP_BENCH_BUF_SIZE, UDP_BENCH_WAIT_MS, &peer, "TC-NET-1608");
TEST_CHECK(n == 20, "TC-NET-1608d: first packet on a just-opened socket (n=%d)", n);
if (n == 20) {
rc = udp_bench_echo(fd, n, &peer, "TC-NET-1608");
TEST_CHECK(rc == 20, "TC-NET-1608e: echo rc=%d", rc);
}
}
/* ---------- TC-NET-1609:分块边界与满 MTU 数据报 ---------- */
DBG_INFO("--- TC-NET-1609: chunk boundary sizes 1/20/1024/1025/1472 ---");
for (i = 0; i < 5; i++) {
uint8_t tag = (uint8_t)(0x08 + i);
int sz = s_sizes[i];
n = udp_bench_expect(fd, tag, sz, UDP_BENCH_BUF_SIZE, UDP_BENCH_WAIT_MS, &peer, "TC-NET-1609");
if (n != sz) {
TEST_CHECK(0, "TC-NET-1609%c: size %d received (n=%d)", (char)('a' + i), sz, n);
continue;
}
TEST_CHECK(1, "TC-NET-1609%c: size %d round trip intact", (char)('a' + i), sz);
rc = udp_bench_echo(fd, n, &peer, "TC-NET-1609");
TEST_CHECK(rc == n, "TC-NET-1609%c: echo %dB rc=%d", (char)('a' + i), n, rc);
}
net_close(fd);
TEST_REPORT("Phase 16");
}
#endif /* ENABLE_PHASE16_TESTS */
+6
View File
@@ -19,6 +19,12 @@ void net_layer_test_main(void);
* MCU 作为 TCP Client 主动连接 PC 回显服务器,覆盖 TC-NET-701/702/703。 */
void net_layer_client_test_main(void);
/* NET 层 BSD UDP 数据面回归入口(定义于 net_test_task.c,阶段 16)。
* 板端做 UDP 服务端(先 bind :6001,再从收到的包里学对端地址回显),
* PC 侧跑 test/host/udp_queue_bench.py 提供逐步激励;PC 缺席时每步
* 都有 25 s 预算,报 [FAIL] 而不是把 testTask 挂死。 */
void net_layer_udp_bench_test_main(void);
#ifdef __cplusplus
}
#endif
+202
View File
@@ -1055,6 +1055,205 @@ static void tc_udp20_rebind_settles_waiter(void)
reset_all();
}
/* ===== UDP-21 两条"错误不是超时"的出口:bind 满队列 + recvfrom_to 真失败 ===== */
static void tc_udp21_bind_and_recvfrom_to_exits(void)
{
struct net_sockaddr_in in;
const struct net_sockaddr *sa = (const struct net_sockaddr *)&in;
char buf[8];
int fd;
int raw_fd;
int chip_fd;
bring_up(); /* 调用方上下文:公开入口一律走队列 */
fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
(void)memset(&in, 0, sizeof(in));
in.sin_family = NET_AF_INET;
in.sin_port = net_htons(1234);
/* 队列被别的请求占满:bind 只能"排队等空位",等不到就 BUSY,
* 绝不能把 fd 留在半个打开状态(芯片侧一条命令都没发)。 */
ns_rtos_q_fill((int)NS_Q_CAP);
CHECK(net_bind(fd, sa, (int)sizeof(in)) == NET_ERR &&
net_get_errno() == NET_ERR_BUSY,
"UDP-21a bind on a saturated queue is BUSY, and the code is the caller's own -11 (not a bare -1 the caller cannot diagnose)");
CHECK(s_net_socks[fd].state != NET_SOCK_STATE_UDP_OPEN && g_chip.open_calls == 0,
"UDP-21b a refused bind opens nothing on the chip: the fd is still just created, not half-bound");
ns_rtos_q_flush();
/* 同一 fd 在队列腾空后必须能正常 bind:BUSY 是"这次没排上",不是给 fd 投毒 */
CHECK(net_bind(fd, sa, (int)sizeof(in)) == 0 &&
s_net_socks[fd].state == NET_SOCK_STATE_UDP_OPEN,
"UDP-21c the same fd binds fine once the queue drains (a BUSY refusal poisons nothing)");
/* net_recvfrom_to 的"真错误"出口:结果 <0 且不是超时 → 原样带精确码回调用方。
* 挑一个未 bind 的 DGRAM fd:芯片侧 Socket 还没开,执行体必返 NOTCONN。 */
raw_fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
CHECK(net_recvfrom_to(raw_fd, buf, (int)sizeof(buf), 50U, NULL, NULL) == NET_ERR &&
net_get_errno() == NET_ERR_NOTCONN,
"UDP-21d net_recvfrom_to maps an execution failure to the precise code (NOTCONN here), and does not disguise it as a timeout");
CHECK(net_udp_armed(raw_fd) == 0,
"UDP-21e2 the refused attempt hands the out-param slot back (an armed slot left behind by a refusal turns this fd permanently BUSY, and the error only surfaces on the *next* caller)");
CHECK(net_recvfrom_to(raw_fd, buf, (int)sizeof(buf), 50U, NULL, NULL) == NET_ERR &&
net_get_errno() == NET_ERR_NOTCONN,
"UDP-21e the same refusal repeats with the same code (BUSY here would mean the fd was poisoned by its own last failure)");
/* 正向:预算内已有数据 → 返回字节数,不走 -2 */
udp_script_server_datagram("time", 4, 123U, "10.20.30.40");
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == 4,
"UDP-21f sanity: the bound fd still delivers a queued datagram byte-for-byte");
/* 执行体在**执行阶段**失败(不是入队失败)时,精确码要能回到等待方:
* 真芯片 open 会回 ERR_BUSY,这条通道是整个"通知值即结果"设计的落点。
* 用一个新 fd,别拿已经 bind 成功的那个——那会看不出"失败没把 fd 变成已 bind"。 */
chip_fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
g_fake.open_rc[chip_fd] = CH395F_ERR_BUSY;
(void)memset(&in, 0, sizeof(in));
in.sin_family = NET_AF_INET;
in.sin_port = net_htons(4567);
CHECK(net_bind(chip_fd, sa, (int)sizeof(in)) == NET_ERR &&
net_get_errno() == NET_ERR_BUSY,
"UDP-21g a bind the chip refuses (open → ERR_BUSY) comes back through the notify channel as -1 + the precise BUSY, not a bare -1 and not a timeout");
CHECK(s_net_socks[chip_fd].state != NET_SOCK_STATE_UDP_OPEN,
"UDP-21h a failed bind does not leave the fd looking bound");
g_fake.open_rc[chip_fd] = CH395F_ERR_SUCCESS;
CHECK(net_bind(chip_fd, sa, (int)sizeof(in)) == 0 &&
s_net_socks[chip_fd].state == NET_SOCK_STATE_UDP_OPEN &&
s_net_socks[chip_fd].local_port == 4567U,
"UDP-21i the same fd binds normally once the chip stops refusing");
/* 队列里有"别人的失败"排在我这条之前:两条在同一轮被排空,
* 覆盖式通知让我拿到的是**自己那条**的回执(后写覆盖先写)。
* 既有 TC-NETSOCK-545 在 TCP 侧钉过同一形态,这里补 UDP bind 这一条。 */
{
net_msg_t stale;
int ghost = 6;
(void)memset(&stale, 0, sizeof(stale));
stale.type = NET_MSG_UDP_BIND;
stale.sockfd = ghost; /* 没 net_socket() 过的 fd:执行体必拒 */
stale.len = 1234U; /* bind 的本地端口走 len(主机序) */
stale.addr = in;
stale.addrlen = (int)sizeof(in);
stale.caller = (TaskHandle_t)&g_ns_task_caller;
CHECK(ns_rtos_q_put_raw(&stale) == 0, "UDP-21j the ghost bind request is queued");
(void)memset(&in, 0, sizeof(in));
in.sin_family = NET_AF_INET;
in.sin_port = net_htons(4567);
CHECK(net_bind(chip_fd, sa, (int)sizeof(in)) == 0,
"UDP-21k a stranger request settling first does not turn my own successful bind into an error (the completion that overwrites the notify value is mine)");
ns_rtos_q_flush();
}
reset_all();
}
/* ===== UDP-22 netTask 直调路径:set_peer 的三条出口(板级用例走这条) ===== */
static void tc_udp22_set_peer_direct_ctx(void)
{
struct net_sockaddr_in peer;
int fd;
int stream_fd;
int puts_before;
int set_ip_before;
bring_up_netctx(); /* 留在 netTask 上下文:owner → 直调,不经队列 */
fd = udp_bind(777);
puts_before = g_ns_put_calls;
(void)memset(&peer, 0, sizeof(peer));
peer.sin_family = NET_AF_INET;
peer.sin_port = net_htons(123);
peer.sin_addr.s_addr = net_inet_addr("119.28.220.214");
CHECK(net_udp_set_peer(fd, &peer) == 0,
"UDP-22a set_peer inside netTask runs directly (the board-test path) and succeeds");
CHECK(g_ns_put_calls == puts_before,
"UDP-22b the direct path does not enqueue a message (a self-addressed queue message would never be drained)");
CHECK(net_udp_get_mode(fd) == NET_UDP_MODE_CLIENT &&
g_fx.des_ip[fd][0] == 119U && g_fx.des_ip[fd][3] == 214U &&
g_fx.des_port[fd] == 123U && g_chip.open_calls == 1,
"UDP-22c direct set_peer writes DesIP/DesPort and flips to CLIENT without reopening the socket");
CHECK(net_udp_set_peer(fd, NULL) == 0 &&
net_udp_get_mode(fd) == NET_UDP_MODE_SERVER &&
g_fx.des_ip[fd][0] == 0xFFU && g_fx.des_port[fd] == 0U,
"UDP-22d NULL peer is the light way back to SERVER: registers restored, still no reopen");
/* 越界 / 未使用 / 面向流:三种拒绝都必须零芯片事务 */
set_ip_before = g_fxchip.set_des_ip_calls;
CHECK(net_udp_set_peer(-1, &peer) == NET_ERR && net_get_errno() == NET_ERR_BADF,
"UDP-22e negative fd is BADF on the direct path too");
CHECK(net_udp_set_peer(NET_MAX_SOCKETS, &peer) == NET_ERR &&
net_get_errno() == NET_ERR_BADF,
"UDP-22f fd >= NET_MAX_SOCKETS is BADF (the range guard is what keeps s_net_udp[] in bounds)");
CHECK(net_udp_set_peer(5, &peer) == NET_ERR && net_get_errno() == NET_ERR_BADF,
"UDP-22g an in-range but unused fd is BADF before anything is touched");
stream_fd = mk_sock(NET_SOCK_STREAM, NET_SOCK_STATE_ESTABLISHED);
CHECK(net_udp_set_peer(stream_fd, &peer) == NET_ERR &&
net_get_errno() == NET_ERR_INVAL,
"UDP-22h set_peer on a STREAM fd is refused as INVAL (the type check runs before the state check: a TCP socket has no UDP mode to switch at all)");
{
int unbound = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
CHECK(net_udp_set_peer(unbound, &peer) == NET_ERR &&
net_get_errno() == NET_ERR_NOTCONN,
"UDP-22h2 an unbound DGRAM fd is refused as NOTCONN on the direct path too (DesIP written before open gets reset by open)");
}
CHECK(g_fxchip.set_des_ip_calls == set_ip_before,
"UDP-22i every refusal above wrote zero chip registers (a refusal that already moved DesIP would leave the socket pointing at a peer nobody asked for)");
reset_all();
}
/* ===== UDP-23 netTask 直调收包正例 + 槽位被占的 BUSY 出口 ===== */
static void tc_udp23_recvfrom_direct_ctx(void)
{
struct net_sockaddr_in from;
struct net_sockaddr *sa = (struct net_sockaddr *)&from;
int alen = (int)sizeof(from);
char buf[16];
int fd;
bring_up_netctx();
fd = udp_bind(888);
/* 数据已在芯片缓冲里:直调路径当场读出并就地经槽位写回源地址 */
udp_script_server_datagram("ABCDEFGH", 8, 5353U, "10.1.2.3");
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, sa, &alen) == 8,
"UDP-23a netTask-context recvfrom reads the datagram on the spot (board tests call it this way)");
CHECK(memcmp(buf, "ABCDEFGH", 8) == 0,
"UDP-23b the 8-byte info header is stripped and the payload starts at byte 0");
CHECK(alen == (int)sizeof(from) && from.sin_addr.s_addr == net_inet_addr("10.1.2.3") &&
from.sin_port == net_htons(5353),
"UDP-23c the source address is written through the caller's pointer, on the direct path as well");
CHECK(net_udp_armed(fd) == 0,
"UDP-23d the slot is handed back by the execution body even on a synchronous hit");
/* 假件的脚本长度耗尽后会**重复最后一个值**(表达"一直有数据"的持续态),
* 这里显式宣布"没包了",否则下面两条会把上一个包再读一遍。 */
ch395f_fake_push_recv_len(0, 0U);
/* 槽位被别人占着:本 fd 上必须 BUSY,且不去碰芯片、也不许动别人的槽 */
CHECK(net_udp_outparam_arm(fd, NULL, NULL) == 0, "UDP-23e slot armed by somebody else");
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, sa, &alen) == NET_ERR &&
net_get_errno() == NET_ERR_BUSY,
"UDP-23f recvfrom while the out-param slot is taken is BUSY rather than writing through a pointer that belongs to another call (公开入口对外仍是 -1 + 精确码)");
CHECK(net_udp_armed(fd) != 0,
"UDP-23g the refusal does not steal the slot it did not own");
(void)net_udp_outparam_disarm(fd);
/* 直调 + 阻塞 + 无数据 = 自等,被守卫拒成 BUSY(tick 就在自己脚下跑,等谁都不会醒) */
CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, NULL, NULL) == NET_ERR &&
net_get_errno() == NET_ERR_BUSY,
"UDP-23h a blocking recvfrom executed inside netTask refuses to register a pending slot nobody can settle (self-wait guard)");
CHECK(net_udp_armed(fd) == 0 && net_udp_pending(fd) == 0,
"UDP-23i that refusal leaves neither an armed nor a pending slot behind");
reset_all();
}
/* ===================== 本分片入口 ===================== */
static void ns_run_s7(void)
@@ -1079,4 +1278,7 @@ static void ns_run_s7(void)
tc_udp18_pending_send_completes();
tc_udp19_tick_fallbacks();
tc_udp20_rebind_settles_waiter();
tc_udp21_bind_and_recvfrom_to_exits();
tc_udp22_set_peer_direct_ctx();
tc_udp23_recvfrom_direct_ctx();
}
+318
View File
@@ -0,0 +1,318 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""UDP 数据面队列化真机回归 —— 板端阶段 16 的 PC 对端。
用途:配合 test/board/net_test_task.c 的 net_layer_udp_bench_test_main()
(板端做 UDP 服务端,从收到的包里学对端地址再回显),把 PR #74「UDP 并入
netMsgQueue」这条新链路放上真芯片逐步激励。之所以板端做服务端:台架 PC 是
DHCP 地址,与 net_test_task.h 的 TEST_PC_PEER_IP 不一定一致,服务端在先就
不需要 PC 侧固定 IP。
跑法(板子烧好阶段 16 固件后):
python test/host/udp_queue_bench.py --board 192.168.31.100
# 可选:--first-wait 180 等板子重启;--late 12 调迟发时刻
判读:每步打印 [PASS]/[FAIL],末尾一行
=== UDP Bench PC Peer: N/N PASSED ===
任一步失败即非零退出。**板端的汇总行在串口日志里**(=== Phase 16: ... ===),
两侧都要看:板端能看见"收到多少字节、模式切没切对",本端只能看见
"回显是否完整、是不是一个数据报"。两端合起来才是完整判据。
与板端必须一致的约定(改一边就改另一边,见 net_test_task.c 的 Phase 16 头注释):
载荷 p[0]=tag,p[i]=(i*7+0x11)&0xFF;板端原样回显。
tag 次序:0x01(20B) 0x02(48B) 0x03(20B 换源端口) 0x04(200B 截断成 64)
0x05(30B 残包排空) —静默超时探针— 0x06(24B 迟发包) 0x07(20B 换端口 6002)
0x08/0x09/0x0A/0x0B/0x0C = 1/20/1024/1025/1472 字节往返。
"""
from __future__ import annotations
import argparse
import socket
import sys
import time
for _stream in (sys.stdout, sys.stderr):
if hasattr(_stream, "reconfigure"):
_stream.reconfigure(encoding="utf-8", errors="replace")
# 阶段 16 步骤表:tag -> (发往端口, 本次发送长度, 期望回显长度)
# 期望回显长度 != 发送长度 的那两行就是截断步。
STEPS = (
("TC-NET-1602", 0x01, 6001, 20, 20),
("TC-NET-1603", 0x02, 6001, 48, 48),
("TC-NET-1604", 0x03, 6001, 20, 20),
("TC-NET-1605a", 0x04, 6001, 200, 64),
("TC-NET-1605b", 0x05, 6001, 30, 30),
# TC-NET-1606 = 板端超时探针,本端不发数据(见 wait_quiet)
("TC-NET-1607", 0x06, 6001, 24, 24),
("TC-NET-1608", 0x07, 6002, 20, 20),
("TC-NET-1609a", 0x08, 6002, 1, 1),
("TC-NET-1609b", 0x09, 6002, 20, 20),
("TC-NET-1609c", 0x0A, 6002, 1024, 1024),
("TC-NET-1609d", 0x0B, 6002, 1025, 1025),
("TC-NET-1609e", 0x0C, 6002, 1472, 1472),
)
def payload(tag: int, n: int) -> bytes:
"""与板端 udp_bench_fill() 同式的自校验载荷。"""
if n <= 0:
return b""
out = bytearray(n)
out[0] = tag & 0xFF
for i in range(1, n):
out[i] = (i * 7 + 0x11) & 0xFF
return bytes(out)
def new_peer_socket() -> socket.socket:
"""建一条 UDP 源套接句;能关就关掉 Windows 的「ICMP 端口不可达回抛」。
真机第一次上台架(2026-11-03 20:54)就是栽在这里:板子还没把 UDP socket 打开时,
我们发过去的第一颗包换来一个 ICMP 端口不可达,Windows 把它记在套接句上,随后的
recvfrom 直接抛 `ConnectionResetError: [WinError 10054] 远程主机强迫关闭了一个
现有的连接`——对端脚本当场死在第一条用例,板子白等到 25 秒超时,两边看到的都是
「对端缺席」,分不清是谁的问题。
能关掉这个行为当然最好(SIO_UDP_CONNRESET=0),但实测本机 CPython 3.14.7 既没有
`socket.SIO_UDP_CONNRESET` 这个常量,也不接受把它的数值 0x9800000C 直接喂给 ioctl
(`ValueError: invalid ioctl command`)。所以**主力防线是 recv 侧把 WinError 10054
当成「这一轮没收到」继续重试**(见 _recv_after / _recv_maybe 的 except),这里的
ioctl 只是有常量时的锦上添花。判据由 udp_queue_bench_loopback.py 的冷端口探针守着。
"""
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
flag = getattr(socket, "SIO_UDP_CONNRESET", None)
if flag is not None:
try:
s.ioctl(flag, False)
except (OSError, ValueError):
pass
return s
class Peer:
def __init__(self, board: str, timeout: float, step_budget: float = 35.0) -> None:
self.board = board
self.timeout = timeout
# 单步总预算:必须盖过板端单步等待(UDP_BENCH_WAIT_MS = 25 s)再加余量,理由见
# exchange() 里的说明。--first-wait 只覆盖第一步(板子要启动+起监听)。
self.step_budget = step_budget
self.total = 0
self.failed = 0
self.ports = {}
self.t_start = time.time()
def stamp(self) -> str:
return "[peer {0:7.2f}s]".format(time.time() - self.t_start)
def log(self, msg: str) -> None:
print("{0} {1}".format(self.stamp(), msg), flush=True)
def note_port(self, sock: socket.socket, tag: int) -> None:
"""首次用到某条源套接句时把它的临时源端口打出来(真机日志要和这个对得上)。"""
key = id(sock)
if key in self.ports:
return
try:
self.ports[key] = sock.getsockname()[1]
self.log("TC 0x{0:02X} 走源端口 {1}".format(tag, self.ports[key]))
except OSError:
self.ports[key] = 0
def check(self, cond: bool, name: str) -> bool:
self.total += 1
self.log("[{0}] {1}".format("PASS" if cond else "FAIL", name))
if not cond:
self.failed += 1
return cond
def close(self) -> None:
pass
def exchange(self, sock: socket.socket, port: int, tag: int, send_len: int,
exp_len: int, first_wait: float = 0.0) -> bool:
"""一个数据报进、一个数据报出;重发只在"完全没收到"时发生。"""
dst = (self.board, port)
data_out = payload(tag, send_len)
want = payload(tag, exp_len)
# 先清掉上一步可能迟到的回显,避免把它当成本步的应答
self._drain(sock)
# 每一步都重发,且单步预算必须盖过板端单步等待(UDP_BENCH_WAIT_MS = 25 s)再加
# 余量:板端只要超时一次就会晚一步,本端若只等 8 秒,后面每一步都会踩进板子的
# 上一步预算里,整条梯子一起错位(2026-11-03 第一次上台架就是这样全灭)。
# 重发是安全的:板端按载荷第 0 字节的步号丢掉旧包(步序自愈),本端每步开头也
# 会先 drain 掉迟到的回显。
step_budget = first_wait if first_wait > 0 else self.step_budget
deadline = time.time() + step_budget
got = None
addr = None
while got is None:
sock.sendto(data_out, dst)
left = deadline - time.time()
if left <= 0:
return self.check(False, "TC 0x{0:02X} 端口 {1}: 板端 {2:.0f}s 内无回显"
.format(tag, port, step_budget))
attempt = 4.0 if first_wait > 0 else 6.0
try:
got, addr = self._recv_after(sock, min(attempt, left))
self.note_port(sock, tag)
except (socket.timeout, ConnectionResetError):
# ConnectionResetError = 对端端口当时没开换来的 ICMP 不可达(见
# new_peer_socket 的说明);这里和「这一轮没收到」同等对待,继续重试。
self.log("TC 0x{0:02X}: 无回显,重发(本步剩余 {1:.0f}s)".format(tag, left))
# 一个数据报进、一个数据报出——唯一例外是本步载荷超过芯片发送缓冲(1KB):
# 那是已登记的芯片限制(NET_SEND_CHUNK_MAX 本是 TCP 流的分块口径,UDP 上每块
# 各发一包,改造前后同一条路径)。>1KB 的步骤按「预期拆包」判,并把实测拆分打
# 出来:真机上哪天不拆了,这条一样会报——那就是芯片行为变了,值得知道。
chip_chunk = 1024
parts = [got]
if exp_len > chip_chunk:
while sum(len(x) for x in parts) < exp_len:
more = self._recv_maybe(sock, 1.0)
if more is None:
break
parts.append(more[0])
else:
again = self._recv_maybe(sock, 0.300)
if again is not None:
parts.append(again[0])
total = b"".join(parts)
ok_len = (len(total) == exp_len)
ok_pat = ok_len and (total == want)
self.check(ok_len, "TC 0x{0:02X} 端口 {1}: 回显 {2} 字节(期望 {3},收到 {4} 片)"
.format(tag, port, len(total), exp_len, len(parts)))
if not ok_len:
self.log(" 各片长度 = {0},首 16 字节 = {1}".format(
[len(x) for x in parts], total[:16].hex()))
self.check(ok_pat, "TC 0x{0:02X}: 载荷逐字节一致(模式 p[i]=(i*7+0x11)&0xFF)".format(tag))
if not ok_pat and ok_len:
bad = next((i for i in range(exp_len) if total[i] != want[i]), -1)
self.log(" 首个差异 @ 字节 {0}: 收到 0x{1:02X},应为 0x{2:02X}"
.format(bad, total[bad], want[bad]))
if exp_len > chip_chunk:
self.check(len(parts) > 1 and len(parts[0]) == chip_chunk,
"TC 0x{0:02X}: >1KB 回显按芯片发送缓冲在 1024 处拆包(实测 {1})"
.format(tag, [len(x) for x in parts]))
elif len(parts) > 1:
self.check(False, "TC 0x{0:02X}: 未超 1KB 却收到 {1} 个数据报 {2}"
" —— 发送路径的分块口径变了"
.format(tag, len(parts) - 1, [len(x) for x in parts[1:]]))
return ok_len and ok_pat and (exp_len > chip_chunk or len(parts) == 1)
def wait_quiet(self, sock: socket.socket, seconds: float, name: str) -> bool:
"""板端超时探针期间本端必须保持静默,且不能有过期包漂进来。"""
time.sleep(seconds)
stray = self._drain(sock)
return self.check(stray == 0, "{0}: 静默期无残留数据报(清掉 {1} 个)".format(name, stray))
# ---- 内部 ----
def _recv_after(self, sock: socket.socket, budget: float):
sock.settimeout(max(budget, 0.05))
data, addr = sock.recvfrom(4096)
return data, addr
def _recv_maybe(self, sock: socket.socket, budget: float):
try:
sock.settimeout(budget)
return sock.recvfrom(4096)
except (socket.timeout, ConnectionResetError):
return None
finally:
sock.settimeout(self.timeout)
def _drain(self, sock: socket.socket) -> int:
n = 0
while True:
r = self._recv_maybe(sock, 0.050)
if r is None:
return n
n += 1
def main() -> int:
ap = argparse.ArgumentParser(description="UDP 队列化真机回归 PC 对端(板端阶段 16)")
ap.add_argument("--board", required=True, help="板子 IP,例如 192.168.31.100")
ap.add_argument("--first-wait", type=float, default=120.0,
help="首步最长等待(覆盖板子烧录后重启+起监听),默认 120 秒")
ap.add_argument("--timeout", type=float, default=20.0, help="单步回显等待,默认 20 秒")
ap.add_argument("--probe-silence", type=float, default=2.0,
help="对应 TC-NET-1606 的静默时长(板端探针预算 1 秒)")
ap.add_argument("--late", type=float, default=12.0,
help="对应 TC-NET-1607 的迟发时长:板端在这一步无限等包")
ap.add_argument("--step-budget", type=float, default=35.0,
help="单步总预算(含重发),必须盖过板端 25 秒单步等待,默认 35 秒")
ap.add_argument("--port", type=int, default=0,
help="覆盖阶段 16 的首个监听端口(默认用板端 UDP_BENCH_PORT 6001)")
ap.add_argument("--port2", type=int, default=0,
help="覆盖重开 socket 后的第二个监听端口(默认用板端 UDP_BENCH_PORT2 6002)")
args = ap.parse_args()
# 板端 TC-NET-1607 把「等待时长」判据钉在 7000~25000 ms:本端迟发时刻必须落在窗内,
# 且必须先盖过板端 TC-NET-1606 那 1 秒超时探针,否则两步会互相污染。
if args.probe_silence <= 1.2:
print("--probe-silence 必须大于板端 1 秒超时探针(默认 2.0)", file=sys.stderr)
return 2
if not (7.0 < args.late <= 22.0):
print("--late 必须落在 (7, 22] 秒内,否则板端 7000~25000ms 判定窗会判失败", file=sys.stderr)
return 2
if args.step_budget <= 26.0:
# 只提醒不拦死:自检脚本用假板子跑,预算给得很小;真机台架则会看到这条。
print("[warn] --step-budget={0:.0f}s 小于板端单步等待 25s + 余量:"
"板端一旦超时一次,本端后续每一步都会踩进板子的上一步预算里(错位全灭)"
.format(args.step_budget), file=sys.stderr)
peer = Peer(args.board, args.timeout, args.step_budget)
# 三条独立源套接句 = 三个不同临时源端口,1604「换源端口」就是这么来的。
# 端口号不能在未 connect 时用 getsockname() 取(Windows 会 WinError 10022,
# 端口要到第一次 sendto 才分配),也不能 connect()——connect 会让内核丢掉
# 非对端的包,那正是这一步要验的东西。改成首次收包后再查,照样打得出来。
socks = {"A": new_peer_socket(),
"B": new_peer_socket(),
"C": new_peer_socket()}
peer.log("对端 = {0},三条源套接句 A/B/C 的临时源端口在首次收包后打印".format(args.board))
# 板端每一步用新源端口的条件:1604(换源端口验形态恢复)及其后
step_sock = {
"TC-NET-1602": "A", "TC-NET-1603": "A",
"TC-NET-1604": "B", "TC-NET-1605a": "B", "TC-NET-1605b": "B",
"TC-NET-1607": "B",
"TC-NET-1608": "C", "TC-NET-1609a": "C", "TC-NET-1609b": "C",
"TC-NET-1609c": "C", "TC-NET-1609d": "C", "TC-NET-1609e": "C",
}
# 端口覆盖:只在显式给了 --port/--port2 时替换,默认沿用板端编译期的 UDP_BENCH_PORT(_2)。
# 板端串口横幅会把自己的端口打出来,照抄那条命令时用它换掉非默认端口。
port_map = {}
if args.port:
port_map[6001] = args.port
if args.port2:
port_map[6002] = args.port2
try:
for name, tag, port, send_len, exp_len in STEPS:
s = socks[step_sock[name]]
port = port_map.get(port, port)
fw = args.first_wait if name == "TC-NET-1602" else 0.0
# 1607 之前先做 1606 的静默探针(板端自己判 -2,本端只需保持静默)
if name == "TC-NET-1607":
peer.wait_quiet(s, args.probe_silence, "TC-NET-1606")
left = max(args.late - args.probe_silence, 0.0)
peer.log("TC-NET-1607: 再静默 {0:.1f}s,等板子挂进无限 net_recvfrom".format(left))
time.sleep(left)
peer.exchange(s, port, tag, send_len, exp_len, fw)
except KeyboardInterrupt:
peer.log("用户中断")
finally:
for s in socks.values():
s.close()
print("=== UDP Bench PC Peer: {0}/{1} PASSED (failed={2}) ===".format(
peer.total - peer.failed, peer.total, peer.failed), flush=True)
return 0 if peer.failed == 0 else 1
if __name__ == "__main__":
sys.exit(main())
+143
View File
@@ -0,0 +1,143 @@
"""PC peer 自身的回路自检:不需要板子,用假板子把 udp_queue_bench.py 的步骤表跑一遍。
为什么要有这个文件:阶段 16 的真机台架是一次性的——板子通电、烧好台架镜像、PC 起对端,
对端脚本自己要是有 bug(取端口、步骤表、拆包预期),一趟台架时间就白费了。这里在
127.0.0.1 上放一个「按板端阶段 16 同样规矩说话」的假板子,把对端脚本的判定逻辑先验一遍。
假板子复刻的三条规矩(与 test/board/net_test_task.c 阶段 16 一致):
1. 收到的数据报原样回显给发送方(tag 字节不动,所以源地址/剥头判据留在板端断言);
2. tag 0x04 这一步板端故意只用 64 字节缓冲去读 200 字节的数据报,所以只回 64 字节;
3. 回显超过芯片发送缓冲(1KB)时按 1024 拆成多个数据报发出(NET_SEND_CHUNK_MAX)。
用法(在本仓库根目录):
python test/host/udp_queue_bench_loopback.py
退出码:0 = 对端脚本判定逻辑全绿;1 = 有问题,先别上台架。
"""
import os
import socket
import sys
import threading
import time
HERE = os.path.dirname(os.path.abspath(__file__))
if HERE not in sys.path:
sys.path.insert(0, HERE)
import udp_queue_bench as peer # noqa: E402
CHIP_SEND_BUF = 1024 # NET_SEND_CHUNK_MAX
BOARD_RX_CAP = 1600 # 板端 s_udp_buf 尺寸 UDP_BENCH_BUF_SIZE
TRUNCATE_TAG = 0x04 # 这一步板端只用 64 字节缓冲
TRUNCATE_CAP = 64
RUNOUT_S = 90.0
LISTEN_PORTS = (6001, 6002) # UDP_BENCH_PORT / UDP_BENCH_PORT2
def echo_one(sock, deadline, stop):
sock.settimeout(0.5)
while (not stop.is_set()) and (time.time() < deadline):
try:
data, addr = sock.recvfrom(4096)
except socket.timeout:
continue
except OSError:
return
if not data:
continue
tag = data[0]
cap = TRUNCATE_CAP if tag == TRUNCATE_TAG else BOARD_RX_CAP
echo = peer.payload(tag, min(len(data), cap))
print("[board] rx 0x{0:02X} n={1} on :{2} -> echo {3}B".format(
tag, len(data), sock.getsockname()[1], len(echo)), flush=True)
for off in range(0, len(echo), CHIP_SEND_BUF):
sock.sendto(echo[off:off + CHIP_SEND_BUF], addr)
def check_cold_port() -> bool:
"""对端脚本必须先扛住「板子还没把 socket 打开」这段时间——真机第一次上台架就死在这。
往一个确定没人监听的端口发一颗包,Windows 会收下 ICMP 端口不可达并记在套接句上,
下一次 recvfrom 就抛 WinError 10054。这里走**脚本真正在用的那条路**(Peer._recv_maybe),
要求它返回 None(= 这一轮没收到,继续重试),而不是把异常抛穿。裸套接句那一段只是
证明这台机器确实会回 ICMP——不然这个探针没有牙,别把它当唯一证据。
"""
dead = 6099 # 假板子只绑 6001/6002,这个端口确定空着
ok = False
p = peer.Peer("127.0.0.1", 1.0)
s = peer.new_peer_socket()
try:
s.settimeout(1.0)
s.sendto(b"\x00", ("127.0.0.1", dead))
try:
r = p._recv_maybe(s, 1.0)
if r is None:
ok = True
else:
print("[selftest] 冷端口探针:竟然收到了数据报,探针失效", flush=True)
except ConnectionResetError:
print("[selftest] 冷端口探针:Peer._recv_maybe 把 WinError 10054 抛穿了"
"(真机上对端会当场死在第一条用例)", flush=True)
finally:
s.close()
except OSError as exc:
print("[selftest] 冷端口探针建句/发送失败:{0}".format(exc), flush=True)
raw = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
raw.settimeout(1.0)
raw.sendto(b"\x00", ("127.0.0.1", dead))
try:
raw.recvfrom(64)
print("[selftest] 裸套接句也收到了数据报(探针无牙,仅记录)", flush=True)
except ConnectionResetError:
print("[selftest] 裸套接句抛 WinError 10054 —— 这台机器确实会踩,"
"上面那条判定有牙", flush=True)
except socket.timeout:
print("[selftest] 裸套接句超时(本机没回 ICMP,探针无牙,仅记录)", flush=True)
except OSError as exc:
print("[selftest] 裸套接句探针失败:{0}".format(exc), flush=True)
finally:
raw.close()
return ok
def main() -> int:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
if not check_cold_port():
print("=== UDP Peer Loopback Self-Test: FAIL(冷端口探针没过,先修 new_peer_socket)===",
flush=True)
return 1
deadline = time.time() + RUNOUT_S
stop = threading.Event()
for listen_port in LISTEN_PORTS:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.bind(("127.0.0.1", listen_port))
except OSError as exc:
print("无法绑定 127.0.0.1:{0}:{1}(端口被占用?先腾出来再跑)".format(listen_port, exc),
file=sys.stderr)
return 2
threading.Thread(target=echo_one, args=(s, deadline, stop), daemon=True).start()
time.sleep(0.3)
# --late 必须落在板端 7000~25000 ms 判定窗内;这里取 9 秒,比默认 12 秒省一点台架外时间
# --step-budget 8:假板子秒回,用不到真机那 35 秒预算;真机台架千万别照抄这个值
#(真机上必须盖过板端单步 25 秒等待,否则一次超时就整条梯子错位)。
sys.argv = ["udp_queue_bench.py", "--board", "127.0.0.1",
"--first-wait", "15", "--timeout", "6", "--step-budget", "8",
"--probe-silence", "2.0", "--late", "9.0"]
try:
rc = peer.main()
finally:
stop.set()
if rc == 0:
print("=== UDP Peer Loopback Self-Test: PASS(对端脚本判定逻辑可用,可上台架)===", flush=True)
else:
print("=== UDP Peer Loopback Self-Test: FAIL(先修对端脚本,别浪费台架时间)===", flush=True)
return rc
if __name__ == "__main__":
raise SystemExit(main())