
简介面向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对应引脚说明SCSPA4SPI1片选软件控制SCLKPA5SPI1时钟MOSIPA7SPI1主机输出MISOPA6SPI1主机输入RSTPA3复位控制低电平有效INTPA2中断输出低电平有效3.3V3.3V电源GNDGND地注意W5500工作电压是3.3VMINISTM32的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回环测试、tcpsTCP服务器、tcpcTCP客户端、udpsUDP服务器这些现成例子。这层代码可以直接参考也可以照葫芦画瓢改写成自己的应用逻辑。理解了这三层移植思路就很清楚了协议层和应用层基本不用动核心工作量在对接第一层硬件抽象层和单片机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位数据、时钟极性CPOL0、时钟相位CPHA1。这个SPI模式必须和W5500的要求匹配W5500手册上写的很清楚SPI Mode 0和Mode 3都支持但我习惯用Mode 3也就是CPOL1、CPHA1实测稳定。如果你用的是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都配成2KB8个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_2EdgeMode 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.hInternet/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还有掉电模式、链路检测这些功能这次的移植只覆盖了最核心的通信链路等项目跑通了之后可以再慢慢把这些功能加上去让整个以太网通信模块变得完整可控。本文还有配套的精品资源点击获取