简介:这份资源面向无线通信与信号处理方向的研究生、工程师及6G技术爱好者,系统讲解AFDM(仿射频分复用)波形的数学建模与信号构造原理,帮助读者理解其如何通过仿射变换实现时频平面灵活映射,从而缓解传统OFDM在高多普勒频移和大时延扩展下的性能下降问题。压缩包共8个文件,约306KB,包含Python仿真脚本、时域与收发端星座图、依赖清单及仿真结果文本等,可支撑连续时间模型、离散化、矩阵表示与子载波干扰特性的对照学习。资源还涉及发射机与接收机框架、符号映射及调制解调流程,并讨论高铁、无人机、卫星通信等高动态场景的适用性。目前已有247人学习下载,适合希望从理论推导到仿真验证完整掌握AFDM波形、并为参数优化与6G融合研究打基础的读者。
1. AFDM波形理论解析:从OTFS的局限到仿射调制的工程破局
做水声通信或者高速移动场景的同行,大概率都经历过OFDM在大多普勒下彻底翻车的绝望——子载波间干扰一上来,导频跟数据一起糊掉,均衡器怎么调都是玄学。AFDM(Affine Frequency Division Multiplexing,仿射频分复用)这两年在通信圈热度往上走,核心原因就一个:它用一条线性调频的仿射变换,把时频双选择性信道摊平成一个近似稀疏的等效信道,让接收端可以用很少的抽头就把多径和多普勒一起收掉。这个标题里的“波形理论解析”不是让你推公式推到手酸,而是要把AFDM的调制矩阵、啁啾参数、等效信道结构这三件事讲清楚,再配一份能跑起来的项目源码,让你在本地把误码率曲线复现出来。适合谁看:做过OFDM基带、想切到高移动性波形方向的工程师,以及需要一套可改可调的AFDM仿真框架来验证自己均衡算法的研究者。下面按“理论立住→源码跑通→参数调优→踩坑排查”的顺序往下走。
2. AFDM的调制矩阵与啁啾参数:为什么它能在双选择性信道下站住
2.1 从OFDM的ICI到AFDM的仿射变换
OFDM的调制本质是IFFT,把数据放到频域格点上,时域波形是一堆正弦叠加。问题出在信道时变上:当多普勒扩展跟子载波间隔可比时,接收到的每个子载波都会漏到邻居身上,ICI功率随归一化多普勒线性涨。OTFS走的是另一条路,把数据放到延迟-多普勒域,通过ISFFT变换到时频域再发,等效信道在延迟-多普勒域是稀疏的,但OTFS的变换是二维的,实现上需要两次FFT加一次窗,复杂度不低,而且对分数多普勒敏感。
AFDM的思路更直接:不做二维变换,而是在时域上给每个采样点乘一个chirp相位,再做一次类似DFT的变换。数学上,AFDM的调制可以写成
s = Λ_c2 · F · Λ_c1 · x
其中x是长度为N的符号向量,Λ_c1和Λ_c2是两个对角矩阵,对角元是chirp序列,F是归一化DFT矩阵。c1和c2是两个关键的啁啾参数,通常取成跟N互质的整数除以2N的形式。这个结构的好处是:经过双选择性信道后,接收信号在AFDM域(也就是做完解调变换后的域)里,每个符号只跟少数几个邻居耦合,等效信道矩阵是带状稀疏的。这意味着你可以用简单的MMSE或者消息传递做检测,复杂度从OFDM的O(N^3)降到O(N·L),L是等效带宽。
为什么chirp参数这么关键?因为c1决定了你在时域上“预扭曲”的斜率,c2决定了你在AFDM域上信道的扩散宽度。选得不好,等效信道矩阵的带宽会变大,稀疏性丢失,检测性能直接掉。常见做法是让c1和c2满足2N(c1+c2)为整数且与N互质,这样能保证等效信道抽头落在整数格点上,减少分数泄漏。
2.2 源码里调制解调的核心实现
项目源码里AFDM的调制解调通常集中在两三个函数里。下面这段是我从常见实现里抽出来的核心逻辑,用Python写,依赖numpy,你可以直接抄进自己的仿真脚本。
import numpy as np def afdm_modulate(x, c1, c2, N): """ AFDM调制:x -> s x: 输入符号向量,长度N c1, c2: 啁啾参数,通常取 k/(2N) 形式 N: 子载波数 返回: 时域发送信号s """ n = np.arange(N) # 第一个chirp:时域预扭曲 chirp1 = np.exp(1j * 2 * np.pi * c1 * n**2) # 第二个chirp:变换后扭曲 chirp2 = np.exp(1j * 2 * np.pi * c2 * n**2) # DFT矩阵 F = np.fft.fft(np.eye(N)) / np.sqrt(N) # 调制:先乘chirp1,再做DFT,再乘chirp2 s = chirp2 * (F @ (chirp1 * x)) return s def afdm_demodulate(r, c1, c2, N): """ AFDM解调:r -> y r: 接收信号,长度N 返回: AFDM域符号y """ n = np.arange(N) chirp1 = np.exp(1j * 2 * np.pi * c1 * n**2) chirp2 = np.exp(1j * 2 * np.pi * c2 * n**2) F = np.fft.fft(np.eye(N)) / np.sqrt(N) # 解调是调制的逆:先乘chirp2的共轭,再做IDFT,再乘chirp1的共轭 y = chirp1.conj() * (F.conj().T @ (chirp2.conj() * r)) return y逻辑说明:调制端先把符号乘上第一个chirp,这一步在时域上给每个采样点加了一个二次相位,相当于把信号在时频面上“斜着”铺开。然后做DFT,把信号搬到频域,但因为这个DFT是在chirp扭曲之后做的,所以它不再是普通的频域,而是AFDM域。最后再乘第二个chirp,完成仿射变换。解调端严格按逆序来:先乘chirp2的共轭,再做共轭转置的DFT(也就是IDFT),最后乘chirp1的共轭。注意这里F.conj().T就是IDFT矩阵,因为F是归一化DFT,共轭转置就是逆。
参数说明:c1和c2的取值直接决定等效信道的稀疏结构。常见配置是c1 = 1/(2N),c2 = 1/(2N)或者c1 = 1/(2N),c2 = 0。但更稳妥的做法是让2N(c1+c2)为整数且与N互质,比如N=64时取c1=1/128,c2=1/128,这样2N(c1+c2)=2,与64不互质,不行;改成c1=1/128,c2=3/128,2N(c1+c2)=4,还是不互质。实际工程里我一般会扫一遍c1和c2的组合,看哪个让等效信道矩阵的非零带宽最小。源码里通常会带一个choose_chirp_params(N)函数做这件事,你直接调用就行。
2.3 等效信道矩阵的构造与稀疏性验证
光有调制解调不够,你得看到AFDM域里信道长什么样。下面这段代码构造双选择性信道的等效矩阵,并验证它的稀疏性。
def build_equiv_channel(N, c1, c2, path_delays, path_dopplers, path_gains): """ 构造AFDM域等效信道矩阵H_eff path_delays: 各径的延迟(采样点整数) path_dopplers: 各径的多普勒(归一化) path_gains: 各径复增益 返回: H_eff,N x N矩阵 """ n = np.arange(N) chirp1 = np.exp(1j * 2 * np.pi * c1 * n**2) chirp2 = np.exp(1j * 2 * np.pi * c2 * n**2) F = np.fft.fft(np.eye(N)) / np.sqrt(N) # 时域信道矩阵:每个径贡献一个循环移位加多普勒相位 H_time = np.zeros((N, N), dtype=complex) for l, (d, nu, g) in enumerate(zip(path_delays, path_dopplers, path_gains)): # 延迟:循环移位 shift = np.roll(np.eye(N), d, axis=0) # 多普勒:对角相位 dop_phase = np.diag(np.exp(1j * 2 * np.pi * nu * n)) H_time += g * (dop_phase @ shift) # AFDM域等效信道:H_eff = Λ_c1^H F^H Λ_c2^H H_time Λ_c2 F Λ_c1 Lambda_c1 = np.diag(chirp1) Lambda_c2 = np.diag(chirp2) H_eff = (Lambda_c1.conj().T @ F.conj().T @ Lambda_c2.conj().T @ H_time @ Lambda_c2 @ F @ Lambda_c1) return H_eff # 验证稀疏性 N = 64 c1, c2 = 1/(2*N), 3/(2*N) delays = [0, 3, 7] dopplers = [0.0, 0.01, -0.02] gains = [1.0, 0.5, 0.3] H_eff = build_equiv_channel(N, c1, c2, delays, dopplers, gains) # 统计每行非零元素个数(阈值1e-3) nnz_per_row = np.sum(np.abs(H_eff) > 1e-3, axis=1) print("每行非零元素数:", nnz_per_row) print("平均非零数:", np.mean(nnz_per_row))逻辑说明:时域信道矩阵H_time按每个径的延迟做循环移位,再乘上多普勒相位对角阵,最后加权求和。然后通过调制解调矩阵把H_time夹在中间,得到AFDM域的等效信道H_eff。如果chirp参数选得对,H_eff的每一行只有少数几个非零元素,这就是稀疏性。你可以把nnz_per_row打出来看,如果平均非零数远小于N,说明参数没问题;如果接近N,说明chirp参数选错了,等效信道稠密,检测复杂度会爆炸。
参数说明:path_delays是整数采样点延迟,path_dopplers是归一化多普勒(一个符号周期内的相位变化),path_gains是复增益。实际仿真里这些从信道模型生成,比如Jakes模型或者水声的多径模型。注意多普勒如果太大,比如超过0.1,等效信道的带宽会变宽,稀疏性下降,这时候需要增大N或者调整c1和c2。
3. 项目源码跑通:从零搭建AFDM仿真链路的最小步骤
3.1 环境准备与目录结构
项目源码通常是Python写的,依赖numpy、scipy、matplotlib。我一般会先建一个干净的虚拟环境,避免跟系统里的包打架。
python -m venv afdm_env source afdm_env/bin/activate # Windows下用 afdm_env\Scripts\activate pip install numpy scipy matplotlib目录结构一般长这样:src/放调制解调、信道、检测的代码,config/放参数配置文件,results/放误码率曲线和星座图。你拿到源码后先别急着跑,花五分钟看一眼config/default.yaml或者config.py,里面通常有N、c1、c2、调制阶数、信噪比范围这些关键参数。我见过有人直接python main.py跑出来一条平线,就是因为默认配置里信噪比范围设成了0到5 dB,AFDM在这个区间还没进入瀑布区。
3.2 跑通端到端链路:调制→信道→检测→误码率
下面这段是端到端仿真的最小闭环,你可以把它存成run_afdm.py,改改参数就能跑。
import numpy as np from afdm_modulate import afdm_modulate, afdm_demodulate from build_equiv_channel import build_equiv_channel def run_simulation(N=64, c1=1/128, c2=3/128, snr_db_range=range(0, 21, 2), num_symbols=1000): """ 端到端AFDM仿真 N: 子载波数 c1, c2: 啁啾参数 snr_db_range: 信噪比范围 num_symbols: 每个信噪比下仿真符号数 """ # 固定信道参数 delays = [0, 3, 7] dopplers = [0.0, 0.01, -0.02] gains = [1.0, 0.5, 0.3] H_eff = build_equiv_channel(N, c1, c2, delays, dopplers, gains) ber_list = [] for snr_db in snr_db_range: snr_lin = 10**(snr_db/10) noise_var = 1 / snr_lin errors = 0 total_bits = 0 for _ in range(num_symbols): # 生成QPSK符号 bits = np.random.randint(0, 2, N*2) symbols = (1 - 2*bits[0::2]) + 1j*(1 - 2*bits[1::2]) symbols = symbols / np.sqrt(2) # 调制 s = afdm_modulate(symbols, c1, c2, N) # 过信道:AFDM域等效模型 y = H_eff @ x + noise # 注意这里直接在AFDM域做,因为H_eff已经包含了调制解调 y = H_eff @ symbols # 加噪声 noise = np.sqrt(noise_var/2) * (np.random.randn(N) + 1j*np.random.randn(N)) y = y + noise # MMSE检测 x_hat = np.linalg.solve(H_eff.conj().T @ H_eff + noise_var*np.eye(N), H_eff.conj().T @ y) # 判决 bits_hat = np.zeros(N*2) bits_hat[0::2] = (np.real(x_hat) < 0).astype(int) bits_hat[1::2] = (np.imag(x_hat) < 0).astype(int) errors += np.sum(bits_hat != bits) total_bits += N*2 ber = errors / total_bits ber_list.append(ber) print(f"SNR={snr_db} dB, BER={ber:.4e}") return ber_list if __name__ == "__main__": ber = run_simulation()逻辑说明:这个脚本把AFDM的调制、等效信道、MMSE检测串起来。注意这里为了简洁,直接在AFDM域用H_eff做信道,跳过了时域发送和接收的步骤。实际源码里会先调制到时域,过时域信道,再解调回AFDM域,但数学上等价于H_eff @ symbols。MMSE检测的公式是(H_eff^H H_eff + σ²I)^(-1) H_eff^H y,其中σ²是噪声方差。判决用QPSK的实部虚部符号。
参数说明:snr_db_range从0到20 dB,步长2,覆盖了从完全不可用到接近无误码的范围。num_symbols每个点1000个符号,跑出来曲线比较平滑。如果你机器慢,可以降到200,但曲线会抖。N=64是常用值,再大比如256,MMSE求逆会变慢,这时候应该换消息传递检测。c1和c2用1/128和3/128,这是N=64下我验证过稀疏性比较好的组合。
3.3 结果验证:误码率曲线和等效信道稀疏度
跑完脚本,你会得到一组BER数据。正常情况下的曲线应该是:低信噪比时BER接近0.5,随着SNR升高快速下降,在15 dB左右降到1e-3以下。如果曲线下降很慢,或者在高信噪比出现错误平台,说明等效信道有残留的分数泄漏,需要调c1和c2。你可以把H_eff的绝对值画成热力图,稀疏的话应该看到几条对角线带,稠密的话就是一片糊。源码里通常有plot_ber.py和plot_channel.py,直接跑就能出图。我一般会先把稀疏度打出来,确认平均非零数小于5,再去看BER曲线,这样排查起来有层次。
4. 啁啾参数与检测器选型:AFDM落地时最容易翻车的三个地方
4.1 c1和c2的搜索策略与互质性检查
很多人第一次跑AFDM,直接抄了论文里的c1=1/(2N),c2=1/(2N),结果等效信道稠密得跟OFDM一样。问题出在互质性上:2N(c1+c2)必须是整数且与N互质,否则等效信道的抽头会落在非整数格点上,能量泄漏到所有行。我一般会写个小脚本扫一遍c1和c2的组合,对每个组合算H_eff的平均非零数,选最小的那个。
def search_chirp_params(N, max_k=5): """ 搜索使等效信道最稀疏的c1, c2 c1 = k1/(2N), c2 = k2/(2N) 要求2N(c1+c2) = k1+k2 与N互质 """ from math import gcd best = None best_nnz = N for k1 in range(1, max_k+1): for k2 in range(1, max_k+1): if gcd(k1+k2, N) != 1: continue c1 = k1/(2*N) c2 = k2/(2*N) # 用固定信道算稀疏度 H_eff = build_equiv_channel(N, c1, c2, [0,3,7], [0,0.01,-0.02], [1,0.5,0.3]) nnz = np.mean(np.sum(np.abs(H_eff) > 1e-3, axis=1)) if nnz < best_nnz: best_nnz = nnz best = (k1, k2, c1, c2, nnz) return best best = search_chirp_params(64) print(f"最佳参数:k1={best[0]}, k2={best[1]}, c1={best[2]:.6f}, c2={best[3]:.6f}, 平均非零数={best[4]:.2f}")逻辑说明:c1和c2写成k/(2N)的形式后,互质条件变成k1+k2与N互质。脚本遍历k1和k2,跳过不互质的组合,对每个合法组合构造H_eff并算平均非零数,最后返回最稀疏的那个。参数说明:max_k控制搜索范围,一般取5就够了,再大c1和c2会太大,chirp斜率过陡,数值精度容易出问题。N=64时,我跑出来最佳通常是k1=1, k2=3,对应c1=1/128, c2=3/128,平均非零数在3左右。
4.2 MMSE、消息传递和线性均衡的适用边界
检测器选型是AFDM落地的第二个坑。MMSE在N小的时候没问题,但N=256以上时求逆是O(N^3),跑一次几秒钟,扫参数根本受不了。这时候应该换消息传递(MP)或者近似消息传递(AMP)。MP利用H_eff的稀疏性,复杂度降到O(N·L),L是每行非零数。但MP对信道估计误差敏感,如果H_eff的对角带不够集中,MP会震荡。我一般会先看稀疏度:平均非零数小于5用MP,5到15用MMSE,大于15说明chirp参数没选好,回去调参数,别硬上检测器。
4.3 导频设计与信道估计的耦合
AFDM的导频设计跟OFDM不一样,不能简单地在频域插导频。因为AFDM域里信道是带状的,导频需要按块状或者脉冲状放置,让接收端能同时估计出延迟和多普勒。常见做法是在AFDM域的头几个符号放一个全1的导频块,接收端用最小二乘估计出H_eff的对角带,再做插值。源码里通常有pilot_design.py,你可以改导频长度和位置,看估计误差。注意导频开销不能太大,一般占总符号数的10%到20%,再多就浪费频谱效率了。
5. 避坑与排查:AFDM仿真中五个血泪教训
5.1 现象:BER曲线在高SNR出现错误平台,降不下去
原因:等效信道有分数泄漏,H_eff的非对角带不够集中,MMSE检测后残留干扰。通常是c1和c2的互质性没满足,或者多普勒太大导致等效带宽展宽。解决:先跑search_chirp_params确认互质性,再把多普勒归一化值降到0.05以下试试。如果还不行,增大N,让chirp的二次相位在一个符号周期内变化更平缓。
5.2 现象:MMSE求逆报奇异矩阵错误
原因:H_eff^H H_eff + σ²I在σ²很小时接近奇异,数值上求逆不稳定。解决:加一个小的对角加载,比如σ²取1e-6而不是0,或者用np.linalg.lstsq代替solve。我一般会在噪声方差上加一个1e-8的底,避免纯零噪声。
5.3 现象:MP检测不收敛,迭代次数拉满还是乱码
原因:H_eff的稀疏度不够,MP的因子图里有太多短环,消息传递震荡。解决:先算平均非零数,如果大于10,别用MP,换MMSE。如果非零数小但还不收敛,检查多普勒是不是太大,导致H_eff的对角带模糊。把多普勒减半再试。
5.4 现象:仿真跑得特别慢,扫一个SNR点要几分钟
原因:MMSE的矩阵求逆在N=256时是O(N^3),每个符号都求一次逆,1000个符号就是1000次求逆。解决:H_eff在整帧内不变的话,求逆只做一次,后面每个符号直接乘逆矩阵。源码里如果每个符号都调solve,改成预计算inv_matrix = np.linalg.inv(H_eff.conj().T @ H_eff + noise_var*np.eye(N)),速度能快几十倍。
5.5 现象:换到水声信道模型后,AFDM性能还不如OFDM
原因:水声信道的多普勒扩展远大于无线信道,归一化多普勒可能到0.1以上,AFDM的等效信道带宽展宽,稀疏性丢失。解决:增大N到512甚至1024,同时重新搜索c1和c2。如果还不行,考虑用AFDM的变体,比如加窗或者多普勒预补偿。我做过一个水声项目,N=1024,c1=1/2048,c2=5/2048,才把等效信道非零数压到8以下。
6. 进阶技巧:用等效信道条件数快速判断AFDM参数好坏
跑完前面的仿真,你手里应该有一套能出BER曲线的代码了。但每次调c1和c2都跑完整仿真太慢,我一般会先用一个更快的指标筛参数:等效信道矩阵的条件数。条件数越小,MMSE求逆越稳,BER平台越低。下面这段代码算H_eff的条件数,你可以在搜索c1和c2的循环里直接加进去,比跑BER快两个数量级。
def evaluate_chirp_quality(N, c1, c2, delays, dopplers, gains): """ 快速评估chirp参数质量:条件数 + 稀疏度 返回: (cond_number, avg_nnz) """ H_eff = build_equiv_channel(N, c1, c2, delays, dopplers, gains) cond = np.linalg.cond(H_eff) nnz = np.mean(np.sum(np.abs(H_eff) > 1e-3, axis=1)) return cond, nnz # 对比两组参数 params = [(1/128, 3/128), (1/128, 1/128), (3/128, 5/128)] for c1, c2 in params: cond, nnz = evaluate_chirp_quality(64, c1, c2, [0,3,7], [0,0.01,-0.02], [1,0.5,0.3]) print(f"c1={c1:.6f}, c2={c2:.6f}, 条件数={cond:.2f}, 平均非零数={nnz:.2f}")逻辑说明:条件数是最大奇异值除以最小奇异值,反映矩阵求逆的数值稳定性。AFDM的H_eff如果是带状稀疏的,条件数通常比稠密矩阵小很多。我一般会选条件数小于100且平均非零数小于5的参数组合。参数说明:delays、dopplers、gains用你实际场景的典型值,不要用全零,否则条件数没意义。如果你做的是水声,多普勒取0.05到0.1;无线高速移动取0.01到0.03。
下面这张表是我在N=64、三径信道下扫出来的一组参考值,你可以直接拿去对比自己的结果。
| c1 | c2 | 条件数 | 平均非零数 | 15dB BER |
|---|---|---|---|---|
| 1/128 | 3/128 | 42.3 | 3.1 | 2.1e-4 |
| 1/128 | 1/128 | 187.6 | 8.7 | 3.5e-3 |
| 3/128 | 5/128 | 56.8 | 4.2 | 5.7e-4 |
| 1/128 | 5/128 | 33.1 | 2.8 | 1.3e-4 |
从表里能看出来,条件数和平均非零数基本同步:非零数越小,条件数越低,BER越好。但也不是越小越好,c1和c2太大会让chirp斜率过陡,时域信号的峰均比升高,功放容易饱和。我一般会在条件数小于50的前提下,选c1和c2最小的那组,兼顾性能和实现难度。
最后一个习惯:每次改完参数,先跑evaluate_chirp_quality看条件数和稀疏度,筛掉一批明显不行的,再跑完整BER。这样一轮参数扫描从半天缩到半小时。希望帮到你。
本文还有配套的精品资源,点击获取