第5章
CHAPTER 5


动态链接库封装和调用





LabVIEW项目开发时,经常会遇到仪器设备控制问题,对于NI提供的仪器设备,可以通过安装相关的开发工具包来解决; 对于非NI提供的设备,可以通过调用动态链接库(Dynamic Link Library,DLL)的方式来解决。本章将以RTLSDR的LabVIEW接口函数为例,介绍LabVIEW中动态链接库的封装和调用方法。
5.1RTLSDR接口函数的封装
LabVIEW通过调用RTLSDR的接口函数实现对RTLSDR的控制。常用的接口函数有open函数、set sample rate函数、set center freq函数、read sync函数和close函数。3.2.2节详细介绍了RTLSDR常用接口函数。3.2.3节通过数据采集实例介绍了RTLSDR接口函数的使用方法。
打开RTLSDR接口函数的程序框图,可以看到,LabVIEW是通过调用库函数(Call Library Function,CLF)模块调用动态链接库rtlsdr.dll的方式实现设备控制。下面将介绍动态链接库和LabVIEW接口函数的封装方法。
5.1.1动态链接库简介
动态链接库(DLL)是Windows操作系统实现共享函数库的一种方式。在Windows操作系统中使用DLL,能够使应用程序代码变得更简洁,计算机内存资源的使用效率变得更高。
RTLSDR的应用程序接口(Application Programming Interface,API)函数是采用C语言编写的。通过调用RTLSDR的动态链接库rtlsdr.dll,上层SDRsharp、LabVIEW、MATLAB/Simulink和GNURadio等应用软件就可以控制RTLSDR设备,如图51所示。


图51RTLSDR接口函数



动态链接库rtlsdr.dll实现了RTLSDR的所有接口函数。RTLSDR源程序中的头文件rtlsdr.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.2RTLSDR接口函数封装
LabVIEW编程中,提供了一套调用动态链接库的方法。根据头文件提供的函数声明,就可以利用CLF模块将头文件中的接口函数封装成子VI,接下来将以RTLSDR_API int rtlsdr_open(rtlsdr_dev_t **dev,uint32_t index)函数为例说明动态链接库的封装和调用过程。
(1) 准备两个文件,一个是RTLSDR的库文件rtlsdr.dll,另一个是头文件RTLSDR.h。需要特别注意的是,如果主机安装的LabVIEW是32位的,库文件rtlsdr.dll必须是32位的,否则LabVIEW将无法调用rtlsdr.dll中的函数。
(2) 新建一个VI,在Connectivity→Libraries & Executables路径下找到CLF模块,如图52所示。通过CLF模块,就可以实现动态链接库的调用。


图52调用库函数(CLF)模块


(3) 在程序框图中创建一个CLF模块,双击该模块,就会弹出该模块的参数配置对话框,在这个对话框中,可以配置DLL文件路径、被调函数名、输入和输出参数等,如图53所示。


图53CLF参数配置


在Function选项卡中,配置被调用函数的信息。在Library name or path文本框中设置rtlsdr.dll的路径(注意选择rtlsdr.dll文件的存放路径)。在Function name下拉列表中选择需要调用的接口函数,本例中选择rtlsdr_open。在Thread选项中选择运行的线程范围,本例中默认选择Run in UI thread。在Calling convention选项中选择被调用函数的调用约定,本例中默认选择C。 
在Parameters选项卡中,根据头文件中的函数声明对输入和输出参数进行设置。需要特别注意的是,为了正确设置这些参数,需要参考rtlsdr.h头文件中的函数声明。例如,在rtlsdr.h中,可以找到rtlsdr_open函数的输入输出参数以及它们的类型: 


RTLSDR_API int rtlsdr_open (rtlsdr_dev_t **dev, uint32_t index);

在Parameters选项卡中,默认创建了一个return type参数,如图54所示。根据头文件,rtlsdr_open函数返回值是int类型,在图54的参数设置Type下拉列表中选择Numeric类型,在Data type下拉列表中选择Signed 32bit Integer类型。


图54Parameters选项卡


在CLF的Parameters选项卡中,还需要创建两个参数,一个是指向设备的句柄指针,另一个是设备索引。选中左侧列表中的DevRefnum,单击页面中的按钮,在Current parameter栏中设置参数,Name可以自定义,如设置为DevRefnum,Type选择为Numeric,Data type选择为Unsigned Pointersized Integer,Pass选择为Point to Value,如图55所示。


图55DevRefnum配置


以同样的方式创建设备索引index。注意index是一个输入参数。Name设置为index,Type选择为Numeric,Data type选择为Unsigned 32bit Integer,Pass选择为Value。DevRefnum和index均创建完成之后,在页面左下角的Function prototype区域可以看到函数声明,此声明应当与头文件中的函数保持一致。
(4) CLF模块配置完成之后,需要定义输入和输出端口,在CLF的输入和输出端口分别创建数值输入控件和显示控件,如图56所示。需要注意的是,在前面板中需要完成端口关联,才可以作为子VI被其他VI调用。


图56rtl_open子VI


(5) 按照同样的方法,可以对rtlsdr.h头文件中的其他函数进行封装。将需要的函数都封装完成之后,可以将封装后的子VI打包成一个库文件,方便维护。执行File→New菜单命令,新建一个Library,如图57所示。


图57Library创建




图58rtlsdr.lvlib创建


然后在库文件名上右击,在弹出的菜单中选择Add→File,选择已经封装好的VI。更改库文件名,就完成了库文件的创建,如图58所示。需要注意的是,rtlsdr.dll文件和rtlsdr.lvlib一般放在同一个文件夹下。
最后将整个库文件夹复制到LabVIEW的\instr.lib文件夹中,这样,在程序框图的函数选板中,就可以直接调用相应的子VI,如图59所示。


图59RTLSDR 子VI


5.2导入共享库向导
手动封装动态链装库中的接口函数是比较麻烦的,尤其是在库文件中的函数比较多的时候。LabVIEW提供了自动封装的方法——导入共享库向导。接下来,本节将以rtlsdr.dll为例,介绍基于导入共享库向导的封装方法。
5.2.1导入前准备
在使用导入共享库向导之前,需要准备两个文件,一个是RTLSDR的库文件rtlsdr.dll,另一个是头文件RTLSDR.h。
5.2.2导入共享库向导过程
(1) 执行Tools(工具)→Import(导入)→Shared Library(.dll)...(共享库)菜单命令,如图510所示。


图510导入共享库向导


(2) 在弹出的对话框中选择Create VIs for a shared library,如图511所示。如果需要对已经生成的VI进行更新,选择第2项Update VIs for a shared library。


图511创建或更新模式


(3) 进入路径配置对话框,设置rtlsdr.dll的路径和头文件的路径。注意这里设置的头文件是rtlsdr.h,如图512所示。


图512选择库文件和头文件


(4) 用记事本打开rtlsdr.h,可以看到头文件rtlsdr.h还包含其他两个头文件: 


#include <stdint.h>

#include <rtl-sdr_export.h>

在Include文件夹中,配置两个头文件stdint.h和rtlsdr_export.h(本书配套程序)的路径,如图513所示。 


图513Include文件夹


(5) 如果路径配置正确,共享库向导就会根据库文件和头文件生成一个函数列表。正常情况下,头文件中的33个接口函数能够被识别和封装,如图514所示。对于FM接收机,只会用到其中的少部分函数,如rtlsdr_open()、rtlsdr_set_center_freq()、rtlsdr_set_sample_rate()函数等。


图514选择需要封装的函数


(6) 选定需要生成的函数之后,设置LabVIEW库函数的文件名和保存路径。本例中设置文件名为rtlsdr,路径为C盘安装文件夹\user.lib下,如图515所示。


图515设置库文件名和保存路径


(7) 在Select Error Handing Mode页面中,可以配置错误输出模式,如图516所示。有3种模式可以选择,本例中选择Simple Error Handing。



图516配置错误输出模式



(8) 在Configure VIs and Controls页面中,可以对每个函数的输入和输出参数进行重配置。例如,选择rtlsdr_set_center_freq()这个函数,在默认的情况下,Control Type设置为Numeric,Pass Type设置为Pass by Value,Representation设置为Unsigned Long,如图517所示。



图517重新配置输入输出参数



(9) 配置完成每个函数的输入和输出参数,单击Next按钮,就可以进行封装。等待几分钟,将返回封装后的rtlsdr库,如图518所示。


图518 封装后的rtlsdr库


(10) 检查接口函数封装的正确性。打开任意一个子VI,如set center freq.vi。进一步打开CLF,可以看到其配置,如图519所示。


图519 封装的正确性验证


5.3动态链接库编译
在5.1.2节和5.2.2节中,不论是手动方式还是导入共享库向导的方式,都需要动态链接库文件rtlsdr.dll。接下来,本节将讨论如何从源程序中编译动态链接库文件。
5.3.1编译前准备
首先在GitHub网站中搜索到rtlsdr的源文件,有多个版本可以选择,如可以选择osmocom/rtlsdr,网址为https://github.com/osmocom/rtlsdr。
在rtlsdrmaster\src文件下,可以找到rtlsdr.dll对应的源程序librtlsdr.c。预先安装pthreadwin32和libusbx1.0.18win(本书配套程序)。预先准备编译依赖的静态库文件libusb1.0.lib和pthreadVC2.lib(本书配套程序)。 
5.3.2编译步骤
将librtlsdr.c文件编译成动态链接库文件,需要预先安装C程序的编译软件,本例中选择VS 2013。Microsoft Visual Studio简称VS,是美国微软公司提供的软件开发工具。librtlsdr.c的编译步骤如下。
(1) 首先启动VS 2013,新建一个Win32项目,设置项目名称和保存路径,如图520所示。


图520新建一个Win32项目




图521解决方案资源管理器

(2) 在弹出的应用程序向导中选择应用程序类型,本例中选择DLL,在附加选项中选择“空项目”。
(3) 右击“源文件”文件夹,在弹出的快捷菜单中依次选择“添加”→“现有项”,将rtlsdrmaster\src文件夹下的librtlsdr.c、tuner_e4k.c、tuner_fc0012.c、tuner_fc0013.c、tuner_fc2580.c、tuner_r82xx.c 6个C文件导入,如图521所示。
(4) 将静态库文件libusb1.0.lib和pthreadVC2.lib添加到VS的VC++目录中,如图522所示。右击图521所示的项目名称rtlsdrdll,在弹出的菜单中选择“配置属性”,在“VC++目录”页面中配置包含目录和库目录。


图522配置包含目录和库目录


编辑包含目录,将Prebuilt.2\include文件夹、libusbx1.0.18win文件夹下的include\libusbx1.0和rtlsdrmaster\include文件夹添加到包含目录中,如图523所示。


图523配置包含目录


编辑库目录,将Prebuilt.2\lib\x86文件夹、libusbx1.0.18win\MS32\static文件夹添加到库目录中,如图524所示。


图524配置库目录


在“链接器”→“附加依赖项”中添加libusb1.0.lib和pthreadVC2.lib静态库文件,如图525所示。


图525在链接器中添加附加依赖项


(5) 编译生成动态链接库rtlsdrdll.dll,VS编译输出如图526所示。需要特别注意,VS默认生成的动态链接库是32位的。


图526VS编译输出


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头文件中的函数定义相匹配,如图527所示。在Type(类型)下拉列表中选择函数返回值的类型,如Void(空)、Numeric(数值)或String(字符串)。本例中选择Numeric。 


图527Parameters选项卡


在Data type下拉列表中可以看到多种数据类型,如图528所示。本例中选择Signed 32bit Integer。


图528Data type下拉列表

这里需要特别指出的一种数据类型,就是C语言中的指针类型,如Signed Pointersized Integer。对于简单的C函数构成的DLL,LabVIEW利用CLF调用时就比较简单,但是对于参数或返回值是指针类型的DLL函数,LabVIEW调用时就比较复杂。
指针就是变量的地址,将地址作为参数传递到DLL函数中,DLL函数就可以操作这个地址指向的变量。需要注意的是,在32位的操作系统中,可以使用int32数值表示指针。在64位的系统中,只能使用I64或U64表示指针。如果无法确知是32位还是64位的系统,则可以使用Pointersized Integer这种数据类型。
CLF中常用指针类型配置和使用如表51所示,指向数值类型和字符类型的指针配置相对简单,找到与之相对应的选项即可。在使用布尔类型时,由于布尔类型在DLL函数和LabVIEW VI之间传递没有对应的数据类型,需要利用数值类型来传递,因此输入时先要把布尔量转换为数值,再传递给DLL函数,输出时再把数值转换为布尔量。需要注意的是,如果在C语言函数参数声明中有const关键字,需要选中Constant选项。


表51CLF中指针配置和使用



C语言声明CLF配置CLF使用

double *a

char *a

bool *a

数组在传递给DLL函数时,只能是指针,如表52所示。在传递数组类型时,Array format(数组格式)要选择为Array Data Pointer(数组数据指针)。需要注意的是,LabVIEW只支持C语言中的数值型数组。


表52CLF中数组配置和使用



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本章小结
本章以RTLSDR为例,介绍了非NI设备控制方法。通过CLF模块,LabVIEW可以调用DLL中的接口函数,从而实现对设备的控制。
首先以RTLSDR的LabVIEW接口函数封装过程为例,介绍了LabVIEW中调用库函数模块的使用方法。
然后详细介绍了LabVIEW中导入共享库向导的使用方法。
接着介绍了动态链接库文件rtlsdr.dll在VS 2013环境下的编译过程。
最后介绍了调用库函数模块中Fuction、Parameters、Callbacks、Error Checking 4个选项卡的配置方法。