第五十二章:基于高速ADDA的频谱仪实验(初学者详解)

本章在讲什么?(先看这个)

前面几章我们一直在"传输"信号(以太网、视频),这一章换一个玩法:分析信号。实验任务是:

  1. FPGA 自己产生正弦波(按键可切换 3 种波形),经高速 DA 芯片变成模拟信号;
  2. 把高速 AD/DA 模块的 AD 端和 DA 端直连,模拟信号再经 AD 芯片采回 FPGA;
  3. 对采回的 256 个采样点做 FFT 变换(用 Vivado 自带的 FFT IP 核);
  4. 对 FFT 结果取模得到幅值,最后把频谱画在 LCD 屏上。

一句话概括:做一个迷你频谱仪。这个实验最大的收获是学会两件事:FFT 到底在算什么、FFT IP 核(AXI4-Stream 接口)怎么用。前者是信号处理的地基,后者是今后做任何通信/音频/图像频域处理的必备技能。

核心概念拆解

1. FFT:把信号从时域搬到频域

FFT(快速傅里叶变换)是 1965 年由库利和图基提出的,它把 DFT(离散傅里叶变换)的计算量大幅降低——采样点数 N 越多,省得越多

为什么需要频域?看个例子。下面是一个 50Hz 和 120Hz 正弦波叠加了噪声的时域波形:

原始波形

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

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模块:按键切换三种波形

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

FIFO控制模块框图

写端(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 不结束运算。

FIFO写端波形图

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

FFT IP 选项卡配置

FFT IP 最终配置界面

6. AXI4-Stream 接口:五通道与握手

FFT IP 全部接口采用标准 AXI4-Stream 协议。握手机制:

AXI4-Stream 握手机制

  • TVALID 由源端(主机)驱动:数据有效;
  • TREADY 由接收端(从端)驱动:可以接收;
  • 两者同拍为高 → 一次传输完成

五个通道中本实验用到三个:

  1. 配置通道(s_axis_config):字段从 LSB 起依次为 NFFT(可选)、CP_LEN(可选)、FWD_INV(必选,1=正变换 0=逆变换)、SCALE_SCH(缩放计划,可选)。本实验只需置 FWD_INV=1;
  2. 数据输入通道(s_axis_data):TDATA 中装 XN_RE(实部)+ XN_IM(虚部),各 9 位,每个字段要填充到 8bit 的倍数,所以实部占 [8:0],虚部占 [24:16],中间 7 位填充;
  3. 数据输出通道(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读写控制模块写端时序图

这里有个设计取舍值得琢磨:为什么用 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 → 取模 → 显示"整条链路工作正常。

初学者容易踩的坑

  1. 忘记 FFT IP 输入是有符号数:AD 采集的是 8 位无符号数,直接送进 IP 会把 128 以上的值当负数处理,频谱完全错乱。必须高位补 1bit 0 扩成 9 位再送(本实验的做法)。

  2. TDATA 字段没做 8bit 对齐:AXI4-Stream 的每个字段都要填充到字节的倍数。9 位实部后面要垫 7 位填充才是 16 位,算错字段偏移就会把实部虚部读串。

  3. 漏发 s_axis_data_tlast:FFT 靠 TLAST 判断一帧结束。用普通 FIFO(非 FWFT)或忘了在最后一个数据处拉高 TLAST,FFT 就一直等不到帧尾,输出通道永远不出数据。

  4. 忽略块浮点的缩放系数:块浮点模式下输出被右移了 N 位(本实验 tuser 显示缩放 256 倍),直接拿输出当幅值会小得看不见。理解 BLK_EXP/tuser 字段才能正确还原。

  5. FIFO 模式选错:读端逻辑是按 FWFT(预读)模式写的——读使能直接当 tvalid 用。如果 FIFO 配成标准模式,第一个数据会晚一拍,且"读使能=数据有效"的假设不成立。

  6. 延迟没对齐就把 eop 打拍送出:取模链路总共 8 拍延迟,若 eop 不打同样的 8 拍,"包结束"会比最后一个幅值数据早到 8 拍,后级 RAM 写地址在末尾错位。

  7. 混频数据直接截位:两个 8 位数相加是 9 位,不除以 2 直接取低 8 位会溢出(最高位丢掉),波形削顶、频谱出现乱七八糟的谐波。

  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 滤波器——正好可以把本章频谱里看到的"噪声"真正滤掉。