在单片机项目开发中,上位机也是一个很重要的部分,主要用于数据显示(波形、温度等)、用户控制(LED,继电器等),下位机(单片机)与 上位机之间要进行数据通讯的两种方式都是基于串口的:前端
- USB转串口 —— 上位机和下位机经过USB转串口链接线直接相连进行数据交互;
- 串口转WIFI(ESP8266) —— 上位机和下位机基于TCP/IP协议经过WIFI传输数据;
- 串口转蓝牙(HC-06)—— 很少用,暂不介绍;
上位机软软件开发主要包括如下两种:git
一、Windows上位机(EXE可执行程序)正则表达式
在Windows上,最先用VB语言开发,后来因为C++的发展,采用MFC开发,近几年,微软发布了基于.NET框架的面向对象语言C#,更加稳定安全,再配合微软强大的VS进行开发,效率奇高;编程
另外,若是想要在Linux上跨平台运行,能够选用Qt;若是想要更加丰富好看的数据显示界面,能够选用Labview开发;数组
二、Android上位机(APP)缓存
在Android操做系统上,主要采用Java语言,使用WIFI或者蓝牙基于TCP/IP协议传输数据,利用Android Studio开发;安全
在此,咱们主要介绍如何经过VS + C#开发电脑上位机,其它上位机的开发暂且不论。框架
注:VS下载与安装参考这篇较详细的博客编程语言
https://blog.csdn.net/qq_36556893/article/details/79430133编辑器
上一篇大体了解了一下单片机实际项目开发中上位机开发部分的内容以及VS下载与安装,按照编程惯例,接下来就是“Hello,World!”
一、新建C#项目工程
首先选择新建Windows窗体应用(.NET Framework),而后选择项目保存位置,填写项目名称,这里由于咱们不须要用git进行版本管理,因此不用新建GIT存储库;
框架是指.net框架,4以及4如下的.NET框架能够在xp上运行,4以上能够在win7/8/10上运行,鉴于当前大多数操做系统都是win7或win10,选择4.5版本。
二、窗体介绍及代码分析
这里咱们双击窗体界面,这也是VS的特性,双击一个控件,就会进入对应代码文件部分,这些代码全由VS在生成项目时自动生成,下面进行详细的解释:
1 /*filename:Form1.cs*/ 2 //使用命名空间 3 using System; 4 using System.Collections.Generic; 5 using System.ComponentModel; 6 using System.Data; 7 using System.Drawing; 8 using System.Linq; 9 using System.Text; 10 using System.Windows.Forms; 11 12 //用户项目工程自定义命名空间HelloWorld 13 namespace HelloWorld 14 { 15 //定义了一个名称为Form1的公共类,而且在定义类的同时建立了一个这个类的对象,名为Form 16 //partial关键字 17 public partial class Form1 : Form 18 { 19 //与类同名的构造方法 20 public Form1() 21 { 22 InitializeComponent(); 23 } 24 //用户自定义方法,窗体加载时由Form对象调用 25 private void Form1_Load(object sender, EventArgs e) 26 { 27 } 28 } 29 }
命名空间(namespace):在C#中用命名空间将不少类的属性及其方法进行封装供调用,相似C语言中将变量和函数封装成一个个.h文件,调用的时候只须要#include "filepath + filename"就可使用,好比刚开始时用关键字using声明了一些所须要的系统命名空间(line1-10);而后采用关键字namespace来自定义一个用户工程所需的命名空间HelloWorld,在咱们定义的这个命名空间里就能够定义一些类和方法来进行下一步的实现;
类(class):C#是一门面向对象的编程语言,因此最基本的就是类和对象,对象的特征是具备属性(C语言中称为变量)和方法(C语言中称为函数),而后咱们定义一个类来描述这个对象的特征,注意:这个时候定义的类不是真实存在的,因此不会分配内存空间,当咱们用所定义的这个类去建立一个类的对象,这个对象是真实存在的,它会占用内存空间,好比在这个工程中定义了一个名称为Form1的公共类,而且在定义类的同时建立了一个这个类的对象,名为Form;
方法:前面已经说过,在面向对象编程中是没有变量和函数的,全部的函数都被封装在类中,属于对象的方法,最基本的是类的构造方法,该方法与类名同名,在用类建立一个具体对象时自动调用,不可缺乏,好比Form1( );另一种是本身定义的用户方法,好比该类中的Form1_Load()方法,就是在初始化窗口时,经过具体对象Form调用:Form.Form1_Load( );
访问修饰符:用来控制类、属性、方法的访问权限,经常使用有5个,默认私有,不能被外部访问;
私有的private,公共的public,受保护的protected,内部的internal,受保护内部的protect internal;
这里有一个重点,在定义Form1类的时候含有一个关键字partial,这里就不得不说C#语言设计一个重要的特性了,能做为大多数人开发上位机的首选,C#有一个特性就是设计的时候界面与后台分离,可是类名相同,首先看一下工程文件结构:
能够看到,Form1.cs文件下面包含了另外一个Form1.Designer.cs文件,再打开Form1.Designer.cs这个文件,是否是很惊奇,和前面如出一辙,再次定义了一个命名空间HelloWorld和Form1类,这个部分类中定义了咱们使用的控件、事件委托以及如Dispose方法等。由于这里面的代码都是自动生成的,所以设计成了一个部分类。最关键的一点,这里类也是用partial关键字修饰的,能够看到,Partial是局部类型的意思,容许咱们将一个类、结构或接口分红几个部分,分别实如今几个不一样的.cs文件中,用partial定义的类能够在多个地方被定义,最后C#编译器编译时会将这些类看成一个类来处理;
1 /*@filename:Form1.Designer.cs */ 2 3 namespace HelloWorld 4 { 5 partial class Form1 6 { 7 /// <summary> 8 /// 必需的设计器变量。 9 /// </summary> 10 private System.ComponentModel.IContainer components = null; 11 12 /// <summary> 13 /// 清理全部正在使用的资源。 14 /// </summary> 15 /// <param name="disposing">若是应释放托管资源,为 true;不然为 false。</param> 16 protected override void Dispose(bool disposing) 17 { 18 if (disposing && (components != null)) 19 { 20 components.Dispose(); 21 } 22 base.Dispose(disposing); 23 } 24 25 #region Windows 窗体设计器生成的代码 26 27 /// <summary> 28 /// 设计器支持所需的方法 - 不要修改 29 /// 使用代码编辑器修改此方法的内容。 30 /// </summary> 31 private void InitializeComponent() 32 { 33 this.SuspendLayout(); 34 // 35 // Form1 36 // 37 this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F); 38 this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 39 this.ClientSize = new System.Drawing.Size(418, 331); 40 this.Name = "Form1"; 41 this.Text = "Form1"; 42 this.Load += new System.EventHandler(this.Form1_Load); 43 this.ResumeLayout(false); 44 45 } 46 #endregion 47 } 48 }
Main: 一切程序都有入口主函数main,C#也是如此,在Program.cs文件中定义了Program类,该类中拥有主函数main( ), 在main函数中,第三行代码是一切的开始,调用Form1类的构造函数,建立一个Form对象,一切由此开始,代码以下:
1 /* @filename: Program.cs */
2 using System;
3 using System.Collections.Generic;
4 using System.Linq;
5 using System.Windows.Forms;
6
7 namespace HelloWorld
8 {
9 static class Program
10 {
11 /// <summary>
12 /// 应用程序的主入口点。
13 /// </summary>
14 [STAThread]
15 static void Main()
16 {
17 Application.EnableVisualStyles();
18 Application.SetCompatibleTextRenderingDefault(false);
19 Application.Run(new Form1()); //调用Form1类的构造函数,建立一个Form对象,一切由此开始 20 } 21 } 22 }
再来解释一下最后三个文件:第一个文件主要是应用程序发布时的一些属性设置,版本号,属性,版权之类的,其他两个文件是工具自动生成的一些设置文件,再也不过多赘述;
/* @filename:Assemblylnfo.cs*/ using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; // 有关程序集的通常信息由如下 // 控制。更改这些特性值可修改 // 与程序集关联的信息。 [assembly: AssemblyTitle("HelloWorld")] [assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("HelloWorld")] [assembly: AssemblyCopyright("Copyright © 2018")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] // 将 ComVisible 设置为 false 会使此程序集中的类型 //对 COM 组件不可见。若是须要从 COM 访问此程序集中的类型 //请将此类型的 ComVisible 特性设置为 true。 [assembly: ComVisible(false)] // 若是此项目向 COM 公开,则下列 GUID 用于类型库的 ID [assembly: Guid("094ac56a-7a59-4f32-a2eb-857135be4d2c")] // 程序集的版本信息由下列四个值组成: // // 主版本 // 次版本 // 生成号 // 修订号 // // 能够指定全部值,也可使用如下所示的 "*" 预置版本号和修订号 // 方法是按以下所示使用“*”: : // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.0.0")] [assembly: AssemblyFileVersion("1.0.0.0")]
三、Hello,World
下面就正式开始C#程序的设计,首先是界面的实现,能够随意从控件工具箱中拖放控件到窗体中,这里我拖动两个Button和一个TextBox,并在右边设置框中修改每一个控价的属性,界面如图:
这个时候若是查看Form1.cs文件,会发现和以前同样,这里就须要介绍另外几个开发GUI界面的知识点了,首先,咱们想要实现的功能是:当按下Send按钮时,文本框显示^_^Hello,World^_^字样,当按下Clear按钮时,文本框清空;这属于人机交互,通常人机交互的处理方式有两种,第一种是查询处理方式,好比在DOS系统下、Linux系统等命令行下的程序设计,第二种是事件处理机制,有了不少的优越性,由传统的查询法耗费CPU一直在检测,变成了事件处理机制下的主动提醒告知,大幅度减轻CPU资源浪费,在事件处理机制中有如下几个概念:
事件源(EventSource):描述人机交互中事件的来源,一般是一些控件;
事件(ActionEvent):事件源产生的交互内容,好比按下按钮;
事件处理:这部分也在C++中被叫作回调函数,当事件发生时用来处理事件;
注:这部分在单片机中也是如此,中断源产生中断,而后进入中断服务函数进行响应;
清楚了这几个概念后,就来实现咱们想要的功能,按下按钮是一个事件,那么,如何编写或者在哪编写这个事件的事件处理函数呢?在VS中很方便,只须要双击这个控件,VS就会自动将该控件的事件处理函数添加进Form1.cs文件,此处我先双击“Send”按钮,能够看到VS自动添加进了 private void button1_Click(object sender, EventArgs e) 这个方法,而后在里面编写代码,让文本框显示:这里全部的控件都是一个具体的对象,咱们要经过这些对象设置其属性或者调用其方法;一样的道理,双击Clear按钮,添加文本框清空代码,完整代码以下:
//用户项目工程自定义命名空间HelloWorld namespace HelloWorld { //定义了一个名称为Form1的公共类,而且在定义类的同时建立了一个这个类的对象,名为Form //partial关键字 public partial class Form1 : Form { //与类同名的构造方法 public Form1() { InitializeComponent(); } private void Form1_Load(object sender, EventArgs e) { } private void button1_Click(object sender, EventArgs e) { //按下Send按钮 textBox1.Text = "^_^Hello,World^_^"; //文本框显示 } private void button2_Click(object sender, EventArgs e) { //按下Clear按钮 textBox1.Text = ""; //文本框清空 } } }
至此,大功告成,第一个应用程序建立成功,点击启动按钮看下效果:
上一篇简单介绍了C#的一些基本知识,并成功的Hello,World,那么从这篇开始,咱们来本身动手写一个串口助手:
一、构思功能
串口助手在单片机开发中常常被用来调试,最基本的功能就是接收功能和发送功能,其次,串口在打开前须要进行一些设置:串口列表选择、波特率、数据位、校验位、中止位,这样就有了一个基本的雏形;而后咱们在下一篇中在此功能上添加:ASCII/HEX显示,发送,发送新行功能,重复自动发送功能,显示接收数据时间这几项扩展功能;
二、设计布局
根据以上功能,将整个界面分为两块:设置界面(不可缩放)+ 接收区和发送区(可缩放),下面就来依次拖放控件实现:
1)容器控件(Panel)
Panel是容器控件,是一些小控件的容器池,用来给控件进行大体分组,要注意容器是一个虚拟的,只会在设计的时候出现,不会显示在设计完成的界面上,这里咱们将整个界面分为6个容器池,如图:
2)文本标签控件(Lable)
用于显示一些文本,可是不可被编辑;改变其显示内容有两种方法:一是直接在属性面板修改“Text”的值,二是经过代码修改其属性,见以下代码;另外,能够修改Font属性修改其显示字体及大小,这里咱们选择微软雅黑,12号字体;
label1.Text = "串口"; //设置label的Text属性值
3)下拉组合框控件(ComboBox)
用来显示下拉列表;一般有两种模式,一种是DropDown模式,既能够选择下拉项,也能够选择直接编辑;另外一种是DropDownList模式,只能从下拉列表中选择,两种模式经过设置DropDownStyle属性选择,这里咱们选择第二种模式;
那么,如何加入下拉选项呢?对于比较少的下拉项,能够经过在属性面板中Items属性中加入,好比中止位设置,如图,若是想要出现默认值,改变Text属性就能够,但要注意必须和下拉项一致:
另一种是直接在页面加载函数代码中加入,好比波特率的选择,代码以下:
private void Form1_Load(object sender, EventArgs e) { int i; //单个添加for (i = 300; i <= 38400; i = i*2) { comboBox2.Items.Add(i.ToString()); //添加波特率列表 } //批量添加波特率列表 string[] baud = { "43000","56000","57600","115200","128000","230400","256000","460800" }; comboBox2.Items.AddRange(baud); //设置默认值 comboBox1.Text = "COM1"; comboBox2.Text = "115200"; comboBox3.Text = "8"; comboBox4.Text = "None"; comboBox5.Text = "1"; }
4)按钮控件(Button)
5)文本框控件(TextBox)
TextBox控件与label控件不一样的是,文本框控件的内容能够由用户修改,这也知足咱们的发送文本框需求;在默认状况下,TextBox控价是单行显示的,若是想要多行显示,须要设置其Multiline属性为true;
TextBox的方法中最多的是APPendText方法,它的做用是将新的文本数据从末尾处追加至TextBox中,那么当TextBox一直追加文本后就会带来自己长度不够而没法显示所有文本的问题,此时咱们须要使能TextBox的纵向滚动条来跟踪显示最新文本,因此咱们将TextBox的属性ScrollBars的值设置为Vertical便可;
至此,咱们的显示控件就所有添加完毕,可是还有一个最重要的空间没有添加,这种控件叫作隐式控件,它是运行于后台的,用户看不见,更不能直接控制,因此也成为组件,接下来咱们添加最主要的串口组件;
6)串口组件(SerialPort)
这种隐式控件添加后位于设计器下面 ,串口经常使用的属性有两个,一个是端口号(PortName),一个是波特率(BaudRate),固然还有数据位,中止位,奇偶校验位等;串口打开与关闭都有接口能够直接调用,串口同时还有一个IsOpen属性,IsOpen为true表示串口已经打开,IsOpen为flase则表示串口已经关闭。
添加了串口组件后,咱们就能够经过它来获取电脑当前端口,并添加到可选列表中,代码以下:
//获取电脑当前可用串口并添加到选项列表中 comboBox1.Items.AddRange(System.IO.Ports.SerialPort.GetPortNames());
启动后能够看到界面布局效果图以下(确保USB转串口CH340已链接):
三、搭建后台
界面布局完成后,咱们就要用代码来搭建整个软件的后台,这部分才是重中之重。
首先,咱们先来控制打开/关闭串口,大体思路是:当按下打开串口按钮后,将设置值传送到串口控件的属性中,而后打开串口,按钮显示关闭串口,再次按下时,串口关闭,显示打开按钮;
在这个过程当中,要注意一点,当咱们点击打开按钮时,会发生一些咱们编程时没法处理的事件,好比硬件串口没有链接,串口打开的过程当中硬件忽然断开,这些被称之为异常,针对这些异常,C#也有try..catch处理机制,在try中放置可能产生异常的代码,好比打开串口,在catch中捕捉异常进行处理,详细代码以下:
private void button1_Click(object sender, EventArgs e) { try { //将可能产生异常的代码放置在try块中 //根据当前串口属性来判断是否打开 if (serialPort1.IsOpen) { //串口已经处于打开状态 serialPort1.Close(); //关闭串口 button1.Text = "打开串口"; button1.BackColor = Color.ForestGreen; comboBox1.Enabled = true; comboBox2.Enabled = true; comboBox3.Enabled = true; comboBox4.Enabled = true; comboBox5.Enabled = true; textBox_receive.Text = ""; //清空接收区 textBox_send.Text = ""; //清空发送区 } else { //串口已经处于关闭状态,则设置好串口属性后打开 comboBox1.Enabled = false; comboBox2.Enabled = false; comboBox3.Enabled = false; comboBox4.Enabled = false; comboBox5.Enabled = false; serialPort1.PortName = comboBox1.Text; serialPort1.BaudRate = Convert.ToInt32(comboBox2.Text); serialPort1.DataBits = Convert.ToInt16(comboBox3.Text); if (comboBox4.Text.Equals("None")) serialPort1.Parity = System.IO.Ports.Parity.None; else if(comboBox4.Text.Equals("Odd")) serialPort1.Parity = System.IO.Ports.Parity.Odd; else if (comboBox4.Text.Equals("Even")) serialPort1.Parity = System.IO.Ports.Parity.Even; else if (comboBox4.Text.Equals("Mark")) serialPort1.Parity = System.IO.Ports.Parity.Mark; else if (comboBox4.Text.Equals("Space")) serialPort1.Parity = System.IO.Ports.Parity.Space; if (comboBox5.Text.Equals("1")) serialPort1.StopBits = System.IO.Ports.StopBits.One; else if (comboBox5.Text.Equals("1.5")) serialPort1.StopBits = System.IO.Ports.StopBits.OnePointFive; else if (comboBox5.Text.Equals("2")) serialPort1.StopBits = System.IO.Ports.StopBits.Two; serialPort1.Open(); //打开串口 button1.Text = "关闭串口"; button1.BackColor = Color.Firebrick; } } catch (Exception ex) { //捕获可能发生的异常并进行处理 //捕获到异常,建立一个新的对象,以前的不能够再用 serialPort1 = new System.IO.Ports.SerialPort(); //刷新COM口选项 comboBox1.Items.Clear(); comboBox1.Items.AddRange(System.IO.Ports.SerialPort.GetPortNames()); //响铃并显示异常给用户 System.Media.SystemSounds.Beep.Play(); button1.Text = "打开串口"; button1.BackColor = Color.ForestGreen; MessageBox.Show(ex.Message); comboBox1.Enabled = true; comboBox2.Enabled = true; comboBox3.Enabled = true; comboBox4.Enabled = true; comboBox5.Enabled = true; } }
接下来咱们构建发送和接收的后台代码,串口发送和接收都是在串口成功打开的状况下进行的,因此首先要判断串口属性IsOpen是否为1;
串口发送有两种方法,一种是字符串发送WriteLine,一种是Write(),能够发送一个字符串或者16进制发送(见下篇),其中字符串发送WriteLine默认已经在末尾添加换行符;
private void button2_Click(object sender, EventArgs e) { try { //首先判断串口是否开启 if (serialPort1.IsOpen) { //串口处于开启状态,将发送区文本发送 serialPort1.Write(textBox_send.Text); } } catch (Exception ex) { //捕获到异常,建立一个新的对象,以前的不能够再用 serialPort1 = new System.IO.Ports.SerialPort(); //刷新COM口选项 comboBox1.Items.Clear(); comboBox1.Items.AddRange(System.IO.Ports.SerialPort.GetPortNames()); //响铃并显示异常给用户 System.Media.SystemSounds.Beep.Play(); button1.Text = "打开串口"; button1.BackColor = Color.ForestGreen; MessageBox.Show(ex.Message); comboBox1.Enabled = true; comboBox2.Enabled = true; comboBox3.Enabled = true; comboBox4.Enabled = true; comboBox5.Enabled = true; } }
接下来开始最后一个任务 —— 串口接收,在使用串口接收以前要先为串口注册一个Receive事件,至关于单片机中的串口接收中断,而后在中断内部对缓冲区的数据进行读取,如图,输入完成后回车,就会跳转到响应代码部分:
//串口接收事件处理 private void SerialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e) { }
一样的,串口接收也有两种方法,一种是16进制方式读(下篇介绍),一种是字符串方式读,在刚刚生成的代码中编写,以下:
//串口接收事件处理 private void SerialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e) { try { //由于要访问UI资源,因此须要使用invoke方式同步ui this.Invoke((EventHandler)(delegate { textBox_receive.AppendText(serialPort1.ReadExisting()); } ) ); } catch (Exception ex) { //响铃并显示异常给用户 System.Media.SystemSounds.Beep.Play(); MessageBox.Show(ex.Message); } }
这里又有了一个新的知识点,这个串口接收处理函数属于一个单独的线程,不属于main的主线程,而接收区的TextBox是在主线程中建立的,因此当咱们直接用serialPort1.ReadExisting()读取回来字符串,而后用追加到textBox_receive.AppendText()追加到接收显示文本框中的时候,串口助手在运行时没有反应,甚至报异常,如图:
因此,这个时候咱们就须要用到invoke方式,这种方式专门被用于解决从不是建立控件的线程访问它,加入了invoke方式后,串口助手就能够正常接收到数据了,如图:
上一篇中咱们完成了一个串口助手的雏形,实现了基本发送和接收字符串功能,并将打开/关闭串口进行了异常处理,这篇就来按照流程,逐步将功能完善:
一、构思功能
首先是接收部分,要添加一个“清空接收”的按钮来清空接收区;由于串口通讯协议经常使用都是8bit数据(低7bit表示ASCII码,高1bit表示奇偶校验),做为一个开发调试工具,它还须要将这个8bit码用十六进制方式显示出来,方便调试,因此还须要添加两个单选框来选择ASCII码显示仍是HEX显示;
而后是发送部分,与以前对应,调试过程当中还须要直接发送十六进制数据,因此也须要添加两个单选框来选择发送ASCII码仍是HEX码;除了这个功能,还须要添加自动发送的功能,自动发送新行功能方便调试;
二、设计布局
1)单选按钮控件(RadioButton)
接收数据显示只能同时选中ASCII显示或者HEX显示,因此要用单选按钮控件,在同一组中(好比以前所讲述的容器)的单选按钮控件只能同时选中一个,恰好符合咱们的要求;
2)复选框控件(CheckBox)
这个一般被用于选择一些可选功能,好比是否显示数据接收时间,是否在发送时自送发送新行,是否开启自动发送功能等,它与以前的RadioButton都有一个很重要的属性 —— CHecked,若为false,则表示未被选中,若为true,则表示被选中;
3)数值增减控件(NumericUpDown)
显示用户经过单击控件上的上/下按钮能够增长和减小的单个数值,这里咱们用来设置自动发送的间隔时长;
4)定时器组件(Timer)
这里之因此称为组件是由于它和以前的串口同样,都不能被用户直接操做;它是按用户定义的间隔引起事件的组件;
Timer主要是Interval属性,用来设置定时值,默认单位ms;在设置定时器以后,能够调用Timer对象的start()方法和stop()方法来启动或者关闭定时器;在启动以后,Timer就会每隔Interval毫秒触发一次Tick事件,若是设置初始值为100ms,咱们只须要设置一个全局变量i,每次时间到后i++,当i==10的时候,就表示计数值为1s(这里Timer的使用方法是否是和单片机相同^_^);
总体设计出来的效果图以下:
三、搭建后台
按照以前的思路,界面布局完成后,就要开始一个软件最重要的部分 —— 搭建后台:
一、状态栏串口状态显示
这里直接添加代码便可,无需多言;
label6.Text = "串口已打开"; label6.ForeColor = Color.Green;
label6.Text = "串口已关闭"; label6.ForeColor = Color.Red;
二、接收部分
以前咱们直接在串口接收事件中调用serialPort1.ReadExisting()方法读取整个接收缓存区,而后追加到接收显示文本框中,但在这里咱们须要在底部状态栏显示接收字节数和发送字节数,因此就不能这样总体读取,要逐字节读取/发送而且计数;
1)类的属性
首先定义一个用于计数接收字节的变量,这个变量的做用至关于C语言中的全局变量,在C#中称之为类的属性,这个属性能够被这个类中的方法所访问,或者经过这个对象来访问,代码以下:
public partial class Form1 : Form { private long receive_count = 0; //接收字节计数, 做用至关于全局变量 ....... }
2)按字节读取缓冲区
首先经过访问串口的BytesToRead属性获取到接收缓冲区中数据的字节数,而后调用串口的Read(byte[ ] buffer, int offset, int count)方法从输入缓冲区读取一些字节并将那些字节写入字节数组中指定的偏移量处:
//串口接收事件处理 private void SerialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e) { int num = serialPort1.BytesToRead; //获取接收缓冲区中的字节数 byte[] received_buf = new byte[num]; //声明一个大小为num的字节数据用于存放读出的byte型数据 receive_count += num; //接收字节计数变量增长nun serialPort1.Read(received_buf,0,num); //读取接收缓冲区中num个字节到byte数组中
//未完,见下 }
上一步咱们将串口接收缓冲区中的数据按字节读取到了byte型数组received_buf中,可是要注意,这里的数据所有是byte型数据,如何显示到接收文本框中呢?要知道接收文本框显示的内容都是以字符串形式呈现的,也就是说咱们追加到文本框中的内容必须是字符串类型,即便是16进制显示,也是将数据转化为16进制字符串类型显示的,接下来说述如何将字节型数据转化为字符串类型数据;
3)字符串构造类型(StringBuilder)
咱们须要将整个received_buf数组进行遍历,将每个byte型数据转化为字符型,而后将其追加到咱们总的字符串(要发送到接收文本框去显示的那个完整字符串)后面,可是String类型不容许对内容进行任何改动,更况且咱们须要遍历追加字符,因此这个时候就须要用到字符串构造类型(StringBuilder),它不只容许任意改动内容,还提供了Append,Remove,Replace,Length,ToString等等有用的方法,这个时候再来构造字符串就显得很简单了,代码以下:
public partial class Form1 : Form { private StringBuilder sb = new StringBuilder(); //为了不在接收处理函数中反复调用,依然声明为一个全局变量 //其他代码省略 }
//串口接收事件处理 private void SerialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e) { //接第二步中的代码 sb.Clear(); //防止出错,首先清空字符串构造器 //遍历数组进行字符串转化及拼接 foreach (byte b in received_buf) { sb.Append(b.ToString()); } try { //由于要访问UI资源,因此须要使用invoke方式同步ui Invoke((EventHandler)(delegate { textBox_receive.AppendText(sb.ToString()); label7.Text = "Rx:" + receive_count.ToString() + "Bytes"; } ) ); } //代码省略
}
接下来咱们运行看一下效果:
能够看到,当咱们发送字符“1”的时候,状态栏显示接收到1byte数据,代表计数正常,可是接收到的倒是字符形式的“49”,这是由于接收到的byte类型的数据存放的就是ASCII码值,而调用byte对象的ToString()方法,由下图可看到,这个方法恰好又将这个ASCII值49转化成为了字符串“49”,而不是对应的ASCII字符'1';
4)C#类库——编码类(Encoding Class)
接着上一个问题,咱们须要将byte转化为对应的ASCII码,这就属于解码(将一系列编码字节转换为一组字符的过程),一样将一组字符转换为一系列字节的过程称为编码;
这里由于转换的是ASCII码,有两种方法实现:第一种采用Encoding类的ASCII属性实现,第二种采用Encoding Class的派生类ASCIIEncoing Class实现,咱们采用第一种方法实现,而后调用GetString(Byte[ ])方法将整个数组解码为ASCII数组,代码以下:
sb.Append(Encoding.ASCII.GetString(received_buf)); //将整个数组解码为ASCII数组
再次运行一下,能够看到正常显示:
5)byte类型值转化为十六进制字符显示
在第3节中咱们分析了byte.ToString()方法,它能够将byte类型直接转化为字符显示,好比接收到的是字符1的ASCII码值是49,就将49直接转化为“49”显示出来,在这里,咱们须要将49用十六进制显示,也就是显示“31”(0x31),这种转化并无什么实质上的改变,只是进行了数制转化而已,因此采用格式控制的ToString(String)方法,具体使用方法见下图:
这里咱们须要将其转化为2位十六进制文本显示,另外,因为ASCII和HEX只能同时显示一种,因此咱们还要对单选按钮是否选中进行判断,代码以下:
if (radioButton2.Checked) { //选中HEX模式显示 foreach (byte b in received_buf) { sb.Append(b.ToString("X2") + ' '); //将byte型数据转化为2位16进制文本显示,用空格隔开 } } else { //选中ASCII模式显示 sb.Append(Encoding.ASCII.GetString(received_buf)); //将整个数组解码为ASCII数组 }
再来运行看一下最终效果(先发送“Mculover66”加回车,而后发送“1”加回车):
6)日期时间结构(DateTime Struct)
当咱们勾选上显示接收数据时间时,要在接收数据前加上时间,这个时间经过DateTime Struct来获取,首先仍是声明一个全局变量:
private DateTime current_time = new DateTime(); //为了不在接收处理函数中反复调用,依然声明为一个全局变量
这个时候current_time是一个DateTime类型,经过调用ToString(String)方法将其转化为文本显示,具体选用哪一种见下图:
在显示的时候,依然要对用户是否选中进行判断,代码以下:
//由于要访问UI资源,因此须要使用invoke方式同步ui Invoke((EventHandler)(delegate { if (checkBox1.Checked) { //显示时间 current_time = System.DateTime.Now; //获取当前时间 textBox_receive.AppendText(current_time.ToString("HH:mm:ss") + " " + sb.ToString()); } else { //不显示时间 textBox_receive.AppendText(sb.ToString()); } label7.Text = "Rx:" + receive_count.ToString() + "Bytes"; } ) );
再来运行看一下效果:
7)清空接收按钮
这里就不须要多说了,直接贴代码:
private void button3_Click(object sender, EventArgs e) { textBox_receive.Text = ""; //清空接收文本框 textBox_send.Text = ""; //清空发送文本框 receive_count = 0; //计数清零 label7.Text = "Rx:" + receive_count.ToString() + "Bytes"; //刷新界面 }
三、发送部分
首先为了不发送出错,启动时咱们将发送按钮失能,只有成功打开后才使能,关闭后失能,这部分代码简单,自行编写;
1)字节计数 + 发送新行
有了上面的基础,实现这两个功能就比较简单了,要注意Write和WriteLine的区别:
2)正则表达式的简单应用
这是一个很重要很重要很重要的知识 —— 正则表达式!咱们但愿发送的数据是0x31,因此功能应该被设计为在HEX发送模式下,用户输入“31”就应该发送0x31,这个不难,只须要将字符串每2个字符提取一下,而后按16进制转化为一个byte类型的值,最后调用write(byte[ ] buffer,int offset,int count)将这一个字节数据发送就能够,那么,当用户同时输入多个十六进制字符呢该符合发送呢?
这个时候就须要用到正则表达式了,用户能够将输入的十六进制数据用任意多个空格隔开,而后咱们利用正则表达式匹配空格,并替换为“”,至关于删除掉空格,这样对整个字符串进行遍历,用刚才的方法逐个发送便可!
完整的发送代码以下:
private void button2_Click(object sender, EventArgs e) { byte[] temp = new byte[1]; try { //首先判断串口是否开启 if (serialPort1.IsOpen) { int num = 0; //获取本次发送字节数 //串口处于开启状态,将发送区文本发送 //判断发送模式 if (radioButton4.Checked) { //以HEX模式发送 //首先须要用正则表达式将用户输入字符中的十六进制字符匹配出来 string buf = textBox_send.Text; string pattern = @"\s"; string replacement = ""; Regex rgx = new Regex(pattern); string send_data = rgx.Replace(buf, replacement); //不发送新行 num = (send_data.Length - send_data.Length % 2) / 2; for (int i = 0; i < num; i++) { temp[0] = Convert.ToByte(send_data.Substring(i * 2, 2), 16); serialPort1.Write(temp, 0, 1); //循环发送 } //若是用户输入的字符是奇数,则单独处理 if (send_data.Length % 2 != 0) { temp[0] = Convert.ToByte(send_data.Substring(textBox_send.Text.Length-1,1), 16); serialPort1.Write(temp, 0, 1); num++; } //判断是否须要发送新行 if (checkBox3.Checked) { //自动发送新行 serialPort1.WriteLine(""); } } else { //以ASCII模式发送 //判断是否须要发送新行 if (checkBox3.Checked) { //自动发送新行 serialPort1.WriteLine(textBox_send.Text); num = textBox_send.Text.Length + 2; //回车占两个字节 } else { //不发送新行 serialPort1.Write(textBox_send.Text); num = textBox_send.Text.Length; } } send_count += num; //计数变量累加 label8.Text = "Tx:" + send_count.ToString() + "Bytes"; //刷新界面 } } catch (Exception ex) { serialPort1.Close(); //捕获到异常,建立一个新的对象,以前的不能够再用 serialPort1 = new System.IO.Ports.SerialPort(); //刷新COM口选项 comboBox1.Items.Clear(); comboBox1.Items.AddRange(System.IO.Ports.SerialPort.GetPortNames()); //响铃并显示异常给用户 System.Media.SystemSounds.Beep.Play(); button1.Text = "打开串口"; button1.BackColor = Color.ForestGreen; MessageBox.Show(ex.Message); comboBox1.Enabled = true; comboBox2.Enabled = true; comboBox3.Enabled = true; comboBox4.Enabled = true; comboBox5.Enabled = true; } }
下面来看看运行效果:
3)定时器组件(Timer)
自动发送功能是咱们搭建的最后一个功能了,第2节介绍定时器组件的时候已经说过,这个定时器和单片机中的定时器用法基本同样,因此,大体思路以下:当勾选自动发送多选框的时候,将右边数值增减控件的值赋给定时器做为定时值,同时将右边数值选择控件失能,而后当定时器时间到后,从新定时器值并调用发送按钮的回调函数,当为勾选自动发送的时候,中止定时器,同时使能右边数值选择控件,代码以下:
private void checkBox2_CheckedChanged(object sender, EventArgs e) { if (checkBox2.Checked) { //自动发送功能选中,开始自动发送 numericUpDown1.Enabled = false; //失能时间选择 timer1.Interval = (int)numericUpDown1.Value; //定时器赋初值 timer1.Start(); //启动定时器 label6.Text = "串口已打开" + " 自动发送中..."; } else { //自动发送功能未选中,中止自动发送 numericUpDown1.Enabled = true; //使能时间选择 timer1.Stop(); //中止定时器 label6.Text = "串口已打开"; } } private void timer1_Tick(object sender, EventArgs e) { //定时时间到 button2_Click(button2, new EventArgs()); //调用发送按钮回调函数 }
运行一下看一下效果: