STM32CubeMX HAL库串口+DMA+IDLE空闲中断不定长度数据接收和发送

本文开发环境:

  • MCU型号:STM32F103ZET6
  • IDE环境: MDK 5.29
  • 代码生成工具:STM32CubeMx 5.3.0
  • HAL库版本:STM32Cube_FW_F1_V1.8.0(STM32Cube MCU Package for STM32F1xx Series)

本文内容:

  1. 不定长数据接收的原理
  2. IDLE空闲中断介绍
  3. 串口接收IDLE空闲中断的配置
  4. 串口收发DMA线的配置
  5. 示例程序及起运行流程
  6. 附件:代码工程(MDK)

一,不定长数据接收的原理及其解决的方法

在 STM32 中。USART 发送接收有三种基本方式,轮询、中断和 DMA。轮询方式为堵塞模式,使用超时管理机制。它每次接收一个字节,在规定时间内接收固定长度的数据。在对于某些数据不固定长度接收的数据,轮询的方式有时候不够灵活。也可以使用中断的方式,如每一个字节都中断一次,当时比较消耗系统资源。特别是HAL库中,从中断到回调函数运行了不少的程序,频繁的中断很可能造成数据溢出。为了避免这个问题,我们使用指定接收一定长度的数据,再调用回调函数,这会让我们可以接收大数据,但是这种情况则造成了,要求每次的包是固定长度。为了解决以上一些问题,网上最常用的办法是使用空闲中断,即在串口空闲的时候,触发一次中断,通知内核,本次运输完成了。数据传输过程为了尽量不占用CPU的处理数据时间,所以就使用DMA接收串口的数据。

二,DMA

DMA(Direct memory access),即直接存储器访问。用于在外设与存储器之间以及存储器与存储器之间提供一种高速数据传输的方式。它在开始发送和接收完成数据时会给CPU相应的信号或者中断,在数据传输过程中无需CPU参与,通过硬件方式为RAM与I/O设备提供一条直接传送数据的通道。

1. DMA串口发送和接收函数

1.1 DMA发送函数

HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
函数主要功能是以DAM模式发送pData指针指向的数据中固定长度的数据,并同时设置和使能DMA中断,具体怎么设置和使能中断的,打开此函数源码会发现下面这个函数。
HAL_DMA_Start_IT(huart->hdmatx, *(uint32_t *)tmp, (uint32_t)&huart->Instance->DR, Size);
此函数是启动DMA传输并启用中断。从函数源码可以看出此函数首先判断DMA传输状态是否是Ready:

  • 如果是Ready则使能了DMA三个中断:DMA 半传输,DMA传输完成和DMA传输出错。如果发送数据正常,进入2次进入DMA中断(DMA 半传输和DMA传输完成);错误的话进入DMA传输出错中断;从这里也能看出HAL库比标准库严谨但效率低,DMA 半传输中断如果觉得效率低可以在程序中屏蔽掉,这样数据正常发送完成就只会进入一次DMA完成中断,三种DMA中断其实是同一个函数HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma)
    此函数功能是处理DMA中断请求,主要工作是清除中断标志位,改写DMA的状态,只有把状态改成HAL_DMA_STATE_READY,下一次才能正常使用DMA功能,否则会进入 HAL_BUSY状态
  • 如果不是Ready状态,则进入HAL_BUSY状态,这也是为什么连续使用HAL_UART_Transmit_DMA()函数发送数据,第二次会发不出来数据,而且第二次函数会进入HAL_BUSY状态,所以要想使用HAL_UART_Transmit_DMA()函数连续发送数据,相邻两次之间要有延时间隔或者检测DMA数据是否完成

1.2 DMA接收函数

HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
函数说明:此函数的功能在DMA模式下接收大量数据,同时设置DMA线和哪个串口外设连接,以及将DMA线接收到的数据搬 *pData对应地内存中,和上面DMA发送函数一样,此函数同时具有设置和使能DMA中断的功能。可以看出此函数不仅是一个接收函数同时也是一个初始化函数,在main()之前调用,初始化串口和DMA连接和DMA接收BUF,以及设置和使能中断。如果DMA模式设置成循环模式时,只需设置这一次,如果DMA模式设置成正常模式时,每次读取完数据后需要再从新设置一次(就是再调用一次此函数),分析函数源码会发现此函数内部同样会调用以下两个函数,使用方法和分析和发送类似,不再赘述。

  • HAL_DMA_Start_IT(huart->hdmatx, *(uint32_t *)tmp, (uint32_t)&huart->Instance->DR, Size);
  • HAL_DMA_IRQHandler(DMA_HandleTypeDef *hdma);

主要说说HAL_UART_Receive_DMA(),怎么配合IDLE串口空闲中断使用,main()函数之前一般调用此函数,一个主要目的是指明DMA传输串口数据存到指定的地方。一般情况我们会开辟一个全局变量的缓存
extern uint8_t receive_buff[BUFFER_SIZE]。比如函数初始化为HAL_UART_Receive_DMA(&huart2, (uint8_t*)receive_buff, BUFFER_SIZE); 就是设置串口2接收到数据通过DMA线直接到receive_buff中了,配合串口空闲中断,当进入串口空闲中断,说明一帧数据已接收完成。我们读取receive_buff相应长度的数据就是此次接收一帧的数据,这里还需要再介绍一个函数;

  • __HAL_DMA_GET_COUNTER(__HANDLE__)

此函数的功能:获取当前DMA通道传输中 receive_buff[BUFFER_SIZE]缓存还剩余多少个数据单元。这样就能算出这一帧数据到底接收了多少单元的数据(数据长度=缓存总长度-缓存剩余的长度),
length = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx);

三,空闲中断

1. 空闲中断介绍

  • 空闲中断是接受数据后出现一个byte的高电平(空闲)状态,就会触发空闲中断.并不是空闲就会一直中断,准确的说应该是上升沿(停止位)后一个byte,如果一直是低电平是不会触发空闲中断的(会触发break中断)。所以为了减少误进入串口空闲中断,串口RX的IO管脚一定设置成Pull-up<上拉模式>,串口空闲中断只是接收的数据时触发,发送时不触发。

2. 空闲中断使用

  • 串口空闲中断的判定是:当串口开始接收数据后,检测到1字节数据的时间内没有数据发生,则认为串口空闲了,进入相应的串口中断。在中断内清除空闲中断标志位和调用串口回调函数,在回调函数内处理读取,判断,处理接收的一帧数据。处理完一帧数据以后我再把串口中断打开重复上面的流程,就可以完整的接收一帧一帧的数据。同时利用空闲中断也可以省去很多的的判断。

3. 空闲函数调用

  • 首先在main()之前初始化的时候调用__HAL_UART_ENABLE_IT(&huart2,UART_IT_RXNE);
    函数的功能是打开了串口的接收中断。注意这个时候我还没有打开空闲中断。而是在接收到了一个byte以后打开空闲中断。
  • 当发送一帧数据接收完成后,会进入串口中断函数,如下函数
    HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
    HAL库提供的这个串口中断函数,并没有针对空闲中断的处理,所以得我们自己加相应的代码。
if(RESET != __HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE))   //判断是否是空闲中断
{
    __HAL_UART_CLEAR_IDLEFLAG(&huart2);                     //清楚空闲中断标志(否则会一直不断进入中断)
    USAR_UART_IDLECallback(huart);                          //调用中断回调函数
}

四,STM32CubeMX中UART和DMA配置

1. 新建STM32CubeMX

点击Start My Project from Mcu 下的 ACCESS TO MCU SELECTOR,如下图所示
STM32CubeMX HAL库串口+DMA+IDLE空闲中断不定长度数据接收和发送_第1张图片
可以通过多种方法来确定一个芯片,比如Core,Series等等,但是通过Part Number来确定是很便捷的。首先,我们在Part Number的搜索框中输入STM32F103ZE,得到了该芯片系列,接着在MCUs List列表中选择STM32F103ZETx确定具体芯片。由于芯片型号的最后一个字母表示包装方式,对生成代码没有影响,所以这里用x来表示可以是任何值,当选中STM32F103ZETx后,MCU 列表上的简介窗口出现了该芯片的信息,右上角的 Start Project也变成了有效按钮,可点击Start Project按钮新建工程,或者双击选中的芯片。
STM32CubeMX HAL库串口+DMA+IDLE空闲中断不定长度数据接收和发送_第2张图片

2. 系统时钟设置

2.1 选择时钟源

MCU有多种时钟源可以选择,如外部高速时钟(HSE),外部低速时钟(LSE),还有内部时钟等。本文硬件平台已经接入一个8M的晶振。这里以外部高速时钟作为系统时钟,首先展开System Core,点击RCC,在Mode栏的 High Speed Clock(HES)选项框选择Crystall/Ceramic Resonator,如果你的板子没有外部晶振,那么也可以使用内部时钟。其余的保持默认,可以看到右侧芯片引脚23,24已经变成绿色,表示已经使用。如下图所示:
STM32CubeMX HAL库串口+DMA+IDLE空闲中断不定长度数据接收和发送_第3张图片

2.2 配置系统时钟和各外设时钟

现在我们只是使能了外部时钟,接着我们进入Clock Configuration来配置系统的时钟源为外部时钟和设置各个外设时钟频率。我的开发板是外接8M晶振,通过PLL的倍率和分频,提高系统时钟至最高的72Mhz。具体配置如下图所示,红框表示修过过,实心表示点选择。
STM32CubeMX HAL库串口+DMA+IDLE空闲中断不定长度数据接收和发送_第4张图片

3. 设置Debug方式

在system Core→SYS设置Debug方式,这个设置一定要选择。其实就是设置芯片那几个调试GPIO口的功能,如果不设置,芯片那几个IO口就会设置成其他功能,当我们插上仿真器调试的时候可能会出问题。之前经历过,因为没有设置Debug方式,程序下载没问题,但单步调试仿真时候出问题。我使用的是Jlink工具,调试模式选择JTAG 5PIN.如图
STM32CubeMX HAL库串口+DMA+IDLE空闲中断不定长度数据接收和发送_第5张图片

4. UART2配置

4.1 UART2的GPIO配置

配置好每一个工程都需要配置的系统时钟源,我们便可以配置外设,外设则根据用户所需来配置,本项目使用的是UART2(PA2,PA3)。我们回到 Pinout & Configuration配置选项卡中,展开System Core,接着选择GPIO,这样就可以开始配置在Pinout view窗口中图形化配置IO口了。如下图所示:
STM32CubeMX HAL库串口+DMA+IDLE空闲中断不定长度数据接收和发送_第6张图片
STM32CubeMX HAL库串口+DMA+IDLE空闲中断不定长度数据接收和发送_第7张图片

4.2 设置UART2和DMA的参数配置

4.2.1 UART 基本参数的配置

设置UART2波特率,长度,停止位,奇偶校验等。如图
STM32CubeMX HAL库串口+DMA+IDLE空闲中断不定长度数据接收和发送_第8张图片

4.2.2 UART 中断配置

使能串口中断
STM32CubeMX HAL库串口+DMA+IDLE空闲中断不定长度数据接收和发送_第9张图片

4.2.3 DMA配置

因为我使用了UART2的DMA发送和接收数据,首先是将UART2_RX和UART2_TX映射到DMA对应的通道上,对应的配置就是在CubeMX配置就是添加相应的DMA通道。再设置DMA几个参数,这几个参数都是通过STM32CubeMX软件配置一个就可以,真的很方便。

  • DMA模式,DMA有循环模式和正常模式2种模式。个人理解:
    (1) DMA接收要用 循环模式,只需调用一次接收函数即可,重复调用也只有第一次调用有效 若是 正常模式,需要在每次接收完成后重复调用
    (2) DMA发送要用 正常模式,需在每次发送时重复调用 还要打开UART中断,否则即使重复调用DMA发送函数也只有第一次发送有效,其余不会执行(UART发送状态一直处于 BUSY) 若是 循环模式,则会连续不断地发送,不会停止。
  • 地址增加(地址增加这块不是特别理解,参考别人的设计,STM32CubeMX配置发送和接收数据,我都使能内存数据地址递增,外设地址不使能)
    (1)在传输数据时,可以配置指向传输双方数据的指针是否自动向后递增。
    (2)通过单个寄存器访问外设源或目标数据时,禁止递增模式十分有用。
    STM32CubeMX HAL库串口+DMA+IDLE空闲中断不定长度数据接收和发送_第10张图片
    STM32CubeMX HAL库串口+DMA+IDLE空闲中断不定长度数据接收和发送_第11张图片

4.2.4 串口GPIO配置

UATR_RX接收GPIO管脚一定要配置成Pull-up
STM32CubeMX HAL库串口+DMA+IDLE空闲中断不定长度数据接收和发送_第12张图片

5. NVIC中断优先级管理

STM32CubeMX HAL库串口+DMA+IDLE空闲中断不定长度数据接收和发送_第13张图片

五,程序

1. DMA和串口空闲中断初始化

首先,我们在初始化的时候,使能串口空闲中断,使能DMA并设置DMA接收Buff,同时使用DMA函数发送一帧数据,下面是需要添加的代码

main.c

... ...
uint8_t receive_buff[255];                //定义接收数组
... ...
__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE);         //使能串UART2 IDLE中断
HAL_UART_Receive_DMA(&huart2, (uint8_t*)receive_buff, 255);   //设置DMA传输,讲串口2的数据搬运到recvive_buff中,
memcpy(tx_buffer,"这是一个串口中断接收回显实验\n",100); //tx_buffer赋值
HAL_UART_Transmit_DMA(&huart2,tx_buffer,29);           //串口DMA发送一帧数据

while (1)
{
  ... ...
}
... ...

2. 中断处理函数和回调函数

File stm32f0xx_it.c

添加中断空闲函数,

void USART1_IRQHandler(void)
{
    /* USER CODE BEGIN USART1_IRQn 0 */
    /* USER CODE END USART1_IRQn 0 */
    
    HAL_UART_IRQHandler(&huart2);
    
    /* USER CODE BEGIN USART1_IRQn 1 */
    USER_UART_IRQHandler(&huart2);       //新添加的函数,用来处理串口空闲中断
    /* USER CODE END USART1_IRQn 1 */
}

usart.c

对该中断空闲函数进行定义。

void USER_UART_IRQHandler(UART_HandleTypeDef *huart)
{
    if(USART2 == huart2.Instance)                                   //判断是否是串口1
    {
        if(RESET != __HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE))   //判断是否是空闲中断
        {
            __HAL_UART_CLEAR_IDLEFLAG(&huart2);                     //清楚空闲中断标志(否则会一直不断进入中断)
            USAR_UART_IDLECallback(huart);                          //调用中断处理函数
        }
    }
}

至此,我们已经可以正常的响应串口中断,并调用了一个新的函数:USAR_UART_IDLECallback(),它是专门用来处理空闲中断的一个回调函数,其定义如下(写在 usart.c 文件即可):

void USAR_UART_IDLECallback(UART_HandleTypeDef *huart)
{
    HAL_UART_DMAStop(&huart2);                                                     //停止本次DMA传输
    
    uint8_t data_length  = BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx);   //计算接收到的数据长度
	HAL_UART_Transmit_DMA(&huart2,receive_buff,data_length);                     //DMA发送函数:将接收到的数据打印出去

    data_length = 0;
}

关于计算数据长度可以具体了解HAL库函数的操作,简单来说__HAL_DMA_GET_COUNTER()函数将返回接收缓存剩余数据单元的数目。自己设置接收缓存总的数据长度,减去还未接收的数据长度,就得到了已经接收到的数据

参考

原文链接:https://blog.csdn.net/qq_17351161/article/details/90415407

你可能感兴趣的:(STM32CubeMX HAL库串口+DMA+IDLE空闲中断不定长度数据接收和发送)