简介:这份PDF文档面向具备一定FPGA开发经验、正在接触高速ADC与JESD204B协议的研发人员,围绕Vivado平台下AD9680的Verilog测试程序展开,重点解决1G采样率、Lane4通道配置与数据接收链路的实现问题。全文以AD9680寄存器初始化、PLL时钟生成和JESD204B接收模块为主线,配有可直接参考的代码片段与状态机框架,并穿插时钟相位对齐、SPI配置时序、链路同步等调试经验,便于读者对照自身采集系统排查问题、调整参数。资源包共1个PDF文件,大小约111KB,篇幅紧凑,适合作为案头查阅的配置与接收流程参考。目前已有135人学习下载,对希望快速理解AD9680在FPGA侧配置要点与JESD204B接收思路的工程师具有一定借鉴价值。
1. 链路先锁再谈采样:为什么1G采样率下最贵的不是ADC而是时钟树
FPGA开发里,高速ADC的JESD204B链路最反直觉的一点是:采样率越高,瓶颈越不在ADC本身,而在时钟树和链路对齐。1G采样率、Lane4、5Gbps线速率这套组合,表面看是Verilog状态机在收数,底层却是器件时钟、收发器参考时钟、SYSREF三者相位关系的合谋。很多人第一次上板,ILA里全是错位数据,误以为是解码写错,实际是时钟管理没把同源不同频的路径约束清楚。搞懂这条链路为什么先锁再采,后面调SPI、调均衡、调lane延迟才有判断依据。
2. SPI配置AD9680:寄存器写顺序为什么比寄存器值更容易翻车
这颗双通道14位1GSPS ADC的配置入口是SPI,但真正让人掉头发的不是某个寄存器值抄错,而是写寄存器的顺序和时序。上电后多久能写、片选拉低后地址和数据之间隔几个时钟、写完要不要回读、Lane映射和采样率模式谁先谁后,这些细节在数据手册里分散在不同章节,代码里却必须串成一条确定的状态机。寄存器值错了,链路通常直接不锁;顺序错了,链路可能偶尔锁上,然后在温度变化或重启时随机失锁,排查成本高得多。
我一般会这么理解:SPI配置不是“发一串16位数据”,而是“在ADC内部状态机允许的窗口里,按它认的节奏把配置表走完”。所以Verilog里用显式状态机比直接例化一个SPI IP更可控,尤其是需要插入上电延迟、寄存器间隔和回读校验的时候。
2.1 上电延迟与SPI时序:状态机比IP核更稳的底层原因
先看一段SPI写寄存器的状态机骨架。这里把地址和数据分成两个字节发,片选在字节之间保持低电平,发完再拉高,中间插入可调的gap计数。SPI时钟不要超过30MHz,片选空闲为低,这是这颗ADC的硬性脾气。
localparam S_PWR_WAIT = 3'd0; // 上电延迟 localparam S_CS_LOW = 3'd1; // 片选拉低 localparam S_SEND_ADDR= 3'd2; // 发地址 localparam S_SEND_DATA= 3'd3; // 发数据 localparam S_CS_HIGH = 3'd4; // 片选拉高 localparam S_GAP = 3'd5; // 寄存器间隔 always @(posedge spi_clk or negedge rst_n) begin if (!rst_n) begin spi_state <= S_PWR_WAIT; pwr_cnt <= 0; spi_cs <= 1'b0; end else begin case (spi_state) S_PWR_WAIT: begin // 上电后至少等500us,实测少于这个值ADC会随机不响应 if (pwr_cnt >= PWR_500US) spi_state <= S_CS_LOW; else pwr_cnt <= pwr_cnt + 1'b1; end S_CS_LOW: begin spi_cs <= 1'b0; spi_state <= S_SEND_ADDR; end S_SEND_ADDR: begin // 先发8位地址,最高位为0表示写 spi_din <= {1'b0, reg_addr}; if (bit_cnt == 7) begin bit_cnt <= 0; spi_state <= S_SEND_DATA; end else begin bit_cnt <= bit_cnt + 1'b1; end end S_SEND_DATA: begin spi_din <= reg_data; if (bit_cnt == 7) begin bit_cnt <= 0; spi_state <= S_CS_HIGH; end else begin bit_cnt <= bit_cnt + 1'b1; end end S_CS_HIGH: begin spi_cs <= 1'b1; spi_state <= S_GAP; end S_GAP: begin // 寄存器之间留一点间隔,避免ADC内部还没处理完 if (gap_cnt >= GAP_1US) begin gap_cnt <= 0; spi_state <= S_CS_LOW; end else begin gap_cnt <= gap_cnt + 1'b1; end end endcase end end这段代码的关键不在spi_din,而在S_PWR_WAIT和S_GAP。上电延迟不够,ADC内部基准和PLL还没准备好,SPI写进去的值会被忽略,现象是“代码波形全对,回读全错”。寄存器间隔太短,前一个写操作还没落地,后一个就冲进去,现象是“大部分寄存器对,个别关键寄存器偶发恢复默认值”。用IP核当然也能做,但IP核通常只管移位,不管这颗ADC的上电脾气,所以状态机反而更贴近硬件。
还有一个容易忽略的点:片选拉低到第一个SCLK边沿之间要有建立时间,片选拉高前最后一个SCLK边沿之后要有保持时间。代码里如果直接spi_cs <= 0然后下一个时钟就发地址,在200MHz系统时钟下可能只隔了5ns,对30MHz SPI来说够用,但换到更快的系统时钟就得加计数器。血泪经验是:SPI时序不要靠“我觉得够”,要在ILA里同时抓spi_cs、spi_clk、spi_din,看片选和第一个边沿之间到底隔了多少。
2.2 寄存器回读与Lane映射:把“写成功”变成“可验证”
写完不读,等于没写。这颗ADC的SPI支持回读,地址最高位为1表示读,回读值要和写入值逐位比较。比较不通过时不要只亮一个错误灯,要把出错的寄存器地址和读回值一起锁进状态寄存器,后面用ILA或串口打出来。否则现场调试时只知道“配置错了”,不知道错在哪一颗,等于给自己上刑。
Lane映射是另一个隐蔽点。物理Lane0到Lane3在PCB上不一定按顺序连到FPGA收发器,IP核里通常有LANE_MAP参数,把物理通道重新排序。如果Lane顺序错了,链路有时也能同步,因为JESD204B的同步头不关心业务数据顺序,但解出来的I/Q会交错,现象是“频谱上出现镜像或杂散,递增数据检查每隔几个点错一次”。这时候别急着怀疑8B/10B解码,先核对Lane映射和ADC端的Lane使能寄存器。
示例配置里,0x15写0xA3,bit5-3控制Lane数量为4,bit2-0设置时钟分频为2;0x16写0x04做Lane4使能;0x14写0x10进1G采样率模式。不同版本的配置表可能把Lane数放在不同寄存器,以手头手册为准,但顺序上建议先软复位、再采样率模式、再Lane使能、最后JESD相关使能。每写一颗关键寄存器就回读一次,确认后再走下一颗。这样链路不锁时,至少能排除“配置根本没写进去”这一层。
3. 时钟管理:312.5MHz器件时钟、5Gbps线速率与MMCM相位约束怎么对齐
1G采样率下,ADC送出的数据不是“一个时钟打一个样点”这么简单。器件时钟负责采样和帧节奏,收发器参考时钟负责SerDes的PLL,SYSREF负责多帧和Lane对齐的起跑线。三者同源时,相位关系必须可预测;三者不同源时,就得老老实实做CDC和弹性缓冲。很多链路偶发失锁,最后查出来不是协议写错,而是MMCM输出相位偏了、参考时钟抖动大了,或者约束文件里把该管的路径设成了false_path。
312.5MHz器件时钟是一个常见示例值,5Gbps线速率对应到收发器参考时钟时,具体频率取决于收发器PLL的分频比。有的方案把参考时钟放在614.4MHz,有的放在312.5MHz,本质是PLL的M/N分频不同,不能看到别人的参数就直接抄。先用手册和IP核配置算出线速率,再反推参考时钟,最后看MMCM能不能在输入频率和VCO范围内同时吐出器件时钟和参考时钟。
3.1 同源不同频:器件时钟与收发器参考时钟的相位关系
下面是一段时钟管理单元的参数量级示意。输入125MHz,VCO约1GHz,输出500MHz和250MHz两路。具体原语名随器件系列变化,这里只保留参数关系,重点看CLKFBOUT_MULT_F和CLKOUTx_DIVIDE_F怎么决定频率,以及CLKOUTx_PHASE为什么不能随便填。
// 输入125MHz,VCO=125MHz * 8 = 1GHz // 输出0:1GHz / 2 = 500MHz,给收发器参考时钟 // 输出1:1GHz / 4 = 250MHz,给后端处理时钟 localparam CLKIN1_PERIOD = 8.0; // 125MHz localparam CLKFBOUT_MULT_F = 8.0; // VCO=1GHz localparam CLKOUT0_DIVIDE_F= 2.0; // 500MHz localparam CLKOUT1_DIVIDE_F= 4.0; // 250MHz localparam CLKOUT0_PHASE = 0.0; localparam CLKOUT1_PHASE = 0.0; // 例化时钟管理单元,输出adc_clk和gt_refclk // 注意:两个输出同源,但频率不同,相位关系要在约束里说清楚这段参数里,CLKOUT0_PHASE和CLKOUT1_PHASE设成0不代表PCB上一定对齐。MMCM内部输出到引脚、再经过PCB到ADC、再回到FPGA收发器,路径延迟各不相同。SYSREF的建立保持时间尤其敏感,输入里提到用IDELAY扫描12到18个tap最可靠,这个做法很实用:让SYSREF相对帧时钟步进延迟,每步读一次JESD状态,找到链路能锁的窗口,再取窗口中心。不要取边界值,边界值在温度变化时最先崩。
实测中还有一个小技巧:先用低速时钟让SPI配置和SYSREF扫描跑起来,确认ADC能回读、SYSREF能被FPGA捕获,再切到1G采样率。上来就全速跑,时钟树还没收敛,ILA抓到的全是无效跳变,根本分不清是配置问题还是时钟问题。
3.2 XDC约束:set_clock_groups、set_max_delay与false_path的边界
同源不同频的时钟,工具默认会按同步路径分析,但器件时钟和收发器参考时钟之间往往没有真实的业务数据跨域,强行分析会报大量无法满足的路径。常见做法是用set_clock_groups -asynchronous把两个时钟组切开,再用set_max_delay约束真正跨域的控制信号,比如SYSREF捕获、sync状态回传。false_path不是“不用管”,而是“这条路径的时序由我自己保证”,所以必须配合硬件上的建立保持时间和逻辑上的握手。
# 器件时钟和收发器参考时钟同源但不同频,先按异步组切开 set_clock_groups -asynchronous \ -group [get_clocks adc_clk] \ -group [get_clocks gt_refclk] # SYSREF从器件时钟域进收发器域,真实跨域路径给最大延迟 set_max_delay -from [get_pins sysref_sync_reg/Q] \ -to [get_pins jesd_rx_inst/sysref_capture_reg/D] 0.5 # rx_clk到后端处理时钟的跨域,如果用了异步FIFO,可以设false_path set_false_path -from [get_clocks rx_clk] -to [get_clocks proc_clk]这里最容易被误用的是最后一条。异步FIFO的读写指针跨域,格雷码同步器本身不需要时序分析,但FIFO的复位、使能和错误标志如果也跨域,就不能一起false_path。我见过一个案例:FIFO数据路径设了false_path,结果满标志跨域没处理,突发流量下满标志晚了一拍,数据被覆盖,现象是“频谱偶尔炸一下,重启就好”。所以false_path要按路径设,不要按时钟组一把梭。
4. JESD204B接收状态机:从K28.5到多帧对齐,链路是怎么“锁死”的
JESD204B接收不是“检测到一个同步字就开始收数”。它至少有三段:代码组同步、初始通道对齐、数据阶段。代码组同步靠K28.5逗号字符找字符边界,初始通道对齐靠K28.3和多帧结构把四个Lane的延迟拉齐,数据阶段才轮到8B/10B解码、解扰和弹性缓冲。三段揉成一个状态机,最容易出现“仿真能过、上板偶发错位”的问题,因为每一段的超时和复位条件不一样。
示例参数按资料里的L=4、F=2、S=1、线速率约5Gbps来拆。这里要提醒一句:线速率一定用采样率、转换器数M、每样本位宽N'、Lane数L重新算,别照抄。如果M=2、N'=16,算出来会比5Gbps高不少;如果配置表写的是5Gbps,那就要回头核对M和N'是不是被设成了单通道或8位模式。参数之间是联动的,单看一个数没有意义。
| 参数 | 示例值 | 作用 |
|---|---|---|
| L | 4 | Lane数量,物理通道数 |
| F | 2 | 每帧字节数 |
| S | 1 | 每个多帧包含的帧数 |
| M | 2 | 转换器数量,双通道通常为2 |
| SCR | 1 | 加扰使能,必须与ADC端一致 |
4.1 同步头、ILA与弹性缓冲:三个状态不能揉成一个
下面这段状态机把同步、对齐、接收分开。sync_status变低表示ADC请求同步,FPGA要在3个多帧周期内完成字符对齐,否则链路会持续失锁。K28_5用来找字符边界,K28_3用来做多帧对齐,align_counter超过16次还收到无效数据就调整相位重来。
localparam K28_5 = 8'hBC; // 逗号字符,用于字符边界 localparam K28_3 = 8'h7C; // 对齐字符,用于多帧对齐 always @(posedge rx_clk or negedge rst_n) begin if (!rst_n) begin state <= S_IDLE; lane_align <= 1'b0; align_cnt <= 0; ila_trigger <= 1'b0; end else begin case (state) S_IDLE: begin // sync拉低,ADC请求重新同步 if (!sync_status) begin lane_align <= 1'b0; ila_trigger <= 1'b1; // 抓sync跳变时的数据眼图 state <= S_WAIT_K28_5; end end S_WAIT_K28_5: begin // 在接收数据里找K28.5,确定字符边界 if (rx_data == K28_5) begin lane_align <= 1'b1; state <= S_ALIGN; end else if (align_cnt > 16) begin // 长时间找不到逗号,调整采样相位再试 ctrl_adj_phase <= 1'b1; align_cnt <= 0; state <= S_IDLE; end else begin align_cnt <= align_cnt + 1'b1; end end S_ALIGN: begin // 检测K28.3,四个Lane的多帧边界要拉到同一拍 if (rx_data == K28_3 && lane_valid) begin state <= S_DATA; end end S_DATA: begin // 数据阶段:8B/10B解码、解扰、弹性缓冲 // rx_valid抖动时先做2拍滤波,避免误判 if (!rx_valid_filtered) begin state <= S_IDLE; end end endcase end end这段代码里,S_WAIT_K28_5和S_ALIGN分开的原因是可观测性。字符边界没找到时,问题在收发器均衡、参考时钟或差分走线;字符边界找到了但K28.3对不齐时,问题在多帧长度、SYSREF相位或Lane映射。揉成一个状态,ILA里只看得到“没进DATA”,根本分不清该调均衡还是调SYSREF。把状态拆开,再给每个状态一个计数器,调试时看计数器停在哪一段,方向就清楚了。
弹性缓冲也不能省。四个Lane到达FPGA的延迟不可能完全一样,弹性缓冲用读写指针把相位差吸收掉。如果FIFO深度太浅,突发流量下容易溢出;如果深度够但复位逻辑没做好,缓冲指针初始相位偏了,现象是“链路锁上后数据整体偏移几个样点”。建议在数据阶段加一个简单的溢出计数器,溢出一次就触发ILA,别等频谱不对了再回头找。
4.2 8B/10B解码与加扰:为什么SCR配置必须和ADC端一致
8B/10B解码负责把10位符号还原成8位数据,同时给出rx_valid和rx_char。加扰器负责把长连0或长连1打散,避免频谱上的离散谱线。SCR使能必须和ADC端一致:ADC开了加扰,FPGA没开,解出来的数据看起来像随机数,但链路状态可能是好的;ADC没开,FPGA开了,数据会被错误解扰,递增数据检查会呈现规律性错误。两者都不会让链路直接失锁,所以最容易被忽略。
我一般会在数据阶段加一个“解扰前后对比”的调试寄存器:把原始8B/10B输出和解扰后的数据同时接到ILA,抓一小段递增数据。如果解扰前有规律、解扰后乱,说明SCR配置反了;如果解扰前就乱,问题在字符对齐或Lane映射。这个对比不需要改业务逻辑,只在调试版本里加一个mux,省很多猜谜时间。
5. 调试与排查:ILA抓眼图之前,先用递增数据和错误计数器定位
高速链路调试最怕一上来就看眼图。眼图能告诉你信号质量,但告诉不了你配置对不对、Lane映射对不对、SYSREF相位对不对。更便宜的入口是让ADC进测试模式,输出递增数据,然后在FPGA里做连续递增检查。递增检查通过,说明SPI配置、Lane映射、8B/10B解码、解扰、弹性缓冲这条链基本通了;递增检查不通过,再根据错误位置去抓眼图或调均衡,顺序反过来会浪费大量时间。
5.1 递增数据检查:把配置问题和时序问题分开
下面这段代码做连续递增检查。adc_data是解扰后的样点,prev_data存上一拍,正常情况下当前值应该等于上一值加1。错误计数器按位记录错误类型:连续错、隔点错、随机错,后面看计数器的分布就能反推问题层。
reg [15:0] prev_data; reg [15:0] error_cnt; reg [1:0] error_type; always @(posedge adc_clk or negedge rst_n) begin if (!rst_n) begin prev_data <= 16'd0; error_cnt <= 16'd0; error_type <= 2'd0; end else begin prev_data <= adc_data; if (adc_data != prev_data + 16'd1) begin error_cnt <= error_cnt + 1'b1; // 隔点错:可能Lane映射反了 if (adc_data == prev_data + 16'd2) error_type <= 2'd1; // 随机错:可能时钟抖动或均衡不足 else if (adc_data[15:8] != prev_data[15:8]) error_type <= 2'd2; else error_type <= 2'd3; end end end这段代码的价值在error_type。如果错误集中在“隔点错”,优先查Lane顺序和M值;如果错误是随机分布,优先查参考时钟抖动、自适应均衡和SYSREF相位;如果错误只在温度升高后出现,优先查PCB差分对等长和收发器均衡。递增检查不需要信号源,也不需要频谱仪,上电就能跑,是高速链路的第一道黑匣子。
5.2 常见踩坑记录:从SPI抽风到Lane2走线差300mil
现象:上电后SPI写寄存器,波形全对,回读全错。
原因:上电延迟不足,ADC内部基准还没起来,SPI写操作被忽略。
解决:在SPI状态机最前面加500us计数器,片选空闲保持低电平,写完关键寄存器立即回读,回读不通过就重试三次。
现象:链路能锁,但频谱上每隔几个点出现固定杂散。
原因:Lane映射和物理通道顺序不一致,业务数据交错。
解决:核对IP核的LANE_MAP参数与PCB实际走线,用递增数据检查看错误是否呈周期性;周期性错误优先改映射,不要先调解码。
现象:常温下链路正常,温度升高后偶发失锁。
原因:收发器均衡没开或均衡参数偏保守,差分对走线长度不一致。
解决:开启自适应均衡,扫描SYSREF的IDELAY,取12到18个tap窗口的中心值;用热风枪局部加热ADC,观察误码率变化,定位到具体Lane。
现象:突发大流量时FIFO溢出,频谱偶尔炸一下。
原因:FIFO深度小于2048,后端处理时钟跟不上,满标志跨域晚一拍。
解决:加深FIFO到2048以上,流式接口加背压,满标志用格雷码同步,别把整条跨域路径一把设成false_path。
现象:sync信号跳变时数据错位,重启后有时好有时坏。
原因:SYSREF建立保持时间不够,或者sync拉低后3个多帧周期内没完成字符对齐。
解决:用IDELAY扫描SYSREF相位,ILA同时抓sync、rx_data和lane_align,确认K28.5出现的位置在窗口中心;对齐超时后主动发同步请求,不要死等。
6. 设计取舍:为什么lane延迟补偿要放到最后调
lane延迟补偿放在最后,不是因为它不重要,而是因为它太容易掩盖前面的问题。时钟树没收敛时调lane延迟,调到一个能锁的值,看起来链路通了,温度一变又失锁;SPI没回读时调lane延迟,可能把Lane映射错误临时绕过去,递增数据检查仍然周期性出错;均衡没开时调lane延迟,眼图已经闭合,延迟补偿只是在错误的数据上找了一个勉强能用的采样点。正确的顺序是:先让上电延迟和SPI回读可靠,再让器件时钟、参考时钟、SYSREF的相位关系可预测,然后让同步、对齐、数据三段状态机可观测,最后才动lane延迟和均衡参数。
验证方法也要按这个顺序排。上电先跑SPI回读,再跑递增数据检查,再用ILA抓sync跳变时的K28.5和K28.3位置,最后接信号源看FFT和ENOB。ENOB到11.2bit只能说明当前温度、当前板子、当前配置下链路是通的,不能说明批量一致。批量验证要加温度循环和多次重启,每次重启都强制走一遍上电延迟、回读、递增检查,任何一步不过就锁错误码,别让系统带病进数据阶段。
从那以后我每次上电都强制走一遍“延迟、回读、递增、抓sync”这四步,哪怕前一天刚调通也不例外。高速链路没有后悔药,只有把顺序做对,后面调均衡和lane延迟才不会变成玄学。希望帮到你。
本文还有配套的精品资源,点击获取