5

YIE002开发探索06-串口(中断)

 3 years ago
source link: http://yiiyee.cn/blog/2021/08/06/yie002%e5%bc%80%e5%8f%91%e6%8e%a2%e7%b4%a206-%e4%b8%b2%e5%8f%a3%e4%b8%ad%e6%96%ad/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

请保留-> 【原文:  https://blog.csdn.net/luobing4365 和 http://yiiyee.cn/blog/author/luobing/】

用轮询的方式获取串口数据,是一种非常低效的方法。本篇尝试使用中断的方式,来实现串口的通信。

1 STM32的串口中断

在上一篇中,简单介绍过STM32的串口。轮询方式的编码中,只需要了解相关API的用法就可以了,编写相对简单。

对中断编程而言,这种程度的背景知识是不够的。STM32的Cube Library构建了完整的串口中断编程架构,也提供了相应的API函数供调用。不过,从我的角度来看,这个架构有点难以理解。需要使用者从原理上去了解,才可能自如的使用这些函数。

因此,建议开发人员对串口的寄存器、中断事件等,有了基本的了解后,仔细阅读Cube Library提供的串口函数的实现,再去构建代码较好。

1.1 STM32的中断事件和寄存器

以下的内容基本来自于《STM32参考手册》,详细的内容可以在原文档中查找,我主要是将需要的内容进行了整理。

STM32的USART中断请求事情比较丰富,如表1所示。

表1 USART中断请求

中断事件事件标志使能位发送数据寄存器空TXETXEIECTS标志CTSCTSIE发送完成TCTCIE接收数据就绪可读TXNETXNEIE检测到数据溢出OREIDELIE检测到空闲线路IDLEIDELIE奇偶检验错PEPEIE断开标志LBDLBDIE噪声标志,多缓冲通信中的溢出错误和帧错误NE或ORT或FEEIE

表中的EIE标志位,仅当使用DMA接收数据时,才会使用。

USART的各种中断事件,被连接到同一中断向量,表中的中断事件大致可分为发送期间产生的中断事件和接收期间发生的中断事件。也即:

1)发送期间:发送完成、清除发送、发送数据寄存器空。
2)接收期间:空闲总线检测、溢出错误、接收数据寄存器非空、校验错误、 LIN断开符号检测、噪音标志(仅在多缓冲器通信)和帧错误(仅在多缓冲器通信)。

在实际的编程中,大部分的中断事件,是不需要关注的。编程中,主要关注的是接收数据就绪可读(也即接收数据寄存器非空)、发送完成以及检测到空闲线路。

当然,为了熟悉这些中断事件,对USART的寄存器也必须了解。图1给出了我绘制的思维导图,只对我关系的寄存器位进行了整理,更详细的资料,请参考《STM32参考手册》。

图1 USART寄存器的思维导图

1.2 Cube Library中的串口中断

为了解Cube Library中的API函数用法,特别是与中断相关的代码,个人认为最好把源代码通读一遍。

在这之中,需要重点关注对接收数据的处理。图2以串口1的中断函数为例,给出了相关的函数结构关系。

图2 Cube Library中的串口函数关系图

与中断相关的函数包括USART1_IRQHandler()、HAL_UART_IRQHandler()、UART_Receive_IT()、HAL_UART_RxCpltCallback()和UART_Transmit_IT(),它们构建了整个串口的中断架构。Cube Library中提供的中断模式接收函数HAL_UART_Receive_IT(),通过全局变量,以及操纵相应的寄存器位,与串口中断架构进行配合,获取需要的数据。

下面对这些函数的作用进行探索。注意,现在的代码是在STM32Cube_FW_F1_V1.8.4版本下的实现,其他版本的代码可能略有不同。

USART1_IRQHandler()

此函数是串口1的中断处理函数,其定义可以在startup_stm32f103xb.s中找到。它是串口接收和发送数据的中断处理函数,如下所示:

void USART1_IRQHandler(void)
{
  HAL_UART_IRQHandler(&huart1);
}

也就是说,在Cube Library提供的架构中,它只调用了HAL_UART_IRQHandler()。实际上,它还允许用户定义其他的功能,以实现Cube Library没有提供的串口中断事件处理,比如后面我们要用到的IDLE中断等。

HAL_UART_IRQHandler()

这是串口处理的主要函数,基本的框架都在此实现。先看下函数的基本内容:

/**
  * @brief  This function handles UART interrupt request.
  * @param  huart  Pointer to a UART_HandleTypeDef structure that contains
  *                the configuration information for the specified UART module.
  * @retval None
  */
void HAL_UART_IRQHandler(UART_HandleTypeDef *huart)
{
  uint32_t isrflags   = READ_REG(huart->Instance->SR);
  uint32_t cr1its     = READ_REG(huart->Instance->CR1);
  uint32_t cr3its     = READ_REG(huart->Instance->CR3);
  uint32_t errorflags = 0x00U;
  uint32_t dmarequest = 0x00U;

  /* If no error occurs */ //1 获取寄存器的各类标志,如果没有错误,进入接收处理;
  errorflags = (isrflags & (uint32_t)(USART_SR_PE | USART_SR_FE | USART_SR_ORE | USART_SR_NE));
  if (errorflags == RESET)
  {
    /* UART in mode Receiver -------------------------------------------------*/
    if (((isrflags & USART_SR_RXNE) != RESET) && ((cr1its & USART_CR1_RXNEIE) != RESET))
    {
      UART_Receive_IT(huart);
      return;
    }
  }

  /* If some errors occur *///2 发生错误,进行相应处理
  if ((errorflags != RESET) && (((cr3its & USART_CR3_EIE) != RESET) || ((cr1its & (USART_CR1_RXNEIE | USART_CR1_PEIE)) != RESET)))
  {
    /* UART parity error interrupt occurred ----------------------------------*/
    if (((isrflags & USART_SR_PE) != RESET) && ((cr1its & USART_CR1_PEIE) != RESET))
    {
      huart->ErrorCode |= HAL_UART_ERROR_PE;
    }

   //以下代码省略
   ......

    /* Call UART Error Call back function if need be --------------------------*/
    if (huart->ErrorCode != HAL_UART_ERROR_NONE)
    {
      /* UART in mode Receiver -----------------------------------------------*/
      if (((isrflags & USART_SR_RXNE) != RESET) && ((cr1its & USART_CR1_RXNEIE) != RESET))
      {
        UART_Receive_IT(huart);
      }
		 //以下代码省略
     ......
     }
      
  } /* End if some error occurs */

  //3 其他处理,分为DMA启用和DMA未启用,代码略
  ......
  
//4 发送数据处理
  /* UART in mode Transmitter ------------------------------------------------*/
  if (((isrflags & USART_SR_TXE) != RESET) && ((cr1its & USART_CR1_TXEIE) != RESET))
  {
    UART_Transmit_IT(huart);
    return;
  }

  /* UART in mode Transmitter end --------------------------------------------*/
  if (((isrflags & USART_SR_TC) != RESET) && ((cr1its & USART_CR1_TCIE) != RESET))
  {
    UART_EndTransmit_IT(huart);
    return;
  }
}

从其具体实现来看,它会对接收数据和发送数据进行处理。发送数据的处理过程相对简单,我们主要关注接收数据的处理,所调用的函数是UART_Receive_IT()。

这个函数有大量的寄存器操作,阅读的时候最好对照参考文档。不过,其脉络还是比较清晰的,理解上不会有太大的困难。

UART_Receive_IT()

此函数是串口接收数据的处理函数,从其实现代码中,可以窥见与HAL_UART_Receive_IT()配合的过程。

/**
  * @brief  Receives an amount of data in non blocking mode
  * @param  huart  Pointer to a UART_HandleTypeDef structure that contains
  *                the configuration information for the specified UART module.
  * @retval HAL status
  */
static HAL_StatusTypeDef UART_Receive_IT(UART_HandleTypeDef *huart)
{
  uint8_t  *pdata8bits;
  uint16_t *pdata16bits;

  /* Check that a Rx process is ongoing */
  if (huart->RxState == HAL_UART_STATE_BUSY_RX)
  {
    //1 将数据存储到用户给的缓冲区中,缓冲区指针是由HAL_UART_Receive_IT()给出的
    if ((huart->Init.WordLength == UART_WORDLENGTH_9B) && (huart->Init.Parity == UART_PARITY_NONE))
    {
      pdata8bits  = NULL;
      pdata16bits = (uint16_t *) huart->pRxBuffPtr;
      *pdata16bits = (uint16_t)(huart->Instance->DR & (uint16_t)0x01FF);
      huart->pRxBuffPtr += 2U;
    }
    else
    {
      pdata8bits = (uint8_t *) huart->pRxBuffPtr;
      pdata16bits  = NULL;

      if ((huart->Init.WordLength == UART_WORDLENGTH_9B) || ((huart->Init.WordLength == UART_WORDLENGTH_8B) && (huart->Init.Parity == UART_PARITY_NONE)))
      {
        *pdata8bits = (uint8_t)(huart->Instance->DR & (uint8_t)0x00FF);
      }
      else
      {
        *pdata8bits = (uint8_t)(huart->Instance->DR & (uint8_t)0x007F);
      }
      huart->pRxBuffPtr += 1U;
    }
		//2 所指定接收的数据量RxXferCount是否达到,若达到,则禁止接收中断
    if (--huart->RxXferCount == 0U)
    {
      /* Disable the UART Data Register not empty Interrupt */
      __HAL_UART_DISABLE_IT(huart, UART_IT_RXNE);

      //禁止其他中断以及相关处理,代码略
      ......
      }
      else
      {
       /* Standard reception API called */
#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 */
      }

      return HAL_OK;
    }
    return HAL_OK;
  }
  else
  {
    return HAL_BUSY;
  }
}

函数中使用的huart->RxXferCount,是由用户调用HAL_UART_Receive_IT()时提供的,表示用户需要接收的串口数据数目。在拿到指定数目的数据后,此函数会关闭中断响应。同时,调用回调函数HAL_UART_RxCpltCallback(),完成数据接收后的操作。

HAL_UART_RxCpltCallback()

这是Cube Library提供给用户的回调函数,由用户实现接收到数据之后的操作。其原型如下:

/**
  * @brief  Rx Transfer completed callbacks.
  * @param  huart  Pointer to a UART_HandleTypeDef structure that contains
  *                the configuration information for the specified UART module.
  * @retval None
  */
__weak void HAL_UART_RxCpltCallback(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_RxCpltCallback could be implemented in the user file
   */
}

函数使用_weak进行描述,当用户定义时,此定义就失效了。所以用户可以对此函数重新实现和定义,实现需要的功能。

也就是说,Cube Library提供的中断框架中,会在接收到用户指定数目的数据后,禁止中断。而实现接收数据的转存处理,以及使能接收中断的操作,则在HAL_UART_Receive_IT()给出。

HAL_UART_Receive_IT()和HAL_UART_Receive_IT()

/**
  * @brief  Receives an amount of data in non blocking mode.
  * @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 u16. In this case, Size must indicate the number
  *         of u16 available through pData.
  * @param  huart Pointer to a UART_HandleTypeDef structure that contains
  *               the configuration information for the specified UART module.
  * @param  pData Pointer to data buffer (u8 or u16 data elements).
  * @param  Size  Amount of data elements (u8 or u16) 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);

    /* Set Reception type to Standard reception */
    huart->ReceptionType = HAL_UART_RECEPTION_STANDARD;

    return(UART_Start_Receive_IT(huart, pData, Size));
  }
  else
  {
    return HAL_BUSY;
  }
}
/**
  * @brief  Start Receive operation in interrupt mode.
  * @note   This function could be called by all HAL UART API providing reception in Interrupt mode.
  * @note   When calling this function, parameters validity is considered as already checked,
  *         i.e. Rx State, buffer address, ...
  *         UART Handle is assumed as Locked.
  * @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 received.
  * @retval HAL status
  */
HAL_StatusTypeDef UART_Start_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
{
  huart->pRxBuffPtr = pData;
  huart->RxXferSize = Size;
  huart->RxXferCount = Size;

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

  /* Process Unlocked */
  __HAL_UNLOCK(huart);

  /* Enable the UART Parity Error Interrupt */
  __HAL_UART_ENABLE_IT(huart, UART_IT_PE);

  /* Enable the UART Error Interrupt: (Frame error, noise error, overrun error) */
  __HAL_UART_ENABLE_IT(huart, UART_IT_ERR);

  /* Enable the UART Data Register not empty Interrupt */
  __HAL_UART_ENABLE_IT(huart, UART_IT_RXNE);

  return HAL_OK;
}

这两个函数相对比较简单,给出来用户的缓冲区、需要接收的数据量,并使能接收中断。正好与之前的中断处理函数对应起来。

在了解了上述函数的实现原理后,下面实现完整的中断接收处理示例。

2 YIE002-STM32的串口编程(中断)

在实现串口编程的过程中,一直存在一个需要解决的问题:所接受的数据串什么时候结束?

如果是ASCII字符串,一般是以’\0’结尾,也有些STM32的开发板上,以此作为判断依据进行编程。但是在实际应用中,这种限定是不可能的,还得找到其他的方式来进行处理。

查看STM32的参考手册,可以知道“空闲帧”的存在。在每个数据帧接收之后,停止位开始一直为高电平,持续一帧的时间,此时会产生空闲中断。

也就是说,我们可以通过空闲中断,来判定数据帧的结束。因此,本篇的示例中,包含了串口接收和串口空闲中断处理程序的编写。

2.1 串口的Cube MX图形配置

在上一篇的串口代码的基础上,添加对串口1中断的支持。需要修改的地方,在Pinout&Configuration栏的System Core下,配置NVIC中与串口1相关的全局中断即可,如图3所示。

图3 USART1的中断配置

将USART1的中断配置抢占优先级为2,子优先级为3就可以了,其他配置都不用改动。

点击GENERATE CODE按钮,生成代码。

2.2 添加应用代码

为实现空闲中断,需要在几个地方添加代码。

首先修改stm32f1xx_it.c的串口1处理函数,修改为:

void USART1_IRQHandler(void)
{
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */
    //robin add 20210803
if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)
{
    __HAL_UART_CLEAR_IDLEFLAG(&huart1);
    MyUser_UART_IDLE_IRQHandler(&huart1);
}
  /* USER CODE END USART1_IRQn 1 */
}

也即添加了对串口1的空闲中断的处理,处理函数为我们添加的MyUser_UART_IDLE_IRQHandler()。

然后在串口1的初始化函数MX_USART1_UART_Init()中,添加对空闲中断的使能代码:

/* USER CODE BEGIN USART1_Init 2 */
    //此处使能IDLE中断
    __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);    //使能IDLE中断
    __HAL_UART_CLEAR_IDLEFLAG(&huart1);             //清除IDLE挂起标志位 
/* USER CODE END USART1_Init 2 */

完成这些后,就可以去实现自定义的数据接收和IDLE中断处理函数了。基本步骤如下:

1) 定义全局变量

所定义的全局变量为:

#define USART1_REC_LEN 200    //接收的最大字符串长度
uint8_t Usart1PackageFlag;        //接收到完整的包的标志,即一串数据接收完毕;
uint8_t bUsart1RxBuff[USART1_REC_LEN]; //接收缓冲,最大USART1_REC_LEN个字节
uint16_t    wUsart1RxNumber=0;      //接收到的数据字节数
uint8_t bUsart1Buffer[1]; //接收用的暂存缓冲区
//IDLE防抖使用的标志:只有接收到数据后,IDLE才有效,防止抖动
//1:数据接收到了;
uint8_t Usart1DataFlag;

2) 在主程序main()中实现数据的发送处理

//….前略
HAL_UART_Receive_IT(&huart1, bUsart1Buffer, 1);         //使能接收中断
  /* USER CODE END 2 */

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

    /* USER CODE BEGIN 3 */
        //robin 20210803
        if(Usart1PackageFlag==1)    //接收到数据包
        {
            Usart1PackageFlag=0;
            rUsart_len=wUsart1RxNumber;
            wUsart1RxNumber=0;
            for(i=0; i<rUsart_len; i++)
                rUsartData[i]=bUsart1RxBuff[i];

            if(HAL_UART_Transmit_IT(&huart1,rUsartData,rUsart_len)!=HAL_OK)
            {
                Error_Handler();
            }               
        }
  }
  /* USER CODE END 3 */
}

3) 在USER CODE BEGIN 4中实现数据的接收函数HAL_UART_RxCpltCallback()

void HAL_UART_RxCpltCallback(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_RxCpltCallback could be implemented in the user file
   */
    if(Usart1PackageFlag==0)        //上一个包处理完了或初次处理
    {
        bUsart1RxBuff[wUsart1RxNumber++]=bUsart1Buffer[0];
        Usart1DataFlag=1;
        if(wUsart1RxNumber>=USART1_REC_LEN)//超过此数目,则认为包已经接收完了
        {
            Usart1DataFlag=0;
            Usart1PackageFlag=1;        
        }
    }
    if(HAL_UART_Receive_IT(&huart1, bUsart1Buffer, 1) != HAL_OK) //  receive 1 byte data
    {
        Error_Handler();
    }
}

4) 实现自定义的IDLE中断函数

void MyUser_UART_IDLE_IRQHandler(UART_HandleTypeDef *huart)
{
    if(huart== &huart1)  //串口1的处理
    {
       if(Usart1DataFlag==1) //只有之前接收到数据了,才证明数据包传送完成,主要用来去除空闲中断的抖动
       {
            Usart1PackageFlag=1;
            Usart1DataFlag=0;
       }
    }   
}

至此就完成了所有的编程工作。

将程序编译下载,按照上一篇同样的方法,使用串口调试助手和USB转串的工具,在PC上进行测试即可。

38 total views, 1 views today


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK