C#: 读取串口 - BytesToRead

5
我正在修改一个基于C#的用户界面,用于与小型PIC微控制器测试设备进行交互。
该界面由一对按钮组成,通过串口连接向微控制器发送“命令”以启动测试。每250毫秒,界面会轮询串行接口,寻找来自PIC的测试结果简短消息。该消息将在文本框中显示。
我继承的代码如下:
try
{
    btr = serialPort1.BytesToRead;
    if (btr > 0)
        Thread.Sleep(300);
    btr = serialPort1.BytesToRead;
    if (btr > 0)
    {
        Thread.Sleep(300);
        btr = serialPort1.BytesToRead;

        numbytes = serialPort1.Read(stuffchar, 0, btr);
        for (x = 0; x < (numbytes); x++)
        {
            cc = (stuffchar[x]);
            stuff += Convert.ToString(Convert.ToChar((stuffchar[x])));
        }

前几行为什么要调用三次BytesToRead和两次300毫秒的休眠,最后才读取串口?除非我对代码的理解出现了问题,否则从串口成功读取所需的时间将超过600毫秒,这似乎有点奇怪。


https://social.msdn.microsoft.com/Forums/vstudio/en-US/e36193cd-a708-42b3-86b7-adff82b19e5e/how-does-serialport-handle-datareceived?forum=netfxbcl - Faizan Mubasher
3个回答

4
这是对SerialPort.Read()行为的可怕黑客方式。它仅返回实际接收到的字节数。通常只有1或2个,串口很慢,现代PC非常快。所以通过调用Thread.Sleep(),该代码延迟了UI线程足够长的时间来使Read()调用返回更多字节。希望所有协议都能收到完整的数据。通常有效,但并非总是如此。在发布的代码中,它失败了,程序员只是随意地延迟了两倍长时间。恼人啊。
当然,巨大的痛苦在于,当UI线程被强制睡眠时会变得相当昏睡。非常明显,它变得非常缓慢、反应迟钝。
首先需要修复这个问题,需要关注协议。PIC需要发送一定数量的字节作为响应,这样你就可以简单地计数它们,或者给PC一种检测到完整响应的方法。通常通过将唯一字节作为响应的最后一个字节(SerialPort.NewLine)发送,或在消息开头包含响应长度的字节值来完成。具体建议很难给出,因为你没有描述协议。
你可以保持这个hacky的代码,并将其移动到工作线程中,这样它不会对UI产生太大影响。你可以从SerialPort.DataReceived事件中免费获得一个。但这往往会产生两个问题,而不是解决核心问题。

0

@TomWr 你说得对,从我所读的来看确实是这样。
以下是你的代码片段和我的评论:

try
{
    // Let's check how many bytes are available on the Serial Port
    btr = serialPort1.BytesToRead;

    // Something? Alright then, let's wait 300 ms.
    if (btr > 0)
        Thread.Sleep(300);

    // Let's check again that there are some bytes available the Serial Port
    btr = serialPort1.BytesToRead;

    // ... and if so wait (maybe again) for 300 ms 
    // Please note that, at that point can be all cumulated about 600ms 
    // (if we actually already waited previously)
    if (btr > 0)
    {
        Thread.Sleep(300);
        btr = serialPort1.BytesToRead;

        numbytes = serialPort1.Read(stuffchar, 0, btr);
        for (x = 0; x < (numbytes); x++)
        {
            // Seems like a useless overhead could directly use
            // an Encoding and ReadExisting method() of the SerialPort.
            cc = (stuffchar[x]);
            stuff += Convert.ToString(Convert.ToChar((stuffchar[x])));
        }

我猜想和idstam上面提到的一样,可能是为了检查设备是否发送了数据并获取它们。

您可以使用适当的SerialPort方法轻松地重构此代码,因为实际上有更好和更简洁的方法来检查串口上是否有可用的数据。

与其像这样“我正在检查端口上有多少字节,如果有什么东西,那么我等待300毫秒,然后再次进行相同的操作。”结果可怜,最终会变成:
“是的,2次300毫秒= 600毫秒,或者只有一次(取决于是否已经第一次),或者根本没有(取决于您通过此UI与之通信的设备,它可以非常懒惰,因为Thread.Sleep阻止了UI ...)。"

首先,让我们考虑一下,如果您尝试保持尽可能相同的代码库,为什么不等待600ms呢?

或者,为什么不直接使用ReadTimeout属性并捕获超时异常,虽然这不太干净,但至少在可读性方面更好,而且您直接获取了String而不是使用Convert.ToChar()调用...

我感觉这段代码已经从C或C++移植过来了(或者至少是背后的原理),来自一个更多地拥有嵌入式软件背景的人。

无论如何,回到可用字节检查的数量上,我的意思是除非在另一个线程/BackgroundWorker/Task处理程序中刷新串行端口数据,否则我不认为需要检查两次,特别是按照编码方式。让它更快?并不是真的,因为如果串行端口上实际上有数据,会有额外的延迟。这对我来说并没有太多意义。
使您的片段稍微改进的另一种方法是使用ReadExisting()进行轮询。
否则,您还可以考虑使用SerialPort BaseStream的异步方法。
总之,在没有访问其余代码库(即上下文)的情况下很难说。
如果您有关于目标/协议的更多信息,可能会给出一些关于要做什么的提示。否则,我只能说这似乎编码很差,再次超出了上下文。
我反复三思汉斯提到的UI响应能力,我真的希望您的片段正在运行一个不是UI的线程中(尽管您在帖子中提到UI正在轮询,但我仍然希望该片段是针对另一个工人的)。

如果这确实是UI线程,那么每次调用Thread.Sleep都会阻塞它,使得UI对用户的交互不太响应,可能会让最终用户感到沮丧。

还值得订阅DataReceived事件,并使用处理程序执行所需操作(例如,使用缓冲区和比较值等)。

请注意,mono仍未实现此事件的触发,但如果您正在运行纯MS .NET实现,则没有多线程的麻烦,这是完全可以接受的。

简而言之:

  • 检查哪个线程负责您的代码片段,并注意UI的响应性。
    • 如果是UI,请使用另一个线程通过Thread、BackgroundWorker(Threadpool)或Task来处理它。
    • 使用异步流方法,以避免UI线程同步的麻烦
  • 尝试看看目标是否真的值得使用两个300毫秒的Thread Sleep方法调用
  • 您可以直接获取String,而不是自己收集Bytes(如果所选编码能够满足您的需求)。

1
默认情况下,任务对象在当前上下文中运行,在UI应用程序中,默认上下文是UI消息调度器。不使用额外的线程。现在,您可以选择将任务强制分配给工作线程,但对于I/O操作,特别是串行I/O,这样做是愚蠢的。工作线程只对CPU密集型操作有优势。 - Ben Voigt
@BenVoigt 在他的情况下是正确的,这确实是个问题(I/O瓶颈)。但是,假设逻辑现在是每10ms轮询串行I/O,例如为了检查电缆是否已断开,然后尽快通过UI通知最终用户,而最终用户正在执行其他任务,例如显示图表或任何密集计算。从上下文中似乎很难知道UI任务是否真正受到I/O限制。我只是问一下,您仍然会建议在这种(特定)情况下使用非工作线程吗? - Natalie Perret
对于轮询设备是否存在,我肯定会在UI线程上执行。这不会占用线程上其他操作的时间。如果你担心轮询因为UI线程繁忙而延迟...那么你已经有了一个无响应的UI问题,这不是由串行访问引起的,也不能通过工作线程解决。请记住,工作线程必须通过UI线程(例如BeginInvoke)来挂起UI更新。如果UI线程被阻塞,则来自工作线程的UI更新也不会发生。 - Ben Voigt
1
如果你有足够的处理能力来配得上一个工作线程,那么就把处理放在工作线程中。I/O仍然不需要自己的线程,它应该共享运行使用数据的任务的线程。如果你的服务器没有UI,那么显然不应该在UI线程中进行I/O操作。但是你永远不会因为有I/O而添加更多的线程。 - Ben Voigt
@BenVoigt,完全同意您的观点。我的错,我有点(很多)偏离了最初的ReadAsync主题。 - Natalie Perret
显示剩余6条评论

0

如果这段代码最初是在循环中的,那么它可能是等待PIC收集数据的一种方式。

如果您有真实的硬件进行测试,我建议您删除两个Sleeps。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接