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与代码里Rb=0.25的矛盾从哪来
这份文档最容易被跳过的是参数表。正文写“信源速率2kbps,扩频因子64”,代码里却是Rb=0.25; Tb=1/Rb;,算出来比特周期4秒,100个比特要跑400秒。2kbps对应的是0.5毫秒,两者差8000倍。更麻烦的是时间轴单位:t=linspace(-T/2,T/2,N)里T=400,xlabel却标t(ms),按秒算是400秒,按毫秒算是0.4秒,后面fc=4e4和fs=4000再一掺和,整条链路的物理量纲就散了。
这种“正文参数和代码参数各写各的”在课程设计文档里很常见。直接跑原代码,图上能出几条波形,但BER曲线没有物理意义,因为扩频因子、码片速率、采样率三者对不上。合格的做法是先定信息速率和扩频因子,再由每chip采样数推采样率,而不是先拍一个fs再倒推。常见做法是:Rb定在1kbps量级,SF取2的幂,nsamp取8或16,fs=Rb*SF*nsamp。
2.2 扩频因子64与每比特采样点数L=400的关系
原文里L=400被当成扩频因子用,但L是每比特采样点数,不是每比特chip数。扩频因子SF是每比特对应的PN码chip数;每chip再采nsamp个点;所以L=SF*nsamp。原文M=100个比特、L=400,总采样点N=40000,然后fs=4000,相当于每比特400点、每chip多少点取决于PN码周期。代码里stage=3的m序列周期只有7,后面k_code又把每个chip重复4次凑到20000点,算下来每比特实际50个chip,和文档写的64、代码里m序列的7都不一致。
三个数各说各话,解扩时PN码和符号边界必然错位。我一般会在脚本第一行写死四个数:Rb、SF、nsamp、Nbit,然后让L、fs、fc全部由它们推出来。这样换参数时只改一处,不会出现“改了下采样率忘了改滤波器截止频率”的血泪经验。
2.3 载波频率fc与采样率fs的取值边界
原文fs=4000、fc=4e4,载波频率是采样率的10倍,直接违反奈奎斯特。采样后的信号里,cos(2*pi*fc*t)每步相位跳太多,混叠成一团,解调时乘同一个载波也恢复不出基带。正确约束是fs >= 2*fc,并且fc要远大于扩频信号带宽(约SF*Rb),否则载波和扩频带宽重叠,滤波分不开。
一套自洽的参数可以这样取:
| 参数 | 符号 | 取值 | 约束说明 |
|---|---|---|---|
| 信息速率 | Rb | 1 kbps | 决定比特周期Tb=1ms |
| 扩频因子 | SF | 64 | 每比特64个chip |
| 每chip采样数 | nsamp | 8 | 至少4,建议8或16 |
| 每比特采样点数 | L=SF*nsamp | 512 | 必须为整数 |
| 采样率 | fs=Rb*L | 512 kHz | 满足fs≥2fc |
| 载波频率 | fc | 128 kHz | fc<fs/2,且fc>>SF*Rb |
| 信息比特数 | Nbit | 1000 | 影响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。stage=3时周期是7,stage=6时周期是63,stage=7时周期是127。原文用stage=3、抽头[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不能全零,否则寄存器锁死。stage=7、tap=[1 7]、init=ones(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是相对位移,user=1时不移位,user每加1多移一位。Gold序列的自相关和互相关旁瓣比m序列可控,地址数也多,工程上做多址仿真时比纯m序列方便。但要注意,两个m序列必须是优选对,随便拿两条m序列模二加,互相关峰值可能很高,解扩时不同用户之间会串。
3.3 扩频操作的维度检查
扩频的本质是信息比特与PN码逐chip相乘。BPSK映射下,比特0映射为-1,比特1映射为+1,PN码的0/1也映射为-1/+1,然后逐位相乘。这里最容易翻车的是维度:SF=64,但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/fs:
t = (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是信号功率与噪声功率之比,单位dB,'measured'表示先测x的实际功率再加噪声。原文直接把SNR(10)丢进去,但SNR数组是1:1:15,对应的是dB值,不是线性值。更隐蔽的问题是:tx的幅度不一定是1,如果前面扩频或载波相乘改变了幅度,'measured'会按实际功率算,SNR的定义就偏了。
稳妥做法是在加噪声前归一化信号功率,或者直接用Eb/N0换算。对于BPSK,SNR = Eb/N0 + 10*log10(Rb/B),其中B=fs/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完成积分,metric>0做硬判决。参数说明: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)/Nbit,Nbit至少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加到0dB,BER不降反升,或者一直在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之后出现地板,降不下去。原因常见于SF=64但PN码周期63,每个比特的码相位比上一个比特偏移一位,解扩时相关峰逐渐偏离积分窗口。原文stage=3周期7,代码里却想扩64,k_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至少1000;f_cut取2*SF*Rb;butter阶数用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),SF=64时约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; % 保证fc<fs/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('SF=16','SF=32','SF=64','SF=128'); xlabel('SNR/dB'); ylabel('BER'); grid on;这段代码的关键是fc=fs/8随SF自动变,避免固定fc在大扩频因子下落进信号带宽。pn截取前SF位,保证每个比特用同一段码。跑完看曲线间距:SF每翻一倍,处理增益多3dB,BER曲线应该向左平移约3dB。如果间距不对,先查nsamp有没有跟着SF变,再查awgn的SNR是不是按采样点定义的。
| 扩频因子SF | 处理增益10log10(SF) | 每比特chip数 | 适用观察点 |
|---|---|---|---|
| 16 | 12.0 dB | 16 | 曲线分开明显,适合教学演示 |
| 32 | 15.0 dB | 32 | 兼顾仿真速度和增益 |
| 64 | 18.1 dB | 64 | 文档常用值,注意PN码截取 |
| 128 | 21.1 dB | 128 | 低SNR段更陡,但仿真时间翻倍 |
从那以后我每次跑直扩仿真,先把Rb、SF、nsamp、fs四个数写在脚本第一行,再检查fc<fs/2,最后才看BER曲线。这套习惯帮我省掉了至少一半的“图不对但不知道哪错”的时间。希望帮到你。
本文还有配套的精品资源,点击获取