ARTICLE DETAIL

资讯详情

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

用MATLAB从零搭建DS-CDMA仿真链路:扩频码、解扩与误码率分析

用MATLAB从零搭建DS-CDMA仿真链路:扩频码、解扩与误码率分析 前阵子有个实验室的学弟问了我一个问题3G都退网了还折腾DS-CDMA干嘛我当时回了他一句CDMA这套“扩频码分”的思路从来没消失它藏在Wi-Fi的CCK/DSSS模式里藏在GPS卫星信号里也藏在各种抗干扰通信系统里。用MATLAB把DS-CDMA完整折腾一遍等于亲手把发射机、信道、接收机这条链路结构过了一遍很多通信课本上“看了一百遍还是模糊”的概念跑完仿真就全通了。这篇直接上干货用MATLAB搭一个DS-CDMA通信系统。说白了就是让多个用户共享同一频段靠不同的扩频码区分彼此。全程不需要额外的通信工具箱基础版MATLAB加几个随机数函数就能跑。代码我按分步实现逻辑拆开讲从扩频码生成一直写到误码率统计中间会穿插我自己踩过的坑和参数调整心得保证看完你也能复现出完整的仿真链路。1. 为什么是“码分”而不是“频分”DS-CDMA的底层逻辑1.1 频谱共享的核心机制传统的FDMA是把一条大路切成好几条车道每辆车占一条道TDMA是大家轮流上路你走一秒我走一秒。而CDMA完全不同——所有车同时挤在一条道上但每辆车有自己的“车牌”接收端只认自己那辆车的车牌其他车都被当成背景噪音。这个“车牌”就是扩频码PN码伪随机噪声序列。通信过程可以写成这个形式发射端把数据比特$d_k(t)$乘上一个高速率的扩频码$c_k(t)$得到扩频信号$s_k(t)d_k(t)\cdot c_k(t)$。接收端收到的是所有用户信号的叠加加噪声$$r(t)\sum_{i1}^{K} d_i(t)\cdot c_i(t) n(t)$$想解出第$j$个用户的数据就用第$j$个用户的本地扩频码$c_j(t)$做相关运算。因为扩频码设计成“自己与自己相关高、自己与别人相关低”所以乘完$c_j(t)$再积分只有$d_j(t)$那一路被放大其他用户的路数被压到接近零。这就是标题里说的“靠不同扩频码区分彼此”的本质。我习惯用一个生活化的类比解释给新手听一群人在一个房间里七嘴八舌你想听张三说话就把自己的听力模式调成“张三的口音频道”其他人嘴里吐出来的话在你耳朵里自动变成嗡嗡的背景声重点信息还是能提取出来。扩频码就相当于每个人的“口音模式”。1.2 处理增益扩频带来的最直观好处扩频码的码片速率$R_c$比数据比特速率$R_b$高很多倍这个倍数$N R_c / R_b$叫处理增益。扩展后每个比特被切成了$N$个码片接收端相关器把这$N$个码片的能量叠加到一起等效于把信噪比抬高了$10\log_{10}N$分贝。比如代码里常用的$N31$处理增益就是$10\log_{10}31 \approx 14.9\text{ dB}$。这意味着即使信号淹没在干扰里只要干扰不是同码结构的解扩之后信号依然能“考”出来。这也是扩频通信抗干扰、低调性低功率谱密度的数学根源。处理增益还有个工程含义CDMA系统里用户容量不是硬性固定的。每多一个用户总干扰功率多一点其他用户的信噪比就降一点误码率就劣化一点。这就是所谓“软容量”是CDMA区别于FDMA/TDMA的一个重要特性。仿真里加大用户数$K$观察误码率曲线往上飘你就能直观感受到软容量的压力。1.3 m序列与Gold码扩频码的两种主流选择MATLAB实现里最常用的扩频码有两种m序列和Gold码。m序列是线性反馈移位寄存器LFSR生成的最长线性反馈序列。$r$级寄存器生成的m序列周期是$2^r-1$例如5级寄存器的m序列周期是31。它的自相关函数很漂亮完全对齐时出现峰值1错开一个码片就掉到$-1/31$。这个尖锐的自相关特性是做码同步检测的基础。m序列的缺点是互相关性能不稳定。两个不同的m序列之间有的组合互相关旁瓣大有的组合勉强能忍。直接拿多个m序列给多个用户当码多址干扰可能失控。Gold码就解决了这个问题。Gold码由两个“优选对”m序列逐比特异或生成它的互相关函数值被限制在一个很小的范围里不会出现夸张的尖峰非常适合多用户场景。生成方法也简单两个同阶m序列其中一个做循环移位每移一位异或一次就得到一个新的Gold码这样一族能产生$2^r1$个Gold码。仿真时6个用户完全够分。这段生成代码在第3节给这里先把选型理由说透——选Gold码不是因为m序列不能用而是因为多用户场景下Gold码的互相关上界可控链路易调。2. MATLAB仿真前的参数设计拍脑袋之前先算一笔账2.1 码长、用户数与比特数的约定仿真不是上来就写代码先把参数定清楚后面90%的麻烦都能避免。我用这组参数跑通了整条链路推荐你也从这组开始参数值说明处理增益 $N$31码片数/比特取经典值方便验证自相关用户数 $K$6不超过Gold码族规模够体现多址干扰每用户比特数 $n_b$5000足够统计误码率又不会让矩阵爆炸码片速率 $R_c$1归一化基带模型简化为一个码片一个样本调制方式BPSK最直观便于理解扩频解扩信道AWGN先不加衰落把核心机制跑通再扩展为什么处理增益选31而不是6331已经能体现扩频增益Gold码族有33个可用码6个用户绰绰有余。63的处理增益更高但每比特多了一倍码片运算量初学者排查问题时会觉得矩阵形状更容易看花眼。先用31把逻辑跑对再上63对比性能还是那句话先对后快。为什么不把用户数设成跟处理增益一样多因为Gold码的互相关虽然被压住了但用户数越接近码族规模互相关干扰越聚积。仿真里K6时6用户BER曲线会比单用户明显抬起来这个差异正好用来演示“软容量”的影响。2.2 仿真链路与代码架构设计整条链路按这个顺序组织代码模块每一段都建议写成函数或独立脚本段方便单步调试码生成m序列、Gold码构造与自相关/互相关验证发射端信源比特 → 比特扩展每比特重复N次→ 与扩频码相乘 → 多用户合路信道加高斯白噪声按给定$E_b/N_0$换算噪声功率接收端解扩相关运算→ 硬判决 → 误码率统计性能对比不同用户数、不同$E_b/N_0$下的BER曲线在MATLAB里我建议全用矩阵和向量化不要写逐比特for循环。这不是矫情而是因为当比特数5000、码片数达到155000时for循环的耗时和内存跳变会让你的调试耐心瞬间归零。向量化写法在可读性上也更接近通信原理的数学表达式——你看代码就能对照公式哪里写错了一眼能定位。2.3 Eb/N0与噪声功率的换算这是新手最容易翻车的地方我见过太多BER曲线差出3dB的人最后都栽在噪声功率换算上。原理其实很简单我们只需要在基带模型里把每个码片当成一个样本码片幅度为$\pm1$那么每码片能量$E_c1$每个比特扩展成N个码片每比特能量就是$$E_b N \cdot E_c N$$给定目标$E_b/N_0$转换成线性值噪声的单边功率谱密度就是$$N_0 \frac{E_b}{10^{(E_b/N_0)_{dB}/10}}$$在实基带BPSK模型里噪声方差取$N_0/2$。这样加噪代码就是三行Eb N; % 每个比特的能量 N 个码片 N0 Eb / (10^(EbN0_dB/10)); % 目标 Eb/N0 对应的噪声谱密度 noise sqrt(N0/2) * randn(1, nb * N); % 实高斯白噪声有两点必须提醒很多教材在带通模型里写噪声功率和带宽、采样率有关那是另一个体系。本文用的是基带等效模型码片速率归一化为1采样率就是码片速率所以噪声样本方差直接用$N_0/2$不要额外乘采样率。如果你用的是MATLAB的awgn函数得先搞清楚它内部默认的SNR定义否则曲线对不齐理论值。加噪时randn生成的序列长度必须和信号等长是nb * N不是nb。我第一次写的时候在这漏了一个N结果解扩后噪声方差完全不对BER曲线直接掉进0.4以下查了整整一下午。3. 发射端链路搭建从二进制比特到基带信号3.1 用LFSR生成m序列再构造Gold码先写一个通用的m序列生成函数输入抽头位置和初始寄存器状态输出0/1序列。寄存器状态初始值不能全0否则移位寄存器永远吐不出1序列就死了。function seq mseq(taps, init, len) % taps: 反馈抽头索引例如 [5 2] % init: 初始寄存器状态如 [1 0 0 0 1] % len: 输出序列长度建议取 2^r - 1 seq zeros(1, len); reg init(:).; for k 1:len seq(k) reg(1); fb 0; for t taps fb xor(fb, reg(t)); end reg [reg(2:end), fb]; end end5级寄存器的m序列周期是$2^5-131$。我选了两个本原多项式对应的抽头组合一个用$[5\ 2]$另一个用$[5\ 4\ 3\ 2]$这两个是生成Gold码的优选对。然后对其中一个m序列做循环移位逐比特异或得到一族Gold码N 31; % 处理增益 K 6; % 用户数 codes zeros(K, N); base1 mseq([5 2], [1 0 0 0 1], N); base2 mseq([5 4 3 2], [1 0 0 0 1], N); for k 1:K shifted circshift(base2, k - 1); gold01 double(xor(base1, shifted)); codes(k, :) 2 * gold01 - 1; % 0/1 映射为 ±1 end这里有个细节扩频码最终要映射成$\pm1$序列因为BPSK调制下数据比特和码片相乘的结果只有1和-1两种值。0/1映射为±1之后自相关峰值从“码长”变成“码长”互相关旁瓣则在Gold码的界内浮动。建议你生成完先画一下自相关图看到0码片偏移处一根尖峰、其他位置都低于$3/31$左右说明码没问题再往后走。3.2 比特扩展与扩频调制扩频操作分两步先把每个比特重复N次再与对应的扩频码逐码片相乘。用repmat和reshape组合是最直观的向量化做法nb 5000; tx_all zeros(K, nb * N); % 预分配每用户一行 for k 1:K data_k 2 * (rand(1, nb) 0.5) - 1; % 随机±1比特流 spread_k reshape(repmat(data_k, N, 1), 1, nb * N); chip_seq repmat(codes(k, :), 1, nb); tx_all(k, :) spread_k .* chip_seq; endrepmat(data_k, N, 1)把每个比特纵向复制31份再reshape成一行得到的就是“每31个连续码片对应一个比特”的序列。repmat(codes(k,:), 1, nb)把用户的Gold码周期性地重复nb次长度正好对上。这里的核心逻辑是扩频码在比特周期内是整周期的。也就是说一个Gold码周期31码片恰好对应一个比特周期这样解扩时把31个码片求和正好把整个比特能量收回来。如果码长和比特周期不对齐解扩会引入可怕的码间干扰后面第5节我会专门说这个坑。3.3 多用户合路与“共享同一频段”的模拟信号合路很简单把K个用户的发射码片流逐码片相加tx_total sum(tx_all, 1);这就是标题里说的“多个用户共享同一频段”的直接物理模拟。默认情况下所有用户等功率发送每个用户发射功率相同。这个假设很理想现实基站会通过功率控制让到达接收端的各用户功率大致相等。但我们在仿真里先跑等功率是给后面的“近远效应”实验留一个对照组——当你把某个用户功率人为抬高6dB其他用户误码率立刻恶化这个对比会让你对功率控制的重要性刻骨铭心。发射端到这里就结束了。如果要做带通载波版本的扩展你只需要在上采样到足够采样率后乘上$\cos(2\pi f_c t)$接收端相干解调后再低通之后进相关器。但基带等效模型的本质没变扩频码、比特扩展、合路这些结构一字不改。4. 信道与接收端加噪、解扩、判决的完整回路4.1 AWGN信道与信噪比控制接收端收到的信号是所有用户的合路加噪声Eb N; % 每比特能量 EbN0_dB 0:2:10; % 仿真扫一遍 Eb/N0 ber_all zeros(K, length(EbN0_dB)); % 每用户一条BER记录 for idx 1:length(EbN0_dB) N0 Eb / (10^(EbN0_dB(idx) / 10)); noise sqrt(N0/2) * randn(1, nb * N); rx_total tx_total noise; % 下面进入接收处理... endEb N这个设置我第2节推导过这里就不重复了。只说一句如果你想模拟某个用户的信号功率特别大就在合路前给该用户那一行乘以sqrt(10^(delta_dB/10))而不是在合路后整体放大——合路后放大是给所有人提音量不是你想要的功率失衡效果。4.2 相关解扩把“找自己人”变成矩阵运算接收端解扩是所有步骤里最体现CDMA精髓的。数学上就是把收到的码片流按比特周期切成$n_b$块每一块31个码片与本地扩频码做内积点乘求和。向量化写法% 针对第 k 个用户解扩 rx_mat reshape(rx_total, N, nb); % 每列对应一个比特周期 y codes(k, :) * rx_mat; % 1×nb 判决变量 dec sign(y); ber mean(dec ~ data_k);这行矩阵乘法codes(k,:) * rx_mat干的事就是把这31个码片与本地码逐项相乘再求和得到一个判决值。我们来拆解一下为什么这个值能恢复出数据设目标用户信号为$d_k(t)c_k(t)$本地码是$c_k(t)$那么相关输出是$d_k \cdot \sum c_k^2 d_k \cdot 31$因为$c_k^21$信号部分获得了31倍的累积增益。再设另一个用户$i$的信号$d_i(t)c_i(t)$它和本地码$c_k(t)$做内积结果是$d_i \cdot \sum c_k c_i$。这个交叉项的大小由Gold码互相关决定被压在很小的范围内对目标用户相当于一份微弱的多址干扰。噪声部分更妙白噪声每个采样点是独立随机的31个采样点求和后噪声幅度只增长$\sqrt{31}$倍而信号增长31倍于是信噪比提升了$\sqrt{31}$倍功率域就是31倍这就是处理增益在接收端的实际体现。至于reshape的方向问题——MATLAB的reshape默认按列优先填充所以你要先把串行码片流rx_total变成N×nb的矩阵保证每一列恰好是一个比特周期的31个码片。如果你把维度写成nb×N那解扩出来的就是一堆垃圾数据。这个坑我踩过当时明明是同一套逻辑单用户BER曲线怎么都对不上理论值最后打印矩阵尺寸才发现方向反了。4.3 误码统计与理论BER曲线对比单用户BPSK在AWGN下的理论误码率是$$P_b Q(\sqrt{2E_b/N_0})$$MATLAB里没有直接封装Q函数但可以用erfc算ber_theory 0.5 * erfc(sqrt(10.^(EbN0_dB / 10)));把仿真结果和理论曲线画在同一张semilogy图上figure; semilogy(EbN0_dB, ber_theory, k-, LineWidth, 1.5); hold on; for k 1:K semilogy(EbN0_dB, ber_all(k,:), o-); end xlabel(Eb/N0 (dB)); ylabel(BER); grid on; legend(理论单用户, 用户1,用户2,用户3,用户4,用户5,用户6);我预期你看到的结果是单用户仿真时BER曲线几乎紧贴理论值多用户6条曲线整体高于理论值尤其在高$E_b/N_0$区域会出现一个“地板”怎么增加信噪比都压不下去。这个地板就是Gold码互相关泄露造成的多址干扰下限是CDMA系统容量的物理约束不是代码bug。能跑出这个现象说明你的链路结构完全正确。5. 仿真结果与踩坑记录理想指标背后藏着的现实问题5.1 多址干扰BER曲线为何压不到理论值当我把用户数从1调到6BER曲线肉眼可见地抬高了。这不是坏事反而是教学里最理想的现象。原因很简单Gold码不是严格正交的不同码之间互相关虽然小但6个用户的信号叠加起来每个用户的解扩器里都攒着其他5个用户贡献的残留能量。这个残留干扰有个特点它是“确定性”的不随高斯噪声平均消失。所以你提高$E_b/N_0$高斯噪声变小了但互相关干扰纹丝不动BER曲线就会在高信噪比区掉不下去。这就是CDMA的“多址干扰地板”。如果你加大$K$到10、15甚至接近31这个地板会越抬越高直到某一天你发现无论给多大信噪比误码率都卡在某个值附近。这时候你就理解了为什么CDMA系统需要精确的功率控制和用户数规划。5.2 近远效应的复现让一个用户功率突然高6dB我强烈建议你做一次这个实验把用户2的发射功率抬高6dB其他用户保持等功率然后观察用户1的BER变化。tx_all(2, :) tx_all(2, :) * sqrt(10^(6/10)); % 用户2 6dB tx_total sum(tx_all, 1); % 后续接收解扩代码不变但用户1的BER会显著恶化我第一次跑这个实验时用户1的BER直接从$10^{-3}$量级劣化到接近$10^{-1}$。原因是用户2的大功率信号经过与用户1扩频码的相关运算后互相关残留被放大了好几倍直接盖过了用户1的目标信号。这就是CDMA著名的“近远效应”——离基站近的手机如果不压低功率会把远处手机的信号彻底淹没。这也是为什么CDMA系统必须有快速闭环功率控制每毫秒级调整一次发射功率。做仿真时你可能觉得这是个理论概念跑完这个实验你会真正明白所谓“远近”不是地理距离问题是功率失衡问题。5.3 码片失步一个码片整个解扩就崩了扩频通信里同步是命根子。我用了一行代码做“失步”实验code_mis circshift(codes(1, :), 1); % 本地码落后1个码片 y code_mis * rx_mat; dec sign(y); % 误码率直接掉到0.5附近结果就是BER约等于0.5跟抛硬币一样。原因在于m序列/Gold码的自相关特性差一个码片相位相关值就从31掉到接近0。这个实验做完你就知道为什么所有CDMA接收机都要先做捕获、再做跟踪——没有同步锁定后面全是白算。我在这个环节还总结过一张避坑表都是新手容易踩的常见问题表现解决方法m序列寄存器初始化为全0扩频码全0解扩失效初始状态不能全0reshape维度方向写反BER完全对不上理论用N×nb每列一个比特周期噪声长度写成nb加噪量级不对BER诡异噪声长度必须是nb*NGold码选错抽头互相关大多用户地板过高使用优选对本原多项式本地码相位偏移BER接近0.5保证收发码相位严格对齐逐比特for循环仿真跑几分钟不出图全矩阵化一行解扩5.4 从单用户跑到多用户别跳级如果你打算自己从头跑一遍我建议你严格按这个递进顺序来先跑单用户把BER曲线和理论对上了再加到6个用户等6个用户的BER现象看明白了再去做近远效应和失步实验。跳级的结果就是某个环节出问题你根本说不清是码的问题、噪声换算的问题还是解扩方向的问题。单用户验证的意义特别大——此时不存在多址干扰仿真曲线应该在所有$E_b/N_0$点都和理论曲线贴合。如果这一步就对不齐优先检查噪声功率换算和reshape方向如果这一步对齐了多用户曲线略微高于理论是正常的不用慌。我在实际跑这个仿真时最大的体会是DS-CDMA看起来公式多、码片多、矩阵大但拆解下来就是“扩频码生成—扩展—相乘—合路—加噪—相关—判决”这七步。每一步的向量化代码都只有一两行真正的复杂度全在参数换算和相位对齐这些细节里。把细节捋顺了整个系统就像搭积木一样清晰。如果你也想亲手验证一下“靠不同扩频码区分彼此”这件事按这个链路从单用户跑起改改用户数、调调功率、偏移一下码相位每个实验都有对应的BER曲线变化等着你比翻十遍教材都来得直观。
返回列表