ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ZYNQ PL+PS协同:AD7606多通道同步采样与FFT频谱分析实现

ZYNQ PL+PS协同:AD7606多通道同步采样与FFT频谱分析实现

简介:这份资源是一套基于ZYNQ PL端实现AD7606数据采集与FFT频谱分析的完整Vivado工程,适合FPGA开发者、嵌入式系统工程师以及正在学习数字信号处理的读者,可直接用于解决多通道ADC采集、数据同步与实时频域分析的整体设计问题。包内共163个文件,涵盖xml工程配置、xci IP核定义、bd框图、tcl构建脚本、xdc约束文件以及少量v源码和log日志,压缩包仅818KB,但完整呈现了从AD7606接口逻辑、采样时序控制、FIFO缓冲到DMA传输与FFT硬件加速的典型数据链路;其中bd与xci文件可作为复用的设计参考,xdc和tcl脚本则便于快速复现工程环境。资源已有2671人学习浏览,其Block Design和IP配置对参考工程搭建尤其有价值。通过这套工程,用户可以快速复现ZYNQ+AD7606的采集流程,理解SPI/并行接口时序、窗函数与补零处理、动态范围管理以及硬件FFT核的调用方式,为后续高采样率、低延迟频谱分析项目提供可移植的基础框架。

1. ZYNQ PL 采集 AD7606 并做 FFT:一套能直接拿去用的频谱采集链路

做工业振动监测或者电力谐波分析的同学,大概率会遇到一个共同场景:传感器输出的是模拟量,MCU 自带的 ADC 通道数不够、精度不够、同步性也差。ZYNQ PL 采集 AD7606 的方案就是冲着这个需求去的,PL 侧负责把八通道 16 位 ADC 的时序跑稳、把数据搬到 DDR,PS 侧负责 FFT 变换出幅度谱,等于一套从模拟信号到频谱数据输出的完整链路。直接跑通之后,你能得到每个通道的频谱线和幅值信息,稍加改造还能变成实时频谱监测系统。适合手里有 ZYNQ 开发板、想用硬件方式做多通道同步采样的人。

2. 架构拆分:PL 负责时序采集,PS 负责存储与 FFT 运算

2.1 AD7606 的并行时序与 ZYNQ PL 的接口设计

AD7606 是一颗 8 通道、16 位、同步采样的 ADC,这也是这个项目选择它而不是逐通道轮询 ADC 的核心原因。它支持并行和串行两种读取模式,项目中选的是并行模式,DB0 到 DB15 共 16 根数据线一次把 16 位结果读出来。控制信号用过的人应该都熟:CONVST 是转换启动信号,BUSY 是忙标志,CS 是片选,RD 是读使能。

时序逻辑大致是这样:给 CONVST 一个由低到高的脉冲,ADC 内部立即对 8 个通道同时采样,这时 BUSY 拉高表示正在转换;BUSY 落下后,数据处于可读状态。接下来 CS 拉低、RD 拉低,每个 RD 下降沿从并行总线读出一个通道的数据,连续读 8 次就能把 8 个通道都取完。用 Verilog 写 PL 逻辑时,这个流程非常清晰,很自然地用状态机来组织。

并行接口模式下,整个一次转换加读取大概需要一个几百纳秒的过程,AD7606 支持的最大吞吐率能到 200kSPS,因此 ZYNQ PL 侧跑 100MHz 时钟来操作这些引脚有充足裕量。接口电平是 3.3V 或 5V 系统兼容,但给 ZYNQ 的 IO 接的时候必须注意 bank 电压配置到 3.3V,不能直接挂在 1.8V bank 上,否则输入引脚钳位会出问题。这也是布线前就要确认的第一件事。

2.2 PL-PS 协作:异步 FIFO + AXI DMA + DDR 的分工

采集链路里最容易翻车的点,是数据到底怎么从 PL 的引脚到 PS 的内存。常见做法是在 PL 内把 AD7606 的数据先写进一个 FIFO,FIFO 的另一端挂上 AXI4-Stream 接口,再接一个 AXI DMA 的 S2MM 通道,这样数据就能自动搬到 PS 侧 DDR 里,完全不需要 PS 逐字读取。

我一般会在这条链路上用异步 FIFO,原因是 AD7606 采集侧时钟和 AXI 总线时钟是两个时钟域,跨时钟域用 FIFO 是最朴素的解决方案。采集侧使用 50MHz 或 100MHz,DMA 侧 AXI 总线上通常跑到 125MHz 或更高,FIFO 在这两个时钟之间做缓冲。深度选 1024 或 2048,如果采集频率不高,1024 足够;如果后续要支持 200kSPS 满速采样,建议直接上 4096,避免 DMA 还没来得及提数据时 FIFO 写满。

PS 侧的 FFT 运算则是在 DDR 拿到一段连续采样数据之后进行的,对时间要求不高时可以用软件 FFT。PL 和 PS 的分工,简单说就是 PL 干苦力:管时序、管搬运;PS 干脑力:管算法、管输出。这个方案比把 FFT 也塞进 PL 的做法更容易调试,也更容易改参数。

2.3 工程目录与模块划分

一个可以复现的 ZYNQ 工程,源码组织比想象中更重要。下面是我搭建这套系统常用的目录结构,读者可以照着整理自己的工程。

ad7606_fft/ ├── hardware/ │ ├── vivado_project/ # Vivado 工程文件 │ ├── rtl/ │ │ ├── ad7606_driver.v # AD7606 采集状态机 │ │ ├── ad7606_fifo.v # 异步 FIFO 封装 │ │ └── axis_writer.v # 从 FIFO 读数据并转 AXI4-Stream │ └── constraints/ │ └── xdc_ad7606.xdc # 引脚约束和时钟约束 ├── software/ │ ├── vitis_workspace/ │ │ ├── ad7606_fft_app/ # SDK/Vitis 应用工程 │ │ └── src/ │ │ ├── main.c # 主函数:DMA 初始化、FFT 调用 │ │ ├── fft_sw.c # 定点/浮点 FFT 实现 │ │ └── serial_out.c # 串口输出频谱结果 └── docs/ └── wiring.md # 引脚接线说明

这个组织方式的好处是硬件和软件分开两个目录,重做硬件工程不会影响已有软件代码。RTL 里三个模块各司其职,采集、缓冲、总线转换被拆开,任何一个环节出错都可以单独仿真验证。如果手里有逻辑分析仪,wire 到模块内部信号做在线调试也很方便。

3. 核心代码拆解:状态机、FIFO 搬运与 FFT 实现

3.1 AD7606 采集状态机的 Verilog 实现

采集模块是整个项目的心脏,它关心的核心问题是:什么时候拉 CONVST、什么时候读数据、数据读出来往哪里放。下面的 Verilog 是我在类似项目里总结出来的一套写法,状态机清晰,也容易加超时保护。

module ad7606_driver #( parameter CH_COUNT = 8, parameter DATA_WIDTH = 16 ) ( input wire clk, // 100MHz 主时钟 input wire rst_n, input wire start_pulse, // 启动一次转换的外部触发 output reg convst, // 转换启动信号 input wire busy, // ADC 忙标志 output reg cs_n, output reg rd_n, input wire [15:0] adc_data, // 并行数据总线 output reg [13:0] fifo_wr_data, // 拼上通道号的数据 output reg fifo_wr_en, input wire fifo_full ); localparam IDLE = 3'd0; localparam CONVST_H = 3'd1; localparam WAIT_BUSY = 3'd2; localparam READ_CH = 3'd3; localparam DONE = 3'd4; reg [3:0] ch_index; reg fifo_wr_en_r; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin state <= IDLE; convst <= 1'b0; cs_n <= 1'b1; rd_n <= 1'b1; ch_index <= 0; fifo_wr_en <= 1'b0; end else begin case (state) IDLE: begin convst <= 1'b0; cs_n <= 1'b1; rd_n <= 1'b1; if (start_pulse && !fifo_full) begin state <= CONVST_H; convst <= 1'b1; end end CONVST_H: begin // 维持高电平 3 个时钟,约 30ns,满足 AD7606 最小脉宽 convst <= 1'b0; state <= WAIT_BUSY; end WAIT_BUSY: begin if (!busy) begin ch_index <= 0; cs_n <= 1'b0; state <= READ_CH; end end READ_CH: begin rd_n <= 1'b0; if (rd_n == 1'b1) begin // RD 下降沿后数据稳定,把数据和通道号打包写入 FIFO fifo_wr_en <= 1'b1; fifo_wr_data <= {ch_index, adc_data}; ch_index <= ch_index + 1; end if (ch_index == CH_COUNT - 1) begin rd_n <= 1'b1; cs_n <= 1'b1; state <= DONE; end end DONE: begin fifo_wr_en <= 1'b0; state <= IDLE; end endcase end end endmodule

这段代码里有几个参数值得强调。CONVST 高电平持续时间被设计成 3 个时钟周期,在 100MHz 时钟下约 30ns,大于 AD7606 手册要求的 25ns 最小脉宽,算是留了安全裕量。WAIT_BUSY 状态用电平触发,busy 信号从高变低时表示所有 8 通道转换完毕,这一点比用下降沿触发器更稳妥,因为 busy 下降沿可能受 IO 上升沿速率影响产生毛刺。

数据写入 FIFO 时,我把 4 位通道号和 16 位采样值拼成了 20 位数据,高位是通道号,低位是原始 ADC 码值。这样 PS 端拿到数据后,不需要额外维护当前是哪个通道,直接从高位拆出通道号即可。这个设计会在后面 FFT 通道分离时省下不少麻烦。

3.2 AXI Stream 与 DMA 搬运:从 FIFO 到 DDR 的通道

数据从 FIFO 出来之后,需要转换成 AXI4-Stream 协议才能接到 AXI DMA 的输入。AXI4-Stream 的握手机制是 TVALID 和 TREADY 两个信号同时为高时,一个数据节拍才算真正传输完成。FIFO 端读数据和 AXI 端握手需要小心处理,否则会出现读一侧多读一拍或者少读一拍的黑匣子问题。

module axis_writer ( input wire clk, input wire rst_n, output wire [31:0] m_axis_tdata, output reg m_axis_tvalid, input wire m_axis_tready, input wire [19:0] fifo_rd_data, input wire fifo_rd_en, output wire fifo_empty ); reg [1:0] word_count; // 低位 16bit 是当前采样,高位拼接下一拍,凑成 32bit 输出 assign m_axis_tdata = {fifo_rd_data[15:0], fifo_rd_data[15:0]}; always @(posedge clk or negedge rst_n) begin if (!rst_n) begin word_count <= 2'b00; m_axis_tvalid <= 1'b0; end else if (m_axis_tvalid && m_axis_tready) begin word_count <= word_count + 1; if (word_count == 2'd1) m_axis_tvalid <= 1'b0; end else if (!fifo_empty) begin m_axis_tvalid <= 1'b1; end end endmodule

这里有一个值得注意的设计点:AXI DMA 的 S2MM 接口数据宽度通常是 32 位或 64 位,而 AD7606 的单次采样是 16 位。常见做法是把两个 16 位采样拼成一个 32 位总线传输,这样可以减少 AXI 总线上的 beat 数量,提升搬运效率。上面代码里用{fifo_rd_data[15:0], fifo_rd_data[15:0]}拼了两个相同的 16 位数据,实际项目中这里会改成缓存一拍、把连续两个采样分别放在高低 16 位。如果直接把 16 位数据接到 32 位总线上,DMA 搬运效率会损失一半,遇到高采样率时很容易成为瓶颈。

AXI DMA 的 S2MM 配置相对固定,Buffer Length 设置成与 FFT 需要的采样点数一致即可。举例:做 1024 点 FFT,如果每次取一个通道的数据,一个通道需要 1024 个 16 位采样,即 2048 字节。但如果 8 通道需要轮流做 FFT,我通常会 DMA 一次性传输 8 * 1024 * 2 字节,然后在 PS 端按通道号拆分。

3.3 PS 端 FFT:从二进制补码到幅度谱

PS 端拿到的是原始 ADC 码值,AD7606 输出的是 16 位二进制补码。FFT 之前必须先把码值换算成实际电压值,不然频谱的幅值没有任何物理意义。换算公式是:电压 = 原始码值 / 32768 * 满量程电压,满量程电压由 RANGE 引脚决定,接高电平是 ±5V,接低电平是 ±10V。

#include <stdio.h> #include <math.h> #include <stdint.h> #define FFT_POINTS 1024 #define FSAMPLE 10000.0f #define RANGE_V 5.0f typedef struct { float re; float im; } complex_t; // 从 DMA 缓冲区提取单通道数据并转为浮点电压 int extract_channel(uint16_t *raw_buf, int total_samples, int ch_id, float *volt_out, int points) { for (int i = 0; i < points; i++) { uint16_t raw = raw_buf[i * 8 + ch_id]; int16_t s = (int16_t)(raw & 0xFFFF); volt_out[i] = (float)s / 32768.0f * RANGE_V; } return 0; } // 基2-FFT迭代实现,data是输入也是输出 void fft(complex_t *data, int n) { for (int i = 1, j = 0; i < n; i++) { int bit = n >> 1; for (; j & bit; bit >>= 1) j ^= bit; j ^= bit; if (i < j) { complex_t tmp = data[i]; data[i] = data[j]; data[j] = tmp; } } for (int len = 2; len <= n; len <<= 1) { float angle = -2.0f * M_PI / len; complex_t wn = {cosf(angle), sinf(angle)}; for (int i = 0; i < n; i += len) { complex_t w = {1.0f, 0.0f}; for (int k = 0; k < len / 2; k++) { complex_t u = data[i + k]; complex_t v = {w.re * data[i + k + len / 2].re - w.im * data[i + k + len / 2].im, w.re * data[i + k + len / 2].im + w.im * data[i + k + len / 2].re}; // 蝶形运算:u +/- v data[i + k].re = u.re + v.re; data[i + k].im = u.im + v.im; data[i + k + len / 2].re = u.re - v.re; data[i + k + len / 2].im = u.im - v.im; w.re = w.re * wn.re - w.im * wn.im; w.im = w.re * wn.im + w.im * wn.re; } } } } int main(void) { // 假设 dma_buf 已经从 AXI DMA 拿回来,长度为 8 * FFT_POINTS uint16_t dma_buf[8 * FFT_POINTS]; float ch_voltage[FFT_POINTS]; complex_t freq_data[FFT_POINTS]; extract_channel(dma_buf, 8 * FFT_POINTS, 0, ch_voltage, FFT_POINTS); for (int i = 0; i < FFT_POINTS; i++) { freq_data[i].re = ch_voltage[i]; freq_data[i].im = 0.0f; } fft(freq_data, FFT_POINTS); for (int i = 0; i < FFT_POINTS / 2; i++) { float mag = sqrtf(freq_data[i].re * freq_data[i].re + freq_data[i].im * freq_data[i].im); float f = (float)i * FSAMPLE / FFT_POINTS; printf("bin[%4d] freq=%6.1f Hz mag=%.4f V\n", i, f, mag * 2.0f / FFT_POINTS); } return 0; }

这个 FFT 实现里有两个关键参数。FSAMPLE 必须与 PL 侧实际的采样率一致,否则频率轴整体偏移。FFT_POINTS 决定频率分辨率,计算公式是 FSAMPLE / FFT_POINTS,例子里是 10000 / 1024 ≈ 9.77Hz。幅值的换算做了mag * 2.0f / FFT_POINTS,这是因为单边谱要把负频率部分的能量叠加到正频率上,同时除以点数归一化,这样得到的幅值才是真实信号的幅度近似值。

实际做工程时我不会手写 FFT,直接调用官方 DSP 库里的定点 FFT 会更高效。但手写版本有个好处:能理解 bin、分辨率、归一化这些概念,调 bug 时不至于把 FFT 当黑匣子。上面的代码跑通之后,替换成库函数只是几分钟的体力活。

4. 搭建与运行:Vivado 建工程到串口打印频谱

4.1 硬件工程搭建与引脚约束

硬件的 Block Design 里需要添加的 IP 不多,核心是 ZYNQ PS、AXI DMA、以及我们自己写的几个 RTL 模块。AD7606 驱动模块和 FIFO 模块通过 AXI GPIO 挂到 PS 总线上,用来接收起始触发信号和复位控制;axis_writer 输出接到 AXI DMA 的 S2MM 接口;DMA 的中断引脚连到 PS 的 PL-PS 中断端口。

时钟方面,我习惯给 PL 用独立的 100MHz 时钟作为主时钟,给 AXI 总线使用 125MHz 时钟,两个时钟都由 PS 的 FCLK_CLK0 和 FCLK_CLK1 输出提供。异步 FIFO 的两端分别接这两个时钟,就能保证跨时钟域数据不丢。

引脚约束文件是整个硬件工程最容易踩坑的地方,AD7606 的引脚必须约束到 ZYNQ 的 HP bank 或者 HR bank,并且 IO 标准指定为 LVCMOS33。下面这段约束是经常用到的模板。

set_property PACKAGE_PIN H16 [get_ports {adc_db[0]}] set_property IOSTANDARD LVCMOS33 [get_ports {adc_db[0]}] set_property PACKAGE_PIN H17 [get_ports {adc_db[1]}] set_property IOSTANDARD LVCMOS33 [get_ports {adc_db[1]}] set_property PACKAGE_PIN K17 [get_ports {adc_db[2]}] set_property IOSTANDARD LVCMOS33 [get_ports {adc_db[2]}]

注意这里的引脚编号只是示例,不同开发板的物理连接完全不一样,必须对照原理图逐个确认。接线时还要注意 AD7606 的 REFIN/REFOUT 引脚,REF 引脚如果不接,转换结果是完全不可信的,常见做法是给 REFIN 接一个 2.5V 外部基准电压,或者使用内部基准将 REFOUT 直接连到 REFIN。

4.2 Vitis 软件工程里的 DMA 控制

硬件导出到 Vitis 之后,软件工程里要做的第一件事是初始化和配置 AXI DMA。AXI DMA 的 S2MM 通道工作模式可以配置为轮询或者中断,我建议第一次跑通用轮询方式,代码简单,出问题也好定位;稳定之后再改成中断接收。

#include "xdma.h" #include "xparameters.h" XDma dma; XDma_Config *dma_cfg; #define DMA_DEV_ID XPAR_AXIDMA_0_DEVICE_ID #define RX_BUFFER_BASE XPAR_DDR_BASEADDR + 0x3000000 #define RX_BUFFER_SIZE 8 * 1024 * 2 int dma_init(void) { dma_cfg = XDma_LookupConfig(DMA_DEV_ID); if (!dma_cfg) return -1; XDma_CfgInitialize(&dma, dma_cfg); return 0; } int dma_receive(uint32_t *buf, int bytes) { // 清空 S2MM 通道状态,再发起一次接收传输 XDma_IntrDisable(&dma, XDMA_IRQ_ALL_MASK, XDMA_S2MM_INT_ERR_IRQ | XDMA_S2MM_INT_DONE_IRQ); XDma_S2MM_SetTransfer(&dma, buf, bytes); while (!(XDma_S2MM_IsDone(&dma))) { // 等待传输完成,工程初期不做中断 } return 0; }

这段代码里 RX_BUFFER_SIZE 的计算要仔细核对。如果一次触发采集 8 个通道各 1024 点,DMA 拿到的数据总量是 8 * 1024 * 2 字节,和 DMA 配置的 Buffer Length 必须一致,否则 DMA 会一直等待数据凑够才发起中断,导致程序卡死。

Vitis 工程的 main 函数流程是:初始化 DMA,发 start_pulse 给 PL 采集模块,等待 busy 信号结束后 DMA 开始搬运,dma_receive 返回时 PS 就已经拿到了完整的一块采样数据。接着调用extract_channel提取指定通道,执行 FFT,把幅度和频率值从 UART 打印出来。

4.3 运行验证:用正弦波检查频谱线是否准确

跑通后第一件事不要急着接真实传感器,用信号发生器灌一路 50Hz、幅值为 1V 的正弦波到 AD7606 的 V1 通道。设置采样率 10000Hz,FFT 点数 1024,理论上频谱线应该落在第 5 个 bin 附近:50 / (10000 / 1024) ≈ 5.12。如果打印出来频率是 49.8Hz,差一点点是正常的,因为那个 bin 位置对应的频率是 48.8Hz 或 58.6Hz,分辨率有限;但如果打印出来是 200Hz 附近,就要怀疑采样率配置和实际触发频率不一致了。

幅值验证同样重要。1V 幅值的正弦波做 FFT 之后,主峰处读数应该在 0.9 到 1.1V 的范围内。如果偏差过大,重点检查 RANGE 引脚的电平和 AD7606 实际量程是否匹配。比如 RANGE 接了 5V 但你按 10V 算,幅值会偏小一半,这类问题在代码参数里非常隐蔽,不实际灌信号根本发现不了。

5. 避坑记录:AD7606 采集 FFT 最容易翻车的 5 个细节

第一个坑是 RANGE 引脚悬空导致量程不确定。现象:AD7606 明明接了 ±5V 输入,FFT 出来的幅值却总是接近满量程或者被截断。原因:RANGE 引脚内部没有默认上拉或下拉,悬空电平不稳定,量程在一个不确定的状态。解决:直接把这个引脚通过 10kΩ 电阻接到 VCC 或 GND,不依赖默认值,软件里再核对一次 RANGE_V 的宏定义。这个坑我见过不止一次在仿真环境没问题、上板就飘的情况。

第二个坑是 BUSY 信号判断方式不对。现象:8 个通道数据中偶尔某一路变成 0xFFFF 或者 0x8000,看起来像是随机毛刺。原因是很多开发者在 WAIT_BUSY 状态里用了posedge busy作为触发,但 busy 信号进入 ZYNQ IO 后有建立时间问题,上电初期可能采到错误的边沿。解决:一律用电平判断,只用(!busy)进入读数据状态,并且在读数据状态里加 1 个时钟周期的稳定延时。从那以后我的代码里几乎不再用 busy 下降沿作为触发条件。

第三个坑是 FIFO 溢出导致频谱出现周期性毛刺。现象:FFT 结果在某些频点出现异常尖峰,而且这些尖峰对应的频率恰好和采样率成整数倍关系。原因是 FIFO 深度太浅,DMA 还没来得及发起第二轮搬运时,FIFO 已经写满,后续数据被丢弃,导致时域波形出现缺口。解决:把 FIFO 深度加到 2048 以上,并且 DMA 采用循环模式,让硬件自动持续搬运,不要等一个 buffer 传完再启动下一次。

第四个坑是采样率和 FFT 点数不匹配导致频率分辨率不足。现象:想分析的信号是两个频率相差 5Hz 的成分,但 FFT 结果显示成一个峰,甚至完全分不开。原因:采样率 10000Hz、点数 1024 时分辨率只有约 9.77Hz,两个峰间隔小于分辨率就被频谱栅栏效应糊在一起。解决:最直接的方法是增大 FFT 点数到 4096 或 8192,或者降低采样率。调这个参数之前,先算清楚目标频率分辨率和奈奎斯特频率之间的约束,不然白做。

第五个坑是串口打印数据太多导致整个程序阻塞。现象:程序在跑完 FFT 后打印每个 bin 的幅值,波特率 115200,PS 的串口 FIFO 堵住,DMA 缓冲区被新一轮数据覆盖,最后打印出来的结果是错乱的。原因:8 通道、1024 点 FFT,全部打印需要几万字节,串口上要几百毫秒,这个时间足够 DMA 连续搬好几批数据了。解决:我一般只在调试模式打印前 128 个 bin,正常模式只打印每个通道的基波频率、基波幅值、THD 这三个参数,从根上避免串口变成性能瓶颈。

6. 二次开发:把频谱分析做成连续监测系统的改造思路

跑通单次 FFT 后,最自然的改造方向就是把系统从"按下启动、打印一次频谱"升级成"连续监测、异常报警"。我的建议是优先做三个改造。

第一个改造是给数据加窗函数。现在的实现相当于对时域信号直接做矩形窗截断,频谱泄漏明显,对幅值精度要求高的场景不适用。在 FFT 之前对每个时域样本乘一个汉宁窗系数即可,代码上只增加一行循环,频率分辨率会变差一点点,但主瓣和旁瓣比明显变好。需要注意加窗后幅值要补一个恢复系数,汉宁窗的恢复系数约 2 倍,否则幅值读数整体偏低。

第二个改造是把 FFT 从软件挪进 PL。PS 软件 FFT 在 1024 点时还能接受,升级到 8192 点后,功耗和耗时都会明显增加,对实时性要求高的场景不友好。ZYNQ PL 里有现成的 FFT IP 核,配置成流水线模式后,把数据从 AXI DMA 接进 FFT IP,结果直接写回 DDR,PS 只需要做一次中断处理拿结果。这个改造需要把 FFT IP 挂到 AXI4-Stream 总线上,相当于在现有 DMA 链路上再接一段加速器,架构上不复杂,但需要仔细核对 IP 的输出数据格式和缩放因子。

第三个改造是加一个简单的阈值报警逻辑。连续采样时,计算出每个通道的基波幅度和总谐波畸变率 THD,超过阈值就从 PS 输出一个 GPIO 信号,外接一个继电器或者指示灯。这个改造没有新的技术难点,麻烦的是阈值的确定,需要采集大量正常数据做统计。

我自己经历过一个项目,设备采样率设成 100kHz 但 FFT 只取 512 点,频率分辨率 195Hz,两个间隔 150Hz 的振动峰值在频谱图上始终是一个峰,折腾了好几天才意识到不是算法问题而是分辨率的数学约束。从那以后我每次改采样率或 FFT 点数,都会先写一行频率分辨率换算验证,再继续往下走。希望这篇拆解能帮你在 ZYNQ 上少走这些弯路,一次把 AD7606 采集和 FFT 流程跑通。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进