C# 串口操作系列(2) -- 入门篇

   第一篇文章我相信很多人不看都能做的出来,但是,用过微软SerialPort类的人,都遇到过这个尴尬,关闭串口的时候会让软件死锁。天哪,我可不是武 断,算了。不要太绝对了。99.9%的人吧,都遇到过这个问题。我想只有一半的人真的解决了。另外一半的人就睁只眼闭只眼阿弥佗佛希望不要在客户那里出现 这问题了。

    你看到我的文章,就放心吧,这问题有救了。我们先回顾一下上一篇中的代码

 

  1. void  comm_DataReceived( object  sender, SerialDataReceivedEventArgs e)     
  2. {     
  3.     //先记录下来,避免某种原因,人为的原因,操作几次之间时间长,缓存不一致   
  4.     int  n = comm.BytesToRead;  
  5.     //声明一个临时数组存储当前来的串口数据   
  6.     byte [] buf =  new   byte [n];     
  7.     //增加接收计数   
  8.     received_count += n;   
  9.     //读取缓冲数据     
  10.     comm.Read(buf, 0, n);     
  11.     //清除字符串构造器的内容   
  12.     builder.Clear();     
  13.     //因为要访问ui资源,所以需要使用invoke方式同步ui。      
  14.     this .Invoke((EventHandler)( delegate {...界面更新,略}));     
  15. }     
  16.     
  17. private   void  buttonOpenClose_Click( object  sender, EventArgs e)     
  18. {     
  19.     //根据当前串口对象,来判断操作      
  20.     if  (comm.IsOpen)     
  21.     {     
  22.         //打开时点击,则关闭串口      
  23.         comm.Close();//这里就是可能导致软 件死掉的地方   
  24.     }     
  25.     else     
  26.     {...}    
  27. }  

    为什么会死锁呢,并发冲突。

    我们要了解一下SerialPort的实现和串口通讯机制,在你打开串口的时候,SerialPort会创建一个监听线程ListenThread,在这 个线程中,等待注册的串口中断,当收到中断后,会调用DataReceived事件。调用完成后,继续进入循环等待,直到串口被关闭退出线程。

    我们的UI主线程如何做的呢,首先创建一个窗体,然后执行了Application.Run(窗体实例)。是这样把,这里的 Application.Run就是创建了一个消息循环,循环的处理相关的消息。

    这里我们就有了2个线程,UI主线程、串口监听线程。那么你在DataReceived处理数据的时候,就需要线程同步,避免并发冲突,什么是并发冲突? 并发冲突就是2个或多个并行(至少看上去像)的线程运行的时候,多个线程共同的操作某一线程的资源,在时序上同时或没有按我们的预计顺序操作,这样就可能 导致数据混乱无序或是彼此等待完成死锁软件。

    而串口程序大多是后者。为什么呢,看看我们的例子中DataReceived做了什么?首先读取数据,然后就是调用this.Invoke方法更新UI 了。这里Invoke的时候,监听线程将等待UI线程的标志,等到后,开始操作UI的资源,当操作完成之前,监听线程也就停在DataReceived方 法的调用这里,如果这个时候。并发了关闭串口的操作会如何呢?SerialPort的Close方法,会首先尝试等待和监听线程一样的一个互斥体、临界 区、或是事件(不确定.net用的哪种)。那这个同步对象什么时候释放呢?每次循环结束就释放,哦。循环为什么不结束呢?因为这一次的循环操作执行到 DataReceived之后,执行了Invoke去更新界面了,那Invoke怎么又没有执行完成呢?看上去很简单的几行代码。虽然我没仔细研读 过.net的Invoke原理,但我猜测是通过消息的方式来同步的,这也是为什么这么多的类,只有控件(窗体也是控件的一种,.net在概念上,颠覆了微 软自己的概念,传统的win32编程,是说所有的控件都是个window,只是父窗体不同,表现形式不同,但都是基于系统消息队列的,.net出于更高的 抽象,正好反过来了。呵呵)才有Invoke方法了。(委托自己的Invoke和这个不同)

    我猜测控件/窗体的Invoke是SendMessage方式实现的,那么发送消息后就会等待消息循环来处理消息了。如果你直接去关闭串口了。你点击按钮 本身也会被转换成消息WM_CLICK,消息循环在处理按钮的WM_CLICK时候,调用你按钮的OnClick方法,进而触发调用你的 ButtonClose_Click事件,这都是同步调用的,你的主线程,处理消息的过程,停在了这个Click事件,而你的Click事件又去调用了 SerialPort的Close方法,Close方法又因为和串口监听线程的同步信号量关联在一起需要等待一次的while结束,而这个while循环 中调用了DataReceived方法,这个方法中调用了Invoke,也就是发送了消息到消息队列等待结果,但消息循环正在处理你的关闭按钮事件等待退 出。

    实在太复杂了,这个情况下,你想要真的关闭串口成功,就需要while中的DataReceived方法调用结束释放同步信号,就需要执行完 Invoke,就需要执行消息循环,幸运的是,我们真的有办法执行消息循环来打破僵局。Application.DoEvents()。还好,不幸中的万 幸。可是问题又来了,你能让Invoke结束,但你无法确定是否在你调用消息循环后,你的某一时刻不会再次并发,可能由于单cpu的串行操作模拟并行中, 又把时间片先分给了优先级高的串口监听线程呢?是有可能的。所以,我们就需要一点方法来避免再次invoke窗体。优化后不会司机的例子如下,我们修改 DataReceived方法,关闭方法,并定义2个标记Listening和Closing。

  1. using  System;  
  2. using  System.Collections.Generic;  
  3. using  System.ComponentModel;  
  4. using  System.Data;  
  5. using  System.Drawing;  
  6. using  System.Linq;  
  7. using  System.Text;  
  8. using  System.Windows.Forms;  
  9. using  System.IO.Ports;  
  10. using  System.Text.RegularExpressions;  
  11.   
  12. namespace  SerialportSample  
  13. {  
  14.     public  partial  class  SerialportSampleForm : Form  
  15.     {  
  16.         private  SerialPort comm =  new  SerialPort();  
  17.         private  StringBuilder builder =  new  StringBuilder(); //避免在事件处理方法中反复的创建,定义到外面。   
  18.         private   long  received_count = 0; //接收计数   
  19.         private   long  send_count = 0; //发送计数   
  20.         private   bool  Listening =  false ; //是否没有执 行完invoke相关操作   
  21.         private   bool  Closing =  false ; //是否正在关 闭串口,执行Application.DoEvents,并阻止再次invoke   
  22.   
  23.         public  SerialportSampleForm()  
  24.         {  
  25.             InitializeComponent();  
  26.         }  
  27.   
  28.         //窗体初始化   
  29.         private   void  Form1_Load( object  sender, EventArgs e)  
  30.         {  
  31.             //初始化下拉串口名称列表框   
  32.             string [] ports = SerialPort.GetPortNames();  
  33.             Array.Sort(ports);  
  34.             comboPortName.Items.AddRange(ports);  
  35.             comboPortName.SelectedIndex = comboPortName.Items.Count > 0 ? 0 : -1;  
  36.             comboBaudrate.SelectedIndex = comboBaudrate.Items.IndexOf("9600" );  
  37.   
  38.             //初始化SerialPort对象   
  39.             comm.NewLine = "/r/n" ;  
  40.             comm.RtsEnable = true ; //根据实际情况吧。   
  41.   
  42.             //添加事件注册   
  43.             comm.DataReceived += comm_DataReceived;  
  44.         }  
  45.   
  46.         void  comm_DataReceived( object  sender, SerialDataReceivedEventArgs e)  
  47.         {  
  48.             if  (Closing)  return ; //如果正在关闭,忽略操作,直接返回,尽快的完成串口监听线程的一次循环   
  49.             try   
  50.             {  
  51.                 Listening = true ; //设置标记,说 明我已经开始处理数据,一会儿要使用系统UI的。   
  52.                 int  n = comm.BytesToRead; //先记录下来,避免某种原因,人为的原因,操作几次之间时间长,缓存不一致   
  53.                 byte [] buf =  new   byte [n]; //声明一个临时数组存储当前来的串口数据   
  54.                 received_count += n;//增加接收计数   
  55.                 comm.Read(buf, 0, n);//读取缓冲数据   
  56.                 builder.Clear();//清除字符串构造器的内容   
  57.                 //因为要访问ui资源,所以需要 使用invoke方式同步ui。   
  58.                 this .Invoke((EventHandler)( delegate   
  59.                 {  
  60.                     //判断是否是显示为16禁止   
  61.                     if  (checkBoxHexView.Checked)  
  62.                     {  
  63.                         //依次的拼接出16进制字符串   
  64.                         foreach  ( byte  b  in  buf)  
  65.                         {  
  66.                             builder.Append(b.ToString("X2" ) +  " " );  
  67.                         }  
  68.                     }  
  69.                     else   
  70.                     {  
  71.                         //直接按ASCII规则转换成字符串   
  72.                         builder.Append(Encoding.ASCII.GetString(buf));  
  73.                     }  
  74.                     //追加的形式添加到文本框末端,并滚动到最后。   
  75.                     this .txGet.AppendText(builder.ToString());  
  76.                     //修改接收计数   
  77.                     labelGetCount.Text = "Get:"  + received_count.ToString();  
  78.                 }));  
  79.             }  
  80.             finally   
  81.             {  
  82.                 Listening = false ; //我用完 了,ui可以关闭串口了。   
  83.             }  
  84.         }  
  85.   
  86.         private   void  buttonOpenClose_Click( object  sender, EventArgs e)  
  87.         {  
  88.             //根据当前串口对象,来判断操作   
  89.             if  (comm.IsOpen)  
  90.             {  
  91.                 Closing = true ;  
  92.                 while  (Listening) Application.DoEvents();  
  93.                 //打开时点击,则关闭串口   
  94.                 comm.Close();  
  95.             }  
  96.             else   
  97.             {  
  98.                 //关闭时点击,则设置好端口,波特率 后打开   
  99.                 comm.PortName = comboPortName.Text;  
  100.                 comm.BaudRate = int .Parse(comboBaudrate.Text);  
  101.                 try   
  102.                 {  
  103.                     comm.Open();  
  104.                 }  
  105.                 catch (Exception ex)  
  106.                 {  
  107.                     //捕获到异常信息,创建一个新的comm对象,之前的不能用了。   
  108.                     comm = new  SerialPort();  
  109.                     //现实异常信息给客 户。   
  110.                     MessageBox.Show(ex.Message);  
  111.                 }  
  112.             }  
  113.             //设置按钮的状态   
  114.             buttonOpenClose.Text = comm.IsOpen ? "Close"  :  "Open" ;  
  115.             buttonSend.Enabled = comm.IsOpen;  
  116.         }  
  117.   
  118.         //动态的修改获取文本框是否支持自动换行。   
  119.         private   void  checkBoxNewlineGet_CheckedChanged( object  sender, EventArgs e)  
  120.         {  
  121.             txGet.WordWrap = checkBoxNewlineGet.Checked;  
  122.         }  
  123.   
  124.         private   void  buttonSend_Click( object  sender, EventArgs e)  
  125.         {  
  126.             //定义一个变量,记录发送了几个字节   
  127.             int  n = 0;  
  128.             //16进制发送   
  129.             if  (checkBoxHexSend.Checked)  
  130.             {  
  131.                 //我们不管规则了。如果写错了一些,我们允许的,只用正则得到有效的十六进制数   
  132.                 MatchCollection mc = Regex.Matches(txSend.Text, @"(?i)[/da-f]{2}" );  
  133.                 List<byte > buf =  new  List< byte >(); //填充到这个临时列表中   
  134.                 //依次添加到列表中   
  135.                 foreach  (Match m  in  mc)  
  136.                 {  
  137.                     buf.Add(byte .Parse(m.Value));  
  138.                 }  
  139.                 //转换列表为数组后发送   
  140.                 comm.Write(buf.ToArray(), 0, buf.Count);  
  141.                 //记录发送的字节数   
  142.                 n = buf.Count;  
  143.             }  
  144.             else //ascii编码直接发送   
  145.             {  
  146.                 //包含换行符   
  147.                 if  (checkBoxNewlineSend.Checked)  
  148.                 {  
  149.                     comm.WriteLine(txSend.Text);  
  150.                     n = txSend.Text.Length + 2;  
  151.                 }  
  152.                 else //不包含换行符   
  153.                 {  
  154.                     comm.Write(txSend.Text);  
  155.                     n = txSend.Text.Length;  
  156.                 }  
  157.             }  
  158.             send_count += n;//累加发送 字节数   
  159.             labelSendCount.Text = "Send:"  + send_count.ToString(); //更新界面   
  160.         }  
  161.   
  162.         private   void  buttonReset_Click( object  sender, EventArgs e)  
  163.         {  
  164.             //复位接受和发送的字节数计数器并更新界面。   
  165.             send_count = received_count = 0;  
  166.             labelGetCount.Text = "Get:0" ;  
  167.             labelSendCount.Text = "Send:0" ;  
  168.         }  
  169.     }  
  170. }  

至此,不会再出现关闭死锁问题了。

希望这篇文章能解你的燃眉之急,非常高兴能与读者分享我层遇到,大多数人都遇到的这个问题。如果说的不明白,欢迎讨论。

后续的有关通讯程序底层设计的文章会讲述一个具有丰富扩展性,但有设计简介的万能通讯库,支持网络、蓝牙、串口通讯、并口通讯。但不要指望我都实现 出来了,我只是设计出这个框架。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值