
简介OTFS正交时频空间调制是面向高速移动场景的先进传输方案这份MATLAB源码包正适合通信方向研究者、研究生或工程师用于理解OTFS与无细胞Cell-free网络的联合仿真实现。包内共5个文件以3个可直接运行的.m仿真脚本为主辅以README说明文档和许可证文件整个压缩包仅15KB轻量但覆盖完整工程流程。源码通过建模多径衰落信道、OTFS调制/解调、多普勒频移、AWGN噪声与检测算法可实现误码率等性能评估其中还涉及分布式接入点协作、资源分配等Cell-free网络关键机制适合作为入门学习和二次开发基础。截至目前已有970人学习下载对希望快速搭建OTFS仿真环境、理解时延-多普勒域信号处理、对比OFDM性能或扩展至无细胞网络的读者而言是一份高价值参考资料。1. OTFS 仿真为什么值得跑先看它在高速移动场景下的表现拿到一份 OTFS 的仿真 matlab 源码很多人的第一反应是拿它替换 OFDM 的收发链路然后对比 BER。真正跑过之后你会发现一个反直觉的结果在高铁、低轨卫星、车联网这类多普勒频移严重的场景里OTFS 在时延-多普勒域做调制性能不是随移动速度下降而是明显优于 OFDM。OFDM 靠子载波正交性抗干扰多普勒一上来ICI 就把星座点搅成一团OTFS 把整个数据块铺在时延-多普勒网格上让每个符号经历几乎相同的信道响应接收端再做二维均衡。这份工程的价值在于给你一套能直接改参数、能对比基线的实验台特别适合做 5G-Advanced / 6G 物理层预研、写论文基线和评估 OTFS 相对 OFDM 增益的从业者。2. 搭建 OTFS 仿真骨架用 MATLAB 类结构把参数和模块分开管理做 OTFS 仿真第一个坑不是算法而是代码结构。很多人把参数、发射机、信道、接收机全写在一个脚本里调多普勒频率时要翻到几十行去改一个常量改完还容易忘记同步更新 CP 长度。常见做法是用一个 settings 结构体集中管理参数用函数或类封装收发模块主脚本只做流程编排和结果绘图。2.1 参数结构体的定义与作用我一般会先建一个otfs_params.m函数返回一个包含全部仿真参数的结构体。这样做的好处是跑参数扫描时可以直接循环修改结构体字段不用复制整个工程。function p otfs_params() % 基础通信参数 p.fc 28e9; % 载波频率, 28GHz 毫米波场景 p.B 20e6; % 系统带宽 20MHz p.M 128; % 延迟维子载波数/每符号采样点数 p.N 32; % 多普勒维符号数 p.df p.B / p.M; % 子载波间隔, 156.25kHz p.T 1 / p.df; % 单符号持续时间 p.cpLen 16; % CP 采样点数 p.modOrder 4; % 4QAM, 可改 16/64QAM p.numPaths 6; % 多径抽头数 p.delayTaps [0 2 5 9 14 20]; % 归一化采样时延 p.dopplerHz [0 300 620 980 1300 2100]; % 各径多普勒频移 p.snrDbList 0:5:30; % 仿真 SNR 范围 p.numFrames 20; % 每 SNR 下的帧数 end这段代码的关键在于定义了两个域之间的分辨率关系。M决定时延分辨率1 / (N * df)是多普勒分辨率理论上能分辨的最大多普勒是N * df / 2。比如df 156.25kHz、N 32时多普勒分辨率约 4.88kHz最大可表示多普勒约 78kHz这对应 28GHz 下载频下约 3000km/h 的移动速度覆盖绝大多数场景。仿真时建议让最大多普勒不超过理论值的一半否则频谱混叠会让 BER 曲线出现地板。2.2 网格坐标系定义OTFS 里最容易搞混的就是矩阵的行列方向。标准约定是延迟多普勒域矩阵X_dd是M x N行方向是延迟对应子载波索引列方向是多普勒对应符号索引。时频域矩阵X_tf也是M x N行方向是子载波列方向是符号。% 初始化时延-多普勒域数据矩阵 X_dd zeros(p.M, p.N); % 初始化时频域数据矩阵 X_tf zeros(p.M, p.N); % 索引说明: % X_dd(k, l): k 是时延索引 [0, M-1], l 是多普勒索引 [0, N-1] % X_tf(m, n): m 是子载波索引 [0, M-1], n 是符号索引 [0, N-1]MATLAB 按列存储如果文献里把多普勒放行方向、时延放列方向你直接复制公式就会出现转置错误。建议在任何变换函数的第一行加个维度断言assert(isequal(size(X_dd), [p.M, p.N]), DD域矩阵维度必须为 M x N);2.3 双正交条件与窗函数OTFS 的理论框架建立在 Gabor 变换上发送窗和接收窗要满足双正交条件。仿真里常见做法是先忽略窗函数设计把发送窗和接收窗都置为全 1 矩阵这样收发端就是一组互逆的辛有限傅里叶变换。参数典型取值影响M64 / 128 / 256时延分辨率越高能分辨的多径越细N16 / 32 / 64多普勒分辨率越高但帧长随之增加cpLen16 / 32 / 64必须大于最大时延采样数否则有 ISI窗函数全1 / 汉明窗改善频谱泄漏但会降低有效 SNR默认先全1窗函数对 BER 的影响在接收端往往比发射端更明显。发射端加窗会破坏正交性接收端加窗可以抑制多普勒旁瓣。先用全 1 窗跑通基线再考虑加窗这是最稳的路径。3. 发射端实现QAM 调制、ISFFT 与海森堡变换的完整代码发射端看起来只有三步QAM 符号映射、ISFFT 变换到时时频域、海森堡变换生成波形。但每一步的维度和归一化都容易出错下面给出一个自包含的发射函数。3.1 QAM 符号映射与串并转换输入是二进制比特流按调制阶数分组映射成 QAM 符号。4QAM 时每 2 bit 映射一个符号16QAM 每 4 bit 映射一个符号。映射后把一维符号向量填充到 M x N 的矩阵里。function X_dd map_bits_to_dd(bits, M, N, modOrder) k log2(modOrder); numBits M * N * k; assert(length(bits) numBits, 比特数不够一帧); bits bits(1:numBits); symbols qammod(bits, modOrder, gray, InputType, bit, UnitAveragePower, true); % 按列填充: 先时延方向后多普勒方向 X_dd reshape(symbols, M, N); endqammod里UnitAveragePower设为 true 很关键它保证各阶调制平均功率为 1这样后续加噪声时可以按 SNR 直接计算噪声功率不用额外做功率归一化。如果用默认的UnitAveragePowerfalse4QAM 星座点功率是 1 或 516QAM 星座功率差异更大噪声功率算出来会和实际差好几 dB。3.2 ISFFT从延迟多普勒域到时频域ISFFT 是 OTFS 的核心公式上就是沿延迟维做 DFT、沿多普勒维做 IDFT。为了不踩归一化的坑建议用三种等效写法里最直白的一种。function X_tf isfft(X_dd, M, N) % ISFFT: 延迟多普勒域 - 时频域 % 第1步: 沿维度1(延迟)做 FFT, 并除以 sqrt(M) 做正交归一化 Y fft(X_dd, M, 1) / sqrt(M); % 第2步: 沿维度2(多普勒)做 IFFT, matlab自带 1/N 因子 % 乘 sqrt(N) 得到 1/sqrt(N) 的正交归一化IDFT X_tf ifft(Y, N, 2) * sqrt(N); % 若想提高可读性, 也可以用 dftmtx 构造矩阵: % FM dftmtx(M) / sqrt(M); FN dftmtx(N) / sqrt(N); % X_tf FM * X_dd * FN; end这种写法的归一化结果是1/sqrt(M*N)和教科书公式一致。很多源码会把归一化因子直接吃掉接收端再加一个整体增益补偿这样在单径 AWGN 信道下 BER 曲线也能对但换到多径信道后功率分配会错导致 MMSE 或 MP 检测器里的噪声方差估计失效。所以收发两端都做对称归一化是最稳妥的工程做法。3.3 海森堡变换与发射波形生成海森堡变换在理想脉冲成形下就是每个时频域符号列做一次 M 点 IDFT等价于 OFDM 的 modulator。function txSig heisenberg_transform(X_tf, M, N, cpLen) % 对时频域矩阵每一列做 IFFT S ifft(X_tf, M, 1); % 加循环前缀: 取每列最后 cpLen 个样本拼到前面 S_cp [S(end-cpLen1:end, :); S]; % 按列展开成一维时域信号, 先发第一个符号 txSig reshape(S_cp, [], 1); endifft自带1/M归一化这会让发射信号幅度变小但接收端对应做 FFT 后幅度又会恢复幅度缩放本身不会破坏信噪比关系。需要留意的是发送波形总能量约等于M * N * mean(abs(X_tf(:)).^2)在加噪声时建议先对txSig做一次整体能量归一化保证不同调制阶数和帧长下 SNR 定义一致txSig txSig / sqrt(mean(abs(txSig).^2));实际系统中海森堡变换还包含发送脉冲成形和重叠相加但仿真基线里用矩形脉冲就够。如果你要评估带外泄露或峰均比再引入 RRC 脉冲那时每个符号之间会有重叠收发链路会复杂一个量级。4. 信道建模与接收端时变信道、维格纳变换和检测器怎么选OTFS 的收益来自信道在延迟多普勒域的稀疏表示。信道建模不能只放一个静态多径必须把每条径的多普勒频移乘到时域相位上。接收端先做维格纳变换回到时频域再做 SFFT 回到延迟多普勒域最后交给检测器。4.1 时变多径信道模型基带时变信道写作h(t, tau) sum_p h_p * exp(j*2*pi*nu_p*t) * delta(tau - tau_p)。仿真时把每条径的时延换算成采样点把多普勒频移换成逐样本相位旋转。function rxSig apply_otfs_channel(txSig, p) L length(txSig); offset p.cpLen; % 让信道从数据起始时刻开始 t (0:L-1) / (p.M * p.df); % 采样时间轴 P p.numPaths; rxSig zeros(L, 1); for idx 1:P delay p.delayTaps(idx) 1; % 1-based phase exp(1j * 2 * pi * p.dopplerHz(idx) * t); amp 1 / sqrt(P) * exp(1j * rand * 2 * pi); % 卷积加时延: 用 filter 实现多径叠加 h_p amp .* phase; % 将时延转化为移位 if delay L rxSig rxSig filter(h_p, 1, [zeros(delay,1); txSig(1:end-delay)]); end end % 加噪由外层统一处理 endfilter(h_p, 1, ...)本质是逐样本卷积慢一点但直观。性能优化时可以把每条径的信号先按抽头延迟做循环移位再逐样本乘相位最后累加。注意多普勒相位exp(j*2*pi*nu*t)的t是绝对时间不是相对符号时间这是新手最容易漏的点——漏掉后多普勒抽头不会在延迟多普勒域形成偏移BER 曲线会虚假地好。4.2 接收端维格纳变换与 SFFT接收端先把时域信号重新组织成 M x N 矩阵去 CP 后按列做 FFT得到时频域接收值再做 SFFT 回到延迟多普勒域。function Y_dd otfs_receiver(rxSig, M, N, cpLen, noiseVar) % 去 CP 并按符号重排 S_rx reshape(rxSig, M cpLen, N); S_rx S_rx(cpLen1:end, :); % 维格纳变换: 沿列做 FFT 得到时频域 Y_tf fft(S_rx, M, 1); % SFFT: 时频域 - 延迟多普勒域 % 与 ISFFT 顺序相反: 先多普勒维 FFT, 再延迟维 IFFT Y1 fft(Y_tf, N, 2) / sqrt(N); Y_dd ifft(Y1, M, 1) * sqrt(M); end这里 SFFT 的归一化和发射端 ISFFT 刚好对称合起来是恒等变换。如果收发不对称单径信道下星座点就会有整体缩放解调时要么加 AGC 要么重新估计噪声方差。在调试阶段可以在 AWGN 信道下比较Y_dd和发射前的X_dd如果星座点错位且功率不一致先检查这一步。4.3 检测器选择MF、MMSE 与 MP延迟多普勒域的等效信道矩阵维度是(M*N) x (M*N)直接做 MMSE 需要求逆O(M^3 * N^3)在 128x32 网格下矩阵是 4096 阶MATLAB 单帧还行跑蒙特卡洛会很慢。三种检测器的取舍如下检测器复杂度适用场景注意事项MF 匹配滤波低单径或弱多普勒多径下性能差只适合调试MMSE中高网格尺寸适中需要估计噪声方差和信道矩阵MP 消息传递中默认选择收敛需要 5-10 次迭代MP 检测的核心是交替更新符号的均值和方差。下面给一个核心迭代片段完整实现需要配合因子图做消息聚合。% 迭代核心: y 是接收DD域符号向量, H 是等效信道矩阵 % 简化起见假设 H 已显式构造或提供抽头索引 mu zeros(M*N, 1); % 符号均值初始化 sigma2 ones(M*N, 1); % 方差初始化 for iter 1:5 for k 1:M*N % 计算外部信息: 去掉自身贡献 y_ext y(k) - H(k,:) * (mu .* (1 - eye(M*N) ... eye(M*N) .* 0)); % 工程上改为索引跳过 k % 更新均值方差(简化示意, 实际用精确消息) mu(k) tanh(2 * real(y_ext) / noiseVar); sigma2(k) 1 - mu(k)^2; end end这段代码只能当骨架看直接跑会有索引错误。我建议先实现 MF 验证链路正确性再按论文补 MP 的联合稀疏消息更新。MP 对噪声方差估计很敏感噪声方差低估会让迭代不收敛甚至发散。5. OTFS 仿真避坑清单常见的 5 个翻车现场与排查方法OTFS 仿真跑出负 SNR 增益或星座图转置大多不是算法问题而是实现细节。下面几条是我在调这类代码时反复踩过的坑。5.1 星座图整体旋转 90 度或上下翻转现象AWGN 信道下 BER 接近 0.5星座点位置错乱。原因ISFFT/SFFT 中 FFT 与 IFFT 的方向或顺序用反了。例如延迟维该做 FFT 却做了 IFFT等效于频域反转。另一类是归一化因子不对称导致 16QAM 星座幅度错位。解决先用随机 QAM 符号构造X_dd过一遍isfft再过一遍 SFFT对比输出和输入。脚本里加一行断言max(abs(X_dd - Y_dd)) 1e-9作为每次仿真的前置检查。5.2 高 SNR 下 BER 出现平台现象BER 曲线在 20dB 之后不再下降平台高度约 0.01。原因CP 长度小于信道最大时延符号间干扰成为底噪或者分数多普勒被直接取整丢失了多普勒扩展的低频成分。解决先查最大时延采样数是否小于cpLen。如果时延抽头[0 2 5 9 14 20]中最大值 20 加信道尾响应很容易超过 CP 16。把cpLen改成 32 再跑。若仍有平台检查多普勒抽头是否有非整数改用时域建模仿真。5.3 增加 N 后性能反而变差现象N16时 BER 正常改成N64后曲线恶化 2-3dB。原因N 增大提高了多普勒分辨率原本分数多普勒的频谱泄漏现在落在更多相邻 bin 上接收端没有相应的精细信道估计等效引入了更强的 ICI。解决增大 N 时同步换用 MP 检测器而不是 MF且每帧重新估计信道抽头位置。理想做法是每帧插导频用 CIR 估计更新抽头幅度和相位后再送入检测器。5.4 矩阵维度或方向在收发端不一致现象仿真结果每次运行都有细微差别但 BER 始终很高检查X_dd和Y_dd尺寸相同但 reshape 后符号顺序错位。原因MATLAB 按列优先 reshape而文献里的延迟多普勒域矩阵常按行优先书写。发射端按行填充、接收端按列展开数据就转置了。解决所有模块统一用reshape(symbols, M, N)填充接收端解析比特时用相同维度并在发射函数入口断言维度。5.5 检测器里的噪声方差估计错误现象MMSE 检测的 BER 比 MF 还差星座缩放正确但判决软信息紊乱。原因ISFFT/SFFT 的归一化因子改变了噪声功率但代码仍用10^(-snr/10)作为噪声方差。经过sqrt(M*N)缩放后实际噪声方差差了 M*N 倍。解决在加噪前先统计接收信号能量或直接把噪声加在时域波形上让后续所有变换自然携带等功率噪声。推荐后者更贴近真实接收机。6. 验证仿真正确性BER 曲线、CIR 提取与参数灵敏度检查跑通收发链路后不要急着贴曲线图。先验证三件事单径 AWGN 下 BER 是否逼近理论值、时变信道下 CIR 抽头是否落在预期坐标、参数扫描结果是否符合理论趋势。6.1 信道脉冲响应提取在 DD 域每帧放一个导频符号接收端在导频位置附近搜索峰值即可看到多普勒抽头的实际位置。这个检查比 BER 更早暴露建模问题。% 发射端在 X_dd(1,1) 放导频, 幅值为 sqrt(M*N) % 接收端取 Y_dd 幅度平方 [peakVal, peakIdx] max(abs(Y_dd(:))); [peakK, peakL] ind2sub([M, N], peakIdx); fprintf(峰值位于 延迟%d, 多普勒%d\n, peakK-1, peakL-1); % 预期第一条径出现在 (0, 0), 第二条在 (delayTaps(2), round(N*nu(2)/(M*df)))如果第二条径的多普勒索引算出来是 3.2峰值却出现在 3 和 4 都有能量说明分数多普勒的频谱泄漏正常如果峰值成对出现且幅度相当说明信道建模有周期性重复——通常是因为时延抽头在 reshape 时被循环移位了。6.2 OFDM 基线对比与参数灵敏度OTFS 仿真必须有 OFDM 基线才有说服力。OFDM 基线建议用同一套参数但去掉 ISFFT/SFFT 步骤直接在时频域调制解调。对比结果时关注两个边界低速场景下 OTFS 增益应在 0.5dB 以内高速场景下应有明显优势若低速下 OTFS 反而差很多多半是检测器或信道估计的差异不是调制本身的损失。参数灵敏度检查可以把多普勒频率乘以 2、CP 长度减半、N 从 32 改成 16分别记录 BER 在 15dB 处的变化。我的一般习惯是先跑 OFDM 基线做背靠背对比再逐项单独改参数确保每次只动一个变量否则曲线你根本不知道是哪一步贡献的。OTFS 仿真最容易自欺欺人的地方就是信道模型偏乐观或者 OFDM 基线没有做同样水平的信道估计。保持两边检测器复杂度一致对比才有意义。希望这些思路帮你在自己的仿真工程里少走几趟弯路。本文还有配套的精品资源点击获取