EPICS IP模块

该IP模块包含对特定基于消息(如串行)设备的支持。

要使用本模块中的支持,在将要被加载到ioc中的.dbd文件中包含ip.dbd或ipSupport.dbd。对于VME机箱,也包括ipVX.db或者ipVXSupport.dbd。在无论是哪种情况,链接库libip。(EPICS有对应每种目标架构的单独lib目录, 但所有目标架构使用相同的dbd目录)。

介绍

deviceCmdReply: 用于构建命令,发送它们和解析响应的运行时支持:

  • deviceCmdReply.db
  • deviceCmdReply_settings.req
  • deviceCmdReply.adl deviceCmdReply_full.adl

deviceCmdReply 是一个可在运行时编程的 EPICS 数据库,用于将基于消息通信的设备集成到 EPICS 中,即使该设备尚未编写专门的 EPICS 驱动程序支持。一个deviceCmdReply数据库可以格式化和发送一条命令字符串给设备并且接着读取并解析一条应答字符串。字符串被限制于39个字节,并且可以包含任何ASCII字符,包括null字符。它们也可以包含由sCalcoutjil支持的任何校验和或者CRC。

集成到EPICS表示"提供一个代表此设备种一个值的EPICS PV",使得如果写入这个EPICS PV,这个值被写入到设备,或者如果从这个EPICS读取,从设备获取一个值。(注意:通过通道访问写入一个EPICS PV会触发写入值到设备的处理过程, 但通过通道访问从一个PV读取不触发处理过程。如果你想要一个PV跟踪设备种某个值,你必须安排那个值要被读取。例如,你可能配置deviceCmdReply数据库周期的运行)。

基于消息的设备是通过字节序列与其使用者通信的设备,尤其,ASCII字符串。基于消息的设备一般通过串口,GPIB或套接字(TCP/IP或UDP/IP)。

deviceCmdReply本质上是一种对EPICS asyn记录的封装。deviceCmdReply数据库实际上由两个sCalcoout记录,一个格式化命令,另一个解析响应,和一个执行实际写和读取的asyn记录组成。asyn记录提供了deviceCmdReply的大部分原生能力。

以下是其中的主要部分:

|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 写入串口,GPIB或套接字或从它们读取 | 这使得deviceCmdReply控制各类设备 |
| 连接若干asyn记录到单个端口 | 这使得deviceCmdReply的多个实例一起运行来控制单个设备的不同方面功能。该基础架构通过异步记录(asyn record)确保多个deviceCmdReply实例之间互不干扰,同时也能避免其他基于asyn的通信支持模块在访问同一设备时产生冲突。 此功能也允许deviceCmdReply补充对基于消息设备的已有支持。 |
| 在不干扰端口实时通信下从一个端口断开并且连接另一个端口 | 这种设计允许加载少量 deviceCmdReply 数据库(即使在加载时可能尚未确定其最终用途),并可根据具体设备需求灵活调用所需的任意数量数据库,以支持必要的指令集。 |
| 运行时修改端口配置 | 这使得使用者可以尝试,例如,不同波特率和握手协议,以便找到有效的协议 |
| 根据实际发送和接收的命令和响应,显示它们 | 这使得使用者能够快速和有效地调试命令格式,响应解析以及接口配置。 |

因而,deviceCmdReply地若干实例可以以单个设备为目标,来实现不同命令,或者读取不同值。例如,单个deviceCmdReply可能从控制器周期地读取回读温度,而另一个deviceCmdReply用于写温度设置点。

写设备支持地其它方式

deviceCmdReply是临时应急运行地快速方式,并且它是原型开发的实用工具,用于尝试命令和端口配置,观察如何如何运行。但它不是编写真实设备支持地最好方法。编写基于消息地设备支持的更佳策略包括以下:

|--------------|--------------------------------------------------------------------------------------|
| streamDevice | 标准EPICS记录类型直接连接到了硬件,使用一个指定命令格式,响应解析的协议文件。 |
| devXxStrParm | 所选的记录类型直接连接到了硬件。命令格式与回复解析规则在输入/输出链接的用户参数部分中指定。当synApps ip模块文档目录种devXxStrParm.README. |
| SNL | 特别,SNL代码监控PV's,并且使用asyn记录写入硬件或从硬件读取 |
| 其它 | 由其它在用的方法,但我对描述它们如何运行知道的不多 |

如何部署deviceCmdReply

deviceCmdReply是synApps ip模块的组件,并且它需要操作EPICS asyn模块和synApps calc模块。本文档假设ip模块版本2.7,asyn版本4.6或更高,以及calc版本2.6.3或更高。

构建和安装

构建此软件的推荐方式是构建synApps,synApps包含它,支持它,并且提供了一个部署和使用它的示例ioc目录。只构建和安装这个特定的软件模块所需的模块当然是可能的和实际的,但我们没有人员编写描述synApps各自模块的构建和安装的文档。

加载到ioc

用以下示例命令加载数据库到ioc:

bash 复制代码
dbLoadRecords("$(IP)/ipApp/Db/deviceCmdReply.db", "P=xxx:,N=1,PORT=serial1,ADDR=0,OMAX=40,IMAX=40")

此处$(IP)将被扩展为环境变量IP的值,ip模块的完整路径。(如果I在configure/RELEASSE种定义了IP,EPICS构建将把这放入cdCommands文件)。

以下宏参数用于针对或配置数据库到一个特定的程序:

  1. P=xxx:定义一个短字符序列,用于区分这个记录种记录名和加载到某个其它ioc种类似记录的名称。
  2. N=1:定义另一个短字符序列,用于相互区分加载到相同ioc中不同deviceCmdReply数据库
  3. PORT=serial1:定义aysn,这个asyn记录初始化连接哪个port. (在运行时可以更高这个port)
  4. ADDR=0:被忽略,除非asyn记录连接的端口可以与不止一个设备通信。例如,如果端口是GPIB接口或者RS485串口,ADDR指定写入或者从若干设备中哪一个读取。(在运行时可以更改地址)
  5. OMAX=40:告诉asyn记录为二进制输出数组BOUT分配多少空间。仅在使用BOUT时才重要,仅在asyn记录的OFMT字段设为了"Binary"或"Hybrid"才使用BOUT。如果OFMT设为了"ASCII"(默认),则使用AOUT字段而不是BOUT。AOUT时一个EPICS字符串,其固定大小40个字节。
  6. IMAX=40:告诉asyn记录为其二进制输入数组BINP分配多少空间。仅在使用BINP时才有用,仅在asyn记录的IFMT字段设为"Binary"或"Hybrid"时才会使用BINP。如果IFMT设为了"ASCII"(默认),则使用AINP字段而不是BINP.AINP是EPICS字符串,其固定大小为40个字节。

数据库将包含以下记录,使用者程序通过这些记录对设备编程:

|--------------------------------|----------|--------------|
| 记录名 | 记录类型 | 功能 |
| xxx:deviceCmdReplyn_formatCmd | sCalcout | 构建要被写入设备的字符串 |
| xxx:deviceCmdReplyn_do_IO | asyn | 发送到/从硬件接收 |
| xxx:deviceCmdReplyn_parseReply | sCalcout | 解析响应字符串 |

此处xxx,由P宏指定,而n是由N宏指定。

从现在起,我们分别称这些记录"..formatCmd", "asyn记录"和"...parseReplay"。

安排存储/恢复

在每次ioc重启后,必须重新编写deviceCmdReply数据库集合是非常不方便的,并且因为它们被设计成由使用者在一个正在运行的ioc上被编写,有人可能想要自动保存数据库。你可以通过添加以下一行到一个autosave request文件做此时。

bash 复制代码
file deviceCmdReply.req P=$(P),N=1

此处P和N与以上dbLoadRecord命令相同,对于每个加载的数据库。

装载使用者界面

在这个模块中与支持deviceCmdReply一起提供了若干MEDM显示文件。该IP模块包含如下图所示(以及其简化版本)的显示界面,它们是单个deviceCmdReply实例的主要用户接口。这些显示界面包含了调用由calc和asyn模块维护的相关显示界面的按钮,它们提供了在deviceCmdReply中使用的sCalcout和asyn记录的详细控制和某些使用者文档。

该显示界面可通过其他MEDM界面中的"关联显示"按钮调用,调用时需使用与上述dbLoadRecords()命令中相同的宏参数进行定义。

编程

1、对deviceCmdReply实例进行编程,首先要选的是将使我们连接到我们想要控制的设备的端口名。接近显示界面中心,靠近"Send/Receive",是一个文本输入字段,它包含了asyn记录连接到的端口名称。这个字段属于asyn记录;它的完整名称是"xxx:deviceCmdReplyn_do_IO.PORT"。你可以更改这个字段为提供了接口asynOctet的任何端口。

2、下一步选取的是要执行说明输入/输出操作。asyn记录的TMOD字段(在这个显示界面和asyn记录的"asynOctet.adl"显示界面中被标记为"传输")控制这个,并且提供以下选项:

|------------|----------------------------------------|
| write/read | 发送一条命令并等待应答 |
| write | 发送一条命令 |
| read | 等待应答 |
| flush | 在deviceCmdRely中位未使用 |
| NoI/O | 对测试有用,并且对在"...formatCmd"记录正在被配置时禁用输出有用 |

当TMOD包含"Write",第一个sCalcout记录("...formatCmd")用于格式化要被发送给设备的字符串。它放置格式化字符串到asyn记录的AOUT字段,并且猝发asyn记录运行。

当TMOD包括"Read"时,在设备读取结束后,由asyn记录运行第二个sCalcout记录('"...parseReply")。它从asyn记录的AINP或BINP字段获取由asyn记录读取的字符串(取决于asyn记录的IFMT字段)。如果IFMT=ASCII,则使用AINP,否则使用BINP。

3、如果TMOD包含"Write", 接下来的任务是编程"...formatCmd" sCalcout记录根据可用信息构建输出命令字符串。sCalcout记录的可用信息包含已经写入它其中一个字段的任何数值或字符串,以及可连接到sCalcout记录输入链接的其他EPICS过程变量(PV)的值。注意:如果asyn记录的OEOS字段(在以上显示界面"Output"部分中标记未"TERM"的文本字段)不是空,asyn记录将向这条命令末尾添加一个终止符。终止符可以是任意一个或两个字符的字符串。常见终止符包括"\r"和"\r\n"。

4、如果TMOD包含"Write",接下来的任务是配置这个asyn记录与连接设备的端口进行会话。点击"I/O details"关联显示按钮,查看可操作的asyn记录显示菜单。例如,你可以选择"Serial port parameters"指定波特率等。

5、如果TMOD包括"Read",接下来任务是配置asyn记录,使得当设备完成发送应答时它能够识别。asyn记录支持用于识别传输终止的三种策略:

  1. 某些设备在末尾添加终止符,通过终止符可以识别字符串末尾。在这种情况中,设置asyn记录的IEOS字段(以上显示界面"Input"部分的标记为"TERM"的文本字段)为设备将使用的终止符。
  2. 其它设备将发送可预测数目的字符。在这种情况中,设置asyn记录的NRRD字段(以上显示界面中"Input"部分标记为"Length Requested"的文本字段)为预计的字符数。
  3. 如果设备不支持上述任何一种,设置asyn记录的TMOT(标记为"Timeout"的文本字段)为秒数,在期之后,应答确实已经到达。

6、如果TMOD包含"Read",接下来的任务是编程"...parseCmd" sCalcout记录解析设备返回的字符串,并且从它提取你感兴趣的数值或字符串。

示例

格式化可打印的输出字符串

对于大多数设备,命令字符串非常简单:某个固定文本,后跟一个用某种方式编码的数值,可能后跟某些更多固定文本。内含数字的简单字符串可使用sCalcout记录的PRINTF(format, variable)功能轻松格式化,这可以简化为$P(format, variable)。

  • 在以下示例中,要被发送给设备地数值被写入到了sCalcout记录地A字段。

|------------|--------------------------------|-----------------|
| 所需输出 | CALC表达式 | 注释 |
| M03; | P("M%02d;",A) | 如果A\>99,不保证2个数字 | | M03; | P("M%02d;", max(0,min(99,A))) | 对A进行限制 |
| S1.2340000 | P("S%f",A) | 默认精度和字段宽度 | | S1.234 | P("S%.3f", A) | 受控精度 |
| S 1.234 | P("S%6.3f",A) | 受控精度和字段宽度 | | S01.234 | P("S%06.3f",A) | 如果需要,开头用0填充 |

  • 有时有两个数值必须被发送(例如,我们假设一个地址在"...formatCmd"记录A字段,并且一个值被发送到那个地址,我们假设它是在B字段)。注意:当A或B被写入(通过通道访问)时,将发送这条命令。当你不想要这条命令被发送时,通过设置asyn记录地TMOD字段为"No I/O",你可以解决这个问题。

|-----------|-----------------------------|---------------|
| 所需输出 | CALC表达式 | 注释 |
| M03=4.235 | P("M%02d=",A)+P("%.3f",B) | 注意$P()仅接收两个参数 |

  • 有时一个设备地命令语法将需要一个整数被分割单独地字节 :

|-------------|---------------------------------------------|------------------|
| 所需输出 | CALC表达式 | 注释 |
| M3=01;M4=74 | P("M%02d;",A\>\>8) + P("M4=%02d", A&255) | A=330, 以10进制数值发送 |
| M3=01;M4=4A | P("M%02X;",A\>\>8) + P("M4=%02X", A&255) | A=330, 以16进制数值发送 |

  • 有时设备想要看到文本字符串,但将使用此设备的软件而是想要发送数值。例如,你可能想要发送数值"1"打开一个快门,和发送数值"0"关闭它,但设备想要看到像"S0 OPEN"和"S0 CLOSE"的字符串。在这种情况下,你可以通过在sCalcout记录的AA字段写入"0"字符串,BB字段写入"1"字符串等等并且把这个字符串字段当作一个数组,使用stringCalc操作符"@@"来索引这个数组生成响应数值的字符串数组。

|---------|------------------|--------------------------------|
| 所需输出 | CALC表达式 | 注释 |
| S0 OPEN | P("0 %s", @@A) | A寻址字符串数组:AA="CLOSE"; BB="OPEN" |

  • 有一个你必须小心的特殊字符。反斜杠字符"\"将被asyn记录当作转义序列的开始。要发送一个反斜杠给设备,你必须用另一个反斜杠转义它:

|------|---------|----------------|
| 所需输出 | CALC表达式 | 注释 |
| \ | \\ | 能够发送不可打印字符的小开销 |

格式化不打印字符

有些设备想要见到原始二进制格式的数值。EPICS 3.14版本前,没有一个广泛支持的方法来从一个记录到另一个记录传递会包含内嵌ASCII NULL字符的字符串,因此deviceCmdReply对此类设备还不可用。

但EPICS 3.14为包含不可打印字符的字符串提供转义翻译服务,把它们内容写入一个能够在正常EPICS字符串中被传递的格式。这使得我们能够加载这样的字符串到数据库,通过通道访问发送它们,自动保存它们等。在此为这个目标,通过从包含转义序列字符串产生原始二进制的dbTranslateEscape()和做相反事情的epicsStrSnPrintEscped()函数对实现这个服务。

在以下表格中,一个单字节二进制值将由<n>表示。

  • 在发送固定二进制数值的最简单情况中,你可以把它们编码为转义序列.

|---------------|---------------|----------|
| 所需输出 | CALC表达式 | 注释 |
| <2>#<254> | "\x02#\xfe" | 使用十六进制序列 |
| <2>#<254> | "\002#\376" | 使用十进制序列 |

  • 要嵌入二进制数值到一个输出字符串,你可以使用sCalc记录的WRITE(format, variable)命令,其可以被简写为$W(format, variable)。为了传输给asyn记录,这个函数将把它的结果编码为一个转义字符串。asyn记录在发送字符串给设备前,将转换这个字符串为其raw,二进制格式。

WRITE()函数中使用的格式指示符字符旨在让您感到熟悉,因为它们借鉴了标准C库中printf()函数的常见用法,但这里使用它们指定将如何编码二进制值,因此将忽略任何字段宽度或精度的说明。

|---------------|-------------------------------|---------------------------|
| 所需输出 | CALC表达式 | 注释 |
| <2> | W("%c", A) | 把sCalcout记录的A字段的值编码为一字节整数 | | \<2\>#\<254\> | W("%hd", A)+"#" +W("%hd",B) | 把A和B编码为2字节整数 | | \<2\> | W("%d", A) | 把A编码为4字节整数 |
| <2.1> | W("%f", A) | 把A编码为4字节浮点 | | \<2.1\> | W("%lf", A) | 把A编码为8字节浮点 |

  • 某些设备需要在命令字符串后追加校验和(或CRC),并且如果校验和不正确,将忽略这条命令。从命令字符串计算校验和,并且必须加到它末尾。sCalcout记录支持少量校验和,并且每种类型以两种风格出现:一个函数,从提供的字符串计算校验和,并且返回它; 一个函数,添加校验和到提供字符串的末尾,并且返回这个字符串。

|-------------------------------------|----------------------------------------|------------------------|
| 所需输出 | CALC表达式 | 注释 |
| <2><10><3><XOR checksum> | ADD_XOR8("\002" + W("%c",A)+"\\003") | 添加XOR8校验和到字符串 | | \<2\>\<10\>\<3\>\ | MODBUS("002"+W(""%c",A),"\003") | 添加modbus/RTU CRC到字符串末尾 |

解析可打印输入字符串

当asyn记录从设备接收了应答时,它将触发"...parseReply" sCalcout记录运行。这个sCalcout记录将做的第一件事情是从asyn记录的AINP字段获取应答字符串,并且把它放到sCalcout记录的AA字段。我们这里的工作只是解析AA字段的内容。解析来自设备的应答一般需要两个操作:

  1. 我们必须指定包含有用信息的应答部分;
  2. 我们必须转换哪些信息为所需的格式

在最简单情况中,sCalcout记录将为我们查找和转换。函数INT(string)搜索string查找第一个看起来像一个整数值的东西,并且返回那个数值的值。类似,函数DBL(string)搜索string查找第一个看起来像浮点数值的东西,并且返回它的值。

|------------|---------|-----------|
| 应答字符串 | CALC表达式 | 注释 |
| VALUE=12 | INT(AA) | 转成整数 |
| VALUE=1.23 | DBL(AA) | 转成双精度浮点数值 |

但只在我们真地想要地数值是第一个看起来像一个数值地东西,这才有效。在更复杂地情况,我们必须在大量枯燥的内容中费力搜寻,才能找到那个数字。如果无感兴趣的内容是固定长度,我们只要在转换前在这个字符串中移动指定数目的字符到 :

|------------|-------------------|----------------------|
| 应答字符串 | CALC表达式 | 注释 |
| VALUE=1.23 | DBL(AA[7,-1]) | 跳过"VALUE1="并转换为float |
| VALUE=1.23 | $S(AA, "%*7c%f") | 跳过7个字符并且转换为float |

有关子串操作符"[]"的更详细信息见sCalcout记录文档。这里为此目的,语法是[<start>,<end>]。如果<start>是一个数值,它表示要跳过的字节数。<end>在此文档中总是-1。

如果不感兴趣的内容不是固定长度,但它变长部分以固定字符串结束(或者甚至某个固定长度的第n个实例),我们可以按如下跳到感兴趣的内容:

|--------------------|-----------------------------|------------------|
| 应答字符串 | CALC表达式 | 注释 |
| REG237=1.23 | DBL(AA["=",-1]) | 跳过"="并且转成double |
| reg:2 1.23 | $S(AA, "%*s %f") | 跳过"="并且转成float |
| R1=1.23 R32=R7 | INT(AA["=",-1]["=",-1]) | 跳过两个"="字符,找到一个整数 |

在以上示例中,我们用字符串值第一个参数--模式字符串--和通常的第二个-1参数使用了子串操作符"[<start>, <end>]"。如果找到这个模式,操作的结果是就在这个模式之后立即开始的子串,并且持续到这个字符串末尾。(如果模式多次出现,仅考虑第一个实例)。

解析不可打印输入

相比于解析可打印输入,解析不可打印输入带来另一种问题。策略仍然非常简单:移动到感兴趣内容,并且转换它。但移动到感兴趣内容可能被被之前字节中转义序列复杂化。我将首先讨论移动作为单独问题,并且接着关心转换它。

  • 在最简单的情况,我们必须移动的字节是固定的,我们可以使用[]操作符,如在之前章节讨论的。如果固定字节包括了转义序列,为了对字符计数或者识别模式的目的,我们只要把它们当作纯文本。在以下示例中,<2>代表一个不可打印的字节,其二进制值是2:

|-------------------------|-------------------------|------------------|-------------------------|
| 实际应答字符串 | 我们所见到的应答字符串 | 表达式 | 注释 |
| <2><target> | \002<target> | AA[4,-1] | 跳过1个被编码为一个四字节转义序列的实际字节 |
| <2>abc<3><target> | \002abc\003<target> | AA[11,-1] | 跳过5个被编码为一个11字节转义序列的实际字节 |
| <2>abc<3><target> | \002abc\003<target> | AA["\003",-1] | 只查找"\003"转义序列 |

  • 如果我们必须移动过以获取感兴趣内容的字节包含了不固定的转义序列,我们不能仅跳过指定的字节数目,因为不同的字节值一般将被编码为不同长度的转义序列。例如,字节值10将被编码为"\n";14将被编码为"\016";而71将被编码为"G"。

因而,在这种情况下,我们不能对转义字符串计算字节数来查找感兴趣的内容,并且像"AA[<number>,-1]"的表达式将不起作用。我们仍然能够使用像AA[<string>,-1]的表达式模式匹配就在感兴趣内容前的字节,但仅在模式是股东的并且不能出现在应答字符串更早的位置。

|-------------------------|-------------------------------|---------------------|------------------|
| 实际应答字符串 | 我们所见到的应答字符串 | 表达式 | 注释 |
| <?>abc<3><target> | <? bytes>abc\003<target> | AA["abc\003",-1] | 找到"abc\003"转义序列 |

  • 在最坏(此软件可以处理)的情况,我们必须跳过以获得感兴趣内容的字节不是固定的,并且不是以我们可以模式匹配的序列结束的,但字节数目已知。在这种情况下,我们必须计数在raw,二进制字符串中的字节。幸运的,READ(string, format)函数可以做这件事,因为它在解析之前会先将读取的字符串转换为二进制形式,并且因为它允许使用一个被抑制赋值的转换指示符。

|-----------------|-----------------------|----------------------|---------------|
| 实际应答字符串 | 我们所见到的应答字符串 | 表达式 | 注释 |
| <n><target> | <N bytes><target> | READ(AA, "%*Nc...") | 跳过raw字符串中N个字节 |

因此现在我们由了进行某些实际转换的工具。我们使用READ(string, format)函数(它也可以缩写为$R())来转换。如之前,用粗体标识感兴趣内容:

|-------------------------------|---------------------------|--------------------------------|----------------------|
| 实际应答字符串 | 我们所见到的应答字符串 | 表达式 | 注释 |
| <2><6> | \002**\006** | READ(AA[4,-1],"%c") | 跳过4字节转义序列,读取8位整数 |
| <2><7><2> | \002\007\002 | READ(AA[4,-1],"%hd") | 跳过4字节转义序列,读取16位整数 |
| <2><5><1><3><4> | \002\005\001\003\004 | READ(AA[4,-1],"%d") | 跳过4字节转义序列,读取32位整数 |
| <2>abc<3><10> | \002abc\003\n | AA[11,-1] | 跳过11字节转义序列,读取8位整数 |
| <2>abc<3><101><5> | \002abc\003A\005 | READ(AA["003",-1],"%hd") | 找到"\003",读取16位整数 |
| <?>abc<3><5> | <? bytes>abc\003\005 | READ(AA["abc\003",-1],"%c") | 找到"abc\003",读取8位整数 |
| <3 bytes><4> | <? bytes>\004 | READ(AA,"*3c%c") | 跳过raw字符串中3个字节,读取8位整数 |

相关推荐
yuyuyuliang007 个月前
Qt5中使用EPICS通道访问读写EPICS PV
linux·开发语言·qt·epics
woshigaowei51469 个月前
自定义EPICS在LabVIEW中的测试
labview·epics
woshigaowei51469 个月前
LabVIEW中EPICS客户端/服务端的测试
labview·epics
yuyuyuliang001 年前
Webmin在EPICS IOC启动中的应用
epics·hexapod c-887
yuyuyuliang002 年前
EPICS modbus 模块数字量读写练习
epics
yuyuyuliang002 年前
基于EPICS stream模块的直流电源的IOC控制程序实例
linux·c语言·epics
yuyuyuliang002 年前
基于asynPortDriver的asyn异步驱动程序示例
c语言·epics