本文相关代码下载:
NetSerialComm.exe (89KB)
导引:在
.NET
环境下编写与
RS252
串口通信的应用程序的唯一方法,就是引用过时了的并且有点限制的
MSComm ActiveX
控件。这篇文章介绍了用
C#
安全代码编写一个多线程的,且时尚的与
RS232
通讯的基础类库。这个类库使用平台调用服务(即
Platform Invocation Services
)来与
Win32 API
直接交互。程序员可以通过继承在任何
.NET
语言下使用这个类库
;
这个文章还探讨了一些用
C#
和
Visual Basic .NET
写的示例程序。
微软
.NET
框架类库(
FCL
)提供了相当全面广泛的功能来替代在
Win32@API
编程下原有的功能,特别是
C#
与
Visual [email protected]
语言的可互访性。尽管如此,
RS232
串口通讯是
.NET
框架类库是明显未被涉及的方面之一。从而很正常的,很多人就把这些接口当成了遗弃物。目前,你还是通过软件层与串行调制解调器进行通讯,比如
TAPI
与
PPP
。其它从前使用这些接口的设备现在正在向
USB
接口移植。不过,一些专业的
RS232
设备的驱动程序仍然有通讯的需要,比如
GPS
接收器,
barcode and swipe card readers,
可编程控制器和一些可预见的程序员将来继续使用的设备。(关于
RS232
接口的规格信息,可以参看
"Hardware Specs".)
平台调用服务
(P/Invoke)
是能够使用托管的
CLR
代码调用非托管
DLLs
的
.NET
技术,包括那些实现
Win32 API
的
DLLs
。在这篇文章里,我将用
C#
把与
RS232
通信的
API
封装到
CLR
托管的代码中去。生成的基类库将使用
.NET
语言开发特定设备的驱动变得相对容易。完整的代码和示例可以从这篇文章顶部的链接中下载到。
设计原理
在把
Win32
串口通讯功能封装到托管类的时候,这里至少有四种实现方式让你选择:
1.
使用
P/Invoke
把
API
函数、常数、结构作为静态成员封装到托管类中。虽然我在里面使用了这种方法,但没有这个类暴露给程序员。
2.
写一个流的处理角色。这是
.NET
框架对文件、控制台、网络通讯的一般地、可扩充的提取。咋一看,这个很有吸引力,但近距离审视的时候,这个更适用于传统的调制解调器,而不适合于现在基于命令响应语法的设备。
3.
做一个直接替换
MSComm OLE Control Extension(OCX)
控件的替代品。换句话说,新建一个封装了
API
文件处理并提供许多基本的方法和事件(比如,
Open,Close,Read,Write
等等)。你可以在应用程序类里初始化这个类库里的一个对象来达到重用的目的――那就是说,通过
COM-style
的集合。
4.
写一个应用程序需要继承的基类。这是一个充分体现
.NET
优点――运行时对不同语言继承的无关性――的面向对象的方法。这些基础的方法被继承进应用程序对象中,虚方法将被使用,而不是使用事件。这个应用程序对象将巧妙地提供一个适用于真实
RS232
设备公共接口(比如,一个
GPS
接收器驱动可能拥有一些关于经度和纬度的公共属性)。
我将采用第四种方法。这个类库将会包含两个被生明为抽象类的基类(它们不能被示例化),但我将使用继承来把它们作为实现某些特定应用的基类。图
1
表明了这种继承的层次关系。
图
1
继承层次
第一个库类,
CommBase
,对数据格式化、更容易开启与关闭通讯接口、发送与接收字节数据、输入与输出交互的控制等都不提供任何实现。
第二个库类,
CommLine
,继承自
CommBase
,并且做了两个实现:接收与发送的字节是
ASCII
编码并使用一个保留的
ASCII
控制编码来标记数据行数的可变长度,能够接收与传输字符串。当然,这个模型是可扩展的
;
比如,你可以编写可选的
Unicode
版的
CommLine
。
使用基类
两个应用程序的例子,
BaseTerm
和
LineTerm
,可以下载的到。他们可以用于与任何串口设备进行一般用途的交流,包括调制解调器。我先从一个用户的观点来简单的看一下
BaseTerm,
然后再更细致地分析一个
LineTerm
的源代码。
图
2 BaseTerm
BaseTerm
(参看图
2
)是一个完全基于
Windows@Form
的应用程序,它继承自
CommBase
并提供一个基于字节的可调节终端。点击
Settings
按纽可以打开了一个对话框来对通讯设置的全部参数进行设置(参看图
3
)。这个窗口上菜单可以帮助用户以结构化的
XML
文件来保存或加载这些设置参数,也保存大量用于普通流控制模式的设置。提示解释了各个设置项的用法。一旦保存为
XML
,当你再次启动这个程序的时候,你可以在命令行格式下设定这个文件。一旦连上线,打出的字符就可以立即被传送到远端设备中支。键盘上的按键发送合适的
ASCII
字节,如果你想发送键盘上没有的编码,你可以使用“
escape facility”.
图
3 Comm
设置
可以通过输入
<
符号来开启“
escape”.
然后,输入任一个
ASCII
控制码名字或一个位于
0
或
255
之间的十进制数。可通过输入一个
>
符号来结束这个“
escape”,
它可以把一个合适的
ASCII
码立即发送过去。当需要传输
<
符号时可以通过把它输入两次的方式进行。你可以在设置对话框中标记为
”Xon Code”
的下拉框中查看所有有效的
ASCII
控制符的名称,站点
http://www.asciitable.com/
还提供其它有用的信息。在终端窗口上大的文本框上将显示所有的
ASCII
形式或
16
进制形式(没进一步分析)的字节。
你可以在接收到一个规定
ASCII
字符或一定数量的字符后使用显示设置对话框来中止接收行。点击状态按纽将提供所传输以及接收队列的情况。
LineTerm使用CommLine作为它的基类,并在源码中声明了如何使用这个库。因为没有创建用户界面用于设置,你需要在Visual Studio .NET环境下来运行它。在Visual Studio .NET中,建立一个新的Visual Basic控制台应用程序。从项目中移除默认的模块。拷贝LineTerm.vb,CommBase.dll和CommBase.xml三个文件到项目文件夹(其中的XML文件这个库文件提供了智能提示信息)。使用项目浏览器中的添加现有项把LineTerm.vb添加到项目中,并通过添加引用把CommBase.dll添加到引用中。现在你可以编译并运行这个项目了。
Imports JH.CommBase
Public Class LineTerm
Inherits CommLine
Public Sub SendCommand(ByVal s As String)
Send(s)
End Sub
Public Sub TransactCommand(ByVal s As String)
Dim r As String
r = Transact(s)
Console.WriteLine("RESPONSE: " + r)
Prompt()
End Sub
Public Sub Prompt()
Console.WriteLine("Type string to send and press ENTER.Empty string
to close comm port.")
End Sub
Protected Overrides Function CommSettings() As CommBaseSettings
Dim cs As New CommLineSettings()
cs.SetStandard("COM1:", 19200, Handshake.none)
cs.rxFilter = New ASCII() {ASCII.LF, ASCII.SOH}
cs.rxTerminator = ASCII.CR
cs.txTerminator = New ASCII() {ASCII.CR}
Setup(cs)
Return cs
End Function
Protected Overrides Sub OnRxLine(ByVal s As String)
Console.WriteLine("RECEIVED: " + s)
Prompt() End Sub Protected Overrides Sub OnTxDone()
Console.WriteLine("TRANSMISSION COMPLETE")
Prompt()
End Sub
End Class Module Module1 Sub Main()
Dim t As New LineTerm()
Dim c As String
Console.WriteLine("Press ENTER to open com port")
Console.ReadLine()
If t.Open() Then
Console.WriteLine("COM PORT OPEN")
t.Prompt()
While True
c = Console.ReadLine().Trim
If c = "" Then Exit While
t.SendCommand(c)
't.TransactCommand(c)
End While
t.Close()
End If
Console.WriteLine("COM PORT CLOSED")
Console.WriteLine("Press ENTER to close program.")
Console.ReadLine()
End Sub
End Module |
图四
LineTerm
示例代码
图
4
向我们展示了这个例子的完整源代码。在这第一行,我引入了库的命名空间。然后我建立了一个新的类,
LineTerm
,它继承自
CommLine
。它提供了打开和关闭这两个公共方法(实际上是继承自
CommBase
类),还有保护型方法发送,且我把它做成公共的当作为
SendCommand
的时候。在我的新类里,我重载了基类中大量的虚方法。在打开窗口配置通讯端口时调用了
CommSettings
方法
;
它会返回一个已经初始化了的
CommBaseSettings
对象。
这里我实际上使用了
CommLineSettings
,因为它继承自
CommBaseSettings.
在这个方法的最后两行,首先我传递了一个继承自
CommLine
的对象给
Setup
方法,并把它返回给
CommBase
类。所有的设置项都是公共的成员,可以直接被设定,但这里也有一个辅助方法,
SetStandard,
它可以把
CommBase
类自动配置成最常用的配置。你也许需要编辑这个方法及终端线、过滤成员的参数来适合你的可用于测试的设备。
应用程序的主方法只是简单的创建了一个我的类的实例,并调用了
Open
方法,并提供了一个可用于发送字符串和显示所收到的字符串的命令行界面。共有两个方法来完成这个,阻塞(
blocking
)和非阻塞(
non-blocking
)。使用
SendCommand
来启动非阻塞通讯。这个方法立刻返回,不久发送结束,重载的
OnTxDone
方法将报告结果。稍后,当远端设备完成了一个响应信号的输入,重载的
OnExLine
方法会在控制台上显示出结果。此时,主进程等待用户输入,但也可能它正在进行其它的工作。如果你注释掉
SendCommand
并用
TransactCommand
来替代,将会同样地使用阻塞式通信。此时,主进程会一直处于阻塞模式,直到出现有效地回应。你可以静静地等着从
OnTxDone
方法返回的结果信息,但代替从
OnRxLine
方法返回的收到的消息,你将看到从
TransactCommand
方法返回的回应的信息。
图
5 GPS
的流控制
在一个真正的应用程序中,比如
GPS
接收器的驱动程序,你不可能让它像我在示例中所仅实现的
Send
和
Transact
公共方法一样。相反,你需要提供那些能够表现这个设备功能的所有公有方法和属性(比如,速度和强度属性,或者比如
PositionChanged
的事件)。这些方法必须集合必要的命令。使用
Transact
方法,并把回应释放转化出返回值。图
5
就是介绍用于这类设备的流控制的。
发送
在串口通信中,在大多数情况下,发送信息比接收信息容易多了。对于接收信息,你也许正对远端设备胡思乱想,然而对于传输,你仍然可以控制时间。尽管如此,一般的位于
2
到
20000
波特的传输速率与计算机以千兆赫的速率相比,你可能不想待在一边等待传输的完成。
Win32 API
把串口通信看成对文件操作的一个特例,并使用了并称作与
I/O
交迭的技术来提供非阻塞式操作。
CommBase
类提供了
Open
的公共方法,它使用了
Win32API
的
CeeatFile
方法来打开了一个串口,并把操作系统处理的结果作为一个私有成员变量存储起来:
hPort = Win32Com.CreateFile(cs.port, Win32Com.GENERIC_READ | Win32Com.GENERIC_WRITE, 0, IntPtr.Zero, Win32Com.OPEN_EXISTING, Win32Com.FILE_FLAG_OVERLAPPED, IntPtr.Zero);
|
第一个参数是
string
类型的端口名,常常是
COM1;
或者是
COM2;
但在这里,你可以使用任何名字,所以我使用了名字而不是一个数字。我还没有方法来决定一串有效的端口名,所以选择一个可以让调用者尝试可以打开任一端口,并接受可以失败的事实。当端口存在但正被另一个应用程序使用时也可能失败。我使用
FILE_FLAG_OVERLAPPED
来描述发生这个文件句柄上的所有操作为非阻塞的,而其它的参数对于串口通信来说只是个样子而已。
Win32Com
是一个封闭了
API
函数、结构、常数的容器型的辅助类,我将通过
P/Invoke
调用它。
CreatFile
在
C#
中像如下进行声明:
[DllImport("kernel32.dll", SetLastError=true)] internal static extern IntPtr CreateFile(String lpFileName, UInt32 dwDesiredAccess, UInt32 dwShareMode, IntPtr lpSecurityAttributes, UInt32 dwCreationDisposition, UInt32 dwFlagsAndAttributes, IntPtr hTemplateFile);
|
各种常数也在这里被定义,比如:
internal const UInt32 FILE_FLAG_OVERLAPPED = 0x40000000;
|
因为现在几乎没有工具支持
P/Invoke
,我不得不自己手动来定义这些。关键的资源包括
Win32
文档和用
C++
语言的提供的头文件。
Visual Studio .NET
中出色的文件搜索引擎对于在头文件中搜索定义是非常有用的。
(
我仅用这些作文档用而已,你在编译库文件的时候是不需要这些的。
)
关于
interop marshaling
的充分讨论,关于把托管数据类型翻译为非托管的
API
所使用的
C
语言定义的部分,不在本文讨论的范围之内。然而,你可以从
Open
方法中的另一段代码片段来理解这其中到底发生了什么:
wo.Offset = 0;
wo.OffsetHigh = 0;
if (checkSends)
wo.hEvent = writeEvent.Handle;
else
wo.hEvent = IntPtr.Zero;
ptrUWO = Marshal.AllocHGlobal(Marshal.SizeOf(wo));
Marshal.StructureToPtr(wo, ptrUWO, true);
|
这里,
wo
是
Win32Com.OVERLAPPED
类型的局部变量,
ptrUWO
是一个
IntPtr
类型的私有类变量。
Marshal
是
System.Runtime.InteropServices
提供的用于对
interop marshaler
进行存取的全局对象。在这个代码里,当调用外部函数时,我手动处理那些
marshaler
平常自动处理的事务。首先,分配一块大小合适的非托管内存,继而把托管结构的内容拷贝到其中,根据需要重新对内存进行分配。在这个功能调用之后,
marshaler
将使用
Marshal.PtrToStructure
来显示拷贝的内容,然后
Marshal.FreeHGlobal
将释放内存。因为
API
使用
OVERLAPPED
结构的特殊方式,我手动来完成这个操作。我将在
WriteFile
方法中对它设值,但在这个调用返回时,操作系统将继续使用它。
不久,我将调用
GetOverlappedResult
方法,再次描述同样的的结构。如果我这些留给自动配制,这任务在两次调用间的非托管内存将被再次分配。如果这样的话,
unmarshaling
就不再是必须的了,因为这些域就再也不需要被存取了。尽管如此,当端口关闭时这些内存必须被释放掉:
if (ptrUWO != IntPtr.Zero) Marshal.FreeHGlobal(ptrUWO);
|
用这些替换的基础代码,实际上发送一组字节就是非常直接的了:
if (!Win32Com.WriteFile(hPort, tosend, (uint)writeCount,
out sent, ptrUWO))
if (Marshal.GetLastWin32Error != Win32Com.ERROR_IO_PENDING)
ThrowException("Unexpected failure");
|
参数
tosend
是字节数据的指针
;writeCount
是字节数据的长度
;
参数
sent
将返回实际发送的字节的数量
;
参数
ptrUWO
是先前创建的非托管版的
OVERLAPPED
指针。正常情况下,这个方法将返回
false
,错误代码将是
ERROR_IO_PENDING.
这是一个表明操作因为排队而不能立即被执行的虚假错误。其它的错误码表明相应操作是不能进行排队队列。由于串口硬件的缓存功能及发送短的字符串,这个操作也许可以立刻被完成,这样的话这个方法将会返回
true.
在发送新数据之前,对先前
Send
方法的结果将会进行出错条件及超时值进行检查。(奇怪地是,
API
把挂起的操作作为错误来处理,但是超时也许是非常正常的――侦测的唯一方式发送少量的几个字节而不是排队。消除这种异常是写这个封装库的一个乐趣!)尽管我允许多个发送的挂起,其中每一个都拥有自己的
OVERLAPPED
结构,它将会增加大量的复杂性。代替这个的是,我已经阻塞了并发的
Send
直到先前的一个完成。如果阻塞是一个问题,可以通过设置
checkAllSends
成员为
false
来关闭这种功能,在这种情况下,
OVERLAPPED
结构可以被重用,且不能保证所有错误和超时可以被捕捉到。
接收
也许你已经猜到,接收数据只是简单的调用了
ReadFile
这个
API
方法。就和先前所提到的,难点不是在接收上,而是在何时接收。为了避免应用程序员不断地检查数据,一些回调形式的设置就是必须的了。工作线程调用的虚方法可以实现这种功能。
CommBase
在接收到每一个字节的时候就调用一个虚方法。这个方法是重载自
CommLine
类的用来缓冲字节的,并且当线路终端连接器接收到时调用另一个虚方法。
我在
Open
方法里利用下面的代码创建了第二个运行线程来完成这个工作:
rxThread = new Thread(new ThreadStart(this.ReceiveThread));
rxThread.Name = "ComBaseRx";
rxThread.Priority = ThreadPriority.AboveNormal;
rxThread.Start;
Thread.Sleep(1);
|
在私有方法
ReceiveThread
中启动一个新线程来运行这段代码。所需的最后一行代码令人我很是吃惊
;
我假使这个新的拥有更高优先级的将取代
Start
命令里中的原线程。某些情况下却不是,这就引起了麻烦,因为当它第一次需要被调用的时候,工作线程并不常常是准备好了的。第二次试验的时候,我使用了
Sleep(0)
,因为在文档中是建议这种取得运行权时不要浪费任何时间(几乎是一毫秒的优势缘故),但实际上这根本不起任何作用。
ReceiveThread
是一段死循环代码,仅有一种情况可以打破这个死循环。我使用下面这行代码在关闭端口时终止线程:
在这个线程里,它抛出了一个
ThreadAboutException
异常,通过用于清理的
catch
子句捕捉并结束它。
Finally
子句也会被使用,但这样的话,也就没有什么分别了,因为只有通过一个异常才能使其退出。
1
private
void
ReceiveThread()
{
2
3 byte[] buf = new Byte[1];
4 uint gotbytes;
5
6 AutoResetEvent sg = new AutoResetEvent(false);
7 Win32Com.OVERLAPPED ov = new Win32Com.OVERLAPPED();
8 IntPtr unmanagedOv = Marshal.AllocHGlobal(Marshal.SizeOf(ov));
9 ov.Offset = 0;
10 ov.OffsetHigh = 0;
11 ov.hEvent = sg.Handle;
12 Marshal.StructureToPtr(ov, unmanagedOv, true);
13
14 uint eventMask = 0;
15 IntPtr uMask = Marshal.AllocHGlobal(Marshal.SizeOf(eventMask));
16
17
18
19 try
20 {
21 while(true)
22 {
23 if (!Win32Com.SetCommMask(hPort, Win32Com.EV_RXCHAR))
24 {
25 throw new CommPortException("IO Error [001]");
26 }
27
28 Marshal.WriteInt32(uMask, 0);
29 if (!Win32Com.WaitCommEvent(hPort, uMask, unmanagedOv))
30 {
31 if (Marshal.GetLastWin32Error() == Win32Com.ERROR_IO_PENDING)
32 {
33 sg.WaitOne();
34 }
35 else
36 {
37 throw new CommPortException("IO Error [002]");
38 }
39 }
40 eventMask = (uint)Marshal.ReadInt32(uMask);
41 if ((eventMask & Win32Com.EV_RXCHAR) != 0)
42 {
43 do
44 {
45 gotbytes = 0;
46 if (!Win32Com.ReadFile(hPort, buf, 1, out gotbytes, unmanagedOv))
47 {
48 if (Marshal.GetLastWin32Error() == Win32Com.ERROR_IO_PENDING)
49 {
50 Win32Com.CancelIo(hPort);
51 gotbytes = 0;
52 }
53 else
54 {
55 throw new CommPortException("IO Error [004]");
56
57 }
58 }
59 if (gotbytes == 1) OnRxChar(buf[0]);
60 }
61 while (gotbytes > 0);
62 }
63 }
64 }
65
66 catch (Exception e)
67 {
68 if (uMask != IntPtr.Zero)
69 Marshal.FreeHGlobal(uMask);
70
71 if (unmanagedOv != IntPtr.Zero)
72 Marshal.FreeHGlobal(unmanagedOv);
73
74 if (!(e is ThreadAbortException))
75 {
76 rxException = e;
77 OnRxException(e);
78 }
79 }
80}
81
图
6
接收线程的简单版
图
6
是一个简化版的
ReceiveThread
。
SetCommMask
表明当一个新字节到达的时候我希望被通知到。
WaitCommEvent
可能返回
true
,在这种情况下已经有一个或更多的字节处于队列中。如果返回附带错误码的
ERROR_IO_PENDING,
我会挂起这个线程直到有一个字节到达。被传递给
WaitCommEvent
的
OVERLAPPED
结构包含一个到
AutoResetEvent
的句柄,它被当作有字节到达的信号。当我执行
AutoResetEvent
的
WaitOne
方法时,执行被挂起直到这个事件被触发。
不管
WaitCommEvent
立即返回
true
还是信号不久完成,
eventMask
变量有一位用于标识
SetCommMask
正常被引发所需的条件(在实际代码中,我也描述了一些其它的家务管理的条件)。
注意我同样为
eventMask
使用了手动排列技巧就像先前在
OVERLAPPED
中描述的一样。我猜测这也许是没必要的,也许自动排列也可以,但在文档中没有精确的描述,因为这样做更安全一些总比遗憾要好的多。用托管变量替换无序的指针作为引用参数好像有用,但那可能只是因为内存没有被重用而已。因为依赖于时间,不只是一个字符要排队,因此每次使用
ReadFile
来排出一个字节,重复使用,并每次使用一个字符来调用虚方法
OnRxChar
。当我接收到
ERROR_IO_PENDING
错误编码时,我就调用
CancelIo
方法,从而避免等在这里
;
我想在
WaitCommEvent
循环等待。
在使用工作线程,错误处理和异常需要好好地处理。任何发生在
ReceiveThread
里的未处理异常,及任何调用它的虚方法,以及由以上调用的方法或引发的事件将会级联下去并由
catch
子句捕获处理。如果产生的异常不是
ThreadAbortException
异常,那么它就存储在
CommBase
类作为一个私有成员,并且这个线程将被中止。下次在主线程里程序代码将调用一个方法然后再引发一个异常,端口就会关闭。这充分利用了内置异常结构的优点,当引发了一个一般性“接收线程错误”异常时,它里面就包含了存储在里面的原线程。
ThrowException
是继承类里的提供的一个辅助方法;它通过它所调用的线程来调节它的行为。
配置和其它细节
我从
CommBaseSettings
辅助类对象读取所有配置。
Open
方法通过调用虚方法
CommSettings
来获得这个对象,并把所有的置拷贝到
API
结构中去。
CommBaseSettings
类还提供了用于保存和覆盖配置到
XML
配置文件的方法,及大量地应用这些一般性配置。我利用窗口上的智能帮助提示为配置提供了帮助文档。因为继承类提供了自己的继承自
CommBaseSettings
类配置类,这种配置提供了一种可扩展性基础配置结构。通过这种方式我继承了
CommLineSettings
类,为
CommLine
类提供了额外的配置。
共有三个
API
方法用于配置通讯协议:
SetupComm,SetCommState
及
SetCommTimeouts
三个方法。
SetupComm
方法需要接收缓冲的大小及传输队列。正常情况下你可以把这些设置为
0
或根据操作系统决定,但对一些文件传输和简单应用程序,可调节的所需的大小是值得的。系统不一定能满足这种需要
;
在
Windowns XP
里,这就好像动态传输队列,并仅接收队列的长度是必须的。
SetCommState
方法在一个称为设备控制块(
DCB
)的结构里提供了波特传输率、字格式及握手设置等配置信息。
SetCommTimeouts
在
COMMTIMEOUTS
结构里提供了三个接收和两个传输超时值。接收超时值对我所选择的设计没有用处,因为单个字符是异步处理的。如果接收超时时间是必须的,那它必须在一个高的水平上实现(比如,
CommLine
为它的传输方法提供了一个超时时间)。传输超时时间很有用,特别对于多字节传输。
Send
方法里的字节的数量由
sendTimeoutMultiplier
方法啬,然后
sendTimeoutConstant
被附加到这并提供以微秒为单位的总时间。
一旦端口被打开并被寝化里,
Open
方法调用了一个
AfterOpen
虚方法,它将被重写来检查到远程设置的连接状态且尽可能地配置它。如果这个返回
false
,端口将再次被关闭且
Open
方法自己将返回
false.
如果需要的话,还有一个
BeforeClose
方法来关闭远程设备。
CommB ase
还提供了两个重载版本的
Send
方法,一个提供了一个字节数组为参数另一个提供了另一个单独的字节作为参数。
CommLine
提供了第三个版本的
Send
方法,用字符串作为参数。在进行合适的数据转化后,所有这些最终使用字节数组版本的方法。还提供了一个以单个字节为参数的
SendImmediate
方法。它将在传输队列里将比其它字节前面传输这个字节,并且对实现自定义流控制模式是非常有用的。它还提供了一些用于传输请求、数据终端准备输出插脚及把
TX
输出到暂停条件。输入插脚-
Clear-to-Send(CTS),Data Set Ready(DSR),Received Line Signal Detector(RLSD),
以及
Ring Detect-
可以使用
GetModemStatus
来直接读,当任意输入或输出插脚改变状态时,虚方法
OnStatusChange
将被调用。
GetQueuesStatus
方法将返回一个
QueueStatus
对象,并给出传输的大小、内容、接收队列以及如果必要的话,流控制条件现在是块传输。
结论
我使用
Platform Invocation Services
来填补了
FCL
功能上的一个空白。但这表明确是一个不平凡但非常可行的锻炼。所存在的绝对多数困难在于还没有用于
P/Invoke
的完全的工具及文档支持。
最后,我做一下总结。作为这个项目的一部分,并在我考虑
ManualResetEvent
和
AutoResetEvent
框架类已经封装了所有我需要的功能之前,我写了和测试了所有的对于
Win32 Waitable Events API
的完全封装。记住:当时候,你仅需要把你所有的时间都花在写全新的类而不是凑合使用已经存在的你所需的东西。在从新改造之前先检查一下你的硬盘。基于这个原理,我希望这些基类能帮助其它的程序员把
RS232
设备通讯带入
.NET
世界。