多线程GPIB/Visa/串口接口通信






4.97/5 (37投票s)
通过命令队列实现多接口的同步/异步控制
- 下载源代码 - 178.5 KB (最后更新于 2018年5月23日,参见历史记录)
引言
此项目源于我为实现高效GPIB控制所做的努力。在某些设备响应缓慢的环境中,它们通常会成为数据流的瓶颈。本软件提出的解决方案是尽可能地利用异步操作,为每个设备使用一个独立的线程,这样不同设备的写/读操作序列就会自动交错进行。这效果很好,因为即使在一些设备响应非常慢(例如,响应时间需要2-3秒)的情况下,我也能轻松获得相对较高的读取速率(例如,10台设备平均每秒读取2-3次/设备,总计每秒20-30次操作)。异步操作被排队处理,这使得编程非常简单。然而,标准的同步(阻塞)操作也是允许的,代码可以安全且透明地处理同步和异步命令的并发使用。
由于软件已经被设计得足够通用,可以适应各种Gpib库,因此它最终也可以适应其他接口,如Visa库和串口。Visa特别有趣,因为它允许通过USB(适用于符合USB-TMC协议的设备)或TCP/IP(LXI标准)控制仪器,而无需开发特定的驱动程序。当然,如果有人能够开发一个不依赖Visa的这些协议的实现,那就太好了。
除了串口外,该软件的运行需要安装第三方驱动程序。
遵循面向对象的思想,所有接口都表示为从代表通用设备的abstract
类派生的类,而底层的接口依赖操作被实现为虚方法,这样接口就可以“多态地”使用:除了创建各种设备的实例时,代码不需要知道每个设备使用的是哪个接口。这种方法也使得添加新接口或通过创建子类覆盖需要修改的方法来调整现有实现变得容易。
所有文件和项目都是为WindowsForms应用程序编写的,并提供两个版本:C#和VB.NET。
所有项目都是在VS2008下创建的(也已在SharpDevelop v4.3下测试过)。
项目和文件包含
项目IODevices
和IODevices_withNINET
构建了一个库(.NET程序集“IODevices.dll”),该库定义了一个通用(abstract
)类IODevice
及其派生自它的各种接口的实现(有关这些类的更多详细信息和所需驱动程序,请参见“IODevices
程序集和实现”部分)。接口包括:
GPIBDevice_NINET
- 用于NI板卡GPIBDevice_ADLink
- 用于ADLink板卡GPIBDevice_gpib488
- 用于Keithley、MCC和较旧的NI板卡VisaDevice
- 通过Visa实现的通用接口(GPIB、USB等)SerialDevice
- 用于串口
该库还定义了两个窗体,用于显示状态和错误消息。
您必须在项目中添加对该程序集的引用才能使用它,请参见测试项目。(或者,您可以直接将必要的文件复制到您的项目中 - 但那样应用程序将可以访问所有“内部”字段和方法 - 原则上不太安全!)。
类GPIBDevice_NINET
引用了National Instruments的.NET程序集来访问Gpib驱动程序(有关详细信息,请参见类描述,由于版权原因,这些文件不在此处包含),包含此文件的任何项目都将无法编译,因此我制作了两个项目:
- 项目
IODevices
:不包含GPIBDevice_NINET
- 项目
IODevices_withNINET
:包含GPIBDevice_NINET
两个项目生成的.NET程序集的名称都是“IODevices.dll”,因此当您在两个版本之间切换时,需要强制VS重新构建库。
其他实现仅使用Windows DLL(由各个板卡供应商提供的“纯C”DLL库),因此项目IODevices
无论系统上是否安装了接口,都将始终编译成功。如果缺少必需的DLL,则仅在实例化相应的类时才会发生错误。请注意,NI的Gpib板卡也可以通过Visa接口访问,Visa通常也随其一起安装,因此可能不需要GPIBDevice_NINET
接口类。
项目testIODevice
和testIODevice-withNINET
是演示如何使用库基本功能的示例。您可以选择两个设备(例如,一个快速一个缓慢)并同时向两者发送命令,观察其工作情况。
测试项目包含对程序集“IODevices.dll”的引用,因此测试和库项目是相互依赖的(如果您想检查IODevices
程序集中的代码如何工作,最好将它们放在同一个解决方案中)。
所有这些项目也都包含在两个“解决方案”中:
testIODevice
testIODevice-withNINET
每个解决方案都包含一个库和一个测试项目。
背景:Gpib通信问题
NI参考
- http://www.ni.com/white-paper/3275/en/
- http://www.ni.com/tutorial/4054/en/
- http://www.ni.com/white-paper/2927/en/
- http://www.ni.com/tutorial/4478/en/
- http://www.ni.com/white-paper/4629/en/
让我们研究一种简单的顺序寻址Gpib设备的方法:
- 向设备1发送命令
- 等待设备1的响应
- 向设备2发送命令
- 等待设备2的响应
- 向设备3发送命令
- 等待设备3的响应
这种方案有几个潜在的缺点:
- 如果通过低级gpib函数等待响应,那么:
- 调用低级gpib函数(“receive”)时,应用程序在等待期间冻结(如果是在主(GUI)线程上)。此问题可以通过为gpib操作使用不同的线程(异步操作)来解决。
- 在低级接口调用期间,GPIB总线会被锁定,因此即使使用多线程,该序列也不是高效的,因为其他设备也必须等待,除非我们尽可能缩短“等待响应”的时间(在“receive”函数内),这些函数在总线被锁定时等待响应。这可以通过轮询或设置非常短的超时值并重复读取以避免超时来实现,这样总线在大部分时间都不会被锁定。
- 顺序查询将花费时间,这会在我们需要定期扫描许多设备时产生问题。获取响应所需的时间不仅取决于传输数据的量,通常最慢的响应是针对触发测量的命令,例如DMMs常用的“
Read
”命令。对于DMMs,延迟取决于测量的分辨率,获取响应可能需要几秒钟,这通常是gpib的主要瓶颈。当然,有一些软件解决方案使用设备特定的配置(触发命令或自动触发模式,如果可用等),然而这种方法需要更多的编程工作,并且更难做到通用(通常,应用程序需要在每次配置新实验时处理来自设备池的许多相似设备)。因此,最好能找到一种通用但高效的方法。
在此,一些限制是GPIB特有的,因为所有设备共享一个公共总线,但其他限制适用于所有接口。特别是,在较长时间内锁定总线的危险性是GPIB特有的,与以太网或USB不同,它不使用分组交换协议。
通过(a)命令/响应序列的交错和(b)轮询,可以最大限度地减少延迟和GPIB总线不可用时间段。如果每个设备都有一个独立的线程用于异步操作,就可以自动实现命令交错,如下所述。
GPIB是一个总线,控制器(PC)决定何时允许每个设备发送数据,因此GPIB的“写”和“读”操作可以交错进行,从而有效地同时并行查询多个设备。
- 向设备1发送命令
- 向设备2发送命令
- 向设备3发送命令
- 等待设备1的响应
- 等待设备2的响应
- 等待设备3的响应
那么,当设备1响应时,设备2和设备3也可能已准备好发送数据,因此设备2和3不会产生额外的性能损失。
通过为每个设备使用不同的线程来执行gpib操作,可以以一种对调用程序完全透明的方式自动实现写/读序列的交错。实现此功能的方案将是:
thread1
:向设备1发送命令;等待设备1的响应thread2
:向设备2发送命令;等待设备2的响应thread3
:向设备3发送命令;等待设备3的响应
在这里,每个线程将在GPIB总线可用时尽快继续。仍然存在等待设备响应时阻塞总线的问题。这可以通过GPIB的“poll
”功能最有效地解决:在邀请设备通信之前,测试它是否已准备好发送数据。所以最后最有效的方案是:
thread1
:向设备1发送命令;定期轮询其“数据就绪”状态;获取设备1的响应thread2
:向设备2发送命令;定期轮询其“数据就绪”状态;获取设备2的响应thread3
:向设备3发送命令;定期轮询其“数据就绪”状态;获取设备3的响应
如果设备不支持轮询,那么可以通过在写和读操作之间设置一个恒定的延迟(在此期间将 respective 线程休眠)来替换它,以缩短总线对其他设备不可用的“等待响应”时间。此外,在接口级别设置一个短超时值并在超时后延迟重复读取也是不错的。
实现此方案的库的目的是提供一个开箱即用的解决方案,以最小的编程工作量创建此类高效代码,所有线程操作对用户代码都是透明的。实际上,我自己的目的是尽可能少地修改代码,以便轻松地将一些使用经典顺序方法的现有应用程序进行改编。
IODevice
类旨在适应任何低级GPIB接口,因此所有低级操作都定义为abstract
(VB:MustOverride
)方法。该项目为几种GPIB接口提供了此类abstract
类的实现(请参阅上面的文件列表和下面的参考)。此外,abstract
方法足够通用,可以构建Visa和串口等其他接口的实现(因此最初的名称“GpibDevice
”最终变成了“IODevice
”)。为其他硬件创建实现应该相当容易。
请注意,标准的GPIB库也提供了一种异步操作,但有些有限。例如,在NI参考手册中提到:
引用“异步I/O调用(BeginRead和BeginWrite)的设计目的是让应用程序可以在I/O进行时执行其他非GPIB操作。一旦异步I/O开始,进一步的NI-488.2调用将受到严格限制。任何干扰正在进行的I/O的调用都是不允许的,并将返回一个异常。”
这意味着不执行任何队列(这些函数仅提供一种方法来避免在总线等待设备响应时阻塞调用线程),但这种限制与上述并行查询方案不兼容(与简单的同步调用相比,它还增加了大量的编程开销),并且使这些功能对我们的目的帮助不大。另一方面,GPIB的“Notify
”回调功能没有这种限制,并且可以实现,从而产生更强大、更灵活的异步读取方案,如下文所述。
此处提供的代码提供了更高一个级别的抽象,因为异步任务被排队处理(见下文),因此调用程序不必关心何时允许发送命令:在这里,所有异步调用都随时允许。但是,我们可以通过PendingTasks
方法检查队列内容来决定是否需要调用。
请注意,在NI Visa中可以对来自异步写/读操作的消息进行排队(参见viWriteAsync
、viReadAsync
函数)。然而,这里的理念不同,因为整个查询(写/读序列,包括命令)是为每个设备排队的,所以程序不必等待异步读操作完成就可以向同一个设备发送其他命令*。这使得异步编程非常简单,并允许自动的“出错重试”功能。如果设置了“retry
”标志,那么在出错时,函数将清除设备并重复整个写/读序列,直到成功或用户中止。
*实际上,我对Visa的经验不足,无法确定它是否可以处理查询队列(但在所有示例中,程序在继续读取之前都等待写事件),因此如果我错了,请纠正我。另一方面,HiSLIP等一些低级协议实现了查询队列。
轮询
引用自:http://www.ni.com/tutorial/4054/en/
“串行轮询是从GPIB设备在请求服务时获取特定信息的一种方法。当您进行串行轮询时,控制器会查询每个设备,以查找断言SRQ的设备。设备通过返回其状态字节的值来响应轮询。数据可用或错误条件的存在决定了此值。ANSI/IEEE标准488.1-1987仅在状态字节的位6中指定了一个位,当设备请求服务时,该位为TRUE。状态字节的其他位由仪器制造商定义。IEEE 488.1兼容仪器具有确定仪器错误发生或设备正在进行自检的位。这些位定义在仪器供应商之间不一致,并且确定服务请求原因的方法因设备而异。
ANSI/IEEE标准488.2-1987通过定义特定的服务请求条件来解决此问题,从而一种模型可以描述所有兼容设备的的状态字节。位6(RQS,请求服务)位保持IEEE 488.1的定义。如果设置了位6,则设备已请求服务。IEEE 488.2标准定义了位4和位5;仪器制造商定义其余位(0到3和7)。位4是消息可用(MAV)位。如果设备先前已查询数据且设备有待发送的消息,则设置此位。
通过设置enablepoll
字段为true
(默认值)来启用轮询选项。如果设备兼容488.2标准,则应启用它。然后,串行轮询用于通过检查其状态字节来查看设备是否准备好发送数据,这样在等待设备响应时,gpib总线在大部分时间不会被锁定。这在查询命令也充当新测量的软件触发器时(DMMs的标准行为)尤其重要。
本库仅使用标准中定义的的状态字节的MAV(消息可用)位,如上所述。
大多数设备都符合488.2标准,但并非所有设备都符合,例如,某些Lakeshore温度控制器定义了它们自己对所有状态字节位的含义。如果您看到“poll timeout”错误,那么很可能是这种情况,您应该设置适当的状态字节掩码MAVmask
或禁用轮询。或者,可以编写一个派生类来覆盖虚方法“pollMAV
”:此方法还会返回整个状态字节,因此很容易编写一个修改后的实现,其中状态字节的解释不同(参见实现部分中的示例)。如果轮询不可用,那么如上所述,我们应该在接口级别设置一个短超时值(读取仍然会在超时后自动重试,稍后将对此进行解释),以免总线长时间被阻塞。
异步接口回调
在仅基于轮询的方案中,设备的有效响应时间有一个由轮询频率定义的最小粒度。对于时间关键型应用程序,我们可以增加此频率,但这也会增加总线上的低效流量(如果后续轮询之间的延迟非常短,甚至可能导致错误)。另一方面,低级驱动程序可以访问硬件中断,因此可以立即了解总线上出现的所有信号。各种接口提供了事件的异步信号机制,这可以使轮询更有效。在GPIB中,我们可以配置选定的设备在准备好发送数据时拉起Service Request(SRQ)GPIB线,并配置驱动程序在每次检测到SRQ时触发一个异步回调(http://www.ni.com/white-paper/4629/en/)。USBTMC、VXI11和HiSLIP协议也实现了异步服务请求,使用带外信令。同样,串口也可以配置为在每次新数据到达时触发一个事件。
IODevice
类提供了一个非常简单(可选)的功能,可以与驱动程序的异步回调一起使用,并且可以无缝地集成到上述轮询方案中:写操作后的读取等待或下一次轮询/读取尝试可以被另一个线程调用设备方法“WakeUp
”异步中断。涉及两个延迟:写和读之间的延迟(delayread
)以及后续读取/轮询尝试之间的延迟(delayrereadontimeout
)。此方法可以从任何线程调用,并且旨在用于低级驱动程序调用的回调函数(即,可以使用无同步的回调)。
在当前项目版本中,此技术在除GPIBDevice_gpib488
之外的所有类中都已实现,因为该驱动程序不支持它:类GPIBDevice_NINET
、GPIBDevice_ADLink
和VisaDevice
提供了一个可选的设置GPIB“Notify
”回调或Visa支持的其他协议中等效回调的选项,而SerialDevice
类默认使用它,实现了SerialPort
类的DataReceived
事件的处理器。
有关此功能的实现和使用,请参阅IODevice
类的描述。
使用代码:IODevice类参考
I/O函数
有两种类型的I/O函数:
- “
Send
”函数用于不需要设备响应的命令。 - “
Query
”函数用于发送命令并读取响应。
两者都使用相同的代码和围绕IOQuery
类构建的体系结构,在代码中,“query
”一词用于两者(IOquery
类中的“type
”字段区分这两个版本)。
注意:没有单独的“read
”操作,因为这与“retry
”功能不兼容(请注意,VISA定义了三种函数:写、查询和读)。
仅“read
”操作仅在“仅发送”设备的情况下需要,我不知道任何这类设备,但如果需要此类操作,则在调用命令为空字符串的查询方法时,它将不会调用“send
”操作,因此它等同于单独的“read”。
I/O函数不抛出任何异常。库函数或用户回调函数中的外部异常可以被捕获也可以不被捕获(参见“catchinterfaceexceptions
”和“catchcallbackexceptions
”标志)。但请注意,类构造函数可能会抛出异常(这是为了避免创建不明确的对象,构造函数异常应在构造函数外部捕获),请参阅下面的不同类的描述。
每种类型的命令都提供两个版本(有关每个命令的语法,请参阅下面的参考):
- 阻塞命令:
SendBlocking
、QueryBlocking
在调用线程(通常是GUI线程)上立即执行,方法将等待直到从接口接收到响应。这里的“阻塞”意味着调用在收到响应之前不会返回,但是总线在整个查询过程中不会被阻塞,因此可以并行进行其他查询,与“async”命令相同。当然,对于每个设备,一次只允许一个阻塞命令。
- 异步命令:
SendAsync
、QueryAsync
:在这里,查询被排队,并且队列在一个不同的线程上处理(生产者-消费者模型)。调用会将查询附加到队列中并立即返回。查询完成后,将调用用户“回调”函数。
对于SendAsync
,指定回调不是强制性的,因此发送命令可以以真正的“即时发送”方式完成。
每个设备都有自己的队列,并运行自己的专用线程来处理队列中的查询。通过这种方式,设备的异步命令按顺序处理,但不同设备的命令可以并行处理,如上所述。
使用这两种查询类型的原因是,主流程(例如,需要复杂的设备配置、启动、采集等序列,其中发送的命令有时可能取决于接收到的数据,因此使用同步调用比链式异步回调更简单自然)通常与一些以恒定间隔重复的附加任务并行运行,以更新实验状态(例如,每秒读取一次温度)。对于这些任务,我们通常使用计时器,并且使用异步查询执行这些任务效率更高,利用可用的总线带宽和时间段。
即使没有计时器,使用异步命令同时发起查询也有好处。然后,可以使用“WaitAsync
”方法在异步命令队列和主线程之间进行同步:此方法等待在调用之前发起的查询完成(注意:不是直到队列为空 - 如果也发出了其他异步查询,这可能永远不会发生)。例如:
device1.QueryAsync(...);
device2.QueryAsync(...);
device1.WaitAsync();
device2.WaitAsync();
如引言中所述,该序列通常比一系列两个阻塞查询的等效序列性能更好。
完全可以在回调函数中调用另一个异步查询。这提供了链接异步操作的可能性(循环链接将生成异步操作的循环)。假设您需要实现一个序列,其中在查询设备1后,使用其查询结果来选择/格式化对设备2的另一个查询。整个序列可以异步实现,如果对设备2的查询是从处理设备1查询的回调函数中调用的。这种基于通过回调链接异步任务的方法与Labview的工作方式有些相似:在Labview中,程序流程控制(调度不同任务,如读取仪器和处理)完全基于数据流,例如,从仪器接收到的数据用作代码图的下一个节点的事件触发器。
您甚至可以在同一设备上混合使用阻塞和异步命令,那么原则上,阻塞命令应在当前异步操作(如果有)完成后或等待重试时立即处理(但是,确切的时间不确定,这由OS任务调度程序决定)。
公共参数
string cmd
:命令字符串bool retry
:如果设置为true
,则在出错时将重复整个查询(直到成功或用户中止)。bool cbwait
:如果设置为true
,则异步线程将等待直到回调函数完成,然后才处理队列中的其余查询(这是短版本默认的行为,其中此参数不存在)。可以设置为false
,例如,如果回调函数启动了长时间处理并且不需要等待,但是在这种情况下,回调函数要么需要阻止事件处理,要么需要是可重入的(可以在返回之前再次调用)。还请注意,使用此设置时,“捕获回调异常”功能(catchcallbackexceptions
标志设置为true
)将不起作用。如果您想了解原因,我推荐这组精彩的文章。int tag
:传递给查询变量的附加字段,如果使用相同的回调函数处理不同的查询,则可用于区分查询。
阻塞命令
1) SendBlocking
C#
public int SendBlocking(string cmd, bool retry)
VB
Public Function SendBlocking(ByVal cmd As String, ByVal retry As Boolean) As Integer
2) QueryBlocking
C#
public int QueryBlocking(string cmd, out IOQuery q, bool retry)
public int QueryBlocking(string cmd, out string resp, bool retry)
public int QueryBlocking(string cmd, out byte[] resparr, bool retry)
VB
Public Function QueryBlocking(ByVal cmd As String, ByRef q As IOQuery, _
ByVal retry As Boolean) As Integer
Public Function QueryBlocking(ByVal cmd As String, ByRef resp As String, _
ByVal retry As Boolean) As Integer
Public Function QueryBlocking(ByVal cmd As String, ByRef resparr As Byte(), _
ByVal retry As Boolean) As Integer
在第一个语法中,变量q
包含查询的完整信息(状态、数据、计时),另外两个更简单的版本直接以字符串(resp
)或字节数组(resparr
)的形式给出结果。如果出错(返回值与0
不同),q
将包含错误代码和消息,但数据字段(ResponseAsString
、ResponseAsByteArray
)将是null
引用(VB:Nothing
),以及另外两个版本中相应的变量resp
和resparr
。
返回值:与q.status
相同(如果成功则为0
,否则为错误代码),否则为-1
表示该设备上已在进行阻塞调用(如果允许事件,则可能发生),-2
表示设备正在卸载。
异步命令:SendAsync和QueryAsync
其中大多数方法使用IOCallback
类型的参数来定义回调函数(在.NET术语中,回调函数等同于事件处理程序,只是在这里,处理程序委托是为每个查询单独指定的,以获得更大的灵活性)。此函数的签名由声明给出:
VB : Public Delegate Sub IOCallback(ByVal q As IOQuery)
C#: public delegate void IOCallback(IOQuery q);
这里,变量q
将包含与操作相关的状态和数据,规则与阻塞调用相同,即,如果发生错误,数据字段将是null
引用。
请注意,即使回调是从不同的线程启动的,回调函数也会在主(GUI)线程上执行(这是为了允许在回调函数内更新GUI组件),换句话说,异步线程会向主线程发送消息以调用函数(“同步回调”)。因此,在GUI线程允许处理消息之前,不会调用回调函数。
所有版本的SendAsync
和QueryAsync
方法的返回值是:0
表示成功,-1
表示队列已满(每个设备的最高队列长度由字段maxtasks
定义,默认为50
),-2
表示设备正在卸载。
SendAsync
C#
public int SendAsync(string cmd, bool retry)
// complete version (with callback)
public int SendAsync(string cmd, IOCallback callback, bool retry, bool cbwait, int tag)
VB
Public Function SendAsync(ByVal cmd As String, ByVal retry As Boolean)
' complete version (with callback)
Public Function SendAsync(ByVal cmd As String, ByVal callback As IOCallback, _
ByVal retry As Boolean, ByVal cbwait As Boolean, ByVal tag As Integer) As Integer
在完整版本中(可能很少需要),回调函数将被调用以指示操作的状态(但是,传递给它的IOquery
变量中没有有效数据)。此版本允许调用程序通过IOQuery
变量的AbortRetry()
方法来中断“retry”循环。
带回调的QueryAsync
当函数完成时(或发生错误时),它将调用回调函数,并将结果传递给IOQuery
变量。回调函数必须检查接收到的IOQuery
变量的状态字段,以确定其中是否有有效数据(如果状态不是0
,则ResponseAsString
和ResponseAsByteArray
将是null
引用)。
C#
//standard:
public int QueryAsync(string cmd, IOCallback callback, bool retry)
//complete:
public int QueryAsync(string cmd, IOCallback callback, bool retry, bool cbwait, int tag)
VB
'standard:
Public Function QueryAsync(ByVal cmd As String, ByVal callback As IOCallback, _
ByVal retry As Boolean) As Integer
'complete:
Public Function QueryAsync(ByVal cmd As String, ByVal callback As IOCallback, _
ByVal retry As Boolean, ByVal cbwait As Boolean, ByVal tag As Integer) As Integer
注意:生产者/消费者异步模型通常使用事件和回调(如这里),或者简单地将异步结果堆叠到用户代码提供的输出队列中。后者方案(即,不是触发事件,而是将查询结果排队以供以后检索,例如,类似于Visa中的事件队列)可以通过定义一个回调来实现,如下所示:
public Queue<IOQuery> myresults;
public void queuecallback(IOQuery q){myresults.Enqueue(q);}
带TextBox的QueryAsync
这是一个“轻量级”版本,它不是调用回调函数,而是将结果(如果没有错误)放入TextBox
变量(其Text
属性)。此版本不返回任何错误信息(除了消息窗口,如果已启用)。
C#
//2nd version : update textbox with data string
public int QueryAsync(string cmd, TextBox text, bool retry)
public int QueryAsync(string cmd, TextBox text, bool retry, int tag)
VB
' 2nd version : update textbox with data string
Public Function QueryAsync(ByVal cmd As String, ByVal text As TextBox, _
ByVal retry As Boolean) As Integer
Public Function QueryAsync(ByVal cmd As String, ByVal text As TextBox, _
ByVal retry As Boolean, ByVal tag As Integer) As Integer
IODevice类的其他方法
实例public
方法
C#
public bool IsBlocking(); // true when blocking call in progress
public int PendingTasks(); // return number of queries in the queue
public int PendingTasks(string cmd); // same for a specific command: number of copies
// of specific command in the queue
public int PendingTasks(int tag) // same for commands with a specific tag value
public void WaitAsync(); // this method can be used to synchronize
// blocking and async calls
public void AbortAllTasks(); // as in the title: aborts all queries
// (blocking and async)
public void Dispose();
VB (请参阅上面的C#代码注释)
Public Function IsBlocking() As Boolean
Public Function PendingTasks() As Integer
Public Function PendingTasks(ByVal cmd As String) As Integer
Public Function PendingTasks(ByVal tag As Integer) As Integer
Public Sub WaitAsync()
Public Sub AbortAllTasks()
Public Sub Dispose()
静态(VB共享)方法
C#
public static void ShowDevices() // shows DevicesForm displaying current
// status of all devices, see below.
public static IODevice DeviceByName(string name) // find device among created devices
// using name
public static void DisposeAll() // dispose all created devices
VB
Public Shared Sub ShowDevices()
Public Shared Function DeviceByName(ByVal name As String) As IODevice
Public Shared Sub DisposeAll()
Public
字段
public int maxtasks; // max queue length (default=50)
public string devname, devaddr; // device name and address
public static string statusmsg; // optional message (status etc.)
// to display in device list window
// some delays to tweak performance (all in ms):
public int delayread; // default delay between cmd and read :
// to avoid blocking
// gpib bus by slow devices when polling
// is not available
public int delayrereadontimeout; // default delay before retrying read
// after timeout
// or delay between polls if polling used
public int delayop; // delay to wait between operations,
// for old devices that may not
// accept frequent requests
public int readtimeout; // cumulative timeout for read
public int delayretry; // delay before retry on error
public bool checkEOI; // use EOI information: if true repeat
// read if EOI
// not detected (e.g.,. buffer too small);
// default=true,
public bool enablepoll; // use serial poll,
// set to false for devices not
// supporting polling ("poll timeout" message)
public byte MAVmask = 16; // for GPIB, USBTMC-USB488, VXI-11:
// standard (488.2) mask for MAV status
// (bit 5 of the status byte),
// change it for devices not quite
// compliant with 488.2
public bool stripcrlf; // remove crlf in ByteArrayToString method
public bool eventsallowed; // for blocking commands :
// when waiting for response
// and during and retry loop
public bool showmessages; // showing error window IOmsg enabled
public volatile IOQuery lastasyncquery;
public bool catchinterfaceexceptions; // default=true , set to false
// when debugging a new interface
public bool catchcallbackexceptions; // default=true
public bool callbackonretry; // default=true, if callback called on
// each retry when error
“编写实现”部分列出了内部方法和属性,供在实现中使用。
设备列表窗口
调用static
方法IODevices.ShowDevices()
时打开,外观如下例所示:
默认情况下,ShowDevices()
会在启动时调用(在IODevices
的static
构造函数中)。如果您觉得它烦人,可以将常量showdevicesonstartup
设置为false
。
错误消息窗口
当“showmessages
”字段设置为true
(默认值)时,发生错误时会打开此窗体。该窗体不是模态的,仅用于信息显示:无论窗体是否显示,程序都会以相同的方式继续。但是,它允许您轻松中止“retry”。在成功的重试后纠正错误后,窗体将自动关闭。在上例中,它显示设备未连接,并且查询将重试。一旦连接,消息将消失。如果您关闭窗口并且错误仍然存在,消息将再次弹出。如果您觉得烦人,请最小化窗口而不是关闭它。
请注意,此窗口中显示的所有信息也包含在IOQuery
变量中(当然,回调异常除外),因此很容易实现您自己的消息显示。
IOQuery类公共成员
C#
public string cmd; // copy of the command to identify query
public int tag; //optional additional query identifier
public string ResponseAsString; // non null only if status=0
public byte[] ResponseAsByteArray; // non null only if status=0
public int status;
//0:ok, otherwise combination:
//bit 1:timeout, bit 2: on send(0) or recv(1), bit 3 : other error (see errcode), ,
//bit 4: aborted by user, bit 5: poll error, bit 8: error in callback
//(exceptions catched when “catchcallbackexceptions” flag is set)
// so if not aborted: status=1: tmo on send; =3 tmo on rcv,
// =4 other err on send, =6 other err on rcv; if aborted add 8 ,
// if poll timeout add 16,
public int errcode; //interface error code (valid if status>0)
public string errmsg; //interface error message(valid if status>0)
//timestamps for testing performance:
public DateTime timecall; // when method called
public DateTime timestart; //when device unlocked and operation started
public DateTime timeend; // when data received (or aborted)
public int type; //type of query: 1 (“send”, no resp.),
// 2: true “query” (wait for response)
public IODevice device ; // device used (to access other fields)
public void AbortRetry(); //abort this task(async or blocking)
public void AbortAll(); // calls device.AbortAllTasks
VB (请参阅上面的C#代码注释)
Public cmd As String
Public tag As Integer
Public ReadOnly Property ResponseAsString() As String
Public ReadOnly Property ResponseAsByteArray() As Byte()
Public status As Integer
Public errcode As Integer
Public errmsg As String
Public timecall As DateTime
Public timestart As DateTime
Public timeend As DateTime
Public type As Integer
Public ReadOnly Property device() As IODevice
Public Sub AbortRetry()
Public Sub AbortAll()
IODevices程序集和实现
该程序集定义了两个命名空间:IODevices
和IODeviceForms
。后者用于内部访问显示设备列表和错误消息的窗体,无需在应用程序中导入。
IODevices
命名空间应该导入到应用程序中,它定义了以下类:
class IOQuery
如上所述
class IODevice
是一个abstract
(VB:MustInherit
)类,各种实际设备作为子类从中派生。它包含主要代码和public
方法,但通过四个abstract
方法来访问底层接口(如果您想实现其他接口,请参见“abstract
方法”下的代码说明)。
遵循面向对象的思想,每个设备都派生自IODevice
,它包含所有public
方法,而底层接口通过private virtual
方法访问,这样,子类就可以多态地使用:除了在创建设备实例时(或调用实现特定的非虚方法时),代码不需要知道使用了哪个接口(请参阅“testIODevice
”项目中的示例代码)。
这里提供的此abstract
类的实现提供了一个基本配置(但已成功与各种设备测试过)。再次,遵循面向对象的思想,为每个特定配置编写一个派生类比创建一个考虑所有可用选项的类更容易。例如,所有GPIB类都使用标准的“EOI”信号来检测消息的结束(参见下面的buffersize参数说明)。如果您的设备不能设置EOI,而是使用特定的字符来终止消息(例如“\n”),您可以编写一个子类,它重新定义构造函数以相应地设置设备选项(或者,您也可以覆盖“ReceiveByteArray
”方法,请参见编写新实现的说明部分)。
这些类的构造函数将在设备初始化失败时抛出异常。这是为了避免创建不明确的对象,构造函数异常应在构造函数外部捕获。
启用GPIB和Visa的异步Notify回调
在早期版本中,这是通过仅在两个派生类中定义的EnableNotify
属性实现的。现在(自2018年2月起),主类定义了一个虚布尔属性EnableNotify
(默认=false
),因此它也可以用于实现它的所有类,以编写更通用的代码。在基类默认实现中,尝试将此属性设置为true
将引发NotSupportedException
。这些更改显然不会破坏使用此属性的现有代码的向后兼容性。在当前版本中,该属性在GPIBDevice_NINET
、GPIBDevice_ADLink
和VisaDevice
中实现(已重写)。在这些类中,将此属性设置为true
将激活板卡的“Notify
”回调以响应SRQ并订阅设备的通知事件。每个SRQ回调将调用设备WakeUp()
方法,如前所述。
要启用在MAV位设置时触发SRQ,您需要启用仪器服务请求使能寄存器中的相应位。通常,它是位4,通过向设备发送命令“*SRE 16”来设置(请注意,您还可以添加其他标志来唤醒,例如OPC、错误等)。测试窗体中有一个执行所有这些配置步骤的示例函数。
public void setnotify(IODevice dev)
{
try{ //use try-catch in case it is not implemented in the actual target class
dev.EnableNotify = true; //enable calling WakeUp on SRQ
if (dev.SendBlocking("*SRE 16",true)==0); //set bit 4 in the Service Request
//Enable Register, so that the
//MAV status will set SRQ
{
dev.delayread = 1000;
dev.delayrereadontimeout = 1000; //set long wait delays :
//will be interrupted anyway
}
}
catch (Exception ex)
{
MessageBox.Show( "cannot set EnableNotify for device " +
dev.devname + CrLf + ex.Message);
}
}
readtimeout
字段与接口超时设置的考虑
低级驱动程序具有“timeout
”参数,该参数定义驱动程序等待操作完成的最大时间,否则将中止操作。对于“receive”操作,此时间包括等待设备准备好和传输数据所需的时间。如果使用轮询(enablepoll=true
),则在设备准备好之前永远不会调用“receive
”操作,因此超时值应仅大于数据传输时间,但确切设置并不关键(对于大多数情况,默认值3秒应该可以)。然而,对于不支持轮询的设备,可能需要调整接口超时值,以避免“receive”操作长时间阻塞接口。当发出接口超时信号时,IODevice
类会自动重试读取(最终的超时条件仅在累积超时延迟readtimeout
后才发出,请参见查询序列),因此接口超时值可以设置得相对较短(但要比预期的长足够,以免中断传输)。例如,如果我们等待大约1秒来读取一个短string
(数据传输时间小于100微秒),我们可以安全地设置,例如,delayread=1000
,接口超时设置为100-300毫秒。
设置GPIB/Visa接口超时的方法取决于具体实现(通过属性NIDevice
、IOTimeoutCode
、IOTimeout
,具体取决于子类)。
SerialDevice
实现中不需要这样的属性,因为这里的接口超时具有非常不同的含义:对于串口,“receive”操作实际上不涉及数据传输(接收由驱动程序在后台持续处理,调用驱动程序函数仅涉及从内部输入缓冲区复制内存)。因此,串口驱动程序超时可以固定为任意短的值,而不会有数据损坏的风险。
checkEOI
标志和buffersize
对于所有GPIB类,如果构造函数中未指定“buffersize
”构造函数参数,则默认设置为32k(实现还提供了一个Buffersize
属性)。
Buffersize
确定在一次调用库的“receive
”函数时可以读取的最大数据量(以字节为单位)。
默认情况下,GPIB使用设置在最后一个传输数据字节上的“EOI”信号来告知接收方已读取所有数据:如果未设置EOI,则表示所有数据都未传输,最常见的原因是它不适合提供的缓冲区大小,并且应重复读取以获取下一部分。如果checkEOI
字段设置为true
(默认值),则每次读取后未设置EOI时,类将自动重复读取并将数据附加到输入缓冲区。当然,以小块读取会带来很大的性能损失(每次都需要重新寻址设备),因此通常最好将缓冲区设置为预期的最大数据长度*。也许,在需要读取大量数据的情况下,可以优先选择限制缓冲区大小,以便将读取分为几个部分,以便在中间释放总线,从而使其更常用于其他设备。
*显然,固件有bug的设备在接收缓冲区太小时也可能表现出未定义行为。
当前版本提供了以下实现:
class GPIBDevice_NINET
这使用了NI Gpib的原生.NET库,引用了NI的以下.NET程序集:NationalInstrumentsCommon
、NationalInstruments.NI4882
(这些随NI GPIB驱动程序安装)。它与NI的GPIB-USB-HS+板卡进行了大量测试。如果找不到这些文件(由于版权原因,此处未包含),程序集将无法编译,因此,也提供了一个不包含此类版本的项目。请注意,在许多情况下,我们不一定需要这个库:通常NI的GPIB软件也会安装Visa。
类构造函数
C#
public GPIBDevice_NINET(string name, string addr)
public GPIBDevice_NINET(string name, string addr, int buffersize)
VB
Sub New(ByVal name As String, ByVal addr As String)
Sub New(ByVal name As String, ByVal addr As String, ByVal buffersize As Integer)
name
是给设备的名称,显示在DevicesForm
中。
addr
是GPIB地址,可以有以下形式:
“n” device n at board n°0 e.g. “1”
“b:n” device n at board n° b e.g. “0:1”
“GPIBb::n::INSTR” (Visa format) device n at board n° b e.g. “GPIB0::1::INSTR”
该类实现了虚属性EnableNotify
,如上所述(默认=false
)。将此属性设置为true
将激活板卡的“Notify
”回调以响应SRQ并订阅设备的notify
事件。每个SRQ回调将调用设备WakeUp()
方法。
NI库提供了两种回调版本:板级回调和设备级回调(后者在提供的示例中最常见)。由于只有一个SRQ线,设备级回调需要启用驱动程序的“自动轮询”功能:然后每次设置SRQ时,驱动程序会自动轮询所有设备以查找其来源。
此代码仅使用板级回调,因此不需要启用自动轮询,因为调用WakeUp
将继续进行轮询(如果启用了轮询;但请注意,即使对于轮询被禁用的设备,您也可以使用EnableNotify
:然后调用WakeUp
将尝试立即读取数据)。该类维护一个列表,其中包含所有已设置EnableNotify
的设备。由于一个板卡只能定义一个回调函数,每个SRQ请求都会调用所有已订阅设备的WakeUp
方法。当然,如果同一块板卡用于多个设备,效率可能会降低,甚至导致驱动程序错误——通常,它应该用于那些对尽快获取数据至关重要的选定设备。
在第一个版本中,我抱怨Notify
功能有时工作不正常,具体取决于硬件配置。此后,我发现问题与NI驱动程序的自动轮询功能(默认激活)的行为有关,该功能似乎会轮询所有设备,无论它们是否订阅了设备级回调(顺便说一句,自动轮询功能需要谨慎使用,参见例如http://www.ni.com/pdf/manuals/321819e.pdf,第7.13节)。在此代码中,已禁用自动轮询(在static
类构造函数中),这样,即使在SRQ启用的仪器与不了解轮询的设备共享总线且预期会影响自动轮询器操作的配置中,Notify
也能很好地工作。
该类添加了以下字段:
public NationalInstruments.NI4882.Device NIDevice;
public NationalInstruments.NI4882.Board NIBoard;
这些可用于调整设备/板卡配置,例如,更改设备的接口超时值。
class GPIBDevice_gpib488
这使用了Measurement&Computing或Keithley板卡以及一些较旧的NI板卡提供的Windows DLL库“gpib488.dll”。此DLL通常提供32位和64位版本(名称相同,但位于不同的Windows目录中)。该类已在Keithley的KUSB-488A板卡上进行了测试。我没有MCC板卡,但所有gpib488.dll函数的签名对两者都完全相同(如果有问题,请告诉我)。对于较旧的NI板卡,有一个名为“NI4882.dll”的等效库,我也对其进行了测试,它原则上有效,但对于某些设备来说似乎有些不稳定,使用Visa显然更可靠。DLL的名称由字符串常量“_GPIBDll
”定义,因此在代码中更改它很容易。然而,对于NI,我注意到许多提供的C/C++编程GPIB示例使用Visa接口而不是这些旧DLL,所以如果您不想使用NINET接口,这可能是个不错的选择。
此库中未提供Notify
功能,因此此处未实现EnableNotify
。
构造函数参数与GPIBDevice_NINET
相同。
该类添加了一个属性:
public int IOTimeoutCode
用于设置/获取设备的接口超时(DLL手册中定义的代码在类中报告为常量)。
class GPIBDevice_ADLink
这使用了ADLink板卡提供的Windows DLL“gpib-32.dll”。
与ADLink的USB-3488A板卡进行了大量测试。
据我所知,此DLL仅提供32位版本(正如其名称所示)。
注意:此DLL的名称与NI软件或MCC旧软件安装的DLL名称相同。在将其安装到Windows目录(64位系统上为Windows/SysWow64)之前重命名此文件,然后在代码中更改字符串常量“_GPIBDll
”可能会更明智。
构造函数参数与GPIBDevice_NINET
相同。
注意:在第一个版本中,该类使用了ADLink库的C#导入模块中提供的驱动程序调用。这些并不都与“标准”调用兼容,其他“gpib-32
”DLL中的调用也一样,但所有标准例程都可以在这里找到,使用DLL浏览器即可。从2017年5月版本开始,我做了一些修改(对类用户透明),以便只使用标准调用。因此,此类也应与NI驱动程序兼容(所以此类可以命名为“GPIBDevice_gpib-32
”,但我不想更改名称),但请注意,错误消息比NI NET库返回的错误消息更不完整。此外,此类代码与GPIBDevice_gpib488
非常相似,除了Notify
功能和驱动程序函数签名的一些微小差异。
该类添加了一个属性 IOTimeoutCode
,其工作方式与GPIBDevice_gpib488
相同。
EnableNotify
现在可用于此类。其实现方式与GPIBDevice_NINET
类似:仅使用板级回调并禁用自动轮询,该类维护一个订阅Notify
服务的设备列表(有关更多详细信息,请参见GPIBDevice_NINET
的描述)。
2018年5月版本已修复bug:ADLink
提供的C#接口类存在一个bug(我在C#和VB版本中都复现了该bug),这是由于原始C头文件中DLL函数签名的错误转录引起的(“long
”类型在C中通常是Int32
,但在C#中,“long
”表示Int64
)。幸运的是,此bug对传递给DLL的数据没有危害,甚至在NET Framework ver. 3.5(VS2008)中可能不会被注意到,因为互操作封送器会修复堆栈(https://msdn.microsoft.com/en-us/library/ff361650(v=VS.100).aspx),但在NET 4.0及更高版本(VS2010及更高版本)中,默认封送器更严格,该bug会触发“PInvokeStackImbalance
”错误。
class VisaDevice
使用Windows DLL“Visa32.dll”。它提供32位和64位版本(名称相同,要强制使用64位版本“Visa64.dll”,您可以更改代码中的常量“_VisaDll
”)。
注意:在32位Windows中,DLL位于Windows/system32;在64位Windows中,32位DLL位于Windows/SysWow64,64位DLL位于Windows/system32。
National Instruments的Visa库提供了一个通用接口,可用于访问各种物理接口(Gpib、串口、USB、TCP/IP等)。Keysight(前Agilent)也有一个等效库,原则上,NI和Keysight版本都是二进制兼容的(两者都可以从其各自的网站下载)。Visa实现了为仪器开发的各种协议,如USBTMC(通过USB)、VXI-11和HiSLIP(通过TCP/IP)。这些协议在许多方面模拟了GPIB的行为,因为它们旨在取代GPIB。我们可以在其中找到轮询状态寄存器、用于服务请求消息的带外信令等的等效项。VXI-11和HiSLIP协议是LXI标准(用于仪器扩展的局域网)的一部分,该标准定义了通过TCP/IP控制仪器的协议。
原则上,Visa处理的所有接口的基本读/写函数都是相同的,但每个接口可能需要特定的配置(选项——在Visa中称为“属性”、错误处理等)。VisaDevice
类提供了一个基本的Visa配置,您可能需要创建派生类来针对特定配置进行调整。它已成功与GPIB(使用NI的GPIB-USB-HS+板卡)、USB和TCP/IP(对于USB,我已用NI和Keysight Visa DLL测试过)进行了测试。对于GPIB,VisaDevice
相较于GPIBDevice_NINET
的一个小优点是它不需要NI的任何外部程序集(所需版本可能取决于编译器、.NET版本等)。
“Notify
”功能现已可用于Visa。该类的属性EnableNotify
的工作方式与GPIBDevice_NINET
类完全相同。
VisaDevice
可用于NI驱动程序的GPIB(然后您无需安装NI Net程序集),此时与GPIBDevice_NINET
相比存在一些细微差别:
- NINET允许创建一个即使设备未连接到系统的设备,而在Visa中,这会导致错误(实际上,错误发生在尝试在启动时清除设备时,可以通过在
VisaDevice
中将常量clearonstartup
设置为false
来禁用此行为)。 - 在
VisaDevice
中,只有少数常见错误会显示完整文本消息,对于其他错误,只报告十六进制错误代码(我只是太懒惰而无法编码所有可能的错误消息)。 - Visa仅提供设备级回调。对于NI GPIB,这些回调依赖于“自动轮询”,因此对于某些硬件配置,
EnableNotify
可能会遇到我以前在GPIBDevice_NINET
中遇到的问题(请参见上面的GPIBDevice_NINET
类描述)。
对于USB,设备必须与“测试和测量类”(USB-TMC)协议兼容(准确地说,完整的GPIB仿真仅在子类USBTMC-USB488中提供,例如,参见https://www.eetimes.com/document.asp?doc_id=1295643)。当连接设备后,它被检测为“测试和测量设备”时,就可以立即使用。如果驱动程序不存在,请参阅http://www.ni.com/tutorial/4478/en/了解创建驱动程序的说明。
对于VXI-11和HiSLIP,使用NI MAX实用程序设置LAN参数可能很方便。
构造函数参数与GPIBDevice_NINET
相同。然而,这里的地址参数格式必须与Visa规范兼容。
for Gpib: GPIB[board]::address::INSTR
for USB: USB[board]::manufacturer ID::model code::serial number::INSTR
for TCPIP: TCPIP[board]::IP address[::LAN device name]::INSTR
此类还添加了设置和获取Visa属性的方法(此处仅限于最常见的属性类型:int
、uint
)以及用于get
/set
设备接口超时值的属性。
C#
public uint SetAttribute(uint attribute, int attrState)
public uint SetAttribute(uint attribute, uint attrState)
public uint GetAttribute(uint attribute, out int attrState)
public uint GetAttribute(uint attribute, out uint attrState)
public int IOTimeout //timeout in ms
VB
Public Function SetAttribute(ByVal attribute As UInteger, _
ByVal attrState As Integer) As UInteger
Public Function SetAttribute(ByVal attribute As UInteger, _
ByVal attrState As UInteger) As UInteger
Public Function GetAttribute(ByVal attribute As UInteger, _
ByRef attrState As Integer) As UInteger
Public Function GetAttribute(ByVal attribute As UInteger, _
ByRef attrState As UInteger) As UInteger
Public Property IOTimeout() As UInteger
class SerialDevice
虽然Visa可用于访问串口,但使用.NET中提供的标准SerialPort
类编写实现更简单(也更有用和高效,因为它不需要Visa资源)。
构造函数
C#
public SerialDevice(string name, string addr)
public SerialDevice(string name, string addr, string termstr, int buffersize)
VB
Sub New(ByVal name As String, ByVal addr As String)
Sub New(ByVal name As String, ByVal addr As String, _
ByVal termstr As String, ByVal buffersize As Integer)
参数
termstr
是“NewLine
”字符串。
地址格式
COM端口,波特率,奇偶校验,数据位,停止位[,换行符]
换行符可以是CR(“\r”)、LF(“\n”)或CRLF(“\r\n”)(对于其他值,请使用构造函数的第二个版本)。
例如
“COM1:9600,N,8,1,CRLF
”
如果在构造函数中定义了“termstr
”,则它将覆盖“addr
”中设置的值。
串口没有轮询功能,因此对于此接口,1)轮询函数在输入缓冲区非空时立即将MAV状态设置为true;2)串口超时设置为非常短的值(几毫秒),这样阻塞命令在等待行完成时不会冻结GUI(这里读取几乎总是会重试多次)。
与其他类一样,当前版本使用异步信令,此处控制它的属性是EnableDataReceivedEvent
,并在类构造函数中默认设置为true
。启用后,它定义了一个SerialPort
类的DataReceivedEvent
的处理器,该处理器调用WakeUp()
以立即中断任何等待延迟。请注意,此事件与GPIB和Visa中的Notify
不同:事件不是为了指示数据已准备好,而是每当数据包到达端口时触发(但是,.NET SerialPort
类不保证每次字符都会被调用,因此默认延迟设置得很短,以防丢失行尾字符)。与所有设备共享单个SRQ线的GPIB不同,每个串口都有自己的处理器,因此它不需要进行大量不必要的轮询来确定事件的来源。
有了此功能,SerialDevice
类将在阻塞和异步查询中尽快提供响应。原则上,没有理由禁用此默认行为——也许除了您期望响应非常长,并且希望避免事件每隔几个传入字符触发一次。
请注意,与GPIB、USBTMC或LXI等旨在统一事物定义的协议不同,串口没有标准的报文协议,因此无法创建像Visa那样适用于所有串口连接的即插即用类。SerialDevice
类实现了一个简单的面向行的协议:每条消息以相同的特殊行尾字符终止,并且每个查询只有一条响应消息。但是,如果您的设备使用不同的方法,例如,没有终止符的固定长度消息,或者混合了终止符和固定长度消息的奇怪语法,或者响应可以使用不同的终止符字符,这取决于命令,那么就需要派生一个类来覆盖“ReceiveByteArray
”方法。在这种情况下,如果此方法需要知道发送的命令是什么,以确定如何格式化响应,我已添加了一个currentactivequery
属性,可以用来访问此信息。
我用Prolific USB-串口转换器(带有四个COM端口)测试了该类。
如果您在使用多个端口时遇到问题,那可能与虚拟端口驱动程序和多线程有关,请阅读最后一节的“查询序列和锁定级别”。
编写实现
创建派生类来创建新实现或为特定配置调整现有实现很容易。有四个abstract
方法需要定义(重写):
C#
protected abstract int ClearDevice(ref int errcode, ref string errmsg);
protected abstract int Send(string cmd, ref int errcode, ref string errmsg);
protected abstract int PollMAV(ref bool mav, ref byte statusbyte,
ref int errcode, ref string errmsg);
protected abstract int ReceiveByteArray(ref byte[] arr, ref bool EOI,
ref int errcode, ref string errmsg);
protected abstract void DisposeDevice();
VB
Protected MustOverride Function ClearDevice_
(ByRef errcode As Integer, ByRef errmsg As String) As Integer
Protected MustOverride Function Send(ByVal cmd As String, _
ByRef errcode As Integer, ByRef errmsg As String) As Integer
Protected MustOverride Function PollMAV(ByRef mav As Boolean, _
ByRef statusbyte As Byte, ByRef errcode As Integer, _
ByRef errmsg As String) As Integer 'poll for status, return MAV bit
Protected MustOverride Function ReceiveByteArray(ByRef arr As Byte(), _
ByRef EOI As Boolean, ByRef errcode As Integer, _
ByRef errmsg As String) As Integer
Protected MustOverride Sub DisposeDevice()
有关参数和返回值的含义,请参见IODevice
类的代码中“interface abstract methods that have to be defined
”注释下的说明。
IODevice
类还定义了一些protected
方法和属性,可以在派生类中调用/应调用:
C#
protected void AddToList(); //register device in "devicelist" shown in DeviceForm,
//this should be called by child class constructors
//(is not called in the base class constructor
//to avoid registering ill-defined objects
//when constructor exception occurs)
protected void WakeUp(); //interrupt waiting for next read or poll trial,
//to be used in interface callbacks
protected IOQuery currentactivequery //to use in implementation methods e.g.
//if the command string is needed to
//determine EOI mode (currently not used)
VB
Protected Sub WakeUp()
Protected Sub AddToList()
Protected ReadOnly Property currentactivequery() As IOQuery
示例:如果您的设备不符合488.2标准关于状态字节位的含义,在最简单的情况下,您只需要更改类的变量MAVmask
(默认值为16,用于轮询位n°5中的状态),如果状态是以更复杂的方式编码的,那么您可能需要重新解释此字节以获得“消息可用”状态,类似如下:
public class MyDevice : GPIBDevice_NINET
{
public MyDevice(string name, string addr) : base(name, addr) { }
protected override int PollMAV(ref bool mav, ref byte statusbyte,
ref int errcode, ref string errmsg)
{
int pollresult = base.PollMAV(ref mav, ref statusbyte, ref errcode, ref errmsg);
if (pollresult == 0) { mav = ...;} //reinterpret the received status byte
return pollresult;
}
}
一些技术细节:查询序列和锁定级别
设备访问受两级锁定机制保护:总线(接口)锁定和设备锁定。
为防止死锁,所有锁在调用用户回调时都会被释放——因此原则上,您可以在回调函数中执行任何操作,不受限制。
设备/接口锁定的理念如下:设备锁定确保不同线程发出的查询序列之间不会发生干扰,即同一设备上的阻塞查询和异步查询之间。接口锁定确保在共享同一锁定对象的设备中,一次只有一个可以发出低级驱动程序调用(当然,这不会抑制通过同一接口的并行查询,因为写/读序列仍然会交错)。请注意,对于现代的Gpib库(如NI NET和Visa库,我没有检查其他库),锁定GPIB接口是不必要的,因为它们被认为是线程安全的。然而,增加这个额外的锁定级别可能是有益的,特别是对于像GPIB这样许多设备共享同一总线的接口:这里通过“Monitor.TryEnter
”在循环中重复进行总线和设备锁定,以防止在总线或设备不可用时阻塞GUI(尤其是在禁用轮询时)。
为了使总线锁定灵活,从索引位于变量的数组中选择相应的锁定对象:
protected int interfacelockid;
此变量必须由子类构造函数设置,索引的允许值为0-99。如果interfacelockid
设置为负值,则不执行接口锁定。
这里的想法是,如果我们有两个不同的接口可以并发使用,它们可以使用不同的锁定对象,这样锁定就不会降低性能。
对于gpib,如果只有一个板卡,那么接口锁定不会减慢系统速度,因为所有设备共享同一总线(并且可以带来更具响应性的GUI的好处,如上所述)。然而,如果有多个板卡由同一驱动程序访问,并且驱动程序是线程安全的,那么最好允许从不同线程同时访问不同板卡,这可以通过为每个板卡设置不同的interfacelockid
值来实现。如果驱动程序不是线程安全的,那么我们必须为使用该驱动程序的所有板卡/设备使用相同的interfacelockid
。
对于通过Visa的USB和TCP/IP以及串口,情况有所不同,因为没有公共总线,这里共享同一个锁给多个设备可能会减慢数据传输速度。Visa是线程安全的,因此无需锁定即可安全地访问驱动程序。SerialPort
类的实例成员不完全是线程安全的,但由于在此代码中,我们从不让两个线程同时与同一个COM端口通信,因此也可以禁用串口接口锁定。但有一个例外:有人报告称,某些USB-串口转换器加密狗具有有bug的虚拟COM端口驱动程序(https://lavag.org/topic/18562-visa-write-read-to-usb-instruments-in-parallel-crashes-labview/),如果出现问题,可能需要为所有与同一虚拟COM端口驱动程序通信的设备定义一个公共interfacelockid
值(注意:我已在禁用接口锁定的情况下测试了SerialDevice
类,通过Prolific USB-串口转换器连接的多个设备,到目前为止我没有发现任何问题)。
在此代码中,interfacelockid
的默认设置遵循以下通用理念:
- GPIBDevice_NINET
interfacelockid=板卡号(假定为线程安全)
- 通过VisaDevice
访问的GPIB
interfacelockid=10+板卡号(假定为线程安全,我假设同一块GPIB板卡不会在同一应用程序中通过Visa和NINET类访问,否则最好设置为与NINET相同的值)
- GPIBDevice_ADLink
:interfacelockid=20(假定为非线程安全)
- GPIBDevice_gpib488
:interfacelockid=21(假定为非线程安全)
- 通过VisaDevice
访问的USB和TCP/IP:interfacelockid=-1(假定为线程安全)
- SerialDevice
:interfacelockid=-1(假定为线程安全)
当然,这些设置可以在相应的类构造函数中轻松修改。
查询序列
查询序列对于阻塞/异步命令是相同的,唯一的区别是 1) 它们在不同的线程上执行 2) 对于在主线程上执行的阻塞命令,允许在等待循环中处理应用程序事件,以免 GUI 冻结(可以通过设置 eventsallowed
= false
来禁用此功能)。
如下所示
- 锁定设备
- 检查自上次操作以来是否已过最短延迟(“
delayop
”),否则等待直到条件为true
(延迟不可中断) - 发送命令(总线在发送期间被锁定)
- 等待延迟“
delayread
”(可以通过调用WakeUp()
的另一个线程中断) - 如果发送成功且需要响应
-
如果轮询已启用(
enablepoll=true
):定期轮询状态字节(每delayrereadontimeout
毫秒
)直到 MAV 位被设置,如果readtimeout
超时或被中止则退出(后续轮询尝试之间的等待可以被调用WakeUp
的任何其他线程中断) - 尝试定期读取(每
delayrereadontimeout
毫秒;总线在每次读取期间被锁定),如果readtimeout
超时或被中止则退出(后续读取尝试之间的等待可以被调用WakeUp
的任何其他线程中断) - 如果
checkEOI
已启用:如果不是 EOI,则重复读取直到 EOI 被设置,将新数据附加到输入缓冲区
-
- 如果任何
gpib
函数返回错误:清除设备,如果设置了showmessages
标志,则显示消息 - 解锁设备
- 对于异步线程,如果定义了用户回调函数:向 GUI 线程发送消息以调用它(如果设置了
cbwait
标志,则等待直到回调返回) - 如果发生错误且
retry
已启用:等待延迟delayretry
(不可中断),然后重复整个查询过程,除非被用户中止
在这里,接口在每次 I/O 操作期间都会被锁定,但在写入和读取之间不会,然而设备在整个查询期间都被锁定。因此,当一个线程正在等待来自另一个设备的响应时,其他异步线程可以向它们各自的设备发送命令。交错会自动实现。
同样,出于相同的原因,如果设置了“eventsallowed
”标志(默认),那么阻塞调用也是可能的:此标志允许在阻塞调用的延迟和等待循环期间处理应用程序事件:如果定时器中有阻塞调用,那么可以在写入和读取之间处理定时器事件(如果 eventsallowed
设置为 false,则禁用此功能,但此时 GUI 在阻塞调用期间可能会冻结!)。
在此必须明确一点,基于线程间同步的设备锁定不会阻止不同进程同时查询同一设备(例如,如果一个 C# 应用程序与一个 Labview 程序并行运行)。对于这种情况,一些 GPIB 驱动程序提供自己的锁定指令以确保进程间同步,但在当前版本中不使用这些指令。
演示
请参阅随附的“test”项目
历史
2017年1月26日
- 提交第一个版本
2017年2月13日
- C# 版本:在 VisaDevice.cs 中修正了一个地址格式问题(错误地,我包含了一个版本,它试图像其他类一样为 GPIB 格式化地址,现在已删除格式化,因此它将正确适用于所有 Visa 地址格式,如 USB 等)
- 其他文件中的小更新
2017年4月10日
-
为时间关键型应用程序添加了从驱动程序进行异步回调的可能性。请参阅 **异步接口回调** 部分,查询序列以及修改后的实现描述:
GPIBDevice_NINET
、SerialDevice
DevicesForm
更新方法已改进- 对
GPIBDevice_ADLink
的小修改,请参阅类描述 - 测试窗体中的小更新
2017年5月24日
- 现在已为
VisaDevice
实现设备服务请求的异步回调,请参阅类描述了解详情 - 类
GPIBDevice_NINET
:驱动程序配置已修改(禁用自动轮询),以便异步回调更可靠(请参阅类描述) - 主类
IODevices
中的小更新(错误消息略有更改;重新设置用于异步回调的命令发送;在 C# 版本中,一些参数从“ref
”更改为“out
”) - 测试窗体已适应更改
2018年2月27日
- 许多小的更正和更新,例如,修正了一些错误处理/消息不一致的地方,在主类中实现了析构函数,MAV 位的掩码,
EnableNotify
作为虚拟属性,用于接口超时设置的属性(注意:但所有类都向后兼容任何为前一版本编写的代码) - 文章中的更新
- 为
GPIBDevice_ADLink
实现了通知 - 修改了
interfacelockid
的默认设置 - 测试窗体已适应更改
2018年5月23日
- 类
GPIBDevice_ADLink
:修复了某些 DLL 签名中的一个错误,该错误在 NET 4.0 及更高版本下会导致互操作封送器错误(请参阅类描述) - 文章中的小更新