本文原发表于水源社区(原饮水思源BBS),意在抱怨通信原理仿真实验既读不懂题目又搞不定软件。但若看官不急着离开,不妨来了解一下这门世上仅存的图形化程序设计语言。
LabVIEW已经在我这里崩溃几十次了,是时候写篇教程了。
… The frustrating one was seeing the extra difficulty people with prior programming experience had in learning LabVIEW. They seemed to be so oriented to sequential programming they had a hard time grasping how to design a program using data-flow. The most egregious example we saw was a VI containing a single sequence structure with dozens of frames, each corresponding to a statement in a sequential program. It was disheartening, particularly when accompanied by a comment that LabVIEW was hard to use.
糟心的是,有编程经验的人反倒更难学会LabVIEW,貌似是因为顺序编程的思想已根深蒂固,用数据流来编就懵了。我们见过一个最极端的,整个VI里就只有一个顺序结构,里面有好多帧,一帧对应一句语句。我们对此心痛不已——更何况旁边还有个注释框,说LabVIEW太难用了。
— Jeffrey Kodosky, LabVIEW, DOI 10.1145/3386328(文中引用皆出自此,中文由我翻译,下略)
C、C++、C#、Java、JS、VB、Ruby、MATLAB…… | G语言 |
---|---|
不可变(immutable)临时变量 | 连线(wire)(又名信号(signal)) |
struct { size_t 长度; char []; } // 8-bit clean | 字符串(只有本地字符集) |
结构体(struct)、元组(tuple) | 簇(cluster) |
枚举(enum) | 枚举(enum) |
数组(array)、向量(vector)、列表(list) | 数组(array) |
指针(pointer)、引用(reference) | 引用(reference) |
string [] | 路径(path) |
struct { bool status; int code; string source; } | 错误簇(error cluster) |
struct { time_t t0; double dt, Y[]; … } | 波形(waveform) |
struct { time_t t0; double dt; 数字数据 Y; … } | 数字波形(digital waveform) |
struct { uint32_t transitions[]; uint8_t data[][]; } | 数字数据(digital data) |
波形 [] | 动态数据(dynamic data) |
for (i = 0; i < N; i++) { … } | For循环 |
do { … } while (…) | While循环 |
在循环内操作的临时变量 | 移位寄存器(shift register) |
case … when … else … end | 条件结构(case structure) |
#if 0 … #else … #endif | 程序框图禁用结构 |
#if … #elif … #else … #endif | 条件禁用结构 |
a[i].x += b 以代 a[i].x = a[i].x + b | 元素同址操作结构 |
函数(function) | 虚拟仪器(VI) |
函数参数表 | 前面板(类型)、连线板(顺序) |
函数,带调用栈(call stack) | 可重入(reentrant)VI |
标准库函数 | 内置VI |
用户定义函数 | 子VI(subVI) |
函数中的静态(static)变量 | 未赋初值的移位寄存器 |
操作静态变量的getter和setter函数 | 功能全局变量 |
类型定义(typedef) | 自定义类型(一种只有前面板的VI) |
自定义控件 | 严格自定义类型 |
线程间通信 | 通道(channel) |
互斥锁(mutex) | 信号量(semaphore) |
函数重载 | 多态VI(polyVI) |
用于实现多态的宏 | 自适应(malleable)VI |
向导(wizard)令简单的事情更简单 | Express VI |
本文没有覆盖以上全部内容。
删除并重连
To do for test and measurement what the spreadsheet did for financial analysis.
金融分析界有电子表格。测量测试界,也要有。
— LabVIEW的口号
LabVIEW Laboratory Virtual Instrumentation Engineering Workbench,实验室虚拟仪器工程工作台,缩写为LV。基础版不配有信号处理功能,专业版“最低仅售”毛五万块钱的屑软件。你不能指望一个1986年首发的产品有多么现代的外观——当然,功能也是。最早的LabVIEW不要说撤销,就连移动节点都得先把周遭的连线擦干净。整整12年后,那是LabVIEW的第5个版本,NIWeek上的演示人员轻点撤销按钮还原误删的内容时,全场都沸腾了。直到现在,功能也还在日々进化中。
G语言 LabVIEW的图形化编程语言。数据流驱动,无环路,自动并行,由LLVM编译,跨平台(write once, debug anywhere?),二进制源码文件,大端序……使用过着色器节点编辑器(shader node editor)的用户应该感到熟悉——看看这一模一样的“删除并重连”菜单:
在截上面两张图的时候,Blender和LabVIEW各崩溃了两次,绷不住了
即时帮助 勾上〖帮助(H) | 显示即时帮助〗或小小的工具栏尾端大大的疑惑,这是集成开发环境为缺少独立编辑器的补偿。常朝那看看,这些都不用背下来。
自动格式化 选择〖编辑(E) | 整理程序框图〗或工具栏中的扫地机器人,这是集成开发环境为不加空格人士的馈赠。它会整理,也时常搞砸,索性一开始就画得更乱,来获取自动整理的快感。
数据流 每根线代表一个数据,连接一个上游驱动源头和多个下游数据接收者,数据单向流动且流向固定。传值不传引用,数组也如此,对象也如此。你现在应该知道为什么有时候操作大数组的LabVIEW程序很慢了。
节点 虚拟仪器(virtual instrument,VI)对应于传统编程语言中的函数。一个函数定义就是一个VI文件,一个函数调用就是一个节点。表现为有输入端口和输出端口可以接线的小元件,输入在左,输出在右。所有输入端口的数据流到来后开始计算,没有输入端口就立即开始计算。输入端口名称里的括号代表默认值,输出端口也可以悬空。要悬空就悬空,接根断线就报错。
自动并行 左上图的三个与门哪个先运行?不知道。但是或门一定在与门之后运行,因为两者数据流送达有先后。(〖与〗是∧,〖或〗是∨。)有多个处理器核心的话,三个与门有可能同时运行。要是你在程序里放两个弹窗的节点,数据流又同时抵达,哪个弹窗在上面可就不知道喽,这可跟程序框图里谁摆在上面没有半点关系。
顺序结构 胶片是时间的缩影,拟物是GUI的性情。数据流是顺序的定义,方框是LabVIEW的新意。这便是本文初那个遭滥用而让NI创始人心碎的结构。
上图解
而我又确定了谁?是谁确定了我?(或者相反) 由于数据流抵达先后不确定,弹窗顺序也不确定。我测试的时候确实出现了两种弹窗顺序,且两对话框共同出现在屏幕上。
是我确定了我!回答正确! 顺序结构使第一张胶片内完全执行完毕后才开始执行第二张胶片内的程序。
回答正确!动鼠标吧! 胶片运行完毕后,传出的数据才被认为可用,从而控制了胶片内外的执行顺序。因此,只有一张胶片的顺序结构也是有用的。
动鼠标吧!*蜂鸣* 虽然弹窗内容固定,但程序流程看上去好像跟前面输出端的结果有依赖关系,从而产生了先后。
何者混沌何者明,请君自行辨清晰。程序风格无绝对,达意才是真目的。
数据类型
我不知是否值得 LabVIEW最初的设计仅有扩展精度浮点数和其衍生的数组、用于控制循环条件的布尔。随后才加入了字符串、簇。为了优化数组的性能,又加入了不同位宽的整数和不同精度的浮点数。为了跨平台和文件系统,加入了路径类型。为了动态调用和修改程序,加入了引用类型。加入了64位整数。加入了矩阵。加入了定点数。加入了类和对象。加入了……
Make difficult tasks possible before making simple tasks trivial.
与其先把简单的事情做得再简单点,不如先去提升可能性的上限。
— LabVIEW设计原则之一
整数“U8”“U16”“U32”“U64”“I8”“I16”“I32”“I64” 8位、16位、32位、64位;有符号和无符号。熟悉的同时也暗示了运算溢出不报错的底层定时炸弹。在〖表示法〗菜单中可以修改常量和控件的位宽,并在整数、浮点数、复数之间转换。
枚举“◀▶” 会检查case语句覆盖情况,但其他地方的值超出范围还是不太容易报错。可以选择内部使用的整数位宽。
浮点数“SGL”“DBL”“EXT” IEEE-754。你知道这意味着1.0 ÷ (−0.0) = −∞,0.1 + 0.2 ≠ 0.3,9999999999999999 = 10000000000000000,NaN ≠ NaN。
复数“CSG”“CDB”“CXT” 实部和虚部,常见的策略。
布尔“TF” #define bool unsigned char;#define true 1;#define false 0。非零表真。
时间标识“⌛︎” 64.64的定点数,格林威治时间1904年1月1日星期五00:00起的秒数,也可记录相对值。
数组“[ ]” 下标从零开始。维数是修饰数据类型的一个属性,因此没有数组的数组,只有二维数组。方括号的粗细和连线的根数代表数组的维数。其实字符串“abc”(8位无符号整数数组)、路径“゜╲。”(字符串数组)、矩阵“[DBL]”“[CDB]”(二维双精度浮点数/复数数组)、动态数据“⧜”(波形数组)都有数组的意思,但系统不认为它们是数组。
簇“▭̠·̄▯” 结构体,但除了按字段名,也可以按顺序存取元素,用〖解除捆绑〗和〖按名称解除捆绑〗打开。数值簇为咖啡色,其他簇为洋红色。
错误簇“?!” struct { bool status; int32_t code; string source; }。能在氛围合适的情况下(条件结构、条件接线端等)自动转换为布尔,有错为真。注意status字段显示为✓✗时,✗代表有错的真。
波形“∿” struct { time_t t0; double dt; double Y[]; void *attributes; },但必须用〖获取波形成分〗打开。Y字段也可以是复数。
数字波形“⎍” struct { time_t t0; double dt; 数字数据 Y; void *attributes; },同样必须用〖获取波形成分〗打开。
数字数据“0101” struct { uint32_t transitions[]; uint8_t data[][]; },同样必须用〖获取波形成分〗打开。(〖获取数字波形成分〗〖获取数字数据成分〗和〖获取波形数据〗是同一个东西。)稀疏编码,transitions记录data对应项的跳变时间,data中的元素是enum : uint8_t { 0, 1, Z, L, H, X, T, V },见〖数字下拉列表常量〗。看上去绿绿的,结果跟布尔型毫不沾边。
剩下的用到再查也不迟
定点“FXP” _Fract,_Accum,_Sat。今天N1169也没加进C标准,LabVIEW十年前就有更好的了,小数点前后位数都可以自定义。
图片“△▩” 图元(primitive)的集合“图元文件(metafile)”,本质上是二进制字节数组数据,但是打不开,只能最终转换为像素。
I/O名称“I/O” 用于DAQ、VISA、IVI。
变体“‑□‑” 说到变体,果然还是Visual Basic吧。LabVIEW中的变体是用于泛型的void *
,但是要手工插入转换函数且要已知类型,相比自适应VI来说不太好用。
对象“OBJ” java.lang.Object
,但是字符串和数值一样不属于对象,数组、簇、波形……都不是对象。记住连线只传值,对象也如此,所以隆重介绍下面的类型——
引用“◰” 有了引用(refnum)就可以调用对象的方法,修改对象的属性。〖属性节点〗和〖调用节点〗接收引用,〖VI服务器引用〗可给出控件的引用。按捺不住怀旧的心情,早年见人以Visual Basic所作,今用LabVIEW再实现。
Private Sub Command1_MouseMove(Button As Integer, Shift As Integer, _ X As Single, Y As Single) ' 来抓我呀! Command1.Left = (Form1.Width - Command1.Width) * Rnd Command1.Top = (Form1.Height - Command1.Height) * Rnd End Sub
强制转换点 1 + 1.0 = 2.0,1UL + 1ULL = 1ULL,隐式转换显示为红点。我们的目标是没有红点。如果常量来源于别处,记得检查表示法,默认的32位有符号整数可能就是红点的原因。
类型转换 〖编程 | 数值 | 转换〗中有各种数值转换函数,还包括一些布尔、时间标识、字符串的转换函数。〖字符串〗〖数组〗〖定时〗〖簇、类与变体〗〖编程 | 波形 | 数字波形 | 数字转换〗等地散落着各式转换函数。也可以搜索“转换”来找到更多不明所以的转换函数。因为需要时常常找不到,先过目一遍留个印象也好。
强制转换至类型和强制转换 〖强制转换至类型〗(coerce to type)是static_cast
,〖强制转换〗(type cast)是bit_cast
。类型接线端仅用于类型推断。由于各种数值类型都有专门的转换函数,一般没有使用这两种转换的必要。题外话:*(类型 *) &x
可造成指针别名,产生未定义行为,详细信息请参考C/C++标准及相关文章。
控制结构
杰弗里有言 测量要求多组重复。数据流图单向传输,循环难画出:沿用反馈画法多环路,执行顺序不清楚,陡增阅读复杂度。一日如有灌顶醍醐:若代环路以框符,不便之处可尽除!框外作节点,框里有框图。左右寄存器成双,相同数据相对望。以此所得结构皆有方,三十年未变,如科罗拉多河上的月光。
← 1986 2018 →
控制结构 G语言与一般数据流图最与众不同的地方就是用方框而非环路来表示循环,融合了数据流图描述数据流的直观和传统编程语言描述程序流程的结构性。
计数循环 〖For循环〗不是C中的通用for循环,而是固定格式的for (i = 0; i < N; i++),无法修改计数的初值和增量。纸张层叠,仿佛多页同行。
条件循环 〖While循环〗不是C中的while循环,而是do ~ while循环,循环体至少执行一次。箭头环绕,指明两轮循环间移位寄存器的数据流向。
条件接线端 不是break语句,不会跳过其后(何来“后”?)程序,循环终止条件在一趟循环末结算。若要跳过,自行添加条件结构。
条件结构 根据输入值的匹配情况来决定执行何分支。无论如何都只会执行一个分支。无法一次性显示全部分支。因此,尽可能用其他方式代替,例如〖编程 | 比较 | 选择〗。但是分支条件式太好用了。
d = (case month when 1, 3, 5, 7..8, 10, 12 31 when 4, 6, 9, 11 30 when 2 year % 4 == 0 && year % 100 != 0 || year % 400 == 0 ? 29 : 28 else raise ArgumentError.new "invalid month" end)
隧道(最终值) 隧道是连接框内外的桥梁。从外面看,控制结构就是普通的节点,隧道就是输入和输出接线端,遵从通常的数据流规则。
上图解
“死循环”节点外面的按钮值作为条件循环节点的输入,在循环开始前计算完成,并在循环内部为常数。
自动索引隧道 多数函数支持向量化,但如果必要,计数循环自动插入自动索引隧道使Array#map很方便。计数循环次数是N和所有自动索引隧道的输入数组长度之中的最小值,所以使用自动索引隧道的时候就不要给N连线了。条件循环同样支持自动索引隧道(在隧道上右击),但输入数组长度不影响终止条件,下标越界得零。
自动连接隧道 自动索引隧道用Array#push,自动连接隧道用Array#concat。
计数条件循环 带条件的for循环,依旧以数组长度为最大循环次数。
移位寄存器 在循环外赋初值,在循环间传递数据,最终传出数据。除此之外,还可代替循环隧道。循环零次的计数循环,因其内部框图从未执行,输出隧道完全无视输入值。若用移位寄存器代之,则无问题。
静态变量的getter & setter
double get_or_set(enum {GET, SET} operation, double input) { static double value ; do { if (operation == SET) value = input ;} while (0) ; return value ;}
不赋初值 这是故意的设计。没有初值的移位寄存器,将不被重新初始化,值在运行期间永久保留,宛如私有全局变量。
自动串行 每个函数定义时可分别设置是否可重入(reentrant),在〖文件(F) | VI属性(I)〗中修改。默认不可,同一时刻只能有一个该函数执行。
功能全局变量 functional global variable,简称FGV;又名action engine,简称AE。因为移位寄存器可以不赋初值,VI可以不可重入,两个听上去十分离谱的特性造就了线程安全的全局变量编程范式。
错误处理
用一个整数表示任何可能发生的错误,其结果是 :( 你的你的电脑遇到问题,需要重新启动。错误代码:0x80000000
错误代码 在一些包装简陋的函数中仍存在。〖帮助(H) | 解释错误(X)…〗是此时的ERRLOOK.EXE。
错误输入 内置的可能抛出错误的函数大都带有这两个接线端。每个这样的函数内部都是这样连的,一旦前面有错进来,就会尽全力把错误送出去:
错误输出 悬空的错误输出接线端如果有错,则会自动报错。这可在〖文件(F) | VI属性(I)〗中〖启用自动错误处理〗设置消音。
图形用户界面
下拉列表和枚举 下拉列表(ring)如同一系列常量定义,不创造新数据类型;枚举(enum)创造新的枚举类型。下拉列表支持值为浮点数,枚举仅支持8位、16位、32位无符号整数。
波形图和波形图表 波形图(waveform graph)显示数组,波形图表(waveform chart)会滚动。
颜色 就是32位无符号整数,0x00RRGGBB,大端序如你所见;0x01000000是透明;没有α通道。
NXG next generation的缩写。
念念不忘打字编程
公式节点 逃课写法,写类似C的代码。真的只是类似C,还是89版的,没有逗号表达式,数据类型名和C标准不一样(float32/64和(u)int8/16/32,没有_t后缀,不支持复数),无法创建变长数组,也没有指针和引用,但是有//注释、**幂运算符、0b二进制字面量、对数组下标越界的容忍(读得零,写无视)、声明时赋初值,但又不能给数组赋初值。这么说来,或许用G语言直接画还比用公式节点好受些。
表达式节点 单输入和单输出的公式节点。〖角度转换至弧度〗看上去是一个常规子VI的样子,拖出来就变成表达式节点了。
MathScript 逃课写法,写类似MATLAB的脚本。但更可能的是LabVIEW的函数面板中未出现MathScript节点。
难检函数索引
传统名称 | 本地化名称和注释 |
---|---|
… ? … : … 三目运算符 | 选择 |
fprintf、sprintf | 格式化写入文件、格式化写入字符串 ——带格式符扩展,如 %b |
fscanf、sscanf | 扫描文件、扫描字符串 |
strftime | 格式化日期/时间字符串 |
RegExp#match | 匹配模式、匹配正则表达式 |
String#split | 电子表格字符串至数组转换 ——不支持空字符串参数 |
Array#join | 数组至电子表格字符串转换 ——不支持空字符串参数,会添加多余换行符 |
Array#join(“”) | 连接字符串 |
a ≤ x < b、clamp、min(max(a, x), b) | 判定范围并强制转换 |
lerp%、(1 − t%)a + t%b | PID百分比转为工程单位 |
逆lerp%、100(x − a)/(b − a) | PID工程单位转为百分比 |
A sin(2πft + φ°) | 正弦波(逐点) |
ax + b | 一元线性函数求值(逐点)、线性函数求值 |
anxn + ⋯ + a1x + a0 | 一元多项式求值(逐点)、多项式求值 |
a:x:b、linspace | 斜坡信号 |
bsearch | 搜索有序表 |
serialize、marshal | 平化至字符串 |
deserialize、unmarshal | 从字符串还原 |
add event listener | 事件回调注册 ——非ActiveX也可用 |
尾声
… It was definitely easier to create a global, but as anyone could have guessed, it was overused and abused and it led to lots of support headaches ever since.
… There have always been nodes with side-effects, e.g., those that do I/O, but the introduction of globals and references circumvented data-flow, sometimes with deleterious effects. …
… I consider my biggest blunder to be the introduction of global variables. A tightly scoped global is much better represented as a tag channel. And a serially reusable stateful subVI is a much better implementation in all other cases because it is more flexible, can sometimes be more efficient (e.g., for global arrays), and the slight extra effort to create it can keep it from being abused.
……创建全局变量会方便很多。当然,也都猜到了,全局变量泛滥成灾,成了萦绕在客服心头的痛。
……副作用一直都在,比方说输入输出节点肯定有副作用。但是全局变量和引用的引入架空了数据流,有时候会引发灾难。……
……我真傻,真的,不应该加上全局变量功能的。作用域小的话,改成tag通道就清楚多了。否则就用顺序可复用带状态子VI(指功能全局变量),也要好得多,因为这样灵活度大些,还有可能性能更好(比如全局数组)。就是操作稍微麻烦点,也好预防滥用。
隐瞒 〖全局变量〗隐藏数据流于程序框图表面之下,面条成粉丝;〖反馈节点〗根本就不是节点,不值得为了对应一张工程图而破坏数据流的先后顺序;若要缓冲线程间数据,可用通道,〖队列〗相关的函数已没有必要使用。
资源 LabVIEW安装目录下的examples文件夹中提供了大量内置VI的用例,仅关于通道的示例就有百余个。在forums.ni.com、lavag.org、labviewwiki.org等网站上能找到很多常见问题的答案。
后日谈 DAQ,GPIB,TDMS,面向对象,自定义控件,数值单位,库,Express VI,状态图编辑器,版本控制,发布,MATLAB互联,Python互联,Win32互联,.NET互联,完全的互联网访问权限,控制底层硬件,用户界面技巧……LabVIEW之万能,其自身就有很多组件是用LabVIEW制作的。奇怪,好像没有什么不支持的,到手边仍倍感无力。这种感觉没有错。LabVIEW作为编程语言是完备的,作为软件生态是残废的。用它做它会做的,采采数据得了。真要控制DAQ,Python不香吗?
致谢 感谢你读这篇谜语人教程到这里。其实我也只是刚开始学LabVIEW,以上这些几乎都是我一方总结、改编自文初所引《LabVIEW》一文、从乱翻两周前从图书馆借的《我和LabVIEW:一个NI工程师的十年编程经验(第2版)》和瞎做通信原理实验作业而来,学得匆忙,我也完全没用过美国国家仪器的硬件,有什么错漏和建议请务必指出……诶,免责声明是不是应该放在开头啊? *Rick Astley的歌声*
Discourse和WordPress都野蛮替换本非emoji的符号,麻了
此版本的WordPress对details/summary标签似乎不友好,编辑几次会多出很多多余的空白段落,毕竟那时候浏览器对这对标签的支持还没有像今天这么好……
Discourse的图片压缩至少不改变像素数据(神秘),WordPress……不管怎么样,都导致LabVIEW提供的复制代码片段功能无法正常使用(它把实际代码数据放在PNG辅助块中)