本文档基于 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) ─────────│
为什么自动协商会失败:
-
FLP脉冲不兼容:W5500发送的Fast Link Pulse脉冲格式与某些路由器交换芯片不完全兼容,导致双方对协商结果理解不一致
-
并行检测问题 :部分路由器在自动协商失败后回退到并行检测,只能检测到载波信号但无法确定双工模式,默认使用半双工。如果W5500协商为全双工,就会产生双工不匹配
-
双工不匹配的危害:
全双工设备 (W5500) 半双工设备 (路由器)
│ │
│──── 发送数据 ──────────────▶│
│──── 发送数据 ──────────────▶│ ← 路由器在发送时检测到冲突
│ │ 但全双工端不监听冲突
│ │ → 数据丢失,重传风暴
│◀─── 数据被丢弃 ────────────│ -
为什么直连电脑能工作:PC网卡PHY芯片兼容性强,能正确解析W5500的协商脉冲;路由器交换芯片更廉价、兼容性差
-
强制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。
解决:
- Keil 项目 Options → Target → 勾选 Use MicroLib
- 在
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;
}
- 在
usart.h中添加#include <stdio.h>
7. W5500 SPI 通信失败 / 读出全0或全FF
现象:W5500 初始化后读寄存器返回全0或全0xFF
原因(常见排查顺序):
-
CS引脚初始状态错误:CS应为 HIGH(未选中),初始化为 LOW(选中)会导致 SPI 总线冲突
- 解决:
HAL_GPIO_WritePin(SPI_CS_Port, SPI_CS_Pin, GPIO_PIN_SET);
- 解决:
-
复位延时不够:W5500 硬件复位后需要约50ms稳定时间才能进行 SPI 访问
- 解决:
cHAL_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 -
SPI模式错误:W5500 只支持 SPI Mode 0 (CPOL=0, CPHA=0) 或 Mode 3 (CPOL=1, CPHA=1)
- 解决:CubeMX 中 SPI 设置为
CPOL=Low, CPHA=1Edge
- 解决:CubeMX 中 SPI 设置为
-
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
原因及解决:
-
每秒都调用 send():ESTABLISHED 状态每次循环都发数据,服务端可能不接受高频无意义数据
- 解决:仅在首次连接或需要时发送,心跳间隔设为5秒以上
-
缺少状态跟踪:每次循环都重新打印连接信息
- 解决:用
tcp_state变量跟踪连接状态,避免重复操作
- 解决:用
-
断线后立即重连:服务端关闭后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; }
}
11. PHY Link UP 但无法通信(自动协商不匹配)⭐ 最关键问题
现象:
- W5500 直连电脑一切正常(TCP通信、数据收发都OK)
- 连接路由器后 PHY Link 显示 UP,但 DHCP获取不到IP 、TCP连接全部失败
connect()返回-4(SOCKERR_SOCKCLOSED),socket状态在0x13(INIT) 和0x00(CLOSED) 之间跳变- 即使连接路由器本身的IP(如192.168.8.1:80)也失败
根本原因:W5500 PHY 自动协商(Auto-Negotiation)与路由器交换芯片不兼容,导致速率/双工模式不匹配。PHY Link虽然UP,但实际数据包全部丢失。
为什么直连电脑能工作:PC网卡兼容性强,能正确协商速率/双工;路由器交换芯片更挑剔。
诊断方法:
- 检查
connect()返回值:-4(SOCKERR_SOCKCLOSED) 表示连接被拒绝/socket异常关闭 - 尝试 DHCP:如果DHCP也超时,说明不是TCP问题而是底层通信问题
- 对比测试:直连电脑正常 + 连路由器异常 = 速率/双工不匹配
解决:强制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半双工可用但速率较低,可尝试以下组合(按兼容性从高到低):
- 10Mbps 半双工 --- 兼容性最好(推荐先用这个验证)
- 10Mbps 全双工 --- 部分路由器支持
- 100Mbps 半双工 --- 速率更快,多数路由器支持
- 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 | 连接超时 |
排查步骤:
- 检查 PHY Link 是否 UP
- 尝试 DHCP ------ 如果DHCP也超时,说明是物理层通信问题(见第11条)
- 尝试连接路由器自身IP(如192.168.8.1:80)------ 如果也失败,确认是底层通信问题
- 检查W5500的IP/网关/子网掩码是否与路由器网段匹配
- 强制PHY为10Mbps半双工测试
13. 连接远程服务器失败(本地测试成功)
现象:直连电脑可以通信,但连接互联网服务器时无法建立TCP连接
原因:W5500 的网络配置(IP/网关/子网掩码)必须与路由器在同一网段。网关必须设为路由器的 LAN IP。
检查步骤:
- 在电脑上运行
ipconfig,查看默认网关(即路由器LAN IP) - W5500 的
gw必须与路由器 LAN IP 一致 - 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. 添加源文件和头文件路径
现象:编译报错找不到头文件或链接时找不到符号
解决:
-
头文件路径:Options → C/C++ → Include Paths 中添加:
../Ethernet;../Ethernet/W5500;../Internet/DHCP -
源文件分组:在 Project 窗口添加:
Ethernet/W5500组:w5500.c,wizchip_conf.c,socket.cInternet/DHCP组:dhcp.cCore/Src组:w5500_spi.c
-
启用 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,倒计时结束后重新打开socketSOCK_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引脚连接 |