
先聊个实际的场景通信方向的研究生或者刚入行的基带工程师多少都听过DS-CDMA直接序列码分多址这个名字但课本上的扩频概念和真实能跑的波形之间总隔着一层窗户纸。我当年第一次在MATLAB里把DS-CDMA的收发链路完整搭起来时最大的感受是理解多址原理最快的方式不是反复看书而是亲手把扩频码生成出来把多用户信号叠加进同一个频带再把它解扩回来亲眼看看误码率曲线和理论值怎么贴合。这篇文章想做的事情很直接——带你把一个最小可运行的DS-CDMA通信系统仿真拆开从扩频码生成、发射端扩频调制、信道加噪到接收端相关解扩和误码率统计每一段都给出可以对着抄的MATLAB代码和背后逻辑。无论你是刚学通信原理的初学者还是需要快速搭链路验证算法的工程师这套实现思路都能直接用上。1. 先弄明白DS-CDMA靠什么区分用户1.1 码分多址的数学底子内积DS-CDMA通俗讲就是多个用户共享同一段频率谁也不让谁靠各自的“签名序列”来区分信号。这个“签名序列”就是扩频码。可为什么一组看起来乱糟糟的±1序列就能把你和别人的信号拆开这不是玄学说到底就是向量内积。假设用户A的扩频码是pn_A用户B的扩频码是pn_B两者长度都是Gp这个值就是扩频因子/处理增益。在接收端用户A如果想解出自己的信息只需要把接收信号和本地pn_A做内积。如果两个扩频码彼此正交或者接近正交那么pn_A和pn_B的内积约等于0B用户的信号在A用户的相关器里几乎不贡献输出而A用户自己的信号因为pn_A和自己的完美相关输出就累加出一个较大的值。反过来也是这个逻辑这就是码分多址的全部秘密指望扩频码的正交性来做信号分离。所以你看DS-CDMA系统的核心竞争力和瓶颈全在这一串码的设计上。用公式表示就是发射端各自信息比特与自己的扩频码相乘相当于每个比特被复制成Gp个码片再逐码片异或接收端接收信号与本用户扩频码的标准化内积再经过判决门限还原比特这里我建议把这个“内积累加”的过程亲手写出来一旦跑过一遍后面所有关于多址干扰、处理增益、伪随机序列的讨论都会变得异常直观。1.2 m序列生成与自相关特性做DS-CDMA仿真第一步是生成扩频码。最常用的基础码是m序列最大长度线性反馈移位寄存器序列。m序列之所以被重用最重要的原因是它的自相关特性非常好周期内的自相关函数是一个尖锐的脉冲在零偏移时很高其他偏移时非常低。这个性质保证了接收端同步后目标用户信号能被有效累加放大。m序列的本质是用线性反馈移位寄存器LFSR产生的周期序列最大周期为2^n - 1其中n是移位寄存器级数。触发反馈抽头的位置必须对应“本原多项式”否则序列周期会缩短。比如6级寄存器常用本原多项式可以是x^6 x^5 1对应抽头是第6位和第5位做模2加法反馈。下面这段MATLAB函数可以直接生成m序列function seq generate_mseq(taps, nReg) % 输入 % taps - 反馈抽头索引向量例如 [6 5] 表示 x^6 x^5 1 % nReg - 寄存器级数 % 输出 % seq - 长度为 2^nReg - 1 的 m 序列0/1 格式 N 2^nReg - 1; reg ones(1, nReg); % 寄存器初态避免全0 seq zeros(1, N); for k 1:N seq(k) reg(nReg); fb mod(sum(reg(taps)), 2); reg [fb, reg(1:nReg-1)]; end end调用方式很简单seq generate_mseq([6 5], 6); % 周期63的m序列生成的是0/1序列实际在通信系统里要把它换成双极性形式也就是0映射成1、1映射成-1或者反过来先定义清楚就行。这一步看似简单其实很多人在后面解扩时符号正负搞反导致判决结果全部翻转。1.3 用户一多就得用Gold码m序列虽好但有个尴尬的问题同一长度下可选的m序列数量有限而且不同m序列之间的互相关在某些相位组合下可能很差无法保证多个用户共存的稳定性。这个问题在DS-CDMA里是致命的因为用户之间靠的就是扩频码的区分能力。解决办法是用Gold码。Gold码的原理很巧妙取两个优选m序列把其中一个做不同的循环移位再和另一个逐位模2加每换一个移位量就得到一个新的Gold码。这样可以从一对m序列生成2^n - 1个Gold码序列数量大幅增加而且互相关特性被控制在一个固定上界内稳定得多。MATLAB生成Gold码的做法如下m1 generate_mseq([6 5], 6); % 优选对1 m2 generate_mseq([6 1], 6); % 优选对2 N length(m1); goldCodes zeros(N, N); % 每一行是一个Gold码0/1格式 for shift 0:N-1 goldCodes(shift1, :) mod(m1 circshift(m2, [0 shift]), 2); end % 取前K个作为K个用户的扩频码 K 4; pn_bits goldCodes(1:K, :); pn 2 * pn_bits - 1; % 转为双极性 ±1这里用了一个关键事实6级m序列周期是63则Gold码长度也是63。Gold码的互相关不再是理想的0而是有一个确定的最大值这个值在理论上是与序列长度相关的。实际仿真中你不需要背公式只需要意识到Gold码的互相关足够小小到多用户干扰可接受。2. MATLAB仿真结构这样搭后面能少走弯路2.1 整条链路的模块划分开始写代码之前先把系统级架构想清楚。DS-CDMA基带仿真说白了就是一条“发射机-信道-接收机”的链路但如果你一头扎进代码几个小时后往往发现自己顺序乱了不知道哪里该扩频、哪里该加噪。我习惯把代码拆成四块参数配置区用户数、扩频因子、信息比特数量、Eb/N0范围等全部集中放在最前面。发射机模块生成每个用户的信息比特、扩频调制、BPSK映射、多用户信号叠加。信道模块这里先只做AWGN后面可以扩展成多径、衰落信道。接收机模块本地扩频码相关解扩、判决、误码率统计。这四块的变量从前往后流动每一块的中间变量都用清晰的命名比如tx_per_user代表每个用户的扩频后码片序列tx代表叠加后的总发射信号rx代表接收信号。这样Debug时你能快速定位是哪一段出了问题。2.2 参数选择和仿真规模设计参数怎么定经常是新手最懵的地方。我建议一组起步参数参数取值说明用户数 K4先跑2、4、8逐渐增加扩频因子 Gp63和m序列周期一致处理增益约18 dB信息比特数 Nbits2000~5000太少BER曲线抖动很大蒙特卡洛次数10~50用多次平均来平滑误码率曲线Eb/N0范围0~10 dB覆盖理论BER曲线的关键区间这里最关键的是信息比特数和蒙特卡洛次数的关系。DS-CDMA的BER统计和普通BPSK不太一样如果你的样本数太少误码率为10^-3这个量级时可能根本测不到错误比特曲线看起来就是0没法跟理论值对比。所以宁可把Nbits设大一点后面画曲线才像样。2.3 关于仿真方式和工具箱的一句话建议有同学会问用通信工具箱里的现成对象不香吗香但我不建议从工具箱开始。DS-CDMA这门课的真正难点在于理解扩频和解扩的含义用底层的矩阵乘法自己写一遍你才真正明白信号是怎么被埋到噪声里、又是怎么被捞回来的。等你把下面的核心代码跑通了再去替换成comm.PNSequence之类的工具箱对象就非常轻松。3. 发射机端核心代码扩频、调制、复合信号3.1 信源与扩频调制信息比特直接随机生成就行重点在扩频调制这一步。扩频的本质是一个信息比特被Gp个码片替代如果信息比特是1则发射的码片序列就是扩频码本身如果信息比特是-1则发射的是扩频码的反相。用表达式表示就是c(t) b * pn(t)。在MATLAB里实现这一步最直观的就是循环每个用户每个比特逐个乘。但代码写多了你会发现在MATLAB里能用矩阵运算就别用循环后面跑多用户、多信噪比点时效率差太多了。这里给一个矢量化的实现% 参数区 K 4; % 用户数 Nbits 4000; % 每个用户的比特数 Gp 63; % 扩频因子 % 每个用户双极性信息比特 data randi([0 1], K, Nbits); b 2 * data - 1; % 已生成好的双极性扩频码 pn尺寸 K×Gp spread zeros(K, Nbits * Gp); for u 1:K % b(u,:). 是 Nbits×1乘 pn(u,:) 得 Nbits×Gp 矩阵 % reshape 按列顺序排成 1×(Nbits*Gp)即每个比特跟着Gp个码片 spread(u, :) reshape(b(u,:). * pn(u,:), 1, []); end这段代码理解起来有几个小技巧b(u,:).乘以pn(u,:)得到的是一个Nbits×Gp矩阵第i行第j列就是第i个比特与第j个码片的乘积reshape按列取值正好把每一行的Gp个值依次排出来形成连续码片流。如果你不喜欢这种矢量化写法先用双循环实现、确保理解正确再优化也不迟。3.2 BPSK基带映射与多用户信号合并扩频后的码片序列还是双极性±1的数值这本身就是BPSK基带信号。这里不需要额外做载波调制因为我们要仿真的是等效基带。如果你想把仿真扩展到频域观察频谱扩展效果可以在基带数据上再做一次脉冲成型滤波但在误码率仿真这个层面直接处理离散基带码片就够了。多用户发送信号就是把每个用户的码片序列直接相加tx sum(spread, 1);这就是DS-CDMA的“Air接口叠加”。每个用户的信号功率相同、中心频率相同、带宽相同在时域上就是简单加法它们完全混叠在一起在频谱上根本看不出来谁是谁。这也是DS-CDMA最迷人的地方——信号看不见只能靠码字认人。3.3 为什么要设置单用户功率为1而不是随便填很多教程里直接给信号乘以某个功率值但在这个仿真里每个码片直接是±1意味着每个码片能量Ec等于1每个比特能量Eb等于Gp。这个关系非常重要因为它决定了后面Eb/N0和噪声方差怎么换算。如果你在发射端随手乘一个增益那后面噪声功率的计算就必须跟着调不然BER曲线会整体偏移好几个dB。所以我的建议是发端固定每个码片能量为1所有功率换算都围绕Ec1这个基准来展开。这样既能保证仿真一致性又能把信噪比换算讲得清清楚楚。4. 噪声功率换算与接收解扩这块是最容易翻车的4.1 Eb/N0、码片SNR和噪声方差先把账算明白我见过很多人在“加噪声”这一步翻车问题几乎都出在把一个东西搞混了信息比特信噪比Eb/N0和码片信噪比Ec/N0不是一回事。DS-CDMA里每个信息比特被扩成Gp个码片所以每个码片的能量Ec只有Eb的1/Gp换算成dB就是Ec/N0 (dB) Eb/N0 (dB) - 10*log10(Gp)这个10log10(Gp)就是处理增益也是扩频系统最核心的数值。以Gp63为例处理增益大约是18dB。也就是说即使每个码片深埋在噪声里码片SNR很低解扩后每个比特的信噪比依然可以恢复到一个可用的水平。在MATLAB的离散基带仿真中噪声方差的计算可以这样推导设码片能量Ec1信息比特能量EbGp。给定目标Eb/N0线性值那么N0 Eb / (Eb/N0) Gp / ebno_lin。对实数BPSK基带信号加性高斯白噪声的双边功率谱密度为N0/2在采样率等于码片速率时噪声方差就等于N0/2。所以EbN0_dB 6; ebno_lin 10^(EbN0_dB/10); N0 Gp / ebno_lin; noiseVar N0 / 2; noise sqrt(noiseVar) * randn(1, length(tx)); rx tx noise;为什么这里噪声方差要除以2因为在等效基带里实BPSK信号只有一个实数维度而噪声的功率谱密度在正负频都有等效到基带采样时同相支路的噪声方差是N0/2。这个细节决定你的BER曲线能否和理论值对齐多试几次你就能感受到差异。4.2 接收端用相关器解扩接收机的核心就是相关器把接收信号按比特长度分块每一块和本地扩频码做内积然后过零判决。MATLAB实现如下rx_bits zeros(K, Nbits); for u 1:K y reshape(rx, Gp, []); % 每列是一个比特对应的Gp个码片 corr_val pn(u,:) * y; % 矩阵乘法实现内积 rx_bits(u,:) corr_val 0; % 大于0判为1 end % 误码率统计 ber zeros(K, 1); for u 1:K ber(u) sum(data(u,:) ~ rx_bits(u,:)) / Nbits; end这里pn(u,:)是1×Gp行向量y是Gp×Nbits矩阵乘出来的corr_val是1×Nbits每个元素就是对应比特的相关输出值。在没有多用户干扰和噪声的理想情况下目标用户的相关输出是±Gp判决阈值取0就足够。4.3 码同步问题一个小实验实际的DS-CDMA接收机面临一个基本难题接收信号的码片起始时刻和本地扩频码的相位必须对齐否则相关器输出直接崩溃。这一点我在仿真里经常被初学者忽略因为大家都假设“接收端知道扩频码相位”但真实系统要经历捕获和跟踪两个阶段被称为码同步。为了让你直观感受一下码同步的重要性可以在接收端故意把pn码做循环移位再解扩试试相关输出的变化shifted_pn circshift(pn(1,:), [0 5]); % 故意移5个码片 corr_shifted shifted_pn * reshape(rx, Gp, []);你会看到相关输出幅度大幅下降判决错误率骤增。所以做接收机设计时不要只盯着AWGN下的性能先用这个简单实验建立直觉解扩前码同步是一切的前提。4.4 扩展两径信道下的RAKE接收思路如果只做AWGN仿真练手可以但离真实移动通信还有距离。多径场景是DS-CDMA最经典的战场因为扩频系统天然具备抗多径的能力只要多径时延差大于一个码片周期不同路径的信号在相关器里就近似不相关可以把它们当成可分离的多个分量。一个朴素的两径模型可以这样搭假设直射径幅度为1延迟径延迟d个码片、幅度为0.5则接收信号是直射径信号、延迟径信号和噪声的叠加。如果你只用一个本地扩频码做相关能量只 собрали了一部分性能不是最优的。改进思路就是RAKE接收机用多个相关器分别对准各条路径再把各相关输出按最大比合并。这个进阶方向可以先记在脑子里等你把AWGN的BER曲线跑顺了再往这个方向扩展。5. 跑BER曲线单用户验证、多用户干扰5.1 理论曲线与仿真曲线对照仿真链路搭好后第一件事是验证正确性。方法很简单先只跑单用户K1在AWGN信道下把BER曲线画出来和理论BPSK曲线对比。单用户DS-CDMA在AWGN下的性能和没扩频的BPSK理论一样因为扩频不改变AWGN条件下的误码率它只是把信号能量分散到更宽的频带上。普通BPSK在AWGN下的理论误码率公式是BER Q(sqrt(2 * Eb/N0))MATLAB计算可以用qfunc函数EbN0_dB 0:2:10; ebno_lin 10.^(EbN0_dB/10); ber_theory qfunc(sqrt(2 * ebno_lin)); semilogy(EbN0_dB, ber_theory, k-, LineWidth, 1.5); hold on; % 叠加仿真结果...仿真曲线和理论曲线如果贴合得很好说明你的扩频解扩链路、噪声功率换算全部正确。这一步是必须过的我强烈建议不要跨过它直接去做多用户否则到时候出错你根本分不清是扩频码问题还是链路问题。5.2 多用户SINR公式推导和实验多用户场景下目标用户解扩后的信号质量不仅取决于噪声还受其他用户的干扰影响。假设K个用户等功率目标用户解扩输出的信号分量幅度是Gp因为自相关而每个干扰用户经过相关器后由于扩频码互相关不为0会贡献一个随机干扰分量其功率约为Gp因为互相关输出等效于Gp个独立±1随机量相加方差为Gp。噪声部分方差为Gp N0/2按之前的定义经过相关器后噪声方差变为Gp倍的噪声方差。所以目标用户解扩后的信干噪比SINR近似为SINR Gp² / [ (K-1)Gp Gp·σ_n² ] Gp / [ (K-1) σ_n² ]其中σ_n²代表码片级噪声方差。这个公式透露了一个很本质的规律DS-CDMA是自干扰系统。当用户数K增加干扰项线性增长即使没有噪声多用户干扰也会限制系统性能。这个“干扰受限”特性是DS-CDMA区别于TDMA/FDMA的根本特征。验证方法很简单固定Eb/N06dB分别跑K1、K4、K8看目标用户BER如何随用户数变差。你甚至会看到当K接近Gp时系统彻底无法工作——这与理论预期一致。5.3 从结果反推CDMA的资源分配逻辑多用户BER结果出来后你就能理解为什么真实CDMA系统要做功率控制。如果某个用户离基站近、功率大它会对其余用户产生不成比例的强干扰这就是远近效应。仿真里所有用户等功率已经很理想化真实场景下没做功率控制的CDMA系统强用户可能直接把弱用户完全淹没。理解了这一点你再回头看IS-95、WCDMA里的闭环功率控制为什么如此重要逻辑就全通了。6. 踩坑记录我在跑DS-CDMA仿真时遇到的那些鬼问题6.1 噪声方差设置翻车BER曲线整体偏移有一次我把噪声方差直接设成N0而不是N0/2结果BER曲线整体往右偏了大约3dB怎么调都跟理论曲线差一段。后来逐项检查才发现问题出在忘记处理双边噪声密度。这个坑几乎每个做BPSK仿真的人都会踩一次而且它表现得很隐蔽因为曲线形状完全正确只是SNR平移了。排查思路是先把单用户BER和理论曲线画在同一张图上如果整体平移正好3dB多半就是噪声方差的系数问题。6.2 样本数不足导致BER曲线抖动成“毛线团”刚开始做多用户仿真时每个信噪比点就跑1000个比特结果3000比特里可能只出现几个错误比特甚至一个都没有BER曲线看起来像一堆乱跳的毛线团根本没法分析。解决办法是确保累计错误比特数足够经验值是至少100个错误比特否则统计置信度太低。如果你跑10^-4量级的BER点那就需要10^6个信息比特这也是为什么仿真代码能矩阵化就尽量矩阵化循环一多跑起来非常痛苦。6.3 reshape排序顺序错了码片全乱扩频调制里我用reshape把一个Nbits×Gp矩阵排成码片流这里的排序方向必须和接收端reshape(rx, Gp, [])的拆解方向严格一致。我第一次写的时候发射端用了按列排接收端却按行排结果接收端每个相关器窗口包含了前后两个相邻比特的码片误码率直接接近50%。排查这个小问题花了半小时后来我在发射端和接收端各自打印前几十个码片人工核对才定位出来。建议你写代码时把“发端拼接方式”和“收端切分方式”写在注释里避免过了几天自己都忘。6.4 m序列初相不同导致Gold码互相关变差Gold码的性能依赖那一对优选m序列的选取。最初我没在意随便找了一个m序列和另一个m序列组合结果生成的“Gold码”互相关非常大多用户BER明显恶化。后来换成本原多项式对应的优选对性能才正常。建议不要自己凭感觉选抽头直接验证一下生成序列的周期是不是2^nReg - 1以及互相关是否在合理范围内。6.5 忽略了脉冲成型直接拿矩形码片当基带波形这里做个提醒很多入门仿真直接用理想的矩形脉冲码片因为它简单BER结论也不受影响。但如果你关心信号的频谱形状就需要进一步加根升余弦滤波器做脉冲成型。DS-CDMA的扩频增益体现在频谱扩展上但矩形码片的频谱旁瓣衰减慢真实系统必须做频谱整形。这个扩展可以等你把基础BER链路跑通后再加别一开始就背上这个复杂度。6.6 仿真加速的建议多用户多信噪比点的BER仿真跑起来可能有点慢有个简单的优化办法外层循环用多个信噪比点内层不要逐比特循环而是生成一个较大的数据块一次处理完。矩阵化程度越高MATLAB跑得越快。另外如果只是验证算法逻辑不必把每个用户的比特数都拉到几十万先用几千比特看趋势确认正确后再跑大量比特拿平滑曲线。做这个DS-CDMA仿真最大的收获是终于把“码分多址”“扩频增益”“远近效应”这些概念全部落到了可见的曲线上。尤其多用户场景下当你看到用户数从1变成4再变成8误码率曲线一点点抬高那种对CDMA本质的理解不是看书能得到的。建议你先把单用户验证通了再慢慢往多用户、多径、RAKE方向扩展每一步改动都有对应的曲线变化可以观察。手边有MATLAB的话现在就可以把m序列生成那段代码粘贴进去跑一跑剩下的链路自然就接起来了。