ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于ACM9767与Cyclone IV的双通道高速数据采集系统设计与调试

2026/9/8 10:55:58 拓冰建站 浏览量
基于ACM9767与Cyclone IV的双通道高速数据采集系统设计与调试 简介这是一套基于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工程配置流程和调试排障过程都整理出来给正在做ADCFPGA采集方案的朋友当个参考。1. 项目概述与方案选型为什么是ACM9767 Cyclone IV1.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会输出随路时钟DCOFPGA必须用这个时钟去采数据而不是自己随便生成一个时钟去对齐否则必然出现亚稳态或数据错位。I/O Bank电压匹配ACM9767的IO电平是3.3V CMOSCyclone 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_clk125MHz送给ADC作为采样时钟。fifo_wr_clk由ADC的DCO输入到FPGA后再经过BUFG全局时钟缓冲得到用于数据采集中所有寄存器的捕获时钟。fifo_rd_clk50MHz用于数据读取侧比如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 { 16h0001, // 0x00 - 0x01 复位/使能 16h0110, // 0x01 - 0x10 工作模式 16h0200, // 0x02 - 0x00 时钟分频 16h0303 // 0x03 - 0x03 输出格式 }; reg [7:0] cnt; reg [3:0] bit_cnt; reg [3:0] word_idx; reg [15:0] shift_reg; localparam IDLE 2d0, START 2d1, SHIFT 2d2, STOP 2d3; reg [1:0] state; always (posedge clk or negedge rst_n) begin if (!rst_n) begin cs_n 1b1; sclk 1b0; sdio 1b0; cfg_done 1b0; state IDLE; word_idx 0; end else begin case (state) IDLE: begin cfg_done 1b0; if (word_idx CFG_WORDS) begin shift_reg CFG_TABLE[CMD_WIDTH*(word_idx1)-1 -: CMD_WIDTH]; bit_cnt 0; cnt 0; state START; end else begin cfg_done 1b1; end end START: begin cs_n 1b0; // 拉低片选 sclk 1b0; if (cnt CLK_DIV-1) cnt cnt 1; else begin cnt 0; state SHIFT; end end SHIFT: begin sclk 1b1; // 上升到沿送数 sdio shift_reg[15]; if (cnt CLK_DIV-1) cnt cnt 1; else begin cnt 0; shift_reg {shift_reg[14:0], 1b0}; if (bit_cnt 15) begin bit_cnt 0; sclk 1b0; state STOP; end else begin bit_cnt bit_cnt 1; sclk 1b0; end end end STOP: begin cs_n 1b1; // 拉高片选, 锁存 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参数按实际系统时钟调整比如系统时钟50MHzSCLK我控制在1MHz左右CLK_DIV50。实测下来这个速率非常稳定配置时序也不容易出错。注意配置完成后要把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 14d0; ch_b_r 14d0; data_out 32d0; wr_en 1b0; end else begin ch_a_r ch_a; ch_b_r ch_b; data_out {ch_a_r, ch_b_r}; wr_en 1b1; end end endmodule这个模块很简单但要注意一个细节**写入FIFO的数据最好在DCO域打一拍后再写不要在组合逻辑里直接拼FIFO的数据端口否则时序很容易出问题。**我上面代码里data_out本身是一个寄存器输出再配合一个wr_en的寄存器输出这两个信号是同拍产生的FIFO的wrclk和数据总线建立时间关系稳定用起来很省心。3.4 跨时钟域FIFO缓存为什么非要FIFO因为DCO域是125MHz的采样节奏而数据读取侧比如UART、USB、以太网是低速率且异步的直接连过去必然丢数据。异步FIFO天然解决了跨时钟域和数据缓冲两个问题。我在Quartus里用的IP配置参数参数项配置值说明Width32 bit双通道各14位高4位可留作通道标志Depth1024约8us缓冲时长的裕量Write Clockdco_bufgADC随路时钟Read Clockclk_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电源引脚旁边加10uF100nF双电容去耦。基准电压引脚用单独的走线不要和数字电源共地连续走线。如果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放在模块顶层调试阶段改参数比改代码舒服太多。本文还有配套的精品资源点击获取