简介:均匀直线阵常规波束形成(CBF)是阵列信号处理中的经典技术,广泛用于通信、雷达与声纳系统。其核心在于利用各阵元接收信号的相位差,通过权矢量进行相位校正与加权合成,从而实现期望方向的信号增强和干扰抑制。这份Matlab资源面向电子信息类学生与相关工程技术人员,可帮助快速理解CBF原理、方位谱图以及波束扫描等关键环节,也适合作为仿真验证或课程设计的参考。压缩包仅2KB,包含2个m格式源文件,覆盖主程序与辅助函数,结构清晰、无需额外依赖即可运行。通过调整阵元数、间距或指向角等参数,可直观观察波束指向、主瓣宽度、旁瓣电平的变化,并对照傅里叶变换得到的方位功率谱图,加深对相位差、加权与叠加过程的理解。目前已有1695人学习/下载,代码配套清晰的讲解思路,便于将理论公式与实际波形一一对应。资源体量虽小,但完整覆盖了数据采集、相位校正、权重计算到波束合成的流程,是均匀线阵波束形成领域易上手的入门范例,也可作为二次开发的基础。
1. 常规波束形成:均匀直线阵的延时求和本质
做阵列信号处理的人,绕不开 CBF 常规波束形成。它的思想极其朴素:让阵列去“听”某个方向的声音,把各阵元收到的信号按这个方向的传播时延对齐再相加。但就是这么朴素的延迟求和,放到窄带信号里就成了相位补偿,性能和失效边界都藏在参数里:阵元数、阵间距、波长、扫描角。如果你正在做的是声呐、麦克风阵列或雷达方向的测向、增强项目,这份资源能帮你把 CBF 从公式变成可运行代码,再搞明白为什么阵列间距超过半波长就会出现假峰、为什么主瓣宽度不是你肉眼以为的那个值。
2. 从几何到数学:导向矢量决定了波束的指向
2.1 均匀直线阵的几何模型:d、N、λ 三个参数决定自由度
均匀直线阵的“均匀”指阵元等间距排在一条直线上,间距记作 d,阵元数记作 N。每个阵元都当成一个全向点接收器,实际工程里的阵元方向图会让幅度谱略微变化,但在 CBF 的第一层推导里先把阵元方向图忽略掉,默认各向同性。信号源假设在远场,也就是波前到达阵列时可以近似为平面波,这个假设是 CBF 的立身之本,一旦目标距离近到波前明显弯曲,后面整套导向矢量模型就要换成近场聚焦模型。
设信号频率在窄带范围内,入射方向与阵法线方向的夹角为 θ,以第一个阵元为参考点,那么相邻阵元之间,同一波前到达的时间差是 d sinθ / c。换到相位域就变成一个固定相位差 2π d sinθ / λ。用这个相位差就能写出从 θ 方向入射一个单位幅度信号时、N 个阵元上各自收到的复数值,这组数值就是这一方向的导向矢量 a(θ):
import numpy as np def steering_vector(N: int, d: float, theta_deg: float, wavelength: float) -> np.ndarray: theta = np.deg2rad(theta_deg) n = np.arange(N) # 相位延迟:第n个阵元相对第0个阵元累积的相移 phase = -2 * np.pi * d * np.sin(theta) / wavelength * n return np.exp(1j * phase)这段代码里最关键的逻辑是把空间传播时延转成复指数形式。相位中 d * sin(theta) / wavelength 是相邻阵元间距折算成的波长数,再乘 2π 得到弧度相移,乘 n 得到累积值。数组 a 的第 n 个元素就是第 n 个阵元对来自 θ 方向的窄带信号的响应。注意符号约定:这里用负数相位表示“后面阵元更晚收到信号”,如果你的代码里把符号写反,波束图会指向 -θ 方向——这是最常见的低级错误。参数方面,theta_deg 是相对阵列法线的角度,侧射方向为 0°,端射方向是 ±90°,扫到 90° 附近时 sinθ 趋于饱和,波束会明显展宽,后面第 4 章专门讲这个现象。
2.2 CBF 的本质是相位补偿:延时求和到加权求和的桥梁
有了导向矢量,常规波束形成就一句话:把接收数据沿着目标方向的导向矢量做共轭加权再求和。为什么是共轭?因为导向矢量里存的是从 θ 方向来的信号在各阵元上的固有相位,如果我猜的目标方向正好是 θ,那么把每个阵元的数据乘上 a(θ) 的共轭,正好把这份固有相位抵消掉,各阵元信号就变成同相相加,幅度最大。等价写法是 y = w^H x,其中权矢量 w = a(θ) / N,N 是归一化系数,让无指向性损耗时输出为单位增益。这里其实用到了阵列处理里的阵增益概念,常规波束形成的阵增益来自 N 个阵元噪声不相关时的相干积累,理论上能让输出信噪比比单阵元提升 10lgN dB。
窄带前提下,时域延时完全等价于频域相位旋转,所以 CBF 也能放到频域做:对每个快拍做 FFT,选感兴趣频点,在这个频点上用 a(θ) 的共轭做加权,再求和。宽带信号想沿用 CBF,得先把数据划进多个窄带子带分别处理,或者直接在时域做真的分数时延滤波器,工程实现复杂度会上一个台阶。做麦克风阵列的朋友经常在这个地方吃到亏:采集的是宽带语音,直接用单频点的 CBF,出来的方向图其实只在一个频率上是对的。
def beamform(signal_matrix: np.ndarray, w: np.ndarray) -> np.ndarray: # signal_matrix: (N, snapshots) 复数阵元数据 # w: (N,) 复数权矢量 return w.conj() @ signal_matrix这段代码就是把权矢量的共轭与数据矩阵做内积,输出的每个元素对应一个快拍的波束输出。w.conj() 是关键,漏掉共轭等于把相位补偿做反了,波束会变成“反方向增强”。参数上建议把 N 归一化放进 w 里而不是在 beamform 里除,这样后续换 MVDR 权时接口不用改。
3. 用 Python 复现 CBF:从波束图到空间谱
3.1 核心代码实现:扫描、加权、谱输出的完整链路
这一节直接给一套能跑的 CBF 扫描代码。把从 -90° 到 90° 的每个角度都算一遍加权输出功率,得到的功率谱就是空间谱,谱峰所在角度就是来波方向估计值。完整的常规波束形成空间谱函数要处理三个环节:生成扫描角度网格、逐角度计算权矢量、逐角度做加权后输出平均功率。
import numpy as np def cbf_spectrum(signal_matrix: np.ndarray, d: float, wavelength: float, scan_deg: np.ndarray) -> np.ndarray: """信号矩阵 shape=(N, snapshots),返回各扫描角的输出功率。""" N = signal_matrix.shape[0] power = np.zeros(len(scan_deg)) for i, theta_deg in enumerate(scan_deg): a = steering_vector(N, d, theta_deg, wavelength) w = a.conj() / N y = w @ signal_matrix # 1 x snapshots power[i] = np.mean(np.abs(y) ** 2) return power N = 16 d = 0.5 # 单位:波长,0.5 即半波长间距 lam = 1.0 angles = np.linspace(-90, 90, 361) # 模拟一个从 30 度入射、含噪声的接收数据 theta_true = 30.0 snap = 500 n_arr = np.arange(N) phase = -2 * np.pi * d * np.sin(np.deg2rad(theta_true)) / lam * n_arr signal = np.exp(1j * phase)[:, None] @ np.ones((1, snap)) noise = (np.random.randn(N, snap) + 1j * np.random.randn(N, snap)) / np.sqrt(2) sig = signal + 0.1 * noise spectrum = cbf_spectrum(sig, d, lam, angles)这段代码里 signal 通过外积构造了一个所有快拍同相位的干净信号,noise 是单位功率的复高斯白噪声,0.1 的噪声系数让 SNR 约为 20dB。cbf_spectrum 里对每个扫描角都重新生成导向矢量和权矢量,语义清楚但计算冗余,实际工程里可以用循环方式写矩阵化扫描提升效率。关键参数是快拍数 snap,快拍越多功率估计方差越小,少于 50 个快拍时谱峰会显得毛糙,峰位抖动明显。运行完取 spectrum 最大值对应的角度,应该非常接近 30 度,这就是 CBF 做测向的全部原理——关于每个快拍统计功率时“取均值而不是取和”的细节值得注意:取均值把增益归一化到角度无关的尺度,方便对比不同阵元数量的结果。
3.2 空间谱归一化与横坐标转换:雷达和声呐习惯不太一样
拿到 spectrum 之后有两个常见处理。第一个是归一化:把谱值转成 dB 单位,同时减去最大值令主瓣为 0dB,这样旁瓣高度可读;第二个是横坐标再处理:雷达、通信里经常直接用 sinθ 作横坐标,因为波束图的数学形式在 sinθ 域是平移不变的,栅瓣间隔也更直观。但我通常更建议先保留 θ 横坐标可视化一次,再切到 sinθ 域第二次确认栅瓣位置,两个域都看过,对参数设置的理解会完全不同。
import matplotlib.pyplot as plt spec_db = 10 * np.log10(spectrum / spectrum.max()) fig, ax = plt.subplots(1, 1, figsize=(8, 4.5)) ax.plot(angles, spec_db, linewidth=1.5) ax.set_xlabel('look direction (deg)') ax.set_ylabel('normalized power (dB)') ax.set_ylim(-40, 3) ax.grid(True) plt.tight_layout() plt.show()这段绘图代码会把主瓣拉到 0dB,旁瓣高度一目了然。16 阵元半波长间距均匀线阵的 CBF 第一旁瓣理论值是 -13.2dB,如果画出来的旁瓣高过 -10dB,优先检查权矢量归一化是不是除错了:除以 N 才对,分母写 sqrt(N) 会让旁瓣抬高约 3dB。绘图前把 y 轴下限设到 -40dB 比较合适,低于 -40dB 的旁瓣值基本可以当作数值噪声。实际画完图你会发现主瓣两侧还各有几个对称小瓣,这些小瓣对不同角度来波都敏感,说明 CBF 的空间分辨率有限——这是测向时误判邻近目标方位的常见来源,两个目标夹角小于主瓣宽度时,谱上只有一个峰。
3.3 加噪验证增益:为什么信噪比提升了约 10lgN dB
做完谱估计,再做一个定量实验验证 CBF 的阵增益:分别看单阵元和波束输出的 SNR 对比。这个实验能帮助你判断自己的仿真链路是否正确,也是从玩具数据走向工程数据之间最简单的一步:如果一个仿真环境里 CBF 的增益对不上 10lgN,说明噪声模型里混入了阵元间的相关成分,或者信号不是严格的窄带平面波。
snr_in = 10 * np.log10(np.mean(np.abs(signal) ** 2) / np.mean(np.abs(noise) ** 2)) w_true = np.exp(1j * phase).conj() / N beam_out = w_true @ sig snr_out = 10 * np.log10(np.mean(np.abs(w_true @ signal) ** 2) / np.mean(np.abs(w_true @ noise) ** 2)) print(f"input SNR: {snr_in:.2f} dB, beamformer SNR: {snr_out:.2f} dB, gain: {snr_out - snr_in:.2f} dB")这里输入 SNR 直接按功率算,输出 SNR 的频率保持一致,因为窄带信号在加权前后频率不变,噪声功率则因相干积累受到抑制。运行结果里 gain 应当在 12dB 附近,对应 10lg16 ≈ 12.04dB。如果你得到 15dB 以上,很可能是两个阵列的噪声用了同一组随机数——别笑,我见过不少实现是 noise 变量在构造时没重新生成,导致各阵元噪声完全相关,CBF 无法抑制这种噪声,增益反而被推高了;反过来如果 gain 只有 9dB,通常就是快拍数不足、功率估计方差太大,把快拍数加到 2000 再试。
4. 参数选择与避坑:栅瓣、波束展宽与模型边界
4.1 避坑第一类:栅瓣假峰,阵列测向的经典误判
CBF 空间谱里出现多个同样高度的峰时,最可能的原因就是栅瓣。栅瓣的产生条件是阵元间距超过半波长:此时相邻阵元的相位差在可见域内可能相差 2π 的整数倍,导向矢量变得不唯一,不同来波方向对应相同的阵元相位分布。具体表现是——真实来波在 30 度出现在谱峰,另一个方向上也会出现一个幅度几乎一样的假峰。
d = 1.2 # 大于半波长,栅瓣出现了 angles_scan = np.linspace(-90, 90, 361) spectrum_alias = cbf_spectrum(sig, d, lam, angles_scan)把这段里的 d 换成 1.2 波长再跑一遍,画出的谱里 30 度旁边会出现第二个大峰。原因是可见域 u = sinθ 的范围是 [-1,1],当 d/λ = 1.2 时,对应 u 域的波束周期是 λ/d ≈ 0.833,在这个范围里能放下不止一个主瓣周期。解决办法按优先级排列:如果系统还没设计定,直接把 d 改成 0.5 波长以下;如果阵列尺寸已经固定没法改间距,至少把扫描范围限制在单值区宽度不大于 λ/d 的范围内,比如 d = 1.2λ 时能无栅瓣扫描的误差角范围大概是 ±asin(1/1.2) ≈ ±56°。工程上很多声呐为了节省阵元数量硬把间距做到 1λ,这时必须清楚记录“在什么角度范围内结果是可信的”。
摆一下判断流程:看到空间谱有等高峰,先算 λ/d,再用这个值估算第一栅瓣角和真实主瓣的间隔间隔。间隔是 asin(λ/d),与主瓣宽度无关。如果你把这个问题误当成双目标分辨问题去调权、调窗,大概率白忙一场,因为根源在几何布局而非权设计。
4.2 避坑第二类:主瓣宽度不等于分辨率,波束宽度会随扫偏变化
CBF 的 3dB 波束宽度近似公式是 Δθ_3dB ≈ 0.886λ / (N d cosθ),注意分母里的 cosθ。这意味着侧射方向(θ=0)波束最窄,往端射方向扫描时 cosθ 变小、波束迅速展宽:30 度时展宽约 15%,60 度时已经变成侧射的两倍。这个现象不是故障,是直线阵投影几何的必然结果,但很多第一次做测向仿真的新手会误以为谱峰变宽是数值误差。如果你看到谱峰明显比手册写的宽,先看扫描到多少度,再判断是不是布阵需要改成圆弧阵或 L 阵来解决端射分辨率退化问题。
另外主瓣宽度给的是“可分辨间隔”的下限,但它不是阈值。两个等幅度目标间距小于 3dB 宽度时,CBF 谱上表现为一个展宽的峰;间距略大于宽度时表现为肩膀或平顶峰;大于约两倍宽度时才出现两个独立峰。工程上做双目标分辨,要么增加阵元数 N,要么改用自适应波束形成或超分辨算法,常规 CBF 只能作为基线参考。如果你实际场景的分辨需求很刚性,直接把 CBF 定位成“监听与测向基线”,把 MVDR、MUSIC 作为真正竞争力那层。
4.3 避坑第三类:窄带与远场假设同时失效时的翻车现场
CBF 有两个隐藏前提,很多人仿真跑通了就忘。第一个是窄带:信号带宽必须窄到相位差在带宽内变化可忽略。典型判据是 B << c / (N d),也就是信号从阵列一头到另一头的传播时间远小于信号带宽倒数。语音信号带宽 300Hz 到 3400Hz,波长跨度十几倍,如果直接拿中心频率做 CBF,波束图在低频和高频相差悬殊,听感就是“远处人声发嗡”。工程解法是先滤波出子带,或使用时域分数延时补偿。第二个是远场:R > 2D^2 / λ 是常见的 Fraunhofer 距离判据,D 是阵列孔径。测试距离小于这个值时,波前是球面波,用平面波导向矢量对齐会引入相位误差,主瓣升高、零陷变浅。麦克风阵列桌面应用经常踩这个坑,说话人离阵列半米,按远场公式算端口径 0.2m 时 R > 0.8m 才勉强算远场,桌面设备实际不到这个距离,近场效应明显——此时要么改用近场聚焦模型,要么分段处理距离与方位二维扫描。
总结一下这几类坑的共性:它们都不是代码调试能解决的问题,而是模型边界认知问题。写 CBF 代码本身一小时就够,花时间的地方在于正确判断你的场景到底配不配用这个模型。
5. 验证与延伸:从仿真到实测处理数据前的最后习惯
5.1 一个固定入射角的“回归测试”脚本,每次跑阵列代码前先过一遍
我处理过的阵列项目里,几乎每次重构代码都会引入符号或单位错误:某一处忘了把角度转弧度、某一处共轭写错位置、某一处波长用了毫米却把间距写成了米。为了不让这些低级错误混进后续算法调试,我养成了一个固定习惯——先让一个单频信号从 30 度入射,跑一次 CBF 全链路,检查四个东西:谱峰位置是不是 30 度、峰值功率是否约等于 0dB、主瓣是否在 -13dB 附近、以及运行时间是否在预期量级。任何一项不对就直接停,不要在错误的基础上调参。
def sanity_check_cbf(N=16, d=0.5, theta_true=30.0): lam = 1.0 angles = np.linspace(-90, 90, 361) n_arr = np.arange(N) phase = -2 * np.pi * d * np.sin(np.deg2rad(theta_true)) / lam * n_arr sig = np.exp(1j * phase)[:, None] @ np.ones((1, 2000)) spec = cbf_spectrum(sig, d, lam, angles) peak_idx = int(np.argmax(spec)) peak_db = 10 * np.log10(spec[peak_idx] / spec.max()) assert abs(angles[peak_idx] - theta_true) < 0.5, "peak direction mismatch" assert abs(peak_db) < 1e-6, "peak power not normalized" # 找第一旁瓣位置(主瓣两侧 -90 度扫描中最近局部极小外的局部最大) spec_db = 10 * np.log10(spec / spec.max()) from scipy.signal import find_peaks peaks, _ = find_peaks(spec_db) sorted_peaks = sorted(peaks, key=lambda i: -spec_db[i]) sll = spec_db[sorted_peaks[1]] if len(sorted_peaks) > 1 else -99 assert sll < -10, f"sidelobe too high: {sll:.1f} dB" print(f"peak={angles[peak_idx]:.1f} deg, SLL={sll:.1f} dB — CBF 链路正常")这段脚本把“波束图形态对不对”量化成了三个断言:峰位偏差小于 0.5°、峰值归一误差小于 1e-6、第一旁瓣低于 -10dB。这里 scipy.signal.find_peaks 的作用是把所有局部峰找出来再排序,第二个峰通常就是第一旁瓣;对 16 阵元来说第一旁瓣理论 -13.2dB,断言值放 -10dB 是留宽裕度。这个脚本我几乎在每个阵列项目里都会保留,换数据只换构造方式,不动验证逻辑。从那以后我每次写阵列处理代码都强制先跑一遍这个 sanity check,跑不过绝不继续调参数,这个习惯让后续所有算法调试都更顺——希望帮到你。
本文还有配套的精品资源,点击获取