简介:这是一套基于ACM9767双通道高速14位ADC与Cyclone IV FPGA的数据采集Verilog例程工程,面向FPGA开发学习者、数字信号采集方向的研究者及电子竞赛备赛人员,可直接在Quartus环境中打开工程参考设计。资源涵盖AD9767_AD9226_DDS等模块源码,包含ADC/DAC时钟与写控制信号、DDS逻辑等关键代码,可帮助理解高速ADC与FPGA的接口时序及数据通路搭建。压缩包内共528个文件,约17.32MB,主要包括.cdb/.hdb/.ddb等Quartus工程数据库文件、.v/.tdf等Verilog/Tcl设计源文件、.qsf/.qpf工程配置文件,以及.sdo/.vo/.rpt等仿真与报告文件,便于学习工程结构。目前已有444人学习浏览。整体资料结构完整,既适合入门者对照工程了解ADC采样流程,也可供进阶开发者在此基础上扩展多通道采集或DDS信号发生功能。 做高速数据采集,绕不开FPGA。最近我基于ACM9767双通道高速14位ADC芯片,搭配一片Cyclone IV搭建了一套双通道数据采集系统,把Verilog例程、Quartus工程文件从零到尾跑通了一遍,采样率稳定跑在125MSPS。中间踩了不少坑,也沉淀了一些调试经验——尤其是ADC与FPGA之间的时序配合、跨时钟域处理、SignalTap抓数这几个环节,网上资料零散,能一次讲透的不多。这篇文章里我把完整的设计思路、顶层架构、核心Verilog代码片段、Quartus工程配置流程和调试排障过程都整理出来,给正在做ADC+FPGA采集方案的朋友当个参考。
1. 项目概述与方案选型:为什么是ACM9767 + Cyclone IV
1.1 拿到这个项目,我先拆了什么
这个项目的需求其实很简单:双通道、14位、高速采集,前端模拟信号进来,FPGA把数据接住,缓冲之后再交给后续处理或传输。需求一句话就能说完,但真落地的时候,每一环都有讲究。我拿到需求后拆成了三块:模拟前端能不能匹配输入信号的范围和带宽,ADC芯片怎么配置才能工作在最佳状态,FPGA这边怎么把高速并行数据稳定收进来。
先说芯片。ACM9767是双通道、14位、采样率最高125MSPS的ADC模块,内置基准、支持差分或单端输入,输出是14位并行CMOS数据线加采样时钟输出。对这个项目来说,14位分辨率对应约84dB的理论信噪比上限,双通道能覆盖I/Q或者左右声道这类需要同步采样的场景,125MSPS对中频采集、超声、通信基带这类应用都够用。也就是说,这颗芯片在性能和性价比之间踩在了一个比较舒服的位置。
再说FPGA。Cyclone IV家族我选了EP4CE15F17C8这一颗,逻辑单元约15K,片上RAM约504Kbit,内置两个PLL,可用引脚充足。高速采集这类任务,逻辑量其实不大——核心就是采集、缓冲、传输,真正吃资源的是FIFO和后续处理逻辑,Cyclone IV完全可以扛住。选它的另一个原因是Quartus II 13.0sp1对Cyclone IV支持非常成熟,License免费,适合快速原型验证,成本也比Zynq、Kintex这类平台低一个量级。
1.2 芯片选型与FPGA选型的匹配逻辑
选型这件事,最容易犯的错是“性能堆料”。很多朋友一上来就想着上高端FPGA,其实对数据采集这种场景,瓶颈往往不在FPGA算力,而在接口时序和存储带宽。ACM9767的14位并行总线,在125MSPS下数据率是1.75Gbps(双通道),Cyclone IV的IO带宽完全能承接。真正需要care的是:
- 采样时钟与FPGA捕获时钟的关系:ACM9767会输出随路时钟(DCO),FPGA必须用这个时钟去采数据,而不是自己随便生成一个时钟去对齐,否则必然出现亚稳态或数据错位。
- I/O Bank电压匹配:ACM9767的IO电平是3.3V CMOS,Cyclone IV的Bank电压必须配置为3.3V,同时注意Bank隔离,避免模拟地和数字地串扰。
- 片上资源余量:FIFO深度、后续FFT或滤波逻辑、可能的调试模块,这些都要提前估算。我当时留了约30%的逻辑余量,以防调试过程中要加逻辑分析仪或调试模块。
这些匹配逻辑,是决定项目能不能顺利跑起来的地基。下面我会从接口信号开始,逐个讲清楚我怎么设计和实现的。
2. ADC接口分析与FPGA引脚规划
2.1 ACM9767的接口信号到底长什么样
ACM9767(以及同类的并行高速ADC模块)对外主要提供以下几组信号:
| 信号 | 方向 | 说明 |
|---|---|---|
| CLK | 输入 | 采样时钟输入,FPGA通过PLL产生 |
| D[13:0](通道A/B) | 输出 | 14位并行采样数据 |
| DCO/随路时钟 | 输出 | 数据有效时钟,FPGA用它捕获数据 |
| CS、SCLK、SDIO | 输入 | 串行配置接口,用于写入寄存器 |
| PDWN | 输入 | 省电模式控制 |
这里有个关键点:很多并行ADC的输出数据和随路时钟之间是存在相位关系的,有的芯片是数据在DCO上升沿稳定,有的是下降沿,还有的是DDR模式(上下沿都采)。ACM9767默认是SDR模式,数据在DCO上升沿有效,但为了保险,我建议在FPGA侧做个可选的边沿反转寄存器,用SignalTap实测之后再做最终确定,不要想当然。
另外,串行配置接口走的是类似SPI的时序:CS拉低、按位写入、CS拉高锁存。不同的芯片寄存器地址和位定义不一样,我建议把配置序列做成一个parameter数组,方便调试时临时修改某个位而不影响整体结构。
2.2 引脚分配与走线的实战注意事项
引脚分配这一步,很多人会随手在Quartus里自动布局,但在高速采集项目里我还是建议手动规划,重点考虑几个因素:
- 数据线分组集中:通道A的14根数据线尽量放在同一个Bank的相邻引脚,通道B同理,这样可以减少走线交叉和长度差异。
- DCO与数据线在同一Bank:随路时钟必须和数据线在同一Bank,这样时钟偏斜最小,也方便设置同一组IO Standard。
- 关键控制引脚单独放置:CS、SCLK、SDIO这些配置信号速率低,不敏感,放在哪个空闲引脚都行,但不要和高速数据线靠太近,避免串扰。
我当时的分配思路是:Bank 3给通道A数据,Bank 4给通道B数据,DCO分别就近接入对应Bank的专用时钟引脚(或普通IO,但要走全局时钟网络),CLK采样时钟从FPGA的PLL输出引脚送到ADC。这块需要注意的是,信号完整性不是靠“看起来整齐”就行的,如果PCB允许,数据线之间加地线隔离,DCO和数据线做等长布线——如果只是做实验板,至少要在逻辑上保证同一通道数据线延迟基本一致。
3. Verilog逻辑设计:四大核心模块逐个拆解
3.1 顶层模块与时钟树设计
整个工程的顶层思路很清晰:一个顶层模块把时钟管理、ADC配置、数据采集、FIFO缓冲和输出接口串起来。我用的是典型的模块化写法,每个模块只管一件事,接口用标准握手信号。这样做的好处是,后续替换芯片或者增加处理逻辑时,改动面能控制在最小范围。
时钟树是这个设计的核心。我在Cyclone IV的PLL里生成了三路时钟:
- adc_clk:125MHz,送给ADC作为采样时钟。
- fifo_wr_clk:由ADC的DCO输入到FPGA后再经过BUFG(全局时钟缓冲)得到,用于数据采集中所有寄存器的捕获时钟。
- fifo_rd_clk:50MHz,用于数据读取侧(比如UART或USB接口)。
这里有一个非常关键的思路:采样数据和采样时钟是同源的,所以捕获逻辑必须完全跑在DCO域,不能混用系统时钟。否则每次上电看到的都是一堆错位数据。交叉到读取侧时,用异步FIFO做跨时钟域转换,这件事不能省。
顶层模块的Verilog骨架大致是这样:
module top_acm9767 ( input clk_50m, input rst_n, // ADC 并行数据 input [13:0] ch_a_data, input [13:0] ch_b_data, input adc_dco, // ADC 采样时钟 output adc_clk, // ADC 配置接口 output adc_cs_n, output adc_sclk, output adc_sdio, // 数据输出接口 output uart_tx ); wire pll_locked; wire adc_clk_int; wire dco_bufg; wire adc_cfg_done; wire [13:0] ch_a_cap; wire [13:0] ch_b_cap; wire fifo_wr_en; wire [31:0] fifo_wr_data; wire fifo_rd_en; wire [31:0] fifo_rd_data; // PLL: 生成 adc_clk(125MHz) pll_adc u_pll ( .inclk0 (clk_50m), .c0 (adc_clk_int), .locked (pll_locked) ); assign adc_clk = adc_clk_int; // DCO 全局时钟缓冲 BUFG u_bufg_dco (.I(adc_dco), .O(dco_bufg)); // ADC 配置模块 adc_config #( .CLK_DIV (50), .CFG_WORDS (4) ) u_adc_cfg ( .clk (clk_50m), .rst_n (rst_n & pll_locked), .cfg_done (adc_cfg_done), .cs_n (adc_cs_n), .sclk (adc_sclk), .sdio (adc_sdio) ); // 数据采集模块(DCO 域) adc_capture u_capture ( .clk (dco_bufg), .rst_n (rst_n & adc_cfg_done), .ch_a (ch_a_data), .ch_b (ch_b_data), .data_out (fifo_wr_data), .wr_en (fifo_wr_en) ); // 异步 FIFO 跨时钟域 fifo_adc u_fifo ( .aclr (~rst_n), .wrclk (dco_bufg), .wrreq (fifo_wr_en), .data (fifo_wr_data), .rdclk (clk_50m), .rdreq (fifo_rd_en), .q (fifo_rd_data), .rdempty (fifo_empty), .wrfull (fifo_full) ); endmodule注意几个细节:
- PLL锁定信号没锁定之前,不能给ADC配置。我在复位链里把pll_locked放在了rst_n的与逻辑里,这样保证PLL稳定后再释放复位。
- DCO进FPGA后必须过BUFG,进入全局时钟网络,保证扇出和偏斜可控。
- 异步FIFO在Cyclone IV上推荐直接用Quartus的FIFO IP,比自己用双口RAM写格雷码省心得多,而且时序更可靠。
3.2 ADC配置模块:串行寄存器写入
ACM9767内部寄存器是通过串行接口配置的。配置模块实现起来不复杂,核心是一个状态机加一个移位寄存器。我把需要写入的配置值存在parameter数组里,每个配置字按[地址位+数据位]拼接成固定宽度,状态机依次“CS拉低 → 移位输出 → CS拉高 → 等一小段间隔”完成一条写入。
module adc_config #( parameter CLK_DIV = 50, parameter CFG_WORDS = 4, parameter CMD_WIDTH = 16 ) ( input clk, input rst_n, output reg cfg_done, output reg cs_n, output reg sclk, output reg sdio ); // 配置表: {地址, 数据} localparam [CFG_WORDS*CMD_WIDTH-1:0] CFG_TABLE = { 16'h0001, // 0x00 <- 0x01 复位/使能 16'h0110, // 0x01 <- 0x10 工作模式 16'h0200, // 0x02 <- 0x00 时钟分频 16'h0303 // 0x03 <- 0x03 输出格式 }; reg [7:0] cnt; reg [3:0] bit_cnt; reg [3:0] word_idx; reg [15:0] shift_reg; localparam IDLE = 2'd0, START = 2'd1, SHIFT = 2'd2, STOP = 2'd3; reg [1:0] state; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin cs_n <= 1'b1; sclk <= 1'b0; sdio <= 1'b0; cfg_done <= 1'b0; state <= IDLE; word_idx <= 0; end else begin case (state) IDLE: begin cfg_done <= 1'b0; if (word_idx < CFG_WORDS) begin shift_reg <= CFG_TABLE[CMD_WIDTH*(word_idx+1)-1 -: CMD_WIDTH]; bit_cnt <= 0; cnt <= 0; state <= START; end else begin cfg_done <= 1'b1; end end START: begin cs_n <= 1'b0; // 拉低片选 sclk <= 1'b0; if (cnt < CLK_DIV-1) cnt <= cnt + 1; else begin cnt <= 0; state <= SHIFT; end end SHIFT: begin sclk <= 1'b1; // 上升到沿送数 sdio <= shift_reg[15]; if (cnt < CLK_DIV-1) cnt <= cnt + 1; else begin cnt <= 0; shift_reg <= {shift_reg[14:0], 1'b0}; if (bit_cnt == 15) begin bit_cnt <= 0; sclk <= 1'b0; state <= STOP; end else begin bit_cnt <= bit_cnt + 1; sclk <= 1'b0; end end end STOP: begin cs_n <= 1'b1; // 拉高片选, 锁存 if (cnt < CLK_DIV-1) cnt <= cnt + 1; else begin cnt <= 0; word_idx <= word_idx + 1; state <= IDLE; end end endcase end end endmodule这个状态机的关键点在于每个SCLK周期之间插入足够多的空闲时钟,保证ADC端的SCLK建立时间。CLK_DIV参数按实际系统时钟调整,比如系统时钟50MHz,SCLK我控制在1MHz左右,CLK_DIV=50。实测下来这个速率非常稳定,配置时序也不容易出错。
注意:配置完成后,要把cs_n恢复为高电平并且在内部保持高,否则ADC可能误判进入其他模式。调试时如果发现输出数据全0或全1,先检查配置阶段是否真正跑完——我遇到过cfg_done已经拉高但最后一个CS拉高的间隔没过完的情况,导致复位释放太早,数据一直没起来。
3.3 数据采集与同步模块
数据采集模块的职责很简单:把14位数据按DCO时钟打进寄存器,通道A和通道B各一组,然后拼成32位写入FIFO。这里面的坑主要是数据与DCO的对齐关系。
我的做法是:先用默认的上升沿捕获,然后通过SignalTap观察。如果发现采集到的数据看起来像“低字节和高字节互换”或“数据跳变无序”,大概率是边沿选反了。处理办法是给采集模块加一个DCO_INVERT参数,用generate块生成两种边沿的捕获逻辑,调试时改一个参数重新综合就行,不用大改代码。
module adc_capture #( parameter DCO_INVERT = 0 ) ( input clk, // DCO(或者反转后的DCO) input rst_n, input [13:0] ch_a, input [13:0] ch_b, output reg [31:0] data_out, output reg wr_en ); reg [13:0] ch_a_r; reg [13:0] ch_b_r; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin ch_a_r <= 14'd0; ch_b_r <= 14'd0; data_out <= 32'd0; wr_en <= 1'b0; end else begin ch_a_r <= ch_a; ch_b_r <= ch_b; data_out <= {ch_a_r, ch_b_r}; wr_en <= 1'b1; end end endmodule这个模块很简单,但要注意一个细节:**写入FIFO的数据,最好在DCO域打一拍后再写,不要在组合逻辑里直接拼FIFO的数据端口,否则时序很容易出问题。**我上面代码里data_out本身是一个寄存器输出,再配合一个wr_en的寄存器输出,这两个信号是同拍产生的,FIFO的wrclk和数据总线建立时间关系稳定,用起来很省心。
3.4 跨时钟域FIFO缓存
为什么非要FIFO?因为DCO域是125MHz的采样节奏,而数据读取侧(比如UART、USB、以太网)是低速率且异步的,直接连过去必然丢数据。异步FIFO天然解决了跨时钟域和数据缓冲两个问题。
我在Quartus里用的IP配置参数:
| 参数项 | 配置值 | 说明 |
|---|---|---|
| Width | 32 bit | 双通道各14位,高4位可留作通道标志 |
| Depth | 1024 | 约8us缓冲时长的裕量 |
| Write Clock | dco_bufg | ADC随路时钟 |
| Read Clock | clk_50m | 读取侧系统时钟 |
| Overflow Protection | 默认 | FIFO满则不写,避免数据覆盖 |
如果用Altera的FIFO IP,记得在例化时把aclr接到复位信号上。这里有个容易犯的错:异步FIFO在跨时钟域时,格雷码指针同步是IP内部处理的,你不需要自己写,但如果用了rdempty和wrfull这两个状态信号去控制读写,就要搞清楚它们是同步到哪个时钟域的。读侧信号(rdempty)要用读时钟采样,写侧信号(wrfull)要用写时钟采样,不要搞混。
4. Quartus工程搭建与在线调试
4.1 工程创建与IP配置
我用的是Quartus II 13.0sp1,对应Cyclone IV系列。建工程的过程就不啰嗦了,只说几个容易踩坑的点:
- 芯片型号选择:注意区分EP4CE15F17C8和I8后缀,C表示商业级、I表示工业级,工程文件里如果选错型号,综合出的资源利用率和时序结果会有差异。
- PLL菜单配置:输入
inclk0接50MHz有源晶振,输出c0设为125MHz。注意Cyclone IV的PLL输出频率上限和VCO范围,125MHz在这个器件上完全OK,但如果你要跑更高速率,就得重新评估器件选型了。 - FIFO IP例化:直接搜索FIFO IP核,按上面表格参数填,注意勾选“Overflow Protection”和“Underflow Protection”,防止读写指针异常。
4.2 引脚约束与时序约束
引脚约束推荐用Tcl脚本或者.qsf文件直接编辑,界面点点点太慢。核心约束就两类:一是引脚位置和IO电平,二是时钟定义。
set_location_assignment PIN_24 -to clk_50m set_location_assignment PIN_120 -to adc_dco set_location_assignment PIN_121 -to ch_a_data[0] set_location_assignment PIN_122 -to ch_a_data[1] ... set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to adc_dco set_instance_assignment -name IO_STANDARD "3.3-V LVTTL" -to ch_a_data[*] ... create_clock -name adc_clk -period 8.000 [get_ports {adc_clk}] create_clock -name sys_clk -period 20.000 [get_ports {clk_50m}]这里给新手一个提示:数据线差一根没分配或者IO电平不一致,编译时不会报错,但下载到板子上就是采不到数据。所以做完引脚分配后,一定要在Pin Planner里核对一遍,尤其检查有没有多选、漏选,以及有没有同Bank混用了不同IO电平的信号。
时序约束方面,Cyclone IV的PLL IP生成后,Quartus会自动把PLL输出的时钟沿定义好,你只需要把外部时钟(clk_50m)定义清楚即可。DCO这个时钟虽然是ADC送进来的,但如果不需要做特别严格的时序分析,可以先把它设为False Path或者用derive_clock_uncertainty自动推导,否则Quartus会把DCO和内部时钟之间做全时序收敛,反而导致一堆虚警,影响你定位真问题。
4.3 SignalTap II实测抓数
调试阶段,SignalTap II是我最依赖的工具。配置步骤:
- 在工程里加入
.stp文件,选择时钟为dco_bufg(一定要选DCO域时钟作为采样时钟,不然抓出来的数据对不齐)。 - 添加要观察的信号——
ch_a_r、ch_b_r、fifo_wr_data、wr_en、adc_cfg_done等。 - 设置触发条件为
adc_cfg_done上升沿,采样深度1024,模式设为连续。 - 重新编译,下载,启动SignalTap,给ADC输入端加上一个已知的正弦波信号。
实测最典型的结果应该是:抓到的数据呈现出连续的正弦波纹理。如果看到的是“全0、全1、乱码、稳定但数值跳动无规律”,那基本可以断定问题出在边沿对齐或复位时序上,而不是代码逻辑本身。这个“用已知信号验证未知链路”的调试思路,是我做高速采集项目最推荐的切入点。
5. 调试实录:常见问题与排查心得
5.1 数据错位对不齐:先查边沿,再查引脚
我遇到的第一个问题就是数据错位。SignalTap抓出来的波形,正弦波存在,但出现了规律的“跳变”或“毛刺”,看起来像是数据在14根线上串位了。排查过程是这样的:
- 先用SignalTap对比DCO上升沿和DCO下降沿捕获的数据——发现上升沿下数据有毛刺,下降沿下数据是干净的。说明ACM9767的数据在DCO下降沿附近更稳定,于是把
DCO_INVERT参数设为1,问题直接消失。 - 如果翻转边沿后还是错位,那就用SignalTap把所有14根数据线单独抓出来看,如果某一位数据和相邻位之间有明显的时间差,大概率是PCB走线长度差异太大。实验板环境下,我处理办法是在采集模块里对可疑位做延迟约束,或者干脆在FPGA内部把14位数据复制成两套,一套延时半拍,一套不延时,然后通过拨码开关选,看哪套数据对。
这招虽然土,但在没有示波器的条件下非常高效。工程上叫“软件对齐”,比反复改PCB快得多。
5.2 采样值跳变不稳定:电源和基准是关键
第二个问题是采集结果在固定输入下,数值在低位几个bit上乱跳,而且看起来不是白噪声那种随机跳,而是周期性跳变。排查后确认是ADC电源纹波造成的——我给ADC供的是一个普通的LDO,负载一拉起来纹波就到了几十毫伏。解决办法:
- 换成低噪声LDO,并在ADC电源引脚旁边加10uF+100nF双电容去耦。
- 基准电压引脚用单独的走线,不要和数字电源共地连续走线。
- 如果PCB已经定型,退而求其次的办法是在配置寄存器里打开芯片内部的数字滤波功能,实测低位跳变会明显收敛。
这几条经验其实适用于所有高速ADC项目,不局限于ACM9767。模拟部分不干净,后面FPGA再怎么处理也是白搭。
5.3 上电初始化偶发失败:关注复位释放时机
第三类问题最隐蔽:上电后有时能采到数据,有时采不到,复位按键按一下就好了,非常不稳定。后来我用SignalTap挂在adc_cfg_done上看,发现偶尔配置模块压根没有正常结束。定位过程:
- 配置模块工作时钟是
clk_50m,复位信号是rst_n & pll_locked。问题在于,如果PLL锁定后配置模块立刻开始跑,但此时ADC端还没有上电完成,CS/SCLK的电平可能还没稳定。 - 解决方法是:在PLL锁定后再加一个硬件稳定延时,比如用计数器延时10ms再把复位释放。
- 同时把SDIO引脚在配置模块空闲时固定拉高,避免悬空误触发。
改了这两处之后,连续上电50次没有再复现过。这类问题在文档里很难找到标准答案,只能靠时序分析和耐心验证。
到这里,整个ACM9767 + Cyclone IV双通道数据采集系统的设计和调试过程就都交代清楚了。我个人在这套设计中最大的体会是:高速数据采集的难点80%不在代码量,而在“时钟和边沿”这两个细节上。DCO域独立、FIFO跨时钟域、边沿对齐可配置、复位释放刻意延时——把这些基础打牢,后面的处理逻辑都是锦上添花。如果你正在做类似的项目,建议先不要急着写业务逻辑,花半天时间把采集链路的信号完整性验证清楚,后面能省下数倍的排查时间。最后再分享一个小技巧:把配置参数表、DCO边沿选择、FIFO深度这些都做成parameter或localparam放在模块顶层,调试阶段改参数比改代码舒服太多。
本文还有配套的精品资源,点击获取