2023年11月29日发(作者:)
如何使用 VI 的重入属性(Reentrant)
在 VI Properties -> Execution 中可以选择 VI 的Reentrant Execution属性(中文译为:
可重入执行)。 我们在《LabVIEW 程序的内存优化》一文中讨论过,尽量不要把 VI 设置为重入
属性,因为这样就多占用了内存,降低了运行效率。此外,如果不加注意的话,还可能引发多线程不
安全的问题。 尽管可重入 VI 在 LabVIEW 中不是必须的,但是在某些情况下使用可重入 VI 可以
简化我们的程序。那么在什么情况下可以使用 Reentrant VI 呢?
首先看一下图 1 所示的程序,程序中调用的两个子 VI 是同一个 VI,并且不是可重入的 VI。
LabVIEW 是自动多线程的语言,那么图中的两个子VI会不会同时执行呢。一定不会的。如果程序
中调用的是两个不同的子 VI,LabVIEW 有可能会同时在不同的线程执行它们,但对于两次调用相
同的子 VI,LabVIEW 一定要等一个执行完,再执行另一个。
图1:并行的两个相同子 VI
其原因是,LabVIEW 会为每个 VI 都开辟一块内存用于数据存储。作为子 VI,每次被调用,
它的局部变量的数据都是被存在同一地址的。与 C 语言相对照,在默认情况下,VI 是不可重入的,
VI 中所有的局部变量都是静态变量。如果 LabVIEW 在不同的线程下执行同一 VI,那么两个线程
就会同时对这一块数据地址进行读写,就会导致这一块地址内数据的混乱。为避免此类不安全情况的
出现,LabVIEW 必须等待一个子 VI 执行结束,再执行另一个子 VI。
如果需要图1 中的两个子 VI 同时运行,比如子 VI 所做的工作是读取文件这样一类耗时多、但
CPU占用不大的操作,则并行执行可以大大提高效率。这时,就需要把子 VI 设置为可重入了。
LabVIEW 在不同的地方调用一个可重入 VI 时,会给它另外分配一个独立的数据地址空间。这样就
做到了线程安全。在两个线程执行的子 VI 使用两份在不同的地址存储的数据,也就不会造成混乱。
但是千万要注意, 这个“在不同的地方”调用:不可重入的 VI 的局部变量与 C 语言中非静态变量
的含义是不同的。在后面提到的计数器的例子可以验证这一点。
我觉得我说得挺清楚了,出道题目给大家测试一下:
图2:延时子 VI
图3:计算延时的主 VI
图2 是一个子 VI 的代码,功能是延时 1000 毫秒。图3 是主 VI 的代码,并行调用同一子 VI
两次,并计算程序的执行时间。运行主 VI,total time 的值是多少?
这是可重入 VI 的一种用途,即希望在不同的线程里同时执行同一个子 VI。
另外还有一种情况下,也可以用到可重入 VI:即需要使用到子 VI 中局部变量保存的数据,而
在不同的调用处,这些数据是独立不同的。这句话可能解释得不那么清楚,看下面例子就会比较容易
理解些。
图4:计数子 VI
图5:测试计数的主 VI
图 4 是一个可重入子 VI 的代码,功能是计算这个VI被运行的次数,每运行一次,输出的 count
值就增加1。图5 是调用它的主VI,用于演示这个计数器。执行主VI一次,output 1 和 output
2 的值分别是 10 和 20,表示这个子 VI 在两处分别被调用了 10 次和 20 次。
如果把图 4 中的 VI 改为不可重入,则 output 1 和 output 2 的输出值是不确定的。大家可
以自己试一试,再想一下原因。
当使用递归结构时,参与了递归调用的 VI 是需要被同时调用多次的。因此这些 VI 中的变量必
须是局部的,也就是说参与了递归调用的 VI 必须都被设置为可重入。参考:在 LabVIEW 中实现 VI
的递归调用
如何创建和使用 LabVIEW 中的 LLB 文件
最近接连有人问我,怎样在 LabVIEW 中创建一个 LLB 文件。于是我就把它写了下来。
通常,需要新建一个文件时,我们很自然就会想到去选中菜单“File->New”。可是,在 LabVIEW
8 中, 在“新建”菜单中是看不到 LLB 这种文件类型的; 要创建或者管理一个 LLB 文件,首先要
选择“Tool->”。在打开 LLB Manager 之后,再在菜单中选择“File -> New LLB”。
这样才能创建一个新的 LLB 文件出来。
我个人认为,在LabVIEW8及以后的版本中,LLB 文件现在已经没有存在的必要了,使用它,
弊大于利。
LLB 文件的功能就是把一组相关的 VI 以及其他文件打包存储在一起。其优点是节省磁盘空间,
LLB 文件是压缩了的。但是,近年来计算机存储介质的容量迅速膨胀。LabVIEW 程序的存储空间
再也不是一个需要考虑的问题了。所以这方面已经不再有诱惑力了。
LLB 文件有很多弊病。
1. 内部文件没有层次关系,所有文件都是平级存放的。这样一来,文件多了,就不能直接看出他
们的调用关系。此外,LLB也允许有同名文件存在。
2. 内部文件名长度有限制,大概限制几十个字符吧,文件名太长会被自动截断。
3. 不利于版本管理。LLB 中的一个文件被修改,整个 LLB 也就被修改了。这样,一是没办法作
增量存储,二是不容易定位到具体被改动了的文件上。
综上所诉,如果新建一个工程,最好不要考虑使用 LLB 文件了。同时为了方便管理工程中的文
件,应当尽量利用 LabVIEW 8 的新功能 :Project 和 Library。
用户自定义控件中 Control, Type Def. 和 Strict Type Def. 的区别
为了解释清楚,先定义一下要用到的概念。我们把以 .ctl 文件名定义的控件叫做用户自定义控件,
把通过拖拽或打开这个 .ctl 文件在 VI 上生成的控件叫做实例。
LabVIEW 的用户自定义控件包括了三种定义形式:打开一个 .ctl 文件,在它上方的“control”
下拉条中有三个选择,分别是无关联控件(Control)、类型定义(Type Def.)或者严格类型定义
(Strict Type Def.)。
无关联控件是指这个控件与它的实例之间没有任何关联。例如,你制作了一个漂亮的按钮控件保
存在 .ctl 文件中。需要用到它时,通过拖拽或打开这个 .ctl 文件就可以在 VI 中生成这个用户自定
义控件的一个实例。这个实例一旦生成,就和原用户自定义控件无任何关联了。无论是你修改这个实
例,还是修改原用户自定义控件,都不会对另一方产生任何影响。
类型定义控件是指实例控件与用户自定义控件的空间类型是相关联的。比如,你的用户自定义控
件是一个数值型控件,那么它的所有实例控件也都是数值型的。如果我们在 .ctl 文件中把用户自定
义控件的类型改为字符串,那么它已有的所有实例都将自动变成字符串类型。
有时候,只是类型相关联还不够。比如对于 Ring(Enum,Combo Box)这类的控件来说,如
果在用户自定义控件中添加了一项内容(item),一般总是希望它所有的实例也同时添加这一选项。
如果使用类型定义控件,因为控件类型没变,还是 Ring,实例们是不会自动跟随更新的。这时就需
要使用严格类型定义控件。选择严格类型定义后,不但实例与用户自定义控件的类型是相关联的,其
他一些控件属性,比如颜色等等,也是相关联的。
使用严格类型定义时有一点容易被误解:严格类型定义只是与实例控件相关联,由它生成的实例
常量的属性是不与之关联的。实例常量是指通过拖拽或生成常量等方法,在程序框图上生成的一个
与 .ctl 文件相关联的常量。比如在 Ring 型用户自定义控件中添加了一项内容,相关的实例常量是
不会发生任何改变的。很多人按常理想,认为常量也应当自动更新,但事实上不行。这也是我不采用
它做常量定义的原因之一。
调整控件和函数面板的首选项
LabVIEW 在 8.0 版对控件和函数面板作了一次较大调整。LabVIEW 功能越来越强大,控件和
函数面板上的东西越来越多。如果增加面板的嵌套深度,用户每次选取面板上的一个控件和函数都要
多点几下鼠标,而且对不熟悉位置的东西找起来也相当费劲;如果扩充每一个面板上的容量,一个面
板上图标太多,用户会眼花,也不利于查找。所以 LabVIEW 8 调整了面板的显示方式:最顶层面
板所有栏目都以文字的方式显示,纵向排成一列。其中第一个栏目是默认就展开的,可以直接看到次
级面板的内容;其它栏目都收起,鼠标挪上去,才看得到它里面的内容。
我现在用的是 LabVIEW 8.2 版,它的函数面板看上去是这样的:(在程序框图的空白处点击鼠
标右键)
图1:用鼠标右键弹出函数面板
函数和子 VI 被分为几大类,每个类的名字被列在弹出的菜单上,其中最前面一个类是展开的。
因为第一个分类中的函数最常用。
但是,有些人可能最常用的函数是在其它分类中的。这样可以调整一下,比如最常用的是 Express
VI,就可以把它最常用挪到最前面来。
用鼠标点弹出菜单左上角的图钉,就可以吧弹出菜单固定住。固定后的菜单每一项左端有两个竖
线,和一个三角。点击三角可以展开和收缩这个类里的图标,鼠标放到两个竖线上,鼠标就会变成带
箭头的十字花。这是就可以按下鼠标拖动这个条目了。点在 Express 项目的竖线上,然后把它托到
最上面。如图2、图3所示:
图2, 3:鼠标点在连个竖线上可以拖动这个项目
从此以后,Express 就在最上面了。看图4,再在程序框图上点鼠标右键,弹出的函数面板,最
上面一栏展开的就已经变成 Express 了。
图4:新的函数面板
你可能会发现,自己的 LabVIEW 在鼠标右击程序框图后,只显示出几个分类,其他的类别统统
都缩了起来。用户是可以自己制定显示或隐藏哪些分类的。在函数或控件面板钉住的状态下(如图2
这种状态),点击面板最上方的“View”按钮,就会出现让你更改显示或隐藏分类的菜单。
如果你觉得 LabVIEW 的面板布局不是很合理,你要用的东西分散在不同的类别里,用哪个坐首
选项都不太方便。那也没有关系,在 LabVIEW 8.5 中又多了一个类别,叫 Favorite (我的最爱)。
在函数或控件面板钉住的状态下,右键点击其它类别的函数,或子面板标题,弹出菜单上有一项就是
加入到 Favorite 中。把你最常用的函数都放到 Favorite 里去,再把 Favorite 移到面板的最上端
作为首选项,就可以了
在文件夹下直接创建新的 VI
在 Windows Explore 中,鼠标右键点击某一文件夹的空白处,弹出的菜单中有“新建”一项。通
过修改 Windows 的注册表,可以给这个新建列表添加一项,从而直接在文件夹下创建一个新的 VI。
如图1所示。
图1:在“新建”菜单中添加一项-创建VI
下载 Create New 文件,然后解压缩。运行里面的 Create Empty ,把它里面
的内容导入到注册表。然后就会发现多出图1所示的新建 VI 的项目了。
让 Windows 多一个新建项目,只要在注册表里添加上相关内容就行了。Create Empty
文件中的 Data 数据其实就是新建出来的文件的内容。在这个例子中,他是一个空白的 LabVIEW
8.0 的 VI。我们可以改变 reg 文件中的数据,是的产生出来的 VI 有所不同。
比如,你希望自己的新 VI 总是使用蓝色背景的,有一个特殊的图标等等,只要改变 reg 文件
中的数据就可以了。ZIP 包中还有一个 Create New Reg ,这个 VI 可以读入一个文件,
把它转换成注册表中实用的 Data 数据。使用者可以自己先造一个自己喜欢的 VI 作为模板,然后
利用Create New Reg 为它创建出一个注册表数据,导入注册表。这样每次在文件夹下用
鼠标右键创建出的 VI 就是模板 VI 的样子了。
选择结构
选择结构相当于文本语言中的条件语句。LabVIEW 8 中新增加的 Diagram Disable
Structure,Conditional Disabled Structure 类似 C 语言中的条件宏定义语句。
一. 程序框图禁用结构(Diagram Disable Structure)
在调试程序时常常会用到程序框图禁用结构。程序框图禁用结构中只有 Enabled 的一页会在运
行时执行,而 Disabled 页是被禁用、即不会执行的;并且在运行时,Disable 页面里的 SubVI 不
会被调入内存。所以,被禁用的页面如果有语法错误也不会影响整个程序的运行。这是一般选择结构
(Case Structure)无法做到的。
图1、2:使用程序框图禁用结构
例如图 1、2 中的示例,如果我们在运行程序的时候暂时不希望将 test 写入到文件里,但又觉
得有可能以后会用到。此时,就可以使用程序框图禁用结构把不需要得程序禁用掉。需要注意的是程
序框图禁用结构可以有多个被禁用的框架,但必须有且只能有一个被使用的框架。在被使用的框架中,
一定要实现正确的逻辑,比如上图的例子中,在被使用的框架中一定要有连线把前后的文件句柄和错
误处理联接好。
二. 条件禁用结构(Conditional Disabled Structure)
条件禁用结构则根据用户设定的符号(symbol)的值来决定执行哪一页面上的程序。其他方面与
程序框图禁用结构相同。
程序中所使用的符号,可以在项目或是运行目标机器(例如“My Computer”)的属性里设置。
图3:条件禁用结构
值得注意的是:程序框图禁用结构与条件禁用结构都是静态的,如果需要在运行时决定执行哪一
部分的程序可以使用选择结构。
程序框图禁用结构和条件禁用结构的一种实用案例可以参考:《其它常用调试工具和方法》
三. 选择结构(Case Structure)
在一般情况下,选择结构类似于 C 语言的 switch 语句。当输入为 bool 数据类型或 error 数
据类型时,选择结构类似于 C 语言中的 if 语句。
图4:枚举类型的 Case Selector
有输出时,则每一个框架中都必须连一个数据,当然也可以选择“Use Default If Unwired”。选
择“Use Default If Unwired”会有一定的风险,因为你可能会忘记了连线,这时候 LabVIEW 并不
会提醒你,程序就可能得到不可预料的结果。
如图5所示,鼠标右击数据输出隧道,可以选择是否使用“Use Default If Unwired”
图5:选择 Use Default If Unwired
顺序结构
一. 程序执行顺序
LabVIEW 是数据流驱动的编程语言。程序在执行时按照数据在连线上的流动方向执行。同时,
LabVIEW 是自动多线程的编程语言。如果在程序中有两个并行放置、它们之间没有任何连线的模块,
则LabVIEW会把它们放置到不同的线程中,并行执行。
图1、2:顺序执行 和 并行执行 的例子
顺序执行(图1):数据会从控制控件流向显示型控件,因此数据流经的顺序为“error in”控件,
“SubVI A”,“SubVI B”,“error out”控件,这也是这个VI的执行顺序。
并行执行(图2):“SubVI A”,“SubVI B”没有数据线相互连接,它们会自动被并行执行。所
以这个VI的执行顺序是“SubVI A”,“SubVI B”同时执行,当它们都执行完成以后,再执行“Merge
”。
二. 顺序结构
如果需要让几个没有互相连线的VI,按照一定的顺序执行,可以使用顺序结构来完成(Sequence
Structure)。
图3:Menu Palette
当程序运行到顺序结构时,会按照一个框架接着一个框架的顺序依次执行。每个框架中的代码全
部执行结束,才会再开始执行下一个框架。把代码放置在不同的框架中就可以保证它们的执行顺序。
LabVIEW 有两种顺序结构,分别是层叠式顺序结构(Stacked Sequence Structure)、平铺
式顺序结构(Flat Sequence Structure)。这两种顺序结构功能完全相同。平铺式顺序结构把所有
的框架按照从左到右的顺序展开在 VI 的框图上;而层叠式顺序结构的每个框架是重叠的,只有一个
框架可以直接在 VI 的框图上显示出来。在层叠式顺序的不同的框架之间如需要传递数据,需要使用
顺序结构局部变量(Sequence Local)方可。
图4:层叠式顺序结构
三. 顺序结构的使用
好的编程风格应尽可能少使用层叠式顺序结构。层叠式顺序结构的优点是及部分代码重迭在一起,
可以减少代码占用的屏幕空间。但它的缺点也是显而易见的:因为每次只能看到程序的部分代码,尤
其是当使用sequence local传递数据时,要搞清楚数据是从哪里传来的或传到哪里去就比较麻烦。
图5:转换顺序结构
使用平铺式顺序结构可以大大提高程序的可读性,但一个编写得好的 VI 是可以不使用任何顺序
结构的。由于 LabVIEW 是数据流驱动的编程语言,那么完全可以使用VI间连线来保证程序的运行
顺序。对于原本没有可连线的 LabVIEW 自带函数,比如延时函数,也可以为其包装一个 VI,并使
用 error in, error out,这样就可以为使用它的VI提供连线,以保证运行顺序。
图6:改进的延时 VI
定时结构
定时结构是从 LabVIEW 7.1 开始出现的。一眼就能看出来,它在外观与其它结构的风格完全不
同。倒是和 LabVIEW 7 力推的 Express VI,风格一致。打开定时结构的函数面板(图1),最
上面两个分别是定时循环,和定时顺序结构。下面的是与控制时间结构相关的的一些VI。
图1:时间结构的函数面板
定时结构,顾名思义,与时间控制有关。LabVIEW 中原本有一些用于延时或定时的函数,比如
Wait, Delay Time 等,他们都位于 Time&Dialog 面板中。利用这些函数,基本可以实现与使用
时间结构相同的功能。定时结构的最大改进在于,它可以选择使用哪个时间源(硬件)来定时。尤其
是当你的 LabVIEW 程序运行在 RT、FPGA 等设备上时,这一点就特别有用了。使用定时结构指
定使用硬件设备上,而不是PC机上的时钟来定时,可以使使运行时序更精准。
即便同样都是在普通PC上使用,定时间结构的定时效果也要比 Wait 等函数精确的多。我曾经
参与过的一个测试程序,开始使用 Wait 函数定时,运行一小时后,时间误差有几分钟。改用定时
结构后,误差缩短到了几秒钟。
缓存重用结构
一、缓存重用
在《LabVIEW 程序的内存优化》一文中有一个利用移位寄存器来降低 VI 内存的例子。
下面这个 VI 大约会占用了2.7M的内存空间
图1: 对数组进行数值运算的顺序执行程序
给它加上一个移位寄存器,如下图所示,内存占用就降低到只有不到400k了。
图4: 利用移位寄存器实现缓存重用
这其实是利用了移位寄存器两端接线端指向的是同一块内存这一特性,主动的告诉 LabVIEW 这
段代码上的每个加法节点的输入输出数据可以使用同一块内存。避免的 LabVIEW 分配不必要的数
据缓存。
但是代码还是不够完美,本来不需要循环,却非得摆上一个只执行一次的循环结构。感觉上总是
有些别扭。
这个问题终于在 LabVIEW 8.5 中被解决了。LabVIEW 8.5 中多出了一个结构——缓存重用结
构,专门用于告诉 LabVIEW 在某段代码上为输入输出数据做缓存重用。上面这个程序用新的缓存
重用结构来写就是这样的:
图3:利用缓存重用结构实现缓存重用
二、使用缓存重用结构
缓存重用结构与其它结构不在同一个函数选板上。这是缓存重用结构不是一个功能性、或改变程
序流程的结构。它的使用不会改变代码的功能,仅仅会改变代码的效率。
要使用缓存重用结构,需要打开函数选板的 Programming->Application Control->Memory
Control。第一个选项就是他了。
图4:缓存重用结构在函数选板上的位置
缓存重用结构为了方便使用,并不是简单的作为循环加移位寄存器的替换,它还有一些可选的边
框节点,帮助编程者处理不同的数据类型。
刚刚被拖到程序框图上的是一个光滑的黄色方框,要使用它的缓存重用功能还要为打算从用的内
存,根据它的数据类型选择相应的边框节点。在黄色的边框上点击鼠标右键,弹出菜单的最后几项就
是可供选择的边框节点类型。如图5所示。
每种边框节点都是成对出现的,一个在输入端,另一个在输出端。
图5:添加边框节点
三、边框节点
1. 数组元素索引和替换节点
这对节点用于改变数组中某个元素的值。输入的数组数据连到缓存重用结构左面的数据索引节点
上,结构内得到的数据,就是需要处理的元素的数值。
在 LabVIEW 中实现 VI 的递归调用
LabVIEW 中使用递归调用不是很方便。不过递归并不是编程必须程序结构,任何需要使用递归
调用的地方,都可以用循环结构来代替。但是在某些情况下,使用递归调用的确可以大大简化程序代
码,对缩短编程时间、提高程序可读性都非常有帮助,所以学习一下递归的实现方法还是有好处的。
一、为什么 VI 不能够被静态的递归调用
LabVIEW 不能通过静态调用的方法(把子 VI 直接放到另一 VI 的程序框图上)来实现递归。
对于一个非可重入的 subVI,在每一个时间,这个 subVI 这能被运行一次。LabVIEW 需要借
此来保证多线程时的数据安全。对于被递归调用的代码,是需要在它执行到中间的时候,就再次被调
用的。所以默认设置下的 VI 不能被静态递归调用。
对于被设置为可重入的 VI,是可以被同时调用多次的,但也不能被静态的递归调用。
除非是通过 VI Server 动态的调用 VI,否则,LabVIEW 是在一个程序被调入内存,开始运行
之前就为它的所有 VI 分配好内存空间的,包括数据区。如果一个 VI 不是可重入的,LabVIEW 会
在这个 VI 运行时局部变量所在的数据区开辟在这个 VI 所在的空间内;对于可重入的 VI,
LabVIEW 把它的数据区开辟在调用者 VI 上,这样就可以保证这个可重入 VI 在不同的地方被同时
调用时使用不同的数据区,以防止多线程运行时数据混乱。
因此,可重入 VI 虽然可以被同时多次调用,但是被调用的次数是运行前就确定的。而递归运算
时的调用次数是运行时决定的。这样,如果是静态调用, LabVIEW 根本没有办法为提前为参与递
归的 VI 开辟好数据区。
二、用动态调用方法实现递归
图1 是一个采用递归算法计算阶乘的例子,可以点击后面的连接直接下载示例 VI:
图1:利用递归结构计算阶乘
正如前文说过的,所有的递归都可以使用循环来代替,计算阶乘也可以使用循环结构,但是这里
介绍的是使用递归结构的方法。因为 n!=n*(n-1)!,所以我们只要编写一个 VI 实现功能
F(n)=n*F(n-1) 就可以了。
程序中,递归调用 VI 自身的结构由三个 VI 动态调用节点实现:Open VI Reference, Call By
Reference Node, Close Reference。这三个节点分别负责动态打开一个 VI(本例中就是这个 VI
自身),运行这个VI,再关闭它。
使用 Call By Reference Node 需要在打开 VI 句柄的时候就要知道 VI 连线板(Connector
Pane)的布局,因此,我们在用 Open VI Reference 打开 VI 的时候要提供 VI 连线板的布局信
息,在例子中就是 Open VI Reference 节点上方的那个常量。
三、使用递归时的几点注意事项
递归调用的退出或结束条件,本例中当输入数据小于1时,就需要结束递归调用返回最底层的值
了。如果递归调用的退出条件设置不当,可能会引起程序死循环甚至崩溃。
LabVIEW 中也可以实现 A 调用 B,B 又调用 A 这种用多个 VI 相互调用的递归结构。
参与递归调用的 VI 必须被设置为可重入。
动态调用的需要把 VI 在运行时调入内存,这个过程是比较耗时的。因此递归结构的运行效率远
不如可实现相同功能的循环结构,内存占用也会更大一些。决定使用递归结构之前要考虑到这些因素
Call Library Node 函数返回的字符串为什么不用在 VI 中先分配内存
Call Library Node 是 LabVIEW 中调用 DLL 函数的节点。如果被调用的函数有一参数数据类
型为 char*,用来输出字符串。我们需要在 CLN 中这个参数对应的左侧接线端连进一个字符串,
并且输入字符串的长度要保证大于输出字符串的长度。这个输入字符串的内容是没有用的,它只被用
作是被开辟的内存,保存输出字符串。否则,会出现数组越界的运行错误,LabVIEW会莫名其妙死
掉。
更糟糕的是,LabVIEW 不会在刚好出现数组越界错误时死掉,而是在之后的某一部确定时候死
掉。如果你意识不到自己的程序中有这种错误,或者你有几百个类似的 CLN,那你调试起来可能会
类似的。
有人问我,如果函数不是用参数输出字符串而是返回字符串,CLN 返回参数是没有左接线端的。
这可咋开辟内存捏?
我打开 LabVIEW 一试,可不是嘛。函数返回字符串的地方根本没法输入任何信息。自己编了一
个DLL试了试,发现 CLN 是可以正确输出函数返回的字符串的,不需要特别指定字符串的大小。
今天早上起得太早,于是就有点发晕,心想,如果既然 LabVIEW 不需要为函数返回的字符串开
辟内存,干嘛非要难为我们为参数输出的字符串开辟内存。否则可以避免多少潜在的错误啊。DLL 函
数参数输出字符串是个比较常见的导致程序崩溃的陷阱。
琢磨了半天,脑袋才清醒过来。所谓返回或输出字符串是口头上的语言。换成计算机的语言来解
释就清楚了:)
函数返回字符串的情况,实际上是函数返回了一个指向字符串指针。既然是函数返回的,LabVIEW
就可以得到该指针,进而就可以得到它所指的字符串。在LabVIEW内部,调用以下 strlen() 得到
字符串的长度,开辟一个相应大小的buffer,再调用以下 strcpy() 就把这个字符串考到 LabVIEW
控件的数据区了。
而参数输出字符串的情况并不是真的输出,而是函数要求输入一个指针。LabVIEW 必须为DLL
函数提供这样一个指针。而LabVIEW自己又不能自动开辟一片缓存就把指针传给函数,因为这时候
我们想要的字符串还不存在呢,LabVIEW没办法知道应该开辟多大的缓存。只好把指定缓存大小的
任务交给编程人员了:(
LabVIEW 编程如果不是考虑调用C编出来的函数,根本不需要内存分配回收的问题。有了内存
分配就是烦啊。
循环结构
LabVIEW中的循环结构有 for 循环和 while 循环。其功能与文本语言的循环结构的功能类似
类似,可以控制循环体内的代码执行多次。
一. for 循环
但是 LabVIEW 中的 for 循环的限制更多一些。
1. For 循环的迭代器只能从 0 开始,并且每次只能增加 1。
2. For 循环不能中途中断退出。C 语言里有 break 语句,但在 LabVIEW 中不要试图中间
停止 for 循环。
外部数据进入循环体是通过隧道进入的,有几种方式:
图1:For 循环结构上的隧道
图 1 所示的 For 循环结构演示了三种隧道结构,就是在 For 循环结构左右边框上用于数据输
入输出的节点。这三种隧道从上至下分别是:索引隧道、移位寄存器(shift register)、一般隧道。
一般隧道,就是把数据传入传出循环结构。数据的类型和值在传入传出循环结构前后不发生变
化。
索引隧道是 LabVIEW 的一种独特功能。一个循环外的数组通过索引隧道连接到循环结构上,隧
道在循环内一侧会自动取出数组的元素,依顺序每次循环取出一个元素。用索引隧道传出数据,可以
自动把循环内的数据组织成数组。
通过移位寄存器传入传出数据,也是数据的类型和值都不会发生变化。移位寄存器的特殊之处在
于在循环结构两端的接线端是强制使用同一内存的。因此,上一次迭代执行产生的某一值,传给移位
寄存器右侧的接线端,如果下一次迭代运行需要用到这个数据,从移位寄存器左侧的接线端引出就可
以了。
C 语言程序员初学 LabVIEW,在使用循环结构时,常常为创建一个中间变量烦恼。为循环中的
变量创建一个 Local Variable 不是好的方法。我们应当时刻记得 LabVIEW 与一般文本语言不同,
LabVIEW 的数据不是保存在显示的变量里,而是在连线上流动的。LabVIEW 是通过移位寄存器把
数据从一次循环传递到下一次的。
图2:反馈节点
如果单纯是为了让下一次迭代使用上次迭代的数据,也可以使用反馈节点,如图2所示。
移位寄存器左侧的接线端可以不只有一个,用鼠标可以把左侧的接线端拉出多个来,如图3所示。
下面的接线端可以记录上两次、三次……的数据。
图3:多接线端移位寄存器
使用数组的隧道有一些需要注意的事项,参考:LabVIEW 代码中常见的错误。
二. While 循环
LabVIEW 的 While 循环相当于文本语言中的 do... 循环。有些语言还有
do... 循环,LabVIEW 没有这样的循环。LabVIEW 的 while 循环至少要运行一次。
for 循环中可以用的数据传递方式,几种隧道也都可以在 while 循环中使用。所以在很多情况下,
while 循环可以替代 for 循环。
While 循环比 for 循环灵活的地方是可以进入循环后在决定何时循环结束。比如,希望当某一变
量大于一个值时停止循环,这种情况下不能预知循环次数,所以一定要使用 while 循环。
while 循环也有不利的方面:
首先,for 循环更利于阅读。读者一眼就可以看出程序会内运行多少次。
其次,while 循环也可以使用带索引的隧道来构造数组,但是它的效率低于 for 循环。
图4:使用循环构造数组
如图4,用两种循环所产生的数组大小是相同的。但是如果使用的是 for 循环,LabVIEW 在循
环运行之前,就已经知道数组的大小是100,因此 LabVIEW 可以一次为 Array1 分配一个大小为
100 的内存空间。但是对于 while 循环,由于循环次数不能在循环运行前确定,LabVIEW 无法一
次就为 Array2 分配合适的内存空间。LabVIEW 会在 while 循环的过程中不断调整 Array2 内
存空间的大小,因此效率较低。
所以,在可以确定次数的情形下,最好使用 for 循环。
二. 移位寄存器
移位寄存器除了在迭代间传递局部数据,还有其他一些功能。
首先,移位寄存器可以用于程序的内存优化。
由于移位寄存器的左右接线段使用的是同一块缓存,可以利用这一特性,显示的告诉 LabVIEW
重用某些数据的内存,并且不对数据做额外的拷贝。详细说明可以参考:LabVIEW 程序的内存优化。
移位寄存器还经常被当作全局变量来使用。<这里还没写完
LabVIEW 中的数字型数据 1 - 控件和常量
一、数值型控件和常量
1. 控件
在LabVIEW的控件栏中有一栏是数值控件。
这一栏内的控件虽然在前面板上的外观各不相同,但是在框图中的端点都是数值类型的。我们在
使用这些数值控件时可以选择适合的界面所需的旋钮、仪表盘等。还可以在数值型控件的属性对话框
里设置它的数值类型、数值范围、格式和精度等,显示方法等等。
我们以最普通的数值控件为例,解释一下如何配置它的显示方式。假如,我们的界面是 Windows
风格的,那么界面上所有的控件都应使用系统风格的控件,包括数值型控件。如果这个控件用于表示
时间,我们就需要对这个控件的显示方法进行高级的配置。
打开这个控件配置面板的格式与精度页,选择“Advanced editing mode”,就可以自己为控件设
置显示方式了。
2. 常量
如果是常量,在设置数值类型时通常会发现“Adapt to Source”(按照输入调整)项是被选中的,
作为控件时这一项不能被选中。此时如果在常量中输入一个正数,比如“34”,常量的类型会自动变为
I32整型(蓝色),而输入“34.3”, 常量的类型会自动变为DBL实数型。如果一定要输入实数型34,
可以输入34.0。
3. 不同表示法(Representation)的选择
数值类型包括各种长度的整型、实数型和虚数型,其中I64和U64类型是LabVIEW 8.0新增
加的。选用短整型数值比选用长整型数值类型节约内存。在大的数值数组中应尽量使用短类型数值以
节约内存。对于单个数值,它可以节约的内存十分有限,但是使用长整型数值可以避免数值越界引起
的错误,所以还是应该使用长整型数值。
你可以自己做个试验:新建一个 VI,在 VI 上放置两个值为 300 的 I16 常量,然后相乘,看
看他们的积是多少。这种错误如果隐藏在一个大工程内,调试起来也是颇为费劲的。
LabVIEW 中的数字型数据 2 - 运算
二、运算
1. 常用函数
与数值数据相关的运算处理节点大都在函数栏的 Programming -> Numeric 项里,如图1 所
示。
图1:Numeric Function Palette
从这些函数节点的图标一眼就可以看出它们的用处了。例如,加、减、四舍五入、求倒数等。更
全面的数学运算函数在 Mathematic 函数栏。Mathematic 函数栏内的很多运算不仅是针对单个数
值的,还可用于数组运算。
这里每一个公式的用途都可以在 LabVIWE 帮助文档上找到,我就不重复了。我们在这里着重讨
论一下,在众多类似的运算方法中,如何选择一个适合你的程序的方案。
对于简单一次性加减乘除,自然使用基本的函数节点就够用了。但是,如果是复杂的数值运算,
则需要大量函数节点。节点之间的连线可能会有转角甚至相互交叉,显得比较杂乱,不利于程序阅读
和维护。这时我们可以使用其它运算节点。
2. 表达式节点
对于只有一个输入和一个输出的运算,我们可以使用表达式节点(Expression Node)。
图2:Expression Node 的应用实例
图2 所示的例子中,完成把华氏温度转换为摄氏温度的计算。F1 到 C1 的转换是通过基本运算
节点完成的。尽管运算并不复杂,但是阅读程序的人仍然无法立即就意识到这个运算与书中给出的公
式相对比是否正确,还需要仔细地一步一步判断。这是图形化语言在表达纯数学计算时不利的一面,
文字表达方式此时会更为直观易懂。表达式节点是使用文字来描述运算的。F2 到 C2 的转换就是使
用表达式节点来完成的,用户可以直观地读出该节点所使用的公式。
与使用基本运算节相比较,表达式节点另一个优点是节省了框图上的空间。
在公式节点中只允许有一个字符串,代表输入参数,例如本例中,参数用 f 表示。LabVIEW 在
线帮助里列出有表达式节点所支持的运算符、函数和表达式规则。
3. Formula Express VI
如果运算有多个输入,可以使用 Formula Express VI。该 VI 在函数栏 Mathematic ->
Scripts & Formulas 下。图3 是这个 Express VI 的配置面板,它看起来就像是一台高档计算器,
基本不需要学习就可以使用了。
图3:Formula Express VI 的配置面板
Formula Express VI 的缺点是:他的表达式是隐藏起来的。用户需要查看,还得先调出配置面
板才行。
4. 公式节点
对于更加复杂的计算,尤其是当输入变量超过一个的时候,应该使用公式节点(Formula Node)。
公式节点中的表达式语法与 C 语言类似。可以把它看作是更为复杂的支持多输入输出的表达式节点。
它的优点也与表达式节点相同:
在实现算法时,人们往往更习惯于文本表达方式,所以使用公式节点的可读性和可维护性更强。
图4:Formula Node
5. MathScript,MATLAB Script 和 Xmath Script 节点
这三个脚本节点比较类似,都应用于处理更为复杂的数学运算,比如大型矩阵运算等。脚本语法
使用 MATLAB 的语法或与 MATLAB 极为类似的语法。
LabVIEW 中的数字型数据 3 - 数值的单位
三、数值的单位
1. 数值控件上的单位
数值型控件和常量是可以带单位的。在数值型控件的快捷菜单上选择“Visible Items -> Unit
Label”,就可输入数值的单位。如果你对某个单位的正确拼写没有把握,可以先任意输入一个字符,
然后用鼠标右键点击单位标签,选择“Build Unit String…”。这时,LabVIEW会弹出一个对话框,
LabVIEW所支持的单位都在这里分类排出。
图1~3:使用数字控件的单位
例如要计算2年有多少天,可以有如下的程序:
图4,5:同类型单位的空间可以由数据传递
2. 单位使数据类型检查更严格
把一个 I32 型的数据赋值给 string 型的控件肯定是一种错误行为,程序员总是希望编译器在编
译时就把这种错误报告出来。虽然现在大多数编程语言都可以在编译时报告此类错误,但 LabVIEW
数值类型的单位可以让这种检查更严格:实数与字符串之间不可以互相赋值;同样是实数型的俩个数
据,一个表示时间,一个表示长度,他们之间也不应当相互赋值。
在编写 LabVIEW 程序的时候,应当尽量使用带单位的数值控件。因为,如果你给一个数据设置
了单位,LabVIEW就会自动帮助你进行单位的一致性检查。比如图6 所示,当你试图把表示时间的
数据和表示长度的数据相加时,LabVIEW会禁止你连线。 着帮助你防止了编程时出现的不一致性
错误。
图6:不同类型的数据不能进行计算
但是,这种严格的一致性检查也可能会带来麻烦。例如,我们编写了一个子VI,用于计算两个时
间单位的和。下次当我们需要一个计算长度单位的和的子VI时,却不能够直接使用已有的计算时间
单位的子VI,因为它们的单位是不同的。为了解决这个问题,LabVIEW 提供了单位统配符。
在编写需要用于不同单位的子VI时,可以使用单位通配符。单位的通配符用 $n 表示,其中 n 是
1 到 9 之间任意一个数字。例如我们以上提到的加法,可以在子 VI 中使用通配符 $1,如果还需
要另外一个执行其他运算的子 VI 中,其单位可以用 $2 表示。
图7:使用单位通配符
3. 单位转换
使用 Numeric->Conversion->Convert Unit 节点可以把一个纯数字量转换为带有单位的数
字量,或者反过来转换。使用 Cast Unit Base 节点可以更灵活地把某一数值的单位直接转换成另
一单位。需要注意的是,Convert Unit 节点的外观和表达式节点的外观一模一样,甚至快捷菜单都
一样,这应该是LabVIEW的一个缺陷。但他们的功能完全不同,你不要试图在表达式节点中使用
build unit 菜单,它不执行单位的转换,也不指示有差错。
几种简单的测试程序流程模型
大多数测试程序主要步骤就是以下几步:采集数据、处理数据、显示数据、保存数据。这几个步
骤可以顺序执行。在一次实验中,通常要多次循环这一过程,因此,这种测试程序的模型如图1所示。
图1中最后一个子 VI 是用来判断实验是否结束,是否进行下一次循环用的。在这个模型中,各个
程序模块是单线程顺序执行的,它的好处是程序逻辑简单,容易设计和理解。
图1:顺序测试程序的模型
但是对于单线程的程序,计算机必须执行完一个任务,才能再进行下一步工作。比如,尽管数据
存储是一个相对比较慢的过程,但计算机必须还是要等到它执行完,才能去做下一步的采集数据工作。
对于速度要求较高的测试程序,最好把这两样工作同时进行,以节约时间。这样,我们可以在两
个循环内分别做数据采集,和其它的工作。因为数据采集的速度一般来说高于处理和存储的速度。当
新数据被采集来,上次的数据可能还没处理完呢。所以可以先把每次采集到的来不及处理的数据放在
缓存里。这种模型如图2所示。它实质上也就是 LabVIEW 在新建VI的模板中的
“Producer/Consumer Design Pattern”。
这个模型的实际应用程序会更加复杂,相比第一个模型不是那么好理解和维护。
图2:数据采集和后续工作并行执行的模型
不过还有一个折衷的方案,既保证各个任务同时运行,又不至于太复杂。如图3所示,在这个模
型下,所有的任务同时运行:采集新的数据、处理上一次采到的数据,显示保存上一次处理好的数据。
在这个模型下,要注意第一次循环运行时处理的数据,和循环头两次运行显示存储的数据是无效的,
实际循环终止条件式也要考虑到,采集的数据再两次循环后才被保存下来。
图3:并行执行每一任务的模型
《我和 LabVIEW》
用 LabVIEW 编写 Wizard 类型的应用程序 2 (LabVIEW 6.1 ~ 7.1)
四、Tab 控件+事件处理结构
LabVIEW 6.1 的出现才第一次大大简化了 Wizard 界面风格程序的编写。LabVIEW 6.1增加
了两个非常重要的新特性,一个是Tab控件,一个是事件处理结构。
有了Tab控件,就可以把 Wizard 中每一页需要的控件分别放在 Wizard 不同的页面上,切换
Tab 的活动页面也就显示了该页面上相应的控件。
事件处理结构的应用更为广泛。有了它,编程者就不需要再添加额外的代码来监视每个控件的状
态改变以及鼠标、键盘等的操作了。
这种利用Tab控件和事件处理结构编写的 Wizard 风格界面程序的方法现在仍然被广泛使用着,
下面这个链接就是一个采用这种方法编写的软件:
/nilex/?id=101NILEX。
它的功能是把一个 C 语言开发的仪器驱动程序转换为 LabVIEW 下的驱动程序。程序虽然是我
编写的,但版权属于NI公司,所以不能把程序源代码公开给大家。
这种方法也有它的弊端。因为整个 Wizard 界面会用到的所有控件都集中在同一个 VI 上,这个
主VI就可能特别庞大:界面可能有数十个控件,程序框图上的事件处理更为复杂,有近百个事件也
不作为奇。如果需要对程序作修改,要找到相应的事件框就已经很困难了,要确定这个改动是否会影
响程序的其他部分就更为困难了。
图1是我编写的一个Tab控件风格的向导型程序,它的主VI中的事件结构中,有近百个事件需
要处理。对这样的程序,想找到一个相应的时间都很麻烦,处理好事件之间的关系就更困难了。
Tab 控件+事件处理结构的架构虽然大大简化了 Wizard 界面风格程序的编写,但是这样的程序
很难对他的代码进行更细致的模块划分,并把模块的私用数据隐藏起来。为了使大型 Wizard 程序
有更好的可读性,可维护性,还需找到一种更好的架构。
图1:使用Tab控件的向导型程序,事件结构中众多事件
相关文章:
用 LabVIEW 编写 Wizard 类型的应用程序 3 (LabVIEW 8.0)
五、SubPanel
主VI太过复杂,是肯定会影响它的可读性和可维护性的。所以,对向导类型程序的进一步改进的
重点,就是把主VI进一步模块化,不但是程序代码要模块化,界面也必须模块化。代码模块化相对
比较简单,多利用子VI就是了。但是界面的模块化,在之前的LabVIEW中是非常困难的,因为
LabVIEW 没办法在运行时,把不同的 VI 的界面拼在一起。是 LabVIEW 7.1 和 8.0 的一些新
功能最终解决了这个问题。
对程序界面模块化,按一般的思路,第一步就是把每个页面划分成一个独立的模块。这似乎又回
到了我们前文提到过的第一、二个阶段。但有所不同的是,旧版本 LabVIEW 功能不全,无法很好
的管理被分为模块的页面,而新 LabVIEW 改进的对这方面的支持。
在 LabVIEW 7.1 中出现了一个新的控件 - SubPanel(子面板)。当一个 VI 运行的时候,它
的 SubPanel 控件中,可以显示另一个 VI 的前面板。我们可以利用这个新的控件,我们可以使用
插件框架式程序架构来编写向导型的程序。图1是这种插件框架式程序结构的示意图。
图1:插件框架式的程序结构
插件框架式程序的实现思路是,把向导的每个页面都分配到一个独立的VI上去,这个页面上所有
的操作,都有这个页面所在的VI完成。图1左上部分的那些 VI 就是为每个页面编写的VI。这些 VI
都被当作插件,在主程序需要的时候被调用显示在主程序上。
图1右下角的VI是主程序的 VI。它的界面上主要是一个 SubPanel 控件,这个控件用于显示
页面VI的界面。主程序在每一步的时候,分别把对应这一步骤页面的VI的界面显示出来,这样就实
现的向导功能。主程序的界面上还有一些公共控件,比如“上一步”“下一步”这样的按钮,这些按钮在
所有步骤中都需要,所以可以放在主框架上,不需要再在每个页面中重复了。
这样的插件框架式程序在运行时,主VI和插件VI是在同时运行的。
主 VI 的运行流程大致如下:创建或注册程序运行时需要的各种事件 -> 初始化程序 -> 等待和
处理事件,主要是管理插件。比如在用户按下“下一步”按钮后,主程序负责把当前的插件移出内存,
把对应下一页的 VI 调入内存,运行,并显示界面。 -> 最后负责销毁创建的事件,关闭所有资源,
退出。
插件 VI 的主要程序结构和主 VI 一样,采用的是事件处理结构。它在运行起来以后执行的流程
也和主 VI 类似: 创建或注册插件运行时需要的各种事件 -> 初始化程序 -> 等待和处理事件,主
要是用户在界面上的操作,和一些后台程序,比如数据处理等等。 -> 销毁创建的事件,关闭插件。
虽然 SubPanel 在 LabVIEW 7.1 中就出现了,但是我当时却并没有在我的程序里采用上述的
设计方案。只是因为当时还有一个棘手的问题没有解决。这个问题就是 VI 太多了,不好管理。
向导页面的多个插件 VI,他们的功能有很多共同之处。在以前,所有页面都在同一个主 VI 中
的时候,那些相同的功能可以通过调用同一个子 VI 来完成。但是,把页面分割成独立 VI 之后,很
多情况,我都不得不为每个页面做一整套子 VI,他们在每个页面上完成的功能都类似,但却不能使
用同一个子 VI。
以处理事件为例,我写了一套子 VI 处理页面 VI 的事件。但是由于不同的页面可能会同时在运
行,每个页面都有自己的事件,如果调用同一套处理事件的子 VI,不同页面之间会相互干扰。
另外,如果想创建一个新的页面,最方便的方法莫过于把一个已有页面的 VI、子 VI 全部复制
一遍,然后在其基础上做改动。LabVIEW 以前是不允许出现同名 VI 的。把一个页面的 VI、子 VI
全部改名,还要保证调用链接不出现混乱,非常的不方便。所以上述的插件框架方案是我等到到
LabVIEW 8.0 出来以后才开始使用的。
六、Project Library
LabVIEW 8.0 作为一大升级版本,拥有了很多新特性。其中之一是“Project Library(工程库)”。
工程库是一组功能相关联的VI或其它文件的集合。一个工程库是把一组功能相关的VI,和其她
文件按一定结构组合封装在一起,以便于代码的管理和发布。工程库的名字也是库中VI的名字空间
(name space)。这个名字空间与C+ +、C#等语言中的名字空间的概念类似。有了它 LabVIEW 就
可以在一个程序中使用两个同文件名的 VI,当然,它们的名字空间不能相同,也就是它们存在于不
同的工程库中。
另外,工程库中的 VI 有操作安全设置, 每一个 VI 可以被设置为公有(Public,可以被库外的
VI调用);或者私有(Private,只能被库的成员VI调用)。
工程库给开发插件框架式的程序带来的很大的方便,特别是在 VI 文件的管理方面。
新的设计思路是这样的:把所有的功能模块都封装在工程库内,比如说每个页面都有一个对应的
工程库。专为这个页面使用的所有子 VI 都被加在它的工程库内。并且,不想被其它库使用的的子 VI
都要标记为私有。被这样组织起来的程序,虽然 VI 数量还是很多,但模块划分清楚,不会出现不希
望出现的调用关系。安全性,可维护性就大大提高了。
另外,可扩展性也得到了提高。如果需要添加一个新的页面,只需要把一个已有页面复制一份。
复制出来的这一份,只要工程库的名字换个新的就行了。再也不需要一个一个的去改VI的名字了。
图2是我的一个程序的工程管理窗口:
图2:采用工程库管理程序的VI
但是,现在的程序结构还是有些令我不太满意的地方-重复的代码太多。不同页面之间,有很多类
似的VI。就比如图2中的程序,每个页面都会用到事件处理的一些 VI,他们的代码在每个页面中都
是相同的。但是,利用这个工程库组织起来的程序却不能把这些重复的VI提取出来,变成共用的子
VI,因为在每个页面里,这些代码相同的VI,处理的数据是不同的。并且这些数据会保留在 VI 的
局部或全局变量中,不同的页面如果共用一套子VI,会相互影响,出现数据混乱的。
直到 LabVIEW 支持了面向对象的编程之后,我们才终于找到了一个完美的解决这一问题的方案
在 LabVIEW 中使用常量定义
如下图所示,在C语言里,使用#define来定义一个常数是非常基本的用法。直接使用数字,时
间一长,就不只到这个数字是哪来的了。而且,这种方法也便于修改在程序中多处使用的常量的值。
在C++一般是用const来达到同样的目的。
图1:C 语言中的常量定义
我以前在LabVIEW中编程,还从没注意过这个问题。一般哪里要用一个常数,直接就放一个
constant在那里。如图2。
图2:在 LabVIEW 中使用常量的最普遍方法
以前编写的LabVIEW程序都比较小,一般是一个人开发的,所以这样写,也没有太大的麻烦。
现在编写的程序规模越来越大,最近做的一个项目,VI数量已经上千了,有4个人参与编程。程序
规模大了,不规范就很难维护。所以开始考虑这个问题。
但是LabVIEW里面没有类似的功能,不知道为什么以前没人提意见?
下面提出几种不算太完美,但有所进步的解决方案。
一种简单的替代方法是使用type define control,自定义一个Ring control。关于 Type Def 的
详细信息,可以参考《用户自定义控件中 Control, Type Def. 和 Strict Type Def. 的区别》。把
要使用的常数作为Ring的值,给他个有意义的文字标签。在需要时用常数的地方,把这个带type
define 的ring常数放上去,而不是直接放数值常量。这样就解决了上面提到的一个问题:可以有自
带的文字说明。如图3所示。
图3:利用 Type Def Ring 的解决方案
但是这样做还是有很多缺陷。首先是统一修改数值的问题。在自定义Ring中修改某一项的值,
相关的常量不会跟着一起更新;还有一个缺陷是 Ring control 不支持多个标签是用同一数值;另外
Ring control 也没办法像 C 语言中一样使用表达式定义值。
一个改进版的解决方案是使用 Enum Type Def 把所有常量名字列出来,再写一个 VI 用于得
到常量的真实值,如图4所示。这样解决了不同标签可以返回相同值的问题,也可以自动更新常量值,
但是使用表达式还是不方便。
图4:利用 Enum Type Def 和 subVI
我目前在程序中使用的方法是,把所有要用到的常量,全部做成全局变量。全局变量可以用
Global ,但我喜欢用 VI 全局变量。就是把变量记载 shift regisiter 中。然后,用一个初始化的 VI
负责在程序运行开始时初始化所有的全局变量。这样,以后如果需要更改某一常数值,就只需改这一
个VI就可以了。
不过,现在回想,还是用 Global 好一些。我以前测试过,Global 读写的速度比 VI 要慢很多,
所以我不喜欢 Global 。但是,常量值在程序中用的并不频繁,所以速度不是个问题。但是数量很多,
用 VI 表示就不太合适了,每个常数都要创建一个 VI 非常费事。另一个缺点是如果在后面板换用一
个常量,还要再拖另一个 VI 上来,很麻烦。用 Global 会好一些,但还不是让我太满意。
要想有一个完美的解决办法,只能再造一个新东西了。@#$%^&* (此处属公司机密,删去256
字)
LabVIEW 的调试环境
1. LabVIEW 的全局选项
在 LabVIEW 8.2 中打开 Tools -> Options 菜单项,选择其中的 Debugging,会出现如下
四个选项:
图1:LabVIEW 与调试相关的选项
a) Show data flow during execution highlighting 表示在高亮显示执行的过程中显示数据的
流动。
b) Auto probe during execution highlighting 表示在高亮显示执行的过程中,数据从每个接
线端流出时,显示数据的数值。
c) Show warnings in Error List dialog by default 表示在默认情况下,在错误列表的对话框
中显示警告信息。
d) Prompt to investigate internal errors on startup 表示在 LabVIEW 启动时检查是否存
在内部错误。
如果你仅从字面上还不能理解上述几个选项的含义,不要紧,后面的章节里会详细介绍它们的含
义。
2. VI 的属性
某些 VI 的属性设置可能会导致你无法调试这个 VI。比如,VI 被设置为有密码保护,而你又不
知道密码是什么;又比如,VI 被设置为不允许调试等。禁止 VI 调试可以大大提高 VI 的运行速度,
降低 VI 的内存占用,所以,在 VI 发布给用户之前,最好把它设为不可调试。
图2:VI 的属性设置
3. 调试工具
VI 程序框图上的工具栏中,某些按键是用于调试的。
图3:正在运行的一个 VI 的程序框图
图3 是一个正在运行的 VI 的程序框图。我们看到的工具栏上的按钮的图形,基本就可以猜出它
的功能了。
用于停止整个程序的执行。
用于暂停或者继续程序的执行。
用于启动高亮显示执行。在高亮显示执行时,LabVIEW 会放慢代码的执行速度,并且在程
序执行到每一个节点时,高亮显示这个正在被执行的结点。高亮显示执行的速度非常慢,所以启用它
要非常小心。如果启动高亮显示的同时,你的某个 VI 前面板是模式的(modal),那么你想中途关
掉它是不可能的了,你只能非常痛苦地等待程序的结束,或杀掉整个 LabVIEW 进程。
用于保留 VI 程序框图上数据线中的数据。
用于单步执行,它们三个分别表示进入、跳过或跳出某个节点、结构以及子 VI。
下拉框表示 VI 的调用关系。打开下拉框,可以看到当前 VI 从低层
到高层的逐级被调用关系。选择下拉菜单中的某一项,即可跳到那个 VI 被调用的地方。
是设置断点的地方。
是设置探针的地方。图3 上的悬浮窗口显示的就是探针所在处的数据。
在需要设置断点和探针的地方按鼠标右键,在弹出菜单里可以选择 Set Breakpoint 或者
Probe,或者通过使用工具选板(Tool Palette)上的断点和探针工具进行设置。
断点和探针
1. 断点
断点和探针是调试 LabVIEW 代码时最常用的两个工具。LabVIEW 中的断点在使用和功能上都
比较简单、直观:使用工具选板上的断点工具,在想要设置或者取消断点的代码处点击鼠标即可;
或直接在程序框图的节点、数据线上右击鼠标,就可以看到设置或取消断点的菜单项。
断点几乎可以设置在程序的任何部分。当程序运行至断点处,就会暂停,等待调试人员的下一步
操作。很多其他语言的调试环境都有条件断点,LabVIEW 的端点没有类似的设置,LabVIEW 是使
用条件探针来实现条件断点功能的。
断点是会保存在 VI 中的。关闭带有断点的 VI,程序执行至断点处还是会停下来,并且这个 VI
会被自动打开。
如果某个 VI 不允许你设置断点,很可能这个 VI 被设为不允许调试了。此时,只要在 VI 属性
中重新设置一下即可。(LabVIEW 的调试环境.2)
2. 探针
探针的功能类似于其他语言调试环境中的查看窗口,用于显示变量当前状态下的数据。LabVIEW
与其他语言不同之处在于,LabVIEW 是数据流驱动型的图形化编程语言。LabVIEW 中的数据传递
主要不是使用变量,而是通过节点之间的连线完成的。所以 LabVIEW 的探针也不是针对变量的,
而是加在某根数据线上的。
LabVIEW 的探针也是图形化显示的。比如为一根数字类型的数据线加探针,探针一般就是一个
数字型显示控件,见图1。Error Cluster 类型的数据线的探针,则看上去就像是个 Error Cluster,
见图2。
图1、图2:数值型和错误信息型数据线的探针
3. 选取其他类型控件作为探针
如果你觉得 LabVIEW 默认的探针不美观或不适用,则可以在数据线上点击鼠标右键,选择
Custom Probe -> Controls -> ... 选取一个其他控件作为探针,如图3。但是要注意,你选取的
控件的数据类型要与数据线的数据类型一致才可以。
图3:使用仪表盘控件作为数值型数据线的探针
4. 条件探针
在你设置断点后,程序在每次执行到断点的时候都会停下来。但有的时候,调试者希望程序只在
被监测的数据满足某一条件时,才暂停运行。比如,被监测的数据在正常情况下应大于零,调试者希
望一旦数据小于零则暂停。在 LabVIEW 中,可以使用条件探针来实现这样的功能。
图4:数值型条件探针
以图4 为例,如果你希望程序中的循环在运行 8 次以后才停下来,就可以使用条件探针。在记
录循环次数的 i 的输出数据线上点击鼠标右键,选择 Custom Probe 下以 Conditional 开头的探
针,打开探针上的 Condition 页,就可以设置条件了。此时,若被探测的数据满足你所设置的条件,
程序就会暂停。
5. 用户自定义探针
如果你觉得 LabVIEW 自带的探针功能还不够强大,或者你自己创建了一种数据类型,而
LabVIEW 没有适合它的探针,这时你可以自己创造一个满意的探针出来。
用户自定义的探针其实也是一个 VI。LabVIEW 自带了一些已经做好的探针,这些探针都被放置
在
做的。比如我们在图4 中所使用的 I32 型条件探针的 VI 是 。
需要新建一个自定义探针时,先在数据线上点击鼠标右键,选择 Custom Probe -> New。这
时 LabVIEW 会弹出一个向导界面。按照向导的提示,输入所需信息,LabVIEW 会为你生成一个
用作探针的 VI 框架,对这个 VI 稍作修改,即可成为一个新的探针。
这个探针 VI 有一个输入和一个输出。输入的是被探测的数据,输出是一个布尔类型,表示程序
是否需要暂停。这个 VI 的界面也就是探针的外观。探针所实现的功能完全依赖于如何对其编程。
其它常用调试工具和方法
除了断点和探针这两种最常用的调试工具外,我们也经常要借助一些其它的工具和方法来找到程
序的问题所在。
1. 性能和内存查看工具(Profile Performance and Memory)
调试的目的并不一定仅要找出功能性错误,有时是要找到程序效率低下的原因,或者潜在危险,
如内存泄漏等。这时就要用到 LabVIEW 的性能和内存查看工具了。参见:LabVIEW 的运行效率 1
- 找到程序运行速度的瓶颈。
2. 显示缓存分配工具(Show Buffer Allocation)
显示缓存分配工具是另一检查 LabVIEW 代码内存分配情况的强大工具。参见:LabVIEW 程序
的内存优化。
3. 程序框图禁用结构(Diagram Disable Structure)
调试首先要找到问题发生的部位。有时候,我们可以使用探针一路跟踪数据在程序执行过程中的
变化。如果数据在某个节点的输出与预期的不一致,这个节点很可能就是问题所在。还有些情况,不
是靠这种简单方法就可以找出问题的。比如程序中出现的数组越界的错误,在错误发生后,程序可能
还会正常运行一段不确定的时间,然后崩溃,或报错。这种程序报错,或者崩溃的地方有可能在每次
调试时都不同,或者找到了最终出错的代码,发现他是个最基本的 LabVIEW 节点,不能再根据去
调试了,而这个节点出错的可能性基本为零,错误肯定是其他地方引起的。
调试这种问题,一般就是把一部分代码禁止掉,看看程序运行是否还有问题。如果没有问题了,
说明有毛病的代码被禁止运行,则在把禁止代码的范围再缩小;如果问题又出现了,说明是刚刚被放
出来的代码有毛病,则对这部分代码再禁掉一部份,继续调试。知道找出引起问题的一个或几个节点,
改正它们。在这个仅用部分调试代码的过程中,使用程序框图禁用结构是最为方便的了,它就好象是
C 语言中用来做注释的关键符号“/* */”或者“//”。使用它可以方便的把一部分代码框住,禁用,
如图1。
图1:程序框图禁用结构
使用程序框图禁用结构需要注意的一点是,这个结构可以有多个 Disable 的页面,同时会有一个
Enable 的页面。调试人员可能还要在 Enable 的页面作一些改动,比如为输出数据添加一些虚拟
值,以使后续程序可以程序可以正确运行下去。例如图2,为了让后续的程序继续正确运行,需要把
reverence 和 error 数据线连接上。
图2:修改 Enable 页面
4. 条件禁用结构(Conditional Disable Diagram)
LabVIEW 中还有一个类似于 C 语言中 #if,#ifdef 的结构,就是条件禁用结构。使用条件禁
用结构可以让某些代码在特定的条件下不运行。与条件结构(Case Structrue)相区别,条件结构
在运行时决定执行哪一个页面中的代码;而条件禁用结构是在编译时就已决定好执行哪一个页面的代
码了,不被执行的页面的代码在运行时都不会被装入内存。
利用条件禁用结构的这一特性,可以把分别需要在调试时和发布后的代码放在不同的条件禁用结
构页面内。这样,既可以在不同条件下运行不同的代码,有不会使程序留有冗余的代码。图3 的是
一个条件禁用结构应用的典型例子,用户希望在开发调试时,如果错误数据线上出现错误,则探出错
误信息的对话框;而在发布之后,又错误发生,也不可以弹出对话框。
图3:使用条件禁用结构控制调试时和发布后程序的不同行为
点击条件禁用结构右键弹出菜单中的 Edit Condition For 条目可以弹出
条件配置窗口,在这个窗口改变使本页运行的条件。LabVIEW 有一些预定义的符号(Symbol)可
供条件禁用结构使用,比如 TARGET_TYPE 表示目标代码在什么系统下运行。如果条件是
“TARGET_TYPE == Mac”表示目标代码运行在苹果机上。
如果你有工程文件“*.lvproj”,那么还可以在工程文件的属性->条件禁用符号栏下配置自己需要
的符号。如图3中的例子,就是我自己在工程的属性对话框中添加了一个“DEBUG”符号,这样我就
可以通过更改 DEBUG 符号的值来控制是否弹出程序的错误对话框。
5. 使用消息对话框和文件
有一些错误是在关闭了调试信息后才出现的,或者出错的代码部分不允许使用 LabVIEW 的调试
环境。这时就要使用类似 C 语言中 printf() 的功能了。具体实现方法就是把可以的数据在程序中
用 messagebox 显示出来,这样就可以跟踪察看程序是在哪一部分出错的。还可以把所有相关的
数据都保存在一个状态记录文件中,察看这个记录文件,就可以找出可以的错误。
状态记录文件可以与第4节提到的条件禁用结构联合起来使用,设置一个调试开关,再调试运行
方式下记录下所有状态信息;在正式发布后不再记录仪提高程序运行效率。
LabVIEW 代码中常见的错误
发现了程序的问题再回头去调试,在查找程序错误时就不可避免地要花大量时间。要调高开发效
率,最好是在编写代码时就避免一些常见的低级错误,这样可以节约大量的调试时间。
有些编程错误差不多是每个 LabVIEW 程序员都曾遇到过的。在编写相关代码的时候,对这些问
题多留心一下,就可以大大减少调试时间。
1. 数值溢出
图1:数值溢出错误
图1 中的 VI 只做了一个简单乘法 300*300 ,不加思索就应该知道答案是 90000,但程序中
乘法节点给出的结果却是 24464。乘法节点是不会错的,错误是由于程序中使用的数据类型是 I16。
I16 能表示的最大数目只有32767,所以在乘法计算中出现了溢出。
避免此类错误的方法是,在程序中使用短数据类型时,一定要确认程序中的数据绝不会超出该类
型可以表示的范围。
2. For 循环的隧道
循环相关的介绍可以参考《循环结构》。
数据传入传出循环结构可以通过移位寄存器(Shift Register)和隧道(Tunnel)两种方式。隧
道又有两种类型:带索引的和不带索引的。
移位寄存器一般用在需要局部变量的情况下,循环运行一次的输出数据要作为下次运行的输入数
据使用;循环外的数组数据通过带索引的隧道在循环体内就可以直接得到数组元素;除此之外,简单
地在循环内外传递数据,使用一般的隧道就可以了。
值得一提的是,如果一个数据传入循环体,又传出来,那么就应该使用移位寄存器或带索引的隧
道来传递这个数据,尽量不要使用不带索引的隧道。因为 For 循环在运行时,循环次数有可能为0。
在循环次数为0时,大多数情况,用户还是希望传出循环的数据就是传入值,但使用不带索引隧道时,
输入值有时会被丢失的。如果使用移位寄存器,即使循环次数为0,也不会丢失传入的数据。因为移
位寄存器在循环上的两个接线柱指向的实际是同一块内存(参考:LabVIEW 程序的内存优化),而
输入输出两个隧道指向的是不同的内存,数据不一定相同。
图2:For 循环上的隧道
图2中的程序, vi reference 传入,再传出循环均使用了隧道。如果循环次数为0(Controls
数组为空),vi reference 再传出循环时,信息就丢失了。这不但有可能造成后续程序的错误,而
且由于 vi reference 的信息丢失,再无法关闭打开的 vi,造成了程序泄漏。
Error 数据线(黄绿色的粗线)在传入传出数组时,一定要使用移位寄存器。原因还不仅是为了
防止在循环次数为0时,错误信息丢失。通常一个节点的 Error Out 有错误输出,意味着后续的程
序都不应该执行。在错误的情况下继续执行程序代码,风险非常大,可能会引起程序,甚至系统崩溃。
只有使用移位寄存器,某次循环产生的错误才会被传递到后续的循环中,从而及时阻止后续循环中的
代码被运行。
3. 循环次数
与其它语言相比,LabVIEW 的 For 循环有一大特点,在某些情况下它并不要求一定要输入循环
次数,而可以根据输入数组的大小自动决定循环次数。通过带索引的隧道,可以把数组分解成元素传
递到循环体内,此时不需另行设置循环次数N,循环的次数就是数组的长度。每次循环,带索引的隧
道便给出一个元素。
循环体上可以有两个或更多的输入数组使用带索引的隧道,此种情况下容易引起错误。这时,循
环的次数等于几个数组中长度最短的那个数组的长度。如果另外又设置了循环次数N,那么循环次数
就是N与输入数组长度这两者的最小值。调试时,如果发现一个本该运行多次的循环没有运行,那
么很可能就是因为它的一个输入数组是空的。
While 循环同样也可以使用带索引的隧道,但是我不建议大家这么用——如果需要用到带索引的
隧道,还是使用 For 循环更为适宜。因为 while 循环的循环次数不由数组个数决定,而是由停止条
件决定的。如使用了带索引的隧道,你还需要考虑当数组大于、小于循环次数时,程序应该如何处理,
所以还是在循环体内作索引比较方便。如果希望循环次数与数组大小保持一致,那自然是用 For 循
环的程序更加清晰易懂了。
4. 移位寄存器的初始化
图3:没有初始化的移位寄存器
看图3中这个程序,因为它在 while 循环上使用了带索引的隧道,所以可读性不那么好。array
out 的运行结果是什么,还要考虑一阵子才能给出答案。实际上这个程序,即使输入不变,每运行一
次,array out 的结果都是不一样的,它的长度一直在增加。这个问题就出在没有给程序中的移位寄
存器一个初始值。
没有初始化的移位寄存器,总是保存上次运行结束时的数据。这个特点在某些情况下可以被程序
员利用,比如用它当作全局变量,随时把数据存入或取出(一个例子是《如何使用 VI 的重入属性》
中的图4)。但多数情况下移位寄存器还是被用作为循环内部的局部变量的,这时就一定要对它初始
化,以防止潜在的错误。
5. Cluster
图4:Cluster 传递数据出错
图4的程序中有个奇怪的错误,明明应该是 weight 加 1 怎么运行完后的结果变成了high 加
1 了呢?直接揭开谜底吧,原因是 Cluster 中的元素有个顺序,这个顺序可以和界面上看到的顺序
不一致。分别鼠标右击程序中的两个 Cluster,选择“Reorder Controls in Cluster”,就可以看到
每个元素在 cluster 中的编号。info out 中的 high 实际上编号是 2,第三个元素。
为了避免 cluster 中用可能出现的错误,以及让 cluster 应用起来更方便,使用 cluster 最好
遵循以下原则:
1. 凡是用到 cluster 的地方,就为它造一个类型定义(《在程序中使用类型定义》),在程序
所有要用到这个 cluster 的地方,都使用类型定义的实例。这样一是可以保证所有的 cluster 都完
全一致,避免图4 这种错误;二是一旦需要变动 cluster 中的元素,只需在类型定义中更新就可以
了,不必挨个 VI 修改。
2. 凡是在需要解开(unbundle)或打包(bundle)的地方统统使用 unbundle by name 和
bundle by name 来实现。使用带名字的 bundle,unbundle 可以直观的显示出 bundle 种元素
的名字,这样不会因为顺序的不同而导致错误的连线。
6. 并行运行
LabVIEW 是自动多线程的编程语言,这一点在方便用户的同时,也会带来一些麻烦。比如最常
见的情况,多线程会引起数据或资源的竞争错误(race condition)。
图5:两个并行运行的子 VI
图5是一个简单的两个子 VI 并行运行的例子,在这个例子中就隐藏着一个潜在的问题。并行执
行的两部分程序,先后次序是不定的。有可能关闭程序中的引用数据(绿色的线上的数据)的节点在
子 VI B 结束前运行。而子 VI B 是要用到这个参考数据的,这是子 VI B 就会因为它所需要的数
据失效而产生错误。
LabVIEW 代码中常见的错误
发现了程序的问题再回头去调试,在查找程序错误时就不可避免地要花大量时间。要调高开发效
率,最好是在编写代码时就避免一些常见的低级错误,这样可以节约大量的调试时间。
有些编程错误差不多是每个 LabVIEW 程序员都曾遇到过的。在编写相关代码的时候,对这些问
题多留心一下,就可以大大减少调试时间。
1. 数值溢出
图1:数值溢出错误
图1 中的 VI 只做了一个简单乘法 300*300 ,不加思索就应该知道答案是 90000,但程序中
乘法节点给出的结果却是 24464。乘法节点是不会错的,错误是由于程序中使用的数据类型是 I16。
I16 能表示的最大数目只有32767,所以在乘法计算中出现了溢出。
避免此类错误的方法是,在程序中使用短数据类型时,一定要确认程序中的数据绝不会超出该类
型可以表示的范围。
2. For 循环的隧道
数据传入传出循环结构可以通过移位寄存器(Shift Register)和隧道(Tunnel)两种方式。隧
道又有两种类型:带索引的和不带索引的。
移位寄存器一般用在需要局部变量的情况下,循环运行一次的输出数据要作为下次运行的输入数
据使用;循环外的数组数据通过带索引的隧道在循环体内就可以直接得到数组元素;除此之外,简单
地在循环内外传递数据,使用一般的隧道就可以了。
值得一提的是,如果一个数据传入循环体,又传出来,那么就应该使用移位寄存器或带索引的隧
道来传递这个数据,尽量不要使用不带索引的隧道。因为 For 循环在运行时,循环次数有可能为0。
在循环次数为0时,大多数情况,用户还是希望传出循环的数据就是传入值,但使用不带索引隧道时,
输入值有时会被丢失的。如果使用移位寄存器,即使循环次数为0,也不会丢失传入的数据。因为移
位寄存器在循环上的两个接线柱指向的实际是同一块内存(参考:LabVIEW 程序的内存优化),而
输入输出两个隧道指向的是不同的内存,数据不一定相同。
图2:For 循环上的隧道
图2中的程序, vi reference 传入,再传出循环均使用了隧道。如果循环次数为0(Controls
数组为空),vi reference 再传出循环时,信息就丢失了。这不但有可能造成后续程序的错误,而
且由于 vi reference 的信息丢失,再无法关闭打开的 vi,造成了程序泄漏。
Error 数据线(黄绿色的粗线)在传入传出数组时,一定要使用移位寄存器。原因还不仅是为了
防止在循环次数为0时,错误信息丢失。通常一个节点的 Error Out 有错误输出,意味着后续的程
序都不应该执行。在错误的情况下继续执行程序代码,风险非常大,可能会引起程序,甚至系统崩溃。
只有使用移位寄存器,某次循环产生的错误才会被传递到后续的循环中,从而及时阻止后续循环中的
代码被运行。
3. 循环次数
与其它语言相比,LabVIEW 的 For 循环有一大特点,在某些情况下它并不要求一定要输入循环
次数,而可以根据输入数组的大小自动决定循环次数。通过带索引的隧道,可以把数组分解成元素传
递到循环体内,此时不需另行设置循环次数N,循环的次数就是数组的长度。每次循环,带索引的隧
道便给出一个元素。
循环体上可以有两个或更多的输入数组使用带索引的隧道,此种情况下容易引起错误。这时,循
环的次数等于几个数组中长度最短的那个数组的长度。如果另外又设置了循环次数N,那么循环次数
就是N与输入数组长度这两者的最小值。调试时,如果发现一个本该运行多次的循环没有运行,那
么很可能就是因为它的一个输入数组是空的。
While 循环同样也可以使用带索引的隧道,但是我不建议大家这么用——如果需要用到带索引的
隧道,还是使用 For 循环更为适宜。因为 while 循环的循环次数不由数组个数决定,而是由停止条
件决定的。如使用了带索引的隧道,你还需要考虑当数组大于、小于循环次数时,程序应该如何处理,
所以还是在循环体内作索引比较方便。如果希望循环次数与数组大小保持一致,那自然是用 For 循
环的程序更加清晰易懂了。
4. 移位寄存器的初始化
图3:没有初始化的移位寄存器
看图3中这个程序,因为它在 while 循环上使用了带索引的隧道,所以可读性不那么好。array
out 的运行结果是什么,还要考虑一阵子才能给出答案。实际上这个程序,即使输入不变,每运行一
次,array out 的结果都是不一样的,它的长度一直在增加。这个问题就出在没有给程序中的移位寄
存器一个初始值。
没有初始化的移位寄存器,总是保存上次运行结束时的数据。这个特点在某些情况下可以被程序
员利用,比如用它当作全局变量,随时把数据存入或取出(一个例子是《如何使用 VI 的重入属性》
中的图4)。但多数情况下移位寄存器还是被用作为循环内部的局部变量的,这时就一定要对它初始
化,以防止潜在的错误。
5. Cluster
图4:Cluster 传递数据出错
图4的程序中有个奇怪的错误,明明应该是 weight 加 1 怎么运行完后的结果变成了high 加
1 了呢?直接揭开谜底吧,原因是 Cluster 中的元素有个顺序,这个顺序可以和界面上看到的顺序
不一致。分别鼠标右击程序中的两个 Cluster,选择“Reorder Controls in Cluster”,就可以看到
每个元素在 cluster 中的编号。info out 中的 high 实际上编号是 2,第三个元素。
为了避免 cluster 中用可能出现的错误,以及让 cluster 应用起来更方便,使用 cluster 最好
遵循以下原则:
1. 凡是用到 cluster 的地方,就为它造一个类型定义(《在程序中使用类型定义》),在程序
所有要用到这个 cluster 的地方,都使用类型定义的实例。这样一是可以保证所有的 cluster 都完
全一致,避免图4 这种错误;二是一旦需要变动 cluster 中的元素,只需在类型定义中更新就可以
了,不必挨个 VI 修改。
2. 凡是在需要解开(unbundle)或打包(bundle)的地方统统使用 unbundle by name 和
bundle by name 来实现。使用带名字的 bundle,unbundle 可以直观的显示出 bundle 种元素
的名字,这样不会因为顺序的不同而导致错误的连线。
6. 并行运行
LabVIEW 是自动多线程的编程语言,这一点在方便用户的同时,也会带来一些麻烦。比如最常
见的情况,多线程会引起数据或资源的竞争错误(race condition)。
图5:两个并行运行的子 VI
图5是一个简单的两个子 VI 并行运行的例子,在这个例子中就隐藏着一个潜在的问题。并行执
行的两部分程序,先后次序是不定的。有可能关闭程序中的引用数据(绿色的线上的数据)的节点在
子 VI B 结束前运行。而子 VI B 是要用到这个参考数据的,这是子 VI B 就会因为它所需要的数
据失效而产生错误。
VI 中的数据空间
LabVIEW 由于比其它语言采用了更多的值传递方式,这必然会影响它的运行效率,也使得
LabVIEW 在这方面要采取一些其它语言不需要的应对措施,尽量提高效率。优化之一是子 VI 中局
部变量使用的内存的分配方式。
C 语言中,函数的局部变量存在于栈中。在调用某一函数时,程序才为这个子函数开辟一块空间
作为用于保存函数中局部变量的栈。子函数运行结束后,栈空间即被释放。下次再调用这个函数,程
序会重新非配栈空间,这时的空间可能与上次分配的并不在同一内存地址。为了节约反复开辟空间的
时间,LabVIEW VI 中并没有采用栈的方式。一般情况下,静态调用 VI,每个 VI 专门有一块存数
据的数据空间,这块数据空间所在的内存地址在 VI 每次运行时是不会变化的,尤其是上次 VI 运行
后所留有的数据还可以被使用。
LabVIEW 这种做法最大的好处是节约了大量开辟、回收内存的开销;但它也有个严重的缺陷,
这也是其他语言不采用类似措施的原因:每次函数调用没有独立的数据区,因此无法实现递归调用
(LabVIEW 静态调用的情况下)。经过权衡,LabVIEW 最终牺牲了递归来换取运行效率。
对于一般的子 VI(非可重入的),不论在程序的哪里被调用时,都使用的是同一块数据区。如果
主 VI 上有两个并排被调用的同一个子 VI(如图1所示的两个 Delay VI),理论上的数据流驱动
语言是应该在两个线程内同时运行两份子 VI 的代码。但是,由于这两次调用会使用到同一块数据区,
为了避免两次运行之间互相干扰,引起数据混乱,LabVIEW 实际上是顺序执行这两次调用的。至于
那部分代码被先调用是不确定的。
图1:并行调用同一子 VI 两次
LabVIEW 只能顺序执行这两次调用,在很多时候并不是一件坏事。比如,子 VI 中的操作是读
写某一串口。LabVIEW 的这一特性恰好防止了多线程同时对这个串口读写而引发的错误。但这种行
为也会引起一些糟糕的问题。比如,子 VI 是用来读写所有串口的。我在一个线程内对串口1做了
操作,另一个线程要对串口2操作。读写串口是比较慢的,本来应该两个串口同时操作,来节约一点
时间。但是如果串口读写子VI不能重入,那其中一个线程就只好慢慢等着了。
LabVIEW 解决这个问题的办法是为 VI 增加了一个可重入(reentrant)属性。非可重入的 VI
的数据区是和这个 VI 其它内容(比如执行代码、界面、源代码等)放在一起的,所以不论这个 VI 在
哪被调用,使用的都是同一数据区。设置为可重入的 VI,它的数据区被开辟在调用它的父 VI 那里。
在父 VI 的程序框图上每一个可重入子 VI 的图标,都意味着父 VI 的空间内为这个 VI 开辟了一
块数据区。所以,并行的两次调用同一可重入子 VI,这两次调用它们使用的是不同的数据区,所以
可以同时运行而不需要担心数据被互相干扰;如果是循环内有一个子VI,那么循环多次执行,每次
调用这个子 VI,使用的还是同样的数据区。
G 语言
G 语言是图形化编程语言(Graphical Programing Language)的缩写。LabVIEW 有的时候
也被叫做 G 语言。我们可以这样理解:LabVIEW 是一个开发环境(类似的如 Visual Studio 也
是一个开发环境),在这个环境下编写的代码就是 G 语言代码(类似的如在 Visual Studio 下写
出的C代码)。
目前在中国,很多工程师认为 LabVIEW 是一个应用在工业测控领域的应用软件,并不理解他是
一个编程语言。原因有两个,首先是因为它和以往其它的编程语言差距太大,第一次看到它的人倒是
更容易联想到电路板布线、工业总线配置软件等;其次是因为 LabVIEW 在中国使用的年头不多,
大多数用户仅用到了 LabVIEW 的一小部分功能,还没有真正体验到 LabVIEW 的强大。
既然是一门编程语言,在使用 LabVIEW 的时候,就应该按照程序设计的思想来解决问题。举一
个例子来说明如果用程序设计的思想来解决问题:
我们需要解决的问题是求两个正整数的最大公约数,这是一个非常常见的编程例子。
用 LabVIEW 来解决这个问题,应当与用其他语言求解这个问体的思路是一致的。按照程序设计
的一般方法,解决这个问题可以三个步骤:
第一:确定问题的需求,给出需求的详细说明。对于这个求最大公约数的问题,我们在这一步需
要做的就是写出程序输入输出的详细定义。如果是用普通的文本语言编程,你至少应该以文档的方式
吧问题需求记录下来。但是 LabVIEW 程序员在这一步有个更方便的设计方法——直接在 VI 的前
面板上定义程序输入输出:程序需要两个输入值(a, b),用 Numeric control 代表,一个输出(x)
用 indicator 代表。输入要求是正整数,我们可以把 Numeric control 的数据类型设置为 U32,
并在这个控件的属性中设置最小值为1。再为 VI 和它每个控件添加上帮助信息,VI 的前面板就可
以用户提供一个详细的 VI 的功能描述以及接口定义。
第二:设计解决问题的算法。一个问题通常不只会有一种解决方法(算法),比如说我们的求最
大公约数问题,你可以采用穷举算法,把1到a之间所有的整数都试一遍,然后找到那个最大的公
约数;也可以使用g.c.d.算法。
多数情况下这一步骤和具体的语言环境无关,比如说我们的问题不论采用哪种语言编写,g.c.d.
算法的效率都高于穷举法。但是某些时候可能要考虑 G 语言不同与文本语言的特性,在 G 语言下
使用不同于其他语言的算法。比如要遍历一棵树,可以使用递归的算法,也可以使用循环的算法。在
C语言下,一般会选择递归的算法,因为递归算法的思维方式更自然,更容易掌握,实现起来也比较
方便;但是在 G 语言下,递归的实现并不那么容易,效率也比较低,所以在 G 语言中,选择循环
的算法更加适合。
对于我们要解决求最大公约数问题,我们还是选用g.c.d.算法,它的运算过程如下:
Step1: If (a mod b == 0) goto Step3; else goto Step2;
Step2: (a, b) = (b, a mod b); goto Step1;
Step3: x=a; return;
第三:在 LabVIEW 下实现设计好的算法。G 语言之所以被称之为图形化的编程语言,并不仅
仅是因为它的程序又图形化的界面(前面板),最本质的原因是因为它的代码也是通过画图的方式来
编写的(程序框图)。
针对本例,可以使用 while 循环,a 和 b 分别用循环上的两对移位寄存器表示。在循环体内首
先判断 a 是否被 b 整除,如果是,结束循环;否则把 b 和 a mod b 赋给两个移位寄存器,进入
下一次循环。
图形化编程语言是数据流驱动(以后再解释)的,与一般文本编程语言的过程驱动机制有很大差
别,因而在程序设计的思路上也与文本编程语言有所区别。尤其是有过文本编程经验的程序员开始使
用 LabVIEW 的时候,会感觉 LabVIEW 缺失了很多文本语言常用的功能,比如使用局部变量、跳
出循环等等,因而 LabVIEW 用起来不是太方便。另外 LabVIEW 编写出来的的代码连线乱七八糟,
造成程序阅读和维护的困难。不过这些问题其实不能算是 LabVIEW 本身的问题,主要是由于编程
者还没有掌握 G 语言的编程思想造成的。
LabVIEW 虽然不能覆盖所有文本语言的优点,但它具有自己的特色。在编写与工业领域设计、
测量、控制等相关的程序或系统时,其开发效率大大高于其它语言。
在 LabVIEW 中可以为代码添加图文并茂的注释,再加上人类对图形的识别速度远远超过对文本
的分析速度,一个优秀程序员编写的 G 语言代码的可读性要高于文本语言一个层次。
LabVIEW 是编译型语言还是解释型语言
LabVIEW 和常用的 VC++、VB 一样,是编译型语言。LabVIEW 的语法定义比较严格,在程
序运行之前会检查所有语句的语法,一旦查出有差错,程序会报错,不能运行。
在LabVIEW是否是编译型语言的问题上容易引起混淆的原因,一是用户看不到编译时生成的目
标文件(在 LabVIEW 的环境中,可以直接运行一个 VI,并不生成任何其他可执行文件);二是
LabVIEW 没有编译这个按钮。此外,VI 运行前似乎也没有占用编译时间。
我们可以把 LabVIEW 和 C 语言的存储与编译方法作一比较:C 语言的原文件存储在 .c 文件
中。需要编译时,要显式地告知编译器进行编译。在耗费一段编译时间后,可以看到编译后生成的含
有可执行二进制代码的 .obj 文件。而LabVIEW 的原代码是存储在 .vi 文件中的。
一个 .c 文件中通常保存了多个函数,一个由几十个函数构成的 C 语言工程,也许只由两三个 .c 文
件组成。而通常情况下,一个 .vi 文件只存储一个 VI,即相当于 C 语言中的一个函数。所以,一
个小型 LabVIEW 工程也可能由几十个 .vi 文件组成。
但在某些情况下,一个 .vi 文件也可能包含了某些子 VI(子函数),即这些子函数没有他们自
己的 .vi 文件。这样的子 VI 被称为实例 VI(Instance VI)。LabVIEW 7版本中出现的、目前
很常用的 Express VI就是这种 Instance VI。他们都是被存储在调用他们的 VI 中的。
.c 文件只保存程序的原代码;而 .vi 文件不仅保存了 LabVIEW 程序的原代码,也保存了程序
编译之后生成的目标代码。在 LabVIEW 的工程中看不到类似 .obj 这样的文件,就是因为编译后
的代码也已经被保存在了 .vi 中的缘故。
LabVIEW 在运行VI 之前无需编译,是因为 LabVIEW 在把 VI 装入内存的时候、以及在编辑
VI 的同时进行了编译。
当把一个 VI 装入内存时,LabVIEW 先要判断一下这个 VI 是否需要被编译。一般情况下,如
果不对VI的代码做改动,是不需要重新编译的。但是在两种情况下需要重新编译。第一种,是在高
版本 LabVIEW 中打开一个用低版本LabVIEW 保存的 VI;第二种,是在不同的操作系统下装入
和打开了同一个 VI。
比如,要在 LabVIEW 8.0 中打开一个原来用 LabVIEW 7.0 编写保存的 VI,则被装入的 VI
需要被重新编译,因为不同版本的 LabVIEW 生成的目标代码会稍有不同。如果你的工程包含有上
百个 VI,在新版本的 LabVIEW 中打开顶层 VI,就会明显地察觉到编译所占用的时间。第二种情
况的例子是,在 Linux 中打开一个原来是在 Windows XP 下编写保存的 VI,LabVIEW 也需要
重新编译。LabVIEW 为不同操作系统生成的目标代码也是不同的。
在以上两种情况下,打开一个 VI 后,会发现 VI 窗口的标题栏中的标题后面出现一个星号,这
表示需要重新保存 VI。此时,虽然 VI 中的程序原代码没有改变,但是编译生成的目标代码已经变
了,所以需要重新保存。
在LabVIEW 安装了升级补丁之后(比如从8.0升级到8.01),程序会提示你是否需要把
LabVIEW 自带的 VI 全部批量编译(mass compile)。如果你选择“是”,则可能需要占用几个小
时的时间才能完成编译。
LabVIEW 在你编辑程序原代码的同时,就会对它进行编译。LabVIEW 只编译你当前正在编辑
的这个 VI,它的子 VI 已经保存有已编译好的目标代码,所以不需要重新编译了。因为每个 .vi 只
相当于一个函数,代码量不会很大,编译速度就相当快,用户基本上是察觉不到的。 你在编写一个
LabVIEW程序时,假如你把两个类型不同的接线端联在一起,会看到程序的运行按钮立即断裂,它
表示程序已经编译了,并且编译后的代码不可执行。程序编写完毕,所有 VI也都已是被编译好了,
程序直接运行即可。
有时会出现这种情况:打开一个 VI,VI 左上方运行按钮上的箭头是断裂的,表示 VI 不能运行。
但是点击断裂的箭头,在错误列表里却没有列出任何错误信息。此时箭头断裂是由于 VI 保存的编译
后的代码不能执行引起的。例如在上一次打开这个 VI 时,有一个被此VI 调用的 DLL 文件没有找
到,编译后的代码自然不能执行。而后关闭 VI 再把缺失的 DLL 文件放回去。下次打开始 VI 时,
理论上 VI 应当可以运行了,但是这时 LabVIEW 没有重新编译这个 VI,VI 中保存的是上一次不
可执行的代码,所以运行按钮的箭头仍然断裂。而程序原代码没有任何错误,所以错误列表中什么都
看不到。
修复箭头状态的方法是按住 Ctrl + Shift 键,再用鼠标左键点击运行按钮(断裂的箭头)。在
LabVIEW 中按住 Ctrl + Shift 键 + 鼠标左键点击运行按钮表示编译,但不运行,这相当于其他
语言的 Compile 按钮。
LabVIEW 采用的把可执行代码与源程序保存在同一文件,分散编译的方式,与其它语言相比是
相当特殊的。它既有优点也有缺点。
它最大的缺点是不利于代码管理。比较正规的做法,程序代码需要每天都上传至代码管理服务器。
因此,源代码管理需要占用大量的硬盘空间。如果只是程序代码还好,把编译好的执行代码也存在同
一个文件里,这就大大加重了代码管理的负担。程序开发的时候,经常需要回头查看过去的修改历史。
如果某个文件发生了变化,代码管理软件就会意识到这是代码作了修改。但是VI中有时只是它包含
的执行代码发生的变化,因此代码管理软件无法正确的判断出是否代码有变化。
它的优点主要有两条:1. 运行子 VI 极为方便。其它语言要运行,只能从主入口进入,不能够单
独运行某一个函数。而 LabVIEW 则可以直接运行任何一个VI;2. 分散了编译时间。大型的C++
程序,编译起来很花时间,有时要用几天。LabVIEW 把编译时间分散到了写代码的同时,因此用户
基本感觉不到 LabVIEW 编译占用的时间。
数据流驱动的编程语言
在面向对象的编程思想出现以前,文本编程语言主要采用的是面向过程的编程方法。面向过程有
时候也被成为控制流驱动的编程方法。想我以前编程序,经常要先设计一个流程图,然后,照着流程
图翻译成 C 代码就行了。
LabVIEW 程序是数据流驱动的,这与面向过程的程序还是比较相似的,但是也有一些区别。
面向过程的程序执行起来,就只有一条线,代码按照设计好的顺序一条一条执行下去。代码中的
某一条语句,即便它的输入条件都已经被满足,它也要等到它前面的代码都被执行完后,才能被运行。
数据流驱动的程序就已经从一条线扩展成一张网了(有时候 LabVIEW 程序的框图线连得乱七八
糟,就像一张网:)。一个节点运行完,数据从这个节点输出,会同时被传给所有用到它的其它节点
去。一个节点只要它所有的输入都已经准备好了,就会被执行,不需要等待其它节点执行完。这样一
来,经常有多个节点同时运行着的,LabVIEW 会自动把他们放到不同的线程中去运行。这就是数据
流驱动的程序的一大特性:是自动多线程运行的。一般的文本编程语言,除非有显示的调用开辟新线
程函数,否则所有代码都在同一个线程内顺序执行。
自动多线程,为编程人员带来的不少方便。但是,由于多线程程序更为复杂,可能导致出错的隐
患更多,LabVIEW 不得不做一些其它语言不需要做的工作,来保证用户可以方便的用 LabVIEW 开
发出安全高效的程序。
多线程程序中最常见的问题就是多个线程访问同一资源或内存时发生冲突。先以内存中的数据为
例,程序运行在单线程状态下,写进这块内存的数据是什么,下次读出来一定就是你写进去的。而多
线程状态下就不一定了,说不定在读写之间,内存被别的线程修改了,读出来的数据就是错误的。一
般的文本语言不需要编译器来考虑如何防止用户做出类似的错误操作,因为他们默认情况下只会使用
一个线程。多线程一定是在用户有意识开辟的。既然是有意识开辟的,用户在使用多线程的时候就也
会留心类似的不安全问题。LabVIEW 却不能像其它语言编译器一样,不去考虑这个问题,LabVIEW
用户会在无意识的状态下就编写出多线程的程序来。如果用 LabVIEW 写出来的程序总是出错,
LabVIEW 就会渐渐失去客户。
LabVIEW 采取的保护措施之一就是它的传参方式。
传值和传引用
在现在常用的文本编程语言(C++, Java, C#)中,调用子函数时的传参方式主要是传引用方式,
就是说,告诉被调用的函数的是参数所在的位置,而不是参数的数据。C++ 为了保持和 C 语言的
兼容,一般的简单数据还是使用值传递,但对于大块的数据,比如数组,字符串,结构,类等等,也
基本上都是引用形式传递的。
值传递的方式的缺点是显而易见的:每次调用子函数的时候,需要把数据拷贝一份,耗费大量的
内存。传引用的方式,不需要每次都拷贝数据,节省了内存空间,和复制数据的时间。但是传引用的
安全性不如直接传值,因为传引用的时候,数据所在的内存也可以被其它函数访问,这在单线程下,
问题不大。但是多线程下,就不能保证数据的安全了。
理论上,一个数据流驱动的编程语言,可以只采用值传递。数据在每一个联线分叉的地方,都做
一个拷贝。这样任何一个节点所处理的数据都是它专用的,不需要担心线程之间会相互影响。在设计
LabVIEW 程序时,可以假设 LabVIEW 就是这样子工作的。但是 LabVIEW 的实际工作情况比这
要复杂些,它在不违背数据流原则的前提下,做了一些优化以避免过多的复制数据。
在某些时候,一个节点得到了输入数据,LabVIEW 如果能够确认这个输入数据的内存肯定不会
被其他部分的程序代码使用到,并且恰好节点的一个输出需要一块内存,LabVIEW 就不在为输出数
据令开辟一块内存了,而是使用那个输入数据所在的内存。这叫做缓存重用。
这种行为实质上和传引用是一样的,告诉函数一个数据的地址,然后函数直接在这个地址上处理
数据。LabVIEW 程序员是不能够直接设置某个参数是传值还是传引用的。到底采用那种传递方式,
是由 LabVIEW 来决定的。LabVIEW 决定采用哪种参数传递方式的原则是:首先保证数据的安全,
其次才估计效率。LabVIEW 并不能总是准确的判断出某段代码采用传引用的方式是否安全。
LabVIEW 本着宁枉勿纵的原则,对凡是拿不准的地方一律不优化,全部采用传值的方式,多拷贝一
份数据。
虽然不能够直接设置某个参数是传值还是传引用的,但追求效率的的程序员,可以通过改变程序
风格,来帮助 LabVIEW 准确判断出那些代码可以优化,无需拷贝数据,从而让自己编写出来的
LabVIEW 代码效率最高。比如,使用移位寄存器和缓存重用结构告诉 LabVIEW 在某个地方使用
传引用的方式。
LabVIEW 中有些节点的输入输出数据类型完全不一样,比如数组索引节点,输入是一个数组和
索引,输出是一个数组的元素。输入和输出的数据类型一般情况下完全不同,所以必须未输出数据新
开辟一块内存,根本不可能做到缓存重用。有些节点,总是有相同类型的输入输出,比如加法节点,
输出值的数据类型总是和其中一个输入同类型(fixed-point 数据类型是个例外)。LabVIEW 要考
虑尽量在这些节点使用缓存重用。
如果输入值是数组数据,它通过分叉的连线被同时输入到一个数组索引节点和一个加法节点。假
设其它数据都已就绪,LabVIEW 作为数据流驱动的程序,理论上应该同时运行着两个节点。但实际
上,为了内存优化,在类似的情况下,LabVIEW 总是运行不可能做缓存重用的节点(比如这里是数
组索引节点),然后再运行可以做缓存重用的节点(加法节点)。
原因是这样的:如果先运行加法节点或者同时运行两个节点,因为加法节点的输入数据所在的内
存还要被数组索引节点读取,因而加法节点是不能够改变这块内存中的数据的,那么加法节点只好再
为输入数据开辟一块新内存;相反,如果先运行完数组索引节点,在运行加法节点的时候,加法节点
输入数据所在的内存就不会再被别的节点使用了,这是加法节点就可以放心的把输入数据放到这块内
存里,做到缓存重用。
LabVIEW 虽然不能设置数据传递给一个节点时,使用值传递还是引用传递,但是 LabVIEW 中
有一类专门的“引用型控件”,用来保证大块的数据不被频繁复制,或者在不同的线程内对同一内存做
数据操作。一般叫做 xxx refnum 的控件都属于之一类,他们所代表的数据(也可用于表示某个设
备)是不随着数据线流动的。程序上的连线出现分叉,虽然 refnum 这个值本身可能会被复制,但
它所指向的数据是不会被拷贝的。
另外,如果一定要在 LabVIEW 代码中实现传引用,可以通过以下的方法:做一个全局数组变量,
把数据存在数组里。VI 间传递的信息是数据在数组中的索引,一个表示序号整数值,就相当于是这
块数据的引用。这样,所有对块数据的操作都是在同一内存中的,并且不同线程可以同时对这块内存
做修改。
用户界面设计 1
有些软件,一打开来就让人眼前一亮,可能是它的界面设计的非常新颖、华丽。但漂亮视觉感只
能是作为锦上添花,评判一个界面好坏的最基本指标首先还是要看这个界面是否完成了它的交互功能
-用户可以通过界面为程序提供必要的信息;用户可以通过界面接受到需要的信息。其次的指标是通
过这个界面用户是否可以简单直观的输入或获取信息。最后才是界面的美观程度。
从这个角度说,一个好的界面,通常是不会引起用户注意的界面。多数时候,引起用户对界面的
注意是因为他觉得别扭:找不到所需的信息,或输入信息的地方了。
使用LabVIEW 开发一个项目,或编写一个软件,比较理想情况下是按照下面五个步骤顺序进行:
收集需求、设计、编码、测试、发布及维护。细分设计阶段,一个项目所需的设计可能有用户界面设
计,程序结构设计,接口设计,模块设计等等。对于编写 LabVIEW 程序,通常首先做用户界面设
计。
先做界面设计可以使界面不受程序实现的影响。若先设计程序结构,再设计界面,难免会朝着最
可能简化编码工作方向去做。但是这样的界面往往不是最方便用户使用的界面。
使用比较老的文本语言编程,设计用户界面时通常现在草稿纸上画出原型。LabVIEW 在这方面
有独特的优势,它的可视化编程做的非常方便。有大量现成的控件,控件属性更改非常方便。因此,
用户可以以拖拽的方式,直接用 LabVIEW 来设计界面原型。
对于界面好坏的评判,每个人都会有不同的观点。仁者见仁,智者见智。但是,好的用户界面都
有一些共同的特点:一致性、使用恰当的数据类型和控件类型、控件的分类排布合理、简洁。我们在
设计自己的程序界面时也要考虑到这些因素。
用户界面设计 2 - 界面的一致性
让用户迅速接受并且方便的操作一个程序界面,最关键的一点就是让这个界面保持高度的一致性。
这里说的一致性包涵一下多个方面的一致:
一、程序内部的一致性
由于应用领域、面向的客户群体的不同,不同的软件可以有自己独特的风格。比如,为儿童设计
的软件(例如使用乐高游戏版的LabVIEW)几面可以加一些卡通图片,走可爱路线;为青年群体设
计的软件,可以采用大量鲜艳颜色,显得活泼;LabVIEW 程序更多的时候是应用于工业领域,面向
专业技术人员,这样的程序界面风格应当柔和、朴素。
不论一个程序采用了哪种风格,它内部不同界面(比如不同的对话框),同一面板上的不同控件
等,它们的风格应当保持一直。一个软件采用统一的风格,才会让用户有一种“和谐”的感觉。
打开 LabVIEW 的控件选板,会发现有三种不同风格的控件:经典风格、现代风格、系统风格,
如图1所示:
图1:三种不同风格的控件
经典风格的控件看上去比较土气,是 LabVIEW 6 之前的版本所使用的控件。一般不要用这种风
格的控件了,有两种情况除外:
第一是维护老程序的时候,老程序可能还是用的这种控件,为了界面风格统一,又不想花时间改
造原来的程序,那就继续用经典风格的控件。
第二是需要造一个透明控件的时候。这是一个小技巧,比如你希望有一段提示文字出现在界面上,
需要使用字符串控件,但是你有希望文字直接出现在面板上,用户看不到包裹它的控件。这时候,就
可以使用一个经典风格的字符串控件,然后用画笔把它的边框和背景都画为透明色即可。
LabVIEW 6 使用了一些重新设计的非常美观的立体效果控件,这就是现代风格的控件。编写测
试领域的软件,可以首先考虑使用这类控件。
系统风格的控件外观与操作系统保持一致。我们编写的一般软件,希望用户比较易于接受时就可
以使用这类控件。使用这类控件编写的界面,与系统自带的程序看上去风格非常一致。系统风格的控
件会随着系统的不同,和系统设置的不同而随之调整。比如,把你的程序拷贝到 MacOS 的机器上,
文本框会自然变成 MacOS 上圆弧角的风格。把系统颜色设为高亮反转显示,文本框也会变为黑底
白字。
但是,LabVIEW 特有的控件,比如波形显示控件等,是没有系统风格的。如果你的程序整体式
系统风格的,在使用这类控件时,要注意调整一下控件的颜色,使他们与其它控件的颜色保持一致。
二、与约定俗成的习惯保持一致
有很多设计或操作方法,已经被大家广为接受了。他们也许不见得美观或优化,但是一旦习惯养
成了,就很难被改变了。据说我们现在使用的键盘,是当年为了延缓打字速度而精心设计出来的打字
最慢的键盘排布方式。但现在大家都用习惯了,没人会为了打字快一些而换用其它按键排布方式。
与软件相关的比如,Ctrl+C 表示拷贝;Ctrl+V 表示粘贴。你如果用这两个键去干你认为更适合
的工作,肯定会被用户骂死。在 LabWindows/CVI 中,查找的快捷键居然不是 Ctrl+F,搞得我只
好不用它的快捷键。
对于应用程序界面,大家最习惯的就是 Windows 默认的界面风格了。简单来说,这样的界面就
是:使用窗口,窗口最上方是标题栏,下面是菜单,再下面是工具条,再下面是主体内容,窗口最下
方是状态栏,右面是滚动条。
如果你非要标新立异,把标题栏和滚动条的位置互换一下,那你的程序一定被用户骂死。不过,
实力强大的公司也许会可以逐渐改变人们的习惯。微软今年推出的 Office 07 比以往的界面风格有
了重大改变,也许是为了配合 Windows Vista。新的界面漂亮的不少,但它还是遭到了很多用户的
抵制,就是因为在使用功能区(ribbon)替代了原来的菜单和工具栏之后,用户再也不能从熟悉的地
方找到他们所需的操作了。
LabVIEW 默认的颜色配置和控件风格,与系统的风格也是有区别的。所以为了照顾新用户,不
妨在程序里尽量使用系统风格的控件和颜色配置。
三、与真实事物保持一致
有很多程序是对现实世界的模拟或模仿,这样的程序若希望便于用户接受,最好是尽量与现实世
界保持一致。比如电脑游戏,规则一定要与现实世界接近,若完全采用不同的规则,比如越练功人品
越差、被人看几刀魅力值会增加等等,玩起来一定特别别扭。
LabVIEW 编写的程序大多与测量、控制等有关,在这些领域,原本也存在着一些相关的仪器或
设备。因此软件的界面可以借鉴这些仪器的外观。比如需要实现的程序要完成一个类似示波器的功能,
那么界面最好设计的和传统的示波器一样:一边是现实波形的控件,周围有调节垂直、水平方向范围
的按钮等。这样,用户只要曾经用过示波器,不需要再学习任何知识,直接就可以使用你的软件了。
NI 公司开发的 Soft Front Panel 产品可以看作是与真实事物保持一致的一个很好范例。
四、建立并遵循界面规范
使界面保持一致性的最好办法就是在设计开发时遵循一定的规范。这个规范可以由公司内部定义,
也可以遵循现有的行业规范。对于开发 Windows 系统风格的程序,可以遵循微软定义的界面规范。
对于一般的 LabVIEW 程序,可以遵循 LabVIEW 程序开发规范。
用户界面设计 3 - 界面元素的关联
图1:两个菜单
上图左边是 LabVIEW 中的一个菜单。右边那个是我自己对它的“改进”。大家觉得那个好一些?
显然用户更喜欢组织清晰合理的那个菜单。当一个界面上的元素比较多,找到自己想要的信息就
要花上一小点时间。用户常常是一眼就看到了一个与自己想要的信息有一点关联的某个元素,他这时
候会期望这个元素就有一定的提示信息,帮他加速找到自己想要的东西。因此,我们要在界面上,告
诉用户哪些元素是相关的,或不相关的。
有很多手段可以把界面的元素之间的关联显示给用户,比如通过元素的排布、边框、空白、颜色、
字体等等方式。
我们总是在相关内容的附近去找想要的信息,所以逻辑上相关的控件或项目,应当在屏幕空间上
相对临近。比如刚刚看到的菜单,Save, Save As, Save All,等等与保存相关的条目应当排在一块。
仅仅把相关内用摆在一起还不够,看看下面这个图片。
图2:小朋友的名字
这是我在网上看到的一个经典笑话:老师发作业本的时候,念小朋友们写在本子上的名字:“黄肚
皮”,“鱼是虫”。但是没人答应,最后有两个小朋友没拿到本子,他们的名字分别是“黄月坡”和“鲁蛋”。
小朋友们虽然把界面元素按照顺序排列了,但却没有合理的组合它们。
我们上面看到的菜单,有二十多个条目,单纯的把他们排在一起还是不利于用户查看。可以把它
们按功能分成几个不同的区域,比如保存文件与Project的操作在功能上相对独立一些,就可以用分
隔线,帮它们的项目划分开。对于面板上的控件,功能相关的几个控件可以通过被边框围住、使用分
割线、采用不同的间隙等等方法,让用户直观的感觉到他们在功能上的紧密关联。
还有一种表示控件间关联性的方法值得多叙述几句,就是利用不同的颜色。球场上的两组队员,
开始分列于球场两端,很容易区分他们是哪一伙的。而一旦比赛开始,这种空间上的提示就不存在了,
这时大家主要靠队员衣服的颜色来区分它们属于哪支队伍。在界面设计上当然也可以使用这种方式,
为不同功能的控件设置不同的颜色。
需要注意的是,颜色只能作为辅助方式,前几种方法不适用的时候,才需要用颜色来表示关联。
颜色与前面提到的几种方式不同:大多数人喜欢排列整齐,布局合理的界面,但喜欢界面颜色丰富的
人就不那么多了。相反,颜色艳丽、对比度高的界面会使人视觉疲劳,让人觉得反感。
图3:LabVIEW 颜色配置
LabVIEW 配置颜色的面板上分了几类不同的颜色区域。设计系统风格的时候,需要使用系统颜
色。其他情况下,尽量使用柔和颜色,避免使用靓丽鲜艳的颜色。还要考虑到色盲、色弱的发病率也
是蛮高的,界面设计时要照顾到这些用户。
所以,界面内容不多时,就尽量不要使用颜色了。只有当界面上信息量特别大的时候,颜色才会
派上用场。需要使用图片的情况就不用说了,除此之外信息量较大的情况是有大量文字的时候。需要
把不同的文字区分开来的时候,比如标注所有拼写错误的单词等等,就可以利用颜色来区分。当然,
这时候也可以利用字体,字号等的不同来达到同样的目的。
VI 中的数据空间
LabVIEW 由于比其它语言采用了更多的值传递方式,这必然会影响它的运行效率,也使得
LabVIEW 在这方面要采取一些其它语言不需要的应对措施,尽量提高效率。优化之一是子 VI 中局
部变量使用的内存的分配方式。
C 语言中,函数的局部变量存在于栈中。在调用某一函数时,程序才为这个子函数开辟一块空间
作为用于保存函数中局部变量的栈。子函数运行结束后,栈空间即被释放。下次再调用这个函数,程
序会重新非配栈空间,这时的空间可能与上次分配的并不在同一内存地址。为了节约反复开辟空间的
时间,LabVIEW VI 中并没有采用栈的方式。一般情况下,静态调用 VI,每个 VI 专门有一块存数
据的数据空间,这块数据空间所在的内存地址在 VI 每次运行时是不会变化的,尤其是上次 VI 运行
后所留有的数据还可以被使用。
LabVIEW 这种做法最大的好处是节约了大量开辟、回收内存的开销;但它也有个严重的缺陷,
这也是其他语言不采用类似措施的原因:每次函数调用没有独立的数据区,因此无法实现递归调用
(LabVIEW 静态调用的情况下)。经过权衡,LabVIEW 最终牺牲了递归来换取运行效率。
对于一般的子 VI(非可重入的),不论在程序的哪里被调用时,都使用的是同一块数据区。如果
主 VI 上有两个并排被调用的同一个子 VI(如图1所示的两个 Delay VI),理论上的数据流驱动
语言是应该在两个线程内同时运行两份子 VI 的代码。但是,由于这两次调用会使用到同一块数据区,
为了避免两次运行之间互相干扰,引起数据混乱,LabVIEW 实际上是顺序执行这两次调用的。至于
那部分代码被先调用是不确定的。
图1:并行调用同一子 VI 两次
LabVIEW 只能顺序执行这两次调用,在很多时候并不是一件坏事。比如,子 VI 中的操作是读
写某一串口。LabVIEW 的这一特性恰好防止了多线程同时对这个串口读写而引发的错误。但这种行
为也会引起一些糟糕的问题。比如,子 VI 是用来读写所有串口的。我在一个线程内对串口1做了
操作,另一个线程要对串口2操作。读写串口是比较慢的,本来应该两个串口同时操作,来节约一点
时间。但是如果串口读写子VI不能重入,那其中一个线程就只好慢慢等着了。
LabVIEW 解决这个问题的办法是为 VI 增加了一个可重入(reentrant)属性。非可重入的 VI
的数据区是和这个 VI 其它内容(比如执行代码、界面、源代码等)放在一起的,所以不论这个 VI 在
哪被调用,使用的都是同一数据区。设置为可重入的 VI,它的数据区被开辟在调用它的父 VI 那里。
在父 VI 的程序框图上每一个可重入子 VI 的图标,都意味着父 VI 的空间内为这个 VI 开辟了一
块数据区。所以,并行的两次调用同一可重入子 VI,这两次调用它们使用的是不同的数据区,所以
可以同时运行而不需要担心数据被互相干扰;如果是循环内有一个子VI,那么循环多次执行,每次
调用这个子 VI,使用的还是同样的数据区。
相关文章:
Caption 和 Label 的书写规范
LabVIEW控件的 Caption 和 Label 的特性和用途很相似,都是给了控件一个有意义的名字。
因此,在很多场合没有必要刻意区分他们。
Caption 和 Label 的最主要区别在于,Caption 可以在程序运行的时候改变;而 Label 则不
可以,一旦程序运行,就固定不变了。鉴于这一点,Caption 和 Label 的用途也略有区别。Label 应
该是给程序自己用的,比如在程序中需要根据控件的名字找到它,那就得跟据 Label 来找,而不能
用Caption来找;Caption 是为了给用户看的,有时控件的名字在运行到不同状态下需要发生改变,
此时显示在界面上的就应该是 Caption。
推荐大家按照下面的规范使用 Caption 和 Label。
先给 VI 分一下类:
1. 底层 VI:用户不会直接使用到的 VI,作为 subVI 随程序一起发布。
2. 用户界面 VI:VI 前面板是给用户看的程序界面的一部分。
3. 程序接口 VI:VI 是提供给用户,在他们编程时,当作 API 被调用。
对于 Caption 和 Label 一个共同的书写规范是:使用有意义的文字,在使用英语短语命名时,
单词之间用空格分隔,不应该有重名。
不同点列于下表:
Label Caption
底层 VI 显示出来
隐藏 显示
用户界面 VI
多语言版本中,只使用英语 多语言版本中,使用本地化语言
隐藏
程序接口 VI 多语言版本中,只使用英语
不用标注控件的默认值
为空。
显示
多语言版本中,使用本地化语言
在后面加一括号,括号内标注控件的默认值
和数据单位
使用 LabVIEW 的默认状态,即 Caption
美化程序 - 隐藏程序框图上的大个 Cluster
在编写某些程序的时候可能会遇到如图1 所示的情形:即用到了一个极为复杂的数据类型常量。
这个常量由于体积巨大,使得在程序框图无论怎么摆放都让人看起来不太舒服。如何才能把这个程序
改造得美观一些呢?
图1:体积巨大的常量会有碍观瞻
要解决这个问题,只有设法把这个常量在主程序框图上隐藏起来。通常可以用以下两种方法。
第一种方法:把这个常数变换成控件,再把控件隐藏起来。这种方法比较简单,但是也有弊病。
①容易引起误解:控件一般表示有值传入,其他人读程序读到这里就可能搞不清楚这个值是从哪里传
来的了;②如果要修改常量 Cluster 中某一个元素的值,操作起来比较麻烦。
第二种方法,也就是我向大家推荐的:把它隐藏到更深层的子 VI 中去。具体操作方法如下:
如图2 先给这个复杂数据类型建立一个 Strict Type Def。我的建议是为所有程序中用到的
Cluster 都建立一个 Strict Type Def。这样可以为以后的程序维护省去很多麻烦。
图2:Strict Type Def.
然后然后再建立一个新的 VI,把我们要隐藏的这个个头巨大的常量摆放在这个 VI 中,并且连
接一个 Indicator ,以把它的值传出来。VI 的接线板采用 4-2-2-4 格式的,最下层第 3 个接线
端用于传出 VI 中唯一的数据,如图3 所示。
图3:用于隐藏个头巨大常量的 VI
这个 VI 的图标要做得小巧漂亮,如图4,图标不一定非要做成正方形。只要 B&W 和 256
Colors 中的图标形状一样,我们就可以画出不规则图标了。
图4:常量数据 VI 的图标
把这个新造出来的常量数据 VI 拖到程序框图上,把它的输出链接到刚才链接常量的地方,再把
位置摆放好。现在我们的程序是不是漂亮多了
图5:改造后的程序框图
相关文章:
《我和 LabVIEW》的其他文章
用户自定义控件中 Control, Type Def. 和 Strict Type Def. 的区别
编辑
2006/7/28 18:04:49 | 添加评论 | 阅读评论 (7) | 发送消息 | 查看引用通告 (0) | 写入日
志 | 我和 LabVIEW
(没有名字)
谢谢大侠
2007/8/13 0:24:37
•
LabVIEW 程序的内存优化 1
一. VI 在内存中的结构
打开一个VI的属性面板(VI Properties),其中的“内存使用”(Memory Usage)是用来查看
这个VI内存占用情况的。它显示了一个VI内存占用所包含的四个主要部分:前面板、框图、代码和
数据,以及这四个部分的总和。但在打开一个VI时,这四段内容并不是同时都会被LabVIEW调入
内存的。
当我们打开一个主VI时,主VI连同它的所有子VI的代码和数据段都会被调入内存。由于主VI
的前面板一般情况下是打开的,它的前面板也就同时被调入内存。但是此时主VI的框图和子VI的前
面板、框图并没有被调入内存。只有当主动查看主VI的框图或是打开子VI的前面板和框图时,它们
才会被调入。
基于LabVIEW的这种内存管理的特性,我们在编写VI的时候可以通过以下方法来优化
LabVIEW程序的内存使用。
第一,把一个复杂VI分解为数个子VI。子VI的使用会增添额外的前面板和框图的空间,但并不
增添额外的代码和数据空间。由于程序运行时只有代码和数据被调入内存,因此使用子VI不会占用
额外的内存。使用子VI的好处还在于当子VI运行结束时,LabVIEW可以及时收回子VI的数据空
间,从而改善了内存的使用效率。
第二,在没有必要时不要设置子VI的重入(Reentrant)属性。重入型VI每次运行时都会对自
己使用的数据生成一个副本,这增加了内存开销。
第三,主VI的面板通常就是用户界面,需要显示给用户。但是要尽量避免开启子VI前面板。比
如,在子VI中使用与其前面板控件有关的属性节点(Property Node)会导致它的前面板被调入内
存中,增加了内存开销,所以要尽量避免在子VI中使用主面板控件的属性节点来设置控件的值,而
可以用局部变量等方法来替代。
第四,我们可以放心地在 VI 的前面板(对于非界面VI)和框图里添加图片,注释等信息来帮助
你编写、维护LabVIEW程序,这些帮助信息不会在VI运行时占用内存。
二. 内存泄漏。
LabVIEW与C语言不同,它没有任何分配或释放内存的语句,LabVIEW可以自动管理内存,在
适当的时候分配或收回内存资源[1]。这样就避免了C语言中常见的因为内存管理语句使用不当而引
起的内存泄漏。
在LabVIEW中一般只有一种情况能够引起内存泄漏,即你打开了某些资源,却忘记了关闭它们。
比如,在对文件操作时,我们需要先打开这个文件,返回它的句柄。随后如果忘记了关闭这个句柄,
它所占用的内存就始终不会被释放,从而产生内存泄漏。LabVIEW中其它带有打开句柄的函数或VI
也会引起同样的问题。
由于内存泄漏是动态产生的,我们无法通过VI的属性面板来查看,但可以通过Windows自带的
任务管理工具来查看LabVIEW程序内存是否有泄漏。也可以使用LabVIEW的Profile
(Tools>>Advanced>>Profile VIs)工具来查看某个VI运行时内存的分配情况。
三. 缓存重用
LabVIEW程序主要是数据流驱动型的。数据传递到不同节点时往往需要复制一个副本。这是
LabVIEW为了防止数据被节点改变引起错误所做的一种数据保护措施。只有当目标节点为只读节点,
不可能对输入数据作任何更改时,才不在这些节点处做备份。例如,数组索引节点(Index)是不会
改变数组值的,LabVIEW在这里就不为输入数组做备份。对于加减法运算等肯定改变输入数据的节
点,LabVIEW往往需要对输入或输出数据作备份。有些LabVIEW程序,比如涉及到大数组运算的
程序,内存消耗极大。其主要原因就是LabVIEW在运算时为数组数据生成了过多的副本。
实际上很多LabVIEW节点是允许使用缓存重用的,这类似C语言调用子函数所使用的地址传递。
通过合理设计和使用缓存重用节点,可以大大优化LabVIEW程序的内存使用。使用LabVIEW 7.1
的Tool>>Advanced>>Show Buffer Allocations (LabVIEW 8.0 之后使用
Tool>>Profile>>Show Buffer Allocations)工具可以在VI框图中查看缓存的分配情况。打开该
工具,凡是在框图中有缓存分配的地方,都会显示出一个黑点。
下面是几个最常用节点的试验结果。LabVIEW节点众多,不可能一一列举,文中未提及的节点读
者在编程时自己可以尝试。
1. 一般顺序执行VI中的运算节点
图1:简单的顺序执行程序
如图1所示,程序对一个常量加1,然后将结果输出。
“+1”节点输出端有一个黑点,表示LabVIEW在此处开辟了一个缓存用于保存运算结果。
其实完全可以利用输入数据的内存空间来保存这个运算结果。我们可以通过如下的方法来告知
LabVIEW编译器,在此运算节点处重用输入数据的内存空间。
首先,用一个控制型数值控件代替图中的数值常量,然后分别将VI中的两个控件与VI的接线器
(Connector Pane)相连。
图2:实现缓存重用
图2是经过我们优化后的VI,LabVIEW在“+1”节点处没有开辟新的缓存。LabVIEW中其它运
算节点也有类似的性质。
2. 移位寄存器(Shift Register in the Loop Structure)
移位寄存器是LabVIEW内存优化中最为重要的一个节点,因为移位寄存器在循环结构两端的接
线端是强制使用同一内存的。这一特性可以被用来通知LabVIEW在编译循环内代码时,重用输入输
出缓存。
图3: 对数组进行数值运算的顺序执行程序
让我们分析一下图3所示的程序:它首先构造了一个数组,然后对这个数组进行了几次数学运算。
每一步运算,LabVIEW都要开辟一块缓存用以保存运算结果的副本。打开VI属性面板上的内存使
用,可以看到这个VI大约会占用2.7M的内存空间。其实这些副本都是不必要的,每一步运算的结
果都可以被保存到输入数据的内存空间。我们可以把所用的运算节点都放到一个子VI中,然后利用
上一段提到的方法,使子VI中的代码缓存重用。还有一种方法,利用移位寄存器也可以实现缓存重
用。
图4: 利用移位寄存器实现缓存重用
如图4,我们可以将运算代码放在一个只运行一次的循环结构内,由于运算部分的输入和输出都
与移位寄存器相连,这就相当于通知了LabVIEW,在运算的输入输出需要使用同一块缓存。因而,
LabVIEW 不再为每一步运算开辟新的缓存而是直接利用输入数据的缓存保存结果。打开VI属性面
板上的内存使用,可以查看到这个VI的内存占用已经减少到了原来的六分之一。
在 LabVIEW 8.5 中,有了一个新的结构——缓存重用结构,专门用于优化代码的内存使用。可
以不必再使用移位寄存器来完成这项工作了。
3. 库函数调用节点(Call Library Node)
以传递整型参数为例:在参数配置面板,我们可以选择值传递(Pass Value)或选择指针传递(Pass
Pointer to Value)。
当选择了值传递时,库函数调用节点是不会改变该参数的内容的。如果我们在该库函数调用节点
参数的左侧接线端引入输入数据,在输出端引出输出参数,那么输出数据其实是直接由输入数据引出
的,LabVIEW不会在这个节点处开辟缓存。
在指针传递方式时,LabVIEW则认为传入的数据会被改变。如果输入数据同时还要发往其它节
点,LabVIEW会在此处开辟缓存,为输入数据作一个副本。选用指针传递方式,库函数调用节点的
每一对接线端也同样是缓存重用的。就是说,库函数调用节点的输出值是直接存放在输入值的缓存空
间的。
如果一个参数只用作输出,我们通常会在库函数调用节点的输入接线端为它建立一个输入常数,这个
常数的地址空间并不能直接被利用,它只是为库函数调用节点开辟的缓存而设置的初始值。不接输入
常数,LabVIEW也会为此参数开辟一块缓存。但是,这样每次传入的参数值都会有变化。例如图5,
库函数调用节点调用的函数功能是为把输入的值加1,然后输出。图5-a中的输出值永远都是1,而
图5-b,每次运行输出结果都会比前次增加1。这是因为库函数调用节点每个指针传递的参数的输入
输出用的是同一块缓存,即每次运行输入值是上回的输出值。
图5: 库函数调用节点
我们可以利用图5-c的例子证明LabVIEW某些节点是缓存重用的。每次运行5-c的例子,输出
结果都会比前次增加2。这是因为示例中的参数接线端以及“+1”节点的输入输出端所使用的都是同
一缓存。
如果,库函数调用节点中某个参数只有输入链进去,没有输出。那么,LabVIEW 是假设你调用
的函数不会修改这个参数的。LabVIEW 不会为这个数据做拷贝,它会重用这个数据的缓存。但如果
你调用的函数修改的这个数据,你的程序就会面临这样一个潜在的危险:这个数据可能被程序其它部
分的代码使用了,在那里,你看不出这个数据有任何被改动的地方,但它在运行时却不是你期望的数
值。因为这个数据所在的缓存,被程序其它一个地方的一个库函数调用节点给重用了,而这个节点又
偷偷摸摸的修改了它。
在图5中的示例中,如果库函数调用节点输出的参数是个数组或者字符串,那么就必须为它相对
应的输入端联入一个与输出数据大小一致的数组或字符串。否则,LabVIEW无法知道输出数据的大
小,而使用默认分配的缓存空间很容易出现数组越界错误。
四. 小结
缓存重用是LabVIEW内存优化的最重要的一个环节。精心设计的LabVIEW程序可以大大节约
内存的占用,提高运行效率。但是,在编写完程序后再按照程序优化的技巧回头去优化一段已有的程
序,这并不是一个好的编程方法。我们应该先熟悉理解优化的方法,在以后的开发过程中自然而然地
将它们应用在编程中。
LabVIEW 程序的内存优化 2 - 子 VI 的优化
1. 子 VI 参数的缓存重用
数据在子 VI 间传入传出,如果程序设计的好,可以做到缓存重用,使得数据在主 VI 和子 VI 中
都不发生拷贝,提高程序的效率。
我们先来看一下图1所示的 VI。打开 Tool>>Profile>>Show Buffer Allocations 工具查看
一下这个 VI 中内存分配的情况,会发现在代码的加法函数处有一个黑点。这个黑点说明程序在这里
有分配了一块内存,这个内存是用来存储加法运算结果的。
图1:控件不与接线器相连时,加法处有内存分配
为什么加法函数在这里不做缓存重用呢?利用其中一个加数的内存空间来保存计算结果。
当这个 VI 运行的时候,图2中,加数 Numeric 的数据是由 VI 前面板的控件提供的。如果用
户不修改控件的值,每次 VI 运行,这个数值应该是保持不变的。如果加法函数在这里做缓存重用,
加数或者说它对应的控件中的数据,就会在加法运算执行后被修改。这样程序就会出现逻辑上的错误。
所以把一个这样的控件联在 LabVIEW 的运算节点上,运算节点是不能重用控件的数据内存的。
同样的道理,链接一个常量到运算节点上,节点同样不能做缓存重用。在子 VI 中,没有连到接线器
上的输入控件就相当与一个常量。
但是,如果我们让 VI 上的控件与 VI 的接线器(Connector Pane)相连,情况就不一样了。
入图2所示,把三个控件连到接线器上,程序中加法节点上那个黑点就消失了,不再为运算结果分配
新的内存。
图2:控件不与接线器相连时,加法处有内存分配
这是因为,当输入控件与接线器连接后,LabVIEW 就认为这个输入值应当是由子 VI 的调用者
(父 VI)提供的:连到接线器上,逻辑上,这个输入控件就不再是常量,而是一个输入变量了。既
然是输入变量,子 VI 不需要记住输入的数据共下次调用时使用,因此可以把新产生的数据放在输入
参数所在的内存,做到缓存重用。
你可能在想,这个输入参数的内存不一定可以被修改吧,万一它的数据还要在父 VI 中被其它节
点使用呢?
子 VI 是不需要考虑这点的,输入数据的数据被修改肯定是安全的,这一点是由父 VI 来保证的。
如果输入数据不能被修改,父 VI 会把传入的数据拷贝一份再传到子 VI 中去。
比如图3中的程序,它所调用的子 VI 就是图2中那个 VI。由于与它的第一个输入参数相连的
是一个常量,而常量的值是不能被改变的。所以 LabVIEW 要把这个常量的值复制一份,再传到子 VI
中去,以保证子 VI 中的运算节点可以做缓存重用。
图3:父 VI 中的数据拷贝
如果图3中的父 VI,他也使用与接线器相连的输入控件为子 VI 提供输入参数,则 LabVIEW 会
知道,父 VI 的这个数据是由再上一层 VI 提供的,这里也不需要需要做数据拷贝。这样,这个 VI
就也做到了缓存重用。设计合理,参数在传递多个深度后都不需要开辟新内存的。
从上面的说明中,还可以发现一个问题。就是,有时候子 VI 的改动,会影响父 VI 的行为,比
如是否为传入子 VI 的数据做个拷贝等等。有时候我们发现改动了一个子 VI,它的父 VI 也需要重
新保存,就是由这个原因引起的。
2. 输入输出参数的排布
在子 VI 的程序框图上,不论代码有多复杂,有多少嵌套的结构,控件终端最好按照这样的方式
排布:所有输入参数(控制型控件的终端)都放在代码的最左端排成一列;所有的输出参数(显示型
控件的终端)都放在代码。比如图4中的代码的风格就比较好。
图4:控件终端整齐的排列在程序框图左右两端
这首先是为了保证程序有良好的可读性。我们在阅读 LabVIEW 代码的时候总是按照从左到右的
顺序,所有的参数都排布在一起,我们就可以以数据线为线索,轻易的找的数据被读写的地方。其次,
这种风格的 VI,在效率上也比较优化。
对于一个输入参数(控制型控件的终端),如果把它放程序代码的最左侧,所有结构的外面,程
序在运行这个子VI之前,就可以得到这个参数的确切值了。
但是,如果这个终端是在代码的某个结构中的,在某一结构的内部,那么LabVIEW必须在运行
到这一结构内部的时候,才可以去读这个参数的值,否则可能会引起罗技上的错误。比如说,一个控
制型控件的终端是在一个循环的内部,开始时它的值是x。在运行到第n次循环之前,这个终端对应
的前面板上的控件被人改为一个新的数值y。那么逻辑上,在执行第n次循环之前,每次用到这个参
数时,它的值要保持为x,而在第n次循环的时候,又要使用它的新值y。这样的数据所在的内存,
LabVIEW 显然是不能将其重用的,否则下次循环再读它的时候,数据就不正确了。
如果这个终端是在所有结构之外,LabVIEW 则可以根据数据线的链接,明确的判断出在某一节
点执行完之后,程序再也不需要用到这个参数的值了,那么 LabVIEW 就可以重用它所在的内存,
以避免开辟新内存,拷贝数据等操作。这样就提高了程序的内存效率。
对于一个输出参数(显示型控件的终端),如果它位于某个条件结构的内部,LabVIEW 就要考
虑,程序有可能执行不到这个条件。LabVIEW 就会多添加一些代码来处理这种情况,当 VI 没有运
行到这个条件时,要给输出参数准备一个默认值。
把这个终端移到所有结构之外,就可以省去这部分 LabVIEW 自动添加上去的工作和,稍微提高
一点效率:)
3. 良好的数据流结构可以优化程序内存效率
先看一个程序:
图5:程序中没有必要的数据线分枝
图5 的程序只是一个演示,不必追究它到底实现了什么功能。图中的左半部分是主 VI,在这个 VI
中对输入的数组数据Array进行了两次操作:一次使用 subVI“My Search” ;另一次使用了数组
排序函数。图5 的右半部分是 subVI“My Search”的程序框图。
需要注意的是,主 VI 上 Sort 1D Array 函数那里有个黑点(这个黑店靠近黄色方块的中心,
这里看不太清楚,和图6对比一下,就可以发现了),说明这里做了一次内存分配。这是因为Array
的数据被同时传递到了“My Search”和“Sort 1D Array”两个节点进行处理。这两个操作可能会同时
进行,LabVIEW 为了安全(两个操作对数据的改动不能相互影响,不能同时对一块内存进行读写),
就必须为这两个节点准备两份数据在两份内存中。所以在“My Search”和“Sort 1D Array”两个节点
中,如果一个节点用了原来Array的内存,另一个节点就需要拷贝一份数据给自己用。
不过,如果看一下“My Search”的程序框图,它其实没有对Array数据进行任何改动,主VI完
全没有比要给“Sort 1D Array”开辟一块新内存。我们只要对程序稍作改动,就可以对此进行优化。
图6 是改进后的程序:
图6:符合数据流风格的主VI
在改进后的程序中,Array 数据首先传入subVI“My Search”,然后又传出来,继续传给“Sort 1D
Array”函数。这样子看上去好像数据要多到子VI中转一圈,但实际上,由于子VI中Array输入输
出是缓存重用的,实际上相当于只是把数组数据的引用传给了子VI,效率是相当高的。而在主 VI 中,
执行“Sort 1D Array”时,LabVIEW 知道输入数据现在是这个节点专用的,改了他也是安全的,于
是也可以缓存重用。图六中,“Sort 1D Array”上的那个小黑点就消失了。
图6 中的主 VI,它的优点首先是符合数据流的风格。一个主要的数据从左到右,流经每个节点。
这样的程序非常容易阅读和理解。LabVIEW 也更容易对这样的代码进行优化,所以这样风格的程序
通常效率也比较高。
有的时候,利用 LabVIEW 的自动多线程特性,书写并行代码,对程序效率有利。比如,程序中
某一部分的代码需要较长时间的计算或者读写时间的情况。但是并不是任何时候并行执行都好。并行
书写的程序不易理解,容易出错,多线程运行也会带来额外的开销。像图5、图6中的程序,数据量
较大,但是并没有比较耗时的运算操作,或数据读写操作,这样的程序,串行运算比并行效率更高。
LabVIEW 程序中的线程 1 - LabVIEW 是自动多线程语言
一. LabVIEW 是自动多线程语言
一般情况下,运行一个 VI,LabVIEW 至少会在两个线程内运行它:一个界面线程(UI Thread),
用于处理界面刷新,用户对控件的操作等等;还有一个执行线程,负责 VI 除界面操作之外的其它工
作。LabVIEW 是自动多线程的编程语言,只要 VI 的代码可以并行执行,LabVIEW 就会将它们分
配在多个执行线程内同时运行。
图1 是一个正在运行的简单 VI,它由单独一个一直在运行的循环组成。在此情况下,这个执行
循环的线程运算负担特别重,其它线程则基本空闲。在单 CPU 计算机上,这个线程将会占用几乎
100% 的 CPU 时间。图1 中的任务管理器是在一个双核 CPU 计算机上截取的。这个循环虽然在
每一个时刻只能运行在一个线程上,但这并不表示他始终不变的就固定在一个线程上。他可能在这个
时刻运行在这个线程上,另一时刻又被调度到其他线程上去运行了。(关于这一段,在看完本文第二
章:LabVIEW 的执行系统,会有更深刻的理解。)
因此,图1 这个程序最多只能占用两个 CPU 内核 50% 的总 CPU 时间,两个 CPU 内核各
被占用一些。
图1:双核 CPU 计算机执行一个计算繁重的任务
图2 是当程序有两个并行的繁重计算任务时的情况,这时 LabVIEW 会自动把两个任务分配到
两个线程中去。这时即便是双核 CPU 也会被 100% 占用。
图2:双核 CPU 计算机执行两个计算繁重的任务
从上面的例子,我们可以得出如下两个结论。
1. 在 LabVIEW 上编写多线程程序非常方便,我们应该充分利用这个优势。一般情况下,编写
程序时应当遵循这样的原则:可以同时运行的模块就并排摆放,千万不要用连线,顺序框等方式强制
它们依次执行。在并行执行时, LabVIEW 会自动地把它们安排在在不同线程下同时运行,以提高
程序的执行速度,节省程序的运行时间。今后多核计算机将成为主流配置,多线程的优势会更为明显。
特殊的情况也是有的,即用多线程时,运行速度反而慢。 以后我们再来详细介绍此类特殊情况。
2. 假如有一个或某几个线程占用了 100% 的 CPU,此时系统对其他线程就会反应迟钝。例如,
程序的执行线程占用了100% 的 CPU,那么用户对界面的操作就会迟迟得不到响应,甚至于用户
会误认为程序死锁了。所以在程序中要尽量避免出现 100% 占用 CPU 的情况。 目前大多数的计
算机还是单核单个 CPU 的,因此要避免任何一个线程试图 100% 占用 CPU 的情况(如图1、图
2 所示的程序)。
此类问题最简单的解决方法就是在循环内加一个延时。在图1、图2 的例子中,如果在每个循环
内加上 100 毫秒的延时,CPU 占用率就会接近为 0。
对于总运行时间较短的循环(假如CPU 占用总时间不足 100毫秒)就没有必要再加延时了。
在很多情况下,运行时间很长的循环往往都只是为了等待某一个任务的完成,在此类循环体的内
部几乎没有耗时较多的、又有意义的运算,所以必须在循环框内加延时。
对于那些确实非常耗费 CPU资源 的运算(如需要 100% 地占用 CPU 几秒钟甚至更长的时
间),最好也在循环内插入少量延时,从而让 CPU 至少 空出 10% 的时间给其它线程或进程。你
的程序会因此而多运行 10% 的时间。 但是由于 CPU 可以及时处理其他线程的需求,比如界面操
作等,其他后台程序也不会被打断,用户反而会感觉到程序似乎运行得更加流畅。反之,假如你的程
序太霸道了,CPU长期被某些运算所霸占,而别的什么都不能做,这样的程序,用户是不可能满意
的。
还有这样一种情况,比如某些运算可能需要程序循环 1,000,000次,每执行一次仅需要 0.1 毫
秒。此时如果在每次循环里都插入延时,即使是 1 毫秒的延时,也会令程序速度减慢 10 倍。 这
当然是不能容忍的。这种情况下,就不能在每次循环都加延时了,但可以采用每一千次循环后加上 10
毫秒延时的策略。此时,程序仅减慢 10% 左右,而 CPU 也有处理其他工作的时间了。
在处理界面操作的 VI 中,常常会使用到 While 循环内套一个 Event Structure 这种结构形
式。在这种情况下,就没有必要再在循环内添加延时了。因为程序在执行到 Event Structure 时,
如果没有事件产生,程序不再继续执行下去,而是等待某一事件的发生。这是,运行这段代码的线程
会暂时休眠,不占用任何 CPU 资源,一直等到有事件发生,这个线程才会重新被唤醒,继续工作。
LabVIEW 程序中的线程 2 - LabVIEW 的执行系统
二、LabVIEW 的执行系统
1. 什么是执行系统
早期 LabVIEW 的 VI 都是单线程运行的,LabVIEW 5.0 后才引入了多线程运行。其实,对于
并排摆放的LabVIEW 函数模块而言,即使LabVIEW 不为它们分配不同的线程,通常也是“并行执
行”的。LabVIEW 会把它们拆成片断,轮流执行:这有一点像是 LabVIEW 为自己设计了一套多线
程调度系统,在系统的单个线程内并行执行多个任务。
LabVIEW 中这样一套把 VI 代码调度、运行起来的机制叫做执行系统。现在的 LabIVEW 有六
个执行系统,分别是:用户界面执行系统、标准执行系统、仪器I/O执行系统、数据采集执行系统、
以及其他1、其他2系统。一个应用程序中使用到的众多子 VI 可以是分别放在不同的执行系统里运
行的。用户可以VI 属性面板上选择 Execution 页面,可以在这个页面指定或更改某个 VI 的首选
执行系统。
2. 执行系统与线程的关系
LabVIEW 在支持多线程以后,不同的执行系统中的代码肯定是运行在不同线程下的。用户界面
执行系统只有一个线程,并且是这个程序的主线程。 这一点与其他执行系统都不一样,其他的执行
系统都可以开辟多个线程来执行代码。用户除了可以设置 VI 的执行系统,还可以设置它的优先级。
优先级分 5 个档次(暂先不考虑 subroutine)。在 LabVIEW 7.0 之前, LabVIEW 在默认情况
下为同一个执行系统下每个档次的优先级开启一条独立的线程;而在LabVIEW 7.0 之后,LabVIEW
在默认会默认的为每个执行系统下每个档次的优先级开启 4 条线程。当然你使用
可以更改这一设置。但是对于普通用户来说最好不要改
动它。
在用 C 语言编写多线程程序时,你还要注意不能开辟太多的线程,因为线程开辟、销毁、切换
等也是有消耗的。线程太多可能效率反而更差。但是使用 LabVIEW 就方便多了。在使用默认设置
的情况下,LabVIEW 最多为你的程序开辟 5 条线程:一条用户界面线程,四条标准执行系统标准
优先级下的线程。五条线程不会引起明显的效率损失。
3. 用户界面执行系统
程序中所有与界面相关的代码都是放在用户界面执行系统下执行的。就算你为一个 VI 设置了其
他的执行系统,这个 VI 的前面板被打开后,他上面的数据更新的操作也会被放在用户界面执行系统
下运行。还有一些工作,比如利用 Open VI Reference 节点动态的把一个 VI 加载到内存的工作,
也是在用户界面执行系统下运行的。
前面提到了,用户界面执行系统一个最特殊的执行系统,因为它只有一个线程(我们就给这个线
程起名叫用户界面线程好了)。LabVIEW 一启动,这个线程就被创建出来了,而其他执行系统下的
线程只有在被使用到时才会被 LabVIEW 创建。
在图1 中的例子中,如果是运行在其他的线程下,都会把我的双核 CPU 占满。原因参考本文第
一章(LabVIEW 是自动多线程语言)的图2。但是如果我们把 VI 的执行系统改为用户界面执行系
统,那么这两个循环就会运行在同一线程下,我的双核 CPU 其中一个核将被占用 100%,另一个
则基本空闲。
图2 是 VI 在运行过程中的一幅截图,虽然程序在单线成下运行,两个循环仍然是并行运行的,
两个显示控件的数据会交替增加。
图1、2:在界面线程-单线程下运行的并行任务
因为 LabVIEW 是自动多线程的,如果一些模块不能保证多线程安全,就需要把他们设定为在用
户界面线程运行。这样就等于强制他们在同一个线程下执行,以保证安全。具体例子在下一节讨论。
4. 其他几个执行系统
在 执行系统一栏还有其他几个条目可选。
“same as caller”是默认选项,它表示这个 VI 沿用调用它的上层 VI 设置的执行系统。如果顶
层 VI 也选择“same as caller”,那么就等于它选择了标准执行系统。
“standard”标准执行系统是最常用的配置方式。
“Instrument I/O”仪器I/O执行系统一般用于发送命令到外部仪器,或从仪器中读取数据。这
是程序中较为重要的操作,需要及时运行。所以仪器I/O执行系统中的线程的优先级比其他执行系统
中的线程要高一些。
“data acquisition”数据采集执行系统一般用于快速数据采集。数据采集执行系统中的线程的数
据堆栈区比较大。
“other 1”,“other 2”其他1、其他2执行系统没什么特别之处。如果你一定要让某些 VI 运行
在独立的线程内,则可以使用这两个选项。
绝大多数情况下,用户使用界面执行系统、标准执行系统就已经足够了。
LabVIEW 程序中的线程 3 - 线程的优先级
三、线程的优先级
在 VI 的属性设置面板 VI Properties -> Execution 中还有一个下拉选项控件是用来设置线程
优先级的(Priority)。这一选项可以改变这个 VI 运行线程的优先级。
优先级设置中共有六项,其中前五项是分别从低到高的五个优先级。优先级越高,越容易抢占到
CPU 资源。比如你把某个负责运算的 VI 的优先级设为最高级(time critical priority),程序在
运行时,CPU 会更频繁地给这个 VI 所在线程分配时间片段,其代价是分配给其它线程的运算时间
减少了。如果这个程序另有一个线程负责界面刷新,那么用户会发现在把执行线程的优先级提高后,
界面刷新会变得迟钝,甚至根本就没有响应。
优先级设置的最后一项是 subroutine, 它与前五项别有很大的不同。严格的说 subroutine 不
能作为一个优先级,设置 subroutine 会改变 VI 的一些属性:
设置为 subroutine 的 VI 的前面板的信息会被移除。所以这样的 VI 不能用作界面,也不能单
独执行。
设置为 subroutine 的 VI 的调试信息也会被移除。这样的 VI 无法被调试。
当程序执行到被设置为 subroutine 的 VI 的时候,程序会暂时变为单线程执行方式。即程序在
subroutine VI 执行完之前,不会被别的线程打断。
以上的三点保证了 subroutine VI 在执行时可以得到最多的 CPU 资源,某些作为关键运算的
VI,又不是特别耗时的,就可以被设置为 subroutine 以提高运行速度。比如有这样一个 VI,他的
输入是一个数值数组,输出是这组数据的平均值。这个运算在程序中需要被尽快完成,以免拖延数据
的显示,这个 VI 就是一个蛮适合的 subroutine VI。
在设置 VI 优先级的时候有几点需要注意的。
提高一个 VI 的优先级一般不能显著缩短程序的运行时间。提高了优先级,它所需要的 CPU 时
间还是那么多,但是 CPU 被它占用的频率会有所提高。
高优先级的 VI 不一定在低优先级 VI 之前执行。现在常用的多线程操作系统采用的都是抢占式
方式,线程优先级别高,抢到 CPU 的可能性比低级别的线程大,但也不是绝对的。
使用 subroutine 时要格外注意,因为他会让你的程序变成单线程方式执行,这在很多情况下反
而会降低你的程序的效率。比如一个 VI 并非只是用来运算,它还需要等待其它设备传来的数据,这
样的 VI 就绝对不能被设置为 subroutine。现在多核 CPU 已经很流行了,在这样的计算机上,单
线程运行的程序通常比多线程效率低,这也是需要考虑的。
LabVIEW 程序中的线程 4 - 动态连接库函数的线程
四、动态连接库函数的线程
1. CLN 中的线程设置
LabVIEW 可以通过 CLN(Call Library Function Node)节点来掉用动态连接库中的函数,
在 Windows 下就是指 .DLL 文件中的函数。用户可以通过 CLN 节点的配置面板来指定被调用函
数运行所在的线程。相对于 VI 的线程配置,CLN 的线程选项非常简单,只有两项:界面线程(Run
in UI thread)和可重入方式(reentrant)。
图1:在 CLN 的配置面板上选择函数运行的线程
在 LabVIEW 的程序框图上直接就可以看出一个 CLN 节点是选用的什么线程。如果是在界面线
程,则节点颜色是较深的橘红色的;如果是可重入方式的,自节点是比较淡的黄色。
图2:不同颜色表示 CLN 不同的线程设置
2. 如何选择合适的线程
对于在 CLN 中选取何种线程,有一个简单的判断方法。如果你要使用的动态连接库是多线程安
全的,就选择可重入方式;否则,动态连接库不是多线程安全的,就选择界面线程方式。
判断一个动态连接库是不是线程安全的,也比较麻烦。如果这个动态连接库文档中没用明确说明
它是多线程安全的,那么就要当他是非线性安全的;如果能看到动态连接库的源代码,代码中存在全
局变量、静态变量或者代码中看不到有 lock 一类的操作,这个动态连接库也就肯定不是多线程安全
的。
选择了可重入方式,LabVIEW 会在最方便的线程内运行动态连接库函数,一般会与调用它的 VI
运行在同一个线程内。因为 LabVIEW 是自动多线程的语言,它也很可能会把动态连接库函数分配
一个单独的线程运行。如果程序中存在没有直接或间接先后关系的两个 CLN 节点,LabVIEW 很可
能会同时在不同的线程内运行它们所调用的函数,也许是同一函数。对于非多线程安全的动态连接库,
这是很危险的操作。很容易引起数据混乱,甚至是程序崩溃。
选择界面线程方式:因为 LabVIEW 只有一个界面线程,所以如果所有的 CLN 设置都是界面线
程,那么就可以保证这些 CLN 调用的函数肯定全部都运行在同一线程下,肯定不会被同时调用。对
于非多线程安全的动态连接库,这就保证了它的安全。
3. 与 VI 的线程选项相配合
如果你的程序中大量频繁的调用了动态连接库函数,那么效率就是一个非常值得注意的问题了。
我曾经编写过一个在 LabVIEW 中使用 OpenGL 的演示程序(为了演示我们开发的“Import
Shared Library 功能”),对 OpenGL 的调用全部是通过 CLN 方式完成的。由于 OpenGL 的
全部操作必需在同一线程内完成,我把所有的 CLN 都设置为在界面线程运行的方式。对 VI 的线程
选项没有修改,还是默认的选项。结果程序运行极慢,每秒钟只能刷新一帧图像,CPU 占用 100%。
但是作为动画每秒至少25帧才能看着比较流畅。
我开始试图用 LabVIEW 的 profile 工具来查找效率低下的 VI,结果居然查找不到。在 Profile
Performance and Memory 工具上显示的 CPU 占用时间只有一点点。这个工具竟然显示不出程
序中最耗时的操作在哪里,自然我也对如何优化这个程序无从下手了。后来这个演示程序被搁置了一
段时间。
直到有一天我从同事给我的提供的一些信息中得到了启发,才突然想通,这些 CPU 全部被消耗
在线程切换中了。我们调用 OpenGL 方法是为每个 OpenGL API 函数包装一个 API VI,这些 API
VI 非常简单,程序框图就只有一个 CLN 节点,调用相应的 OpenGL 函数。由于每个 VI 都是在
默认的执行线程中运行,而 CLN 调用的函数却是在界面线程下运行的。所以每次执行一次这样的
API VI,LabVIEW 都要做两次线程切换,从执行线程切换到界面线程,执行完函数,在切换回执行
线程。
线程切换是比较耗时的。我的演示程序刷新一帧要调用大约两千次 OpenGL API VI,总耗时接
近一秒。
解决这个问题,要么把所有 API VI 中的 CLN 都改为可重入方式,但编写程序时要保证所有被
调用的函数都运行在同一线程内,这比较困难。比较容易实现的是,把程序中对 OpenGL 操作相关
的 VI 也全部都设置为在界面线程下运行。我选择的就是后一种方法。改进后的程序,每秒钟画30
帧图像也不会占满 CPU。
由此,我也想通了另一个问题。就是我曾经发现调用 Windows API 函数遇到的错误信息丢失的
问题。在调用某一 Windows API 函数返回值为0时,表示有错误发生了。这时你可以调用
GetLastErr 和 FormatMessage 得到错误代码和信息。但是我经常遇到的问题是:前一个函数明
明返回值为0,但是随后调用的 GetLastErr 函数却无法查到错误代码。
我想这一定是看上去两个函数是先后被 LabVIEW 调用的,但实际上 LabVIEW 在它们之间还
要做两次线程切换才行。错误代码就是在线程切换的过程中被丢失了。解决这个问题的办法也是:把
调用这三个函数的 CLN 和调用它们的 VI 全部设置为在界面线程下运行就可以了。
相关文章:
LabVIEW 的运行效率 1 - 找到程序运行速度的瓶颈
一、找到程序运行速度的瓶颈
想要提高程序的运行效率,首先要找到程序运行的瓶颈在哪里。LabVIEW 程序的运行也符合
80/20 定理:20%的程序代码占用了80%的运行时间。如果能找到这20%的代码,加以优化,就
可以达到事半功倍的效果。
对于已经编写好的程序,可以通过内存和信息工具来查看程序中每个 VI 运行了多长时间。对程
序的效率进行优化,要从最耗时的 VI 着手。
内存和信息工具可以从 LabVIEW 的菜单项 Tools->Profile->Performance and Memory
中启动。图1 是这个工具的界面。
图1:内存和信息(Profile Performance and Memory)工具
在内存和信息工具中会列出一个程序中的全部子 VI。在运行这个程序之前,先按下工具界面上的
Start 按钮,工具就开始为所有的子 VI 进行统计了。你的程序运行结束后,点击工具上的
Snapshot,就会显示出每个子 VI 在刚才的运行中占用了多少 CPU 时间。按照 VI Time 降序排
序,排在最前面的几个 VI 就是程序的瓶颈,是需要重点优化的对象。
一个子 VI 占用了大量 CPU 时间,有可能是因为它内部的运算较为复杂,那就需要打开它,对
它的算法进行优化。但更有可能的是因为这个 VI 被程序执行的次数太多。这时,你就要考虑程序结
构了,是否可以减少这个 VI 的运行次数,比如把它从某些不必要的循环中挪出去,或者拆分这个 VI
的代码,把没有必要循环执行的部分分离出去,挪到循环体外面。
并不是所有的运行效率问题都可以在内存和信息工具中体现出来的。
VI Time 列出的只是子 VI 的 CPU 占用时间,如果你的程序里存在大量的不必要延时,或者程
序常常被某些低速工作(如读写外部仪器,通过网络传输数据等)所阻塞。这样的程序效率肯定也是
很低的,但是这一类的低效率因素在内存和信息工具上是体现不出来的。
有些非常耗用 CPU 的操作也无法体现在内存和信息工具上。比如我在《LabVIEW 的线程》第
四章中会提到的使用 OpenGL 的例子,由于程序线程设计不当,CPU 被大量消耗在线程切换上。
从系统资源管理器看,CPU 被 LabVIEW 占满,在内存和信息工具却看不到任何一个 VI 占用了如
此多的 CPU 时间。
在多核 CPU 的计算机上,由于程序可以在多个 CPU 内核上同时执行,某些子 VI 虽然占用的
大量的 CPU 时间,如果程序线程设置合理,是可以让这些 VI 不影响到程序的整体效率的。
LabVIEW 的运行效率 2 - 程序慢在哪里
二、程序慢在哪里?
仅仅使用内存和信息工具还不能发现所有程序效率问题的。并且一旦程序的主体部分已经完成,
再对其进行修改,成本是比较高的。尤其是涉及到结构性的改动时更是如此:以前做过的测试需要重
新做,构建在这个模块之上的代码需要作相应更新。如果时间紧迫,同时考虑到这种代码改动所带来
的风险,完全可能在程序完成后就无法再对其性能进行优化了。
所以最有效的编写高效率程序的方法是在设计程序结构的时候,就考虑到可能会影响程序效率的
所有因素,直接设计出高效率的程序。而不是在程序完成后,再回头查找程序瓶颈。
下面讨论的是一些常见的运行比较慢的程序代码部分。一个程序运行效率的瓶颈通常就出现在这
些部分。所以在设计程序时,对这些部分要格外注意。
a) 读写外设、文件
相对于计算机的中央处理器、内存读写的速度而言,计算机的外围设备的处理和传输数据的速度
是非常慢的。比如,GPIB 的传输速率最高也只有 1Mbps,比内存的传输速率低了两个数量级以上。
在一个测试应用软件中,造成整个系统效率低下的瓶颈很可能就在于这类数据传输当中,程序的大部
分时间都消耗在等待外部数据上了。
b) 界面
界面刷新和等待事件也是比较耗费时间的工作,这是由于人的反应速度远不如计算机引起的。比
如你可以设置屏幕上的数据指示控件中的数值以每秒一千次的速度刷新,但是这对于用户来说毫无意
义,因为人眼和大脑根本处理不了如此快速的变化。还有,在显示给用户一条信息后,等待用户的后
续指令也需要等待一段时间。
c) 循环内的运算
设计循环的时候总是要格外小心些,因为就算一段代码运行得再快,循环个几千,甚至几百万次,
耗费是时间也不得了了。所以越是执行次数多的循环,他内部代码的效率对整体影响越大。
d) Global Variable
全局变量不但会破坏LabVIEW的代码风格,并且它的代码读写速度也是特别的慢。
e) 子VI
使用子VI是会有一定开销的,但是我们在其它文章(LabVIEW 程序的内存优化)里曾经讨论过,
使用子VI利大于弊。从这一点来说,子 VI 使用得越多越好。不过需要注意的是,动态调用子VI
的速度是非常慢的。因为他需要先把被调用的VI从磁盘装入到内存中,然后才能运行。而且,装载 VI
的工作一定是在界面线程(LabVIEW 的执行系统)中执行的。如果被动态调用的 VI 太大,就会迟
滞界面刷新,影响用户的感觉。
f) 调试信息
这一条对于已经做成可执行文件的程序是没有意义,因为 LabVIEW 在把 VI 转换成可执行文件
的时候,一定会去除调试信息的。但是还有相当一部分程序是以 .vi 文件的格式,直接在 LabVIEW
的编译环境中运行的,去除调试信息可以让这种程序降低约 50% 的 CPU 占用时间和内存。
g) 多线程和内存使用不当
LabVIEW 是自动多线程运行的,并且自动开辟、回收内存空间。这意味着对于 LabVIEW 初级
用户来说,可以不去关心有关线程和内存的问题。但是对于高级用户而言,需要追求更高的效率,还
是需要考虑多线程和内存对程序的影响的。
相关文章:
LabVIEW 对多核 CPU 的支持
以前,在计算机领域有个摩尔定律,是说每一年半,CPU 的主频都会提高一倍。但是近几年这个
定律在CPU主频上已经失效了,我 4 年前用的计算机 CPU 主频是 2G,我前几天换了一台新电脑,
CPU 主频还是 2G。
现在主要两个 CPU 生产商都意识到单纯通过提高处理器主频来提升性能的办法行不通了。他们
的新策略是通过增加 CPU 的内核来提升系统整体性能。
现在双核 CPU 是商用电脑的主流配置,也有高端电脑采用了四核 CPU。Intel 更是宣称他们用
不了5年就会做出有 80 个核的 CPU 来。
多个 CPU 同时工作,效率固然是高。但是,为了充分发挥多核的优势,为了发挥多核的威力,
还要你的软件针对多核进行一定的优化才行。首先,你的程序至少是多线程运行的。
使用常用的文本语言,比如 C++ 编写一个多线程的程序并不是一项简单的工作。除了要非常熟
悉 C++ 的基本编程方法,程序员还需要了解 Windows 多线程的运行机制,熟悉 Windows API
的调用方法,或者 MFC 的架构等等。在 C++ 上调试多线程程序,更是被许多程序员视为噩梦。
但如果使用 LabVIEW 编写多线程程序,情况就大为不同了。LabVIEW 是自动多线程的编程语
言,LabVIEW 程序员可以不需要了解任何与多线程相关概念与知识。只要他在 VI 的程序框图上,
并排放上两段没有先后关系的代码,LabVIEW 就会自动把这两段代码放在不同的线程中,并行运行。
而在多核 CPU 的计算机上,操作系统会自动为这两个线程分配两个 CPU 内核。这样就有效地利用
了多核 CPU 可以并行运算的优势。LabVIEW 的程序员不知不觉中就完成了一段支持多核系统的程
序。
有操作系统来分配 CPU 也许效率还不是最高的。
比如我现在有这样一个程序(图1),有数据采集、显示和分析三个模块。三个模块是并行执行
的。我的电脑是双核的,于是操作系统分配 CPU0 先做数据采集,CPU1 先做数据显示,等数据采
集做完了,CPU0 又会去做数据处理(图2)。数据处理是个相对任务较为繁重的线程,而电脑一个
CPU做数据处理时,另一个 CPU 却空闲在那里。这种负载不均衡就造成了程序对于整体系统的CPU
利用率不高。
图1, 2:操作系统为多线程程序自动分配CPU
对于效率要求极为苛刻的程序,还需要更高效的解决方案。LabVIEW 8.5 提供了一种解决方案,
就是利用它的定时结构来有程序员人为指定 CPU 的分配方案。
定时结构包括定时循环结构(Time Loop)和定时顺序结构(Time Sequence),他们的主要
用于在程序中精确的定时执行某段代码,但是在 LabVIEW 8.5 中它们又多了一个新的功能,就是
指定结构内的代码运行在哪一个 CPU 上。在图3中,定时顺序结构左边四边带小爪的黑方块所代表
的接线柱就是用来指定哪一个CPU或CPU内核的。
图3:一个时间数序结构
图2:时间顺序结构的输入配置面板
这个CPU设置可以在配置面板(图2)中静态的指定好,也可以像图1这样,在程序运行时指定。
执行图1所示的程序,在0和1之间切换结构内代码运行的CPU,就可以在系统监视器中看到指定
的CPU被占用的情况了。
还是以刚才那段程序为例,这一次我手工为每个任务指定他们运行的 CPU。
图4:手工指定每个任务运行的 CPU
这样一来,两个耗时较少的任务占用同一个 CPU,耗时较多的任务单独占用一个 CPU。不同 CPU
被分配到的任务比较均衡,程序整体运行速度大大加快,如图5所示:
图5:两个CPU负载均衡
用LabVIEW工程库实现面向对象编程
利用LabVIEW工程库实现面向对象编程
阮奇桢
ruanqizhen@
注意:
我写这篇文章的时候,LabVIEW 8.2 还没有出来。现在 LabVIEW 8.2 本身就以支持面向对象
的编程方法,所以这里介绍的方法有点过时。我有时间会再写一篇关于新 LVOOP 的文章。
摘 要:
本文将简要介绍图形化编程语言LabVIEW 中面向对象的编程思想。并且提出了一种实现面向对
象编程具体方法,即利用LabVIEW 8.0的新特性:工程库,来帮助实现对象的程序设计思想。
关键词:
LabVIEW,面向对象,类,工程库
Implementing Object Oriented Programming in LabVIEW
with Project Library
Abstract:
This paper introduces the Object Oriented Programming in LabVIEW, which is also
called as GOOP. And it also introduces a new way of implementing the GOOP
application: with the help of Project Library, a new feature in LabVIEW 8.0
Key Words:
LabVIEW, GOOP, Class, Project Library
一. 背景
LabVIEW是一个强大的编程语言,但是随着开发程序规模变大,LabVIEW程序员可能会觉得对
程序越来越难于管理和维护。其根本原因就是LabVIEW是面向过程的编程语言,它采用基于数据流
的运行方法。而这种程序设计方式在模块划分方面有着天然的缺陷。使用LabVIEW编写程序时关注
的是按流程完成功能,而不是程序功能模块的划分。因此LabVIEW程序划分出来的不同的块之间可
能会公用很多子VI,或全局变量,它们的存在使得程序各个模块无法完全独立,更糟糕的事模块之
间的关系可能不为编程人员所察觉。当程序规模大到一定程度,尤其是需要多名开发人员共同参与的
时候,编写出来程序会越来越显得杂乱无章,使得程序的调试、维护、和升级都变得非常困难。
解决这一问题的途径就是引入更加抽象化的面向对象的编程方法[2]。通过构造类的方法,把不同
模块之间的数据彻底分离开来,甚至把数据和操作分离开来。这样就保证了不同模块可以完全独立的
开发、测试。对某一模块的修改将不会影响到任何其他模块。这样,就可以将一个大的工程分解为可
以完全独立开发的多个模块,彻底解决前文所提到的开发困难。
早在1999年,NI就曾向用户演示过在LabVIEW中使用面向对象的编程思想的示例。一些第三
方的公司还为LabVIEW面向对象编程提供了一些开放工具。但是由于这些工具使用复杂,功能简单,
LabVIEW面向对象的编程思想当时并没有引起用户广泛的注意和重视。
刚刚推出的LabVIEW 8.0版的一些新特性明显体现出面向对象的编程思想。尽管它仍然没能实
现对面向对象的编程的整体支持,但是可以预见,LabVIEW将在后续的版本中完整的实现对面向对
象的编程的支持。
二. LabVIEW工程库(LabVIEW Project Library)
LabVIEW 8.0的一个重要新特性就是“工程库”,这也是LabVIEW向现行对象开发语言过渡的
一个重要体现。工程库是一组功能相关联的VI或其它文件的集合。工程库与传统的LabVIEW的LLB
文件有着本质的区别。LLB文件只是将一组VI打包存储的一种形式,而工程库与如何存储VI无关,
它更关注是把功能相关的VI按一定结构组合封装,以便于代码的管理和发布。
工程库的一些特性可以帮我们方便地实现面向对象的编程:
1. 工程库的名字也是库中VI的名字空间(name space)。
名字空间是LabVIEW 8.0的一个新特性。在8.0前的LabVIEW中无法打开两个文件名相同但
内容不同的VI,这就好比在C语言中,一个工程不能拥有两个名字相同的函数。新版本的LabVIEW
不再有此限制,但是被同时打开两个同名VI必须存在于不同的名字空间,也就是在不同的工程库中
的同名VI才能被同时打开。这与C++、C#等语言中的名字空间的概念类似。
2. 库中的VI有操作安全设置, 每一个VI成员可以被设置为公有(Public,可以被库外的VI调
用);或者私有(Private,只能被库的成员VI调用)。
3. 使用VI Scripting技术,可以在运行时方便的得到库的组织结构信息。
VI Scripting技术也是LabVIEW的新特性。利用它可以直接在LabVIEW中解析或更改LabVIEW
VI。
三. LabVIEW面向对象编程的具体实现方法
我们可以把一组相关的数据和VI放在一个工程库内,借以实现类的封装功能,但是这种方法不能实
现类的继承和多态。
图1:LabVIEW工程库的结构
1. 工程库的结构
例如,要建立一个表示“猪”的类,我们先要为它新建一个名为Pig的LabVIEW工程库。然后按
一定的分类方法建立文件夹结构,比如将表示数据的VI放在Attribute文件夹下;把表示动作的VI
放在Method文件夹下。也可以划分两个文件夹分别存放公有VI和私有VI。各种分类组织方法并无
本质区别,可凭个人爱好选择。
从数据和操作安全的角度考虑,需要在工程库的属性面板中设置成员VI的公有或私有属性。为了
维护和使用方便,还应当为库设置适当的版本号、图标等属性。
2. 类的设计
LabVIEW工程库一般是不能直接就拿来当作一个类来使用的。类是一个抽象概念,在使用时,
需要类进行实例化,类的实例才是真正参与工作的。类的每个实例要保存自己的一份数据,而类中的
方法则只需存有一份。因此我们需要为类编写一些用来管理数据的VI,例如图1中的。它就
相当于这个类中构造函数。
我们可以使用两种方法来为每个实例保存一份数据。
简单的方法是在初始化一个结构(cluster),把所有这个类可能用到的数据都包括在这
个结构里。例如,在本例中,可以做一个结构,有一个字符串和一个数字量构成,分别表示我们将用
到的名字,和重量。其他类中的成员VI都必须使用这个结构作为传入参数,这样就保证了每份实例
的数据互不影响。
另一种方法需要借助C语言的帮助,比较复杂,但是可以避免把一个大的数据结构作为参数传来
传去。我们可以使用C编写一套专门处理类数据的API函数,生成DLL文件供LabVIEW调用。具
体操作时,用C语言为类中所有的数据开辟一块内存空间,然后返回内存地址给LabVIEW。我们可
以在中把返回的内存指针强制转换为自定义的Data Log File Refnum数据类型,这样我们
还可以为每个类定义一个专用的reference类型。其他类中的成员VI都使用这个reference作为主
要参数。需要使用某一数据时,可以调用C语言编写的API从内存里读出数据。使用这种方法一定
要有一个类似析构函数功能的VI,释放开始时开辟的内存。这种方式类似于LabVIEW中的文件操
作VI。
图2:借助C语言的帮助实现开辟多份实例
3. 类的使用
类的使用相对来说要简单得多,与面向对象的文本语言的编写方法相类似。基本步骤也是首先调
用构造VI创建类的实例,然后对类的实例进行操作,操作结束需要调用析构VI释放实例占用的资源。
如图3所示的例子,用我们在前文中设计的“猪”的类编写的一段程序。程序中我们创建了“两头猪”,
然后经过不同的喂养方法,再比较一下他们的体重。
相信读者仅凭代码中的图标就已经可以读懂程序的功能。由此也可见面向对象编程对程序提高可
读性的帮助。而使用传统方法编写类似LabVIEW程序,由于没有很好的数据封装,在程序框图中数
据连线多且杂乱,极易引起错误。
更值得一提的是,LabVIEW与C++不同,使用面向对象的编程方法不会引起程序效率的损失。
图3:面向对象编程方法的示例
4. 其它方法实现面向对象编程
除了文中提到的借助LabVIEW工程库实现面性对象编程的编程方法之外,我们还可以借助于
LabVIEW 8.0的其它一些新特性,比如XControl等帮助实现面向对象的编程方法。他们的具体实
现方法留待其它文章讨论。
四. 面向对象的方法对LabVIEW程序设计的影响
目前,LabVIEW程序开发的一般流程是先设计和实现顶层VI,一般来说顶层VI也就是程序的
主界面。然后自上而下的设计和编写LabVIEW程序。面向对象的编程方法由于大大提高了程序模块
之间连接和搭建的效率,它使得程序员把更多的精力集中在模块的设计开发上。而不同的模块之间相
对独立,可以并行的开发、测试。这就使得LabVIEW开发大型程序的效率大大提高。可以预见,随
着面向对象的编程方法在LabVIEW中的推广,LabVIEW程序的规模将有一个本质性的飞跃提高。
下载文章中的示例程序:
/.7z
相关文章:
我和 LabVIEW
模块接口 API 的两种设计方案
假如你要设计一个程序模块,它的功能是读写 INI 文件。用户调用这个模块,就可以方便的把信
息写入 INI 文件,或从其中读出信息。
你将如何设计这个模块的接口呢?LabVIEW 中常见的方式有两种,第一,为模块的每个方法都
做一个子 VI,比如写数值型数据的方法做一个 VI,写字符串的做一个 VI,读字符串的一个 VI 等
等;另一种方案:把所有的方法都放到一个子 VI 里去,用户通过一个变量来选择运行哪个方法。
这两种方案各有优缺点。第一种方案符合一般人的思维模式,更容易让用户理解和学会使用。现
在 LabVIEW 中处理 INI 文件的模块采用的就是这种方案。每个用户可能用到的方法(甚至是每一
种数据类型),都有一个对应的 VI。维护起来也容易,哪个方法有 bug,到它对应的那个 VI 中去
调试就可以了。
但是打开这些处理 INI 文件的 VI,他们调用了一个更底层的模块,这个模块采用的是第二种接
口方案。所有对 INI 文件底层的操作,都被放到了一个子 VI(Config Data )里。用
输入参数("function")来控制执行不同的功能。
这种方案也有它的好处,我看过一本叫做《软件工程方法在LabVIEW中的应用》的书,它的内
容用一句话来概括,就是号召大家把模块都写成上述的第二种方案。不过我们先来说一下着第二种方
案的弊端。
首先,给外部用户的感觉就不如第一种方案那么清晰易学。如果把所有方法分开成独立的 VI,用
户可以只专注学习自己可能会用到的功能对应的 VI;而第二种方案,所有功能在一个接口 VI 里,
那就强迫用户把所有功能都要了解一下。
其次,每种不同功能所用到的参数都不尽相同。采用第二种方案,就意味着这个唯一的接口 VI 要
包含所有方法时用到的控件(参数)。所以这个 VI 上的控件会比较多。并且,有的控件在调用不同
功能时,用途(或者说所表达的意思)不同。这样不但会造成用户学习的困难,在使用时,也非常容
易出错。
还有一条,第二种方案的效率在某些情况下非常低下。我们把一个模块提供给用户,但用户不见
得会使用这个模块中所有的功能。第一种方案,用户程序是在编译时选择使用模块中的那些方法;而
第二种方案是在运行时选择使用什么方法。如果用户只用到一个模块中的一两个功能,采用第二种方
案,只用用户用到的方法相关的代码才会被链接到它的程序中;而采用第二个方案,不论用户是否需
要,整个模块都会被链接到它的程序中去。
这是因为这几个缺点,造成现在 LabVIEW 提供给用户的库中,几乎都是采用的第一种接口方案。
但是,着第二种方案,一度是 LabVIEW 程序设计中一个非常流行的方法,自然也有他的优点。
其一是更好的解决模块封装的问题。在 LabVIEW 8 之前,LabVIEW 本身不支持面向对象编程,
也没有提供对一个模块进行封装的功能。我如果编写一个功能模块给用户,我这个模块中所有的 VI,
即便是我只把它当作内部使用,都可以被用户调用。这是很不安全的,因为内部 VI 随时都可能被改
变调整,从而引起客户应用程序的错误。如果所有的功能都通过一个 VI 暴露给用户,则用户更容易
搞清楚只有这个 VI 他可以用,其它的 VI 都是不能被他直接使用的。并且这样也可以使自己编写的
一大堆 VI 看上去也更像是一个模块或组件。
LabVIEW 的另一个问题是,它作为数据流驱动的编程语言,不像文本语言那样可以方便的使用
全局或局部变量。在 LabVIEW 中使用全局或局部变量不但效率查,还会严重影响程序的可维护性。
我编写的模块,它所用到的内部数据如何组织呢?全局变量既然不好,那就只能考虑使用移位寄存器
了。
LabVIEW 程序如果设计的不好,数据在不同节点间传递时会产生很多份拷贝,造成效率低下。
为了解决这个问题,最好是我内部使用数据,就不要再在 VI 之间传来传去了。打开 Config Data
,你会发现这个 VI 的主体框架是一个只运行一次的循环。凡是这种只运行一次的循环,
程序真正想利用的都是循环上的移位寄存器。这个 VI 里的多个移位寄存器都是既无输入又无输出
的,它们的功能是用来保存模块的私有数据。
用移位寄存器保存模块的全部私有数据,模块的所有方法都在移位寄存器之间完成。这样数据始
终在一个 VI 内,避免了数据在不同 VI 之间传递可能会引起的复制。这是很长一段时间内都相当流
行的 LabVIEW 程序模块设计思路,不过我觉得也许现在可以放弃这个方案了。
首先,这个实现方法只适合功能简单的小模块,模块的大部分代码都放到一个 VI 中。如果模块
数据功能较多,还用这个方法编出来的 VI 就很难读懂,没法维护了。Config Data 虽
然功能并不复杂,但代码已经不那么清晰易懂了。
如果这个模块在程序中只有一个实例还好办,若要支持多个实例,那数据部分就要设计个更为复
杂以确保模块不同实例之间的数据不会混乱。
最重要的是现在 LabVIEW 自身已经开始支持面向对象的功能了。在 LVClass 中,既可以有数
据,也可以有方法;方法可以被定义为是私有的或共有的;另外之支持继承、多态等。所有这些都为
功能模块的封装和接口提供了更好的解决方案。与其费尽心机的自己想办法把格模块包装的更合理,
不如直接利用 LVOOP 已有的功能。把自己的的模块都设计为 LVClass。
一个 XControl 的实例
XControl 与 .ctl 用户定义控件相比,其最大的提高就在于它不但可以定义控件的外观,还可以
定义控件的行为。
在 XControl 出现之前,同样可以在程序中编写代码,控制程序的行为。在《用 XControl 实现
面向组件的编程》一文中提到了,这种方法在程序模块划分上有缺陷。如果用户想发布一个带有特定
行为的控件也是不可能的,因为控制控件行为的代码,是同其它代码混杂在一起的。
利用 XControl 可以解决上面提到的问题,这里以一个例子说明一下如何利用 XControl 实现一
个有特定行为的控件。
Windows 风格的工具条上的按钮有一个特点,就是当鼠标移动到按钮上方,按钮就会变亮或浮
起。LabVIEW 中默认的按钮没有这样的特性,但是实现这一点是很容易的。
以鼠标移上,按钮变亮为例:在程序中,当按钮的 Mouse Enter 事件发生时,把按钮的颜色设
置为浅颜色;当按钮的 Mouse Leave 事件发生时,把按钮的颜色设置为深色即可。现在把界面上
的按钮和控制颜色的代码都封装在一个 XControl 中。这样,其他人在使用这个 XControl时,就
无需修改他的代码,而直接获得这种颜色变化的特性了。
一、简单行为的 XControl
首先创建一个空的 XControl。
图1、2:创建一个新的 XControl
新的 XControl 中有四个 VI。
定义 XControl 的数据类型。比如我们要做一个按钮,数据类型应该是布尔型。如果
要作一个工具条,数据类型就应该是布尔型数组了。
定义 XControl 内部要用到的一些数据,类似于类的私有变量。我们这个简单的例子
用不到任何变量,所以可以不去动它。
类似于类的构造函数。在我们这个简单的例子中也不需要去改变它。
是最主要的 VI,XControl 的外观和行为都是在这个 VI 中定义的。 的
界面就是 XControl 控件的外观。控制控件行为的代码也是放在这个 VI 的程序框图上。
我们要做的是个按钮,所以就在 的前面板上放一个按钮。如果希望用户在使用这个
XControl 时可以调整它的大小,在我们这个简单例子中,只要设置 窗口尺寸属性中的
“在窗口尺寸变化时,按比例调整控件大小”这个选项就可以了。对于复杂的 XControl 控件,要另
写代码,在窗口尺寸变化后重新计算每个控件的大小和位置。
图3:窗口尺寸属性设置
控制按钮颜色的代码也需要放在 中:把前文提到的按钮的 Mouse Enter 和 Mouse
Leave 放在这里即可。具体实现方法,可以参考文章结尾给出的范例程序。
二、有持续运动的 XControl
不能够持续运行,只有在有事件发生时,LabVIEW 才会调用这个 VI。处理完这个
事件, 就会停止运行。不要试图让 持续运行,否则会导致整个 LabVIEW 被
挂起。
有时候,需要控件能够循环地或者持续一段时间地作一个动作。比如说,需要做一个不停闪烁的
小灯。控制灯光闪烁的代码就不能够放在 中。实现这种功能的一个方法是:
把定时控制小灯颜色的代码放在一个可重入 VI 中,通过小灯控件的引用参考来定时更改它的颜
色属性。在 XControl 的 中把这个定时 VI 动态加载并以异步方式运行;在 XControl 的
中再把这个定时 VI 卸载即可。 不是一个必须的 XControl 功能定义 VI
(Ability VI),新建的 XControl 没有这个 VI。可以在工程浏览窗口,鼠标右击这个 XControl 来
为它添加新的功能定义 VI。
范例在这里,它只能在 LabVIEW 8.5 下打开。
XControl 是可以在 VI 的面板上放多个实例的,每个实例小灯的闪烁频率可能不同。我在这个
例子里,每个 XControl 实例都有自己的一个专用定时 VI,因为这些 VI 是可重入的。定时的方法
我采用的是加延时。
我做了一下测试,发现现在的 XControl 有个问题,就是在程序面板上放多个 XControl 实例之
后,定时就变得非常不准确了,小灯闪烁速度明显减慢。这也许是 XControl 的 bug,也许是
LabVIEW 延时函数的问题。解决这个问题的方法就是使用一个定时 VI 控制所有的实例,当然这样
的实现方法会比较麻烦一些。
用 XControl 实现面向组件的编程
XControl 是 LabVIEW 8 中出现的新功能。关于 XControl 功能介绍和实现方法可以参见这个
网址:
/devzone//webmain/1AE46AF02D67AF468625706E006
E577C。
面向组件的编程(Component Oriented Programming ,COP)技术建立在对象技术之上,
它是对象技术的进一步发展。“类”这个概念仍然是组件技术中一个基础的概念,但是组件技术更核心
的概念是“接口”。组件技术的主要目标是复用—粗粒度的复用,组件的核心是接口。
LabVIEW 为我们提供了大量漂亮的控件,可以让我们非常方便地就搭建出一个程序界面来。然
而,对于追求完美的用户而言,LabVIEW 提供的为数有限的控件是远远不够的。比如图1,是
LabVIEW 8.2 的一个新功能—导入导入共享库向导的界面。在它右上方有四个按钮,这四个按钮有
着特殊的外观图标,在 LabVIEW 中并没有直接提供这样的按钮。要拥有这样的按钮,并保存下来
以供再次使用,就只能自己来制作这样一个自定义控件。关于(用户自定义控件可以参考文章《用户
自定义控件中 Control, Type Def. 和 Strict Type Def. 的区别》)
图1:LabVIEW 8.2 中 Import Shared Library 的界面
自定义控件虽然可以定义控件的外观,但无法定义控件的行为。功能复杂一点的控件,.ctl 文件
就爱莫能助了。还是以图1 为例,它的 Include Paths 控件是“一个”功能比较复杂的控件,它比
LabVIEW 自带的列表框多了编辑功能。用户添加或编辑一个路径时,这个控件要为用户在所编辑的
路径项目上,显示出供编辑使用的文本框和浏览路径按钮。
实际上这个编辑功能是由三个 LabVIEW 提供的标准控件合作完成的:一个 Listbox、一个
String 和一个 Button 控件。它们的行为(位置、是否可见,值,等等)是在程序运行时决定的:
当用户选择编辑控件中某一路径时,程序就把 String 和 Button 挪到 Listbox 上需要编辑的那一
项,并遮挡住 Listbox 原本的内容。这样,用户只能在 String 控件内输入内容,或者点击浏览按
钮选择一个路径。编辑完成,程序把 String 控件的值写到 Listbox 上相应的项目中。
我们虽然看不见图1 例子中的程序框图,但是可以想象,上述的一系列操作,如判断 String 和
Button 应当显示的位置;然后挪动它们、把 String 值传给 Listbox;处理用户对他们操作的消息
等等,都会为这个程序添加不少复杂的代码。这些代码应该是与程序的其它部分没有任何直接关系的。
但是如果把它们也写在这个界面 VI 的程序框图上,一方面影响了程序的可读性;另一方面,编程人
员有可能在更改程序其它问题时,一不小心改变了这部分代码,降低了代码的安全性。
从逻辑关系上来看,图1 中上半部分的 Listbox、String、浏览按钮以及右上方四个操作按钮,
合作共同完成一个功能,与它们之外的界面控件没有什么直接关联,所以他们七个应当被作为一个整
体,或者说是一个组件。这个组件需要与程序其它模块之间的接口就只是一个字符串数组—用于输入
或输出一组路径。其它的数据和操作,都应当是组件私有的、外部不可见的。
在 LabVIEW 8 之前,想分离和封装出这样一个组件是非常困难的。因为既然这七个控件都在这
个 VI 的面板上,对它们的操作和相应的代码必须放在这个 VI 的程序框图上,无法与其他代码隔离
开。当然也不是说绝对没有办法,比如你可以使用 sub-panel、动态注册事件等方法,强行地把它
们的代码分隔开。但是这些方法既不简单直观,使用它们又可能会让程序变得更为复杂、难以阅读和
维护。
XControl 的出现终于为这个问题提供了一个比较完美的解决方案。图1 中我们提到的七个应当
划分在同一组件的控件可以被制作成一个 XControl。这个 XControl 的外观就是图1 中上半部分
七个控件组合在一起的样子。XControl 与用户自定义控件相比,它不仅定义了控件的外观,更重要
的是,开发人员可以通过编写 LabVIEW 代码定义 XControl 的行为,这些代码是对外隐藏的。开
发人员还可以定义 XControl 的属性和方法,通过 Property Node 和 Invoke Node 在程序中使
用这些属性和方法。
同样完成选取一组路径这个功能,可以有各种不同的界面。比如各种 C++ 编译器都会提供类似
的功能,但它们外观各不相同。你可以利用 XControl,编写多个外观、行为大相径庭的组件。但是,
只要他们的接口相同—都是一个字符串数组,用户就可以在这些组件内任意互换,选用自己喜欢的组
件,而不需改动程序的任何其它部分!
现在,XControl 仍然不太令人满意的地方是它还不支持用户自定义的事件。
XControl 具有封装的特性。因此我在《利用 LabVIEW 工程库实现面向对象编程》一文中提到,
同样可以使用 XControl 来达到面向对象的编程方法。但是 XControl 不具备继承和多态的特性。
与之相比较,Library 和 LVClass 只能够把程序中的某些功能封装成模块,而涉及到包含界面的模
块,就无能为力了。XControl 则非常适合制作有界面的程序组件。
参考文章:
估算项目工时
一个项目在前期调研的时候就要估计一下项目开发的周期大约有多长。有很多不同的估计方法,
适合不同的项目类型。我平时设计 LabVIEW 编写的应用程序中用到过三种估计方法:代码量度量
(Size-Based Metrics)、工作量估计(Effort Estimation)和专家估计(Wideband Delphi
Estimation)。
代码量度量的估计方法就相当于使用其它文本编程语言时的代码行估计法。一个软件需要多商行
代码、每行代码要花多少时间,是相对来说比较容易统计的。所以代码行估计法是最流行的估计项目
工时的方法之一。LabVIEW 的代码不是按行来计算的,它以节点数为计量单位。
在 LabVIEW 的菜单上选择 Tools->Profile->VI Metrics 就可以调出如下的面板。这个 VI
度量面板可以帮你统计的的 LabVIEW 代码中总共有多少个节点。
图1:VI度量工具
利用代码量度量方法估算工时的具体实施步骤大致如下:
首先,先把项目拆分成小的模块。功能单一的小模块更容易进行准确估算。然后估计实现每个小
模块需要多少 LabVIEW 代码(节点个数)。所有模块节点数的和就是整个项目所需的节点数,每
个节点所需工时是已知的,所以整个项目的工时也就估计出来的。
利用代码量度量方法估算工时,是需要有一些历史经验才行的。比如某种规模的功能模块到底需
要多少节点,只有有过项目经验,统计过,才能心里有谱;写一个节点需要多少时间,对于不同类型
的公司,不同经验的程序员,这一数值都是不同的。自己公司的每节点编程耗时,也只有做过之后才
有数。
如果缺少历史统计数据,可以使用精确度稍差一些的工作量估计法来估算项目工时。工作量估计
法与代码量度量方法是很类似的。首先要把项目拆分成便于估算的小模块。但是,由于不便于对程序
节点数进行估算,就只能评直觉,估计每个模块所需的工时,然后累加出项目总工时。
Wideband Delphi Estimation 方法也可以用于缺少历史统计数据的情况,并且它的结论比工作
量估计法要精确。只是这种方法实时起来比较麻烦,一般只有比较重要的项目,我们才会用此方法。
Wideband Delphi 方法的实时过程大致如下。首先组织一个十人左右的团队,队员是来自不同
部门但与此项目紧密相关的人,比如开发者、文档人员、测试人员等等。
让所有队员各自估计一下项目所需的时间。
把所有人凑到一起,把估计的结果汇总,让大家看看估计值的分布情况。然后由每个队员讲一下
影响自己做估计的因素,包括有利的因素和不利的因素。比如有人会提出,我们的项目与某某项目很
类似,可以借用那个项目中的很多代码。另一个人说,我们的项目用到一个什么非常难掌握的技术,
可能会花费很多时间等等。
等大家讨论过后,再各自重新估计项目的工时。
然后在汇总,在讨论……
经过几轮讨论估计后,大家估计出来的工时的差距就比较小了。取大家的平均值作为最终结果即
可。
下午13:00—17:00
度。全体员工都必须自觉遵守工作时间,实行不定时工作制的员工不必打卡。
3.1.2.2打卡次数:一日两次,即早上上班打卡一次,下午下班打卡一次。
3.1.2.3打卡时间:打卡时间为上班到岗时间和下班离岗时间;
3.1.2.4因公外出不能打卡:因公外出不能打卡应填写《外勤登记表》,注明外出日期、事由、外勤起止时间。因公外出需事先申请,如因特殊情况不能事先申请,应在事毕到岗当日完成申请、
审批手续,否则按旷工处理。因停电、卡钟(工卡)故障未打卡的员工,上班前、下班后要及时到部门考勤员处填写《未打卡补签申请表》,由直接主管签字证明当日的出勤状况,报部门经理、
人力资源部批准后,月底由部门考勤员据此上报考勤。上述情况考勤由各部门或分公司和项目文员协助人力资源部进行管理。
3.1.2.5手工考勤制度
3.1.2.6手工考勤制申请:由于工作性质,员工无法正常打卡(如外围人员、出差),可由各部门提出人员名单,经主管副总批准后,报人力资源部审批备案。
3.1.2.7参与手工考勤的员工,需由其主管部门的部门考勤员(文员)或部门指定人员进行考勤管理,并于每月26日前向人力资源部递交考勤报表。
3.1.2.8参与手工考勤的员工如有请假情况发生,应遵守相关请、休假制度,如实填报相关表单。
3.1.2.9 外派员工在外派工作期间的考勤,需在外派公司打卡记录;如遇中途出差,持出差证明,出差期间的考勤在出差地所在公司打卡记录;
3.2加班管理
3.2.1定义
加班是指员工在节假日或公司规定的休息日仍照常工作的情况。
A.现场管理人员和劳务人员的加班应严格控制,各部门应按月工时标准,合理安排工作班次。部门经理要严格审批员工排班表,保证员工有效工时达到要求。凡是达到月工时标准的,应扣减
员工本人的存休或工资;对超出月工时标准的,应说明理由,报主管副总和人力资源部审批。
B.因员工月薪工资中的补贴已包括延时工作补贴,所以延时工作在4小时(不含)以下的,不再另计加班工资。因工作需要,一般员工延时工作4小时至8小时可申报加班半天,超过8小
时可申报加班1天。对主管(含)以上管理人员,一般情况下延时工作不计加班,因特殊情况经总经理以上领导批准的延时工作,可按以上标准计加班。
3.2.2.2员工加班应提前申请,事先填写《加班申请表》,因无法确定加班工时的,应在本次加班完成后3个工作日内补填《加班申请表》。《加班申请表》经部门经理同意,主管副总经理审核
报总经理批准后有效。《加班申请表》必须事前当月内上报有效,如遇特殊情况,也必须在一周内上报至总经理批准。如未履行上述程序,视为乙方自愿加班。
3.2.2.3员工加班,也应按规定打卡,没有打卡记录的加班,公司不予承认;有打卡记录但无公司总经理批准的加班,公司不予承认加班。
3.2.2.4原则上,参加公司组织的各种培训、集体活动不计加班。
3.2.2.5加班工资的补偿:员工在排班休息日的加班,可以以倒休形式安排补休。原则上,员工加班以倒休形式补休的,公司将根据工作需要统一安排在春节前后补休。加班可按1:1的比例冲
抵病、事假。
3.2.3加班的申请、审批、确认流程
3.2.3.1《加班申请表》在各部门文员处领取,加班统计周期为上月26日至本月25日。
3.2.3.2员工加班也要按规定打卡,没有打卡记录的加班,公司不予承认。各部门的考勤员(文员)负责《加班申请表》的保管及加班申报。员工加班应提前申请,事先填写《加班申请表》加班
前到部门考勤员(文员)处领取《加班申请表》,《加班申请表》经项目管理中心或部门经理同意,主管副总审核,总经理签字批准后有效。填写并履行完审批手续后交由部门考勤员(文员)保
管。
3.2.3.3部门考勤员(文员)负责检查、复核确认考勤记录的真实有效性并在每月27日汇总交人力资源部,逾期未交的加班记录公司不予承认。
从群体上看,中专 毕业生的劣势是阅历较少、知识层次相对不高;优势是学校专业设置大多
贴近市场实际、贴近一线需要,且中专毕业生年青、肯吃苦、可塑性强。从个体来说,每位
毕业生的优势与长项又各不相同,如有相当一部分毕业生动手操作能力较好;有些学生非常
上进,上学期间还同时参加了职业资格考试或自学考试。所以,在实事求是,不弄虚作假的
前提下,要特别注意扬长避短,从而在竞争中取得优势,打动聘任者。没有重点和章法的写
作易使文章显得头绪不清、条理紊乱。
非常热爱市场销售
工作,有着十分饱满的创业激情。在××××两年从事现磨现煮的咖啡市场销售工作中积累了
大量的实践经验和客户资源。与省内主要的二百多家咖啡店铺经销商建立了十分密切的联
系,并在行业中拥有广泛的业务关系。在去年某省的咖啡博览会上为公司首次签定了海外的
定单。能团结自己的同事一起取得优异的销售业绩。
合理分配自我介绍的时间前文说过,自我介绍一般也就持续1—3分钟,所以应聘者得合理
分配时间。常规安排是:第一段用于表述个人基本情况,中段重点谈自己的工作经历或社会
实践经验,最后展望下自己的职位理想。但如果自我介绍被要求在1分钟完成,应聘者就
要有所侧重,突出最有料的一点。在实践中,有些应聘者试图在短短的时间内吐露自己的全
部经历,而有些应聘者则是三言两语就完成了自我介绍,这些都是不明智的做法。
突出和应聘职位相关的信息自我介绍的内容不宜太多的停留在诸如姓名、教育经历等部分
上,因为面试官可以在应聘者的简历上一目了然地看到这些内容。应聘者应该在自我介绍时
选择一至两项跟自己所应聘的职位相关的经历和成绩作简述,以证明自己确实有能力胜任所
应聘的工作职位。一个让人更有机会在面试中出彩的方法是在做一段自我介绍后适当停顿。
比如在“我曾在大学期间组织过有2000人参与的大型校园活动”之后的停顿可能会引导面试
官去问“那是什么样的活动呢?”,这样做的目的是为面试的深入打下基础。
一切以事实说话在证明自己确实有能力胜任所 应聘的工作职位时,应聘者可以
使用一些小技巧,如介绍自己做过的项目或参与过的活动来验证某种能力,也可以适当地引
用老师、同学、同事等第三方的言论来支持自己的描述。而这一切的前提是以事实为基础,
因为自吹自擂一般是很难逃过面试官的眼睛的,一旦被发现掺假,基本预示着应聘者将被无
情“秒杀”。2×××年5月—至今: 担任某咖啡茶品配送服务部的市场部业务员。主要负责
与经销商签定经销合同、办理产品的包装、运输、保险、货款结算、售后产品跟踪、市场反
馈以及开拓新的销售渠道等。负责公司新业务员的培训,在实际工作中具体指导和协调业务
员的销售工作,并多次受到公司的表扬。


发布评论