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 的调试环境
断点和探针
其它常用调试工具和方法
我和 LabVIEW

记录梦境 4

    上午工作时,不经意间线右侧的窗外望了一下,一部手臂长长的吊车映入眼帘。这让我突然想起的昨晚的梦,梦中也有一部吊车。我看着它晃来晃去,突然就坍塌了。关于这个梦就只能想起这么一点了。

    昨晚我还是只盖了一条毛毯,但是天气已经明显比前几天转凉了,所以昨晚的梦中我也被冻的瑟瑟发抖。我梦见和朋友们一起吃饭,旁边一个人穿了件毛衣,看见我还是穿的短袖T桖,就问我冷不冷。我于是打着哆嗦说:“我也没想到,咋就突然来寒流了呢?”。

    别人写日记,都是记录白天发生的事;我是专门写梦记。

《生活随笔》

 编辑

 

人的脑袋还是很结实的

    昨晚和同事一起打羽毛球,是双打。我同伙拼劲全力一记扣杀,结果直接把球拍拍到了我的前额上。他的球拍当场断裂,而我的头也出了点血。

    虽然头上肿起一个大包,却也没影响我工作。我现在摸着我的脑袋想,我的头骨还真结实啊,否则裂开的就不是球拍,而是我的脑袋了。赶紧吃点钙片,骨头结实是很重要的 :)

《管理自己》读后感

    这篇《管理自己》是我在《商业评论》期刊上读到的一篇文章,它节选自彼得•德鲁克(Peter F. Drucker)的《21世纪的管理挑战》一书。

    以前我对彼得•德鲁克的了解是,他是一位写书高手。有一次我逛书店,发现在经管类书籍区,有整个一个书架上的书都是他写的。这么多的书我是没兴趣去看的,因为有一位浏览过他的书的同事讲过,他的书籍太深奥了,看不懂。也许像我这样没有多少管理经验的人,并不适合花费大量时间去研究他的书,但是阅读这样一篇短文不会耽误太多时间,况且这篇《管理自己》确实给了我很大的启发。

    是《管理自己》前言中的一句话吸引我读完了它的全文。它写道:“今天的公司并不怎么管员工的职业发展;实际上,只有工作者必须成为自己的首席执行官”。尽管很多企业都宣传自己如何以人为本,如何关心自己员工的成长,但真正能对一个人的前途负责的,只有他自己。如果你所在的企业不错,你在有了自己的职业目标后,企业可能会给你提供一些帮助。但无论如何,自己今后的道路要自己决定自己走,企业不可能替你考虑安排妥当的。

    我刚刚开始参加工作时,实际上并没有确立一个明确的目标,要几年以后如何如何。但是到了现在,就不能不考虑这个问题了。因为随着年龄的增长,危机感也越来越强了。
    我现在已经开始认真考虑自己未来的职业发展道路了,但是至今还不能下定决心,对是否应该朝一个方向去努力还在犹豫。因为可以预料到,我今后几年还会有一些不确定的因素,所以就同时考虑了几条道路。当然有些想法现在还无法公开。

    有两个职业选择是我自己也一直下不了决心的:将来是从事技术,还是从事管理。
    如果单纯从自身的优势考虑,我应当做技术。比如按照《管理自己》中的几个问题来考虑:
    我的长处是什么? 有耐心、承受挫折的能力强、不轻易放弃,比较善于听取别人的想法,善于思考,好奇。
    我属于读者型还是听者型? 听者型。
    我如何学习? 自己试验、研究、推理,不善记忆、不喜欢重复已经知道的东西。
    我的价值观是什么? 这点我自己很清楚,尽管没有写在这里。
    我属于何处? 这正是我在考虑和犹豫不决的问题。
    我该做出什么贡献? 我希望可以创造发明出一些对社会对大众有益的东西。

    看了这么一堆答案,我有什么理由不做技术呢?唯一让我担心的就是现在所处的工作环境,从事技术的难度要远远大于从事管理。
从事技术工作,相对来说,两极分化大一些:要么是工程师,在技术行业最底层做苦力;要么是某一领域专家,吃香喝辣。如果我不能做到专家,就只能当苦力,可供选择的中间层次并不多。管理人员就不一样了,高管不好做,基层干部还是比较容易熬到的,运气稍好一点,就可以到中层了。
    我是在外企工作的,外企到中国开研发部历史还不长,真正的核心技术挪到中国的并不多。如果一直接触不到核心技术,想成为专家也很难。
    狭隘意义上的技术并不是每个人所必须的,不成为专家,也有很多其它方法让自己生活过的很好。但管理知识是必须的,每个人至少要学会管理一个人,就是自己,否则无论做什么都不可能成功。反正管理知识是一定要学的,还不如专心学管理。

《生活随笔》

编辑

记录梦境 3

    昨晚做了个梦,因为特别清晰,所以记录下来。在梦里我指挥着几个弟兄围堵恐怖分子。为了减少伤亡,我发明出了一种罩子,罩在每个人身上,就刀枪不入了。
    梦里的罩子是一口钟的形状,大概受了“金钟罩”的启发:)。现实中的士兵不会顶着大钟上战场的,不过不是有防弹衣嘛。穿上防弹衣应该大大降低伤亡的,可是电影里的战争场面好像很少有人穿避弹衣的。不知现在的解放军是否配备有。

    前天,国庆假期还没结束就跑到公司来了。打开信箱一看,满屏都是红颜色,有几百封未读邮件。赶紧又把信箱给关了。今天正式上班才开始看信,等把所有的信看一边,该回的回掉,基本上一天也过去了。

    《生活随笔》

编辑

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 程序中的线程 1 – LabVIEW 是自动多线程语言
    LabVIEW 程序中的线程 2 – LabVIEW 的执行系统
    我和 LabVIEW

编辑

昨天看了《夜宴》

    我看之前,根本不知道故事情节。但网上对它的评论级差,不过我看了之后觉得还行,比评论的好。

    这部片还是有很多出彩的地方的:武士的服饰很威武;章子怡在这部戏的打扮比以前漂亮;张靓颖的结尾曲唱得很好听。

    让我不满意的地方是:武打动作太差了,既不实用也不美观;太过血腥和暴力;开始几个蒙面艺人被杀的时候还在摆造型,他们全部都是神经病?厉帝莫名其妙就自杀了,不符合一个弑兄篡位者的逻辑;皇后最后被杀设计的不好,有悬念是可以的,但是按正常推理,皇后不会是被这部戏中的主要角色杀死的,这样的悬念就没有意思了。

    《生活随笔》

编辑

在 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:http://decibel.ni.com/content/docs/DOC-1274


图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 在运行时调入内存,这个过程是比较耗时的。因此递归结构的运行效率远不如可实现相同功能的循环结构,内存占用也会更大一些。决定使用递归结构之前要考虑到这些因素。

下载示例:
   
利用递归结构计算阶乘

相关文章:
   
我和 LabVIEW
    LabVIEW 程序的内存优化
    如何使用 VI 的重入属性(Reentrant)
    LabVIEW 的运行效率 2 – 程序慢在哪里