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 轮
  「台架必验」指向本阶段的可执行形态,并写明尚未在真机上跑过。
This commit is contained in:
2026-11-03 20:18:24 +08:00
parent fff961d803
commit 8a81fa9e41
7 changed files with 669 additions and 1 deletions
+347
View File
@@ -986,3 +986,350 @@ 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 /* 迟发包实测等待的上界 */
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 */