简介:这份MATLAB程序资源围绕DSSS(直接序列扩频)信号生成展开,面向通信工程专业学生、射频研发人员及无线通信算法学习者,可配合通信原理课程或扩频实验使用。压缩包共4个文件,全部为.m脚本,体积仅2KB,结构精简。主要脚本串联扩频流程,分别负责主控、m序列生成与DSSS信号构造,能从代码层面直接观察PN码、扩频因子与基带调制的结合方式;PN码由线性反馈移位寄存器产生的思想也体现在m序列源码中,便于理解伪随机特性与频谱扩散机制。解扩频、码片同步等接收端原理,可对照发送端参数设计一并领会。已有361人学习,适合作为课程设计或MATLAB仿真的入门样本;对于需要掌握抗干扰通信机制、快速搭建扩频模型的读者,这份小巧实例提供了可运行参考,稍加改动即可扩展Gold码、多址接入或FEC编码等进阶功能。
1. DSSS扩频信号到底怎么生成:一套能直接跑的Matlab源码
做无线通信仿真这些年,我拆过不少扩频相关的代码包,有个很深的体感:DSSS扩频信号的理论讲起来不算难,但真要把PN码、扩频因子、基带调制这几块拼成一个能跑的仿真,大多数人会在“PN码怎么来的”第一步就卡住。这套资源正好补上这个断层,它包含gen_mseq.m、gen_dsss.m、main.m和gen_fhss.m四个文件,从m序列生成到DSSS信号输出是一条完整链路,末尾还带了一个FHSS对照实验。它适合两类人:一是刚接触扩频通信、想在Matlab里把“扩频”两个字变成波形看个究竟的学生;二是做通信系统仿真、需要一份干净可改的DSSS参考实现来嵌入自己链路的工程师。你不需要先啃完通信原理,跟着代码走一遍,扩频信号的一生就能看明白。
2. m序列与LFSR实现:PN码不是随机数,是算出来的
2.1 为什么DSSS里几乎都用m序列
DSSS扩频的核心操作,是把每个数据比特乘上一段高速的伪随机码序列,也就是PN码。PN码里的“伪”字提醒我们,它只是看起来随机,实际上完全确定,接收端能复制出一份一模一样的,这才是后面解扩的基础。PN码的候选方案有m序列、Gold码、Kasami序列几种,而m序列是最简单也最常用的一种,因为它只需要一个线性反馈移位寄存器(LFSR)就能产生,硬件实现极其便宜,且周期特性非常强。
m序列的全称是最大长度序列,由n级寄存器产生的m序列长度为2的n次方减1,这个长度恰好是LFSR能走完的所有非零状态总数。它的自相关特性非常好:完全对齐时相关峰高,错开一位以上旁瓣恒为固定小值。这种尖锐的相关峰决定了接收端只要把本地PN码和收到的信号做相关运算,就能在正确对齐的位置看到一个明显的凸起。正是由于“能产生、能复制、能对齐”这三个特点,m序列被大量用于点对点扩频和低速控制信道。Gold码则是从两个优选m序列异或派生出来的,互相关性能更好,常用于CDMA多用户场景,但在单用户DSSS仿真里,先用m序列把链路跑通是更务实的做法。
2.2 gen_mseq.m完整实现与参数说明
直接看代码。gen_mseq.m的作用是输入寄存器级数n、本原多项式poly和初始状态init,输出一个完整的m序列。下面是我拆包后重写并加了注释的版本:
% gen_mseq.m - 由线性反馈移位寄存器LFSR生成m序列 % 输入参数: % n : 移位寄存器级数,序列长度 = 2^n - 1 % poly : 本原多项式系数,长度 n+1 的向量, % 例如 [1 0 0 1 0 1] 表示 1 + x^2 + x^5 % init : 初始状态,长度 n 的0/1向量,不能为全零 % 输出结果: % mseq : 生成的m序列,元素为0/1,长度 2^n - 1 function mseq = gen_mseq(n, poly, init) if nargin < 3 init = [1, zeros(1, n-1)]; % 默认初相:最高位置1 end len = 2^n - 1; mseq = zeros(1, len); reg = init; for i = 1:len mseq(i) = reg(n); % 从末级输出当前码片 feedback = mod(sum(reg .* poly(2:end)), 2); % 抽头异或,模2求和 reg = [feedback, reg(1:end-1)]; % 反馈值移入最低位 end end这段代码里有几个地方容易看懵。首先是poly(2:end)的取法:poly的第一个元素是x的0次方系数,恒等于1,真正参与反馈计算的是从x的1次方到x的n次方这部分系数,所以我把首位跳过,只让剩余系数和寄存器做逐位点乘。其次是feedback的计算,它把当前寄存器的每一位和对应抽头系数相乘后累加再模2,本质就是把抽头位置上的寄存器值全部做异或运算。得到feedback之后把它放到寄存器最前面,其他位依次后移一格,这就完成了一次移位。
调用时要注意本原多项式的正确写法。比如n等于5时,多项式x的5次方加x的2次方加1对应的系数向量是[1 0 0 1 0 1],寄存器级数正好是5,向量长度是6。我一般会在草稿纸上先把寄存器链画出来,对照着写poly向量,能避免一半以上的低级错位。init的改变会改变m序列的相位,也就是得到原序列的循环移位版本。接收端捕获时需要搜索这个相位偏移,这一点在后面的避坑章节会着重说。
2.3 自相关验证:用xcorr证明PN码能用
光生成出来还不够,必须验证它能不能用。m序列最核心的特征是二值自相关特性:完全对齐时相关值等于N,也就是2的n次方减1;错开一位以上时相关值恒等于负1。把序列映射为正负1之后,用Matlab自带的xcorr函数就能直接验证:
% 验证m序列自相关性 n = 5; % 5级移位寄存器 poly = [1 0 0 1 0 1]; % x^5 + x^2 + 1,本原多项式 init = [1 0 0 0 0]; % 初始状态 mseq = gen_mseq(n, poly, init); % 生成0/1序列 p = 2*mseq - 1; % 映射为±1 [r, lags] = xcorr(p, p, 'coeff'); % 归一化自相关 stem(lags, r, 'filled'); xlabel('码片偏移'); ylabel('归一化自相关值'); title('m序列自相关特性');运行后观察结果:零偏移位置会出现一个明显的峰值1,其余所有偏移处的值应该接近负1除以N。以n等于5为例,N等于31,旁瓣值大约是负0.032,主峰和旁瓣的落差接近33倍,在图上看起来就是一根尖刺立在一条平线上,到处都低于它。这个尖峰就是接收端捕获码相位的依据:滑动相关搜索时,最大相关值出现的位置就是PN码对齐的位置。
还有一个小技巧是同时验证互相关特性,让两组不同初始状态生成的m序列互相做xcorr。如果互相关旁瓣也低,说明这套PN码在后续多用户场景里有一定抗多址干扰能力。眼下先把自相关的尖峰确认好,我一般就会进入下一章正式做扩频调制。
3. gen_dsss.m扩频与调制:处理增益和带宽怎么落地
3.1 扩频因子到底取多少:一个参数表就清楚
进入核心环节之前,先花一分钟把扩频因子SF讲透。SF的中文叫法很多,扩频因子、扩频比、处理增益,指的都是同一个东西:一个数据比特被拆成多少个码片。举个例子,你要传1Mbps的数据,扩频后码片速率变成8Mchip/s,SF就是8。SF越大,每个比特分配到的码片越多,抗干扰能力和隐蔽性越强,代价是同样数据量占用的带宽变大,传输速率下降。这不是玄学,是带宽和信噪比之间的直接权衡。
SF在代码里就是一个整数,但要注意它和PN码周期的关系。最常见的做法是让每个数据比特对应PN码的一个完整周期,这样SF就等于PN码长度,也就是2的n次方减1。好处是每个比特的相关特性都是完整的,自相关峰不会被中途截断。下表是几个常用参数组合,直接照着用就可以跑仿真:
| 级数n | PN码长度N | 扩频因子SF | 处理增益(dB) |
|---|---|---|---|
| 5 | 31 | 31 | 14.9 |
| 7 | 127 | 127 | 21.0 |
| 9 | 511 | 511 | 27.1 |
处理增益的计算公式是10乘以log10(SF)。SF每翻一倍,增益涨3dB,直观感受是同样的误码率要求下,系统所需信噪比更低。选SF时要先看带宽预算:数据速率乘以SF就是码片速率,码片速率又决定了扩频信号的主瓣带宽,三者绑在一起,动一个另外两个跟着动。仿真初期建议从n等于5或7开始,序列短好调试,波形算得快,等链路稳定了再换长序列。
3.2 扩频加BPSK调制的核心实现
下面这段是gen_dsss.m的核心逻辑。输入数据按位取出,映射为正负1后与PN码相乘,再把扩频后的码片序列调制到载波上。基带调制我选BPSK,因为DSSS最常见的搭档就是对相位敏感、实现又最简单的BPSK,扩频和调制叠加在一起时,信号模型不会变得过于复杂:
% gen_dsss.m - 生成DSSS扩频信号 % 输入参数: % data : 1×M的0/1数据向量 % pn : 1×N的0/1 PN码序列 % SF : 扩频因子,通常是PN码长度N % fc : 载波频率 % Fs : 采样率 % chipT : 单个码片持续时间 % 输出结果: % t : 时间轴 % dsss_rf: 调制后的DSSS射频信号 function [t, dsss_rf] = gen_dsss(data, pn, SF, fc, Fs, chipT) M = length(data); N = length(pn); pn_bipolar = 2*pn - 1; % 0映射为-1,1映射为+1 % 第一步:扩频,每个数据比特复制SF个码片并与PN逐位相乘 spread = zeros(1, M*SF); for i = 1:M seg = (i-1)*SF + 1 : i*SF; spread(seg) = (2*data(i)-1) .* pn_bipolar(1:SF); end % 第二步:BPSK上变频,码片流直接乘余弦载波 chipRate = 1/chipT; totalTime = length(spread) * chipT; t = 0:1/Fs:totalTime - 1/Fs; baseband = repmat(spread, ceil(Fs*chipT), 1); baseband = baseband(:)'; % 零阶保持,码片电平保持一个chipT dsss_rf = baseband .* cos(2*pi*fc*t); end第一步扩频里,我用2乘data(i)减1把数据0变成负1、1变成正1,这样和PN码相乘的结果刚好满足同号输出正、反号输出负的物理含义。第二步上变频用了零阶保持近似,每个码片电平在整个chipT内不变,再和载波相乘。载波频率fc要结合采样率Fs来选,工程上常用准则是Fs至少大于两倍的fc加上信号带宽,否则频谱会混叠,时域波形也会出现明显失真。
如果你的仿真只关心基带处理,直接输出spread即可,上变频那步可以跳过去。需要接射频信道模型时再保留载波部分。main.m里调用gen_dsss的典型写法大致是这样的:先用randi生成一批随机数据比特,再用gen_mseq生成PN码,最后调用gen_dsss得到时间轴和射频信号,后续可以交给加噪、解扩和误码率统计模块。
3.3 从频谱确认扩频成功:功率谱密度一目了然
扩频有没有做对,不要用眼睛空猜,最直接的办法是观察信号频谱。扩频前数据信号的能量集中在窄带范围,扩频后同一个能量被摊开到以码片速率决定的宽带上,功率谱密度明显下降,这就是扩频两个字在物理层面的含义:
% 观察扩频前后的频谱对比 Fs = 100e3; fc = 10e3; chipT = 0.001; SF = 31; data = randi([0 1], 1, 100); % 随机数据 pn = gen_mseq(5, [1 0 0 1 0 1], [1 0 0 0 0]); % m序列 [t, dsss_rf] = gen_dsss(data, pn, SF, fc, Fs, chipT); % 用周期图法估计功率谱 [Pxx, f] = pwelch(dsss_rf, 256, [], 1024, Fs); plot(f/1e3, 10*log10(Pxx)); xlabel('频率 (kHz)'); ylabel('功率谱密度 (dBHz)'); title('DSSS扩频信号功率谱');运行后会看到能量不再集中在fc附近一个窄峰上,而是铺开成以fc为中心的sinc函数形状,主瓣宽度由chipT决定,大约是2倍码片速率。频谱铺得越平,说明PN码随机性越好,扩频效果越“干净”。看完这张图,从数据比特到DSSS信号的整条链路就确认通了。这个习惯我坚持了很久:每次改完参数都先看一眼频谱再往下走,能拦住大多数链路层的低级错误。
4. DSSS仿真避坑指南:PN相位、初相和模2运算的经典翻车现场
这一章写的都是我在拆这个项目和跑DSSS仿真时实际遇到或帮别人排查过高频问题,每条都按现象、原因、解决三段式展开。提前看完,至少能帮你省两个晚上的调试时间。
4.1 初始状态设成全零:LFSR输出整条全零序列
现象:调用gen_mseq时把init写成全零向量,生成的mseq全部是0;后续扩频输出也都是0,误码率直接爆表,但代码逻辑看起来没有任何问题。
原因:LFSR遇到全零状态后会被永久锁住。反馈异或运算里,任何值异或0都等于它自己,寄存器一旦全部归零,无论怎么移位和反馈,输出永远是0。这是LFSR的结构性缺陷,不是Matlab的问题。
解决:初始化时保证至少有一级寄存器为1。我习惯在函数内部加一道兜底判断,状态全零时自动改成默认初相:
if all(init == 0) init = [1, zeros(1, n-1)]; % 强行把最高位置1,避免锁死 end4.2 生成多项式没选本原:m序列变成短周期序列
现象:生成的序列长度理论上是2的n次方减1,实际却短了一截;自相关曲线除了主峰之外,还出现多个等高的旁瓣,捕获时容易误判同步点。
原因:m序列要求生成多项式必须是本原多项式,所谓本原,是说多项式不可约且能让LFSR遍历所有非零状态。很多人从网上随手抄一个多项式系数就直接用,遇到非本原多项式,周期就会缩短,序列特性随之劣化。
解决:直接查通信原理教材后面的本原多项式表,不要自己凑系数。表格列出几个最常用的组合:
| 级数n | 常用本原多项式 | 周期 |
|---|---|---|
| 3 | 1 + x + x^3 | 7 |
| 5 | 1 + x^2 + x^5 | 31 |
| 7 | 1 + x^3 + x^7 | 127 |
拿到多项式后先做一遍自相关验证,如果旁瓣乱七八糟,优先怀疑多项式选错了。
4.3 解扩时PN相位差一个码片:相关峰直接消失
现象:发送端和接收端用的是同一个gen_mseq生成的PN码,但解扩后恢复的数据和原始数据相比完全错乱,而且是系统性错位,不是零星错误。
原因:DSSS解扩要求收发两端的PN码不只是“同一个序列”,码相位还必须严格对齐。只要差一个码片,相关峰就没有了。这和m序列自相关尖锐的特性是配套的,峰越尖,对相位越敏感。
解决:接收端先做滑动相关捕获,搜索相关峰位置,把本地PN码对齐到峰值处再开始解扩:
% 码相位捕获:滑动相关找峰值 received_cp = 2*received - 1; % 收到的码片映射为±1 pn_local = 2*pn - 1; % 本地PN映射为±1 xc = zeros(1, N); for shift = 0:N-1 xc(shift+1) = sum(received_cp(1:N) .* circshift(pn_local, -shift)); end [~, bestShift] = max(xc); % 峰值位置即同步点 pn_aligned = circshift(pn_local, -bestShift);这一步是DSSS仿真的“后悔药”,宁可在这里多算几下,也别跳过。实际通信系统里码同步是一个专门模块,一般会用早迟门跟踪环持续校准,仿真里滑动搜索已经够用。
4.4 0/1运算混入浮点误差:判决边界翻车
现象:自相关结果出现0.9999和负0.002这类数值,误码率测试里边界比特忽对忽错,同样的代码跑两次结果不一样。
原因:Matlab里0/1逻辑运算和浮点运算混在一起用,比如直接用乘法代替异或,乘出来的中间值带浮点小数,后续sum或比较时精度就会漂。m序列生成和PN相关都必须严格停留在整数与模2语义里。
解决:凡是0/1运算,统一用mod(a加b,2)或xor(a,b),不要用乘法。PN码内部保持0/1整数,只有到了扩频入口才做映射成正负1的转换。转换时用round处理一次,别让浮点误差流进判决。
4.5 省掉捕获逻辑直接解调:仿真全对,实测全懵
现象:仿真链路直接把发送端PN复制到接收端使用,不写任何捕获步骤,误码率完美;但同样代码换成带时延或频偏的信道模型后,系统瞬间崩溃。
原因:仿真里的捷径掩盖了同步问题。发送端PN和接收端PN相位天然一致,真实系统里信号经过信道后,码片边界、载波相位全都变了,不捕获直接解扩等于盲人摸象。
解决:就算是纯仿真,也强制在信号路径里加一步捕获逻辑。至少保留滑动相关的代码框,哪怕当前场景下偏移是零,也要让信号走一遍这个流程。这个习惯能让你在从仿真往硬件或实时系统迁移时少掉一层皮。
5. 解扩验证与FHSS对照:相关峰之外还能学到什么
5.1 自检模板:自相关和互相关一起看
把第2章的验证代码整理成一个固定脚本,每次更换PN参数时先跑一遍,再进入链路仿真。我的自检模板是同时打印自相关峰值和互相关峰值:
autoPeak = max(xcorr(pn1, pn1)); crossPeak = max(xcorr(pn1, pn2)); fprintf('自相关峰值: %.3f, 互相关峰值: %.3f\n', autoPeak, crossPeak);自相关和互相关的比值越大,抗干扰能力越强。这个模板单独看很简单,但能提前拦住一大半序列层面的问题,比等到整个链路跑完再回头找原因省事得多。
5.2 解扩三步走:乘回去、积分、阈值判决
解扩就是再做一次扩频的逆运算,把接收信号乘以同一段PN码,然后按SF长度积分,最后和零阈值比较。注意判决前不要round,保留浮点值做能量比较:
y = received_dsss .* pn_aligned; % 乘以本地PN,完成相关 for i = 1:M energy(i) = sum(y((i-1)*SF+1 : i*SF)); % 按SF积分 end bits_rx = energy > 0; % 零阈值判决这里received_dsss必须是码片级的浮点序列,pn_aligned必须是与发送端相位对齐的正负1序列,顺序错了结果全乱。验证时把这个流程跑通,DSSS的核心收发链路就算真正闭环了。
5.3 gen_fhss.m对照实验:DSSS和FHSS的差别一目了然
资源里还带了gen_fhss.m,是调频扩频FHSS的信号生成文件。DSSS和FHSS经常被拿来对比:DSSS在宽频带上把能量摊开,同一时刻所有用户共享整个宽信道;FHSS是在很多窄频带之间快速跳变,同一时刻只占用一个窄信道,跳频图案由PN码决定。跑gen_fhss.m时重点看载波频率随时间切换的规律,再回到gen_dsss.m的频谱上对比一次,两种扩频思路的差别就非常直观了。对初学者来说,这两个文件放在一起本身就是一份很好的扩频技术对照实验。
说实话,这套资源跑通不难,跑通之后真正留在我脑子里的不是那几行Matlab代码,而是“先验证序列、再进链路”的顺序。做扩频仿真以来,我踩得最深的坑几乎都集中在PN码生成和相位同步环节,而这些坑几乎都能在自相关和滑动相关的图形上提前发现。从那以后我每次做DSSS相关实验都强制自己走一遍“生成PN码、画自相关、做滑动捕获、再扩频调制”的流程,宁可在这上面多花十分钟,也不想把问题一路带到解扩端再回头翻车。这套流程建议你也保留下来。希望帮到你。
本文还有配套的精品资源,点击获取