远古的一篇博客,内容散落于博文和评论 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 属性 (完全不可靠)
  • ReadReadExistingReadLine 函数(处理错误完全错误,并且是同步的)
  • 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 是对串口寄存器错误标志位的原子交换——你只会看到一次,然后就会清除他们。框架中正在查看这些标志的其他代码以触发 PinChangedErrorReceived 事件由于 BytesToRead(查看并清除寄存器) 会忽略它们,因此事件会丢失。事实上,ErrorReceived 事件的 MSDN 页面显示“由于操作系统决定是否引发此事件,因此不会报告所有奇偶校验错误。这是一个彻头彻尾的谎言 — 事件丢失发生在 BytesToReadBytesToWrite 的 getter 函数中。

译者注:这段原因写在作者和游客的讨论中,感兴趣的可以看看原文。如果写过STM32就能理解作者的意思(STM32的有些寄存器在读后就会清除,这样就会导致丢失了一些事件)。作者表示如果你对串口通信的要求很高,(高性能/低时延/错误等)你就应该使用Win32 API或者 p/invoke 或 C++/CLI。(作者列了写商业库说也能用,感兴趣的在原文评论区自行找一下)。如果你对串口通信的要求不高,其实DataReceived的方式其实也能用。

【摘译+整理】System.IO.Ports.SerialPort使用注意的更多相关文章

  1. [转]c# System.IO.Ports SerialPort Class

    本文转自:https://docs.microsoft.com/en-us/dotnet/api/system.io.ports.serialport?redirectedfrom=MSDN& ...

  2. 串口编程 System.IO.Ports.SerialPort类

    从Microsoft .Net 2.0版本以后,就默认提供了System.IO.Ports.SerialPort类,用户可以非常简单地编写少量代码就完成串口的信息收发程序.本文将介绍如何在PC端用C# ...

  3. System.IO.Ports.SerialPort串口通信接收完整数据

    C#中使用System.IO.Ports.SerialPort进行串口通信网上资料也很多,但都没有提及一些细节: 比如 串口有时候并不会一次性把你想要的数据全部传输给你,可能会分为1次,2次,3次分别 ...

  4. System.IO.IOException: The handle is invalid.

    System.IO.IOException: The handle is invalid. 00022846 11:39:49.098 AM [892] 00022847 11:39:49.098 A ...

  5. 【等待事件】等待事件系列(3+4)--System IO(控制文件)+日志类等待

     [等待事件]等待事件系列(3+4)--System IO(控制文件)+日志类等待   1  BLOG文档结构图     2  前言部分   2.1  导读和注意事项 各位技术爱好者,看完本文后,你可 ...

  6. 高效方便的IO库: System.IO.Pipelines

    我们在编写网络程序的时候,经常会进行如下操作: 申请一个缓冲区 从数据源中读入数据至缓冲区 解析缓冲区的数据 重复第2步 表面上看来这是一个很常规而简单的操作,但实际使用过程中往往存在如下痛点: 数据 ...

  7. Fiddler的一些坑: !SecureClientPipeDirect failed: System.IO.IOException

    手机的请求Fiddler可以捕捉,但是手机一直无法上网,在logs中看到的日志如下: !SecureClientPipeDirect failed: System.IO.IOException 由于远 ...

  8. 服务 在初始化安装时发生异常:System.IO.FileNotFoundException: "file:///D:\testService"未能加载文件或程序集。系统找不到指定文件。

    @echo.@if exist "%windir%\Microsoft.NET\Framework\v4.0.30319\InstallUtil.exe" goto INSTALL ...

  9. C#、.Net代码精简优化(空操作符(??)、as、string.IsNullOrEmpty() 、 string.IsNullOrWhiteSpace()、string.Equals()、System.IO.Path 的用法)

    一.空操作符(??)在程序中经常会遇到对字符串或是对象判断null的操作,如果为null则给空值或是一个指定的值.通常我们会这样来处理: .string name = value; if (name ...

  10. System.IO.Directory.Delete目录删除

    在程序运行的时候,如果直接获取一个目录路径,然后执行删除(包括子目录及文件): System.IO.Directory.Delete(path,true); 或者 System.IO.Director ...

随机推荐

  1. CMake 进行多项目中dll的编译和链接

    前言(maybe废话) 最近正在学习cherno的游戏引擎教程,他使用的是vs进行构建的,后面换了premake.而我用的是vscode+cmake,所以在构建整个项目的时候踩了不少的坑,也找了很多资 ...

  2. js 判断闰年

    首先,我们需要了解闰年的判断方式 1.能被4整除 2.并且不能被100整除 3.或者被400整除的 其次我们再来回顾下函数封装的知识,所谓的函数封装就是将一段函数封装成一个工具,有人用到了拿过来就可以 ...

  3. python+k8s(基础,遇到的问题)

    python+k8s(基础,遇到的问题) CoreV1Api和ApiClient的区别 kubernetes.client.CoreV1Api kubernetes.client.ApiClient ...

  4. JDBC的简单使用以及介绍

    JDBC(Java DataBase Connectivity) Java 语言连接数据库 再本模块中,java提供里一组用于连接数据库的类和接口 Java 语言开发者,本身没有提供如何具体连接数据库 ...

  5. kettle从入门到精通 第三十一课 mysql 数据连接连接池配置

    无论开发应用程序还是做ETL研发,都离不开连接池的应用,如下是kettle中mysql 连接池设置界面,今天重点讲解下连接池中的参数配置. defaultAutoCommit 当 defaultAut ...

  6. Scrapy框架(五)--请求传参

    在某些情况下,我们爬取的数据不在同一个页面中,例如,我们爬取一个电影网站,电影的名称,评分在一级页面,而要爬取的其他电影详情在其二级子页面中. 这时我们就需要用到请求传参. 请求传参的使用场景 当我们 ...

  7. Java中创建对象的5种方式总结

    引言 作为Java开发人员,我们每天都会代码中创建对象,但我们通常使用依赖管理系统,比如Spring框架,然后,这里有很多种创建对象的方式,本文就对Java创建对象的几种方式进行总结 五种创建方式 创 ...

  8. Docker入门系列之四:Docker镜像

    在本文中,您将学习如何加快Docker构建周期并创建轻量级镜像.遵循之前的文章中的食物隐喻,我们将沙拉隐喻为Docker镜像,同时减少Docker镜像的数量. 在本系列的第3部分中,我们介绍了十几个D ...

  9. kettle从入门到精通 第七十一课 ETL之kettle 再谈http post,轻松掌握body中传递json参数

    场景: kettle中http post步骤如何发送http请求且传递body参数? 解决方案: http post步骤中直接设置Request entity field字段即可. 1.手边没有现成的 ...

  10. TensorFLow手写字识别深度学习网络分析详解

    Tensorflow和MNIST简介 TensorFlow 是一个采用数据流图,用于数值计算的开源软件库.它是一个不严格的"神经网络"库,可以利用它提供的模块搭建大多数类型的神经网 ...