前阵子帮朋友排查一块 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 周期只有 1ns(1000ps)。如果 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 系列是 IDELAYE2,UltraScale 和 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 系列 IDELAYE2 | UltraScale/UltraScale+ IDELAYE3 |
|---|---|---|
| 参考时钟校准 | 需要单独例化 IDELAYCTRL | 不再需要独立 IDELAYCTRL,机制内建 |
| 延迟控制位宽 | CNTVALUEIN 为 5 bit | CNTVALUEIN 为 9 bit,范围更大 |
| 默认参考时钟频率 | 通常 200MHz | 通常 300MHz |
| 单 tap 延迟分辨率 | 约 78ps @ 200MHz refclk | 约 52ps @ 300MHz refclk |
| 工作模式 | FIXED / VARIABLE / VAR_LOAD | FIXED / VARIABLE / VARIABLE_LOAD / VARIABLE_LOAD_SYNC |
| 温度电压补偿 | 依赖 IDELAYCTRL 统一校准 | 增加 EN_VTC 端口,可独立控制 |
| 级联能力 | 常规用法不涉及 | 支持 CASCADE 级联扩展延迟范围 |
一个最直观的变化是位宽。IDELAYE2 的 CNTVALUEIN 只有 5 bit,满打满算 32 个 tap;IDELAYE3 的 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 是 800ps,52ps 的步进足够把采样点调到眼图中心;如果只有 78ps,虽然也能用,但选点粒度会粗一点,眼图中心的命中精度会差一些。
实际工程里,参考时钟怎么选有几个原则。第一,不要刻意去搞一个非常高频率的参考时钟追求极小 tap,因为参考时钟自身的抖动会直接映射到延迟链上。第二,最好直接复用系统里已经存在的高质量时钟,比如给 GTP/GTY 参考的差分时钟,或者 PCIe 参考时钟。第三,如果控制逻辑的时钟和参考时钟不是同源,要注意跨时钟域问题,或者用 UPDATE_MODE 把更新行为同步到确定时钟域。
需要提醒的是,tap 分辨率公式计算出来的是理论值。真实延迟会受到工艺、温度、电压影响。所以你在工程里看到的实际 tap 延迟,会围绕理论值有少量偏差。这也是为什么我会建议后期用“扫描眼图”的方式去定最终延迟值,而不是拿计算器算完就写死在工程里。计算只负责给初值,测量才负责给终值。
3. IDELAYE3 配置与 LVDS 接收链路例化
3.1 什么时候该手写原语,什么时候用 IP
Xilinx 提供了 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 (1'b0), .IS_RST_INVERTED (1'b0), .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 (1'b0 ), // 级联输入 .CASC_RETURN (1'b0 ), // 级联回路输入 .CE (idelay_ce ), // 使能 INC/增减 .CLK (ctrl_clk ), // 控制时钟 .CNTVALUEIN (idelay_value_in ), // 要加载的 tap 值 .DATAIN (1'b0 ), // 内部数据输入,本例不用 .IDATAIN (lvds_data_single_end), // 来自 IBUFDS_DIFF_OUT 的单端数据 .INC (idelay_inc ), // 增/减控制 .LOAD (idelay_load ), // 加载 CNTVALUEIN .RST (rst ), // 复位 .EN_VTC (1'b0 ) // 温度电压补偿使能 );这段代码里几个关键信号的意思:
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 (1'b0), .INC (1'b0), .CLK (clk_div), .RST (rst), .EN_VTC (1'b0) ); // 串并转换,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 字对齐的联调细节。到时候我会把可以综合的代码放出来,咱们一起把这套流程彻底跑通。