最近一段时间都在学习基于LabWindows/CVI(后文简称CVI)开发模拟软件,由于已有一个不太稳健,但基本框架较为齐备的工程。所以我的工作主要是在这个已有的工程上进行debug、整理修改、开发新功能,从5月开始已经持续了接近三个月。
在之前的开发过程中我就留意到了一点,当在某个具有表格控件与图表控件的子界面传输数据(数据个数约10^6左右)结束后,退出界面时有5-8s左右的卡顿,期间无法进行任何操作。这让我开始对基于CVI开发的这个软件其运行速度有所留意,毕竟谁都希望使用的软件流畅顺滑。
而在实现某个功能完毕,进行模块测试时,发现由于涉及了两重循环,且内部需要进行复数运算(基本都是使用自定义的函数实现的,如求e的复数次方),程序运行速度极慢,10x245000的循环,内部基本为复数运算,在CVI中使用C语言耗费了20s才完成,而按相同的思路(也使用两重循环)完成的程序,使用Matlab仅需要2s,在使用行向量直接运算进行优化后,时间缩短到0.2s。因此我产生了在CVI中调用Matlab以优化复杂运算速度的需求与想法。
有了这个想法后,我开始查阅相关的书籍和论文,然后发现虽然CVI作为一款极you其dian成guo熟shi的软件,但相关的资料并不完整,尤其很难面面俱到某个细分小方向的实现细节。
书籍中较为全面介绍与Matlab混合编程的,是刘君华主编的《基于LabWindows/CVI的虚拟仪器设计》。其4.2节即为《LabWindows/Cvi与Matlab的接口原理与方法》,在我自行实现混合编程的前期,主要参考的就是这个章节的内容。
经历了这个开发过程后,我发现相比书籍而言更有价值的是很多文献资料里都提到而没有进行深入分析的示范案例(位于安装目录\CVI2012\samples\activex\matlab下的工程demoforMATLABinterface)。个人建议直接阅读这个工程中的主文件demoforMATLABinterface.c,研究各函数的使用方法。
当然不只是Matlab的混合编程,Sample文件夹下的例子都值得细细品味学习。
下面我将从ActiveX服务配置、常用函数及开发小结、测试结果三个方面进行总结。
由于一般而言sample中的例子由于未在本机上正常链接CVI与Matlab的接口,故无法直接运行。并且报“没有注册类”错误。这里给出配置本机适用的ActiveX服务并建立一个可执行版本的demoMATLABinterface工程的全过程。目前暂时我还没有发现有相关的教程,各位初学者可以参考一下。
软件开发环境:Matlab 2009a(32-bit),LabWindows/CVI 2012 SP1
系统环境:Windows 7 64位专业版
硬件环境:处理器i7-4790主频3.6GHz,内存16G。
根据多篇论文归纳可知,CVI和Matlab的混合编程有引擎、ActiveX服务、编译器三种主流实现方法,各有优劣。本文主要针对第二种方法进行讲解。
需要着重强调。由于CVI是32位软件(至少2012以及2017均为32-bit),所以使用本方法进行混合编程实现时,需使用32位版本的Matlab,如2009a,否则CVI与Matlab的混合编程无法正常运行。
首先建立空工程,依次选择“Tool——Create ActiveX Controller ”,弹出名为“ActiveX Controller Wizard”的引导,首先是有点基本介绍的欢迎页,点next跳过,然后CVI自动索引所有可以使用的ActiveX Server,直接往下拉,选中”Matlab Application(Version 8.5) Type Library“。
注:CVI软件需在matlab安装完毕后再进行安装,否则CVI无法找到Matlab。若此处出现“the tyoe library is not intended for use on win32”一般原因即为Matlab版本并非32位,由于CVI自身就是32位软件。
进入到下一步后,输入工具前缀名(可以简单理解为所生成的服务控件名称),并选择.fp文件的保存路径。
进入下一步,点击Advanced Option,在弹出窗口中点击“check all”,然后点击“OK”即可,再下一步即完成ActiveX服务控件的生成,空工程中包含了一个名为MLApp.fp的工具文件,而在文件夹中,出现了一个名为msvc的文件夹以及五个名称均为MLApp的文件,扩展名分别为.c .fp .h .obj .sub。
新建一个名为demoMatlab的文件夹,将samples\activex\matlab文件夹中,名为demoforMATLABinterface的.c .cws .h .prj .uir五个文件,以及chirp.m、cvispiral.m、meshgauss.m三个.m文件,名为matlabutil的.c与.h两个文件拷贝到demoMatlab中,此外,将上段段尾提到的四个名为MLApp的文件(除MLApp.c)也拷贝到demoMatlab中。
在CVI中打开prj,由于demoMATLABinterface工程下包含的是名为matlabsrvr7的ActiveX服务,故将matlabsrvr7的.c .h .fp文件全部移除,通过“Edit——Add Files To Project”将四个名为MLApp的文件(除MLApp.c)添加进工程,再进行编译,此时demoforMATLABinterface即可正常运行,正常调用matlab完成各种功能,如下所示。
工程所包含的文件如下图所示。
通过ActiveX服务调用Matlab最重要的地方在于成功建立CVI与Matlab的接口,一般需要在本机上生成对本机有效的服务控件,否则由于不同电脑上CVI以及Matlab版本不同,位数不同,一般情况下均无法直接运行。
而在正常生成服务控件后,将 .fp .h .obj .sub四个文件拷贝至各目标工程所在的文件夹下,并成功添加到工程,即可完成对Matlab的调用。
上文讲解了ActiveX服务的配置。本节主要讲解一下demo中涉及到的一些函数用法注意事项,最后根据我开发相关功能的经历,进行一个小结。
demo的面板如下所示
由图可一目了然,demo通过例子实现了加载Matlab、退出Matlab、退出演示demo、运行Matlab命令、改变Matlab脚本窗口大小、进行FFT运算、矩阵转置、解方程并绘图、发送序列到Matlab、从Matlab中接收序列、运行M文件这几种功能。
下面对其中使用到的函数进行对应的介绍:
1.加载Matlab:MLApp_NewDIMLApp。
由于CVI有一定年头了,很多书籍介绍混合编程时使用的都是较早的版本,加载函数仅有两个参数,故运行在新版本CVI软件中时可能出现“过多输入”的错误,此时需要使用新版的MLApp_NewDIMLApp函数。
该函数在ActiveX服务配置后生成的最底层MLApp.c文件中被定义(再次说明了在本机上第一次使用ActiveX服务时需要对其进行配置的重要性),其在头文件中的声明和使用如下所示。
//声明
HRESULT CVIFUNC MLApp_NewDIMLApp (const char *server, int supportMultithreading,
LCID locale, int reserved,
CAObjHandle *objectHandle);
//使用
stat = MLApp_NewDIMLApp(NULL,1,LOCALE_NEUTRAL,0,&hMatlab); //加载Matlab
if(result != SUCCESS)
{
MessagePopup("警告","Matlab加载出错!");
return 0;
}
函数末尾的hMatlab为全局的Matlab句柄,需提前定义(尤其当你想在其他地方也调用Matlab时)
不仅是该函数,大多数函数均会返回一个状态变量,故一般使用上面这种形式进行编程,以对软件的运行进行更好的控制,当加载错误时,能有效报错,防止软件无故崩溃
2.改变窗口大小:MinMaxMatlab
该函数在maltbutil.c文件中被定义,在最底层的MLApp之上进行进一步封装得到,0表示最小化窗口,1表示最大化窗口。
int MinMaxMatlab(CAObjHandle hMatlab, int minmaxFlag)
MinMaxMatlab(hMatlab,0);
3.退出Matlab脚本:MLApp_DIMLAppQuit
该函数在MLApp.c文件中被定义,也可以对其进行进一步的封装。
HRESULT CVIFUNC MLApp_DIMLAppQuit (CAObjHandle objectHandle,
ERRORINFO *errorInfo);
stat = MLApp_DIMLAppQuit (*hMatlab, NULL);
4.运行Matlab命令:RunMatlabCommand
该函数在maltbutil.c文件中被定义,仅可执行Matlab内部函数相关的命令,无法执行自定义函数命令,否则会报“undefined function or method”错误。
int RunMatlabCommand(CAObjHandle hMatlab, char *command)
result = RunMatlabCommand(hMatlab,"mMatrix=inv(cMatrix);");
if (result != SUCCESS)
{
MessagePopup ("ERROR", "Error in sending command to MATLAB");
return 0;
}
涉及的变量mMatrix与cMatrix无需在CVI中进行声明。
5.发送/接收字符串:SendString/GetString
该函数在maltbutil.c文件中被定义,由于Matlab不支持BSTRs,故发送接收过程中需要使用Fmt函数进行字符串与双精度浮点数的转换。
Fmt函数示例如下:
Fmt(CVIString,"%s);
Fmt(command,"%s<%s=transpose(%s)",matStringName,matStringName);
Fmt(command,"%s<%s=char(%s)",matStringName,matStringName);
发送接收函数如下所示
int SendString(CAObjHandle hMatlab, char *matStringName, char *CVIString)
int GetString(CAObjHandle hMatlab, char *matStringName, char **cString)
result = SendString(hMatlab, "matStr", CVIString);
result = GetString(hMatlab,"matStr",&cStr);
demo给出的例子是,将“Hello MATLAB”字符串转换为双精度浮点数,变量名为CVIString,传输到Matlab中,在Matlab中的名称为matStr,而后从Matlab中获得这个字符串,变量名为cStr。
在CVI程序中,matStr无需声明,然而CVIString与cStr是需要声明的,形式是一维数组。
6.发送/接收矩阵
同5,demo矩阵的传输演示是将一个2x2的矩阵传入Matlab,进行转置运算后接收回来。
int SendMatrix(CAObjHandle hMatlab, char *matlabName, double *matrixReal,
double *matrixImag, size_t dim1, size_t dim2)
int GetMatrix(CAObjHandle hMatlab, char *matlabName, double **matrixReal,
double **matrixImag, size_t *dim1, size_t *dim2)
result = SendMatrix(hMatlab,"cMatrix",(double*)matrix_r,(double*)matrix_i,2,2);
result = GetMatrix(hMatlab,"mMatrix",&matrixReal,&matrixImag,&dim1,&dim2);
传输变量包括Matlab句柄,在Matlab中的名称,矩阵实部数值(也是矩阵),矩阵虚部数值(也是矩阵),行数,列数。其中没有虚部或者实部时可以用NULL表示。
同4,mMatrix与cMatrix无需在CVI中进行声明。但发送矩阵的实部数值matrix_r与虚部数值matrix_i需要在CVI中声明,形式是二维数组。接收矩阵的实部数值matrixReal和虚部数值matrixImag为初始化为NULL的指针,也需要提前声明,行数列数也需要提前确定。
7.运行M文件脚本:RunMatlabScript
该函数在matlabutil中定义。
int RunMatlabScript(CAObjHandle hMatlab, char *mFilePath)
result = RunMatlabScript(hMatlab,"G:\\sig_gen\\demo_0728\\create_chaffsig.m");
在设定路径时,Windows下的分隔符需使用\\以强制转义,否则会使得路径解析出错,另外由于函数中对路径长度以及文件名长度最大值仅设为256,故运M文件脚本不可设置过深,以防止无法解析。
另外还可利用FileSelectPopupEx等系统相关的函数,实现选择m文件运行的操作,这里不再赘述。
若要调用自定义函数,建议采用以下两种方式:
1.不足5行的函数,直接使用RunMatlabCommand,拆分为多个语句进行代替。
2.程序较长,但不存在额外调用自定义函数的情况(额外调用自定义函数指:例如本打算调用自定义函数A,A函数中又使用了自定义函数B),则在Matlab中充分测试A函数后,去掉首行的声明(即function [输出变量] = 函数名(输入变量))以及末尾的”end”,将所需要的输入变量提前构成数组,直接传入Matlab,再在Matlab中还原为各参数变量。如下所示:
在CVI中将变量构成数组
m_out_data[0] = fc;
m_out_data[1] = fs;
m_out_data[2] = (double)m;
m_out_data[3] = TotalTime*(1e-6);
在M文件头部还原
f0 = m_out_data(1);
fs = m_out_data(2);
m = m_out_data(3);
T = m_out_data(4);
完成运算后再将所需要的数据导出,由于一般调用Matlab的原因是C语言计算信号的各种变换较慢,所以所需数据一般也为行向量,可以用1xN的矩阵表示。
调用Matlab时,需要在CVI代码中首先加载Matlab,其次具有一定的技巧地使用自定义函数,最后调用完成后需关闭Matlab避免拖累系统。
在同一计算机上进行相同计算任务,
按上文的思路,CVI使用原方法完成50*245000个循环,耗时100s左右。
Matlab环境下运行(也是两重循环),耗时60s
Matlab改为单循环时,耗时0.0648s
CVI与Matlab进行混合编程后,耗时不足2s。
由上可知,混合编程成功地优化了功能,体现了混合编程的优越性。
后续打算对另外两种调用方法,以及通过修改Matlab注册码使用ActiveX服务三个方面进行研究,并尽快完善本文。
参考资料
刘君华等,《基于LabWindows/CVI的虚拟仪器设计》,电子工业出版社
LabWindows/CVI 自带的Sample
本文所述工程已上传到Github,点我跳转本文实例代码
如有疑问,欢迎留言。