@@ -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 */