W5500标准库代码分析

Tutorial: 电子电路 Category: 嵌入式 Published: 2026-10-08 01:26:37 Views: 0 Likes: 0 Comments: 0

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)一次事务为:

  1. 主机拉低 SCSn;
  2. 发送 16 位地址;
  3. 发送 8 位控制字,控制块选择 BSB、读写位 RWB 和工作模式 OM;
  4. 继续传输一个或多个数据字节;
  5. 等 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、TIM2SPI/GPIO/定时器等 MCU 相关操作

这种分层就是移植的切入点:尽量保留已经验证过的 W5500 寄存器和 Socket 逻辑,把直接调用标准库的 MCU 适配层换成 HAL。

1.3.1 主函数分析

main() 的启动顺序不可随意颠倒:

  1. System_Initialization() 先让时钟、SPI、定时器和 GPIO 可用;
  2. Load_Net_Parameters() 只给 RAM 中的配置数组赋值;
  3. W5500_Hardware_Reset() 硬件复位,并在原例程中等待网线 Link;
  4. W5500_Initialization() 才把网络配置真正写入 W5500,并初始化 Socket 0;
  5. 主循环持续维护 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 主函数分析

主循环不是“只执行一遍初始化后就一直发数据”,而是在每一轮都运行一个简化状态机:

  1. W5500_Socket_Set() 在 Socket 未初始化时重试创建连接;
  2. W5500_Interrupt_Process() 读取并清理 W5500 事件,维护连接状态;
  3. 若有接收事件,优先执行回显;
  4. 否则在约 500 ms 到期且已连接时发送测试字符串。

TIM2 中断每 1 ms 递增 Timer2_Counter 与 W5500_Send_Delay_Counter。这两个变量同时被中断和主线程访问,移植时应声明为 volatile,否则编译器可能把主循环读取优化成不再重新取内存。共享的多字节计数值还应按目标内核的原子访问能力考虑临界区;在 32 位 Cortex-M3 上对齐的 32 位单次读写是原子的,但“读取后比较再清零”的整个序列仍可能与中断交错。

三、迁移到 HAL 库时的边界

建议按下面的顺序迁移并逐层验证:

  1. 用 CubeMX 配好 SPI1 模式 0、软件片选、GPIO 输出初始电平和调试串口;先把 SPI 时钟设得保守,再逐步提高。
  2. 写 W5500_Select()、W5500_Unselect()、单字节/多字节收发、复位和毫秒时间函数;每个阻塞操作都带超时。
  3. 先读 VERSIONR。W5500 的版本寄存器应读到 0x04,这是确认 SPI 模式、引脚、片选和帧格式的最小闭环。
  4. 再写回并读出网络公共寄存器,确认 16 位地址和控制字没有次序错误。
  5. 最后接入 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;否则网络层错误会掩盖最基础的总线问题。

参考资料