蓝桥杯物联网设计与开发基础模块4-串口通信详解

目录

一、串口资源

(1)STM32L0 串口资源

(2)电路图

(3)串口收发单元功能框图

(4)实现方法

二、串口轮询方式

(1)接口函数

(2)STM32CubeMX 软件配置 

(3)代码编写

(4)实验现象

三、串口重定向

(1)设计思路

(2)代码编写

(3)实验现象

四、串口中断方式

(1)中断方式特点

(2)空闲中断

(3)接口函数

(4)STM32CubeMX 软件配置 

(5)代码编写

1. 普通串口中断方式

2. 定时器 + 串口中断方式

3. 串口空闲中断方式

(6)实验现象

五、串口 + DMA 方式

(1)DMA

(2)接口函数

(3)STM32CubeMX 软件配置 

(4)代码编写

(5)实验现象

六、踩坑日记

(1)代码不运作

(2)DMA 部分不能添加串口缓冲区清除函数


一、串口资源

(1)STM32L0 串口资源

        STM32L0 系列提供四个 USART 接口(USART1USART2USART4 和 USART5)能够以高达 4 Mit/s 的速度进行通信。
        USARTUniversa Synchronous/Asynchronous Receiver/Transmitter,通用同步/异步串行接收/发送器。它可以灵活的使用3根信号线来进行高速率同步通信,也可以使用2根信号线进行异步通信。 USART 可以使用相互独立的接收数据和发送数据方式的全双工操作。当处于同步操作时,可与主机时钟同步,也可与从机时钟同步,此时可以使用独立的高精度波特率发生器而不占用定时/计数器。USART 支持同步模式,因此 USART 需要同步时钟信号 USART CK。通常情况同步信号很少使用,因此一般的单片机 UART 和 USART 使用方式是一样的,都使用异步模式。USART 使用 TTL 电平,因此在与PC通信的时候需要进行电平转换。

(2)电路图

        🔵蓝桥杯物联网竞赛实训平台串口通信部分如下图1所示:

        实训平台左侧的芯片 GD32F350C8T6 是作为节点 A 和节点 B 的程序下载器和串口通信中继器。即 PC 机与芯片 GD32F350C8T6 相互连接,芯片 GD32F350C8T6 引出串口引脚 PA9/TXPA10/RX 通过芯片 CH443K (开关选择芯片)与节点的串口二引脚 PA2_TX PA3_RX 相互连接。

        故要实现节点与 PC 机串口通信,需要配置 USART2;

图1        CH443K 电路图
图2        USART2​​​

        USART1 根据电路图可知,与 Lora 芯片相连接,故本文不做探讨,会在 Lora 通信部分讲解;

图3       USART1 

(3)串口收发单元功能框图

        串口收发单元主要利用数据寄存器 DR,发送引脚 TX,接收引脚 RX,以及三个通信状态位 TXETC RXNE 来完成数据的接收和发送。

图4        串口收发单元
  • 数据寄存器 DR 在硬件上分为 TDR RDR 两个寄存器,通过数据的流向进行区分,在结构设计上采用了双缓冲结构;
  • 发送时,数据通过数据总线送入 TDR 寄存器,然后传送到发送移位寄存器完成数据转换,从并行数据转为串行数据,最后通过 TX 引脚发送;
  • 接收时,数据通过 RX 引脚逐位送入接收移位寄存器,8位数据接收完成后,送入 RDR 寄存器,供用户读取。
  • 数据收发过程中,可同时写入新的数据或读取已接收的数据,提高数据的传输效率

    ⭐⭐⭐通信状态标志位

    表1 通信状态标志位

    标志位

    名称

    含义
    TXE

    发送数据寄存器空标志。TDR 寄存器的内容已经传送到发送移位寄存器时,该位由硬件置1。如果串口控制寄存器 CR1 中的 TXEIE 位为1,将会触发发送数据寄存器空中断。

    注意:TXE 置1时,数据有可能还在发送。

    TC 发送完成标志。当发送移位寄存器的内容发送完成,同时 TDR 寄存器也为空时,该位由硬件置1,表示本次数据传输已经完成。如果串口控制寄存器 CR1 中的 TCIE 位为1,将会触发发送完成中断。
    注意:TC 置1时,数据才是真正地发送完成。
    RXNE 接收数据寄存器不为空标志。当移位寄存器的内容已经传送到接收数据寄存器 RDR 时,该位由硬件置1。如果串口控制寄存器 CR1 中的 RXNEIE 位为1,将会触发接收数据寄存器不为空中断。
  • 轮询方式下,可以直接检测标志位;
  • 中断方式下,需要在中断服务程序中通过检测不同的中断标志位,来判断出中断类型,然后执行后续的任务处理;
  • (4)实现方法

            由于串口部分实现方法较多,后文将从以下几个部分展开讲解:

  • 串口轮询方式
  • 串口重定向
  • 串口中断方式(三种方式)
  • 串口 + DMA方式

  • 二、串口轮询方式

    (1)接口函数

            🔅发送接口函数

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

  • 该函数连续发送数据,发送过程中通过判断 TXE 标志来发送下一个数据,通过判断 TC 标志来结束数据的发送;
  • 如果在等待时间内没有完成发送,则不再发送,返回超时标志;
  • 该函数由用户调用;
  •         🔅接收接口函数

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

  • 该函数连续接收数据,在接收过程中通过判断 RXNE 标志来接收新的数据;
  • 如果在超时时间内没有完成接收,则不再接收数据,返回超时标志;
  • 该函数由用户调用;
  • (2)STM32CubeMX 软件配置 

    🔅“工程建立、时钟树配置、Debug 串行线配置、代码生成配置” 在【蓝桥杯——物联网设计与开发】基础模块1- GPIO输出  一文中有讲解,这里不再赘述❗️

    1️⃣点击左侧 "Connectivity" → 选择 "USART2"  → 模式选择 "Asynchronous" (异步通信模式),不使能硬件流控制;

    图5        USART2 模式配置

    2️⃣在 "Parameter Settings" 中对串口参数进行修改(具体请根据题目要求修改);

  • 波特率9600Bit/s
  • 8位数据位
  • 无奇偶校验
  • 1位停止位
  • 使能接收和发送
  • 16倍过采样
  • 图6        设置通信参数

    3️⃣生成代码即可;

    (3)代码编写

    🟢️main 函数

    /* Private includes ----------------------------------------------------------*/
    /* USER CODE BEGIN Includes */
    #include <string.h>
    /* USER CODE END Includes */
    /* Private variables ---------------------------------------------------------*/
    
    /* USER CODE BEGIN PV */
    uint8_t puc_uart[10];		// 串口接收缓冲区定义
    /* USER CODE END PV */
    /**
      * @brief  The application entry point.
      * @retval int
      */
    int main(void)
    {
      /* USER CODE BEGIN 1 */
    
      /* USER CODE END 1 */
    
      /* MCU Configuration--------------------------------------------------------*/
    
      /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
      HAL_Init();
    
      /* USER CODE BEGIN Init */
    
      /* USER CODE END Init */
    
      /* Configure the system clock */
      SystemClock_Config();
    
      /* USER CODE BEGIN SysInit */
    
      /* USER CODE END SysInit */
    
      /* Initialize all configured peripherals */
      MX_GPIO_Init();
      MX_USART2_UART_Init();
      /* USER CODE BEGIN 2 */
    
      /* USER CODE END 2 */
    
      /* Infinite loop */
      /* USER CODE BEGIN WHILE */
      while (1)
      {
    		/* 接收 5 个字符完成 */
    		if(HAL_UART_Receive(&huart2, puc_uart, 5, 10) == HAL_OK)
    		{
    			/* 把接收的字符原样发送回去 */
    			HAL_UART_Transmit(&huart2, puc_uart, 5, 10);
    			/* 清空串口接收缓冲区 */
    			memset(puc_uart, 0, sizeof(puc_uart));
    		}
    		HAL_Delay(1);	// 1ms延时,防止芯片进入低功耗模式
        /* USER CODE END WHILE */
    
        /* USER CODE BEGIN 3 */
      }
      /* USER CODE END 3 */
    }

            在 while(1) 死循环中调用接收接口函数,固定判断5个字符是否接收完成,若接收完成,则将接收的字符原样传输回 PC 端,现象如下图所示:

    图7        实验现象

    (4)实验现象

  • PC 端发送5个字符后,节点将字符原样回传;

  • 三、串口重定向

    (1)设计思路

    ⭐在C语言中,printf 函数是将数据格式化输出到屏幕,scanf 函数是从键盘格式化输入数据;在嵌入式系统中,一般采用串口进行数据的输入和输出;
    重定向是指用户改写C语言的库函数,当链接器检查到用户编写了与C库函数同名的函数时,将优先使用用户编写的函数,从而实现对库函数的修改;
    ⭐printf 函数内部通过调用 fputc 函数来实现数据输出,scanf 函数内部通过调用 fgetc 函数来实现数据输入,因此用户需要改写这两个函数实现串门重定向。

    (2)代码编写

    ⚠️注意:实现重定向时,需要添加头文件 <stdio.h>

    🟡️fputc 函数

    #include <stdio.h>
    int fputc(int ch, FILE *f)
    {
    	/* 采用轮询方式发送1字节数据,超时时间设置为无限等待 */
    	HAL_UART_Transmit(&huart2, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    	return ch;
    }

    🟡️fgetc 函数

    #include <stdio.h>
    int fgetc(FILE *f)
    {
    	uint8_t ch;
    	/* 采用轮询方式接收1字节数据,超时时间设置为无限等待 */
    	HAL_UART_Receive(&huart2, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    	return ch;
    }

    🟢️main 函数

    uint8_t dat_rec;
    /**
      * @brief  The application entry point.
      * @retval int
      */
    int main(void)
    {
      /* USER CODE BEGIN 1 */
    
      /* USER CODE END 1 */
    
      /* MCU Configuration--------------------------------------------------------*/
    
      /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
      HAL_Init();
    
      /* USER CODE BEGIN Init */
    
      /* USER CODE END Init */
    
      /* Configure the system clock */
      SystemClock_Config();
    
      /* USER CODE BEGIN SysInit */
    
      /* USER CODE END SysInit */
    
      /* Initialize all configured peripherals */
      MX_GPIO_Init();
      MX_USART2_UART_Init();
      /* USER CODE BEGIN 2 */
      /* 测试字符串 */
      printf("Test uart\r\n");
      /* USER CODE END 2 */
    
      /* Infinite loop */
      /* USER CODE BEGIN WHILE */
      while (1)
      {
    		/* 判断是否接收1个字符的字符串 */
    		if(scanf("%c", &dat_rec) == 1)
    		{
    			/* 返回接收的字符串 */
    			printf("Received: %c", dat_rec);
    		}
    		else
    		{
    			/* 如果字符串不等于1个字符,则返回错误 */
    			printf("Error!");
    		}
    		HAL_Delay(1);  // 1ms延时,防止芯片进入低功耗模式
        /* USER CODE END WHILE */
    
        /* USER CODE BEGIN 3 */
      }
      /* USER CODE END 3 */
    }

            在 while(1) 死循环中调用重定向后的 scanf() 函数,固定判断1个字符是否接收完成,若接收完成,则将接收的字符传输回 PC 端,现象如下图所示:

    图8        实验现象

    (3)实验现象

  • PC 端发送1个字符后,节点将字符回传;

  • 四、串口中断方式

    (1)中断方式特点

    1. 发送数据时,将一字节数据放入数据寄存器 DR;接收数据时,将 DR 的内容存放到用户存储区;
    2. 中断方式不必等待数据的传输过程,只需要在每字节数据收发完成后,由中断标志位触发中断,在中断服务程序中放入新的一字节数据或者读取接收到的一字节数据;
    3. 在传输数据量较大,且通信波特率较高 (大于38400) 时,如果采用中断方式,每收发一个字节的数据,CPU 都会被打断,造成 CPU 无法处理其他事务。因此在批量数据传输,通信波特率较高时,建议采用 DMA 方式。

    (2)空闲中断

    1. 在一帧数据传输结束后,通信线路将会维持高电平,这个状态称为空闲状态;
    2. CPU 检测到通信线路处于空闲状态时,且空闲状态持续时间大于一个字节传输时间时,空闲状态标志 IDLE 将由硬件置1。如果串口控制寄存器 CR1 中的 IDLEIE 位为1,将会触发空闲中断(IDLE 中断);
    3. 由于空闲标志是在一帧数据传输完成后才置位,在有效数据传输过程中不会置位,因此借助空闲中断,可以实现不定长数据的收发

    (3)接口函数

            🔅中断方式发送接口函数

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

  • 函数将使能串口发送中断;
  • 函数将置位 TXEIE TCIE,使能发送数据寄存器空中断和发送完成中断。完成指定数量的数据发送后,将会关闭发送中断,即清零 TXEIE TCIE因此用户采用中断方式连续发送数据时,需要重复调用该函数,以便重新开启发送中断;
  • 当指定数量的数据发送完成后,将调用发送中断回调函数 HAL_UART_TxCpltCallback 进行后续处理;
  • 该函数由用户调用;
  • 发送过程:每发送一个数据进入一次中断,在中断中根据发送数据的个数来判断数据是否发送完成

            🔅中断方式接收接口函数

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

  • 函数将使能串口接收中断;
  • 函数将置位 RXNEIE,使能接收数据寄存器非空中断 RXNE。完成指定数量的数据接收后,将会关闭接收中断,即清零 RXNEIE因此用户采用中断方式连续接收数据时,要重复调用该函数,以重新开启接收中断;
  • 当指定数量的数据接收完成后,将调用接收中断回调函数 HAL_UART_RxCpltCallback 进行后续处理;
  • 该函数由用户调用;
  • 接收过程:每接收一个数据进入一次中断,在中断中根据接收数据的个数来判断数据是否接收完成

            🔅发送中断回调函数

    void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart);

  • 函数由串口中断通用处理函数 HAL_UART_IRQHandler 调用,完成所有串口的发送中断任务处理;
  • 函数内部需要根据串口句柄的实例来判断是哪一个串口产生的发送中断;
  • 函数由用户根据具体的处理任务编写;
  •         🔅接收中断回调函数

    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);

  • 函数由串口中断通用处理函数 HAL_UART_IRQHandler 调用,完成所有串口的接收中断任务处理;
  • 函数内部需要根据串口句柄的实例来判断是哪一个串口产生的接收中断;
  • 函数由用户根据具体的处理任务编写;
  • (4)STM32CubeMX 软件配置 

    🔅“工程建立、时钟树配置、Debug 串行线配置、代码生成配置” 在【蓝桥杯——物联网设计与开发】基础模块1- GPIO输出  一文中有讲解,这里不再赘述❗️

    1️⃣点击左侧 "Connectivity" → 选择 "USART2"  → 模式选择 "Asynchronous" (异步通信模式),不使能硬件流控制;

    图9        USART2 模式配置

    2️⃣在 "Parameter Settings" 中对串口参数进行修改(具体请根据题目要求修改);

  • 波特率9600Bit/s
  • 8位数据位
  • 无奇偶校验
  • 1位停止位
  • 使能接收和发送
  • 16倍过采样
  • 图10        设置通信参数

    3️⃣在 "NVIC Settings" 中,勾选使能 USART2 中断;

    图11        USART2 中断使能

    4️⃣生成代码即可;

    (5)代码编写

    1. 普通串口中断方式

    🟢️main 函数

    /* USER CODE BEGIN Includes */
    #include <string.h>
    #include <stdio.h>
    /* USER CODE END Includes */
    /* Private variables ---------------------------------------------------------*/
    
    /* USER CODE BEGIN PV */
    uint8_t puc_uart[10];		// 串口接收缓冲区定义
    uint8_t	flag_uart;			// 串口接收完成标志
    /* USER CODE END PV */
    
    /**
      * @brief  The application entry point.
      * @retval int
      */
    int main(void)
    {
      /* USER CODE BEGIN 1 */
    
      /* USER CODE END 1 */
    
      /* MCU Configuration--------------------------------------------------------*/
    
      /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
      HAL_Init();
    
      /* USER CODE BEGIN Init */
    
      /* USER CODE END Init */
    
      /* Configure the system clock */
      SystemClock_Config();
    
      /* USER CODE BEGIN SysInit */
    
      /* USER CODE END SysInit */
    
      /* Initialize all configured peripherals */
      MX_GPIO_Init();
      MX_USART2_UART_Init();
      /* USER CODE BEGIN 2 */
      /* 使能接收中断 */
      HAL_UART_Receive_IT(&huart2, (uint8_t *)puc_uart, 1);
      /* 测试字符串 */
      printf("Test uart_it\r\n");
      /* USER CODE END 2 */
    
      /* Infinite loop */
      /* USER CODE BEGIN WHILE */
      while (1)
      {
    		/* 判断数据是否接收完成 */
    		if(flag_uart)
    		{
    			/* 清除标志位 */
    			flag_uart = 0;
    			/* 将接收的字符发回 */
    			HAL_UART_Transmit_IT(&huart2, (uint8_t *)puc_uart, 1);
    			/* 清空接收缓冲区 */
    			memset(puc_uart, 0, sizeof(puc_uart));
    		}
    		HAL_Delay(1);		// 1ms延时,防止芯片进入低功耗模式
        /* USER CODE END WHILE */
    
        /* USER CODE BEGIN 3 */
      }
      /* USER CODE END 3 */
    }

            在 while(1) 中循环判断接收完成标志位,当接收完成时,清除标志位,并且将接收的字符通过中断方式发回 PC 端,然后清空接收缓冲区;

    🟠️串口接收中断回调函数

    /* Private user code ---------------------------------------------------------*/
    /* USER CODE BEGIN 0 */
    /* 串口接收回调函数 */
    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
    {
    	/* 判断发生接收中断的串口 */
    	if(huart->Instance == USART2)
    	{
    		/* 置位接收完成标志位 */
    		flag_uart = 1;
    		/* 使能接收中断 */
    		HAL_UART_Receive_IT(&huart2, (uint8_t *)puc_uart, 1);
    	}
    }
    /* USER CODE END 0 */

            ⚠️注意:在中断回调函数中一定要再次使能接收中断,否则无法再次触发中断;

    2. 定时器 + 串口中断方式

    🟢️main 函数

    /**
      * @brief  The application entry point.
      * @retval int
      */
    int main(void)
    {
      /* USER CODE BEGIN 1 */
    
      /* USER CODE END 1 */
    
      /* MCU Configuration--------------------------------------------------------*/
    
      /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
      HAL_Init();
    
      /* USER CODE BEGIN Init */
    
      /* USER CODE END Init */
    
      /* Configure the system clock */
      SystemClock_Config();
    
      /* USER CODE BEGIN SysInit */
    
      /* USER CODE END SysInit */
    
      /* Initialize all configured peripherals */
      MX_USART2_UART_Init();
      /* USER CODE BEGIN 2 */
      /* 使能串口接收中断 */
      HAL_UART_Receive_IT(&huart2, puc_uart, 1);
      /* USER CODE END 2 */
    
      /* Infinite loop */
      /* USER CODE BEGIN WHILE */
      while (1)
      {
    	Task_Uart();
        /* USER CODE END WHILE */
    
        /* USER CODE BEGIN 3 */
      }
      /* USER CODE END 3 */
    }

            ⚠️注意:此处使能串口接收中断为接收1个字符触发一次中断,用于不定长字符串接收;

    🔴串口任务处理函数

    void Task_Uart(void)
    {
        /* 判断接收标志位 */
    	if(flag_uart == 0)	return;
        /* 判断是否接收完毕 */
    	if(cnt_uart > 0)	return;
        /* 清除接收标志位 */
    	flag_uart = 0;
        /* 此处判断接收字符是否超过1位 */
    	if(index_uart > 1)
    		printf("error\r\n");
    	else
    	{
            /* 将接收的字符发回 */
    		printf("%c\r\n", puc_uart[1]);
    	}
        /* 清空接收缓冲区索引 */
    	index_uart = 0;
        /* 清空接收缓冲区 */
    	memset(puc_uart, 0, 10);
    }
    1. 首先判断接收标志位是否置位;
    2. 然后判断1帧是否接收完成;
    3. 对接收标志位清除;
    4. 处理串口逻辑;
    5. 清空接收缓冲区索引;
    6. 清空接收缓冲区;

    🟠️串口接收中断回调函数

    /* USER CODE BEGIN 0 */
    /* 串口接收回调函数 */
    void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
    {
        /* 判断是否是串口2 */
    	if(huart->Instance == USART2)
    	{
            /* 将接收的字符放在数组中 */
    		puc_uart[++index_uart] = puc_uart[0];
            /* 接收标志位置1 */
    		flag_uart = 1;
            /* 串口10ms计时赋值 */
    		cnt_uart = 10;
            /* 使能串口接收中断 */
    		HAL_UART_Receive_IT(&huart2, puc_uart, 1);
    	}
    }
    /* USER CODE END 0 */
    1. 由于每接收一个字符进入一次中断,每次接收的字符存放的位置在 puc_uart[0] 处,故将接收的字符存放到数组后面以防被接收覆盖;
    2. 随后将接收标志位置位1,表示串口接收到字符;
    3. 将串口 10ms 计时标志位赋值,如若 10ms 内未进入中断,则 cnt_uart 值变为0,表示 10ms 内没有新的字符接收,即一帧接收完毕;
    4. 使能串口接收中断;

    🟠️Systick 中断函数

    /**
      * @brief This function handles System tick timer.
      */
    void SysTick_Handler(void)
    {
      /* USER CODE BEGIN SysTick_IRQn 0 */
    
      /* USER CODE END SysTick_IRQn 0 */
      HAL_IncTick();
      /* USER CODE BEGIN SysTick_IRQn 1 */
      /* 如果cnt_uart 值>0,则每1ms减少1 */
      if(cnt_uart > 0) --cnt_uart;
      /* USER CODE END SysTick_IRQn 1 */
    }
  • 在 Systick 中断函数中对 cnt_uart 值进行判断:若大于0,则每 1ms 较少1,从而进行 10ms 计时;
  • 3. 串口空闲中断方式

    🟢️main 函数

    /**
      * @brief  The application entry point.
      * @retval int
      */
    int main(void)
    {
      /* USER CODE BEGIN 1 */
    
      /* USER CODE END 1 */
    
      /* MCU Configuration--------------------------------------------------------*/
    
      /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
      HAL_Init();
    
      /* USER CODE BEGIN Init */
    
      /* USER CODE END Init */
    
      /* Configure the system clock */
      SystemClock_Config();
    
      /* USER CODE BEGIN SysInit */
    
      /* USER CODE END SysInit */
    
      /* Initialize all configured peripherals */
      MX_GPIO_Init();
      MX_USART2_UART_Init();
      /* USER CODE BEGIN 2 */
    	/* 清除空闲标志位 */
    	__HAL_UART_CLEAR_IDLEFLAG(&huart2);
    	/* 使能IDLE中断 */
    	__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE);
    	/* 使能串口接收中断 */
    	__HAL_UART_ENABLE_IT(&huart2,UART_IT_RXNE);
    	/* 测试字符串 */
    	printf("Test uart_idle\r\n");
      /* USER CODE END 2 */
    
      /* Infinite loop */
      /* USER CODE BEGIN WHILE */
      while (1)
      {
    		/* 判断数据是否接收完成 */
    		if(flag_uart)
    		{
    			/* 清除标志位 */
    			flag_uart = 0;
    			/* 将接收的字符串发回 */
    			printf("%s\r\n", puc_uart);
    			/* 清除缓冲区索引 */
    			index_uart = 0;
    			/* 清空接收缓冲区 */
    			memset(puc_uart, 0, sizeof(puc_uart));
    		}
    		HAL_Delay(1);		// 1ms延时,防止芯片进入低功耗模式
        /* USER CODE END WHILE */
    
        /* USER CODE BEGIN 3 */
      }
      /* USER CODE END 3 */
    }

    while(1) 中检测接收完成标志位:

    1. 清除标志位;
    2. 将接收的字符串发回;
    3. 清除缓冲区索引;
    4. 清空接收缓冲区;

    🟠️串口中断函数

    /**
      * @brief This function handles USART2 global interrupt / USART2 wake-up interrupt through EXTI line 26.
      */
    void USART2_IRQHandler(void)
    {
      /* USER CODE BEGIN USART2_IRQn 0 */
    
      /* USER CODE END USART2_IRQn 0 */
      HAL_UART_IRQHandler(&huart2);
      /* USER CODE BEGIN USART2_IRQn 1 */
    	/* 检查接收标志位 */
    	if(__HAL_UART_GET_IT(&huart2,UART_IT_RXNE) !=RESET)
    	{
    		/* 将串口寄存器的值放入接收缓冲区 */
    		puc_uart[index_uart++] = USART2->RDR;
    	}
    	/* 添加IDLE中断处理 */
    	if(__HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE) != RESET)	//判断是否发送IDLE中断
    	{
    		/* 清除IDLE中断标志 */
    		__HAL_UART_CLEAR_IDLEFLAG(&huart2);
    		/* 调用用户编写的IDLE中断回调函数 */
    		HAL_UART_IdleCpltCallback(&huart2);
    	}
    	/* USER CODE END USART2_IRQn 1 */
    }

    在串口中断函数中分别检查两个标志位:接收标志位 RXNE 和空闲标志位 IDLE

  • 如果接收到字符,则从串口寄存器中取出字符放入接收缓冲区;
  • 如果检测到线路空闲,则调用用户编写的 IDLE 中断回调函数;
  •  🟠️IDLE 中断回调函数

    /* 串口空闲中断回调函数 */
    void HAL_UART_IdleCpltCallback(UART_HandleTypeDef *huart)
    {
    	/* 置位接收完成标志位 */
    	flag_uart = 1;
    }

            在 IDLE 中断回调函数中置位接收标志位,表示一帧数据接收完成;

  • 此处不做逻辑是为了简洁中断,防止中断时间过长!
  • (6)实验现象

  • PC 端发送1个字符后,节点将字符回传;
  • 图12        实验现象

    五、串口 + DMA 方式

    (1)DMA

            DMA(直接存储器访问): 用于在外设与存储器之间以及存储器与存储器之间进行高速数据传输。DMA 传输过程的初始化和启动由 CPU 完成,传输过程由 DMA 控制器来执行,无需 CPU 参与,从而节省 CPU 资源,提高利用率。

    (2)接口函数

            🔅DMA 方式发送接口函数

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

  • 该函数将启动 DMA 方式的串口数据发送;
  • 完成指定数量的数据发送后,可以触发 DMA 中断,在中断中将调用发送中断回调函数 HAL_ UART_TxCpltCallback 进行后续处理;
  • 该函数由用户调用;
  •         🔅DMA 方式接收接口函数

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

  • 该函数将启动DMA方式的串口数据接收;
  • 完成指定数量的数据接收后,可以触发 DMA 中断,在中断中将调用接收中断回调函数 HAL_ UART_RxCpltCallback 进行后续处理;
  • 该函数由用户调用;
  • (3)STM32CubeMX 软件配置 

    🔅“工程建立、时钟树配置、Debug 串行线配置、代码生成配置” 在【蓝桥杯——物联网设计与开发】基础模块1- GPIO输出  一文中有讲解,这里不再赘述❗️

    1️⃣点击左侧 "Connectivity" → 选择 "USART2"  → 模式选择 "Asynchronous" (异步通信模式),不使能硬件流控制;

    图13       USART2 模式配置

    2️⃣在 "Parameter Settings" 中对串口参数进行修改(具体请根据题目要求修改);

  • 波特率9600Bit/s
  • 8位数据位
  • 无奇偶校验
  • 1位停止位
  • 使能接收和发送
  • 16倍过采样
  • 图14        设置通信参数

     3️⃣在 "NVIC Settings" 中,勾选使能 USART2 中断;

    图15        USART2 中断使能

    4️⃣在 "DMA Settings" 中 → 点击 "Add",分别选择 USART2_TX 和 USART2_RX

    图16        DMA 设置

    5️⃣配置 "USART2_TX "参数;

  • 传输模式:普通模式
  • 地址递增:外设不递增,存储器递增
  • 数据宽度:字节
  • 图17        USART2_TX 参数配置

    6️⃣配置 "USART2_RX "参数;

  • 传输模式:普通模式
  • 地址递增:外设不递增,存储器递增
  • 数据宽度:字节
  • 图18        USART2_RX 参数配置

    7️⃣生成代码即可;

    (4)代码编写

    🟢️main 函数

    /* Private includes ----------------------------------------------------------*/
    /* USER CODE BEGIN Includes */
    #include <string.h>
    #include <stdio.h>
    /* USER CODE END Includes */
    /* Private variables ---------------------------------------------------------*/
    
    /* USER CODE BEGIN PV */
    extern DMA_HandleTypeDef hdma_usart2_tx;
    extern DMA_HandleTypeDef hdma_usart2_rx;
    uint8_t puc_uart[LENGTH];		// 串口接收缓冲区定义
    uint8_t	flag_uart;			// 串口接收完成标志
    uint8_t index_uart;			// 串口接收缓冲区索引
    /* USER CODE END PV */
    
    /**
      * @brief  The application entry point.
      * @retval int
      */
    int main(void)
    {
      /* USER CODE BEGIN 1 */
    
      /* USER CODE END 1 */
    
      /* MCU Configuration--------------------------------------------------------*/
    
      /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
      HAL_Init();
    
      /* USER CODE BEGIN Init */
    
      /* USER CODE END Init */
    
      /* Configure the system clock */
      SystemClock_Config();
    
      /* USER CODE BEGIN SysInit */
    
      /* USER CODE END SysInit */
    
      /* Initialize all configured peripherals */
      MX_GPIO_Init();
      MX_DMA_Init();
      MX_USART2_UART_Init();
      /* USER CODE BEGIN 2 */
    	/* 清除空闲标志位 */
    	__HAL_UART_CLEAR_IDLEFLAG(&huart2);
    	/* 使能IDLE中断 */
    	__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE);
    	/* 启动DMA接收 */
    	HAL_UART_Receive_DMA(&huart2, (uint8_t *)puc_uart, LENGTH);
    	/* 测试字符串 */
    	printf("Test uart_DMA\r\n");
      /* USER CODE END 2 */
    
      /* Infinite loop */
      /* USER CODE BEGIN WHILE */
      while (1)
      {
    		/* 判断数据是否接收完成 */
    		if(flag_uart)
    		{
    			/* 清除标志位 */
    			flag_uart = 0;
    			/* 关闭串口DMA */
    			HAL_UART_DMAStop(&huart2);
    			/* 计算接收字符串长度,实现不定长字符接收 */
    			index_uart = LENGTH - __HAL_DMA_GET_COUNTER(&hdma_usart2_rx);
    			/* 将接收的字符发回 */
    			HAL_UART_Transmit_DMA(&huart2, (uint8_t *)puc_uart, index_uart);
    			/* 清空缓冲区索引 */
    			index_uart = 0;
    			/* 重新使能DMA接收 */
    			HAL_UART_Receive_DMA(&huart2, (uint8_t *)puc_uart, LENGTH);
    			/* 清空接收缓冲区 */
    //			memset(puc_uart, 0, sizeof(puc_uart));
    		}
    		HAL_Delay(1);		// 1ms延时,防止芯片进入低功耗模式
        /* USER CODE END WHILE */
    
        /* USER CODE BEGIN 3 */
      }
      /* USER CODE END 3 */
    }

    while(1) 中对接收标志位进行判断,判断成功后进行如下操作:

    1. 清除接收完成标志位;
    2. 关闭串口DMA;
    3. 计算接收字符串长度,实现不定长字符接收;
    4. 将接收的字符发回;
    5. 清除缓冲区索引;
    6. 重新使能DMA接收;

    ⚠️注意:此处不能清空接收缓冲区(会在踩坑日记里具体说明)❗️

    🟠️串口中断函数

    /**
      * @brief This function handles USART2 global interrupt / USART2 wake-up interrupt through EXTI line 26.
      */
    void USART2_IRQHandler(void)
    {
      /* USER CODE BEGIN USART2_IRQn 0 */
    
      /* USER CODE END USART2_IRQn 0 */
      HAL_UART_IRQHandler(&huart2);
      /* USER CODE BEGIN USART2_IRQn 1 */
    	/* 添加IDLE中断处理 */
    	if(__HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE) != RESET)	//判断是否发送IDLE中断
    	{
    		/* 清除IDLE中断标志 */
    		__HAL_UART_CLEAR_IDLEFLAG(&huart2);
    		/* 调用用户编写的IDLE中断回调函数 */
    		HAL_UART_IdleCpltCallback(&huart2);
    	}
      /* USER CODE END USART2_IRQn 1 */
    }

            在串口中断函数中判断空闲标志位,如果空闲则清除空闲中断标志位,并且调用用户编写的 IDLE 中断回调函数

    🟠️IDLE 中断回调函数

    /* 串口空闲中断回调函数 */
    void HAL_UART_IdleCpltCallback(UART_HandleTypeDef *huart)
    {
    	/* 置位接收完成标志位 */
    	flag_uart = 1;
    }

            在 IDLE 中断回调函数中置位接收标志位,表示一帧数据接收完成;

  • 此处不做逻辑是为了简洁中断,防止中断时间过长!
  • (5)实验现象

    图19        实验现象
  • PC 端发送不定长字符后,节点将字符回传;
  • 六、踩坑日记

    (1)代码不运作

            🔅这款芯片属于低功耗类芯片,当 while(1) 中判断条件没有及时发生时,会进入低功耗状态,无法处理逻辑。通过 Debug 查找汇编代码,发现程序会卡在这一行汇编代码中:

    图20        汇编查询

            B 是汇编中的跳转指令,而后面是跳转的指令地址,仔细观察可以发现,该指令地址和需要跳转的指令地址是一样的,即代码死循环卡在这条指令

    ⭐当前解决方案:

            当需要处理的逻辑较少时,在 while(1) 循环中添加代码 HAL_Delay(1); 让 CPU “忙起来”,即可解决这个 bug

    (2)DMA 部分不能添加串口缓冲区清除函数

            CPU 执行代码到139行时,CPU 将发送任务转交给 DMA 进行,而 CPU 继续执行代码;如果有第145行清空接收缓冲区的代码,则 DMA 还未发送完成,而 CPU 将缓冲区清除,那么 DMA 后续发的字符都为空,此时串口助手接收字符数没有问题,但是显示为空!

    ⚠️注意:CPU DMA 是两个硬件单元,可以同时展开工作,所以需要考虑临界资源运用的情况!!!

    图21        代码

    ⭐当前解决方案:

            注释掉清空接收缓冲区的代码;

    物联沃分享整理
    物联沃-IOTWORD物联网 » 蓝桥杯物联网设计与开发基础模块4-串口通信详解

    发表评论