C# ISP 串口调试工具的开发(二)—— 多线程处理

C# ISP 串口调试工具的开发(二)—— 多线程处理

前言

上一篇文章《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("串口已关闭!");
            }
        }
    }
}
  • 4
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值