【闲来无事玩C#】1、简单的串口上位机程序

编程入门 行业动态 更新时间:2024-10-26 04:25:50

【闲来<a href=https://www.elefans.com/category/jswz/34/1764822.html style=无事玩C#】1、简单的串口上位机程序"/>

【闲来无事玩C#】1、简单的串口上位机程序

这一篇来做一个简单的串口上位机程序,配合【STM32F103笔记】中的串口程序使用,后续还可以在这个串口小程序的基础上添加更多功能,可以根据预先设计的数据格式,将串口小程序接收到的数据进行不同的显示,并根据接收到的数据向STM32发送控制指令,比如上位机PID控制STM32电机调速或者转角控制等等,会很有意思。
笔者也是刚开始学C#,就当做和大家一起学习进步啦。

C#开发环境

Visual Studio下载

作为C#小程序开发的第一篇,首先介绍一下C#的开发环境——Visual StudioVS),是美国微软公司的开发工具包系列产品,这里给出官方下载地址(/):

可以看到VS共有三个发行版本:Community、Professional、Enterprise,作为个人开发一般选择Community就已经非常够用啦,如果你的网络不太好的话,可以点击下面的如何离线安装进行参考,具体安装就不再介绍了。

新建工程

打开VS,点击左上角文件->新建->项目,弹出新建项目对话框:

在左侧选择编程语言,这里笔者的默认编程语言设置的是C++,需要在其他语言里找到Visual C#,选择Window 桌面->Windows 窗体应用,然后在下方设置好项目名称位置等,点击确定就可以了:

左侧是项目的树形结构选项卡,右侧为工具箱属性设置等选项卡,这些选项卡都可以自己调整位置,这里按默认就很舒服,正中间就是我们的窗体了。

  • 工具箱:包含许多窗体控件,比如按钮、文本框等等;
  • 属性:用于设置控件属性和管理控件事件

拖动工具箱中的控件就可以在窗体中摆放,还请大家先了解一下C#的基本编程操作,这里就不展开说明了。(这个系列前几篇的程序都会详细给出并加上注释,可以就此了解C#程序设计的基本操作)

程序设计

界面设计

按照下面图中设置好窗体控件:

左侧主要为用户操作区,包括串口设置信息显示发送数据以及程序介绍,为了控件的分块整齐,这里使用了GroupBox控件放置同一功能区的控件。(这里所有的操作都放置在相近的地方(左侧),便于使用);右侧为显示区域,用于显示串口接收到的数据或者文本。

简单介绍一下:

  • 串口设置区:使用了两个ComboBox用于选择串口的端口号和波特率,其左边用两个Label控件进行信息提示,最下方是两个Button控件用于设置操作;
  • 信息显示区:使用一个CheckBox用于勾选是否将串口数据显示为十六进制,用两个Label控件对发送和接收的数据字节进行计数,右侧是一个Button控件用于清除计数和最右侧的数据显示区;
  • 发送数据区:使用一个TextBox用于输入发送的数据,两个Button用于进行发送和清除发送框;
  • 右侧显示区域:使用一个TextBox控件用于显示显示串口接收到的数据或者文本。

在设计程序之前,应该对窗体各个位置的分布进行设计,尽量操作方便且美观。

界面设计完成后,应该对各个控件进行命名,按照其控件类型与对应的功能,如上图中的“打开”按钮,将其命名为Btn_OpenPort,其中Btn为Button缩写,表示为按钮控件(控件的常用缩写可以在网上查到),OpenPort表示其功能为打开对应的串口端口号。

程序设计

各个控件设计运行的思路是:

  • 在程序开始运行时,检测电脑上已经存在的串口端口,将其在下拉选框控件进行显示,并配置默认的端口号和波特率;
  • 点击检测按钮,重新检测电脑上已经存在的串口端口,并配置默认的端口号和波特率;
  • 点击打开按钮,打开对应的串口,开始接收数据,并更新接收、发送数据字节数;
  • 点击清除按钮,清除计数及右侧显示;
  • 点击发送按钮,检测发送框中的数据并发送;
  • 点击发送数据区中的清除按钮,清除发送框中的数据。

添加控件的事件处理函数
添加控件的事件处理函数,一般双击控件可以添加默认的事件处理函数,对于其它事件处理函数需要在属性选项卡中的事件列表下双击添加:

MainForm_Load函数

窗体上双击或者在属性选项卡的事件下的Load事件右侧双击,如上图,自动生成窗体加载程序,即窗体加载时运行的程序:

		// 串口变量,这里需要添加命名空间using System.IO.Portsprivate SerialPort _serial;// 数据接收、端口关闭标志private bool _receiving, _closing;// 接收、发送计数private int _receiveCount, _sendCount;// 窗体加载程序private void MainForm_Load(object sender, EventArgs e){// 调用InitializeSettings函数对控件进行初始化InitializeSettings();// 变量赋初值_receiving = false; _closing = false;_receiveCount = 0; _sendCount = 0;// 在串口没有打开之前不允许发送Btn_Send.Enabled = false;}/// <summary>/// 控件初始化函数,设置串口的端口号及波特率/// </summary>private void InitializeSettings(){// 获取所有存在的端口号string[] ports = SerialPort.GetPortNames();// 排序一下,显得整齐一点Array.Sort(ports);// 重新添加端口号下拉选框控件的内容cb_PortNum.Items.Clear();cb_PortNum.Items.AddRange(ports);// 选择第一个端口为默认端口cb_PortNum.SelectedIndex = cb_PortNum.Items.Count > 0 ? 0 : -1;// 默认波特率string[] baudrate = { "2400", "4800", "9600", "19200", "38400", "57600", "115200" };// 设置波特率下拉选框的内容cb_Baudrate.Items.Clear();cb_Baudrate.Items.AddRange(baudrate);// 默认波特率选择115200cb_Baudrate.SelectedIndex = cb_Baudrate.Items.IndexOf("115200");// _serial赋初值_serial = new SerialPort{NewLine = "\n",//RtsEnable = false,//DtrEnable = false};// 添加串口数据接收事件处理函数_serial.DataReceived += _serial_DataReceived;}

MainForm_Load() 函数会在窗体加载(也就是显示窗口出来)的过程中运行,主要是检测系统中已经存在的串口的端口号,填入cb_PortNum(端口号下拉选框控件)中,同理初始化cb_Baudrate波特率下拉选框控件;
初始化串口变量_serial(这里初始化了换行控制符,以及添加数据接收处理函数),初始化其它后续需要使用的变量。

Btn_Detect_Click函数

双击检测按钮(Btn_Detect),或者在属性选项卡的事件下的Click事件右侧双击,添加按钮点击事件处理函数:

        private void Btn_Detect_Click(object sender, EventArgs e){// 调用InitializeSettings函数进行检测并设置InitializeSettings();}

这里检测按钮功能是重新检测系统中已经存在的串口的端口好,并初始化端口下拉选框和波特率下拉选框,因此直接调用InitializeSettings()函数就可以了。

Btn_OpenPort_Click函数

双击打开按钮(Btn_OpenPort),或者在属性选项卡的事件下的Click事件右侧双击,添加按钮点击事件处理函数;打开按钮用于打开或者关闭串口:

        private void Btn_OpenPort_Click(object sender, EventArgs e){// 判断串口端口是否已经打开// 如果已经打开了,那么应该将其关闭if (_serial.IsOpen){// 正在关闭串口的标志_closing = true;// 若此时正在接收数据,那么等待这次的接收完成再关闭while (_receiving)Application.DoEvents();try{// 关闭串口_serial.Close();}catch (Exception ex){// 如果出错则提示错误信息// 一般为串口端口号不存在(因为串口可能在别处已经被关闭或者占用)_serial = new SerialPort();MessageBox.Show(ex.Message);}// 关闭过程结束,关闭标志清除(置false)_closing = false;}else{ // 若串口没有打开则应该打开串口// 首先判断串口端口号是否设置好if (!string.IsNullOrEmpty(cb_PortNum.Text)){// 将_serial的端口号、波特率复制_serial.PortName = cb_PortNum.Text;_serial.BaudRate = int.Parse(cb_Baudrate.Text);try{// 尝试打开串口端口_serial.Open();}catch (Exception ex){// 打开失败则输出错误信息_serial = new SerialPort();MessageBox.Show(ex.Message);}}elseMessageBox.Show("请选择端口号!");}// 设置按钮显示字样,串口已经打开则显示“关闭”,否则显示“打开”Btn_OpenPort.Text = _serial.IsOpen ? "关闭" : "打开";// 当串口打开才能允许发送Btn_Send.Enabled = _serial.IsOpen;}

这里在关闭串口的过程中,首先将标志_closing置true,保证不再进行数据接收(后续函数中可以看到),同时判断_receiving标志,若为true则说明已经进入数据接收程序,那么应该等待此次接收完成再关闭串口;这两个标志位的用处是,在关闭串口和接收串口数据的过程中,可以避免两者相互冲突,造成程序出错(即卡死)。

Btn_ClearAll_Click

双击信息显示区的清除按钮(Btn_ClearAll),或者在属性选项卡的事件下的Click事件右侧双击,添加按钮点击事件处理函数:

        private void Btn_ClearAll_Click(object sender, EventArgs e){txt_Display.Text = "";_receiveCount = _sendCount = 0;lbl_Received.Text = "接收:0";lbl_Send.Text = "发送:0";}

清除按钮用于清除数据显示区以及接收发送计数。

Btn_Send_Click

双击的发送按钮(Btn_Send),或者在属性选项卡的事件下的Click事件右侧双击,添加按钮点击事件处理函数:

        private void Btn_Send_Click(object sender, EventArgs e){// 调用_serial.Write函数将发送数据框里的字符串发送出去_serial.Write(txt_Send.Text);// 更新发送计数_sendCount += txt_Send.Text.Length;lbl_Send.Text = "发送:" + _sendCount.ToString();}

这里直接调用了串口SerialPort的Write函数,直接向串口中写入字符串数据,即发送数据。

Btn_ClearSend_Click

双击数据发送区的清除按钮(Btn_ClearSend),或者在属性选项卡的事件下的Click事件右侧双击,添加按钮点击事件处理函数:

        private void Btn_ClearSend_Click(object sender, EventArgs e){txt_Send.Text = "";}

这个函数用于清除数据发送框。

_serial_DataReceived

_serial_DataReceived函数为本篇最重要的函数,作用是当串口接收到数据时,将其读取并展示在数据显示文本框内:

        private void _serial_DataReceived(object sender, SerialDataReceivedEventArgs e){// 如果正在关闭串口,则不再进行数据接收if (_closing)return;try{// 将_receiving标志置true,表示正在进行数据接收_receiving = true;// 接收缓冲区内的数据字节数int n = _serial.BytesToRead;// buf变量用于存储从接收缓冲区内读取的字节数据byte[] buf = new byte[n];// 更新接收数据计数_receiveCount += n;// 将数据读取到buf中_serial.Read(buf, 0, n);// 字符串构建StringBuilder sb = new StringBuilder();// invoke委托,进行数据处理及更新this.Invoke((EventHandler)(delegate{// 判断是否显示十六进制,若是则将buf中的每个字节数据转换成十六进制// 否则直接转换成ASCII编码的字符串if (ckb_DataHexView.Checked)foreach (byte b in buf)sb.Append(b.ToString("X2") + " ");elsesb.Append(Encoding.ASCII.GetString(buf));// 更新数据接收显示txt_Display.AppendText(sb.ToString());// 更新计数显示lbl_Received.Text = "接收:" + _receiveCount.ToString();}));}finally{// 数据接收结束后将_receiving标志置false_receiving = false;}}

从这里可以看出_closing和_receiving标志的相互配合使用,防止关闭串口的操作和数据读取操作相冲突。当然也可以在使用Invoke时改为使用BeginInvoke,这样就不需要使用_closing和_receiving这两个标志位了;
在更新数据显示时,使用了invoke委托。
笔者理解如下:

  • 窗体上的控件主线程的资源,在C#中不能跨线程访问资源,也就是说不能在其它线程中去改变另一个线程创建的控件的值;
  • 在打开串口时,会创建一个串口的监听线程对串口缓存区进行监听处理,当缓冲区有了数据之后,线程会调用接收数据处理函数(_serial_DataReceived);
  • 而在_serial_DataReceived函数中要对txt_Display等控件进行更新,即子线程访问主线程的资源,因此需要使用委托Invoke
  • this.Invoke((EventHandler)(delegate{ 委托的操作}));
    也就是说把操作委托给主线程,等待主线程执行操作,这样才能在数据接收到的同时更新txt_Display等控件;
  • 若不使用委托,直接操作txt_Display等控件,会发生错误:System.InvalidOperationException:“线程间操作无效: 从不是创建控件“txt_Display”的线程访问它。”
  • Invoke:在拥有此控件的基础窗口句柄的线程上执行指定的委托,这样会在子线程中等待主线程的操作执行完毕再向下执行,也就是在_serial_DataReceived函数中等待txt_Display等控件更新完毕;
  • 因此,若在接收大量数据后,主线程被委托更新控件,但由于数据量大,需要一定的时间,若正好在这时点击了关闭串口,而串口监听线程还在等待中,那么就会产生冲突(具体冲突原因还在查资料中),导致程序卡死;
  • 所以在使用Invoke委托时,由于其等待,需要对程序运行的状态进行判断(数据接受中_receiving,还是串口关闭中_closing);
  • BeginInvoke:在创建控件的基础句柄所在线程上异步执行指定委托,也就是说,BeginInvoke不会等待而继续执行,这时,串口监听线程调用的_serial_DataReceived函数将继续执行并结束,此时调用串口的Close函数就不会产生冲突;

至此,程序分析结束。

完整程序

using System;
using System.Text;
using System.Windows.Forms;using System.IO.Ports;namespace _1_SimpleSerialTool
{public partial class MainForm : Form{private SerialPort _serial;private bool _receiving, _closing;private int _receiveCount, _sendCount;public MainForm(){InitializeComponent();}private void MainForm_Load(object sender, EventArgs e){InitializeSettings();_receiving = false; _closing = false;_receiveCount = 0; _sendCount = 0;Btn_Send.Enabled = false;}private void Btn_Detect_Click(object sender, EventArgs e){InitializeSettings();}private void Btn_OpenPort_Click(object sender, EventArgs e){if (_serial.IsOpen){//_closing = true;//while (_receiving)//    Application.DoEvents();try{_serial.Close();}catch (Exception ex){_serial = new SerialPort();MessageBox.Show(ex.Message);}//_closing = false;}else{if (!string.IsNullOrEmpty(cb_PortNum.Text)){_serial.PortName = cb_PortNum.Text;_serial.BaudRate = int.Parse(cb_Baudrate.Text);try{_serial.Open();}catch (Exception ex){_serial = new SerialPort();MessageBox.Show(ex.Message);}}elseMessageBox.Show("请选择端口号!");}Btn_OpenPort.Text = _serial.IsOpen ? "关闭" : "打开";Btn_Send.Enabled = _serial.IsOpen;}private void Btn_ClearAll_Click(object sender, EventArgs e){txt_Display.Text = "";_receiveCount = _sendCount = 0;lbl_Received.Text = "接收:0";lbl_Send.Text = "发送:0";}private void Btn_Send_Click(object sender, EventArgs e){_serial.Write(txt_Send.Text);_sendCount += txt_Send.Text.Length;lbl_Send.Text = "发送:" + _sendCount.ToString();}private void Btn_ClearSend_Click(object sender, EventArgs e){txt_Send.Text = "";}private void _serial_DataReceived(object sender, SerialDataReceivedEventArgs e){//if (_closing)//    return;try{_receiving = true;int n = _serial.BytesToRead;byte[] buf = new byte[n];_receiveCount += n;_serial.Read(buf, 0, n);StringBuilder sb = new StringBuilder();// this.Invoke((EventHandler)(delegatethis.BeginInvoke((EventHandler)(delegate{if (ckb_DataHexView.Checked)foreach (byte b in buf)sb.Append(b.ToString("X2") + " ");elsesb.Append(Encoding.ASCII.GetString(buf));txt_Display.AppendText(sb.ToString());lbl_Received.Text = "接收:" + _receiveCount.ToString();}));}finally{_receiving = false;}}private void InitializeSettings(){string[] ports = SerialPort.GetPortNames();Array.Sort(ports);cb_PortNum.Items.Clear();cb_PortNum.Items.AddRange(ports);cb_PortNum.SelectedIndex = cb_PortNum.Items.Count > 0 ? 0 : -1;string[] baudrate = { "2400", "4800", "9600", "19200", "38400", "57600", "115200" };cb_Baudrate.Items.Clear();cb_Baudrate.Items.AddRange(baudrate);cb_Baudrate.SelectedIndex = cb_Baudrate.Items.IndexOf("115200");_serial = new SerialPort{NewLine = "\n",//RtsEnable = false,//DtrEnable = false};_serial.DataReceived += _serial_DataReceived;}}  // end of MainForm
}

运行结果

将USB转串口设备的TX和RX用杜邦线短接,这样相当于发送给自己接收,用于测试串口上位机程序:

调试运行程序,结果如下:

完结撒花✿✿ヽ(°▽°)ノ✿

更多推荐

【闲来无事玩C#】1、简单的串口上位机程序

本文发布于:2024-03-10 21:47:30,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1729115.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:无事   上位   串口   闲来   简单

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!