
做阵列方向图计算时如果还在用双重循环逐点扫角度当阵元数涨到几百、扫描角度网格细到上千点时一次方向图计算可能要等十几秒甚至更久。我最初调试相控阵仿真时也这么干过直到把阵列因子求和改写成FFT形式才真正感受到“FFT加速阵列方向图快速计算”这个思路到底有多值钱。这篇文章我就把整个公式推导过程、代码实现和踩过的坑完整梳理一遍。1. 为什么阵列方向图计算会卡在性能和精度上1.1 传统逐点扫描的真实瓶颈阵列方向图本质上是“阵列因子Array Factor, AF”与“单元方向图”的乘积。计算时最常见的方法是对于N个阵元的均匀线性阵列ULA在给定频率下阵列因子可以写成AF(θ) Σ w·e^(j·(2π/λ)·d·n·sin(θ))其中w是第n个阵元的复加权系数d是阵元间距λ是波长θ是观察方向与法线方向的夹角。如果要绘制方向图需要在θ从-90°到90°之间取M个采样点这时对于每个角度都要对N项求和总计算量是N×M次复数乘加。当N32、M1801以0.1°为步进时大约5.7万次复数运算这还勉强能接受。但当阵元数到256M到3601时接近92万次复数乘加。在MATLAB/Python里虽然向量化能提速但底层循环或矩阵构造的开销依然显著尤其当你在做阵列综合、自适应波束形成等需要反复计算方向图的迭代优化时这个瓶颈会被放大数百倍。我第一次做64阵元的分辨率分析时为了把方向图画得平滑一些把角度步进改成0.01°直接等了快一分钟那一刻就意识到必须换思路。1.2 阵列因子的数学形式和FFT的天然契合点FFT为什么能跟阵列方向图扯上关系关键在于阵列因子的表达式恰好是一个离散指数和的形式。回顾一下DFT的定义X(k) Σ x(n)·e^(-j·2π·k·n/N)对比AF(θ)的表达式只要令(2π/λ)·d·sin(θ) -2π·k/N即sin(θ) -k·λ/(N·d)那么方向图在特定角度上的值就刚好等于加权序列w的DFT在对应频点k上的值。也就是说一次FFT就能同时算出N个角度方向上的阵列因子值。这个发现不是我灵机一动想出来的而是在一次项目评审中被一位老工程师点醒的。他看了一眼我的仿真代码就说“你这就是在算DFT只不过信号是阵元加权域是空间频率。”当时我还愣了半天回家翻书把阵列因子公式和DFT定义并排写在纸上才彻底看明白这个等价关系。2. 从离散求和到FFT的完整推导2.1 均匀线性阵列的阵列因子展开通常阵列因子公式如下AF(θ) Σ w·e^(j·k0·d·n·sin(θ))其中k02π/λ是自由空间波数。如果我们用ssin(θ)作为自变量来定义方向图即U空间或正弦空间那么方向图在s域的表达式就是一个标准的多项式求值问题AF(s) Σ w·e^(j·k0·d·n·s)s∈[-1,1]在s域中方向图是以e^(j·k0·d·s)为基的离散多项式。如果把s均匀离散化成M个点问题就变成在单位圆上等间隔取值的一系列指数和。而FFT恰恰是计算这种指数和的最高效工具。这里特别重要的一点是到目前为止变量间的映射关系只是初等代数的替换还没有引入任何近似。FFT法和传统逐点法在数学上是严格等价的区别只在于计算路径不同。2.2 频域变换把角度采样变成频域采样下面我们从DFT的表达式出发推导出FFT计算方向图的具体映射关系。设需要对方向图在s域进行M点等间隔采样。为了让指数项匹配DFT的核我们要对ssinθ取等差序列而不是直接对θ取等差序列。假设s_k -1 2k/Mk0,1,...,M-1即s从-1到1均匀变化。代入AF(s)AF(s_k) Σ w·e^(j·k0·d·n·(-12k/M)) Σ w·e^(-j·k0·d·n)·e^(j·(k0·d·2/M)·n·k)令c k0·d·2/M则有AF(s_k) Σ (w·e^(-j·k0·d·n))·e^(j·c·n·k)如果取MN即方向图采样点数等于阵元数且dλ/2时k0·d (2π/λ)·(λ/2) π所以c 2π/N。这时AF(s_k) Σ (w·e^(-j·π·n))·e^(j·2π·n·k/N)这个式子和DFT定义X(k) Σ x(n)·e^(-j·2π·n·k/N)对比指数符号正好相反是共轭关系。利用IFFT的数学定义IFFT(x)[k] (1/N)·Σ x(n)·e^(j·2π·n·k/N)可以得到一个非常简洁的结论AF(s_k) N·IFFT(x)[k]其中x(n) w·e^(-j·π·n)。这一个式子就是整个FFT快速计算的核心。当然如果坚持用FFT函数而不是IFFT只需要对x取共轭再做FFT最后再取共轭即可AF(s_k) conj(FFT(conj(x)))[k]两种写法在数学上是等价的工程上选哪种都行只要记得最后做归一化和坐标映射就好。我习惯用N·IFFT的形式因为少写两次conj不容易在符号上绕晕。2.3 关键DFT索引与空间角度的映射关系在得到FFT结果后要把离散频点索引k映射回空间角度θ。根据前面的假设s_k -1 2k/N所以θ_k arcsin(s_k) arcsin(-1 2k/N)其中k 0,1,...,N-1。注意FFT的输出顺序是k0到N-1其中0对应s-1θ-90°N/2对应s0θ0°N-1接近s1θ90°。这个顺序和我们通常画方向图时角度从-90到90的扫描方向在起点上是一致的但标准的FFT函数输出顺序默认从“直流分量”开始而“直流分量”在方向图里对应的是空间频率为0的方向也就是θ0°的方向——并不是角度轴的左端点。打个比方传统逐点法好比在一条路上从西往东挨家挨户敲门FFT法则是先跳进路中间再从中间往两头分别敲门最后你把两段拼接起来。如果不做fftshift对频率轴重新排列拿到的方向图就是“中间展开、两头拼接”的错乱状态主瓣会被劈成两半分别出现在横轴的两端。因此对FFT结果做fftshift操作是绕不开的一步。s轴的具体构造方法假设N点FFTs轴为linspace(-1,1,N)fftshift之后的结果对应的s轴仍然是linspace(-1,1,N)但顺序变成了先负后正的自然排列。角度轴为asind(s_axis)。这一步非常容易出错我在第一次实现时忘了做fftshift看到的方向图左右两个峰值怎么也对不上参考曲线排查了半小时最后才发现是轴的顺序问题。3. 复杂度对比与代码级实践3.1 复数乘加次数的真实差距传统逐点法需要N×M次复数乘加。FFT法取MN时一次FFT的复杂度为(N/2)·log2(N)次蝶形运算每个蝶形包含1次复数乘法和2次复数加法。为了直观我做了一个对比表阵元数N传统法(取M10N)FFT法(N点FFT)加速比1625603280倍6440960192213倍2566553601024640倍10241.05e751202048倍可以看到当N1024时FFT快了约2000倍。更重要的是FFT算法在嵌入式平台比如STM32F4的CMSIS-DSP库里也有现成的优化实现所以这个方法的工程价值远不止省几秒钟仿真时间。在实时波束指向演示系统里传统算法根本没法做到“方向图随扫描角动态刷新”而FFT方案可以做到毫秒级更新。3.2 一个可直接运行的MATLAB验证示例下面给出一个完整的MATLAB示例用于验证推导的正确性% 参数定义 N 64; % 阵元数 d 0.5; % 阵元间距单位波长 theta0 10; % 波束指向角单位度 % 生成加权相位加权实现波束指向 n (0:N-1).; w exp(1j * 2*pi * d * n * sind(theta0)); % 方法1传统逐点扫描参考 theta_ref linspace(-90, 90, 1801); AF_ref zeros(size(theta_ref)); for idx 1:length(theta_ref) AF_ref(idx) sum(w .* exp(1j * 2*pi * d * n * sind(theta_ref(idx)))); end % 方法2IFFT快速计算 x w .* exp(-1j * pi * n); % d0.5时k0*dpi AF_fft N * ifft(x); AF_fft_shift fftshift(AF_fft); % 构造角度轴 s_axis linspace(-1, 1, N); theta_fft asind(s_axis); % 绘图对比 figure; plot(theta_ref, 20*log10(abs(AF_ref)/max(abs(AF_ref))), b-); hold on; plot(theta_fft, 20*log10(abs(AF_fft_shift)/max(abs(AF_fft_shift))), ro); grid on; legend(传统逐点法,FFT/IFFT法); xlabel(角度(deg)); ylabel(归一化幅度(dB)); title([N num2str(N) , d num2str(d) λ, 波束指向 num2str(theta0) °]);这段代码直接跑出来两个曲线基本重合。我最初实现时发现一个细节如果不做fftshift方向图会在左右两端被切成两半图形看起来就像“破”了。另外这里我把x定义为w·e^(-j·π·n)这个预乘因子的作用是对s域的采样起点做调整让s轴从-1开始而不是从0开始。很多人推导FFT方向图时会把这一步漏掉拿到的角度轴整体平移了90°看起来主瓣方向全错了。3.3 Python版本的快速实现如果你用Python做算法验证可以用NumPy很轻松地复现同样的逻辑import numpy as np import matplotlib.pyplot as plt N 64 d 0.5 theta0 10 n np.arange(N).reshape(-1, 1) w np.exp(1j * 2 * np.pi * d * n * np.sin(np.deg2rad(theta0))) # 传统法 theta_ref np.linspace(-90, 90, 1801).reshape(1, -1) AF_ref np.sum(w * np.exp(1j * 2 * np.pi * d * n * np.sin(np.deg2rad(theta_ref))), axis0) # FFT法 x w[:, 0] * np.exp(-1j * np.pi * n[:, 0]) AF_fft N * np.fft.ifft(x) AF_fft_shift np.fft.fftshift(AF_fft) s_axis np.linspace(-1, 1, N) theta_fft np.rad2deg(np.arcsin(s_axis)) # 归一化绘图 AF_ref_db 20 * np.log10(np.abs(AF_ref) / np.max(np.abs(AF_ref))) AF_fft_db 20 * np.log10(np.abs(AF_fft_shift) / np.max(np.abs(AF_fft_shift))) plt.figure(figsize(10, 6)) plt.plot(theta_ref.flatten(), AF_ref_db, b-, label传统逐点法) plt.plot(theta_fft, AF_fft_db, ro, markersize4, labelFFT/IFFT法) plt.grid(True) plt.legend() plt.xlabel(角度(deg)) plt.ylabel(归一化幅度(dB)) plt.show()这个Python版本是我平时做阵列信号处理快速验证时最常用的模板。要注意的是NumPy的ifft默认不做归一化所以需要自己乘N来和阵列因子表达式保持一致。如果你在C语言环境里实现可以调FFTW库的fftw_plan_dft_1d注意它对多维数组有in-place和out-of-place两种模式使用前先分配好内存避免反复创建plan造成性能损耗。4. 非均匀阵列的推广和方法的边界条件4.1 非均匀阵列怎么处理很多天线阵并不是等间距的比如稀疏阵、稀布阵、圆环阵。对于非均匀阵列直接套用上面的推导是不行的因为阵列因子中的相位项不再是n·d的等差数列。但也不是完全没有办法可以对方位角/俯仰角做插值或者采用非均匀FFTNUFFT的思路把非均匀阵元位置映射到均匀网格上再用FFT加速。不过NUFFT的工程实现复杂度较高而且需要额外处理插值误差属于“杀鸡用牛刀”的场景。更务实的做法是混合使用把阵元位置就近归并到均匀网格上网格间距取最大公约数或稍小于最小阵元间距。归并会产生位置误差但可以通过最小二乘补偿加权系数来缓解。我在处理一个稀疏阵方向图综合问题时就用了这种归并FFT的思路把迭代速度提升了30倍以上代价是方向图精度损失不到0.1dB完全在可接受范围。4.2 采样条件与栅瓣问题使用FFT计算方向图时方向图在s域的采样点数为N当MN时s域采样间隔Δs 2/N。方向图在s域的理论周期为λ/d以s为自变量时。当我们取dλ/2时方向图在s∈[-1,1]区间内恰好覆盖一个周期不会产生栅瓣。但若dλ/2方向图在s域会出现混叠FFT结果会叠加多个周期的贡献导致方向图出现栅瓣。这个特性恰好和天线理论中的栅瓣条件一致——当dλ/2时s域出现多个可视区副本扫描方向图会在某些角度出现多余的大瓣。所以我在做FFT方向图计算时会先检查d/λ是否超过0.5一旦超过就直接提醒自己要么加密阵元要么接受栅瓣要么只在特定扫描范围内使用FFT结果。另一个容易被忽略的点是如果阵元间距远小于λ/2方向图在s域只是中间一小段有值两端全是零。这时候如果仍然用等间隔的s轴做FFTs∈[-1,1]的很多采样点都落在无信号区看起来“浪费”了不少计算。但这其实不是问题——FFT的复杂度只和N有关和M无关该做的运算量一分不会多。4.3 二维阵列方向图的扩展二维阵列如平面阵的方向图同样可以用二维FFT计算。核心思想是将二维阵列因子展开为两个维度可分离的指数和前提是矩形栅格然后对加权矩阵做二维FFT。我在做平面相控阵方向图时直接用MATLAB的fft2对64×64的矩形阵做方向图计算传统方法需要遍历64×64×3601个点的复指数而fft2只需要对64×64的矩阵做二维FFT速度提升非常直观。这里有个细节二维FFT之后需要同时对两个维度都做fftshift即fftshift(fftshift(AF_2d, 1), 2)然后角度轴是两组arcsin映射的组合。如果阵列是矩形的x方向和y方向的阵元间距可能不同那么两个维度的s轴映射公式也要分别写不能套同一个linspace(-1,1,N)。对于圆形阵列或三角栅格处理就要麻烦得多。圆形阵列的阵元坐标是(r·cosφ, r·sinφ)阵列因子展开后包含贝塞尔函数的叠加不能直接套FFT。这种情况下我一般退回传统逐点法或者把圆形阵投影到多个线性子阵上分别用FFT计算再合成具体看精度需求。5. 实操中的关键注意事项5.1 FFT点数与角度分辨率的矛盾很多人不理解为什么FFT法得到的方向图角度分辨率是固定的。因为角度分辨率取决于N阵元数和d阵元间距。如果想获得更细的角度采样比如0.1°步进直接用N点的FFT是不够的因为N点FFT只给出N个方向图采样点。可以采用补零zero-padding技术把x序列补零到MMN点做FFT这样能在s域得到更细的采样间隔。但补零带来的“更细”只是插值效果不会增加物理上的真实分辨率。用天线术语说真正的波束宽度由阵列孔径决定补零只是让方向图曲线看起来更平滑。在写论文或做工程报告时要特别注意别把插值当成真实的超分辨。我在一次相控阵校准项目中吃过这个亏补零后方向图主瓣显得非常“尖”领导看了以为分辨率提升了实际物理波束宽度一点没变后来不得不在报告里加了段说明解释补零和真实分辨率的区别。5.2 幅度归一化和绘图习惯FFT输出的幅度谱系数与方向图幅度的比例关系是AF_fft是N倍的IFFT结果所以方向图幅值有一个固定的N倍缩放。绘图时我通常直接归一化不影响方向图形状。如果你要拿FFT结果去做阵列综合比如和自适应算法的期望方向图比较就要注意幅度标定问题。具体做法是先算一次均匀加权w全为1的FFT方向图峰值应该出现在0°方向幅度值为N因为N个同相单位复指数相加。拿这个值做标定系数之后再算其他加权的方向图就能得到绝对幅度。这个标定值在传统法里是自动满足的但FFT法因为IFFT自带1/N因子容易差一个N倍我最初就是没注意画出来的方向图整体比参考值低了20·log10(N)dB。5.3 嵌入式移植的选型建议如果你要在STM32之类的嵌入式平台上做FFT方向图计算建议使用ARM CMSIS-DSP库的实数/复数FFT函数它支持4到4096点的基4 FFT速度快且占用Flash少。需要注意CMSIS-DSP的FFT输出格式是自然顺序需要自行处理位反转但库函数内部已经做了处理只需调用arm_cfft_f32接口即可。不同FFT库的对比如下平台/库支持点数精度备注MATLAB fft/ifft任意双精度最方便适合验证算法NumPy fft/ifft任意双精度Python生态适合快速原型FFTW任意单/双精度C语言首选性能高CMSIS-DSP16/64/256/1024等单精度嵌入式首选已针对ARM优化STM32 HAL库软件FFT有限单精度速度较慢不推荐我在STM32F4上移植过一次64点复数FFT用于圆形阵列方向图实时显示运行频率168MHz一次方向图计算含坐标变换和幅度归一化耗时约几毫秒完全满足实时波束指向演示的需求。如果阵元数需要超过1024建议分段处理或直接使用蝶形运算优化后的定点FFT库浮点FFT在大点数时对MCU的资源消耗比较明显。5.4 对称性与共轭利用如果加权w是实对称的比如均匀加权、切比雪夫加权方向图关于θ0近似对称可以只算一半的FFT点数然后镜像对称得到另外一半利用这个特性可以再省一半运算。但要注意当波束指向不为0时w是复指数这个对称性会被打破所以不能无脑套用。另外即使加权是实数FFT结果也存在共轭对称性利用这个性质可以从N点FFT的输出里恢复出2N点方向图数据这是一个更高级的技巧适合对性能极度敏感的场景。6. 推导过程中最容易错的三个地方6.1 正负号约定搞混DFT的定义在不同的书里有不同约定有的用e^(-j·...)有的用e^(j·...)这会导致最后映射关系差一个共轭或索引反转。我在推导时习惯先把DFT公式明确写在纸上再代入AF的表达式以免中途符号混淆。一个实用技巧是用N4、d0.5、w为全1的特殊情况做手算验证。全1加权下方向图在θ0处应该有最大值对应FFT结果中fftshift后的中心位置。如果算出来峰值不在中心大概率是共轭或fftshift处理错了。这个“特殊值验证法”看起来简单但能帮你在一分钟内定位符号问题比抱着公式反复推效率高得多。6.2 角度域采样与s域采样的混淆这是我在一个实际项目里踩过的坑。当时做波束扫描时需要方向图数据我直接对角度θ等间隔采样然后试图套FFT公式结果发现无论怎么调整映射关系峰值位置总对不上。后来才意识到FFT天然是在等间隔频点/空间频率上采样对应到方向图就是sinθ等间隔而不是θ等间隔。如果项目需求要求固定角度步进比如0.5°用FFT方法就不合适要么接受s域等间隔带来的角度非均匀步进要么做插值。我当时的做法是先用FFT快速算出一个粗方向图然后只在主瓣附近做局部加密扫描这样既保证了关键区域的角度步进又避免了全角度范围逐点扫描的低效。6.3 补零后的映射关系补零不是简单的“把数组变长”就完事了。补零后FFT点数M和实际加权序列长度N的关系会影响s轴的映射。当dλ/2时补零到M点后的s_k -1 2k/M。此时方向图的s域采样间隔变为Δs 2/M角度分辨率看起来更细了但实际上物理分辨率依然受限于N·d/λ。这里有一个容易误用的场景如果你想用FFT方向图做精密测角补零后的插值确实能在一定程度上提升角度估计的“读数精度”类似曲线拟合的效果但不能突破阵列瑞利限。在报告里我会明确标注“补零后的方向图是sinc插值结果不代表实际角分辨率的提升”——这句话虽然啰嗦但能避免很多人拿着补零结果去挑战瑞利限。7. 几个延伸和应用思考7.1 与快速阵列综合的结合FFT方向图计算最有价值的场景不是单次计算而是配合阵列综合算法做迭代。我拿了某个遗传算法做阵列加权优化每一代需要计算几百个方向图如果使用传统逐点法一次优化要跑几小时改成FFT方向图计算后每代方向图计算耗时从秒级降到毫秒级整个优化流程缩短到一个数量级以上。具体做法是把方向图计算封装成一个函数输入是加权向量w输出是N点复数方向图。优化算法只负责更新w方向图计算全部走FFT路径。这里的性能提升来自两点一是FFT本身的复杂度优势二是避免了传统方法中“构造大矩阵再求和”的内存开销。7.2 波束扫描的批处理如果需要对多个波束指向角做扫描方向图计算只需要修改w的相位项然后对每个w做一次FFT。由于不同角度之间没有依赖关系非常适合并行化或矩阵化处理。MATLAB里可以预先构造一个(N_beam × N)的加权矩阵每个通指向角一行然后对矩阵的每一行做FFT用MATLAB的fft函数直接对矩阵按维度操作即可。我在做相控阵波束扫描动画时用这种方法一次性计算了180个波束指向角的方向图耗时不到0.1秒而传统方法需要遍历180×64×3601个复指数即使向量化也要好几秒。7.3 和“FFT频谱分析系统”的类比做嵌入式FFT频谱分析系统的人可能不知道自己在单片机里算的那套FFT跟阵列方向图计算在数学上是同一个内核。区别只是频谱分析把时域信号变换到频域阵列方向图把空间采样阵元幅度/相位变换到空间频域角度域。理解了这种统一性你就能在两个领域间自由切换。我在做水下声呐波束形成时直接用嵌入式FFT库算方向图代码几乎不用改——本质都是复指数求和。7.4 FFT实现选型的几个要点场景建议方案理由算法验证/教学MATLAB或Python双精度高调试方便C语言桌面应用FFTW性能强文档完善ARM嵌入式CMSIS-DSP已适配ARM指令集快GPU加速CUDA cuFFT适合大矩阵批处理如果场景是FPGA实现可以用Vivado的FFT IP核。它的可配置性强支持多种FFT点数、流水线结构和蝶形结构。需要注意IP核的输入输出采用AXI-Stream接口数据格式是定点数需要自己做浮点到定点的Q格式转换这一步的精度损失要在系统设计阶段就评估好。我在实际项目中的体会是FFT加速阵列方向图这个思路看起来像一个小技巧但它把计算复杂度从O(NM)降到O(NlogN)并且在嵌入式、仿真、优化算法等多个场合都能直接受益。掌握推导过程比单纯会用fft函数重要得多——因为只有理解了映射关系你才知道什么时候能用FFT、什么时候不能用、用的时候该注意什么。这条推导和实现的经验基本覆盖了我从最初在MATLAB里用for循环算方向图到后来在工程中大规模使用FFT计算方向图的完整历程。如果你正在做雷达、通信、声呐相关的阵列信号处理希望这篇笔记能帮你省下一些猜公式、试错的时间。