1. 为什么在光通信链路里必须用TEMAC核:MAC层的脏活累活到底干了什么
1.1 从SFP光口到UDP数据包,中间隔了多少层
拿到一块带SFP光口的板子,想要跑起来千兆UDP传输,很多初学者第一反应是:我是不是应该自己写一个MAC控制器?毕竟UDP协议栈也不复杂,CRC校验网上也有现成代码,为什么要花钱花时间去例化一个Tri Mode Ethernet MAC IP核?
我先把这个链路拆开讲。一个UDP包要从FPGA逻辑发到对端电脑,实际经过的路径是:用户逻辑 → MAC层 → SGMII/PCS子层 → PHY芯片 → SFP光模块 → 光纤 → 对端。其中MAC层的职责范围,比很多人想象的要大得多:
- 生成前导码和帧起始定界符(SFD),这是物理层能识别"帧从哪开始"的关键。
- 计算并附加FCS(帧校验序列),也就是CRC32,这玩意儿自己写不难,难的是在千兆速率下做到每个时钟周期都正确且时序收敛。
- 保证帧间隙(IFG)符合协议要求,千兆以太网标准规定帧间隙至少96 bit time,也就是96ns。如果IFG不对,对端交换机会直接把帧丢掉。
- 处理冲突检测、退避算法(半双工时需要,现在基本用不到,但核里自带了)。
- 处理超大帧、VLAN标签、流控(PAUSE帧),这些功能如果全自己写,工作量是厂商IP核的三倍以上。
所以在千兆光通信这个场景里,TEMAC核不是"选不选"的问题,而是"必须用成熟方案"的问题。你自己写的MAC可能在小包、低速率下能跑通,一旦到了千兆线速、数据不间断灌进来的时候,各种边界条件会把你逼疯——比如CRC校验在多字节对齐时怎么处理、FIFO快满的时候刚好来一个超长帧怎么办。
1.2 Tri Mode到底指什么,光通信场景下实际用哪种模式
Tri Mode Ethernet MAC支持10Mbps、100Mbps、1000Mbps三种速率自适应,但做光通信的都知道,光模块一上来就是千兆,根本不会协商到百兆去。这块的重点不是速率自动协商,而是你要搞清楚:TEMAC核内部的PCS/PMA层,在SGMII配置下负责了8B/10B编码、时钟恢复、自动协商这些脏活,这些才是真正难搞的部分。
千兆光模块工作在1000BASE-X标准下,物理编码子层用的是8B/10B编码,并且有专门的自动协商机制(CJPAT、DCP等配置序列)。如果不用TEMAC自带的SGMII功能,你就要自己在逻辑里实现8B/10B编解码、comma对齐、运行不一致校验,工程量直接翻倍。
TEMAC核选择SGMII接口时,内部会自动例化一个SGMII IP核。你在配置界面里勾选一个选项,生成的工程里就多了一整套PCS/PMA逻辑,这就是IP核的价值——它把"以太网MAC + SGMII物理层适配"打包成了一个黑盒,你只需要关心用户侧接口和物理侧引脚。
1.3 一个反直觉的结论:MAC核不是性能瓶颈,用户逻辑才是
很多人在设计初期会担心:TEMAC核是不是很占资源、是不是延时很高?实测下来,在7系列和UltraScale系列上,单纯TEMAC核的资源消耗大概是几百个LUT加几个Block RAM,相比整个工程的资源占用可以忽略不计。真正的性能瓶颈在用户侧——你给MAC核喂数据的逻辑,能不能做到每个时钟周期都有数据输出。
举个例子,我的发送逻辑一开始是用状态机逐字节搬运UDP数据,结果千兆速率下(用户时钟125MHz,64位数据总线),状态机每跳转一次要花好几个周期,实际吞吐量只有300Mbps左右。后来改成流水线结构,数据从一个FIFO直接流到MAC核的AXI4-Stream接口,带宽才跑满。这个问题的根子不在IP核,而在用户逻辑的设计思路。
2. 图形化配置里每个关键选项对应的硬件行为
2.1 接口模式:SGMII与1000BASE-X到底怎么选
打开TEMAC IP核的配置界面,第一个让你纠结的就是Physical Interface选SGMII还是1000BASE-X。这个选项直接决定物理侧引脚和时钟方案,选错了后面全盘皆输。
| 配置选项 | 适用场景 | 物理侧差异 | 编码方式 |
|---|---|---|---|
| SGMII | 板上有PHY芯片,PHY再接RJ45或光模块 | 通过SGMII接口连接外部PHY,引脚是差分对 | 8B/10B |
| 1000BASE-X | 板上没有PHY,SGMII信号直接进光模块 | 无需外部PHY,GMII信号直接映射到光模块接口 | 8B/10B |
我的板子是无PHY方案,SFP光模块通过一个小的电平转换电路直接连到FPGA的高速串行收发器(GTX/GTH),这时候TEMAC核内部要把MAC层的数据转换成1000BASE-X的PCS/PMA信号,再交给GTX去发送。如果选成SGMII,核会认为外部还有一个PHY芯片,协商逻辑就会出问题,表现为光模块link不上,或者能link上但数据全是乱码。
判断方法很简单:看你的板卡上,FPGA的高速引脚是直接连SFP光模块,还是先经过一个PHY芯片(比如88E1111、RTL8211之类的)。前者选1000BASE-X,后者选SGMII。如果选错了,现象是:MDIO能读到PHY的寄存器,但链路永远处于down状态,或者光模块的RX_LOS信号一直拉高。
2.2 Shared Logic选项的深坑:一个决定你少写很多代码的选项
配置界面里有一组单选按钮,叫Shared Logic,选项是Include Shared Logic in core和Include Shared Logic in example design。这个选项的官方解释很晦涩,我给你翻译成人话:
- Include Shared Logic in core:把SGMII需要的时钟管理(MMCM/PLL)、复位逻辑、状态监测这些公共模块,直接编译进IP核内部。你拿到手的IP核是一个完整的、自包含的模块,顶层只需要引出物理引脚和用户接口。
- Include Shared Logic in example design:IP核只包含MAC核心逻辑,把SGMII的时钟和复位逻辑放在外面的example design里。你可以修改这些公共逻辑,比如把MMCM换成自己的时钟方案,但代价是你必须自己把SGMII核、GTX、时钟管理这些模块连起来。
实际项目中,我强烈建议选Include Shared Logic in core。理由有三条:第一,Xilinx把GTX和SGMII的时钟连接关系都给你接好了,你自己连的话容易漏掉一些初始化时序;第二,SGMII的复位时序比较讲究,核内部处理好了,外部只要给一个全局复位即可;第三,调试的时候引脚少很多,不容易接错。
如果非要选Include Shared Logic in example design,你要有心理准备:example design里有大量IO约束和原语,比如IBUFDS_GTE2、BUFG_GT这些,迁移到自己的工程时,光删这些代码就要花半天时间。我最初为了灵活性选了后者,结果光是适配自己的板卡时钟就折腾了一整天,最后还是老老实实改回核内共享逻辑。
2.3 数据位宽、时钟频率和复位极性的实际选择
配置界面里还有几个关键选项,直接影响你后面的代码写法和约束文件:
数据位宽:AXI4-Stream接口的Data Width可选8、16、32、64位。千兆以太网在125MHz时钟下,8位宽刚好够用(8bit × 125MHz = 1000Mbps),但实际使用中几乎没人选8位,因为用户逻辑处理UDP帧头、IP校验和时,8位总线意味着每个字节都要单独操作,状态机极其繁琐。我用64位,原因是125MHz下64位总线带宽是8Gbps,远高于千兆,这样即使发送逻辑偶尔停一拍(比如计算校验和时),也不会成为性能瓶颈。而且64位对齐到以太网帧的8字节倍数,每帧数据可以整块写入FIFO,状态机逻辑简单很多。
用户侧时钟:TEMAC核会根据你的接口位宽自动生成对应的用户时钟。64位位宽时,用户时钟为125MHz;32位时,用户时钟为31.25MHz的倍数关系,注意是125MHz × 32 / 64 = 62.5MHz。这里有无数人踩过坑:在配置界面里看到"User Interface Clock"显示125MHz,以为所有模式都一样,实际上选了32位后用户时钟会变成62.5MHz,而SGMII和RGMII接口的物理时钟仍是125MHz,很多人按照125MHz去写约束,结果时序收敛不了,跑起来偶发丢包。
复位配置:复位极性选Active Low,这是AXI4-Stream的标准约定,跟很多国产IP核的习惯不一样。另外Synchronous或Asynchronous复位都可以,建议选Synchronous,避免在时钟未稳定时复位释放产生亚稳态。核会产生一个tx_reset和rx_reset信号,这两个信号在复位释放后会保持一段时间,必须等它们拉低后才能开始收发数据,否则第一帧大概率是坏帧。
3. UDP帧的用户侧组装:从组帧到接口时序
3.1 软件视角和硬件视角看到的以太网帧,差了整整8个字节
用wireshark抓包的时候,你看到的每一帧都是以目的MAC地址开头的。但在FPGA侧,MAC核在实际发送到线路上的数据是:前导码(7字节0x55)+ SFD(1字节0xD5)+ 目的MAC(6字节)+ 源MAC(6字节)+ 类型/长度(2字节)+ 载荷(46~1500字节)+ FCS(4字节)。其中前导码和SFD是MAC核自动生成的,FCS也是MAC核自动计算并附加的,用户不需要也不应该提供这些字节。
用户侧给的帧,只需要从目的MAC开始,一直到IP/UDP数据结束。MAC核会在你给的数据前面自动加前导码,在后面自动加CRC32。我第一次用TEMAC核时不知道这个规则,在用户逻辑里手动加了前导码和FCS,结果对端wireshark显示每一帧都带了一堆奇怪的字节,抓包界面里前导码全都显示成unknown字段。
UDP数据报文的完整组包结构如下:
- 目的MAC地址(6字节)——FF:FF:FF:FF:FF:FF是广播,单播写对端网卡的MAC
- 源MAC地址(6字节)——写自己板卡的MAC,注意不能全零
- 以太网类型(2字节)——0x0800表示IPv4
- IP头(20字节,标准无选项)——版本+头长(0x45)、服务类型(0)、总长度、标识、标志位、TTL、协议号(17表示UDP)、IP头校验和、源IP、目的IP
- UDP头(8字节)——源端口、目的端口、UDP长度、UDP校验和
- 应用数据(N字节)
很多人问到UDP校验和不计算行不行。IPv4的UDP校验和是可选的,填0即可,但有个前提:如果对端设备较严格,可能会丢包。实测下来,电脑端的wireshark能正常识别UDP校验和为0的报文,大部分网络的中间设备也不会拦截,但在光通信这种对可靠性要求高的场景,我建议还是把UDP校验和算上。
3.2 用Verilog手写一个最简单的UDP发送状态机
搞清楚帧结构之后,发送逻辑的框架就清晰了。核心是用一个状态机把上面这些字节按顺序输出到AXI4-Stream接口。以下是一个64位数据位宽的简化发送逻辑核心部分:
// 发送状态机:IDLE → SEND_ETH_HEADER → SEND_IP_UDP_HEADER → SEND_PAYLOAD → DONE always @(posedge user_clk or negedge reset_n) begin if (!reset_n) begin state <= IDLE; end else begin case (state) IDLE: if (tx_start) state <= SEND_ETH_HEADER; // 每个周期输出64bit(8字节)数据,计数发送了多少个周期 SEND_ETH_HEADER: begin if (byte_count == ETH_HEADER_BYTES/8 - 1) state <= SEND_IP_UDP_HEADER; end // ... endcase end end // last信号在最后一拍拉高,keep信号指示有效字节数 assign s_axis_tlast = (byte_count == TOTAL_BYTES/8 - 1) && valid_condition; assign s_axis_tkeep = last_byte_valid ? 8'h0F : 8'hFF; // 最后一拍可能只有部分字节有效这段代码的关键点是tkeep和tlast。tlast告诉MAC核这一帧结束了,MAC核收到tlast后会在下一拍把前导码和FCS补完发送出去。tkeep告诉MAC核最后一拍有几个字节是有效的——比如UDP数据总长度不是8的倍数,最后一拍只有4个字节有效,就把tkeep设成4'b1111,MAC核会从数据里正确截断。
处理UDP长度字段和IP总长度字段时需要特别注意:这两个字段的单位不一样,UDP长度包含UDP头本身(8字节),IP总长度包含IP头(20字节)加后面的UDP段。很多人算错这里,导致对端收到包后wireshark显示"Malformed Packet"。
3.3 接口时序上的三个"必须"
AXI4-Stream接口时序如果不满足,就算数据内容全对,MAC核也不会按预期工作。以下是实测总结的要点:
必须保证tvalid在tready拉高时保持稳定。TEMAC核的发送接口在大部分情况下tready是常高的,但在FIFO快满时,tready会拉低一小段时间。如果你的逻辑在tready拉低时改变了tdata或tvalid,就会造成数据错位。规范做法是:tvalid拉高后,必须等到tready也拉高,数据才算被成功接收,在此之前不能改变tdata。
必须为帧间隙留出空间。MAC核内部会处理IFG,但用户侧如果在连续两帧之间一拍都不停,MAC核的FIFO可能来不及处理,表现为偶发丢帧。我在发送逻辑里,每发完一帧就回到IDLE状态至少等8个时钟周期,再发送下一帧。
必须注意tuser信号。接收方向的tuser信号在以太网MAC核里表示接收错误指示,当MAC核检测到FCS错误、长度错误、接收超时等情况时,会在对应帧的最后一拍把tuser拉高。这是在调试阶段最实用的信号——如果tuser高频拉高,说明收到的数据帧有问题,MAC核已经帮你做了第一道质检。
4. 系统联调与实测排错:从ILA波形到wireshark报文
4.1 先用三种回环把链路切成四段,逐段定位
完整的光通信链路是:FPGA用户逻辑 → TEMAC核 → GTX → SFP光模块 → 光纤 → 对端。任何一环出问题,现象都很相似——收不到数据或者疯狂报错。我的排查方法是把链路切成四段,从两端向中间逼近:
- 内部回环(FPGA内部):在用户逻辑侧,把发送的AXI4-Stream数据直接接到接收的AXI4-Stream接口上。这一步验证用户逻辑组帧是否正确。
- PCS/PMA回环(TEMAC核内部):通过TEMAC核的配置寄存器,把发送的PCS数据直接环回接收PCS。这一步验证GTX和SGMII逻辑是否正常,不经过外部PHY。
- PHY回环(光模块自环):插一根光纤跳线,把SFP的发端和收端短接。这一步验证光模块和光纤链路。
- 远端回环:对端设备的网卡把收到的帧原样发回来。这一步验证对端协议栈是否正常工作。
以最常用的ILA调试为例,抓取AXI4-Stream接口上的信号时,建议设置触发条件为tlast == 1,这样每抓到一帧的结束时刻,方便观察完整帧的波形。如果能看到一帧数据正常地从发送接口流出,且接收接口的tvalid在对应时刻拉高,说明用户逻辑和MAC核协同工作正常。
4.2 排查案例:接收方向tuser频繁拉高,最终锁定IP头校验和
有一次联调,发送端逻辑看起来一切正常,ILA上能看到数据从TEMAC发送接口送出去了,但接收端(同一块FPGA的另一路)的tuser频繁拉高,说明接收到的帧CRC校验失败。
排查链路如下:
- 先做内部回环,发现tuser没有拉高,说明用户逻辑组帧正确,排除了发送侧问题。
- 换成PCS/PMA回环,tuser正常,说明TEMAC核和GTX工作正常。
- 插上光纤让两端对打,tuser开始拉高——问题锁定在光模块或远端设备。
- 用wireshark在远端电脑上抓包,发现电脑能收到UDP包,但wireshark报"Invalid IP header checksum"。
- 回去仔细检查IP头校验和算法,发现校验和计算时把UDP长度字段当成了IP总长度,导致校验和计算错误。
这是一个非常经典的低级错误。IP头校验和的计算方法网上到处都是:先把校验和字段置零,以16位为单位做反码求和,再把结果取反。但计算时一定要从IP头第一个字节算到最后一个字节,只算IP头本身(20字节),不包括后面的UDP头和数据。如果算错,帧能发出去但无法通过对端的校验。而MAC核算的CRC32是覆盖整个帧的,IP校验和错了CRC也错,所以tuser会拉高——这是一个连锁反应。
4.3 link状态与PHY寄存器调试
如果光模块link不上,最先看的是SFP模块的RX_LOS信号和GTX的rx_reset状态。在TEMAC核生成的设计里,可以通过MDIO接口读取外部PHY(如果有的话)的状态寄存器。我的板子无外部PHY,所以是通过GTX的eyescan功能和光模块的数字诊断接口(I2C)来确认光路是否正常。
这里分享一个实测技巧:用wireshark筛选UDP前后两包的时间间隔,用frame.time_delta_displayed这个过滤字段,可以查看相邻两个UDP包的到达时间差。如果发现时间间隔不稳定,忽大忽小,说明发送端的发送节奏不均匀,可能是用户逻辑里FIFO读空导致的,需要检查发送FIFO的深度和水线设置。在iperf3使用UDP打流时,如果Wireshark里看到重传、乱序,多半不是网络问题,而是发送端FPGA的包间隔控制不合理。
5. 收尾建议:先跑Example Design仿真,再改自己的逻辑
最后分享一个我自己走了弯路才明白的流程,希望对你有帮助。
第一次做TEMAC核接SGMII的项目时,我犯了一个大忌——拿到IP核配置完,就直接复制example design里的端口连接代码,然后急着写自己的UDP发送逻辑。结果仿真波形一出来,全是一些匪夷所思的信号。后来老老实实花了一个下午,把example design里的仿真工程完整跑了一遍,看着那些预置的测试用例怎么收发数据,才算真正理解了各个信号的时序关系。
具体建议:先在Vivado里把生成的example design综合、仿真跑通,观察几个关键信号——s_axis_tready什么时候拉低、m_axis_tvalid什么时候拉高、tx_ifg_delay的设置对帧间隙的影响。仿真通过之后,再用计数器构造一个简单的测试帧(目标MAC和源MAC填固定值,IP和UDP头全部手工算好),通过ILA抓到线上实际数据,和wireshark抓到的对端数据做逐字节对比。这一套流程走下来,UDP光通信的收发链路基本上就能稳定跑起来。
TEMAC核本身不难,难的是把它周边的时钟、复位、FIFO、GTX这些模块理顺。按照"仿真先行 → 回环分段验证 → 实机对打"的顺序来,能省掉至少一半的调试时间。