C#串口编程
串行口Serial Port是早期计算机机箱的标配,当时后部往往有DB9接口,使用RS-232标准。不过随着计算机的发展,现在的机箱往往不再设置DB9接口,但因为这种数据传输方式简单有效,符合这种异步通信协议的产品还被广泛使用,特别是在工业领域及嵌入式系统中,为此出现了USB-uart模块以实现计算机与具有串口的系统之间的转换通信。
使用微软的Visual Studio提供的免费社区版,界面组件中就有Serial Port,使用时可以将其放入编程界面中,一般为this.serialPort1,然后就可以使用此组件实现串口设备之间的通信。
1)串口设备搜索:
在计算机的USB接口带电插入USB-uart模块后,计算机设备中会出现一个虚拟的COM口,具体序号由计算机与模块之间的协议来配置,可以使用C#编程来查询到。
因为计算机中可能存在多个自带的COM口或USB-uart模块实现的COM口,一般界面中加入一个下拉列表来显示,方便用户选择其一来进行通信,这里将下拉列表属性的name设为cbbCOMPorts。搜索计算机中COM口并加入下拉列表中的函数为:
代码中,使用串行口SerialPort类的GetPortNames()方法来获取计算机中的所有串口,得到的是一个字符串数组,将其赋值给portNames数组变量。
如果不是第一次打开界面,下拉列表cbbCOMPorts中有可能存在之前存入的内容,需要先使用Clear()方法将其清空。然后使用for循环对portNames数组进行遍历,分别加入下拉列表cbbCOMPorts中成为显示的各个项目。
下拉列表中各项需要在鼠标点击时才能显示出来,选择后在界面中显示的项目为cbbCOMPorts.SelectedItem,此时为空。为了方便,可将最上面一项直接显示在界面中,即将此项赋值给SelectedItem。当然只有在已经搜索到不止一个串口情况下才会赋值,如果计算机中没有搜索到串口就不能赋值。
可将上述函数加入Form1_Load()方法的代码中,使其在界面加载情况时就能运行。因为使用USB-uart模块实现的串口会动态变化,因此也可以把此函数加入cbbCOMPorts下拉列表的Click事件处理中,点击就会更新。
2)连接串口设备:
一般在界面中会放入一个“连接”按钮,将与选择的串口连接的代码放入Click事件处理函数中。
为了方便用户使用时看到相关提示,界面中加入了lblMessage只读文本框,用于输出信息。
代码中首先检查下拉列表中是否已经有选择的串口,然后检查串口是否已经被占用,因为计算机中有可能有多个程序在使用串口,这个检查是很有必要的。
因为使用串口通信有可能出现异常,因此下面的打开串口代码需要使用try-catch结构,如果异常就出现一个提示窗口,提醒用户做相应处理。
串口使用异步通信方式,现在一般使用的是全双工三线连接,必须要设置的参数有数据位、校验位、停止位和波特率,其他一些设置可以使用默认值。参数设置后就可以使用Open()方法来打开。
3)通过串口发送数据:
串口通信是一个字节一个字节来传输的,这里使用的是串口的Write(byData, start, length)方法,其中byData为byte类型的数组,start为数组的起始索引,length为一帧要传输的字节数。
因此,需要把待发送的数据先转换成字节数组,比如将十六进制数据字符串转为字节数组可以使用:
如果要发送的数据是普通的字符串,也可以使用Encoding.Default.GetBytes(str)来得到字节数组,或者直接使用串口的Write()或WriteLine()方法直接发送字符串,而不用先转换为字节数组。也就是要根据待发送内容的编码情况来确定。
4)通过串口接收数据:
串口数据接收一般是使用事件处理方法,先要在界面加载Form1_Load()的代码中加入事件:
上述代码中注册了串口数据接收事件处理函数DataReceived,其中的代码为:
代码中使用的主要方法是Read(tmpBuf, 0, count),其中count为串口得到的数据字节数,可以通过串口的BytesToRead属性获得,得到此数值后就可以创建相应长度的字节数组tmpBuf,将串口得到的数据暂存入此字节数组中以便进行后续处理。
但只是使用串口的Read()方法,实际得到的数据长度长短不一,使后续处理变得困难。串口数据虽然是按字节传输的,但多个字节一般是组成一个数据帧,单片机中往往都有检测数据帧的idle标志,以便把一帧数据合在一起进行处理。但在计算机中,并没有相应的帧标志,因此需要使用软件方式探测这种成帧的数据,不然读入的数据格式就会很混乱。
为此,代码中使用了一个死循环,实际上根据判断给出了出口。串口数据帧结束,其实就是在一定时间间隔内不再收到新的数据,一般这个间隔为2~3个数据字节的传输时间,如果使用115200波特率,每个数据字节传输大约需要1ms,因此代码中使用了1ms的延迟。
因为C#数组是固定长度的,不能更改,为了存储多次读取的数据来组成完整一帧,使用了List<byte>泛型列表。如果读到一组数据,就加入泛型列表中,如果没有读到就延迟1ms再读,反复循环。但如果连续几次循环都未能读到数据,就认为一帧数据结束,将泛型列表中的数据转换为字节数组,然后使用委托输出到界面中。
为了得到比较恰当的帧结束判断的延迟时间,在循环中设置了两个变量,idle和th,idle用来计数未读到数据的循环次数,th则为设置的次数门限。通过调整这两个参数,就可以在一定的波特率情况下取得较好的值,以组成一个完整的数据帧。
最终获取的串口接收数据需要在界面中显示处理,但因为串口数据接收的处理线程与界面显示的线程不同,需要用委托方法来传递数据,具体代码在后面的页面介绍。
串口接收数据也可以使用serialPort.ReadExisting()方法,这时直接得到的是转换后的字符串。
5)关闭串口:
串口传输数据完成,需要关闭串口,使用Close()方法。为了对可能出现的异常进行处理,也需要使用try-catch的代码结构。而且,关闭串口也需要加入到界面窗口的Form1_Form_Closing()事件处理代码中,避免界面关闭后串口还被占用,影响以后的使用。
使用微软的Visual Studio提供的免费社区版,界面组件中就有Serial Port,使用时可以将其放入编程界面中,一般为this.serialPort1,然后就可以使用此组件实现串口设备之间的通信。
1)串口设备搜索:
在计算机的USB接口带电插入USB-uart模块后,计算机设备中会出现一个虚拟的COM口,具体序号由计算机与模块之间的协议来配置,可以使用C#编程来查询到。
因为计算机中可能存在多个自带的COM口或USB-uart模块实现的COM口,一般界面中加入一个下拉列表来显示,方便用户选择其一来进行通信,这里将下拉列表属性的name设为cbbCOMPorts。搜索计算机中COM口并加入下拉列表中的函数为:
private void Search_serialport()
{
string[] portNames = System.IO.Ports.SerialPort.GetPortNames();
cbbCOMPorts.Items.Clear();
for (int i = 0; i <= portNames.Length - 1; i++)
{
cbbCOMPorts.Items.Add(portNames[i]);
}
if(portNames.Length>0) cbbCOMPorts.SelectedItem = cbbCOMPorts.Items[0];
}
代码中,使用串行口SerialPort类的GetPortNames()方法来获取计算机中的所有串口,得到的是一个字符串数组,将其赋值给portNames数组变量。
如果不是第一次打开界面,下拉列表cbbCOMPorts中有可能存在之前存入的内容,需要先使用Clear()方法将其清空。然后使用for循环对portNames数组进行遍历,分别加入下拉列表cbbCOMPorts中成为显示的各个项目。
下拉列表中各项需要在鼠标点击时才能显示出来,选择后在界面中显示的项目为cbbCOMPorts.SelectedItem,此时为空。为了方便,可将最上面一项直接显示在界面中,即将此项赋值给SelectedItem。当然只有在已经搜索到不止一个串口情况下才会赋值,如果计算机中没有搜索到串口就不能赋值。
可将上述函数加入Form1_Load()方法的代码中,使其在界面加载情况时就能运行。因为使用USB-uart模块实现的串口会动态变化,因此也可以把此函数加入cbbCOMPorts下拉列表的Click事件处理中,点击就会更新。
2)连接串口设备:
一般在界面中会放入一个“连接”按钮,将与选择的串口连接的代码放入Click事件处理函数中。
private void btnConnect_Click(object sender, EventArgs e)
{
if (cbbCOMPorts.Text != string.Empty)
{
if (this.serialPort1.IsOpen)
{
lblMessage.Text = cbbCOMPorts.Text + " 已打开,需要先关闭";
}
else
{
try
{
this.serialPort1.PortName = cbbCOMPorts.Text;
this.serialPort1.BaudRate = 115200;
this.serialPort1.Parity = System.IO.Ports.Parity.None;
this.serialPort1.DataBits = 8;
this.serialPort1.StopBits = System.IO.Ports.StopBits.One;
this.serialPort1.Open();
lblMessage.Text = cbbCOMPorts.Text + " 已连接";
}
catch (Exception ex)
{
MessageBox.Show(cbbCOMPorts.Text + " 无法打开,请检查是否已被占用或更改");
}
}
}
}
为了方便用户使用时看到相关提示,界面中加入了lblMessage只读文本框,用于输出信息。
代码中首先检查下拉列表中是否已经有选择的串口,然后检查串口是否已经被占用,因为计算机中有可能有多个程序在使用串口,这个检查是很有必要的。
因为使用串口通信有可能出现异常,因此下面的打开串口代码需要使用try-catch结构,如果异常就出现一个提示窗口,提醒用户做相应处理。
串口使用异步通信方式,现在一般使用的是全双工三线连接,必须要设置的参数有数据位、校验位、停止位和波特率,其他一些设置可以使用默认值。参数设置后就可以使用Open()方法来打开。
3)通过串口发送数据:
串口通信是一个字节一个字节来传输的,这里使用的是串口的Write(byData, start, length)方法,其中byData为byte类型的数组,start为数组的起始索引,length为一帧要传输的字节数。
因此,需要把待发送的数据先转换成字节数组,比如将十六进制数据字符串转为字节数组可以使用:
byte[] bytes = new byte[s.Length / 2];
for (int i = 0; i < bytes.Length; i++)
{
bytes[i] = (byte)Convert.ToByte(s.Substring(i * 2, 2), 16);
}
如果要发送的数据是普通的字符串,也可以使用Encoding.Default.GetBytes(str)来得到字节数组,或者直接使用串口的Write()或WriteLine()方法直接发送字符串,而不用先转换为字节数组。也就是要根据待发送内容的编码情况来确定。
4)通过串口接收数据:
串口数据接收一般是使用事件处理方法,先要在界面加载Form1_Load()的代码中加入事件:
this.serialPort1.DataReceived += new System.IO.Ports.SerialDataReceivedEventHandler(DataReceived);
上述代码中注册了串口数据接收事件处理函数DataReceived,其中的代码为:
private void DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
int count = 0;
int idle = 0;
int th = 3;
List<byte> rcvList = new List<byte>();
while(true)
{
count = this.serialPort1.BytesToRead;
if(count > 0)
{
byte[] tmpBuf = new byte[count];
this.serialPort1.Read(tmpBuf, 0, count);
rcvList.AddRange(tmpBuf);
idle = 0;
}
else
{
idle++;
if (idle > th)
{
idle = 0;
break;
}
System.Threading.Thread.Sleep(1); //毫秒
}
}
byte[] rcvBuf = rcvList.ToArray();
txtDataReceived.BeginInvoke(new delegateParam(UpdateTextBox), rcvBuf);
}
代码中使用的主要方法是Read(tmpBuf, 0, count),其中count为串口得到的数据字节数,可以通过串口的BytesToRead属性获得,得到此数值后就可以创建相应长度的字节数组tmpBuf,将串口得到的数据暂存入此字节数组中以便进行后续处理。
但只是使用串口的Read()方法,实际得到的数据长度长短不一,使后续处理变得困难。串口数据虽然是按字节传输的,但多个字节一般是组成一个数据帧,单片机中往往都有检测数据帧的idle标志,以便把一帧数据合在一起进行处理。但在计算机中,并没有相应的帧标志,因此需要使用软件方式探测这种成帧的数据,不然读入的数据格式就会很混乱。
为此,代码中使用了一个死循环,实际上根据判断给出了出口。串口数据帧结束,其实就是在一定时间间隔内不再收到新的数据,一般这个间隔为2~3个数据字节的传输时间,如果使用115200波特率,每个数据字节传输大约需要1ms,因此代码中使用了1ms的延迟。
因为C#数组是固定长度的,不能更改,为了存储多次读取的数据来组成完整一帧,使用了List<byte>泛型列表。如果读到一组数据,就加入泛型列表中,如果没有读到就延迟1ms再读,反复循环。但如果连续几次循环都未能读到数据,就认为一帧数据结束,将泛型列表中的数据转换为字节数组,然后使用委托输出到界面中。
为了得到比较恰当的帧结束判断的延迟时间,在循环中设置了两个变量,idle和th,idle用来计数未读到数据的循环次数,th则为设置的次数门限。通过调整这两个参数,就可以在一定的波特率情况下取得较好的值,以组成一个完整的数据帧。
最终获取的串口接收数据需要在界面中显示处理,但因为串口数据接收的处理线程与界面显示的线程不同,需要用委托方法来传递数据,具体代码在后面的页面介绍。
串口接收数据也可以使用serialPort.ReadExisting()方法,这时直接得到的是转换后的字符串。
5)关闭串口:
串口传输数据完成,需要关闭串口,使用Close()方法。为了对可能出现的异常进行处理,也需要使用try-catch的代码结构。而且,关闭串口也需要加入到界面窗口的Form1_Form_Closing()事件处理代码中,避免界面关闭后串口还被占用,影响以后的使用。