最近在公司让用C#写一个串口调试的工具,要求向串口中输入16进制数据或字符串。因为我刚到公司,并且对C#也不是很熟悉,针对硬件编程更是从来没接触过,确实耗掉了一些时间。好在一切都可以慢慢来,通过网上查资料,几天工作下来,还是小有成就。下面我就将这次遇到的问题和解决方法奉献出来,目的是和同行交流,回馈网友们提供的帮助,也是为了自己对知识加深一下巩固。
说了一大通废话之后,我们来看具体的实现步骤。
公司要求实现以下几个功能:
1):实现两台计算机之前的串口通信,以16进制形式和字符串两种形式传送和接收。
2):根据需要设置串口通信的必要参数。
3):定时发送数据。
4):保存串口设置。
看着好像挺复杂,其实都是纸老虎,一戳就破,前提是你敢去戳。我尽量讲的详细一些,争取说到每个知识点。
在编写程序前,需要将你要测试的COM口短接,就是收发信息都在本地计算机,短接的方式是将COM口的2、3号针接起来。COM口各针的具体作用,度娘是这么说的:COM口。记住2、3针连接一定要连接牢固,我就是因为接触不良,导致本身就不通,白白花掉了一大半天时间调试代码。
下面给出主要的操作界面,如下:
顺便,我将所有控件对应的代码名字也附上了,相信对初学者来说,再看下面的代码会轻松很多。控件名字命名的方法是“控件名+作用”的形式,例如“打开串口”的开关按钮,其名字是btnSwitch (btn就是button的简写了)。我认为这种命名控件的方式比较好,建议大家使用,如果你有好的命名方式,希望你能告诉我!
下面我们将各个功能按照从主到次的顺序逐个实现。(我分块给出代码实现,详细代码见链接:《C#串口通信工具》)
一、获取计算机的COM口总个数,将它们列为控件cbSerial的候选项,并将第一个设为cbSerial的默认选项。
这部分是在窗体加载时完成的。请看代码:
(很多信息代码的注释里讲的很清楚,我就不赘述了。)
[csharp] view plaincopy
- //检查是否含有串口
- 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; //设置<span style="font-size:18px; "><strong>cbSerial的默认选项</strong></span>
二、“串口设置”
这面我没代码编程,直接从窗体上按照串口信息设置就行。我们仅设置它们的默认选项,但这里我用到了ini文件,暂时不讲,我们先以下面形式设置默认。
[csharp] view plaincopy
- cbBaudRate.SelectedIndex = 5;
- cbDataBits.SelectedIndex = 3;
- cbStop.SelectedIndex = 0;
- cbParity.SelectedIndex = 0;
- radio1.Checked = true; //发送数据的“16进制”单选按钮(这里我忘了改名,现在看着很不舒服!)
- rbRcvStr.Checked = true;
三、打开串口
在发送信息之前,我们需要根据选中的选项设置串口信息,并设置一些控件的属性,最后将串口打开。
[csharp] view plaincopy
- private void btnSwitch_Click(object sender, EventArgs e)
- {
- <span style="white-space:pre"> </span>//sp1是全局变量。 SerialPort sp1 = new SerialPort();
- if (!sp1.IsOpen)
- {
- try
- {
- //设置串口号
- string serialName = cbSerial.SelectedItem.ToString();
- sp1.PortName = serialName;
- //设置各“串口设置”
- 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; //数据位
- 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:参数不正确!", "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:参数不正确!", "Error");
- break;
- }
- if (sp1.IsOpen == true)//如果打开状态,则先关闭一下
- {
- sp1.Close();
- }
- //状态栏设置
- tsSpNum.Text = "串口号:" + sp1.PortName + "|";
- tsBaudRate.Text = "波特率:" + sp1.BaudRate + "|";
- tsDataBits.Text = "数据位:" + sp1.DataBits + "|";
- tsStopBits.Text = "停止位:" + sp1.StopBits + "|";
- tsParity.Text = "校验位:" + sp1.Parity + "|";
- //设置必要控件不可用
- cbSerial.Enabled = false;
- cbBaudRate.Enabled = false;
- cbDataBits.Enabled = false;
- cbStop.Enabled = false;
- cbParity.Enabled = false;
- sp1.Open(); //打开串口
- btnSwitch.Text = "关闭串口";
- }
- catch (System.Exception ex)
- {
- MessageBox.Show("Error:" + ex.Message, "Error");
- return;
- }
- }
- else
- {
- //状态栏设置
- tsSpNum.Text = "串口号:未指定|";
- tsBaudRate.Text = "波特率:未指定|";
- tsDataBits.Text = "数据位:未指定|";
- tsStopBits.Text = "停止位:未指定|";
- tsParity.Text = "校验位:未指定|";
- //恢复控件功能
- //设置必要控件不可用
- cbSerial.Enabled = true;
- cbBaudRate.Enabled = true;
- cbDataBits.Enabled = true;
- cbStop.Enabled = true;
- cbParity.Enabled = true;
- sp1.Close(); //关闭串口
- btnSwitch.Text = "打开串口";
- }
- }
四、发送信息
因为这里涉及到字符的转换,难点在于,在发送16进制数据时,如何将文本框中的字符数据在内存中以同样的形式表现出来,例如我们输入16进制的“eb 90”显示到内存中,也就是如下形式:
或输入我们想要的任何字节,如上面的“12 34 56 78 90”.
内存中的数据时16进制显示的,而我们输入的数据时字符串,我们需要将字符串转换为对应的16进制数据,然后将这个16进制数据转换为字节数据,用到的主要方法是:
Convert.ToInt32 (String, Int32);
Convert.ToByte (Int32);
这是我想到的,如果你有好的方法,希望你能告诉我。
下面看代码:
[csharp] view plaincopy
- 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(‘ ‘);
- <span style="white-space:pre"> </span>//strArray数组中会出现“”空字符的情况,影响下面的赋值操作,故将<span style="rgb(255, 255, 255); ">byteBufferLength相应减小</span>
- int byteBufferLength = strArray.Length;
- for (int i = 0; i <<span style="background-color: rgb(255, 255, 255); ">strArray.Length</span><span style="background-color: rgb(255, 255, 255); ">; i++ )</span>
- {
- if (strArray[i]=="")
- {
- byteBufferLength--;
- }
- }
- byte[] byteBuffer = new byte[byteBufferLength];
- int ii = 0;<span style="white-space:pre"> </span>//用于给<span style="rgb(255, 255, 255); ">byteBuffer赋值</span>
- 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 //防止输错,使其只能输入一个字节的字符,即只能在txtSend里输入 “eb 90”等字符串,不能输入“123 2345”等超出字节范围的数字
- {
- byteBuffer[ii] = Convert.ToByte(decNum);
- }
- catch (System.Exception ex)
- {
- MessageBox.Show("字节越界,请逐个字节输入!", "Error");
- return;
- }
- ii++;
- }
- sp1.Write(byteBuffer, 0, byteBuffer.Length);
- }
- else //以字符串形式发送时
- {
- sp1.WriteLine(txtSend.Text); //写入数据
- }
- }
五、数据的接收
亮点来了,看到这里,如果你还没吐(可能是我的代码比较拙劣!),那么下面的知识点对你也不成问题。
这里需要用到 委托 的知识,我是搞C/C++出身,刚碰到这个知识点还真有点不适应。为了不偏离主题,关于委托,我仅给出两条比较好的链接,需要的网友可以去加深学习:C#委托、订阅委托事件。
在窗体加载时就订阅上委托是比较好的,所以在Form1_Load中添加以下代码:
[csharp] view plaincopy
- Control.CheckForIllegalCrossThreadCalls = false; //意图见解释
- sp1.DataReceived += new SerialDataReceivedEventHandler(sp1_DataReceived); //订阅委托
注意,因为自.net 2.0以后加强了安全机制,,不允许在winform中直接跨线程(事件触发需要产生一个线程处理)访问控件的属性,第一条代码的意图是说在这个类中我们强制不检查跨线程的调用是否合法。处理这种问题的解决方案有很多,具体可参阅以下内容:解决方案。
好了,订阅委托之后,我们就可以处理接收数据的事件了。
[csharp] view plaincopy
- 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("请打开某个串口", "错误提示");
- }
- }
为了友好和美观,我将当前时间也显示出来,又将显示字体的颜色做了修改:
[csharp] view plaincopy
- <span style="white-space:pre"> </span>//输出当前时间
- DateTime dt = DateTime.Now;
- txtReceive.Text += dt.GetDateTimeFormats(‘f‘)[0].ToString() + "\r\n";
- txtReceive.SelectAll();
- txtReceive.SelectionColor = Color.Blue; //改变字体的颜色
做到这里,大部分功能就已实现了,剩下的工作就是些简单的操作设置了,有保存设置、定时发送信息、控制文本框输入内容等。
六、保存设置
这部分相对简单,但当时我没接触过,也花了点时间,现在想想,也不过如此。
保存用户设置用ini文件是个不错的选择,虽然大部分都用注册表实现,但ini文件保存还是有比较广泛的使用。
.ini 文件是Initialization File的缩写,也就是初始化文件。
为了不偏离正题,也不过多说明,可参考相关内容(网上资源都不错,因人而异,就不加链接了)。
使用Inifile读写ini文件,这里我用到了两个主要方法:
[csharp] view plaincopy
- //读出ini文件
- a:=inifile.Readstring(‘节点‘,‘关键字‘,缺省值);// string类型
- b:=inifile.Readinteger(‘节点‘,‘关键字‘,缺省值);// integer类型
- c:=inifile.Readbool(‘节点‘,‘关键字‘,缺省值);// boolean类型
- 其中[缺省值]为该INI文件不存在该关键字时返回的缺省值。
- //写入INI文件:
- inifile.writestring(‘节点‘,‘关键字‘,变量或字符串值);
- inifile.writeinteger(‘节点‘,‘关键字‘,变量或整型值);
- inifile.writebool(‘节点‘,‘关键字‘,变量或True或False);
请看代码:
[csharp] view plaincopy
- //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声明成了内置对象,可以方便各函数的调用。
下面是“保存设置”的部分代码:
[csharp] view plaincopy
- 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
[csharp] view plaincopy
- 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
[csharp] view plaincopy
- 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");
- }
- }
注意在一些情况下不要忘了让定时器失效,如在取消“定时发送数据"和“关闭串口”时等。
好了,主要内容就是这些,希望以上内容对大家有所帮助,如你有好的想法,还请不吝赐教!