
LVDS信号在ZYNQ上的双向传输从IBUFDS接收再到OBUFDS输出的完整链路设计做FPGA项目这么多年LVDS接口几乎是躲不开的。尤其是上了ZYNQ平台之后LVDS不仅用在ADC/DAC的数据采集上还经常要跟外部设备做双向通信——比如接一个LVDS接口的摄像头模组、连一个差分信号驱动的工业编码器或者是两块板卡之间的高速互联。今天就把我最近在ZYNQ上做的一条LVDS双向传输链路完整的梳理一遍重点放在IBUFDS和OBUFDS这对原语的使用、双向方向控制的处理、终端匹配、引脚约束和常见坑上。先说结论ZYNQ上做LVDS双向传输关键在于三件事——选对Bank、用对原语、控好方向切换。选对Bank解决的是电气标准兼容性问题用对原语解决的是差分信号与单端逻辑之间的转换问题控好方向切换解决的是双向总线在物理层不能打架的问题。三个环节只要有一个出岔子轻则数据错误重则烧毁IO。下面逐步展开给正在做或者准备做这个方向的兄弟一个可复现的参考。1. LVDS在ZYNQ平台上的基础认知1.1 LVDS到底解决了什么问题LVDS全称Low-Voltage Differential Signaling低电压差分信号。它最大的特点是利用两根线之间的电压差来传递信息而不是像单端信号那样以地平面为参考。常用的LVDS电平标准里差分摆幅大约在350mV左右共模电压在1.2V附近。这个电压摆幅比单端3.3V甚至1.8V都要低很多所以功耗优势非常明显同时由于是差分信号抗共模干扰的能力也远强于单端。传输速率方面普通的LVDS就可以跑到几百Mbps到Gbps级别在ZYNQ这种平台上跑个200-600Mbps的接口非常常见。那为什么选LVDS而不选别的拿工业场景举例如果你要接一个外部设备设备端和FPGA端可能隔着几十厘米甚至一两米的线缆机上还有电机、电源模块这种强干扰源。单端信号跑快了地弹和共模噪声能把信号眼图彻底毁掉。而LVDS因为共模噪声同时叠加在两根差分线上接收端只看差值所以噪声大部分被抵消掉了。这也就是为什么ADC、DAC、摄像头接口、编码器接口这类对噪声敏感的场合普遍采用LVDS。1.2 为什么在ZYNQ上做LVDS双向传输ZYNQ这块芯片严格来说是一个ARMFPGA的异构SoCFPGA部分叫PLProgrammable LogicARM部分叫PSProcessing System。很多项目里选择ZYNQ做LVDS接口是因为系统里不光要采数据还要做算法处理、跑Linux、上网络协议栈。LVDS采集到的数据进PL之后可以直接通过AXI总线送到PS去做后续处理也可以从PS侧下发配置参数通过LVDS差分线送出去。我这里说的双向传输是指同一条LVDS物理通道在不同时间段内既能接收外部信号又能向外发送信号。典型场景就是半双工差分总线比如RS-485在差分总线上的玩法——同一对差分线一会儿当发送线一会儿当接收线通过方向控制信号来切换。LVDS也可以这么干但需要在FPGA内部处理好IOBUFDS或者IBUFDSOBUFDS的组合逻辑并配合精确的方向切换时序。1.3 ZYNQ的Bank结构与LVDS支持约束ZYNQ-7000系列的PL侧IO分成若干Bank每个Bank有独立的VCCO电压引脚某个Bank上的IO标准必须与该Bank的VCCO匹配。这是新手最容易忽略的地方。ZYNQ-7020 CLG484封装上有几个HR Bank和HP Bank其中HP Bank的VCCO典型配置为1.8VHR Bank的VCCO则可以配到1.8V、2.5V、3.3V。LVDS标准在Xilinx FPGA上要求VCCO必须是2.5V也就是说如果你要用LVDS必须把信号放在支持2.5V VCCO的Bank上同时该Bank所有IO的电压标准都要统一。这一点在原理图设计阶段就要确认好否则板子做回来发现LVDS引脚所在的Bank被拉到了3.3V那就非常尴尬。即使强行配置成LVDS硬件上也无法保证合规信号质量很差。另外一个容易踩的坑是LVDS引脚必须分配在Bank里支持差分对的pin pair上。每个差分对由P引脚和N引脚组成Xilinx在引脚规划里已经定义好了哪些引脚可以组成合法的差分对不是任意两个IO就能拼成LVDS的。做管脚约束的时候如果用的不是芯片原生的差分对引脚综合布局布线会直接报错。2. IBUFDS与OBUFDS原语深度解读2.1 IBUFDS接收端原语拆解IBUFDS的作用是把一对差分输入信号I和IB转换成单端信号O送到FPGA内部逻辑处理。这是LVDS接收链路的最前端。它的实例化方式很简单但在工程里很多人容易把I和IB搞反或者忘了给IBUFDS加差分终端电阻。先看标准Verilog例化IBUFDS #( .DIFF_TERM(TRUE), // 使能内置100欧姆差分终端 .IOSTANDARD(LVDS) // 指定IO标准 ) u_ibufds_rx ( .O(rx_data_single), // 单端输出到FPGA内部 .I(rx_p), // 差分正端直连FPGA引脚 .IB(rx_n) // 差分负端直连FPGA引脚 );这里最关键的一个参数是DIFF_TERM。很多第一次接触LVDS接收的人以为外接一个100欧姆电阻就行了其实Xilinx的HP Bank和HR Bank在特定条件下可以打开FPGA内部的100欧姆差分终端电阻这样外部就不需要再并一个电阻了能节省面积也能让信号反射控制在芯片内部。但要注意DIFF_TERM(TRUE)不是对所有IO Standard和Bank都支持的。在HR Bank上LVDS标准的DIFF_TERM是支持的在HP Bank上如果VCCO是1.8VLVDS25才是标准操作。具体要以Xilinx的UG471文档里“SelectIO Signaling”那张表为准。如果外部你已经放了终端电阻内部又打开了DIFF_TERM两个电阻并联等效为50欧姆反而会造成阻抗失配信号反射变大。所以这个参数必须和硬件设计严格对齐。2.2 OBUFDS发送端原语拆解OBUFDS是差分输出缓冲器把FPGA内部单端逻辑信号转换成一对差分信号输出到引脚。做LVDS发送时要让内部数据经过OBUFDS原语才能驱动一对差分引脚。标准例化OBUFDS #( .IOSTANDARD(LVDS) ) u_obufds_tx ( .O(tx_p), // 差分正端 .OB(tx_n), // 差分负端 .I(tx_data_single) // 单端输入 );OBUFDS不像IBUFDS那样有DIFF_TERM参数因为输出端不需要终端电阻。但输出端同样受Bank VCCO约束IOSTANDARD(LVDS)只能工作在2.5V VCCO的Bank上。这里我想多说一句OBUFDS的输出强度Xilinx没有像单端IO那样提供驱动强度等级选择输出摆幅和共模电压由芯片内部LVDS驱动器决定。所以PCB设计上串阻和端接电阻的选择就变得非常重要。差分走线特征阻抗通常按100欧姆控制源端可以串一个几十欧姆的电阻来抑制过冲但电阻值需要通过实测信号质量来调整。2.3 双向链路如何用原语搭出来真正的双向传输需要让同一对引脚既能走IBUFDS方向也能走OBUFDS方向。在ZYNQ的PL侧最正统的做法是使用IOBUFDS原语——它把输入缓冲和输出缓冲集成在一起内置方向控制引脚TT为高时处于高阻输入态T为低时处于输出态。IOBUFDS的例化IOBUFDS #( .DIFF_TERM(TRUE), .IOSTANDARD(LVDS) ) u_iobufds ( .O(rx_data_single), // 接收路径输入到FPGA内部 .IO(tx_p), // 差分正端连接到FPGA引脚 .IOB(tx_n), // 差分负端 .I(tx_data_single), // 发送路径来自FPGA内部 .T(tri_enable) // 方向控制1输入0输出 );T信号的语义要注意Xilinx IO缓冲器的T信号是低有效使能输出T0时输出生效T1时输出为高阻外部信号可以驱动该引脚。所以T信号实际上就是方向控制——接收态时拉高发送态时拉低。从学习或者兼容性角度来说直接用IBUFDSOBUFDS做双向链路也是可以的只需要把OBUFDS的输出三态化。Xilinx在IOB里本身有T控制的三态驱动能力但是OBUFDS并不带三态控制所以如果你用IBUFDSOBUFDS的组合需要在外面自己加一个BUFG或者用组合逻辑做三态门控输出高阻时用1bz驱动OBUFDS输入。这在行为仿真里可行但综合进FPGA之后IOB内部的物理结构可能不会完美映射时序上也可能产生额外的路径延迟。我的建议是如果芯片支持IOBUFDS就优先用IOBUFDS如果目标库或者特定Bank不提供IOBUFDS再退而求其次用IBUFDSOBUFDS外部三态门控。2.4 参数DIFF_TERM与终端匹配的关系关于终端匹配我把几种常见情况列成一个表方便大家对照使用场景外部硬件做法FPGA内部配置注意事项点对点接收走线短5cm可不放外部终端DIFF_TERMTRUE内部100R终端生效外部不要再并电阻点对点接收走线长10cm外部靠近FPGA引脚并100RDIFF_TERMFALSE避免内外电阻并联变成50R双向传输同一对线收发两端各放100R或都不放需计算按半双工总线设计内部DIFF_TERM尽量关方向切换瞬间终端不匹配会产生振铃多点总线较少用LVDS两端都有终端不使能内部终端LVDS不适合多点尽量点对点双向链路里终端电阻的位置对信号质量影响很大。如果同一对差分线上有两个节点A节点内部打开了DIFF_TERMB节点也打开了那等效于在线的两端各接了100R阻抗匹配算出来是合理的但如果两端的接收器同时打开DIFF_TERM而线上的驱动器在输出时看到两个并联的100R等效50R负载对驱动器而言负载偏重输出摆幅会被压低。所以设计双向链路时我的经验是发送期间对端的接收器DIFF_TERM打开本端发送器的DIFF_TERM参数不产生影响反过来也一样。在外置电阻的场景下只在总线两端各放一个100R电阻中间节点不放。3. 完整双向链路设计实操3.1 硬件端确认Bank电压、差分对引脚、串阻做任何FPGA引脚相关的逻辑设计之前先回到原理图板级设计确认三件事Bank电压、引脚分配、串阻端接。以我当前项目的ZYNQ-7020为例LVDS双向接口放在了Bank 34上。这个Bank的VCCO被设计成2.5V所有引脚以LVDS为标准。引脚选择上我选了引脚对L14和M14这两个引脚在Xilinx官方引脚定义里是合法的差分对P端在L14N端在M14。硬件上我在FPGA引脚到连接器之间放了一对0402封装的0欧姆串阻目的是方便调试时断开测量。如果信号质量有问题可以替换成22R或者33R来抑制振铃。连接器出来之后外部差分线按100欧姆阻抗控制。如果对端设备内部自带了100R终端FPGA这边的DIFF_TERM就设成FALSE如果不确定对端有没有终端FPGA内部打开DIFF_TERM这样至少保证本端信号质量可控。这个设计里有个细节双向链路的对端如果是一个也能控制方向的设备两个方向控制信号必须通过某种握手协议来防止冲突。硬件上对端和FPGA之间除了差分数据线外还单独引了一根单端方向控制握手信号用来协调“你发我听”还是“你听我发”。这根握手信号在FPGA里也做了防抖滤波。3.2 Verilog设计IOBUFDS做方向收发控制核心逻辑其实不复杂就是“方向切换 数据收发”。我把代码拆成三大块给大家看顶层模块、IOBUFDS控制模块、方向切换状态机。先看顶层的端口声明module lvds_bidir_top ( input wire clk_100m, input wire rst_n, inout wire lvds_p, // 差分正端 inout wire lvds_n, // 差分负端 input wire dir_switch, // 外部方向切换请求 output wire [7:0] rx_data, // 接收数据 input wire [7:0] tx_data // 发送数据 ); wire rx_single; wire tx_single; wire dir_t; // 1输入0输出 wire dir_t_from_state; // 方向信号同步 reg [2:0] dir_sync; always (posedge clk_100m or negedge rst_n) begin if (!rst_n) begin dir_sync 3b000; end else begin dir_sync {dir_sync[1:0], dir_switch}; end end assign dir_t_from_state dir_sync[2]; // 数据发送端寄存器避免组合路径直接驱动OBUF reg [7:0] tx_data_reg; always (posedge clk_100m or negedge rst_n) begin if (!rst_n) tx_data_reg 8b0; else if (!dir_t_from_state) tx_data_reg tx_data; else tx_data_reg tx_data_reg; end // 串行化发送为了简洁这里用8位并行转一路串行 // 实际项目中根据协议决定是否用OSERDES reg tx_shift; always (posedge clk_100m or negedge rst_n) begin if (!rst_n) tx_shift 1b0; else if (!dir_t_from_state) tx_shift tx_data_reg[7]; // 简单示意实际是移位发送 end // 接收端解串同样简化处理 // 实际项目使用ISERDES处理高速LVDS reg [7:0] rx_data_reg; always (posedge clk_100m or negedge rst_n) begin if (!rst_n) rx_data_reg 8b0; else if (dir_t_from_state) rx_data_reg {rx_data_reg[6:0], rx_single}; end assign rx_data rx_data_reg; // 方向状态机切换前必须保证总线空闲 reg [1:0] state; localparam IDLE 2d0; localparam TX 2d1; localparam RX 2d2; always (posedge clk_100m or negedge rst_n) begin if (!rst_n) begin state RX; end else begin case (state) IDLE: begin if (dir_t_from_state) state RX; else state TX; end TX: begin if (dir_t_from_state) state IDLE; end RX: begin if (!dir_t_from_state) state IDLE; end default: state RX; endcase end end assign dir_t (state TX) ? 1b0 : 1b1; // 例化IOBUFDS IOBUFDS #( .DIFF_TERM(TRUE), .IOSTANDARD(LVDS) ) u_iobufds ( .O(rx_single), .IO(lvds_p), .IOB(lvds_n), .I(tx_shift), .T(dir_t) ); endmodule这段代码的核心逻辑在于T信号为高时IOBUFDS输出为高阻外部的差分信号可以通过输入缓冲进入FPGA内部T信号为低时IOBUFDS由内部逻辑驱动差分引脚对外输出。通过状态机来管理IDLE状态避免从RX切换到TX的时候总线还处于不确定态。注意几点方向切换之间要留出空档时间。如果当前处于RX态外部设备也在发数据你突然切到TX两个驱动同时驱动同一对差分线轻则数据混乱重则让芯片的IO驱动管脚过流发热。所以我加了一个IDLE态方向切换时先进IDLE让总线彻底释放一个周期以上再切方向。数据发送路径上我加了一级寄存器避免组合逻辑直接驱动OBUFDS。这样做能改善timing防止数据在发送过程中因为组合逻辑延时产生毛刺。上面的串行化只是为了结构演示实际项目中如果LVDS数据率在几百Mbps以上需要用ISERDES/OSERDES做并行转串行或串行转并行。ZYNQ的HP Bank上自带ISERDESE2和OSERDESE2原语能够支持SDR和DDR模式用起来很方便。3.3 约束文件引脚分配与差分约束综合之后就要写XDC约束。LVDS引脚约束和普通单端不太一样除了IO Location之外还要成对约束并且在电气标准上写清楚。set_property PACKAGE_PIN L14 [get_ports {lvds_p}] set_property PACKAGE_PIN M14 [get_ports {lvds_n}] set_property IOSTANDARD LVDS [get_ports {lvds_p}] set_property IOSTANDARD LVDS [get_ports {lvds_n}] set_property DIFF_TERM TRUE [get_ports {lvds_p}]这里有个小知识点DIFF_TERM约束只需要在P端设一次工具会自动应用到整个差分对。PACKAGE_PIN那一行如果你在XDC里把lvds_n的引脚绑定到与lvds_p不属于同一差分对的位置Vivado会在DRC阶段报错错误信息类似“LVDS pair mismatch”。如果用了IOBUFDS工具要求两个端口必须定义为inout类型否则综合报错。另外一个容易忘的约束是电流强度LVDS标准下通常不需要设但如果你用的不是标准LVDS而是LVDS_33之类的变种就必须手动加DRIVE属性。多数场景下LVDS 2.5V标准就够用。3.4 时序约束与跨时钟域处理LVDS接收端的时钟处理是另一个大坑。如果LVDS数据自带随路时钟那接收端的ISERDES需要把随路时钟作为时钟源然后用MMCM/PLL把串行时钟和并行时钟的关系处理好。如果LVDS数据没有随路时钟只有数据线那就必须从数据边沿恢复时钟。ZYNQ上可以用IDELAYE2ISERDESE2MMCM做源同步接收这个方案链路比较长我建议新手先从随路时钟开始做没有随路时钟的LVDS链路调试难度会高不少。在XDC里要明确告诉工具接收时钟的频率和相位关系否则时序收敛无从谈起。举个简单例子create_clock -period 10.000 -name clk_100m [get_ports clk_100m] create_clock -period 3.333 -name lvds_rx_clk [get_ports rx_clk_p] set_input_delay -clock lvds_rx_clk -max 1.0 [get_ports {rx_data_p}] set_input_delay -clock lvds_rx_clk -min 0.5 [get_ports {rx_data_p}] set_output_delay -clock clk_100m -max 2.0 [get_ports {tx_data_p}] set_output_delay -clock clk_100m -min 1.0 [get_ports {tx_data_p}]这里的set_input_delay和set_output_delay数值来源于芯片手册上外部器件的建立保持时间要求不能拍脑袋填。如果外部器件手册不明确先按保守值来后面再用实测数据修正。跨时钟域方面接收解串出来的并行数据如果LVDS随路时钟和系统时钟不是同源必须先做CDC处理。最简单的做法是用两级同步器或者用异步FIFO。我习惯在解串完成后用一个16深度的异步FIFO把数据从LVDS时钟域搬到系统时钟域这样最稳妥。3.5 上板调试验证ILA抓波形的关键点写完代码、跑完综合布局布线之后上板调试是验证链路的关键。我用的是Vivado自带的ILAIntegrated Logic Analyzer来抓内部信号。这里有几个实际调试经验先看方向控制信号T是否正常翻转。在确定外部设备连接正常的前提下用ILA抓dir_t和rx_single信号。如果T为高rx_single应该能采到外部输入的数据变化如果T为低rx_single理论上保持不变或为无效值。这一步能快速判断IOBUFDS的工作状态。再看接收解串数据的边界对齐。用ILA抓rx_data_reg应该能看到稳定的8位数据。如果数据出现比特错乱大概率是采样的相位有问题。ZYNQ的ISERDES IDELAYE2链路里可以用IDELAYE2的CNTVALUEOUT来微调输入延迟。调节延迟直到数据稳定为止每次调节步长大约78ps通常可以从0开始尝试逐步增加步数找到数据稳定的“眼图中央”。调试过程中遇到一个很奇怪的现象LVDS接收数据高两位始终为固定值低六位正常。排查了很久最后发现是引脚约束时N端和P端的信号定义反了。外部设备输出的P到了FPGA的NN到了FPGA的PLVDS接收器仍然能识别出差值但逻辑上数据位序全乱了。所以管脚分配的检查真的不能省。4. 常见问题与排查技巧实录4.1 引脚分配错误与Bank非法组合这是最最常见的错误。Vivado在综合后或者实现后会给DRC报告但很多人不看直接生成比特流上板之后发现完全没反应。检查顺序是先看P和N是否在同一差分对内。再看Bank的VCCO是否满足LVDS需要的2.5V。最后看Bank里有没有混用其他IO标准。如果同一个Bank里既有LVDS又有LVCMOS33引脚VCCO就没办法同时满足2.5V和3.3V这时候硬件设计本身就有缺陷。有时候Vivado不报错但上板信号质量极差检查到最后是Bank电压不匹配。这时候万用表量一下Bank的VCCO引脚就很直观。如果是2.5V基本可以排除这个问题是3.3V或1.8V那就要查原理图为什么把Bank电压拉错了。4.2 接收端数据全为1或全为0这个现象通常意味着差分输入缓冲没有正常工作。先从最简单的查起用万用表量P和N脚的直流电压。LVDS在静态时共模电压在1.2V左右如果P和N之间电压差为0或者异常说明外部没有正常驱动。还有一种可能是DIFF_TERM没打开而外部也没有接终端电阻差分线上的反射导致接收器无法判断高低电平。此时打开DIFF_TERM或者外部补上100R电阻数据往往就恢复稳定了。如果是双向链路里切入接收态后数据全为0有可能是对端设备还在输出驱动你的方向切换时序没有等待总线空闲导致总线被两边同时驱动电压差被压低到接收器的阈值以下。4.3 方向切换瞬间毛刺导致误码方向切换时总线上会出现一段“空闲窗口”。如果这个窗口没有正确处理对端设备可能把空闲窗口当成传输间隙也可能在驱动释放和接管之间产生毛刺导致接收端误码。解决办法是在发送端的数据包协议里留出前导码和空闲间隔比如发送帧头之前先让总线空闲几个时钟周期。同时方向控制信号的切换要经过同步器避免异步跨时钟域导致亚稳态。我在前面那个状态机里就专门设计了一个IDLE态实际使用中误码率明显降低。4.4 内部终端和外置终端重复导致信号劣化这是一个非常典型的坑。某次做板级联调对端设备和FPGA之间线缆比较长硬件工程师在对端做了100R终端FPGA这边本来不该再打开DIFF_TERM但工程里默认设成了TRUE。结果信号幅度被压掉一半接收端误码严重。排查方法是示波器差分探头直接测FPGA引脚处的差分波形如果看到幅度只有正常的一半左右基本就是终端电阻重复了。解决方式是对端保留终端、FPGA内部DIFF_TERM设为FALSE或者FPGA打开DIFF_TERM、把外部对端的100R去掉。总之整条链路只能有一组100R终端要么在源端要么在终端不能两头都放。4.5 IOBUFDS的T信号时序要求最后再补充一个IOBUFDS特有的问题。T信号从输出态切到输入态时IOBUFDS的输出驱动器不是瞬间就变成高阻的而是有几十到几百皮秒的释放时间。如果T信号变化的同时外部对端已经开始驱动总线那这段时间内总线可能被两个驱动器同时拉扯产生短暂的电流尖峰。因此在设计方向切换时序时T信号的切换必须比真正的数据驱动早一拍、比数据接收晚一拍。从我实践来看方向切换期间至少要预留2-3个系统时钟周期的死区时间这样最保险。5. 一些经验总结把这条LVDS双向链路从原理到实现完整走下来其实核心点并不多但每一个都决定成败。Bank电压选对、差分对引脚配对正确、DIFF_TERM和外部终端匹配一致、方向切换预留死区时间、数据发送前加寄存、接收端做好跨时钟域处理这几步做到位链路基本就稳了。我个人在实际项目中的体会是LVDS双向传输真正考验的不是逻辑复杂度而是对IO物理层细节的理解。很多问题看起来像逻辑bug排查到最后都是硬件和约束层面的问题。比如终端电阻重复、Bank电压不对、引脚P/N反了、方向切换太快这些在仿真里根本发现不了只能靠扎实的硬件常识和上板调试经验来兜底。最后再分享一个小技巧如果条件允许LVDS双向链路的上板调试最好用示波器差分探头先看物理层波形确认电平幅度、共模电压、上升沿质量都正常之后再去用ILA调逻辑。省掉物理层的验证直接抓逻辑波形遇到问题容易绕圈子反而更慢。