FPGA Verilog ADC/DAC设计详解:从接口时序到板上调试实战 简介本资源是一套面向电子类本科课程设计与FPGA初学者的Verilog数字接口实践项目聚焦模拟-数字双向转换核心环节解决ADC采样与DAC重建在可编程逻辑平台上的建模、实现与验证难题。压缩包共251个文件含62个.cdb编译数据库、60个.hdb层次化数据库、37个.tdf时序数据文件及8个关键.v源码文件如TLV1544 ADC驱动、TLC5620 DAC控制器、DA_AD顶层模块等辅以.rpt综合报告、.qsf引脚约束、.sof配置文件及readme说明文档完整覆盖从代码编写、仿真验证到FPGA下载全流程总大小1.8MB。已有1337人学习下载适合数字电路、EDA技术或嵌入式系统课程实践者。读者可直接复用模块级Verilog代码、参考状态机控制逻辑与时序协同设计方法并通过配套仿真结果与硬件约束文件快速定位时序违例与接口匹配问题切实掌握信号链路中采样保持、量化编码、权电阻映射等关键原理的硬件落地路径。 FPGA的verilogADC和DAC设计.rar——这标题很接地气一看就是工程包里常见的命名方式。压缩包里面大概率是一整套ADC采样、DAC输出、滤波处理的Verilog源码和仿真工程。今天就把这类项目从模块划分、接口时序到板上调试的完整链路说透附带我在实际调板时踩过的坑和验证过的做法。无论是准备入门FPGA数据采集还是正在被DAC毛刺和ADC采样噪声折磨这篇内容都能给你一个可参考的落地思路。1. 项目整体设计与技术选型1.1 这个工程到底在解决什么FPGA本身是数字器件引脚上只有高低电平但现实世界要给它模拟信号——传感器输出、音频波形、射频中频信号——所以必须靠ADC把模拟量变成数字量给FPGA处理靠DAC把FPGA算出来的数字量再变回模拟量。ADC/DAC这两个模块在FPGA工程里不是孤立的它往往连着一段完整的信号链采样 → 数字滤波 → 数据缓存 → 波形恢复。一个典型的工程包解压后大概能看到这些文件adc_interface.vADC芯片接口时序可能是SPI、并行或LVDSdac_interface.vDAC芯片接口或直接用IO拼出R-2R电阻网络filter模块滑动窗口平均、CIC抽取/插值wave_gen.v任意波形生成tb_xxx.v仿真测试文件约束文件、时钟管理模块、顶层模块这套东西的价值在于它把模拟世界和数字世界在FPGA里打通了。你可以用一块FPGA开发板加一个外置ADC芯片做出示波器前端或者用DAC输出任意频率的方波、正弦波做成信号发生器。这也是为什么“FPGA信号的ADC/DAC”能牵出这么多热搜词——从滑动窗口滤波到SAR ADC校准从R-2R DAC到JESD204B背后全是这条信号链上的不同环节。1.2 为什么用Verilog而不是其他语言这类工程选择Verilog一是因为FPGA厂商的工具链对Verilog支持最成熟随便一个工程模板都是Verilog二是ADC/DAC接口本身就是并行数据搬运用Verilog的always块描述时钟边沿采样、状态机控制时序比用高级综合工具更直接可控。举个实际例子最常用的SPI接口ADC——比如AD7680、ADS1115这类——时序就是SCLK、CS、DOUT三根线用Verilog写一个状态机去拉CS电平、数SCLK的个数、拼数据位也就是几十行代码的事。换成C语言思维去理解就是一个串行接收函数但Verilog里要明确每个时钟周期状态机跳到哪一步这才是数字逻辑的本来面貌。另外很多老工程师习惯用VHDL但新开的项目用Verilog的比例明显更高而且网上开源IP核、参考代码大多是Verilog遇到问题也好搜资料。项目包里选择Verilog基本是业内默认。1.3 技术方案的整体选型思路拿到需求后首先想清楚三件事信号带宽多少采样率要多高这决定了ADC接口选并行还是串行以及时钟怎么管理。输出波形是否要求平滑这决定DAC用外置芯片、R-2R电阻梯形还是PWM加RC滤波。精度要求多高12位和16位的布局布线难度、电源纹波要求完全不同。比如做低速数据采集几百kHz采样率SPI接口ADC完全够用做中频信号处理几十MHz以上采样率就要用并行接口或LVDS接口的高速ADC代码里还要处理位对齐和延迟校准。这个判断如果做错后面代码怎么优化都白搭——接口选型错了采样率上限就卡死了。2. ADC采样的核心逻辑与Verilog实现2.1 串行ADC接口时序怎么写最稳SPI模式ADC是新手最容易上手的也是工程包里最常见的。以12位ADC为例通常配置是CPOL0、CPHA1也就是SCLK空闲为低、数据在SCLK下降沿移出。核心代码逻辑大概是module adc_spi #( parameter DATA_WIDTH 12 )( input wire clk, // 系统时钟比如50MHz input wire rst_n, output reg spi_cs_n, output reg spi_sclk, input wire spi_miso, output reg sample_done, output reg [DATA_WIDTH-1:0] sample_data ); localparam IDLE 3d0; localparam CONV_WAIT 3d1; localparam SHIFT_IN 3d2; localparam DONE 3d3; reg [2:0] state; reg [4:0] bit_cnt; reg [15:0] shift_reg; reg [7:0] delay_cnt; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; spi_cs_n 1b1; spi_sclk 1b0; bit_cnt 0; shift_reg 0; sample_data 0; sample_done 1b0; end else begin case (state) IDLE: begin spi_cs_n 1b1; sample_done 1b0; state CONV_WAIT; end CONV_WAIT: begin // 拉低CS等待转换完成 spi_cs_n 1b0; if (delay_cnt 8d20) begin delay_cnt 0; bit_cnt 0; state SHIFT_IN; end else begin delay_cnt delay_cnt 1b1; end end SHIFT_IN: begin if (bit_cnt DATA_WIDTH) begin spi_sclk 1b1; shift_reg {shift_reg[14:0], spi_miso}; spi_sclk 1b0; bit_cnt bit_cnt 1b1; end else begin sample_data shift_reg[DATA_WIDTH-1:0]; sample_done 1b1; spi_cs_n 1b1; state IDLE; end end endcase end end endmodule这段代码有几个关键点刻意处理过。第一CS拉低后等了20个时钟周期才采数据因为ADC芯片从CS下降沿到数据线真正有效有一段转换时间采样太早读到的全是垃圾数据。第二移位寄存器用了16位宽实际只取低12位这样做是为了兼容有些ADC会在高位前额外输出无效位或者输出符号扩展位多移几位再截断更稳妥。第三sample_done信号只拉高一个周期方便外部逻辑用它做FIFO写使能或者触发状态机跳转。2.2 并行高速ADC与DDR采样如果做高速采集SPI就不够看了。常用的并行ADC比如AD922612位65MSPS接口是12根数据线加一个时钟输出。这种ADC的时序比较透明因为数据线和时钟都是芯片直接给出的FPGA只要保证采样时刻和数据有效窗口对齐就行。但这里有个新手必踩的坑高速ADC输出的数据在时钟上升沿和下降沿附近会有不确定态如果直接用系统时钟去采很容易采到亚稳态。正确做法是用ADC随路时钟或叫DCO作为采样时钟并且在约束文件里加set_false_path或set_multicycle_path处理跨时钟域路径。如果ADC接口是LVDS差分输出并且数据在DDR模式下传输——上升沿降沿各传一位——那么Verilog里要在always (posedge clk or negedge clk)两个边沿分别采数据。注意现代FPGA的IO逻辑里专门有IDDR原语来处理这个IDDR #( .DDR_CLK_EDGE(SAME_EDGE_PIPELINED), .INIT_Q1(1b0), .INIT_Q2(1b0) ) iddr_inst ( .Q1(data_r[0]), .Q2(data_r[1]), .C(dco_clk), .CE(1b1), .D(adc_data_pin), .R(1b0), .S(1b0) );用原语而不是自己写双沿采样逻辑是为了吃FPGA硬件的红利。IDDR在IOB里就完成了串并转换不走CLB内部逻辑时序余量好很多。好多初学者在这里为了省事自己写了双沿打拍逻辑结果跑到高速时各种时序违例。FPGA设计里有一条铁律能用原语解决的问题不要用逻辑拼。2.3 ADC数据的数字滤波怎么选ADC采回来的裸数据通常没法直接用因为噪声、工频干扰、尖峰毛刺全混在里面。工程包里最常见的三种滤波手段滑动窗口平均。这是最朴素也是最实用的。窗口长度N取2的幂次比如16、32、64这样除法直接变成移位。// 滑动平均窗口长度N16 reg [15:0] data_sum; reg [DATA_WIDTH3:0] data_buf [0:15]; always (posedge clk) begin data_sum data_sum adc_data - data_buf[ptr]; data_buf[ptr] adc_data; ptr ptr 1b1; avg_out data_sum 4; end这个结构对直流或慢变信号效果很好但有个致命缺点对高频分量衰减严重。如果输入信号本身是10kHz正弦滑动窗口却做了64点平均高频成分基本被抹平了。所以使用场景要分清滑动平均适合做慢速采集、按键消抖、电源电压监控这类场合。CIC滤波器。CIC级联积分梳状滤波器在FPGA实现里属于性价比之王因为它不需要乘法器只靠加法器和延迟单元就能完成抽取/插值。软件无线电系统里ADC后端第一级滤波器基本都是CIC先降采样再把信号丢给后面的FIR做精细整形。这个在热搜词里也出现了说明很多人在做类似的东西。中值滤波。如果干扰源是脉冲毛刺比如电机启停带来的尖峰滑动平均会把毛刺平均到输出里反而污染正常信号。中值滤波的思路是把连续N个采样排序取中间值输出。N3的中值滤波Verilog实现只有几十行对毛刺的压制效果立竿见影。但中值滤波会带来输出延迟而且实时性一般不适合高速流处理。我的建议是工程包接回去之后不要盲目默认值。先把采集到的原始数据用在线逻辑分析仪抓出来看确认噪声类型再决定滤波策略。我曾经在一个数据采集项目里只看平均滤波效果好就用了结果后续做FFT分析时频谱上全是滤波器衰减产生的伪峰排查了半天才找到根因。3. DAC输出与波形合成的关键细节3.1 从方波过冲看DAC的驱动能力问题热搜词里“DAC输出方波上升有过冲振荡”是一个特别真实的工程困扰。FPGA控制DAC输出方波时输出波形上升沿出现振荡原因基本逃不出以下三个第一电路板上寄生电感和电容形成LC谐振。DAC输出端经过长走线、过孔、接插件到示波器探头这一段有寄生电感而DAC输出端和示波器探头有寄生电容两者加起来构成一个欠阻尼的LC回路方波高频分量激励它产生振荡。解决办法是输出端加一个几十到几百欧的串联电阻或者并联一个几pF到几十pF的电容到地压低Q值。第二DAC输出端没有做好电源去耦。DAC的模拟电源和数字电源如果共用同一路数字切换瞬间的电流尖峰耦合到模拟输出端也会引起过冲。规范做法是模拟电源用单独的LDO并且π型滤波数字电源和模拟电源单点连接。第三示波器探头本身的振铃。如果用的探头地线夹比较长探头本身会形成振铃看起来像输出过冲其实是测量伪影。这种判断方法很简单换用短地弹簧接地直接扭在输出端子上波形如果明显改善就是探头问题不是电路问题。3.2 R-2R梯形网络DAC低成本方案的正确打开方式用FPGA IO直接拼R-2R电阻网络驱动DAC是很多低成本项目的选择。原理很简单每位IO驱动一个等效2R的电阻在节点上按二进制权重叠加电流形成模拟电压。12位R-2R网络理论上只用24颗电阻12组2R12组R但实际做起来电阻精度就是最大瓶颈。R-2R DAC的电阻精度直接决定INL积分非线性和DNL微分非线性。12位精度要求R和2R的匹配误差小于1/4096也就是0.024%左右。普通5%精度的电阻根本做不出来1%精度的也不够得用0.1%甚至更高精度的薄膜电阻。很多人画了个R-2R网络图以为买一包1%电阻随便焊上就完事测出来非线性误差大得吓人。我在实际项目里测试下来的结论是R-2R适合8位到10位这种中低精度、对成本极敏感的场合再往高位走性价比就急剧下降还不如直接用集成DAC芯片。还有一点要注意FPGA的IO输出高电平并不是严格等于电源电压不同IO Bank的电压、驱动强度、摆率设置都会影响R-2R的输出精度因为IO输出电阻不是0相当于在R-2R网络上串联了一个额外内阻这个影响在低速时能凑合高速时不校正基本没法用。如果是高速任意波形发生器至少也要用AD9708这种8位、125MSPS的集成DAC芯片数据接口只比R-2R多几根线但性能完全是两个量级。3.3 PWM加RC当DAC只适合特定场景还有一种非常容易被搜到的架构是PWM DAC——利用PWM占空比调节平均值再经RC低通滤波得到平滑电压。这个方案的缺点很明显波纹大、响应慢。PWM频率如果只有1kHz想滤出干净的直流RC截止频率得压到100Hz以下那响应时间就到毫秒级了。不过它有个不可替代的优势零成本且可以用在那些本身只有一个PWM外设的单片机或FPGA上。如果设计目标是控制一个加热器、一个LED亮度或者是一个慢速的电压基准PWM DAC完全够用不必杀鸡用牛刀。关键参数就两个PWM分辨率位数决定输出电压精度PWM载波频率决定波纹频率。两者是矛盾的位数越高载波频率就越低需要根据实际需求取折中。4. 完整工程实操从仿真到板上验证4.1 搭建顶层文件和模块互联合理的顶层结构是分层的。我习惯先画一张数据流图——不是画在文档里而是在脑子里模拟信号进来 → ADC接口模块 → 滤波模块 → 缓存FIFO→ 处理模块比如FFT、峰值检测→ DAC接口模块 → 模拟信号出去。每个模块端口尽量少而明确数据通路上用valid/ready握手信号或valid加使能信号。顶层模块的Verilog大致长这样module top ( input wire clk_50m, input wire rst_n, // ADC引脚 input wire adc_sclk, input wire adc_cs_n, input wire adc_miso, // DAC引脚 output wire dac_cs_n, output wire dac_sclk, output wire dac_sdi, // 输出LED指示 output wire [3:0] led ); wire [11:0] adc_data; wire adc_done; wire [11:0] dac_data; adc_spi #(.DATA_WIDTH(12)) u_adc ( .clk(clk_50m), .rst_n(rst_n), .spi_cs_n(adc_cs_n), .spi_sclk(adc_sclk), .spi_miso(adc_miso), .sample_data(adc_data), .sample_done(adc_done) ); // 滤波模块 sliding_avg #(.DATA_WIDTH(12), .AVG_N(16)) u_filter ( .clk(clk_50m), .rst_n(rst_n), .din(adc_data), .din_valid(adc_done), .dout(dac_data), .dout_valid(dac_valid) ); // DAC输出模块 dac_spi #(.DATA_WIDTH(12)) u_dac ( .clk(clk_50m), .rst_n(rst_n), .dac_cs_n(dac_cs_n), .dac_sclk(dac_sclk), .dac_sdi(dac_sdi), .data_in(dac_data) ); endmodule这个顶层就完成了最简单的“ADC采进来 → 滤波 → DAC送出去”闭环。接上信号发生器输入正弦波示波器看DAC输出应该能看到一条跟在输入后面、但毛刺被压下去的平滑波形。4.2 仿真验证的几条经验仿真测试文件虽然看起来简单但有几个细节不处理好仿出来的东西没法信。时钟不要用无限循环写得太随便。很多人写forever #10 clk ~clk;这样没问题但如果后面要用到异步复位释放的时序检查最好用initial块显式初始化后再进入循环。给足够的仿真时间。ADC采样一个点可能就要几百个时钟周期滤波窗口又要累计16个点DAC输出又要移位整个链路跑通至少要几万个周期。很多初次写testbench的人只跑几百个周期就停了看到一个模块的输出就以为全系统OK这是大忌。我一般会把仿真总时间设成实际需要的10倍以上确保所有状态机都遍历过边界路径。加中间信号断言。仿真不只是看波形更重要的是检查信号时序。比如ADC接口在CS拉低到第一次收到有效数据之间有没有出现意外的电平跳变滤波模块的输出相比输入有没有多出一拍延迟导致下游状态机错位。用$display或断言语句把这些关键点打出来比盯着波形肉眼看靠谱得多。4.3 上板调试的实测流程仿真通过只说明逻辑功能正确不代表板上能跑起来。第一步先检查时钟。用逻辑分析仪的内嵌功能或者Ila核抓内部时钟信号确认PLL输出频率和预期一致时钟有没有毛刺。第二步单模块验证——先把ADC接口单独拉到顶层把采到的数据直接送到LED或串口打印出来看数值是否随输入变化。第三步联调——滤波和DAC模块逐级接入每接一级就抓一次波形确认各级数据通路没有断。我当时调一个SPI ADC仿真怎么跑都对上板后数据全是0。排查到最后发现是复位逻辑的问题ADC模块的异步复位和系统复位信号没对齐导致状态机偶发进入死状态。把复位同步器加上、复位释放时序用recovery/removal约束固定住之后问题才彻底解决。这类问题仿真里很难复现因为仿真时钟和复位是理想化的板上时钟树延迟和复位释放窗口是真实存在的所以板上调试一定不要跳过这个环节。5. 常见问题与排查技巧实录5.1 数据采集一直是固定的满码或零码这个现象大概率是接口时序完全错位。满码12位ADC输出0xFFF说明所有位都被采成了1零码0x000则所有位都是0。排查思路是拿逻辑分析仪看SPI四根线的真实波形对比芯片数据手册的时序图重点检查CS低电平保持时间够不够、SCLK频率有没有超过芯片最高限制、数据是在哪个沿采的。很多人在Verilog里写了spi_sclk ~spi_sclk;用来产生时钟但这种方式生成的时钟是分频出来的占空比可能不是50%相位抖动也大。更稳的做法是用高频系统时钟直接计数在计数到某个值时采样MISO而不是自己生成一个时钟再去采数据。这样数据采样的相位是确定的不容易受产生时钟的jitter影响。5.2 DAC输出波形有台阶感或毛刺台阶感一般是分辨率不够或更新率太低。分辨率不够就只能换高位DAC更新率低就要检查DAC接口模块的移位时钟是不是没跑满。毛刺则有两个常见来源一是并行DAC的数据线在切换时各bit翻转时间不一致产生了glitch——解决方法是查约束文件里DAC数据输出管脚的set_output_delay约束有没有加或者通过调整IObuffer的slew rate让翻转时间尽量一致二是DAC输出缓冲的摆率不够高频分量被削输出波形在转折处出现非线性畸变。还有一种是数据对齐问题。比如你的DAC是12位的但FPGA内部数据处理后是16位的直接把高位截断送去DAC输出波形会有明显的分段跳变。正确做法是加一个饱和或舍入模块先把16位数据折算到12位范围再做四舍五入或抖动处理波形会平滑很多。5.3 ADC采样数据跳动滤波后依然有波动先判断噪声来源。如果输入接的是稳定的直流电压ADC输出却在最低几位跳可能是电源噪声大、参考电压不稳或者PCB走线耦合了数字噪声。先用示波器测电源纹波纹波超过10mV级别就要处理了。其次是参考电压很多ADC芯片的Vref引脚对噪声极其敏感必须在Vref引脚就近放一个低ESR电容最好再加一个RC低通或LC滤波。如果这些都没问题再考虑是不是ADC芯片本身需要校准。SAR ADC尤其是高位SAR存在电容阵列失配问题很多芯片自带校准功能初始化时发一条校准命令就行。热搜词里也有“SAR ADC模拟前台校准”这个就是通过内部寄存器配置让芯片在校准模式下自己修正电容失配。别直接跳过这一步精度不够时多半是它。5.4 JESD204B的问题值得单独说一句如果你的设计中用了高速ADC/DAC比如250MSPS以上的那大概率绕不开JESD204B接口。这个接口是高速串行协议不是普通的并行数据线它涉及到多通道对齐、时钟同步、链路训练、误码检测调试难度比SPI/并行接口高一个数量级。热搜词里“FPGA工程师的JESD204B调试笔记”能成为热词说明这领域确实坑多。JESD204B一个常见坑是SCLK和SYSREF的时序关系不对导致链路无法进入正常工作状态。调试时一定要先用BERT误码率测试或PRBS模式验证物理层通不通再谈应用层。另外不同厂家的FPGA对JESD204B硬核的初始化寄存器配置不太一样别直接抄别的芯片的配置组合要对着官方参考设计和芯片手册逐项核对。6. 一个完整回环测试的实操记录最后放一个我在某个项目里的实测流程可以当做一个可复制的模板。硬件平台一块中端FPGA开发板ADC采用AD768016位、100kSPS、SPI接口DAC采用AD566216位、SPI接口模拟前端用一个运算放大器做阻抗变换和电平搬移。测试步骤用信号源生成1kHz、1Vpp的正弦波经过运放调理到ADC输入范围0~2.5V。FPGA采集数据在内部做64点滑动平均滤波。滤波后的数据直接送DAC输出。示波器测量DAC输出波形对比输入信号。实测中发现DAC输出的正弦波THD比输入信号劣化了不少。排查后发现是DAC的更新率太低——AD5662是慢速芯片更新率只有几十kHz跟不上1kHz正弦波每一周期的重建需求。后来把信号源频率降到100Hz输出波形就明显平滑了。这说明一个道理回环测试里系统瓶颈往往是链路中最慢的那个环节要先把瓶颈找出来。另一个发现是ADC输入端的运放供电用了开关电源的一路输出纹波有20mV直接体现在采集数据的最低位跳动上。换成LDO供电后跳动从±3个LSB降到了±1个LSB以内。这件事给我的教训是模拟前端的电源设计比FPGA内部代码对数据质量的影响大得多不要只盯着Verilog看。这个回环测试看着简单却是验证整个FPGA ADC/DAC设计链路最有效的手段。它能一次性暴露接口时序、滤波处理、模拟前端电路、电源完整性这四层问题。你的工程包里如果还没有一个完整的回环测试模块强烈建议补上。从接到项目到最终跑通FPGA相关的ADC/DAC设计本质上是数字逻辑和模拟电路的交叉地带单靠Verilog写得漂亮远远不够还得对芯片时序、电路布局、电源噪声有足够的敏感度。希望这篇内容能帮你把压缩包里的源码变成真正能在板上跑起来、在示波器上看到漂亮波形的系统。本文还有配套的精品资源点击获取