STM32 HAL库中串口空闲中断+DMA 实现串口数据的不定长接收

本文开发环境:

  • MCU型号:STM32F051R8T6
  • IDE环境: MDK 5.25
  • 代码生成工具:STM32CubeMx 5.2.0
  • HAL库版本:v1.10.0(STM32Cube MCU Package for STM32F0 Series)

本文内容:

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

重要提示:

由于本文运行的硬件环境预留了BootLoader,固工程的地址做了对应的偏移,若需要使用附件工程,请读者自行修改,否则直接下载会无法运行:
在这里插入图片描述
此处请根据具体工程改正,若不清楚值,请直接参考其他可以正常运行的工程。

勘误

自定义的 void USER_UART_IRQHandler(UART_HandleTypeDef *huart)函数是所有串口中断都会调用的函数,其函数内判断具体某个串口发生中断时,原判断语句if(USART1 == huart1.Instance)有误, 此处应写 if(huart->Instance == USART1),感谢评论区m0_37204608 指正。



一、不定长数据接收的原理及其解决的问题

在 STM32 中,UART是最为常见的通信方式——它每次接收一个字节。我们可以使用轮询的方式,但是对于某些数据不固定时间发送的数据,轮询的方式有时候不够灵活。也可以使用中断的方式,如每一个字节都中断一次,当时比较消耗系统资源。特别是HAL库中,从中断到回调函数运行了不少的程序,频繁的中断很可能造成数据溢出。在本文使用F051单片机中,这种情更加明显。为了避免这个问题,我们使用指定接收一定长度的数据,再调用回调函数,这会让我们可以接收大数据,但是这种情况则造成了,要求每次的包是固定长度。为了解决以上一些问题,网上最常用的办法是使用空闲中断,即在串口空闲的时候,触发一次中断,通知内核,本次运输完成了。串口空闲中断的判定是:当串口开始接收数据后,检测到1字节数据的时间内没有数据发送,则认为串口空闲了。由于我们的内核在串口接收数据到空闲这段时间,是不受理串口数据的,所以我们还需要使用DMA来协助我们把数据传送到指定的地方,当数据传输完成后,通知内核去处理。

二、STM32CubeMx 中 UART 和 DMA 的配置

当然,这里省略了每一个工程都需要配置的 系统时钟,晶振部分,如果你还不熟悉这些通用的配置或通过Cubemx建立一个工程,请参考:STM32CubeMx 初始化和读/写 IO口操作

1. UART 的 配置

1.1 UART 基本参数的配置

首先需要配置串口的基本参数,波特率可根据实际情况设置,一般设置为115200bps,9600bps 等:在这里插入图片描述

1.2 使能串口 NVIC 中断并生成代码

接着需要使能中断,让串口内核可以相应串口的中断:
在这里插入图片描述
注意,这里还要配置NVIC生成代码,否则中断无法正常响应:
在这里插入图片描述

1.3 DMA 的配置

接着配置DMA,让DMA和串口接收联系起来,实现DMA串口数据的运输,记得切换回USART1选项。
在这里插入图片描述

1.4 配置串口 IO 口模式

某些电路可能已经配置有了外部上拉,本文在默认的模式下也可以通信,但为了保证更稳定的电平,这里配置为上拉输入:
在这里插入图片描述
至此,我们已经把串口,以及和串口相关的NVIC和DMA配置完毕,接下来就可以开始程序的实现了。

三、 编程步骤

  1. 开启串口空闲中断:在程序初始化时候,使能串口中断
  2. 定义串口空闲中断处理函数:在串口中断中添加串口空间中断处理函数
  3. 定义串口空闲中断回调函数:用以标记数据接收完成,计算接收到数据的长度

四、程序示例

首先,我们在初始化的时候,使能串口空闲中断,让串口在中断的时候,MCU可以调用串口中断函数:

1. 开启串口空闲中断

File main.c :

... ...
void main(void)
{
	.. ...
	__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
	while(1)
	{
	.. ..
	}
}
... ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

当添加这个函数到工程以后,每发送一次数据,都会调用一次USART1_IRQHandler()函数,你可以在该函数中插入打印语句,来验证是否空闲中断正常。

2. 配置 DMA 接收

虽然我们使用的CubeMx来配置DMA,但只是配置DMA模式为串口到内存,所以还需要在程序中进一步指定:DMA具体搬运到内存的哪一个位置中,我们建立一个数组用以存放DMA搬运的串口数据,并使用HAL_UART_Receive_DMA()函数来配置,具体代码如下所示:
File main.c :

... ...
uint8_t receive_buff[255];                //定义接收数组
... ...
void main(void)
{
	.. ...
	__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
	HAL_UART_Receive_DMA(&huart1, (uint8_t*)receive_buff, 255);     //设置DMA传输,讲串口1的数据搬运到recvive_buff中,
	                                                                //每次255个字节
	while(1)
	{
	.. ..
	}
}
... ...
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

注意:由于main.c 和 usart.c 中都需要用到数组的大小,所以在uart.h 中进行定义

File usart.h :

... ...
#define  BUFFER_SIZE  (255)
... ...
  • 1
  • 2
  • 3

3. 添加中断处理函数和回调函数

CubeMx 按上述操作后,生成的工程中,已经有了串口中断的处理函数 HAL_UART_IRQHandler(),但是其里面未发现该函数中对空闲中断的处理,所以我们额外添加一个函数: USER_UART_IRQHandler(),添加后完整代码如下:

File stm32f0xx_it.c :

void USART1_IRQHandler(void)
{
    /* USER CODE BEGIN USART1_IRQn 0 */
    /* USER CODE END USART1_IRQn 0 */
<span class="token function">HAL_UART_IRQHandler</span><span class="token punctuation">(</span><span class="token operator">&amp;</span>huart1<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">/* USER CODE BEGIN USART1_IRQn 1 */</span>
<span class="token function">USER_UART_IRQHandler</span><span class="token punctuation">(</span><span class="token operator">&amp;</span>huart1<span class="token punctuation">)</span><span class="token punctuation">;</span>                                <span class="token comment">//新添加的函数,用来处理串口空闲中断</span>
<span class="token comment">/* USER CODE END USART1_IRQn 1 */</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

当然,还需要对该函数进行定义。
File usart.c:

void USER_UART_IRQHandler(UART_HandleTypeDef *huart)
{
    if(USART1 == huart1.Instance)                                   //判断是否是串口1(!此处应写(huart->Instance == USART1)
    {
        if(RESET != __HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))   //判断是否是空闲中断
        {
            __HAL_UART_CLEAR_IDLEFLAG(&huart1);                     //清楚空闲中断标志(否则会一直不断进入中断)
            printf("\r\nUART1 Idle IQR Detected\r\n");
            USAR_UART_IDLECallback(huart);                          //调用中断处理函数
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

extern uint8_t receive_buff[255];                                                  //声明外部变量 
void USAR_UART_IDLECallback(UART_HandleTypeDef *huart)
{
    HAL_UART_DMAStop(&huart1);                                                     //停止本次DMA传输
uint8_t data_length  <span class="token operator">=</span> BUFFER_SIZE <span class="token operator">-</span> <span class="token function">__HAL_DMA_GET_COUNTER</span><span class="token punctuation">(</span><span class="token operator">&amp;</span>hdma_usart1_rx<span class="token punctuation">)</span><span class="token punctuation">;</span>   <span class="token comment">//计算接收到的数据长度</span>

<span class="token function">printf</span><span class="token punctuation">(</span><span class="token string">"Receive Data(length = %d): "</span><span class="token punctuation">,</span>data_length<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token function">HAL_UART_Transmit</span><span class="token punctuation">(</span><span class="token operator">&amp;</span>huart1<span class="token punctuation">,</span>receive_buff<span class="token punctuation">,</span>data_length<span class="token punctuation">,</span><span class="token number">0x200</span><span class="token punctuation">)</span><span class="token punctuation">;</span>                     <span class="token comment">//测试函数:将接收到的数据打印出去</span>
<span class="token function">printf</span><span class="token punctuation">(</span><span class="token string">"\r\n"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token function">memset</span><span class="token punctuation">(</span>receive_buff<span class="token punctuation">,</span><span class="token number">0</span><span class="token punctuation">,</span>data_length<span class="token punctuation">)</span><span class="token punctuation">;</span>                                            <span class="token comment">//清零接收缓冲区</span>
data_length <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
<span class="token function">HAL_UART_Receive_DMA</span><span class="token punctuation">(</span><span class="token operator">&amp;</span>huart1<span class="token punctuation">,</span> <span class="token punctuation">(</span>uint8_t<span class="token operator">*</span><span class="token punctuation">)</span>receive_buff<span class="token punctuation">,</span> <span class="token number">255</span><span class="token punctuation">)</span><span class="token punctuation">;</span>                    <span class="token comment">//重启开始DMA传输 每次255字节数据</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

关于计算数据长度可以具体了解HAL库函数的操作,简单来说__HAL_DMA_GET_COUNTER()函数将返回待接收的数据。将设置需要接收的数据长度,减去待接收的数据,就得到了已经接收到的数据。

至此,我们使用了DMA+串口空闲中断的方式来实现不定长数据的接收。

附件

如果你正使用同系列单片机,那么直接下载工程编的Hex文件后,通过串口调试助手向串口1发送不定长数据,将可以观察到数据返回,并打印出接收到数据的长度。
链接:DMA_USAR MDK 工程
提取码:h81c

posted @ 2023-07-21 11:41  SymPny  阅读(2277)  评论(0编辑  收藏  举报