FPGA数据采集链路怎么搭?ZYNQ PL驱动AD7606做FFT频谱分析全解析 简介ZYNQ PL采集AD7606数据并做FFT变换是基于Vivado的完整工程方案面向FPGA/SoC开发者与数字信号处理学习者解决从AD7606高速采样、接口时序控制、数据缓冲到FFT频域分析的全流程搭建问题。资源共163个文件压缩包约818KB以Vivado工程文件为主xci与bd定义IP核和块设计v源码实现RTL逻辑xdc完成管脚约束tcl脚本可辅助工程重建便于二次开发。该工程已有2671人学习/下载热度较高内容实用性受认可。通过它可直接查看PL端与AD7606的硬件连接、DMA传输逻辑及FFT处理结构理解数据通路构建与硬件加速思路工程备份日志和仿真中间文件也有助于排查时序与带宽问题其中的DMA与缓冲设计还可迁移到其他高速ADC采集场景。适合想在ZYNQ上实现高速数据采集与实时频谱分析的开发者参考模仿。1. ZYNQ PL采集AD7606做FFT把8通道数据链从AD到频谱完整打通ZYNQ的PL侧驱动AD7606做8通道同步数据采集、再在硬件里完成FFT频谱分析是电能质量监测、旋转机械振动分析和多通道超声检测这类工程里非常典型的链路。很多人会以为「SPI驱动一跑数据往FFT IP一灌」就完事实际上AD7606读回来的原始码值和FFT频点位置上正确的幅值信息之间还隔着采样率设计、FIFO流控、数据对齐和幅值标定四道门。这里就把这套链路从AD7606串行时序开始一路拆到FFT IP核的配置、输出数据解读和上板验证给正在搭同类采集系统的你一个可以直接照搬的参考。2. AD7606串行模式驱动一个三段式状态机完成8通道读回2.1 先定接口为什么这个项目选串行模式而不是并行模式AD7606输出接口支持并行、串行和并行字节三种模式硬件上通过PAR/SER_BYTE_SEL引脚配置。很多初学者看到并行模式「一拍读出16位」会觉得效率最高实际用起来反而不舒服。并行模式至少需要CS_n、RD_n、BYTE_SEL、加上16根数据线总共20根左右信号接到PL侧这对ZYNQ的管脚分配、PCB走线和FPGA内部布局都会造成压力。而且并行模式读时序对RD_n脉冲宽度有严格要求在高速运行时需要额外关心建立保持时间。串行模式只需要CS_n、SCLK、SDO_A、SDO_B配合CONVST和BUSY两根控制线总共6根信号线。8通道数据通过SDO_A通道1~4和SDO_B通道5~8并行移位读出在100kSPS采样率下SPI时钟用25MHz128个SCLK周期只占5.12us远小于10us的采样周期吞吐没有任何瓶颈。所以在这个采集场景里串行模式是更合理的选择。如果你的项目板子已经定了并行接线也完全可以只是后级代码逻辑要换一套读时序整体框架思路一致。2.2 AD7606串行读回的时序拆解一个完整的采样周期分三个动作启动转换、等待BUSY下降沿、移位读回。CONVST上升沿触发8个ADC通道同时执行采样保持紧接着BUSY拉高转换期间任何数据读取操作都会被忽略。转换完成后BUSY拉低此时CS_n拉低SCLK开始移位SDO_A/SDO_B并行输出。FRSTDATA信号在通道1的数据出现时拉高相当于帧同步标志解决多通道数据排列的起始判断问题。这里有一个容易犯的错误把BUSY当作普通电平信号直接接进状态机跳转条件。因为AD7606的BUSY是异步信号用100MHz时钟去采它跨时钟域采样的结果可能正好采到跳变沿状态机收到一个错误的高电平或低电平导致等待时间异常。我一般会在模块内部加两级同步寄存器。2.3 核心状态机Verilog代码下面给出我在项目里经常用的框架三段式写法逻辑清楚方便后面加超时保护// AD7606串行采集状态机启动转换→等待BUSY→移位读回 // 系统时钟100MHzSCLK通过div_cnt分频约25MHz localparam IDLE 2d0, CONV 2d1, WAIT 2d2, READ 2d3; reg [1:0] state_c, state_n; reg [1:0] div_cnt, conv_cnt; reg [7:0] bit_cnt; wire clk_en (div_cnt 2d3); // SCLK使能脉冲 wire sclk_out (state_c READ) ? (div_cnt 2d2) : 1b0; always (posedge clk) begin if (!rst_n) state_c IDLE; else state_c state_n; end always (*) begin state_n state_c; case (state_c) IDLE: if (sample_pulse) state_n CONV; CONV: if (conv_cnt 2d3) state_n WAIT; WAIT: if (!busy_sync) state_n READ; READ: if (bit_cnt 8d127) state_n IDLE; endcase end always (posedge clk) begin if (!rst_n) begin div_cnt 2d0; conv_cnt 2d0; bit_cnt 8d0; end else begin div_cnt div_cnt 1b1; if (state_c CONV) conv_cnt conv_cnt 1b1; else conv_cnt 2d0; if (state_c READ clk_en) bit_cnt bit_cnt 1b1; else if (state_c ! READ) bit_cnt 8d0; end end逻辑说明状态机里四个状态对应前面时序拆解的四个阶段。CONV状态里conv_cnt计数保证CONVST高电平宽度不低于规格书要求的25ns这里100MHz下3个时钟周期就是30ns留了一点裕量。WAIT状态只认busy_sync的下降沿busy_sync已经经过两级寄存器同步能避开亚稳态。READ状态持续128个SCLK计数满回到IDLE整帧数据完成。参数说明bit_cnt的上限7d127来自8通道×16bit128bit。如果只用单SDO模式上限改成7d63但SCLK需要加倍或者延长采样周期。conv_cnt的上限可以根据实际系统时钟调整总之让CONVST脉宽落在25ns~2.5us之间即可。提示严格看AD7606数据手册BUSY下降沿到第一个SCLK有效沿之间需要一点恢复时间代码里可以依赖SCLK的第一个完整周期做对齐实测没有出过问题。另外这里sclk_out是组合逻辑输出如果在意信号质量建议用ODDR原语把SCLK做成真正的时钟输出避免组合毛刺。2.4 移位装配把串行比特流变成通道数据状态机只负责时序控制真正的数据读取还需要移位寄存器配合。SDO_A在SCLK下降沿变化、上升沿稳定所以我在系统时钟的上升沿采样sdo_a用clk_en作为采样使能bit_cnt低4位计数判断16bit边界// SDO_A移位拼装每个SCLK周期采1bit每16bit拼成一个通道 reg [15:0] shift_reg; always (posedge clk) begin if (state_c READ clk_en) begin shift_reg {shift_reg[14:0], sdo_a}; // 高位先出左移拼装 if (bit_cnt[3:0] 4d15) ch_data[bit_cnt[7:4]] shift_reg; // 每16bit生成一个通道 end end这里的通道索引bit_cnt[7:4]是从0到7的自然序列刚好对应通道1到通道8。实际在ZYNQ里如果定义reg [15:0] ch_data[0:7]就能直接通过索引访问每个通道的采样值。SDO_B需要同样的逻辑再例化一份或者把移位寄存器位宽改成32bit一次拼两个通道。这段代码有个细节要注意shift_reg在bit_cnt[3:0]为15时刚好完整拼完16bit此时把shift_reg锁存到ch_data而不是把shift_reg和sdo_a拼接后的结果放进去否则会把下一位数据提前混入。3. 采样率与FIFO设计让100kSPS采集不丢一帧数据3.1 采样率怎么定先算频率分辨率FFT的频谱分辨率由采样率Fs和变换点数N共同决定公式是Δf Fs / N这个值决定你能不能把两个相近的频率分量在频谱上分开。举个例子用AD7606做三相电压谐波分析目标要分辨50Hz基波到19次谐波950Hz如果采样率用100kSPS1024点FFT的频率分辨率是97.66Hz50Hz和150Hz谐波当然分得开但你要看2kHz和2.1kHz两个相近分量就完全重叠了。这时候把点数加到4096分辨率变成24.41Hz才勉强可辨。AD7606的规格是每通道200kSPS最高吞吐率8个通道同步转换没有轮询分摊的问题。实际项目中通常预留20%裕量常用配置在100k~160kSPS之间。采样率还受模拟前端抗混叠滤波器约束如果前端没有做低通滤波FFT结果里高频折返会出现无法解释的虚假谱峰。提示如果前端没有抗混叠滤波采样率至少要高于目标最高频率的2.56倍以上工程上通常取2.56~5倍。3.2 用计数器分频产生CONVST触发脉冲采样率控制的本质是控制CONVST脉冲的间隔。在PL里最直接的方式就是计数器分频。100MHz系统时钟100kSPS采样率计数周期是1000个时钟。// 采样率发生器产生100kSPS的采样触发脉冲 localparam SAMPLE_DIV 1000; // 100MHz / 100kSPS reg [15:0] div_cnt; wire sample_pulse (div_cnt SAMPLE_DIV - 1); always (posedge clk) begin if (div_cnt SAMPLE_DIV - 1) div_cnt 0; else div_cnt div_cnt 1; end这个sample_pulse直接接入第2章状态机的IDLE跳转条件就完成了一整个闭环计数器产生脉冲→ADC转换→数据读回→等待下一个脉冲。参数说明SAMPLE_DIV的取值决定采样率改起来就是改一个本地参数。有一点要提醒这样的分频产生的是均匀触发脉冲但如果系统时钟本身来自晶振且精度一般采样率的长期稳定性会受时钟源影响。ZYNQ平台上如果希望采样率绝对准确可以用PS侧的定时器或PLL锁定到高精度参考源但一般嵌入式采集场景晶振精度50ppm足够。3.3 FIFO的角色缓存解耦不让数据流卡顿AD7606在100kSPS采样率下每10us产生一帧128bit数据。XFFT需要积累N个连续样本才能输出一次频谱如果让FFT模块直接跟着ADC数据走前N-1个样本期间FFT一直空闲等第N个样本到了才启动效率浪费极大。中间加一级FIFO把数据缓存起来让ADC端始终全速采样FFT端攒够一批就拉走这是最主流的数据路径设计。FIFO位宽有两种选法一种是16bit写入侧把8个通道的数据按顺序逐字写入读侧按通道筛选另一种是128bit一帧数据一次写入读侧并行拆开。前者逻辑简单后者吞吐高。开发中按资源够用原则选16bit即可。代码里用Vivado FIFO IP核配置为同步FIFO写端口连接ADC通道数据读端口连接FFT的AXI-Stream接口。下面展示128bit拼接的写法// FIFO写入侧一帧数据拼成128bit一次写入 always (posedge clk) begin if (frame_valid) begin for (int i 0; i 8; i i 1) begin fifo_din[16*i : 16] ch_data[i]; // 8通道拼成128bit end fifo_wr_en 1b1; end else begin fifo_wr_en 1b0; end end提示如果FIFO配置为16bit写位宽就得在每个通道数据有效时依次写8次写使能要维持8个周期配置为128bit时写使能只拉高一个周期吞吐更高代价是FPGA里要凑出128bit的连续寄存器布局紧张时会增加布线延迟。3.4 通道对齐保证FFT处理的是某个通道的连续序列AD7606读回的数据默认按通道1~8顺序排列但FFT需要的是单一通道的连续时间序列。要把通道拆出来有两种常见做法一是把8通道数据全部写入FIFO读侧用状态机按通道号逐个提取每提取N点就触发一次FFT二是写侧就把同一个通道的数据集中存放比如用8个独立的小FIFO分别缓存通道1到通道8FFT需要哪一路就选哪一路。我一般用第二种因为它在多通道并发扫描时逻辑更清晰。如果你的应用只需要分析某一两个通道第一种做法最省资源。两种方式都不会丢帧关键是读侧和写侧的通道下标要一致否则FFT出来的频谱张冠李戴。4. XFFT IP核对接点数、输入格式与频谱电压标定4.1 先选点数三档常用配置对比PL侧做FFTXilinx提供XFFT IP核支持8点到65536点的变换长度。点数的选择取决于频率分辨率和资源消耗。FFT点数100kSPS下频率分辨率单帧时长16bit定点下BRAM参考占用102497.66Hz10.24ms约2~3个BRAM204848.83Hz20.48ms约4~6个BRAM409624.41Hz40.96ms约8~12个BRAMBRAM占用只是个粗略参考值具体取决于FFT实现架构、数据位宽和缩放方式。从这个表能看出点数翻倍频率分辨率减半但BRAM消耗和计算时间都在涨。我一般先算目标频段内最近的两个频率分量间隔再反推N如果间隔3kHz、采样率100kSPSN至少要大于100000/3000约34取2的幂就是64点但工程上为了波形细节往往取1024点以上。对振动监测这种窄带分析场景4096点起步很正常。4.2 XFFT的接口配置要点Vivado里XFFT IP核配置界面有几个关键选项Implementation选项Pipelined Streaming流水线模式吞吐最高适合连续数据流Radix-2 Burst I/O资源占用少但处理间隙大适合单帧触发。AD7606连续采样场景我建议选Pipelined Streaming。Data FormatFixed Point固定点16bit和AD7606的输出位宽匹配如果输入信号动态范围大可以考虑Block Floating Point让IP自动做缩放。缩放方式可以指定每级缩放因子或者用Block Floating Point输出指数因子。后者方便但还原真实幅值时得额外乘一个2^exp。XFFT的输入数据接口是AXI-Streamtdata的位宽由实部虚部各16bit组成共32bit。AD7606输出是实数样本所以虚部直接置0// XFFT输入数据拼装实部为ADC采样值虚部填0 wire [31:0] xfft_tdata; assign xfft_tdata {16d0, adc_sample}; // 高16位虚部低16位实部这里adc_sample必须是有符号数AD7606输出本身就是二进制补码所以不需要额外转换。如果用无符号数强行左移频谱幅值会整体失真。如果你后续要用到多通道同时做FFTXFFT也支持多数据通道配置tdata位宽会变成通道数×32bit输入时把各通道数据分别放到对应位段即可。只是这样BRAM消耗成倍上涨不如单通道多次调用的方式灵活。4.3 输出频域数据怎么读XFFT输出也是AXI-Streamtdata里实部和虚部同样是各16bit。为了得到频谱幅值需要计算sqrt(re^2 im^2)。在PL里一般用CORDIC IP核的Vector Translate模式做开根号配置成16位输入、16位幅度输出即可不需要自己写浮点。一个常被忽略的步骤是幅值标定。AD7606在±5V量程下16位有符号码值的每一LSB对应电压约0.1526mV5V/32768因此码值换算回电压是V_real code × 5.0 / 32768FFT输出的幅度单位现在是码值幅度。要把码值幅度变成物理电压幅度公式是V_amp(k) 2 × sqrt(re^2 im^2) / N × (5.0 / 32768)这个公式针对单边谱也就是只取0~Fs/2的一半频点所以前面乘2直流分量k0不乘2双边谱也不需要乘2。很多人在这一步卡住输出频谱峰值位置正确但幅值普遍偏小多半就是漏了乘以2或忘了除以N。提示如果应用里需要看谱线之间有泄漏可以给时域数据加窗。XFFT本身不提供窗函数得自己在数据写入FIFO之前用乘法器逐点乘一个汉宁窗系数表这会占用不少DSP资源务必先评估乘法和CORDIC的资源预算。5. 避坑记录AD7606FFT链路里最容易翻车的五个位置5.1 状态机卡死在WAIT采集链路完全没数据现象上板后ILA抓不到任何通道数据状态机一直停在WAIT。原因BUSY引脚悬空或者网络走线过长信号幅度不足采到的始终是低电平。解决先用示波器量BUSY引脚是否确实有转换期的低电平出现。硬件没问题时检查busy_sync两级同步的时钟域是否正确同步逻辑是否放在正确的always块里。我后来在所有采集板设计里都给BUSY加了一个10kΩ上拉到3.3V实测稳定性明显提升。5.2 SCLK频率給太高高采样率下FFT底噪整体抬高现象把SCLK从25MHz改成50MHz后FFT噪声底从-80dB抬高到-65dB且出现规律的杂散。原因50MHz已经不利于稳定读回加上信号完整性问题采样值偶发错一位这种随机错误在频域就表现为底噪抬升。解决把SCLK降回25MHz采样率不变的情况下每帧占空比从2.56us变成5.12us采样周期10us内完全来得及。如果确实需要更高SCLK建议先在SPI链路上加串阻或降低IO drive strength再用ILA长时间抓数据看有没有bit error。5.3 FIFO溢出后数据帧边界丢失FFT出现镜像频率现象连续跑了几分钟后频谱上突然出现一个不存在的频率峰值过一会儿又消失。原因FIFO深度不够在采样率高、FFT处理延迟抖动大时偶尔溢出溢出后读侧拿到的数据帧不完整XFFT把一个截断序列当成完整帧做变换频谱自然出错。解决这个现象用ILA抓FIFO的prog_full标志就能确认。把FIFO深度扩大一倍或者把FFT的处理流程改成「集齐N点再启动一次」而不是「边读边算」避免处理延迟波动。我后来的做法是在FIFO读侧加一个帧计数器每次读完N点校验一次校验不过就丢弃整帧宁可丢掉一帧也不输出错帧。5.4 频谱幅值始终差一个固定倍数现象用信号源灌入一个已知幅值的正弦波FFT输出峰值幅度总是比理论值高一倍或低一倍。原因标定公式里的2和N用错。之前调试时一直用双边谱的长度N却套用了单边谱的乘2系数导致所有幅度整体偏大两倍。解决把标定公式逐项对V_amp 2 × sqrt(re^2im^2) / N × LSB电压直流分量不乘2。对照信号源示数验证确认只做单边谱还是双边谱全程保持一致。从那以后我习惯在工程里写一个宏定义把LSB电压和N固化进去避免每次换算时手算出错。5.5 通道交错分析的是通道1频谱上却是通道2的数据现象代码里明明选择的是通道1的FIFO数据源FFT出来的频谱峰值频率却对应另一路输入。原因FIFO写入侧的通道下标和读侧通道下标不一致。常见于定义ch_data[0:7]时索引和通道号差1或者FIFO写入时把最后一个通道先写了。解决在FIFO写入侧加一个debug计数器把每帧第一个字的通道号打出来确认和期望一致。或者直接在代码里给每帧数据打上帧头读侧校验帧头再分通道能彻底杜绝张冠李戴。5.6 XFFT的s_axis_data_tlast漏接导致IP不输出结果现象XFFT一直没有m_axis_data_tvalid输出但输入数据明明已经给了很多帧。原因XFFT在Pipelined Streaming模式下依赖tlast信号标记一帧数据的结束如果tlast没有在最后一个样本时拉高IP不知道当前帧边界处理永远停留在等待状态。解决在给XFFT送数据时用计数器在数据最后一个有效周期同步拉高tlast。这个信号在数据通路里很容易被遗漏联调时必须检查。6. 上板验证三板斧单频标定、连续帧流控、多通道交叉确认6.1 单频标定从时域到频域逐级验证上板后先用信号源给AD7606某一通道灌入一个已知幅值的正弦波频率取10kHz、幅值1V。用ILA分别抓时域原始码值和FFT输出频点。理想情况下10kHz对应第102点附近100kSPS/1024点第k个频点对应频率k×100000/1024检查FFT峰值出现在第102点或103点并按第4章的标定公式换算幅值是否接近1V。这一步能把驱动、FIFO、FFT、标定公式整个串起来验证。如果峰值位置偏差超过两个频点先查采样率是否为精确的100kSPS如果幅值偏差回到5.4的标定公式核对。6.2 连续帧流控检查连续采集几百帧观察FIFO的prog_full是否经常触发如果触发频率过高说明读写速率不匹配需要加大深度或降低采样率。也可以用计数器记录被丢弃的帧数正常情况下应该是0。6.3 关于多通道的一点习惯一套采集系统最难验证的不是单通道正确而是多通道之间的相位关系。AD7606所有通道是同步采样的但FIFO读出和FFT处理若分时轮询各通道会引入固定的通道间时间偏移。如果应用关心通道间相位差建议对每个通道单独维护缓存和FFT实例而不是分时复用同一个FFT。这套链路我前后调过几块板子最快的记录是半天跑通单通道慢的一次在通道交错上卡了两天。后来我养成一个习惯每次新板子回来不管多着急第一天必做单频标定、连续帧检查、多通道交叉确认这三件事过了这三关才敢谈后续算法。这套思路希望帮到你。本文还有配套的精品资源点击获取