简介:面向STM32F103平台的MINISTM32 W5500 ioLibrary移植工程资料,为嵌入式开发者提供了一套可复用的以太网开发基础与移植范例。资源包共217个文件,约9.17MB,以66个.c源码与66个.h头文件为核心,覆盖SPI驱动接入和socket API封装,同时包含编译中间文件(.d/.o/.crf)、Keil工程配置(.uvprojx/.uvoptx)、调试二进制(.hex/.axf)、帮助文档(.chm)及常用脚本(keilkilll.bat)等,适合直接作为参考工程学习、编译或烧录验证。内容涉及STM32F103RCT6的GPIO与SPI初始化、ioLibrary底层适配、socket网络编程示例,也附带了标准外设库中的定时器、ADC、USART等驱动文件,便于读者结合实际代码理解W5500驱动与STM32标准库之间的时序配合,从而更快完成移植。已有1000人学习下载。相比零散代码片段,这套完整工程目录结构直观、配置齐全,适合希望绕开底层TCP/IP细节、专注应用层开发的工程师作为移植蓝本,也对刚接触网络嵌入式开发的初学者十分友好,可帮助遇到W5500时序或引脚配置问题的读者排查问题。 在嵌入式开发里,给MINISTM32(也就是正点原子那款经典的STM32F103RCT6迷你板)加上以太网通信能力,最顺手的方案就是接一颗W5500硬协议栈芯片。网上相关的例程和资料非常多,但大部分都是基于官方评估板或者特定开发板的移植代码,真正要把它挪到自己的MINISTM32工程里时,往往会在工程结构、底层接口对接上卡壳。这篇就把我实际把W5500 ioLibrary移植进MINISTM32标准库工程的完整过程和踩坑记录整理出来,给后面要动这块的朋友做个参考。
ioLibrary是WIZnet官方提供的驱动库,里面包含了W5500的寄存器读写、Socket API、以及TCP/IP应用层接口,相当于厂家已经把协议栈的脏活累活都干完了,我们要做的核心工作就两件事:第一,把库文件正确加进MDK工程;第二,把底层SPI读写、复位、片选这些硬件相关的函数实现好,让库能跑在STM32F103上。整个过程不需要去啃TCP/IP协议细节,但需要对SPI通信、中断机制、以及标准外设库的工程组织方式有一定了解。这篇适合正在调W5500驱动、或者想把网络功能加进现有STM32项目但还没理清移植思路的开发者,尤其是第一次接触ioLibrary的朋友。
1. 移植前的准备:硬件连接与软件环境
1.1 硬件连接与W5500模块选型
MINISTM32开发板上没有板载以太网接口,所以需要外接一个W5500以太网模块。市面上常见的W5500模块大多是SPI接口,引脚定义基本统一:SCS(片选)、SCLK(时钟)、MOSI、MISO、RST(复位)、INT(中断),部分模块还会引出PDN(掉电模式控制脚)。
我的连接方式是这样的:
| W5500模块引脚 | MINISTM32对应引脚 | 说明 |
|---|---|---|
| SCS | PA4 | SPI1片选,软件控制 |
| SCLK | PA5 | SPI1时钟 |
| MOSI | PA7 | SPI1主机输出 |
| MISO | PA6 | SPI1主机输入 |
| RST | PA3 | 复位控制,低电平有效 |
| INT | PA2 | 中断输出,低电平有效 |
| 3.3V | 3.3V | 电源 |
| GND | GND | 地 |
注意:W5500工作电压是3.3V,MINISTM32的IO口也是3.3V电平,可以直接连接。但如果你用的是5V供电的STM32开发板,就需要加电平转换。
选模块的时候尽量选带网络变压器的版本,就是RJ45网口和变压器集成在一块的那种。既能省去外部变压器的麻烦,而且信号完整性也有保障。别为了省那几块钱选个裸芯片自己搭外围电路,高速信号布线没处理好容易出现丢包、连不上的怪问题。
1.2 软件环境准备与工程模板确认
软件方面我用的还是经典组合:Keil MDK5 + STM32F10x标准外设库V3.5。MINISTM32的官方例程本身就是基于标准库写的,工程结构清晰,而且网上绝大多数参考资料也都是这个组合,遇到问题方便对照排查。
开始移植之前,建议先把官方例程里的LED流水灯程序跑一遍,确认开发板、调试器、工程环境都正常。这一步不是多余的,因为后续如果网络功能调不通,需要确保基础的外设操作环境没有问题,排除时钟配置或调试器设置方面的干扰。
工程模板建议用正点原子自带的标准库工程,它已经配置好了系统时钟(72MHz)、延时函数、串口打印这些基础功能。串口打印在调试W5500的时候非常有用,可以看到初始化状态、Socket连接状态、收发数据内容。我习惯用串口1作为调试输出,波特率115200,这样几乎所有串口助手都能正常收。
ioLibrary的源码从WIZnet的GitHub仓库获取,搜W5500 ioLibrary就能找到。注意看分支和版本,master分支通常维护得比较勤,拿这个分支就行。压缩包下载下来后,里面的目录结构大致是这样的:
ioLibrary ├── Application │ ├── loopback │ ├── multicast │ └── ... ├── Ethernet │ ├── W5500 │ │ ├── w5500.c │ │ ├── w5500.h │ │ └── ... │ └── ... └── Internet ├── DHCP ├── DNS ├── ... └── TCPIP ├── socket.c ├── socket.h └── ...Ethernet/W5500目录下是芯片底层的寄存器读写驱动,Internet/TCPIP目录下是Socket API层,Internet/DHCP、Internet/DNS这些是应用层协议。我们最基础的移植只需要Ethernet/W5500和Internet/TCPIP这两块,其他的按需添加。
2. ioLibrary结构分析与移植思路
2.1 官方库目录结构和文件角色梳理
拿到压缩包先别急着往工程里塞文件,先花半小时把结构搞清楚。ioLibrary的逻辑分层其实很清晰,有层次感:
第一层是硬件抽象层,对应Ethernet/W5500/w5500.c和w5500.h。这一层的函数直接操作W5500的寄存器,比如写寄存器、读寄存器、写Socket缓冲区、读Socket缓冲区。它不关心数据是什么意思,只负责把数据通过SPI接口搬到芯片内部。看代码你会发现,w5500.c里大量调用了WIZCHIP_READ、WIZCHIP_WRITE这些宏,这些宏最终指向reg_wizchip_cs_select、reg_wizchip_cs_deselect、reg_wizchip_spi_readbyte、reg_wizchip_spi_writebyte这些由用户实现的功能函数。
第二层是协议层,对应Internet/TCPIP/socket.c和socket.h。这一层基于底层的寄存器读写,实现了socket()、connect()、listen()、send()、recv()这些和标准Berkeley Socket风格一致的API。这一层封装了TCP三次握手、数据包解析、重传机制这些细节,调用者只需要像用普通Socket那样调函数就行。
第三层是应用层,对应Application目录下的示例代码。里面是loopback(回环测试)、tcps(TCP服务器)、tcpc(TCP客户端)、udps(UDP服务器)这些现成例子。这层代码可以直接参考,也可以照葫芦画瓢改写成自己的应用逻辑。
理解了这三层,移植思路就很清楚了:协议层和应用层基本不用动,核心工作量在对接第一层硬件抽象层和单片机SPI外设。
2.2 移植的核心工作:驱动函数的对接思路
W5500官方库在wizchip_conf.h里给了非常明确的用户接口注册机制。你不用去改w5500.c的源码,只需要在工程里写好几个回调函数,然后在初始化时通过reg_wizchip_cs_cbfunc、reg_wizchip_spi_cbfunc这些注册函数把回调地址告诉库就行。
需要实现的底层函数就这么几个:
void wizchip_select(void):CS引脚拉低,选中W5500void wizchip_deselect(void):CS引脚拉高,释放W5500uint8_t wizchip_read(void):从SPI读取一个字节void wizchip_write(uint8_t wb):通过SPI写入一个字节void wizchip_reset(void):复位W5500
可能有朋友会问:SPI的读写速度怎么选?这里有个实际经验:STM32F103的SPI1最高可以跑到18MHz,但W5500官方的SPI时钟建议不要超过33.3MHz。考虑到F103性能以及线材质量,我一般把SPI波特率预分频设为8,也就是9MHz,稳定性和速度都比较均衡。如果板子布线质量一般或者杜邦线比较长,降到4.5MHz会更稳。
另外要注意的是W5500一次SPI事务的时序:W5500 SPI帧由“地址段+控制段+数据段”三部分组成。具体来说,发送帧的时候,先发8位地址字节,高5位是寄存器地址;再发8位控制字节,其中最高位BSB是区块选择位,用来区分是通用寄存器区还是Socket寄存器区;然后是连续的数据字节。这些细节w5500.c里已经处理好了,我们只需要保证SPI的读写时序正确就行。
3. 移植实战:把ioLibrary接入标准库工程
3.1 工程文件分类与目录整理
这一步看着琐碎,但如果文件放得乱七八糟,后面调试的时候找问题会非常痛苦。我的做法是在MDK工程里新建几个分组(Group),按功能模块划分,别把所有文件都堆在一个组里:
W5500_Driver:放w5500.c、socket.c,以及我写的w5500_stm32_drv.c(底层对接文件)App:放应用逻辑代码,比如tcp_server_app.c、main.c相关的应用层部分User:保持原本标准库工程自带的main.c、stm32f10x_it.c等
在User组里需要添加stm32f10x_it.c中关于SPI中断的定义——不过我们这种用轮询方式读写SPI的移植方式,其实用不到SPI中断,所以中断部分可以先不管。如果你后面要跑大量数据、追求高吞吐,再考虑用DMA+中断的方案来优化。
物理文件目录我建议统一放在工程的HARDWARE/W5500下,把.c和.h文件都放在这个目录里,然后在MDK的Include Paths里添加对应路径。注意w5500.h中会#include "wizchip_conf.h",这个头文件在Ethernet/W5500目录下,也必须加入头文件搜索路径,否则编译直接报错找不到头文件。
3.2 底层SPI接口与回调函数实现
SPI配置属于比较常规的操作。我用的是SPI1,模式配置为:主机模式、8位数据、时钟极性CPOL=0、时钟相位CPHA=1。这个SPI模式必须和W5500的要求匹配,W5500手册上写的很清楚,SPI Mode 0和Mode 3都支持,但我习惯用Mode 3,也就是CPOL=1、CPHA=1,实测稳定。如果你用的是SPI Mode 0,只要收发双方一致也没问题。
底层初始化的代码骨架如下:
void W5500_SPI_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_SPI1, ENABLE); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = GPIO_Pin_2 | GPIO_Pin_3 | GPIO_Pin_4; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 初始状态:CS高、RST高 GPIO_SetBits(GPIOA, GPIO_Pin_3 | GPIO_Pin_4); SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; SPI_InitStructure.SPI_Mode = SPI_Mode_Master; SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8; SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; SPI_Init(SPI1, &SPI_InitStructure); SPI_Cmd(SPI1, ENABLE); }这里有个细节:MOSI、SCLK、MISO这三个引脚必须配置为复用推挽(GPIO_Mode_AF_PP),不能配成普通推挽输出。有的朋友例子抄过来发现SPI一直读不到数据,最后排查半天发现是引脚模式没配对。CS、RST这两个是软件控制的输出引脚,配成普通推挽输出就行。
SPI的读写字节函数也要自己封装一下。标准库原生API用起来有个小陷阱,SPI_I2S_SendData只是往数据寄存器丢数据,并不能保证数据发完了。最稳妥的做法是用SPI_I2S_GetFlagStatus去轮询SPI_I2S_FLAG_TXE和SPI_I2S_FLAG_RXNE标志位:
uint8_t SPI_ReadWriteByte(uint8_t byte) { while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET); SPI_I2S_SendData(SPI1, byte); while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); return SPI_I2S_ReceiveData(SPI1); }因为SPI是全双工的,读一个字节其实需要同时写一个字节,主从设备通过时钟交换数据。所以不管你要读还是写,本质都是调用这个收发函数来完成。W5500官方库在实现寄存器读写的时,会在一次SPI帧内先发送地址和控制字节,然后视情况继续发送数据字节(写操作)或接收数据字节(读操作)。
3.3 回调函数注册与wizchip初始化配置
底层SPI搞定了,接下来就把回调函数注册给W5500库。在main初始化流程中,按照下面的顺序执行:
#include "wizchip_conf.h" #include "w5500.h" #include "socket.h" // CS/RST控制 void wizchip_select(void) { GPIO_ResetBits(GPIOA, GPIO_Pin_4); } void wizchip_deselect(void) { GPIO_SetBits(GPIOA, GPIO_Pin_4); } void wizchip_reset(void) { GPIO_SetBits(GPIOA, GPIO_Pin_3); delay_ms(10); GPIO_ResetBits(GPIOA, GPIO_Pin_3); delay_ms(10); GPIO_SetBits(GPIOA, GPIO_Pin_3); } // SPI单字节读写 uint8_t wizchip_read(void) { return SPI_ReadWriteByte(0xFF); } void wizchip_write(uint8_t wb) { SPI_ReadWriteByte(wb); } void W5500_Init(void) { uint8_t tmp; uint8_t memsize[2][8] = {{2,2,2,2,2,2,2,2},{2,2,2,2,2,2,2,2}}; W5500_SPI_Init(); wizchip_reset(); reg_wizchip_cs_cbfunc(wizchip_select, wizchip_deselect); reg_wizchip_spi_cbfunc(wizchip_read, wizchip_write); reg_wizchip_reset_cbfunc(wizchip_reset); if (ctlwizchip(CW_INIT_WIZCHIP, (void*)memsize) == -1) { printf("W5500 init failed!\r\n"); return; } // 读取版本寄存器确认通信正常 tmp = getVR(); printf("W5500 version reg: 0x%02X\r\n", tmp); // 0x04表示W5500,如果是0x00或者0xFF说明SPI通信有问题 }重点讲一下memsize这个数组。它是配置W5500内部TX/RX缓冲区的分配。W5500内部有32KB收发缓冲区,memsize[0]是8个Socket的TX缓冲区大小,memsize[1]是8个Socket的RX缓冲区大小,单位为KB。这里我把每个Socket的TX和RX都配成2KB,8个Socket加起来16KB TX和16KB RX,刚好32KB。如果你只用1个Socket,也可以把memsize[0][0]配成8KB甚至16KB,减少缓冲区分配导致的内存浪费。
getVR()返回W5500版本寄存器的值,正常情况下是0x04。这个函数在调试时特别有用,只要打印出来是0x04,说明SPI通信链路完全正常,硬件连接无误。如果打印出0xFF,大概率是MISO线没接好或者SPI模式不对;如果打印出0x00,一般是CS引脚控制有问题,或者W5500芯片没正常工作(复位不彻底、供电异常等)。
3.4 网络参数配置:MAC、IP、网关、掩码
初始化完芯片底层,接下来要配置网络层参数。W5500库提供了两个关键函数:setSHAR(设置MAC地址)和setSIPR(设置IP地址)。注意MAC地址要保证局域网内唯一,随便填可能会造成IP冲突,在实际应用场景里需要从芯片唯一ID里生成MAC。
uint8_t mac[6] = {0x00, 0x08, 0xDC, 0x12, 0x34, 0x56}; uint8_t ip[4] = {192, 168, 1, 200}; uint8_t gw[4] = {192, 168, 1, 1}; uint8_t sub[4] = {255, 255, 255, 0}; setSHAR(mac); setSIPR(ip); setGAR(gw); setSUBR(sub);提示:MINISTM32没有EEPROM,这些配置掉电就没,所以每次上电都要重新设置。如果项目有保存配置的需求,可以考虑把网络参数存到STM32内部Flash的末尾扇区,或者外挂一颗AT24C02。
4. 跑通第一个TCP服务器通信
4.1 Socket API使用流程与代码示例
设备要能被上位机或手机App连上,最基础的场景就是TCP服务器。W5500库的Socket API风格和Linux下几乎一模一样,熟悉网络编程的人上手很快。
下面是一个完整的思路:初始化网络参数后,开启一个TCP服务器,监听端口5000,然后阻塞等待客户端连接。连上之后将收到的数据原样返回。
#define TCP_SERVER_PORT 5000 int8_t tcp_server_loop(void) { int8_t sock = socket(0, Sn_MR_TCP, TCP_SERVER_PORT, 0); if (sock < 0) { printf("socket create failed: %d\r\n", sock); return -1; } if (listen(sock) == SOCK_OK) { printf("TCP server listening on port %d...\r\n", TCP_SERVER_PORT); } else { printf("listen failed\r\n"); close(sock); return -1; } while (1) { int32_t ret = getSn_SR(sock); if (ret == SOCK_ESTABLISHED) { // 客户端已连接 uint32_t len = getSn_RX_RSR(sock); if (len > 0) { uint8_t buf[512]; int32_t rlen = recv(sock, buf, len > 512 ? 512 : len); if (rlen > 0) { printf("Recv %ld bytes: %s\r\n", rlen, buf); send(sock, buf, rlen); } } } else if (ret == SOCK_CLOSE_WAIT || ret == SOCK_CLOSED) { printf("Client disconnected, closing socket...\r\n"); close(sock); // 重新建立监听 sock = socket(0, Sn_MR_TCP, TCP_SERVER_PORT, 0); if (sock < 0) return -1; listen(sock); } delay_ms(10); } }4.2 状态机跳转与收发缓冲处理
注意SOCK_CLOSED和SOCK_CLOSE_WAIT这两个状态的判断不能省略。连接断开后,如果不重新调用socket()和listen(),后续客户端就再也连不上了。这是新手最容易栽的坑:第一次连接成功后,断开再连就连不上,代码逻辑也没报错,就是因为没有处理断开重连。
收发数据这里有个优化空间:getSn_RX_RSR(sock)返回的是当前Socket接收缓冲区中待读取的字节数,为了不阻塞主循环太长时间,我限制每次最多读512字节。如果你要在W5500上跑大文件传输,建议改用DMA+中断的方式,一次从Socket缓冲区搬走一整个包,主循环只负责判断状态。
还有一个容易忽略的点:W5500的Socket缓冲区是环形缓冲区,recv函数内部会处理绕回情况。但当你要读取的数据跨过缓冲区末尾时,官方驱动的recv函数会自动分两次从SPI读取,这个逻辑在socket.c里已经实现了。我们不需要额外处理,但是要知道这个机制,因为如果你自己写底层的recvfrom,就必须考虑缓冲区绕回的问题。
5. 常见问题与调试经验实录
5.1 SPI通信异常:版本寄存器读取不到0x04
这个是最常见的问题,没有之一。现象就是getVR()打印出来是0xFF或0x00。
排查顺序建议按下面这个来:
- 量电压:先确认W5500模块的3.3V供电正常。有的模块直接从STM32开发板的3.3V取电,如果板子上外设太多导致压降,W5500可能工作不稳定。
- 检查SPI引脚复用配置:
MOSI、MISO、SCLK是不是都配成了GPIO_Mode_AF_PP。这是最容易出错的地方。 - 确认时钟极性和相位:Mode 3对应
SPI_CPOL_High, SPI_CPHA_2Edge,Mode 0对应SPI_CPOL_Low, SPI_CPHA_1Edge。协议要匹配。 - 用逻辑分析仪看时序:如果有逻辑分析仪,抓一下CS、SCLK、MOSI、MISO这几根线的波形。一眼就能看出问题出在哪。如果没有,可以加长CS低电平后的延时,看看是不是时序太紧凑了。
5.2 连接不稳定、频繁掉线
如果TCP连接建立了但过一会儿就断,或者数据收发时不时出错,大概率是SPI时序余量不够。可以按下面的思路解决:
降低SPI时钟频率。F103的SPI1从18MHz降到9MHz、4.5MHz试试。W5500跑在9MHz非常稳,除非你用的是那种很差的长杜邦线。
检查电源去耦。W5500模块上一般有去耦电容,但如果你是从别的板上飞线供电,建议在模块的电源引脚旁边就近并一个10uF电容和一个0.1uF电容,效果很明显。
看中断和片选波形。如果INT引脚没有拉低到W5500库所期望的状态,也可能导致Socket状态判断异常。调试时可以把INT引脚配置成输入模式,读取电平打印出来看看。
5.3 编译报错:重复定义和头文件找不到
ioLibrary的wizchip_conf.h里有些宏定义和STM32标准库宏名冲突,最典型的是MAX_SOCK_NUM之类的宏。如果编译报重复定义错误,优先检查是不是工程里其他文件也定义了同名宏,或者把wizchip_conf.h里冲突的宏做条件编译保护。
头文件找不到的问题好解决,在MDK的Options for Target->C/C++->Include Paths里把以下路径全部加进去:
W5500驱动目录(存放w5500.c、w5500.h、wizchip_conf.h)Internet/TCPIP目录(存放socket.c、socket.h)- 你自己写的驱动对接文件所在目录
5.4 一个隐藏的坑:SPI字节顺序
W5500是高位先行(MSB first),你用的固件库SPI_InitStructure.SPI_FirstBit一定要设置成SPI_FirstBit_MSB。默认也是这个,但如果之前某个地方改成了LSB忘了改回来,就会出现寄存器读到的数据错位、版本寄存器返回乱值的情况。这个坑排查起来比较隐蔽,因为SPI能正常“通信”,数据却完全不对。
最后分享一点实际的体会
W5500这个芯片加上MINISTM32的组合,可以说是STM32入门以太网开发的一条“捷径”。ioLibrary移植本身并不难,难的是理解分层结构、搞清底层对接点在哪。很多人一上来就埋头改代码,反而被一堆陌生的函数名搞得一头雾水。我的建议是:先花半小时把w5500.c和socket.c的函数列表过一遍,搞清楚哪些函数是给上层调用的,哪些函数是需要你实现的回调,然后再动手。另外,做这类嵌入式网络开发,一定要有逻辑分析仪或者至少示波器,SPI时序这种问题光靠猜是猜不出来的。W5500还有掉电模式、链路检测这些功能,这次的移植只覆盖了最核心的通信链路,等项目跑通了之后,可以再慢慢把这些功能加上去,让整个以太网通信模块变得完整可控。
本文还有配套的精品资源,点击获取