ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

W5500硬协议栈芯片移植STM32实战:从SPI驱动到TCP/UDP通信

W5500硬协议栈芯片移植STM32实战:从SPI驱动到TCP/UDP通信 搞嵌入式的人应该都有这种经历项目里突然要加以太网功能板上资源又有限不想上Linux也不想折腾lwIP。我第一次接到这个需求时第一反应是又要开始调协议栈了后来发现其实有一条非常省事的路——用带硬协议栈的以太网芯片。W5500就是这类芯片里口碑很好的一款SPI接口、官方驱动开源、移植工作量集中在几层回调上。这篇文章就把我移植W5500官方驱动到STM32的完整过程以及测试中遇到的坑整理出来。项目里要做TCP/UDP通信、想快速给单片机加网口的朋友可以直接按这个思路来。1. 方案选型与驱动结构拆解1.1 硬协议栈解决了什么问题W5500最核心的价值是把TCP/IP协议栈用硬件逻辑做进了芯片内部。MCU端不需要移植lwIP、uIP这类软件协议栈也不需要给协议栈预留大块RAM和Flash。MCU通过SPI发几条指令W5500就帮你完成了TCP连接管理、重传、分片、校验和这些脏活。这颗芯片支持8个独立Socket同时工作内部有16KB发送缓冲和16KB接收缓冲支持TCP、UDP、ICMP、IGMP、ARP、PPPoE这些常用协议。对MCU的资源要求低到什么程度呢以STM32F103C8T6为例这颗MCU只有20KB RAM跑完系统后剩下十几KB照样能带W5500做多路TCP连接。这种“MCU出SPI芯片出国网”的方案非常适合传感器数据上报、设备对接TCP服务器、简单UDP广播这类中小数据量场景。有人可能会问既然能挂W5500为什么不直接用带MAC的MCU加一颗PHY再跑lwIP答案很简单复杂度不同。lwIP调通裁剪、内存池、协议栈线程池那一套少说一周起步而且出了问题排查链路很长。W5500的硬协议栈把内核协议栈的活全包了你只需要关心业务层的socket收发这对产品迭代速度要求高的小团队来说非常友好。1.2 官方驱动怎么分层你的移植工作其实只有几个回调官方仓库是WIZnet在GitHub上的ioLibrary_Driver最关键的部分在library/Ethernet目录里W5500.c / W5500.h芯片寄存器读写以及SPI帧的拼接、收发时序这部分是厂商写好的一般不用动。wizchip_conf.c / wizchip_conf.h芯片初始化、PHY配置、网络参数设置还有回调函数的注册入口。socket.c / socket.h面向业务层的socket APIlisten、connect、send、recv、close都有。wiznet.c / wiznet.h一些辅助工具。这个驱动把“芯片怎么操作”和“业务怎么用网络”彻底分开了。你不需要读懂W5500.c里每一个寄存器操作细节甚至不需要看懂socket.c内部的状态机只需要按wizchip_conf.h里定义的函数指针类型把MCU的SPI读写、片选控制、临界区保护这几个底层事件实现出来注册进去初始化一遍剩下的就是用socket API写业务逻辑。这也是我推荐用官方驱动而不是自己造轮子的原因移植改动非常收敛代码量小出问题容易定位。市面上有些教程喜欢让你对着寄存器手册手写驱动说这样更“透”。如果你的目标是快速稳定上线真没必要。官方驱动被全球工程师反复用过边界情况处理得比大多数人自己写的完善得多。1.3 和其他以太网方案的对比方案协议栈位置硬件成本开发难度适用场景W5500芯片内部硬协议栈中芯片变压器RJ45低SPI官方驱动即可中小数据量TCP/UDP快速上线ENC28J60 uIP/lwIPMCU软件协议栈低中高协议栈裁剪费劲老方案资料多但性能和稳定性一般MCU内置MAC PHY lwIPMCU软件协议栈中高高要调RMII/PHY驱动数据量较大、需要灵活协议栈的场景上Linux内核自带高高复杂上层应用需要特别提醒的是W5500不是万能的。它内部的16KB TX加16KB RX缓冲决定了单连接吞吐有上限SPI总线带宽也受限如果是要做百兆级大流量网关W5500不合适。但绝大多数物联网设备的数据量都是几十KB级别在这种体量下W5500的稳定性和易用性优势非常明显。2. 移植前的准备硬件电路与开发环境2.1 W5500最小系统电路怎么画W5500内部集成了MAC和PHY外部电路比很多方案都简单但有几个点不能省。电源3.3V供电每个电源引脚旁边放0.1uF去耦电容总电源入口再放一颗10uF钽电容或电解电容。W5500上电瞬间电流不小电源设计太弱容易出现第一次上电连不上、按复位又好的怪问题。晶振W5500需要25MHz无源晶振接XI/XO引脚两个负载电容按晶振规格书来一般18pF到22pF。画板时晶振尽量靠近芯片引脚走线短而直。我见过好几块板子Ping不通最后查下来都是晶振起振问题。网络变压器W5500不带RJ45需要外部接1:1隔离变压器。最省事的是用HR911105A这种自带变压器和RJ45座的集成器件外围就少了一个变压器封装。变压器中心抽头和Bob Smith端接电路要按参考电路画别偷懒。接口线SPI三根数据线加一根片选SCSn外加nRST复位引脚和INTn中断引脚。INTn可以不接靠轮询Socket状态也能跑但建议还是接到MCU一个GPIO上万一以后要中断通知不用飞线。W5500是3.3V电平器件STM32也是3.3V电平直接连就行。如果你用5V单片机先查数据手册确认IO是否兼容5V不确定的话加强上拉或者做电平转换。2.2 获取官方驱动并加入工程我这边环境是STM32CubeMX生成工程Keil MDK编译目标芯片STM32F103C8T6。换成其他M内核芯片完全一样思路不依赖具体型号。从GitHub拉WIZnet/ioLibrary_Driver的最新版本或者下载release包。把library目录整个复制进工程在Keil的头文件包含路径里加上library/Ethernet library/Internet如果你的项目只用TCP/UDP可以不把DHCP、DNS的源文件加入编译。但头文件路径可以都加上免得以后想用还要回来补。这里提醒一个版本差异不同版本的ioLibrary_Driver底层回调函数签名略有不同。老版本一般就是void func(uint8_t *buf, uint16_t len)这种批量读写形式新版本可能拆成更细的读写接口。拿到驱动后先打开wizchip_conf.h看函数指针typedef的定义按照当前版本的定义实现就好。这个细节能决定你的代码能不能一次编译通过别照着网上老教程的签名硬抄。3. 核心移植从注册回调到Ping通3.1 SPI与GPIO初始化W5500支持SPI Mode 0CPOL0, CPHA0和Mode 3CPOL1, CPHA1官方例程两种都见过。选哪种都行关键是底层和驱动保持一致。我习惯用Mode 0CubeMX里配置如下/* SPI1 初始化主模式8位MSB先发 */ void MX_SPI1_Init(void) { hspi1.Instance SPI1; hspi1.Init.Mode SPI_MODE_MASTER; hspi1.Init.Direction SPI_DIRECTION_2LINES; hspi1.Init.DataSize SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity SPI_POLARITY_LOW; /* CPOL 0 */ hspi1.Init.CLKPhase SPI_PHASE_1EDGE; /* CPHA 0 */ hspi1.Init.NSS SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler SPI_BAUDRATEPRESCALER_4; hspi1.Init.FirstBit SPI_FIRSTBIT_MSB; if (HAL_SPI_Init(hspi1) ! HAL_OK) { Error_Handler(); } }SPI时钟我习惯先压到18MHz跑通功能稳定之后再考虑提频。W5500这颗芯片的SPI理论速率能做到很高数据手册里标称值很激进但实际板卡受布线、接插件、杜邦线影响高频下经常出现偶发读错寄存器。先低后高是调试的基本操作。GPIO配置比较简单CS配成推挽输出初始拉高RST配成推挽输出初始拉高。INTn如果不用就不管。3.2 底层回调实现与注册官方驱动的底层回调分四类我把它们集中放在一个w5500_port.c文件里#include w5500_port.h #include wizchip_conf.h void w5500_cs_select(void) { HAL_GPIO_WritePin(W5500_CS_GPIO_Port, W5500_CS_Pin, GPIO_PIN_RESET); } void w5500_cs_deselect(void) { HAL_GPIO_WritePin(W5500_CS_GPIO_Port, W5500_CS_Pin, GPIO_PIN_SET); } void w5500_spi_read(uint8_t *buf, uint16_t len) { HAL_SPI_Receive(hspi1, buf, len, HAL_MAX_DELAY); } void w5500_spi_write(uint8_t *buf, uint16_t len) { HAL_SPI_Transmit(hspi1, buf, len, HAL_MAX_DELAY); } void w5500_cris_enter(void) { __disable_irq(); } void w5500_cris_exit(void) { __enable_irq(); }注册动作放在复位之后void w5500_hw_init(void) { /* 硬件复位 */ HAL_GPIO_WritePin(W5500_RST_GPIO_Port, W5500_RST_Pin, GPIO_PIN_RESET); HAL_Delay(20); HAL_GPIO_WritePin(W5500_RST_GPIO_Port, W5500_RST_Pin, GPIO_PIN_SET); HAL_Delay(200); /* 注册回调 */ reg_wizchip_cs_cbfunc(w5500_cs_select, w5500_cs_deselect); reg_wizchip_spi_cbfunc(w5500_spi_read, w5500_spi_write); reg_wizchip_cris_cbfunc(w5500_cris_enter, w5500_cris_exit); }这里有几个关键点需要理解。先说SPI帧结构。W5500的每笔SPI传输都由三个阶段组成16位目标地址、8位控制字节、数据。控制字节的高3位是块选择000表示公共寄存器、001表示Socket寄存器、010表示TX缓冲区、011表示RX缓冲区bit4是读写标志低4位是传输模式寄存器访问用固定数据长度模式缓冲区访问用可变数据长度模式。驱动内部的WIZCHIP_READ、WIZCHIP_WRITE、WIZCHIP_READ_BUF这些函数就是把这些帧拼出来再调用你注册的SPI收发回调。这意味着一个很重要的事实在一整帧SPI传输过程中片选必须全程保持低电平。你不能在驱动内部“先发地址、抬CS、再收数据”我注册的批量收发函数就会被驱动在同一帧片选内连续调用。如果你用HAL库的HAL_SPI_TransmitReceive一次调用就把地址、控制、数据全发完没有问题如果某些函数拆成多次收发注意别让CS在这期间被拉高。临界区回调是为了保护寄存器读改写操作比如修改Sn_CR命令寄存器时不允许中断打断。裸机环境关中断最简单跑FreeRTOS时可以考虑把回调实现成挂起调度器或获取互斥量避免长时间关中断影响系统实时性。3.3 芯片初始化与网络参数配置底层回调注册完成后调用wizchip_init做芯片初始化。这里需要为8个socket分配TX/RX缓冲区大小单位是KB发送侧8个值加起来不能超过16接收侧同理。uint8_t txsize[8] { 8, 8, 0, 0, 0, 0, 0, 0 }; uint8_t rxsize[8] { 8, 8, 0, 0, 0, 0, 0, 0 }; if (wizchip_init(txsize, rxsize) -1) { /* 初始化失败的处理 */ }上面这个配置表示只启用socket 0和socket 1各自分配8KB发送和8KB接收。如果只用一个socket可以配成16其余全给0。但注意socket缓冲区太小会直接影响TCP吞吐因为TCP窗口、MSS、重传缓冲都要从这个池子里出我建议每个活跃socket至少分配4KB以上。然后设置网络参数。官方驱动推荐用wiz_NetInfo结构体一次性写入wiz_NetInfo netinfo { .mac { 0x00, 0x08, 0xDC, 0x12, 0x34, 0x56 }, .ip { 192, 168, 1, 100 }, .sn { 255, 255, 255, 0 }, .gw { 192, 168, 1, 1 }, .dns { 8, 8, 8, 8 }, .dhcp NETINFO_STATIC }; wizchip_setnetinfo(netinfo);MAC地址一定不能全0WIZnet官方没有在芯片里烧录唯一MAC需要自己规划。同一个局域网里多个设备MAC不能重复否则会出现Ping时通时不通、ARP表冲突这种诡异问题。常见做法是固定厂商前缀加产品序列号或者随机数。3.4 快速验证三板斧版本寄存器、PHY状态、Ping初始化完不要急着写业务代码先做三个快速检查。第一回读版本寄存器uint8_t version getVERSIONR(); /* W5500 正常应返回 0x04 */如果读出来是0xFF或者0x00大概率是SPI模式不对、CS引脚没拉对、或者芯片没跑起来。这一步通过说明SPI物理链路和驱动底层已经通了。第二检查PHY链路状态uint8_t phycfgr getPHYCFGR(); if (phycfgr PHYCFGR_LNK) { /* 网线已连接 */ }链路有信号但Ping不通基本就是网络参数问题看看网关、子网掩码、本机IP是否和测试电脑在同一网段。W5500的ARP和ICMP是硬件处理的只要网络参数正确Ping通不需要MCU参与。4. Socket编程实战从Ping通到收发数据4.1 TCP服务器一个直接能跑的环回例程官方socket.c提供了一套类BSD的API。TCP服务器流程是socket - listen - 轮询状态 - recv/send。下面是一个能直接跑的环回代码收到什么回什么#define W5500_SOCK_TCPSRV 0 void tcp_server_task(void) { uint8_t buf[1460]; int32_t ret; /* 监听5000端口 */ socket(W5500_SOCK_TCPSRV, Sn_MR_TCP, 5000, Sn_MR_ND); listen(W5500_SOCK_TCPSRV); while (1) { if (getSn_SR(W5500_SOCK_TCPSRV) SOCK_ESTABLISHED) { ret recv(W5500_SOCK_TCPSRV, buf, sizeof(buf)); if (ret 0) { send(W5500_SOCK_TCPSRV, buf, ret); } else if (ret SOCKERR_TIMEOUT || ret SOCK_BUSY) { /* 超时或忙碌继续等待 */ } } if (getSn_SR(W5500_SOCK_TCPSRV) SOCK_CLOSE_WAIT) { close(W5500_SOCK_TCPSRV); /* 等socket回到CLOSED然后重新监听 */ while (getSn_SR(W5500_SOCK_TCPSRV) ! SOCK_CLOSED); socket(W5500_SOCK_TCPSRV, Sn_MR_TCP, 5000, Sn_MR_ND); listen(W5500_SOCK_TCPSRV); } delay_ms(1); } }三个细节说清楚。第一socket()的第四个参数flag传Sn_MR_ND表示禁止延迟ACK测试环回时建议加上可以减少半包、粘包对判断的干扰。第二listen只需要调用一次之后socket状态会在SOCKINIT和SOCK_LISTEN之间切换别在循环里重复listen。第三recv返回值的含义要记清楚返回0表示对端关闭返回负数是对应错误码官方头文件里定义了SOCKERR_TIMEOUT等常量调试时打出来看比盲猜强得多。4.2 TCP客户端与连接状态机客户端流程比服务器简单socket - connect - 轮询状态 - send/recvsocket(W5500_SOCK_TCPCLI, Sn_MR_TCP, 0, 0); connect(W5500_SOCK_TCPCLI, server_ip, server_port); if (getSn_SR(W5500_SOCK_TCPCLI) SOCK_ESTABLISHED) { send(W5500_SOCK_TCPCLI, tx_buf, tx_len); }注意connect不是阻塞等待它只是把SYN包发出去真正建立成功要看socket状态变成SOCK_ESTABLISHED。如果对端不可达状态过一会儿会回到SOCK_CLOSED不要在一个死循环里空转等待。TCP状态机是很多新手栽跟头的地方。你去看socket.c源码connect内部会推进Sn_CR寄存器依次走TCP的SYN_SENT、ESTABLISHED等状态。业务层不要自己去操作Sn_CR也不要频繁调用connect状态不对时先close再重新socket。4.3 UDP收发要点UDP协议比TCP简单很多socket时协议参数用Sn_MR_UDP绑定本地端口后直接收发socket(W5500_SOCK_UDP, Sn_MR_UDP, 6000, 0); int32_t len recvfrom(W5500_SOCK_UDP, rx_buf, sizeof(rx_buf), remote_ip, remote_port); if (len 0) { sendto(W5500_SOCK_UDP, rx_buf, len, remote_ip, remote_port); }recvfrom会同时返回源IP和源端口这是UDP调试最有用的信息。如果发现数据发出去对端收不到先打印remote_ip和remote_port绝大多数情况是这两个参数填错了而不是网络链路有问题。另外UDP不像TCP有确认和重传机制业务层如果需要可靠传输得自己做应用层确认这是协议设计范畴不细展开。4.4 需要DHCP时怎么办产品如果要求自动获取IP官方库的Internet目录里有DHCP客户端。把dhcp.c加入编译初始化时先定位一个socket作为DHCP通道然后在主循环里周期调用DHCP_runuint8_t dhcp_buf[600]; /* DHCP报文缓冲官方例程建议600字节 */ uint8_t dhcp_sock 7; /* 使用socket 7跑DHCP */ DHCP_init(dhcp_sock, dhcp_buf); while (1) { DHCP_run(); if (getDHCPStatus() DHCP_IP_ASSIGN) { /* DHCP分配完成从寄存器读回IP、网关、子网等 */ wizchip_getnetinfo(netinfo); break; } delay_ms(100); }DHCP在实际部署时会遇到很多环境问题比如路由器没开DHCP、MAC地址冲突、DHCP租期到期续约失败。所以我的建议是固件里保留固定IP和DHCP两种模式出厂默认固定IP方便产测需要时通过配置切换为DHCP。这样既能快速定位问题也符合多数产品的实际用法。5. 避坑指南移植与调试实录5.1 常见问题速查表现象可能原因解决办法版本寄存器读出0xFF/0x00SPI模式不对、CS没初始化、复位没完成检查CPOL/CPHA确认CS先拉低再收发复位后多等几百毫秒Ping不通但PHY链路寄存器显示LNKIP/网关/子网掩码不在同一网段先用固定IPPC端关闭防火墙改用同一交换机直连Ping不通链路指示灯也不亮晶振没起振、变压器电路问题查25MHz晶振和负载电容检查网络变压器参考电路能Ping通TCP连不上端口被占、listen状态不对、防火墙打印socket状态确认处于SOCK_LISTEN检查端口号收发一段时间后卡死缓冲指针回写异常、中断丢失业务层不要动Sn_RX_RD/Sn_TX_WR明确临界区保护吞吐上不去SPI分频太高、CS切换频繁提SPI时钟尝试DMA收发吞吐需求大时换方案DHCP拿不到IP环境没有DHCP服务器、MAC重复、缓冲太小先固定IP测试再排查MAC分配增大dhcp_buf换不同厂家的板子偶尔通信失败上电时序问题确保RST释放后等待足够时间再初始化必要时读版本寄存器做握手5.2 三个容易忽略的细节第一个是socket缓冲区总和。W5500发送侧总共16KB、接收侧总共16KB这个总量是硬限制。有人把8个socket都配成2KB觉得“都开了”结果一跑多连接就发现吞吐互相挤压。我实际项目里习惯只开真正用到的socket比如一个TCP加一个UDP那socket 0给8KB、socket 1给8KB就非常宽裕。不要贪多。第二个是Sn_RX_RD指针的回写。官方socket.c的recv函数内部会自动处理Sn_RX_RD寄存器业务层不要再自己操作。但如果你自己接管缓冲区收发比如用DMA直接读RX buffer读完一定要把Sn_RX_RD写回去否则W5500会认为数据还在缓冲区Sn_RX_RSR永远不归零socket最终会被撑满。第三个是CS时序。W5500对SPI片选有最短高电平和低电平的时间要求高频SPI下尤其明显。如果出现偶发读错某个寄存器十次里错一次优先怀疑CS时序。我调试时用逻辑分析仪抓过波形发现CS在帧间隔里几乎没抬起或者某一次的CS低电平时间过短加一点延时后问题消失。后来重新画了板子缩短了SPI走线长度才把问题彻底解决。5.3 RTOS环境下的使用建议官方的ioLibrary_Driver本身不依赖操作系统裸机、FreeRTOS、RT-Thread都能跑。但官方socket API不是线程安全的如果多个任务同时操作同一个socket或者不同任务各自操作不同socket但共用同一条SPI总线需要有同步机制。我实际用过两种模式。第一种是SPI总线加互斥锁所有W5500操作先取锁再执行。这个方案简单直接适配业务随意分配到多个任务代价是高并发场景下吞吐受锁竞争影响。系统里SPI本身是单主设备理论上并发也不会真的并行所以锁的代价通常可以接受。第二种是把W5500收敛到单一任务所有业务任务通过消息队列向这个任务发起网络操作请求socket完全由这个任务独占。这种模型跟官方socket API配合最顺不需要在驱动内部加锁调试时也容易复现问题。缺点是业务层发一个请求要过一层队列延迟略高但绝大多数场景感觉不到。临界区回调在RTOS里也要注意。注册成__disable_irq()虽然能保证原子性但如果临界区过长会拉高系统中断延迟。有实时性要求的任务如果被W5500的寄存器操作挡住容易踩到硬实时红线。更好的做法是临界区回调里只保护对临界资源的互斥访问或者使用可嵌套的临界区机制。5.4 调试工具与波形观察强烈建议准备一个逻辑分析仪最低几十块钱那种就够用。W5500的SPI帧格式非常规整抓到的波形里能清楚看到地址、控制字节、数据三段。调试时重点看两点CS是否在整帧传输期间稳定保持低电平控制字节的块选择位是否是预期值。如果读公共寄存器时抓到的控制字节显示块选择不对说明驱动版本配置有偏差。还有一个经验是分阶段验证。先把版本寄存器跑通确认SPI物理层OK再配置固定IP确认Ping通然后跑TCP环回确认socket收发OK最后再上DHCP、DNS和你的业务协议。每一步都有明确验证点出了问题直接锁定在哪一层不要一上来就全链路调试。最后再分享一个小技巧。如果你只是想确认驱动移植是否成功根本不用先写完整业务。main里初始化完w5500_hw_init再回读VERSIONR只要读到0x04底层就是通的。之后测Ping通了就说明网络参数和PHY没问题。剩下的TCP/UDP业务错误绝大多数都能通过打印socket状态码定位。按照这个节奏走W5500的移植一般不会超过半天。
返回列表