作为一名编写桌面软件的嵌入式开发人员,主要用于外围设备的配置和数据下载,我经常使用串行数据流。主要是来自 FTDI的USB 虚拟串行帖子,还有 USB 通信设备类和 PCI 总线上真正的 16550 兼容 UART。由于通过在线仿真器调试接口查看数据通常是一种痛苦的体验,因此与定制 PC 应用程序进行串行数据通信对于分析数据质量和提供有关硬件设计的反馈至关重要。 C# 和 .NET Framework提供了快速的应用程序开发,非常适合需要随着硬件设计发展而跟踪不断变化的需求的早期开发。我应该说,在大多数方面都很理想。
.NET 附带的System.IO.Ports.SerialPort类是一个明显的例外。说得委婉些,它是由计算机科学家设计的,他们的工作范围远远超出了他们的核心竞争力领域。他们既不了解串行通信的特性,也不了解常见的用例,它显示了。在发货之前,它也无法在任何真实世界场景中进行测试,没有发现缺陷会导致记录的接口和未记录的行为都乱扔垃圾,并使使用System.IO.Ports.SerialPort(以下简称 IOPSP)的可靠通信成为真正的噩梦。(StackOverflow 上的大量证据证明了这一点,来自在 Hyperterminal 但不是 .NET 中工作的设备,因为IOPSP 强制设置某些参数,尽管它们不适用于虚拟端口,并在失败时关闭端口。在 IOPSP 初始化期间,无法绕过或忽略这些设置的失败。) 更令人惊讶的是,当底层的 kernel32.dll API 非常好时(我在使用 .NET 之前使用过 WinAPI,当我想使用 .NET 没有的功能时仍然会这样做)会发生这种级别的故障有一个包装器,其中特别包括设备枚举)。.NET 工程师不仅没有设计出合理的接口,他们选择无视已经非常成熟的 WinAPI 设计,也没有从内核团队二十年的串口经验中吸取教训。
未来的系列文章将介绍基于 WinAPI 串行端口功能并保留其风格的合理串行端口接口的设计和实现。它与 .NET 事件分派模型无缝匹配,并且多个同事表示这正是他们希望串行端口类工作的方式。但我意识到外部环境有时会禁止使用 C++/CLI 混合模式程序集。C++/CLI 解决方案不兼容:
- 部分信任(不是一个真正的因素,因为 IOPSP 的 Open 方法也需要 UnmanagedCode 权限)
- 单可执行部署(可能存在涉及 ILMerge 或使用 netmodules 将 C# 代码链接到 C++/CLI 程序集的解决方法)
- 禁止第三方项目的开发政策
- .NET Compact Framework(不支持混合模式程序集)
公共许可证(尚未确定)也可能会给某些用户带来问题。
或者,也许您负责改进已经编写的 IOPSP 代码,而项目决策者还没有准备好换马。(这不是一个好的决定,IOPSP 在以后的维护中造成的麻烦远远超过切换的工作量,最终您将最终切换以绕过无法修复的错误。)
因此,如果您属于这些类别之一并且必须使用基类库,那么您不必遭受最糟糕的噩梦。IOPSP 的某些部分比其他部分的损坏要少得多,但是您永远不会在 MSDN 示例中找到这些部分。(不出所料,这些对应于 .NET 包装器最薄的地方。)这并不是说所有错误都可以解决,但如果你有幸拥有不会触发它们的硬件,你可以获得IOPSP 以涵盖大多数使用的有限方式可靠地工作。
我计划从一些关于如何识别需要返工的损坏的 IOPSP 代码的指导开始,并考虑给你一个永远不应该使用的成员列表。但是该列表将有几页长,因此我将仅列出最令人震惊的列表以及安全的列表。
最糟糕的System.IO.Ports.SerialPort成员,它们不仅不应该被使用,而且是代码味道很重的迹象,并且需要重新架构所有 IOPSP 使用:
- DataReceived事件(100% 冗余,也完全不可靠)
- BytesToRead属性(完全不可靠)
- Read、ReadExisting 、ReadLine方法(处理错误完全错误,并且是同步的)
- PinChanged事件(关于您可能想知道的每件有趣的事情都乱序传递)
可以安全使用的会员:
- 模式属性:BaudRate、DataBits、Parity、StopBits,但仅在打开端口之前。并且仅适用于标准波特率。
- 硬件握手控制:Handshake属性
- 端口选择:构造函数、PortName属性、Open方法、IsOpen属性、GetPortNames方法
还有一个没有人使用的成员,因为 MSDN 没有给出示例,但对您的理智绝对必要:
- BaseStream属性_
唯一正常工作的串行端口读取方法是通过BaseStream访问的。它的实现System.IO.Ports.SerialStream类(具有内部可见性;您只能通过 Stream 虚拟方法使用它)也是我不会选择重写的几行代码的所在地。
最后,一些代码。
这是示例显示接收数据的(错误)方式:
这是正确的方法,它与底层 Win32 API 的使用方式相匹配:
它看起来有点多,更复杂的代码,但它会导致更少的 p/invoke 调用,并且不会受到BytesToRead属性的不可靠性的影响。(是的,可以调整BytesToRead版本以处理在检查BytesToRead和调用Read之间到达的部分读取和字节,但这些只是最明显的问题。)
从 .NET 4.5 开始,您可以改为在BaseStream对象上调用ReadAsync ,该对象在内部调用BeginRead和EndRead。
直接调用 Win32 API,我们将能够进一步简化这一点,例如通过重用内核事件句柄而不是为每个块创建一个新句柄。我们将在未来探索 C++/CLI 替代品的帖子中讨论这个问题以及更多内容。