STM32串口通信的三种方式———HAL库实现

前言

         本次将用到的单片机是STM32F103c8t6最小系统板,调试器是ST-Link V2、串口USB to TTL。开发软件用的是Clion、STM32CubeMX、串口调试助手。本次主要分享实现串口通信的3种方式,分别是阻塞、中断、DMA。下面将会通过HAL库实现上述3种方式。细节时刻部分可以选看。

目录

前言

一、HAL库实现

1.阻塞

1.阻塞发送       

2.阻塞接收

3.细节时刻

2.中断

1.中断发送

2.中断接收

3.细节时刻

3.DMA

1.DMA发送

2.DMA接收

3.细节时刻

总结


一、阻塞方式

STM32CubeMX配置串口阻塞

        首先根据自己的下载器在SYS的debug中选择对应的调试方式,我使用的是STlink-V2,因此在debug下拉选项中选择Serial Wire

         其次配置时钟。在RCC(Reset and Clock Control 复位和时钟控制)的HSE(高速外部)时钟源中选择Crystal/Ceramic Resonator(外部晶振),此时时钟树可设置外部震荡电路的晶振频率

          紧接着在Clock Configuration中设置时钟。如下如图,再按下回车后时钟树会自动生成所需要的时钟。

          最后配置串口1。在Connectivity组件下找到USART1,并在Mode中选择Asynchronous(异步,默认串口波特率为115200)。

 主要函数

HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size, uint32_t Timeout);

函数名:HAL_UART_Transmit

函数功能:用于指定串口发送指定长度数据

返回值:HAL_OK表示发送成功,HAL_ERROR、HAL_BUSY、HAL_TIMEOUT都表示发送失败

各个参数含义:

huart:串口句柄指针,如&huart1、&huart2等

pData:需要发送的数据缓冲区指针

Size:指定发送数据的长度

Timeout:发送超时时间,单位为毫秒

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

函数名:HAL_UART_Receive

函数功能:用于指定串口接收指定长度数据

返回值:HAL_OK表示接收成功,HAL_ERROR、HAL_BUSY、HAL_TIMEOUT都表示接收失败

各个参数含义:

huart:串口句柄指针,如&huart1、&huart2等

pData:需要接收的数据缓冲区指针(简单来说就是你接收的数据需要放到那个数组)

Size:指定接收数据的长度

Timeout:接收超时时间,单位为毫秒

1.阻塞发送       

        你可以直接在主循环之前,串口初始化之后,直接添加以下代码:

  /* USER CODE BEGIN 2 */
  HAL_UART_STATUS=HAL_UART_Transmit(&huart1,"hello embedded!\n", sizeof("hello embedded!\n"),0xFFFF);
  if(HAL_OK == HAL_UART_STATUS)//如果成功发送就执行下面的语句
      HAL_UART_Transmit(&huart1,"success", sizeof("success"),0xFFFF);
  /* USER CODE END 2 */

        当然这会使变量HAL_UART_STATUS报错,这时你需要在主函数一开始时声明这个变量,如下图:

        完成以上的代码编写即可编译和调试。这时设置好波特率并打开串口助手,随后复位STM32F103c8t6,串口助手接收数据如下图

2.阻塞接收

        首先在主函数开始处,声明一个数组用来做串口1的接收缓冲区。值得注意的是,这个数组的类型最好和HAL_UART_Receive函数的第二个参数类型uint8_t)一样。

        这个UART1_RxBuffer数组的长度为2,也就意味着串口最多接收2字节的数据。在主循环中编写如下代码

  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
    //如果UART1在1秒内成功接收,则发送接收到的数据
    if(HAL_OK == HAL_UART_Receive(&huart1,UART1_RxBuffer, sizeof(UART1_RxBuffer),1000))
        HAL_UART_Transmit(&huart1,UART1_RxBuffer, sizeof(UART1_RxBuffer),0xFFFF);
  }
  /* USER CODE END 3 */

        随后调试串口助手,在数据发送区发送ok,显示结果如下

3.细节时刻

        如果在发送区输入的数据是 gay ,那么数据接收区会显示 ga ,这不难理解,因为接收数据缓冲区UART1_RxBuffer的大小为两字节。要是你以较慢的速度在数据发送区发送数据 ,你会惊讶的发现数据接收区没有任何显示,如果你是快速连点两次发送数据,那么数据接收区会出现AA 。这又是怎么导致的呢?

        HAL_UART_Receive(&huart1,UART1_RxBuffer, sizeof(UART1_RxBuffer),1000)阻塞接收函数的第4个参数是关键。它的第四个参数规定了接收超时时间,将其改为0xFFFF(即65535毫秒,65.535秒)毫秒,那么这个时候发送数据 ,在后续的65.535秒内,串口接收区也不会超时接收。

二、中断方式

STM32CubeMX配置串口中断

         在原有的工程上,去STM32CubeMX中配置串口中断使能即可,如下图:

  主要函数

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

函数名:HAL_UART_Transmit_IT

函数功能:用于指定串口以中断方式发送指定长度数据

返回值:HAL_OK表示发送成功,HAL_ERROR、HAL_BUSY都表示发送失败

各个参数含义:

huart:串口句柄指针,如&huart1、&huart2等

pData:需要发送的数据缓冲区指针 

Size:指定发送数据的长度

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

函数名:HAL_UART_Receive_IT

函数功能:用于指定串口以中断方式接收指定长度数据

返回值:HAL_OK表示设置成功,HAL_ERROR、HAL_BUSY都表示设置失败

各个参数含义:

huart:串口句柄指针,如&huart1、&huart2等

pData:需要接收的数据缓冲区指针 

Size:指定接收数据的长度

__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
   */
}

        HAL_UART_RxCpltCallback( )是以HAL_UART_Receive_IT( )启动串口数据接收,并完成指定长度数据接收后调用的回调函数。__weak表示为”弱的“,简单来说这个回调函数可以被重载(重新定义)。

void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */

  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */
  
  /* USER CODE END USART1_IRQn 1 */
}

        USART1_IRQHandler(中断服务函数)是定义在stm32f1xx_it.c文件中的库函数,该函数在UART1触发中断时被调用,可以在里面直接添加需要执行的任务。

1.中断发送

        直接在主循环前,串口初始化之后,调用HAL_UART_Transmit_IT函数,代码如下

  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  HAL_UART_Transmit_IT(&huart1,"hello interrupt\n", sizeof("hello interrupt\n"));
  /* USER CODE END 2 */
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

  }
  /* USER CODE END 3 */

        紧接着调试并复位单片机,观察串口助手数据接收区显示如下图

2.中断接收

        首先需要在main.c文件中定义一个全局变量UART1_RxBuffer用来作为串口1的接收缓冲区,如下图

        其次需要调用HAL_UART_Receive_IT开启一次串口的中断接收,代码如下

 MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  HAL_UART_Transmit_IT(&huart1,"hello interrupt\n", sizeof("hello interrupt\n"));
  //开启一次中断接收
  HAL_UART_Receive_IT(&huart1,UART1_RxBuffer, sizeof(UART1_RxBuffer));
  /* USER CODE END 2 */
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
  while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */

  }

        然后在主函数前重新定义接收回调函数。注意回调函数HAL_UART_RxCpltCallback( )中调用了一次中断发送函数HAL_UART_Transmit_IT( ),它将接收到的数据发送给串口助手,后面会考察这个点。代码实现如下图:

       【难点】值得注意的是HAL_UART_Receive_IT( )函数完成一次数据接收后会关闭串口中断接收,不会自动进行下次的接收,因此需要再次调用HAL_UART_Receive_IT( )函数才能启动下次接收,但一般不在HAL_UART_RxCpltCallback( )函数中调用HAL_UART_Receive_IT( )函数。这是由于在HAL_UART_RxCpltCallback( )函数内调用HAL_UART_Receive_IT( )函数可能会导致递归调用和栈溢出,尤其是回调函数中没有适当的逻辑来限制或管理这些调用时。

        因此可以在中断服务函数(USART1_IRQHandler( ))中重新启动串口接收中断,即调用HAL_UART_Receive_IT( )函数。代码如下

//外部引用UART1_RxBuffer
extern uint8_t UART1_RxBuffer[4];

void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
  //重新开启串口中断接收
  HAL_UART_Receive_IT(&huart1,UART1_RxBuffer, sizeof(UART1_RxBuffer));
  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */
  
  /* USER CODE END USART1_IRQn 1 */
}

        在串口中断服务程序中,重新开启串口中断接收时,由于接收数据缓冲区(UART1_RxBuffer)定义在main.c文件中,因此需要使用 关键词extern 外部引用UART1_RxBuffer。

        接着在USART1_IRQHandler( )中断服务函数中调用HAL_UART_Receive_IT( )中断接收函数开启串口中断接收,准备下次接收数据。此时调试并复位单片机,接着在数据发送区先发送数据ABCD ,串口助手数据接收区显示如下图

        符合预期效果。下面试着在数据发送区依次发送 E、F、G、H 。不出意外的,只有在发送完后,数据接收区才会显示 EFGH 。这也就意味着 HAL_UART_RxCpltCallback( )回调函数被调用,因为在数据H被发送过去前,UART1_RxBuffer缓冲区已经接收到了EFG,还差一个数据就达到了HAL_UART_Receive_IT( )串口中断接收函数设置的接收缓冲区大小。所以,在H被发送过去后,数据缓冲区达到了指定长度, HAL_UART_RxCpltCallback( )回调函数被调用。过程如下图所示

        发送完 F,注意发送字节数和接收字节数。随后在发送完 后,观察发送字节数和接收字节数,如下图

3.细节时刻

       下面来看一看HAL_UART_Transmit_IT ( )串口中断发送函数、HAL_UART_Receive_IT( )串口中断接收函数、HAL_UART_RxCpltCallback( )接收回调函数、USART1_IRQHandler( )串口1中断服务函数它们之间的关联。

        首先来看HAL_UART_RxCpltCallback( )接收回调函数和USART1_IRQHandler( )串口1中断服务函数它们的执行先后顺序。不难发现 中断服务函数是在回调函数之前被调用 的,为了验证这一点,需要在USART1_IRQHandler( )函数中添加如下代码

void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
  //将接收缓存第一个数据修改为A
  UART1_RxBuffer[0] = 'A';
  //重新开启串口中断接收
  HAL_UART_Receive_IT(&huart1,UART1_RxBuffer, sizeof(UART1_RxBuffer));
  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */

  /* USER CODE END USART1_IRQn 1 */
}

        下面调试串口,复位单片机后,在数据发送区发送 1234 后,观察数据接收区接收的数据为 A234 (如下图)

        这就意味着USART1_IRQHandler( )在HAL_UART_RxCpltCallback( )之前被调用。那么在HAL_UART_RxCpltCallback( )被调用前USART1_IRQHandler( )被调用了多少次呢?在USART1_IRQHandler( )添加如下代码

//外部引用UART1_RxBuffer
extern uint8_t UART1_RxBuffer[4];

void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */
  //声明静态变量IR_cnt记录该函数被调用的次数
  static uint8_t IR_cnt = 0;
  //每调用一次就加1
  IR_cnt++;
  //调用次数赋值到缓冲区的第一个数据
  UART1_RxBuffer[0] = 'A'+ IR_cnt;
  //重新开启串口中断接收
  HAL_UART_Receive_IT(&huart1,UART1_RxBuffer, sizeof(UART1_RxBuffer));
  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */
  //调用HAL_UART_RxCpltCallback( )
  /* USER CODE END USART1_IRQn 1 */
}

        调试串口助手,并复位。在数据发送区发送 1234 ,观察数据接收区如下图所示 

      

        发现接收数据区显示 X234 ,从接收数据区的X表明一共调用了 23 次( 'X' - ' A' )串口中断服务函数。为什么会调用 23 次?,23 次又是否正确?

       首先,程序会先调用HAL_UART_Transmit_IT ( )函数发送 ”hello interrup\n“ ( 一共17字节),对于每个要发送的字符,都会触发一次中断。并且在 完成数据发送后还会触发一次中断 ,至此光发送字符串”hello interrup\n“就已经调用了18次串口中断,即 IR_cnt = 18

        随后在串口助手向单片机发送 1234 。此时触发串口接收中断,每接收一个字符就会触发一次串口中断,也就是4次,这个时候 IR_cnt = 22 。当接收数据缓冲区达到指定长度,也就是接收完成后,会调用接收回调函数(当你为串口设置了接收回调函数,并且串口接收完成指定长度的数据后,通常情况下,不会再次调用串口中断服务函数(ISR)来处理这些已经在回调函数中处理过的数据)。

        在接收回调函数中,又使用HAL_UART_Transmit_IT ( )函数发送接收数据缓冲区中的数据 1234 。当准备发送字符 ' 1 ' 时,触发中断,此时 IR_cnt = 23  ,那么 UART1_RxBuffer[0] = ' A ' + 23 (也就是 ‘ X ’)。因此串口助手接收到的数据是 X234 ,在发送完数据后 IR_cnt = 27

        当下次再向单片机发送数据 5678 时, 接收触发4次中断,在接收回调函数中调用HAL_UART_Transmit_IT ( )发送接收到的数据。当发送字符 ' 5 ' 时,触发中断,此时 IR_cnt = 32,即 UART1_RxBuffer[0] = 'A' + 32 (也就是 ‘ a ’) 。因此串口助手数据接收区显示 a678 。(如下图)

三、DMA方式

STM32cubeMX配置DMA

DMA简介

        DMA(Direct Memory Access,直接内存访问)是一种计算机硬件技术,它允许某些硬件子系统在不直接涉及CPU的情况下,读写主内存。 这种技术大大提高了数据传输的效率,尤其是在处理大量数据时,可以减少CPU的负担,使得CPU能够专注于执行计算和控制任务。

主要函数

HAL_StatusTypeDef HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, const uint8_t *pData, uint16_t Size)

函数名:HAL_UART_Transmit_DMA

函数功能:用于指定串口以直接内存访问方式发送指定长度数据

返回值:HAL_OK表示发送成功,HAL_ERROR、HAL_BUSY都表示发送失败

各个参数含义:

huart:串口句柄指针,如&huart1、&huart2等

pData:需要发送的数据缓冲区指针 

Size:指定发送数据的长度

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

函数名:HAL_UART_Receive_DMA

函数功能:用于指定串口以直接内存访问方式接收指定长度数据

返回值:HAL_OK表示设置成功,HAL_ERROR、HAL_BUSY都表示设置失败

各个参数含义:

huart:串口句柄指针,如&huart1、&huart2等

pData:需要接收的数据缓冲区指针 

Size:指定接收数据的长度

1.DMA发送

        在主循环之前,DMA初始化和串口初始化之后直接调用HAL_UART_Transmit_DMA( )函数发送 " hello DMA "。如下图

        调试并复位单片机,串口助手数据接收区显示如下图

2.DMA接收

        DMA接收的用法和中断接收的用法大差不差,因此这里介绍 串口空闲中断和DMA实现接收不定长度数据 。 

        串口空闲中断(IDLE interrupt)是在串口接收/发送完一帧数据后,检测到接收/发送线在一个字节时间内没有新的数据到来时触发的。 这意味着,当串口从接收/发送状态转换到空闲状态时,会产生一个中断信号,以便CPU能够及时响应并采取相应的动作,例如读取数据或准备接收新的数据帧。简单来说,串口空闲中断是在串口不收发数据时产生的中断。

        首先定义接收数据缓冲区UART1_RxBuffer,接着开启空闲中断,设置DMA接收缓冲区和大小,代码如下

         随后在USART1_IRQHandler( )中添加如下代码,记得外部引用UART1_RxBuffer。

void USART1_IRQHandler(void)
{
  /* USER CODE BEGIN USART1_IRQn 0 */

  /* USER CODE END USART1_IRQn 0 */
  HAL_UART_IRQHandler(&huart1);
  /* USER CODE BEGIN USART1_IRQn 1 */
  //获取空闲状态
  uint8_t idle_state= __HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE);
  uint8_t UART1_TxBuffer[128];
  if(RESET != idle_state) {
      //清除空闲中断标志位
      __HAL_UART_CLEAR_IDLEFLAG(&huart1);
      HAL_UART_DMAStop(&huart1);

      strcpy(UART1_TxBuffer, UART1_RxBuffer);
      //回显接收到的数据
      HAL_UART_Transmit_DMA(&huart1, UART1_TxBuffer, strlen(UART1_TxBuffer));

      memset(UART1_RxBuffer, '\0', 128);
      HAL_UART_Receive_DMA(&huart1, UART1_RxBuffer, 128);
  }
  /* USER CODE END USART1_IRQn 1 */
}

        最后调试串口助手,复位单片机后,向单片机分别发送 ABCD0123456789 ,串口助手数据接收区结果如下

3.细节时刻

        值得注意的是如果要使用串口空闲中断,记得使能开启串口空闲中断使能

//使能串口空闲中断
  __HAL_UART_ENABLE_IT(&huart1,UART_IT_IDLE);

总结

   以上3种方式各有千秋,要根据具体开发要求来选择实现方式。对于阻塞在对实时性要求不高的开发中可以使用,反之对实时要求较高的可以用到中断,和DMA。

在使用STM32G4的HAL库进行串口通信的例程中,可以使用fputc函数来完成重定向的工作。重定向时,我们可以将fputc函数的形参ch作为要发送到串口的数据。在使用printf函数时,它首先调用fputc函数,然后使用ST库的串口发送函数USART_SendData将数据转移到发送数据寄存器TDR,从而触发串口向PC发送相应的数据。在调用完USART_SendData函数后,需要使用while(USART_GetFlagStatus(USART1, USART_FLAG_TC) != SET)语句来不停地检查串口发送是否完成的标志位TC,并保持检测直到标志位为"完成",然后才能进入下一步的操作,以避免出错。同时,通过使用电平转换芯片,可以将串口信号线中使用的RS-232标准的电平信号转换成控制器能识别的TTL标准的电平信号,以实现通信。具体的例程可以在CubeMX下完成,并且可以参考模拟IIC HAL库移植而来的程序(例如基于HAL库模拟IIC驱动1.54寸OLED屏幕的例程)。如果需要获取模拟IIC HAL库驱动例程和本案例的完整代码工程,可以通过在公众号中回复IIC驱动获取下载地址来获取。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [STM32G431——串口通信](https://blog.csdn.net/m0_65088451/article/details/128811627)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] - *3* [STM32G030F6P6基于HAL库硬件IIC驱动1.54寸OLED屏幕](https://blog.csdn.net/chanchairen/article/details/124635254)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值