做FPGA板间互联,很多人第一反应是“用光纤啊,SFP+插上就行”。
但真正把两块板子用光纤连起来跑通Aurora,很多人第一版都会遇到这几个问题:
链路起来了,但数据传一会就断
两边时钟不同源,误码率飙升
帧边界对不上,上层协议解析全乱
ILA抓到的数据跟发出去的不一样
这篇文章把Aurora 8B/10B在GTX上的实战经验全部拆开,每个坑都给你可复现的代码和排查方法。
痛苦点一:链路起来了但传一会就断——你没处理Lane Up/Down事件
❌ 错误做法
verilog
// 很多人这样写——只要lane_up就认为链路可用
always @(posedge user_clk) begin
if (lane_up) begin
tx_valid <= 1'b1; // 直接开始发数据
// ... 往Aurora写数据
end
end
现象:刚上电正常,跑几分钟数据突然全断,ILA看lane_up已经拉低了,但你的发送逻辑还在拼命写——数据丢失+链路无法自动恢复。
✅ 正确做法
verilog
// 必须同时检测 lane_up + channel_up
reg [1:0] lane_up_dly;
reg [1:0] channel_up_dly;
wire lane_up_rising, lane_up_falling;
wire channel_up_rising, channel_up_falling;
always @(posedge user_clk or negedge user_rst_n) begin
if (!user_rst_n) begin
lane_up_dly <= 2'b00;
channel_up_dly <= 2'b00;
end else begin
lane_up_dly <= {lane_up_dly[0], lane_up};
channel_up_dly <= {channel_up_dly[0], channel_up};
end
end
assign lane_up_rising = ~lane_up_dly[1] & lane_up_dly[0];
assign lane_up_falling = lane_up_dly[1] & ~lane_up_dly[0];
assign channel_up_rising = ~channel_up_dly[1] & channel_up_dly[0];
assign channel_up_falling= channel_up_dly[1] & ~channel_up_dly[0];
// 只有channel_up为高时才发送数据
always @(posedge user_clk or negedge user_rst_n) begin
if (!user_rst_n) begin
tx_valid <= 1'b0;
tx_data <= 64'h0;
end else if (channel_up) begin
tx_valid <= s_axis_tvalid; // 来自上层AXI Stream
tx_data <= s_axis_tdata;
end else begin
tx_valid <= 1'b0; // channel_up掉了,停止发送
tx_data <= 64'h0;
end
end
// 检测到链路掉线 → 自动触发重训
always @(posedge user_clk or negedge user_rst_n) begin
if (!user_rst_n) begin
do_retrain <= 1'b0;
end else if (lane_up_falling || channel_up_falling) begin
do_retrain <= 1'b1; // 触发Aurora重训
end else if (channel_up_rising) begin
do_retrain <= 1'b0; // 链路恢复,清除重训标志
end
end
📌 原理解释
Aurora协议有两个状态信号,必须同时关注:
| 信号 | 含义 | 什么时候为高 |
|---|---|---|
lane_up | 物理Lane训练完成 | GT收发器8B/10B对齐完成 |
channel_up | 逻辑信道建立完成 | 两端Aurora核完成握手,可以传数据 |
只检测lane_up是不够的——物理层通了,但逻辑层可能因为时钟偏差、缓冲区溢出等原因还没准备好。
正确顺序:
text
GT训练完成 → lane_up=1 → Aurora握手 → channel_up=1 → 开始传数据
链路异常 → channel_up=0 → 停止发送 → 触发重训 → lane_up可能也掉
痛苦点二:两边时钟不同源,误码率居高不下
问题描述
两块板子各有一个独立的100MHz晶振,分别给各自的GTX提供参考时钟。看似没问题,但实际跑起来误码率(BER)高达10⁻⁶ ~ 10⁻⁹——对于Aurora这种没有CRC重传的协议来说,这个误码率足以让链路频繁重训。(注:工业级优秀链路BER应≤10⁻¹²,时钟不同源会导致劣化到10⁻⁶~10⁻⁹)
根本原因
GTX接收端对参考时钟的容忍度是±200ppm,但两个独立晶振的频差可能超过这个值,特别是在温度变化时。更关键的是:Aurora 8B/10B没有CDR之后的软件层时钟补偿——时钟偏差会直接累积成采样误差。
✅ 正确做法:使用Aurora的时钟补偿功能
verilog
// Aurora IP核配置时,必须使能时钟补偿
// 在Vivado IP核配置界面:
// Shared Logic: Include Shared Logic in Core ← 推荐
// Clock Compensation: Enabled
// Clock Compensation Period: 6 (适配GTX标准12.5Gb/s速率,速率变更时需同步调整)
// 同时,在约束文件里保证参考时钟质量
create_clock -name gt_refclk -period 10.000 [get_ports gt_refclk_p]
set_input_jitter gt_refclk 0.005 // 参考时钟抖动≤5ps
如果无法共享参考时钟(板间距离太远),怎么办?
verilog
// 方案:使用Aurora的"Clock Compensation" + 更大的弹性缓冲区
// IP核配置里把RX弹性缓冲区深度调大
// (在Advanced Configuration里,RX Buffer Depth设为最大1024)
// 同时在FPGA内部做跨时钟域处理
// Aurora输出的user_clk跟你本地逻辑时钟不同源!
// 必须用异步FIFO隔离
aurora_rx_fifo u_fifo (
.wr_clk (user_clk), // 写时钟:Aurora恢复时钟(异步)
.rd_clk (local_clk), // 读时钟:本地逻辑时钟
.wr_en (aurora_rx_valid),
.wr_data (aurora_rx_data),
.rd_en (fifo_rd_en),
.rd_data (local_rx_data),
.empty (fifo_empty),
.full (fifo_full)
);
痛苦点三:帧边界对不上,上层协议解析全乱
问题描述
Aurora是流式协议,本身不保证帧边界对齐。你发了一帧100字节的数据,接收端拿到的可能是从第50字节开始的——帧头对不上。
❌ 错误做法
verilog
// 直接把Aurora接收数据送给上层,假设帧边界对齐
always @(posedge local_clk) begin
if (aurora_rx_valid) begin
rx_buffer[wr_ptr] <= aurora_rx_data; // 直接写,不检测帧头
wr_ptr <= wr_ptr + 1'b1;
end
end
// 结果:帧头可能在任意位置,上层解析全乱
✅ 正确做法:在Aurora之上封装帧格式
verilog
// 发送侧:每帧数据前面加帧头和长度字段
localparam FRAME_SOF = 16'hFBFC; // 帧起始标志(避开8B/10B控制字符)
reg [15:0] tx_frame_cnt;
reg tx_in_frame;
always @(posedge user_clk) begin
if (!channel_up) begin
tx_in_frame <= 1'b0;
end else if (s_axis_tvalid && !tx_in_frame) begin
// 发送帧头 + 长度
tx_valid <= 1'b1;
tx_data <= {16'h0, FRAME_SOF, s_axis_tdata[15:0]};
tx_in_frame <= 1'b1;
end else if (tx_in_frame) begin
tx_valid <= s_axis_tvalid;
tx_data <= s_axis_tdata;
if (s_axis_tlast) tx_in_frame <= 1'b0;
end
end
// 接收侧:帧头对齐 + 长度校验
reg [15:0] rx_hdr;
reg rx_got_sof;
reg [15:0] rx_frame_len, rx_byte_cnt;
always @(posedge local_clk) begin
if (fifo_rd_en && !fifo_empty) begin
rx_hdr <= local_rx_data[63:48];
if (rx_hdr == FRAME_SOF && !rx_got_sof) begin
rx_got_sof <= 1'b1;
rx_frame_len <= local_rx_data[47:32];
rx_byte_cnt <= 16'h0;
end else if (rx_got_sof) begin
rx_byte_cnt <= rx_byte_cnt + 16'd8;
if (rx_byte_cnt >= rx_frame_len) rx_got_sof <= 1'b0;
end
end
end
更简单的方案:直接用Aurora的User Flow Control(UFC)来传递帧边界信息,在IP核配置中使能即可。
痛苦点四:ILA抓到的数据跟发出去的不一样——你没有处理Aurora的空闲字符
问题描述
你在发送端ILA里看到数据是0x12345678_ABCDEF01,但在接收端ILA里看到的是0xBCDEF011_2345678——数据被Aurora插入的空闲字符(/I/和/K/字符)打乱了。
根本原因
Aurora 8B/10B在链路空闲时会自动插入/I/(Idle)字符来维持链路同步。如果你用ILA直接抓rx_data,看到的数据流里会混有这些空闲字符。你的逻辑必须能识别并丢弃这些字符。
✅ 正确做法
verilog
// 接收侧必须过滤Aurora的空闲和控制字符
// Aurora 8B/10B使用的K字符:
// /K28.5/ = 8'hBC(Comma,对齐用)
// /I0/~/I3/ = 空闲字符
reg rx_is_kchar;
reg [7:0] rx_kchar_value;
always @(posedge user_clk) begin
rx_kchar_value <= rx_data[7:0];
rx_is_kchar <= rx_is_k; // Aurora输出的K字符标志
if (rx_is_kchar) begin
// 控制字符,丢弃
case (rx_kchar_value)
8'hBC: ; // Comma,忽略
default: ; // 其他K字符,忽略
endcase
end else begin
// 有效数据,往上传递
app_rx_valid <= 1'b1;
app_rx_data <= rx_data;
end
end
ILA调试技巧:
verilog
// 同时抓rx_data和rx_is_k,过滤控制字符后再分析
wire [63:0] ila_rx_filtered = rx_is_k ? 64'h0 : rx_data;
ila_aurora u_ila (
.clk (user_clk),
.probe0 (aurora_lane_up),
.probe1 (aurora_channel_up),
.probe2 (ila_rx_filtered), // 只看有效数据
.probe3 (aurora_rx_valid && !aurora_rx_is_k), // 有效数据valid
.probe4 (tx_data),
.probe5 (aurora_err_count)
);
⚠️ 五大注意事项(必读)
注意事项一:user_clk不是自由运行的时钟
user_clk是Aurora IP核输出的时钟,它只在lane_up为高时才稳定输出。如果链路训练失败或掉线,user_clk可能停止翻转或输出不稳定时钟。
verilog
// ❌ 错误:用user_clk驱动需要持续运行的逻辑
always @(posedge user_clk) begin
heartbeat_counter <= heartbeat_counter + 1'b1; // 链路掉线后心跳停了!
end
// ✅ 正确:需要持续运行的逻辑用本地时钟
always @(posedge local_clk) begin // local_clk来自FPGA本地晶振,永远稳定
if (!channel_up) begin
heartbeat_counter <= 32'h0;
end else begin
heartbeat_counter <= heartbeat_counter + 1'b1;
end
end
注意事项二:复位时序必须满足最小脉宽
Aurora IP核的gt_reset必须保持至少10个init_clk周期。
verilog
// ✅ 正确的复位时序
reg [7:0] rst_cnt;
reg gt_reset_i;
always @(posedge init_clk) begin // init_clk通常≤50MHz
if (rst_cnt < 8'd10) begin
rst_cnt <= rst_cnt + 1'b1;
gt_reset_i <= 1'b1;
end else begin
gt_reset_i <= 1'b0;
end
end
// gt_reset必须保持至少10个init_clk周期!仅拉高1个周期无效
另外,user_reset_n是Aurora输出给用户逻辑的复位信号,不是输入。
注意事项三:AXI-Stream禁止依赖Aurora反压
Aurora IP核的TX接口tready常态为高,内部FIFO深度有限。上游持续灌入数据会直接溢出丢包,严禁依赖tready做流量控制。
verilog
// ❌ 错误:试图用tready反压Aurora
always @(posedge user_clk) begin
if (aurora_tx_tready) begin
tx_valid <= s_axis_tvalid; // 以为在等ready,实际Aurora几乎永远ready
end
end
// 结果:当Aurora内部缓冲区满时,数据直接被丢弃,不报错!
// ✅ 正确:用异步FIFO做流量控制
assign s_axis_tready = !fifo_almost_full; // 用FIFO反压,不是Aurora
注意事项四:多Lane链路务必等待通道完全对齐
多Lane Aurora(x4/x8)在所有Lane训练完成(lane_up=1)之后,还需要额外时间做Lane对齐,PCB走线偏斜会直接影响对齐时间。
工程规范:差分对内偏斜 < 5mil,Lane间偏斜 < 20mil。超标会大幅拉长对齐时间,甚至导致
channel_up无法拉高。
调试技巧:用ILA同时抓所有Lane的lane_up和最终的channel_up,测量对齐时间。实测4-Lane Aurora通常需要额外100μs ~ 1ms。如果对齐时间异常长(>10ms),优先检查PCB走线等长。
注意事项五:Vivado版本与IP核兼容性
不同Vivado版本生成的Aurora IP核信号名可能变化,跨版本移植时务必对照IP手册。
| Vivado版本 | Aurora IP版本 | 注意事项 |
|---|---|---|
| 2018.3及之前 | v11.x | 不推荐,BUG较多 |
| 2019.1 ~ 2020.2 | v12.x | 稳定,推荐老项目维护 |
| 2021.1 ~ 2022.2 | v13.x | 信号名变更(如*_lane_up→*_lane_up_i),还有其他状态/错误信号同步变更 |
| 2023.1及之后 | v14.x | 支持UltraScale+,IP核界面较大改动 |
💡升级建议:升级Vivado版本后,务必重新生成Aurora IP核,不要直接复用旧工程的.xci文件。建议先跑Example Design验证IP核兼容性。
Aurora 8B/10B 完整调试清单
| 检查项 | 方法 | 期望结果 |
|---|---|---|
| GT参考时钟频率 | 示波器测晶振输出 | 100MHz ±50ppm |
| 链路是否训练完成 | ILA看lane_up | 持续为高 |
| 信道是否建立 | ILA看channel_up | 持续为高 |
| 误码率 | 读取aurora_err_count寄存器 | 不增长或增长极慢(BER<1e-12) |
发送侧是否在channel_up前发数据 | ILA看tx_valid和channel_up时序 | tx_valid在channel_up之后才拉高 |
| 接收数据是否有K字符混入 | ILA看rx_is_k | 有效数据时段rx_is_k=0 |
| 时钟域是否隔离 | 检查user_clk和本地时钟是否有路径跨越 | 必须经过异步FIFO |
| 帧边界是否对齐 | 接收侧搜索FRAME_SOF | 能稳定搜到帧头 |
user_clk是否用于持续运行逻辑 | 代码审查 | 持续运行逻辑必须用本地时钟 |
复位时长是否≥10个init_clk周期 | ILA看gt_reset脉冲宽度 | 实测≥10周期 |
| 多Lane对齐时间 | ILA测lane_up→channel_up延迟 | <1ms为正常;若>10ms检查PCB走线偏斜 |
常见问题 FAQ
Q1:Aurora 8B/10B和64B/66B有什么区别?我该选哪个?
| 对比项 | 8B/10B | 64B/66B |
|---|---|---|
| 编码开销 | 20%(8→10bit) | ~3.125%(64→66bit) |
| 最大线速率(GTX) | 12.5 Gb/s | GTX不支持 |
| 最大线速率(GTH/GTY) | 10.3125 Gb/s | 16.375 Gb/s |
| 时钟补偿 | 需要(插入/K/字符) | 不需要(靠SYNC头) |
| 兼容性 | GTX/GTH/GTY全支持 | GTH/GTY支持,GTX不支持 |
| 推荐场景 | GTX器件、<10Gb/s | GTH/GTY器件、>10Gb/s |
💡 8B/10B也可用于GTH/GTY,但高速场景(>10Gb/s)官方强烈推荐64B/66B以获得更高效率。
Q2:单Lane Aurora可以用吗?还是必须x4/x8?
可以用单Lane。Aurora支持1/2/4/8 Lane配置。
单Lane优势:省引脚(1对收发只用4根差分线)、延迟更低(无需Lane对齐)、适合低速(<10Gb/s)
单Lane劣势:带宽上限低、无冗余(一Lane坏则全链路挂)
推荐:板间距离短、带宽<10Gb/s用单Lane;需高带宽/冗余用x4。
Q3:Aurora的实际吞吐量能达到线速率的多少?
8B/10B编码理论有效带宽公式(以12.5Gb/s线速率为例):
纯编码开销后:
12.5 Gb/s × 8/10 = 10 Gb/s叠加时钟补偿(约3%额外开销):约
9.7 Gb/s实测满载:约9.5 Gb/s(扣除空闲字符、协议头等)
Q4:为什么我的Aurora链路err_count一直在涨?
按优先级排查:
参考时钟质量差→ 示波器测晶振相位噪声,jitter >50ps影响大
PCB走线问题→ SFP+到FPGA走线是否100Ω差分阻抗匹配?
光模块问题→ 换一个光模块试试(10G光模块便宜,先排除)
温度问题→ 高温下晶振频偏增大,降温后再测
IP核配置错误→ 检查Line Rate和Reference Clock是否与硬件匹配
快速定位:用ILA抓aurora_err_count的增长速度。突发式增长(几分钟涨100)→时钟或光模块问题;持续缓慢增长 → PCB信号完整性问题。
Q5:Aurora和PCIe、以太网相比,我该选哪个?
| 协议 | 优势 | 劣势 | 典型场景 |
|---|---|---|---|
| Aurora | 简单、延迟低、FPGA间专用 | 需专用光纤、距离受限 | 板间高速互联(<100m) |
| PCIe | 生态成熟、主机可直接访问 | 协议复杂、需主机支持 | FPGA ↔ 主机 |
| 以太网 | 距离不限、交换机扩展 | 协议栈复杂、延迟较高 | 远距离、多节点 |
结论:FPGA板间互联(同一机架内),Aurora是最简单选择;需与主机通信用PCIe;需远距离或多节点用以太网。
Q6:Vivado仿真里怎么验证Aurora?
Xilinx提供了Aurora的仿真模型,在IP核生成后的example_design目录中。
步骤:
生成Aurora IP核后,右键 →Open Example Design
Vivado自动创建完整仿真工程
运行仿真,观察关键信号时序
关键仿真节点:
lane_up拉高 → 物理层训练完成channel_up拉高 → 逻辑层握手完成之后可发送测试数据
常见错误:lane_up一直为0 → 检查GT参考时钟频率是否正确(仿真模型需输入正确的时钟频率)。
总结
| 痛苦点 | 根因 | 解法 |
|---|---|---|
| 链路传一会就断 | 只检测lane_up,没处理channel_up掉线 | 检测channel_up+掉线自动重训 |
| 误码率高 | 两板时钟不同源,无补偿 | 使能时钟补偿+异步FIFO隔离 |
| 帧边界对不上 | Aurora是流式协议,不保证帧对齐 | 自定义帧头+长度字段封装 |
| ILA数据对不上 | 空闲字符/K字符混入有效数据 | 过滤rx_is_k,只看有效数据 |
核心设计原则:
物理层看lane_up,逻辑层看channel_up;时钟域必隔离,控制逻辑优先本地时钟。
一句话:Aurora 8B/10B上手简单,但要做到工业级稳定,这四个坑+五个注意事项一个都绕不开。关注我,FPGA实战系列持续更新。