采集、处理、显示
程序结构的划分
对于规模不是非常大的 LabVIEW 程序,不论是分析一个程序,还是设计一个程序,一般都是从总体到细节,也就是从高到低地分析和设计。如果一开始就研究细节,可能效率会比较低下。
在设计 程序时,编程者会首先在纵向上把程序划分成若干个层次。然后,从程序的最高层起,考虑如何按照程序功能将最高层分为几个部分,以及每一部分之间的关系。再从最高层的每一部分开始,考虑下一层次,并按照功能把下一层次划分成更细致的功能模块。一层接一层,依此类推。
在划分程序的层次时,可以按照程序的大小和复杂度,把程序划分成不同的层次。最简单的程序或许只有一个层次,一个 VI 即可完成;稍微复杂的程序,可以包含两层,由一个主 VI 和数个子 VI 构成;更复杂的程序,层次可以更多一些。
简单程序的层次结构可以在 VI 层次结构中体现出来(参见使用子 VI)。但,对于大型程序而言,就不再适合使用 VI 层次结构图来分析程序了。如下图中的程序,满屏显示也只能看到其中一小部分 VI 的层次结构。对于类似的大型程序,应当采用更抽象的程序层次划分方法。

测试测量程序的一个比较常见的层次划分方法是把程序划分为三个层次。
最上层是主 VI,通常被称为 视图层 (View) 或 界面层。它负责展示数据以及接收用户的操作指令。为了保证界面的响应速度,这一层通常不包含复杂的业务逻辑。
其下一层是 功能逻辑层 (或控制器 Controller / 模型 Model 层)。一个测试程序常由以下几个核心业务逻辑组成:数据分析处理、数据存储、状态机调度等。本章将会详细讨论这几个功能的设计和实现。
最底层被称为 硬件抽象层 (HAL, Hardware Abstraction Layer) 和驱动层。程序各功能通过调用底层 的 API 来完成与实际硬件或操作系统的交互(如数据采集卡驱动、文件 I/O、串口通信等)。引入硬件抽象层的好处是,当未来需要更换不同型号的采集设备时,只需修改驱动层代码,而功能层和界面层无需做任何改动。
主 VI 在调用几个功能模块时,会根据不同的程序要求,采用不同的程序结构。下面介绍几种测试程序常用的结构模型。
普通循环模型
一个简单的测试程序的过程可以简化为数据的采集、处理、显示、保存。其主 VI 程序框图模型如下图所示。
大多数程序并非只需运行一次,而是要不断地持续采集数据,然后处理、显示、保存这些数据。这样的程序模型如下图所示,即在上述模型的基础上再增加一个循环。
上图程序中的最后一个子 VI 是用来判断试验是否结束、是否需要进行下一次循环的。在这个模型中,各个程序模块是单线程顺序执行的。它的优点是程序逻辑简单,容易设计和理解。
但是,此模型中每个功能模块之间存在着严格的顺序依赖关系。计算机必须等待文件保存完毕后,才能进入下一次循环去读取采集卡的数据。
这种单线程架构在实时数据采集中具有致命的缺陷:极易导致数据丢失。文件 I/O 或复杂的数学运算耗时往往是不确定的,如果某次循环中“保存”或“处理”耗费了过长的时间,导致软件读取数据的速度跟不上硬件采集的速度,底层数据采集设备的硬件缓冲区(FIFO)就会溢出,从而导致程序报错并丢失关键数据。因此,这种模型仅适用于对实时性要求极低、低速或单点采集的简单场景。
管道流水线模型
改进效率的方法之一是同时运行这几个功能。当然,对于单个的数据来说,还是需要先采集、然后处理、之后再显示和保存的。所以,同时运行这几个功能并不是对同一数据同时运行这几个功能。而是程序在采集新数据的同时,处理上一次循环迭代采集到的数据,同时,显示和存储上一次循环迭代处理好的数据。这有点类似于一个需要多道工序产品的生产作业流水线。
流水线在一定程度上提高了程序的整体吞吐量。在 LabVIEW 中,这通常是通过在循环中使用移位寄存器 (Shift Registers) 来实现的,将前一道工序(如采集)的输出暂存,在下一次循环迭代时再交给下一道工序(如处理)执行。
需要注意的是,流水线模型虽然提高了处理频率,但增加了数据的延迟 (Latency)。例如,在第 N 次循环采集到的数据,可能要在第 N+1 次循环才被处理,在第 N+2 次循环才被显示。此外,流水线模型的循环周期依然受限于各道工序中最慢的那一个步骤。
实际程序运行过程中,受数据传输线路的影响,数据被采集进电脑的速度可能时快时慢。而受计算机中运行的其它程序的影响,计算机对数据的处理过程也可能时快时慢。流水线的速度总是取决于各工序中最慢的那个,如果能使用一个缓存,在采集数据较快,处理数据较慢的时候,把数据先存下来,等数据采集速度减慢或处理速度变快时再对缓存中的数据进行处理,程序的总体效率就可以进一步提高。
生产者消费者模型
按照上文所提的思路,对采集到的数据做一缓存,其程序模型如下图所示。
这里采用队列作为缓存,也可以使用其它方式如数组作为数据缓存。新采集到的数据直接被放到队列中,这一简单过程耗时极少。也就是说,数据采集多快,就可以以多快的速度存放数据到缓存。程序的另一部分则不停地从队列中取出数据,对其进行处理。如果需要,还可以把这个模型中的数据显示和保存部分也放到另一单独的循环中去运行。
在跨越两个并行循环(线程)传递数据时,绝对不能使用普通的数组、局部变量或全局变量作为数据缓存。普通数组缺乏线程同步与锁机制,极易引发“竞态条件”(数据被覆盖丢失或被重复读取);同时,在循环中不断动态增加数组长度会导致严重的内存重分配开销,迅速耗尽系统资源。
这一模型也被称为 "生产者 - 消费者模型"。采用基于队列的“生产者 - 消费者模型”,新采集到的数据被安全地放入队列(入队),这一操作耗时极短,确保了数据采集循环(生产者)能够全速运行,绝不会因为处理速度慢而丢失数据。同时,处理循环(消费者)在队列另一端等待(出队),当队列中有数据时迅速处理,队列为空时则自动休眠释放 CPU 资源,完美解决了不同模块间执行速率不匹配的难题。
在 LabVIEW 的新建对话框中可以找到这一模型的模板。它初看起来都比较复杂。但是理解了这类程序中两个主要循环的用处,再分析程序就比较容易了。这个模型的实际应用程序会更加复杂,与管道流水线模型相比较难理解和维护。