简介这份MATLAB代码用于绘制二进制频移键控BFSK系统在不同信噪比下的误码率曲线适合无线通信课程学习者、科研人员以及需要验证调制解调性能的工程师。代码覆盖随机二进制序列生成、AWGN信道叠加、解调、误码统计与semilogy绘图完整流程同时代码变量命名清晰方便修改载波频率、采样率与SNR范围能从信号产生到性能评估完整复现运行后直接观察BER随SNR的变化规律也便于调整参数做拓展实验。压缩包内共有1个文件为m格式脚本整体仅2KB结构简洁无需复杂配置适合快速学习与二次修改。目前已有1927人浏览学习。借助该脚本既能理解BFSK频率切换与相干检测原理也能掌握通信系统蒙特卡洛仿真、误码率计算与曲线绘制方法还可将框架推广至M-FSK、PSK、QAM等调制方式的性能对比适合作为教学演示与算法验证的参考。1. BFSK误码率曲线的MATLAB代码先从一次仿真翻车说起如果你在搜BFSK误码率曲线的MATLAB代码多半是被理论曲线和仿真曲线对不上这件事卡住了。明明书上写着误码率公式自己一跑蒙特卡洛要么整体往右偏几个dB要么高信噪比区域乱跳。这份资源就是一个能直接改参数跑的MATLAB脚本发射、加噪、相干解调、非相干解调、误码统计、理论曲线一条龙全串起来。适合三类人通信课程作业要交仿真图的同学、准备面试想手撕数字调制链路的候选人、还有做项目预研懒得从头翻教材的工程师。读完之后你不仅能复现出两条经典曲线还能知道曲线歪了该往哪个方向查。2. BFSK信号模型与误码率理论仿真前先立住三个坐标2.1 信号表达式、正交条件与归一化频率间隔BFSK做的事情很简单用两个不同频率的载波分别表示比特0和比特1。通带形式可以写成s0(t) cos(2πf1t)s1(t) cos(2πf2t)在复基带等效模型里这两路信号直接写成复指数形式 exp(j2πf1t) 和 exp(j2πf2t)。很多第一次写仿真的人看到代码全是复数就发懵其实这是故意的复基带仿真省掉了高频载波项只保留幅度和相对相位信息误码率计算完全不受影响。这里有一个决定仿真是否可信的参数归一化频率间隔 h Δf × Tb其中 Δf f2 - f1Tb 是一个符号的持续时间。当 h 取整数时两个频率的波形在一个符号周期内正交相关器输出没有串扰当 h 不是整数时两路信号互不正交判决时会互相干扰理论误码率公式也不再适用。工程上最常用的最小正交间隔是 h 1也就是 Δf 1/Tb。我在代码里默认 f1 2000Hz、f2 4000Hz、Tb 1ms所以 Δf 2000Hzh 2满足正交条件。这个间隔比最小正交间隔大了一倍好处是频率间隔足够远判决余量大曲线更贴近理论值坏处是占用带宽更大。你拿到代码后想改小频率间隔一定要先算一下 h 是不是整数。频率间隔 h | 是否正交 | 理论误码率公式是否适用 h 0.5 | 否 | 不适用存在符号间串扰 h 0.8 | 否 | 不适用性能明显恶化 h 1.0 | 是最小正交间隔 | 适用 h 1.5 | 否 | 不适用 h 2.0 | 是 | 适用2.2 相干解调与非相干解调的理论误码率公式BFSK 有两种标准解调方式误码率公式不同曲线形状也不同。相干解调用本地载波做相关因为恢复了载波相位信息性能更好非相干解调只比较包络大小实现简单但性能略差。相干 BFSK 在 AWGN 信道下的误码率是Pe Q(sqrt(Eb/N0))非相干 BFSK 的误码率是Pe 0.5 × exp(-Eb / (2N0))两个公式的适用前提是等能量正交信号、AWGN信道、接收端做匹配滤波或等效的相关运算。如果仿真里频率间隔不满足正交条件或者噪声功率加错了仿真点就会和这两条理论曲线分家。从公式能直接看出趋势非相干比相干大约差 1dB 左右Eb/N0 越高差距越不明显在低信噪比区域两条曲线靠得很近。所以如果你的仿真曲线在 0dB 附近两条线几乎重合那很正常但如果到了 10dB 还重合就要怀疑是不是相干解调写成了包络检测。MATLAB 里算理论曲线可以直接用通信工具箱的 qfunc如果不想依赖工具箱用 0.5 × erfc(x / sqrt(2)) 也能得到完全一样的结果。后面给出的完整代码里我写了两种写法方便没有安装通信工具箱的环境直接跑。2.3 为什么用复基带等效仿真而不是直接搬载波有人会问公式里明明是 cos(2πf1t)仿真里为什么直接写 exp(j2πf1t)这就要说到复基带仿真和通带仿真的区别。通带仿真要在时域把载波周期采出来。假设载频是 2GHz符号速率 1kHz每个符号里要采几百万个点仿真一段误码率曲线就要跑几小时起步。复基带仿真把高频载波项去掉只保留信号相对于某个参考频率的偏移采样率只需要略高于信号带宽每个符号采 8 到 16 个点就足够。更重要的是前面给出的误码率理论公式本身就是在等效低通模型下推导的复基带仿真和理论公式天然对齐。代码里信号用 exp(j2πf1t) 和 exp(j2πf2t) 表示接收端做相关时也用复指数共轭运算整个过程和教材上的正交信号模型完全一致。理解了这一层后面看代码里的 real 和 abs 就不会晕real 提取的是相干相关的实部abs 提取的是包络。3. 从零写BFSK误码率仿真发射、加噪、解调、统计一条龙3.1 参数区先把信噪比刻度和符号率定下来写蒙特卡洛仿真第一步不是写调制而是定参数。参数决定仿真时间、频率间隔是否正交、信噪比范围是否合理。我常用的配置如下% 仿真参数区 numBits 1e5; % 每个Eb/N0下发送的比特数 nsamp 16; % 每个符号的采样点数 bitRate 1e3; % 比特速率 1kbps fs bitRate * nsamp; % 采样率 16kHz f1 2000; % 载频1 f2 4000; % 载频2 deltaF f2 - f1; % 频率间隔 2kHz Tb 1 / bitRate; % 符号持续时间 1ms h deltaF * Tb; % 归一化频率间隔 2正交 EbN0dB -2:1:12; % 扫描的信噪比范围 EbN0Lin 10.^(EbN0dB / 10); % 转成线性值 rng(2024); % 固定随机种子结果可复现这段代码里最容易改错的是 nsamp 和 bitRate 的关系。fs bitRate × nsamp意思是每个符号持续时间内采 nsamp 个点。如果 nsamp 太小比如只有 4波形离散化误差会直接影响相关器输出如果太大仿真数据量成倍增加但误码率结果不会变好。16 是个平衡值。正常情况下整个仿真跑下来大概需要几十秒到几分钟取决于机器和信噪比点数。后面所有调制、加噪、解调的代码都要放进for snrIdx 1:length(EbN0dB)的循环体里每个信噪比点独立做一次完整实验。3.2 向量化FSK调制与复AWGN信道加噪调制部分我推荐直接向量化不要用 for 循环逐符号生成波形。下面的代码用矩阵外积一次性生成全部符号% 生成随机0/1比特流 bits randi([0 1], 1, numBits); % 单个符号时间网格 tvec (0:nsamp-1) / fs; % 向量化FSK调制每个比特选一个频率 freqSeq f1 bits * deltaF; % 1 x numBits sigMat exp(1j * 2 * pi * freqSeq. * tvec); % numBits x nsamp sigTx reshape(sigMat., 1, []); % 转成串行样本流 % 计算符号能量并加噪 Es sum(abs(sigTx).^2) / numBits; % 平均每符号能量 Eb Es / 1; % BFSK每符号1bit N0 Eb / EbN0Lin(snrIdx); % 噪声单边谱密度 noise sqrt(N0/2) * (randn(size(sigTx)) 1j*randn(size(sigTx))); r sigTx noise; % 接收信号关键点是freqSeq. * tvec这个外积运算。freqSeq 是 1×numBits 的行向量转置后变成 numBits×1再乘以行向量 tvec就得到 numBits×nsamp 的矩阵。每一行对应一个符号的复指数波形行内的列对应这个符号内的 16 个采样点。reshape(sigMat., 1, [])是为了把矩阵重新排列成一维样本流。这里必须先转置再展开因为 MATLAB 的 reshape 是按列优先展开的不转置的话会把第一个符号的第 1 个点、第二个符号的第 1 个点先拼到一起信号顺序就全乱了。噪声功率的写法sqrt(N0/2)是复基带 AWGN 的标准约定。复噪声的实部和虚部各占 N0/2 的方差两部分合起来总功率才是 N0这样 Eb/N0 的定义才和理论公式一致。3.3 相干解调相关器组的实现细节相干解调的本质是做相关运算把接收信号分别与本地参考波形做内积然后比较两个相关值。MATLAB 里这步用矩阵乘法一次完成% 接收信号按符号切成矩阵每一列是一个符号 rMat reshape(r, nsamp, []); % 本地参考波形复共轭在做内积时生效 s1 exp(1j * 2 * pi * f1 * tvec).; % nsamp x 1 s2 exp(1j * 2 * pi * f2 * tvec).; % 相干相关取实部比较 z1c real(s1 * rMat); % 1 x numBits z2c real(s2 * rMat); % 判决哪个相关值大就判哪个 bitsHatCoh (z2c z1c); % 统计错误比特数 errCoh sum(bitsHatCoh ~ bits);s1 * rMat中s1 是本地参考波形的共轭转置数学上就是匹配滤波。如果发送的是比特0接收信号近似等于 s1那么 z1c 会得到接近符号能量的大数值z2c 因为频率正交会接近 0判决自然偏向 0。为什么这里必须用real而不是abs因为相干解调利用了完整的相位信息。取实部以后造近信号方向上的投影是正的正交方向上的投影接近零如果改成 abs实际上是把相位信息丢掉了那就变成了包络检测也就是非相干解调。这个细节是相干和非相干代码唯一的区别也是最容易写错的地方。3.4 非相干解调包络检测与判决非相干解调不需要恢复载波相位实现方式是在相关器后面取包络再比较。对应到复基带代码里就是同样的内积运算只是把real换成abs% 非相干取相关输出的包络 z1nc abs(s1 * rMat); z2nc abs(s2 * rMat); % 包络比较判决 bitsHatNoncoh (z2nc z1nc); % 统计错误比特数 errNoncoh sum(bitsHatNoncoh ~ bits);这段代码很短但信息量不小。abs 在复基带等效模型里就相当于包络检波器它把复数的幅度提取出来丢掉相位。发送比特0时z1nc 大致是信号能量加噪声的幅度z2nc 则只有噪声幅度信噪比越高两路包络差距越大判决越可靠。非相干比相干实现简单不需要本地载波同步代价是性能差 1dB 左右。你在跑仿真时会看到两条曲线几乎平行非相干始终在相干上方一点点这个结果和理论公式完全吻合。3.5 误码统计与理论曲线叠画每个信噪比点跑完以后把误码率存进数组循环结束后一起画图。画图部分注意两个小细节一是用 semilogy 画对数纵轴二是高信噪比区域误码率可能统计到 0直接画会丢点最好做个下限保护。% 循环结束后计算误码率 BERcoh(snrIdx) errCoh / numBits; BERnoncoh(snrIdx) errNoncoh / numBits; % 理论曲线 BERcohTheory qfunc(sqrt(EbN0Lin)); % 相干BFSK理论误码率 BERnonTheory 0.5 * exp(-EbN0Lin / 2); % 非相干BFSK理论误码率 % 如果没有通信工具箱qfunc用下面这行替代 % BERcohTheory 0.5 * erfc(sqrt(EbN0Lin / 2)); % 画图 figure; semilogy(EbN0dB, max(BERcoh, 1e-6), bo-, LineWidth, 1.5); hold on; semilogy(EbN0dB, max(BERnoncoh, 1e-6), rs-, LineWidth, 1.5); semilogy(EbN0dB, BERcohTheory, b--, LineWidth, 1.5); semilogy(EbN0dB, BERnonTheory, r--, LineWidth, 1.5); grid on; xlabel(Eb/N0 (dB)); ylabel(误码率 BER); legend(相干仿真, 非相干仿真, 相干理论, 非相干理论);max(BER, 1e-6)是防止误码率为 0 时 semilogy 画不出点。如果你发现某个信噪比点下面没有曲线符号多半就是这里在起作用实际应用中这个点要么增加比特数重跑要么在图上标注为“本点零误码”。4. BFSK仿真为什么对不上理论五个高频坑与排查顺序4.1 现象曲线整体向右偏移1.5到3dB原因复噪声方差写错这是BFSK仿真里最经典的翻车现场。跑出来的仿真曲线形状和理论曲线完全一致但整体往右移了大约 1.5 到 3dB也就是仿真误码率比理论差。很多人第一反应是调制有问题其实绝大多数情况是噪声功率加多了。原因很简单复基带噪声分成实部和虚部两路每一路的方差都应该是 N0/2。如果写成noise sqrt(N0) * (randn 1j*randn)那么实部虚部各带 N0 的方差合成噪声功率就是 2N0相当于把信噪比拉低了一倍也就是 3dB。我见过有人在这里纠错纠了一整天最后把sqrt(N0/2)改成sqrt(N0/2)就一秒通过。排查顺序上每跑一个新脚本先进去看加噪那行代码再去看调制。4.2 现象相干曲线和非相干曲线几乎重合原因判决变量取了绝对值只改一行代码相干解调就变成了非相干解调。如果相干解调里把 z1c、z2c 用abs()计算而不是real()那相关性里的相位信息全部丢失得到的其实就是包络。这样两条曲线自然重合而且相干曲线的理论标注是错的。解决方法是分清 use case要模拟相干接收机就用real(s1 * rMat)要模拟非相干接收机就用abs(s1 * rMat)。调试时可以在某个信噪比点打印 z1c 和 z1nc 的分布相干输出可能为负包络输出永远大于等于零视觉上一眼就能看出来。4.3 现象高信噪比区域曲线抖动剧烈甚至断点原因错误比特太少当 Eb/N0 到 10dB 以上BFSK 理论误码率已经掉到 10 的负 5 次方以下。如果你每个点只跑 1e5 比特理论上期望错误比特还不到 1 个统计结果自然一会是 0 一会是 2e-5曲线看起来像锯齿。解决方法是动态增加比特数。一个朴素准则是每个信噪比点至少累计 50 到 100 个错误比特再停止仿真。实现上就是加一个 while 循环每次发一批新比特累加错误数直到达到阈值。代价是低误码率点仿真时间会明显变长这是正常的没有免费午餐。4.4 现象仿真与理论整体对不上改了多进制尤其明显原因Es/N0换算错位BFSK 每符号只有 1 比特Es 和 Eb 数值上相等所以这个坑在 2FSK 里不明显。但代码一推广到 4FSK、8FSK每符号 2 或 3 比特如果还拿 Eb/N0 当 Es/N0 用曲线的位置会整体错开一个 log2(M) 倍。通用的换算是 Es Eb × log2(M)。在仿真代码里符号能量 Es 是按整个符号波形算出来的而公式里的 Eb/N0 必须和比特能量对齐。我一般手动加噪而不是用awgn函数就是因为awgn默认按输入信号平均功率算没有显式的符号能量换算很容易埋雷。4.5 现象两次运行结果不一样原因随机种子没固定蒙特卡洛仿真本身有随机起伏不固定随机种子等于没有基线。你改了一个参数跑出新曲线和旧曲线不一样你没法判断是参数引起的变化还是随机噪声引起的抖动。解决方法是所有随机数生成之前固定rng(seed)每次运行结果完全一致。更进一步我在下面第五小节会给出独立随机流的做法让不同解调方式或不同频率间隔的对比在相同随机条件下进行曲线差异才能真正反映信号设计本身的影响。5. 把脚本升级成小工具固定种子、扫频率间隔、看曲线迁移5.1 用独立随机流做公平对比当你需要对比相干和非相干或者对比不同调制指数 h 的曲线时随机抖动会干扰判断。一个实用做法是把整段仿真封装成函数函数内部用rng(seed snrIdx)给每个信噪比点分配一个独立但固定的随机种子。function [BERcoh, BERnon] simBfsk(h, numBits, seed) % h由外部指定内部根据h设置f2频率 ... for snrIdx 1:length(EbN0dB) rng(seed snrIdx); % 每个SNR点独立随机流 bits randi([0 1], 1, numBits); ... end end这样做的好处是同一个 h 值连续跑两次结果完全一致不同 h 值之间因为种子不同保证实验独立对比两条曲线时它们之间的差异来自频率间隔和解调方式而不是随机数生成顺序。这是工程上非常实用的习惯。5.2 扫描调制指数h观察正交性如何改变误码率把仿真封装成函数以后就可以批量扫参数了。我扫过最多次的是 h 从 0.5 到 2 的变化一步就能看出正交条件的分水岭hList [0.5, 0.8, 1, 1.5, 2]; figure; hold on; for hidx 1:length(hList) [~, BERnon] simBfsk(hList(hidx), 2e5, 100); semilogy(EbN0dB, max(BERnon, 1e-6), o-, ... DisplayName, sprintf(h%.1f, hList(hidx))); end grid on; legend;预期会看到 h 1 和 h 2 的曲线完全重合因为都满足正交条件理论误码率相同h 0.8 和 h 0.5 的曲线明显抬高其中 h 0.5 在高信噪比区域会有地板效应误码率降到一定程度就下不去了。这就是非正交 FSK 代价的直接可视化。如果你想省带宽又要保性能简单的 BFSK 做不到得换 MSK 这类连续相位调制方案。从那以后我每写一套 FSK 仿真都会强制走一遍三板斧先固定种子再审一遍 Es/N0 和 Eb/N0 的换算最后跑 h 1 和 h 2 两个正交间隔确认仿真曲线贴得住理论。这套流程帮我拦下了不少低级错误也让我改参数改得放心。希望帮到你。本文还有配套的精品资源点击获取