☰
QAM调制与最大似然检测:从星座图到误码率曲线完整链路
2026/10/12 16:18:24 网站建设 项目流程

简介:这份资源面向通信工程、数字通信课程学习者及MATLAB仿真入门者,聚焦64QAM调制、误码率计算与最大似然检测三个核心知识点,帮助读者理解高阶调制在噪声信道下的性能评估方法。压缩包内共1个文件,为m脚本类型,整体约1KB,可直接在MATLAB环境中运行,用于模拟随机二进制序列映射为64QAM符号、叠加信道噪声、执行最大似然检测并统计误码率的完整流程。已有569人学习下载,说明该仿真脚本在同类教学资源中具有一定参考价值。读者可借助脚本直观观察星座映射与判决距离的关系,理解MLD为何能在理论上取得最优误码性能,并对比不同信噪比条件下的BER变化趋势,为课程实验、算法验证或进一步扩展近似检测方案提供可复用的代码基础。

1. QAM 调制与最大似然检测:从星座图到误码率曲线的完整链路

做数字通信基带仿真的人,迟早会撞上 QAM 调制和误码率这两件事。我最早接触是在一个 OFDM 链路里,当时用 16QAM 跑出来的 BER 曲线跟理论值差了将近 3 dB,排查了两天才发现是判决门限没做归一化。QAM 调制本身不复杂——把比特映射到 I/Q 两路的幅度上,但真正决定系统性能的是接收端怎么判、怎么算误码率、最大似然检测在什么条件下才比硬判决有优势。这篇笔记就按这条链路走一遍:星座图怎么建、AWGN 下理论误码率怎么推、最大似然检测怎么实现、仿真和理论对不上的时候该查哪里。适合正在做基带仿真、链路预算或者通信算法验证的工程师,新手能跟着代码跑通,熟手可以直接看参数边界和踩坑部分。

2. QAM 星座图构建与格雷码映射:比特怎么变成 I/Q 幅度

2.1 为什么 QAM 的星座图不能随便画

QAM 调制的核心是把一组比特映射到复平面上的一个点,这个点的实部是 I 路幅度,虚部是 Q 路幅度。听起来简单,但星座图的排列方式直接决定了误码率。最常见的做法是方形星座图,16QAM 就是 4×4 的网格,64QAM 是 8×8。每个维度上的幅度取值通常是 ±1, ±3, ±5, … 这样的奇数序列,归一化之后平均功率为 1。

为什么用奇数序列而不是 ±1, ±2, ±3?因为方形 QAM 的每个维度本质上是脉冲幅度调制(PAM),PAM 的最优判决门限在相邻电平的中点,等间距排列能让最小欧氏距离最大化。如果间距不均匀,最小距离会变小,误码率直接恶化。常见做法是让相邻星座点的距离为 2d,然后整体归一化。

格雷码映射是另一个关键点。相邻星座点之间只差一个比特,这样当一个符号被判错到相邻点时,只有一个比特出错。对于 16QAM,格雷映射能把误比特率近似降到误符号率的 1/4 左右。不做格雷映射的话,一个符号错误可能带来多个比特错误,BER 曲线会明显抬高。

2.2 用 Python 生成 16QAM 星座图并验证格雷映射

下面这段代码生成 16QAM 的星座图,做格雷映射,并验证相邻点的比特差异。

import numpy as np def gray_code(n): """生成 n 位格雷码序列""" return n ^ (n >> 1) def qam16_constellation(): """生成 16QAM 星座图,返回符号点和对应的 4bit 格雷码""" # I/Q 两路各 4 个电平:-3, -1, 1, 3 levels = np.array([-3, -1, 1, 3]) points = [] bits = [] for i, re in enumerate(levels): for j, im in enumerate(levels): # 格雷码:I 路用 i 的格雷码,Q 路用 j 的格雷码 gray_i = gray_code(i) gray_j = gray_code(j) # 拼接成 4bit:高 2 位给 I,低 2 位给 Q bit_str = format(gray_i, '02b') + format(gray_j, '02b') points.append(re + 1j * im) bits.append(bit_str) # 归一化平均功率为 1 points = np.array(points) avg_power = np.mean(np.abs(points)**2) points = points / np.sqrt(avg_power) return points, bits points, bits = qam16_constellation() for p, b in zip(points, bits): print(f"符号 {p:.3f} -> 比特 {b}") # 验证相邻点的汉明距离 def hamming_distance(b1, b2): return sum(c1 != c2 for c1, c2 in zip(b1, b2)) # 检查水平相邻的点 for idx in range(len(points)): for jdx in range(idx+1, len(points)): dist = abs(points[idx] - points[jdx]) if dist < 0.7: # 相邻点距离约 0.632 hd = hamming_distance(bits[idx], bits[jdx]) if hd != 1: print(f"警告:相邻点 {bits[idx]} 和 {bits[jdx]} 汉明距离为 {hd}")

这段代码的逻辑是:先生成 4 个电平值,然后对每个 I/Q 组合,用格雷码把索引转成比特。格雷码的规则是n ^ (n >> 1),这样相邻索引只差一个比特。归一化那一步很关键,不归一化的话后面加噪声时信噪比的定义会乱。最后验证相邻点的汉明距离,正常情况下应该都是 1,如果有不是 1 的,说明映射有问题。

参数说明:levels数组决定了星座图的形状,改成[-1, 1]就是 QPSK,改成 8 个值就是 64QAM。归一化系数np.sqrt(avg_power)对 16QAM 大约是 2.236,对 64QAM 是 6.325。这个系数在计算理论误码率时要用到,因为信噪比的定义依赖于符号功率。

提示:格雷映射不是唯一的选择,但在 AWGN 信道下它是最优的。如果信道有相位噪声或者非线性,可能需要重新考虑映射方式。

3. AWGN 信道下 QAM 误码率推导与仿真:理论曲线怎么算、仿真怎么跑

3.1 理论误码率的公式从哪来

QAM 在 AWGN 信道下的误符号率有一个近似公式。对于方形 M-QAM,M = 2^k,k 为偶数,误符号率的近似为:

SER ≈ 4(1 - 1/√M) Q(√(3γ_s / (M - 1)))

其中 γ_s 是符号信噪比,Q 函数是高斯尾概率。这个公式的推导思路是:把 QAM 看成两个正交的 PAM,每个维度的误码率是 PAM 的误码率,然后联合起来。更精确的表达式要考虑两个维度同时出错的概率,但近似公式在信噪比不太低的时候足够准。

误比特率和误符号率的关系取决于映射方式。格雷映射下,BER ≈ SER / log2(M)。对于 16QAM,log2(16) = 4,所以 BER ≈ SER / 4。这个关系在信噪比高的时候很准,信噪比低的时候会有偏差,因为相邻符号错误可能带来多个比特错误。

γ_s 和 γ_b 的关系是 γ_s = γ_b × log2(M)。很多人在仿真时把 Eb/N0 和 SNR 搞混,导致曲线平移。Eb/N0 是每比特信噪比,SNR 是符号信噪比,两者差一个 log2(M) 的因子。画图的时候通常用 Eb/N0 做横轴,这样不同阶数的 QAM 可以放在一起比较。

3.2 蒙特卡洛仿真:从加噪声到统计误码率

下面这段代码实现完整的 16QAM 蒙特卡洛仿真,包括调制、AWGN 加噪、硬判决解调、误码率统计。

import numpy as np def qam16_modulate(bits): """16QAM 调制:输入比特流,输出符号序列""" # 每 4 个比特一组 bits = np.array(bits).reshape(-1, 4) # 格雷码逆映射:比特 -> 索引 # 格雷码转二进制:g = b ^ (b >> 1),逆变换是 b = g ^ (g >> 1) ^ (g >> 2) ... # 这里直接用查表法 gray_map = { '0000': 0, '0001': 1, '0011': 2, '0010': 3, '0110': 4, '0111': 5, '0101': 6, '0100': 7, '1100': 8, '1101': 9, '1111': 10, '1110': 11, '1010': 12, '1011': 13, '1001': 14, '1000': 15 } levels = np.array([-3, -1, 1, 3]) / np.sqrt(10) # 归一化 symbols = [] for b in bits: bit_str = ''.join(str(x) for x in b) idx = gray_map[bit_str] re = levels[idx // 4] im = levels[idx % 4] symbols.append(re + 1j * im) return np.array(symbols) def qam16_demodulate(symbols): """16QAM 硬判决解调""" levels = np.array([-3, -1, 1, 3]) / np.sqrt(10) bits_out = [] for s in symbols: # 找最近的 I 和 Q 电平 re_idx = np.argmin(np.abs(s.real - levels)) im_idx = np.argmin(np.abs(s.imag - levels)) idx = re_idx * 4 + im_idx # 逆格雷映射 inv_gray = {v: k for k, v in { '0000': 0, '0001': 1, '0011': 2, '0010': 3, '0110': 4, '0111': 5, '0101': 6, '0100': 7, '1100': 8, '1101': 9, '1111': 10, '1110': 11, '1010': 12, '1011': 13, '1001': 14, '1000': 15 }.items()} bit_str = inv_gray[idx] bits_out.extend([int(x) for x in bit_str]) return np.array(bits_out) def simulate_ber(ebn0_db, num_bits=100000): """仿真给定 Eb/N0 下的误码率""" ebn0 = 10 ** (ebn0_db / 10) # 16QAM: Es/N0 = Eb/N0 * log2(16) = 4 * Eb/N0 esn0 = 4 * ebn0 # 噪声功率:符号功率为 1,噪声方差 = 1 / (Es/N0) noise_var = 1 / esn0 noise_std = np.sqrt(noise_var / 2) # 复噪声,实部虚部各一半 tx_bits = np.random.randint(0, 2, num_bits) tx_symbols = qam16_modulate(tx_bits) noise = noise_std * (np.random.randn(len(tx_symbols)) + 1j * np.random.randn(len(tx_symbols))) rx_symbols = tx_symbols + noise rx_bits = qam16_demodulate(rx_symbols) # 对齐长度 min_len = min(len(tx_bits), len(rx_bits)) errors = np.sum(tx_bits[:min_len] != rx_bits[:min_len]) return errors / min_len # 跑一组 Eb/N0 ebn0_range = np.arange(0, 16, 2) ber_results = [] for ebn0_db in ebn0_range: ber = simulate_ber(ebn0_db, num_bits=200000) ber_results.append(ber) print(f"Eb/N0 = {ebn0_db} dB, BER = {ber:.6f}")

这段代码的关键点:调制时用查表法做格雷映射,解调时找最近的 I/Q 电平。噪声方差的计算是1 / esn0,因为符号功率归一化到 1 了。复噪声的实部和虚部各占一半方差,所以noise_std = sqrt(noise_var / 2)。仿真点数要足够多,200000 个比特在 BER 10^-4 量级还能有统计意义,再低就要加点数或者用重要性采样。

参数说明:ebn0_db是每比特信噪比,单位 dB。num_bits是仿真比特数,越大越准但越慢。esn0 = 4 * ebn0是因为 16QAM 每符号 4 比特。如果换成 64QAM,这个系数变成 6。噪声方差的计算依赖于符号功率归一化,如果星座图没归一化,这里要相应调整。

注意:仿真曲线和理论曲线在低信噪比时会有偏差,因为近似公式在高误码率时不准。另外,如果仿真点数不够,高信噪比下的 BER 会变成 0,画图时要用对数坐标并处理零值。

4. 最大似然检测:什么时候比硬判决强、怎么实现

4.1 最大似然检测的原理和适用场景

硬判决解调是逐个符号找最近的星座点,然后逆映射成比特。这在 AWGN 信道下是最优的,因为噪声是高斯分布,最大似然准则退化成最小欧氏距离。但硬判决有个问题:它丢掉了软信息。当后面接信道编码时,软信息比硬比特更有价值。

最大似然检测的思路是:对每个接收符号,计算它到所有星座点的似然函数,然后选似然最大的那个。在 AWGN 下,似然函数是高斯概率密度,最大化似然等价于最小化欧氏距离。所以对于无编码的 QAM,最大似然检测和硬判决的结果是一样的。区别在于,最大似然检测可以输出软信息,比如对数似然比(LLR),供译码器使用。

如果信道有记忆或者有干扰,最大似然检测就要考虑符号序列的联合似然,这时候复杂度会指数增长。常见做法是用 Viterbi 算法或者球形译码来降低复杂度。但在纯 AWGN 无编码的场景下,逐符号的最大似然就是最优的,不需要序列检测。

4.2 逐符号最大似然检测的实现与软信息输出

下面这段代码实现 16QAM 的逐符号最大似然检测,输出每个比特的 LLR。

def qam16_ml_detect(rx_symbols, noise_var): """16QAM 最大似然检测,输出每个比特的 LLR""" levels = np.array([-3, -1, 1, 3]) / np.sqrt(10) # 生成所有星座点 constellation = [] bit_labels = [] gray_map = { '0000': 0, '0001': 1, '0011': 2, '0010': 3, '0110': 4, '0111': 5, '0101': 6, '0100': 7, '1100': 8, '1101': 9, '1111': 10, '1110': 11, '1010': 12, '1011': 13, '1001': 14, '1000': 15 } inv_gray = {v: k for k, v in gray_map.items()} for idx in range(16): re = levels[idx // 4] im = levels[idx % 4] constellation.append(re + 1j * im) bit_labels.append([int(x) for x in inv_gray[idx]]) constellation = np.array(constellation) bit_labels = np.array(bit_labels) llrs = [] for s in rx_symbols: # 计算到所有星座点的距离 distances = np.abs(s - constellation) ** 2 # 似然:exp(-distance / (2 * noise_var)) likelihoods = np.exp(-distances / (2 * noise_var)) # 对每个比特计算 LLR llr = [] for bit_idx in range(4): # 比特为 0 的星座点似然之和 p0 = np.sum(likelihoods[bit_labels[:, bit_idx] == 0]) p1 = np.sum(likelihoods[bit_labels[:, bit_idx] == 1]) llr.append(np.log(p0 / (p1 + 1e-300))) llrs.append(llr) return np.array(llrs) # 测试 np.random.seed(42) tx_bits = np.random.randint(0, 2, 400) tx_symbols = qam16_modulate(tx_bits) noise_var = 0.01 noise = np.sqrt(noise_var/2) * (np.random.randn(len(tx_symbols)) + 1j*np.random.randn(len(tx_symbols))) rx_symbols = tx_symbols + noise llrs = qam16_ml_detect(rx_symbols, noise_var) print("前 5 个符号的 LLR:") print(llrs[:5])

这段代码的逻辑是:对每个接收符号,计算它到 16 个星座点的欧氏距离,然后转成似然。对每个比特,把所有该比特为 0 的星座点的似然加起来,除以该比特为 1 的似然之和,取对数得到 LLR。LLR 为正表示比特 0 更可能,为负表示比特 1 更可能,绝对值越大越可靠。

参数说明:noise_var是噪声方差,必须和仿真时用的噪声功率一致。如果噪声方差估计不准,LLR 的尺度会偏,影响译码性能。1e-300是防止除零的小量。LLR 的计算可以用 max-log 近似来降低复杂度,就是用最大似然代替似然之和,性能损失很小。

提示:在无编码系统中,最大似然检测和硬判决的误码率是一样的。LLR 的价值在于接信道编码时,软判决译码能比硬判决多出 2 dB 左右的增益。

5. 避坑与排查:QAM 误码率仿真对不上理论的 5 个常见原因

5.1 信噪比定义混淆导致曲线平移

现象:仿真曲线和理论曲线形状一样,但整体平移了 3 dB 或 6 dB。

原因:Eb/N0 和 Es/N0 搞混了。Eb/N0 是每比特信噪比,Es/N0 是每符号信噪比,两者差 log2(M)。16QAM 差 4 倍,也就是 6 dB。另外,有些代码用 SNR 表示符号信噪比,有些用比特信噪比,不统一就会平移。

解决:画图时统一用 Eb/N0 做横轴。仿真时先算 Es/N0 = Eb/N0 × log2(M),再算噪声方差。理论公式里的 γ_s 是符号信噪比,代入前要确认。

5.2 星座图未归一化导致噪声功率算错

现象:低信噪比时 BER 偏高,高信噪比时曲线斜率不对。

原因:星座点没归一化,符号平均功率不是 1,但噪声方差还是按 1 算的。这样实际信噪比比设定值低,BER 自然偏高。

解决:调制后除以sqrt(mean(abs(points)^2))。16QAM 的归一化系数是sqrt(10),64QAM 是sqrt(42)。归一化后再加噪声,噪声方差按1 / esn0算。

5.3 格雷映射错误导致高信噪比下 BER 偏高

现象:低信噪比时曲线正常,高信噪比时 BER 下降变慢,比理论值高。

原因:格雷映射写错了,相邻星座点的汉明距离不是 1。一个符号错误带来多个比特错误,BER 就降不下去。

解决:用代码验证相邻点的汉明距离,确保都是 1。常见错误是 I 路和 Q 路的格雷码顺序搞反了,或者比特拼接顺序不对。

5.4 仿真点数不足导致高信噪比下 BER 为 0

现象:Eb/N0 大于 12 dB 时 BER 突然变成 0,画图时出现断点。

原因:仿真比特数不够,高信噪比下错误事件太少,统计不到。比如 BER 10^-5,要至少 10^7 个比特才能有 100 个错误。

解决:高信噪比时增加仿真点数,或者用重要性采样。画图时对 BER 为 0 的点做特殊处理,比如设成 10^-6 或者不画。

5.5 噪声方差计算错误导致 LLR 尺度不对

现象:最大似然检测的 LLR 绝对值偏大或偏小,接译码器时性能差。

原因:LLR 计算用的噪声方差和实际加噪的方差不一致。LLR 对噪声方差很敏感,差一倍会导致 LLR 尺度差一倍。

解决:在接收端估计噪声方差,或者用已知的导频符号来估计。仿真时确保noise_var和加噪时用的方差完全一致。

6. 从误码率曲线到链路预算:一个可复用的验证习惯

跑通仿真之后,我一般会做一件事:把仿真曲线和理论曲线画在同一张图上,看它们在中高信噪比区域是否重合。如果重合,说明调制、加噪、解调这条链路没问题。如果不重合,先查信噪比定义,再查归一化,最后查格雷映射。这个顺序能解决 90% 的问题。

对于最大似然检测,我的习惯是先用无编码场景验证 LLR 的符号是否正确——正负号应该和硬判决结果一致。然后再接一个简单的卷积码或者 LDPC 码,看软判决比硬判决能多出多少增益。16QAM 在 AWGN 下,软判决通常比硬判决好 1.5 到 2 dB。如果增益明显小于这个值,就要查 LLR 的尺度或者译码器的输入范围。

还有一个容易忽略的点:QAM 的阶数越高,对信噪比的要求越高。16QAM 比 QPSK 多需要约 4 dB,64QAM 再多 4 dB 左右。做链路预算时,把这些余量算进去,别等到现场测试才发现 margin 不够。我自己的做法是建一个表格,把不同调制阶数、不同码率下的所需 Eb/N0 列出来,每次设计新链路时直接查表,省得重复推导。

最后说一个验证技巧:用蒙特卡洛仿真跑出来的 BER 曲线,在 BER = 10^-3 附近和理论值对比,如果误差在 0.2 dB 以内,基本可以认为链路是干净的。如果误差超过 0.5 dB,一定要找到原因,因为在实际系统中,这 0.5 dB 可能就是链路余量的全部。希望帮到你。

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

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

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

立即咨询