STM32与QT实现串口传输结构体含源码

news2024/11/28 6:25:07

文章目录

  • 一. 关于数据传输的方式
    • 1.1 基本数据类型传输
    • 1.2 结构体传输
  • 二. STM32与QT实现串口传输结构体实例
    • 2.1 下位机的实现
    • 2.2 上位机的实现
    • 2.3 演示Demo
  • 三. 注意事项
    • 3.1 关于字节对齐问题
    • 3.2 关于大小端问题

一. 关于数据传输的方式

在日常开发过程中,我们时常需要用到串口来传输数据,无论是MCU与MCU通信,还是上位机与下位机之间的通信,串口传输数据都非常普遍。对于传输方式,可以直接分别将单个数据类型进行拆分后发送;也可以将数据封装成结构体后再进行拆分发送。

1.1 基本数据类型传输

如果是传输少量数据,可以根据数据类型,把数据拆分成多个字节后通过串口发送。另一端串口接受后,根据拆分的方式对数据进行合并。下面以STM32的板间通信为例:

发送端:

uint8_t  msg_send[10] = {0};	

uint16_t meg_uint16;
float    meg_float;;

/*帧头*/
msg_send[0] = 0x0A;
msg_send[1] = 0x0B;
/*数据段*/
msg_send[2] = (uint8_t)(meg_uint16>> 8);//高8位
msg_send[3] = (uint8_t)(meg_uint16);    //低8位
/*数据段*/
msg_send[4] = (uint8_t)(meg_float>> 24);//同上
msg_send[5] = (uint8_t)(meg_float>> 16);
msg_send[6] = (uint8_t)(meg_float>> 8);
msg_send[7] = (uint8_t)(meg_float);
/*帧尾*/
msg_send[8] = 0x0C;
msg_send[9] = 0x0D;

HAL_UART_Transmit(&huart1,msg_send,60xff);//串口发送

接收端:

uint8_t  msg_receive[10] = {0};	
HAL_UART_Receive_IT(&huart1, msg_receive, 10);//串口接受

if(msg_receive[0]==0x0A&&msg_receive[1]==0x0B&&msg_receive[8]==0x0C&&msg_receive[9]==0x0D)//验证帧头帧尾
{
	uint16_t meg_uint16 = (uint16_t)(msg_receive[2]<<8|msg_receive[3]);
	float    meg_float  = (float)(msg_receive[4]<<24|msg_receive[5]<<16|msg_receive[6]<<8|msg_receive[7]);
}

1.2 结构体传输

上面的方式虽然简单,但是我们需要对每个数据进行单独的拆分与合并。发送和接受时,就需要计算出数据格式中每个单元所对应的位置,即数组中第i个元素对应的内容。这样显然是很麻烦的,效率很低,这就相当于先织了一个大网,捕捉到一网鱼,还得过下称,才能按照重量分类开来一样。

那么如果我们能提前根据接收的数据格式来做一个容器,直接把接收的数据复制到这个容器内,就可以剩下很多的操作,这就是通过结构体的方式来传输,如上图所示。

二. STM32与QT实现串口传输结构体实例

下面以STM32与QT通信为例,讲述具体的代码实现:

开发平台:STM32F030F4P6 / PC (window10)

开发环境:Keil-v5.31/ Qt Creator-v4.8.2

开发语言:C/C++

注意,由于STM32串口输出的是TTL电平,因此还需要加多一个CH340芯片转成USB信号,再与PC串口通信。

假设我们要传输的结构体如下,#pragma pack(1)代表结构体中变量会在内存中按照一字节对齐的方式存储,所以结构体的所占的字节数就是所有成员类型所占字节数之和,即14个字节。关于字节对齐的问题,可以学习这篇文章(链接)。

#pragma pack(1)//按照1字节对齐

typedef struct Data_Structure
{
    uint8_t    Info_u8; 
      
    uint16_t   Info_u16;    
     
    float      Info_float;
}DataS;

typedef struct CSInfoStrcutre
{
    uint8_t    data_u8;    
     
    uint16_t   data_u16; 
             
    float      data_float;    
     
    DataS      data_Structure;  
    
} CSInfoS;

#pragma pack()//恢复默认字节对齐规则

typedef struct CSInfoStrcutre* ptrCSInfo;

2.1 下位机的实现

主要实现步骤如下:

uint8_t infoArray[14]={0};
uint8_t infoPackage[18]={0};

CSInfo_2Array_uint8(ptrCSInfo,infoArray);
CSInfo_Pack(infoPackage,infoArray,sizeof(infoArray));

HAL_UART_Transmit(&huart1,infoPackage,18,0xFF);

通过CSInfo_2Array_uint8()将结构体拆分为单个字节的数组。再通过CSInfo_Pack()给数组加入帧头与帧尾构成完整的数据包。最后通过STM32 HAL库自带的串口发送函数HAL_UART_Transmit(),将数据发送去上位机。本文数据处理部分参考的是这篇博客(链接)。

/**
  * @brief  将数据段(CSInfoS)重组为uint8类型的数组
  * @param  infoSeg   指向一个CSInfoS的指针
  * @param  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));
	}
}
/**
    @Protocol
    ----------------------------------------------
        头   |            信息           |     尾     |
    ----------------------------------------------
    0x0A|0x0B|       CSInfoStrcutre     | 0x0C|0x0D  |
    ----------------------------------------------
      2Byte  |          14Byte          |    2Byte   |
    ----------------------------------------------
  * @brief  按协议打包
  * @param  infopackage 打包结果,按协议结果为2+14+2=18字节 
  * @param  infoArray   由数据段重组的uint8类型数组 	
  * @param  infoSize    数据段的大小
  * @retval 无
  */
void CSInfo_Pack(uint8_t* infopackage,uint8_t* infoArray,uint8_t infoSize)
{
	uint8_t ptr=0;
	infopackage[0] = 0x0A;
	infopackage[1] = 0X0B;
	
	/* 将信息封如入数据包中 */
	for(;ptr<infoSize;ptr++){
		infopackage[ptr+2] = infoArray[ptr];
	}
	
	infopackage[ptr+2] = 0X0C;
	infopackage[ptr+3] = 0X0D;
}

2.2 上位机的实现

上位机的大致处理流程如下,这里只讲解串口数据处理部分,关于其他内容,可以去看源码。

在MainWindow的初始函数中加入connect(m_serialport,SIGNAL(readyRead()),this,SLOT(receive_data())),目的在于当串口接收到数据时,则会进入到数据处理函数receive_data()中。

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    m_serialport = new QSerialPort();

    serialport_init();
    m_serialport->setReadBufferSize(90);//设置串口缓存区大小为90字节

    connect(m_serialport,SIGNAL(readyRead()),this,SLOT(receive_data()));//串口数据接受

    connect(ui->pushButton_1,SIGNAL(clicked()),this,SLOT(serialport_init()));//刷新串口
    connect(ui->pushButton_2,SIGNAL(clicked()),this,SLOT(open_serialport()));//打开与关闭串口

    QTimer *timer1 = new QTimer(this);  //初始化一个定时器,用于定时刷新UI上的Qlabel
    connect(timer1, SIGNAL(timeout()), this, SLOT(refresh_data())); //定时更新Qlaber
    timer1->start(400);  //刷新频率。
}

receive_data()中对串口中的数据进行处理,具体流程如下:

/**
    @Protocol
    ----------------------------------------------
         头  |            信息           |  尾   |
    ----------------------------------------------
    0x0A|0x0B|       CSInfoStrcutre     | 0x0C  |
    ----------------------------------------------
        2Byte|          57Byte          | 1Byte |
    ----------------------------------------------
* @brief  数据到来时触发数据接收
*/
void MainWindow::receive_data()
{
    if(m_serialport->bytesAvailable()>=18)//查看串口缓存区有多少数据
    {
        uint8_t package_serial[36];//串口提取出的原始数据
        uint8_t infoArray[14];     //提取出一个数据段
        
         /* 读取数据 */
        int numHasRead = readInfoFromSerialport(package_serial); //从串口读取数据至数组

        /* 提取数据段 */
        bool readable = CSInfo_GetInfoArrayInpackages(infoArray,package_serial,numHasRead);//根据帧头寻找数据段

        /* 数据段解包 */
        if(readable)
        {CSInfo_InfoArray2CSInfoS(infoArray,this->ptrCSInfo);}//把数据传入结构体中
    }
}
/**
* @brief  把当前serialport缓冲区的数据全部读取到一个uint8类型的数组中
* @param  packages 从串口读取到的包含数据包的数据
* @retval numHasRead 从缓冲区读取到的字节数
*/
int MainWindow::readInfoFromSerialport(uint8_t* packages)
{
    int numHasRead = 0;

    /* 没有可用的串口设备则中止读取操作 退出函数 */
    if(m_serialport->isOpen())
    {
       /* 读取串口缓冲区数据 */
       QByteArray dataArray = m_serialport->read(36);
       
       /*计算数据长度*/
       numHasRead = dataArray.size();

       /*将读出来的数据迁移至dataArray中*/
       if(numHasRead>=18){
           for(int i=0;i<numHasRead;i++){
               *(packages+i) = (uint8_t)dataArray[i];
           }
       }
       return numHasRead;
    }
    else {return 0;}
}
/**
* @brief  在串口读取到的数据中根据帧头提取出数据段
* @param  infoArray 串口缓冲区读出的数据
* @param  packages  从串口读取到的包含数据包的数据
* @param  sizepackages 从串口读取到的字节数(packages的大小)
* @retval readable 读取成功为true,失败为false
*/
bool MainWindow::CSInfo_GetInfoArrayInpackages(uint8_t* infoArray,uint8_t* packages,int sizepackages)
{
    int ptr;

    if(sizepackages<18){
        return false;
    }
    else
    {
        /*检测出帧头所在的位置*/
        for(ptr=0;ptr<sizepackages;ptr++){
            if((packages[ptr]==0x0A)&&(packages[ptr+1]==0x0B)&&(packages[ptr+16]==0x0C)&&(packages[ptr+17]==0x0D))
            {
                /*把帧头后的14个字节读取出来*/
                ptr += 2;
                for(int i=0;i<14;i++){
                   infoArray[i] = packages[ptr+i];
                }
                return true;
            }
        }
    }
    return false;
}
/**
* @brief  把存有一个数据段的数组解析为一个CSInfoStructure,结果存到参数2对应的地址
* @param  infoArray 存有一个数据段的uint8类型的数组
* @param  infoStrc 从串口读取到的字节数(packages的大小)
* @retval 无
*/
void MainWindow::CSInfo_InfoArray2CSInfoS(uint8_t* const infoArray,ptrCSInfo infoStrc)
{
    uint8_t* u8PtrOStrc = (uint8_t*)infoStrc;

    for(int i=0;i<14;i++)
        *(u8PtrOStrc+i) = infoArray[i];
}

2.3 演示Demo

请添加图片描述

这里根据上面所写的代码简单的做了个Demo,左边为QT的页面,右边则是Keil的调试页面。

功能主要是:将STM32中的结构体数据传输到QT上,QT接受到后把数据打印在页面上。每个数据都递增处理,方便查看变量的变化。

关于这个Demo的源码,已经上传到CSDN上了,有需要的自取(下载链接)。

三. 注意事项

3.1 关于字节对齐问题

关于字节对齐的问题,可以学习这篇文章(链接)。

结构体的所占的字节数并不一定是所有成员类型所占字节数的总和,这涉及到字节对齐的问题。STM32默认的字节对齐数是4,如果我们希望实现结构体的所占的字节数是所有成员类型所占字节数的总和,我们就需要规定结构体按照1字节对齐来存储,但是这也一定上增加总线的访问次数,降低了访问效率。

#pragma pack(1)//按照1字节对齐

typedef struct Data_Structure
{
    uint8_t    Info_u8; 
      
    uint16_t   Info_u16;    
     
    float      Info_float;
}DataS;
#pragma pack(0)

上位机与下位机传输结构体的时候,传递双方的对于传输对象的字节对齐规则需要一致。

3.2 关于大小端问题

大小端指的是字节序,就是内存中存储数据的字节顺序

大端模式: 高字节存于内存低地址,低字节存于内存高地址。
小端模式: 低字节存于内存低地址,高字节存于内存高地址。

STM32使用的是小段模式,QT我测了也是使用的小段模式,因此在源码里,我没有针对大小端做处理。但是,对于大小端模式不同的平台,串口通信时还需要做数据的转换。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2206644.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用aloam跑hesai Pandar-XT32激光雷达数据

参考自利用aloam跑数据集_aloam数据集-CSDN博客 第一步&#xff1a;查看bag的信息 输入rosbag info来查看bag包的信息&#xff1a; joeyjoey-Legion-Y7000P-IRX9:~$ rosbag info /home/joey/Downloads/data2022/indoor/LiDAR_IMU.bag path: /home/joey/Downloads/da…

Java_EE 多线程技术(Thread)

多线程与并发编程 多线程介绍什么是程序&#xff1f;程序&#xff08;Program&#xff09;是一个静态的概念&#xff0c;一般对应于操作系统中的一个可执行文件。什么是进程?执行中的程序叫做进程(Process)&#xff0c;是一个动态的概念。其实进程就是一个在内存中独立运行的程…

2025秋招倒计时---招联金融

【投递方式】 直接扫下方二维码&#xff0c;或点击内推官网https://wecruit.hotjob.cn/SU61025e262f9d247b98e0a2c2/mc/position/campus&#xff0c;使用内推码 igcefb 投递&#xff09; 【招聘岗位】 后台开发 前端开发 数据开发 数据运营 算法开发 技术运维 软件测试 产品策…

股市期市内外盘高频分钟tick及均线策略分享

【数据库】银河金融数据库&#xff08;yinhedata.com&#xff09;可以获取大量历史行情数据&#xff0c;包含分钟&#xff0c;tick&#xff0c;日。不限于国内外&#xff0c;股票期货基金&#xff0c;ETF、期权等 【策略分享】一、引言均线策略作为技术分析的重要工具&#xff…

Kubesphere4.1插件网关、devops控制页面白页面问题解决

在使用Kubesphere4.1版本时&#xff0c;安装完成devops插件后‘DevOps项目’管理页面出现白页面&#xff0c;无法进行配置。包括网关等控制页面都会出现白页面。 经过查看版本信息发现是4.1版本存在问题&#xff0c;目前不支持。 处理问题&#xff1a; 目前 Kubesphere发布了4…

【软件测试】最佳软件测试基础入门教程

目录 前言一、顺序式开发模型二、 瀑布模型三 、V型模型四、迭代和增量开发模型五、 项目和产品背景下的软件开发 前言 软件开发生命周期的测试 本章简要介绍了软件开发项目中常用的生命周期模型&#xff0c;并解释了测试在每个模型中扮演的角色。它讨论了各种测试级别和测试…

python-PyQt项目实战案例:制作一个简单的图像处理工具

文章目录 1.设计UI2.编写功能代码2.1 初始化ui界面及类成员参数2.2 添加菜单栏2.3 建立信号/槽连接 3.主要功能代码及效果4.设置图像自动调节长宽尺寸但不改变长宽比例参考文献 1.设计UI 对于UI的设计可以通过qt designer直接绘制&#xff0c;也可以通过编写python代码实现。当…

10万+收藏!10万转发!AI绘画如何助力育儿赛道引爆短视频平台?

在自媒体领域&#xff0c;内容创作的竞争日益激烈&#xff0c;但育儿自媒体一直是一块热门且持久的“金矿”&#xff0c;吸引了无数创作者投身其中。然而&#xff0c;如何在这片红海中脱颖而出&#xff0c;成为了许多创作者面临的难题。而AI绘画的出现&#xff0c;无疑解决了创…

约束使用方法:

设置主键&#xff1a; 该列的值用来唯一标识表中每一行&#xff0c;用于强制表的实体完整性。这样的列定义为表的主键&#xff0c;也就是说主键的列中不允许有相同的数据。 设置默认&#xff1a; CREATE TABLE pet( #not null 非空 ZEROFILL 补零 先补零&#xff0c;在非空 id…

解锁机器人视觉与人工智能的潜力,从“盲人机器”改造成有视觉能力的机器人(上)

正如人类依赖眼睛和大脑来解读世界&#xff0c;机器人也需要自己的视觉系统来有效运作。没有视觉&#xff0c;机器人就如同蒙上双眼的人类&#xff0c;仅能执行预编程的命令&#xff0c;容易碰撞障碍物&#xff0c;并犯下代价高昂的错误。这正是机器人视觉发挥作用的地方&#…

k8s1.27部署ingress 1.11.2

k8s1.27部署ingress 1.11.2 要求&#xff1a; 1、使用主机网络。 2、多节点部署&#xff0c;以来标签&#xff1a;isingressistrue ingress1.11.2支持版本 官方参考链接&#xff1a; https://github.com/kubernetes/ingress-nginx/ 官网yaml https://raw.githubuserconten…

DYNPRO_SYNTAX_ERROR 主屏幕调用子屏幕,程序运行时错误

文章目录 问题描述问题查找和解决 问题描述 问题查找和解决

比特币社区心心念念的BTCFi进展如何了?——比特币与DeFi的未来

比特币在去中心化金融&#xff08;DeFi&#xff09;中的角色正在发生深刻变革。作为全球首个加密货币&#xff0c;比特币的主要用途从最初的点对点支付正逐渐转向更复杂的金融应用。通过BTCFi&#xff08;比特币与DeFi的结合&#xff09;&#xff0c;比特币生态系统正加速崛起&…

QD1-P16 HTML 按钮标签(button)

本节学习 HTML 常用标签&#xff1a;button ‍ 本节视频 www.bilibili.com/video/BV1n64y1U7oj?p16 ‍ ​<button>​ 标签在 HTML 中用于创建按钮&#xff0c;它是一个交互式元素&#xff0c;通常用于提交表单或触发某个脚本。以下是 <button>​ 标签的一些基本…

基于FPGA的DDS信号发生器(图文并茂+深度原理解析)

篇幅有限&#xff0c;本文详细源文件已打包 至个人主页资源&#xff0c;需要自取...... 前言 DDS&#xff08;直接数字合成&#xff09;技术是先进的频率合成手段&#xff0c;在数字信号处理与硬件实现领域作用关键。它因低成本、低功耗、高分辨率以及快速转换时间等优点备受认…

C++ stack和queue的使用介绍和模拟实现

内容摘要&#xff1a; 本文介绍了stack和queue的构造函数和一些成员函数&#xff0c;并模拟实现了stack和queue&#xff0c;分析了为什么选择deque作为适配器默认封装的对象 stack的介绍 栈是只能够在一端进行插入和删除的&#xff0c;这就是我们一直常说的“后进先出”&#x…

未来10年,哪些行业将被AI彻底颠覆?

随着人工智能&#xff08;AI&#xff09;技术的快速发展&#xff0c;许多行业的工作方式正在发生显著变化。一些原本依赖人工处理的任务&#xff0c;正逐渐由AI接手并优化。在未来&#xff0c;AI将不仅仅是辅助工具&#xff0c;它可能会彻底改变某些行业的运作模式&#xff0c;…

通过AI技术克服自动化测试难点(下)

前面的文章里我们对可以应用到测试中的AI技术做了整体介绍&#xff0c;详细介绍了OpenCV技术、OCR技术和神经网络&#xff0c;本文我们继续为大家介绍卷积神经网络、数据集以及AI技术在其他方面和测试相关的创新。 卷积神经网络整体上的原理是这样的&#xff0c;首先在底层特征…

筛选因数快速法+map

前言&#xff1a;老是忘记怎么快速筛选因数&#xff0c;我们只需要枚举小于sqrt&#xff08; num &#xff09; 的数&#xff0c;这样可以降低很多复杂度&#xff0c;而且我们的因数一定是成对出现的&#xff0c;所以我们遇到一个因数的时候x&#xff0c;判断 x 2 x^2 x2 是否…

Java基础知识全面总结

第一章&#xff1a;类与对象 第一课&#xff1a;什么是面向对象编程 1.面向对象编程和面向过程编程的区别 无论是面向过程编程还是面向对象编程都是用于解决一个实际问题&#xff0c;当面向过程编程在解决一个问题时&#xff0c;更多的情况下是不会做出重用的设计思考的&…