【问题标题】:How to do robust SerialPort programming with .NET / C#?如何使用 .NET / C# 进行健壮的 SerialPort 编程?
【发布时间】:2015-10-01 18:33:11
【问题描述】:

我正在编写一个 Windows 服务,用于与串行磁条阅读器和中继板(访问控制系统)进行通信。

在另一个程序通过打开与我的服务相同的串行端口“中断”进程后,我遇到了代码停止工作(我得到 IOExceptions)的问题。

部分代码如下:

public partial class Service : ServiceBase
{
    Thread threadDoorOpener;
    public Service()
    {
        threadDoorOpener = new Thread(DoorOpener);
    }
    public void DoorOpener()
    {
        while (true)
        {
            SerialPort serialPort = new SerialPort();
            Thread.Sleep(1000);
            string[] ports = SerialPort.GetPortNames();
            serialPort.PortName = "COM1";
            serialPort.BaudRate = 9600;
            serialPort.DataBits = 8;
            serialPort.StopBits = StopBits.One;
            serialPort.Parity = Parity.None;
            if (serialPort.IsOpen) serialPort.Close();
            serialPort.Open();
            serialPort.DtrEnable = true;
            Thread.Sleep(1000);
            serialPort.Close();
        }
    }
    public void DoStart()
    {
        threadDoorOpener.Start();
    }
    public void DoStop()
    {
        threadDoorOpener.Abort();
    }
    protected override void OnStart(string[] args)
    {
        DoStart();
    }
    protected override void OnStop()
    {
        DoStop();
    }
}

我的示例程序成功启动了工作线程,并且 DTR 的打开/关闭和提升导致我的磁条阅读器通电(等待 1 秒)、关闭(等待 1 秒)等等。

如果我启动超级终端并连接到同一个 COM 端口,超级终端会告诉我该端口当前正在使用中。如果我在超级终端中反复按 ENTER,尝试重新打开 重试几次后会成功的端口。

这会在我的工作线程中导致 IOExceptions,这是预期的。但是,即使我关闭了超级终端,我的工作线程中仍然会收到相同的 IOException。唯一的治疗方法实际上是重新启动计算机。

其他程序(未使用 .NET 库进行端口访问)此时似乎可以正常工作。

关于造成这种情况的任何想法?

【问题讨论】:

    标签: c# .net serial-port


    【解决方案1】:

    @thomask

    是的,Hyperterminal 实际上在 SetCommState 的 DCB 中启用了 fAbortOnError,这解释了 SerialPort 对象抛出的大多数 IOExceptions。一些 PC / 手持设备还具有默认打开错误中止标志的 UART - 因此串行端口的 init 例程必须清除它(微软忽略了这样做)。我最近写了一篇长文来更详细地解释这一点(如果您有兴趣,请参阅this)。

    【讨论】:

    • @Zach Saw:你写的文章很棒,我敢让你拿出一个工作样本来清除 fAbortOnError 吗? :)
    • @thomask:当然。我会快速写一些东西,稍后作为后续文章发布。
    • 非常感谢,这将是一个很好的治疗方法:)
    • @Zach:有趣的东西。如果 DCB 上的这个 fAbortOnError 标志关闭了会发生什么,尽管有错误,它会被吞没吗?
    【解决方案2】:

    你不能关闭别人与端口的连接,下面的代码永远不会起作用:

    if (serialPort.IsOpen) serialPort.Close();
    

    因为你的对象没有打开你不能关闭的端口。

    即使发生异常,您也应该关闭并处理串行端口

    try
    {
       //do serial port stuff
    }
    finally
    {
       if(serialPort != null)
       {
          if(serialPort.IsOpen)
          {
             serialPort.Close();
          }
          serialPort.Dispose();
       }
    }
    

    如果你希望进程是可中断的,那么你应该检查端口是否打开,然后退出一段时间,然后重试,类似的。

    while(serialPort.IsOpen)
    {
       Thread.Sleep(200);
    }
    

    【讨论】:

    • 不只是关闭它,还要丢弃它! SerialPort 实现了 IDisposable,所以你可能想使用 using (SerialPort serialPort = new SerialPort) { /* Daniel's code */ }
    • 但是,这并不能真正解释为什么我的应用程序在被另一个应用程序中断后拒绝打开端口。还是这样?
    • 另见 FryGuy 的回答,超级终端将无法中断他的代码。
    • @daniel:当超级终端打开端口时,您的代码会在尝试打开端口时出现异常,然后退出线程。如果你重新启动线程,它应该会成功打开
    • 我实际上希望它能够被中断,但也希望在中断应用程序再次关闭端口后恢复运行。
    【解决方案3】:

    您是否尝试过让应用程序中的端口保持打开状态,只打开/关闭 DtrEnable,然后在应用程序关闭时关闭端口?即:

    using (SerialPort serialPort = new SerialPort("COM1", 9600))
    {
        serialPort.Open();
        while (true)
        {
            Thread.Sleep(1000);
            serialPort.DtrEnable = true;
            Thread.Sleep(1000);
            serialPort.DtrEnable = false;
        }
        serialPort.Close();
    }
    

    我不熟悉 DTR 语义,所以我不知道这是否可行。

    【讨论】:

    • 此设备需要 DTR=1 才能“唤醒”,因此基本上代码每秒都会打开/关闭设备。
    • 我的代码能满足你的需要吗?似乎与问题所要求的相同,但没有放弃资源。
    • 我实际上需要该端口在周期之间可供另一个应用程序使用。
    【解决方案4】:

    如何进行可靠的异步通信

    不要使用阻塞方法,内部帮助类有一些微妙的错误。

    将 APM 与会话状态类一起使用,其实例管理跨调用共享的缓冲区和缓冲区游标,以及将 EndRead 包装在 try...catch 中的回调实现。在正常操作中,try 块应该做的最后一件事是通过调用BeginRead() 来设置下一个重叠 I/O 回调。

    当出现问题时,catch 应该异步调用一个委托来重新启动方法。回调实现应该在catch 块之后立即退出,以便重新启动逻辑可以破坏当前会话(会话状态几乎肯定已损坏)并创建一个新会话。重启方法必须在会话状态类上实现,因为这会阻止它破坏和重新创建会话。

    当 SerialPort 对象关闭时(这将在应用程序退出时发生),很可能会有一个挂起的 I/O 操作。在这种情况下,关闭 SerialPort 将触发回调,并且在这些情况下EndRead 将抛出一个与一般的 comms shitfit 无法区分的异常。您应该在会话状态中设置一个标志来禁止catch 块中的重新启动行为。这将阻止您的重启方法干扰自然关机。

    可以依赖此架构不会意外地持有 SerialPort 对象。

    restart 方法管理串口对象的关闭和重新打开。在SerialPort 对象上调用Close() 后,调用Thread.Sleep(5) 给它一个放手的机会。其他东西可能会抢占端口,因此请准备好在重新打开它时处理它。

    【讨论】:

    【解决方案5】:

    我想我已经得出结论,超级终端不能很好地运行。我已经运行了以下测试:

    1. 在“控制台模式”下启动我的服务,它开始打开/关闭设备(我可以通过它的 LED 来判断)。

    2. 启动超级终端并连接到端口。 设备保持开启状态(超级终端提高 DTR) 我的服务写入事件日志,它无法打开端口

    3. 停止超级终端,我使用任务管理器验证它已正确关闭

    4. 设备保持关闭状态(超级终端降低了 DTR),我的应用不断写入事件日志,说它无法打开端口。

    5. 我启动了第三个应用程序(我需要与之共存的应用程序),并告诉它连接到端口。我这样做。这里没有错误。

    6. 我停止了上述应用程序。

    7. 瞧,我的服务再次启动,端口成功打开,LED 亮/灭。

    【讨论】:

      【解决方案6】:

      我尝试过像这样更改工作线程,结果完全相同。一旦超级终端成功“捕获端口”(当我的线程处于休眠状态时),我的服务将无法再次打开端口。

      public void DoorOpener()
      {
          while (true)
          {
              SerialPort serialPort = new SerialPort();
              Thread.Sleep(1000);
              serialPort.PortName = "COM1";
              serialPort.BaudRate = 9600;
              serialPort.DataBits = 8;
              serialPort.StopBits = StopBits.One;
              serialPort.Parity = Parity.None;
              try
              {
                  serialPort.Open();
              }
              catch
              {
              }
              if (serialPort.IsOpen)
              {
                  serialPort.DtrEnable = true;
                  Thread.Sleep(1000);
                  serialPort.Close();
              }
              serialPort.Dispose();
          }
      }
      

      【讨论】:

        【解决方案7】:

        此代码似乎工作正常。我已经在我的本地机器上的控制台应用程序中对其进行了测试,使用 Procomm Plus 打开/关闭端口,并且程序一直在运行。

            using (SerialPort port = new SerialPort("COM1", 9600))
            {
                while (true)
                {
                    Thread.Sleep(1000);
                    try
                    {
                        Console.Write("Open...");
                        port.Open();
                        port.DtrEnable = true;
                        Thread.Sleep(1000);
                        port.Close();
                        Console.WriteLine("Close");
                    }
                    catch
                    {
                        Console.WriteLine("Error opening serial port");
                    }
                    finally
                    {
                        if (port.IsOpen)
                            port.Close();
                    }
                }
            }
        

        【讨论】:

        • 是的,该代码有效。虽然当被超级终端中断时,它会惨遭失败,并在 catch 块中重复出现错误消息。这让我相信超级终端离开端口的状态有问题。
        • 关于 finally 块中的代码。如果此时此应用程序未打开端口,port.Close() 语句是否会出现另一个异常?
        • 没有。 SerialPort.IsOpen 如果串行端口的instance 端口打开,则返回true,而不是如果串行端口通常是打开的。把它想象成一个文件。如果你有一个 TextReader 实例,如果文件被任何人打开,调用 IsOpen 不会返回,只要你打开了文件。
        【解决方案8】:

        这个答案很长才能成为评论......

        我相信当您的程序处于 Thread.Sleep(1000) 并且您打开超级终端连接时,超级终端会控制串行端口。当您的程序唤醒并尝试打开串行端口时,会引发 IOException。

        重新设计您的方法并尝试以不同的方式处理端口的打开。

        编辑: 当您的程序失败时,您必须重新启动计算机...

        这可能是因为您的程序并没有真正关闭,请打开您的任务管理器,看看您是否可以找到您的程序服务。请务必在退出应用程序之前停止所有线程。

        【讨论】:

        • 我的程序真的已关闭。一旦应用程序退出,工作线程就会使用 Abort() 方法停止。基本上一旦超级终端打开了一次端口,.NET 将无法打开相同的端口,直到重新启动。
        • 这很有趣。我要看看 HT 对我的代码做了什么,但首先我必须找到 XP 的副本(Vista 中没有 HT)。
        【解决方案9】:

        是否有充分的理由阻止您的服务“拥有”端口?看看内置的 UPS 服务——一旦你告诉它有一个 UPS 连接到,比如说,COM1,你就可以和那个端口说再见了。我建议您也这样做,除非有强烈的操作要求来共享端口。

        【讨论】:

        • 我需要与另一个应用程序共享对开门器继电器设备的访问权限。这个应用程序也很友好地在它完成后释放端口(即已经单独打开它)。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-02-08
        相关资源
        最近更新 更多