1. 项目缘起与整体方案设计
两块ZYNQ板子之间用光口互传数据,这个需求在工业采集、多板卡级联、雷达信号处理这类场景里非常常见。我这次拿到的任务很直接:两块ZYNQ-7020板子,各自挂一个SFP光模块,通过光纤对接,实现双向数据互传,速率跑在3.125Gbps,用Aurora 8b/10b协议。选Aurora而不是自己撸一个SerDes收发逻辑,原因很简单——Xilinx已经把GT收发器的物理层、通道绑定、时钟校正、CRC校验这些脏活累活全封装好了,你只需要关心用户接口的AXI4-Stream握手就行。
Aurora 8b/10b这个IP核本质上是一个链路层协议引擎。它把GT收发器的串行差分信号转换成并行数据,做8b/10b编解码,管理通道初始化握手,处理时钟补偿序列,还支持帧模式和流模式两种数据传输方式。我这次用的是帧模式(Framing Mode),因为需要传输不定长数据包,帧模式自带CRC校验和帧边界标识,接收端能直接判断一帧数据是否完整。
整体架构上,每块ZYNQ里例化一个Aurora 8b/10b IP核,配置成Full Duplex、Framing Mode、2字节数据位宽(对应3.125Gbps线速率)。用户逻辑这边用一个简单的发送状态机,从BRAM里读数据打包成AXI4-Stream帧发出去;接收端把Aurora输出的AXI4-Stream数据写入另一个BRAM,PL通过AXI-Lite寄存器通知PS读取。PS端跑一个裸机程序,负责初始化、配置寄存器、搬运数据。
为什么选2字节位宽而不是4字节?因为3.125Gbps线速率下,8b/10b编码后有效数据率是2.5Gbps,2字节位宽对应125MHz的用户时钟,时序压力小,布线容易收敛。4字节位宽虽然吞吐更高,但用户时钟要跑到62.5MHz的双沿或者125MHz的DDR,对PL逻辑的时序要求更苛刻。对于大多数中低速采集场景,2字节位宽完全够用。
注意:Aurora IP的线速率和参考时钟必须匹配。3.125Gbps对应125MHz参考时钟,如果你板子上是156.25MHz的差分晶振,需要在IP配置里选对应的线速率档位,否则GT根本起不来。
2. Aurora 8b/10b IP核配置详解与避坑指南
2.1 IP核关键参数逐项拆解
打开Vivado的IP Catalog,搜Aurora 8B/10B,双击进去。第一页是Physical Layer配置,这里有几个参数必须和硬件严格对应:
- Lane Width:选2 Bytes。这个决定了用户接口的数据位宽,也影响GT的内部数据路径宽度。
- Line Rate:填3.125 Gbps。这个值必须和你的参考时钟频率成整数倍关系,否则PLL锁不住。
- GT Refclk:选125 MHz。这是GT收发器的参考时钟输入,来自板子上的差分晶振。
- INIT Clk:选50 MHz。这是Aurora内部逻辑的初始化时钟,可以用PL的普通时钟。
- Data Decoding:选8B/10B。这个不用改,Aurora 8b/10b只支持这一种。
- Interface:选Framing。流模式没有帧边界,接收端不知道一包数据从哪开始到哪结束,不适合我的场景。
- Flow Control:选None。我不需要背压,发送端只管发,接收端FIFO深度够大就行。
- Little Endian:勾上。ZYNQ是小端架构,数据字节序要一致。
第二页是GT Selection,这里要选具体的GT Quad和Channel。ZYNQ-7020的PL里通常有4个GTX收发器,分布在两个Quad里。你得根据原理图确认SFP光模块连的是哪个GT通道。我板子上SFP接的是GTX Quad 0的Channel 0,所以这里选Quad 0、Channel 0。
第三页是Core Options,这里有几个容易踩坑的地方:
- Column Used:选Left或Right,取决于你的GT在芯片的哪一侧。选错了布局布线会报错。
- DRP Mode:选AXI4-Lite。这样PS可以通过AXI总线动态改GT参数,调试时很方便。
- Vivado Lab Tools:不勾。我们不需要ILA在线调试GT内部信号,省点资源。
配置完点OK,IP核就例化好了。这时候你会看到Aurora IP对外暴露了几组接口:用户收发AXI4-Stream、GT差分对、参考时钟输入、初始化时钟、复位、以及一堆状态信号。
2.2 复位逻辑与时钟域处理
Aurora IP的复位逻辑是新手最容易翻车的地方。它有三个复位相关信号:gt_reset、reset、power_down。这三个信号的时序关系必须严格遵守数据手册。
gt_reset是给GT收发器的硬复位,必须保持至少6个init_clk周期。reset是给Aurora逻辑的软复位,必须在gt_reset释放之后、GT时钟稳定之后再释放。power_down正常工作时拉低,低功耗模式才拉高。
我实测下来,最稳妥的复位顺序是这样的:
// 复位状态机 localparam IDLE = 0, GT_RST = 1, WAIT_LOCK = 2, LOGIC_RST = 3, READY = 4; reg [2:0] rst_state = IDLE; reg gt_reset_r = 1'b1; reg reset_r = 1'b1; reg power_down_r = 1'b0; always @(posedge init_clk) begin case(rst_state) IDLE: begin if (user_reset) begin gt_reset_r <= 1'b1; rst_state <= GT_RST; end end GT_RST: begin if (cnt_gt_rst == 6'd63) begin // 保持64个周期 gt_reset_r <= 1'b0; rst_state <= WAIT_LOCK; end end WAIT_LOCK: begin if (gt_pll_lock && !gt_reset_done) begin rst_state <= LOGIC_RST; end end LOGIC_RST: begin reset_r <= 1'b0; rst_state <= READY; end READY: begin // 正常工作 end endcase end提示:
gt_pll_lock信号来自Aurora IP,表示GT的PLL已经锁定。如果这个信号一直不拉高,先检查参考时钟有没有输入,再检查线速率配置对不对。
时钟域方面,Aurora IP的用户接口时钟user_clk是从GT恢复出来的,频率等于线速率除以20(8b/10b编码开销)再除以位宽。3.125Gbps、2字节位宽下,user_clk是125MHz。你的发送逻辑和接收逻辑都必须在这个时钟域下工作,或者做好跨时钟域处理。
3. 发送与接收逻辑的Verilog实现
3.1 发送端状态机设计
发送端的任务很简单:从BRAM里读出一包数据,加上帧头,通过AXI4-Stream发给Aurora IP。AXI4-Stream的握手协议是tvalid和tready同时为高时数据传输有效。
我设计了一个四状态的状态机:
localparam S_IDLE = 0, S_SEND_HEAD = 1, S_SEND_DATA = 2, S_SEND_TAIL = 3; reg [1:0] tx_state = S_IDLE; reg [15:0] tx_cnt = 0; reg [31:0] pkt_len = 0; always @(posedge user_clk) begin case(tx_state) S_IDLE: begin if (tx_start) begin pkt_len <= data_len; tx_state <= S_SEND_HEAD; end end S_SEND_HEAD: begin if (m_axis_tready) begin m_axis_tdata <= 16'h55AA; // 帧头 m_axis_tvalid <= 1'b1; m_axis_tlast <= 1'b0; tx_state <= S_SEND_DATA; end end S_SEND_DATA: begin if (m_axis_tready && m_axis_tvalid) begin m_axis_tdata <= bram_data[tx_cnt]; tx_cnt <= tx_cnt + 1; if (tx_cnt == pkt_len - 1) begin m_axis_tlast <= 1'b1; tx_state <= S_SEND_TAIL; end end end S_SEND_TAIL: begin if (m_axis_tready) begin m_axis_tvalid <= 1'b0; m_axis_tlast <= 1'b0; tx_state <= S_IDLE; end end endcase end这里有个细节:m_axis_tlast必须在最后一个数据字的同一拍拉高,不能提前也不能延后。Aurora IP靠这个信号判断帧结束,然后自动插入CRC和帧尾序列。
3.2 接收端数据解析与BRAM写入
接收端的状态机更简单,因为Aurora IP已经把帧边界解析好了,你只需要在tvalid和tready同时为高时把数据写进BRAM。
always @(posedge user_clk) begin if (s_axis_tvalid && s_axis_tready) begin if (s_axis_tlast) begin // 一帧结束,产生中断通知PS rx_done <= 1'b1; rx_len <= rx_cnt; end else begin bram[wr_addr] <= s_axis_tdata; wr_addr <= wr_addr + 1; rx_cnt <= rx_cnt + 1; end end end注意:BRAM的写地址必须在帧开始时清零,否则第二帧数据会覆盖到第一帧后面。我一开始忘了清零,结果PS读到的数据长度一直累加,查了半天才发现是地址没复位。
3.3 中断与PS-PL交互
PL收到一帧数据后,通过AXI-Lite寄存器产生一个中断给PS。PS端在裸机程序里注册中断服务函数,收到中断后从BRAM里把数据读走。
AXI-Lite寄存器映射我定义了四个:
| 寄存器偏移 | 名称 | 功能 |
|---|---|---|
| 0x00 | CTRL | bit0: 发送启动,bit1: 接收使能 |
| 0x04 | STATUS | bit0: 发送完成,bit1: 接收完成 |
| 0x08 | TX_LEN | 发送数据长度 |
| 0x0C | RX_LEN | 接收数据长度 |
PS端的中断服务函数大概长这样:
void aurora_isr(void *CallbackRef) { u32 status = Xil_In32(AURORA_BASE + 0x04); if (status & 0x02) { u32 len = Xil_In32(AURORA_BASE + 0x0C); // 从BRAM读数据 for (int i = 0; i < len; i++) { rx_buf[i] = Xil_In32(BRAM_BASE + i * 4); } // 清中断 Xil_Out32(AURORA_BASE + 0x04, 0x02); } }4. 板级调试与常见问题排查实录
4.1 GT收发器不锁定的排查思路
两块板子上电、加载bit流之后,第一件事是看gt_pll_lock和channel_up信号。如果gt_pll_lock不拉高,说明GT的PLL没锁住,问题一定出在参考时钟或线速率配置上。
我遇到过两种情况:一是参考时钟晶振没起振,用示波器量差分对没有波形;二是线速率填错了,3.125Gbps填成了3.125MHz,差了1000倍。排查顺序是先用示波器确认参考时钟频率和幅度,再核对IP配置里的线速率和参考时钟是否匹配。
如果gt_pll_lock拉高了但channel_up不拉高,说明GT的物理层通了但Aurora的通道初始化没完成。这时候要检查光纤是不是接反了——TX接RX、RX接TX,这是最基本的。另外,两块板子的Aurora IP配置必须完全一致,线速率、位宽、参考时钟任何一个不一样都握不上手。
4.2 数据错位的典型原因
数据能收到但内容不对,最常见的原因是字节序问题。Aurora IP默认是小端模式,但如果你在IP配置里没勾Little Endian,或者PS端读BRAM时按大端解析,数据就会错位。
另一个原因是帧头对齐。我在发送端插入了16'h55AA作为帧头,接收端如果直接从第一个数据字开始存BRAM,帧头也会被存进去。PS读数据时要从第二个字开始读,或者接收端状态机跳过帧头。
还有一种情况是时钟补偿序列被误当成数据。Aurora在空闲时会发送时钟补偿序列,接收端如果没正确过滤,这些序列会混进BRAM。不过Aurora IP内部已经处理了时钟补偿,用户接口只会输出有效数据,所以这个问题一般不会出现。
4.3 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| gt_pll_lock不拉高 | 参考时钟缺失或频率错误 | 示波器量差分时钟,核对IP配置 |
| channel_up不拉高 | 光纤接反或两端配置不一致 | 交换TX/RX,核对两端IP参数 |
| 数据错位 | 字节序不一致 | 检查Little Endian配置和PS解析方式 |
| 接收数据长度累加 | BRAM写地址未复位 | 在帧开始时清零写地址 |
| 中断不触发 | AXI-Lite寄存器未使能 | 检查CTRL寄存器bit1和GIC配置 |
| 误码率高 | 光纤质量差或GT预加重不足 | 换光纤,调GT的TX Diff Swing |
提示:调试Aurora时,Vivado的ILA是神器。把
gt_pll_lock、channel_up、m_axis_tvalid、m_axis_tready、s_axis_tvalid、s_axis_tready这几个信号抓出来,一眼就能看出问题出在哪个环节。
5. 性能实测与优化经验
5.1 实测吞吐与延迟
两块ZYNQ板子通过1米光纤对接,Aurora跑3.125Gbps,2字节位宽,用户时钟125MHz。实测单向吞吐能跑到250MB/s左右,接近理论值312.5MB/s的80%。瓶颈在BRAM的读写带宽和PS端的中断响应延迟。
延迟方面,从发送端写入BRAM到接收端PS读出数据,端到端延迟大约15微秒。其中GT传输延迟约1微秒,PL逻辑处理约2微秒,PS中断响应约10微秒。如果对延迟敏感,可以把PS中断改成轮询,能降到5微秒以内。
5.2 提升吞吐的几种手段
如果250MB/s不够用,有几个方向可以优化:
- 加大位宽:把Lane Width从2字节改成4字节,用户时钟不变的情况下吞吐翻倍。但时序收敛难度增加,需要仔细约束。
- 提高线速率:从3.125Gbps提到6.25Gbps,参考时钟也要相应提高到250MHz。ZYNQ-7020的GTX支持到6.6Gbps,但PCB走线质量要够好。
- 用DMA搬运:PS端用DMA从BRAM搬数据到DDR,比CPU逐字读取快得多。ZYNQ的DMAC支持AXI4-Stream到Memory的搬运,配置好描述符就行。
- 流模式替代帧模式:流模式没有CRC和帧边界开销,有效数据率更高,但需要你自己管理数据包边界。
5.3 长时间运行的稳定性
我让这套系统连续跑了72小时,每秒钟发一包1KB的数据,总共发了约2.6亿包。误码率为零,没有出现通道断开或数据错乱。关键措施是:
- GT的预加重和均衡参数用默认值,没有乱调。
- 光纤用工业级单模光纤,接头用LC/PC打磨过的。
- 电源用低纹波的LDO给GT供电,开关电源的纹波会耦合到GT的模拟电路里。
- 板子放在恒温箱里,温度控制在25±5℃。
注意:GT收发器对电源纹波非常敏感。如果误码率偏高,先用示波器量一下GT电源的纹波,超过50mVpp就要考虑换LDO或加滤波电容。
6. 工程文件组织与复现建议
6.1 Vivado工程目录结构
我的工程目录是这样组织的:
aurora_loopback/ ├── src/ │ ├── rtl/ │ │ ├── aurora_top.v │ │ ├── tx_fsm.v │ │ ├── rx_fsm.v │ │ └── axi_lite_reg.v │ ├── ip/ │ │ └── aurora_8b10b_0/ │ └── constr/ │ └── aurora_pins.xdc ├── sdk/ │ └── aurora_test/ │ └── src/ │ └── main.c └── scripts/ └── build.tcl约束文件里最关键的是GT差分对的引脚约束和参考时钟约束:
# GT差分对 set_property PACKAGE_PIN F6 [get_ports gt_txp_out] set_property PACKAGE_PIN E6 [get_ports gt_txn_out] set_property PACKAGE_PIN D6 [get_ports gt_rxp_in] set_property PACKAGE_PIN C6 [get_ports gt_rxn_in] # 参考时钟 set_property PACKAGE_PIN F10 [get_ports gt_refclk_p] set_property PACKAGE_PIN E10 [get_ports gt_refclk_n] create_clock -period 8.000 [get_ports gt_refclk_p]6.2 复现步骤清单
如果你想复现这套系统,按这个顺序来:
- 在Vivado里新建工程,选对ZYNQ型号。
- 例化Aurora 8B/10B IP,按第2节的参数配置。
- 写发送和接收状态机,例化BRAM和AXI-Lite寄存器。
- 写约束文件,绑定GT引脚和参考时钟。
- 综合、实现、生成bit流。
- 导出硬件到SDK,写裸机程序。
- 两块板子都加载bit流,光纤对接,跑测试程序。
提示:两块板子的bit流可以完全一样,Aurora IP会自动协商主从。不需要一块配成Master一块配成Slave,IP内部有自动协商机制。
6.3 后续扩展方向
这套框架搭好之后,可以往几个方向扩展:一是加一个DDR缓存,把接收到的数据先存DDR再慢慢处理;二是把Aurora封装成自定义IP,用户接口改成AXI4-Stream,方便集成到更大的系统里;三是加一个简单的协议层,在帧头里加源地址、目的地址、包序号,实现多板卡路由。
我个人在实际操作中的体会是,Aurora 8b/10b这个IP核本身很稳定,大部分问题都出在复位时序、时钟配置和引脚约束上。把这三样搞对了,剩下的就是写状态机搬数据的事。第一次调通可能需要两三天,后面再复现就是半天的事。