/* * 模块名称: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(); }