AD9680 1G采样率Lane4 JESD204B接收链路调试避坑

发布时间:2026/10/29 8:47:49
AD9680 1G采样率Lane4 JESD204B接收链路调试避坑

简介:面向具备一定FPGA开发经验、关注高速ADC与JESD204B协议的研发人员,这份PDF文档围绕Vivado平台上的AD9680测试程序展开,重点解决1G采样率、Lane4条件下的数据接收与链路调试问题。内容从AD9680寄存器初始化与采样模式配置入手,结合PLL时钟生成方案说明稳定采样时钟的设计要点,并给出基于状态机的JESD204B接收模块实现框架,帮助读者理解从芯片配置到数据接收的完整链路。压缩包内为1个PDF文件,大小约111KB,篇幅紧凑,便于随时查阅。已有135人学习,适合作为高速采集系统开发时的代码参考与排错手册。文中还涉及时钟相位对齐、SPI配置时序、JESD204B链路同步等常见问题,并提供可借鉴的Verilog代码片段和调试思路,能帮助开发者缩短联调周期,并在具体项目中按需调整与优化。

1. 从一次「链路 5 秒后才锁上」说起:1G 采样率 Lane4 接收到底卡在哪

板子刚上电,ILA 里sync一直是低电平,等五秒、十秒,偶尔自己跳高,偶尔永远不跳。这是 AD9680 在 Vivado 下做 1G 采样率、Lane4 JESD204B 接收时最典型的一幕:SPI 回读全0xFF、MMCM 明明LOCKED了但眼图是闭的、SYNC 拉低后进不了 ILAS、rx_valid抖三下才开始出数。用 Verilog 把接收链路搭出来不算难,难的是每一层都有它自己的时序门槛,任何一处差半个周期,现象都会以「没数据」这种信息量最低的方式呈现。下面按报错现象拆成几条线,每条给出根因、能直接抄的代码和约束、以及上板时真正会翻车的位置,适合手里已经有一版能编译、但跑不出波形的工程的人。

2. SPI 配置读不回来:AD9680 寄存器写不进、回读全 0xFF 的排查顺序

AD9680 的绝大多数「没数据」问题,往上追三层都会落到 SPI 上:配置根本没写进去,或者写进去了但写在了错误的时刻。我见过最常见的情况是:工程里 SPI 状态机写得漂漂亮亮,仿真波形完美,上板一读寄存器,0xFF、0x00或者原地不动。这不是协议理解错了,是上电时序和片选电平这两件「手册图上看不出来」的事没做对。

先把排查顺序摆清楚,别一上来就怀疑 JESD。第一步用示波器或 ILA 抓CSB、SCLK、SDIO三根线,确认有没有波形;第二步确认CSB空闲电平是高还是低;第三步确认第一次访问距离上电过了多久;第四步才去核对寄存器值。这个顺序能把 80% 的「配置无效」挡在门外。

2.1 上电时序与 SPI 时钟:手册时序图上没画出来的那段等待

AD9680 内部有电源稳定和带隙建立的过程,上电后立刻发 SPI 命令,芯片会整片不响应,而且这种不响应是随机的——同一块板子冷启动十次,可能有三次正常。这种「玄学」现象最容易骗人,让你以为代码没问题。

我一般的做法是在 SPI 状态机前面串一个纯计数器,把上电等待硬编码成可参数化的延时,复位释放后先数满再放行:

// 上电延时放行:复位释放后等待 PWR_UP_CNT 个时钟周期才允许 SPI 启动 // clk_25m 为 25MHz,40us/tick,12000 tick ≈ 480us,覆盖 500us 量级要求 module pwr_up_gate #( parameter PWR_UP_CNT = 16'd12000 )( input wire clk_25m, input wire rst_n, output reg spi_go // 高电平表示允许 SPI 状态机启动 ); reg [15:0] cnt; reg done; always @(posedge clk_25m or negedge rst_n) begin if (!rst_n) begin cnt <= 16'd0; done <= 1'b0; spi_go <= 1'b0; end else if (!done) begin if (cnt == PWR_UP_CNT - 16'd1) begin cnt <= cnt; done <= 1'b1; spi_go <= 1'b1; // 只拉高一个周期也够,状态机自己锁存 end else begin cnt <= cnt + 16'd1; end end else begin spi_go <= 1'b0; end end endmodule

这段代码的作用不是「延时」,是把「芯片还没准备好」这个不可观测的状态变成一个可观测的使能信号。PWR_UP_CNT按你的输入时钟换算,目标是覆盖数据手册里电源稳定时间那一段,留 20% 余量。done一旦置位就不再回退,避免计数器溢出后二次触发。上板时把spi_go接到 ILA 上,和CSB第一下降沿对比,两者间隔一眼就能看出来够不够。

SPI 时钟本身也有坑。AD9680 的 SCLK 上限按手册一般给到几十 MHz,但那是理想条件;板上走线长、上拉电阻偏大时,实际能用的是 10MHz 到 25MHz。我习惯先用 1MHz 把配置流程跑通,再往上升,每升一档就回读一次关键寄存器。另外 SDIO 是双向的,写的时候 FPGA 驱动、读的时候必须及时把方向切回输入,切晚一个周期就会和 ADC 的输出打架,回读值会呈现「高位对低位错」这种很规律的错误——看到这种规律基本可以直接判定是方向切换时机问题,不是寄存器地址算错。

2.2 寄存器位怎么算:Lane 数量、分频比与采样率模式

这套工程里用到的几个关键寄存器是0x14(采样率模式)、0x15(链路与分频参数)、0x16(Lane 使能)。位定义不能靠猜,要把「我要 4 个 Lane、1G 采样率」翻译成具体的位组合,然后再反推验证一遍。

寄存器目标位段含义本工程取值反推校验
0x14bit41G 采样率模式使能1关掉该位后线速率应减半,ILA 里rx_valid频率同步减半
0x15bit5-3Lane 数量编码,100表示 4 Lane100改成011后 IP 侧L参数必须同步改,否则永久失锁
0x15bit2-0时钟分频比,010表示 2 分频010分频改一档,SERDES 参考时钟跟着变,眼图会立刻塌
0x16bit2-0物理 Lane 使能位图1111只开 3 个 Lane 时,lane_align永远等不到第 4 个字符

写配置的时候别用一堆spi_write平铺,把它做成带索引的配置表,顺序由表决定。JESD 相关寄存器必须在采样时钟链路稳定之后再写,否则芯片内部状态机会停在中间态:

// 配置表驱动:先写通用配置,再写 JESD 相关,最后写使能位 // 每一项 {addr, data, delay_us},delay 用于关键寄存器之间留出内部建立时间 localparam CFG_NUM = 5; reg [7:0] cfg_addr [0:CFG_NUM-1]; reg [7:0] cfg_data [0:CFG_NUM-1]; reg [7:0] cfg_wait [0:CFG_NUM-1]; initial begin cfg_addr[0] = 8'h14; cfg_data[0] = 8'b0001_0000; cfg_wait[0] = 8'd10; // 采样率模式 cfg_addr[1] = 8'h15; cfg_data[1] = 8'b1010_0010; cfg_wait[1] = 8'd10; // 4 Lane + 2 分频 cfg_addr[2] = 8'h16; cfg_data[2] = 8'b0000_1111; cfg_wait[2] = 8'd50; // 使能 4 个物理 Lane cfg_addr[3] = 8'h20; cfg_data[3] = 8'b0000_0001; cfg_wait[3] = 8'd100;// 触发内部校准 cfg_addr[4] = 8'h21; cfg_data[4] = 8'b0000_0001; cfg_wait[4] = 8'd200;// 校准完成前不要发 JESD 使能 end

配置表写法有两个好处:一是顺序可以随时调整而不用改状态机,二是每一项后面的等待时间可以单独调,不用为了某一个慢寄存器把整条流程拖慢。cfg_wait的单位我按微秒级的 tick 来算,具体乘多少取决于你的延时计数器位宽。第 3、4 项那两个「触发校准 / 等校准完成」是我加进去的,目的是让 JESD 使能一定落在内部校准之后——顺序错了,sync就永远不会响应。

2.3 回读校验状态机:把「写成功」从口号变成可观测信号

只写不读,等于在盲调。AD9680 的 SPI 支持读操作,指令字节最高位置 1 表示读,读回一个字节。我的习惯是每写完一组就回读一次,把「写成功」变成 ILA 上能看见的高电平。

// 回读校验:写完立即读回,和期望值逐位比较,不匹配则拉高 err 并锁存 // 指令字节 bit7 = 1 表示读,bit6-0 为地址;读回数据在 SDIO 上移入 always @(posedge clk_25m or negedge rst_n) begin if (!rst_n) begin rd_state <= RD_IDLE; cfg_err <= 1'b0; err_addr <= 8'h00; end else case (rd_state) RD_IDLE: if (wr_done) begin spi_cs_n <= 1'b0; shift_reg <= {1'b1, wr_addr, 8'h00}; // 读指令 + 空字节凑满 16 bit rd_state <= RD_SHIFT; end RD_SHIFT: if (bit_cnt == 4'd15) begin spi_cs_n <= 1'b1; rd_state <= RD_CHECK; end RD_CHECK: begin // 低 8 位是 ADC 返回的数据,高位是移入过程产生的无效位 if (shift_reg[7:0] !== wr_data) begin cfg_err <= 1'b1; // 锁存错误,不自动清除 err_addr <= wr_addr; // 记录出错地址,方便 ILA 触发 end rd_state <= RD_IDLE; end default: rd_state <= RD_IDLE; endcase end

关键点有三个。第一,读指令要凑满 16 个 SCLK 才能把数据完整移出来,缺一个周期会读偏一位,症状是「所有回读值都像期望值左移或右移了一位」,这个特征很好认。第二,cfg_err必须锁存而不是电平跟随,否则错误只闪一个周期,ILA 抓不到;把err_addr一起锁存,触发条件直接设成cfg_err上升沿,一抓一个准。第三,比较用!==而不是!=,因为 SPI 输入在无效期间可能是x,用!=会在仿真里放过不定态。

有一类回读失败很特殊:数值是对的,但只对了一次,第二次就错。这通常是CSB在两次访问之间没有拉高足够长的时间,芯片内部没把这次事务收尾。解决很简单,在每次CSB拉高后插几个空闲周期,别让下一帧紧贴着来。这类问题在仿真里永远不会出现,只有上板才会暴露,属于典型的「不实测就发现不了」的坑。

3. 时钟一歪全盘皆输:MMCM 抖动、相位对齐与 Vivado 时序报错

SPI 通了以后,下一层是时钟。1G 采样率下,采样时钟周期只有 1ns,JESD 参考时钟在几百 MHz 量级,两者又必须严格同源。这一层的问题表现非常统一:MMCM 的LOCKED是高的,但数据就是错的,或者时序分析器一口气报出几十条违例,让你以为整个设计都崩了。实际情况往往只是两三条约束没写对。

排查顺序:先看 MMCM 输出频率和相位是否符合预期(用 ILA 或者时钟监测核,别看代码里的参数),再看约束文件里时钟域关系写没写,最后才看眼图。跳过前两步直接调眼图,等于在没有刻度的尺子上量长度。

3.1 一路时钟喂两家:device clock 与 JESD refclk 的同源约束

AD9680 要一路 device clock,JESD204B 收发器要一路 refclk,这两路必须来自同一个 MMCM,否则相位关系不确定,sysref的建立保持时间根本无从谈起。我用一个 MMCM 出两路,CLKOUT0给 ADC 侧,CLKOUT4给 JESD 参考时钟,VCO 频率按输入 125MHz 乘 8 得到 1GHz,再各自分频:

// MMCM:125MHz 输入 -> VCO 1GHz -> CLKOUT0 500MHz(ADC 侧) / CLKOUT4 250MHz(JESD refclk) // 关键:两路输出必须同源,相位关系才可约束、sysref 才有意义 MMCME2_ADV #( .CLKIN1_PERIOD (8.000), // 125MHz,周期 8ns .CLKFBOUT_MULT_F (8.000), // VCO = 125 * 8 = 1000MHz .DIVCLK_DIVIDE (1), .CLKOUT0_DIVIDE_F (2.000), // 500MHz .CLKOUT0_PHASE (0.000), .CLKOUT4_DIVIDE (4.000), // 250MHz .CLKOUT4_PHASE (0.000), .CLKOUT4_DUTY_CYCLE (0.500), .STARTUP_WAIT ("FALSE") ) u_mmcm ( .CLKIN1 (clk_125m), .CLKFBIN (clk_fb), .CLKFBOUT (clk_fb), .CLKOUT0 (adc_clk), .CLKOUT4 (jesd_refclk), .LOCKED (mmcm_locked), .RST (!rst_n), .PWRDWN (1'b0) );

参数说明:CLKFBOUT_MULT_F决定 VCO,VCO 必须落在器件允许的区间内,太高会失锁、太低抖动大;CLKOUT0_DIVIDE_F和CLKOUT4_DIVIDE分别决定两路输出,两者都是整数分频时相位关系最干净;CLKOUT4_DUTY_CYCLE设成 0.5 是必须的,某些默认值在奇数分频下会给出不均衡占空比,导致 SERDES 采样窗口偏移。

不同源的典型症状是:单板常温下能跑,换个温度或者换块板子就失锁,而且失锁和复位次数有关。原因就是两路时钟各自上电,相位每次都不一样。用同一个 MMCM 之后,这个随机性就消失了。

还有一点容易忽略:LOCKED只是 MMCM 自己说「我稳了」,不代表输出已经稳定到可以给 SERDES 用。我一般在LOCKED之后再等几百个周期才释放 JESD IP 的复位,这段时间里时钟树上的缓冲器和 BUFG 才真正稳定下来。

3.2 时序分析器疯狂报错时,set_clock_groups 与 set_max_delay 该加在哪

1G 采样率工程里,Vivado 报出大量跨时钟域违例是常态,因为 JESD 接收侧的rx_clk是从数据流里恢复出来的,和本地时钟没有固定相位关系。这些路径本来就不该按同步路径分析,必须显式声明为异步。

# 1) ADC 采样时钟:从差分输入端口进来,周期 1ns create_clock -name clk_adc -period 1.000 [get_ports adc_clk_p] # 2) JESD 恢复时钟:由 IP 输出,声明为生成时钟,不要重复 create_clock create_generated_clock -name rx_core_clk \ -source [get_pins u_jesd/txoutclk] \ [get_pins u_jesd/rxoutclk] # 3) 本地时钟域与恢复时钟域之间是异步关系,声明后不再互相分析 set_clock_groups -asynchronous \ -group [get_clocks clk_adc] \ -group [get_clocks rx_core_clk] \ -group [get_clocks clk_125m] # 4) MMCM 输出到 JESD 参考时钟引脚的这段路径,用最大延时单独约束 set_max_delay -from [get_pins u_mmcm/CLKOUT4] \ -to [get_pins u_jesd/refclk] 0.500

顺序不能颠倒:先create_clock再create_generated_clock,再set_clock_groups,最后才是针对具体路径的set_max_delay。-asynchronous的作用是告诉工具这几个域之间不用做建立保持检查,set_max_delay则是只针对参考时钟那一段走线,保证它不会绕远路导致相位漂移。这两个约束的目的完全不同,不能互相替代。

我见过有人图省事直接对rx_clk相关的所有路径写set_false_path,编译是过了,但真实存在的建立违例也被一起屏蔽了,上板表现为「大部分时候正常,偶尔错一个样本」。这种偶发错误最难查,因为 BER 很低,跑一次测试可能一个都不出。正确的做法是:跨域用set_clock_groups,域内该分析的路径一条都别放过。

3.3 眼图闭合:50ps 的偏差怎么被 ILA 抓出来

约束对了,还有一类问题是物理层的。1G 采样率下,采样点偏差超过 50ps 就可能让眼图闭合,而 50ps 在 ILA 的采样精度下几乎是不可见的。我的做法是把相位调整做成可扫描的,而不是一次定死。

具体做法是用 MMCM 的CLKOUT4_PHASE或者 IDELAY 的 tap 值做扫描,每扫一档就跑一次数据自检,把错误计数记下来,最后选错误计数最低的那一档。这个过程不需要改硬件,只改一个参数重新编译,但能把「眼图闭着」这种定性描述变成一条有最小值的曲线。

// 相位扫描控制:phase_sel 从 0 递增,每档停留 SCAN_HOLD 个周期后切换 // 配合错误计数器 err_cnt,扫描结束后取 err_cnt 最小的那一档 always @(posedge clk_125m or negedge rst_n) begin if (!rst_n) begin phase_sel <= 6'd0; hold_cnt <= 16'd0; scan_done <= 1'b0; best_phase <= 6'd0; best_err <= 32'hFFFF_FFFF; end else if (!scan_done) begin if (hold_cnt == SCAN_HOLD) begin hold_cnt <= 16'd0; // 当前档位的错误数优于历史最优则更新 if (err_cnt < best_err) begin best_err <= err_cnt; best_phase <= phase_sel; end if (phase_sel == 6'd63) begin scan_done <= 1'b1; // 扫完 64 档,锁存结果 end else begin phase_sel <= phase_sel + 6'd1; err_cnt <= 32'd0; // 换档清零,避免累计 end end else begin hold_cnt <= hold_cnt + 16'd1; end end end

SCAN_HOLD要足够长,让每一档都跑到上万次采样,否则统计没有意义。err_cnt的统计口径要统一,我一般用「连续递增数据自检」的结果,比 FFT 快得多。best_phase扫完之后直接写回 MMCM 参数或者 IDELAY 控制寄存器,一次编译只扫一轮,效率比手工试高太多。

看到这条曲线有最小值、且最小值附近有一段平坦区,说明眼图是开的;如果整条曲线都很高或者没有明显低谷,问题不在相位,回去查参考时钟质量和 PCB 走线。

4. JESD204B 链路起不来:SYNC 拉低后不进 ILAS、K28.5 抓不到

时钟稳定之后,才轮到 JESD204B 本身。这一层的问题最像「协议玄学」,其实都是有明确判据的:sync拉低后,链路要在若干个多帧周期内完成字符对齐、收到 ILAS、建立弹性缓冲,任何一步超时都会表现为「永远停在同步中」。

排查顺序:先确认参数匹配(L/M/F/S 和线速率),再看字符对齐状态机有没有抓到 K28.5,再看 ILAS 有没有收全,最后看弹性缓冲。每一步都有对应的可观测信号,别用「等一会儿看看」来判断。

4.1 参数对照:L=4 / M=2 / F=2 / S=1 与 5Gbps 线速率怎么对上

JESD204B 的参数不匹配是最常见的「链路起不来」原因,而且报错信息极其吝啬——IP 核一般只给你一个sync不响应,不会告诉你哪个参数错了。

参数本工程取值含义改错后的典型现象
L4物理 Lane 数与 ADC 的0x15位段不一致时,lane_align永远等不齐 4 个字符
M2转换器(虚拟转换器)数改错后帧内样本错位,FFT 上出现固定频率的杂散
F2每帧的字节数(octet)改错后多帧边界算错,ILAS 收不全
S1每帧每转换器的样本数改错后数据速率和采样率对不上,表现为丢点
K32每个多帧的帧数与对端不一致时,多帧同步永远建立不了
线速率5GbpsSERDES 串行速率与 ADC 侧 PLL 分频不一致时,误码率极高或完全无数据
加扰SCR1是否开启加扰两端不一致时,收到的全是看似随机的数据,但对齐信号正常

这张表里最容易搞错的是线速率和SCR。线速率必须和 ADC 侧 SERDES PLL 的分频严格对应,差一点点都不行,因为收发器不会帮你做速率自适应。加扰开关则是一个「看起来正常但数据全错」的陷阱:sync会正常拉高、字符对齐也能完成,但解出来的数据是乱码,检查时如果只看到sync高了就以为通了,会白白浪费几个小时。

我的做法是在 IP 例化时把参数写成localparam,并且和 SPI 配置表里的值用同一个宏定义,从源头消除「两处写得不一样」的可能:

// JESD204B 接收核例化:参数与 SPI 配置表共用同一组宏,避免两边不一致 localparam JESD_L = 4; // 4 Lane localparam JESD_M = 2; // 2 个转换器 localparam JESD_F = 2; // 每帧 2 个 octet localparam JESD_S = 1; // 每帧每转换器 1 个样本 jesd204b_rx_core #( .L (JESD_L), .M (JESD_M), .F (JESD_F), .S (JESD_S), .K (32), .SCRAMBLER_EN (1), // 必须与 ADC 侧 0x15 的加扰位一致 .LINE_RATE (5000), // 单位 Mbps,与 ADC 侧 SERDES PLL 分频对应 .BUFFER_EN (1) // 开弹性缓冲,吸收两端时钟的微小频差 ) u_jesd_rx ( .refclk (jesd_refclk), .rx_clk (rx_core_clk), .sysref (sysref), .sync (jesd_sync), .rx_valid (rx_valid), .rx_data (rx_data) );

BUFFER_EN建议始终开着。弹性缓冲的作用是吸收本地时钟和恢复时钟之间的微小频率偏差,关掉之后短时间内可能正常,跑几分钟就会因为缓冲溢出或读空而出错。

4.2 Lane 对齐状态机:从字符边界到 ctrl_adj_phase

字符对齐是 JESD204B 接收里最需要手写逻辑的部分。IP 核通常只给出rx_valid和rx_data,但字符边界是否对齐、对齐点是否漂移,需要自己判断。

我的做法是在rx_clk域里维护一个小状态机,检测到 K28.5(0xBC,多帧起始对齐字符)之后进入对齐态,之后持续监测rx_valid,一旦出现无效数据就说明对齐点漂了,此时拉起ctrl_adj_phase让 IP 核重新调整相位,并回到同步态重新等 K28.5。

// 字符对齐状态机:K28_5 出现后进入对齐,rx_valid 掉下来则重新调整相位 // ctrl_adj_phase 拉高一拍,通知 IP 核把采样相位挪一个 tap localparam K28_5 = 8'hBC; // 多帧对齐字符 localparam K28_3 = 8'h7C; // 帧对齐字符,调试时可观察其位置判断边界 always @(posedge rx_clk or negedge rst_n) begin if (!rst_n) begin state <= SYNC_INIT; lane_align <= 1'b0; align_counter <= 8'd0; ctrl_adj_phase <= 1'b0; end else begin ctrl_adj_phase <= 1'b0; // 默认清零,只拉高一拍 case (state) SYNC_INIT: begin lane_align <= 1'b0; if (jesd_sync == 1'b0) // sync 拉低表示对端请求同步 state <= WAIT_COMMA; end WAIT_COMMA: begin // 在 4 个 Lane 上都要看到对齐字符,缺一个就继续等 if (rx_char_vld && rx_char == K28_5) begin align_counter <= align_counter + 8'd1; if (align_counter == 8'd3) begin lane_align <= 1'b1; align_counter <= 8'd0; state <= DATA_ALIGN; end end end DATA_ALIGN: begin if (!rx_valid) begin // 出现无效数据,说明对齐点漂了,超过阈值就重新调整 align_counter <= align_counter + 8'd1; if (align_counter > 8'd16) begin ctrl_adj_phase <= 1'b1; align_counter <= 8'd0; state <= SYNC_INIT; end end else begin align_counter <= 8'd0; // 连续有效则清零,避免误触发 end end default: state <= SYNC_INIT; endcase end end

几个参数的意义:align_counter在WAIT_COMMA里数到 3 才置对齐,是为了避免单个误判字符就让状态机跳走;在DATA_ALIGN里数到 16 才重新调整,是为了过滤偶发误码,不然链路会一直在「对齐—重调—对齐」之间震荡。ctrl_adj_phase只拉高一拍,由 IP 核自己完成相位移动,不要一直拉高,否则会持续偏移。

调试这个状态机时,把rx_char接到 ILA 上,观察 K28.3(0x7C)出现的位置,可以反推帧边界是否正确。如果 K28.3 的位置在每次复位后都不一样,说明对齐点不固定,问题多半在参考时钟质量上,而不是状态机逻辑。

4.3 弹性缓冲与 ILAS 超时:rx_valid 抖动为什么要滤波

链路能对齐、能出数据,但数据偶尔错一个,这类问题通常出在弹性缓冲和 ILAS 超时设置上。

弹性缓冲的深度要和两端时钟的频率偏差匹配。偏差越大,需要的缓冲越深;但缓冲越深,延迟越大,sysref的确定性延迟就越难保证。这套工程里 FIFO 深度用到 2048,低于这个值时,在突发大流量下会溢出,表现为「跑一段时间后开始连续出错,复位后恢复」。判断方法很直接:把 FIFO 的almost_full和almost_empty都接到 ILA,看它们有没有同时接近满或空。

ILAS 超时是另一个隐蔽点。IP 核里有个超时计数器,从sync拉低开始计时,超过设定值还没收到完整的 ILAS 就报错重来。这个值设得太小,链路在正常建立过程中就被打断;设得太大,真出问题时你等很久才看到现象。我的经验是设成 3 个多帧周期左右,配合前面状态机的重试逻辑,既能容忍正常的建立时间,又不会让故障状态一直挂着。

rx_valid抖动是必须处理的。IP 核输出的rx_valid在某些边界条件下会短暂掉低一两个周期,如果下游直接拿它当 FIFO 写使能,就会写进无效数据。加一个简单的多数表决滤波:

// rx_valid 三拍多数表决:连续 2 拍以上有效才认为是真的有效 // 单拍掉低被滤掉,避免下游 FIFO 写入无效数据 reg [2:0] vld_sr; always @(posedge rx_clk or negedge rst_n) begin if (!rst_n) vld_sr <= 3'b000; else vld_sr <= {vld_sr[1:0], rx_valid}; end wire rx_valid_flt = (vld_sr[2] & vld_sr[1]) | (vld_sr[2] & vld_sr[0]) | (vld_sr[1] & vld_sr[0]);

这个滤波会引入一拍延迟,如果下游对延迟敏感,可以把三拍改成两拍,代价是抗抖动能力弱一些。滤波之后的rx_valid_flt才能接到 FIFO 的写使能上。

5. 避坑记录:五个让我返工到凌晨的现场问题

这一节是纯血泪经验,每条都按「现象 → 原因 → 解决」写,都是仿真里看不到、只有上板才会暴露的问题。

5.1 冷启动十次有三次配置无效

现象:同一块板子,冷启动十次里大约三次 SPI 完全无响应,回读全是0xFF,重新按一下复位又正常。

原因:SPI 状态机在复位释放后立刻启动,没有等 AD9680 内部电源稳定。芯片的响应是随机的,所以表现为间歇性故障。

解决:在 SPI 前面加上电延时门控(见 2.1 的pwr_up_gate),把等待时间参数化。加完之后同批板子冷启动 50 次,配置失败次数为零。这类问题的教训是:凡是「间歇性」的配置失败,先怀疑时序余量,别先怀疑代码逻辑。

5.2 常温正常,跑十分钟后链路失锁

现象:刚上电一切正常,跑十分钟左右sync掉高,链路失锁,重启后又能跑十分钟。

原因:温度上升后 SERDES 通道的损耗特性变化,固定的均衡参数不再适用,接收端采样点偏离最佳位置。

解决:打开收发器的自适应均衡,让接收端根据实际信道自动调整。同时加一个自动重训练机制:检测到连续错误超过阈值就主动拉低sync重新发起同步,而不是死等。

// 自动重训练:连续错误超过阈值则主动请求重新同步 // RETRY_TH 取 256,太小会被正常误码触发,太大会让故障状态持续过久 localparam RETRY_TH = 16'd256; always @(posedge rx_clk or negedge rst_n) begin if (!rst_n) begin err_cnt <= 16'd0; retrain_req <= 1'b0; end else if (rx_valid_flt && rx_data !== exp_data) begin err_cnt <= err_cnt + 16'd1; if (err_cnt >= RETRY_TH) begin retrain_req <= 1'b1; // 拉高一拍,通知上层复位链路 err_cnt <= 16'd0; end end else begin retrain_req <= 1'b0; if (err_cnt != 16'd0) err_cnt <= err_cnt - 16'd1; // 缓慢衰减,避免长期累积 end end

错误计数用缓慢衰减而不是到点清零,是为了区分「持续误码」和「偶发误码」。偶发误码在衰减机制下永远到不了阈值,持续误码会快速累积,两者行为完全不同。retrain_req只拉高一拍,交给上层状态机去处理复位时序。

5.3 FIFO 深度不够,突发流量下溢出

现象:低速输入时一切正常,一旦输入信号幅度变大或者切换到满速采样,数据开始周期性出错。

原因:FIFO 深度不足,突发流量下写速度短时超过读速度,almost_full拉高后没有背压机制,直接溢出。

解决:把 FIFO 深度从 512 加到 2048,同时把almost_full接到上游的背压逻辑。深度不能无限加,因为会吃掉块 RAM 并且增加延迟,2048 是这套工程里的平衡点。加完之后观察almost_full,正常情况下应该很少拉高,如果经常拉高说明读侧带宽不够,要回去查后级处理模块。

5.4 SYSREF 的建立保持时间没扫,偶发多帧错位

现象:链路能锁,数据大体正确,但每隔几秒出现一次整帧错位,错位后能自己恢复。

原因:sysref相对于帧时钟的相位落在建立保持窗口边缘,温度或者电压微小变化就会让它跨过边界,导致多帧计数器错一拍。

解决:用 IDELAY 对sysref做 tap 扫描,找出稳定的延迟区间。7 系列器件的 IDELAY 在 200MHz 参考时钟下每 tap 约 78ps,扫 0 到 31 一共覆盖约 2.4ns。

// sysref 延迟扫描:每档停留一段时间,统计错误数,选出最优 tap // IDELAYE2 的 CNTVALUEIN 范围 0-31,每 tap 约 78ps IDELAYE2 #( .IDELAY_TYPE ("VAR_LOAD"), .IDELAY_VALUE (5'd0), .REFCLK_FREQUENCY (200.0), .DELAY_SRC ("IDATAIN") ) u_sysref_idelay ( .IDATAIN (sysref_in), .DATAOUT (sysref_dly), .CNTVALUEIN (sysref_tap), .CE (tap_update), .LD (tap_load), .C (clk_200m), .INC (1'b0) );

实测这套板子上 12 到 18 之间的 tap 最稳定,低于 8 或高于 24 时错位概率明显上升。扫描逻辑和 3.3 的相位扫描可以共用一套框架,只是改了被扫描的对象。这里要提醒一句:sysref必须在建链之前就稳定,扫描过程要在正式接收数据之前完成,别一边收数据一边扫。

5.5 JESD 相关寄存器写在时钟稳定之前

现象:SPI 全部回读正确,但sync永远不响应,ILA 上链路停在初始态。

原因:配置表里 JESD 相关寄存器的写入时机早于 MMCMLOCKED,芯片内部状态机在时钟没稳定时就收到了使能命令,停在了中间态。

解决:把配置流程拆成两段,第一段写通用寄存器,第二段等mmcm_locked且稳定若干周期后再写 JESD 相关寄存器。这不是代码风格问题,是芯片内部时序的硬要求。改完之后,同一版代码在几块板子上都能一次建链成功。

6. 快速定位:从「数据全 0」到「ENOB 11.2bit」的排查清单

调这类系统,最耗时间的不是解决问题,是不知道问题在哪一层。我后来固定了一套从下往上的检查顺序,每一层都有明确的通过标准,不通过就不往上走,避免在错误的前提上瞎调。

层级检查项通过标准不通过时先看哪
电源与复位各路电源、复位释放顺序电源纹波在容许范围,复位按时序释放上电时序计数器
SPI回读值、CSB空闲电平、时钟频率全部回读匹配,cfg_err不拉高方向切换时机、上电延时
时钟MMCMLOCKED、输出频率、相位实测频率与配置一致,相位可扫描输入时钟质量、约束文件
字符对齐K28.5 / K28.3 位置、lane_align4 个 Lane 都能对齐,K28.3 位置固定参考时钟、均衡参数
ILAS 与缓冲sync响应、almost_fullsync在 3 个多帧内拉高,缓冲不常满K 值、线速率、FIFO 深度
数据递增自检、FFT、ENOB自检零错误,ENOB 达到设计值相位、sysreftap、走线

递增自检是最省事的验证手段:让 ADC 工作在测试模式输出递增码,FPGA 侧逐样本比较,错一个加一。它比 FFT 快得多,而且能定位到具体是哪个样本出的错。

// 递增数据自检:只在 ADC 测试模式下有效,用于快速区分配置问题和时序问题 // 连续两个样本不满足 +1 关系就计一次错,err_cnt 接 ILA 触发 always @(posedge adc_clk or negedge rst_n) begin if (!rst_n) begin prev_data <= 16'd0; err_cnt <= 32'd0; end else if (test_mode_en) begin if (adc_data !== (prev_data + 16'd1)) err_cnt <= err_cnt + 32'd1; prev_data <= adc_data; end end

如果递增自检零错误,说明配置和时序都没问题,接下来才用信号源加 FFT 看 ENOB。这套工程最后实测 ENOB 到 11.2bit,满足设计预期。如果自检就错了,别去做 FFT,回去查前面几层。

还有几个能明显缩短排查时间的习惯。第一,所有关键状态信号(cfg_err、mmcm_locked、lane_align、almost_full、err_cnt)都接到同一个 ILA,触发条件设成cfg_err或lane_align的下降沿,一次抓取能看到整条链路的状态变化。第二,给每个模块加一条 SystemVerilog 断言,专门盯不定态:

// 断言:rx_valid 有效时数据不能是不定态,否则说明上游有时序问题 assert property (@(posedge rx_clk) disable iff (!rst_n) rx_valid_flt |-> !$isunknown(rx_data)) else $error("ADC data contains X at time %0t", $time);

第三,任何参数改动都只改一处,别让同一个值在 SPI 配置表和 IP 例化里各写一遍。这一点看着像洁癖,实际上是省时间——我吃过一次亏,L参数两边不一样,查了整整一晚上才发现是手写时抄错了一位数字。

从那以后我每次上板前都强制走一遍这张表,从电源到数据一层层打勾,宁可多花二十分钟,也不要在链路失锁的时候靠猜。高速链路里没有孤立的 bug,所有「偶尔」「随机」「重启就好」的背后都是一个没被约束住的时序条件。希望帮到你。

本文还有配套的精品资源,点击获取