AD7768多通道同步采样ADC的Verilog驱动设计与FPGA实现 简介基于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 频率可以降四倍。输出模式数据线数量一帧位数适用场景SPI18 × 24 192中低采样率、逻辑资源紧张时最稳SPI444 × 48 192高采样率、SCLK 受限的采集板TDM128 × 24 / 链式扩展多片 ADC 菊花链、通道扩展选模式时要同步检查寄存器里的 SDO 映射配置。单线 SPI 模式下 SDO 按通道号从低到高输出 24 位SPI4 模式则要把四线并行数据按位拼回 192 位帧。很多人单线仿真通过切到 SPI4 后数据全乱就是因为没做四线到帧的位序还原。2.3 寄存器配置表和 Verilog SPI 写时序从复位到第一个 DRDYAD7768 在正常采样前必须写配置寄存器。配置链路与读数据共用 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] 8h00; cfg_data[0] 8h80; // DEVICE_CONFIG: 软复位位 cfg_addr[1] 8h01; cfg_data[1] 8h01; // MCLK_CONFIG: DCLK 分频 cfg_addr[2] 8h02; cfg_data[2] 8h00; // DATA_CONTROL: 使能8通道 cfg_addr[3] 8h03; cfg_data[3] 8h04; // 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 5d15; spi_cs_n 1b0; end else if (tx_bit_cnt ! 5d31) begin if (sclk_falling) tx_bit_cnt tx_bit_cnt - 1b1; end else begin spi_cs_n 1b1; write_done 1b1; 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 和可选的数据请求握手信号。信号名方向说明clkinFPGA 主时钟例如 100 MHzrst_nin异步复位低有效adc_mclkout给 AD7768 的 MCLK约 32.768 MHzspi_cs_n, spi_sclk, spi_sdiout配置与读帧共用的 SPI 三线spi_sdoinAD7768 数据输出单线模式drdy_nin转换完成标志下降沿触发读帧data_validout一帧 192 位整理完成后的单周期脉冲data_outout打包后的 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 2d0; localparam WAIT_DRDY 2d1; localparam READ_FRAME 2d2; 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 1b1; 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 8d191) 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 1b1; data_out[191 - bit_cnt] spi_sdo; end if (bit_cnt 8d191) data_valid 1b1; 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] 直接输出 SCLKCS 拉低时 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 1b1; else if (!spi_clk_en) spi_sclk 1b1; 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 1b1; end这段代码把 spi_clk_en 在进入 READ_FRAME 时拉高退出时拉低。spi_sclk 是寄存器输出不是组合逻辑门控综合后更容易满足时序。上电或复位时 SCLK 停在 1CS 拉低后的第一个边沿必然是下降沿与 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 顺序送到 SDOCS 拉低后开始移位。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 1b1; for (int i 0; i 8; i i 1) sample[i] 24h400000 i; end always (posedge mclk) begin if (frame_cnt MCLK_PERIOD-1) begin drdy_n 1b0; frame_cnt 0; end else begin drdy_n 1b1; frame_cnt frame_cnt 1b1; 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 8d191) ? 0 : bit_ptr 1b1; 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 clockVivado 会把 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 大概给 24 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 通路。本文还有配套的精品资源点击获取