做阵列方向图计算时,如果还在用双重循环逐点扫角度,当阵元数涨到几百、扫描角度网格细到上千点时,一次方向图计算可能要等十几秒甚至更久。我最初调试相控阵仿真时也这么干过,直到把阵列因子求和改写成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次复数乘加。
当N=32、M=1801(以0.1°为步进)时,大约5.7万次复数运算,这还勉强能接受。但当阵元数到256,M到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(θ))
其中k0=2π/λ是自由空间波数。如果我们用s=sin(θ)作为自变量来定义方向图(即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的核,我们要对s=sinθ取等差序列,而不是直接对θ取等差序列。
假设s_k = -1 + 2k/M,k=0,1,...,M-1,即s从-1到1均匀变化。代入AF(s):
AF(s_k) = Σ w·e^(j·k0·d·n·(-1+2k/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)
如果取M=N(即方向图采样点数等于阵元数),且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的输出顺序是k=0到N-1,其中0对应s=-1(θ=-90°),N/2对应s=0(θ=0°),N-1接近s=1(θ=+90°)。这个顺序和我们通常画方向图时角度从-90到90的扫描方向在起点上是一致的,但标准的FFT函数输出顺序默认从“直流分量”开始,而“直流分量”在方向图里对应的是空间频率为0的方向,也就是θ=0°的方向——并不是角度轴的左端点。
打个比方:传统逐点法好比在一条路上从西往东挨家挨户敲门,FFT法则是先跳进路中间,再从中间往两头分别敲门,最后你把两段拼接起来。如果不做fftshift对频率轴重新排列,拿到的方向图就是“中间展开、两头拼接”的错乱状态,主瓣会被劈成两半,分别出现在横轴的两端。因此对FFT结果做fftshift操作是绕不开的一步。
s轴的具体构造方法:假设N点FFT,s轴为linspace(-1,1,N),fftshift之后的结果对应的s轴仍然是linspace(-1,1,N),但顺序变成了先负后正的自然排列。角度轴为asind(s_axis)。这一步非常容易出错,我在第一次实现时忘了做fftshift,看到的方向图左右两个峰值怎么也对不上参考曲线,排查了半小时最后才发现是轴的顺序问题。
3. 复杂度对比与代码级实践
3.1 复数乘加次数的真实差距
传统逐点法需要N×M次复数乘加。FFT法取M=N时,一次FFT的复杂度为(N/2)·log2(N)次蝶形运算,每个蝶形包含1次复数乘法和2次复数加法。为了直观,我做了一个对比表:
| 阵元数N | 传统法(取M=10N) | FFT法(N点FFT) | 加速比 |
|---|---|---|---|
| 16 | 2560 | 32 | 80倍 |
| 64 | 40960 | 192 | 213倍 |
| 256 | 655360 | 1024 | 640倍 |
| 1024 | 1.05e7 | 5120 | 2048倍 |
可以看到,当N=1024时,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 % 方法2:IFFT快速计算 x = w .* exp(-1j * pi * n); % d=0.5时,k0*d=pi 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))), axis=0) # 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', markersize=4, label='FFT/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的等差数列。但也不是完全没有办法:可以对方位角/俯仰角做插值,或者采用非均匀FFT(NUFFT)的思路,把非均匀阵元位置映射到均匀网格上,再用FFT加速。不过NUFFT的工程实现复杂度较高,而且需要额外处理插值误差,属于“杀鸡用牛刀”的场景。
更务实的做法是混合使用:把阵元位置就近归并到均匀网格上,网格间距取最大公约数或稍小于最小阵元间距。归并会产生位置误差,但可以通过最小二乘补偿加权系数来缓解。我在处理一个稀疏阵方向图综合问题时,就用了这种归并+FFT的思路,把迭代速度提升了30倍以上,代价是方向图精度损失不到0.1dB,完全在可接受范围。
4.2 采样条件与栅瓣问题
使用FFT计算方向图时,方向图在s域的采样点数为N(当M=N时),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轴做FFT,s∈[-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序列补零到M(M>N)点做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-DSP | 16/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的表达式,以免中途符号混淆。
一个实用技巧是:用N=4、d=0.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计算方向图的完整历程。如果你正在做雷达、通信、声呐相关的阵列信号处理,希望这篇笔记能帮你省下一些猜公式、试错的时间。