十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Matlab与MFC的MFSK调制解调仿真与可视化实现

基于Matlab与MFC的MFSK调制解调仿真与可视化实现 简介一套基于Matlab的MFSK多进制频移键控水声通信仿真代码面向通信工程、信号处理领域的学生与研究人员可辅助理解MFSK调制原理并用于水下信道中抗噪声、抗多径性能的初步实验与算法改进。资源共11个文件全部为.m格式的Matlab源码压缩包仅2KB轻量且重点突出方便快速阅读和二次开发。仿真链路设计完整主程序负责整体流程控制编码模块完成二进制到符号的映射频率选择模块决定各载波频率接收端包含同步检测、符号判决和误码率统计另有多径效应模拟、同步参考信号添加等函数能够较真实地还原水声通信中的关键影响因素。代码模块划分清晰变量命名直观适合作为相关课程设计、毕业设计或科研预研的参考基线。目前已有359人学习浏览对希望入门MFSK水声通信仿真的读者具有一定借鉴价值。1. 三种技术堆在一起是想解决什么问题MFSKM进制频移键控是数字通信里很经典的一类调制方式但现实工作中你很少只拿Matlab画一条误码率曲线就交差。一旦算法要进桌面工具、进实验演示、配合硬件采集就得有一个带界面的程序去显示波形、调参数、看解调结果这时候MFCMicrosoft Foundation Classes就重新获得了意义。这篇博文要讲的不是“Matlab比MFC好”或“MFC比Matlab好”而是把三者串成一条链路在Matlab里把MFSK调制解调跑通让MFC对话框按同样的参数把信号画出来再通过文件或Engine接口在两者之间交换数据。适合正在做通信类课程设计、数字调制仿真平台或者手里有一批Matlab脚本想快速变成Windows桌面工具的工程师。2. MFSK调制的数学基础与Matlab最小仿真2.1 频率选择法生成MFSK信号的三个关键参数MFSK的原理一句话能说完每个符号周期内用M个可能频率中的一个代表log2(M)比特。M4时一个符号对应2比特M8对应3比特。信号表达式一般写成s_i(t) A * cos(2π (f_c i * Δf) t)i 0, 1, …, M-10 ≤ t ≤ T_s其中f_c是载波中心频率Δf是相邻频率间隔T_s是符号周期。工程上真正需要反复斟酌的只有三个参数符号速率R_s决定T_s、频率间隔Δf、采样率f_s。Δf如果太小非相干解调时相邻频率的包络会重叠误码率明显抬升Δf如果太大占用带宽会浪费。常见做法是取Δf k / T_sk取1或2。对于非相干包络检测k1是理论上的最小正交间隔但工程上为了对抗频偏经常直接取k2。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(idx1, :) 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(i1) 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(i1)保存的是第i个频率上的相关能量max返回的最大值位置减1就得到发射符号。若把这段代码跑通后往rx上加高斯白噪声错误数会随信噪比变化这就是误码率仿真的雏形。2.2.2 相干解调要注意的正交性问题相干解调需要接收端恢复出与发射端同步的载波相位计算M个相关器的输出并取最大。理论上当频率间隔为1/Ts的整数倍时M个频率在一个符号周期内两两正交相干检测没有相互串扰。实际仿真里最容易翻车的地方是采样率设置Fs除以Rs如果不是整数t_symbol里的点数会在符号边界处错位导致正交性被破坏。下表给出一组能直接复用的参数搭配覆盖M2、4、8三种常见场景M每个符号比特数符号速率 Rs频率间隔 df采样率 FsFs/Rs21100200800080421002008000808310020016000160注意最后一列的比例就是每符号采样点数保持整数是让Matlab的reshape和MFC的绘制代码都不需要处理跨符号边界的根本前提。把Fs从8000改成7500Fs/Rs75仍是整数也可以但会出现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::vectordouble m_waveData保存整个时域波形采样int m_sps保存每符号采样点数double m_fc和double 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); }xScale、yScale这两行是整段绘制的比例基础窗口拉伸时波形随之缩放不需要额外处理分辨率适配。yOffset把零电平放在窗口垂直正中符合查看基带信号的直觉。代码里逐点画LineTo在采样点很多时效率一般但用于查看几千个采样点完全够如果数据量超过十万点再考虑抽点或分段绘制。3.2.2 绘制符号网格和频率标注只看波形很难确认MFSK到底切了几个频率。在波形图上方叠加垂直网格线每根线代表一个符号边界能直观对照Matlab里的t_symbol分段。符号边界位置很好算第n个符号的右边界在x 10 (int)(n * m_sps * xScale)。把这些线画成浅灰色与蓝色波形区分开代码在OnPaint里插在绘制波形之前即可。频率标注的做法是把纵轴左侧留出区域画几条水平虚线分别标记fc - df、fc、fc 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_s和AfxExtractSubString在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/include和extern/lib/win64/microsoft。链接时还需要把libeng.lib、libmx.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设计成能除尽的数如Fs8000、Rs100、sps80保真度最高。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(i1, j1) 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的包络和频率跳变没有影响。真正要精细看局部再提供按住鼠标左键框选放大把那一段重新映射到整个宽度并重绘。这个框选交互只涉及OnLButtonDown、OnMouseMove、OnLButtonUp三个消息比滚动条体验自然得多。把m_viewStart和m_viewEnd两个成员变量引入OnPaint的x轴映射就把全量折线替换成局部缩放绘制代码改动量控制在二十分钟以内。最后建议把上面的验证方法统一在Debug构建里用断言检查m_waveData.size()与参数文件的一致性因为文件桥接类问题最容易在Release下静默出错早断言比晚弹窗更省时间。本文还有配套的精品资源点击获取
返回列表