用C#一步步寫串口通信
附言:
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#串口通信工具》
