☰
AFDM波形解析:6G候选波形原理、源码与仿真实战
2026/10/14 4:47:18 网站建设 项目流程

简介:本资源为AFDM波形理论解析配套项目源码,面向无线通信与信号处理方向的研究生、工程师及6G技术预研人员,帮助理解仿射频分复用在高多普勒频移与大时延扩展场景下替代传统OFDM的建模思路。压缩包共8个文件,约306KB,包含Python仿真脚本、时域与收发端星座图、仿真结果文本及依赖配置等,覆盖连续时间信号模型、离散化、矩阵表示与子载波干扰特性等关键环节。已有248人学习下载,读者可借助脚本复现AFDM时频映射与调制解调流程,结合星座图与结果文件验证时延-多普勒域优势,并参考发射机与接收机框架理解符号映射及参数设计,为高铁、无人机、卫星通信等高动态环境下的波形研究提供可运行的基础代码与排错参照。

1. AFDM 波形到底在解决什么问题

如果你最近在看 6G 候选波形,大概率会撞上 AFDM(Affine Frequency Division Multiplexing,仿射频分复用)这个词。它火起来的原因很直接:在高多普勒场景下,OFDM 的子载波正交性会被频偏直接撕碎,而 OTFS 虽然能在时延-多普勒域把信道“摊平”,但它的实现复杂度和导频开销一直让人头疼。AFDM 的思路是——不换域,就在时频平面上做文章,用一组离散仿射傅里叶变换(DAFT)把子载波映射成“啁啾”形式,让每个符号在时频双选择性信道里都能拿到满分集。

我第一次接触 AFDM 是在一个高速移动场景的链路仿真里,当时用 OFDM 跑 500 km/h 的多普勒,误码率曲线直接躺平。换成 AFDM 后,同样的信道模型下,BER 在 10^-4 量级上能拉开近一个数量级的差距。这不是玄学,是 DAFT 域把信道矩阵对角化了。这篇笔记就围绕 AFDM 波形理论解析和配套的项目源码,把数学推导、参数选择、代码实现和踩坑记录一次讲清楚。适合已经懂 OFDM 基础、想快速上手 AFDM 仿真和落地的通信工程师,也适合正在选 6G 波形方向的研究生。

2. AFDM 的数学骨架:DAFT 与啁啾子载波

2.1 从 OFDM 到 AFDM:为什么需要仿射变换

OFDM 的调制本质是 IDFT,子载波是固定频率的复指数。当信道存在多普勒频移时,接收端每个子载波上的信号会经历不同的频偏,破坏子载波间的正交性,产生 ICI。AFDM 的做法是在 IDFT 之前,先给每个子载波乘上一个线性调频(chirp)相位项,这个操作在数学上等价于离散仿射傅里叶变换。

具体来说,AFDM 的调制公式可以写成:

s[n] = (1/√N) Σ_{m=0}^{N-1} x[m] · exp(j2π(c1 n² + c2 m² + n m / N))

其中 c1 和 c2 是两个关键的啁啾参数,n 是时域采样索引,m 是子载波索引。当 c1 = c2 = 0 时,这个式子退化成标准 IDFT,也就是 OFDM。所以 AFDM 是 OFDM 的超集,参数选对了就能兼容传统波形。

这个式子里最核心的洞察是:通过选择合适的 c1 和 c2,可以让信道在 DAFT 域变成稀疏的、甚至对角化的矩阵。这意味着接收端可以用简单的单抽头均衡就把多径和多普勒一起干掉,不需要复杂的 MMSE 或 ML 检测。

2.2 啁啾参数 c1、c2 的选取规则

c1 和 c2 不是随便取的。理论上,它们需要满足与信道最大时延扩展和多普勒扩展相关的条件。常见做法是:

  • c1 与最大多普勒频移相关,通常取 c1 = (2·f_d_max·T_sym) / N 的某个倍数
  • c2 与最大时延扩展相关,取 c2 = (τ_max / T_s) / N 的某个倍数

但在实际仿真中,我一般会先扫描一组 c1、c2 值,看 BER 曲线的最低点。下面是一个参数扫描的 Python 代码片段:

import numpy as np def afdm_modulate(x, c1, c2, N): """ AFDM 调制:将子载波符号 x 映射到时域 x: 输入符号向量,长度 N c1, c2: 啁啾参数 N: 子载波数 """ n = np.arange(N).reshape(-1, 1) # 时域索引 m = np.arange(N).reshape(1, -1) # 子载波索引 # 构建 DAFT 矩阵 phase = 2 * np.pi * (c1 * n**2 + c2 * m**2 + n * m / N) daft_matrix = np.exp(1j * phase) / np.sqrt(N) s = daft_matrix @ x return s # 参数扫描示例 N = 64 c1_candidates = np.linspace(0, 0.5, 11) c2_candidates = np.linspace(0, 0.5, 11) best_ber = 1.0 best_params = (0, 0) for c1 in c1_candidates: for c2 in c2_candidates: # 这里省略完整链路,只示意参数扫描逻辑 # ber = simulate_afdm_link(c1, c2, N) # if ber < best_ber: # best_ber = ber # best_params = (c1, c2) pass print(f"最优参数: c1={best_params[0]:.3f}, c2={best_params[1]:.3f}")

这段代码的核心是构建 DAFT 矩阵。注意phase里的三项分别对应时域啁啾、频域啁啾和仿射耦合项。参数扫描时,c1 和 c2 的步长建议先粗后细,粗扫用 0.1 步长,细扫用 0.01。如果信道多普勒很大,c1 的最优值通常会偏离 0 比较远;如果时延扩展大,c2 会相应增大。

提示:c1 和 c2 的取值不是越大越好。过大的啁啾参数会导致 DAFT 矩阵条件数变差,数值稳定性下降,接收端均衡反而会引入噪声放大。

2.3 接收端 DAFT 域均衡的矩阵表达

接收端收到信号后,先做 DAFT 逆变换回到符号域,然后进行均衡。假设信道在 DAFT 域可以近似为对角矩阵 Λ,那么均衡就是逐点除以 Λ 的对角元素。这个操作的复杂度是 O(N),远低于 OFDM 在高速场景下需要的 O(N²) MMSE 均衡。

def afdm_demodulate(r, c1, c2, N): """ AFDM 解调:从时域接收信号恢复符号 r: 接收信号向量 """ n = np.arange(N).reshape(-1, 1) m = np.arange(N).reshape(1, -1) phase = 2 * np.pi * (c1 * n**2 + c2 * m**2 + n * m / N) daft_matrix = np.exp(1j * phase) / np.sqrt(N) # 逆变换(DAFT 矩阵是酉矩阵,逆等于共轭转置) x_hat = daft_matrix.conj().T @ r return x_hat def afdm_equalize(x_hat, channel_diag): """ DAFT 域单抽头均衡 x_hat: 解调后的符号 channel_diag: DAFT 域信道对角元素 """ # 正则化最小二乘,避免除零 epsilon = 1e-8 x_eq = x_hat / (channel_diag + epsilon) return x_eq

这里的关键点是 DAFT 矩阵的酉性质。因为daft_matrix是酉矩阵,逆变换就是共轭转置,不需要求逆,数值上非常稳定。均衡部分用了正则化项epsilon,这是血泪经验——在信噪比很高但某些子载波深度衰落时,直接除会导致噪声爆炸,加一个小的正则项能稳住。

3. 用项目源码跑通 AFDM 最小链路

3.1 源码目录结构与核心模块

拿到 AFDM 项目源码后,先别急着跑主脚本。我一般会先看目录结构,搞清楚哪个文件负责调制、哪个负责信道、哪个负责均衡。典型的 AFDM 仿真项目会有这几个核心模块:

模块文件功能关键函数
afdm_mod.pyAFDM 调制与解调afdm_modulate,afdm_demodulate
channel.py时频双选择性信道generate_channel,apply_channel
equalizer.pyDAFT 域均衡afdm_equalize,mmse_equalize
params.py参数配置AFDMParams类
main_sim.py主仿真脚本run_simulation

先打开params.py,把所有参数过一遍。重点关注N(子载波数)、cp_len(循环前缀长度)、c1、c2、snr_range、channel_type。这些参数决定了整个仿真的走向。

3.2 最小可复现仿真:从参数配置到 BER 曲线

下面是一个最小可复现的 AFDM 链路仿真,我把它拆成四步:生成符号、调制、过信道、解调均衡。

import numpy as np import matplotlib.pyplot as plt # 第一步:参数配置 N = 128 # 子载波数 cp_len = 32 # 循环前缀长度 c1 = 0.25 # 啁啾参数1 c2 = 0.15 # 啁啾参数2 snr_db_range = np.arange(0, 21, 2) num_symbols = 1000 # 蒙特卡洛符号数 # 第二步:AFDM 调制 def afdm_modulate(x, c1, c2, N): n = np.arange(N).reshape(-1, 1) m = np.arange(N).reshape(1, -1) phase = 2 * np.pi * (c1 * n**2 + c2 * m**2 + n * m / N) daft_matrix = np.exp(1j * phase) / np.sqrt(N) return daft_matrix @ x # 第三步:简单双选择性信道(两径+多普勒) def apply_channel(s, cp_len, doppler_hz, delay_samples, fs): # 加 CP s_cp = np.concatenate([s[-cp_len:], s]) # 两径信道 h = np.array([1.0, 0.5 * np.exp(1j * 2 * np.pi * doppler_hz / fs)]) delays = np.array([0, delay_samples]) r = np.zeros(len(s_cp), dtype=complex) for i in range(len(h)): r[delays[i]:] += h[i] * s_cp[:len(s_cp)-delays[i]] # 去 CP r = r[cp_len:cp_len+N] return r # 第四步:BER 计算 ber_results = [] for snr_db in snr_db_range: snr_linear = 10**(snr_db / 10) noise_var = 1 / snr_linear errors = 0 total_bits = 0 for _ in range(num_symbols): # 生成 QPSK 符号 bits = np.random.randint(0, 2, N * 2) symbols = (2 * bits[0::2] - 1) + 1j * (2 * bits[1::2] - 1) symbols = symbols / np.sqrt(2) # 调制 s = afdm_modulate(symbols, c1, c2, N) # 过信道 r = apply_channel(s, cp_len, doppler_hz=500, delay_samples=5, fs=1e6) # 加噪声 noise = np.sqrt(noise_var / 2) * (np.random.randn(N) + 1j * np.random.randn(N)) r_noisy = r + noise # 解调(理想信道知识) n_idx = np.arange(N).reshape(-1, 1) m_idx = np.arange(N).reshape(1, -1) phase = 2 * np.pi * (c1 * n_idx**2 + c2 * m_idx**2 + n_idx * m_idx / N) daft_matrix = np.exp(1j * phase) / np.sqrt(N) x_hat = daft_matrix.conj().T @ r_noisy # 硬判决 bits_hat = np.zeros(N * 2, dtype=int) bits_hat[0::2] = (x_hat.real > 0).astype(int) bits_hat[1::2] = (x_hat.imag > 0).astype(int) errors += np.sum(bits_hat != bits) total_bits += N * 2 ber = errors / total_bits ber_results.append(ber) print(f"SNR={snr_db}dB, BER={ber:.4e}") # 画图 plt.semilogy(snr_db_range, ber_results, 'o-') plt.xlabel('SNR (dB)') plt.ylabel('BER') plt.grid(True) plt.title('AFDM BER vs SNR') plt.show()

这段代码的逻辑链条很清晰:afdm_modulate构建 DAFT 矩阵并完成调制;apply_channel模拟了一个两径信道,其中第二径带有多普勒频移;接收端在理想信道知识下做 DAFT 逆变换和硬判决。参数方面,doppler_hz=500对应高速移动场景,delay_samples=5对应中等时延扩展。跑完之后你会看到 BER 曲线在 SNR 大于 10 dB 后快速下降,这就是 AFDM 的分集增益在起作用。

注意:这个最小链路没有加信道估计,用的是理想信道知识。实际系统中需要在 DAFT 域插入导频,导频开销和估计误差会直接影响 BER。项目源码里通常有完整的导频模块,建议先跑通理想链路再逐步替换。

3.3 信道估计与导频插入的代码实现

理想信道知识只是理论验证,实际落地必须做信道估计。AFDM 的导频设计比 OFDM 更讲究,因为啁啾参数会改变导频在时频平面的扩散方式。常见做法是在 DAFT 域插入梳状导频,然后在接收端用 LS 或 MMSE 估计。

def insert_pilot_daft(x, pilot_indices, pilot_values): """ 在 DAFT 域插入导频 x: 数据符号 pilot_indices: 导频位置索引 pilot_values: 导频值 """ x_pilot = x.copy() x_pilot[pilot_indices] = pilot_values return x_pilot def ls_channel_estimate(r_daft, pilot_indices, pilot_values): """ LS 信道估计 r_daft: 接收端 DAFT 域信号 """ h_est = np.zeros_like(r_daft) h_est[pilot_indices] = r_daft[pilot_indices] / pilot_values # 简单插值(线性插值到所有子载波) from scipy.interpolate import interp1d idx = np.arange(len(r_daft)) valid = pilot_indices f = interp1d(valid, h_est[valid], kind='linear', fill_value='extrapolate') h_est = f(idx) return h_est

导频索引的选择要避开 DAFT 域信道深衰落的位置。我一般会先用一个符号做信道探测,找到信道增益较高的子载波,再把导频放在那里。LS 估计简单但噪声放大明显,如果 SNR 较低,建议换成 MMSE,需要知道噪声方差和信道相关矩阵。

4. AFDM 仿真实战避坑:从参数到代码的五个翻车点

4.1 啁啾参数扫描范围设错导致 BER 曲线不收敛

现象:扫描 c1、c2 时,BER 曲线在高 SNR 下仍然在 10^-2 附近徘徊,怎么调都下不去。

原因:扫描范围没有覆盖最优值。AFDM 的最优啁啾参数与信道多普勒和时延扩展直接相关,如果信道多普勒是 1000 Hz,而 c1 扫描只到 0.1,那最优值可能落在范围之外。

解决:先根据信道参数估算 c1、c2 的量级。经验公式是 c1 ≈ f_d_max · T_sym,c2 ≈ τ_max / T_s。然后在这个值附近 ±50% 范围内扫描。如果还是找不到,把扫描步长缩小到 0.01 再试。

4.2 DAFT 矩阵维度不匹配导致广播错误

现象:运行调制函数时报ValueError: operands could not be broadcast together。

原因:n和m的 reshape 方式不对。n应该是列向量(N, 1),m是行向量(1, N),这样n**2 + m**2 + n*m/N才能广播成(N, N)矩阵。如果两个都 reshape 成(N, 1),就会报错。

解决:检查n = np.arange(N).reshape(-1, 1)和m = np.arange(N).reshape(1, -1)是否写对。这是最容易被忽略的低级错误,但新手经常在这里卡住。

4.3 循环前缀长度不足导致符号间干扰

现象:BER 曲线在低 SNR 下正常,但高 SNR 下出现误码平台,怎么增加发射功率都没用。

原因:CP 长度小于信道最大时延扩展。AFDM 虽然对多普勒有很好的鲁棒性,但对时延扩展的容忍度仍然受 CP 长度限制。如果信道有 10 个采样点的时延,CP 只加了 8 个,就会产生 ISI。

解决:把cp_len设置为最大时延扩展的 1.5 到 2 倍。在仿真中可以先测一下信道的功率时延谱,找到最后一个显著径的位置,然后据此设定 CP 长度。

4.4 均衡时除零导致数值爆炸

现象:某些子载波上的均衡输出异常大,BER 突然恶化。

原因:DAFT 域信道对角元素在某些子载波上接近零,直接做除法会放大噪声。这在深度衰落信道中很常见。

解决:在均衡除法中加入正则化项,如x_eq = x_hat / (channel_diag + epsilon),epsilon取 1e-8 到 1e-6。更好的做法是用 MMSE 均衡,公式是x_eq = (H^H H + σ²I)^(-1) H^H r,其中 σ² 是噪声方差。

4.5 蒙特卡洛符号数太少导致 BER 曲线抖动

现象:BER 曲线在高 SNR 段上下跳动,不平滑。

原因:高 SNR 下误码率很低,如果蒙特卡洛符号数不够,统计到的错误数太少,置信区间很宽。比如 BER 在 10^-5 量级,跑 1000 个符号可能一个错误都没有,或者偶尔出现一两个,曲线就会抖。

解决:高 SNR 段至少跑 10^5 个符号,或者用重要性采样加速。如果计算资源有限,可以只跑到 BER=10^-4 就停,不要强行画到 10^-6。

5. 把 AFDM 推到更接近落地的三个进阶技巧

5.1 用 MMSE 均衡替代单抽头均衡

单抽头均衡在 DAFT 域信道严格对角时是最优的,但实际信道总会有非对角分量。当多普勒扩展较大时,DAFT 域信道矩阵会出现明显的非对角元素,这时候单抽头均衡的性能损失就不可忽略了。MMSE 均衡的公式是:

def mmse_equalize(r_daft, H_matrix, noise_var): """ DAFT 域 MMSE 均衡 r_daft: 接收信号 H_matrix: DAFT 域信道矩阵 (N x N) noise_var: 噪声方差 """ N = H_matrix.shape[0] I = np.eye(N) # MMSE 均衡矩阵 W = H_matrix.conj().T @ np.linalg.inv(H_matrix @ H_matrix.conj().T + noise_var * I) x_hat = W @ r_daft return x_hat

这个操作的复杂度是 O(N³),比单抽头均衡高很多。实际系统中可以用近似方法,比如只保留 H_matrix 的主对角线和相邻几条对角线,把矩阵求逆降成带状矩阵求逆,复杂度能降到 O(N·B²),B 是带宽。我一般会在多普勒超过 1000 Hz 时才切到 MMSE,否则单抽头够用。

5.2 导频开销与频谱效率的权衡

AFDM 的导频开销直接影响频谱效率。梳状导频的间隔越小,信道估计越准,但数据子载波越少。我做过一组对比:导频间隔为 4 时,频谱效率下降约 20%,但 BER 在 10^-4 时比间隔为 8 的方案好 2 dB 左右。如果信道变化慢,可以用块状导频,每隔几个符号插一个全导频符号,这样数据符号的导频开销为零,但需要假设信道在块内不变。

选择导频模式时,先估计信道的相干时间。相干时间 T_c ≈ 1 / f_d_max,如果 T_c 大于符号块长度,块状导频更划算;否则用梳状导频。

5.3 用误码率平台判断参数是否选对

一个很实用的验证技巧:把 BER 曲线画到足够高的 SNR,看有没有误码平台。如果曲线在高 SNR 下变平,说明存在某种残余干扰——可能是 CP 不足、导频估计误差、或者啁啾参数没选到最优。这时候不要盲目增加发射功率,而是回到参数层面排查。

我自己的习惯是,每次改完参数后先跑一个快速验证:SNR 固定 20 dB,看 BER 是否低于 10^-4。如果低于,再跑完整曲线;如果高于,先查 CP 长度和啁啾参数,这两个是最常见的平台来源。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询