【摘译+整理】System.IO.Ports.SerialPort使用注意
远古的一篇博客,内容散落于博文和评论 https://sparxeng.com/blog/software/must-use-net-system-io-ports-serialport
C# 和 .NET Framework 提供了一种快速的应用程序开发,非常适合需要随着硬件设计的发展跟踪不断变化的需求的早期开发。在大多数方面都很理想。但.NET 附带的 System.IO.Ports.SerialPort
类是一个明显的例外。委婉地说,它是由计算机科学家设计的,远远超出了他们的核心能力领域。他们既不了解串行通信的特征,也不了解常见的用例。在发布之前,它也不可能在任何真实场景中进行测试,而不会发现漏洞,这些缺陷会使 System.IO.Ports.SerialPort
(以下简称IOPSP)的可靠通信成为真正的噩梦。(StackOverflow 上的大量证据证明了这一点)。
更令人惊讶的是,当底层kernel32.dll API 非常好时,还会发生这种级别的问题(我在使用 .NET 之前使用过 WinAPI)。.NET 工程师不仅没有设计出合理的接口,还选择无视非常成熟的 WinAPI 设计,也没有从二十年的内核团队串行移植经验中吸取教训。
下面列出其可靠和不可靠的成员列表:
- event DataReceived (100%冗余,也完全不可靠)
- BytesToRead 属性 (完全不可靠)
Read
,ReadExisting
,ReadLine
函数(处理错误完全错误,并且是同步的)PinChanged
event (顺序完全不能保证)
可以安全使用的列表:
- 属性:
BaudRate
、DataBits
、Parity
、StopBits
,但仅在打开端口之前。并且仅适用于标准波特率。 Handshake
属性- 构造函数、 PortName属性、 Open函数 IsOpen函数、 GetPortNames函数
还有一个没人使用的成员,因为 MSDN 没有给出任何示例,但对你绝对是必不可少的
BaseStream
唯一正常工作的串行端口读取方法是通过 BaseStream
访问。
以下示例是接收数据的错误方式:
port.DataReceived += port_DataReceived;
// (later, in DataReceived event)
try {
byte[] buffer = new byte[port.BytesToRead];
port.Read(buffer, 0, buffer.Length);
raiseAppSerialDataEvent(buffer);
}
catch (IOException exc) {
handleAppSerialError(exc);
}
下面是正确的方法,它与基础 Win32 API 的使用方式相匹配:
byte[] buffer = new byte[blockLimit];
Action kickoffRead = null;
kickoffRead = delegate {
port.BaseStream.BeginRead(buffer, 0, buffer.Length, delegate (IAsyncResult ar) {
try {
int actualLength = port.BaseStream.EndRead(ar);
byte[] received = new byte[actualLength];
Buffer.BlockCopy(buffer, 0, received, 0, actualLength);
raiseAppSerialDataEvent(received);
}
catch (IOException exc) {
handleAppSerialError(exc);
}
kickoffRead();
}, null);
};
kickoffRead();
从 .NET 4.5 开始,可以改为调用 ReadAsync
BaseStream 对象,该对象在内部调用 BeginRead
和 EndRead
。
或者就直接调用 Win32 API的方式。
第一个例子的问题在于:
第一个也是最严重的是 DataReceived
在线程池线程上触发,并且可以再次触发,而无需等待上一个事件处理程序返回。因此,它会导致你进入竞争条件,当你去读取缓冲区时,比 BytesToRead
承诺的要少,因为事件处理程序的另一个实例同时读取它们。应用程序程序员可以通过显式同步来克服这个问题。
但同步不会解决实现本身中存在的竞争条件。BytesToRead
调用 ClearCommError
以获取缓冲区级别,并丢弃其他所有内容。但是 ClearCommError
是对串口寄存器错误标志位的原子交换——你只会看到一次,然后就会清除他们。框架中正在查看这些标志的其他代码以触发 PinChanged
和 ErrorReceived
事件由于 BytesToRead
(查看并清除寄存器) 会忽略它们,因此事件会丢失。事实上,ErrorReceived
事件的 MSDN 页面显示“由于操作系统决定是否引发此事件,因此不会报告所有奇偶校验错误。这是一个彻头彻尾的谎言 — 事件丢失发生在 BytesToRead
和 BytesToWrite
的 getter 函数中。
译者注:这段原因写在作者和游客的讨论中,感兴趣的可以看看原文。如果写过STM32就能理解作者的意思(STM32的有些寄存器在读后就会清除,这样就会导致丢失了一些事件)。作者表示如果你对串口通信的要求很高,(高性能/低时延/错误等)你就应该使用Win32 API或者 p/invoke 或 C++/CLI。(作者列了写商业库说也能用,感兴趣的在原文评论区自行找一下)。如果你对串口通信的要求不高,其实DataReceived
的方式其实也能用。