简介在MATLAB平台下这套QPSK误码率随信噪比变化的仿真程序源码面向通信方向的新手及有一定开发经验的工程人员。源码主体是一个完整的M脚本文件从随机比特序列生成开始依次完成QPSK调制映射、噪声叠加、接收端判决、误码计数与曲线绘制注释详细结构清晰便于读者对照学习仿真的每一个环节。另外附有一份说明文档对代码中的关键变量和运行流程进行补充说明能帮助使用者快速上手并调整信噪比范围、每个信噪比下的比特数等参数用于不同条件下的对比实验。压缩包共2个文件包括1个M脚本文件和1份Word说明文档整体大小仅13KB下载和运行都非常方便目前已有854人浏览学习。适合作为课程设计、毕业设计或通信原理实验的参考通过本次仿真可以直观看出信道质量对误码率的影响掌握蒙特卡洛仿真的基本思路后续还可将框架推广到其他数字调制方式的误码率分析中。1. QPSK误码率仿真理论曲线和蒙特卡洛曲线一次性对上做通信课程设计也好写毕业论文也好在MATLAB里计算QPSK误码率随信噪比变化的曲线第一关往往不在算法本身而在把理论和仿真这两条线压到同一张图上。QPSK的理论误码率公式确实很简单但自己写蒙特卡洛仿真时曲线要么整体偏移3dB要么高信噪比处抖动到没法看甚至中途冒出零误码让log坐标直接崩掉。这份源码把整条链路完整串了起来随机比特生成、串并转换、星座映射、加高斯白噪声、硬判决、误码统计到最后和理论曲线叠加绘图。适合通信方向做课程设计、毕设仿真以及刚把MATLAB基础过完、想拿一个完整链路练手的人直接对照着改。2. 误码率与信噪比先把EbN0和SNR的关系理清再看曲线2.1 为什么QPSK一个符号里塞了2个比特QPSK的全称是Quadrature Phase Shift Keying正交相移键控。它把数据分成I、Q两路每路取一个比特两路组合出四种相位状态每个符号对应2个比特。4个星座点均匀分布在复平面单位圆上相邻点之间的相位差是90度。这样做的直接收益是频谱效率翻倍同样的符号速率下比特速率是BPSK的两倍代价是星座点变密同样的噪声功率下误码率比BPSK略高。格雷映射是QPSK实现里默认的主流选择。相邻星座点只差1个比特比如00、01、11、10按顺序转一圈从任何一点错到相邻点只错1位而不是2位。这一点直接影响理论误码率和仿真里误比特率的统计口径后文避坑章节会专门展开。源码里映射写成2*bitsI - 1就是把0映射成-1、1映射成1再组合成复信号symI 1j*symQ默认走的就是格雷映射的路子。2.2 理论误码率公式qfunc和3dB到底差在哪QPSK的理论误比特率在高斯白噪声信道下有一个近似表达式BER Q(sqrt(2 * EbN0))其中EbN0是每比特能量与噪声功率谱密度之比注意这不是dB值。只要用格雷映射相邻符号错1个比特这个近似在常规信噪比范围内和精确值贴合得很好。MATLAB里算理论曲线只需要几行代码这份源码里也是用这几行作为比对基准。自己复现时先把这个跑通后面仿真曲线才有对照物EbN0_dB 0:1:10; % 扫描信噪比范围单位dB EbN0 10.^(EbN0_dB/10); % 转成线性值 ber_theory qfunc(sqrt(2 * EbN0)); % 理论误比特率 semilogy(EbN0_dB, ber_theory, k-, LineWidth, 1.2); grid on; xlabel(Eb/N0 (dB)); ylabel(Bit Error Rate);这里的核心参数是EbN0_dB它决定了横轴范围和曲线位置。dB转线性用10.^(EbN0_dB/10)这是所有信噪比相关计算的地基很多后续偏差就是在这里埋下的。qfunc是MATLAB自带的高斯Q函数输入线性值输出概率值不需要自己写积分。真正容易翻车的是EbN0和SNR的换算。QPSK一个符号带2个比特符号能量和比特能量的关系是Es 2 * Eb换算到dB域就是SNR_dB EbN0_dB 3dB。如果直接把EbN0_dB当成信噪比塞进awgn函数或者反过来用错了整条仿真曲线会整体平移3dB这是QPSK误码率仿真里出现频率最高的错误没有之一。记住这个关系后面第三、四章的所有代码都建立在它之上。3. 仿真链路拆解从随机比特到误码统计的四段核心代码3.1 发端随机比特、串并转换与格雷映射发端做三件事生成随机比特、做串并转换、映射成复基带符号。这份源码里每个信噪比点都重新生成比特序列而不是把所有信噪比点共用同一批数据这样能保证各点统计独立。具体代码可以单独拎出来看% 参数设置 Nbits 1e6; % 总比特数越大曲线越平滑 M 4; % QPSK调制阶数 k log2(M); % 每符号比特数 2 % 生成随机0/1比特序列 bitsTx randi([0, 1], 1, Nbits); % 串并转换奇数位进I路偶数位进Q路 bitsI bitsTx(1:2:end); bitsQ bitsTx(2:2:end); % 格雷映射0 - -11 - 1 symI 2 * bitsI - 1; symQ 2 * bitsQ - 1; % 组成复基带符号 symTx symI 1j * symQ;Nbits决定了后续误码率统计的样本量。我一般会按目标误码率来倒推想要在1e-4这个量级上有可靠统计最少要有100个误码做支撑这就意味着至少需要1e6个比特。如果把randi生成的比特在循环内反复利用各信噪比点的样本会强相关曲线会出现诡异的毛刺。所以建议每个信噪比点独立生成比特或者在循环外一次性生成一个大数组、按帧切分使用。串并转换的写法决定了判决后并串转换怎么恢复。奇数位进I路、偶数位进Q路是这份源码的约定如果你改成前一半进I路、后一半进Q路那么收端的并串转换也必须同步修改否则误码率统计会乱七八糟。映射部分2*bits - 1生成的是幅度为±1的实部虚部这样符号能量天然为2对应Es 2Eb和相位调制理论模型一致。3.2 信道按EbN0给复信号加高斯白噪声加噪声是整条链路里最容易出偏差的一步。AWGN信道下复基带信号每个维度实部、虚部的噪声方差是N0/2总噪声方差是N0。所以给symTx加噪声时噪声必须写成sqrt(N0/2) * (randn j*randn)而不是sqrt(N0) * randn单独加在实部上。这份源码里加噪声的逻辑封装成了一段独立代码它的参数核心在于EbN0到N0的换算。由于前面映射得到的符号能量Es 2而Es 2Eb所以Eb 1于是N0 1 / EbN0_lin。如果以后改用16QAM或者8PSKEs会变这里的换算也要跟着改这是最容易忽略的坑。function symRx add_awgn(symTx, EbN0_dB) % EbN0_dB: 每比特信噪比单位dB EbN0_lin 10^(EbN0_dB / 10); % 转线性值 N0 1 / EbN0_lin; % 归一化Eb1所以N01/EbN0 noiseVarPerDim N0 / 2; % 每维噪声方差 noise sqrt(noiseVarPerDim) * (randn(size(symTx)) 1j * randn(size(symTx))); symRx symTx noise; endrandn(size(symTx))生成了与符号等长的标准正态分布随机序列实部和虚部各一份分别乘上sqrt(noiseVarPerDim)。之所以每维用N0/2是因为复噪声的总功率等于实部虚部功率之和总功率是N0。如果在这里用了sqrt(N0)乘在实部虚部上实际加进去的噪声功率翻倍高信噪比处的误码率会对不上理论值这属于第五章节里讲的高信噪比上翘问题。EbN0_dB这一项传入的是每比特信噪比而不是符号信噪比。很多人习惯直接把EbN0_dB传给awgn函数但awgn的SNR参数针对的是信号功率需要先把EbN0换算成符号信噪比也就是EbN0_dB 10*log10(k)。这份源码没有用awgn而是在时域自己控制噪声功率好处是参数流向完全透明反过来也提醒大家用自带函数前先把换算关系算清楚。3.3 收端硬判决、并串转换与误码统计QPSK接收端用硬判决就能满足一般教学和验证需求不需要走软解调。判决规则很简单实部大于0判为0、小于0判为1虚部同理。因为发射端映射是0到-1、1到1所以接收端用0作为门限等价于最近邻判决。% 硬判决实部虚部分别和0比较 bitsRxI real(symRx) 0; bitsRxQ imag(symRx) 0; % 并串转换按发射端的顺序恢复原始比特流 bitsRx zeros(1, Nbits); bitsRx(1:2:end) bitsRxI; bitsRx(2:2:end) bitsRxQ; % 误码统计 nErr sum(bitsRx ~ bitsTx); ber nErr / Nbits;real(symRx) 0返回逻辑数组1代表判决为1、0代表判决为-1但这里注意逻辑值1对应的是比特1还是比特0取决于发射端映射约定。发射端bits1映射到1而sy mRx 0判出来为逻辑1恰好在数值上和原比特一致因此直接比较bitsRx ~ bitsTx没问题。如果哪天改了映射约定这里的判决也要跟着调。误码统计用sum(bitsRx ~ bitsTx)比sum(bitsRx - bitsTx ~ 0)更稳妥因为逻辑数组求和就是误码个数。最后nErr / Nbits得到的就是该信噪比点下的误比特率估计值。注意这里统计的是误比特率而不是误符号率如果统计对象换成符号就要先比较symRx判决后的符号和symTx符号是否一致再决定是否乘上格雷映射的比特折算系数。4. 主循环与参数设置蒙特卡洛次数、扫描范围和曲线复现4.1 主循环结构逐点扫描并累计误码前文的发端、信道、收端三段代码凑齐后主循环只是把这些函数按信噪比点串起来。这份源码的主循环思路是逐点扫描每个EbN0点独立生成比特、独立加噪、独立统计误码最后把误码率数组一次性画出来。结构如下EbN0_dB 0:1:8; % 横轴扫描范围 Nbits 5e5; % 每个信噪比点的仿真比特数 ber_sim zeros(size(EbN0_dB)); for idx 1:length(EbN0_dB) % 发端独立生成随机比特 bitsTx randi([0, 1], 1, Nbits); bitsI bitsTx(1:2:end); bitsQ bitsTx(2:2:end); symTx (2*bitsI - 1) 1j * (2*bitsQ - 1); % 信道加高斯白噪声 symRx add_awgn(symTx, EbN0_dB(idx)); % 收端硬判决 并串转换 误码统计 bitsRxI real(symRx) 0; bitsRxQ imag(symRx) 0; bitsRx zeros(1, Nbits); bitsRx(1:2:end) bitsRxI; bitsRx(2:2:end) bitsRxQ; nErr sum(bitsRx ~ bitsTx); ber_sim(idx) nErr / Nbits; endfor循环里的三个环节和第三章的分段代码完全对应只是把每一步都封装成了循环体内的连续操作。每个信噪比点都重新调用randi这一点保证了统计独立性。如果你把bitsTx的生成挪到循环外所有信噪比点用同一批比特虽然跑起来更快但曲线会带上相关性高信噪比点和低信噪比点的起伏会被人为拉平。循环变量idx对应的EbN0_dB(idx)直接传入add_awgn。这里有个隐含约定add_awgn内部已经把EbN0换算成了噪声功率所以循环里不需要再做任何dB转线性的操作所有换算集中在一处。以后如果想把扫描范围改成-2到12dB只需要改第一行数组。4.2 蒙特卡洛次数怎么定最少误码数与置信度蒙特卡洛仿真最容易被低估的是样本量需求。很多人习惯固定Nbits 1e5跑完全部信噪比点结果低信噪比处曲线还算凑合一到高信噪比就崩。原因是固定比特数下误码数随信噪比指数级下降高信噪比点可能一个误码都没有误码率算出来是0log坐标直接画不出来。我一般按“最少误码数”来倒推样本量。经验法则是每个信噪比点至少收集100个误码误码率才有工程意义上的可信度。BER1e-2时100个误码只需要1e4个比特但BER1e-5时需要1e7个比特。固定Nbits 5e5只够把BER测到约2e-4的量级再往下就是无效估计。可以改成动态循环直到误码数达标或达到帧数上限minErr 100; % 最少误码数 maxBits 5e6; % 单点的最大仿真比特数 nErr 0; totalBits 0; while nErr minErr totalBits maxBits bitsTx randi([0, 1], 1, Nbits); % ... 调制、加噪、解调判决 ... nErr nErr sum(bitsRx ~ bitsTx); totalBits totalBits Nbits; end ber nErr / totalBits;这个结构的优点是把“仿真精度”和“仿真时长”变成两个显式参数。minErr控制统计置信度maxBits防止BER太低时无限循环跑死机器。实际使用时还可以加一个进度显示每累计100次打印当前误码数和总比特数这样能看到仿真是不是卡住了。参数名推荐取值说明minErr100每信噪比点最少误码数100是经验值maxBits5e6每个点最大比特数防止高信噪比死循环Nbits1e5~1e6每次循环生成比特数影响单次迭代耗时EbN0_dB扫描范围0:1:8根据目标误码率量级灵活调整参数表里的EbN0_dB扫描范围如果改成0:0.5:10曲线会采样更密但每点多跑一次完整链路总耗时约翻倍。我通常在确认链路正确后用稀疏扫描汇报结果前再加密一版。4.3 绘图叠加理论曲线一眼确认仿真是否正常仿真跑完最后一步是把理论曲线和仿真曲线画在同一张图上用semilogy而不是plot因为误码率跨越多个数量级线性坐标下低误码率段会被压缩成一条贴地的线。绘图代码一般在主程序末尾figure; semilogy(EbN0_dB, ber_theory, k-, LineWidth, 1.2); hold on; semilogy(EbN0_dB, ber_sim, ro, MarkerSize, 6, MarkerFaceColor, r); grid on; xlabel(Eb/N0 (dB)); ylabel(Bit Error Rate); legend(理论值, 仿真值, Location, southwest); set(gca, YScale, log);ber_theory来自第二章的理论计算ber_sim来自主循环的统计结果。两者叠加后的判断标准要明确低信噪比处仿真点贴着理论线走高信噪比处仿真点围绕理论线上下小幅波动这就算验证通过。如果仿真点整体偏了3dB大概率是EbN0和SNR换算错了如果低信噪比处抖动剧烈是样本量不足如果高信噪比处下不去是噪声功率加倍。这些偏差在下一章逐条展开。set(gca, YScale, log)和直接用semilogy是两种等价写法。semilogy只不过是把数据轴属性预设好实际渲染效果一致。还有一个细节当BER为0时semilogy会弃点不画图像上表现为某个信噪比点之后曲线突然断掉这正好提醒我们该点的误码数太少了。5. 避坑QPSK误码率仿真里最常见的四处偏差源头5.1 现象一仿真曲线比理论曲线整体右移3dB这个偏差非常典型仿真点和理论点完全平行但仿真曲线整体往右挪了3dB。原因是把符号信噪比EsN0当成了每比特信噪比EbN0使用。QPSK每个符号带2个比特符号能量是比特能量的2倍所以在dB域两者差10*log10(2)≈3dB。加噪声时如果直接用awgn(symTx, EbN0_dB)而awgn默认处理的SNR是符号信噪比就相当于多给了信号3dB的功率仿真曲线自然右移。解决方法是统一使用EbN0作为自变量加噪前先换算成符号信噪比SNR_dB EbN0_dB 10*log10(k)k是每符号比特数。这份源码选择自己写加噪函数直接在函数内部完成换算能从根源上规避awgn带来的口径问题。5.2 现象二低信噪比点抖动剧烈高信噪比点出现零误码低信噪比处误码率高低起伏大高信噪比处BER直接为0、曲线断线这两个症状其实同源样本量不足。低信噪比处虽然误码多但每个单独符号的错误是随机事件帧数少了就出现明显的统计涨落高信噪比处误码是稀有事件固定帧长跑完可能一个误码都没遇到。解决方法是把固定比特数改成动态循环按最少误码数来终止。每个信噪比点累计到100个误码以后再算BER如果累计比特数超过上限还没到100个误码就用当前累计值计算同时打个标记不要画进图里。100误码的经验值可以按需调整想画画更光滑可以提到200但仿真时间也会线性增长。5.3 现象三高信噪比处误码率下不去甚至出现上翘高信噪比段的误码率理论上应该单调下降但有时跑到1e-4时开始降不动甚至往回翘。最大嫌疑是加噪声时每维方差用错只生成了实部噪声没生成虚部噪声或者实部虚部各加了N0而不是N0/2。复基带信号的噪声是二维的总噪声功率N0要分摊到实部虚部两个维度。正确写法是sqrt(N0/2) * (randn 1j*randn)。如果写成了sqrt(N0) * randn只加实部等价于噪声总功率减半高信噪比处信号被噪声污染的程度不符实际曲线就会比理论低一截。反过来如果实部和虚部各乘sqrt(N0)噪声总功率翻倍曲线高信噪比处下不去。这两种都表现为高信噪比段系统性偏离理论线。5.4 现象四仿真结果系统性高于理论但形状看起来合理仿真曲线贴着理论线走但整体高出半个数量级乍一看像统计噪声细看每个点都偏。这个情况大概率是映射方式选错了用的是自然码映射相邻星座点之间差2个比特符号错误时平均错2个比特。理论公式的Q(sqrt(2*EbN0))基于格雷映射假设实现时却用了自然码误比特率自然比理论高。解决方法是发端改用格雷映射在将比特映射成符号前先做格雷编码转换。如果一定要用自然码理论公式要替换成精确的误比特率表达式不能再用近似式。判断当前是哪种映射很简单画出星座图看相邻点之间是差1位还是差2位。验证时不放心的话直接把收端误码数先分解成误符号数统计误符号率和理论Ps2*Q(sqrt(2*EbN0))对比能更快隔离问题。6. 进阶验证星座图、眼图与半解析法怎么配合主程序6.1 星座图验证四团点是否中心对称方差是否均匀跑完误码率曲线只说明统计结果对得上不代表链路内部每阶段都对。我习惯在仿真主程序里顺手加一个星座图检查选一个中等信噪比点比如6dB把接收符号画在复平面上。这个点信噪比既够高让四团点分得开又够低能看到噪声散布。EbN0_check 6; % 星座图检查点 symRx add_awgn(symTx, EbN0_check); figure; scatter(real(symRx), imag(symRx), 6, filled, MarkerFaceAlpha, 0.4); hold on; scatter(real(symTx), imag(symTx), 60, rx, LineWidth, 1.5); axis equal; grid on; xlabel(同相分量 I); ylabel(正交分量 Q);检查要点是看四团接收点是否分别以发射星座点为中心散布是否近似圆形均匀分布。如果四团点中心位置整体旋转了一个角度说明本振相位不同步如果某一团点比其它团点更散说明I/Q两路增益不平衡。星座图正常是后续所有验证的前提。这份代码里MarkerFaceAlpha控制点的透明度点数多时能看清密度分布比纯色点直观得多。注意加axis equal否则横纵轴比例不一致会把圆形散布拉成椭圆。6.2 眼图验证与半解析法两条加速验证的路眼图可以直观看出判决裕量。QPSK是二维调制通常分别看I路和Q路的眼图从接收信号里取实部按符号周期切片把每段叠加画在一起。眼图张开得越宽判决容错越好。配合眼图还有一个常见做法把接收序列通过匹配滤波器再抽样眼图会更干净。当BER目标低于1e-5时全蒙特卡洛仿真会非常慢我一般改用半解析法验证。核心思路是只对部分环节做蒙特卡洛仿真其余环节用理论公式折现。比如先仿真出符号信噪比映射关系再用Q函数直接折算误码率。对QPSK来说常见做法是只仿真信道噪声方差然后把误码率用公式闭合求解生成N个符号映射成QPSK星座点只加噪声统计接收符号的星座点散布方差用该方差推算等效EbN0代入qfunc(sqrt(2*EbN0))得到BER在高信噪比段和全蒙特卡洛结果对比偏差控制在10%以内即视为链路正确。验证方法适用信噪比范围耗时量级反馈信息全蒙特卡洛0~8dB分钟级逐点BER曲线星座图检查任意单点秒级相位、增益、噪声方差半解析法高信噪比段秒级快速估算BER量级半解析法不适合做最终汇报图因为它本质上跳过了大量随机样本统计可信度不如真蒙特卡洛但作为验证手段非常高效。我通常会先跑半解析确认量级不偏再决定全仿真要跑多少比特数避免一上来就砸几百万比特却跑出一个错误结果。从那以后我每次写QPSK误码率仿真都强制走一遍“星座图低信噪比对比理论线半解析抽查”这三道关卡再回头看BER曲线基本不会翻车。这套源码也是一样的节奏拿去对照着跑一遍比自己从零开始查文档快得多希望帮到你。本文还有配套的精品资源点击获取