From fff961d8033b057b4014a5880776945d2e9991f4 Mon Sep 17 00:00:00 2001 From: super_agent_bot Date: Tue, 3 Nov 2026 18:08:27 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(net/udp):=20UDP=20=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E9=9D=A2=E5=B9=B6=E5=85=A5=20netMsgQueue=20=E4=B8=B2=E8=A1=8C?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=EF=BC=88#73=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 前置来自 #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/任务规划/**。 --- Drivers/BSP/NET/net_pending.c | 31 +- Drivers/BSP/NET/net_pending.h | 13 +- Drivers/BSP/NET/net_socket.c | 821 +++++++---- Drivers/BSP/NET/net_socket.h | 205 ++- Drivers/BSP/NET/net_types.h | 29 + Drivers/BSP/NET/net_udp.c | 765 +++++++++++ Drivers/BSP/NET/net_udp.h | 190 +++ MDK-ARM/STM32F407-Demo.uvprojx | 5 + docs/内存管理/RAM使用分析与优化方案.md | 6 +- docs/内存管理/RAM预算台账.md | 6 +- docs/子系统设计/系统墙钟时间维护说明.md | 2 +- docs/测试说明/单元测试/主机单测覆盖率口径.md | 27 +- docs/网络/BSD_Socket_API_使用指南.md | 163 ++- docs/网络/BSD_Socket_API_函数调用关系.md | 83 +- docs/问题记录/CH395F_Trap_Records.md | 15 + scripts/check_registration.py | 1 + scripts/ram_baseline.tsv | 1 + scripts/test_coverage_baseline.json | 130 +- test/host/common/ch395f_fake.c | 49 +- test/host/common/ch395f_fake.h | 23 + test/host/net_socket/net_socket_test.c | 35 + test/host/net_socket/ns_peers.c | 15 + test/host/net_socket/ns_peers.h | 2 + test/host/net_socket/ns_s5_io.c | 42 +- test/host/net_socket/ns_s6_edges.c | 164 +-- test/host/net_socket/ns_s7_udp.c | 1284 ++++++++++++++++++ 26 files changed, 3550 insertions(+), 557 deletions(-) create mode 100644 Drivers/BSP/NET/net_udp.c create mode 100644 Drivers/BSP/NET/net_udp.h create mode 100644 test/host/net_socket/ns_s7_udp.c diff --git a/Drivers/BSP/NET/net_pending.c b/Drivers/BSP/NET/net_pending.c index 673ada68..694f03d7 100644 --- a/Drivers/BSP/NET/net_pending.c +++ b/Drivers/BSP/NET/net_pending.c @@ -518,13 +518,32 @@ int net_send_pending_register(int sockfd, const void *buf, int len, int sent, return NET_PENDING_REGISTERED; } +/* + * 函数功能:判"本 Socket 的发送会话是否已不可继续"(TCP/UDP 统一判据) + * 入口参数:p_sock - Socket 控制块指针 net_sock_t * + * 出口参数:无 + * 返回值:1 - 不可继续(应报 CONNRESET/NOTCONN);0 - 可继续 int + * 限定条件:p_sock 非空;任意上下文(纯读控制块) + * 函数说明:TCP 的门是 ESTABLISHED,UDP 的门是 UDP_OPEN(芯片侧由 net_bind 打开)。 + * 判据只写这一处:net_send_locked 的入口门与两处中途检查、 + * net_send_pending_tick 的 ① 都用它——两条发送路径各自复述一遍 + * "state != ESTABLISHED"正是 UDP 接入时最容易漏改的形态。 + */ +int net_send_state_down(const net_sock_t *p_sock) { + if (p_sock->type == NET_SOCK_DGRAM) { + return (p_sock->state != NET_SOCK_STATE_UDP_OPEN) ? 1 : 0; + } + return (p_sock->state != NET_SOCK_STATE_ESTABLISHED) ? 1 : 0; +} + /* * 函数功能:挂起 send 推进与完成检查(netTask 每轮 net_process_messages 调用一次,非阻塞) * 入口参数:p_socks - Socket 控制块表首指针(net_socket.c 的 s_net_socks) net_sock_t * * 出口参数:无 * 返回值:无 * 限定条件:netTask 上下文 - * 函数说明:1. 逐 fd 按序:①断连 → 以已发字节数结束(剩余字节不可能送达) + * 函数说明:1. 逐 fd 按序:①会话不可继续(TCP 断连 / UDP 芯片侧被关)→ 以已发字节数结束 + * (剩余字节不可能送达) * 2. ②推进:发送缓冲空闲则写下一块(每轮至多一块——写完即清 send_ready, * 下一块须等 SENBUF_FREE 中断重新置位,由 net_poll 刷新) * 3. ③全部写完 → 以总长度结束;④无进展超时 → 以已发字节数结束 @@ -553,13 +572,13 @@ void net_send_pending_tick(net_sock_t *p_socks) { continue; } - /* ① 断连:本会话已结束 → 剩余字节不可能送达,按已发字节数结束。 - * 判据 `state != ESTABLISHED`(同快速路径):state 离开 ESTABLISHED 后 - * 即便再回到 ESTABLISHED,也是**新会话**(relisten + 新 CONNECT)—— - * 旧会话的剩余字节绝不能投到新连接上。session_dead 一并覆盖 + /* ① 会话已不可继续 → 剩余字节不可能送达,按已发字节数结束。 + * 判据见 net_send_state_down(TCP:state 离开 ESTABLISHED 后即便再回到 + * ESTABLISHED 也是**新会话**,旧会话剩余字节绝不能投到新连接上; + * UDP:芯片侧 Socket 被关/复位即不可继续)。session_dead 一并覆盖 * (net_types.h:TIMEOUT/DISCONNECT 两条路径都同时置 CLOSED,此处为 * 与 recv 侧判据对齐的冗余保险) */ - if ((p_sock->state != NET_SOCK_STATE_ESTABLISHED) || (p_sock->session_dead != 0U)) { + if ((net_send_state_down(p_sock) != 0) || (p_sock->session_dead != 0U)) { DBG_ERROR("sock%d send pending aborted (%d/%d): session down", i, p->sent, p->len); (void)net_set_errno(NET_ERR_CONNRESET); diff --git a/Drivers/BSP/NET/net_pending.h b/Drivers/BSP/NET/net_pending.h index ffec901d..248d339d 100644 --- a/Drivers/BSP/NET/net_pending.h +++ b/Drivers/BSP/NET/net_pending.h @@ -163,7 +163,8 @@ int net_send_pending_register(int sockfd, const void *buf, int len, int sent, * 出口参数:无 * 返回值:无 * 限定条件:netTask 上下文 - * 函数说明:逐 fd 按序判定:①断连(state 非 ESTABLISHED / session_dead)→ 以已发 + * 函数说明:逐 fd 按序判定:①会话不可继续(net_send_state_down:TCP 看 + * ESTABLISHED / UDP 看 UDP_OPEN;session_dead 一并覆盖)→ 以已发 * 字节数结束;②推进——发送缓冲空闲则写下一块(每轮至多一块,写完即清 * send_ready,下一块等 SENBUF_FREE 由 net_poll 重新置位);③全部写完 → * 以总长度结束;④无进展超时 → 以已发字节数结束。结果均为**已发字节数** @@ -171,6 +172,16 @@ int net_send_pending_register(int sockfd, const void *buf, int len, int sent, */ void net_send_pending_tick(net_sock_t *p_socks); +/* + * 函数功能:判"本 Socket 的发送会话是否已不可继续"(TCP/UDP 统一判据) + * 入口参数:p_sock - Socket 控制块指针 net_sock_t * + * 返回值:1 - 不可继续;0 - 可继续 int + * 限定条件:p_sock 非空;纯读控制块,任意上下文 + * 函数说明:TCP 门 = ESTABLISHED,UDP 门 = UDP_OPEN。快速路径(net_send_locked) + * 与挂起路径(net_send_pending_tick)共用,判据只此一处。 + */ +int net_send_state_down(const net_sock_t *p_sock); + /* * 函数功能:清除本 fd 的挂起 send(不通知;"新 fd 不得继承"与 close 后的幂等清理) * 入口参数:sockfd - Socket 描述符 (0~7) int diff --git a/Drivers/BSP/NET/net_socket.c b/Drivers/BSP/NET/net_socket.c index 020fbc86..1c4d685d 100644 --- a/Drivers/BSP/NET/net_socket.c +++ b/Drivers/BSP/NET/net_socket.c @@ -21,6 +21,7 @@ #include "net_listener.h" #include "net_events.h" /* 事件处理 + 周期对账(真机专属,同目录内部头) */ #include "net_pending.h" +#include "net_udp.h" /* UDP 数据面执行器与挂起槽(真机专属,同目录内部头) */ #include "ch395f.h" #include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */ #include "net_latency_test.h" /* 延迟探针接口(chunk_wait 采样) */ @@ -112,17 +113,23 @@ static TaskHandle_t s_net_owner_task; * 出口参数:无 * 返回值:1 - 是本层执行任务(或未捕获,见下);0 - 明确不是(应拒绝直调芯片) * 限定条件:net_init 之后判据才有效 - * 函数说明:给 net_sendto / net_recvfrom 这类**绕过消息队列、在调用方上下文里 - * 直接 net_poll + 读写芯片缓冲**的接口把危险面结构性关掉:非执行任务 - * 调用会与本层的 net_poll 抢同一根 SPI2(TRAP-NET-14 的根因形态), - * 而 SPI 事务没有跨任务互斥(ch395f_spi_begin/end 只是片选与分频)。 - * 刻意**不做**成"排队改写":recvfrom 要把源地址交回调用方,按 AGENTS - * 不能塞进按值拷贝的 net_msg_t,真要串行化得设计 per-task 结果槽, - * 那是需要真机回归的独立工程(2026-10-24 定案:本轮先把危险面关掉,串行化留后续)。 + * 函数说明:判据服务于两类接口: + * 1. 已队列化的接口(含 net_sendto / net_recvfrom,2026-11-03 起): + * **执行任务上下文走直接执行分支**——本任务就是队列消费者,往自己 + * 的队列里投递要等自己回来泵,队列满时更是自等死锁;板级硬件测试 + * (testTask 里 net_init)因此保留"在自己任务里裸调"的用法。 + * 非执行任务上下文经 netMsgQueue 串行到本任务,SPI 不再被两个任务 + * 同时碰(TRAP-NET-14 的根因形态:ch395f_spi_begin/end 只是片选与 + * 分频,事务没有跨任务互斥)。 + * 2. 挂起登记的"执行者自等待"守卫:登记前用它确认完成动作有人做 + * (net_udp_recvfrom_exec),自等自通知 = 死锁。 + * 历史:2026-10-24 曾把 sendto/recvfrom 做成"非执行任务一律拒 + * (NET_ERR_NOT_IN_NETTASK)"以先关危险面;源地址回传所需的 per-fd + * 结果槽已于 2026-11-03 落在 net_udp.c,串行化改造完成后该拒绝分支删除。 * 未捕获(net_init 未跑)时返回 1:那时任何 socket 都不存在, * 调用只会先在参数检查上拿到 BADF,不构成绕过。 */ -static uint8_t net_ctx_is_owner_task(void) { +uint8_t net_ctx_is_owner_task(void) { if (s_net_owner_task == NULL) { return 1U; } @@ -130,6 +137,15 @@ static uint8_t net_ctx_is_owner_task(void) { return (xTaskGetCurrentTaskHandle() == s_net_owner_task) ? 1U : 0U; } +/* + * 函数功能:绑定消息队列句柄(队列由 CubeMX 创建,本模块只持有句柄) + * 入口参数:queue_id - MX_FREERTOS_Init() 里建好的 netMsgQueue 句柄 osMessageQueueId_t + * 出口参数:s_net_msg_queue 由 NULL 变为本模块唯一的投递目标 + * 返回值:0 - 绑定成功;-1 - 参数为空或已绑过(重复绑定一律拒) int + * 限定条件:osKernelInitialize() 之后、netTask 启动之前调用一次(见 net_socket.h 声明处) + * 函数说明:1. 已绑定后再调用不改写句柄:运行期换队列会让在途消息的回执无人认领 + * 2. 绑定前所有投递点判空后直接返回 NET_ERR,不阻塞调用方 + */ int net_msg_queue_adopt(osMessageQueueId_t queue_id) { if (queue_id == NULL) { return -1; @@ -235,6 +251,24 @@ int net_set_errno(int err) { return err; } +/* + * 函数功能:把"已映射过的 -1"还原成本任务的精确错误码(直接执行路径专用) + * 入口参数:mapped - 执行体返回的负值(通常是 NET_ERR_RETURN_1 之后的 -1) int + * 出口参数:无 + * 返回值:精确错误码(本任务槽里没有负值时原样返回 mapped) int + * 限定条件:仅在**与执行体同一任务上下文**(直接路径)调用 + * 函数说明:net_udp_*_exec 失败时按模块惯例 NET_ERR_RETURN_1(code) —— 返回 -1 + * 并把精确码记进本任务槽。公开入口还要再做一次 BSD 映射,若直接把 -1 + * 再喂给 NET_ERR_RETURN_1,精确码就被覆盖成 -1 了。队列路径没有这个问题: + * dispatch 已按同样方式从 netTask 的槽里取码搭通知值带回来。 + */ +static int net_err_restore(int mapped) { + /* 本任务最后一次失败的精确码 */ + int code = net_get_errno(); + + return (code < 0) ? code : mapped; +} + /* * 私有函数声明区 @@ -242,14 +276,18 @@ int net_set_errno(int err) { static int alloc_socket(void); static void parse_ip_string(const char *str, uint8_t *ip_arr); -static int net_send_locked(net_sock_t *p_sock, const void *buf, int len, - TaskHandle_t caller); +/* net_send_locked 已改为非 static:netTask 的 UDP 执行体要调它复用同一份 + * 分块/挂起 send 实现,声明见 net_socket.h 内部 API 段 */ static int net_recv_locked(net_sock_t *p_sock, void *buf, int len, int flags, uint32_t timeout_ms, TaskHandle_t caller); static int net_close_locked(net_sock_t *p_sock); static int net_drop_conn_locked(net_sock_t *p_sock); static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen); static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int addrlen); +/* UDP 收包共用实现(net_recvfrom / net_recvfrom_to 两个入口一条路) */ +static int net_recvfrom_impl(int sockfd, void *buf, int len, int flags, + struct net_sockaddr *src_addr, int *addrlen, + uint32_t to_ms); /* * 函数功能:判断 6 字节 MAC 是否全零 @@ -287,9 +325,10 @@ static bool s_mac_is_zero(const uint8_t *p_mac) { /* 网络重建前取消全部挂起请求并通知调用方(调用方阻塞等回执, * 取消不通知 = 永久挂死);必须早于控制块清零与芯片复位。 - * recv / send 两张表各自独立,须分别取消 */ + * recv / send / UDP recv 三张表各自独立,须分别取消 */ net_pending_cancel_all(NET_ERR_CONNRESET); net_send_pending_cancel_all(NET_ERR_CONNRESET); + net_udp_cancel_all(NET_ERR_CONNRESET); /* 清零 Socket 控制块 */ memset(s_net_socks, 0, sizeof(s_net_socks)); @@ -654,13 +693,16 @@ void net_process_messages(void) { return; } - /* 四个非阻塞自愈 tick 置于消息处理之前:断连后即使无应用消息,监听器也能自主恢复 */ + /* 五个非阻塞自愈/完成 tick 置于消息处理之前:断连后即使无应用消息,监听器 + * 也能自主恢复,挂起中的 recv/send/UDP recv 也各自有推进者 */ /* 监听重建唯一执行者(TRAP-NET-09 D10 节奏;消费各路径登记的请求) */ net_listener_tick(); /* 挂起 recv 完成检查:数据到达 / 到点 / 断连时通知调用方 */ net_pending_tick(s_net_socks); /* 挂起 send 推进与完成检查(SEND pending):断连 / 推进写块 / 写完 / 无进展超时 */ net_send_pending_tick(s_net_socks); + /* 挂起 UDP 收包完成检查(2026-11-03 队列化):数据到达 / 到点 / fd 失效 */ + net_udp_tick(s_net_socks); /* TRAP-NET-17 状态对账 + 监听重建合一(单一 100ms tick): * 判据读驱动缓存(net_poll 每轮全量刷新),①②⑤ 失同步 + ③④ 重建 + * U11 重同步全部在此(本层唯一周期重建入口,监听生命周期 P2 单一所有者) */ @@ -779,6 +821,45 @@ void net_process_messages(void) { } break; } + /* ===== UDP 数据面(2026-11-03 队列化改造)===== + * 芯片事务全部在 netTask 里执行;源地址出参不搭 net_msg_t(按值拷贝, + * AGENTS 禁止加"回传用"字段),而是经 net_udp.c 的 per-fd 槽写回调用方 + * 内存,通知值只带结果码。执行体定义见 net_udp.c。 */ + case NET_MSG_UDP_BIND: { + /* 打开芯片侧 Socket(恒服务器模式);端口 0 = 由执行体取动态游标 */ + msg.result = net_udp_bind_exec(msg.sockfd, net_ntohs(msg.addr.sin_port)); + break; + } + case NET_MSG_UDP_PEER: { + /* 切芯片模式:addrlen > 0 = 客户端模式对端(公开 API 仅在 peer 非 NULL + * 时填),addrlen == 0 即"回服务器模式"(msg.addr 全零,不参与判读) */ + msg.result = net_udp_set_peer_exec(msg.sockfd, + (msg.addrlen > 0) ? &msg.addr : NULL); + break; + } + case NET_MSG_UDP_SENDTO: { + /* 目的地址与 CONNECT 同一口径按值携带;addrlen == 0 = 不改 DesIP。 + * 之后与 TCP 共用 net_send_locked:超预算登记挂起 send,回执推迟 */ + msg.result = net_udp_sendto_exec(msg.sockfd, msg.buf, msg.len, + (msg.addrlen > 0) ? &msg.addr : NULL, + msg.caller); + if (msg.result == NET_PENDING_REGISTERED) { + /* 挂起已登记:不发即时回执,完成时才通知 */ + do_notify = 0; + } + break; + } + case NET_MSG_UDP_RECVFROM: { + /* 三条出口:数据已在 → 读出一个完整数据报(剥头 + 源地址按槽回填); + * DONTWAIT/非阻塞 → WOULDBLOCK;阻塞无数据 → 登记挂起(哨兵, + * 完成条件由 net_udp_tick 判定,netTask 零占用) */ + msg.result = net_udp_recvfrom_exec(msg.sockfd, msg.buf, msg.len, msg.flags, + msg.timeout_ms, msg.caller); + if (msg.result == NET_PENDING_REGISTERED) { + do_notify = 0; + } + break; + } /* 手动自愈 API 退役(docs/网络/网络自愈架构设计.md §4.2): * NET_MSG_RESYNC_LISTEN / NET_MSG_RELISTEN 两个 * 消息已删——RESYNC 语义并入对账⑤(net_reconcile_tick), @@ -919,6 +1000,8 @@ int net_socket(int domain, int type) { net_pending_clear(sockfd); /* 新 fd 不得继承上一次挂起的 send 请求 */ net_send_pending_clear(sockfd); + /* 新 fd 不得继承上一次挂起的 UDP 收包请求(含源地址出参槽,同一纪律) */ + net_udp_clear(sockfd); /* 返回描述符(控制块已就绪) */ return sockfd; @@ -933,8 +1016,17 @@ int net_socket(int domain, int type) { * 返回值:0 - 绑定成功 int * -1 - 绑定失败(参数无效、Socket 未使用等) int * 限定条件:Socket 已通过 net_socket() 创建且未被其他操作占用;net_init() 已调用 - * 函数说明:1. TCP Socket 仅记录本地端口,进入 BOUND 状态等待 listen - * 2. UDP Socket(SOCK_DGRAM)立即打开 CH395F Socket 并配置为 Server 模式(Socket 4~7 需显式分配收发缓冲区) + * 函数说明:1. TCP Socket 仅记录本地端口(纯簿记,调用方上下文即可完成), + * 进入 BOUND 状态等待 listen 打开芯片侧。 + * 2. UDP Socket(SOCK_DGRAM)需要**立刻打开 CH395F Socket**(无 listen 态), + * 故 2026-11-03 起与其余 socket 操作同一口径:非执行任务经 netMsgQueue + * 串行到 netTask 执行(芯片命令序列见 net_udp_bind_exec),执行任务 + * 上下文直接执行。端口 0 = 由 netTask 分配动态端口(取号游标只有一个)。 + * 3. UDP bind 恒进入**服务器模式**(DesIP=0xFFFFFFFF,收任意源 + 芯片加 + * 8 字节信息头):BSD 的 bind 只表达本地地址,不选对端;对端由 + * net_sendto(带目的地址) 或 net_udp_set_peer 指定并切到客户端模式。 + * 4. 同一 fd 二次 bind = 重开芯片侧(收发缓冲与端口全部重来,接收缓冲内 + * 未读数据作废),也是"从客户端模式回服务器模式"的完整形态。 */ int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) { /* Socket 控制块指针 */ @@ -963,75 +1055,83 @@ int net_bind(int sockfd, const struct net_sockaddr *addr, int addrlen) { /* 地址强转为 IPv4 结构体 */ p_addr_in = (const struct net_sockaddr_in *)addr; + /* UDP (SOCK_DGRAM):打开芯片侧 = 芯片事务,交 netTask 串行执行 */ + if (p_sock->type == NET_SOCK_DGRAM) { + /* 队列消息(按值拷贝入队;地址按 BSD 原样带着网络序端口走,与 CONNECT 同一口径) */ + net_msg_t msg; + /* 执行结果 */ + int result; + + /* 执行任务上下文:直接执行(板级用例路径) */ + if (net_ctx_is_owner_task() != 0U) { + int rc = net_udp_bind_exec(sockfd, net_ntohs(p_addr_in->sin_port)); + + return (rc != 0) ? NET_ERR_RETURN_1(net_err_restore(rc)) : 0; + } + + if (s_net_msg_queue == NULL) { + return NET_ERR; + } + + (void)memset(&msg, 0, sizeof(msg)); + msg.type = NET_MSG_UDP_BIND; + msg.sockfd = sockfd; + msg.addr = *p_addr_in; + msg.addrlen = (int)sizeof(struct net_sockaddr_in); + msg.caller = xTaskGetCurrentTaskHandle(); + + if (osMessageQueuePut(s_net_msg_queue, &msg, 0, osWaitForever) != osOK) { + return NET_ERR_RETURN_1(NET_ERR_BUSY); + } + + result = net_wait_result("ubind", sockfd); + if (result < 0) { + return NET_ERR_RETURN_1(result); + } + return 0; + } + + /* TCP:纯簿记,无芯片事务,留在调用方上下文 */ /* 设置本地端口(结构体中为网络序 → 转主机序) */ p_sock->local_port = net_ntohs(p_addr_in->sin_port); /* port=0 时自动分配动态端口(49152~65535) */ if (p_sock->local_port == 0) { - /* 取当前动态端口值 */ - p_sock->local_port = s_dynamic_port; - /* 推进动态端口,供下次分配 */ - s_dynamic_port++; - /* 回绕:越过 65535 后回到动态区起始 */ - if (s_dynamic_port == 0) { - s_dynamic_port = 49152; - } + p_sock->local_port = net_dynamic_port_next(); } - /* UDP (SOCK_DGRAM):直接在此打开 CH395F Socket(无需等 listen) */ - if (p_sock->type == NET_SOCK_DGRAM) { - /* open_socket 返回状态 */ - uint8_t status; - - /* net_init 已为全部 8 个 Socket 分配缓冲;此处仅对 Socket 4~7 兜底重设, - * 使用与 net_init 相同的布局(固定 6 块/Socket,与 MSS 解耦,见 net_init) */ - if (sockfd >= 4) { - /* 每 Socket 块数 = 接收 + 发送 */ - uint8_t blocks_per_socket = (uint8_t)(CH395F_RECV_BLOCKS + CH395F_SEND_BLOCKS); - /* 本 Socket 接收缓冲起始块 */ - uint8_t recv_start = (uint8_t)(sockfd * blocks_per_socket); - /* 发送缓冲起始块(接收块之后) */ - uint8_t send_start = (uint8_t)(recv_start + CH395F_RECV_BLOCKS); - /* (重)设置发送缓冲块 */ - ch395f_set_send_buf((uint8_t)sockfd, send_start, (uint8_t)CH395F_SEND_BLOCKS); - /* (重)设置接收缓冲块 */ - ch395f_set_recv_buf((uint8_t)sockfd, recv_start, (uint8_t)CH395F_RECV_BLOCKS); - } - - /* 设置协议类型为 UDP */ - ch395f_set_proto_type((uint8_t)sockfd, CH395F_PROTO_TYPE_UDP); - /* 设置本地(源)端口 */ - ch395f_set_sour_port((uint8_t)sockfd, p_sock->local_port); - - /* DesIP=0xFFFFFFFF:UDP Server 模式(接受任意来源) */ - { - /* 任意源地址(全 0xFF) */ - uint8_t broadcast[4] = {0xFF, 0xFF, 0xFF, 0xFF}; - /* 目的 IP 设为任意源 */ - ch395f_set_des_ip((uint8_t)sockfd, broadcast); - } - /* 目的端口 0(服务端不限定对端端口) */ - ch395f_set_des_port((uint8_t)sockfd, 0); - - /* 打开芯片侧 Socket */ - status = ch395f_open_socket((uint8_t)sockfd); - if (status != CH395F_ERR_SUCCESS) { - /* open 失败:留痕并返回 BUSY(芯片忙/复位冲突) */ - DBG_ERROR("sock%d open_socket FAIL: status=0x%02X (expected 0x00)", sockfd, status); - return NET_ERR_RETURN_1(NET_ERR_BUSY); - } - - /* UDP 打开即可用(无监听态) */ - p_sock->state = NET_SOCK_STATE_UDP_OPEN; - } else { - /* TCP 仅记录本地端口,待 listen 打开芯片侧 */ - p_sock->state = NET_SOCK_STATE_BOUND; - } + /* TCP 仅记录本地端口,待 listen 打开芯片侧 */ + p_sock->state = NET_SOCK_STATE_BOUND; /* 绑定成功 */ return 0; } +/* + * 函数功能:取下一个动态端口并推进游标(49152~65535 回绕) + * 入口参数:无 + * 出口参数:无 + * 返回值:分配到的端口(主机序) uint16_t + * 限定条件:单线程语义——TCP 绑定路径(调用方上下文)与 UDP 绑定执行体 + * (netTask)都经此取号;游标本就是本模块 static,不新增锁 + * 函数说明:原逻辑内联在 net_bind 的 port==0 分支;UDP 队列化后绑定动作挪进 + * netTask 执行,两处都要用同一游标,故抽成函数(同一事实只在一处)。 + * 与抽取前一致:**不做**"是否已被其他 Socket 占用"检查(8 条 fd 的 + * 并发绑定组合由调用方保证,登记簿无此项待办)。 + */ +uint16_t net_dynamic_port_next(void) { + /* 本次分配值 */ + uint16_t port = s_dynamic_port; + + /* 推进动态端口,供下次分配 */ + s_dynamic_port++; + /* 回绕:越过 65535 后回到动态区起始 */ + if (s_dynamic_port == 0) { + s_dynamic_port = 49152; + } + return port; +} + /* * 函数功能:驱动 sock_reset 原语的有界确定性排渣等待(排渣收口; * 替代原"close + 盲等 500ms"式忙等) @@ -1733,7 +1833,7 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a * 出口参数:无 * 返回值:实际发送字节数(>= 0);-1 表示参数无效或 Socket 未连接; * NET_PENDING_REGISTERED 表示已登记挂起 send(分派据此跳过即时通知) int - * 限定条件:p_sock 非空;Socket 处于 ESTABLISHED 状态;netTask 上下文 + * 限定条件:p_sock 非空;TCP 须 ESTABLISHED、UDP 须 UDP_OPEN(已 net_bind);netTask 上下文 * 函数说明:1. 按 NET_SEND_CHUNK_MAX(硬件发送缓冲上限,见 TRAP-NET-16)分块写入 CH395F 发送 FIFO * 2. 等待发送缓冲腾出采用**有界自旋**:单次等待 ≤ NET_SEND_SPIN_MS * (健康路径实测 219µs 中位,5 倍余量)→ 多块帧仍在一次分派内发完, @@ -1744,12 +1844,15 @@ static int net_connect_locked(int sockfd, const struct net_sockaddr *addr, int a * ⚠ 仅**写块前**的等待可挂起:末块写完后 sent == len,此时超预算必须 * 直接返回 sent 而非登记(登记函数按 sent >= len 拒收,会把完整发送 * 报成 -1;详见《Net层阻塞模型架构设计.md》 §5.5 末块边界) - * 4. 发送中途断连(状态非 ESTABLISHED)→ 立即失败并返回已发字节 + * 4. 发送中途"会话已不可继续"(TCP 断连 / UDP 芯片侧被关,判据见 + * net_send_state_down)→ 立即失败并返回已发字节 * (errno=CONNRESET,不再空等超时) * 5. 未写完即返回(含挂起完成)均为**短写**:调用方须按 ret != len 判定 + * 6. 2026-11-03 UDP 队列化:本函数由 static 提升为内部 API,UDP 发送执行体 + * 调它复用同一份分块/挂起逻辑;状态门按 type 分流,TCP 路径逐位不变。 */ -static int net_send_locked(net_sock_t *p_sock, const void *buf, int len, - TaskHandle_t caller) { +int net_send_locked(net_sock_t *p_sock, const void *buf, int len, + TaskHandle_t caller) { /* 累计实际发送字节数 */ int sent = 0; /* 当前等待阶段起始时刻(tick) */ @@ -1762,8 +1865,9 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len, return NET_ERR_RETURN_1(NET_ERR_INVAL); } - /* 未建立连接 → NOTCONN */ - if (p_sock->state != NET_SOCK_STATE_ESTABLISHED) { + /* 可发起发送的门:TCP = 已建立连接;UDP = 芯片侧已由 net_bind 打开 + * (UDP 无连接,"能不能发"的唯一前提就是芯片侧 Socket 已开) */ + if (net_send_state_down(p_sock) != 0) { return NET_ERR_RETURN_1(NET_ERR_NOTCONN); } @@ -1781,9 +1885,10 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len, /* 发送缓冲未空:等待腾出后再写下一块 */ if (!p_sock->send_ready) { - /* 发送中途断连(DISCONNECT/TIMEOUT 已由 net_poll 处理并置状态非 ESTABLISHED): - * 立即失败,不再空等满超时(旧行为:白等 1s 后报 TIMEDOUT) */ - if (p_sock->state != NET_SOCK_STATE_ESTABLISHED) { + /* 发送中途"会话已不可继续"(TCP:DISCONNECT/TIMEOUT 已由 net_poll 置状态 + * 非 ESTABLISHED;UDP:芯片侧被关/复位):立即失败,不再空等满超时 + * (旧行为:白等 1s 后报 TIMEDOUT)。判据与入口门同一处,见 net_send_state_down */ + if (net_send_state_down(p_sock) != 0) { (void)net_set_errno(NET_ERR_CONNRESET); break; } @@ -1827,8 +1932,8 @@ static int net_send_locked(net_sock_t *p_sock, const void *buf, int len, tick_start = HAL_GetTick(); /* 等待本块发完(SENBUF_FREE 中断重新置 send_ready) */ while (!p_sock->send_ready) { - /* 发送中途断连:立即失败(同外层,避免白等超时) */ - if (p_sock->state != NET_SOCK_STATE_ESTABLISHED) { + /* 会话已不可继续:立即失败(同外层,避免白等超时) */ + if (net_send_state_down(p_sock) != 0) { (void)net_set_errno(NET_ERR_CONNRESET); break; } @@ -2011,6 +2116,9 @@ static int net_close_locked(net_sock_t *p_sock) { net_pending_settle_close((int)(p_sock - s_net_socks), p_sock); /* 挂起 send 同样先结束(以已发字节数通知;未发部分放弃),再关芯片 Socket */ net_send_pending_cancel((int)(p_sock - s_net_socks), NET_ERR_CONNRESET); + /* 挂起 UDP 收包同理:芯片缓冲里已到的数据报先完整交付(含剥头与源地址), + * 无数据才按 CONNRESET 结束——关闭不得吞掉已到的一包 */ + net_udp_settle_close((int)(p_sock - s_net_socks), p_sock); /* 排渣 + 关闭 + 等 CLOSED 一体(见函数头注释 1); * 已 CLOSED 的 fd 只清本 Socket 残留事件(防下一连接误判"刚建立就断开") */ @@ -2032,6 +2140,8 @@ static int net_close_locked(net_sock_t *p_sock) { net_pending_clear(sockfd); /* 挂起 send 同样幂等清理(cancel 已在上方通知,此处仅保证状态归零) */ net_send_pending_clear(sockfd); + /* UDP 收包槽幂等清理(settle_close 已通知,此处只保证源地址出参槽不泄漏) */ + net_udp_clear(sockfd); /* 关闭成功 */ return 0; @@ -2225,6 +2335,13 @@ int net_recv(int sockfd, void *buf, int len, int flags) { return NET_ERR_RETURN_1(NET_ERR_BADF); } + /* UDP fd 拒绝面向流的 recv(net_recv_nb 同路):本接口按字节读芯片缓冲, + * 不剥服务器模式的 8 字节信息头,也不保证"一次一个数据报"——会把头当载荷 + * 交给调用方。数据报语义请用 net_recvfrom / net_recvfrom_to。 */ + if (s_net_socks[sockfd].type == NET_SOCK_DGRAM) { + return NET_ERR_RETURN_1(NET_ERR_INVAL); + } + /* 消息队列未创建(网络层未初始化)→ 通用错误 */ if (s_net_msg_queue == NULL) { return NET_ERR_RETURN_1(NET_ERR); @@ -2311,6 +2428,12 @@ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms) { return NET_ERR_RETURN_1(NET_ERR_BADF); } + /* UDP fd 拒绝面向流的带超时 recv:理由同 net_recv(本接口按字节读芯片缓冲、 + * 不剥信息头);数据报请用 net_recvfrom_to(同为 -2 = 超时的口径) */ + if (s_net_socks[sockfd].type == NET_SOCK_DGRAM) { + return NET_ERR_RETURN_1(NET_ERR_INVAL); + } + /* ---- 2. 运行环境检查:消息队列须已创建(网络层已初始化) ---- */ if (s_net_msg_queue == NULL) { /* 网络层未初始化 */ @@ -2499,161 +2622,44 @@ int net_drop_conn(int sockfd) { * 出口参数:无 * 返回值:实际发送字节数 int > 0 || -1 * -1 - 发送失败 int - * 限定条件:Socket 已创建且为 UDP 类型(SOCK_DGRAM);net_init() 已调用 - * 函数说明:1. UDP 数据报无需连接:目标地址随消息一并投递 - * 2. 与 net_send 同一消息通道,SPI 访问串行在 netTask + * 限定条件:Socket 已创建且为 UDP 类型(SOCK_DGRAM)**并已 net_bind**;net_init() 已调用 + * 函数说明:1. UDP 数据报无需连接:目标地址随消息按值投递(net_msg_t.addr, + * 与 NET_MSG_CONNECT 同一先例),不在 net_msg_t 上开任何回传字段。 + * 2. 与 net_send 同一消息通道,SPI 访问串行在 netTask(2026-11-03 队列化, + * 取代 2026-10-24「先把危险面关掉」的临时守卫 NET_ERR_NOT_IN_NETTASK)。 + * 执行任务(netTask,或硬件层的 testTask)上下文直接执行不投自己—— + * 板级用例仍可在自己任务里裸调本接口。 + * 3. 带 dest_addr 会把芯片切成 **UDP 客户端模式**(DesIP=具体 IP):此后芯片 + * 只收该 IP 的包、且不再附带 8 字节信息头。本层按 p_sock->udp_mode 记账, + * net_recvfrom 据此决定剥不剥头——改造前恒剥头,客户端模式下会吃掉 + * 载荷前 8 字节并回填一个假源地址(SNTP 这类"请求-应答"用法必踩)。 + * 4. 分块粒度、停滞超时、挂起 send 续传与 TCP 共用 net_send_locked, + * 不留第二份自旋。单包超过 NET_SEND_CHUNK_MAX(1024) 会被写成多个数据报 + * (改造前后一致);一个应用数据报请控制在 1024 B 内。 + * 5. dest_addr == NULL = 不改 DesIP,沿用上次的对端(与改造前一致)。 + * 6. flags 目前不被本层使用(与改造前一致,发送不接受 DONTWAIT 语义)。 + * 7. 直接路径(执行任务上下文)不做挂起续传:芯片发送缓冲超 2 ms 未腾出时 + * 返回 NET_ERR_BUSY,而不是像改造前那样自旋到 1000 ms 后短写。 */ int net_sendto(int sockfd, const void *buf, int len, int flags, const struct net_sockaddr *dest_addr, int addrlen) { - /* Socket 控制块指针 */ - net_sock_t *p_sock; - /* 目标地址(IPv4)强转 */ - const struct net_sockaddr_in *p_addr_in; - /* 累计实际发送字节数 */ - int sent = 0; - /* 当前等待阶段起始时刻(tick) */ - uint32_t tick_start; + /* 目标地址(IPv4)视图 */ + const struct net_sockaddr_in *p_addr_in = NULL; + /* 队列消息(按值拷贝入队) */ + net_msg_t msg; + /* 执行结果 */ + int result; - /* 参数检查 */ + (void)flags; + + /* 参数检查:本批检查全部留在守卫/队列之前,参数错误先报参数错误 */ if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { /* 描述符越界 → BADF */ return NET_ERR_RETURN_1(NET_ERR_BADF); } - /* 取控制块 */ - p_sock = &s_net_socks[sockfd]; /* 控制块未在用 → BADF */ - if (!p_sock->in_use) { - return NET_ERR_RETURN_1(NET_ERR_BADF); - } - - /* 缓冲区无效 → INVAL(本块必须留在守卫之前:参数错误先报参数错误) */ - if (buf == NULL || len <= 0) { - return NET_ERR_RETURN_1(NET_ERR_INVAL); - } - - /* netTask 上下文守卫:本接口在本任务上下文里直接 net_poll + 读写芯片缓冲, - * 非执行任务调用会与本层 net_poll 抢同一根 SPI2(TRAP-NET-14)。放在参数 - * 校验**之后**:越界/未在用/参数非法这些正常错误路径照旧先报,守卫不吞它们。 - * 判据与留痕见 net_ctx_is_owner_task()。 */ - if (net_ctx_is_owner_task() == 0U) { - DBG_ERROR("sendto from non-exec task: refused (SPI race, use netTask)"); - return NET_ERR_RETURN_1(NET_ERR_NOT_IN_NETTASK); - } - - /* UDP 模式:设置目标地址 */ - if (dest_addr != NULL && addrlen >= (int)sizeof(struct net_sockaddr_in)) { - /* 强转为 IPv4 地址结构体 */ - p_addr_in = (const struct net_sockaddr_in *)dest_addr; - - /* 转换 IP 为线序数组(同 TCP 客户端:set_des_ip 要 [0]=第一段)。 - * s_addr 的内存字节顺位即线序(2026-10-24 定案:全仓 s_addr 只此一族),按顺位取, - * 不再按数值右移取最高字节——理由与 net_connect_locked 内同一处相同 */ - /* 线序 IP 数组 */ - uint8_t ip_arr[4]; - /* 目的 IP(网络序 32 位) */ - uint32_t ip = p_addr_in->sin_addr.s_addr; - /* 字节顺位拷贝:ip_arr[i] = s_addr 的第 i 个内存字节 */ - (void)memcpy(ip_arr, &ip, sizeof(ip_arr)); - - /* 设置目的 IP */ - ch395f_set_des_ip(sockfd, ip_arr); - /* 设置目的端口(网络序 → 主机序) */ - ch395f_set_des_port(sockfd, net_ntohs(p_addr_in->sin_port)); - } - - /* 记录起始时刻(用于停滞超时判定) */ - tick_start = HAL_GetTick(); - - /* 分块发送循环 */ - while (sent < len) { - /* 剩余待发字节 */ - int chunk = len - sent; - /* 单块以硬件发送缓冲为上限(TRAP-NET-16) */ - if (chunk > NET_SEND_CHUNK_MAX) { - chunk = NET_SEND_CHUNK_MAX; - } - - /* 发送缓冲未空:等待腾出 */ - if (!p_sock->send_ready) { - /* 停滞超时 */ - if (NET_SEND_TIMEOUT_MS > 0 && - (HAL_GetTick() - tick_start) >= NET_SEND_TIMEOUT_MS) { - DBG_ERROR("sock%d send stall: send_ready not set (%d/%d)", sockfd, sent, len); - (void)net_set_errno(NET_ERR_TIMEDOUT); - break; - } - /* 轮询一次,下一轮再试 */ - net_poll(); - continue; - } - - /* 本块写入芯片发送 FIFO */ - ch395f_write_send_buf(sockfd, (uint8_t *)buf + sent, (uint16_t)chunk); - /* 标记发送缓冲占用中 */ - p_sock->send_ready = 0; - /* 累计发送偏移 */ - sent += chunk; - /* 累计发送字节统计 */ - p_sock->send_bytes += (uint32_t)chunk; - - /* 等待发送完成 */ - /* 重置本等待阶段计时 */ - tick_start = HAL_GetTick(); - /* 等待本块发完(SENBUF_FREE 中断重新置 send_ready) */ - while (!p_sock->send_ready) { - /* 轮询:处理 SEND_OK/SENBUF_FREE 中断 */ - net_poll(); - /* 等待超时:本块放弃(外层统一处理) */ - if (NET_SEND_TIMEOUT_MS > 0 && - (HAL_GetTick() - tick_start) >= NET_SEND_TIMEOUT_MS) { - break; - } - } - } - - /* 发送未完成 = 短写 */ - if (sent < len) { - /* 短写告警:调用方必须检查返回值。静默截断会让对端永久等待剩余字节 - * (2026-09-21 实证:UI 推流应答被截断 → 客户端反复判定"全程无字节"失联)。 */ - DBG_ERROR("sock%d short send %d/%d (errno=%d)", sockfd, sent, len, - s_net_errno_slots[net_errno_slot()].err); - } - /* 返回实际发送字节数(调用方必须检查) */ - return sent; -} -/* - * 函数功能:接收 UDP 数据报(含源地址) - * 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS - * buf - 接收缓冲区指针 void * - * len - 缓冲区大小 int > 0 - * flags - 标志位,通常为 0 int - * 出口参数:src_addr - 输出:发送方地址结构体指针 struct net_sockaddr *(可为 NULL) - * addrlen - 输入输出:源地址长度 int *(可为 NULL) - * 返回值:实际接收字节数 int >= 0 || -1 - * -1 - 接收失败 int - * 限定条件:Socket 已创建且为 UDP 类型(SOCK_DGRAM);net_init() 已调用;有数据可用 - * 函数说明:1. 轮询检查芯片接收缓冲(net_poll + get_recv_len),有数据即返回 - * 2. UDP 数据报前 8 字节为报文头(源端口+源 IP),据此填充 src_addr - * 3. 无数据时(与 net_recv 同构):DONTWAIT / 非阻塞属性 - * 立即返回 -1(WOULDBLOCK);阻塞模式**无限等待**(30s 默认预算已删; - * 本接口为轮询自旋实现且无生产调用者,受 IWDG 监控的任务勿用无限形态) - */ -int net_recvfrom(int sockfd, void *buf, int len, int flags, - struct net_sockaddr *src_addr, int *addrlen) { - /* Socket 控制块指针 */ - net_sock_t *p_sock; - - /* 参数检查 */ - if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { - /* 描述符越界 → BADF */ - return NET_ERR_RETURN_1(NET_ERR_BADF); - } - - /* 取控制块 */ - p_sock = &s_net_socks[sockfd]; - /* 控制块未在用 → BADF */ - if (!p_sock->in_use) { + if (!s_net_socks[sockfd].in_use) { return NET_ERR_RETURN_1(NET_ERR_BADF); } @@ -2662,79 +2668,306 @@ int net_recvfrom(int sockfd, void *buf, int len, int flags, return NET_ERR_RETURN_1(NET_ERR_INVAL); } - /* netTask 上下文守卫:本接口自旋 net_poll + 读芯片缓冲,同样只允许执行任务调; - * 位置与 net_sendto 同一原则(参数校验之后、任何芯片事务之前) */ - if (net_ctx_is_owner_task() == 0U) { - DBG_ERROR("recvfrom from non-exec task: refused (SPI race, use netTask)"); - return NET_ERR_RETURN_1(NET_ERR_NOT_IN_NETTASK); + /* 给了地址就必须给够长度:长度不足还被忽略,等于悄悄发到上一个对端 */ + if ((dest_addr != NULL) && (addrlen < (int)sizeof(struct net_sockaddr_in))) { + return NET_ERR_RETURN_1(NET_ERR_INVAL); + } + if (dest_addr != NULL) { + p_addr_in = (const struct net_sockaddr_in *)dest_addr; } - /* 轮询直到:有数据 / 非阻塞(阻塞 = 无限等待,无默认预算) */ - while (1) { - /* 轮询一次:更新状态机与可读标志 */ - net_poll(); + /* 执行任务上下文:直接执行(本任务就是队列消费者,自投递要等自己泵队列) */ + if (net_ctx_is_owner_task() != 0U) { + int rc = net_udp_sendto_exec(sockfd, buf, len, p_addr_in, + xTaskGetCurrentTaskHandle()); - /* 查询芯片侧可用字节数 */ - int recv_len = ch395f_get_recv_len(sockfd); - /* 有数据 */ - if (recv_len > 0) { - /* UDP Server 模式:前 8 字节为固定格式(IP + 端口 + 长度)*/ - /* UDP 数据报:解析 8 字节报文头 */ - if (p_sock->type == NET_SOCK_DGRAM && recv_len >= 8) { - /* 8 字节报文头 */ - uint8_t header[8]; - /* 读出报文头(剥头 = 消费芯片缓冲) */ - ch395f_read_recv_buf(sockfd, header, 8); - /* 数据长度 = 总长 - 头长 */ - recv_len -= 8; - - /* 填充来源地址 */ - if (src_addr != NULL &&addrlen != NULL && - *addrlen >= (int)sizeof(struct net_sockaddr_in)) { - /* 强转为 IPv4 地址结构体 */ - struct net_sockaddr_in *p_addr_in = (struct net_sockaddr_in *)src_addr; - /* 地址族 = IPv4 */ - p_addr_in->sin_family = NET_AF_INET; - /* CH395F UDP 头 [0-1] 数据长度(LE) [2-3] src_port(LE) [4-7] src_ip(§9.2.4 表) */ - /* 源端口(小端 2 字节 → 主机序 → 网络序) */ - p_addr_in->sin_port = net_htons((uint16_t)(header[2] | (header[3] << 8))); - /* 源 IP:芯片给的是线序字节([4]=第一段),按"首段落在最低 - * 字节"组装即得网络序整数——与本层 s_addr 唯一约定同族 - * (2026-10-24 定案:全仓 s_addr 只此一族),所以这里**不能再加一次换端**。 - * 源 IP(小端 4 字节组装为网络序) */ - p_addr_in->sin_addr.s_addr = ((uint32_t)header[4]) | - ((uint32_t)header[5] << 8) | - ((uint32_t)header[6] << 16) | - ((uint32_t)header[7] << 24); - /* 回填结构体长度 */ - *addrlen = sizeof(struct net_sockaddr_in); - } - } - - /* 读取数据 */ - if (recv_len > 0) { - /* 本次读取量 = min(可用, 缓冲容量) */ - int read_len = (recv_len > len) ? len : recv_len; - /* 读入调用方缓冲区 */ - ch395f_read_recv_buf(sockfd, (uint8_t *)buf, (uint16_t)read_len); - /* 累计接收字节统计 */ - p_sock->recv_bytes += (uint32_t)read_len; - /* 返回本次读出字节数 */ - return read_len; - } + /* 挂起登记在直接路径下必被"执行者自等待"守卫拒绝,这里只是把 + * 万一出现的 0x70000000 挡住,不让它当成字节数返回给调用方 */ + if (rc == NET_PENDING_REGISTERED) { + return NET_ERR_RETURN_1(NET_ERR_BUSY); } - - /* 非阻塞模式:NET_MSG_DONTWAIT 单次覆盖,或 socket blocking 属性为 0 - * (与 net_recv_locked 同判据):无数据立即返回 */ - if ((flags & NET_MSG_DONTWAIT) || (p_sock->blocking == 0U)) { - /* 置 WOULDBLOCK */ - return NET_ERR_RETURN_1(NET_ERR_WOULDBLOCK); + if (rc < 0) { + return NET_ERR_RETURN_1(net_err_restore(rc)); } - - /* 阻塞模式:无数据继续轮询等待(无限等待,无默认预算—— - * 旧 NET_RECV_TIMEOUT_MS 30s 兜底已删;唤醒源 = 数据,与 net_recv 的 - * 差异是本接口为自旋实现,勿在受 IWDG 监控任务中使用无限形态) */ + return rc; } + + /* 队列未绑定(net_init 前的早期上下文)→ 与其余队列化接口同一口径 */ + if (s_net_msg_queue == NULL) { + return NET_ERR; + } + + (void)memset(&msg, 0, sizeof(msg)); + msg.type = NET_MSG_UDP_SENDTO; + msg.sockfd = sockfd; + /* 载荷指针:调用方内存,须有效至本调用返回(与 net_send 同一契约) */ + msg.buf = (void *)buf; + msg.len = len; + if (p_addr_in != NULL) { + /* 目的地址按值随消息走(CONNECT 先例) */ + msg.addr = *p_addr_in; + msg.addrlen = (int)sizeof(struct net_sockaddr_in); + } + msg.caller = xTaskGetCurrentTaskHandle(); + + if (osMessageQueuePut(s_net_msg_queue, &msg, 0, osWaitForever) != osOK) { + return NET_ERR_RETURN_1(NET_ERR_BUSY); + } + + /* 睡在自己的任务通知上(与 net_send 同一等待原语),netTask 零占用 */ + result = net_wait_result("usnd", sockfd); + if (result < 0) { + return NET_ERR_RETURN_1(result); + } + /* 返回实际发送字节数(调用方必须检查是否短写) */ + return result; +} +/* + * 函数功能:接收 UDP 数据报(含源地址)——无限等待形态 + * 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS + * buf - 接收缓冲区指针 void * + * len - 缓冲区大小 int > 0 + * flags - NET_MSG_DONTWAIT = 本次非阻塞,其余按 0 处理 int + * 出口参数:src_addr - 输出:发送方地址结构体指针 struct net_sockaddr *(可为 NULL) + * addrlen - 输入输出:源地址长度 int *(可为 NULL) + * 返回值:实际接收字节数 int > 0 || -1 + * -1 - 接收失败(精确码见本任务 net_get_errno()) int + * 限定条件:Socket 已创建且为 UDP 类型(SOCK_DGRAM)**并已 net_bind**;net_init() 已调用 + * 函数说明:1. 一次调用交付**一个完整数据报**:服务器模式剥掉芯片的 8 字节信息头并 + * 据此填 src_addr;客户端模式整包都是载荷、src_addr 取本 Socket 的固定 + * 对端(芯片在该模式下不加头,见 net_types.h UDP 模式宏注释)。 + * 剥不剥头按 p_sock->udp_mode 判定,不再按"字节数 >= 8"猜——猜错即吃掉 + * 载荷前 8 字节(本接口改造前的缺陷,SNTP 应答 48 B 会只剩 40 B)。 + * 2. 载荷超出 len 时交付前 len 字节并把剩余部分**读空丢弃**(手册 §9.2.4 + * 要求服务器模式一次性读完;留半包会让下一个数据报从尾巴开始解析)。 + * 3. 无数据时(与 net_recv 同构):DONTWAIT / 非阻塞属性立即返回 -1 + * (WOULDBLOCK);阻塞模式睡在 netTask 上无限等待——**本接口不再自旋, + * 不占 IWDG 预算**(2026-11-03 队列化)。受 IWDG 监控的任务仍建议用 + * net_recvfrom_to 带显式预算。 + * 4. 任意任务可调用:非执行任务经 netMsgQueue 串行到 netTask;执行任务 + * (netTask / 硬件层 testTask)上下文直接执行(板级用例保留原用法)。 + */ +int net_recvfrom(int sockfd, void *buf, int len, int flags, + struct net_sockaddr *src_addr, int *addrlen) { + /* 收包结果(原始码,见 net_recvfrom_impl) */ + int rc = net_recvfrom_impl(sockfd, buf, len, flags, src_addr, addrlen, 0U); + + /* 无限预算不会走到 NET_ERR_TIMEDOUT;其余负值恢复为线程级精确码, + * 对外保持 BSD 语义——失败一律 -1 */ + if (rc < 0) { + return NET_ERR_RETURN_1(rc); + } + return rc; +} + +/* + * 函数功能:接收 UDP 数据报(含源地址),带显式等待预算 + * 入口参数:sockfd - Socket 描述符 int >= 0 && < NET_MAX_SOCKETS + * buf - 接收缓冲区指针 void * + * len - 缓冲区大小 int > 0 + * timeout_ms - 等待预算(毫秒,**> 0**;无限等待请用 net_recvfrom) uint32_t + * 出口参数:src_addr - 输出:发送方地址(可为 NULL) struct net_sockaddr * + * addrlen - 输入输出:源地址长度(可为 NULL) int * + * 返回值:实际接收字节数(> 0);-2 = 预算内无数据;-1 = 出错(精确码见本任务 errno) int + * 限定条件:同 net_recvfrom + * 函数说明:与 net_recv_to 同一口径——超时是**结果**不是错误:返回 -2 且不动 errno; + * 其余 -1 都带精确码。受 IWDG 监控的任务用本接口,预算写在自己账上。 + */ +int net_recvfrom_to(int sockfd, void *buf, int len, uint32_t timeout_ms, + struct net_sockaddr *src_addr, int *addrlen) { + int rc; + + /* 预算必须 > 0:0 在本层一律表示"无限等待",与本接口"有界等待"的存在理由 + * 相反(同 net_recv_to 的分片长度检查);无限形态请用 net_recvfrom */ + if (timeout_ms == 0U) { + return NET_ERR_RETURN_1(NET_ERR_INVAL); + } + + rc = net_recvfrom_impl(sockfd, buf, len, 0, src_addr, addrlen, timeout_ms); + + /* 到点 = 结果,不是错误:与 net_recv_to 一致映射为 -2,不占 errno */ + if (rc == NET_ERR_TIMEDOUT) { + return -2; + } + if (rc < 0) { + return NET_ERR_RETURN_1(rc); + } + return rc; +} + +/* + * 函数功能:切换/查询 UDP 芯片模式(不重开 Socket) + * 入口参数:sockfd - Socket 描述符 int + * peer - 非 NULL = 客户端模式对端;NULL = 回服务器模式 const struct net_sockaddr_in * + * 出口参数:无 + * 返回值:0 - 成功;-1 - 失败(精确码见本任务 net_get_errno()) int + * 限定条件:本 Socket 已 net_bind(state == UDP_OPEN) + * 函数说明:语义见 net_socket.h 同名声明。经 netMsgQueue 串行在 netTask。 + */ +int net_udp_set_peer(int sockfd, const struct net_sockaddr_in *peer) { + /* 队列消息(按值拷贝入队) */ + net_msg_t msg; + /* 执行结果 */ + int result; + + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { + return NET_ERR_RETURN_1(NET_ERR_BADF); + } + if (s_net_socks[sockfd].in_use == 0U) { + return NET_ERR_RETURN_1(NET_ERR_BADF); + } + + /* 执行任务上下文:直接执行 */ + if (net_ctx_is_owner_task() != 0U) { + int rc = net_udp_set_peer_exec(sockfd, peer); + + if (rc != 0) { + return NET_ERR_RETURN_1(net_err_restore(rc)); + } + return 0; + } + + if (s_net_msg_queue == NULL) { + return NET_ERR; + } + + (void)memset(&msg, 0, sizeof(msg)); + msg.type = NET_MSG_UDP_PEER; + msg.sockfd = sockfd; + if (peer != NULL) { + /* 对端地址按值随消息走;NULL = 回服务器模式,msg.addr 保持全零即可 */ + msg.addr = *peer; + msg.addrlen = (int)sizeof(struct net_sockaddr_in); + } + msg.caller = xTaskGetCurrentTaskHandle(); + + if (osMessageQueuePut(s_net_msg_queue, &msg, 0, osWaitForever) != osOK) { + return NET_ERR_RETURN_1(NET_ERR_BUSY); + } + + result = net_wait_result("upeer", sockfd); + if (result < 0) { + return NET_ERR_RETURN_1(result); + } + return 0; +} + +/* + * 函数功能:查本 Socket 当前的 UDP 芯片模式 + * 入口参数:sockfd - Socket 描述符 int + * 出口参数:无 + * 返回值:NET_UDP_MODE_SERVER / NET_UDP_MODE_CLIENT;-1 = 描述符非法 int + * 限定条件:无(纯读控制块,任意上下文) + * 函数说明:给调用方与自测用:sendto(带目的地址) 之后模式是 CLIENT,此时 + * recvfrom 不剥信息头;bind 之后是 SERVER。 + */ +int net_udp_get_mode(int sockfd) { + const net_sock_t *p_sock = net_get_sock(sockfd); + + if (p_sock == NULL) { + return NET_ERR_RETURN_1(NET_ERR_BADF); + } + if (p_sock->type != NET_SOCK_DGRAM) { + return NET_ERR_RETURN_1(NET_ERR_INVAL); + } + return (int)p_sock->udp_mode; +} + +/* + * 函数功能:UDP 收包内部实现(net_recvfrom / net_recvfrom_to 共用一条路) + * 入口参数:sockfd - Socket 描述符 int + * buf - 接收缓冲区 void * + * len - 缓冲区容量(> 0) int + * flags - NET_MSG_DONTWAIT = 本次非阻塞 int + * src_addr - 源地址出参(可为 NULL) struct net_sockaddr * + * addrlen - 源地址出参长度(可为 NULL) int * + * to_ms - 等待预算(0 = 无限等待) uint32_t + * 出口参数:见 net_recvfrom + * 返回值:字节数(> 0);<= 0 为**原始码**(NET_ERR_* 或 NET_ERR),errno 由公开入口写 int + * 限定条件:参数已由本函数校验 + * 函数说明:1. 源地址回传不占 net_msg_t 字段——先在本任务上下文把出参登记进 + * net_udp 的 per-fd 槽(AGENTS:net_msg_t 按值拷贝,禁止加回传字段), + * netTask 执行时按槽写回调用方内存,通知值只带结果码。 + * 同一 fd 上槽位互斥:并发收包在数据报语义下无法区分归属,直接 BUSY。 + * 2. 本函数不碰 errno:超时与错误都以原始码返回,由 net_recvfrom / + * net_recvfrom_to 各自决定"记精确码"还是"当结果返回"(-2 口径)。 + */ +static int net_recvfrom_impl(int sockfd, void *buf, int len, int flags, + struct net_sockaddr *src_addr, int *addrlen, + uint32_t to_ms) { + /* 队列消息(按值拷贝入队) */ + net_msg_t msg; + /* 执行结果 */ + int result; + + /* 参数检查(顺序与改造前一致:BADF → INVAL → 出参容量) */ + if (sockfd < 0 || sockfd >= NET_MAX_SOCKETS) { + return NET_ERR_BADF; + } + if (s_net_socks[sockfd].in_use == 0U) { + return NET_ERR_BADF; + } + if (buf == NULL || len <= 0) { + return NET_ERR_INVAL; + } + /* 给了 src_addr 却没给够容量:静默不填比报错更坏,直接拒 */ + if ((src_addr != NULL) && + ((addrlen == NULL) || (*addrlen < (int)sizeof(struct net_sockaddr_in)))) { + return NET_ERR_INVAL; + } + + /* 执行任务上下文:直接执行(板级用例路径,无队列也照旧能收) */ + if (net_ctx_is_owner_task() != 0U) { + int rc; + + /* 直接路径同样经槽位交付源地址(exec 只认槽,不认参数) */ + if (net_udp_outparam_arm(sockfd, src_addr, addrlen) != 0) { + return NET_ERR_BUSY; + } + rc = net_udp_recvfrom_exec(sockfd, buf, len, flags, to_ms, + xTaskGetCurrentTaskHandle()); + if (rc == NET_PENDING_REGISTERED) { + /* 执行任务自等自通知必被 exec 守卫拒绝;万一出现,把槽位交回, + * 绝不能把 0x70000000 当成字节数返回给调用方 */ + net_udp_clear(sockfd); + return NET_ERR_BUSY; + } + if (rc < 0) { + /* exec 已按模块惯例记好本任务精确码:还原成原始码,公开入口统一映射 */ + return net_err_restore(rc); + } + return rc; + } + + /* 出参登记:槽位被占(上一条还没结束)→ BUSY */ + if (net_udp_outparam_arm(sockfd, src_addr, addrlen) != 0) { + return NET_ERR_BUSY; + } + + /* 队列未绑定(net_init 前的早期上下文):把刚登记的槽位交回, + * 别把这条 fd 永久钉成 BUSY */ + if (s_net_msg_queue == NULL) { + net_udp_outparam_disarm(sockfd); + return NET_ERR; + } + + (void)memset(&msg, 0, sizeof(msg)); + msg.type = NET_MSG_UDP_RECVFROM; + msg.sockfd = sockfd; + msg.buf = buf; + msg.len = len; + msg.flags = flags; + msg.timeout_ms = to_ms; + msg.caller = xTaskGetCurrentTaskHandle(); + + if (osMessageQueuePut(s_net_msg_queue, &msg, 0, osWaitForever) != osOK) { + net_udp_outparam_disarm(sockfd); + return NET_ERR_BUSY; + } + + /* 睡在自己的任务通知上:数据/到点/关闭三者之一由 netTask 回执(零自旋) */ + result = net_wait_result("urcv", sockfd); + return result; } /* diff --git a/Drivers/BSP/NET/net_socket.h b/Drivers/BSP/NET/net_socket.h index ceea41f6..ffd62079 100644 --- a/Drivers/BSP/NET/net_socket.h +++ b/Drivers/BSP/NET/net_socket.h @@ -39,11 +39,31 @@ typedef enum { NET_MSG_LISTEN, NET_MSG_ACCEPT, NET_MSG_DROP_CONN, /* 主动拆会话、保监听(见 net_drop_conn) */ + /* UDP 队列化改造(2026-11-03,工单前置 = SNTP 客户端 #57):UDP 三个碰芯片的 + * 动作从"调用方上下文直调 SPI"改为交 netTask 串行执行,字段用法: + * NET_MSG_UDP_BIND - sockfd + len=本地端口(主机序,0 = 执行体内取动态号) + * 恒 DesIP=0xFFFFFFFF 服务器模式(bind 不选对端, + * 客户端模式见 NET_MSG_UDP_PEER / SENDTO 的 addr) + * NET_MSG_UDP_SENDTO - buf/len=载荷 + addr=本包目的地址(按值拷入,**入参**方向) + * NET_MSG_UDP_RECVFROM - buf/len=载荷 + timeout_ms=等待预算 + flags + * ⚠ 源地址出参(src_addr / addrlen 两个指针)**不走本结构**:结构按值拷入 + * 队列,指针若在 int 字段里过桥,64 位主机单测构建会被截断,且违反 + * AGENTS「勿给 net_msg_t 加回传用字段」。改走 net_udp.c 的 per-fd 挂起槽 + * (调用方在入队前登记自己的指针,netTask 完成后写进调用方内存 + 通知值回码), + * 与 TCP 挂起 recv 把数据直接写进调用方 buf 是同一个模型。 + * 详见 docs/网络/BSD_Socket_API_使用指南.md §6 */ + NET_MSG_UDP_BIND, + NET_MSG_UDP_SENDTO, + NET_MSG_UDP_RECVFROM, /* 静默回收认领 + 手动自愈 API 退役(docs/网络/网络自愈架构设计.md §4.1、§4.2): * NET_MSG_RESYNC_LISTEN / NET_MSG_RELISTEN 已删—— * 静默回收由对账⑤自动认领(net_reconcile_tick),重建由 reconciler * 形态③④/D10 单一 * 所有者负责;手动自愈 API 面退役(生产调用者本就为零) */ + /* UDP 模式切换(追加在枚举末尾:既有成员编号是主机单测/板测的钉值对象, + * 不在中间插入)——addr 按值携带对端则切客户端模式,addrlen == 0 则回 + * 服务器模式;不重开 Socket,只写 DesIP/DesPort,语义见 net_udp_set_peer */ + NET_MSG_UDP_PEER } net_msg_type_t; typedef struct { @@ -331,60 +351,132 @@ int net_setblocking(int sockfd, int on); int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms); /* - * 函数功能:发送 UDP 数据 + * 函数功能:发送 UDP 数据报(**走消息队列**,任意任务可调用) * 入口参数:sockfd - socket 描述符 * buf - 数据缓冲区 - * len - 数据长度 - * flags - 通常为 0 - * dest_addr - 目标地址 - * addrlen - 地址长度 + * len - 数据长度(>0) + * flags - 目前不使用,传 0(与 net_send 同) + * dest_addr - 本包目的地址;NULL = 沿用本 Socket 当前 DesIP + * addrlen - 地址长度(NULL 目的时传 0 即可) * 返回值:实际发送字节数 int > 0 * -1 失败,精确码经本任务错误码槽给出(net_get_errno()): - * NET_ERR_BADF / NET_ERR_INVAL / NET_ERR_TIMEDOUT / - * NET_ERR_NOT_IN_NETTASK(调用上下文不对,见「限定条件」) - * 限定条件:**仅限本层执行任务(net_init 的调用者,NET 层 = netTask)调用**; - * socket 已创建且为 DGRAM;指针非空 - * 函数说明:1. 设置目标地址后调用 CH395F 发送 - * 2. ⚠ 本接口**不走消息队列**:它在调用方上下文里直接 net_poll + - * 写芯片缓冲,与 net_send/net_recv/net_connect 那一族(入队交 netTask - * 串行执行)不是同一条路径。别的任务调用它 = 与本层的 net_poll 抢 - * 同一根 SPI2(TRAP-NET-14 的根因形态),故加上下文守卫直接拒掉, - * 回 NET_ERR_NOT_IN_NETTASK。**该码重试永远无意义**(既非忙也非超时): - * 要么把调用挪进执行任务,要么改用走队列的 net_send——没有第三条。 - * 3. 「把它改成走消息队列」是已知后续项,未做的原因:UDP 的目的地址 - * 与源地址回传都要穿过 net_msg_t,而 net_msg_t 按值拷入队列、 - * 错误码只搭任务通知值回来(AGENTS 明文「勿给 net_msg_t 加回传字段」), - * 真要串行化得先设计 per-task 结果槽并配真机回归(2026-10-24 定案: - * 本轮只把危险面关掉,串行化仍未做)。 - * 4. 全仓当前**无生产调用者**(只有 test/board 与模拟器),正因如此 - * 这条守卫要在有人开始用之前就在。 + * NET_ERR_BADF / NET_ERR_INVAL / NET_ERR_TIMEDOUT / NET_ERR_BUSY + * 限定条件:socket 已创建(DGRAM 用法须先 net_bind 或 net_udp_set_peer 打开芯片侧 + * Socket);net_init() 已执行;**任意任务上下文**(含 netTask 自身, + * netTask 内直接执行不入队——自投递给自身 = 死锁) + * 函数说明:1. 目的地址是**入参**,按值拷进 net_msg_t.addr 随消息进 netTask, + * 与 net_connect 传地址同一条通道;结果(已发字节数/错误码) + * 搭任务通知值回来(AGENTS「勿给 net_msg_t 加回传字段」)。 + * 2. 带显式目的地址 = 芯片 DesIP 切到该 IP → 本 Socket 进入 + * **UDP 客户端模式**(p_sock->udp_mode = NET_UDP_MODE_CLIENT): + * 此后芯片只收该 IP 的包且**不再添加 8 字节信息头**, + * net_recvfrom 相应地不剥头、源地址回填该固定对端。 + * 要回服务器模式(任意源 + 带信息头)调 net_udp_set_peer(sockfd, NULL)。 + * 3. dest_addr=NULL 时不改 DesIP:服务器模式下 DesIP=0xFFFFFFFF 即 + * 向 255.255.255.255 广播(板级 UDP 广播测试的用法),模式不变。 + * 4. 分块上限 NET_SEND_CHUNK_MAX(1KB 发送 FIFO,TRAP-NET-16), + * 门控 SENBUF_FREE(TRAP-NET-04);单块等待超 NET_SEND_SPIN_MS + * 登记挂起 send、由 net_send_pending_tick 续传 → **netTask 不被 + * 一次大报发送占住**(与 TCP 发送同一套机制)。 + * 5. 短写:返回值 < len 表示未全部交付(停滞预算 NET_SEND_TIMEOUT_MS + * 用尽),调用方须检查返回值——同 net_send 契约。 + * 6. 手册 §9.2.4:UDP 一包最大 1472 字节;本层不做长度上限校验 + * (超出只会分多块写、对端收不到完整包),调用方自负。 */ int net_sendto(int sockfd, const void *buf, int len, int flags, const struct net_sockaddr *dest_addr, int addrlen); /* - * 函数功能:接收 UDP 数据 + * 函数功能:接收 UDP 数据报(含源地址;**走消息队列**,任意任务可调用) * 入口参数:sockfd - socket 描述符 * buf - 接收缓冲区 - * len - 缓冲区大小 - * flags - 通常为 0;NET_MSG_DONTWAIT 单次强制非阻塞 - * src_addr - 输出:发送方地址 - * addrlen - 输入输出:地址长度 - * 返回值:实际接收字节数(0 = 无数据可报的边界形态见说明) int >= 0 + * len - 缓冲区大小(>0) + * flags - 0 = 按 socket 的 blocking 属性等待;NET_MSG_DONTWAIT = 单次非阻塞 + * src_addr - 输出:发送方地址(可为 NULL) + * addrlen - 输入输出:src_addr 结构容量/实际长度(可为 NULL) + * 返回值:实际接收字节数 int >= 0 * -1 失败,精确码经本任务错误码槽给出(net_get_errno()): * NET_ERR_BADF / NET_ERR_INVAL / NET_ERR_WOULDBLOCK / - * NET_ERR_NOT_IN_NETTASK(调用上下文不对,见「限定条件」) - * 限定条件:**仅限本层执行任务(net_init 的调用者,NET 层 = netTask)调用**; - * socket 已创建且为 DGRAM;指针非空 - * 函数说明:1. 读取 CH395F 接收缓冲区,获取对端地址 - * 2. 阻塞语义与 net_recv 同:blocking 属性下无数据 - * 无限等待,DONTWAIT / 非阻塞属性立即 NET_ERR_WOULDBLOCK; - * 本接口为轮询自旋实现(历史遗留,UDP 专用),受 IWDG 监控的 - * 任务同样禁止对其无限等待 + * NET_ERR_TIMEDOUT / NET_ERR_CONNRESET(close/重建取消)/ NET_ERR_BUSY + * 限定条件:socket 已创建且为 DGRAM;net_init() 已执行;**任意任务上下文** + * (netTask 内直接执行不入队;netTask 内的阻塞等待被登记守卫拒绝 → + * 须用 NET_MSG_DONTWAIT) + * 函数说明:1. 与 net_recv 同一条路径(入队 → netTask 串行执行芯片事务 → + * 结果搭通知值回来),**不再是**"仅限执行任务、自旋 net_poll"的 + * 历史形态;非 netTask 调用不再回 NET_ERR_NOT_IN_NETTASK。 + * 2. 一次调用 = 一个数据报:芯片缓冲内该包**全部读空**(服务器模式 + * 手册要求一次性读出),载荷超出 len 的部分按 BSD 语义截断丢弃, + * 不会留下半包污染下一次解析。 + * 3. 源地址按 udp_mode 取:服务器模式剥 8 字节信息头 + * ([2-3] 源端口芯片小端、[4-7] 源 IP 线序字节 → 本层 s_addr 网络序 + * 约定,见 net_inet_addr 注释,2026-10-24 定案同族); + * 客户端模式不剥头、源地址=本 Socket 固定对端。 + * ⚠ 客户端模式若仍按服务器模式剥头,等于吃掉载荷前 8 字节 + * (SNTP 应答 48 字节会前 8 字节丢失)——这是本字段存在的理由。 + * 4. 阻塞语义与 net_recv 同构:无数据 + 阻塞 → **登记挂起、netTask + * 零占用返回**,数据到点断连由 net_udp_tick 按 1ms 节拍结束; + * 不再是自旋(旧形态会吃满 IWDG 预算并违反 netTask 契约)。 + * 5. ⚠ IWDG required_mask 内任务禁止无限等待形态:用 + * net_recvfrom_to() 显式预算或 net_select()(同 net_recv 契约)。 */ int net_recvfrom(int sockfd, void *buf, int len, int flags, struct net_sockaddr *src_addr, int *addrlen); +/* + * 函数功能:接收 UDP 数据报(**带调用方指定超时**,对应 net_recv_to 的 UDP 版) + * 入口参数:sockfd - socket 描述符 + * buf/len - 接收缓冲与容量 + * flags - 目前不使用,传 0(超时由 timeout_ms 表达) + * timeout_ms - 本次等待预算(毫秒,>0) + * src_addr - 输出:发送方地址(可为 NULL) + * addrlen - 输入输出:src_addr 容量/实际长度(可为 NULL) + * 返回值:>0 实际字节数;-1 = 真错误(errno 可查);**-2 = 超时无数据** + * 限定条件:非 netTask 上下文(netTask 内须用 net_recvfrom + NET_MSG_DONTWAIT) + * 函数说明:1. 与 net_recv_to 同一条路径与同一套超时收口:挂起登记带 deadline, + * 到点由 net_udp_tick 以超时结束,对外统一 -2(不靠 errno 区分, + * 无跨任务 errno 竞态)。 + * 2. 受 IWDG 监控的任务(含未来的 SNTP/对时任务)**必须**用本接口或 + * net_select,不得用 net_recvfrom 的无限等待形态。 + * 3. timeout_ms = 0 视为无效参数(无限等待请用 net_recvfrom)。 + */ +int net_recvfrom_to(int sockfd, void *buf, int len, uint32_t timeout_ms, + struct net_sockaddr *src_addr, int *addrlen); + +/* + * 函数功能:设置/查询 UDP Socket 的芯片模式(客户端 ↔ 服务器,见 net_types.h + * NET_UDP_MODE_* 与手册 §9.2.4) + * 入口参数:sockfd - socket 描述符(须为 DGRAM) + * peer - 非 NULL = 客户端模式:DesIP/DesPort 设为该对端,芯片只收 + * 它的包且**不加** 8 字节信息头; + * NULL = 服务器模式:DesIP=0xFFFFFFFF、DesPort=0,收任意来源 + * 且每个包带信息头(net_bind 的默认形态) + * 返回值:0 - 成功 int + * -1 - 失败(NET_ERR_BADF / NET_ERR_INVAL / NET_ERR_BUSY,errno 可查) + * 限定条件:socket 已创建且为 DGRAM 且已 net_bind(芯片侧 Socket 已打开); + * 任意任务上下文(走消息队列,netTask 内直接执行) + * 函数说明:1. 存在的理由:模式由"最近一次写 DesIP"决定,net_sendto 带目的地址 + * 发一次就把芯片静默切到客户端模式。要给多个对端收发(TFTP 式 + * "收到谁的包回给谁、然后继续听所有人")必须在每次带地址发送后 + * 显式回服务器模式,否则回程包会被当成带头的包错解析。 + * 2. 只改 DesIP/DesPort 两个寄存器,不重开 Socket(不丢接收缓冲内 + * 已到的数据);本层 udp_mode 同步更新,recvfrom 剥头判定据此走。 + * 3. 客户端 → 服务器切换后**先前**收到的包(无头)无法补上源地址, + * 模式切换时刻之后的包才按新模式解析——协议栈请"先切模式再等包"。 + * 4. 切模式不改动 local_port,也不影响已登记的挂起 recv;挂起期间 + * 切模式由 tick 在结算时读当前模式解析(同一包不会两解)。 + */ +int net_udp_set_peer(int sockfd, const struct net_sockaddr_in *peer); + +/* + * 函数功能:查询 UDP Socket 当前芯片模式 + * 入口参数:sockfd - socket 描述符 + * 返回值:NET_UDP_MODE_SERVER / NET_UDP_MODE_CLIENT uint8_t + * -1 - 描述符无效或非 DGRAM int + * 限定条件:任意上下文(纯 RAM 读,不发起芯片事务) + * 函数说明:诊断/单测/上层"要不要切回服务器模式"的判读入口 + */ +int net_udp_get_mode(int sockfd); + /* * 函数功能:关闭 socket * 入口参数:sockfd - socket 描述符 @@ -598,6 +690,18 @@ int net_getpeername(int sockfd, struct net_sockaddr *addr, int *addrlen); * 内部 API(需在 netTask 上下文中调用,绕过消息队列) */ +/* + * 函数功能:取下一个动态端口并推进游标(49152~65535 回绕,内部使用) + * 入口参数:无 + * 返回值:分配到的端口(主机序) uint16_t + * 限定条件:单线程语义——TCP 绑定路径(调用方上下文)与 UDP 绑定执行体 + * (netTask)都经此取号,簿记只住 net_socket.c 一处 + * 函数说明:原逻辑内联在 net_bind 里;UDP 队列化后绑定动作挪进 netTask 执行, + * 两处都要用同一游标,故抽成函数(同一事实只在一处)。 + * 不做"是否已被其他 Socket 占用"检查——与抽取前行为一致。 + */ +uint16_t net_dynamic_port_next(void); + /* * 函数功能:发送数据(内部版本,直接操作,不经过消息队列) * 入口参数:p_sock - Socket 控制块指针 @@ -634,6 +738,33 @@ int net_close_sock(net_sock_t *p_sock); * 限定条件:必须在 netTask 上下文中调用 */ int net_listen_locked(int sockfd); + +/* + * 函数功能:判当前上下文是否本层执行任务(netTask / 硬件层的 testTask) + * 入口参数:无 + * 返回值:1 - 是本层执行任务(或 net_init 前未捕获);0 - 明确不是 uint8_t + * 限定条件:net_init 之后判据才有效;任意上下文可读 + * 函数说明:队列化接口用本判据决定"直接执行"还是"投递给 netTask"——执行任务 + * 自己不能往自己的队列里投(要等自己回来泵,队列满即自等死锁); + * 挂起登记前也用它确认"完成动作有人做"。判据与留痕见定义处注释。 + */ +uint8_t net_ctx_is_owner_task(void); + +/* + * 函数功能:发送数据(内部锁定版本,TCP 与 UDP 共用同一份分块实现) + * 入口参数:p_sock - Socket 控制块指针 + * buf - 数据缓冲区 + * len - 数据长度 + * caller - 挂起 send 的完成通知目标(执行任务自身传 NULL 即拒登记) + * 返回值:实际发送字节数(>= 0);NET_PENDING_REGISTERED = 已挂起;-1 = 错误 + * 限定条件:必须在 netTask 上下文中调用 + * 函数说明:UDP 队列化后 net_udp_sendto_exec 也走本函数——分块粒度 + * (NET_SEND_CHUNK_MAX)、SENBUF_FREE 门控、末块不等、停滞超时、 + * 挂起 send 表全部只有一份(TRAP-NET-16 的复发形态就是"两条路径 + * 各写一套分块自旋",故不复制)。状态门按 type 区分:TCP 须 + * ESTABLISHED,UDP 须 UDP_OPEN。 + */ +int net_send_locked(net_sock_t *p_sock, const void *buf, int len, TaskHandle_t caller); #endif /* NET_NO_OS */ #ifdef __cplusplus diff --git a/Drivers/BSP/NET/net_types.h b/Drivers/BSP/NET/net_types.h index 189968fc..f145f168 100644 --- a/Drivers/BSP/NET/net_types.h +++ b/Drivers/BSP/NET/net_types.h @@ -50,6 +50,25 @@ extern "C" { #define NET_MSG_DONTWAIT 0x01 /* 非阻塞模式(单次覆盖 socket 的 blocking 属性; * 具名入口见 net_recv_nb() / net_setblocking()) */ +/* + * UDP 芯片模式(仅 type == NET_SOCK_DGRAM 有意义;手册 §9.2.4 步骤②: + * **目的 IP 决定模式**,两种模式的收包格式不同,见下) + * + * 服务器模式 DesIP = 0xFFFFFFFF:可与任意远端通讯,CH395F 在**每个**收到的 + * 数据报前添加 8 字节信息头([0-1] 数据长度、[2-3] 源端口(芯片小端)、 + * [4-7] 源 IP(线序字节,首段在低地址)),且手册要求一次性把该包全部读出。 + * 客户端模式 DesIP = 具体 IP:芯片只收该 IP 的包,且**不加**信息头—— + * 数据流直接落到接收缓冲区(可整读也可部分读)。 + * + * 为什么本层要存这个状态:模式是芯片属性、由"最近一次写 DesIP"决定,而软件 + * 原先看不见它——net_sendto 带目的地址发一次就把芯片静默切到客户端模式,之后 + * 的收包仍按服务器模式剥 8 字节 = **吃掉载荷前 8 字节**并把垃圾字节当源地址 + * (SNTP 48 字节应答会丢掉前 8 字节,2026-11-03 定性,见 + * docs/问题记录/CH395F_Trap_Records.md「UDP 模式」条)。剥不剥头必须由本字段决定。 + */ +#define NET_UDP_MODE_SERVER 0U /* DesIP=0xFFFFFFFF:收包含 8 字节信息头(net_bind 的默认模式) */ +#define NET_UDP_MODE_CLIENT 1U /* DesIP=具体 IP:收包无信息头,源地址=本 Socket 固定对端 */ + /* * 网络字节序转换宏 */ @@ -167,6 +186,16 @@ typedef struct { net_sock_state_t state; /* Socket 状态 */ uint8_t type; /* SOCK_STREAM / SOCK_DGRAM */ uint8_t in_use; /* 是否在用标志 */ + /* UDP 芯片模式(NET_UDP_MODE_SERVER / _CLIENT,宏注释见本文件 UDP 模式定义处)。 + * 仅 type == NET_SOCK_DGRAM 有意义;置位/清零路径:net_bind 的 UDP 分支 → + * SERVER(DesIP=0xFFFFFFFF);net_sendto 带显式目的地址、net_udp_set_peer(非 + * NULL) → CLIENT(DesIP=该 IP);net_udp_set_peer(NULL) → 回 SERVER。 + * 新 fd / close 由整块 memset 归零 = SERVER,与 bind 默认同值,故不得继承上一条。 + * CLIENT 模式的对端簿记复用 remote_ip_arr[4] + remote_port(与 TCP 同一对字段, + * 语义同为"本会话对端"),recvfrom 据此回填 src_addr。 + * 位置选择:紧接 in_use 之后,落在 type/in_use 之后原有的 2 字节填充里 + * (本结构 8 个实例,净增 RAM = 0,见 AGENTS RAM 红线)。 */ + uint8_t udp_mode; uint16_t local_port; /* 本地端口(主机序) */ uint16_t remote_port; /* 远端端口(主机序) */ uint32_t remote_ip; /* 远端 IP(网络字节序) */ diff --git a/Drivers/BSP/NET/net_udp.c b/Drivers/BSP/NET/net_udp.c new file mode 100644 index 00000000..4a982930 --- /dev/null +++ b/Drivers/BSP/NET/net_udp.c @@ -0,0 +1,765 @@ +/* + * 模块名称:UDP 数据面执行器与挂起槽(netTask 串行化) + * 文件说明:UDP 队列化改造(2026-11-03)的执行侧。公开 API 在 net_socket.c 里只做 + * "校验参数 → 装 net_msg_t 入队 → 等任务通知值",真正的芯片事务全部 + * 在本文件的 *_exec 函数里执行(netTask 上下文,与 TCP 同一根 SPI 的 + * 唯一持有者)。源地址出参经本文件的 per-fd 槽位回到调用方内存。 + * 架构位置:docs/网络/BSD_Socket_API_使用指南.md §6(UDP 两种芯片模式)+ + * docs/网络/Net层阻塞模型架构设计.md(挂起模型与通知值回传) + * 依赖:net_socket.h(net_get_sock / net_poll / net_set_errno / 动态端口)、 + * net_pending.h(NET_PENDING_REGISTERED / NET_SEND_CHUNK_MAX / 挂起 send 表)、 + * ch395f.h(芯片命令)、main.h(HAL_GetTick) + * 作者:super_agent_bot + * 创建日期:2026-11-03 + * 修改记录: + * 2026-11-03 super_agent_bot 创建:UDP 队列化改造(SNTP 客户端的前置) + */ + +#include +#include + +#include "FreeRTOS.h" +#include "task.h" + +#include "net_types.h" +#include "net_socket.h" +#include "net_udp.h" +#include "net_pending.h" +#include "ch395f.h" +#include "dbg_log.h" +#include "main.h" + +/* + * 私有常量定义区 + */ + +/* 服务器模式信息头长度(手册 §9.2.4:[0-1] 数据长度、[2-3] 源端口、[4-7] 源 IP) */ +#define NET_UDP_HDR_LEN 8U +/* 源端口在信息头内的偏移 */ +#define NET_UDP_HDR_PORT_OFF 2U +/* 源 IP 在信息头内的偏移 */ +#define NET_UDP_HDR_IP_OFF 4U +/* 截断数据报的排空粒度(字节):读空不丢后续包,只在调用方缓冲偏小时走这条路 */ +#define NET_UDP_DRAIN_CHUNK 64U + +/* + * 私有类型定义区 + */ + +/* + * 一次 UDP 接收请求的槽位(每 fd 至多一个) + * armed = 1:调用方已登记源地址出参(消息可能还在队列里,也可能正在执行) + * pending = 1:已登记挂起等待(数据/到点/关闭三者之一将结束它) + * 两个标志的差别是"谁欠调用方一个回执":armed 期间回执还没发;pending 期间 + * 连即时回执都还没生成,结束动作由 net_udp_tick 负责。 + */ +typedef struct { + uint8_t armed; /* 1 = 槽位被占用(禁止第二个接收请求入队) */ + uint8_t pending; /* 1 = 挂起等待中 */ + struct net_sockaddr *src_addr; /* 调用方源地址出参(可为 NULL) */ + int *addrlen; /* 调用方长度出参(可为 NULL) */ + void *buf; /* 载荷缓冲(调用方持有,须有效至回执) */ + int len; /* 载荷缓冲容量 */ + uint32_t deadline; /* 结束时刻(0 = 无限等待,同 net_pending 约定) */ + TaskHandle_t caller; /* 回执目标 */ +} net_udp_req_t; + +/* + * 私有变量定义区 + */ + +/* per-fd 接收槽位表:与 net_pending(TCP recv)/ net_send_pending 各自独立, + * 同一 fd 上三者互不阻塞(UDP 只用本表 + send 表) */ +static net_udp_req_t s_net_udp[NET_MAX_SOCKETS]; + +/* + * 私有函数声明区 + */ + +static void net_udp_src_from_header(net_sock_t *p_sock, const uint8_t *hdr, + struct net_sockaddr *src_addr, int *addrlen); +static void net_udp_src_from_peer(net_sock_t *p_sock, struct net_sockaddr *src_addr, + int *addrlen); +static int net_udp_datagram_read(int sockfd, net_sock_t *p_sock, void *buf, int len, + struct net_sockaddr *src_addr, int *addrlen, + int avail); +static void net_udp_req_complete(int sockfd, int result); + +/* + * 公共函数实现(调用方上下文) + */ + +/* + * 函数功能:登记本次接收的源地址出参(调用方上下文,入队前) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * src_addr - 源地址出参指针(可为 NULL) struct net_sockaddr * + * addrlen - 出参长度指针(可为 NULL) int * + * 出口参数:无 + * 返回值:0 - 成功;-1 - 失败(NET_ERR_BADF / NET_ERR_BUSY) int + * 限定条件:任意上下文 + * 函数说明:临界区只包"查占位"三步,不碰芯片;占位成功后本 fd 的第二个接收 + * 请求被拒(BUSY)。同一 fd 的并发接收在数据报语义上无法区分归属, + * 拒绝比交错投递安全(TCP 挂起 recv 同一守卫,见 net_pending_register)。 + */ +int net_udp_outparam_arm(int sockfd, struct net_sockaddr *src_addr, int *addrlen) { + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + return NET_ERR_RETURN_1(NET_ERR_BADF); + } + + taskENTER_CRITICAL(); + if (s_net_udp[sockfd].armed != 0U) { + taskEXIT_CRITICAL(); + return NET_ERR_RETURN_1(NET_ERR_BUSY); + } + s_net_udp[sockfd].armed = 1U; + s_net_udp[sockfd].src_addr = src_addr; + s_net_udp[sockfd].addrlen = addrlen; + taskEXIT_CRITICAL(); + + return 0; +} + +/* + * 函数功能:归还源地址出参槽(调用方上下文,入队失败这条出口) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * 出口参数:无 + * 返回值:无 + * 限定条件:任意上下文;仅在未拿到 netTask 回执的失败路径调用 + * 函数说明:netTask 的结算路径自己清槽(net_udp_req_complete),本函数只服务 + * 调用方侧"消息没投出去"的清理,防槽位泄漏把该 fd 永久钉成 BUSY。 + */ +void net_udp_outparam_disarm(int sockfd) { + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + return; + } + + taskENTER_CRITICAL(); + s_net_udp[sockfd].armed = 0U; + s_net_udp[sockfd].pending = 0U; + s_net_udp[sockfd].src_addr = NULL; + s_net_udp[sockfd].addrlen = NULL; + s_net_udp[sockfd].buf = NULL; + s_net_udp[sockfd].len = 0; + s_net_udp[sockfd].deadline = 0U; + s_net_udp[sockfd].caller = NULL; + taskEXIT_CRITICAL(); +} + +/* + * 函数功能:清除本 fd 的 UDP 接收槽(不通知) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * 出口参数:无 + * 返回值:无 + * 限定条件:任意上下文(netTask 的 close/net_socket 分配路径调用) + * 函数说明:与 net_pending_clear 同一契约——"新 fd 不得继承上一条连接的挂起", + * 调用方侧通知已在各 settle/cancel 路径发过,这里只做幂等归零。 + */ +void net_udp_clear(int sockfd) { + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + return; + } + (void)memset(&s_net_udp[sockfd], 0, sizeof(s_net_udp[sockfd])); +} + +/* + * 公共函数实现(netTask 上下文) + */ + +/* + * 函数功能:UDP 绑定/打开芯片侧 Socket(服务器模式) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * local_port - 本地(源)端口,主机序;0 = 分配动态端口 uint16_t + * 出口参数:无 + * 返回值:0 - 成功;-1 - 失败(NET_ERR_BADF/INVAL/BUSY,errno 已记) int + * 限定条件:netTask 上下文;控制块在用且为 DGRAM + * 函数说明:1. 命令顺序按手册 §9.2.4:SET_PROTO_TYPE → SET_SOUR_PORT → + * SET_DES_IP(0xFFFFFFFF) → SET_DES_PORT(0) → OPEN_SOCKET。 + * 目的地址必须在 OPEN 之前下发——open 会复位端口/IP 寄存器 + * (docs/网络/BSD_Socket_API_函数调用关系.md §4)。 + * 2. DesIP=0xFFFFFFFF 即服务器模式:收任意来源,芯片给每个包加 + * 8 字节信息头(net_types.h UDP 模式宏注释)。 + * 3. BSD 的 bind 只表达本地地址,**不选对端**,故本函数恒为服务器模式; + * 客户端模式由 net_sendto(带目的地址) 或 net_udp_set_peer 进入。 + * 4. 重绑(同一 fd 二次 bind)会重开芯片侧 Socket:这是"从客户端模式 + * 回到服务器模式"的完整形态(比 net_udp_set_peer 更重,连缓冲与 + * 端口一起重来),接收缓冲内未读数据随之作废——调用方自选。 + * 作废不等于可以不吭声:重开之前先把本 fd 的挂起收包按 CONNRESET + * 结算并通知,否则"另一个任务重绑 + 本任务无限等包"= 永久睡死。 + * 5. 动态端口在 netTask 内取号(原在调用方上下文改 s_dynamic_port)。 + */ +int net_udp_bind_exec(int sockfd, uint16_t local_port) { + /* 控制块 */ + net_sock_t *p_sock = net_get_sock(sockfd); + /* open_socket 返回状态 */ + uint8_t status; + /* 最终端口(主机序) */ + uint16_t port = local_port; + /* 服务器模式 DesIP(线序字节:首段在低地址) */ + static const uint8_t s_des_ip_any[4] = {0xFFU, 0xFFU, 0xFFU, 0xFFU}; + + if (p_sock == NULL) { + return NET_ERR_RETURN_1(NET_ERR_BADF); + } + if (p_sock->type != NET_SOCK_DGRAM) { + return NET_ERR_RETURN_1(NET_ERR_INVAL); + } + + /* 重开芯片侧会丢掉接收缓冲里已到的数据报:本 fd 若正有人在等包,必须在动手 + * 之前先给一个明确的了断(CONNRESET + 通知)。少了这一步,"A 任务 net_recvfrom + * 无限等 + B 任务重新 bind 同一 fd"就是永久睡死——本函数末尾的 net_udp_clear + * 只归零状态、不通知,那是兜底,不是结算。 */ + net_udp_req_complete(sockfd, NET_ERR_CONNRESET); + + /* net_init 已为全部 8 个 Socket 分配缓冲;此处仅对 Socket 4~7 兜底重设, + * 布局与 net_init 一致(固定 6 块/Socket,与 MSS 解耦) */ + if (sockfd >= 4) { + /* 每 Socket 块数 = 接收 + 发送 */ + uint8_t blocks_per_socket = (uint8_t)(CH395F_RECV_BLOCKS + CH395F_SEND_BLOCKS); + /* 本 Socket 接收缓冲起始块 */ + uint8_t recv_start = (uint8_t)(sockfd * blocks_per_socket); + /* 发送缓冲起始块(接收块之后) */ + uint8_t send_start = (uint8_t)(recv_start + CH395F_RECV_BLOCKS); + + ch395f_set_send_buf((uint8_t)sockfd, send_start, (uint8_t)CH395F_SEND_BLOCKS); + ch395f_set_recv_buf((uint8_t)sockfd, recv_start, (uint8_t)CH395F_RECV_BLOCKS); + } + + /* 协议类型 = UDP */ + ch395f_set_proto_type((uint8_t)sockfd, CH395F_PROTO_TYPE_UDP); + + /* 端口 0 = 请本层分配 */ + if (port == 0U) { + port = net_dynamic_port_next(); + } + p_sock->local_port = port; + ch395f_set_sour_port((uint8_t)sockfd, port); + + /* DesIP=0xFFFFFFFF + DesPort=0:服务器模式(不限对端) */ + ch395f_set_des_ip((uint8_t)sockfd, (uint8_t *)s_des_ip_any); + ch395f_set_des_port((uint8_t)sockfd, 0U); + + status = ch395f_open_socket((uint8_t)sockfd); + if (status != CH395F_ERR_SUCCESS) { + DBG_ERROR("udp sock%d open FAIL status=0x%02X", sockfd, status); + return NET_ERR_RETURN_1(NET_ERR_BUSY); + } + + /* 打开即可用(UDP 无监听态) */ + p_sock->state = NET_SOCK_STATE_UDP_OPEN; + /* 模式与对端簿记复位:重开后的芯片就是服务器模式,软件账本必须同步 */ + p_sock->udp_mode = NET_UDP_MODE_SERVER; + (void)memset(p_sock->remote_ip_arr, 0, sizeof(p_sock->remote_ip_arr)); + p_sock->remote_port = 0U; + p_sock->remote_ip = 0U; + /* 重开不得继承上一条连接的电平 */ + p_sock->readable = 0U; + /* 本 fd 的接收槽兜底归零(上方的 req_complete 已把"重开前还在等包"的调用方结算掉) */ + net_udp_clear(sockfd); + + return 0; +} + +/* + * 函数功能:切换 UDP 芯片模式(改 DesIP/DesPort,不重开 Socket) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * peer - 非 NULL = 客户端模式对端;NULL = 回服务器模式 const struct net_sockaddr_in * + * 出口参数:无 + * 返回值:0 - 成功;-1 - 失败(NET_ERR_BADF/INVAL/NOTCONN,errno 已记) int + * 限定条件:netTask 上下文;控制块在用、type == DGRAM、state == UDP_OPEN + * 函数说明:语义见 net_socket.h net_udp_set_peer。open 之后改这两个寄存器 + * 是否立即生效已有实证:net_sendto 一直是"发之前才写 DesIP", + * 芯片据此就换模式(docs/问题记录/CH395F_Trap_Records.md「UDP 模式」条)。 + */ +int net_udp_set_peer_exec(int sockfd, const struct net_sockaddr_in *peer) { + /* 控制块 */ + net_sock_t *p_sock = net_get_sock(sockfd); + /* 目的 IP 线序字节 */ + uint8_t ip_arr[4]; + + if (p_sock == NULL) { + return NET_ERR_RETURN_1(NET_ERR_BADF); + } + if (p_sock->type != NET_SOCK_DGRAM) { + return NET_ERR_RETURN_1(NET_ERR_INVAL); + } + if (p_sock->state != NET_SOCK_STATE_UDP_OPEN) { + /* 芯片侧 Socket 还没打开:先 net_bind(否则 open 会把这里设的值复位) */ + return NET_ERR_RETURN_1(NET_ERR_NOTCONN); + } + + if (peer != NULL) { + /* 客户端模式:对端 IP 按内存顺位取(s_addr 已是网络序 = 首段在低地址) */ + (void)memcpy(ip_arr, &peer->sin_addr.s_addr, sizeof(ip_arr)); + ch395f_set_des_ip((uint8_t)sockfd, ip_arr); + ch395f_set_des_port((uint8_t)sockfd, net_ntohs(peer->sin_port)); + + p_sock->udp_mode = NET_UDP_MODE_CLIENT; + (void)memcpy(p_sock->remote_ip_arr, ip_arr, sizeof(ip_arr)); + p_sock->remote_port = net_ntohs(peer->sin_port); + p_sock->remote_ip = peer->sin_addr.s_addr; + } else { + /* 服务器模式:DesIP=0xFFFFFFFF、DesPort=0(与 bind 同值) */ + ip_arr[0] = 0xFFU; + ip_arr[1] = 0xFFU; + ip_arr[2] = 0xFFU; + ip_arr[3] = 0xFFU; + ch395f_set_des_ip((uint8_t)sockfd, ip_arr); + ch395f_set_des_port((uint8_t)sockfd, 0U); + + p_sock->udp_mode = NET_UDP_MODE_SERVER; + (void)memset(p_sock->remote_ip_arr, 0, sizeof(p_sock->remote_ip_arr)); + p_sock->remote_port = 0U; + p_sock->remote_ip = 0U; + } + + return 0; +} + +/* + * 函数功能:UDP 发送执行体(分块 + 有界自旋 + 挂起 send 续传) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * buf - 载荷(调用方内存) const void * + * len - 载荷长度(>0) int + * dest - 本包目的地址;NULL = 不改 DesIP const struct net_sockaddr_in * + * caller - 调用方任务句柄(挂起 send 通知目标) TaskHandle_t + * 出口参数:无 + * 返回值:实际发送字节数(>= 0);NET_PENDING_REGISTERED;-1 = 失败 int + * 限定条件:netTask 上下文;控制块在用且已 bind(state == UDP_OPEN) + * 函数说明:目的地址簿记 + 模式置位在本函数做完,之后把分块/等待/挂起全部 + * 交给 net_send_locked——分块粒度、SENBUF_FREE 门控、末块不等、 + * 短写与挂起 send 表都与 TCP 共用同一份实现,不留第二份自旋 + * (TRAP-NET-16 的复发形态正是"两条路径各写一套分块")。 + */ +int net_udp_sendto_exec(int sockfd, const void *buf, int len, + const struct net_sockaddr_in *dest, TaskHandle_t caller) { + /* 控制块 */ + const net_sock_t *p_sock = net_get_sock(sockfd); + + if (p_sock == NULL) { + return NET_ERR_RETURN_1(NET_ERR_BADF); + } + if ((buf == NULL) || (len <= 0)) { + return NET_ERR_RETURN_1(NET_ERR_INVAL); + } + if (p_sock->state != NET_SOCK_STATE_UDP_OPEN) { + return NET_ERR_RETURN_1(NET_ERR_NOTCONN); + } + + if (dest != NULL) { + /* 带显式目的地址:芯片 DesIP 切到该 IP → 客户端模式(硬件行为,如实记账) */ + struct net_sockaddr_in peer; + int rc; + + (void)memset(&peer, 0, sizeof(peer)); + peer.sin_family = NET_AF_INET; + peer.sin_port = dest->sin_port; + peer.sin_addr.s_addr = dest->sin_addr.s_addr; + rc = net_udp_set_peer_exec(sockfd, &peer); + if (rc != 0) { + return rc; + } + } + + /* 分块发送(含挂起 send 登记)与 TCP 同一实现。 + * 必须走 net_send_locked 而不是 net_send_sock:后者把 caller 写成自己 + * (netTask),挂起登记会被"执行者自等待"守卫拒成 BUSY,真正的调用方 + * 就永远等不到续传完成的通知。 */ + return net_send_locked(net_get_sock(sockfd), buf, len, caller); +} + +/* + * 函数功能:UDP 接收执行体(即时读 / 登记挂起 / 立即拒) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * buf - 调用方缓冲 void * + * len - 缓冲容量(>0) int + * flags - NET_MSG_DONTWAIT = 单次非阻塞 int + * to_ms - 等待预算(0 = 无限等待) uint32_t + * caller - 调用方任务句柄(挂起完成通知目标) TaskHandle_t + * 出口参数:无(源地址经槽位里的调用方指针直接写回) + * 返回值:实际接收字节数(> 0);NET_PENDING_REGISTERED;-1 = 失败(errno 已记) int + * 限定条件:netTask 上下文;控制块在用、type == DGRAM、state == UDP_OPEN + * 函数说明:1. 一次调用 = 一个完整数据报(剥头 + 截断排空),见 net_udp_datagram_read。 + * 2. 无数据 + 阻塞 → 登记挂起(复用本 fd 已 arm 的槽位),netTask 零占用 + * 返回;完成条件由 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) { + /* 控制块 */ + net_sock_t *p_sock = net_get_sock(sockfd); + /* 本 fd 接收槽 */ + net_udp_req_t *p; + /* 芯片侧可用字节 */ + int avail; + + if (p_sock == NULL) { + return NET_ERR_RETURN_1(NET_ERR_BADF); + } + /* ⚠ 取槽指针必须排在 fd 合法性检查之后:s_net_udp[] 只有 NET_MAX_SOCKETS 项, + * 在越界 fd 上先取地址 = 越界指针(哪怕还没解引用,后续登记就是野写)。 */ + 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); + } + + /* 刷新状态机与可读/可写标志(与 net_recv_locked 同一入口动作) */ + net_poll(); + + avail = (int)ch395f_get_recv_len((uint8_t)sockfd); + if (avail > 0) { + /* 数据已在:读出这一个数据报并立即回执 */ + int rd = net_udp_datagram_read(sockfd, p_sock, buf, len, p->src_addr, p->addrlen, + avail); + + net_udp_clear(sockfd); + return rd; + } + + /* 非阻塞:DONTWAIT 单次覆盖或 blocking 属性为 0(与 TCP recv 同判据) */ + if (((flags & NET_MSG_DONTWAIT) != 0) || (p_sock->blocking == 0U)) { + net_udp_clear(sockfd); + return NET_ERR_RETURN_1(NET_ERR_WOULDBLOCK); + } + + /* 执行任务自身:无他人可完成本挂起 → 拒绝(自等死锁守卫,同 + * net_pending_register 的 `caller == s_owner_task` 判据)。 + * ⚠ 这里**不能**用 net_ctx_is_owner_task():那个谓词是给"调用方选路"用的, + * 而本函数按设计就跑在执行任务里——用它等于把每一条经队列投进来的正常 + * 请求都当成自等拒掉(2026-11-03 主机单测 UDP-8a 当场抓到)。 + * 执行侧要问的是"这条请求的调用方是不是正在执行的我",与"我在不在执行 + * 任务里"是两个问题;直接路径由 net_recvfrom_impl 传进当前句柄,仍然命中。 */ + if ((caller == NULL) || (caller == xTaskGetCurrentTaskHandle())) { + net_udp_clear(sockfd); + return NET_ERR_RETURN_1(NET_ERR_BUSY); + } + + /* 登记挂起:载荷与出参已在槽内,补上预算与通知目标 */ + p->pending = 1U; + p->buf = buf; + p->len = len; + p->deadline = (to_ms == 0U) ? 0U : (HAL_GetTick() + to_ms); + p->caller = caller; + + return NET_PENDING_REGISTERED; +} + +/* + * 函数功能:挂起 UDP 接收完成检查(netTask 每轮调用一次,非阻塞) + * 入口参数:p_socks - Socket 控制块表首指针(net_socket.c 的 s_net_socks) net_sock_t * + * 出口参数:无 + * 返回值:无 + * 限定条件:netTask 上下文 + * 函数说明:逐 fd 按序 ①fd 失效/不再是已打开的 UDP → CONNRESET ②数据到达 → + * 读包 + 回填源地址 + 字节数 ③到点(仅显式预算)→ TIMEDOUT。 + * 三条出口共用 net_udp_req_complete(必通知,漏通知 = 调用方永久挂死)。 + */ +void net_udp_tick(net_sock_t *p_socks) { + /* 当前 tick */ + uint32_t now = HAL_GetTick(); + /* 循环索引 */ + int i; + + for (i = 0; i < NET_MAX_SOCKETS; i++) { + /* 本 fd 接收槽 */ + net_udp_req_t *p = &s_net_udp[i]; + /* 本 fd 控制块 */ + net_sock_t *p_sock = &p_socks[i]; + + if (p->pending == 0U) { + continue; + } + + /* ① fd 已失效或本 Socket 不再是打开的 UDP(close/重建/被复位) */ + if ((p_sock->in_use == 0U) || + (p_sock->type != NET_SOCK_DGRAM) || + (p_sock->state != NET_SOCK_STATE_UDP_OPEN)) { + net_udp_req_complete(i, NET_ERR_CONNRESET); + continue; + } + + /* ② 数据到达:芯片缓冲有包即完成(readable 只是省 SPI 的门, + * 电平型 RECV_OK 每轮重读会重新置位,事件未收到也能自愈) */ + if (p_sock->readable != 0U) { + int avail = (int)ch395f_get_recv_len((uint8_t)i); + + if (avail > 0) { + int rd = net_udp_datagram_read(i, p_sock, p->buf, p->len, + p->src_addr, p->addrlen, avail); + net_udp_req_complete(i, rd); + continue; + } + /* 可读标志是陈旧电平(包已被别的路径读空):清掉,别每轮白问一次 SPI */ + p_sock->readable = 0U; + } + + /* ③ 到点(仅显式预算;deadline = 0 表示无限等待,不参与判定) */ + if ((p->deadline != 0U) && ((int32_t)(now - p->deadline) >= 0)) { + net_udp_req_complete(i, NET_ERR_TIMEDOUT); + } + } +} + +/* + * 函数功能:取消全部挂起 UDP 接收并逐一通知(网络重建/复位路径) + * 入口参数:err - 通知的错误码 int + * 出口参数:无 + * 返回值:无 + * 限定条件:netTask 上下文;须在控制块清零与芯片复位之前调用 + */ +void net_udp_cancel_all(int err) { + int i; + + for (i = 0; i < NET_MAX_SOCKETS; i++) { + net_udp_req_complete(i, err); + } +} + +/* + * 函数功能:关闭前结算本 fd 的挂起 UDP 接收(有数据先送达,无数据则取消) + * 入口参数:sockfd - 将被关闭的描述符 (0~7) int + * p_sock - 将被关闭的控制块指针 net_sock_t * + * 出口参数:无 + * 返回值:无(无在位挂起时静默返回) + * 限定条件:netTask 上下文;须在 ch395f 关闭/复位该 Socket 之前调用 + * 函数说明:与 net_pending_settle_close 同一契约——关闭不丢已到达数据。 + * 送达走同一条剥头/截断路径,源地址照常回填,调用方拿到的是一个 + * 完整数据报而不是被关闭流程截走的半包。 + */ +void net_udp_settle_close(int sockfd, net_sock_t *p_sock) { + /* 本 fd 接收槽 */ + net_udp_req_t *p; + /* 芯片侧可用字节 */ + int avail; + + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + return; + } + p = &s_net_udp[sockfd]; + if (p->pending == 0U) { + return; + } + + avail = (int)ch395f_get_recv_len((uint8_t)sockfd); + if (avail > 0) { + int rd = net_udp_datagram_read(sockfd, p_sock, p->buf, p->len, + p->src_addr, p->addrlen, avail); + net_udp_req_complete(sockfd, rd); + return; + } + net_udp_req_complete(sockfd, NET_ERR_CONNRESET); +} + +/* + * 私有函数实现 + */ + +/* + * 函数功能:统一出口——通知调用方并归还本 fd 接收槽 + * 入口参数:sockfd - Socket 描述符 (0~7) int + * result - 结果原值(> 0 字节数;负 = 错误码,搭通知值回传) int + * 出口参数:无 + * 返回值:无(槽位未在位时静默返回,防重复通知) + * 限定条件:netTask 上下文 + * 函数说明:与 net_pending_complete 同构:通知值 32 位整体即结果, + * 错误码由调用方侧 NET_ERR_RETURN_1 记入**自己**的 errno 槽位, + * 不存在跨任务 errno 竞态。 + */ +static void net_udp_req_complete(int sockfd, int result) { + /* 本 fd 接收槽 */ + net_udp_req_t *p; + + if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) { + return; + } + p = &s_net_udp[sockfd]; + if (p->pending == 0U) { + return; + } + + xTaskNotify(p->caller, (uint32_t)result, eSetValueWithOverwrite); + net_udp_clear(sockfd); +} + +/* + * 函数功能:从 8 字节信息头回填源地址(服务器模式) + * 入口参数:p_sock - 控制块(本函数不用,保留与 peer 版同形签名) net_sock_t * + * hdr - 已读出的 8 字节信息头 const uint8_t * + * src_addr - 调用方出参(可为 NULL) struct net_sockaddr * + * addrlen - 调用方出参长度(可为 NULL) int * + * 出口参数:src_addr - 填充发送方地址;*addrlen = sizeof(sockaddr_in) + * 返回值:无 + * 限定条件:仅由 net_udp_datagram_read 在服务器模式分支调用 + * 函数说明:头布局(手册 §9.2.4)[0-1] 数据长度、[2-3] 源端口(芯片小端)、 + * [4-7] 源 IP(线序字节,[4] = 第一段)。s_addr 直接按"首段落在低字节" + * 组装即得本层网络序约定(2026-10-24 定案:全仓 s_addr 只此一族, + * 与 net_inet_addr / net_getpeername 同族,可直接相等比较), + * **不能再加一次换端**。 + */ +static void net_udp_src_from_header(net_sock_t *p_sock, const uint8_t *hdr, + struct net_sockaddr *src_addr, int *addrlen) { + /* 可写判定:两个出参都到位且调用方给的容量足够 */ + int writable; + + (void)p_sock; + + writable = ((src_addr != NULL) && (addrlen != NULL) && + (*addrlen >= (int)sizeof(struct net_sockaddr_in))) ? 1 : 0; + if (writable == 0) { + return; + } + + { + /* IPv4 视图 */ + struct net_sockaddr_in *p_addr_in = (struct net_sockaddr_in *)src_addr; + + p_addr_in->sin_family = NET_AF_INET; + /* 源端口:芯片小端 → 主机序 → 本层网络序字段 */ + p_addr_in->sin_port = net_htons((uint16_t)((uint16_t)hdr[NET_UDP_HDR_PORT_OFF] | + ((uint16_t)hdr[NET_UDP_HDR_PORT_OFF + 1U] << 8))); + p_addr_in->sin_addr.s_addr = ((uint32_t)hdr[NET_UDP_HDR_IP_OFF]) | + ((uint32_t)hdr[NET_UDP_HDR_IP_OFF + 1U] << 8) | + ((uint32_t)hdr[NET_UDP_HDR_IP_OFF + 2U] << 16) | + ((uint32_t)hdr[NET_UDP_HDR_IP_OFF + 3U] << 24); + *addrlen = (int)sizeof(struct net_sockaddr_in); + } +} + +/* + * 函数功能:从控制块固定对端回填源地址(客户端模式:芯片不加信息头) + * 入口参数:p_sock - 控制块(remote_ip_arr/remote_port = 本 Socket 的对端) net_sock_t * + * src_addr - 调用方出参(可为 NULL) struct net_sockaddr * + * addrlen - 调用方出参长度(可为 NULL) int * + * 出口参数:src_addr - 填充对端地址;*addrlen = sizeof(sockaddr_in) + * 返回值:无 + * 限定条件:仅由 net_udp_datagram_read 在客户端模式分支调用 + * 函数说明:客户端模式下芯片只收这个 IP 的包,源地址不需要从头里取—— + * 反过来说它也**没有**头可取,本函数是唯一的源地址来源。 + */ +static void net_udp_src_from_peer(net_sock_t *p_sock, struct net_sockaddr *src_addr, + int *addrlen) { + int writable; + + writable = ((src_addr != NULL) && (addrlen != NULL) && + (*addrlen >= (int)sizeof(struct net_sockaddr_in))) ? 1 : 0; + if (writable == 0) { + return; + } + + { + /* IPv4 视图 */ + struct net_sockaddr_in *p_addr_in = (struct net_sockaddr_in *)src_addr; + + p_addr_in->sin_family = NET_AF_INET; + p_addr_in->sin_port = net_htons(p_sock->remote_port); + /* 对端字节已是线序数组,按内存顺位拷进 s_addr 即网络序整数 */ + (void)memcpy(&p_addr_in->sin_addr.s_addr, p_sock->remote_ip_arr, + sizeof(p_addr_in->sin_addr.s_addr)); + *addrlen = (int)sizeof(struct net_sockaddr_in); + } +} + +/* + * 函数功能:从芯片缓冲读出**一个完整数据报**(剥头 + 截断排空 + 清可读标志) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * p_sock - 控制块(读 udp_mode) net_sock_t * + * buf - 调用方缓冲 void * + * len - 缓冲容量(> 0) int + * src_addr - 源地址出参(可为 NULL) struct net_sockaddr * + * addrlen - 源地址出参长度(可为 NULL) int * + * avail - 调用方刚从芯片读到的可读字节数(> 0) int + * 出口参数:buf - 载荷(最多 len 字节,多余部分丢弃);src_addr - 发送方地址 + * 返回值:交付调用方的字节数(>= 0) int + * 限定条件:netTask 上下文;avail 必须是本函数调用前刚读到的 ch395f_get_recv_len() + * 函数说明:1. 服务器模式剥 8 字节信息头并从中取源地址;客户端模式整包都是 + * 载荷、源地址取固定对端。模式判错 = 吃掉载荷前 8 字节(本函数 + * 存在的理由,见 net_types.h UDP 模式注释)。 + * 2. 服务器模式手册要求"一次性将数据全部读出";且不论哪种模式, + * 载荷超出 len 时必须把剩余部分读空——留半包在芯片缓冲里, + * 下一次收包会从上一包的尾巴开始解析,源地址与载荷从此全部错位 + * (旧实现只读 min(可用, len),正是这个缺陷)。 + * 3. 服务器模式收到不足 8 字节的残包:无从判头,整包按载荷交付 + * (保持既有定案 TC-NETSOCK-440o,不猜芯片行为)。 + * 4. 长度由调用方传入、本函数**不再自己读第二次**:一次收包的剥头量、 + * 载荷量、排空量必须由同一个数字算出。读两次等于多一次 SPI,而且 + * 两次之间芯片缓冲可能已经变了(下一包已到),拿后一次的小值去剥 + * 前一次的包,头与载荷从此错位——正是本节第 2 条要避免的那类缺陷。 + */ +static int net_udp_datagram_read(int sockfd, net_sock_t *p_sock, void *buf, int len, + struct net_sockaddr *src_addr, int *addrlen, + int avail) { + /* 本包载荷字节数(剥头后) */ + int payload; + /* 交付调用方的字节数 */ + int copy; + /* 需排空的剩余字节 */ + int left; + + if (avail <= 0) { + return 0; + } + + if ((p_sock->udp_mode == NET_UDP_MODE_SERVER) && + ((uint32_t)avail >= NET_UDP_HDR_LEN)) { + /* 信息头暂存(只在服务器形态用得到,放在本分支栈上) */ + uint8_t hdr[NET_UDP_HDR_LEN]; + + /* 剥头:读即消费芯片缓冲 */ + ch395f_read_recv_buf((uint8_t)sockfd, hdr, (uint16_t)NET_UDP_HDR_LEN); + net_udp_src_from_header(p_sock, hdr, src_addr, addrlen); + payload = avail - (int)NET_UDP_HDR_LEN; + } else { + /* 客户端模式(或服务器模式残包):无头可剥,源地址取固定对端 */ + net_udp_src_from_peer(p_sock, src_addr, addrlen); + payload = avail; + } + + copy = (payload > len) ? len : payload; + if (copy > 0) { + ch395f_read_recv_buf((uint8_t)sockfd, (uint8_t *)buf, (uint16_t)copy); + } + + /* 截断剩余:读空丢弃,保证"一次调用 = 一个数据报"。 + * 暂存放本函数栈上(仅 netTask 上下文使用,netTask 栈 960 字); + * 不用 ch395f_clear_recv_buf——它清的是整个接收缓冲,会连带丢掉 + * 后续已到、调用方还没读的数据报。 */ + left = payload - copy; + if (left > 0) { + uint8_t scratch[NET_UDP_DRAIN_CHUNK]; + + /* 与 TCP 短写同一口径记 ERROR:静默丢字节最难查。 + * ⚠ 本仓无 DBG_WARN 宏(dbg_log.h 只有 ERROR/INFO/DEBUG),勿写 DBG_WARN */ + DBG_ERROR("sock%d udp datagram truncated %d->%d, %d bytes drained", + sockfd, payload, copy, left); + while (left > 0) { + uint16_t n = (left > (int)NET_UDP_DRAIN_CHUNK) + ? (uint16_t)NET_UDP_DRAIN_CHUNK + : (uint16_t)left; + + ch395f_read_recv_buf((uint8_t)sockfd, scratch, n); + left -= (int)n; + } + } + + /* 本包已消费完:清可读电平,否则 net_select 会永久报可读 */ + p_sock->readable = 0U; + p_sock->recv_bytes += (uint32_t)copy; + + return copy; +} diff --git a/Drivers/BSP/NET/net_udp.h b/Drivers/BSP/NET/net_udp.h new file mode 100644 index 00000000..52cf8bed --- /dev/null +++ b/Drivers/BSP/NET/net_udp.h @@ -0,0 +1,190 @@ +/* + * 模块名称:UDP 数据面执行器与挂起槽(netTask 串行化) + * 文件说明:UDP 三个碰芯片的动作(bind 打开、sendto、recvfrom)在 netTask 上下文 + * 里的实际执行体,加上 per-fd 的"一次接收请求"槽位(载荷缓冲 + 源地址 + * 出参 + 等待预算)。公开 API(net_sendto / net_recvfrom / + * net_recvfrom_to / net_udp_set_peer / net_udp_get_mode)住在 + * net_socket.h,那边只负责参数校验、装消息入队、等通知值回结果。 + * 为什么单独成模块:源地址(src_addr/addrlen)是**出参指针**,不能塞进按值拷入 + * 队列的 net_msg_t(AGENTS 明文「勿给 net_msg_t 加回传用字段」; + * 指针过 int 桥在 64 位主机单测构建还会被截断)。所以出参走本模块的 + * per-fd 槽:调用方入队前登记自己的指针,netTask 结算时把源地址写进 + * 调用方内存、错误码/字节数搭任务通知值回去——与 TCP 挂起 recv 把数据 + * 直接写进调用方 buf 是同一个模型(docs/网络/Net层阻塞模型架构设计.md)。 + * 依赖:net_types.h(net_sock_t / NET_UDP_MODE_*)、ch395f.h(芯片事务)、 + * net_socket.c(net_get_sock / net_poll / 消息分派 / 钩子调用点) + * 限定条件:除 net_udp_outparam_arm / net_udp_outparam_disarm / net_udp_clear( + * 纯 RAM,任意上下文)外,全部函数须 netTask 上下文 + * 关联文档:docs/网络/BSD_Socket_API_使用指南.md §6(UDP 两种芯片模式与队列化) + * 作者:super_agent_bot + * 创建日期:2026-11-03 + * 修改记录: + * 2026-11-03 super_agent_bot 创建:UDP 队列化改造(工单前置 = SNTP 客户端) + */ +#ifndef NET_UDP_H +#define NET_UDP_H + +#include +#include "FreeRTOS.h" +#include "task.h" +#include "net_types.h" + +/* + * 函数功能:登记本次接收的源地址出参(调用方上下文,入队**前**调用) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * src_addr - 源地址出参指针(可为 NULL = 不关心来源) struct net_sockaddr * + * addrlen - 出参长度指针(可为 NULL) int * + * 出口参数:无 + * 返回值:0 - 登记成功 int + * -1 - 失败(NET_ERR_BADF / NET_ERR_BUSY:本 fd 已有在途接收) + * 限定条件:任意上下文(临界区内检查并占位,与 netTask 的读改不竞态) + * 函数说明:占位成功后本 fd 的第二次接收请求会被拒(BUSY)——同一 fd 并发 + * recvfrom 在数据报语义上无法区分归属,拒绝比交错投递安全。 + * 调用方若入队失败必须 net_udp_outparam_disarm 归还槽位。 + */ +int net_udp_outparam_arm(int sockfd, struct net_sockaddr *src_addr, int *addrlen); + +/* + * 函数功能:归还源地址出参槽(调用方上下文,入队失败/取消后的清理) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * 出口参数:无 + * 返回值:无(越界或空槽静默返回) + * 限定条件:任意上下文;仅在**未收到 netTask 回执**的失败路径调用 + * 函数说明:netTask 结算路径自己清槽(幂等),本函数只服务调用方侧的 + * "消息没投递出去"这一条出口,防槽位泄漏导致该 fd 永久 BUSY。 + */ +void net_udp_outparam_disarm(int sockfd); + +/* + * 函数功能:UDP 绑定/打开芯片侧 Socket(netTask 上下文执行体,恒服务器模式) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * local_port - 本地(源)端口,主机序;0 = 由本函数分配动态端口 uint16_t + * 出口参数:无 + * 返回值:0 - 成功 int + * -1 - 失败(NET_ERR_BADF 描述符非法 / NET_ERR_INVAL 非 DGRAM / + * NET_ERR_BUSY 芯片 open 被拒;errno 已记) + * 限定条件:netTask 上下文;sockfd 对应控制块在用且为 DGRAM + * 函数说明:1. 序列按手册 §9.2.4:Socket 4~7 兜底重设收发缓冲 → SET_PROTO_TYPE(UDP) + * → SET_SOUR_PORT → SET_DES_IP(0xFFFFFFFF) → SET_DES_PORT(0) → OPEN_SOCKET。 + * 2. open 之后写端口/IP 会被 open 复位,故目的地址必须在 open 前下发 + * (docs/网络/BSD_Socket_API_函数调用关系.md §4 调用链表)。 + * 3. DesIP 恒取 0xFFFFFFFF = **服务器模式**:BSD 的 bind 只表达本地地址、 + * 不选对端,客户端模式由 net_udp_set_peer_exec / sendto(带目的地址) 进入。 + * 4. 动态端口分配在本函数内做(原在调用方上下文的 net_bind 里写 + * s_dynamic_port——芯片事务既然挪进 netTask,端口簿记一并挪, + * 避免两个上下文改同一个静态量;取号游标经 net_dynamic_port_next 共用)。 + * 5. 顺带本 fd 生命周期收口:清挂起接收槽、udp_mode 置 SERVER、清对端 + * 簿记、清 readable(重开不得继承上一条连接的残留电平)。 + */ +int net_udp_bind_exec(int sockfd, uint16_t local_port); + +/* + * 函数功能:设置芯片 DesIP/DesPort 并切换 UDP 模式(不重开 Socket,netTask 执行体) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * peer - 非 NULL = 客户端模式(对端);NULL = 服务器模式 const struct net_sockaddr_in * + * 出口参数:无 + * 返回值:0 - 成功 int + * -1 - 失败(NET_ERR_BADF / NET_ERR_INVAL,errno 已记) + * 限定条件:netTask 上下文;控制块在用、type == DGRAM、芯片侧 Socket 已打开 + * 函数说明:公开语义见 net_socket.h net_udp_set_peer。只写两个寄存器 + * (SET_DES_IP + SET_DES_PORT,约数十微秒),不 CLOSE/OPEN, + * 故接收缓冲里已到的数据不丢。 + */ +int net_udp_set_peer_exec(int sockfd, const struct net_sockaddr_in *peer); + +/* + * 函数功能:UDP 发送执行体(netTask 上下文,分块 + 有界自旋 + 挂起续传) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * buf - 载荷(调用方内存,须有效至完成) const void * + * len - 载荷长度(>0) int + * dest - 本包目的地址;NULL = 不改 DesIP const struct net_sockaddr_in * + * caller - 调用方任务句柄(挂起 send 的完成通知目标) TaskHandle_t + * 出口参数:无 + * 返回值:实际发送字节数(>= 0);NET_PENDING_REGISTERED = 已登记挂起 send; + * -1 - 失败(NET_ERR_INVAL / NET_ERR_BADF,errno 已记) int + * 限定条件:netTask 上下文 + * 函数说明:1. dest 非 NULL → 先 SET_DES_IP/SET_DES_PORT,并把 udp_mode 置 + * CLIENT、对端簿记写入 remote_ip_arr/remote_port(芯片静默进 + * 客户端模式是硬件事实,本层必须如实记账,否则收包剥头判错)。 + * 2. 分块与等待语义与 net_send_locked 完全同源(NET_SEND_CHUNK_MAX / + * NET_SEND_SPIN_MS / NET_SEND_TIMEOUT_MS / 末块不等 SENBUF_FREE / + * 短写留痕),复用挂起 send 表推进——**不复制一份 UDP 专用自旋**, + * 两条路径分块粒度漂移就是 TRAP-NET-16 的复发形态。 + * 3. 载荷指针在挂起期间由调用方持有(调用方阻塞在 net_wait_result, + * 栈与静态缓冲均有效),与 TCP 挂起 send 同一条铁律。 + */ +int net_udp_sendto_exec(int sockfd, const void *buf, int len, + const struct net_sockaddr_in *dest, TaskHandle_t caller); + +/* + * 函数功能:UDP 接收执行体(netTask 上下文:即时读 / 登记挂起 / 立即拒) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * buf - 调用方缓冲 void * + * len - 缓冲容量(>0) int + * flags - NET_MSG_DONTWAIT = 单次非阻塞 int + * to_ms - 等待预算(0 = 无限等待,须 blocking 属性) uint32_t + * caller - 调用方任务句柄(挂起完成通知目标) TaskHandle_t + * 出口参数:无(源地址经 net_udp_outparam_arm 登记的指针直接写回调用方内存) + * 返回值:实际接收字节数(> 0);NET_ERR_RETURN_1(WOULDBLOCK) = 非阻塞无数据; + * NET_PENDING_REGISTERED = 已登记挂起;-1 = 失败 int + * 限定条件:netTask 上下文;控制块在用且为 DGRAM + * 函数说明:1. 判据 = ch395f_get_recv_len()(芯片事实),readable 仅作省 SPI 的 + * 门(同 net_pending_tick:事件未收到时 net_poll 每轮重读电平型 SINT + * 会重新置位,U15 探针同源结论)。 + * 2. 服务器模式(udp_mode == SERVER)且本包 >= 8 字节 → 先读 8 字节 + * 信息头解析源地址再读载荷;客户端模式 → 整包都是载荷,源地址 = + * 本 Socket 固定对端(remote_ip_arr/remote_port)。 + * 3. **一个数据报一次读完**:可用字节 > len 时读满 len 后把剩余部分 + * 读空丢弃(BSD 截断语义;手册 §9.2.4 对服务器模式要求一次性全部 + * 读出)。旧实现只读 min(可用, len) 把半包留在芯片缓冲里,下一次 + * 收包会把上一包的尾巴当成新包头的起点——整条链路的源地址从此错位。 + * 4. 读空后清 readable(旧实现从不清,net_select 会永久报可读)。 + * 5. 无数据 + 阻塞 → 登记挂起(同 fd 已有挂起 / 执行任务自身 → 拒绝, + * BUSY),netTask 零占用返回;完成条件由 net_udp_tick 判定。 + */ +int net_udp_recvfrom_exec(int sockfd, void *buf, int len, int flags, + uint32_t to_ms, TaskHandle_t caller); + +/* + * 函数功能:挂起 UDP 接收完成检查(netTask 每轮 net_process_messages 调用一次,非阻塞) + * 入口参数:p_socks - Socket 控制块表首指针(net_socket.c 的 s_net_socks) net_sock_t * + * 出口参数:无 + * 返回值:无 + * 限定条件:netTask 上下文 + * 函数说明:逐 fd 按序判定:①fd 已关闭/已非 UDP → 以 NET_ERR_CONNRESET 结束(必通知) + * ②数据到达 → 读包(含剥头/截断/清 readable)+ 回填源地址 + 以字节数结束 + * ③到点(仅显式预算)→ 以 NET_ERR_TIMEDOUT 结束(调用方 net_recvfrom_to + * 映射为对外 -2)。三条出口都必须经统一出口通知,漏通知 = 调用方永久挂死。 + */ +void net_udp_tick(net_sock_t *p_socks); + +/* + * 函数功能:清除本 fd 的 UDP 接收槽(不通知;"新 fd 不得继承"与 close 后幂等清理) + * 入口参数:sockfd - Socket 描述符 (0~7) int + * 出口参数:无 + * 返回值:无(越界静默返回) + * 限定条件:任意上下文(netTask 的 close/socket 分配路径调用) + */ +void net_udp_clear(int sockfd); + +/* + * 函数功能:取消全部挂起 UDP 接收并逐一通知(网络重建/复位路径) + * 入口参数:err - 通知的错误码 int + * 出口参数:无 + * 返回值:无 + * 限定条件:netTask 上下文;须在控制块清零与芯片复位之前调用 + */ +void net_udp_cancel_all(int err); + +/* + * 函数功能:关闭前结算本 fd 的挂起 UDP 接收(有数据先送达,无数据则取消) + * 入口参数:sockfd - 将被关闭的描述符 (0~7) int + * p_sock - 将被关闭的控制块指针(与 sockfd 对应) net_sock_t * + * 出口参数:无 + * 返回值:无(无在位挂起时静默返回) + * 限定条件:netTask 上下文;须在 ch395f 关闭/复位该 Socket 之前调用 + * 函数说明:与 net_pending_settle_close 同一契约——关闭不丢已到达数据 + */ +void net_udp_settle_close(int sockfd, net_sock_t *p_sock); + +#endif /* NET_UDP_H */ diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index 791de211..0ddd0d41 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -1673,6 +1673,11 @@ 1 ..\Drivers\BSP\NET\net_pending.c + + net_udp.c + 1 + ..\Drivers\BSP\NET\net_udp.c + net_cfg.c 1 diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 474e7877..254e91cd 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -47,10 +47,10 @@ dhara 失败类 +84;RO 另 +208,全是该失败类名表)→ `ER_IROM1` | 区域 | 用量 | 容量 | 占比 | |---|---|---|---| -| ER_IROM1 | 277,384 B (270.88 KB) | 1,048,576 B (1024 KB) | 26.5% | -| RW_IRAM1 | 94,368 B (92.16 KB) | 131,072 B (128 KB) | 72.0% | +| 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 合计)** | **279,436 B (272.9 KB)** | 1,048,576 B (1024 KB) | **26.6%** | +| **Flash (ROM 合计)** | **281,012 B (274.4 KB)** | 1,048,576 B (1024 KB) | **26.8%** | 各区的容量与常量来源: diff --git a/docs/内存管理/RAM预算台账.md b/docs/内存管理/RAM预算台账.md index 4a015430..ab42836a 100644 --- a/docs/内存管理/RAM预算台账.md +++ b/docs/内存管理/RAM预算台账.md @@ -15,14 +15,14 @@ | 区域 | 用量 | 容量 | 占比 | 门禁(≤92%) | |---|---|---|---|---| -| ER_IROM1 (Flash) | 277,384 B | 1,048,576 B | 26.5% | 参考 | +| 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,368 B | 131,072 B | 72.0% | PASS | +| RW_IRAM1 | 94,624 B | 131,072 B | 72.2% | PASS | | 子系统(静态 RW+ZI;除 RTOS 堆在 CCM 外均在 SRAM1) | 现值 | 配额 | 状态 | |---|---|---|---| -| 网络 | 13,702 B | 12 KB | ⚠ 超 1.4 KB | +| 网络 | 13,958 B | 12 KB | ⚠ 超 1.6 KB | | 存储 | 7,072 B | 12 KB | OK | | UI | 36,473 B | 16 KB | ⚠ 超 19.6 KB | | RTOS 堆 | 57,376 B | 56 KB | 堆=配额 | diff --git a/docs/子系统设计/系统墙钟时间维护说明.md b/docs/子系统设计/系统墙钟时间维护说明.md index 2478a644..f2a6387f 100644 --- a/docs/子系统设计/系统墙钟时间维护说明.md +++ b/docs/子系统设计/系统墙钟时间维护说明.md @@ -385,7 +385,7 @@ for (;;) { - 所有 socket 操作(`net_socket` / `net_bind` / `net_listen` / `net_accept` / `net_recv` / `net_close`)均走 `net_socket` 的**线程安全 API**,内部经消息队列由 `netTask` 串行执行; - netTask 因此只需做 `net_poll()` + `net_process_messages()`,不掺杂任何应用层网络逻辑; -- **TCP 的全部 API 已经是基于消息队列的**(已在 `net_socket.c` 实现),而 UDP 的 `net_bind` / `net_sendto` / `net_recvfrom` 绕过队列。选用 TCP 即可让时间同步作为独立任务运行,且**无需改动 `net_socket.c` 的 UDP 路径**。 +- **TCP 的全部 API 已经是基于消息队列的**(已在 `net_socket.c` 实现);UDP 的 `net_bind` / `net_sendto` / `net_recvfrom` **曾经**绕过队列,**2026-11-03 已并入队列**(改造事实见 `docs/网络/BSD_Socket_API_使用指南.md` §9.0)。也就是说:**"UDP 不安全"不再是本节选 TCP 的理由**——本节结论不变(现有 TCP:8888 时间同步通道继续有效),但若日后要换成 UDP(#57 SNTP 方向),不再需要先改 net 层。 ### 15.5 Socket 分配与冲突 diff --git a/docs/测试说明/单元测试/主机单测覆盖率口径.md b/docs/测试说明/单元测试/主机单测覆盖率口径.md index 19c6c709..609ea820 100644 --- a/docs/测试说明/单元测试/主机单测覆盖率口径.md +++ b/docs/测试说明/单元测试/主机单测覆盖率口径.md @@ -503,7 +503,7 @@ uint16_t ch395f_link_change_count(void); | 2 | `App/log/sys_log_mcu.c:527~529` | `rc==-1` 打 `DBG_ERROR("drop %lu entries …")`,但该分支 `done=0` → 整批其实**全部回投**、一条没丢 → 文案与行为相反,且持续写故障时每轮 drain 都重投重试一遍 | TC-SYSLOG-036 | | 3 | `App/log/sys_log_mcu.c:616` vs `627~628` | 守卫写成 `desc_len <= SYSLOG_DESC_MAX` 就 `-1`,而 `dl` 恒 ≤ 28 → 截断分支**永不可达**。无害(宁可拒绝不越界),但"传小缓冲能拿到截断内容"的直觉是错的 | TC-SYSLOG-098 / 057 | | 4 | `Drivers/BSP/NET/net_socket.c:2825` / `:2869` / `:3080` | **同一文件两套 IP 字节序约定,`inet_addr` 与 `ntoa`/`getsockname` 不互逆**:`net_inet_addr` 小端组装后又 `NET_HTONL` → `0xC0A80164`;`net_inet_ntoa` 交换后高段在前 → 只能正确还原 `getsockname` 那族(`0x6401A8C0`);`net_connect_locked:1571` 用 `>>24` 取首段 → 只配 `inet_addr` 那族。**为什么长期不被看见**:`ntoa` 自己 `:2865~2867` 那段 E13 修复注释里的例子「192.168.1.100 → 0xC0A80164 → "192.168.1.100"」取的是**另一族**的值(交换宏下实算 `"100.1.168.192"`)。前提已核清:`Drivers/BSP/NET/net_types.h`:56 是 `#ifdef __BIG_ENDIAN` 才恒等,STM32 与 x86/MSVC 都走交换分支 → **不存在"主机语义≠目标语义"**。当前无调用者跨族(`lftpd.c`:864 走 `getpeername→ntoa` 同族恰好正确),属埋在库 API 面上的地雷;统一必须跨 `inet_addr`/`ntoa`/`getsockname`/`connect` 解析/`lftpd.c`:548 一起过 | TC-NETSOCK-100b / 100c / 100d、107f、430h(钉的是**具体数值**,统一时会五条一起红并互相指认,不会被误读成单点回归) | -| 5 | `Drivers/BSP/NET/net_socket.c:2408` / `:2537` | **UDP 两个公开 API 绕过 netTask 串行化**:`net_sendto`/`net_recvfrom` 直接 `net_poll()`(`:2482`/`:2501`/`:2563`)+ `ch395f_write_send_buf`(`:2487`)/`ch395f_read_recv_buf`(`:2575`/`:2604`),**全程不经 `netMsgQueue`** → 与 AGENTS「所有 `net_*` 经 netTask 串行执行」及 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-14 正面冲突。全仓无生产调用者(只有 `test/board` 与模拟器)所以未爆。建议:改走消息队列,或在 `net_socket.h` 显式标注"未串行化,仅限 netTask" | TC-NETSOCK-440 | +| 5 | `Drivers/BSP/NET/net_socket.c:2408` / `:2537` | **UDP 两个公开 API 绕过 netTask 串行化**:`net_sendto`/`net_recvfrom` 直接 `net_poll()`(`:2482`/`:2501`/`:2563`)+ `ch395f_write_send_buf`(`:2487`)/`ch395f_read_recv_buf`(`:2575`/`:2604`),**全程不经 `netMsgQueue`** → 与 AGENTS「所有 `net_*` 经 netTask 串行执行」及 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-14 正面冲突。全仓无生产调用者(只有 `test/board` 与模拟器)所以未爆。建议:改走消息队列,或在 `net_socket.h` 显式标注"未串行化,仅限 netTask"。**处置(2026-11-03):已按前者落地**——UDP 全面走 `netMsgQueue`,出参经 per-fd 槽回传,见本文「第 4 轮:UDP 数据面队列化(2026-11-03)」 | TC-NETSOCK-440 | | 6 | `App/log/soe_store.c` 函数头 ④ | 注释写"打开/写入结构性错误 → `unlink` 重试一次(同 run.log)",实现里 `f_open` 失败**直接 return -1、不重试**(只有 `f_write` 失败才重试);且 `run.log` 侧 `s_append_rec` 的重试码集合还含 `FR_INVALID_NAME`/`FR_NO_PATH` → 两套自愈集合不一致。属"注释夸大实现"还是"实现少一半"待裁 | TC-SOESTORE-022 | | 7 | `App/cfg/cfg_file_store_mcu.c:50` | 函数头写"未挂载时 `f_open` 返回 `FR_NO_SYSTEM`"——**`FRESULT` 里没有这个码**(`Lib/FatFs/ff.h` 共 20 个码,未挂载实际是 `FR_NOT_READY`/`FR_NO_FILESYSTEM`)。只在注释里不参与编译,危害是后人照它写判据会写出永不成立的分支 | —(注释面) | | 8 | `App/log/console_log.c:317` vs `:285` | 冷却起点用 `s_con_cool_until = (uint32_t)HAL_GetTick()`,而判据拿 `!= 0U` 当哨兵 → 故障恰好落在 `HAL_GetTick()==0` 那一拍(开机头 1 ms,或 49.7 天回绕到 0)时**冷却等于没记**,下一轮 drain 立刻再耗一次 100 ms 块超时。撞的正是控制台域那条已登记的"背靠背块超时拖长主循环 → 喂狗节奏饥饿"。建议:另用一个"在冷却"标志,别拿 tick 值当哨兵 | TC-CONLOG-081 / 082(按现状钉) | @@ -526,18 +526,18 @@ uint16_t ch395f_link_change_count(void); | 6 soe_store 注释夸大实现 | **已修(改注释,实现不动)**:打开失败**任何错误码都不 unlink** 是有意的——soe.log 是事件序列,「打开失败就删文件」等于销毁事件证据;两侧资料块各写一份理由并**互指对方文件与测试号**,要求同步修改 | TC-SOESTORE-022 | | 7 FR_NO_SYSTEM 不存在 | **已修**:改成实际会返回的 `FR_NOT_READY(3)` / `FR_NO_FILESYSTEM(13)`,并写明「本层对任何非 FR_OK 一律转 -1,别去枚举错误码集合」,唯一例外指向自愈触发集 | 注释面(无行为改动) | | 4 IP 两套字节序 | **已修(统一到 POSIX 网络序)**:改 `net_inet_addr`(`&s_addr[0] == 第一段`,小端上 192.168.1.100 = `0x6401A8C0`)+ `net_connect_locked`/`net_sendto` 的 IP 数组改按内存字节顺位拆装(**进与出同时换族 → 芯片侧数组净效果不变**,由 550h 直接钉住)+ 模拟器 `net_socket_sim.c` 逐字同语义;`net_inet_ntoa` **函数体一字未动**,只把它注释里那个取了废弃族值的例子改对(就是那条隐蔽原因)。lftpd 两处经算术核对**确认无需改**(PASV 227 的双重反转仍成立),只加「勿顺手改对」的注释 | TC-NETSOCK-100 组(新增往返不变量 100g/100h、边界 100i/100j、截断 100k)、550h | -| 5 UDP 绕过 netTask | **危险面已结构性关掉,串行化改造仍未做**(按 Lead 定案,未自升级为改队列):两个 UDP 入口加**执行任务上下文守卫**,非 netTask 调用回新码 `NET_ERR_NOT_IN_NETTASK = -12`(住 `net_types.h`,与 BUSY/TIMEDOUT 的本质差别 = **重试永远无意义**,不参与任何可重试判定);守卫位于参数校验之后、任何芯片事务之前;模拟器**刻意不加**并标注为有意的不对称(单线程 + Winsock 自身线程安全、没有那根 SPI)。**附带关掉第二条更隐蔽的危险**:非 netTask 在 recvfrom 里自旋期间,netTask 可经 close 清零控制块并把同一 sockfd 分给新会话 → 旧自旋者消费新会话数据报,这条路现在断了 | TC-NETSOCK-560 组(含「零芯片事务」与「守卫不吞正常错误路径」) | +| 5 UDP 绕过 netTask | **2026-10-24 那轮只关掉了危险面,串行化改造本身已于 2026-11-03 落地**:两个 UDP 入口不再"非 netTask 就拒",而是与 TCP 同构走 `netMsgQueue`(执行体拆到新文件 `Drivers/BSP/NET/net_udp.c`);`NET_ERR_NOT_IN_NETTASK = -12` 保留为**历史码**——全仓已无运行时返回点,只剩注释说明它为什么不再可能发生。**顺带修掉一条当年看不见的问题**:`net_sendto` 为发一个包把 DesIP 从 `0xFFFFFFFF` 改成具体 IP,芯片就此静默切到 UDP Client 形态(收包**不再带 8 字节信息头**),而 `net_recvfrom` 仍按服务器形态无条件剥 8 字节 → 请求-响应式协议(SNTP 48 字节应答)会**丢掉载荷前 8 字节**并交出垃圾源地址。现已按形态分路。当年守卫顺带关掉的"自旋期间 netTask 把同一 sockfd 分给新会话 → 旧自旋者消费新会话数据报",现由"只有 netTask 读芯片 + 每 fd 一个排他接收槽"结构性排除 | `ns_s7_udp.c` UDP-1~14 组 + 改写后的 TC-NETSOCK-440 / 555 / 560 组("非 netTask 调用零芯片事务"仍逐条钉住) | | 8 冷却哨兵与 tick 撞值 | **已修**:新增独立 `s_con_cooling` 判定「是否在冷却」,`s_con_cool_until` 只存截止时刻(无符号差跨回绕的写法保留) | TC-CONLOG-080~084、107、108 | | 9 环空位判据在临界区外 | **已修**:「复核空位 + 整行拷贝 + 推进 head」收进同一个临界区(最长 192 B、168 MHz 下亚微秒;`vTaskDelay` 与 DWT/tick 判据一律留区外,区内绝不睡眠)→ 多生产者结构性安全,写者仍唯一。**原「这条主机测不出来」的结论本轮作废**:桩里记临界区嵌套深度并把第二生产者精确塞进第一个生产者的临界区,再把环形缓冲的宏名成函数调用使每一次碰环都可断言 | TC-CONLOG-160~167(**167 是正控**,证明 160~166 不是空转) | | 10 未初始化被误当僵尸通道 | **已修**:drain 的失败处理先分诊——`Instance == NULL` 视为通道不可用(不 reinit、不计 tx_fault、不进冷却、只记 dropped);只有确认超时/僵尸通道才走软复位自愈 + 冷却 | TC-CONLOG-103~106 | -| (撤销)非阻塞 recv 看不见 EOF | **误报,未改任何码**:`net_recv_locked:1885` 的非阻塞分支里 `CLOSED || session_dead` 已判 EOF(0),只有会话存活才 WOULDBLOCK。顺带核实 `net_recvfrom` **不存在** EOF 误判面(UDP 无连接,无数据只有 WOULDBLOCK 或继续自旋两条出口) | —(记一次「描述得很合理」的误报:修之前先读码,误报比漏报更贵) | +| (撤销)非阻塞 recv 看不见 EOF | **误报,未改任何码**:`net_recv_locked:1885` 的非阻塞分支里 `CLOSED || session_dead` 已判 EOF(0),只有会话存活才 WOULDBLOCK。顺带核实 `net_recvfrom` **不存在** EOF 误判面(UDP 无连接,无数据只有 WOULDBLOCK 或继续自旋两条出口;2026-11-03 队列化后"继续自旋"改为"登记挂起等 tick 结算",本条结论不变) | —(记一次「描述得很合理」的误报:修之前先读码,误报比漏报更贵) | **修复过程中顺带照出的两条(原表没有,一并修掉)**: - `syslog_post` 的 `queue full, drop` / `preboot full, drop` **无节流**——1 号修好之后「整批回投 → 队列常驻满」成为必然场景,不修等于**用一个新刷屏换掉一个静默丢**。已按同一「正常→故障」纪律加两个独立抑制位(**丢弃语义一字未改**,满则丢照旧)。TC-SYSLOG-041b/041d/041e/041f、105、106、107、107b、107c。 - 自愈触发集里混着 `FR_INVALID_NAME` / `FR_NO_PATH` / `FR_NO_FILE` / `FR_INVALID_OBJECT`——**比原报告的 6 号严重**:这类码下 unlink 自己的文件必然同样失败(同名同因),后果是**每轮排空删一次自己的历史日志且永远修不好**,正是 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1 第 10 条严禁的归集。已新增代码侧唯一入口 `s_is_selfheal_code()`:触发集收敛为 `{FR_DISK_ERR, FR_INT_ERR, FR_NO_FILESYSTEM, FR_DENIED}`,逐码依据(含 `Lib/FatFs/ff.c` 行号)与显式排除清单写在资料块里;`soe_store.c` 侧同源但**取更保守子集**。TC-SYSLOG-101/101b/102/103/103b、TC-SOESTORE-022b。 -**本轮刻意没做(后续项,含理由)**:① UDP 改走 `netMsgQueue`(需先设计 per-task 结果槽:`net_msg_t` 按值拷入队列、错误码只搭任务通知值回传,AGENTS 明文禁止给 `net_msg_t` 加回传字段,且必须配真机回归); +**本轮刻意没做(后续项,含理由)**:① UDP 改走 `netMsgQueue`(当年列的前置约束——`net_msg_t` 按值拷入队列、错误码只搭任务通知值回传、AGENTS 明文禁止给 `net_msg_t` 加回传字段,**至今成立**)→ **已于 2026-11-03 落地**:出参槽放进新模块 `net_udp.c` 内部,`net_msg_t` 一字未加;**真机回归仍待做**,见本文「第 4 轮:UDP 数据面队列化(2026-11-03)」的台架清单; ② `simulator/sim/sim_syslog.c:171` 那条与 3 号**同源的不可达截断支**(本轮编译验证不到它——模拟器走 CMake+VS,不在 `/keil-build` 覆盖内,不交没编译过的改动);③ `syslog_post` 的**丢弃计数**(属新增对外可见信息,本轮没有实测频率支撑阈值); ④ `dbg_console_tx_timeout()` 的**读端接线**(现无生产调用者;候选位置 `task_watchdog.c` 的 `STALL snap=… req=… h=… mg=… cf=… seg=…` 是**被解析的证据行**,加字段要连解析端与台架判据一起改,本批不碰台架);⑤ 给 104 加来源地址日志(现网 `iec104_serv.c` 根本不打印来源 IP、无 `getpeername/ntoa`,**没有输出点可看**)。 @@ -657,7 +657,7 @@ uint16_t ch395f_link_change_count(void); ⚠ 结构体成员访问本身(`u->SR`/`u->DR`)钩不到,别绕进去。 3. **跑飞保险要挂在被测模块的天然计数点上**:`net_send_locked`/`net_sendto`/`net_recvfrom` 的等待环里 **没有 `osDelay`**,只给 `osDelay` 加保险挡不住这些死循环(实测第一次跑就真卡死)。挂在 `net_poll()` - 每轮必读的 `HAL_GPIO_ReadPin`(INT#)上,附带让 `NET_SEND_SPIN_MS` 这类有界自旋预算**第一次可量测**。 + 每轮必读的 `HAL_GPIO_ReadPin`(INT#)上,附带让 `NET_SEND_SPIN_MS` 这类有界自旋预算**第一次可量测**(2026-11-03 更新:UDP 收包已不再在调用方自旋,改为登记挂起、由 `net_udp_tick` 结算——同段那条"泵 50 轮仍无通知就 FATAL"的纪律因此成了主力把关:漏一次 `xTaskNotify` 当场红,而不是慢慢卡死)。 同一纪律的另一半:假内核对 `xTaskNotifyWait(portMAX_DELAY)` 泵 50 轮仍无通知就 `exit(2)` 打 `FATAL` —— 它当场抓出一处把 `net_close` 返回语义判错的用例,而不是让门禁卡死。 4. **未插桩全绿、插桩后 `0xC0000005`** 基本都是测试自己的越界读:假 UART 的上线字节日志是二进制缓冲没补 @@ -710,6 +710,23 @@ uint16_t ch395f_link_change_count(void); - 本轮改动面:`App/log/sys_log_mcu.c`(条目结构 + 投递拷贝)、`App/log/sys_log.h`(参数契约)、`test/host/sys_log_mcu/sys_log_mcu_test.c`(新增 2 条断言 + `MQ_MAXMSG`)、`scripts/archive/id_gate/id_registry.json`(游标 `TC-SYSLOG-108`)。 - **台架与临时改动已全部还原**:钩子、负向对照、套件宏逐一反向删除(`git status` 归零),设备上 `BENCH.STS` / `BENCH2.STS` / `B5FILL.STS` 与含钩子记录的 `SELFTEST.LOG` 已删除,产品固件重建后重烧并回读校验通过。 +### 第 4 轮:UDP 数据面队列化(2026-11-03) + +前置背景:`#57` SNTP 调研把"UDP 先串行化"列为 SNTP 落地的第一步——`net_sendto` / `net_recvfrom` 直调 SPI 且自旋收包,既违反"netTask 唯一芯片访问者",又把 IWDG 预算吃在自旋上。 + +- **改造面**:新增 `Drivers/BSP/NET/net_udp.c/.h`(真机专属,模拟器不编译)承接 4 个执行体与挂起 recv;`net_msg_type_t` 加 `NET_MSG_UDP_BIND / UDP_SENDTO / UDP_RECVFROM / UDP_PEER` 四型;公开 API 一律"入队 → 等通知"。`net_init` 重建、`net_socket` 分配 fd、`net_close`、掉线与对账复位四处接上 `net_udp_cancel_all / clear / settle_close`,`net_process_messages` 的第 5 个 tick 位挂 `net_udp_tick`。 +- **源地址怎么回来(AGENTS 硬约束)**:`net_msg_t` 按值拷入队列、明文禁止加"回传用"字段 → 出参指针**不进消息**,改住 `net_udp.c` 的 per-fd 槽:调用方 `arm`(临界区只包"查占位 + 占位 + 记 owner"三步)、入队失败立刻 `disarm`、netTask 执行体经该指针就地写回调用方自己的内存,通知值只带结果码。互斥 + 泄漏 = 该 fd 永久 `BUSY`,所以执行体**每条出口**都清(UDP-7d / UDP-9e 钉住)。 +- **不写第二份分块逻辑**: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` 共 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 只是归零**。 +- **本轮覆盖率**(收尾那次全仓跑数,`--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 侧对照)。 +- 本轮改动面:`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/网络/BSD_Socket_API_使用指南.md b/docs/网络/BSD_Socket_API_使用指南.md index 6f8bc330..daa4c192 100644 --- a/docs/网络/BSD_Socket_API_使用指南.md +++ b/docs/网络/BSD_Socket_API_使用指南.md @@ -65,6 +65,7 @@ | `net_socket.c` | API 核心实现:消息队列处理与线程安全封装、内部 `_sock()` 版本;另含挂起 recv 登记路径 | | `net_events.c`(**真机专属**) | **状态面收敛**(2026-10-14 从 `net_socket.c` 抽出,与 `net_listener.c` 同族):5 个 `handle_*_event`(`net_poll` 检测中断后的分派目标)+ `fire_event` + TRAP-NET-17 周期对账 + 监听重建**合一** reconciler(`net_reconcile_tick`,100ms,分级去抖+冷却,形态①②③④⑤/U11)+ `net_lost_connect_probe`;**只置事实与登记意图**,重建动作在 `net_listener_tick`;内部头 `net_events.h`,模拟器不编译 | | `net_listener.c`(**真机专属**) | **监听生命周期**(E17 单一所有者收口):`net_listener_request`(**只登记意图**,5 个登记点)/ `net_listener_tick`(**唯一执行者**,门集 + D10 重试 30ms/2s)/ `net_listener_clear` / `net_listener_settle`(沉降/隔离 100ms/400ms);"该不该重建"的周期判据在 `net_reconcile_tick` 形态③④(单一 100ms tick);内部头,模拟器不编译 | +| `net_udp.c`(**真机专属**) | **UDP 数据面执行体**(2026-11-03 队列化改造新增):`net_udp_bind_exec` / `net_udp_set_peer_exec`(切芯片形态,不重开 Socket)/ `net_udp_sendto_exec`(**复用** `net_send_locked` 的 1KB 分块与挂起 send)/ `net_udp_recvfrom_exec` + `net_udp_tick`(挂起 recv 完成 / 预算超时)+ per-fd 出参槽(源地址写回调用方自己的内存,不占 `net_msg_t`)+ 截断排空;内部头 `net_udp.h`,模拟器不编译;用法与约束见 §6.0 / §6.3,调用结构见 `docs/网络/BSD_Socket_API_函数调用关系.md` §4.11 | | `net_select.h` | select/poll API 头文件 | | `net_select.c` | select/poll 实现(仅 MCU 端编译) | | `net_socket_sim.c`(`simulator/sim/`) | Winsock2 后端(模拟器端,与 `net_socket.c` 互斥编译),详见 §2.3 | @@ -156,10 +157,10 @@ sequenceDiagram | 规则 | 说明 | |------|------| -| **`netTask` 外(任意应用任务)** | 公共 API:`net_send()` / `net_recv()` / `net_recv_to()` / `net_accept()` / `net_connect()` / `net_close()` / `net_listen()` / `net_drop_conn()`——请求入队列,netTask 串行执行,调用方阻塞等回执。**自愈无显式 API**(`net_relisten()` / `net_resync_listen()` 已退役,静默回收/监听失位由对账⑤与 reconciler 形态③④自动认领,见 §3.6) | +| **`netTask` 外(任意应用任务)** | 公共 API:`net_send()` / `net_recv()` / `net_recv_to()` / `net_accept()` / `net_connect()` / `net_close()` / `net_listen()` / `net_drop_conn()`——请求入队列,netTask 串行执行,调用方阻塞等回执。UDP 侧同一口径:`net_bind()`(UDP) / `net_sendto()` / `net_recvfrom()` / `net_recvfrom_to()` / `net_udp_set_peer()`(2026-11-03 队列化改造,见 §6)。**自愈无显式 API**(`net_relisten()` / `net_resync_listen()` 已退役,静默回收/监听失位由对账⑤与 reconciler 形态③④自动认领,见 §3.6) | | **`netTask` 内** | 内部 API `net_send_sock()` / `net_recv_sock()` / `net_close_sock()` / `net_listen_locked()` | -| **`net_socket()` / `net_bind()`** | 直接调用,无需队列;实际在就绪屏障后的服务任务(timeSyncTask / ftpTask / uiTask)中创建监听 | -| **`net_poll()` 和 `net_process_messages()`** | 必须在 `netTask` 主循环调用;`net_process_messages()` 每轮先跑 **4 个非阻塞 tick**(D10 重监听重试 → 挂起 recv 完成 → 挂起 send 推进 → TRAP-NET-17 对账+监听重建合一 reconciler 100ms),再处理队列 | +| **`net_socket()` / `net_bind()`** | `net_socket()` 纯 RAM(分配控制块),任意任务直接调用;`net_bind()` 分两支——**TCP** 只记本地端口,仍是直接调用;**UDP** 要开芯片侧 Socket,走队列在 netTask 里执行(要发 5 条芯片命令,见 §6.0) | +| **`net_poll()` 和 `net_process_messages()`** | 必须在 `netTask` 主循环调用;`net_process_messages()` 每轮先跑 **5 个非阻塞 tick**(D10 重监听重试 → 挂起 recv 完成 → 挂起 send 推进 → **UDP 挂起 recv 完成/超时** → TRAP-NET-17 对账+监听重建合一 reconciler 100ms),再处理队列 | ### 3.4 内部 API 说明 @@ -440,8 +441,29 @@ void tcp_client(void) ## 6. UDP 编程 -> **本项目不使用 UDP(2026-10-15 定案)**:UDP API 保留为 BSD socket 完整性,仅**单任务独占期**可用 -> (`net_sendto` / `net_recvfrom` 直碰 SPI、不经队列,见 §2 三例外);本平台无 UDP 消费者。 +> **UDP 已于 2026-11-03 队列化**(原"本项目不使用 UDP(2026-10-15 定案)"作废): +> `net_bind`(UDP) / `net_sendto` / `net_recvfrom` / `net_recvfrom_to` / `net_udp_set_peer` 与 TCP 同一口径 +> ——请求投 `netMsgQueue`,芯片事务**全部在 `netTask` 里串行执行**,调用方阻塞等回执(结果搭 32 位任务 +> 通知值回来)。任何任务都能安全调用,不再有"单任务独占期"这个前提。第一个真实消费者是 SNTP(请求-应答)。 + +### 6.0 先分清楚:芯片的 UDP 两种模式 + +CH395F 用 `DesIP` 一个寄存器区分两种 UDP 形态(手册 `docs/芯片手册/CH395F.md` §9.2.4),本层把它做成 +socket 的**模式属性**(`net_types.h` `NET_UDP_MODE_SERVER` / `NET_UDP_MODE_CLIENT`)。两种形态收上来的 +字节**不一样多**,这是 UDP 侧最容易踩的坑: + +| 模式 | DesIP | 收包布局 | 谁能发给我 | 本层如何进入 | +|---|---|---|---|---| +| **服务器** | `0xFFFFFFFF` | **8 字节信息头** + 载荷(头 [0-1] 长度、[2-3] 源端口、[4-7] 源 IP 线序) | 任意主机 | `net_bind()`(恒此模式:BSD 的 bind 只定本地端口,不选对端) | +| **客户端** | 具体 IP | **无信息头**,整段就是载荷 | 只有该 IP | `net_sendto()` 带目的地址,或 `net_udp_set_peer(fd, &peer)` | + +- 收包时**按模式**决定是否剥头:客户端模式绝不剥。旧实现不分模式恒剥 8 字节,而 `net_sendto` 一旦给出 + 目的地址芯片就已是客户端形态——于是 SNTP 这类应答的**前 8 字节载荷被吃掉**,源地址还是从载荷里当 + 头读出来的假值(缺陷登记见 `docs/问题记录/CH395F_Trap_Records.md`,回归钉子在 + `test/host/net_socket/ns_s7_udp.c` UDP-3 / UDP-4)。 +- 从客户端模式回服务器形态:`net_udp_set_peer(fd, NULL)`,只改 DesIP/DesPort,**不重开 Socket**; + 再次 `net_bind()` 则是重开(顺带丢掉芯片缓冲里的残留包),两者用途不同。 +- 客户端模式下 `net_recvfrom` 回填的源地址就是那个固定对端——芯片已经替我按 IP 过滤过一遍,包上也没有头可取。 ### 6.1 UDP Client @@ -463,37 +485,43 @@ void udp_client(void) return; } - /* 2. 绑定本地端口(可选) */ + /* 2. 绑定本地端口:UDP 侧**必做**(芯片侧 Socket 要在这里 open, + * 没 bind 就收发一律 NET_ERR_NOTCONN)。端口给 0 = 本层取动态端口 */ local_addr.sin_family = AF_INET; local_addr.sin_port = net_htons(12345); local_addr.sin_addr.s_addr = net_inet_addr("0.0.0.0"); - net_bind(sock, (struct net_sockaddr *)&local_addr, sizeof(local_addr)); + if (net_bind(sock, (struct net_sockaddr *)&local_addr, sizeof(local_addr)) != 0) { + printf("绑定失败\r\n"); + net_close(sock); + return; + } /* 3. 设置目标地址 */ dest_addr.sin_family = AF_INET; dest_addr.sin_port = net_htons(8888); dest_addr.sin_addr.s_addr = net_inet_addr("192.168.31.200"); - /* 4. 发送数据 */ + /* 4. 发送数据:带目的地址的 sendto 顺手把 DesIP 指向该地址, + * 芯片从此是**客户端模式**(只收这个 IP 的包,包上不带信息头,见 §6.0) */ const char *msg = "UDP Hello!"; - net_sendto(sock, msg, strlen(msg), 0, - (struct net_sockaddr *)&dest_addr, sizeof(dest_addr)); - printf("发送: %s\r\n", msg); + if (net_sendto(sock, msg, strlen(msg), 0, + (struct net_sockaddr *)&dest_addr, sizeof(dest_addr)) < 0) { + printf("发送失败 %d\r\n", net_get_errno()); + net_close(sock); + return; + } - /* 5. 接收数据 */ - struct net_sockaddr_in src_addr; - int src_addr_len = sizeof(src_addr); - - len = net_recvfrom(sock, buf, sizeof(buf) - 1, 0, - (struct net_sockaddr *)&src_addr, &src_addr_len); + /* 5. 接收应答:受 IWDG 监管的任务用带预算的 net_recvfrom_to, + * 超时返回 -2(不置 errno),不会把喂狗窗口耗在等待上(§10.1) */ + len = net_recvfrom_to(sock, buf, sizeof(buf) - 1, 2000U, NULL, NULL); if (len > 0) { buf[len] = '\0'; - char ip_str[16]; - printf("收到: %s (from %s:%d)\r\n", buf, - net_inet_ntoa(src_addr.sin_addr.s_addr, ip_str), - net_ntohs(src_addr.sin_port)); + /* 客户端模式的源地址就是这个固定对端,包上没有信息头 */ + printf("收到: %s (from server)\r\n", buf); + } else if (len == -2) { + printf("应答超时\r\n"); } net_close(sock); @@ -531,15 +559,15 @@ void udp_server(void) printf("UDP Server 启动,监听端口 8888\r\n"); - /* 3. 循环接收数据 */ + /* 3. 循环接收数据:一次调用 = 一个完整数据报(服务器模式自动剥 8 字节信息头) */ while (1) { - net_poll(); - struct net_sockaddr_in src_addr; int src_addr_len = sizeof(src_addr); - int len = net_recvfrom(sock, buf, sizeof(buf) - 1, 0, + /* 用带预算的收包当循环节拍:-2 = 本轮没人发包,不是错误; + * 换 net_recvfrom(...) 就是无限等,受 IWDG 监管的任务里别这么写(§10.1) */ + int len = net_recvfrom_to(sock, buf, sizeof(buf) - 1, 500U, (struct net_sockaddr *)&src_addr, &src_addr_len); if (len > 0) { @@ -549,14 +577,29 @@ void udp_server(void) net_inet_ntoa(src_addr.sin_addr.s_addr, ip_str), net_ntohs(src_addr.sin_port)); - /* 回显数据 */ - net_sendto(sock, buf, len, 0, + /* 回显数据。⚠ 带目的地址的 sendto 会把芯片切成**客户端模式** + * (DesIP = 这个客户端),此后只有它一家能再发包进来。要长期服务 + * 多个客户端,回完就把它切回来: */ + (void)net_sendto(sock, buf, len, 0, (struct net_sockaddr *)&src_addr, src_addr_len); + (void)net_udp_set_peer(sock, NULL); } } } ``` +### 6.3 UDP 调用约束(2026-11-03 队列化之后) + +| 约束 | 行为与理由 | +|---|---| +| 未 `net_bind()` 就 `sendto` / `recvfrom` / `set_peer` | 一律 `NET_ERR_NOTCONN`(-5):芯片侧 Socket 还没 open,此时写 DesIP 也会被随后的 open 复位 | +| 一次收包 = 一个完整数据报 | 缓冲装不下时交付 `len` 字节并把剩余部分**读空丢弃**(留半包在芯片缓冲里,下一次收包会从上一包的尾巴开始解析,源地址与载荷从此全部错位);短交付靠返回值判读 | +| 同一 fd 并发收包 | 第二个接收者拿 `NET_ERR_BUSY`(-11):数据报语义下无法把包归属到两个等待者,拒绝比交错投递安全(与 TCP 挂起 recv 同一守卫) | +| 阻塞预算 | 受 IWDG 监管的任务禁用无限等待的 `net_recvfrom`,改 `net_recvfrom_to(fd, buf, len, to_ms, ...)`:超时返回 **-2** 且不置 errno(与 `net_recv_to` 同口径);或先 `net_select()` / `net_can_read()` 门控 | +| 单包载荷 | ≤ 1472 字节(手册 §9.2.4);更长由本层按 1KB 分块、复用 TCP 的挂起 send 在 netTask 里推进,短写返回已发送字节数,调用方须检查返回值 | +| fd 额度 | 与 TCP 共用 `NET_MAX_SOCKETS`(8),一个 UDP fd 占一个名额 | +| 收包不占 netTask | 没数据时登记挂起(本 fd 一个槽)立即让出 netTask,完成条件由 `net_udp_tick` 每轮判定——与挂起 recv/send 同一套账,netTask 不会被一次 UDP 等待钉住 | + --- ## 7. I/O 多路复用 @@ -738,10 +781,13 @@ void event_callback_example(void) 的 `s_addr` **同族、可直接相等比较**。历史背景(同一文件曾并存两套约定、`ntoa` 注释里的例子取了废弃族的值) 见 `docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-21 的 2026-10-24 后续条目。 -**UDP 直调限制**:`net_sendto` / `net_recvfrom` **不走消息队列**——它们在调用方上下文里直接 `net_poll()` + 读写芯片缓冲, -因此**仅限本层执行任务(NET 层 = netTask)调用**;别的任务调用回 `NET_ERR_NOT_IN_NETTASK`(-12)。该码**不是运行时故障, -重试永远无意义**:要么把调用挪进执行任务,要么改用走队列的 `net_send` / `net_recv`,没有第三条路。 -「把这两个接口改成经 `netMsgQueue` 串行化」是已知后续项,未做的理由见 `Drivers/BSP/NET/net_socket.h` 同名函数头。 +**UDP 也走队列(2026-11-03 改造)**:`net_bind`(UDP) / `net_sendto` / `net_recvfrom` / `net_recvfrom_to` / +`net_udp_set_peer` 与 TCP 同一口径——投 `netMsgQueue`、在 `netTask` 里碰芯片、结果搭 32 位任务通知值回来。 +改造前有、现在作废的两件事:①"仅限本层执行任务(netTask)调用"——`NET_ERR_NOT_IN_NETTASK`(-12)自本 +改造起**没有任何返回路径**,常量保留只为老代码与历史文档对得上号,新代码不要拿它当判据;②"UDP 只在单任务 +独占期可用"。源地址回传不占 `net_msg_t`(该结构按值拷入队列,AGENTS 禁止加"回传用"字段),而是经 +`Drivers/BSP/NET/net_udp.c` 的 per-fd 出参槽写回调用方自己的内存,代价是**同一 fd 不允许两个任务同时收包** +(第二个拿 `NET_ERR_BUSY`)。芯片形态(服务器 / 客户端)与剥头规则见 §6.0,调用约束见 §6.3。 ### 9.1 端口号转换 @@ -1007,20 +1053,71 @@ int net_recv_to(int sockfd, void *buf, int len, uint32_t timeout_ms); 调用方累计自身总预算:见到 -2 发起下一片,直到收齐 / EOF / 总预算耗尽。生产用途:lftpd 数据行读、timeSync 帧读(队头阻塞修复,完整例 §11.2);受 IWDG 监控任务的长等待必须走本接口(或 `net_select`)+ 每片喂心跳。 -#### net_sendto - 发送 UDP 数据 +#### net_sendto - 发送 UDP 数据报 ```c int net_sendto(int sockfd, const void *buf, int len, int flags, const struct net_sockaddr *dest_addr, int addrlen); ``` -#### net_recvfrom - 接收 UDP 数据 +| 参数 | 说明 | +|------|------| +| `sockfd` | **必须已 `net_bind()`**(未 bind 回 -1 / `NET_ERR_NOTCONN`:芯片侧 Socket 还没 open) | +| `buf` / `len` | 一个数据报的载荷,单包 ≤ 1472 字节(手册 §9.2.4);更长由本层按 1KB 分块、复用 TCP 的挂起 send 在 netTask 里推进 | +| `flags` | 目前只消费 `NET_MSG_DONTWAIT`:芯片发送缓冲不可用时立即回 `NET_ERR_WOULDBLOCK`,不登记挂起 | +| `dest_addr` / `addrlen` | 非 NULL = 顺带把芯片 DesIP 指向它 → 芯片转**客户端模式**(此后只收该 IP 的包、收包不带信息头,见 §6.0);NULL = 不改目的地址,沿用当前形态 | +| 返回值 | > 0 = 已发送字节数(**可能短写**,须检查);-1 = 失败(`net_get_errno()`) | + +任意任务可调用:请求入 `netMsgQueue`,芯片事务在 `netTask` 串行执行(2026-11-03 起,旧"仅限 netTask 直调"形态作废)。 + +#### net_recvfrom - 接收 UDP 数据报(无限等待形态) ```c int net_recvfrom(int sockfd, void *buf, int len, int flags, struct net_sockaddr *src_addr, int *addrlen); ``` +- 一次调用 = 一个**完整数据报**:服务器模式自动剥 8 字节信息头并从中取源地址,客户端模式整段是载荷、源地址回填固定对端(§6.0)。 +- `buf` 装不下时交付 `len` 字节并把剩余部分**读空丢弃**——半包留在芯片缓冲里,下一次收包会从上一包的尾巴开始解析。 +- `src_addr` / `addrlen` 可为 NULL;非 NULL 时 `*addrlen` 给 `sizeof(struct net_sockaddr_in)`,回填的 `s_addr` 是网络序整数(§9.0)。 +- ⚠ **无数据即无限等待**:受 IWDG 监管的任务改 `net_recvfrom_to()`,或先 `net_select()` / `net_can_read()` 门控。 +- 同一 fd 已有别的任务在收 → 本调用回 -1 / `NET_ERR_BUSY`(数据报无法归属两个等待者)。 + +#### net_recvfrom_to - 接收 UDP 数据报(显式预算形态) + +```c +int net_recvfrom_to(int sockfd, void *buf, int len, uint32_t timeout_ms, + struct net_sockaddr *src_addr, int *addrlen); +``` + +| 参数 | 说明 | +|------|------| +| `timeout_ms` | 等待预算(ms,**必须 > 0**;0 = 无限等待,与本接口"有界"的存在理由相反,直接拒 `NET_ERR_INVAL`——无限形态请用 `net_recvfrom`) | +| 返回值 | > 0 = 本包交付字节数(小于芯片内包长 = 被截断,多余部分已丢弃);**-2 = 预算内没等到**(**不是 errno**,不得据此判错误);-1 = 真错误 | + +与 `net_recv_to()` 同一口径,是受 IWDG 监控任务的合法 UDP 收包入口;等待期本 fd 登记一个挂起槽、netTask 立即让出(不占 CPU、不占 netTask)。 + +#### net_udp_set_peer - 切换 UDP 芯片形态(不重开 Socket) + +```c +int net_udp_set_peer(int sockfd, const struct net_sockaddr_in *peer); +``` + +| 参数 | 说明 | +|------|------| +| `peer` | 非 NULL = 客户端模式,DesIP/DesPort 指向该对端(只收它的包,收包无信息头);NULL = 回服务器模式(DesIP = `0xFFFFFFFF`、DesPort = 0,任意主机可发,收包带 8 字节信息头) | +| 返回值 | 0 成功;-1 失败(未 bind → `NET_ERR_NOTCONN`) | + +只写两个寄存器、**不重开 Socket**,因此不清芯片缓冲;要连残留包一起丢掉就再 `net_bind()` 一次(重开形态)。 + +#### net_udp_get_mode - 读当前 UDP 芯片形态 + +```c +int net_udp_get_mode(int sockfd); +``` + +返回 `NET_UDP_MODE_SERVER` / `NET_UDP_MODE_CLIENT`,参数错回 -1。给诊断、单测和上层判读"要不要切回服务器模式"用;纯 RAM 读,不入队。 + #### net_close - 关闭 Socket ```c diff --git a/docs/网络/BSD_Socket_API_函数调用关系.md b/docs/网络/BSD_Socket_API_函数调用关系.md index f8c14093..a1e9e6fc 100644 --- a/docs/网络/BSD_Socket_API_函数调用关系.md +++ b/docs/网络/BSD_Socket_API_函数调用关系.md @@ -5,9 +5,10 @@ > > **范围**:MCU 端 `Drivers/BSP/NET/net_socket.c` + 其拆出的 > `Drivers/BSP/NET/net_listener.c`(**监听生命周期**)+ -> `Drivers/BSP/NET/net_pending.c`(**挂起 recv / send 请求表**)。 +> `Drivers/BSP/NET/net_pending.c`(**挂起 recv / send 请求表**)+ +> `Drivers/BSP/NET/net_udp.c`(**UDP 数据面执行体与挂起 recv 槽**,2026-11-03 队列化改造新增,§4.11)。 > 模拟器端后端 `simulator/sim/net_socket_sim.c`(Winsock)是另一套实现,见 `docs/网络/BSD_Socket_API_使用指南.md` §2.3;`net_select.c` 不在此展开,见 `docs/网络/BSD_Socket_API_使用指南.md` §7 / §10.5。 -> `net_listener.c` / `net_pending.c` 真机专属(模拟器不编译);设计依据分别见 +> `net_listener.c` / `net_pending.c` / `net_udp.c` 真机专属(模拟器不编译);设计依据分别见 > `docs/网络/网络自愈架构设计.md` §11 与 > `docs/网络/Net层阻塞模型架构设计.md`。 > @@ -25,11 +26,11 @@ | 执行域 | 运行位置 / 节奏 | 允许的操作 | 典型调用者 | |---|---|---|---| -| **netTask 主循环** | 独立 FreeRTOS 任务,1ms/tick | **唯一**可碰芯片(SPI2)的地方;4 个非阻塞 tick(D10 重试 / 挂起 recv / 挂起 send / TRAP-NET-17 对账+监听重建合一 reconciler);队列分派;事件处理器;用户回调 | `net_poll` / `net_process_messages` 及其下游全部 `_locked` / `_tick` / `handle_*` | +| **netTask 主循环** | 独立 FreeRTOS 任务,1ms/tick | **唯一**可碰芯片(SPI2)的地方;5 个非阻塞 tick(D10 重试 / 挂起 recv / 挂起 send / **UDP 挂起 recv**(2026-11-03 队列化) / TRAP-NET-17 对账+监听重建合一 reconciler);队列分派;事件处理器;用户回调 | `net_poll` / `net_process_messages` 及其下游全部 `_locked` / `_tick` / `handle_*` | | **应用任务侧** | ftpTask / uiTask / timeSyncTask / testTask 等 | 经队列的公共 API(入队 + `xTaskNotifyWait` 等回执);纯 RAM 直调 API;**禁止直碰芯片** | 各服务任务 | -| **直调快路径** | 同步调用,不经过队列 | `net_socket` / 查询族 / 回调注册(纯 RAM);**直碰 SPI 的三例外**:`net_bind`(UDP) / `net_sendto` / `net_recvfrom` | 初始化阶段、netTask 上下文、测试套件(硬件层测试模式下 netTask 自挂起,testTask 独占芯片) | +| **直调快路径** | 同步调用,不经过队列 | `net_socket` / 查询族 / 回调注册 / `net_bind`(TCP) 记账(纯 RAM);**UDP 侧不再是例外**:`net_bind`(UDP) / `net_sendto` / `net_recvfrom` / `net_recvfrom_to` / `net_udp_set_peer` 已于 2026-11-03 全部并入队列 | 初始化阶段、netTask 上下文、测试套件(硬件层测试模式下 netTask 自挂起,testTask 独占芯片) | -> ⚠ 直碰 SPI 的三例外**只能在"芯片访问者唯一"的上下文**调用:产品构建中应用任务一律走队列(TCP 全路径);三例外的实际消费者是测试套件(见 AGENTS.md"测试构建")。从其他任务直接调用会与 `net_poll` 的 SPI 事务竞争,破坏 CH395F 寄存器(TRAP-NET-14 实证:CONNECT 超时)。 +> ⚠ 2026-11-03 起,**公开 API 里已经没有直碰 SPI 的口子**——原"三例外"(`net_bind`(UDP) / `net_sendto` / `net_recvfrom`)已全部并入队列,产品构建中应用任务的每一条 `net_*` 都是"入队 + 等回执"。仍需独占芯片的只剩**板级硬件层测试**:它绕过本层直接调 `ch395f_*`,前提是该测试模式下 netTask 自挂起、testTask 独占芯片(见 AGENTS.md"测试构建")。任何情况下从别的任务直接驱动 CH395F 都会与 `net_poll` 的 SPI 事务竞争、破坏芯片寄存器(TRAP-NET-14 实证:CONNECT 超时)。 --- @@ -38,9 +39,9 @@ ```mermaid flowchart TB subgraph APP["应用任务(非 netTask)"] - PUB["经队列公共 API
netListen · netAccept · netConnect · netSend
netRecv · netRecvTo · netClose · netDropConn"] + PUB["经队列公共 API
netListen · netAccept · netConnect · netSend
netRecv · netRecvTo · netClose · netDropConn
UDP:netBind · netSendto · netRecvFrom · netRecvFromTo · netUdpSetPeer"] RAM["直调 · 纯 RAM
netSocket · netSockState · netGetSock
netGetErrno · netSetEventCb · netGetSockname
字节序(netHtons / netInetAddr …)"] - SPI["直调 · 直碰 SPI(须芯片唯一上下文)
netBind(UDP) · netSendto · netRecvFrom"] + SPI["板级硬件层测试 · 直调 BSP(须芯片访问者唯一)
ch395f_*(该模式下 netTask 自挂起)"] CB["用户事件回调
(在 netTask 上下文执行!)"] end @@ -53,8 +54,9 @@ flowchart TB RetryTick["① netListenerRetryTick
D10 重监听重试:30ms / 2s 时限"] PendTick["② netPendingTick
挂起 recv:①数据 ②到点(仅显式预算) ③断连 ④tripwire 60s 只留痕"] PendShimTick["③ netSendPendingTick
挂起 send:①断连 ②推进(每轮≤1块) ③写完 ④无进展超时"] - ReconcileTick["④ netReconcileTick
TRAP-NET-17 对账 + 监听重建合一:100ms / 分级去抖 / 冷却
读驱动状态缓存(①②③④⑤ + U11)"] - DISP["队列分派 switch
SEND · RECV · CLOSE
CONNECT · LISTEN · ACCEPT · DROP_CONN"] + ReconcileTick["⑤ netReconcileTick
TRAP-NET-17 对账 + 监听重建合一:100ms / 分级去抖 / 冷却
读驱动状态缓存(①②③④⑤ + U11)"] + UdpTick["④ netUdpTick
UDP 挂起 recv:①数据到达(读一个完整数据报) ②预算到点 -2 ③会话已死"] + DISP["队列分派 switch
SEND · RECV · CLOSE
CONNECT · LISTEN · ACCEPT · DROP_CONN
UDP_BIND · UDP_SENDTO · UDP_RECVFROM · UDP_PEER"] subgraph LK["_locked / 内部分派目标(仅 netTask 上下文)"] LL["netListenLocked
初次监听(open 重试 3× + sock_reset 排渣)"] LN["netConnectLocked
open 重试 3×(sock_reset 排渣)+ tcpConnect"] @@ -64,19 +66,30 @@ flowchart TB LC["netCloseLocked
结束挂起 + cmd_done 快路径 / sock_reset(GRACEFUL)"] DC["netDropConnLocked
拆会话保监听:sock_reset(QUARANTINE) + 登记重监听"] RLS["netListenerTry → netRelistenLocked
单次尝试 + TRAP-NET-18 排渣隔离"] + UB["netUdpBindExec
开芯片侧 Socket(恒服务器模式,DesIP=FFH 在 open 前写)"] + US["netUdpSendtoExec
带地址则先写 DesIP/DesPort(转客户端模式)"] + UR["netUdpRecvfromExec
快路径 / DONTWAIT / 登记挂起 recv"] + UP["netUdpSetPeerExec
只改 DesIP/DesPort,不重开 Socket"] end EV["事件处理器
handleConnect · handleDisconnect
handleRecv · handleSend · handleTimeout"] ST["netListenerSettle
只置沉降/隔离事实(100ms / 400ms)"] FE["fireEvent → 用户回调"] PR["s_net_pending
(每 fd 至多一个挂起 recv)"] SR["s_net_send
(每 fd 至多一个挂起 send)"] + SU["s_net_udp
(每 fd 至多一个接收槽:调用方出参指针 + 挂起 recv)"] CH[("ch395f.* 驱动
SPI2 串行事务")] end PUB -->|入队| MQ MQ --> DISP - DISP --> LL & LN & LS & LR & LA & LC & DC & RLS - NPM --> RetryTick & PendTick & PendShimTick & ReconcileTick & NP + DISP --> LL & LN & LS & LR & LA & LC & DC & RLS & UB & US & UR & UP + NPM --> RetryTick & PendTick & PendShimTick & UdpTick & ReconcileTick & NP + US -->|共用同一条分块与挂起 send| LS + UR -->|登记 / 结算| SU + UdpTick -->|读出一个完整数据报| SU + UdpTick -->|xTaskNotify 完成或超时| XN + LC -->|结束 UDP 挂起 recv(settle_close)| SU + UB & US & UR & UP & UdpTick --> CH RetryTick --> RLS ReconcileTick -->|"① 已断"| EV ReconcileTick -->|"② 补 accept"| EV @@ -97,14 +110,14 @@ flowchart TB XN -->|唤醒| PUB NP --> CH LL & LN & LS & LR & LA & LC & DC & RLS --> CH - SPI -->|直碰 SPI| CH + SPI -->|仅板级硬件层测试:netTask 自挂起时独占芯片| CH CH -->|INT# 低电平| NP ``` **读图要点**: -1. 应用侧只有两种"等":① 入队后 `xTaskNotifyWait` 等回执(同步语义);② 挂起 recv / send 的等待(同一通道,只是"完成"由 `netPendingTick` / `netSendPendingTick` 判定,不由 netTask 立即处理)。 -2. `net_process_messages()` 开头的 **4 个 tick 是网络层自愈主干**:没有任何应用消息时,重试 / 挂起 recv 完成 / 挂起 send 推进 / 状态对账 + 监听重建照常运转——"断连后即使没人发消息,监听器也能自己活过来"靠的就是它们。 +1. 应用侧只有两种"等":① 入队后 `xTaskNotifyWait` 等回执(同步语义);② 挂起 recv / send 的等待(同一通道,只是"完成"由 `netPendingTick` / `netSendPendingTick` / `netUdpTick` 判定,不由 netTask 立即处理)。 +2. `net_process_messages()` 开头的 **5 个 tick 是网络层自愈与等待结算主干**:没有任何应用消息时,重试 / 挂起 recv 完成 / 挂起 send 推进 / UDP 挂起 recv 完成与超时 / 状态对账 + 监听重建照常运转——"断连后即使没人发消息,监听器也能自己活过来"靠的就是它们。 3. 事件处理器**只由 `net_poll` 驱动**,例外是 `net_reconcile_tick`(分级去抖 + 1s 冷却)可再入 `handle_disconnect_event` / `handle_connect_event`——对账是事件未收到的自愈通道(TRAP-NET-17)。**注意职责边界(对账与重建合并后)**:对账与事件处理器**只置事实**(置 CLOSED / 登记沉降),**重建监听的动作统一由 `net_reconcile_tick` 形态③④ 做**(本层唯一重建入口)——"谁先看到 DISCONNECT 就谁重建"的竞争在结构上消失。 4. 回调 `event_cb` 在 **netTask 上下文**执行:回调内不得阻塞、不得发网络操作。 5. **模块归属**:`RetryTick`(D10 重试)/ `RLS` 的 `net_listener_request` / `ST`(settle)在 `net_listener.c`(真机专属、模拟器不编译);挂起 recv 完成检查(`net_pending_tick`)、挂起 send 推进(`net_send_pending_tick`)与两张挂起表(`PR` / `SR`)在 `net_pending.c`(真机专属);其余 tick(含合并后的 `ReconcileTick` reconciler)与 `_locked` 在 `net_socket.c`。 @@ -126,7 +139,7 @@ flowchart TB | 函数 | 设计作用 | 主要调用 | 意图 / 注意 | |---|---|---|---| | `net_poll()` | 事件泵:GPIO 轮询 INT#(低=有待处理)→ `GET_GLOB_INT_STATUS_ALL`(读=清)→ 按位图逐 Socket 读 SINT → 分派 5 类事件;**每轮末尾**对在用 Socket 全量刷新**驱动状态缓存** + 步进 `ch395f_sock_reset_poll` | `HAL_GPIO_ReadPin` → `ch395f_get_glob_int_status_all` → `ch395f_get_sock_int_status` ×n → `handle_connect/disconnect/recv/send/timeout_event` → `ch395f_get_sock_state` ×n(刷新缓存)+ `ch395f_sock_reset_poll` ×n | **RECV 中断是电平触发**:每次调用**只处理一批**(do-while(0)),处理完立即返回,否则主循环(echo/应用逻辑)饿死(TRAP-NET-08);SENBUF_FREE 只置 `send_ready` 不记事件;状态缓存**唯一刷新点**在此(reconciler 只读缓存,E2 实测单次回读 18.1µs);`sock_reset_poll` 步进使同步包装 600ms 放弃后的复位机不搁浅(IDLE 槽纯 RAM 判零 SPI) | -| `net_process_messages()` | 队列入口 + 自愈 tick 宿主:每轮**先**跑 4 个非阻塞 tick,再排空队列逐条分派,结果 `xTaskNotify` 回传 | `net_listener_tick` → `net_pending_tick` → `net_send_pending_tick` → `net_reconcile_tick`(对账+重建合一)→ `osMessageQueueGet`(0 超时) ×n → switch 分派 → `xTaskNotify` | ① tick 放在队列**之前**:断连后即使没有任何应用消息,监听器也能自主恢复、挂起 recv / send 也能完成;② RECV 若返回 `NET_PENDING_REGISTERED`(哨兵 0x70000000)则**不发即时回执**(`do_notify=0`),完成时才通知;③ 失败结果把 `s_net_errno`(具体负错误码)压入通知值——调用方恢复 errno 后对外统一 -1(BSD 语义);④ 通知值**整个 32 位**都是结果(不压位域标签,否则 32768 这类大字节数会溢出为负);⑤ **对账①置 CLOSED 后形态③④同轮即可重建**(合并后,原"pump 置于对账之后"的顺序依赖在单 tick 内自然成立) | +| `net_process_messages()` | 队列入口 + 自愈 tick 宿主:每轮**先**跑 5 个非阻塞 tick,再排空队列逐条分派,结果 `xTaskNotify` 回传 | `net_listener_tick` → `net_pending_tick` → `net_send_pending_tick` → `net_udp_tick`(UDP 挂起 recv 完成/超时)→ `net_reconcile_tick`(对账+重建合一)→ `osMessageQueueGet`(0 超时) ×n → switch 分派 → `xTaskNotify` | ① tick 放在队列**之前**:断连后即使没有任何应用消息,监听器也能自主恢复、挂起 recv / send 也能完成;② RECV 若返回 `NET_PENDING_REGISTERED`(哨兵 0x70000000)则**不发即时回执**(`do_notify=0`),完成时才通知;③ 失败结果把 `s_net_errno`(具体负错误码)压入通知值——调用方恢复 errno 后对外统一 -1(BSD 语义);④ 通知值**整个 32 位**都是结果(不压位域标签,否则 32768 这类大字节数会溢出为负);⑤ **对账①置 CLOSED 后形态③④同轮即可重建**(合并后,原"pump 置于对账之后"的顺序依赖在单 tick 内自然成立) | ### 4.3 经队列的公共 API 及其 netTask 侧实现 @@ -145,14 +158,17 @@ flowchart TB | `net_drop_conn` | `NET_MSG_DROP_CONN` | `net_drop_conn_locked` | `net_pending_settle_close` + `net_sock_reset_sync(QUARANTINE)` +(auto_relisten)`net_listener_request` | 0(含幂等)/ -1 | **拆会话保监听**(队头阻塞收口):应用放弃会话(对时 30s 预算耗尽 / 协议超时)而单连接监听被挂起对端永久占住时的应用侧出口;EOF 路径**不得**调(`auto_relisten` 已接管);非 ESTABLISHED/CONNECTING 直接返 0 不动监听;本层收口 CLOSED 不清 fd(保留 local_port/auto_relisten),监听型 fd 登记重监听、失败交 D10 + reconciler 形态④ | | ~~`net_relisten`~~ | ~~`NET_MSG_RELISTEN`~~ | **已删除**(消息类型同删) | — | — | 原"无条件芯片侧重建"与 pump/D10 完全重叠,且对健康监听器是 footgun(open→USE→close 自毁 ~1s);生产零调用。历史实现见 `net_socket.c` git 历史 | | ~~`net_resync_listen`~~ | ~~`NET_MSG_RESYNC_LISTEN`~~ | **已删除**(语义并入对账⑤) | — | — | D14 静默回收(芯片自行回监听、不发 DISCONNECT)现由 `net_reconcile_tick` 判据⑤自动认领:本层 ESTABLISHED 且芯片 0x05/0x01 → 只修本层(state→LISTENING + 转登 `last_peer_port` + 清 `lost_conn_seen` + 取消挂起 recv),**不发芯片命令**(纪律原样保留,run8 实证 170s 拒绝);去抖/冷却沿用对账框架 | +| `net_bind`(**UDP 支**) | `NET_MSG_UDP_BIND`(端口按 `ntohs` 后的值传,0 = 由执行体取动态号) | `net_udp_bind_exec` | Socket 4~7 兜底 `set_send_buf` / `set_recv_buf` + `set_proto_type(UDP)` + `set_sour_port` + `set_des_ip(0xFFFFFFFF)` + `set_des_port(0)` + `open_socket` | 0 / -1 | **bind 恒服务器模式**(BSD 的 bind 只定本地端口,不选对端);DesIP/端口写在 open **之前**(open 复位寄存器,写晚了等于没写);动态端口游标在 netTask 内推进(旧形态在调用方上下文改静态量 = 跨任务竞争) | +| `net_sendto` | `NET_MSG_UDP_SENDTO`(目的地址按值拷进 `msg.addr`,`addrlen > 0` 才改 DesIP) | `net_udp_sendto_exec` → **共用** `net_send_locked` | `set_des_ip` / `set_des_port`(带地址时)+ `write_send_buf` 分块 ≤ 1KB | 实际字节数(**可能短写**)/ -1 | 带目的地址 = 切**客户端模式**并记账 `remote_*`;发送与 TCP 走同一条分块 + 挂起 send(`net_send_pending`)通路,不为 UDP 复制第二份 1KB 分块逻辑;未 bind → NOTCONN(芯片侧还没 open) | +| `net_recvfrom` / `net_recvfrom_to` | `NET_MSG_UDP_RECVFROM`(**出参指针不入队**:`net_udp_outparam_arm` 存进本 fd 槽,netTask 直接写回调用方内存) | `net_udp_recvfrom_exec`(即时读 / 登记挂起)+ `net_udp_tick`(完成 / 超时结算) | `net_poll` + `get_recv_len` + `read_recv_buf`(剥头 + 截断排空) | >0 / -1 / **-2(`_to` 预算内无数据,不占 errno)** | 一次调用 = 一个完整数据报;**按 `udp_mode` 决定剥不剥 8 字节信息头**(服务器剥、客户端不剥——旧形态不分模式恒剥,客户端模式等于吃掉载荷前 8 字节并回填假源地址);无数据 + 阻塞 → 登记挂起立即让出 netTask;缓冲装不下时剩余部分**读空丢弃**;同 fd 第二个接收者 → BUSY | +| `net_udp_set_peer` | `NET_MSG_UDP_PEER`(`addrlen == 0` = 回服务器模式) | `net_udp_set_peer_exec` | `set_des_ip` + `set_des_port`(**不重开 Socket**) | 0 / -1 | 模式切换的轻量入口,给"上一轮当客户端问、这一轮要听所有人"的收包方;要连芯片缓冲残留一起丢掉就再 `net_bind()` 一次(重开形态) | ### 4.4 直调 API(不经队列) | 函数 | 设计作用 | 主要调用 | 意图 / 注意 | |---|---|---|---| | `net_socket(domain,type)` | 分配空闲控制块(`alloc_socket` 线性扫描 `in_use=0`)→ CREATED | `alloc_socket` + `net_listener_clear` + `net_pending_clear` | 新 fd 不得继承上一次的待重试/挂起状态(复用描述符的经典坑);domain 仅 `NET_AF_INET` | -| `net_bind(sockfd,addr,addrlen)` | TCP:仅记录本地端口(port=0 → 动态端口 49152 起递增)→ BOUND,**无芯片命令**;UDP:完整开 Socket → UDP_OPEN | UDP 路径:`set_proto_type(UDP)` + `set_sour_port` + `set_des_ip(0xFFFFFFFF)` + `set_des_port(0)` + `open_socket`(Socket 4~7 兜底重设缓冲) | TCP 的 bind 只是"记账",真正的芯片动作在 listen/connect;UDP 直碰 SPI(§2 三例外之一);动态端口回绕 65535→49152 | -| `net_sendto` / `net_recvfrom` | UDP 收发(直碰 SPI,不经队列) | `set_des_ip` / `set_des_port` + `write_send_buf`(分块 1KB + `net_poll` 门控);`net_poll` + `get_recv_len` + `read_recv_buf` | 同 §2 上下文约束;recv 时 UDP Server 模式前 8 字节是固定头(源 IP + 端口 + 长度),先剥头填 `src_addr` 再读数据;recvfrom 阻塞语义与 net_recv 同构(blocking 属性 / DONTWAIT,无默认预算;轮询自旋实现,生产零调用) | +| `net_bind(sockfd,addr,addrlen)` | **TCP 支**:仅记录本地端口(port=0 → 动态端口 49152 起递增)→ BOUND,**无芯片命令** | 写 `s_net_socks[fd].local_port` / `state` | TCP 的 bind 只是"记账",真正的芯片动作在 listen/connect;**UDP 支自 2026-11-03 起入队**(要发 5 条芯片命令,见 §4.3 `net_bind`(UDP 支)行);动态端口回绕 65535→49152 | | `net_setblocking(sockfd,on)` | 设置阻塞**模式属性**(直写控制块,不经队列) | 写 `s_net_socks[fd].blocking` | 对齐 BSD `O_NONBLOCK` / Python `setblocking`:"模式归使用者、超时归调用方";单字节原子写(先例 `net_set_event_cb`),最坏一次在途 recv 用旧模式;`net_socket()` 默认置 1(close 的 memset 清零后新 fd 必须显式重置) | | `net_wait_result(kind,sockfd)` | 调用侧两段式等待:**零等待拾取** → 阻塞等回执 | `xTaskNotifyWait` ×2 | ① 零等待段兜住"入队→等待入口"间隙落地的通知(2026-09-22 台架实证丢通知);② **清除掩码必须全 1**(传 0 = 不清 → 上次结果残留被重读,run3~11 挂死/错位的真根因);③ 必须判 **pdTRUE** 而非 `v!=0`(合法结果 0 = EOF 会被误判"无通知"→ 永久挂死);④ 32 位整体作结果(32768 大字节数不截断);⑤ 本路径**禁止**加逐调用日志(曾占日志 98.6%、冲掉 [LAT] 证据行,run14 实证) | | `net_sock_state(sockfd)` | 跨任务只读状态快照(单字节读,无锁) | 无 | 描述符无效/未占用 → -1;值可能滞后 netTask 最新状态——用于**轮询**(客户端等 ESTABLISHED),不替代 accept/connect | @@ -257,8 +273,31 @@ flowchart TB --- +### 4.11 UDP 数据面(`net_udp.c`,2026-11-03 队列化改造) + +> **模块归属**:`Drivers/BSP/NET/net_udp.c`(真机专属,模拟器不编译——模拟器端 UDP 由 `simulator/sim/net_socket_sim.c` 的 Winsock 形态承担)。 +> **使用侧**契约(芯片两种模式、调用约束、超时口径)权威在 `docs/网络/BSD_Socket_API_使用指南.md` §6.0 / §6.3; +> 本节只讲调用结构与"为什么这么切"。 + +| 函数 | 上下文 | 设计作用 | 关键决策 | +|---|---|---|---| +| `net_udp_outparam_arm` / `net_udp_outparam_disarm` | 调用方任务(临界区只包"查占位"三步) | 登记"这次收包的源地址写回哪块调用方内存" | **源地址不搭 `net_msg_t`**:该结构按值拷入队列,AGENTS / TRAP-NET-10 明令禁止给它加"回传用"字段;出参指针进 per-fd 槽,netTask 直接写回调用方自己的内存,通知值只带结果码 | +| `net_udp_clear` | 两侧 | 清本 fd 槽(`net_socket` 分配新 fd、入队失败、执行体每条出口都要清) | 槽位泄漏 = 该 fd 从此永久 `BUSY`;出口必清(ns_s7_udp.c UDP-7d / UDP-9e 钉住) | +| `net_udp_bind_exec` | netTask | 服务器模式开芯片侧 Socket(5 条命令 + Socket 4~7 缓冲兜底) | DesIP/端口写在 open **之前**(手册 §9.2.4;open 复位寄存器);bind **恒服务器模式**——BSD 语义里 bind 不选对端;动态端口游标在 netTask 内推进(消掉跨任务写静态量) | +| `net_udp_set_peer_exec` | netTask | 只写 DesIP/DesPort 切形态;`peer == NULL` 回服务器模式 | 不重开 Socket,因此**不清**芯片缓冲;要连残留包一起丢就再 `net_bind()`(重开形态) | +| `net_udp_sendto_exec` | netTask | 记对端账 + **复用 `net_send_locked`** 发送 | 不为 UDP 复制第二份 1KB 分块 / 挂起 send 逻辑;"会话是否已死"由 `net_send_state_down()`(`net_pending.c`)按 `type` 分流——TCP 看 ESTABLISHED、UDP 看 UDP_OPEN,一条判据两侧共用(`net_send_locked` / `net_send_pending_tick` / `net_pending_tick` 三处原先各写一遍) | +| `net_udp_recvfrom_exec` | netTask | 三出口:数据已在 → 读一包;DONTWAIT / `blocking=0` → WOULDBLOCK;阻塞 → 登记挂起并立即让出 netTask | 自等守卫判 `caller == xTaskGetCurrentTaskHandle()`,**不是** `net_ctx_is_owner_task()`:执行体本身就跑在 netTask 里,用后者等于把每一条经队列投进来的正常请求都当自等拒掉(UDP-8a 当场抓到过这个错) | +| `net_udp_tick` | netTask 1ms | 挂起 recv 的完成 / 预算超时 / 会话已死三类结算 + `xTaskNotify` | 与挂起 recv、挂起 send 同一节奏;`readable` 只当"省一次 SPI"的门,电平型 RECV_OK 每轮重读可自愈(事件漏了也不永久瞎) | +| `net_udp_datagram_read` | netTask(private) | 一个完整数据报:按 `udp_mode` 决定剥不剥 8 字节信息头 → 截断排空 → 清 `readable` | `avail` 由调用方读一次**传进来**:剥头量、载荷量、排空量必须由同一个数字算出(读两次 = 决策与内容可能不同源);服务器模式不足 8 字节的残包不猜头,整包按载荷交付(TC-NETSOCK-440o 定案) | +| `net_udp_settle_close` / `net_udp_cancel_all` | netTask | close / 网络重建时结算等待者 | **任何结算都必须通知**:close 时有数据先交付、无数据回 CONNRESET;`net_init` 重建走 `net_udp_cancel_all`(UDP-8 / UDP-12 钉住"不许静默丢通知")。⚠ **没有** per-fd `net_udp_cancel`:唯一的 per-fd 取消点在 `net_events.c` 的 reconcile 形态⑤,而形态①⑤的判据门是 `state == ESTABLISHED`(TCP 专属,UDP 在 `net_events.c` 里被"其余状态不归本 tick 认领"挡掉),UDP 挂起收包的"会话已死"结算由 `net_udp_tick` 第①条按状态门当轮完成(UDP-19 钉住),不留没人调的 API | + +改造前后的差别一句话:**改造前 UDP 在调用方上下文自旋碰 SPI**(既违反"netTask 唯一芯片访问者",也把 +IWDG 预算吃在自旋上,且源地址无法回传);**改造后 UDP 与 TCP 同构**——入队、netTask 串行执行、挂起态 +交 tick 结算、结果搭 32 位通知值回来。 + ## 5. 典型端到端路径 + ### 5.1 发送(队列路径) ```mermaid @@ -409,15 +448,15 @@ sequenceDiagram | `ch395f_set_mac_addr` | `net_init`(**须在 `ch395f_init` 之前**) | 写芯片 EEPROM;INIT_CH395 加载 MAC(`docs/芯片手册/CH395F.md` §9.2.1 步骤①) | | `ch395f_set_ip_addr` / `set_gwip_addr` / `set_mask_addr` / `set_dhcp` | `net_init`(**均须在 `ch395f_init` 之前**) | 固定 IP 或 DHCP | | `ch395f_init` / `ch395f_ping_enable` | `net_init` | 协议栈初始化(INIT_CH395)/ PING(须在 init 之后) | -| `ch395f_set_send_buf` / `ch395f_set_recv_buf` | `net_init`(8 Socket 布局);`net_bind`(UDP)(Socket 4~7 兜底) | 每 Socket 收 4 块 + 发 2 块 | +| `ch395f_set_send_buf` / `ch395f_set_recv_buf` | `net_init`(8 Socket 布局);`net_udp_bind_exec`(Socket 4~7 兜底,2026-11-03 起在 netTask 内执行) | 每 Socket 收 4 块 + 发 2 块 | | `ch395f_get_glob_int_status_all` | **仅 `net_poll`**(唯一读者红线:全局寄存器唯一读者,见 `docs/网络/网络自愈架构设计.md` §4.10;`net_listen_locked` / `net_connect_locked` / `net_close_locked` / `net_relisten_locked` 的原全局读清已全部删除) | **读 = 清**(中断清除机制,无按位手段)——其他位置读清会焊死 net_poll 的按 Socket 门控 | | `ch395f_get_sock_int_status` | `net_poll`(逐 Socket SINT);各 open 路径后清残留;事件处理器清 TIMEOUT | **读 = 清** | | `ch395f_sock_events` | `net_connect_locked`(open 后清残留,唯一读者红线的护栏原语,见 `docs/网络/网络自愈架构设计.md` §4.10);`net_close_locked`(芯片已 CLOSED 快路径只清本 Socket 残留事件) | 只碰**本 Socket** SINT 的护栏原语 | -| `ch395f_open_socket` / `ch395f_close_socket` | `net_bind`(UDP)、`net_listen_locked`、`net_connect_locked`、`net_relisten_locked`(含沉降期主动 close);`net_close_locked` 的 CLOSE 已改经 `ch395f_cmd_done` / `ch395f_sock_reset_*` | TRAP-NET-09:close 后需沉降才能重开 | +| `ch395f_open_socket` / `ch395f_close_socket` | `net_udp_bind_exec`(UDP 开 Socket)、`net_listen_locked`、`net_connect_locked`、`net_relisten_locked`(含沉降期主动 close);`net_close_locked` 的 CLOSE 已改经 `ch395f_cmd_done` / `ch395f_sock_reset_*` | TRAP-NET-09:close 后需沉降才能重开 | | `ch395f_tcp_listen` / `ch395f_tcp_connect` | `net_listen_locked` / `net_relisten_locked`;`net_connect_locked` | — | -| `ch395f_set_proto_type` / `set_sour_port` / `set_des_ip` / `set_des_port` | `net_bind`(UDP)、`net_listen_locked`、`net_connect_locked`、`net_relisten_locked`、`net_sendto` | open 之后写端口/IP(open 会复位寄存器);set_des_ip 期望**大端数组** | -| `ch395f_write_send_buf` | `net_send_locked` / `net_send_pending_tick`(挂起 send 推进)/ `net_sendto` | 单次 ≤1KB(硬件 FIFO 上限,TRAP-NET-16) | -| `ch395f_get_recv_len` / `ch395f_read_recv_buf` | `net_recv_locked`、`net_pending_tick`、`net_pending_settle_close`、`net_recvfrom`、`net_drain_chip_rx`、`handle_disconnect_event`(M3 可疑门 RX 非空判据) | `handle_recv_event` **故意不读**(防竞争,旧机理未证实) | +| `ch395f_set_proto_type` / `set_sour_port` / `set_des_ip` / `set_des_port` | `net_udp_bind_exec`、`net_udp_set_peer_exec`、`net_udp_sendto_exec`(2026-11-03 起 UDP 三条都只在 netTask 内跑)、`net_listen_locked`、`net_connect_locked`、`net_relisten_locked` | **两种顺序都存在,别当成一条**:TCP 各路径在 open **之后**写(open 会复位寄存器,TRAP-NET-14);UDP bind 按手册 §9.2.4 在 open **之前**写完 5 条——两侧都有钉子(TCP 见 TC-NETSOCK-410 组,UDP 见 ns_s7_udp.c UDP-1f 的命令顺序断言);set_des_ip 期望**线序数组**(`arr[0]` = 点分第一段) | +| `ch395f_write_send_buf` | `net_send_locked`(TCP 与 **UDP sendto 共用**,2026-11-03)/ `net_send_pending_tick`(挂起 send 推进,同样两侧共用) | 单次 ≤1KB(硬件 FIFO 上限,TRAP-NET-16) | +| `ch395f_get_recv_len` / `ch395f_read_recv_buf` | `net_recv_locked`、`net_pending_tick`、`net_pending_settle_close`、`net_udp_recvfrom_exec`、`net_udp_tick`、`net_udp_settle_close`(UDP 剥头与截断排空都在这三处)、`net_drain_chip_rx`、`handle_disconnect_event`(M3 可疑门 RX 非空判据) | `handle_recv_event` **故意不读**(防竞争,旧机理未证实);一次收包**只读一次长度**——剥头量/载荷量/排空量必须由同一个数字算出(`net_udp_datagram_read` 的 `avail` 参数) | | `ch395f_get_socket_status` | `handle_disconnect_event`(两处甄别——事件路径**主动 SPI**,缓存必然滞后于刚到达的中断)、`net_relisten_locked`(沉降门)、`net_connect_locked`(诊断回读)。⚠ **周期对账不再直接调**——改读缓存(下行) | 输出 **2 字节**([0]=Socket 状态 [1]=TCP 状态),缓冲必须给 2 字节 | | `ch395f_get_sock_state` / `ch395f_sock_state_cached` | 刷新**仅 `net_poll`** 每轮对在用 Socket 调 `get_sock_state`(唯一刷新点);`sock_state_cached` 读缓存 = `net_reconcile_tick`(形态①②③④⑤/U11 判据) | 芯片状态唯一事实源;E2 实测单次回读 18.1µs,缓存新鲜度 ≤1ms | | `ch395f_sock_reset_begin` / `ch395f_sock_reset_poll` | `net_sock_reset_sync`(`net_listen_locked` / `net_connect_locked` open 重试、`net_close_locked`、`net_drop_conn_locked`);`net_poll` 每轮对在用 Socket **步进**(IDLE 槽纯 RAM 判零 SPI) | 驱动复位状态机:读空 RX → CLOSE → 排空事件(quiescent) → 等 byte0=00H;内置 LISTEN 态二段 CLOSE 补发(E2 补充发现);GRACEFUL 100ms 底 / QUARANTINE 400ms 底 | diff --git a/docs/问题记录/CH395F_Trap_Records.md b/docs/问题记录/CH395F_Trap_Records.md index db1322e0..6822303a 100644 --- a/docs/问题记录/CH395F_Trap_Records.md +++ b/docs/问题记录/CH395F_Trap_Records.md @@ -33,6 +33,7 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修 | TRAP-NET-19 | SET_MAC 顺序 | ✅ | §9.2.1:SET_MAC 必须在 `INIT_CH395` **之前**(INIT 从 EEPROM 加载) | | TRAP-NET-20 | FTP 数据连接三连故障 | ✅ | 三重定案:状态回读补 accept + 幽灵甄别 / closed-with-data / 背靠背小段毒化 TX("丢中断"归因已被 E14 复核推翻) | | TRAP-NET-21 | IP 显示段序与对端地址 | ✅ | `net_inet_ntoa` 段序 MSB 首;对端地址用 `net_getpeername`,勿用 `getsockname` | +| TRAP-NET-22 | UDP 形态与剥头不一致 | ✅ | DesIP 决定有无 8 字节信息头;旧 `sendto` 静默把芯片切成 Client 形态而 `recvfrom` 仍剥头 → 应答丢前 8 字节 | --- @@ -204,6 +205,7 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修 - **现象**:UDP 发送后立即接收,接收长度恒 0 或数据错误。 - **根因(定案)**:手册要求每次 `WRITE_SEND_BUF` 后必须等 `SINT_STAT_SENBUF_FREE`,否则下次写入被静默丢弃。 - **修复**:每次 `ch395f_write_send_buf()` 后轮询 `ch395f_get_sock_int_status()` 检查该标志。 +- **现状(2026-11-03 UDP 队列化后)**:UDP 发送已改由 netTask 内的 `net_send_locked()` 执行(与 TCP 同一条分块 / 等 SENBUF_FREE 路径,见 `docs/网络/BSD_Socket_API_函数调用关系.md` §4.11),本条要求由该实现统一满足;调用方上下文不再直接碰 SPI,也不再自旋。 ## TRAP-NET-14 · NET 层目的 IP 字节序写反 → CONNECT TIMEOUT @@ -235,6 +237,17 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修 - **教训**:显示层字节序 bug 不影响协议本身(FTP 功能全程正常),但会系统性误导排障——**日志里的地址也要当被测对象核对**。 - **后续(2026-10-24):同一文件里的`s_addr` 两套约定已统一**。修完段序之后,本条只解决了显示侧——当时 `net_inet_addr` 仍属**另一族**(小端累加后又 `NET_HTONL`,返回 `0xC0A80164`),而 `net_inet_ntoa` / `net_getsockname` 属 POSIX 网络序那族(`0x6401A8C0`),两者**不互逆**;按前者写的消费者(`net_connect_locked` 用 `>>24` 取首段)恰好自洽,所以一直没人撞上。现已统一到 **POSIX 网络序一族**(判据 `&s_addr[0] == 第一段`;小端上 192.168.1.100 = `0x6401A8C0`):改 `net_inet_addr` + 出向 IP 数组改按内存字节顺位拆装(**进与出同时换族 → 芯片侧数组净效果不变**)+ 模拟器 `simulator/sim/net_socket_sim.c` 逐字同步;`net_inet_ntoa` 函数体一字未动。**为什么它能长期不被发现**:本条"修复"行下方 `ntoa` 自己注释里的例子(`192.168.1.100 → 0xC0A80164`)**取的是废弃族的值**——注释替错误背书,比没有注释更糟。规范定义见 `docs/网络/BSD_Socket_API_使用指南.md` §9.0;往返不变量由 TC-NETSOCK-100g / 100h 钉住。 +## TRAP-NET-22 · UDP 形态(DesIP)与剥头不一致 → 应答丢前 8 字节 + +- **状态**:✅(2026-11-03 修复,UDP 队列化改造顺带照出) +- **现象**(改前):`net_bind(UDP)` 后 `net_sendto()` 发请求、再 `net_recvfrom()` 收应答,**收到的 48 字节应答少了前 8 字节**(SNTP 用例里魔数 / 时间戳整体左移 8 字节),且 `src_addr` 是垃圾。 +- **根因(定案)**:CH395F 的 UDP 有两种形态,**由 DesIP 这一个寄存器决定,写一次即全局生效**(手册 §9.2.4):DesIP = `0xFFFFFFFF` → **UDP Server**,每个到达包**前置 8 字节信息头**(`[0-1]` 长度、`[2-3]` 源端口、`[4-7]` 源 IP);DesIP = 具体 IP → **UDP Client**,**不带**信息头,且只收该对端的包。旧实现 `net_bind()` 把 DesIP 写成 `0xFFFFFFFF`(服务器形态),但 `net_sendto()` 为了发出这个包把 DesIP 改成目的 IP —— **芯片就此静默切到 Client 形态**,而 `net_recvfrom()` 仍按服务器形态**无条件剥掉 8 字节**(判据只有 `recv_len >= 8`,没有形态条件)。 +- **为什么长期没人发现**:全仓无生产 UDP 调用者(只有 `test/board` 的硬件层用例,直接用 `ch395f_*`、不经这层 API),`docs/网络/BSD_Socket_API_使用指南.md` §6 甚至写着"本项目不使用 UDP";唯一会踩中它的是**请求-响应式单包应答**协议(SNTP / DHCP 类),本仓一个都没有。 +- **修复(定案)**:`net_sock_t` 新增 `udp_mode`(`NET_UDP_MODE_SERVER / CLIENT`,住 `net_types.h`,塞进原有 2 字节 padding → `sizeof(net_sock_t)` 不变);`net_bind()` 恒置 SERVER(BSD 语义里 bind 不选对端);显式对端的 `net_sendto()` 切 CLIENT;新增 `net_udp_set_peer(fd, NULL)` 回 SERVER(只写 DesIP/DesPort、**不重开 Socket**,因此不清芯片缓冲;要把残留包一起丢就再 `net_bind()`);剥头条件收紧为 `udp_mode == SERVER && avail >= 8`,CLIENT 形态源地址取对端记账。 +- **教训**:① **芯片的"形态"是隐式全局状态,API 里每一次"顺手写一个寄存器"都可能切换它**——写之前先问"这一下改变了芯片的什么模式、谁负责改回来";② **解析收包字节流必须有形态前提**,`if (len >= 8) 剥 8` 这种"看着像头就剥"的写法,在协议边界上迟早吃掉载荷;③ "没有生产调用者"不等于"被测过"——这层 API 的 UDP 路径是本轮第一次被端到端测到(`ns_s7_udp.c` UDP-1~14 + TC-NETSOCK-440 / 555)。 +- **关联**:`docs/网络/BSD_Socket_API_使用指南.md` §6.0(形态表 + 谁负责切换)/ §6.3;`docs/网络/BSD_Socket_API_函数调用关系.md` §4.11;`docs/测试说明/单元测试/主机单测覆盖率口径.md`「第 4 轮」;TRAP-NET-14(同一族字节序口径:信息头里源端口小端、源 IP 点分内存顺位);TRAP-NET-10(收包出参为何不能搭 `net_msg_t`)。 +- **未上台架**:本条差异在主机测里靠假件契约钉住(真芯片不校验我们写的值),真机复验项见"第 4 轮"台架清单。 + --- # 第五章 上层集成与主机侧工具 @@ -253,6 +266,8 @@ CH395F 以太网驱动与 NET 层的实证陷阱:现象、定案根因、修 - **根因(定案)**:`osMessageQueue` 传 `net_msg_t` 是**值拷贝**——netTask 修改的是队列副本,调用方栈变量的 `msg.result` 从未被更新。 - **修复**:结果搭 `xTaskNotify(caller, (uint32_t)msg.result, eSetValueWithOverwrite)` 通知值回来,调用方 `xTaskNotifyWait` 取回。**勿给 `net_msg_t` 加"回传用"字段**(调用方只会读到栈垃圾)。 - **注意**:指针成员(`msg.buf`)不受值拷贝影响;内核级 `_locked` API 直接操作不受影响。 +- **现状补充(2026-11-03)**:UDP 队列化时"要回传的不止结果码、还有源地址"这一类需求,按本条纪律**没有**往 `net_msg_t` 加字段,而是走 per-fd 出参槽(调用方 arm 自己的内存,netTask 就地写回,通知值只带结果码)——机制见 `docs/网络/BSD_Socket_API_函数调用关系.md` §4.11,坑见 TRAP-NET-22。 +- **后续(2026-11-03)· UDP 的出参怎么回来**:`net_recvfrom` 的 `src_addr` / `addrlen` 同样**不进** `net_msg_t`——既不能加"回传用"字段,也不建议把调用方指针塞进 `msg.buf` / `msg.len` 这两个已有语义的槽(`net_accept` 那么做是历史例外,它没有别的出参要带)。新模块 `net_udp.c` 改用 **per-fd 出参槽**:调用方 `net_udp_outparam_arm()` 登记指针 → netTask 执行体经该指针就地写回调用方自己的内存 → 通知值只带结果码。两条硬约束:**互斥**(两个任务收同一 fd 时,前者的源地址会被写进后者的缓冲)与**每条出口都清**(槽泄漏 = 该 fd 从此永久 `BUSY`)。 ## TRAP-NET-13 · DHCP 成功后 IP 寄存器提交滞后 diff --git a/scripts/check_registration.py b/scripts/check_registration.py index 15613e78..51139663 100644 --- a/scripts/check_registration.py +++ b/scripts/check_registration.py @@ -134,6 +134,7 @@ SIM_EXEMPT = [ ("Drivers/BSP/NET/net_listener.c", "MCU 监听器生命周期(依赖 ch395f_* SPI 驱动与 net_socket.c 静态状态),模拟器无对应(Winsock 后端无孤儿态/重监听概念)"), ("Drivers/BSP/NET/net_events.c", "MCU 芯片事件处理 + 100ms 周期对账(依赖 ch395f_* SPI 驱动与驱动状态缓存),模拟器无对应(Winsock 后端无\"事件未收到/静默回收\"概念,无失同步可收敛)"), ("Drivers/BSP/NET/net_pending.c", "MCU 挂起 recv 模型(依赖 ch395f_* SPI 读出与 FreeRTOS 通知),模拟器另有 net_socket_sim.c 按外部语义模拟"), + ("Drivers/BSP/NET/net_udp.c", "MCU UDP 数据面执行器与挂起 recv 槽(2026-11-03 UDP 队列化改造;依赖 ch395f_* SPI 读写、net_send_locked 与 FreeRTOS 通知),模拟器另有 net_socket_sim.c 按外部语义模拟"), ("Drivers/BSP/RS485/rs485.c", "MCU RS-485 驱动"), ("Drivers/BSP/SD2506/sd2506.c", "MCU RTC 驱动"), ("Drivers/BSP/ScreenLink/sb_proto.c", "屏板链路协议层纯逻辑(帧 A 解析 / 帧 B 组帧 / CRC-16),消费者 = MCU 侧 USART1 收发起发(批 1/2),模拟器无消费路径;主机侧纯逻辑单测覆盖于 test/host/sb_proto"), diff --git a/scripts/ram_baseline.tsv b/scripts/ram_baseline.tsv index a2d7ecc3..76d32deb 100644 --- a/scripts/ram_baseline.tsv +++ b/scripts/ram_baseline.tsv @@ -15,3 +15,4 @@ date code ro rw zi er_irom1 er_pct iram1 iram1_pct ccm ccm_pct top_rwzi 2026-10-29 243552 28784 1972 145388 272336 26.0 89936 68.6 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6420;nand_ftl.o=6216;wave_ring.o=5296;ch395f.o=4162;console_log.o=4132;sys_log_mcu.o=4055 2026-10-30 246720 29836 2052 149708 276556 26.4 94336 72.0 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6770;ui_port.o=6432;nand_ftl.o=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 2026-11-02 247548 29836 2052 149740 277384 26.5 94368 72.0 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;nand_ftl.o=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 +2026-11-03 249112 29836 2052 149996 278948 26.6 94624 72.2 57376 87.9 heap_4.o=57376;lv_mem_core_builtin.o=16384;lftpd.o=7884;ui_net_sink.o=6802;ui_port.o=6432;nand_ftl.o=6216;wave_ring.o=5296;sb_link.o=4376;ch395f.o=4162;console_log.o=4132 diff --git a/scripts/test_coverage_baseline.json b/scripts/test_coverage_baseline.json index fc8abac3..327c933b 100644 --- a/scripts/test_coverage_baseline.json +++ b/scripts/test_coverage_baseline.json @@ -329,12 +329,12 @@ "lines_valid": 126 }, "Drivers/BSP/NET/net_pending.c": { - "func_rate": 0.75, - "funcs_covered": 12, - "funcs_valid": 16, - "line_rate": 0.8711, - "lines_covered": 169, - "lines_valid": 194 + "func_rate": 0.7647, + "funcs_covered": 13, + "funcs_valid": 17, + "line_rate": 0.8693, + "lines_covered": 173, + "lines_valid": 199 }, "Drivers/BSP/NET/net_select.c": { "func_rate": 1.0, @@ -346,11 +346,19 @@ }, "Drivers/BSP/NET/net_socket.c": { "func_rate": 1.0, - "funcs_covered": 53, - "funcs_valid": 53, - "line_rate": 0.9978, - "lines_covered": 891, - "lines_valid": 893 + "funcs_covered": 59, + "funcs_valid": 59, + "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.9916, + "lines_covered": 237, + "lines_valid": 239 }, "Drivers/BSP/NETCFG/ftp_cfg.c": { "func_rate": 1.0, @@ -396,9 +404,9 @@ "func_rate": 0.7692, "funcs_covered": 10, "funcs_valid": 13, - "line_rate": 0.7302, - "lines_covered": 157, - "lines_valid": 215 + "line_rate": 0.622, + "lines_covered": 158, + "lines_valid": 254 }, "Drivers/BSP/UI/ui_w_soe.c": { "func_rate": 1.0, @@ -573,9 +581,9 @@ }, "net_pending": { "files": { - "Drivers/BSP/NET/net_pending.c": 194 + "Drivers/BSP/NET/net_pending.c": 199 }, - "lines_total": 194, + "lines_total": 199, "ok": true }, "net_select": { @@ -587,9 +595,10 @@ }, "net_socket": { "files": { - "Drivers/BSP/NET/net_socket.c": 893 + "Drivers/BSP/NET/net_socket.c": 969, + "Drivers/BSP/NET/net_udp.c": 239 }, - "lines_total": 893, + "lines_total": 1208, "ok": true }, "phasor": { @@ -730,9 +739,9 @@ }, "ui_sink": { "files": { - "Drivers/BSP/UI/ui_net_sink.c": 215 + "Drivers/BSP/UI/ui_net_sink.c": 254 }, - "lines_total": 215, + "lines_total": 254, "ok": true }, "vol_idle_sync": { @@ -827,6 +836,7 @@ "Drivers/BSP/NET/net_pending.c", "Drivers/BSP/NET/net_select.c", "Drivers/BSP/NET/net_socket.c", + "Drivers/BSP/NET/net_udp.c", "Drivers/BSP/NETCFG/ftp_cfg.c", "Drivers/BSP/NETCFG/net_cfg.c", "Drivers/BSP/ScreenLink/sb_link.c", @@ -837,14 +847,14 @@ ], "totals": { "files_generated_excluded": 5, - "files_tested": 51, - "files_untested": 172, - "func_rate": 0.9243, - "funcs_covered": 598, - "funcs_valid": 647, - "line_rate": 0.9019, - "lines_covered": 8055, - "lines_valid": 8931, + "files_tested": 52, + "files_untested": 173, + "func_rate": 0.9266, + "funcs_covered": 619, + "funcs_valid": 668, + "line_rate": 0.9012, + "lines_covered": 8372, + "lines_valid": 9290, "modules_ok": 47, "modules_total": 47 }, @@ -858,7 +868,7 @@ "path": "Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c" }, { - "code_lines": 533, + "code_lines": 570, "path": "App/task/ui_task.c" }, { @@ -882,7 +892,7 @@ "path": "Drivers/BSP/UI/ui_w_setview.c" }, { - "code_lines": 300, + "code_lines": 303, "path": "Drivers/BSP/UI/ui_content_page.c" }, { @@ -894,7 +904,7 @@ "path": "Drivers/BSP/SD2506/sd2506.c" }, { - "code_lines": 248, + "code_lines": 251, "path": "Drivers/BSP/UI/ui_page_wave_play.c" }, { @@ -953,6 +963,10 @@ "code_lines": 142, "path": "App/task/protect_task.c" }, + { + "code_lines": 134, + "path": "Drivers/BSP/UI/ui_menu.c" + }, { "code_lines": 129, "path": "Drivers/BSP/UI/ui_page_clear.c" @@ -961,10 +975,6 @@ "code_lines": 129, "path": "Drivers/BSP/UI/ui_page_comm.c" }, - { - "code_lines": 126, - "path": "Drivers/BSP/UI/ui_menu.c" - }, { "code_lines": 123, "path": "App/task/net_task.c" @@ -977,18 +987,22 @@ "code_lines": 112, "path": "Drivers/BSP/UI/ui_page_ac.c" }, + { + "code_lines": 104, + "path": "Drivers/BSP/NET/net_types.h" + }, { "code_lines": 103, "path": "Drivers/BSP/UI/ui_port.c" }, - { - "code_lines": 101, - "path": "Drivers/BSP/NET/net_types.h" - }, { "code_lines": 98, "path": "Drivers/BSP/SD2506/sd2506.h" }, + { + "code_lines": 94, + "path": "Drivers/BSP/NET/net_socket.h" + }, { "code_lines": 93, "path": "Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h" @@ -1001,10 +1015,6 @@ "code_lines": 84, "path": "Drivers/BSP/RS485/rs485.c" }, - { - "code_lines": 83, - "path": "Drivers/BSP/NET/net_socket.h" - }, { "code_lines": 77, "path": "App/algo/prot_set.h" @@ -1074,12 +1084,12 @@ "path": "App/algo/adc_input.h" }, { - "code_lines": 42, - "path": "App/cfg/cfg_file.h" + "code_lines": 43, + "path": "Drivers/BSP/UI/ui_content_page.h" }, { "code_lines": 42, - "path": "Drivers/BSP/UI/ui_content_page.h" + "path": "App/cfg/cfg_file.h" }, { "code_lines": 41, @@ -1137,6 +1147,10 @@ "code_lines": 33, "path": "Drivers/BSP/NET/net_select.h" }, + { + "code_lines": 33, + "path": "Drivers/BSP/UI/ui_net_sink.h" + }, { "code_lines": 33, "path": "Drivers/BSP/UI/ui_page_action.h" @@ -1185,6 +1199,10 @@ "code_lines": 27, "path": "App/task/task_handles.h" }, + { + "code_lines": 27, + "path": "Drivers/BSP/NET/net_pending.h" + }, { "code_lines": 27, "path": "Drivers/BSP/UI/ui_w_setview.h" @@ -1197,10 +1215,6 @@ "code_lines": 26, "path": "App/util/task_prof.h" }, - { - "code_lines": 26, - "path": "Drivers/BSP/NET/net_pending.h" - }, { "code_lines": 26, "path": "Drivers/BSP/UI/ui_port.h" @@ -1269,10 +1283,6 @@ "code_lines": 22, "path": "Drivers/BSP/UI/ui_page_yk.c" }, - { - "code_lines": 20, - "path": "Drivers/BSP/UI/ui_net_sink.h" - }, { "code_lines": 19, "path": "App/app/sys_events.h" @@ -1309,6 +1319,10 @@ "code_lines": 15, "path": "App/util/sys_clock.h" }, + { + "code_lines": 15, + "path": "Drivers/BSP/NET/net_udp.h" + }, { "code_lines": 15, "path": "Drivers/BSP/UI/ui_net_sink_backend_mcu.c" @@ -1353,6 +1367,10 @@ "code_lines": 12, "path": "Drivers/BSP/NET/lftpd/private/lftpd_io.h" }, + { + "code_lines": 12, + "path": "Drivers/BSP/UI/ui_menu.h" + }, { "code_lines": 12, "path": "Drivers/BSP/UI/ui_net_sink_backend.h" @@ -1381,10 +1399,6 @@ "code_lines": 11, "path": "Drivers/BSP/NET/lftpd/private/lftpd_inet.h" }, - { - "code_lines": 11, - "path": "Drivers/BSP/UI/ui_menu.h" - }, { "code_lines": 11, "path": "Drivers/BSP/UI/ui_page_calib.h" diff --git a/test/host/common/ch395f_fake.c b/test/host/common/ch395f_fake.c index 1b2c11f5..8ca5d337 100644 --- a/test/host/common/ch395f_fake.c +++ b/test/host/common/ch395f_fake.c @@ -194,7 +194,21 @@ void ch395f_read_recv_buf(uint8_t sock, uint8_t *p_data, uint16_t len) g_chip.last_read_len = len; fake_log('r'); if ((p_data != NULL) && (len > 0U)) { - if (g_fx.recv_bytes_n > 0U) { + if ((g_fx.recv_bytes_n > 0U) && (g_fx.recv_stream_en != 0U)) { + /* 流式读:从游标处继续交(UDP 一个数据报 = 头 + 载荷两次读, + * 每次都从第 0 字节重交的话,"剥头"与"把包头当载荷"断言不出差别)。 + * 游标越过预置内容后补零:截断排空要的就是"读空了这个包"这一事实。 */ + uint16_t left = (g_fx.recv_pos < g_fx.recv_bytes_n) + ? (uint16_t)(g_fx.recv_bytes_n - g_fx.recv_pos) + : 0U; + uint16_t n = (len < left) ? len : left; + + (void)memcpy(p_data, &g_fx.recv_bytes[g_fx.recv_pos], n); + g_fx.recv_pos = (uint16_t)(g_fx.recv_pos + n); + if (n < len) { + (void)memset((uint8_t *)p_data + n, 0, (size_t)(len - n)); + } + } else if (g_fx.recv_bytes_n > 0U) { /* 已预置内容:按预置交付,不足部分补零(默认分支行为不变) */ uint16_t n = (len < g_fx.recv_bytes_n) ? len : g_fx.recv_bytes_n; @@ -219,15 +233,46 @@ void ch395f_fake_set_recv_bytes(const uint8_t *p, uint16_t n) g_fx.recv_bytes[i] = (p != NULL) ? p[i] : (uint8_t)0xA5U; } g_fx.recv_bytes_n = n; + /* 既有语义:内容预置不带动游标(默认关,逐字节行为与 2026-10-24 一致) */ + g_fx.recv_stream_en = 0U; + g_fx.recv_pos = 0U; +} + +/* 内容预置 + 开流式读 + 游标归零:一次调用 = 芯片缓冲里躺着这么多字节 */ +void ch395f_fake_set_recv_stream(const uint8_t *p, uint16_t n) +{ + ch395f_fake_set_recv_bytes(p, n); + g_fx.recv_stream_en = (n > 0U) ? 1U : 0U; + g_fx.recv_pos = 0U; +} + +/* 打开 WRITE_SEND_BUF 内容捕获(默认关:既有假件"内容不可断言"的行为不变) */ +void ch395f_fake_enable_write_capture(void) +{ + g_fx.write_cap_en = 1U; + g_fx.write_cap_n = 0U; + g_fx.write_bytes = 0U; } void ch395f_write_send_buf(uint8_t sock, uint8_t *p_data, uint16_t len) { - (void)p_data; g_chip.write_calls++; g_chip.last_sock = sock; g_chip.last_read_len = len; fake_log('w'); + if (g_fx.write_cap_en != 0U) { + /* 按写入顺序拼接(分块发送时能看出第二块是不是接着第一块的尾巴); + * 越界部分只计数,避免为长报文再开一份大缓冲。 */ + uint16_t space = (uint16_t)(CH395F_FAKE_WRITE_CAP - g_fx.write_cap_n); + uint16_t n = (len < space) ? len : space; + + if ((p_data != NULL) && (n > 0U)) { + (void)memcpy(&g_fx.write_cap[g_fx.write_cap_n], p_data, n); + g_fx.write_cap_n = (uint16_t)(g_fx.write_cap_n + n); + } + g_fx.write_bytes += (uint32_t)len; + } + (void)p_data; } uint8_t ch395f_open_socket(uint8_t sock) diff --git a/test/host/common/ch395f_fake.h b/test/host/common/ch395f_fake.h index c33a99f2..d2b91148 100644 --- a/test/host/common/ch395f_fake.h +++ b/test/host/common/ch395f_fake.h @@ -67,6 +67,9 @@ #define CH395F_FAKE_QUEUE_LEN 24 #define CH395F_FAKE_LOG_LEN 96 #define CH395F_FAKE_GLOB_QUEUE 24 +/* 写内容捕获容量(测试自用,真实头无此宏 → 不参与假芯片常量对齐门): + * 256 够覆盖"一个小数据报全文"与"分块发送首块的前 256 字节"两类断言。 */ +#define CH395F_FAKE_WRITE_CAP 256 /* ===== 扩展区(2026-10-24,net_socket 主机单测批次;**纯新增**,上面既有宏/字段/函数 * 一字未动,net_listener / net_events / net_pending 三套的语义不受影响)===== @@ -266,6 +269,20 @@ typedef struct { * 就还会再报一次。net_socket.c 的多块发送要的就是这个稳态,没有它就只能 * 造"写完一块就再也等不到空闲"的假死形态。 */ uint8_t sint_sticky[CH395F_FAKE_SOCK_MAX]; + /* —— 2026-11-03 UDP 队列化改造追加(默认全 0 → 既有行为逐字不变)—— */ + /* GET_RECV_BUF 的**流式读**开关:UDP 一个数据报 = 8 字节信息头 + 载荷, + * 生产代码是**两次**读(先剥头再读载荷),而 set_recv_bytes 每次都从预置 + * 内容第 0 字节交付——那样"剥掉头"与"把包头当载荷"在断言里长得一样。 + * 本开关让交付内容带游标:第二次读拿到的是第 8 字节往后的载荷。 */ + uint8_t recv_stream_en; + uint16_t recv_pos; /* 游标:已交付字节数(截断排空要断言它) */ + /* WRITE_SEND_BUF 的**内容捕获**开关:假芯片原本忽略 p_data(既有用例只数次数), + * 但 UDP 发送要钉"写进芯片的是不是调用方那份字节、分块边界对不对"。 + * 默认 0 → 既有"内容不可断言"的行为一字不变。 */ + uint8_t write_cap_en; + uint8_t write_cap[CH395F_FAKE_WRITE_CAP]; /* 按写入顺序拼接的字节(够 2 块 1KB 的前 256B) */ + uint16_t write_cap_n; /* 已捕获字节数(超出容量后只计数不存) */ + uint32_t write_bytes; /* 芯片收到的总字节数(含未捕获部分) */ } ch395f_fake_ext_t; typedef struct { @@ -310,6 +327,12 @@ void ch395f_fake_push_glob(uint16_t v); /* 预置全局中断返回序列 */ void ch395f_fake_set_sint_sticky(uint8_t sock, uint8_t mask); /* 预置 GET_RECV_BUF 交付的字节(n=0 撤销预置);p 可为 NULL(表示填 n 个 0xA5) */ void ch395f_fake_set_recv_bytes(const uint8_t *p, uint16_t n); +/* 同上并**开启流式读**(游标归零):UDP 用例要的是"头 8 字节 + 载荷"两次读 + * 各自拿到正确片段,而不是每次都从第 0 字节重交。 */ +void ch395f_fake_set_recv_stream(const uint8_t *p, uint16_t n); +/* 开启 WRITE_SEND_BUF 内容捕获(g_fx.write_cap / write_cap_n / write_bytes): + * 默认关 → 既有"内容不可断言"的行为一字不变。 */ +void ch395f_fake_enable_write_capture(void); /* 顺序断言辅助:清空调用日志 / 找某个短名第一次出现的位置(-1=没调过)。 * 刻意提供"清零日志",否则一次长轮询(如 sock_reset 的几十轮 'x')会把 96 格 * 环形外的顺序信息挤掉,让后续场景的顺序断言失真。 */ diff --git a/test/host/net_socket/net_socket_test.c b/test/host/net_socket/net_socket_test.c index 74d99bd5..fa22c74f 100644 --- a/test/host/net_socket/net_socket_test.c +++ b/test/host/net_socket/net_socket_test.c @@ -50,6 +50,10 @@ int g_ns_debug_count; /* —— 被测生产源 —— */ #include "net_socket.c" +/* UDP 数据面模块(2026-11-03 队列化改造):与 net_socket.c 同一 TU —— + * 它调 net_send_locked / net_get_sock / net_poll / net_dynamic_port_next 等 + * net_socket.c 内部符号,分开编译会撞链接面(本套既有做法) */ +#include "net_udp.c" /* ===================== 断言框架 ===================== */ @@ -90,6 +94,8 @@ static int ns_slot_index_of(TaskHandle_t h) /* 生产模块的 static 在场景之间会残留,每个场景开头必须显式清干净(漏一次红一片) */ static void reset_all(void) { + int i; + (void)memset(s_net_socks, 0, sizeof(s_net_socks)); (void)memset(s_net_errno_slots, 0, sizeof(s_net_errno_slots)); (void)memset(s_local_ip, 0, sizeof(s_local_ip)); @@ -100,6 +106,11 @@ static void reset_all(void) /* UDP 上下文守卫的判据也是生产 static:不清就会把上一场景的"执行任务"带进本场景 * (表现为守卫判错上下文,且与任务句柄复用后的地址撞车) */ s_net_owner_task = NULL; + /* net_udp.c 的 per-fd 槽(挂起接收 + 源地址出参指针 + 武装态)同为生产 static: + * 残留一个 armed 槽,下一场景就会把源地址写进上一场景的栈内存 */ + for (i = 0; i < NET_MAX_SOCKETS; i++) { + net_udp_clear(i); + } g_ns_info_count = 0; g_ns_error_count = 0; g_ns_debug_count = 0; @@ -137,6 +148,25 @@ static void bring_up_netctx(void) g_ns_cur_task = (TaskHandle_t)&g_ns_task_net; } +/* UDP 场景前置:DGRAM + bind(2026-11-03 队列化改造后,UDP 两个数据面接口都要求 + * 芯片侧 Socket 已打开,未 bind 一律 NOTCONN——所以"先 bind"从可选项变成每个 + * UDP 场景的第一行)。**不在这里 bring_up**:当前上下文(netTask 还是 caller) + * 决定 bind 走直接执行还是入队,由场景自己选,本函数只做 socket + bind。 + * 返回 fd,失败返回 -1(bind 失败=假芯片 open 被拒,场景必须显式失败而不是继续跑)。 */ +static int udp_bind_ctx(int port_host) +{ + struct net_sockaddr_in in; + int 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((uint16_t)port_host); + if (net_bind(fd, (const struct net_sockaddr *)&in, (int)sizeof(in)) != 0) { + return -1; + } + return fd; +} + /* 直接造一个在用的控制块(多数场景不需要走完整的 socket()/bind()) */ static int mk_sock(int type, net_sock_state_t state) { @@ -155,6 +185,8 @@ static int mk_sock(int type, net_sock_state_t state) #include "ns_s4_state.c" #include "ns_s5_io.c" #include "ns_s6_edges.c" +/* 2026-11-03 UDP 队列化改造:数据面(模式/剥头/单包交付)+ 串行化 + 挂起收包 */ +#include "ns_s7_udp.c" /* = 分片 1 纯函数面与 fd 表 ===================== */ @@ -706,6 +738,9 @@ int main(void) printf("[shard6] edge branches and remaining exits\n"); ns_run_s6(); + printf("[shard7] UDP datagram plane: serialised chip work, modes, pending receive\n"); + ns_run_s7(); + printf("=== NET Socket Tests: %d/%d PASSED (failed=%d) ===\n", s_passed, s_total, s_failed); return (s_failed == 0) ? 0 : 1; diff --git a/test/host/net_socket/ns_peers.c b/test/host/net_socket/ns_peers.c index fee85cb9..6ea2bc2a 100644 --- a/test/host/net_socket/ns_peers.c +++ b/test/host/net_socket/ns_peers.c @@ -230,6 +230,21 @@ int net_pending_active(int sockfd) /* ================= net_pending.h(send 侧) ================= */ +/* 会话是否已断(net_send_locked / net_send_pending 的共用判据,真实实现按 type 分流)。 + * 桩**必须复刻分流**:写死 TCP 语义的话,UDP 挂起 send 的"会话断了要中止"分支 + * 在本套里永远测不到(2026-11-03 UDP 队列化改造新增边界)。 */ +int net_send_state_down(const net_sock_t *p_sock) +{ + g_peers.state_down_calls++; + if (p_sock == NULL) { + return 1; + } + if (p_sock->type == NET_SOCK_DGRAM) { + return (p_sock->state != NET_SOCK_STATE_UDP_OPEN) ? 1 : 0; + } + return (p_sock->state != NET_SOCK_STATE_ESTABLISHED) ? 1 : 0; +} + int net_send_pending_register(int sockfd, const void *buf, int len, int sent, TaskHandle_t caller) { diff --git a/test/host/net_socket/ns_peers.h b/test/host/net_socket/ns_peers.h index 67c9b54c..59d65350 100644 --- a/test/host/net_socket/ns_peers.h +++ b/test/host/net_socket/ns_peers.h @@ -57,6 +57,8 @@ typedef struct { int send_cancel_all_calls; int last_snd_cancel_err; int send_notified; + /* 会话断开判据被问次数(2026-11-03 起该判据按 type 分流,TCP/UDP 共用一份) */ + int state_down_calls; /* —— net_events —— */ int evt_connect_calls; diff --git a/test/host/net_socket/ns_s5_io.c b/test/host/net_socket/ns_s5_io.c index eb95f062..bcc4664e 100644 --- a/test/host/net_socket/ns_s5_io.c +++ b/test/host/net_socket/ns_s5_io.c @@ -425,7 +425,10 @@ static void tc_430_connect(void) reset_all(); } -/* TC-NETSOCK-440 UDP 数据报:sendto 分块 / recvfrom 剥 8 字节报文头 */ +/* TC-NETSOCK-440 UDP 数据报(2026-11-03 队列化改造后): + * 三条硬前提写进每一段——UDP 必须**先 net_bind**(芯片侧 Socket 没打开就发/收, + * 是 NOTCONN 而不是"看起来能用"),执行任务(netTask)上下文走**直接执行**分支, + * 别的任务走 netMsgQueue(完整队列链路由分片 7 ns_s7_udp.c 钉)。 */ static void tc_440_udp_datagram(void) { static char big[2100]; @@ -445,62 +448,71 @@ static void tc_440_udp_datagram(void) fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); CHECK(net_sendto(fd, NULL, 5, 0, NULL, 0) == -1 && net_get_errno() == NET_ERR_INVAL, - "TC-NETSOCK-440d sendto invalid arguments -> INVAL"); + "TC-NETSOCK-440d sendto invalid arguments -> INVAL (argument checks come before the bind check)"); CHECK(net_recvfrom(fd, NULL, 5, NET_MSG_DONTWAIT, NULL, NULL) == -1 && net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-440e recvfrom with a NULL buf -> INVAL"); + /* 参数没错但还没 bind:新收紧,明确报 NOTCONN(改造前这里会直调芯片 open 都没开的 Socket) */ + CHECK(net_sendto(fd, "x", 1, 0, NULL, 0) == -1 && net_get_errno() == NET_ERR_NOTCONN, + "TC-NETSOCK-440p sendto before net_bind -> NOTCONN (the chip socket is not open yet)"); - /* 带目的地址:先设 DesIP/DesPort 再写数据(不带上限的自旋实现,健康链路一次写完) */ + /* 带目的地址:先设 DesIP/DesPort 再写数据(执行任务上下文=直接执行,健康链路一次写完) */ reset_all(); bring_up_netctx(); - fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + fd = udp_bind_ctx(8888); + CHECK(fd >= 0, "TC-NETSOCK-440q bind precondition"); (void)memset(&in, 0, sizeof(in)); in.sin_port = net_htons(8888); in.sin_addr.s_addr = net_inet_addr("192.168.31.9"); + ch395f_fake_enable_write_capture(); CHECK(net_sendto(fd, "hello", 5, 0, sa, (int)sizeof(in)) == 5, "TC-NETSOCK-440f sendto returns the number of bytes sent"); CHECK(g_fx.des_ip[fd][3] == 9U && g_fx.des_port[fd] == 8888U, "TC-NETSOCK-440g the destination address travels with this send (UDP is connectionless, address is per packet)"); CHECK(g_chip.write_calls == 1 && s_net_socks[fd].send_bytes == 5U, "TC-NETSOCK-440h small packet written in one go plus statistics"); + CHECK(g_fx.write_cap_n == 5U && memcmp(g_fx.write_cap, "hello", 5) == 0, + "TC-NETSOCK-440r the chip received exactly the caller's five bytes (the queue carries a pointer, not a copy of the payload)"); + CHECK(s_net_socks[fd].udp_mode == NET_UDP_MODE_CLIENT, + "TC-NETSOCK-440s an explicit destination puts the chip in client mode and the software books it"); - /* 大报分块(仍受 NET_SEND_CHUNK_MAX 上界约束) */ + /* 大报分块(仍受 NET_SEND_CHUNK_MAX 上界约束,与 TCP 同一份实现) */ reset_all(); bring_up_netctx(); - fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + fd = udp_bind_ctx(8888); g_ns_int_pin_set = 0; ch395f_fake_push_glob((uint16_t)(1U << (fd + 4))); ch395f_fake_set_sint_sticky((uint8_t)fd, CH395F_SINT_STAT_SENBUF_FREE); CHECK(net_sendto(fd, big, 2100, 0, NULL, 0) == 2100, "TC-NETSOCK-440i all 2100 bytes delivered"); CHECK(g_chip.write_calls == 3, "TC-NETSOCK-440j UDP also chunks at the 1024 bound (the FIFO overrun constraint matches TCP)"); - /* recvfrom:前 8 字节是芯片 UDP 头(长度/srcPort/srcIP),要剥掉再交出去 */ + /* recvfrom:服务器模式前 8 字节是芯片信息头(长度/源端口/源 IP),剥掉再交出去 */ reset_all(); bring_up_netctx(); - fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + fd = udp_bind_ctx(8888); ch395f_fake_push_recv_len((uint8_t)fd, 8U + 20U); addrlen = (int)sizeof(in); (void)memset(&in, 0, sizeof(in)); - CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, sa, &addrlen) == 20, + CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, sa, &addrlen) == 20, "TC-NETSOCK-440k returns 20: the 8-byte report header was stripped, the header is not handed over as data"); CHECK(addrlen == (int)sizeof(in) && in.sin_family == NET_AF_INET, "TC-NETSOCK-440l fills the source address structure and length"); - /* 无数据 + 非阻塞 → WOULDBLOCK(阻塞形态是自旋无预算,本套刻意不测,见交付说明) */ + /* 无数据 + 非阻塞 → WOULDBLOCK */ reset_all(); bring_up_netctx(); - fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + fd = udp_bind_ctx(8888); CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == -1 && net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-440m non-blocking with no data -> WOULDBLOCK"); s_net_socks[fd].blocking = 0U; CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, NULL, NULL) == -1 && net_get_errno() == NET_ERR_WOULDBLOCK, "TC-NETSOCK-440n the non-blocking attribute follows the same decision"); - /* 短于 8 字节的残包:不剥头,整包交出去(钉当前实现的边界,不猜"应该怎么算") */ + /* 服务器模式短于 8 字节的残包:无从判头,整包按载荷交付(440o 的既有定案保留) */ reset_all(); bring_up_netctx(); - fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + fd = udp_bind_ctx(8888); ch395f_fake_push_recv_len((uint8_t)fd, 5U); - CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, NULL, NULL) == 5, - "TC-NETSOCK-440o with recv_len<8 no header is stripped, the whole packet is returned (current implementation)"); + CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == 5, + "TC-NETSOCK-440o with recv_len<8 no header is stripped, the whole packet is delivered"); reset_all(); } diff --git a/test/host/net_socket/ns_s6_edges.c b/test/host/net_socket/ns_s6_edges.c index 105c9b50..8876e353 100644 --- a/test/host/net_socket/ns_s6_edges.c +++ b/test/host/net_socket/ns_s6_edges.c @@ -179,37 +179,51 @@ static void tc_505_inner_wait_and_last_chunk(void) reset_all(); } -/* TC-NETSOCK-506 UDP sendto:等待腾出与短写留痕(与 TCP 同一套语义的另一份实现) */ +/* TC-NETSOCK-506 UDP 发送卡住:两条出口都要留痕(2026-11-03 队列化改造后重写) + * 改造前这里钉的是"netTask 内自旋到 1s 停滞超时后短写"——那 1 秒全部花在 netTask + * 里,正是本次要消掉的形态。改造后 UDP 与 TCP 共用 net_send_locked 的预算机: + * ① 执行任务上下文:无人能完成挂起 → 拒绝自等 → -1(BUSY),且不写忙缓冲; + * ② 调用方上下文:超预算转挂起(登记进 net_send_pending,与 TCP 同一张表), + * 登记里带的是**已写进芯片的字节数**——续传从断点起,已交付的字节绝不重发 + * (重发半截数据报 = 对端收到两个包,比丢包更难查)。 + */ static void tc_506_sendto_stall_and_short(void) { static char big[2100]; int fd; int rc; + /* ① 执行任务上下文、发送缓冲一开始就不空闲 */ bring_up_netctx(); - fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + fd = udp_bind_ctx(8888); s_net_socks[fd].send_ready = 0U; g_ns_int_pin_set = 0; ch395f_fake_push_glob((uint16_t)(1U << (fd + 4))); - /* 每轮只走 1ms:先命中 NET_SEND_TIMEOUT_MS(1s) 之前不会有别出口 → 短写 */ + /* 每轮 500ms:停在自旋预算(2ms)与停滞超时(1000ms)之间 → 走"转挂起被拒"这条口 */ g_ns_poll_tick_inc = 500U; rc = net_sendto(fd, big, 2100, 0, NULL, 0); - CHECK(rc < 2100 && rc >= 0, - "TC-NETSOCK-506a sendto stalled returns only what went out (short-write semantics match TCP)"); - CHECK(g_ns_error_count >= 2, - "TC-NETSOCK-506b both the stall [ERR] and the short-write [ERR] must be there (missing one degrades into a silent truncation)"); + CHECK(rc == -1 && net_get_errno() == NET_ERR_BUSY, + "TC-NETSOCK-506a a stall inside netTask now gives up with BUSY instead of spinning a full second and short-writing"); + CHECK(g_chip.write_calls == 0, + "TC-NETSOCK-506b it never wrote into a busy buffer (no half datagram left in the chip FIFO)"); + CHECK(g_peers.send_reg_calls == 1, + "TC-NETSOCK-506c the hand-over was really attempted (the refusal comes from the register side, not from us not trying to hand over)"); reset_all(); - /* 写完一块后等发完的超时出口 */ - bring_up_netctx(); - fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + /* ② 调用方上下文:第一块写得进、之后芯片不再报空闲 → 第二块转挂起 */ + bring_up(); + fd = udp_bind_ctx(8888); g_ns_int_pin_set = 0; ch395f_fake_push_glob((uint16_t)(1U << (fd + 4))); - g_fake.int_status[fd] = CH395F_SINT_STAT_SENBUF_FREE; /* 只给一次 */ + g_fake.int_status[fd] = CH395F_SINT_STAT_SENBUF_FREE; /* 只给一次空闲 */ g_ns_poll_tick_inc = 500U; - rc = net_sendto(fd, big, 2100, 0, NULL, 0); - CHECK(rc >= 1024 && rc < 2100, - "TC-NETSOCK-506c first chunk delivered and the second chunk wait timed out -> a value between 1024 and 2099"); + rc = net_send_locked(&s_net_socks[fd], big, 2100, (TaskHandle_t)&g_ns_task_caller); + CHECK(rc == NET_PENDING_REGISTERED, + "TC-NETSOCK-506d a UDP send past the spin budget converts to pending exactly like TCP (no in-place busy wait)"); + CHECK(g_peers.send_reg_calls == 1 && g_peers.last_snd_sent == 1024, + "TC-NETSOCK-506e the pending entry carries the bytes already written (1024), so the resume starts at the break point"); + CHECK(g_chip.write_calls == 1, + "TC-NETSOCK-506f exactly one chunk reached the chip before handing over"); reset_all(); } @@ -447,7 +461,10 @@ static void tc_555_udp_src_addr(void) int fd; bring_up_netctx(); - fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + /* 2026-11-03 队列化改造:剥头只发生在**服务器模式**,而服务器模式由 bind 建立 + * (DesIP=0xFFFFFFFF)。改造前这段直接 net_socket 就能收,是因为它无条件剥头—— + * 那正是本组要修的缺陷(客户端模式没有头,照剥就吃掉载荷前 8 字节)。 */ + fd = udp_bind_ctx(8888); ch395f_fake_push_recv_len((uint8_t)fd, (uint16_t)(8U + 3U)); ch395f_fake_set_recv_bytes(hdr, (uint16_t)sizeof(hdr)); @@ -467,91 +484,84 @@ static void tc_555_udp_src_addr(void) } -/* TC-NETSOCK-560 UDP 上下文守卫:非执行任务不得直调芯片 - * 三条一起才成立,缺一条都不算把危险面关掉: - * ① 执行任务内照常成功(守卫不是"把 UDP 判死") - * ② 非执行任务被拒且**一次芯片事务都没有**(拒了却已经抢过 SPI = 白拒) - * ③ 正常错误路径不被守卫吞掉,且**走消息队列的接口完全不受影响** - * (那些接口生来就是给非 netTask 用的,误伤它们才是大事故) +/* TC-NETSOCK-560 UDP 上下文契约(2026-11-03 队列化改造**反转**了本组的原命题) + * 改造前:非执行任务调用 net_sendto / net_recvfrom 一律 NET_ERR_NOT_IN_NETTASK(-12) + * ——那是 2026-10-24"先把危险面关掉"的临时守卫(SPI 竞争 TRAP-NET-14)。 + * 改造后:任意任务都能调,芯片事务全部串行到 netTask 执行;守卫随之退役。 + * 三条一起才成立,缺一条都不算把改造钉住: + * ① 非执行任务调用**成功**,且芯片事务发生在泵(=netTask 上下文)里,不在调用方 + * ② 执行任务内直接执行,不投自己(板级用例照旧可在自己任务里裸调) + * ③ 参数/状态错误优先于上下文;-12 这个码在 UDP 路径上不再出现 */ static void tc_560_udp_context_guard(void) { char buf[8]; int fd; - /* ① 执行任务上下文:与修复前逐字相同 */ - bring_up_netctx(); - fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); - CHECK(net_sendto(fd, "abc", 3, 0, NULL, 0) == 3, - "TC-NETSOCK-560a net_sendto unchanged inside the executing task"); - reset_all(); - - /* ② 非执行任务:拒绝 + 零芯片事务(bring_up 里 net_init 是以 netTask 身份跑的, - * 此刻 g_ns_cur_task 已还原成 caller,正是"别的任务") */ + /* ① 非执行任务(bring_up 已把身份还原成 caller):改造前被拒,改造后必须成 */ bring_up(); - fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + fd = udp_bind_ctx(8888); + CHECK(fd >= 0, "UDP-560a0 bind from a caller task succeeds (queue path, no direct SPI)"); g_chip.write_calls = 0; g_chip.read_calls = 0; g_fxchip.set_des_ip_calls = 0; - g_fxchip.glob_calls = 0; - g_ns_poll_calls = 0; - g_ns_error_count = 0; + g_ns_put_calls = 0; + g_ns_notify_calls = 0; + CHECK(net_sendto(fd, "abc", 3, 0, NULL, 0) == 3, + "UDP-560a non-executing task's sendto now succeeds (it used to be refused with -12)"); + CHECK(g_chip.write_calls == 1 && g_ns_put_calls >= 1 && g_ns_notify_calls >= 1, + "UDP-560b the write happened while the pump ran as netTask: the caller enqueued, blocked, and was notified (serialised, not forbidden)"); + CHECK(net_get_errno() != NET_ERR_NOT_IN_NETTASK, + "UDP-560c the retired guard does not come back: -12 is never returned on a success path"); - CHECK(net_sendto(fd, "abc", 3, 0, NULL, 0) == -1 && - net_get_errno() == NET_ERR_NOT_IN_NETTASK, - "TC-NETSOCK-560b non-executing task gets NET_ERR_NOT_IN_NETTASK (exact code, not -1)"); - CHECK(g_chip.write_calls == 0 && g_fxchip.set_des_ip_calls == 0 && - g_ns_poll_calls == 0, - "TC-NETSOCK-560c clean refusal: zero writes, zero set_des_ip, zero net_poll rounds"); - CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == -1 && - net_get_errno() == NET_ERR_NOT_IN_NETTASK, - "TC-NETSOCK-560d net_recvfrom guarded the same way"); - CHECK(g_chip.read_calls == 0 && g_fxchip.glob_calls == 0 && g_ns_poll_calls == 0, - "TC-NETSOCK-560e recvfrom touched no chip and no global interrupt register either"); - CHECK(g_ns_error_count == 2, - "TC-NETSOCK-560f each refusal logs one ERR line (silence hides the wrong task)"); + ch395f_fake_push_recv_len((uint8_t)fd, 8U + 3U); + s_net_socks[fd].readable = 1U; + CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == 3, + "UDP-560d non-executing task's recvfrom goes the same route and returns the payload"); + CHECK(g_chip.read_calls >= 2 && g_ns_block_waits >= 1, + "UDP-560e the caller slept on its own notification while netTask did the SPI reads (the old spin-in-caller shape is gone)"); - /* ③ 正常错误路径优先:守卫排在参数校验之后 */ + /* ③ 参数检查排在上下文与状态之前(守卫退役不许顺手把错误优先级改掉) */ CHECK(net_sendto(-1, "abc", 3, 0, NULL, 0) == -1 && - net_get_errno() == NET_ERR_BADF, "TC-NETSOCK-560g out-of-range fd still reports BADF first"); + net_get_errno() == NET_ERR_BADF, "UDP-560f out-of-range fd still reports BADF first"); CHECK(net_sendto(fd, NULL, 3, 0, NULL, 0) == -1 && - net_get_errno() == NET_ERR_INVAL, - "TC-NETSOCK-560h NULL buf still reports INVAL, the guard does not swallow it"); + net_get_errno() == NET_ERR_INVAL, "UDP-560g NULL buf still reports INVAL"); CHECK(net_recvfrom(fd, NULL, 3, NET_MSG_DONTWAIT, NULL, NULL) == -1 && - net_get_errno() == NET_ERR_INVAL, "TC-NETSOCK-560i recvfrom keeps the same precedence"); - - /* ④ 走消息队列的接口不受守卫影响:同一个非 netTask 上下文照常能把活干完 */ - CHECK(net_close(fd) == 0, - "TC-NETSOCK-560j net_close from a non-executing task still works (queue path untouched)"); - /* 执行完的证据用"收到回执 + 槽位释放",不看 close_calls:芯片此刻已是 - * CLOSED,走的是快路径(不再补发 CLOSE,见 TC-NETSOCK-320a/b),拿它当证据 - * 会把一条正确的快路径判成失败 */ - CHECK(s_net_socks[fd].in_use == 0U && g_ns_notify_calls >= 1, - "TC-NETSOCK-560k queue path really completed (receipt seen and slot released)"); + net_get_errno() == NET_ERR_INVAL, "UDP-560h recvfrom keeps the same precedence"); reset_all(); - /* ⑤ 未捕获执行任务(net_init 没跑):守卫不抢先,仍由参数/状态检查给确定错误 */ + /* ② 执行任务上下文:直接执行,不投自己(板级 testTask 形态) */ + bring_up_netctx(); + fd = udp_bind_ctx(8888); + g_ns_put_calls = 0; + CHECK(net_sendto(fd, "abc", 3, 0, NULL, 0) == 3, + "UDP-560i netTask context sends directly"); + CHECK(g_ns_put_calls == 0, + "UDP-560j the executing task never enqueues to itself (a self-post would have to wait for its own pump)"); reset_all(); - g_ns_pump = net_process_messages; - CHECK(net_sendto(0, "abc", 3, 0, NULL, 0) == -1 && - net_get_errno() == NET_ERR_BADF, - "TC-NETSOCK-560l before net_init sendto reports BADF, no invented context error"); - /* 未捕获执行任务、但 socket 确实在用时,守卫**选择放行**:宁可不做判定,也不给 - * 一个无从判定的场景编造错误码。放行后走的是完全正常的执行路径(这里 DONTWAIT - * 无数据 → WOULDBLOCK),证明守卫不是"没初始化就一律拒绝"的粗暴实现。 - * 刻意用 recvfrom 而不是 sendto:未初始化时 net_poll 直接短路返回、时间基准 - * 不前进,sendto 的等待环永远等不到超时(真机上不可达:net_socket 之后还有 - * NET_READY 屏障,但这条差异值得记一笔,见交付报告)。 */ + /* ④ 队列未绑定(net_init 之前的早期上下文):与其他队列化接口同一口径 NET_ERR, + * 不是 BADF 也不是 -12——无从投递就是无从投递 */ reset_all(); - g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; /* net_init 没跑:判据恒 NULL */ + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); - g_fxchip.glob_calls = 0; + CHECK(net_sendto(fd, "abc", 3, 0, NULL, 0) == NET_ERR && + s_net_udp[fd].armed == 0U, + "UDP-560k before net_init (no queue bound) sendto returns NET_ERR and leaves no slot armed"); + CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == NET_ERR, + "UDP-560l the same for recvfrom (the armed out-param slot is handed back on that exit)"); + reset_all(); + + /* ⑤ 执行任务内的**阻塞** UDP 收包仍被拒(改造没有打开自等死锁的门): + * 完成它的 tick 就跑在同一个任务里 */ + bring_up_netctx(); + fd = udp_bind_ctx(8888); + CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, NULL, NULL) == -1 && + net_get_errno() == NET_ERR_BUSY, + "UDP-560m a blocking UDP receive inside netTask is refused with BUSY (self-wait = deadlock)"); CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == -1 && net_get_errno() == NET_ERR_WOULDBLOCK, - "TC-NETSOCK-560m guard stays open when the executing task was never captured"); - CHECK(net_get_errno() != NET_ERR_NOT_IN_NETTASK && g_fxchip.glob_calls == 0, - "TC-NETSOCK-560n no context error invented and no global register read"); + "UDP-560n the same call with NET_MSG_DONTWAIT is the legal netTask form (WOULDBLOCK, no wait)"); reset_all(); } diff --git a/test/host/net_socket/ns_s7_udp.c b/test/host/net_socket/ns_s7_udp.c new file mode 100644 index 00000000..aaf2058c --- /dev/null +++ b/test/host/net_socket/ns_s7_udp.c @@ -0,0 +1,1284 @@ +/* + * 模块名称:UDP 数据面分片(net_socket 主机侧单测,2026-11-03 UDP 队列化改造) + * 模块功能:钉 UDP 从"调用方上下文直调 SPI"改为"netMsgQueue 交 netTask 串行执行" + * 之后的全部契约,以及改造顺带修掉的**芯片模式缺陷**: + * - DesIP=0xFFFFFFFF(服务器模式)收包带 8 字节信息头; + * - DesIP=具体 IP(客户端模式)收包**没有**信息头。 + * 改造前 recvfrom 恒剥 8 字节,客户端模式(SNTP 这类请求-应答)等于吃掉 + * 载荷前 8 字节并回填一个假源地址——本分片用**逐字节内容**把两种模式钉死。 + * + * 与既有分片的分工: + * - ns_s5/ns_s6 里 440/555/560 三组是"改造前形态"的历史钉子,已按新契约改写; + * - 本分片只测**队列化之后**新增或变化的决策:出参经 per-fd 槽回传、挂起 recv、 + * 截断排空、模式切换、以及"任何一条出口都必须通知调用方"。 + * + * ⚠ 断言只钉真实契约(AGENTS 模块约定 + docs/芯片手册/CH395F.md §9.2.4 + + * docs/问题记录/CH395F_Trap_Records.md),不钉假件规则。 + * + * 创建日期:2026-11-03 + */ + +/* 造一个"已 bind 的 UDP fd"(bind 走队列,故调用前的上下文决定走哪条路) */ +static int udp_bind(int port_host) +{ + struct net_sockaddr_in in; + int 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((uint16_t)port_host); + if (net_bind(fd, (const struct net_sockaddr *)&in, (int)sizeof(in)) != 0) { + return -1; + } + return fd; +} + +/* 组一个"服务器模式数据报"的字节流:8 字节信息头 + n 字节载荷,写进假芯片的 + * 可读流并返回总长。头与载荷是分两次读的,所以内容必须按芯片缓冲的真实字节序预置。 */ +static int udp_build_server_datagram(uint8_t *buf, int cap, const char *payload, int n, + uint16_t src_port_host, const char *src_ip) +{ + uint8_t ip[4]; + int i; + + if (n + 8 > cap) { + return -1; + } + (void)memset(buf, 0, (size_t)(8 + n)); + parse_ip_string(src_ip, ip); + buf[0] = (uint8_t)(n & 0xFF); /* [0-1] 数据长度(本层不消费,仅占位) */ + buf[1] = (uint8_t)((n >> 8) & 0xFF); + buf[2] = (uint8_t)(src_port_host & 0xFFU); /* [2-3] 源端口:芯片小端 */ + buf[3] = (uint8_t)((src_port_host >> 8) & 0xFFU); + buf[4] = ip[0]; /* [4-7] 源 IP 线序:首段在低地址 */ + buf[5] = ip[1]; + buf[6] = ip[2]; + buf[7] = ip[3]; + for (i = 0; i < n; i++) { + buf[8 + i] = (uint8_t)payload[i]; + } + return 8 + n; +} + +/* "此刻就有这一个数据报可读":流 + 长度一起给(多数用例走这条) */ +static void udp_script_server_datagram(const char *payload, int n, + uint16_t src_port_host, const char *src_ip) +{ + uint8_t buf[128]; + int total = udp_build_server_datagram(buf, (int)sizeof(buf), payload, n, + src_port_host, src_ip); + + if (total < 0) { + return; + } + ch395f_fake_set_recv_stream(buf, (uint16_t)total); + ch395f_fake_push_recv_len(0, (uint16_t)total); /* 覆盖所有 fd:只有一个在用 */ +} + +/* "内容先备好、长度还没宣布":测"先没有、后面才到"必须这样分—— + * 芯片长度才是"有没有包"的唯一依据,长度一宣布就是"现在就有"。 */ +static void udp_stage_server_datagram(const char *payload, int n, + uint16_t src_port_host, const char *src_ip) +{ + uint8_t buf[128]; + int total = udp_build_server_datagram(buf, (int)sizeof(buf), payload, n, + src_port_host, src_ip); + + if (total < 0) { + return; + } + ch395f_fake_set_recv_stream(buf, (uint16_t)total); +} + +/* s_net_udp 是 net_udp.c 的生产 static:本分片直接读它的挂起标志, + * 不为了测试在生产里开一个只读 API(既有分片同样直读 s_net_socks)。 */ +static int net_udp_pending(int fd) +{ + return (s_net_udp[fd].pending != 0U) ? 1 : 0; +} + +static int net_udp_armed(int fd) +{ + return (s_net_udp[fd].armed != 0U) ? 1 : 0; +} + +/* 手工投一条 UDP 收包消息(不经公开入口):公开入口在"挂起"这条出口上会睡在 + * 任务通知里,测试拿不到"已登记、回执未发"这个中间态;这里自己装消息自己入队, + * 再自己以 netTask 身份泵一轮,断言登记与取消。与 net_recvfrom_impl 的入队段逐字同形。 */ +static void udp_arm_and_enqueue(int fd, void *buf, int len, uint32_t to_ms, net_msg_t *msg) +{ + (void)memset(msg, 0, sizeof(*msg)); + msg->type = NET_MSG_UDP_RECVFROM; + msg->sockfd = fd; + msg->buf = buf; + msg->len = len; + msg->timeout_ms = to_ms; + msg->caller = (TaskHandle_t)&g_ns_task_caller; + (void)net_udp_outparam_arm(fd, NULL, NULL); + (void)ns_rtos_q_put_raw(msg); +} + +/* ================= UDP-1 bind:命令序列与模式复位 ================= */ + +static void tc_udp1_bind_sequence(void) +{ + struct net_sockaddr_in in; + int 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(123U); + + /* 队列化:调用方任务里 bind 不该碰芯片一下(TRAP-NET-14 的正面形态) */ + ch395f_fake_clear_log(); + CHECK(net_bind(fd, (const struct net_sockaddr *)&in, (int)sizeof(in)) == 0, + "UDP-1a caller-context bind succeeds (enqueued, executed in netTask, result via notify)"); + CHECK(s_net_socks[fd].state == NET_SOCK_STATE_UDP_OPEN, + "UDP-1b state UDP_OPEN (open succeeded, no listen state in UDP)"); + CHECK(s_net_socks[fd].udp_mode == NET_UDP_MODE_SERVER, + "UDP-1c bind is always server mode (BSD bind names a local port, it never picks a peer)"); + CHECK(g_fx.des_ip[fd][0] == 0xFFU && g_fx.des_ip[fd][1] == 0xFFU && + g_fx.des_ip[fd][2] == 0xFFU && g_fx.des_ip[fd][3] == 0xFFU && + g_fx.des_port[fd] == 0U, + "UDP-1d DesIP=0xFFFFFFFF + DesPort=0 (the hardware definition of server mode)"); + CHECK(g_chip.last_proto == CH395F_PROTO_TYPE_UDP && g_chip.last_port == 123U, + "UDP-1e proto UDP then source port 123 (handbook §9.2.4 order)"); + CHECK(ch395f_fake_log_find('P') >= 0 && + ch395f_fake_log_find('P') < ch395f_fake_log_find('o'), + "UDP-1f DesIP/ports come BEFORE OPEN_SOCKET (open resets them: the ordering is the bug fix)"); + CHECK(g_chip.open_calls == 1, "UDP-1g exactly one open"); + + /* 端口 0 = 动态取号,取号游标在 netTask 内推进(原来在调用方上下文改静态量) */ + reset_all(); + bring_up(); + fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + (void)memset(&in, 0, sizeof(in)); + in.sin_family = NET_AF_INET; + CHECK(net_bind(fd, (const struct net_sockaddr *)&in, (int)sizeof(in)) == 0, + "UDP-1h bind with port 0 succeeds"); + CHECK(s_net_socks[fd].local_port == 49152U && g_chip.last_port == 49152U, + "UDP-1i dynamic port allocated inside netTask and written to the chip (no cross-context write to s_dynamic_port)"); + reset_all(); +} + +/* ================= UDP-2 未 bind 的接口一律拒绝(新收紧) ================= */ + +static void tc_udp2_requires_bind(void) +{ + struct net_sockaddr_in in; + struct net_sockaddr *sa = (struct net_sockaddr *)∈ + char buf[16]; + int 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(123U); + in.sin_addr.s_addr = net_inet_addr("192.168.31.9"); + + CHECK(net_sendto(fd, "hi", 2, 0, sa, (int)sizeof(in)) == -1 && + net_get_errno() == NET_ERR_NOTCONN, + "UDP-2a sendto before bind -> NOTCONN (an unopened chip socket cannot send)"); + CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == -1 && + net_get_errno() == NET_ERR_NOTCONN, + "UDP-2b recvfrom before bind -> NOTCONN"); + CHECK(net_udp_set_peer(fd, &in) == -1 && net_get_errno() == NET_ERR_NOTCONN, + "UDP-2c set_peer before bind -> NOTCONN (writing DesIP before open would be reset by open)"); + CHECK(g_chip.write_calls == 0 && g_chip.open_calls == 0 && g_fxchip.set_des_ip_calls == 0, + "UDP-2d a refusal is a clean refusal: no write, no open, no DesIP"); + reset_all(); +} + +/* ================= UDP-3 服务器模式:剥头 + 源地址逐字节 ================= */ + +static void tc_udp3_server_strip(void) +{ + struct net_sockaddr_in in; + struct net_sockaddr *sa = (struct net_sockaddr *)∈ + char buf[64]; + int addrlen; + int fd; + int rc; + + bring_up(); + fd = udp_bind(123); + CHECK(fd >= 0, "UDP-3a bind precondition"); + + /* 载荷 20 字节,头里写着 src 192.168.31.9:51999 */ + udp_script_server_datagram("ABCDEFGHIJKLMNOPQRST", 20, 51999U, "192.168.31.9"); + s_net_socks[fd].readable = 1U; + addrlen = (int)sizeof(in); + (void)memset(&in, 0, sizeof(in)); + rc = net_recvfrom(fd, buf, (int)sizeof(buf), 0, sa, &addrlen); + CHECK(rc == 20, "UDP-3b server mode returns the payload length only (20 of 28 chip bytes)"); + CHECK((int)g_fx.recv_pos == 28, + "UDP-3c the whole datagram was read out (header + payload): the handbook requires draining in one go"); + CHECK(memcmp(buf, "ABCDEFGHIJKLMNOPQRST", 20) == 0, + "UDP-3d delivered bytes are the payload, byte for byte (not the header, not shifted)"); + CHECK(addrlen == (int)sizeof(in) && in.sin_family == NET_AF_INET, + "UDP-3e source address filled and addrlen reported"); + CHECK(in.sin_addr.s_addr == net_inet_addr("192.168.31.9"), + "UDP-3f source IP matches the net_inet_addr family (line-order header -> network-order s_addr, no extra byteswap)"); + CHECK(net_ntohs(in.sin_port) == 51999U, + "UDP-3g source port: chip little-endian in the header, network-order in the struct"); + CHECK(s_net_socks[fd].readable == 0U, + "UDP-3h readable cleared after the read (a level-triggered event must not fire the same packet twice)"); + CHECK(s_net_socks[fd].recv_bytes == 20U, + "UDP-3i statistics count delivered payload, not the chip header"); + reset_all(); +} + +/* ================= UDP-4 客户端模式:不剥头(改造修的缺陷本体) ================= */ + +static void tc_udp4_client_no_strip(void) +{ + struct net_sockaddr_in in; + struct net_sockaddr *sa = (struct net_sockaddr *)∈ + uint8_t stream[64]; + char buf[64]; + int addrlen; + int fd; + int rc; + int i; + + bring_up(); + fd = udp_bind(123); + (void)memset(&in, 0, sizeof(in)); + in.sin_family = NET_AF_INET; + in.sin_port = net_htons(123U); + in.sin_addr.s_addr = net_inet_addr("119.28.220.214"); + CHECK(net_udp_set_peer(fd, &in) == 0, "UDP-4a set_peer to an NTP-like server succeeds"); + CHECK(s_net_socks[fd].udp_mode == NET_UDP_MODE_CLIENT, "UDP-4b mode is now client"); + CHECK(g_fx.des_ip[fd][0] == 119U && g_fx.des_ip[fd][1] == 28U && + g_fx.des_ip[fd][2] == 220U && g_fx.des_ip[fd][3] == 214U && + g_fx.des_port[fd] == 123U, + "UDP-4c DesIP carries the peer octets in line order (the chip's own byte convention)"); + CHECK(g_chip.open_calls == 1, + "UDP-4d switching mode does NOT reopen the socket (a reopen would drop buffered replies)"); + + /* 48 字节 SNTP 应答,前 8 字节故意长得像"信息头"——旧实现会把它们剥掉 */ + (void)memset(stream, 0, sizeof(stream)); + for (i = 0; i < 48; i++) { + stream[i] = (uint8_t)(0x24 + i); /* 0x24 = LI=0,VN=4,Mode=3 起手 */ + } + ch395f_fake_set_recv_stream(stream, 48U); + ch395f_fake_push_recv_len((uint8_t)fd, 48U); + s_net_socks[fd].readable = 1U; + addrlen = (int)sizeof(in); + (void)memset(&in, 0, sizeof(in)); + rc = net_recvfrom(fd, buf, (int)sizeof(buf), 0, sa, &addrlen); + CHECK(rc == 48, + "UDP-4e client mode delivers ALL 48 bytes (the pre-refactor build returned 40 and silently ate the first 8)"); + CHECK((uint8_t)buf[0] == 0x24U && (uint8_t)buf[7] == 0x2BU, + "UDP-4f the first delivered byte is the reply itself, not the byte after a stripped header"); + CHECK(in.sin_addr.s_addr == net_inet_addr("119.28.220.214") && + net_ntohs(in.sin_port) == 123U, + "UDP-4g source address comes from this socket's peer bookkeeping (client mode has no header to read)"); + reset_all(); +} + +/* ================= UDP-5 带地址发送 = 切客户端模式,不带地址 = 不动 ================= */ + +static void tc_udp5_sendto_dest_switches_mode(void) +{ + struct net_sockaddr_in in; + struct net_sockaddr *sa = (struct net_sockaddr *)∈ + int fd; + + bring_up(); + fd = udp_bind(123); + ch395f_fake_enable_write_capture(); + (void)memset(&in, 0, sizeof(in)); + in.sin_family = NET_AF_INET; + in.sin_port = net_htons(8888); + in.sin_addr.s_addr = net_inet_addr("192.168.31.9"); + + CHECK(net_sendto(fd, "ping", 4, 0, sa, (int)sizeof(in)) == 4, "UDP-5a sendto with a destination returns the byte count"); + CHECK(s_net_socks[fd].udp_mode == NET_UDP_MODE_CLIENT, + "UDP-5b an explicit destination silently switches the chip to client mode: the software now books it (that is the whole point of udp_mode)"); + CHECK(g_fx.des_ip[fd][3] == 9U && g_fx.des_port[fd] == 8888U, + "UDP-5c the destination travels with this one send (address is per packet in UDP)"); + CHECK(g_fx.write_cap_n == 4U && g_fx.write_bytes == 4U && + memcmp(g_fx.write_cap, "ping", 4) == 0, + "UDP-5d the chip received exactly the caller's bytes, not a copy of something else"); + CHECK(s_net_socks[fd].remote_port == 8888U && + s_net_socks[fd].remote_ip == net_inet_addr("192.168.31.9"), + "UDP-5e peer bookkeeping written in the same family as net_connect (remote_ip is network order)"); + + /* 不带地址:DesIP 保持上次的对端,模式也不动;载荷按写入顺序续在捕获流后面。 + * 上一块的 SENBUF_FREE 不在本场景要考的范围内(挂起续传见 TC-NETSOCK-506d), + * 这里直接把"缓冲又空了"这一步做掉,等价于 net_poll 收到 SENBUF_FREE。 */ + s_net_socks[fd].send_ready = 1U; + (void)memset(&in, 0, sizeof(in)); + CHECK(net_sendto(fd, "pong", 4, 0, NULL, 0) == 4, "UDP-5f sendto with NULL destination succeeds"); + CHECK(g_fx.write_bytes == 8U && memcmp(&g_fx.write_cap[4], "pong", 4) == 0, + "UDP-5g the destination-less send still carries the caller's own bytes (the chunk pointer is advanced, not re-based)"); + CHECK(g_fx.des_ip[fd][3] == 9U && g_fx.des_port[fd] == 8888U, + "UDP-5h NULL destination means reuse, not reset (unchanged DesIP/DesPort)"); + CHECK(s_net_socks[fd].udp_mode == NET_UDP_MODE_CLIENT, "UDP-5i mode unchanged by a destination-less send"); + reset_all(); +} + +/* ================= UDP-6 挂起 recv:数据到达即结束(netTask 零占用) ================= */ + +/* 泵:以 netTask 身份跑一轮分派,并按脚本伪造"某一轮才有数据"。 + * 真机上 netTask 是 1ms 主循环,HAL_GetTick 自己走;这里的假时钟必须由泵推进, + * 否则挂起 recv 的 deadline 永远不到点(表现为等满 50 轮被假件判成漏通知)。 + * s_udp_pump_arrive_round < 0 → 纯推进时钟,不注入数据(测超时用) + * s_udp_pump_len > 0 → 到那一轮置 readable 并给出该长度的芯片数据 */ +static int s_udp_pump_arrive_round = -1; +static int s_udp_pump_fd; +static int s_udp_pump_len; +static uint32_t s_udp_pump_tick_inc = 1U; +static int s_udp_pump_calls; + +static void udp_pump(void) +{ + if ((s_udp_pump_arrive_round >= 0) && (s_udp_pump_calls == s_udp_pump_arrive_round)) { + s_net_socks[s_udp_pump_fd].readable = 1U; + ch395f_fake_push_recv_len((uint8_t)s_udp_pump_fd, (uint16_t)s_udp_pump_len); + } + s_udp_pump_calls++; + g_ns_tick += s_udp_pump_tick_inc; + net_process_messages(); +} + +static void udp_pump_stop(void) +{ + g_ns_pump = net_process_messages; + s_udp_pump_arrive_round = -1; + s_udp_pump_len = 0; + s_udp_pump_tick_inc = 1U; +} + +static void tc_udp6_pending_recv_arrival(void) +{ + struct net_sockaddr_in in; + struct net_sockaddr *sa = (struct net_sockaddr *)∈ + char buf[32]; + int addrlen; + int fd; + int rc; + + bring_up(); + fd = udp_bind(123); + s_udp_pump_fd = fd; + /* 内容先备好但**不宣布长度**:第 0 轮芯片缓冲是空的,数据报到第 1 轮才出现 + * (8 字节头 + 12 字节载荷)——顺序反了就变成"一进来就有数据",测不到挂起 */ + udp_stage_server_datagram("TIME12345678", 12, 51999U, "192.168.31.9"); + ch395f_fake_push_recv_len((uint8_t)fd, 0U); + s_udp_pump_arrive_round = 1; + s_udp_pump_len = 8 + 12; + s_udp_pump_calls = 0; + g_ns_pump = udp_pump; + g_ns_int_pin_set = 0; /* 有 INT# 电平但不注入 SINT:readable 由泵自己造 */ + addrlen = (int)sizeof(in); + (void)memset(&in, 0, sizeof(in)); + + rc = net_recvfrom_to(fd, buf, (int)sizeof(buf), 5000U, sa, &addrlen); + udp_pump_stop(); + + CHECK(rc == 12, "UDP-6a a blocked recv ends with the datagram that arrives later (the caller slept on its own notification, not on a spin)"); + CHECK(memcmp(buf, "TIME12345678", 12) == 0, "UDP-6b the late datagram's payload is delivered to the caller buffer"); + CHECK(net_ntohs(in.sin_port) == 51999U, "UDP-6c the source address is filled by netTask into the caller's own memory (no return field on net_msg_t)"); + CHECK(s_udp_pump_calls >= 2, + "UDP-6d the wait really slept and was woken by the completion notify (at least one pump round between)"); + reset_all(); +} + +/* ================= UDP-7 挂起 recv:到点返回 -2 且不占 errno ================= */ + +static void tc_udp7_pending_timeout(void) +{ + char buf[8]; + int fd; + int rc; + int puts_before; + int reads_before; + + bring_up(); + fd = udp_bind(123); + ch395f_fake_push_recv_len((uint8_t)fd, 0U); + g_ns_tick = 1000U; + + /* 预算 10ms:不注入任何数据,泵每轮把假时钟推到 deadline 之后 */ + s_udp_pump_arrive_round = -1; + s_udp_pump_tick_inc = 10U; + s_udp_pump_calls = 0; + g_ns_pump = udp_pump; + rc = net_recvfrom_to(fd, buf, (int)sizeof(buf), 10U, NULL, NULL); + udp_pump_stop(); + + CHECK(rc == -2, "UDP-7a the budget expired -> -2 (a timeout is a result, not an error - same contract as net_recv_to)"); + CHECK(g_ns_error_count == 0, + "UDP-7b a timeout logs nothing and does not steal the caller's errno slot"); + CHECK(s_net_socks[fd].state == NET_SOCK_STATE_UDP_OPEN, "UDP-7c the socket stays usable after a timeout"); + CHECK(net_udp_pending(fd) == 0 && net_udp_armed(fd) == 0, + "UDP-7d the timeout hands both the pending flag and the out-param slot back (a leaked slot pins the fd as BUSY forever)"); + + /* 超时后立刻再来一次,数据到了必须能收到(槽位没泄漏的正面证据) */ + udp_script_server_datagram("AFTER", 5, 7U, "10.0.0.1"); + s_net_socks[fd].readable = 1U; + rc = net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL); + CHECK(rc == 5, "UDP-7e the same fd receives normally right after a timeout"); + + /* 预算 0 被这个入口拒绝:0 在本层一律是"无限等待",而本接口存在的理由就是 + * 有界(与 net_recv_to 的分片长度检查同口径)——静默改成无限等会把 IWDG 咬死 */ + puts_before = g_ns_put_calls; + reads_before = g_chip.read_calls; + rc = net_recvfrom_to(fd, buf, (int)sizeof(buf), 0U, NULL, NULL); + CHECK(rc == -1 && net_get_errno() == NET_ERR_INVAL, + "UDP-7f recvfrom_to with timeout_ms = 0 is refused as INVAL (0 means forever, which is what this entry must never do)"); + CHECK(g_ns_put_calls == puts_before && g_chip.read_calls == reads_before, + "UDP-7g the refused budget never reaches the queue or the chip"); + reset_all(); +} + +/* ================= UDP-8 挂起 recv 被取消:close / 重建 ================= */ + +static void tc_udp8_pending_cancel(void) +{ + char buf[16]; + net_msg_t msg; + int fd; + + /* 关闭结算:无数据 -> CONNRESET 必通知 */ + bring_up(); + fd = udp_bind(123); + ch395f_fake_push_recv_len((uint8_t)fd, 0U); + udp_arm_and_enqueue(fd, buf, (int)sizeof(buf), 0U, &msg); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_net; + net_process_messages(); + CHECK(net_udp_pending(fd) == 1, "UDP-8a the receive is registered (netTask occupancy is zero while waiting)"); + (void)net_close_locked(&s_net_socks[fd]); + CHECK(net_udp_pending(fd) == 0, "UDP-8b close settles the pending slot (nothing may outlive the fd)"); + CHECK(g_ns_notify_last_to == (TaskHandle_t)&g_ns_task_caller && + g_ns_notify_last_val == (uint32_t)(int)NET_ERR_CONNRESET, + "UDP-8c the waiter is woken with CONNRESET, not left sleeping forever"); + reset_all(); + + /* 关闭结算:有数据 -> 先把这一包送达(与 net_pending_settle_close 同一契约) */ + bring_up_netctx(); + fd = udp_bind(123); + udp_script_server_datagram("LASTPKT", 7, 9U, "10.0.0.2"); + ch395f_fake_push_recv_len((uint8_t)fd, 15U); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + if (net_udp_outparam_arm(fd, NULL, NULL) != 0) { + CHECK(0, "UDP-8d arm the slot for the settle test"); + } + s_net_udp[fd].pending = 1U; + s_net_udp[fd].buf = buf; + s_net_udp[fd].len = (int)sizeof(buf); + s_net_udp[fd].caller = (TaskHandle_t)&g_ns_task_caller; + g_ns_cur_task = (TaskHandle_t)&g_ns_task_net; + net_udp_settle_close(fd, &s_net_socks[fd]); + CHECK(g_ns_notify_last_val == 7U, + "UDP-8e closing with a datagram already in the chip buffer delivers it (7 bytes) instead of cancelling"); + CHECK(memcmp(buf, "LASTPKT", 7) == 0, "UDP-8f the delivered payload is intact on the close path"); + reset_all(); + + /* 网络重建:cancel_all 逐个通知 */ + bring_up_netctx(); + fd = udp_bind(123); + ch395f_fake_push_recv_len((uint8_t)fd, 0U); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + (void)net_udp_outparam_arm(fd, NULL, NULL); + s_net_udp[fd].pending = 1U; + s_net_udp[fd].buf = buf; + s_net_udp[fd].len = (int)sizeof(buf); + s_net_udp[fd].caller = (TaskHandle_t)&g_ns_task_caller; + g_ns_cur_task = (TaskHandle_t)&g_ns_task_net; + net_udp_cancel_all(NET_ERR_CONNRESET); + CHECK(net_udp_pending(fd) == 0 && g_ns_notify_last_to == (TaskHandle_t)&g_ns_task_caller, + "UDP-8g cancel_all wakes every waiter (the rebuild path cannot leave a task hanging)"); + reset_all(); +} + +/* ================= UDP-9 出参互斥与泄漏(AGENTS 禁止的回传字段,本改造的替代面) ================= */ + +static void tc_udp9_outparam_slot(void) +{ + struct net_sockaddr_in in; + struct net_sockaddr *sa = (struct net_sockaddr *)∈ + char buf[16]; + int addrlen; + int fd; + int rc; + int puts_before; + int reads_before; + + bring_up(); + fd = udp_bind(123); + ch395f_fake_push_recv_len((uint8_t)fd, 0U); + g_ns_int_pin_set = 0; + /* 基线:bind 自己投过一条消息,"这次拒绝有没有投递"只能跟调用前的计数比 */ + puts_before = g_ns_put_calls; + reads_before = g_chip.read_calls; + + /* 槽位被别的调用方占着:第二个并发收包必须被拒(数据报语义下无法判归属) */ + (void)memset(&in, 0, sizeof(in)); + in.sin_family = NET_AF_INET; + addrlen = (int)sizeof(in); + if (net_udp_outparam_arm(fd, sa, &addrlen) != 0) { + CHECK(0, "UDP-9a arming a free slot succeeds"); + } + rc = net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, sa, &addrlen); + CHECK(rc == -1 && net_get_errno() == NET_ERR_BUSY, + "UDP-9b a second receiver on the same fd gets BUSY (two datagrams cannot be attributed to two callers)"); + CHECK(g_ns_put_calls == puts_before && g_chip.read_calls == reads_before, + "UDP-9c the refusal is clean: nothing enqueued, nothing read from the chip"); + net_udp_outparam_disarm(fd); + + /* 入队前失败(队列满)不许把槽位钉住 */ + ns_rtos_q_fill(8); + rc = net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, sa, &addrlen); + CHECK(rc == -1 && net_get_errno() == NET_ERR_BUSY, "UDP-9d a full queue -> BUSY"); + CHECK(s_net_udp[fd].armed == 0U, + "UDP-9e the slot is given back on the enqueue-failure path (a leaked slot would pin this fd as BUSY forever)"); + ns_rtos_q_flush(); + reset_all(); +} + +/* ================= UDP-10 截断排空(旧实现留半包,下一包从此错位) ================= */ + +static void tc_udp10_truncation_drains(void) +{ + char buf[8]; + int fd; + int rc; + + bring_up_netctx(); + fd = udp_bind(123); + udp_script_server_datagram("0123456789ABCDE", 17, 9U, "10.0.0.3"); + ch395f_fake_push_recv_len((uint8_t)fd, 25U); + rc = net_recvfrom(fd, buf, 4, NET_MSG_DONTWAIT, NULL, NULL); + CHECK(rc == 4, "UDP-10a a short buffer yields exactly its capacity (BSD truncation)"); + CHECK(memcmp(buf, "0123", 4) == 0, "UDP-10b the first four payload bytes, header excluded"); + CHECK((int)g_fx.recv_pos == 25, + "UDP-10c the WHOLE datagram was drained (25 chip bytes consumed): a leftover tail would desync every later packet"); + CHECK(g_ns_error_count >= 1, + "UDP-10d the truncated drop is logged (silent byte loss is the hardest thing to debug)"); + CHECK(s_net_socks[fd].readable == 0U, "UDP-10e readable cleared after the full read"); + reset_all(); +} + +/* ================= UDP-11 执行任务自等自通知被拒(同 net_pending 守卫) ================= */ + +static void tc_udp11_self_wait_refused(void) +{ + char buf[16]; + int fd; + + bring_up_netctx(); + fd = udp_bind(123); + ch395f_fake_push_recv_len((uint8_t)fd, 0U); + g_ns_int_pin_set = 0; + CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, NULL, NULL) == -1 && + net_get_errno() == NET_ERR_BUSY, + "UDP-11a a blocking UDP receive inside netTask is refused (the tick that would complete it runs in that same task: self-wait = deadlock)"); + CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == -1 && + net_get_errno() == NET_ERR_WOULDBLOCK, + "UDP-11b the same call with NET_MSG_DONTWAIT is the legal netTask form -> WOULDBLOCK, no wait"); + reset_all(); +} + +/* ================= UDP-12 net_init 重建:挂起全部结算 ================= */ + +static void tc_udp12_rebuild_cancels(void) +{ + char buf[16]; + int fd; + + bring_up(); + fd = udp_bind(123); + ch395f_fake_push_recv_len((uint8_t)fd, 0U); + g_ns_cur_task = (TaskHandle_t)&g_ns_task_caller; + (void)net_udp_outparam_arm(fd, NULL, NULL); + s_net_udp[fd].pending = 1U; + s_net_udp[fd].buf = buf; + s_net_udp[fd].len = (int)sizeof(buf); + s_net_udp[fd].caller = (TaskHandle_t)&g_ns_task_caller; + g_ns_cur_task = (TaskHandle_t)&g_ns_task_net; + + (void)net_init("192.168.31.101", "255.255.255.0", "192.168.31.1", NULL); + CHECK(net_udp_pending(fd) == 0, + "UDP-12a a stack rebuild cancels every pending UDP receive (the same rule the TCP pending table follows)"); + CHECK(g_ns_notify_last_val == (uint32_t)(int)NET_ERR_CONNRESET, + "UDP-12b the waiter is woken with CONNRESET on rebuild"); + reset_all(); +} + +/* ================= UDP-13 公开流式接口不接受 DGRAM(剥头只有一条路) ================= */ + +static void tc_udp13_stream_api_rejects_dgram(void) +{ + char buf[16]; + int fd; + + bring_up_netctx(); + fd = udp_bind(123); + udp_script_server_datagram("XYZW", 4, 9U, "10.0.0.4"); + ch395f_fake_push_recv_len((uint8_t)fd, 12U); + CHECK(net_recv(fd, buf, (int)sizeof(buf), 0) == -1 && net_get_errno() == NET_ERR_INVAL, + "UDP-13a net_recv on a DGRAM socket is refused (it reads raw and would hand the chip header over as data)"); + CHECK(net_recv_nb(fd, buf, (int)sizeof(buf)) == -1 && net_get_errno() == NET_ERR_INVAL, + "UDP-13b net_recv_nb refused the same way (one UDP read path only: net_recvfrom)"); + CHECK(net_recv_to(fd, buf, (int)sizeof(buf), 100U) == -1 && net_get_errno() == NET_ERR_INVAL, + "UDP-13c net_recv_to refused the same way"); + CHECK(g_chip.read_calls == 0, "UDP-13d refusal touched the chip zero times"); + reset_all(); +} + +/* ================= UDP-14 队列化改造的存在理由:调用方上下文零 SPI ================= */ + +static void tc_udp14_caller_context_zero_spi(void) +{ + struct net_sockaddr_in in; + struct net_sockaddr *sa = (struct net_sockaddr *)∈ + char buf[32]; + int addrlen; + int fd; + int reads_before; + int writes_before; + + bring_up(); + fd = udp_bind(123); + + /* 调用方上下文:日志里出现任何一个芯片事务短名都算违反约定 */ + reads_before = g_chip.read_calls; + writes_before = g_chip.write_calls; + ch395f_fake_clear_log(); + udp_script_server_datagram("HELLO", 5, 53U, "192.168.31.200"); + ch395f_fake_push_recv_len((uint8_t)fd, 13U); + s_net_socks[fd].readable = 1U; + addrlen = (int)sizeof(in); + (void)memset(&in, 0, sizeof(in)); + CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, sa, &addrlen) == 5, + "UDP-14a caller-context recvfrom succeeds"); + CHECK(g_chip.read_calls == reads_before + 2, + "UDP-14b exactly header + payload reads, all issued while netTask owned the pump (the caller blocked, it did not touch SPI)"); + CHECK(g_chip.write_calls == writes_before, "UDP-14c a receive writes nothing to the send buffer"); + CHECK(g_ns_block_waits >= 1, + "UDP-14d the caller waited on its own task notification (this is what replaces the old spin-poll in the caller task)"); + reset_all(); +} + +/* ================= UDP-15 执行体与槽位的入参守卫(netTask 直调) ================= */ + +/* 这些守卫是"队列化之后仍然存在的第二条入口":板级用例与 netTask 内部直调 exec。 + * 一旦入参检查松掉,轻则把越界 fd 当合法 fd 用,重则在 s_net_udp[] 上算出越界指针。 + * 断言顺带钉住"拒绝对芯片零操作"——守卫失败不许留下半个芯片事务。 */ +static void tc_udp15_exec_arg_guards(void) +{ + struct net_sockaddr_in in; + struct net_sockaddr *sa = (struct net_sockaddr *)∈ + char buf[8]; + int udp_fd; + int raw_fd; + int stream_fd; + int addrlen; + int reads_before; + int writes_before; + int desip_before; + + bring_up_netctx(); + udp_fd = udp_bind(123); + stream_fd = mk_sock(NET_SOCK_STREAM, NET_SOCK_STATE_ESTABLISHED); + raw_fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); /* 只 socket 不 bind */ + reads_before = g_chip.read_calls; + writes_before = g_chip.write_calls; + desip_before = g_fxchip.set_des_ip_calls; + /* 越界 fd:四个执行体 + 槽位三件套 */ + CHECK(net_udp_bind_exec(-1, 123U) == -1 && net_get_errno() == NET_ERR_BADF, + "UDP-15a bind_exec refuses a negative fd"); + CHECK(net_udp_bind_exec(NET_MAX_SOCKETS, 123U) == -1 && net_get_errno() == NET_ERR_BADF, + "UDP-15b bind_exec refuses fd == NET_MAX_SOCKETS (the array has no such entry)"); + CHECK(net_udp_set_peer_exec(99, NULL) == -1 && net_get_errno() == NET_ERR_BADF, + "UDP-15c set_peer_exec refuses an out-of-range fd"); + CHECK(net_udp_sendto_exec(-1, buf, 4, NULL, NULL) == -1 && net_get_errno() == NET_ERR_BADF, + "UDP-15d sendto_exec refuses a negative fd"); + CHECK(net_udp_recvfrom_exec(NET_MAX_SOCKETS, buf, (int)sizeof(buf), + NET_MSG_DONTWAIT, 0U, NULL) == -1 && + net_get_errno() == NET_ERR_BADF, + "UDP-15e recvfrom_exec refuses an out-of-range fd BEFORE it takes &s_net_udp[fd] (an out-of-range slot pointer is a wild write waiting to happen)"); + CHECK(net_udp_outparam_arm(-1, NULL, NULL) == -1 && net_get_errno() == NET_ERR_BADF, + "UDP-15f the out-param slot refuses an out-of-range fd"); + + /* 越界的 disarm/clear 只许静默返回,且不许碰合法槽位 */ + CHECK(net_udp_outparam_arm(udp_fd, NULL, NULL) == 0, "UDP-15g in-range arm succeeds"); + net_udp_outparam_disarm(-1); + net_udp_clear(999); + CHECK(net_udp_armed(udp_fd) == 1, + "UDP-15h an out-of-range disarm/clear leaves the in-range slot exactly as it was"); + net_udp_outparam_disarm(udp_fd); + CHECK(net_udp_armed(udp_fd) == 0, "UDP-15i the in-range disarm releases it"); + + /* 类型不符:UDP 执行体不碰 TCP fd(bind/set_peer/recvfrom 看 type,sendto 看状态) */ + CHECK(net_udp_bind_exec(stream_fd, 123U) == -1 && net_get_errno() == NET_ERR_INVAL, + "UDP-15j bind_exec on a STREAM fd is refused (a chip reopen would silently convert its protocol)"); + CHECK(net_udp_set_peer_exec(stream_fd, NULL) == -1 && net_get_errno() == NET_ERR_INVAL, + "UDP-15k set_peer_exec on a STREAM fd is refused"); + CHECK(net_udp_recvfrom_exec(stream_fd, buf, (int)sizeof(buf), + NET_MSG_DONTWAIT, 0U, NULL) == -1 && + net_get_errno() == NET_ERR_INVAL, + "UDP-15l recvfrom_exec on a STREAM fd is refused (stream reads belong to net_recv)"); + CHECK(net_udp_sendto_exec(stream_fd, buf, 4, NULL, NULL) == -1 && + net_get_errno() == NET_ERR_NOTCONN, + "UDP-15m sendto_exec on a STREAM fd fails on the state gate (it shares net_send_locked, so the UDP-open check is the guard - not a type check)"); + + /* 未 bind 的 UDP fd:芯片侧 Socket 还没开,三条执行体一律 NOTCONN */ + CHECK(net_udp_sendto_exec(raw_fd, buf, 4, NULL, NULL) == -1 && + net_get_errno() == NET_ERR_NOTCONN, + "UDP-15n sendto_exec on an unbound UDP fd is refused (open would reset DesIP/DesPort written before it)"); + CHECK(net_udp_recvfrom_exec(raw_fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, 0U, NULL) == -1 && + net_get_errno() == NET_ERR_NOTCONN, + "UDP-15n2 recvfrom_exec on an unbound UDP fd is refused"); + CHECK(net_udp_set_peer_exec(raw_fd, NULL) == -1 && net_get_errno() == NET_ERR_NOTCONN, + "UDP-15n3 set_peer_exec on an unbound UDP fd is refused"); + CHECK(net_udp_sendto_exec(raw_fd, buf, 0, NULL, NULL) == -1 && net_get_errno() == NET_ERR_INVAL, + "UDP-15p a zero-length payload is refused before any state check"); + CHECK(net_udp_sendto_exec(raw_fd, NULL, 4, NULL, NULL) == -1 && net_get_errno() == NET_ERR_INVAL, + "UDP-15q a NULL payload is refused"); + CHECK(net_udp_recvfrom_exec(raw_fd, NULL, (int)sizeof(buf), NET_MSG_DONTWAIT, 0U, NULL) == -1 && + net_get_errno() == NET_ERR_INVAL, + "UDP-15r recvfrom_exec refuses a NULL buffer"); + CHECK(net_udp_recvfrom_exec(raw_fd, buf, 0, NET_MSG_DONTWAIT, 0U, NULL) == -1 && + net_get_errno() == NET_ERR_INVAL, + "UDP-15s recvfrom_exec refuses a zero-capacity buffer"); + + /* 公开入口:给了地址就必须给够长度(静默忽略 = 悄悄发到上一个对端) */ + (void)memset(&in, 0, sizeof(in)); + in.sin_family = NET_AF_INET; + in.sin_port = net_htons(8888); + in.sin_addr.s_addr = net_inet_addr("192.168.31.9"); + CHECK(net_sendto(udp_fd, buf, 4, 0, sa, 4) == -1 && net_get_errno() == NET_ERR_INVAL, + "UDP-15t net_sendto with a destination but a short addrlen is refused"); + addrlen = 4; + CHECK(net_recvfrom(udp_fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, sa, &addrlen) == -1 && + net_get_errno() == NET_ERR_INVAL, + "UDP-15u net_recvfrom with a src_addr but *addrlen too small is refused (it would half-fill the caller's struct)"); + CHECK(net_recvfrom(udp_fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, sa, NULL) == -1 && + net_get_errno() == NET_ERR_INVAL, + "UDP-15v net_recvfrom with src_addr but NULL addrlen is refused"); + CHECK(g_chip.read_calls == reads_before && g_chip.write_calls == writes_before && + g_fxchip.set_des_ip_calls == desip_before, + "UDP-15w every one of these refusals left the chip untouched (a rejected call must not start a transaction)"); + + /* 最后才是正例:端口 0 = 请本层分配动态端口(取号游标只有一个,就在 netTask 里) */ + CHECK(net_udp_bind_exec(raw_fd, 0U) == 0, "UDP-15x bind on a fresh UDP fd succeeds (port 0 = dynamic)"); + CHECK(s_net_socks[raw_fd].local_port >= 49152U && s_net_socks[raw_fd].state == NET_SOCK_STATE_UDP_OPEN, + "UDP-15y the dynamic port came from the netTask-side cursor and the socket is open (the cursor used to be poked from whichever task called bind)"); + reset_all(); +} + +/* ================= UDP-16 包装层的失败出口:队列未绑定 / 队列满 ================= */ + +/* 队列化接口最怕的一种泄漏:出参槽登记成功、消息没投出去。槽位留在原地, + * 这条 fd 之后每一次收包都被自己的旧登记判成 BUSY——而且永远不会自己好。 */ +static void tc_udp16_wrapper_failure_exits(void) +{ + struct net_sockaddr_in in; + struct net_sockaddr *sa = (struct net_sockaddr *)∈ + char buf[16]; + int addrlen; + /* 队列句柄存档(清掉之后要还原,还原走的是同一条 adopt 路径) */ + osMessageQueueId_t q_saved; + int fd; + int rc; + + bring_up(); + fd = udp_bind(123); + q_saved = s_net_msg_queue; + (void)memset(&in, 0, sizeof(in)); + in.sin_family = NET_AF_INET; + in.sin_port = net_htons(456); + in.sin_addr.s_addr = 0U; + addrlen = (int)sizeof(in); + + /* 「执行任务已建档、队列还没绑上」这个窗口:net_init 早期,或 CubeMX 里 + * 根本没建 netMsgQueue 的构建。手工把队列指针清掉才能走到这条出口—— + * 未 net_init 时每个任务都被当成执行任务,走的是直调分支,到不了这里。 + * ⚠ 这条出口按模块惯例是**裸 NET_ERR、不记 errno**(它表达的是"这套构建没配好 + * 队列",不是某次调用的可分类失败),所以这里只钉 -1,不钉 net_get_errno()。 */ + s_net_msg_queue = NULL; + CHECK(net_bind(fd, sa, (int)sizeof(in)) == -1, + "UDP-16a bind with no queue bound fails (never pretends success without an open)"); + CHECK(net_sendto(fd, "ping", 4, 0, NULL, 0) == -1, + "UDP-16b sendto with no queue bound fails"); + CHECK(net_udp_set_peer(fd, NULL) == -1, + "UDP-16c set_peer with no queue bound fails"); + rc = net_recvfrom(fd, buf, (int)sizeof(buf), 0, sa, &addrlen); + CHECK(rc == -1, + "UDP-16d recvfrom with no queue bound fails"); + CHECK(net_udp_armed(fd) == 0, + "UDP-16e the failed send-back hands the out-param slot straight back"); + rc = net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, sa, &addrlen); + CHECK(rc == -1, + "UDP-16f the NEXT call on the same fd fails the same way, not as BUSY - proof the slot did not leak"); + + /* 队列满:投不进去 → BUSY,且槽位同样当场归还 */ + s_net_msg_queue = q_saved; + ns_rtos_q_fill((int)NS_Q_CAP); + CHECK(net_sendto(fd, "ping", 4, 0, NULL, 0) == -1 && net_get_errno() == NET_ERR_BUSY, + "UDP-16g sendto on a full queue returns BUSY (osWaitForever on a queue nobody is draining would freeze the caller task)"); + CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), 0, sa, &addrlen) == -1 && + net_get_errno() == NET_ERR_BUSY, + "UDP-16h recvfrom on a full queue returns BUSY"); + CHECK(net_udp_armed(fd) == 0, + "UDP-16i the full-queue exit hands the slot back too (this is the exit that used to poison the fd forever)"); + CHECK(net_udp_set_peer(fd, NULL) == -1 && net_get_errno() == NET_ERR_BUSY, + "UDP-16j set_peer on a full queue returns BUSY"); + ns_rtos_q_flush(); + CHECK(net_recvfrom(fd, buf, (int)sizeof(buf), NET_MSG_DONTWAIT, NULL, NULL) == -1 && + net_get_errno() == NET_ERR_WOULDBLOCK, + "UDP-16k once the queue drains, the same fd works normally again (the refusals above were stateless)"); + reset_all(); +} + +/* ================= UDP-17 公开 set_peer/get_mode 与"回服务器模式" ================= */ + +static void tc_udp17_set_peer_and_mode(void) +{ + struct net_sockaddr_in in; + struct net_sockaddr *sa = (struct net_sockaddr *)∈ + uint8_t stream[64]; + char buf[64]; + int addrlen; + int fd; + int raw_fd; + int stream_fd; + int rc; + int i; + + bring_up(); + fd = udp_bind(123); + raw_fd = net_socket(NET_AF_INET, NET_SOCK_DGRAM); + stream_fd = mk_sock(NET_SOCK_STREAM, NET_SOCK_STATE_ESTABLISHED); + + CHECK(net_udp_get_mode(fd) == NET_UDP_MODE_SERVER, + "UDP-17a a freshly bound UDP socket is in chip SERVER mode (that is what bind programs: DesIP=0xFFFFFFFF)"); + CHECK(net_udp_get_mode(-1) == -1 && net_get_errno() == NET_ERR_BADF, + "UDP-17b get_mode refuses an out-of-range fd"); + CHECK(net_udp_get_mode(stream_fd) == -1 && net_get_errno() == NET_ERR_INVAL, + "UDP-17c get_mode on a STREAM fd is INVAL (there is no such thing as its chip mode)"); + CHECK(net_udp_set_peer(raw_fd, NULL) == -1 && net_get_errno() == NET_ERR_NOTCONN, + "UDP-17d set_peer before bind is refused (open would reset what we just wrote)"); + + /* 切客户端模式(经队列,调用方上下文) */ + (void)memset(&in, 0, sizeof(in)); + in.sin_family = NET_AF_INET; + in.sin_port = net_htons(123); + in.sin_addr.s_addr = net_inet_addr("119.28.220.214"); + CHECK(net_udp_set_peer(fd, &in) == 0, "UDP-17e set_peer succeeds through the queue"); + CHECK(net_udp_get_mode(fd) == NET_UDP_MODE_CLIENT, "UDP-17f the socket is now CLIENT mode"); + CHECK(g_fx.des_ip[fd][0] == 119U && g_fx.des_ip[fd][3] == 214U && + g_fx.des_port[fd] == 123U, + "UDP-17g the chip's destination registers carry the peer (line order, as the chip wants it)"); + CHECK(s_net_socks[fd].remote_ip == net_inet_addr("119.28.220.214") && + s_net_socks[fd].remote_port == 123U, + "UDP-17h peer bookkeeping is written on the switch, not only on send (client-mode receive has no header to read)"); + CHECK(g_chip.open_calls == 1, + "UDP-17i switching to client mode does NOT reopen the socket (a reopen would drop the reply already buffered - the whole reason SNTP can use this)"); + + /* 回服务器模式:不重开,只把 DesIP/DesPort 复原并清对端账 */ + CHECK(net_udp_set_peer(fd, NULL) == 0, "UDP-17j set_peer(NULL) returns to server mode"); + CHECK(net_udp_get_mode(fd) == NET_UDP_MODE_SERVER, "UDP-17k mode is SERVER again"); + CHECK(g_fx.des_ip[fd][0] == 0xFFU && g_fx.des_ip[fd][1] == 0xFFU && + g_fx.des_ip[fd][2] == 0xFFU && g_fx.des_ip[fd][3] == 0xFFU && + g_fx.des_port[fd] == 0U, + "UDP-17l the chip registers are back to the bind-time server values"); + CHECK(s_net_socks[fd].remote_ip == 0U && s_net_socks[fd].remote_port == 0U && + s_net_socks[fd].remote_ip_arr[0] == 0U, + "UDP-17m the peer bookkeeping is cleared with the mode (a stale peer would be used as the source address by a later client-mode read)"); + CHECK(g_chip.open_calls == 1, "UDP-17n returning to server mode does not reopen either"); + + /* 端到端:回到服务器模式后,收包又必须剥掉 8 字节信息头 */ + for (i = 0; i < 64; i++) { + stream[i] = (uint8_t)(0xC0 + i); /* 前 8 字节故意像信息头 */ + } + ch395f_fake_set_recv_stream(stream, 20U); + ch395f_fake_push_recv_len((uint8_t)fd, 20U); + s_net_socks[fd].readable = 1U; + addrlen = (int)sizeof(in); + (void)memset(&in, 0, sizeof(in)); + rc = net_recvfrom(fd, buf, (int)sizeof(buf), 0, sa, &addrlen); + CHECK(rc == 12, + "UDP-17o after the mode was restored the header is stripped again (20 chip bytes in, 12 payload bytes out)"); + CHECK((uint8_t)buf[0] == (uint8_t)0xC8U && (uint8_t)buf[11] == (uint8_t)0xC0U + 19U, + "UDP-17p delivery starts at the 9th chip byte: the header really is gone, not copied then skipped in the buffer"); + reset_all(); +} + +/* ================= UDP-18 挂起 sendto:分派处不许发第二次回执 ================= */ + +/* UDP 发送与 TCP 共用 net_send_locked:发送缓冲没空出来时登记挂起 send,由 + * net_send_pending_tick 续传。这条出口的正确性只有一句话:**回执只能有一份**, + * 而且必须是续传完成那一份。登记时就回执,调用方会拿到 0x70000000 当字节数。 */ +static void tc_udp18_pending_send_completes(void) +{ + static char payload[1400]; + int fd; + int rc; + int writes_before; + + bring_up(); + fd = udp_bind(123); + writes_before = g_chip.write_calls; + + /* 没有 SENBUF_FREE:发送缓冲始终没空,一块都写不进去 */ + g_ns_int_pin_set = 0; + s_net_socks[fd].send_ready = 0U; + /* 假对端:续传由它下一轮的 tick 结算(真实实现在 net_pending 自己的单测里测) */ + ns_peers_arm_send_pending_done(fd, 1400); + + rc = net_sendto(fd, payload, (int)sizeof(payload), 0, NULL, 0); + CHECK(rc == 1400, + "UDP-18a the caller gets the byte count of the COMPLETED send (if the dispatch notified on registration, this would be 0x70000000 or an error)"); + CHECK(g_peers.send_reg_calls == 1 && g_peers.last_snd_len == 1400 && + g_peers.last_snd_sent == 0, + "UDP-18b nothing was sent when the pending send was registered (0 of 1400) - the spin budget expired without a free send buffer"); + CHECK(g_chip.write_calls == writes_before, + "UDP-18c a stalled UDP send wrote nothing to the chip (writing into a full send buffer is silently dropped - TRAP-NET-04)"); + CHECK(g_peers.state_down_calls >= 1, + "UDP-18d the shared state predicate was consulted (UDP and TCP share one chunking implementation, so TRAP-NET-16 cannot come back as a second path)"); + reset_all(); +} + +/* ================= UDP-19 tick 的兜底:会话已死、陈旧电平、零长度 ================= */ + +static void tc_udp19_tick_fallbacks(void) +{ + char buf[16]; + int fd; + int rc; + int reads_before; + int notifies_before; + + bring_up_netctx(); + fd = udp_bind(123); + + /* 挂起收包登记中,控制块被拆(close/复位/被别的任务抢走) */ + CHECK(net_udp_outparam_arm(fd, NULL, NULL) == 0, "UDP-19a slot armed for the fabricated wait"); + s_net_udp[fd].pending = 1U; + s_net_udp[fd].buf = buf; + s_net_udp[fd].len = (int)sizeof(buf); + s_net_udp[fd].caller = (TaskHandle_t)&g_ns_task_caller; + s_net_socks[fd].state = NET_SOCK_STATE_CLOSED; + notifies_before = g_ns_notify_calls; + + net_udp_tick(s_net_socks); + CHECK(net_udp_pending(fd) == 0, + "UDP-19a2 a pending UDP receive whose socket died is settled in that same tick (nobody else will ever cancel it: reconcile ignores UDP)"); + CHECK(g_ns_notify_calls == notifies_before + 1 && + g_ns_notify_last_to == (TaskHandle_t)&g_ns_task_caller && + g_ns_notify_last_val == (uint32_t)(int)NET_ERR_CONNRESET, + "UDP-19b the settlement notifies the waiter with the error code (a lost notification = that task sleeps on a port that no longer exists)"); + net_udp_tick(s_net_socks); + CHECK(g_ns_notify_calls == notifies_before + 1, + "UDP-19c the second tick settles nothing again (no double notification)"); + + /* 陈旧 readable:标志说有、芯片说没有 → 清标志,请求继续等 */ + CHECK(net_udp_outparam_arm(fd, NULL, NULL) == 0, "UDP-19d re-armed for the stale-level case"); + s_net_udp[fd].pending = 1U; + s_net_udp[fd].buf = buf; + s_net_udp[fd].len = (int)sizeof(buf); + s_net_udp[fd].caller = (TaskHandle_t)&g_ns_task_caller; + s_net_socks[fd].state = NET_SOCK_STATE_UDP_OPEN; + s_net_socks[fd].readable = 1U; + ch395f_fake_push_recv_len((uint8_t)fd, 0U); + reads_before = g_chip.read_calls; + + net_udp_tick(s_net_socks); + CHECK(s_net_socks[fd].readable == 0U, + "UDP-19e a stale readable flag is cleared (leaving it set = one wasted SPI query every millisecond, forever)"); + CHECK(net_udp_pending(fd) == 1, "UDP-19f a stale flag is not data - the request keeps waiting"); + CHECK(g_chip.read_calls == reads_before, "UDP-19g the empty chip was not read past the length query"); + net_udp_clear(fd); + + /* 零长度不许进剥头逻辑:剥几字节、拷几字节、排空几字节必须同源 */ + rc = net_udp_datagram_read(fd, &s_net_socks[fd], buf, (int)sizeof(buf), NULL, NULL, 0); + CHECK(rc == 0, "UDP-19h datagram_read with avail == 0 delivers nothing (it must not invent a header out of a zero-length report)"); + rc = net_udp_datagram_read(fd, &s_net_socks[fd], buf, (int)sizeof(buf), NULL, NULL, -1); + CHECK(rc == 0, "UDP-19i a negative length report is treated the same way"); + + /* 越界的 settle_close 静默返回(幂等兜底,不崩、不误结算别的 fd) */ + CHECK(net_udp_outparam_arm(fd, NULL, NULL) == 0, "UDP-19j armed for the range-guard check"); + s_net_udp[fd].pending = 1U; + s_net_udp[fd].caller = (TaskHandle_t)&g_ns_task_caller; + notifies_before = g_ns_notify_calls; + net_udp_settle_close(-1, &s_net_socks[fd]); + net_udp_settle_close(NET_MAX_SOCKETS, &s_net_socks[fd]); + CHECK(net_udp_pending(fd) == 1 && g_ns_notify_calls == notifies_before, + "UDP-19k out-of-range settle_close settles nothing (it must not walk into a neighbouring fd's waiter)"); + reset_all(); +} + +/* ================= UDP-20 重绑必须给等待者一个了断 ================= */ + +static void tc_udp20_rebind_settles_waiter(void) +{ + struct net_sockaddr_in in; + const struct net_sockaddr *sa = (const struct net_sockaddr *)∈ + char buf[16]; + int fd; + int notifies_before; + + bring_up_netctx(); + fd = udp_bind(123); + + /* 有人在等包(手工登记,理由同 UDP-12:公开入口一进挂起就睡在通知里) */ + CHECK(net_udp_outparam_arm(fd, NULL, NULL) == 0, "UDP-20a slot armed"); + s_net_udp[fd].pending = 1U; + s_net_udp[fd].buf = buf; + s_net_udp[fd].len = (int)sizeof(buf); + s_net_udp[fd].caller = (TaskHandle_t)&g_ns_task_caller; + notifies_before = g_ns_notify_calls; + + /* 另一个任务重新 bind 同一 fd:芯片侧重开,缓冲里可能到的包一起作废 */ + (void)memset(&in, 0, sizeof(in)); + in.sin_family = NET_AF_INET; + in.sin_port = net_htons(456); + CHECK(net_bind(fd, sa, (int)sizeof(in)) == 0, "UDP-20b the rebind itself succeeds"); + CHECK(g_ns_notify_calls == notifies_before + 1 && + g_ns_notify_last_val == (uint32_t)(int)NET_ERR_CONNRESET, + "UDP-20c the rebind hands the waiter an explicit CONNRESET before reopening (clearing the slot without notifying = that task sleeps forever on a socket that was reopened under it)"); + CHECK(net_udp_pending(fd) == 0 && net_udp_armed(fd) == 0, + "UDP-20d no slot survives the rebind (neither pending nor armed)"); + CHECK(s_net_socks[fd].local_port == 456U && + s_net_socks[fd].udp_mode == NET_UDP_MODE_SERVER && + s_net_socks[fd].readable == 0U, + "UDP-20e the reopened socket is a clean server-mode socket: new port, SERVER mode, no inherited level"); + CHECK(g_chip.open_calls == 2, "UDP-20f a rebind is the HEAVY way back to server mode (it reopens; net_udp_set_peer(fd, NULL) is the light one)"); + 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 *)∈ + 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) +{ + tc_udp1_bind_sequence(); + tc_udp2_requires_bind(); + tc_udp3_server_strip(); + tc_udp4_client_no_strip(); + tc_udp5_sendto_dest_switches_mode(); + tc_udp6_pending_recv_arrival(); + tc_udp7_pending_timeout(); + tc_udp8_pending_cancel(); + tc_udp9_outparam_slot(); + tc_udp10_truncation_drains(); + tc_udp11_self_wait_refused(); + tc_udp12_rebuild_cancels(); + tc_udp13_stream_api_rejects_dgram(); + tc_udp14_caller_context_zero_spi(); + tc_udp15_exec_arg_guards(); + tc_udp16_wrapper_failure_exits(); + tc_udp17_set_peer_and_mode(); + 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(); +} -- 2.55.0.windows.2 From 8a81fa9e4164e4897b6895459ed15387812489f3 Mon Sep 17 00:00:00 2001 From: super_agent_bot Date: Tue, 3 Nov 2026 20:18:24 +0800 Subject: [PATCH 2/4] =?UTF-8?q?test(net/udp):=20=E9=98=B6=E6=AE=B5=2016=20?= =?UTF-8?q?UDP=20=E6=95=B0=E6=8D=AE=E9=9D=A2=E7=9C=9F=E6=9C=BA=E5=9B=9E?= =?UTF-8?q?=E5=BD=92=E5=8F=B0=E6=9E=B6=EF=BC=88=E6=9D=BF=E7=AB=AF=20+=20PC?= =?UTF-8?q?=20peer=EF=BC=89=EF=BC=8C=E5=8F=B0=E8=B4=A6=E8=A1=A5=E5=AE=9E?= =?UTF-8?q?=E6=B5=8B=E6=95=B0=E5=AD=97=EF=BC=88#73=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 板端 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 轮 「台架必验」指向本阶段的可执行形态,并写明尚未在真机上跑过。 --- docs/测试说明/单元测试/主机单测覆盖率口径.md | 1 + docs/测试说明/板级测试/CH395F_Test_Guide.md | 59 ++++ test/board/ch395f_test.h | 20 +- test/board/ch395f_test_task.c | 4 + test/board/net_test_task.c | 347 +++++++++++++++++++ test/board/net_test_task.h | 6 + test/host/udp_queue_bench.py | 233 +++++++++++++ 7 files changed, 669 insertions(+), 1 deletion(-) create mode 100644 test/host/udp_queue_bench.py diff --git a/docs/测试说明/单元测试/主机单测覆盖率口径.md b/docs/测试说明/单元测试/主机单测覆盖率口径.md index 609ea820..dd2fb2a7 100644 --- a/docs/测试说明/单元测试/主机单测覆盖率口径.md +++ b/docs/测试说明/单元测试/主机单测覆盖率口径.md @@ -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 只是归零**。 - **本轮覆盖率**(收尾那次全仓跑数,`--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 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 段 + 新条)。 ### P3|**明确不做**(把理由写死,免得下次又被列回待办:29 个 `.c` = 4,814 行) diff --git a/docs/测试说明/板级测试/CH395F_Test_Guide.md b/docs/测试说明/板级测试/CH395F_Test_Guide.md index dbac5280..66b8cfc7 100644 --- a/docs/测试说明/板级测试/CH395F_Test_Guide.md +++ b/docs/测试说明/板级测试/CH395F_Test_Guide.md @@ -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 测试脚本参考 | 命令 | 模式 | 说明 | @@ -1134,6 +1192,7 @@ 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) | | `dhcp_server ...` | DHCP Server | 内置 DHCP(阶段 4,需管理员权限) | ### 通用参数 diff --git a/test/board/ch395f_test.h b/test/board/ch395f_test.h index c4fc233e..f8441d72 100644 --- a/test/board/ch395f_test.h +++ b/test/board/ch395f_test.h @@ -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 diff --git a/test/board/ch395f_test_task.c b/test/board/ch395f_test_task.c index 62d593d7..7e57a18f 100644 --- a/test/board/ch395f_test_task.c +++ b/test/board/ch395f_test_task.c @@ -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 diff --git a/test/board/net_test_task.c b/test/board/net_test_task.c index 031d2d35..88ef66c4 100644 --- a/test/board/net_test_task.c +++ b/test/board/net_test_task.c @@ -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 --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 */ diff --git a/test/board/net_test_task.h b/test/board/net_test_task.h index dc529f2a..c4d630c8 100644 --- a/test/board/net_test_task.h +++ b/test/board/net_test_task.h @@ -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 diff --git a/test/host/udp_queue_bench.py b/test/host/udp_queue_bench.py new file mode 100644 index 00000000..3713e470 --- /dev/null +++ b/test/host/udp_queue_bench.py @@ -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()) -- 2.55.0.windows.2 From b0a6394aa170f07b368f6e6811bb5c35d284f1f0 Mon Sep 17 00:00:00 2001 From: super_agent_bot Date: Tue, 3 Nov 2026 20:46:15 +0800 Subject: [PATCH 3/4] =?UTF-8?q?fix(test/udp):=20peer=20=E6=8D=A2=E6=BA=90?= =?UTF-8?q?=E7=AB=AF=E5=8F=A3=E4=B8=8A=E6=8A=A5=E6=96=B9=E5=BC=8F=E5=B9=B6?= =?UTF-8?q?=E8=A1=A5=20PC=20=E5=9B=9E=E8=B7=AF=E8=87=AA=E6=A3=80=EF=BC=88#?= =?UTF-8?q?73=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 的合并前提不变。 --- docs/测试说明/板级测试/CH395F_Test_Guide.md | 23 ++++-- test/host/udp_queue_bench.py | 91 ++++++++++++++++----- test/host/udp_queue_bench_loopback.py | 89 ++++++++++++++++++++ 3 files changed, 173 insertions(+), 30 deletions(-) create mode 100644 test/host/udp_queue_bench_loopback.py diff --git a/docs/测试说明/板级测试/CH395F_Test_Guide.md b/docs/测试说明/板级测试/CH395F_Test_Guide.md index 66b8cfc7..99218ff9 100644 --- a/docs/测试说明/板级测试/CH395F_Test_Guide.md +++ b/docs/测试说明/板级测试/CH395F_Test_Guide.md @@ -1155,24 +1155,30 @@ WAIT_CLOSE 阶段已内置二段补发(只补一次),生产 net 层则靠 **运行方法**: -1. `test/board/test_config.h` 开 `TEST_SUITE_CH395F`;`test/board/ch395f_test.h` **只**开 +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 面。 -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`,再给板子上电/复位; +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 重发,能盖住板子启动那段时间。 -4. 判据是**两侧都绿**:板端串口 `=== Phase 16: N/N PASSED (failed=0) ===`,PC 端 +5. 判据是**两侧都绿**:板端串口 `=== 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` 钩子也会拦住带测试宏的提交)。 +6. 跑完把这四行宏恢复成产品态重新构建 + 烧录(`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 根本没有多任务调用者。 + 这是既有行为、不是本轮引入(改造前的直调 `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,176 / @@ -1193,6 +1199,7 @@ WAIT_CLOSE 阶段已内置二段补发(只补一次),生产 net 层则靠 | `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,需管理员权限) | ### 通用参数 diff --git a/test/host/udp_queue_bench.py b/test/host/udp_queue_bench.py index 3713e470..bcc39fc4 100644 --- a/test/host/udp_queue_bench.py +++ b/test/host/udp_queue_bench.py @@ -72,6 +72,7 @@ class Peer: self.timeout = timeout self.total = 0 self.failed = 0 + self.ports = {} self.t_start = time.time() def stamp(self) -> str: @@ -80,6 +81,17 @@ class Peer: 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)) @@ -114,31 +126,51 @@ class Peer: .format(tag, port)) try: got, addr = self._recv_after(sock, min(attempt, left)) + self.note_port(sock, tag) 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])) + # 一个数据报进、一个数据报出——唯一例外是本步载荷超过芯片发送缓冲(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(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)) + 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(" 首 16 字节 = {0}".format(got[:16].hex())) + 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 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 + 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: """板端超时探针期间本端必须保持静默,且不能有过期包漂进来。""" @@ -180,6 +212,10 @@ def main() -> int: help="对应 TC-NET-1606 的静默时长(板端探针预算 1 秒)") ap.add_argument("--late", type=float, default=12.0, help="对应 TC-NET-1607 的迟发时长:板端在这一步无限等包") + 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 秒超时探针,否则两步会互相污染。 @@ -191,12 +227,14 @@ def main() -> int: 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「换源端口」就是这么来的。 + # 端口号不能在未 connect 时用 getsockname() 取(Windows 会 WinError 10022, + # 端口要到第一次 sendto 才分配),也不能 connect()——connect 会让内核丢掉 + # 非对端的包,那正是这一步要验的东西。改成首次收包后再查,照样打得出来。 + socks = {"A": socket.socket(socket.AF_INET, socket.SOCK_DGRAM), + "B": socket.socket(socket.AF_INET, socket.SOCK_DGRAM), + "C": socket.socket(socket.AF_INET, socket.SOCK_DGRAM)} + peer.log("对端 = {0},三条源套接句 A/B/C 的临时源端口在首次收包后打印".format(args.board)) # 板端每一步用新源端口的条件:1604(换源端口验形态恢复)及其后 step_sock = { @@ -207,9 +245,18 @@ def main() -> int: "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": diff --git a/test/host/udp_queue_bench_loopback.py b/test/host/udp_queue_bench_loopback.py new file mode 100644 index 00000000..a71ae2bc --- /dev/null +++ b/test/host/udp_queue_bench_loopback.py @@ -0,0 +1,89 @@ +"""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 main() -> int: + if hasattr(sys.stdout, "reconfigure"): + sys.stdout.reconfigure(encoding="utf-8", errors="replace") + 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 秒省一点台架外时间 + sys.argv = ["udp_queue_bench.py", "--board", "127.0.0.1", + "--first-wait", "15", "--timeout", "6", + "--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()) -- 2.55.0.windows.2 From fc0d3e4b5c5346a7948b88e4ab368f65eb7d1197 Mon Sep 17 00:00:00 2001 From: super_agent_bot Date: Tue, 3 Nov 2026 21:28:36 +0800 Subject: [PATCH 4/4] =?UTF-8?q?test(net/udp):=20=E9=98=B6=E6=AE=B5=2016=20?= =?UTF-8?q?=E5=8F=B0=E6=9E=B6=E5=8A=A0=E5=9B=BA=20+=20=E7=9C=9F=E6=9C=BA?= =?UTF-8?q?=E9=A6=96=E8=B7=91=E8=AF=81=E6=8D=AE=E5=9B=9E=E5=A1=AB=EF=BC=88?= =?UTF-8?q?#73=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 第一次上真机把台架的两处软肋照出来了,都不是被测代码的问题: - 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 在产品里被预处理干净。 --- docs/测试说明/单元测试/主机单测覆盖率口径.md | 2 +- docs/测试说明/板级测试/CH395F_Test_Guide.md | 29 ++++++-- test/board/net_test_task.c | 72 +++++++++++++++----- test/host/udp_queue_bench.py | 72 +++++++++++++++----- test/host/udp_queue_bench_loopback.py | 56 ++++++++++++++- 5 files changed, 189 insertions(+), 42 deletions(-) diff --git a/docs/测试说明/单元测试/主机单测覆盖率口径.md b/docs/测试说明/单元测试/主机单测覆盖率口径.md index dd2fb2a7..150e3e41 100644 --- a/docs/测试说明/单元测试/主机单测覆盖率口径.md +++ b/docs/测试说明/单元测试/主机单测覆盖率口径.md @@ -725,7 +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 只是归零**。 - **本轮覆盖率**(收尾那次全仓跑数,`--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 19:19 起断电(SWD `No target connected`、ping 不通、串口静默),而 PR #74 的合并前提就是这条回归出证据;跑完请把结果回填本条并把上面「台架必验」改写成实测结论。 +- **这 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 行) diff --git a/docs/测试说明/板级测试/CH395F_Test_Guide.md b/docs/测试说明/板级测试/CH395F_Test_Guide.md index 99218ff9..643fbd80 100644 --- a/docs/测试说明/板级测试/CH395F_Test_Guide.md +++ b/docs/测试说明/板级测试/CH395F_Test_Guide.md @@ -1181,11 +1181,30 @@ 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,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 的合并前提就是这条回归跑完拿到证据。 +- 本阶段构建实测(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` 落盘更稳。 --- diff --git a/test/board/net_test_task.c b/test/board/net_test_task.c index 88ef66c4..c2bfbb70 100644 --- a/test/board/net_test_task.c +++ b/test/board/net_test_task.c @@ -1023,6 +1023,7 @@ void net_layer_test_main(void) #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]; @@ -1095,27 +1096,62 @@ static int udp_bench_expect(int fd, uint8_t tag, int exp, int cap, uint32_t to_m { 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; - (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; + /* + * 函数说明(步序自愈):步号写在载荷第 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), diff --git a/test/host/udp_queue_bench.py b/test/host/udp_queue_bench.py index bcc39fc4..ad5b4b9f 100644 --- a/test/host/udp_queue_bench.py +++ b/test/host/udp_queue_bench.py @@ -66,10 +66,38 @@ def payload(tag: int, n: int) -> bytes: 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) -> None: + 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 = {} @@ -112,26 +140,29 @@ class Peer: # 先清掉上一步可能迟到的回显,避免把它当成本步的应答 self._drain(sock) - deadline = time.time() + (first_wait if first_wait > 0 else self.timeout) + # 每一步都重发,且单步预算必须盖过板端单步等待(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) - # 首步:板子可能还在启动/还没 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)) + 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: - 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)) + 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 的步骤按「预期拆包」判,并把实测拆分打 @@ -188,7 +219,7 @@ class Peer: try: sock.settimeout(budget) return sock.recvfrom(4096) - except socket.timeout: + except (socket.timeout, ConnectionResetError): return None finally: sock.settimeout(self.timeout) @@ -212,6 +243,8 @@ def main() -> int: 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, @@ -225,15 +258,20 @@ def main() -> int: 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) + peer = Peer(args.board, args.timeout, args.step_budget) # 三条独立源套接句 = 三个不同临时源端口,1604「换源端口」就是这么来的。 # 端口号不能在未 connect 时用 getsockname() 取(Windows 会 WinError 10022, # 端口要到第一次 sendto 才分配),也不能 connect()——connect 会让内核丢掉 # 非对端的包,那正是这一步要验的东西。改成首次收包后再查,照样打得出来。 - socks = {"A": socket.socket(socket.AF_INET, socket.SOCK_DGRAM), - "B": socket.socket(socket.AF_INET, socket.SOCK_DGRAM), - "C": socket.socket(socket.AF_INET, socket.SOCK_DGRAM)} + socks = {"A": new_peer_socket(), + "B": new_peer_socket(), + "C": new_peer_socket()} peer.log("对端 = {0},三条源套接句 A/B/C 的临时源端口在首次收包后打印".format(args.board)) # 板端每一步用新源端口的条件:1604(换源端口验形态恢复)及其后 diff --git a/test/host/udp_queue_bench_loopback.py b/test/host/udp_queue_bench_loopback.py index a71ae2bc..e60cf114 100644 --- a/test/host/udp_queue_bench_loopback.py +++ b/test/host/udp_queue_bench_loopback.py @@ -54,9 +54,61 @@ def echo_one(sock, deadline, stop): 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: @@ -71,8 +123,10 @@ def main() -> int: 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", + "--first-wait", "15", "--timeout", "6", "--step-budget", "8", "--probe-silence", "2.0", "--late", "9.0"] try: rc = peer.main() -- 2.55.0.windows.2