上篇博客中,给大家介绍了在串口通信中的线程应用,解决接收数据的问题。但是在最后给大家提了3个问题。今天就给大家介绍一下第一个问题的处理方法,也就是——如果返回的数据的数据长度不同,如何写Read方法中的循环来读取完整的返回数据。
当然如果您没有看我的上篇博客,这里我再单独把read方法写一下:
Private Sub Read() Try serialPort.DiscardInBuffer() Dim str As String = "" Dim buf() As Byte For i = 0 To 10'假设接收到的数据每次应为11个 Dim d As Integer d = _serialPort.ReadByte'从串口中读取一个字节的数据,如果设置了读取超时时间,在规定的时间未读到数据会触发超时错误。 str += Convert.ToString(d, 16).PadLeft(2, "0") Next buf = GetByte(str) '根据协议,处理收到的数据 Catch ex As Exception Throw ex End Try End Sub
上面的代码中重点是其中的For循环,循环的次数是根据返回数据的长度而确定的,这样保证每条返回的数据的完整性。而且Port类的ReadByte是堵塞调用的,在规定的时间内未读到数据会触发超时错误,这也就要求我们必须保证每次接受的操作的循环次数必须和返回的数据长度一致。但在我们的实际应用中不可能保证我们做的系统面对的只是单一类型的返回数据。我也说过可以通过命令类型来控制Read方法中For循环的次数,只需要在Send方法中多添加一个参数(或封装一个实体,实体中多加一个属性,一个意思。)但总是有例外的情况,这个我就遇到了(我遇到的是命令的类型一样。但返回的数据根据硬件的不同返回的数据长度就不同。)
有人就会问了。如此强大的微软没有封装这样的方法吗可以每次保证接收数据的完整性。这个我当然也寻找了好长时间,以下是我对SerialPort类的一些Read方法的整理:
这两个方法的解释:
从 SerialPort 输入缓冲区中读取大量字符(字节),然后将这些字符(字节)写入到一个字符(字节)数组中指定的偏移量处。
参数:
buffer
类型:System.Char[]
将输入写入到其中的字符(字节)数组。
offset
类型:System.Int32
缓冲区数组中开始写入的偏移量。
count
类型:System.Int32
要读取的字符(字节)数。
返回值:
类型:System.Int32
读取的字符(字节)数。
总结: 其中很重要的一点就是要读取的字符(字节)数。这也需要们先知道我们需要读取的数据的长度。
这两个方法的解释:
从 SerialPort 输入缓冲区中同步读取一个字符(字节)。
总结:这两个方法都是读取缓冲区的一个字节或字符,所以我们必须制定要读取多少次,也就是必须先知道我们要读取的数据的长度.
这个方法的解释:
在编码的基础上,读取 SerialPort 对象的流和输入缓冲区中所有立即可用的字节。
总结:好像这个方法就是我们一直苦苦寻找的方法,但我们分析一下串口通信的机制,首先说我们的read方法,是在发送完数据以后马上开启Read线程,而串口读数据是需要时间的,这包括发送、硬件处理(取决于其频率!)、返回的时间,而这些时间加起来一定是比处理器处理read方法这点代码所用的时间要长的,所以往往导致ReadExisting读到的数据往往是空的(我们如果是逐句的代码调试,这个方法往往又是正确的,因为我们手动的减慢了处理器处理这些代码的时间。)
对于串口自己的监听时间DataReceive(收到数据才会触发)事件来说,好像这个方法会更有用。但是MSDN上这样说的这个方法:“不保证对接收到的每个字节引发 DataReceived 事件”。这也就说明我对于我们调用这个方法读到的数据个数,就更不能确定了。当然完全可以把收到的数据都保存起来,然后等着一定的时间收不到数据了,就作为全部收到的数据来处理。这样做的重点也就是这个“一定时间的把控”(和后面说的方法类似)但这里就不详细说了,毕竟今天的重点是我们的Read方法。
这个方法的解释;
一直读取到输入缓冲区中的 NewLine 值。( NewLine:表示行尾的值。 默认值为换行符 )
总结:这个方法很适合一个聊天的程序,我们没说完自己的一句话,需要发送的话,就点击一下回车(一个 NewLine)。但对于我们面对的硬件来说,没有一个 NewLine在那等着你用。
这个方法的解释:
一直读取到输入缓冲区中的指定 value 的字符串。
总结:这个方法和前面的几个方法有类似的问题,也就是你不可能提前知道你要读的最后一个字符是什么.(但如果你提前规定的协议中已经设定有结束的标志,这个就有可能实现)
说了这么多的方法,我没有找到合适的方法经过的的思考也就有了我下面的实现方法,希望对同样遇到我这样问题的人来说有所帮助:
Read方法:
Private Sub Read() Try Dim str As String = "" '定义临时保存返回数据的字符串变量 Dim buf() As Byte '定义保存返回数据类型为byte的变量 Dim dataCount As Integer = _serialPort.BytesToRead '定义保存缓冲区数据的变量 Dim dataCountNew As Integer = 0 '定义保存最新的需要读取的缓冲区数据的变量 Dim CountTimes As Integer = 0 '尝试从缓冲区读数据的次数 '此处是本段的核心,1000*sleep(5)表示自己定义的最长等待串口返回数据的时间 '如果第一次取得的缓冲区的需要读取的数据为0,则线程暂停5ms(当然也可以自己定,数字越小性能未必会更好。合适就行)。一旦读到数据就执行下面的代码。或者循环完毕,也不能读到数据就会抛出异常。 While (dataCount = 0 And CountTimes < 1000) Thread.Sleep(5) '线程休眠5ms dataCount = _serialPort.BytesToRead '重新取出缓冲区待读到的数据 '如果取的的数据还为0 If (dataCount <= 0) Then '读取的次数加1 CountTimes = CountTimes + 1 Continue While '继续执行循环 Else '否则退出循环 Exit While End If End While '如果等待1000*5ms=5秒以后仍然没有读到数据 If dataCount = 0 Then '抛出异常 Throw New Exception("连接分机超时,请查看分机是否启动,或检查通讯网络!!") End If '如果读到数据,首先休眠500ms(这个数据一般是根据返回的数据的次数和晶振的频率,以及数据传输的一些时间设定,如果查询数据对于时间的性能要求不高可以适当的加长),再次读取缓冲区待读数据,和刚才读到的数据进行比较,如果相等,则说明读到的数据已经完整。可以进行处理。 While True Thread.Sleep(500) dataCountNew = _serialPort.BytesToRead '重新读取缓冲区数据的个数。 '如果不等则把这次读到的缓冲区数据的个数作为最新的数据保存 If dataCount <> dataCountNew Then dataCount = dataCountNew Else '如果相同,说明数据返回完全,退出循环 Exit While End If End While For i = 0 To dataCount '这时的循环,会根据实际情况的不同而不同。 Dim d As Integer d = _serialPort.ReadByte '从串口中读取一个字节的数据,如果设置了读取超时时间,在规定的时间未读到数据会触发超时错误。 str += Convert.ToString(d, 16).PadLeft(2, "0") Next buf = GetByte(str) '将收到的数据转换为字节数组。 '根据协议,处理收到的数据 Catch ex As Exception Throw ex End Try End Sub
上面的代码中的注释已经很清楚了我也不想多做说明。
还需要说明两点: