在單片機項目開發中,上位機也是一個很重要的部分,主要用於數據顯示(波形、溫度等)、用戶控制(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()); //調用發送按鈕回調函數 }
運行一下看一下效果: