ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MATLAB实现DS-CDMA系统仿真:从扩频码到误码率分析

MATLAB实现DS-CDMA系统仿真:从扩频码到误码率分析 直接上干货咱们用MATLAB折腾个DS-CDMA通信系统玩玩。这玩意儿说白了就是让多个用户共享同一频段靠不同的扩频码区分彼此。别慌咱们分步拆解实现逻辑。DS-CDMA直接序列码分多址在通信系统里算是老前辈了3G时代的标志性技术到现在民用军用里还到处有它的影子。我对它的理解就一句话把窄带信号扩展成宽带信号然后用码子的正交性把各路信号“叠”在一起传接收端再用同样的码子把目标信号“捞”出来。MATLAB做这套仿真特别合适不需要硬件环境就能把发射、信道、接收全链路跑通还能直观地看误码率曲线。这篇文章直接给你完整可跑的代码配上每一步的原理说明适合学过通信原理但没动手写过系统级仿真的同学也适合想在毕设或者项目里快速搭一个CDMA链路做验证的工程师。1. 先拆明白DS-CDMA系统里到底有哪些模块真正上手写代码之前我习惯先画一张模块图在脑子里。DS-CDMA链路往简单了说就是三大部分发射端、信道、接收端。发射端负责把用户的数据比特用扩频码展宽再调制到载波上信道就是加噪声、加干扰、加多径的地方仿真里最常干的事就是给它加AWGN高斯白噪声接收端干的事就反着来——先解调再相关解扩最后判决出原始比特。1.1 扩频码才是灵魂为什么不是随便一串01都行CDMA的核心区分度来自扩频码。理论上每个用户分配一个唯一的伪随机序列这个序列要满足一个关键性质不同用户之间的互相关要足够低同一用户的自相关要足够尖锐。只有这样才能做到“你说话的时候我不觉得吵”。常用的扩频码有三类m序列、Gold序列、Walsh码。我在仿真里最常用的是Gold码原因很简单m序列的数量太少给不了太多用户用Walsh码正交性好但在多径下容易崩Gold码数量多互相关也控制得好最适合做多用户场景的仿真。Gold码的生成方法不复杂就是取两个优选m序列把它们逐比特异或再循环移位组合出一族新序列。两个长度为2^n-1的m序列可以生成2^n1个Gold序列。这个数量级在仿真里非常够用。1.2 系统级参数怎么定先算处理增益再谈性能不少新手一上来就写代码结果跑出来的BER曲线完全不合理最常见的原因就是参数没设计好。我每次搭仿真都会先定这几个参数数据速率Rb、扩频码片速率Rc、码片长度处理增益、载波频率、采样率。处理增益Gp Rc / Rb在数字域里就等于一个比特对应的码片数。比如Rb 10kbpsRc 1.28Mcps那么Gp 128也就是每个信息比特扩展成128个码片。处理增益越大抗干扰能力越强代价是占用带宽越大。MATLAB仿真里我非常建议直接用每个比特对应的码片数来做离散仿真不要纠结连续时间这样计算量小而且逻辑清楚。采样率的选择也有讲究。如果做带通调制解调载波频率必须满足奈奎斯特条件同时每个码片至少要采2个点以上。如果只是做基带等效仿真那可以完全绕开载波直接处理复基带信号计算效率高很多。我下面的代码就先用基带等效方式因为它聚焦在CDMA的核心逻辑上不容易被调制细节带偏思路。2. 发射端实现数据怎么变成宽带信号发射端的逻辑可以拆成四步生成用户比特、把比特映射成扩频后的码片序列、叠加多用户信号、送入信道。这里面最容易犯错的地方是扩频和调制之间的速率关系很多人把数据速率和码片速率搞混导致解扩时候序对不上。2.1 m序列生成器自己写还是用内置函数MATLAB的通信工具箱里其实有comm.GoldSequence这类现成对象但我想先带大家手写一个m序列生成器因为你只有亲手实现过一遍后面调时序问题的时候才知道底层在干什么。m序列本质上是线性反馈移位寄存器LFSR的输出。以生成多项式1 x^2 x^7为例代码可以这样写function seq generateMSeq(poly, init_state, len) % poly: 生成多项式系数最低位对应x^0 % init_state: 初始状态长度与寄存器级数相同 n length(init_state); state init_state(:); seq zeros(1, len); for k 1:len seq(k) state(end); % 输出最高位 feedback mod(sum(poly .* state), 2); % 线性反馈 state [feedback; state(1:end-1)]; end seq 2 * seq - 1; % 映射到 1/-1 end注意这里我把0/1映射成了1/-1这个习惯我从一开始就养成了。原因很直接扩频码是拿来做相关运算和乘法的1/-1的表示下自相关峰值就是N互相关接近0数学上特别干净。如果你用0/1去做乘法你得到的就不是相关而是异或逻辑后面很多推导都会出问题。2.2 用Gold码给每个用户分配“身份ID”生成Gold码的标准流程是先找两个优选m序列然后异或。两个m序列长315阶移位组合后能得到33条Gold码足够仿真里给十几个用户分配了。% 生成一对优选m序列 poly1 [1 0 1 0 0 1]; % x^5 x^2 1 poly2 [1 1 0 0 1 1]; % x^5 x^3 x^2 x 1 init1 ones(1, 5); init2 ones(1, 5); m1 generateMSeq(poly1, init1, 31); m2 generateMSeq(poly2, init2, 31); % 生成Gold序列族 gold_codes zeros(31, 33); for shift 0:30 m2_shift circshift(m2, shift); gold_codes(:, shift1) m1 .* m2_shift; end % 额外一条直接m1他就是gold族里面的一条 gold_codes(:, 32) m1; gold_codes(:, 33) m2;每次循环移位得到的Gold码就是一条新码分配给一个新用户。实际系统里还会做扰码加扰把小区间的干扰也随机化但仿真里Gold码本身已经够用加扰不是必需项。2.3 扩频和调制每个用户的数据怎么“铺”到码片上扩频在数学上就是数据比特和扩频码逐位相乘。一个数据比特d乘上长度为N的扩频码c得到的N个码片就是这个比特的扩频结果。多个用户的码片序列直接相加就合成了基站端发送的复合信号。function tx_signal spread_bits(bits, code) % bits: 行向量取值1/-1 % code: 列向量长度N N length(code); tx_signal bits * code; % 外积展平 tx_signal tx_signal(:); end这道乘法做完信号带宽就展宽了N倍。BPSK调制在基带等效模型里就是不做任何处理直接把码片序列当成复基带信号。如果你非要用真实载波做个带通仿真那就在码片序列上乘以cos(2pifc*t)但那个除了帮你验证频谱搬移之外对CDMA核心逻辑没任何贡献我建议初学阶段别干这事等整套基带链路通了你再玩带通。3. 信道模型加噪声的时机和方式会决定仿真可信度通信系统仿真里最让人头疼的不是发射机和接收机逻辑而是噪声怎么加。以前我见过很多同学把噪声直接加到扩频前的比特上那等于是在模拟一个完全没有扩频的系统处理增益的优势完全体现不出来。正确的做法是把AWGN加到信道中的码片级信号上。3.1 从EbN0到码片信噪比的换算公式代码里最需要仔细处理的就是EbN0每比特能量与噪声功率谱密度之比和实际施加在码片上的噪声方差之间的换算关系。这里有个关键点BPSK调制的BER与EbN0一一对应而扩频不会改变这个对应关系扩频只会让你在更低的信噪比环境下工作而已。推导过程是这样码片能量Ec Eb / N其中N是扩频因子。噪声单边功率谱密度为N0那么码片信噪比Ec/N0 (Eb/N0) / N。在MATLAB里生成复高斯噪声时噪声方差 N0/2实数部分所以EbN0_dB 6; % 示例 EbN0 10^(EbN0_dB/10); EcN0 EbN0 / spreading_gain; noise_variance 1 / (2 * EcN0); noise sqrt(noise_variance) * randn(1, length(signal));这里有个我踩过两次的坑如果你用复基带信号噪声的实部和虚部每部分都要加方差为noise_variance的高斯噪声最终总的噪声功率就是noise_variance。如果你只加了实部等效噪声功率就减半测出来的BER会比理论值好3dB看起来好像比理论还牛实际上是仿真错了。3.2 多径衰落先不做不等于以后不做很多教程干脆就不碰多径直接在AWGN下跑完收工。多径衰落对CDMA的影响其实非常大因为CDMA本身有抗多径的能力——RAKE接收机就是专门为CDMA设计的。但RAKE接收机实现复杂度上了一个台阶如果你第一次写CDMA仿真我建议分两步走第一步先把AWGN下的链路跑通把各模块的逻辑验证完第二步再验证RAKE接收机、多径信道模型这些进阶内容。我在这篇文章里的完整代码就聚焦AWGN和单用户/多用户场景多径部分我会在文章末尾给扩展思路。4. 接收端实现相关解扩和比特判决接收端的设计思想和发射端完全对称——先用本地Gold码对接收信号做相关运算然后判决。但是这里面藏着整个CDMA系统最关键的一个细节同步。如果收发两端的码相位对不齐相关峰就出不来解扩结果直接就是一坨噪声。4.1 为什么相关解扩能把信号“捞”回来接收信号r(t)里包含目标用户的码片信号、其他用户的干扰以及噪声。解扩的本质是拿本地码片和接收信号做内积[ \hat{d} \frac{1}{N}\sum_{k1}^{N} r(k) \cdot c_j(k) ]如果当前要解的用户是用户j而接收信号里第i个用户的贡献是d_i * c_i(k)那么内积之后就变成[ \frac{d_i}{N}\sum_{k1}^{N} c_i(k)c_j(k) \frac{1}{N}\sum_{k1}^{N} n(k)c_j(k) ]理想情况下自相关项在ij时等于N互相关项在i≠j时接近0噪声项由于扩频码的随机性也被摊薄了。这就是CDMA能在同频段内区分用户的原因也是“处理增益”的来源。4.2 逐比特解扩的MATLAB实现解扩的代码实现最直观的方式就是逐比特循环处理。把接收信号按扩频码长度N切成一段段每段和对应的本地码做点乘再求和然后除以N取判决。function rx_bits despread_bits(rx_signal, code) N length(code); num_bits floor(length(rx_signal) / N); rx_bits zeros(1, num_bits); for k 1:num_bits segment rx_signal((k-1)*N 1 : k*N); decision_var sum(segment .* code); rx_bits(k) sign(decision_var); end end注意sign(decision_var)只适用于BPSK。如果你以后扩到QPSK判决就得分I/Q两路分别做还得考虑相位旋转的问题。4.3 一个比特都不差为什么我建议每一步都留个中间变量写这种多模块仿真的一个血泪教训是调试的时候如果你没有留中间变量一旦最终BER对不上你完全不知道是哪一级出了错。我自己的习惯是在每个关键节点保存一份数据发射端扩频前的原始比特tx_bits扩频后的码片序列spread_signal加噪后的接收信号rx_signal解扩后的判决变量decision_var恢复出的比特rx_bits调试的时候先看decision_var的分布而不是光看BER。如果判决变量的星座图是正负两坨干净的云团说明链路没问题如果云团旋转了90度那大概率是载波同步或者相位问题如果云团挤在一起分不开说明信噪比太低或者扩频码没用对。5. 完整可跑的仿真代码单用户到多用户一步到位我把自己平时用的一个最简可扩展的DS-CDMA仿真框架贴出来它支持任意用户数、任意Gold码数量、任意EbN0扫描跑完直接画出BER曲线和理论BPSK曲线做对比。%% DS-CDMA 基带仿真框架 (AWGN信道) clear; close all; clc; rng(42); %% 系统参数配置 num_users 4; % 用户数 N 31; % 扩频因子Gold码长度 num_bits 2000; % 每个用户仿真比特数 EbN0_dB_list 0:2:12; % 信噪比扫描范围 %% 生成Gold码序列族 poly1 [1 0 1 0 0 1]; poly2 [1 1 0 0 1 1]; init1 ones(1, 5); init2 ones(1, 5); m1 generateMSeq(poly1, init1, N); m2 generateMSeq(poly2, init2, N); gold_codes zeros(N, num_users); for u 1:num_users m2_shift circshift(m2, u-1); gold_codes(:, u) m1 .* m2_shift; end %% 生成所有用户的数据 tx_bits_all (rand(num_users, num_bits) 0.5) * 2 - 1; %% 主仿真循环 ber_results zeros(size(EbN0_dB_list)); for idx 1:length(EbN0_dB_list) EbN0_dB EbN0_dB_list(idx); EbN0 10^(EbN0_dB/10); EcN0 EbN0 / N; noise_variance 1 / (2 * EcN0); total_bit_errors 0; total_bits 0; for bit_idx 1:num_bits tx_signal zeros(1, N); for u 1:num_users tx_signal tx_signal tx_bits_all(u, bit_idx) * gold_codes(:, u); end noise sqrt(noise_variance) * randn(1, N); rx_signal tx_signal noise; % 解调用户0的数据 decision_var sum(rx_signal .* gold_codes(:, 1)); rx_bit sign(decision_var); if rx_bit ~ tx_bits_all(1, bit_idx) total_bit_errors total_bit_errors 1; end total_bits total_bits 1; end ber_results(idx) total_bit_errors / total_bits; end %% 画图 figure; semilogy(EbN0_dB_list, ber_results, bo-, LineWidth, 1.5); hold on; theory_ber qfunc(sqrt(2 * 10.^(EbN0_dB_list/10))); semilogy(EbN0_dB_list, theory_ber, r*-, LineWidth, 1.5); grid on; xlabel(Eb/N0 (dB)); ylabel(BER); legend(DS-CDMA 仿真 (4用户), BPSK 理论值); title(DS-CDMA 与理论BPSK性能对比 (AWGN, Gold码));这个框架我把解扩过程放在bits循环里逐比特处理看起来有点笨但优点是逻辑一目了然特别适合教学和调试。等这个跑通了你再改造成矩阵化批量处理也不迟。5.1 仿真结果的正确打开方式跑完这段代码你会发现一个有意思的现象单用户场景下DS-CDMA的BER曲线和BPSK理论曲线几乎完全重合。这是对的因为扩频本质上不改变AWGN下的性能它只是让信号更抗干扰、更抗截获。如果哪天你发现单用户BER比理论差很多那肯定是解扩逻辑或者噪声功率算错了。多用户场景下BER会在高信噪比时出现一个平台效应这叫误码地板error floor原因是多用户干扰不可能完全消除Gold码的互相关不为零。这个平台的位置取决于用户数和扩频码的互相关特性。想压低地板要么换正交性更好的Walsh码要么加多用户检测器比如MMSE检测。5.2 处理增益对系统的影响改一个数字你就懂了我建议你把N从31改成63再跑一遍。你会看到两件事第一高信噪比区域的误码地板明显降低了因为码更长互相关更小第二相同EbN0下的BER略有改善因为解扩后噪声被平均得更好。但与此同时同样的数据速率需要占用两倍的带宽。这就是扩频通信最核心的trade-off带宽换性能性能换容量。6. 常见问题与调试心得那些MATLAB新手一定会踩的坑这个仿真框架从0到能跑通我自己也修过无数遍。有些问题你光看报错信息根本意识不到得自己踩过才知道怎么回事。我把最常见的几类问题整理在这里说实话这些问题比理论推导更值钱因为当年我就是在这些坑里浪费了好几天的。6.1 序列长度错位扩频码和码片对齐问题最常见的错误是发射端把N个码片当成1个比特来扩接收端又按错误的分割窗口来解扩导致判决变量完全没意义。这种错误的表现是BER在低EbN0时接近0.5纯瞎猜在高EbN0时也不下降因为信号根本没对齐。调试方法就是打印decision_var的分布正常情况下应该是两个峰如果只有一个零附近的峰那基本就是同步错了或者本地码用错了。6.2 噪声功率算错BER好得离谱其实是假象前面提到过复基带仿真只加实部噪声会让等效噪声减半BER好3个dB。这类错误很有欺骗性因为仿真曲线形状完全正常只是左移了。我的经验是每次跑完先对比理论BPSK曲线在单用户场景下仿真曲线应该和理论曲线重合如果偏了1~3dB先怀疑噪声功率再怀疑同步。6.3 Gold码生成出来全是0或全是1反馈多项式没选对如果你手写的LFSR生成序列全是0大概率是初始状态全0导致的。线性反馈移位寄存器初始状态不能全0否则状态永远停留在全0。另外任意两个m序列异或不一定是Gold码必须是“优选对”它们的互相关函数才有界。如果选错多项式对生成出来的码字互相关可能很高多用户性能会差很多。6.4 功能验证的正确姿势先逼死单个用户再放开多用户我强烈建议你在做多用户之前先把用户数改成1把Gold码固定成一条然后把信噪比调到10dB以上跑通整条链路确认无误后再加大用户数。我说句不好听的很多人一上来就搞8个用户结果谁都没解对最后发现单用户也解不对白白浪费了调多用户的半天时间。6.5 提升效率的小习惯仿真代码如果逐比特循环跑速度会非常慢尤其是扩频因子大、用户多、EbN0扫描点多的时候。等你调试通过之后建议把内层循环向量化。核心思想是用矩阵运算代替循环所有用户的扩频码排成一个矩阵所有用户的数据比特也排成矩阵一次性算出所有码片。% 向量化发射端示例 % gold_matrix: N x num_users每列一个用户的扩频码 % bits_matrix: num_users x num_bits spread_all gold_matrix * bits_matrix; % N x num_bits每列是一个时间点的所有码片累加这个矩阵化之后速度能提升一两个数量级但调试起来就不那么直观了。所以我建议调试用循环版确认正确后再改向量版做蒙特卡洛统计。7. 多用户干扰和功率控制把仿真往工程应用推一步完成了基础仿真之后下一步就是把问题往真实系统的方向推。我周围不少朋友拿到代码跑通AWGN后就觉得大功告成了但真正做工程的人知道AWGN只是起点而不是终点。7.1 远近效应为什么CDMA系统必须做功率控制CDMA里有个著名的“远近效应”问题。如果一个离基站近的用户的发射功率远大于一个离基站远的用户那么接收端在解调远处用户信号时近处用户的强信号会形成巨大的干扰。这个干扰在功率差超过几十dB时会让远处用户完全无法通信。在我们的仿真框架里这种效应可以通过给不同用户设置不同的发射功率来模拟。比如把用户1的幅度设为10其他用户幅度设为1然后看用户1还能不能解出来。你会发现基站的自动增益控制都救不了这种场景必须靠闭环功率控制把每个用户到达基站的功率校准到同一水平。7.2 加个功率控制模块性能立刻变好在仿真里做开环功率控制很简单根据用户的路径损耗把发射端乘以一个距离相关的衰减因子然后调制发射功率保证接收端功率一致。你可以试两个对照组一组所有用户功率相等另一组按距离衰减但无补偿。后者的BER曲线会惨不忍睹这就从数据上验证了功率控制的重要性。7.3 从单小区到多小区的扩展思路再往前走一步就是多小区干扰仿真。多小区CDMA的核心矛盾是导频序列规划和邻区干扰。你可以给每个小区分配不同的Gold码偏移或扰码然后模拟用户在小区边界处同时收到两个基站的信号。这个仿真跑通之后你才算是真正理解了CDMA网络规划里那些扰码规划参数是怎么来的。我的建议是把这个基础框架留着每研究一个特性就在上面加一个模块而不是每次都从零开始。我自己就是从这套代码一路改到了带信道编码、交织、RAKE接收的完整链路整整用了一个学期的课设最后拿了个优秀。这就是代码可扩展性的价值也是我坚持框架化组织仿真逻辑的原因。最后说点实在的MATLAB跑通信仿真最怕的不是代码写不出来而是跑出来之后你不知道它为什么对、也不知道它为什么错。建议你把每个模块单独验证把中间变量打印出来观察不要上来就闷头跑全链路。这一套流程走熟了你看到BER曲线心里基本就有数知道哪个环节是瓶颈哪里还能榨出性能来。
返回列表