MATLAB直接序列扩频通信系统仿真与参数排查
简介面向通信原理、扩频通信课程学习与MATLAB仿真实践的技术文档围绕直接序列扩频系统的建模、解扩与性能评估展开适合课程设计、实验复现及DSSS入门者参考。资源为1个doc文件压缩包约320KB属单篇实验报告型资料便于集中阅读和按章节检索。正文从直扩系统组成框图切入说明信息码与PN码模二加扩频、BPSK调制、接收端相关解扩的链路并比较m序列、GOLD序列、WALSH码与OVSF码的特点给出信源速率2kbps、扩频因子64、高斯信道下的仿真参数、部分MATLAB代码及误码率随信噪比变化的曲线归纳抗同频干扰、抗衰落和抗多径干扰等优势。已有143人学习可帮助读者理解处理增益与扩频因子的关系对照代码搭建仿真流程并通过调整参数观察BER变化为后续系统设计提供参考。1. 一份DSSS仿真文档拆开看它真正能帮你跑通哪条链路直接序列扩频DSSS的MATLAB仿真几乎是每个通信方向学生都会碰到的一道坎文档里公式推导齐全代码一跑不是维度报错就是误码率曲线反着走。这份基于MATLAB的直接序列扩频通信系统仿真设计把信源、PN码、BPSK调制、高斯信道、解扩判决和BER统计串成一条完整链路适合拿来做课程设计复现、补直扩链路实操或者给后续加窄带干扰、加多径做底稿。互联网上流传的同类文档不少但能把参数对账讲清楚的少这份材料的价值在于它保留了原始代码的粗糙感正好用来练排错。下面按“时间轴对账—PN码生成—调制解调—避坑—验证”的顺序拆。2. 先把时间轴钉死Rb、扩频因子与采样率的三方对账2.1 信源速率2kbps与代码里Rb0.25的矛盾从哪来这份文档最容易被跳过的是参数表。正文写“信源速率2kbps扩频因子64”代码里却是Rb0.25; Tb1/Rb;算出来比特周期4秒100个比特要跑400秒。2kbps对应的是0.5毫秒两者差8000倍。更麻烦的是时间轴单位tlinspace(-T/2,T/2,N)里T400xlabel却标t(ms)按秒算是400秒按毫秒算是0.4秒后面fc4e4和fs4000再一掺和整条链路的物理量纲就散了。这种“正文参数和代码参数各写各的”在课程设计文档里很常见。直接跑原代码图上能出几条波形但BER曲线没有物理意义因为扩频因子、码片速率、采样率三者对不上。合格的做法是先定信息速率和扩频因子再由每chip采样数推采样率而不是先拍一个fs再倒推。常见做法是Rb定在1kbps量级SF取2的幂nsamp取8或16fsRb*SF*nsamp。2.2 扩频因子64与每比特采样点数L400的关系原文里L400被当成扩频因子用但L是每比特采样点数不是每比特chip数。扩频因子SF是每比特对应的PN码chip数每chip再采nsamp个点所以LSF*nsamp。原文M100个比特、L400总采样点N40000然后fs4000相当于每比特400点、每chip多少点取决于PN码周期。代码里stage3的m序列周期只有7后面k_code又把每个chip重复4次凑到20000点算下来每比特实际50个chip和文档写的64、代码里m序列的7都不一致。三个数各说各话解扩时PN码和符号边界必然错位。我一般会在脚本第一行写死四个数Rb、SF、nsamp、Nbit然后让L、fs、fc全部由它们推出来。这样换参数时只改一处不会出现“改了下采样率忘了改滤波器截止频率”的血泪经验。2.3 载波频率fc与采样率fs的取值边界原文fs4000、fc4e4载波频率是采样率的10倍直接违反奈奎斯特。采样后的信号里cos(2*pi*fc*t)每步相位跳太多混叠成一团解调时乘同一个载波也恢复不出基带。正确约束是fs 2*fc并且fc要远大于扩频信号带宽约SF*Rb否则载波和扩频带宽重叠滤波分不开。一套自洽的参数可以这样取参数符号取值约束说明信息速率Rb1 kbps决定比特周期Tb1ms扩频因子SF64每比特64个chip每chip采样数nsamp8至少4建议8或16每比特采样点数LSF*nsamp512必须为整数采样率fsRb*L512 kHz满足fs≥2fc载波频率fc128 kHzfcfs/2且fcSF*Rb信息比特数Nbit1000影响BER统计精度对应初始化代码clear; close all; Rb 1e3; % 信息速率1 kbps Tb 1/Rb; % 比特周期1 ms SF 64; % 扩频因子每比特64个chip nsamp 8; % 每个chip的采样点数 L SF*nsamp; % 每比特采样点数512 fs Rb*L; % 采样率512 kHz fc 128e3; % 载波频率128 kHz小于fs/2 Nbit 1000; % 仿真比特数这段代码的逻辑是先锁信息速率和扩频因子再让采样率服从链路带宽。fc取128kHz既低于256kHz的奈奎斯特上限又比64kHz的扩频带宽高出一截后面用低通滤波提取基带时不会把载波泄漏留在带内。参数改动的顺序建议是先改SF再检查fc是否仍大于SF*Rb最后确认fs是否仍大于2*fc。3. PN码生成与扩频m序列抽头、Gold优选对和模二加的代码实现3.1 m序列的级数、抽头与初始状态m序列由线性反馈移位寄存器产生周期为2^n-1。stage3时周期是7stage6时周期是63stage7时周期是127。原文用stage3、抽头[1 3]、初始寄存器[1 1 1]代码里调了一个没有给出的mseq函数。自己补这个函数时抽头位置和移位方向要对齐不同教材的写法可能差一位跑之前先看输出是不是周期7且包含全部非零状态。一个可用的m序列生成函数如下function code mseq(stage, tap, init) % stage: 寄存器级数 % tap: 反馈抽头位置1-based % init: 初始寄存器状态长度等于stage N 2^stage - 1; reg init(:).; code zeros(1, N); for k 1:N code(k) reg(end); % 当前输出取最右位 fb mod(sum(reg(tap)), 2); % 抽头模二加 reg [fb, reg(1:end-1)]; % 右移并补反馈位 end end逻辑说明每次循环先取寄存器最右位作为输出再把指定抽头位置的比特模二加得到反馈位最后整体右移。参数说明stage决定周期长度tap决定本原多项式init不能全零否则寄存器锁死。stage7、tap[1 7]、initones(1,7)可以得到周期127的m序列。如果输出全是0或者周期不对先检查tap是不是本原多项式对应的抽头再检查移位方向是不是反了。3.2 Gold序列优选对与相对位移Gold序列由两个码长相同、码时钟相同的m序列优选对模二加构成改变相对位移就能得到不同的码。原文里goldseq(m1,m2,user)没有给实现补的时候注意user控制的是位移量不是重新生成m序列。常见写法是function code goldseq(m1, m2, user) % m1, m2: 两个等长m序列优选对 % user: 用户编号控制相对位移 shift mod(user-1, length(m1)); code mod(m1 circshift(m2, [0 shift]), 2); end参数说明shift是相对位移user1时不移位user每加1多移一位。Gold序列的自相关和互相关旁瓣比m序列可控地址数也多工程上做多址仿真时比纯m序列方便。但要注意两个m序列必须是优选对随便拿两条m序列模二加互相关峰值可能很高解扩时不同用户之间会串。3.3 扩频操作的维度检查扩频的本质是信息比特与PN码逐chip相乘。BPSK映射下比特0映射为-1比特1映射为1PN码的0/1也映射为-1/1然后逐位相乘。这里最容易翻车的是维度SF64但6级m序列周期是63差一个chip。直接拿63位PN码去扩64位每个比特的码相位都会漂移BER曲线会在某个信噪比之后出现地板。我一般用7级m序列截前64位每个比特重复同一段64位码% 生成信息比特并做BPSK映射 bits randi([0 1], 1, Nbit); symbols 2*bits - 1; % 7级m序列周期127截取前SF位作为每个比特的扩频码 pn_full mseq(7, [1 7], ones(1,7)); pn pn_full(1:SF); pn_seq repmat(pn, 1, Nbit); % 扩展到所有比特 % 每个chip对应一个信息比特逐chip相乘 bit_index ceil((1:Nbit*SF)/SF); spread symbols(bit_index) .* (2*pn_seq - 1); % 每chip重复nsamp次升采样到采样率 spread_upsample reshape(repmat(spread, nsamp, 1), 1, []);逻辑说明bit_index把1:Nbit*SF映射到每个chip所属的比特序号symbols(bit_index)把比特值展开到chip速率2*pn_seq-1把0/1码映射成±1相乘完成扩频。repmat(spread, nsamp, 1)生成nsamp行、每行是扩频序列的矩阵reshape按列展开后每个chip重复nsamp次长度正好是Nbit*L。参数说明SF必须和pn的长度一致nsamp决定每个chip的采样密度太小会导致滤波后波形台阶明显太大只是增加计算量。接收端必须用同一个pn_seq和同样的nsamp做解扩否则相关峰对不上。这里截取m序列前64位会破坏严格周期性但仿真里每个比特都用同一段码收发一致BER趋势不受影响如果要做多用户建议换成Gold序列或长度63的m序列并把SF改成63。4. BPSK调制、awgn信道与相干解调一条能出BER曲线的链路4.1 BPSK调制与载波相乘的采样点对齐扩频后的信号还是基带码片序列要搬到载波上。BPSK调制就是基带信号乘余弦载波。原文代码里变量名一会儿QPSK一会儿BPSK但实际只做了一路不用纠结名字。关键是时间轴t要和spread_upsample等长且步长是1/fst (0:length(spread_upsample)-1)/fs; carrier cos(2*pi*fc*t); tx spread_upsample .* carrier;参数说明t从0开始步长1/fs长度和扩频序列一致fc必须小于fs/2。如果t用了linspace但没对齐端点载波相位会在最后一个点跳变解调时乘同一个载波会出现相位不连续低通滤波后基带波形上有毛刺。我一般用(0:N-1)/fs这种写法端点不重复相位连续。4.2 awgn的SNR参数与measured模式awgn(x, snr, measured)的snr是信号功率与噪声功率之比单位dBmeasured表示先测x的实际功率再加噪声。原文直接把SNR(10)丢进去但SNR数组是1:1:15对应的是dB值不是线性值。更隐蔽的问题是tx的幅度不一定是1如果前面扩频或载波相乘改变了幅度measured会按实际功率算SNR的定义就偏了。稳妥做法是在加噪声前归一化信号功率或者直接用Eb/N0换算。对于BPSKSNR Eb/N0 10*log10(Rb/B)其中Bfs/2是噪声带宽。仿真里如果想看扩频增益可以先按采样点SNR扫描解扩积分后噪声功率下降等效Eb/N0会提高10*log10(SF)左右。我一般把扫描范围设在-15dB到0dB正好覆盖解扩前后的转折区。4.3 解扩、低通滤波与积分判决接收端先乘同一个载波做相干解调再低通滤波取出基带然后乘PN码解扩最后按比特周期积分判决。原文用了T2F和lpf两个没有给出的函数自己补的时候用butter加filtfilt最省事。截止频率不能按fc设要按扩频信号带宽设大约SF*Rb的1到2倍% 相干解调 rx_mix rx .* carrier; % 低通滤波截止频率取2倍扩频带宽 f_cut 2*SF*Rb; [b_lp, a_lp] butter(4, f_cut/(fs/2)); rx_lp filtfilt(b_lp, a_lp, rx_mix); % 解扩接收端使用与发送端相同的PN码 pn_upsample reshape(repmat(pn_seq, nsamp, 1), 1, []); rx_despread rx_lp .* (2*pn_upsample - 1); % 按比特周期积分并判决 rx_reshape reshape(rx_despread, L, Nbit); metric sum(rx_reshape, 1); rx_bits metric 0;逻辑说明rx_mix把载波搬回基带filtfilt零相位滤波去掉二倍频分量rx_despread用同一PN码做相关解扩reshape把每个比特的L个采样点排成一列sum完成积分metric0做硬判决。参数说明f_cut取2*SF*Rb是经验值太小会削掉扩频信号边带太大会把噪声放进来butter的阶数4够用阶数太高会增加瞬态filtfilt要求信号长度大于滤波器阶数的几倍Nbit*L通常几十万点没问题。注意reshape(rx_despread, L, Nbit)是按列排要求rx_despread的长度正好是L*Nbit。如果前面滤波或截断导致长度少几个点这里会直接报错。跑之前先length(rx_despread)L*Nbit检查一下。4.4 误码率扫描循环的写法单点SNR只能看波形要看性能得扫BER。循环里每次重新加噪声但滤波器系数、PN码、载波可以放在循环外省时间。统计误码时用sum(rx_bits ~ bits)/NbitNbit至少1000否则低BER段全是零曲线画不出来。snr_dB -15:1:0; ber zeros(size(snr_dB)); for k 1:length(snr_dB) rx awgn(tx, snr_dB(k), measured); rx_mix rx .* carrier; rx_lp filtfilt(b_lp, a_lp, rx_mix); rx_despread rx_lp .* (2*pn_upsample - 1); rx_reshape reshape(rx_despread, L, Nbit); metric sum(rx_reshape, 1); rx_bits metric 0; ber(k) sum(rx_bits ~ bits)/Nbit; end semilogy(snr_dB, ber, o-); xlabel(SNR/dB); ylabel(BER); grid on;参数说明snr_dB的范围要覆盖BER从接近0.5降到1e-3左右的区间扩频因子64时-15dB到0dB差不多如果曲线在某个点突然降到0说明Nbit不够增大到1e4再跑。semilogy画对数纵轴低BER段才看得清。循环里没有重新生成bits保证每次比较的是同一组比特否则统计的是随机误码率曲线会抖。5. 避坑与排查五个让仿真结果对不上的典型现场5.1 现象误码率随SNR升高反而变大现象很直观SNR从-15dB加到0dBBER不降反升或者一直在0.5附近打转。原因通常不是算法而是PN码对齐错了。发送端用pn_seq扩频接收端如果用了circshift过的码、或者reshape顺序和发送端不一致相关峰就偏了。另一个常见原因是filtfilt虽然零相位但reshape时把滤波器的瞬态也算进了积分区间第一个比特的判决被污染。解决在解扩前打印pn_seq(1:10)和接收端用的码前10位确认完全一致在reshape前把前L个点丢掉避开滤波器瞬态用isequal(pn_seq, pn_seq_rx)做一次断言。血泪经验是只要BER曲线形状不对先查码对齐再查滤波器最后才怀疑信道模型。5.2 现象解扩后波形还是高频振荡现象是rx_despread画出来仍然有密集的余弦波纹积分判决前信号没有回到基带。原因多半是低通滤波器的截止频率设错了。原文lpf(f, rf, fc/10000)把fc除以10000当截止频率量纲和归一化方式都不对。butter的Wn是归一化频率1对应奈奎斯特频率不是Hz。直接填f_cut会得到一个接近1的Wn滤波器几乎不滤。解决Wn f_cut/(fs/2)且f_cut取2*SF*Rb左右。如果f_cut接近fs/2说明采样率不够回去改nsamp。滤波后画一下频谱确认二倍频分量被压下去20dB以上再进积分。5.3 现象awgn报维度错误或SNR含义不对awgn报错通常是tx的行列向量和snr的维度不匹配或者tx是复数但没指定measured。更隐蔽的是SNR含义awgn默认按采样点功率加噪声如果tx的幅度是0.5实际SNR会比设定值低6dB。原文里BPSK信号除以了sqrt(2)功率不是1后面又直接用SNR(10)曲线和理论对不上。解决加噪声前做tx tx/sqrt(mean(tx.^2))归一化或者用awgn(tx, snr_dB, measured)并接受实际功率定义。要对比理论BER时把采样点SNR换算成Eb/N0换算时注意噪声带宽是fs/2不是Rb。5.4 现象PN码周期与符号边界错位现象是BER曲线在某个SNR之后出现地板降不下去。原因常见于SF64但PN码周期63每个比特的码相位比上一个比特偏移一位解扩时相关峰逐渐偏离积分窗口。原文stage3周期7代码里却想扩64k_code用mod(i,7)循环每个比特的码相位都在变接收端用固定码解扩自然对不齐。解决让length(pn)SF并且每个比特重复同一段pn。用7级m序列截64位或者把SF改成63用6级m序列。如果非要用完整周期把SF设成2^n-1不要设成2的幂。检查方法把pn_seq按SF分段看每一段是否完全相同。5.5 现象滤波器截止频率设错导致判决全错现象是metric全是正数或全是负数rx_bits要么全1要么全0。原因除了5.2的归一化问题还有filtfilt的瞬态没处理。filtfilt对长信号是零相位但信号开头和结尾仍有边缘效应如果Nbit很小瞬态占比大积分判决会被拉偏。另外butter的阶数太高、f_cut太低时群延迟虽然被filtfilt抵消但幅度衰减可能把信号压到噪声以下。解决Nbit至少1000f_cut取2*SF*Rbbutter阶数用4判决前检查mean(metric)如果接近0说明解扩没成功回去查PN码和载波相位。我一般会在循环里加一句if mean(metric)0.5 || mean(metric)-0.5, warning(判决门限异常); end跑长仿真时能提前发现翻车。6. 把仿真做成可复用的实验台参数化脚本与处理增益验证把上面的片段串成一个脚本后最有价值的不是那张BER图而是能快速改参数验证趋势。我一般把SF做成外层循环固定Rb和nsamp看不同扩频因子下的BER曲线怎么分开。处理增益的理论值是10*log10(SF)SF64时约18dB意味着解扩后等效Eb/N0比解扩前高18dB曲线应该整体左移。验证时不用追求和理论公式完全重合看两条曲线的相对位置对不对就够了。SF_list [16 32 64 128]; snr_dB -20:2:0; ber_all zeros(length(SF_list), length(snr_dB)); for s 1:length(SF_list) SF SF_list(s); L SF*nsamp; fs Rb*L; fc fs/8; % 保证fcfs/2且远离扩频带宽 pn mseq(7, [1 7], ones(1,7)); pn pn(1:SF); pn_seq repmat(pn, 1, Nbit); pn_upsample reshape(repmat(pn_seq, nsamp, 1), 1, []); % 后续扩频、调制、加噪、解扩、判决同上 % ... ber_all(s,:) ber; end semilogy(snr_dB, ber_all.); legend(SF16,SF32,SF64,SF128); xlabel(SNR/dB); ylabel(BER); grid on;这段代码的关键是fcfs/8随SF自动变避免固定fc在大扩频因子下落进信号带宽。pn截取前SF位保证每个比特用同一段码。跑完看曲线间距SF每翻一倍处理增益多3dBBER曲线应该向左平移约3dB。如果间距不对先查nsamp有没有跟着SF变再查awgn的SNR是不是按采样点定义的。扩频因子SF处理增益10log10(SF)每比特chip数适用观察点1612.0 dB16曲线分开明显适合教学演示3215.0 dB32兼顾仿真速度和增益6418.1 dB64文档常用值注意PN码截取12821.1 dB128低SNR段更陡但仿真时间翻倍从那以后我每次跑直扩仿真先把Rb、SF、nsamp、fs四个数写在脚本第一行再检查fcfs/2最后才看BER曲线。这套习惯帮我省掉了至少一半的“图不对但不知道哪错”的时间。希望帮到你。本文还有配套的精品资源点击获取