做高速ADC采集的FPGA工程,十有八九会遇到采样数据不对齐的问题。之前我调一块250MSPS的ADC板卡,数据率一上来,直接用普通IO去采就是乱码,眼图看起来没问题,采回来却是错位的,折腾了两天才发现是数据和时钟的相位偏差把采样沿逼到了建立时间边缘。后来把IDELAYE3和ISERDESE3这套I/O原语用起来,问题才算根治。这篇就把这套基于Vivado 2018.3的高速ADC数据采集方案完整拆开,从原理到仿真代码到调试踩坑,一次说清。
这套方案解决的核心问题是:ADC输出的高速源同步数据,如何在FPGA内部稳定无误地采集下来。它适合正在做高速数据采集、软件无线电、信号处理前端的朋友,也适合准备在UltraScale/UltraScale+器件上使用IDELAYE3和ISERDESE3但还没完全弄清配置细节的朋友。看完你就能搭出一个带仿真和源码验证的采集通道。
1. 高速ADC采集的时序痛点与解决思路
1.1 为什么普通IO采集会翻车
很多ADC芯片输出的是源同步接口,也就是说芯片会同时送出一路随路时钟,数据以这个时钟为基准传输。听起来很简单,数据跟着时钟走就行了,但实际工程里坑很多。
第一,PCB走线长度不可能完全一致。假设ADC数据率是500Mbps,数据的有效窗口只有2ns左右,走线哪怕差个1cm,传输延迟就差约70ps,再加上过孔、连接器等不连续因素,各bit的到达时间会出现明显的分散。
第二,FPGA内部IOB触发器本身也有建立保持时间要求。数据到达IOB的路径如果穿过普通逻辑,延迟会进一步放大。更麻烦的是,每一根数据线的延迟都不一样,统一用时钟去采,有的bit落在眼图中间,有的正好落在边缘甚至已经翻转到下一个bit。
第三,温度和电压变化会改变路径延迟。你调试环境里能采对,放到高温箱里可能又开始出错。
普通IO采集方案本质上是用一根时钟同时去采所有数据线,而所有数据线之间、数据与时钟之间的相对延迟又无法单独调整。这种方案只适用于低速或者数据宽度很小的场景,高速ADC完全行不通。
1.2 IDELAYE3和ISERDESE3各管哪部分
IDELAYE3和ISERDESE3是Xilinx UltraScale和UltraScale+系列FPGA里的专用I/O原语,各管一段活。
IDELAYE3负责给每根数据线加精细可调的延迟。你可以把它理解成每根线前面接了一个高精度的可调延时器,精度能做到几十皮秒甚至更低。数据线到达时间不一致,就分别调整每一根的延迟值,把每根线都对齐到采样窗口的中央位置。这不光是固定延迟,还能在上电后动态调整,做眼图扫描时特别有用。
ISERDESE3负责把高速串行数据转成FPGA内部能处理的并行数据。高速ADC的数据率动辄几百Mbps甚至上Gbps,直接靠内部逻辑寄存器去采不现实。ISERDESE3在IOB附近直接做串并转换,1:4或者1:8展开后,内部时钟频率就能降到几十兆赫兹,后续逻辑才能稳定收敛。
这两个原语搭配起来,数据通路就成了这样:ADC串行数据先进IDELAYE3做粗细调整,再进ISERDESE3完成高速串并转换,输出低速并行数据后交给内部逻辑处理。
1.3 前期准备:器件选型与开发环境
用IDELAYE3和ISERDESE3有个前提,器件必须是UltraScale或UltraScale+系列。如果你用的是7系列器件,对应原语是IDELAYE2和ISERDESE2,用法类似但端口和参数有明显差异,别搞混了。
开发环境这块,Vivado 2018.3对这两个原语的支持已经很成熟,我用的就是这个版本。安装和license相关的问题网上资料很多,这里不多说。需要注意一点,如果要用仿真,Vivado自带的仿真器直接就能跑,不需要额外装第三方工具,考虑到IDELAYE3和ISERDESE3都有对应的仿真模型,只要在工程里正常例化就能出波形。
2. 系统架构与数据通路设计
2.1 数据通路总览
整个采集通道的逻辑架构是固定的套路,我画了个框给大家一个整体感。
ADC差分数据进入FPGA后,先经过IBUFDS转成单端信号,然后接IDELAYE3做延迟调整。延迟后的信号进入ISERDESE3做DDR串并转换,输出并行数据。并行数据再通过一个简单的寄存器级继续处理,最后以并行数据和有效标志的形式输出。
高速差分时钟DCO同样经过IBUFDS进入FPGA,之后接BUFG或者BUFGCE_DIV生成两路时钟:一路作为ISERDESE3的高速采样时钟,另一路分频后作为并行数据的随路时钟。
这个结构里有几个设计要点值得展开说。
2.2 时钟方案设计
时钟是这套方案的关键,时钟选不好,配置全对也白搭。
ISERDESE3有两个时钟输入:CLK和DCLK。其中CLK接的是高速采样时钟,也就是ADC的DCO经过BUFG后的时钟信号。DCLK接的是分频后的并行时钟,它的频率取决于你配置的并行数据宽度。
举个例子。ADC的DCO是250MHz,DDR模式下每个周期送2bit,数据率就是500Mbps。如果ISERDESE3配置成8位并行输出,那么DCLK的频率就是DCO的四分之一,也就是62.5MHz。这两个时钟之间的相位关系由BUFGCE_DIV保证,不需要额外操心。
在UltraScale系列里,BUFGCE_DIV这个原语可以完成整数分频,同时保持与输入时钟的相位关系,比用MMCM手动生成要方便得多。我用的是BUFGCE_DIVIDE=4,直接得到62.5MHz的并行时钟,同时作为输出数据随路时钟和内部逻辑时钟。
需要特别留意的是,ISERDESE3的CLK和CLK_B在DDR模式下必须接互补时钟。上板时如果只有单端时钟源,可以通过BUFG输出反转或者用原语自己生成反相时钟。仿真阶段直接用~clk就行,Vivado仿真模型能正确处理这种接法。
2.3 字对齐策略
串并转换之后还有一个问题:输出的8位并行数据从哪里开始。ISERDESE3不管字节边界,它只是机械地每4个时钟周期打包一个8位数据。如果ADC协议本身没有帧同步信号,你就得自己从数据流里找边界。
常见做法是先在ADC配置寄存器里把测试码型打开。比如让ADC输出固定pattern,像0xA5、0x5A交替。FPGA在接收端做滑动检测,不断调整数据取出的起点,直到匹配到pattern为止。这种用pattern做字对齐的方法实现简单,工程里最常用。
如果ADC支持帧同步信号FCO,会把问题再简化一些。FCO对应的是每个采样组的第一位,可以把它作为字对齐的参考信号,但要注意它和数据线的相对延时也需要做和普通数据一样的延迟调整。
在我这个演示工程里,为了聚焦IDELAYE3和ISERDESE3的用法,直接把并行数据的每次采出的8bit做连续比对,通过监测pattern的方法来验证对齐是否正确。实际产品里你需要一个状态机来管理和锁定这个对齐过程。
3. IDELAYE3和ISERDESE3原语使用详解
3.1 IDELAYE3端口与参数拆解
IDELAYE3的端口不算多,但每个都值得弄清楚。
数据相关端口有IDATAIN、DATAIN和DATAOUT。IDATAIN是来自IO的直接输入,DATAIN是来自FPGA逻辑的输入,二者选其一,通过DELAY_SRC参数控制。实际ADC采集场景用IDATAIN就够了,它直接连来自IBUFDS的单端信号,路径延迟最小,时序最好控制。
控制相关端口包括C、CE、INC、LD、LDPIPEEN。C是控制时钟,所有动态调整操作都同步于这个时钟。CE和INC用于步进式调整,CE使能时,每个C时钟周期根据INC电平决定延迟增加还是减少。LD是加载端口,在VAR_LOAD模式下,LD拉高一个周期就能把CNTVALUEIN的值加载为新的延迟值。LDPIPEEN是管道加载使能,配合VAR_LOAD_PIPE模式使用,一般用不上。
状态输出端口CNTVALUEOUT会实时输出当前的延迟计数值,调试时可以接到ILA里查看当前延迟值,非常实用。
参数方面重点讲三个。DELAY_TYPE支持FIXED、VARIABLE、VAR_LOAD三种。FIXED是固定延迟,综合后不能改,适合最终定稿。VARIABLE是动态可变的,通过CE和INC步进调整。VAR_LOAD是直接加载指定值,工程调试阶段最推荐这种方式,因为你可以通过寄存器或者ILA动态改延迟值,做眼图扫描很方便。DELAY_VALUE配置初始延迟值,DELAY_FORMAT决定了这个值是时间(TIME模式)还是计数(COUNT模式)。
3.2 ISERDESE3端口与配置要点
ISERDESE3比IDELAYE3更简单直接。
D是串行数据输入,接IDELAYE3的DATAOUT。Q是并行数据输出,宽度由DATA_WIDTH参数决定。CLK和CLK_B是高速采样时钟,DCLK是并行时钟。RST是高有效复位,这一点容易搞错,很多习惯用~rst_n的人第一次用会在这里栽跟头。RDY是数据有效标志,每个DCLK周期指示Q上的数据是否有效。
配置上的关键是DATA_WIDTH和DDR_MODE。DDR模式下DATA_WIDTH支持4和8,对应并行时钟频率是高速时钟的1/2或1/4。SDR模式下支持2、4、8,并行时钟频率是高速时钟的1/2或1/8。做高速ADC采集,基本都用DDR模式,因为大部分高速ADC芯片输出就是DDR接口,8位宽度的选择也比较均衡,并行时钟不会太高,内部逻辑比较好收敛。
有一个细节需要特别提醒:ISERDESE3没有BITSLIP功能。7系列里的ISERDESE2有专用的BITSLIP端口,可以实现比特级滑动对齐,而ISERDESE3简化掉了这个功能,字对齐的重任落在了外部逻辑上。很多从7系列迁到UltraScale系列的工程师会在这里困惑,误以为是端口没接全。解决办法是用一个可配置起始位置的移位寄存器完成等效操作,兼容性问题并不大。
3.3 延迟值怎么定,如何做眼图扫描
延迟值的选择是最考验经验的部分,但也有标准化的调试流程。
先给出一个粗略估算。ADC芯片的数据相对DCO会有输出延迟,这个值在数据手册里叫tOD,可能有几百皮秒到几纳秒。PCB走线还会引入额外延迟。你要做的,就是让采样点落到数据眼图的正中央,即让IDELAYE3的延迟大致等于数据到达时间与采样时钟边沿之间的差值。
实际工程中很少去精确计算这个值,因为路径延迟受温度和电压影响很大,算得再准也架不住现场环境变化。更实用的办法是做眼图扫描。
眼图扫描的操作方法是:将IDELAYE3配成VAR_LOAD模式,从0开始逐步增加延迟值,每一步采集一段固定数量的数据并统计误码率,把误码率最低的区间找出来,然后选择这个区间的中间值作为最终延迟设置。这样得出的值留有一定裕量,温度变化时不会立刻压到边界。
这里推荐一个小技巧:做眼图扫描时,给ADC配置一条特定的测试码型,比如伪随机序列,然后利用FPGA内部的ILA或者自动化位置统计标志,识别各delay value下数据是否通过CRC校验。通过值和失败值的分布,就能直接画出眼图轮廓。
4. 完整代码实现与仿真验证
4.1 工程目录与模块划分
先说工程结构,方便照着搭。整个demo工程分成三个核心文件:adc_capture_top.v是顶层,负责系统时钟生成、DCO时钟管理、IDELAYE3和ISERDESE3的原语例化;adc_model.v模拟ADC输出的行为模型,由它产生DCO和DDR格式的串行数据;tb_adc_capture.v是仿真激励,也是这次要重点解读的部分。
如果你的工程要扩展到更多通道,可以按单通道模块复制,每路有自己的IDELAYE3和ISERDESE3实例,延迟值分别独立配置。
4.2 核心代码:IDELAYE3和ISERDESE3实例化
先看顶层模块的美鸟。
module adc_capture_top #( parameter DATA_WIDTH = 8 )( input wire sys_clk_p, // 板级差分时钟 200MHz input wire sys_clk_n, input wire rst_n, // ADC 差分接口 input wire adc_d_p, // ADC 数据通道 input wire adc_d_n, input wire adc_dco_p, // ADC 随路时钟 DCO input wire adc_dco_n, // 并行数据输出 output wire [DATA_WIDTH-1:0] adc_data, output wire adc_data_valid, output wire adc_data_clk );时钟部分示意:
wire sys_clk_200m; wire dco_clk; wire dco_clk_div; IBUFDS u_sys_clk_ibufds ( .I (sys_clk_p), .IB (sys_clk_n), .O (sys_clk_200m_buf) ); BUFG u_sys_clk_bufg ( .I (sys_clk_200m_buf), .O (sys_clk_200m) ); IBUFDS u_dco_ibufds ( .I (adc_dco_p), .IB (adc_dco_n), .O (dco_clk_buf) ); BUFG u_dco_bufg ( .I (dco_clk_buf), .O (dco_clk) ); BUFGCE_DIV #( .BUFGCE_DIVIDE (4), // 250MHz / 4 = 62.5MHz .BUFG_DIVIDE ("TRUE") ) u_dco_div ( .I (dco_clk), .CE (1'b1), .CLR(1'b0), .O (dco_clk_div) );数据通道部分,这是核心中的核心:
wire adc_d_single; wire adc_d_delayed; wire [7:0] q_data; wire rdy; wire [8:0] delay_cnt_out; // 差分转单端 IBUFDS u_data_ibufds ( .I (adc_d_p), .IB (adc_d_n), .O (adc_d_single) ); // 延迟调整 IDELAYE3 #( .DELAY_FORMAT ("COUNT"), .DELAY_SRC ("IDATAIN"), .DELAY_TYPE ("VAR_LOAD"), .DELAY_VALUE (0), .REFCLK_FREQUENCY (200.0), .SIM_DELAY_D (0) ) u_idelaye3 ( .IDATAIN (adc_d_single), .DATAIN (1'b0), .DATAOUT (adc_d_delayed), .C (sys_clk_200m), .CE (1'b0), .INC (1'b0), .LD (delay_ld), .LDPIPEEN (1'b0), .CNTVALUEIN (delay_cnt), .CNTVALUEOUT(delay_cnt_out), .RST (~rst_n), .EN_VTC (1'b0) ); // 串并转换 ISERDESE3 #( .DATA_WIDTH (8), .DDR_MODE ("TRUE"), .SIM_DELAY_D(0) ) u_iserdese3 ( .D (adc_d_delayed), .Q (q_data), .CLK (dco_clk), .CLK_B(~dco_clk), .DCLK(dco_clk_div), .RST (~rst_n), .RDY (rdy) ); // 输出寄存器 reg [7:0] adc_data_r; reg adc_valid_r; always @(posedge dco_clk_div or negedge rst_n) begin if (!rst_n) begin adc_data_r <= 8'd0; adc_valid_r <= 1'b0; end else begin adc_data_r <= q_data; adc_valid_r <= rdy; end end assign adc_data = adc_data_r; assign adc_data_valid = adc_valid_r; assign adc_data_clk = dco_clk_div;这段代码有两点要注意。第一,delay_cnt和delay_ld在调试时可以通过寄存器或者ILA动态改,在正式使用时可以直接把delay_cnt赋固定值。第二,IDELAYE3的C端口接的是200MHz系统时钟,不是DCO。IDELAYE3的控制逻辑只需要一个稳定的参考时钟,不需要和数据时钟同源。
4.3 仿真测试平台与激励构造
写testbench前要理解一个关键模拟对象,ADC的行为模型。实际ADC芯片在DCO上升沿和下降沿都会输出数据,DDR模式,而且数据相对时钟边沿有个tOD延迟。我在模拟模型里故意加了一个0.5ns的延迟,用来模拟真实场景下的数据与时钟相位偏差。
module adc_model ( input wire dco, input wire rst_n, output reg dout, output wire dco_out ); reg [7:0] tx_data; reg [2:0] bit_idx; reg bit_valid; assign dco_out = dco; // 测试码型: 0xA5, 0x5A 交替 always @(posedge dco or negedge rst_n) begin if (!rst_n) begin tx_data <= 8'hA5; bit_idx <= 3'd7; bit_valid <= 1'b0; end else if (bit_idx == 3'd0) begin tx_data <= (tx_data == 8'hA5) ? 8'h5A : 8'hA5; bit_idx <= 3'd7; end else begin bit_idx <= bit_idx - 3'd1; end end // DDR输出,rise edge 输出偶数位,fall edge 输出奇数位 always @(posedge dco or negedge rst_n) begin if (!rst_n) begin dout <= 1'b0; end else begin #0.5 dout <= tx_data[bit_idx]; // tOD=0.5ns end end always @(negedge dco or negedge rst_n) begin if (!rst_n) begin dout <= 1'b0; end else begin #0.5 dout <= tx_data[bit_idx - 1]; // tOD=0.5ns end end endmodule这里做了一个简化处理,实际ADC在上升沿和下降沿输出哪一位,取决于协议定义。这里通过bit_idx模拟最常见的MSB-first规则。关键是#0.5这个延迟,它让数据边沿相对时钟偏移0.5ns,如果在testbench中不调节IDELAYE3的延迟值,采样就会落在数据翻转区域附近,出现不稳定。
testbench主体如下。
`timescale 1ns / 1ps module tb_adc_capture; reg sys_clk_p; reg sys_clk_n; reg rst_n; reg adc_d_p; reg adc_d_n; wire adc_dco_p; wire adc_dco_n; wire [7:0] adc_data; wire adc_data_valid; wire adc_data_clk; reg delay_ld; reg [8:0] delay_cnt; integer err_cnt; sys_clk_p = 1'b0; sys_clk_n = 1'b1; forever begin #2.5; sys_clk_p = ~sys_clk_p; sys_clk_n = ~sys_clk_n; end // DCO 250MHz reg dco; initial begin dco = 1'b0; forever #2 dco = ~dco; end assign adc_dco_p = dco; assign adc_dco_n = ~dco; adc_model u_adc_model ( .dco (dco), .rst_n (rst_n), .dout (adc_d_p), .dco_out(adc_dco_p) ); assign adc_d_p = u_adc_model.dout; assign adc_d_n = ~u_adc_model.dout; adc_capture_top u_dut ( .sys_clk_p (sys_clk_p), .sys_clk_n (sys_clk_n), .rst_n (rst_n), .adc_d_p (adc_d_p), .adc_d_n (adc_d_n), .adc_dco_p (adc_dco_p), .adc_dco_n (adc_dco_n), .adc_data (adc_data), .adc_data_valid(adc_data_valid), .adc_data_clk (adc_data_clk) );实际的testbench中直接连接模型的dout到顶层输入,这里为了可读性省略了一些中间连线,但思路就是这样。
下面这段监测逻辑很有用,它每收到一个有效数据就比对一次预期值,并打印出错位置,方便判断延迟配置是否正确。
reg [7:0] expect_data; always @(posedge adc_data_clk or negedge rst_n) begin if (!rst_n) begin err_cnt <= 0; expect_data <= 8'hA5; end else if (adc_data_valid) begin if (adc_data !== expect_data) begin err_cnt <= err_cnt + 1; $display("DATA MISMATCH at %0t: expect %02X, got %02X", $time, expect_data, adc_data); end #1 expect_data <= (expect_data == 8'hA5) ? 8'h5A : 8'hA5; end end需要注意expect_data的更新要模拟高低字节交替规律,和adc_model中的码型保持一致。
4.4 仿真结果与延迟调整验证
仿真运行后,如果delay_cnt默认是0,大概率会看到大量DATA MISMATCH报错。这时候做一步操作,把延迟值改成大约6到10,再跑一轮,mismatch会明显减少甚至完全消失。
为什么是这个区间?前面说过IDELAYE3在200MHz参考时钟下单个tap的延迟约为几十皮秒量级,ADC模型里加了0.5ns的tOD,所需延迟值就落在5到15之间。具体数值和Vivado模型的精度有关,但这个数量级是稳定的。
这里的核心验证逻辑是:不调延迟,采不到正确数据;调了延迟,数据就对了。这恰好证明了IDELAYE3在采样时刻调整中的作用。
实际工程中,这个adjust过程就是靠上板后反复扫描,找到最稳的tap值。仿真阶段的好处是环境可控,能快速验证逻辑和原语配置本身有没有问题。
5. 常见问题与调试技巧
5.1 数据对不齐,老是多一个bit或少一个bit
现象是数据能采到,但值不对,看起来像byte内的bit顺序颠倒了。先分清是bit顺序问题还是bit对齐问题。
bit顺序问题通常是ADC协议定义的MSB/LSB顺序和你串转并后的输出顺序不一致。解决办法是把Q输出按位重排,或者调整测试pattern去验证。bit对齐问题则是字边界没有找对,需要根据pattern去滑动边界。
排查方法建议从确定性的测试pattern入手。让ADC输出固定值,比如所有通道输出0x55,看采回来是不是0x55。如果不是,先画一下数据位序映射关系,找到规律后再统一调整位映射或者字对齐逻辑。
5.2 眼图扫描找不到合适的延迟值
一种典型情况是所有延迟值扫完,数据都有误码。先检查时钟域是否正常。ISERDESE3的CLK和DCLK如果不是同一个来源生成的,相位关系不对,数据流会彻底错乱。
另一种情况是延迟步进太大,把眼图中间的有效区域跳过去了。这时候需要检查IDELAYE3的参考时钟频率。参考时钟频率越高,每个tap代表的延迟越小;扫描粒度越细,越容易找到稳定区间。
如果扫描后稳定区间的宽度非常窄,比如只有1到2个tap,说明PCB布线质量存在隐患。走线阻抗不连续、时钟和数据线长度差过大,都会导致眼图收窄,这时候靠FPGA内部调整能做的有限,需要回到PCB层面处理。时钟线尽量短且等长,数据线之间保持等长,电源引脚附近做好去耦,这些基础工作做到位,眼图裕量自然就出来了。
5.3 仿真通过,上板却采集失败
这是最让人头疼的情况。仿真只是验证了逻辑功能和原语配置的正确性,真实板子上的延迟值、时钟质量、电源噪声,仿真里统统没有。
仿真时IDELAYE3的tap延迟是模型值,上板后的实际值受工艺、电压、温度影响,可能差很多。解决思路是不要用仿真确定的延迟值直接上板,而是把它作为初始值,上电后跑一遍实际眼图扫描,锁定真正合适的延迟区间。
还有一个容易忽视的点,差分信号的端接电阻。IBUFDS的DIFF_TERM参数要打开,否则高速差分信号在跨过接收阈值时会反复抖动,导致采样到的数据边缘抬升。这个参数在仿真中不明显,上板后非常关键。
5.4 调试工具与技巧总结
把IDELAYE3的CNTVALUEOUT接到ILA上是必须做的一步。ILA里可以实时看到当前延迟值,配合动态调整VAR_LOAD的能力,就能在调试界面里直接改变延迟配置,观察数据是否变正确。这就相当于在FPGA内部做了一台简易示波器。
再一个建议是延迟值的调整不要采用连续步进方式,连续扫描会花大量时间。可以先粗扫一遍,比如步进8到16,找到大致区间,再在区间内细化。大部分情况下,稳定的延迟窗口不止一个tap,粗扫找到的趋势足以指导你快速锁定位置。
最后,产品化阶段把VAR_LOAD改成FIXED,固定延迟值,省掉动态加载逻辑的资源开销。但保留一份接口和寄存器定义,后续如果需要做温度补偿,随时可以切回来。
这套IDELAYE3加ISERDESE3的方案我在多个项目里验证过,只要配置正确、扫描流程走一遍,高速数据的稳定性就有保障。仿真代码的核心逻辑都在这篇里了,照着梳理下来应该能少走不少弯路。