test(net/udp): 阶段 16 UDP 数据面真机回归台架(板端 + PC peer),台账补实测数字(#73)
- 板端 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 轮 「台架必验」指向本阶段的可执行形态,并写明尚未在真机上跑过。
This commit is contained in:
@@ -725,6 +725,7 @@ uint16_t ch395f_link_change_count(void);
|
|||||||
- **补测阶段又抓到两条(同一批缺口分析顺手挖的)**:③ **槽指针必须在 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 只是归零**。
|
- **补测阶段又抓到两条(同一批缺口分析顺手挖的)**:③ **槽指针必须在 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 行)。
|
- **本轮覆盖率**(收尾那次全仓跑数,`--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 侧对照)。
|
- **台架必验(本轮无板子,主机测替代不了)**:① 两端真机互通,服务器形态收发 + 客户端形态往返,重点看 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 19:19 起断电(SWD `No target connected`、ping 不通、串口静默),而 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 段 + 新条)。
|
- 本轮改动面:`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 行)
|
### P3|**明确不做**(把理由写死,免得下次又被列回待办:29 个 `.c` = 4,814 行)
|
||||||
|
|||||||
@@ -1125,6 +1125,64 @@ 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. `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 面。
|
||||||
|
2. `/keil-build` 后烧录(`.dsh/skills/stlink-flash/scripts/stlink_flash.py flash`,`-Rst -Run` 必带)。
|
||||||
|
3. PC 先起 peer:`python test/host/udp_queue_bench.py --board 192.168.31.100`,再给板子上电/复位;
|
||||||
|
peer 首步每 4 s 重发,能盖住板子启动那段时间。
|
||||||
|
4. 判据是**两侧都绿**:板端串口 `=== Phase 16: N/N PASSED (failed=0) ===`,PC 端
|
||||||
|
`=== UDP Bench PC Peer: N/N PASSED (failed=0) ===`。PC 绿板红 = 板端断言没对上激励;
|
||||||
|
板端绿 PC 红 = 回显内容或分片不对。Windows 防火墙静默丢包会让首步一直"无回显"(§14 已记)。
|
||||||
|
5. 跑完把这四行宏恢复成产品态重新构建 + 烧录(`test-suite-switch` 钩子也会拦住带测试宏的提交)。
|
||||||
|
|
||||||
|
**限度与观测点**:
|
||||||
|
|
||||||
|
- `net_send_locked` 的分块上限 `NET_SEND_CHUNK_MAX = 1024` 是为 TCP 流设计的;UDP 上它会把一条
|
||||||
|
超过 1024 B 的 `sendto` 拆成多次 `WRITE_SEND_BUF`,芯片就发成**多个数据报**(UDP 没有可拼的流语义)。
|
||||||
|
TC-NET-1609 的 1025 / 1472 两条会在 PC 侧把这件事直接量出来(多收到一片就记 [FAIL] 并打印各片长度)。
|
||||||
|
产品侧结论:**UDP 单包保持在 1024 B 以内**(#57 的 SNTP 是 48 B,碰不到这条)。这不是本轮引入的——
|
||||||
|
改造前的直调 `net_sendto` 就是同一个分块循环,只是那时 UDP 根本没有多任务调用者。
|
||||||
|
- 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,176 /
|
||||||
|
RO 33,900 / RW 2,060 / ZI 151,596;RW_IRAM1 **96,232 B = 73.4%**(距 92% 红线余 24,354 B),
|
||||||
|
RW_CCM 57,376 B / 87.9% 与产品一致。台账数字以 `test/board/ch395f_test.h` 那段为准。
|
||||||
|
- **本阶段尚未在真机上跑过**:本轮台架的板子 19:19 起断电(SWD 报 `No target connected`、
|
||||||
|
ping 不通、串口静默),而 PR #74 的合并前提就是这条回归跑完拿到证据。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 13. Python 测试脚本参考
|
## 13. Python 测试脚本参考
|
||||||
|
|
||||||
| 命令 | 模式 | 说明 |
|
| 命令 | 模式 | 说明 |
|
||||||
@@ -1134,6 +1192,7 @@ WAIT_CLOSE 阶段已内置二段补发(只补一次),生产 net 层则靠
|
|||||||
| `tcp_file --ip ... --port 8080 --size 20480` | PC 作文件接收 | 测试大文件传输(阶段 7) |
|
| `tcp_file --ip ... --port 8080 --size 20480` | PC 作文件接收 | 测试大文件传输(阶段 7) |
|
||||||
| `udp_client --ip ... --port 60000` | PC 作 UDP Client | 测试 UDP Echo(阶段 3) |
|
| `udp_client --ip ... --port 60000` | PC 作 UDP Client | 测试 UDP Echo(阶段 3) |
|
||||||
| `udp_server --port 8082` | PC 作 UDP Server | 接收 CH395F UDP 数据 |
|
| `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) |
|
||||||
| `dhcp_server ...` | DHCP Server | 内置 DHCP(阶段 4,需管理员权限) |
|
| `dhcp_server ...` | DHCP Server | 内置 DHCP(阶段 4,需管理员权限) |
|
||||||
|
|
||||||
### 通用参数
|
### 通用参数
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
* Phase 12 | 硬件层 | 独立运行:SPI 字节命令 / DMA 时延基线(SPI_CMD/SPI_DMA,无需 PC)
|
* Phase 12 | 硬件层 | 独立运行:SPI 字节命令 / DMA 时延基线(SPI_CMD/SPI_DMA,无需 PC)
|
||||||
* Phase 13 | NET 层 | testTask:芯片 + 队列时延(CHUNK_WAIT~FRAME_RT/LOGTAX,需 PC peer,独立运行)
|
* 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 14 | NET 层 | testTask:生产路径时延(PROD_CYCLE~UI_LOOP + 模拟器 A/B,需 PC peer)〔M3 规划〕
|
||||||
|
* Phase 16 | NET 层 | testTask:BSD UDP 数据面真机回归(形态切换/剥头/截断排空/挂起唤醒/分块边界,需 PC peer,独立运行)
|
||||||
* 时延分段定义/判定基线/执行流程:docs/测试说明/专项验证/链路延迟测试说明.md
|
* 时延分段定义/判定基线/执行流程:docs/测试说明/专项验证/链路延迟测试说明.md
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -75,6 +76,10 @@ extern "C" {
|
|||||||
* 与 Phase 11 共存安全:peer 缺席时有 LAT_P14_TIMEOUT_MS 整体超时,自动 done,
|
* 与 Phase 11 共存安全:peer 缺席时有 LAT_P14_TIMEOUT_MS 整体超时,自动 done,
|
||||||
* 不永久挡 accept */
|
* 不永久挡 accept */
|
||||||
#define ENABLE_PHASE14_TESTS /* [网] 生产路径时延(PROD_CYCLE~UI_LOOP) */
|
#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 构建矩阵实测踩出来的,别改回去):
|
* 开关行注释规范(2026-10-25 构建矩阵实测踩出来的,别改回去):
|
||||||
@@ -106,6 +111,18 @@ extern "C" {
|
|||||||
* ch395f_test_task.o 数据段仍是 12,285 B)。上表 04 点那组的产品基线已前进,
|
* ch395f_test_task.o 数据段仍是 12,285 B)。上表 04 点那组的产品基线已前进,
|
||||||
* 作为当日历史记录保留、不追改(两组差值都在 +136~+152 B 量级,来自新增的
|
* 作为当日历史记录保留、不追改(两组差值都在 +136~+152 B 量级,来自新增的
|
||||||
* adc_cal 数据段;结论"最坏态仍余约 29 KB、不必扩 scatter"不变)。
|
* 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):
|
* 台架冒烟(2026-10-25,_wt 同树构建 → 主树烧录,boot log 20261025_050539):
|
||||||
* 镜像 ch395f_hw_nopc_v2.hex(戳 suite=CH395F dstr=off)单次开机跑完 1/12/15 三段,
|
* 镜像 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。
|
* 汇总行 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) || \
|
#if defined(ENABLE_PHASE6_TESTS) || defined(ENABLE_PHASE7_TESTS) || \
|
||||||
defined(ENABLE_PHASE8_TESTS) || defined(ENABLE_PHASE9_TESTS) || \
|
defined(ENABLE_PHASE8_TESTS) || defined(ENABLE_PHASE9_TESTS) || \
|
||||||
defined(ENABLE_PHASE10_TESTS) || defined(ENABLE_PHASE11_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
|
#define ENABLE_NET_LAYER_TESTS 1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -1757,6 +1757,10 @@ void ch395f_test_run(void)
|
|||||||
DBG_INFO("netTask ready");
|
DBG_INFO("netTask ready");
|
||||||
#if defined(ENABLE_PHASE7_TESTS)
|
#if defined(ENABLE_PHASE7_TESTS)
|
||||||
net_layer_client_test_main();
|
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
|
#else
|
||||||
net_layer_test_main();
|
net_layer_test_main();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -986,3 +986,350 @@ void net_layer_test_main(void)
|
|||||||
osDelay(10);
|
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 /* 迟发包实测等待的上界 */
|
||||||
|
|
||||||
|
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);
|
||||||
|
int n;
|
||||||
|
int bad;
|
||||||
|
|
||||||
|
(void)memset(&from, 0, sizeof(from));
|
||||||
|
n = net_recvfrom_to(fd, s_udp_buf, cap, to_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 != 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;
|
||||||
|
}
|
||||||
|
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 */
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ void net_layer_test_main(void);
|
|||||||
* MCU 作为 TCP Client 主动连接 PC 回显服务器,覆盖 TC-NET-701/702/703。 */
|
* MCU 作为 TCP Client 主动连接 PC 回显服务器,覆盖 TC-NET-701/702/703。 */
|
||||||
void net_layer_client_test_main(void);
|
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
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
#!/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)
|
||||||
|
|
||||||
|
|
||||||
|
class Peer:
|
||||||
|
def __init__(self, board: str, timeout: float) -> None:
|
||||||
|
self.board = board
|
||||||
|
self.timeout = timeout
|
||||||
|
self.total = 0
|
||||||
|
self.failed = 0
|
||||||
|
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 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)
|
||||||
|
|
||||||
|
deadline = time.time() + (first_wait if first_wait > 0 else self.timeout)
|
||||||
|
got = None
|
||||||
|
addr = None
|
||||||
|
while got is None:
|
||||||
|
sock.sendto(data_out, dst)
|
||||||
|
# 首步:板子可能还在启动/还没 bind,每 4 秒重发一次直到首步总预算用尽。
|
||||||
|
# 重发不会污染下一步——下一步开头会先 drain 掉多出来的那一份回显。
|
||||||
|
attempt = 4.0 if first_wait > 0 else self.timeout
|
||||||
|
left = deadline - time.time()
|
||||||
|
if left <= 0:
|
||||||
|
return self.check(False, "TC 0x{0:02X} 端口 {1}: 板端无回显(预算内)"
|
||||||
|
.format(tag, port))
|
||||||
|
try:
|
||||||
|
got, addr = self._recv_after(sock, min(attempt, left))
|
||||||
|
except socket.timeout:
|
||||||
|
if first_wait <= 0:
|
||||||
|
return self.check(False, "TC 0x{0:02X} 端口 {1}: 板端 {2:.0f}s 内无回显"
|
||||||
|
.format(tag, port, self.timeout))
|
||||||
|
self.log("TC 0x{0:02X}: 无回显,重发(首步剩余 {1:.0f}s)".format(tag, left))
|
||||||
|
# 一个数据报进必须只出一个数据报出:再等一小会儿看有没有第二片
|
||||||
|
again = self._recv_maybe(sock, 0.300)
|
||||||
|
extra = []
|
||||||
|
if again is not None:
|
||||||
|
extra.append(len(again[0]))
|
||||||
|
|
||||||
|
ok_len = (len(got) == exp_len)
|
||||||
|
ok_pat = ok_len and (got == want)
|
||||||
|
self.check(ok_len, "TC 0x{0:02X} 端口 {1}: 回显 {2} 字节(期望 {3})".format(tag, port, len(got), exp_len))
|
||||||
|
if not ok_len:
|
||||||
|
self.log(" 首 16 字节 = {0}".format(got[: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 got[i] != want[i]), -1)
|
||||||
|
self.log(" 首个差异 @ 字节 {0}: 收到 0x{1:02X},应为 0x{2:02X}".format(bad, got[bad], want[bad]))
|
||||||
|
if extra:
|
||||||
|
self.check(False, "TC 0x{0:02X}: 一个请求收到 {1} 个额外数据报 {2}"
|
||||||
|
" —— 回显被分片了(芯片发送缓冲 = 1KB,>1KB 的分块会各发一包)"
|
||||||
|
.format(tag, len(extra), extra))
|
||||||
|
return ok_len and ok_pat and not extra
|
||||||
|
|
||||||
|
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:
|
||||||
|
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 的迟发时长:板端在这一步无限等包")
|
||||||
|
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
|
||||||
|
|
||||||
|
peer = Peer(args.board, args.timeout)
|
||||||
|
sock_a = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock_b = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
sock_c = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
socks = {"A": sock_a, "B": sock_b, "C": sock_c}
|
||||||
|
peer.log("对端 = {0},本机临时源端口 A={1} B={2} C={3}".format(
|
||||||
|
args.board, sock_a.getsockname()[1], sock_b.getsockname()[1], sock_c.getsockname()[1]))
|
||||||
|
|
||||||
|
# 板端每一步用新源端口的条件: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",
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
for name, tag, port, send_len, exp_len in STEPS:
|
||||||
|
s = socks[step_sock[name]]
|
||||||
|
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())
|
||||||
Reference in New Issue
Block a user