
前阵子帮朋友排查一块 LVDS 采集卡现象非常典型示波器上差分眼图很漂亮但 FPGA 内部收进来的数据就是隔三差五错几个 bit。一开始怀疑 PCB 走线后来一步步往输入路径上查发现是数据 lane 和随路时钟之间的 skew 没对齐。最后在 Xilinx Ultrascale 系 FPGA 上把每个 lane 的可编程输入延迟IDELAYE3单独微调了一轮问题几分钟就解决了。这篇是 LVDS 系列的第 11 篇。跟着这个系列一路看下来的朋友应该知道前面我们已经把 LVDS 电平标准、差分对端接、IBUFDS/OBUFDS、以及基础的 SERDES 收发都过了一遍。这次聊的是 Xilinx Ultrascale 系里专门用来补偿输入路径延迟的资源也就是标题里说的可编程输入延迟。不管你是刚开始接触 LVDS 接收还是在 7 系列上用过 IDELAYE2、想了解 Ultrascale 系有什么变化这篇文章都值得花十分钟看完。我会先讲清楚为什么非要折腾输入延迟再看 IDELAYE3 的内部机制最后给出可以直接照抄的例化方法和调试思路。1. LVDS 接收为什么非要折腾输入延迟1.1 源同步传输的“同时到达”是个理想假设LVDS 在绝大多数板级应用里走的是源同步方式发送端同时把数据和随路时钟推出来接收端拿随路时钟去采样数据。这种方式比 CDR 简单因为不需要在接收端恢复时钟代价是——所有 lane 的时间对齐关系几乎完全依赖传输路径的一致性。问题在于“同时到达”这件事在物理世界里根本不成立。从发送端芯片的引脚出来经过封装、PCB 走线、过孔、连接器、再进接收端 FPGA 的引脚、封装、内部布线每个 lane 的路径都不可能一模一样。哪怕 PCB 设计时做了等长也只会把走线长度差控制在一定范围内不可能做到零偏差。连接器的针脚长短、过孔残桩、FPGA 封装内部的 bond wire 长度差异这些都不是画板时能完全消除的。举个具体的例子。一条 1Gbps 的 LVDS 链路单个 bit 周期只有 1ns1000ps。如果 PCB 上某个数据 lane 比随路时钟 lane 长出 50ps再加连接器和封装引入的 100ps 偏差总 skew 就有 150ps相当于吃掉 UI 的 15%。当这条链路是 4 对数据 lane 加 1 对时钟的并行结构时每个 lane 的偏移方向和大小还不一样有的早到、有的晚到整个数据的采样窗口就被严重压缩了。更麻烦的是FPGA 片内从引脚到采样寄存器的路径也不是完全一致的。同样是 HP Bank 里的引脚位置不同内部走线长度不同路径延迟就会有差异。这个差异在低速时无所谓但到了 LVDS 这种动辄几百 Mbps 到 Gbps 的速率上就成了必须正视的问题。1.2 固定延迟补偿解决不了的问题有人会问既然知道有 skew直接在 PCB 上把线拉长、或者查表给每条 lane 加一个固定延迟不就行了固定延迟能解决一部分问题但解决不了全部。首先PCB 等长是“工作前”做的事情。等长只保证走线延迟一致但连接器、封装、焊接、叠层结构带来的差异仍然存在。一块板子即使画得再对称生产出来也会因为板材介电常数误差、焊点大小差异、连接器批次不同而有细微差别。这些差别在高速信号上会被放大。其次也是更关键的延迟是随温度、电压、工艺漂移的。FPGA 内部路径延迟会随 die 温度变化也会随供电电压波动。白天板和晚上板的温度可能差二三十度风扇转速一变、板卡负载一变输入路径上的建立保持时间裕量就在变。固定延迟链只能对准某一个状态无法跟踪环境变化。之前我见过一个产品常温老化测试全部通过到了高温房就随机出误码。查了半天最后定位到就是输入数据采样点落在了临界区。温度升高后片内路径延迟和 PCB 走线延迟的漂移方向不一致原本居中的采样点被推到了边沿附近。这种情况用固定延迟补偿是压不住的必须有可调的手段。1.3 FPGA 里缓解这个问题的标准答案FPGA 厂商很早就意识到这个问题。Xilinx 从 Virtex-4 时代就开始在 IO 附近集成可编程延迟单元到了 7 系列是 IDELAYE2UltraScale 和 UltraScale 系列则升级为 IDELAYE3。名字里带 IDELAY 的就是“Input Delay”专门用来对进入 FPGA 的输入信号做精细延时调节。它的工作方式可以理解成每个输入引脚旁边都有一颗可调的“延迟旋钮”你可以通过配置寄存器、或者运行中的动态控制信号把该 lane 的输入信号往后推若干个 tap。每个 tap 的延迟很小小到几十皮秒量级所以可以做到非常精细的对齐。有了这个东西板级工程师就不用再为了 0.1mm 的等长误差反复改版。同理温度变了、电压漂了也可以通过重新调整延迟值或开启自动补偿来把采样点拉回眼图中心。这也是为什么在 LVDS 接收、特别是多 lane 并行 LVDS 接收场景里IDELAY 几乎是必选项。2. Ultrascale 系可编程输入延迟的资源底子2.1 IDELAYE3 在输入通道里的位置聊资源之前先把数据通路画出来。以典型的 LVDS 接收为例信号从引脚进来后经过的路径是LVDS 差分对 - IBUFDS_DIFF_OUT - IDELAYE3 - ISERDESE3 - FPGA 内部逻辑IBUFDS_DIFF_OUT 完成差分转单端把 LVDS 电平转成 FPGA 内部的单端信号。IDELAYE3 就接在它后面对这个单端信号做延迟。延迟完的信号再交给 ISERDESE3 完成串并转换最后变成并行数据进 fabric。这里要注意 IDELAYE3 的输入有两个来源一个是IDATAIN专门接来自 IO 引脚的数据另一个是DATAIN可以接 FPGA 内部逻辑产生的信号。LVDS 接收场景里用的是前者——IDATAIN。有人会把这两个输入接反导致绕了半天的延迟根本没作用在引脚数据上这个问题后面避坑部分我会再提。从物理位置上看IDELAYE3 是嵌在 IO Bank 内部的和 IOB、ISERDESE3 紧挨着。这也是为什么它能补偿“片外 片内”的组合偏差——它处在输入路径的最前端在信号进入内部逻辑之前就把时间关系捋平了。2.2 IDELAYE3 和 IDELAYE2 到底改了什么如果你是 7 系列的老用户对 IDELAYE2 应该不陌生。到了 UltraScale 系资源升级成了 IDELAYE3虽然功能大方向一致但很多细节必须重新理解。我列个表方便对照对比项7 系列 IDELAYE2UltraScale/UltraScale IDELAYE3参考时钟校准需要单独例化 IDELAYCTRL不再需要独立 IDELAYCTRL机制内建延迟控制位宽CNTVALUEIN 为 5 bitCNTVALUEIN 为 9 bit范围更大默认参考时钟频率通常 200MHz通常 300MHz单 tap 延迟分辨率约 78ps 200MHz refclk约 52ps 300MHz refclk工作模式FIXED / VARIABLE / VAR_LOADFIXED / VARIABLE / VARIABLE_LOAD / VARIABLE_LOAD_SYNC温度电压补偿依赖 IDELAYCTRL 统一校准增加 EN_VTC 端口可独立控制级联能力常规用法不涉及支持 CASCADE 级联扩展延迟范围一个最直观的变化是位宽。IDELAYE2 的 CNTVALUEIN 只有 5 bit满打满算 32 个 tapIDELAYE3 的 CNTVALUEIN 是 9 bit可表示的 tap 数量多得多。这意味着 UltraScale 系能补偿的延迟范围更大也更适合多 lane、高速率、大 skew 的场景。另一个重要变化是 IDELAYCTRL 被拿掉了。7 系列里你必须在工程里例化一个 IDELAYCTRL它负责给所有 IDELAYE2 提供统一的参考时钟校准。到了 UltraScale 系这套校准机制从“外部原语”变成了“架构内建”你不再需要在代码里单独例化 IDELAYCTRL只需要告诉 IDELAYE3 参考时钟的频率参数即可。很多人从 7 系列迁移到 UltraScale 时还在到处找参考时钟怎么接其实这一代已经不需要了。2.3 tap 分辨率怎么算参考时钟怎么选IDELAYE3 的每个 tap 到底延时多少不是拍脑袋定的而是由参考时钟频率决定的。计算公式是tap 分辨率 1 / (64 × FREF)其中 FREF 是参考时钟频率。为什么是 64这是 Xilinx 内部延迟链校准机制决定的你可以把它理解成参考时钟周期被均匀切成了 64 份。用 300MHz 参考时钟算一下1 / (64 × 300MHz) 1 / 19.2GHz ≈ 52ps也就是说refclk 每提高一点tap 的“步进”就更细。如果参考时钟是 200MHz单 tap 分辨率就只有 78ps。对于高速 LVDS 接收比如单 lane 跑到 1.25Gbps一个 UI 是 800ps52ps 的步进足够把采样点调到眼图中心如果只有 78ps虽然也能用但选点粒度会粗一点眼图中心的命中精度会差一些。实际工程里参考时钟怎么选有几个原则。第一不要刻意去搞一个非常高频率的参考时钟追求极小 tap因为参考时钟自身的抖动会直接映射到延迟链上。第二最好直接复用系统里已经存在的高质量时钟比如给 GTP/GTY 参考的差分时钟或者 PCIe 参考时钟。第三如果控制逻辑的时钟和参考时钟不是同源要注意跨时钟域问题或者用 UPDATE_MODE 把更新行为同步到确定时钟域。需要提醒的是tap 分辨率公式计算出来的是理论值。真实延迟会受到工艺、温度、电压影响。所以你在工程里看到的实际 tap 延迟会围绕理论值有少量偏差。这也是为什么我会建议后期用“扫描眼图”的方式去定最终延迟值而不是拿计算器算完就写死在工程里。计算只负责给初值测量才负责给终值。3. IDELAYE3 配置与 LVDS 接收链路例化3.1 什么时候该手写原语什么时候用 IPXilinx 提供了 SelectIO Interface IP可以在 GUI 里勾选 IDELAY 配置。如果你的需求非常简单比如固定延迟、不用动态调整、lane 数量也不多用 IP 确实省事。但我个人在 LVDS 接收项目里更倾向于手写原语。原因有三个。第一SelectIO IP 的很多参数是封装好的真到了需要运行中动态配延迟的时候绕到 AXI 接口上反而麻烦。第二你需要在同一个工程里对多个 lane 做“相互独立但步调一致”的延迟调整手写原语可以直接用 Generate 循环批量例化代码更简洁。第三FPGA 调试免不了要用 ILA 去观察每个 lane 的 CNTVALUEOUT手写原语可以很方便地把这些信号引到顶层IP 方式反而不好操作。3.2 IDELAYE3 完整例化与端口说明下面给一份可以直接放进工程里参考的 IDELAYE3 例化代码。参数按 UltraScale 常用配置写注释给得比较细。IDELAYE3 #( .CASCADE (NONE), // 不使用级联 .DELAY_FORMAT (COUNT), // 延迟值单位COUNT 表示 tap 数可配 TIME .DELAY_SRC (IDATAIN), // 延迟源IDATAIN 表示来自引脚的数据 .DELAY_TYPE (VARIABLE_LOAD), // 动态加载模式 .DELAY_VALUE (0), // 初始 tap 值 .IS_CLK_INVERTED (1b0), .IS_RST_INVERTED (1b0), .REFCLK_FREQUENCY (300.0), // 参考时钟频率单位 MHz .SIM_DEVICE (ULTRASCALE_PLUS), .UPDATE_MODE (ASYNC) // 延迟更新与 CLK 异步 ) idelaye3_inst ( .CASC_OUT ( ), // 级联输出 .CNTVALUEOUT (idelay_value_out ), // 当前 tap 值回读 .DATAOUT (idata_delayed ), // 延迟后数据接 ISERDESE3.D .CASC_IN (1b0 ), // 级联输入 .CASC_RETURN (1b0 ), // 级联回路输入 .CE (idelay_ce ), // 使能 INC/增减 .CLK (ctrl_clk ), // 控制时钟 .CNTVALUEIN (idelay_value_in ), // 要加载的 tap 值 .DATAIN (1b0 ), // 内部数据输入本例不用 .IDATAIN (lvds_data_single_end), // 来自 IBUFDS_DIFF_OUT 的单端数据 .INC (idelay_inc ), // 增/减控制 .LOAD (idelay_load ), // 加载 CNTVALUEIN .RST (rst ), // 复位 .EN_VTC (1b0 ) // 温度电压补偿使能 );这段代码里几个关键信号的意思CNTVALUEIN是 9 bit 的 tap 值输入配合LOAD信号使用。LOAD拉高的时钟沿会把CNTVALUEIN的值加载进延迟链。CNTVALUEOUT是当前实际生效的 tap 值调试时通过 ILA 观察这个信号能确认延迟配置有没有真正写进去。CE和INC是 VARIABLE 模式下用的配合使用可以按步进加一或减一。VARIABLE_LOAD 模式下这两个信号可以常低。EN_VTC是温度电压补偿使能。如果是动态训练流程建议先把 VTC 关掉训练完成后再打开否则你写入的延迟值可能被 VTC 的自动调整逻辑覆盖导致“写了没效果”的错觉。如果选择 FIXED 模式代码会更简单只需要把DELAY_TYPE改成FIXED然后通过DELAY_VALUE给一个固定的 tap 数动态控制相关的信号都可以不接或者置为常值。3.3 四种模式到底怎么选IDELAYE3 的工作模式决定了延迟值是由静态参数还是动态信号决定。选型判断直接影响后期调试的灵活性我一般这么区分模式调整方式典型应用场景FIXED由参数 DELAY_VALUE 决定上电固定静态 skew 补偿调试定稿后的交付版本VARIABLE通过 CE INC 按 tap 步进增/减简单动态微调临时补偿VARIABLE_LOAD通过 CNTVALUEIN LOAD 加载任意值自动训练、上电扫描、寄存器后门访问VARIABLE_LOAD_SYNC同步加载模式行为更确定对延迟更新时序有严格要求的链路个人建议调试阶段直接用 VARIABLE_LOAD。这个模式最灵活你可以把CNTVALUEIN挂到一组寄存器上通过 JTAG、AXI-Lite、UART 随便什么方式在线改延迟值调试效率极高。等所有 lane 的最优延迟值都确定下来再把DELAY_TYPE改成 FIXED把最优 tap 数写进参数完成固化。当然如果项目对温度变化非常敏感交付版本也可以保留 VARIABLE_LOAD配合一个上电自动校准模块每次上电重新扫描一遍延迟值。这种做法比 FIXED 更稳代价是多写一点逻辑。3.4 和 ISERDESE3 组成一条完整 LVDS 接收链路光有 IDELAYE3 还不够LVDS 接收真正干活的是 ISERDESE3。给一个简化但完整的示例展示了 IDELAYE3 和 ISERDESE3 的级联方式。module lvds_rx_7to1 #( parameter integer IDELAY_VALUE 0 )( input wire clk, // bit 时钟 input wire clk_div, // 字时钟也是控制时钟 input wire rst, input wire lvds_data_p, input wire lvds_data_n, input wire [8:0] idelay_cnt, // 外部送来的目标 tap 值 input wire idelay_load, // 外部送来的加载使能 output wire [7:0] data_out ); wire data_in, data_delayed; // 差分转单端 IBUFDS_DIFF_OUT #( .DIFF_TERM(TRUE) ) ibufds_inst ( .I (lvds_data_p), .IB (lvds_data_n), .O (data_in), .OB () ); // 输入延迟 IDELAYE3 #( .DELAY_FORMAT (COUNT), .DELAY_SRC (IDATAIN), .DELAY_TYPE (VARIABLE_LOAD), .DELAY_VALUE (IDELAY_VALUE), .REFCLK_FREQUENCY (300.0), .UPDATE_MODE (ASYNC) ) idelaye3_inst ( .IDATAIN (data_in), .DATAOUT (data_delayed), .CNTVALUEIN (idelay_cnt), .LOAD (idelay_load), .CE (1b0), .INC (1b0), .CLK (clk_div), .RST (rst), .EN_VTC (1b0) ); // 串并转换7:1 ISERDESE3 #( .DATA_WIDTH(8) ) iserdese3_inst ( .D (data_delayed), .CLK (clk), .CLKB(~clk), .RST (rst), .Q (data_out) ); endmodule这个模块里IDELAYE3 对进来的数据做延迟DATAOUT直接接到 ISERDESE3 的D输入。ISERDESE3 在 bit 时钟clk的上下沿采样完成串并转换。这里要特别提一句时钟处理。示例里时钟通道没有画完整实际工程中 LVDS 的随路时钟要单独走一条 IBUFDS然后接 BUFG 或者 MMCM产生 bit 时钟和字时钟。clk对应 bit 时钟速率最高clk_div对应字时钟频率是 bit 时钟除以并行位宽。IDELAYE3 的CLK我习惯用clk_div因为动态加载、CE/INC 这些控制信号本身频率不高用字时钟域逻辑去控制更安全。关于 ISERDESE3 的DATA_WIDTH这个参数决定并行输出位宽和你的 LVDS 协议格式有关比如常见的 7:1 或 4:1。不同位宽下ISERDESE3 的时序要求会不同具体以 UG578 为准。4. 调试、避坑与自动训练思路4.1 常见问题的快速排查表做 LVDS 接收时我见过太多调试现场把典型现象和排查方向整理成了下面这张表遇到类似问题可以直接对照现象可能原因处理办法只有个别 lane 数据错误lane 间 skew 差异过大逐 lane 调整 IDELAY观察 CNTVALUEOUT动态加载延迟时出现瞬间误码LOAD/CE 与数据时钟关系没处理好将控制时钟切到字时钟域必要时用 VARIABLE_LOAD_SYNC上电正常高温一段时间后突发误码温度漂移导致采样点偏移开启 EN_VTC或重新扫描延迟并校准示波器看眼图很好FPGA 仍误码采样点落在数据跳变沿调延迟让采样点对准眼图中心延迟配置写了但 CNTVALUEOUT 不变DELAY_SRC 选错或 LOAD 时序异常检查 DELAY_SRC 是否为 IDATAIN确认 LOAD 满足建立保持时间ILA 观察时好时坏时序本来就处于临界区先把 skew 和延迟值调好再上逻辑分析仪验证4.2 延迟值扫描的土办法与自动训练路线调试 LVDS 接收最朴素也最有效的办法就是“扫延迟”。把测试 pattern 设为 PRBS接收端做误码检测然后让 IDELAYE3 的 tap 值从 0 开始递增每个值跑一段时间记录是否误码。把所有“不误码的 tap 区间”找出来取中间值作为最终配置。如果不想一开始就写复杂的训练逻辑可以用 Vivado ILA 手扫。把CNTVALUEIN做成一个可以通过 ILA 在线修改的信号跑起来后每次改一个 tap 值观察误码统计或者直接用眼睛看抓到的数据是否稳定。虽然慢但很直观适合先把链路调通、了解大概窗口位置。等需求稳定后再把它升级成真正的自动训练模块。模块的核心逻辑是复位后进入扫描状态tap从DELAY_MIN开始对每个 tap 值等待一段固定的“观察时间”观察时间内如果误码计数器没有超过阈值标记这个 tap 为“有效”扫描结束后在所有“有效 tap”里选中间值写入 IDELAYE3然后通知上层训练完成链路进入工作状态。这个流程听起来简单但有几个实施细节要注意。第一观察时间不能太短否则会把瞬时毛刺当成有效也不能太长否则上电初始化时间不可接受。我一般先按一个 bit 错误就立刻淘汰这个 tap 值来扫第一轮求区间第二轮用较长观察时间精挑。第二扫描过程中要保证测试 pattern 一直在发且接收端误码检测器的复位状态要处理好。第三扫描结束后如果发现“有效区间”宽度很窄说明链路余量本身不足别硬靠延迟去兜先回头查硬件。4.3 几个容易忽略的底层细节第一个要提醒的是参考时钟的干净程度。IDELAYE3 的 tap 精度依赖参考时钟的周期稳定性。参考时钟抖动大的话每个 tap 的实际延迟也会抖最终采样点会跟着晃。所以不要随便拿一个普通 IO 产生的内部时钟当参考最好用板上已有时钟源、走专用时钟引脚进来的信号。第二个细节是延迟值别贴着边界用。每个延迟链都有线性度比较好的区域太靠近 0 或者太靠近最大值的位置tap 与 tap 之间的差值可能不均匀甚至出现非线性。扫描后如果最优值落在边界附近我会慎重考虑这个设计是否稳妥——更合理的做法是调整 PCB 或者换一个 lane 映射让最优工作点尽量落在中间区域。第三个细节是 IDELAYE3 的放置。它必须在 IO 附近使用不能把它当作普通内部逻辑来搬移。如果综合后看到告警说延迟单元无法正确布局先检查是不是因为在代码里把它放到了非 IO 路径上或者被综合工具优化掉了。需要保持原语不优化时可以加(* dont_touch yes *)这类综合属性。第四个细节是 EN_VTC 和动态训练的关系。VTC 是 UltraScale 系的温度电压自动补偿功能初衷是让延迟值跟随环境漂移自动修正。但这东西和动态加载天然冲突你前脚通过CNTVALUEIN写入一个值后脚 VTC 可能又把它改回去了。所以我的顺序是训练阶段把 EN_VTC 拉低等训练完成、最优值写入后再拉高 EN_VTC让它接管后续的微调整。4.4 延迟调整和字对齐的顺序问题最后再讲一个容易踩的坑IDELAY 调整解决的是“位采样点”问题字对齐解决的是“并行字边界”问题两者别混在一起。在 LVDS 接收链路里ISERDESE3 完成串并转换之后如果并行数据是 7 bit 或者 8 bit你还要做字对齐才能知道每一组并行数据对应的正确边界。如果先做完字对齐然后又去大幅调整 IDELAY 的 tap 值数据被整体推移后原本的字边界可能就错了。所以在调试顺序上我强烈建议第一步先用粗延迟把所有 lane 的采样点大致对准眼图中心第二步做字对齐/训练序列检测把并行输出调到正确的边界第三步再做精细的 IDELAY 微调并且微调幅度不要太大第四步重新验证一遍字对齐状态确保没有因为延迟调整丢掉同步。这个流程虽然多了一步验证但能避免后面出现“明明眼图看着很好但收上来的数据就是不对”的诡异问题。结尾这篇先聊到这儿。我个人在 UltraScale 上做 LVDS 接收初期最喜欢把 IDELAYE3 配成 VARIABLE_LOAD然后把CNTVALUEIN挂到一组 AXI-Lite 寄存器上。这样做的好处是调试时不用反复重新综合直接在软件里改寄存器就能观察每个 lane 的延迟变化效率高很多。等波形稳定、最优 tap 值确定下来再把代码改成 FIXED 模式交付风险小、问题也少。另外一个小技巧如果时间允许尽量把每个 lane 的CNTVALUEOUT都引到顶层留个测试点哪怕最终版本不用。真到了现场联调出问题的时候这几根线能帮你省下大把抓头发的时间。下一部分我会重点讲怎么在 UltraScale 系上写一个完整的自动延迟训练模块包括扫描状态机、误码统计、最优 tap 选点以及和 ISERDESE3 字对齐的联调细节。到时候我会把可以综合的代码放出来咱们一起把这套流程彻底跑通。