简介:面向通信与电子信息专业学习者及科研人员的MFSK仿真资源,围绕多进制频移键控的频谱效率、误码率与频谱特性展开,提供可直接运行的Matlab源码及配套操作演示。压缩包约743KB,共含3个文件:runme.m主程序、操作录像avi演示视频以及txt说明文档,结构简单清晰,适合快速上手。主程序基于Matlab 2021a及以上版本编写,运行Runme.m即可复现仿真结果,视频演示全过程操作步骤,能帮助初学者规避路径设置等常见问题。目前已有598人学习使用,可支撑课程设计、毕业设计或科研预研中对MFSK性能的仿真验证,也可作为理解数字调制原理的实践参考。
1. 调MFSK仿真时,先弄清要复现的是哪条曲线
拿到这个MFSK仿真包,第一件该做的事不是立刻双击runme.m,而是先想清楚要复现的是哪一条结果:M阶数变化下的频谱效率、误码率曲线,还是功率谱密度图。我拆过不少类似的通信课程设计,很多人在第一次运行时卡在“当前文件夹不是工程路径”或“误码率曲线和理论对不上”,根子都在于没有理解MFSK的频谱效率和误码率是如何随M变化的。
不同M值下,MFSK的带宽占用和误码性能会呈现完全不同的趋势。M越大,频率点越多,抗噪声能力越强,但频谱效率反而可能下降。这个反直觉结论直接决定了仿真结果怎么看、参数怎么调,也会影响后续FPGA实现时的频率点分配思路。
下面把信号模型、参数设置、频谱测量和误码率仿真串在一起讲,涉及的核心代码可以直接在MATLAB 2021a及以上版本运行。适合做通信原理大作业、毕业设计预研,或者想快速验证MFSK链路仿真结果的工程师。
2. MFSK的信号模型、正交条件与频谱效率推导
2.1 从BFSK到MFSK:不是并行发多个频率
MFSK中的“M”很容易让人误以为它像OFDM那样同时用M个频率传输数据。实际上相反:MFSK每个符号周期只在M个可选频率中发送一个,接收端判断当前符号是哪个频率,所以它属于频率选择调制,不是多载波调制。这个区别直接决定带宽怎么算:M个频率要占用一段连续频带,而不是通过叠加获得更高频谱效率。
用复基带信号表示,第i个符号的发送波形为:
s_i(t) = exp(j*2*pi*((i-(M-1)/2)*Δf)*t), 0 <= t < Ts
其中i取值0到M-1,Δf是相邻频率间隔,Ts是符号周期。仿真里通常把载频设为0,只看复包络。以M=4为例,四个频率偏移分别是-1.5Δf、-0.5Δf、0.5Δf、1.5Δf,这样频谱图关于0频率对称。频率偏移公式里减去(M-1)/2,是为了让最低和最高频率围绕原点分布,方便在FFT上观察。
那么Δf取多小才不会让接收端的相关器互相串扰?MFSK的正交性是在一个符号周期内定义的。当两个频率之间的间隔满足(f1-f2)*Ts为整数时,两个频率在时间窗内的内积为0。因此取Δf=1/Ts,也就是频率间隔等于符号速率Rs时,相邻频率在符号周期内相差整数个周期,相关器输出为零。这个参数在FPGA实现里很常用,因为M路频率检测可以用同一次FFT结果去查对应频点,不需要额外搭M组滤波器。
2.2 频谱效率的理论值:log2(M)/M
在Δf=1/Ts条件下,MFSK基带信号近似占用W ≈ M/Ts = M*Rs的带宽。每个符号携带k=log2(M)比特,所以比特速率Rb=k*Rs。频谱效率定义为:
η = Rb/W = log2(M)/M
这个公式会得到一个反直觉的结论:增大M并不能一直提高频谱效率。
| M | 2 | 4 | 8 | 16 | 32 |
|---|---|---|---|---|---|
| 频谱效率 η (bit/s/Hz) | 0.5 | 0.5 | 0.375 | 0.25 | 0.156 |
M=2和M=4的频谱效率相同,M=8以后继续下降。也就是说MFSK是用频带换抗干扰能力,适合低信噪比、功率受限场景,不适合频谱资源紧张的高速链路。下面的MATLAB脚本用表格输出理论频谱效率,可以直接复制运行:
Mset = [2 4 8 16 32]; % 调制阶数 Rs = 1000; % 符号速率 k = log2(Mset); % 每符号比特数 BW = Mset .* Rs; % 基带近似占用带宽 Rb = k .* Rs; % 比特速率 eta = Rb ./ BW; % 频谱效率 T = table(Mset', Rb', BW', eta', ... 'VariableNames', {'M', 'Rb_Hz', 'BW_Hz', 'Efficiency_bt_sHz'}); disp(T);脚本里的频带宽度是按单边基带信号估算的,所以结果直接可用。如果教材采用双边带通带带宽2M/Ts,频谱效率会差一个2倍因子,但M之间的相对关系不变。课程设计答辩时建议先手算一次M=4的取值,再让脚本显示表格,这样被问“频谱效率怎么来的”时能答得清楚。
2.3 相干与非相干检测会改变频率间隔选择
如果接收端使用相干检测,MFSK的最小正交频率间隔可以缩小到1/(2Ts),理论频谱效率会比非相干检测高。但相干检测要求恢复每路载波的相位,工程实现成本高。课程设计里的蒙特卡洛仿真大多数使用包络检测,也就是非相干检测,频率间隔仍然按Δf=1/Ts处理。看到仿真结果与书上理论对不上时,先确认代码里使用的是相干还是非相干公式,这是最容易被忽视的差异。
3. MATLAB仿真MFSK信号:观察频谱并测99%占用带宽
3.1 生成带符号跳变的MFSK基带波形
频谱仿真需要一个能体现MFSK频率切换的时间序列。我一般用如下方式生成,代码简单且便于和FPGA实现对照:
M = 4; % 四进制FSK Rs = 1000; % 符号速率 Fs = 48000; % 采样率 sps = Fs / Rs; % 每符号采样点数 df = Rs; % 频率间隔=1/Ts numSym = 2000; % 符号总数 symbols = randi([0 M-1], 1, numSym); t = (0:sps-1) / Fs; x = zeros(1, numSym * sps); for n = 1:numSym f_offset = (symbols(n) - (M-1)/2) * df; seg = exp(1j * 2 * pi * f_offset * t); x((n-1)*sps + 1 : n*sps) = seg; end代码中每个符号用固定频率的指数信号填充,符号边界处相位会跳变。相位不连续会让频谱旁瓣稍微抬高,但对误码率影响很小。如果希望做到连续相位MFSK,可以用vco或对瞬时频率做累积积分,眼前这套代码适合快速观察频率切换过程。df=Rs是前面说的正交间隔,改成2*Rs能进一步拉开频点,但带宽会增大。
3.2 用periodogram和obw测量99%占用带宽
生成x之后,用信号处理工具箱的periodogram看功率谱,再用obw测带宽:
[pxx, f] = periodogram(x, hann(length(x), 'periodic'), [], Fs); plot(f, 10*log10(pxx)); xlabel('Frequency (Hz)'); ylabel('Power Spectrum (dB)'); grid on; xlim([0 Fs/2]); BW99 = obw(x, Fs); % 默认返回99%占用带宽 Rb = log2(M) * Rs; fprintf('M=%d, 99%% bandwidth = %.1f Hz, SP = %.3f bit/s/Hz\n', ... M, BW99, Rb / BW99);obw(x,Fs)默认从功率谱两侧向中间收,直到包含99%的功率,返回区间的宽度。M=4、Rs=1000Hz的信号,理论主瓣带宽约4000Hz,实际测出来的99%占用带宽会略高一些,因为sinc旁瓣仍携带少量功率。如果报告需要稳定复现,就把这段代码放在runme.m的频谱图之前,用fprintf把实测数值输出到命令行。
3.3 窗函数和FFT bin对频谱测量结果的影响
测量MFSK频谱时,矩形窗和汉宁窗的差别很明显,建议按下表选择:
| 场景 | 推荐窗 | 原因 |
|---|---|---|
| 频率恰好落在FFT bin上 | 矩形窗 | 主瓣窄,测量结果更接近理论值 |
| 频率是任意小数 | 汉宁窗 | 抑制旁瓣泄漏,obw结果更稳定 |
这里说的“频率恰好落在FFT bin上”,是指让每个符号的频率偏移是Rs的整数倍,并且Fs/Rs为整数。这样观测时长内的信号刚好包含整数个周期,FFT不会出现栅栏效应。如果频率偏移不是整数关系,矩形窗的旁瓣会把频谱“糊”开,obw测出的带宽可能偏大,这时候用汉宁窗更合适。实际操作中先运行一次两种窗函数下的曲线,再看频谱图是否出现不该有的高旁瓣,就能判断当前参数是否落在整数bin上。
4. 误码率仿真:信噪比换算、非相干检测与误码率误信率关系
4.1 EbN0与EsN0的换算决定了曲线位置
误码率仿真最容易出错的地方不是调制代码,而是信噪比定义。横轴通常是Eb/N0,而接收端相关器处理的是符号能量Es/N0,两者关系是:
EsN0dB = EbN0dB + 10*log10(log2(M))
不同M下需要加到Eb/N0上的偏移量如下:
| M | log2(M) | 换算增量(dB) |
|---|---|---|
| 2 | 1 | 0 |
| 4 | 2 | 3.010 |
| 8 | 3 | 4.771 |
| 16 | 4 | 6.021 |
如果直接把EbN0当成EsN0代入噪声方差,M=4时仿真曲线会比理论曲线右移3dB。反过来说,把EsN0当成EbN0,曲线又会左移,看起来优于理论。先校准信噪比,再做曲线对比。
4.2 用非相干包络检测跑蒙特卡洛仿真
下面代码是MFSK基带非相干检测的最小示例。发送端每次从M个频率里随机选一个,接收端用M个复指数相关器取包络,选最大输出作为判决结果。这种结构和FPGA里的Goertzel滤波器组思路一致。
M = 4; Rs = 1000; sps = 48; Fs = Rs * sps; df = Rs; k = log2(M); t = (0:sps-1) / Fs; EbN0dB = 0:2:12; numSym = 1e4; ber = zeros(size(EbN0dB)); ser = zeros(size(EbN0dB)); for idx = 1:length(EbN0dB) EsN0dB = EbN0dB(idx) + 10*log10(k); EsN0 = 10^(EsN0dB/10); N0 = sps / EsN0; % 复噪声每采样方差 noiseAmp = sqrt(N0/2); bitErr = 0; symErr = 0; for n = 1:numSym sIdx = randi([0 M-1]); fTx = (sIdx - (M-1)/2) * df; tx = exp(1j*2*pi*fTx*t); rx = tx + noiseAmp * (randn(1,sps) + 1j*randn(1,sps)); corr = zeros(1,M); for m = 1:M fRef = (m-1 - (M-1)/2) * df; corr(m) = abs(sum(rx .* exp(-1j*2*pi*fRef*t))); end [~, rIdx] = max(corr); bitErr = bitErr + sum(bitget(sIdx,1:k) ~= bitget(rIdx-1,1:k)); symErr = symErr + (sIdx ~= (rIdx-1)); end ber(idx) = bitErr / (numSym * k); ser(idx) = symErr / numSym; end berTheory = berawgn(EbN0dB, 'fsk', M, 'noncoherent'); semilogy(EbN0dB, ber, 'o-', EbN0dB, ser, 's-', EbN0dB, berTheory, '--'); xlabel('Eb/N0 (dB)'); ylabel('Error Rate'); legend('BER仿真','SER仿真','BER理论','Location','southwest'); grid on;代码里的N0计算需要解释:一个符号有sps个采样点,每个采样点的复噪声方差是N0,相关器输出端信号幅度累加成sps,噪声功率累加成sps*N0,输出信噪比等于sps/N0,也就是Es/N0。因此用N0 = sps / EsN0反推采样点噪声方差,再拆成实部和虚部各一半方差。bitget按二进制位统计比特错误,避免依赖额外通信工具箱。
如果手头没有Communications Toolbox,berawgn那行可以用理论SER公式替换。但课程设计里berawgn很常用,直接用它与仿真结果对比是最快的方式。
4.3 误码率与误信率的大小关系图:SER与BER怎么换算
很多报告里说“误码率”其实指的是BER,“误信率”是教材里的另一种叫法,也指比特错误率。MFSK仿真里同时统计SER和BER,会得到两条趋势相同、数值不同的曲线。上面代码已经同时算出了ser和ber,画出来就是一张典型的大小关系图。
M=4、自然二进制映射时,一个错误符号平均产生4/3个比特错误;每个符号有2比特,所以BER约等于(2/3)*SER。这个比例在高Eb/N0区域比较稳定,低信噪比时因为符号错误概率高,比例会轻微偏离。如果M=8或16,且符号到比特的映射不是格雷码,比特错误比例会随M变化。最稳妥的做法不是套近似公式,而是像上面代码那样用bitget逐位比较,同时统计SER和BER。
需要精确定位曲线位置时,把numSym提高到5e4以上,误码率曲线会平滑很多。用1e4符号跑出来的曲线在10^-3以后会有抖动,这是正常的蒙特卡洛误差,不要因此怀疑代码写错。
5. 跑通runme.m:路径、版本和验证输出
5.1 先看入口文件,再动参数
压缩包内的文件顺序建议按下表处理:
| 文件 | 作用 | 操作注意 |
|---|---|---|
| runme.m | 主入口脚本 | 先打开,按F5运行 |
| 操作录像0003.avi | 演示完整操作过程 | 慢放观看路径切换和参数变化 |
| fpga&matlab.txt | 常见是MATLAB与FPGA验证的对照说明 | 实现前再读,不用一开始看 |
运行前先把MATLAB当前文件夹切到工程根目录,再在编辑器里打开runme.m。不要直接单独运行子函数文件,因为某些子函数内部没有参数默认值,依赖runme.m在调用前设置好的工作区变量;单独运行会得到“未定义函数或变量”的报错。使用MATLAB 2021a打开时,如果提示某个工具箱缺失,先看runme.m开头是否调用了fskmod、berawgn等通信工具箱函数,再决定安装补全工具箱还是替换成手写函数。
5.2 自动检查路径并验证曲线
为了减少“找不到文件”这类低级问题,我一般会先执行两行检查:
if exist('runme.m', 'file') ~= 2 error('请先用cd命令切换到工程根目录'); end cd(fileparts(which('runme.m')));第一行确认runme.m可见,第二行直接把工作目录切到它所在目录。这样无论从哪里启动MATLAB,都能保证相对路径不跑偏。验证结果时,把仿真误码率曲线和理论曲线画在同一张图上,观察高信噪比区域是否落在理论值的±1dB范围内。如果脚本内部产生5000个符号以下的误码率统计,则允许在10^-4以下有一定抖动;如果整条曲线系统性偏移,优先怀疑信噪比换算或MFSK检测方式选错。
另一个实用技巧是修改M参数后重新运行runme.m,对比M=2、4、8时的频谱图横轴范围和误码率曲线位置。M增大时,频谱图占用带宽明显变宽,而误码率曲线会向左移,这正好对应前述频谱效率下降的结论。操作录像0003.avi如果展示了运行顺序,可以逐帧对照runme.m中参数区在切换M前后的变化,这个观察方法比单纯看曲线更能确认整套MFSK仿真链路是通的。
本文还有配套的精品资源,点击获取