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

1 STM32CubeMX配置部分

1.1 初始化配置

打开STM32CubeMX,点击ACCESS TO MCU SELECTOR,在Commercial Part Number中输入MCU型号,例如我在这里输入了STM32L431RCT6。选中正确型号然后双击进入下一步点击配置界面。


1.2 SYS配置如图

注意开启Debug的IO口。


1.3 RCC配置如图

开启了外部晶振,若没有则都选择Diable。


1.4 USART1配置

NVIC设置:注意这里需要打开USART1 global interrupt全局中断。


DMA设置:打开串口接收DMA,模式选择Normal。(TX的DMA也要添加进来,此处图中没有标出)


2 软件执行流程

需要关注下面几个函数:

// 在阻塞模式下接收一定数量的数据,直到接收到预期数量的数据或发生空闲事件

HAL_UARTEx_ReceiveToIdle(UART_HandleTypeDef* huart, uint8_t* pData, uint16_t Size, uint16_t* RxLen, uint32_t Timeout);

// 在中断模式下接收一定数量的数据,直到接收到预期数量的数据或发生空闲事件

HAL_UARTEx_ReceiveToIdle_IT(UART_HandleTypeDef* huart, uint8_t* pData, uint16_t Size);

// 在DMA模式下接收一定数量的数据,直到接收到预期数量的数据或发生空闲事件

HAL_UARTEx_ReceiveToIdle_DMA(&huart2, U2_rx_buffer, sizeof(U2_rx_buffer)); //串口中断+dma

__HAL_DMA_DISABLE_IT(&hdma_usart2_rx,DMA_IT_HT); //关闭dma接收半满中断函数

// 使用空闲中断时的接收回调函数

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)

void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)

void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)

2.1 HAL_UARTEx_ReceiveToIdle_DMA()

HAL库中通过HAL_UARTEx_ReceiveToIdle_DMA()函数可以方便的实现串口空闲中断,下面来分析一下这个实现的过程。

首先来看这个函数本身,可以看到函数中大部分的内容是条件判断,函数中会把接收类型设置成HAL_UART_RECEPTION_TOIDLE,然后开启DMA接收,清除一次IDLEF标志位,重新开启IDLEF标志位。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
/**
* @brief Receive an amount of data in DMA mode till either the expected number
* of data is received or an IDLE event occurs.
* @note Reception is initiated by this function call. Further progress of reception is achieved thanks
* to DMA services, transferring automatically received data elements in user reception buffer and
* calling registered callbacks at half/end of reception. UART IDLE events are also used to consider
* reception phase as ended. In all cases, callback execution will indicate number of received data elements.
* @note When the UART parity is enabled (PCE = 1), the received data contain
* the parity bit (MSB position).
* @note When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01),
* the received data is handled as a set of uint16_t. In this case, Size must indicate the number
* of uint16_t available through pData.
* @param huart UART handle.
* @param pData Pointer to data buffer (uint8_t or uint16_t data elements).
* @param Size Amount of data elements (uint8_t or uint16_t) to be received.
* @retval HAL status
*/
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
HAL_StatusTypeDef status;

/* Check that a Rx process is not already ongoing */
if (huart->RxState == HAL_UART_STATE_READY)
{
if ((pData == NULL) || (Size == 0U))
{
return HAL_ERROR;
}

__HAL_LOCK(huart);

/* Set Reception type to reception till IDLE Event*/
huart->ReceptionType = HAL_UART_RECEPTION_TOIDLE;

status = UART_Start_Receive_DMA(huart, pData, Size);

/* Check Rx process has been successfully started */
if (status == HAL_OK)
{
if (huart->ReceptionType == HAL_UART_RECEPTION_TOIDLE)
{
__HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_IDLEF);
ATOMIC_SET_BIT(huart->Instance->CR1, USART_CR1_IDLEIE);
}
else
{
/* In case of errors already pending when reception is started,
Interrupts may have already been raised and lead to reception abortion.
(Overrun error for instance).
In such case Reception Type has been reset to HAL_UART_RECEPTION_STANDARD. */
status = HAL_ERROR;
}
}

return status;
}
else
{
return HAL_BUSY;
}
}

注意看status = UART_Start_Receive_DMA(huart, pData, Size),该函数的定义中给串口DMA传输完成一半时的回调函数赋了值。(该函数会使能传输完成、半传输、传输错误等中断)。

这样在执行下面的HAL_DMA_Start_IT函数时,就会开启DMA_IT_HTDMA_IT_TCDMA_IT_TE这个中断。

开启这个中断后会影响程序的流程吗?我们来看DMA_IT_HT中断的回调函数UART_DMARxHalfCplt中的内容。可以看到,发生串口DMA传输过半完成中断时,会调用一次 HAL_UARTEx_RxEventCallback回调函数。也就是说调用HAL_UARTEx_ReceiveToIdle_DMA就会默认开启DMA_IT_HT中断。这与我们预期不符,我们希望在传输完成,等待数据接收完整后再进入回调函数,处理完整的数据,接收完整的一轮数据后执行一次HAL_UARTEx_RxEventCallback回调函数,而因为默认开启了DMA_IT_HT中断后,一轮数据接收意外的执行了两次HAL_UARTEx_RxEventCallback回调函数。(使用HAL_UARTEx_ReceiveToIdle_DMA就会默认开启DMA(DMA_IT_TC | DMA_IT_HT | DMA_IT_TE),这样接收一次数据会进两次HAL_UARTEx_RxEventCallback回调函数)

开启DMA_IT_HT中断后,提前进入了回调函数。为了避免出错,我们需要调用__HAL_DMA_DISABLE_IT(),把这个中断手动关闭。(关闭dma接收半满中断函数,这样我们在接收一组数据时就不会触发半满中断,dma就可以接收一组数据)

搞明白其中的逻辑,我们接着回到HAL_UARTEx_ReceiveToIdle_DMA中剩下的部分。调用函数会将当前的串口接收类型设置为HAL_UART_RECEPTION_TOIDLE

1
2
/* Set Reception type to reception till IDLE Event*/
huart->ReceptionType = HAL_UART_RECEPTION_TOIDLE;

调用UART_Start_Receive_DMA(),启用DMA将接收到的数据放在指针 pData 指向的位置。

在接收正确的情况下,会执行以下的内容。

1
2
__HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_IDLEF);		// 清除UART_CLEAR_IDLEF标志位
ATOMIC_SET_BIT(huart->Instance->CR1, USART_CR1_IDLEIE); // 设置usart cr1寄存器的USART_CR1_IDLEIE标志位

在参考手册中关于CR1寄存器IDLEIE标志位的介绍。

IDLEIE: IDLE interrupt enable This bit is set and cleared by software. 开启IDLE中断。这个标志位由软件控制。
0: Interrupt is inhibited 中断被禁用。
1: A USART interrupt is generated whenever IDLE=1 in the USART_ISR register 串口空闲时产生串口中断

于是我们可以得知,调用HAL_UARTEx_ReceiveToIdle_DMA()函数后只要发生了串口空闲事件,就会产生串口中断。以及默认开启了DMA_IT_HT这个中断,在DMA的数据传输达到预期数据量的一半时会触发中断,执行回调函数。
DMA_IT_HT中断这是我们不需要的,不做过多的分析,我们只需手动关闭即可。我们接下来看串口中断处理函数中的情况。

因为我们在STM32CubeMX中勾选了USART1串口全局中断,这里已经自动生成了代码。开启IDLEF标志位后,如果串口中断来临就会执行中断处理函数 void USART1_IRQHandler(void)USART2_IRQHandler(void)再调用HAL_UARTEx_RxEventCallback回调函数。

1
2
3
4
5
6
7
8
9
10
11
// 由于dma采用的模式:Normal,DMA会在完成一次接收后自动关闭,再次需要重新手动打开
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */

/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE); //重新开启串口空闲中断和DMA接收
/* USER CODE END USART1_IRQn 1 */
}

2.2 HAL_UART_IRQHandler(&huart1)

接下来继续查看HAL_UART_IRQHandler(&huart1)中的内容,该函数中会判断各种中断类型,并执行对应的操作,主要关注其中关于IDLE中断的部分。首先判断串口的接收类型 ReceptionType,在 HAL_UARTEx_ReceiveToIdle_DMA() 中已经被设为 HAL_UART_RECEPTION_TOIDLE__HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_IDLEF) 等带有CLEAR的函数会清除相关中断的标志位。

查看与DMA串口空闲中断相关的部分,我们可以简单的理解为,在条件判断都满足的情况下,发生串口空闲中断以后,会开启DMA功能并调用回调函数HAL_UARTEx_RxEventCallback()。如果接收过程中发生错误,会调用HAL_UART_ErrorCallback();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* @brief Handle UART interrupt request.
* @param huart UART handle.
* @retval None
*/
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
{
......

/* Check current reception Mode :
If Reception till IDLE event has been selected : */
if ((huart->ReceptionType == HAL_UART_RECEPTION_TOIDLE)
&& ((isrflags & USART_ISR_IDLE) != 0U)
&& ((cr1its & USART_ISR_IDLE) != 0U))
{
__HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_IDLEF);

/* Check if DMA mode is enabled in UART */
if (HAL_IS_BIT_SET(huart->Instance->CR3, USART_CR3_DMAR))
{
/* DMA mode enabled */
/* Check received length : If all expected data are received, do nothing,
(DMA cplt callback will be called).
Otherwise, if at least one data has already been received, IDLE event is to be notified to user */
uint16_t nb_remaining_rx_data = (uint16_t) __HAL_DMA_GET_COUNTER(huart->hdmarx);
if ((nb_remaining_rx_data > 0U)
&& (nb_remaining_rx_data < huart->RxXferSize))
{
/* Reception is not complete */
huart->RxXferCount = nb_remaining_rx_data;

/* In Normal mode, end DMA xfer and HAL UART Rx process*/
if (HAL_IS_BIT_CLR(huart->hdmarx->Instance->CCR, DMA_CCR_CIRC))
{
/* Disable PE and ERR (Frame error, noise error, overrun error) interrupts */
ATOMIC_CLEAR_BIT(huart->Instance->CR1, USART_CR1_PEIE);
ATOMIC_CLEAR_BIT(huart->Instance->CR3, USART_CR3_EIE);

/* Disable the DMA transfer for the receiver request by resetting the DMAR bit
in the UART CR3 register */
ATOMIC_CLEAR_BIT(huart->Instance->CR3, USART_CR3_DMAR);

/* At end of Rx process, restore huart->RxState to Ready */
huart->RxState = HAL_UART_STATE_READY;
huart->ReceptionType = HAL_UART_RECEPTION_STANDARD;

ATOMIC_CLEAR_BIT(huart->Instance->CR1, USART_CR1_IDLEIE);

/* Last bytes received, so no need as the abort is immediate */
(void)HAL_DMA_Abort(huart->hdmarx);
}
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
/*Call registered Rx Event callback*/
huart->RxEventCallback(huart, (huart->RxXferSize - huart->RxXferCount));
#else
/*Call legacy weak Rx Event callback*/
HAL_UARTEx_RxEventCallback(huart, (huart->RxXferSize - huart->RxXferCount));
#endif /* (USE_HAL_UART_REGISTER_CALLBACKS) */
}
return;
}
}

......

}

2.3 HAL_UARTEx_RxEventCallback()

我们先来分析HAL_UARTEx_RxEventCallback()这个回调函数。这是一个__weak类型的函数,对于这种类型的函数我们不能在原位修改,需要在其他位置手动实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @brief Reception Event Callback (Rx event notification called after use of advanced reception service).
* @param huart UART handle
* @param Size Number of data available in application reception buffer (indicates a position in
* reception buffer until which, data are available)
* @retval None
*/
__weak void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(huart);
UNUSED(Size);

/* NOTE : This function should not be modified, when the callback is needed,
the HAL_UARTEx_RxEventCallback can be implemented in the user file.
*/
}

以下是我手动重新实现的HAL_UARTEx_RxEventCallback()函数,发生串口空闲中断后会被调用。STM32CubeMX中,DMA的Mode我们选择了Normal,DMA会在完成一次接收后自动关闭,再次调用需要重新手动打开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef * huart, uint16_t Size)
{
if(huart->Instance == USART1)
{
if (Size <= BUFF_SIZE)
{
uint16_t cnt = BUFF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
HAL_UART_Transmit(&huart1, rx_buff, cnt, 0xffff);
HAL_UART_Transmit(&huart1, rx_buff, Size, 0xffff); // 将接收到的数据再发出
memset(rx_buff, 0, BUFF_SIZE); // 清除接收缓存
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buff, BUFF_SIZE); // 接收完毕后重启
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT); // 手动关闭DMA_IT_HT中断
}
else // 接收数据长度大于BUFF_SIZE
{

}
}
}

第二种方法:

关闭dma接收中断,这样dma只需要搬运数据,当触发串口中断标志位时将会进入回调函数进行处理。


2.4 HAL_UART_ErrorCallback()

然后来看HAL_UART_ErrorCallback()这个回调函数,串口发生错误时会调用这个回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @brief UART error callback.
* @param huart UART handle.
* @retval None
*/
__weak void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(huart);

/* NOTE : This function should not be modified, when the callback is needed,
the HAL_UART_ErrorCallback can be implemented in the user file.
*/
}

同样这也是个__weak类型的函数,我们也来手动实现一下。
主要功能为:发生错误时调用HAL_UARTEx_ReceiveToIdle_DMA()来重新开启串口空闲中断和DMA。

1
2
3
4
5
6
7
8
9
10
void HAL_UART_ErrorCallback(UART_HandleTypeDef * huart)
{
if(huart->Instance == USART1)
{
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buff, BUFF_SIZE); // 接收发生错误后重启
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT); // 手动关闭DMA_IT_HT中断
memset(rx_buff, 0, BUFF_SIZE);

}
}

2.5 HAL_UARTEx_ReceiveToIdle_IT()

1
2
3
4
5
6
7
8
9
/**
* @brief 在中断模式下接收一定数量的数据,直到接收到预期数量的数据或发生空闲事件。
* @note 接收由此功能调用启动。由于RXNE和空闲事件引发的UART中断,接收的进一步进展得以实现。在接收结束时调用Callback,指示接收到的数据元素的数量。
* @param huart UART handle
* @param pData 指向数据缓冲区(uint8_t或uint16_t数据元素)的指针。
* @param Size 要接收的数据元素的大小(uint8_t或uint16_t)。
* @retval HAL status
**/
HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);

使用方法:

1.在主函数中调用HAL_UARTEx_ReceiveToIdle_IT()
2.然后在回调函数 HAL_UARTEx_RxEventCallback()中做相应处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int main(void)
{
/* USER CODE BEGIN 1 */
//__HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);
/* USER CODE END 1 */

/* MCU Configuration--------------------------------------------------------*/

/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();

/* USER CODE BEGIN Init */

/* USER CODE END Init */

/* Configure the system clock */
SystemClock_Config();

/* USER CODE BEGIN SysInit */

/* USER CODE END SysInit */

/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
//空闲中断接收
HAL_UARTEx_ReceiveToIdle_IT(&huart1,Uart_ReadCache,410);
/* USER CODE END 2 */

/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}

1
2
3
4
5
6
7
8
9
10
11
//空闲中断回调函数,参数Size为串口实际接收到数据字节数
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if(huart->Instance==USART1)
{
// 把收到的一包数据通过串口回传
HAL_UART_Transmit(&huart1,Uart_ReadCache,Size,0xff);
// 再次开启空闲中断接收,不然只会接收一次数据
HAL_UARTEx_ReceiveToIdle_IT(&huart1,Uart_ReadCache,410);
}
}

2.6 关于dma

1个字节由8个比特组成,对于115200波特率,每个比特的持续时间为1/115200秒,每个字节的传输时间为8 * 8.68微秒 = 约69.4微秒。115200bps波特率,1s传输11520字节,大约69us需响应一次中断。串口一般来说是低速通信,波特率通常小于等于115200bps。因此对于数据量不大的通信场景,一般没必要使用DMA,完全不需要。

对于数量大,或者波特率提高时,频繁的进入中断可能会导致出现问题,所以采用dma进行搬运。但是dma使用时需要注意一些细节:

dma收一半还是接收固定字节?

使用dma+串口空闲中断时,如果使用HAL_UARTEx_ReceiveToIdle_DMA()实现串口不定长数据接收,需要手动关闭dma接收半满中断或者关闭dma接收中断。


3 调用实例

3.1 调用步骤

  • 在while()循环前,调用一次 HAL_UARTEx_ReceiveToIdle_DMA()
  • 重新实现HAL_UARTEx_RxEventCallback()
  • 在“usart.h”中声明对应的DMA_HandleTypeDef结构体以便在外部调用,用于关闭DMA_IT_HT中断
  • 重新实现HAL_UART_ErrorCallback()

usart.h中新增内容如下:

1
2
3
/* USER CODE BEGIN Private defines */
extern DMA_HandleTypeDef hdma_usart2_rx;
/* USER CODE END Private defines */

完整的main.c文件内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/* Includes ------------------------------------------------------------------*/
#include "main.h"
#include "dma.h"
#include "usart.h"
#include "gpio.h"
#include <string.h>

#define BUFF_SIZE 100

uint8_t rx_buff[BUFF_SIZE];

void SystemClock_Config(void);

int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
/* 需要在初始化时调用一次否则无法接收到内容 */
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buff, BUFF_SIZE);
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT); // 手动关闭DMA_IT_HT中断
while (1)
{

}
}

void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

if (HAL_PWREx_ControlVoltageScaling(PWR_REGULATOR_VOLTAGE_SCALE1) != HAL_OK)
{
Error_Handler();
}
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSE;
RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;

if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK)
{
Error_Handler();
}
}

void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef * huart, uint16_t Size)
{
if(huart->Instance == USART1)
{
if (Size <= BUFF_SIZE)
{
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buff, BUFF_SIZE); // 接收完毕后重启
HAL_UART_Transmit(&huart1, rx_buff, Size, 0xffff); // 将接收到的数据再发出
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT); // 手动关闭DMA_IT_HT中断
memset(rx_buff, 0, BUFF_SIZE); // 清除接收缓存
}
else // 接收数据长度大于BUFF_SIZE,错误处理
{

}
}
}

void HAL_UART_ErrorCallback(UART_HandleTypeDef * huart)
{
if(huart->Instance == USART1)
{
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buff, BUFF_SIZE); // 接收发生错误后重启
__HAL_DMA_DISABLE_IT(&hdma_usart1_rx, DMA_IT_HT); // 手动关闭DMA_IT_HT中断
memset(rx_buff, 0, BUFF_SIZE); // 清除接收缓存

}
}

void Error_Handler(void)
{
__disable_irq();
while (1)
{
}

}

3.2 测试结果


3.3 总结

利用HAL_UARTEx_ReceiveToIdle_DMA()可以方便的实现串口不定长数据接收,使用时有2个地方需要注意:

  • 需要手动关闭 DMA_IT_HT中断
  • 记得手动实现HAL_UART_ErrorCallback()

4 踩坑

4.1 问题描述

大部分文章中,习惯把 HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE); //重新开启串口空闲中断和DMA 接收放在重新实现的函数 void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) 中,而不是放在void USART1_IRQHandler(void)中的方式。

1
2
3
4
5
6
7
8
9
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buffer, BUF_SIZE); // 选择放置在此处
/* USER CODE END USART1_IRQn 1 */
}

其他大部分文章如下所示放置。

1
2
3
4
5
6
7
8
9
10
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART1)
{
cnt = BUF_SIZE - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
HAL_UART_Transmit(&huart1, rx_buffer, cnt, 0xffff);
memset(rx_buffer, 0, cnt);
HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE); //其他大部分文章放置于此处
}
}

放置在回调函数HAL_UARTEx_RxEventCallback中似乎也说的通,产生串口空闲中断时,会调用回调函数,并在其中重新开启串口空闲中断和DMA,等待下一次串口空闲中断来临。但是这样做会不会有bug?
实际测试一下:
首先,串口初始化时波特率配置成 9600bps。


4.2 测试流程

测试流程:第一次发送,波特率正确9600bps,发送消息后收到正确的回复。

第二次发送: 波特率错误 115200bps 发送消息后收不到回复。

第三次发送 波特率改回正确的9600bps 发送后依然收不到回复。

以上就是模拟波特率不小心设置错误的情况,居然产生了严重的问题,即使改回正确的波特率依然无法收到回复。
说明这种做法存在一定的风险。

经过大量的排查,最后确定问题就出在函数 HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE)放置的位置

波特率正确的正常情况下,USART的CR1寄存器中,IDLEIE位打开,ISR寄存器中IDLE位关闭。

同时对应DMA通道寄存器的情况如下:

目前收发正常。

也能正常进入回调函数。

接着改成错误的波特率,收不到回复。

并且仿真未在回调函数中的断点处停下,说明不再进入回调函数。

查看DMA寄存器,发现DMA没有正确打开(与之前的截图对比)。

查看USART寄存器发现也未正确开启。


4.3 原因

串口波特率错误时,不再进入回调函数。一轮接收结束时,串口空闲中断与DMA均被关闭,而两者的重启在回调函数中通过
HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE);开启。这就导致波特率错误时,无法接收后续的新数据。


4.4 总结

HAL_UARTEx_ReceiveToIdle_DMA(&huart1,rx_buffer,BUF_SIZE);必须放在void USART1_IRQHandler(void)中。
这样即使接收错误,也能重新开启串口空闲中断和DMA,不影响下次接收。


4.5 最新解释

解决波特率设置错误的方法有问题,当波特率不匹配的时候,UART会报错并产生中断(应该是帧错误或者溢出错误ORT/FE),在中断处理函数HAL_UART_IRQHandler中会关闭DMA,所以改成正确的波特率也无法恢复。
因为串口异常后关闭DMA,之后你调用HAL_UARTEx_ReceiveToIdle_DMA来重置接收确实解决了问题,但是每次UART中断都重置接收肯定是不对的,因为有很多UART事件都会触发中断,比如TXE/CTS/TC/RXNE等,这种情况也重置接收就可能导致接收异常。
正确的处理方式是在HAL_UART_ErrorCallback中重置接收。
还有一个问题就是使用HAL_UARTEx_ReceiveToIdle_DMA来接收,会开启三个中断,DMA半满/DMA溢满/UART空闲,这三个中断都会调用HAL_UARTEx_RxEventCallback,你的这个callback根本没有去判断中断源,真的没问题吗?