☰
16QAM星座图仿真与误码率仿真:从Gray编码到蒙特卡洛实现
2026/9/29 19:34:40 网站建设 项目流程

简介:这是一份面向通信系统仿真学习者的MATLAB工程,针对16QAM数字调制方式,可实现星座图绘制与误码率(BER)仿真,帮助理解幅度相位联合调制及AWGN信道下的系统性能。资源压缩包共8个文件,全部为m脚本,体积仅3KB,涵盖主流程控制、随机二进制序列生成、比特与符号映射转换、调制解调函数、星座图绘制和基带成形滤波等环节,结构精简,适合MATLAB初学者快速部署和二次修改。已有1926人学习这套代码,兼具教学演示与实验参考价值。通过运行主函数,可在不同信噪比下观察接收信号星座点的分布变化,并得到误码率随信噪比变化的性能曲线。同时,比特转换函数展示了4位二进制码字到16QAM符号的映射/逆映射过程,便于深入理解编码映射与解调判决的原理。

1. 16QAM星座图仿真与误码率仿真:开工前的三个判断

做物理层算法验证时,最直观的抓手就是星座图和误码率曲线。16QAM把4个比特映射成一个IQ符号,星座图上16个点排成方阵,看起来简单,但真正把仿真跑起来、让误码率曲线和理论值对上,涉及信噪比定义、映射表、判决边界和统计量设置一堆细节。这篇笔记围绕16QAM星座图仿真及误码率仿真展开,讲清楚每一段代码为什么这么写、参数怎么定、对不上时往哪排查。

适合三类人:刚接触数字调制想验证理论公式的初学者、在FPGA或硬件实现前先用仿真确认算法指标的工程师、以及要给整条链路做误码率预算的系统设计者。先想明白三件事再动手:用Eb/N0还是Es/N0作横坐标、Gray编码映射表有没有写对、蒙特卡洛仿真要统计到多少比特才可信。

2. 16QAM调制与星座映射:一张Gray编码表定全局

2.1 Gray编码不是可选项:相邻星座点只差1bit是误码率性能的底线

16QAM的16个星座点,I路和Q路各取4个电平。常见电平取值是-3、-1、+1、+3,电平间隔为2,这样星座图是均匀方阵,平均符号能量可以精确算出来。关键是相邻星座点分配的比特必须使用Gray编码,也就是二进制码按00、01、11、10的顺序对应到-3、-1、+1、+3,保证任何相邻电平组合只差1个比特。

如果不做Gray编码而直接用自然二进制00、01、10、11,接收机判决时最容易把相邻电平搞混,一次符号错误可能同时错2个甚至3个比特,误码率比Gray编码差一截。这在仿真里非常容易踩中:星座图画出来完全正常,但BER曲线抬得很高,怎么调噪声都压不回理论值。所以映射表是16QAM仿真的第一个全局参数,写成常量表而不是临时拼凑。

常见映射表如下:2比特Gray码的十进制键值0、1、3、2,分别对应电平-3、-1、+1、+3。4个比特为一组时,高2位映射到I路,低2位映射到Q路。平均符号能量Eavg=(4×(1+9))/4=10,为了后面方便设信噪比,把星座点统一除以根号10,让平均符号能量归一化为1。这个归一化动作决定了后续噪声功率的计算基准,漏掉它,整个误码率曲线都会错位。

提示:归一化基准建议固定写在映射函数里,不要留在主流程里单独处理,避免不同模块各归各的。

2.2 用Python生成16QAM星座图:最小可运行代码与坐标核对

先写映射函数。这个函数直接决定发射端的符号序列,也决定了接收端逆映射的查表方式。

import numpy as np # Gray编码映射表:2-bit十进制键 -> I/Q电平 GRAY_LEVEL = {0: -3, 1: -1, 3: 1, 2: 3} def map_16qam(bits): """把比特流映射为16QAM符号,每4bit一组,高2位为I路,低2位为Q路""" n = len(bits) - len(bits) % 4 # 丢弃不满4bit的尾巴 symbols = [] for i in range(0, n, 4): i_key = bits[i] * 2 + bits[i + 1] # 高2位合成键值 q_key = bits[i + 2] * 2 + bits[i + 3] # 低2位合成键值 s = (GRAY_LEVEL[i_key] + 1j * GRAY_LEVEL[q_key]) / np.sqrt(10) symbols.append(s) return np.array(symbols)

这段代码的要点在于键值的计算方式:bits[i] * 2 + bits[i+1]把2个比特拼成一个0到3的整数,正好作为GRAY_LEVEL的键。之所以用键值查表而不是直接写电平运算,是因为映射关系是Gray码,不是简单的线性关系,查表最不容易错。np.sqrt(10)是能量归一化,16个符号的平均能量算出来正好是1,后面加噪声时直接用符号能量1做基准。

画星座图时,把16个参考星座点和仿真产生的符号点叠在一起看:

import matplotlib.pyplot as plt # 16个归一化参考星座点 const = np.array([(i + 1j * q) / np.sqrt(10) for i in [-3, -1, 1, 3] for q in [-3, -1, 1, 3]]) # 生成随机比特并映射 np.random.seed(42) bits = np.random.randint(0, 2, 4000) symbols = map_16qam(bits) plt.figure(figsize=(4, 4)) plt.scatter(symbols.real, symbols.imag, s=4, alpha=0.5, label='symbols') plt.scatter(const.real, const.imag, marker='+', s=120, color='C3', label='constellation') plt.axis('equal') # 强制I/Q坐标等比例,否则星座图会被拉伸成椭圆 plt.grid(True, linestyle='--', alpha=0.5) plt.xlabel('I') plt.ylabel('Q') plt.legend() plt.title('16QAM constellation') plt.show()

axis('equal')这个参数必须加。不加的话,matplotlib会根据数据范围自动缩放坐标轴,符号点在I方向和Q方向的视觉距离不一致,会误导你对判决边界的判断。归一化后,内圈4个点在坐标±1/√10(约±0.316)处,外圈12个点在±3/√10(约±0.949)处,用这个参照系去核对别的模块产生的星座点,能快速发现是振幅缩放错误还是映射错误。

3. AWGN信道下的误码率仿真:蒙特卡洛与理论曲线的对齐方法

3.1 信噪比定义与Eb/N0、Es/N0换算:横坐标差0.5dB都是这里引起的

误码率仿真的横坐标,行业里最常见的是Eb/N0,即每比特能量与噪声功率谱密度的比值。但噪声实际是加在符号上的,仿真代码里最先拿到的是Es/N0(每符号能量与噪声功率谱密度之比)。16QAM一个符号带4个比特,所以Es=4Eb,换算成dB就是加10log10(4)≈6.02dB。也就是说,同样的信道条件下,Es/N0比Eb/N0大6dB左右。

理论误码率公式也要用对基准。在AWGN信道下,16QAM的符号错误概率可以近似写成:

Ps ≈ 3Q(√(Es/(5N0)))

Q函数是高斯概率积分,工程上用互补误差函数erfc表示为Q(x)=0.5·erfc(x/√2)。Gray编码下高信噪比时一个符号错误大约只影响1个比特,所以比特误码率近似为:

Pb ≈ (3/4)·Q(√(4Eb/(5N0)))

代码里实现理论曲线时,直接用erfc版本:

from scipy.special import erfc def ber_theory_16qam(eb_n0_db): """16QAM在AWGN下的理论误比特率,输入Eb/N0单位dB""" eb_n0_lin = 10 ** (eb_n0_db / 10) x = np.sqrt(4 * eb_n0_lin / 5) return 0.75 * 0.5 * erfc(x / np.sqrt(2))

这里4 * eb_n0_lin / 5来自公式推导:Es/N0=4Eb/N0,代入Q函数自变量√(Es/(5N0))。0.75就是系数3/4,0.5 * erfc(...)是Q函数展开。建议把理论公式独立成函数,仿真曲线和它对比时,横坐标统一用dB、纵坐标统一用对数刻度,避免两边单位不一致。

3.2 蒙特卡洛误码率仿真的完整链路:从加噪声到BER统计

蒙特卡洛误码率仿真的基本流程是:生成随机比特→映射成符号→按指定信噪比加AWGN噪声→判决还原成比特→和发射比特对比统计错误比例。关键在于加噪声这一步,复噪声的实部和虚部要分别加方差为N0/2的高斯噪声,合起来复噪声功率才是N0。

def add_awgn(symbols, es_n0_db): """给符号序列叠加AWGN,输入Es/N0单位dB,符号能量已归一化为1""" es_n0_lin = 10 ** (es_n0_db / 10) n0 = 1.0 / es_n0_lin # Es=1,所以N0=1/(Es/N0) noise = np.sqrt(n0 / 2) * ( np.random.randn(len(symbols)) + 1j * np.random.randn(len(symbols)) ) return symbols + noise

n0 = 1.0 / es_n0_lin的前提是符号平均能量已经归一化为1。如果前面映射时没有除以√10,这里就要额外除以符号平均能量,很多人误码率曲线整体偏移就是因为漏了这一步。np.sqrt(n0 / 2)分别给实部、虚部生成噪声,两个维度合起来的噪声功率才是N0。

判决采用最近邻准则:把接收符号和16个参考星座点逐一算欧氏距离,取距离最小的点作为判决输出。逆映射时用查表还原成4个比特:

def demod_16qam(rx_symbols): """最近邻判决,返回还原的比特序列""" bits = [] for s in rx_symbols: idx = np.argmin(np.abs(s - const)) # 找最近星座点下标 s_raw = const[idx] * np.sqrt(10) # 还原归一化前的电平 i_key = {v: k for k, v in GRAY_LEVEL.items()}[int(round(s_raw.real))] q_key = {v: k for k, v in GRAY_LEVEL.items()}[int(round(s_raw.imag))] bits.append((i_key >> 1) & 1) bits.append(i_key & 1) bits.append((q_key >> 1) & 1) bits.append(q_key & 1) return np.array(bits)

逆映射这里临时构造了GRAY_LEVEL的反向字典,实际工程里建议把正向和反向两个字典都定义成模块级常量,避免每个符号都重复构造。判决逻辑本身没有玄学,就是全搜索最近邻,16个点规模很小,不需要K-D树这类优化。

主循环遍历需要的Eb/N0点,每个点独立跑一批比特并统计误码率:

n_bits = 400000 eb_n0_range = np.arange(0, 15, 1) ber_sim = [] for eb_n0_db in eb_n0_range: bits = np.random.randint(0, 2, n_bits) tx = map_16qam(bits) es_n0_db = eb_n0_db + 10 * np.log10(4) # Es/N0 = Eb/N0 + 6.02dB rx = add_awgn(tx, es_n0_db) dec = demod_16qam(rx) ber = np.sum(dec != bits) / len(bits) ber_sim.append(ber)

这个循环里最容易忽略的是es_n0_db的换算。发射端符号能量确定后,噪声功率只取决于Es/N0;如果直接用eb_n0_db去加噪声,实际信噪比低了6dB,仿真曲线会比理论曲线整体右移,看起来像是“性能变差”。跑完后用plt.semilogy把ber_sim和ber_theory_16qam画在同一条坐标轴上,10dB以上的区间两者差在0.3dB以内基本算正常。

3.3 叠加噪声后的星座图:判断链路健康的第一个窗口

误码率曲线是统计结果,星座图则是单样本视图,两者互补。在Eb/N0等于8dB左右加噪声后画星座图,能看到16个高斯簇,每一簇的中心就是参考星座点,簇的半径由噪声标准差决定。簇与簇之间会有粘连,这正是符号错误发生的区域——落在判决边界附近的点,噪声稍微一推就跨到隔壁簇。

把Eb/N0调到14dB重画,16个簇清晰分离,误码率降到1e-5量级。用这个可视化检查链路时,注意三个细节:簇是否圆形对称、簇心是否落在参考点位置、是否有整体旋转。簇呈椭圆说明I/Q增益不平衡;整体旋转说明存在频偏;簇心偏移说明归一化或判决有系统误差。这三种现象靠误码率曲线很难定位,星座图一眼就能看出来。

4. 误码率仿真跑不通的5个常见坑:从曲线发散到星座图翻车

4.1 横坐标单位混用导致BER曲线整体偏移6dB

现象:仿真误码率曲线形状和理论曲线完全一致,但整体差了大约6dB,仿真BER比理论值在同一横坐标下“差”很多。

原因:几乎可以断定是单位混用。最常见的是用Eb/N0做横坐标画图,加噪声时却直接把Eb/N0当成了Es/N0;或者反过来,加噪声用对了Es/N0,画图时标成了Eb/N0。16QAM的Es/N0和Eb/N0相差6.02dB,这个固定偏移最容易出现在单位换算不彻底的时候。

解决:把换算固定写在主循环里,只在横坐标变量处写一次Eb/N0,加噪声前用es_n0_db = eb_n0_db + 10 * np.log10(4)单独算一行。不要在图里临时换算,也不要在加噪函数里做隐含换算。我习惯在代码注释里写明“此函数输入Es/N0”,从源头上避免调用时传错。

4.2 复噪声功率少加一半:星座点抖动范围明显不对

现象:星座图在低信噪比下显得比预期更干净,高信噪比下簇的半径偏小,BER普遍低于理论值。

原因:AWGN信道的复噪声,实部和虚部各占一半功率。如果把复噪声总功率设成N0,又让实部和虚部分别取N0方差的高斯噪声,实际噪声功率就是2N0,等效信噪比低了3dB。反过来,只给实部虚部各加N0/2,才是标准做法。

解决:按np.sqrt(n0 / 2) * (randn + 1j * randn)生成复噪声,这是教科书标准写法。检查代码要点是:n0的数值应该等于1 / (Es/N0线性值),且乘以sqrt(n0/2)后实部虚部的各自方差恰好是n0/2。

4.3 低误码率区间的蒙特卡洛发散:BER在1e-5处跳来跳去

现象:Eb/N0大于12dB后,BER曲线不再是单调下降,而是在1e-5到1e-6之间上下跳动,重复跑脚本结果也不稳定。这个现象在工程里常被叫仿真发散,实际上是统计样本不够。

原因:误码率本身就是小概率事件的频率估计。要测到1e-5量级,至少要积累几十个错误事件才有统计意义。400k比特在12dB时理论上只有个位数错误,随机性太强,算出来的BER方差极大。

解决:两个方向。一是增加样本量,比如把n_bits提到几千万,但仿真时间会线性增长;二是接受现实,只对比到10dB左右的曲线段,把仿真验证和理论公式的分工定清楚——理论公式负责外推低BER区,仿真负责验证中低信噪比段。另外固定随机种子,至少在调代码阶段保证结果可复现。

4.4 映射表和逆映射表各写一套:无噪声判决也出错的隐形bug

现象:星座图完全正常,但BER高得出奇,甚至无噪声时误码率不为0。

原因:正映射和逆映射各写了一份查表逻辑,两份逻辑用的Gray编码顺序不一致。常见情况是发射端按00-01-11-10的Gray顺序,接收端却按00-01-10-11的自然顺序逆查,导致相邻星座点正确判决后比特还原错误。

解决:在仿真主循环之前,先跑一遍无噪声自检:随机生成几万比特,映射后直接判决再逆映射,对比输入输出必须完全一致。这一步5秒能跑完,却能排除一大半映射类bug。我个人的做法是把正反映射表定义成同一份字典的两个视图,反向字典用推导式从一个源生成,杜绝手写两遍出错的可能。

4.5 理论公式硬套通用近似:低信噪比段误差大到没法看

现象:Eb/N0小于6dB时,仿真点稀疏地散落在理论曲线附近,系统性偏差明显,越往低信噪比偏差越大。

原因:常见的16QAM误码率近似公式Ps≈3Q(√(Es/(5N0)))是忽略高阶项的近似结果,在高信噪比下很准,但在低信噪比下,双符号同时出错、边界符号错误概率差异等因素开始显现,近似公式的系统误差变得不可忽略。

解决:如果是验证系统性能,把对比区间限定在BER低于1e-3的段,比如Eb/N0在7dB以上。如果非要在低信噪比段对齐,用精确的符号错误概率表达式,把星座图边界效应逐项积分算进去。工程上一般不需要,但要清楚手里用的是近似公式,不要拿它当全区间真理。

提示:理论公式和仿真曲线的关系是“互相印证”,不是“单方面校验”。仿真也要怀疑,理论近似也要怀疑,两边独立推导才能发现各自的错误。

5. 从理想AWGN走向真实链路:脉冲成形、频偏与定时误差

5.1 根升余弦成形与匹配滤波:矩形脉冲仿真为何不够

前面几章的仿真默认每个符号是一个冲激,没有考虑实际发射机里基带脉冲的频带限制。真实系统里符号序列要经过脉冲成形滤波器,最常见的配对是发射端根升余弦成形、接收端根升余弦匹配滤波,合起来构成升余弦脉冲,满足奈奎斯特第一准则,采样点处无符号间干扰。

滚降系数α是这里最关键的参数。α越小,占用带宽越窄,但脉冲在时域的拖尾越长,对定时误差越敏感;α越大,频谱滚降越平缓,牺牲带宽换定时鲁棒性。LTE和WiFi系统里α=0.22是常见的选择,实验室原型机里也有用0.35的,它抗定时偏差能力更强。仿真参数对比如下:

滚降系数占用带宽倍数定时误差敏感度典型应用
0.151.15倍符号速率高卫星链路窄带场景
0.221.22倍符号速率中LTE、WiFi主流选择
0.351.35倍符号速率低实验室原型、教学仿真

成形过程要做过采样,也就是每个符号插入多个采样点,再与根升余弦滤波器卷积。接收端先匹配滤波,再按最佳采样时刻抽取,每个符号取一个判决点。如果跳过成形直接发矩形脉冲,频谱旁瓣会泄漏到相邻信道,这在带限信道里会产生实质性能损失,单纯AWGN仿真里很难暴露。

def rrc_filter(sps, alpha, span=8): """生成根升余弦滤波器系数,sps为每符号采样数,alpha为滚降系数""" n = span * sps t = (np.arange(n) - n / 2) / sps h = np.zeros(n) for i, ti in enumerate(t): if abs(ti) < 1e-8: h[i] = (1 - alpha) + 4 * alpha / np.pi elif abs(abs(ti) - 1 / (4 * alpha)) < 1e-8: h[i] = (alpha / np.sqrt(2)) * ( (1 + 2 / np.pi) * np.sin(np.pi / (4 * alpha)) + (1 - 2 / np.pi) * np.cos(np.pi / (4 * alpha)) ) else: num = np.sin(np.pi * ti * (1 - alpha)) + 4 * alpha * ti * np.cos(np.pi * ti * (1 + alpha)) den = np.pi * ti * (1 - (4 * alpha * ti) ** 2) h[i] = num / den return h / np.sqrt(sps)

这个滤波器系数的三个参数要单独解释:sps是过采样率,决定频谱复制间隔,取值4到16之间比较常见,太小滤波器逼近误差大,太大计算量浪费;alpha是滚降系数,直接写进公式里的分子分母;span是滤波器截断的符号数,设成8意味着两边共截断8个符号,截断太短会引入额外ISI。发射成形和接收匹配共用这组系数,只是接收端滤波后要丢弃边界样本,再按sps的间隔抽样判决。

5.2 载波频偏与定时误差:星座图旋转和拖影是怎么来的

加了成形滤波之后,还有两个真实链路里躲不开的问题:载波频偏和定时误差。频偏的直观效果是星座图整体旋转,而且是随符号序号累加的——第k个符号旋转角度是2πΔf·k,符号越往后转得越多,表现在星座图上就是一圈圆弧或同心圆环,而不是清晰的16个点。

def apply_freq_offset(symbols, norm_freq_offset): """施加归一化频偏,norm_freq_offset单位为1/符号周期""" n = len(symbols) k = np.arange(n) return symbols * np.exp(1j * 2 * np.pi * norm_freq_offset * k)

仿真时这个函数要放在加噪声之前,频偏作用在信号上再叠噪声才符合物理顺序。norm_freq_offset=1e-3意味着每1000个符号转一整圈,这个量级看起来小,但在16QAM中,星座点间最小距离本来就小,旋转超过内圈点间距就会开始压垮判决。真实接收机里频偏靠载波同步环路估计和补偿,仿真阶段可以用这个函数验证:同步算法没有收敛时,星座图呈环形,误码率趋于50%。

定时误差的影响类似但表现不同。最佳采样时刻偏离时,眼图张开度变小,判决点处的ISI增加,星座图上的簇不再是圆形,而是沿对角线方向拉长成椭圆或产生“拖影”。原因在于脉冲成形滤波的升余弦特性只在精确的采样时刻保证无ISI,采样点偏移后相邻符号的拖尾叠加上来。仿真中模拟定时误差最简单的方式,是在过采样序列里按小数采样点抽取,用线性插值近似。

5.3 仿真模型搭建顺序:分步逼近真实系统才是效率最高的路径

不要一上来就把成形、频偏、定时偏差全部加进仿真。链路模型每加一个模块,排查难度翻一倍。我建议的顺序是:第一步跑纯AWGN加理想采样,把映射、逆映射、BER统计验证到和理论一致;第二步加上根升余弦成形和匹配滤波,确认无噪声时判决输出依然完全正确;第三步加频偏,先加已知固定频偏,观察星座图旋转方向,再考虑加同步补偿;最后一步加定时误差。每一步都回归一下BER曲线,确认它没有偏离理论值太多。

反过来做的教训很多:模块全加上之后曲线不平、星座图一团糊,根本分不清是频偏没补偿还是定时偏差过大还是噪声算错。分步逼近,每一步留一个可验证的中间指标,翻车时能快速定位到具体模块。

6. 验收误码率仿真结果的三件事:对照理论、检查星座图、验证收敛性

仿真跑完不是画出曲线就结束了,要证明这组结果可信,我一般做三件事。第一件是把蒙特卡洛结果和理论曲线叠在同一个对数坐标里看,重点看Eb/N0在7到11dB这一段,两者偏差应该在0.3dB以内。这一段BER覆盖1e-4到1e-2,样本量足够有统计意义,又是理论近似最准的区间,是最可靠的对齐窗口。

第二件是星座图质量检查。加噪声后的接收星座图,16个簇应该近似圆形、簇心应该精确落在参考星座点位置、簇的半径应该随Eb/N0的增大按预期缩小。如果簇是椭圆形,去查I/Q增益不平衡;如果有环状旋转拖影,去查频偏估计;如果簇心偏移,去查判决边界和参考星座点是不是同名不同值。

第三件是收敛性验证。把一次仿真产生的比特流对半切分,分别统计前半段和后半段的BER。如果两个半段的误码率差别超过2倍,说明这次仿真的样本量不够,BER结果不可信。这个检验要固定随机种子重跑,结果就不会每次都不一样。之前见过有人用40万比特在13dB处跑出BER=1e-6,对半切分后两段一个为0一个2e-6,显然是撞运气而不是测出了真实性能。

最后说个我自己踩过的坑:赶进度时跳过无噪声自检直接跑完整曲线,结果曲线在10dB处比理论低了差不多半个数量级,排查半天才发现是逆映射查表把Gray顺序写反了。从那以后所有调制解调仿真都先做无噪声自检,再跑有噪曲线,整个过程连10秒都用不上。这个习惯也推荐给你,希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询