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