简介: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 矩阵,这样收发端就是一组互逆的辛有限傅里叶变换。
| 参数 | 典型取值 | 影响 |
|---|---|---|
| M | 64 / 128 / 256 | 时延分辨率越高,能分辨的多径越细 |
| N | 16 / 32 / 64 | 多普勒分辨率越高,但帧长随之增加 |
| cpLen | 16 / 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 直接计算噪声功率,不用额外做功率归一化。如果用默认的UnitAveragePower=false,4QAM 星座点功率是 1 或 5,16QAM 星座功率差异更大,噪声功率算出来会和实际差好几 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-cpLen+1: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(cpLen+1: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 后性能反而变差
现象:N=16时 BER 正常,改成N=64后曲线恶化 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 基线没有做同样水平的信道估计。保持两边检测器复杂度一致,对比才有意义。希望这些思路帮你在自己的仿真工程里少走几趟弯路。
本文还有配套的精品资源,点击获取