一种通用hex协议解析器的简单实现


背景描述

在嵌入式开发中,MCU通常会与其它模块进行数据交互,简单来说有两种形式,一种是明文字符串流(比如类似AT指令),一种是hex流,本文实现一种能较为广泛的通用协议解析器,用于hex数据解析。


一、前言

hex协议基本是私有定制的,但他们都具有一定的共性,比如一个例子如下

帧头帧类型数据长度数据校验帧尾
A5 5A AA 551 byte1 byten bytexor0D 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,请务必反馈,不胜感激~~~

  • 29
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Hex文件合并是将多个hex文件合并成一个单一的hex文件的过程。通常,hex文件是以十六进制编码的内容文件,用于存储程序或数据的二进制表示。 合并多个hex文件有以下几个步骤: 1. 读取所有的hex文件。 2. 解析每个hex文件的内容。每个hex文件包括起始地址(通常是16位的地址)和数据。解析时需要考虑起始地址和数据的格式以及校验和,以确保文件完整性和正确性。 3. 对每个hex文件中的数据进行地址偏移。由于合并后的hex文件需要保持地址连续性,因此需要计算每个hex文件的起始地址相对于整个合并后hex文件的起始地址的偏移量。 4. 将所有解析和地址偏移后的数据按照起始地址排序。这样可以保证合并后的hex文件按照地址顺序存储。 5. 计算合并后hex文件的校验和,以确保文件完整性。 6. 将所有合并后的数据写入一个新的hex文件中。 合并hex文件有很多应用场景,例如在嵌入式系统开发中,多个hex文件可能分别包含不同的功能模块或者固件版本,合并后可以得到一个完整的可用文件。此外,在芯片烧录时,合并hex文件可以减少烧录的次数,提高效率。 需要注意的是,在进行hex文件合并时,需要确保合并的文件之间没有地址冲突或者数据重叠的问题,否则可能会导致程序执行错误或数据丢失等问题。因此,在合并之前,应该仔细检查每个hex文件的起始地址和数据范围,确保合并后的文件是正确的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值