☰
BPSK到64QAM调制解调仿真:星座图、误码率与信噪比全解析
2026/10/14 9:09:31 网站建设 项目流程

简介:针对数字通信中BPSK、QPSK、16QAM与64QAM四种调制方式,这份代码包从原理出发给出了完整的调制解调实现方案,适合通信专业学生、算法研究者或需要快速验证星座图与误码率性能的工程人员。压缩包共5个文件,含1个main.m主程序与4个对应的fig星座图文件,体积仅31KB,便于直接运行和修改。main.m采用自写方式完成载波生成、符号映射、解调判决及误码率统计,覆盖从BPSK到64QAM的链路;fig文件可清晰展示不同调制阶数下的星座分布,帮助理解相位/幅度变化与抗噪声性能的权衡。已有2897人学习下载,代码结构紧凑且无工具箱依赖,适合作为教学演示或课程设计的参考基线,也可在此基础扩展更高阶QAM或加入信道编码对比。

1. BPSK到64QAM的调制解调:一条越往上越吃信噪比的路

一条无线链路在距离拉远、信道变差时,系统会把调制阶数从64QAM一路回落到16QAM、QPSK、甚至BPSK;信号质量恢复后再逐级往上提。这个BPSK-QPSK-16QAM-64QAM调制解调链条,几乎是所有物理层算法验证里最基础也最容易出错的部分。多数人卡住的地方不是“星座图是什么”,而是把星座点映射出来后,噪声功率、归一化、误码率统计全都对不上,仿真曲线翻车后再没动力往下调。这篇文章把从映射、加噪、判决到误码率统计的完整链条拆开讲透:每个参数改动会影响什么,出错时先查哪里。适合正在写调制解调仿真、搭通信链路原型,或者做毕业设计需要一份可复现结果的人。

2. 星座图里藏着全部答案:从BPSK、QPSK到16QAM、64QAM的排布逻辑

数字调制本质上就是“把比特映射成复数符号”,这些符号画在复平面上就是星座图。四种调制方式共享同一套物理框架,区别只是星座点数量和排列方式不同。理解这一点,后面写代码时就能用一个函数把四种调制全部覆盖,而不是每种调制单独写一套。

2.1 每符号多传几个比特,星座点之间的距离就在缩小

BPSK一个符号带1个比特,只有两个星座点,功率归一化后两点距离是2.0。QPSK(工程里也直接写成4QAM)一个符号带2个比特,四个点在复平面上构成正方形,归一化后相邻距离约1.414。16QAM一个符号4个比特,星座点变成4×4方阵,相邻距离被压缩到约0.632。64QAM一个符号6个比特,星座点排成8×8方阵,相邻距离只剩约0.309。

调制方式k(bit/符号)星座点数归一化最小距离 d_min
BPSK122.000
QPSK(4QAM)241.414
16QAM4160.632
64QAM6640.309

这个表格是所有后续判断的锚点。星座点之间靠“最小欧氏距离”区分,距离越小,同样的加性高斯白噪声就越容易把一个点推到相邻点区域,误判概率随之上升。QPSK到16QAM,距离从1.414缩到0.632,大约是2.24倍,折算成功率约7dB;16QAM到64QAM,距离又从0.632缩到0.309,约2.05倍,约6.2dB。所以高阶调制不是“免费午餐”,每多传1比特/符号,都对应一段实打实的信噪比代价。

这也是为什么实际系统不会永远用64QAM。离基站近、信道质量好的用户用64QAM拿高吞吐,边缘用户自动回落到QPSK甚至BPSK保连接。工程上把这种机制叫自适应调制编码(AMC),四种调制并存是常态而非特例。

2.2 QPSK是两个BPSK叠加,16QAM和64QAM则是I/Q两路PAM组合

QPSK的另一种理解方式:它把比特拆成I、Q两路,每路各做一个BPSK,再正交叠加。因为I路和Q路在复平面上互不干扰,所以理论上QPSK和BPSK在AWGN信道下的误码率曲线几乎重合。仿真时如果发现QPSK比BPSK“好很多”,大概率是噪声功率或者归一化出了错,这是值得记住的一个反直觉结论。

16QAM和64QAM则是把两路BPSK各自扩展成多电平PAM。16QAM的I路和Q路各自是4-PAM,64QAM各自是8-PAM,组合出来的方形星座天然满足“M是平方数”这个条件。这个观察对写代码很重要,因为只要能生成一个m-PAM的电平序列,再I/Q正交组合,就能覆盖M=4、16、64,只有BPSK需要额外处理。同时,从QPSK到QAM,信号不只靠相位区分,也靠幅度区分,这就引出峰均比问题。星座点离原点越远,信号峰值功率越高,对发射机线性度的要求也越苛刻,这是高层调制在工程上“难做”的另一面,仿真阶段可以先不管,但心里要有数。

3. 用Python跑通四种调制的完整收发链路:映射、加噪、判决三步走

这一节的代码可以直接复制运行,实现从随机比特流到误码率统计的最小闭环。整个链路只有三个环节:比特映射成星座符号,符号经过AWGN信道,接收端按最近距离判决还原比特。

3.1 格雷映射星座生成与功率归一化

先实现星座生成。这里用格雷映射,保证相邻星座点之间只有一个比特位不同,这样一次符号错判最多引入1个比特错误,误码率曲线不会虚高。

import numpy as np def gray_pam_levels(m): """m-PAM电平,按格雷码顺序排列,未归一化""" idx = np.arange(m) gray = idx ^ (idx >> 1) # 十进制索引转格雷码值 levels = 2 * idx - m + 1 # 未归一化电平,如4-PAM为[-3,-1,1,3] return levels[np.argsort(gray)] # 按格雷码顺序重排电平 def qam_constellation(M): """生成M-QAM格雷映射星座,仅支持平方数QAM,平均功率归一化为1""" if M == 2: return np.array([-1.0, 1.0], dtype=complex) m = int(np.sqrt(M)) if m * m != M: raise ValueError("仅支持平方数QAM(4/16/64)") k = int(np.log2(M)) pam = gray_pam_levels(m) const = np.zeros(M, dtype=complex) for i in range(M): i_idx = (i >> (k // 2)) & (m - 1) # 高半比特给I路 q_idx = i & (m - 1) # 低半比特给Q路 const[i] = pam[i_idx] + 1j * pam[q_idx] return const / np.sqrt(np.mean(np.abs(const) ** 2))

逻辑上分了三步:先构造一维PAM电平,再按格雷码重新排序,最后把I路和Q路两维组合成复数符号并整体归一化。gray = idx ^ (idx >> 1)是把自然二进制顺序转成格雷码顺序的经典写法;np.argsort(gray)得到的索引可以把电平数组重新排列成“格雷码友好”的顺序,这样后面直接用二进制索引查表就能保证相邻星座点只有1比特差异。

参数说明:M是调制阶数,支持2、4、16、64;k是每符号比特数;归一化除以np.sqrt(np.mean(np.abs(const) ** 2))确保星座平均功率为1,这一步直接决定后面噪声方差的正确性。细心的读者会发现这个生成器给出的QPSK是旋转45度的4QAM(星座点在四个对角方向)。性能和标准QPSK一致,但画星座图时会觉得“歪了”。如果项目协议里要求标准QPSK相位,在qam_constellation(4)返回后整体乘np.exp(-1j * np.pi / 4)即可对齐,误码率不变。

3.2 AWGN信道、最大似然解调与比特恢复

星座生成后,核心就是加噪声和解调。加噪时看似简单的“信噪比换算”,实际上是整个仿真的黑匣子所在,参数搞错整条曲线都会漂移。

def default_rng(): return np.random.default_rng(42) def bits_to_symbols(bits, const): """把0/1比特流按每log2(M)比特一组映射成星座符号""" M = len(const) k = int(np.log2(M)) if len(bits) % k != 0: raise ValueError("比特数必须是每符号比特数的整数倍") weights = 1 << np.arange(k - 1, -1, -1) # 高位在前 idx = bits.reshape(-1, k).dot(weights) return const[idx] def add_awgn(symbols, eb_n0_db, M, rng): """AWGN信道,输入星座已归一化到平均功率Es=1""" k = int(np.log2(M)) eb_n0 = 10 ** (eb_n0_db / 10) es_n0 = k * eb_n0 # Es/N0 = k * Eb/N0 n0 = 1.0 / es_n0 # Es=1,反推N0 noise = np.sqrt(n0 / 2) * ( rng.standard_normal(len(symbols)) + 1j * rng.standard_normal(len(symbols)) ) return symbols + noise def demodulate(rx, const): """最小欧氏距离硬判决,返回二进制比特流""" dist = np.abs(rx[:, None] - const[None, :]) ** 2 idx = np.argmin(dist, axis=1) k = int(np.log2(len(const))) weights = 1 << np.arange(k - 1, -1, -1) return ((idx[:, None] >> weights) & 1).reshape(-1)

加噪声的换算逻辑是:星座平均功率已经归一化为1,每符号信噪比Es/N0等于k乘以Eb/N0,所以先由eb_n0_db得到线性值eb_n0,再乘k得到Es/N0,最后反推N0。复基带噪声的实部和虚部各占N0/2方差,所以噪声表达式里是np.sqrt(n0 / 2)乘两个独立高斯序列。

解调的rx[:, None] - const[None, :]是把每个接收符号和全部星座点做差,得到一个二维距离矩阵,argmin取最近星座点的索引,最后把索引拆回比特。这就是最大似然硬判决在AWGN下的完整实现,没有引入任何载波同步或者信道估计,仿真的是“理想同步”的上界性能。

3.3 四种调制一次性仿真跑起来

把链路串成蒙特卡洛循环,四种调制跑同一个误码率统计逻辑,不做任何特殊分支。

def run_simulation(M, eb_n0_db, min_errors=100, max_bits=2_000_000): """在给定Eb/N0下统计BER,至少积累min_errors个错误比特""" const = qam_constellation(M) k = int(np.log2(M)) rng = np.random.default_rng(0) total_errors = 0 total_bits = 0 batch_symbols = 10_000 while total_errors < min_errors and total_bits < max_bits: bits = rng.integers(0, 2, size=batch_symbols * k) tx = bits_to_symbols(bits, const) rx = add_awgn(tx, eb_n0_db, M, rng) rx_bits = demodulate(rx, const) total_errors += int(np.sum(bits != rx_bits)) total_bits += batch_symbols * k ber = total_errors / total_bits return ber, total_errors, total_bits for M in [2, 4, 16, 64]: ber, err, nbits = run_simulation(M, 8.0) print(f"M={M:2d} BER={ber:.3e} errors={err} bits={nbits}")

这里batch_symbols=10000是每批处理的符号数,控制内存占用;min_errors=100保证误码率估计的变异系数在10%左右;max_bits=2000000是为了在64QAM高信噪比段强行停住,避免跑几十分钟只凑不够错误数。这组循环在Eb/N0=8dB下,QPSK理论BER约1.9e-4,仿真跑出1.5e-4到2.5e-4都是正常波动;16QAM理论约9e-3,64QAM约5e-2,仿真值应在各自量级附近。如果你想画完整误码率曲线,把eb_n0_db从0到14每隔1dB扫一遍,低阶调制很快,64QAM跑到12dB以后建议换更小的min_errors或切到理论曲线,不要硬等。

4. 误码率曲线怎么仿得像回事:Eb/N0换算与蒙特卡洛参数

误码率曲线横轴到底用Eb/N0还是SNR,是初学者最容易踩的第一个门槛。不同调制方式每符号携带的比特数不同,横轴不统一,对比就没有意义。Eb/N0去掉了“每符号比特数”这个变量,让BPSK、QPSK、16QAM、64QAM在同一条公平的线上比较。

4.1 Eb/N0和SNR到底差多少:一张换算表

同样的Eb/N0下,不同调制方式的Es/N0不一样,差的就是k的增益。换算关系就是Es/N0 = k * Eb/N0,转成dB是Es/N0_dB = Eb/N0_dB + 10 * log10(k)。

调制方式k10*log10(k)
BPSK10 dB
QPSK23.01 dB
16QAM46.02 dB
64QAM67.78 dB

如果你把SNR当Eb/N0直接用,QPSK会凭空偏3dB,16QAM偏6dB,64QAM偏7.8dB。最明显的特征就是四条曲线挤在一起,高阶和低阶拉不开差距。正确做法是像前面的add_awgn那样,先由Eb/N0推出Es/N0,再由Es/N0反推N0。

这里补充一个细节:很多教科书公式里写的是“SNR = Eb/N0 + 10log10(Rb/B)”,其中Rb/B是频谱效率。基带仿真里符号速率等于带宽,所以频谱效率约等于k。严格说还要看脉冲成形滚降系数,但理想条件下用k做换算就够了,这也是大多数验证链路的默认假设。

4.2 蒙特卡洛终止条件:错100个比特再停手

误码率是“错误比特数除以总比特数”的统计量,统计误差主要取决于错误个数而不是总比特数。如果只跑固定10万比特,在高信噪比段可能一个错误都没有,误码率就是0,画在log坐标上直接断点。正确做法是用“累计错误数”作为终止条件。

100个错误对应的估计变异系数约1/sqrt(100)=10%,支撑三位有效数字够用,也是工程上跑误码率曲线的通用折中。想要更平滑的曲线就提到300个错误,代价是仿真时间乘3。64QAM在Eb/N0=14dB时误码率大约1e-5到1e-6量级,想凑100个错误至少要跑1000万到1亿比特,纯Python循环会非常慢。工程上仿真曲线只画到1e-4或1e-5为止,更低误码率交给理论公式外推,或用半解析方法。

4.3 理论曲线与仿真曲线对照:一个能直接复制的循环

工程上常用的方形QAM误码率一阶近似公式是:

import math def qfunc(x): return 0.5 * math.erfc(x / math.sqrt(2)) def ber_theory(eb_n0_db, M): """方形QAM格雷映射一阶近似,M=2/4/16/64""" k = math.log2(M) eb_n0 = 10 ** (eb_n0_db / 10) arg = 3 * k * eb_n0 / (M - 1) pb = (4 * (1 - 1 / math.sqrt(M)) / k) * qfunc(math.sqrt(arg)) return pb for M in [2, 4, 16, 64]: print(M, f"{ber_theory(8.0, M):.3e}")

该公式在QPSK时正好退化为Q(sqrt(2*Eb/N0)),和BPSK理论一致;16QAM、64QAM在BER低于1e-2后和精确仿真误差很小,足够用来判断仿真链路有没有系统性偏置。将仿真点叠在理论曲线上,四条曲线应该贴着走,如果某条调制系统性偏离超过1dB,基本可以断定参数有问题,不需要先怀疑统计误差。

5. 调制解调仿真翻车现场:五个必查项与排查顺序

下面五条是我在仿这四种调制时踩过、也帮别人查过的坑,按出现频率排序,翻车时按这个顺序过一遍基本能定位。每一条都按现象、原因、解决三个层面说清楚。

5.1 星座点没有归一化,整组曲线集体右移

现象:仿真误码率曲线和理论曲线能对上形状,但所有调制的曲线都偏左,也就是“性能凭空变好”。

原因:生成星座后没除以平均功率。16QAM原始电平均方根是10,如果你以为星座功率已经是1,那么实际加噪声用的N0是按Es=1算的,但真实信号功率是10,等效信噪比比预期高10dB。64QAM差得更多,达到10log10(42)约16.2dB。

解决:在星座生成函数末尾固定写一行断言:

assert np.isclose(np.mean(np.abs(const) ** 2), 1.0), "星座平均功率必须等于1"

遇到仿真和理论对不上时,先跑这个断言,比对着公式找半天快得多。

5.2 复噪声只加了实部,16QAM/64QAM性能“凭空变好”

现象:BPSK和QPSK仿真与理论基本吻合,16QAM明显低于理论,64QAM低得更多。

原因:AWGN复基带噪声必须同时给实部和虚部各加N0/2方差的白噪声。BPSK和QPSK的判决边界分别只受一个方向影响,少一个噪声维度问题不大;16QAM和64QAM的判决区域是二维方格,少一半噪声功率相当于等效信噪比抬高3dB。

解决:加噪声统一写成sqrt(N0/2) * (randn + 1j * randn),不要自作聪明只加实部。顺带提醒,如果后面接脉冲成形滤波器,噪声是经过滤波器后才叠加到信号上的,噪声方差还要乘滤波器能量增益,那是另一层坑。

5.3 自然映射冒充格雷映射,误码率平台降不下来

现象:高信噪比段误码率曲线斜率对了,但整体比理论高出一截,16QAM和64QAM尤其明显。

原因:星座点按自然二进制顺序排列,相邻星座点可能差2到4个比特,一次错判就把多个比特翻转。格雷映射保证相邻点只差1位,误码率统计自然更好。

解决:用idx ^ (idx >> 1)生成格雷码顺序后再排星座点,并自己打印验证。随机取两个相邻星座点,计算比特编码的汉明距离,确认都是1,这个检查写进调试代码里,一劳永逸。

5.4 把Eb/N0当成SNR用,64QAM整整偏移7.8dB

现象:仿真曲线和理论曲线的形状完全一致,但各条调制曲线横向错位,高阶错得最多。

原因:参数传入函数时把eb_n0_db直接当符号信噪比,没有乘k。BPSK不受影响,QPSK偏3dB,16QAM偏6dB,64QAM偏7.8dB,整组曲线看起来像“被什么东西拉开”。

解决:回顾4.1的换算表,在add_awgn入口处统一换算,不要在外部临时折算。加上作为防御的注释以及变量命名区分eb_n0_db和es_n0_db,能极大减少这类误用。

5.5 错误数不够就停,误码率曲线抖成心电图

现象:每条曲线在低误码率段剧烈抖动,同一个信噪比反复跑,结果相差几倍。

原因:终止条件用了固定总比特数。误码率越低,错误数越少,统计波动越大。比如只跑到1e-5量级,可能只有十几个错误,变异系数超过25%。

解决:把终止条件从“总比特数达到N”改成“累计错误达到100”。在最低误码率那一段再加一个最大比特数兜底,避免64QAM高信噪比点无限跑下去。兜底参数建议写2e6或5e6,够验证趋势即可,不要再往下降。

6. 判断调制解调有没有写对的快速手段:星座图、EVM和理论曲线三方对账

6.1 三步自查:从无噪到有噪再到数值对账

写完调制解调后,先别急着刷误码率曲线,按下面三步走,每一步都能快速暴露问题。

第一步,无噪声直连。发射符号直接做解调,错误数必须为0。这验证映射和解调索引方向一致,也是最容易被忽视的。第二步,加小噪声后画出接收端星座散点图,观察每个簇的形状。AWGN下的簇应该是各向同性的圆形,如果横向或纵向拉长,说明噪声只加了一个维度,或者I/Q两路增益不平衡。第三步,用误差矢量幅度(EVM)跟信噪比对账,这是数值上最硬的一条。

def evm_rms(rx, tx): """接收符号与发射符号的RMS误差矢量幅度,基于平均功率归一化""" num = np.sum(np.abs(rx - tx) ** 2) den = np.sum(np.abs(tx) ** 2) return np.sqrt(num / den)

这个EVM定义以星座平均功率为基准,幅值在0到1之间。在AWGN下,EVM和SNR存在一个经验换算:SNR_dB ≈ -20 * log10(EVM_rms)。比如64QAM在Eb/N0=14dB时,换算成Es/N0大约是21.8dB,理论上EVM应该接近8%,差别太大就说明某个环节出了问题。注意EVM的定义有不同版本,有些仪器用峰值功率归一化,对账前要先确认两边基准一致。

个人习惯是每次跑误码率之前,先打印对应信噪比下的EVM,确认它和理论换算值偏差在0.5dB以内,再开始蒙特卡洛循环。这条习惯帮我拦下过不少“曲线看起来对但细节不对”的情况。希望帮到你。

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

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

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

立即咨询