【STM32+CubeMX+HAL库教程】USART1 DMA发送、DMA空闲中断 接收不定长数据

本文详细图解了通过CubeMX对STM32的UART1进行配置、串口通信HAL库函数使用及收发程序编写。介绍了准备工作、CubeMX配置步骤,解释发送操作函数,实现printf重定向,还给出接收代码编写方法及使用示范,适用于多种串口模块通信。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本篇将详细地图解:

  1. 通过CubeMX 对 UART1 进行配置;
  2. 串口通信的 HAL库函数使用;
  3. 收、发程序的编写;

文章目录:

前言        

一、准备工作

1、接线

2、新建工程        

二、CubeMX的配置

1、USART1 配置 异步通信

2、通信协议参数

3、打开DMA发送、接收

三、发送操作、代码解释

四、printf 重定向到USART1

五、接收代码的编写

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

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

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

4、接收的使用示范


前言        

本篇将详细地图解:

  1. 通过CubeMX 对 UART1 进行配置
  2. HAL函数使用
  3. 收发程序的编写

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

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

不讲:串口通信原理 (请自行扫盲:串口通讯工作原理!)  

只讲:实际使用操作;

    

提醒:

  1. STM32的USART外设,包括了:同步通信USART、异步通信UART;
  2. 目前常用的串口设备,基本都是使用异步通信UART,而非同步通信USART。
  3. 虽然使用的是UART,但称呼习惯上,网上一般都叫它“USART”。
  4. 本篇,  将使用“UART”作命名。   

一、准备工作

1、接线

大部分的开发板(核心板除外),不管哪家的,基本都设计了USB转TTL电路。

尽管各种USB转TTL设计电路有不同,如CH340、CP2102、虚拟串口等,但是使用上是一致的。

另外 ,各板商的USB转TTL电路,一般都都会使用:UART1 (TX-PA9、RX-PA10)。 

在开发板上,如果有多个USB座,找到对应的接口,插上USB线,即可完成接线操作  !      
 

        

2、新建工程        

为了展示如何在已有工程上添加串口功能,这里不另行新建工程。

直接使用已建立的工程,例如,使用之前已有的点灯工程:GPIO_推挽输出模式 点亮LED灯

复制、粘贴已有的工程文件夹,修改文件夹名称,如:UART1_DMA发送+DMA空闲中断接收

提醒:

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

2、复制现有工程,是“新建工程"的更优操作,能延用已调试好的配置、功能代码,大幅度地减少开发时间。

相关文章传送链接:

        新建一个工程(STM32F103)

        新建一个工程(STM32F407)

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


二、CubeMX的配置

串口通信程序的代码 编写,主要分为两个部分:初始化、收发处理。        

以前使用标准库,进行串口通信的代码编写,那是相当地耗时,需要不断调试排错。

CubeMX出现后, 图形化生成初始化配置,后面通过HAL库的函数调用,能轻松地实现串口通信。

在CubeMX上,初始化的配置过程,极度的简便,只需简单的几步,1分钟也用不了。

我们先双击工程文件夹中的ioc文件,打开CubeMX的工程配置 。

1、USART1 配置 异步通信

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

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

2、通信协议参数

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

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

        

3、打开DMA发送、接收        

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

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

       

        添加完成后的状态:

4、使能USART1中断

        这一步,要记得手动打勾,不然有部分HAL函数无法使用。

5、设置RX引脚上拉

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

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

        我们把RX接收引脚,修改为:上拉(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,而不直接使用上面那几个发送函数?

        因为printf能控制格式!如,在字符串中插入变量值,而上面的发送函数就没它方便了;

        另外 ,printf通常用于调试阶段。当调试完成了,我们无除一行行地查找和取消printf的输出,只要注释了重定向函数中的发送函数行,即可取消整个工程中的printf调试信息输出。

        要使用 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个函数相对应:

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库现成的资源,另敲十来行代码,令串口的接收机制:更实用、更灵活。

        完成后,整个接收过程,将全程自动接收,在外部判断是否收到新数据即可。

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

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

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

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

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

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

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

        如何打开main.h?

        方法1:在main.c中,右击空白位置,弹出菜单中选择:Toggle Header/Code File

        方法2:在文件树中,点击main.c左边的+号,即可看到关联的文件,双击其中的main.h。

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

        新声明的结构体类型,它有3个成员:   

        uint16_t  RxNum;            // 接收字节数,只要字节数>0,即为接收到新一帧数据
        uint8_t   RxData[512];     // 接收到的数据(对外使用)
        uint8_t   RxTemp[512];    // 临时缓存(对内作接收),在DMA空闲中断中将把其内容复制到RxData[ ]   

        有些网上的教程,还会有一个Flag成员变量,用作标记是否接收到数据。 无需Flag变量,直接判断 RxNum > 0 即可,更直接、清晰。

        为什么要用两个缓存?双缓存结构,能增加数据处理上的时间空间,以避免单缓存在未及时处理数据时又被新的接收过程所覆盖。

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

技巧

  • 当希望声明的结构体类型能被全局可调用,就在h文件中定义,其它文件引用这个h文件。
  • 当希望定义的变量能被全局可调用,可在c文件中定义,然后在h中用extern声明,其它文件引用这个h文件。
  • 上述声明结构体时用x作名称开头,这不是语法的规定,只是个人命名习惯,表示这个变量是一个结构体。

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

        在 /*  USER CODE BEGIN 0 */ 与 /*  END 0 */ 之间,用刚才声明的结构体类型,定义我们的结构体变量。

      (这里同时定义了1~6的结构体变量,只是为了方便编写示例。)        

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

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

        上述结构体的声明、定义,还有全局变量的使用方法,新手注意理解,做项目时相当好使。

         

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

        在main()函数的初始化部分,增加一行代码,使DMA接收开始工作:

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

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

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

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

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

        插入后的位置,如下图:

        

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

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

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

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

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

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

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

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

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

        这个弱函数,定义在stm32xxxx_hal_uart.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);                                                          // 解锁串口状态

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

/* USER CODE END 4 */

        重点解释上面代码的后四行:         

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

        ②  memset(xUART1.RxData, 0, sizeof(xUART1.RxData));  
             清0数组中的数据,准备存入新数据          

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

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

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

        至此,接收工作已准备妥当。程序运行后,硬件、程序会自动开始接收工作,并把接收的帧数据,存放到结构体中: 最新一帧的字节数保存在xUART1.RxNum,  最新一帧的数据保存在xUART1.RxData。   

4、接收的使用示范

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

        ①  在main.c的while函数里,判断xUART1.RxNum,  即可知道是否收到新数据。

        具体操作方法如下:

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

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



        /** UART1 的接收示范(通过串口助手自发自收) **/
        if (xUART1.RxNum)
        {
            // 获取数据
            uint16_t rxNum = xUART1.RxNum;                           // 获取接收到的字节数
            uint8_t *rxData = xUART1.RxData;                         // 获取接收到数据(地址)
            // 输出、处理数据
            printf("\r<<<<< UART1 接收到一帧数据 \r");               // 提示
            printf("字节数:%d \r", rxNum);                          // 显示字节数
            printf("ASCII : %s\r", (char *)rxData);                  // 显示数据,以ASCII方式显示,即以字符串的方式显示
            printf("16进制: ");                                      // 显示数据,以16进制方式,显示每一个字节的值
            for (uint16_t i = 0; i < rxNum; i++)                     // 逐个字节输出
                printf("0x%X ", rxData[i]);                          // 以16进制显示
            printf("\r\r");                                          // 显示换行
            // 清0接收字节数                                           
            xUART1.RxNum = 0;                                        // 清0接收标记
        }

    }
    /* USER CODE END 3 */

             

②  工程,编译,烧录!

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

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

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

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

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

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

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

  

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

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


使用DMA空闲中断接收不定长数据的步骤如下: 1. 初始化串口和DMA:配置串口为接收模式,启用DMA传输;配置DMA为循环模式,传输大小为1字节,传输方向为从串口接收数据到内存。 2. 开启DMA传输:调用DMA启动函数启动DMA传输。 3. 开启串口接收中断:调用串口中断使能函数,开启空闲中断。 4. 在空闲中断中处理数据:当DMA传输完成并且串口没有接收到新数据时,说明接收完成,可以在空闲中断中处理接收到的数据。 下面是一个简单的例子: ```c #include "stm32f4xx_hal.h" #define RX_BUF_SIZE 256 UART_HandleTypeDef huart1; DMA_HandleTypeDef hdma_usart1_rx; uint8_t rx_buf[RX_BUF_SIZE]; uint8_t rx_len = 0; uint8_t rx_flag = 0; void UART_Init(void) { huart1.Instance = USART1; huart1.Init.BaudRate = 115200; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); } hdma_usart1_rx.Instance = DMA2_Stream2; hdma_usart1_rx.Init.Channel = DMA_CHANNEL_4; hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE; hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_usart1_rx.Init.Mode = DMA_CIRCULAR; hdma_usart1_rx.Init.Priority = DMA_PRIORITY_LOW; hdma_usart1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE; if (HAL_DMA_Init(&hdma_usart1_rx) != HAL_OK) { Error_Handler(); } __HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx); HAL_NVIC_SetPriority(DMA2_Stream2_IRQn, 0, 0); HAL_NVIC_EnableIRQ(DMA2_Stream2_IRQn); HAL_UART_Receive_DMA(&huart1, rx_buf, RX_BUF_SIZE); } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart == &huart1) { rx_len += RX_BUF_SIZE - hdma_usart1_rx.Instance->NDTR; HAL_UART_Receive_DMA(huart, rx_buf, RX_BUF_SIZE); } } void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart) { if (huart == &huart1) { rx_len += RX_BUF_SIZE / 2 - hdma_usart1_rx.Instance->NDTR; } } void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { if (huart == &huart1) { __HAL_UART_CLEAR_PEFLAG(&huart1); HAL_UART_Receive_DMA(huart, rx_buf, RX_BUF_SIZE); } } void DMA2_Stream2_IRQHandler(void) { HAL_DMA_IRQHandler(&hdma_usart1_rx); } void HAL_UART_IdleCallback(UART_HandleTypeDef *huart) { if (huart == &huart1) { if (hdma_usart1_rx.Instance->NDTR == RX_BUF_SIZE) { rx_len = RX_BUF_SIZE; rx_flag = 1; } else { rx_len = RX_BUF_SIZE - hdma_usart1_rx.Instance->NDTR; } } } int main(void) { HAL_Init(); UART_Init(); while (1) { if (rx_flag) { // 处理接收到的数据 rx_flag = 0; } } } ``` 在上面的例子中,我们使用了循环DMA传输模式,当接收到一定数量的数据后,将触发空闲中断,并在空闲中断中处理接收到的数据。同时,在DMA传输完成和空闲中断中,我们使用了两个不同的回调函数,分别处理DMA传输完成和空闲中断的事件。
评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值