单片机串口通讯的进阶

串口接收中断断帧是很重要的,那么该如何判断一帧数据是否接受完成呢?常用的两种方法是:

时间断帧,我们可以通过在定时器里对两帧数据的时间进行比较,一帧数据里边两个数据的时间差是很短的,如果超过一定时间之后仍旧无心的数据被接收到,那么我们就可以判断为一帧数据结束。

协议断帧,这就好比我么两个人约定好数据大致长度,或某个字符出现即为一帧数据的结束,由此可以来进行断帧,判断为一帧数据的接收完成。

实现串口数据帧断帧,有很多的方法,比如使用串口的IDLE中断进行断帧,使用定时器根据时间断帧、使用特殊标识符进行断帧等等,刚开始工作的时候,我自己就写好几个版本,基本上一个项目一个版本,但是现在我使用的基本上只有这一个版本。

空闲中断断帧

使用串口的IDLE中断:比如STM32有串口的IDLE中断,在数据发送完成,串口进入空闲的时候会产生一个IDLE中断,在HAL中也使用到这种方式。但是,其他的单片机不一定有IDLE中断,比如51单片机、MSP430等等,所以这个方式并不是万能的,.方法二:使用特殊字符:给数据包的帧头和帧尾使用一个或者一组特定的字符,也就是我们说的帧头和帧尾。这个有一定的概率会造成数据包错位或者数据包内容和帧头尾内容一样。如果数据帧帧尾丢失,着会这造成单片机一直等待数据帧帧尾,进入死等情况。方法三:使用串口+定时的方式进行断帧。定时器主要用来计时数据帧与帧之间的时间,这要这个时间大于一个设定的阈值,超出这个时间就表示当前数据帧发送完毕,下一次来的数据,一定是一个新的数据帧。本次主要对方法三做一个说明。这也是我在项目中常用一种方式,这种方式可以适用于串口、RS485、RS422等通讯接口,其带有循环缓存队列的思想用在CAN总线通讯中也十分合适(CAN通讯不需要自己去判断报文的帧头帧尾),这样的通讯设置方式能够极大的降低数据掉包率。

使用资源串口 1个定时器 1个

实现基本原理采用循环队列,将串口的数据流进行接收,根据定时器计时单个BYTE之间的时间,只要这个时间超过一个阈值,则认为下一个数据为新的数据帧开头。(使用循环缓存队列进行数据接收、采用定时器进行数据断帧),数据接收采用的是单字节中断接收方式。在查串口的中断函数中,只进行数据接收,不对数据进行处理。在主函数中,才对数据帧的合法性进行判断和数据帧内容进行处理和应答。

特点采用循环缓存数据队列,有效的降低数据掉包风险适用于串行通讯要求发送数据必须连续,且帧与帧之间需要有一定的时间间隔。数据帧可以实现任意长度采用生产消费者模型的设计思路


下面我就举个自己设计的项目例子吧。本次使用STM32F103RCT6单片机,当然我也将这种方式用到过MSP430、8051、NRF24LE1等单片机中。

数据帧规定一个完整的数据包由开始字符、设备类型码、设备地址码、命令字、数据长度、数据区、CRC校验及结束字符组成。如下所示:

各区域含义如下:开始字符:1字节,表示一个数据包的开始。固定为0xAA。设备类型:1字节,表示设备类型,固定为0x01。所有设备都能接收广播类型(0xFF)的命令请求设备地址:1字节,表示设备在系统中的地址,由用户可以自行定义,默认值为0x01。所有设备都能接收广播地址(0xFF)的命令请求。命令字:1字节,即功能命令。见下文,按照规定的格式访问设备。数据长度:1字节,表示后面跟随的有效数据区的字节数,范围0  -  240。数据区:N字节,为有效数据,长度为前面数据长度定义的字节数。数据长度为0时没有数据区。数据区的数值,参见命令描述。CRC校验:2字节,从开始字符到数据区最后一个字节的所有字符的16位CRC校验值。校验值低位再前,高位在后。结束字符:1字节,表示数据包结束。固定为0x0E。

数据帧断帧时间阈值在发送数据包的时候,要求先准备好所有要发送的字符,连续发送出去,中间不得有较长时间停顿。多个数据包间必须有3.5个BYTE传送时间的时间间隔。例: RS232传输位如下:1 起始位8 数据位,首先发送最低有效位0 位作为奇偶校验1 停止位

计算时间:T=3.5*( 1+数据位+奇偶校验+停止位) / 波特率如:使用9600,则间隔时间为4ms。在波特率大于19200的时候,使用固定的时间1.75ms

注意:既然定义了这样的时间间隔,那么就必须要要求发送端发过来的数据帧也要满足这个时间间隔的标准。

eg:约定总线的波特率为115200,如果发送端需要发送3个数据帧的时候,那么就必须要要求这个三个数据帧与帧的时间间隔必须要要大于1.75MS,否者接收端就会把这三个数据帧认为是一个数据帧。

程序代码实现首先需要对串口初始化:将单片机的串口配制成实际要求的串口波特率,和通讯格式,我这里默认配置为115200,8,N,1的方式。串口要打开接收中断,每接收一个BYTE数据,就要进入中断一次。定时器也需要进行初始化,将定时的时间间隔为50uS,并且启动定时器,让定时器间隔50uS进入中断函数一次。串口与定时器初始化的代码我就不公布了,这是单片机开发人员的常规操作。而且每款单片机的初始化的方式不一样。

接下来就进入主题了,在进入主题之前,先介绍下我所定义的一些结构体类型,不然后面看起来会相当的吃力:首先,定义了一个T_FramCtlType的数据结构体。这个结构体用来记录帧与帧之间的时间间隔。主要是用在定时器的中断函数中,这里大家有个相关的映象即可:

然后我定义了一个消息和循环缓存的数据结构体:

ComMsgType单个数据帧的最大长度,我这里定义为为250个字节,大家在使用的时候,可以根据自己的协议设计要求,可以自行定义。MsgFifoType是串口消息的循环缓存队列和帧时间的一个结构体,可以缓存10个数据帧,后面我们所有的操作都是围绕着这个结构体进行操作。接下来是协议相关的,我这里宏定义了数据帧的帧头,帧尾和数据帧类容的结构体。

下面开始看看代码的编写,首先是结构体数据的初始化。

注意:COMMsgFifo[_COMx]这个是我为了适配多路串口同时使用,所以定义了多个MsgFifoType类型的缓存,一路串口一个COMMsgFifo缓存池。所以这里需要稍微把思路转换下。紧接着就是BandIndex的值更改。这个值主要为了定义我们帧时间间隔阈值。

比如,我这里使用的是115200的波特率,那么BandIndex的值为6.后面进行断帧的时候只需要根据这个值去查找Tim3_5ByteTab数值,确定出断帧阈值。

一切准备就绪的时候,就可以开始接收数据了,当串口中断接收到一个BYTE的时候,会进入串口中断函数。

中断函数就只干了一件事行,把接收到的数据dat通过Pro_ComDataSaveToBuf()函数写入到缓存中。

在Pro_ComDataSaveToBuf()函数中,首先要使能T_FramCtl结构体中的超时检测机制,然后对Counter清零。接下来就是将这个数据写到合适的位置,根据当前缓存数据帧的位置,以及数据帧Len的长度,同时Len要自加。COMMsgFifo[_COMx].Msg[COMMsgFifo[_COMx].AddWrite].Data[COMMsgFifo[_COMx].Msg[COMMsgFifo[_COMx].AddWrite].Len++] = _Dat;这句代码很关键,需要多花点时间进行研究。虽然这里只写了简单的基于,但是却反复的调用了MsgFifoType结构体中的成员。接下来的if语句,也仅仅是对单个数据帧的长度进行判断,避免数据帧长度超过250个自己,而造成内存越界。

到了这里,我们开始接收数据了,超时机制检测已经启动了。在看看定时器中断是怎么操作的?

定时器是按照50us周期进行中断的,里面也有其他的功能计时,但是与我们串口断帧相关的就一个函数,pro_ComFramTim50uS()。

在这个函数中,看以看到,当T_FramCtl.Enable使能之后,T_FramCtl.Counter开始自加,如果串口中断中一直有数据来,这个T_FramCtl.Counter的值会一直被清零,T_FramCtl.Counter值一直达不到阈值条件。当串口中断接收数据完毕之后,串口没有数据了,由于T_FramCtl.Counter自加,当计数达到35之后,也就是我们之间设置的1.75ms,这时就满足了if (COMMsgFifo[_COMx].T_FramCtl.Counter > Tim3_5ByteTab[COMMsgFifo[_COMx].BandIndex] )的条件。判断条件if ( COMMsgFifo[_COMx].Remain < COM_RXTX_FIFO_SIZE )是为了判断缓存池是否满了,如果没有满COMMsgFifo[_COMx].Remain才自加,其表示当前缓存数据帧的数目,另外COMMsgFifo[_COMx].AddWrite也自加一次,为下一次数组帧写的位置加1。最后还有个重要的事情COMMsgFifo[_COMx].T_FramCtl.Enable = 0关闭超时检测机制。

这样,一个数据帧就被完成的接收到数据缓存中了,生产者已经完成。后面所有的数据帧都是按照这个机制,进行数据产生;接下来就是数据消费者,数据处理了。

数据处理,我是放在主循环while(1)中

由DealUartProtocolEvent()函数进行数据处理。

在DealUartProtocolEvent()函数中,首先判读COMMsgFifo[COMx].Remain 的值,只有大于0的时候,才表示缓存队列中有数据帧需要处理。接下来数据帧需要通过pro_UartPacketAnalyse()函数进行数据帧合法性进行判断,如果数据帧合法,则将数据帧的类容传递给InstructCMD_Uart结构体(当然包含设备类型、设备地址、命令字、数据内容、数据长度等参数)。由pro_UartFrameProtocolParse()函数对数据内容和功能进行解析。这里我就不对这个函数进行展示了。最后就是循环队列的COMMsgFifo[COMx].AddRead自加,实现首位相连循环的读,同时不要忘记了COMMsgFifo[COMx].Remain减一。这样,一个数据帧在串口接收的时候就已经实现了数据断帧,后面在主函数中处理的不过是后期的数据帧处理而已。

最后的最后,在让我展示下,数据帧是如何进行合法性判断的。

大致内容,就是先判断了帧头、然后是设备类型、设备ID、数据帧长度、帧尾,以及CRC校验值。只有都通过了才将数据帧的内容传递出去,否者就传递固定的错误应答和应答代码。

实际测试:说了这么多,那就实际测试一下吧。蓝色的为发送数据帧,红色的设备应答数据帧。

错误功能码定义:

第一个数据帧 AA FF FF 23 00 0C 25 0E 为正常完整的数据帧。所以设备正常应答。第二个数据帧FF FF 22 00 9C 24 0E 缺少帧头,应答数据帧应答错误功能码2F,数据长度为1,数据内容为80(帧头错误)第三个数据帧AA FF FF 24 01 3C 27 0E 缺功能码为24,数据长度为1,但是数据区内容丢失,后面的两个字节为CRC16校验和帧尾,应答数据帧应答错误功能码2F,数据长度为1,数据内容为83(长度错误)第四个数据帧AA FF FF 25 00 AC 26 缺少帧尾,应答数据帧应答错误功能码2F,数据长度为1,数据内容为83(长度错误)

总结一下:这种断帧方式,基本上是比较通用的一种方式,我也运用很多的项目设计中,特别是针对低端的单片机,当然STM32的IDLE中断也是一个很优秀的方式。

声明:本内容为作者独立观点,不代表电子星球立场。未经允许不得转载。授权事宜与稿件投诉,请联系:editor@netbroad.com
觉得内容不错的朋友,别忘了一键三连哦!
赞 2
收藏 2
关注 202
成为作者 赚取收益
全部留言
0/200
成为第一个和作者交流的人吧