文章首发于同名微信公众号:DigCore
欢迎关注同名微信公众号:DigCore,及时获取最新技术博文。
原文链接:https://mp.weixin.qq.com/s/amO7rGkqFJtCzuyjyIVwDw
串口实现了两个终端设备之间进行可靠的通信,串口在这中间完成了传输层的作用。本次要讲的是关于数据的协议。
洞幺!洞幺!我是洞拐!收到请回答!收到请回答!over!
在战争题材影视剧中经常能够看到这样的对白,在通过对讲机等相关无线设备呼叫队友时,先呼叫对方名称,然后告知自己身份,说完内容最后再说over,表示一次呼叫结束。
是的,没错,这就是本节要讲的在串口通信中发挥重要作用的起止式协议!
UART的时序本身就是起止式协议,具体可参考《嵌入式硬件通信接口协议-UART(一)协议基础》这一篇内容的介绍。
事实上串口实现了数据通信过程中的传输层,而应用层就系统功能的业务逻辑,应用层控制需要收发的各种数据内容。
数据解析的前提是通信双方都是用统一的数据帧格式,因此在这里将设计一个简单的起止式的数据帧格式,保证设备之间进行可靠的通信。
现在的很多无线模块,为了使用简单和易于集成,模块对外使用UART接口,并采用AT指令来完成配置和使用,常见的有ESP8266的WiFi模块、HC-05蓝牙串口模块。
AT指令的特点是易于人机交互,使用者对其发AT指令时,都是用ASCII字符发送,对于模块的处理,也是以字符来处理。这样的AT指令,它的起止式特点是以“AT”两个字符开头,并以回车换行“\r\n”字符结束。
HC-05蓝牙模块指令示例
但是项目工程中,数据在嵌入式设备是以HEX数据(16进制)运算和处理,如果参考AT指令去设计帧结构,那么在收发处理时候,必然要将收到的纯数据(16进制)按照字符处理。
比如一个终端设备,其功能就是环境检测,可能包含温湿度、光照强度、二氧化碳浓度、PM2.5浓度等等,如果要发出一个温度采集结果24℃数据,采集设备将数据24分成2个字节发送,因为ASCII字符’2’对应的16进制是0x32、ASCII字符’4’对应的16进制是0x34,这样的一个温度数据就需要2个字节来发送。接收端接收到的是0x32、0x34后,再以查表方式逆向换算出原温度数据’24’,这个过程就是采用字符处理的麻烦之一。
因此不考虑使用ASCII字符来组帧结构。
最简单的帧,就是有开头+结尾做起止标志。
比如0x55 + 数据包 + 0xAA。
在一长串的数据流中,接收端逐字节接收,并判断是否存在0x55,如果存在则开始存入数据包缓冲器,直到接收了0xAA数据,认为完成一帧数据的接收。
这个方法确实相当简单,不用太多的处理,只需要判断开头和结尾即可。
而这样存在很大的问题,如果传输的内容也有0xAA这样的数据,这个0xAA并非结尾标志,而程序接收过程就提前结束,这样就不能保证完整接收一帧数据包了。
在精简起止式结构基础上,增加一数据来标志数据包长度。
比如0x55 + 长度 + 数据包 + 0xAA。
这样一来,接收端判断接收到了0x55的开头标志,紧接着再接收一个“长度”的字节,基于这个长度来继续接收后续剩余的数据。
可见如果有了长度的约束,那么最后都不需要0xAA作为结尾标志了。
这样的接口,即使有开头、长度、结尾,还存在风险。比如传输数据时,物理线路受到未知干扰,导致数据内容出现了异常,那么接收端即使完整接收所有数量的数据下来,也是错误的内容。
解决在发送过程中出现的未知错误问题,必然需要对数据进行校验。再增加一字段来标志数据内容的校验计算结果。
比如0x55 + 长度 + 校验值 + 数据内容 + 0xAA。
校验值是对数据包采用算法计算而得,接收方完整收下所有数量的数据,再对数据包采用同样的算法计算出校验值,从而对比校验值来确定数据包的准确性。
对于校验值的运算,采用CRC-16运算的方式,检错能力强,开销小。
综上所述,基于起止式的帧结构可以设计成:0x55 + 长度 + CRC校验 + 数据包。
在这里,帧头标志采用0x55一个字节。
0x55二进制是01010101,这样在UART物理线路上输出的信号将会是占空比50%的方波,方波是最容易进行测量和诊断的,在实际波形观测时可以确定稳定性、噪声毛刺等。
要说0xAA(二进制10101010)也是可以,但是UART发送时候是有一个起始位0,并且是以LSB方式先发送bit0的最低位,0xAA的bit0已经是0,而0x55的bit0是1,因此想得到方波当然优先考虑用0x55。
长度采用一个字节表示,则后续的CRC校验 + 数据包的总数量最多能放255个字节。
CRC校验采用CRC-16算法,占2个字节,此时后续的数据包最多能放253个字节。
终上所述,得出最终的起止式帧结构:
接下来开始设计处理程序。
根据帧结构,可以定义如下的结构体:
typedef struct{
uint8_t head;
uint8_t len;
uint8_t crc16L;
uint8_t crc16H;
uint8_t packet[253];
}sst_frame_t;
其中要特别说明的:
packet数据包最大长度设为253,是因为len是uint8_t类型,len最大255,而CRC校验值占了2个字节,因此packet数据包最多可253个字节。
CRC校验值采用的是CRC-16标准,校验值是个uint16_t类型的数据,传输时采用的是LSB模式,因此将CRC校验值设为两个uint8_t类型的数据,这样做便于在源码移植过程中,不同平台的大小端差异能够得到正确处理。
简述嵌入式设备内存大小端差异在结构体定义以及使用时存在的问题:
假如对帧结构定义了如下的结构体:
typedef struct{
uint8_t head;
uint8_t len;
uint16_t crc16;
uint8_t packet[253];
}sst_frame_t;
计算后得到某一次的校验值结果是 0xDC66,这是一个uint16_t类型的数据,如果直接使用这个结构体来处理数据发送,那么:
在LSB的小端模式平台下,数据的发送顺序是
head、len、0x66、0xDC、packet[0]、packet[1]、...
反之在MSB大端模式的平台里,数据的发送顺序是
head、len、0xDC、0x66、packet[0]、packet[1]、...
因此采用2个字节uint8_t数据类型代替uint16_t来定义结构体中的CRC校验值,使得在跨平台收发数据时无需做差异化处理。
使用起止式进行数据传输时,把应用层的数据包进行组帧,这样可构造一个完整的数据帧,便于在应用层将完整的一帧数据传递给传输层发出。
这里的构造过程,事实上是对帧结构的“填充”过程。
首先是计算数据包的CRC校验值,随后就是“填充”的过程。
为了防止应用层调用接口时,传进来的数据包的地址、组帧结果的首地址指向同一个内存地址,所以在组帧前需要将源数据内容单独缓存,再进行“填充”的操作。
解析帧结构其实就是对一长串的数据流进行解析处理,从而提取出数据包。
这里被解析的数据来源是一个循环缓冲区,对循环缓冲区内的可读数据进行解析。因此需要使用循环缓冲区配合。
代码截图:
解析思路是:
1.确保环形缓冲区有足够一个帧结构的数据量,否则返数据量不足的错误;
2.接着读出一个字节判断帧头标志是否为0x55,否则返帧头错误;
3.再次读一个字节作为帧长度数据,且长度至少3个字节(2个CRC校验值+至少1字节数据包),否则返帧长度错误;
4.读出帧长度数据,如果此时环形缓冲区的可读数量比长度数值小,出现这情况的原因可能是帧长度字段在发送期间出现异常,或是对端设备串口传输慢而未完整传输一帧,此时可做适当的延时等待,如果超时退出,且返帧长度错误;
5.继续读出2个字节作为CRC校验值,且需要注意先收到的是crc16L,先收到小端数值;
6.紧接着把数据包读出,此时读的长度应该是第4步中的帧长度数据少2个字节;
7.最后对数据包计算一个CRC校验值,对比接收到的校验值,校验值不一致则返错误校验码。
函数返回值符合以下枚举的错误码:
看到这里也许仍有疑问,用于解析的数据源哪来?数据什么时候被写进环形缓冲区内?
参考上一篇《嵌入式硬件通信接口-使用RingBuffer处理数据(二)详细设计过程》介绍的关于向环形缓冲区写入一个字节,但dclib_ringbuffer这个模块属于应用库模块层,而如果直接把dclib_rb_writebyte这一个接口放在串口接收中断里执行,这就破坏了系统的架构层次,对工程代码的维护和移植是个麻烦事,因此采用回调函数的方式。
嵌入式开发工程师都知道,一般在使用官方的库时,经常会遇到需要自己实现一些回调函数,从而利用注册接口把回调函数传递给库或者驱动层,使库或者驱动层在执行时调用该回调函数。
根据这个思路,同样的这里也采用回调函数的形式,回调函数内完成了把串口接收到的数据写入环形缓冲区内。
回调函数的实现源码截图:
事实上仅仅调用了dclib_ringbuffer功能里的写一字节接口dclib_rb_writebyte,回调函数传进来的参数dat就是串口接收到的数据。
有了回调函数,还要把这个回调函数的地址传给底层驱动,这也就是常说的“注册”的过程,注册接口在固件板级接口层里串口模块dcbsp_uart实现,注册接口时dclib_uart_callback_reg函数:
又偏题了,关于回调函数在此不做深入论述。
简而言之,环形缓冲区写入一字节的执行过程,放在回调函数里,当串口接收中断触发后,中断里会根据注册的回调函数地址,进而执行回调函数,实现对环形缓冲区写入一个字节数据。如此操作的理由是不改变工程代码的分层架构,并且便于维护与移植!
为了缩减篇幅,最后贴上测试代码的部分:
最后也附上调试期间串口打印的解析结果:
起止式帧结构的讲解稍有匆忙,篇幅也略大,文中基础技术要点未能细致讲解,后续统筹规划再做单独介绍!
接下来在此帧结构基础上,讲述如何设计在数据包放置应用层的交互指令,敬请期待下回分!
《ASCII》@百度百科
https://baike.baidu.com/item/ASCII
《CRC》@百度百科
https://baike.baidu.com/item/CRC/1453359
《CRC开源项目》@github
https://github.com/lammertb/libcrc
长按二维码关注DigCore
关注公众号,发消息:
下载&StartStopType
获取本文PDF原文或源代码下载链接