简介这份rar压缩包围绕AD7928这颗16位逐次逼近型ADC提供一套基于Verilog的FPGA驱动设计与工程实现面向需要做数据采集的FPGA开发者、嵌入式工程师及学习数字接口设计的初学者也适用于通信、测试测量等数据采集系统。压缩包共257个文件约14.84MB以Quartus工程文件、Verilog源码、仿真波形、TDF逻辑描述、编译报告及库文件等为主并包含readme和txt说明文档可在Quartus环境中直接打开学习或重新编译验证。目前已有628人学习浏览。资源价值在于给出了从器件时序分析、状态机控制到仿真验证和硬件下载的完整驱动参考代码包含AD7928所需串行时钟、片选、串行数据输入输出等信号逻辑便于读者对照数据手册理解驱动流程工程内大量仿真与报告文件也可帮助排查时序问题适合作为快速搭建ADC采集通道的起点。1. FPGA读AD7928这件事难在SPI帧里挤了两个阶段如果在搜索引擎里看到“ad7928.rar_AD7928的FPGA_FPGA ad7928_ad7928读取程序_verilog_驱动fpga”这类资源标题基本可以判断包里的核心是一份用Verilog在FPGA上驱动AD7928的源码外带仿真或说明。AD7928是ADI的8通道、12位SPI接口ADC常见于多通道模拟量采集、电源监控、工业控制前端的低速信号采集场景。标题里反复出现“读取程序”说明多数人卡住的不是ADC芯片本身而是怎么把SPI读写时序在FPGA里精确地切出来。写驱动时有个反直觉点AD7928的一帧不仅仅是“读12位数据”而是前12个SCLK写控制字、后12个SCLK读结果两部分必须连续拼在同一帧里。很多人把这两件事拆成两个独立状态去做结果不是丢通道号就是读到的数据整体错位。下面按“时序先梳理、状态机后实现、仿真再收尾”的顺序把这条路径完整走通。适合正把AD7928接到Xilinx或Cyclone上、手头只有一份说明书和一块示波器的人。2. AD7928的SPI时序控制字位域与读写帧结构为什么要先看手册2.1 控制字把通道选择、电源管理与编码方式压进12位AD7928的串行接口只有CS、SCLK、DIN、DOUT四根线控制字长度固定12位。和很多半双工SPI ADC不同它的DIN和DOUT是分开引脚不是三线一根DIO。FPGA侧DIN是输出DOUT是输入physically分开的好处是驱动模块不必处理IO方向切换但也容易让人随手把一个双向IO复用在上面白费一组逻辑。控制字承载的信息包括片内通道号8选1、编码方式二进制补码还是偏移二进制、电源管理模式正常/自动关断、输出数据长度和位序。12位把这些全部塞进bit11到bit0。完整位域必须对照ADI手册的Control Register表格确认不同版本芯片在bit4:3、bit2:1这类字段上也有细节差异。老工程师常见的做法是直接抄旧工程只改通道号如果新板子调不通最先要怀疑的就是这一行拼接。// 12位控制字位域MSB在前通过DIN逐位发送 // 注意具体位含义以AD7928数据手册Control Register为准下面只演示拼接方式 localparam [2:0] CH_VIN0 3d0; wire [11:0] cfg_word {1b1, // bit11: 帧起始标识位置 1b0, // bit10: 编码方式选择 2b11, // bit9:8: 电源管理 ch_sel, // bit7:5: 通道地址 2b00, // bit4:3: 通道工作模式 2b01, // bit2:1: MSB优先输出 1b0}; // bit0: 保留位上面这段总共12位和SPI移位寄存器直接对接。ch_sel由外部输入或采样调度逻辑给出取值范围0到7。发送顺序从最高位开始也就是说第一个出现在DIN引脚上的电平就是bit11。不要凭记忆写这份控制字把手册第一页的时序图打印出来逐位对着核对一次。2.2 一帧24拍前12拍下发通道配置后12拍移出转换值AD7928在CS拉低后的第一个SCLK上升沿捕捉DIN上的控制字。前12个SCLK周期是“写阶段”芯片内部根据DIN完成通道切换和配置紧接着的12个SCLK周期是“读阶段”DOUT在SCLK下降沿更新FPGA在上升沿采样逐位移出12位转换结果。读阶段开始前如果SCLK停住并等待转换结束会拉低吞吐率如果SCLK连续翻转芯片内部自己完成转换并把结果串行送出CS在整个过程中保持低电平即可。所以驱动里最常见的帧结构是CS拉低后连续发24个SCLK中间不插入等待状态。这正是ad7928.rar这类资源包代码里最一致的部分。和FPGA驱动直接相关的主要参数集中在下面一张表里参数含义对FPGA驱动的影响SCLK max最大串行时钟频率决定分频系数能不能再压小tCONVERTADC转换时间连续SCLK模式一般无需显式等待tQUIESCENTCS拉高后最小安静时间决定相邻两帧的最小间隔控制字长度12 bit位计数器要用5位寄存器DOUT高阻态CS无效时输出高阻FPGA输入脚不能开推挽FPGA侧用系统时钟分频产生SCLK只要SCLK周期大于手册要求就有裕量。常见做法是分频系数取8到3250MHz系统时钟对应SCLK大约6.25MHz到1.6MHz对AD7928这类1 MSPS级别的ADC足够且时序余量大。tCONVERT和tQUIESCENT具体数值随供电电压和温度变化以上板验证为准。DOUT在CS无效时是高阻。FPGA侧这个输入引脚要按三态输入处理不要配置成推挽输出。这个细节正好回应一个常被问到的问题FPGA的IO和ARM那种推挽、开漏、上拉模式是否完全对应——在AD7928的SPI上输入脚默认不开内部上拉也能工作因为CS拉低后DOUT会被芯片驱动开了上拉反而在CS无效时把总线钳到高电平增加无效功耗。3. 用Verilog写AD7928驱动状态机最小可综合骨架与关键参数3.1 模块接口与时序控制字装载实操中AD7928驱动模块的接口通常围绕“FPGA核心侧发起采样、SPI侧收发”来设计。核心侧给一个start单脉冲和通道号模块回一个data_valid和12位data_outSPI侧给CS、SCLK、DIN、DOUT。这样外部调度逻辑可以方便地把它嵌进FIFO、AXI-Stream或轮询状态机。module ad7928_driver #( parameter CLK_DIV 8 // SCLK clk / CLK_DIV )( input wire clk, input wire rst_n, input wire start, // 单周期高电平触发一次采样 input wire [2:0] ch_sel, // 本次采样通道号 0~7 output reg busy, output reg ad_cs_n, // 低有效 output reg ad_sclk, output reg ad_din, input wire ad_dout, output reg [11:0] data_out, output reg data_valid ); // 控制字位域按自己选定的寄存配置填写 wire [11:0] cfg_word {1b1, 1b0, 2b11, ch_sel, 2b00, 2b01, 1b0};参数CLK_DIV控制SCLK分频系数。取8表示SCLK周期等于8个系统时钟周期一个高半周期占4拍。start只在IDLE状态下被采样所以外部要等busy拉低再发下一次触发busy从IDLE之外有效避免一帧还没结束就被重复触发。3.2 两段式状态机IDLE、SHIFT、DONE怎么衔接状态机采用两段式写法一个always块管理状态转移和输出寄存器另一个块专门产生SCLK。三个状态足够IDLE等待触发SHIFT里产生24拍SCLK前12拍发控制字、后12拍收数据DONE拉高CS并产生data_valid。帧结构保持连续SCLK中间不加wait状态这样吞吐率最高。localparam IDLE 2d0; localparam SHIFT 2d1; localparam DONE 2d2; reg [1:0] state, next; reg [4:0] bit_cnt; // 0~23一帧共24拍 reg [2:0] sclk_cnt; // SCLK分频计数 reg [11:0] din_shift; reg [11:0] dout_shift; // 分频事件高半周期结束为上升沿整周期结束为下降沿 wire sclk_rise (sclk_cnt CLK_DIV/2 - 1); wire sclk_fall (sclk_cnt CLK_DIV - 1); // 产生SCLK always (posedge clk or negedge rst_n) begin if (!rst_n) begin ad_sclk 1b0; end else begin if (sclk_rise) ad_sclk 1b1; else if (sclk_fall) ad_sclk 1b0; end end // 主状态机 always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; ad_cs_n 1b1; busy 1b0; data_valid 1b0; end else begin data_valid 1b0; case (state) IDLE: begin if (start) begin ad_cs_n 1b0; busy 1b1; bit_cnt 5d0; sclk_cnt 3d0; din_shift cfg_word; // 预先装载控制字 state SHIFT; end end SHIFT: begin if (sclk_fall) begin sclk_cnt 3d0; if (bit_cnt 5d23) begin ad_cs_n 1b1; busy 1b0; data_out dout_shift; data_valid 1b1; state DONE; end else begin bit_cnt bit_cnt 1b1; // 前12拍发送控制字 if (bit_cnt 5d12) din_shift {din_shift[10:0], 1b0}; end end else begin sclk_cnt sclk_cnt 1b1; end // 后12拍在SCLK上升沿采样DOUT if (sclk_rise (bit_cnt 5d12) (bit_cnt 5d24)) dout_shift {dout_shift[10:0], ad_dout}; end DONE: begin if (!start) state IDLE; end default: state IDLE; endcase end end assign next state; // 两段式中的下一状态占位实际状态转移在case内直接完成代码里需要注意的细节集中在几个地方。din_shift在IDLE分支装载cfg_word装载后ad_din立刻输出最高位第一个sclk_rise到来时ADC采到的正好是bit11符合“CS拉低后第一个上升沿采控制字首位”的时序要求。sclk_fall只做移位和计数真正的采样放在sclk_rise分支正好对上“DOUT在SCLK下降沿更新、上升沿稳定”的特性。data_valid在bit_cnt等于23的那个时钟沿被拉高下一拍进入DONE并拉低所以它是一个单周期脉冲。外部FIFO写使能直接采样这个脉冲没有问题如果下游是AXI-Stream的tvalid就不能这样用tvalid需要保持到tready握手完成。3.3 三个最容易改错的参数DIV、采样沿、data_valid脉冲CLK_DIV容易踩的第一个坑是周期与半周期混淆。上面代码里SCLK周期等于CLK_DIV个系统时钟周期不是CLK_DIV/2。如果按“半周期”去理解实际SCLK频率会比预期快一倍仿真Modelsim可能仍然通过但板上时序裕量会被吃掉距离拉长后数据采错。采样沿是最经典的问题。AD7928的DIN在SCLK上升沿被采样DOUT在SCLK下降沿更新所以FPGA端必须在SCLK上升沿采样DOUT。不少资源包代码写成always (negedge ad_sclk)来采集如果只是用下降沿做位计数器问题不大如果直接在这个边沿采dout读回数据会整体移位一位。波形上的特征很明确固定通道读出的值总是差一位通道切换后数值对不上。data_valid的脉冲宽度也要单独想清楚。单帧模式下它只有一拍如果把它接到不带反压的同步FIFO写使能上通常不需要额外处理如果FIFO满了要等得把data_valid改成“断言后保持直到FIFO回应”的握手信号否则丢数。提示如果板载时钟不是50MHz而是100MHz或125MHz直接把CLK_DIV调大即可状态机本身不依赖系统时钟频率只依赖分频系数和SCLK对外时序。4. 用Modelsim仿AD7928驱动先做回环再做行为模型4.1 最小testbench在SCLK上升沿捕获DIN并回读仿真模型和真实芯片行为对齐的关键就是“上升沿采DIN、下降沿更新DOUT”。testbench里不需要调用任何厂商库直接用两个always块模拟AD7928的移位行为即可。下面的模型每次CS拉高后把回读数据重新装载为0x555驱动端如果正确工作data_out就应该读到0x555。module tb_ad7928(); reg clk 0, rst_n 0, start 0; reg [2:0] ch 0; wire cs_n, sclk, din, dout, dvalid; wire [11:0] dout_data; // 50MHz系统时钟 always #10 clk ~clk; // 例化驱动 ad7928_driver #(.CLK_DIV(8)) u_dut( .clk(clk), .rst_n(rst_n), .start(start), .ch_sel(ch), .ad_cs_n(cs_n), .ad_sclk(sclk), .ad_din(din), .ad_dout(dout), .data_valid(dvalid), .data_out(dout_data), .busy() ); // 行为模型每次CS拉高时重装回读数据0x555 reg [11:0] sdo_buf; reg dout_r; always (posedge cs_n) sdo_buf 12h555; always (negedge sclk) begin if (!cs_n) begin dout_r sdo_buf[11]; sdo_buf {sdo_buf[10:0], 1b0}; end end assign dout cs_n ? 1bz : dout_r; initial begin #100 rst_n 1; #20 start 1; #20 start 0; #100_000 $finish; end endmodule行为模型里sdo_buf初始值0x555CS拉高后重装等价于每次转换都回传0x555。dout在CS无效时给高阻和真实芯片一致驱动端在sclk_rise采样doutsdo_buf在SCLK下降沿更新所以引脚稳定后再被采到。仿真波形里重点看start发出后CS下降沿到第一个SCLK上升沿的间隔dvalid产生的位置是否对应第24个SCLK下降沿data_out是否为0x555。4.2 板级ILA调试抓哪几个信号、什么时候触发上板调试一般不直接接传感器而是先把ADC引脚接地或接一个稳定分压验证通道值和电压是否成比例。ILA或逻辑分析仪抓取ad_cs_n、ad_sclk、ad_din、ad_dout、data_valid以及ch_sel。触发条件建议设为ad_cs_n下降沿不要用data_valid做触发因为它只有一拍调试时窗口容易错过。最容易看错的时点是CS下降沿与第一个SCLK上升沿的相对位置。PCB走线长了之后这段间隔如果太短SCLK上升沿时DIN可能还没稳定控制字第一位就被采错。波形上如果出现“第一拍独有毛刺、后面全部正常”优先查这里。另一个检查点是DOUT从高阻切换到驱动的过程ILA采样到正确的逻辑电平即可示波器探头带来的耦合噪声不用太在意。提示仿真时可以在initial块里加一个$display在dvalid拉高时打印data_out的十六进制值。一次成功打印出555比反复盯波形快得多。5. AD7928驱动调通后值得加的三个Verilog功能5.1 多通道自动轮询驱动只有一个通道号输入多通道采集在外部做轮询即可。常见做法是采样完成时自动把ch_sel加1并立刻触发下一帧这个调度逻辑放在外层更方便因为要同时控制FIFO写使能。轮询回路注意一点最后一个通道回到通道0时要给FIFO写一个帧起始标记否则下游解析数据时分不清哪一包是一轮采样的开始。5.2 四阶滑动窗口均值滤波对12位ADC来说末位跳动是常态一个长度为4的滑动窗口均值足以在平滑性和响应速度之间取得平衡。Verilog实现适合用移位寄存器加累加器避免每个周期重新求和。长度取2的幂次均值用右移两位完成不消耗除法器。// 四阶滑动窗口均值输入12位输出14位累加和取高12位 reg [11:0] hist [0:3]; reg [13:0] acc; always (posedge clk) begin if (data_valid) begin acc acc {2b00, data_out} - {2b00, hist[3]}; hist[3] hist[2]; hist[2] hist[1]; hist[1] hist[0]; hist[0] data_out; end end assign filter_out acc[13:2];hist数组保存最近4个采样点acc是14位累加和4个12位数累加最大为1638014位不会溢出。注意累加更新必须先减hist[3]再移位顺序反了会把当前刚丢掉的数据减进去。滤波放在AD7928驱动里做还有一个好处它只跟随data_valid不占用SPI帧间隔每来一个有效转换就更新一次输出。5.3 数据打包写FIFO采集端和消费端通常是异步时钟域跨时钟域用FIFO是标准做法。AD7928一次转换只有12位直接写12位FIFO浪费不了多少资源但通道信息会丢失。更实用的方式是把通道号拼到高4位凑成一个16位写入FIFO{ch_sel, data_out}。下游读FIFO时先看高4bit确认通道再看低12bit得到数据省掉单独维护通道号FIFO的逻辑。若采用AXI-Streamtvalid就用data_validtdata直接接16位拼装结果需要用tready做反压时在驱动外层补一个握手寄存器即可。本文还有配套的精品资源点击获取