三、协议文件
1、一般信息
协议文件描述与一种设备类型的通信。 它包含这种设备类型每个功能和变量的协议,它们影响在一个协议中命令如何工作。它不包含有关单独设备或者使用的通信总线的信息。
每种设备类型应该有它自己的协议文件。我建议选择一个包含这种设备类型名称的文件名。在此文件名中不要使用空格并且保持它简短。在使用这个文件的记录的INP或OUT链接中通过其名称引用这个文件。协议文件必须存储在环境变量STREAM_PROTOCOL_PATH中列出目录中的一个目录中。
协议文件是一个纯文本文件。不在引号(单‘或双")封闭中的所有东西不是大小写敏感的。这包括命令的名称,协议和变量。在名称,带引号字符串以及特殊字符(诸如={};)之间可能有任意数目的空白(空格,tab,新行等)或者注释。一个注释是从一个不带引号的#开始到本行末尾的所有东西。
示例协议文件:
# This is an example protocol file
Terminator = CR LF;
# Frequency is a float
# use ai and ao records
getFrequency {
out "FREQ?"; in "%f";
}
setFrequency {
out "FREQ %f";
@init { getFrequency; }
}
# Switch is an enum, either OFF or ON
# use bi and bo records
getSwitch {
out "SW?"; in "SW %{OFF|ON}";
}
setSwitch {
out "SW %{OFF|ON}";
@init { getSwitch; }
}
# Connect a stringout record to this to get
# a generic command interface.
# After processing finishes, the record contains the reply.
debug {
ExtraInput = Ignore;
out "%s"; in "%39c"
}
2、协议
为这种设备类型的每个功能,定义一个协议。一个协议由一个名称以及其后跟着位于花括号中{}一个语句体组成。名称在这个协议文件中必须唯一。它被用于在记录的INP或OUT链接中引用这个协议,因而保持它简短。它应该描述这个协议的功能。它一定不能包含空格或者以下任何字符,:={}$'"#。
协议体包含一个由;分隔的命令以及可选变量赋值的序列。
引用其它协议
为了节省输入,一个先前定义的协议可以在另一个像一条不带参数的命令的协议内部被调用。协议名称被在被引用协议中的命令替代。但,这不包含来自被引用协议的任何变量赋值或异常handlers。见以上示例中的@init handler。
限制
StreamDevice协议不是一种编程语言。它没有循环和条件(在此版本StreamDevice中)。但如果出现错误,例如超时或者在输入解析中不匹配,可以调用一个异常handler进行清理。
3、命令
在一个协议总可以使用7条不同的命令:out, in, wait, event, exec, disconnect和connect。大部分协议将只有单个写某个值的out命令或者一个在out命令后跟读取一个值的in命令组成。但在一个协议中能够有任意数目的命令。
1) out string;
写输出到设备。参数string可以包含格式转换器,在发送前其被这个记录的格式化值替代。
2)in string;
读取和解析来自设备的输入。参数string可以包含格式转换器,它们指定解析要被输入到记录的数据。输入必须匹配这个参数字符串。来自设备的任何输入应该用一个in命令消耗。例如,如果一个设备确认一个设置,使用in命令检查这个确认,即使它不包含用户输入。
3) wait milliseconds;
就等待一些毫秒。取决于计时器系统的分辨率,实际的延时可能稍长于指定的。
4) event(eventcode) milliseconds;
用某个超时时间等待事件eventcode。一个事件实际表示什么取决于使用的总线。一些总线完全不支持事件,一些总线提供很多不同事件。如果总线只支持一个事件,(eventcode)可有可无。
5) exec string;
参数string被传递给IOC shell作为要执行的一条命令。
6) disconnect;
从硬件断开连接。这可能不是被所有总线支持。任何in或out命令将自动重新连接。仅用"I/O Intr"模式读取的记录将不引起一个重连。
7) connect milliseconds;
显式地用milliseconds超时事件连接硬件。由于连接被自动处理,通常不需要这个命令。它可能在一个disconnect后有用。
4、Strings
在一个StreamDevice协议文件中,可以用带引号的文字(单引号或双引号),用一个字节值序列或者用一个二者的组合编写字符串。
带引号字符串的示例是:
"That's a string."
'Say "Hello"'
双引号和单引号文字之间没有区别,它只是使得在一个字符串中使用零一类型的引号变得简单。要把协议文件的长字符串分成多行,在换行前闭合引号并且在下一行重新打开它们。不要再引号中使用换行。
作为out或in命令的参数,字符串文字可以包含格式转换器。一个格式转换器以%开始并且作用类似在C函数printf()和scanf()中的格式化。
StreamDevice使用反斜杠字符\在带引号字符串文字中定义转义序列:
\", \', \%和\\表示字面", ', %和\。
- \a表示警铃(ASCII代码7)。
- \b表示退格(ASCII代码8)。
- \t表示tab(ASCII代码9)。
- \n表示新行(ASCII代码10)。
- \r表示回车(ASCII代码13)
- \e表示退出(ASCII代码27)
- \x后面最多跟2个16进制数字表示一个十六进制值的字节。
- \0后面最懂跟3个8进制数字表示一个八进制值的字节。
- \1到\9后面最多再跟2个十进制数字表示一个十进制值的字节。
- \?再输入中匹配任意字节,再输出中,它不打印任何东西。
- \_在输入中匹配任意数目空格(包括没有),在输出中它打印一个空格。
- \$后跟一个协议变量的名称被那个变量的内容替代。
对于非打印字符,编写字节值得序列替代被转义的引号字符串文字经常更简单。一个字节分别被写成一个范围-128到255,-0x80到0xff(大小写不敏感)或者-0200到0377没有引号的十进制,十六进制或者八进制数值。
对于若干字节代码,StreamDevice也识别ASCII符号名称(大小写不敏感):
- NUL (=0x00) null
- SOU (=0x01) 头开始
- STX (=0x02) 文本开始
- ETX (=0x03) 文本结束
- EOT (=0x04) 传输结束
- ENQ (=0x05) 询问
- ACK (=0x06) 确认
- BEL (=0x07) 铃
- BS (=0x08) 回退
- HT或TAB (=0x09) 水平制表符
- LF或NL (=0x0A或10) 换行/新行
- VT (=0x0B或11) 垂直制表符
- FF或NP(=0x0C或12) 换页或新页
- CR (=0x0D或13) 回车
- SO (=0x0E或14) 移出
- SI (=0x0F或15) 移进
- DLE (=0x10或16) 数据链路退出
- DC1 (=0x11或17) 设备控制1
- DC2 (=0x12或18) 设备控制2
- DC3 (=0x13或19) 设备控制3
- DC4 (=0x14或20) 设备控制4
- NAK (=0x15或21) 否定回答
- SYN (=0x16或22) 同步空闲
- ETB (=0x17或23) 传输块结束
- CAN (=0x18或24) 取消
- EM (=0x19或25) 介质结束
- SUB (=0x1A或26) 替代
- ESC (=0x1B或27) 退出
- FS (=0x1C或28) 文件分隔符
- GS (=0x1D或29) 组分隔符
- RS (=0x1E或30) 记录分隔符
- US (=0x1F或31) 单位分隔符
- DEL (=0x7F或127) 删除
- SKIP或?匹配任何输入字节。
通过空格或逗号分隔若干带引号的文字和字节值能够构建单个字符串。
示例:
以下行代表相同的字符串:
"Hello world\r\n"
'Hello',0x20,"world",CR,LF
72 101 108 108 111 32 119 111 114 108 100 13 10
5、协议变量
StreamDevice在一个协议文件中使用三种变量类型。系统变量影响in和out命令的行为。协议参数作用类似函数参数并且可以在记录的INP或OUT链接中被指定。用户变量可以在协议中被定义和使用,作为经常使用变量的简写。
系统和用户变量可以在协议文件的全局上下文或者在协议内局部地被设置。当被全局设置时,一个变量在被重写前保持它地值。当被局部设置时,一个变量仅在协议内有效。要设置一个变量,使用这样的语法:
variable = value;
通过$variable或${variable}可以引用带引号字符串之外设置的变量,通过\$variable或\${variable}能够引用在带引号字符串内设置的变量。引用将被到此的变量值替代。
系统变量
这是一个系统变量,它们的默认设置以及它们影响睡眠的列表。
1) LockTimeout = 5000;
整数。影响在一个协议的第一个out命令。如果其它记录当前使用这个设备,在放弃前等待对设备独占访问多少毫秒。
2) WriteTimeout = 100;
整数。影响out命令。如果我们能够访问设备,但输出不能立即被写,在放弃前等待多少毫秒。
3) ReplyTimeout = 1000;
整数。影响in命令。
不同的设备需要不同时间计算一个响应并且启动发送它。等待从设备输入的第一个字节多少毫秒。由于若干其它记录可能在这段时间等待访问这个设备,LockTimeout应该比ReplyTimeout更大。
4) ReadTimeout = 100;
整数。影响in命令。
设备可能分片发(例如:字节)送输入。当它停止发送时,在放弃前等待更多输入字节多少毫秒。如果InTerminator = "",一个读取超时不是一个错误而是一个有效的输入终止。
5) PollPeriod = $ReplyTimeout;
整数。影响在I/O Intr模式中第一个in命令。
在此模式中,如果此刻没有其它记录执行一个in命令,某些总线需要周期查询来获取输入。在上次查询后或上次接收的输入后在再次查询前等待多少毫秒。一个代表值是预计输入周期时间的一半。更长的值引起延时,而更短的值会增加CPU消耗。如果未设置,使用用于ReplyTimeout的相同值。
6) Terminator
字符串。影响out和in命令。
大部分设备在每条消息后发送并且预计终止符,例如CR LF。Terminator变量的值自动被添加到任何输出末尾。它也被用于查找输入末尾。在输入被传递给in命令前,移除它。如果没有定义Terminator或InTerminator,底层驱动程序会使用它自己的终止符设置。例如,asynDrvier定义它自己的终止符设置。
7) OutTerminator = $Terminator
字符串。影响out命令。
如果一个设备对于输入和输出有不同的终止符,使用这个用于输出终止符。
8) InTerminator = $Terminator;
字符串。影响in命令。
如果一个设备对输入和输出有不同终止符,使用这个用于输入终止符。如果没有定义Terminator或InTerminator,底层驱动会使用它自己的终止符设置。如果InTerminator="",一个读取超时不是一个错误而是一个有效的输入终止符。
9) MaxInput = 0;
整数。影响in命令。
某些设备不发送终止符,但总是发送一个固定消息大小。在终止输入前读取多少字节,即使没有输入终止符或读取超时?值0表示"无限"。
10) Separator = "";
字符串。影响out和in命令。
当用格式转换器格式化或者解析数组值时,在值之间要写或者预计什么字符串?要在输入中匹配任意数目空格,使用"\_"。
11) ExtraInput = Error;
Error或Ignore。影响in命令。
通常,当输入解析结束时,在输入中留下的任何字节被当作解析错误。如果extra输入字节应该被忽略,设置ExtraInput = Ignore。
协议参数
有时,协议差别非常小。在哪种情况中,只编写一个协议并且为差别使用协议参数会是方便的。例如,一个用于3个轴X,Y,Z的电机控制器需要三个协议来设置一个位置。
moveX { out "X GOTO %d"; }
moveY { out "Y GOTO %d"; }
moveZ { out "Z GOTO %d"; }
任何其它协议,它也需要三个版本。那意味着基本编写所有东西三次。要使得这个更简单,可以使用协议参数:
move { out "\$1 GOTO %d"; }
现在相同的协议可以以move(X), move(Y)和move(Z)在三个不同记录的OUT字段中使用。
在括号中最多可以指定9个参数,由逗号分隔。在协议中,引号外以$1, ..., $9引用它们,在引号内以\$1 ... \$9引用它们。参数$0解析成这个协议名。
要使链接可读性更好,在每个逗号以及闭合的括号前和后允许一个空格。这个空格不是参数字符串的组成部分。任何其它的空格是这个参数的组成部分。
如果一个参数包含匹配的括号对,这些以及在内的所有逗号是这个参数的组成部分。这使得传递像(1,2)参数字符串变得简单而没有太多转义。
不匹配的括号必须被双反斜杠\\转义,以及是在括号对外的逗号。因为一个反斜杠已经被db文件解析器消耗,所有双反斜杠是必须的。要在参数字符串中传递一个文字反斜杠,使用4个反斜杠\\\\。
注意:可以在参数中使用宏。这使得传递部分记录名给在重定向中使用的协议成为可能。
示例:
record(ai, "$(PREFIX)recX5") {
field(DTYP, "stream")
field(INP, "@$(PROTOCOLFILE) read(5, X\\,Y $(PREFIX)) $(PORT)")
}
record(ai, "$(PREFIX)recY5") {}
read { out 0x8$1 "READ \$2"; in "%f,%(\$3recY\$1)f" }
协议解析成:
read { out 0x85 "READ X,Y"; in "%f,%($(PREFIX)recY5)f" }
在这里,$(PREFIX)被其宏值替代。但注意:宏实际上在链接被解析前被替换,因而包含逗号或括号的宏值会有意料不到的作用。
用户变量
用户定义的变量只是节省输入的手段。一旦设置,一个用户变量可以之后在协议中被引用。
f = "FREQ"; # sets f to "FREQ" (including the quotes)
f1 = $f " %f"; # sets f1 to "FREQ %f"
getFrequency {
out $f "?"; # same as: out "FREQ?";
in $f1; # same as: in "FREQ %f";
}
setFrequency {
out $f1; # same as: out "FREQ %f";
}
6、异常handlers
当一个错误发生时,可以调用一个异常handler。异常handlers是在一个协议中一类子协议。它们由命名的相同集合组成,并且是为了在遇到通信问题时重置设备或者干净地结束这个协议。像变量,异常handlers可以被全局或者局部地被定义。被全局地定义的handlers除非被一个局部handler重写,否则用于所有以下协议。有一个以@起始的异常handler名称的固定集合。
1) @mismatch
当输入在一个in命令中不匹配时被调用。它表示设备已经发送了除此协议预计外的东西。如果这个handler以一个in命令开始,则这条命令重新解析来自不成功in的旧输入。来自不成功in的错误消息被抑制。无论如何,这个记录将以INVALID/CALC状态终结。
2) @writetimeout
当在一条out命令中发生一个写超时时被调用。
它表示输出不能被写到设备。注意:在这种情况中在这个handler中的out命令也可能失败。
3) @replytimeout
当在一条in命令中发生一个响应超时时,被调用。
它表示设备没有发送任何数据。注意:在此情况中在这个handler中in命令也可能失败。
4) @readtimeout
当在一条in命令中发生一个读取超时时,被调用。
它表示在发送至少一个字节后,设备意外地停止了发送数据。
5) @init
不真的是一个异常,但通常用相同语法被指定。这个handler可以用于哟弄个一个从设备读取的值初始化一个输出记录。
示例:
setPosition {
out "POS %f";
@init { out "POS?"; in "POS %f"; }
}
在执行这个异常handler后,协议终结。如果在一个异常handler中发生任何异常,不调用其它handler,而是协议立即结束。一个异常handler使用来自这个异常发生的协议的所有系统变量设置。