1. 从XAPP585开始:为什么7:1 LVDS是绕不开的硬骨头
前阵子做一块工业相机的采集板卡,接口正好是7:1 LVDS,分辨率1080p60。第一反应是“LVDS嘛,差分对,接收就行”,结果真正调起来才发现,事情完全不是“接两根线、打几个IO”那么简单。数据率一上来,所有平时不在意的细节全部变成坑:时钟怎么倍频、数据怎么解串、字边界怎么对齐、通道和通道之间怎么对齐、PCB上那几对差分线的等长误差到底该控多少——每个问题都能卡你两三天。
后来翻到Xilinx的老牌应用笔记XAPP585,标题就是“7:1 LVDS SerDes”,这才算把整个链路学明白。这篇笔记写得早,用的是Spartan-6时代的ISERDES/OSERDES原语,但后面7系列、UltrasScale+的底层思路几乎一点没变。直到今天,你在ISE和Vivado里搜“LVDS 7:1”或者视频接口相关IP,还是能追踪到XAPP585这套方法。可以说,搞懂XAPP585,就搞懂了FPGA上所有源同步串行接口的收法。
这篇博客我不打算把XAPP585原文翻译一遍,而是结合自己实际调板子的经历,把7:1 LVDS源同步SerDes里最核心的设计方法和容易踩的坑梳理出来。适合谁看?两类人:一是刚接触LVDS接口、要在FPGA上做视频采集或显示驱动的硬件/FPGA工程师,二是被某些带LVDS接口的工业主板、相机、屏幕折腾过,想搞清楚“信号进来之后到底发生了什么”的嵌入式开发者。读完你至少能明白:7:1到底是哪来的7、时钟倍频怎么设计、解串为什么要个“训练过程”、以及信号不稳定时第一步该查什么。
关于XAPP585里给的结构,我提炼成一个核心思想:用一对随路时钟,把低速并行数据在发送端并转串,在接收端串转并,整个链路靠同一个时钟源维持同步,不需要专门的CDR(时钟数据恢复)电路。这个思想和PCIE、SATA那种嵌入式时钟SerDes完全不同,它属于“源同步”机制,代价是PCB布线必须保证数据和时钟严格等长,好在代价对应的是FPGA内部实现非常简单,普通IO资源就能跑。
2. SerDes与源同步:先把“7:1”这张皮揭掉
2.1 7:1那个数字到底是哪来的
很多做软件出身的同学第一次听到“7:1 LVDS”会很懵,以为是一种“7倍速LVDS协议”,其实根本不是。7:1指的是每个像素时钟周期里,一条LVDS差分对上串行传输7个bit。为什么会是7?因为在传统的RGB视频信号里,一个像素包含R、G、B三个通道,每个通道6bit或8bit,加上HSYNC、VSYNC、DE这些控制信号。以24bit RGB为例,RGB各8bit一共24个数据位,加上3个控制位就是27bit。如果用3对LVDS来传,27除以3等于9,所以会有9:1的LVDS;如果用4对LVDS数据线传24bit数据加3个控制位,刚好每对传7bit,这就是7:1的来历。你可以把7:1理解成“每通道位宽为7”的并行转串行比率,而不是某种编码协议。
实际用FPGA做1080p30这种分辨率时,像素时钟约74.25MHz,7:1串行之后每条差分线的数据率就是74.25×7=519.75Mbps,如果跑1080p60,像素时钟148.5MHz,数据率就是1.0395Gbps。这个速率对LVDS来说已经是比较极限的区间了,对PCB走线和连接器质量也比较敏感。
2.2 源同步和CDR SerDes的本质区别
理解源同步,可以对比一下嵌入式时钟方案。PCIE的发送端不会把时钟单独送出去,数据编码里就隐含了时钟信息,接收端要用CDR锁相环把时钟从数据流里“挤”出来。好处是少一对时钟线、没有时钟和数据之间的偏斜问题,代价是实现非常复杂,必须用专用的高速收发器,没有普通IO什么事。
LVDS的源同步则完全相反:发送端单独发一对时钟LVDS,数据和时钟是平行着一起走的。接收端拿这对时钟去采样数据即可,不需要恢复时钟。这个做法的前提是:时钟和数据必须同源同相,在PCB上必须保证等长,让时钟边沿到达接收端时,数据仍然稳定。通俗点说,CDR像是一个人听鼓点找节奏,源同步则是鼓手把节拍器信号也一起递给你,你照着节拍直接弹就行,但前提是谱子和节拍器不能对不上。
这个区别决定了7:1 LVDS在FPGA上的实现路径:发送端负责把7bit并行数据串行发送出去,接收端靠外部时钟把串行数据流按时间顺序切成7bit一组,再通过字对齐把正确的位边界锁定。整个过程不需要PLL从数据里恢复时钟,只需要一个PLL做倍频或者相移,难度一下子低了一截。
2.3 XAPP585里的双时钟结构
XAPP585的接收端结构有一个关键细节:它不直接用外部LVDS时钟去采数据,而是先把外部时钟经过MMCM/PLL处理,生成两个内部时钟:一个是慢时钟(像素时钟频率,比如7倍分频,也即74.25MHz),用来给并行逻辑用;另一个是快时钟(7倍频,比如519.75MHz),用来驱动移位寄存器,把DDR取回来的数据进一步并转串。
为什么不能直接用外部时钟采样?因为LVDS接收进来的时钟只是普通IO时钟,无法直接作为FPGA内部全局时钟网络使用,直接使用会带来很大的扇出和布线延迟不确定性。正确做法是把外部时钟送入专用的时钟输入引脚(MRCC/SRCC),Mapped到全局时钟缓冲区域,再经由MMCM的PLL重定时,产生稳定的内部时钟树。XAPP585里还利用了MMCM的相位调整能力,可以微调采样时钟相对数据的相位,找到眼睛窗口最中间的位置。
我自己调试时发现,PLL倍频的选择要格外谨慎。比如像素时钟不是绝对的74.25MHz,可能因为摄像头驱动芯片晶振精度原因有几十ppm偏差,这个偏差问题不大,因为发送端和接收端用的是同一对随路时钟,像素时钟进来多快,MMCM就能跟多快,PLL的输入频率在Fpga允许范围内就能锁住。真正麻烦的是输入时钟的抖动,如果送进来的LVDS时钟本身抖动太大,MMCM输出的高速时钟会额外引入随机误码,这种问题不加眼图仪几乎看不出来。
2.4 位滑动(BitSlip)为什么必备
解串之后,FPGA拿到的是8bit或7bit的并行数据。但串行数据流里,从哪一位开始算第一个bit,在没有训练的情况下是随机的。比如物理上第1个bit可能是R0,但解串器恰好是从R3开始截断的,那么一整行图像都会错位,表现就是满屏雪花、颜色错乱、或者画面完全不可解析。
BitSlip就是解决这个问题的机制。它本质上是一种循环移位,可以在并行域把解串后的数据左右移动1位,重新选择字边界。这在XAPP585里对应的是“训练序列”过程:发送端在消隐区间送出特定码型(比如一串0101或特定字节),接收端不断尝试BitSlip,每次滑动后检查收到的码型是否匹配预期值,如果不匹配就继续滑动,直到完全对齐。
这个训练逻辑如果自己从零写,坑非常多。最常见的坑是:接收端初始化顺序错误,在链路还没有稳定的情况下就开始滑动,导致怎么滑都对不上;其次是训练码型选择不当,用了周期性很短的重复码型,比如0xAA和0x55这种只有两位重复的,会产生多个“假匹配”位置,有些时候滑到错误位置也看起来像对齐了。实践下来,最好选那种具有足够唯一性的码型,例如无符号的固定图像数据或者伪随机序列,对齐成功后再加上持续监测机制,运行中如果发现同步丢失,要能自动重新进入训练流程。
3. 时钟倍频与内部IO结构:XAPP585是怎么把LVDS跑起来的
3.1 时钟倍频链路设计
现在仔细看一下时钟倍频设计。7:1模式下,外部进来的像素时钟频率Fpix,内部需要一个7倍频时钟Fser=7×Fpix,用于并转串和串转并。用户会看到Xilinx的DCM/PLL使用指南上通常建议一种做法:在MMCM中输入Fpix,输出Fser(BUFIO驱动IO逻辑),再把Fser分频得到并行时钟Fpar(建议用BUFR,而不是全局BUFG)。XAPP585里的方案不一样,它建议同时使用两个时钟:一个快时钟用于串行移位,一个慢时钟用于并行数据,这里有个关键取舍问题。
为什么不用一个7倍频时钟打天下?因为在IOB中的ISERDESE2/OSERDESE2原语,串行端(快时钟域)和并行端(慢时钟域)天然就是两个时钟域。并行端的数据总线必须用慢时钟来同步,如果强行用7倍频时钟去做并行数据的流水,第一是Fmax不满足,第二是综合工具会因为慢速逻辑挂在高速时钟网络上产生严重的时序违例。正确的思路永远是:尽量让2个时钟域分工明确,快时钟只管移动bit,慢时钟管并行逻辑。
表:7:1模式下推荐时钟分配
| 时钟 | 频率 | 作用 | 驱动资源 |
|---|---|---|---|
| 外部像素时钟 | Fpix | 源同步参考时钟 | IBUFDS→BUFG→MMCM |
| MMCM输出快时钟 | 7×Fpix | 驱动ISERDESE2的CLK引脚 | BUFIO(高扇出IO时钟) |
| MMCM输出慢时钟 | Fpix | 并行数据流、状态机 | BUFR或BUFG |
| 相位调节 | 可调 | 保证采样点居中 | MMCM的CLKOUT相位 |
这里我特别想提醒:不要省略BUFIO/BUFR直接全走BUFG。早期的设计图省事,快时钟全部走全局时钟网络BUFG,结果跑到800Mbps以上就会出现数据采样错误。原因很简单,BUFG的传输延迟大,而且整个FPGA所有逻辑都挂在上面,电源噪声和串扰会把快时钟边沿搞得乱七八糟。IO时钟专用网络BUFIO的延迟是专门针对IOB优化的,只能驱动IO相关逻辑,但延迟小、确定性好,很适合这个任务。BUFR可以把快时钟分频出慢时钟,并且分频后的时钟和原始时钟保持在同一个区域,时延可控。
3.2 ISERDESE2/OSERDESE2原语怎么接
如果只记XAPP585里最核心的代码,那一定是ISERDESE2和OSERDESE2的例化。7系列FPGA上,ISERDESE2支持DDR模式下的8:1、10:1、14:1等比率,可实现1:7解串。不过注意:不是说原语支持7:1就可以直接接7根线出来。ISERDESE2在DDR模式下通常是1:8输出,要得到7:1,需要把并行输出宽度设为8bit,然后只取其中7bit有效数据,或者通过级联(master/slave)实现。更常见的做法是直接把ISERDESE2配成8:1,让训练电路使用第8位作为边界标记,这样逻辑更简单。
一个很关键的细节是ISERDESE2的DATA_RATE参数必须设成“DDR”,意味着时钟的上升沿和下降沿都采样。这很好理解:7:1串行数据在外部的1个像素时钟周期里送7个bit,外部时钟只有一对边沿,不采用双沿采样就无法在1个时钟周期内取回7bit。DDR模式下,ISERDESE2内部通过两个半速率支路(I支路和Q支路)先并行取出两组数据,再合路成完整的并行总线。
OSERDESE2发送端则正好相反,并行总线送入慢时钟域,OSERDESE2在快时钟域里按bit依次移出。实操中发送端的时钟分配和接收端完全对称:外部像素时钟进MMCM,快时钟通过BUFIO驱动OSERDESE2的CLK,慢时钟作为并行数据侧时钟。
我曾见过有人直接用通配符连接ISERDESE2的Q1~Q8,把顺序搞错导致图像整体错位。这个顺序不是随便接的:DDR输出时,Q1对应的是第一个被采样的bit,Q8是最后一个,而且Q7、Q8在7:1场景下通常是无效位或者是边界位。因此并行数据总线的bit0应来自Q1还是Q8,要看仿真,不能想当然。最靠谱的验证方法是写个简单的仿真testbench,输入一串已知pattern,观察解串输出bit顺序对不对。
3.3 训练模式为的是什么
写软件的人总爱把“初始化序列”想得很简单,发几个配置字就行。但LVDS链路的字对齐不是软件能直接操作的,必须硬件逻辑配合。XAPP585里的做法是在训练阶段利用HSYNC/VSYNC/DE这些控制信号作为对齐参照。由于这些信号在消隐区间具有固定的电平特征,训练逻辑可以尝试20种不同的BitSlip偏移,每一种偏移下都连续检查若干行内的控制信号是否符合预期,符合则锁定,完成对齐。
注意:训练阶段的匹配判断不能只依赖单个像素时钟周期的瞬态值,因为噪声很可能造成偶发错误。正确的做法是连续统计N个像素周期,比如256个或1024个,统计正确率超过99%才判定对齐成功。我在实际项目中曾经为了省资源把窗口设成32个周期,结果在干扰大的工业环境里频繁失锁,后来改成1024个周期,稳定性好非常多。
发送端在训练期间应持续发送训练码型。如果发送端是另外一个厂家的ASIC或者相机,它可能在初始化时还没有输出有效视频数据,甚至链路还没建立。此时需要在FPGA侧做“超时重训练”机制,比如训练不成功就自动重置发送端配置、重新初始化相机。联调时这种异步握手最容易出问题,我习惯在代码里预留足够多的寄存器给上位机,方便查看当前训练状态、已尝试的BitSlip次数、以及当前错误率计数器,排查起来一目了然。
4. 实操过程:从引脚约束到上板调试要点
4.1 引脚分配与电平标准确认
LVDS在FPGA里通常由工具自动识别差分对,物理上需要一对相邻引脚(P和N)。先确认bank电压:7系列HP bank支持1.8V的LVDS标准,HR bank支持2.5V/3.3V的LVDS_25和LVPECL等标准。如果你的电路板上是3.3V供电的LVDS电平,最好显式使用“LVDS_25”还是“LVDS”要严格查手册,因为这两个标准的共模电压不同,接错会导致接收端无法正确判定高低电平,出现随机误码。工业相机常见的是LVDS_25(2.5V),笔记本内屏常见的是LVDS(1.8V)或者更复杂的eDP,必要时加电平转换芯片。
另外,现代FPGA支持内部差分端接。Xilinx的IBUFDS原语上有个DIFF_TERM属性,设置为TRUE时内部自动接上100Ω差分端接电阻。对于高速LVDS信号,100Ω差分端接必须存在,否则信号在接收端会发生反射,眼图恶化,表现为数据出错率随线长、温度变化而波动。如果使用外部端接,记得放在接收端靠近FPGA引脚的位置,一般建议小于5mm。
4.2 xdc约束中容易被忽视的细节
一个常见的困惑是:LVDS引脚约束到底要不要指定PACKAGE_PIN还是直接让工具自动分配?我的建议是:对于PCB已经定死的设计,必须手动指定引脚;对于自由度大的原型板,可以自动分配但要用IO Planning视图核对差分对关系。xdc里简单的写法:
set_property PACKAGE_PIN AC11 [get_ports lvds_clk_p] set_property IOSTANDARD LVDS_25 [get_ports lvds_clk_p] set_property PACKAGE_PIN AD11 [get_ports lvds_clk_n] set_property IOSTANDARD LVDS_25 [get_ports lvds_clk_n] set_property PACKAGE_PIN AC12 [get_ports lvds_d0_p] set_property IOSTANDARD LVDS_25 [get_ports lvds_d0_p] set_property PACKAGE_PIN AD12 [get_ports lvds_d0_n] set_property IOSTANDARD LVDS_25 [get_ports lvds_d0_n]不要忘记在顶层使用差分缓冲器,这样工具才会认为这两个管脚是差分对。Verilog里可以直接用原语IBUFDS/OBUFDS,也可以依赖引脚约束自动推断。头一次做的时候很容易犯一个错:只约束了P端、忘了N端,结果工具报错“没有匹配的差分引脚”,排查半天。
如果你需要做时序约束,尤其是跨时钟域和相位对齐,可以考虑用set_clock_groups把PLL的两个输出时钟设成异步,防止工具对两个时钟域做不合理的时序收敛。同时,对输入延迟set_input_delay需要根据PCB走线等长和器件数据手册参数来填,虽然很少有人认真填,但不填在高速接口上综合结果往往更差。
4.3 RTL实现要点
这里写一个简化版的接收端状态机核心流程:
- 初始状态WAIT_PLL_LOCK:等待MMCM的LOCKED信号拉高,同时等待外部输入时钟稳定。
- 然后进入TRAIN状态:启动训练计数器,每个时钟周期做一次BitSlip尝试,然后进入CHECK状态。
- CHECK状态:连续采样若干数据,比对预期码型。如果全部符合则进入RUN状态;如果出现不匹配,则回退到TRAIN,bitslip一次,再次检查。
- RUN状态:正常传递并行数据,同时用滑动窗口监控码型错误率。如果错误率超过阈值,自动回到TRAIN重新对齐。
实际的BitSlip信号时延和时序要求值得注意,ISERDESE2原语的BITSLIP引脚是异步脉冲触发的,但这个信号从慢时钟域到快时钟域属于跨时钟域,需要做两级同步再打给BITSLIP。如果不做同步,偶尔会出现BitSlip脉冲采样到亚稳态,导致一次性滑动两三位,训练结果飘忽不定。
发送端则要比接收端简单很多。如果数据源来自DDR3或者异步FIFO,记得在发送端加一个FIFO做跨时钟域缓冲,防止上游数据突发时打乱OSERDESE2的发送节奏。更多的细节是:如果7:1的7bit里包含DE、HSYNC、VSYNC等控制信号,在消隐期间也要保持输出有效,否则接收端训练状态机可能因为“缺失控制信号”而误判。
4.4 多通道扩展与对齐
如果你处理的是多对LVDS数据线,比如4对数据+1对时钟,每对数据流独立完成BitSlip的情况下,还需要做通道间对齐。XAPP585的标准做法是:训练阶段发送端在每一组数据通道中插入相同的对齐码型,接收端完成每通道字对齐后,再评估通道间偏斜。偏斜来源主要是PCB上几对差分线长度差异、连接器接触、以及驱动器内部的微弱差异,典型数值在几百ps到几个ns之间。如果偏斜在1个像素时钟以内,可以通过FIFO调整;如果更大,需要检查PCB等长设计是否有问题。
我调过一块偏斜比较恶劣的板子,4通道中某条线在连接器处走了额外的分支,导致比其他通道晚了近5ns,几乎是半个像素周期。这种问题靠逻辑很难完全补救,只能加FIFO做弹性缓冲,并把允许的偏斜范围放大。但FIFO深度增大会引入帧延迟,对于视频链路还能接受,如果做工业控制类的同步采集,就得认真控制线长差。一般7:1 LVDS的线长差建议控制在±5mm以内,对应约±25ps,基本无碍。
5. 实测中的常见问题与排查方法
5.1 丢同步和误码的排查顺序
遇到LVDS画面出现雪花、闪屏或者偶发花块,按下面这个顺序排查,每步都确认之后再往下:
- 确认物理层连接:差分P/N有没有接反?示波器探一下两脚之间的摆幅是否正常(一般LVDS为350mV差动摆幅),共模电压是否在合理范围。
- 检查端接电阻:内部端接是否开启,或者外部100Ω是否焊好。关掉端接观测信号,波形可能会有明显振铃。
- 检查时钟链路:MMCM是否LOCKED?外部像素时钟是否接入了正确的MRCC引脚?快时钟频率是否正好是像素时钟的7倍,可以用频率计在测试引脚上量。
- 确认BitSlip训练逻辑是否工作:看训练状态机的状态寄存器和错误计数器,如果一直在重训练,说明字对齐没有成功。
- 如果对齐成功但运行中偶发失锁:大概率是信号质量问题或者通道串扰,用示波器看眼图、调MMCM相位,而不是继续改逻辑。
我用文字总结成一个速查表:
| 现象 | 可能原因 | 快速定位手段 |
|---|---|---|
| 完全无数据 | 没有时钟/端接错/引脚错 | 示波器查LVDS时钟有无、PLL是否锁定 |
| 画面整体雪花 | 字对齐失败/训练没完成 | 查看训练状态机、Bitslip计数 |
| 画面错位/颜色乱 | 数据位序错/字节映射错 | 发送固定pattern对比仿真 |
| 偶发花块 | 信号质量差/通道偏斜大 | 示波器测眼图、检查等长、调相位 |
| 长时间后失锁 | 温度/噪声导致相位漂移 | 增加持续监测、周期性重训练 |
5.2 调相位到底怎么调
源同步接口的采样窗口来自MMCM的输出相位调整。Vivado里可以通过动态相位偏移(Dynamic Phase Shift)在运行中微调,也可以在xdc里固定初始相位。核心思路很简单:采样时钟的边沿必须落在数据稳定区间,而不是数据跳变沿。怎么找最佳点?当链路训练成功后,连续改变相位值,同时统计错误率。某个相位范围内错误率始终为0,取这个区间的中间值作为最佳相位。
这个动态调整逻辑我在项目里是通过AXI接口读写一个寄存器组、从软件触发多次扫描实现的。第一次扫的时候很痛苦,因为每调一次相位需要复位BitSlip、重新训练,整体耗时较长。后来优化成:只在链路初始化时做扫描,后续正常运行就锁定这个相位值。环境温度变化大的工业现场,可以考虑设计定期重新扫描,牺牲一小段时间的切换,换长时间工作的可靠。
5.3 内嵌训练逻辑要克制
见过不少同事喜欢在训练模块里堆功能,加了一堆调试探针、软复位、状态上报,结果本身就成了不稳定因素。我的体会是:训练逻辑越简单越可靠。核心逻辑只保留状态机、计数器和错误统计,其余的寄存器读写、debug抓波全部用独立模块挂在总线后面,互不干扰。
另外,有一个实操技巧:上板调试时,在发送端预先写死一个递增的数据流,比如每7bit组里依次放0x01、0x02、0x03…,接收端按字节对数据,一眼就能看出是不是出现了位错。如果递增序列都能正确恢复,说明链路正常;如果数字是乱序的,优先查BitSlip逻辑,而不是查上层图像算法。这个技巧在排除“链路问题”和“业务逻辑问题”非常好用。
5.4 强烈建议:有条件就用集成IP
如果你用的是相对较新的Vivado版本,可以搜一下“LVDS Video”相关的IP或者参考设计,它们内部已经封装好了完整的7:1解串、训练、对齐逻辑。和XAPP585相比,IP方式是图形化配置,支持更多的通道数和更丰富的调试点;但缺点也很明显——IP屏蔽了底层细节,出了问题黑盒难排查。我的建议是第一次做LVDS接口,一定要手动用原语搭一遍,把XAPP585吃透,之后再用IP提高效率。跳过原理直接上IP,调试时会非常被动。
6. 与其他方案的横向对比:反正都得懂底层
FPGA软解串与专用LVDS解串器芯片
市面上有专用的LVDS解串器/解串芯片,比如常见的DS90CR288A(28bit LVDS转并行)、SN65LVDS93(并行RGB转LVDS)等等。它们的好处是简易:输入输出全都是现成的并行总线,不需要自己写SerDes逻辑。尤其适合应用逻辑非常简单的嵌入式设备,一颗芯片解决接口转换问题。
代价则是功能固定,通道数、位宽、甚至训练序列都固化在芯片内部,几乎不可配置。如果你的屏幕分辨率或信号格式稍有变化,就得换芯片。而FPGA方案最大的灵活性在于无限可重构:同一块板卡可以通过加载不同bitstream支持单通道7:1、多通道7:1、甚至其它比例。工业相机、医疗设备这类产品经常要面对多种传感器格式,FPGA方案几乎成了唯一选择。
与嵌入式时钟SerDes的边界
还有一类容易混淆的方案是使用FPGA内部的高速收发器(GTP/GTX/GTY),这些收发器支持PCIE、SATA、10G以太网等串行协议,是嵌入式时钟CDR方式。很多人想当然认为收发器性能更好,想用它来跑LVDS。这里要明确一个底线:LVDS是电气标准,高速收发器是物理层收发通道,两者不是一回事。如果信号源本身输出的就是LVDS电平的7:1串行数据,用普通IO加XAPP585的方式才是正确的。硬要接进GTX,还需要额外的电平转换和协议适配,事倍功半。
最后再说一个领域外但很常见的坑:笔记本或主板说明书里提到的“LVDS内屏接口”,很多是直接接屏的排线接口,电平并不是标准LVDS,有时是eDP转换后的信号,有的直接用HSCL串行时钟配LVDS数据线。遇到这类接口先看屏端控制器的datasheet,分清物理层信号类型再动手,否则摩拳擦掌实现了半天,接口根本对不上。