16QAM星座图与误码率仿真:映射、加噪、判决对账指南

发布时间:2026/10/23 18:29:26
16QAM星座图与误码率仿真:映射、加噪、判决对账指南

简介:围绕16QAM星座图与误码率仿真整理的MATLAB学习素材,面向正在学习数字调制、通信系统仿真与BER性能评估的本科生、研究生及工程入门者,可用于理解幅度相位联合调制、AWGN信道下端到端链路搭建与解调判决流程。压缩包共8个文件,均为.m脚本与函数文件,整体约3KB,涵盖主流程调用、星座图绘制、四进制与二进制转换、调制解调、随机二进制序列生成及比特成形等模块,便于按功能拆解阅读和二次修改。已有1927人学习下载,说明其在通信仿真入门场景中具有一定参考价值。读者可据此复现16QAM信号映射、星座点分布观察、接收端最大似然检测以及不同信噪比下的误码率曲线,并借助模块化脚本梳理从数据源到性能评估的完整仿真思路,适合课程实验、自学练习和代码对照。

1. 星座图仿真不是画点:16QAM 的误码率要从映射、加噪、判决三处对账

16QAM 星座图仿真和误码率仿真,在 MATLAB 里十分钟就能跑出一条曲线,但曲线和理论差 3 dB 的时候,很多人就卡住了——不知道问题出在映射表、噪声方差,还是判决门限。这份仿真资源想解决的就是这件事:从比特到复数符号的格雷映射、平均符号能量归一化、AWGN 加噪、最近邻判决、蒙特卡洛统计,一条链路跑通,最后能和 erfc 理论曲线对上。适合正在做数字通信课程设计、刚进链路级仿真门槛的开发者,也适合要给自家接收机算法做误码性能基线的人。下面按「映射表怎么建 → 星座图怎么画 → 误码率怎么统计 → 哪里容易翻车 → 怎么扩到衰落信道」的顺序拆开。

2. 16QAM 映射表怎么建:格雷码、归一化与 MATLAB 查表实现

这一章是整个仿真的地基。映射表建错,后面画出来的星座图看着也是 16 个点、看着也挺对称,但误码率曲线会莫名其妙比理论好或者比理论差,而且这种错很难靠肉眼发现。血泪经验是:先把映射和归一化写死成一张常量表,再往下写别的。

2.1 为什么 16QAM 必须配格雷码

16QAM 每个符号携带 4 个比特,16 个星座点排成 4×4 的方阵。符号在传输中被打错是常态,关键在于:一个符号错了,会连带错几个比特。如果采用自然二进制顺序排列,相邻星座点之间可能相差 2 个甚至 3 个比特,一个符号判决滑到隔壁,就一次性错 2~3 bit;采用格雷码排列,任意两个相邻星座点的比特表示只差 1 位,符号错一次最多错 1 bit。

这个差别在仿真里体现得非常直接。不加格雷映射时,同样的 Es/N0 下 BER 会比格雷映射高出一截,高信噪比区间尤其明显——因为这时候错误主要来自「滑到最近邻点」,而不是「滑到远处」。有人拿自然映射跑出来的曲线去和教科书上的公式比,发现怎么也对不上,原因就在这里:教科书的公式默认格雷映射。

我的做法是把「2 bit → 4 个电平」的映射单独抽出来做成常量表。I 路 2 bit 对应 4 个电平,Q 路 2 bit 对应 4 个电平,两路正交叠加就是 16 个点。2 bit 格雷码的顺序是00 → 01 → 11 → 10,对应电平-3, -1, +1, +3,这样相邻电平(-3 和 -1、-1 和 +1、+1 和 +3)对应的标号都只差一位。

2.2 归一化:把平均符号能量压到 1

电平取±1, ±3的时候,每个符号的平均能量是(1+1+9+9)/4 = 5(单维),I/Q 两维加起来是 10。如果不做归一化,平均符号能量 Es = 10,你按 Es = 1 去算噪声方差,实际加进去的噪声就偏大 10 dB,曲线整体左移,看起来「仿真比理论好 10 dB」,这是个很典型的假象。

标准做法是整张表除以sqrt(10),让 Es 归一化到 1。这样后面写噪声方差的时候可以直接用N0 = 1/(Es/N0 线性值),不用每次去乘系数,减少一处出错点。

提示:归一化只对电平表做一次,不要在调制函数里每次调用都现算sqrt(10),也不要在解调端用另一套电平值。一旦调制和解调的电平表不一致,判决门限就会整体偏,BER 曲线会在高信噪比处卡在一个错误平台上不往下掉。

2.3 MATLAB 查表实现:4 比特拆 I/Q 两路

下面是调制与加噪的两个核心函数。整个实现只有十几行,但没有循环、没有bi2de,靠的是向量化索引,跑几百万比特也在秒级。

function s = qam16_mod(bits) % 16QAM 格雷映射调制:每 4 bit 一组,前 2 bit 定 I 路,后 2 bit 定 Q 路 % 输入 bits 长度必须是 4 的整数倍,输出为列向量复符号,平均能量 Es = 1 bits = bits(:); tab = [-3 -1 3 1] / sqrt(10); % 格雷码电平表:索引 1..4 对应标号 00,01,10,11 idxI = bits(1:4:end)*2 + bits(2:4:end); % I 路 2bit 标号 0..3 idxQ = bits(3:4:end)*2 + bits(4:4:end); % Q 路 2bit 标号 0..3 s = tab(idxI + 1) + 1j * tab(idxQ + 1); % MATLAB 索引从 1 开始,所以要 +1 end function r = awgn_esn0(s, esn0_dB) % 按 Es/N0 加复高斯白噪声,前提是 s 的平均符号能量已经归一化到 1 esn0 = 10^(esn0_dB/10); n0 = 1 / esn0; % Es = 1 时,N0 = 1/(Es/N0) n = sqrt(n0/2) * (randn(size(s)) + 1j*randn(size(s))); % 每维方差 N0/2 r = s + n; end

tab的顺序是[-3 -1 3 1]而不是[-3 -1 1 3],这是整段代码里最容易写错的地方。索引 1、2、3、4 分别对应标号00、01、10、11,按格雷码规则,标号10应该落在+3、标号11落在+1,所以表里第 3、4 项是反的。写成[-3 -1 1 3]程序照样跑,星座图照样好看,但误码率会比理论差一截——这就是那种「跑得通但对不上」的典型故障。

awgn_esn0里n0 = 1/esn0成立的前提就是上一节的归一化。每维方差取n0/2是因为复基带噪声的总功率是n0,实部虚部各分一半,用randn生成实数再拼成复数,正好符合这个分布。

qam16_mod的输入要求是 4 的倍数,这个约束要在调用方保证。常见做法是randi([0 1], 4*N, 1)直接生成,N 是符号数。如果比特源来自外部文件或者编码器输出,长度不整除 4 的时候需要补零,补了多少位要在误码统计时相应扣掉,否则统计出来的 BER 会偏低。

3. 星座图可视化:把加噪前后的点画在一张图上

星座图这部分的验收标准不是「好看」,是「能不能一眼看出判决边界和噪声扩散的关系」。画对了,你能从图上直接读出为什么某个信噪比下开始大量误判;画错了,就是一堆糊在一起的散点。

3.1 Eb/N0 到 Es/N0:那 6.02 dB 别省

论文和教材上的误码率曲线横轴几乎都是 Eb/N0(每比特能量噪声比),而加噪的时候需要的是 Es/N0(每符号能量噪声比)。两者的关系是Es/N0 = Eb/N0 + 10*log10(k),16QAM 的 k = 4,也就是要加 6.02 dB。

这一步漏掉的话,仿真曲线相对理论会整体偏移 6 dB,形状一模一样,位置全错。有个取巧但很稳的习惯:在脚本里统一用 Eb/N0 作为外层循环变量,进加噪函数之前转成 Es/N0,函数内部只认 Es/N0,永远不在加噪函数里做比特能量换算。这样「换算」这件事在整条链路上只出现一次,出错面就收窄了。

k = 4; % 16QAM 每符号比特数 ebn0_dB = 12; esn0_dB = ebn0_dB + 10*log10(k); % 12 + 6.02 = 18.02 dB r = awgn_esn0(qam16_mod(randi([0 1], 4000, 1)), esn0_dB);

3.2 四档信噪比的散点图

取 10、16、22、30 dB 四档,2×2 排开画,能很直观地看到「噪声把点吹散 → 点云开始越过判决线 → 点云基本收拢」的过程。30 dB 那一档基本就是干净星座图,用来确认映射表是对的;10 dB 那一档基本糊成一团,用来确认仿真没有算错噪声。

figure('Position', [100 100 900 900]); esn0List = [10 16 22 30]; for ii = 1:4 subplot(2,2,ii); b = randi([0 1], 4*2000, 1); s = qam16_mod(b); r = awgn_esn0(s, esn0List(ii)); plot(real(s), imag(s), 'o', 'MarkerSize', 5, 'LineWidth', 1); % 理想星座点 hold on; plot(real(r), imag(r), '.', 'MarkerSize', 6); % 加噪后的接收点 grid on; axis equal; axis([-1.6 1.6 -1.6 1.6]); xlabel('同相分量 I'); ylabel('正交分量 Q'); title(sprintf('Es/N0 = %d dB', esn0List(ii))); hold off; end

axis equal不能省,否则 I/Q 两轴比例不一致,看起来像矩形星座,会误导判断。坐标范围取±1.6是因为归一化后最外圈点的坐标是±3/√10 ≈ ±0.949,留出约 0.65 的余量正好能看清噪声尾巴。

3.3 判决域与门限线

16QAM 的最优判决(AWGN 下)就是 I、Q 两路各自独立做最近邻,门限落在-2/√10、0、+2/√10三条竖线(Q 路同理是三条横线)。把这几条线画到散点图上,噪声点和判决边界的相对位置一目了然。

th = [-2 0 2] / sqrt(10); for v = th xline(v, '--', 'Color', [0.4 0.4 0.4]); yline(v, '--', 'Color', [0.4 0.4 0.4]); end

门限值取 ±2 而不是 ±1 或 ±3,是因为相邻电平和为 -4、0、+4,中点就是 ±2。这一点在解调函数里必须和调制表严格对应,很多「BER 曲线在高信噪比下有错误平台」的问题,根子就在门限算成了±1/√10或者±3/√10。

如果要在图上标格雷码标号方便对照,可以用text在每点旁边写上 4 位二进制串,注意标号顺序仍然是00, 01, 10, 11对应电平-3, -1, +3, +1,别按大小顺序标。

4. 误码率仿真:蒙特卡洛循环的帧结构与误差控制

星座图只能定性,误码率曲线才是定量结论。这一章的重点是循环怎么写:帧长、停止条件、统计口径,三样东西定错一样,跑一晚上出来的数据就白费。

4.1 帧长怎么定:20000 符号是个折中

误码率仿真的基本结构是「生成比特 → 调制 → 加噪 → 解调 → 比对」。有两种循环写法:一是外层固定信噪比、内层一直跑,二是把整条链路的比特数固定住。我一般用前者,内层按帧跑,每帧 20000 个符号(8 万比特)。

20000 这个数字不是玄学,是内存和统计独立性的折中。一帧 8 万比特,双精度复数符号占 20000×16 字节 ≈ 320 KB,加上中间变量一帧不到 2 MB,跑一万帧也不会撑爆内存;同时每帧重新randi生成比特、重新randn生成噪声,天然保证了帧与帧之间统计独立。

帧长如果取得太小(比如 100 符号),循环次数暴涨,MATLAB 的函数调用开销占比过高;取得太大(比如 1000 万符号),单帧内存几十 MB,而且一旦中途出错前面全白跑。20000 这个量级比较稳。

4.2 错误数达标比固定比特数更省时间

高信噪比区间的 BER 可能低到 1e-5 甚至 1e-6。如果你固定跑 100 万比特,那在 1e-5 处只期望错 10 个,统计涨落能有 30% 以上,曲线会抖得像心电图;跑到 1e-6 就更夸张,可能一个错都没有,直接断线。

我的处理是按错误数停:累计错误达到 100 个就停,同时设一个总比特上限(比如 2000 万)防止低信噪比时跑不完。100 个错误对应的相对标准差约 10%,画到图上曲线比较平滑。

targetErr = 100; % 目标错误数 maxBit = 2e7; % 总比特上限,防低 SNR 死循环 nErr = 0; nBit = 0; while nErr < targetErr && nBit < maxBit b = randi([0 1], 4*20000, 1); % 一帧 20000 符号 s = qam16_mod(b); r = awgn_esn0(s, esn0_dB); bh = qam16_demod(r); nErr = nErr + sum(bh ~= b); nBit = nBit + numel(b); end ber = nErr / nBit;

maxBit要设,是因为 0~2 dB 这一段 BER 在 0.1 以上,100 个错误几百比特就到了;但如果你把曲线往外推到负信噪比做演示,没有上限的话循环可能跑很久。这两个参数按需调整,目标错误数一般取 100~200,往下降到 50 曲线就开始抖了。

4.3 与 erfc 理论曲线对齐:完整链路

矩形 16QAM 在 AWGN 下的精确符号错误率是Ps = 3Q(x) - 2.25Q²(x),其中x = sqrt(0.8·Eb/N0 线性值),Q(x) = 0.5·erfc(x/√2)。格雷映射下比特错误率近似为Ps/4。这个公式在中等以上信噪比区间和仿真的吻合度很好,可以直接拿来当验收尺子。

function bits = qam16_demod(r) % 16QAM 格雷映射解调:I/Q 各自最近邻判决,门限 ±2/sqrt(10) 和 0 tab = [-3 -1 3 1] / sqrt(10); th = [-2 0 2] / sqrt(10); invGray = [0 1 3 2]; % 排序位置 -> 格雷标号 idxI = invGray(sum(real(r) > th, 2) + 1); % 0..3 idxQ = invGray(sum(imag(r) > th, 2) + 1); n = numel(r); bits = zeros(4*n, 1); bits(1:4:end) = floor(idxI/2); % I 路高位 bits(2:4:end) = mod(idxI, 2); % I 路低位 bits(3:4:end) = floor(idxQ/2); bits(4:4:end) = mod(idxQ, 2); end % ---- 主循环与理论曲线对比 ---- ebn0 = 0:2:14; berSim = zeros(size(ebn0)); for ii = 1:numel(ebn0) esn0_dB = ebn0(ii) + 10*log10(4); nErr = 0; nBit = 0; while nErr < 100 && nBit < 2e7 b = randi([0 1], 4*20000, 1); r = awgn_esn0(qam16_mod(b), esn0_dB); bh = qam16_demod(r); nErr = nErr + sum(bh ~= b); nBit = nBit + numel(b); end berSim(ii) = nErr / nBit; end ebn0Lin = 10.^(ebn0/10); x = sqrt(0.8 * ebn0Lin); Qf = 0.5 * erfc(x/sqrt(2)); berTheory = (3*Qf - 2.25*Qf.^2) / 4; semilogy(ebn0, berSim, 'o-', ebn0, berTheory, 'k--'); grid on; xlabel('Eb/N0 (dB)'); ylabel('BER'); legend('仿真', '理论', 'Location', 'southwest');

invGray = [0 1 3 2]是tab的逆向查找表:排序位置 0、1、2、3 分别对应电平-3, -1, +1, +3,而这三个电平在tab里的标号是 0、1、3、2。写错这张表的表现是:星座图完全正常,但 BER 曲线会比理论差——因为解调出来的比特顺序和调制时对不上,相当于人为增加了错误。

bits(1:4:end)这一组赋值的顺序必须和qam16_mod里bits(1:4:end)的取用顺序一一对应(I 高位、I 低位、Q 高位、Q 低位)。这两处只要有一处顺序换了,整条曲线就废了,而且因为错的是比特位置而不是符号,星座图看不出来。

5. 避坑:仿真的 BER 和理论曲线对不上,先查这五处

曲线对不上理论,九成的问题集中在这五个地方。按「现象 → 原因 → 解决」列出来,下次直接照单排查。

5.1 映射与归一化环节的三个坑

坑一:曲线形状对,整体平移了 6 dB。现象是仿真曲线和理论曲线平行,但始终差大约 6 dB 的位置。 原因是 Eb/N0 和 Es/N0 换算漏了。16QAM 每符号 4 bit,10*log10(4) = 6.02 dB,加噪函数拿到的是 Es/N0,如果直接把 Eb/N0 传进去,实际噪声就小了 6 dB,仿真显得「更好」。 解决是把换算固定在主循环里做一次,加噪函数只接受 Es/N0,函数名里带上esn0字样,写代码的时候就不会混。

坑二:高信噪比处 BER 跌到某个值就不动了,形成错误平台。现象是曲线在 1e-3 或 1e-4 附近走平,再加大功率也不降。 原因是调制表和判决门限不匹配。最常见的是tab写成[-3 -1 1 3](少了格雷交换),或者门限写成±1/√10、±3/√10。这时候符号本身没有错,但比特映射错位,等价于恒定数量的固定错误。 解决是单独写一段自检:调制一个已知的 4 比特,看输出的星座点坐标是不是落在预期位置,再把坐标喂回解调,看比特能不能原样出来。这个往返测试十行代码,比盯曲线快得多。

坑三:归一化系数用错,曲线整体左移约 10 dB。现象是仿真比理论好一大截,而且信噪比越高差距越明显。 原因是电平表没除以sqrt(10),Es 实际是 10 而不是 1,但加噪函数仍按 Es=1 算N0 = 1/(Es/N0),等于噪声按 1/10 的功率在加。 解决办法是把归一化写进电平表定义那一行,注释里标明Es = 1,全脚本不再出现第二个sqrt(10)的除法。

5.2 信噪比与统计环节的两个坑

坑四:低信噪比区曲线剧烈抖动,相邻点能差一个数量级。现象是 0~4 dB 那几个点上下跳。 原因是错误数太少。低 SNR 下虽然 BER 高,但如果总比特数只有几万,统计涨落依然很大;更常见的是有人在高 SNR 段仍固定跑 1e5 比特,一个错误都没有,画出来直接断线。 解决办法是用错误数作为停止条件,目标 100 个错,同时设总比特上限。另外画图前把ber == 0的点剔除,否则semilogy会因为 log(0) 报警告并且断线。

坑五:换了随机种子,曲线整体跳了一截。现象是两次运行结果差异超出预期。 原因是rng没固定,或者固定在了错误的位置——比如在循环内部每次都rng(1),那每一帧的比特和噪声完全一样,等于只有一帧的样本,统计量严重不足。 解决是在脚本开头rng(2024)固定一次,循环内部绝不重置。同时记住:固定种子只保证可复现,不改善统计精度,曲线是否可信还是要看错误数。

注意:蒙特卡洛仿真的结果是随机变量,两条曲线在低信噪比区有 10%~20% 的相对差异属于正常范围。判断「仿真错了」之前,先把每个点的错误数打印出来看一眼,错误数不到 100 的点不值得较真。

6. 从 AWGN 推到瑞利:一套能反复用的验证套路

AWGN 跑通只是及格线,真正能说明接收机抗不抗打,得看衰落信道。改动其实只有两行:乘一个复高斯信道系数,接收端做理想信道估计除回去。

function r = rayleigh_chan(s, esn0_dB) % 平坦瑞利衰落 + AWGN,接收端假设理想信道估计(完美 CSI) h = (randn(size(s)) + 1j*randn(size(s))) / sqrt(2); % CN(0,1) esn0 = 10^(esn0_dB/10); n0 = 1 / esn0; n = sqrt(n0/2) * (randn(size(s)) + 1j*randn(size(s))); r = h .* s + n; r = r ./ h; % 理想均衡:噪声被 h 除,等效方差变成 n0/|h|^2 end

关键在最后一行。除以h之后,信号恢复成s,但噪声方差变成了n0/|h|²——深衰落时|h|很小,噪声被放大得很厉害,这就是瑞利信道误码率曲线在高 SNR 处斜率变缓的原因。这个「除回去」的动作必须在加噪之后做,顺序反了就成了先把噪声除掉再加噪,等于白送增益。

验证套路我一般按三步走。第一步,先在 AWGN 下把仿真和erfc理论对齐,误差控制在 0.5 dB 以内,这一步确认映射、归一化、判决都没问题。第二步,把信道换成瑞利,和瑞利下的理论 BER(0.5*(1 - sqrt(Eb/N0/(1+Eb/N0)))是 BPSK 的,16QAM 要用数值积分或者近似式)对比,观察高 SNR 处的斜率是否一致。第三步,扫不同的targetErr(50 / 100 / 200)看曲线是否收敛,收敛了才敢往报告里写。

下面这张表是我每次跑完会对照的检查项:

检查项合格标准不合格的典型表现
星座点往返调制→解调比特零错误固定数量错误,BER 有平台
Es 归一化平均abs(s).^2≈ 1曲线整体平移约 10 dB
Eb/Es 换算与理论曲线差 < 1 dB平行平移 6 dB
统计量每点错误数 ≥ 100低 SNR 段剧烈抖动
判决门限±2/√10与0高 SNR 处提前走平

有个教训印象很深:早些年跑一组 16QAM 曲线,仿真和理论差 3 dB,我盯着噪声方差和 erfc 公式查了大半天,最后发现是qam16_demod里invGray写成了[0 1 2 3]。星座图漂亮得挑不出毛病,曲线却怎么也贴不上。从那以后我每次改完映射表,都强制走一遍「单符号往返测试」——随便挑 4 个比特,过一遍调制再解调,打印出来对一眼,十秒钟的事,能省掉一整晚的排查。希望帮到你。

本文还有配套的精品资源,点击获取