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

2023-05-16

先说解决方案,细节和实现代码都放在正文


下位机:把结构体拆分成8位的整型数据,加上数据包头和包尾,然后按顺序单个单个地发出;
上位机:把串口里的数据读取出来,找到包头,按顺序装填到结构体中,然后使用结构体引用数据;


一、串口通信

串口通讯(Serial Communication)是一种设备间非常常用的串行通讯方式


相信浏览本文的朋友都已经使用过串口通信协议在各机器之间传递信息。这种通讯方式只需要四只引脚就能在短距离内实现全双工的通信,非常方便。目前许多通用芯片(如STM32)还提供了硬件支持,并给定了数据收发的接口函数。


在这里插入图片描述


以stm32为例,我们可以通过一些特定的函数使用芯片上的USART外设,不需要操心协议的电平规定就能够进行数据的收发,相关的函数在STM32的参考手册中列出:


在这里插入图片描述


在固件库中也可以找到:


在这里插入图片描述


大致看上去十分方便,但细看就会发现一个十分重要的问题,譬如发送数据的这个函数USART_SendData()


/**
  * @brief  Transmits single data through the USARTx peripheral.
  * @param  USARTx: Select the USART or the UART peripheral. 
  *   This parameter can be one of the following values:
  *   USART1, USART2, USART3, UART4 or UART5.
  * @param  Data: the data to transmit.
  * @retval None
  */
void USART_SendData(USART_TypeDef* USARTx, uint16_t Data)
{
  /* Check the parameters */
  assert_param(IS_USART_ALL_PERIPH(USARTx));
  assert_param(IS_USART_DATA(Data)); 
    
  /* Transmit Data */
  USARTx->DR = (Data & (uint16_t)0x01FF);
}

他只给你提供了单个数据的发送方式,而且数据类型限制位16位无符号整型(uint16_t),这就引出了我们在文章开头提到的拆分并封包发送的必要性。下面我们先介绍一下发送的结构体的样子,然后再说拆分封包的问题。


二、定义要发送的结构体

首先明确发送的结构体是什么样子的


/**
	@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_uint8CSInfo_PackCSInfo_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);

也即是依次调用readInfoFromSerialportCSInfo_GetInfoArrayInpackagesCSInfo_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把串口中所有的数据读取到dataArrayreadAll()的返回值就是一个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.cSources/CSInfoParser.c文件中。

下位机的代码暂未上传,需要的朋友可以留言索取。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

怎样用串口发送结构体-简单协议的封包和解包 的相关文章

随机推荐

  • 自定义target命令:add_custom_target

    一 前置知识 1 CMake中一切都是基于target的 xff0c 如add library会产生一个library的target xff0c add executable会产生一个exe的target 2 以上命令生成的target放在
  • 软件设计原则:迪米特法则

    一 定义 迪米特法则 xff1a 要求一个对象应该对其他对象有最少的了解 xff0c 所以又叫做最少知识原则 二 法则内容 xff1a 1 不该有直接依赖关系的类之间 xff0c 不要有依赖 xff1a 即 xff0c 不和陌生人说话 xf
  • ElasticSearch最佳入门实践(六十二)type底层数据结构

    type xff0c 是一个index中用来区分类似的数据的 xff0c 类似的数据 xff0c 但是可能有不同的fields xff0c 而且有不同的属性来控制索引建立 分词器 field的value xff0c 在底层的lucene中建
  • 四轴的组成及参数评定

    电气工程及其自动化专业 xff0c 坐标广东湛江 xff0c 大一时期对专业上很感兴趣 xff0c 自学了许多东西 xff0c 但是只是停留在理论基础上而缺乏实践 xff0c 和学校在这方面的普及有点关系吧 xff0c 趁着国家有这方面的支
  • sudo rosdep init报错的解决方式

    Ubuntu16 04下安装ROS时 xff0c 执行到sudo rosdep init这一步时会遇到问题 xff0c 如下图所示 xff1a 尝试了很多办法 xff0c 都没有成功的 后来参考了https www ioiox com ar
  • VS版本和VC版本的对应【完整版】

    看到网上杂七杂八 xff0c 很乱 xff0c 索性自己发帖多版本开发福音 xff08 该帖不更新了 xff0c 请看参考里连接中的官方文档 xff0c 非常清楚 xff0c 还保持最新 xff09 MSC 1 0 MSC VER 61 6
  • 搭建运行激光slam环境中遇到的问题

    1 先是踩了一些坑 xff0c 重复安装了一些库 xff0c 因为ros noetic里面就自带了一些库 xff0c 所以安装的时候重复安装了 解决方法 xff1a 删掉重装 另外缺少一些库 xff0c 乱装一顿 xff0c 居然凑齐 Ub
  • mac上用VSCode搭建 c++ 工程,用于学习Opengl

    先下载VSCode安装c c 43 43 插件 xff0c 安装微软这个 创建一个文件夹作为项目 xff0c 然后用VSCode打开这个目录在这个文件夹中创建好四个目录 xff0c 分别是src xff0c lib include bin
  • 刷赞与评论

    网站自动刷帖 xff0c 刷赞 xff0c 刷评论等网络推广方式的基本实现 里面的思路有东西
  • 系统复制-快速重装系统

    ubuntu 直接把安装好常用软件和环境的系统打包成镜像 xff0c 用systemback安装 xff0c 便捷很多 之前那种 xff0c ubuntu安装都要好久 xff0c 少说也得20分钟吧 xff0c 之前就是等 xff0c 等它
  • 机器人 控制领域

    机器人 控制领域好像没太有很新很有用的工作 xff0c 还是依据Dynamic Model的Motion Planning更接近于任务层 其实 xff0c 感觉自己喜欢的不是控制 而是motion xff0c motion control
  • 树莓派电压过低 串口数据错误增多

    调试过程中 xff0c 树莓派串口读单片机上传的数据 的程序突然一堆checksum error 换一块满电的LiPo电池就大幅减少了报错 一开始猜测原因 可能是电压过低导致CPU运行慢了 xff08 可能叫做 降频 xff09 xff0c
  • 机器人知识体系

    纲 机电力算控感 知识体系体系各元素特点体系的建立和完善 机电力算控感 知识体系 机械 电子电气 力学 xff08 静力学与动力学分析 流体力学 材料力学等 xff09 计算 xff08 通用计算机和嵌入式计算机 xff09 控制理论 感知
  • OpenCV之imwrite()等基本操作

    参考 xff1a Opencv之imwrite 函数的用处 imwrite 函数用来保存图片 opencv3中的imwrite函数是用来输出图像到文件 xff0c 其声明如下 xff1a CV EXPORTS W bool imwrite
  • 麦克纳姆轮全向移动原理

    什么是麦克纳姆轮 在竞赛机器人和特殊工种机器人中 xff0c 全向移动经常是一个必需的功能 全向移动 意味着可以在平面内做出任意方向平移同时自转的动作 为了实现全向移动 xff0c 一般机器人会使用 全向轮 xff08 Omni Wheel
  • 卡尔曼滤波(KF)与扩展卡尔曼滤波(EKF)的一种理解思路及相应推导(1)

    前言 xff1a 从上个世纪卡尔曼滤波理论被提出 xff0c 卡尔曼滤波在控制论与信息论的连接上做出了卓越的贡献 为了得出准确的下一时刻状态真值 xff0c 我们常常使用卡尔曼滤波 扩展卡尔曼滤波 无迹卡尔曼滤波 粒子滤波等等方法 xff0
  • Qt Cmake添加*.qrc资源文件

    cmake minimum required VERSION 3 5 project Test LANGUAGES CXX 这里 file GLOB RECURSE QRC SOURCE FILES CMAKE CURRENT SOURCE
  • IOS 加载本地HTML

    web qtt以 folder形式添加到项目中 xff0c 注意是蓝色的颜色 创建swift项目 xff0c 写入如下代码 span class token comment span span class token comment Vie
  • C#实现:将十进制数转换为十六进制(含完整源码)

    C 实现 将十进制数转换为十六进制 含完整源码 在C 中 我们可以使用基础数据类型来存储整数值 如int long等 而十进制数是我们最常用的数制 但有些场景下需要将其转换为其它进制 如十六进制 本文将介绍如何使用C 来实现将十进制数转换为
  • 怎样用串口发送结构体-简单协议的封包和解包

    先说解决方案 xff0c 细节和实现代码都放在正文 下位机 xff1a 把结构体拆分成8位的整型数据 xff0c 加上数据包头和包尾 xff0c 然后按顺序单个单个地发出 xff1b 上位机 xff1a 把串口里的数据读取出来 xff0c