FEATURED · 精选文章

MATLAB手写基-2 FFT:蝶形运算、位反转与精度对比

发布时间 / 2026/9/18 10:58:48
来源 / 创域科博编辑部
栏目 / 资讯中心
MATLAB手写基-2 FFT:蝶形运算、位反转与精度对比 简介《老生谈算法》系列的MATLAB实现FFT算法程序文档面向数字信号处理初学者、通信类课程学生及需用频谱分析的MATLAB开发者系统讲解快速傅里叶变换的数学原理与工程实现。文档先阐明FFT结果中每个频率点的物理含义包括复数的模与相位、直流分量倍率、N/2幅值关系、频率分辨率与采样时间的关系以及结果的对称性质。随后以采样频率100Hz、采样点数128、频率10Hz的正弦信号和矩形波为对象给出完整的MATLAB源代码演示幅值谱、均方根谱、功率谱、对数谱的绘制方法并通过IFFT恢复时域波形。整个压缩包仅含1个docx文件大小15KB便于阅读和复制代码适合课程设计、通信信号处理实验以及复习FFT核心概念的快速参考。目前已有354人学习下载对初学者理解时频变换和频谱图解读有直接帮助。1. 为什么在 MATLAB 里还要手动实现一遍 FFTfft(x)在 MATLAB 里几乎是最常用的一行信号处理代码但“会用”和“能实现”是两回事。自己实现一遍 FFT 不是要替代内置函数而是为了看清三件事频谱结果是怎么从逐点求和变成蝶形迭代的复数运算的舍入误差会累积到什么量级以及当你要给vivado fft这类 FPGA IP 核做浮点参考模型时手里有没有一份能看懂也能改的基-2 脚本。这篇内容按“数学结构 → MATLAB 迭代实现 → 与内置 fft 的 benchmark → 工程验证”推进主程序代码量不大但位反转、旋转因子和循环边界都需要逐条确认每一处参数我都会单独说明。适合已经会用fft接口但想深入底层或要对齐硬件实现的工程师。纯调包选手可以留个印象再走。2. 从 DFT 到基-2 FFT蝶形运算与旋转因子的结构2.1 直接 DFT 的计算代价与分解前提离散傅里叶变换DFT的定义是$X[k]\sum_{n0}^{N-1} x[n] \cdot W_N^{kn},\quad W_N e^{-j2\pi/N}$。如果直接按这个公式计算每算一个X[k]需要做 N 次复数乘法N 个频点总共是 N² 次。N 取 65536 时这个量级是 4.3e9 次复数乘加放在 MATLAB 的循环里基本就是几分钟到几十分钟的量级完全没法当工具用。FFT 之所以能把这个量级压到N·log2N靠的是一前一后两个动作先把旋转因子按“奇偶分组”拆开再利用它的对称性合并同类项。拆分的起点是观察 W_N 的两条性质$W_N^{kN/2} -W_N^k$以及 $W_N^{2kn} W_{N/2}^{kn}$。这两条式子意味着偶下标点和奇下标点可以独立做 N/2 点 DFT最后再用一个“带符号翻转”的加减法合起来。这个合并过程画在信号流图上形状像蝴蝶翅膀所以叫蝶形运算。基-2 FFT 要求每级长度正好减半一路拆到 1 点所以输入长度必须满足 N2^m。长度不对时常见做法是先补零到最近的 2 的幂再进 FFT代价是频率分辨率会变细但幅度谱包络不变。dso138 示波器的 FFT 固件里用的也是同样的补零策略采集长度不够 2 的幂时先把尾部填零。2.2 旋转因子的周期性与对称性蝶形运算里最消耗时间的是旋转因子计算。假设某一级蝶形的跨度为len半跨为half len/2这一级只需要知道 $W_{len}^{0}$ 到 $W_{len}^{half-1}$ 共 half 个值。由于下半个区间的旋转因子正好是上半个区间的相反数自始至终只需要存 N/2 个复数值内存和计算都能减半。用 MATLAB 预计算旋转因子表就是这个常见写法N 16; % FFT 点数必须是 2 的幂 k 0:N/2-1; % 只需要前 N/2 个角度 W exp(-2j * pi * k / N); % 复数旋转因子表N16 时长度 82j是 MATLAB 的虚数单位写法exp(-2j*pi*k/N)一次性生成长度为 N/2 的复数向量。后续迭代过程中第 m 级需要旋转因子时直接在这个表里隔点取不用每次重新调cos和sin。若写成2i也合法但项目里建议统一用2j避免和循环变量i混淆。旋转因子的默认精度是双精度浮点单次计算误差在 1e-16 量级但蝶形级数多了以后误差会逐步累积这是后面对比手写实现与内置fft误差时要重点盯的地方。2.3 位反转排序的索引规律蝶形运算先按奇偶拆分、再逐级合并这会导致一个副作用输入顺序被打乱了。以 N8 为例第一级把 [0..7] 拆成偶数组 [0,2,4,6] 和奇数组 [1,3,5,7]第二级再把每个四分之一继续拆最终x[1]会被排到索引 4 的位置。这个重排规律叫位反转排序即输入索引的二进制位序颠倒后就是实际处理位置。原始索引二进制位反转实际位置0000000010011004201001023011110641000011510110156110011371111117这张表就是位反转排序的全部秘密。迭代实现只需要在开头做一次重排后面各级蝶形不管数据顺序只按块跨度计算。递归实现不需要显式做位反转因为分治过程已经把顺序折叠进调用栈里了但迭代实现必须做这一道否则输出频率顺序完全错乱。下一节代码里的bit_reverse子函数就是按这个表的关系用原地交换实现的。3. MATLAB 实现基-2 FFT迭代代码与参数说明3.1 递归写法先验证蝶形逻辑递归版代码短先实现它来验证原理最直接。蝶形合并的核心是“E W.*O”和“E - W.*O”上臂加、下臂减这就是 2.2 节里对称性的实际落地。function X fft_recursive(x) % 递归基-2 FFT输入长度必须为 2 的幂 N length(x); if N 1 X x; % 单点 DFT 就是它自己 return; end x_even x(1:2:end); % 偶下标子序列 x_odd x(2:2:end); % 奇下标子序列 E fft_recursive(x_even); % 递归求偶部 DFT O fft_recursive(x_odd); % 递归求奇部 DFT W exp(-2j * pi * (0:N/2-1) / N); X [E W .* O, E - W .* O]; % 上下两半拼成 N 点 end每一处参数的含义都需要和数学定义对上1:2:end是 MATLAB 的步进索引偶奇拆分靠它完成注意索引从 1 开始所以取出来的是第 1、3、5… 个元素对应数学上的偶下标W使用当前级长度 N 生成长度为 N/2. *是逐元素复数乘法不能用*否则变成矩阵乘法拼接输出时前半E W.*O对应频点 0..N/2-1后半E - W.*O对应频点 N/2..N-1顺序颠倒会让两个半区的谱线互换。递归版逻辑清楚但 N65536 时会递归约 16 层每层都产生多个中间数组内存峰值是迭代版的数倍以上长时间跑还会触发 MATLAB 的递归深度限制。所以递归版只用来理解结构和验证蝶形拼接正式计算用迭代版。3.2 迭代版位反转加两级循环蝶形迭代版把递归的调用栈换成两个循环外层while负责逐级扩大蝶形跨度内层for负责按块处理同跨度的所有蝶形。位反转放在最前面预先做好整个程序只有一个m文件function X fft_radix2(x) % 基-2 迭代 FFT输入长度必须为 2 的幂 N length(x); if log2(N) ~ floor(log2(N)) error(输入长度必须是 2 的幂当前长度 %d, N); end x x(:); % 统一转为列向量避免维度陷阱 x bit_reverse(x); % 先位反转重排 len 2; % len 是当前蝶形的跨度 while len N half len / 2; % 半跨决定旋转因子数量 % 当前级旋转因子直接按 half 个点生成 W exp(-2j * pi * (0:half-1) / len); for block 1:len:N top x(block:blockhalf-1); % 上臂数据 bot x(blockhalf:blocklen-1); % 下臂数据 t W .* bot; % 下臂乘旋转因子 x(block:blockhalf-1) top t; x(blockhalf:blocklen-1) top - t; end len len * 2; % 跨度翻倍进入下一级 end X x; end function y bit_reverse(x) % 位反转重排原地交换N 必须是 2 的幂 N length(x); y x; j 0; % j 是下一个要交换的索引 for i 1:N-1 if i j 1 % MATLAB 索引起始为 1 tmp y(i); y(i) y(j1); y(j1) tmp; end bit N/2; while j bit % 模拟二进制的进位反转 j j - bit; bit bit / 2; end j j bit; end end主要参数拆解log2(N) ~ floor(log2(N))是长度校验的常用写法整数判断不会误伤x(:)把行向量、列向量统一为列向量后面块切片的行数才不会乱len从 2 开始因为最小蝶形是 2 点W在每级重算多付一点exp成本但换来清晰度。如果在意性能可以预生成整张 N/2 表再按W(1: N/len : end)抽点使用这样能省掉多级重复计算。内层block 1:len:N的步长等于跨度保证块与块不重叠top t和top - t直接改写原数组内存上比递归版省得多。3.3 边界条件和维度陷阱必踩的几个坑先说清楚。长度不是 2 的幂时bit_reverse内部bit N/2无法一直整除到 1死循环或越界几乎必然发生所以入口校验不能省。输入是行向量时x(1:2:end)取出来仍然是行向量但x(:)统一后能避免top t出现维度不匹配。旋转因子表里 k 从 0 开始写1:N/2会让第一个角度偏移频谱整体相位出错。逆变换只需要对输入做conj调用正变换后再对结果conj并除以 N这是单程实现复用最简洁的写法。4. 手写 FFT 与内置 fft 的 benchmark复杂度、精度与内存4.1 基准脚本与测试环境要验证“复杂度压下来了”最直接的办法是同一台机器上对比暴力 DFT、手写fft_radix2和内置fft。下面脚本循环取三档长度分别计时% fft_bench.m N_list [1024, 8192, 65536]; x randn(N_list(end), 1) 1j * randn(N_list(end), 1); for n N_list xn x(1:n); % 暴力 DFT按定义逐点求和 tic; X1 zeros(n, 1); for k 0:n-1 X1(k1) sum(xn .* exp(-2j*pi*k*(0:n-1)/n)); end t_dft toc; % 手写基-2 迭代 tic; X2 fft_radix2(xn); t_radix toc; % 内置 fft tic; X3 fft(xn); t_builtin toc; fprintf(N%6d DFT%.4fs radix2%.4fs builtin%.6fs\n, ... n, t_dft, t_radix, t_builtin); end测试环境是常见的双核 CPU 笔记本MATLAB R2023b双精度复数输入每个长度重复 5 次取中值。tic/toc测量会受系统调度轻微干扰所以建议一次完整跑完后取中间值。暴力 DFT 循环里exp(-2j*pi*k*(0:n-1)/n)每次新建一个复指数向量内存一直在分配这部分开销也计入了计时不影响对比结论。4.2 复杂度曲线与实测对照在我这边的实测结果大致如下N暴力 DFT手写 radix2MATLAB fft10240.148 s0.0019 s0.000065 s81929.36 s0.021 s0.00072 s65536约 600 s0.19 s0.0058 s绝对时间随机器浮动但相对倍数关系非常稳定。DFT 那段从 N1024 到 N8192 扩大了 8 倍耗时膨胀约 63 倍正好对应 N² 增长曲线fft_radix2从 8192 到 65536 扩大 8 倍耗时增长约 9 倍对应 N·log2N 的增长趋势。内置fft比手写快一个数量级因为 FFTW 库用了 SIMD、多线程和缓存分块优化手写 MATLAB 循环达不到那个水平。这个差距本身说明手写实现的价值不在于替代内置函数而在于算法边界可见、能改、能移植比如为混基 FFT 或单精度定点仿真提供浮点参考。4.3 精度对比与单双精度边界精度对比用范数相对误差最直观但要分双精度和单精度看不同量级N 4096; x randn(N, 1) 1j * randn(N, 1); X fft(x); X_my fft_radix2(x); err_double norm(X - X_my) / norm(X); % 双精度下的误差 xs single(x); Xs fft(xs); X_my_s fft_radix2(double(xs)); % 手写按双精度参考算 err_single norm(Xs - single(double(X_my_s))) / norm(Xs);双精度典型误差在 1e-14 到 1e-13 之间单精度典型误差在 1e-6 到 1e-5 之间。这个差距主要来自single只保留约 7 位十进制有效数字。旋转因子预计算带来的误差不是主项因为双精度下exp的计算误差和被乘数据的离散误差相比可以忽略真正的误差大头在蝶形加减法里大数减小数的有效位丢失。所以如果你的信号带直流或强低频分量高频谱线附近的相对误差会比均方误差指标差一两个数量级排查时要看abs(X - X_my) ./ abs(X)的逐点分布不能只看一个范数。5. 用三种方法验证 FFT 程序并定位硬件对标细节5.1 冲激、正弦和随机序列三种验证法程序写完先做三个快速测试能覆盖大多数实现错误% 1) 单位冲激频谱全 1检验幅度和相位 N 256; x [1; zeros(N-1, 1)]; X fft_radix2(x); disp(max(abs(X - 1))); % 期望接近 1e-16 % 2) 单频正弦峰值出现在目标频率点 fs 1000; t (0:N-1) / fs; f0 50; x_sin sin(2*pi*f0*t); X fft_radix2(x_sin); [~, idx] max(abs(X(1:N/2))); f_est (idx - 1) * fs / N; % 期望接近 50 % 3) 随机序列对照内置 fft整体相对误差 x_rand randn(N, 1) 1j * randn(N, 1); err norm(fft_radix2(x_rand) - fft(x_rand)) / norm(fft(x_rand)); disp(err);第一个测试对位反转错误和旋转因子符号错误非常敏感任何一位错都会让幅度偏离 1。第二个测试能验证频率轴标定idx-1是因为 MATLAB 索引从 1 开始而频点从 0 开始正弦不加窗且有整周期截断时f_est会精确等于 50。第三个测试用随机复数序列兜底它同时覆盖了复数点和各频段err小于 1e-12 基本可以确认实现无误。5.2 从浮点 MATLAB 到定点 FPGA 的对标要点硬件移植时vivado fft的 IP 核按定点格式配置数据宽度和小数位都有限制不能直接把 MATLAB 的浮点系数搬过去。常见做法是先在 MATLAB 里把旋转因子截断成 Q 格式定点数再与浮点结果逐点比对。FPGA 工程里偶尔会遇到“FFT IP 核无法设置小数时钟输入”这类配置问题本质是采样率参数只存在于仿真激励层IP 本身只认时钟沿和有效信号和 FFT 算法无关用 MATLAB 做浮点参考时确保有效采样点一一对齐就行。如果下板数据是 16 进制补码先用typecast或有符号数转换脚本读成十进制再喂给这里的浮点模型否则符号位会被当成数值参与蝶形运算比对结果毫无意义。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻