STM32—HAL库中断/DMA控制和完成串口通信

一、解决的问题

     1、安装 stm32CubeMX,配合Keil,使用HAL库(或标准库)方式,设置USART1 波特率为115200,1位停止位,无校验位,完成下列任务:

1)STM32系统给上位机(win10)连续发送“hello windows!”。win10采用“串口助手”工具接收。

2)在完成以上任务基础,继续扩展功能:当上位机给stm32发送一个字符“#”后,stm32暂停发送“hello windows!”;发送一个字符“*”后,stm32继续发送;

      2、在没有示波器条件下,可以使用Keil的软件仿真逻辑分析仪功能观察串口输出波形,并分析时序状态正确与否,计算波特率实际为多少。

     3. 采用串口中断方式重做上面任务二(2)的串口通信实验。STM32采用串口DMA方式,用115200bps或更高速率向上位机连续发送数据。

二、串口通讯协议和RS-232的介绍以及USB/TTL转232模块的工作原理

1.串口协议和RS-232标准:

(1)串口协议:

       串口通讯(Serial Communication)是一种设备间非常常用的串行通讯方式,因为它简单、便捷,因此大部分电子设备都支持该通讯方式,电子工程师在调试设备时也经常使用该通 讯方式输出调试信息。
       在计算机科学里,大部分复杂的问题都可以通过分层来简化。如芯片被分为内核层和片上外设;STM32标准库则是在寄存器与用户代码之间的软件层。对于通讯协议,我们也以分层的方式来理解,最基本的是把它分为物理层和协议层。

名称 组成作用
物理层 具有机械、电子功能部分的特性,确保原始数据在物理媒体的传输
协议层 规定通讯逻辑,统一收发双方的数据打包、解包标准。

(2)RS-232标准:

   RS-232 标准主要规定了信号的用途通讯接口以及信号的电平标准。

      在上面的通讯方式中,两个通讯设备的“DB9接口”之间通过串口信号线建立起连接,串口信号线中使用“RS-232标准”传输数据信号。由于RS-232电平标准的信号不能直接被控制器直接识别,所以这些信号会经过一个“电平转换芯片”转换成控制器能识别的“TTL校准”的电平信号,才能实现通讯。

2、RS-232电平与TTL电平的区别

根据通讯使用的电平标准不同,串口通讯可分为 TTL标准和 RS-232标准:

标准名称 逻辑1 逻辑0
TLL 2.4V~5V 0~0.5V
RS-232 -15V~3V +3V~+15V

从表格中不难看出,两种标准划分的逻辑电压不同。在电子电路中常使用 TTL 的电平标准,理想状态下,使用 5V 表示二进制逻辑 1,使用 0V 表示逻辑 0;而为了增加串口通讯的远距离传输及抗干扰能力,它使用-15V表示逻辑 1,+15V 表示逻辑 0。

 下图为用RS232与TTL电平校准表示同一个信号时的对比:

 3、USB/TTL转232“模块(CH340芯片为例)

(1)基本原理:

 USB转串口即实现计算机USB接口到物理串口之间的转换。可以为没有串口的计算机或其他USB主机增加串口,使用USB转串口设备等于将传统的串口设备变成了即插即用的USB设备。

      USB主机检测到USB转串口设备插入后,首先会对设备复位,然后开始USB枚举过程。USB枚举时过程会获取设备描述符、配置描述符、接口描述符等。描述符中会包含USB设备的厂商ID,设备ID和Class类别等信息。操作系统会根据该信息为设备匹配相应的USB设备驱动。

      USB虚拟串口的实现在系统上依赖于USB转串口驱动,一般由厂家直接提供,也可以使用操作系统自带的CDC类串口驱动等。驱动主要分为2个功能,其一注册USB设备驱动,完成对USB设备的控制与数据通讯,其二注册串口驱动,为串口应用层提供相应的实现方法。

串口收发对应的驱动数据流向一览表:

发送or接收 数据流向
串口发送 串口应用发送数据→USB串口驱动获取数据→驱动将数据经过USB通道发送给USB串口设备→USB串口设备接收到数据通过串口发送
串口接收 USB串口设备接收串口数据→将串口数据经过USB打包后上传给USB主机→USB串口驱动获取到通过USB上传的串口数据→驱动将数据保存在串口缓冲区提供给串口应用读取

(2)CH340模块介绍:

CH340电路与实物图:

TXD:发送端,一般表示为自己的发送端,正常通信必须接另一个设备的RXD。

RXD:接收端,一般表示为自己的接收端,正常通信必须接另一个设备的TXD。

正常通信的时候本身的TXD永远接设备的RXD。

 USB转TTL串口模块与单片机连接电路图如下所示:

 三、搭建STM32开发环境(HAL库环境)

参考我的博客:STM32使用HAL库点亮流水灯,并使用proteus仿真

 四、利用HAL库新建一个工程

(1)打开STM32CubeMX,在主界面点击:ACCESS TO MCU SELECTOR:

(2)选择的单片机型号以及点击开始工程项目: 

(3)配置GPIO:PA0。如果仅仅是完成串口通信的话,这一步可以跳过。但是根据实验要求,为了区分串口通信的开启与关闭,要使用一个LED灯来显示。当串口通信开启(STM32向电脑发送信息)的时候,LED灯亮,当串口通信关闭(STM32停止向电脑发送消息)的时候,LED灯灭。

(4)配置USART1,我们使用USART1进行数据传输。在这个界面按下图进行配置。我们对USART1的配置要做的只有两件事:一是选择串口工作模式为异步,二是开启USART1全局中断

(5)进入Project Manager(工程管理),进行工程设置点击生成工程与代码:

注意:路径不能包含中文和空格,不然生成的工程文件无法在Keil中打开;

五、完善keil5工程

(1)本工程中几个函数简介:

HAL_UART_Receive_IT:

HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_int *data, uint16_t Size)
 
/*
    huart:使用哪个串口进行通信
    data: 一个地址,用于保存接受到的数据
    Size: 接收的数据个数
*/

 

 在调用此函数后,程序会将对应串口的接收中断开启,当我们向单片机发送数据时会触发这个中断。在触发这个中断后,程序会接收数据到你传入的地址中,会读取Size个数据。读取完成后,关闭接收中断使能。

      由于程序在接收完数据后会关闭接收中断。因此这个函数我们要写在main的死循环中,保证接收中断可以一直开启。

HAL_UART_Transmit_IT:

HAL_UART_Transmit_IT(UART_HandleTypeDef *huart, uint8_int *data, uint16_t Size)
 
/*
    huart:使用哪个串口进行通信
    data: 一个地址,里面是要发送的数据通常是数组
    Size: 发送的数据个数
*/

 使用这个函数开启发送中断,发送寄存器为空时触发中断,将要发送的数据送入发送寄存器并发送。发送完成后关闭中断。在此实验中,我们把它当做普通的发送函数即可。

HAL_GPIO_WritePin:

HAL_GPIO_WritePin(GPIOX,GPIO_PIN_X,GPIO_PIN_STATUS)
/*
    GPIOX:目标GPIO的组号
    GPIO_PIN_X: 目标GPIO的引脚编号 
    GPIO_PIN_STATUS: 引脚状态
*/

 

使用这个函数修改GPIO_ODR寄存器,将非复用输出的GPIO引脚输出电平设置成自己想要的。 

 HAL_Delay(uint ms):

HAL_Delay(uint ms)

 (2)编写代码思路:

main函数中用一个uint8类型的变量,接收发过来的字符(*/#),默认为*
进入死循环,调用HAL_UART_Receive_IT使能接收中断
如果电脑发送了字符,接收变量的值会变
如果接收变量为*,led阴极置低电平,led亮,向电脑发数据“hello windows”
如果接收变量为#,led阴极置高电平,led灭,不向电脑发送数据

 (3)完善keil5工程代码:

首先,点击刚刚生成的keil5工程文件,双击main.c文件,然后再main.c中找到图示框住的函数, 接着右击此函数,进入其定义的地方处:

(2)将图中框住的部分改为SET即可:此步骤是将这个GPIO口设置为高电平,初始时不亮

(3)回到main.c文件中,在main函数里把while(1)那一块替换成如下代码:

uint8_t rcData = '*';
      while (1)
      {
      //接收中断使能
        HAL_UART_Receive_IT(&huart1,&rcData,1);
        if(rcData == '#')
        {//如果接收#
            HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_SET);
        }
        else if(rcData == '*')
        {//如果接收*                 
            HAL_GPIO_WritePin(GPIOA,GPIO_PIN_0,GPIO_PIN_RESET);
            uint8_t hello[20]="hello world\n";
            HAL_UART_Transmit_IT(&huart1,hello,20);
            HAL_Delay(500);
        }
      }

六、电路连接与烧录运行

1、电路连接:

USB转TTL与STM32的连接,参考下图:

PA0——白灯;

连接好的实物图如下所示:

2、 USB转TTL环境配置:

需要在电脑上安装CH340驱动(USB串口驱动)或者CH341驱动(USB串口驱动):

在网上下载好所需的CH340驱动(USB串口驱动)或者CH341驱动(USB串口驱动)

 3、下载烧录软件与串口通信软件:

烧录软件推荐使用:FLYMCU,如下图所示图样:

串口通信软件推荐使用:XCOM,如下图所示图样:

 

 4、keil5工程里面对于USB转TTL的配置:

5、编译生产hex文件,用于后面的烧录步骤:

 6、烧录:

(1)将USB转TTL插上电脑的USB接口上去,打开刚刚下载好的FLYMCU软件,按照下图所示进行相关配置,其中的第二步就是选择刚刚上一步编译生成的hex文件:

(2)改变STM32最小系统板子的跳线帽连接方式:BOOTO的跳线帽连接方式由0——>1:

 

(3)点击FLYMCU的开始编程(P),接着马上点击STM32最小系统板子的复位键即可完成烧录:

第一步:

第二步:

烧录成功示意图:

烧录完成之后还需要下面的关键一步,把STM32最小系统板子的跳线帽连接方式还原:BOOTO的跳线帽连接方式由1——>0:

7、配置XCOM,打开XCOM软件,按下图所示进行配置:

 8、运行结果演示:

打开串口,并且同时点击STM32最小系统板子的复位键即可开始运行:

注:输入“*”:让STM32单片机继续向电脑发送信息;

输入“#”:让STM32单片机停止向电脑发送消息;

七、仿真调试

1、进入keil5仿真:

(1)点击第一步,Target界面中,选择跟正确的晶振大小,使用8MHz的外部晶振:

(2)接着进行Debug页的设置:  

(3)点击图示圈住的地方进入仿真调试界面:

(4)选择逻辑分析仪:

(5)点击Setup设置添加要进行观察的引脚:

 添加引脚信息:添加引脚信息时候,PA脚输入:PORTA,PB脚输入PORTB,PC脚输入PORTC;接着输入".",接着在“.”后面输入对应引脚号,最后回车即可;例如:我这里要输入PA0这个引脚的信息:添加一个引脚,添加一个引脚信息:PORTA.0,最后回车完成添加

 接着对引脚的配置信息进行修改,如下图所示: 

我这里选择的引脚仿真波形对应的颜色为:PA0——红色;

2、开始仿真:

(1)点击图示圈住的部分,进行仿真 

(2)仿真结果:

3、仿真结果分析:

     下图中的红色波形线表示开始仿真之后PA0一直处于低电平状态,LED黄灯亮,也表示STM32一直在向电脑发送消息。直到电脑发送“#”才会使得PA0上升到高电平状态,LED黄灯灭,STM32停止向电脑发送信息:

因为PA0被我们设置为用来完成这个功能:当串口通信开启(STM32向电脑发送信息)的时候,LED灯亮,当串口通信关闭(STM32停止向电脑发送消息)的时候,LED灯灭。

      所以由刚刚的分析与图示情况来看,串口PA0的输出波形正确,完全符合本人板子设置与仿真预期的效果,而且通过观察发现PA0时序状态也是正常的

八、利用HAL库新建一个DMA控制串口通信的工程

同四,直到添加信道:

九、完善通过DMA方式控制串口通信的keil5工程

1、 本工程中的几个函数简介:

HAL_UART_Transmit_DMA():串口DMA模式发送

 HAL_UART_Transmit_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
功能:串口通过DMA发送指定长度的数据。
 
参数:
 
UART_HandleTypeDef *huart UATR的别名 如 : UART_HandleTypeDef huart1; 别名就是huart1
*pData 需要发送的数据
Size 发送的字节数

 本文运用举例:

HAL_UART_Transmit_DMA(&huart1, (uint8_t *)message, sizeof(message));

HAL_UART_Receive_DMA():串口DMA模式接收

HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
功能:串口通过DMA接受指定长度的数据。
 
参数:
 
UART_HandleTypeDef *huart UATR的别名 如 : UART_HandleTypeDef huart1; 别名就是huart1
*pData 需要存放接收数据的数组
Size 接受的字节数

 本文运用举例:

 HAL_UART_Receive_DMA(&huart1,(uint8_t*)rx_buf,5);//设置DMA接收到的数据存放在rx_buf中

 2、代码编写思路

main函数外用一个uint8_t类型的数组:rcData,接收发过来的连续字符串,默认为:start
main函数中进入死循环,调用HAL_UART_Receive_DMA()
如果电脑发送了字符串,接收变量flag的值会变
如果接收变量为:start,led阴极置低电平,向电脑发数据“hello windows!”
如果接收变量为:stop!,led阴极置高电平,不向电脑发送数据

 3、完善keil5工程代码:

(1)在main.c文件中,详细编写主要代码:

         重新定义串口接收完成回调函数:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    //当输入的指令为“stop!"时,发送提示并改变flag=0
    if(strEqual(rx_buf,"stop!"))
    {
        flag=0;
    }
    
    //当输入的指令为"start"时,发送提示并改变flag=1
    else if(strEqual(rx_buf,"start"))
    {
        flag=1;
    }
    HAL_UART_Receive_DMA(&huart1,(uint8_t*)rx_buf,5);
}

(2:用字符串进行判断,因此接受变量用数组来储存,Size要改成数组的大小为:6。单片机收到串口助手发的信息后,与"stop2!"和"start"进行匹配。根据匹配结果执行不同的代码。“stop!”,"start"与收到的数据都用uint8_t数组保存。为执行匹配操作,我们需要写一个函数对每一位进行判断与匹配:

int strEqual(char rcData[6],char rcData2[6])
    {
    for(uint8_t i = 0 ; i < 6 ; i++){
        if (rcData[i] != rcData2[i]) return 0;
    }
    return 1;
}

 (3:在main函数前面添加上如下代码(接收信息储存数组,接收信息匹配处理函数,信息标志flag):

uint8_t flag=1;
uint8_t rx_buf[6];//接收串口数据存放的数组
 
int strEqual(char rcData[6],char rcData2[6])
    {
    for(uint8_t i = 0 ; i < 6 ; i++){
        if (rcData[i] != rcData2[i]) return 0;
    }
    return 1;
}

(4:main里面的while(1)替换为如下信息接收与发送处理代码:

      if(flag==1)
      {
        HAL_UART_Transmit_DMA(&huart1, (uint8_t *)message, sizeof(message));
        HAL_Delay(600);
      }

 5:在main函数下面重写串口接收完成回调函数:

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    //当输入的指令为“stop!"时,发送提示并改变flag=0
    if(strEqual(rx_buf,"stop!"))
    {
        flag=0;
    }
    
    //当输入的指令为"start"时,发送提示并改变flag=1
    else if(strEqual(rx_buf,"start"))
    {
        flag=1;
    }
    HAL_UART_Receive_DMA(&huart1,(uint8_t*)rx_buf,5);
}

6:完善之后的main.c的全部代码编写如下所示:

 /* USER CODE BEGIN Header */
/**
  ******************************************************************************
  * @file           : main.c
  * @brief          : Main program body
  ******************************************************************************
  * @attention
  *
  * Copyright (c) 2023 STMicroelectronics.
  * All rights reserved.
  *
  * This software is licensed under terms that can be found in the LICENSE file
  * in the root directory of this software component.
  * If no LICENSE file comes with this software, it is provided AS-IS.
  *
  ******************************************************************************
  */
/* USER CODE END Header */
/* Includes ——————————————————————*/
#include "main.h"
#include "dma.h"
#include "usart.h"
#include "gpio.h"
 
/* Private includes ———————————————————-*/
/* USER CODE BEGIN Includes */
 
/* USER CODE END Includes */
 
/* Private typedef ———————————————————–*/
/* USER CODE BEGIN PTD */
 
/* USER CODE END PTD */
 
/* Private define ————————————————————*/
/* USER CODE BEGIN PD */
 
/* USER CODE END PD */
 
/* Private macro ————————————————————-*/
/* USER CODE BEGIN PM */
 
/* USER CODE END PM */
 
/* Private variables ———————————————————*/
 
/* USER CODE BEGIN PV */
 
/* USER CODE END PV */
 
/* Private function prototypes ———————————————–*/
void SystemClock_Config(void);
/* USER CODE BEGIN PFP */
 
/* USER CODE END PFP */
 
/* Private user code ———————————————————*/
/* USER CODE BEGIN 0 */
 
/* USER CODE END 0 */
uint8_t flag=1;
uint8_t rx_buf[6];//接收串口数据存放的数组
 
int strEqual(char rcData[6],char rcData2[6])
    {
    for(uint8_t i = 0 ; i < 6 ; i++){
        if (rcData[i] != rcData2[i]) return 0;
    }
    return 1;
}
 
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    //当输入的指令为“stop!"时,发送提示并改变flag=0
    if(strEqual(rx_buf,"stop!"))
    {
        flag=0;
    }
    
    //当输入的指令为"start"时,发送提示并改变flag=1
    else if(strEqual(rx_buf,"start"))
    {
        flag=1;
    }
    HAL_UART_Receive_DMA(&huart1,(uint8_t*)rx_buf,5);
}
/**
  * @brief  The application entry point.
  * @retval int
  */
int main(void)
{
  HAL_Init();
  uint8_t message[] = "hello windows!\n";  //定义数据发送数组
  SystemClock_Config();
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();
  HAL_UART_Receive_DMA(&huart1,(uint8_t*)rx_buf,5);//设置DMA接收到的数据存放在rx_buf中
  while (1)
  {
      if(flag==1)
      {
        HAL_UART_Transmit_DMA(&huart1, (uint8_t *)message, sizeof(message));
        HAL_Delay(600);
      }
  }
}
/**
  * @brief System Clock Configuration
  * @retval None
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
  /** Initializes the RCC Oscillators according to the specified parameters
  * in the RCC_OscInitTypeDef structure.
  */
  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSI;
  RCC_OscInitStruct.HSIState = RCC_HSI_ON;
  RCC_OscInitStruct.HSICalibrationValue = RCC_HSICALIBRATION_DEFAULT;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_NONE;
  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }
  /** Initializes the CPU, AHB and APB buses clocks
  */
  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
                              |RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_HSI;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV1;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;
  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_0) != HAL_OK)
  {
    Error_Handler();
  }
}
/* USER CODE BEGIN 4 */
/* USER CODE END 4 */
/**
  * @brief  This function is executed in case of error occurrence.
  * @retval None
  */
void Error_Handler(void)
{
  /* USER CODE BEGIN Error_Handler_Debug */
  /* User can add his own implementation to report the HAL error return state */
  __disable_irq();
  while (1)
  {
  }
  /* USER CODE END Error_Handler_Debug */
}
#ifdef  USE_FULL_ASSERT
/**
  * @brief  Reports the name of the source file and the source line number
  *         where the assert_param error has occurred.
  * @param  file: pointer to the source file name
  * @param  line: assert_param error line source number
  * @retval None
  */
void assert_failed(uint8_t *file, uint32_t line)
{
  /* USER CODE BEGIN 6 */
  /* User can add his own implementation to report the file name and line number,
     ex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
  /* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */

 十、 基于DMA方式控制串口通信的电路连接与烧录运行

1、电路的连接、软件的下载以及环境的配置:与中断方式一致!

2、运行结果演示:

打开串口,并且同时点击STM32最小系统板子的复位键即可开始运行:

注:输入“start”:让STM32单片机继续向电脑发送信息;

输入“stop!”:让STM32单片机停止向电脑发送消息 ;

十一、基于DMA控制串口通信的keil5仿真调试

1、进入keil5仿真与开始仿真:

参考本文前面第七大点:基于中断控制串口通信的keil5仿真调试!里面有详细介绍了关于如何进入keil5仿真、环境设置和详细操作等等!!!

2、仿真结果演示:

十二、总结

实验心得

     通过本次实验,我深入理解了串口通信的原理以及波特率对数据传输效率的影响。实验中,理论计算的传输时间与实际结果存在一定差异,主要原因在于协议开销和误码重传等实际因素的影响。这让我认识到,在实际工程中需要综合考虑传输效率和数据完整性,合理选择波特率和通信参数。

    实验还验证了接地线(GND)的重要性。虽然在短距离通信中偶尔可以不接地线,但长期传输容易因信号漂移导致数据错误。这进一步强调了统一电位参考的重要性,地线的稳定性对通信可靠性至关重要,是实验设计和设备连接中必须重视的环节。

     此外,本次实验让我掌握了串口工具的使用和测试方法,提高了对数据传输效率与误差的分析能力。结合理论与实践,通过数据对比和实验验证,积累了宝贵经验。

十三、参考资料

1.STM32—HAL库中断/DMA控制和完成串口通信

stm32使用hal库中断控制串口通信

作者:Elon Josh

物联沃分享整理
物联沃-IOTWORD物联网 » STM32—HAL库中断/DMA控制和完成串口通信

发表回复