第五十二章:基于高速ADDA的频谱仪实验(初学者详解)
本章在讲什么?(先看这个)
前面几章我们一直在"传输"信号(以太网、视频),这一章换一个玩法:分析信号。实验任务是:
- FPGA 自己产生正弦波(按键可切换 3 种波形),经高速 DA 芯片变成模拟信号;
- 把高速 AD/DA 模块的 AD 端和 DA 端直连,模拟信号再经 AD 芯片采回 FPGA;
- 对采回的 256 个采样点做 FFT 变换(用 Vivado 自带的 FFT IP 核);
- 对 FFT 结果取模得到幅值,最后把频谱画在 LCD 屏上。
一句话概括:做一个迷你频谱仪。这个实验最大的收获是学会两件事:FFT 到底在算什么、FFT IP 核(AXI4-Stream 接口)怎么用。前者是信号处理的地基,后者是今后做任何通信/音频/图像频域处理的必备技能。
核心概念拆解
1. FFT:把信号从时域搬到频域
FFT(快速傅里叶变换)是 1965 年由库利和图基提出的,它把 DFT(离散傅里叶变换)的计算量大幅降低——采样点数 N 越多,省得越多。
为什么需要频域?看个例子。下面是一个 50Hz 和 120Hz 正弦波叠加了噪声的时域波形:

时域上噪声糊成一团,看不出有效信息。做 FFT 变换到频域后:

50Hz 和 120Hz 两个峰一眼可见,其余频率都是噪声。滤掉它们再观察:

这就是 FFT 的价值:时域里难看出的特征,频域里一目了然。
几个必须记住的基本概念:
- 采样定理(奈奎斯特定理):采样频率 F 必须大于信号最高频率 fmax 的 2 倍(F > 2·fmax),采样后的数字信号才能完整保留原始信息。例:人声频率一般在 8KHz 以内,采样率必须大于 16KHz。
- 频点与模值:N 个采样点做 FFT 得到 N 点复数结果,每个点的模值 = √(实部² + 虚部²)。若原始信号峰值为 A,除直流分量(第 1 个点,模值为 A 的 N 倍)外,各点模值是 A 的 N/2 倍。
- 共轭对称性:第 k 个频点与第 N-k 个频点共轭,信息量相同。所以频域只需处理前一半(精确说是 N/2+1 个点,因为 0 频点没有共轭伙伴)。这也是后面 LCD 只画 128 个点的原因。
- 频率分辨率(基波频率):fk = F/N。第 n 个点对应的频率 = F×(n-1)/N(n≥1,n=1 是直流)。
自己写 FFT 算法对新手太难,好在我们直接用 Vivado 的 FFT IP 核。
2. 系统架构

数据流梳理一遍:
| 模块 | 时钟 | 职责 |
|---|---|---|
| pll(时钟模块) | — | 输出 100M(FFT 运算)/ 50M(DA + LCD)/ 25M(AD 驱动)/ 25M_deg(AD 采样,相位偏移 90°,让采样时刻错开数据变化沿、采得更稳) |
| hs_da(高速DA模块) | 50MHz | 2 个 ROM 存正弦波表,按键切换输出:ROM0 / ROM1 / 两者混频 |
| fifo_ctrl(FIFO控制) | 25M → 100M | 跨时钟域:每帧截取 256 个 AD 采样点,写入异步 FIFO,按 FFT 的握手节奏读出 |
| xfft_0(FFT IP 核) | 100MHz | 256 点 FFT,AXI4-Stream 接口,输出复数 |
| data_modulus(数据取模) | 50MHz | 补码转原码 → 平方和 → CORDIC 开方,得到幅值 |
| lcd_top(LCD顶层) | 50M/100M | RAM 缓存频谱数据,按频谱位置画柱状图 + 坐标轴 |
3. 高速DA模块:按键切换三种波形

两个 ROM 都由 WaveToMem 软件生成,深度 256、数据 8 位,区别是倍频系数:
- ROM0 倍频 2:一个正弦周期 = 128×20ns = 2560ns → 最快输出频率 ≈ 390.6KHz;
- ROM1 倍频 8:一个正弦周期 = 32×20ns = 640ns → 最快输出频率 ≈ 1.56MHz。
DA 数据发送模块(da_wave_send)的核心逻辑:
//数据rd_data 是在clk 的上升沿更新的,所以DA 芯片在clk 的下降沿锁存数据是稳定的时刻
//而DA 实际上在da_clk 的上升沿锁存数据,所以时钟取反,这样clk 的下降沿相当于da_clk 的上升沿
assign da_clk = ~clk; //关键技巧:时钟取反
parameter FREQ_ADJ = 8'd0; //频率调节,越大频率越低,范围0~255
//频率调节计数器:freq_cnt 计到 FREQ_ADJ 才让 ROM 地址加一
always @(posedge clk or negedge rst_n) begin
if(rst_n == 1'b0)
freq_cnt <= 8'd0;
else if(freq_cnt == FREQ_ADJ)
freq_cnt <= 8'd0;
else
freq_cnt <= freq_cnt + 8'd1;
end
//产生波形累加信号(混频)
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
addsub_data <= 9'd0;
else
addsub_data <= rd_data_0 + rd_data_1; //9位,所以输出时要除以2
end
//根据按键次数来切换波形数据
always @(posedge clk or negedge rst_n) begin
if(!rst_n)
da_data <= 8'd0;
else begin
if(cnt_key_flag == 0)
da_data <= addsub_data/2; //混频:累加和9位 > DA的8位,除以2
else if(cnt_key_flag == 1)
da_data <= rd_data_0;
else
da_data <= rd_data_1;
end
end
三个要点:
da_clk = ~clk的取反技巧:ROM 数据在 clk 上升沿更新,DAC 在 da_clk 上升沿锁存。取反后,数据更新半个周期后(数据最稳定时)才被 DAC 锁走;- 混频要除以 2:两个 8 位数据相加是 9 位,DA 数据口只有 8 位,直接截位会溢出失真;
- FREQ_ADJ 的原理:它是读 ROM 的"减速档"。本实验设 0(不降频,地址每个时钟加一);设得越大,读表越慢,输出频率越低。
按键对下降沿计数(cnt_key_flag 0→1→2→0 循环),依次切换"混频 → ROM0 → ROM1"。
4. FIFO 控制模块:跨时钟域 + 凑够 256 个点
AD 数据在 25MHz 时钟域,FFT 在 100MHz 时钟域,两者必须用异步 FIFO 隔开。但这个模块不只是搬运,还要解决两个问题:采哪段数据、怎么喂给 FFT。

写端(25MHz 时钟域):帧同步信号相对 AD 时钟是异步的,先打三拍,用后两拍 Detect 上升沿:
//跨时钟域:帧同步打三拍(打拍 + 采沿)
in_vsync_t <= in_vsync;
in_vsync_t0 <= in_vsync_t;
in_vsync_t1 <= in_vsync_t0;
//场同步上升沿到来时开始写,写满256个停止
else if(in_vsync_t0 && ~in_vsync_t1) //上升沿
fifo_wr_en <= 1'b1;
else if(cnt_fifo_wr_en >= 9'd255)
fifo_wr_en <= 1'b0;
为什么要打三拍而不是两拍?因为要在上升沿拉高写使能——第三拍用来和第二拍做"前一拍低、当前拍高"的比较。写使能拉高后计数,写满 256 个就停(FFT 点数就是 256)。
读端(100MHz 时钟域):
//在读有效使能为高并且FFT 数据通道准备完成信号有效的时候去读出数据
assign fifo_rd_en = rd_en && s_axis_data_tready;
//FWFT 模式下,读使能可以直接当数据有效使能用
assign ad_data_out_en = fifo_rd_en;
//读出最后一个数据时,告知 FFT 这是本帧最后一个
assign s_axis_data_tlast = ((cnt_rd_en == 255) && ad_data_out_en) ? 1'b1 : 1'b0;
//FIFO 剩余数据量达到254(≈写满256)才开始读
else if(rd_data_count >= 9'd254)
rd_en <= 1'b1;
注意两点:
- FIFO 必须配成 FWFT(预读)模式:数据在读使能拉高之前就已经出现在输出总线上,所以读使能可以直接充当给 FFT 的数据有效信号(tvalid),省一级逻辑;
s_axis_data_tlast是 AXI4-Stream 的"包尾"标志:FFT 靠它知道 256 个点送完了,漏发会导致 FFT 不结束运算。

5. FFT IP 核配置:每个选项在选什么
IP Catalog 搜 "fft",选 Fast Fourier Transform。三个选项卡的关键配置:
Configuration 选项卡:
| 选项 | 本实验取值 | 说明 |
|---|---|---|
| Number of Channels | 1 | 1~12 通道,多通道仅 Burst I/O 可选 |
| Transform Length | 256 | 运算点数,必须是 2 的次幂 |
| Target Clock Frequency | 100MHz | 运算时钟 |
| Architecture Choice | Radix-4 Burst I/O | 见下 |
五种架构怎么选:
- Pipelined Streaming I/O:可连续处理,吞吐最高,资源最多;
- Radix-4 Burst I/O:迭代方式分批加载数据,资源较少、变换时间较长(本实验选它);
- Radix-2 Burst I/O / Radix-2 Lite Burst I/O:蝶形运算更小/分时复用,资源更省但更慢;
- Automatically Select:让工具自己挑。
Implementation 选项卡(最容易被新手配错):
| 选项 | 本实验取值 | 说明 |
|---|---|---|
| Data Format | Fixed-Point | 定点格式;浮点在多通道时不可用 |
| Scaling Options | Block Floating-Point | 块浮点:IP 内部按每级数据自动缩放防溢出 |
| Rounding Modes | Convergent Rounding | 收敛舍入(奇偶就近舍入) |
| Input Data Width | 9 | AD 输出 8 位无符号数,而 IP 输入是有符号补码,高位补 1bit 0 变成 9 位。注意这是实部或虚部各自的位宽 |
| Phase Factor Width | 9 | 通常与输入位宽一致 |
| Output Ordering | Natural Order | 顺序输出(否则是倒序,要自己重排) |
| Throttle Schemes | Non-Real Time | 非实时:无严格时序限制 |
Detailed Implementation 选项卡:存储器选 Block RAM,复数乘法器与蝶形运算的构造方式(DSP 多少、CLB 多少)按资源预算选择即可。本配置最终消耗约 9 个 DSP、7 个 BRAM,256 点运算需 871 个时钟周期,100MHz 下一次运算约 8.71us。


6. AXI4-Stream 接口:五通道与握手
FFT IP 全部接口采用标准 AXI4-Stream 协议。握手机制:

- TVALID 由源端(主机)驱动:数据有效;
- TREADY 由接收端(从端)驱动:可以接收;
- 两者同拍为高 → 一次传输完成。
五个通道中本实验用到三个:
- 配置通道(s_axis_config):字段从 LSB 起依次为 NFFT(可选)、CP_LEN(可选)、FWD_INV(必选,1=正变换 0=逆变换)、SCALE_SCH(缩放计划,可选)。本实验只需置 FWD_INV=1;
- 数据输入通道(s_axis_data):TDATA 中装 XN_RE(实部)+ XN_IM(虚部),各 9 位,每个字段要填充到 8bit 的倍数,所以实部占 [8:0],虚部占 [24:16],中间 7 位填充;
- 数据输出通道(m_axis_data):TDATA 输出 XK_RE/XK_IM(块浮点模式下位宽与输入相同),TUSER 携带 XK_INDEX(频点索引)和 BLK_EXP(块指数)。本实验观察到 tuser 高 8 位为 8,表示输出被右移 8 位(缩放了 256 倍)——计算真实幅值时要记得把缩放补回来。
仿真验证频点计算是否正确:256 个采样点里出现 4 个完整正弦周期,则正弦周期 = (256/4)×40ns = 2560ns,频率 ≈ 390.6KHz。FFT 输出在第 5 个频点(非 0)出现峰值,按公式:频率 = (25MHz/256)×(5-1) ≈ 390.6KHz,与输入一致。第 0 个频点是直流分量。
7. 数据取模模块:从复数到幅值
FFT 输出是复数(实部+虚部),LCD 上画不了复数,要取模:|XK| = √(RE² + IM²)。

平方好办(乘法器),开平方用 CORDIC IP 的 Square Root 模式。CORDIC IP 关键配置:Functional Selection 选 Square root;Data Format 选 Unsigned Integer(输入 16 位无符号整数);Round Mode 选 Nearest Even;Flow Control 选 NonBlocking(非阻塞:无输出反压、不要求完整握手,用起来简单)。
模块代码分两步:
//第一步:补码转原码,再取平方和
if(source_real[8]==1'b0) //符号位0,正数,补码即原码
data_real <= source_real[7:0];
else
data_real <= ~source_real[7:0] + 1'b1; //负数:取反加一
//虚部同理...
source_data <= (data_real * data_real) + (data_imag * data_imag);
//第二步:CORDIC 开平方
cordic_0 u_cordic_0 (
.aclk (clk ),
.s_axis_cartesian_tvalid (source_valid_d[1] ),
.s_axis_cartesian_tdata (source_data ), //16位平方和
.m_axis_dout_tvalid (data_valid ),
.m_axis_dout_tdata (data_modulus ) //开方结果
);
注意两个细节:
- 输入位宽是 9 位补码,取模只用低 8 位:先看符号位,负数则"取反加一"转原码(取模只关心幅值,符号丢弃);
- 延迟要对齐:补码转换 1 拍 + 平方和 1 拍 + CORDIC 内部 6 拍 ≈ 共 8 拍延迟,所以
source_eop要打 8 拍后再输出(data_eop = source_eop_d[7]),保证"包结束"标志和最后一个幅值数据同拍到达后级。这种"延迟对齐"是流水线设计里非常常见的处理手法。
另外从仿真可以看到:data_valid 拉高后第 5 个采样点和拉低前第 4 个采样点的幅值相同(都是 64)——这正是 FFT 的共轭对称在真实数据中的体现。
8. RAM 读写控制 + LCD 显示

这里有个设计取舍值得琢磨:为什么用 RAM 而不是 FIFO? 因为 LCD 每一行都要取一遍 128 个频谱点来画图,而 FFT 每帧才算一次——数据要被"反复读",FIFO 读走就没了,RAM 才能满足。
写端:fft_valid 作写使能,写地址随数据累加,fft_eop 时清零。
读端:LCD 显示模块发 data_req 请求时,读地址和频谱位置 fft_point_cnt 同步累加;fft_point_done(当前频谱画完)时清零。只取前 128 个点:
//产生数据无效标志,因为FFT 的数据是对称的,所以只要取前一半数据就可以了
else if(ram_raddr == TRANSFORM_LEN/2)
data_invalid <= 1'd1;
assign ram_data_out = data_invalid ? 8'd0 : ram_rd_data;
读到 128 之后把数据置 0(data_invalid),LCD 上就只画有效的前半段频谱。
LCD 显示模块在 LCD 彩条显示实验的框架上修改:根据每个频谱点的幅值画柱状高度,同时叠加坐标轴,结构上其余不变。
9. 下载验证(简述)
硬件连接与"高速AD/DA 实验"相同:高速 AD/DA 模块插到开发板对应插座,并且用导线/杜邦线把模块的 DA 输出端口与 AD 输入端口短接(让 DA 发出的模拟信号直接喂给 AD);LCD 屏接 RGB LCD 接口。程序下载后,LCD 屏上会出现频谱柱状图:
- 默认显示混频波形的频谱——应该能看到两根谱线(对应 ROM0 的 ≈390.6KHz 和 ROM1 的 ≈1.56MHz 两个频率分量);
- 按一次 KEY0 切到 ROM0 单正弦——只剩一根谱线;
- 再按一次切到 ROM1 单正弦——谱线跳到更高频率处。
观察谱线位置随按键切换而移动、且与理论频率对得上,说明"波形产生 → DA/AD 回环 → FFT → 取模 → 显示"整条链路工作正常。
初学者容易踩的坑
-
忘记 FFT IP 输入是有符号数:AD 采集的是 8 位无符号数,直接送进 IP 会把 128 以上的值当负数处理,频谱完全错乱。必须高位补 1bit 0 扩成 9 位再送(本实验的做法)。
-
TDATA 字段没做 8bit 对齐:AXI4-Stream 的每个字段都要填充到字节的倍数。9 位实部后面要垫 7 位填充才是 16 位,算错字段偏移就会把实部虚部读串。
-
漏发 s_axis_data_tlast:FFT 靠 TLAST 判断一帧结束。用普通 FIFO(非 FWFT)或忘了在最后一个数据处拉高 TLAST,FFT 就一直等不到帧尾,输出通道永远不出数据。
-
忽略块浮点的缩放系数:块浮点模式下输出被右移了 N 位(本实验 tuser 显示缩放 256 倍),直接拿输出当幅值会小得看不见。理解 BLK_EXP/tuser 字段才能正确还原。
-
FIFO 模式选错:读端逻辑是按 FWFT(预读)模式写的——读使能直接当 tvalid 用。如果 FIFO 配成标准模式,第一个数据会晚一拍,且"读使能=数据有效"的假设不成立。
-
延迟没对齐就把 eop 打拍送出:取模链路总共 8 拍延迟,若 eop 不打同样的 8 拍,"包结束"会比最后一个幅值数据早到 8 拍,后级 RAM 写地址在末尾错位。
-
混频数据直接截位:两个 8 位数相加是 9 位,不除以 2 直接取低 8 位会溢出(最高位丢掉),波形削顶、频谱出现乱七八糟的谐波。
-
AD 采样时钟没用 90° 相移版本:
clk_25m_deg相位偏移 90° 就是为了避开数据变化沿采样。直接用同相位的 25MHz 采样,建立保持余量不足,数据可能出现亚稳态或采错。
小结 & 下一步
本章完成了一个五脏俱全的迷你频谱仪:FPGA 产波 → DA/AD 回环 → FIFO 跨时钟域凑 256 点 → FFT IP 做变换 → CORDIC 取模 → LCD 画频谱。最值得带走的三样东西:FFT 的基本数学(采样定理、模值=A×N/2、共轭对称、分辨率 F/N)、FFT IP 核的 AXI4-Stream 五通道模型与配置要点、"跨时钟域 FIFO + TLAST 帧尾 + 延迟对齐"这套流水线衔接手法。有了这章的基础,下一章我们学习 FIR 滤波器——正好可以把本章频谱里看到的"噪声"真正滤掉。