W5500标准库代码分析
162.12.4W5500标准库代码分析
void W5500_Initialization(void) /* 对 W5500 寄存器和 Socket 0 做上层初始化。 */
{ /* 初始化函数开始。 */
W5500_Init(); /* 写入网关、子网掩码、MAC、IP,并为 8 个 Socket 分配收发缓冲区。 */
Detect_Gateway(); /* 执行厂商例程自带的连通性探测;它并不等同于通用的“默认网关检测”。 */
Socket_Init(0); /* 把本地/远端地址及端口写入 Socket 0 的寄存器。 */
} /* 初始化函数结束。 */
void W5500_Socket_Set(void) /* 根据设定模式创建 Socket 0。 */
{ /* Socket 配置函数开始。 */
if (S0_State == 0) /* 仅在软件状态尚未初始化时尝试建立 Socket。 */
{ /* 未初始化分支开始。 */
if (S0_Mode == TCP_SERVER) /* 若配置为 TCP 服务器。 */
{ /* TCP 服务器分支开始。 */
if (Socket_Listen(0) == TRUE) /* 打开 Socket 0 并进入监听,检查调用是否成功。 */
S0_State = S_INIT; /* 监听启动成功,记录已初始化状态。 */
else /* 监听启动失败。 */
S0_State = 0; /* 保持未初始化,主循环下一轮会重试。 */
} /* TCP 服务器分支结束。 */
else if (S0_Mode == TCP_CLIENT) /* 若配置为 TCP 客户端,本课实际走这个分支。 */
{ /* TCP 客户端分支开始。 */
if (Socket_Connect(0) == TRUE) /* 向 192.168.1.190:6000 发起连接,检查命令是否成功。 */
S0_State = S_INIT; /* 连接命令已启动,先记录已初始化;建链完成由后续事件更新。 */
else /* 连接命令启动失败。 */
S0_State = 0; /* 保持未初始化,主循环下一轮会重试。 */
} /* TCP 客户端分支结束。 */
else /* 其余模式按 UDP 处理。 */
{ /* UDP 分支开始。 */
if (Socket_UDP(0) == TRUE) /* 打开 UDP Socket 0,检查调用是否成功。 */
S0_State = S_INIT | S_CONN; /* UDP 无连接握手,打开后直接标记为可收发。 */
else /* UDP Socket 打开失败。 */
S0_State = 0; /* 保持未初始化,主循环下一轮会重试。 */
} /* UDP 分支结束。 */
} /* 未初始化分支结束。 */
} /* Socket 配置函数结束。 */
void Process_Socket_Data(SOCKET s) /* 接收一帧可读数据并原样回送。 */
{ /* 回显函数开始。 */
unsigned short size; /* 保存本次从 W5500 RX 缓冲区取出的字节数。 */
size = Read_SOCK_Data_Buffer(s, Rx_Buffer); /* 读取 Socket 接收缓冲区,并推进 W5500 的读指针。 */
memcpy(Tx_Buffer, Rx_Buffer, size); /* 将收到的数据复制到发送缓冲区;这里没有业务变换。 */
Write_SOCK_Data_Buffer(s, Tx_Buffer, size); /* 把相同数据写回 W5500 TX 缓冲区并触发发送。 */
} /* 回显函数结束。 */
int main(void) /* 厂商标准外设库示例的主入口。 */
{ /* 主函数开始。 */
System_Initialization(); /* 初始化 STM32 时钟、NVIC、SPI1、TIM2 和控制 GPIO。 */
Load_Net_Parameters(); /* 装入本机 192.168.1.199:5000 和服务器 192.168.1.190:6000。 */
W5500_Hardware_Reset(); /* 拉低 RSTn 50 ms,释放后等待 PHY Link。 */
W5500_Initialization(); /* 初始化网络公共寄存器和 Socket 0。 */
while (1) /* 进入永久轮询。 */
{ /* 主循环开始。 */
W5500_Socket_Set(); /* 按 Socket 软件状态尝试监听、连接或打开 UDP。 */
W5500_Interrupt_Process(); /* 轮询并处理 W5500 中断寄存器,名称含“Interrupt”但此处不是 MCU 外部中断入口。 */
if ((S0_Data & S_RECEIVE) == S_RECEIVE) /* 如果 Socket 0 的接收事件已置位。 */
{ /* 接收分支开始。 */
S0_Data &= ~S_RECEIVE; /* 清除软件接收标志。 */
Process_Socket_Data(0); /* 将收到的数据原样回显。 */
} /* 接收分支结束。 */
else if (W5500_Send_Delay_Counter >= 500) /* 若没有待处理接收且已累计约 500 ms。 */
{ /* 周期发送分支开始。 */
if (S0_State == (S_INIT | S_CONN)) /* 只有 Socket 0 已初始化且已连接时才发送。 */
{ /* 已连接分支开始。 */
S0_Data &= ~S_TRANSMITOK; /* 清除上一次发送成功的软件标志。 */
memcpy(Tx_Buffer, "\r\nSPI W5500 NET TEST!\r\n", 23); /* 准备恰好 23 字节的测试报文。 */
Write_SOCK_Data_Buffer(0, Tx_Buffer, 23); /* 经 Socket 0 发送测试报文。 */
} /* 已连接分支结束。 */
W5500_Send_Delay_Counter = 0; /* 无论是否发送,都重新开始 500 ms 计时。 */
} /* 周期发送分支结束。 */
} /* 主循环结束;正常情况下不会退出。 */
} /* 主函数结束。 */
void SPI1_Send_Byte(unsigned char dat) /* 原例程的单字节 SPI 写函数,保留以便分析问题。 */
{ /* 单字节写函数开始。 */
SPI_I2S_SendData(SPI1, dat); /* 直接写 SPI 数据寄存器。 */
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); /* 只等发送缓冲区空,没有等 RXNE 和 BSY。 */
} /* 单字节写函数结束;这段实现不宜原样迁移到 HAL。 */
void W5500_Hardware_Reset(void) /* 原例程的 W5500 硬件复位与链路等待。 */
{ /* 复位函数开始。 */
GPIO_ResetBits(W5500_RST_PORT, W5500_RST); /* RSTn 拉低,进入硬件复位。 */
Delay(50); /* 保持 50 ms,远大于数据手册要求的至少 500 us。 */
GPIO_SetBits(W5500_RST_PORT, W5500_RST); /* RSTn 拉高,释放复位。 */
Delay(200); /* 等待芯片内部与 PHY 稳定。 */
while ((Read_W5500_1Byte(PHYCFGR) & LINK) == 0); /* 原代码无限等待网线 Link,实际项目应增加超时和错误返回。 */
} /* 复位函数结束。 */
本课所说的“标准库代码”,是采用 STM32F1 Standard Peripheral Library 编写的模块厂商示例,不是 WIZnet 官方 ioLibrary。课程目标是先看懂它的分层和调用关系,为下一步移植到 HAL 库做准备。
一、SPI基础知识
1.1 SPI
SPI 是同步串行总线。STM32 作为主机时,常见信号为 SCK、MOSI、MISO 和片选 CS,再加共地与供电;W5500 模块还使用 RSTn,若采用硬件中断方案还会接 INTn。这里的 SPI1 引脚为 PA5/SCK、PA6/MISO、PA7/MOSI,片选由普通 GPIO PA4 控制,复位由 PC5 控制。
“SPI 没有协议”并不准确。SPI 外设只负责时钟边沿、移位顺序和收发数据寄存器,不规定所有从设备统一使用什么命令;但每个器件一定有自己的事务格式。W5500 的可变长度数据模式(VDM)一次事务为:
- 主机拉低 SCSn;
- 发送 16 位地址;
- 发送 8 位控制字,控制块选择 BSB、读写位 RWB 和工作模式 OM;
- 继续传输一个或多个数据字节;
- 等 SPI 真正传输完毕后拉高 SCSn。
W5500 支持 SPI 模式 0 和模式 3,数据在上升沿采样、下降沿改变。示例使用 CPOL=0、CPHA=0,即模式 0;APB2 为 72 MHz、预分频为 4 时,SPI1 时钟为 18 MHz。源码中“预分频值为 2”的注释与实际枚举 SPI_BaudRatePrescaler_4 不一致,应以配置值为准。
原 SPI_Configuration() 还有两个需要修正的细节:
- PA5/SCK 和 PA7/MOSI 应配置为复用推挽输出;主机的 PA6/MISO 是输入,不应与前两者一起配置成复用推挽输出。CubeMX 生成的 SPI 主机代码通常会把 MISO 配为输入。
SPI_CRCPolynomial=0与旁边“CRC 多项式为 7”的旧注释矛盾;本例也没有启用硬件 CRC,所以该注释与业务无关。
原 SPI1_Send_Byte() 只在写入后等待 TXE。TXE 仅表示发送数据寄存器可以装下一个字节,并不表示最后一位已经从引脚发完;全双工 SPI 每发送一字节也会收到一字节,若不读取 RXNE/DR 还可能产生接收溢出。移植时可用带超时的 HAL_SPI_TransmitReceive(),或实现完整的 TXE、RXNE、BSY 与错误状态处理,并在 BSY 清零后再释放 CS。
1.2 实验演示
本课演示的是 TCP 客户端例程:W5500 使用固定地址 192.168.1.199:5000,主动连接电脑端服务器 192.168.1.190:6000。连接成功后,程序有两条数据路径:
- 收到服务器数据:读入
Rx_Buffer,复制到Tx_Buffer,再原样发回,因此它是一个回显(echo)测试。 - 大约每 500 ms:若 Socket 0 已连接,发送 23 字节
\r\nSPI W5500 NET TEST!\r\n。
由于主循环使用 if ... else if ...,接收事件优先于周期发送;如果接收事件持续到来,周期报文可能被推迟。TIM2 每 1 ms 增加计数器,但 Delay() 是忙等,CPU 在延时期间不能做其他主循环工作。HAL 迁移时,初始化阶段可用 HAL_Delay();周期任务更适合比较 HAL_GetTick() 的时间差,避免阻塞。
1.3 官方代码分析
这一节标题沿用课程原结构,但资料来源应说清楚:仓库中的工程目录名为“泥人 W5500 模块-客户端模式例程”,属于模块厂商提供的 STM32 标准外设库示例。WIZnet 官方则另有 ioLibrary_Driver,两者接口、文件组织和质量边界不能混称。
示例大致分为三层:
| 层次 | 主要文件/函数 | 职责 |
|---|---|---|
| 应用层 | main.c | 网络参数、模式选择、连接维护、周期发送、接收回显 |
| W5500 驱动层 | W5500.c/.h | 公共寄存器、Socket 寄存器、收发缓冲区和命令操作 |
| STM32 适配层 | SPI_Configuration()、GPIO、TIM2 | SPI/GPIO/定时器等 MCU 相关操作 |
这种分层就是移植的切入点:尽量保留已经验证过的 W5500 寄存器和 Socket 逻辑,把直接调用标准库的 MCU 适配层换成 HAL。
1.3.1 主函数分析
main() 的启动顺序不可随意颠倒:
System_Initialization()先让时钟、SPI、定时器和 GPIO 可用;Load_Net_Parameters()只给 RAM 中的配置数组赋值;W5500_Hardware_Reset()硬件复位,并在原例程中等待网线 Link;W5500_Initialization()才把网络配置真正写入 W5500,并初始化 Socket 0;- 主循环持续维护 Socket 状态、轮询中断寄存器、收发数据。
上一课已列出完整网络参数。端口数组采用网络字节序:0x13 0x88 = 0x1388 = 5000,0x17 0x70 = 0x1770 = 6000。IP 与掩码计算得到双方都位于 192.168.1.0/24,因此直连测试的数据不会经过默认网关。
W5500_Interrupt_Process() 虽然名字含“中断处理”,本例却是在 while (1) 中主动调用它,实质是轮询 W5500 的中断状态寄存器并更新 S0_State、S0_Data 等软件标志;不要把它误认为 STM32 的 EXTI 中断服务函数。
1.3.2 代码的烧写
烧写失败不能只看“J-LINK 还是 ST-LINK”。Keil 工程至少要同时核对:
- Device:必须选择开发板上真实的 MCU;
- Debug Adapter:选择实际连接的 ST-LINK/J-LINK,并使用正确接口,本板通常为 SWD;
- Flash Download Algorithm:容量和器件系列必须匹配真实片上 Flash;
- 复位与连线:SWDIO、SWCLK、GND、目标电压参考、NRST(需要时)要可靠连接。
本地 Project.uvproj 当前写的是 STM32F101R8,并配置了 128 KiB Flash 算法;这与课程所用 STM32F103 大容量开发板并不一致。即使外围寄存器地址有大量共性,也不能把“能编译”当成目标配置正确。下载前应按板载真实型号(例如 STM32F103ZE)重选 Device、启动文件、容量宏和 Flash 算法。用 STM32CubeProgrammer 单独烧录 HEX 只能绕开 Keil 的下载器操作,不能修正编译/链接时选错芯片的问题。
二、OLED屏幕代码分析
OLED 项目的调用关系通常是:
main 应用逻辑 → oled.c 显示驱动 → I2C 底层读写 → STM32 外设/引脚 → OLED 控制器
W5500 工程对应为:
main 网络应用逻辑 → W5500 Socket/寄存器驱动 → SPI 底层读写 → STM32 SPI/GPIO → W5500
类比的价值在于识别“上层器件逻辑”和“下层 MCU 接口”。但 OLED 的命令格式与 W5500 的地址/控制字/数据帧完全不同,不能因为都通过串行总线通信就直接复用器件驱动。
2.1 oled.c文件
oled.c 会把写命令、写数据、光标、字符/图形等显示语义封装起来,调用者不必在 main() 里逐次翻转 I2C 引脚。若从标准库迁移到 HAL,通常保留字模与显示命令逻辑,只替换 I2C/GPIO/延时等平台相关函数。
这一思路同样适用于 W5500:迁移不是把所有标准库函数机械改名,而是先确定抽象边界,再替换边界以下的实现。
2.2 w5500.c文件
W5500.c 既包含 SPI/GPIO 这种平台相关代码,也包含 W5500 寄存器与缓冲区算法。阅读时可按下面的调用链追踪一次发送:
Write_SOCK_Data_Buffer() → 读取 TX 空闲空间与写指针 → Write_W5500_SOCK_Buffer() → 发送 16 位地址、8 位控制字和数据 → 更新 TX 写指针 → 向 Sn_CR 写 SEND 命令。
一次接收则会读取 RX 已接收字节数和读指针,从 W5500 RX 缓冲区取数,更新读指针,再发出 RECV 命令。W5500 内部共有 32 KiB Socket 缓冲区;示例为 8 个 Socket 各分配 2 KiB TX 和 2 KiB RX,合计正好 32 KiB。
2.2.1 w5500初始化配置函数
W5500_Initialization() 实际串起三步:
W5500_Init():写入 GAR(网关)、SUBR(掩码)、SHAR(MAC)、SIPR(本机 IPv4),设置重传参数,并配置各 Socket 收发缓冲区大小。Detect_Gateway():厂商例程自定义的连通性探测。检查其源码可见,它并非简单读取或验证Gateway_IP,名字容易让人误解;同网段直连也不依赖默认网关,移植时不应把它当成通用必需步骤。Socket_Init(0):把 Socket 0 的源端口、目的 IP 和目的端口写入寄存器,并清理软件状态。
硬件复位的低电平 50 ms 满足 W5500 数据手册规定的至少 500 μs,但最后的 Link 等待没有超时。未插网线、对端未上电或协商失败时,程序会永远停在初始化阶段。更稳妥的接口应返回“复位成功但链路未就绪”等错误状态,由主循环稍后重试并允许打印诊断信息。
2.2.2 w5500 socket连接函数
W5500 有 8 个硬件 Socket(0~7)。本例只用 Socket 0,并用两组软件变量描述状态:
S0_State:记录已初始化S_INIT、已连接S_CONN等连接状态;S0_Data:记录接收S_RECEIVE、发送完成S_TRANSMITOK等数据事件。
TCP 客户端的典型过程是 CLOSED → OPEN → INIT → CONNECT → ESTABLISHED。连接并不是调用 Socket_Connect() 后立刻完成;W5500 随后根据硬件中断寄存器报告连接成功、超时、断开或复位,轮询函数再更新软件状态。TCP 服务器走 OPEN → INIT → LISTEN → ESTABLISHED;UDP 只需 OPEN,没有 TCP 握手,因此示例打开后直接写入 S_INIT | S_CONN 作为“可收发”标志。
驱动函数和应用缓冲区都要检查长度边界与超时。原示例的 Process_Socket_Data() 直接按驱动返回的 size 复制,精简易懂,但生产代码必须确认 size 不超过 Rx_Buffer 和 Tx_Buffer 的容量,并处理 0、错误码和连接中途关闭。
2.2.3 主函数分析
主循环不是“只执行一遍初始化后就一直发数据”,而是在每一轮都运行一个简化状态机:
W5500_Socket_Set()在 Socket 未初始化时重试创建连接;W5500_Interrupt_Process()读取并清理 W5500 事件,维护连接状态;- 若有接收事件,优先执行回显;
- 否则在约 500 ms 到期且已连接时发送测试字符串。
TIM2 中断每 1 ms 递增 Timer2_Counter 与 W5500_Send_Delay_Counter。这两个变量同时被中断和主线程访问,移植时应声明为 volatile,否则编译器可能把主循环读取优化成不再重新取内存。共享的多字节计数值还应按目标内核的原子访问能力考虑临界区;在 32 位 Cortex-M3 上对齐的 32 位单次读写是原子的,但“读取后比较再清零”的整个序列仍可能与中断交错。
三、迁移到 HAL 库时的边界
建议按下面的顺序迁移并逐层验证:
- 用 CubeMX 配好 SPI1 模式 0、软件片选、GPIO 输出初始电平和调试串口;先把 SPI 时钟设得保守,再逐步提高。
- 写
W5500_Select()、W5500_Unselect()、单字节/多字节收发、复位和毫秒时间函数;每个阻塞操作都带超时。 - 先读
VERSIONR。W5500 的版本寄存器应读到0x04,这是确认 SPI 模式、引脚、片选和帧格式的最小闭环。 - 再写回并读出网络公共寄存器,确认 16 位地址和控制字没有次序错误。
- 最后接入 Socket 初始化、连接状态机、收发缓冲区和应用逻辑。
HAL 迁移后可用 HAL_GPIO_WritePin() 控制 CS/RST,用 HAL_SPI_TransmitReceive() 完成全双工字节交换,用 HAL_GetTick() 实现非阻塞超时。不要把标准库内部的 SPI_I2S_SendData() 直接复制进业务代码,也不要只替换函数名而保留旧例程的 MISO 配置、无超时死循环和只等 TXE 等问题。
四、本课易错点总结
- W5500 不是“SPI 转网口”这么简单,而是集成硬件 TCP/IP 核、10/100M 以太网 MAC 和 PHY 的网络控制器;外部 MCU 通过 SPI 操作它。
- W5500 的硬件 TCP/IP 核支持 IPv4,不提供 IPv6 硬件协议栈。
W5500_Interrupt_Process()在本例是轮询调用,不等于使用 INTn + EXTI 的真正中断驱动方案。- SPI 的 TXE、RXNE 和 BSY 含义不同;拉高 CS 前必须确保最后一位已经发送完成。
- RSTn 低 50 ms 没问题,真正的问题是 Link 无限等待没有超时。
- 烧写器、目标 MCU、启动文件、容量宏和 Flash 算法需要同时匹配,不能只改调试器名称。
- 先用
VERSIONR == 0x04验证底层 SPI,再调 Socket;否则网络层错误会掩盖最基础的总线问题。
参考资料
- WIZnet W5500 Datasheet v1.1.0
- WIZnet W5500 产品概览
- ST RM0008:STM32F101/102/103/105/107 参考手册
- 本地源码:
源码/3、STM32单片机裸机部分/code/KEIL/10泥人W5500模块-客户端模式例程/USER/main.c - 本地驱动:
源码/3、STM32单片机裸机部分/code/KEIL/10泥人W5500模块-客户端模式例程/USER/W5500.c - 本地 Keil 配置:
源码/3、STM32单片机裸机部分/code/KEIL/10泥人W5500模块-客户端模式例程/USER/Project.uvproj