socket.c
源文件路径:
midware/ethernt/lan/socket.c所属模块:midware/ethernt/lan(W5500 以太网协议栈 — Socket API 实现层) 代码行数:719
1. 文件职责
本文件是 WIZnet W5500 官方 socket 库在本工程中的实际实现体(文件头标注 V1.0.3,2014/05/01),把 W5500 的 8 个硬件 socket 封装成一套类 Berkeley 接口,向上层网络应用屏蔽 Sn_* 寄存器操作、TX/RX 环形缓冲区搬运和 socket 状态机细节。
它处在数据流的最底层:所有 TCP/UDP 报文的收发最终都落到本文件的 send/recv/sendto/recvfrom 上。在 EMS 中它的调用者有四类:midware/ethernt/ethernet.c(云端 TCP 客户端 socket 2)、midware/interface/modbus/modbus.c(Modbus TCP 从站 socket 3)、midware/ethernt/lan/dhcp.c 与 dns.c(UDP)、net/ems.c 与 net/server.c(直接调用 send/recv 收发云端报文)。
与官方原版相比,本文件被 Alpha 改动过两处以适配 uCOS-II:recv() 里加入了基于 OSTimeGet() 的 2000 tick 阻塞超时(socket.c:319、socket.c:354),sendto() 里加入了 OSTimeDlyHMSM(0,0,0,1) 让出 1 ms(socket.c:431)。因此本文件不能当作纯裸机驱动看待——它是与 RTOS 时基耦合的。文件本身不含业务逻辑,不含任务,靠上层的网络任务调用驱动。
2. 关键数据结构与宏
本文件没有自定义结构体/枚举,类型全部来自 socket.h 与 wizchip_conf.h;核心内容是一组文件级静态状态变量与四个形参校验宏。
| 名称 | 类型 | 说明 |
|---|---|---|
SOCK_ANY_PORT_NUM | 宏 | 0xC000,socket() 未指定端口时的自增起始端口(第 54 行,定义末尾多了一个分号,见第 7 节) |
SEND_TIME_OUT | 宏 | (500),值 500,在本文件中定义后从未被使用(第 62 行) |
CHECK_SOCKNUM() | 宏(do-while 语句块) | 校验 sn > _WIZCHIP_SOCK_NUM_ 则返回 SOCKERR_SOCKNUM(第 68-71 行) |
CHECK_SOCKMODE(mode) | 宏(do-while) | 校验 getSn_MR(sn) & 0x0F 是否等于 mode,不符返回 SOCKERR_SOCKMODE(第 73-76 行) |
CHECK_SOCKINIT() | 宏(do-while) | 校验 getSn_SR(sn) == SOCK_INIT,否则返回 SOCKERR_SOCKINIT(第 78-81 行) |
CHECK_SOCKDATA() | 宏(do-while) | 校验 len == 0 则返回 SOCKERR_DATALEN(第 83-86 行) |
sock_any_port | static uint16_t | 自动端口分配游标,初值 SOCK_ANY_PORT_NUM,回绕点 0xFFF0(第 56 行) |
sock_io_mode | static uint16_t | 每个 socket 一位的"非阻塞 IO"位图;bit n = socket n 处于非阻塞模式(第 57 行) |
sock_is_sending | 全局 uint16_t(非 static) | 每个 socket 一位的"发送中"位图,send() 用它做重入保护(第 58 行) |
sock_remained_size[_WIZCHIP_SOCK_NUM_] | static uint16_t[8] | 非 TCP 模式下"整包尚未取完的字节数"(第 59 行) |
sock_pack_info[_WIZCHIP_SOCK_NUM_] | static uint8_t[8] | 非 TCP 模式下的包分片标志,取 PACK_FIRST/PACK_REMAINED/PACK_COMPLETED(第 60 行) |
sock_next_rd[_WIZCHIP_SOCK_NUM_] | static uint16_t[8] | 仅 _WIZCHIP_ == 5200 时定义,用于规避 W5200 ARP errata。本工程 _WIZCHIP_ == 5500,该数组不参与编译(第 64-66 行) |
sockint_kind | 枚举(socket.h:333) | socket 中断位掩码:SIK_CONNECTED(1<<0)/SIK_DISCONNECTED/SIK_RECEIVED/SIK_TIMEOUT/SIK_SENT(1<<4)/SIK_ALL(0x1F) |
ctlsock_type | 枚举(socket.h:347) | ctlsocket() 操作码:CS_SET_IOMODE…CS_GET_INTMASK |
sockopt_type | 枚举(socket.h:364) | setsockopt()/getsockopt() 选项码:SO_FLAG…SO_PACKINFO |
PACK_FIRST / PACK_REMAINED / PACK_COMPLETED | 宏(socket.h:129-131) | 0x80 / 0x01 / 0x00 |
SF_IO_NONBLOCK | 宏(socket.h:124) | 0x01,本文件用 (flag & SF_IO_NONBLOCK) << sn 把标志位搬到对应 socket 位上,依赖其值为 1 |
SF_TCP_NODELAY / SF_IGMP_VER2 / SF_MULTI_ENABLE / SF_UNI_BLOCK | 宏(socket.h:112-121) | 分别映射 Sn_MR_ND(0x20)/Sn_MR_MC(0x20)/Sn_MR_MULTI(0x80)/Sn_MR_UCASTB(0x10),在 socket() 的参数校验中使用 |
SOCK_OK/SOCK_BUSY/SOCK_FATAL/SOCKERR_*/SOCKFATAL_PACKLEN | 宏(socket.h:88-107) | 返回码。SOCK_BUSY 与 SOCK_ERROR 同为 0,与 SOCK_OK 为 1 互补,调用者用 == SOCK_OK / < 0 判断 |
_WIZCHIP_ | 宏(wizchip_conf.h:59) | 本工程为 5500,决定大量 #if 分支的走向 |
_WIZCHIP_SOCK_NUM_ | 宏(wizchip_conf.h:138) | 本工程为 8,即数组长度与合法 socket 号上界 |
3. 函数清单
| 函数名 | 作用域 | 说明 |
|---|---|---|
socket | 全局 | 打开一个 socket:校验协议/标志、close(sn) 复位、写 Sn_MR/Sn_PORT、发 Sn_CR_OPEN,未给端口时从 sock_any_port 自增分配(第 90 行) |
close | 全局 | 发 Sn_CR_CLOSE,清全部中断 setSn_IR(sn,0xFF),清发送/剩余包状态,等到 SOCK_CLOSED 返回(第 160 行) |
listen | 全局 | 仅在 TCP 且 socket 处于 SOCK_INIT 时发 Sn_CR_LISTEN,轮询等到 SOCK_LISTEN(第 176 行) |
connect | 全局 | 仅在 TCP 且已初始化时设置目的 IP/端口并发 Sn_CR_CONNECT;非阻塞模式下立即返回 SOCK_BUSY(第 195 行) |
disconnect | 全局 | 仅在 TCP 下发 Sn_CR_DISCONN,阻塞等 SOCK_CLOSED,期间检查 Sn_IR_TIMEOUT(第 229 行) |
send | 全局 | TCP 发送:检查发送中标志与 SENDOK/TIMEOUT 中断、等 TX 空闲空间足够、wiz_send_data、发 Sn_CR_SEND(第 251 行) |
recv | 全局 | TCP 接收:检查 socket 状态与 RX_RSR,并带有本工程新增的 2000 tick 阻塞超时(第 315 行) |
sendto | 全局 | UDP/MACRAW 发送:设置目的 IP/端口、等 TX 空闲、发 Sn_CR_SEND,然后轮询 Sn_IR_SENDOK/Sn_IR_TIMEOUT(第 372 行) |
recvfrom | 全局 | UDP/MACRAW/IPRAW 接收:先读 8 字节(UDP)或 2 字节(MACRAW)包头取出对端地址、端口与包长,再搬数据,支持分包续传(第 454 行) |
ctlsocket | 全局 | socket 控制:读写 IO 模式、读写最大 TX/RX 缓冲、清/读中断、设/读中断掩码(第 574 行) |
setsockopt | 全局 | 设置 socket 选项:SO_TTL/SO_TOS/SO_MSS/SO_DESTIP/SO_DESTPORT/SO_KEEPALIVESEND/SO_KEEPALIVEAUTO(第 617 行) |
getsockopt | 全局 | 读取 socket 选项与运行时信息:SO_FLAG/SO_TTL/SO_TOS/SO_MSS/SO_DESTIP/SO_DESTPORT/SO_KEEPALIVEAUTO/SO_SENDBUF/SO_RECVBUF/SO_STATUS/SO_REMAINSIZE/SO_PACKINFO(第 670 行) |
共 12 个函数,全部为全局作用域,文件内没有 static 函数。注意 socket.h:467 声明了 send_u32(),但本文件没有实现(全工程也无调用点,见第 7 节)。
4. 核心逻辑解析
4.1 文件级状态:三个位图 + 两个数组 + 一个游标
本文件全部状态都是"按 socket 号索引"的:
sock_io_mode(bit 位图)由socket()按传入的SF_IO_NONBLOCK置位、由ctlsocket(CS_SET_IOMODE)改写;send/recv/sendto/recvfrom在"缓冲不够"时据此决定是"立即返回SOCK_BUSY"还是"原地自旋等待",这是阻塞/非阻塞行为的唯一开关。sock_is_sending(bit 位图)是 TCP 发送的重入保护:send()起始若发现该位已置,就去查Sn_IR——有SENDOK则清中断并清位(上一笔已完成),有TIMEOUT则直接close(sn)并返回SOCKERR_TIMEOUT,否则返回SOCK_BUSY。发送成功后置位。close()/disconnect()/socket()都会清位。sock_remained_size[]/sock_pack_info[]只在非 TCP 模式下有意义,用于 UDP/MACRAW 分包:一次recvfrom()取不完一整包时,剩余长度记入sock_remained_size[sn],并把PACK_REMAINED或到PACK_FIRST写进sock_pack_info[sn];下层的SO_REMAINSIZE/SO_PACKINFO就是读它们。sock_any_port是自动端口分配器:socket()收到port == 0时用当前值并自增,到0xFFF0回绕到0xC000(第 143-148 行)。
4.2 socket 生命周期与合法性校验
socket() 的顺序是严格且不可跳过的:
CHECK_SOCKNUM();- 协议白名单——
Sn_MR_TCP(0x01)、Sn_MR_UDP(0x02)、Sn_MR_MACRAW(0x04);Sn_MR_IPRAW/Sn_MR_PPPoE两个 case 被#if (_WIZCHIP_ < 5200)裁掉了(第 99-103 行),即 W5500 下不开放; - 标志合法性——
if((flag & 0x06) != 0) return SOCKERR_SOCKFLAG;先排除 bit1/bit2;TCP 下若既没有SF_TCP_NODELAY也没有SF_IO_NONBLOCK则报错;UDP 下若给了SF_IGMP_VER2却没有SF_MULTI_ENABLE则报错; close(sn)强制复位 →setSn_MR(sn, protocol | (flag & 0xF0))(高 4 位标志写进模式寄存器)→ 写端口 →Sn_CR_OPEN并while(getSn_CR(sn))等命令被硬件接收;- 刷新本文件的三个状态位,最后
while(getSn_SR(sn) == SOCK_CLOSED)等状态机离开 CLOSED,返回sn。
close() 与 disconnect() 的差别是:close() 立即发 Sn_CR_CLOSE、清 Sn_IR 全 0xFF、无条件等到 SOCK_CLOSED;disconnect() 只在 TCP 下发 Sn_CR_DISCONN(正常四次挥手),等待过程中若发现 Sn_IR_TIMEOUT 则退化为 close() 并返回 SOCKERR_TIMEOUT。
listen() 里有一个可疑的判据:等待 SOCK_LISTEN 的循环中写的是 if(getSn_CR(sn) == SOCK_CLOSED)(第 185 行)——把命令寄存器 Sn_CR 与 socket 状态 SOCK_CLOSED 比较,几乎恒为假。正常写法应是 getSn_SR(sn)。这一段容错分支实际上是死代码。
4.3 TCP 发送 send()
流程(第 251-313 行):
- 校验 socket 号 / 模式(必须是 TCP)/
len != 0; Sn_SR必须是SOCK_ESTABLISHED或SOCK_CLOSE_WAIT,否则SOCKERR_SOCKSTATUS;- 若
sock_is_sending对应位已置,按Sn_IR处理上一笔(见 4.1); len先被裁剪到getSn_TxMAX(sn);- 循环等
getSn_TX_FSR(sn) >= len:期间一旦发现 socket 掉出 ESTABLISHED/CLOSE_WAIT 就close(sn)返回错误;非阻塞模式且空间不足则返回SOCK_BUSY; wiz_send_data()把数据写进 W5500 的 TX 环形缓冲 → 发Sn_CR_SEND→ 等命令寄存器 → 置sock_is_sending位 → 返回实际发送长度len。
注意返回值语义:返回值就是请求发送的字节数,并不代表对端已收到(真正的完成由下一次调用时的 SENDOK 确认)。
4.4 TCP 接收 recv()(本工程改动点)
流程(第 315-370 行):
len裁剪到getSn_RxMAX(sn);- 循环读
getSn_RX_RSR(sn)与Sn_SR:状态为SOCK_CLOSE_WAIT时会先把残留数据读完再关;其它非 ESTABLISHED 状态一律close(sn)返回SOCKERR_SOCKSTATUS; - 非阻塞模式且
RX_RSR == 0→SOCK_BUSY; - 本工程新增:
s_u32_timeout_tick = OSTimeGet()记录起点,每次循环判断(OSTimeGet() - s_u32_timeout_tick) > 2000则 break(OS_TICKS_PER_SEC = 1000,即 2000 ms);并处理OSTimeGet()回绕(若当前 tick 小于起点则重置起点)。这段是 Alpha 为了解决"阻塞式recv()在 TCP 连接被静默断开时把网络任务卡死"而加的; - 取到数据后按
min(len, recvsize)调wiz_recv_data(),发Sn_CR_RECV释放 W5500 接收缓冲,返回实际长度。
因为第 4 步会以 len = 0 结束该函数,超时路径的返回值为 0,与 SOCK_BUSY(=0) 同值,调用方无法区分"超时"与"非阻塞无数据",这是本文件最容易踩的坑。
4.5 UDP/MACRAW 发送 sendto()
第 372-452 行。与 send() 的差别:
- 模式校验用
getSn_MR(sn) & 0x0F只接受Sn_MR_UDP/Sn_MR_MACRAW; - 目的 IP 与端口直接由参数覆盖(
setSn_DIPR/setSn_DPORT),无需事先connect; - 先
if(*((uint32_t*)addr) == 0) return SOCKERR_IPINVALID;拒绝 0.0.0.0,随后又把addr逐字节拼成taddr(第 389-392 行)——taddr拼完即弃,从未被使用,是 V1.0.3 修改时留下的残余; Sn_SR必须是SOCK_UDP或SOCK_MACRAW;- 发完
Sn_CR_SEND后OSTimeDlyHMSM(0,0,0,1)让出 1 ms,然后for(;;)轮询Sn_IR:SENDOK→ 清中断并 break;TIMEOUT→ 清中断并返回SOCKERR_TIMEOUT;其它任何情况也直接返回SOCKERR_TIMEOUT(第 446-449 行的else分支),也就是说这一轮询几乎没有"继续等"的能力,第一次读到Sn_IR == 0就会返回超时。
4.6 UDP/MACRAW 接收 recvfrom() 与分包
这是本文件最复杂的一段(第 454-571 行)。
启动条件:若 sock_remained_size[sn] == 0(表示这是新包的开头),先自旋等 Sn_RX_RSR != 0,非阻塞模式下为 0 则返回 SOCK_BUSY,socket 关闭则 SOCKERR_SOCKCLOSED。
随后把 sock_pack_info[sn] 置为 PACK_COMPLETED,按 mr & 0x07 分三种包头解析:
- UDP(
Sn_MR_UDP,第 491-513 行):读 8 字节包头 → 前 4 字节是对端 IP、第 5-6 字节位移出端口、第 7-8 字节位移出整包长度存入sock_remained_size[sn];然后pack_len = min(len, sock_remained_size[sn])搬数据,并把sock_pack_info[sn]改为PACK_FIRST。 - MACRAW(第 514-535 行):读 2 字节长度字段,若长度 > 1514 认为帧非法,
close(sn)并返回SOCKFATAL_PACKLEN(-1001);addr/port参数在此模式下不填(与头文件注释一致)。 - IPRAW(第 537-557 行):被
#if (_WIZCHIP_ < 5200)裁掉,W5500 下不编译。
收尾(第 563-570 行):再发一次 Sn_CR_RECV 释放缓冲,sock_remained_size[sn] -= pack_len;若还剩数据就把 PACK_REMAINED 或进 sock_pack_info[sn];返回 pack_len。下一次调用会走到 sock_remained_size[sn] != 0 的分支,跳过包头解析,继续搬剩余部分。
状态机图:
RX_RSR != 0
[空闲] ─────────────────────────► 读包头(UDP 8B / MACRAW 2B)
remained_size=0 │
▼
pack_info = PACK_FIRST
│
┌───────────────────┴────────────────────┐
│ len >= remained len < remained│
▼ ▼
一次取完整包 只取 len 字节
pack_info 保持 FIRST pack_info |= REMAINED
│ │
└──────────────┬──────────────────────────┘
▼
remained_size -= pack_len; 返回 pack_len
│
remained_size != 0? ──是──► 下次调用跳过包头直接续传
4.7 控制与选项 ctlsocket / setsockopt / getsockopt
ctlsocket()(第 574-615 行)是纯 switch 转发:除CS_GET_MAXTXBUF/CS_GET_MAXRXBUF外都做(*(u8*)arg) > SIK_ALL或取值合法性检查,非法时返回SOCKERR_ARG。setsockopt()(第 617-668 行)中SO_KEEPALIVESEND会发Sn_CR_SEND_KEEP并while(getSn_CR(sn) != 0)自旋,同时检查Sn_IR_TIMEOUT——这是唯一带超时出口的选项。工程中ethernet.c:699(TCP 客户端)与modbus.c:9657(Modbus TCP 从站)使用SO_KEEPALIVEAUTO设置保活定时。getsockopt()(第 670-719 行)除SO_PACKINFO要求 TCP 模式(明显是笔误,SO_PACKINFO只对非 TCP 有意义)与SO_KEEPALIVEAUTO要求 TCP 外,其余直通读寄存器。
5. 对外接口
本文件导出(供谁调用)
| 导出项 | 调用方 |
|---|---|
socket / close / listen / connect / disconnect | midware/ethernt/ethernet.c:723/:732/:708/:688/:970(云端 TCP 客户端,socket 号 SOCK_TCP_CLIENT=2);midware/interface/modbus/modbus.c:9744/:9647(Modbus TCP 从站,SOCK_TCP_SERVER=3,端口 502);midware/ethernt/lan/dns.c:520、dhcp.c:700(UDP) |
send | midware/ethernt/ethernet.c:593;net/ems.c:6241(云端上报直接发送并打印 ret) |
recv | net/server.c:6298、net/server.c:6326(先收 7 字节头再收体,依赖本文件新增的超时行为) |
sendto / recvfrom | midware/ethernt/lan/dns.c:527/:539、dhcp.c:409/:505/:565/:581/:911(全部为裸机式同步收发,未改 uCOS 超时) |
ctlsocket / setsockopt / getsockopt | ethernet.c:699、modbus.c:9657 等设置保活;其余为通用能力 |
sock_is_sending | 全局变量(未加 static),外部模块理论上可读,工程中未见外部引用 |
本文件调用的外部函数/变量
| 外部符号 | 来源 |
|---|---|
getSn_MR / getSn_SR / setSn_MR / setSn_CR / getSn_CR / setSn_IR / getSn_IR / getSn_IMR / setSn_IMR / setSn_PORT / setSn_DIPR / getSn_DIPR / setSn_DPORT / getSn_DPORT / getSn_TTL / setSn_TTL / getSn_TOS / setSn_TOS / getSn_MSSR / setSn_MSSR / getSn_TxMAX / getSn_RxMAX / getSn_TX_FSR / getSn_RX_RSR / getSn_KPALVTR / setSn_KPALVTR / getSn_TX_RD | midware/ethernt/lan/w5500.h / w5500.c(寄存器访问层) |
wiz_send_data / wiz_recv_data / wiz_recv_ignore | midware/ethernt/lan/w5500.c:327/:343/:361 |
SOCK_INIT / SOCK_CLOSED / SOCK_LISTEN / SOCK_ESTABLISHED / SOCK_CLOSE_WAIT / SOCK_UDP / SOCK_MACRAW / Sn_CR_* / Sn_IR_* / Sn_MR_* | w5500.h |
OSTimeGet | uCOS-II(UCOSII/uCOS-II/Source/os_time.c) |
OSTimeDlyHMSM | uCOS-II(os_time.c),仅在 sendto() 第 431 行使用 |
#if _WIZCHIP_ == 5200 分支中的 setSUBR | w5200.h,本工程 _WIZCHIP_ = 5500,该分支不编译 |
6. 依赖关系
依赖
socket.c
└─ socket.h
└─ wizchip_conf.h (_WIZCHIP_=5500、_WIZCHIP_SOCK_NUM_=8、wiz_send_data 等声明)
└─ w5500.h / w5500.c (Sn_* 寄存器宏与实现)
└─ uCOS-II 时基 (本文件新增的 OSTimeGet/OSTimeDlyHMSM)
被依赖(全部通过 #include "socket.h",共 7 个文件)
midware/ethernt/ethernet.c— 云端 TCP 客户端主循环,最重度使用者midware/ethernt/lan/dhcp.c、midware/ethernt/lan/dns.c— UDP 配置协议midware/ethernt/wireless/wireless_driver.c— 经wireless_*封装(实际走 e_linker,不直接调 socket)midware/interface/modbus/modbus.c— Modbus TCP 从站net/ems.c、net/server.c— 云端通信与服务器协议
对应头文件文档见 docs/knowledge/代码解析/midware/ethernt/lan/socket.h.md。
7. 备注与风险点
CHECK_SOCKNUM()的边界少一(socket.c:70):判据是sn > _WIZCHIP_SOCK_NUM_,而_WIZCHIP_SOCK_NUM_ = 8是数组长度。传sn == 8时校验通过,随后sock_remained_size[8]/sock_pack_info[8]就是数组越界写。当前调用方只用了 socket 2/3,暂未触发,但这是个真实的越界隐患。connect()的 IP 校验算式有误(socket.c:202-205):四次左移的最后一个字节取的是addr[0]而不是addr[3],导致taddr只是"IP 前 3 字节 + 首字节"。例如1.2.3.0会得到0x01020301而非0x01020300,非法地址不会按预期被SOCKERR_IPINVALID拦下。工程中connect()只连固定服务器 IP,实际影响有限,但属于 V1.0.3 注释里号称已修的问题("Validation a destination ip in connect() & sendto()")在connect()中的残留错误。sendto()里的*((uint32_t*)addr)未对齐 32 位访问(socket.c:394):addr是调用方给的uint8_t*,工程中多为结构体成员或栈数组,不保证 4 字节对齐;Cortex-M4 对单条 LDR 支持非对齐访问,但这是未定义行为级别的写法。同时第 389-392 行算出的taddr完全未被使用(既没参与判断也没参与告警),是明显的死代码,编译器会报"变量赋值后未使用"。sendto()的Sn_IR轮询形同虚设(socket.c:446-449):else { return SOCKERR_TIMEOUT; }让"中断尚未产生"也被判成超时。因为前面只让出了 1 ms,实测中 UDP 发送在负载稍高时容易误报超时,而调用方(dns.c/dhcp.c)拿到 -13 只会重试,掩盖了这一行为。recvfrom()的 IPRAW 分支有拼写错误(socket.c:548):sock_remaiend_size[sn] = ...(多了个end)。该行位于#if (_WIZCHIP_ < 5200)内,_WIZCHIP_ = 5500时不参与编译,所以现在是"潜伏的编译错误"——一旦换回 W5200 或 W5100 就会编译失败。同类问题还有send()里的#if _WZICHIP_ == 5200(socket.c:269)把_WIZCHIP_拼成了_WZICHIP_,宏未定义 → 恒为 0 → 该分支永远不编译。recvfrom()的sock_remained_size可能下溢(socket.c:566):sock_remained_size[sn] -= pack_len;没有先判pack_len <= sock_remained_size[sn]。在len > 剩余长度时上游已保证pack_len = 剩余长度,但在"上一次调用被SOCK_BUSY提前中断"等边界下,Default分支会用pack_len直接赋值sock_remained_size[sn] = pack_len,逻辑分支较绕,后续维护时容易踩。listen()的容错判据写错寄存器(socket.c:185):if(getSn_CR(sn) == SOCK_CLOSED)应为getSn_SR(sn)。结果是listen()在端口被占用/对端拒绝等异常路径下会永远自旋在while(getSn_SR(sn) != SOCK_LISTEN)里,把一个 uCOS 任务钉死。工程中modbus.c:9647会调用listen(),若 Modbus TCP 端口创建失败,该网络任务将失去调度。getsockopt()多处缺少break(socket.c:684-689、:698-704):SO_MSS会继续执行SO_DESTIP的getSn_DIPR(sn,(u8*)arg),把 4 字节 IP 覆盖掉调用方那个u8*目标;SO_SENDBUF会继续写SO_RECVBUF、SO_STATUS;SO_RECVBUF会继续写SO_STATUS。若调用方按u8语义传参,SO_SENDBUF这类"应为 u16"的取值会越界写 1 字节。当前工程未见使用这些选项,属埋雷。ctlsocket()的CS_GET_INTMASK缺break(socket.c:610-612):读完getSn_IMR后直接落入default: return SOCKERR_ARG;,即"读取中断掩码"这个功能永远返回错误码,尽管副作用已经发生。#define SOCK_ANY_PORT_NUM 0xC000;末尾多了分号(socket.c:54):展开后sock_any_port = 0xC000;;,多一个空语句,当前无害,但若被写进if (x) sock_any_port = SOCK_ANY_PORT_NUM;之外的表达式位置会出问题。SEND_TIME_OUT (500)定义了却从未使用(socket.c:62)。从命名与数值看,编写者原本想给 TCP 发送加 500 ms 超时,最终没有落地——send()在 TX 缓冲不足时用的是无限自旋,只靠Sn_SR变化退出。send()的 TX 等待循环没有超时(socket.c:289-302):非阻塞模式下会返回SOCK_BUSY,但阻塞模式下(ethernet.c、modbus.c的 socket 用了Sn_MR_ND而未带SF_IO_NONBLOCK,属阻塞)会在while(1)里一直等 TX 空闲,一旦对端不读导致窗口关闭,任务会长时间不释放 CPU。与之对照,recv()已经被补上了 2000 ms 超时,send()没有,二者不对称。send()与recv()的返回值语义不一致:send()返回的是请求长度(乐观),recv()返回的是实收长度(悲观);两者都可能在错误时返回与SOCK_BUSY(0) 同值的 0(send()的len被裁到 0 的情况、recv()的超时路径),调用方若只判> 0会把"无数据"和"超时"混为一谈。_WIZCHIP_SOCK_NUM_与数组实际长度耦合:sock_remained_size/sock_pack_info/sock_next_rd都以该宏为长度,sock_io_mode/sock_is_sending又是uint16_t位图(上限 16 个 socket)。这套设计与wizchip_conf.h中的 socket 数定义强绑定,换芯片型号需同步复核。socket()的sock_io_mode |= ((flag & SF_IO_NONBLOCK) << sn)(socket.c:152)依赖SF_IO_NONBLOCK == 0x01。若哪天该宏被改成其它位掩码(例如0x08),这一行会把状态位搬到错误的 socket 上,且编译器不会报警。