拓冰建站拓冰建站
首页 / 资讯中心 / 正文

FPGA实现UDP协议栈设计实战:从协议格式到状态机与RGMII调试

做FPGA开发做到第八个Part终于聊到网络通信这一步迈出去前面攒的那点时序基础、状态机感觉、模块划分经验全部都能派上用场。UDP模块之所以值得单独写一篇是因为它几乎是FPGA接入网络的“最低门槛协议”——不需要维护连接状态、没有ACK重传机制逻辑上就是把数据包按格式封装好丢出去或者按格式收下来剥开。对搞FPGA的人来说UDP能在不需要CPU参与的情况下完成高速数据传输延迟可以做到微秒级甚至更低图像采集、ADC数据回传、运动控制指令下发这些场景全靠它撑场面。这篇就围绕“从近似0基础开始做UDP模块代码设计”展开我会从协议格式拆解、模块划分、状态机设计、关键代码骨架到仿真和板级调试把整个链路讲明白。目标读者是已经能写简单状态机、能跑通UART或SPI这类基础外设的人正在纠结“要不要碰网络协议栈”“怎么在FPGA里处理MAC/IP/UDP头”这个坎的看完这篇能少走很多弯路。1. UDP协议基础与FPGA侧的设计取舍1.1 为什么偏偏要在FPGA里自己搭UDP协议栈很多刚接触网络通信的人第一反应是FPGA这玩意儿还能跑协议栈是不是得在ZYNQ上跑Linux然后调用socket这个思路没问题但并不是唯一方案而且对于追求低延迟、高确定性的场景跑Linux软协议栈反而吃力。FPGA里自己实现UDP的好处很直接数据从PHY芯片进来在纯硬件逻辑里完成MAC帧解析、IP层校验、UDP端口匹配然后直接交给下游逻辑整个过程不需要CPU排队、不需要操作系统调度、不存在缓存拷贝。我做过的一个图像传输项目里FPGA从Sensor拿到的图像数据经过简单打包后以UDP方式在万兆网卡上跑到线速端到端延迟稳定在几微秒级别这在纯CPU方案里几乎不可能做到。当然“自己搭协议栈”听着吓人但UDP协议栈其实很薄。以太网帧格式、IPv4头部、UDP头部加起来不到60字节剩下的事情就是“填字段”和“拆字段”。没必要自己从零写一百个文件我的做法是只做必要子集支持IPv4、支持UDP、支持最基本的ARP静态映射硬件逻辑里不处理TCP、不处理IP分片、不做DHCP。这些裁剪能让代码量骤减又能覆盖绝大多数局域网通信场景。反过来说如果项目要求复杂组网、跨网段、动态IP那确实老老实实上ZYNQ跑Linux更合适不要在FPGA纯逻辑里硬扛。还有一点纯逻辑UDP的“不可靠”特性你要提前接受。以太网本身就是尽力而为的物理链路抖一下、交换机队列满了包就丢了。FPGA里不会自动重传所以应用层必须能容忍丢包或者自己加序号和重传机制。这个逻辑要在整体方案设计时想清楚否则后面调试起来心态容易崩。1.2 三个层的数据格式一次吃透做UDP模块前先把三个层的数据格式背熟以太网帧头、IP头、UDP头。这三层是层层套嵌的关系数据在链路上实际发送的是最外面的MAC帧。以太网帧Ethernet II从目的MAC开始字段长度说明前导码 SFD8字节同步用前7字节0x55最后1字节0xD5目的MAC6字节目标网卡地址源MAC6字节本机网卡地址EtherType2字节IPv4填0x0800ARP填0x0806Payload46~1500字节IP包整体塞在这里FCS4字节CRC32校验前导码和FCS通常是PHY或MAC核处理的自己做纯逻辑时如果直连PHY芯片需要自己生成和检查。从FPGA发出去的帧最简单的方式也是把这些字段全按字节送到数据总线上。IP头IPv4最短20字节主要关注这些VERS: 4bit固定为4IHL: 4bit标识头部长度无选项时填5表示20字节Total Length: 16bitIP头UDP头数据的总字节数Protocol: 8bitUDP填17源IP、目的IP: 各4字节Header Checksum: 16bit只对IP头做校验UDP头更简单源端口、目的端口各2字节Length字段2字节表示UDP头数据的长度Checksum 2字节在IPv4下可以填0不校验但某些网络环境下系统会强制校验为了稳妥我一般会计算。关键点链路层会限制IP包最大1500字节包括IP头所以UDP数据长度最大是1472字节1500减去20字节IP头再减去8字节UDP头。超过这个值就要IP分片而很多FPGA侧的UDP实现不处理分片所以应用层发送数据时最好把包控制在1472字节以内。1.3 收发包的整体数据通路FPGA做UDP收发的思路本质就是“串行流水线拆包/组包”。我以常见的1Gbps RGMII接口PHY为例描述整体数据通路逻辑上同样适用于百兆网。接收方向PHY芯片把差分信号还原成4bit或8bit的并行数据和数据有效标志FPGA内部先做RGMII转GMII如果是1Gbps双沿采样得到8bit数据总线。然后MAC接收状态机识别帧头逐字节往下走先比对目的MAC地址和广播地址再看EtherType是否为0x0800随后解析IP头确认协议号是17再解析UDP头匹配目的端口号。这一串判断都通过后payload部分就很简单了——按字节写入FIFO或RAM供用户逻辑读取。发送方向则是逆过程用户逻辑准备好payload数据发送状态机先输出MAC头目的MAC、源MAC、类型再输出IP头再输出UDP头最后把payload数据从FIFO里读出来逐个字节发送。所有头字段在发送之前提前算好放到寄存器里发送时只是顺序搬移。整个过程不需要处理器干预靠状态机迁移和计数器的推进就能完成。模块化划分得好接收和发送是两条相对独立的通路中间用FIFO缓冲跨时钟域和速率差异。2. 模块划分与状态机设计2.1 顶层架构每个模块该管哪摊事代码设计之前先想清楚模块边界。我的UDP工程顶层一般长这样module top_udp ( input wire clk_125m, input wire rst_n, // RGMII 接口 input wire [3:0] rgmii_rxd, input wire rgmii_rx_ctl, input wire rgmii_rx_clk, output wire [3:0] rgmii_txd, output wire rgmii_tx_ctl, output wire rgmii_tx_clk, // 用户侧接口 output wire [7:0] user_rx_data, output wire user_rx_valid, output wire user_rx_last, input wire user_rx_ready, input wire [7:0] user_tx_data, input wire user_tx_valid, input wire user_tx_last, output wire user_tx_ready );这个顶层只做例化具体逻辑分四个模块mac_rxRGMII转GMII后的接收解析模块负责前导码检测、MAC地址过滤、CRC校验输出分开的IP包数据。mac_tx发送模块负责前导码和FCS生成、帧间隔控制接收从UDP封装模块传来的完整IP包并发送。udp_ip_rxIP和UDP头解析模块输入是来自mac_rx的IP包数据输出是剥离掉所有协议头后的payload数据。udp_ip_txIP和UDP头封装模块输入是用户payload数据输出是组装好的IP包数据。有些教程把IP层和UDP层再拆开逻辑上更清晰但对初学来说IPUDP合在一起写更省事因为UDP头就在IP头后面连续发送没必要拆成两级状态机。至于ARP我在模块里单独加了arp处理但允许通过参数关掉初学阶段用静态MAC也能跑。接收方向各级模块之间用valid/ready握手信号传递发包数据量不大时甚至可以用简单的FIFO断裂处理。我自己习惯是每个模块输入输出各挂一个小FIFO深度256就够这样模块和模块之间天然解耦不担心上下游状态机节奏不匹配。2.2 接收路径状态机从头到尾逐字节拆包mac_rx的接收状态机是整个UDP链路的“入口”设计时主要考虑几个状态等待前导码、接收数据、校验帧尾。localparam IDLE 3d0; localparam PREAMBLE 3d1; localparam RECV_DATA 3d2; localparam FCS_CHECK 3d3; localparam DROP 3d4;IDLE状态检测8字节前导码检测到最后一个字节0xD5后进入RECV_DATA然后开始数帧长。数据有效期间每收到一个字节data_valid拉高同时要把FCS做进来等最后一个字节结束data_valid拉低把收到的长度和CRC结果一起判断不通过就丢弃通过就把payload送出去。mac_rx提供的IP包数据和valid信号给到udp_ip_rx后udp_ip_rx再进一步解析。它的状态机围绕“当前处理的是协议头的第几个字节”展开最直接的做法是定义一个计数器reg [4:0] byte_cnt; always (posedge clk) begin if (rx_sop) byte_cnt 0; else if (rx_valid) byte_cnt byte_cnt 1; end然后按字节序号取字段。注意从mac_rx出来的数据流是“目的MAC 6字节 - 源MAC 6字节 - EtherType 2字节 - IP头 - UDP头 - payload”。EtherType不等于0x0800的帧直接丢弃IP头的IHL字段如果大于5有选项说明IP头不止20字节这时候UDP头从第20N字节开始初学阶段可以直接认为没有选项IHL写死5。payload输出的时机当字节计数器进入UDP头之后的第一个字节时开始输出给用户的FIFO一直持续到帧结束。如果目的端口不匹配过滤条件可以整帧丢弃不发payload。2.3 发送路径状态机把裸数据打扮成网络包发送路径其实是接收的“镜像”但要注意几个差异点。发送状态机按字节计数推进从IDLE开始先连续发8字节前导码0x55、0x55……0xD5然后发14字节MAC头接着20字节IP头8字节UDP头然后是用户payload最后补4字节CRC。状态定义大致如下localparam IDLE 4d0; localparam PREAMBLE 4d1; localparam MAC_HDR 4d2; localparam IP_HDR 4d3; localparam UDP_HDR 4d4; localparam PAYLOAD 4d5; localparam CRC 4d6;写发送逻辑最容易忽略的是时序配合。用户侧写FIFO的速率和PHY发送速率可能不同而且用户可能不是一次把整包数据准备好而是分多个周期写入。我的做法是先用一个FIFO把用户payload缓冲起来状态机进入PAYLOAD状态后按FIFO的读数据有效信号逐拍发送。发送结束条件是FIFO空了并且用户告诉我是最后一拍user_tx_last有效且FIFO读空。还有帧间隔IFGInter-Frame Gap标准是以太网帧与帧之间至少空闲96bit时间。如果你状态机从上一帧CRC发完立刻回到IDLE下一包数据又马上到中间没有空闲PHY或交换机可能丢包。我一般会在发送完CRC后强制等待12个时钟周期1Gbps下96bit就是12个8bit周期再回到IDLE。2.4 ARP模块能省就省但逃不掉UDP包封装时MAC头的目的MAC地址从哪来正常是ARP协议动态获取但初学阶段我不建议一上来就写ARP状态机会分散精力。最简单的做法是把目的设备的MAC地址在代码里写死。比如你上位机网卡的MAC是00:1B:44:11:3A:B7在发送模块里用一个参数固定下来。这样做在本机直连或小型局域网里完全够用。缺点是换电脑、换网卡就得重新综合一次比较麻烦。如果希望灵活一点可以写一个简化版ARP模块收到ARP请求时判断目标IP是否是本FPGA的IP如果是就回一个ARP应答。这样上位机发一个ping命令就能学到FPGA的MAC地址后续所有UDP包都会带上正确的目的MAC。这个模块不算难核心就是一个状态机根据收到ARP包里的字段构造应答包。我在第五小节会聊怎么扩。3. 核心代码实现要点与参数计算3.1 关于时钟、复位和数据位宽先解决两个容易劝退新手的坑RGMII时钟和复位。RGMII接口下PHY提供125MHz的接收时钟但数据是DDR方式在时钟上下升沿各传输4bit合起来一个时钟周期8bit。很多FPGA开发板的PHY芯片如RTL8211、88E1512工作在1Gbps时就用这种模式。FPGA内部要先例化IDDR原语把双沿数据转成单沿8bit代码里常见的写法是genvar i; generate for (i 0; i 4; i i 1) begin : gen_idata IDDR #(.DDR_CLK_EDGE(SAME_EDGE_PIPELINED)) u_iddr_rxd ( .D(rgmii_rxd[i]), .C(rgmii_rx_clk), .CE(1b1), .R(1b0), .S(1b0), .Q1(rxd_low[i]), .Q2(rxd_high[i]) ); end endgenerate接着用alternative /* synthesis keep */等方式把它们拼接成8bit。或者更省事百兆模式下不用DDR直接把4bit拼成8bit就行。初学阶段可以先用百兆模式把逻辑跑通再到千兆模式调时序。我在网上见过不少人卡在RGMII数据错位一整天最后发现是IDDR的SAME_EDGE和SAME_EDGE_PIPELINED模式理解错了。复位信号建议做异步复位同步释放保证释放时刻对齐时钟避免亚稳态。多个时钟域不要共用一个复位接收侧用rx_clk域复位发送侧用tx_clk域复位用户逻辑用系统时钟域复位。3.2 接收路径的代码骨架下面给出udp_ip_rx的关键逻辑完整框架关键判断点我细讲。module udp_ip_rx #( parameter LOCAL_IP {8d192, 8d168, 8d1, 8d10}, parameter LOCAL_MAC 48h00_11_22_33_44_55, parameter UDP_PORT 16d8080 )( input wire clk, input wire rst_n, input wire [7:0] ip_rx_data, input wire ip_rx_valid, input wire ip_rx_last, output wire [7:0] payload_data, output wire payload_valid, output wire payload_last, input wire payload_ready ); localparam [1:0] S_HEAD 2d0, S_IP 2d1, S_UDP 2d2, S_PAY 2d3; reg [1:0] state; reg [7:0] byte_cnt; reg [31:0] mac_dst; reg [15:0] ethertype; reg [15:0] total_length; reg [7:0] protocol; reg [15:0] udp_dst_port; reg rx_payload_en; // 核心判断逻辑 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state S_HEAD; end else begin case (state) S_HEAD: begin if (ip_rx_valid) begin if (byte_cnt 0) mac_dst[31:24] ip_rx_data; // ... 省略具体的MAC/ETYPE拼接 if (byte_cnt 11) begin if (ip_rx_data ! LOCAL_MAC[7:0]) state S_HEAD; end if (byte_cnt 13) begin ethertype {ethertype[7:0], ip_rx_data}; if ({ethertype[7:0], ip_rx_data} ! 16h0800) state S_HEAD; else state S_IP; end end end // ... 后续状态 endcase end end endmodule这段代码的核心思想是S_HEAD阶段收完14字节MAC头记录EtherTypeS_IP阶段解析IP头重点是确认协议号等于17以及拿到总长度S_UDP阶段解析目的端口S_PAY阶段才真正产生payload_valid信号。注意在S_HEAD阶段每次判断不匹配不要立刻跳回IDLE而是先把整包收完再丢弃否则后面数据来不及清理影响下一帧的同步。3.3 发送路径的代码骨架发送路径最关键的坑是“头字段预计算”。因为你不可能在发送每个字节的当拍再去累加校验和或者算长度那样时序肯定崩。正确做法是用户发起发送请求时先根据用户提供的payload长度算出IP总长度和UDP长度完成校验和把这些值锁存到寄存器里然后才开始状态机逐字节发送。wire [15:0] ip_length 8d28 payload_len; // 20字节IP头 8字节UDP头 wire [15:0] udp_length 8d8 payload_len; // 8字节UDP头 reg [31:0] ip_checksum; always (*) begin ip_checksum 32h0000; ip_checksum ip_checksum 32h4500; // 版本/首部长度 服务类型 ip_checksum ip_checksum {16d0, ip_length}; ip_checksum ip_checksum 32h0000; // 标识 ip_checksum ip_checksum 32h4000; // 标志片偏移 ip_checksum ip_checksum 32h4011; // TTL 协议 ip_checksum ip_checksum {16d0, LOCAL_IP[31:16]}; ip_checksum ip_checksum {16d0, LOCAL_IP[15:0]}; ip_checksum ip_checksum {16d0, REMOTE_IP[31:16]}; ip_checksum ip_checksum {16d0, REMOTE_IP[15:0]}; // 16bit折叠求和 ip_checksum (ip_checksum[31:16] ip_checksum[15:0]); ip_checksum (ip_checksum[31:16] ip_checksum[15:0]); end这段用组合逻辑算校验和等价于把IP头所有16bit字段加起来再取反。注意加法器的进位要折叠回低位这个步骤叫“16位累加和校验”手工算和代码算都要做别漏了。发送时逐字节按顺序输出头部字段我习惯用case语句根据byte_cnt直接输出对应字节// 发送IP/UDP头 always (*) begin case (byte_cnt) 8d14: tx_data 8h45; // IP版本IHL 8d15: tx_data 8h00; // DSCP/ECN 8d16: tx_data ip_length[15:8]; 8d17: tx_data ip_length[7:0]; // ... 8d33: tx_data 8h00; // 源端口高8位 8d34: tx_data 8h00; // 源端口低8位假设端口5000 // ... default: tx_data 8h00; endcase end发送payload时从FIFO读数据FIFO空则插入等待状态。这一段的时序相对简单只要在FIFO读valid为高时发送数据、读last为高时置帧结束标志即可。3.4 CRC32计算两种思路别混淆以太网FCS用的是CRC32生成多项式是0x04C11DB7标准以太网多项式注意字节序反着算。初学阶段最不容易绕明白的是数据按字节进入CRC移位寄存器的初值是全1数据流从最高位算起计算完后再取反输出。很多网上的Verilog示例是用查表法按字节处理代码长但效率高。// 按位计算直观但慢 always (posedge clk) begin if (crc_en) begin crc_next crc_reg; for (int i 0; i 8; i i 1) begin feedback crc_next[31] ^ data_in[7-i]; crc_next {crc_next[30:0], 1b0}; if (feedback) crc_next crc_next ^ 32h04C11DB7; end crc_reg crc_next; end end发送侧的计算相对麻烦因为CRC需要覆盖从目的MAC到payload的最后字节发送完成后紧接着送出4字节CRC并且字节顺序要“反序”CRC结果按字节逆序发送。如果你做的是百兆以太网发送数据是4bit的NibbleCRC的计算方式还不太一样。这几年我吃过最多的亏就是CRC模块仿真没问题上板抓包显示“FCS error”最后发现是对外发送时没有把CRC结果按字节反序、也没有取反。不会算CRC也有一招把CRC计算器的启动延迟到MAC头第一个字节而不是帧前导码然后接收侧可以只做“帧长检查”不做“CRC检查”先把功能跑通再补校验细节。很多开发板自带的UDP例程其实不查CRC一样能跑因为你面对的不是去硬核交换机而是直连电脑操作系统网卡驱动对FCS错误容忍度较高但严谨的项目不能这么偷懒。4. 仿真验证与板级调试4.1 仿真激励怎么搭最有效刚开始学写UDP仿真比上板好调得多因为能精确控制输入时序还能用文本方式把构造的数据包写出来。testbench的套路很固定生成125MHz时钟拉高复位然后按照时序往rx数据线灌入一个预先构造好的以太网帧。这个帧可以用任务task逐字节发送task send_byte; input [7:0] data; begin (posedge clk); valid 1b1; data_out data; (posedge clk); valid 1b0; end endtask然后按顺序调用先发8字节前导码0x55 x7 0xD5接着目的MAC00:11:22:33:44:55源MACDE:AD:BE:EF:00:01EtherType 0x0800然后20字节IP头8字节UDP头最后数据内容。仿真时最值得关注几个信号状态机是否按预期跳转、payload_valid是否在正确位置拉高、CRC最终值和预期是否一致。建议用$display或者直接在波形里抓状态机状态寄存器像看动画一样看数据流过各个阶段。我见过不少新手仿真通过但上板失败一个原因是仿真里使用了不可综合的initial赋值解决方法是testbench里用task驱动但DUT内部全部用同步时序。4.2 板级调试三板斧回环、打流、抓信号仿真通过只是第一步。上板调试我一般按三步走。第一FPGA收到PC发来的UDP包后原样回发给PC。这个回环测试只要上位机发送一个UDP包到目标端口FPGA把收到的payload重新打包发回去PC收到内容一致就说明MAC/IP/UDP收发全链路通了。成本最低推荐先做。第二用网络调试工具或Python脚本连续发包比如每秒发几百个包FPGA每个包把收到的数据累加以后再回发。这样既验证了收包的稳定性没有漏帧、没有错位又验证了回包的响应能力。第三ILA逻辑分析仪抓内部信号。当遇到“PC能收到包但内容不对”这种诡异问题时信号分析器是唯一的依靠。我会在udp_ip_rx模块内部挂上ILA触发条件设置为payload_valid上升沿然后观察byte_cnt、状态state、数据data等信号在触发的瞬间是什么值基本能定位是头部解析错位还是payload拼接出错。4.3 常见问题排查实录整理一份排查表按“症状-可能原因-解决办法”列出来能省下很多求助时间症状可能原因处理方法PC发UDP包FPGA完全收不到目的MAC不对/端口不匹配/帧被丢弃先用回环测试排除链路用ILA抓mac_rx输出确认数据是否进入FPGA核对MAC过滤逻辑FPGA发出的包PC收不到发送状态机卡死/IFG没满足/MAC头错误Wireshark抓包看是否有错误帧用计数器确认tx_valid脉冲数量检查发送FIFO读空逻辑回环测试数据错位RGMII数据位序/字节序错误对比字节计数器和期望字段检查IDDR的Q1/Q2拼接方向尝试在接收侧做“字节交换”测试IP校验和报错IP头字段计算错误手工用wireshark里的checksum验证工具对比确认TTL/协议字段和长度字段填对偶发性丢包用户FIFO深度不足/发送FIFO拥塞增大FIFO深度检查握手信号是否有反压确认用户逻辑是否在payload_last后才拉低valid我重点说一下“RGMII字节序”这个坑。RGMII对4bit数据在上升沿和下降沿采样的顺序有明确规定但不同PHY的命名和文档表达各有差异很容易把高低Nibble拼反。如果PC收到的UDP包内容一坨乱码先不要怀疑上层逻辑把FIFO里收到的raw字节和Wireshark里的十六进制对比如果每个字节的两个Nibble对调了那就是RGMII拼接那行代码写反了。还有一个特别容易被忽略的点发送侧CRC错误可能不是CRC模块错了而是你忘了在CRC输出前插入一个时钟周期的空闲。以太网FCS是紧跟在数据后的但如果数据总线在帧尾之后仍然输出高阻或0PHY会把多余字节也算进FCS范围导致校验失败。发送完payload后next状态必须是CRC状态其他状态一律不驱动数据线。5. 性能优化与进阶扩展5.1 吞吐还能再提速吗把基础通路跑通后很多人开始关心带宽。1Gbps下8bit数据总线需要125MHz时钟这是RGMII的极限带宽如果PHY配置为百兆则只有12.5MB/s差别很大。如果你的数据源是高速ADC或图像SensorByte-by-Byte处理可能成为瓶颈。换用32bit数据总线可以把逻辑时钟降到31.25MHz时序压力小很多代价是跨时钟域和位宽转换电路复杂一些。实际上大部分FPGA里UDP的吞吐瓶颈不在收发状态机而在FIFO深度和用户侧数据写入速率。深度不够时上游数据阻塞包只能丢掉这是初学最容易忽略的“隐藏瓶颈”。还有一个小优化不要在发送状态机里对每个字节做复杂的判断逻辑把能预计算的值全部提前放到寄存器状态机内只做计数器递增和数据搬移。这样能有效提升时序收敛质量让综合工具把关键路径优化得好一点。我见过同一个工程优化前310MHz都不能收敛优化后350MHz轻松过差别就是状态机里组合逻辑链条的长短。5.2 从UDP模块到完整网络方案实现了UDP收发就等于打通了FPGA与外界的高速数据通道往上叠加什么完全看应用场景ARP自动应答基础版静态MAC太死板实现ARP模块后PC直接ping FPGA的IP就能自动学到MAC地址代码量增加不大。多端口复用部分场景需要不同端口跑不同数据流解析UDP端口号后进行多路分发即可。图像传输UDP模块前面接一个图像缓存FIFO或DDR读写控制后面接RGMII上电后上位机就能看到连续的画面流。与MCU协同不少项目是FPGA做数据采集、STM32H743做控制逻辑两边通过FMC接口通信此时UDP模块可以负责把MCU下发的指令转发到远端或者把FPGA采集的数据封装上传。FPGA和MCU之间通过FMC传输的数据格式完全可以在FPGA内部用寄存器或RAM缓冲再由UDP模块向上位机推送。与PCIE配合如果需要更高速率或更复杂的链路PCIE DMA UDP的组合也是常见方案此时UDP模块负责把DMA搬来的数据封装发送核心逻辑不变。说实话做了这么多次FPGA网络通信我最深的体会是UDP模块的技术难度不在UDP本身而在你愿不愿意把以太网帧结构、IP头、校验和这些“底层琐事”耐心地逐字节抠清楚。很多人一听协议栈就觉得博大精深下意识想放弃实际拆开看能跑通的最小子集就那么点事。我的建议是别上来就追求完整ARP、不要追求校验和全覆盖先把“点对点静态MAC收发UDP”这个最小系统跑通一旦通了后面加ARP、加CRC、加多端口就只是工作量问题不是智力问题。最后分享一个调试时的小技巧如果FPGA开发板旁边有交换机千万别把FPGA直接插到办公室的大交换机上调试因为交换机可能会做MAC学习、VLAN隔离之类的处理导致你发的广播包和UDP包行为变得奇怪。我都是在开发板旁边单独拉一台小交换机或者干脆用网线直连PC这样抓包、重发、断电重启都方便排查问题效率高一个量级。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门