1. 先把概念掰开:DFT 波束形成到底指哪一种做法
在阵列信号处理这个圈子里,DFT 波束形成这个说法其实被两拨人用成了两个不同的东西,混着用的时候特别容易出问题。我见过不少人拿着别人的代码跑,结果波束图死活对不上,最后发现是两个人在讲完全不同的实现。
第一拨人说的是空域 DFT 波束形成。均匀线阵(ULA)的导向矢量里,第 n 个阵元的相位是exp(-j2π·n·d·sinθ/λ),这个形式和 DFT 的旋转因子exp(-j2π·k·n/N)长得一模一样。于是把阵元维数据直接送进一个 DFT 矩阵(工程上就是fft),一次就能同时形成 N 个指向不同方向的波束。这就是所谓的"用 FFT 做多波束",本质是空域滤波器组或者说波束域变换,雷达、声呐、麦克风阵列的快速扫描都用这个套路。
第二拨人说的是频域 DFT 波束形成,指的是宽带信号处理时,先把每个阵元的时域数据分帧做 FFT,在每一个频率 bin 上按照该频率对应的波长单独算导向矢量加权,再 IFFT 和重叠相加回时域。这里的 DFT 是时间维的,作用是让宽带信号在频域"分而治之",从而规避窄带假设失效的问题。
这两件事名字像、代码里都出现fft,但一个是空间换角度,一个是时间换频率,做的东西完全不一样。本文两条线都走一遍,因为实际系统里它们经常是串联使用的:先做时间维 FFT 把宽带拆成窄带,再在窄带内做空间维 DFT 多波束。
Matlab 实现这件事上,这两条线的坑还各不相同。空域那条主要坑在索引到角度的映射和栅瓣,频域那条主要坑在分帧边界和逐频点导向矢量的相位对齐。下面我从参数计算开始,一路写到能直接跑起来、能对着波束图验证的实现。
2. 阵列模型与参数:先算数,再写代码
2.1 ULA 信号模型与空间频率
远场窄带平面波从方向 θ 入射到 N 元均匀线阵,阵元间距 d,以第 0 个阵元为参考,相邻阵元接收信号的时延差是
τ = d·sinθ / c对应载波相位差2π·d·sinθ/λ。写成导向矢量:
a(θ) = [1, e^{-j2π·d·sinθ/λ}, ..., e^{-j2π·(N-1)·d·sinθ/λ}]^T为了偷懒,工程上定义一个无量纲空间频率u = d·sinθ/λ,导向矢量变成a(u) = [1, e^{-j2πu}, ..., e^{-j2π(N-1)u}]^T。这一步变量替换是整个 DFT 波束形成的起点,因为它把"角度"这个非线性的东西,变成了一个和阵元下标成线性关系的相位斜率。
我记得第一次动手时,就是没做这个替换,直接拿 θ 去写循环,结果波束图在 0° 附近被拉得特别宽,端射方向挤成一团。原因很简单:sinθ和 θ 不是线性关系,等角度步进在 sinθ 域里就是不等间隔采样,画出来自然变形。做波束图一定要在 sinθ 或 u 域上画,再换算回角度看,这两条曲线形状完全不同。
2.2 关键参数的手算过程
我拿一套具体的数来算,后面代码全部围绕这套参数,你可以直接照着改。
| 参数 | 符号 | 取值 | 说明 |
|---|---|---|---|
| 阵元数 | N | 16 | 决定自由度与孔径 |
| 载频 | f0 | 3 GHz | 用于波长换算 |
| 光速 | c | 3e8 m/s | |
| 载波波长 | λ0 | 0.1 m | λ0 = c/f0 |
| 阵元间距 | d | 0.05 m | 取 λ0/2 |
| 阵列孔径 | L | 0.75 m | L = (N-1)·d |
| 等效带宽 | B | 300 MHz | 复基带采样率 400 MHz |
几个必须手算出来的数:
半功率波束宽度。均匀加权时约等于0.886·λ/(N·d)弧度。代入:0.886 × 0.1 / (16 × 0.05) = 0.1108 rad ≈ 6.35°。这个数是阵列的"角分辨率"量级,后面的所有角度分辨能力都不能比它更细,想做更细就必须加阵元或者提高频率(缩短波长)。
栅瓣条件。当d > λ/2时,除了主瓣方向,还会有别的方向满足相位差为 2π 的整数倍,形成和主瓣一样高的假峰。d = λ0/2 是临界值,考虑带宽 B 之后,最低频率对应的波长最长,最高频率对应波长λ_min = c/(f0 + B/2) = 3e8/3.15e9 = 0.0952 m,要求d ≤ λ_min/2 = 0.0476 m。所以我上面写的 d = 0.05 m 在带宽边缘其实略微超标,会在大约边缘频率处冒出很小的栅瓣。这个细节很少有人在示例里提,但真实宽带系统里必须按λ_min/2来定阵元间距,否则带内高频端方向图会翘起来。
孔径渡越时间。τ_max = (N-1)·d/c = 0.75/3e8 = 2.5 ns。判断窄带近似是否成立的经验判据是B·τ_max << 1。这里300e6 × 2.5e-9 = 0.75,已经不算是"远小于 1"了,属于必须做频域处理的区间。如果按窄带那样全带宽只用一个 λ0,带宽边缘会出问题,具体误差下面第 4 节会量化。
2.3 窄带假设的体检:什么时候不能直接空域 DFT
窄带假设的核心是"信号在整个孔径上的相位变化,可以用单一频率的相位差来近似"。数学上就是包络在阵元之间不发生形变。信号带宽越宽、孔径越长,包络在孔径上的变化越剧烈,这个近似就越站不住。
我把这条判据做成一个简单的心算规则:先算 B·τ_max,小于 0.1 就放心用窄带;0.1 到 1 之间要留神,做波束图时会看到主瓣变形;大于 1 就必须走频域分 bin 的路子。
举个具体的失败案例。假设只用一个导向矢量(按 f0 算)去处理 300 MHz 带宽的信号,最高频率处每个阵元的相位偏差是2π·d·sinθ·Δf/c,取 sinθ = 1、Δf = 150 MHz,得到每个阵元偏2π × 0.05 × 1.5e8 / 3e8 = 0.157 rad。16 个阵元一路累积下来最多差15 × 0.157 = 2.36 rad,超过 π,意味着阵列两端阵元的相位已经完全反相。这时候波束不仅没对准,增益还可能掉到负值。所以宽带系统里,"用 f0 算一组权重打天下"这种做法基本必挂。
3. 空域 DFT 多波束的 Matlab 实现
3.1 导向矢量与 DFT 矩阵的那层关系
把a(u)和 N 点 DFT 矩阵的第 k 行放一起看:
DFT 矩阵第 k 行第 n 列元素是e^{-j2πkn/N},导向矢量第 n 个元素是e^{-j2πnu}。两者对上,当u = k/N时完全相等。也就是说,第 k 个 DFT 输出对应的空间频率是 u = k/N,对应角度满足d·sinθ/λ = k/N,即sinθ_k = kλ/(Nd)。
取 d = λ/2 代进去:sinθ_k = 2k/N。k 从 0 到 N-1,sinθ 从 0 一直走到接近 2。因为 sinθ 的物理范围只有 [-1, 1],超过 1 的部分自动折返到负角度上,这就是 DFT 的周期性在空间域的体现。
用fftshift把零频挪到中间后,索引 k = -N/2 到 N/2-1 就整齐对应sinθ = 2k/N,从 -1 走到 1,覆盖全空域。d = λ/2 这个取值特别舒服,就是因为 N 点 DFT 的角度网格刚好铺满整个可见空间,不多不少。换成 d = λ/3,DFT 网格只覆盖 ±2/3 的可见空间,剩下的角度你根本扫不到,得靠补零或者额外算内积。
3.2 一段可直接跑的波束扫描代码
这段代码我改过好几版,最后留下的是最不容易出错的那种写法:导向矢量统一用列向量,波束图矩阵按"波束 × 角度"组织,画图前先归一化到 dB。
% ---- 参数 ---- N = 16; % 阵元数 d = 0.05; % 阵元间距 (m) c = 3e8; f0 = 3e9; lam0 = c/f0; % ---- 波束索引 -> 角度 (d = lam0/2 时) ---- k = (-N/2 : N/2-1).'; sin_beam = 2*k/N; % 空间频率网格 theta_beam = asind(sin_beam); % 对应角度 (deg) % ---- 空间 DFT 矩阵 (酉矩阵, 带 1/sqrt(N) 归一化) ---- n = (0:N-1).'; Fdft = exp(-1j*2*pi*n*k.'/N) / sqrt(N); % N x N Fdft = fftshift(Fdft, 2); % 行顺序与 sin_beam 对齐 % ---- 用一条单频平面波验证 ---- theta0 = 20; % 真实来向 x = exp(-1j*2*pi*d*n*sind(theta0)/lam0); % N x 1 快拍 y = Fdft * x; % N x 1 多波束输出 [~, idx] = max(abs(y)); fprintf('估计角度 = %.2f deg\n', theta_beam(idx));跑出来theta_beam(idx)应该是 20°,允许有网格量化误差,因为波束网格步进是2/N = 0.125的 sinθ,对应角度间隔在 20° 附近大约是 5.7°。这个量化误差是 DFT 波束形成的固有代价,不是代码写错了。想更细就得在阵元维补零,也就是在fft里把点数从 N 提到 4N 甚至 8N,相当于对波束图做插值,角度网格变密但分辨率不变(后文会说明这个"变密不等于变好"的区别)。
3.3 波束图、栅瓣与加窗的取舍
验证实现对不对,最靠谱的是画波束图,而不是只看峰值位置。波束图的计算是B(θ) = |w_k^H · a(θ)|,扫一遍 θ 网格。
sin_grid = linspace(-1, 1, 4001); a_grid = exp(-1j*2*pi*d*n*sin_grid/lam0); % N x L % 取第 k0 个波束的权重(Fdft 的一行) k0 = 8; % 约 0 deg w = Fdft(k0, :).'; B = abs(w' * a_grid); % 1 x L B = 20*log10(B / max(B)); plot(sin_grid, B); xlabel('sin\theta'); ylabel('dB'); grid on;三个必须看的地方:
主瓣宽度是否和手算的 6.35° 对得上。如果明显更宽,先检查你是不是把w的共轭弄反了,取错共轭会让整个方向图镜像翻转,看起来"能对准"但角度是反的。
旁瓣电平。均匀加权(也就是纯 DFT)第一旁瓣约 -13.2 dB,这个数很经典。想要更低就得加窗,比如改用 Hamming 或者 Taylor 加权。代价是主瓣展宽,Hamming 大概让主瓣宽 1.3 倍,旁瓣压到 -43 dB 左右。这是典型的用分辨率换动态范围,探测强目标旁边的小目标时,压旁瓣比抠那点分辨率更值钱。
栅瓣。把 d 改成 0.08 m 再跑一遍,你会看到 ±90° 附近冒出几个和主瓣差不多的尖峰。这就是空域混叠,没救,只能改 d。
注意:加窗的时候,窗函数是加在阵元维上的,也就是
w = w .* win,不是加在时间上。很多人会下意识套用一维信号的窗,对着时间序列做,那就完全跑偏了。
另外一个实用经验是,波束之间其实并不"干净"地互不干扰。虽然 N 个 DFT 波束在sinθ = 2k/N这些离散点上满足正交性(F 是酉矩阵,能量守恒,波束域变换不损失信息),但相邻波束在主瓣区域的重叠非常严重,一个目标通常会同时点亮 2 到 3 个相邻波束。做角度估计时用"峰值位置"能凑合,想要精细角度就得上插值或者别的超分辨方法,别指望单靠 DFT 网格能抠出亚度级精度。
4. 宽带频域 DFT 波束形成:STFT 域逐频点加权
4.1 分帧、FFT、逐频点加权、重叠相加
宽带处理的骨架就是四步,我用 300 MHz 带宽、400 MHz 复基带采样率来写:
- 每个阵元的复基带数据分帧,帧长 Nfft,帧间重叠 50%。
- 每帧做 FFT,得到
X_n[m],n 是阵元,m 是频率 bin。 - 对每个 bin 单独算这个频率下的导向矢量,加权求和。
- 结果 IFFT 回时域,重叠相加(WOLA)拼回连续输出。
第二步和第三步是关键。第 m 个 bin 对应的物理频率是f_m = f0 + f_bin[m],该频率下的波长λ_m = c/f_m,导向矢量必须用λ_m而不是λ0。
fs = 400e6; Nfft = 1024; hop = Nfft/2; n = (0:N-1).'; % 频率 bin 转成 [-fs/2, fs/2) fb = (0:Nfft-1).'/Nfft*fs; fb(fb >= fs/2) = fb(fb >= fs/2) - fs; f_phys = f0 + fb; % 实际射频频率 lam = c ./ f_phys; % 每个 bin 的波长 % 期望方向的阵元时延 (N x 1) tau0 = n * d * sind(theta0) / c; % 逐频点导向矢量: A 是 N x Nfft A = exp(-1j*2*pi*(f_phys.') .* tau0); % 这里 (1xNfft).*(Nx1) 自动扩展 % 波束形成权重 (匹配滤波形式, 1/N 归一化) W = conj(A) / N; % N x NfftW里每一列对应一个频率 bin 的 N 个阵元权重,结构非常清晰:同一条代码覆盖了所有子带,不需要 for 循环逐 bin 算。这样写不只快,更重要的是不容易出维度错。
帧处理部分:
win = sqrt(hann(Nfft, 'periodic')); % 分析+合成各用一次, 平方和补偿 frame_idx = 1:hop:(size(x,1)-Nfft+1); y_out = zeros(size(x,1)+Nfft, 1); win_sq_sum = zeros(size(y_out)); for ii = 1:numel(frame_idx) s = frame_idx(ii); Xf = fft(x(s:s+Nfft-1, :) .* win, Nfft, 1); % Nfft x N Yf = (conj(W).' * Xf.').'; % Nfft x 1 逐 bin 加权 yt = real(ifft(Yf)); % 复基带也可保留虚部 seg = (s:s+Nfft-1).'; y_out(seg) = y_out(seg) + yt .* win; win_sq_sum(seg)= win_sq_sum(seg)+ win.^2; end % 归一化, 消除窗的重叠增益 valid = win_sq_sum > 1e-8; y_out(valid) = y_out(valid) ./ win_sq_sum(valid);注意最后那步归一化。如果不做窗平方和补偿,帧重叠区域的幅度会比单帧区域高,合成信号会周期性起伏,听起来像"突突"的调幅,看起来像一段段方波。这是最常见的翻车点之一,我第一次做 WOLA 就被这个折腾了半天,一直以为是波束形成的相位算错了。
4.2 频域导向矢量的符号约定与对齐
这一节讲清楚两个容易乱的符号问题。
第一,相位符号。复基带接收信号模型是x_n(t) = s(t-τ_n)·e^{-j2πf0·τ_n}。傅里叶变换后得到X_n(f) = S(f)·e^{-j2π(f0+f)τ_n}。想让 N 个阵元相干相加,权重必须取conj(a_n) = e^{+j2π(f0+f)τ_n}。也就是权重是导向矢量的共轭,因为波束形成输出是w^H x,共轭转置里已经含了一个共轭。这一点在复数域代码里非常容易写混,尤其当你在不同函数之间传参的时候。我建议统一成"权重矩阵 W 直接对应w^H x",别在中间到处转置。
第二,帧对齐。所有阵元必须用同一时间窗切帧。如果为了省事,让某个通道独立做 STFT 并取不同起点,就会引入额外的帧间相位差。避免方法是把多通道数据组织成[采样点 × 通道]的矩阵,一次fft出来,物理上就不可能错位。
第三,块卷积的群延迟。如果你不走 STFT 加权、而是把每个频点的权重 IFFT 成长度 Nfft 的 FIR 滤波器、再去做分块卷积,那么每路滤波器会引入Nfft/2的群延迟,必须统一补偿,否则不同通道的时延就被你人为改掉了,波束直接指向错误方向。这是 overlap-save 实现里最阴的一个坑,因为你从波形上完全看不出异常,只是增益低了几 dB,很容易被归咎于"噪声"。
4.3 完整实现与验证方法
验证频域实现对不对,最直接的办法是拿一个已知方向、已知波形的宽带信号走一遍完整流程,看两件事:波形是否保真、角度是否对准。
% 构造宽带线性调频信号 (复基带) t = (0:1/fs:200e-6).'; s = exp(1j*pi*(B/ (t(end)) )*t.^2); % LFM, 带宽 B % 各阵元时延 tau0 = n * d * sind(theta0) / c; x = zeros(numel(t), N); for nn = 1:N x(:, nn) = interp1(t, s, t - tau0(nn), 'linear', 0) ... .* exp(-1j*2*pi*f0*tau0(nn)); end注意interp1那里的t - tau0(nn),时延是亚采样级的,纳秒级的时延在 400 MHz 采样下不足一个采样点,直接整数移位会损失精度。用线性或者样条插值补,误差可以接受;要求更高就上分数延迟滤波器。
跑完后对比三组结果:
| 处理方式 | 主瓣宽度 | 峰值增益 | 带宽边缘表现 |
|---|---|---|---|
| 全带单权重(窄带假设) | 偏宽且畸变 | 明显下降 | 严重失配 |
| 频域逐 bin 加权 | 接近理论值 | 接近 10log10(N) | 正常 |
| 频域 + 阵元加窗 | 宽 1.3 倍 | 略降 | 正常,旁瓣低 |
理论增益10log10(16) = 12 dB。实测如果只有 8 dB 左右,先查是不是导向矢量用错了频率,再查帧对齐。这个 12 dB 是个很好的"体检指标",白噪声背景下做一次,比对一下输入输出信噪比,差得多说明实现有问题。
还有一点:频域处理天然支持"每个 bin 单独设计权重",这意味着你可以在某些 bin 上做干扰抑制(例如把已知的窄带干扰所在 bin 的权重置零或者做投影),这在时域 DFT 波束形成里是几乎免费的副产品。做声呐或者雷达抗干扰的时候,这个能力比波束本身更有价值。
5. 从 DAS 到 MVDR:DFT 框架下还能怎么往上加
5.1 协方差矩阵估计与对角加载
DFT 波束形成本质是数据独立的(DAS,延时相加),权重只看几何,不看数据。好处是稳健、不需要训练样本、算得快;坏处是旁瓣高、抗干扰能力差,一个强干扰落在旁瓣上就能把你的目标淹掉。
在频域框架下升级到 MVDR(Capon)几乎是顺手的:每个 bin 上估一个协方差矩阵,然后解一个约束优化。
% 在某个频点 m 上累积 K 个快拍 Xm = Xf(m, :).'; % N x 1, 单快拍示例 % 实际用 K 帧累积: Xm_all 是 N x K R = (Xm_all * Xm_all') / K; % 对角加载, 提高小样本下的稳健性 dl = trace(R)/N * 1e-2; R = R + dl*eye(N); a_m = A(:, m); % 该频点的导向矢量 w_mvdr = (R \ a_m) / (a_m' * (R \ a_m));几个实打实的经验:
快拍数至少要是 2N 到 3N,否则 R 是病态的,MVDR 会"自消"(把目标也一起压掉)。N = 16 的话,至少 32 帧,稳妥起见用 64 帧以上。
对角加载量不要省。我一般取trace(R)/N的 1% 到 10%,具体看信噪比。加载太少,小样本下方向图会出现尖锐的凹陷甚至主瓣畸变;加载太多,退化成 DAS,白忙一场。这是一个纯靠试出来的参数,建议在 -10 dB 到 20 dB 信噪比区间各跑一遍,看主瓣是否稳定。
白噪声增益要注意。DAS 的 WNG 就是 N(12 dB),MVDR 在抑制干扰时 WNG 会掉,掉太多意味着它对误差极度敏感。实际系统里通道幅相误差一般有百分之几,WNG 掉到 0 dB 以下基本就不能用了。
5.2 两种方法的实测对比
我在仿真里放了一个目标(20°)和一个比目标强 20 dB 的干扰(-30°),分别用 DAS 和 MVDR 处理,结果差异非常直观:
| 指标 | DAS(DFT 波束) | MVDR(对角加载 1%) |
|---|---|---|
| 目标方向增益 | 12.0 dB | 12.0 dB(约束保证) |
| 干扰方向响应 | -13 dB(落在旁瓣) | -40 dB 以下(零陷) |
| 输出信干噪比 | 差 | 提升约 25 dB |
| 需要的快拍数 | 1 | 60 左右 |
| 对通道误差敏感度 | 低 | 高 |
表格里"干扰方向响应 -13 dB"这一栏很说明问题:如果你的目标信噪比比干扰低 20 dB,那干扰经过旁瓣进来之后,仍然和目标一样强,DAS 根本没戏。这也是为什么实际系统里 DAS 通常只作为第一级粗处理,后面必须接自适应。
5.3 通道幅相误差的现场校正
说到误差,这个必须单拎出来讲。理论仿真里通道是完全一致的,实测里每个射频通道的增益差可能有 ±0.5 dB、相位差 ±5° 甚至更大。相位误差是最致命的,因为波束形成全靠相位相干,5° 的随机相位误差在 16 元阵上累积起来,相干增益能掉 1 到 2 dB,旁瓣抬高 3 到 5 dB。
校正的常规做法是注入一个已知方向的参考信号(近场耦合或者远场辅助源),测出每个通道相对参考通道的复增益:
% 假设 ref 是参考通道数据, x 是所有通道 G = mean(x .* conj(ref), 1) ./ mean(abs(ref).^2); x_cal = x ./ G; % 逐通道复除, 同时校正幅度和相位这一步看着简单,但有两个注意点:必须在工作频带内做,如果只在单频点校,宽带系统的高频端还是错的,得逐 bin 校;校正系数要定期刷新,温度变化会让相位漂移,尤其是长时间工作的系统。
我在一套设备上就吃过这个亏:冷机校正完,跑了两小时之后方向图明显变差,一开始以为是算法问题,查了半天才发现是功放温漂导致的通道相位变化。
6. 常见问题速查与踩坑记录
6.1 症状 - 原因 - 处置对照表
| 症状 | 最可能的原因 | 处置 |
|---|---|---|
| 波束指向和真实来向反号 | 导向矢量/权重共轭搞反了 | 检查w^H x里的共轭位置,用单频信号验证 |
| 波束图出现多个等高尖峰 | d > λ_min/2,空域混叠 | 按最高频率重定阵元间距,或做稀疏阵设计 |
| 主瓣比理论值宽很多 | 孔径不足,或加窗过重 | 核对0.886λ/(Nd),减小窗的滚降 |
| 输出幅度周期性起伏 | WOLA 窗平方和未归一化 | 加win_sq_sum补偿 |
| 宽带边缘角度偏移 | 用了 f0 的导向矢量 | 改为逐 bin 用λ(f)计算 |
| 相干增益只有 6 dB | 通道相位未校准,或帧错位 | 注入参考信号校幅相,检查分帧索引 |
| MVDR 把目标也压掉了 | 快拍不足导致 R 病态 | 增加快拍到 3N 以上,加大对角加载 |
| 端射方向波束特别胖 | 孔径在端射方向投影变短 | 正常物理现象,端射附近避开使用 |
| 补零后角度网格变密但分辨不开 | 补零只是插值不是增加信息 | 需要真分辨率就加阵元或提高频率 |
6.2 我个人踩过的几个坑
第一个坑是角度映射。我一开始直接theta = linspace(-90, 90, N)和 DFT 输出一一对应,跑出来的波束图完全对不上教科书。折腾一晚上才反应过来,DFT 网格是等 sinθ 间隔的,不是等角度间隔。修正之后还有个新问题:d 不取 λ/2 的时候,网格覆盖不了全空域,我又得手动去补零。现在我的习惯是写死一行注释在代码顶上:% 角度网格: sin(theta_k) = k*lam/(N*d), 索引需 fftshift,再也不犯。
第二个坑是复基带和实信号搞混。用实信号做 FFT 的时候,正负频率是镜像的,只处理正频率半边是对的,但权重也得按正频率算,不能两边都用同一个。我有一版代码忘了这个,导致波束出现对称的双峰,一开始还以为是两个目标。
第三个坑是对"补零能提高分辨率"的误解。早年我以为fft(x, 4*N)就能让波束变窄,实际上它只是把波束图插值画得更细,两个靠得很近的目标该糊还是糊。真正的角分辨率由孔径决定,也就是那个0.886λ/(Nd),任何后处理都突破不了(除非上超分辨算法,那是另一套东西)。这个认识转过弯之后,我在阵列设计阶段就把孔径算够,省掉很多后期折腾。
第四个坑是端射方向。用的 d = λ/2,端射方向(±90°)的波束会明显变胖、增益下降,因为此时阵列的有效投影孔径接近零。如果你的应用需要覆盖大角度范围,要么改用平面阵,要么老老实实接受端射方向性能衰减,别在那儿调参数,调不出来的。
最后分享一个实用的验证套路。不管是空域 DFT 还是频域 DFT 实现,我都会先做一个"三点测试":放三个不同方向的单频源,看波束图峰值位置、看主瓣宽度、看旁瓣电平,三个都对上了才算过关。这套流程闭着眼睛执行,绝大多数实现错误十分钟内就能定位。