利用DMA双缓冲或半完成中断实现串口不定长数据的接收

        在《HAL版本DMA循环模式串口数据收发》中介绍了利用DMA循环模式进行串口数据的收发,STM32F4xx的DMA还提供了双缓冲的功能,采用双缓冲模式,可以在一个DMA完成接收后,对其缓冲区内数据进行处理的过程中,将此时接收到的数据放入第二个DMA缓冲区。双缓冲模式尤其对高速数据接收有着明显的优势,本文以上述循环接收方案为基础,提供实现双缓冲接收数据的实现方式。

        首先,从寄存器的角度讲,要实现双缓冲,需要将CR寄存器的DBM位置1,将该位置1后,硬件会强制使用循环模式,当一个缓冲满后,会自动交换缓冲区的地址。但我们采用HAL库版本的程序时,不太需要关注这些,但是值得注意的是,HAL库的stm32f4xx_hal_uart.c中并没有提供实现双缓冲的接口,因此需要我们对HAL库的程序进行一些改写:

HAL_StatusTypeDef HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
    /*省略库中起始部分的程序*/  
    /* Set the DMA abort callback */
    huart->hdmarx->XferAbortCallback = NULL;

    /* Enable the DMA stream */

    //HAL_DMA_Start_IT(huart->hdmarx, (uint32_t)&huart->Instance->DR, *(uint32_t *)tmp, Size);
    HAL_DMAEx_MultiBufferStart_IT(huart->hdmarx, (uint32_t)&huart->Instance->DR, (uint32_t)pData, (uint32_t)(pData+Size), Size);

    /* Clear the Overrun flag just before enabling the DMA Rx request: can be mandatory for the second transfer */
    __HAL_UART_CLEAR_OREFLAG(huart);

   /*省略库中结尾部分的程序*/
}

        上文中已经介绍采用HAL_UART_Receive_DMA接口使能DMA的接收,真正起到作用的是接口内部的HAL_DMA_Start_IT这个接口,如果想采用双缓冲模式,需要采用HAL_DMAEx_MultiBufferStart_IT这个接口才可以,这个接口的参数中需要提供两个缓冲区的地址,为了保证对外接口的一致性,我暂采用的是将第一个缓冲区首地址偏移缓冲区的长度作为第二个缓冲区的首地址。当然也可以直接改写HAL_UART_Receive_DMA,让使用者提供两个缓冲区的首地址,或者单独写一个接口都OK。

        当第一个缓冲区数据满后会产生完成中断,回调HAL_UART_TxCpltCallback这个函数,当第二个缓冲区满后,也会回调一个函数,但这个函数需要我们事先注册一下,在串口初始化的时候添加第二个缓冲区满后回调的函数:

int8_t MW_UART_Init(int8_t id,UART_HandleTypeDef *handle)
{ 
	MX_UART_ATTR *pUartAttr = &sUartAttr;	
	pUartAttr->id = id;	
	pUartAttr->handle = handle;
	pUartAttr->DamOffset = 0;
	pUartAttr->TransFlag = MW_TRANS_IDLE;
	pUartAttr->DmaSize = MW_UART_DMA_LEN / 2;
	pUartAttr->pReadDma = Uart1RxDma;
	pUartAttr->pWriteDma = Uart1TxDma;
	CFIFO_Init(&pUartAttr->ReadCFifo, Uart1RxBuff, MW_UART_BUFFER_LEN);
	CFIFO_Init(&pUartAttr->WriteCFifo, Uart1TxBuff, MW_UART_BUFFER_LEN);
        /*注册DMA的第二个缓冲区接收完成时回调的函数*/
	HAL_DMA_RegisterCallback(pUartAttr->handle->hdmarx,HAL_DMA_XFER_M1CPLT_CB_ID,UART_DMAReceiveMem1Cplt);
	/*使能串口空闲中断*/
	__HAL_UART_ENABLE_IT(handle, UART_IT_IDLE);
	/*使能DMA工作*/
	if(HAL_OK != HAL_UART_Receive_DMA(pUartAttr->handle, pUartAttr->pReadDma, pUartAttr->DmaSize))
	{
		return MW_FAIL;
	}

	return MW_SUCCESS;
}

        添加那句HAL_DMA_RegisterCallback,将UART_DMAReceiveMem1Cplt注册进去,这样当第二个DMA缓冲区满后就会调用UART_DMAReceiveMem1Cplt接口对第二个DMA缓冲区内的数据进行处理。当然串口空闲中断内的函数也需要进行处理,我们应该知道当产生空闲中断的时候,哪个DMA缓冲区在工作:

void32_t MW_UART_IRQHandler(UART_HandleTypeDef *huart)
{
	uint8_t *pDmaMem = NULL;
	int32_t RecvNum = 0;
	int32_t WriteNum = 0;
	int32_t DmaIdleNum = 0;
	MX_UART_ATTR *pUartAttr = &sUartAttr;

	if((__HAL_UART_GET_FLAG(huart,UART_FLAG_IDLE) != RESET))
	{ 
		__HAL_UART_CLEAR_IDLEFLAG(huart);
		DmaIdleNum = __HAL_DMA_GET_COUNTER(huart->hdmarx);
		
		if((huart->hdmarx->Instance->CR & DMA_SxCR_CT) == RESET)
		{
                /*实际指向第一个DMA缓冲区的首地址*/
			pDmaMem = pUartAttr->pReadDma;
		}
		else
		{
                /*实际指向第二个DMA缓冲区的首地址*/
			pDmaMem = pUartAttr->pReadDma + pUartAttr->DmaSize;
		}
			
		RecvNum = pUartAttr->DmaSize - DmaIdleNum - pUartAttr->DamOffset;                                        
		WriteNum = CFIFO_Write(&pUartAttr->ReadCFifo,pDmaMem + pUartAttr->DamOffset,RecvNum);
		if(WriteNum != RecvNum)
		{
			loge("Uart ReadFifo is not enough\r\n");
		}
		pUartAttr->DamOffset += RecvNum;
	}
}

        当产生空闲中断的时候,通过DMA的CR寄存器中的CT值来判断当前哪个DMA缓冲区在工作即可。至此,便可实现利用DMA双缓冲进行不定长数据的接收。本文中采用双缓冲的本质是为了才DMA完成中断响应的时候对其内存就行数据处理的时候,仍有空闲的内存来接收串口传递的数据。文中采用的两个内存实际是连续的,那采用DMA的半完成中断代替双缓冲一样可以实现预期功能,且HAL库中提供了半完成中断的回调函数,并且半完成中断时默认开启的,因此我们只要复写HAL库提供的HAL_UART_RxHalfCpltCallback接口函数就可以,不需要像采用双缓冲时需要改写HAL库内函数和注册回调函数。

        采用半完成中断时需要注意的一点是,在空闲完成中断回调函数内,如何配合两个DMA中断获取数据:

void32_t MW_UART_IRQHandler(UART_HandleTypeDef *huart)
{
	uint8_t *pDmaMem = NULL;
	int32_t RecvNum = 0;
	int32_t WriteNum = 0;
	int32_t DmaIdleNum = 0;
	MX_UART_ATTR *pUartAttr = &sUartAttr;

	if((__HAL_UART_GET_FLAG(huart,UART_FLAG_IDLE) != RESET))
	{ 
		__HAL_UART_CLEAR_IDLEFLAG(huart);

		/*计算DMA缓冲空闲大小*/
		DmaIdleNum = __HAL_DMA_GET_COUNTER(huart->hdmarx);
		/*计算产生空闲中断时,DMA数据写到了DMA缓冲区的前半部分还是后半部分*/
		if(DmaIdleNum <= (pUartAttr->DmaSize / 2))
		{
			pDmaMem = pUartAttr->pReadDma + pUartAttr->DmaSize / 2;
			RecvNum = pUartAttr->DmaSize / 2 - DmaIdleNum - pUartAttr->DamOffset;
		}
		else
		{
			pDmaMem = pUartAttr->pReadDma;
			RecvNum = pUartAttr->DmaSize  - DmaIdleNum - pUartAttr->DamOffset;		
		}			
		
		/*将接收到的数据写到接收循环临时缓冲区*/                                           
		WriteNum = CFIFO_Write(&pUartAttr->ReadCFifo,pDmaMem + pUartAttr->DamOffset,RecvNum);
		if(WriteNum != RecvNum)
		{
			loge("Uart ReadFifo is not enough\r\n");
		}
		pUartAttr->DamOffset += RecvNum;
	}
}

        可以在空闲中断回调函数里根据获取DMA缓冲区剩余的大小来判断当前数据写到了缓冲区的前半部分还是后半部分,以进行不同的操作。笔者最后采用的就是这种DMA半完成中断,DMA完成中断和串口空闲中断三者配合使用实现串口数据接收的方案,不需要改写HAL库内的程序,也可实现类似双缓冲带来的优势。