简介:基于Xilinx FPGA的AD7768 Verilog驱动源码,面向高速数据采集系统开发者与FPGA进阶学习者,提供一套已仿真验证、可直接调用的完整控制方案。代码完整覆盖AD7768的上电时序、电源与增益控制、同步输入、工作模式配置以及数据读取链路,顶层模块对使能、同步、增益等关键引脚均给出明确接口,便于按实际硬件裁剪适配。压缩包共72个文件,整体约28.81MB,主要包含Verilog/VHDL驱动源码、XDC约束文件,以及时钟、FIFO、ILA调试等关键IP核配置和工程辅助文件,能够在Vivado中较快搭建工程并开展板级验证。目前已有46人学习下载,适合用于研究AD7768的时序控制逻辑,也可作为各类FPGA数据采集板卡的驱动参考与二次开发基础。
1. 直接读 SDO 之前,先想清楚 AD7768 的 192 位帧
一片 Xilinx FPGA 要同时采集 AD7768 的 8 路 24 位同步采样数据,最隐蔽的坑不是布线也不是时钟,而是把 SDO 当成普通 SPI 从机来读。AD7768 上电后不会主动向 FPGA 送数据,必须先通过 SPI 寄存器配置把内部状态机切换到连续采样,再由 DRDY 下降沿告诉 FPGA“这一帧转换结果已经齐了”。从 DRDY 有效开始,SDO 线上是一个连续排布的 192 位长帧:通道 0 的 24 位、通道 1 的 24 位……一直排到通道 7。若 Verilog 驱动只按 8 位寄存器方式去读,拿到的一定是“第一通道正确、后面全部错位”的结果。这篇把配置、读帧、仿真和 Xilinx 工程里的调用整理成一套可直接落地的驱动思路,适合正在做多路同步采集或想接 Zynq 的工程师。
2. AD7768 的时钟、输出模式和寄存器:写驱动前先定这三个参数
AD7768 的 Verilog 驱动本质上是一个受 DRDY 控制的多字节收发器:读帧本身不难,难的是 MCLK 与 SCLK 的关系、输出模式的选择,以及上电后那条配置链路。这一节先把这三件事定下来,后面代码才不会反复返工。
2.1 AD7768 的 MCLK/SCLK/DRDY:用哪根时钟去对齐 192 位帧
AD7768 的Σ-Δ 调制器由 MCLK 驱动,MCLK 通常由 FPGA 或板载晶振提供,典型值在 24.576 MHz、32.768 MHz,更高可以到百 MHz 量级。经过内部抽取滤波后,最终输出速率 ODR 由 MCLK、MCLK 分频、DCLK 分频和过采样倍数共同决定。写驱动时不必精确计算 ODR,只要保证两点:第一,MCLK 上电后稳定无毛刺;第二,DRDY 的下降沿与 MCLK 之间有固定建立关系,FPGA 内必须把 DRDY 同步两拍,不能拿异步边沿直接驱动状态机。
SCLK 是 FPGA 主动发出的串行时钟,负责把 SDO 上的 8 通道数据逐位移出来。SCLK 与 MCLK 可以异步,AD7768 会把 SDO 的数据切换同步到 SCLK 上。为了降低时序约束难度,我一般让 SCLK 由 FPGA 主时钟分频产生,而不是从 MCLK 分出来;两个时钟域在驱动内部用握手信号交叉,状态机只关心 SCLK 边沿,MCLK 只作为 AD7768 的参考工作时钟。
2.2 SPI、SPI4、TDM 三种输出模式怎么选:单线帧还是四线帧
AD7768 并不是只有一路 SDO。它支持 SPI(单根 SDO 串行输出)、SPI4(四根数据线并行输出)和 TDM(菊花链/时隙复用)三种常见用法。对大多数采集系统来说,单线 SPI 模式最容易实现和仿真,8 通道全速率高采样率输出时 SCLK 受限,才需要切到 SPI4,把一帧拆成四线并行,SCLK 频率可以降四倍。
| 输出模式 | 数据线数量 | 一帧位数 | 适用场景 |
|---|---|---|---|
| SPI | 1 | 8 × 24 = 192 | 中低采样率、逻辑资源紧张时最稳 |
| SPI4 | 4 | 4 × 48 = 192 | 高采样率、SCLK 受限的采集板 |
| TDM | 1~2 | 8 × 24 / 链式扩展 | 多片 ADC 菊花链、通道扩展 |
选模式时要同步检查寄存器里的 SDO 映射配置。单线 SPI 模式下 SDO 按通道号从低到高输出 24 位;SPI4 模式则要把四线并行数据按位拼回 192 位帧。很多人单线仿真通过,切到 SPI4 后数据全乱,就是因为没做四线到帧的位序还原。
2.3 寄存器配置表和 Verilog SPI 写时序:从复位到第一个 DRDY
AD7768 在正常采样前必须写配置寄存器。配置链路与读数据共用 SCLK 和 SDI/SDO,区别只在 CS 拉低时传输方向不同。常见流程是:软复位、等待复位完成、写 MCLK_CONFIG、写 DATA_CONTROL、读回校验,然后等 DRDY。寄存器名和地址以 ADI 手册为准,下面给出工程可用的配置数组框架。
// 配置序列,每个元素 {reg_addr, reg_data} localparam int CFG_LEN = 4; reg [7:0] cfg_addr [0:CFG_LEN-1]; reg [7:0] cfg_data [0:CFG_LEN-1]; initial begin cfg_addr[0] = 8'h00; cfg_data[0] = 8'h80; // DEVICE_CONFIG: 软复位位 cfg_addr[1] = 8'h01; cfg_data[1] = 8'h01; // MCLK_CONFIG: DCLK 分频 cfg_addr[2] = 8'h02; cfg_data[2] = 8'h00; // DATA_CONTROL: 使能8通道 cfg_addr[3] = 8'h03; cfg_data[3] = 8'h04; // SPI_CONFIG: 模式/极性 end这段代码里,地址 0x00 的 DEVICE_CONFIG 控制软复位,0x01 的 MCLK_CONFIG 设置主时钟与 DCLK 分频,具体倍数按你的 MCLK 频率和 ODR 需求换算;0x02 决定通道使能和输出模式;0x03 配置 SPI 时钟极性与帧格式。注意 AD7768 手册上的 bit 定义可能因版本不同有细微差异,换芯片批次时第一步是回读这些寄存器,而不是拿旧工程的常数直接烧。
SPI 写过程中,FPGA 在 SCLK 上升沿把数据打进 AD7768。下面是最简单的移位写法:CS 拉低后按 MSB first 逐位移出 16 位帧,高 8 位是地址,低 8 位是数据。
// spi_write_frame: 串行发送 {config_addr[7:0], config_data[7:0]} reg [15:0] tx_buffer; reg [4:0] tx_bit_cnt; reg spi_cs_n; always @(posedge clk) begin if (write_start) begin tx_buffer <= {config_addr[7:0], config_data[7:0]}; tx_bit_cnt <= 5'd15; spi_cs_n <= 1'b0; end else if (tx_bit_cnt != 5'd31) begin if (sclk_falling) tx_bit_cnt <= tx_bit_cnt - 1'b1; end else begin spi_cs_n <= 1'b1; write_done <= 1'b1; end end assign spi_sdi = tx_buffer[15];这里使用 sclk_falling 使能信号,目的是不在内部生成门控时钟。tx_bit_cnt 从 15 递减到 0,每个 SCLK 下降沿把 tx_buffer 左移一位;计数器减到 0 后继续走到 31,是为了给 CS 释放留时序余量。write_done 作为配置状态机转移条件,防止各寄存器写入互相打架。
2.4 位序和采样沿:先定 MSB first 与 Mode 3,再写状态机
AD7768 的串行帧固定为 MSB first,这一点和多数 SPI ADC 一致。但 SCLK 空闲电平与数据采样沿,不同板卡接法不同,AD7768 的 SPI_CONFIG 里也有对应配置位。驱动代码若写死某个采样沿,换板卡就要改状态机。工程上我按 SCLK 空闲为高、AD7768 在 SCLK 下降沿切换 SDO、FPGA 在上升沿采样来处理,对应 SPI Mode 3。代码里产生 sclk_falling 与 sclk_rising 两个单周期脉冲,一个用于驱动移位,一个用于锁存 SDO。这个习惯统一后,仿真和真实板卡行为基本一致。
3. 可调用的 Verilog 驱动主框架:读 DRDY、收 192 位、出数据和握手
配置链路完成后,驱动要解决的是“一帧长数据怎么不丢不串地收进来”。这一节给出一套可直接放进 Xilinx FPGA 工程的驱动主框架:一个顶层模块加两个内部计数器,全部逻辑跑在同一个 FPGA 主时钟上。
3.1 驱动模块端口设计:哪些信号是输入、哪些是输出
顶层端口要把 AD7768 物理引脚与用户侧数据分开,避免上层逻辑看到 SPI 细节。AD7768 侧暴露 MCLK、CS、SCLK、SDI、SDO、DRDY、复位;用户侧只暴露 data_valid、data_out 和可选的数据请求握手信号。
| 信号名 | 方向 | 说明 |
|---|---|---|
| clk | in | FPGA 主时钟,例如 100 MHz |
| rst_n | in | 异步复位,低有效 |
| adc_mclk | out | 给 AD7768 的 MCLK,约 32.768 MHz |
| spi_cs_n, spi_sclk, spi_sdi | out | 配置与读帧共用的 SPI 三线 |
| spi_sdo | in | AD7768 数据输出,单线模式 |
| drdy_n | in | 转换完成标志,下降沿触发读帧 |
| data_valid | out | 一帧 192 位整理完成后的单周期脉冲 |
| data_out | out | 打包后的 192 位 {ch7,ch6,...,ch0} |
data_out 打包成 192 位看起来宽,但在 Xilinx FPGA 里综合代价不大,反而省掉通道索引仲裁。如果要进 DDR 或 AXI 总线,后续按 32 位切片写入 FIFO 即可。这里不建议把数据压缩成 24 位 × 通道轮询,那会增加握手复杂度。
3.2 主状态机代码:WAIT_DRDY、READ_FRAME、data_valid 三段
以下代码省略了 2.3 的配置阶段,展示的是数据接收主循环。为便于阅读,SCLK 先采用连续二分频,spi_sclk 由 clk_cnt 直接产生。
module ad7768_driver #( parameter integer SCLK_DIV = 2 )( input wire clk, input wire rst_n, output wire adc_mclk, output reg spi_cs_n, output reg spi_sclk, output reg spi_sdi, input wire spi_sdo, input wire drdy_n, output reg data_valid, output reg [191:0] data_out ); localparam IDLE = 2'd0; localparam WAIT_DRDY = 2'd1; localparam READ_FRAME = 2'd2; reg [1:0] state, next_state; reg [7:0] bit_cnt; reg [4:0] clk_cnt; wire sclk_rising, sclk_falling; assign spi_sclk = clk_cnt[0]; assign sclk_rising = (clk_cnt == SCLK_DIV-1); assign sclk_falling = (clk_cnt == SCLK_DIV/2-1); always @(posedge clk or negedge rst_n) begin if (!rst_n) clk_cnt <= 0; else clk_cnt <= clk_cnt + 1'b1; end always @(posedge clk or negedge rst_n) begin if (!rst_n) state <= IDLE; else state <= next_state; end always @(*) begin next_state = state; case (state) IDLE: next_state = WAIT_DRDY; WAIT_DRDY: if (!drdy_n) next_state = READ_FRAME; READ_FRAME: if (bit_cnt == 8'd191) next_state = IDLE; endcase end always @(posedge clk or negedge rst_n) begin if (!rst_n) begin bit_cnt <= 0; data_valid <= 0; end else begin data_valid <= 0; case (state) WAIT_DRDY: bit_cnt <= 0; READ_FRAME: begin if (sclk_rising) begin bit_cnt <= bit_cnt + 1'b1; data_out[191 - bit_cnt] <= spi_sdo; end if (bit_cnt == 8'd191) data_valid <= 1'b1; end endcase end end endmodule状态机在 WAIT_DRDY 里把 bit_cnt 清零;检测到 DRDY 低电平后进入 READ_FRAME,每个 SCLK 上升沿采样一次 SDO,并把数据写到 data_out 对应位。bit_cnt 从 0 数到 191,最后一拍同时完成最低位采样和 data_valid 拉高。data_valid 只持续一个主时钟周期,作为上层 FIFO 的写使能。
3.3 更严格的 SCLK 输出:CS 有效后再启动,空闲保持高电平
上面代码用 clk_cnt[0] 直接输出 SCLK,CS 拉低时 SCLK 相位不一定和 SPI Mode 3 对齐。真实板卡建议把 SCLK 改成寄存器输出,CS 有效后再启动分频,空闲时保持高电平。
reg spi_clk_en; reg [3:0] spi_div_cnt; always @(posedge clk or negedge rst_n) begin if (!rst_n) spi_sclk <= 1'b1; else if (!spi_clk_en) spi_sclk <= 1'b1; else if (spi_div_cnt == SCLK_DIV-1 || spi_div_cnt == SCLK_DIV/2-1) spi_sclk <= ~spi_sclk; end always @(posedge clk or negedge rst_n) begin if (!rst_n) spi_div_cnt <= 0; else if (!spi_clk_en) spi_div_cnt <= 0; else spi_div_cnt <= (spi_div_cnt == SCLK_DIV-1) ? 0 : spi_div_cnt + 1'b1; end这段代码把 spi_clk_en 在进入 READ_FRAME 时拉高,退出时拉低。spi_sclk 是寄存器输出,不是组合逻辑门控,综合后更容易满足时序。上电或复位时 SCLK 停在 1,CS 拉低后的第一个边沿必然是下降沿,与 AD7768 的 Mode 3 输入要求一致。之前 clk_cnt 方案用于功能仿真没问题,上板前应替换成这版。
3.4 24 位符号扩展与多通道输出:data_out 怎么摆最顺手
AD7768 输出 24 位二进制补码,在接入 DSP 或浮点运算前要决定是否做符号扩展。如果后续用 Xilinx DSP48 做累加滤波,把 24 位直接赋给 32 位寄存器,负数会变成很大的正数,这是最容易漏的错误。最简单的方式是把 data_out 拆成 8 个 24 位通道,再按位扩展。
wire [191:0] adc_frame; wire [23:0] ch0 = adc_frame[191:168]; wire [23:0] ch1 = adc_frame[167:144]; wire signed [31:0] ch0_ext = {{8{ch0[23]}}, ch0};符号扩展只做一次,不要把扩展逻辑塞进驱动内部。驱动保持纯位搬运,既方便仿真比对,也方便上层替换字节序。假如板卡 SDO 反接,通道排序会整组颠倒,改这里比改状态机快得多。
4. 用仿真把驱动钉死:行为模型、命令行跑批和三个常见误判
标着“已仿真”的驱动,价值在于验证状态机从 DRDY 开始到 data_valid 结束的 192 拍逻辑是否稳定。对 AD7768 这类带长帧的设备,没必要在仿真里实现 Σ-Δ 调制和抽取滤波器,一个模拟 DRDY 节拍和 SDO 串行输出的行为模型就够了。
4.1 写一个能装进 XSim 的 AD7768 行为模型
模型内部放一个 8×24 的采样阵列,每个 DRDY 周期把数据按 TDM 顺序送到 SDO,CS 拉低后开始移位。
module ad7768_model #( parameter integer MCLK_PERIOD = 32 )( input wire mclk, input wire sclk, input wire cs_n, input wire sdi, output reg sdo, output reg drdy_n ); reg [23:0] sample [0:7]; reg [7:0] bit_ptr; reg [9:0] frame_cnt; initial begin drdy_n = 1'b1; for (int i = 0; i < 8; i = i + 1) sample[i] = 24'h400000 + i; end always @(posedge mclk) begin if (frame_cnt == MCLK_PERIOD-1) begin drdy_n <= 1'b0; frame_cnt <= 0; end else begin drdy_n <= 1'b1; frame_cnt <= frame_cnt + 1'b1; end end integer ch_idx; always @(negedge sclk) begin if (!cs_n) begin ch_idx = bit_ptr / 24; sdo <= sample[ch_idx][23 - (bit_ptr % 24)]; bit_ptr <= (bit_ptr == 8'd191) ? 0 : bit_ptr + 1'b1; end end endmodule模型中 DRDY 每 32 个 MCLK 周期产生一个低脉冲;SDO 在 SCLK 下降沿更新,bit_ptr 按 0 到 191 循环,和驱动状态机一一对应。这个模型刻意不处理寄存器写入,只给驱动接收逻辑一个可预测的数据源。寄存器校验由 testbench 里的 monitor 脚本完成。
4.2 用命令行跑仿真:Vivado XSim 与 17.1 license 问题
拿到驱动后,建议先用命令行仿真,不开 Vivado GUI。写好 testbench 后用 xvlog、xelab、xsim 三条命令完成编译、elaborate 和运行。命令行方式更容易进回归,也能绕过部分版本的 XSim license 问题。比如旧版 Vivado 可能报 17.1 error: failure to obtain a verilog simulation license,这多半是许可证不含仿真组件,换 ModelSim 或 Icarus 跑同一个 testbench 即可,驱动代码不需要改。
xvlog -i ../rtl ../tb/ad7768_tb.sv xelab ad7768_tb -timescale 1ns/1ps xsim ad7768_tb -runalltestbench 至少检查三个点:复位后配置写完成、DRDY 下降沿后 data_valid 能否在一个完整帧后拉高、data_out 的通道 0 与 model 内部 sample[0] 是否一致。把这三个检查写成断言,回归时只看 pass/fail,不用肉眼看波形。
4.3 仿真波形里三个最容易误判的地方
| 现象 | 原因 | 处理 |
|---|---|---|
| 只在第一个 DRDY 后收到一帧 | bit_cnt 没有在 WAIT_DRDY 清零 | 检查状态转移时是否复位计数器 |
| 数据整体左移一位 | 采样沿选错,采到切换前的老值 | 对调采样沿,或检查 SCLK 极性配置 |
| 通道数据交换 | 模型 bit_ptr 或 data_out 位序定义相反 | 单通道赋固定值,逐位定位错位点 |
| data_valid 持续多个周期 | 有效后没清标志 | 在 READ_FRAME 最后周期只拉一拍 data_valid |
不要把仿真通过当成板上一定能跑。仿真模型没有 SDO 高阻态,也没有 I/O 延时。真实板上 DRDY 与 SCLK 距离近时,需要给模型加几十纳秒 delay 再回归一次。多字节收发场景下,先锁定一帧数据正确,再调采样率,是最省时间的排查顺序。
5. 在 Xilinx 工程里调用的三个收尾技巧:约束、ILA 和 AXIS 封装
驱动 RTL 仿真通过后,放到 Xilinx 工程里还要处理时钟约束、在线调试接口和对外握手,否则数据在板上一跑就可能变得“静态仿真全对,上板全是乱码”。最后只挑三个最影响调用效果的收尾点。
5.1 XDC 约束:MCLK 与 SCLK 用 generated clock 锁相
SCLK 和 MCLK 都由 FPGA 主时钟分频产生。如果只约束输入时钟,不声明 generated clock,Vivado 会把 SCLK 路径按组合逻辑路径分析,报出一堆不可达的时序违例。给输出时钟分别建 generated clock 即可:
create_clock -period 10.000 -name sys_clk [get_ports clk] create_generated_clock -name adc_mclk -source [get_ports clk] \ -divide_by 4 [get_pins mmcm_out/CLKOUT0] create_generated_clock -name spi_sclk -source [get_ports clk] \ -divide_by 2 [get_pins sclk_ff/Q]第二条 mmcm_out/CLKOUT0 要替换成实际 MMCM/PLL 的输出引脚名;第三条拿分频寄存器 Q 引脚作为源,是为了让工具理解 spi_sclk 是系统时钟的分频时钟,不是独立时钟域。输出到 AD7768 的 CS、SDI 属于单端同步输出,SCLK 在 25 MHz 以下时 set_output_delay 大概给 2~4 ns 就能收住。ISE 14.7 用户换成 UCF 里的 TIMESPEC 写法,思路一致。
5.2 ILA 在线抓取:不要抓 192 位整帧,抓 data_valid 和关键通道
把 data_out[191:0] 全部接进 ILA 是调试里最容易犯的错。192 位在 256 深度下要占掉多个 BRAM,而且大多数位在出错时根本没参考价值。更实用的做法是只抓 drdy_n、data_valid 和其中一个通道的 24 位数据,采样深度设 1024;确认该通道波形连续且量化台阶正确后,再临时把 data_out 接到 ILA 做一次性全帧 dump。用 MARK_DEBUG 或 set_property 都可以,前端 RTL 不要为调试改业务逻辑。
5.3 把驱动包成 AXIS 接口:给 Zynq 或 DDR 缓存一个干净的入口
驱动输出的 data_valid 是单周期脉冲,直接连 FIFO 没问题,连 AXI-Stream 必须补 ready 握手。常见做法是在驱动后加一个跨时钟 FIFO,写侧用 data_valid 当 wr_en,数据宽度按 24 位;读侧按 AXIS 的 tvalid/tready 输出。上层无论软核还是硬核,拿到的都是一个标准 24 位采样流,不必关心 AD7768 的 192 位帧结构。封装时有一个细节:data_valid 只持续一拍,如果 FIFO 的写延迟导致连续两帧间隔冲突,会丢数据,因此要在写侧加一个简单的 valid/ready 转换状态机,把单周期脉冲变成 FIFO 能接受的写请求,再进 AXIS 通路。
本文还有配套的精品资源,点击获取