简介:BPSK与QPSK调制方案比较的Matlab仿真代码,面向电子信息工程、通信、数学等专业学生,适用于课程设计、期末大作业或毕业设计。代码采用参数化编程,信噪比等关键参数可灵活调整,并配有详细注释,便于学习者理解两种调制方式在接收机复杂度、频谱利用率与误码性能等方面的差异。BPSK用两种相位表示二进制码元,QPSK用四种相位同时传输两比特,对比代码可直观呈现两者在带宽效率与解调复杂度之间的取舍。压缩包共2个文件,由一个可运行的m脚本文件与一个说明txt文件组成,整体仅844B,轻量简洁。已有68人学习使用,可通过调整参数对比不同信噪比下的仿真结果,借助附赠的可直接运行的案例快速上手,适合在Matlab 2014/2019a/2024a等版本中运行,是掌握BPSK与QPSK原理及仿真流程的实用资料。
1. 一份能直接跑的调制比较代码:BPSK 和 QPSK 的 Matlab 实现
做通信课程设计时,最耗时间的不是理解公式,而是把调制、加噪、判决、误码统计这四段逻辑在 Matlab 里拼起来。这份BPSK和QPSK调制方案的比较Matlab代码,把两套调制链路放在同一个工程里,跑一次 main 脚本就能同时输出误码率曲线和星座图,用同一套随机数和噪声参数做对比,直接省掉你在代码里反复对照调参的功夫。它不依赖通信工具箱里的高封装函数,核心调制解调都是用基础矩阵运算写的,适合还没彻底搞懂 Simulink 的学生,也适合需要快速验证算法性能的工程师。你拿到手要做的第一件事不是跑通,而是把里面的符号映射和噪声方差两处看清楚,这两处决定了后面所有曲线是否可信。
2. 调制原理与比较维度:从星座图到误码率公式
2.1 BPSK 为什么是“把比特映射到符号”的最简模型
BPSK 是二进制相移键控,每个符号只携带 1 bit,映射规则就一句话:比特 0 映射为发送符号 -1,比特 1 映射为 +1。在等效基带模型里,它就是乘一个幅度,没有正交分量。所以 BPSK 的星座图只有两个点,在实轴左右两侧。这个最简单的模型很适合用来核对代码里的能量归一化:如果发送符号是 ±1,那么平均符号能量 E_s = 1,对应的每比特能量 E_b = 1,因为在 BPSK 里 E_b = E_s。很多新手在写 AWGN 信道时会卡在这一步,把噪声方差直接当成 1,于是低信噪比区间的误码率怎么都不对。
BPSK 在加性高斯白噪声下的理论误比特率是 Q(sqrt(2E_b/N_0)),Matlab 里计算这个值可以直接用 erfc 函数或 qfunc 函数。代码包里通常有一个理论曲线函数,我见到的常见写法是:
function ber = ber_awgn_theory(EbN0_dB, mod_type) % 计算 AWGN 信道下 BPSK/QPSK 的理论误比特率 % 输入:EbN0_dB 是每比特信噪比向量(单位 dB) % mod_type 是 'BPSK' 或 'QPSK' % 输出:ber 是与 EbN0_dB 等长的误比特率向量 EbN0 = 10.^(EbN0_dB/10); switch mod_type case 'BPSK' ber = 0.5 * erfc(sqrt(EbN0)); % 与 qfunc(sqrt(2*EbN0)) 等价 case 'QPSK' ber = 0.5 * erfc(sqrt(EbN0)); % 格雷映射下每比特误码率与 BPSK 相同 otherwise error('unknown modulation type'); end end这段代码里的 erfc 和 Q 函数关系需要说清楚:Q(x) = 0.5 * erfc(x/sqrt(2)),所以上述公式里的 sqrt(EbN0) 实际上已经完成了 x = sqrt(2EbN0) 的换算。如果你用 qfunc 写,就是qfunc(sqrt(2*EbN0)),结果完全一样。参数上,EbN0_dB 一般取 0:2:12,这样曲线能覆盖从误码率 0.1 到 1e-6 的区间,又不至于让坐标轴太挤。注意这个理论曲线默认了相干解调和理想同步,一旦你后面加入频率偏差或者相位偏移,理论曲线就不再适用,别拿它去跟带失真的仿真硬比。
2.2 QPSK 的四相映射与 I/Q 正交分解
QPSK 每个符号携带 2 bit,星座图上有四个点,相位分别是 45°、135°、225°、315°。为什么常用这个旋转角?因为对应到 I/Q 两路,恰好每个分量都是 ±1/√2,方便硬判决。映射常见有两种方式:自然编码和格雷编码。格雷编码下,相邻相位只差 1 bit,这样判决出错时更可能错 1 bit,而不是错 2 bit,所以误比特率更低。代码包里做方案比较时一定要确认用的是格雷映射,否则 QPSK 的理论曲线会变成接近二倍 BPSK。
QPSK 符号的复数表达是:
s = (2*b1 - 1)/sqrt(2) + 1j*(2*b2 - 1)/sqrt(2);其中 b1、b2 是两个独立比特。这个结构意味着:QPSK 的实部、虚部各自独立承载一个比特,接收端可以像 BPSK 那样分别判决 I、Q,误比特率自然和 BPSK 一致。这也是比较方案时最直观的结论:相同 E_b/N_0 下,QPSK 的误比特率曲线和 BPSK 重合,但带宽效率是两倍。不过符号误码率不同,QPSK 的符号误码率约等于两倍 BPSK 的误比特率(在高信噪比近似下),因为一个符号由两比特组成,任何一个比特出错都导致符号错误。
实际实现中,发送端要把串行比特流变成两路并行。常见代码里用 reshape 实现:
data_matrix = reshape(data, 2, N_bits/2).'; b1 = data_matrix(:,1); b2 = data_matrix(:,2);这里把长度为 N_bits 的比特流重排成 N_bits/2 行 2 列的矩阵,第一列给 I 路,第二列给 Q 路。注意 reshape 是按列填充的,所以 data 的前一半会进 b1,后一半会进 b2,而不是交替进入。对于随机比特流这种安排没问题,但如果你后续要对比发送比特和接收比特,就必须在接收端按同一顺序拆回,否则错位统计出的误码率没有任何意义。
2.3 比较维度:带宽效率、误码率、解调复杂度
做方案比较不能只看误码率曲线,要同时列出带宽和实现成本。下面这张表是我在这类代码包中常用的对照,也是你写实验报告时可以直接引用的结论:
| 维度 | BPSK | QPSK |
|---|---|---|
| 每符号比特数 | 1 | 2 |
| 带宽效率(相对) | 1 bit/s/Hz | 2 bit/s/Hz |
| 理论误比特率(AWGN,格雷映射) | 0.5*erfc(sqrt(EbN0)) | 与 BPSK 相同 |
| 符号误码率(高SNR近似) | 约等于误比特率 | 约为 2 倍误比特率 |
| 解调复杂度 | 只需判决实部 | 实部虚部分别判决,复杂度略高 |
| 对 I/Q 幅度平衡的敏感度 | 不敏感 | 敏感,I/Q 不平衡会带来星座畸变 |
从这张表能看出,QPSK 在带宽效率上碾压 BPSK,但抗噪性能在高斯白噪声信道下并没有损失。代价是它对前端电路的线性度和 I/Q 平衡性要求更高,基带仿真里看不出来,但射频端会翻车。代码包里通常只对比误码率曲线,你如果能在报告里补一段关于带宽效率和 I/Q 失配的分析,整个方案就比较完整了。
3. 代码结构与核心函数:调制、解调、加噪模块怎么配合
3.1 文件清单与运行入口
这份资源解压后一般不超过 10 个 .m 文件,核心文件是主脚本和几个功能函数。常见布局我列一下:
| 文件 | 职责 |
|---|---|
| main_bpsk_qpsk_comparison.m | 主入口,设置参数并调用其余函数 |
| mod_bpsk.m | BPSK 调制:比特到 ±1 |
| mod_qpsk.m | QPSK 调制:成对比特到四相复数符号 |
| awgn_channel.m | 加高斯白噪声,按 Eb/N0 换算噪声功率 |
| demod_decision.m | 硬判决恢复比特 |
| ber_calc.m | 统计误比特数和误比特率 |
| plot_constellation.m | 画接收星座图 |
运行入口只有主脚本,在 Matlab 当前目录下执行:
main_bpsk_qpsk_comparison不需要手动 addpath,除非你把文件放在子文件夹里。如果报“未定义函数”,优先检查当前目录是否切换到了代码目录,而不是函数名拼错。我拿到这个资源时会先打开主脚本,看它调用了哪些函数,再对照文件列表确认一个不差。如果某个函数缺失,Matlab 会在命令行直接提示红色错误,不用猜。
3.2 发送端代码:随机比特、调制映射、加噪声
主脚本里前几行通常是设置仿真参数。我抽取一个常见版本的核心段:
% 参数设置 N_bits = 1e6; % 总比特数,越大曲线越平滑,但仿真越慢 EbN0_dB = 0:2:12; % 每比特信噪比(dB),横轴从 0 到 12 dB N_points = length(EbN0_dB); rng(42); % 固定随机数种子,让每次跑图一致 data = randi([0 1], N_bits, 1); % 生成随机比特流 % BPSK 调制:0 -> -1, 1 -> +1 tx_bpsk = 2*data - 1; % QPSK 调制:每两比特映射一个复数符号 % 先把比特流重排为两列,第一列 I,第二列 Q data_matrix = reshape(data, 2, N_bits/2).'; b1 = data_matrix(:,1); b2 = data_matrix(:,2); tx_qpsk = ((2*b1 - 1) + 1j*(2*b2 - 1))/sqrt(2);逻辑说明:2*data - 1是 BPSK 调制的矩阵写法,把 0 变成 -1、1 变成 +1,符号功率归一化为 1。QPSK 里b1映射到实部、b2映射到虚部,除sqrt(2)是为了让四个星座点的模长保持为 1,这样每个符号的平均能量同样是 1。很多新手漏掉这个归一化,导致后面信噪比计算全偏。
加噪声部分经典写法如下:
% 加噪函数:根据 EbN0_dB 生成对应方差的高斯噪声 % 这里统一用复数高斯噪声,BPSK 只取实部也一样 Es = 1; % 符号能量,因为归一化了 M = 2; % BPSK 的 M=2;QPSK 的 M=4 Eb = Es / log2(M); % 每比特能量 N0 = Es / (10^(EbN0_dB(k)/10) * log2(M)); % 噪声功率谱密度 noise_power = N0 / 2; % 每个维度分到一半功率 noise = sqrt(noise_power) * (randn(size(tx)) + 1j*randn(size(tx))); rx = tx + noise;参数说明:这里的k是循环遍历信噪比点的循环变量。关键在N0的换算公式:因为输入是每比特信噪比,而发送符号是每符号能量,两者差一个log2(M)因子。BPSK 的 M=2,所以log2(M)=1;QPSK 的 M=4,所以log2(M)=2。如果你直接把 BPSK 的噪声公式套给 QPSK,高信噪比下 QPSK 的误码率会比理论低约 0.5 dB。这个坑我至少在三个同学的代码里见过。
3.3 接收端与判决
硬判决是这套代码里最朴素也最稳定的解调方式,不需要任何信道估计,只做符号级判决:
% BPSK 硬判决:实部大于 0 判为 1,否则判为 0 rx_bpsk_I = real(rx_bpsk); rx_bpsk_bits = double(rx_bpsk_I > 0); % QPSK 硬判决:实部和虚部分别判决 rx_qpsk_I = real(rx_qpsk); rx_qpsk_Q = imag(rx_qpsk); dec1 = double(rx_qpsk_I > 0); % I 路对应发送端的 b1 dec2 = double(rx_qpsk_Q > 0); % Q 路对应发送端的 b2 % 将两路判决结果恢复成一串比特 dec_bits = reshape([dec1, dec2].', [], 1);这里的判决门限是 0,因为星座点关于原点对称。如果发射端引入了相位偏移,比如把星座图旋转了 30°,这个门限就直接失效,你会看到大量误码。代码包里没有做载波同步,所以如果你加了频偏,请先自行补一个相位估计。
reshape([dec1, dec2].', [], 1)这行的意思是:先把 dec1 和 dec2 并排成 N 行 2 列矩阵,转置后变成 2 行 N 列,再 reshape 成列向量。这样得到的比特顺序与发送端的data原始顺序完全一致。如果发送端用的是[b1 b2]矩阵而不是交替组合,这里必须对应,否则统计误码率时逐位比较必然错位。
3.4 如何调整 SNR 范围和仿真次数
仿真次数直接影响曲线质量和运行时间。我的经验值如下:
| 目标 | N_bits | EbN0_dB 范围 | 运行参考时间 |
|---|---|---|---|
| 课设报告出图 | 1e5 | 0:2:12 | 约 10 秒 |
| 需要看到 1e-5 以下 | 1e6 | 0:1:10 | 约 1 分钟 |
| 严谨对比曲线 | 1e6 以上 | -2:1:12 | 约 3 分钟 |
高 SNR 时若某点的错误数少于 10 个,该点的误码率数据不可信。代码里可以加一个最小错误数判断:
if errors(k) > 0 && errors(k) < 10 warning('SNR=%d dB 处错误数只有 %d,结果仅供参考', EbN0_dB(k), errors(k)); end这个判断不会让结果变准,但能提醒你别把偶然的 0 误码当成“完美解调”。如果你在写课程报告,建议把N_bits设为 1e6 直接跑一遍,曲线平滑度足够放进论文。
4. 仿真实战:跑通脚本并看懂误码率曲线和星座图
4.1 跑通主脚本的步骤与预期输出
打开 Matlab,把当前目录切到解压后的文件夹,在命令行输入:
run('main_bpsk_qpsk_comparison.m')或者直接打开脚本点 F5。通常 1 分钟内会看到两个 figure:第一个是误码率曲线,横轴 Eb/N0(dB)、纵轴 BER,坐标用对数刻度;第二个是最后一组信噪比下的接收星座图。正常的 BPSK 星座图是两个实轴上的点云,QPSK 是四个象限的中心点簇。若星座图呈现环状或整体旋转,多半是归一化或相位问题,具体排查见第 5 章。
主脚本运行时会在命令行打印过程信息,比如“Simulating at EbN0 = 6 dB ...”,这个输出不是乱码,是循环进入每个信噪比点的进度提示。如果你用的是旧版 Matlab,打印频率太高会拖慢速度,可以改成每 3 个点打印一次。
4.2 读懂误码率曲线:AWGN 下的理论对照
误码率曲线是整个比较代码的核心输出。你会看到 BPSK 和 QPSK 两条仿真曲线几乎重合在一条理论曲线上,这就是前面第 2 章说的结论:在 AWGN 信道、格雷映射、相干解调的前提下,两者的误比特率一致。曲线从 Eb/N0 = 0 dB 时的约 0.08 开始下降,每增加 3 dB,误码率大约下降一个数量级。到 12 dB 时,理论上误码率已经在 1e-7 量级,如果 N_bits 只有 1e5,这里可能显示为 0。
绘图代码里常见的是 semilogy 加 loglog 的切换:
figure; semilogy(EbN0_dB, ber_bpsk, 'b-o', 'LineWidth', 1.5); hold on; semilogy(EbN0_dB, ber_qpsk, 'r-s', 'LineWidth', 1.5); semilogy(EbN0_dB, ber_theory, 'k--', 'LineWidth', 1.5); xlabel('Eb/N0 (dB)'); ylabel('BER'); legend('BPSK 仿真', 'QPSK 仿真', '理论值'); grid on;注意semilogy的纵轴是对数刻度,所以纵坐标数值 1e-6 显示为 -6 的指数,不要把观察重点放在小抖动上。要判断仿真是否可信,先看低信噪比区域(0~4 dB)是否与理论曲线贴合,再看高信噪比区域的抖动是否在合理范围。只有两三个点紧贴理论不算数,整条曲线都要贴合才算通过。
4.3 星座图分析:旋转相位和幅度归一化
星座图是直观理解调制质量的工具。代码包里的绘制脚本通常这样写:
figure; subplot(1,2,1); plot(real(rx_bpsk), imag(rx_bpsk), '.', 'MarkerSize', 6); axis equal; axis([-3 3 -3 3]); title('BPSK 接收星座图'); subplot(1,2,2); plot(real(rx_qpsk), imag(rx_qpsk), '.', 'MarkerSize', 6); axis equal; axis([-2 2 -2 2]); title('QPSK 接收星座图');这里的real和imag分别取复基带信号的实部和虚部。点云的扩散半径取决于噪声功率,信噪比越高,点越聚拢。如果发现 BPSK 的星座点不是对称分布在 ±1 中心,而是整体偏移,说明发送端映射有直流偏置或者噪声均值不为零。如果 QPSK 的四个点簇重心不在 (±0.707, ±0.707),而是像旋转了 45 度,多半是漏了除sqrt(2)。还有个细节:axis equal是必须的,否则横纵轴比例不一致会把圆形点云压成椭圆,干扰视觉判断。这个坑我不少见,很多人画星座图时没加这句,结果点云看起来像发生频率偏移。
4.4 对比表的三种写法
跑完仿真后,你需要把数据整理成可写进报告的形式。最直接的方式是在脚本末尾用 fprintf 打印表格:
fprintf('Eb/N0(dB) BPSK_BER QPSK_BER Theory_BER\n'); for k = 1:N_points fprintf('%10.1f %10.4e %10.4e %10.4e\n', ... EbN0_dB(k), ber_bpsk(k), ber_qpsk(k), ber_theory(k)); end第二种是把结果存成 .mat 文件,后续用 Excel 或 Python 绘制更精细的图:
save('ber_results.mat', 'EbN0_dB', 'ber_bpsk', 'ber_qpsk', 'ber_theory');第三种是直接在脚本里生成统计摘要,比如比较在 BER=1e-4 时 BPSK 和 QPSK 需要多少 dB,这需要用到插值。注意,interp1的默认方法是线性插值,对对数坐标下的 BER 会失真,建议先对 BER 取对数再插值,或者用interp1(..., 'log')不存在的,你需要手动10.^(interp1(log10(ber), ...))。这就是为什么我一般只打印原始曲线,需要具体门限值时再单独算。
5. 避坑与常见问题:五个让仿真结果翻车的细节
5.1 误码率曲线低 SNR 段偏离理论值
现象:在 0~4 dB 区间,仿真误码率明显高于或低于理论曲线,但高信噪比段又慢慢靠拢。
原因:噪声方差公式错了,最常见的是把发送符号能量当成单位量,却忘了按点数除,或者 QPSK 的log2(M)因子漏掉。代码里表现为N0 = 10.^(-EbN0_dB/10)直接做开方乘到噪声上,这种写法在 BPSK 下碰巧对,因为 BPSK 的 Es=Eb=1;但换到 QPSK 就偏了 0.5 dB。
解决:统一用N0 = Es / (EbN0_linear * log2(M)),并强制要求发送符号能量 Es=1。写完后用sum(abs(tx_bpsk).^2)/length(tx_bpsk)检查符号平均功率,确认是 1。
5.2 QPSK 星座图旋转了 45 度
现象:接收星座图上四个点簇的质心在 (1,0)、(0,1)、(-1,0)、(0,-1),而不是 (±0.707, ±0.707) 的四象限中心。
原因:调制映射时没有除sqrt(2),符号能量变成 2,星座点从 45° 角的斜线变成了轴上点,看起来像旋转。这个错误不影响硬判决门限 0 的判断,所以误码率曲线可能照样正确,但星座图明显难看,报告里一放就露馅。
解决:确认tx_qpsk = ((2*b1-1) + 1j*(2*b2-1))/sqrt(2);。同时检查加噪时是否用了复数噪声,如果只用实噪声加在实部,虚部没噪声,星座图会在纵向上收缩成一条线。
5.3 高 SNR 时误码率突然跳变为 0
现象:曲线在 8 dB 之后直接为 0,看起来系统性能好得离谱。
原因:仿真比特数太少,比如N_bits = 1e4。在 BER=1e-5 的信噪比下,1e4 个比特里平均只有 0.1 个错误,没有错误事件导致 BER=0,而 0 在对数坐标上无法显示,曲线就掉出图外。
解决:要么把 N_bits 加到 1e6 以上,要么采用“错误数达到 100 就提前停止”的循环机制。我习惯用前者,因为简单且结果稳定。如果机器性能不够,可以减小最大信噪比的仿真点,而不是减少比特数。
5.4 矩阵维度对不上,报错“每个变量至少有 n 个元素”
现象:运行到加噪或误码统计时报矩阵尺寸不一致,命令行提示左右两边的元素个数不同。
原因:QPSK 调制后符号数是比特数的一半,但噪声数组长度还按比特数生成。有的人用randn(N_bits,1)生成噪声,然后直接加到tx_qpsk上,维数不匹配;有的人虽然改了噪声长度,但误码统计时没意识到待判决的比特数是符号数的两倍。
解决:建议在脚本里明确区分两个量:
N_sym = N_bits / log2(M); % 符号数 noise = sqrt(N0/2) * (randn(N_sym,1) + 1j*randn(N_sym,1));接收端判决后得到的比特数一定是2*N_sym,即原始的N_bits,如果统计时发现位数不对,先查这句。
5.5 误码率曲线看似重合但整体偏左或偏右
现象:BPSK 和 QPSK 仿真曲线互相重合,但相对理论曲线整体平移了约 0.5 dB 或 1 dB。
原因:信噪比定义混乱。如果你在代码里用的是符号信噪比 Es/N0,却把横轴标成 Eb/N0,那么 QPSK 的曲线会比理论偏左 3 dB(因为 Es=2Eb)。另一种情况是理论曲线用了单边噪声谱密度,而实际噪声功率谱设成了 N0 而不是 N0/2。
解决:先明确你在对比什么。横轴写 Eb/N0 时,噪声公式里必须出现log2(M)因子;横轴写 Es/N0 时,QPSK 的仿真曲线和 BPSK 在相同横轴下不再重合,而是 QPSK 在低信噪比区域误码率更低(因为它有更多比特能量摊分)。我每次拿到别人的代码,第一件事就是看横轴变量名和噪声计算式的对应关系,这个弄错,后面所有结论都是白搭。
6. 验证与进阶:给你的代码包加上三个可信度检查
6.1 用眼图观察带限信道的 ISI
基带仿真的误码率曲线只验证了 AWGN 下的性能,实际通信系统还要过脉冲成形滤波器。你可以在现有链路上加一个升余弦滤波器:
rolloff = 0.3; span = 6; rrcFilter = rcosdesign(rolloff, span, sps, 'sqrt'); tx_filtered = upfirdn(tx_bpsk, rrcFilter, sps, 1);加完之后,接收端要加匹配滤波器再抽样,否则误码率会急剧恶化。眼图可以用eyediagram命令绘制,它能看到符号间干扰的程度。如果眼图“眼睛”睁开得越小,说明 ISI 越严重。这个检查在代码包里没有,但你自己加上后,会把方案比较从“单纯看 BER”升级到“看系统抗 ISI 能力”,这是课设拿高分的常见手段。
6.2 把单次仿真改成蒙特卡洛循环
单次仿真的 BER 在高信噪比区域抖动较大,为了获得稳定结果,可以重复多次取平均:
num_trials = 5; ber_sum = 0; for trial = 1:num_trials rng(100*trial); % 每次用不同随机种子 [ber_temp] = run_one_ber_sim(N_bits, EbN0_dB(k)); ber_sum = ber_sum + ber_temp; end ber_ave = ber_sum / num_trials;这个做法的代价是运行时间乘以 num_trials,所以我只在关键信噪比点(比如 BER 接近 1e-5 的最后一个点)做重复,其他点保持单次。你还可以把循环改成parfor,但要注意随机数种子管理,否则并行时可能生成相同的随机序列,导致平均结果失真。
6.3 扩展对比:加卷积编码后的编码增益
如果你手头有 Communications Toolbox,可以把同样的 BPSK/QPSK 链路接上卷积编码,观察编码增益。常见做法是:
code_rate = 1/2; trellis = poly2trellis(7, [171 133]); data_encoded = convenc(data, trellis); ber_coded = run_with_channel(data_encoded, EbN0_dB);注意这时横轴应该用编码后的信息比特信噪比,而不是编码符号的信噪比,否则编码增益会被高估。这个扩展能回答“为什么实际系统宁愿牺牲带宽也要加编码”这个问题,是从仿真到工程认知的重要一步。我在给研究生做课程代码审查时,最基本的要求就是能解释清楚编码后曲线左移的那 3~5 dB 是从哪来的。
从那以后,我每次拿到任何调制类代码,都先花两分钟检查符号能量、噪声方差和横轴定义这三处,再去看仿真结果。这个习惯帮我在课设和项目里少走了很多弯路,希望帮到你。
本文还有配套的精品资源,点击获取