背景描述
在嵌入式开发中,MCU通常会与其它模块进行数据交互,简单来说有两种形式,一种是明文字符串流(比如类似AT指令),一种是hex流,本文实现一种能较为广泛的通用协议解析器,用于hex数据解析。
一、前言
hex协议基本是私有定制的,但他们都具有一定的共性,比如一个例子如下
帧头 | 帧类型 | 数据长度 | 数据 | 校验 | 帧尾 |
---|---|---|---|---|---|
A5 5A AA 55 | 1 byte | 1 byte | n byte | xor | 0D 0A 0D 0A |
一般hex协议的基本数据流都类似这种,或是其变种,这些数据流可以通过UART、网口、蓝牙、2.4g、SPI等等等等进行传输,简单的,我们就串口而言,解析它的方式大致两种。
一种是串口空闲中断,逐帧接收处理,这种方式最简单,CPU开销也小,但并不是每一种MCU都会有可靠的串口空闲中断,还有就是如果发送过程中有可能出现沾包,比如网口通信时由于网络延迟,会几帧一起收到。
另一种就是本文要实现的,逐字节匹配并解析,其优点是可以保证不沾包,如果加上DMA或者缓冲区,其CPU开销也还能接受,但就是写起代码来稍微麻烦一点。于是本文应运而生,直接用本文实现的模块,5分钟完成这部分的移植~~~
二、实现思路
简单的才是稳定好用的,这个模块要足够易于移植,如果是我用,我想要的API主要有3个。
初始化:将协议的帧头帧尾、长度、如何校验这些信息告诉给协议解析器。
获取数据:协议解析器里面开辟一个队列,将串口等接收到的数据往协议解析器里面放。
获取一帧完整的数据:轮询调用,将协议解析器队列里面的数据逐个拿出,逐个匹配并进行帧校验,校验通过将这一帧传出。
1、提取协议共性
我们要将这一帧数据完整的提取出来,需要匹配帧头,然后要知道这帧数据的总长度,以便去匹配帧尾(如果有的话),然后再对这一帧包含帧头帧尾的数据进行校验。
所以,协议解析器的结构我设计成如下:
//校验函数原型
//buffer,指向一帧数据
//len,该帧数据长度
typedef uint8_t b_check_cb_t(uint8_t *buffer, uint16_t len);
//获取帧长函数原型
//buffer,指向一帧数据
//len,该帧数据长度
typedef uint16_t b_get_frame_len_cb_t(uint8_t *buffer, uint16_t len);
//协议描述
typedef struct{
const uint8_t *pname;
uint16_t head_len; /* 帧头长度 */
uint16_t end_len; /* 帧尾长度 */
const char *head; /* 指向帧头数据 */
const char *end; /* 指向帧尾数据 */
b_get_frame_len_cb_t *get_frame_len_cb;
b_check_cb_t *check_cb; /* 帧校验函数指针 */
}b_frame_init_type;
用户只需要初始化帧头帧尾的格式和长度,实现获取帧长、帧校验的函数即可。
b_get_frame_len_cb_t 函数传入的是假定的完整一帧数据,然后返回这协议中这一帧数据的长度
比如按上面那个协议举例实现如下
uint16_t b_get_frame_len_cb(uint8_t *buffer, uint16_t len)
{
if(len < 4) return 0;//如果是非法长度,返回0即可
return buffer[5];
}
uint16_t b_get_frame_len_cb(uint8_t *buffer, uint16_t len)
{
uint16_t i;
uint8_t xor = 0;
for(i=0; i<len; i++){
xor ^= buffer[i];
}
return xor;//返回值为0代表校验成功
}
2、API设计
这套协议解析器最开始只能同时支持一种协议,如果想到一个项目里有两种不同的hex协议也不是不可能,到时候我还得搞两个.C文件,两个.H文件,然后改函数名(刚毕业工作我就这样干过)…,所以稍微稍微借鉴了一点点面向对象类的思想。如下
//协议类
typedef struct
{
b_frame_init_type frame_init;
//协议处理需要用到的中间变量,用户不可操作
ring_buf_t _frame_ring;
uint8_t _in_frame_buffer[MAX_FRAME_BUFFER_SIZE];
uint8_t _out_frame_buffer[MAX_FRAME_BUFFER_SIZE];
uint32_t _idie_timer;
uint32_t _systick;
} b_frame_type;
上面代码是整个协议解析器运行需要用到的全局变量,主要是frame_init(描述协议格式),和一个环形队列依赖,即_frame_ring、_in_frame_buffer,然后就是_idie_timer和_systick先按住不表,解析好的数据放在_out_frame_buffer。这个环形队列是一个大佬写的,我工作种用得非常多,主要用到的接口如下~~~
/*环形缓冲区管理器*/
typedef struct {
unsigned char *buf; /*环形缓冲区 */
unsigned int size; /*环形缓冲区大小 */
unsigned int front; /*头指针 */
unsigned int rear; /*尾指针 */
}ring_buf_t;
bool ring_buf_init(ring_buf_t *r,unsigned char *buf,unsigned int size);
unsigned int ring_buf_len(ring_buf_t *r);
unsigned int ring_buf_put(ring_buf_t *r,unsigned char *buf,unsigned int len);
unsigned int ring_buf_get(ring_buf_t *r,unsigned char *buf,unsigned int len);
//这个接口是为了实现这个解析器,我自己加的,ring_buf_get会在获取队列数据后,移动队列指针,
//ring_buf_check_get就是可以从同一个队列位置多次获取数据(其实就是把移动队列指针步骤删掉了)
unsigned int ring_buf_check_get(ring_buf_t *r,unsigned char *buf,unsigned int len);
然后是3个主要的API函数原型
// 协议格式描述初始化函数
// pframe:传入协议类指针
// pframeinit:协议描述结构体
// ret: B_SUCESS/B_ERR
extern uint8_t b_frame_init(b_frame_type *pframe,b_frame_init_type *pframeinit);
// 将数据丢入协议解析器缓冲区
// pframe:传入协议类指针
// dat:指向数据buffer
// len:丢入的数据长度
// ret:B_SUCESS,数据成功放到缓冲区,B_ERR,数据放入缓冲区失败
extern uint8_t b_frame_put(b_frame_type *pframe,uint8_t *dat, uint32_t len);
// 输出解析结果
// pframe:传入协议类指针
// ret len:存放解析出来的数据长度
// ret:解析出来的数据指针,NULL代表未解析出数据
extern const uint8_t *b_frame_check_get(b_frame_type *pframe,uint16_t *len);
使用步骤大概是
b_frame_type frame_engine
b_frame_init_type frame_init;
frame_init.head = "\xAA\x55\xA5\x5A";
frame_init.head_len = 4;
frame_init.end = "\x0D\x0A\x0D\x0A";
frame_init.end_len = 4;
frame_init.check_cb = check_frame_cb;
frame_init.get_frame_len_cb = get_frame_len_cb;
frame_init.pname = "engine";
b_frame_init(&frame_engine,&frame_init);
然后收到数据,调用b_frame_put将数据put到frame_engine里。
然后主函数轮询
uint8_t *pbuf = NULL;
uint16_t frame_len = 0;
pbuf = (uint8_t *)b_frame_check_get(&frame_engine,&frame_len);//pbuf指向一帧合法的数据,frame_len表示这帧数据的总长(包括帧头帧尾)
if (pbuf == NULL)return 0;
switch (pbuf[CMD])//或者直接把pbuf拷贝到结构体里进行解析,一般都这样做
{
.....
}
代码实现
日志
因为涉及数据交互,如果出问题不好排查原因,所以代码里嵌入一个小型的日志系统还是很有必要的,也不需要太复杂,什么日志等级啊都用不着,能打个log就好了
// 使能调试信息输出
#define EN_FRAME_DEBUG 1
#if (EN_FRAME_DEBUG != 0)
#include "stdio.h"
#define FRAME_LOG_PRINTF(...) printf(__VA_ARGS__)
#else
#define FRAME_LOG_PRINTF(...)
#endif
然后代码里用FRAME_LOG_PRINTF()打印就好啦,到时要关掉就改宏定义。嗯,够简单了~
初始化
代码如下,主要就是初始化队列,然后把pframeinit中的协议描述搬移到pframe中。(因为pframeinit可以是局部变量)
uint8_t b_frame_init(b_frame_type *pframe, b_frame_init_type *pframeinit)
{
uint8_t err = 0;
if ((!pframeinit) || (!pframe))
{
FRAME_LOG_PRINTF("%s-err:frame parameter error\r\n", pframe->frame_init.pname);
return B_ERROR;
}
pframe->frame_init.pname = pframeinit->pname;
pframe->frame_init.check_cb = pframeinit->check_cb;
pframe->frame_init.end = pframeinit->end;
pframe->frame_init.end_len = pframeinit->end_len;
pframe->frame_init.get_frame_len_cb = pframeinit->get_frame_len_cb;
pframe->frame_init.head = pframeinit->head;
pframe->frame_init.head_len = pframeinit->head_len;
err = ring_buf_init(&pframe->_frame_ring, pframe->_in_frame_buffer, MAX_FRAME_BUFFER_SIZE);
if (err)
return B_SUCCESS;
FRAME_LOG_PRINTF("%s-err:ring_buf_init err\r\n", pframe->frame_init.pname);
return B_ERROR;
}
put函数实现
如下,基本没啥好讲的,就是把数据丢队列里面去,然后加些安全机制和日志信息。
uint8_t b_frame_put(b_frame_type *pframe, uint8_t *dat, uint32_t len)
{
if (!dat || len == 0)
{
return B_ERROR;
}
uint8_t putlen = ring_buf_put(&pframe->_frame_ring, dat, len);
if (putlen != len)
{
ring_buf_clr(&pframe->_frame_ring);
FRAME_LOG_PRINTF("%s-err:The ringbuffer is full\r\n", pframe->frame_init.pname);
return B_ERROR;
}
pframe->_idie_timer = 0;
return B_SUCCESS;
}
匹配帧头
static uint8_t b_check_head(b_frame_type *pframe)
{
uint8_t i = 0;
uint8_t tmp;
uint16_t len = ring_buf_len(&pframe->_frame_ring);
if (len < (pframe->frame_init.head_len + pframe->frame_init.end_len) + 1)
{
return B_ERROR;
}
if (pframe->frame_init.head_len == 0)
return B_SUCCESS;
do
{
ring_buf_get(&pframe->_frame_ring, &tmp, 1);
if (tmp == pframe->frame_init.head[i])
{
if (++i == pframe->frame_init.head_len)
{
return B_SUCCESS;
}
}
else
{
break;
}
} while (1);
return B_ERROR;
}
解析实现
代码如下,因为代码量稍多,在代码里面逐步注释讲解
const uint8_t *b_frame_check_get(b_frame_type *pframe, uint16_t *len)
{
static uint8_t head_match_flg = 0;
uint16_t i, j;
uint16_t unhead_frame_len;
//head_match_flg是匹配帧头的标志,为一个静态全局变量,匹配到帧头之后
//head_match_flg置位,后面就不用再匹配了,直到需要重新匹配帧头时,清零head_match_flg
if (!head_match_flg)
{
uint8_t err = b_check_head(pframe);
if (err != B_SUCCESS)
{
return NULL;
}
if (pframe->frame_init.head_len != 0)
{
memcpy(pframe->_out_frame_buffer, pframe->frame_init.head, pframe->frame_init.head_len);
}
head_match_flg = 1;
FRAME_LOG_PRINTF("%s-successful:Frame header matching\r\n", pframe->frame_init.pname);
}
if (pframe->frame_init.get_frame_len_cb != NULL)
{
//按最大长度预期去获取环形队列的数据,并返回实际获取到的数据的长度
uint16_t data_len = ring_buf_check_get(&pframe->_frame_ring, &pframe->_out_frame_buffer[pframe->frame_init.head_len], MAX_FRAME_BUFFER_SIZE);
//获取整帧数据长度,并减去帧头长度,以得到帧尾的位置
unhead_frame_len = pframe->frame_init.get_frame_len_cb(pframe->_out_frame_buffer, data_len + pframe->frame_init.head_len);
if (unhead_frame_len >= pframe->frame_init.head_len)
{
unhead_frame_len = unhead_frame_len - pframe->frame_init.head_len;
}
FRAME_LOG_PRINTF("%s-info:unhead_frame_len = %d,ring_data_len = %d\r\n", pframe->frame_init.pname, unhead_frame_len, data_len);
//一些安全机制...,最后讲
if ((data_len < unhead_frame_len) || (unhead_frame_len == 0))
{
if (pframe->_idie_timer > IDIE_FRAME_FACTOR * unhead_frame_len + 1)
{
pframe->_idie_timer = 0;
head_match_flg = 0;
FRAME_LOG_PRINTF("%s-err:idie_timer timeout\r\n", pframe->frame_init.pname);
}
return NULL;
}
}
else
{
FRAME_LOG_PRINTF("%s-err:get_frame_len_cb is null\r\n", pframe->frame_init.pname);
return NULL;
}
//获取帧尾数据,并去匹配帧尾
i = unhead_frame_len + pframe->frame_init.head_len - pframe->frame_init.end_len;
for (j = 0; j < pframe->frame_init.end_len;)
{
if (pframe->_out_frame_buffer[i] == pframe->frame_init.end[j])
{
i++;
j++;
}
else
{
break;
}
}
FRAME_LOG_PRINTF("%s-info:data>>>\r\n", pframe->frame_init.pname);
printHexAscii(pframe->_out_frame_buffer, pframe->frame_init.head_len + unhead_frame_len);
//帧尾匹配成功
if (j == pframe->frame_init.end_len)
{
//通过指针返回整帧数据长度
*len = pframe->frame_init.head_len + unhead_frame_len;
//对整帧数据进行校验
if (pframe->frame_init.check_cb != NULL)
{
uint8_t err = pframe->frame_init.check_cb(pframe->_out_frame_buffer, *len);
if (err == B_SUCCESS)
{
//校验通过,重新匹配下一帧帧头
//并且将这一合法帧从队列提取出来,拷贝到_out_frame_buffer里,并返回其指针
head_match_flg = 0;
ring_buf_get(&pframe->_frame_ring, &pframe->_out_frame_buffer[pframe->frame_init.head_len], unhead_frame_len);
FRAME_LOG_PRINTF("%s-successful:Frame all matching\r\n", pframe->frame_init.pname);
return pframe->_out_frame_buffer;
}
}
}
head_match_flg = 0;
return NULL;
}
杂项
如上,基本实现了协议的解析,还记得协议类结构体里面的_idie_timer和_systick吗,它们主要是用来实现一个安全机制,我在一次使用这个模块时,帧长度是2个byte字节,由于数据量非常大,会产生一些帧头冲突,刚好这个时候长度字段的值是0xFFFF。这个时候帧尾就一直匹配不到…哦豁,协议解析器卡死。
然后我就引入了串口空闲中断的一个模拟机制,获取到长度字节后,会根据长度字节设定一个超时时间,如果超时了就放弃匹配帧尾,重新去匹配帧头。
所以需要给模块提供一个心跳,下面这个函数周期调用,比如1ms。
void b_frame_idie_timer(b_frame_type *pframe)
{
pframe->_idie_timer++;
pframe->_systick++;
}
结尾
okkkkk,基本讲得差不多了,代码gitee仓库地址在这,欢迎点赞关注,如果你在使用这个模块时有什么bug,请务必反馈,不胜感激~~~