简介:这是一份面向通信方向初学者与有一定开发经验的工程师的Matlab源码包,用于计算QPSK调制下误码率随信噪比变化的仿真,运行即可直接得到误码率-信噪比波形,免去自行搭建仿真环境的麻烦。包内共2个文件:一个.m脚本完整实现信号生成、加噪、解调与误码率统计,代码注释详尽,方便逐行阅读;另一个docx文档给出了算法补充说明,可作为扩展学习资料。压缩包整体仅13KB,轻量精简,尤其适合快速下载学习,也可用于课程设计或毕业设计中的通信仿真部分。目前已有854人学习浏览,经过达摩老生亲测校正,质量可靠,值得信任。通过这份资源,读者既能获得可直接运行的QPSK误码率计算程序,也能从注释与文档中理解仿真设计的完整思路,掌握误码率性能分析方法,减少自主摸索的时间成本。
1. 拿到“matlab计算QPSK误码率随信噪比变化的程序源码”时,先想清楚这三件事
做QPSK误码率随信噪比变化的仿真,是通信方向学生和工程师绕不开的一道基础题。你手里的这个matlab程序源码,目标不外乎是画出一条误码率曲线,让它从高误码区一路降到低误码区,并且跟理论值贴在一起;但它真正考你的是信噪比怎么在仿真里换算、噪声怎么加、统计多少次才算数。它适合刚开题需要跑通通信仿真流程的在校学生,也适合要做链路预算或评估编码增益的射频工程师。这篇笔记不假装见过某个具体压缩包的内容,而是把这一类程序背后最常见、最可靠的实现方案完整讲透,从调制映射到噪声模型,从主循环到避坑,按你会实际动手的顺序来。
2. QPSK仿真为什么绕不开信噪比换算:Eb/N0与SNR的差异与理论基础
2.1 QPSK调制与星座映射:比特怎么变成I/Q两路符号
QPSK的全称是Quadrature Phase Shift Keying,一次传输两个比特,对应四个相位状态。最常见的星座图是把四个点放在单位圆的四个象限上,实部叫I路,虚部叫Q路,每一路的幅度都是±1/√2,这样每个符号的平均功率刚好是1。做仿真时,第一步永远是把一串随机的 0/1 比特流切成长度为 2 的块,再按映射表把每一块变成一个复数符号。
这个切分和映射过程看起来简单,但新手最容易在“比特顺序”上翻车。matlab 里常见做法是先reshape成两列,再用bi2de把每一行转成十进制索引;这里要特别注意left-msb和right-msb的选择,它决定 01 和 10 映射到哪个符号。我一般会用格雷映射,也就是相邻相位只差一个比特的映射,因为在高信噪比下最常见的错误是误判成相邻符号,格雷映射能让这类错误尽量只引起一个比特错误,实测误码率也更接近理论值。
M = 4; % QPSK 阶数 k = log2(M); % 每符号比特数 = 2 nBits = 100000; % 总仿真比特数 data = randi([0 1], nBits, 1); % 随机二进制信源 bitMatrix = reshape(data, k, [])'; % 每行是一个两比特组 symbolIdx = bi2de(bitMatrix, 'left-msb'); % 比特对 -> 0~3 十进制索引这里reshape把长度为nBits的列向量变成nBits/2行 2 列的矩阵,每一行对应一个 QPSK 符号。bi2de(..., 'left-msb')表示左边的比特是高位,于是 00→0、01→1、10→2、11→3。选left-msb还是right-msb会影响星座图上四点的顺序,但对后续误码率结果没有本质影响,只要你发射和接收用同一套就行;真正的坑在于后续de2bi转回比特时也必须用同一个方向,否则解调出来的比特流会完全错乱,曲线直接飘到天上。
这一层还要注意:不同的 matlab 版本对pskmod的默认映射行为不完全一样,较新版本默认走格雷映射,老版本则可能是自然映射。如果你后续要用手写判决而不是pskdemod,就得保证自己的映射表和调制时用的完全一致,否则会出现“星座点明明长得一模一样,误码率却对不上理论曲线”的怪现象。这个细节在第四章我会专门展开。
2.2 AWGN信道下的噪声模型:Eb/N0与SNR的换算公式
误码率曲线的横坐标到底用 SNR 还是 Eb/N0,是这一类源码里分歧最大的地方。做通信的人习惯用 Eb/N0,因为它把调制阶数的影响剥离开来,对比不同调制方式时更公平;但 matlab 的awgn函数只接受功率比 SNR,所以你必须自己完成换算。QPSK 里每个符号携带 2 个比特,符号功率归一化为 1 时,每比特能量 Eb = Es/k = 1/2。于是:
SNR_linear = EbN0_linear * k SNR_dB = EbN0_dB + 10*log10(k) = EbN0_dB + 3.0103这个 3dB 的偏移量是 QPSK 仿真的标志性数字。如果你把 EbN0_dB 直接当成awgn的 SNR 参数传进去,实测误码率会比理论值偏大约 3dB——也就是曲线整体往左移,看起来你“做出了更好的误码率曲线”,实际上只是噪声加少了。反过来,如果有人拿 BPSK(k=1,没有这 3dB 偏移)的代码直接改成 QPSK,不改换算,就会得到完全错误的绝对信噪比指标。判断一段代码是否靠谱,第一眼就应该看它有没有做这个+10*log10(k)的补偿。
EbN0_dB = 6; % 目标每比特信噪比 snr_dB = EbN0_dB + 10*log10(k); % 换算成符号信噪比 rxSig = awgn(txSig, snr_dB, 'measured');awgn的第三个参数'measured'表示先实测信号功率再加噪声,这能避免因滤波器或归一化残留导致的功率误差;如果你的调制信号已经严格归一化,也可以省略这个参数,直接按默认的 0dB 信号功率加噪。但要注意:awgn的 SNR 定义是信号功率与噪声功率之比,不是信号幅度与噪声幅度之比,所以当你手写噪声时,每一维噪声功率应该是 N0/2,而不是 N0。这个“2”是第四章第一个大坑的来源。
2.3 理论误码率公式:用erfc把Q函数落到matlab里
有了星座映射和噪声模型,接下来需要一条理论曲线作为基准。QPSK 在 AWGN 信道下、采用格雷映射时,误码率理论值可以写成:
BER = 0.5 * erfc(sqrt(EbN0_lin))这里 EbN0_lin 是线性值,不是 dB 值。很多人会把 dB 值直接代进去,算出来的理论曲线完全不对。注意这里用的是误码率 BER,不是误符号率 SER;QPSK 的误符号率理论值是erfc(sqrt(EbN0_lin))的高阶修正形式,约等于 BER 的两倍。拿 SER 的理论值去对比仿真 BER,曲线会始终高出一截,怎么调噪声模型都救不回来。
EbN0_dB = 0:8; EbN0_lin = 10.^(EbN0_dB/10); ber_theory = 0.5 * erfc(sqrt(EbN0_lin)); semilogy(EbN0_dB, ber_theory, 'k-', 'LineWidth', 1.5); hold on;matlab 里erfc是内置函数,不需要额外装工具箱;qfunc也存在于通信工具箱里,两者差一个系数,qfunc(x) = 0.5*erfc(x/sqrt(2))。理论上这两条路写出来的数值是一样的,但qfunc在部分老版本里对输入参数的向量化处理有边界限制,我建议直接用erfc,少一层环境依赖。上面这段代码把 0 到 8dB 的理论曲线算出来了,后面仿真的实测点如果要和它对比,横坐标必须也是 EbN0,而不是建模时的符号 SNR。
这一节最后还要强调一个概念:Eb/N0 和信噪比 SNR 不是一回事,前者是每比特能量对噪声功率谱密度的比,后者是信号功率对噪声功率的比。两者之间差了10*log10(k),还差了采样率相关的因子——如果你后面引入过采样、匹配滤波或者升余弦脉冲成型,SNR 表达式里还会多一项-10*log10(nSamp)。做基带等效仿真的话,我一般建议直接用 EbN0 作为主变量,全程换算到符号 SNR 去加噪,这样理论曲线和实测点天然对齐。
3. 在matlab里搭起QPSK误码率仿真的主循环:信号生成、加噪、判决、统计
3.1 用通信工具箱快速跑通第一版:pskmod + awgn 组合
如果你机器上装了 Communications Toolbox,最快的一条路是用pskmod、awgn、pskdemod三个函数搭主循环。这个组合的好处是不用自己写星座映射和最近邻判决,代码短、出错面窄,特别适合先验证整体思路。第一版我一般写成下面这样:
clear; clc; M = 4; k = log2(M); nBits = 5e5; % 不要一上来就跑到1e7,先小规模验证 EbN0_dB = 0:2:10; % 偶数格点先跑通,再加密 ber_mc = zeros(size(EbN0_dB)); for idx = 1:length(EbN0_dB) data = randi([0 1], nBits, 1); symIdx = bi2de(reshape(data, k, [])', 'left-msb'); txSig = pskmod(symIdx, M, pi/M, 'gray'); % 格雷映射、相位偏移 pi/4 snr_dB = EbN0_dB(idx) + 10*log10(k); rxSig = awgn(txSig, snr_dB, 'measured'); rxSym = pskdemod(rxSig, M, pi/M, 'gray'); rxBits = reshape(de2bi(rxSym(:), k, 'left-msb')', [], 1); ber_mc(idx) = mean(rxBits ~= data); end这一段里,pskmod的第三个参数pi/M是相位偏移,让四个星座点位于 45°、135°、225°、315°,这是 QPSK 教科书里最常见的画法。第四个参数'gray'指定星座映射顺序为格雷映射。de2bi(rxSym(:), k, 'left-msb')返回nSymbols行 2 列的矩阵,'转置后按列reshape成 1D 比特流,目的是把每个符号的两位比特按顺序排列回去。这行转置加reshape的顺序是所有脚本能否对齐的关键,建议自己用很小的nBits(比如 8)单步调试确认映射无误。
mean(rxBits ~= data)是误码率的朴素估计,它默认你的比特流没有对齐延迟。如果实际系统中存在传播延迟或滤波器群延迟,你需要先做相关对齐再比较,否则误码率会假性偏高;但在基带等效的单点仿真里,发射和接收都是理想同步的,所以这个简单写法没问题。
3.2 不用工具箱的手写版:映射、加噪、判决全流程
很多工程机没有通信工具箱,或者你想彻底搞清楚每一步在算什么,那就手写整个链路。手写版更能暴露对噪声功率谱密度 N0 的理解是否到位,我建议至少完整写一遍,即使最后跑通版本用的是工具箱函数。下面的代码是完整的手写版 QPSK 仿真:
clear; clc; M = 4; k = 2; constellation = [1+1i, -1+1i, -1-1i, 1-1i] / sqrt(2); grayMap = [0 1 3 2]; % 格雷映射: 00->0, 01->1, 11->3, 10->2 nBits = 1e6; EbN0_dB = 0:2:10; ber = zeros(size(EbN0_dB)); for idx = 1:length(EbN0_dB) EbN0_lin = 10^(EbN0_dB(idx)/10); N0 = 1 / (k * EbN0_lin); % 归一化条件下 Eb = 1/k noiseVar = N0 / 2; % 实部虚部各占一半噪声功率 data = randi([0 1], nBits, 1); symIdx = bi2de(reshape(data, k, [])', 'left-msb'); txSig = constellation(grayMap(symIdx+1)).'; noise = sqrt(noiseVar) * (randn(size(txSig)) + 1i*randn(size(txSig))); rxSig = txSig + noise; rxSym = zeros(size(txSig)); for n = 1:length(txSig) [~, minIdx] = min(abs(rxSig(n) - constellation)); rxSym(n) = grayMap(minIdx); % 映射回十进制索引 end rxBits = reshape(de2bi(rxSym(:), k, 'left-msb')', [], 1); ber(idx) = mean(rxBits ~= data); end这里的核心是噪声功率计算。因为星座点归一化到单位功率,符号能量 Es=1,每比特能量 Eb = Es/k = 1/k,于是 N0 = Eb / EbN0_lin = 1/(k·EbN0_lin)。高斯白噪声在 I、Q 两路各占一半功率,所以每路噪声方差是 N0/2,幅度标准差是sqrt(N0/2)。randn(...) + 1i*randn(...)产生复高斯噪声,实部虚部独立同分布,这正是 AWGN 的复基带等价形式。很多手写代码忘记给实部虚部分别乘sqrt(0.5),结果总噪声功率变成 N0 而不是 N0/2,实测曲线会整体右移 3dB。
判决部分用暴力最近邻遍历。constellation只有 4 个点,这个循环开销可以忽略;但如果你后面扩展到 16QAM 或 64QAM,建议用knnsearch或者把星座点拆成实部虚部单独判决,否则循环会拖慢整个仿真。格雷映射在这里体现为:grayMap(symIdx+1)完成从索引到星座点的查表,判决时又把最近的星座点通过grayMap(minIdx)映射回索引,保证收发一致。grayMap的选择会影响理论 BER 公式——只有格雷映射下0.5*erfc(sqrt(EbN0))才严格成立。
3.3 把主循环包装成脚本:数据长度、信噪比范围、随机种子怎么定
跑通单点仿真后,要把它扩展成一条完整的曲线,就需要考虑三个全局参数:仿真比特数、信噪比范围、是否固定随机种子。固定随机种子用rng(0)放在脚本开头,能让每次运行结果可复现——这个习惯在调参阶段非常重要,否则你每次看到的曲线都是新一批随机数,无法判断改动是否有效。
rng(0); % 固定随机种子,保证可复现 EbN0_dB = 0:1:12; % 加密横坐标,但高信噪比区要加仿真量 nBitsMin = 1e5; % 低信噪比区的基础样本数 nBitsMax = 2e7; % 高信噪比区的最大样本数 ber = zeros(size(EbN0_dB)); for idx = 1:length(EbN0_dB) if EbN0_dB(idx) <= 6 nBits = nBitsMin; else nBits = nBitsMax; end % ... 主仿真链路同 3.1 或 3.2(复用同一份代码)... ber(idx) = sum(errBits) / nBits; end逐点分配nBits的动机是误码率的统计精度。误码率的相对方差大约正比于1/sqrt(N_errors),在高信噪比区(比如 10dB 以上)误码率可能只有 1e-6,如果还用 1e5 个比特,整个仿真过程一个误码都碰不到,曲线的尾部就会变成 0,在 log 坐标上直接掉出图外。常见做法是先跑一遍粗曲线,观察误码率在哪个 EbN0 区间低于 1e-4,然后只对尾段增加样本数。我自己的习惯是低信噪比区用 1e5,每上升 2dB 提高一倍样本量,最多跑到 2e7 就打住——再往上不是不能跑,而是单次仿真耗时会从秒级变成分钟级,且系统误差(比如awgn的'measured'估计误差)会开始主导,加样本的钱包边际收益很低。
主循环的写法上,for循环逐 dB 扫描最直观也最容易加打印进度。也可以用arrayfun或parfor并行化,但 QPSK 单次仿真本身很快,并行化的收益要到大规模蒙特卡洛扫描时才体现。我更建议先把串行版跑通,再用parfor替换最外层的for作为优化手段,不要在调试阶段引入并行。
4. 常见避坑与排查:误码率曲线不平滑、信噪比偏3dB、判决方向不一致
4.1 加噪功率算错,实测BER与理论曲线差3dB怎么排查
现象:仿真出的误码率曲线形态和理论曲线完全一致,斜率一样,但整体向左或向右平移约 3dB,高信噪比区尤其明显。原因几乎总是噪声功率多加了或少加了一半。用awgn时把 EbN0 直接传进去,少了10*log10(k)的换算,噪声偏小,曲线左移;手写噪声时把 N0 当成总方差而不是每维方差,噪声偏大,曲线右移。排查办法很简单:在固定 EbN0=6dB 处,打印当前理论的 BER 和实测 BER,如果相差正好接近 3dB 对应的数量级,就先检查换算式里的 k,再检查噪声方差有没有多乘 2。
EbN0_test = 6; EbN0_lin = 10^(EbN0_test/10); ber_theory_test = 0.5 * erfc(sqrt(EbN0_lin)); % 理论值约为 0.0024,实测若明显小于它,大概率是噪声加少了这个 3dB 偏移的隐蔽之处在于:它不改变曲线的形状,只改变横坐标的偏移,所以如果你只画 semilogy 图、不对照理论值,肉眼很难发现。我见过不少人拿着一条“看起来很好”的曲线发到报告里,结果评审老师一对照理论曲线就发现整体平移了。务必在图上同时画ber_theory和ber_mc,并且两者用同一横坐标变量,才能暴露这类问题。
4.2 高信噪比下误码数为0:统计窗口不够与曲线断裂
现象:仿真曲线在 EbN0 超过某个点之后,误码率变成 0,semilogy 图上曲线突然掉到图外。这不是系统做了什么神奇优化,纯粹是统计样本不够。在误码率 1e-5 以下,想要测到一个误码平均需要 1e5 个比特;如果误码率为 1e-6,你还用 1e6 个比特,那么“刚好一个误码都没出现”的概率约等于 37%,任何一次仿真都可能撞上。解决思路不是无脑加样本,而是设一个最小误码数目标。
targetErrors = 100; % 至少统计到 100 个误码 while errTotal < targetErrors % 继续生成新一批数据,累加误码数与总比特数 % 每批之间共用同一 EbN0 end ber_est = errTotal / nBitsTotal;“等够误码数再停”比“固定跑多少个比特”在统计上更稳,因为它保证了相对估计误差大致在 1/sqrt(100)=10% 以内。代价是仿真时间不可控,所以在循环里要加最大迭代次数,防止在误码率 1e-8 这种极端条件下跑到天荒地老。另一个折中方案是按误码率量级分级设nBits,像我在 3.3 里写的那样,大部分场景都够用。
还有一个和零误码同源的现象:低信噪比区 BER 曲线抖动大。原因是误码事件相关性较强,尤其在比特级统计时,误码往往成串出现。解决办法可以是增大随机种子重掷次数,把多次独立仿真的误码数合并再算,不要只跑一次就下结论。rng换不同种子跑 5 次取平均,误码率曲线的光滑度会有明显改善。
4.3 接收端映射方向不一致:星座点对了但全错
现象:星座图看起来四相位置完全正确,解调的星座点与发射点重叠,但逐比特对比时误码率高达 50%。原因是grayMap的使用在发射和接收两端不对称。比如发射端用pskmod(symIdx, M, pi/M, 'gray'),接收端却用pskdemod(rxSig, M, pi/M, 'gray'),这两个版本对同样的复数信号解出的符号索引,与你的de2bi解释方式是否一致,取决于 matlab 内部映射表的方向。更常见的是手写映射表时grayMap = [0 1 3 2]与pskmod内置的格雷顺序不同,导致某一象限的判决结果被映射错。
排查方法:把发射的符号索引和接收判决的符号索引直接画出来对比,不要只比较最终的比特流。在nBits=8的极小规模下打印symIdx和rxSym,逐项核对。如果只有部分符号错,通常是某一列的比特位顺序反了;如果全部错,通常是映射表整体错位。这里最稳的写法是收发共用同一个映射表常量,不要在两处分别定义。
4.4 实部虚部交换:一种只在复数信道上出现的隐蔽错误
现象:误码率曲线在高信噪比区域比理论高出大约一倍,且不随 EbN0 增加而趋于和理论重合,而是始终保留一个固定的差距。原因:发射时用txSig = constellation(idx).',如果写成constellation(idx)而没有转置,向量方向不对;接收端判决时又恰好按行向量处理,导致符号顺序错位。这种方向错误在低信噪比时被噪声掩盖,误码率看起来还在理论附近;一旦信噪比升高,噪声不再是主导因素,系统性的符号错乱就会显露出来。
% 错误写法:txSig 是行向量还是列向量会被后续加法广播规则悄悄改变 txSig = constellation(symIdx); % 可能变成行向量 noise = sqrt(noiseVar) * randn(1, length(symIdx)); % 行噪声 % 正确写法:统一为列向量 txSig = constellation(symIdx).'; noise = sqrt(noiseVar) * (randn(length(symIdx), 1) + 1i*randn(length(symIdx), 1));这类问题用size()逐一检查就能发现,但新手往往只盯着误码率数字,忽略了中间变量的维度。我建议在每步操作后加一行assert(size(txSig,1) == size(noise,1)),让维度错误在第一时间爆出来,而不是等你对着曲线猜半天。这一条算是血泪经验:matlab 的隐式扩展让很多形状问题被掩盖,等到最终结果异常时,排查成本远高于一开始的防御性检查。
5. 用三条验证线路确认仿真没白跑:从单点数值到整条曲线拟合
5.1 先验单点,再验曲线,最后验斜率
很多同学把仿真代码一跑完,看到一条下降的曲线就直接截图放到报告里。这个习惯隐患很大,因为误码率曲线只要大方向对了,细节错误很难看出来。我给自己的验证流程是三层:第一层,选一个中等信噪比点,比如 EbN0=6dB,把理论 BER 算出来,实测值在它上下 20% 以内才算通过;第二层,把 0dB 到 4dB 区间内的实测曲线和理论曲线画在同一张图上,允许的偏差是每点都在同一数量级;第三层,检查曲线斜率,高信噪比区每增加 1dB,误码率应显著下降,如果某一段变成平缓甚至回升,说明有系统错误或仿真点数不够。
EbN0_check = 6; ber_theory_check = 0.5 * erfc(sqrt(10^(EbN0_check/10))); ber_mc_check = ber(EbN0_dB == EbN0_check); fprintf('theory = %.4e, simulated = %.4e\n', ber_theory_check, ber_mc_check);这层验证本质上是在检验“理论模型”与“仿真模型”的边界条件是否一致。理论公式假定理想同步、无定时误差、AWGN 信道;如果你的仿真里加入成型滤波、载波同步或多径信道,理论曲线就不再适用,这时候对比对象要换成更接近实际系统的链路级模型。对于没有工具箱的纯手写仿真,我建议至少保留理论曲线作为 sanity check,不要直接拿别人的链路预算当基准。
5.2 参数调优:仿真点数、信噪比步长、重复次数怎么取舍
做蒙特卡洛 BER 仿真,本质是用计算时间换统计精度。下面这张参数表是我跑 QPSK 时常用的起点,按信号处理链路复杂度和机器性能调整:
| 参数 | 低配起点 | 标准配置 | 说明 |
|---|---|---|---|
| 每点仿真比特数 | 1e5 | 1e6 | 高信噪比区至少 1e7 |
| EbN0 范围 | 0:2:12 | 0:1:14 | 步长 1dB 够画曲线,0.5dB 用于精细对比 |
| 随机种子 | rng(0) | rng(0) + 5 次重复 | 重复次数多用于低信噪比区平滑 |
| 目标最小误码数 | 50 | 100 | 保证相对误差低于 10% |
信噪比步长从 2dB 细化到 1dB,会直接影响高信噪比区的曲率表现,但代价是总点数翻倍。如果不确定某个区间是否需要加密,可以先跑 2dB 粗曲线,找到曲线拐点(通常是误码率从 1e-2 到 1e-5 那段),再单独加密这个区间。样本数的分配上,一个容易被忽略的点是:EbN0=0dB 附近 BER 约 0.08,用 1e5 个比特测试能得到约 8000 个误码,统计已经很稳;而 EbN0=10dB 附近 BER 约 4e-6,需要 2.5e7 个比特才能得到 100 个误码。所以把大部分计算预算投给高信噪比区,永远是合算的。
5.3 图上验证:semilogy坐标系下的视觉检查清单
仿真结果的呈现方式也会影响排查效率。误码率曲线必须用semilogy或loglog,在线性坐标下 1e-2 和 1e-5 看起来几乎都是零,无法判断趋势。画图时把理论曲线画成实线黑线,仿真点在图上用圆圈标出,不要用点线连接起来——仿真点是样本估计,不是连续函数,用虚线连反而会误导观察。检查时重点看三处:曲线末端是否出现平台期、曲线是否与理论曲线交叉、低信噪比区是否出现系统性偏移。出现平台期是零误码;交叉意味着某个区段内模型偏差方向反转,常见于映射错乱或残余符号间干扰;系统性偏移则几乎总是功率换算问题。
semilogy(EbN0_dB, ber_theory, 'k-', 'LineWidth', 1.5); hold on; semilogy(EbN0_dB, ber_mc, 'b-o', 'MarkerSize', 6); grid on; xlabel('Eb/N0 (dB)'); ylabel('BER'); legend('Theory', 'Simulation', 'Location', 'southwest');图本身也是最早能暴露问题的地方:如果你的实测点在 0 到 4dB 区间比理论点低很多(误码率更小),第一反应不应该是高兴,而是几乎可以断定噪声少加了——这是一个“看起来太好”恰是错误信号的高频场景。反过来,实测明显差于理论,则先检查接收判决方向、噪声方差、以及比特流对齐这三大元凶。
6. 从“能跑”到“能复用”:把QPSK脚本改造成参数化仿真函数
验证通过之后,下一步是把散乱的脚本改造成一个参数化函数,让它的输入是调制阶数、EbN0范围、目标误码数,输出是 BER 向量和耗时信息。这样同一套代码可以复用于 BPSK、16QAM,甚至以后扩展 OFDM 子载波仿真。
function [ber, EbN0_dB] = simulateBER(M, EbN0_dB, doPlot) if nargin < 3, doPlot = false; end k = log2(M); % 星座点和格雷映射表可按 M 生成,这里只列 QPSK constellation = [1+1i, -1+1i, -1-1i, 1-1i] / sqrt(2); grayMap = [0 1 3 2]; ber = zeros(size(EbN0_dB)); for idx = 1:length(EbN0_dB) % 内部复用 3.2 的加噪、判决链路 end if doPlot semilogy(EbN0_dB, 0.5*erfc(sqrt(10.^(EbN0_dB/10))), 'k-'); hold on; semilogy(EbN0_dB, ber, 'b-o'); end end函数化的好处不只是代码整洁,更重要的是把“调制方式选择”“噪声模型”“判决规则”这三个变点隔离出来,以后换方案时只需要改星座生成和映射表。我自己最早的 QPSK 脚本就是满屏全局变量的面条代码,后来在对比 16QAM 时吃了大亏——改动一个参数结果完全乱了。从那以后我养成的习惯是:任何要跑超过三天的仿真脚本,第一天就写成函数,哪怕参数少,也要让每个输入变量的含义明确。
如果你以后要在这个基础上做信道编码对比、加卷积码或 LDPC,还可以把simulateBER里的加噪和判决部分单独抽成channelModel和demodulator两个子函数,这样误码率统计逻辑与具体调制方案解耦。回头看,QPSK 误码率仿真永远是通信链路仿真里最透明、最不容易出错的模块,但恰恰因为简单,很多人忽略了系统化验证。从固定随机种子开始,到逐点核对样本量,再到最后曲线与理论的吻合,每一步都能筛出几类不同的问题。希望这份操作路径能帮你少走几趟弯路,也希望你那版源码跑出来的第一条曲线,就是一条跟理论贴得漂亮、可信度经得起追问的曲线。
本文还有配套的精品资源,点击获取