C#串口通信相关

有一关于串口通信的题目,在寝室闲来无事考虑用代码进行实现。

因为考虑到以GUI的方式呈现,所会的语言仅仅包括C/C++、C#以及少量python【非常的惭愧,大学啥也没学会 = =】

由于以前用过C#做过一些小工具,因此选用该语言进行实现。

本次选用的串口为RS-232,其电气特性参考ELA(美国电子工业协会)的相关文档。

-----------------------------------------

在进行代码实现的时候,需要先了解关于串口的基本参数。

1.波特率

2.停止位

3.数据位

4.奇偶校验方式

本次选用的最常规的设置参数:即波特率9600 停止位1,数据位8,不适用校验方式(None)

在进行代码编写之前,先分析该项目所需要的考虑的功能。

1.能够检索计算机的串口,并且对不同串口进行操作。

2.能过通过串口发送和接受信息。

因为只考虑2个基本功能,因此本次操作较为简单,只是单纯的实现串口通信模拟以及记录一些常见的问题。

整个软件的UI设计如下:    

    

其中,在该软件启动时,能够加载计算机上的串口数目,也就是需要对串口进行初始化:

private void IniPort()
        {
            string[] str = SerialPort.GetPortNames();

            if (str.Length == 0)
            {
                MessageBox.Show("本计算机没有串口");
                return;
            }//检查串口

            foreach (string i in str)
            {
                cbSerial.Items.Add(i);
            }//将获取的串口添加

            cbSerial.SelectedIndex = 0;//设置默认选项
        }//获取串口

通过system.IO.Ports所包含的函数SerialPort.GerPortNames进行串口获取,并将其放入待选列表(cbSerial),该函数运行在Form1_Load中,也就是加载界面时进行获取。

在获取的计算的串口后,要对串口进行操作时首先需要打开串口。我们在打开串口之前,需要对打开的串口进行基本参数设置。在本次程序中,我们用全局变量Sp1来代表我们所操作的串口,避免在不同函数中都需要对串口进行重新定义:

               string strPort = cbSerial.Text;//获取所选中的串口
               sp1.PortName = strPort;//串口名称                    
               sp1.BaudRate = 9600;//波特率
               sp1.DataBits = 8;//数据位
               sp1.StopBits = StopBits.One;//停止位
               sp1.Parity = Parity.None;//奇偶校验or无校验
               sp1.ReceivedBytesThreshold = 1;//DataReceived事件发生前内部输入缓冲区的字节数
               sp1.WriteTimeout = 3000;//写超时
               sp1.ReadTimeout = 3000;//读超时
同时,将所设置的参数表现在UI中
                    textBox1.Text = sp1.PortName;
                    textBox2.Text = sp1.BaudRate.ToString();
                    textBox3.Text = sp1.StopBits.ToString();
                    textBox4.Text = sp1.DataBits.ToString();
                    textBox5.Text = sp1.Parity.ToString();
原则上而言,在我们执行打开串口操作的时候,要确认串口本身没有处于打开状态,因此,需要使用SerialPort.IsOpen来判断串口是否打开,如果已经打开,将其关闭,再将其打开,之后清空该串口的缓冲区,该变量为布尔变量:
 
 
if (sp1.IsOpen)
{
   sp1.Close();
}                
sp1.Open();
sp1.DiscardOutBuffer();
sp1.DiscardInBuffer();//清空串口缓存区

需要注意的时,整个过程需要包含异常处理,以防止出现不可知的异常,特别是对串口本身不是很熟悉的情况下。

关于关闭串口功能,本次采用的是将一个按钮进行复用,即用一个按钮实现开/关功能,因此,在按钮的Onclick函数中需要先判断是进行打开还是关闭操作。

以上是常规的串口检索与打开操作,并没有发现什么不对的地方,如果有什么问题,希望能够相互讨论。

接下来就是博主遇见了很多问题的步骤,也是在我查询资料的过程中发现问题出现比较多的地方——串口数据的发送和接受。

本来而言,串口数据的发送和接受并不困难。C#中提供了serialport.write进行16进制数的发送,也提供了serialport.writeLine进行字符型数据的发送,对应的接受为serialport.Read与serialport.readline。

在这里需要考虑到的第一个问题是,我们在GUI上只能看见发送按钮,那么如何让一个按钮实现发送与接受功能呢?

博主之前对于编程接触的很少,很多朋友应该一目了然,C#中的事件与委托恰好可以完成我们的目标。关于事件和委托,文末会附上一些博主的参考资料,希望与大家共同进步。

由于目前只是想进行简单的串口收发调试,因此,在数据类型方面,只使用了字符型数据,关于十六进制数据,需要string到byte的转变之后再进行发送,这里就不再详谈。

发送按钮的函数编辑也很简单:

if (!sp1.IsOpen)
            {
                MessageBox.Show("请打开串口!");
                return;
            }
            else
            {
                string strSend = textSend.Text;
                sp1.WriteLine(strSend);
               // MessageBox.Show("串口发送成功");
              
               
            }
        }//发送字符型数据

同样,先对串口的开关状态进行检查,而后读取待发送区中的数据【也就是我们输入想要发送的内容】,将其通过sp1串口发送出去。

在进行串口参数设置的时候,有一个参数叫做ReceivedBytesThreshold,VS2017中对其的解释为:

“DataReceived事件发生前内部输入缓冲区的字节数”

我们将其置于1,也就是当缓冲区中有1个字节时就触发DataReceived事件。

之前就说过,我们的目的是想要发送后就触发接收事件,那么我们就要用到DataReceived。我们将这个事件绑定在窗口加载时,也就是form_load函数中:

sp1.DataReceived += new SerialDataReceivedEventHandler(sp1_DataReceived);

这个函数有2个地方是我们要注意的,其中是DataRecevied,是我们串口接收到数据后所触发的事件,之后是sp1_DataReveived,是指该事件委托的方法,也就是我们需要定义的,当我们触发事件时需要做的东西。

整个过程中,我们通过serialport.writeLine进行发送,将数据发送到串口的缓冲区,满足ReceivedBytesthreShold规定的字节后,我们触发DataReceived事件。需要注意的是,串口发送的过程是有时延的,博主最开始在发送后立即通过BytestoRead检查接受缓冲区的字节数, 有可能出现字节数为0,不能用此进行信息的发送判断。只能通过是否触发DataReceived事件来判断是否发送成功。

那么当我们触发时间需要做什么呢?我们要把数据从缓冲区读出来,然后将其显示在我们的软件上。

通过serialport.readline将缓冲区中的字符型数据读出:

string strRev = sp2.ReadLine();

博主最开始想通过TextRev.text=strRev的方法将其表示在接收区,但是会抛出异常。原因是因为serialport.readline函数的执行过程中创建了一个辅助线程进行读取,因此我们的strRev是在辅助线程中,如果我们进行跨线程操作,就会导致异常,在这个地方,我们需要用的invoke来避免这个问题。

this.Invoke((EventHandler)delegate
                {
                    textRev.Text += strRev;
                });

关于invoke的使用博主也刚开始接触,了解的不是很充分,但是大体而言,涉及到跨线程的操作,应该都会使用这个方法。

正常情况下,这就是基本的串口通信流程,接下来记录一些博主在编程时遇到的问题以及一些解决方法以备不时之需。

-------------------------------------

博主本身没有串口设备,笔记本电脑也只有USB口,因此使用VSPD软件安装虚拟串口。吐槽一句,也就是这个虚拟串口软件折磨了我快一天的时间_(:з」∠)_Sad

感觉也会有小伙伴遇见,因此拿出来和大家分享

通过VSPD软件安装的串口如下:


在软件中我选择的是COM1口,但是发现无论如何也无法触发DataReceived事件,因此我单独设立了一个按钮来进行数据读取,来检测是否是事件触发失败的原因,但是当我用serialpor.readline后,程序进入死锁状态。初步判定为要么缓冲区没有数据,要么缓冲区数据量极大。但是缓冲区大小只有2048字节,不应该长时间没有响应。因此判定为第一种状态。

于是我同时使用serialport.BytesToWrite和serialport.BytesToRead来监视输入缓冲区与接受缓冲区的字节数,发现无论怎么执行都是0.也就是serialport.write函数并没有执行。由于又没有异常抛出,因此函数本身是没有问题的。

那么所发送的数据在哪里呢?其实回头看看这个问题是非常理所应得的,我的发送口为COM1,虚拟串口连接的口为COM2。也就是说,COM1口发送的数据应该给了COM2口。

因此,我在建立第二个串口的全局变量sp2,设置其为COM2口,配置与COM1完全相同的基本参数。

再去监听sp2的BytesToRead就发现果然数据储存在了接受口的缓冲区。

因此,在整个程序中,我们需要引入2个串口sp1,sp2

同时,也需要对代码进行修改,我们的接受事件应该为sp2的DataReceived事件。更改完之后果然解决了问题。实现了基本的串口数据的收发。

但是依然有一些疑惑,希望以后的学习中能够进行解决:

在操作时我们同时使用了输入缓冲区与接受缓冲区,是否意味着一个串口对应2个数据池?

为什么在COM1口输出数据后,数据只出现在COM2口的待读区(因为数据是发送给COM2的,因此这里出现没有问题),而COM1口的输入缓冲区中没有数据,是否意味着输入缓冲区的数据发送后自动清空?如何使得数据暂留在输入缓冲区中暂时不发送?


猜你喜欢

转载自blog.csdn.net/qq_35995375/article/details/79962826