第5章 CHAPTER 5 动态链接库封装和调用 LabVIEW项目开发时,经常会遇到仪器设备控制问题,对于NI提供的仪器设备,可以通过安装相关的开发工具包来解决; 对于非NI提供的设备,可以通过调用动态链接库(Dynamic Link Library,DLL)的方式来解决。本章将以RTLSDR的LabVIEW接口函数为例,介绍LabVIEW中动态链接库的封装和调用方法。 5.1RTLSDR接口函数的封装 LabVIEW通过调用RTLSDR的接口函数实现对RTLSDR的控制。常用的接口函数有open函数、set sample rate函数、set center freq函数、read sync函数和close函数。3.2.2节详细介绍了RTLSDR常用接口函数。3.2.3节通过数据采集实例介绍了RTLSDR接口函数的使用方法。 打开RTLSDR接口函数的程序框图,可以看到,LabVIEW是通过调用库函数(Call Library Function,CLF)模块调用动态链接库rtlsdr.dll的方式实现设备控制。下面将介绍动态链接库和LabVIEW接口函数的封装方法。 5.1.1动态链接库简介 动态链接库(DLL)是Windows操作系统实现共享函数库的一种方式。在Windows操作系统中使用DLL,能够使应用程序代码变得更简洁,计算机内存资源的使用效率变得更高。 RTLSDR的应用程序接口(Application Programming Interface,API)函数是采用C语言编写的。通过调用RTLSDR的动态链接库rtlsdr.dll,上层SDRsharp、LabVIEW、MATLAB/Simulink和GNURadio等应用软件就可以控制RTLSDR设备,如图51所示。 图51RTLSDR接口函数 动态链接库rtlsdr.dll实现了RTLSDR的所有接口函数。RTLSDR源程序中的头文件rtlsdr.h对所有的接口函数作了定义。 #ifndef __RTL_SDR_H #define __RTL_SDR_H #ifdef __cplusplus extern "C" { #endif #include <stdint.h> #include <rtl-sdr_export.h> typedef struct rtlsdr_dev rtlsdr_dev_t; RTLSDR_API uint32_t rtlsdr_get_device_count(void); RTLSDR_API const char* rtlsdr_get_device_name (uint32_t index); RTLSDR_API int rtlsdr_get_device_usb_strings (uint32_t index, char *manufact, char *product, char *serial); RTLSDR_API int rtlsdr_get_index_by_serial (const char *serial); RTLSDR_API int rtlsdr_open (rtlsdr_dev_t **dev, uint32_t index); RTLSDR_API int rtlsdr_close (rtlsdr_dev_t *dev); RTLSDR_API int rtlsdr_set_xtal_freq (rtlsdr_dev_t *dev, uint32_t rtl_freq, uint32_t tuner_freq); RTLSDR_API int rtlsdr_get_xtal_freq (rtlsdr_dev_t *dev, uint32_t *rtl_freq, uint32_t *tuner_freq); RTLSDR_API int rtlsdr_get_usb_strings (rtlsdr_dev_t *dev, char *manufact, char *product, char *serial); RTLSDR_API int rtlsdr_write_eeprom (rtlsdr_dev_t *dev, uint8_t *data, uint8_t offset, uint16_t len); RTLSDR_API int rtlsdr_read_eeprom (rtlsdr_dev_t *dev, uint8_t *data, uint8_t offset, uint16_t len); RTLSDR_API int rtlsdr_set_center_freq (rtlsdr_dev_t *dev, uint32_t freq); RTLSDR_API uint32_t rtlsdr_get_center_freq (rtlsdr_dev_t *dev); RTLSDR_API int rtlsdr_set_freq_correction (rtlsdr_dev_t *dev, int ppm); RTLSDR_API int rtlsdr_get_freq_correction (rtlsdr_dev_t *dev); enum rtlsdr_tuner { RTLSDR_TUNER_UNKNOWN = 0, RTLSDR_TUNER_E4000, RTLSDR_TUNER_FC0012, RTLSDR_TUNER_FC0013, RTLSDR_TUNER_FC2580, RTLSDR_TUNER_R820T, RTLSDR_TUNER_R828D }; RTLSDR_API enum rtlsdr_tuner rtlsdr_get_tuner_type (rtlsdr_dev_t *dev); RTLSDR_API int rtlsdr_get_tuner_gains (rtlsdr_dev_t *dev, int *gains); RTLSDR_API int rtlsdr_set_tuner_gain (rtlsdr_dev_t *dev, int gain); RTLSDR_API int rtlsdr_set_tuner_bandwidth (rtlsdr_dev_t *dev, uint32_t bw); RTLSDR_API int rtlsdr_get_tuner_gain (rtlsdr_dev_t *dev); RTLSDR_API int rtlsdr_set_tuner_if_gain (rtlsdr_dev_t *dev, int stage, int gain); RTLSDR_API int rtlsdr_set_tuner_gain_mode (rtlsdr_dev_t *dev, int manual); RTLSDR_API int rtlsdr_set_sample_rate (rtlsdr_dev_t *dev, uint32_t rate); RTLSDR_API uint32_t rtlsdr_get_sample_rate (rtlsdr_dev_t *dev); RTLSDR_API int rtlsdr_set_testmode (rtlsdr_dev_t *dev, int on); RTLSDR_API int rtlsdr_set_agc_mode (rtlsdr_dev_t *dev, int on); RTLSDR_API int rtlsdr_set_direct_sampling (rtlsdr_dev_t *dev, int on); RTLSDR_API int rtlsdr_get_direct_sampling (rtlsdr_dev_t *dev); RTLSDR_API int rtlsdr_set_offset_tuning (rtlsdr_dev_t *dev, int on); RTLSDR_API int rtlsdr_get_offset_tuning (rtlsdr_dev_t *dev); RTLSDR_API int rtlsdr_reset_buffer (rtlsdr_dev_t *dev); RTLSDR_API int rtlsdr_read_sync (rtlsdr_dev_t *dev, void *buf, int len, int *n_read); typedef void(*rtlsdr_read_async_cb_t) (unsigned char *buf, uint32_t len, void *ctx); RTLSDR_API int rtlsdr_wait_async (rtlsdr_dev_t *dev, rtlsdr_read_async_cb_t cb, void *ctx); RTLSDR_API int rtlsdr_read_async (rtlsdr_dev_t *dev, rtlsdr_read_async_cb_t cb, void *ctx, uint32_t buf_num, uint32_t buf_len); RTLSDR_API int rtlsdr_cancel_async (rtlsdr_dev_t *dev); RTLSDR_API int rtlsdr_set_bias_tee (rtlsdr_dev_t *dev, int on); RTLSDR_API int rtlsdr_set_bias_tee_gpio (rtlsdr_dev_t *dev, int gpio, int on); #ifdef __cplusplus } #endif #endif /* __RTL_SDR_H */ rtlsdr.h头文件定义了34个接口函数。常用的接口函数有10个,如rtlsdr_open(获取设备句柄)函数、rtlsdr_set_center_freq(设置中心频率)函数、rtlsdr_set_sample_rate(设置采样率)函数、rtlsdr_read_async(同步读取I/Q数据)函数、rtlsdr_close(关闭设备句柄)函数等。从头文件的函数声明中,可以查到函数输入参数和输出参数的名称和类型。 5.1.2RTLSDR接口函数封装 LabVIEW编程中,提供了一套调用动态链接库的方法。根据头文件提供的函数声明,就可以利用CLF模块将头文件中的接口函数封装成子VI,接下来将以RTLSDR_API int rtlsdr_open(rtlsdr_dev_t **dev,uint32_t index)函数为例说明动态链接库的封装和调用过程。 (1) 准备两个文件,一个是RTLSDR的库文件rtlsdr.dll,另一个是头文件RTLSDR.h。需要特别注意的是,如果主机安装的LabVIEW是32位的,库文件rtlsdr.dll必须是32位的,否则LabVIEW将无法调用rtlsdr.dll中的函数。 (2) 新建一个VI,在Connectivity→Libraries & Executables路径下找到CLF模块,如图52所示。通过CLF模块,就可以实现动态链接库的调用。 图52调用库函数(CLF)模块 (3) 在程序框图中创建一个CLF模块,双击该模块,就会弹出该模块的参数配置对话框,在这个对话框中,可以配置DLL文件路径、被调函数名、输入和输出参数等,如图53所示。 图53CLF参数配置 在Function选项卡中,配置被调用函数的信息。在Library name or path文本框中设置rtlsdr.dll的路径(注意选择rtlsdr.dll文件的存放路径)。在Function name下拉列表中选择需要调用的接口函数,本例中选择rtlsdr_open。在Thread选项中选择运行的线程范围,本例中默认选择Run in UI thread。在Calling convention选项中选择被调用函数的调用约定,本例中默认选择C。 在Parameters选项卡中,根据头文件中的函数声明对输入和输出参数进行设置。需要特别注意的是,为了正确设置这些参数,需要参考rtlsdr.h头文件中的函数声明。例如,在rtlsdr.h中,可以找到rtlsdr_open函数的输入输出参数以及它们的类型: RTLSDR_API int rtlsdr_open (rtlsdr_dev_t **dev, uint32_t index); 在Parameters选项卡中,默认创建了一个return type参数,如图54所示。根据头文件,rtlsdr_open函数返回值是int类型,在图54的参数设置Type下拉列表中选择Numeric类型,在Data type下拉列表中选择Signed 32bit Integer类型。 图54Parameters选项卡 在CLF的Parameters选项卡中,还需要创建两个参数,一个是指向设备的句柄指针,另一个是设备索引。选中左侧列表中的DevRefnum,单击页面中的按钮,在Current parameter栏中设置参数,Name可以自定义,如设置为DevRefnum,Type选择为Numeric,Data type选择为Unsigned Pointersized Integer,Pass选择为Point to Value,如图55所示。 图55DevRefnum配置 以同样的方式创建设备索引index。注意index是一个输入参数。Name设置为index,Type选择为Numeric,Data type选择为Unsigned 32bit Integer,Pass选择为Value。DevRefnum和index均创建完成之后,在页面左下角的Function prototype区域可以看到函数声明,此声明应当与头文件中的函数保持一致。 (4) CLF模块配置完成之后,需要定义输入和输出端口,在CLF的输入和输出端口分别创建数值输入控件和显示控件,如图56所示。需要注意的是,在前面板中需要完成端口关联,才可以作为子VI被其他VI调用。 图56rtl_open子VI (5) 按照同样的方法,可以对rtlsdr.h头文件中的其他函数进行封装。将需要的函数都封装完成之后,可以将封装后的子VI打包成一个库文件,方便维护。执行File→New菜单命令,新建一个Library,如图57所示。 图57Library创建 图58rtlsdr.lvlib创建 然后在库文件名上右击,在弹出的菜单中选择Add→File,选择已经封装好的VI。更改库文件名,就完成了库文件的创建,如图58所示。需要注意的是,rtlsdr.dll文件和rtlsdr.lvlib一般放在同一个文件夹下。 最后将整个库文件夹复制到LabVIEW的\instr.lib文件夹中,这样,在程序框图的函数选板中,就可以直接调用相应的子VI,如图59所示。 图59RTLSDR 子VI 5.2导入共享库向导 手动封装动态链装库中的接口函数是比较麻烦的,尤其是在库文件中的函数比较多的时候。LabVIEW提供了自动封装的方法——导入共享库向导。接下来,本节将以rtlsdr.dll为例,介绍基于导入共享库向导的封装方法。 5.2.1导入前准备 在使用导入共享库向导之前,需要准备两个文件,一个是RTLSDR的库文件rtlsdr.dll,另一个是头文件RTLSDR.h。 5.2.2导入共享库向导过程 (1) 执行Tools(工具)→Import(导入)→Shared Library(.dll)...(共享库)菜单命令,如图510所示。 图510导入共享库向导 (2) 在弹出的对话框中选择Create VIs for a shared library,如图511所示。如果需要对已经生成的VI进行更新,选择第2项Update VIs for a shared library。 图511创建或更新模式 (3) 进入路径配置对话框,设置rtlsdr.dll的路径和头文件的路径。注意这里设置的头文件是rtlsdr.h,如图512所示。 图512选择库文件和头文件 (4) 用记事本打开rtlsdr.h,可以看到头文件rtlsdr.h还包含其他两个头文件: #include <stdint.h> #include <rtl-sdr_export.h> 在Include文件夹中,配置两个头文件stdint.h和rtlsdr_export.h(本书配套程序)的路径,如图513所示。 图513Include文件夹 (5) 如果路径配置正确,共享库向导就会根据库文件和头文件生成一个函数列表。正常情况下,头文件中的33个接口函数能够被识别和封装,如图514所示。对于FM接收机,只会用到其中的少部分函数,如rtlsdr_open()、rtlsdr_set_center_freq()、rtlsdr_set_sample_rate()函数等。 图514选择需要封装的函数 (6) 选定需要生成的函数之后,设置LabVIEW库函数的文件名和保存路径。本例中设置文件名为rtlsdr,路径为C盘安装文件夹\user.lib下,如图515所示。 图515设置库文件名和保存路径 (7) 在Select Error Handing Mode页面中,可以配置错误输出模式,如图516所示。有3种模式可以选择,本例中选择Simple Error Handing。 图516配置错误输出模式 (8) 在Configure VIs and Controls页面中,可以对每个函数的输入和输出参数进行重配置。例如,选择rtlsdr_set_center_freq()这个函数,在默认的情况下,Control Type设置为Numeric,Pass Type设置为Pass by Value,Representation设置为Unsigned Long,如图517所示。 图517重新配置输入输出参数 (9) 配置完成每个函数的输入和输出参数,单击Next按钮,就可以进行封装。等待几分钟,将返回封装后的rtlsdr库,如图518所示。 图518 封装后的rtlsdr库 (10) 检查接口函数封装的正确性。打开任意一个子VI,如set center freq.vi。进一步打开CLF,可以看到其配置,如图519所示。 图519 封装的正确性验证 5.3动态链接库编译 在5.1.2节和5.2.2节中,不论是手动方式还是导入共享库向导的方式,都需要动态链接库文件rtlsdr.dll。接下来,本节将讨论如何从源程序中编译动态链接库文件。 5.3.1编译前准备 首先在GitHub网站中搜索到rtlsdr的源文件,有多个版本可以选择,如可以选择osmocom/rtlsdr,网址为https://github.com/osmocom/rtlsdr。 在rtlsdrmaster\src文件下,可以找到rtlsdr.dll对应的源程序librtlsdr.c。预先安装pthreadwin32和libusbx1.0.18win(本书配套程序)。预先准备编译依赖的静态库文件libusb1.0.lib和pthreadVC2.lib(本书配套程序)。 5.3.2编译步骤 将librtlsdr.c文件编译成动态链接库文件,需要预先安装C程序的编译软件,本例中选择VS 2013。Microsoft Visual Studio简称VS,是美国微软公司提供的软件开发工具。librtlsdr.c的编译步骤如下。 (1) 首先启动VS 2013,新建一个Win32项目,设置项目名称和保存路径,如图520所示。 图520新建一个Win32项目 图521解决方案资源管理器 (2) 在弹出的应用程序向导中选择应用程序类型,本例中选择DLL,在附加选项中选择“空项目”。 (3) 右击“源文件”文件夹,在弹出的快捷菜单中依次选择“添加”→“现有项”,将rtlsdrmaster\src文件夹下的librtlsdr.c、tuner_e4k.c、tuner_fc0012.c、tuner_fc0013.c、tuner_fc2580.c、tuner_r82xx.c 6个C文件导入,如图521所示。 (4) 将静态库文件libusb1.0.lib和pthreadVC2.lib添加到VS的VC++目录中,如图522所示。右击图521所示的项目名称rtlsdrdll,在弹出的菜单中选择“配置属性”,在“VC++目录”页面中配置包含目录和库目录。 图522配置包含目录和库目录 编辑包含目录,将Prebuilt.2\include文件夹、libusbx1.0.18win文件夹下的include\libusbx1.0和rtlsdrmaster\include文件夹添加到包含目录中,如图523所示。 图523配置包含目录 编辑库目录,将Prebuilt.2\lib\x86文件夹、libusbx1.0.18win\MS32\static文件夹添加到库目录中,如图524所示。 图524配置库目录 在“链接器”→“附加依赖项”中添加libusb1.0.lib和pthreadVC2.lib静态库文件,如图525所示。 图525在链接器中添加附加依赖项 (5) 编译生成动态链接库rtlsdrdll.dll,VS编译输出如图526所示。需要特别注意,VS默认生成的动态链接库是32位的。 图526VS编译输出 5.4调用库函数的配置 在接口函数封装过程中,使用了CLF模块。接下来,本节将对CLF配置对话框中的Function(函数)、Parameters(参数)、Callbacks(回调)、Error Checking(错误检测)4个选项卡加以说明。 5.4.1函数配置 在Function(函数)选项卡中,可以设置DLL文件路径。在Library name or path(库名/路径)文本框中,单击文件夹图标,选择DLL所在的路径,就可以完成设置。当调用包含DLL的VI被装入内存时,DLL被自动装入内存。在Library name or path文本框的下方,还有一个Specify path on diagram(在程序框图中指定路径)选项,选中该选项,DLL将会被“动态加载”: 只有程序运行到需要使用该DLL中的函数时,DLL才会被装入内存。需要注意的是,选中Specify path on diagram之后,Library name or path中输入的路径将无效。与此同时,CLF模块将多出一对输入和输出端口,用于指定DLL的路径。 设置完DLL文件路径之后,在Function name(函数名称)下拉列表中选择被调用函数的名称。单击下拉列表,将显示DLL中所有的可用函数。 在Function选项卡中可以配置Thread(线程)。在Thread选项下,有Run in UI thread和Run in any thread两个单选按钮。Run in UI thread表示仅运行在UI线程中; Run in any thread表示运行在任意线程中。在本例的程序中,选择默认的Run in UI thread。 在Function选项卡中还可以指明被调用函数的Calling convention(调用约定)。CLF支持两种调用约定: stdcall和C。stdcall由被调用者负责清理堆栈,C由调用者清理堆栈。Windows API一般使用的都是stdcall,标准C的库函数则大多使用C。 5.4.2端口参数配置 在CLF配置中,端口参数(Parameters)的配置比较复杂。DLL调用出现的问题,大多是由于Parameters配置错误所引起的。接下来,本节将通过rtlsdr实例介绍该页面的配置。 在Parameters选项卡中,可以添加相应的参数并修改它们的返回值类型,直到页面底部的Function prototype(函数原型)与DLL头文件中的函数定义相匹配,如图527所示。在Type(类型)下拉列表中选择函数返回值的类型,如Void(空)、Numeric(数值)或String(字符串)。本例中选择Numeric。 图527Parameters选项卡 在Data type下拉列表中可以看到多种数据类型,如图528所示。本例中选择Signed 32bit Integer。 图528Data type下拉列表 这里需要特别指出的一种数据类型,就是C语言中的指针类型,如Signed Pointersized Integer。对于简单的C函数构成的DLL,LabVIEW利用CLF调用时就比较简单,但是对于参数或返回值是指针类型的DLL函数,LabVIEW调用时就比较复杂。 指针就是变量的地址,将地址作为参数传递到DLL函数中,DLL函数就可以操作这个地址指向的变量。需要注意的是,在32位的操作系统中,可以使用int32数值表示指针。在64位的系统中,只能使用I64或U64表示指针。如果无法确知是32位还是64位的系统,则可以使用Pointersized Integer这种数据类型。 CLF中常用指针类型配置和使用如表51所示,指向数值类型和字符类型的指针配置相对简单,找到与之相对应的选项即可。在使用布尔类型时,由于布尔类型在DLL函数和LabVIEW VI之间传递没有对应的数据类型,需要利用数值类型来传递,因此输入时先要把布尔量转换为数值,再传递给DLL函数,输出时再把数值转换为布尔量。需要注意的是,如果在C语言函数参数声明中有const关键字,需要选中Constant选项。 表51CLF中指针配置和使用 C语言声明CLF配置CLF使用 double *a char *a bool *a 数组在传递给DLL函数时,只能是指针,如表52所示。在传递数组类型时,Array format(数组格式)要选择为Array Data Pointer(数组数据指针)。需要注意的是,LabVIEW只支持C语言中的数值型数组。 表52CLF中数组配置和使用 C语言声明CLF配置CLF使用 int a[] int *a[] 簇结构在LabVIEW中是常用的数据类型,C语言的struct(结构体)数据类型与之对应。在CLF节点的配置面板中,没有专门命名为struct或cluster参数类型,选择Adapt to Type就可以。需要注意的是,在cluster较为复杂的情况下,需要考虑字节对齐问题。 5.4.3回调函数配置 Callbacks(回调)为DLL设置一些回调函数。DLL文件中实现了各种类型的函数,当程序需要调用函数时,就要先载入DLL,取得函数地址然后进行调用。有些情况下,需要将应用程序的某些功能提供给DLL使用,于是就可以使用回调函数。回调函数通常用于程序初始化、资源清理等工作。 5.4.4错误检测配置 Error Checking(错误检测)用于设置错误处理方式,有Maximum(最高级)、Default(默认)和Disabled(不检测)3个选项。最高级别的检测会对运行过程中检测到的每处错误进行反馈,但同时也会使CLF节点的运行速率降低。默认级别的检测会对程序执行最小程度的错误进行排查,对CLF节点的运行速率影响较小。不检测即不对程序中的错误进行检测,此时CLF节点以最快速度运行。 5.5本章小结 本章以RTLSDR为例,介绍了非NI设备控制方法。通过CLF模块,LabVIEW可以调用DLL中的接口函数,从而实现对设备的控制。 首先以RTLSDR的LabVIEW接口函数封装过程为例,介绍了LabVIEW中调用库函数模块的使用方法。 然后详细介绍了LabVIEW中导入共享库向导的使用方法。 接着介绍了动态链接库文件rtlsdr.dll在VS 2013环境下的编译过程。 最后介绍了调用库函数模块中Fuction、Parameters、Callbacks、Error Checking 4个选项卡的配置方法。