尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

基于CORDIC的8点FFT蝶形运算FPGA实现与验证

发布时间:2026/9/15 14:25:53

资讯中心
01
ARTICLE

基于CORDIC的8点FFT蝶形运算FPGA实现与验证

基于CORDIC的8点FFT蝶形运算FPGA实现与验证
简介一套基于坐标旋转数字算法实现快速傅里叶变换的Verilog工程源码运行在Vivado平台面向数字信号处理与FPGA设计人员也适合相关课程项目与毕业设计参考。项目以蝶形运算结构为核心通过坐标旋转迭代完成复数乘法减少硬件资源消耗并配有测试台文件用于功能验证适合想深入理解FFT硬件实现的开发者。资源包共78个文件约10.28MB其中含7个Verilog源码文件、6个波形仿真文件、8个日志文件以及脚本与工程配置文件等文件类型集中在v、wdf、log、tcl、bat等可直接恢复Vivado工程并复现仿真结果。已有261人学习。该资源最大价值在于完整呈现从CORDIC原理、蝶形运算到顶层集成的设计链路对照源码可理解旋转因子如何映射到硬件电路参考测试台可学习激励编写与波形检查方法是FPGA平台上手FFT实现的实用样例。1. CORDIC 算蝶形比查表好在哪FFT 的蝶形运算里最花资源的是那组旋转因子 W_N^k。直接查表的话块 RAM 和查找表都要为 cos/sin 各存一份点数超过 256 后占用明显CORDIC 则把复数旋转拆成移位和加减法用迭代角度累加替代 ROMVivado 上实现 8 点 FFT 时可以做到不用硬核乘法器也能按时序收敛。这篇按 CORDIC 旋转模式、蝶形单元、地址状态机、testbench 一路讲下来最后会给出和 FFT IP 核做交叉验证的 6 个参数。适合不想直接调 IP、想把数据流抠明白的 Verilog 开发者也适合评估小点数、低资源实现的 FPGA 工程师。2. CORDIC 旋转模式与旋转因子的角度定标2.1 蝶形里乘 W 为什么可以换成“旋转”8 点 FFT 每一级蝶形都长这样p a bW_N^kq a - bW_N^k。把 W 换成直角坐标就是cosθ - j sinθ于是bW等价于把复数 b 的向量按顺时针旋转 θ。查表方案把 cos 和 sin 分开存内存翻倍CORDIC 旋转模式则把这次复数乘降成两组移位加一个方向判断迭代 12 次后得到的实部虚部就是bW的实虚部。刚接触这套做法的人通常会问CORDIC 不是算 atan 和开方用的吗实际上旋转模式收敛的是“把输入向量转到我指定的角度”计算结果里自然带有 cos/sin 的乘积。蝶形里不需要单独抽出 W 的实部虚部直接把 b 的实部虚部送进 CORDIC输出就是乘完旋转因子后的值。这样蝶形单元只剩加法和减法。2.2 角度定标与反正切表生成CORDIC 的角度累加器 z 在 RTL 里不能直接用浮点弧度否则综合会做出一堆比较器。常见做法是把角度按半周归一化[-π, π)映射到[-1, 1)量化系数取1 (DW-1)。16 位定标下45° 对应π/4 / π * 32768 8192。因为 FFT 用的是W e^{-jθ}所以送入 CORDIC 的角度不是正 θ而是负 θ。8 点 FFT 前 4 个旋转因子的角度字如下旋转因子实际相角CORDIC 输入角度实值16 位角度字W_8^0000W_8^1-45°-π/4-8192W_8^2-90°-π/2-16384W_8^3-135°-3π/4-24576这个符号非常容易翻错。如果做的是逆变换输入角度全部取反即可正变换写成负角CORDIC 输出的就是b * e^{-jθ}。2.3 旋转模式核心迭代代码CORDIC 迭代公式的核心是一个组合逻辑块每次迭代根据 z 的符号决定旋转方向// CORDIC 旋转模式单次迭代it 从 0 开始 // DW16, atan_table[it] atan(2^-it) / π * 32768 wire signed [DW-1:0] x_shift x_cur it; wire signed [DW-1:0] y_shift y_cur it; always (*) begin if (z_cur[DW-1]) begin // z 为负往正方向旋 x_next x_cur y_shift; y_next y_cur - x_shift; z_next z_cur atan_table[it]; end else begin // z 为正往负方向旋 x_next x_cur - y_shift; y_next y_cur x_shift; z_next z_cur - atan_table[it]; end end代码里的是算术右移保证负数符号位扩展正确DW是数据位宽it是当前迭代序号。实际 RTL 中要把x_next/y_next/z_next寄存到下一拍的x_cur/y_cur/z_cur形成流水线迭代 12 次就是 12 级寄存器链。角度表不需要手敲用下面脚本生成后粘进 localparam 即可import math DW 16 for i in range(12): val int(round(math.atan(2**-i) / math.pi * (1 (DW-1)))) print(f{val:#06x}, // {math.atan(2**-i):.6f} rad)迭代次数超过 12 后角度表低位的量化增量已经小于 1 LSB继续增加会让寄存器数量线性上涨精度收益却很小。2.4 增益补偿An 和定标截位CORDIC 每次旋转都会把向量模长放大√(12^{-2i})收敛后总增益An ≈ 1.64676。补偿不能漏否则 FFT 输出整体偏大 1.6 倍做频谱幅度比较时会被人怀疑是逻辑错误。常见做法是在数据进入蝶形单元前先缩小而不是在输出端乘回去localparam integer K 16d19900; // 1/An * 32768 ≈ 19900 wire signed [DW-1:0] in_cm_i (in_i * K) 15; wire signed [DW-1:0] in_cm_q (in_q * K) 15;先缩小能让中间级数据留出些余量避免第一级蝶形刚算完就接近饱和。迭代次数不同收敛增益也不同下表是常见量级迭代次数剩余角度误差量级典型定点噪声影响8约 4e-3 rad8~10 bit 数据够用12约 2.4e-4 rad12~16 bit 数据够用16约 1.5e-5 rad16 bit 数据可接受如果数据位宽只有 8 位迭代 8 次足够如果做到 16 位建议至少 12 次再往上主要影响面积。3. 蝶形运算单元与地址状态机8 点 FFT 结构3.1 按时间抽取的三级蝶形数据流8 点 FFT 需要 3 级蝶形每级 4 个蝶形共 12 个蝶形。输入数据先做位反转然后逐级原地更新。传统写法用两层循环内层循环的旋转因子索引和蝶形跨度都会随级数变化。级蝶形跨度蝶形输入索引旋转因子索引01(0,1)(2,3)(4,5)(6,7)全 012(0,2)(4,6) (1,3)(5,7)0, 0, 2, 224(0,4)(1,5)(2,6)(3,7)0, 1, 2, 3第 1 级全用 W_8^0等价于不用旋转第 2 级出现 0 和 2 两个索引第 3 级才用满 0~3。这个特点让 CORDIC 方案可以按“索引用 case 分支产生 z0”而不必放一张完整的角度 ROM。3.2 CORDIC 加蝶形单元例化关系用 CORDIC 计算bW后蝶形单元就不再需要乘法器。顶层里把 CORDIC 的 x/y 输出直接接到蝶形的 bw 端口// 用 CORDIC 旋转 b 向量蝴蝶只做加减 cordic_rotate #(.DW(DW), .ITER(ITER)) rot ( .clk(clk), .rst_n(rst_n), .x_in(b_i), .y_in(b_q), .angle_in(rot_angle), .x_out(bw_i), .y_out(bw_q), .valid(rot_valid) ); butterfly #(.DW(DW)) bfly ( .a_i(a_i), .a_q(a_q), .bw_i(bw_i), .bw_q(bw_q), .sum_i(o_i_plus), .sum_q(o_q_plus), .dif_i(o_i_minus), .dif_q(o_q_minus) );蝶形模块本身极其简单module butterfly #(parameter DW 16)( input wire signed [DW-1:0] a_i, a_q, input wire signed [DW-1:0] bw_i, bw_q, output wire signed [DW-1:0] sum_i, sum_q, output wire signed [DW-1:0] dif_i, dif_q ); assign sum_i a_i bw_i; assign sum_q a_q bw_q; assign dif_i a_i - bw_i; assign dif_q a_q - bw_q; endmodule这里没有做饱和处理两位 16 位定点数相加可能变成 17 位输出 wire 会直接截掉最高位。对 FFT 这种多级运算最好在写回 RAM 前加一个饱和模块否则溢出后信号可能从最大正跳变到最大负误差会传导到后面所有级。3.3 位反转与地址生成8 点输入的位反转只有 3 bit用函数最直观function [2:0] rev3; input [2:0] v; begin rev3 {v[0], v[1], v[2]}; end endfunction地址生成我习惯用 case 写出 12 个蝶形的索引比套公式更容易仿真检查和定位问题。stage和bfly各 2 bit拼起来做 casereg [1:0] stage; reg [1:0] bfly; reg [2:0] addr_a, addr_b; reg [1:0] tw_addr; always (*) begin case ({stage, bfly}) 4b0000: begin addr_a 3d0; addr_b 3d1; tw_addr 2d0; end 4b0001: begin addr_a 3d2; addr_b 3d3; tw_addr 2d0; end 4b0010: begin addr_a 3d4; addr_b 3d5; tw_addr 2d0; end 4b0011: begin addr_a 3d6; addr_b 3d7; tw_addr 2d0; end 4b0100: begin addr_a 3d0; addr_b 3d2; tw_addr 2d0; end 4b0101: begin addr_a 3d4; addr_b 3d6; tw_addr 2d0; end 4b0110: begin addr_a 3d1; addr_b 3d3; tw_addr 2d2; end 4b0111: begin addr_a 3d5; addr_b 3d7; tw_addr 2d2; end 4b1000: begin addr_a 3d0; addr_b 3d4; tw_addr 2d0; end 4b1001: begin addr_a 3d1; addr_b 3d5; tw_addr 2d1; end 4b1010: begin addr_a 3d2; addr_b 3d6; tw_addr 2d2; end 4b1011: begin addr_a 3d3; addr_b 3d7; tw_addr 2d3; end default: begin addr_a 3d0; addr_b 3d0; tw_addr 2d0; end endcase endtw_addr再经过一个小组合逻辑映射到负角度即第 2 章表格里的角度字。控制状态机用 CORDIC 的rot_valid作为推进信号每完成一个旋转就进入下一个蝶形always (posedge clk or negedge rst_n) begin if (!rst_n) begin stage 0; bfly 0; done 0; end else if (start) begin stage 0; bfly 0; done 0; end else if (rot_valid) begin if (bfly 2d3) begin bfly 0; if (stage 2) done 1; else stage stage 1; end else begin bfly bfly 1; end end end这段状态机没有把“读 RAM”和“写回 RAM”单独拆分真实工程里还需要在 CORDIC 完成前先读数据、在 CORDIC 完成后延迟一拍写回。要点是让stage/bfly作为唯一的数据流坐标这样 testbench 里数着rot_valid就能确定当前算到第几个蝶形。4. testbench、定点误差与 Vivado 仿真流程4.1 激励源直接填 cos 系数的 testbench验证 8 点 FFT 最直接的输入是cos(2πn/8)它只有 0、45°、90°、135° 等几个固定值可以直接填常数避免依赖仿真器的三角函数库timescale 1ns/1ps module tb_fft_cordic; reg clk 0, rst_n 0, start 0; reg signed [15:0] in_i [0:7], in_q [0:7]; wire signed [15:0] out_i [0:7], out_q [0:7]; wire done; integer i; initial begin in_i[0] 16sd32767; in_i[1] 16sd23170; in_i[2] 16sd0; in_i[3] -16sd23170; in_i[4] -16sd32767; in_i[5] -16sd23170; in_i[6] 16sd0; in_i[7] 16sd23170; for (i 0; i 8; i i 1) in_q[i] 16sd0; repeat(10) (posedge clk); rst_n 1; (posedge clk); start 1; (posedge clk); start 0; wait(done); for (i 0; i 8; i i 1) $display(bin[%0d] I%0d Q%0d, i, out_i[i], out_q[i]); $finish; end always #5 clk ~clk; fft_cordic_top #(.DW(16), .ITER(12)) dut ( .clk(clk), .rst_n(rst_n), .start(start), .in_i(in_i), .in_q(in_q), .out_i(out_i), .out_q(out_q), .done(done) ); endmodule这段 testbench 的检查逻辑不是看绝对值而是看 bin 1 和 bin 7 的幅度明显大于其他位置。因为输入是完整一个周期的余弦DFT 理论值会在 bin 1 和 bin 7 各出现幅度 0.5 的谱线bin 0 附近的理论值是 0实际会有定点误差。Q1.15 下最大值取 32767 而不是 32768是为了避免正负数不对称带来的溢出。4.2 用 Vivado 命令行跑回归在 Vivado 的 Tcl Console 里可以直接跑 xvlog/xelab/xsim比反复点 GUI 更省时间xvlog -sv tb_fft_cordic.sv cordic_rotate.v butterfly.v fft_cordic_top.v xelab -debug typical tb_fft_cordic xsim -runall tb_fft_cordicxvlog负责编译-sv表示允许 SystemVerilog 语法方便 testbench 里使用wait和数组端口xelab做例化和仿真库绑定xsim -runall运行到$finish结束。如果用的是老版本遇到17.1 error: failure to obtain a verilog simulation license这类错误先检查机器上是否装了 Vivado Simulator 组件再检查 License 环境变量很多情况下并不是代码问题而是安装时漏选了仿真工具。4.3 定点误差怎么统计只靠$display看几次值很难说服自己。建议把 I/Q 打印到文本文件再用 Python 和 NumPy 参考结果比较import numpy as np N 8 x np.cos(2 * np.pi * np.arange(N) / N) ref np.fft.fft(x) # 读取 testbench 输出的 bin I/Q按 Q1.15 归一化 got np.array([(I 1j * Q) for I, Q in results]) / 32768.0 err np.max(np.abs(got - ref)) print(max abs error:, err)比较前一定要把两边都归一化到同一单位否则会把定标差异当成误差。CORDIC 迭代 12 次、16 位数据时最大绝对值误差一般能压在 3 个 LSB 以内如果误差到几十个 LSB优先检查角度符号倍数关系也就是第 2 章那张角度字表有没有乘错系数。testbench 的覆盖点至少包括下面几项检查项操作通过标准频率 bin输入 cos观察第 1、7 bin幅度明显高于其他 bin相位atan2(Q, I)与 Python 对比误差 0.03 rad数据合法全程监控输出端口无 x/z无饱和翻转4.4 波形观察看角度收敛仿真时除了看最终 FFT 输出最好把 CORDIC 内部的角度字和旋转向量拉出来。在 xsim GUI 的 Tcl Console 里输入add_wave {/tb_fft_cordic/dut/rot/x_cur} add_wave {/tb_fft_cordic/dut/rot/y_cur} add_wave {/tb_fft_cordic/dut/rot/z_cur} run 500 ns看z_cur是否逐级逼近 0x_cur/y_cur是否在一个圆上收敛。这一步能快速区分“CORDIC 没算对”和“FFT 地址没走对”两类问题角度收敛但最终结果错问题多半在地址或蝶形角度本身不收敛问题在角度定标或移位方向。5. 换用 Vivado FFT IP 核前先盯住这 6 个参数5.1 两套方案交叉验证的做法手工 CORDIC 方案最容易被人质疑“有没有参考模型”。最省事的参考不是自己写浮点模型而是用 Vivado IP Catalog 直接生成一个 8 点 FFT IP 核把它的输出当 golden。IP 核配置为相同位宽、Natural Order、Fixed-point例化到 testbench 里和手工模块输入同一份激励最后逐 bin 做差。5.2 6 个参数对照表两套方案要对齐的参数不是 FFT 点数而是下面这张表参数手工 CORDIC 推荐值需要对齐的 IP 设置数据格式Q1.15 signed 16bitFixed-point, 16 bit角度符号FFT 送负角IP 核输入为复数自然序迭代深度12~16 次无直接对应看 latency输出顺序状态机自然序输出Output Order 选 Natural Order缩放策略每级饱和截位Scaled 或 Block Floating PointCORDIC 增益入点补偿 AnIP 核内部自带缩放IP 核的m_axis_data_tdata输出一般是低 16 位实部、高 16 位虚部Natural Order 下第一个 tvalid 对应 bin 0。手工模块从 start 到 done 的延迟大约等于CORDIC 迭代次数 3 级蝶形 读写 RAM 缓冲和 IP 核的延迟不同所以不能直接把两个 done 信号对齐。5.3 一个实用的对齐技巧用计数器把 FFT IP 核的 tvalid 延迟对齐到手工模块的 done是我最常用的验证方式reg [3:0] delay_cnt; wire ref_valid m_axis_data_tvalid (delay_cnt 4d0); always (posedge clk) begin if (start) delay_cnt 4d0; else if (m_axis_data_tvalid delay_cnt[3] 1b0) delay_cnt delay_cnt 1; enddelay_cnt补偿 IP 核内部从配置输入到结果输出的流水级数。当ref_valid为高时开始读m_axis_data_tdata和手工模块的out_i/out_q按同一个 bin 对齐。把两侧输出存成文本后用第 4 章的 Python 脚本算最大绝对值误差连续用几组随机数压测如果能稳定压在 3 LSB 以内就可以认为数据通路正确。剩下的事是根据时序报告决定 CORDIC 迭代次数继续压还是放宽。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。