
简介基于Verilog的FPGA AD数据采集完整工程面向FPGA学习者、课程设计及毕业设计人员解决ADC采样时序控制、跨时钟域同步、数字滤波与数据存储等典型问题。压缩包共270个文件、约488KB以Quartus工程为主体包含Verilog/VHDL源码.v/.vhd、原理图文件.bdf/.bsf、仿真波形.vwf、分析综合报告.rpt/.summary及配置文件可直接在Quartus中重建并运行仿真验证。已有600人学习下载适合作为入门参照或项目预研基础。工程内含采样控制、同步电路、数字滤波FIR/IIR与BRAM存储等关键模块各模块以独立文件呈现便于裁剪复用其中采样控制通过时钟分频产生采样时钟同步电路采用边沿检测与握手协议处理跨时钟域问题并配合仿真波形和综合报告可完整梳理AD采集链路的设计思路方便在此基础上进行二次开发与调试。1. 解包 ADC.rar.bdf 与 .bsf 才是 Verilog 采集系统的入口解压 ADC.rar最显眼的不是 Verilog 源码而是 adc.bdf、adc_1.bsf 到 adc_4.bsf、lpm_ram_io0.bsf 这堆 Quartus II 原理图工程文件。.bdf 是顶层原理图.bsf 是原理图里被调用的模块符号lpm_ram_io0 是 Altera 的同步 RAM 宏功能adc.pre_map.cdb 是编译到布局前的中间产物。这类工程的正确读法不是满盘找 .v 文件而是沿着原理图顶层连线找到每个符号背后用 Verilog 描述的行为模块再把采样、同步、缓存三条链路串起来。FPGA 做 ADC 采集麻烦点从来不在 ADC 芯片本身而是采样时钟怎样从系统时钟分出来、转换完成信号怎么安全送进 FPGA、采到的数据放到哪里。这套工程恰好把三件事都覆盖了适合正在用 Verilog 接外部并行或串行 ADC 做信号采集的工程师对只碰过数字逻辑、没摸过模拟前端的人也有参考价值。2. ADC 采样控制状态机时钟分频、转换周期与读取时序2.1 采样周期不是主频除出来的是两段拼出来的很多方案把 FPGA 主时钟分频后直接当采样率这是第一个容易算错的地方。ADC 的采样周期由两段组成转换时钟频率以及一次转换消耗的转换时钟周期数。以并行逐次逼近型 ADC0809 为例clk 允许范围大约在 10kHz 到 1.2MHz一次完整转换要消耗 64 到 73 个时钟周期。所以 clk 给 500kHz 时实际采样率只有 7kHz 上下clk 给到上限附近采样率也不到 18kHz。换言之系统时钟除一个分频系数得到的是 ADC 的转换时钟而不是采样周期。ADC 采样原理里的采样、量化、编码三步在 FPGA 数字侧真正要盯的是第一步的时机什么时候把通道地址锁进 ADC什么时候给启动脉冲数据总线在哪一刻稳定下来。通道锁存偏了、启动脉冲太短、读取时刻踩在总线切换毛刺上量化结果都会看起来正常但数值不对。下面这张表按 50MHz 系统时钟列出常用分频组合可以直接作为参数设计起点。分频系数adc_clk 频率一次转换周期数实际采样率100500 kHz约 70约 7.1 kHz501 MHz约 70约 14.3 kHz252 MHz超出规格不建议不可用2.2 采样状态机怎么切四个状态比八个状态好用参考工程里 adc_1 到 adc_4 这几个模块符号最常见的切法是采样控制、时钟分频、数据同步、存储控制各自独立。采样控制本身我一般只保留四个状态IDLE 等待触发START 里同时拉高 ale 和 adc_start把通道号锁存进 ADCWAIT_EOC 等待转换完成标志READ 拉高 oe 并锁存数据总线。状态太多会让握手信号之间的因果关系变乱四个状态配合两级同步正好覆盖 ADC0809 这种并行 ADC 的完整时序。有一个细节值得注意ADC0809 的 EOC 在启动转换后拉低转换完成时拉高所以完成事件是上升沿不是很多人直觉里的下降沿。工程里如果按下降沿写状态机会在转换开始瞬间误判一次完成数据锁存到的是总线上的无效电平。2.3 采样控制 Verilog 实现与关键参数下面是一段可综合的采样控制模块按 50MHz 系统时钟、500kHz adc_clk 设计。代码里做了简化START 状态只保持一个周期实际使用时要把该状态展开到至少 5 个周期保证 ale 和 adc_start 的高电平宽度超过 100ns否则部分 ADC 芯片不响应。module adc_ctrl #( parameter CLK_DIV 100 )( input wire clk, // 系统时钟 50MHz input wire rst_n, // 低有效复位 input wire [2:0] ch_addr, // 模拟输入通道号8 选 1 input wire start_req, // 启动一次采集 input wire eoc_in, // ADC 转换完成标志低有效 output reg adc_clk, // 分频后的转换时钟500kHz output reg ale, // 地址锁存使能 output reg adc_start, // 启动脉冲 output reg oe, // 输出使能 input wire [7:0] adc_db, // 8bit 并行数据总线 output reg [7:0] sample_out, // 锁存后的采样结果 output reg done // 单次采集完成脉冲 ); // 分频产生 adc_clk reg [6:0] div_cnt; wire div_hit (div_cnt (CLK_DIV/2 - 1)); always (posedge clk or negedge rst_n) begin if (!rst_n) div_cnt 0; else if (div_hit) div_cnt 0; else div_cnt div_cnt 1; end always (posedge clk or negedge rst_n) begin if (!rst_n) adc_clk 0; else if (div_hit) adc_clk ~adc_clk; end // EOC 两级同步 上升沿检测 reg eoc_r1, eoc_r2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin eoc_r1 1; eoc_r2 1; end else begin eoc_r1 eoc_in; eoc_r2 eoc_r1; end end wire eoc_pos eoc_r1 ~eoc_r2; localparam IDLE 2d0, START 2d1; localparam WAIT 2d2, READ 2d3; reg [1:0] state; always (posedge clk or negedge rst_n) begin if (!rst_n) begin state IDLE; ale 0; adc_start 0; oe 0; done 0; sample_out 8d0; end else begin ale 0; adc_start 0; oe 0; done 0; case (state) IDLE: if (start_req) begin ale 1; adc_start 1; state START; end START: state WAIT; WAIT: if (eoc_pos) begin oe 1; state READ; end READ: begin sample_out adc_db; // 数据总线已稳定 done 1; state IDLE; end endcase end end endmodule代码里eoc_pos eoc_r1 ~eoc_r2是核心它检测的是「第二拍同步寄存器从 0 变 1」的上升沿对应 EOC 在转换完成时拉高的行为。done在 READ 状态拉高一个周期给下游 RAM 写控制当触发信号。分频参数CLK_DIV 100并不是采样率采样率还要再除以 70 左右这一点在联调时最容易产生误会。另外adc_db在顶层被声明为 input 是合理的因为 FPGA 只作为接收端ADC0809 的数据线虽然内部是三态输出但由 ADC 的 OE 引脚自己控制FPGA 不需要反向驱动总线。3. 跨时钟域同步与 IO 约束EOC 边沿检测和引脚时序收敛3.1 为什么每一根信号都要先打两拍再判断EOC 和 adc_db 在真实板子上都是相对系统时钟异步的信号。EOC 由 ADC 内部状态机产生数据总线在 oe 有效后由 ADC 内部三态门输出两者和 FPGA 的 50MHz 时钟没有任何相位关系。如果直接把 EOC 送进状态机采到的边沿可能落在触发器建立/保持时间窗口内触发器输出进入亚稳态表现就是状态机随机多等一次或少等一次转换而且换一块板子、换一个温度就复现不出来。两级寄存器同步解决的就是这个问题让异步信号先在第一个触发器上完成亚稳态收敛第二拍拿到的已经是确定值。代价是边沿检测晚两个时钟周期对低速 ADC 采集完全可接受。但要注意两级同步解决的是亚稳态不解决信号毛刺。如果 EOC 线上有噪声抖动同步器会原样把抖动传进来。工程上常见做法是在 adc_db 和 EOC 引脚附近串 33 到 100 欧姆电阻配合 10nF 对地电容把沿变缓代价是边沿斜率下降所以阻容值要按 adc_clk 周期去算别让 RC 时间常数超过一个系统时钟周期的一半。3.2 边沿检测电路上升沿和下降沿各写各的边沿检测模块应该独立出来不要内联在状态机里。我用过一个很紧凑的写法两个 always 块加两条 assign上升沿和下降沿同时输出状态机按需取值module edge_detect ( input wire clk, input wire rst_n, input wire sig_in, output wire rising, output wire falling ); reg sig_r1, sig_r2; always (posedge clk or negedge rst_n) begin if (!rst_n) begin sig_r1 1b0; sig_r2 1b0; end else begin sig_r1 sig_in; sig_r2 sig_r1; end end assign rising sig_r1 ~sig_r2; assign falling ~sig_r1 sig_r2; endmodule两拍寄存器之间只差一个时钟周期所以输出的rising和falling脉冲宽度恰好是一个系统时钟周期这对状态机来说是最友好的事件信号。用这个模块去替换第 2 章代码里的手写同步逻辑状态机代码会干净很多。握手协议上还有一个习惯值得保持进入 WAIT 状态前先确认 EOC 处于高电平避免在 ADC 刚上电或复位释放瞬间把残留的低电平误判成转换开始。3.3 引脚分配与 Quartus 约束写法采样控制模块综合之后引脚分配是另一道坎。下面这张表格是典型的双列直插 ADC 接法实际管脚编号以自己板子的原理图为准不要照抄工程信号名方向IO 电平管脚举例说明clkinput3.3-V LVTTLPIN_E1板载 50MHz 晶振adc_clkoutput3.3-V LVTTLPIN_AA14接 ADC 的 CLKaleoutput3.3-V LVTTLPIN_AB14接 ADC 的 ALEadc_startoutput3.3-V LVTTLPIN_AC14接 ADC 的 STARTeoc_ininput3.3-V LVTTLPIN_AA15接 ADC 的 EOCoeoutput3.3-V LVTTLPIN_AB15接 ADC 的 OEadc_db[7:0]input3.3-V LVTTLPIN_AA16 等并行数据总线引脚分配除了用 Quartus 的 Pin Planner 图形界面更推荐直接写在 .qsf 里方便版本管理。SDC 错误地不写 input delay时序分析器会默认数据在时钟沿同时到达导致明明功能正确但时序报告一片红。set_location_assignment PIN_AA14 -to adc_clk set_instance_assignment -name IO_STANDARD 3.3-V LVTTL -to adc_clk create_clock -name sys_clk -period 20.000 [get_ports clk] create_generated_clock -name adc_clk -source [get_ports clk] \ -divide_by 100 [get_ports adc_clk] # 数据手册中 OE 有效到数据稳定典型值约 100ns保守取 120ns set_input_delay -clock adc_clk -max 120 [get_ports adc_db] set_input_delay -clock adc_clk -min 5 [get_ports adc_db]注意set_input_delay的时钟基准是 adc_clk因为数据总线是随着 oe 有效而出现的而 oe 又由 adc_ctrl 在系统时钟域里驱动。如果数据手册给出的建立时间比 adc_clk 周期还大说明分频系数太小ADC 根本来不及在下一个转换周期前稳定输出这时要往上调CLK_DIV而不是硬压时序约束。4. LPM_RAM_IO 缓存模块同步 RAM 例化、读写控制与 FIFO 边界4.1 为什么这个工程选同步 RAM 而不是 FIFO文件列表里的 lpm_ram_io0.bsf 说明原工程用的是 Altera 的同步单口 RAM 宏功能。LPM_RAM_IO 这个名字已经把端口说清楚了data 数据线、address 共用地址、wren 写使能、clock 时钟、q 读出。单口 RAM 意味着读写共用一个地址端口同一时刻只能读或者只能写。它适合「采集满一段数据再整体搬走」的批次型应用和这个 ADC 采集工程的使用方式完全匹配ADC 采样完成后逐笔写入采集结束后再从 RAM 里顺序读出送显示或通信。很多方案在这里会顺手换成 FIFO但 FIFO 是异步读写指针管理适合两端速率不一致且需要持续流动的数据通路本工程采样率只有 7kHz 上下读端也不存在突发吞吐要求用 FIFO 反而把简单问题复杂化。如果确实需要边采边读应该用双口 RAMlpm_ram_dp而不是单口 RAM 硬切时间片。顺带提一句DDR3 读写控制那种大工程里的缓存管理思路和这里有本质区别DDR3 解决的是容量和带宽LPM_RAM_IO 解决的是跨时钟隔离和批次存储两者目标不同。4.2 例化参数与读写时序在 Quartus 的 MegaWizard 里例化 lpm_ram_io 时需要敲定的参数如下参数项推荐取值说明data width8 bit与 ADC 位宽一致address width10 bit深度 1024约能存 0.7 秒7kHzclock modesynchronous读写共用系统时钟避免额外时钟域q 输出延迟one clock读地址给出后的下一拍才出数据同步 RAM 的读写时序比异步 RAM 简单所有动作对齐时钟上升沿唯一要记牢的是 q 读出一拍延迟地址在 T 时刻送上 address 端口q 上的数据在 T1 时刻才有效。如果连续读第一个地址的数据在第三次时钟沿才能采到这个特性在仿真和板级调试里都要提前留好余量。4.3 读写控制 Verilog 代码与地址保护写侧控制直接复用第 2 章 adc_ctrl 的done信号地址计数要加溢出保护。下面这段代码放在 RAM 的包装模块里把 adc_ctrl 的done转成 RAM 写使能module ram_wr_ctrl ( input wire clk, input wire rst_n, input wire sample_done, // 来自 adc_ctrl.done input wire [7:0] sample_data, // 来自 adc_ctrl.sample_out output reg [9:0] wr_addr, output reg wr_en, output wire [7:0] ram_q, input wire [9:0] rd_addr ); // 例化 LPM_RAM_IO 同步单口 RAM lpm_ram_io0 u_ram ( .data (sample_data), .address (rd_addr), // 单口 RAM 只有一套地址端口 .wren (wr_en), .clock (clk), .q (ram_q) ); always (posedge clk or negedge rst_n) begin if (!rst_n) begin wr_addr 10d0; wr_en 1b0; end else begin wr_en 1b0; if (sample_done) begin wr_en 1b1; wr_addr wr_addr 1b1; if (wr_addr 10d1023) wr_addr 10d0; // 写满后回绕避免地址越界 end end end endmodule注意这里 RAM 的 address 端口在写场景下接的是 rd_addr这是单口 RAM 的固有限制读写共用一个地址总线示例代码里为了示意把写地址与读地址分开声明实际工程中需要在顶层做一个二位选择器写阶段把wr_addr送上 address读阶段切到rd_addr。这个切换逻辑如果漏掉数据会写进错误的地址读出来的序列和采样时刻对不上。RAM 的q读出一拍延迟也是一个关键点读地址切换后ram_q上还是上一个地址的内容联调时看到第一个数据不对不要急着怀疑 ADC先查读时序。5. ModelSim 验证采样完成信号与 RAM 写使能的竞争处理5.1 用 testbench 模拟 ADC 行为验证这套采集链路建议直接建一个模拟 ADC0809 行为的 testbench关键是把 EOC 的行为建模对。EOC 在收到启动脉冲后拉低经过约 70 个 adc_clk 周期后拉高同时把数据总线驱动为一个固定值这样采样控制模块的输出sample_out应当能稳定复现这个固定值reg eoc_tb; reg [7:0] db_tb; reg [6:0] conv_cnt; always (posedge adc_clk or negedge rst_n) begin if (!rst_n) begin eoc_tb 1b1; conv_cnt 7d0; db_tb 8hA5; end else if (adc_start_tb) begin eoc_tb 1b0; conv_cnt 7d0; db_tb 8hA5; end else if (!eoc_tb) begin conv_cnt conv_cnt 1b1; if (conv_cnt 7d70) begin eoc_tb 1b1; // 转换完成EOC 拉高 db_tb 8hA5; end end end这段代码里db_tb刻意固定为 0xA5是为了在波形里一眼看出采样数据是否在正确时刻被锁存。用 adc_clk 沿驱动 EOC 是近似建模真实芯片的 EOC 相对 adc_clk 是异步的所以我建议在实际验证时把eoc_tb的翻转位置前后各偏 1ns比如#1 eoc_tb 1b1模拟真实异步抖动这样能暴露同步逻辑里的临界问题。验证步骤按这个顺序走先看 ale 和 adc_start 的高电平宽度是否大于 100ns再看 eoc_pos 脉冲是否出现在 EOC 拉高后的第一个系统时钟沿最后确认sample_out在done之前一拍已经稳定。把这三步跑完采样控制链路基本可信。5.2 容易踩的坑写使能比数据早到半个周期这个坑我在实际调试中踩过。adc_ctrl 的done和sample_out在 READ 状态同一拍产生但sample_out adc_db是非阻塞赋值它的新值要到当前时间步结束才生效。如果下游把done直接接 RAM 的wrenRAM 在同一时钟沿看到的data端口可能还是旧值写进去的就是上一笔数据或全零。解决办法是给done打一拍再送wren保证数据先稳定再写入reg done_r; always (posedge clk or negedge rst_n) begin if (!rst_n) done_r 1b0; else done_r done; // done 延迟一拍 end assign wr_en done_r;这个done_r代替原来的done去触发 RAM 写使能数据总线和写使能之间的竞争窗口就消失了。如果采样回来的数值总比实际偏大或偏小且规律性很强除了这个写使能问题还应该检查 ADC 的参考电压引脚FPGA 侧再在后处理里加一个 8 点滑动窗口滤波用移位寄存器和加法树实现即可对 7kHz 采样率来说资源开销几乎可以忽略效果比单纯调 RC 滤波直观得多。先把时序对齐再谈滤波顺序不能反。本文还有配套的精品资源点击获取