STM32串口初始化与使用详解(基于HAL库编程)

2023-05-16

STM32串口初始化与使用详解

  • 串口简介
  • 串口初始化具体步骤
  • 串口收发理论
  • 代码执行

串口简介

USART(Universal Synchronous/Asynchronous Receiver/Transmitter)通用同步/异步串行接收发送器
即串口。是一个高度灵活的收发模块,它可以做到与上位机,或者其他MCU进行通信,实现上位机,其他MCU间的相互控制

STM32单片机芯片内部集成了多个串口。

一般而言,我们使用串口都使用异步收发模式。同步收发模式在外设和代码上的处理较为复杂,而且对时钟的要求严格,MCU的频率较PC频率低得多,所以没很大的必要使用同步模式。而同步通信的目的就是为了加快通信速率,但在使用上有些不尽如人意。以我用过的串口模块(外设)来说,基本上都是异步收发的串口模块(外设)。如ESP8266,其他一些蓝牙模块。

以STM32F4为例,该芯片集成了4个同步异步串口(USART1,USART2,USART3,USART6),2个异步串口(UART4,UART5),同步异步串口可以初始化为同步模式,也可以初始化为异步模式,而异步串口仅能初始化为异步模式。

若要将同步异步串口初始化为同步模式,则调用同步模式的串口初始化函数,否则调用异步模式串口初始化函数来初始化串口。

串口初始化具体步骤

这里我们先介绍初始化串口要用到的函数

相关HAL库函数具体功能
HAL_UART_Init顾名思义,异步串口初始化函数,初始化串口时我们调用的函数。所在源文件:stm32f4xx_hal_uart.c
HAL_UART_DeInit取消对某异步串口的初始化
HAL_UART_MspInit串口MSP初始化
HAL_UART_MspDeInit取消MSP初始化

这是MSP初始化函数的声明,可以发现,在void关键字之前有一个weak关键字

__weak void HAL_UART_MspInit(UART_HandleTypeDef *huart)

这个关键字告诉编译器,这个定义是弱定义,若在编译时,有一处函数定义不含此关键字,则所有该函数的调用都指向那一个非weak型的函数定义。这个操作叫函数重定向。因为在系统头文件里,MSP层是没有声明的,MSP层是具体对应每一个MCU的物理状态,系统文件不会声明也不能声明,但在单片机做上电初始化时,有一些函数又必须调用,所以系统文件采取了这种用户可以重定向的写法。我们在初始化串口时,需要自己写一个MSPInit,而且不含weak。函数声明如下:

	void HAL_UART_MspInit(UART_HandleTypeDef *huart)

我们要初始化串口,就要定义一个属于自己的MSP函数,并且系统会自动的把所有的MSP调用都指向我们定义的这个MSP函数

以下为系统定义的MSP函数,可见,内部几乎没什么内容,唯一一个函数调用是使该串口变为UNUSED(不使用)

__weak void HAL_UART_MspInit(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_MspInit could be implemented in the user file
   */
}

取消MSP使能函数的声明如下,可见在内部代码中的该函数也为弱定义,内部代码也十分简单,此处不做赘述

__weak void HAL_UART_MspDeInit(UART_HandleTypeDef *huart)

具体初始化过程(以CUBEMX生成的空项目为例):

使用串口第一步。添加串口同步异步源文件到项目中。(stm32f4xx_hal_usart.c,stm32f4xx_hal_uart.c)若只使用异步模式,应该不需要添加同步异步模式的头文件进入项目,但一方面是为了方便,一方面是为了少出一点事情,还是将它加进项目吧)
点击箭头指向的品字型

点击stm32f4xx_HAL_Driver
点击Add Files
点击Add Files
返回项目文件夹层级,可见有如上几个文件夹。点击Drivers
返回项目文件夹层级,可见有如上几个文件夹。点击Drivers

点击HAL_Driver,下一栏点src,从中找到stm32f4xx_hal_usart.c和stm32f4xx_hal_uart.c,点击Add加入项目。文件即加入了我们的项目

文件加入我们的项目后,我们仅仅引用了源文件,而头文件的引用,进入main.h中,打开stm32f4xx_hal.h,找到stm32f4xx_hal_conf.h

可以发现头文件的模块使能宏定义被注释掉了,把注释去掉即可。
这样一来所有的准备工作都做好了,我们开始着手初始化串口吧

串口初始化函数

void Usart1Init(int baudRate)
{
	UART1_Handle.Instance = USART1;							//串口句柄1与串口1对应
	UART1_Handle.Init.BaudRate = baudRate;					//初始化波特率为输入波特率
	UART1_Handle.Init.HwFlowCtl = UART_HWCONTROL_NONE;		//无硬件控制流
	UART1_Handle.Init.Mode = UART_MODE_TX_RX;				//串口异步收发模式
	UART1_Handle.Init.OverSampling = UART_OVERSAMPLING_16;	//默认过采样16倍
	UART1_Handle.Init.Parity = UART_PARITY_NONE;			//默认无校验位
	UART1_Handle.Init.StopBits = UART_STOPBITS_1;			//默认1位停止位
	UART1_Handle.Init.WordLength = UART_WORDLENGTH_8B;		//8bit数据位
	if (HAL_UART_Init(&UART1_Handle) != HAL_OK)
  {
    Error_Handler();
  }
}

其中UART1_Handle为UART_HandleTypeDef类型的结构体
结构体内容如下:

typedef struct __UART_HandleTypeDef
{
  USART_TypeDef                 *Instance;        /*!< UART registers base address        */
  UART_InitTypeDef              Init;             /*!< UART communication parameters      */
  uint8_t                       *pTxBuffPtr;      /*!< Pointer to UART Tx transfer Buffer */
  uint16_t                      TxXferSize;       /*!< UART Tx Transfer size              */
  __IO uint16_t                 TxXferCount;      /*!< UART Tx Transfer Counter           */
  uint8_t                       *pRxBuffPtr;      /*!< Pointer to UART Rx transfer Buffer */
  uint16_t                      RxXferSize;       /*!< UART Rx Transfer size              */
  __IO uint16_t                 RxXferCount;      /*!< UART Rx Transfer Counter           */
  DMA_HandleTypeDef             *hdmatx;          /*!< UART Tx DMA Handle parameters      */
  DMA_HandleTypeDef             *hdmarx;          /*!< UART Rx DMA Handle parameters      */
  HAL_LockTypeDef               Lock;             /*!< Locking object                     */
  __IO HAL_UART_StateTypeDef    gState;           /*!< UART state information related to global Handle management
                                                       and also related to Tx operations.
                                                       This parameter can be a value of @ref HAL_UART_StateTypeDef */
  __IO HAL_UART_StateTypeDef    RxState;          /*!< UART state information related to Rx operations.
                                                       This parameter can be a value of @ref HAL_UART_StateTypeDef */

  __IO uint32_t                 ErrorCode;        /*!< UART Error code                    */

#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
  void (* TxHalfCpltCallback)(struct __UART_HandleTypeDef *huart);        /*!< UART Tx Half Complete Callback        */
  void (* TxCpltCallback)(struct __UART_HandleTypeDef *huart);            /*!< UART Tx Complete Callback             */
  void (* RxHalfCpltCallback)(struct __UART_HandleTypeDef *huart);        /*!< UART Rx Half Complete Callback        */
  void (* RxCpltCallback)(struct __UART_HandleTypeDef *huart);            /*!< UART Rx Complete Callback             */
  void (* ErrorCallback)(struct __UART_HandleTypeDef *huart);             /*!< UART Error Callback                   */
  void (* AbortCpltCallback)(struct __UART_HandleTypeDef *huart);         /*!< UART Abort Complete Callback          */
  void (* AbortTransmitCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Abort Transmit Complete Callback */
  void (* AbortReceiveCpltCallback)(struct __UART_HandleTypeDef *huart);  /*!< UART Abort Receive Complete Callback  */
  void (* WakeupCallback)(struct __UART_HandleTypeDef *huart);            /*!< UART Wakeup Callback                  */

  void (* MspInitCallback)(struct __UART_HandleTypeDef *huart);           /*!< UART Msp Init callback                */
  void (* MspDeInitCallback)(struct __UART_HandleTypeDef *huart);         /*!< UART Msp DeInit callback              */
#endif  /* USE_HAL_UART_REGISTER_CALLBACKS */

} UART_HandleTypeDef;

这个结构体声明串口句柄。句柄是什么?因为笔者在初学时也未能理解,这里做一个简单的介绍。句柄,可以理解为一个对象的控制终端,所有和这个对象有关的数据和内容,或者相关的控制传递,通过这个句柄就可以完成。所以不论在串口初始化,还是在数据收发过程中,都需要传入相关串口的句柄。比较常见的句柄还有窗口句柄,控制PC显示时会用到。

串口句柄对使用串口而言十分重要,可以说是HAL库操纵串口的核心部位。也体现出了HAL库封装度之高。

可以发现,串口句柄中还包含一个串口初始化结构体,在串口初始化函数中便是往这个对象中写入了相关的初始化数据,再经由初始化函数对MCU进行配置

再下一步:初始化MSP层

void HAL_UART_MspInit(UART_HandleTypeDef* huart)								
{
	GPIO_InitTypeDef UART1_GPIO = {0};	
	if(huart->Instance == USART1)
	{
		
		__HAL_RCC_USART1_CLK_ENABLE();							//使能串口1时钟
  
    	__HAL_RCC_GPIOA_CLK_ENABLE();							//GPIOA时钟使能
		
		UART1_GPIO.Pin = GPIO_PIN_9 | GPIO_PIN_10;				//初始化A9 A10
		UART1_GPIO.Alternate = GPIO_AF7_USART1;					//串口1复用模式
		UART1_GPIO.Mode = GPIO_MODE_AF_PP;						//推挽复用模式
		UART1_GPIO.Pull = GPIO_PULLUP;							//上拉
		UART1_GPIO.Speed = GPIO_SPEED_FREQ_VERY_HIGH;			//高速
		
		HAL_GPIO_Init(GPIOA,&UART1_GPIO);						//GPIO初始化函数
		
		HAL_NVIC_SetPriority(USART1_IRQn, 1, 0);				//串口中断初始化及使能
    	HAL_NVIC_EnableIRQ(USART1_IRQn);
		
	}	
}

这样一来,串口便初始化完毕了。我们拿简单的代码来测试数据收发是否能成功吧。

先来介绍串口收发相关的函数

HAL库函数具体功能
HAL_UART_Transmit阻塞模式下的串口发送函数
HAL_UART_Receive阻塞模式下的串口接收函数
HAL_UART_Transmit_IT非阻塞模式下的串口发送配置函数
HAL_UART_Receive_IT非阻塞模式下的串口接收配置函数
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)

串口发送函数。该函数轮询发送DR寄存器状态,DR为空,则传输1字节给DR,重复此动作直到串口内部数据发送完毕,超时则返回

HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)

串口接收函数,该函数轮询接收DR寄存器状态,DR非空则从DR中获取内容存入pData,等待设置的时间,在时间内获得新的数据则接收放在前一个数据后,超时则返回

HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)

开启串口发送寄存器空中断(文章中若没有特殊说明,发送中断既是串口发送寄存器空中断)。注意!此并非以中断方式的串口发送函数,这个函数只是帮我们使能发送中断,而真正的发送,依靠的是串口中断服务函数中的UART_Transmit_IT()函数

HAL_StatusTypeDef HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)

开启串口接收寄存器非空中断(文章中若没有特殊说明,接收中断既是串口接收寄存器非空中断)同上,这个函数也并非中断方式的串口接收函数,而是帮我们开启了接收中断,真正的中断接收在中断服务函数中

此次讲解主要围绕着这几个函数与一些系统调用初始化会执行到的函数进行,以上函数请尽可能理解其工作原理。
另外,串口收发工作模式除轮询,中断外,还有DMA方式,DMA方式在计组(计算机组成原理,以下简称计组)中讲过原理,而要深究,最好要有计组的理论支撑。理论虽然不难,但在这里讲解还是略为复杂,毕竟本文的初衷是详解“串口初始化与使用”,而要好好地使用串口,笔者认为,理解了如何使用中断收发,就已经做到了,而中断收发的效率,较DMA方式也不差多少,再者中断方式可以以较复杂的模式处理收发数据,在优劣上不劣于DMA方式,只是各有千秋。所以,DMA方式执行的读写,在以后若有时间会专门做一次详解。此处不进行赘述。

调用HAL库的收发函数即可实现收发
Main.c的代码如下
Main.c

串口收发内容

串口收发理论

首先,我们来剖析一下,上面具体列出的几个函数的具体功能。
HAL_UART_Init:HAL库初始化函数。

HAL_StatusTypeDef HAL_UART_Init(UART_HandleTypeDef *huart)
{
  /* Check the UART handle allocation */
  if (huart == NULL)	//如果传入为空句柄,则返回ERROR
  {
    return HAL_ERROR;
  }

  /* Check the parameters */
  if (huart->Init.HwFlowCtl != UART_HWCONTROL_NONE)	//无硬件输入流时
  {
    /* The hardware flow control is available only for USART1, USART2, USART3 and USART6.
       Except for STM32F446xx devices, that is available for USART1, USART2, USART3, USART6, UART4 and UART5.
    */
    assert_param(IS_UART_HWFLOW_INSTANCE(huart->Instance));
    assert_param(IS_UART_HARDWARE_FLOW_CONTROL(huart->Init.HwFlowCtl));
  }
  else
  {
    assert_param(IS_UART_INSTANCE(huart->Instance));
  }
  assert_param(IS_UART_WORD_LENGTH(huart->Init.WordLength));
  assert_param(IS_UART_OVERSAMPLING(huart->Init.OverSampling));

//和串口有关的各种配置的设置(波特率,字长等)

  if (huart->gState == HAL_UART_STATE_RESET)
  {
    /* Allocate lock resource and initialize it */
    huart->Lock = HAL_UNLOCKED;

if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
    UART_InitCallbacksToDefault(huart);

    if (huart->MspInitCallback == NULL)
    {
      huart->MspInitCallback = HAL_UART_MspInit;
    }

    /* Init the low level hardware */
    huart->MspInitCallback(huart);
#else
    /* Init the low level hardware : GPIO, CLOCK */
    HAL_UART_MspInit(huart);								//对MSP层初始化进行调用
#endif /* (USE_HAL_UART_REGISTER_CALLBACKS) */
  }

  huart->gState = HAL_UART_STATE_BUSY;

  /* Disable the peripheral */
  __HAL_UART_DISABLE(huart);

  /* Set the UART Communication parameters */
  UART_SetConfig(huart);

  /* In asynchronous mode, the following bits must be kept cleared:
     - LINEN and CLKEN bits in the USART_CR2 register,
     - SCEN, HDSEL and IREN  bits in the USART_CR3 register.*/
  CLEAR_BIT(huart->Instance->CR2, (USART_CR2_LINEN | USART_CR2_CLKEN));
  CLEAR_BIT(huart->Instance->CR3, (USART_CR3_SCEN | USART_CR3_HDSEL | USART_CR3_IREN));

  /* Enable the peripheral */
  __HAL_UART_ENABLE(huart);					//外围设备使能

  /* Initialize the UART state */
  huart->ErrorCode = HAL_UART_ERROR_NONE;
  huart->gState = HAL_UART_STATE_READY;
  huart->RxState = HAL_UART_STATE_READY;

  return HAL_OK;
}

我们稍微理清一下这个函数的逻辑。库函数中基本都有许多选择分支,所以会显得比较长,但实际上执行的步骤并不多。可以看到,这里严谨地为每一种可能的状况都做出了回应,具体是在哪对这些设置进行修改,我们不需要关注。但是这个函数帮我们做了什么我们需要知道。所以,具体看以下几行代码

 if (huart == NULL)	//如果传入为空句柄,则返回ERROR
	...
	...
if (huart->Init.HwFlowCtl != UART_HWCONTROL_NONE)	//无硬件输入流时
	...
	...
 assert_param(IS_UART_HWFLOW_INSTANCE(huart->Instance));
 assert_param(IS_UART_HARDWARE_FLOW_CONTROL(huart->Init.HwFlowCtl));
  }
  else
  {
    assert_param(IS_UART_INSTANCE(huart->Instance));
  }
  assert_param(IS_UART_WORD_LENGTH(huart->Init.WordLength));
  assert_param(IS_UART_OVERSAMPLING(huart->Init.OverSampling));
	//和串口有关的各种配置的设置(波特率,字长等)
	...
	...
HAL_UART_MspInit(huart);
	...
	...
	 /* Enable the peripheral */
  __HAL_UART_ENABLE(huart);					//外围设备使能

  /* Initialize the UART state */
  huart->ErrorCode = HAL_UART_ERROR_NONE;
  huart->gState = HAL_UART_STATE_READY;
  huart->RxState = HAL_UART_STATE_READY;

  return HAL_OK;

可以发现,初始化函数的逻辑是这样的。首先判断传入句柄是否存在,不存在返回ERROR,存在则接着执行,下面的代码将句柄中的设置具体配置到串口中,然后调用初始化MSP层函数,最后使能这个外围设备,将所有的标志位都复位,返回HAL_OK。
HAL库对底层硬件层层封装,所以呈现在我们用户眼里的,就是简单的调用HAL_UART_Init即可完成对串口的初始化,但了解内部的代码帮我们做了什么是十分重要的,这将在我们介绍串口收发函数时,有更深刻的理解。

HAL_UART_DeInit:失能函数则主要将使能函数做的那些都关闭,内部逻辑简单,而且较少使用,为简洁易懂,不在此处贴出代码解析。

HAL_UART_MspInit:MSP层函数由用户编写,是物理层对应管脚、模式等的具体实现

void HAL_UART_MspInit(UART_HandleTypeDef* huart)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(huart->Instance==USART1)
  {
    __HAL_RCC_USART1_CLK_ENABLE();
  
    __HAL_RCC_GPIOA_CLK_ENABLE();
    GPIO_InitStruct.Pin = GPIO_PIN_9|GPIO_PIN_10;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_PULLUP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF7_USART1;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    HAL_NVIC_SetPriority(USART1_IRQn, 1, 0);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
  }
}

函数具体操作了串口和对应的GPIO的时钟,最开始让对应GPIO和该串口的时钟使能,并将所有管脚配置都初始化好,最后设置中断优先级并使能中断

HAL_UART_MspDeInit:将MSPINIT使能的部分关闭,较少使用,逻辑简单,不贴出代码解析。
HAL_UART_Transmit:我们比较关心的部分来了,串口作为一个通信口,我们更多的关注一定是如何实现数据的收发和处理,所以这个函数是很重要的函数,函数将发送过程封装成了一个函数待我们使用,只要我们做好了串口初始化的工作,就可以拿这个函数以阻塞的方式发送我们想要发送的数据。输入参数分别为,要使用串口的串口句柄,待发送数据的地址,发送数据的大小,发送等待时间。前三个参数第一次接触都很好理解。这个发送等待时间,第一次接触可能会有点疑惑。我们来讲解一下。由于这个函数是以阻塞的方式发送数据,当程序执行到这一行之后,函数试图从data地址处读出指定长度大小的数据,但是此时,数据还为空,读不到。那怎么办?由于这是单片机,外设上的很多事情还等待刷新,不能一直阻塞在这个地方,所以设置了一个时间上限,到这个时间上限时不论有没有数据都跳出这个函数,执行接下来的代码,这就是发送等待时间。

所以这个函数实现:在指定的时间内,从指定的区域读取出指定长度的数据,用指定的串口发送,若超时则返回

我们来看内部的代码实现

HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
{
  uint16_t *tmp;
  uint32_t tickstart = 0U;

  /* Check that a Tx process is not already ongoing */
  if (huart->gState == HAL_UART_STATE_READY)			//检查串口当前状态(如果是忙则不向串口提出发送请求)
  {
    if ((pData == NULL) || (Size == 0U))						//若无数据则返回HAL_ERROR
    {
      return  HAL_ERROR;
    }

    /* Process Locked */
    __HAL_LOCK(huart);

    huart->ErrorCode = HAL_UART_ERROR_NONE;
    huart->gState = HAL_UART_STATE_BUSY_TX;		//设置串口发送忙

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

    huart->TxXferSize = Size;				//告知串口发送数据大小
    huart->TxXferCount = Size;

    /* Process Unlocked */
    __HAL_UNLOCK(huart);

    while (huart->TxXferCount > 0U)				//串口待发送的数据大于0时,根据设置的发送模式发送数据,直到串口发送空
    {
      huart->TxXferCount--;
      if (huart->Init.WordLength == UART_WORDLENGTH_9B)
      {
        if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, tickstart, Timeout) != HAL_OK)
        {
          return HAL_TIMEOUT;
        }
        tmp = (uint16_t *) pData;
        huart->Instance->DR = (*tmp & (uint16_t)0x01FF);//(作为扩展内容在下面介绍,了解更好,不了解不是很影响对串口的理解)
        if (huart->Init.Parity == UART_PARITY_NONE)
        {
          pData += 2U;
        }
        else
        {
          pData += 1U;
        }
      }
      else
      {
        if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TXE, RESET, tickstart, Timeout) != HAL_OK)
        {
          return HAL_TIMEOUT;
        }
        huart->Instance->DR = (*pData++ & (uint8_t)0xFF);
      }
    }

    if (UART_WaitOnFlagUntilTimeout(huart, UART_FLAG_TC, RESET, tickstart, Timeout) != HAL_OK)
    {
      return HAL_TIMEOUT;
    }

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

    return HAL_OK;
  }
  else
  {
    return HAL_BUSY;
  }
}

所以可以看出,串口发送函数在这里做了什么:首先在发送标志不为忙,且有数据发送的情况下,记录下发送的SIZE大小,然后用while对其进行循环发送,直到SIZE个数据被发送出去,做完一切之后返回HAL_OK

HAL_UART_Receive:其实理解了发送函数接收函数就同理了,不赘述占用篇幅

HAL_UART_Transmit_IT:非阻塞模式下发送数据函数。这个函数需要拿出来仔细讲。因为这个函数本身并不实现发送的功能

HAL_StatusTypeDef HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, 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;
    }

    /* Process Locked */
    __HAL_LOCK(huart);

    huart->pTxBuffPtr = pData;
    huart->TxXferSize = Size;
    huart->TxXferCount = Size;

    huart->ErrorCode = HAL_UART_ERROR_NONE;
    huart->gState = HAL_UART_STATE_BUSY_TX;

    /* Process Unlocked */
    __HAL_UNLOCK(huart);

    /* Enable the UART Transmit data register empty Interrupt */
    __HAL_UART_ENABLE_IT(huart, UART_IT_TXE);

    return HAL_OK;
  }
  else
  {
    return HAL_BUSY;
  }
}


代码如上。可以发现代码长度短了很多,代码很多配置相关的,都和阻塞发送一样。唯一需要注意的是加黑的那一行。这一行代码的意思是开启串口发送空中断。只要串口发送DR寄存器中的数据为空则产生,请求新数据,而具体的发送则在中断服务函数中。这个函数只是配置中断使能的函数。接收函数也同理

串口中断接收配置函数:

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->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;
  }
  else
  {
    return HAL_BUSY;
  }
}

接收函数唯一不同的就是多开了两个错误中断。具体接收也在中断服务函数中

扩展资料:
在硬件层串口如何实现发送的?我们需要做哪些事情?哪些事情是串口硬件设备做的?

这个问题在笔者刚接触串口时困扰了很久,而且资料很少,因为涉及到内部的寄存器控制,现在不是主流,但对理解串口十分重要。

内部原理图如下:
STM32F0开发实战图18-1

串口硬件做了哪些?**串口硬件当配置好内部的CR寄存器,开始按指定的要求开始工作时,检测到DR寄存器内非空,便利用SR寄存器开始移位进行发送。所以我们发送数据的过程,就是往DR寄存器内部写入数据的过程。**串口通信硬件在DR寄存器发送结束后也会产生相应的标志位,告知MCU发送完毕,然后检测到标志位,我们就可以往DR寄存器中填入新的内容。

我们需要做哪些事情?如果用HAL库的话,只用调用发送函数就好了!如果不是用hal库是用寄存器的话,我们刚刚说了——发送的过程,就是往DR寄存器中写入数据的过程。所以我们要自己利用寄存器发送数据的话,就需要自己把数据从发送缓冲区移入到DR寄存器。换句话说,我们要做到——把自己的数据填入DR寄存器。就可以实现发送。当然大前提是串口已经初始化好了。初始化配置各种寄存器的函数就是相应的初始化函数。所以这些函数大大地减少了我们开发所需要的时间,也体现了封装的重要性。

在硬件层面如何实现发送的?过程如下。由MCU运算或采集到相关的数据,再经过代码填充到发送缓冲中,执行把发送缓冲的数据按序循环发送到DR寄存器中,DR寄存器由SR移位寄存器指挥发送到外部,循环这个过程直到数据发送完毕

代码执行

重要的函数介绍完毕了。最后我们开始看数据发送的过程在代码层面上如何执行的。这个过程我们需要关注的是,我们输入的代码,在MCU上电之后具体是如何执行的。如果使用了STLINK的数据线(其他调试线也行),可以进入DEBUG模式一步一步移动查看代码是按什么顺序执行的,具体调用了哪些底层的代码。虽然这在使用串口上没有多大的作用,但对实际理解MCU对串口是如何配置的,还是很有帮助的。而理解了这个,更能加深我们对MCU以及串口的理解

首先是配置过程。配置过程代码先要求MCU初始化了总线时钟,初始化了相应GPIO时钟,初始化了HAL库,调用该串口初始化的初始化函数,这个函数调用了HAL_UART_Init去按用户设置的初始化串口,然后再调用了用户编写的HAL_MSP_Init()初始化MSP层,配置相应的管脚和输入输出模式等,并开启了串口的时钟。这样一来,被指定的串口设备就变得可用

接下来是收发过程。以中断收数据为例,中断发数据不太常用,在理论上也是同理。中断接收,需要开启中断接收DR寄存器非空中断。每当中断接收寄存器中有数据输入,就会产生中断信号告知MCU,MCU执行相关操作,在数据全部被读入后,中断标志消失。上面介绍到的中断接收函数

HAL_UART_Receive_IT()帮我们打开了接收中断,在中断服务函数中调用的UART_Receive_IT()帮我们将数据放入了我们给串口指定的数据缓冲区。也就是说,使用HAL库读取,只需要调用HAL_UART_Receive_IT()函数,指定一个缓冲区。有数据产生后HAL库的内核代码会帮我们处理好所有和IO有关的操作,至于我们如何处理读入的数据,就可以依靠串口中断回调函数,对缓冲区内的数据进行相关的运算和执行相应的指令。

另外,稍微讲一下我对发送中断的看法。发送中断个人在使用时,正常使用时没有问题的,但由于数据是发出不是读入,很多情况下不需要回调函数再进行额外的操作。而且,将数据从发送缓冲区移入DR寄存器的操作需要MCU来进行,中断模式节约的时间仅有等待DR寄存器内的数据全部发出的时间。在MCU运算速度不高的情况下,发送中断节约的时间和提高的效率不高,操作起来有些繁琐。当然,目前的32位MCU的处理速度确实比较快,在MCU资源被大部分利用上的情况下,中断模式很有必要,正常使用时其实阻塞发送带来的影响几乎可以忽略不计。这也是我与一位从事嵌入式开发的资深工作者交流后,他给我的意见。经过交流探讨后,我较为支持这种想法。但是节省MCU资源总是一件好事,所以写出我对发送中断的看法,只是对发送的阻塞模式和非阻塞模式提出一点见解,并且让大家能够大概思考一下发送的阻塞模式和非阻塞模式的效率差距大概有多少。实际工程中,是否使用阻塞模式发送,还是看个人的想法。

至于接收中断,接收中断是十分必要的。发送的行为是MCU主动的行为,所以节约的时间只是等待串口DR寄存器中数据全部发出的时间,而接收则是被动的行为,阻塞将会让CPU的工作长时间停止,等待数据到来,然后处理数据。这是对CPU资源极大的浪费。读取数据,请尽可能不要使用阻塞模式。

因为本人水平实在有限,文章内容若有错误,敬请谅解。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

STM32串口初始化与使用详解(基于HAL库编程) 的相关文章

  • 简单算法——二分搜索的递归版本和非递归版本

    二分搜索 这是大家比较熟悉的算法了 xff0c 我们今天来复习一下 xff1a 前提 xff1a 二分查找要求所查找的顺序表必须是有序的 算法思路 定义left为顺序表最左端元素位置 xff0c right为顺序表右端元素位置 定义mid
  • Mysql(14)——事务

    概念 一个事务是由一条或者多条对数据库操作的SQL语句所组成的一个不可分割的单元 只有当事务中的所有操作都正常执行完了 xff0c 整个事务才会被提交给数据库 xff1b 如果有部分事务处理失败 xff0c 那么事务就要回退到最初的状态 x
  • Mysql(15)——锁机制 + MVCC(全)

    前言 事务的隔离级别在之前我们已经学习过 xff0c 那么事务隔离级别的实现原理是什么呢 xff1f 锁 43 MVCC 下面我们就来分开讲解 xff1a 表级锁 amp 行级锁 注意 xff1a 表锁和行锁说的是锁的粒度 xff0c 不要
  • DIY无人机组装与飞控参数调试记录(DJI NAZA-LITE)

    早就想玩一玩无人机 xff0c 奈何各种原因一直没有机会 xff0c 工作之后资金富足 xff0c 加上本身工作和这个相关性比较大 xff0c 于是就自己DIY了一台无人机 一 材料准备 xff1a F450机架 GPS支架 好盈乐天 20
  • Mysql(16)——日志

    前言 我们之前了解过redo log和undo log xff0c 他们是作用在InnoDb存储引擎层的 xff0c 今天我们来讲讲服务层的其他日志类型 一 错误日志 错误日志是 MySQL 中最重要的日志之一 xff0c 它记录了当 my
  • Mysql(17)——优化

    前言 一 SQL和索引优化 二 应用优化 除了优化SQL和索引 xff0c 很多时候 xff0c 在实际生产环境中 xff0c 由于数据库服务器本身的性能局限 xff0c 就必须要对上层的应用来进行一些优化 xff0c 使得上层应用访问数据
  • 项目——C++实现数据库连接池

    前言 在学习Mysql的时候 xff0c 我们都有这个常识 xff1a 对于DB的操作 xff0c 其实本质上是对于磁盘的操作 xff0c 如果对于DB的访问次数过多 xff0c 其实就是涉及了大量的磁盘IO xff0c 这就会导致MYsq
  • Redis入门——发展历程及NoSQL

    前言 随着社会的发展 xff0c 数据存储经历了诸多的过程 xff0c 这篇文章就是介绍Redis的发展由来 xff1a 1 单机Mysql时代 这种模式存在以下的瓶颈 xff1a 数据量太大 xff0c 一个机器存放不下数据的索引太大 x
  • Redis(1)——基本命令及数据类型(5+3)

    Redis的基本概念 Remote Dictionary Server xff1a 远程字典服务Redis 是一个开源 xff08 BSD许可 xff09 的 xff0c 内存中的数据结构存储系统 xff0c 它可以用作数据库 缓存和消息中
  • Redis(2)——事务机制

    Redis的事务机制 Redis的事务本质 xff1a 一组命令的集合一个事务中的所有命令都会都被序列化 xff0c 在事务执行的过程中 xff0c 会按照顺序执行 xff01 一次性 顺序性 排他性 执行一系列的命令Redis没有事务隔离
  • Redis(3)—— 持久化、发布订阅

    持久化 Redis是内存数据库 xff0c 如果不将内存中的数据库状态保存到磁盘中 xff0c 那么一旦服务器进程退出 xff0c 服务器中的数据库状态也会消失 所以Redis提供了持久化的功能 1 RDB xff08 Redis Data
  • Redis(4)——主从复制

    Redis主从复制 主从复制 xff1a 指的是将一个Redis服务器的数据 xff0c 复制到其他的Redis服务器 前者称为主节点 xff08 master leader xff0c 后者称为从节点 xff08 slave follow
  • Redis(5)——缓存穿透和雪崩

    概要 Redis缓存的使用 xff0c 极大的提高了应用程序的性能和效率 xff0c 特别是数据查询等 但同时 xff0c 它也带来了一些问题 其中 xff0c 最主要的问题就是数据一致性 xff0c 从严格意义上来讲 xff0c 这个问题
  • 复习:结构体大小的内存对齐问题

    内存对齐 内存对齐是指 xff1a 任意单个类型的数据都需要存放在能被它本身大小所能整除的地址上 基本类型的大小 char 1 short 2 int 4 long 4 long long 8 float 4 double 8 指针 4 8
  • 0.一些自己初学Solidworks的疑惑

    1 为什么要选择学习SolidWorks 首先 作为初学者 我们对一个东西并不是很了解 那么就需要别人来教我们 对吧 这些人可以是老师 可以是同学 可以是师傅 可以是网络上热心肠的大神 可以是一些培训机构 等等 首先呢 学习三维设计软件 看
  • LInux——五种IO模型

    Linux中的IO简述 IO主要分为以下的三种 xff1a 内存IO网络IO磁盘IO 通常我们所说的IO是后两者 xff0c Linux中无法直接操作IO设备 xff0c 必须通过系统调用请求kernal来协助完成IO的动作 xff0c 内
  • 复习:Linux中的软连接和硬连接

    前言 首先我们先来复习以下Linux的文件系统 Linux的文件系统是EXT4 以EXT4文件系统格式化磁盘时 xff0c 将磁盘分成了三个区 xff0c 分别是 xff1a 1 superblock xff1a 记录文件系统的整体信息 x
  • 复习:字节对齐的原则

    为什么需要字节对齐 xff1f 现代计算机中内存空间都是按照byte划分的 xff0c 从理论上讲似乎对任何类型的变量的访问可以从任何地址开始 xff0c 但实际情况是在访问特定类型变量的时候经常在特定的内存地址访问 xff0c 这就需要各
  • Reactor模型

    前言 首先让我们来回顾一下select poll和epoll是如何获取网络事件的 xff1a 在获取事件时 xff0c 先把我们要关心的连接传给内核 xff0c 再由内核检测 xff1a 若没有事件发生 xff0c 线程只需阻塞在这个系统调
  • Proactor模型

    前言 上一篇讲解的Reaactor是非阻塞的同步网络模式 xff0c 而Proactor是异步网络模式 至于异步IO怎么理解 xff1a 可以参考我的这一篇博客 xff1a Linux的五种IO模型 理解之后 xff1a 你就会感受到 xf

随机推荐

  • STL空间配置器(一级配置器及二级配置器)

    前言 在我们日常使用STL中的容器时 xff0c 我们是几乎感受不到空间配置器的存在 xff0c 因为他一直在默默工作 xff0c 我们在之前的这一篇博客中也大概介绍过 xff1a C 43 43 xff08 21 xff09 vector
  • HTTP各个版本的区别

    HTTP 1 0 短连接版本 HTTP 1 0规定浏览器与服务器只保持短暂的连接 xff0c 即每一次请求都需要与服务器建立一次TCP连接 xff0c 服务器完成请求处理后立即断开TCP连接 服务器不会跟踪每个客户也不记录过去的请求 xff
  • 实时时钟芯片DS1307的使用及驱动代码

    DS1307实时时钟芯片的介绍及驱动代码 目录 一 DS1307是什么 xff1f 二 DS1307的功能 三 DS1307的寄存器 四 代码 1 读出数据 2 写入数据 3 时间初始化设置 4 获取当前时间 五 注意事项 总结 一 DS1
  • 单片机测量NTC热敏电阻温度的方法(含程序代码)

    1 NTC介绍 NTC是负温度系数热敏电阻 xff0c 随着温度的升高 xff0c NTC的阻值会呈非线性的下降 2 硬件连接 这里采用100k 3950的热敏电阻 xff0c 100k代表的是在25 下的标准阻值 xff0c 3950是热
  • 代码编写规范

    目录 1 头文件 2 函数 3 标识符命名与定义 3 1 通用命名规则 3 2 文件命名规则 3 3 变量命名规则 3 4 函数命名规则 3 5 宏的命名规则 4 变量 5 宏 常量 6 质量保证 7 程序效率 8 注释 9 排版与格式 1
  • 1.SolidWorks各模块的学习顺序

    1 草图模块 nbsp nbsp nbsp nbsp nbsp nbsp nbsp nbsp 草图就是用线段画出零件的某一个视角的轮廓 草图是下面功能的基础 因为零件的三维建模 其实就是先画出草图 然后再通过拉伸 旋转 扫描 切除等命令生成
  • parser用法

    parser用法 导入库示例化添加参数解析参数设置属性 导入库 span class token keyword import span argparse 示例化 parser span class token operator 61 sp
  • roslaunch realsense2_camera rs_camera.launch和sudo apt-get install ros-melodic-rgbd-launch报错

    roslaunch realsense2 camera rs camera launch和roslaunch realsense2 camera rs rgbd launch报错 具体报错信息 roslaunch realsense2 ca
  • 如何设置cmake将外部文件作为资源添加到工作目录

    https stackoverflow com questions 46995733 how to set cmake in order to add txt files into working directory as resource
  • string、char*和char[]的转换

    char 和const char 的转换 const char 转 char xff08 1 xff09 为什么不能直接赋值 xff1f 这里你可以这么想 xff0c 假如const char类型字符串可以赋值给char类型 xff0c 那
  • 11-串口通信

    微控制器与外部设备的数据通信 xff0c 分为并行通信和串行通信 并行 xff1a 数据的各位同时发送或接受 xff0c 每个数据位使用一条导线 串行 xff1a 数据一位接一位地顺序发送或接收 串行通信有SPI IIC UART多种 xf
  • C语言编程规范设置 (vscode设置)

    1 打开vscode设置后 2 搜索format 3 把以下选项打上对勾 Editor Format On Paste Editor Format On Save Editor Format On Type 4 C Cpp 这一选项选择以下
  • c++ vscode 环境一键配置

    致谢 首先感谢原作者为我等初学者所做的软件 xff0c 其他文章讲了一堆的东西都没解决 xff0c 作者一个软件一步到位 xff0c 如果觉得不错的话可以star一下 xff0c 原作者视频地址 xff1a https www bilibi
  • 使用ESP8266实现单片机与上位机之间的wifi通信。

    使用ESP8266实现单片机与上位机之间的wifi通信 首先弄清楚8266的工作模式 xff0c 分别是 模式1 xff1a station xff0c 模式2 xff1a ap xff0c 模式3 xff1a station 43 ap
  • 【C 陷阱与缺陷】(四)连接

    码字不易 xff0c 对你有帮助 点赞 转发 关注 支持一下作者 微信搜公众号 xff1a 不会编程的程序圆 看更多干货 xff0c 获取第一时间更新 代码 xff0c 练习上传至 xff1a https github com hairrr
  • DIY无人机(匿名拓控者P2+F330机架)

    今年三月份的时候DIY过一个大疆NAZA 43 F450机架的无人机 xff0c 第一次体验DIY多旋翼无人机的全流程 xff0c 目的其实是为了后面更深入了解做准备 不然的话 xff0c 这钱买个大疆MINI3不香吗 xff1f DIY无
  • 在lammps模拟过程中的常用势函数设置

    文章目录 1 lj cut1 1 lj cut在in文件中使用方法1 2 lj cut在data文件中使用方法1 3 lj cut参数查询方法1 4 lj cut参数单位转换方法1 5 lj cut不同原子之间的参数1 6 lj cut参数
  • C语言十进制转16进制

    int DEC HEX uint32 t Dec int ram 61 0 整 int ray 61 0 余 uint32 t Hex 61 0x0 int i 61 0 do ram 61 Dec 16 ray 61 Dec 16 Dec
  • Windows系统下的Visual studio2019 安装 opencv4.5.1的安装

    OpenCV文档 xff1a https docs opencv org 4 5 1 examples html 安装OpenCV 4 5 1 xff0c 下载地址 https opencv org releases 下载完成后得到open
  • STM32串口初始化与使用详解(基于HAL库编程)

    STM32串口初始化与使用详解 串口简介串口初始化具体步骤串口收发理论代码执行 串口简介 USART Universal Synchronous Asynchronous Receiver Transmitter 通用同步 异步串行接收发送