今天继续研究STM32系列单片机的串口知识,之前研究过使用DMA与空闲中断接收不定长的知识。
之前帖子链接如下(已对串口的知识做了一些简单的介绍,这里就不做过多的介绍,感兴趣的话可以去了解下):
https://forum.eepw.com.cn/thread/388443/1
本片文章主要是对串口的使用DMA发送做一些开发过程中的讲解和避坑、经验分享!
一:在STM32的串口通讯模式中,我个人的理解是,阻塞方式,非阻塞方式和DMA发送,共计三种不同的的通讯方式,在HAL库中相关的函数代码如下:
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout); HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout); HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size); HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size); #if defined(HAL_DMA_MODULE_ENABLED) HAL_ StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size); HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
我在工作实际使用中,在代码量不大,要求不高的情况下,一般使用阻塞式的发送和中断方式接收,即HAL_UART_Transmit 和HAL_UART_Receive_IT的方式,实现CPU与外设的交互。
这种通讯方式比较简单,串口每接收到一个字节单片机就会进入一次串口的中断,我们在接收的过程中,只需要判断一下接收到数据的有效性,然后放入决定是否放到接收数据组。这种接收方式是最简单的,但是换来却是单片机需要频繁的进入中断,这样代码大的情况下,根据中断优先级的不同,可能会导致项目中某些程序被打断的情况出现。
使用DMA的接收方式可以实现内存与外设之间的数据直接传输,可以为CPU分担一些工作量,从而为CPU减轻负担,增加数据的传输处理速度。
这里重点的和大家分享一下串口DMA发送函数的使用技巧:
HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size);
/** * @brief Send an amount of data in DMA mode. * @note When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01), * the sent data is handled as a set of u16. In this case, Size must indicate the number * of u16 provided through pData. * @note When UART parity is not enabled (PCE = 0), and Word Length is configured to 9 bits (M1-M0 = 01), * address of user data buffer containing data to be sent, should be aligned on a half word frontier (16 bits) * (as sent data will be handled by DMA from halfword frontier). Depending on compilation chain, * use of specific alignment compilation directives or pragmas might be required * to ensure proper alignment for pData. * @param huart UART handle. * @param pData Pointer to data buffer (u8 or u16 data elements). * @param Size Amount of data elements (u8 or u16) to be sent. * @retval HAL status */ HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size) { /* Check that a Tx process is not already ongoing */ if (huart->gState == HAL_UART_STATE_READY) { if ((pData == NULL) || (Size == 0U)) { return HAL_ERROR; } /* In case of 9bits/No Parity transfer, pData buffer provided as input parameter should be aligned on a u16 frontier, as data copy into TDR will be handled by DMA from a u16 frontier. */ if ((huart->Init.WordLength == UART_WORDLENGTH_9B) && (huart->Init.Parity == UART_PARITY_NONE)) { if ((((uint32_t)pData) & 1U) != 0U) { return HAL_ERROR; } } huart->pTxBuffPtr = pData; huart->TxXferSize = Size; huart->TxXferCount = Size; huart->ErrorCode = HAL_UART_ERROR_NONE; huart->gState = HAL_UART_STATE_BUSY_TX; if (huart->hdmatx != NULL) { /* Set the UART DMA transfer complete callback */ huart->hdmatx->XferCpltCallback = UART_DMATransmitCplt; /* Set the UART DMA Half transfer complete callback */ huart->hdmatx->XferHalfCpltCallback = UART_DMATxHalfCplt; /* Set the DMA error callback */ huart->hdmatx->XferErrorCallback = UART_DMAError; /* Set the DMA abort callback */ huart->hdmatx->XferAbortCallback = NULL; /* Enable the UART transmit DMA channel */ if (HAL_DMA_Start_IT(huart->hdmatx, (uint32_t)huart->pTxBuffPtr, (uint32_t)&huart->Instance->TDR, Size) != HAL_OK) { /* Set error code to DMA */ huart->ErrorCode = HAL_UART_ERROR_DMA; /* Restore huart->gState to ready */ huart->gState = HAL_UART_STATE_READY; return HAL_ERROR; } } /* Clear the TC flag in the ICR register */ __HAL_UART_CLEAR_FLAG(huart, UART_CLEAR_TCF); /* Enable the DMA transfer for transmit request by setting the DMAT bit in the UART CR3 register */ ATOMIC_SET_BIT(huart->Instance->CR3, USART_CR3_DMAT); return HAL_OK; } else { return HAL_BUSY; } }
(一)函数功能是CPU以DAM模式发送 *pData指针指向的数据中固定长度的数据,并同时设置和使能DMA中断。
我们从代码中不难发现,DMA中断函数最终使用的居然是串口的中断函数。
进入到HAL_UART_Transmit_DMA这个函数中可以看到,它将DMA传输完成、半完成、错误的回调函数分别定向到了串口DMA传输完成、半完成、错误的回调函数
我们在DMA的函数中发现 static void UART_DMATransmitCplt(DMA_HandleTypeDef *hdma),打开函数功能的内部其实就是串口的发送完成中断函数 HAL_UART_TxCpltCallback(huart);可见即使使用DMA的发送方式,最后还是需要串口的发送完成中断函数相联系。
(二)串口的DMA接收
HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size);
该函数我们在之前的帖子里面有实际的使用过这里就不过多的介绍了。
(三)串口DMA的停止函数
HAL_UART_DMAStop(huart);
我们在进入DMA中断中,需要关闭该函数,这个时候DMA中就不能打开DMA的接收,需要等待数据处理完毕,在进行DMA的接收
(四)查询DMA中有效的数据个数
__HAL_DMA_GET_COUNTER(&hdma_usart2_rx)
#define __HAL_DMA_GET_COUNTER(__HANDLE__) ((__HANDLE__)->Instance->CNDTR)
在宏定义中我们不难发现,其实就是读取CNDTR寄存器内的数值。这个寄存器存储的是DMA传输的剩余传输数量。在接收数据时,用定义的最大传输数量减去这个剩余数量就是已经接收的数据个数。在发送数据时,用定义的最大传输数量减去这个剩余数量就是已经发送的数据个数。
(五)串口DMA的接收函数
HAL_UART_DMAStop(UART_HandleTypeDef *huart);
二:STM32 cube 软件配置如下:
我们之前在原来的工程中,打开DMA的发送函数,配置一下DMA的发送函数
这里我们配置成正常模式就好,如果配置成循环接收模式,在DMA接收的缓存区内,会实现数据顺延的情况。稍后在图片中体现
四:软件代码编写
4.1DMA的初始化部分
void HAL_UART_MspInit(UART_HandleTypeDef* huart) { GPIO_InitTypeDef GPIO_InitStruct = {0}; RCC_PeriphCLKInitTypeDef PeriphClkInit = {0}; if(huart->Instance==USART2) { /* USER CODE BEGIN USART2_MspInit 0 */ /* USER CODE END USART2_MspInit 0 */ /** Initializes the peripherals clocks */ PeriphClkInit.PeriphClockSelection = RCC_PERIPHCLK_USART2; PeriphClkInit.Usart2ClockSelection = RCC_USART2CLKSOURCE_PCLK1; if (HAL_RCCEx_PeriphCLKConfig(&PeriphClkInit) != HAL_OK) { Error_Handler(); } /* Peripheral clock enable */ __HAL_RCC_USART2_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); /**USART2 GPIO Configuration PA2 ------> USART2_TX PA3 ------> USART2_RX */ GPIO_InitStruct.Pin = GPIO_PIN_2; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_PULLUP; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH; GPIO_InitStruct.Alternate = GPIO_AF7_USART2; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); GPIO_InitStruct.Pin = GPIO_PIN_3; GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; GPIO_InitStruct.Pull = GPIO_PULLDOWN; GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW; GPIO_InitStruct.Alternate = GPIO_AF7_USART2; HAL_GPIO_Init(GPIOA, &GPIO_InitStruct); /* USART2 DMA Init */ /* USART2_RX Init */ hdma_usart2_rx.Instance = DMA1_Channel1; hdma_usart2_rx.Init.Request = DMA_REQUEST_USART2_RX; 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_LOW; if (HAL_DMA_Init(&hdma_usart2_rx) != HAL_OK) { Error_Handler(); } __HAL_LINKDMA(huart,hdmarx,hdma_usart2_rx); /* USART2_TX Init */ hdma_usart2_tx.Instance = DMA1_Channel2; hdma_usart2_tx.Init.Request = DMA_REQUEST_USART2_TX; hdma_usart2_tx.Init.Direction = DMA_MEMORY_TO_PERIPH; hdma_usart2_tx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_usart2_tx.Init.MemInc = DMA_MINC_ENABLE; hdma_usart2_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_usart2_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_usart2_tx.Init.Mode = DMA_NORMAL; hdma_usart2_tx.Init.Priority = DMA_PRIORITY_LOW; if (HAL_DMA_Init(&hdma_usart2_tx) != HAL_OK) { Error_Handler(); } __HAL_LINKDMA(huart,hdmatx,hdma_usart2_tx); /* USART2 interrupt Init */ HAL_NVIC_SetPriority(USART2_LPUART2_IRQn, 2, 0); HAL_NVIC_EnableIRQ(USART2_LPUART2_IRQn); /* USER CODE BEGIN USART2_MspInit 1 */ /* USER CODE END USART2_MspInit 1 */ } }
使用DMA发送的时候,注意需要在 cube mx 中开始DMA的发送中断,否则导致调用该函数时候吗,进入硬件错误。
在串口初始化完毕后,开始DMA的接收功能代码如下所示:
HAL_UARTEx_ReceiveToIdle_DMA(&huart2,(uint8_t *)&RecBuffer2,256); //串口1开启DMA接受
4.2 使用串口DMA接收,并回传功能如下所示:
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if(huart->Instance == USART2) { //DMA为Normal模式 HAL_UART_DMAStop(huart); usart2_rx_len = 256 - (__HAL_DMA_GET_COUNTER(&hdma_usart2_rx)); HAL_UART_Transmit_DMA(&huart2, (uint8_t *)RecBuffer2, usart2_rx_len); HAL_UARTEx_ReceiveToIdle_DMA(&huart2,RecBuffer2,sizeof(RecBuffer2)); //HAL_UART_DMAStart(huart); } }
接收图片如下所示:
至此使用DMA的发送功能调试完毕。