简介:面向通信工程、电子信息类专业学生与初学者的一份MATLAB课程设计/仿真分析文档,围绕CDMA码分多址通信系统的建模、仿真与性能评估展开,可帮助读者在缺少硬件平台的情况下理解多址接入原理并完成课程设计或论文写作。压缩包内共1个doc文件,约435KB,内容为完整的设计报告,章节涵盖绪论、CDMA发展现状与趋势、基本模型、卷积编码与扩频、调制、信道传输、解调恢复以及结果分析等模块,并给出对应MATLAB仿真语句与参数设置说明,中英文摘要与关键词齐备,结构接近正式毕业论文,便于直接参考行文框架与模块划分。文中还结合AWGN等信道条件讨论系统抗干扰能力,指出单用户场景下即使在人为与物理噪声干扰中仍可准确恢复数据。目前已有146人学习下载,适合需要快速搭建CDMA仿真流程、对照参数与结论的读者使用。
1. CDMA 系统仿真的价值到底在哪里
CDMA 这套东西,纸上推公式谁都会:扩频增益 10log10(SF),相关接收,Q 函数算误码率。但真到工程里,理论曲线和实测之间总会差出一截——正交码在异步下互相关泄漏、远近效应把弱用户直接压死、功率控制环路跟不上深衰落,这些在公式里看不见,只有把链路搭起来跑一遍才会暴露。MATLAB 恰好适合干这件事:既有现成的通信工具箱,也有足够的底层可控性,能让人从 PN 序列生成一路调到 BER 曲线,中间每一步都能抓出来看波形、看星座、看功率谱。这篇文章面向做通信课设的同学和刚转链路级仿真的工程师,按"码 → 扩频 → 信道 → 接收 → 多用户"的顺序展开,每一步都给出能直接跑的代码和参数取值理由。
2. 扩频码的 MATLAB 生成:m 序列、Gold 序列与 Walsh 码
2.1 为什么 CDMA 里至少需要两套码
刚接触的人容易把"扩频码"当成一个东西,实际上 CDMA 链路里至少有两个层次。第一层是信道化码,负责把同一小区内不同信道(或不同用户)区分开,要求严格正交,Walsh-Hadamard 码就是干这个的。第二层是扰码,负责把不同小区、不同基站区分开,要求互相关低而不是正交,m 序列和 Gold 序列是典型选择。
这两层码的数学性质完全不同。Walsh 码的正交性只在符号级对齐时才成立,一旦引入随机时延——也就是异步场景——正交性立刻退化,这正是后面多址干扰的来源。扰码则相反,它不追求正交,只要任意两条码在任意相对时延下的互相关都足够小,让接收端做相关时不被别的小区淹没即可。
理解这一点,仿真里的很多"反直觉"结果就顺了:同步单径下 Walsh 码的 MAI 等于零,BER 曲线和单用户完全重合;一旦把时延加进去,曲线立刻抬起来。不用纠结 matlab 下载安装教程里那些版本选择,近几个大版本的通信工具箱 API 基本稳定,直接装完开工即可。
2.2 用 LFSR 生成 m 序列的最小实现
m 序列本质是最大长度线性反馈移位寄存器序列,周期 2^m - 1。用 MATLAB 写出来只有十几行:
function seq = gen_mseq(taps, m, N) % taps: 反馈抽头位置(从1开始),如 [4 1] 对应 x^4 + x + 1 % m : 移位寄存器级数,决定周期 2^m-1 % N : 输出码片数,一般取整数倍周期 reg = [1 zeros(1, m-1)]; % 初态,非全零即可 seq = zeros(1, N); for n = 1:N seq(n) = reg(end); % 输出末级 fb = mod(sum(reg(taps)), 2); % 模2求和作为反馈 reg = [fb reg(1:end-1)]; % 右移一位,反馈填入首位 end seq = 2*seq - 1; % 0/1 映射为 -1/+1,方便做乘法扩频 end逻辑很直白:每一步先读末级作为输出,再把抽头位置的寄存器值模 2 相加作为新输入,整体右移。最后一步映射成双极性是为了让扩频变成纯乘法,省掉异或判断。
参数上有两个坑。taps必须是本原多项式对应的抽头,否则周期达不到 2^m-1,序列会提前进入短循环。常用的几个:m=4 用 [4 1],m=5 用 [5 2],m=7 用 [7 1],m=10 用 [10 3]。m一旦确定,周期就定死了,N取周期整数倍可以在做相关时得到干净的周期性自相关峰。
2.3 Walsh-Hadamard 码与正交性的数值验证
H = hadamard(64); % 64 阶 Walsh 码,每行一个码字 G = H * H' / 64; % 归一化互相关矩阵 offdiag = max(abs(G - eye(64)), [], 'all'); fprintf('最大非对角元素: %.3e\n', offdiag); % 应接近 2.2e-16,即数值零这段代码的价值在于把"正交"落成了可打印的数字。如果打印出来不是 1e-15 量级,多半是阶数写成非 2 的幂了,hadamard 对非 2 的幂支持有限,会静默返回错误维度。
| 码类型 | 长度 | 主要用途 | 同步要求 | 相关特性 |
|---|---|---|---|---|
| m 序列 | 2^m-1 | 扰码、PN 参考 | 低 | 自相关双值,互相关不定 |
| Gold 序列 | 2^m-1 | 扰码族 | 低 | 互相关三值,峰值有界 |
| Walsh 码 | 2^k | 信道化 | 高,需符号对齐 | 完全正交 |
2.4 Gold 序列族的生成与选择
Gold 码由一对优选 m 序列的循环移位模 2 加得到。双极性表示下模 2 加等价于相乘,代码很紧凑:
s1 = gen_mseq([5 2], 5, 31); % 优选 m 序列之一 s2 = gen_mseq([5 4 3 2], 5, 31); % 另一个抽头需与其构成优选对 gold = zeros(31, 31); for d = 0:30 gold(d+1,:) = s1 .* circshift(s2, [0 d]); % 移位后逐点相乘 end产出的 31 条序列就是一个 Gold 码族。选码时不要随机挑,最好先算出整个码族的互相关矩阵,挑互相关峰值最小的那几条给相邻小区用:
C = gold * gold' / 31; % 31x31 互相关 C(logical(eye(31))) = 0; % 去掉自相关对角线 [maxCC, idx] = max(abs(C(:))); fprintf('最大互相关 %.3f, 位置 %d\n', maxCC, idx);理论上的互相关峰值被约束在 -1、-t(m)、t(m)-2 三个值上,其中 t(m) = 1 + 2^floor((m+1)/2)。m=5 时 t=9,所以互相关绝对值不会超过 9/31 ≈ 0.29,比随机二值序列好得多。
3. 单用户 CDMA 链路的 MATLAB 实现:从扩频到 BER 曲线
3.1 链路结构设计与参数取值
仿真之前先把参数定下来,比边写边改省事得多。下面这套取值是课设和验证里的常见配置:
| 参数 | 取值 | 选取理由 |
|---|---|---|
| 扩频因子 SF | 64 | 与 Walsh 码长一致,扩频增益 18.06 dB |
| 信息速率 | 9.6 kbps | 经典速率,便于与教科书曲线对照 |
| 码片速率 | 614.4 kchip/s | 等于 SF × 信息速率 |
| 调制方式 | BPSK | 理论 BER 有闭式解,便于校准 |
| 信道 | AWGN,单径 | 先排除干扰,标定基线 |
| 接收 | 相干相关接收 | 与扩频码做匹配滤波 |
扩频增益换算成 dB 就是 10log10(64) ≈ 18.06 dB。这个数字的意义是:即使解扩前 SNR 是负的,解扩后仍能正过来。
3.2 发送端:向量化的扩频实现
SF = 64; Nbits = 1e5; % 传输比特数 codeBook = hadamard(SF); pn = codeBook(1, :); % 本用户扩频码 data = randi([0 1], 1, Nbits); % 随机比特流 sym = 2*data - 1; % BPSK: 0->-1, 1->+1 tx = kron(sym, pn); % 逐符号扩频这里的关键是kron。它在数学上是 Kronecker 积,实现上等价于"每个符号复制 SF 份,再与码字逐点相乘"。相比双重 for 循环,速度差两个数量级。Nbits = 1e5、SF = 64 时产生的 tx 长度是 640 万,double 占约 50 MB,普通笔记本内存完全够。
3.3 AWGN 加噪:Eb/N0 到噪声方差的换算
这一步是最容易出错的地方。很多人直接调awgn(tx, EbN0_dB, 'measured'),看起来简洁,实则隐患很大:'measured'会按信号实际功率反推噪声,一旦发送功率因为某些原因变化,Eb/N0 的定义就跟着漂了,曲线对不上理论值。
稳妥的做法是手工换算,把能量关系写死:
EbN0_dB = 0:2:10; ber = zeros(size(EbN0_dB)); for k = 1:length(EbN0_dB) EbN0_lin = 10^(EbN0_dB(k)/10); % 码片幅度为1时,每比特包含 SF 个码片,故 Eb = SF % 单边噪声功率谱密度 N0 = SF / EbN0_lin % 双边实噪声每维方差 = N0/2 noiseVar = SF / (2 * EbN0_lin); noise = sqrt(noiseVar) * randn(size(tx)); rx = tx + noise; % ... 解扩判决 end推导链条是:码片能量 Ec = 1(因为幅度 ±1),每比特能量 Eb = SF × Ec = SF;由 Eb/N0 定义反解出 N0 = SF / EbN0_lin;实高斯噪声每维方差为 N0/2。这条链子写清楚之后,任何 SF 下都不会算错。
3.4 解扩、判决与 BER 统计
rxSym = reshape(rx, SF, []).' * pn.' / SF; % 一个符号周期内相关累加 rxBit = rxSym > 0; % BPSK 门限判决 [~, ber(k)] = biterr(data, rxBit); % 误比特率reshape(rx, SF, [])把长向量折成 SF × Nbits 的矩阵,每一列恰好是一个符号的码片。转置后左乘 pn 的转置,得到的就是每个符号位置的相关值。除以 SF 是归一化,让判决门限回到 0,正好对应 BPSK 的最大似然判决点。
biterr返回误比特数和误比特率,第一个输出(错误数)在后面判断统计置信度时非常有用。
3.5 仿真曲线与理论值的对照
theory = qfunc(sqrt(2 * 10.^(EbN0_dB/10))); semilogy(EbN0_dB, ber, 'o-', EbN0_dB, theory, 'k--'); grid on; xlabel('E_b/N_0 (dB)'); ylabel('BER'); legend('仿真', '理论 Q(\surd(2E_b/N_0))');理论上几个锚点可以作为快速验证:Eb/N0 = 0 dB 时 Q(√2) ≈ 7.9e-2,4 dB 时 Q(2.24) ≈ 1.25e-2,8 dB 时 Q(4.0) ≈ 3.2e-5。
| Eb/N0 (dB) | 理论 BER | 仿真 BER(1e6 比特) |
|---|---|---|
| 0 | 7.87e-2 | 与理论偏差 < 5% |
| 4 | 1.25e-2 | 与理论偏差 < 8% |
| 8 | 3.17e-5 | 需 1e7 比特才稳,否则抖动大 |
如果仿真点明显低于理论曲线(比如在 8 dB 处跑出 1e-6),先怀疑噪声方差算小了,或者数据比特太少导致统计涨落。如果明显高于理论,检查解扩有没有漏掉归一化的 /SF,这一项漏了会让相关值放大 64 倍,判决门限全乱。
4. 多用户场景:MAI、远近效应与功率控制的 MATLAB 验证
4.1 同步正交下的"零 MAI"到底是什么
很多课设做到多用户就卡住:加了 8 个用户,每个用一条 Walsh 码,结果 BER 曲线和单用户一模一样,怀疑代码写错了。其实没写错——在同步、单径、符号级对齐的理想条件下,Walsh 码两两正交,别的用户经过本用户码相关后贡献恰好为零,MAI 严格等于 0。
真正让 CDMA 曲线塌下来的是两个条件被破坏:一是异步,用户到达基站的时间不对齐;二是多径,同一用户的不同径对 Walsh 码的时延不同。这两者都会破坏正交性,产生残余干扰。仿真要想复现 MAI,必须显式把这些条件加进去。
4.2 构造一个能体现 MAI 的多用户上行模型
K = 8; % 用户数 SF = 64; codes = hadamard(SF); userCode = codes(1:K, :); % 每个用户分一条 Walsh 码 amp = [1 1 1 1 1 1 1 20]; % 第8个用户离基站很近,功率大20倍 delay = randi([0 SF-1], 1, K); % 每个用户随机码片级时延,破坏同步 frameLen = Nbits * SF; txAll = zeros(K, frameLen + SF); for u = 1:K d = randi([0 1], 1, Nbits); s = 2*d - 1; txAll(u, delay(u)+1 : delay(u)+frameLen) = amp(u) * kron(s, userCode(u,:)); end rxSum = sum(txAll, 1); % 各用户信号在基站侧叠加delay这一行是整个仿真的转折点。只要它不全为零,Walsh 码的正交性就不再成立。amp里第 8 个用户的 20 倍功率模拟的是"离基站很近的强用户",这是典型的远近效应场景。
接收端仍然只用用户 1 的码做相关:
noiseVar = SF / (2 * 10^(EbN0_dB/10)); rx = rxSum(1:frameLen) + sqrt(noiseVar) * randn(1, frameLen); y = reshape(rx, SF, []).' * userCode(1,:).' / SF;对比有无时延两种设置下的 BER,就能看到 MAI 的破坏力:时延全零时曲线贴着理论线,加入随机时延后弱用户的 BER 会抬高好几个数量级。
4.3 功率控制的迭代实现
功率控制的目标很朴素:让所有用户到达基站时的接收功率尽量相等,把 20 dB 的功率差压到 1 dB 以内。工程上用闭环迭代实现,仿真里可以写成一个简化版:
targetSINR_dB = 7; % 目标信干噪比 pwr = ones(1, K); % 各用户发射功率,初始相等 for iter = 1:20 rxAll = zeros(K, frameLen + SF); for u = 1:K rxAll(u, delay(u)+1 : delay(u)+frameLen) = ... pwr(u) * amp(u) * kron(2*randi([0 1],1,Nbits)-1, userCode(u,:)); end sig = sum(rxAll, 1); for u = 1:K y_u = reshape(sig, SF, []).' * userCode(u,:).' / SF; % 简单估计:把相关输出的均值平方当作信号,方差当作干扰加噪声 sinr_est = mean(y_u).^2 / max(var(y_u), 1e-12); % 按目标与实际之比开方调整功率,0.5 次方是为了平滑收敛 pwr(u) = pwr(u) * (10^(targetSINR_dB/10) / sinr_est)^0.5; end end收敛性的关键在于指数 0.5:直接用比值会在目标附近来回震荡,取平方根相当于给调整步长加了个阻尼。迭代 20 次后pwr会自发分化——强用户降功率,弱用户升功率,最后到达基站的功率大致齐平。
| 场景 | 弱用户到达功率 | 强用户到达功率 | 弱用户 BER 量级 |
|---|---|---|---|
| 无功率控制 | 1× | 20× | 接近 0.5,链路不可用 |
| 有功控(20 次迭代) | ≈ 1× | ≈ 1.2× | 回到 1e-2 量级 |
表格里的具体数字随随机种子变化,但趋势是稳定的:不开功控时最弱用户基本被打成盲猜,开了之后整条曲线能压回单用户基线附近几个 dB 以内。
4.4 多用户检测:从匹配滤波到解相关
单用户检测把其他用户一律当噪声,容量上不去。多用户检测的思路是联合处理所有用户的匹配滤波输出。解相关检测是最简单的一种,代码只有几行:
R = userCode * userCode.' / SF; % K×K 码互相关矩阵 R = R + 1e-6 * eye(K); % 对角加载,防止病态 Y = zeros(K, Nbits); for u = 1:K Y(u,:) = reshape(rx, SF, []).' * userCode(u,:).' / SF; end dHat = sign(inv(R) * Y); % 解相关后逐用户判决R在严格正交时是单位阵,解相关退化成匹配滤波;时延破坏正交后R出现非对角元素,取逆相当于把 MAI 从观测里"减掉"。代价是同时放大了噪声,尤其在用户数接近 SF 时噪声放大非常严重。对角加载1e-6*eye(K)是数值上的常规操作,避免R接近奇异时求逆爆掉。往上一档还可以试 MMSE 检测,把噪声方差也算进去做加权求逆,性能和解相关各有胜负,取决于 Eb/N0 落在哪个区间。
5. 让仿真跑得动、跑得准:CDMA 链路仿真的几个实用技巧
5.1 向量化与并行的加速比实测
链路级仿真的瓶颈几乎全在循环次数上。把发端和解扩都向量化之后,还可以用parfor把 Eb/N0 扫描并行化:
EbN0_dB = 0:1:10; ber = zeros(size(EbN0_dB)); parfor k = 1:length(EbN0_dB) ber(k) = runOnePoint(EbN0_dB(k), 1e6); % 每个点独立跑,互不依赖 endparfor有几个硬性约束:循环体内部不能依赖前一次迭代的变量,循环变量会被切片,随机数种子要按k单独设定,否则每次跑出来的点不一样,曲线不可复现。runOnePoint内部第一行写rng(1000+k, 'twister')就能解决。
5.2 跑多少比特才够:BER 的统计置信度
BER = 1e-4 时如果只跑 1e5 比特,期望错误数是 10 个,误差棒大到没法看。经验公式是让期望错误数至少到 100:
N_bits ≥ 100 / BER_target
| 目标 BER | 建议最小比特数 | 期望错误数 |
|---|---|---|
| 1e-2 | 1e4 | 100 |
| 1e-3 | 1e5 | 100 |
| 1e-4 | 1e6~1e7 | 100~1000 |
| 1e-5 | 1e7 以上 | 建议改用重要性采样 |
走到 1e-5 以下,直接蒙特卡洛会变得很贵,这时可以考虑重要性采样或者把 Eb/N0 曲线只画到 8 dB 就收手——超过这个点已经远离实际工作区间了。
5.3 固定随机种子并保存中间量
rng(20240501, 'twister'); % 全局种子,保证结果可复现 % ... 仿真 ... save('cdma_ber.mat', 'EbN0_dB', 'ber', 'SF', 'Nbits', 'K');把 SF、Nbits、K 一起存进去是个好习惯。过几天回头看曲线偏了,第一件事就是确认这些参数和当时一致。只存 ber 数组,事后基本没法复现。
5.4 用频谱图确认扩频真的发生了
扩频的效果在时域上看不出来,必须转频域。pwelch(tx, [], [], [], 'centered')画出的功率谱应该明显比未扩频的信号宽 SF 倍,且主瓣平坦。如果谱形和未扩频时一样,多半是kron的顺序写反了——kron(pn, sym)会把整个比特序列复制 SF 遍,得到完全不同的波形。
一个简单的判别方法:
fprintf('扩频前后长度比: %.1f (应等于 SF=%d)\n', ... length(tx)/Nbits, SF);长度比只有在 1 和 SF 两个值之间,写错了立刻现形。
本文还有配套的精品资源,点击获取