最近在公司讓用C#寫一個串口調(diào)試的工具,要求向串口中輸入16進制數(shù)據(jù)或字符串,。下面我將這次遇到的問題和解決方法奉獻出來,目的是和同行交流,,回饋網(wǎng)友們提供的幫助,,也是為了自己對知識加深一下鞏固。 我們來看具體的實現(xiàn)步驟,。 公司要求實現(xiàn)以下幾個功能: 1):實現(xiàn)兩臺計算機之前的串口通信,,以16進制形式和字符串兩種形式傳送和接收。 2):根據(jù)需要設(shè)置串口通信的必要參數(shù),。 3):定時發(fā)送數(shù)據(jù),。 4):保存串口設(shè)置。 看著好像挺復(fù)雜,,其實都是紙老虎,,一戳就破,前提是你敢去戳,。我盡量講的詳細(xì)一些,,爭取說到每個知識點。 在編寫程序前,,需要將你要測試的COM口短接,,就是收發(fā)信息都在本地計算機,短接的方式是將COM口的2,、3號針接起來,。COM口各針的具體作用,度娘是這么說的:COM口,。記住2,、3針連接一定要連接牢固,我就是因為接觸不良,,導(dǎo)致本身就不通,,白白花掉了一大半天時間調(diào)試代碼。 下面給出主要的操作界面,,如下: 順便,,我將所有控件對應(yīng)的代碼名字也附上了,相信對初學(xué)者來說,,再看下面的代碼會輕松很多,。控件名字命名的方法是“控件名+作用”的形式,,例如“打開串口”的開關(guān)按鈕,,其名字是btnSwitch (btn就是button的簡寫了),。我認(rèn)為這種命名控件的方式比較好,建議大家使用,,如果你有好的命名方式,,希望你能告訴我! 下面我們將各個功能按照從主到次的順序逐個實現(xiàn),。(我分塊給出代碼實現(xiàn),,詳細(xì)代碼見鏈接:《C#串口通信工具》) 一、獲取計算機的COM口總個數(shù),,將它們列為控件cbSerial的候選項,,并將第一個設(shè)為cbSerial的默認(rèn)選項。 這部分是在窗體加載時完成的,。請看代碼: (很多信息代碼的注釋里講的很清楚,,我就不贅述了。) [csharp] view plaincopyprint? //檢查是否含有串口 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); //串口設(shè)置默認(rèn)選擇項 cbSerial.SelectedIndex = 0; //設(shè)置cbSerial的默認(rèn)選項 二,、“串口設(shè)置” 這面我沒代碼編程,直接從窗體上按照串口信息設(shè)置就行,。我們僅設(shè)置它們的默認(rèn)選項,,但這里我用到了ini文件,暫時不講,,我們先以下面形式設(shè)置默認(rèn),。 [csharp] view plaincopyprint? cbBaudRate.SelectedIndex = 5; cbDataBits.SelectedIndex = 3; cbStop.SelectedIndex = 0; cbParity.SelectedIndex = 0; radio1.Checked = true; //發(fā)送數(shù)據(jù)的“16進制”單選按鈕(這里我忘了改名,現(xiàn)在看著很不舒服?。?nbsp; rbRcvStr.Checked = true; 三、打開串口 在發(fā)送信息之前,,我們需要根據(jù)選中的選項設(shè)置串口信息,,并設(shè)置一些控件的屬性,最后將串口打開,。 [csharp] view plaincopyprint? private void btnSwitch_Click(object sender, EventArgs e) //sp1是全局變量,。 SerialPort sp1 = new SerialPort(); if (!sp1.IsOpen) try //設(shè)置串口號 string serialName = cbSerial.SelectedItem.ToString(); sp1.PortName = serialName; //設(shè)置各“串口設(shè)置” string strBaudRate = cbBaudRate.Text; string strDateBits = cbDataBits.Text; string strStopBits = cbStop.Text; Int32 iBaudRate = Convert.ToInt32(strBaudRate); Int32 iDateBits = Convert.ToInt32(strDateBits); sp1.BaudRate = iBaudRate; //波特率 sp1.DataBits = iDateBits; //數(shù)據(jù)位 switch (cbStop.Text) //停止位 case "1": sp1.StopBits = StopBits.One; break; case "1.5": sp1.StopBits = StopBits.OnePointFive; break; case "2": sp1.StopBits = StopBits.Two; break; default: MessageBox.Show("Error:參數(shù)不正確!", "Error"); break; switch (cbParity.Text) //校驗位 case "無": sp1.Parity = Parity.None; break; case "奇校驗": sp1.Parity = Parity.Odd; break; case "偶校驗": sp1.Parity = Parity.Even; break; default: MessageBox.Show("Error:參數(shù)不正確!", "Error"); break; if (sp1.IsOpen == true)//如果打開狀態(tài),則先關(guān)閉一下 sp1.Close(); //狀態(tài)欄設(shè)置 tsSpNum.Text = "串口號:" + sp1.PortName + "|"; tsBaudRate.Text = "波特率:" + sp1.BaudRate + "|"; tsDataBits.Text = "數(shù)據(jù)位:" + sp1.DataBits + "|"; tsStopBits.Text = "停止位:" + sp1.StopBits + "|"; tsParity.Text = "校驗位:" + sp1.Parity + "|"; //設(shè)置必要控件不可用 cbSerial.Enabled = false; cbBaudRate.Enabled = false; cbDataBits.Enabled = false; cbStop.Enabled = false; cbParity.Enabled = false; sp1.Open(); //打開串口 btnSwitch.Text = "關(guān)閉串口"; catch (System.Exception ex) MessageBox.Show("Error:" + ex.Message, "Error"); return; else //狀態(tài)欄設(shè)置 tsSpNum.Text = "串口號:未指定|"; tsBaudRate.Text = "波特率:未指定|"; tsDataBits.Text = "數(shù)據(jù)位:未指定|"; tsStopBits.Text = "停止位:未指定|"; tsParity.Text = "校驗位:未指定|"; //恢復(fù)控件功能 //設(shè)置必要控件不可用 cbSerial.Enabled = true; cbBaudRate.Enabled = true; cbDataBits.Enabled = true; cbStop.Enabled = true; cbParity.Enabled = true; sp1.Close(); //關(guān)閉串口 btnSwitch.Text = "打開串口"; 四,、發(fā)送信息 因為這里涉及到字符的轉(zhuǎn)換,,難點在于,在發(fā)送16進制數(shù)據(jù)時,,如何將文本框中的字符數(shù)據(jù)在內(nèi)存中以同樣的形式表現(xiàn)出來,,例如我們輸入16進制的“eb 90”顯示到內(nèi)存中,也就是如下形式: 或輸入我們想要的任何字節(jié),,如上面的“12 34 56 78 90”. 內(nèi)存中的數(shù)據(jù)時16進制顯示的,,而我們輸入的數(shù)據(jù)時字符串,,我們需要將字符串轉(zhuǎn)換為對應(yīng)的16進制數(shù)據(jù),然后將這個16進制數(shù)據(jù)轉(zhuǎn)換為字節(jié)數(shù)據(jù),,用到的主要方法是: Convert.ToInt32 (String, Int32),; Convert.ToByte (Int32); 這是我想到的,如果你有好的方法,,希望你能告訴我,。下面看代碼: [csharp] view plaincopyprint? private void btnSend_Click(object sender, EventArgs e) if (!sp1.IsOpen) //如果沒打開 MessageBox.Show("請先打開串口!", "Error"); return; String strSend = txtSend.Text; if (radio1.Checked == true) //“16進制發(fā)送” 按鈕 //處理數(shù)字轉(zhuǎn)換,,目的是將輸入的字符按空格,、“,”等分組,,以便發(fā)送數(shù)據(jù)時的方便(此處轉(zhuǎn)的比較麻煩,,有高見者,請指點?。?nbsp; 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數(shù)組中會出現(xiàn)“”空字符的情況,影響下面的賦值操作,,故將byteBufferLength相應(yīng)減小 int byteBufferLength = strArray.Length; for (int i = 0; i <strarray.length; i++ ) < p=""> if (strArray[i]=="") byteBufferLength--; byte[] byteBuffer = new byte[byteBufferLength]; int ii = 0; //用于給byteBuffer賦值 for (int i = 0; i < strArray.Length; i++) //對獲取的字符做相加運算 Byte[] bytesOfStr = Encoding.Default.GetBytes(strArray[i]); int decNum = 0; if (strArray[i] == "") continue; else decNum = Convert.ToInt32(strArray[i], 16); //atrArray[i] == 12時,,temp == 18 try //防止輸錯,使其只能輸入一個字節(jié)的字符,,即只能在txtSend里輸入 “eb 90”等字符串,,不能輸入“123 2345”等超出字節(jié)范圍的數(shù)字 byteBuffer[ii] = Convert.ToByte(decNum); catch (System.Exception ex) MessageBox.Show("字節(jié)越界,請逐個字節(jié)輸入,!", "Error"); return; ii++; sp1.Write(byteBuffer, 0, byteBuffer.Length); else //以字符串形式發(fā)送時 sp1.WriteLine(txtSend.Text); //寫入數(shù)據(jù) 五,、數(shù)據(jù)的接收 亮點來了,看到這里,,如果你還沒吐(可能是我的代碼比較拙劣?。敲聪旅娴闹R點對你也不成問題,。 這里需要用到 委托 的知識,,我是搞C/C++出身,剛碰到這個知識點還真有點不適應(yīng),。為了不偏離主題,,關(guān)于委托,我僅給出兩條比較好的鏈接,,需要的網(wǎng)友可以去加深學(xué)習(xí):C#委托,、訂閱委托事件。 在窗體加載時就訂閱上委托是比較好的,所以在Form1_Load中添加以下代碼: [csharp] view plaincopyprint? Control.CheckForIllegalCrossThreadCalls = false; //意圖見解釋 sp1.DataReceived += new SerialDataReceivedEventHandler(sp1_DataReceived); //訂閱委托 注意,,因為自.net 2.0以后加強了安全機制,,,不允許在winform中直接跨線程(事件觸發(fā)需要產(chǎn)生一個線程處理)訪問控件的屬性,第一條代碼的意圖是說在這個類中我們強制不檢查跨線程的調(diào)用是否合法,。處理這種問題的解決方案有很多,,具體可參閱以下內(nèi)容:解決方案。 好了,,訂閱委托之后,,我們就可以處理接收數(shù)據(jù)的事件了。 [csharp] view plaincopyprint? void sp1_DataReceived(object sender, SerialDataReceivedEventArgs e) if (sp1.IsOpen) //此處可能沒有必要判斷是否打開串口,,但為了嚴(yán)謹(jǐn)性,,我還是加上了 byte[] byteRead = new byte[sp1.BytesToRead]; //BytesToRead:sp1接收的字符個數(shù) if (rdSendStr.Checked) //'發(fā)送字符串'單選按鈕 txtReceive.Text += sp1.ReadLine() + "\r\n"; //注意:回車換行必須這樣寫,單獨使用"\r"和"\n"都不會有效果 sp1.DiscardInBuffer(); //清空SerialPort控件的Buffer else //'發(fā)送16進制按鈕' try Byte[] receivedData = new Byte[sp1.BytesToRead]; //創(chuàng)建接收字節(jié)數(shù)組 sp1.Read(receivedData, 0, receivedData.Length); //讀取數(shù)據(jù) 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("請打開某個串口", "錯誤提示"); 為了友好和美觀,,我將當(dāng)前時間也顯示出來,,又將顯示字體的顏色做了修改: [csharp] view plaincopyprint? //輸出當(dāng)前時間 DateTime dt = DateTime.Now; txtReceive.Text += dt.GetDateTimeFormats('f')[0].ToString() + "\r\n"; txtReceive.SelectAll(); txtReceive.SelectionColor = Color.Blue; //改變字體的顏色 做到這里,大部分功能就已實現(xiàn)了,,剩下的工作就是些簡單的操作設(shè)置了,,有保存設(shè)置、定時發(fā)送信息,、控制文本框輸入內(nèi)容等,。 六、保存設(shè)置 這部分相對簡單,,但當(dāng)時我沒接觸過,,也花了點時間,現(xiàn)在想想,,也不過如此,。 保存用戶設(shè)置用ini文件是個不錯的選擇,雖然大部分都用注冊表實現(xiàn),,但ini文件保存還是有比較廣泛的使用,。 .ini 文件是Initialization File的縮寫,也就是初始化文件,。 為了不偏離正題,也不過多說明,,可參考相關(guān)內(nèi)容(網(wǎng)上資源都不錯,,因人而異,就不加鏈接了),。 使用Inifile讀寫ini文件,,這里我用到了兩個主要方法: [csharp] view plaincopyprint? //讀出ini文件 a:=inifile.Readstring('節(jié)點','關(guān)鍵字',缺省值);// string類型 b:=inifile.Readinteger('節(jié)點','關(guān)鍵字',缺省值);// integer類型 c:=inifile.Readbool('節(jié)點','關(guān)鍵字',缺省值);// boolean類型 其中[缺省值]為該INI文件不存在該關(guān)鍵字時返回的缺省值。 //寫入INI文件: inifile.writestring('節(jié)點','關(guān)鍵字',變量或字符串值); inifile.writeinteger('節(jié)點','關(guān)鍵字',變量或整型值); inifile.writebool('節(jié)點','關(guān)鍵字',變量或True或False); 請看代碼: [csharp] view plaincopyprint? //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"); //讀數(shù)據(jù),下同 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); //寫數(shù)據(jù),,下同 _file.WriteString("CONFIG", "DataBits", G_DATABITS); _file.WriteString("CONFIG", "StopBits", G_STOP); _file.WriteString("CONFIG", "G_PARITY", G_PARITY); private static IniFile _file;//內(nèi)置了一個對象 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聲明成了內(nèi)置對象,可以方便各函數(shù)的調(diào)用,。 下面是“保存設(shè)置”的部分代碼: [csharp] view plaincopyprint? private void btnSave_Click(object sender, EventArgs e) //設(shè)置各“串口設(shè)置” 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+""; //數(shù)據(jù)位 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(); //保存設(shè)置 讀取ini文件主要在加載窗體時執(zhí)行: INIFILE.Profile.LoadProfile();//加載所有 七、控制文本輸入這里倒挺簡單,,只是注意一點,。當(dāng)我們控制輸入非法字符時,可通過控制e.Handed的屬性值實現(xiàn),,注意這里的Handed屬性是“操作過”的含義,,而非“執(zhí)行此處操作”之意,Handled是過去式,看字面意思,"操作過的=是,;",,將這個操作的狀態(tài)設(shè)為已處理過,自然就不會再處理了。具體參見MSDN:Handed [csharp] view plaincopyprint? 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 八,、定時發(fā)送信息 這邊看似很簡單,,但也有一點需要注意,當(dāng)定時器生效時,,我們要間隔訪問“發(fā)送”按鍵的內(nèi)容,,怎么實現(xiàn)?還好MS給我們提供了必要的支持,使用Button的 PerformClick可以輕松做到,, PerformClick參見MSDN:PerformClick [csharp] view plaincopyprint? private void tmSend_Tick(object sender, EventArgs e) //轉(zhuǎn)換時間間隔 string strSecond = txtSecond.Text; try int isecond = int.Parse(strSecond) * 1000;//Interval以微秒為單位 tmSend.Interval = isecond; if (tmSend.Enabled == true) btnSend.PerformClick(); //產(chǎn)生“發(fā)送”的click事件 catch (System.Exception ex) MessageBox.Show("錯誤的定時輸入,!", "Error"); 注意在一些情況下不要忘了讓定時器失效,如在取消“定時發(fā)送數(shù)據(jù)"和“關(guān)閉串口”時等,。 好了,,主要內(nèi)容就是這些,希望以上內(nèi)容對大家有所幫助,,如你有好的想法,,還請不吝賜教! |
|