简介:面向通信工程专业学生与数字调制初学者的MATLAB仿真资料,围绕16QAM星座图绘制与误码率分析展开,可用于课程实验、自学验证及链路性能对比。压缩包共8个文件,均为.m脚本,大小约3KB,涵盖主流程调用、星座点可视化、四比特与二进制码字转换、调制解调、随机二进制源及比特成形等环节,便于按模块阅读和二次修改。已有1927人学习下载。读者可据此搭建端到端16QAM仿真链路,在AWGN条件下观察理想与非理想星座分布,完成最大似然检测并统计不同信噪比下的BER曲线,从而理解幅度相位联合调制、码字映射与接收判决之间的关系,也为后续扩展到高阶QAM或信道编码提供可复用的脚本框架。
1. 从一张糊掉的星座图说起:16QAM 仿真到底在验证什么
有人在 MATLAB 里跑完 16QAM 星座图仿真,星座点整齐漂亮,转头看误码率仿真曲线却和理论值差 3 dB;也有人把点数堆到几十万,BER 曲线在高信噪比处突然躺平。16QAM 星座图仿真及误码率仿真这类脚本,真正要验证的不是图好不好看,而是比特映射、功率归一化、Eb/N0 换算、最小距离判决这条链路有没有对齐。这份 MATLAB 仿真把 16QAM 的 Gray 映射、AWGN 信道、相干解调和蒙特卡洛误码统计串成一个可运行入口,适合通信原理课程设计、链路级仿真入门,以及需要快速改调制阶数做对比的开发者。下面按能跑、能看、能改的顺序拆。
2. 项目骨架与运行入口:MATLAB 里把 16QAM 跑出第一张图
拿到一份 MATLAB 仿真脚本,最怕的是一上来就翻主函数,翻到一半发现参数散在三个文件里。这个项目的组织方式比较朴素:入口、参数、调制、解调、信道、理论曲线、绘图各自一个文件,主函数只负责串流程。先把目录和调用关系看清楚,后面改参数才不会翻车。
2.1 目录结构与脚本分工
我一般会把这类链路仿真拆成下面这种粒度,文件不多,但每个文件只做一件事,排查时能快速定位到某一环。
| 文件 | 作用 | 输入 | 输出 |
|---|---|---|---|
main.m | 仿真入口,串起调制、加噪、解调、统计 | 无 | 命令行 BER 表、两张图 |
config.m | 集中存放调制阶数、比特数、Eb/N0 扫描范围 | 无 | cfg结构体 |
qam16_mod.m | 比特流到 16QAM 复符号 | 比特列向量 | 复符号列向量 |
qam16_demod.m | 复符号到比特流的硬判决 | 接收复符号 | 比特矩阵 |
qam16_const_table.m | 生成 16 个星座点及对应比特表 | 无 | const、bitTable |
gray2pam.m | 2 bit 到 PAM 电平的 Gray 映射 | 两列比特 | 电平值 |
chan_awgn.m | 按 Eb/N0 加复高斯白噪声 | 发送符号、Eb/N0、k | 接收符号 |
ber_theory.m | 计算方形 16QAM 理论误码率 | Eb/N0 线性值 | 理论 BER |
plot_constellation.m | 画发送/接收星座图 | 发送、接收符号 | Figure |
plot_ber.m | 画仿真与理论 BER 曲线 | BER 数组 | Figure |
目录结构可以摆成这样,根目录下直接放函数文件,MATLAB 当前文件夹切过来就能调用,不需要额外加路径:
16qam_sim/ ├── main.m ├── config.m ├── qam16_mod.m ├── qam16_demod.m ├── qam16_const_table.m ├── gray2pam.m ├── chan_awgn.m ├── ber_theory.m ├── plot_constellation.m └── plot_ber.m这种平铺结构适合课程设计和小型链路仿真,缺点是文件一多就容易乱。二次开发时可以把utils/、channels/、plots/拆出来,但第一版不建议过度设计,先把主链路跑通。
2.2 参数初始化与一键运行
参数入口我习惯单独放config.m,不把M=16、N=2e5这种数字硬编码在主循环里。后面换 64QAM、改蒙特卡洛点数、扫不同 Eb/N0 范围,只动一个文件。
function cfg = config() % 16QAM 仿真参数入口 cfg.M = 16; % 调制阶数 cfg.k = log2(cfg.M); % 每符号比特数,16QAM 为 4 cfg.N = 2e5; % 每次蒙特卡洛的比特数 cfg.EbN0_dB = 0:2:16; % Eb/N0 扫描范围,单位 dB cfg.constScale = sqrt(10); % 16QAM 电平 ±1、±3 的平均功率为 10 cfg.rngSeed = 2024; % 固定随机种子,便于复现 endcfg.N是比特数,不是符号数。16QAM 每个符号带 4 bit,所以N=2e5实际产生 5e4 个符号。cfg.EbN0_dB从 0 到 16 dB,步长 2 dB,够画出一条完整的瀑布曲线。cfg.constScale单独拎出来,是为了后面换调制阶数时提醒自己归一化因子也要跟着换。
主入口只做四件事:读参数、生成比特、循环扫 Eb/N0、画图。
function main() cfg = config(); rng(cfg.rngSeed); bits = randi([0 1], cfg.N, 1); % 总比特流,列向量 tx = qam16_mod(bits); % 16QAM 调制 berSim = zeros(size(cfg.EbN0_dB)); for ii = 1:numel(cfg.EbN0_dB) rx = chan_awgn(tx, cfg.EbN0_dB(ii), cfg.k); bitsHat = qam16_demod(rx); berSim(ii) = mean(bitsHat(:) ~= bits); fprintf('Eb/N0 = %2d dB, BER = %.3e\n', cfg.EbN0_dB(ii), berSim(ii)); end berTheory = ber_theory(cfg.EbN0_dB); plot_constellation(tx(1:2000), rx(1:2000)); plot_ber(cfg.EbN0_dB, berSim, berTheory); end这里每个 Eb/N0 点复用同一组发送符号,只重新加噪。这样做的好处是发送端固定,误码率差异只来自噪声,对比理论曲线时更干净。缺点是高信噪比下错误数可能不够,后面第 4 章会讲怎么加错误数停止条件。
运行步骤不复杂,但有几个地方容易卡:
- 打开 MATLAB,把当前文件夹切到
16qam_sim根目录,不要只打开main.m所在文件夹却停在上一层。 - 命令行输入
main,不要输入main.m,MATLAB 会把.m当成文件名解析。 - 看命令行输出的 BER 表,确认 0 dB 时 BER 大约在 0.2 到 0.3 量级,16 dB 时降到 1e-4 以下。
- 看 Figure 1 的星座图,发送端应该是 16 个清晰点,接收端在低 SNR 下散成云团。
- 看 Figure 2 的 BER 曲线,仿真点应该贴着理论线走,偏差在 1 dB 以内算正常。
提示:如果命令行报“未定义函数或变量”,先
pwd看当前路径,再which qam16_mod确认函数能被找到。MATLAB 路径问题比代码问题更常见。
2.3 第一次运行要看的三类输出
第一类输出是命令行 BER 表。不要只看最后一行,从低 SNR 到高 SNR 扫一遍,看曲线是不是单调下降。0 dB 时 16QAM 误码率接近 0.25,因为噪声大到判决边界附近全是错;8 dB 时大概在 1e-2 到 1e-3 之间;12 dB 以后进入瀑布区。如果 0 dB 时 BER 是 0.5,说明比特映射或解调表对不上,先别怀疑噪声。
第二类输出是星座图。发送端 16 个点应该落在±1/√10、±3/√10这两组电平上,归一化后最大幅度约 0.9487。接收端在 0 dB 时点会糊成一团,8 dB 时还能看出 16 个簇,12 dB 时簇心基本清晰。星座图旋转 45 度、点挤成 4 个、I/Q 互换,都是映射表或复数方向的问题。
第三类输出是 BER 曲线和理论线的贴合程度。理论线用3/8*erfc(sqrt(2*EbN0/5))计算,仿真点用蒙特卡洛统计。两条线在低 SNR 处可能有一点偏差,因为理论公式是 Gray 编码下的近似;高 SNR 处如果仿真点突然翘起或躺平,通常是错误数不够或噪声方差算错。
3. 核心链路拆解:调制、加噪、判决与误码统计
主函数只有十几行,真正的细节在四个函数里:比特怎么映射成星座点、Eb/N0 怎么换成噪声方差、接收符号怎么判成比特、误码率怎么统计。这四步任何一步有偏差,曲线都会整体平移,而且这种平移很容易被当成“理论公式不准”糊弄过去。
3.1 星座图映射:从比特到复符号
16QAM 每个符号带 4 bit,拆成高 2 bit 和低 2 bit,分别映射到 I 路和 Q 路的 4 个 PAM 电平。Gray 映射的作用是让相邻星座点只差 1 bit,这样噪声把符号判到邻居点时,只错 1 bit,而不是 2 bit 或 3 bit。
function sym = qam16_mod(bits) % 输入 bits 为列向量,长度必须是 4 的整数倍 b = reshape(bits, [], 4); % 每行 4 bit iVal = gray2pam(b(:,1), b(:,2)); % 前 2 bit 映射 I 路 qVal = gray2pam(b(:,3), b(:,4)); % 后 2 bit 映射 Q 路 sym = (iVal + 1j*qVal) / sqrt(10); % 归一化平均符号功率为 1 end function v = gray2pam(b1, b2) % Gray 顺序:00 -> -3, 01 -> -1, 11 -> +1, 10 -> +3 idx = b1*2 + b2; v = zeros(size(b1)); v(idx == 0) = -3; v(idx == 1) = -1; v(idx == 3) = +1; v(idx == 2) = +3; endreshape(bits, [], 4)把长比特流按每 4 bit 一行排开,行数就是符号数。gray2pam里idx = b1*2 + b2把两位二进制转成 0 到 3 的整数,再按 Gray 顺序落到-3、-1、+1、+3。注意idx==2对应10,映射到+3;idx==3对应11,映射到+1,这两个顺序不能反,反了相邻点就不是 Gray 关系。
归一化除以sqrt(10)是因为 16QAM 四个电平±1、±3的单维平均平方是(1+1+9+9)/4=5,I/Q 两维合计平均功率为 10。不归一化也能跑,但后面算噪声方差时Es不是 1,Eb/N0 换算会多一个系数,曲线整体平移,这块最容易翻车。
星座点与比特的对应关系可以列成表,调映射时直接对:
| I 电平 | I 路 2 bit | Q 电平 | Q 路 2 bit |
|---|---|---|---|
| -3 | 00 | -3 | 00 |
| -1 | 01 | -1 | 01 |
| +1 | 11 | +1 | 11 |
| +3 | 10 | +3 | 10 |
I 路和 Q 路使用同一套 Gray 顺序,合起来 16 个点。相邻水平点只差 I 路 1 bit,相邻垂直点只差 Q 路 1 bit,对角点差 2 bit。噪声把点推到对角时才会错 2 bit,所以 Gray 映射下的 BER 接近 SER/4。
3.2 加噪与信噪比换算:Eb/N0 到 SNR
AWGN 信道看着简单,但Eb/N0、Es/N0、SNR三个量混用是血泪经验里排第一的坑。16QAM 的符号能量Es和比特能量Eb差一个k=4的因子:Es = k*Eb。仿真里通常扫Eb/N0,加噪时先换成Es/N0,再算复噪声方差。
function rx = chan_awgn(tx, EbN0_dB, k) % tx: 发送复符号,平均功率为 1 % EbN0_dB: 比特信噪比,单位 dB % k: 每符号比特数 EbN0 = 10.^(EbN0_dB/10); EsN0 = EbN0 * k; % 16QAM: Es/N0 = Eb/N0 + 10log10(4) N0 = 1 ./ EsN0; % 信号功率归一化为 1 时,N0 = 1/EsN0 noise = sqrt(N0/2) * (randn(size(tx)) + 1j*randn(size(tx))); rx = tx + noise; endEbN0 = 10.^(EbN0_dB/10)把 dB 转线性。EsN0 = EbN0 * k是 16QAM 的关键一步,漏掉它曲线会偏 6 dB 左右,不是 3 dB,因为10log10(4)=6.02 dB。N0 = 1./EsN0成立的前提是发送符号平均功率为 1,所以 3.1 节的sqrt(10)归一化必须做。
复噪声的实部和虚部各占一半方差,所以sqrt(N0/2)分别乘到randn上。如果只给实部加噪、虚部不加,星座图会压成一条线,BER 曲线也会怪。randn(size(tx))保证噪声维度和发送符号一致,列向量进、列向量出。
注意:
awgn(tx, snr, 'measured')也能加噪,但它的snr是信号功率与噪声功率之比,不是Eb/N0。用之前先算snr = EbN0_dB + 10*log10(k),否则曲线对不上还找不到原因。
3.3 解调与判决:最小欧氏距离
解调端做硬判决,把接收复符号判到最近的星座点上,再查表还原 4 bit。16 个点不多,直接算欧氏距离取最小,代码短且不容易错。熟手可以拆成 I/Q 两路独立判决,门限是-2、0、2,但第一版用最小距离更直观。
function bitsHat = qam16_demod(rxSym) % 最小欧氏距离硬判决 [const, bitTable] = qam16_const_table(); rxSym = rxSym(:); dist = abs(rxSym - const.'); % N×16 距离矩阵 [~, idx] = min(dist, [], 2); % 每行最近星座点 bitsHat = bitTable(idx, :); % 查表还原比特 end function [const, bitTable] = qam16_const_table() levels = [-3 -1 1 3]; bit2 = [0 0; 0 1; 1 1; 1 0]; % 与 gray2pam 保持一致 const = zeros(16,1); bitTable = zeros(16,4); p = 1; for i = 1:4 for q = 1:4 const(p) = (levels(i) + 1j*levels(q)) / sqrt(10); bitTable(p,:) = [bit2(i,:), bit2(q,:)]; p = p + 1; end end endconst.'是 1×16 行向量,rxSym是 N×1 列向量,MATLAB 的隐式扩展会生成 N×16 的距离矩阵。min(dist, [], 2)沿第二维取最小,得到每个接收符号的最近星座点索引。bitTable的行顺序必须和const一一对应,否则判决对了、查表错了,BER 会停在一个固定值下不去。
bit2的顺序是00、01、11、10,对应电平-3、-1、+1、+3,和gray2pam的赋值完全一致。二次开发换 64QAM 时,levels要扩成 8 个电平,bit2要扩成 3 bit 的 Gray 表,const和bitTable的循环也要跟着改。不要只改M一个数字就指望全链路自动适配,这块没有后悔药。
3.4 误码率统计与理论曲线对齐
误码率统计就是比对发送比特和判决比特,数错位个数除以总比特数。理论曲线用方形 16QAM 的近似公式,Gray 编码下 BER 约等于3/8*erfc(sqrt(2*EbN0/5))。仿真点和理论线贴不贴,是判断链路对不对的最快标准。
function ber = ber_theory(EbN0_dB) EbN0 = 10.^(EbN0_dB/10); ber = 3/8 * erfc(sqrt(2*EbN0/5)); end这个公式来自SER ≈ 3Q(sqrt(0.8*EbN0)),再除以每符号 4 bit 得到 BER。低 SNR 时近似误差稍大,高 SNR 时贴合得很好。画图时用半对数坐标,BER 为 0 的点要置成NaN,否则semilogy会直接跳过或者报警。
function plot_ber(EbN0_dB, berSim, berTheory) figure; semilogy(EbN0_dB, berTheory, 'k-', 'LineWidth', 1.5); hold on; semilogy(EbN0_dB, berSim, 'bo', 'MarkerSize', 6); grid on; xlabel('E_b/N_0 (dB)'); ylabel('BER'); legend('理论值', '仿真值'); ylim([1e-6 1]); endberSim里如果有 0,semilogy不会画那个点,看起来像曲线断了一截。可以在画图前加berSim(berSim==0)=NaN,让 MATLAB 明确跳过。理论线用黑色实线,仿真点用蓝色圆圈,一眼能看出偏差。如果仿真点整体在理论线右边 1 dB 以内,算正常;偏 3 dB 以上,先查EsN0 = EbN0*k和归一化因子。
4. 踩坑排查:误码率曲线对不上理论值时先查这五处
16QAM 仿真的代码量不大,但出错后的表现很相似:曲线平移、曲线躺平、星座图旋转、跑得太慢、换阶数全错。下面五条是我在调试这类链路时反复遇到的,每条按现象、原因、解决三步走,照着排查能省不少时间。
4.1 现象:误码率曲线整体平移 3 dB 或 6 dB
现象很直观:仿真曲线形状和理论线平行,但整体往右偏,低 SNR 和高 SNR 都偏同样的量。原因通常是Eb/N0和Es/N0混用,或者归一化因子用错。16QAM 的Es = 4Eb,如果加噪时直接把EbN0当EsN0用,曲线会偏约 6 dB;如果归一化时除以sqrt(16)而不是sqrt(10),符号平均功率不是 1,噪声方差也会跟着错。
解决分两步。先看chan_awgn里有没有EsN0 = EbN0 * k,没有就补上。再算发送符号的平均功率mean(abs(tx).^2),16QAM 归一化后应该接近 1,如果不是 1,检查qam16_mod末尾是不是/sqrt(10)。这两个地方对上了,曲线平移基本能消掉。
4.2 现象:高 SNR 处 BER 降到 1e-4 就不动了
现象是曲线尾巴突然变平,理论线还在往下走,仿真点却横着。原因一般是蒙特卡洛点数不够,高 SNR 下错误事件太少,统计不到更低的误码率。比如N=2e5比特,BER=1e-4 时平均只有 20 个错误,再往下就靠运气了。固定随机种子也会让某些点重复出现,看起来像“降不下去”。
解决是加错误数停止条件,不要只按固定比特数跑。每个 SNR 点至少累计 100 个错误,或者达到最大比特数就停。N从 2e5 提到 1e6 到 1e7,高 SNR 点单独加长。跑之前把rng种子换一下,确认曲线不是种子造成的玄学。
maxErr = 100; maxBits = 1e7; errCnt = 0; total = 0; while errCnt < maxErr && total < maxBits bits = randi([0 1], 1e5, 1); tx = qam16_mod(bits); rx = chan_awgn(tx, EbN0_dB, 4); bitsHat = qam16_demod(rx); errCnt = errCnt + sum(bitsHat(:) ~= bits); total = total + numel(bits); end ber = errCnt / total;maxErr控制统计精度,maxBits防止低 SNR 时死循环。1e5是每批比特数,分批跑比一次性开1e7更省内存。total累加的是比特数,不是符号数,别写错。
4.3 现象:星座图旋转 45 度或点挤成 4 个
星座图旋转通常不是噪声问题,而是 I/Q 顺序或复数转置弄错了。比如qam16_mod里把iVal + 1j*qVal写成qVal + 1j*iVal,整个星座图会沿对角线翻转;qam16_demod里用const'而不是const.',共轭转置会把虚部符号翻掉,判决全乱。点挤成 4 个,多半是bitTable和const的行列顺序对不上,16 个点判成了 4 个。
解决是先把发送端 16 个点画出来,对照 3.1 节的电平表,确认±1/√10和±3/√10都在。再画接收端,低 SNR 下应该是云团,不是 4 个孤立簇。最后检查const.'是不是非共轭转置,bitTable的每一行是不是和const同一个循环顺序生成。这两处对上了,旋转和挤点基本能消。
4.4 现象:跑一晚上还没出图
现象是 MATLAB 一直忙,命令行不输出,CPU 占满。原因通常是逐符号循环判决,16 个星座点每个符号循环一次,N=1e6就是 1600 万次循环,MATLAB 的 for 循环扛不住。也有人把semilogy放在循环里,每跑一个点画一次图,图窗刷新比计算还慢。
解决是向量化。qam16_demod里用 N×16 距离矩阵一次算完,不要写for n=1:N。主循环里每个 SNR 点只画一次图,或者先存数组最后统一画。parfor可以加速 SNR 扫描,但每个点内部要独立,随机种子要按 worker 设置,否则结果不可复现。先把向量化做掉,再考虑并行。
4.5 现象:换成 64QAM 后 BER 全是 0.5
现象是改完M=64,BER 停在 0.5 左右,星座图看着也不对。原因通常是只改了M,没改k、归一化因子、星座表和理论公式。64QAM 每符号 6 bit,电平是±1、±3、±5、±7,平均功率是 42,归一化要除以sqrt(42),不是sqrt(10)。bitTable还是 16 行,解调查表直接越界或错位。
解决是把调制阶数参数化:k=log2(M),levels=-(sqrt(M)-1):2:(sqrt(M)-1),归一化因子sqrt(2/3*(M-1))。星座表和比特表按sqrt(M)的维度循环生成,理论公式换成对应阶数的 SER 近似。改完先跑M=16确认和原来一致,再跑M=64,不要一步跳过去。
5. 二次开发:把 16QAM 仿真改成可复用的链路模板
能跑通只是第一步,真正花时间的是后面改链路:加瑞利信道、加脉冲成型、换调制阶数、对比不同均衡算法。如果第一版把参数硬编码在主循环里,后面每改一个点都要翻三个文件。我的做法是先抽配置层,再把函数签名统一,最后加信道和成型模块,每加一块都用 BER 曲线做回归。
5.1 抽参数配置层:一个结构体管住所有开关
配置层不是简单把数字挪到config.m,而是把所有会影响 BER 的开关集中起来:调制阶数、信道类型、是否开脉冲成型、均衡方式、蒙特卡洛精度。主函数只读cfg,不直接写死任何数字。
function cfg = config() cfg.M = 16; cfg.k = log2(cfg.M); cfg.N = 2e5; cfg.EbN0_dB = 0:2:16; cfg.channel = 'awgn'; % 'awgn' 或 'rayleigh' cfg.usePulseShape = false; % 是否开根升余弦 cfg.sps = 4; % 每符号采样数 cfg.beta = 0.35; % 滚降系数 cfg.equalizer = 'none'; % 'none' 或 'zf' cfg.maxErr = 100; cfg.maxBits = 1e7; cfg.seed = 2024; endcfg.channel用字符串开关,主函数里switch cfg.channel分流到chan_awgn或chan_rayleigh。cfg.usePulseShape控制是否走成型滤波,不开就走符号级仿真,开了就走波形级仿真。cfg.equalizer先留'none'和'zf'两个选项,后面加 MMSE 再扩。这样改链路时只动配置和对应函数,不动主循环。
5.2 加瑞利信道与均衡:从理想信道估计到导频
AWGN 跑通后,很多人第一件事是加瑞利信道。瑞利信道不是简单乘一个复高斯系数,还要考虑接收端怎么估计这个系数。第一版可以先用理想信道估计,确认 BER 曲线形状对,再加导频和插值。
function [rx, h] = chan_rayleigh(tx, EbN0_dB, k) EbN0 = 10.^(EbN0_dB/10); EsN0 = EbN0 * k; N0 = 1 ./ EsN0; h = (randn(size(tx)) + 1j*randn(size(tx))) / sqrt(2); noise = sqrt(N0/2) * (randn(size(tx)) + 1j*randn(size(tx))); rx = h .* tx + noise; endh是均值为 0、每维方差 1/2 的复高斯系数,abs(h).^2服从均值为 1 的指数分布。rx = h.*tx + noise是平坦瑞利信道,没有多径时延。理想均衡直接rxEq = rx./h,但这要求接收端已知h,实际系统要用导频估计。
% 理想信道估计下的迫零均衡 rxEq = rx ./ h; bitsHat = qam16_demod(rxEq);迫零均衡在深衰落时会把噪声放大,h接近 0 的那些符号误码率很高。第一次加瑞利信道,BER 曲线会比 AWGN 差很多,这是正常的,不要以为代码错了。想改善就加 MMSE 均衡或者分集,但那是另一个话题,先把理想估计跑通。
5.3 加脉冲成型与匹配滤波:从符号级到波形级
符号级仿真只关心采样点,波形级仿真要加上采样、成型滤波、匹配滤波和降采样。根升余弦滤波器成对使用,发送端和接收端各一个,合起来是升余弦响应,满足无码间干扰条件。
sps = cfg.sps; beta = cfg.beta; span = 6; hRRC = rcosdesign(beta, span, sps, 'sqrt'); txUp = upsample(tx, sps); txWave = conv(txUp, hRRC, 'same'); rxWave = conv(rx, hRRC, 'same'); rxSym = downsample(rxWave, sps);rcosdesign(beta, span, sps, 'sqrt')生成根升余弦滤波器,span是符号数的一半,sps是每符号采样数。upsample(tx, sps)在符号间插零,conv(..., 'same')保持波形长度和输入一致。接收端匹配滤波后再downsample回符号速率。
这里有个延迟对齐的坑:conv的'same'只是取中间段,实际群延迟是span*sps/2个采样点,严格来说要做延迟补偿。第一版可以用'same'近似,看星座图有没有明显旋转或发散;如果发散,改成conv(..., 'full')再手动截取span*sps/2+1 : end-span*sps/2。脉冲成型这块不追求一次完美,先把波形跑出来,再用 BER 曲线验证。
5.4 验证方法:三张图对照改造前后
每加一个模块,不要只看 BER 一个数。我一般会留三张图做回归:星座图、BER 曲线、频谱或眼图。星座图看判决点有没有收敛,BER 曲线看和理论值的偏差,频谱看成型滤波后的带宽是不是收窄了。
| 改造项 | 先看什么 | 合格标准 |
|---|---|---|
| 加瑞利信道 | 接收星座图 | 点围绕原点扩散,不是固定 16 簇 |
| 加迫零均衡 | BER 曲线 | 高 SNR 处有下降趋势,不躺平在 0.5 |
| 加脉冲成型 | 频谱/眼图 | 带宽比矩形脉冲窄,眼图张开 |
| 换 64QAM | 星座图 + BER | 64 个点清晰,BER 比 16QAM 差约 4 到 5 dB |
改造顺序建议从 AWGN 开始,每加一块先关掉其他开关,确认单模块没问题再叠加。比如先开瑞利信道、不开成型,跑通再开成型、不开均衡,最后全开。这样出问题时能快速定位是哪一块引入的,不用在整条链路里猜。
6. 从能跑到能改:我给 16QAM 仿真留的三个后门
这份仿真我改过好几轮,最早的一版把M=16、N=2e5、sqrt(10)全写死在主循环里,后来想加一个 64QAM 对比,改了半小时发现qam16_demod里还有一张 16 行的比特表没动,BER 直接跑到 0.5。从那以后我每次搭链路仿真都会留三个后门,后面改起来省事。
第一个后门是配置结构体。所有会影响 BER 的参数都进cfg,主函数不出现裸数字。M、k、EbN0_dB、channel、usePulseShape这些开关集中在一处,改链路时只动配置和对应函数,不动主循环。第二个后门是函数签名统一。调制、信道、解调都按(输入, cfg)或者(输入, 参数)的固定顺序传参,不靠全局变量,也不在函数内部读config()。这样单测某一个模块时可以直接构造参数,不用先跑整个main。第三个后门是输出结构体。main不直接画图,而是把berSim、berTheory、tx、rx、cfg打包成result返回,画图函数只读result。后面加新图或者批量跑参数扫描,不用改主流程。
现在每次改完链路,我的习惯是先跑M=16、AWGN、不开成型,确认 BER 和理论线偏差在 1 dB 以内,再逐项打开瑞利信道、均衡、脉冲成型。每开一项存一张图,出问题就回退到上一个能对上的版本。这套流程不高级,但能避免“改了一个参数,曲线偏了 6 dB,查一晚上发现是归一化因子没换”的翻车。希望帮到你。
本文还有配套的精品资源,点击获取