STM32CubeMX系列04——串口(查询、中断、DMA、不定长接收、重定向)

====>>> 文章汇总(有代码汇总) <<<====

1. 所用硬件

正点原子Mini板,主控 STM32F103RCT6.

用到的外设:LED、串口1(PA9、PA10)。原理图:
在这里插入图片描述

2. 生成工程

2.1. 创建工程选择主控

在这里插入图片描述

2.2. 系统配置

配置时钟源
在这里插入图片描述

配置debug模式(如果需要ST-Link下载及调试可以勾选)
在这里插入图片描述

配置时钟树(可以直接在HCLK那里输入72,然后敲回车会自动配置)
在这里插入图片描述

2.3. 配置工程目录

在这里插入图片描述

勾选上会单独生成 c文件和头文件
在这里插入图片描述

2.4. 配置用到的外设

PA8:LED0。
在这里插入图片描述

3. 查询模式

3.1. 配置串口

在这里插入图片描述

3.2. 生成代码

在这里插入图片描述

3.3. 编写代码

编写main.c代码

int main(void)
{
    
    
  /* USER CODE BEGIN 1 */
	char light_on[11] = "Light on \r\n";
	char light_off[12] = "Light off \r\n";
	char rec_buf[1] = {
    
    0};
  /* USER CODE END 1 */

  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* Configure the system clock */
  SystemClock_Config();

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();

  /* USER CODE BEGIN WHILE */
  while (1)
  {
    
    
  	  // 串口1、接收数据的数组、接收的长度、等待时间
	  if(HAL_OK == HAL_UART_Receive(&huart1, (uint8_t *)rec_buf, 1, 0xFFFF))
	  {
    
    
			if(rec_buf[0] == 'A')
			{
    
    
				// 串口1、发送的数据、发送的长度(单位:字节)、超时时间
				HAL_UART_Transmit(&huart1, (uint8_t *)light_on, 11, 0xFFFF);
				HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET);
			}
			else
			{
    
    
				// 串口1、发送的数据、发送的长度(单位:字节)、超时时间
				HAL_UART_Transmit(&huart1, (uint8_t *)light_off, 12, 0xFFFF);
				HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET);
			}
	  }
	  
	  
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

3.4. 效果验证

编译、烧录、查看结果。
效果:使用串口调试助手
在这里插入图片描述
电脑发送一个字符A,单片机收到后 发送 Light on 给电脑,同时LED亮。
电脑发送一个字符B(其他字符),单片机收到后 发送 Light off 给电脑,同时LED灭。

4. 中断模式

4.1. 配置串口

在这里插入图片描述

配置中断
在这里插入图片描述

4.2. 生成代码

在这里插入图片描述

4.3. 运行原理及代码分析

和分析中断的过程一样。打开工程,在stm32f1xx_it.h中可以看到函数USART1_IRQHandler,显然,这是串口中断处理函数,在中断处理函数中又调用了函数HAL_UART_IRQHandler(&huart1);

在这里插入图片描述

然后去看看函数HAL_UART_IRQHandler(&huart1);都干了什么事。在HAL_UART_IRQHandler(&huart1);函数中考虑了很多情况,从注释可以看出,不出错误的话,会调用函数UART_Receive_IT(huart);然后就返回了。
在这里插入图片描述

因此,我们看看函数UART_Receive_IT(huart);,在这个函数中也分了很多很多情况,我们直接看最后,最后有个函数HAL_UART_RxCpltCallback(huart);,从注释可以看出,这是个接收完成的回调函数。(学完配置以后,一定要看看7.1小节,关于这个函数的触发条件说明)
在这里插入图片描述

然后再去看看这个回调函数。还是在这个文件中,定义如下。
在这里插入图片描述

可以看到这个函数是个弱函数,用户可以再次定义该函数。也就是说,我们可以重新定义这个函数,并在函数中编写我们处理中断的逻辑。

注释写的也很清楚:
当需要回调时,不应修改此函数,HAL_UART_RxCpltCallback可以在用户文件中实现

这段放在哪都行。大的工程可以创建一个文件放进去;这里直接放在main.c中了。

/* USER CODE BEGIN PV */
char light_on[11] = "Light on \r\n";
char light_off[12] = "Light off \r\n";
char rec_buf[1] = {
    
    0};
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    
    
	// 判断是不是串口1 中断
	if(huart->Instance == USART1)
	{
    
    
		if(rec_buf[0] == 'A')
		{
    
    
			// 串口1、发送的数据、发送的长度(单位:字节)、超时时间
			HAL_UART_Transmit(&huart1, (uint8_t *)light_on, 11, 0xFFFF);
			HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_RESET);
		}
		else
		{
    
    
			// 串口1、发送的数据、发送的长度(单位:字节)、超时时间
			HAL_UART_Transmit(&huart1, (uint8_t *)light_off, 12, 0xFFFF);
			HAL_GPIO_WritePin(LED0_GPIO_Port, LED0_Pin, GPIO_PIN_SET);
		}
		// 重新使能接收中断
		HAL_UART_Receive_IT(&huart1, (uint8_t *)rec_buf, 1);
	}
}
/* USER CODE END PFP */

int main(void)
{
    
    
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* Configure the system clock */
  SystemClock_Config();

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART1_UART_Init();
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
	// 开启串口接收中断
	HAL_UART_Receive_IT(&huart1, (uint8_t *)rec_buf, 1);
	// 发送开始提示信息
	HAL_UART_Transmit(&huart1, (uint8_t *)"start...\r\n", 10, 0xFFFF);
  while (1)
  {
    
    
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

4.4. 效果验证

编译、烧录、查看结果。

效果:使用串口调试助手
在这里插入图片描述

  1. 复位开发板,电脑收到 start…
  2. 电脑发送字符 A,单片机收到后 发送 Light on 给电脑,同时 LED 亮;
  3. 电脑发送字符 B(其他字符也可),单片机收到后 发送 Light off 给电脑,同时 LED 灭。

5. DMA模式

5.1. 配置串口

在这里插入图片描述

配置DMA

DMA (Direct Memory Access)直接存储器访问,可以不使用CPU,将数据在存储器与外设之间传输(也可以从存储器 到 存储器)。
STM32 最多有 2 个 DMA 控制器(DMA2 仅存在大容量产品中),STM32F103RCT6 有两个 DMA 控制器, DMA1 和 DMA2,DMA1 有 7 个通道。DMA2 有 5 个通道。

接收通道(设置为循环接收)在这里插入图片描述

发送通道(设置为普通模式)
在这里插入图片描述

DMA Request:

  • USART1_TX:表示在发送数据的时候使用 DMA通道。
  • USART1_RX:表示在接收数据的时候使用 DMA通道。

Mode:

  • Normal 模式:表示 CPU 发起 DMA 传输请求,就把数据通过DMA通道传输出去,传输完成之后会触发中断;
  • Circular模式,表示会循环把数据传输出去,传输完成之后从头再来。

Increment Address:这里在内存上勾选,没勾选外设,因为外设(这里是串口)地址是固定的,只需要把数据的 起始地址传输过去,地址不断累加就可以了。

勾选中断(DMA中断优先级设置的比串口中断要低,不过其实不低也能运行)。
在这里插入图片描述

5.2. 生成代码

在这里插入图片描述

5.3. 编写代码

/* USER CODE BEGIN PV */
char buf[13] = "running... \r\n";
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
    
    
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* Configure the system clock */
  SystemClock_Config();

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();

  /* USER CODE BEGIN WHILE */
	
	  // 开启DMA接收数据通道,将串口数据存到buf数组。
	  // 这里设置的循环模式,会不断接收串口数据到内存。
	  HAL_UART_Receive_DMA(&huart1, (uint8_t *)buf, 13);
	
	// 串口其他 DMA 相关函数
//	HAL_UART_DMAResume(&huart1); 恢复串口DMA
//	HAL_UART_DMAPause(&huart1) 暂停串口DMA
//	HAL_UART_DMAStop(&huart1); 结束串口DMA

  while (1)
  {
    
    
	  // 开启DMA发送传输通道
	  // 这里设置的普通模式,发送一次就关闭了。延迟一段时间后,重新打开发送。
	  HAL_UART_Transmit_DMA(&huart1, (uint8_t *)buf, 13);
	  HAL_Delay(1000);

    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

这里有个bug。

  • MX_DMA_Init();
  • MX_USART1_UART_Init();

这两个初始化,必须DMA在前,USART在后,否则编译通过,但是运行会卡死。
如果一开始就设置了DMA和串口,一般没啥问题。但是如果一开始只设置了串口,生成代码后,又加了DMA,就容易出现 DMA在后面的情况。
另外:可以通过设置,来设置先后顺序。
在这里插入图片描述

5.4. 效果验证

编译、烧录、查看结果。

效果:使用串口调试助手
在这里插入图片描述

  1. 初始化后,串口助手每隔一秒收到 字符串 running…
  2. 使用串口助手向开发板发送 任意字符(比如 1234567890ab),该字符会被存储到buf数组中,之后串口助手会不断收到 发送的任意字符串(如果发送的长度不足13个字符,则只会替换前几个字符,后面几个还是原来的)。
    在这里插入图片描述

6. 串口重定向

为了方便发送数据,我们使用printf函数发送数据,但是使用此函数需要先进行配置。

6.1. 配置原理

printf函数定义在 <stdio.h>头文件中,printf 函数根据 format 字符串给出的格式打印输出到 std::out(标准输出)中。
在这里插入图片描述
在这里插入图片描述
printf 函数会调用更底层的 I/O 函数:fputc去逐个字符打印。fputc 也定义于头文件 <stdio.h>中。
在这里插入图片描述
在这里插入图片描述
因此,我们如果想用 printf 打印数据到串口,只需要重新定义fputc函数,在fputc的函数中将数据通过串口发送,称之为:fputc重定向或者printf重定向

6.2. 实现

配置还是上面三种的配置,随便啦。

6.2.1. 使用MicroLIB库

MicroLib是对标准C库进行了高度优化之后的库,供MDK默认使用,相比之下,MicroLIB的代码更少,资源占用更少。

步骤一:在MDK中使用MicroLib重定向printf
在这里插入图片描述
步骤二:在生成的usart.c中添加重定向代码。这里放到最后
在这里插入图片描述

/* USER CODE BEGIN 1 */
/**********************printf重定向****************************/
// 需要调用stdio.h文件
#include <stdio.h>
int fputc(int ch, FILE *f)
{
    
      
	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
	return ch;
}
/* USER CODE END 1 */

main.c

  while (1)
  {
    
    
      printf("Hello, %s \r\n", "world");		// 字符串
      printf("Test int: i = %d \r\n", 100);		// 整数
      printf("Test float: i = %f \r\n", 1.234);	// 浮点数
      printf("Test hex: i = 0x%2x \r\n",100);	// 16进制
	  printf("中文测试 \r\n");					// 中文
	  HAL_Delay(2000);
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
  }

效果验证
编译、烧录、查看结果。

效果:使用串口调试助手
在这里插入图片描述
每隔两秒,发送数据。

6.2.2. 不使用MicroLIB库

不用勾选 Use MicroLIB,直接在 usart.c 文件中添加以下代码。
在这里插入图片描述

// 需要调用stdio.h文件
#include <stdio.h>
//取消ARM的半主机工作模式
#pragma import(__use_no_semihosting)//标准库需要的支持函数                 
struct __FILE 
{
    
     
	int handle; 
}; 
FILE __stdout;       
void _sys_exit(int x) //定义_sys_exit()以避免使用半主机模式
{
    
     
	x = x;
} 

int fputc(int ch, FILE *f)
{
    
      
	HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
	return ch;
}

main.c

  while (1)
  {
    
    
      printf("Hello, %s \r\n", "world");		// 字符串
      printf("Test int: i = %d \r\n", 100);		// 整数
      printf("Test float: i = %f \r\n", 1.234);	// 浮点数
      printf("Test hex: i = 0x%2x \r\n",100);	// 16进制
	  printf("中文测试 \r\n");					// 中文
	  HAL_Delay(2000);
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
  }

效果验证
和上面的一样。
每隔两秒,发送数据。
PS:在其他文件中使用printf会有警告 unction “printf” declared implicitly 出现,不影响使用。如果不想看到警告,可以在调用的文件中添加#include <stdio.h>头文件即可。

7. 进阶——接收不定长数据

7.1. 串口中断函数、回调函数的补充明⭐⭐⭐

串口中断函数
每接收到一个字节,就会进入一次串口中断函数。如果一个数据帧发送了多个字节,就会连续进入多次中断处理函数。

比如:通过串口助手给单片机发送"0123456789"(不要勾选发送新行),则单片机会连续进入10次void USART1_IRQHandler(void)中断处理函数。

接收完成回调函数
每接收完成一次,会执行一次接收完成回调函数。

前面中断章节中,说了接收完毕后会执行接收完成回调函数void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart),但是没有具体说明,什么时候才算接收完成。

可以看下面这行代码,显然,这里是想接收10个字节,并把它放到rec_buf数组中。那么,接收够10个字节,就表示接收完成了。

// 开启串口接收中断
HAL_UART_Receive_IT(&huart1, (uint8_t *)rec_buf, 10);

那如果,我只发送5个字节呢?那就只会进入5次中断,不会执行接收完成回调函数,当再次发送5个字节的时候,就会在执行第5次中断的时候,执行接收完成回调函数。
再如果,发送8个字符呢?那就只会进入8次中断,不会执行接收完成回调函数,当再次发送8个字节的时候,会在执行2次中断并执行回调函数,然后再次执行一次中断函数就卡死了…

示例
串口配置和中断章节的配置一样:打开串口,勾选中断即可。
main.c

char rec_buf[10] = {
    
    0};
volatile uint8_t num = 0;	 	 // 记录进入中断的次数
volatile uint8_t numback = 0;	 // 记录进入回调的次数

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    
    
	// 判断是不是串口1 中断
	if(huart->Instance == USART1)
	{
    
    
		printf("已经进入了 %d 次中断函数了 \r\n", num);
		printf("第 %d 次进入回调 \r\n", ++numback);
		// 重新开启接收
		HAL_UART_Receive_IT(&huart1, (uint8_t *)rec_buf, 10);
	}
}

int main(void)
{
    
    
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();

	// 开启串口接收中断	
	HAL_UART_Receive_IT(&huart1, (uint8_t *)rec_buf, 10);
  while (1)
  {
    
    
		printf("已经进入了 %d 次中断函数了 \r\n", num);
		HAL_Delay(1000);
  }
}

stm32f1xx_it.c

extern volatile uint8_t num;

void USART1_IRQHandler(void)
{
    
    
	num++; // 记录进入中断的次数
  HAL_UART_IRQHandler(&huart1);
}

发送“1234567890”10个字符(10个字节),可以看到,会进入10次中断,在最后一次中断时,刚好接收够10个字节,也就执行了回调函数。
在这里插入图片描述
而当发送“12345”5个字符(5个字节)时,进入了5次中断,但是并没有接收完成,如果再次发送5个字符,则再进入5次中断,再第5次中断时接收完成,执行了回调函数。
在这里插入图片描述
而如果发送“12345678”8个字符(8个字节)时,进入了8次中断,但是并没有接收完成,如果再次发送8个字符,第二次中断会执行回调函数,并再次进入中断,之后再点发送就没用了。。。
在这里插入图片描述
总之,前面的三种方案,只能处理数据长度比较固定的数据。

7.1. DMA+空闲中断

先说要一下串口的中断。主要说两个串口接收中断串口空闲中断

  • 串口接收中断(UART_IT_RXNE):一帧数据是表示一个或多个字节组成的有含义的字符串,比如前面案例中发送的一个字符串。在一帧数据发送过程中,每接收到一个字节,就会进入一次到串口中断函数。如果满足接收完成的条件,在中断函数中还会执行了发送完成的回调函数。
  • 串口空闲中断(UART_IT_IDLE):上面发送一个字符串(一帧数据)可能包含多个字节,会不断进入接收中断函数,因此发送两个字节之间的时间很短,串口不能认为是空闲的。但是整个字符串(一帧数据)发送完成之后,串口在可以发送一个字节的时间内没有再次发送数据,此时,就会发生空闲中断。

总结:

接收中断 空闲中断
处理函数 USARTx_IRQHandler() USARTx_IRQHandler()
回调函数 HAL_UART_RxCpltCallback() HAL库没有提供
USART状态寄存器中的位 UART_IT_RXNE UART_IT_IDLE
触发条件 接受到一个字节数据触发一次 接受完一帧数据又过了一个字节时间没有接收到数据

在上面的程序中,当DMA串口接收开始后,DMA通道会不断的将发送来的数据转移到内存,也就是说接收程序一直在运行,那该如何判断串口接收是否完成从而及时关闭DMA通道?如何知道接收到数据的长度,以便于接收不定长的呢?答案便是使用串口空闲中断。

解决办法

  1. 串口收到数据,DMA不断传输数据到内存。
  2. 一帧数据发送完毕,串口暂时空闲,触发串口空闲中断。
  3. 在中断服务函数中,根据空闲中断的标志位判断是否发生空闲中断,可以计算刚才收到了多少个字节的数据。
  4. 存储接收到的数据,清除标志位,开始下一帧接收。

代码(配置就是DMA模式的配置)
main.c

/* USER CODE BEGIN PV */
// 自己定义两个变量
uint8_t rx_buffer[100];   			// 接收数据的数组
volatile uint8_t rx_len = 0; 		// 接收数据的长度
/* USER CODE END PV */

/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);

/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
    
    
  /* Reset of all peripherals, Initializes the Flash interface and the Systick. */
  HAL_Init();

  /* Configure the system clock */
  SystemClock_Config();

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();

  /* USER CODE BEGIN WHILE */
	  //开启空闲中断
	  __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);
	  // 开启DMA接收数据通道,将串口数据存到buf数组。
	  HAL_UART_Receive_DMA(&huart1, (uint8_t *)rx_buffer, 100);
  while (1)
  {
    
    
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

stm32f1xx_it.c

/* USER CODE BEGIN PV */

// 定义在main.c中,在此处声明
extern uint8_t rx_buffer[100];   			// 接收数据的数组
extern volatile uint8_t rx_len; 			// 接收数据的长度

/* USER CODE END PV */

/**
  * @brief This function handles USART1 global interrupt.
  */
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 */

	// 获取IDLE状态
	uint8_t tmp_flag = __HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE); 
	if((tmp_flag != RESET))	// 判断接收是否结束
	{
    
     
		__HAL_UART_CLEAR_IDLEFLAG(&huart1);	// 清除空闲中断标志
		
		HAL_UART_DMAStop(&huart1); 			// 停止DMA通道
		// 查询DMA剩余传输数据个数
		uint8_t temp = __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);
		//	最大接收长度 - 剩余长度 = 已发送长度	
		rx_len = 100 - temp;
		/* === 在下面写自己的处理逻辑(注意这里是中断,尽量简短) === */ 
		
		// 发送接收到的数据
		HAL_UART_Transmit_DMA(&huart1, rx_buffer, rx_len);
		
		/* === 在上面写自己的处理逻辑(注意这里是中断,尽量简短) === */ 
		// 重新开启DMA
		HAL_UART_Receive_DMA(&huart1,rx_buffer,100);
	}

  /* USER CODE END USART1_IRQn 1 */
}

效果验证
不管给开发板发送多长的数据,都会重新发送回来。
在这里插入图片描述

7.2. 中断+环形缓冲区

代码有点长,代码链接在主页,代码有详细注释和使用说明。

猜你喜欢

转载自blog.csdn.net/weixin_46253745/article/details/127807452