☰
AD9680 JESD204B 1G/Lane4调试:SPI配置、失锁与时序约束
2026/10/29 8:47:50 网站建设 项目流程

简介:这是一份面向具备一定FPGA开发经验工程师的AD9680测试程序说明文档,聚焦在Vivado平台上用Verilog实现1G采样率、Lane4配置的JESD204B接收链路。压缩包仅含1个PDF文件,大小约111KB,内容围绕AD9680寄存器初始化、PLL时钟生成与JESD204B接收状态机展开,并给出可参考的代码片段与调试思路。文档结合高速ADC配置、时钟相位对齐、SPI配置时序和链路同步等常见问题,帮助读者理解从芯片配置到数据接收的关键环节,适合用于高速数据采集系统的开发与调试参考。已有135人学习,便于开发者对照具体场景调整参数与优化实现。

1. 链路起来了、数据是乱的:1G 采样率配 Lane4 最容易先出问题的地方

上电、SPI 配置、链路 SYNC 拉高、ILA 里数据在滚动,乍看一切正常,把抓到的样本按帧对下来却是错位的——这是我在 Vivado 里用 Verilog 搭 AD9680 测试工程、跑 1G 采样率 Lane4 的 JESD204B 接收时,最先撞上的那类故障。它不给任何红色报错:综合过、时序收敛、状态机也走进了 RECEIVE,可数据就是不对。高速 ADC 的坑大多长这样,时钟管理、SPI 配置、物理层对齐这三件事里任何一处差一点,表现出来都是同一副面孔。下面按我实际踩过的顺序拆:配置为什么读回 0x00、链路为什么卡在等 K28.5、时序约束为什么越修越乱,最后给一套十分钟内判断故障落在哪一层的方法。

2. 配置写不进去:SPI 读回 0x00、链路不抬头怎么查

AD9680 这颗高速 ADC,SPI 是唯一能让它按你要的模式工作的入口。1G 采样率、Lane4 这些参数全躺在寄存器里,FPGA 端 JESD204B 收端的 L/M/F 只是跟着它走。所以每次链路不抬头,我第一件事不是去翻 JESD IP 的状态机,而是先把 SPI 的写—读回路验一遍。这一步没验通,后面所有调试都是在猜:你可能花半天怀疑收发器参考时钟,实际问题卡在上电那一毫秒里,芯片根本没接你的命令。

这条经验很关键:配置阶段的所有异常,最终都会伪装成链路异常。SYNC 不拉低、收发器一直复位在 JESD 核里,看起来像物理层问题,实际是 ADC 的工作模式没设对。

2.1 读回 0x00 和读回 0xFF 是两类完全不同的故障

同样是"配置没生效",读回值不同,根因差得很远。我一般先做一次全表回读,把结果分四类看:

回读现象常见原因先查什么
全 0x00芯片没响应、上电时序不够、CS 没真正拉低供电与参考时钟是否稳定、上电等待够不够、CS 引脚电平
全 0xFFMISO/SDIO 悬空被上拉、读写标志位没置对、三线模式方向没切指令最高位、SDIO 方向控制、外部上拉电阻
值是随机的SPI 时钟太快、CS 建立/保持时间不足、走线串扰把 SCLK 降到 5MHz 以下再试、示波器看 CS 与 SCLK 边沿
只有某几个寄存器读回正常配置顺序错、地址自增模式被打开、写入被后续复位覆盖软复位后有没有等够、是否开了多字节流模式

这张表用了很多次,几乎每次都能在五分钟内把范围缩到一层。全 0x00 我第一反应永远是"命令根本没进去",而不是"值写错了"。全 0xFF 则基本是电气层面——数据线那头没人驱动,上拉把它拉高了。这两种现象混在一起谈,就会陷入反复改寄存器值的死循环。

有个细节值得单独说:三线模式下 SDIO 是双向复用引脚,方向切换必须在 SCLK 的空闲窗口里完成。切换晚半个周期,读回值就会在 0x00 和正确值之间乱跳,看起来像"偶发配置失败",实际是时序硬伤。

2.2 上电时序与 SPI 帧格式:状态机里必须有的两段

ADI 这类高速 ADC 对上电顺序有明确要求:电源稳定、参考时钟有效之后还要等一段时间,内部状态机才准备好接收 SPI 命令。常见做法是等 500us 到 1ms,我一般直接给足 1ms,宁可慢一点也别赌。上电期间 CS 必须保持高电平,一个时钟都不能漏出去——漏出去的那半个字节,芯片可能就进了个未定义状态,之后怎么配都别扭。

// AD9680 SPI 主机骨架:上电闸门 + 一帧 24bit(指令 + 地址 + 数据) // 器件指令位宽不同时,只改 SHIFT_W 一个参数,状态机不用动 module ad9680_spi #( parameter CLK_HZ = 50_000_000, // 系统时钟频率 parameter SCLK_HZ = 5_000_000, // SPI 时钟,手册上限以下留足余量 parameter PWRUP_US = 1000 // 上电等待,宁长勿短 )( input wire clk, input wire rst_n, input wire start, // 单次传输启动 input wire rw, // 1 = 读,0 = 写 input wire [14:0] addr, input wire [7:0] wdata, output reg [7:0] rdata, output reg busy, output reg cs_n, output reg sclk, output reg sdin, input wire sdout ); localparam integer DIV = CLK_HZ / (2 * SCLK_HZ); // 半个 SCLK 周期的拍数 localparam integer PWR_CNT = (CLK_HZ / 1_000_000) * PWRUP_US; reg [31:0] pwr_cnt; reg ready; // 上电闸门打开标志 reg [23:0] shifter; reg [5:0] bit_cnt; reg [15:0] div_cnt; reg tick; // 1) 上电闸门:ready 之前 CS 恒高,SPI 一根线都不动 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin pwr_cnt <= 32'd0; ready <= 1'b0; end else if (!ready) begin cs_n <= 1'b1; // 上电期间片选必须为高 if (pwr_cnt >= PWR_CNT) ready <= 1'b1; else pwr_cnt <= pwr_cnt + 1'b1; end end // 2) 半周期节拍:SCLK 空闲低,上升沿采样,下降沿换数据 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin div_cnt <= 0; tick <= 1'b0; end else if (div_cnt >= DIV - 1) begin div_cnt <= 0; tick <= 1'b1; end else begin div_cnt <= div_cnt + 1'b1; tick <= 1'b0; end end // 3) 帧收发:CS 拉低后先挂上第一位,再开始打时钟 always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cs_n <= 1'b1; sclk <= 1'b0; sdin <= 1'b0; busy <= 1'b0; bit_cnt <= 0; end else if (ready && start && !busy) begin shifter <= {rw, addr, wdata}; // 24 位一帧 bit_cnt <= 6'd24; busy <= 1'b1; cs_n <= 1'b0; // 拉低后第一位立刻挂上去 sdin <= rw; // 最高位是读写标志 end else if (busy && tick) begin sclk <= ~sclk; if (sclk) begin // 下降沿换下一位 shifter <= {shifter[22:0], 1'b0}; sdin <= shifter[22]; bit_cnt <= bit_cnt - 1'b1; if (bit_cnt == 6'd1) begin // 最后一位走完才抬 CS busy <= 1'b0; cs_n <= 1'b1; end end end end endmodule

这段代码有三处参数值得掰开看。SCLK_HZ是 SPI 时钟,手册给了上限,但实际板子上引线一长、探头一挂,跑到上限附近就容易出错,我一般在 5~10MHz 之间取值。PWRUP_US是上电闸门,这是整段代码里最容易被省掉、也最容易造成随机异常的参数:省掉它,冷启动十次里可能有两三次配置不进去,热复位又一切正常,排查起来特别费劲。bit_cnt决定帧长,指令位宽改成 16 位或者地址扩到 16 位时,这里和shifter的宽度要一起改,只改一处就会错位半帧——错位半帧的表现恰恰是"读回值看着像个合法值但就是不生效",最难发现问题。

tick那一段单独抽出来是有原因的:把时钟分频和状态机混在一个 always 块里,改分频比的时候极容易连带改坏移位逻辑。分开之后,调 SPI 速率只需要动SCLK_HZ。

提示:SPI 调试阶段把 CS、SCLK、SDIO 三根线引到 ILA 上,抓一次完整帧。字符长度对不上、CS 抬起早半个周期这类问题,一眼就能看出来,比对着代码数拍子快得多。

2.3 配置顺序与回读校验:把寄存器表当成数据,而不是代码

寄存器配置最常见的做法是把地址和值硬编码进 case 分支,写一长串spi_write(8'h16, 8'b0000_0100)。刚开始能跑,改一版参数就要翻遍整个文件,而且很容易出现"改了值忘了改顺序"。我后来统一改成表驱动:地址和值放两个数组,状态机只管按索引往下走。

// 配置表:数组顺序就是执行顺序,改值不动流程 localparam NR = 6; reg [14:0] cfg_a [0:NR-1]; reg [7:0] cfg_d [0:NR-1]; initial begin cfg_a[0] = 15'h0000; cfg_d[0] = 8'h3C; // 软复位;写完至少等 5ms 再发下一条 cfg_a[1] = 15'h0010; cfg_d[1] = 8'h0B; // 时钟/分频相关,示例值 cfg_a[2] = 15'h0016; cfg_d[2] = 8'h04; // Lane 数配置,示例:4 Lane cfg_a[3] = 15'h0014; cfg_d[3] = 8'h10; // 采样率模式,示例值 cfg_a[4] = 15'h005E; cfg_d[4] = 8'h01; // 加扰开关,必须和接收端一致 cfg_a[5] = 15'h000F; cfg_d[5] = 8'h01; // 触发更新,示例 end // 回读比对:不等就停在这一条,绝不带病往下配 always @(posedge clk) begin if (rd_done) begin if (rdata != cfg_d[cur_idx]) begin cfg_err <= 1'b1; // ILA 抓 cfg_err + cur_idx,直接看到卡在哪条 fail_idx <= cur_idx; state <= S_HALT; // 停下来,别继续污染后面的寄存器 end else begin cur_idx <= cur_idx + 1'b1; state <= (cur_idx == NR-1) ? S_DONE : S_WR; end end end

上面表里的地址和值是结构和顺序的示例,不是可以直接抄的数值,具体每个字段的含义要对着你手上那颗器件版本的手册核。真正值得抄的是三件事:软复位必须排第一且后面留足等待、加扰开关这类跨芯片参数必须两端一致、每写一条立刻回读比对。

回读比对这一步,很多人只在最后做一次全表校验。我吃过亏:某条 JESD 相关寄存器被后面的写操作覆盖了,最后校验时顺序已经错乱,读回来的值看着都对,链路就是起不来。改成逐条比对之后,fail_idx直接把问题指到具体某一条,省掉的排查时间以小时计。

还有一点:配置完成后不要立刻认为 ADC 已经进入工作状态。内部 PLL 锁定、JESD 链路起来都需要时间,稳妥的做法是拉一个配置完成标志,等 ADC 的链路就绪指示(或者收端 SYNC 电平变化)之后再放数据通路,否则会看到"上电瞬间数据全是 0,过了一会儿自己好了"这类假故障,误以为是时序问题。

3. JESD204B 链路失锁:SYNC 拉低之后收不到 K28.5 的排查顺序

配置验通了,接下来才是真正耗时间的部分。1G 采样率、Lane4 的 JESD204B 链路,一旦失锁,表现出来往往是同一个现象:sync拉低、收发器在跑、rx_valid偶尔抖两下、对齐状态机在 IDLE 和 WAIT 之间反复横跳。这时候如果直接去调 lane 延时补偿,大概率会越调越乱。我踩过三次同一个坑——顺序错了。

3.1 先算清线速率和 L/M/F/S:参数不自洽,后面全是白忙

JESD204B 的参数表有硬约束,几个数之间必须自洽:

单 Lane 线速率 = (M × N' × 10/8 × fs) / L 每帧字节数 F = (M × S × N') / (8 × L)

拿单通道 1GSPS、14 位量化(N' 取 16,含控制位)、L=4 来算:(1 × 16 × 1.25 × 1G) / 4 = 5Gbps,这正是工程里那个 5000Mbps 的来处。同一个采样率下如果 M 变成 2(双通道同时开),线速率直接翻到 10Gbps,收发器参考时钟和通道能力都得跟着改,硬套原参数是起不来的。

再看 F:F = (M × S × N') / (8 × L)。M=1、N'=16、L=4 时算出 0.5,小于 1,说明这组参数本身不成立,得换组合;M=2、N'=16、L=4 时 F=1。所以当你看到某个工程里写着 L=4、M=2、F=2,先别急着抄——按公式它对应的 L 应该是 2,或者 N' 是 32。参数不自洽时,接收核会把 SYNC 拉低然后一直等,等多久都不会有数据。

参数含义常见错误
LLane 数与物理通道映射LANE_MAP不一致
M转换器数双通道只填了 1,线速率算出来差一倍
F每帧字节数没按公式校验,与 ADC 端不一致
K每多帧的帧数两端不同,表现为能对齐但周期性地掉
SCR加扰开关一端开一端关,链路永远起不来

这张表我贴在工位上,每次改参数先对一遍。K 这一项特别容易漏:两端 K 值不同时,字符对齐能过、ILAS 也能过,但每隔几个多帧就失锁一次,现象是"能出数,几秒钟断一次",很容易误判成硬件接触不良。

3.2 SYNC 到 K28.5:对齐窗口里到底该看哪几个信号

sync拉低意味着接收端在请求同步,接下来发送端会连发逗号字符(K28.5),接收端找到它才能确定字节边界。这个窗口是有时间限制的,超时就要重新发起同步请求。调试时我只看三个东西:sync的下降沿、第一个 K 字符出现的位置、以及rx_valid在这段时间里是否连续。三个一起看,能立刻区分是"根本没收到字符"还是"收到了但边界定错"。

// 字符对齐:SYNC 拉低 -> 等 K28.5 -> 连续确认 -> 进 ILAS // K28.5 在 8B/10B 解码输出上通常表现为 8'hBC 或 8'h9C, // 取决于极性和 RD 约定,先用 ILA 抓一次确认,别硬抄 localparam [2:0] A_IDLE = 3'd0, A_WAIT_K = 3'd1, A_ALIGN = 3'd2, A_ILAS = 3'd3, A_DATA = 3'd4; always @(posedge rx_clk or negedge rst_n) begin if (!rst_n) begin state <= A_IDLE; wait_cnt <= 0; lock_cnt <= 0; err_cnt <= 0; end else case (state) A_IDLE : if (!sync_n) begin // 收端请求同步 wait_cnt <= 0; state <= A_WAIT_K; end A_WAIT_K: if (rx_is_k && rx_data == 8'hBC) begin lock_cnt <= 0; state <= A_ALIGN; // 找到逗号,开始确认边界 end else if (wait_cnt >= MF_WINDOW) begin err_cnt <= err_cnt + 1'b1; // 窗口内没等到,记一次失锁 state <= A_IDLE; // 回 IDLE 重新发同步请求 end else wait_cnt <= wait_cnt + 1'b1; A_ALIGN : if (rx_is_k && rx_data == 8'hBC) begin lock_cnt <= lock_cnt + 1'b1; if (lock_cnt >= 3) state <= A_ILAS; // 连续 3 次才算稳 end else begin lock_cnt <= 0; state <= A_WAIT_K; end A_ILAS : if (ilas_done) state <= A_DATA; A_DATA : if (sync_n) state <= A_IDLE; // 掉锁立刻回退,别硬撑 endcase end

三个参数决定这段代码的脾气。MF_WINDOW是等待窗口,填太小,稍微有点延迟就误判失锁,链路反复重训;填太大,真的失锁时要等很久才回退,恢复慢。我一般按多帧周期的 3 倍取值。lock_cnt >= 3是确认次数,只认一次逗号就进 ILAS,rx_valid一抖动就会误判对齐成功,后面数据整段错位。A_DATA里那个if (sync_n)是一道保护:运行中一旦掉锁,立刻回退到 IDLE,而不是继续往 FIFO 里灌无效数据——这个保护救过我一次,否则下游缓存里混进的一段坏数据,很容易被当成"算法问题"。

3.3 能锁上但隔一会儿掉一次:弹性缓冲与 SYSREF 的边界

链路能起来、跑几分钟掉一次,这类故障最磨人。我遇到过三种成因,都跟"边界"有关。

第一种是弹性缓冲没开或者深度不合适。收发两端时钟同源但存在相位漂移,缓冲把这点漂移吸收掉;缓冲关掉时,漂移积累到一定程度就是一次掉锁。这种情况开缓冲、给够余量就能压住。

第二种是 SYSREF 的建立保持时间不够。SYSREF 是用来确定多帧边界的,它相对帧时钟的相位如果正好落在跳变沿附近,采到的边界每次都可能不一样,表现出来就是"锁上之后多帧计数随机偏移"。常见做法是用 IDELAY 微调 SYSREF 相位,扫一遍找稳定窗口,实测落在 12~18 个 tap 之间最稳(不同板子不一样,必须自己扫)。

// SYSREF 相位扫描:掉锁计数最小的那个 tap 就是稳定窗口中心 always @(posedge clk) begin if (scan_en) begin idelay_set(sysref_tap); // 设置当前 tap if (lock_stable_cnt >= STABLE_TH) begin best_tap <= sysref_tap; // 记下稳定窗口的第一个点 scan_en <= 1'b0; end else if (sysref_tap >= 31) begin sysref_tap <= 0; // 扫完一轮还没稳,回头查时钟本身 scan_en <= 1'b0; scan_fail <= 1'b1; end else sysref_tap <= sysref_tap + 1'b1; end end

STABLE_TH是连续稳定计数阈值,设小了容易把偶发的假稳定当成真窗口,我一般要求连续几百个多帧不掉锁才算数。scan_fail这个标志很关键:扫完 32 个 tap 都找不到稳定点,说明问题不在 SYSREF 相位,而在时钟质量或者走线匹配上,继续扫下去是浪费时间。

第三种是时钟质量本身不行。参考时钟抖动大、MMCM 输出相位不对,眼图张不开。这种情况软件层面怎么调都是治标,得回到时钟树和 PCB 去看。

注意:调 lane 延时补偿和 SYSREF 相位这两件事,一定要放在时钟树完全稳定之后。时钟还在飘的时候调对齐参数,调出来的值今天能用明天就废,白折腾。

4. 时序约束修不动:MMCM 输出到 JESD 参考时钟那段路径怎么处理

综合过了、实现过了,一看时序报告,红的全在 MMCM 输出到 JESD 接收核参考时钟这段路径上。Vivado 会老老实实给你报一堆路径延迟违例,越修越多。这类报告看着吓人,本质上大多数不是真违例,而是工具不知道那两个时钟根上是什么关系。

4.1 set_max_delay 到底约束了什么

MMCM 的 CLKOUT4 送到 JESD 核的 refclk 输入端,这条路径上可能经过 BUFG、时钟缓冲,工具按普通数据路径去算延迟,算出来当然不满足。实际上这里只要求相位关系确定,不要求逐拍对齐,用set_max_delay给它一个上限就行。

# MMCM 输出到 JESD 核参考时钟:只要求相位关系确定,用上限约束兜住 set_max_delay -from [get_pins u_mmcm/CLKOUT4] \ -to [get_pins u_jesd_rx/inst/refclk] 0.500 # 同一条路径的 hold 侧一起给,避免只修 setup 之后又被 hold 报回来 set_min_delay -from [get_pins u_mmcm/CLKOUT4] \ -to [get_pins u_jesd_rx/inst/refclk] 0.000

0.500这个数不是随便写的:它要小于一个参考时钟周期,又要大于实际走线的物理延迟,1G 采样率下参考时钟周期通常在两纳秒上下,给 0.5ns 留了足够余量。给太大等于没约束,给太小实现会拼命插缓冲,反而把时钟质量搞坏。

4.2 哪些时钟是真异步:set_clock_groups 和 set_false_path 的边界

set_false_path是双刃剑。用对了,它把工具从无意义的跨域分析里解放出来;用错了,它把真正需要检查的路径也一起关掉了,最后板子上跑出一堆偶发错误,回头查时序报告全是绿的。

我给自己定了条规矩:只有物理上不可能同时有效的时钟对,才用set_clock_groups -asynchronous;单条路径用set_false_path时,必须在旁边写清楚为什么。

# ADC 采样时钟域与 JESD 核时钟域:确认同源、相位关系由 MMCM 固定之后才声明异步 set_clock_groups -asynchronous \ -group [get_clocks -include_generated_clocks clk_adc_500m] \ -group [get_clocks -include_generated_clocks clk_jesd_core] # 收端恢复时钟:由收发器内部产生,不参与跨域分析 set_false <p> <a href="https://download.csdn.net/download/cdDmuzHUD/90543808" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询