前言
上一篇文章《ISP 串口调试工具的开发(一)》介绍了如何实现一个简单具有读写功能的串口调试工具,这篇文章会介绍进阶版的串口调试工具。
因为上一篇文章的所有读写操作都是在主线程中进行的,但是这样存在一个弊端,因为在 Windows 的桌面开发中,由于界面编程中的 “Windows 消息队列” 机制,当在主线程(UI 线程)中有耗时较长的操作的话,就会造成界面卡、不流畅、反应慢等现象。如果界面出现长时间静止,会造成使用者觉得软件卡死的错觉。可以查看《.net开发笔记(十三)Winform 常用开发模式第一篇》了解更多。
数据接收
在接收数据时,可以使用 SerialPort.DataRecevied 事件来接收,也可以为数据接收新开启一个新线程。
使用事件接收
使用 SerialPort.DataRecevied 事件接收数据,需要注册接收事件创建 SerialDataReceivedEventHandler 委托,即把接收数据的事件关联到相应的事件去,否则接收事件发生时无法触发对应的方法, DataRecevied 只做数据接收,数据处理则是交给了委托进行。
private void Demo_Load(object sender, EventArgs e)
{
Init_Port_Confs();
serialPort.DataReceived += new SerialDataReceivedEventHandler(DataReceived);
}
private void DataReceived(object sender, SerialDataReceivedEventArgs e)
{
if(serialPort.IsOpen)
{
byte[] receviedData = new byte[serialPort.BytesToRead];
serialPort.Read(receviedData, 0, receivedData.Length);
//数据处理操作
}
}
使用线程接收
本文采用的是开启新的监听线程方式,当点击读取参数的按钮后,开启一个新线程 loadThread 关联委托方法 LoadData,将数据的处理放在委托中:
/// <summary>
/// 读取参数按钮事件
/// </summary>
private void BtnGetData_Click(object sender, EventArgs e)
{
if (!serialPort.IsOpen)
{
MessageBox.Show("串口未连接!", "传输错误");
return;
}
BtnSendParams.Enabled = false;
BtnGetParams.Enabled = false;
Thread loadThread = new Thread(LoadData);
try
{
loadThread.Start();
}
catch (Exception exception)
{
MessageBox.Show(exception.Message);
loadThread.Abort();
}
}
在 LoadData 中,读取了缓冲区中的数据后将参数渲染到对应的 TextBox:
/// <summary>
/// 加载参数
/// </summary>
private void LoadData()
{
// 清空缓冲区
serialPort.DiscardInBuffer();
int byteCount = 0;
while (byteCount < 3)
{
byteCount = serialPort.BytesToRead;
}
// 获取数据并检查响应数据
byte[] data = new byte[byteCount];
serialPort.Read(data, 0, byteCount);
//展示数据
int index, value;
string controlName;
// 循环读取参数
for (int i = 0; i < data.Length; i += 3)
{
index = (int)data[i];
// 获取参数名
controlName = "param" + index;
// 获取参数值
value = data[i + 1] * 256 + data[i + 2];
TextBox textbox = (TextBox)Controls.Find(controlName, true)[0];
textbox.Text = value.ToString();
}
CourseRecord("读取参数完成!");
BtnSendParams.Enabled = true;
BtnGetParams.Enabled = true;
}
数据发送
同样的,在数据发送中,也是开启一个新线程 sendThread 关联委托方法 SendData,数据处理交给委托方法:
/// <summary>
/// 发送参数按钮事件
/// </summary>
private void BtnSendParams_Click(object sender, EventArgs e)
{
if (!serialPort.IsOpen)
{
MessageBox.Show("串口未连接!", "传输错误");
return;
}
BtnSendParams.Enabled = false;
BtnGetParams.Enabled = false;
Thread sendThread = new Thread(SendData);
try
{
sendThread.Start();
}
catch (Exception exception)
{
MessageBox.Show(exception.Message);
sendThread.Abort();
}
}
在 SendData 中,先清空发送缓冲区的数据,然后读取非空的参数发送出去:
/// <summary>
/// 发送数据
/// </summary>
private void SendData()
{
// 清空缓冲区
serialPort.DiscardOutBuffer();
ushort length = 0;
// 计算参数个数
foreach (Control control in groupParams.Controls)
{
if (control is TextBox && control.Text.Length != 0)
{
length = (ushort)(length + 3);
}
}
// 获取数据字节数
byte[] len = new byte[2];
len[0] = (byte)(length << 8);
len[1] = (byte)(length << 0);
// 添加非空参数
byte[] data = new byte[length];
string name = "";
int i = 0;
byte[] value = new byte[2];
foreach (Control control in groupParams.Controls)
{
if (!(control is TextBox))
{
continue;
}
if (control.Text.Length != 0)
{
name = control.Name.Replace("param", "");
byte.TryParse(name, out byte type);
ushort.TryParse(control.Text, out ushort param);
data[i] = type;
value[0] = (byte)(param << 8);
value[1] = (byte)(param << 0);
value.CopyTo(data, i + 1);
i += 3;
}
}
//判断参数非空
if (length == 0)
{
MessageBox.Show("参数为空", "警告");
CourseRecord("参数为空,请重新发送!");
//CourseRecord("参数为空,请重新发送!");
TimeCount.Text = "";
BtnSendParams.Enabled = true;
BtnGetParams.Enabled = true;
return;
}
//发送数据
serialPort.Write(data, 0, data.Length);
CourseRecord("发送参数完成!");
BtnSendParams.Enabled = true;
BtnGetParams.Enabled = true;
}
跨线程操作
如果按上面的方式接收或发送数据,程序会出现异常,如下图:
因为上面的数据处理中,都有对 Windows 窗体上的控件进行操作,而 Windows GUI 编程中规定了,只能通过创建控件的线程来操作控件的数据。
因为如果多个线程都能对 UI 资源进行操作,就需要线程同步,避免并发冲突,否则多个线程与 UI 线程同时竞争 UI 资源,就可能造成页面混乱,甚至死锁。深入了解可以查看 《Invoke and BeInvoke》。
在接收数据中,无论事件接收还是开启线程接收,都会出现异常,因为都是从非创建控件的线程访问控件,其中 SerialPort.DataRecevied 是从辅助线程而非主线程上引发的,所以其中修改主线程的 UI 控件时会引发线程异常。
在这里有个简单暴力的解决方式,即关闭对错误线程调用的捕获:
/// <summary>
/// 加载主窗体
/// </summary>
private void Demo_Load(object sender, EventArgs e)
{
// 初始化串口
Init_Port_Confs();
// 关闭对错误线程调用的捕获
CheckForIllegalCrossThreadCalls = false;
}
但是这种方式并不是正确的解决方式,因为开启后任何子线程都能对 UI 线程的控件进行更改,这就不能保证自身事务的一致性,因为在其中一个线程更改了一个控件,如 Label ,另一个线程也更改了这个控件,这时就会造成页面显示的混乱。
下一篇文章将会给出正确的解决方式。
代码
下面贴出完整代码:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.IO.Ports;
using System.Threading;
namespace Demo
{
public partial class Demo : Form
{
//实例化串口对象
SerialPort serialPort = new SerialPort();
public Demo()
{
InitializeComponent();
}
/// <summary>
/// 初始化串口界面参数设置
/// </summary>
private void Init_Port_Confs()
{
/*------串口界面参数设置------*/
//检查是否含有串口
string[] ports = SerialPort.GetPortNames();
if (ports == null)
{
//cbSerials.SelectedIndex = -1;// 可以设置默认为 -1
MessageBox.Show("本机没有串口!", "Error");
return;
}
//添加串口选项
cbSerials.Items.AddRange(ports);
//设置默认串口选项
cbSerials.SelectedIndex = 0;
/*------波特率设置-------*/
string[] baudRate = { "9600", "19200", "38400", "57600", "115200" };
cbBaudRate.Items.AddRange(baudRate);
cbBaudRate.SelectedIndex = 0;
/*------数据位设置-------*/
string[] dataBit = { "5", "6", "7", "8" };
cbDataBits.Items.AddRange(dataBit);
cbDataBits.SelectedIndex = 3;
/*------校验位设置-------*/
string[] checkBit = { "None", "Even", "Odd", "Mask", "Space" };
cbParity.Items.AddRange(checkBit);
cbParity.SelectedIndex = 0;
/*------停止位设置-------*/
string[] stopBit = { "None", "One", "Two", "OnePointFive" };
cbStopBits.Items.AddRange(stopBit);
cbStopBits.SelectedIndex = 1;
}
/// <summary>
/// 加载主窗体
/// </summary>
private void Demo_Load(object sender, EventArgs e)
{
// 初始化串口
Init_Port_Confs();
CheckForIllegalCrossThreadCalls = false;
}
/// <summary>
/// 串口的开关操作
/// </summary>
private void BtnConnectSwitch_Click(object sender, EventArgs e)
{
if (!serialPort.IsOpen)// 串口处于关闭状态
{
try
{
// 判断所选端口是否有效
if (cbSerials.SelectedIndex == -1)
{
MessageBox.Show("错误:无效的端口,请重新选择", "Error");
return;
}
// 获取串口参数
string strSerialName = cbSerials.SelectedItem.ToString();
string strBaudRate = cbBaudRate.SelectedItem.ToString();
string strDataBit = cbDataBits.SelectedItem.ToString();
string strCheckBit = cbParity.SelectedItem.ToString();
string strStopBit = cbStopBits.SelectedItem.ToString();
serialPort.PortName = strSerialName;//串口号
serialPort.BaudRate = Convert.ToInt32(strBaudRate);//波特率
serialPort.DataBits = Convert.ToInt32(strDataBit);//数据位
switch (strStopBit) //停止位
{
case "None":
serialPort.StopBits = StopBits.None;
break;
case "One":
serialPort.StopBits = StopBits.One;
break;
case "OnePointFive":
serialPort.StopBits = StopBits.OnePointFive;
break;
case "Two":
serialPort.StopBits = StopBits.Two;
break;
default:
MessageBox.Show("错误:停止位参数不正确!", "Error");
break;
}
switch (strCheckBit) //校验位
{
case "None":
serialPort.Parity = Parity.None;
break;
case "Odd":
serialPort.Parity = Parity.Odd;
break;
case "Even":
serialPort.Parity = Parity.Even;
break;
default:
MessageBox.Show("错误:校验位参数不正确!", "Error");
break;
}
//打开串口
serialPort.Open();
CourseRecord("串口已打开!");
//串口开启时设置有效
BtnSendParams.Enabled = true;
BtnGetParams.Enabled = true;
}
catch (Exception ex)
{
//serialPort = new SerialPort();// 可以重新创建一个端口
MessageBox.Show("错误:" + ex.Message + "请检查端口是否被占用!", "Error");
return;
}
}
else // 串口处于打开状态
{
// 关闭串口
serialPort.Close();
CourseRecord("串口已关闭");
//串口关闭时设置有效
BtnSendParams.Enabled = false;
BtnGetParams.Enabled = false;
}
BtnConnectSwitch.Text = serialPort.IsOpen ? "断开连接" : "连接端口";
connectStatus.Text = serialPort.IsOpen ? "已连接" : "未连接";
//串口设置参数按钮控制
cbSerials.Enabled = !serialPort.IsOpen;
cbBaudRate.Enabled = !serialPort.IsOpen;
cbDataBits.Enabled = !serialPort.IsOpen;
cbParity.Enabled = !serialPort.IsOpen;
cbStopBits.Enabled = !serialPort.IsOpen;
BtnReset.Enabled = !serialPort.IsOpen;
BtnScan.Enabled = !serialPort.IsOpen;
}
/// <summary>
/// 读取参数按钮事件
/// </summary>
private void BtnGetData_Click(object sender, EventArgs e)
{
if (!serialPort.IsOpen)
{
MessageBox.Show("串口未连接!", "传输错误");
return;
}
BtnSendParams.Enabled = false;
BtnGetParams.Enabled = false;
Thread loadThread = new Thread(LoadData);
try
{
loadThread.Start();
}
catch (Exception exception)
{
MessageBox.Show(exception.Message);
loadThread.Abort();
}
}
/// <summary>
/// 加载参数
/// </summary>
private void LoadData()
{
// 清空缓冲区
serialPort.DiscardInBuffer();
int byteCount = 0;
while (byteCount < 3)
{
byteCount = serialPort.BytesToRead;
}
// 获取数据并检查响应数据
byte[] data = new byte[byteCount];
serialPort.Read(data, 0, byteCount);
//展示数据
int index, value;
string controlName;
// 循环读取参数
for (int i = 0; i < data.Length; i += 3)
{
index = (int)data[i];
// 获取参数名
controlName = "param" + index;
// 获取参数值
value = data[i + 1] * 256 + data[i + 2];
TextBox textbox = (TextBox)Controls.Find(controlName, true)[0];
textbox.Text = value.ToString();
}
CourseRecord("读取参数完成!");
BtnSendParams.Enabled = true;
BtnGetParams.Enabled = true;
}
/// <summary>
/// 发送参数按钮事件
/// </summary>
private void BtnSendParams_Click(object sender, EventArgs e)
{
if (!serialPort.IsOpen)
{
MessageBox.Show("串口未连接!", "传输错误");
return;
}
BtnSendParams.Enabled = false;
BtnGetParams.Enabled = false;
Thread sendThread = new Thread(SendData);
try
{
sendThread.Start();
}
catch (Exception exception)
{
MessageBox.Show(exception.Message);
sendThread.Abort();
}
}
/// <summary>
/// 发送数据
/// </summary>
private void SendData()
{
// 清空缓冲区
serialPort.DiscardOutBuffer();
ushort length = 0;
// 计算参数个数
foreach (Control control in groupParams.Controls)
{
if (control is TextBox && control.Text.Length != 0)
{
length = (ushort)(length + 3);
}
}
// 获取数据字节数
byte[] len = new byte[2];
len[0] = (byte)(length << 8);
len[1] = (byte)(length << 0);
// 添加非空参数
byte[] data = new byte[length];
string name = "";
int i = 0;
byte[] value = new byte[2];
foreach (Control control in groupParams.Controls)
{
if (!(control is TextBox))
{
continue;
}
if (control.Text.Length != 0)
{
name = control.Name.Replace("param", "");
byte.TryParse(name, out byte type);
ushort.TryParse(control.Text, out ushort param);
data[i] = type;
value[0] = (byte)(param << 8);
value[1] = (byte)(param << 0);
value.CopyTo(data, i + 1);
i += 3;
}
}
//判断参数非空
if (length == 0)
{
MessageBox.Show("参数为空", "警告");
CourseRecord("参数为空,请重新发送!");
//CourseRecord("参数为空,请重新发送!");
TimeCount.Text = "";
BtnSendParams.Enabled = true;
BtnGetParams.Enabled = true;
return;
}
//发送数据
serialPort.Write(data, 0, data.Length);
CourseRecord("发送参数完成!");
BtnSendParams.Enabled = true;
BtnGetParams.Enabled = true;
}
/// <summary>
/// 记录进程
/// </summary>
public void CourseRecord(string record)
{
//输出当前时间
DateTime dateTimeNow = DateTime.Now;
CourseText.Text += string.Format("{0}\r\n", dateTimeNow);
CourseText.Text += string.Format("{0}\r\n\r\n", record);
}
/// <summary>
/// 扫描端口
/// </summary>
private void BtnGetPort_Click(object sender, EventArgs e)
{
cbSerials.Text = "";
cbSerials.Items.Clear();
string[] str = SerialPort.GetPortNames();
if (str == null)
{
MessageBox.Show("本机没有串口!", "Error");
return;
}
//添加串口
foreach (string s in str)
{
cbSerials.Items.Add(s);
}
//设置默认串口
cbSerials.SelectedIndex = 0;
}
/// <summary>
/// 退出
/// </summary>
private void FormClosing_Click(object sender, FormClosingEventArgs e)
{
if (serialPort.IsOpen)
{
serialPort.Close();//关闭串口
CourseRecord("串口已关闭!");
}
}
}
}