IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 嵌入式 -> 简单自定义协议的封包和解包 -> 正文阅读

[嵌入式]简单自定义协议的封包和解包

原文链接:https://blog.csdn.net/sinat_16643223/article/details/118830297

一、通信协议

有一些初学者总觉得通信协议是一个很复杂的知识,把它想的很高深,导致不知道该怎么学。

同时,偶尔有读者问关于串口自定义通信协议相关的问题,今天就来写写串口通信协议,并不是你想想中的那么难?

1什么通信协议?
通信协议不难理解,就是两个(或多个)设备之间进行通信,必须要遵循的一种协议。

1 百度百科的解释

通信协议是指双方实体完成通信或服务所必须遵循的规则和约定。通过通信信道和设备互连起来的多个不同地理位置的数据通信系统,要使其能协同工作实现信息交换和资源共享,它们之间必须具有共同的语言。交流什么、怎样交流及何时交流,都必须遵循某种互相都能接受的规则。这个规则就是通信协议。

相应该有很多读者都买过一些基于串口通信的模块,市面上很多基于串口通信的模块都是自定义通信协议,有的比较简单,有的相对复杂一点。

举一个很简单的串口通信协议的例子:比如只传输一个温度值,只有三个字节的通信协议:
在这里插入图片描述
这种看起来是不是很简单?它也是一种通信协议。

只是说这种通信协议应用的场合相对比较简单(一对一两个设备之间),同时,它存在很多弊端。

2 过于简单的通信协议引发的问题

上面那种只有三个字节的通信协议,相信大家都看明白了。虽然它也能通信,也能传输数据,但它存在一系列的问题。

比如:多个设备连接在一条总线(比如485)上,怎么判断传输给谁?(没有设备信息)

还比如:处于一个干扰环境,你能保障传输数据正确吗?(没有校验信息)

再比如:我想传输多个不确定长度的数据,该怎么办?(没有长度信息)。

上面这一系列问题,相信做过自定义通信的朋友都了解。

所以,在通信协议里面要约定更多的“协议信息”,这样才能保证通信的完整。

3 通信协议常见内容

基于串口的通信协议通常不能太复杂,因为串口通信速率、抗干扰能力以及其他各方面原因,相对于TCP/IP这种通信协议,是一种很轻量级的通信协议。

所以,基于串口的通信,除了一些通用的通信协议(比如:Modubs、MAVLink)之外,很多时候,工程师都会根据自己项目情况,自定义通信协议。

下面简单描述下常见自定义通信协议的一些要点内容。
在这里插入图片描述
(这是一些常见的协议内容,可能不同情况,其协议内容不同)

1.帧头

帧头,就是一帧通信数据的开头。

有的通信协议帧头只有一个,有的有两个,比如:5A、A5作为帧头。
在这里插入图片描述

2.设备地址/类型

设备地址或者设备类型,通常是用于多种设备之间,为了方便区分不同设备。
在这里插入图片描述
这种情况,需要在协议或者附录中要描述各种设备类型信息,方便开发者编码查询。

当然,有些固定的两种设备之间通信,可能没有这个选项。

3.命令/指令

命令/指令比较常见,一般是不同的操作,用不同的命令来区分。
在这里插入图片描述
举例:温度:0x01;湿度:0x02;

4.命令类型/功能码

这个选项对命令进一步补充。比如:读、写操作。
在这里插入图片描述
举例:读Flash:0x01; 写Flash:0x02;

5.数据长度

数据长度这个选项,可能有的协议会把该选项提到前面设备地址位置,把命令这些信息算在“长度”里面。

这个主要是方便协议(接收)解析的时候,统计接收数据长度。
在这里插入图片描述
比如:有时候传输一个有效数据,有时候要传输多个有效数据,甚至传输一个数组的数据。这个时候,传输的一帧数据就是不定长数据,就必须要有【数据长度】来约束。

有的长度是一个字节,其范围:0x01 ~ 0xFF,有的可能要求一次性传输更多,就用两个字节表示,其范围0x0001 ~ 0xFFFFF。

当然,有的通信长度是固定的长度(比如固定只传输、温度、湿度这两个数据),其协议可能没有这个选项。

6.数据

数据就不用描述了,就是你传输的实实在在的数据,比如温度:25℃。

7.帧尾

有些协议可能没有帧尾,这个应该是可有可无的一个选项。

8.校验码

校验码是一个比较重要的内容,一般正规一点的通信协议都有这个选项,原因很简单,通信很容易受到干扰,或者其他原因,导致传输数据出错。

如果有校验码,就能比较有效避免数据传输出错的的情况。
在这里插入图片描述
校验码的方式有很多,校验和、CRC校验算是比较常见的,用于自定义协议中的校验方式。

还有一点,有的协议可能把校验码放在倒数第二,帧尾放在最后位置。

4 通信协议代码实现(DGUS串口屏的例子)

自定义通信协议,代码实现的方式有很多种,怎么说呢,“条条大路通罗马”你只需要按照你协议要写实现代码就行。

当然,实现的同时,需要考虑你项目实际情况,比如通信数据比较多,要用消息队列(FIFO),还比如,如果协议复杂,最好封装结构体等。

下面分享一些以前用到的代码,可能没有描述更多细节,但一些思想可以借鉴。

1.消息数据发送

a.通过串口直接发送每一个字节

这种对于新手来说都能理解,这里分享一个之前DGUS串口屏的例子:

#define DGUS_FRAME_HEAD1          0xA5                     //DGUS屏帧头1
#define DGUS_FRAME_HEAD2          0x5A                     //DGUS屏帧头2


#define DGUS_CMD_W_REG            0x80                     //DGUS写寄存器指令
#define DGUS_CMD_R_REG            0x81                     //DGUS读寄存器指令
#define DGUS_CMD_W_DATA           0x82                     //DGUS写数据指令
#define DGUS_CMD_R_DATA           0x83                     //DGUS读数据指令
#define DGUS_CMD_W_CURVE          0x85                     //DGUS写曲线指令


/* DGUS寄存器地址 */
#define DGUS_REG_VERSION          0x00                     //DGUS版本
#define DGUS_REG_LED_NOW          0x01                     //LED背光亮度
#define DGUS_REG_BZ_TIME          0x02                     //蜂鸣器时长
#define DGUS_REG_PIC_ID           0x03                     //显示页面ID
#define DGUS_REG_TP_FLAG          0x05                     //触摸坐标更新标志
#define DGUS_REG_TP_STATUS        0x06                     //坐标状态
#define DGUS_REG_TP_POSITION      0x07                     //坐标位置
#define DGUS_REG_TPC_ENABLE       0x0B                     //触控使能
#define DGUS_REG_RTC_NOW          0x20                     //当前RTCS


//往DGDS屏指定寄存器写一字节数据
void DGUS_REG_WriteWord(uint8_t RegAddr, uint16_t Data)
{
  DGUS_SendByte(DGUS_FRAME_HEAD1);
  DGUS_SendByte(DGUS_FRAME_HEAD2);
  DGUS_SendByte(0x04);


  DGUS_SendByte(DGUS_CMD_W_REG);                 //指令
  DGUS_SendByte(RegAddr);                        //地址


  DGUS_SendByte((uint8_t)(Data>>8));             //数据
  DGUS_SendByte((uint8_t)(Data&0xFF));
}


//往DGDS屏指定地址写一字节数据
void DGUS_DATA_WriteWord(uint16_t DataAddr, uint16_t Data)
{
  DGUS_SendByte(DGUS_FRAME_HEAD1);
  DGUS_SendByte(DGUS_FRAME_HEAD2);
  DGUS_SendByte(0x05);


  DGUS_SendByte(DGUS_CMD_W_DATA);                //指令


  DGUS_SendByte((uint8_t)(DataAddr>>8));         //地址
  DGUS_SendByte((uint8_t)(DataAddr&0xFF));


  DGUS_SendByte((uint8_t)(Data>>8));             //数据
  DGUS_SendByte((uint8_t)(Data&0xFF));
}

b.通过消息队列发送

在上面基础上,用一个buf装下消息,然后“打包”到消息队列,通过消息队列的方式(FIFO)发送出去。

static uint8_t  sDGUS_SendBuf[DGUS_PACKAGE_LEN];


//往DGDS屏指定寄存器写一字节数据
void DGUS_REG_WriteWord(uint8_t RegAddr, uint16_t Data)
{
  sDGUS_SendBuf[0] = DGUS_FRAME_HEAD1;           //帧头
  sDGUS_SendBuf[1] = DGUS_FRAME_HEAD2;
  sDGUS_SendBuf[2] = 0x06;                       //长度
  sDGUS_SendBuf[3] = DGUS_CMD_W_CTRL;            //指令
  sDGUS_SendBuf[4] = RegAddr;                    //地址
  sDGUS_SendBuf[5] = (uint8_t)(Data>>8);         //数据
  sDGUS_SendBuf[6] = (uint8_t)(Data&0xFF);


  DGUS_CRC16(&sDGUS_SendBuf[3], sDGUS_SendBuf[2] - 2, &sDGUS_CRC_H, &sDGUS_CRC_L);
  sDGUS_SendBuf[7] = sDGUS_CRC_H;                //校验
  sDGUS_SendBuf[8] = sDGUS_CRC_L;


  DGUSSend_Packet_ToQueue(sDGUS_SendBuf, sDGUS_SendBuf[2] + 3);
}


//往DGDS屏指定地址写一字节数据
void DGUS_DATA_WriteWord(uint16_t DataAddr, uint16_t Data)
{
  sDGUS_SendBuf[0] = DGUS_FRAME_HEAD1;           //帧头
  sDGUS_SendBuf[1] = DGUS_FRAME_HEAD2;
  sDGUS_SendBuf[2] = 0x07;                       //长度
  sDGUS_SendBuf[3] = DGUS_CMD_W_DATA;            //指令
  sDGUS_SendBuf[4] = (uint8_t)(DataAddr>>8);     //地址
  sDGUS_SendBuf[5] = (uint8_t)(DataAddr&0xFF);
  sDGUS_SendBuf[6] = (uint8_t)(Data>>8);         //数据
  sDGUS_SendBuf[7] = (uint8_t)(Data&0xFF);


  DGUS_CRC16(&sDGUS_SendBuf[3], sDGUS_SendBuf[2] - 2, &sDGUS_CRC_H, &sDGUS_CRC_L);
  sDGUS_SendBuf[8] = sDGUS_CRC_H;                //校验
  sDGUS_SendBuf[9] = sDGUS_CRC_L;


  DGUSSend_Packet_ToQueue(sDGUS_SendBuf, sDGUS_SendBuf[2] + 3);
}

c.用“结构体”代替“数组SendBuf”方式

结构体对数组更方便引用,也方便管理,所以,结构体方式相比数组buf更高级,也更实用。(当然,如果成员比较多,如果用临时变量方式也会导致占用过多堆栈的情况)

比如:

typedef struct
{
  uint8_t  Head1;                 //帧头1
  uint8_t  Head2;                 //帧头2
  uint8_t  Len;                   //长度
  uint8_t  Cmd;                   //命令
  uint8_t  Data[DGUS_DATA_LEN];   //数据
  uint16_t CRC16;                 //CRC校验
}DGUS_PACKAGE_TypeDef;

d.其他更多

串口发送数据的方式有很多,比如用DMA的方式替代消息队列的方式。

2.消息数据接收

串口消息接收,通常串口中断接收的方式居多,当然,也有很少情况用轮询的方式接收数据。

a.常规中断接收

还是以DGUS串口屏为例,描述一种简单又常见的中断接收方式:

void DGUS_ISRHandler(uint8_t Data)
{
  static uint8_t sDgus_RxNum = 0;                //数量
  static uint8_t sDgus_RxBuf[DGUS_PACKAGE_LEN];
  static portBASE_TYPE xHigherPriorityTaskWoken = pdFALSE;


  sDgus_RxBuf[gDGUS_RxCnt] = Data;
  gDGUS_RxCnt++;


  /* 判断帧头 */
  if(sDgus_RxBuf[0] != DGUS_FRAME_HEAD1)       //接收到帧头1
  {
    gDGUS_RxCnt = 0;
    return;
  }
  if((2 == gDGUS_RxCnt) && (sDgus_RxBuf[1] != DGUS_FRAME_HEAD2))
  {
    gDGUS_RxCnt = 0;
    return;
  }


  /* 确定一帧数据长度 */
  if(gDGUS_RxCnt == 3)
  {
    sDgus_RxNum = sDgus_RxBuf[2] + 3;
  }


  /* 接收完一帧数据 */
  if((6 <= gDGUS_RxCnt) && (sDgus_RxNum <= gDGUS_RxCnt))
  {
    gDGUS_RxCnt = 0;


    if(xDGUSRcvQueue != NULL)                    //解析成功, 加入队列
    {
      xQueueSendFromISR(xDGUSRcvQueue, &sDgus_RxBuf[0], &xHigherPriorityTaskWoken);
      portEND_SWITCHING_ISR(xHigherPriorityTaskWoken);
    }
  }
}

b.增加超时检测

接收数据有可能存在接收了一半,中断因为某种原因中断了,这时候,超时检测也很有必要。

比如:用多余的MCU定时器做一个超时计数的处理,接收到一个数据,开始计时,超过1ms没有接收到下一个数据,就丢掉这一包(前面接收的)数据。

static void DGUS_TimingAndUpdate(uint16_t Nms)
{
  sDGUSTiming_Nms_Num = Nms;
  TIM_SetCounter(DGUS_TIM, 0);                   //设置计数值为0
  TIM_Cmd(DGUS_TIM, ENABLE);                     //启动定时器
}


void DGUS_COM_IRQHandler(void)
{
  if((DGUS_COM->SR & USART_FLAG_RXNE) == USART_FLAG_RXNE)
  {
    DGUS_TimingAndUpdate(5);                     //更新定时(防止超时)
    DGUS_ISRHandler((uint8_t)USART_ReceiveData(DGUS_COM));
  }
}

c.更多

接收和发送一样,实现方法有很多种,比如接收同样也可以用结构体方式。但有一点,都需要结合你实际需求来编码。

5 最后

以上自定义协议内容仅供参考,最终用哪些、占用几个字节都与你实际需求有关。

基于串口的自定义通信协议,有千差万别,比如:MCU处理能力、设备多少、通信内容等都与你自定义协议有关。

有的可能只需要很简单的通信协议就能满足要求。有的可能需要更复杂的协议才能满足。

最后强调两点:

1.以上举例并不是完整的代码(有些细节没有描述出来),主要是供大家学习这种编程思想,或者实现方式。

2.一份好的通信协议代码,必定有一定容错处理,比如:发送完成检测、接收超时检测、数据出错检测等等。所以说,以上代码并不是完整的代码。

二、怎样用串口发送结构体-简单协议的封包和解包

https://blog.csdn.net/qq_33904382/article/details/112718948

定义要发送的结构体

/**
	@part 通信数据结构
*/

/* 加速度信息结构体-XYZ三分量 */
typedef struct CSModuleInfo_ACC{
	float _acc_X;
	float _acc_Y;	
	float _acc_Z;
}CSInfo_Acc;

/* 经纬度信息结构体-经纬两分量 */
typedef struct CSmouduleInfo_LL{
	float _latitude;
	float _longitude;
}CSInfo_LL;

/* 测控站信息结构体 */
typedef struct CSInfoStrcutre CSInfoS;
typedef struct CSInfoStrcutre{
	/* 核心温度 MCU温度 */
	float _temp_O_MCU;
	/* 气温 */
	float _temp_env;
	/* 气压 */
	float _gp;
	/* 加速度 */
	CSInfo_Acc _acc;
	/* 经纬度 */
	CSInfo_LL _ll;
}* ptrCSInfo;

为了保证本文能符合大伙的需求,咱搞一个结构体嵌套,并且把数据类型都定义浮点数。意在说明我们这种传输结构体的方式不受结构体类型的限制,也不受浮点数的存储方式的限制,请放心学习使用。

注:代码中/* 测控站信息结构体 */部分的ptrCSInfo是这个大结构体的指针类型,CSInfoS是这个结构体的别名,这种写法是C语言的语法规则所允许的,不用感到奇怪。

下位机封包发送

封包发送的过程可以用下面的代码实现:

/**
* @brief  将数据打包并发送到上位机
  * @param  
			ptrInfoStructure 指向一个装填好信息的infoStructure的指针
  * @retval 无
  */

void CSInfo_PackAndSend(ptrCSInfo ptrInfoStructure)
{
	uint8_t infoArray[32]={0};
	uint8_t infoPackage[38]={0};
	CSInfo_2Array_uint8(ptrInfoStructure,infoArray);
	CSInfo_Pack(infoPackage,infoArray,sizeof(infoArray));
	CSInfo_SendPack(infoPackage,sizeof(infoPackage));
}

向这个函数传入一个装有数据的结构体的指针ptrInfoStructure,依次调用CSInfo_2Array_uint8、CSInfo_Pack、CSInfo_SendPack这三个自定义函数,即可通过串口将结构体发送出去。

这三个函数分别对应着擦拆分结构体、按照协议/规则封包和发送数据三个过程,具体说明和代码如下:

1、拆分

文章开头我们已经说了,先要把结构体拆分成8位无符号整型(uint8_t)的数据:

/**
  * @brief  将数据段(CSInfoStructure)重组为uint8类型的数组
  * @param  
		infoSeg 指向一个CSInfoStructure的指针
	    infoArray 由数据段重组的uint8类型数组			
  * @retval 无
  */
void CSInfo_2Array_uint8(ptrCSInfo infoSeg,uint8_t* infoArray)
{
	int ptr=0;uint8_t 
	*infoElem=(uint8_t*)infoSeg;
	for(ptr=0;ptr<sizeof(CSInfoS);ptr++){
		infoArray[ptr] = (*(infoElem+ptr));
	}
}

传入一个结构体的指针,并传入一个对应大小(uint8_t)类型的数组,用来装结构体拆分出来的元素。

那么数组需要多大呢?我们知道8位(bit)就是一个字节(Byte),所以这个数组理论上只需要和这个结构体的字节数一样大就可以了!也就是:

sizeof(CSInfoS)

的返回值。这里我们也可以口算一下,结构体中总共有8个float类型的数据,也就是8×32bit=8×4Byte=32Byte。结构体的大小也就是32字节,所以可以拆分成32个unit8_t类型的元素,数组大小也就需要32。

注意:传入的数组需要足够的大小,不要整个空指针或者不够大的数组进去。当然,你也可以返回一个数组,但我喜欢这种隐式返回的风格。

2、封包

选定一组特定的数据作为数据包的头部,选定另一组特定的数据作为数据包的尾部,方便我们在上位机接收数据后找到每一组数据的开始和结尾。

这里我们选定:

0x80 0x81 0x82

作为数据包的头部,同时选定:

0x82 0x81 0x80

作为数据包的尾部。所以我们向上位机发送的单个数据包都是如下形式的:

/**
	@part 通信协议
	@Protocol
    ------------------------------------------------------------
         头       |            信息              |      尾
	------------------------------------------------------------
	0x80|0x81|0x82|         CSInfoStrcutre       |0x82|0x81|0x80                 
	------------------------------------------------------------
	    3Byte     |            32Byte            |     3Byte 
    ------------------------------------------------------------
*/

上面|CSInfoStrcutre|的位置就是我们在上一步获得的uint8_t类型的数组infoArray,内容是CSInfoStrcutre中的数据。

封包的过程如下面的代码所示:

/**
  * @brief  按协议打包
  * @param  
		package 打包结果,按协议结果为3+32+3=38字节 (38*8bit)
	    infoArray 由数据段重组的uint8类型数组 | 结果	
		infoSize 数据段的大小--占用内存字节数(协议规定为32Byte)
  * @retval 无
  */
void CSInfo_Pack(uint8_t* infopackage,uint8_t* infoArray,uint8_t infoSize)
{
	uint8_t ptr=0;
	infopackage[0] = HEAD1;
	infopackage[1] = HEAD2;
	infopackage[2] = HEAD3;
	
	
	/* 将信息封如入数据包中 */
	for(;ptr<infoSize;ptr++){
		infopackage[ptr+3] = infoArray[ptr];
	}
	
	infopackage[ptr+3] = TAIL1;
	infopackage[ptr+4] = TAIL2;
	infopackage[ptr+5] = TAIL3;
}

3、发送

接着,我们将把这个玩意儿(infopackage)通过串口发送出去:

/**
* @brief  将数据包发送到上位机
  * @param  
			infoPackage 数据包
			packSize 数据包的大小--占用内存字节数(协议规定为38Byte)
  * @retval 无
  */
void CSInfo_SendPack(uint8_t* infoPackage,uint8_t packSize)
{
	int ptr=0;
	for(ptr=0;ptr<packSize;ptr++){
		USART_SendByte(infoPackage[ptr]);
	}
}

注意,为了方便使用,这里我们用到了一个名为USART_SendByte的自定义函数,其定义如下:

 /**
  * @brief  通过USART通道向上位机发送一个字节(8bit)的数据
  * @param  byte 要发送的8位数据
  * @retval 无
  */
void USART_SendByte(uint8_t byte)
{
	/* 发送一个字节数据到串口 */
	USART_SendData(DEBUG_USARTx,byte);
	/* 等待发送完毕 */
	while (USART_GetFlagStatus(DEBUG_USARTx, USART_FLAG_TXE) == RESET);		
}

到这里,我们就了解完了下位机打包发送的部分,接下来我们转到上位机视角,看一下咋个接收数据,咋个解析数据,也就是咋个把数据又装回一个结构体里,方便我们引用。

上位机接收数据并解包

回顾一下文章开头,我们说上位机的这部分工作的流程是这样的:

1、把串口里的数据读取出来
2、找到包头,
3、把数据包中对应数据的部分按顺序装填到结构体中

大致流程如下面的代码所示:

    /* 读取数据 */
    uint8_t packages[INFOSIZE*3]={0};
    int numHasRead = readInfoFromSerialport(packages);
    /* 解析数据 */
    uint8_t infoArray[INFOSIZE];
    /* 提取数据包 */
    bool readable = CSInfo_GetInfoArrayInpackages(infoArray,packages,numHasRead);
    /* 解包 */
    if(readable)
        CSInfo_InfoArray2CSInfoS(infoArray,this->_ptrCSInfo);

也即是依次调用readInfoFromSerialport、CSInfo_GetInfoArrayInpackages、CSInfo_InfoArray2CSInfoS在这三个函数,从串口缓冲区的一堆数据里找到一个完整的数据包并把它装填到结构体里。

下面详细介绍这三个自定义函数:

1、读取数据

你可以用你所知的任何方法从串口的缓冲区读取出来,只要你能把它们放到一个方便后续的解包操作访问的地方。

这里我使用Qt开发的上位机界面,故而也顺带使用Qt提供的serialport类中的方法来读取,具体可以参考Qt的帮助文档,这里只做简要说明:

/**
  * @brief  把当前serialport缓冲区的数据全部读取到一个uint8类型的数组中
  * @param
        packages 从串口读取到的包含数据包的数据
  * @retval
  *     numHasRead 从缓冲区读取到的字节数
  */
int readInfoFromSerialport(uint8_t* packages)
{
    int numHasRead(0);
    /* 没有可用的串口设备则中止读取操作 退出函数 */
    if(QSerialPortInfo::availablePorts().isEmpty())
        return 0;
    _port = new QSerialPort(QSerialPortInfo::availablePorts()[0]);
    _port->setPort(QSerialPortInfo::availablePorts()[0]);
    if(!_port->open(QIODevice::ReadWrite)){
        goto next;
    }else{
        _port->setParity(QSerialPort::NoParity);
        _port->setBaudRate(QSerialPort::Baud115200);
        _port->setDataBits(QSerialPort::Data8);
        _port->setStopBits(QSerialPort::OneStop);
        _port->setFlowControl(QSerialPort::NoFlowControl);
        /* 开始从serialport读取数据 */
        /* 读取串口缓冲区所有的数据到CSInfo的缓冲区infoArray */
        _port->waitForReadyRead();
        QByteArray dataArray = _port->read(200);
        numHasRead = dataArray.size();
        if(INFOSIZE*3<numHasRead){
            for(int i=0;i<INFOSIZE*3;i++){
                *(packages+i) = dataArray[i];
            }
        }
    }

    next:;
    delete  _port;
    return numHasRead;
}

上述代码首先获取了一个serialport类的对象_port,然后通过一系列的setxxx函数配置了必要的参数。接着调用readAll把串口中所有的数据读取到dataArray(readAll()的返回值就是一个QByteArray类型的容器),然后把大小等同于三个infoStructure的数据放到packages中,预备进行下一步的解包操作。

注意,之所以要读取三个基数,是为了保证至少包含一个完整的数据包。

2、找到一个完整的数据包

前面提到了,我们设定每个数据包的头部是0x80|0x81|0x82,而数据包的尾部则反过来。根据这个特征:
/**
	@part 通信协议
	@Protocol
    ------------------------------------------------------------
         头       |            信息              |      尾
	------------------------------------------------------------
	0x80|0x81|0x82|         CSInfoStrcutre       |0x82|0x81|0x80                 
	------------------------------------------------------------
	    3Byte     |            32Byte            |     3Byte 
    ------------------------------------------------------------
*/

我们可以先在上一步获得的packages中找到一个数据包的头部,以确定一个数据包的开始位置:

/**
  * @brief  在串口读取到的数据中提取出一个数据包的数据段(CSInfoStructure对应的部分)
  * 转存到infoArray中,供后续解析为CSInfoStructure.
  * @param
        infoArray 转存CSInfo的数组
        packages 从串口读取到的包含数据包的数据
        sizepackages 从串口读取到的字节数(packages的大小)
  * @retval 无
  */
bool CSInfo_GetInfoArrayInpackages(uint8_t* infoArray,uint8_t* packages,int sizepackages)
{
    int ptr;bool readable(true);
    if(sizepackages<INFOSIZE*3){
        readable = false;
        return readable;
    }
    for(ptr=0;ptr<INFOSIZE*3;ptr++){
    // or: for(ptr=0;ptr<sizepackages-3;ptr++){ */
        if((packages[ptr]==HEAD1)&&(packages[ptr+1]==HEAD2)&&(packages[ptr+2]==HEAD3))
            break;
    }
    ptr += 3;
    for(int i=0;i<INFOSIZE;i++)
        infoArray[i] = packages[ptr+i];
    return readable;
}

通过调用这个函数,我们把packages中的一个完整的数据包的InfoStructure部分放到了infoArray中。接下看第三步,我们将把这个结构体的数据写入一个结构体中,真正还原它在下位机中的样子:

3、解析数据

直接把结构体当成一个数组,把数据依次填写进去就ok了

/**
  * @brief  把存有一个数据段的数组解析为一个CSInfoStructure,结果存到参数2对应的地址
  * @param
        infoArray 存有一个数据段的uint8类型的数组
        infoStrc 从串口读取到的字节数(packages的大小)
  * @retval 无
  */
void CSInfo_InfoArray2CSInfoS(uint8_t* const infoArray,ptrCSInfo infoStrc)
{
    uint8_t* u8PtrOStrc = (uint8_t*)infoStrc;
    for(int i=0;i<INFOSIZE;i++)
        *(u8PtrOStrc+i) = infoArray[i];
}

到这里,我们就完成了使用串口发送结构体的任务,而且了解了封包和解包的基本思路。

我把上位机的源代码链接放到这里,需要的可以单击自取。读取和解析的代码分别在Sources/CSInfoReader.c和Sources/CSInfoParser.c文件中。

三、嵌入式硬件通信接口协议-UART(五)数据包设计与解析

https://blog.csdn.net/DigCore/article/details/85710462

应用层数据包设计思路

回到工程本身,帧结构中的数据包才是应用程序最终需要解析使用的,且与具体的业务需求有关。

这篇文章将简单介绍,在数据包里如何设计应用层的交互指令,从而实现具体的业务需求。分享个思路,就当抛砖引玉了。

类似于帧结构,在设计数据包时,根据交互逻辑的具体需求,同样采用逐字节组成字段,字段组成数据包,从而完成指令交互。

具体到项目中,一般地有目标地址、源地址、指令类型、传输方向、级联序号、参数ID、参数值等等。

字段的定义因项目需求而定,以上提及的字段可能存在且不限于此。

以下介绍在具体项目中,对数据包设计与解析思路。工程实践中方法众多,相信很多经验娴熟的老工程师肯定都有各自巧妙的编程思路,欢迎在本页留言交流。

项目案例

基于nRF51822的BLE终端设备,与上位机使用UART通信,物理线路使用USB转UART。

常规解析过程

解析函数,一般地会把输入参数的 *indata,利用一个新的结构体指针指向该输入参数,之后的解析使用结构体指针来对数据处理,增强代码可读性!
常规的判断处理,多采用switch(){case :}联合if(…){;}else(…){;}判断逻辑,这个模式的判断处理架构如下:

以上的做法,依次去判断类型type、参数名para,然后直接处理。当这两个字段的枚举成员数量少,倒还可以这么判断;但是如果工程需要扩展、业务有了新的需求,那么if(…){;}else(…){;}的逐一判断将会使得解析函数里的代码量巨大!

总结有这几个缺点:
1.业务需求有多少个类型或者其他分支,就需要多少个这样的判断逻辑,对于编写代码变成个体力活;
2.在代码查看、维护时,面对的还是罗列了一大堆的switch(){case :}和if(…){;}else(…){;}语句;
3.增删功能时,需要找到代码中具体的判断位置,然后小心翼翼给注释或者修改掉。

这里已经没有任何的技术含量,基本上就是复制粘贴判断语句、修改判断对象,说到底也就是个查表的过程!

构建查表方式解析

既然要查表,当然是有个while()循环,然后递增某一变量来查表的过程。在这里,数据包结构体中定义的类型type、参数名para,都可以作为查表的对象,该如何选择?

假设:
1.以类型作为查表对象,假如查表后类型等于查询参数,那么参数名仍然是个多个分支的情况,要么继续查表要么继续采用switch(){case :}或者if(…){;}else(…){;}来判断众多不同的参数名;
2.以参数名作为查表对象,假如查表后参数名等于设备运行状态,那么类型需要做最多三种判断:查询、设置、其他。

对比以上两种,必然是第2个更能提高编程效率、缕清逻辑框架。

要查表就要建表,建表的结构体,以参数名para作为被查对象,并且以回调函数的形式执行查表结果。建表如下:

在执行数据包解析的时候,查表的思路是:

1.先创建一个表结构的指针ptable指向表的开始位置,也就是指向数组内第一个元素{ECHO, dcapp_dev_echo}
2.再创建一个数据包结构的指针
pbuf指向输入数据首地址
3.通过递增ptable指针,对ptable与pbuf的参数名成员进行比对
4.最后执行ptable指针对应回调函数

以上的思路,放到代码中,仅仅数行就可以实现对输入数据包参数名的解析!高效、清晰!

另外,建表时,把无效参数名对应的值和对应的回调函数放在最后,这样做的好处是查完整个表,无需区分是否找到对应的参数名,而直接执行指针对应的回调函数即可。

这样即使是未找到参数名,也会执行表中最后一个元素,就是错误解析的回调函数dcapp_parser_err()。

有了这样一个查表的处理方式,增删指令功能就变得简单太多了!增加功能,只需要在表中添加参数名和对应的回调函数,删除某功能,也是回到表中找到对应的参数名和回调函数即可!

总结一下,虽然查表方式非常清晰,但是对应的回调函数内部,需要独自处理和实现,并且每个参数名都需要单独处理。相比于采用switch(){case :}联合if(…){;}else(…){;}判断逻辑,确实清晰很多。

以上的查表思路,来源于经历的项目,同时还参考了

《STM32CubeExpansion_MEMSMIC1_V1.1》

这个ST官方的数字麦克风开源项目示例,作为USB音频设备时,类似的回调函数方式:

四、DL-T 645协议格式或者DL-T 698协议格式的数据帧的串口解析思路

https://blog.csdn.net/weixin_40872563/article/details/95076982

题目要求如下:

用C语言写一个程序,此程序持续从串口读取数据(串口速率是115200,偶校验),串口数据有可能包含DL-T 645协议格式或者DL-T 698协议格式的数据帧(协议见word文档)。
要求把收到的符合以上两种格式的数据帧检出来。
附加要求:注意程序的可扩展性(如果以后此程序再支持其他协议数据解析,要容易扩展)。*
由于文档比较内容繁琐,特贴出来,协议格式

DL-T 645协议格式
(以下简称协议A)

在这里插入图片描述
DL-T 698协议格式
(以下简称协议B)
在这里插入图片描述

串口解析思路

 - 一、串口数据接收: 	
 		定义一个1024字节的buf,将串口接收到的数据依次追加到此buf中; 
 - 二、解析串口数据流程:
 		1、从buf中检索起始字符0x68的位置,->2
 		2、去匹配是否符合协议A,会有三种解析结果
 			a.解析到完整的一帧数据,->5
 			b.数据未接收完 ->3
 			c.解析不满足规则 ->3
 		3、去匹配是否符合协议B,会有三种解析结果
 			a.解析到完整的一帧数据, ->5
 			b.数据未接收完 ->6
 			c.解析不满足规则
 				协议A也不满足规则 ->7
 				协议A未接收完 ->6
 		5、解析到完整的一帧数据,->10
 		6、协议匹配未接收完 ->9
 		7、两个协议解析都不满足,->8
 		8、从1中的位置继续寻找下一个0x68的位置
 			a.找到0x68    ->1
 			b.未找到0x68 ->6
 		9、继续循环,等待串口数据过来
 		10、解析完成,将buf中剩余数据前移到位置0,

代码如下:
先定义两种协议对应的结构体

typedef struct dl_t_698
{
    uint8_t st_byte;//起始字符
    uint16_t data_length;//长度域--此处注意大小端的问题
    uint8_t control_byte;//控制域
    uint8_t address_bytes[100];//地址域
    uint16_t frame_head_hcs;//帧头校验
    uint8_t user_data[100];//用户数据
    uint16_t frame_crc_fcs;//帧校验
    uint8_t end_byte;//结束字符
}dlt_698_frame_body;

typedef struct dl_t_645
{
    uint8_t st_byte;//帧起始符
    uint8_t address_bytes[5];//地址欲
    uint8_t mid_st_byte;//帧起始符
    uint8_t control_byte;//控制码
    uint16_t data_length;//长度域--此处注意大小端的问题-- 前面字符长度为10
    uint8_t user_data[100];//数据data
    uint16_t frame_crc_cs;//校验
    uint8_t end_byte;//结束字符
}dlt_645_frame_body;

//解析到一帧数据可能出现的情况
typedef enum frame_result
{
    UNKNOWN,
    OK,               //成功找到一帧
    UNFINISHED,//未接收完成
    ERROR,        //不满足此协议
} frame_result_t;

//定义协议类型
typedef enum protocol_type {
    PROTOCOL_UNKNOWN,
    PROTOCOL_DL_T_698,
    PROTOCOL_DL_T_645,
    PROTOCOL_OTHER,
}protocol_type_t;

char uart_rcvd_buf[UART_BUFFER_LEN];//接收串口发送过来的数据
char frame_buf[FRAME_BUFFER_LEN];//用来存取一帧数据
uint16_t uart_rcvd_pos = 0;//当前buf接收到的数据长度

dlt_698_frame_body s_dlt_698_frame_body;
dlt_645_frame_body s_dlt_645_frame_body;

/*
 * 功能:接收串口过来的数据
 **/
/*void uart_rev_data(uint8_t data)
{
    uart_rcvd_buf[uart_rcvd_len] = data;
    uart_rcvd_len++; 

    if (uart_rcvd_len >= UART_BUFFER_LEN) {
        //清空所有命令
        uart_rcvd_len = 0;
        //my_memset(uart_rcvd_buf,0,sizeof(uart_rcvd_buf));
    } 
}*/

/*
 * 该函数由库函数调用
 **/
/*void UART_INT_Func(void)
{
    uint8_t u8TempData=0;

    if(1 == M0P_UART1->ISR_f.RI)
    {    
        u8TempData = M0P_UART1->SBUF_f.SBUF; 
        uart_rev_data(u8TempData);          
        
        M0P_UART1->ICR_f.RICLR = 0;
    }    
}*/

/*
 * 功能:检索一帧数据将值赋给结构体       
 * 校验OK return 1;
 **/
uint8_t parse_dlt645_frame(char *p_frame, uint16_t frame_len, dlt_645_frame_body* sframe_body) {
    uint16_t temp16_t = 0;//一帧数据的总长度
    uint16_t i = 0;

    //计算校验码
    for (i = 0; i < frame_len - 3; i ++) {
        temp16_t += p_frame[i];
    }
    if (temp16_t == p_frame[frame_len - 3] | p_frame[frame_len - 2]) {
        sframe_body->st_byte = p_frame[0];
        for (i = 0; i < 6; i ++) {
            sframe_body->address_bytes[i] = p_frame[1+i];

        }
        sframe_body->mid_st_byte = p_frame[7];
        sframe_body->control_byte = p_frame[8];
        temp16_t = (p_frame[9]<<8) |p_frame[10];
        sframe_body->data_length = temp16_t;
        for (i = 0; i < temp16_t; i ++) {
            sframe_body->user_data[i] = p_frame[11 + i];
        }
        sframe_body->frame_crc_cs = p_frame[frame_len - 3] | p_frame[frame_len - 2];
        sframe_body->end_byte = p_frame[frame_len - 1];
        
        return 1;
    }
    return 0;
} 

/*
 * 功能:检索一帧数据将值赋给结构体               
 **/
uint8_t parse_dlt698_frame(char *p_frame, uint16_t frame_len, dlt_698_frame_body* sframe_body) {
    uint16_t temp16_t = 0;//一帧数据的总长度
    uint16_t adr_temp16_t = 0;//地址域的地址长度
    uint16_t i = 0;

    //校验
    for (i = 0; i < frame_len - 3; i ++) {
        temp16_t += p_frame[i];
    }
    if (temp16_t == p_frame[frame_len - 3] | p_frame[frame_len - 2]) {
        
        sframe_body->st_byte = p_frame[0];
        temp16_t = ((p_frame[1]<<8) |p_frame[2]) & 0x3FFF;
        sframe_body->data_length = temp16_t;
        sframe_body->control_byte = p_frame[3];
        sframe_body->address_bytes[0] = p_frame[4];//地址域第一个字节
        adr_temp16_t = p_frame[4] & 0x0F;
        for (i = 0; i < adr_temp16_t; i ++) {
            sframe_body->address_bytes[i] = p_frame[5 + i];
        }
        sframe_body->frame_head_hcs = (p_frame[6 + adr_temp16_t - 1] >> 8) | p_frame[6 + adr_temp16_t];
        for (i = 0; i < temp16_t; i ++) {
            sframe_body->user_data[i] = p_frame[adr_temp16_t + 7];
        }
        sframe_body->frame_crc_fcs = p_frame[frame_len - 3] | p_frame[frame_len - 2];
        sframe_body->end_byte = p_frame[frame_len - 1];
        
        
        return 1;
    }
    return 0;
} 

/*
 * 功能:从缓存区buf中检索dlt645帧数据
 * 将一帧数据读取到frame_buf中     
 * line:缓存区0x68开头的数据
 * out:将捡出来的帧复制到该数组中
 * frame_len:捡出来的帧的长度,
 * line_len:缓存区buf中0x68开头的数据长度
 **/
frame_result_t find_dlt645_frame(char* line, char* out, uint16_t* frame_len, uint16_t line_len) {
    uint16_t frame_length = 0;//一帧数据的总长度
    uint16_t temp_len = 0;

    if (line_len < DLT_645_LEAST_LEN) {
        return UNFINISHED;
    }

    //判断第七位
    if (line[7] != 0x68) {
        return ERROR;
    }

    frame_length = 9;/*帧起始符+地址域+帧起始符+控制域*/
    temp_len = (line[9]<<8) |line[10];//数据data的长度
    printf("645 data len = %d\n", temp_len);
    frame_length = frame_length + 2 + temp_len;/*2-长度域占的字节*/
    frame_length += 3;/*校验码和结束符*/
    if (frame_length > FRAME_BUFFER_LEN) {
        //超过单包缓存区的最大长度
        return ERROR;
    } else {
        if (frame_length <= line_len) {
            if (line[frame_length - 1] == 0x16) {
                //检到一帧数据                
                for (temp_len = 0; temp_len < frame_length; temp_len ++) {
                    out[temp_len] = *line;
                    line++;
                }
                *frame_len = frame_length;
                return OK;
            } else {
                //不满足此协议的0x16结束符
                return ERROR;
            }
        } else {
            //数据还没接收完整
            return UNFINISHED;
        }
    }
    
    return UNKNOWN;
}

/*
 * 功能:从缓存区buf中检索dlt698帧数据
 * 将一帧数据读取到frame_buf中     
 * line:缓存区0x68开头的数据
 * out:将捡出来的帧复制到该数组中
 * frame_len:捡出来的帧的长度,
 * line_len:缓存区buf中0x68开头的数据长度
 **/
frame_result_t find_dlt698_frame(char* line, char* out, uint16_t* frame_len, uint16_t line_len) {
    uint16_t frame_length = 0;//一帧数据的总长度
    uint16_t temp_len = 0;

    if (line_len < DLT_698_LEAST_LEN) {
        return UNFINISHED;
    }
    
    frame_length = 4;/*起始符+长度域+控制域*/
    //地址域
    temp_len = line[4] & 0x0F;
    //printf("698 address len = %d\n", temp_len);    
    frame_length = frame_length + 1 + temp_len;
    //帧头校验
    frame_length += 2;
    //用户数据长度
    temp_len = ((line[1]<<8) |line[2]) & 0x3FFF;
    //printf("698 data len = %d\n", temp_len);
    frame_length += temp_len;//data长度
    //
    frame_length += 3;//帧校验+结束符
    if (frame_length > FRAME_BUFFER_LEN) {
        //超过单包缓存区的最大长度
        return ERROR;
    } else {
        if (frame_length <= line_len) {
            if (line[frame_length - 1] == 0x16) {
                //检到一帧数据                
                for (temp_len = 0; temp_len < frame_length; temp_len ++) {
                    out[temp_len] = *line;
                    line++;
                }
                *frame_len = frame_length;
                return OK;
            } else {
                //不满足此协议的0x16结束符
                return ERROR;
            }
        } else {
            //数据还没接收完整        
            return UNFINISHED;
        }
    }

    return UNKNOWN;
}


/*
 * 功能:协议数据解析
 **/
void parse_buf (void)
{
    uint16_t frame_length = 0;//一帧数据的总长度
    uint16_t i = 0, temp_len = 0;
    uint8_t has_content = 0;//buf中是否有数据
    uint8_t frame_error = 0;//缓存区当前的数据对所有协议都不满足
    char* p_buf;
    protocol_type_t protl_type = PROTOCOL_UNKNOWN;
    frame_result_t find_frame_re = UNKNOWN;

    //用来保存每个协议解析后的结果 
    //frame_results[0] 保存PROTOCOL_DL_T_645协议解析结果
    //frame_results[1] 保存PROTOCOL_DL_T_698协议解析结果
    frame_result_t frame_results[2] = {UNKNOWN, UNKNOWN}; 

    has_content = uart_rcvd_pos > 2;
    while (has_content) {
        p_buf = uart_rcvd_buf;
        printf("p_buf = %#x\n", *p_buf);
        //检索0x68开头的数据
        while (*p_buf != 0x68 && p_buf < uart_rcvd_buf + uart_rcvd_pos) {
            p_buf ++;
        }
        
        if (p_buf == uart_rcvd_buf + uart_rcvd_pos) {
            //检索当前包数据,都不包含,清空
            uart_rcvd_pos = 0;
            break;
        }

        //uart_rcvd_buf中剩余的数据长度
        temp_len = uart_rcvd_pos - (p_buf - uart_rcvd_buf);

        printf("while start has_content uart_rcvd_pos - (p_buf - uart_rcvd_buf) = %d\n", temp_len);
        
        //以下处理不包含校验
        switch(protl_type) {
            case PROTOCOL_UNKNOWN:
                memset(frame_buf,0,sizeof(frame_buf));
                find_frame_re = UNKNOWN;
                frame_error = 0;
                frame_length = 0;
                for (i = 0; i < 3; i ++) {
                    frame_results[i] = UNKNOWN;
                }
                
            case PROTOCOL_DL_T_645:
                find_frame_re = find_dlt645_frame(p_buf, frame_buf, &frame_length, temp_len);
                frame_results[0] = find_frame_re;
                if (find_frame_re == OK) {
                    printf("\nfind dlt_645 OK frame_buf = %s, frame_length = %d\n", frame_buf, frame_length);
                    printf("\n");                    
                    memset(&s_dlt_645_frame_body, 0, sizeof(dlt_645_frame_body));                
                    if (parse_dlt645_frame(frame_buf, frame_length, &s_dlt_645_frame_body)) {
                        //解析到一包有效数据
                    }
                    break;
                }
                
            case PROTOCOL_DL_T_698:
                find_frame_re = find_dlt698_frame(p_buf, frame_buf, &frame_length, temp_len);
                frame_results[1] = find_frame_re;
                if (find_frame_re == OK) {
                    printf("\nfind dlt_698 OK frame_buf = %s, frame_length = %d\n", frame_buf, frame_length);
                    printf("\n");
                    memset(&s_dlt_698_frame_body, 0, sizeof(dlt_698_frame_body));

                    break;
                }

            case PROTOCOL_OTHER:
                //此处添加其他协议解析
                //break;
                
            default :
                if (frame_results[0] == ERROR && frame_results[1] == ERROR) {
                    //缓存区的数据不满足现有协议的解析
                    //继续找下一个0x68起始符
                    p_buf ++;//跳过当前的0x68
                    //检索0x68开头的数据
                    while (*p_buf != 0x68 && p_buf < uart_rcvd_buf + uart_rcvd_pos) {
                        p_buf ++;
                    }
                    
                    if (p_buf == uart_rcvd_buf + uart_rcvd_pos) {
                        //检索当前包数据,都不包含,清空
                        uart_rcvd_pos = 0;
                        break;
                    }

                    //找到下一条0x68开头的数据帧
                    frame_error = 1;
                    
                }
                break;
        }


        //当成功检索到一帧数据或缓存区的数据不满足现有协议的解析
        //buf中剩余的有效数据前移
        if (find_frame_re == OK || frame_error) {
            //uart_rcvd_buf剩余的数据长度
            temp_len = uart_rcvd_pos - (p_buf - uart_rcvd_buf) - frame_length;
            if (temp_len > 0) {
                //当前uart_rcvd_buf中剩余的数据前移
                for (i = 0; i < temp_len; i ++) {
                    uart_rcvd_buf[i] = *(p_buf + frame_length + i);
                    *(p_buf + frame_length + i) = 0x00;
                }
                has_content = 1;//继续循环解析
            } else {
                //解析过的位清空
                for (i = 0; i < (p_buf - uart_rcvd_buf) + frame_length; i ++) {
                    uart_rcvd_buf[i] = 0x00;
                }
                has_content = 0;
            }
            uart_rcvd_pos = temp_len;
        } else {
            has_content = 0;
        }
        printf("while end has_content = %d, uart_rcvd_pos = %d\n", has_content, uart_rcvd_pos);
        
    }

}


int main(void)
{

    uint16_t timer;

    //RCH 24MHz 使用内部时钟
    Clk_SwitchTo(ClkRCL);
    Clk_SetRCHFreq(ClkFreq24Mhz);
    Clk_SwitchTo(ClkRCH);

    //enable module clk
    M0P_CLOCK->PERI_CLKEN_f.GPIO = 1;  //打开GPIO的clk
    M0P_CLOCK->PERI_CLKEN_f.BASETIM = 1;
    M0P_CLOCK->PERI_CLKEN_f.UART1   = 1;
    M0P_CLOCK->PERI_CLKEN_f.I2C   = 1;

    //UART init    
    Gpio_SetFunc_UART1TX_P23();
    Gpio_SetFunc_UART1RX_P24();    

    M0P_UART1->SCON_f.DBAUD = 1;    //双倍波特率
    timer = 0x10000-((24000000*2)/(115200*32));  //单倍波特率,定时器配置
    //使用basetimer1作为串口的波特率产生器   
    M0P_BT1->CR_f.GATE_P = 0u;
    M0P_BT1->CR_f.GATE   = 0u;
    M0P_BT1->CR_f.PRS    = 0u;
    M0P_BT1->CR_f.TOG_EN = 0u;
    M0P_BT1->CR_f.CT     = 0u;         //定时器模式
    M0P_BT1->CR_f.MD     = 1u;         //重载模式
    M0P_BT1->ARR_f.ARR = timer;
    M0P_BT1->CNT_f.CNT = timer;
    M0P_BT1->CR_f.TR = TRUE;

    M0P_UART1->SCON_f.SM01 = 0x1;     //模式1
    M0P_UART1->SCON_f.SM2  = 0;       //多主机通信disable

    EnableNvic(UART1_IRQn, 3u, TRUE);
    M0P_UART1->SCON_f.TIEN = 0;
    M0P_UART1->SCON_f.RIEN = 1;    
    M0P_UART1->ICR_f.RICLR = 0;
    M0P_UART1->ICR_f.TICLR = 0;    
    M0P_UART1->SCON_f.REN = 1;

测试代码

char* p_temp;
    uint8_t i = 0;
    char dlt_645_frame_msg[38] = {0x06, 0x06,0x68, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x68,0x03, 
                                                                    0x00,0x04, 0x0D, 0x0D, 0x0D, 0x0D, 0x02, 0x0A, 0x16, 
                                                                    0x68, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x68,0x03, 
                                                                    0x00,0x04, 0x0D, 0x0D, 0x0D, 0x0D, 0x02, 0x0A, 0x16};

    char dlt_698_frame_msg[34] = {0x06, 0x06,0x68, 0x00, 0x05, 0x0C, 0x01, 0x0A, 0x00, 0xCC,
                                                                    0x0A, 0x0D, 0x0D, 0x0D, 0x0E, 0x0F, 0xCC, 0x16,
                                                                    0x68, 0x00, 0x05, 0x0C, 0x01, 0x0A, 0x00, 0xCC,
                                                                    0x0A, 0x0D, 0x0D, 0x0D, 0x0E, 0x0F, 0xCC, 0x16 };
    
    char error_frame_msg[20] = {0x06, 0x06,0x68, 0x00, 0x05, 0x0C, 0x01, 0x0A, 0x00, 0xCC, 
                                                0x16, 0x06,0x68, 0x60, 0x05, 0x01, 0x01, 0x0A, 0x00, 0xCC};


    char dlt_645_frame_msg_half_st[29] = {0x06, 0x06,0x68, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x68,0x03, 
                                                                    0x00,0x04, 0x0D, 0x0D, 0x0D, 0x0D, 0x02, 0x0A, 0x16, 
                                                                    0x68, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x68,0x03};

    char dlt_645_frame_msg_half_end[9] = {0x00,0x04, 0x0D, 0x0D, 0x0D, 0x0D, 0x02, 0x0A, 0x16};
    
    
    p_temp = uart_rcvd_buf;


//DLT_645 TEST
#if 0

    printf("\n ****** dlt 645 start ******\n");

    //stpcpy(uart_rcvd_buf, dlt_645_frame_msg);
    //strcat(uart_rcvd_buf, end_byte);
    memcpy(uart_rcvd_buf, dlt_645_frame_msg, sizeof(dlt_645_frame_msg));
    uart_rcvd_pos = sizeof(dlt_645_frame_msg);
    printf("main start 645 msg uart_rcvd_buf = %s, uart_rcvd_pos = %d\n", uart_rcvd_buf, uart_rcvd_pos);
    parse_buf();
    printf("\n ****** dlt 645 end ******\n");

#endif


//DLT_698 TEST
#if 0
    printf("\n ****** dlt 698 start ******\n ");
    memcpy(uart_rcvd_buf, dlt_698_frame_msg, sizeof(dlt_698_frame_msg));
    uart_rcvd_pos = sizeof(dlt_698_frame_msg);
    printf("main start 698 msg uart_rcvd_buf = %s, uart_rcvd_pos = %d\n", uart_rcvd_buf, uart_rcvd_pos);
    parse_buf();
    printf("\n ****** dlt 698 end ******\n ");

#endif


//ALL TEST
#if 0

    printf("\n ****** dlt 698 and 645 start ******\n ");
    memcpy(uart_rcvd_buf + uart_rcvd_pos, dlt_698_frame_msg, sizeof(dlt_698_frame_msg));
    uart_rcvd_pos += sizeof(dlt_698_frame_msg);
    memcpy(uart_rcvd_buf + uart_rcvd_pos, dlt_645_frame_msg, sizeof(dlt_645_frame_msg));
    uart_rcvd_pos += sizeof(dlt_645_frame_msg);
    
    printf("main start 698 and 645 msg uart_rcvd_buf = %s, uart_rcvd_pos = %d\n", uart_rcvd_buf, uart_rcvd_pos);
    //parse_buf();
    printf("\n ****** dlt 698 and 645 end ******\n ");

#endif

//Error msg TEST
#if 1
    printf("\n ****** dlt error msg start ******\n ");
    memcpy(uart_rcvd_buf + uart_rcvd_pos, error_frame_msg, sizeof(error_frame_msg));
    uart_rcvd_pos += sizeof(error_frame_msg);
    printf("main start error msg uart_rcvd_buf = %s, uart_rcvd_pos = %d\n", uart_rcvd_buf, uart_rcvd_pos);
    //parse_buf();
    printf("\n ****** dlt error msg end ******\n ");

#endif


//Half test
#if 1

    printf("\n ****** dlt 645 half start ******\n ");
    memcpy(uart_rcvd_buf + uart_rcvd_pos, dlt_645_frame_msg_half_st, sizeof(dlt_645_frame_msg_half_st));
    uart_rcvd_pos += sizeof(dlt_645_frame_msg_half_st);
    parse_buf();

    printf("\n ****** dlt 645 half middle +++ ******\n ");
    
    memcpy(uart_rcvd_buf + uart_rcvd_pos, dlt_645_frame_msg_half_end, sizeof(dlt_645_frame_msg_half_end));
    uart_rcvd_pos += sizeof(dlt_645_frame_msg_half_end);

    printf("main start 645 half msg uart_rcvd_buf = %s, uart_rcvd_pos = %d\n", uart_rcvd_buf, uart_rcvd_pos);
    printf("\n ****** dlt 645 half end ******\n ");

#endif

//ALL TEST
#if 1

    printf("\n ****** dlt 698 and 645 start ******\n ");
    memcpy(uart_rcvd_buf + uart_rcvd_pos, dlt_645_frame_msg, sizeof(dlt_645_frame_msg));
    uart_rcvd_pos += sizeof(dlt_645_frame_msg);
    memcpy(uart_rcvd_buf + uart_rcvd_pos, dlt_698_frame_msg, sizeof(dlt_698_frame_msg));
    uart_rcvd_pos += sizeof(dlt_698_frame_msg);
    memcpy(uart_rcvd_buf + uart_rcvd_pos, dlt_645_frame_msg, sizeof(dlt_645_frame_msg));
    uart_rcvd_pos += sizeof(dlt_645_frame_msg);
    
    printf("main start 698 and 645 msg uart_rcvd_buf = %s, uart_rcvd_pos = %d\n", uart_rcvd_buf, uart_rcvd_pos);
    printf("\n ****** dlt 698 and 645 end ******\n ");

#endif

//Error msg TEST
#if 1
    printf("\n ****** dlt error msg start ******\n ");
    memcpy(uart_rcvd_buf + uart_rcvd_pos, error_frame_msg, sizeof(error_frame_msg));
    uart_rcvd_pos += sizeof(error_frame_msg);
    printf("main start error msg uart_rcvd_buf = %s, uart_rcvd_pos = %d\n", uart_rcvd_buf, uart_rcvd_pos);
    //parse_buf();
    printf("\n ****** dlt error msg end ******\n ");

#endif

    parse_buf();

    printf("main end msg uart_rcvd_buf = %s, uart_rcvd_pos = %d\n", uart_rcvd_buf, uart_rcvd_pos);
    
}

五、Qt 实现数据协议控制–组帧、组包、解析帧、解析包

https://blog.csdn.net/qq_21291397/article/details/109641476

数据传输中的组帧和组包

一、数据帧,数据包的概念

数据帧
数据传输往往都有一定的协议,通过CRC校验来验证数据的可靠性。数据帧包含三部分,帧头、数据部分、帧尾。其中帧头和帧尾包含一些必要的控制信息,比如同步信息,地址信息、差错控制信息等等。

组包
多个数据帧可以捆在一起,添加包头信息,就可以组包。组包可以使得多帧的数据同时发送,提高通信的效率。

数据的帧包可以提高数据传输的可靠性。

下面来介绍一种数据帧和包的封装:

组帧格式:

在这里插入图片描述
为了保证数据的可靠性,我们在帧结构中的长度,指令类型,数据,校验和数据包含5A556A69时需要转义,接收时也需要转义,以防止帧解析出现异常。

一帧数据只有一个指令。指令用于控制设备的状态等

组包格式:
在这里插入图片描述
这里我们将包头内容包含 版本信息和帧数据的长度信息。

按照该协议,我们可以串口传输,SOCKET TCP传输中来实现数据的发送和接收。

二、 程序实现:

这里我们讨论上位机SOCKET端的组帧和组包,以及解析帧和解包。我们下Qt中编写测试代码。

2.1、frame(帧)类的实现:

  1. 新建一个frame类,命名为frame。 在frame.h中我们如下设计

第一步:
设置数据区格式:

#define INT8U unsigned char
#define INT32U unsigned int
#define INT16U unsigned short  
#define MAX_MSG_LEN 128     
      
typedef struct _Msg_
{
    INT8U   length;
    INT8U		crc;
    INT8U		data[MAX_MSG_LEN];
}Msg,*pMsg;

第二步:
设计组帧和解析帧

bool PackFrame(Msg src, INT8U * dst, INT8U *len); //组包
INT8U UnpackFrame(INT8U ch, Msg *pmsg);   //解包

第三步:
因为我们还要实现对帧中的帧长度,数据区,校验中实现转义,于是我们定义两个函数:

INT8U protocol_convert(INT8U ch);  //转义
INT8U protocol_deconvert(INT8U ch);  //反转义

最后,我们添加校验函数

INT8U CRC8( INT8U*buffer, INT8U len);

因为在数据转义中,需要对帧的格式进行判断,我们这里设计一个枚举结构

enum FRAME_STATE
{undefined
F_ERROR = -1,
F_HEADER_H,
F_HEADER_L,
F_LENGTH,
F_DATA,
F_CRC,
F_END_H,
F_END_L,
F_OVER,
};

frame.h 预览如下:

#ifndef FRAME_H
#define FRAME_H

#include "encrypt/type.h"
#include "encrypt/encrypt.h"

# define MAX_MSG_LEN 128

#pragma pack(1)
typedef struct _Msg_
{
    INT8U   length;
    INT8U		crc;
    INT8U		data[MAX_MSG_LEN];
}Msg,*pMsg;
#pragma pack()


class Frame
{
public:
    Frame();

    bool PackFrame(Msg src, INT8U * dst, INT8U *len); //组包
    INT8U UnpackFrame(INT8U ch, Msg *pmsg);   //解包

private:
    enum FRAME_STATE
    {
        F_ERROR = -1,
        F_HEADER_H,
        F_HEADER_L,
        F_LENGTH,
        F_DATA,
        F_CRC,
        F_END_H,
        F_END_L,
        F_OVER,
    };

    Encrypt *_encrypt;    //加密对象

    int converter = 0;
    int data_point = 0;
    FRAME_STATE frame_state;

    INT8U protocol_convert(INT8U ch);  //转义
    INT8U protocol_deconvert(INT8U ch);  //反转义
    INT8U CRC8( INT8U*buffer, INT8U len);

};

#endif // FRAME_H

2、frame.cpp 设计如下:
校验:
这里我们通过加密类中的CRC来返回一个CRC校验值,当然我们也一个自定义一个CRC计算的算法来实现

INT8U Frame::CRC8( INT8U*buffer, INT8U len)
 {
     return _encrypt->CRC8(buffer, len);
 }

转义:

 INT8U Frame::protocol_convert(INT8U ch)
 {
     if ((converter == 1) && (ch == 0xA5))
     {
         converter = 0;
         ch = 0x5A;
     }
     else if ((converter == 1) && (ch == 0x66))
     {
         converter = 0;
         ch = 0x99;
     }
     else if ((converter == 1) && (ch == 0x95))
     {
         converter = 0;
         ch = 0x6A;
     }
     else if (converter == 1)
     {
         frame_state = F_ERROR;
     }
     return ch;
 }

反转义:

INT8U Frame::protocol_deconvert(INT8U ch)
 {
     INT8U rtn = 0;
     switch(ch)
     {
         case 0x5A:
                 rtn = 0xA5;
                 break;
         case 0x99:
                 rtn = 0x66;
                 break;
         case 0x6A:
                 rtn = 0x95;
                 break;
         default:
                 rtn = ch;
                 break;
     }
     return rtn;
 }

组帧和解析帧:

bool Frame::PackFrame(Msg src, INT8U * dst, INT8U *len)
{

     // 增加CRC校验
     src.crc = CRC8(src.data, src.length);

     dst[0] = 0x5A;
     dst[1] = 0x55;
     int8_t j = 2;
     // lenth
     if (src.length == protocol_deconvert(src.length))
     {
         dst[j++] = src.length;
     }
     else
     {
         dst[j++] = 0x99;
         dst[j++] = protocol_deconvert(src.length);
     }
     //data
     for (int i = 0; i < src.length; i++)
     {
         if (src.data[i] == protocol_deconvert(src.data[i]))
         {
             dst[j++] = src.data[i];
         }
         else
         {
             dst[j++] = 0x99;
             dst[j++] = protocol_deconvert(src.data[i]);
         }
     }
     //crc
     if (src.crc == protocol_deconvert(src.crc))
     {
         dst[j++] = src.crc;
     }
     else
     {
         dst[j++] = 0x99;
         dst[j++] = protocol_deconvert(src.crc);
     }

     dst[j++] = 0x6A; //packet tail1
     dst[j++] = 0x69; //packet tail2
     (*len) = j;

     return true;

 }


INT8U Frame::UnpackFrame(INT8U ch, Msg *pmsg)
{
  if ((ch == 0x5a) && (frame_state != F_HEADER_H) && (frame_state != F_CRC))
  {
      frame_state = F_HEADER_H;
  }
  if ((ch == 0x6a) && (frame_state != F_END_H) && (frame_state != F_CRC))
  {
      frame_state = F_ERROR;
  }

  if (frame_state == F_HEADER_H)
  {
      if (ch == 0x5A)
      {
          data_point = 0;
          frame_state = F_HEADER_L;
      }
      else
      {
          frame_state = F_ERROR;
      }
  }
  else if (frame_state == F_HEADER_L)
  {
      if (ch == 0x55)
      {
          frame_state = F_LENGTH;
      }
      else
      {
          frame_state = F_ERROR;
      }
  }
  else if (frame_state == F_LENGTH)
  {
      if (ch == 0x99)
      {
          converter = 1;
          return 0;
      }
      pmsg->length = protocol_convert(ch);
      if (pmsg->length > MAX_MSG_LEN)
      {
          frame_state = F_ERROR;
      }
      else
      {
          frame_state = F_DATA;
      }
  }
  else if (frame_state == F_DATA)
  {
      if (pmsg->length == 0)//没有数据区
      {
          frame_state = F_CRC;
          return 0;
      }

      if (ch == 0x99)    //转义
      {
          converter = 1;
          return 0;
      }

      pmsg->data[data_point] = protocol_convert(ch);
      data_point++;
      if (data_point == pmsg->length)
      {
          data_point = 0;
          frame_state = F_CRC;
      }
  }
  else if (frame_state == F_CRC)
  {
      if (ch == 0x99)    //转义
      {
          converter = 1;
          return 0;
      }
      pmsg->crc = protocol_convert(ch);
      frame_state = F_END_H;
  }
  else if (frame_state == F_END_H)
  {
      if (ch != 0x6A)
      {
          frame_state = F_ERROR;
      }
      else
      {
          frame_state = F_END_L;
      }

  }
  else if (frame_state == F_END_L)
  {
      if (ch != 0x69)
      {
          frame_state = F_ERROR;
      }
      else
      {
          // frame_state = FRAME_STATE.F_HEADER_H;
          //CRC success
          if (pmsg->crc == CRC8(pmsg->data, pmsg->length))
          {
              frame_state = F_HEADER_H;
              return 1;
          }
          else
          {
              frame_state = F_ERROR;
          }
      }
  }

  if (frame_state == F_ERROR)
  {
      frame_state = F_HEADER_H;
      return 2;
  }

  return 0;
}

在解析帧的过程中,我们用frame_state 作为协议状态机的转换状态,用于确定当前字节处于一帧数据中的那个部位,在数据包接收完的同时也进行了校验的比较。
接收过程中,只要哪一步收到的数据不是预期值,则直接将状态机复位,用于下一帧数据的判断,因此系统出现状态死锁的情况非常少,系统比较稳定。

2.2、Pack(包)类的实现:
packer.h

#ifndef PACKER_H
#define PACKER_H
#include<QList>
#include "protocal/frame.h"

const int packVersion  = 1;

class Packer
{
public:
    Packer();

    Frame *ptc;  //帧对象指针
    QList<Msg*> *lstMsg;// 解包后的通讯数据

    QByteArray  Pack(QList<Msg> lstMsg);      //组包
    QList<Msg*> *UnPack(INT8U * data, INT16U packLen);   //解包
};

#endif // PACKER_H

packer.cpp

#include "packer.h"
#include<QDebug>
#include<QString>

Packer::Packer()
{

   ptc = new Frame();
   lstMsg = new QList<Msg*>();
}


QByteArray Packer:: Pack(QList<Msg> lstMsg)
   {

    QByteArray pack;
    pack.resize(4);
    pack[0]= (uint8_t)((packVersion & 0xff00)>>8);
    pack[1] = (uint8_t)(packVersion &0xff);
    pack[2] = 0;
    pack[2] = 0;

    int pos = 4;

    Msg msg;

    int i = 0;
    foreach( msg , lstMsg)
    {
      INT8U dst[256];
      INT8U len = 0;
      ptc->PackFrame(msg,  dst,  &len);
      INT8U pre_len = pack.size() ;
      INT8U cur_len = pack.size() + len;
      pack.resize( cur_len);
      for(int j = pre_len; j<cur_len;j++ )
      {
         pack[j] = dst[j-pre_len];

      }

//      char * p_buf= new char[128]();
//      std::memcpy(p_buf,dst,len);
//      pack.append(p_buf);
      pos += len;
    }
    pos = pos - 4;
    pack[2] = (uint8_t)((pos & 0xff00) >> 8);
    pack[3] = (uint8_t)(pos & 0xff);

    return pack;
   }

 QList<Msg*> *Packer::UnPack(INT8U * data, INT16U packLen)   //packLen: 数据区的长度
  {
          if (data == NULL)
          {
              qDebug()<< "数据为空!";
              return NULL;
          }

          int version = data[0] << 8 | data[1];
          // 版本异常
          if (version != packVersion)
          {
              qDebug()<< "协议版本不正确!";
              return NULL;
          }

          int len = data[2] << 8 | data[3];
          //数据长度异常
          if (len + 4 > packLen)
          {
              qDebug()<<  "数据截断异常!" ;
              return NULL;
          }
          if(len + 4 < packLen)
          {
              qDebug()<<  "数据过长异常!" ;
          }

          Msg *pmsg = new Msg();
          packLen = (INT16U)(len + 4);
          for (int i = 4; i < packLen; i++)
          {
              INT8U ch = data[i];
              INT8U result = ptc->UnpackFrame(ch, pmsg);
              if (result == 1)
              {
                  lstMsg->append(pmsg);
                  pmsg = new Msg();
              }
          }

          return lstMsg;
      }

三、测试
我们在main() 函数中添加如下代码 进行测试:

    //解析帧测试

    unsigned char destdata[] = {0x00,0x01,0x00,0x1b,0x5A,0x55,0x15,0x81,0x31,
                         0xFF,0xD8,0x05,0x4E,0x56,0x33,0x36,0x25,0x39,
                         0x22,0x43,0x72,0xF7,0xFD,0x30,0x23,0x51,0x09,
                         0xEF,0x0A,0x6A,0x69};

     QList<Msg*> *testlist;

     Packer *testpacker = new Packer();

     testlist = testpacker->UnPack(destdata,31);


     QList<Msg*>::iterator i;
     for (i = testlist->begin(); i != testlist->end(); ++i)

     {
         for(int j = 0;j<(*i)->length;j++)
         {
             qDebug()<<QString::number((*i)->data[j],16) ;
         }
     }

   //组包测试


     Msg testmsg;
     testmsg.length = 21;


     testmsg.data[0] = 0x81;
     testmsg.data[1] = 0x31;
     testmsg.data[2] = 0xFF;
     testmsg.data[3] = 0xD8;
     testmsg.data[4] = 0x05;
     testmsg.data[5] = 0x4E;

     testmsg.data[6] = 0x56;
     testmsg.data[7] = 0x33;
     testmsg.data[8] = 0x36;
     testmsg.data[9] = 0x25;
     testmsg.data[10] = 0x39;
     testmsg.data[11] = 0x22;
     testmsg.data[12] = 0x43;

     testmsg.data[13] = 0x72;
     testmsg.data[14] = 0xF7;
     testmsg.data[15] = 0xFD;
     testmsg.data[16] = 0x30;
     testmsg.data[17] = 0x23;
     testmsg.data[18] = 0x51;

     testmsg.data[19] = 0x09;
     testmsg.data[20] = 0xEF;

      QList<Msg> lstMsg ;
      lstMsg.append(testmsg);

     QByteArray ba;

     ba = testpacker->Pack(lstMsg);

     qDebug()<<ba.toHex();
输出:
jjjj
“81”
“31”
“ff”
“d8”
“5”
“4e”
“56”
“33”
“36”
“25”
“39”
“22”
“43”
“72”
“f7”
“fd”
“30”
“23”
“51”
“9”
“ef”
“0001001b5a55158131ffd8054e5633362539224372f7fd30235109ef0a6a69”

六、如何用串口解析出协议帧,并解决分包,组包,粘包问题?

原文链接:https://blog.csdn.net/qq_32166779/article/details/99943574

生产者(4个):

硬件:串口一; 串口二; 串口三; 串口四;采用普通接受中断

软件: 中断时以字节传入到 ringbuffer,建立ringbuffer数组[4],分别在四个中断里存入

void USART1_IRQHandler(void)
{
...
	ring_buffer_write_byte_forced(&RingBuffer[UART_1], DeviceList[UART_1].handle->DR);
...
}

消费者(4个):

软件:

第一步:

保证事件独立开

如果是前后台程序

while1{

				串口一事件;

				串口二事件;

				串口三事件;

				串口四事件;

			}

//rtos程序

while1{undefined

//串口一事件;

}

while2{undefined

//串口二事件;

}

第二步:建立 解析库 cmd_parser(&CmdParser, buffer, bufferLen)函数

解析库作用:

消费任何ringbuffer的内容,放到解析库中,如果消费内容不够,则继续消费,如果消费内容过多,用长度来控制需要消费的完整帧,进行下一步解析。

虽然每个协议获取整帧的长度方法不一样,但是几乎都是head + datalen+taildata 的方法

1,AT指令head是“+IPD”, datalen在其后

2,modbus(RTU)的head是address,datalen在其后,taildata是crc校验数据

3,如果没有head和datalen,则直接获取固定长度

第三步:建立数据结构

建立协议需要的数据结构

struct

{undefined

1,processFunc解析函数指针,因为每个串口解析函数不一样,所以在每个串口事件中,增加相关回调函数

2,第二步 中的会用到的一些变量,比如head,datalen,u8 *buffer;等

}cmdParser

第四步:

建立自己的解析函数,process_parser(),这里的形参和cmdParser里的processFunc的形参是一样的,

这个函数就是在获取完整帧之后,接下来处理的函数。

第五步:

当获取完整帧之后,把整帧buff提取出来,调用cmdParser.processFunc(buff,bufflen)相当于调用process_parser(),这里为什么要这样做? 因为调用这个函数时还在解析库cmd_parser(&CmdParser, buffer, bufferLen)函数内,为了做到多串口通用性,用这个cmdParser.processFunc(buff,bufflen)代替所有自己生成的process_parser()函数。

如何使用?

拿串口一事件举例子

串口一事件函数

{
    ui8 buffer[128];
    si32 bufferLen;
    cmdParser.processFunc = process_parser();

    bufferLen= ring_buffer_read(&RingBuffer[UART_1], buffer, sizeof(buffer));

    if(bufferLen > 0) {
    cmd_parser(&CmdParser, buffer, bufferLen);
    }
}

难点: cmd_parser(&CmdParser, buffer, bufferLen) 如何实现?

根据公司的多种协议帧 实现,用状态机 保证 各个情况,比如校验函数,解密函数,计算数据长度函数等等,如果做到了高鲁棒性,那么 这个库可以封装好不动了。

七、串口协议包的接收及解析处理

https://blog.csdn.net/xiaoyuanwuhui/article/details/104775612

  嵌入式 最新文章
基于高精度单片机开发红外测温仪方案
89C51单片机与DAC0832
基于51单片机宠物自动投料喂食器控制系统仿
《痞子衡嵌入式半月刊》 第 68 期
多思计组实验实验七 简单模型机实验
CSC7720
启明智显分享| ESP32学习笔记参考--PWM(脉冲
STM32初探
STM32 总结
【STM32】CubeMX例程四---定时器中断(附工
上一篇文章      下一篇文章      查看所有文章
加:2022-03-08 22:42:39  更:2022-03-08 22:44:36 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 7:41:17-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码