OFDM/OTFS与LDPC/Turbo物理层仿真:从星座映射到BER曲线

发布时间:2026/10/7 5:20:06
OFDM/OTFS与LDPC/Turbo物理层仿真:从星座映射到BER曲线 搞通信物理层仿真的同学应该都有这种经历手里攒了一堆名词OFDM、OTFS、16QAM、QPSK、LDPC、Turbo每一个单独拎出来好像都看得懂但想把它们串成一条完整的收发链路在高斯白噪声AWGN信道下跑出BER曲线就会发现处处是坑。我最近正好把这样一套MATLAB仿真完整调通了从信道编码到星座映射从OFDM到OTFS所有模块都能在一个项目里自由切换对比。这篇内容就把整套仿真的设计思路、关键代码、参数换算和踩过的坑完整记录下来。适合正在做通信课设、准备保研面试、或者刚接触物理层仿真的同学参考。它解决的问题不是“某个函数怎么调用”而是“为什么这么设计、曲线差多少、哪里最容易翻车”。1. 项目定位与整体设计思路1.1 一条链路的完整拼图这套仿真的核心是一条最经典的数字通信物理层链路信源比特 → 信道编码LDPC/Turbo二选一→ 星座映射QPSK/16QAM二选一→ 多载波调制OFDM/OTFS二选一→ AWGN信道 → 接收端逆操作 → 硬判决/软信息译码 → 统计误码率。整个链路跑通之后你能在同一套代码里回答好几个问题编码增益到底有多大16QAM比QPSK省一半带宽代价是什么OTFS在AWGN里和OFDM有什么区别Turbo和LDPC谁更适合这个场景我在设计这个项目时特意把每个模块都做成了可切换的开关而不是写死。原因是通信仿真的变量太多了如果每次想对比QPSK和16QAM都要改一大片代码排查错误的时间会盖过真正分析问题的时间。用参数结构体统一控制编码方式、调制阶数、波形类型后面所有对比实验都只需要改一个字段效率会高很多。1.2 为什么偏要在AWGN信道下做这组对比很多同学会问OTFS不是为高移动场景设计的吗放在AWGN里是不是大材小用恰恰相反AWGN信道是验证系统实现正确性的“试金石”。高斯白噪声没有频率选择性也没有时间选择性信道在时延-多普勒域上只有一个零时延零多普勒的冲激这种条件下OFDM和OTFS经过各自的收发变换之后理论上性能应该完全一致。如果你把OTFS放在AWGN里跑出来和OFDM差了好几个dB那基本可以断定是实现有bug而不是波形本身的问题。另外LDPC和Turbo的香农极限性能本身就是以AWGN为参考的。只有在AWGN下把编码增益曲线跑明白后续换成多径衰落信道时才能分清性能损失到底是信道带来的还是编码器参数没调好。所以这个项目表面上是“多个模块对比”本质上是给通信物理层打一个可以进行后续扩展的验证平台。1.3 基带等效仿真的通用做法整条链路不仿真真实载波频率全部在复基带上完成。可以理解为把信号搬到零中频用I/Q两个分量表示这样采样率就等于符号速率仿真速度快很多内存占用也小。发端的复数符号向量进入AWGN信道时噪声的实部和虚部各加一份方差为N0/2的高斯白噪声这在MATLAB里用randn加个缩放系数就能搞定。我习惯把系统参数统一写成一个结构体param.Nfft 64; % FFT 点数 param.Ncp 16; % 循环前缀长度 param.Npilot 4; % 导频子载波数 param.M 16; % QPSK:4 / 16QAM:16 param.modType qam; % psk 或 qam param.codeType ldpc; % none / ldpc / turbo param.waveType ofdm; % ofdm / otfs param.codeRate 1/2; param.numFrames 2000;这种集中式参数管理在调参时非常省心。所有模块读的都是这个结构体调代码时不用满文件找数字。2. QPSK与16QAM的映射细节2.1 星座图、格雷码与能量归一化QPSK本质上是4点PSK每符号携带2比特。我用的映射方式是格雷码相邻星座点之间只差1个比特这样AWGN里最常见的“误判到相邻点”只产生1比特错误统计出来的BER更贴近实际FEC处理的效果。星座点集合为$$QPSK \frac{1}{\sqrt{2}} {1j, -1j, -1-j, 1-j}$$16QAM则是16个点的矩形星座每个符号承载4比特。标准格雷映射后的星座点坐标是$$16QAM \frac{1}{\sqrt{10}} \left[ \begin{matrix} -33j -13j 13j 33j \ -3j -1j 1j 3j \ -3-j -1-j 1-j 3-j \ -3-3j -1-3j 1-3j 3-3j \end{matrix} \right]$$那个$\sqrt{10}$是归一化因子它的来源是16个星座点模平方的平均值是10除以$\sqrt{10}$之后整个星座的平均发射功率等于1。这个细节比大多数人想象的重要。如果忘了做归一化信号平均功率不是1awgn函数加噪声时按信号实测功率来加BER曲线整体会偏移看起来像是系统性能差了几个dB实际只是能量标定不对。2.2 MATLAB里直接调用还是手写映射MATLAB的qammod函数已经封装了格雷映射非常方便modData qammod(dataBits, param.M, gray, InputType, bit); demodData qamdemod(rxSymbol, param.M, gray, OutputType, bit);要注意的是dataBits的长度必须是log2(M)的整数倍也就是QPSK每2个比特一组、16QAM每4个比特一组。如果帧长和调制阶数不匹配系统会直接报错。我在项目里是用编码后的比特流长度反过来决定每帧放多少个QAM符号这样不会出现莫名其妙的长度错位。如果你希望练手或者调软判决可以手写映射表。我建议至少手写一遍QPSK因为16QAM的硬判决边界在I和Q轴的0、±2处写一遍之后对解调原理的理解会深很多。但仿真阶段直接用自带函数就好没必要重复造轮子。2.3 硬判决与软信息LLR的取舍无编码系统可以直接对解调后的符号做硬判决然后数比特错误。但接了LDPC或Turbo之后译码器吃的是软信息也就是对数似然比直接把符号硬判决成比特再喂给LDPC编码增益会明显下降。MATLAB里更稳妥的做法是让译码器接收未判决的复数符号不是的这里需要显式地计算LLR。我在这套仿真里做了一个简化QPSK的软信息直接取接收符号的实部和虚部乘上信道增益这就是最简单的LLR线性近似16QAM则对I/Q两路分别计算分段LLR。具体函数不展开网上有现成实现关键是译码器输入端口的正负约定要对上。我踩过这个坑LDPC译码器输出的全零比特概率上极高但偶尔冒出一两个1检查半天发现是LLR符号反了把所有LLR取负之后曲线立刻恢复正常。3. LDPC与Turbo编码的实现选择3.1 LDPC校验矩阵与MATLAB通信工具箱MATLAB的通信工具箱里LDPC编码器依赖的是校验矩阵而不是直接喂生成多项式。常用做法是调dvbs2ldpc生成DVB-S.2标准的稀疏校验矩阵rate 1/2; H dvbs2ldpc(rate); % 默认码长64800 enc comm.LDPCEncoder(H); dec comm.LDPCDecoder(H, OutputValue, Whole codeword);默认码长64800对仿真来说太长逐帧跑会非常慢。可以查一下dvbs2ldpc是否支持短帧参数有的版本支持设置16200长度的码字如果不支持也可以自己构造准循环LDPC码。我这里用的是自带的短帧校验矩阵帧长在几千比特量级蒙特卡洛统计才跑得动。LDPC编码器的输入长度是信息位长度输出是码字长度。以码率1/2为例输入K比特输出2K比特。你要保证输入的信息比特数正好等于K否则会报长度错误。建议在代码里加一行assert(mod(length(infoBits), K) 0, 信息比特长度必须能被K整除);3.2 Turbo并行级联卷积码Turbo码的核心是并行级联两个递归卷积码并加交织器。MATLAB里用comm.TurboEncoder实现trellis poly2trellis(4, [13 15], 13); interleaverIndices randperm(L_info); % 交织深度 enc comm.TurboEncoder(TrellisStructure, trellis, ... InterleaverIndices, interleaverIndices, ... TerminationMethod, Terminated); dec comm.TurboDecoder(TrellisStructure, trellis, ... InterleaverIndices, interleaverIndices, ... NumIterations, 6);Turbo的优势是迭代译码在中等码长下性能非常强但每轮迭代的计算量不小。仿真时可以把迭代次数定在5~8次再往上增加的增益有限。交织深度必须等于信息位长度这是Turbo的硬性要求。如果你发现编码后比特率对不上先检查交织器长度。从实际对比曲线看码率1/2、码长几千比特时LDPC和Turbo的差距不会特别大通常只在低SNR区域的收敛速度上有差异。真正决定选型的是场景广播系统偏好LDPC译码器吞吐高、无迭代调度麻烦移动通信里控制信道常用Turbo或类似PCC结构低时延场景则两种都嫌复杂。3.3 编码增益的直观对比方法要看出编码的价值我建议同一组仿真里至少跑三条曲线无编码QPSK、LDPCQPSK、TurboQPSK。无编码QPSK的BER在1e-4时需要的EbN0大约8.5dB而码率1/2的LDPC或Turbo可以把这条曲线往左推4~5dB也就是说同样误码率下所需信噪比明显降低这就是编码增益。这里有一个容易误解的点码率1/2意味着每1比特信息实际要发2比特带宽翻倍。接收端用EbN0而不是SNR来衡量就是为了公平比较“每比特信息能量”自动扣除了带宽开销。画图时如果发现编码后曲线往右移而不是往左移先检查横坐标是不是误用了SNR而不是EbN0。4. OFDM与OTFS两种调制方式的核心区别4.1 OFDM的IFFT与循环前缀OFDM其实不复杂。把调制好的复数符号按列排列成Nfft个并行子载波做完IFFT就得到时域信号然后把时域信号的尾部复制到开头作为循环前缀。接收端去掉循环前缀再做FFT就还原出了频域符号。txFreq reshape(modSymbols, [], param.Nfft); % 每行为一个OFDM符号 txTime ifft(txFreq, param.Nfft, 2); % IFFT到频域 txTimeCp [txTime(:, end-param.Ncp1:end), txTime]; % 加CP循环前缀的作用是把线性卷积变成循环卷积这样频率选择性信道造成的多径延展不会干扰到下一个符号同时每个子载波的频域信道响应变成了一个复数增益均衡变得非常简单。在AWGN下多径长度为零不加CP也能跑通但为了后续扩展到多径信道我在项目里始终保留CP。另一点要注意IFFT之后最好做一次功率缩放保证发送信号平均功率依然是1。MATLAB的ifft会自动乘1/Nfft能量会变小所以要在时域信号上乘sqrt(Nfft)补偿或者直接在加噪声时用measured让awgn自动测量功率。4.2 OTFS把信息放到时延-多普勒域OTFS的思路是把调制符号排成二维网格两个维度分别是时延和多普勒。简单理解OFDM是在时频面上撒符号OTFS则是在时延多普勒面上撒符号然后通过二维傅里叶变换变成时频面的符号再走一次OFDM调制过程。工程上快速实现时可以直接用二维变换把DD域矩阵变成TF域矩阵和发端配对使用X_dd reshape(modSymbols, M_delay, N_doppler); % DD域 X_tf ifft2(fft2(X_dd)); % ISFFT简化实现 % 每个时频点再按OFDM调制 txTime ifft(X_tf, param.Nfft, 2);接收端做逆过程Y_tf fft(rxTimeCp(:, param.Ncp1:end), param.Nfft, 2); Y_dd ifft2(fft2(Y_tf)); % SFFT简化实现这里的关键是收发端变换约定一致。不同论文对ISFFT/SFFT的归一化和旋转方向写法不同但只要收发端使用同一对变换AWGN信道下的性能就是等价的。我建议第一版就跑AWGN目的是验证链路通了、BER落在理论上而不是急着加高多普勒信道。4.3 AWGN下两者为什么性能一样这个问题如果弄懂了OTFS的原理就通了一半。AWGN信道在时延-多普勒域上的响应只有一个抽头位于零时延零多普勒点相当于冲激响应是一个单位冲激乘上常数增益。此时DD域和TF域之间只是二维傅里叶变换关系信息在数学上是可逆且等价的没有哪个域天然有优势。OTFS真正的优势在高移动性场景信道在时间方向变化快、多普勒频移明显传统OFDM的子载波正交性会被破坏而OTFS把符号铺在时延多普勒平面双选信道在DD域变成一个稀疏矩阵几乎不破坏符号之间的正交性再用均衡器恢复就简单很多。所以用AWGN做OTFS验证不是为了看增益而是为了确认变换方向、归一化、帧结构这些基础环节没写错。这个认知想清楚之后你在项目答辩里就不会说出“OTFS在AWGN更有优势”这种外行话了。5. 完整仿真链路搭建与参数标定5.1 一套可扩展的仿真参数表我在这套系统里实际使用的参数如下参数取值说明FFT点数 Nfft64OFDM子载波总数循环前缀 Ncp16占总符号时长20%数据子载波52其余做导频/空子载波调制方式QPSK / 16QAM映射前可选编码方式LDPC / Turbo / 无编码三选一码率1/2编码/译码统一帧数2000~5000按目标BER调整EbN0范围-5:1:15 dB覆盖误码率多个数量级帧数的选择直接影响曲线平滑度。如果你在BER1e-4处只仿真了100帧每一帧1000比特总共10万比特里理论上只有10个错误随机性太大曲线毛刺会非常重。我的经验是统计到至少100个错误比特再记录这个点的BER也就是说帧数要和预期误码率匹配不能一套参数从头用到尾。5.2 主循环里的核心顺序仿真主循环的伪代码框架大概是这样for snrIdx 1:length(EbN0dB) snrVal EbN0dB(snrIdx); SNR_dB snrVal 10*log10(log2(M)) 10*log10(codeRate); % 如果考虑CP开销再扣除 10*log10(Nfft/(NfftNcp)) berSum 0; bitSum 0; for frame 1:param.numFrames txBits randi([0 1], infoBitsLen, 1); % --- 信道编码 --- if strcmp(codeType, ldpc) codedBits encoder(txBits); elseif strcmp(codeType, turbo) codedBits turboEncoder(txBits); else codedBits txBits; end % --- 星座映射 --- modSymbols qammod(codedBits, M, gray, InputType, bit); % --- OFDM / OTFS 调制 --- if strcmp(waveType, ofdm) txSignal ofdmModulate(modSymbols, param); else txSignal otfsModulate(modSymbols, param); end % --- AWGN 信道 --- rxSignal awgn(txSignal, SNR_dB, measured); % --- 解调 --- rxSymbols ofdmDemodulate(rxSignal, param); % 或 otfs解调 % --- 软信息/硬判决 --- llrValues qamDemodSoft(rxSymbols, M); % 有编码用软信息 rxBits ldpcDecode(llrValues); % 或turbo译码 % --- 统计误比特率 --- errBits sum(xor(txBits, rxBits)); berSum berSum errBits; bitSum bitSum length(txBits); end berCurve(snrIdx) berSum / bitSum; end这个顺序是无数通信仿真总结下来的标准动作别跳步。特别提醒awgn函数最好用measured选项它会在加噪声前实测信号功率避免因为手工缩放不一致导致整条曲线漂移。如果你的信号功率本来就是严格1也可以直接用缩放倍数加噪声但实测方式更不容易埋雷。5.3 EbN0与SNR的换算公式这是整个项目中最容易出错、也最影响曲线可解释性的地方。AWGN仿真时awgn函数需要的是SNR信号功率/噪声功率而横坐标我们习惯画EbN0每比特能量/噪声功率谱密度。换算关系是$$SNR_{dB} EbN0_{dB} 10\log_{10}(k)$$其中k是每个信息比特实际对应的信道符号数的倒数。具体地说每个调制符号携带 $\log_2(M)$ 个编码比特编码码率R则每个调制符号携带 $R \cdot \log_2(M)$ 个信息比特如果有CP开销有效信息速率再乘 $N_{fft}/(N_{fft}N_{cp})$所以实际换算k log2(M) * codeRate * (Nfft / (Nfft Ncp)); SNR_dB EbN0dB 10*log10(k);举个例子QPSKlog2(M)2、码率1/2、FFT64、CP16则k2×0.5×(64/80)0.8也就是SNR比EbN0低约0.97dB。如果你忽略CP开销横坐标会偏小约1dB曲线对比时就会无中生有地差出1dB。5.4 画图时该用什么坐标系误码率曲线一定要用semilogy纵坐标是BER取对数之后才能看清瀑布区和高斯区域的区别。横坐标我建议0:1:12或-5:1:15根据编码类型调整因为无编码16QAM在低信噪比时BER接近0.5看不出曲线形态。每一条曲线保存到独立变量里最后统一画图加网格线、图例、标注每条曲线对应的配置。我习惯把已经跑出来的曲线数据存成.mat这样以后改参数时老结果还在可以直接对比不用重跑。6. 踩坑实测与排查速查表6.1 噪声功率与SNR定义不一致这个坑几乎人人都踩过。症状是编码后的BER曲线反而比无编码的曲线更差或者理论QPSK曲线在8.5dB处对应BER1e-4但仿真结果整体右偏2dB以上。原因几乎都是SNR换算时遗漏了码率、调制阶数或CP开销中的某一项。我的排查步骤是先跑无编码QPSK直通链路和理论误码率 $0.5 , erfc(\sqrt{EbN0})$ 对照。如果这条曲线都对不上后面的全都不用看。对照上了再逐级加上CP、IFFT、编码每加一层都要确认曲线没有异常移动。6.2 LDPC译码输出全零或者错误率反而升高编码链路里最诡异的问题就是译码输出了全零。我的经验是先构造一个全零信息帧编码后经过无噪声信道观察译码输入和输出是否完全一致。如果不一致说明LLR正负约定、比特极性unipolar还是bipolar至少有一个地方反了。QPSK软信息常用“1对应正LLR”但具体到译码器的端口说明必须查文档看清楚。另外LDPC在低信噪比区域会出现“不收敛”甚至“错误地板”BER曲线在高SNR段突然变平这通常是迭代次数不够或者帧长太短。增加译码迭代次数到50或者增大帧长错误地板通常能压下去。6.3 OTFS变换方向写反但BER并没有完全错乱OTFS实现里一个隐蔽问题是ISFFT和SFFT的维度方向或归一化写反了。由于AWGN下变换是正交的方向写反的表现不是BER变成0.5而是BER在一个中间值徘徊比全错好一点比正确链路差很多特别迷惑人。排查方法很简单把发射端和接收端的变换函数相互配合地反过来试一遍哪个让BER回到OFDM参考曲线哪个就是对的。在这套项目里我会同时跑一组OFDM的BER作为基准曲线OTFS的曲线只要偏离OFDM超过0.2dB就先停下去查变换实现而不是查信道。6.4 仿真速度太慢怎么办当LDPC码长在16200甚至64800时逐帧译码会非常慢特别是在中低信噪比区域迭代次数高且错误多跑一组曲线可能要几小时。我的处理方式是三个层面同时优化第一把仿真帧数降下来先用200帧快速验证链路是否正确确认无误后再加大帧数跑正式数据第二使用MATLAB的parfor并行跑不同SNR点多核利用率能大幅度提升第三把LDPC换成自己构造的短码或者部分迭代译码速度可以提升一个数量级。如果要正经做学术对比建议在集群上把不同SNR点切成独立任务并行跑本地调试只跑一两个点确认趋势。6.5 典型问题速查表现象可能原因解决方式编码后曲线比无编码差横坐标用了SNR而不是EbN0或码率折算错误核对K值换算公式16QAM曲线在低SNR处BER≈0.5正常不用惊慌从高位SNR开始画OFDM和OTFS曲线差0.5dB以上ISFFT/SFFT方向或归一化不一致让OTFS对齐OFDM参考曲线LDPC译码输出全零LLR极性反了全零帧无噪声测试BER曲线有大量锯齿统计错误比特太少增加帧数或按错误数决定仿真长度QAM符号长度报错帧长不是log2(M)倍数用assert检查长度Turbo编码后码率不是1/2TerminationMethod未配好改用Terminated并核对输出长度我在实际使用中还有一个习惯所有关键中间变量都保存下来编码前后比特、调制前后符号、加噪前后信号出问题时直接看中间值而不是一遍遍猜。比如加噪声前后信号功率差、星座图是否旋转、LLR正负分布是否对称这些一眼就能看见问题的信号比在BER曲线上猜原因高效太多。这套仿真我前后改了三版最大的体会是调试必须分层进行先跑无编码直连链路对标理论值再加CP和IFFT验证OFDM框架再加编码看增益曲线最后做OTFS扩展。每一步都站在上一步的验证结果上才能保证最终曲线是可信的。最后分享一个小技巧正式跑大量帧之前先把解调后的星座图画出来看一眼如果星座点有旋转或缩放异常很多问题五秒钟就能看出来完全不用等BER曲线跑完再返工。这套链路现在跑AWGN已经完全稳定了下一步我打算把信道换成带多普勒扩展的衰落信道到时候OTFS的优势才能真正显现出来那是另一个更长的故事了。