简介:ACM9767双通道高速14位ADC芯片与Cyclone IV FPGA结合的数据采集Verilog例程工程包,面向FPGA开发者、数字信号处理学习者及电子竞赛备赛人员。资源完整包含Quartus工程文件,以Verilog源码为核心,涵盖ADC数据采集、DDS信号发生及DAC输出链路,并带有AD9767_AD9226_DDS模块的引脚分配、时序约束与仿真文件,适合快速上手高速ADC驱动与FPGA数据采集系统设计。包内共有528个文件,涉及v/tdf源码、qpf/qsf工程配置、sdo/vo仿真输出、rpt报告及sof烧写文件等,工程结构完整,压缩包仅17.32MB。目前已有444人学习下载。通过阅读代码与工程,可理解ADC时钟管理、数据读取、DAC写时序等关键逻辑,也能参考其顶层模块划分和Quartus工程组织方式,为自主实现高速数据采集、波形发生等功能提供可直接修改的基础框架。 搞FPGA数据采集有一段时间了,最近正好把一个ACM9767双通道高速14位ADC搭配Cyclone IV FPGA的完整工程整理出来,含Verilog例程源码和Quartus工程文件。这中间踩了不少坑,也总结了一些可以直接抄作业的经验,写出来给同样在折腾高速采集的朋友做个参考。
1. 项目概述与整体设计思路
1.1 这套方案到底解决了什么问题
先捋一下这套系统的定位。ACM9767是双通道、14位分辨率的高速ADC芯片,搭配Cyclone IV系列FPGA(EP4CE系列)做数据采集,本质上是一条完整的信号链:模拟信号输入 -> ADC量化 -> FPGA接收与缓存 -> 数据输出(串口/并口/SRAM等)。很多场景都会用到这个组合:电机振动监测、音频频谱分析、电源纹波测量、软件无线电前端等等。
选ACM9767而不是那些嵌入式MCU自带的ADC,核心原因是速度和精度的平衡。MCU内置ADC通常12位、采样率几Msps到头,而ACM9767能做到更高采样率,14位分辨率在动态范围上也比12位实用不少。搭配FPGA而非直接用MCU读ADC,是因为FPGA擅长并行处理,可以同时抓两个通道的数据、做实时预处理(比如滑动平均、FFT前的数据搬移),不会像MCU那样频繁中断导致丢数。
1.2 为什么选Cyclone IV而不是更新的器件
很多新手一上来就要上最新的Cyclone 10或者Artix-7,其实对于学数据采集来说完全没必要。Cyclone IV(EP4CE系列)现在非常便宜,开发板一两百块就能搞定,Quartus II 13.0/13.1对它的支持非常成熟,资料满天飞,遇到问题一搜就有解决方案。更重要的是,ACM9767的输出速率对Cyclone IV来说绰绰有余——EP4CE6的全局时钟能跑到两百多MHz,FIFO和RAM资源也够用,完全不会成为瓶颈。
我在这个项目里用的是EP4CE6F17C8,逻辑单元6K左右,片内RAM大概270Kbit,对于双通道14位ADC的数据缓存和简单处理来说足够了。如果你预算多一点点,EP4CE10/EP4CE15资源更宽裕,后面想做FFT或者DDS叠加处理也不至于太挤。这套工程的代码在这几个型号上可以直接编译,只需改一下引脚约束。
提示:选FPGA型号时不要只看价格和资源,还要看封装。F17封装是BGA,焊接麻烦;如果是自己在洞洞板上飞线调试,建议选QFP封装的EP4CE6E22C8。
2. 核心细节解析:ADC接口时序与FPGA端设计
2.1 ACM9767的关键时序参数
拿到任何ADC芯片,第一件事就是看数据手册里的时序图,而不是急着写代码。ACM9767的输出是并行CMOS电平,每个采样周期在转换时钟(CLK)的驱动下,数据线D[13:0]上更新一次采样结果。关键时序参数有三个:
- 转换时钟频率(CLK):决定采样率,典型配置下可以跑到几十Msps级别。
- 数据建立时间(t_setup):CLK边沿之后数据线稳定的时间。
- 数据保持时间(t_hold):CLK边沿之后数据保持有效的时间。
如果你在FPGA里用CLK的上升沿直接抓数据,有可能会采到数据线的毛刺或跳变沿附近的不稳定状态,这时候就要考虑用PLL把采样时钟相位偏移半个周期,或者用双沿采样(DDR)去抓。实测下来,对ACM9767这种并行输出芯片,最稳的方法是把ADC的CLK输入到FPGA,然后内部PLL生成一个相位偏移约90度的时钟去采数据,能稳稳地采到有效数据。
2.2 ADC数据格式与二进制码处理
14位ADC常见的输出码制有二进制补码和偏移二进制两种,ACM9767支持配置。这里很多人会栽跟头:直接把14位数据塞进16位寄存器后做有符号数运算,结果出来负数全不对。原因就是你忘了根据码制做符号扩展。
如果输出是偏移二进制,0x1FFF附近对应0V,0x0000对应负满量程,0x3FFF对应正满量程。FPGA处理时建议先转成补码再做运算:
wire signed [15:0] data_s = { {2{adc_data[13]}}, adc_data[13:0] } ^ 14'h2000;我习惯的做法是先用^ 14'h2000把偏移二进制转成补码,再做符号扩展。这样后续做数字滤波、FFT运算时,正负含义才对得上。
3. Verilog逻辑设计与核心模块实现
3.1 时钟管理与采样时钟生成
整个工程的时钟架构很重要。我建议这样设计:
- 板上50MHz晶振作为FPGA主时钟。
- 通过PLL生成两路时钟:一路给ADC提供采样时钟(可调频率),一路给FPGA内部逻辑做系统时钟(相位偏移90度,专门用于采ADC数据)。
用Altera的ALTPLL IP核来生成时钟,配置起来非常简单,在Quartus里用MegaWizard向导几步就生成好了。核心参数设置:
输入时钟:50MHz 输出时钟c0:采样时钟(例如20MHz),直接输出到ADC CLK引脚 输出时钟c1:偏移90度后的采样时钟,用于FPGA内部数据采集逻辑为什么偏移90度是经验值?因为ADC的数据输出相对于采样时钟边沿会有固定的延迟(t_od),数据手册上会给一个范围。用90度偏移采样,让数据采样的时刻落在数据稳定的当中位置,容差最大。如果偏移太小靠近边沿,容易采到毛刺;太大则可能逼近下一个数据的跳变沿。
3.2 ADC采样控制模块Verilog实现
采样控制模块的逻辑很简单:每个采样时钟的边沿,把14位数据寄存进来。但工程上要考虑到多通道的同步对齐。ACM9767双通道是同一个时钟驱动的,数据线是并行摆放的,所以两个通道天然同步,不需要额外对齐逻辑。真正容易出问题的是接下来数据往FIFO里写的时候发生了跨时钟域。
我按下面的框架写了一个adc_capture模块:
module adc_capture( input wire clk_adc, // 相位偏移后的采样时钟 input wire rst_n, input wire [13:0] adc_ch0, input wire [13:0] adc_ch1, input wire capture_en, output reg [31:0] data_out, output reg data_valid ); reg [13:0] ch0_reg, ch1_reg; always @(posedge clk_adc or negedge rst_n) begin if (!rst_n) begin ch0_reg <= 14'd0; ch1_reg <= 14'd0; data_out <= 32'd0; data_valid <= 1'b0; end else if (capture_en) begin ch0_reg <= adc_ch0; ch1_reg <= adc_ch1; data_out <= { ch1_reg, ch0_reg }; // 高16位放通道1,低16位放通道0 data_valid <= 1'b1; end else begin data_valid <= 1'b0; end end endmodule这里有一个掩饰的设计点:为了简化跨时钟域问题,这套工程里我把采样、缓存和输出全部放到同一个时钟域里跑,也就是偏移90度那一路时钟。这样FIFO的写和读都在这一个时钟下,省掉了同步处理。代价是时钟频率不能太高,但对ACM9767 + EP4CE这个量级的数据采集来说,完全合适。
3.3 跨时钟域与FIFO缓存设计
如果你的系统更复杂,比如采样时钟200MHz,但后端是串口输出或USB接口,那就要面对跨时钟域问题了。最稳妥的办法是使用Altera的FIFO IP核,配置成“独立时钟”模式——写时钟用采样时钟域,读时钟用输出接口时钟域。
配置FIFO时,数据宽度可以设置成32位(双通道各16位,实际各用到14位),深度的话2048或4096就够用。深度太深浪费RAM,太浅容易溢出——这里给个经验公式:FIFO深度至少是采样率与输出速率之比的2倍,留出余量应对突发写入。比如20MHz采样、1Mbps串口输出、一帧数据1024点,那你需要至少1024个深度的缓冲,建议用2048。
用FIFO IP的好处是内部已经处理好了读写指针的格雷码同步,不会产生亚稳态问题。我一开始图省事自己写了个简单FIFO,结果上板测的时候偶尔出现数据错位,后来排查发现是读写指针跨时钟域同步没做干净,换成IP核后问题立刻消失。这类基础设施性质的东西,真的没必要重复造轮子,除非你要学原理。
4. Quartus工程搭建与调试实录
4.1 工程配置与引脚约束
搭建Quartus工程时,我强调一个习惯:从一开始就把引脚约束写好,不要过几天再补。尤其是ADC这种并行接口,十几个引脚,如果随意分配,布线会非常混乱,甚至出现两个信号互相串扰导致数据读不对。
我的工程里主要引脚分配如下:
| 信号 | FPGA引脚 | 说明 |
|---|---|---|
| clk_50m | PIN_R8 | 板载50MHz晶振 |
| adc_clk | PIN_E1 | 输出给ADC的采样时钟 |
| adc_d[13:0] | PIN_E3...PIN_E15 | ADC并行数据线 |
| adc_clk_in | PIN_C1 | ADC回传的时钟/锁存信号 |
| uart_tx | PIN_D1 | 串口发送 |
引脚约束文件(.qsf)里写清楚每个引脚的IO标准和电流强度,ADC数据线建议全部设为同一种IO标准(如3.3V LVCMOS),避免不同电压域转换带来的问题。
这里特别提醒一下:ACM9767的电源要单独做好去耦。我第一版PCB没注意模拟电源和数字电源分开,结果采集的数据低两位一直在跳,看起来像是ADC不行,实际上是电源噪声惹的祸。模拟电源AVDD和数字电源DVDD在走线时要用磁珠或0欧电阻隔离,每个电源引脚附近放104和10uF去耦电容。
4.2 SignalTap抓数:验证数据对不对的关键
Quartus自带的SignalTap II逻辑分析仪是调试FPGA内部信号的神器。这个项目里,我把adc_capture模块里的adc_ch0、adc_ch1、data_valid信号全部拉进SignalTap里面观察。
实际抓数时有个非常实用的技巧:设置SignalTap的触发条件为data_valid上升沿,然后让采集模块运行,抓完一帧数据后停下来静态查看。如果数据是正弦波输入,理论上你看到的波形应该是光滑的正弦曲线,码值在某个范围内均匀变化。如果看到的是杂乱无章的跳动,基本可以确定是时序问题或者接线问题。
SignalTap的配置步骤:
- 在Quartus菜单里打开SignalTap II Logic Analyzer。
- 添加需要观测的信号(注意要选post-fitting,否则综合后会找不到信号)。
- 设置采样时钟为偏移后的采样时钟(和adc_capture模块同一时钟)。
- 设置采样深度(Sample depth),一般256或512足够。
- 编译工程,下载到FPGA,运行SignalTap。
实测下来,SignalTap能抓到的信号最高频率取决于工程布局布线质量。如果采样时钟超过100MHz,SignalTap可能不太好抓,这时候就要把观测点换成降频后的信号,比如在FIFO读侧加一个计数器分频,再抓分频后的数据。
4.3 Modelsim仿真:上板前先把逻辑跑通
上板之前,我强烈建议先做一次仿真,哪怕是很简单的testbench也行。在这个工程里,testbench的主要工作就是模拟ADC在采样时钟驱动下输出数据的时序,看FPGA能不能正确采到。
写testbench的核心是模拟ACM9767的输出行为:
always @(posedge adc_clk) begin // 模拟ADC数据输出,这里用一个可配置的序列 adc_ch0 <= adc_ch0 + 14'd1; // 模拟斜坡信号 adc_ch1 <= adc_ch1 - 14'd1; end仿真时重点观察adc_capture模块的data_out是否在data_valid有效时保持了正确的数据。Modelsim本身是Quartus Prime自带可用的(ModelSim-Altera版),不需要额外安装破解。
我第一次仿真时,发现data_valid有效后data_out的数据总是不对,后来才意识到问题出在testbench里我给了adc_ch数据后,没有考虑到capture的时序窗口——adc_capture是在时钟上升沿采数据的,而testbench的数据变化也是上升沿,相当于数据还在跳变时就被采到了。解决办法是让testbench的数据沿延迟半个时钟周期再变化,模拟真实器件的数据建立时间。
5. 常见问题与排查技巧实录
5.1 数据全0或全1
出现这种情况先不要怀疑FPGA,先拿万用表量一下ADC的电源和参考电压是否正常。ACM9767的参考电压如果没接好,数据会固定输出全0或全1。实测中我遇到过一次,是板子上参考电压芯片虚焊了,补焊后立即恢复正常。如果电源正常,再看ADC的片选/使能引脚有没有拉对电平。很多ADC芯片是有PD(Power Down)引脚的,如果高电平有效而你恰好没拉低,芯片就一直处于掉电模式,什么都不输出。
5.2 数据错位或周期性跳变
如果在SignalTap里看到数据的低两位周期性跳变(比如每64个点跳一次),大概率是电源噪声问题或者触发了地弹效应。特别是ADC数字输出总线翻转时,瞬间电流变化会通过地回路串到模拟部分。这种情况下除了加强电源去耦,还可以尝试降低ADC输出驱动强度(如果芯片支持配置的话),减缓边沿速度,减小噪声耦合。
5.3 采样率上不去,时序收敛失败
当采样时钟频率提高后,Quartus编译可能会报时序不满足约束。这种问题分两种原因:一是时钟约束没写对,二是逻辑路径确实太长。先检查SDC约束文件里是否正确约束了采样时钟;如果是逻辑路径太长,就得在adc_capture的寄存器之间插流水线——把组合逻辑拆成两拍来完成。
5.4 串口收到乱码
如果数据从FPGA经过UART输出到PC上出现乱码,先不要怀疑ADC,八成是UART的波特率误差累计了。FPGA端用50MHz时钟生成115200波特率,整数分频是434,误差很小可以忽略。但如果用非整数分频比如400,误差就大了,传输多字节后必然乱码。实际计算一下:50MHz / 115200 ≈ 434.027,取434后实际波特率为115207,误差0.006%,完全没问题。如果你用的是12MHz之类的非标时钟,就要仔细算了,误差超过2%就很容易出错。
6. 采集数据的进一步利用
6.1 从原始码值到真实电压的换算
采集到14位数据后,如果要做上位机显示或分析,得先知道每一个码值对应多少电压。这个方法非常基础但容易算错:14位ADC的满量程一般是参考电压的两倍,即输入范围是-REF到+REF。如果一个码(LSB)对应的电压是:
V_lsb = (2 * Vref) / 2^14假设Vref = 2.5V,则V_lsb = 5V / 16384 ≈ 0.305mV。偏移二进制码值N对应的电压为:
V = (N - 8192) * V_lsb比如N = 0,则V = -2.5V;N = 8192,V = 0V;N = 16383,V ≈ 2.4997V。这个计算在写上位机时要弄清楚,否则显示出来的波形会整体偏一个直流偏置。
6.2 上位机串口显示与数据后处理
我做这套工程时,上位机只是用串口助手加一个小脚本做数据解析。推荐一个简单方案:FPGA端每采满1024个点(每个点两字节,双通道共4KB)自动打包通过UART发送,上位机用Python的pyserial读取并解析,用matplotlib实时画图。
如果你有更高要求,比如需要做FFT看频谱,建议直接把原始数据存成文件,再用Python或MATLAB离线分析,比在FPGA里硬写FFT简单得多——当然,如果你是冲着学习FPGA图像处理或实时频谱去的,那可以后续自己把FFT IP核加进来,作为这个项目的进阶方向。
7. 工程文件说明与使用建议
最后把这套工程的目录结构简单说一下,方便大家拿到源码以后快速上手:
├── acm9767_top.v // 顶层模块:实例化PLL、adc_capture、FIFO、UART ├── adc_capture.v // ADC数据采集模块 ├── uart_tx.v // 串口发送模块 ├── pll.v // ALTPLL生成的时钟管理模块 ├── fifo.v // FIFO IP核 ├── acm9767_top.qsf // Quartus工程文件(含引脚约束) ├── acm9767_top.sdc // 时序约束文件 ├── simulation/ │ ├── tb_acm9767_top.v // testbench │ └── wave.do // Modelsim波形脚本拿到工程后的操作顺序建议是这样的:
- 先用Quartus II 13.1打开工程,编译一遍,确认能综合通过。
- 用ModelSim跑一下仿真,看adc_capture的时序波形是否正确。
- 确认板卡型号和引脚分配后,修改.qsf里的引脚约束。
- 下载到FPGA,用SignalTap抓实际信号,对比仿真结果。
- 接上真实信号源(低频信号发生器等),验证ADC采样结果。
另外提醒一下:如果你是刚接触FPGA,Quartus II的安装也容易卡住。Quartus II 13.1在Windows 10/11上安装时,USB-Blaster驱动在Win10及以上需要手动指定驱动路径,默认的驱动安装方式在Win10上会提示“无法验证发布者”,这时选择“仍要安装”就行。安装完以后记得把器件库选上Cyclone IV,不然编译时会提示找不到器件。这个版本对EP4CE系列支持得非常完善,比新版Quartus Prime更稳定兼容。
这个项目做到后面,我最大的体会是:数据采集链路看起来简单,实际上每个环节都可能暗藏问题——芯片配置、时序约束、电源完整性、跨时钟域处理,每一处都得认真对待。如果你的目标是把ACM9767跑起来并且稳定采集数据,这套工程完全可以作为起点。后续可以根据实际需求扩展,比如加大FIFO深度做更长时间的记录,或加上双通道的幅值校准系数,让两个通道的一致性更好。
本文还有配套的精品资源,点击获取