基于事件触发方式的串行通信接口数据接收案例
广东职业技术学院 欧浩源
1、案例背景
之前写过一篇《基于多线程方式的串行通信接口数据接收案例》的博文,讨论了采用轮询方式接收串口数据的情况。经过使用了多线程来处理,而然轮询的办法比较还是比较笨拙的。我们在实际的项目开发中,更加常用的是基于事件触发的方式,这个方式不但好用,而且灵活,只是使用起来需要更多的一点专业知识。在本博文中,就“传感器模块每隔1秒钟向上位机传送4字节的电压数据帧”的项目,对该方法的设计进行详细的讲述。
数据帧的格式:帧头(0xAF) 电压数据高8位 电压数据低8位 帧尾(0xFA)
2、事件触发方式的工作原理
在SerialPort类中有一个DataReceived事件,当串口接收到了ReceivedBytesThreshold属性设置的字符个数或者接收到了文件结束符并将其放入了串口接收缓冲区时,就会触发DataReceived事件。
ReceivedBytesThreshold属性决定了串口读缓存中数据达到多少字节时才触发DataReceived事件,其默认值为1。如果串口接收的是固定长度的数据,则将ReceivedBytesThreshold属性设置为接收数据的长度;如果接收数据的结尾是固定的字符或字符串,则可以采用ReadTo方法或在DataReceived事件中判断接收的字符是否满足条件。
由于DataReceived事件在辅线程中被触发,不能与主线程中的数据显示控件直接进行数据传输,必须使用间接方式来实现。当收到完整的一条数据时,返回主线程处理或在主窗体上显示时,要使用跨线程的处理方式,在C#中可以采用控件异步委托的方法BeginInvoke或者控件同步委托的方法Invoke。
3、引入命令空间
使用多线程的方式,需要引入命名空间:System.Threading;
使用串行通信接口,需要引入命名空间:System.IO.Ports;
4、初始化工作
给主窗体添加窗体装载事件(即Load事件),在该事件中对各个控件的属性进行初始化工作。
重点:在这里要给DataReceived事件添加一个委托,将事件与数据接收处理方法DataReceivedHandler关联起来。
com.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);
SerialPort com = new SerialPort(); //实例化一个串口对象 private void Form2_Load(object sender, EventArgs e) { string[] ports = { "COM1", "COM2", "COM3", "COM4", "COM5" }; foreach (string str in ports) { comboBox1.Items.Add(str); } comboBox1.SelectedIndex = 2; string[] baudrate = { "2400", "4800", "9600", "19200", "57600", "115200" }; foreach (string str in baudrate) { comboBox2.Items.Add(str); } comboBox2.SelectedIndex = 2; comboBox3.Items.Add("6"); comboBox3.Items.Add("7"); comboBox3.Items.Add("8"); comboBox3.SelectedIndex = 2; comboBox4.Items.Add("1"); comboBox4.Items.Add("1.5"); comboBox4.Items.Add("2"); comboBox4.SelectedIndex = 0; comboBox5.Items.Add("None"); comboBox5.SelectedIndex = 0; com.ReceivedBytesThreshold = 4; //设置串口接收到4个字节数据才触发DataReceived事件 //为串口DataReceived事件添加处理方法 com.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler); }
5、数据接收处理方法DataReceivedHandler
当串口接收到ReceivedBytesThreshold属性设置的字节数时,就会触发DataReceived事件,从而执行DataReceivedHandler方法。在该方法里进行对串口接收数据的分析处理等工作。如果需要在这个方法里面将接收到的数据或者对数据的处理结果显示到窗体的控件上,那么就需要进行跨线程的处理了。
注意:在本方法中采用了BeginInvoke方法来处理跨线程的问题,在这个过程中,涉及到Action委托和Lambda表达式的知识点是很常用的,但在这里就不展开叙述了,大家可以百度查找学习。
private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e) { string strRcv = ""; int count = com.BytesToRead; //获取串口缓冲器的字节数 byte[] readBuffer = new byte[count]; //实例化接收串口数据的数组 com.Read(readBuffer, 0, count); //从串口缓冲区读出数据到数组 for (int i = 0; i < readBuffer.Length; i++) { strRcv += readBuffer[i].ToString("X2") + " "; //16进制显示 } this.BeginInvoke(new Action(() => { textBox1.AppendText(strRcv); })); if (readBuffer[0] == 0xAF && readBuffer[3] == 0xFA) //判断数据的帧头和帧尾 { Int32 ad = readBuffer[1]; double advalue; ad <<= 8; ad |= readBuffer[2]; //从数据帧中将电压数据取出 advalue = ad; advalue = (advalue * 3.3) / 32768; //将数据换算为实际的电压值 this.BeginInvoke(new Action(() => { label2.Text = advalue.ToString("F2") + " V"; })); } }
6、打开串口
在进行串口通信的时候,一般的流程是:先设置通信的端口号、波特率、数据位、停止位和校验位,然后打开串口,接着发送数据和接收数据,最后要关闭串口。
private void button1_Click(object sender, EventArgs e) { if (button1.Text == "打开串口") { com.PortName = comboBox1.Text; //选择串口号 com.BaudRate = int.Parse(comboBox2.Text); //选择波特率 com.DataBits = int.Parse(comboBox3.Text); //选择数据位数 com.StopBits = (StopBits)int.Parse(comboBox4.Text); //选择停止位数 com.Parity = Parity.None; //选择是否奇偶校验 try { if (com.IsOpen) //判断该串口是否已打开 { com.Close(); com.Open(); } else { com.Open(); } } catch (Exception ex) { MessageBox.ReferenceEquals("错误:" + ex.Message, "串口通信"); } button1.Text = "关闭串口"; } else if (button1.Text == "关闭串口") { com.Close(); //关闭串口 button1.Text = "打开串口"; } }
7、运行结果
8、结语
关于串口数据的接收读取无非就两种方法:一是通过轮询方式实时读取串口,而是通过事件触发实现串口读取。尽管可以使用多线程处理,但是轮询方式读取串口的效率并不十分高效,因此,本人觉得还是采用事件触发方式比较好。或许有人问,为什么只写串口读取数据,不讲串口发送数据呢?嗯.......没错,串口的数据读取和数据发送是同样重要的,但是数据读取的处理比数据发送要复杂很多,我想,如果能把串口的数据读取搞明白了,那串口数据发送还成问题吗?