STM32H7 串口 空闲中断 任意长接收 Hal库 IDLE

今天主要记录一下STM32H7系列串口的使用,正点原子、野火等各大家都有教程,当然用起来也没有问题。

解决方法后边有红色大字提醒,直接看后边就可以,如果你没有时间想去了解HAL库的接收思想。

ST推的HAL库,在整个接收过程中,是没有用到串口的接收空闲中断,它的处理有三种,分别是轮询,接收完成中断(每一个字节一次),DMA接收。

整个Hal库把接收和发送过程都封装好了,就用最简单的轮询方式看,先看库函数代码:

/**
  * @brief Receive an amount of data in blocking mode.
  * @note When FIFO mode is enabled, the RXFNE flag is set as long as the RXFIFO
  *       is not empty. Read operations from the RDR register are performed when
  *       RXFNE flag is set. From hardware perspective, RXFNE flag and
  *       RXNE are mapped on the same bit-field.
  * @param huart   UART handle.
  * @param pData   Pointer to data buffer.
  * @param Size    Amount of data to be received.
  * @param Timeout Timeout duration.
  * @retval HAL status
  */
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
  uint8_t  *pdata8bits;
  uint16_t *pdata16bits;
  uint16_t uhMask;
  uint32_t tickstart;

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

    /* Process Locked */
    __HAL_LOCK(huart);

    huart->ErrorCode = HAL_UART_ERROR_NONE;
    huart->RxState = HAL_UART_STATE_BUSY_RX;

    /* Init tickstart for timeout managment*/
    tickstart = HAL_GetTick();

    huart->RxXferSize  = Size;
    huart->RxXferCount = Size;

    /* Computation of UART mask to apply to RDR register */
    UART_MASK_COMPUTATION(huart);
    uhMask = huart->Mask;

    /* In case of 9bits/No Parity transfer, pRxData needs to be handled as a uint16_t pointer */
    if ((huart->Init.WordLength == UART_WORDLENGTH_9B) && (huart->Init.Parity == UART_PARITY_NONE))
    {
      pdata8bits  = NULL;
      pdata16bits = (uint16_t *) pData;
    }
    else
    {
      pdata8bits  = pData;
      pdata16bits = NULL;
    }

    /* as long as data have to be received */
    while (huart->RxXferCount > 0U)
    {
      if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_RXNE, RESET, tickstart, Timeout) != HAL_OK)
      {
        return HAL_TIMEOUT;
      }
      if (pdata8bits == NULL)
      {
        *pdata16bits = (uint16_t)(huart->Instance->RDR & uhMask);
        pdata16bits++;
      }
      else
      {
        *pdata8bits = (uint8_t)(huart->Instance->RDR & (uint8_t)uhMask);
        pdata8bits++;
      }
      huart->RxXferCount--;
    }

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

    /* Process Unlocked */
    __HAL_UNLOCK(huart);

    return HAL_OK;
  }
  else
  {
    return HAL_BUSY;
  }
}

简单的看,里边有个while循环,在规定的时间内接收固定字节数的数据,在裸机中,接收过程真就是接收过程,主循环就什么都干不了了,想想,480M的主频,你让我在这个地方等待接收直至完成,本来串口传输就慢(常用115200传输),简直浪费,如果是这样,我为何不用一个51 去玩,所以实际项目中这个方法,只能说拜拜。

但是这个方法作为一个串口接收原理说明是一个很好的例子。

接下来看中断接收方式,继续看库函数代码:

/**
  * @brief Receive an amount of data in interrupt mode.
  * @param huart UART handle.
  * @param pData Pointer to data buffer.
  * @param Size  Amount of data to be received.
  * @retval HAL status
  */
HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
  /* Check that a Rx process is not already ongoing */
  if (huart->RxState == HAL_UART_STATE_READY)
  {
    if ((pData == NULL) || (Size == 0U))
    {
      return HAL_ERROR;
    }

    /* Process Locked */
    __HAL_LOCK(huart);

    huart->pRxBuffPtr  = pData;
    huart->RxXferSize  = Size;
    huart->RxXferCount = Size;
    huart->RxISR       = NULL;

    /* Computation of UART mask to apply to RDR register */
    UART_MASK_COMPUTATION(huart);

    huart->ErrorCode = HAL_UART_ERROR_NONE;
    huart->RxState = HAL_UART_STATE_BUSY_RX;

    /* Enable the UART Error Interrupt: (Frame error, noise error, overrun error) */
    SET_BIT(huart->Instance->CR3, USART_CR3_EIE);

    /* Configure Rx interrupt processing*/
    if ((huart->FifoMode == UART_FIFOMODE_ENABLE) && (Size >= huart->NbRxDataToProcess))
    {
      /* Set the Rx ISR function pointer according to the data word length */
      if ((huart->Init.WordLength == UART_WORDLENGTH_9B) && (huart->Init.Parity == UART_PARITY_NONE))
      {
        huart->RxISR = UART_RxISR_16BIT_FIFOEN;
      }
      else
      {
        huart->RxISR = UART_RxISR_8BIT_FIFOEN;
      }

      /* Process Unlocked */
      __HAL_UNLOCK(huart);

      /* Enable the UART Parity Error interrupt and RX FIFO Threshold interrupt */
      SET_BIT(huart->Instance->CR1, USART_CR1_PEIE);
      SET_BIT(huart->Instance->CR3, USART_CR3_RXFTIE);
    }
    else
    {
      /* Set the Rx ISR function pointer according to the data word length */
      if ((huart->Init.WordLength == UART_WORDLENGTH_9B) && (huart->Init.Parity == UART_PARITY_NONE))
      {
        huart->RxISR = UART_RxISR_16BIT;
      }
      else
      {
        huart->RxISR = UART_RxISR_8BIT;
      }

      /* Process Unlocked */
      __HAL_UNLOCK(huart);

      /* Enable the UART Parity Error interrupt and Data Register Not Empty interrupt */
      SET_BIT(huart->Instance->CR1, USART_CR1_PEIE | USART_CR1_RXNEIE_RXFNEIE);
    }

    return HAL_OK;
  }
  else
  {
    return HAL_BUSY;
  }
}

乍一眼没有看出什么问题吧,它仅仅做了一个,启动接收状态,并打开接收完成中断,看函数名就知道接收过程是在中断中进行的,中断函数比较长,咱就不贴完整的代码了,只贴接收部分相关的:

/**
  * @brief Handle UART interrupt request.
  * @param huart UART handle.
  * @retval None
  */
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
{
  uint32_t isrflags   = READ_REG(huart->Instance->ISR);
  uint32_t cr1its     = READ_REG(huart->Instance->CR1);
  uint32_t cr3its     = READ_REG(huart->Instance->CR3);

  uint32_t errorflags;
  uint32_t errorcode;

  /* If no error occurs */
  errorflags = (isrflags & (uint32_t)(USART_ISR_PE | USART_ISR_FE | USART_ISR_ORE | USART_ISR_NE));
  if (errorflags == 0U)
  {
    /* UART in mode Receiver ---------------------------------------------------*/
    if (((isrflags & USART_ISR_RXNE_RXFNE) != 0U)
        && (((cr1its & USART_CR1_RXNEIE_RXFNEIE) != 0U)
            || ((cr3its & USART_CR3_RXFTIE) != 0U)))
    {
      if (huart->RxISR != NULL)
      {
        huart->RxISR(huart);
      }
      return;
    }
  }
/* 这里省略了其他关系不大的代码 */
}

从这能看出的是接收过程在huart->RxISR()中,因此我们还要看这个代码,上一段代码的倒数16行处对它有赋值,我们也需要去看,函数名是UART_RxISR_8BIT(),又要贴代码了   ^v^    ^v^:

/**
  * @brief RX interrrupt handler for 7 or 8 bits data word length .
  * @param huart UART handle.
  * @retval None
  */
static void UART_RxISR_8BIT(UART_HandleTypeDef *huart)
{
  uint16_t uhMask = huart->Mask;
  uint16_t  uhdata;

  /* Check that a Rx process is ongoing */
  if (huart->RxState == HAL_UART_STATE_BUSY_RX)
  {
    uhdata = (uint16_t) READ_REG(huart->Instance->RDR);
    *huart->pRxBuffPtr = (uint8_t)(uhdata & (uint8_t)uhMask);
    huart->pRxBuffPtr++;
    huart->RxXferCount--;

    if (huart->RxXferCount == 0U)
    {
      /* Disable the UART Parity Error Interrupt and RXNE interrupts */
      CLEAR_BIT(huart->Instance->CR1, (USART_CR1_RXNEIE_RXFNEIE | USART_CR1_PEIE));

      /* Disable the UART Error Interrupt: (Frame error, noise error, overrun error) */
      CLEAR_BIT(huart->Instance->CR3, USART_CR3_EIE);

      /* Rx process is completed, restore huart->RxState to Ready */
      huart->RxState = HAL_UART_STATE_READY;

      /* Clear RxISR function pointer */
      huart->RxISR = NULL;

#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
      /*Call registered Rx complete callback*/
      huart->RxCpltCallback(huart);
#else
      /*Call legacy weak Rx complete callback*/
      HAL_UART_RxCpltCallback(huart);
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */
    }
  }
  else
  {
    /* Clear RXNE interrupt flag */
    __HAL_UART_SEND_REQ(huart, UART_RXDATA_FLUSH_REQUEST);
  }
}

这样差不多一个接收过程就完整了,也仅仅是个差不多。这个函数里边做了一下工作:1.从外设里边读接收到的字节,并存储到缓存数组中。2.更新接收控制部分,就是那三行自增自减的代码。3.如果接收数据达到指定的数量,就调用HAL_UART_RxCpltCallback函数,并释放接收串口接收状态,关闭接收完成中断。用户需要实现HAL_UART_RxCpltCallback函数,需要注意的是,这个函数是在中断中执行的,单片机这类东西,我们都希望中断里边尽可能精简,显然HAL库那是不可能了。

DMA接收方式,H7暂时还没有仔细看,我在F1、F4中一律用DMA接收发送的,这里暂时不发表意见。

轮询和终端接收方式,都需要用户提供一个数据缓存区,并制定接收数据的量,否则返回值就是错误或者超时,实际上,比如你用串口和串口转WiFi模块通信的时候,每次传输的数据量都是不固定也是未知的,这就比较麻烦了。

当然解决方法有很多,如果你有心,完全可以直接寄存器操作,抛弃这个HAL库,并且效率你说了算。

正点原子的解决方式是,函数入口HAL_UART_Receive_IT填参数的时候,数量设为1个字节,相当于HAL库没有什么作用,反而占用了大量的执行空间和时间,效率更低。

精华来了,重点来了,然后再加个粗,把字号也放大一些。

先贴上我的代码,后边简要说明实现方法,代码如下:

#include "Uart.h"
#include "stm32h7xx_hal.h"

#define RxBufSize			1024

UART_HandleTypeDef huart1 = {0};
uint8_t RxBuf[2][RxBufSize] __attribute__((section (".RAM_D1")));
void (*Uart1RxCompleteCallback)(uint8_t *pData,uint16_t *Count);

void Uart1Init(uint32_t BaudRate,void (*RxCompleteCallback)(uint8_t *pData,uint16_t *Count))
{
	huart1.Instance = USART1;
	huart1.Init.BaudRate = BaudRate;
	huart1.Init.ClockPrescaler = UART_PRESCALER_DIV2;
	huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
	huart1.Init.Mode = UART_MODE_TX_RX;
	huart1.Init.OneBitSampling = UART_ONEBIT_SAMPLING_DISABLED;
	huart1.Init.OverSampling = UART_OVERSAMPLING_8;
	huart1.Init.Parity = UART_PARITY_NONE;
	huart1.Init.StopBits = UART_STOPBITS_1;
	huart1.Init.WordLength = UART_WORDLENGTH_8B;
	huart1.FifoMode = UART_FIFOMODE_DISABLE;
	
	HAL_UART_Init(&huart1);
	HAL_UART_Receive_IT(&huart1,RxBuf[0],RxBufSize);
	__HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);
	HAL_NVIC_EnableIRQ(USART1_IRQn);
	HAL_NVIC_SetPriority(USART1_IRQn,11,0);
	
	Uart1RxCompleteCallback = RxCompleteCallback;
}

void HAL_UART_MspInit(UART_HandleTypeDef *huart)
{
	if(huart == &huart1)//串口1
	{
		GPIO_InitTypeDef GPIO_InitStruct;
		
		GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
		GPIO_InitStruct.Pull = GPIO_NOPULL;
		GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_MEDIUM;
		GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
		GPIO_InitStruct.Pin = GPIO_PIN_10 | GPIO_PIN_9;
		
		__HAL_RCC_GPIOA_CLK_ENABLE();
		__HAL_RCC_USART1_CLK_ENABLE();
		
		HAL_GPIO_Init(GPIOA,&GPIO_InitStruct);
	}
}

void Uart1TxData(uint8_t *pData,uint16_t Count)
{
	HAL_UART_Transmit_IT(&huart1,pData,Count);
}

void USART1_IRQHandler(void)
{
	HAL_UART_IRQHandler(&huart1);
	if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE))
	{
		static uint16_t count;
		__HAL_UART_CLEAR_IDLEFLAG(&huart1);
		if(Uart1RxCompleteCallback)
		{
			count = RxBufSize - huart1.RxXferCount;
			huart1.RxState = HAL_UART_STATE_READY;
			if(huart1.pRxBuffPtr <= RxBuf[1])
				Uart1RxCompleteCallback(RxBuf[0],&count);
			else
				Uart1RxCompleteCallback(RxBuf[1],&count);
			HAL_UART_RxCpltCallback(&huart1);
		}
	}
}

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
	if(huart == &huart1)
	{
		if(huart->pRxBuffPtr <= RxBuf[1])
			HAL_UART_Receive_IT(&huart1,RxBuf[1],RxBufSize);
		else
			HAL_UART_Receive_IT(&huart1,RxBuf[0],RxBufSize);
	}
}

这是C文件,共计84行,也不多,连带着初始化也有了,下边是H文件,代码如下:

#ifndef __Uart_H_
#define __Uart_H_

#include "stdint.h"

void Uart1Init(uint32_t BaudRate,void (*RxCompleteCallback)(uint8_t *pData,uint16_t *Count));
void Uart1TxData(uint8_t *pData,uint16_t Count);

#endif /* End __Uart_H_ */

就是这么简单明了,咦,是不是发现接口函数只有发送没有接收,没关系,咱继续,贴一个简单应用,代码如下:

#include "Uart1Task.h"
#include "limits.h"//ULONG_MAX
#include "Uart.h"

#define Uart1RxCompleteFlag		0x01

static uint8_t *Uart1RxData;
static uint16_t Uart1RxCount;
TaskHandle_t Uart1TaskHandle;

void Uart1RxIRQ(uint8_t *pData,uint16_t *Count);

TaskHandle_t *GetTaskHandle_t(void)
{
	return &Uart1TaskHandle;
}

void Uart1Task(void *pvParameter)
{
	Uart1Init(115200,Uart1RxIRQ);
	while(1)
	{
		uint32_t NotifyValue = 0;
		xTaskNotifyWait(pdFALSE,ULONG_MAX,&NotifyValue,portMAX_DELAY);
		Uart1TxData(Uart1RxData,Uart1RxCount);
	}
}

void Uart1RxIRQ(uint8_t *pData,uint16_t *Count)
{
	Uart1RxData = pData;
	Uart1RxCount = *Count;
	BaseType_t pxHigherPriorityTaskWoken;
	xTaskNotifyFromISR(*GetTaskHandle_t(),Uart1RxCompleteFlag,eSetBits,&pxHigherPriorityTaskWoken);
	portYIELD_FROM_ISR(pxHigherPriorityTaskWoken);
}

以上应用,仅仅是做一个,把接收到数据再发回去,这个Uart1Task函数是在FreeRTOS里边的一个任务,裸机中你就可以当main函数理解,用到了任务通知,这个不是重点,重点是初始化有连个参数,一个是波特率没啥好说的,另一个是注册了一个回调函数,在空闲中断发送的时候调用,这个函数也有连个参数,这个不需要你关系,你只需要知道,你接收到的数据在哪里,接收了多少,pData就是接收到数据起始位置的一个指针,Count就是接收到的数据量,目前单帧最大可接收1024字节,缓存为2K,暂时不支持连续超过1K数据的接收行为,当你仔细看一遍我的方法,你就知道该怎么解决这个问题了,以后有空再适配到DMA接收方式,那才是真正的完美。

简单说一下,实现思路,在HAL库的基础上,把固定死的大小设为缓存,接收过程由HAL库完成。每当空闲中断来,意味着,一帧不定长数据接收完毕了,此时只需要把接收到的数据起始地址和接收到的数据量发给用户代码即可,然后把缓存切换到另一个缓存区,防止覆盖,且留给用户足够多的时间处理串口数据。

 

 

上一篇:【数通面试私房菜之BGP专题】第二期:BGP状态机详解


下一篇:数据泵导出导入物化视图