C#串口通信及数据表格存储

1.开发环境

系统:win10

开发工具:Visual Studio 2017

程序下载地址:https://download.csdn.net/download/wang0huan/11145500

分不够的可留言邮箱。

2.界面设计

串口通信的界面大致如此,在此基础上添加项目所需的调试指令与数据存储功能,界面排布方面可参考其他教程。

3.串口通信的实现

本文串口通信主要使用的技术点有:队列、多线程、List等,相比传统单线程方案更加稳定,适合大数据收发。

  • 串口基础参数设置

  

        #region 设置串口的属性SetPortProperty
        private void SetPortProperty()//设置串口的属性
        {
            sp = new SerialPort
            {
                PortName = cbPortName.Text.Trim(), //设置串口名
                //BaudRate = Convert.ToInt32(cbBaudRate.Text.Trim()) //设置串口波特率
                BaudRate = int.Parse(cbBaudRate.Text.Trim())
            };
            float f = Convert.ToSingle(cbStop.Text.Trim());//设置停止位
            if (f == 0)
            {
                sp.StopBits = StopBits.None;
            }
            else if (f == 1.5)
            {
                sp.StopBits = StopBits.OnePointFive;
            }
            else if (f == 1)
            {
                sp.StopBits = StopBits.One;
            }
            else if (f == 2)
            {
                sp.StopBits = StopBits.Two;
            }
            else
            {
                sp.StopBits = StopBits.One;
            }
            sp.DataBits = Convert.ToInt16(cbDataBits.Text.Trim());//设置数据位

            string s = cbParity.Text.Trim();//设置奇偶校验位
            if (s.CompareTo("") == 0)
            {
                sp.Parity = Parity.None;
            }
            else if (s.CompareTo("奇校验") == 0)
            {
                sp.Parity = Parity.Odd;
            }
            else if (s.CompareTo("偶校验") == 0)
            {
                sp.Parity = Parity.Even;
            }
            else
            {
                sp.Parity = Parity.None;
            }
            sp.ReadTimeout = -1; //设置超市读取时间
            //sp.NewLine = "/r/n"; //根据实际情况吧,当使用ReadLine()时需要定义一下
            //sp.RtsEnable = true; //启用RTS发送请求信号,根据实际情况吧

            //定义串口DataReceived事件,当串口接受到数据后触发事件
            sp.DataReceived += Sp_DataReceived; //添加事件注册
        }
        #endregion

下面重点说明串口的发送和接收的实现方法:

  • 串口接收实现

  串口接收事件Sp_DataReceived,串口组件的接收触发事件,接收数据并在接收文本框显示(便于调试),注意多线程访问UI资源要用invoke方式来同步,同时将接收的数据添加到SerialRevList中,同时加锁保护List。同时用一个独立线程来进行接收数据的处理,线程之间数据加锁同步。

        #region 串口接受事件Sp_DataReceived
        private void Sp_DataReceived(object sender, SerialDataReceivedEventArgs e)//串口接受事件
        {
            try
            {
                if (this.sp.BytesToRead > 0)
                {
                    byte[] buffer = new byte[this.sp.BytesToRead];
                    this.sp.Read(buffer, 0, buffer.Length);
                    received_count += buffer.Length;//增加接收计数
                    Rev_builder.Clear();//清除字符串构造器的内容
                    //因为要访问ui资源,所以需要使用invoke方式同步ui,串口接收事件会自动创建线程,多线程访问控件需要使用invoke来委托
                    this.Invoke((EventHandler)(delegate
                    {
                        if (rbRcvHex.Checked == false)//接受数据字符串显示
                        {
                            //tbxRcvData.Text += sp.ReadLine(); //一直读取到输入缓冲区中的 NewLine 值,使用这个需要注意换行符
                            //直接按ASCII规则转换成字符串  
                            Rev_builder.Append(Encoding.Default.GetString(buffer));
                        }
                        else//接受数据Hex显示
                        {
                            //依次的拼接出16进制字符串  
                            foreach (byte b in buffer)
                            {
                                Rev_builder.Append(b.ToString("X2") + " ");
                            }
                        }
                        this.tbxRcvData.AppendText(Rev_builder.ToString());//接受数据显示在文本框
                        labelRcvCount.Text = "接收字节数:" + received_count.ToString();//修改接收计数
                        sp.DiscardInBuffer();//丢弃接受缓冲区数据
                    }));

                    Monitor.Enter(this.SerialRevList);   //数据保护
                    this.SerialRevList.AddRange(buffer); //交由接收处理线程处理
                    Monitor.Exit(this.SerialRevList);
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }
        #endregion

单独开一个线程来进行串口接收数据的处理,下面给出了常用帧处理给出的实例,充分利用了List集合带来的数据处理的便捷;

        #region 串口接受缓存数据处理线程函数
        /// <summary>
        /// 串口接受缓存数据处理线程函数
        /// </summary>
        /// <param name="obj"></param>
        private void SerialRev(object obj)
        {
            while (true)
            {
                try
                {
                    this.SerialRevWaiter.WaitOne();
                    Monitor.Enter(this.SerialRevList);

                    //AWGM.AWGHandle(SerialRevData);
                    //至少包含帧头(1字节)+长度(2字节)+数据 + 结束位(1字节)
                    if (this.SerialRevList.Count > 0) //接收缓存有数据
                    {
                        //AWG接收字符串协议处理方式
                        string str = Encoding.Default.GetString(SerialRevList.ToArray());
                        if (str.Substring(0, 1) == "*")  //起始位
                        {
                            int len = Convert.ToInt32(str.Substring(1, 2));
                            if (SerialRevList.Count < len + 4)//未接收完整
                            {
                                break;
                            }
                            else
                            {
                                if (str.Substring(len + 3, 1) == "^")//结束位
                                {
                                    //lineyValue1 = Convert.ToDouble(str.Substring(3, 5));
                                    //lineyValue2 = Convert.ToDouble(str.Substring(8, 5));

                                    //多线程访问控件使用invoke来委托
                                    this.Invoke((EventHandler)(delegate
                                    {
                                        this.tbxRcvData.AppendText(str);
                                        //tbxAWGMTem.Text = str.Substring(3, 5);
                                    }));
                                }

                            }
                            SerialRevList.RemoveRange(0, len + 4);//去掉处理一帧的数据
                        }
                        else
                        {
                            SerialRevList.RemoveAt(0);//帧头不正确时,记得清除
                        }

                        //IODH接收16进制处理方式

                    }
                    Monitor.Exit(this.SerialRevList);
                }
                catch (Exception ex)
                {
                    MessageBox.Show(ex.Message, "串口接受线程处理错误!", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
                Thread.Sleep(50);
            }
        }

        #endregion
  • 串口发送

  考虑到UI界面可能触发多个串口发送需求,此时也需要用到多线程来保证数据发生的安全;

  串口发送采用队列的方式来存储发送数据,然后利用一个线程来一次发送队列中的数据;

  串口发送实例:

private void btnTECPIDSet_Click(object sender, EventArgs e)
        {
            Monitor.Enter(this.SerialSendQueue);
            this.SerialSendQueue.Enqueue("pidsettem" + string.Format("{0:0.000}", Convert.ToSingle(tbxTecKp.Text)) + string.Format("{0:0.000}", 
       Convert.ToSingle(tbxTecKi.Text)) + string.Format("{0:0.000}", Convert.ToSingle(tbxTecKd.Text))); Monitor.Exit(this.SerialSendQueue); }

  串口发送线程:

#region 串口发送数据线程函数
        /// <summary>
        /// 这个线程函数负责发送串口命令
        /// </summary>
        /// <param name="obj"></param>
        private void SerialSend(object obj)
        {
            while (true)
            {
                try
                {
                    this.SerialSendWaiter.WaitOne();
                    Monitor.Enter(this.SerialSendQueue); //队列排他锁,实现同步访问
                    string buf = null;
                    if (this.SerialSendQueue.Count > 0) //有命令
                    {
                        buf = this.SerialSendQueue.Dequeue(); //取出发送队列第一个命令
                    }
                    if (buf != null)
                    {
                        byte[] buffer = Encoding.Default.GetBytes(buf);
                        this.sp.DiscardInBuffer();
                        this.sp.Write(buffer, 0, buffer.Length);

                        tbxSendData.AppendText(buf + "\r\n");
                        send_count += buf.Length;
                        labelSendCount.Text = "发送字节数:" + send_count.ToString();//更新发送计数
                    }
                    Monitor.Exit(this.SerialSendQueue); //释放锁
                    Thread.Sleep(100);
                }
                catch (Exception ex)
                {
                    throw ex;
                }
            }
        }
        #endregion

4.表格数据存储EXCEL

常用测试中经常用到测试数据的自动化记录,这样由串口采集输出到excel中功能就变得非常实用;

下面介绍实现方法:结合的技术是dataGridView控件和SaveFileDialog;

由于电脑不一定都安装有office,直接使用office组件功能缺乏一定的通用性,因此本实例采用存储为csv格式表格数据文件,然后根据设置的格式转化为EXCEL文件即可。

代码参考如下:

#region 测试数据存储(DataGridView和DataTable的使用)
        /// <summary>
        /// 初始化DataTable,并将datatable绑定到DataGridView的数据源,新建列标题
        /// </summary>
        private void InitDatable()
        {
            //新建列  
            DataColumn col1 = new DataColumn("休眠", typeof(string));
            DataColumn col2 = new DataColumn("门磁A-12V", typeof(string));
            DataColumn col3 = new DataColumn("门磁A-5V", typeof(string));
            DataColumn col4 = new DataColumn("门A-LED", typeof(string));
            DataColumn col5 = new DataColumn("门磁B-12V", typeof(string));
            DataColumn col6 = new DataColumn("门磁B-5V", typeof(string));
            DataColumn col7 = new DataColumn("门B-LED", typeof(string));
            //添加列  
            dt.Columns.Add(col1);
            dt.Columns.Add(col2);
            dt.Columns.Add(col3);
            dt.Columns.Add(col4);
            dt.Columns.Add(col5);
            dt.Columns.Add(col6);
            dt.Columns.Add(col7);

            this.dataGridView1.DataSource = dt.DefaultView;
        }

        /// <summary>
        /// 按键触发记录测试数据,即添加行数据
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnTestRecord_Click(object sender, EventArgs e)
        {
            //dt.Rows.Clear();//清空数据  
            DataRow dr = dt.NewRow();//新增行 
            dr[0] = "OK";
            dr[1] = "OK";
            dr[2] = "OK";
            dr[3] = "OK";
            dr[4] = "OK";
            dr[5] = "OK";
            dr[6] = "F";
            this.dt.Rows.Add(dr);//增加行
        }

        /// <summary>
        /// 清除表格记录内容
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnCleanRecord_Click(object sender, EventArgs e)
        {
            dt.Rows.Clear();//清空数据
        }

        /// <summary>
        /// 测试记录导出
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnExport_Click(object sender, EventArgs e)
        {
            IODM_TEST.DataGridViewToExcel(dataGridView1);
        }
        #endregion
DataGridViewToExcel实现如下:
#region DateGridView导出到csv格式的Excel     
        /// <summary>     
        /// 常用方法,列之间加/,一行一行输出,此文件其实是csv文件,不过默认可以当成Excel打开。     
        /// </summary>     
        /// <remarks>     
        /// using System.IO;     
        /// </remarks>     
        /// <param name="dgv">表格数据控件</param>     
        public static void DataGridViewToExcel(DataGridView dgv)
        {
            SaveFileDialog dlg = new SaveFileDialog();
            dlg.Filter = "Execl files (*.csv)|*.csv";
            dlg.FilterIndex = 0;
            dlg.RestoreDirectory = true;
            dlg.CreatePrompt = true;
            dlg.Title = "保存为Excel文件";

            if (dlg.ShowDialog() == DialogResult.OK)
            {
                Stream myStream;
                myStream = dlg.OpenFile();
                StreamWriter sw = new StreamWriter(myStream, System.Text.Encoding.GetEncoding(-0));
                string columnTitle = "";
                try
                {
                    //写入列标题     
                    for (int i = 0; i < dgv.ColumnCount; i++)
                    {
                        if (i > 0)
                        {
                            columnTitle += "/";
                        }
                        columnTitle += dgv.Columns[i].HeaderText;
                    }
                    sw.WriteLine(columnTitle);

                    //写入列内容     
                    for (int j = 0; j < dgv.Rows.Count; j++)
                    {
                        string columnValue = "";
                        for (int k = 0; k < dgv.Columns.Count; k++)
                        {
                            if (k > 0)
                            {
                                columnValue += "/";
                            }
                            if (dgv.Rows[j].Cells[k].Value == null)
                                columnValue += "";
                            else
                                columnValue += dgv.Rows[j].Cells[k].Value.ToString().Trim();
                        }
                        sw.WriteLine(columnValue);
                    }
                    sw.Close();
                    myStream.Close();
                }
                catch (Exception e)
                {
                    MessageBox.Show(e.ToString());
                }
                finally
                {
                    sw.Close();
                    myStream.Close();
                }
            }
        }
        #endregion

 

 
posted @ 2018-05-16 14:50  silencehuan  阅读(8875)  评论(40编辑  收藏  举报