OFDM-CDMA Matlab仿真:从原理到误码率实测的完整链路搭建
简介基于MATLAB的正交频分复用与码分多址联合仿真源码面向通信工程专业学生、信号处理方向研究生以及需要快速搭建无线物理层仿真环境的开发者。资源从无到有演示了正交频分复用-码分多址系统的主要处理流程包括基带随机数据生成、Walsh码或Gold码扩频、快速傅里叶逆变换引入子载波正交性、循环前缀插入抑制符号间干扰以及接收端的快速傅里叶变换解调与解扩恢复结构紧凑、便于逐段对照学习。压缩包内共有1个m文件整体体积约3KB单文件内含完整主流程与可调参数无需额外数据文件即可在MATLAB中直接运行适合作为课程设计、期末作业或算法研究的基础模板。通过修改子载波数量、扩频码序列、循环前缀长度和调制方式等参数使用者可以直观分析多径衰落与加性噪声对系统误码性能的影响加深对正交频分复用-码分多址抗干扰原理的理解。尤其适合对移动通信物理层技术感兴趣的读者以最小代码量快速上手仿真验证。目前该资源已有32人浏览学习是一份实用、轻量的参考代码。1. 正交频分复用CDMA仿真为什么值得在自己电脑上跑一遍正交频分复用CDMAOFDM-CDMA是通信仿真里少有的“一条链路包含两条技术主线”的题目。做Matlab通信仿真的人迟早会遇到它——用一套不算复杂的代码把多用户叠加、频域扩频、子载波映射、多径均衡全部串在同一个基带链路上。它的价值不在性能碾压某种制式而在“一跑就懂”不做理论推导只观察IFFT前后的星座图就能理解为什么CDMA信号能在频域上扩开、为什么正交码之间能彼此让路也能看到多径信道是如何破坏这些正交性的。适合三类人通信方向做课程设计或毕业设计的学生想验证OFDM-CDMA链路误码率结论的从业者以及准备补全物理层仿真功底的工程师。下面这套做法按我实际调试过的链路来写每个参数取舍都有据可查踩过的坑也都列在最后。2. 从时频资源说起OFDM-CDMA的原理、扩频映射和链路架构2.1 时频网格与用户叠加为什么说这是OFDM和CDMA的“杂交优势”OFDM把带宽切成一堆窄带子载波每个子载波近似平坦衰落CDMA则用扩频码让多个用户共享同一段频谱。两者结合时最直观的理解是把OFDM的子载波看成“频域格子”把CDMA的扩频码看成“在格子上画的图案”。以发射端为例。一个用户调制出一个数据符号后并不直接占用某一个子载波而是把这个符号复制成扩频因子SF份每一份乘上扩频码的一个码片再放到SF个子载波上。如果系统里有4个用户、扩频因子也是4那么在某几个子载波上4个用户的信号同时叠加。接收端拿到子载波上的叠加信号后用对应扩频码做相关解扩就能把属于自己的那一份能量“加权”出来同时把其他用户的干扰在理想同步下完全抑制掉。这个设计的关键词是“正交”。Walsh-Hadamard码的任意两行内积为零因此满负载且完全同步时多用户之间没有干扰。这就是OFDM-CDMA与纯粹OFDMA每个用户独占子载波最本质的区别OFDMA把格子分给用户OFDM-CDMA让所有用户画在同一张格子上用码图案区分彼此。2.2 频域扩频与时域扩频两种映射的典型取舍OFDM-CDMA在实现上分两大流派频域扩频和时域扩频。常见的教程和仿真包默认是频域扩频因为它和OFDM的IFFT结构结合得最紧密。频域扩频的含义是一个数据符号被扩到多个子载波上子载波本身承载的是“码片×符号”的结果。接收端先做FFT再在频域做解扩。好处是可以直接利用OFDM的频域均衡对频率选择性衰落有天然的对抗能力——某个子载波深衰落只会损失这个符号的一部分码片能量不至于整个符号消失。时域扩频则不同数据符号先被扩频码乘一遍得到一个码片序列然后再按OFDM的方式把这些码片当作普通时间采样处理。这种方案在频域上每个用户占用的子载波是独立的更像“先用CDMA扩频再用OFDM搬运”。它的问题是接收端要做两套解调复杂度更高且频域分集增益不如频域扩频直接。我做仿真时默认选频域扩频原因有三个第一链路结构清晰发射端到接收端共用一个FFT第二多径信道下的均衡处理非常成熟MMSE均衡后直接解扩即可第三误码率曲线收敛得比时域扩频更干净适合调参数。如果你拿到一个仿真包先看它的扩频映射发生在IFFT之前还是之后就能判断它属于哪一派。2.3 一条完整基带链路的模块划分与信号流一个能跑出误码率的OFDM-CDMA链路最少需要六个模块信源与调制 - 扩频与子载波映射 - IFFT与加CP - 信道 - 去CP与FFT - 均衡与解扩 - 判决与统计这里补一个处理细节加CP不是可有可无的装饰。在实际仿真里如果直接用频域乘法fft(h, fftLen) .* freqVec来模拟信道CP的作用已经被内隐处理了——频域乘法等价于“信道冲激响应长度小于CP时的循环卷积”。但如果你在时域用conv做线性卷积就必须先加CP再截取有效部分否则前几个符号的ISI会把BER曲线拖死。模块划分决定了调试顺序。我的习惯是先把发射端修到“IFFT后星座点能量稳定”再把接收端修到“AWGN下BER和理论QPSK曲线重合”最后才加入多径和均衡。反过来调试会让你分不清是映射错了还是均衡错了。2.4 仿真参数先定死再放开仿真一开始不要追求参数可调性先把固定参数跑通。以下是这组最小链路的基础参数后面的所有代码都基于这个配置% 基础参数先固定再逐步放开 fftLen 64; % IFFT/FFT 点数 cpLen 16; % 循环前缀长度点 sf 4; % 扩频因子 numUsers 4; % 活跃用户数必须不大于 sf modOrder 4; % QPSK bitsPerSym 2; % 每符号比特数 numBitsPerUser 3200; % 每个用户的对比特数 numOfdmSymbols 100; % 对应 3200/2/(64/4) snrList 0:2:18; % 扫SNR numTrials 50; % 蒙特卡洛次数注意numOfdmSymbols不是随便定的每个OFDM符号能容纳的每用户数据符号数是fftLen / sf 16所以一个用户100个OFDM符号能送16 × 100 1600个调制符号对应1600 × 2 3200个比特。这个换算关系需要代码里显式写出来否则改FFT长度或扩频因子时切分数据块的位置就会错位误码率乱跳。3. 用Matlab搭一个最小可运行的OFDM-CDMA链路发射、信道、接收、误码统计3.1 扩频码生成与功率归一化最容易埋雷的一步Walsh-Hadamard码是在同步正交条件下最稳的选择Matlab一行就能生成% 生成 4x4 Walsh-Hadamard 矩阵取前 numUsers 行作为扩频码 hadMat hadamard(sf); codes hadMat(1:numUsers, :) / sqrt(sf); % 归一化到单位能量这里sqrt(sf)是功率归一化。如果不除码片幅度是±1一个数据符号扩到4个子载波上后总能量变成原来的4倍解扩后判决变量幅度直接放大虽然BER曲线在AWGN下仍可能正常但加多径均衡后均衡器的权重是按信道幅度计算的能量标度一旦不一致MMSE正则项1/snrLin和实际信噪比就对不上导致曲线整体偏移。我见过不止一次“代码看起来对但曲线差1.5dB”的情况根因就是这里少了归一化。解扩时同样要注意标度。接收端对某个用户解扩的操作为y_despread codes(u, :) * y_eq(subIdx); % 1xSF 向量乘 SFx1 向量得到标量由于扩频码已经归一化解扩后信号幅度和调制符号幅度一致噪声方差也不会被SF放大。这样AWGN下的BER才能和理论QPSK曲线重合后续比较均衡算法才有意义。3.2 发射端调制、扩频、子载波映射与IFFT发射端的核心循环如下。假定txSyms是一个numOfdmSymbols * symsPerOfdm行、numUsers列的矩阵每列是一个用户的全部调制符号symsPerOfdm fftLen / sf; % 每个OFDM符号里每个用户可放的符号数 for ofdmIdx 1:numOfdmSymbols freqVec zeros(fftLen, 1); for u 1:numUsers % 取出该用户当前OFDM符号周期要发送的 symsPerOfdm 个数据符号 symBlock txSyms((ofdmIdx-1)*symsPerOfdm (1:symsPerOfdm), u); % 频域扩频每个符号占 sf 个连续子载波 for g 1:symsPerOfdm subIdx (g-1)*sf (1:sf); freqVec(subIdx) freqVec(subIdx) symBlock(g) * codes(u, :); end end % 反变换到时域 txTime ifft(freqVec); endsubIdx的映射顺序是“先组后码片”第1组子载波放所有用户的第1个符号的4个码片第2组放第2个符号依此类推。这个顺序决定了接收端解扩时取子载波的索引方式发射端和接收端必须完全一致。如果你选择交织映射码片均匀散布在整个频带也能工作但那个更适合强频率选择性信道最小链路用连续分组映射足够了。ifft默认没有能量缩放问题Matlab的ifft是归一化的频域向量能量等于时域向量能量。调试时可以在这一步打印mean(abs(txTime).^2)如果这个值随用户数变化太大八成是扩频码没有归一化。3.3 信道与接收端AWGN、多径、均衡与解扩接收端分为信道模拟、加噪、FFT、均衡、解扩五步。下面的代码同时覆盖AWGN和两径信道通过chanType控制% 信道选择 if strcmp(chanType, multipath2) h [1; 0.3; 0.1]; % 三抽头最大时延小于 cpLen16 Hf fft(h, fftLen); % 频域响应 else Hf ones(fftLen, 1); % AWGN end % 接收端逐OFDM符号处理 for ofdmIdx 1:numOfdmSymbols % …… 重新构建 freqVec与发射端相同 …… % 经过信道频域乘法等价于CP保护下的循环卷积 rxTime ifft(Hf .* freqVec); % 加噪声噪声功率按接收时域信号功率折算 sigPow mean(abs(rxTime).^2); noiseVar sigPow / 10^(snrDb/10); rxTime rxTime sqrt(noiseVar/2) * (randn(fftLen,1) 1i*randn(fftLen,1)); % FFT 回到频域 yFreq fft(rxTime); % MMSE 均衡若用追零把下面分母去掉正则项 snrLin 10^(snrDb/10); yEq conj(Hf) ./ (abs(Hf).^2 1/snrLin) .* yFreq; % 解扩与判决 for u 1:numUsers recSyms zeros(symsPerOfdm, 1); for g 1:symsPerOfdm subIdx (g-1)*sf (1:sf); recSyms(g) codes(u, :) * yEq(subIdx); end % 存入接收符号矩阵最后统一解调统计 end end关键点在于均衡和解扩的顺序不能颠倒必须先均衡恢复子载波间的幅度相位一致性再做码片相关求和。如果先解扩再均衡同一组内不同子载波经历不同信道增益扩频码正交性在频域已经被破坏解扩输出会混入严重的用户间干扰。3.4 把误码率算出来外层循环与统计口径最外层的蒙特卡洛循环建议按“SNR点→trial”组织误码统计放在最内层循环结束之后for snrIdx 1:length(snrList) snrDb snrList(snrIdx); errBits zeros(1, numUsers); for trial 1:numTrials % 随机生成所有用户的比特 txBits randi([0 1], numBitsPerUser, numUsers); % 调制、走链路、解调得到 rxBits % …… errBits errBits sum(rxBits ~ txBits, 1); end berUser(snrIdx, :) errBits / (numTrials * numBitsPerUser); end统计口径要注意两个细节。第一numTrials和numBitsPerUser相乘后的总比特数要足够大否则低SNR区域波动很厉害。3200比特×50次16万比特在BER1e-3时大概能统计到160个错误比特误差在可接受范围。第二误码率曲线画的是每用户平均还是全用户平均要写清楚。这里按每用户分别统计最后再取平均能顺带检查各用户是否对称。4. 必调参数与性能权衡扩频因子、子载波数、信道抽头与均衡器怎么选4.1 扩频因子SF容量、抗干扰和频选衰落之间的权衡扩频因子是这个仿真里最核心的杠杆。SF越大每个数据符号被摊到越多的子载波上频率分集增益越明显抗窄带干扰能力越强。但代价是每个OFDM符号能容纳的每用户符号数fftLen/sf随之变小或者在用户数不变时浪费更多的码道资源。举一个具体权衡。FFT长度固定为64时SF4意味着每个用户每个OFDM符号能发16个数据符号最多支持4个正交用户满负载SF16时每个用户只能发4个符号但可以支撑16个正交用户。在用户数固定的小系统里提高SF不会改变总能效却会让单用户BER曲线明显变好因为它获得了更多的频域分集。调参时的判断依据是信道频率选择性子载波之间相关性越弱时延扩展越大提高SF的收益越明显。如果信道基本平坦SF提高只会增加解扩后的噪声积累码片变多求和噪声也在积累收益有限。因此仿真包里如果给了SF参数先跑一遍两径信道对比SF4和SF16你会看到曲线间隔比AWGN下大得多。4.2 FFT长度与CP长度先想清楚时延和频偏的边界FFT长度决定子载波间隔。采样率固定时FFT长度每翻一倍子载波间隔缩小一半符号周期拉长一倍。OFDM系统对时延扩展的容忍度由CP长度决定CP必须大于信道最大附加时延。这个仿真里h [1; 0.3; 0.1]的冲激响应长度为3个采样点CP取16绰绰有余但如果你为了模拟更恶劣的信道把抽头延后到15个采样点CP就必须相应加大。CP长度也不能一味增加。CP不携带数据它占用的时间是纯开销。实际操作中我会先用max_tap_index检查信道最大时延位置再按“CP长度取最大时延的1.5~2倍”来设。另外有一个容易被忽略的点如果你直接在频域用Hf fft(h, fftLen)做信道模拟CP不明显参与计算不太容易触发ISI但一旦改用时域conv加真实CPCP长度不足的后果立刻反映在BER曲线上。4.3 均衡器选追零还是MMSE噪底和残留干扰的取舍追零均衡ZF的核心是yFreq ./ Hf实现简单但它会无差别地放大处于深衰落的子载波上的噪声。在OFDM-CDMA里这个问题会被解扩再度放大一组内只要有一个子载波处于深度衰落ZF就把那里的噪声抬得很高解扩后的判决变量SNR被拖累。MMSE均衡的分母是abs(Hf).^2 1/snrLin加上的正则项是对噪声方差的估计。高SNR时正则项趋近于零MMSE退化为ZF低SNR时它会压住深衰落子载波的放大倍数换取整体均方误差最小。解扩之后这个优势尤其明显。我的建议是AWGN下用哪个都无所谓多径信道下直接上MMSE不要在ZF上浪费调试时间。调MMSE正则项时有个坑1/snrLin的分母必须是线性信噪比不能把dB值直接填进去。写成1/10^(snrDb/10)才正确。如果你在代码里看到 1/snrDb这种写法基本可以断定是参数标度错误。4.4 多用户数与负载因子半负载性能反而更值得关注当活跃用户数小于扩频因子时系统处于半负载状态。表面上看干扰变小性能应该更好但实际BER曲线不一定按预期变好原因是噪声积累的方式变了。解扩过程相当于用户对接收向量做一次相关运算。即使未使用的码道没有信号码道上的噪声仍然存在相关运算会把整个扩频空间的噪声能量都叠加进来。由于扩频码已归一化解扩后的噪声方差近似等于单个子载波上的噪声方差和负载因子关系不大。真正变化的是总发射功率用户越少频域叠加的信号总能量越低在相同每用户SNR定义下半负载系统的绝对信噪比其实更高。实操时的观察点保持SNR定义不变对比numUsers4和numUsers2的BER曲线。前者满负载后者半负载。若两条曲线几乎重合说明噪声被码间干扰主导的概率低码片间正交性保持良好若半负载系统明显更好说明满负载下存在残留多用户干扰需要检查均衡是否彻底。5. OFDM-CDMA仿真的常见翻车点现象、原因与排查顺序5.1 误码率在高信噪比时不再下降现象BER曲线在SNR大于14dB后突然变成一条水平线再怎么加大SNR也不动。这个现象在低SNR区域正常的曲线到了高SNR区就失效了。原因通常有两个。一是CP长度不够多径信道的拖尾超出CP范围形成与信号功率无关的ISI地板二是均衡后的残留用户间干扰往往来自Z F均衡放大了深衰落子载波噪声。注意MMSE正则项若用了1/snrDb这种错误标度在高SNR区域同样会留下一个错误地板。解决先检查信道的最大时延是否小于CP然后把ZF换成MMSE。如果地板仍存在检查是不是解扩前没有做均衡——漏掉这一步会导致高SNR下误码率卡在0.1附近下不去。5.2 不同用户的误码曲线完全重合怀疑用户区分失效现象4个用户的BER曲线几乎一模一样甚至某个用户明显比其他用户差一大截。当你确认所有用户的调制方式、信道条件都相同时曲线重合是正常的不用惊慌。真正的异常是曲线相同但星座图上有明显旋转或多址干扰。原因如果扩频码用错了行——比如取了hadamard(sf)的两行相同码——正交性被破坏某个用户会收到其他用户的完整干扰。另一个常见错误是发射端把不同用户的信号放到了不同子载波组上退化成OFDMA这样“多址干扰”完全消失曲线重合也正常但这个仿真已经不再是OFDM-CDMA。解决打印codes * codes.验证单位阵再检查发射端是否真的把每用户的扩频结果叠加到了同一组子载波上。如果用户数设为2、SF设为4两个用户的前2行码必须正交。5.3 加了信道编码后BER反而更差现象把卷积码或LDPC码加进链路后解调后的BER不降反升或者提升极其有限看起来像编码起反作用。原因这个场景往往是编码增益的计算口径出了问题。OFDM-CDMA链路中的扩频本身就是一种冗余再加入编码后如果SNR坐标仍然按照“每调制符号信噪比”定义而没有折算成比特信噪比Eb/N0编码带来的增益会在坐标换算中被吞掉。还有一个隐蔽问题编码可能引入额外的交织延迟如果你的仿真没有把交织器和信道频域选择性对齐编码块内的多个符号同时经历深衰落纠错能力被击穿。解决把横轴从SNR换成Eb/N0换算公式为EbN0 snrDb - 10*log10(bitsPerSym)。同时确认编码后的数据是否经过交织交织深度至少要大于信道相干带宽对应的符号数。如果不打算深入编码这个方向最稳妥的做法是先把编码从最小链路里摘出去。5.4 改变扩频因子后仿真结果完全不变现象把SF从4改成8BER曲线、星座图看不出任何变化。这在实际项目里几乎不可能除非扩频因子没有真正参与链路。原因最常见的写法错误是扩频码生成后没有把sf变量传入子载波映射循环循环里写死了4或者扩频矩阵维度在数值上恰好等于旧值。另一种可能是接收端解扩时用codes(u,:) * yEq(subIdx)但subIdx的长度依然是旧SF导致多出来的码片没有参与解扩。解决在发射端和接收端分别打印size(codes)和sf再检查subIdx (g-1)*sf (1:sf)是否从变量sf动态取值。快速验证办法把SF改成16星座图上每个符号的散布应该明显变窄因为频率分集变强了。5.5 噪声功率计算和SNR定义错位现象同一组代码把numTrials调大之后BER曲线整体平移或者SNR从0到18dB扫完曲线斜率异常陡峭。原因噪声功率逐次随机生成如果sigPow每次都用mean(abs(rxTime).^2)重新测量而rxTime是包含多个OFDM符号的帧信号测量窗口和加噪窗口不一致SNR会随随机数据波动。更隐蔽的问题是如果你在频域加噪但噪声方差按时域能量计算FFT归一化因子会带来固定偏差曲线的横轴整体偏移却形状不变。解决把每个OFDM符号的信号能量计算限定在同一符号的有效长度内并且统一在时域加噪。推荐做法是每次发送前先计算sigPow然后用固定公式noiseVar sigPow / 10^(snrDb/10)生成噪声不要手动乘以任何与FFT长度相关的系数。6. 把仿真做扎实星座图、EVM和蒙特卡洛验证习惯6.1 星座图观察从视觉上确认链路状态BER曲线只是最终统计结果调试阶段真正有用的是解扩后的星座图。在接收端解扩出recSyms后加一行plot(real(recSyms), imag(recSyms), .)就能立刻判断问题出在哪一段。AWGN下星座图应该是围绕四个理论星座点的圆簇旋转方向取决于信道相位估计。多径信道下如果均衡正确星座点应该重新聚拢只是会比AWGN下略微发散。如果看到星座点之间出现明显的“拖尾”说明均衡后的残留干扰没有被压住如果看到整团星座整体旋转一个固定角度说明信道相位没有被均衡掉多半是Hf的共轭用错了位置。6.2 EVM数值化代替肉眼的判断星座图看多了容易疲劳建议给接收符号算一个误差向量幅度% 计算某用户解扩后符号的EVM evm sqrt(mean(abs(recSyms - refSyms).^2)) / sqrt(mean(abs(refSyms).^2)) * 100;不要把refSyms用发射端的原始符号而要用发射符号经过信道且不加噪声后的理想接收值。最方便的做法是在纯AWGN低SNR和高SNR下分别测EVM低SNR下EVM在30%以上是正常的高SNR下EVM应该小于5%。如果高SNR下EVM依然在15%以上回查均衡和解扩顺序。6.3 蒙特卡洛的置信区间习惯单次仿真的误码率本身是一个随机变量。想要曲线平滑不只是提高numTrials那么简单——如果某个SNR点统计到的错误比特少于100个该点的BER标称值就未必可靠。低SNR区域BER高50次trial足够高SNR区域BER低到1e-4以下每个trial的错误比特非常稀疏必须提高numBitsPerUser或numTrials。我的习惯是保证每个SNR点至少统计到100个错误比特。收尾时我会固定做两件验证第一把多径关闭跑一遍AWGN确认所有用户BER曲线与理论QPSK曲线在1dB内第二把扩频因子提高一倍确认高频选信道下BER明显改善。这两条过了链路的主干才算稳后面加均衡、编解码或MIMO才有意义。做了一整轮OFDM-CDMA仿真最让我印象深刻的是这类链路往往不是被复杂算法难住的而是被功率归一化、索引映射和SNR标度这些基础细节反复绊倒。希望帮到你也希望你跑完代码后能顺手验证一下本文提的四个“翻车点”它们能帮你确认自己写的仿真到底是真对还是碰巧对。本文还有配套的精品资源点击获取