STM32串口接收不定长数据(接收中断+超时判断)

玩转 STM32 单片机,肯定离不开串口。串口使用一个称为串行通信协议的协议来管理数据传输,该协议在数据传输期间控制数据流,包括数据位数、波特率、校验位和停止位等。由于串口简单易用,在各种产品交互中都有广泛应用。

但在使用串口通讯的时候,我们并不知道对方会发送多少个数据,也不知道数据什么时候发送完,简单来讲就是:如何确保收到一帧完整的数据?

串口发送的数据有长有短,如果没有接收完整,肯定会影响后续业务的处理。为了接收不定长数据,常见的处理方法有:

1. 固定格式

比如双方约定,一帧的数据以 AA BB 开头,以 BB AA 结尾,这样在从机接收数据的时候,一旦收到 AA BB 字符,就知道对方要发来一个数据包了,然后就把后面发来的数据保存起来,直到接收到 BB AA 为止。

这种方法简单高效,但缺点就是需要每个字符都进行判断,浪费 CPU 资源,增加功耗。

2. 接收中断+超时判断

串口接收到一个数据时,就会触发接收中断。但如何判断数据已经发送完了呢?

通常来讲,两帧数据之间,会有个时间间隔。因此,我们可以使用一个计时器,如果在一个固定的时间点里没接收到新的字符,则认为一帧数据接收完成了。

3. 空闲中断

串口在空闲时,也就是说串口在一段时间里没有接收到新数据,则会触发空闲中断。细心的同学应该发现了,空闲中断实际上跟上面的超时判断是一样样的,只不过空闲中断是硬件自带,但超时判断需要我们自己实现。

所以,一旦接收到空闲中断,可以认为接收到一帧完整的数据。

但是,空闲中断并不是所有的 MCU 都具备,一般高端一点的 MCU 才有,低端一些的 MCU 并没有空闲中断。

1. 源码下载及前置阅读

本文首发 良许嵌入式网 :https://www.lxlinux.net/e/ ,欢迎关注!

本文所涉及的源码及安装包如下(由于平台限制,请点击以下链接阅读原文下载):

https://www.lxlinux.net/e/stm32/stm32-usart-receive-data-using-rxne-time-out.html

如果你是个零基础的小白,连 STM32 都没见过,我也给你准备了一个保姆级教程,手把手教你搭建好 STM32 开发环境,并教你如何下载程序,简直业界良心!

https://www.lxlinux.net/e/stm32/stm32-quick-start-for-beginner.html

如果你连代码都不知道怎么烧录到 STM32 的,可以参考下文,提供了 5 种代码烧录方式:

https://www.lxlinux.net/e/stm32/five-ways-to-flash-program-to-stm32.html

如果你想自己搭一个属于自己的工程模板,可以参考下面这篇文章:

https://www.lxlinux.net/e/stm32/create-stm32-hal-project-template.html

在本文中,我们详细来介绍如何使用接收中断+超时判断完成不定长数据的接收,对于空闲中断的接收,请查看下文

https://www.lxlinux.net/e/stm32/stm32-usart-receive-data-using-idle-dma.html

2. 什么是接收中断?

前文已经提到,当接收到一字节数据时,会触发接收中断,对应串口状态寄存器第 5 位被置 1 ,如下图示。

当我们将 DR 寄存器的值读取之后,该位又被自动清零。

3. 硬件准备

  • STM32 核心板

本文使用正点原子 M48Z 核心板,小巧好用,某宝 20 元出头。

  • USB 转 TTL

这种设备主要作用是用来调试或下载程序。价格也很便宜,普遍 5~8 元。

  • ST-Link

ST-Link 是一种用于 STM32 微控制器的调试和编程工具,它可以通过 SWD 或 JTAG 接口与开发板进行通信。一般也很便宜,七八元左右。

4. 编程实战

在本实验中,我们将串口 1 作为 log 输出端口,串口 2 作为本次实验的接收端口。

因此我们需要提前创建 uart2 模块,包含 uart2.c 及 uart2.h 两个文件,并加载进工程模板。

4.1 串口初始化

串口的初始化大家应该不陌生,主要步骤为:

  1. 定义串口句柄 uart2_handle ,并调用 HAL_UART_Init 进行初始化;
  2. 初始化串口底层函数,调用 HAL_UART_MspInit 函数。

第一步在 uart2.c 文件里进行:

UART_HandleTypeDef uart2_handle;

void uart2_init(uint32_t baudrate)
{
    uart2_handle.Instance          = UART2_INTERFACE;              /* UART2 */
    uart2_handle.Init.BaudRate     = baudrate;                     /* 波特率 */
    uart2_handle.Init.WordLength   = UART_WORDLENGTH_8B;           /* 数据位 */
    uart2_handle.Init.StopBits     = UART_STOPBITS_1;              /* 停止位 */
    uart2_handle.Init.Parity       = UART_PARITY_NONE;             /* 校验位 */
    uart2_handle.Init.Mode         = UART_MODE_TX_RX;              /* 收发模式 */
    uart2_handle.Init.HwFlowCtl    = UART_HWCONTROL_NONE;          /* 无硬件流控 */
    uart2_handle.Init.OverSampling = UART_OVERSAMPLING_16;         /* 过采样 */
    HAL_UART_Init(&uart2_handle);                                  /* 使能UART2 */
}

第二步在 usart.c 文件里进行,其实也可以在 uart2.c 文件里做,但我懒~

在最下面一行代码,我们使用 __HAL_UART_ENABLE_IT() 使能接收中断。

void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
    GPIO_InitTypeDef gpio_init_struct;

    if (huart->Instance == USART_UX)                            /* 如果是串口1,进行串口1 MSP初始化 */
    {
        ....
        // 节略串口1相关代码
        ....
    }
    else if (huart->Instance == UART2_INTERFACE)                /* 如果是UART2 */
    {
        UART2_TX_GPIO_CLK_ENABLE();                             /* 使能UART2 TX引脚时钟 */
        UART2_RX_GPIO_CLK_ENABLE();                             /* 使能UART2 RX引脚时钟 */
        UART2_CLK_ENABLE();                                     /* 使能UART2时钟 */

        gpio_init_struct.Pin    = UART2_TX_GPIO_PIN;            /* UART2 TX引脚 */
        gpio_init_struct.Mode   = GPIO_MODE_AF_PP;              /* 复用推挽输出 */
        gpio_init_struct.Pull   = GPIO_NOPULL;                  /* 无上下拉 */
        gpio_init_struct.Speed  = GPIO_SPEED_FREQ_HIGH;         /* 高速 */
        HAL_GPIO_Init(UART2_TX_GPIO_PORT, &gpio_init_struct);   /* 初始化UART2 TX引脚 */

        gpio_init_struct.Pin    = UART2_RX_GPIO_PIN;            /* UART2 RX引脚 */
        gpio_init_struct.Mode   = GPIO_MODE_INPUT;              /* 输入 */
        gpio_init_struct.Pull   = GPIO_NOPULL;                  /* 无上下拉 */
        gpio_init_struct.Speed  = GPIO_SPEED_FREQ_HIGH;         /* 高速 */
        HAL_GPIO_Init(UART2_RX_GPIO_PORT, &gpio_init_struct);   /* 初始化UART2 RX引脚 */

        HAL_NVIC_SetPriority(UART2_IRQn, 0, 0);                 /* 抢占优先级0,子优先级0 */
        HAL_NVIC_EnableIRQ(UART2_IRQn);                         /* 使能UART2中断通道 */

        __HAL_UART_ENABLE_IT(huart, UART_IT_RXNE);              /* 使能UART2接收中断 */
    }
}

4.2 判断接收中断

在串口 2 接收中断里,我们先使用 __HAL_UART_GET_FLAG() 函数判断 RXNE 这一位有没有被置 1 ,如果被置 1 ,则代表接收到字符,调用 HAL_UART_Receive() 函数接收字符,并保存于临时变量 receive_data 中。

之后,再调用 HAL_UART_Transmit() 函数将接收到的字符打印出来。

void UART2_IRQHandler(void)
{
  uint8_t receive_data = 0;   
  if(__HAL_UART_GET_FLAG(&uart2_handle,UART_FLAG_RXNE) != RESET)
  {
    HAL_UART_Receive(&uart2_handle, &receive_data, 1, 1000);        //串口2接收1位数据
    HAL_UART_Transmit(&uart2_handle, &receive_data, 1, 1000);       //将接收的数据打印出来
  }
}

现在我们通过接收中断就可以实现了自发自收,编译后烧进板子,效果如下:

但现在我们只实现了字符的接收,并不知道一帧的数据什么时候接收完。

在下面的操作里,我们就通过超时的方法,进一步判断数据是否完成传输。

4.3 数据接收完成判断

如何判断一帧的数据接收完成了?

在本文中,我们使用超时的方法进行判断,这种方法虽然会耗费 CPU 资源,但因为比较简单,所以使用也很广泛。在下一篇文章里,我们将使用空闲中断+DMA 的方法,更高效进行帧数据接收完成判断。

超时判断的思路如下:

  1. 将接收到的字符保存在接收缓冲区里,并定义一个变量 uart2_cnt 计算总共收到了多少个字符;
  2. 假如一帧的数据接收完成了,那么 uart2_cnt 变量的值应该维持不变。

第一个步骤比较好实现,还是在串口 2 接收中断里,做一些小小的改动:

uint16_t uart2_cnt = 0, uart2_cntPre = 0;

void UART2_IRQHandler(void)
{
    uint8_t receive_data = 0;   
    if(__HAL_UART_GET_FLAG(&uart2_handle, UART_FLAG_RXNE) != RESET){    //获取接收RXNE标志位是否被置位
        if(uart2_cnt >= sizeof(uart2_rx_buf))                           //如果接收的字符数大于接收缓冲区大小,
            uart2_cnt = 0;                                              //则将接收计数器清零
        HAL_UART_Receive(&uart2_handle, &receive_data, 1, 1000);        //接收一个字符
        uart2_rx_buf[uart2_cnt++] = receive_data;                       //将接收到的字符保存在接收缓冲区
    }
}

关键是第二步,我们如何判断 uart2_cnt 什么时候维持不变(也就是一帧的数据接收完成了)?也很简单,我们就定时去查看一下这个变量的值,看看是否跟上一次一样,如果一样的话就说明数据接收完成了。

因此我们需要再借助一个新的变量 uart2_cntPre ,记录上一次接收到的数据的长度(上面的代码已经定义好了)。

uint8_t uart2_wait_receive(void)
{
    if(uart2_cnt == 0)                                      //如果接收计数为0,则说明没有处于接收数据中,所以直接跳出,结束函数
        return UART_ERROR;

    if(uart2_cnt == uart2_cntPre) {                         //如果上一次的值和这次相同,则说明接收完毕
        uart2_cnt = 0;                                      //清0接收计数
        return UART_EOK;                                    //返回接收完成标志
    }

    uart2_cntPre = uart2_cnt;                               //置为相同
    return UART_ERROR;                                      //返回接收未完成标志
}

然后我们在 main 函数里的 while 死循环定期(例如10ms)调用 uart2_wait_receive 函数,如果返回值为 UART_EOK 则代表帧数据接收完成,我们就可以将数据打印出来。

while(1)
{
    if(uart2_wait_receive() == UART_EOK) {      //判断串口2是否数据接收完成
        printf("recv: %s\r\n", uart2_rx_buf);   //打印收到的数据
        uart2_rx_clear();                       //清空接收缓冲区
    }

    delay_ms(10);                               //每隔10毫秒判断一次
}

当然,接收到的数据使用完成之后,我们就应该清空接收缓冲区,并将计数器置 0 ,方便下一次接收,所以我们调用了 uart2_rx_clear() 函数,其代码实现为:

void uart2_rx_clear(void)
{
    memset(uart2_rx_buf, 0, sizeof(uart2_rx_buf));          //清空接收缓冲区
    uart2_cnt = 0;                                          //接收计数器清零
}

uart2.h 文件内容如下:

#include <stdint.h>
#include "usart.h"

/* 引脚定义 */
#define UART2_TX_GPIO_PORT           GPIOA
#define UART2_TX_GPIO_PIN            GPIO_PIN_2
#define UART2_TX_GPIO_CLK_ENABLE()   do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)

#define UART2_RX_GPIO_PORT           GPIOA
#define UART2_RX_GPIO_PIN            GPIO_PIN_3
#define UART2_RX_GPIO_CLK_ENABLE()   do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)

#define UART2_INTERFACE              USART2
#define UART2_IRQn                   USART2_IRQn
#define UART2_IRQHandler             USART2_IRQHandler
#define UART2_CLK_ENABLE()           do{ __HAL_RCC_USART2_CLK_ENABLE(); }while(0)

/* 错误代码 */
#define UART_EOK                     0   /* 没有错误 */
#define UART_ERROR                   1   /* 通用错误 */
#define UART_ETIMEOUT                2   /* 超时错误 */
#define UART_EINVAL                  3   /* 参数错误 */

/* UART收发缓冲大小 */
#define UART2_RX_BUF_SIZE            128
#define UART2_TX_BUF_SIZE            64

void uart2_init(uint32_t baudrate);
uint8_t uart2_wait_receive(void);
void uart2_rx_clear(void);

一切判断就绪后,我们就可以将代码烧进板子,现象如下:

5. 小结

STM32 串口通讯在项目中使用的频率非常高,但由于不知道数据发送方会发送多少数据量,所以串口接收不定长数据成了一个急需解决的问题。

本文使用串口的接收中断+超时判断方法解决了此问题,并给出了详细的教程,希望对读者朋友有所帮助。

另外,想进大厂的同学,一定要好好学算法,这是面试必备的。这里准备了一份 BAT 大佬总结的 LeetCode 刷题宝典,很多人靠它们进了大厂。

刷题 | LeetCode算法刷题神器,看完 BAT 随你挑!

有收获?希望老铁们来个三连击,给更多的人看到这篇文章

推荐阅读:

  • 程序员必备编程资料大全
  • 程序员必备软件资源

欢迎关注我的博客:良许嵌入式教程网,满满都是干货!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

4_企业架构双点服务器HA

企业架构双点服务器HA 学习目标和内容 1、能够描述高可用HA的作用 2、能够理解VIP的切换 3、能够描述keepalived作用 4、能够理解主master和备backup服务器关系 5、能够实现主备服务器高可用配置 6、能够实现模拟业务宕机服务切换 一、背景描述及其方案设计 1、业务背景描述 时…

activemq启动成功但web管理页面却无法访问

前提&#xff1a; 在linux启动activemq成功&#xff01;本地能ping通linux 处理方案&#xff1a; 确定防火墙是否关闭&#xff0c; 有两种处理方案&#xff1a;第一种-关闭防火墙&#xff1b;第二种-暴漏8161和61616两个端口 netstat -lnpt查看8161和61616端口 注意&#xf…

【算法每日一练]-图论(保姆级教程篇12 tarjan篇)#POJ3352道路建设 #POJ2553图的底部 #POJ1236校园网络 #缩点

目录 POJ3352&#xff1a;道路建设 思路&#xff1a; POJ2553&#xff1a;图的底部 思路&#xff1a; POJ1236校园网络 思路&#xff1a; 缩点&#xff1a; 思路&#xff1a; POJ3352&#xff1a;道路建设 由于道路要维修&#xff0c;维修时候来回都不能走&#xff0c;现要…

Python实战:批量加密Excel文件指南

更多Python学习内容&#xff1a;ipengtao.com 大家好&#xff0c;我是彭涛&#xff0c;今天为大家分享 Python实战&#xff1a;批量加密Excel文件指南&#xff0c;全文3800字&#xff0c;阅读大约10分钟。 在日常工作中&#xff0c;保护敏感数据是至关重要的。本文将引导你通过…

跟着Nature Communications学习Hisat-Trinity-PASA等分析流程

一边学习&#xff0c;一边总结&#xff0c;一边分享&#xff01; 详细教程请访问&#xff1a; 组学分析流程 本期分析流程 Hisat2-SamtoolsTrinity_GG_denovoPASA … 本期教程文章 题目&#xff1a;Genomic insights into local adaptation and future climate-induced vu…

untiy webgl常见问题与操作

文章目录 1 untiy和网页相互通信2 打开新页面&#xff08;同标签页和新标签页&#xff09;3 获取网页的URL4 解析Url内的参数5 后处理与色彩空间问题 1 untiy和网页相互通信 看这个文章 2 打开新页面&#xff08;同标签页和新标签页&#xff09; 先看本文untiy和网页相互通信…

代码随想录算法训练营 ---第五十三天

第一题&#xff1a; 简介&#xff1a; 本题和昨天的最大重复子串问题很相似&#xff0c;只不过本题不一定是连续的。 动规五部曲分析如下&#xff1a; 确定dp数组&#xff08;dp table&#xff09;以及下标的含义 dp[i][j]&#xff1a;长度为i-1 的字符串text1与长度为j-1的…

Git 分支详解

目录 1. Git 分支管理 2. 如何自己创建分支&#xff1f; 3. 创建分支修改内容&#xff0c;之后合并到主分支 4. 删除分支 5. 出现 merge 冲突如何解决 6. 分支策略 前言 之前只是知道有 master 分支这个东西&#xff0c;但是具体是啥意思还是不知道&#xff0c;今天详…

【面试HOT200】二叉树——广度优先搜索篇

系列综述&#xff1a; &#x1f49e;目的&#xff1a;本系列是个人整理为了秋招面试的&#xff0c;整理期间苛求每个知识点&#xff0c;平衡理解简易度与深入程度。 &#x1f970;来源&#xff1a;材料主要源于【CodeTopHot200】进行的&#xff0c;每个知识点的修正和深入主要参…

MFC 绘制单一颜色圆形、渐变颜色边框圆形、渐变填充圆形以及绘制三角函数正弦函数曲线.

MFC 绘制三种不同圆形以及绘制正弦函数曲线 本文使用visual Studio MFC 平台实现绘制单一颜色圆形、渐变颜色边框圆形、渐变填充圆形以及绘制三角函数正弦函数曲线. 关于基础工程的创建请参考 01-Visual Studio 使用MFC 单文档工程绘制单一颜色直线和绘制渐变颜色的直线 02-vis…

设计模式-结构型模式之适配器设计模式

文章目录 一、结构型设计模式二、适配器模式 一、结构型设计模式 这篇文章我们来讲解下结构型设计模式&#xff0c;结构型设计模式&#xff0c;主要处理类或对象的组合关系&#xff0c;为如何设计类以形成更大的结构提供指南。 结构型设计模式包括&#xff1a;适配器模式&…

【前沿技术】扩散模型是什么

0. 前言 扩散模型的灵感来自非平衡热力学。他们定义了一个马尔可夫扩散步骤链&#xff0c;以缓慢地将随机噪声添加到数据中&#xff0c;然后学习逆转扩散过程以从噪声中构建所需的数据样本。与VAE或流动模型不同&#xff0c;扩散模型是通过固定程序学习的&#xff0c;并且潜在变…

Linux下activemq的安装与安装成功确认

一、下载 apache-activemq-5.14.0-bin.tar.gz 二、安装 将压缩包拷入linux内&#xff0c;进行解压tar -zxvf apache-activemq-5.14.0-bin.tar.gz&#xff0c;与redis、nginx不同的是&#xff0c;active解压不需要安装就可以直接启动&#xff01; 启动命令&#xff1a;./bin…

PMP考试解析

PMP考试解析 考试动机 1、裁员太猛了&#xff0c;多一条生存技能 2、学习管理技能往管理方向靠 3、转行&#xff0c; 考试路径 1、报班费&#xff1a; 考PMP必须报班&#xff0c;不然没有考试资格&#xff08;无语的规定&#xff09; &#xff08;类似于考驾照&#xff0c;…

PhotoZoom 2024中文版全新版本震撼来袭!PhotoZoom 8怎么使用

PhotoZoom 2023&#xff08;PhotoZoom 8&#xff09;全新版本震撼来袭。 一款划时代的、技术上产生革命性影响的数码图片放大工具。 我们获取图片的方法&#xff0c;一般是从度娘图片和各个图库里找素材。但一般网上搜索到的很多图片像素都非常小&#xff0c;普通方法放大就像打…

DeDeCMS v5.7 SP2 正式版 前台任意用户密码修改(漏洞复现)

1.环境搭建 PHP 5.6 DeDeCMSV5.7SP2 正式版 安装phpstudy&#xff0c;https://www.xp.cn/小皮面板 先启动Apache2.4.39和MySQL5.7.26 如果他会让你下载&#xff0c;点击是就好&#xff01; 让后点击网站—>点击创建网站 域名自己创建&#xff0c;自己取 其他的不变 点击…

详解Spring对Mybatis等持久化框架的整合

&#x1f609;&#x1f609; 学习交流群&#xff1a; ✅✅1&#xff1a;这是孙哥suns给大家的福利&#xff01; ✨✨2&#xff1a;我们免费分享Netty、Dubbo、k8s、Mybatis、Spring...应用和源码级别的视频资料 &#x1f96d;&#x1f96d;3&#xff1a;QQ群&#xff1a;583783…

RPG项目01_脚本代码

基于“RPG项目01_场景及人物动画管理器”&#xff0c;我们创建一个XML文档 在资源文件夹下创建一个文件夹&#xff0c; 命名为Xml 将Xnl文档拖拽至文件夹中&#xff0c; 再在文件夹的Manager下新建脚本LoadManager 写代码&#xff1a; using System.Collections; using System…

【目标检测实验系列】YOLOv5创新点改进实验:通过转置卷积,动态学习参数,减少上采用过程特征丢失,提高模型对目标的检测精度!(超详细改进代码流程)

1. 文章主要内容 本篇博客主要涉及两个主体内容。第一个&#xff1a;简单介绍转置卷积的原理。第二个&#xff1a;基于YOLOv5 6.x版本&#xff0c;将Neck部分的upSample改为nn.ConvTranspose2d转置卷积&#xff08;通读本篇博客需要10分钟左右的时间&#xff09;。 小提…

QNX常用调试方法

QNX常用调试方法 1. top 查询系统状态最常用的工具是top&#xff0c;它可以显示系统资源的使用情况。我们最关心的通常是系统可用内存和CPU使用率。如果CPU使用率过高可能是因为某些应用存在bug&#xff0c;重点关注下面显示的占用CPU资源最多的几个线程。如果可用内存太少&am…