[排版、电脑浏览最佳]

在单片机项目开发中,上位机也是重要的部分,主要有数据显示(波形、温度等)、用户控制(LED、继电器等)、下位机(单片机)和上位机之间数据通信两种方法,都是基于串口的。

USB-串行——父计算机和子计算机通过USB-串行电缆直接进行数据交互。串行端口到WIFI(ESP8266) ——父计算机和子计算机根据TCP/IP协议通过WIFI传输数据。串行端口到蓝牙(HC-06)——使用不多,暂时不介绍。主机计算机软件开发主要包括以下两个方面:

1、Windows主机计算机(EXE可执行文件)

最初在Windows上用VB语言开发,然后随着C的发展采用了MFC开发,近年来,微软…发布了基于NET框架的面向对象语言C#。更可靠、更安全,与Microsoft强大的VS一起开发,效率非常高。

此外,如果想在Linux上跨平台运行,可以选择QT。如果您想要更丰富、更美观的数据显示界面,可以选择开发Labview。

2、Android主机计算机(APP)。

Android操作系统主要使用Java语言、使用WIFI或蓝牙根据TCP/IP协议传输数据,以及使用Android StudIO进行开发。

这里主要介绍如何通过VS C#开发PC,其他PC的开发暂不讨论。

注:VS下载与安装参考这篇较详细的博客

我们来自己动手写一个串口助手:

1、构思功能

串口助手在单片机开发中经常被用来调试,最基本的功能就是接收功能和发送功能,其次,串口在打开前需要进行一些设置:串口列表选择、波特率、数据位、校验位、停止位,这样就有了一个基本的雏形;然后我们在下一篇中在此功能上添加:ASCII/HEX显示,发送,发送新行功能,重复自动发送功能,显示接收数据时间这几项扩展功能;

2、设计布局

根据以上功能,将整个界面分为两块:设置界面(不可缩放)+ 接收区和发送区(可缩放),下面就来依次拖放控件实现:

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.I()); //添加波特率列表 } //批量添加波特率列表 string[] baud = { "43000","56000","57600","115200","128000","230400","256000","460800" }; comboBox2.IRange(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.IRange());

启动后可以看到界面布局效果图如下(确保USB转串口CH340已连接):

布局效果图

3、搭建后台

界面布局完成后,我们就要用代码来搭建整个软件的后台,这部分才是重中之重。

首先,我们先来控制打开/关闭串口,大致思路是:当按下打开串口按钮后,将设置值传送到串口控件的属性中,然后打开串口,按钮显示关闭串口,再次按下时,串口关闭,显示打开按钮;

在这个过程中,要注意一点,当我们点击打开按钮时,会发生一些我们编程时无法处理的事件,比如硬件串口没有连接,串口打开的过程中硬件突然断开,这些被称之为异常,针对这些异常,C#也有try..catch处理机制,在try中放置可能产生异常的代码,比如打开串口,在catch中捕捉异常进行处理,详细代码如下:

private void button1_Click(object sender, EventArgs e) {         try { //将可能产生异常的代码放置在try块中 //根据当前串口属性来判断是否打开 if ) { //串口已经处于打开状态 (); //关闭串口 bu = "打开串口"; bu = Color.ForestGreen; comboBox1.Enabled = true; comboBox2.Enabled = true; comboBox3.Enabled = true; comboBox4.Enabled = true; comboBox5.Enabled = true;              = ""; //清空接收区              = ""; //清空发送区 } else { //串口已经处于关闭状态,则设置好串口属性后打开 comboBox1.Enabled = false; comboBox2.Enabled = false; comboBox3.Enabled = false; comboBox4.Enabled = false; comboBox5.Enabled = false; = comboBox1.Text; = Convert.ToInt32); = Convert.ToInt16); if ("None")) = Sy; else if("Odd")) = Sy; else if ("Even")) = Sy; else if ("Mark")) = Sy; else if ("Space")) = Sy; if ("1")) = Sy; else if ("1.5")) = SyPointFive; else if ("2")) = Sy; (); //打开串口 bu = "关闭串口"; bu = Color.Firebrick; } } catch (Exception ex) { //捕获可能发生的异常并进行处理 //捕获到异常,创建一个新的对象,之前的不可以再用 serialPort1 = new Sy(); //刷新COM口选项 comboBox1.I(); comboBox1.IRange()); //响铃并显示异常给用户 Sy(); bu = "打开串口"; bu = Color.ForestGreen; Me); 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 ) { //串口处于开启状态,将发送区文本发送 (); } } catch (Exception ex) { //捕获到异常,创建一个新的对象,之前的不可以再用 serialPort1 = new Sy(); //刷新COM口选项 comboBox1.I(); comboBox1.IRange()); //响铃并显示异常给用户 Sy(); bu = "打开串口"; bu = Color.ForestGreen; Me); comboBox1.Enabled = true; comboBox2.Enabled = true; comboBox3.Enabled = true; comboBox4.Enabled = true; comboBox5.Enabled = true; } }

接下来开始最后一个任务 —— 串口接收,在使用串口接收之前要先为串口注册一个Receive事件,相当于单片机中的串口接收中断,然后在中断内部对缓冲区的数据进行读取,如图,输入完成后回车,就会跳转到响应代码部分:

//串口接收事件处理 private void SerialPort1_DataReceived(object sender, Sy e) { }

同样的,串口接收也有两种方法,一种是16进制方式读(下篇介绍),一种是字符串方式读,在刚刚生成的代码中编写,如下:

//串口接收事件处理 private void SerialPort1_DataReceived(object sender, Sy e) { try { //因为要访问UI资源,所以需要使用invoke方式同步ui ((EventHandler)(delegate { ()); } ) ); } catch (Exception ex) { //响铃并显示异常给用户 Sy(); Me); } }

这里又有了一个新的知识点,这个串口接收处理函数属于一个单独的线程,不属于main的主线程,而接收区的TextBox是在主线程中创建的,所以当我们直接用()读取回来字符串,然后用追加到()追加到接收显示文本框中的时候,串口助手在运行时没有反应,甚至报异常,如图:

所以,这个时候我们就需要用到invoke方式,这种方式专门被用于解决从不是创建控件的线程访问它,加入了invoke方式后,串口助手就可以正常接收到数据了,如图:

上一篇中我们完成了一个串口助手的雏形,实现了基本发送和接收字符串功能,并将打开/关闭串口进行了异常处理,这篇就来按照流程,逐步将功能完善:

1、构思功能

首先是接收部分,要添加一个“清空接收”的按钮来清空接收区;因为串口通信协议常用都是8bit数据(低7bit表示ASCII码,高1bit表示奇偶校验),作为一个开发调试工具,它还需要将这个8bit码用十六进制方式显示出来,方便调试,所以还需要添加两个单选框来选择ASCII码显示还是HEX显示;

然后是发送部分,与之前对应,调试过程中还需要直接发送十六进制数据,所以也需要添加两个单选框来选择发送ASCII码还是HEX码;除了这个功能,还需要添加自动发送的功能,自动发送新行功能方便调试;

2、设计布局

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的使用方法是不是和单片机相同^_^);

整体设计出来的效果图如下:

3、搭建后台

按照之前的思路,界面布局完成后,就要开始一个软件最重要的部分 —— 搭建后台:

1、状态栏串口状态显示

这里直接添加代码即可,无需多言;

label6.Text = "串口已打开"; label6.ForeColor = Color.Green; label6.Text = "串口已关闭"; label6.ForeColor = Color.Red;

2、接收部分

之前我们直接在串口接收事件中调用()方法读取整个接收缓存区,然后追加到接收显示文本框中,但在这里我们需要在底部状态栏显示接收字节数和发送字节数,所以就不能这样整体读取,要逐字节读取/发送并且计数;

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, Sy e) { int num = ; //获取接收缓冲区中的字节数 byte[] received_buf = new byte[num]; //声明一个大小为num的字节数据用于存放读出的byte型数据 receive_count += num; //接收字节计数变量增加nun (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, Sy e) { //接第二步中的代码 (); //防止出错,首先清空字符串构造器 //遍历数组进行字符串转化及拼接 foreach (byte b in received_buf) { ()); } try { //因为要访问UI资源,所以需要使用invoke方式同步ui Invoke((EventHandler)(delegate { ()); 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数组,代码如下:

再次运行一下,可以看到正常显示:

5)byte类型值转化为十六进制字符显示

在第3节中我们分析了by()方法,它可以将byte类型直接转化为字符显示,比如接收到的是字符1的ASCII码值是49,就将49直接转化为“49”显示出来,在这里,我们需要将49用十六进制显示,也就是显示“31”(0x31),这种转化并没有什么实质上的改变,只是进行了数制转化而已,所以采用格式控制的ToString(String)方法,具体使用方法见下图:

这里我们需要将其转化为2位十六进制文本显示,另外,由于ASCII和HEX只能同时显示一种,所以我们还要对单选按钮是否选中进行判断,代码如下:

if ) { //选中HEX模式显示 foreach (byte b in received_buf) { ("X2") + ' '); //将byte型数据转化为2位16进制文本显示,用空格隔开 } } else { //选中ASCII模式显示 (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 ) { //显示时间 current_time = Sy; //获取当前时间 ("HH:mm:ss") + " " + ()); } else { //不显示时间 ()); } label7.Text = "Rx:" + receive_count.ToString() + "Bytes"; } ) );

再来运行看一下效果:

7)清空接收按钮

这里就不需要多说了,直接贴代码:

private void button3_Click(object sender, EventArgs e) { = ""; //清空接收文本框 = ""; //清空发送文本框 receive_count = 0; //计数清零 label7.Text = "Rx:" + receive_count.ToString() + "Bytes"; //刷新界面 }

3、发送部分

首先为了避免发送出错,启动时我们将发送按钮失能,只有成功打开后才使能,关闭后失能,这部分代码简单,自行编写;

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 ) { int num = 0; //获取本次发送字节数 //串口处于开启状态,将发送区文本发送 //判断发送模式 if ) { //以HEX模式发送 //首先需要用正则表达式将用户输入字符中的十六进制字符匹配出来 string buf = ; string pattern = @"\s"; string replacement = ""; Regex rgx = new Regex(pattern); string send_data = rgx.Replace(buf, replacement); //不发送新行 num = - % 2) / 2; for (int i = 0; i < num; i++) { temp[0] = Convert.ToByte(i * 2, 2), 16); (temp, 0, 1); //循环发送 } //如果用户输入的字符是奇数,则单独处理 if % 2 != 0) { temp[0] = Convert.ToByte(.Length-1,1), 16); (temp, 0, 1); num++; } //判断是否需要发送新行 if ) { //自动发送新行 Line(""); } } else { //以ASCII模式发送 //判断是否需要发送新行 if ) { //自动发送新行 Line(); num = .Length + 2; //回车占两个字节 } else { //不发送新行 (); num = .Length; } } send_count += num; //计数变量累加 label8.Text = "Tx:" + () + "Bytes"; //刷新界面 } } catch (Exception ex) { (); //捕获到异常,创建一个新的对象,之前的不可以再用 serialPort1 = new Sy(); //刷新COM口选项 comboBox1.I(); comboBox1.IRange()); //响铃并显示异常给用户 Sy(); bu = "打开串口"; bu = Color.ForestGreen; Me); 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 ) { //自动发送功能选中,开始自动发送 numericU = false; //失能时间选择 = (in; //定时器赋初值 (); //启动定时器 label6.Text = "串口已打开" + " 自动发送中..."; } else { //自动发送功能未选中,停止自动发送 numericU = true; //使能时间选择 (); //停止定时器 label6.Text = "串口已打开"; } } private void timer1_Tick(object sender, EventArgs e) { //定时时间到 button2_Click(button2, new EventArgs()); //调用发送按钮回调函数 }

运行一下看一下效果:

[已排版,电脑浏览最佳]

1.《R语言数据串行如何处理》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。

2.《R语言数据串行如何处理》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。

3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/keji/3209776.html