STM32-HAL库串口DMA空闲中断的正确使用方式+解析SBUS信号
- 一. 问题描述
- 二. 方法一——使用HAL_UART_Receive_DMA
- 三. 方法二——使用HAL_UARTEx_ReceiveToIdle_DMA
- 四. 方法三——使用HAL_UARTEx_ReceiveToIdle_IT(不使用DMA)
- 五. 总结
一. 问题描述
能够点进这篇文章的小伙伴肯定是对STM32串口DMA空闲中断接收数据感兴趣的啦,今天用这一功能实现串口解析航模遥控器sbus信号时,查阅了很多网友发布的文章(勤劳的搬运工~),包括自己之前写过一篇博客 STM32_HAL库_CubeMx串口DMA通信(DMA发送+DMA空闲接收不定长数据)。本文进一步梳理一下HAL库串口空闲中断三种不同的使用方式,其中前两种使用DMA方式,最后一种使用HAL库自带的空闲中断机制。
本文环境:
- Keil MDK5.14
- STM32CubeMX6.2.1
- 开发板/芯片:自制板/STM32F407ZGT6
实现功能:
- 串口DMA/非DMA空闲中断接收不定长数据/解析航模遥控器SBUS信号
二. 方法一——使用HAL_UART_Receive_DMA
最常见的方法就是使用HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
这个库函数,其使用方法类似于HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
,初始化时需要调用一次,然后每次在中断服务函数里面处理完数据后重新调用一次。
使用HAL_UART_Receive_IT
只需要打开串口接收中断,即HAL_NVIC_EnableIRQ(UART5_IRQn)
;
使用HAL_UART_Receive_DMA
空闲中断需要在初始化时打开串口空闲中断使能,调用方式为:__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE)
。
此时DMA中断可开可不开,开了也不用管,因为数据处理是在串口空闲中断中进行的。
1. 串口初始化代码:
void MX_USART2_UART_Init(void)
{
huart2.Instance = USART2;
huart2.Init.BaudRate = 100000;
huart2.Init.WordLength = UART_WORDLENGTH_9B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_EVEN;
huart2.Init.Mode = UART_MODE_TX_RX;
huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart2.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart2) != HAL_OK)
{
Error_Handler();
}
__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE); // 开启串口空闲中断,必须调用
HAL_UART_Receive_DMA(&huart2,USART2_RX_BUF,USART_REC_LEN); // 启动DMA接收
}
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
if(uartHandle->Instance==USART2)
{
__HAL_RCC_USART2_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_2|GPIO_PIN_3;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART2;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
hdma_usart2_rx.Instance = DMA1_Stream5;
hdma_usart2_rx.Init.Channel = DMA_CHANNEL_4;
hdma_usart2_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart2_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart2_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart2_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart2_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart2_rx.Init.Mode = DMA_NORMAL;
hdma_usart2_rx.Init.Priority = DMA_PRIORITY_MEDIUM;
hdma_usart2_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
if (HAL_DMA_Init(&hdma_usart2_rx) != HAL_OK)
{
Error_Handler();
}
__HAL_LINKDMA(uartHandle,hdmarx,hdma_usart2_rx);
/* USART2 interrupt Init */
HAL_NVIC_SetPriority(USART2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART2_IRQn);
}
}
初始化中调用__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE)
开启空闲中断,HAL_UART_Receive_DMA(&huart2,USART2_RX_BUF,USART_REC_LEN)
启动DMA串口接收,USART2_RX_BUF
是接收缓冲区,USART_REC_LEN
是定义的DMA接收缓冲区长度,接收的数据不能超过这个长度。
2. 中断处理:
中断处理有两种方式,第一种是直接定义在void USART1_IRQHandler(void)
中,第二种是自定义一个函数,然后在void USART1_IRQHandler(void)
中调用。注意网上很多资源自定义了中断回调函数但代码又没贴全,读者可能以为是HAL库自带的回调函数,结果因为没有在void USART1_IRQHandler(void)
中调用导致失败。
第一种形式——直接定义:
void USART2_IRQHandler(void)
{
uint32_t tmp_flag ;
u8 len;
u8 data[25];
tmp_flag = __HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE);
if( tmp_flag != RESET)
{
__HAL_UART_CLEAR_IDLEFLAG(&huart2);//清除标志位
HAL_UART_DMAStop(&huart2); //停止DMA接收,防止数据出错
len = USART_REC_LEN - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx);// 获取DMA中传输的数据个数
// 以下为用户数据处理,将数据拷贝出去
if(len == 25)
{
memcpy(data,USART2_RX_BUF,len);
update_sbus(data);
}
HAL_UART_Receive_DMA(&huart2,USART2_RX_BUF,USART_REC_LEN); //打开DMA接收,数据存入rx_buffer数组中。
}
HAL_UART_IRQHandler(&huart2); //调用HAL库中断处理公用函数
}
第二种形式——自定义回调函数:
void HAL_UART_IdleCpltCallback(UART_HandleTypeDef *huart)
{
uint32_t tmp_flag ;
u8 len;
u8 data[25];
if (huart->Instance == USART2)
{
tmp_flag = __HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE);
if( tmp_flag != RESET)
{
__HAL_UART_CLEAR_IDLEFLAG(huart);//清除标志位
HAL_UART_DMAStop(huart); //停止DMA接收
len = USART_REC_LEN - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx);// 获取DMA中传输的数据个数
// 以下为用户数据处理,将数据拷贝出去
if(len == 25)
{
memcpy(data,USART2_RX_BUF,len);
update_sbus(data);
}
HAL_UART_Receive_DMA(huart,USART2_RX_BUF,USART_REC_LEN); //打开DMA接收,数据存入rx_buffer数组中。
}
}
}
void USART2_IRQHandler(void)
{
HAL_UART_IdleCpltCallback(&huart2)
HAL_UART_IRQHandler(&huart2); //调用HAL库中断处理公用函数
}
再次提醒一下,HAL库中并没有类似void HAL_UART_IdleCpltCallback(UART_HandleTypeDef *huart)
这样的回调函数,大家不要看到了以为是自带的回调函数而不在void USART2_IRQHandler(void)中调用而导致失败。
中断处理中的函数说明:
__HAL_UART_GET_FLAG(huart, UART_FLAG_IDLE)
:返回中断标志位,产生空闲中断后会返回1;
__HAL_UART_CLEAR_IDLEFLAG(&huart2)
:清除空闲中断标志位,必须调用;
HAL_UART_DMAStop(&huart2)
:停止DMA接收,防止数据处理出错,数据处理完成后重新打开;
__HAL_DMA_GET_COUNTER(&hdma_usart2_rx)
:返回DMA中未传输的数据量,用缓冲区总长减去未传输数量就是已接收的数据长度;
HAL_UART_Receive_DMA(huart,USART2_RX_BUF,USART_REC_LEN)
:重新打开DMA接收。
以上就能成功实现不定长数据的接受了,其实只要步骤对了还是很简单的。
三. 方法二——使用HAL_UARTEx_ReceiveToIdle_DMA
第二种方式是使用HAL_StatusTypeDef HAL_UARTEx_ReceiveToIdle_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
这个库函数,并重定义void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
这个中断回调函数。
Attention:__weak void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
是正儿八经的库函数(弱函数),不是自定义的,我们可以重定义它。
使用这个库函数,串口初始化代码中就不需要调用__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE)
了,因为HAL_UARTEx_ReceiveToIdle_DMA
函数内部已经打开了空闲中断,当然你加上也没事。
1. 串口初始化代码:
void MX_USART2_UART_Init(void)
{
huart2.Instance = USART2;
huart2.Init.BaudRate = 100000;
huart2.Init.WordLength = UART_WORDLENGTH_9B;
huart2.Init.StopBits = UART_STOPBITS_1;
huart2.Init.Parity = UART_PARITY_NONE;
huart2.Init.Mode = UART_MODE_TX_RX;
huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart2.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart2) != HAL_OK)
{
Error_Handler();
}
//__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE);// 开启串口空闲中断,可省略
HAL_UARTEx_ReceiveToIdle_DMA(&huart2,USART2_RX_BUF,USART_REC_LEN); // 使用串口DMA空闲中断,会自动开启空空闲中断
}
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
if(uartHandle->Instance==USART2)
{
__HAL_RCC_USART2_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_2|GPIO_PIN_3;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF7_USART2;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
hdma_usart2_rx.Instance = DMA1_Stream5;
hdma_usart2_rx.Init.Channel = DMA_CHANNEL_4;
hdma_usart2_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart2_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart2_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart2_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart2_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart2_rx.Init.Mode = DMA_NORMAL;
hdma_usart2_rx.Init.Priority = DMA_PRIORITY_MEDIUM;
hdma_usart2_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
if (HAL_DMA_Init(&hdma_usart2_rx) != HAL_OK)
{
Error_Handler();
}
__HAL_LINKDMA(uartHandle,hdmarx,hdma_usart2_rx);
/* USART2 interrupt Init */
HAL_NVIC_SetPriority(USART2_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(USART2_IRQn);
}
}
这里__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE)
是可省略的,HAL_UARTEx_ReceiveToIdle_DMA(&huart2,USART2_RX_BUF,USART_REC_LEN)
启动DMA串口空闲中断接收,USART2_RX_BUF
是接收缓冲区,USART_REC_LEN
是定义的DMA接收缓冲区长度,接收的数据不能超过这个长度。
2. 中断处理:
中断处理定义在void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
这个中断回调函数中。
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
u8 len;
u8 data[25];
if(huart->Instance == USART2)
{
HAL_UART_DMAStop(huart);
len = USART_REC_LEN - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx);// 获取DMA中传输的数据个数
if (USART2_RX_BUF[0] == 0x0F && len == 25) //接受完一帧数据
{
memcpy(data,USART2_RX_BUF,len);
update_sbus(data);
}
HAL_UARTEx_ReceiveToIdle_DMA(&huart2,USART2_RX_BUF,USART_REC_LEN); // 再次开启DMA空闲中断
}
}
void USART2_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart2); //调用HAL库中断处理公用函数
}
当然也可以像方法一种一样,中断逻辑处理直接定义在void USART2_IRQHandler(void)
中。
四. 方法三——使用HAL_UARTEx_ReceiveToIdle_IT(不使用DMA)
这种方法参考这篇文章: STM32 hal库串口空闲中断最新用法
使用方法:
1、在主函数中调用HAL_UARTEx_ReceiveToIdle_IT()
2、在回调函数HAL_UARTEx_RxEventCallback()
中做中断逻辑处理。
这里我换用串口4,实现上位机发送,单片机原封不动返回数据。
1. 串口初始化代码:
void MX_UART4_Init(void)
{
huart4.Instance = UART4;
huart4.Init.BaudRate = 115200;
huart4.Init.WordLength = UART_WORDLENGTH_8B;
huart4.Init.StopBits = UART_STOPBITS_1;
huart4.Init.Parity = UART_PARITY_NONE;
huart4.Init.Mode = UART_MODE_TX_RX;
huart4.Init.HwFlowCtl = UART_HWCONTROL_NONE;
huart4.Init.OverSampling = UART_OVERSAMPLING_16;
if (HAL_UART_Init(&huart4) != HAL_OK)
{
Error_Handler();
}
// HAL_UARTEx_ReceiveToIdle_IT同时开启空闲中断,开启接收
HAL_UARTEx_ReceiveToIdle_IT(&huart4,USART4_RX_BUF,USART_REC_LEN);
}
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
if(uartHandle->Instance==UART4)
{
__HAL_RCC_UART4_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
GPIO_InitStruct.Pin = GPIO_PIN_10|GPIO_PIN_11;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_PULLUP;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF8_UART4;
HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);
HAL_NVIC_SetPriority(UART4_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(UART4_IRQn);
}
}
可以看到这里是没有使用DMA的,初始化中调用了HAL_UARTEx_ReceiveToIdle_IT(&huart4,USART4_RX_BUF,USART_REC_LEN)
来开启串口4的空闲中断,USART4_RX_BUF是接收缓冲区,USART_REC_LEN是缓冲区的长度。这里可以看下库中HAL_UARTEx_ReceiveToIdle_IT
的定义和提示:
框1的地方说,这个函数用于在中断模式下接收一定量数据,直到指定的数据长度被接收到或者空闲中断产生。也就是说,如果USART_REC_LEN定义很小,还没到产生空闲中断,接收就会完成。
框2的地方,即是打开串口空闲中断。所以不需要额外再去开启空闲中断了。
2. 中断处理:
中断处理定义在void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
这个中断回调函数中。注意到,和使用DMA传输时是同一个回调函数。
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
u8 len=0;
u8 data[25];
u8 uart4_len=0;
u8 uart4_data[50];
if(huart->Instance == USART2)
{
HAL_UART_DMAStop(huart);
len = USART_REC_LEN - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx);// 获取DMA中传输的数据个数
if (USART2_RX_BUF[0] == 0x0F && len == 25) //接受完一帧数据
{
memcpy(data,USART2_RX_BUF,len);
update_sbus(data);
}
HAL_UARTEx_ReceiveToIdle_DMA(&huart2,USART2_RX_BUF,USART_REC_LEN); // 再次开启DMA空闲中断
}
if(huart->Instance == UART4)
{
while (USART4_RX_BUF[uart4_len] != '\0') uart4_len++;
memcpy(uart4_data,USART4_RX_BUF,uart4_len);
HAL_UART_Transmit(&huart4,uart4_data,uart4_len,0xff);
// 调用printf是使用串口1
printf("data: %s\r\n", uart4_data);
printf("data length: %d\r\n", uart4_len);
memset(USART4_RX_BUF, 0, USART_REC_LEN);
memset(uart4_data, 0, sizeof(uart4_data));
HAL_UARTEx_ReceiveToIdle_IT(&huart4,USART4_RX_BUF,USART_REC_LEN);
}
}
void USART2_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart2); //调用HAL库中断处理公用函数
}
void UART4_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart4); //调用HAL库中断处理公用函数
}
回调函数中就是读取USART4_RX_BUF
缓冲区的数据和数据长度,然后做相应处理,这里只是简单的原数据发送回去,如果是SBSU信号或Modbus或其他带协议帧的数据,则可根据帧头帧尾、数据长度、校验位等做进一步处理。
最终实现的效果就是上位机发送什么,单片机返回什么,如图:
五. 总结
以上三种方式都能实现串口空闲中断接收不定长数据,适合用于处理不定长数据接收、减少CPU负担,相比较而言,方法三不使用DMA,使用上更简洁。方法一、二使用DMA,好处是可以减少CPU负担。