
简介面向FPGA开发者的AD7928驱动设计资料聚焦十六位逐次逼近型ADC与Verilog硬件描述语言的结合解决数据采集系统中ADC时序控制、转换结果读取与FPGA逻辑实现的工程问题适合有Verilog基础的中级开发者参考。AD7928具备高速采样与低噪声特性适用于通信、测试和测量等场景。压缩包共有二百五十七个文件大小约十五兆主要包含仿真波形、工程配置、编译报告及Verilog源程序等覆盖逻辑设计、时序仿真到工程构建的完整环节其中多个配置文件与报告便于排查时序问题。已有六百二十八人学习下载。资源内附说明文档并包含引脚分配、编译报告等辅助内容可帮助理解AD7928的工作时序与状态机设计思路同时为基于Quartus的FPGA驱动开发提供可直接参考的工程模板。1. 一块12位ADC的FPGA驱动为什么值得抠时序参数SPI接口的ADC看起来只是片选、时钟、串行数据三根线的事但真正做过连续采集的人都知道AD7928这类8通道12位逐次逼近型ADC帧格式里控制字的流水线延迟、SCK与CS之间的建立保持时间、以及帧结束后的静默间隔任何一个环节没卡准采集结果就会出现整片错位或首帧丢数据。这个资源包里不是零散代码片段而是带Quartus映射数据库、块符号文件和Verilog备份源文件的完整工程参考。对想用FPGA驱动AD7928读取数据的工程师可以直接拆出驱动骨架改到自己的模块对FPGA入门的人来说价值在于把数据手册的时序图翻译成了看得懂的状态机和分频计数器。下面从帧格式开始把驱动实现的关键点逐层拆开。2. AD7928帧结构与时序参数把数据手册翻译成状态机2.1 帧宽决定状态机的边界AD7928是8通道12位逐次逼近型ADC最大采样率1 MSPS接口是四线SPI。第一次接触的人最容易在“12位ADC”和“16位串行帧”之间犯迷糊数据手册标注明明是12位分辨率为什么串行传输经常按16位帧操作原因是AD7928支持12位和16位两种帧宽16位帧内前4个SCK周期从SDO送出的是上一帧的通道残留标志之后12位才是本次转换的有效数据。12位帧没有前4位开销单帧传输时间更短但控制字写入时的时序裕量也变窄。我自己做驱动时固定用16位帧写入控制字、切换通道、读取结果都走同一条状态机路径代码里不需要对帧长做条件分支付出的是每帧多4个SCK周期的代价。帧宽参数放在顶层定义器件更新批次时方便调整。控制字在SCK下降沿被ADC采样AD7928按固定位顺序解析写标志、序列器标志、保留位、通道地址、电源模式。按顺序把“通道3”“普通模式”写成16位常量就是下面这段// 控制字构造16位帧按数据手册控制寄存器位序排列 // WRITE1SEQ0DONTC0CH_ADD3b011 // PM2b00其余位补0 localparam [15:0] CTRL_WR_CH3 16h1380;这个常量的用途很简单每次发起帧传输前把目标通道编码填进CH_ADD字段其余位保持不变作为SDI线上的串行数据逐位移出即可。需要提醒的是不同帧宽下控制字的有效位段位置不完全一致代码注释里标注了位序来源换用其他批次芯片时先回数据手册核对这张位表。2.2 四个写进状态机的时序参数AD7928数据手册中与FPGA驱动直接相关的四个时间参数如下参数符号最小值对驱动的影响CS到首SCK建立时间t120nsCS拉低后不能立即出现SCK边沿SCLK半周期tHIGH/tLOW25ns决定了系统时钟分频的下限末SCK到CS上升沿t1220ns帧末CS不能与SCK同时翻转帧间静默时间tQUIET50ns连续转换帧之间必须等待这四组参数里t1和t12最容易被忽略。网上很多SPI读取程序CS拉低的同时SCK就开始翻转帧结束时CS又立刻拉高这种写法对慢速时钟可能碰运气能跑但系统时钟一旦提高分频后的SCK半周期接近25ns时建立与保持时间立刻不够ADC回读的数据会出现随机毛刺。稳妥的状态机至少要有五个状态IDLE、CS_SETUP、FRAME、CS_HOLD、QUIET。CS_SETUP和CS_HOLD用于显式消耗t1和t12QUIET用于等待tQUIET五个状态串成环形任何一次转换启动都完整走一遍。多说一句tQUIET。这个时间不是纯粹的电平要求而是留给AD7928内部SAR转换完成的窗口——CS上升沿之后芯片才开始真正做电压比较和位逼近。连续采集时如果帧间隔压得太短上一次转换没有完成就启动下一次结果会带上上一通道的残余电荷。最典型的表现是单通道采集完全正常、8通道轮询时通道间串扰明显。所以帧间隔宁可多算几个时钟周期也不要顶着50ns的最小值跑。2.3 上电初始化序列与连续采集首次上电时AD7928内部寄存器处于随机状态推荐做法是先写入一次控制字把通道与电源模式初始化然后再进入正式采集。初始化的方式就是向SDI发送一帧16位数据与普通转换帧一样区别只在控制字的WRITE字段置1。初始化完成后每次帧传输等价于一次读取上层把要采集的通道号写到adc_channel然后给adc_start一个高脉冲等待adc_done回读数据。连续采集时把adc_done接回一个计数器让计数器自动产生下一个adc_start就能实现对同一通道的定频采样。这种结构下驱动模块本身不需要知道外部采样的频率频率控制全交给上层的触发逻辑整体耦合非常干净。3. Verilog驱动实现与仿真状态机落到可综合代码3.1 参数化模块接口驱动模块按通用SPI主机的思路设计下面是接口声明也是后续做引脚分配时的信号清单module ad7928_driver #( parameter SYS_CLK_HZ 50_000_000, // 系统时钟如50MHz parameter SCLK_HZ 5_000_000 // ADC串行时钟上限20MHz )( input wire clk, // FPGA主时钟 input wire rst_n, // 异步复位低有效 input wire adc_start, // 外部给一个周期的启动脉冲 input wire [2:0] adc_channel, // 本次帧的通道地址 output reg [11:0] adc_data, // 12位转换结果 output reg adc_done, // 完成标志一个周期高脉冲 output reg sclk_o, // 到AD7928SCLK output reg cs_n_o, // 到AD7928片选低有效 output reg sdi_o, // 到AD7928DIN控制字 input wire sdo_i // AD7928来的DOUT数据 );接口上只暴露四根与ADC直接相连的信号其余全部封装在模块内部。SCLK_HZ建议先按5MHz起步跑通以后再往上提这样可以排除“SCK过高导致时序不收敛”的干扰因素。adc_start由外部计数器、软核或者调试用VIO核产生均可驱动模块不关心它的来源只负责在脉冲到来后完成一帧操作。3.2 状态机代码骨架五个状态的定义和主要转移逻辑如下localparam IDLE 3d0; localparam CS_SETUP 3d1; localparam FRAME 3d2; localparam CS_HOLD 3d3; localparam QUIET 3d4; reg [3:0] state; reg [4:0] bit_cnt; // 当前移位的位数0~15 reg [31:0] div_cnt; // 分频用计数器 reg [1:0] wait_cnt; // 等待状态计数 reg [15:0] tx_reg; // SDI发送移位寄存器 reg [15:0] rx_reg; // SDO接收移位寄存器 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; cs_n_o 1b1; sclk_o 1b1; adc_done 1b0; end else begin adc_done 1b0; // 默认不产生完成脉冲 case (state) IDLE: begin if (adc_start) begin tx_reg {3b000, 1b1, 2b00, adc_channel, 2b00, 6b000000}; div_cnt 32d0; wait_cnt 2d0; bit_cnt 5d0; state CS_SETUP; end end CS_SETUP: begin cs_n_o 1b0; // 拉低片选 state FRAME; end FRAME: begin if (div_cnt (SYS_CLK_HZ / SCLK_HZ / 2 - 1)) begin sclk_o ~sclk_o; div_cnt 32d0; if (!sclk_o) begin // 下降沿更新SDI并锁存SDO tx_reg {tx_reg[14:0], 1b0}; rx_reg {rx_reg[14:0], sdo_i}; bit_cnt bit_cnt 1b1; if (bit_cnt 5d15) begin state CS_HOLD; end end end else begin div_cnt div_cnt 1b1; end end CS_HOLD: begin if (wait_cnt 2d1) begin cs_n_o 1b1; // 满足t12后拉高片选 adc_data rx_reg[11:0]; state QUIET; end else begin wait_cnt wait_cnt 1b1; end end QUIET: begin if (wait_cnt 2d3) begin adc_done 1b1; // 帧间隔足够后给完成信号 state IDLE; end else begin wait_cnt wait_cnt 1b1; end end endcase end endFRAME状态的SCLK翻转逻辑把一次移位拆成半个SCLK周期完成sclk_o为高时计数器走到分频点翻转成低同时更新发送和接收移位寄存器sclk_o为低时计数器再走到分频点翻转成高供AD7928在上升沿附近准备好下一位输出。这样SDI的更新总是发生在SCK下降沿之后满足数据手册对建立时间的隐性要求。t1和t12的等待分别落在CS_SETUP和CS_HOLD两个状态里用wait_cnt实现不依赖具体时钟频率。bit_cnt计数到15说明一帧16位全部移完此时SCK刚进入最后一拍的下降沿所以要先进入CS_HOLD消耗t12再拉高CS。adc_data在CS_HOLD里锁存只有adc_done出现时才对上层有效外部模块直接用adc_done当写使能接FIFO不需要额外的valid信号。3.3 ModelSim仿真验证Testbench的核心任务是验证驱动时序是否自洽而不是验证AD7928本身。用一段行为模型模拟ADC的SDO回读流程module tb_ad7928_driver; reg clk 1b0; reg rst_n 1b0; reg adc_start 1b0; reg [2:0] adc_channel 3d0; wire [11:0] adc_data; wire adc_done; wire sclk_o, cs_n_o, sdi_o; reg sdo_i 1b0; reg [15:0] model_reg; // 模拟AD7928的输出移位行为 always #10 clk ~clk; // 50MHz时钟 always (posedge sclk_o) begin #1 sdo_i model_reg[15]; model_reg {model_reg[14:0], 1b0}; end initial begin model_reg 16h0A5F; // 任意构造的转换结果 #1000 rst_n 1b1; #100; adc_start 1b1; #20 adc_start 1b0; #2000 $finish; end ad7928_driver u_dut ( .clk (clk), .rst_n (rst_n), .adc_start (adc_start), .adc_channel(adc_channel), .adc_data (adc_data), .adc_done (adc_done), .sclk_o (sclk_o), .cs_n_o (cs_n_o), .sdi_o (sdi_o), .sdo_i (sdo_i) ); endmoduleModelSim中run -all之后打开wave窗口把sclk_o、cs_n_o、sdi_o、adc_done加进来重点量下面三个时间区间检查项波形位置期望结果t1CS下降沿到首个SCK上升沿大于20nst12最后SCK下降沿到CS上升沿大于20nstQUIETCS高电平到下一次CS低大于50ns三个间隔全部满足综合后的时序风险就很小。如果有某个间隔刚好贴着临界值优先加大CS_SETUP或QUIET状态的等待周期而不是去调SCLK频率因为时序裕量比吞吐率更值得保护。4. Quartus工程集成从源文件到完整数据采集链路4.1 压缩包里的文件怎么用文件类型实际用途ad7928.v.bakVerilog源码备份驱动本体核心资产ad7928.bsf块符号文件原理图设计时作为模块图标ad7928.map.ammdbQuartus映射数据库编译中间产物ad7928.cmp.ammdb编译数据库与.bpm配套ad7928.cmp.bpm / ad7928.map.bpm二进制分区增量编译用普通开发可忽略_info / _vmake工程管理记录Quartus自动生成无需手工修改重建工程最省事的路径是复制ad7928.v.bak为ad7928.v加进Quartus源文件列表然后重新编译。.bsf文件的意义在于如果你的设计使用原理图Block设计可以直接把这个块符号拖进BDF省去重新画模块图标。.ammdb和.bpm这类数据库文件严格说是原工程编译过程的中间产物换电脑、换器件型号之后基本失效正常做法是让Quartus重新生成。4.2 引脚约束与时序约束AD7928接到普通3.3V I/O bank时QSF里引脚约束这样写set_location_assignment PIN_M9 -to clk set_location_assignment PIN_A12 -to adc_sclk set_location_assignment PIN_A13 -to adc_cs_n set_location_assignment PIN_B12 -to adc_sdi set_location_assignment PIN_B13 -to adc_sdo引脚编号只是示意实际分配以开发板原理图为准。sdi_o是输出、sdo_i是输入Pin Planner里方向不要填反。时钟约束方面如果系统时钟是50MHz晶振SDC里至少加这几行create_clock -name sys_clk -period 20.000 [get_ports {clk}] set_output_delay -clock [get_clocks {sys_clk}] -max 5.0 [get_ports {adc_sclk adc_cs_n adc_sdi}] set_input_delay -clock [get_clocks {sys_clk}] -max 5.0 [get_ports {adc_sdo}]set_output_delay和set_input_delay描述的是FPGA引脚到AD7928引脚之间的走线延迟以及ADC自身建立保持时间约束的组合。初版驱动不需要把这两个值卡得很严但建议把这组约束放上去因为后续把SCLK从5MHz提到20MHz时差距就在这几个纳秒的裕量上。4.3 数据链路对接FIFO这套ad7928读取程序的输出接法非常直白adc_done作为写请求adc_data作为数据总线。// 用adc_done作FIFO写使能数据同步进入后续数据通路 fifo_16x256 u_fifo ( .wrclk (clk), .wrreq (adc_done), .data ({4b0000, adc_data}), // 12位结果扩充到16位 .rdclk (sys_clk_out), .rdreq (fifo_rd), .q (fifo_dout), .full (fifo_full), .empty (fifo_empty) );FIFO的作用是把采集速率和使用速率解耦AD7928按帧节奏产生数据后端的UART、以太网或DMA按自己的节奏消费中间用FIFO缓冲。adc_data只有12位给16位FIFO时高位补0即可这也是多数数据采集链路的通用约定。驱动模块每完成一帧只产生一个adc_done脉冲FIFO写端口不会收到多余写请求握手逻辑天然成立。这里用到的FIFO可以是Quartus IP核也可以是手写的同步FIFO接口保持wrreq和rdreq这个模式驱动一侧完全不感知FIFO内部实现。5. 硬件调试时可以直接照做的三个动作5.1 在SignalTap里抓三个关键间隔把sclk_o、cs_n_o、adc_done挂到SignalTap触发器设为cs_n_o下降沿采样深度1024。启动采集后重点看CS下降沿之后的第一个SCK是否稳定出现在20ns之后以及CS上升沿和最后SCK之间是否有一段明显的低电平过渡。波形上这两个间隔是驱动是否正确的最直接证据。5.2 三种典型故障的定位思路现象直接原因定位方法adc_done一直不出现状态机卡在IDLE或FRAME观察state信号确认adc_start有有效脉冲数值规律但整体偏小数据位截取偏差核对rx_reg锁存的是低12位而不是高12位多通道时相邻通道串扰tQUIET不足加大QUIET状态等待周期看通道串扰是否消失5.3 一个提高有效吞吐率的小细节AD7928在CS上升沿之后才开始内部SAR转换这段时间对FPGA来说SCK和CS都是空闲的。QUIET状态正好落在这个窗口里等待周期够用就行。按数据手册的50ns最小值算50MHz时钟下3个周期已经满足要求但写成5个周期更稳妥温度漂移和电源噪声下不至于把时序裕量吃到零。若后端走DMA建议在adc_done置高时用一个frame_count寄存器累积帧数DMA传输长度直接取这个计数省去跨时钟域统计FIFO深度的麻烦。本文还有配套的精品资源点击获取