AD9680 1GSPS Lane4 JESD204B接收链路调试与实现

发布时间:2026/10/29 8:50:49
AD9680 1GSPS Lane4 JESD204B接收链路调试与实现

简介:面向具备一定FPGA开发基础、需要开发或调试高速数据采集系统的工程师,这份PDF文档围绕AD9680在Vivado平台上的Verilog测试程序展开,重点解决1G采样率、4通道(Lane4)JESD204B数据接收的实现与联调问题。内容按AD9680寄存器初始化、PLL时钟生成、JESD204B接收状态机三条主线组织,并穿插时钟相位对齐、SPI配置时序、链路同步等调试经验与常见问题处理思路。包内共1个PDF文件,约111KB,篇幅紧凑,便于随查随用。已有135人学习。读者可从中获得可参考的Verilog模块框架、寄存器配置与时钟参数设置方法,以及链路建立和排错的具体切入点,适合在具体采集方案中对照调整与验证。

1. 一块 1GSPS 采样板卡的三次失锁:这套 AD9680 测试工程能干什么

上板第一天就跑不起来。频谱上没有信号,链路状态灯一直在闪,sync 拉低又拉高,链路反复失锁。板子上挂着一颗 14 位双通道、1G 采样率的高速 ADC,FPGA 侧用 Vivado 带着 Verilog 代码去收 4 条 JESD204B Lane。这套工程正是为这种场景准备的:把 AD9680 的 SPI 配置、MMCM 时钟树、JESD204B 接收对齐三段链路完整写成可综合的 Verilog,跑通 1G 采样率 Lane4 的数据通路,同时配好 ILA 与数据自检逻辑,方便定位问题出在哪一段。

这套工程没打算做成通用 IP,它更像一份能直接改的参考实现。寄存器序列、约束文件、对齐状态机、上板排查顺序都写在里面。适合已经写过基本 Verilog、正在对接高速 ADC 或 JESD204B 链路的人;也能照着跑通,但需要清楚:这类链路最后能不能通,八成取决于时钟和约束,跟代码写得多漂亮关系不大。

2. 工程骨架拆解:三条主线与 Lane4 的速率账

在 Vivado 里打开这套工程,第一眼能看到的不是代码量,而是结构。高速采集工程的难点从来不在于写多少个模块,而在于三条主线各自的时钟域能不能对得上。一旦把这三条线分开,后面的模块划分、目录组织、约束写法都会变得很固定。

2.1 三条主线:配置面、时钟面、数据面

配置面是慢速、低速、可重试的。它负责通过 SPI 把 AD9680 内部几十个寄存器按顺序写进去:掉电控制、输出模式、JESD204B 参数、Lane 数量、加扰使能。这条线的工作时钟一般是板上 25MHz 或 50MHz 的普通晶振,跟采样时钟没有任何关系,出错了可以复位重来,风险最低但最容易被忽略,因为大多数人第一次上板就卡在这里。

时钟面是整条链路的基础。AD9680 需要一路器件时钟(device clock)才能工作,FPGA 内部的 JESD204B 收发器需要一路参考时钟(refclk),JESD204B IP 核的核时钟(core_clk)又是一路。这三路时钟通常同源,但必须按异步时钟组处理,否则时序分析器会在同源路径上报出大量虚假违例,开发者又不敢直接忽略。

数据面是真正跑高速的部分。4 条 Lane 各自的串行数据进入收发器,8b/10b 解码后拼成帧,再按 M/S/F 参数拆成 I/Q 两路样本,最后落到 AXI-Stream 上,接 FIFO、接自检、接 FFT。这条线的每一个握手信号都跟着 core_clk 走,任何一次跨时钟域处理不当,表现都是“数据看起来对,但每隔几百个样本错一个”。

把这三条线在顶层显式分开,是我在这个工程里最坚持的一件事。顶层只做例化和连线,不写任何逻辑;每条线内部自己管自己的复位、自己的状态机、自己的错误计数。这样调试的时候,看哪个计数器在涨,就知道问题在哪条线上,不至于对着波形图从早看到晚。

2.2 文件清单与目录约定:RTL、XDC、仿真、脚本

工程按功能分目录,不按文件类型平铺。这一点在后期二次开发时差别很大——当工程从“跑通”变成“交付”,你会不断往里加模块,平铺的目录两周就会乱。

目录内容说明
rtl/cfg/ad9680_spi_cfg.v、cfg_rom.vSPI 状态机与寄存器序列表
rtl/clk/clk_gen.v、sysref_gen.vMMCM 例化与 sysref 产生
rtl/jesd/jesd204b_rx_top.v、lane_align_fsm.vIP 核封装与对齐状态机
rtl/data/axis_fifo_wrap.v、sample_check.v数据面缓冲与递增自检
rtl/top/ad9680_top.v顶层,只例化不写逻辑
xdc/ad9680_pin.xdc、ad9680_clk.xdc管脚与时钟约束,分开两份
sim/tb_jesd_align.v、tb_spi_cfg.v两个核心模块的行为级 testbench
tcl/build.tcl、program.tcl一键建工程与下载脚本

管脚约束和时钟约束拆成两份文件,是踩过坑之后改的。管脚约束跟着板子走,时钟约束跟着器件速度等级走,两者变更频率完全不同。混在一个文件里,换一块板子就得整份重写,很容易把时钟约束顺手删掉。

build.tcl里把器件型号、IP 核版本、综合策略都写死,任何人拿到仓库跑一条命令就能重建工程,不用在 GUI 里点十几下。这条习惯在多人协作时价值极高,尤其是在核对“我这边能过、他那边不过”的问题时。

2.3 先把速率账算明白:1G 采样率 × 4 Lane 的线速率

动手写代码之前,先在一张纸上把速率账算清楚。JESD204B 的线速率公式是固定的:

Lane Rate = (M × S × N' × 10/8 × fs) / L

M是参与传输的转换器数量,S是每帧每转换器的样本数,N'是每个样本在链路上占的比特数(14 位样本一般补齐到 16),10/8是 8b/10b 编码开销,fs是采样率,L是 Lane 数量。

工程实际配置是M=1, S=1, N'=16, L=4, fs=1GHz:

1 × 1 × 16 × 1.25 × 1e9 / 4 = 5 Gbps

5Gbps 正好对上工程里LINE_RATE = 5000这组参数。如果两个通道全开(M=2),线速率直接翻倍到 10Gbps,参考时钟、收发器参数、XDC 里的周期约束全都要跟着改,这也是二次开发时第一个要确认的地方。

注意:线速率算错的表现非常隐蔽。IP 核不会报错,只是永远收不到有效的对齐字符,日志里看到的就是 sync 反复拉低。先把 M、L、F、S 和 fs 对一遍,再去看波形。

参考时钟不是随便给的,JESD204B 的子类 1 通常要求refclk = 线速率 / 40:

// 5Gbps / 40 = 125MHz,这就是 MMCM 输出 CLKOUT0 要给的频率 localparam integer LINE_RATE_MBPS = 5000; localparam integer REFCLK_KHZ = LINE_RATE_MBPS * 1000 / 40; // 125000kHz

这条关系写进代码里而不是写在注释里,后面改线速率只要动一个LINE_RATE_MBPS,参考时钟的期望值会跟着变,仿真里可以直接断言实际测到的 refclk 频率对不对。这种“把假设写成可检查的常量”的做法,在高速链路里能省掉一半的排查时间。

顶层例化保持极简,三条线之间只通过少量状态信号连接:

ad9680_top u_top ( .clk_125_p (clk_125_p), // 板载差分晶振 .rst_n (rst_n), .spi_cs_n (ad_spi_cs_n), // 配置面输出 .spi_sclk (ad_spi_sclk), .spi_sdin (ad_spi_sdin), .spi_sdout (ad_spi_sdout), .ad_devclk_p (ad_devclk_p), // 时钟面输出给 ADC .ad_devclk_n (ad_devclk_n), .sysref_p (ad_sysref_p), // 多帧同步 .sync_n (ad_sync_n), // ADC 请求建链 .gt_rx_p (gt_rx_p), // 数据面,4 条 Lane .gt_rx_n (gt_rx_n), .cfg_done (cfg_done), // 配置完成,其他两条线才启动 .link_up (link_up), .err_cnt (err_cnt) );

cfg_done这条线很关键:时钟面一上电就可以跑,但数据面必须等 SPI 配置全部写完、回读校验通过之后才允许启动对齐状态机。少了这个门控,状态机在 ADC 还没进 JESD204B 模式时就开始找 K28.5,必然超时,然后触发重试逻辑,日志里全是噪声。

3. 配置与时钟两条腿:SPI 寄存器序列和 MMCM 输出约束

配置和时钟这两块,单独看都不难,难在它们互相依赖:SPI 写完寄存器,ADC 才会把内部 PLL 锁到指定采样率;采样率定了,线速率才定;线速率定了,refclk 才定;refclk 定了,MMCM 参数才能反推。这个链条是单向的,顺序搞反就会反复返工。

3.1 SPI 为什么手写状态机:时序余量与回读

AD9680 的 SPI 是标准四线制,SCLK、CS_N、SDIO 双向。工程里没有用厂商的 SPI IP 核,而是手写了一个 16 位移位状态机,原因有三个。

第一,AD9680 的 SPI 时序要求SCLK空闲为低电平,很多通用 IP 核默认空闲为高,改配置反而比手写麻烦。第二,写完之后需要立刻把 SDIO 切成输入做回读校验,通用 IP 核的双向切换要额外加三态控制,手写反而清晰。第三,也是最实际的:手写状态机出问题时,每一拍的信号都在自己眼皮底下,用 ILA 一抓就明白;IP 核出问题只能看手册猜。

// 可综合的 SPI 写时序:地址 8 位 + 数据 8 位,MSB first localparam S_PWRUP = 3'd0, // 上电等待 S_CS_LOW = 3'd1, // 片选拉低 S_SHIFT = 3'd2, // 移出 16 位 S_CS_HIGH= 3'd3, // 片选拉高 S_GAP = 3'd4, // 寄存器间间隔 S_READBK = 3'd5, // 回读校验 S_DONE = 3'd6; always @(posedge clk_25m or negedge rst_n) begin if (!rst_n) begin state <= S_PWRUP; bit_cnt <= 5'd0; cfg_done <= 1'b0; spi_sclk <= 1'b0; // 空闲低电平,符合 AD9680 要求 spi_cs_n <= 1'b1; end else begin case (state) S_PWRUP: begin // 上电后必须等够 500us,25MHz 下是 12500 个周期 if (pwr_cnt == 16'd12500) state <= S_CS_LOW; else pwr_cnt <= pwr_cnt + 1'b1; end S_CS_LOW: begin spi_cs_n <= 1'b0; shift_reg <= {rom_addr, rom_data}; // 高位在前 bit_cnt <= 5'd16; state <= S_SHIFT; end S_SHIFT: begin spi_sdin <= shift_reg[15]; shift_reg <= {shift_reg[14:0], 1'b0}; spi_sclk <= ~spi_sclk; // 半周期翻转 if (!spi_sclk) bit_cnt <= bit_cnt - 1'b1; if (bit_cnt == 5'd1 && spi_sclk) state <= S_CS_HIGH; end S_CS_HIGH: begin spi_sclk <= 1'b0; spi_cs_n <= 1'b1; state <= S_GAP; end S_GAP: begin // 寄存器之间留 2us 间隔,避免内部逻辑来不及更新 if (gap_cnt == 8'd50) begin gap_cnt <= 8'd0; state <= S_READBK; end else gap_cnt <= gap_cnt + 1'b1; end S_READBK: begin if (!rb_match && retry_cnt < 3) begin retry_cnt <= retry_cnt + 1'b1; state <= S_CS_LOW; // 回读不一致,重写 end else begin rom_addr <= rom_addr + 1'b1; state <= (rom_addr == LAST_ADDR) ? S_DONE : S_CS_LOW; end end S_DONE: cfg_done <= 1'b1; endcase end end

这段状态机的关键是S_PWRUP和S_READBK两个状态。上电延迟计数器看着多余,实际上不做这件事,ADC 有相当概率完全不响应后续的 SPI 命令,表现是“第一次上电必须按复位才能用”。回读校验则是把“玄学问题”变成“可观测问题”的唯一办法:只要rb_match拉低,retry_cnt就开始涨,涨到 3 还不过就停在S_DONE之前的错误态,ILA 一抓就能定位到是哪一条寄存器写不进去。

S_SHIFT里spi_sclk每拍翻转一次,25MHz 时钟下得到 12.5MHz 的 SCLK,远低于 AD9680 30MHz 的上限,余量足够。想提速就改分频比,但别贴着上限跑,板子温度一高就容易出错。

3.2 寄存器写入序列:0x14 采样率模式与 0x16 Lane 使能

寄存器序列在代码里做成 ROM 表,比一堆if好维护得多:

reg [15:0] cfg_rom [0:15]; initial begin cfg_rom[0] = {8'h00, 8'h3C}; // 软复位,先清干净 cfg_rom[1] = {8'h00, 8'h18}; // 退出掉电模式 cfg_rom[2] = {8'h08, 8'h00}; // 输出接口选 JESD204B cfg_rom[3] = {8'h14, 8'h10}; // 采样率模式 / 时钟分频 cfg_rom[4] = {8'h15, 8'hA3}; // 链路参数组:Lane 数与分频比 cfg_rom[5] = {8'h16, 8'h04}; // Lane 数量 = 4 cfg_rom[6] = {8'h2B, 8'h01}; // JESD204B 链路使能 cfg_rom[7] = {8'h5E, 8'h00}; // 关闭测试图样 end

0x15那个0xA3是最容易写错的一个。bit5~bit3 决定 Lane 数量,写100就是 4 条 Lane;bit2~bit0 决定时钟分频比,写011对应分频后得到 1G 采样率所需的内部时钟。这两个字段挤在一个字节里,改 Lane 数量时很容易顺手把分频比改掉,结果采样率变了,线速率跟着变,refclk 对不上,链路直接不通。

0x16单独留一个寄存器写 Lane 使能,是这块板子上位机工具的习惯做法,实际芯片内部0x15已经带了一部分 Lane 信息。两个寄存器都要写,只写一个的表现是“有时能通有时不能通”,非常折磨人。

提示:每次改寄存器表之后,先把整张表打印出来,对着手册逐条核一遍字段位。这类错误没有仿真能替你发现。

3.3 MMCM 参数反推:从 125MHz 参考时钟到 500MHz 器件时钟

时钟这一块反推的思路是:线速率 → refclk → VCO → 各输出分频。

线速率 5Gbps 对应 refclk 125MHz。板载晶振是 125MHz 差分,正好可以拿来做 MMCM 的输入。VCO 要落在器件允许的范围内,取 1000MHz 比较稳,那么CLKFBOUT_MULT_F = 8.0。

剩下三个输出:

  • CLKOUT0= 1000/8 = 125MHz,给 JESD204B 收发器做 refclk
  • CLKOUT1= 1000/4 = 250MHz,给 IP 核做 core_clk
  • CLKOUT2= 1000/2 = 500MHz,差分输出给 AD9680 做器件时钟,芯片内部再 ×2 得到 1GSPS

三个分频都是整数,没有小数分频带来的相位抖动,这是刻意的选择。312.5MHz 那一档理论上也能凑,但 VCO 要么超范围,要么得用小数分频,两种都不划算。

MMCME2_ADV #( .BANDWIDTH ("OPTIMIZED"), .CLKFBOUT_MULT_F (8.000), // 125MHz x 8 = 1000MHz VCO .DIVCLK_DIVIDE (1), .CLKIN1_PERIOD (8.000), // 125MHz 输入 .CLKOUT0_DIVIDE_F (8.000), // 125MHz refclk .CLKOUT1_DIVIDE (4), // 250MHz core_clk .CLKOUT2_DIVIDE (2), // 500MHz device clock .CLKOUT2_DUTY_CYCLE (0.500), .STARTUP_WAIT ("FALSE") ) u_mmcm ( .CLKIN1 (clk_125_ibuf), .CLKFBIN (clk_fb), .CLKFBOUT (clk_fb), .CLKOUT0 (jesd_refclk), .CLKOUT1 (core_clk), .CLKOUT2 (ad_devclk), .LOCKED (mmcm_locked), .RST (~rst_n), .PWRDWN (1'b0) );

ad_devclk要送出演变为差分,用OBUFDS推出去,注意在引脚约束里把它标成时钟专用管脚,别随便找个普通 IO。走普通 IO 的时候综合能过,上板眼图直接闭合,这类问题查起来非常费劲。

mmcm_locked必须参与复位逻辑:rst_n && mmcm_locked才能释放后续模块的复位。少了这一条,上电瞬间 MMCM 还没锁,core_clk 是抖的,状态机跑飞的概率相当高。

3.4 XDC 里必须手写的那几行

自动生成的约束只管管脚,时钟约束得自己写:

# 输入主时钟 create_clock -name clk_125 -period 8.000 [get_ports clk_125_p] # MMCM 三路输出的生成时钟 create_generated_clock -name jesd_refclk -source [get_pins u_mmcm/CLKIN1] \ -divide_by 8 [get_pins u_mmcm/CLKOUT0] create_generated_clock -name core_clk -source [get_pins u_mmcm/CLKIN1] \ -divide_by 4 [get_pins u_mmcm/CLKOUT1] create_generated_clock -name ad_devclk -source [get_pins u_mmcm/CLKIN1] \ -divide_by 2 [get_pins u_mmcm/CLKOUT2] # refclk 与 core_clk 虽然同源,但对 IP 核内部来说是异步关系 set_clock_groups -asynchronous \ -group [get_clocks jesd_refclk] \ -group [get_clocks core_clk] # 收发器参考时钟路径单独约束,别让工具自己算 set_max_delay -datapath_only \ -from [get_pins u_mmcm/CLKOUT0] \ -to [get_pins u_jesd_rx/gt_refclk] 0.500

set_clock_groups -asynchronous那一段是血泪经验。JESD204B IP 核内部有跨时钟域的逻辑,工具不知道你打算怎么处理,就按同步时钟去分析,然后报出一堆路径违例,周期余量是负的。手写这一句之后违例清零,但要注意:它同时也屏蔽了真正需要检查的路径,所以每加一条异步组,都要确认相关路径上确实有握手或异步 FIFO。

set_max_delay -datapath_only那一行针对的是 refclk 进 IP 核的那一段。这段路径在 IP 核内部没有时序弧,工具算不出来,只能手动给一个上限。0.5ns 是个经验值,实际按板级走线长度调整,走线长了就放宽到 0.8ns。

4. JESD204B 接收链路:IP 核参数、字符对齐与数据面落地

数据面是这套工程真正跑起来的部分。链路能不能建起来,八成看前三章的配置和时钟;能不能稳定收到数据,就看这一章的对齐和数据缓冲。

4.1 IP 核参数怎么填:L/M/F/S 与 SCR

JESD204B 接收 IP 核的参数必须和 ADC 侧的寄存器严格对应,任何一项对不上都表现为“永远等不到对齐字符”。

参数取值说明
L4Lane 数量,对应0x16 = 0x04
M1参与传输的转换器数,单通道模式
F2每帧字节数,2 字节 = 16bit,正好一个样本
S1每帧每转换器样本数
K32每个多帧包含的帧数
SCR1加扰使能,必须与 ADC 端一致
CS0控制位,不额外传输
CF0每帧控制字数
SUBCLASS1使用 sysref 做确定性延迟

F和S相乘要等于每帧承载的字节数,M × S × N' / 8要能被 8 整除,否则帧结构不成立。IP 核在参数化阶段会检查这些,但报错信息往往只提示“参数非法”,不会告诉你是哪一项,所以填之前先在纸上把帧结构画出来。

SUBCLASS选 1 意味着链路依赖 sysref 做多帧对齐。sysref 如果没接,IP 核会一直等,表现是link_up永远不拉高。切到 Subclass 0 可以绕开 sysref,但就失去了确定性延迟,同时多帧对齐也失效,一般不用。

例化时把参数写成localparam分组,别直接塞数字:

localparam integer JESD_L = 4; localparam integer JESD_M = 1; localparam integer JESD_F = 2; localparam integer JESD_S = 1; localparam integer JESD_K = 32; jesd204b_rx_core #( .L (JESD_L), .M (JESD_M), .F (JESD_F), .S (JESD_S), .K (JESD_K), .SCRAMBLER_EN (1'b1), // 必须与 ADC 0x2B 配置一致 .BUFFER_EN (1'b1), // 弹性缓冲,吸收多 Lane 间偏斜 .LANE_MAP ("(0,1,2,3)") // 物理 Lane 到逻辑通道的映射 ) u_jesd_rx ( .refclk (jesd_refclk), .core_clk (core_clk), .sysref (sysref), .rx_serial_p (gt_rx_p), .rx_serial_n (gt_rx_n), .sync_n (adian_sync_n), .m_axis_tdata (jesd_tdata), .m_axis_tvalid (jesd_tvalid), .m_axis_tready (jesd_tready), .link_up (link_up), .lane_align_done(lane_align_done) );

LANE_MAP这行不要随便改。板子上 4 条 Lane 的物理顺序和 PCB 走线顺序不一定一致,写反了以后数据能收到,但 I/Q 是错位的,FFT 出来的频谱看起来像是有信号,一测 ENOB 只有三四位。

4.2 对齐状态机:K28.5 检测与相位调整

IP 核负责 8b/10b 解码和帧解构,但字符边界对齐这件事,工程里自己做了一层状态机做兜底。原因是 IP 核的自动对齐偶尔会锁在错误的位置上,lane_align_done拉高了,数据却是错的。

localparam S_WAIT_SYNC = 2'd0, S_ALIGN = 2'd1, S_DATA = 2'd2, S_RELOCK = 2'd3; always @(posedge core_clk or negedge rst_n) begin if (!rst_n) begin state <= S_WAIT_SYNC; align_cnt <= 8'd0; phase_adj <= 1'b0; end else begin case (state) S_WAIT_SYNC: begin phase_adj <= 1'b0; if (!sync_n) begin // ADC 拉低 sync 请求建链 align_cnt <= 8'd0; state <= S_ALIGN; end end S_ALIGN: begin if (rx_char_k28_5) begin // 收到 K28.5 起始字符 state <= S_DATA; end else if (align_cnt == 8'd255) begin align_cnt <= 8'd0; phase_adj <= ~phase_adj; // 超时,翻转采样相位重试 state <= S_RELOCK; end else begin align_cnt <= align_cnt + 1'b1; end end S_DATA: begin if (!lane_align_done) state <= S_WAIT_SYNC; end S_RELOCK: begin state <= S_ALIGN; // 相位已翻转,重新找 K28.5 end endcase end end

三个细节值得单独说。sync_n拉低之后的 255 拍窗口不是随便定的,JESD204B 要求在若干个多帧周期内完成对齐,超时就该重试而不是死等。phase_adj翻转的是收发器内部采样相位,相当于换一个相位点去找字符边界,比整条链路复位快得多。S_DATA里还要持续监视lane_align_done,一旦丢了就退回S_WAIT_SYNC,这套“自愈”逻辑在温度变化的现场环境里是必需品。

4.3 sysref 的建立保持:IDELAY 扫描 tap

sysref 是 Subclass 1 的确定性延迟基准,它必须满足相对于帧时钟的建立保持时间。这一条在仿真里永远看不出来,只有上板才暴露,而且表现是“跑半小时突然失锁”。

工程里用 IDELAY 对 sysref 做延迟扫描,从 0 到 31 个 tap 逐个试,记录每一档跑多久会失锁:

// sysref 延迟扫描:找到最稳的那一档 tap 值 always @(posedge core_clk or negedge rst_n) begin if (!rst_n) begin tap <= 5'd0; scan_state <= S_SCAN_INIT; end else begin case (scan_state) S_SCAN_INIT: begin idelay_set(tap); // 写入当前 tap run_cnt <= 32'd0; scan_state <= S_SCAN_RUN; end S_SCAN_RUN: begin // 在这一档上跑 100ms,期间失锁次数记到一个表里 if (run_cnt == 32'd10_000_000) begin ber_log[tap] <= err_cnt; err_cnt <= 32'd0; tap <= tap + 5'd1; scan_state <= (tap == 5'd31) ? S_SCAN_DONE : S_SCAN_INIT; end else run_cnt <= run_cnt + 32'd1; end S_SCAN_DONE: idelay_set(best_tap); // 取错误最少的那档 endcase end end

实测下来,有效区间通常只有中间七八个 tap,两端都会失锁。取有效区间正中间那一档,不要取边缘。扫描过程中顺便就验证了 PCB 上 sysref 走线的质量,如果整个 0~31 都跑不通,基本可以判定是走线问题而不是代码问题。

4.4 数据面:AXIS 接 FIFO 与递增自检

数据从 IP 核出来是 AXI-Stream 接口,接一个异步 FIFO 跨到处理时钟域,后面接自检和 FFT。

axis_data_fifo_0 u_adc_fifo ( .s_axis_aresetn (rst_n), .s_axis_aclk (core_clk), .s_axis_tvalid (jesd_tvalid), .s_axis_tready (jesd_tready), .s_axis_tdata ({adc_q, adc_i}), // I/Q 各 16bit 拼成 32bit .m_axis_aclk (proc_clk), .m_axis_tvalid (proc_tvalid), .m_axis_tready (proc_tready), .m_axis_tdata (proc_tdata) );

FIFO 深度不要低于 2048。突发流量下的瞬时速率远高于平均速率,深度不够的表现是偶发丢点,而这种丢点在频谱上看不出异常,只有做递增自检才能发现。

自检逻辑让 ADC 工作在图样模式,输出连续递增的样本,FPGA 侧比对步进:

// ADC 测试模式下的连续数据检查,步进为 2(I/Q 各 +1 拼成 32bit) always @(posedge proc_clk) begin if (proc_tvalid && proc_tready) begin if (proc_tdata != prev_data + 32'd2) err_cnt <= err_cnt + 1'b1; prev_data <= proc_tdata; end end

这个方法比直接看 FFT 快得多。FFT 只能告诉你“有问题”,递增自检能告诉你“第几个样本开始有问题”,配合 ILA 抓取错误样本前后的波形,基本能定位到是配置、时序还是走线。

提示:自检通过之后再切回正常采样模式,不要一上来就接信号源。图样模式是唯一能把“链路问题”和“信号问题”分开的手段。

5. 上板才暴露的五个问题:现象、原因与处理顺序

仿真里全绿、上板全红,是高速链路的常态。下面这五条都是上板之后才暴露出来的,按出现频率排序。

5.1 SPI 写完没反应,第一次上电必须手动复位

现象:工程下载进去,cfg_done永远不拉高,SPI 总线上看不到任何波形,或者有波形但 ADC 完全不理。

原因:AD9680 上电后内部需要一段时间完成初始化,这段时间内对 SPI 命令是无响应的。原代码里没有上电延迟,复位一释放就开始写寄存器,第一条软复位命令直接丢掉,后续所有寄存器的状态都建立在错误的基础上。

解决:在 SPI 状态机最前面加上电计数器,25MHz 时钟下数满 12500 个周期(500us)再开始。实测这一条加上之后,“第一次上电必须复位”的现象彻底消失。计数器数值不要贴着手册下限,留一倍余量。

5.2 时序分析报大量负余量,但功能实际是通的

现象:综合实现能过,时序报告里 JESD 相关路径一片红色,负余量从 -2ns 到 -8ns 不等。上板跑起来功能正常,不敢忽略又不知道从哪下手。

原因:MMCM 输出的 refclk 和 core_clk 同源,工具默认按同步时钟分析,把 IP 核内部本就不需要检查的跨域路径全算进去了。这些路径在设计上就是异步的,靠握手和弹性缓冲保证正确性。

解决:在 XDC 里用set_clock_groups -asynchronous显式声明两组时钟异步,同时给 refclk 进 IP 核的那一段加set_max_delay -datapath_only。加完之后违例清零。但每加一条异步组都要回头确认:这条路径上确实有异步 FIFO 或者握手信号,不是靠“加了就不报了”蒙过去。

5.3 sync 拉低后链路反复失锁,日志里全是重试

现象:sync_n周期性拉低,状态机在S_ALIGN和S_RELOCK之间来回跳,align_cnt每次都数到 255 才超时。

原因:两个可能。一是 K28.5 检测的时机不对,IP 核输出的字符指示信号有延迟,状态机在字符真正有效之前就开始判断;二是加扰配置不一致,ADC 端开了加扰,IP 核没开,收到的数据永远不可能是 K28.5。

解决:先查SCR参数和 ADC 的0x2B寄存器是否一致,这一条占七成。剩下的三成,把rx_char_k28_5打两拍再送进状态机,消除 IP 核输出的组合逻辑毛刺。两拍延迟对 255 拍的窗口来说完全可以接受。

5.4 数据能收到,但每隔几百个样本错一个

现象:递增自检的err_cnt缓慢增长,FFT 频谱底噪抬高,ENOB 只有 8 位左右。波形上看不出任何异常,样本值也是合理的。

原因:弹性缓冲深度不够,或者多 Lane 之间的偏斜没有被完全吸收。4 条 Lane 的走线长度不可能完全等长,PCB 上差 300mil 就对应几十 ps 的偏斜,累积到一定程度就吃掉了缓冲余量。

解决:先把 FIFO 深度从 1024 提到 4096,如果错误间隔变长但没消失,那就是物理偏斜。这时候去查 PCB 上每条 Lane 的差分对走线长度,用热风枪局部加热能加速复现——温度升高时偏斜会变大,错误频率明显上升的那条 Lane 就是长度最不匹配的那条。

5.5 室温下跑得好好的,机箱一热就失锁

现象:实验室常温下连续跑几小时都没事,装进机箱跑四十分钟开始周期性失锁,断电冷却再跑又正常。

原因:sysref 的建立保持时间余量不足。温度升高后器件和走线的延迟特性变化,本来刚好卡在边界上的 sysref 相位就滑出去了。

解决:用 IDELAY 扫描 tap 值,把有效区间找出来取中间值,别用默认值也别用边界值。同时打开收发器的自适应均衡,让接收端在温度变化时自己调均衡参数。这两条一起上之后,同一套硬件在机箱里连续跑几天都没再失锁。

6. 二次开发:把测试工程改成可交付采集链路的改造顺序

这套工程能跑通 1G 采样率 Lane4,但它本质上是个测试程序。真要往产品上走,改造顺序建议按“先改

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