简介:一套面向16QAM调制解调仿真的Matlab代码,适合通信工程专业学生、研究人员以及期望掌握数字调制误码率分析的技术开发者。压缩包共含8个MATLAB脚本文件,大小仅3KB,代码结构清晰,包含主控流程、星座图绘制、调制/解调、4位与2位比特转换、随机二进制序列生成及波形整形等功能模块。通过运行主函数,可完成从数据产生、16QAM调制、AWGN信道模拟、解调判决到误码率统计的完整链路仿真,并能直接绘制星座图,直观展示信号分布。星座图上的坐标分布清晰,便于观察判决门限与噪声容限。用户可根据注释调整信噪比等参数,绘制不同条件下的误码率曲线,结合星座图观察噪声对信号的影响,通过对比不同信噪比下的误码率结果,可评估系统抗噪声能力,并加深对调制机制的理解。目前已有1927人学习下载。
1. MATLAB 里的 16QAM 星座图与误码率仿真:从跑通到看懂
刚接触数字通信仿真那会儿,我拿着别人的 16QAM 脚本跑出一张星座图,满屏五颜六色的点挤成一团,压根分不清哪个是哪个;再看误码率曲线,跟课本理论值差了十万八千里,对着代码较劲了整整两天。后来才发现,问题出在信噪比定义、映射表和判决边界这三处没对齐。这篇笔记就把我拆过的 16QAM 星座图仿真与误码率仿真完整捋一遍,覆盖调制映射、AWGN 信道建模、星座图观察、误码率统计与理论对比,以及新手最容易翻车的几个大坑。面向所有用 MATLAB 做通信仿真、毕设和课程实验的读者,跟着步骤走,你也能得到一张干净的 16QAM 星座图和一条贴着理论曲线的 BER 图。
2. 调制映射与参数准备:先算对符号和能量,再做仿真
2.1 星座图不是画出来的是映射出来的
很多人一上来就调qammod()函数,星座图是出来了,但换几个参数就不会用了。我建议先理解 16QAM 的星座点布局。16QAM 把每 4 个比特映射成一个复符号,I 路和 Q 路各取 4 个电平:-3、-1、+1、+3。这样就有 4×4=16 个组合,映射成复数符号时,实部对应 I 路电平,虚部对应 Q 路电平。
这里有一个关键选择:怎么把 4 比特编码到 16 个点上。最常见的做法是格雷映射(Gray mapping),保证相邻星座点之间只差 1 个比特,这样即使判决出错,多数情况下也只错 1 个比特,误码率(BER)会比随机映射低不少。格雷映射表可以直接用comm.RectangularQAMModulator这类工具箱对象生成,但为了看清原理,我一般直接手写映射关系。下面是一份常用的格雷映射电平顺序:
| 二进制(I/Q 各 2 比特) | 十进制索引 | I 电平 | Q 电平 |
|---|---|---|---|
| 00 | 0 | -3 | -3 |
| 01 | 1 | -1 | -3 |
| 11 | 2 | +1 | -3 |
| 10 | 3 | +3 | -3 |
| 00(Q 电平递增后) | 4 | -3 | -1 |
| 01 | 5 | -1 | -1 |
| 11 | 6 | +1 | -1 |
| 10 | 7 | +3 | -1 |
以此类推,Q 电平按 -3、-1、+1、+3 循环,就得到一个完整的 16 点星座。注意这里 I 路和 Q 路都是格雷码顺序,码型上是 00、01、11、10,而不是自然二进制 00、01、10、11。这一点必须刻在脑子里,后面对比理论与仿真误码率时,它决定了两条曲线能不能贴合。
2.2 MATLAB 手写映射函数:参数与逻辑拆解
下面这个函数接收比特串,输出复数符号序列,不依赖通信工具箱也能跑:
function sym = qam16_gray_map(bits) % bits: 行向量,长度必须是 4 的整数倍 % sym: 复数符号列向量 bits = bits(:); nSym = length(bits) / 4; if mod(length(bits), 4) ~= 0 error('比特数必须是 4 的倍数'); end bitsMat = reshape(bits, 4, nSym)'; % 每行一个符号的 4 比特 % 格雷映射表,行索引对应 [I2 I1 Q2 Q1] 转十进制 +1 grayMap = [ -3-3i, -3-1i, -3+1i, -3+3i, ... -1-3i, -1-1i, -1+1i, -1+3i, ... +1-3i, +1-1i, +1+1i, +1+3i, ... +3-3i, +3-1i, +3+1i, +3+3i ]; % 按列顺序取索引:第1列是I路高位,第2列是I路低位,第3列是Q路高位,第4列是Q路低位 idx = bitsMat(:,1)*8 + bitsMat(:,2)*4 + bitsMat(:,3)*2 + bitsMat(:,4) + 1; sym = grayMap(idx).'; end这段代码的关键在于索引计算。bitsMat(:,1)*8 + bitsMat(:,2)*4 + ...是把 4 比特看成一个 4 位二进制数,左侧高位在前。而grayMap的行顺序必须与这个索引顺序严格一一对应,否则映射就乱了。我在初版脚本里就吃过亏:映射表写成了自然二进制顺序,结果理论曲线需要右移 1~2 个 dB 才对得上,整个调试过程极其痛苦。所以建议把映射表打印出来,跟星座图逐个点校验一遍。
2.3 符号能量归一化:仿真里最容易被忽略的一步
星座图画出来之后,下一个问题就是功率。16QAM 的 16 个符号平均功率必须算对,否则加噪声时方差怎么设都是错的。16 个点的坐标模平方分别是:4 个角点模平方为 9+9=18,8 个边点为 9+1=10 或 1+9=10,4 个内点为 1+1=2,平均功率为 (4×18 + 8×10 + 4×2) / 16 = 160 / 16 = 10。等等,这个算错了,重新算:角点 4 个是 18,边点 8 个是 10,内点 4 个是 2,总和是 72+80+8=160,平均确实是 10。但标准 16QAM 星座点坐标若取 ±1、±3,平均功率就是 10,而归一化到单位功率需要乘 1/sqrt(10)。很多教材直接按单位能量星座点画图,那坐标就变成 ±1/sqrt(10)、±3/sqrt(10)。我习惯先按 ±1、±3 映射,再统一归一化:
% 映射后归一化,使平均符号能量为 1 sym = sym / sqrt(10);这样后续无论算 EsN0、还是对比理论误码率,信噪比定义都能直接对上。这个sqrt(10)就是 16QAM 的归一化因子,对应平均能量 10。如果你改用 64QAM,归一化因子就是 42 的平方根;改成 4QAM 就是 2。归一化做完之后,星座图的平均半径应该接近 1,而不是原来的 3 点多。
3. AWGN 信道与星座图绘制:让噪声功率和信噪比严格对应
3.1 加噪声的两种写法,本质是一样的
AWGN 信道下,接收信号 r = s + n,其中 n 是复高斯白噪声,实部和虚部相互独立,每一维方差为 N0/2。N0 是单边噪声功率谱密度,取决于信噪比设定。两种常见的加噪方式:
第一种直接用awgn()函数:
rx = awgn(tx, snr_db, 'measured');第二种是手动加噪声,这种更可控,也更容易理解参数含义:
N0 = 10^(-EsN0_db / 10); % 由 EsN0 反推噪声功率谱密度 noise = sqrt(N0/2) * (randn(size(tx)) + 1j*randn(size(tx))); rx = tx + noise;两种写法在信噪比定义一致时结果相同。我总用手动方式,因为可以精确控制每维方差,而且当你要研究相位噪声、IQ 失衡时,直接改噪声生成那行就行,不必依赖工具箱封装。关键在于sqrt(N0/2)前面那个系数,很多新手在这一步把方差搞错,导致误码率曲线整体偏移。
3.2 星座图仿真完整流程:从发端到接收端
这里给一段可以直接跑的脚本骨架:
% 参数设置 M = 16; k = log2(M); nBits = 1e6; % 总比特数 EbN0_db = 15; % 每比特信噪比 EsN0_db = EbN0_db + 10*log10(k); % 每符号信噪比 % 发端 bits = randi([0 1], nBits, 1); txSym = qam16_gray_map(bits); % 调用映射函数 txSym = txSym / sqrt(10); % 能量归一化 % 信道 N0 = 10^(-EsN0_db / 10); noise = sqrt(N0/2) * (randn(size(txSym)) + 1j*randn(size(txSym))); rxSym = txSym + noise; % 星座图 figure; plot(real(rxSym(1:5000)), imag(rxSym(1:5000)), '.'); hold on; plot(real(txSym(1:5000)), imag(txSym(1:5000)), 'rx', 'MarkerSize', 8); axis equal; grid on; title('16QAM 接收星座图 (Eb/N0 = 15 dB)'); xlabel('I 分量'); ylabel('Q 分量');逻辑说明:先随机生成 100 万比特,映射成 25 万个 16QAM 符号并归一化;接着根据 EsN0=18.45 dB 反推 N0,生成复高斯噪声叠加到信号上。绘图时只取前 5000 个点,否则点太密看不清趋势。红色叉号标出理想星座点,蓝色圆点就是带噪声的接收符号。注意axis equal很重要,不然横纵轴比例不一致,正方形星座会显示成长方形。
3.3 星座图怎么看:噪声半径、判决边界和 EVM
画出来的星座图不只是好看,它能直接反映信道质量。当 Eb/N0 在 15 dB 时,噪声斑点应该比较紧密地围绕理想点,四个角尤其清晰;当 Eb/N0 降到 5 dB 时,斑点会连成一片,内点相互交叠,判决区域边界模糊。
我一般会顺手算出误差矢量幅度 EVM:
% 误差矢量幅度 EVM(RMS) EVM_rms = sqrt(mean(abs(rxSym - txSym).^2)) / sqrt(mean(abs(txSym).^2)) * 100; fprintf('EVM (RMS) = %.2f%%\n', EVM_rms);EVM 是一个更直观的星座图质量指标,工程上经常用它快速评估链路。EVM 和 SNR 的关系近似为 SNR ≈ -20*log10(EVM/100)。比如 EVM 是 10% 时,SNR 大约 20 dB。画星座图时我习惯把判决边界也叠加上去,即 I=0、Q=0 的两条轴线,以及 ±2/sqrt(10) 的电平线,这样一眼就能看出哪些点会判错。判决边界在仿真里不一定要画,但对于理解误码率成因很有帮助。
4. 误码率仿真与理论曲线对比:蒙特卡洛循环里的关键参数
4.1 蒙特卡洛误码率主循环
误码率仿真的核心是重复发符号、加噪、判决、比对,统计错误比例。下面这段是完整的误码率扫描循环:
% 参数 M = 16; k = log2(M); EbN0_dB_list = 0:2:16; % 扫描 Eb/N0 numBits = 2e6; % 每个信噪比下的比特数 ber_sim = zeros(size(EbN0_dB_list)); ser_sim = zeros(size(EbN0_dB_list)); for idx = 1:length(EbN0_dB_list) EbN0_dB = EbN0_dB_list(idx); EsN0_dB = EbN0_dB + 10*log10(k); N0 = 10^(-EsN0_dB / 10); bitsTx = randi([0 1], numBits, 1); symTx = qam16_gray_map(bitsTx); symTx = symTx / sqrt(10); noise = sqrt(N0/2) * (randn(size(symTx)) + 1j*randn(size(symTx))); symRx = symTx + noise; % 最小距离判决 symRx = symRx * sqrt(10); % 还原到 ±1, ±3 坐标 I_rx = real(symRx); Q_rx = imag(symRx); I_hat = 2*(I_rx > 0) - 1; % 先粗判 I_hat(I_rx > 2) = 3; I_hat(I_rx < -2) = -3; I_hat(I_rx > 0 & I_rx < 2) = 1; I_hat(I_rx < 0 & I_rx > -2) = -1; % Q 路同理 Q_hat = 2*(Q_rx > 0) - 1; Q_hat(Q_rx > 2) = 3; Q_hat(Q_rx < -2) = -3; Q_hat(Q_rx > 0 & Q_rx < 2) = 1; Q_hat(Q_rx < 0 & Q_rx > -2) = -1; % 还原符号索引并解映射为比特 symHat = I_hat + 1j*Q_hat; [~, symIdx] = ismember(symHat, grayMap); % grayMap 是原来的非归一化映射表 bitsRx = de2bi(symIdx - 1, k, 'left-msb'); bitsRx = bitsRx(:); bitErr = sum(bitsTx ~= bitsRx); symErr = sum(symHat ~= symTx * sqrt(10)); ber_sim(idx) = bitErr / numBits; ser_sim(idx) = symErr / (numBits / k); end这个判决思路是先按正负粗判到 ±1,再根据幅度是否超过 2 修正到 ±3。看着笨,但它不依赖通信工具箱,也方便改成软判决或对数似然比。需要注意ismember那一步要求grayMap与映射函数里的顺序一致,否则索引就乱了。另一个隐蔽的点是symTx * sqrt(10)在比对时也要还原,因为判决是在非归一化坐标里做的,归一化因子不能漏。如果全程在归一化坐标里做,判决阈值就应该是 ±1/sqrt(10) 和 ±2/sqrt(10),各有利弊;我偏好非归一化坐标,数字看着舒服,不容易写错阈值。
4.2 理论误码率公式:SER 与 BER 不是一回事
16QAM 的理论符号误码率(SER)公式有多种写法,常见的一种是基于最小欧氏距离的近似。精确一点的公式是:对 M=16 方形 QAM,SER = 2(1 - 1/√M) erfc(√(3EsN0/(2(M-1)))) - (1 - 1/√M)² erfc²(...)。这个公式看着复杂,实际用 MATLAB 一行就行:
% 理论 SER:按符号能量计算 EsN0_lin = 10.^(EsN0_dB_list / 10); ser_theory = 2*(1 - 1/sqrt(M)) .* erfc(sqrt(3*EsN0_lin/(2*(M-1)))) ... - (1 - 1/sqrt(M))^2 .* erfc(sqrt(3*EsN0_lin/(2*(M-1)))).^2;注意 erfc 是余误差函数,erfc(x) = 2Q(√2 x),MATLAB 里用erfc比qfunc更通用(qfunc 在通信工具箱里)。还有,公式里的 EsN0 必须是线性值,不是 dB 值,别直接代进去。
BER 就不能简单套 SER 公式了。格雷映射下,相邻符号误判时通常只错 1 个比特,因此 BER ≈ SER / k。注意这是近似,不是精确相等,因为还存在跨多个比特的误判,但在高信噪比下误差很小。我在对比曲线时会把理论 SER 除以 k 画成 BER 理论曲线,与仿真的 BER 对比。这样如果两条曲线还差 0.5 个 dB 以上,就说明映射表或判决有 bug。
4.3 画误码率曲线:semilogy 与坐标轴的三个细节
画 BER 曲线代码很简短,但有几个细节不处理就会让曲线“消失”:
figure; semilogy(EbN0_dB_list, max(ber_sim, eps), 'b-o', 'LineWidth', 1.5); hold on; semilogy(EbN0_dB_list, max(ser_sim, eps), 'r-s', 'LineWidth', 1.5); semilogy(EbN0_dB_list, max(ser_theory/k, eps), 'k--', 'LineWidth', 1.5); grid on; xlabel('Eb/N0 (dB)'); ylabel('误码率 / 误符号率'); legend('BER 仿真', 'SER 仿真', 'BER 理论');三个细节:第一,eps兜底,否则某段信噪比下一个错误都没有时,log10(0)会得到负无穷,曲线直接断掉。第二,横轴用 Eb/N0 还是 Es/N0 必须想清楚;我习惯横轴用 Eb/N0,因为和理论公式里的 EsN0 换算已经做完了,横轴语义更贴近比特级比较。第三,仿真点不要连成平滑线,要带标记符号,信噪比间隔取 2 dB 足够观察趋势。如果你跑 16 dB、18 dB 时误码率已经低于 1e-6,那就得加大比特数,不然统计噪声会严重抖动。
5. 仿真避坑记录:5 个最容易翻车的操作细节
5.1 Eb/N0 与 Es/N0 换算少加 10*log10(k)
现象:曲线整体比理论右移约 6 dB,仿真误码率比理论大得多。
原因:16QAM 一个符号承载 4 个比特,符号能量是比特能量的 4 倍。若直接把 Eb/N0 当作 Es/N0 用,等效信噪比被低估了 6 dB。反过来,如果把 Es/N0 当 Eb/N0 画横轴,理论曲线又会左移。
解决:统一按 EsN0_dB = EbN0_dB + 10*log10(k) 换算,k=4 时就是加 6.02 dB。每次写循环前先打印一遍换算结果,确认无误再跑。
5.2 星座图所有点堆成一团,看不出 16 个簇
现象:接收星座图不是 16 团点,而是乱糟糟一片,或者所有点挤在一起。
原因:噪声方差设太小,比如 N0 直接写成了 1e-4,而信号幅度接近 1,信噪比极高,噪声几乎不可见。另一个常见原因是没有做能量归一化,信号幅度有 3 点多,噪声相对幅度太小。
解决:先把 Eb/N0 设成 0 dB 甚至 -5 dB,这时候星座图应该明显糊成一团;然后慢慢增大到 10 dB、15 dB,观察斑点逐渐收紧。如果 0 dB 时还是清晰的 16 团,那噪声方差肯定算错了。检查 N0 计算式里的 EsN0 有没有换算对。
5.3 误码率为 0 导致曲线断点,semilogy 直接报错或图形缺失
现象:高信噪比下 BER 曲线突然消失或出现断崖,log 坐标下看不到点。
原因:蒙特卡洛仿真中某个信噪比下错误数为 0,BER 恰好是 0,而 semilogy 对 0 取对数得到负无穷。我还见过有人为了得到“完美曲线”故意加大信噪比到 20 dB,结果错误数为 0,曲线直接断掉。
解决:两种方案任选。一是统计时保证最少错误数,比如当错误数不足 100 时继续增加数据量,直到统计到 100 个错误再停止;二是绘图时用max(ber, eps)处理。我推荐前者,因为误码率为 0 的“漂亮”曲线没有实际意义,统计误差太大,别人根本没法复现你的结果。
5.4 用了格雷映射表,但判决时按自然二进制还原比特
现象:仿真 BER 比理论值明显偏高,尤其在低信噪比时能高出几倍,但 SER 曲线和理论 SER 对得很好。
原因:判决出来的符号索引又按自然二进制转回比特,丢掉了格雷映射的相邻单比特特性。比如符号从 11 判到 10,自然二进制下两个比特都错,格雷码下只错 1 个比特。SER 不受影响,但 BER 被放大了。
解决:解映射必须使用与调制完全一致的格雷表。最保险的做法是维护一张映射表,调制的逆过程直接查表反推,而不是自己写“电平到比特”的手动规则。我在代码里用grayMap(idx)正向映射,逆过程用ismember查表,两头走同一个表,就不会出现这种错位。
5.5 仿真重复性差,同一段代码每次跑出的曲线不一样
现象:同样的参数,两次运行得到的 BER 曲线有小幅抖动,尤其在低误码率区间。
原因:随机数生成器未固定种子。蒙特卡洛仿真本身是随机试验,不设种子时每次结果都不同。
解决:脚本开头加rng(42);固定随机种子,这样别人复现你的实验能得到完全一致的曲线。如果要跑多组对比实验,可以用不同种子并取平均,但发布代码时一定要固定种子,否则结果的置信度无从谈起。我一般会固定种子,同时在注释里写明用了多少比特,方便他人判断统计精度。
6. 进阶玩法:把脚本包成参数化函数,顺便观察相位旋转的影响
到这一步,基础仿真已经跑通了,但直接复制粘贴脚本改参数始终不是工程习惯。我会把整个流程封装成一个函数,输入参数是调制阶数 M、Eb/N0 范围、比特数,输出 BER 和星座图:
function [ber, ser] = qam_sim(M, EbN0dB_list, maxBits) k = log2(M); % 调用 qammod 生成星座图(工具箱版),或手动扩展 qam16_gray_map % 这里以 16QAM 为例,M 可变时建议用三角函数星座生成 ... end封装之后,你可以同时跑 4QAM、16QAM、64QAM 三组曲线,对比它们在相同 Eb/N0 下的性能差异,这是毕设和面试里常被问到的点:高阶 QAM 频谱效率高,但误码性能急剧下降,64QAM 要达到 16QAM 相同误码率大约需要多 6~10 dB 的 SNR。接着还可以在接收端人为加入相位偏移,比如把接收符号整体旋转 10 度,然后观察星座图旋转后判决误码率飙升的现象,这能帮你理解为什么实际系统需要载波同步。我自己的习惯是每次仿真都在收尾时固定随机种子、打印出 Eb/N0 换算表,并把归一化因子写进注释里;从那以后,凡是跑通信仿真,我都强制走一遍固定种子、能量归一化、误码统计三个检查点,再也没被“玄学曲线”折磨过。希望这些能帮到你,少走我踩过的那几步弯路。
本文还有配套的精品资源,点击获取