做FPGA的,谁没被LVDS折腾过几次。尤其是高速ADC、相机接口、雷达数据采集这类的板卡,动不动就是十几对差分线从外部进来,板上走线稍微有点偏差,数据就是满屏乱码。以前我拿到这类需求也喜欢自己写IBUFDS、ISERDES原语,感觉更能控制细节,后来踩的坑多了,才慢慢养成用Vivado SelectIO Interface IP的习惯,把LVDS接收的差分转单端、延迟调节、串并转换这些脏活都交给IP去例化,自己只关心数据对齐和上层协议。这篇帖子就从一个实际项目的角度,把“SelectIO IP配置 + Bitslip自动训练 + 仿真验证”这条完整链路拆开讲一遍,内容包括参数怎么填、时钟怎么接、训练状态机怎么写、tb怎么搭,以及上板后最容易栽的几个坑。适合刚接触LVDS接收、或者已经被数据错位折磨到想砸板子的朋友参考。
1. 选型分析:用IP还是手撸原语
1.1 LVDS接收链路到底在做什么
先别急着开Vivado,把接收链路想清楚。LVDS本质是差分串行数据,一对线上每个时钟周期传1个bit(SDR)或2个bit(DDR),接收端FPGA要完成三件事:第一,用IBUFDS把差分信号转成单端逻辑电平;第二,用IDELAY对输入数据做可编程延迟,补偿PCB走线长度差和芯片内部的时钟数据偏移;第三,用ISERDES把高速串行数据转换成内部并行总线,让后面的逻辑可以在低速时钟下处理。
很多朋友在最开始容易忽略IDELAY。LVDS是源同步接口,数据和随路时钟一起从发送端过来,但到了FPGA引脚内部,数据和时钟经过的路径完全不一样,多路LVDS之间的走线也不可能做到完全等长。你直接用BUFIO采数据,可能在这块板子上没问题,换个批次的板子、换根连接器就崩了。IDELAY就是干这个的,它把数据延迟细粒度地拉开,让采样点落在眼图正中间。
ISERDES则是解决“并转串”的速度差。FPGA内部逻辑跑100MHz、200MHz没问题,但外部LVDS数据率可能到800Mbps甚至更高,内部逻辑跟不上,就必须先串转并。ISERDES的作用就是把高速串行bit流按固定倍数展开,比如8个bit并成一个字节输出。这个“按固定倍数展开”听着简单,实际落地时最大的难题是:你并出来的第0位到底对应外部串行数据流的哪一位,这是整个LVDS接收最核心的问题,后面第3章会重点讲。
1.2 IP方案和手写原语的取舍
LVDS接收逻辑看起来不外乎IBUFDS + IDELAYE2 + ISERDESE2几个原语,很多老工程师也习惯了手写。手写的好处是可控性好,尤其在做精细延迟调节、DQS门控这类特殊场景时,IP反而碍手碍脚。但付出的代价是,你要自己搞清楚一整套时序约束,包括输入延迟约束、跨时钟域约束、BUFIO/BUFR的使用、IDELAYCTRL参考时钟的连接,任何一个环节漏掉,综合不报错,上板就是跑不稳。
SelectIO Interface IP把这些都封装好了。你把并行位宽、数据速率、串行因子、是否差分、是否使用IDELAY等参数一填,IP自动帮你例化原语、生成约束、提供仿真模型。Vivado综合和实现时,IP自带约束会保证BUFIO/BUFR这些专用时钟资源被正确使用,这比手写原语后自己补XDC靠谱得多。
我的建议是,常规的LVDS、LVDS_25、Sub-LVDS接收,尤其是DDR模式、速率在几百Mbps到1Gbps左右的场景,直接用IP,省出来的时间拿去调训练逻辑和数据校验,收益更大。只有当你需要极细粒度延迟调节、或者数据源和标准LVDS时序差异很大的时候,才退回手写原语。
1.3 一个能直接套用的接收方案框图
整个LVDS接收方案可以拆成三段。第一段是物理层,包括SelectIO IP,完成差分转单端、延迟调节、串并转换,并对外提供bitslip接口;第二段是链路训练逻辑,用一个状态机检测训练序列,发现word边界不对就自动拉一次bitslip,直到并行输出稳定匹配训练码;第三段是数据有效信号和用户逻辑,训练完成之后lock信号拉高,后续数据才能被上层当作有效数据使用。
这里要提前说明一个关键概念:SelectIO IP本身并没有“自动训练”这个功能,它只提供bitslip接口,让用户每次拉高一个脉冲,内部ISERDES输出就循环移位一位。真正的“自动训练”是用户在外部自己写一个闭环逻辑,把“检测训练码 + 触发bitslip + 验证对齐”做成状态机。工程上说的LVDS自动对齐,指的是这整套闭环,这个理解贯穿全文,后面不再重复。
2. Vivado SelectIO IP配置与参数计算
2.1 在IP Catalog里选型,参数这么填
打开Vivado的IP Catalog,搜索“SelectIO Interface”,双击创建一个输入模式的IP。关键的参数分成几组,逐个说。
第一组是“Input Data Type”。这里选DDR还是SDR,取决于上游芯片的接口类型。常见的LVDS ADC、相机传感器很多是DDR模式,也就是时钟的上升沿和下降沿都传数据。第二组是“Data Bus Width”,指的是串并转换之后并行总线的宽度,比如8bit、16bit。第三组是“Serialization Factor”,即串行化因子,DDR模式下合法值通常是4、6、8,SDR模式可以是2到8。这个参数决定了ISERDES内部展开倍数,也决定了串行时钟和并行时钟之间的频率关系。
参数之间有一个很基本、但经常被人搞混的计算公式。以并行8bit、DDR模式、串行化因子8为例,并行时钟(clk_div_in)如果是100MHz,那么ISERDES的串行时钟(clk_in)频率就是并行时钟频率乘以串行化因子再除以2,也就是100MHz×8/2=400MHz。此时每条LVDS通道的数据率就是400MHz×2=800Mbps。换句话说,并行位宽除以2之后,乘以并行时钟频率,就是数据率。
还有几个容易忽略的选项。如果你的外部信号是差分对,勾选“Differential”;“Use IDELAY”建议打开,给后面训练留出调节空间;“Enable Bitslip”必须打开,否则没法做word对齐;“Data Bus Order”根据芯片手册选择MSB First还是LSB First,选反了会发现并出来的数据高低位完全倒置,仿真都过不了。这些参数填完后,IP会生成一个例化模板,里面所有原语细节都被封装好,不用去碰。
2.2 时钟方案:clk_in、clk_div_in与BUFIO/BUFR
SelectIO IP的时钟连接是比较容易翻车的地方。ISERDES工作时需要两个时钟:高速串行时钟clk_in和低速并行时钟clk_div_in。clk_in通常来自LVDS随路时钟,经过IBUFDS后接入BUFIO,再由BUFIO驱动ISERDES的CLK输入。clk_div_in则由同一个时钟源分频得到,一般由BUFR产生,这样能保证两个时钟同源且相位关系稳定。
在IP配置里,时钟部分有“Internal”和“External”两种模式。Internal模式由IP内部例化BUFR自动分频,用起来最省心,你只需要把源同步时钟送到clk_in,IP内部会自己生成分频时钟,推荐工程上优先用这种。External模式则要求用户外部提供clk_div_in,灵活性更高,但你必须保证clk_div_in和clk_in的相位关系,很多上板调试半天数据不对的,都是这个分频时钟来自不同MMCM或经过了BUFG,导致相位关系不满足ISERDES要求。
需要注意,如果开了IDELAY,IP内部会例化IDELAYCTRL,它需要一个200MHz的参考时钟。这个时钟从哪里来要提前规划,可以用专门的MMCM输出200MHz接到全局时钟网络,也可以复用板上已有的200MHz时钟源。仿真阶段往往不关心这个,但上板实现时IDELAYCTRL没有参考时钟,整个延迟链就是废的,体现出来的现象是IDELAY调节完全没反应,或者数据怎么调都有大量误码。
2.3 引脚约束、电平标准与DRC RTSTAT-2
IP配置完了,XDC引脚约束是另一个大坑。LVDS输入引脚首先要约束电平标准,常用的有LVDS和LVDS_25,具体用哪个取决于FPGA Bank的VCCO电压。2.5V Bank用LVDS_25,1.8V Bank用LVDS,也有不少器件支持LVDS。除了IOSTANDARD,还要给差分对设置终端电阻,XDC里写法是这样:
set_property -dict {PACKAGE_PIN AE14 IOSTANDARD LVDS_25 DIFF_TERM TRUE} [get_ports {rx_data_p[0]}] set_property -dict {PACKAGE_PIN AF14 IOSTANDARD LVDS_25 DIFF_TERM TRUE} [get_ports {rx_data_n[0]}]DIFF_TERM的作用是使能FPGA内部跨在差分P/N引脚之间的100欧姆差分终端电阻。LVDS标准规定驱动器在100欧姆终端上产生350mV左右的差分摆幅,如果你没开终端或者板上已经贴了外部电阻,信号反射和幅度不足会让接收端数据质量很差。所以务必确认:板级没有外部端接时,用内部DIFF_TERM;板上已经放了端接电阻,就不要重复设置,否则等效阻抗可能变成50欧姆,信号幅度反而异常。
很多人会在实现阶段碰到一个DRC报错,叫drc rtstat-2。这类报错通常和Static pin constraint、Clock Region的资源分配有关,常见于大量差分引脚挤在同一个Clock Region,或者BUFIO/BUFR的放置与引脚位置严重不匹配。处理方法不是硬改DRC,而是先看报错里具体指出了哪个网表和Cell,然后检查约束文件里的信号是否真的在它所属的Clock Region内,必要时调整引脚到其他Bank,或者给相关时钟网络加上合理的时钟区域约束。这个报错在纯RTL仿真阶段根本不会暴露,一般要到Implementation才能发现,所以建议在设计初期就按Clock Region规划好引脚,不要元素凑到最后一锅烩。
3. 自动训练的核心:Bitslip对齐机制
3.1 为什么要做自动对齐
LVDS接收并出来的并行数据,一开始是没有“边界感”的。假设外部发送端按字节发送0xBC,也就是串行bit流11011100,但接收端ISERDES并不知道哪一位是字节的起点。如果起点刚好错了一位,它会截成10111001(0xB9)。这种情况靠改代码是治不好的,因为问题出在链路延迟、时钟相位差、PCB走线长度差这些物理因素上,每次上电都可能不一样。
所以必须让接收端具备一种能力:动态调整并行数据的截取位置,直到截出来的数值和约定的训练码一致。这个动作在SelectIO IP里就是bitslip。每次在clk_div_in域给bitslip一个脉冲,ISERDES的输出数据就会在内部做一次循环移位,相当于把并行word的边界往后挪一位。如果当前截取位置不对,那就拉一次bitslip,再看一次结果,如此反复,直到匹配上训练码。
我有一个很直观的类比:bitslip像在火车站找站名,但有个捣蛋的人把站牌上的字随机裁掉了一部分,你每次只能看到一条很窄的缝。你的任务是按一下“滚动”按钮,让站牌整体平移一个字,再看看缝里露出来的字是不是站名。不是,就再按一下,直到站名完整出现在缝里为止。
3.2 训练序列怎么选
训练码不是随便挑一个值就行的。它必须满足一个条件:在“错误截取”的情况下,不能大概率地也匹配上。比如你用8bit并行,训练码选择0xFF,这是最差的选择,因为任意一位错开时,只要错开的那位恰好也是1,就会被误判。选择0x00同理。
好的训练序列通常具有比较好的自相关特性,常见的选择包括K28.5(8b/10b编码中的特殊字符,10bit下是0x1BC,取8bit时往往是0xBC)、PRBS序列的短截段、或者干脆用一个16bit的自定义pattern。如果并行宽度是8bit,我建议训练码至少用16bit来验证,因为8bit的0xBC在循环移位后,仍可能存在一个相邻的候选位置也能匹配成功,尤其是当外部数据流里恰好有一段正好和旋转变体相同。
实际工程里,我通常会选两组训练词,一组是正向对齐码,一组是反向校验码。例如发送端先发0xBC重复若干拍,接收端在这段窗口内完成bitslip搜索;锁定后再发0x35,如果接收端能稳定解出0x35,说明不是“碰巧匹配”,而是真锁对了。这种双向验证的方式在上板调链路时非常好用。
3.3 对齐状态机设计与Verilog实现
自动训练状态机可以按“搜索-移位等待-验证-锁定”四个状态来设计。初始状态搜索,每个clk_div周期比较data_in和训练码;如果匹配上,进入验证状态,连续匹配若干拍后认为真正对齐,拉高locked;如果没匹配上,拉一次bitslip脉冲,进入等待状态,等ISERDES完成移位后再回到搜索状态。
这个状态机可以直接用Verilog写出来,下面是我在实际项目里用过的简化版本:
module lvds_align #( parameter int DATA_WIDTH = 8, parameter bit [DATA_WIDTH-1:0] TRAIN_PATTERN = 8'hBC, parameter int VERIFY_CNT = 8, parameter int SLIP_WAIT_CNT = 4 )( input logic clk_div, input logic rst_n, input logic [DATA_WIDTH-1:0] data_in, output logic bitslip, output logic locked, output logic [DATA_WIDTH-1:0] data_aligned ); typedef enum logic [2:0] { ST_SEARCH, ST_SLIP_WAIT, ST_VERIFY, ST_LOCKED } state_t; state_t state; logic [$clog2(VERIFY_CNT+1)-1:0] v_cnt; logic [$clog2(SLIP_WAIT_CNT+1)-1:0] w_cnt; assign data_aligned = data_in; always_ff @(posedge clk_div or negedge rst_n) begin if (!rst_n) begin state <= ST_SEARCH; bitslip <= 1'b0; locked <= 1'b0; v_cnt <= '0; w_cnt <= '0; end else begin bitslip <= 1'b0; case (state) ST_SEARCH: begin if (data_in == TRAIN_PATTERN) begin v_cnt <= 1; state <= ST_VERIFY; end else begin w_cnt <= '0; bitslip <= 1'b1; state <= ST_SLIP_WAIT; end end ST_SLIP_WAIT: begin if (w_cnt == SLIP_WAIT_CNT) begin state <= ST_SEARCH; end else begin w_cnt <= w_cnt + 1; end end ST_VERIFY: begin if (data_in == TRAIN_PATTERN) begin if (v_cnt == VERIFY_CNT) begin state <= ST_LOCKED; end else begin v_cnt <= v_cnt + 1; end end else begin state <= ST_SEARCH; v_cnt <= '0; end end ST_LOCKED: begin locked <= 1'b1; end endcase end end endmodule这里面有几个时序细节值得强调。bitslip脉冲只拉高一拍,但ISERDES内部完成移位并让data_out稳定下来需要时间,所以ST_SLIP_WAIT这个等待状态不能省。不同模式需要的等待拍数不一样,一般等4到8个clk_div周期比较保险。另外,搜索匹配是在clk_div域做的,而data_in由IP输出,理论上直接比较没有问题,但如果你在真实工程里发现data_in在训练时会有中间态毛刺,建议在状态机前加一级寄存器打拍,避免比较电路上产生亚稳态。
多通道LVDS场景下,状态机不会只有这一个。通常以Lane0为基准,先让Lane0锁定,然后把其他通道的bitslip按相同逻辑单独跑,直到每个通道都对齐到同样的word边界。有些协议还要求所有通道必须在同一拍lock,那就要在每通道lock后增加一个汇聚状态,等所有lane都lock再统一向上层发ready信号,这个过程叫lane alignment,它和word alignment是两回事,不要混在一起。
4. 仿真流程与代码全解
4.1 怎么搭Testbench才靠谱
很多人在仿真阶段偷懒,直接用并行数据去驱动IP的data_out端口,等于把“串行转并行”这一步直接跳过了。这样做不是不能测状态机,但完全没有覆盖IBUFDS和ISERDES的行为,对实际工程参考意义不大。既然标题说了附仿真代码,这个仿真就得认真把差分串行数据真正生成出来,让时钟和数据都从“物理端口”进IP,才能提前暴露问题。
Testbench里至少要包含三个部分:一是差分时钟生成,二是串行差分数据生成,三是DUT例化和结果自动比对。差分时钟很简单,直接产生一对互补时钟接到IP的clk_in_p/clk_in_n即可。差分数据生成则是一个模型,把并行字节按选定的串行顺序,在DDR模式下逐个bit地输出到数据线上。
下面这段代码示意了最基础的差分串行数据激励怎么生成,实际使用中建议在此基础上把所有通道都做出来,方便验证多lane训练逻辑:
logic clk_ser = 1'b0; always #1.25ns clk_ser = ~clk_ser; // 400MHz 串行时钟 logic [7:0] tx_byte = 8'hBC; logic ser_data; logic ser_data_d; // DDR模式下,一个clk_ser周期输出2bit:上升沿一个,下降沿一个 always @(posedge clk_ser) begin ser_data <= tx_byte[6]; ser_data_d <= tx_byte[2]; end always @(negedge clk_ser) begin ser_data_d <= tx_byte[4]; // 示例,真实bit顺序需按协议定义 end // 生成差分对 assign rx_data_p = ser_data; assign rx_data_n = ~ser_data;这里要提醒的是,真正的串行bit顺序取决于发送端芯片和你的Data Bus Order设置,testbench里的tx_byte赋值顺序要与之一致。否则接收端即使bitslip锁对了word边界,并出来的字节也全是反的。实际项目里最靠谱的做法,是用发送端芯片的SPI模型配合其手册里的bit顺序表,先在仿真里把值对上,再上板。
4.2 核心仿真代码与波形判断
DUT例化时,SelectIO IP的例化模板直接照搬,但要把仿真模型对应的端口连对。推荐把IP的example design跑一遍,看它里面的tb是怎么处理clk_in和clk_div_in的,再在此基础上改造。我这里以外部提供clk_div_in为例,tb里同时生成400MHz的clk_ser和100MHz的clk_div,让它们严格同源,保证ISERDES的采样时钟和并行时钟相位关系是合理的。
仿真跑起来之后,重点看三个信号:data_out、bitslip、locked。训练开始前,data_out的值应该是在不停变化的,因为ISERDES在持续移位截取并行word;状态机开始逐次拉bitslip后,data_out会逐步趋近训练码0xBC;一旦连续多次匹配,locked信号拉高,此时data_out应当稳定在0xBC上。
有一种常见情况:locked已经拉高了,但data_out对应的不是0xBC,而是0xBC的某个循环移位结果。这说明你的训练码在某个bitslip状态下“碰巧”也匹配上了,但实际截取边界并不对。这种情况恰恰说明训练码选得太短,或者验证次数太少。仿真阶段遇到这种问题,先把VERIFY_CNT调大,比如从4到16,再看能否排除伪匹配;如果还不行,就要把训练码换成16bit的扩展码。
4.3 仿真中要覆盖的几种测试场景
光测一个理想相位是不够的。真实链路里,每根差分线的skew、IDELAY的延迟值、电源噪声都会影响采样位置。仿真阶段我一般会覆盖三类场景:第一,默认零延迟,验证训练状态机的基本功能;第二,给数据线叠加一个固定的bit间偏移,模拟PCB走线不等长,看训练逻辑能否在多个候选位置中找对;第三,在数据波形中穿插若干个随机误码,验证验证状态机不会因为单个误码就误判lock。
叠加偏移的方法很简单,给差分数据加上一个#0.7ns之类的延迟即可,但这个延迟不要超过一个bit周期。考虑DDR模式800Mbps时,一个bit只有1.25ns,这时候叠加0.3ns的偏移已经能明显改变采样点位置。仿真通过之后再上板,上板仍然需要扫描IDELAY值来对真实链路做细调,但至少你的训练逻辑已经经过了不同相位下的验证,排除了逻辑自身的问题。
另外,仿真时不要忘记检查复位时序。IP的io_reset和clk_reset释放要满足要求,很多IP要求复位信号至少保持几个clk_div周期。复位释放的瞬间,状态机必须是从ST_SEARCH开始,如果复位释放太快,ISERDES内部状态还没稳定,data_out会出现一段未知值,这时直接进SEARCH状态比较训练码,可能会拉掉好几个没必要的bitslip。
5. 上板常见问题与排查经验
5.1 从报错到定位:一张速查表
上板调试和仿真完全是两个世界,仿真过了只是起点。我把这几年在实际项目里遇到最多的几类问题整理成一张速查表,供大家快速对照:
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| Implementation报DRC RTSTAT-2 | IO引脚/Clock Region静态约束冲突 | 看报错指出的路径,调整引脚放置,检查BUFIO/BUFR约束 |
| Implement Design变红 | 时序不收敛或DRC critical warning | 查timing report,重点看clk_in到clk_div_in路径,以及BUFIO/BUFR使用是否违规 |
| 数据总是差几个字节/位 | word边界未对齐,bitslip没有真正生效 | 检查训练状态机时序,确保bitslip脉冲在clk_div域正确同步 |
| 偶发一位滑码 | 采样点接近眼图边缘,IDELAY余量不足 | 扫描IDELAY值,找最大裕量窗口,取中间值固化 |
| LVDS引脚没信号 | 电平标准错误、差分端接未开、Bank电压不对 | 示波器看差分P/N波形,检查IOSTANDARD、DIFF_TERM、VCCO |
| IDELAY调节没反应 | IDELAYCTRL参考时钟没接入 | 确认IDELAYCTRL有200MHz参考时钟,且工具正确连接 |
Implement Design变红这个现象里面,最常见的根因是时钟专用资源没走对。SelectIO IP的约束要求源同步时钟经过IBUFDS后走BUFIO驱动ISERDES,如果你在顶层又额外加了一个BUFG把时钟扇出,工具就可能因为时钟资源串联冲突而无法布线。解决方法是检查Implementation的Route阶段是否有unrouted nets,结合报告定位到具体时钟路径,通常做法是把BUFG去掉,让IP生成的约束自己管理时钟网络。
5.2 数据错位、偶发滑码这类顽固问题
“偶尔错一个字节”这类问题是最难查的,因为它不一定能在短时间测试里稳定复现。我遇到过一种情况,板子跑温度循环测试时,每隔几分钟就出一个错位字节,大概率是IDELAY设置的采样点落在眼图边缘,温度变化导致链路延迟漂移,采样点越过眼图边界,从而误采到相邻bit。这种问题一次性通过的仿真根本发现不了,只能上板扫IDELAY。
扫描IDELAY的方法是做一个寄存器,把delay value从0递增到最大值,每个值固定跑一段已知pattern,统计错误数,最后画出一条“延迟值-误码率”的曲线。正常情况下曲线上会有一段平坦的低误码区间,这就是眼图的开口;挑选开口区间的中点作为最终IDELAY值,给两侧都留出裕量,而不是随便挑一个零误码点。这个操作在SelectIO IP里可以通过IDELAY控制接口或者VIO核手动调,量产前再用ILA抓数据做交叉验证。
另外要提醒,训练成功和长期稳定是两回事。很多设计训练lock了,但没做运行时的误码监控,偶发滑码就漏掉,直到上层应用发现数据异常才回过头来查。稳妥的做法是在锁定后增加一个误码率计数器,一旦连续错误数量超过阈值,自动重新进入训练流程。这等于把“初始训练”扩展成了“运行时周期自检”,对无人值守的设备尤其重要。
5.3 采样点优化与长时间稳定运行的几个建议
最后分享几个让系统稳定运行的小建议。第一,训练状态机在上电后不要急着一上来就lock,先给电源和时钟稳定留出时间,尤其对带PLL的上游芯片,建议在收到上游lock信号后再启动训练。第二,多通道LVDS接收时,建议把训练码放在一个固定的“训练窗口”里,避免数据流中随机出现和训练码相同的值,干扰训练结果。第三,数据速率如果超过600Mbps,PCB上的LVDS走线阻坑控制、过孔stub长度都会直接影响眼图质量,这时候FPGA内部怎么调都救不了物理层,示波器看一眼P端N端的差分波形就什么都明白了。
还有一个小细节,我在好几个项目里反复踩过:SelectIO IP用了IDELAY之后,delay_locked信号一定要参与复位管理。IDELAYCTRL锁定之前,IDELAY输出是无效的,如果此时训练状态机已经开始跑,data_out会受到延迟链不稳定影响而出现随机跳变。我一般会把delay_locked作为状态机复位条件之一,delay_locked为低时强制待在ST_SEARCH之前,等它拉高再放开。
另外,如果你的板子上同时有多个SelectIO IP核,注意IDELAYCTRL并不是每个IP一个,而是可以共享的,Vivado在必要时会自动合并。但这要求所有使用IDELAY的IP参考时钟来源一致,如果发现仿真和实现行为不一致,先查一下是不是多个IDELAYCTRL参考时钟网络不同源导致的。
我最近一次调LVDS接收,数据一直差半个字节,最后发现是训练码匹配到了两个相邻候选位置,把训练码从8bit换成16bit、再把验证次数从4改成16之后,问题立刻消失。所以如果你也卡在这种“训练了却没有完全对齐”的状态,先别怀疑硬件,回仿真里把训练码和验证逻辑重新审视一遍,很多时候问题就出在“验证不够严格”上。LVDS调试就是这样,前面把原理和流程捋顺,后面就是耐心迭代的事,急不来。