FPGA以太网免软核:W5500硬件协议栈驱动设计与实测
简介这是一份面向FPGA开发者的W5500驱动实现与范例工程定位于解决W5500与FPGA之间SPI接口设计、状态机驱动及高速数据收发等实际问题。W5500内置硬件TCP/IP协议栈配合FPGA可达到3.5M字节/秒的实测传输速度接口简洁易用适用于工业控制、物联网设备、远程监控等需要实时网络通信的场景。压缩包大小为21.55MB内含可直接下载使用的应用示例与驱动代码开发者可据此快速完成初始化配置、SPI时序控制、中断处理及数据收发功能的移植或二次开发。已有1212人学习下载工程结构清晰适合具备一定FPGA基础、希望快速集成以太网通信能力的开发者参考。 搞FPGA以太网这个需求最怕的不是写代码而是协议栈那一大堆状态。之前项目里遇到网络功能我第一反应是挂软核跑lwIP后来换用W5500这颗芯片才发现很多场景下根本不用那么折腾。硬件协议栈把TCP/IP全部固化在芯片内部FPGA这边只需要通过SPI接口读写寄存器相当于把网络协议栈简化成一个“读写外部寄存器”的活。我在实际工程里用它跑下载实测速度稳定在3.5M字节/秒左右也就是大概28Mbps的有效吞吐做远程配置、数据采集、固件升级这类应用完全够用。这篇文章就围绕这个项目梳理一下为什么选W5500而不是软核方案、SPI协议里有哪些关键寄存器、FPGA驱动模块怎么设计、3.5M字节/秒是怎么测出来的以及下载下来的驱动怎样才能做到“直接用”。如果你是第一次在FPGA上接网络功能这篇文章能省你不少查手册的时间。1. 选型逻辑FPGA配W5500而不是CPU软核跑lwIP1.1 硬件协议栈把网络功能变成了寄存器访问FPGA上实现以太网其实有三条路可以走。第一是用内嵌MAC加外部PHY自己写或者移植TCP/IP协议栈灵活度最高但工作量非常大要处理ARP、IP分片、TCP重传、超时管理这些细节。第二是在FPGA里跑MicroBlaze或者Nios II软核再在软核上跑lwIP协议栈好处是代码生态成熟坏处是软件调试链路长而且软核和缓存会占用不少片内资源。第三就是今天说的W5500方案芯片内部把MAC、PHY、TCP/IP协议栈全部做成了硬件逻辑FPGA只负责通过SPI发命令和搬运数据。从FPGA开发者的视角看W5500就是一个SPI从设备你不需要懂TCP的拥塞控制不需要处理ACK重传只要按顺序操作寄存器协议栈内部会自动把连接状态维护好。这个思路很像在生活中请了一个专门处理信件往来的文员你只需要把信投进邮箱剩下寄信、回执、催件都是他搞定。对FPGA工程师来说这种“把复杂协议藏起来”的做法特别解压因为时序和状态机才是我们擅长的事情。1.2 这套组合最适合什么样的项目我实际用下来觉得这套方案最适合下面几类场景。一类是吞吐需求在几十Mbps以内就够用的工业数据采集和远程监控设备这类设备往往既要联网又不能把FPGA资源都花在协议栈上。另一类是FPGA里已经跑了很多信号处理或者图像算法逻辑剩余资源紧张根本塞不下一个完整的CPU子系统。还有一类是项目排期紧希望一两周内就能把网络功能稳定接起来前期不想花太多时间在协议栈上。反过来它也有明显的边界。如果项目要求万兆或者至少千兆线速转发W5500完全不够看如果需要同时维护几十个TCP连接或者要处理非标准私有协议硬件协议栈的灵活性也会成为限制。W5500最多提供4个Socket每个Socket的收发缓冲是可配置的但协议栈是固化的想改TCP窗口策略或者加私有选项几乎不可能。选型时候要有这个预期别拿它和通用处理器去比。2. W5500 SPI协议要点三段式帧与关键寄存器2.1 三段式SPI帧结构W5500的SPI每次完整访问都分三个阶段。地址阶段是16bit最高位表示读还是写接着3bit选择寄存器区域低12bit是寄存器偏移地址。区域选择可以指向通用寄存器、Socket0到Socket3的寄存器块以及对应的收发数据缓冲区。控制阶段是8bit主要用低3位来表示读操作的数据长度这也是W5500支持连续读的关键如果是连续传输几十字节这个控制字段能告诉芯片接下来要读多长。数据阶段就是真正要读写的那些数据字节。SPI模式要用模式0或者模式3实际工程里我习惯配成CPOL0、CPHA0。片选信号在整次访问期间必须保持低电平访问结束再拉高。有些调不通的板子问题就出在CS时序上CS和SCK相对关系不对读出来的数据全是0xFF。建议上板之前先用逻辑分析仪抓一下CS、SCK、MOSI、MISO四根线确认波形和手册一致再往下写驱动逻辑。2.2 初始化、TCP连接、收发数据要关注哪些寄存器以TCP客户端为例最小操作序列大概是这样的。先写MR寄存器配置芯片做软复位然后延时一段时间等待芯片稳定。接着设置Sn_MR把当前Socket配置成TCP模式往Sn_CR写OPEN命令。再之后写目标IP地址和端口发CONNECT命令轮询Sn_SR直到进入SOCK_ESTABLISHED状态。发送数据的时候先读Sn_TX_FSR确认发送缓冲剩余空间把用户数据连续写到Sn_TX_BUF区域更新Sn_TX_WR写指针再往Sn_CR发SEND命令。接收数据时先读Sn_RX_RSR看接收缓冲有多少字节有数据就连续读Sn_RX_BUF读完更新Sn_RX_RD读指针最后发RECV命令释放缓冲。这个流程里最容易出错的是指针更新Sn_TX_WR、Sn_RX_RD这类指针最好连续写两次让芯片内部正确锁存边界否则会在高负载下偶发丢包。3. FPGA驱动模块设计SPI状态机与对外接口3.1 SPI主机状态机的实现思路驱动模块的底层需要实现一个SPI主机它负责把地址、控制、数据三个阶段按位串行送出去。我用移位寄存器加计数器来做一个发送移位寄存器、一个接收移位寄存器、一个bit计数器主状态机在IDLE、ADDR、CTRL、DATA、DONE之间跳转。ADDR阶段把16bit地址逐位移出CTRL阶段送控制字节DATA阶段如果是写则将数据逐位移出如果是读则从MISO采样并拼成字节。网上很多驱动喜欢把状态机做得特别复杂用多个嵌套的条件判断。我按字节边界精简状态后整个状态机非常干净。关键点在于SPI是位流但寄存器是字节对齐的所以每次状态切换必须发生在字节边界上不能用半字节判断。这里用计数器对8取模到0的时候再切换时序上不容易出气泡。3.2 对外接口写地址、给数据、看结果驱动模块设计完以后顶层接口我刻意做得非常克制分成了三组。配置类接口就是一组简单的读写握手cfg_valid、cfg_addr、cfg_wdata、cfg_rdata、cfg_done用户想读写W5500任意寄存器直接给地址给数据就行。发送类接口用了类似AXI-Stream的风格有tx_valid、tx_ready、tx_data和tx_last整包数据输入模块内部在包尾自动触发SEND命令。接收类接口同样有rx_valid、rx_ready、rx_data、rx_lastW5500收到完整数据包后驱动会把数据流水一样输出。我给上层用户留了一组状态信号链路建立后会拉高link_upTCP连接成功后会拉高socket_established。用这三个接口用户完全不需要了解W5500的寄存器映射、SPI帧格式和Socket模式只要在连接建立后往发送接口喂数据有数据进来接收接口就会给valid脉冲需要流控就拉低ready。很多拿到驱动的人第一反应是“这就完事了”但确实不需要更多东西接口做得直接使用门槛才低。4. 实测3.5M字节/秒测法、限速分析与提速4.1 测试环境和测法测试环境是Kintex-7开发板加一块W5500模块PC用网线直连模块的RJ45座PC端跑iperf3或者直接通过HTTP下载文件。FPGA侧SPI时钟最开始只配了20MHz实测下载速度只有1.2M字节/秒左右。后来把SPI时钟分频系数调小提升到50MHz之后实测速度稳定在3.5M字节/秒。这个数字对应约28Mbps有效吞吐虽然和千兆网完全不是一个量级但用来做现场数据采集和远程运维已经很够用。测量下载速度比ping更有参考价值因为下载是连续的大数据包能同时压测SPI总线、内部缓冲和协议栈处理能力。ping只能说明连接通断测不出吞吐天花板。建议大家在项目验收时也用iperf这类工具压一压收发通路这样对整个系统的性能上限会更有数。4.2 为什么上限在这里以及还能怎么挤理论上SPI时钟50MHz下字节速率是6.25M字节/秒实测3.5M字节/秒意味着有效负载只占56%左右其余开销主要在三个地方。第一个是寄存器读写都要额外传2字节地址加1字节控制如果频繁单字节读写有效吞吐会非常低。第二个是W5500内部Socket缓冲操作有同步等待写指针更新后芯片内部需要时间完成状态切换。第三个是收发共用同一根SPI总线半双工机制决定了不能一边收一边发。实测中最有效的优化是减少小帧读写频率尽量使用连续块传输。读Sn_RX_BUF的时候一次把整包数据读完而不是分多次小帧读写Sn_TX_BUF的时候把整包数据积攒在一起连续写几十甚至几百字节再发SEND命令。同样条件下连续读256字节比逐字节读吞吐提升接近一倍这个差距在高速采集项目中非常明显。5. 调试中踩过的三个坑与最终定位5.1 复位时序和初始化顺序第一个坑是上电后立刻配置寄存器会偶发失败。W5500内部PHY上电需要稳定时间芯片还要做内部校准这个期间SPI虽然能访问但寄存器的配置可能不生效。后来在驱动初始化状态机里加了严格时序先拉低RST引脚保持至少50us释放后再延时等待芯片内部稳定最后才开始写MR软复位并执行后续配置。加了这一个状态之后实测上电成功率从大概70%提升到了接近100%。这个坑在仿真阶段几乎发现不了因为仿真模型不会模拟PHY上电时序只有在真机上电时才暴露。建议拿到开发板先写一个最简单的寄存器回读测试确认版本寄存器能读到正确值再继续配置Socket否则问题排查时很难分清是时序问题还是协议问题。5.2 接收缓冲环形回卷和指针更新第二个坑出现在接收方向。W5500的接收缓冲本质是环形缓冲区如果Sn_RX_RSR指示的数据量跨越了缓冲末尾直接线性读取会读到回卷边界之外数据就错位了。正确处理是每次接收前先根据当前读指针计算到缓冲末尾还剩多少空间再和Sn_RX_RSR取最小值分段读取读完后统一更新Sn_RX_RD指针并执行RECV命令。我一开始图省事直接按Sn_RX_RSR长度连续读结果在数据量大时经常出现半个包乱码。后来抓了W5500的输出波形才发现是环形回卷导致的问题。这个坑在普通TCP连接测试中不一定触发只有长时间高负载跑数据时才会暴露排查起来比较隐蔽。5.3 中断标志位的清除方式第三个坑是中断标志的清除方式。W5500的中断寄存器是写1清除但有些Socket的中断标志位如果清除不干净会导致后续中断不触发。我们遇到过TCP连接建立后只能收到第一包数据之后就完全没有中断的情况最后定位到是清中断时用了整字节覆盖把不该清的状态位也写掉了。正确做法是读-改-写只把当前需要处理的中断位写1其他位保持原值。这个坑特别容易出现在复用了别人代码的情况下大家在处理中断标志位时都要养成读-改-写的习惯别直接赋值一个固定常量。中断标志处理错误不会立刻暴露通常在连续收发一段时间后才会让协议栈卡死定位成本很高。6. 下载即可用的移植方式与扩展建议6.1 引脚约束和时钟分频配置下载驱动源码后第一步是核对W5500模块的引脚。大多数W5500模块是3.3V电平FPGA端IO要对应配置成LVCMOS33。如果开发板IO是2.5V或者1.8V需要加电平转换或选择兼容的Bank。复位信号要连接到模块的RST引脚片选、SCK、MOSI、MISO四根线对应连接。部分模块还带中断引脚用来告诉主机有事件发生如果不想轮询中断标志可以把这个引脚接到FPGA的输入中断上。时钟分频参数SPI_CLK_DIV决定SPI实际速率。外部输入时钟如果是50MHz建议先配一个比较大的分频系数让SPI时钟稳定工作在10MHz到20MHz先把链路调通再逐步提高频率。不要一上来就直接跑到W5500支持的上限真机走线、电平转换都会影响时序留点余量更稳。6.2 顶层例化与验证路径驱动模块的例化非常直接下面是一个最小化的示例w5500_driver #( .SPI_CLK_DIV(4) ) u_w5500 ( .clk (clk_50m), .rst_n (rst_n), .spi_cs_n (w5500_cs_n), .spi_sck (w5500_sck), .spi_mosi (w5500_mosi), .spi_miso (w5500_miso), .rst_out (w5500_rst), .link_up (link_up), .socket_established (conn_flag), .tx_valid (user_tx_valid), .tx_ready (user_tx_ready), .tx_data (user_tx_data), .tx_last (user_tx_last), .rx_valid (user_rx_valid), .rx_ready (user_rx_ready), .rx_data (user_rx_data), .rx_last (user_rx_last) );接好例化之后建议先做一次简单的回环自测在FPGA内部把接收FIFO的数据再回填到发送接口板上跑起来后PC端用TCP工具发送一段数据如果FPGA能把数据原样回传说明底层通路已经通了一半。然后再跑iperf或者大文件下载验证持续吞吐。这样分阶段验证出错时能快速缩小范围。6.3 后续扩展的几个方向驱动本身做得很精简后续要扩展也不难。想做多路通信可以继续开放其他Socket四个Socket都能独立工作。想把驱动挂到复杂系统里可以把发送接收接口前面加一个FIFO做速率匹配避免用户逻辑偶尔忙不过来时丢包。如果项目里处理器是ZYNQ也可以把底层的SPI主接口换成AXI接口方便ARM端远程访问。整体来看这套W5500的FPGA驱动方案最大的价值是把网络协议栈的复杂度屏蔽掉了留下的就是很标准的读写、收发、状态检测接口。实测3.5M字节/秒的速度在工业场景里完全够用接口简洁到接上就能跑这也是为什么我们在项目里把网络功能固定成这个方案的原因。如果你也在给FPGA找网络方案建议按这个思路先跑通一个最小系统再按实际需要扩展。本文还有配套的精品资源点击获取