上一篇中我們完成了一個串口助手的雛形,實現了基本發送和接收字符串功能,並將打開/關閉串口進行了異常處理,這篇就來按照流程,逐步將功能完善:
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()); //調用發送按鈕回調函數 }
運行一下看一下效果: