
做这个项目的诱因很简单手头有个通信链路仿真的需求要求是 32QAM 调制、软解调、扩频解扩、Matlab 误码率仿真四件事一起上。网上能查到的资料大多是单独做 QAM 硬判决或者单独做扩频把这三者串成一条完整链路还能跑出 BER 曲线的确实不多。自己从零搭了一遍踩了不少坑把关键思路和能直接用的 Matlab 实现整理出来给后面做同类仿真的人一个参照。这套仿真解决的问题很具体在 AWGN 信道下验证 32QAM 结合软解调与直接序列扩频DSSS后的误码性能同时对比不同扩频因子下的表现。它比较适合通信工程方向的学生做课程设计、毕业设计或者刚开始接触软解调、扩频系统的人练手。读完你可以得到一版可运行的仿真框架以及理解 LL 软信息、扩频增益这些概念是如何在代码里落地的。1. 这个仿真到底在模拟一条什么样的链路1.1 为什么把 32QAM、软解调、扩频凑在一起先说 32QAM。它是 QAM 家族中每个符号携带 5 个比特的调制方式因为 2^532频谱效率比 16QAM 高 25%又比 64QAM 的星座点疏一些抗噪声能力稍好。用在带宽受限且 SNR 不是特别高的场景里32QAM 是个折中的选择。问题是 32 个星座点在星座图上排得比较紧密尤其是交叉型cross星座外围点的欧氏距离不大。硬解调直接按最小距离判符号比特判决误差在边界点上特别容易发生。软解调则不一样它不直接判 0 或 1而是输出每个比特的置信度也就是对数似然比后续如果接 LDPC、Turbo 这类纠错码软信息能带来 2 dB 左右的编码增益。即便没有纠错码软解调配合正确的判决门限在高 SNR 下也能略优于硬判决的次优近似。扩频的作用就更直观。直接序列扩频把每个调制符号乘上一组伪随机码片相当于把符号能量“摊”到更宽的带宽上。这样做的好处是抗窄带干扰、抗多径以及低截获概率。但在纯 AWGN 仿真里扩频并不会带来理论上的 BER 改善因为处理增益和带宽扩展带来的噪声增大会互相抵消。很多第一次做这个仿真的人会纠结“为什么扩频之后 BER 曲线没有变好”这个后面会专门解释。所以把这三者放在一条链路里实际是在模拟一个带软输出的扩频 QAM 系统。这类系统在现代军用通信、卫星通信里很常见先用 QAM 提高频谱效率再用扩频获得抗干扰能力接收端用软解调给后面的纠错码喂软信息。1.2 链路整体结构和仿真指标整条链路在 Matlab 里是这么组织的发射端随机二进制比特 → 串并转换5 bit 为一组 → 映射成 32QAM 复符号 → 每个符号与一个 PN 序列扩频得到码片序列 → 可选脉冲成型。信道加性高斯白噪声AWGN仿真时直接在码片序列上叠加复数高斯噪声根据设定的 Eb/N0 换算噪声功率。接收端接收码片序列 → 与本地 PN 序列相关解扩累加得到一个软符号估计 → 用这个软符号估计做 32QAM 软解调输出每比特 LLR → LLR 符号判决得到比特 → 并串转换恢复二进制流。统计将恢复比特与发送比特逐位比较统计误比特率并和理论 32QAM AWGN 性能曲线对照。仿真指标不是瞬时误码量而是 BER 随 Eb/N0 的变化曲线。Eb/N0 是每比特能量与噪声功率谱密度的比值和 SNR 的换算关系要特别注意尤其是扩频之后码片速率变了按符号还是按比特算结果完全不同。这部分我在第 3.3 节里详细展开。2. 三块硬骨头星座映射、软解调、扩频解扩怎么实现2.1 32QAM 星座定义与归一化Matlab 的通信工具箱里直接有qammod(x, M, gray)函数指定 M32 就能得到默认的 32QAM 星座映射。默认星座是交叉型排列方式可以参考doc qammod。需要注意的是默认星座图的平均功率是 1这对仿真很有用因为后面加噪声时信号功率直接用 1 来算即可。如果你不想依赖工具箱也可以自定义星座图。我试过一种简单做法在方形网格上删掉四个角上的点凑出 32 个点% 生成自定义32QAM星座6x6网格去掉4个角 [I, Q] meshgrid(-3:2:3, -3:2:3); const I(:) 1j*Q(:); % 去掉四个角 const setdiff(const, [-3-3j, -33j, 3-3j, 33j]); % 归一化平均功率 const const / sqrt(mean(abs(const).^2));不过强烈建议优先用官方qammod因为官方映射带了 Gray 编码相邻星座点之间只有 1 个比特不同。这对软解调性能影响很大如果比特映射是随机排布星座点距离越近比特差异反而可能越大软解调的 LLR 置信度会混乱BER 会明显变差。星座归一化是一个极其容易忽略的细节。很多人在自定义星座时忘了除以平均功率导致 SNR 计算全部偏离。经验做法是无论用什么方式生成星座都要保证mean(abs(const).^2) 1。这样发射符号平均功率为 1加噪声时若噪声功率为 N0则符号 SNR 就是 1/N0后续 Eb/N0 换算就非常干净。2.2 软解调的核心LLR 计算与 max-log 简化软解调的本质是计算“给定接收符号 r 时某个发送比特 b_k 为 0 或 1 的后验概率之比的对数”。公式是L_k ln( P(b_k0|r) / P(b_k1|r) )在高斯信道下这个公式可以化为对星座点距离的运算。精确 LLR 需要对所有星座点求和复杂度较高工程上常用 max-log 近似L_k ≈ (1/σ^2) × [ min_{s∈S_k^1} |r-s|^2 - min_{s∈S_k^0} |r-s|^2 ]其中 S_k^0 和 S_k^1 分别是第 k 个比特为 0 和 1 的星座点集合。在 Matlab 里写这段代码并不难关键是不要用 for 循环遍历所有星座点而是用矩阵广播一次性算出 r 与所有星座点的欧氏距离。我的实现是这样function LLR softDemod32QAM(rxSym, constellation, noizeVar) % rxSym: 列向量接收符号 % constellation: 32个星座点的行向量归一化 % noizeVar: 噪声方差复数噪声总方差 numBits 5; % 所有可能的比特组合 bits de2bi(0:31, numBits, left-msb); % 32行5列 % 计算每个接收符号与所有星座点的距离平方 dist2 abs(rxSym - constellation).^2; % N x 32 LLR zeros(length(rxSym), numBits); for k 1:numBits dist0 dist2 .* (bits(:, k). 0); % 该比特为0的列保留其他变成0 dist0(dist0 0) Inf; % 将置零的位置变成无穷大 dist1 dist2 .* (bits(:, k). 1); dist1(dist1 0) Inf; min0 min(dist0, [], 2); min1 min(dist1, [], 2); LLR(:, k) (min0 - min1) / noizeVar; end end这段代码有个小坑把不相关的列置零后求 min需要先把 0 替换成 Inf否则最小距离永远是 0。也可以用NaN加min的忽略选项但实测Inf更直观。噪声方差noizeVar是复数噪声的总方差。如果噪声模型是n sqrt(N0/2) * (randn 1j*randn)那么噪声总方差就是 N0。LLR 公式里除以方差相当于把距离差归一化。这个值对后面接软判决解码很重要如果只是统计 BER其实不除也不影响硬判结果但会导致 LLR 幅度失真。为了让软信息有意义这个参数必须传对。还有一点上述 max-log LLR 对 16QAM、64QAM 同样适用只需改变星座点和比特数是一个通用函数。它比专门推导 QAM 的快速 LLR 算法慢一些但 32QAM 的星座点不多仿真符号量在百万级别时这个写法在合理时间内能跑完。2.3 扩频与解扩麻将里喊暗号可以把扩频理解成一种“喊暗号”每个调制符号本来是一张牌扩频就是用一串伪随机码片去替代这张牌在信道中传输。接收方知道暗号把收到的乱序码片按照暗号重新排列再相加就能把原始符号“拼”回来。而不知道暗号的人看不到任何有效信息。具体到代码每个 32QAM 复符号 s 乘以 PN 序列 p长度为 SF元素为 ±1得到 SF 个码片 c_i s * p_i。发射端把码片串行发送。接收端收到 y_i c_i n_i解扩时做相关运算将接收码片逐位乘以同样的 p_i然后累加r_hat sum_i (y_i * p_i) s * sum_i (p_i^2) sum_i (n_i * p_i) SF * s n_eff因为 p_i^21所以信号能量加倍了 SF 倍噪声项是 SF 个独立噪声的加权和方差也变成原来的 SF 倍。所以解扩后符号 SNR 与扩频前一致这就是为什么在纯 AWGN 下 BER 曲线不因扩频而变。但要注意如果信道里有窄带干扰扩频会把干扰功率摊开解扩后干扰项被“平均”掉一部分这才是扩频增益的来源。PN 序列我用的是 m 序列用comm.PNSequence生成或者简单用randi([0 1])产生随机序列再映射成 ±1。仿真中注意每一个符号都要独立地扩频但 PN 序列可以每个符号都相同也可以不同。实际系统中常用一个 long code 截取不同相位给不同符号但仿真里为了方便一个符号一个短 PN 序列即可只要收发一致。解扩的输出是一个软符号估计我把它直接送给软解调函数。需要留意解扩后幅度放大 SF 倍而噪声方差也放大 SF 倍所以传软解调的噪声方差应当是N0 * SF。如果忘了这一步LLR 幅度会差一个 SF 因子。但只看 BER 的话符号判决不受影响只有接 FEC 编码时才要严谨。3. 完整的 Matlab 实现从参数到跑图3.1 链路参数表先给出一组能直接跑的仿真参数我实测下来BER 曲线在 Eb/N0 0~14 dB 范围内很光滑。参数取值说明调制阶数 M32每个符号 5 bit扩频因子 SF1 / 4 / 16SF1 表示不扩频作为基线PN 序列随机 ±1 序列长度等于 SF收发端预先约定每帧符号数10000一次送入的数据量仿真帧数200用于统计误码的最小单位Eb/N0 范围0:1:14 dB常见 QAM 工作区误码统计最小个数100低于这个数继续加大符号数再跑3.2 主循环与核心代码展示有了参数主循环很简洁。为了同时对比不同 SF我定义了一个发射函数和一个接收函数循环调用即可。%% 参数设置 M 32; k log2(M); numBitsPerSym k; sfVec [1 4 16]; % 扩频因子集合 EbN0dB 0:1:14; % 生成星座与比特表 constellation qammod(0:M-1, M, gray); constellation constellation / sqrt(mean(abs(constellation).^2)); bitsTable de2bi(0:M-1, k, left-msb); % 准备存储 BER berMat zeros(length(sfVec), length(EbN0dB)); %% 主仿真循环 for m 1:length(sfVec) sf sfVec(m); pnSeq sign(randn(1, sf)); % 随机PN序列收发共用 for n 1:length(EbN0dB) ebN0 10^(EbN0dB(n)/10); totalBits 0; totalErrBits 0; while totalErrBits 100 totalBits 1e7 % 随机数据 txBits randi([0 1], numBitsPerSym, numSymbolsPerFrame); txSyms qammod(bi2de(txBits, left-msb), M, gray); txSyms txSyms / sqrt(mean(abs(txSyms).^2)); % 功率归一 % 扩频 txChips zeros(sf, numSymbolsPerFrame); for symIdx 1:numSymbolsPerFrame txChips(:, symIdx) txSyms(symIdx) * pnSeq.; end txChips txChips(:).; % 转化为码片串行流 % 加噪声 N0 1 / (ebN0 * k); % 因为符号能量归一化为1且每符号k bit noiseVar N0; noise sqrt(noiseVar/2) * (randn(size(txChips)) 1j*randn(size(txChips))); rxBox txChips noise; % 解扩 rxSymEst zeros(1, numSymbolsPerFrame); for symIdx 1:numSymbolsPerFrame chipVec rxBox((symIdx-1)*sf (1:sf)); rxSymEst(symIdx) sum(chipVec .* pnSeq); end % 软解调 LLR softDemod32QAM(rxSymEst., constellation, noiseVar*sf); rxBits double(LLR 0); % 统计 errBits sum(rxBits(:) ~ txBits(:)); totalErrBits totalErrBits errBits; totalBits totalBits numel(txBits); end berMat(m, n) totalErrBits / totalBits; fprintf(SF%d, EbN0%ddB, BER%.2e\n, sf, EbN0dB(n), berMat(m, n)); end end %% 绘图 theoryBer berawgn(EbN0dB, qam, M); semilogy(EbN0dB, berMat(1,:), o-, ... EbN0dB, berMat(2,:), s-, ... EbN0dB, berMat(3,:), ^-, ... EbN0dB, theoryBer, k-); grid on; legend(SF1, SF4, SF16, 理论32QAM); xlabel(Eb/N0 (dB)); ylabel(BER); title(32QAM扩频软解调链路误码率仿真);这段代码有几个地方要说明我把qammod的输出又手动归一化了一次因为qammod(M)默认平均功率已经是 1但手动归一化保险noiseVar*sf是解扩后的等效噪声方差扩频循环用了双 for在numSymbolsPerFrame较大时有点慢后面会讲提速方法。3.3 Eb/N0 与 SNR 的换算到底差在哪这个坑我至少踩了三遍必须单独列一节。在基带复数等效仿真中常用的噪声添加方式是noise sqrt(N0/2) * (randn 1j*randn)这里的 N0 是复噪声两维的总方差。如果信号平均功率 P1则符号 SNR P/N0 1/N0。Eb/N0 与 SNR 的关系要乘上“每符号比特数”和可能存在的频谱扩展因子。调制层面每符号有 k 个比特所以 Es/N0 k * Eb/N0。仿真时我们直接给定 Eb/N0那么 N0 1 / (k * Eb/N0)其中 Es 归一化为 1。这也是上面代码里N0 1 / (ebN0 * k)的由来。扩频之后码片速率变成符号速率的 SF 倍但每个比特的能量没有变。如果按照码片 SNR 来算噪声带宽也增加了 SF 倍所以码片 SNR 比符号 SNR 低 SF。然而我们始终是以“比特能量/噪声谱密度”作为横轴扩频因子不会进入 N0 的计算。上面代码里发送端没有因为扩频而把信号能量翻倍接收端解扩后信号幅度变成了 SF 倍等效噪声方差变成了 SF 倍所以最终 BER 与 SF 无关。这是正确的关系不要试图在 N0 里乘以 SF。如果你改成以 SNR 为横轴那扩频系统的曲线会左移因为扩频导致带宽扩展、噪声增加相同 SNR 下每比特的质量更差。这个区别文档里不常讲清楚但仿真中经常被问到。4. 踩坑实录BER 曲线不光滑、LLR 符号反了、扩频没有增益……4.1 问题一硬判决和软判决在高 SNR 下曲线重合很多人以为软解调一定优于硬解调于是拿 LLR 硬判与直接最小距离符号判决比较发现曲线基本重合怀疑实现有问题。这里要澄清软解调本身不改变星座点的判决边界如果最终输出就是“LLR 的符号”那么它和最小欧氏距离判决是完全等价的。软信息真正的增益必须在后续的软判决解码器里体现比如 LDPC 迭代译码。所以在纯 BER 仿真里软解调 vs 硬解调应该得到几乎一致的曲线这恰恰是对的。如果想要展示软解调的“可用性”可以额外打印某个 SNR 下的 LLR 直方图高 SNR 时 LLR 分布两极化低 SNR 时 LLR 集中在 0 附近。这比直接比 BER 更有意义。SNR 高低LLR 幅值分布判决可靠性低接近 0不可靠中两端不对称部分可靠高绝对值大且集中可靠4.2 问题二误码率到了 0 仿真卡死BER 仿真都会碰到这个问题。我们设定的循环条件是while totalErrBits 100 totalBits 1e7。当 SNR 很高时需要极多的符号才能统计到 100 个错误仿真时间可能超过几十分钟。解决方法是双上限一是错误个数达到统计要求二是最大发送比特数达到上限。另外还可以在循环里加一个判断如果前一帧错误数为 0且当前 SNR 已经接近理论 BER 小于 1e-6就提前跳出标记 BER 为 0。绘制时用semilogy看不到 0可以把 0 替换成一个小值比如 1e-8图上就会显示为 10^-8 量级。我在实际运行时只跑到 14 dB理论 BER 约为 1e-6为了跑出置信区间每帧 10 万符号、共 10 帧总共 500 万比特耗时十几秒。若是 20 dB 以上千万别硬跑。4.3 问题三扩频后曲线反而变差如果出现 16 倍扩频的 BER 比不扩频差很多几乎可以肯定是在加噪声时把扩频后的码片总能量错误地归一化了。比如有人会把txChips再除以sqrt(sf)相当于降低了发射功率导致曲线右移。扩频本身不改变每符号能量码片序列中每个码片的能量是符号能量的 1/sf这里要说清楚如果发射符号 s 功率为 1那么码片 c_i s * p_i 的功率也是 1不是 1/sf。因为 p_i 是 ±1幅度不变所以扩频后整个码片序列的平均功率仍然为 1。解扩后累加 sf 个码片信号幅度变大 sf 倍功率变大 sf^2 倍噪声累加方差变为原来的 sf 倍。所以解扩后 SNR sf^2 * P_s / (sf * N) sf * (P_s/N)这看起来多了个 sf。但实际上解扩前单个码片的 SNR 是 P_s/N解扩后 SNR 是 sf 倍。等等这个会不会导致 BER 变好如果你按“解扩后符号 SNR sf * 原始符号 SNR”算似乎扩频增益出现了。但这里有个隐性问题在 AWGN 信道中如果发送端发送的是 sf 个码片接收端接收同样多的噪声但若是不扩频发送端这 sf 个时间资源可以用来发送 sf 个不同符号。也就是说比较相同带宽、相同数据速率时扩频多占用了时间/带宽资源单位时间内的能量要补偿。由于我们横轴是 Eb/N0扩频和未扩频都需要传输相同的比特数未扩频系统可以用更多的时间资源发送更多符号而每个符号的输入 Eb/N0 相同所以两者理论 BER 一致。解扩后 SNR 提升的 sf 倍来源于噪声带宽变宽而在相同 Eb/N0 下扩频系统每个码片收到的噪声能量更小。具体仿真时只要发送端不额外归一化能量解扩后 nx闭环 SNR 自然按解扩后的信号幅度算最终 BER 应当与 SF 无关。若变差检查是不是把txChips除以sqrt(sf)了或者 PN 序列与解扩序列不一致或者噪声功率计算错了。4.4 问题四LLR 计算太慢上面给出的softDemod32QAM函数里用了 for 循环遍历 5 个比特每个比特内用矩阵乘法生成 dist0/dist1矩阵维度是符号数乘以 32。如果每帧符号 10 万距离矩阵是 10 万×32内存占用约 25 MB可以接受。但在主循环里频繁调用该函数可能成为瓶颈。优化方法有三种减少需要解调的点数。先做一次粗略的符号判决只对 SNR 较低、距离边界较近的符号计算 LLR高置信度符号直接用硬判结果填充。用knnsearch或分象限减少候选星座点。32QAM 可以按象限划分每个接收符号多半落在某一象限只需要计算该象限附近的几个点但边界处理麻烦。用coder生成 C 代码或者用parfor并行。我实测下来最实用的还是先算距离平方矩阵再对每个比特用min的列索引方式% 一次性计算所有星座点距离再用逻辑索引化简 dist2 abs(rxSym - constellation).^2; [~, hardIdx] min(dist2, [], 2); % 只有当硬判星座点距离次小点非常接近时才计算完整LLR这个方法适合大批量仿真但代码量会增加。如果是课程设计级别10 万符号、200 帧并不慢老实用双层循环也行。4.5 常见问题速查表现象可能原因解决方式BER 曲线整体右移星座未归一化N0 计算错误能量重复归一化检查mean(abs(const).^2)1核对 EbN0 与 N0 关系BER 曲线有平台格雷映射错位LLR 比特位顺序与 bit 表不一致打印星座图与比特表逐一对照软解调 LLR 全为正/全为负噪声方差传错或比特映射表排序不对测试无噪声时 LLR 符号是否与真实比特对应扩频因子越大性能越差发送端除以 sqrt(sf)PN 序列不一致保持功率不变检查收发 PN 同步曲线抖动剧烈误码统计数量太少强制totalErrBits 100或增加帧数仿真在低 SNR 下特别慢每帧符号数过大误码多降低每帧符号数多跑帧用 parfor5. 一些仿真之外的体会这套链路搭完再回头看其实单一的 BER 曲线并不稀奇真正有价值的在于把三个独立的通信概念组合在一起时每个模块的接口该怎么设计。比如 LL 软信息在解扩后应该用什么噪声方差比如格雷映射对软解调 LLR 质量的影响这些在教科书里往往是分开讲的。仿真把它们串起来以后我对“处理增益”的理解也更具体了它不是凭空提升 SNR而是通过时间/带宽资源的交换把干扰和噪声的作用方式改变了在 AWGN 下不体现在抗干扰场景下才体现。如果后续还想继续扩展建议至少做两件事一是把 AWGN 信道换成带窄带干扰的信道对比扩频和未扩频的 BER 差异你会直观看到抗干扰增益二是给软解调的输出接入一个简单的卷积码维特比软译码或者 LDPC 译码看误码曲线在低 SNR 处的下降斜率是否变得更陡。这两步做完这套仿真从课程设计级别直接变成可以写进论文的验证平台。最后分享一个实用小技巧在跑大仿真之前先用 2000 个符号跑通所有 SNR 点确认曲线形状和理论值大致吻合再换大数据量跑精确曲线。我一开始就贪快直接 10 万符号一帧结果星座图表映射反了跑了五分钟才发现曲线在 8 dB 之后就掉不下去。小数据量试错几分钟就能定位问题这是所有通信仿真通用的省时方法。