尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

FPGA实现FFT蝶形运算:用CORDIC替代乘法器的硬件设计

FPGA实现FFT蝶形运算:用CORDIC替代乘法器的硬件设计 简介面向FPGA与数字信号处理开发者的Verilog实现工程基于CORDIC算法构建蝶形运算单元完成FFT可在Vivado平台直接打开工程运行并附带testbench测试源码适合正在学习硬件FFT实现、数字信号处理算法落地的工程师或学生。压缩包共78个文件约10.28MB核心包含cordic.v、butterfly.v、main_cordic_fft.v等RTL源码以及仿真testbench、Vivado工程文件、波形文件与综合脚本覆盖设计输入、逻辑综合、仿真验证全流程目录结构经过整理便于检索。目前已有261人学习。通过研读工程可以理解CORDIC迭代逼近复数旋转的硬件实现原理掌握Verilog模块划分与蝶形运算级联方法还能学习测试bench激励生成、结果比对与波形查看的验证思路为独立开发更复杂的FFT或数字信号处理IP积累实践经验。1. 当FFT蝶形运算抛弃乘法器CORDIC旋转的硬件解题思路在FPGA上做FFT最容易踩的坑不是时序收敛而是DSP48乘法器资源不够。8点FFT看不出问题扩到64点、256点时蝶形运算每一级的复数乘法会迅速蚕食硬核乘法器综合报告里LUT和DSP使用率双双飙升。CORDIC算法用移位和加减法逼近目标旋转角度把一个复数乘法从4个乘法器降为0个乘法器这正是本工程的核心取舍。工程基于Verilog在Vivado上实现用cordic.v、butterfly.v、main_cordic_fft.v搭出完整FFT流水线并配齐cordic_tb、butterfly_tb、main_tb三个测试平台。适合不满足于直接调用FFT IP核、想深入内部结构的FPGA工程师也适合DSP硬核资源紧张但需要频域分析的信号处理项目。下面按CORDIC原理、蝶形级联、工程组织、验证技巧的顺序拆开讲。2. CORDIC旋转模式下的蝶形因子生成与复数乘法2.1 蝶形因子本质上是旋转不是通用乘法FFT的蝶形运算在硬件里只干一件事X(k) A(k) W·B(k)其中W e^(−j2πr/N)落在单位圆上所以“乘以W”就是“把B向量旋转固定角度”。直接实现这个复数乘法需要4个实数乘法器和2个实数加减法16位定点下每个乘法器在7系列FPGA上对应一个DSP48E1级联的时候关键路径还要插流水寄存器。N点FFT一共有N/2×log2(N)个蝶形资源随点数线性上涨。CORDIC换了个角度因为W的模长为1而且FFT的旋转角在每一级是预先确定的所以可以用“移位加减”逼近旋转角一步乘法都不做。相比直接调用CORDIC IP核手写RTL的好处是逐级可观测哪儿溢出、哪儿丢精度都能在仿真时单独拉出来检查代价是迭代级数要自己平衡。2.2 旋转模式迭代角度残差驱动方向CORDIC旋转模式的核心是“拆角”。把目标角拆成一串递减固定角45°、26.565°、14.036°、7.125°、3.576°……逐次逼近。第i级迭代中判断当前角度残差的符号符号为正就向顺时针方向旋转atan(2⁻ⁱ)符号为负就逆时针转。每次旋转对向量(x,y)的修正很简单x x − d·y·2⁻ⁱy y d·x·2⁻ⁱ其中d由残差符号决定2⁻ⁱ即右移i位。这样迭代一圈得到的(x,y)就是“旋转了目标角度”的结果只是长度被放大了一个固定倍数1.64676这个倍数和迭代次数无关由CORDIC算法的收敛特性决定业界叫CORDIC增益。注意迭代不是“向量直接到达目标角”而是依据残差逐步逼近级数越多残差越小。8次迭代角度误差约0.447°16次就到了0.0017°再往上提升很小但中间寄存器的位宽会持续增长。这决定了FFT蝶形模块的位宽策略不在CORDIC内部扩展太多把缩放放在蝶形加减之后统一处理。2.3 cordic.v的16级流水线实现与参数本工程的cordic.v就是按这个迭代展开的16级流水线每一级寄存器独立打拍valid信号逐级传递。核心结构示意如下代码按工程实际风格整理过// cordic.v 旋转模式核心逻辑16级流水 // DW16 数据位宽ITER16 迭代级数 module cordic_rotate #( parameter DW 16, parameter ITER 16 )( input wire clk, input wire rst_n, input wire valid_in, input wire signed [DW-1:0] x_in, // 实部输入 input wire signed [DW-1:0] y_in, // 虚部输入 input wire signed [DW-1:0] angle_in, // 旋转目标角 output reg signed [DW-1:0] x_out, output reg signed [DW-1:0] y_out, output reg valid_out ); reg signed [DW-1:0] x_s [0:ITER]; reg signed [DW-1:0] y_s [0:ITER]; reg signed [DW-1:0] a_s [0:ITER]; reg valid_s [0:ITER]; // 角度表前四项示意数值按弧度定点格式给出 // 实际工程用脚本生成保证 atan(2^-i) 与 angle_in 定点格式一致 reg signed [DW-1:0] atan_lut [0:ITER-1]; initial begin atan_lut[0] 16h3244; // 0.7854 rad (45°) atan_lut[1] 16h1DA9; // 0.4636 rad (26.565°) atan_lut[2] 16h0FAF; // 0.2450 rad (14.036°) atan_lut[3] 16h07F5; // 0.1244 rad (7.125°) // 剩余项按 atan(2^-i) 定点展开共 ITER 项 end // 输入级打拍 always (posedge clk or negedge rst_n) begin if (!rst_n) begin x_s[0] {DW{1b0}}; y_s[0] {DW{1b0}}; a_s[0] {DW{1b0}}; valid_s[0] 1b0; end else begin x_s[0] x_in; y_s[0] y_in; a_s[0] angle_in; valid_s[0] valid_in; end end genvar i; generate for (i 0; i ITER; i i 1) begin : gn_cordic wire angle_neg a_s[i][DW-1]; // 符号位1负角度残差 always (posedge clk or negedge rst_n) begin if (!rst_n) begin x_s[i1] {DW{1b0}}; y_s[i1] {DW{1b0}}; a_s[i1] {DW{1b0}}; valid_s[i1] 1b0; end else if (valid_s[i]) begin if (angle_neg) begin // 残差为负逆时针旋转 x_s[i1] x_s[i] (y_s[i] i); y_s[i1] y_s[i] - (x_s[i] i); a_s[i1] a_s[i] atan_lut[i]; end else begin // 残差为正顺时针旋转 x_s[i1] x_s[i] - (y_s[i] i); y_s[i1] y_s[i] (x_s[i] i); a_s[i1] a_s[i] - atan_lut[i]; end valid_s[i1] 1b1; end else valid_s[i1] 1b0; end end endgenerate // 末级输出 always (posedge clk or negedge rst_n) begin if (!rst_n) begin x_out {DW{1b0}}; y_out {DW{1b0}}; valid_out 1b0; end else begin x_out x_s[ITER]; y_out y_s[ITER]; valid_out valid_s[ITER]; end end endmodule代码里三个关键点要提醒。第一是算术右移有符号负数右移时补符号位必须与区分否则迭代到负数时结果会偏差很大。第二valid_s逐级打拍是为了让数据和使能对齐valid_out比x_out晚ITER个周期输出后续蝶形单元的加法必须等valid_out拉高后再取数。第三angle_in的定点格式要和角度表一致工程里如果输入是角度值而非弧度值角度表也要按同样格式扩展。这个模块没有补偿CORDIC增益补偿是放在蝶形级之后的截位环节完成的。参数上ITER选16级是因为这个工程的FFT点数是8~64点量级16级迭代的0.0017°误差远小于最小旋转因子间隔如果做1024点最小角度间隔约0.35°12级迭代也够用。提示与在综合时会推断出不同的运算单元有符号定点数据一律使用算术右移仿真和综合结果才一致。2.4 迭代级数、精度与资源权衡综合一次16级迭代的简单工程资源消耗大概如下表这些数据在项目综合报告里能看到这里按16位输入为例列出参考值迭代级数角度误差估计LUT估计FF关键路径80.447°约120约140较短120.028°约160约200中160.0017°约220约270较长200.0001°约280约360最长迭代级数与FFT点数是联动关系FFT点数越大第一级蝶形的旋转因子间隔越小CORDIC级数就需要越多才能区分相邻的旋转角但这个增长不是线性的。16级到20级误差下降了一个数量级资源只增加四分之一左右所以实际工程做到16~20级之间性价比最高。如果发现频谱峰值位置偏差先查CORDIC级数是不是小于12再看旋转因子角度表的定点格式是不是和实际一致。3. 从单级蝶形到多级级联的蝶形运行结构设计3.1 butterfly.v如何把CORDIC结果变成蝶形输出当CORDIC完成复数乘法后butterfly.v要做的事情收敛为“对齐、相加、相减、截位”四件事。对齐的含义是旋转结果经过ITER16个时钟周期从cordic_rotate输出与其配对的A路数据也必须延迟相同的周期否则减法算出来的就是错位数据。常见做法是在butterfly.v里放一组移位寄存器// butterfly.v 中同步对齐 A 路输入 // 对齐延时 CORDIC_ITER 个周期 reg signed [DW1:0] ar_delayed [0:ITER]; reg signed [DW1:0] ai_delayed [0:ITER]; always (posedge clk) begin ar_delayed[0] ar; // ar 为蝶形输入实部 ai_delayed[0] ai; // ai 为蝶形输入虚部 end genvar j; generate for (j 1; j ITER; j j 1) begin : gn_delay always (posedge clk) begin ar_delayed[j] ar_delayed[j-1]; ai_delayed[j] ai_delayed[j-1]; end end endgenerate这段代码的作用是把A路复数信号同样延迟16拍再送去和CORDIC输出做加减。实际工程里要注意延时链会多消耗LUT和FF资源所以有些设计会把A路输入直接接到更高级别的缓冲寄存器而不是在butterfly.v内部做链式延迟但这个项目的做法是在butterfly内部统一维护方便替换蝶形级联数。之后是加减法代码结构和2.3节末尾一致。截位是蝶形级最容易出错的环节CORDIC输出没补偿增益蝶形加法结果位宽从DW变成DW1如果每级都不截位到FFT最后一级结果位宽会增长log2(N)位。常见做法是每级蝶形输出处保留DW1位或者做一次带符号饱和截位牺牲少量精度换取位宽可控。Vivado FFT IP核内部是自动处理的但手写RTL的时候这个位宽策略必须自己定。3.2 原位运算与位翻转寻址FFT的标准做法是输入按照位翻转顺序进入第一级蝶形之后输出自然排序。8点输入的位翻转映射可以直接查表输入序列号二进制位翻转后序列号0000010014201023011641001510156110371117硬件实现位翻转常见三种方式RAM初始化时把数据写到翻转地址地址生成器每级跳变翻位或者采用流水线输入自然序、输出交换顺序。main_cordic_fft.v里的地址线采用了后两种方式结合的做法因为它本身是流式输入数据到达顺序固定用地址翻转比重新排列RAM写地址更节省控制逻辑。3.3 级间缓冲与流式调度由于CORDIC有固定延迟而FFT多级级联时必须等待所有上一级输入数据到达才能开始下一级。对于N8每一级需要8个时钟周期收集数据再加上CORDIC延迟和蝶形输出延迟总周期约N×log2(N)再加上ITER×log2(N)个时钟。如果使用乒乓buffer缓存区是一块双口RAM一写一读同时进行吞吐才能提升。这种流式调度在main_cordic_fft.v里面有体现工程里的valid信号线基本就是为流式调度准备的。调试时最容易看到的怪现象是仿真波形里蝶形输出完全正确但FFT最终输出乱序这时候80%是级间valid时序没对齐20%是位翻转地址在某一级翻错了。用工程里的butterfly_tb单独验证每一级比直接跑到main_tb再回头查快得多。4. Vivado工程组织与testbench分层仿真验证4.1 project_2目录结构与文件角色这个zip文件解压以后是project_2工程核心设计只有三个Verilog源文件其余是Vivado自动生成的目录。不要删除.cache和.runs否则工程打开会重新综合。各文件职责如下表文件/目录角色cordic.vCORDIC旋转模块迭代逼近旋转因子butterfly.v蝶形单元做复加法/复减法、对齐main_cordic_fft.v顶层FFT控制模块集成多级蝶形cordic_tb.v单元级验证给cordic注入角度检查旋转结果butterfly_tb.v模块级验证给butterfly注入两路数据比对加减结果main_tb.v系统级验证送完整数据序列检查FFT输出project_2.xprVivado工程文件双击打开project_2.runs综合/实现后的网表与报告project_2.sim仿真目录存放行为级仿真结果project_2.cache增量编译缓存提高重编译速度project_2.ip_user_filesIP核用户文件本工程没调用IP核则为空注意这几个testbench可以独立运行也可以在Vivado的仿真设置里分别指定。如果打开工程后直接run simulation默认调用main_tb.v其他两个需要在仿真设置中切换顶层才能跑。4.2 testbench的激励与参考模型设计cordic_tb最直接输入一组已知角度比如45°验证x_out、y_out是不是预期的旋转结果。butterfly_tb则做一个固定输入表给A(1,0)、B(0,1)、W(0,1)那么B*W(-1,0)蝶形输出应该是(0,0)和(2,0)。手动算完再对照波形比直接跑全FFT仿真容易定位问题。main_tb的系统级仿真基本思路是生成一组已知频点的正弦信号接入FFT把输出频谱与理论结果比较。核心代码如下// main_tb.v 关键片段生成单音正弦激励并送入FFT timescale 1ns/1ps module main_tb; reg clk 0; reg rst_n 0; reg start 0; reg [15:0] din_re, din_im; wire [15:0] dout_re, dout_im; wire valid_out; // 100MHz 时钟周期 10ns always #5 clk ~clk; // 8点FFT输入8kHz采样1kHz正弦幅度16384 reg [15:0] stimulus [0:7]; integer i; initial begin $readmemb(fft_input.txt, stimulus); rst_n 0; #100 rst_n 1; #20; for (i 0; i 8; i i 1) begin (posedge clk); start (i 0) ? 1b1 : 1b0; // 首拍拉高启动脉冲 din_re stimulus[i]; din_im 0; end start 0; #2000 $finish; end // 顶层例化端口名以 main_cordic_fft.v 实际声明为准 main_cordic_fft u_fft ( .clk(clk), .rst_n(rst_n), .start(start), .din_re(din_re), .din_im(din_im), .dout_re(dout_re), .dout_im(dout_im), .valid_out(valid_out) ); endmodule这次的激励是8kHz采样率下的1kHz正弦波8点FFT会把能量放到频率点1也就是第1个bin。如果不方便预先算好定点数据也可以在initial里用仿真器的正弦函数生成但这需要依赖仿真器支持。start仅在第一个数据时拉高一拍之后交给顶层内部控制相比每个周期都持续拉高更接近实际硬件端的握手启动方式。4.3 Vivado编译仿真流程与常用命令在Vivado里跑仿真的标准流程是新建工程、添加三个设计文件、添加三个testbench文件然后Run Behavioral Simulation。仿真自动打开后把dout_re、dout_im、valid_out加到波形窗口运行2us足够看到8点FFT完整输出。若要用命令行跑xsim工具链对应的流程是# 编译设计文件 xvlog -i . cordic.v butterfly.v main_cordic_fft.v # 编译测试文件 xvlog -i . cordic_tb.v butterfly_tb.v main_tb.v # 链接仿真可执行文件顶层为 main_tb xelab main_tb -timescale 1ns/1ps -debug typical # 运行仿真直到 $finish xsim main_tb --runall第一条xvlog把三个设计文件编译进默认库work第二条把testbench编译进同一个库xelab阶段指定顶层为main_tb-debug typical支持导出波形--runall表示跑完整个仿真直到$finish。需要注意如果testbench里用了$readmemb文件路径默认是仿真运行目录路径不对会出读写警告实际调试时建议先改成绝对路径。5. CORDIC FFT结果验证中的三个关键检查点5.1 检查点1频谱峰值是否落在正确的bin上用1kHz正弦、8kHz采样、8点FFT理论上只有第1个bin有能量。仿真结束后在Vivado波形窗口里找dout_re、dout_im用光标测量峰值位置。如果峰值出现在bin 1说明位翻转和蝶形级联的地址逻辑没有错如果出现在bin 0或bin 2~3说明输入数据没有按位翻转顺序进入或者CORDIC角度表配错了。注意CORDIC输出是复数频谱的bin幅度要做平方和再开根dout_re和dout_im单看一路不可靠。5.2 检查点2valid信号是否和数据对齐CORDIC固定延迟16个周期每一级蝶形又有排队所以dout_re从start拉高到valid_out拉高的时间不是固定值这个延迟大约是迭代级数加若干级缓存之和。在波形窗口用两个Marker分别标start上升沿和valid_out上升沿检查输出数据和valid_out边沿是否对齐在同一个时钟域。如果valid_out比数据早一个周期或者晚一个周期后级采到的就是错位数据。xsim里看到这个现象多数是testbench启动时少等了一个时钟少数是蝶形级间valid打拍写错。5.3 检查点3CORDIC增益补偿是否正确如果输入正弦幅度为16384那么FFT输出在bin 1的幅度理论上是输入幅度乘以N/2即65536。因为CORDIC没有补偿1.64676倍增益蝶形内部又每级加宽1位实际频谱幅值会和理论值差一个固定比例。验证方法是记录bin 1的实部虚部算sqrt(re²im²)再除以理论值看结果是否在1.64676附近。如果是说明增益是CORDIC产生的正常放大在后续设计里通过右移或乘法器归一即可如果不是就要查截位逻辑是否把符号位截掉了。这三个检查点做完就可以把这个CORDIC FFT接入到实际频谱显示或信号检测模块里。剩下要调的参数集中在main_cordic_fft.v顶部CORDIC迭代级数决定角度精度蝶形内部位宽决定动态范围valid握手逻辑决定和外部模块的衔接方式这几个参数在工程里都是独立可改的。本文还有配套的精品资源点击获取
返回列表