c#串口编程(傻瓜教程,手把手教你学会)

2023-05-16

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

  • USB转串口 —— 上位机和下位机通过USB转串口连接线直接相连进行数据交互;
  • 串口转WIFI(ESP8266) ——  上位机和下位机基于TCP/IP协议通过WIFI传输数据;
  • 串口转蓝牙(HC-06)—— 不多用,暂不介绍;

上位机软软件开发主要包括以下两种:

  1、Windows上位机(EXE可执行程序)

     在Windows上,最早用VB语言开发,后来由于C++的发展,采用MFC开发,近几年,微软发布了基于.NET框架的面向对象语言C#,更加稳定安全,再配合微软强大的VS进行开发,效率奇高;

    另外,如果想要在Linux上跨平台运行,可以选用Qt;如果想要更加丰富好看的数据显示界面,可以选用Labview开发;

  2、Android上位机(APP)

    在Android操作系统上,主要采用Java语言,使用WIFI或者蓝牙基于TCP/IP协议传输数据,利用Android Studio开发;

  在此,我们主要介绍如何通过VS + C#开发电脑上位机,其它上位机的开发暂且不论。

 

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

   https://blog.csdn.net/qq_36556893/article/details/79430133

上一篇大致了解了一下单片机实际项目开发中上位机开发部分的内容以及VS下载与安装,按照编程惯例,接下来就是“Hello,World!”

1、新建C#项目工程

   首先选择新建Windows窗体应用(.NET Framework),然后选择项目保存位置,填写项目名称,这里因为我们不需要用git进行版本管理,所以不用新建GIT存储库;

   框架是指.net框架,4以及4以下的.NET框架可以在xp上运行,4以上可以在win7/8/10上运行,鉴于当前大多数操作系统都是win7或win10,选择4.5版本。

  

2、窗体介绍及代码分析

  这里我们双击窗体界面,这也是VS的特性,双击一个控件,就会进入对应代码文件部分,这些代码全由VS在生成项目时自动生成,下面进行详细的解释:

/*filename:Form1.cs*/
//使用命名空间
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;

//用户项目工程自定义命名空间HelloWorld
namespace HelloWorld
{
    //定义了一个名称为Form1的公共类,并且在定义类的同时创建了一个这个类的对象,名为Form
    //partial关键字
    public partial class Form1 : Form
    {
        //与类同名的构造方法
        public Form1()
        {
            InitializeComponent();
        }
     //用户自定义方法,窗体加载时由Form对象调用
        private void Form1_Load(object sender, EventArgs e)
        {
        }
    }
}

 

 

  命名空间(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#编译器编译时会将这些类当作一个类来处理;

/*@filename:Form1.Designer.cs */

namespace HelloWorld
{
    partial class Form1
    {
        /// <summary>
        /// 必需的设计器变量。
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// 清理所有正在使用的资源。
        /// </summary>
        /// <param name="disposing">如果应释放托管资源,为 true;否则为 false。</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows 窗体设计器生成的代码

        /// <summary>
        /// 设计器支持所需的方法 - 不要修改
        /// 使用代码编辑器修改此方法的内容。
        /// </summary>
        private void InitializeComponent()
        {
            this.SuspendLayout();
            //
            // Form1
            //
            this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(418, 331);
            this.Name = "Form1";
            this.Text = "Form1";
            this.Load += new System.EventHandler(this.Form1_Load);
            this.ResumeLayout(false);

        }
        #endregion
    }
}

  Main: 一切程序都有入口主函数main,C#也是如此,在Program.cs文件中定义了Program类,该类中拥有主函数main( ), 在main函数中,第三行代码是一切的开始,调用Form1类的构造函数,创建一个Form对象,一切由此开始,代码如下:

/* @filename: Program.cs */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;

namespace HelloWorld
{
    static class Program
    {
        /// <summary>
        /// 应用程序的主入口点。
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());  //调用Form1类的构造函数,创建一个Form对象,一切由此开始
        }
    }
}

  再来解释一下最后三个文件:第一个文件主要是应用程序发布时的一些属性设置,版本号,属性,版权之类的,其余两个文件是工具自动生成的一些设置文件,不再过多赘述;

 

/* @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")]

3、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,那么从这篇开始,我们来自己动手写一个串口助手:

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.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已连接):

3、搭建后台

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

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

  在这个过程中,要注意一点,当我们点击打开按钮时,会发生一些我们编程时无法处理的事件,比如硬件串口没有连接,串口打开的过程中硬件突然断开,这些被称之为异常,针对这些异常,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方式后,串口助手就可以正常接收到数据了,如图:

 

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

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、接收部分

  之前我们直接在串口接收事件中调用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";   //刷新界面
        }

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 (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());    //调用发送按钮回调函数
        }

运行一下看一下效果:

https://www.cnblogs.com/liaocheng/p/9144317.html

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

c#串口编程(傻瓜教程,手把手教你学会) 的相关文章

  • 基于stm32cubeMX的平衡小车HAL库+蓝牙遥控+直立环+速度环+转向环 STM32F103C8T6

    基于stm32cubeMX的平衡小车HAL库 蓝牙遥控 直立环 速度环 转向环 一 代码工程链接 我的平衡车代码 HAL库 cubeMX配置 主控stm32f103 程序代码容易移植 建议先看完正文 工程 平衡车工程 PCB 代码程序 蓝牙
  • Java日常学习:运用链栈实现进制转换

    一 简单介绍 今天我就给大家带来java中的链栈实现进制转换的方法 xff0c 该方法也是在学习栈的时候发现的 xff0c 我们都知道在进制转换的时候有很多种方法 xff0c 其中最常用也最容易理解的就是除基倒取余法 xff0c 那么我们先
  • CLion解决中文输出乱码(2022年最新教程)

    CLion是个很好用的IDE xff0c 但是在用CLion写C C 43 43 代码时 xff0c 中文输出会是乱码 xff0c 此前博主也找了很多资料 xff0c 但是网上的教程大多都是脱裤子放屁 xff0c 治标不治本 xff0c 在
  • 【Linux网络编程】TCP并发服务器的实现(IO多路复用select)

    文章目录 一 服务器模型1 1 服务器概念1 2 TCP并发服务器的意义1 3 实现TCP并发服务器的方式 二 使用IO多路复用实现TCP并发服务器优势三 select函数四 TCP并发服务器的构建4 1 创建套接字4 2 填写服务器网络信
  • 树莓派解决cannot currently show the desktop最优法

    最近在搞树莓派前面的步骤都轻轻松松的解决了这个问题卡了我很久 这个是解决之后的效果 xff0c 我使用改分辨率用了很多办法这个办法解决的效率最快 直接在下载好文件的解决 1 将有sd卡的读卡器插入 2 在config txt文件中加入四句话
  • 十、C++中的类 class与struct的区别

    面向对象程序设计 xff0c 需要诸如类和对象这样的概念 C 43 43 支持面向过程 基于对象 面向对象 泛型编程四种 C语言不支持面向对象编程 类是一种将数据和函数组织在一起的方式 一个函数参数过多 xff0c 代码不好维护 xff0c
  • 飞控开发--气压计MS5611

    ms5611简介 xff1a 官方给出的最大分辨率 xff1a 10cm 工作电压 xff1a 1 8v 3 6v 气压 AD 精度 xff1a 24位 工作环境 xff1a xff0d 40 43 85 C xff0c 10 1200mb
  • 如何使用JavaScript将Set转换为Array?

    如何使用JavaScript将Set转换为Array xff1f 下面本篇文章家里给大家介绍一下在JavaScript中将Set转换为Array的方法 xff0c 希望对大家有所帮助 在JavaScript中 xff0c 想要将Set xf
  • 如何在Markdown中插入图片并顺利共享

    使用PicGo 43 GitHub搭建图床 实现便捷的Markdown图片管理 昨天在呕心沥血写 xff08 搬 xff09 完我的第一篇技术分享博文m1 使用 VMware 安装 CentOS7 并部署 k8s 高可用集群之后 xff0c
  • 海康威视网络摄像头sdk的开发(Demo的使用)指南

    如果您是想实现海康sdk包的Demo实例中MFC的分功能 xff0c 那么请慢慢看 xff0c 这篇文章百分之九十九可以帮你实现 提醒 xff1a 内容来自网络和自己实际操作 xff0c 如有问题请联系hww168 64 yeah net
  • Proxy反向代理解决跨域问题

    一 问题的产生 在项目开发过程中遇到了一个问题 xff0c 访问图片与访问其他的数据的服务器不是同一个 xff0c 虽然后台已经将两个服务器合并到一个 xff0c 但是前台配置好后访问就会产生了一个问题 xff0c 具体报错如下 xff1a
  • 什么是枚举【详解】

    本期介绍 x1f356 主要介绍 xff1a 什么是枚举 xff0c 枚举是如何定义 初始化的 xff0c 以及枚举到底如何使用 xff0c 还有枚举这种语法存在的优点有那些 x1f440 文章目录 一 什么是枚举 x1f356 二 枚举类
  • vscode将项目导入远程仓库 git

    1 打开软件 点击第三个图标 远程仓库 点击代码管理右上角三个小点 远程 远程仓库 添加远程存储库 跳出输入框 2 复制git的地址 3 将上一步复制的仓库链接 粘贴进去 回车 4 命名 5 打开命令行 输入下列命令 用户名 邮箱 PS D
  • 深度学习二

    BT神经元为按照误 差逆向传播算法训练的多层前馈神经网络 BT神经网络分为输入层 隐藏层 输出层 输入层一般有数据种类多个神经元 xff0c 接受数据 隐藏层的神经元为根号下隐藏层 输出层加b个 xff0c 输入的每个数据加权和返回数之和为
  • ROS学习(五)

    学习古月 ROS机器人开发实践 一书时 xff0c 在第六章的6 3遇到的问题的总结 在执行将xacro文件转化成urdf的文件格式时 xff0c 执行语句 rosrun xacro xacro py mrobot urdf xacro g
  • 新建Mavlink消息

    1 下载Mavlink生成器 1 1 Git clone 需要在翻墙的网络环境下下载 span class token function git span clone https github com mavlink mavlink git
  • Python从入门到精通11天(lambda匿名函数和map函数的使用)

    lambda匿名函数和map函数的使用 lambda匿名函数map函数lambda与map的联用 lambda匿名函数 匿名函数在计算机编程中是指一类无需定义标识符 xff08 函数名 xff09 的函数或子程序 xff0c lambda函
  • STM32F407单片机移植ADS1115驱动程序

    最近一个工程项目需要使用ADS1115采集电压 xff0c 网上研究了一下 xff0c 测试成功 xff0c 期间走了很多弯路 xff0c 为避免后来的研究者重走我的老路 xff0c 特分享给大家 注 xff1a 网上很多ADS1115驱动
  • Python入门到精通12天(迭代器与生成器)

    迭代器与生成器 迭代器生成器 迭代器 迭代器是可迭代的对象 xff0c 即可以进行遍历的对象 列表 字符串 元组 字典和集合这些都是可迭代的对象 xff0c 都可以进行遍历 迭代器是一种访问序列元素的方式 xff0c 它可以通过next 函
  • C语言从入门到精通第8天(分支结构if、else、switch的使用)

    分支结构if else switch的使用 if语句if else语句if else嵌套if else if else语句switch语句 if语句 语法 xff1a if 表达式 语句 xff1b 如果表达式为真 xff0c 则执行 里面

随机推荐

  • Python入门到精通13天(global和nonlocal关键字的使用)

    global和nonlocal关键字的使用 作用域global关键字的使用nonlocal关键字的使用 作用域 在Python中变量的作用域由其代码块决定 xff0c 在代码块中定义的的变量和函数属于局部作用域 xff1b 在函数中定义的变
  • Python从入门到精通14天(eval、literal_eval、exec函数的使用)

    eval literal eval exec函数的使用 eval函数literal eval函数exec函数三者的区别 eval函数 eval 是Python中的内置函数 xff0c 它可以将一个字符串作为参数 xff0c 并将该字符串作为
  • Python从入门到精通15天(浅拷贝和深拷贝)

    浅拷贝和深拷贝 深浅拷贝概述浅拷贝深拷贝 深浅拷贝概述 在Python中 xff0c 对象是通过引用传递的 xff0c 这意味如果创建了一个对象 xff0c 然后将其赋值给另一个变量 xff0c 那么两个变量将引用同一个对象 xff0c 即
  • C语言从入门到精通第9天(循环结构的使用)

    循环结构的使用 while语句do while语句for语句嵌套循环 循环结构可以重复的执行一段代码块 xff0c 在C语言中提供了三种不同类型的循环结构 xff1a for while和do while while语句 语法 xff1a
  • C语言从入门到精通第10天(break和continue的使用)

    break和continue的使用 break语句continue语句 break和continue是两种控制流程的语句 xff0c 他们只能在循环中被使用 xff0c 用于控制循环的执行 如果在非循环中被使用了则会导致语法的错误 brea
  • C语言从入门到精通第11天(数组的基本操作)

    数组的基本操作 数组的概念一维数组二维数组 数组的概念 在程序设计中 xff0c 为了方便处理数据把具有相同类型的若干变量按有序形式集合在一起 xff0c 这些按序排列的同类数据元素的集合称为数组 在C语言中 xff0c 数组属于构造数据类
  • C语言从入门到精通第12天(函数的定义)

    函数的定义 函数的概念函数的定义函数的参数函数的返回值 函数的概念 在程序设计中 xff0c 为了实现某个功能需要编写多行代码 xff0c 我们每次在使用时都将原来的代码重复编码 xff0c 这样就非常的麻烦 xff0c 而且编程的效率也不
  • C语言从入门到精通第13天(函数的调用)

    函数的调用 无参函数的调用有参函数的调用函数的嵌套调用递归函数的调用函数的声明 函数在定义完以后 xff0c 如果不被调用时不会被执行到的 xff1b 在程序中main函数是主函数 xff0c 是会被自动调用 xff0c C程序有且只有一个
  • 项目实战-外卖自提柜 2. CubeMX + FreeRTOS入门

    项目实战 外卖自提柜 1 项目介绍 协议制定 项目实战 外卖自提柜 2 CubeMX 43 FreeRTOS入门 项目实战 外卖自提柜 3 FreeRTOS主要API的应用 项目实战 外卖自提柜 4 FreeRTOS 堆栈分配 调试技巧 项
  • C语言从入门到精通第14天(局部变量和全局变量)

    局部变量和全局变量 局部变量全局变量 局部变量 简单来说 xff0c 在C语言中的局部变量就是定义在 中的变量 xff0c 他的作用域也在 内 xff0c 他的生命周期随着 结束而结束 例如 xff1a span class token k
  • C语言从入门到精通第15天(C语言预处理)

    C语言预处理 预处理概述宏定义条件编译 预处理概述 在前面我们已经对C语言的基础语法知识有所了解了 xff0c 每次进行程序的编写之前 xff0c 我们会使用 include命令去导入我们的库函数 xff0c 而这种以 号开头的命令称为预处
  • C语言从入门到精通第16天(指针的定义与基本使用)

    指针的定义与基本使用 什么是指针 xff1f 指针变量的定义指针变量的基本使用 什么是指针 xff1f 在使用指针之前我们需要对指针进行初步的了解 xff0c 首先我们要知道什么是指针 xff1f 通过前面的学习我们已经知道了内存的存储方式
  • 作为一个大学生你应该知道的事情

    作为一个大学生你应该知道的事情 大学生毕业去向 今天 xff0c 我们不写技术 xff0c 来谈一谈大学生的毕业现状 xff1a 以下内容为本人的一些观点和看法 xff0c 仅限于沟通交流 大学生毕业去向 大学生的毕业去向大致可以分为 xf
  • C语言从入门到精通第17天(指针和数组联用)

    指针和数组联用 不同类型指针变量之间的区别数组的指针指针数组 不同类型指针变量之间的区别 在了解数组和指针联用之前 xff0c 我们先对指针变量进行补充 我们对比一下int p1和char p2的区别 xff1f 相同点 xff1a 都是指
  • 使用 Keil uVision 和 STM32CubeMX 对 STM32F103C8 进行编程

    采用ARM Cortex M架构的STM32微控制器因其特性 成本和性能而在许多应用中得到广泛应用 在之前的教程中 xff0c 我们已经使用Arduino IDE编程了STM32F103C8 使用Arduino IDE编程STM32很简单
  • 适用于 STM32F103C8 的 FreeRTOS,STM32的多任务同时进行

    概述 xff1a 适用于 STM32F103C8 的 FreeRTOS STM32F103C 是一款ARM Cortex M3 处理器 我们可以在 Arduino IDE 中使用适用于 STM32F103C8 的 FreeRTOS 我们也可
  • PLC为什么会被上位机取代

    随着我们进入高速发展的轨道 xff0c 许多工厂都已经完成了自动化流水线生产的打造 我们可以看到很多大厂已经搭建了智能生产线 所以越来越多的plc工程师开始感觉到 xff0c 只会传统的plc控制 xff0c 已经无法满足公司的企业发展业务
  • 【Vue2】生命周期——钩子函数

    钩子函数 xff1a 在一个Vue实例从创建到销毁的过程自动执行的函数 1 分析生命周期 1 xff09 初始化阶段 xff1a beforeCreate xff08 xff09 生命周期中第一个函数 xff0c 在该函数执行时Vue实例仅
  • 计算机考研全年规划

    此文转载的 xff0c 作为参考 文章目录 一 关于考研常识二 择校择专业 xff08 一 xff09 为什么要考研 xff08 二 xff09 怎么样才能考上研究生 xff08 三 xff09 如何择校选专业1 学硕和专硕该如何选择 xf
  • c#串口编程(傻瓜教程,手把手教你学会)

    在单片机项目开发中 xff0c 上位机也是一个很重要的部分 xff0c 主要用于数据显示 xff08 波形 温度等 xff09 用户控制 xff08 LED xff0c 继电器等 xff09 xff0c 下位机 xff08 单片机 xff0