正确使用微软自带的串口类库 SYSTEM.IO.PORTS.SERIALPORT

2023-07-05  乐帮网

.net

作为一名嵌入式开发人员,我主要使用.Net编写的桌面软件实现外围设备的配置和数据下载,所以对串口使用频率也高。更多的场景是USB虚拟串口通讯,也有一些是USB PCI总线上真实串口。 通过串口线连接调试接口看数据是很不只管的,所以获得与自定义PC应用程序的串行数据通信对于分析数据质量和提供硬件设计的反馈至关重要。 C#和.NET框架提供了一个快速开发类库,对于需要跟踪硬件设计发展过程中的要早期开发来说是十分便捷的,这个基本上是够用了。

.NET中的System.IO.Ports.SerialPort类是一个明显的例外。 说得温和一点,它是由计算机科学家设计的,远远超出了他们的核心能力范围。 他们既不了解串行通信的特点,也不了解常见的用例,这一点很明显。 在发布之前,它也不可能在任何现实世界的场景中进行测试,最后稀少的文档使得驾驭它变得更困难,使用System.IO.Ports.SerialPort(以下简称IOPSP)做可靠通信时真正成为每个开发者的噩梦。 (StackOverflow上的大量证据证明了这一点,这些设备在终端可以正常工作(C、C++时正常),但使用.NET读取串口时不行,因为IOPSP强制设置某些参数,尽管它们不适用于虚拟端口,并在失败时关闭端口。 在IOPSP初始化过程中,没有办法绕过或忽略这些设置的失败)。

更令人吃惊的是,在Windows底层的kernel32.dll APIs 对串口的控制已经非常成熟的情况下,.Net的封装却十分的失败(我在使用.NET之前就使用过WinAPI,当我想使用一个.NET没有封装的功能时仍然如此,其中特别包括设备枚举)。 .NET的工程师们不仅没有设计出一个合理的接口,他们还选择无视已经非常成熟的WinAPI设计,也没有从20年的内核团队的串口经验中学习。

未来的一系列文章将介绍一个合理的串口接口的设计和实现,该接口建立在WinAPI串口函数的基础上,并保留了其风格。 它与.NET的事件调度模型无缝契合,多个同事表示这正是他们希望串口类工作的方式。 但我意识到,外部环境有时禁止使用C++/CLI混合模式的程序集。 C++/CLI的解决方案与之不兼容:

1、部分信任(不是一个真正的因素,因为IOPSP的Open方法也需要UnmanagedCode权限)
2、单一可执行的部署(可能有涉及ILMerge或使用netmodules将C#代码链接到C++/CLI程序集的变通办法)
3、禁止第三方项目的开发政策
4、.NET Compact Framework(不支持混合模式的程序集)

公共许可证(尚未确定)也可能给一些用户带来问题。

或者你负责改进已经写好的IOPSP代码,而项目决策人还不想推倒重来。 (这不是一个好的决定,IOPSP在未来的维护中所带来的麻烦远远超过了重新设计的成本,最后你会为了绕过那些无法修复的bug而不得不重新开发) 。

所以,如果你属于这些类别之一,并且使用基类库是强制性的,那你不必遭受噩梦。 IOPSP的某些部分比其他部分要少得多,但你永远不会在MSDN样本中找到。 (这并不是说所有的错误都可以解决,但是如果你足够幸运,拥有不会触发这些错误的硬件,你可以让IOPSP以有限的方式可靠地工作,满足大多数用途)。

我打算从如何识别需要重做的破损的IOPSP代码的一些指导开始,并考虑给你一个永远不应该使用的成员列表。 但是这个列表会有好几页长,所以我只列出最令人震惊的那些,同时也列出安全的那些。


最严重的违规System.IO.Ports.SerialPort成员,这些成员不仅不应该被使用,而且是深刻的代码气味的标志,需要重新架构所有IOPSP的使用:

1、DataReceived事件(100%冗余,也是完全不可靠的)。
2、BytesToRead属性(完全不可靠)
3、Read, ReadExisting, ReadLine方法(处理错误完全错误,而且是同步的)。
4、PinChanged事件(在你可能想知道的每件有趣的事情上都不按顺序传递)。

可以安全使用的成员:
模式属性: 波特率(BaudRate)、数据位(DataBits)、奇偶数(Parity)、停止位(StopBits),但只有在打开端口之前。而且只适用于标准波特率。
硬件握手控制:握手属性
端口选择:构造函数、PortName属性、Open方法、IsOpen属性、GetPortNames方法
还有一个没有人使用的成员,因为MSDN没有给出例子,但对你的代码是绝对必要的:
BaseStream属性

唯一能正常工作的串行端口读取方法是通过BaseStream访问的。 它的实现,System.IO.Ports.SerialStream类(它有内部可见性;你只能通过Stream虚拟方法使用它)也是我不会选择重写的几行代码的所在地。

最后,一些代码示例:下面是例子中显示的接收数据的(错误)方式:

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();

它看起来有点多,而且代码更复杂,但它导致的 p/invoke 调用要少得多,而且不会受到BytesToRead属性的不可靠的影响。 (是的,BytesToRead版本可以被调整以处理部分读取和在检查BytesToRead和调用Read之间到达的字节,但这些只是最明显的问题)。

从.NET 4.5开始,你可以在BaseStream对象上调用ReadAsync,它在内部调用BeginRead和EndRead。

直接调用Win32的API,我们将能够更加精简,例如通过重复使用内核事件句柄,而不是为每个区块创建一个新的。 我们将在未来探索C++/CLI替代方案的文章中研究这个问题和更多的问题。

原文 地址:https://sparxeng.com/blog/software/must-use-net-system-io-ports-serialport

公众号二维码

关注我的微信公众号
在公众号里留言交流
投稿邮箱:1052839972@qq.com

庭院深深深几许?杨柳堆烟,帘幕无重数。
玉勒雕鞍游冶处,楼高不见章台路。
雨横风狂三月暮。门掩黄昏,无计留春住。
泪眼问花花不语,乱红飞过秋千去。

欧阳修

付款二维码

如果感觉对您有帮助
欢迎向作者提供捐赠
这将是创作的最大动力