
简介一套完整可运行的AD9653四通道高速ADC采集Verilog工程基于Xilinx Vivado平台开发实测支持125MHz采样率。工程面向需要多通道高速同步采集的FPGA开发者着重解决LVDS源同步接收的时序对齐难题集成SPI寄存器配置模块与自动相位扫描延时自校准逻辑可动态调整IDELAY或ISERDES捕获窗口确保四通道LVDS数据稳定对齐。压缩包共7个文件、仅798KB包括注释详尽的Verilog源代码、PDF硬件连接与时序约束说明以及三张JPEG实测波形图分别展示SPI写入过程、LVDS眼图收敛效果与四通道同步输出结果另外附带HTML结构预览与工程配置项方便快速理解代码组织。目前已有68人学习下载。模块化设计配合清晰中文注释适合雷达信号采集、超声多通道采样等需要高精度时间对齐的场景也便于在此基础上二次开发或移植。 第一次把AD9653的四通道采集工程跑起来的时候我以为最难的部分是寄存器配置结果SPI读ID一次就过了LVDS数据却怎么都对不上。用ILA一层层看到最后问题出在DCO沿和四根lane的相位差上——同一个时刻有的lane已经在采样下一个bit了。这趟弯路走完之后我把方案固化成一套相对通用的模板125MHz采样、SPI寄存器配置、LVDS延时自校准全部在Vivado平台上用Verilog实现。本文把设计思路和关键代码片段完整写出来给打算用AD9653或者类似LVDS接口ADC做采集的同学一个参考。1. 硬件链路AD9653的LVDS输出怎么接进FPGA才靠谱1.1 AD9653输出接口的几个关键参数AD9653是四通道、16bit、最高125MSPS的ADC数字输出支持CMOS和LVDS两种模式。做高速采集时基本都会选LVDS DDR输出因为CMOS在100MHz以上不管是引脚数还是时序余量都很难受。LVDS模式下每个通道有两对数据线D0为低字节、D1为高字节再配一条DCO时钟线。数据在DCO的双沿输出FPGA这边需要做DDR解串才能把16bit拼回来。这里有个容易被忽略的点DCO与采样的关系在不同配置下并不完全一样DCO频率和采样率之间的倍率取决于输出为模式。以我们常用的4:1双lane方案为例每条lane在每个DCO周期内传输多个bitFPGA侧通过ISERDESE2做4:1解串解出来的4bit与另一条lane的4bit拼成8bit两个时钟周期再拼一次才是完整的16bit。这块建议直接以芯片手册里的数字输出时序图为准别想当然地认为DCO边沿一定落在数据中央。电平方面AD9653的LVDS输出共模电平在1.2V附近Vivado里对应的IO标准可以直接选LVDS_25同时在Bank电压满足条件的情况下打开DIFF_TERM片内终端。注意Xilinx 7系列FPGA的LVDS接收尽量放在HP BankHR Bank虽然也支持LVDS但在高速场景下性能余量小而且有些封装对HR Bank的LVDS引脚有限制布局布线时容易踩坑。1.2 FPGA侧接收架构的选型思路AD9653这类LVDS ADC接进FPGA后标准的处理路径是DCO引脚进入BUFIO作为ISERDESE2的采样时钟每根数据lane先过IDELAYE2做相位微调再进入ISERDESE2解串采样时钟再用BUFR做分频得到内部逻辑可用的低速时钟比如125MHz或者62.5MHz。为什么不用MMCM/PLL直接生成相位补偿因为MMCM是全局性调整只能整体挪时钟相位但PCB上四通道、每通道两条lane走线长度不可能完全一致pin-to-pin之间的skew和DCO到各lane的相位差各不相同必须用IDELAYE2做逐lane的微调。IDELAYE2在7系列里是每根输入引脚独立可调的延迟单元这才是解决这类问题的正解。另外要注意DCO进入BUFIO后原本在GCLK上的约束需要跟着改。BUFIO只能驱动IO Bank内部的资源不能直接驱动内部逻辑。解串后的数据和时钟要进入BUFR分频时钟域再通过异步FIFO跨到用户逻辑时钟域。不要图省事直接拿DCO去跑用户逻辑那样时钟树会乱套。2. SPI寄存器配置从“读不到ID”到正常出数的调试路径2.1 SPI时序与Verilog状态机设计AD9653的SPI是标准的ADI式三线接口SCLK、SDIO、CSB。指令字是16bit后面跟8bit数据MSB在前。写寄存器时先发16bit指令再发8bit数据读寄存器时先发指令再在下一个时钟周期读数据。很多第一次调的人会把读写方向搞错尤其是SDIO这个双向引脚读之前必须把输出使能拉低否则FPGA侧驱动和三态没切换干净数据永远是乱的。我用的是一段简单的三段式状态机状态划分是IDLE、SEND_CMD、SEND_DATA、WAIT、DONE。在SEND_CMD阶段逐bit发送16bit指令SEND_DATA阶段发送8bit数据WAIT阶段留几个时钟周期让ADC内部锁存再拉高CSB。这里有个实际经验指令和数据的bit位序一定要先核对芯片手册如果是MSB first发送时从bit15开始循环到bit0别用for循环里顺手从0开始的小习惯否则整套配置静默失败寄存器全没写进去。下面给一个写寄存器的核心片段方便直接复用// spi_wr_reg: 发送16bit指令 8bit数据 always (posedge clk or negedge rst_n) begin if (!rst_n) begin sclk 0; sdio_out 1b0; csb 1; state IDLE; end else begin case (state) IDLE: begin csb 1; if (wr_en) begin tx_cmd {1b0, 1b0, reg_addr[12:0]}; // 写指令 tx_data reg_wdata; bit_cnt 4d16; csb 0; state SEND_CMD; end end SEND_CMD: begin sclk 0; sdio_out tx_cmd[15]; tx_cmd {tx_cmd[14:0], 1b0}; if (bit_cnt 0) begin // SCLK上升沿锁存发完16bit切数据段 end end ... endcase end endSCLK空闲电平建议拉低在上升沿输出数据、下降沿让ADC锁存这个组合在多数ADI ADC上比较稳。实际工程里还要加一个发送完成后的回读校验步骤写完后把同一地址读回来FPGA内部比较读回值和期望值是否一致不一致就在调试寄存器里置错误标志而不是闷头往下执行。这个习惯帮我抓出过两次因为时序余量不足导致的偶发写错。2.2 关键寄存器配置顺序AD9653的寄存器映射有很多版本差异网上教程给的地址不一定对得上你的批次所以下面只给配置类别和顺序具体地址务必对照手册。上电后第一步做软复位通常写0x00地址第二步配置输出模式和LVDS驱动包括DCO极性、DCO分频、LVDS电流等第三步配置测试模式比如输出RAMP斜坡码或者K码用于后面自校准第四步在校准完成后关闭测试模式切回正常工作数据。有一个值得强调的点是配置顺序不能随意调。比如输出模式还没配好就开测试码型测出来的东西其实是CMOS模式下某个通道的数据会让FPGA侧误以为解串逻辑错了白白排查半天。另外AD9653的DCO极性有时候会因为采样率和输出模式的组合不同而不同手册里的时序图会标明数据是在DCO上升沿还是下降沿有效。FPGA端ISERDESE2的CLKB连接方式要跟着DCO极性来不能所有工程都抄同一个模板。3. LVDS接收核心ISERDESE2、IDELAYE2和XDC约束怎么配合3.1 ISERDESE2的例化与DDR解串参数在7系列FPGA里接AD9653的LVDS数据我习惯让每条lane独立例化一个ISERDESE2核心参数是DATA_RATE(DDR)、DATA_WIDTH(4)、INTERFACE_TYPE(NETWORKING)。用4bit宽而不是8bit是因为窄解串在高频下的时序余量更好而且两条lane各自解出4bit拼出8bit后在逻辑里再拼字节流程清晰。如果你的DCO频率相对较低、想一次解8bit也可行但要注意ISERDESE2的DDR模式宽度到8时需要master-slave级联写起来更绕。ISERDESE2 #( .DATA_RATE(DDR), .DATA_WIDTH(4), .INTERFACE_TYPE(NETWORKING), .DYN_CLKDIV_INV_EN(FALSE), .INIT_Q1(1b0), .INIT_Q2(1b0), .INIT_Q3(1b0), .INIT_Q4(1b0), .NUM_CE(1) ) iserdes_lane0 ( .D(adc_lane0_delayed), // 经过IDELAYE2后 .CLK(dco_bufio), // DCO经过BUFIO .CLKB(~dco_bufio), // DDR双沿 .CLKDIV(dco_bufr), // BUFR分频时钟 .RST(serdes_rst), .Q1(q1), .Q2(q2), .Q3(q3), .Q4(q4), .BITS LIP(bitslip_lane0) );这段例化里有几个细节容易踩。第一CLKB用取反的DCO前提是DCO占空比满足要求实际调试时用ILA看解出来的Q1-Q4是否稳定不稳就说明DCO占空比或者时序约束有问题。第二Q1/Q3对应DCO上升沿采到的相邻bitQ2/Q4对应下降沿拼数据时注意顺序不是简单的Q1Q2Q3Q4拼接而是Q1Q2Q3Q4按照时间先后确定bit位拼反了现象是数据完全乱套但自检逻辑又查不出时钟问题。3.2 IDELAYE2的例化与XDC约束要点IDELAYE2我用的是VAR_LOAD模式因为自校准过程中要动态写入tap值而VAR_LOAD可以直接通过CNTVALUEIN加载目标延迟值不需要像VAR_LOADABLE模式那样反复打CE和INC脉冲校准逻辑会简单很多。IDELAYE2 #( .IDELAY_TYPE(VAR_LOAD), .IDELAY_VALUE(0), .REFCLK_FREQUENCY(200.0), // 参考时钟影响tap分辨率 .HIGH_PERFORMANCE_MODE(TRUE) ) idelay_lane0 ( .C(delay_clk), .LD(idelay_ld), .CE(1b0), .INC(1b0), .CNTVALUEIN(tap_value[4:0]), .CNTVALUEOUT(idelay_tap_out[4:0]), .IDATAIN(adc_lane0), // 原始LVDS单端信号 .DATAOUT(adc_lane0_delayed) );7系列IDELAYE2的tap分辨率默认约78ps参考时钟为200MHz时共32个tap总调节范围约2.5ns。AD9653在125MSPS下每lane的bit周期要看实际DCO频率但这个延迟范围覆盖一个完整UI通常没问题。当采样率更高或者需要更大调节范围时可以从IDELAYCTRL参考时钟上做文章或者考虑两条lane共用DCO来减少相对skew。XDC约束是另一个大坑。LVDS差分对的约束要成对写比如ADC_D0_p和ADC_D0_nIOSTANDARD要一致DIFF_TERM必须同时添加。DCO引脚要单独创建时钟create_clock -name adc_dco -period 4.000 [get_ports {ADC_DCO_p}]这里的period只是示例实际要根据你的DCO频率填写。如果不加这条约束Vivado会把DCO当成普通数据信号时序分析时所有解串逻辑都变成无时钟域路径bitstream照样能生成但上板后数据就是时好时坏。另外BUFR/BUFIO路径上不要手动添加create_generated_clock让工具自动推断否则容易产生跨时钟域的错误约束。4. 延时自校准扫描tap值找出采样眼图中心4.1 为什么非做自校准不可很多人以为只要约束对了、代码仿真好上板数据就该是对的。但实际PCB上四通道各条lane的走线长度不一样DCO到每个通道的时钟偏斜也不一样再加上温度变化固定延迟值调一次焊死不是长久之计。AD9653每通道的DCO和数据线是一起输出的理论上同源同相但芯片内部输出驱动到不同pin的延迟不一致板级走线也不一致实际到达FPGA引脚时DCO边沿可能正好压在某个bit的跳变沿上。这时候无论怎么改逻辑都修不好只能Per-lane调延迟。4.2 扫描算法与Verilog实现思路自校准的基本流程是FPGA通过SPI把ADC切成测试码型输出最常用的是RAMP斜坡码因为每个样本的16bit值都是线性递增比对逻辑非常简单。接着对每条lane的IDELAYE2从tap 0到31逐个扫描每个tap值下采集固定数量的数据和期望的斜坡码比较记录误码bit数和误码率。扫描状态的伪代码for lane 0 to 3: for tap 0 to 31: 写IDELAYE2的CNTVALUEIN tap 等待FIFO复位和同步完成 采集4096个采样点 统计与RAMP码不一致的bit数 记录到结果表中 tap_optimal 取连续误码为0区间的最中间值 把tap_optimal写回IDELAYE2并锁存判断最优tap值有个容易忽略的操作不能只找一个误码为0的点要找一个连续误码为0的区间取区间中点。这才是采样眼图的正中心两边都留出余量。如果只取第一个误码为0的tap温度稍漂一点就滑出采样窗口了。实际工程里我一般要求连续无错误tap数至少大于8少于8就说明这个通道的信号质量本身有问题应该检查硬件而不是继续调软件。AD9653的测试码型在RAMP模式下偶数样本和奇数样本规律略有不同建议用16bit全量累加比用低字节判断要可靠。下面是一段简单的判定伪代码// 每个采样周期期望值 上一周期期望值 1 // 解串拼出的data expected_value时计为正确 always (posedge clk) begin if (sample_valid) begin if (data_out ! expected) err_cnt err_cnt 1; expected expected 1b1; end end校准完成后这些tap值建议尽量在软件里固化一份上电后由一个小状态机自动加载不必每次都重新扫描。但如果板卡工作环境温度变化大每次上电扫描也只需要几十毫秒这个开销完全可以接受。5. 实测踩坑测试码型、ILA抓取和四条lane错位的根因5.1 第一次上电数据是乱的先查SPI再查DCO极性我遇到过最典型的乱数场景是寄存器配置全都回读正确但ILA里抓到的数据完全不是RAMP而是像噪声一样的随机数。后来发现是DCO极性反了。AD9653的DCO在默认配置下可能是上升沿出数据、下降沿锁存也可能反过来不同配置下会出现一个沿的先后关系差异。FPGA这边ISERDESE2的CLKB连法跟着变或者通过SPI把DCO极性配置项改过来。排查方法很简单把ADC切成固定码型比如输出0xAAAA如果DCO极性反了解出来大概率是0x5555或者乱序一眼就能判断。另一个低频问题反而是SPI配置没生效常见原因是上电后没有做软复位或者复位完后立刻开始配置芯片内部还没稳定。建议上电后至少延时1ms再操作SPI软复位完成后延时几百微秒再写后续寄存器。5.2 四通道数据错位看起来像随机数其实是各lane tap值不一致图像类应用最怕这种错位单看每个通道数据像是正常的但四个通道对齐后互相之间差了若干毫秒。这个问题的根子是四个通道一共八条lane的延迟各不相同有的lane落在采样窗口左边缘有的落在右边缘解出来的字边界就错开了。解决办法就是前面写的自校准扫描必须逐lane独立执行不能图省事让四条lane共用一个tap值。调试时用ILA同时抓四个通道的RAMP码很容易看出差别正确lane的数值序列是0x0000、0x0001、0x0002递增错位lane则可能出现0x0102、0x0103这种字节互换现象这就是两条lane延迟不一致导致高低字节拼接错位。遇到这个现象不要改逻辑直接回到tap扫描表上看每条lane的无错误区间在哪把区间中心写回去就好了。5.3 其他值得留意的坑FIFO读时钟和校准结果保存解串后的数据进入异步FIFO写时钟是BUFR读时钟是用户逻辑时钟。这里有个常见的坏习惯用复位信号同时门控读写时钟或者用组合逻辑直接生成读使能。FIFO的读时钟一定要保证自由运行读使能用脉冲信号而不是电平信号。否则在系统复位释放瞬间FIFO指针可能出现亚稳态数据流里偶尔冒出一个坏点自校准扫描时还会把误码率抬得很高。另外校准结果在调试阶段要打印出来或者存到寄存器不要只在仿真里看一下。我记得有一次板卡从实验室搬到现场温度变了大约十几度之前定的tap值从眼图中心滑到了边缘数据开始偶发跳字。后来把校准逻辑改成每次上电扫一遍这个现象就再没有出现过。实物阶段遇到环境变化直接看日志里的tap值变化比拿示波器去戳引脚快得多。我自己常用的一种做法是在校准完成后把四通道、八条lane的最优tap值打一组log并同步到板上的EEPROM区域。下次上电先按历史tap值加载校验通过就直接跑不通过再全扫描。这样既不牺牲启动速度又留了一条温漂后的自恢复路径后期维护时也能清楚看到每块板子的差异。本文还有配套的精品资源点击获取