资讯动态

FPGA+W5500硬件协议栈实现以太网通信的工程实践

发布时间:2026/10/21 21:53:28 来源:尧图企业网站定制
前阵子做采集板卡FPGA把多路ADC数据汇总之后要实时上报网络通信顺理成章成了刚需。一开始我天真地想在FPGA里自己写精简TCP/IP协议栈折腾两个星期后老实了改成了FPGA W5500的方案FPGA只用SPI接口访问W5500这颗自带MAC/PHY和硬件协议栈的芯片把以太网、IP、TCP/UDP、ARP这些脏活全部交给它。这个改动让项目进度一下子顺畅起来也让我把W5500从硬件连接到寄存器操作、从UDP回环到TCP断线重连完整走了一遍。这篇就把整段“奇妙之旅”拆开讲清楚适合正打算在FPGA上做以太网通信、又不想被协议栈细节劝退的工程师参考。1. 项目概述与方案选型1.1 W5500在FPGA系统里扮演什么角色W5500是WIZnet家族里一颗非常经典的以太网控制芯片内部集成了10/100M以太网MAC和PHY更重要的是把TCP/IP协议栈做成了硬件TCP、UDP、IPv4、ARP、ICMP、IGMP这些协议的处理不再占用FPGA逻辑资源。FPGA和W5500之间只有SPI四线接口SCLK、MOSI、MISO、SCS外加一个复位引脚和一个中断引脚。对FPGA工程师来说W5500把“联网”这个任务从“需要精通协议栈”降级成了“会操作SPI外设”。你需要关心的不是以太网帧怎么封装、TCP三次握手怎么实现、重传超时怎么计算而是怎么通过SPI读写芯片内部寄存器、怎么把应用数据放进发送缓冲区、怎么从接收缓冲区把数据取出来。协议栈里的状态机被封装在芯片内部FPGA侧只需要维护一个简单的应用层状态机。它解决的典型问题是数据采集系统需要把测量结果、设备状态、告警信息上报给上位机或者上位机需要下发配置指令给FPGA设备。用W5500构建以太网通信开发周期可以压缩到原来的三分之一左右而且通信行为很稳定因为协议处理不再受FPGA时序收敛问题影响。1.2 为什么没有选择更“硬核”的协议栈方案做以太网通信摆在桌面上的方案其实有好几种我简单对照一下当时纠结的几个方向方案优点缺点场景匹配度FPGA纯逻辑实现TCP/IP灵活可深度定制无额外芯片成本协议栈极其耗时资源占用大时序收敛难调试周期按月算需要线速处理、自定义帧格式、低延迟特定场景软核处理器 轻量协议栈开发友好有现成协议栈需要额外例化CPU核实时性受中断和调度影响启动流程复杂数据流复杂、需要跑操作系统或复杂应用的场合独立MAC芯片 自研协议层灵活性较高仍要自己做MAC层管理和上层协议工作量依然很大对传输效率要求极高的专业网络设备FPGA W5500开发快协议栈硬件化SPI接口简单不占用FPGA逻辑吞吐受SPI时钟限制连接数有限不适合超高速场景工业控制、数据采集板卡、仪表设备、物联网网关等中小流量场景对我这个采集板卡项目来说数据量是小百Kbps到几Mbps量级W5500的吞吐完全够用。而且项目进度是硬指标与其把两三个月砸在自研协议栈上不如把省下来的时间投入到采集链路和上位机软件上。这就是选W5500最朴素也最实际的逻辑。1.3 整体架构与数据流设计我最终搭建的系统架构分成两层。硬件上FPGA开发板通过SPI接口连接一块W5500模块模块上自带网络变压器和RJ45座插上网线就能和PC交换机连通。FPGA内部则按功能拆成几个模块应用数据源模块负责产生需要上报的数据包发送FIFO暂存待发送数据W5500控制器负责所有SPI读写事务接收FIFO存放下行数据应用处理模块解析接收到的指令。数据流方向很清晰采集模块把ADC数据拼成报文格式写入发送FIFO。W5500控制器检测到FIFO非空后按UDP或TCP发送流程组织SPI事务把数据搬进W5500的发送缓冲区触发芯片发送。接收方向完全相反W5500收到网络数据后FPGA控制器通过SPI把数据从接收缓冲区读出来写入接收FIFO应用模块再逐条解析。一开始做模块划分时我犯过一个错误把SPI时序控制、寄存器操作、socket管理全写在一个大状态机里结果调试时一崩全崩根本分不清是SPI时序问题还是协议流程问题。后来强制拆层底层只做SPI字节收发中间层只做寄存器读写原语上层才做socket生命周期管理。这个分层思路是这次项目里最值得保留的经验。2. 拆解W5500的寄存器与数据通路2.1 SPI帧格式3个字节的“信封”要把W5500玩明白第一关是理解它的SPI帧格式。W5500一次SPI事务由两部分组成3字节的“地址控制”信息后面跟真正的数据段。这3个字节相当于一个信封告诉芯片你要访问哪个区域、干什么操作。具体来说前16位是寄存器或缓冲区偏移地址紧随其后的8位控制字节里最高位是读写方向中间两位决定数据段长度模式低5位是块选择位BSB。BSB负责选通不同的区域通用寄存器、Socket0到Socket3的寄存器、发送缓冲区、接收缓冲区。所有SPI事务都必须带着这三个字节开头芯片才能知道后续数据应该落到哪里。实际用FPGA实现SPI Master时要特别注意这个帧格式不是普通的SPI寄存器读写模式不能简单地用“发地址字节地址寄存器”的通用IP核代打。我试过直接用FPGA自带的SPI控制器跑W5500发现它默认把地址当作普通数据发送根本不认识W5500的块选择机制。最后老老实实自己写了一个状态机按W5500手册的帧格式组帧才把通信跑通。刚开始接触W5500时我建议先用信号发生器或逻辑分析仪抓一遍SPI线上的实际波形看三字节信封和数据段的排列是否符合手册时序图。这一步走稳后面所有寄存器读写都不会跑偏。2.2 从VERSIONR开始自检与初始化W5500内部有个版本寄存器VERSIONR偏移地址0x001F芯片正常工作时默认读回值通常是0x04。这几乎是所有W5500调试的第一步只要能通过SPI读到这个版本号就说明供电、复位、SPI接线、时钟极性这些基础条件全部正常。我习惯把初始化流程写成固定的顺序每一步都有明确目的硬件复位拉低RSTn并保持至少几百微秒再拉高然后延时等待芯片内部稳定。SPI通路自检读VERSIONR确认读回0x04如果不对立刻停住排查硬件。配置网络参数写本机MAC地址SHAR、IP地址SIPR、子网掩码SUBR、默认网关GAR。这些寄存器都是连续字节地址MAC 6字节、IP 4字节可以用SPI连续写。配置收发缓冲区大小通过RMSR和TMSR给各个socket分配发送、接收缓冲区配额。配置重传参数RTR和RCR控制TCP重传延时和次数对可靠性影响很大。按需使能中断IMR、SIMR控制全局和socket级中断使能。查询PHY状态通过PHYCFGR的链接状态位确认网线已插好PHY已建立链路。这套顺序我后面在FPGA状态机里原封不动实现了。实测下来只要第2步版本号能读对后面配置大概率不会出幺蛾子如果卡在后面的某一步优先怀疑协议状态而不是SPI通道。2.3 缓冲区分配把32KB花在刀刃上W5500片内有一块约32KB大小的存储划分成多个发送缓冲区和接收缓冲区区域分配策略直接决定你能同时开几个socket、每个socket能跑多大的吞吐。每个socket都有独立的RX和TX空间大小通过RMSR接收内存配置和TMSR发送内存配置来控制。如果不改配置芯片复位后会把空间在4个socket之间平分每个socket只能分到很小的收发缓冲。这对只做简单UDP透传是够用的但如果要做大包收发或高吞吐传输就一定要调整分配策略。我在项目里只使用了1个socket做TCP Server于是把剩下的socket缓冲区全部配成最小把所有空间集中到这个socket上收发缓冲各给到最大。这样单个连接能缓存更多数据突发流量到来时不容易丢包。这里有个容易踩的坑缓冲区大小配置必须在socket打开之前完成。socket一旦处于打开状态再修改RMSR/TMSR缓冲区布局变化可能导致指针错乱、数据写到错误区域。所以在初始化流程里我把它放在第4步优先级排在打开socket之前顺序不能乱。2.4 一条UDP包在W5500里走完的路径以UDP发送为例把数据从FPGA送出去W5500内部大概经历这么几步先读当前发送写指针Sn_TX_WR这相当于记录“接下来该往缓冲区哪里写”。然后把要发的数据通过SPI连续写入发送缓冲区对应位置。因为缓冲区是环形结构如果写指针加上数据长度越过了缓冲区末端必须拆成两段写先写末尾剩下的部分再写从开头开始的剩余部分这一步很多新手会漏。写完数据后把Sn_TX_WR更新为“原指针数据长度”并往Sn_CR写入SEND命令。W5500硬件看到SEND命令自动完成UDP头封装、IP校验、以太网帧打包然后从PHY发出去。SEND命令执行期间Sn_CR不会立即清零需要轮询直到硬件自动清0才能继续下一条发送指令。接收路径是反向的。W5500收到UDP包后会把一段8字节的头部信息放到接收缓冲区里面包含源IP、源端口、数据长度等字段紧接着才是UDP载荷数据。FPGA侧要判断Sn_RX_RSR接收数据大小寄存器非零然后读Sn_RX_RD指向的数据先读8字节头部再读载荷处理完更新Sn_RX_RD并写RECV命令释放缓冲区空间。理解这条路径后你会发现FPGA的工作其实非常机械读指针、写数据、更新指针、发命令、等命令完成。协议栈的复杂逻辑全部被W5500消化掉了。3. FPGA侧模块划分与状态机设计3.1 模块划分别把状态机写成意大利面FPGA侧如果所有功能挤在一个模块里后面调试会非常痛苦。我最终把逻辑拆成了4层第一层是SPI物理层负责把SCLK、MOSI、MISO、SCS的时序精确实现出来对外只提供两个接口写寄存器和读寄存器读写参数是寄存器地址、数据长度、数据缓存指针。它不关心W5500的协议细节。第二层是寄存器层在SPI物理层之上封装对具体寄存器的操作比如写网络配置寄存器、读中断寄存器、读写缓冲区等。这一层让上层代码看起来像在操作“一个个有名字的寄存器”而不是一堆字节流。第三层是socket管理层负责实现socket的打开、监听、连接、发送、接收、关闭这些状态转换以及超时处理。第四层是应用层处理具体业务逻辑比如组装报文、解析指令、维护回环测试状态。分层最大的好处是定位问题快。回环测试数据乱码我只需要查第三层指针更新有没有问题不需要去翻SPI时序。SPI读回全FF我也只需要在第一层和第二层排查不会牵扯协议流程。3.2 SPI主机状态机怎么样读写才稳W5500对SPI模式的兼容性算比较好常用的Mode 0或Mode 3都能正常工作。我最终选了Mode 0即CPOL0、CPHA0空闲时时钟低电平数据在上升沿采样、下降沿变化。和FPGA内部逻辑时钟配合起来比较自然。我的SPI状态机分四步第一步是IDLE等待读写请求。第二步是发送3字节信封通过MOSI逐位移出地址和控制字节同时这个阶段MISO上的数据无意义。第三步是数据阶段。写操作时MOSI继续逐字节移出数据读操作时FPGA产生同样的SCLK但在每个数据位的采样时刻读取MISO。第四步是拉高SCS结束事务返回IDLE回报完成标志。有一个细节很重要读操作在发完控制字之后W5500需要一点时间把第一个数据位准备好。如果时序余量不够可以在数据阶段开始时先产生一个空时钟沿再从第二个有效沿开始采样。实际用FPGA实现时我通过内部计数器把SCLK周期分成了多个时间片发送沿和采样沿分别放到不同的时间片这样就能精确控制建立保持时间不会出现采样到毛刺的问题。3.3 初始化序列时序比代码顺序更重要FPGA上电后初始化流程要严格按时序推进不能像写软件那样想先做什么就做什么。我用一个master初始化状态机执行以下序列IDLE状态先等待一个固定延时的复位低电平确保W5500完全复位。随后进入POST状态读版本寄存器确认SPI通路OK。然后依次进入网络参数配置、缓冲区分配、重传参数配置、中断配置、PHY状态查询等状态。每个状态都设置超时计数器万一某一环卡住状态机能自动上报初始化失败而不是无限等下去。实际调试中我发现上电后立刻进行SPI访问有时会失败原因是W5500内部还在初始化所以我在复位拉高后加了约5毫秒的延时再去读版本寄存器一次性通过了。这个延时数值不算大但确实避免了很多偶发问题。如果你遇到“第一回读版本号失败第二回又成功”这种玄学问题多半就是上电时序没给够。初始化期间不要打开socket也不要写任何数据。所有寄存器配置完成后再去初始化socket否则可能在配置过程中触发意外的网络行为导致连接状态混乱。3.4 发送/接收状态机的关键转移条件发送状态机最忌讳的是“写完SEND命令就认为发送完成”。W5500的Sn_CR寄存器写入SEND之后需要等待芯片把命令处理完它会自动清零。我一开始用固定延时结果偶尔会导致下一条命令被吞掉后来改成轮询Sn_CR直到为0效果好很多。发送链路状态机大致是检查发送FIFO是否有数据。读取Sn_TX_FSR确认发送缓冲区空闲空间足够。读取Sn_TX_WR指针。按环形缓冲区分段写入数据。更新Sn_TX_WR。写Sn_CR SEND命令。轮询Sn_CR直到清零。回到空闲状态等待下一包。接收链路状态机则更依赖中断或轮询。我采用轮询方式定时读Sn_RX_RSR发现接收字节数非零就启动接收流程。接收流程要先读头部确认数据长度再读载荷最后更新Sn_RX_RD并写RECV命令释放空间。在轮询方式下注意不要频繁读寄存器否则会占用大量SPI带宽。我设定接收轮询周期为几十微秒一次对吞吐影响很小又可以及时响应数据到达。4. 实操FPGA W5500 打通回环通信4.1 硬件连接与上电自检清单硬件连线其实只有6根线SCLK接FPGA时钟输出引脚、MOSI接FPGA输出、MISO接FPGA输入、SCS接片选输出、RSTn接复位输出、INTn接FPGA输入。W5500模块的供电是3.3VFPGA的IO银行也要配成3.3V电平这点容易被忽略如果IO电平不匹配SPI信号会有随机毛刺。上电之前先用量表确认VCC对地没有短路然后检查25MHz晶振是否起振。晶振不起振PHY层就完全无法工作网线插上也不会有链路灯。我再三提醒一点有些W5500模块的RJ45座自带网络变压器有些则是分离的变压器RJ45两种接法不同参考模块原理图接别臆测。上电后的自检顺序是FPGA启动完成后先复位W5500再读版本号确认SPI通路再读PHYCFGR确认链路状态。如果链路状态寄存器显示LINK0拔插网线、换网线、看交换机端口都是常规操作但更常见的原因是晶振没起振或者PMODE引脚配置不对。4.2 寄存器读写自测读版本号是一个好兆头我在FPGA里做了一个简单的调试寄存器接口通过串口命令能触发SPI读操作。第一个测试就是读W5500的VERSIONR寄存器。当串口打印出0x04时基本可以确认SPI模式正确、地址信封格式正确、W5500已经启动成功。这个“动起来”的感觉很重要它把整套链路拆成了“已打通”和“待调试”两部分。接下来我会继续做连读测试比如连续读多个网络配置寄存器确保SPI状态下数据连续性和地址递增逻辑没有bug。这一步能提前发现字节错位、丢位的问题不用等上网之后抓狂。注意读寄存器时如果数据错位往往不是数据线接反而是SPI采样沿不对。遇到读回数据像是“整体左移一位”的情况优先检查CPHA配置。4.3 UDP回环的FPGA侧流程回环测试是最快验证整条链路的办法PC端通过UDP发送任意数据给FPGA设备FPGA收到后原封不动返回给PC。这个功能涵盖了W5500的接收、读取、再发送全流程。我用的流程是这样的初始化时将socket0配置为UDP模式绑定端口5000。主循环不断轮询Sn_RX_RSR一旦收到数据先读出接收缓冲区的8字节头部从头部拿到源IP、源端口、数据长度。然后把数据原样复制到发送缓冲区设置目标IP为目标源IP、目标端口为目标源端口触发发送。这里有个容易踩的坑UDP接收头部的8字节不是载荷数据如果不跳过就直接把缓冲区内容发回去PC端收到的会多出8字节乱码。我当时就被这个坑绊了一个小时后来对照手册的接收头部结构才反应过来。为了验证数据内容是否完整我建议PC端发送带递增序号的数据包FPGA回环后PC校验序号连续性这样丢包、错序问题一目了然。4.4 上位机脚本验证与效果上位机验证我直接用Python的socket库写了几十行测试脚本。脚本向设备端UDP端口5000连续发送1000个数据包每包128字节包内带序号和填充数据然后统计回包数量和完整性。import socket import time DEST_IP 192.168.1.100 DEST_PORT 5000 TOTAL 1000 PAYLOAD bx * 124 s socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.settimeout(1) recv_ok 0 for i in range(TOTAL): data i.to_bytes(4, big) PAYLOAD s.sendto(data, (DEST_IP, DEST_PORT)) try: r, addr s.recvfrom(512) if r[:4] i.to_bytes(4, big): recv_ok 1 except socket.timeout: pass print(freceived {recv_ok} / {TOTAL})第一次跑通回环时1000个包全部原样返回那一刻的成就感很直接。不过等我把发包速率调高之后突然开始丢包了这就是下一节要说的性能问题。4.5 性能实测与计算W5500的吞吐上限主要受SPI时钟和帧开销限制。每次SPI事务不管读写多少数据都要先支付3字节信封的成本所以数据包越小有效带宽越吃亏。以SPI时钟20MHz为例如果发送一个1000字节的数据包一次事务传输的总字节数约为1003字节总耗时约为1003×8/20MHz0.4毫秒。换算成纯载荷吞吐理论上限约为20Mbps量级实际加上轮询、寄存器操作、帧间隔等开销实测能做到12到15Mbps就算不错了。如果换成64字节的小包每包3字节开销占比明显增大而且每包都要执行读指针、写数据、更新指针、发命令、等完成这一整套流程固定开销远大于传输时间实际小包吞吐会低很多。实测下来我把SPI时钟从10MHz逐步提高到25MHz吞吐相应上涨但再往上就遇到信号完整性问题了。所以高速应用建议在PCB上做好SPI走线缩短连接线调试阶段不要用过长的杜邦线。5. 常见问题与排查技巧实录5.1 寄存器读回全F/全0先别急着改代码现象是读W5500任何寄存器都返回0xFF或者0x00看起来像是芯片没工作但大部分时候不是芯片坏了而是SPI通路没建立好。按这个顺序查先查W5500的RSTn引脚电平如果一直拉在低电平芯片永远处于复位状态。然后查SCLK和MOSI引脚有没有虚焊、接触不良用手按压芯片会不会导致读数变化。再查SCS片选极性W5500是低有效片选如果FPGA侧片选极性配反了所有SPI事务都会被芯片忽略。最后查SPI模式CPOL/CPHA组合错了也会导致读回全错。这块的经验是读VERSIONR之前不要急着在FPGA代码里加各种打印和分支先用逻辑分析仪抓SCLK、MOSI、MISO、SCS四根线对比手册时序图往往一眼就能看出问题。5.2 PHY链路不建立检查顺序怎么排另一个高频问题是SPI通信正常版本号能读但网线插上后链路状态始终是断开网卡也显示“网络电缆被拔出”。我先检查25MHz晶振是否起振用示波器量晶振引脚没有波形就换晶振或检查负载电容。然后检查W5500模块的PHY模式配置引脚PMODE不同接法对应不同PHY工作模式接错会导致芯片内部PHY异常。再检查网络变压器部分中心抽头是否需要接电源或接地不同模块设计不一样。最后才怀疑网线本身。链路建立后PHYCFGR寄存器里的速度和双工位也会更新通过读这个寄存器可以确认PHY协商结果。如果总是协商成半双工检查一下网线和交换机端口配置一般不用太纠结。5.3 数据错位、CRC报错、偶发丢包数据错位通常发生在SPI读数据阶段表现为读回来的字节顺序不对或者某一位丢了。多半是采样沿设置不对或者SCLK频率太高导致建立时间不够。把SPI时钟降下来通常能缓解治本方案是重新计算FPGA内部SPI状态机的发送沿和采样沿时序。偶发丢包则要区分是接收缓冲区溢出还是发送缓冲区不足。接收方向如果PC端短时间内猛发包而FPGA来不及读取W5500的接收缓冲区新到的包会覆盖旧数据或直接被丢弃。解决思路是加大该socket的接收缓冲区配额同时提高接收轮询频率。发送方向如果应用层产生数据的速度超过W5500发送能力发送缓冲区会满这时应该通过反馈机制让数据源临时暂停而不是强行往里塞数据。我在项目里为发送FIFO加了高水位信号FIFO超过80%满时数据源模块就暂停产生新包等FIFO降到低水位再恢复。这个背压机制一下子消除了偶发丢包。5.4 TCP握手异常与连接断开做TCP Server时最容易出问题的环节是握手。SN_MR协议类型必须设置为TCP模式SN_PORT绑定端口后Sn_CR写LISTEN命令进入监听状态。PC端连接时W5500内部自动完成三次握手Sn_SR状态寄存器会从LISTEN切换到ESTABLISHED之后才能正常收发。常见错误是写完OPEN或LISTEN命令后没等Sn_CR清零就进行下一步导致命令丢失socket状态停留在不该停留的位置。另一种错误是网络配置里的IP地址、网关、子网掩码填错导致跨网段时ARP解析失败握手永远完不成。TCP连接建立后PC端断开时W5500会进入CLOSE_WAIT等状态FPGA侧一定要轮询Sn_IR里的断开标志发现断开后主动关闭socket并重新监听否则设备端会卡在旧连接上PC端重连不上。5.5 快速自查清单现象优先排查项说明读寄存器全FF/00RSTn电平、SCS极性、SPI模式、接线用逻辑分析仪抓波形对比手册PHY链路不建立25MHz晶振、PMODE、网络变压器LINK位可读PHYCFGR查看数据错位SPI采样沿、SCLK频率降频测试检查CPHA配置UDP偶发丢包接收缓冲区大小、接收轮询频率必要时加大单socket缓冲配额TCP握手失败Sn_CR等待、网络参数、ARP检查协议类型、网关配置发送卡死Sn_CR未清零、发送缓冲区满命令后必须轮询加超时保护6. 项目心得与扩展方向这趟FPGA W5500的实践给我最大的体会是在FPGA世界里不是所有事都要用FPGA的方式“硬解”。W5500把网络协议栈封装成一颗SPI外设相当于把最消耗开发精力的协议部分变成了标准零件FPGA工程师可以把精力集中在自己真正擅长的高速采集、实时控制、信号处理上。实际项目里我后续又基于这套方案做了两个扩展一是用W5500的第二路socket同时跑UDP和TCP一路做实时状态广播一路做配置指令下发二是把W5500的中断引脚用起来代替轮询中断事件触发FPGA内部的事件处理状态机CPU占用和SPI带宽都更省。如果接下来想继续深挖方向可以是把这套SPI控制器封装成可复用的IP核方便其他项目直接调用或者研究一下W5500的重传参数对TCP恶劣网络环境的适应能力在工控场景下做更细的调优。总之这趟从协议栈泥潭里爬出来的经历让我以后遇到网络通信需求时第一反应不再是“自己写协议”而是先评估有没有成熟、可靠、能把复杂度挡在外面的方案。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑