基于HAL库的串口中断通信


前言

在实际学习stm32中,串口通讯不仅仅时一种设备与设备通信的外设,更是在调试过程中的一种重要手段,在本文当中,将着重介绍如何使用串口中断并进行单片机与上位机的通信。


一、串口基础知识

注:以下均参考正点原子的手册

1.串口功能概述

接口通过三个引脚与其他设备连接在一起(见图248)。任何USART双向通信至少需要两个脚:接
收数据输入(RX)和发送数据输出(TX)。
RX:接收数据串行输。通过过采样技术来区别数据和噪音,从而恢复数据。
TX:发送数据输出。当发送器被禁止时,输出引脚恢复到它的I/O端口配置。当发送器被激活,
并且不发送数据时,TX引脚处于高电平。在单线和智能卡模式里,此I/O口被同时用于数据的发
送和接收。
● 总线在发送或接收前应处于空闲状态
● 一个起始位
● 一个数据字(8或9位),最低有效位在前
● 0.5,1.5,2个的停止位,由此表明数据帧的结束
● 使用分数波特率发生器 —— 12位整数和4位小数的表示方法。
● 一个状态寄存器(USART_SR)
● 数据寄存器(USART_DR)
● 一个波特率寄存器(USART_BRR),12位的整数和4位小数
● 一个智能卡模式下的保护时间寄存器(USART_GTPR)
25.6
517/754
关于以上寄存器中每个位的具体定义,请参考寄存器描述第 节:USART寄存器描述。
在同步模式中需要下列引脚:
● CK:发送器时钟输出。此引脚输出用于同步传输的 时钟, (在Start位和Stop位上没有时钟
脉冲,软件可选地,可以在最后一个数据位送出一个时钟脉冲)。数据可以在RX上同步被接
收。这可以用来控制带有移位寄存器的外部设备(例如LCD驱动器)。时钟相位和极性都是软
件可编程的。在智能卡模式里,CK可以为智能卡提供时钟。
在IrDA模式里需要下列引脚:
● IrDA_RDI: IrDA模式下的数据输入。
● IrDA_TDO: IrDA模式下的数据输出。
下列引脚在硬件流控模式中需要:
● nCTS: 清除发送,若是高电平,在当前数据传输结束时阻断下一次的数据发送

2.串口寄存器介绍

1,串口时钟使能。串口作为 STM32 的一个外设,其时钟由外设时钟使能寄存器控制,这
里我们使用的串口1是在APB2ENR寄存器的第14位。APB2ENR寄存器在之前已经介绍过了,
这里不再介绍。只是说明一点,就是除了串口 1 的时钟使能在 APB2ENR 寄存器,其他串口的
时钟使能位都在 APB1ENR 寄存器,而 APB2(72M)的频率一般是 APB1(36M)的一倍。
2,串口复位。当外设出现异常的时候可以通过复位寄存器里面的对应位设置,实现该外设
的复位,然后重新配置这个外设达到让其重新工作的目的。一般在系统刚开始配置外设的时候,
都会先执行复位该外设的操作。串口 1 的复位是通过配置 APB2RSTR 寄存器的第 14 位来实现
的。APB2RSTR 寄存器的各位描述如下图所示:

在这里插入图片描述

3,串口波特率设置。每个串口都有一个自己独立的波特率寄存器USART_BRR,通过设置该寄存器就可以达到配置不同波特率的目的
4,串口控制。STM32的每个串口都有3个控制寄存器USART_CR1~3,串口的很多配置都是通过这3个寄存器来设置的。这里我们只要用到USART_CR1就可以实现我们的功能了,该寄存器的各位描述如下图所示:

在这里插入图片描述

该寄存器的高18位没有用到,低14位用于串口的功能设置。UE为串口使能位,通过该位置1,以使能串口。M为字长选择位,当该位为0的时候设置串口为8个字长外加n个停止位,停止位的个数(n)是根据USART_CR2的[13:12]位设置来决定的,默认为0。PCE为校验使能位,设置为0,则禁止校验,否则使能校验。PS为校验位选择,设置为0则为偶校验,否则为奇校验。TXIE为发送缓冲区空中断使能位,设置该位为1,当USART_SR中的TXE位为1时,将产生串口中断。TCIE为发送完成中断使能位,设置该位为1,当USART_SR中的TC 位为1时,将产生串口中断。RXNEIE为接收缓冲区非空中断使能,设置该位为1,当USART_SR 中的ORE或者RXNE位为1时,将产生串口中断。TE为发送使能位,设置为1,将开启串口的发送功能。RE为接收使能位,用法同TE。
5,数据发送与接收。STM32的发送与接收是通过数据寄存器USART_DR来实现的,这是一个双寄存器,包含了TDR和RDR。当向该寄存器写数据的时候,串口就会自动发送,当收到数据的时候,也是存在该寄存器内。该寄存器的各位描述如下图所示:

请添加图片描述

可以看出,虽然是一个32位寄存器,但是只用了低9位(DR[8:0]),其他都是保留。
DR[8:0]为串口数据,包含了发送或接收的数据。由于它是由两个寄存器组成的,一个给发送用(TDR),一个给接收用(RDR),该寄存器兼具读和写的功能。TDR寄存器提供了内部总线和输出移位寄存器之间的并行接口。RDR寄存器提供了输入移位寄存器和内部总线之间的并行接口。
当使能校验位(USART_CR1中PCE位被置位)进行发送时,写到MSB的值(根据数据的长度不同,MSB是第7位或者第8位)会被后来的校验位取代。
当使能校验位进行接收时,读到的MSB位是接收到的校验位。
6,串口状态。串口的状态可以通过状态寄存器USART_SR读取。USART_SR的各位描述如下图所示:

请添加图片描述

这里我们关注一下两个位,第 5、6 位 RXNE 和 TC。
RXNE(读数据寄存器非空),当该位被置 1 的时候,就是提示已经有数据被接收到了,并
且可以读出来了。这时候我们要做的就是尽快去读取 USART_DR,通过读 USART_DR 可以将
该位清零,也可以向该位写 0,直接清除。
TC(发送完成),当该位被置位的时候,表示 USART_DR 内的数据已经被发送完成了。如
果设置了这个位的中断,则会产生中断。该位也有两种清零方式:1)读 USART_SR,写
USART_DR。2)直接向该位写 0。
通过以上一些寄存器的操作外加一下 IO 口的配置,我们就可以达到串口最基本的配置了,
关于串口更详细的介绍,请参考《STM32 参考手册》第 516 页至 548 页,通用同步异步收发器
一章。
串口设置的一般步骤可以总结为如下几个步骤:

  1. 串口时钟使能,GPIO 时钟使能。
  2. 设置引脚复用器映射:调用 GPIO_PinAFConfig 函数。
  3. GPIO 初始化设置:要设置模式为复用功能。
  4. 串口参数初始化:设置波特率,字长,奇偶校验等参数。
  5. 开启中断并且初始化 NVIC,使能中断(如果需要开启中断才需要这个步骤)。
  6. 使能串口。
  7. 编写中断处理函数:函数名格式为 USARTxIRQHandler(x 对应串口号)。
    接下来我们将着重讲解使用 HAL 库实现串口配置和使用的方法。在 HAL 库中,串口相关的函
    数和定义主要在文件 stm32f1xx_hal_uart.c 和 stm32f1xx_hal_uart.h 中。接下来我们看看 HAL 库
    提供的串口相关操作函数。

二、基于HAL库的串口配置

这里将不赘诉在Cubemx上的配置,以下代码是由Cubemx自动生成的串口初始化代码:

UART_HandleTypeDef huart1;
UART_HandleTypeDef huart2;

/* USART1 init function */

void MX_USART1_UART_Init(void)
{

  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;
  huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart1.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }

}
/* USART2 init function */

void MX_USART2_UART_Init(void)
{

  huart2.Instance = USART2;
  huart2.Init.BaudRate = 9600;
  huart2.Init.WordLength = UART_WORDLENGTH_8B;
  huart2.Init.StopBits = UART_STOPBITS_1;
  huart2.Init.Parity = UART_PARITY_NONE;
  huart2.Init.Mode = UART_MODE_TX_RX;
  huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE;
  huart2.Init.OverSampling = UART_OVERSAMPLING_16;
  if (HAL_UART_Init(&huart2) != HAL_OK)
  {
    Error_Handler();
  }

}

void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};
  if(uartHandle->Instance==USART1)
  {
  /* USER CODE BEGIN USART1_MspInit 0 */

  /* USER CODE END USART1_MspInit 0 */
    /* USART1 clock enable */
    __HAL_RCC_USART1_CLK_ENABLE();
  
    __HAL_RCC_GPIOA_CLK_ENABLE();
    /**USART1 GPIO Configuration    
    PA9     ------> USART1_TX
    PA10     ------> USART1_RX 
    */
    GPIO_InitStruct.Pin = GPIO_PIN_9;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = GPIO_PIN_10;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    /* USART1 interrupt Init */
    HAL_NVIC_SetPriority(USART1_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(USART1_IRQn);
  /* USER CODE BEGIN USART1_MspInit 1 */

  /* USER CODE END USART1_MspInit 1 */
  }
  else if(uartHandle->Instance==USART2)
  {
  /* USER CODE BEGIN USART2_MspInit 0 */

  /* USER CODE END USART2_MspInit 0 */
    /* USART2 clock enable */
    __HAL_RCC_USART2_CLK_ENABLE();
  
    __HAL_RCC_GPIOA_CLK_ENABLE();
    /**USART2 GPIO Configuration    
    PA2     ------> USART2_TX
    PA3     ------> USART2_RX 
    */
    GPIO_InitStruct.Pin = GPIO_PIN_2;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    GPIO_InitStruct.Pin = GPIO_PIN_3;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    /* USART2 interrupt Init */
    HAL_NVIC_SetPriority(USART2_IRQn, 0, 0);
    HAL_NVIC_EnableIRQ(USART2_IRQn);
  /* USER CODE BEGIN USART2_MspInit 1 */

  /* USER CODE END USART2_MspInit 1 */
  }
}

void HAL_UART_MspDeInit(UART_HandleTypeDef* uartHandle)
{

  if(uartHandle->Instance==USART1)
  {
  /* USER CODE BEGIN USART1_MspDeInit 0 */

  /* USER CODE END USART1_MspDeInit 0 */
    /* Peripheral clock disable */
    __HAL_RCC_USART1_CLK_DISABLE();
  
    /**USART1 GPIO Configuration    
    PA9     ------> USART1_TX
    PA10     ------> USART1_RX 
    */
    HAL_GPIO_DeInit(GPIOA, GPIO_PIN_9|GPIO_PIN_10);

    /* USART1 interrupt Deinit */
    HAL_NVIC_DisableIRQ(USART1_IRQn);
  /* USER CODE BEGIN USART1_MspDeInit 1 */

  /* USER CODE END USART1_MspDeInit 1 */
  }
  else if(uartHandle->Instance==USART2)
  {
  /* USER CODE BEGIN USART2_MspDeInit 0 */

  /* USER CODE END USART2_MspDeInit 0 */
    /* Peripheral clock disable */
    __HAL_RCC_USART2_CLK_DISABLE();
  
    /**USART2 GPIO Configuration    
    PA2     ------> USART2_TX
    PA3     ------> USART2_RX 
    */
    HAL_GPIO_DeInit(GPIOA, GPIO_PIN_2|GPIO_PIN_3);

    /* USART2 interrupt Deinit */
    HAL_NVIC_DisableIRQ(USART2_IRQn);
  /* USER CODE BEGIN USART2_MspDeInit 1 */

  /* USER CODE END USART2_MspDeInit 1 */
  }
} 

在实验过程中我们需要用到更为便捷的Printf函数,所以在这里我们需要重定义该函数:

#include "stdio.h"	
#if 1
#pragma import(__use_no_semihosting)             
//标准库需要的支持函数                 
struct __FILE 
{ 
	int handle; 

}; 

FILE __stdout;       
//定义_sys_exit()以避免使用半主机模式    
void _sys_exit(int x) 
{ 
	x = x; 
} 
//重定义fputc函数 
int fputc(int ch, FILE *f)
{      
	while((USART1->SR&0X40)==0);//循环发送,直到发送完毕   
    USART1->DR = (u8) ch;      
	return ch;
}
#endif 

定义一个标志表示我在主函数里需要执行的代码,在串口中断函数中,我们对收到的字节进行判断:

	switch(aRxBuffer)
	{
		case '#':flag=0;break;
		case '*':flag=1;break;
	}

main函数如下:

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_TIM1_Init();
  MX_USART1_UART_Init();
  MX_USART2_UART_Init();
  /* USER CODE BEGIN 2 */
  HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_1);
   HAL_TIM_PWM_Start(&htim1,TIM_CHANNEL_4);
  /* USER CODE END 2 */
HAL_UART_Receive_IT(&huart1,(uint8_t *)&aRxBuffer,1);
HAL_UART_Receive_IT(&huart2,(uint8_t *)&bRxBuffer,1);
motor_ctrl(1);
  /* Infinite loop */
  /* USER CODE BEGIN WHILE */
	
  while (1)
  {
    /*USER CODE END WHILE */
	  if(flag==1)
	  {
		  printf("%s","hello windows");
	  }
    /* USER CODE BEGIN 3 */
  }
  /* USER CODE END 3 */
}

完成上诉操作就可以完成串口的中断收发了,在本文当中当串口收到上位机发出的*时就开始发出hello windows,当收到#时结束发送。

总结

在本问中,了解串口发送规律,学会串口寄存器的配置,学会运用HAL库对串口进行配置,在实践过程中总结以下几点:
1.串口的重定义一定要加入头文件stdio.h。
2.串口配置,收发时一定要注意波特率。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
HAL库提供了一些函数和宏定义来帮助我们配置和使用串口中断,以实现多机通信。下面是一个简单的示例代码,演示如何使用HAL库实现串口中断多机通信: ```c #include "stm32f4xx_hal.h" UART_HandleTypeDef huart2; void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_USART2_UART_Init(void); int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART2_UART_Init(); // 启用串口中断 HAL_UART_Receive_IT(&huart2, (uint8_t *)rx_data, 1); while (1) { // 主循环 } } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART2) { if (rx_data[0] == 0xAA) // 接收到数据头 { // 接收数据 HAL_UART_Receive_IT(&huart2, (uint8_t *)rx_data, 7); } else // 接收到数据 { // 处理数据 // ... // 继续接收数据 HAL_UART_Receive_IT(&huart2, (uint8_t *)rx_data, 1); } } } static void MX_USART2_UART_Init(void) { huart2.Instance = USART2; huart2.Init.BaudRate = 115200; huart2.Init.WordLength = UART_WORDLENGTH_8B; huart2.Init.StopBits = UART_STOPBITS_1; huart2.Init.Parity = UART_PARITY_NONE; huart2.Init.Mode = UART_MODE_TX_RX; huart2.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart2.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart2) != HAL_OK) { Error_Handler(); } } ``` 在上面的代码中,我们启用了USART2的中断接收模式,当接收到数据时,会调用`HAL_UART_RxCpltCallback`函数进行处理。在该函数中,我们可以根据接收到的数据进行相应的处理,并继续启用中断接收模式以接收下一帧数据。例如,当接收到数据头0xAA时,我们启用中断接收模式以接收接下来的7个字节的数据;当接收到数据时,我们可以进行相应的处理,例如打印到终端或者进行数据解析等等。 需要注意的是,在使用多机通信时,需要对数据帧进行设计,例如在数据帧中添加数据头、数据长度、校验位等等,以确保数据的正确性和完整性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值