实现上位机和下位机之间的通信,通常使用的是串口通信,接下来实现一个通过上位机和串口调试助手来完成串口通信测试。

  首先创建一个WInfrom窗体应用工程文件,创建过程可参考https://www.cnblogs.com/xionglaichuangyichuang/p/13734179.html;

  在创建好的工程下面,通过工具箱中已有的控件完成界面的搭建,如下图所示,为了方便初学者容易看懂程序,下图将控件的命名一并标注出来:

C# 简易的串口监视上位机实现-LMLPHP

  直接进入正题,将完整的工程代码黏贴出来:

C# 简易的串口监视上位机实现-LMLPHPC# 简易的串口监视上位机实现-LMLPHP
  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.Threading.Tasks;
  9 using System.Windows.Forms;
 10 using System.IO.Ports;
 11 using System.Diagnostics;
 12
 13 namespace Tem_Hum_Monitorring
 14 {
 15
 16     public partial class Form1 : Form
 17     {
 18         //实例化串口
 19         SerialPort s = new SerialPort();
 20
 21         public Form1()
 22         {
 23             InitializeComponent();
 24             Control.CheckForIllegalCrossThreadCalls = false;
 25             button1.Text = "打开串口";
 26             int[] item = { 9600,115200};    //遍历
 27             foreach (int a in item)
 28             {
 29                 comboBox2.Items.Add(a.ToString());
 30             }
 31             comboBox2.SelectedItem = comboBox2.Items[1];
 32         }
 33
 34         private void Form1_Load(object sender, EventArgs e)
 35         {
 36             portInit();
 37         }
 38
 39         /// <summary>
 40         /// 串口初始化
 41         /// </summary>
 42         private void portInit()
 43         {
 44             string[] ports = SerialPort.GetPortNames();
 45             comboBox1.Items.AddRange(ports);
 46             comboBox1.SelectedItem = comboBox1.Items[0];
 47         }
 48
 49         #region 开关串口
 50         private void button1_Click(object sender, EventArgs e)
 51         {
 52             try
 53             {
 54                 if (!s.IsOpen)
 55                 {
 56                     s.PortName = comboBox1.SelectedItem.ToString();
 57                     s.BaudRate = Convert.ToInt32(comboBox2.SelectedItem.ToString());
 58                     s.Open();
 59                     s.DataReceived += s_DataReceived; //"+="代表指定响应事件时要调用的方法
 60                     button1.Text = "关闭串口";
 61                 }
 62                 else
 63                 {
 64                     s.Close();
 65                     s.DataReceived -= s_DataReceived;
 66                     button1.Text = "打开串口";
 67                 }
 68             }
 69             catch(Exception ee)
 70             {
 71                 MessageBox.Show(ee.ToString());
 72             }
 73         }
 74         #endregion
 75
 76         #region 串口接收
 77         void s_DataReceived(object sender, SerialDataReceivedEventArgs e)
 78         {
 79             int count = s.BytesToRead;
 80             string str = null;
 81             if (count == 8)
 82             {
 83                 //数据解析
 84                 byte[] buff = new byte[count];
 85                 s.Read(buff, 0, count);
 86                 foreach (byte item in buff)
 87                 {
 88                     str += item.ToString("X2") + " ";
 89                 }
 90                 richTextBox1.Text = "[" + System.DateTime.Now.ToString() + "] " + str + "\n" + richTextBox1.Text;
 91                 if (buff[0] == 0x04)
 92                 {
 93                     ID.Text = buff[0].ToString();
 94                     switch (buff[2])
 95                     {
 96                         case 0x01:
 97                             {
 98                                 Tem.Text = (buff[5] * 4 + buff[4] * 0.05 - 30).ToString();
 99                                 Hum.Text = (buff[6] + buff[7]).ToString();
100                                 break;
101                             }
102                         case 0x02:
103                             {
104                                 Light.Text = (buff[6] + buff[7]).ToString();
105                                 break;
106                             }
107                         case 0x04:
108                             {
109                                 Dust.Text = (buff[6] + buff[7]).ToString();
110                                 break;
111                             }
112                         default:
113                             break;
114                     }
115                 }
116             }
117             else
118             {
119                 //当接收数据不在设定的数据位范围之内时,会出现接受到的数据一直保存在接收缓存区之内,后续每次接手数据都会将上一次的数据进行叠加,造成只能通过关闭串口的方法来清除缓冲区的数据
120                 s.DiscardInBuffer(); //丢弃来自串行驱动程序的接收缓冲区的数据
121             }
122         }
123         #endregion
124
125         #region 串口发送
126         private void button3_Click(object sender, EventArgs e)
127         {
128             string[] sendbuff = richTextBox2.Text.Split();
129             Debug.WriteLine("发送字节数:" + sendbuff.Length);
130             foreach (string item in sendbuff)
131             {
132                 int count = 1;
133                 byte[] buff = new byte[count];
134                 buff[0] = byte.Parse(item, System.Globalization.NumberStyles.HexNumber);
135                 s.Write(buff,0,count);
136             }
137         }
138         #endregion
139
140         private void button2_Click(object sender, EventArgs e)
141         {
142             int count = 1;
143             byte[] buff = new byte[count];
144             buff[0] = byte.Parse("04", System.Globalization.NumberStyles.HexNumber);
145             s.Write(buff, 0, count);
146         }
147     }
148 }
View Code

  在Winfrom窗体设计中,实现串口可以通过工具箱中的串口控件来实现,不过一般推荐直接通过代码来实例化串口,实例化串口需使用如下代码来实现:

    //实例化串口
    SerialPort s = new SerialPort();

  串口初始化可以在窗体的Load函数中实现,以下初始化可以自动化取当前设备中的存在的串口,包括真实串口和虚拟串口:

private void Form1_Load(object sender, EventArgs e)
        {
            portInit();
        }

        /// <summary>
        /// 串口初始化
        /// </summary>
        private void portInit()
        {
            string[] ports = SerialPort.GetPortNames();
            comboBox1.Items.AddRange(ports);
            comboBox1.SelectedItem = comboBox1.Items[0];
        }

  通过对开关按键button1控件的点击事件,实现串口的开关,通过对控件的文字修改,可以实现一个控件机能实现开又能实现关串口的作用:

        #region 开关串口
        private void button1_Click(object sender, EventArgs e)
        {
            try
            {
                if (!s.IsOpen)
                {
                    s.PortName = comboBox1.SelectedItem.ToString();
                    s.BaudRate = Convert.ToInt32(comboBox2.SelectedItem.ToString());
                    s.Open();
                    s.DataReceived += s_DataReceived; //"+="代表指定响应事件时要调用的方法
                    button1.Text = "关闭串口";
                }
                else
                {
                    s.Close();
                    s.DataReceived -= s_DataReceived;
                    button1.Text = "打开串口";
                }
            }
            catch(Exception ee)
            {
                MessageBox.Show(ee.ToString());
            }
        }
        #endregion

  串口数据接收和数据解析,首先获取数据接收缓存区数据的字节长度,通过确认长度是否是设定中的长度大小,如果是设定的8位数据长度则对接收的数据进行解析:

        #region 串口接收
        void s_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            int count = s.BytesToRead;
            string str = null;
            if (count == 8)
            {
                //数据解析
                byte[] buff = new byte[count];
                s.Read(buff, 0, count);
                foreach (byte item in buff)
                {
                    str += item.ToString("X2") + " ";
                }
                richTextBox1.Text = "[" + System.DateTime.Now.ToString() + "] " + str + "\n" + richTextBox1.Text;
                if (buff[0] == 0x04)
                {
                    ID.Text = buff[0].ToString();
                    switch (buff[2])
                    {
                        case 0x01:
                            {
                                Tem.Text = (buff[5] * 4 + buff[4] * 0.05 - 30).ToString();
                                Hum.Text = (buff[6] + buff[7]).ToString();
                                break;
                            }
                        case 0x02:
                            {
                                Light.Text = (buff[6] + buff[7]).ToString();
                                break;
                            }
                        case 0x04:
                            {
                                Dust.Text = (buff[6] + buff[7]).ToString();
                                break;
                            }
                        default:
                            break;
                    }
                }
            }
            else
            {
                //当接收数据不在设定的数据位范围之内时,会出现接受到的数据一直保存在接收缓存区之内,后续每次接手数据都会将上一次的数据进行叠加,造成只能通过关闭串口的方法来清除缓冲区的数据
                s.DiscardInBuffer(); //丢弃来自串行驱动程序的接收缓冲区的数据
            }
        }
        #endregion

  当接收到的数据长度不等于8的时候,将丢弃来自串行驱动程序的接收缓冲区的数据,接下来通过断点调试来分析丢弃缓冲区和不丢弃缓冲区数据两种情况进行仿真,分析如下几点。

  • 使用串口助手给上位机发送数据数据位长度为8位的数据,串口调试助手和上位机的终端的显示界面如下,发送端数据和接收端数据一样,并未出现异常:

C# 简易的串口监视上位机实现-LMLPHP

  •  将串口调试助手发送数据位修改成9位之后,进行发送,可以发现上位机并未接收到相关的数据:

  •  接着修改串口调试助手的发送数据位,修改成8位,可以发现上位机尚未能接收到来自串口调试助手发来的数据,这是为什么呢?

C# 简易的串口监视上位机实现-LMLPHP

  •  接下来将通过断点逐步进行调试,来解释是为啥上位机没有接收到调试助手发来的数据,当串口调试助手发来的数据长度位9位时,通过监视器可以查看到接收缓冲器中的数据长度长度是9

  •  第一次点击完发送之后,上位机未能成功接收到数据,我们就会好奇,并且一般都会点击第二次、第三次、甚至一直点下去,观察是否会出现啥异常现象,当点击第二次时,通过监视窗口,可以观察到到串口缓冲区的数据长度变成了18,这是因为缓冲区将上一次接收的数据给保留了下来并没有删除,就算下次发送的数据长度为8位的时候,也一样是通过叠加的方式将其保存到缓冲区,这样就会造成缓冲区的数据位长度会一直大于8;如果不通过 s.DiscardInBuffer()方法丢弃来自串行驱动程序的接收缓冲区的数据,就只能通过关闭串口然后重新打开相应的串口来实现缓冲区的数据清除。

C# 简易的串口监视上位机实现-LMLPHP

  •  使用s.DiscardInBuffer()对不符合长度的数据进行丢弃,实现的效果如下所示:

C# 简易的串口监视上位机实现-LMLPHP

   需要完整源码的朋友可以通过以下链接进行下载,如有大佬有更好的优化意见欢迎一块进行讨论,谢谢!

  链接:https://pan.baidu.com/s/1Yb1YjdAZfficRtx2srBpzw
  提取码:7yc2

11-21 00:35