基于Matlab与MFC的MFSK调制解调仿真与可视化实现
2026/9/13 14:57:10 网站建设 项目流程

简介:一套基于Matlab的MFSK(多进制频移键控)水声通信仿真代码,面向通信工程、信号处理领域的学生与研究人员,可辅助理解MFSK调制原理,并用于水下信道中抗噪声、抗多径性能的初步实验与算法改进。资源共11个文件,全部为.m格式的Matlab源码,压缩包仅2KB,轻量且重点突出,方便快速阅读和二次开发。仿真链路设计完整:主程序负责整体流程控制,编码模块完成二进制到符号的映射,频率选择模块决定各载波频率,接收端包含同步检测、符号判决和误码率统计,另有多径效应模拟、同步参考信号添加等函数,能够较真实地还原水声通信中的关键影响因素。代码模块划分清晰,变量命名直观,适合作为相关课程设计、毕业设计或科研预研的参考基线。目前已有359人学习浏览,对希望入门MFSK水声通信仿真的读者具有一定借鉴价值。

1. 三种技术堆在一起,是想解决什么问题

MFSK(M进制频移键控)是数字通信里很经典的一类调制方式,但现实工作中你很少只拿Matlab画一条误码率曲线就交差。一旦算法要进桌面工具、进实验演示、配合硬件采集,就得有一个带界面的程序去显示波形、调参数、看解调结果,这时候MFC(Microsoft Foundation Classes)就重新获得了意义。这篇博文要讲的不是“Matlab比MFC好”或“MFC比Matlab好”,而是把三者串成一条链路:在Matlab里把MFSK调制解调跑通,让MFC对话框按同样的参数把信号画出来,再通过文件或Engine接口在两者之间交换数据。适合正在做通信类课程设计、数字调制仿真平台,或者手里有一批Matlab脚本想快速变成Windows桌面工具的工程师。

2. MFSK调制的数学基础与Matlab最小仿真

2.1 频率选择法生成MFSK信号的三个关键参数

MFSK的原理一句话能说完:每个符号周期内,用M个可能频率中的一个代表log2(M)比特。M=4时一个符号对应2比特,M=8对应3比特。信号表达式一般写成:

s_i(t) = A * cos(2π (f_c + i * Δf) t),i = 0, 1, …, M-1,0 ≤ t ≤ T_s

其中f_c是载波中心频率,Δf是相邻频率间隔,T_s是符号周期。工程上真正需要反复斟酌的只有三个参数:符号速率R_s(决定T_s)、频率间隔Δf、采样率f_s。Δf如果太小,非相干解调时相邻频率的包络会重叠,误码率明显抬升;Δf如果太大,占用带宽会浪费。常见做法是取Δf = k / T_s,k取1或2。对于非相干包络检测,k=1是理论上的最小正交间隔,但工程上为了对抗频偏,经常直接取k=2。

Matlab里用频率选择法生成MFSK信号,不需要查表或调用通信工具箱,直接用数值振荡器就能写。下面的代码生成一个4FSK基带信号,参数全部放在脚本顶部方便改:

%% 4FSK调制:频率选择法 clear; clc; M = 4; % 进制数 fc = 1000; % 载波中心频率 Hz Fs = 8000; % 采样率 Hz Rs = 100; % 符号速率 Baud Ts = 1/Rs; % 符号周期 df = 2/Ts; % 频率间隔,取2/Ts留保护带 t_symbol = 0 : 1/Fs : Ts - 1/Fs; % 单个符号内的采样时间点 Nsym = 20; % 发送符号个数 data = randi([0 M-1], 1, Nsym); % 随机生成M进制符号 % 预生成M个频率的波形 waveforms = zeros(M, length(t_symbol)); for idx = 0 : M-1 waveforms(idx+1, :) = cos(2*pi*(fc + idx*df) * t_symbol); end % 按数据拼接发射信号 tx = []; for n = 1 : Nsym tx = [tx, waveforms(data(n)+1, :)]; end

代码里有一个容易被忽略的细节:waveforms矩阵一次性生成M个符号波形,随后按数据索引拼接。这样做比在每个符号循环里反复计算cos快得多,也方便后续在MFC里对照实现同一张查找表。df = 2/Ts不是必须,改成1/Ts就可以观察到相邻频率的串扰变大,这是调参时最值得对比的一组实验。

2.2 相干与非相干的解调路径怎么选

2.2.1 包络检测:免载波同步的工程选择

非相干解调对每个候选频率做带通滤波后取包络,比较M路包络的大小,最大的那一路就是判决结果。它不需要恢复载波相位,抗频偏能力也强,缺点是误码率比相干解调差一点。工程里大量FSK接收机(包括很多低功耗无线芯片)都采用这条路。

Matlab里包络检测可以借助FFT近似实现,也可以直接乘同频正弦再低通滤波。对4FSK这种符号速率不高的情况,最简单稳定的是“相关+包络”方法:将接收信号与每个频率的本地载波相乘,通过低通滤波器提取直流分量,直流最大的就是判决输出:

%% 非相干包络检测(无噪声版本,演示判决逻辑) lpf_coeff = ones(1, round(Fs/Ts)); % 移动平均低通,窗口 = 一个符号周期 lpf_coeff = lpf_coeff / sum(lpf_coeff); rx = tx; % 先不带噪声 % 把接收信号重新排列成 Nsym × samples_per_symbol 的矩阵 sps = length(t_symbol); rx_mat = reshape(rx, sps, Nsym).'; % 每一行是一个符号的采样 decision = zeros(1, Nsym); for n = 1 : Nsym env = zeros(1, M); for i = 0 : M-1 local = cos(2*pi*(fc + i*df) * t_symbol); % 本地参考载波 mixed = rx_mat(n, :) .* local; env(i+1) = abs(sum(mixed)); % 积分相当于低通滤波 end [~, decision(n)] = max(env); decision(n) = decision(n) - 1; end err = sum(decision ~= data); fprintf('符号错误数:%d / %d\n', err, Nsym);

这里没有用filter而是直接用sum(mixed),是因为一个符号周期内的积分就是移动平均在符号末尾的输出值。注意reshape之前要保证rx的长度正好是Nsym * sps,否则会报错。env(i+1)保存的是第i个频率上的相关能量,max返回的最大值位置减1就得到发射符号。若把这段代码跑通后往rx上加高斯白噪声,错误数会随信噪比变化,这就是误码率仿真的雏形。

2.2.2 相干解调要注意的正交性问题

相干解调需要接收端恢复出与发射端同步的载波相位,计算M个相关器的输出并取最大。理论上当频率间隔为1/Ts的整数倍时,M个频率在一个符号周期内两两正交,相干检测没有相互串扰。实际仿真里最容易翻车的地方是采样率设置:Fs除以Rs如果不是整数,t_symbol里的点数会在符号边界处错位,导致正交性被破坏。

下表给出一组能直接复用的参数搭配,覆盖M=2、4、8三种常见场景:

M每个符号比特数符号速率 Rs频率间隔 df采样率 FsFs/Rs
21100200800080
42100200800080
8310020016000160

注意最后一列的比例就是每符号采样点数,保持整数是让Matlab的reshape和MFC的绘制代码都不需要处理跨符号边界的根本前提。把Fs从8000改成7500(Fs/Rs=75,仍是整数)也可以,但会出现75个采样点对应一个非整数的符号持续时长,后面做时间轴标注会麻烦许多。

3. 用基于MFC的对话框工程把波形画出来

3.1 让控制台程序支持MFC或直接建对话框工程

Matlab负责算法验证,MFC负责可视化。两者之间的边界要划清楚:不要在MFC里重写调制解调的全部数学推导,只让它做三件事——读参数、画波形、显示判决结果。工程建立在VS里建议直接创建“MFC对话框应用”,如果已经有控制台工程,也可以在项目属性里把“使用MFC”从“使用标准Windows库”改为“在共享DLL中使用MFC”,但这只解决库链接问题,消息循环、窗口类这些还是得自己补。

对话框界面上放几个必要的控件:一个绘制波形的区域(自定义CStatic子类或直接响应OnPaint)、一个“打开CSV”按钮、一个“开始解调”按钮、一个显示误码结果和参数摘要的Edit或ListBox。控件布局要考虑“mfc 控件自适应屏幕分辨率”的问题,简单做法是重写对话框的OnSize,拿GetClientRect的比例去MoveWindow绘图区域,不要让波形图固定成像素尺寸。

画波形最核心的成员变量是三个:std::vector<double> m_waveData保存整个时域波形采样,int m_sps保存每符号采样点数,double m_fcdouble m_df保存载波频率和频率间隔。这三个变量和Matlab脚本里的同名变量一一对应,直接从CSV读取后填充。

3.2 用GDI+绘制时域波形与频率网格

3.2.1 OnPaint里的双缓冲绘制

波形绘制不建议直接在OnPaint里逐点调用SetPixel,屏幕会闪到没法看。先创建一个内存位图,把所有线条画到内存DC,再一次BitBlt到窗口。下面是一段可以在对话框OnPaint里直接用的代码:

void CMfskDlg::OnPaint() { CPaintDC dc(this); CRect rc; GetClientRect(&rc); CDC memDC; memDC.CreateCompatibleDC(&dc); CBitmap bmp; bmp.CreateCompatibleBitmap(&dc, rc.Width(), rc.Height()); CBitmap* pOld = memDC.SelectObject(&bmp); // 背景 memDC.FillSolidRect(&rc, RGB(255, 255, 255)); if (m_waveData.size() > 1) { // 横轴映射:整个窗口宽度对应全部采样点 double xScale = (double)(rc.Width() - 20) / (double)m_waveData.size(); double yOffset = rc.Height() / 2.0; double yScale = (double)(rc.Height() - 40) / 2.0; // 计算最大幅度用于归一化 double maxAmp = 0.0; for (double v : m_waveData) { if (fabs(v) > maxAmp) maxAmp = fabs(v); } if (maxAmp < 1e-10) maxAmp = 1.0; CPen pen(PS_SOLID, 1, RGB(20, 80, 160)); CPen* pOldPen = memDC.SelectObject(&pen); for (size_t i = 1; i < m_waveData.size(); i++) { int x1 = 10 + (int)((i - 1) * xScale); int y1 = (int)(yOffset - (m_waveData[i-1] / maxAmp) * yScale); int x2 = 10 + (int)(i * xScale); int y2 = (int)(yOffset - (m_waveData[i] / maxAmp) * yScale); memDC.MoveTo(x1, y1); memDC.LineTo(x2, y2); } memDC.SelectObject(pOldPen); } dc.BitBlt(0, 0, rc.Width(), rc.Height(), &memDC, 0, 0, SRCCOPY); memDC.SelectObject(pOld); }

xScaleyScale这两行是整段绘制的比例基础,窗口拉伸时波形随之缩放,不需要额外处理分辨率适配。yOffset把零电平放在窗口垂直正中,符合查看基带信号的直觉。代码里逐点画LineTo在采样点很多时效率一般,但用于查看几千个采样点完全够;如果数据量超过十万点,再考虑抽点或分段绘制。

3.2.2 绘制符号网格和频率标注

只看波形很难确认MFSK到底切了几个频率。在波形图上方叠加垂直网格线,每根线代表一个符号边界,能直观对照Matlab里的t_symbol分段。符号边界位置很好算:第n个符号的右边界在x = 10 + (int)(n * m_sps * xScale)。把这些线画成浅灰色,与蓝色波形区分开,代码在OnPaint里插在绘制波形之前即可。

频率标注的做法是把纵轴左侧留出区域,画几条水平虚线,分别标记fc - dffcfc + df对应的幅值。注意MFC默认坐标系y轴向下,频率高不代表坐标值大,绘图时要用y = yOffset - (amplitude / maxAmp) * yScale把幅值转换成像素坐标。字体用CreateFont指定_T("Consolas")12号,输出fc %d Hz类似文本即可。

4. Matlab与MFC的数据桥接和参数对齐

4.1 用CSV作为中间交换格式的写法

Matlab和MFC之间最常见、也最不容易引入环境依赖的桥接方式是CSV文件。Matlab负责把调制好的波形、符号数据、参数表写出去,MFC工程用CStdioFile按行读取。比起MAT文件,CSV的优点是MFC侧不需要任何Matlab运行库;比起二进制文件,CSV又能直接用记事本查错。

Matlab端输出代码只要两行:

% 把发送波形和符号数据写到一个CSV,第一行是参数头 params = [M, fc, df, Fs, Rs, Nsym]; writematrix([tx; data], 'mfsk_wave.csv'); % 再写一个参数文件,方便MFC不用猜 writematrix(params, 'mfsk_params.csv');

这里把矩阵写成两行:第一行是波形采样tx,第二行是对应的符号data。写完后CSV的列数等于总采样点数加,如果Nsym变化,列数也随之变化。MFC读取时先按参数文件里的Nsym和sps计算总长度,再逐行解析。更稳妥的做法是把参数放在同一文件第一行,这样只用一个文件就能完全还原参数与波形。

MFC侧读取CSV的代码:

BOOL CMfskDlg::LoadCsvWave(const CString& strFile) { CStdioFile file; if (!file.Open(strFile, CFile::modeRead)) { AfxMessageBox(_T("打开CSV失败")); return FALSE; } CString strLine; file.ReadString(strLine); // 第一行:M,fc,df,Fs,Rs,Nsym int M, Nsym; double fc, df, Fs, Rs; _stscanf_s(strLine, _T("%d,%lf,%lf,%lf,%lf,%d"), &M, &fc, &df, &Fs, &Rs, &Nsym); m_waveData.clear(); m_symbolData.clear(); while (file.ReadString(strLine)) { // 每行可能是波形,也可能是符号序列,按逗号切分 int pos = 0; CString token; while (AfxExtractSubString(token, strLine, pos++, _T(','))) { m_waveData.push_back(_ttof(token)); if (m_waveData.size() >= (size_t)(Nsym * Fs / Rs)) break; } } m_sps = (int)(Fs / Rs); m_fc = fc; m_df = df; file.Close(); return TRUE; }

读取时的关键点是必须依赖参数文件里的Nsym和Fs/Rs来控制循环终止条件。直接读到文件尾会混入第二行符号数据,导致波形数据点数量错误。另一个隐患是_stscanf_sAfxExtractSubString在Unicode工程里的字符集差异:工程属性如果设置成Unicode,这两处需要统一使用宽字符版本,否则中文路径或负数会解析失败。检查此类问题不靠断点,直接看m_waveData.size()是否等于Nsym * Fs / Rs是效率最高的手段。

4.2 用Matlab Engine替代文件交换的适用场景

文件交换适合“跑完仿真再打开工具看结果”的离线流程,而如果你在MFC按钮里点击“仿真”,希望Matlab实时把波形和误码率传回来,就应该用MATLAB Engine API。典型做法是MFC作为客户端,调用engOpen(NULL)启动一个Matlab进程,用engEvalString执行.m脚本,再engGetVariable取回mxArray。

#include "engine.h" Engine* pEng = engOpen(NULL); if (pEng == NULL) { AfxMessageBox(_T("启动Matlab Engine失败")); return; } engEvalString(pEng, "run('C:/work/mfsk_sim.m')"); mxArray* pResult = engGetVariable(pEng, "tx"); if (pResult != NULL) { double* pData = mxGetPr(pResult); size_t n = mxGetNumberOfElements(pResult); m_waveData.assign(pData, pData + n); mxDestroyArray(pResult); } engClose(pEng);

这套方案能跑的前提是开发机上装了完整Matlab,且MFC工程“附加包含目录”和“附加库目录”指向Matlab安装目录下的extern/includeextern/lib/win64/microsoft。链接时还需要把libeng.liblibmx.lib写进“附加依赖项”。注意Engine API每次调用都有进程启动和变量拷贝开销,适合按按钮手动触发,不适合在OnPaint或定时器里高频调用。如果你的MFC程序需要转发给其他没有Matlab的机器跑,这条路就直接堵死,只能退回文件交换或把算法翻译成C++。

另外还有一种折中:把Matlab算法直接编译成mex或C++共享库,由MFC调用。“matlab怎么运行c++程序”的反向需求在这里正好对上,但要注意编译出的库同样依赖Matlab运行库,部署时文件更多,黑盒程度更高,对参数可视化调试反而不利。

4.3 参数对齐:Matlab与MFC必须一致的五个数值

桥接之后最容易出的问题不是程序崩溃,而是Matlab里看到的波形和MFC里画出来的波形长相完全不同。原因几乎总是出在下面这张表:

参数Matlab变量MFC成员变量不一致时的现象
采样率Fsm_fs时间轴变快或变慢
符号周期Ts = 1/Rsm_sps = Fs/Rs符号网格与波形错位
载波频率fcm_fc波形零交叉频率不同
频率间隔dfm_df频率跳变幅度不同
每符号采样点数spsm_sps网格线数量错误

有一个很容易被忽略的坑是浮点精度:Matlab里df = 2/Ts如果Ts是一个无限小数,写入CSV后再用_ttof读回,数值会与Matlab内存中的值差一个极小量。这个误差在数千个采样点上看不出影响,但当MFSK符号数变大到上万,相位偏差累积会导致解调判决错误增加。解决办法是在Matlab写出时用sprintf把参数保留12位小数,或者干脆把Ts设计成能除尽的数(如Fs=8000、Rs=100、sps=80,保真度最高)。

5. 把MFSK代码推向工程可用的三个检验技巧

5.1 误码率曲线与理论曲线放在同一张图里

调制代码能跑,不代表解调端的判决边界是对的。最快验证方式是让Matlab把仿真的误码率点和理论误码率曲线画在同一张图上,偏差超过0.5个dB就要回头查实现。

%% 误码率仿真:逐信噪比累加错误符号 EbN0_dB = 0 : 2 : 14; pe = zeros(size(EbN0_dB)); for k = 1 : length(EbN0_dB) snr = EbN0_dB(k) + 10*log10(log2(M)); % 符号信噪比 errors = 0; for trial = 1 : 50 noise = randn(1, length(tx)) * sqrt(10^(-snr/10)); % 对带噪声信号做包络检测,累加错误符号 % errors = errors + ... end pe(k) = errors / (Nsym * 50); end

对于非相干MFSK,理论误符号率可以用berawgn(EbN0_dB, 'fsk', M, 'noncoherent')直接拉出来,这里不展开。注意仿真中每轮trial都要重新生成符号和噪声,固定符号集会让随机性不足,相邻信噪比点的曲线抖动过大。仿真点数建议每轮不低于1000个符号;如果运行时间过长,减少trial次数比减少符号数更划算。

5.2 判断频率间隔是否踩到正交红线的自查方法

不需要上仪器,直接用Matlab看的办法是检查M个频率上的自相关矩阵。对角线元素是1,非对角线如果远小于对角线,说明正交性良好。将这个矩阵在MFC里画成热力图(用COLORREF按数值映射RGB),能直观看到df过小时的非对角线泄漏。

covM = zeros(M, M); for i = 0 : M-1 for j = 0 : M-1 covM(i+1, j+1) = abs(sum(exp(1j*2*pi*(i-j)*df*t_symbol))); end end

这个矩阵与接收端相关器的输出直接对应。若df取1/Ts,非对角线元素接近0但仍有细小残留,这是矩形符号窗的频谱旁瓣导致的。若df取2/Ts,非对角值会进一步压低一个量级。在MFC工具栏里加一个ComboBox让用户切df,加载CSV后自动重算矩阵,并把归一化数值显示在状态栏,这个交互对讲课和验收都非常直观。

5.3 MFC波形显示的高频采样点性能处理

当Ts很小、Fs很大时,m_waveData动辄几十万点,OnPaint里逐点LineTo会卡顿到无法拖动窗口。处理方法不是上双缓冲和硬件加速,而是先用抽点算法,把屏幕像素宽度对应的波形点数量降下来:

size_t step = max(1, (size_t)(m_waveData.size() / (rc.Width() * 2))); for (size_t i = 0; i < m_waveData.size(); i += step) { // 只对i处的点画线 }

step动态跟随窗口宽度变化,放大窗口自动密一点,缩小窗口自动疏一点。抽点会丢失极小尖峰,对看MFSK的包络和频率跳变没有影响。真正要精细看局部,再提供按住鼠标左键框选放大,把那一段重新映射到整个宽度并重绘。这个框选交互只涉及OnLButtonDownOnMouseMoveOnLButtonUp三个消息,比滚动条体验自然得多。把m_viewStartm_viewEnd两个成员变量引入OnPaint的x轴映射,就把全量折线替换成局部缩放绘制,代码改动量控制在二十分钟以内。最后建议把上面的验证方法统一在Debug构建里用断言检查m_waveData.size()与参数文件的一致性,因为文件桥接类问题最容易在Release下静默出错,早断言比晚弹窗更省时间。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询