diff --git a/docs/测试说明/单元测试/主机单测覆盖率口径.md b/docs/测试说明/单元测试/主机单测覆盖率口径.md index 0be47d23..851d82ae 100644 --- a/docs/测试说明/单元测试/主机单测覆盖率口径.md +++ b/docs/测试说明/单元测试/主机单测覆盖率口径.md @@ -727,6 +727,7 @@ uint16_t ch395f_link_change_count(void); - **本轮覆盖率**(收尾那次全仓跑数,`--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 的合并前提仍是这条回归跑完整;跑完请把结果回填本条并把上面「台架必验」改写成实测结论。 +- **同一轮补的免对端那半(2026-11-04)**:第一次上台架就是被"没有对端"卡住的——peer 脚本被 Windows 的 ICMP 10054 打死、梯子错位,1601 之后一步都测不到,整趟证据归零。故阶段 16 加了 **TC-NET-1610**:17 条断言不需要 PC 发一个包,把「形态记账(新建 socket 与 bind 都读 SERVER、`sendto` 显式对端才切 CLIENT)」「参数与出参守卫(未 bind 报 -5、流式 API 读 `DGRAM` 报 -3、`to_ms == 0` 报 -3、`src_addr` 缺 `addrlen` 或容量只有 4 字节报 -3、空缓冲 `DONTWAIT` 报 -7)」「`net_recvfrom_to(2000)` 返回 **-2** 且实测睡眠落在 2~4.5 s(等待睡在任务通知上,不在 SPI 总线上自旋)」「`net_close` 后同一槽重新 bind 不被上一条命毒化」四类在真芯片上一次确认。执行位置在 1601 之前、编号留在段尾(编号是台账用的,不等于执行顺序)。代价只有 flash:Code +2,380 B、RO +3,108 B(全是断言字符串),`RW/ZI` 一字未增——它复用那块 1,600 B 收发缓冲。**ARMCC 已编过(bench 与产品两态都 0 error 0 warning,2026-11-04 01:06),但仍未在真机上跑过**:台架断线期间它一条也测不到,所以这里只算"编过",不算证据。 - 本轮改动面:`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 行) diff --git a/docs/测试说明/板级测试/CH395F_Test_Guide.md b/docs/测试说明/板级测试/CH395F_Test_Guide.md index 643fbd80..abf34868 100644 --- a/docs/测试说明/板级测试/CH395F_Test_Guide.md +++ b/docs/测试说明/板级测试/CH395F_Test_Guide.md @@ -1152,6 +1152,12 @@ WAIT_CLOSE 阶段已内置二段补发(只补一次),生产 net 层则靠 | 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 | 每条往返字节完全一致 | — | +| TC-NET-1610 | **免对端自检**(17 条,编号留在段尾、执行放在 1601 之前,是为了断线重跑时编号不漂) | PC 一个包都不发也能确认:新建 DGRAM socket 读到 SERVER;未 bind 时 `net_sendto` 报 -5、流式 `net_recv` 读 DGRAM 报 -3;经队列 `bind :6003` 成功且 mode = SERVER;空缓冲 `DONTWAIT` 报 -7;`net_recvfrom_to(2000)` 返回 **-2** 且实测睡眠 2~4.5 s(等待睡在任务通知上,没在 SPI 总线上自旋);`to_ms == 0` 报 -3;`src_addr` 缺 `addrlen`、容量只有 4 字节都报 -3;`set_peer(fd, NULL)` 在本就是 SERVER 时返回 0 且无副作用;`net_close` 后同一槽重新 bind :6003 不被上一条命毒化 | — | + +**没有对端也要有证据**:1601 之后每一步都在等 PC 的包——网线没插好、防火墙静默丢包、peer 脚本没起, +任何一条都会让整条阶梯停在 1602(2026-11-03 首跑就是这么停的)。TC-NET-1610 只依赖芯片能把 socket +open 起来,形态记账(SERVER/CLIENT)、参数与出参守卫、"阻塞收包已改成挂起登记"这三件事在完全不互通 +时也能一次确认;断线的时候,串口日志里的 `TC-NET-1610*` 就是判断"板子这半边是不是好的"的唯一依据。 **运行方法**: @@ -1181,10 +1187,11 @@ WAIT_CLOSE 阶段已内置二段补发(只补一次),生产 net 层则靠 它只证明**对端脚本**可用,不证明板端行为——真机那一趟仍然一条都不能省。 - 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 的代码在产品里被预处理干净。台账数字以 +- 本阶段构建实测(2026-11-04 01:06,含 TC-NET-1610 免对端自检,`MDK-ARM/STM32F407-Demo/STM32F407-Demo.map`):Code 259,768 / + RO 37,244 / RW 2,060 / ZI 151,596;RW_IRAM1 **96,232 B = 73.4%**(距 92% 红线余 24,354 B), + RW_CCM 57,376 B / 87.9% 与产品一致。免对端自检那 17 条只加 Code(+2,380) 与 RO(+3,108,全是断言 + 字符串),RW/ZI 一字未增——它复用 `s_udp_buf`,没带新缓冲。产品态(宏回默认后重新构建)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)与 diff --git a/test/board/ch395f_test.h b/test/board/ch395f_test.h index f8441d72..342870f6 100644 --- a/test/board/ch395f_test.h +++ b/test/board/ch395f_test.h @@ -118,6 +118,10 @@ extern "C" { * 是期间合入的东西,与阶段 16 无关,别把它算到本阶段头上。 * 配置(TEST_SUITE_CH395F + 只开阶段 16) Code ZI RW_IRAM1 距红线余量 * UDP 台架镜像 257,176 151,596 96,232 73.4% 24,354 B + * 2026-11-04 01:06 加了 TC-NET-1610 免对端自检(17 条)后重编:Code 259,768 / RO 37,244 / + * RW 2,060 / ZI 151,596 —— 较上一版台架镜像(含步序自愈那版:Code 257,388 / RO 34,136) + * 涨的 2,380 B Code 与 3,108 B RO 全是断言字符串与判定码, + * RW/ZI 一字未增(它复用那块 1,600 B 收发缓冲,没带新缓冲),RW_IRAM1 仍是 96,232 B / 73.4%。 * 阶段 16 自己的全部代价 = `net_test_task.o` 的 1,600 B .bss(一个 1600 B 收发缓冲, * 装得下 1472 B 上限的数据报)。产品构建(四行宏全恢复默认)RW+ZI 与改造前逐字节相同; * UDP 队列化本身在产品镜像里只多 `net_udp.o` 的 224 B .bss。跑法与判据见 diff --git a/test/board/net_test_task.c b/test/board/net_test_task.c index c2bfbb70..dd4c262a 100644 --- a/test/board/net_test_task.c +++ b/test/board/net_test_task.c @@ -1024,6 +1024,8 @@ void net_layer_test_main(void) #define UDP_BENCH_LATE_MIN_MS 7000U /* 迟发包实测等待的下界 */ #define UDP_BENCH_LATE_MAX_MS 25000U /* 迟发包实测等待的上界 */ #define UDP_BENCH_STALE_MAX 8U /* 单步最多丢掉几个旧步数据报(步序自愈的上限) */ +#define UDP_BENCH_PORT3 6003U /* 免对端自检(TC-NET-1610)独占的端口,与阶梯错开 */ +#define UDP_BENCH_PARK_MS 2000U /* 免对端自检的有界等待预算(真机 IWDG 窗口之下) */ static uint8_t s_udp_buf[UDP_BENCH_BUF_SIZE]; @@ -1192,6 +1194,129 @@ static int udp_bench_echo(int fd, int n, const struct net_sockaddr_in *dst, cons return rc; } +/* + * 函数功能:阶段 16 的**免对端**自检(TC-NET-1610)——PC 一个包都不发, + * 也能确认队列化后的 UDP 数据面在真芯片上通到哪一步 + * 入口参数:无 + * 出口参数:断言计数进 g_test_stats(走 TEST_CHECK 宏) + * 返回值:无 + * 限定条件:netTask 已就绪;临时占用一个 Socket,绑定 UDP_BENCH_PORT3,用完即关 + * 函数说明:为什么要这一段——TC-NET-1601 之后的每一步都在等 PC 的包,网线没插好、 + * 防火墙静默丢包、对端脚本没起,任何一条都会让整条阶梯停在 1602, + * 真机证据直接归零。本段只依赖芯片能把 Socket open 起来:形态记账、 + * 参数守卫、"阻塞收包已改成挂起登记"(凭 -2 超时与实测睡眠时长判定) + * 这三件事在没有对端时也能一次性确认。 + * 用例号排在 1609 之后、执行却放在阶梯之前,是为了断线重跑时 + * 编号不漂(编号是台账用的,不等于执行顺序)。 + */ +static void udp_bench_peer_free_check(void) +{ + struct net_sockaddr_in sa; + uint32_t t_start; + uint32_t dt; + int fd; + int rc; + int n; + int alen; + + DBG_INFO("--- TC-NET-1610: peer-free self-check (no PC traffic needed) ---"); + + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + TEST_CHECK(fd >= 0, "TC-NET-1610a: udp socket fd=%d", fd); + if (fd < 0) { + return; + } + TEST_CHECK(net_udp_get_mode(fd) == NET_UDP_MODE_SERVER, + "TC-NET-1610b: a fresh DGRAM socket already reads SERVER (%d)", + net_udp_get_mode(fd)); + + /* 未 bind 前一律拒:芯片侧 Socket 还没 open,改 DesIP/DesPort 没有落点 */ + udp_bench_fill(s_udp_buf, 8, 0x10U); + rc = net_sendto(fd, s_udp_buf, 8, 0, NULL, 0); + TEST_CHECK(rc == NET_ERR && net_get_errno() == NET_ERR_NOTCONN, + "TC-NET-1610c: sendto before bind is refused as errno -5 NOTCONN (rc=%d errno=%d)", + rc, net_get_errno()); + + /* 流式 API 读 DGRAM 会绕过剥头,等于静默吃掉 8 字节信息头:必须拒 */ + rc = net_recv(fd, s_udp_buf, 8, NET_MSG_DONTWAIT); + TEST_CHECK(rc == NET_ERR && net_get_errno() == NET_ERR_INVAL, + "TC-NET-1610d: net_recv on a DGRAM socket is refused as errno -3 INVAL (errno=%d)", + net_get_errno()); + + (void)memset(&sa, 0, sizeof(sa)); + sa.sin_family = NET_AF_INET; + sa.sin_port = net_htons((uint16_t)UDP_BENCH_PORT3); + sa.sin_addr.s_addr = 0U; + rc = net_bind(fd, (const struct net_sockaddr *)&sa, (int)sizeof(sa)); + TEST_CHECK(rc == 0, "TC-NET-1610e: bind :%u through the queue rc=%d", + (unsigned)UDP_BENCH_PORT3, rc); + if (rc != 0) { + (void)net_close(fd); + return; + } + TEST_CHECK(net_udp_get_mode(fd) == NET_UDP_MODE_SERVER, + "TC-NET-1610f: bind always lands in SERVER mode (%d)", net_udp_get_mode(fd)); + + /* 空缓冲 + DONTWAIT:当场回 -7,不登记挂起也不拖 netTask 下水 */ + n = net_recvfrom(fd, s_udp_buf, UDP_BENCH_BUF_SIZE, NET_MSG_DONTWAIT, NULL, NULL); + TEST_CHECK(n == NET_ERR && net_get_errno() == NET_ERR_WOULDBLOCK, + "TC-NET-1610g: DONTWAIT on an empty chip buffer gives errno -7 WOULDBLOCK (n=%d errno=%d)", + n, net_get_errno()); + + /* 有界等待:-2 = 超时无数据(与"真失败 -1 + errno"两条出口分家); + * 睡眠时长落在预算附近,说明调用方睡在任务通知上,netTask 同时还能服务队列。 + * 预算取 2 s:在产品 IWDG(2.5 s)与 netTask 喂狗窗口之下, + * 万一"挂起"没生效、退回旧版那种自旋,这里会先被看门狗抓到。 */ + t_start = HAL_GetTick(); + n = net_recvfrom_to(fd, s_udp_buf, UDP_BENCH_BUF_SIZE, UDP_BENCH_PARK_MS, NULL, NULL); + dt = HAL_GetTick() - t_start; + TEST_CHECK(n == -2, "TC-NET-1610h: bounded park with no peer gives -2 timeout (n=%d)", n); + TEST_CHECK(dt >= UDP_BENCH_PARK_MS && dt <= (UDP_BENCH_PARK_MS * 2U + 500U), + "TC-NET-1610i: park slept %ums against a %ums budget (the waiter sleeps on a notification, not on the SPI bus)", + (unsigned)dt, (unsigned)UDP_BENCH_PARK_MS); + + /* to_ms==0 在本 API 里是"永远等"的编码,不许被当成"立即返回",也不许塞进有界等待 */ + rc = net_recvfrom_to(fd, s_udp_buf, 8, 0U, NULL, NULL); + TEST_CHECK(rc == NET_ERR && net_get_errno() == NET_ERR_INVAL, + "TC-NET-1610j: to_ms==0 refused as errno -3 INVAL, never mistaken for infinite (errno=%d)", + net_get_errno()); + + /* 出参元数据两条守卫:给了 src_addr 却不给 addrlen,或者容量不足,都会越界写调用方栈 */ + rc = net_recvfrom(fd, s_udp_buf, 8, NET_MSG_DONTWAIT, (struct net_sockaddr *)&sa, NULL); + TEST_CHECK(rc == NET_ERR && net_get_errno() == NET_ERR_INVAL, + "TC-NET-1610k: src_addr with a NULL addrlen is refused (errno=%d)", net_get_errno()); + alen = 4; + rc = net_recvfrom(fd, s_udp_buf, 8, NET_MSG_DONTWAIT, (struct net_sockaddr *)&sa, &alen); + TEST_CHECK(rc == NET_ERR && net_get_errno() == NET_ERR_INVAL, + "TC-NET-1610l: addrlen=4 (< sizeof net_sockaddr_in) is refused (errno=%d)", net_get_errno()); + + /* 轻量回服务器模式:此刻本来就是 SERVER,重复调必须无害且不许重开 Socket */ + rc = net_udp_set_peer(fd, NULL); + TEST_CHECK(rc == 0 && net_udp_get_mode(fd) == NET_UDP_MODE_SERVER, + "TC-NET-1610m: set_peer(NULL) on a SERVER socket is a no-op that returns 0 (rc=%d mode=%d)", + rc, net_udp_get_mode(fd)); + + /* 关闭 + 复用:槽与芯片状态不许带走(net_udp_settle_close / net_udp_clear 的必查面) */ + rc = net_close(fd); + TEST_CHECK(rc == 0, "TC-NET-1610n: close rc=%d", rc); + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + TEST_CHECK(fd >= 0, "TC-NET-1610o: a socket allocated after close fd=%d", fd); + if (fd < 0) { + return; + } + rc = net_bind(fd, (const struct net_sockaddr *)&sa, (int)sizeof(sa)); + TEST_CHECK(rc == 0, "TC-NET-1610p: re-bind :%u on the recycled slot rc=%d", + (unsigned)UDP_BENCH_PORT3, rc); + if (rc == 0) { + n = net_recvfrom(fd, s_udp_buf, UDP_BENCH_BUF_SIZE, NET_MSG_DONTWAIT, NULL, NULL); + TEST_CHECK(n == NET_ERR && net_get_errno() == NET_ERR_WOULDBLOCK, + "TC-NET-1610q: the recycled slot is not poisoned by the previous life (errno=%d)", + net_get_errno()); + } + (void)net_close(fd); + (void)memset(s_udp_buf, 0, UDP_BENCH_BUF_SIZE); +} + void net_layer_udp_bench_test_main(void) { struct net_sockaddr_in me; @@ -1222,6 +1347,9 @@ void net_layer_udp_bench_test_main(void) 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-1610:免对端自检(PC 不在也能交付的那一半证据) ---------- */ + udp_bench_peer_free_check(); + /* ---------- TC-NET-1601:socket + bind(服务端形态) ---------- */ DBG_INFO("--- TC-NET-1601: socket/bind -> server mode ---"); fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM);