ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

FPGA从零实现以太网UDP通信:RGMII接口与协议栈实战解析

FPGA从零实现以太网UDP通信:RGMII接口与协议栈实战解析 先聊点题外话。这个系列写到第7篇前面几篇分别折腾过LED、按键、UART、状态机、FIFO和时序约束都是相对“稳定”的内容。但到了网络通信这一节很多人的心态会突然从“轻松玩票”变成“严肃工程”因为网络不是单板上的一个外设它是两个甚至多个系统之间的协议协作出问题时不仅查不到老半天还往往搞不清是硬件问题还是协议问题。也正是这个原因我决定把这部分单独拿出来结合我自己从零开始做FPGA以太网通信的完整过程把里头涉及的设计思路、协议裁剪、代码组织、调试验证按“近似0基础也能看懂”的方式讲清楚。这篇文章不会堆full协议栈也不会把TCP的拥塞控制展开给你看。我们聚焦一个很实际的目标给你一块入门级FPGA开发板和一台电脑让FPGA能通过网线把数据包发出去也能把电脑发来的数据包收进来而且我们明确知道每一比特走到哪一步了。这件事一旦走通后面做图像传输、音频流、采集系统与上位机交互全都能顺着这个底子往上搭。1. 内容整体设计与思路拆解1.1 为什么网络通信让很多FPGA初学者卡壳网络通信和UART这类接口最大的区别在于UART只有一条数据线只要约定好波特率、停止位、校验位双方就能通信。但以太网是一整套分层协议从物理层的电平标准、时钟沿到数据链路层的帧头、MAC地址、CRC校验再到网络层的IP地址、传输层的端口号。每一项协议字段都有它的存在意义你少填一个、错一个字节对方网卡要么直接丢弃要么解析出来的信息完全不对。初学者最容易犯的错误是拿“串口思维”去套网络觉得只要把数据丢给某个模块它自己就能发到对方电脑上。其实FPGA里没有现成协议栈你要做的就是按协议规定的格式一级一级把数据“摆”对。这个过程需要清晰的时序概念还要能读懂波形图而这两点恰恰是初学者在刚起步时最薄弱的地方。1.2 确定“最小可行设计”的目标范围要入门就必须给设计划定边界不能一上来就做TCP。用我自己的经验来说第一次做FPGA网络通信目标不要超过UDP甚至可以先从“FPGA发固定数据电脑用网络调试助手收包”这一步开始跑通了再倒过来做“电脑发数据FPGA收包并解出有效数据”。为什么这样定因为UDP是IP层之上的传输层协议它没有TCP那种三次握手、四次挥手、序号确认的过程相对简单而ARP这个看似底层的协议又必须在开局阶段处理妥当否则你连目标MAC地址都不知道往哪儿填。把目标定义为“能实现UDP收发、能响应ARP请求”这个范围足够小也足够覆盖后续大部分开发场景。1.3 方案选型硬件上用什么、接口上走什么现在市面上的入门级FPGA开发板板上PHY芯片主要有两种一种是老牌的RTL8201另一种是国产的YT8531或类似型号。不管用哪种它们的硬件接口都分成MII、RMII、RGMII、GMII这几种。为了兼顾引脚数量和性能现在中低端开发板最常引出的是RGMII接口时钟频率125MHz用DDR方式在时钟上下沿各采4位数据组合起来一个时钟周期能传8 bit。这个方案对入门和进阶都友好资源占用也不高我建议你把“基于RGMII接口的UDP通信”作为第一次网络学习的目标。1.4 FPGA和软件工程师写网络代码的思维差异这里我必须多说一句。写过C语言的软件工程师转过来做FPGA网络通信最容易踩的坑是把“流程”当成“状态机”。在CPU上你写一个socket系统帮你处理各种超时重传、缓冲区管理但在FPGA里一切都是并行和时序的逻辑协议栈里的每个步骤都需要你自己拆成状态机里的一个状态而且每个状态转移都伴随明确的时序条件。你要学会“像硬件一样思考”你的数据不是一批批处理的而是一拍一拍流动的。这个观念转不过来后面读代码、写代码都会觉得别扭。2. 核心细节解析与实操要点2.1 以太网帧结构的逐字段拆解一个标准的以太网帧从底层看长这样前导码Preamble7个字节的0x55用于接收端恢复时钟同步。帧起始定界符SFD1个字节的0xD5表示帧数据从这里正式开始。目的MAC地址6字节接收方网卡用它判断这个帧是不是发给自己的。源MAC地址6字节告诉接收方这个帧从哪里来。长度/类型字段2字节。小于1536时表示数据长度大于等于1536时表示上层协议类型常用值是0x0800IPv4。数据区46到1500字节。不够46字节时要在尾部填充这是很多人忽略的细节。帧校验序列FCS4字节用CRC32校验帧内从目的MAC到数据区结尾的所有字段。我在第一次写MAC发送模块时前导码和SFD是在FPGA内部按固定值生成的MAC地址和类型字段则来自配置寄存器或手动指定的参数。这里有个常见的心理障碍网络调试助手和Wireshark本身不会显示前导码和FCS所以初学者常误以为前导码不需要发送。其实PHY内部有时会自动帮你处理这部分但并不保证每个PHY都会这么做所以设计时要把“从发送端MAC是否带前导和FCS”这个配置搞清楚避免数据发到线上后发现多了几个字节或者少了几个字节。2.2 MAC层发送状态机的设计思路FPGA接收PC发来的数据并回发一个响应整个过程核心就是状态机。我习惯把发送路径设计成如下几个状态IDLE等待发送请求FIFO为空时停在这里。Preamble连续发送7个0x55。SFD发送0xD5。Header按顺序发送目的MAC、源MAC、类型字段。Data从FIFO读出数据并逐个字节发送。Pad如果数据长度不足46字节补零到46字节。FCS计算并输出CRC校验值。IFG发送帧间间隙Inter-Frame Gap至少保持96 bit时间即12个字节周期的空闲。我用一个计数器来记录当前帧已经发送了多少字节这个计数器既能用做状态切换的依据也能在Data状态判断是否已经发完Data。这里要特别注意CRC的计算范围是从目的MAC地址的第一个字节开始到数据区的最后一个字节结束不包括前导码和SFD这一点写代码时极其容易弄错因为从直觉上你会觉得校验应该覆盖整个帧。2.3 接收方向怎么从字节流中找回帧的边界接收方向相对发送方向更难因为线上信号是连续的你没有“请求”这个信号来告诉你一帧从哪里开始。接收状态机要做的第一件事是检测前导码和SFD。在RGMII接口下每个时钟周期采样两个字节上下沿各一个所以检测逻辑要放在双沿数据转换成单沿字节流之后。检测到SFD之后就开始按字节接收同时把接收到的每个字节都送入CRC计算模块直到检测到帧结束条件。帧结束条件的判定有两种方式一种是根据长度字段知道数据区有多少字节数到就停另一种是检测到FCS字段之后停止存储。实际设计中我更习惯把这两种方式结合先以长度字段为准决定“有效数据区”的范围紧跟着的4个字节作为CRC值接收并拿来和本地计算值比对。若一致就置一个帧有效信号若不一致则丢弃整帧并置错误标志。MAC地址过滤也在这个阶段做如果目的MAC与本地MAC不一致而且不是广播地址FF-FF-FF-FF-FF-FF或多播地址就可以直接丢弃省去后续解析的麻烦。2.4 关于CRC32的常见误区以太网用的CRC32算法校验结果有固定的输出异或值、初始值和多项式。网上很多代码直接照抄初始化值写成0xFFFFFFFF最终异或值写成0xFFFFFFFF这没问题。但关键点是在发送端要把计算出的CRC取反按位取反再放到帧尾在接收端把包括FCS在内的整个帧拿来做CRC如果结果为0x2144DF1C就表示校验通过。这个“魔法数”有很多人不知道导致自己写的参考模型一直比对不上。具体实现时可以用查表法也可以用逐bit状态机FPGA上更推荐用LFSR结构的并行CRC。我在写并行CRC时用的是从网上找的CRC Generator工具生成的RTL代码然后先用纯仿真验证再上板验证不要直接信手写代码的结果。3. 实操过程与核心环节实现3.1 RGMII接口的收发时序在开始撸代码之前必须先理解RGMII信号的引脚含义。一个典型的RGMII接口有四组关键信号TXC发送时钟125MHz源同步时钟和TXD[3:0]对齐。TXD[3:0]发送数据线上升沿发低4位下降沿发高4位。TX_CTL发送控制信号上升沿表示TX_EN下降沿表示TX_EN与TX_ER的异或。RXC接收时钟也是125MHz由PHY恢复出来。RXD[3:0]接收数据线同样在上下沿各采样4位。RX_CTL接收控制信号上升沿表示RX_DV下降沿表示RX_DV与RX_ER的异或。在FPGA端如果直接用IDDR原语去采RGMII信号需要把数据位宽从4位扩成8位。Xilinx的7系列里IDDR和ODDR是标准原语在Vivado里可以通过Language Templates直接插入。我实际使用的是IDDR #( .DDR_CLK_EDGE(SAME_EDGE_PIPELINED) ) rx_data_ddr[3:0] ( .Q0(rxd_int[0]), .Q1(rxd_int[1]), .C(rxc_int), .CE(1b1), .D(rxd[0]), .R(1b0), .S(1b0) );“SAME_EDGE_PIPELINED”模式的意思是Q0和Q1会在同一个时钟沿之后同时有效这样下游逻辑只需要用一个125MHz时钟就能处理8位宽的数据。这个细节很关键因为你如果选择默认的OPPOSITE_EDGE模式会得到两个交替有效的4位数据处理起来要额外用一个字节装配状态机代码复杂度和出错的概率都会明显上升。3.2 MAC发送模块的代码结构示例下面这段代码是我实际项目里发送状态机的一段核心骨架截掉了寄存器配置和FIFO接口部分只保留核心逻辑思路localparam ST_IDLE 4d0; localparam ST_PREAM 4d1; localparam ST_SFD 4d2; localparam ST_HEADER 4d3; localparam ST_DATA 4d4; localparam ST_PAD 4d5; localparam ST_FCS 4d6; localparam ST_IFG 4d7; always (posedge clk125) begin if (!rstn) begin state ST_IDLE; end else begin case (state) ST_IDLE: begin if (fifo_empty 1b0) begin state ST_PREAM; tx_byte_cnt 0; crc32_init 1b1; end end ST_PREAM: begin if (tx_byte_cnt 4d6) begin tx_byte_cnt 0; state ST_SFD; end else begin tx_byte_cnt tx_byte_cnt 1b1; end end ST_SFD: begin state ST_HEADER; tx_byte_cnt 0; end ST_HEADER: begin if (tx_byte_cnt 4d13) begin // 662-1 tx_byte_cnt 0; state ST_DATA; end else begin tx_byte_cnt tx_byte_cnt 1b1; end end // ... 后续状态类似 endcase end end发送的数据来源应该是一个异步FIFO逻辑侧比如100MHz或150MHz时钟域往FIFO里写要发送的数据发送状态机所在的125MHz时钟域从FIFO读数据。FIFO的读使能信号只在ST_DATA状态且当前字节有效时拉高一个周期这个信号同时作为数据有效标志给到CRC计算模块。3.3 接收模块与CRC校验接收模块的设计步骤可以拆成四段第一段做RGMII的IDDR采样把4位DDR数据转成8位SDR数据第二段做字节对齐和前导码检测第三段是主状态机负责把有效数据写入FIFO第四段是CRC校验器边接收边计算。接收状态机中有一个非常细节的地方RGMII在复位期间或未接收数据时RXD和RX_CTL信号并不是稳定的常量可能在整个接口上跑来跑去甚至出现毛刺。所以你不能看到RXD出现0x55就断定这是前导码一定要同时检测RX_CTL是否有效即RX_DV为高。通常做法是在RX_CTL上升沿且值为1时才开始检测前导前导检测到至少3个字节的0x55且随后出现0xD5才确认一帧开始。这可以大幅减少噪声干扰导致的误触发。CRC校验模块用标准的以太网CRC32多项式0x04C11DB7采用并行计算方式每周期处理8位。我建议直接用开源工具或在线生成器生成Verilog代码然后加一个用比特串行方式写的参考模型在仿真里把两个模型的输出逐周期比对这样能保证并行版本没有逻辑错误。3.4 时间戳和跨时钟域处理FPGA内部多数逻辑工作在非125MHz的时钟域比如100MHz或150MHz。MAC层收发模块工作在125MHz。这中间必须有FIFO做跨时钟域隔离。收发方向各用一个独立的异步FIFO。这里我踩过一个坑异步FIFO的读写指针同步不能直接打两拍就完事需要在设计时确认FIFO的格雷码处理是否正确。Xilinx自带的FIFO IP核是最稳妥的不要为了炫技自己写异步FIFO除非你真的能把指针同步、空满标志生成的所有边界情况都想清楚。数据帧从逻辑层进入MAC发送FIFO之前还需要完成UDP和IP头的封装。这一步通常可以分两段完成先用一个“头生成模块”在FIFO里预先写入IP头、UDP头再把上层用户数据写入FIFO。这个顺序能保证MAC发送模块只需要从头到尾逐字节读FIFO不需要跳变位置。如果你想精简逻辑甚至可以在一个状态机里边生成头部边读用户数据但代码复杂度会明显增加。4. 步入UDP通信从ARP开始实现4.1 为什么必须先处理ARP当你把板子和电脑用网线直连然后打开上位机软件向FPGA的IP地址发送UDP数据包时你可能会发现第一个发送出去的不是UDP包而是ARP请求包。电脑的协议栈在不知道FPGA网卡MAC地址时会先广播一个“谁是192.168.1.10请告诉你的MAC地址”的ARP请求然后等待回应。如果FPGA没有实现ARP应答电脑就一直无法完成到IP层的映射UDP数据根本不会发生。所以在做UDP之前先实现一个精简的ARP应答模块是必须的。这个模块做三件事识别ARP请求判断目标IP是否为本机IP是则回一个ARP应答。4.2 ARP应答状态机的细节实现ARP应答的以太网帧格式是目的MAC为请求方MAC源MAC为本机MAC类型字段为0x0806ARP数据区里操作码为2表示应答发送方IP、MAC填本机信息目标IP、MAC填请求方信息。这里要强调一个非常容易忽略的点ARP应答帧的填充区需要补到46字节最小帧长。因为ARP数据区只有28字节加上14字节以太网头总共42字节不满足46字节最小以太网帧数据区要求所以尾部要填充4个字节的0。很多资料里的示例代码没有做这个填充实测中有些交换机和电脑网卡也能容忍但严格按标准来收到未填充帧时某些仪器会报错误帧。ARP请求包还能起到“被动学习”的作用收到PC发来的ARP请求后可以顺便把请求方MAC、IP存到一个寄存器里后面发送UDP数据时如果已知对方IP就能直接查表用对应MAC。如果没有表项就得在发送UDP前先发一个ARP请求去询问这又是一个小的状态机。4.3 UDP数据报的封装流程UDP包封装在IP包内IP包再封装在以太网帧内。在我的设计里UDP发送流程可以简单用以下步骤实现上层模块产生一个发送请求并给出目的IP、目的端口、数据长度、源端口。UDP发送模块读取目的IP查询本地维护的ARP表若查不到则先返回一个忙信号启动ARP查询。查找到MAC后开始填充以太网头目的MAC、源MAC、类型0x0800。填充IP头版本号4、IHL为5、总长度设为208数据长度、标识、片偏移为0、TTL为64、协议号17UDP、头部校验和。填充UDP头源端口、目的端口、长度为8数据长度、校验和为0IPv4下UDP校验和可选实用中建议先置0。数据区顺序写入FIFO。启动MAC发送模块发送整个帧。整个过程中我建议把IP头和UDP头的长度、校验和计算预先用参数算出结果省去在状态机里做加法的麻烦。尤其IP头校验和很多人第一次写都会在心里打鼓其实它的算法就是“把IP头按16位为单位累加回卷进位最后取反”。一帧长度1500字节时这个计算在PC上瞬间就能完成在FPGA里如果不想写纯组合逻辑也可以做一个简单的补数累加器分两三个周期算完。4.4 UDP接收方向的拆包接收方向做拆包时需要注意帧到达后不能确定它一定是UDP因此接收解析模块要判断以太网类型字段是否为0x0800。IP头中的协议字段是否为17。目的IP是否为本机IP或广播地址。目的端口是否为本机绑定的端口。以上任一条件不满足就丢帧满足条件才把UDP数据区的有效字节写入用户FIFO同时给出一个“包有效”的脉冲信号。拆包过程中有个很容易出错的地方IP头长度字段是IHL单位为4字节一般固定为5表示20字节。UDP数据区长度 UDP长度字段值 - 8UDP长度又可以通过IP总长度字段减掉IP头长度得到。实际使用时我建议直接使用UDP长度字段来计算数据区长度少做一次间接换算减少出错机会。5. 常见问题与排查技巧实录5.1 用Wireshark验证“数据是否真的发出去了”初学者常在FPGA侧或网络调试助手上找问题却忘了在电脑上用Wireshark抓包这是最直接也最有效的定位手段。如果你在Wireshark上能看到FPGA发出的UDP包但解析不出来协议类型通常是MAC地址或类型字段填错如果能看到帧但CRC错误多半是CRC计算范围或输出极性有误如果连帧都看不到就很可能是PHY配置或RGMII时序有问题。用Wireshark时还有一个职业习惯建议过滤表达式直接写udp ip.src192.168.1.10不要用eth.addr去过滤因为你电脑和FPGA直连时可能会收到一堆来自其他协议栈的乱帧尤其是开启了系统自带网络服务的时候。5.2 收不到包从PHY状态开始逐级检查如果你确认发送方向已经没有问题但FPGA始终收不到电脑发出的数据包按照下面这个顺序排查会快很多用万用表或逻辑分析仪确认PHY的Link状态是否正确用网线直连时PHY的Link指示灯是否点亮。检查RX_CLK是否有125MHz稳定时钟输出如果没有说明PHY的配置有问题重点检查PHY地址配置引脚和时钟输入。用Vivado的ILA核抓RX_CTL信号看是否有RX_DV的脉冲出现。如果RX_DV一直是低说明PHY根本没有检测到有效数据问题多半在网线、对端设备或PHY配置。抓RXD信号看是否有前导码0x55和SFD 0xD5。如果有但MAC没有产生有效帧信号说明你的接收状态机识别逻辑有bug。实际测试中我遇到过RX_DV有脉冲但RXD数据一直不对的情况最后发现是PHY的RGMII工作模式没有配置正确把RGMII模式初始化成了MII模式导致上下沿采样逻辑完全错位。5.3 CRC不过的常见原因整理CRC错误在以太网调试中非常常见把经验归纳如下症状可能原因排查方向所有包都显示CRC错误CRC计算范围错误或输出极性反了检查FCS计算模块是否只覆盖硬件头数据区偶发包CRC错误数据FIFO时序问题导致个别字节读取错误抓FIFO读使能和数据信号检查空标志是否处理正确帧长固定偏大发送了前导码但没有去掉或IFG过短对照标准检查发送状态机状态数量固定偏小填充区没有补零确认PAD状态是否按需启用5.4 用ILA定位跨时钟域问题的独门心得ILAIntegrated Logic Analyzer是Xilinx FPGA调试的利器但很多人用不好它。我有一条真实经验不要一开始就把信号全部抓上先抓最少的必要信号。比如排查发送问题时先只抓tx_ctl、txd、tx_clk和FIFO读使能这几个关键信号把状态机当前状态也作为信号一起抓。跑一次抓包分析问题改代码再跑一次。我见过太多人一次性抓几十路信号结果数据的深度设置不够关键波形早就被覆盖了回头重新抓浪费了大量时间。另外ILA的触发条件值得多用。比如接收UDP时可以设置uart_state ST_DATA作为触发条件这样抓到的数据就是你要分析的片段而不是从头到尾一大块无意义的内容。5.5 板级调试过程中的环境坑实际板级调试中环境问题比代码问题更容易让人抓狂。我用过的RTL8201和YT8531都遇到过网线质量问题劣质网线在百兆模式下可能正常工作但你的PHY如果工作在千兆模式同一根网线可能完全不通。这时候可以先强制PHY为100M模式检查有没有改善能快速排除网线问题。还有一个容易忽视的因素是电源纹波。FPGA和PHY共用一组电源、且开关电源纹波较大时可能会造成RGMII接口在高速翻转时出现随机错误错误帧率不高但会周期性出现。这个问题在代码层面怎么查都查不到最后用示波器看PHY电源引脚才发现纹波到了100mV以上补了几颗去耦电容就稳定了。所以当你的调试陷入“完全符合逻辑但就是不通”的怪圈时先量一下电源和参考时钟往往比反复改代码更有效。6. 经验总结与扩展建议6.1 我强烈建议你按这个顺序跑通第一个回环例程如果你现在正准备开始动手我建议不要按“先写MAC层、再写ARP、再写UDP”的结构去写而是设计一个从简单到复杂的递增方案第一步用电脑网络调试助手向开发板发送任意UDP数据FPGA只负责把收到的数据原封不动地再发回电脑回环模式。这个阶段先把MAC收和MAC发全部跑通但不涉及ARP和IP校验。第二步在回环模式的基础上把电脑发送的ARP请求抓出来回一个硬编码的ARP应答确认电脑能学到FPGA的MAC地址。第三步去掉回环模式用固定源和固定目的封装UDP数据让FPGA主动向电脑发包这时能验证你的UDP发送封装逻辑。第四步实现接收拆包把上位机发来的数据长度和内容回发或显示形成完整双向通信。这四步每完成一步都有明确的可观测结果方便定位错误。我甚至建议每步都新建一个Vivado工程并将之前的模块复制过去而不是在同一工程里不断改因为网络调试最容易出现“改回来又改回去”的混乱多个工程快照反而有助于对比出问题。6.2 下一步还能扩展什么把UDP收发跑通之后可以继续往上扩展的方向很多。最常见的是把UDP有效数据加载到DDR3或BRAM再配合摄像头接口做实时图像传输这套组合在工业视觉、桌面机器人、数据采集仪中非常常见。如果想深入技术细节可以看看TCP的状态机设计很多FPGA TCP实现会省略拥塞控制只保留滑动窗口这需要更强的协议理解。还有一条路线是PQRI或TSN这类面向时间敏感网络的实现虽然门槛高但是方向很新也很有价值。从我的个人体会而言FPGA网络通信最大的门槛不在于某个具体协议有多难而在于你要学会一层一层地“手动维护协议状态”。只要你愿意沉下心从最底层的一根线开始验证用规范的方式去拆解每一个字段整个过程其实非常有成就感——当你在Wireshark里看到自己设计的板子发出的完全合法的UDP包时那种“这网络上的一帧是我用逻辑拼出来的”的感觉是软件工程师很难体验到的。这个内容后续还可以扩展的方向非常多我目前正在把自己的UDP基础框架往“多通道高速ADC采集千兆以太网实时上传”方向改等做通了再来分享具体实现细节。
返回列表