STM32F4+W5500 移植与开发常见问题

本文档基于 STM32F4 + W5500 (SPI接口) 的实际开发经验整理。


零、核心原理详解

0.1 W5500 硬件架构

W5500 是一颗硬件TCP/IP协议栈芯片,与传统软件协议栈(如lwIP)有本质区别:

复制代码
┌─────────────────────────────────────────────────┐
│                   W5500 芯片内部                  │
│                                                   │
│  ┌──────────┐   ┌──────────────┐   ┌──────────┐ │
│  │ SPI接口   │──▶│  寄存器组     │──▶│ TCP/IP   │ │
│  │ (SCSn/   │   │  (Common +   │   │ 硬件引擎  │ │
│  │  SCLK/   │   │   Socket)    │   │          │ │
│  │  MISO/   │   │              │   │  ┌────┐  │ │
│  │  MOSI)   │◀──│              │◀──│  │8个  │  │ │
│  └──────────┘   └──────────────┘   │  │Socket│ │ │
│                                     │  │缓冲区│ │ │
│  ┌──────────┐                      │  │2KB×8│ │ │
│  │ PHY      │◀─────────────────────│  └────┘  │ │
│  │ (10/100M │                      │          │ │
│  │  Auto-   │                      │  ARP/ICMP│ │
│  │  Nego)   │                      │  DNS/DHCP│ │
│  └────┬─────┘                      └──────────┘ │
│       │ RJ45                                       │
└───────┼─────────────────────────────────────────┘
        │ 网线
        ▼
     路由器/PC

核心特点

  • STM32只需通过SPI读写寄存器,不需要实现TCP/IP协议栈
  • W5500内部有8个独立Socket,每个可独立做TCP/UDP通信
  • 每个Socket有独立的2KB收发缓冲区(可配置大小)
  • PHY层(物理层)集成在芯片内,支持10/100Mbps自动协商

0.2 SPI通信原理

W5500 SPI通信采用帧格式,每次读写包含3段:

复制代码
SPI帧格式:
┌─────────────┬──────────┬──────────────┐
│ 控制段(3B)   │ 地址段    │ 数据段        │
│ Opcode+BSB  │ 2 bytes  │ 1~N bytes    │
└─────────────┴──────────┴──────────────┘

控制段:
  Bit 7: Opcode[0]  --- 0=读, 1=写
  Bit 6: Opcode[1]  --- 0=VDM, 1=FDM
  Bit 5-3: 保留
  Bit 2-0: BSB --- 选择数据块

BSB数据块选择:
  000 = Common寄存器块
  001 = Socket 0寄存器块
  010 = Socket 0 TX缓冲区
  011 = Socket 0 RX缓冲区
  100 = Socket 1寄存器块
  ...

CS时序

复制代码
CSN ──┐                                          ┌── HIGH (空闲)
       │                                          │
       └──────────  整个帧期间保持LOW  ──────────┘
SCLK ──────┐  ┌──┐  ┌──┐  ┌──┐  ┌──┐      ┌──┐──
            └──┘  └──┘  └──┘  └──┘  ...  └──┘
MOSI ──[Op][BSB]──[AddrH][AddrL]──[Data0][Data1]...
MISO ────────────────────────────[Data0][Data1]...

关键点

  • CS必须初始为HIGH(未选中),否则总线冲突
  • 每次SPI操作必须:CS拉低 → 发送帧 → CS拉高
  • 临界区保护:SPI操作期间禁止中断打断,防止CS时序错乱

0.3 PHY自动协商原理

PHY(物理层)负责将数字信号转换为可在网线上传输的电信号。两台设备连接时,需要协商以下参数:

协商内容

参数 选项 说明
速率 10Mbps / 100Mbps 数据传输速率
双工 半双工 / 全双工 半双工不能同时收发,全双工可以

自动协商过程

复制代码
设备A (W5500)                    设B (路由器/PC)
    │                                 │
    │──── FLP脉冲 (能力通告) ────────▶│
    │◀─── FLP脉冲 (能力通告) ────────│
    │                                 │
    │  双方比较能力,选择最优组合       │
    │  优先级: 100M全双工 > 100M半双工 │
    │         > 10M全双工 > 10M半双工  │
    │                                 │
    │──── 链路建立 (Link UP) ─────────│

为什么自动协商会失败

  1. FLP脉冲不兼容:W5500发送的Fast Link Pulse脉冲格式与某些路由器交换芯片不完全兼容,导致双方对协商结果理解不一致

  2. 并行检测问题 :部分路由器在自动协商失败后回退到并行检测,只能检测到载波信号但无法确定双工模式,默认使用半双工。如果W5500协商为全双工,就会产生双工不匹配

  3. 双工不匹配的危害

    全双工设备 (W5500) 半双工设备 (路由器)
    │ │
    │──── 发送数据 ──────────────▶│
    │──── 发送数据 ──────────────▶│ ← 路由器在发送时检测到冲突
    │ │ 但全双工端不监听冲突
    │ │ → 数据丢失,重传风暴
    │◀─── 数据被丢弃 ────────────│

  4. 为什么直连电脑能工作:PC网卡PHY芯片兼容性强,能正确解析W5500的协商脉冲;路由器交换芯片更廉价、兼容性差

  5. 强制10Mbps半双工为什么有效:跳过自动协商,双方都明确知道是10Mbps半双工,无歧义。10Mbps半双工是最基础的以太网模式,所有设备都支持

0.4 TCP连接建立与状态机

W5500内部硬件实现TCP状态机,STM32通过读写Socket寄存器控制:

TCP三次握手过程

复制代码
STM32+W5500 (Client)                    Server (8.130.55.27:1111)
    │                                          │
状态: SOCK_INIT (0x13)                     状态: LISTEN
    │                                          │
    │ 1. STM32调用 connect()                   │
    │    W5500发送 SYN包                        │
    │─────────── SYN ─────────────────────────▶│
    │                                          │
    │                            2. Server回应  │
    │◀──────── SYN+ACK ───────────────────────│
    │                                          │
状态: SOCK_ESTABLISHED (0x17)               状态: ESTABLISHED
    │ 3. W5500自动回复 ACK                      │
    │─────────── ACK ─────────────────────────▶│
    │                                          │
    │          连接建立,可以收发数据             │

Socket状态转换图

复制代码
                    socket(TCP)
    SOCK_CLOSED ───────────────▶ SOCK_INIT
    (0x00)          open           (0x13)
      ▲                        ┌─────┴─────┐
      │                        │           │
      │                   connect()     listen()
      │                        │           │
      │                        ▼           ▼
      │              SOCK_SYNSENT     SOCK_LISTEN
      │                (0x15)          (0x14)
      │                  │                │
      │         ┌────────┼────────┐       │ 收到SYN
      │         │        │        │       │ (Client连接请求)
      │    收到RST/  收到SYN+ACK  超时    │
      │    被拒绝    (握手成功)      │     │
      │         │        │        │       ▼
      └─────────┘   SOCK_ESTABLISHED◀───┘  (自动回复SYN+ACK)
                     (0x17)
                         │
              收发数据    │        超时/断开(FIN/ACK)
                         ▼              │
              SOCK_CLOSE_WAIT ◀────────┘
                 (0x1C)
                   │
              close()
                   │
                   ▼
              SOCK_CLOSED (0x00)

connect()返回值原理

c 复制代码
// ioLibrary connect() 内部逻辑(简化)
int8_t connect(sn, addr, port) {
    setSn_DIPR(sn, addr);       // 设置目标IP
    setSn_DPORTR(sn, port);     // 设置目标端口
    setSn_CR(sn, Sn_CR_CONNECT); // 发出CONNECT命令
    while (getSn_CR(sn));        // 等待命令被接受

    // 阻塞等待连接结果
    while (getSn_SR(sn) != SOCK_ESTABLISHED) {
        if (getSn_IR(sn) & Sn_IR_TIMEOUT)   // 超时中断
            return SOCKERR_TIMEOUT;          // -13
        if (getSn_SR(sn) == SOCK_CLOSED)    // 被拒绝/异常关闭
            return SOCKERR_SOCKCLOSED;       // -4
    }
    return SOCK_OK;  // 1
}
  • 返回 -4:socket在等待过程中变成了CLOSED,说明收到了RST(拒绝)或物理层不通导致W5500内部超时关闭socket
  • 返回 -13:TCP重试次数耗尽(由RCR寄存器控制,默认8次),通常是网络不可达

0.5 DHCP原理

DHCP(动态主机配置协议)用于自动获取IP地址,使用UDP协议:

DHCP交互过程

复制代码
W5500 (DHCP Client)                    Router (DHCP Server)
    │                                          │
    │ 1. DHCP DISCOVER (UDP广播 255.255.255.255)
    │──── "谁有IP给我一个?" ─────────────────▶│
    │      (源端口68, 目标端口67)                │
    │                                          │
    │ 2. DHCP OFFER                            │
    │◀─── "我有192.168.8.34给你" ─────────────│
    │                                          │
    │ 3. DHCP REQUEST                          │
    │──── "我要这个IP" ──────────────────────▶│
    │                                          │
    │ 4. DHCP ACK                              │
    │◀─── "确认,IP给你用" ───────────────────│
    │                                          │
    │  获取到: IP=192.168.8.34                 │
    │         GW=192.168.8.1                   │
    │         SN=255.255.255.0                 │
    │         DNS=192.168.8.1                  │
    │         租约时间=7200秒                   │

DHCP使用UDP广播,这也是为什么DHCP失败能作为底层通信的诊断工具:

  • 如果DHCP成功 → 说明W5500能发送和接收数据包,PHY层通信正常
  • 如果DHCP超时 → 说明物理层有问题(如速率/双工不匹配),数据包根本发不出去或收不到

租约维护 :DHCP获取的IP有租约时间(通常2小时),需要定期调用 DHCP_run() 续约:

复制代码
获取IP ──▶ 50%租约时间 ──▶ DHCP_REQUEST(续约) ──▶ ACK ──▶ 继续使用
                                    │
                              (每秒调用DHCP_run)

0.6 ARP原理与网关转发

当W5500要发送数据到某个IP时,需要先知道目标MAC地址(以太网是硬件地址通信):

同一网段(直接ARP)

复制代码
W5500 (192.168.8.10)                    PC (192.168.8.100)
    │                                          │
    │  ARP Request (广播)                       │
    │──── "192.168.8.100的MAC是多少?" ───────▶│
    │                                          │
    │  ARP Reply (单播)                         │
    │◀─── "我的MAC是 xx:xx:xx:xx:xx:xx" ──────│
    │                                          │
    │  现在可以直接发送以太网帧                   │

跨网段(通过网关)

复制代码
W5500 (192.168.8.10)              Router (192.168.8.1)           Server (8.130.55.27)
    │                                    │                              │
    │ 1. W5500发现8.130.55.27不在192.168.8.x网段                        │
    │    需要通过网关转发                                               │
    │                                    │                              │
    │ 2. ARP请求网关MAC                   │                              │
    │──── "192.168.8.1的MAC是多少?" ──▶│                              │
    │◀─── "网关MAC是 yy:yy:yy:yy:yy:yy" │                              │
    │                                    │                              │
    │ 3. 发送TCP SYN到网关MAC             │                              │
    │    (目标IP=8.130.55.27)            │                              │
    │    (目标MAC=网关MAC)               │                              │
    │──────────────────────────────────▶│                              │
    │                                    │ 4. 路由器NAT转发              │
    │                                    │───────────────────────────▶│

关键点:W5500内部自动处理ARP,用户只需设置正确的网关IP。但如果PHY层通信有问题,ARP请求发不出去或回应收不到,所有上层通信都会失败。

0.7 中断方式原理

W5500的INTn引脚在以下事件发生时拉低(低电平有效):

复制代码
事件发生                    INTn引脚        STM32
    │                          │              │
    │  W5500设置Sn_IR寄存器     │              │
    │  对应bit置1               │              │
    │                          │              │
    │──────── INTn拉低 ──────────────────▶│   │
    │                          │         EXTI│  │
    │                          │         触发 │  │
    │                          │              │
    │  STM32读Sn_IR寄存器       │              │
    │  判断是哪个事件            │              │
    │                          │              │
    │  STM32写Sn_IR清除中断     │              │
    │  (写1清除)                │              │
    │                          │              │
    │  如果所有Sn_IR都清零      │              │
    │  且SIR寄存器清零          │              │
    │──────── INTn拉高 ──────────────────▶│   │
    │                          │         恢复  │

中断掩码寄存器

  • Sn_IMR:Socket中断掩码,控制哪些事件触发中断
  • SIMR:Socket中断汇总掩码,控制哪些Socket的中断输出到INTn引脚
  • Sn_IR:Socket中断状态,读取判断事件,写1清除

中断 vs 轮询的本质区别

复制代码
轮询方式:
  主循环 ──▶ 读Sn_SR ──▶ 没事 ──▶ 等待100ms ──▶ 读Sn_SR ──▶ 有数据!处理
                    └─────────── CPU持续占用 ───────────────┘

中断方式:
  主循环 ──▶ 做其他事 ──▶ 做其他事 ──▶ [INTn中断] ──▶ 处理数据 ──▶ 继续做其他事
                                          ▲
                                   CPU空闲,事件驱动

一、编译错误

1. ptrdiff_t is undefined

现象 :编译 wizchip_conf.c 报错 identifier "ptrdiff_t" is undefined

原因wizchip_conf.c 中使用了 (ptrdiff_t)AddrSel 进行指针类型转换,但未包含定义 ptrdiff_t 的头文件。

解决 :在 wizchip_conf.c 顶部添加:

c 复制代码
#include <stddef.h>

2. _WIZCHIP_ID_ is undefined

现象 :编译报错 _WIZCHIP_ID_ 未定义

原因wizchip_conf.h 只定义了 _WIZCHIP_ 宏(枚举值),但 wizchip_conf.c 中 WIZCHIP 结构体的 id 字段需要字符串形式的 _WIZCHIP_ID_

解决 :在 wizchip_conf.h 中添加:

c 复制代码
#define _WIZCHIP_ID_   "W5500\0"

3. WIZCHIP 结构体初始化报错

现象id 字段初始化类型不匹配

原因id 是字符数组,不能用字符串字面量直接赋值,需要用花括号包裹。

解决

c 复制代码
_WIZCHIP WIZCHIP = {
    _WIZCHIP_IO_MODE_,
    {_WIZCHIP_ID_,},          // 注意花括号
    ...
};

4. SOCK_DISCONN is undefined

现象 :编译报错 SOCK_DISCONN 未定义

原因SOCK_DISCONN 是 W6100/W6300 芯片的状态,W5500 没有此状态码。W5500 断开时应使用 SOCK_CLOSE_WAIT (0x1C)。

解决 :将 SOCK_DISCONN 替换为 SOCK_CLOSE_WAIT

W5500 的 Socket 状态码:

状态 说明
SOCK_CLOSED 0x00 已关闭
SOCK_INIT 0x13 已初始化(TCP socket打开后)
SOCK_LISTEN 0x14 监听状态(TCP Server)
SOCK_ESTABLISHED 0x17 连接已建立
SOCK_CLOSE_WAIT 0x1C 等待关闭
SOCK_UDP 0x22 UDP模式
SOCK_MACRAW 0x42 MACRAW模式

5. 变量跨 case 初始化警告

现象 :ARMCC 编译器警告 transfer of control bypasses initialization of variable

原因:在 switch-case 中直接声明变量,其他 case 可能跳过该变量的初始化。

解决 :用 {} 块作用域包裹变量声明:

c 复制代码
case SOCK_ESTABLISHED:
{
    int32_t len = getSn_RX_RSR(sn);
    ...
}
break;

二、运行问题

6. printf 串口输出无内容

现象 :代码中有 printf() 但串口看不到任何输出

原因 :ARMCC 标准库的 printf 使用 semihosting,需要调试器支持;MicroLib 直接调用 fputc

解决

  1. Keil 项目 Options → Target → 勾选 Use MicroLib
  2. usart.c 中添加 fputc 重定向:
c 复制代码
#ifdef __GNUC__
#define PUTCHAR_PROTOTYPE int __io_putchar(int ch)
#else
#define PUTCHAR_PROTOTYPE int fputc(int ch, FILE *f)
#endif

PUTCHAR_PROTOTYPE
{
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
    return ch;
}
  1. usart.h 中添加 #include <stdio.h>

7. W5500 SPI 通信失败 / 读出全0或全FF

现象:W5500 初始化后读寄存器返回全0或全0xFF

原因(常见排查顺序)

  1. CS引脚初始状态错误:CS应为 HIGH(未选中),初始化为 LOW(选中)会导致 SPI 总线冲突

    • 解决:HAL_GPIO_WritePin(SPI_CS_Port, SPI_CS_Pin, GPIO_PIN_SET);
  2. 复位延时不够:W5500 硬件复位后需要约50ms稳定时间才能进行 SPI 访问

    • 解决:
    c 复制代码
    HAL_GPIO_WritePin(RST_Port, RST_Pin, GPIO_PIN_RESET);
    HAL_Delay(10);
    HAL_GPIO_WritePin(RST_Port, RST_Pin, GPIO_PIN_SET);
    HAL_Delay(50);  // 关键:50ms
  3. SPI模式错误:W5500 只支持 SPI Mode 0 (CPOL=0, CPHA=0) 或 Mode 3 (CPOL=1, CPHA=1)

    • 解决:CubeMX 中 SPI 设置为 CPOL=Low, CPHA=1Edge
  4. SPI速率过高:W5500 SPI 最高 80MHz(3.3V时),但初试建议用低速率

    • 解决:先设较低速率验证,成功后再提高

8. Socket缓冲区初始化失败

现象ctlwizchip(CW_INIT_WIZCHIP, (void*)txsize) 返回 -1

原因:该函数假设 txsize 和 rxsize 在连续内存中,但它们是独立数组,地址不相邻。

解决 :使用 wizchip_init() 函数替代:

c 复制代码
// 错误方式
ctlwizchip(CW_INIT_WIZCHIP, (void*)txsize);

// 正确方式
wizchip_init(txsize, rxsize);

9. TCP连接反复断开重连

现象:TCP连接建立后立即断开,循环执行 open → connect → close → open

原因及解决

  1. 每秒都调用 send():ESTABLISHED 状态每次循环都发数据,服务端可能不接受高频无意义数据

    • 解决:仅在首次连接或需要时发送,心跳间隔设为5秒以上
  2. 缺少状态跟踪:每次循环都重新打印连接信息

    • 解决:用 tcp_state 变量跟踪连接状态,避免重复操作
  3. 断线后立即重连:服务端关闭后1秒就重连,造成频繁连接

    • 解决:增加 reconnect_delay,断开后等待3-5秒再重连

推荐代码结构:

c 复制代码
static uint8_t tcp_state = 0;

switch (getSn_SR(SOCK_TCP))
{
  case SOCK_CLOSED:
    if (reconnect_delay > 0) { reconnect_delay--; break; }
    socket(SOCK_TCP, Sn_MR_TCP, 0, 0);
    break;

  case SOCK_INIT:
    connect(SOCK_TCP, server_ip, SERVER_PORT);
    break;

  case SOCK_ESTABLISHED:
    if (tcp_state != 2) { tcp_state = 2; printf("Connected!\r\n"); }
    /* 先接收,再按需发送 */
    break;

  case SOCK_CLOSE_WAIT:
    close(SOCK_TCP);
    tcp_state = 0;
    reconnect_delay = 3;  // 3秒后重连
    break;
}
HAL_Delay(100);  // 循环间隔100ms,及时处理数据

三、网络配置问题

10. PHY Link: DOWN

现象 :初始化打印 PHY Link: DOWN

原因

  • 网线未连接或未插紧
  • 网线插在电脑而非路由器(直连测试时正常,但无法访问外网)
  • W5500 复位时网线尚未连接

解决

  • 确保网线连接到路由器 LAN 口
  • 复位板子前先插好网线
  • 如需访问互联网,必须通过路由器
  • 在代码中添加PHY Link等待逻辑,避免链路未就绪时开始通信:
c 复制代码
uint8_t wait_cnt = 0;
while (wizphy_getphylink() != PHY_LINK_ON)
{
    HAL_Delay(500);
    if (++wait_cnt > 20) { printf("PHY timeout!\r\n"); break; }
}

现象

  • W5500 直连电脑一切正常(TCP通信、数据收发都OK)
  • 连接路由器后 PHY Link 显示 UP,但 DHCP获取不到IPTCP连接全部失败
  • connect() 返回 -4 (SOCKERR_SOCKCLOSED),socket状态在 0x13(INIT) 和 0x00(CLOSED) 之间跳变
  • 即使连接路由器本身的IP(如192.168.8.1:80)也失败

根本原因:W5500 PHY 自动协商(Auto-Negotiation)与路由器交换芯片不兼容,导致速率/双工模式不匹配。PHY Link虽然UP,但实际数据包全部丢失。

为什么直连电脑能工作:PC网卡兼容性强,能正确协商速率/双工;路由器交换芯片更挑剔。

诊断方法

  1. 检查 connect() 返回值:-4(SOCKERR_SOCKCLOSED) 表示连接被拒绝/socket异常关闭
  2. 尝试 DHCP:如果DHCP也超时,说明不是TCP问题而是底层通信问题
  3. 对比测试:直连电脑正常 + 连路由器异常 = 速率/双工不匹配

解决:强制PHY为10Mbps半双工(最基础、最兼容的模式):

c 复制代码
wiz_PhyConf phyconf;
phyconf.by = PHY_CONFBY_SW;       // 软件配置
phyconf.mode = PHY_MODE_MANUAL;   // 手动模式(非自动协商)
phyconf.speed = PHY_SPEED_10;     // 10Mbps
phyconf.duplex = PHY_DUPLEX_HALF; // 半双工
ctlwizchip(CW_SET_PHYCONF, (void *)&phyconf);

PHY配置枚举值

说明
PHY_CONFBY_HW 0 硬件引脚配置
PHY_CONFBY_SW 1 软件配置
PHY_MODE_MANUAL 0 手动指定速率/双工
PHY_MODE_AUTONEGO 1 自动协商
PHY_SPEED_10 0 10Mbps
PHY_SPEED_100 1 100Mbps
PHY_DUPLEX_HALF 0 半双工
PHY_DUPLEX_FULL 1 全双工

注意 :宏名是 PHY_CONFBY_SW(带Y),不是 PHY_CONFBG_SW(带G),容易拼错。

性能优化:10Mbps半双工可用但速率较低,可尝试以下组合(按兼容性从高到低):

  1. 10Mbps 半双工 --- 兼容性最好(推荐先用这个验证)
  2. 10Mbps 全双工 --- 部分路由器支持
  3. 100Mbps 半双工 --- 速率更快,多数路由器支持
  4. 100Mbps 全双工 --- 速率最快,但兼容性问题最多

12. connect() 返回 -4 (SOCKERR_SOCKCLOSED)

现象 :TCP connect() 返回 -4,socket状态在 SOCK_INIT(0x13) 和 SOCK_CLOSED(0x00) 之间跳变

connect() 返回值含义

返回值 说明
1 SOCK_OK 连接成功
0 SOCK_BUSY 非阻塞模式下连接中
-3 SOCKERR_SOCKINIT Socket未初始化
-4 SOCKERR_SOCKCLOSED Socket异常关闭(连接被拒/物理层不通)
-12 SOCKERR_IPINVALID IP地址无效
-13 SOCKERR_TIMEOUT 连接超时

排查步骤

  1. 检查 PHY Link 是否 UP
  2. 尝试 DHCP ------ 如果DHCP也超时,说明是物理层通信问题(见第11条)
  3. 尝试连接路由器自身IP(如192.168.8.1:80)------ 如果也失败,确认是底层通信问题
  4. 检查W5500的IP/网关/子网掩码是否与路由器网段匹配
  5. 强制PHY为10Mbps半双工测试

13. 连接远程服务器失败(本地测试成功)

现象:直连电脑可以通信,但连接互联网服务器时无法建立TCP连接

原因:W5500 的网络配置(IP/网关/子网掩码)必须与路由器在同一网段。网关必须设为路由器的 LAN IP。

检查步骤

  1. 在电脑上运行 ipconfig,查看默认网关(即路由器LAN IP)
  2. W5500 的 gw 必须与路由器 LAN IP 一致
  3. W5500 的 ip 必须在同一网段且不冲突

示例

路由器LAN IP W5500配置
192.168.1.1 ip=192.168.1.10, gw=192.168.1.1
192.168.8.1 ip=192.168.8.10, gw=192.168.8.1
192.168.0.1 ip=192.168.0.10, gw=192.168.0.1

推荐:使用DHCP自动获取IP,避免网段不匹配问题(见第14条)。


14. 使用DHCP自动获取IP

场景:避免手动配置IP/网关/子网掩码,自动适配不同路由器

实现

c 复制代码
#include "dhcp.h"

uint8_t dhcp_buf[1024];

/* 初始化DHCP(使用socket 1) */
DHCP_init(1, dhcp_buf);

/* DHCP获取IP循环 */
uint8_t dhcp_timeout = 15;
uint32_t last_tick = HAL_GetTick();
while (dhcp_timeout > 0)
{
    uint8_t ret = DHCP_run();
    if (HAL_GetTick() - last_tick >= 1000)
    {
        last_tick = HAL_GetTick();
        DHCP_time_handler();  // 每秒调用一次
        dhcp_timeout--;
    }
    if (ret == DHCP_IP_ASSIGN || ret == DHCP_IP_LEASED)
    {
        /* 获取到IP,应用到W5500 */
        getIPfromDHCP(gWIZNETINFO.ip);
        getGWfromDHCP(gWIZNETINFO.gw);
        getSNfromDHCP(gWIZNETINFO.sn);
        getDNSfromDHCP(gWIZNETINFO.dns);
        gWIZNETINFO.dhcp = NETINFO_DHCP;
        ctlnetwork(CN_SET_NETINFO, (void *)&gWIZNETINFO);
        break;
    }
    if (ret == DHCP_FAILED) break;
}

主循环中维护DHCP租约(每秒调用一次):

c 复制代码
static uint32_t dhcp_last = 0;
if (HAL_GetTick() - dhcp_last >= 1000)
{
    dhcp_last = HAL_GetTick();
    DHCP_time_handler();
    DHCP_run();
}

注意:DHCP需要占用一个socket(如socket 1),TCP客户端使用另一个socket(如socket 0)。


15. 直连电脑测试方法

场景:无路由器,W5500网线直插电脑网口,用于本地调试

电脑配置(手动设置IP):

项目
IP地址 192.168.8.100(与W5500同网段)
子网掩码 255.255.255.0
默认网关 可留空

网络调试助手配置

项目
协议类型 TCP Server
本机IP 192.168.8.100
监听端口 1111

W5500 作为 TCP Client 主动连接电脑的 TCP Server。

注意:直连电脑测试时PHY自动协商通常能正常工作,但这不代表连路由器也没问题。直连电脑成功但连路由器失败时,请检查第11条(PHY自动协商不匹配)。


四、Keil 项目配置

16. 添加源文件和头文件路径

现象:编译报错找不到头文件或链接时找不到符号

解决

  1. 头文件路径:Options → C/C++ → Include Paths 中添加:

    复制代码
    ../Ethernet;../Ethernet/W5500;../Internet/DHCP
  2. 源文件分组:在 Project 窗口添加:

    • Ethernet/W5500 组:w5500.c, wizchip_conf.c, socket.c
    • Internet/DHCP 组:dhcp.c
    • Core/Src 组:w5500_spi.c
  3. 启用 MicroLib:Options → Target → 勾选 Use MicroLib


五、最终工作配置

17. 推荐的网络初始化流程

经过实际调试验证的完整初始化流程:

复制代码
1. HAL_Init / SystemClock_Config / MX_GPIO_Init / MX_SPI1_Init / MX_USART1_UART_Init
2. W5500_SPI_Init() --- RST引脚初始化 + 硬件复位(RST低10ms → RST高 → 等50ms)
3. 注册SPI回调函数(CrisEnter/Exit, CS Select/Deselect, ReadByte/WriteByte, ReadBurst/WriteBurst)
4. wizchip_init(txsize, rxsize) --- 初始化Socket缓冲区(2KB×8)
5. ctlnetwork(CN_SET_NETINFO) --- 设置初始网络信息(MAC等)
6. 强制PHY为10Mbps半双工 ← 关键步骤
7. 等待PHY Link UP
8. DHCP获取IP ← 推荐
9. 应用DHCP获取的网络配置
10. 主循环:DHCP租约维护 + TCP客户端

18. 最终网络配置参数

项目
PHY模式 10Mbps 半双工(手动配置,解决自动协商不匹配)
IP获取方式 DHCP(自动从路由器获取IP)
Socket 0 TCP客户端(连接远程服务器)
Socket 1 DHCP(租约维护)
MAC地址 00:08:DC:00:00:01

19. TCP客户端状态机

复制代码
SOCK_CLOSED → socket() → SOCK_INIT → connect() → SOCK_ESTABLISHED
                                                           ↓
                                                    (数据收发 / 5秒心跳)
                                                           ↓
                                               SOCK_CLOSE_WAIT → close()
                                                           ↓
                                                    (延迟3秒) → SOCK_CLOSED

状态处理要点

  • SOCK_CLOSED:检查 reconnect_delay,倒计时结束后重新打开socket
  • SOCK_INIT:只调用一次 connect()(用 tcp_state 标志防止重复调用)
  • SOCK_ESTABLISHED:先检查接收数据,再按5秒间隔发送心跳
  • SOCK_CLOSE_WAIT:调用 close() 关闭socket,设置重连延迟

六、项目文件清单

文件 说明
Core/Inc/w5500_spi.h SPI桥接层头文件
Core/Src/w5500_spi.c SPI读写、CS/RST控制、临界区保护
Ethernet/W5500/w5500.h W5500寄存器定义
Ethernet/W5500/w5500.c W5500 HAL(SPI帧读写)
Ethernet/wizchip_conf.h WIZCHIP配置头文件
Ethernet/wizchip_conf.c WIZCHIP配置实现(回调注册、PHY控制)
Ethernet/socket.h Socket API头文件
Ethernet/socket.c Socket API实现(TCP/UDP操作)
Internet/DHCP/dhcp.h DHCP头文件
Internet/DHCP/dhcp.c DHCP实现

SPI桥接层提供的回调函数

函数 用途
SPI_CrisEnter / SPI_CrisExit 临界区进入/退出(禁用/启用中断)
SPI_CS_Select / SPI_CS_Deselect W5500 CS片选控制
SPI_ReadByte / SPI_WriteByte SPI单字节读写
SPI_ReadBurst / SPI_WriteBurst SPI批量读写

引脚分配

引脚 功能
PA5 (SPI1_SCK) SPI时钟
PA6 (SPI1_MISO) SPI数据输入
PA7 (SPI1_MOSI) SPI数据输出
PA4 (SPI_CS) W5500片选
PB0 (W5500_RST) W5500硬件复位
PB1 (W5500_INT) W5500中断输出(可选,中断方式时使用)

七、中断方式 vs 轮询方式

20. 中断方式驱动W5500

W5500 支持通过 INTn 引脚(低电平有效)通知STM32有事件发生。

硬件连接:W5500 INTn → STM32 PB1(EXTI1,下降沿触发)

中断配置

c 复制代码
/* gpio.c 中添加 */
GPIO_InitStruct.Pin = W5500_INT_Pin;        // PB1
GPIO_InitStruct.Mode = GPIO_MODE_IT_FALLING; // 下降沿中断
GPIO_InitStruct.Pull = GPIO_PULLUP;          // 上拉
HAL_GPIO_Init(W5500_INT_GPIO_Port, &GPIO_InitStruct);
HAL_NVIC_SetPriority(EXTI1_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(EXTI1_IRQn);

中断掩码设置

c 复制代码
/* 启用Socket 0的 CON/DISCON/RECV/TIMEOUT 中断 */
setSn_IMR(SOCK_TCP, Sn_IR_CON | Sn_IR_DISCON | Sn_IR_RECV | Sn_IR_TIMEOUT);
/* 启用Socket 0中断输出到INTn引脚 */
setSIMR(0x01);

EXTI中断服务函数

c 复制代码
void EXTI1_IRQHandler(void) {
    HAL_GPIO_EXTI_IRQHandler(W5500_INT_Pin);
}

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
    if (GPIO_Pin == W5500_INT_Pin)
        w5500_int_flag = 1;  // 设置标志,主循环中处理
}

主循环处理中断事件

c 复制代码
if (w5500_int_flag) {
    w5500_int_flag = 0;
    uint8_t ir = getSn_IR(SOCK_TCP);
    if (ir & Sn_IR_CON)     { setSn_IR(SOCK_TCP, Sn_IR_CON);     /* 连接建立 */ }
    if (ir & Sn_IR_DISCON)  { setSn_IR(SOCK_TCP, Sn_IR_DISCON);  /* 断开 */ }
    if (ir & Sn_IR_RECV)    { setSn_IR(SOCK_TCP, Sn_IR_RECV);    /* 数据到达 */ }
    if (ir & Sn_IR_TIMEOUT) { setSn_IR(SOCK_TCP, Sn_IR_TIMEOUT); /* 超时 */ }
}

对比

轮询方式 中断方式
数据接收 每100ms查询 getSn_RX_RSR() RECV中断触发,近实时
连接/断开 每100ms查询 getSn_SR() CON/DISCON中断触发
CPU占用 持续轮询 无事件时CPU空闲
响应延迟 最大100ms 近实时
实现复杂度 简单 需配置EXTI + 中断掩码
额外硬件 不需要 需要INTn引脚连接
相关推荐
qyr67892 小时前
全球新能源汽车电机驱动器市场深度调研报告
大数据·网络·人工智能·自动化·汽车
Hiyajo pray3 小时前
SDN 访问控制性能调优方法
服务器·网络·网络安全
初晴融雪-快雪时晴3 小时前
网络硬件全景梳理:从光纤到手机,一张网是如何连接的?
网络·智能手机·智能路由器
huainingning4 小时前
交换机通过ftp下载文件或者传输文件到ftp服务器
运维·服务器·网络
花生了什么事o6 小时前
DNS:把域名翻译成 IP 的层级查询机制
网络·网络协议·tcp/ip
海域云-罗鹏7 小时前
多云连接策略实战指南:企业如何打破云孤岛,构建跨公有云、私有云与混合云的高效网络
网络
其实防守也摸鱼7 小时前
渗透--损坏的对象级别鉴权漏洞(Broken Object Level Authorization, BOLA)
大数据·网络·数据库·学习·tcp/ip·安全·安全威胁分析
hehelm7 小时前
IO 多路复用 — Reactor
linux·服务器·网络·数据库·c++
线束线缆组件品替网7 小时前
Amphenol ICC RJE1Y22610C42401线束组件应用解析
网络·数码相机·智能手机·计算机外设·电脑·电视盒子