【STM32CubeMX串口通信详解】USART2 -- DMA发送 + DMA空闲中断 接收不定长数据

发布时间:2024年01月20日

(?本篇正在编写、更新状态中.....)?

文章目录:

前言? ? ? ??


前言? ? ? ??

????????本篇,详细地用截图解释?CubeMX 对 USART2?的配置,HAL函数使用,和收发程序的编写。

? ? ? ? 收、发机制:DMA发送 + DAM空闲中断接收。

? ? ? ? DMA+空闲中断的搭配,相当高效,而且最大地节省芯片运行资源。

? ? ? ? 本篇代码,适用于绝大部分的串口模块通信,如ESP8266、串口屏、蓝牙模块等。

? ? ? ? 不讲解串口通信原理,?串口通信原理,请自行扫盲:串口通讯工作原理!??

? ? ? ? 只详细地截图:CubeMX配置、代码编写过程。

? ? ? ? 如有错漏,欢迎指正。? ? ?

? ? ? ? 约定1:本篇所述USART通信,是指常用的异步通信,即UART。? ??


一、准备工作

1、接线

? ? ? ? 为了方便测试,在上篇USART1通信的基础上,增加USART2的代码。

? ? ? ? 使用USART2接串口通信模块,本文所用,是蓝牙模块:ECB02;

? ? ? ? 实验将通过串口助手(USART1),?对蓝牙模块ECB02 (USART2),进行调试和通信。

? ? ? ?

????????①? USART1的接线

?????????????使用了板载USB转TTL。USART1的PA9、PA10,?已连接USB转TTL。

? ? ? ? ? ? ?使用USB线,插到开发板上的CMSIS-DAP接口,无需外接USB转TTL模块。

????????

? ? ? ? ②? USART2接线

?????????本文引脚连接,如下表:

蓝牙?ECB02 模块STM32?开发板
RXDTX-PC10? (UART4)
TXDRX-PC11? ? (UART4)
GNDGND
VCC3.3V?

2、新建工程? ? ? ??

? ? ? ? 为了减少篇幅长度,复制之前已建立的工程:USART1 --?DMA发送+DMA空闲中断接收

????????复制它整个文件夹、重新修改文件夹名称,如:USART2 --?DMA发送+DMA空闲中断接收

? ? ? ? 技巧:只能修改工程文件夹名称,不要修改工程文件的名称,否则,CubeMX无法重新生成。

? ? ? ? 技巧:复制已有工程,是老司机的日常操作,能延用已写好的代码和配置,大大减少开发时间。

? ? ? ? 相关基础文章链接:

????????新建一个工程(STM32F103)

? ? ? ??新建一个工程(STM32F407)

????????GPIO 推挽输出模式,点亮LED灯
?

????????USART1 -- DMA发送 + DMA空闲中断 接收不定长数据


二、CubeMX的配置

? ? ? ? 串口通信的收发机制,有很多方式、组合。

? ? ? ? 本文,介绍操作最简单、实用的一种组合:

? ? ? ? 发送:DMA。

? ? ? ? 接收:DMA+空闲中断接收。

? ? ? ? 适用于绝大部分的串口模块通信,如ESP8266、串口屏、蓝牙模块等。

1、USART2?配置 异步通信

? ? ? ? 在选择异步通信后,将会使用默认引脚:TX-PA2, RX-PA3.

? ? ? ? 我们无需对引脚进行任何配置,CubeMX帮我们自动配置好!

2、通信协议参数

? ? ? ? 本篇,使用常用配置:115200-None-8-1。

? ? ? ? 如下图,4个主要的通信协议参数,一般只需修改波特率。蓝色的3项,基本万年不动。

3、使能中断

? ? ? ? USART1配置DMA后,会自动使能中断。

? ? ? ? USART2要手动打开中断。

? ? ? ??

4、打开DMA发送、接收? ? ? ??

? ? ? ? 网上很多教程,只使能了USART_RX的DMA,而不使能USART_TX的DMA。

? ? ? ? 除非,TX所用的DMA通道,已被其它设备占用了,否则,你为何能容忍它躺平不干活?!

? ? ? ?

????????添加完成后的状态:

5、设置RX引脚上拉

? ? ? ? 在选择异步通信后,CubeMX会自动配置引脚的工作模式。

? ? ? ? 这时的默认配置:不打开上下拉。这可能会使引脚在悬空状态时电平不确定,产生误接收。

? ? ? ? 我们把RX接收引脚的PA3,修改为:上拉(Pull-up),给引脚固定一个弱上拉,以避免悬空时产生误接收。

? ? ? ?

????????好了,就这么简单。

? ? ? ? 优先级配置,默认就行。DMA配置,默认就行。? ?

? ? ? ? 然后,点击?GENERATE CODE,生成工程吧!


三、发送操作、代码解释

? ? ? ? 我们打开生成后的Keil工程。? ?

? ? ? ? 在main.c文件能看到,已增加了DMA和USART1的初始化代码。

? ? ? ? ? ??

? ? ? ? 初始化部分,CubeMX都已帮我们编写好了,发送部分的底层处理、逻辑,CubeMX也编写好了。????????????????

? ? ? ? 在工程中,现在就能直接使用下面这 3个 函数,发送任何数据:

HAL_UART_Transmit     (&huart1, uint8_t *pData, uint16_t Num, 超时值);    
HAL_UART_Transmit_IT  (&huart1, uint8_t *pData, uint16_t Num);         
HAL_UART_Transmit_DMA (&huart1, uint8_t *pData, uint16_t Num);         

? ? ? ? 先上板测试,后面再解释!

????????在/* USER CODE BEGIN 2 */? 与 /* USER CODE END 2 */?之间,敲入以下发送代码:

    /* USER CODE BEGIN 2 */
    /* 用户代码,必须写在配对的BEGIN与END之间 */

    static char  strTem[100] = "Hello World!\r";                            // 定义一个数组,也可以是其它的数据,如结构体等
    HAL_UART_Transmit (&huart1, (uint8_t*)strTem, strlen(strTem), 0xFFFF);  // 发送方式2:HAL_UART_Transmit(), 不推荐使用; 阻塞式发送,当调用后,程序会一直死等,不干其它事了(中断除外),直到发送完毕
    HAL_UART_Transmit_IT (&huart1, (uint8_t*)strTem, strlen(strTem));       // 发送方式3:HAL_UART_Transmit_IT(), 推荐使用; 利用中断发送,非阻塞式,大大减少资源占用; 注意:当上次的调用还没完成发送,下次的调用会直接返回(放弃),所以,要想连接发送,两行调用间,要么判断串口结构体gState的值,要么调用延时HAL_Delay(ms), ms值要大于前一帧发送用时, 用时计算:1/(波特率*11*前一帧字节数) 
    while((&huart1)->gState != HAL_UART_STATE_READY);                       // 等待上条发送结束; 也可以用HAL_Delay延时法,但就要计算发送用时; 两种方法都是死等法,程序暂时卡死不会往下运行; 如果两次发送间隔时间大,如,大于100ms, 就不用判断语句了。
    HAL_UART_Transmit_DMA (&huart1,(uint8_t*)strTem, strlen(strTem));       // 发送方式4:HAL_UART_Transmit_DMA(),推荐使用; 利用DMA发送,非阻塞式,最大限度减少资源占用; 注意:当上次的调用还没完成发送,下次的调用会直接返回(放弃); 所以,要想连接发送,两行调用间,要么判断串口结构体gState的值,要么调用延时HAL_Delay(ms), ms值要大于前一帧发送用时,用时计算:1/(波特率*11*前一帧字节数)

    /* USER CODE END 2 */

? ? ? ? 打开电脑的串口助手。

????????编译、烧录。串口助手马上有显示:

? ? ? ? 如果你那边,烧录后没有显示,要么是串口号错了,要么是没有打勾keil的自动复位。

????????

????????下面,对3个函数的使用,逐一解释,不建议新手跳过,有避坑干货。

????????1、HAL_UART_Transmit ??(&huart1,? uint8_t *pData,? uint16_t Num,? 超时值); ?

? ? ? ? 阻塞式发送。参数:串口,数据地址,发送的字节数,ms超时值?

? ? ? ? 每发送一个字节,死等,好了继续发下一个,再死等,不断重复。

? ? ? ? 就是以前标准库种那最普通的死等法,只是它增加了一个超时值。

? ? ? ? 超时值:如果指定时间内没发送完毕,就直接返回,防止卡死。数据发送通信需时:

????????1秒?÷?波特率?×?字节数?× 10?× 1000ms。举例:115200波特率,100字节,大约用时 9ms。

????????新手如果不会计算,直接把超时值填大一点,如50ms。

? ? ? ? 2、HAL_UART_Transmit_IT ?(&huart1,? uint8_t *pData,? uint16_t Num); ?

????????利用中断发送。参数:串口,数据地址,发送的字节数

? ? ? ? 向寄存器填入一个字节,程序就继续干其它的事去,当一个字节发送完成后会产生发送中断,CubeMX生成的回调函数,自动填入下一个字节,不断重复,不用干预。? ?

? ? ? ? 非阻塞式发送。能大大地减少程序运行时间的占用。

? ? ? ? 有一点要注意:当连续地调用本中断发送函数时,调用的间隔时间,小于通信所需的用时(按上),这时,后面那条函数调用,会直接返回,放弃发送。?因为函数内部,在发送前会判断串口的忙状态,如果在忙(还在发送上一包数据),就放弃本包数据,返回。???

? ? ? ? 解决的方法,有两个:

? ? ? ? ①? 最常用的,两行中断发送函数间,插入:HAL_Delay(10),原理参考上面的发送需时。??

? ? ? ? ②? 两行中断发送函数间,插入 while((&huart1)->gState != HAL_UART_STATE_READY);?和?HAL_Delay()?一样,都是死等,但能省了那么一点点运行时间。

? 3、HAL_UART_Transmit_DMA (&huart1,? uint8_t *pData,? uint16_t Num); ? ? ?

? ? ? ? DMA发送。参数:串口,数据地址,发送的字节数。? ? ? ?

? ? ? ? 上面的中断发送函数,100个字节,会产生100次中断。这个DMA发送函数,全程只产生一次中断。

? ? ? ? 调用后,函数给DMA数据地址,DMA就自动开始搬砖,它会把数据逐字节搬运到串口的DR寄存器上,等串口发送完这个字节了,再自动搬运下一个,过程完全不占用程序运行资源。搬完了,就产生一个中断,给程序打个招呼。通常,我们程序上,把这个“招呼”也省略了,不用理会它。

? ? ? ? 3个发送函数中,推荐使用这个DMA发送函数,发送的最优解。

? ? ? ? 同样的,两行DMA发送函数间,注意发送间隔,否则放弃发送直接返回。处理方法同上。


四、printf?重定向到USART1

? ? ? ? 约定俗成地,常使用printf函数,输出一些调试信息。它能很灵活地控制输出字符串的格式。

? ? ? ? 约定俗成地,printf 常通过 USART1 输出数据到串口助手,而非USART2、3...。

? ? ? ? 要使用 printf?,需要做两个事:

? ? ? ? ①? 在文件头,插入: #include "stdio.h"?;

? ? ? ? ②? 重定向 printf,?使它能通过 USART1?输出。

? ? ? ? 把下面代码,复制到 main.c的 BEGIN 4?与 END 4 注释行之间,即可使用。

????????无需打勾“Use MicroLIB"。

#include <stdio.h>
#pragma import(__use_no_semihosting)

struct __FILE
{
    int handle;
};                                                         // 标准库需要的支持函数

FILE __stdout;                                             // FILE 在stdio.h文件
void _sys_exit(int x)
{
    x = x;                                                 // 定义_sys_exit()以避免使用半主机模式
}

int fputc(int ch, FILE *f)                                 // 重写fputc函数,使printf的输出由UART1实现,  这里使用USART1
{
    // 注意,不能使用HAL_UART_Transmit_IT(), 机制上会冲突; 因为调用中断发送函数后,如果上次发送还在进行,就会直接返回!它不会继续等待,也不会数据填入队列排队发送
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0x02);   // 使用HAL_UART_Transmit,相等于USART1->DR = ch, 函数内部加了简单的超时判断(ms),防止卡死
    return ch;
}

? ? ? ? 现在,试试我们的 printf 输出效果:

? ? ? ? 在刚才敲入代码的位置,在3个发送函数之前,添加一行printf,? 尝试输出系统运行时钟的值。

????????整体如下:????????

    /* USER CODE BEGIN 2 */
    /* 用户代码,必须写在配对的BEGIN与END之间 */
    printf("\r系统运行时钟:%d MHz\r", SystemCoreClock/1000000);             // 发送方式1:使用printf发送,它有灵活的格式化,很适合处理字符串; 注意:printf需要重定向fputc函数才能使用,否则程序会卡死; 本示例已重写fputc, 在main.c的底部附近 
    
    static char  strTem[100] = "Hello World!\r";                            // 定义一个数组,也可以是其它的数据,如结构体等
    HAL_UART_Transmit (&huart1, (uint8_t*)strTem, strlen(strTem), 0xFFFF);  // 发送方式2:HAL_UART_Transmit(), 不推荐使用; 阻塞式发送,当调用后,程序会一直死等,不干其它事了(中断除外),直到发送完毕
    HAL_UART_Transmit_IT (&huart1, (uint8_t*)strTem, strlen(strTem));       // 发送方式3:HAL_UART_Transmit_IT(), 推荐使用; 利用中断发送,非阻塞式,大大减少资源占用; 注意:当上次的调用还没完成发送,下次的调用会直接返回(放弃),所以,要想连接发送,两行调用间,要么判断串口结构体gState的值,要么调用延时HAL_Delay(ms), ms值要大于前一帧发送用时, 用时计算:1/(波特率*11*前一帧字节数) 
    while((&huart1)->gState != HAL_UART_STATE_READY);                       // 等待上条发送结束; 也可以用HAL_Delay延时法,但就要计算发送用时; 两种方法都是死等法,程序暂时卡死不会往下运行; 如果两次发送间隔时间大,如,大于100ms, 就不用判断语句了。
    HAL_UART_Transmit_DMA (&huart1,(uint8_t*)strTem, strlen(strTem));       // 发送方式4:HAL_UART_Transmit_DMA(),推荐使用; 利用DMA发送,非阻塞式,最大限度减少资源占用; 注意:当上次的调用还没完成发送,下次的调用会直接返回(放弃); 所以,要想连接发送,两行调用间,要么判断串口结构体gState的值,要么调用延时HAL_Delay(ms), ms值要大于前一帧发送用时,用时计算:1/(波特率*11*前一帧字节数)

    /* USER CODE END 2 */

? ? ? ?

????????串口助手的输出效果,如下图,能正常输出了!

? ? ? ? 然后,我们来试个错,把printf这行,剪切到3个发送函数之下。再编译,烧录运行。

? ? ? ? 怎样,没有输出了吧?!

? ? ? ? 原因、解决的方法,如上面发送函数所述,不再重述。? ? ?

? ? ? ? 上述3个函数,小编在测试时发现,还有两三处暂时没理解透的现象,但是发生的场景较少,这里就先不啰嗦了,等以后有更深入的理解时,再更新上来。


五、接收代码的编写

? ? ? ? 发送数据可以调用现成的函数,而接收数据,现成函数不太好用。

? ? ? ? 接收也有3个函数,和发送的3个函数相对应:

HAL_UART_Receive     (&huart1, uint8_t *pData, uint16_t Num, 超时值);    
HAL_UART_Receive_IT  (&huart1, uint8_t *pData, uint16_t Num);         
HAL_UART_Receive_DMA (&huart1, uint8_t *pData, uint16_t Num);         

? ? ? ? 一般,大家都不使用这三个函数,太TM的难用了,有兴趣的可csdn搜它们的使用优劣分析。

? ? ? ? 我们利用HAL库现成的资源,另敲十来行代码,令串口的接收机制:更实用、更灵活。

? ? ? ? 完成后,整个接收过程,1个结构体 + 1个HAL库函数 + 1个回调函数,全程自动接收。

? ? ? ? 共分4小项,下面将有详细操作图解:

? ? ? ? ①? 定义一个结构体变量:存放接收的字节数、数据数组。

? ? ? ? ②? 开启DMA:让硬件自动接收数据放到缓存

? ? ? ? ③? 重写回调函数:当一帧数据接收好了,把缓存的数据,转存到全局结构体变量里,备用。

? ? ? ? ④? 在需要使用串口接收的地方,如在while中,判断接收字节数>0,? 即为接收到新一帧数据了。

1、定义一个结构体变量:存放接收的字节数、数据

? ? ? ? ? 首先,在main.h文件,新建一个结构体类型

? ? ? ? 在?/* USER CODE BEGIN ET */? 与?/* USER CODE END ET */?之间,新建一个结构体类型。

/* USER CODE BEGIN ET */
/* 所有用户代码,必须写在配对的BEGIO与END注释行之间,否则重新生成时会被删除 */

typedef struct                         // 声明一个结构体,方便管理变量
{
    uint16_t  ReceiveNum;              // 接收字节数
    uint8_t   ReceiveData[512];        // 接收到的数据
    uint8_t   BuffTemp[512];           // 接收缓存; 注意:这个数组,只是一个缓存,用于DMA逐个字节接收,当接收完一帧后,数据在回调函数中,转存到 ReceiveData[ ] 存放。即:双缓冲,有效减少单缓冲的接收过程新数据覆盖旧数据
} xUSATR_TypeDef;

extern xUSATR_TypeDef xUSART1 ;        // 定义结构体,方便管理变量。也可以不用结构体,用单独的变量

/* USER CODE END ET */

? ? ? ? 它有3个成员:? ?

????????uint16_t ?ReceiveNum;? ? ? ? ? ? // 接收字节数,只要字节数>0,即为接收到新一帧数据
????????uint8_t ? ReceiveData[512];? ? ?// 接收到的数据
????????uint8_t ? BuffTemp[512]; ? ? ? ? ? //?临时缓存,在DMA空闲中断中将把一帧数据复制到ReceivedData[ ]? ?

? ? ? ? 有些教程,还有一个Flag变量,用来标记是否接收到数据。我们直接用ReceiveNum判断,更简单。

? ? ? ? 在定义结构体类型的下面一行代码中,用extern声明了一个结构体变量,它将在main.c中定义。? ??

????????技巧:如果希望定义的结构体类型,工程全局可用,就要在h文件中定义,其它文件引用这个h文件。? ? ? ??

? ? ? ? 技巧:如果希望定义的变量,能被工程全局调用,就在h文件中用extern声明,然后在某个c文件中定义。

? ? ? ?②? 回到main.c,定义结构体变量

????????在 /*? USER CODE BEGIN 0 */?与?/*? END 0 */?之间,使用新建的结构体类型,定义我们的结构体变量。

/* Private user code ---------------------------------------------------------*/
/* USER CODE BEGIN 0 */
/* 所有用户代码,必须写在配对的BEGIO与END注释行之间,否则重新生成时会被删除 */

xUSATR_TypeDef xUSART1 = {0};          // 定义结构体,方便管理变量。也可以不用结构体,用单独的变量

/* USER CODE END 0 */

????????现在,我们拥有一个了全局变量:xUSART1。

? ? ? ? 以后的其它文件,如蓝牙模块驱动、串口屏驱动,只要在文件中引用:main.h,就能通过这个结构体变量,使用串口1接收的数据了。? ??

?????????

2、开启DMA,让硬件自动接收数据

????????我们整个接收过程,仅使用到1个HAL库函数,

????????只需在main()函数的初始化部分,调用HAL库函数:

????????HAL_UARTEx_ReceiveToIdle_DMA (串口、缓存、字节数) ;

????????参数:串口、接收缓存区、最大接收字节数???

? ? ? ? 作用:使能DMA、使能串口的空闲中断,正式进入接收状态。??? ? ??? ? ? ? ? ?

? ? ? ? 操作:在?main.c的?? /* USER CODE BEGIN 2 */? 与? /* END 2 */?之间,插入函数:

HAL_UARTEx_ReceiveToIdle_DMA(&huart1, xUSART1.BuffTemp, sizeof(xUSART1.BuffTemp)); ?// 开启DMA空闲中断 ? ?

? ? ? ? 插入后的位置,如下图:

? ? ? ??

? ? ? ? 调用函数后,硬件就会立刻进入自动接收状态:从RX引脚接收到的数据,会逐个字节顺序存放到指定缓存中,这里我们指定的缓存是:xUSART1.BuffTemp。

? ? ? ? 因为函数内部,开启了DMA中断、空闲中断,所以达成下列两个条件之一,就会触发中断:

? ? ? ? ①? DMA接收的字节数,达到了参数中的最大值
? ? ? ??②? 串口发生空闲中断,即RX引脚,超过1字节的时间,没有新信号。? ? ? ??

? ? ? ? 当上述中断产生时,硬件自动调用其相关的中断服务函数,再继而调用回调函数。

????????CubeMX生成的代码,已编写好上述两个中断服务函数,还定义了一个它俩最终调用的回调函数。注意,这个回调函数是一个弱函数。

? ? ? ? 因此,我们不用管中断服务函数,只需重写这个回调函数,就能实现对接收数据的处理。? ?

3、重写DMA空闲中断回调函数

????????DMA完成中断、空闲中断,所调用的回调函数:

????????HAL_UARTEx_RxEventCallback(串口,接收到的字节数);? ? ? ?

????????弱函数定义在stm32xx_hal_gpio.c文件的底部。??????

? ? ? ? 现在,我们对它进行重写,以实现对接收数据的处理。

????????在main.c的底部,/* USER CODE BEGIN 4 */?与?/*? END 4 */?之间,新建函数,并编写其代码:

/* USER CODE BEGIN 4 */
/* 所有用户代码,必须写在配对的BEGIN与 END之间 */

/******************************************************************************
?* 函 ?数: HAL_UARTEx_RxEventCallback
?* 功 ?能: DMA+空闲中断回调函数
?* 参 ?数: UART_HandleTypeDef ?*huart ? // 触发的串口
?* ? ? ? ? ?uint16_t ? ? ? ? ? ? Size ? ?// 接收字节
?* 返回值: 无
?* 备 ?注: 1:这个是回调函数,不是中断服务函数。技巧:使用CubeMX生成的工程中,中断服务函数已被CubeMX安排妥当,我们只管重写回调函数
?* ? ? ? ? ?2:触发条件:当DMA接收到指定字节数时,或产生空闲中断时,硬件就会自动调用本回调函数,无需进行人工调用;
?* ? ? ? ? ?2:必须使用这个函数名称,因为它在CubeMX生成时,已被写好了各种函数调用、函数弱定义(在stm32xx_hal_uart.c的底部); 不要在原弱定义中增添代码,而是重写本函数
?* ? ? ? ? ?3:无需进行中断标志的清理,它在被调用前,已有清中断的操作;
?* ? ? ? ? ?4:生成的所有DMA+空闲中断服务函数,都会统一调用这个函数,以引脚编号作参数
?* ? ? ? ? ?5:判断参数传进来的引脚编号,即可知道是哪个串口接收收了多少字节
******************************************************************************/
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
? ? if (huart == &huart1) ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 判断串口
? ? {
? ? ? ? __HAL_UNLOCK(huart); ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 解锁串口状态

? ? ? ? xUSART1.ReceiveNum ?= Size; ? ? ? ?  ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 把接收字节数,存入结构体xUSART1.ReceiveNum,以备使用
? ? ? ? memset(xUSART1.ReceiveData, 0, sizeof(xUSART1.ReceiveData)); ?   ? ? ? ? ? ? ? ? ? ? // 清0前一帧的接收数据
? ? ? ? memcpy(xUSART1.ReceiveData, xUSART1.BuffTemp, Size); ?  ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 把新数据,从临时缓存中,复制到xUSART1.ReceiveData[], 以备使用
? ? ? ? HAL_UARTEx_ReceiveToIdle_DMA(&huart1, xUSART1.BuffTemp, sizeof(xUSART1.BuffTemp)); ? // 再次开启DMA空闲中断; 每当接收完指定长度,或者产生空闲中断时,就会来到这个
? ? }
}

/* USER CODE END 4 */

? ? ? ? 上面代码,我们重点解释后四行:? ? ? ? ?

????????①??xUSART1.ReceiveNum ?= Size; ?
? ? ? ? ?????把接收的字节数,存入结构体 xUSART1.ReceiveNum,以备使用 。
? ? ? ? ? ? ?在程序的其它地方,判断 ReceivNum > 0,?就能知道是否收到新一帧数据了。? ? ? ??

????????②? memset(xUSART1.ReceivedData, 0, sizeof(xUSART1.ReceivedData)); ?
? ? ? ? ?????清0前一帧的数据缓存?? ? ? ? ? ? ?

????????③? memcpy(xUSART1.ReceivedData, xUSART1.BuffTemp, Size);
? ? ? ? ? ? ?把新数据,从临时缓存中,复制到xUSART1.ReceivedData[], 以备使用??
? ? ? ? ? ? ?从结构体和这段回调函数中,可以发现,这是一个双缓存的操作思路。
? ? ? ? ? ? ?.ReceivedData用于存放接收后完整的一帧数据,对外使用?。
? ? ? ? ?????.BuffTemp用于DMA接收过程,是一个中间缓存。

? ? ? ? ④??HAL_UARTEx_ReceiveToIdle_DMA(&huart1, xUSART1.BuffTemp, sizeof(xUSART1.BuffTemp));
? ? ? ? ? ? ?再次开启DMA空闲中断,进入接收状态。
? ? ? ? ? ? ?我们在main()函数的初始化部分,已调用过这个函数了,为什么要在回调函数中再次调用?
? ? ? ? ? ? ?因为在DMA的中断服务函数里,会关闭DMA,即只接收一次。所以,在接收完一帧后,再次调用函数,就能让DMA开始工作接收下一帧。在这个位置调用 ,能让DMA不断地循环工作。
? ? ? ? ? ? ?其实,在CubeMX配置中,DMA有一个选项 :Mode的circular,?可以让DMA进行连续地的工作,接收完成后,无需在回调函数里再次开启DMA?。但是,目前的CubeMX版本(V6.10),这个参数的选择,会使我们上面的DMA接收与发送,相冲突。那我们二选一好了,自行手工调用。

? ? ? ? 注意一点:本篇的处理,是保存最后一帧数据。当有新一帧数据来了,会自动盖掉旧帧数据。

? ? ? ? 至此,接收工作已准备妥当。程序运行后,硬件会自动接收,并把接收的帧数据,存放到结构体中。? ?

4、接收的使用示范

????????我们来试试使用的效果吧!

? ? ? ? ①? 在main.c的while函数里,编写接收判断代码:

/* USER CODE BEGIN WHILE */
? ? while (1)
? ? {
? ? ? ? /* USER CODE END WHILE */

? ? ? ? /* USER CODE BEGIN 3 */
? ? ? ? /* 用户代码,必须写在配对的BEGIN与END之间 */

? ? ? ? if (xUSART1.ReceiveNum)? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? //?判断字节数
? ? ? ? {
? ? ? ? ? ? printf("\r<<<<< USART1 接收到一帧数据 \r"); ? ? ? ? ? ? ? ? ?// 提示
? ? ? ? ? ? printf("字节数:%d \r", xUSART1.ReceiveNum);? ? ? ? ? ?? // 显示字节数
? ? ? ? ? ? printf("ASCII : %s\r", (char *)xUSART1.ReceiveData);? ? // 显示数据,以ASCII方式显示,即以字符串的方式显示
? ? ? ? ? ? printf("16进制: ");? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 显示数据,以16进制方式,显示每一个字节的值
? ? ? ? ? ? for (uint16_t i = 0; i < xUSART1.ReceiveNum; i++) ? ? ? ? ?// 逐个字节输出
? ? ? ? ? ? ? ? printf("0x%X ", xUSART1.ReceiveData[i]); ? ? ? ? ? ? ? ? ? // 以16进制显示
? ? ? ? ? ? printf("\r\r");? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 显示换行

? ? ? ? ? ? xUSART1.ReceiveNum = 0;? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?// 清0接收标记
? ? ? ? }

? ? }
? ? /* USER CODE END 3 */

? ? ? ? ? ? ?

②? 工程,编译,烧录!

③? 打开串口助手,参数设置 115200-None-8-1,?打开对应的串口端口。

? ? ?按一下板子右下角的复位键,串口输出,如下图:

③? 在串口的发送区,输入字符串 "天气不错喔~~",或者其它数据。

? ? ?点击发送:串口助手将通过PA10,发送到开发板。在程序的while函数中,那段代码判断接收到数据后,为了方便观察,将通过USART1的PA9发出数据,串口助手接收后,显示如下:

④? 试试16进制数据的发送。

? ? ?发送区:打勾16进制发送,输入随意16进制值,不用加0x,用空格作间隔。

? ? ?注意,16进制的值,不一定是ASCII码表的显示范围值,所以在ASCII显示中,会出现乱码,正常现象。? ? ?

??

至此,USART1的收发,已完整地展示完毕。

如有错漏,欢迎留言指正修改~~


USART2

USART3

UART4

UART5

USART6

文章来源:https://blog.csdn.net/qq_49053936/article/details/135558209
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。