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