☰
OFDM系统中QPSK与16QAM误码率对比:噪声功率换算与MATLAB仿真
2026/10/1 4:00:31 网站建设 项目流程

最近在折腾通信系统仿真,把OFDM链路搭起来之后,第一个让我觉得有点意思的点,就是调制方式的选择对误码率的影响。同样是发数据,用QPSK和16QAM,抗噪能力完全是两种画风——QPSK像穿了一件厚防弹衣,16QAM则为了多捎带两倍的比特,把防弹衣换成了衬衫。这篇文章就把我做的一个最简单的OFDM链路下的QPSK vs 16QAM对比实验完整记录下来,代码直接给,能跑,顺便把我调参时踩过的几个关键坑也一并说清楚。

本文适合正在学通信原理、做课程设计、或者刚接触通信系统仿真、想验证课本上误码率公式的朋友。看完你不仅能复现这条对比曲线,更重要的是能搞明白仿真里最容易搞错的“噪声功率到底该怎么加”。下面开工。

1. 调制阶数与误码率:先搞清楚差距是怎么来的

1.1 星座图视角:同样的功率,点越多越挤

OFDM系统里,IFFT之前的每个子载波都承载一个星座符号。QPSK用的是4个点,每个符号携带2比特;16QAM用的是16个点,每个符号携带4比特。

在平均功率归一化到1的前提下,16个点要均匀铺在同样大小的能量范围内,彼此之间的间距必然比4个点小得多。这个间距叫星座点最小欧氏距离,它直接决定了抗噪声能力。

计算一下:

  • QPSK的星座点坐标是(±1±1j)/√2,最小距离d_min = √2 ≈ 1.414;
  • 16QAM归一化星座点的最小距离d_min = 2/√10 ≈ 0.632。

也就是说,QPSK相邻星座点的距离大约是16QAM的2.24倍。接收机在判决时,噪声会让接收符号在星座图周围形成一个“模糊圈”,噪声功率相同的情况下,模糊圈的半径差不多,但星座点间距不一样,越挤自然越容易判错。

打个不严谨但好记的比方:一个房间里摆4张床和16张床,人半夜迷迷糊糊起来找床,4张床的房间随便摸一下不太容易躺错,16张床的房间基本必错无疑。调制阶数越高,就是往同样的功率“房间”里塞了更多可区分的状态。

这个差距最终体现在误码率曲线上,就是同等信噪比下16QAM的误码率比QPSK高一截,而且信噪比越低越明显。

1.2 理论误码率公式:曲线长什么样,先有个预期

在AWGN信道下,QPSK的理论误码率公式很干净:

Pb = Q(√(2Eb/N0))

16QAM在采用格雷映射时,误码率可以近似写成:

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

这里Eb/N0是每比特信噪比。Q(x)是高斯Q函数。

先算几个典型点,给后续仿真曲线建立一个心理预期:

Eb/N0QPSK理论BER16QAM理论BER
6 dB约 2.3×10⁻³约 2.8×10⁻²
10 dB约 3.3×10⁻⁵约 4.4×10⁻³
14 dB约 1×10⁻¹²量级约 2.7×10⁻⁶

可以看到,在BER=10⁻³这个常用参考点附近,16QAM需要的Eb/N0比QPSK高约3.8~4 dB。这就是高频谱效率换来的功率代价——你多传了1倍的比特,却要多付大约4 dB的发射功率(或者接受更高的误码率)。

需要特别说明的是,我这里统一用Eb/N0作为横轴。这个选择其实是有讲究的:Eb/N0是每信息比特的能量,它剔除了调制阶数本身带来的“速率红利”,让两种调制方式站在公平的赛道上比较“单位比特的抗噪能力”。如果拿Es/N0(每符号信噪比)做横轴,16QAM曲线会显得更差,因为一个16QAM符号要额外承担4比特的传输任务,同样Es/N0下每比特分到的能量天然就少。这个区别在第4章还会再提。

2. 仿真链路怎么搭:OFDM基础模块与噪声功率换算

2.1 OFDM收发链路里每个模块在干什么

代码里面其实就三步:调制后的频域符号做IFFT变到时域,加循环前缀,然后过信道;接收端先去循环前缀,再FFT回频域,最后解调。

具体过程是这样:

  1. 随机产生N_sub×k的比特矩阵,每一行k个比特映射成一个子载波符号;
  2. 用qammod做调制,得到64个频域复符号;
  3. 对这64个符号做IFFT,得到64点时域信号;
  4. 把时域信号末尾N_cp个采样复制到头部,形成带循环前缀的OFDM符号;
  5. 送入AWGN信道,叠加复高斯噪声;
  6. 接收端丢掉前N_cp个采样,再对剩下的64点做FFT;
  7. 把得到的频域符号送进qamdemod解调,对比发送比特统计误码。

这里我要强调一下:在纯AWGN信道下,OFDM的IFFT/FFT和循环前缀并不会改变每个子载波的信噪比,它们更像是“无损通道”。OFDM的真正价值在于对抗频率选择性衰落——那需要多径信道才能体现。本文的实验目的比较单纯,就是看调制方式对BER的影响,所以用AWGN隔离其他变量,逻辑上更干净。

2.2 最容易搞错的参数:时域噪声功率怎么算

这一节是全篇最重要的部分。很多刚做OFDM仿真的朋友,代码跑出来的BER曲线跟理论值差一大截,十有八九是噪声功率换算错了。

问题的根源在于MATLAB的ifft默认做了1/N缩放。频域符号的平均功率如果是1,经过ifft之后,时域信号的每个采样点平均功率就变成了1/N。也就是说,时域信号比频域“看起来”弱了N倍。

如果这时候你不管不顾,直接在时域上按照“频域SNR=1/N0”的方式去叠加噪声方差,那接收端做FFT之后,噪声功率会被FFT的累加效应放大N倍。最终算出来的SNR其实比预期高或者低(取决于你怎么定义),BER曲线就会整体偏移。这里给出标准换算流程:

  1. 将目标Eb/N0(dB)转成线性值EbN0_lin;
  2. 计算符号信噪比EsN0 = k × EbN0_lin,其中k=log2(M);
  3. 由于调制符号平均功率归一化为1,即Es=1,所以N0 = 1/EsN0;
  4. 时域噪声每个采样点的总功率 = N0 / N_sub;
  5. 复高斯噪声实部和虚部各自独立,每维方差再除以2。

写成代码就是:

noise_pow_time = 1 / (k * EbN0_lin * N_sub); noise_std = sqrt(noise_pow_time / 2); noise = noise_std * (randn(size(ofdmTxCP)) + 1j*randn(size(ofdmTxCP)));

这个换算的物理含义是:FFT本身不归一化,时域上N个独立噪声样本的功率会在线性变换后叠加起来。所以想让FFT之后的频域噪声功率恰好等于N0,时域上每个采样点的噪声功率就必须是N0/N。这一点理解了,比背公式有用得多。

3. 直接上代码:OFDM链路下QPSK与16QAM对比实验

3.1 完整MATLAB仿真脚本

下面是完整的仿真代码。我用64个子载波,16个采样点的循环前缀,每个信噪比点跑1000个OFDM符号。Eb/N0从0 dB扫到16 dB。

clear; close all; clc; %% 参数设置 N_sub = 64; % OFDM子载波数 N_cp = 16; % 循环前缀长度 N_sym = 1000; % 每个信噪比点下的OFDM符号数 EbN0_dB = 0:2:16; % 每比特信噪比扫描范围 M_qpsk = 4; % QPSK(4-QAM) M_16qam = 16; % 16QAM k_qpsk = log2(M_qpsk); k_16qam = log2(M_16qam); ber_qpsk = zeros(size(EbN0_dB)); ber_16qam = zeros(size(EbN0_dB)); %% 主仿真循环 for snrIdx = 1:length(EbN0_dB) EbN0_lin = 10^(EbN0_dB(snrIdx)/10); %% ---------- QPSK ---------- EsN0 = k_qpsk * EbN0_lin; N0 = 1 / EsN0; noise_pow_time = N0 / N_sub; noise_std = sqrt(noise_pow_time / 2); errBits = 0; totalBits = 0; for sym = 1:N_sym bits = randi([0 1], N_sub, k_qpsk); txSym = qammod(bits, M_qpsk, 'InputType', 'bit', 'UnitAveragePower', true); % OFDM调制 ofdmTx = ifft(txSym, N_sub); ofdmTxCP = [ofdmTx(end-N_cp+1:end); ofdmTx]; % AWGN信道 noise = noise_std * (randn(size(ofdmTxCP)) + 1j*randn(size(ofdmTxCP))); ofdmRxCP = ofdmTxCP + noise; % OFDM解调 ofdmRx = ofdmRxCP(N_cp+1:end); rxSym = fft(ofdmRx, N_sub); % 解调与误码统计 rxBits = qamdemod(rxSym, M_qpsk, 'OutputType', 'bit', 'UnitAveragePower', true); errBits = errBits + sum(sum(bits ~= rxBits)); totalBits = totalBits + numel(bits); end ber_qpsk(snrIdx) = errBits / totalBits; %% ---------- 16QAM ---------- EsN0 = k_16qam * EbN0_lin; N0 = 1 / EsN0; noise_pow_time = N0 / N_sub; noise_std = sqrt(noise_pow_time / 2); errBits = 0; totalBits = 0; for sym = 1:N_sym bits = randi([0 1], N_sub, k_16qam); txSym = qammod(bits, M_16qam, 'InputType', 'bit', 'UnitAveragePower', true); ofdmTx = ifft(txSym, N_sub); ofdmTxCP = [ofdmTx(end-N_cp+1:end); ofdmTx]; noise = noise_std * (randn(size(ofdmTxCP)) + 1j*randn(size(ofdmTxCP))); ofdmRxCP = ofdmTxCP + noise; ofdmRx = ofdmRxCP(N_cp+1:end); rxSym = fft(ofdmRx, N_sub); rxBits = qamdemod(rxSym, M_16qam, 'OutputType', 'bit', 'UnitAveragePower', true); errBits = errBits + sum(sum(bits ~= rxBits)); totalBits = totalBits + numel(bits); end ber_16qam(snrIdx) = errBits / totalBits; end % 仿真结果为0的点给个下限,方便画对数坐标 ber_qpsk(ber_qpsk == 0) = 1e-7; ber_16qam(ber_16qam == 0) = 1e-7; %% 理论曲线 EbN0_lin_vec = 10.^(EbN0_dB/10); ber_qpsk_theory = qfunc(sqrt(2*EbN0_lin_vec)); ber_16qam_theory = (3/4)*qfunc(sqrt(4*EbN0_lin_vec/5)); %% 画图 figure; semilogy(EbN0_dB, ber_qpsk, 'o-', 'LineWidth', 1.5); hold on; semilogy(EbN0_dB, ber_16qam, 's-', 'LineWidth', 1.5); semilogy(EbN0_dB, ber_qpsk_theory, '--', 'LineWidth', 1); semilogy(EbN0_dB, ber_16qam_theory, '--', 'LineWidth', 1); grid on; xlabel('Eb/N0 (dB)'); ylabel('误码率 BER'); legend('QPSK 仿真', '16QAM 仿真', 'QPSK 理论', '16QAM 理论', 'Location', 'southwest'); title('AWGN信道下OFDM系统QPSK与16QAM误码率对比');

3.2 代码里的几个关键选择,说一下为什么

首先,qammod我特意加了'UnitAveragePower', true。这个参数会把星座图的平均功率归一化到1。没有它,星座点坐标是按最小距离2来定义的,符号能量不等于1,那信噪比设置和噪声功率换算全部要跟着改,非常容易出错。归一化之后,Es就可以心安理得地当成1来参与信噪比换算。

其次,调制对象是N_sub×k的比特矩阵。qammod的'InputType','bit'模式要求输入矩阵每行正好是k个比特,输出对应一个复符号。这样就不用手写比特到符号的映射了,16QAM的格雷码映射交给库函数处理,省心。

最后,我这里保留了完整的IFFT、加CP、去CP、FFT流程,而不是偷懒直接在频域上加噪声。虽然在AWGN下效果一样,但保留完整链路的好处是:如果你后面把信道换成多径衰落信道,这套代码框架可以直接复用,只要把信道那一段替换掉就行。

4. 实验结果:误码率曲线到底差了多少

4.1 仿真曲线与理论曲线对照

跑完代码之后,你会看到仿真点基本都落在理论曲线上,QPSK和16QAM两条曲线在1e-2到1e-4区间跟理论值贴合得相当好。这是我最喜欢这组实验的一点:代码简简单单,但结果和公式“严丝合缝”,说明噪声功率换算没有出错。

几个典型观察:

  • 在Eb/N0=8 dB附近,QPSK的BER大约是2×10⁻⁴量级,16QAM则到不了这么低,大约在1×10⁻²量级,差了两个数量级左右;
  • 16QAM要拿到和QPSK相同的BER,Eb/N0大约需要高出4 dB。比如要BER=10⁻³,QPSK在7 dB附近就能做到,16QAM得推到10.5 dB以上;
  • 高信噪比端,QPSK在12 dB以后误码率就低到单次仿真可能一个错误比特都抓不到,曲线直接掉底。这是有限样本统计的必然结果,不是代码bug。

如果你画完图发现两条曲线的间距比理论预期明显偏大或偏小,优先回头检查噪声波形。最常见的低级错误是把时域噪声标准差直接写成了sqrt(N0/2),忘了除以子载波数N_sub。

4.2 如果横轴换成Es/N0,结论会变化吗

有些朋友习惯把横轴设为SNR(也就是Es/N0),两种画法结论并不矛盾,但表达的重点不同。

用Eb/N0做横轴,QPSK和16QAM的曲线差距是“调制自带的抗噪差距”加上“采样判决时星座点疏密的影响”,这是一个公平的比特级比较。用Es/N0做横轴,等于假设两种调制都能拿到相同的符号能量,那16QAM每个比特分到的能量天然只有QPSK的一半,曲线差距会拉得更开。

工程上做链路预算时,两种口径都会出现。但在做“同样发射功率下,哪种调制更可靠”这个问题时,Eb/N0是更干净的参考系。这也是为什么本文所有公式和仿真点都以Eb/N0为基准。

5. 实验中的几个坑和我的调参心得

5.1 噪声功率换算的快速自检方法

噪声加得对不对,有个特别快的自检方法:看BER在极低信噪比和极高信噪比两端的表现。

在Eb/N0=0 dB这种极低信噪比下,QPSK的理论BER大约是0.08量级,16QAM大约是0.14量级。如果你的仿真结果在这附近,那说明噪声功率量级基本对了。如果出来是0.4或者0.5这样的数值,基本等于在扔硬币猜比特,那说明噪声加太大了,或者调制解的映射方式有问题。

高信噪比端的自检反过来:QPSK在16 dB时BER应该低到你根本数不出几个错误比特,如果这时曲线还在1e-2量级趴着,赶紧回查是否忘了做归一化。

5.2 为什么我坚持用“公平横轴”这个说法

再强调一次Eb/N0和Es/N0的换算关系:

Es = Eb × log2(M)

QPSK里k=2,所以Es/N0比Eb/N0高3 dB;16QAM里k=4,所以Es/N0比Eb/N0高6 dB。很多人仿真出来的QPSK和16QAM曲线在图上错位得很奇怪,有可能是把“SNR”直接当成横轴,然后用同一个N0去套两种调制,却忽略了每符号携带比特数不同导致Es对应的Eb不同。只要严格按本文第2章的步骤走,这个坑基本不会踩到。

5.3 仿真点数、抖动与统计置信度

N_sym=1000的话,每个信噪比点QPSK要统计64×2×1000=128000个比特,这个量级在低信噪比下足够稳定,但在高信噪比下就会显得寒酸。

我的经验是:如果你的目标误码率要到1e-5以下,至少要让误码数累积到几十个以上,否则两次跑脚本的结果可能差好几倍。建议做法是分段设置仿真点数——低信噪比区间用1000个OFDM符号,高信噪比区间用5000甚至更多。仿真时间多花不了几秒,曲线平滑度会好很多。

5.4 不想依赖通信工具箱的话,怎么自己写映射

qammod是Communications Toolbox的函数,如果机器上没装这个工具箱,可以自己写映射。QPSK很简单,00/01/11/10映射到(±1±1j)/√2,注意采用格雷码排列。16QAM稍微麻烦一点,需要把每4个比特拆成I路2比特和Q路2比特,分别映射到{-3,-1,+1,+3}/√10,然后I+jQ组合。

我自己做实验的时候喜欢先用工具箱把流程跑通,确认理论曲线对上了,再手写映射去验证对星座图的理解。两者结果完全一致的话,说明你对调制原理的理解已经过关了。

最后说点实在的

我个人的体会是,做通信仿真最难的不是写代码本身,而是确保你测的物理量和你以为的物理量是同一个东西。Eb/N0、Es/N0、时域噪声功率、频域噪声功率,这几个概念折腾清楚了,后面加多径信道、加载波频偏、加相位噪声,都是有章可循的,因为核心里面都是同一套信噪比思维。

这次实验做完之后,我又顺手把信道换成了频率选择性衰落信道,加上循环前缀和多径时延,再跑QPSK和16QAM,曲线形态和AWGN下完全不一样——OFDM在对抗多径上的价值立刻体现出来了。如果你想继续深入,这个方向是个很自然的下一站。先写到这,下一篇聊衰落信道下的对比时再接着展开。

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

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

立即咨询