【STM32】标准库与HAL库对照学习教程八--串口通信详解_hal库串口通信_修成真的博客-程序员资料

技术标签: stm32  STM32  arm  单片机  

一、前言

本篇文章是对单片机串口通信的详讲,串口通信作为STM32单片机的一个重要功能,在程序调试中发挥着重要的作用,本篇从通信基本原理讲解开始,一步一步让您理解并会使用STM32的串口通信,实验程序包括标准库与HAL库,例程为串口中断通信实验,您也可以点击目录跳转到自己想看的内容。

二、准备工作

  • STM32F103开发板(我用的是普中的STM32F103ZE开发板)
  • cubemx软件、keil 5(MDK)
  • USB转串口模块(CH340)

三、通信的基本概念

1、通信方式

通信的方式可以分为多种:

  • 按照数据传送方式可分为串行通信和并行通信
  • 按照通信的数据同步方式,可分为异同通信和同步通信
  • 按照数据的传输方向又可分为单工、半双工和全双工通信

下面我们就来简单介绍这几种通信方式。

2、串行通信与并行通信

(1)串行通信

串行通信是指使用一条数据线,将数据一位一位地依次传输,每一位数据占据一个固定的时间长度。
在这里插入图片描述

  • 优点:传输线少,长距离传送时成本低。
  • 缺点:数据的传送控制比并行通信复杂且速度相对较慢。

(2)并行通信

并行通信通常是将数据字节的各位用多条数据线同时进行传送,通常是8位、16位、32位等数据一起传输。
在这里插入图片描述

  • 优点:控制简单、传输速度快。
  • 缺点:长距离传送时成本高且接收方的各位同时接收存在困难,抗干扰能力差 。

3、异步通信与同步通信

(1)异步通信

异步通信是指通信的发送与接收设备使用各自的时钟控制数据的发送和接收过程。
异步通信是以字符(构成的帧)为单位进行传输,字符与字符之间的间隙(时间间隔)是任意的。
在这里插入图片描述
在这里插入图片描述

优点:不要求收发双方时钟的严格一致,实现容易。
缺点:每个字符要附加2~3位用于起止位,各帧之间还有间隔,因此传输效率不高 。

(2)同步通信

同步通信时要建立发送方时钟对接收方时钟的直接控制使双方达到完全同步。实现方法有外同步和自同步两种。
在这里插入图片描述
优点:由于传输因此传输效率高。
缺点:同步实现困难且开销大。

4、单工、半双工与全双工通信

(1)单工通信

单工是指数据传输仅能沿一个方向,就是一个设备只管发送,一个设备只管接收。
在这里插入图片描述

(2)半双工通信

半双工是指数据传输可以沿两个方向,但需要分时进行。也就是发送的时候不能接收,接收的时候不能发送。
在这里插入图片描述

(3)全双工通信

全双工是指数据可以同时进行双向传输。也就是发送的时候可以接收,接收的时候可以发送。
在这里插入图片描述

5、通信速率

衡量通信性能的一个非常重要的参数就是通信速率,通常以比特率(Bitrate)来表示。
比特率每秒钟传输二进制代码的位数,单位是:位/秒(bps)。
例如:每秒钟传送200个字符,而每个字符格式包含10位(1个起始位、1个停止位、8个数据位),这时的比特率为:10位×200个/秒 = 2000 bps

四、STM32F1的USART介绍

1、串口通信简介

串口通信(Serial Communication),是指外设和计算机之间,通过数据信号线、地线等,按位进行传输数据的一种通信方式,属于串行通信方式
串口是一种接口标准,它规定了接口的电气标准,没有规定接口插件电缆以及使用的协议。

2、USART简介

USART即通用同步异步收发器,它能够灵活地与外部设备进行全双工数据交换。UART即通用异步收发器,它是在USART基础上裁剪掉了同步通信功能。STM32F103ZET6芯片含有3个USART,2个UART外设

3、USART结构框图

在这里插入图片描述

图片在STM32F1xx中文参考手册 通用同步异步收发器章节

结构图重点在框住的部分,通过寄存器USART_SRTXE、TC、RXNE位知道串口收发情况。

4、USART寄存器重点控制位

(1)TXE

TXE位USART_SR寄存器的第七位,为1时,TDR寄存器中的数据已经被转移到移位寄存器,为0时,TDR寄存器中的数据还没有被转移到移位寄存器复位时为1TDR寄存器中有数据时,该位立即为0
在这里插入图片描述
在这里插入图片描述

(2)TC

TC位USART_SR寄存器的第六位,为1时,移位寄存器中的数据发送完成,为0时,移位寄存器还有数据复位时为1需要手动清0,或者进行读操作让其为0
在这里插入图片描述
在这里插入图片描述

(3)RXNE

接收完数据时,该位为1,其他时候为0
在这里插入图片描述
在这里插入图片描述

5、STM32与PC主机通信的方法

串口数据收发线要交叉连接计算机的TXD要对应单片机的RXD计算机的RXD要对应单片机的TXD,并且共GND
如下图:
在这里插入图片描述

五、USB转串口模块

单片机与电脑进行通信需要用到USB转串口模块,因为电脑上没有RXD、TXD引脚,并且电脑还要安装CH340的驱动
市场常见的USB转串口模块
在这里插入图片描述
接上电脑,TX接单片机的RX,RX接单片机的TX,电源与地接到单片机上就可以串口通信了。
电脑上的CH340驱动我放在网盘上了,有需要自己去下载。
https://pan.baidu.com/s/1bO7mpkwjkB19HXvmD0083Q
密码:kpa6

六、标准库使用串口中断通信

一、配置步骤

(1)使能串口时钟及GPIO端口时钟
(2)GPIO端口模式设置,设置串口对应的引脚为复用功能
(3)初始化串口参数,包含波特率、字长、奇偶校验等参数
(4)使能串口
(5)设置串口中断类型并使能
(6)设置串口中断优先级,使能串口中断通道
(7)编写串口中断服务函数

二、配置工程


(1)复制上一章的工程,并重命名为8、串口中断通信
在这里插入图片描述


(2)进入工程文件,进入APP文件,新建USART文件夹用来存放与串口相关的文件。
在这里插入图片描述


(3)打开工程,新建文件,并命名为usart.h与usart.c

在这里插入图片描述

在这里插入图片描述


(4)添加文件到目录,并添加头文件路径

在这里插入图片描述

在这里插入图片描述


(5)要使用串口需要添加相应的文件

在这里插入图片描述

在这里插入图片描述


三、相关程序


mian.c

#include "LED.h"
#include "Delay.h"
#include "System.h"
#include "usart.h"


/*************************************************
*函数名:    main
*函数功能: 主函数
*输入:     无  
*返回值:   无
**************************************************/
int main()
{
    
	
	SysTick_Init(72);
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);  //抢占式优先级与响应式优先级的分组
	LED_Init();
	USART1_Init(9600);
	while(1)
	{
    
		  
	}
}

usart.h

#ifndef USART_H_
#define USART_H_

#include "stm32f10x.h"

/************串口引脚************/
#define USART1_GPIO_Port  GPIOA
#define USART1_RX_Pin     GPIO_Pin_10
#define USART1_TX_Pin     GPIO_Pin_9

/************串口函数************/
void USART1_Init(u32 bound);          //串口初始化
void USART_SendBit(USART_TypeDef* USARTx,u16 Data);   //发送单个数据
uint16_t USART_ReceiveBit(USART_TypeDef* USARTx);     //接收单个数据
void USART_SendString(USART_TypeDef* USARTx,char *string); //发送字符串


#endif

usart.c

#include "usart.h"


/*************************************************
*函数名:     USART1_Init
*函数功能:   串口1的初始化
*输入:       bound:波特率   
*返回值:     无
**************************************************/
void USART1_Init(u32 bound)
{
    
	GPIO_InitTypeDef GPIO_InitStruct;
	USART_InitTypeDef USART_InitStruct;
	NVIC_InitTypeDef NVIC_InitStruct;
	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA|RCC_APB2Periph_USART1, ENABLE); //时钟使能
	
	GPIO_InitStruct.GPIO_Pin = USART1_TX_Pin;      //发送引脚
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;   //复用推挽输出
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; //发送速度
	GPIO_Init(USART1_GPIO_Port, &GPIO_InitStruct); //引脚初始化
	
	GPIO_InitStruct.GPIO_Pin = USART1_RX_Pin;      //接收引脚
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; //浮空输入
	GPIO_Init(USART1_GPIO_Port, &GPIO_InitStruct); //引脚初始化
	
	USART_InitStruct.USART_BaudRate = bound; //波特率
	USART_InitStruct.USART_HardwareFlowControl = USART_HardwareFlowControl_None; //无硬件数据流控制
	USART_InitStruct.USART_Mode = USART_Mode_Rx|USART_Mode_Tx; //收发模式
	USART_InitStruct.USART_Parity = USART_Parity_No; //没有校验位
	USART_InitStruct.USART_StopBits = USART_StopBits_1;  //一位停止位
	USART_InitStruct.USART_WordLength = USART_WordLength_8b; //8位一个字节
	USART_Init(USART1, &USART_InitStruct); //初始化串口
	
	USART_Cmd(USART1, ENABLE);  //串口使能
	
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);  //接收中断使能
	
	NVIC_InitStruct.NVIC_IRQChannel = USART1_IRQn;  //要打开的中断通道
	NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 2; //抢占式优先级
	NVIC_InitStruct.NVIC_IRQChannelSubPriority = 2;   //相应式优先级
	NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE;  //NVIC通道使能
	NVIC_Init(&NVIC_InitStruct);
	
	USART_ClearFlag(USART1, USART_FLAG_TC); //TC位初始值位1,要先清0
}


//直接使用串口发送接收函数会出现内容覆盖的问题,所以需要我们重写函数
/*************************************************
*函数名:      USART_SendBit
*函数功能:    串口发送函数
*输入:        Data:发送的数据
*返回值:      无
**************************************************/
void USART_SendBit(USART_TypeDef* USARTx,u16 Data)   
{
    
	USART_SendData(USARTx, Data);
	
	//while(!USART_GetFlagStatus(USARTx, USART_FLAG_TXE));  //要等待数据全部转到移位寄存器
	//USART_ClearFlag(USARTx, USART_FLAG_TXE); //清空标志位
	
	while(!USART_GetFlagStatus(USARTx, USART_FLAG_TC));//要等待数据全部发出
	USART_ClearFlag(USARTx, USART_FLAG_TC); //清空标志位
}

/*************************************************
*函数名:       USART_ReceiveBit
*函数功能:     串口接收函数
*输入:         USARTx:串口
*返回值:       接收到的数据
**************************************************/
uint16_t USART_ReceiveBit(USART_TypeDef* USARTx)
{
    
	while(!USART_GetFlagStatus(USARTx, USART_FLAG_RXNE)); //等待接收的数据全部接收
	USART_ClearFlag(USARTx, USART_FLAG_RXNE);
	return USART_ReceiveData(USARTx);
}

/*************************************************
*函数名:        USART_SendString
*函数功能:      串口发送字符串函数
*输入:          USARTx:串口,string:字符型指针
*返回值:        无
**************************************************/
void USART_SendString(USART_TypeDef* USARTx,char *string)
{
    
	while(*string)
	{
    
		USART_SendBit(USARTx,*string++);
	}
}

/*************************************************
*函数名:         USART1_IRQHandler
*函数功能:       串口中断函数-将从电脑发送的数据发回给电脑
*输入:           无
*返回值:         无
**************************************************/
void USART1_IRQHandler()
{
    
	u16 r;
	if(USART_GetITStatus(USART1, USART_IT_RXNE) == SET)  //判断中断标志
	{
    
		r = USART_ReceiveData(USART1);
		USART_SendBit(USART1,r);
	}
}

四、实验效果

在这里插入图片描述

七、HAL库使用串口中断通信

一、cubemx配置工程


(1) 打开cubemx,新建工程,选择自己的芯片。
在这里插入图片描述


(2) 配置RCC,选择外部高速时钟
在这里插入图片描述


(3) 配置时钟树
在这里插入图片描述


(4) 配置串口

在这里插入图片描述

  • Mode设置为异步通信(Asynchronous)
  • 基础参数:波特率为115200 Bits/s。传输数据长度为8 Bit。奇偶检验无,停止位1 接收和发送都使能 (默认的就行)
    打开串口中断

    在这里插入图片描述

(5) 工程文件配置并生成工程

在这里插入图片描述

在这里插入图片描述


二、常用函数讲解


  • HAL_UART_Transmit();串口发送数据,使用超时管理机制
  • HAL_UART_Receive();串口接收数据,使用超时管理机制
  • HAL_UART_Transmit_IT();串口中断模式发送(只触发一次中断)
  • HAL_UART_Receive_IT();串口中断模式接收(只触发一次中断)
  • HAL_UART_Transmit_DMA();串口DMA模式发送
  • HAL_UART_Transmit_DMA();串口DMA模式接收
  • HAL_UART_GetState();判断接收与发送是否结束

相关参数:

  • UART_HandleTypeDef *huart 串口的别名 如 : 我们使用串口USART1的别名就是huart1
  • *pData 需要发送的数据
  • Size 发送的字节数
  • Timeout 最大发送时间
  • HAL_UART_STATE_BUSY_RX,接收完成标志
  • HAL_UART_STATE_BUSY_TX,发送完成标志

回调函数

  • void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart);//接收中断回调函数
  • HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart); //发送中断回调函数
  • void HAL_UART_TxHalfCpltCallback(UART_HandleTypeDef *huart); //串口发送一半中断回调函数
  • void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart);//串口接收一半回调函数

串口中断接收完成之后,会进入该函数,该函数为空函数,用户需自行修改。


三、主函数程序


在这里插入图片描述

#include "string.h"
uint8_t Rx_String[100];    //接收字符串数组
uint8_t Rx_Flag=0;         //接收字符串计数
uint8_t Rx_buff;           //接收缓存

在这里插入图片描述

HAL_UART_Receive_IT(&huart1, (uint8_t *)&Rx_buff, 1);   //开启接收中断

在这里插入图片描述

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    
	if(huart == &huart1)
	{
    
		Rx_String[Rx_Flag++] = Rx_buff;  //接收字符
		if(Rx_String[Rx_Flag-1] == 0x0A) //判断是否接收结束
		{
    
			HAL_UART_Transmit(&huart1, (uint8_t *)&Rx_String, Rx_Flag,0xFFFF); //字符串发送
			while(HAL_UART_GetState(&huart1) == HAL_UART_STATE_BUSY_TX); //判断发送是否完毕
			memset(Rx_String,0x00,sizeof(Rx_buff)); //清空接收字符串
			Rx_Flag = 0; //清空计数器
		}
		HAL_UART_Receive_IT(&huart1, (uint8_t *)&Rx_buff, 1);   //再开启接收中断
	}
}

四、实验效果

在这里插入图片描述

八、关于printf重定向

C语言中printf函数默认输出设备是显示器,如果要实现在
串口或者LCD上显示,必须重定义标准库函数里调用的与输出设备相关的函数。比如使用printf输出到串口,需要将fputc里面的输出指向串口
这一过程就叫重定向


对于标准库
将这段程序加入主函数中

int fputc(int ch,FILE *p)  //函数默认的,在使用printf函数时自动调用
{
    
	USART_SendData(USART1,(u8)ch);	
	while(USART_GetFlagStatus(USART1,USART_FLAG_TXE)==RESET);
	return ch;
}

int fgetc(FILE *p)
{
    
  uint8_t ch = 0;
  USART_SendData(USART1, ch);
  return ch;
}

并添加stdio.h头文件,就可以使用C语言中的printf函数将字符串通过串口打印在电脑上了。


对于HAL库
将这段程序加入主函数中

/**
  * 函数功能: 重定向c库函数printf到DEBUG_USARTx
  * 输入参数: 无
  * 返 回 值: 无
  * 说    明:无
  */
int fputc(int ch, FILE *f)
{
    
  HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xffff);
  return ch;
}
 
/**
  * 函数功能: 重定向c库函数getchar,scanf到DEBUG_USARTx
  * 输入参数: 无
  * 返 回 值: 无
  * 说    明:无
  */
int fgetc(FILE *f)
{
    
  uint8_t ch = 0;
  HAL_UART_Receive(&huart1, &ch, 1, 0xffff);
  return ch;
}

并添加stdio.h头文件,就可以使用C语言中的printf函数将字符串通过串口打印在电脑上了。


九、RS232补充说明(选看)

1、接口标准

串口通信的接口标准有很多,有RS-232C、RS-232、RS-422A、RS-485等。常用的就是RS-232和RS-485。RS-232其实是RS-232C的改进,原理是一样的。这里我们就以RS-232C接口进行讲解。
RS-232C是EIA(美国电子工业协会)1969年修订RS-232C标准。RS-232C定义了数据终端设备(DTE)与数据通信设备(DCE)之间的物理接口标准。
RS-232C接口规定使用25针连接器,简称DB25,连接器的尺寸及每个插针的排列位置都有明确的定义 。
在这里插入图片描述

2、逻辑电平规定

RS-232C对逻辑电平也做了规定,如下:

  • 在TXD和RXD数据线上,逻辑1为(-3)V到(-15)V的电压,逻辑0为3V到15V的电压。
  • 在RTS、CTS、DSR、DTR和DCD等控制线上,信号有效(ON状态)为3V到15V的电压,信号无效(OFF状态)为(-3)V到(-15V)的电压。

由此可见,RS-232C是用正负电压来表示逻辑状态,与晶体管-晶体管逻辑集成电路(TTL)以高低电平表示逻辑状态的规定正好相反

到这里就结束啦!
在这里插入图片描述

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_51447215/article/details/121429721

智能推荐

.vue文件_一起学习VUE:记住vue-cli工程中每个文件夹和文件的用处_weixin_39917576的博客-程序员资料

VUE作为一个常用的前端框架,虽然支持自定义,但是作为一个懒人程序员,在学习过程中都会使用大家通用的语言来完成我们的程序,方便我们的交流语言就应该是合规的,下面分享一下vue-cli工程中的目录作用,方便我们进行统计的语言交流build 文件夹:用于存放 webpack 相关配置和脚本。开发中仅 偶尔使用 到此文件夹下 webpack.base.conf.js 用于配置 less、sass等css...

Performance 介绍及使用_代码performance_bliss河北小宝的博客-程序员资料

Performance介绍为什么使⽤Performance呢?GC 的⽬的是为了实现内存空间的良性循环,⽽良性循环的基⽯是合理的使⽤内存空间。由于 ECMAScript 并没有提供操作内存的 API,所以内存分配是否合理我们不可知。Performance 提供了多种⽅式,在程序运⾏时可以时时监控,确定内存分配是否合理。https://developers.google.com/web/tools/chrome-devtools/memory-problems使⽤具体步骤打开浏览器输⼊⽬标⽹址

Oracle-sqlldr_belief1117的博客-程序员资料

 1引言1.1 概述SQLLDR简介 2sql loader的特点oracle自己带了很多的工具可以用来进行数据的迁移、备份和恢复等工作。但是每个工具都有自己的特点。exp和imp:逻辑转移备份,对象主要是是数据库用户,将一个用户的所有表结构及索引,触发器,过程,函数等导入到另一用户里expdp和impdp:10g新添功能,逻辑转移备份,对象是数据库或数据库用户,实...

linux 两台服务器间带端口参数拷贝文件夹_172.16.0.45_Epic_dyg的博客-程序员资料

scp -P 【端口号】-r 本机文件夹路径 目标服务器用户名@IP地址:目标文件夹路劲如:scp -P 12328 -r /home/date/mysql/ [email protected]:/home/date/mysql

容器生态系统 (续) - 每天5分钟玩转容器技术(3)_weixin_34054931的博客-程序员资料

容器生态系统包含核心技术、平台技术和支持技术三个方面。上一节我们讨论了核心技术,今天讨论另外两个部分。容器平台技术容器核心技术使得容器能够在单个 host 上运行。而容器平台技术能够让容器作为集群在分布式环境中运行。容器平台技术包括容器编排引擎、容器管理平台和基于容器的 PaaS。容器编排引擎基于容器的应用一般会采用微服务架构。在这种架构下,...

lnmp访问php页面出现404,lnmp 环境安装laravel出现404问题解决_weixin_39610759的博客-程序员资料

首先,请使用composer安装laravel,解决依赖问题;安装后运行发现出了根目录可以访问,其他访问都是404;解决方案:打开nginx配置文件,如果是lnmp一键安装的话,在/usr/local/nginx/conf/vhost目录下有单独针对项目的配置文件;找到你的文件,如我配的域名是mp;那么在该文件下就会有mp.conf打开文件加入以下代码即可:location / { ...

随便推点

华为2019秋招面试问答题!(附带笔试参考题)_遗传算法 面试_程序员陈平安的博客-程序员资料

前言:采菊东篱下,悠然见南山”,菊花厂的名气就不多介绍了!今天为大家带来的内容是华为2019秋招面试问答题!(附带笔试参考题)具有不错的参考意义,希望能够帮助到大家!内容如下:总共分为技术面试和综合面试,并附带了一个参考题(含解题思路)1. 技术面面试步骤:自我介绍, 感觉面试官都没怎么仔细听, 也是听多了各种花式自我介绍, 已经厌烦了. 你觉得现在的深度学习算法, ...

redhat7.2 搭建oracle 11g RAC 问题与处理_天涯小冷的博客-程序员资料

1 udev编辑编辑asm映射关系: redhat7.2 udev编辑脚本: for i in b c d e f g h i j k l m n o p q r s t u v w x y z aa ab ac ad ae af ag ah ai aj ak al am an ao ap aq ar; do echo “KERNEL==\”sd*[!0-9]\”, ENV{DEVTY...

Coroutine '协程名称' couldn't be started because the the game object '游戏物体 is inactive!-报错_红叶920的博客-程序员资料

报错的主要原因是当协程还没运行完毕时脚本或游戏物体的enabled未fase,隐藏掉了.解决办法:判断,当当前的游戏物体激活时执行协程if (this.gameObject.active){StartCoroutine(“协程名称”);}else{return;}...

使用EasyExcel解析表格时报错ExcelAnalysisException: File type error,io must be available markSupported_一米阳光zw的博客-程序员资料

问题描述:在使用EasyExcel解析Excel表格的时候报错了,我一开始使用的是指定文件类型的方式进行解析,文件的格式是xls的格式,方法是原来带有文件枚举类型的方法进行解析,其实这样也是不会报错的,如下:后来看这个过时方法有点别扭打算给他换掉如下图,就点进去看到类似的方法,换成不带类型的进行解析,就报如文章所提到的错,其实里面ExcelTypeEnum excelTypeEnum = E...

实现顺序查找的算法_实验题1:实现顺序查找的算法目的:领会顺序查找的过程和算法设计。内容:编写一_静能生悟的博客-程序员资料

/***    实验题目:*        实现顺序查找的算法*    实验目的:*        领会顺序查找的过程和算法设计*    实验内容:*        设计程序,输出在顺序表(3, 6, 2, 10, 1, 8, 5, 7, 4, 9)中采用*    顺序查找方法查找关键字5的过程**/#include <stdio.h>#define MAX_...

GDOI2017模拟第四轮总结_Akakii的博客-程序员资料

Day1T1应该是推式子+SA,比较麻烦,先打50分暴力。 T2原题,但是不太记得具体怎么做。 T3乍一看以为是状压DP。 T4又是见过的原题,但是这题比较难,之前看的时候没有搞懂。 然后开始码T1,50分暴力。出了一点小状况,我的式子推的有问题,结果调试花了太长时间,大大超出预算。然后就赶紧码了T3和T4暴力。回过头来刚T2,花了点时间重新推出正解,打完之后又写了一个暴力,但是只剩5min