1.HEX数据包定义
数据包的作用是把一个个单独的数据给打包起来,方便我们进行多字节的数据通信,在实际应用中,我们可能需要把多个字节打包为一个整体进行发送,比如说,我们有个陀螺仪传感器,需要用串口发送数据到STM32,陀螺仪的数据,比如X轴一个字节、Y轴一个字节、Z轴一个字节,总共3个数据,需要连续不断地发送,
当你像这样,XYZXYZX么连续发送的时候,就会出现一个问题,就是接收方,它不知道这数据哪个对应义、哪个对应Y、哪个对应Z,因为接收方可能会从任意位置开始接收,可能会出现数据错位的现象,这时候,我们就需要研究一种方式,把这个数据进行分割,把X这一批教据分割开,分成一个个数据包,这样再接受的时候就知道了,数据包的第一个数据就是义、第二个是、第三个是Z,这就是数据包的任务,就是把属于同一批的数据进行打包和分割,方便接收方进行识别,那有关分割打包的方法,可以自己发挥想象力来设计,只要逻辑行得通就行
串口数据包,通常使用的是额外添加包头包尾这种方式,如上图,那当我接收到0xFF之后,我就知道了,一个数据包来了,接着我再接收到的4个字节,就当做数据包的第1、2、3、4个数据,存在一个数组里,最后跟一个包尾,当我收到0xFE之后,就可以置一个标志位,告诉程序,我收到了一个数据包,然后新的数据包过来,再重复之前的过程,这样就可以在一个连续不断的数据流中,分割出我们想要的数据包了,这就是通过添加包头包尾实现数据分割打包的思路。
接着有几个问题:
第一个问题就是,包头包尾和数据载荷重复的问题,这里定义FF为包头,FE为包尾,如果我传输的数据本身就是F和E怎么办呢,那这个问题确实存在,如果数据和包头包尾重复,可能会引起误判,对应这个问题我们有如下几种解决方法,
- 第一种,限制载荷数据的范围,如果可以的话,我们可以在发送的时候,对数据进行限幅,比如X亿,3个数据,变化范围都可以是0~100,那就好办了,我们可以在载荷中只发送0~100的数据,这样就不会和包头包尾重复了
- 第二种,如果无法避免载荷数据和包头包尾重复,那我们就尽量使用固定长度的数据包,这样由于载荷数据是固定的,只要我们通过包头包尾对齐了数据,我们就可以严格知道,哪个数据应该是包头包尾,哪个数据应该是载荷数据,在接收载荷数据的时候,我们并不会判断它是否是包头包尾,而在接收包头包尾的时候,我们会判断它是不是确实是包头包尾,用于数据对齐,这样,在经过几个数据包的对齐之后,剩下的数据包应该就不会出现问题了
- 第三种,就是增加包头包尾的数量,并且让它尽量呈现出载荷数据出现不了的状态,比如我们使用F、FE作为包头,FD、FC作为包尾,这样也可以避免载荷数据和包头包尾重复的情况发生
第二个问题是,这个包头包尾并不是全部都需要的,比如我们可以只要一个包头,把包尾删掉,这样数据包的格式就是,一个包头所,加4个数据,这样也是可以的,当检测到F,开始接收,收够4个字节后,置标志位,一个数据包接收完成,这样也可以,不过这样的话,载荷和包头重复的问题会更严重一些,比如最严重的情况下,我载荷全是FF,包头也是FF,那你肯定不知道哪个是包头了,而加上了E作为包尾,无论数据怎么变化,都是可以分辨出包头包尾的
第三个问题,就是固定包长和可变包长的选择问题,对应HEX数据包来说,如果你的载荷会出现和包头包尾重复的情况,那就最好选择固定包长,这样可以避免接收错误,如果你又会重复,又选择可变包长,那数据很容易就乱套了,如果载荷不会和包头包尾重复,那数据可以选择可变长度,
最后一个问题,就是各种数据转换为字节流的问题,这里数据包都是一个字节一个字节组成的,如果你想发送16位的整型数据、32位的整型数据,float、double,甚至是结构体,其实都没问题,因为它们内部其实都是由一个字节一个字节组成的,只需要用一个uint8 t的指针指向它,把它们当做一个字节数组发送就行了
2.文本数据包
在HX数据包里面,数据都是以原始的字节数据本身呈现的,而在文本数据包里面,每个字节就经过了一层编码和译码,最终表现出来的,就是文本格式,但实际上,每个文本字符背后,其实都还是一个字节的HX数据
这里同样给出了固定包长和可变包长这两种模式,由于数据译码成了字符形式,这就会存在大量的字符可以作为包头包尾,可以有效避免载荷和包头包尾重复的问题,比如我这里规定的就是,以@这个字符作为包头,以以\r\n,也就是换行,这两个字符作为包尾,在载荷数据中间可以出现除了包头包尾的意字符
我们接收到载荷数据之后,得到的就是一个字符串,在软件中再对字符串进行操作和判断,就可以实现各种指令控制的功能了,而且,字符串数据包表达的意义很明显,可以把字符串数据包直接打印到串口助手上,什么指令、什么数据,一眼就能看明自,所以这个文本数据包,通常会以换行作为包尾,这样在打印的时候,就可以一行一行地显示了,非常方便
那HIX数据包和文本数据包这两种对比下来,各有优缺点,
- HEX数据包优点是,传输最直接,解标数据非常简单,比较适合一些模块发送原始的数据,比如一些使用串口通信的陀螺仪、温湿度传感器,缺点就是灵活性不足、载荷容易和包头包尾重复
- 文本数据包优点是,数据直观易理解,非常灵活,比较适合一些输入指令进行人机交互的场合,比如蓝牙模块常用的AT指令,CNC和3D打即机常用的G代码,都是文本数据包的格式,那缺点就是解析效率低
如果发送一个数100,HEX数据包就是一个字节100,完事,文本数据包就得是3个字节的字符,'1','0','0',收到之后还要把字符转换成数据,才能得到100,所以说,我们需要根据实际场景来选择和设计数据包格式
3.数据包收发流程
3.1HEX数据包接收
我们先看一下如何来接收这个固定包长的HX数据包,首先,根据之前的代码,我们知道,每收到一个字节,程序都会进一遍中断,在中断函数里,我们可以拿到这一个字节,但拿到之后,我们就得退出中断了,所以,每拿到一个数据,都是一个独立的过程,而对手数据包来说,很明显它具有前后关联性,包头之后是数据,数据之后是包尾,对于包头、数据和包尾这3种状态,我们都需要有不同的处理逻辑,所以在程序中,我们需要设计一个能记住不同状态的机制,在不同状态执行不同的操作,同时还要进行状态的合理转移,这种程序设计思维,就叫做“状态机” ,
在这里我们就使用状态机的方法来接收一个数据包,要想设计一个好的状态机程序,画一个这样的状态转移图是必要的,对于上面这样一个固定包长HEX数据包来说,我们可以定义3个状态,第一个状态是等待包头,第二个状态是接收数据,第三个状态是等待包尾,
每个状态需要用一个变量来标志一下,比如我这里用变量S来标志,三个状态依次为S=0、S=1、S=2,这一点类似于置标志位,只不过标志位只有0和1,而状态机是多标志位状态的一种方式
然后执行流程是,最开始,S=0,收到一个数据,进中断,根据S=0,进入第一个状态的程序,判断数据是不是包头FF,如果是FF,则代表收到包头,之后置S=1,退出中断,结束,这样下次再进中断,根据S=1,就可以进行接收数据的程序了,那在第一个状态,如果收到的不是FF,就证明数据包没有对齐,我们应该等待数据包包头的出现,这时状态就仍然是0,下次进中断,就还是判断包头的逻辑,直到出现所,才能转到下一个状态,那之后,出现了FF,我们就可以转移到接收数据的状态了,这时再收到数据,我们就直接把它存在数组中,另外再用一个变量,记录收了多少个数据,如果没收够4个数据,就一直是接收状态,如果收够了,就置S=2,下次中断时,就可以进入下一个状态了,那最后一个状态就是等待包尾了,判断数据是不是FE,正常情况,应该是FE,这样就可以置S=0,回到最初的状态,开始下一个轮回,当然也有可能,这个数据不是FE,比如数据和包头重复,导致包头位置判断错了,那这个包尾位置就有可能不是FE,这时就可以进入重复等待包尾的状态,直到接收到真哐的包尾,这样加入包尾的判断,更能预防因数据和包头重复造成的错误,这就是使用状态机接收数据包的思路
这个状态机其实是一种很广泛的编程思路,使用的基本思路是,先根据项目要求定必状态,画几个圈,然后考虑好各个状态在什么情况下会进行转移,如何转移,画好线和转移条件,最后根据这个图来进行编程
3.2文本数据包接收(可变包长)
同样使用状态机,定义三个状态,第一个状态,等待包头,判断收到的是不是我们规定的@符号,如果收到@,就进入接收状态,在这个状态下,依次接收数据,同时,这个状态还应该要兼具等待包尾的功能,因为这是可变包长,我们接收数据的时候,也要时刻监视,是不是收到包尾了,一但收到包尾了,就结束,那这里,这个状态的逻辑就应该是收到一个数据,判断是不是\r,如果不是,则正常接收,如果是,则不接收,同时跳到下一个状态,等待包尾\n,因为我这里数据包有两个包尾\,\n,所以需要第三个状态,如果只有一个包尾,那在出现包尾之后,就可以直接回到初始状态了,只需要两个状态就行,因为接收数据和等待包尾需要在一个状态里同时进行,由于串口的包头包尾不会出现在数据中,所以基本不会出现数据错位的现象,这就是使用状态机接收文本数据包的方法