用C#一步步寫串列埠通訊

GeekWay發表於2012-08-10

   附言:

1. 有網友反應我寫的這篇文章還不錯,索性就將它置頂了,希望對大家串列埠程式設計的學習有所幫助。

2.在此吐槽一下東家CSDN的編輯框,非常難用,剛才排版還好好的,現在開啟又是一團糟,對你造成的閱讀不便我帶csdn向您道歉!

以下是正文:

===============================================================================================================================

  

      最近在公司讓用C#寫一個串列埠除錯的工具,要求向串列埠中輸入16進位制資料或字串。下面我將這次遇到的問題和解決方法奉獻出來,希望對工作中需要的朋友有所幫助!
     
我們來看具體的實現步驟。

   公司要求實現以下幾個功能:
1):實現兩臺計算機之前的串列埠通訊,以16進位制形式和字串兩種形式傳送和接收。
2):根據需要設定串列埠通訊的必要引數。
3):定時傳送資料。
4):儲存串列埠設定。


      看著好像挺複雜,其實都是紙老虎,一戳就破,前提是你敢去戳。我儘量講的詳細一些,爭取說到每個知識點。

      在編寫程式前,需要將你要測試的COM口短接,就是收發資訊都在本地計算機,短接的方式是將COM口的2、3號針接起來。COM口各針的具體作用,度娘是這麼說的:COM口。記住2、3針連線一定要連線牢固,我就是因為接觸不良,導致本身就不通,白白花掉了一大半天時間除錯程式碼。

下面給出主要的操作介面,如下:



順便,我將所有控制元件對應的程式碼名字也附上了,相信對初學者來說,再看下面的程式碼會輕鬆很多。控制元件名字命名的方法是“控制元件名+作用”的形式,例如“開啟串列埠”的開關按鈕,其名字是btnSwitch  (btn就是button的簡寫了)。我認為這種命名控制元件的方式比較好,建議大家使用,如果你有好的命名方式,希望你能告訴我!

下面我們將各個功能按照從主到次的順序逐個實現。(我分塊給出程式碼實現,程式碼下載見文章最後。)


一、獲取計算機的COM口總個數,將它們列為控制元件cbSerial的候選項,並將第一個設為cbSerial的預設選項。

這部分是在窗體載入時完成的。請看程式碼:
(很多資訊程式碼的註釋裡講的很清楚,我就不贅述了。)

 //檢查是否含有串列埠
            string[] str = SerialPort.GetPortNames();
            if (str == null)
            {
                MessageBox.Show("本機沒有串列埠!", "Error");
                return;
            }

            //新增串列埠專案
            foreach (string s in System.IO.Ports.SerialPort.GetPortNames())
            {//獲取有多少個COM口
                cbSerial.Items.Add(s);
            }

            //串列埠設定預設選擇項
            cbSerial.SelectedIndex = 0;         //設定cbSerial的預設選項



二、“串列埠設定”
這面我沒程式碼程式設計,直接從窗體上按照串列埠資訊設定就行。我們僅設定它們的預設選項,但這裡我用到了ini檔案,暫時不講,我們先以下面形式設定預設

 
cbBaudRate.SelectedIndex = 5;
 cbDataBits.SelectedIndex = 3;
 cbStop.SelectedIndex = 0;
 cbParity.SelectedIndex = 0;
 radio1.Checked = true;  //傳送資料的“16進位制”單選按鈕(這裡我忘了改名,現在看著很不舒服!)
 rbRcvStr.Checked = true;


三、開啟串列埠
在傳送資訊之前,我們需要根據選中的選項設定串列埠資訊,並設定一些控制元件的屬性,最後將串列埠開啟。

private void btnSwitch_Click(object sender, EventArgs e)        { //sp1是全域性變數。 SerialPort>
  private void btnSwitch_Click(object sender, EventArgs e)
        {
	//sp1是全域性變數。 SerialPort>

四、傳送資訊因為這裡涉及到字元的轉換,難點在於,在傳送16進位制資料時,如何將文字框中的字元資料在記憶體中以同樣的形式表現出來,例如我們輸入16進位制的“eb 90”顯示到記憶體中,也就是如下形式:

        

或輸入我們想要的任何位元組,如上面的“12 34 56 78 90”.記憶體中的資料時16進位制顯示的,而我們輸入的資料時字串,我們需要將字串轉換為對應的16進位制資料,然後將這個16進位制資料轉換為位元組資料,用到的主要方法是:

Convert.ToInt32  (String, Int32);Convert.ToByte  (Int32);


這是我想到的,如果你有好的方法,希望你能告訴我。

下面看程式碼:

private void btnSend_Click(object sender, EventArgs e) { 
		if (!sp1.IsOpen) //如果沒開啟 
			{ MessageBox.Show("請先開啟串列埠!", "Error");
			return; 
			} 
		String strSend = txtSend.Text; 
		if (radio1.Checked == true)	//“16進位制傳送” 按鈕 
			{ 
			//處理數字轉換,目的是將輸入的字元按空格、“,”等分組,以便傳送資料時的方便(此處轉的比較麻煩,有高見者,請指點!) 
			string sendBuf = strSend; string sendnoNull = sendBuf.Trim(); 
			string sendNOComma = sendnoNull.Replace(',', ' '); //去掉英文逗號 string sendNOComma1 = sendNOComma.Replace(',', ' '); //去掉中文逗號 
			string strSendNoComma2 = sendNOComma1.Replace("0x", ""); //去掉0x 
			strSendNoComma2.Replace("0X", ""); //去掉0X 
			string[] strArray = strSendNoComma2.Split(' ');	 //strArray陣列中會出現“”空字元的情況,影響下面的賦值操作,故將byteBufferLength相應減小 
			int byteBufferLength = strArray.Length; 
			for (int i = 0; i <strArray.Length; i++ ) 
			{
				if (strArray[i]=="") 
				{ 
					byteBufferLength--;
				} 
				} byte[] byteBuffer = new byte[byteBufferLength]; 
			}
		int ii = 0;	//用於給byteBuffer賦值 >
	}


五、資料的接收

亮點來了,看到這裡,如果你還沒吐(可能是我的程式碼比較拙劣!),那麼下面的知識點對你也不成問題。這裡需要用到 委託 的知識,我是搞C/C++出身,剛碰到這個知識點還真有點不適應。為了不偏離主題,關於委託,我僅給出兩條比較好的連結,需要的網友可以去加深學習:C#委託訂閱委託事件。       在窗體載入時就訂閱上委託是比較好的,所以在Form1_Load中新增以下程式碼:

Control.CheckForIllegalCrossThreadCalls = false;    //意圖見解釋
sp1.DataReceived += new SerialDataReceivedEventHandler(sp1_DataReceived); //訂閱委託
      注意,因為自.net 2.0以後加強了安全機制,,不允許在winform中直接跨執行緒(事件觸發需要產生一個執行緒處理)訪問控制元件的屬性,第一條程式碼的意圖是說在這個類中我們強制不檢查跨執行緒的呼叫是否合法。處理這種問題的解決方案有很多,具體可參閱以下內容:解決方案。
      好了,訂閱委託之後,我們就可以處理接收資料的事件了。
 void sp1_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            if (sp1.IsOpen)     //此處可能沒有必要判斷是否開啟串列埠,但為了嚴謹性,我還是加上了
            {
                byte[] byteRead = new byte[sp1.BytesToRead];    //BytesToRead:sp1接收的字元個數
                if (rdSendStr.Checked)                          //'傳送字串'單選按鈕
                {
                    txtReceive.Text += sp1.ReadLine() + "\r\n"; //注意:回車換行必須這樣寫,單獨使用"\r"和"\n"都不會有效果
                    sp1.DiscardInBuffer();                      //清空SerialPort控制元件的Buffer 
                }
                else                                            //'傳送16進位制按鈕'
                {
                    try
                    {
                        Byte[] receivedData = new Byte[sp1.BytesToRead];        //建立接收位元組陣列
                        sp1.Read(receivedData, 0, receivedData.Length);         //讀取資料                       
                        sp1.DiscardInBuffer();                                  //清空SerialPort控制元件的Buffer
                        string strRcv = null;

                        for (int i = 0; i < receivedData.Length; i++) //窗體顯示
                        {
                          
                            strRcv += receivedData[i].ToString("X2");  //16進位制顯示
                        }
                        txtReceive.Text += strRcv + "\r\n";
                    }
                    catch (System.Exception ex)
                    {
                        MessageBox.Show(ex.Message, "出錯提示");
                        txtSend.Text = "";
                    }
                }
            }
            else
            {
                MessageBox.Show("請開啟某個串列埠", "錯誤提示");
            }
        }




       為了友好和美觀,我將當前時間也顯示出來,又將顯示字型的顏色做了修改:

//輸出當前時間
                DateTime>

做到這裡,大部分功能就已實現了,剩下的工作就是些簡單的操作設定了,有儲存設定、定時傳送資訊、控制文字框輸入內容等。六、儲存設定這部分相對簡單,但當時我沒接觸過,也花了點時間,現在想想,也不過如此。儲存使用者設定用ini檔案是個不錯的選擇,雖然大部分都用登錄檔實現,但ini檔案儲存還是有比較廣泛的使用。.ini 檔案是Initialization File的縮寫,也就是初始化檔案。為了不偏離正題,也不過多說明,可參考相關內容(網上資源都不錯,因人而異,就不加連結了)。使用Inifile讀寫ini檔案,這裡我用到了兩個主要方法:

//讀出ini檔案
a:=inifile.Readstring('節點','關鍵字',預設值);// string型別 
b:=inifile.Readinteger('節點','關鍵字',預設值);// integer型別 
c:=inifile.Readbool('節點','關鍵字',預設值);// boolean型別 
其中[預設值]為該INI檔案不存在該關鍵字時返回的預設值。 
//寫入INI檔案: 
inifile.writestring('節點','關鍵字',變數或字串值); 
inifile.writeinteger('節點','關鍵字',變數或整型值); 
inifile.writebool('節點','關鍵字',變數或True或False); 

請看程式碼:
//using 省寫了
namespace INIFILE
{
    class Profile
    {
        public static void LoadProfile()
        {
            string strPath = AppDomain.CurrentDomain.BaseDirectory;
            _file = new IniFile(strPath + "Cfg.ini");
            G_BAUDRATE = _file.ReadString("CONFIG", "BaudRate", "4800");    //讀資料,下同
            G_DATABITS = _file.ReadString("CONFIG", "DataBits", "8");
            G_STOP = _file.ReadString("CONFIG", "StopBits", "1");
            G_PARITY = _file.ReadString("CONFIG", "Parity", "NONE");
         
        }

        public static void SaveProfile()
        {
            string strPath = AppDomain.CurrentDomain.BaseDirectory;
            _file = new IniFile(strPath + "Cfg.ini");
            _file.WriteString("CONFIG", "BaudRate", G_BAUDRATE);            //寫資料,下同
            _file.WriteString("CONFIG", "DataBits", G_DATABITS);
            _file.WriteString("CONFIG", "StopBits", G_STOP);
            _file.WriteString("CONFIG", "G_PARITY", G_PARITY);
        }

        private static IniFile _file;//內建了一個物件

        public static string G_BAUDRATE = "1200";//給ini檔案賦新值,並且影響介面下拉框的顯示
        public static string G_DATABITS = "8";
        public static string G_STOP = "1";
        public static string G_PARITY = "NONE";    
    }
}



_file宣告成了內建物件,可以方便各函式的呼叫。
下面是“儲存設定”的部分程式碼:

private void btnSave_Click(object sender, EventArgs e)
        {
            
            //設定各“串列埠設定”
            string strBaudRate = cbBaudRate.Text;
            string strDateBits = cbDataBits.Text;
            string strStopBits = cbStop.Text;
            Int32 iBaudRate = Convert.ToInt32(strBaudRate);
            Int32 iDateBits = Convert.ToInt32(strDateBits);


            Profile.G_BAUDRATE = iBaudRate+"";       //波特率
            Profile.G_DATABITS = iDateBits+"";       //資料位
            switch (cbStop.Text)            //停止位
            {
                case "1":
                    Profile.G_STOP = "1";
                    break;
                case "1.5":
                    Profile.G_STOP = "1.5";
                    break;
               //防止過多刷屏,下面省寫了
		……
            }
            switch (cbParity.Text)             //校驗位
            {
                case "無":
                    Profile.G_PARITY = "NONE";
                    break;
              	…………
            }
        Profile.SaveProfile();	//儲存設定
}



    讀取ini檔案主要在載入窗體時執行:
INIFILE.Profile.LoadProfile();//載入所有


七、控制文字輸入這裡倒挺簡單,只是注意一點。當我們控制輸入非法字元時,可通過控制e.Handed的屬性值實現,注意這裡的Handed屬性是“操作過”的含義,而非“執行此處操作”之意,Handled是過去式,看字面意思,"操作過的=是;",將這個操作的狀態設為已處理過,自然就不會再處理了。具體參見MSDN:Handed

private void txtSend_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (radio1.Checked== true)
            {
                //正則匹配
                string patten = "[0-9a-fA-F]|\b|0x|0X| "; //“\b”:退格鍵
                Regex r = new Regex(patten);
                Match m = r.Match(e.KeyChar.ToString());


                if (m.Success )//&&(txtSend.Text.LastIndexOf(" ") != txtSend.Text.Length-1))
                {
                    e.Handled = false;
                }
                else
                {
                    e.Handled = true;
                }
            }//end of radio1


八、定時傳送資訊
     這邊看似很簡單,但也有一點需要注意,當定時器生效時,我們要間隔訪問“傳送”按鍵的內容,怎麼實現?還好MS給我們提供了必要的支援,使用Button的 PerformClick可以輕鬆做到,  PerformClick參見MSDN:PerformClick


private void tmSend_Tick(object sender, EventArgs e)
        {
            //轉換時間間隔
            string strSecond = txtSecond.Text;
            try
            {
                int isecond = int.Parse(strSecond) * 1000;//Interval以微秒為單位
                tmSend.Interval = isecond;
                if (tmSend.Enabled == true)
                {
                    btnSend.PerformClick();	//產生“傳送”的click事件
                }
            }
            catch (System.Exception ex)
            {
                MessageBox.Show("錯誤的定時輸入!", "Error");
            }
            
        }


   千萬注意在一些情況下不要忘了讓定時器失效,如在取消“定時傳送資料"和“關閉串列埠”時等。


程式碼下載:

有CSDN賬號的童鞋:《C#串列埠通訊工具》

無CSDN賬戶的童鞋:C#串列埠通訊工具》









相關文章