简介:

基于STM32标准库,主要涉及初始化、收发原理、中断收发、常用配置。STM32F103 有2个USART,其中,USART1位于APB2总线,USART2/3是APB1总线,支持同步/异步模式,常用异步串口UART模式。参考江科大视频,如有错误,欢迎指正,侵删!

一、串口核心知识点

1、总线时钟

USART1挂APB2,预分频器不分频,72MHz,串口时钟=APB2时钟。

USART2/3挂APB1,APB1最大36MHz,串口时钟=APB1时钟*2(APB1预分频不为1时加倍)。STM32标准库自动计算波特率分频值,不用手动算。

2、串口引脚复用功能

默认复用引脚:

USART1:TX-PA9、RX-PA10

USART2:TX-PA2 、RX-PA3

USART3:TX-PB10、RX-PB11

注:必须开启GPIO复用时钟,USART外设时钟,GPIO配置为复用推挽输出。

3、串口

1)工作模式:异步串口:只需要TX(发送)、RX(接收)2根线,无时钟线。

帧格式:起始位1bit +数据位8/9bit +停止位0.5/1.5/2bit +奇偶检验(可选),常见配置:8N1,9600/115200波特率。

2)硬件

注:RX与Tx交叉连接,只需担心数据传输,可以只接一根线。电平标准不一致时,加电平转换芯片。

3)常用串口电平标准如下:

TTL电平:+3.3V或+5V表示1,0V表示0(最常见,单片机常用)

RS232电平:-3~-15V表示1,+3~+15V表示0

RS485电平:两线压差+2~+6V表示1,-2~-6V表示

4)串口参数及时序

波特率:串口通信速率

起始位:标志一个数据帧的开始,固定为低电平

数据位:数据帧的有效载荷,1为高电平,0为低电平;

校验位:数据验证,根据数据位计算;

停止位:数据帧间隔,固定位高电平

   

建议:不需要校验位选8位数据,需要选9位数据。

二、标准库库函数

1、USART初始化结构体 USART_InitTypeDef()

typedef struct{
uint32_t USART_BaudRate;//波特率
uint16_t USART_WordLength;/数据位长度
uint16_t USART_StopBits;//停止位
uint16_t USART_Parity; //奇偶校验
uint16_t USART_Mode;//收发模式:只发/只收/收发
uint16_t USART_HardwareFlowControl;//硬件流控制,一般关闭
}USART_InitTypeDef;

2、常用宏定义:

USART_WordLength_8b //8位数据
USART_StopBits_1 //1位停止位
USART_Parity_No //无校验
USART_Mode_Tx |USART_Mode_Rx //收发全开
USART_HardwareFlowControl_None //关闭硬件流控制

3、中断配置结构体USART_ITConfig

串口常用中断:

USART_IT_RXNE:接收数据寄存器非空中断(收到一个字节就触发中断,最常用);

USART_IT_TXE:发送数据寄存器空中断;

USART_IT_TC:发送完成中断;

4、关键库函数

USART_Init(USART_TypeDef* USARTx,USART_InitTypeDef*USART_InitStruct);//串口初始化函数
USART_Cmd(USART_TypeDef*USARTx,FunctionState NewState); //使能/关闭串口外设
USART_SendData(USART_TypeDef*USARTx,uint16_t Data);//发送一个字节
uint16_t USART_ReceiveData(USART_TypeDef*USARTx);//读取接收到的一个字节
FlagStatus USART_GetFlagStatus(USART_TypeDef*USARTx,uint16_t USART_FlAG);//查询标志位
void USART_ITConfig(USART_TypeDef*USARTx ,uint16_t USART_IT,FunctionState NewState)//开启串口对应中断

USART_FLAG_RXNE(Read data register not empty):读数据寄存器非空,接收标志(有数据可读);

USART_FLAG_TXE(Transmit data register empty):发送数据寄存器空,可以写数据;

USART_FLAG_TC(Transmission complete):一帧数据全部发送完成;

三、收发过程

1、概述

单片机串口:硬件独立工作,CPU只管写数据、读数据,收发是USART外设自行移位处理,不用CPU一位位搬运。分2部分:单片机发送数据、电脑/外设给单片机发送数据(单片机接收)。

2、发送过程(Tx接PA9)

寄存器核心:DR(数据寄存器)、TXE标志位、移位寄存器。

步骤1:CPU执行 USART_SendData(USART1,data);

函数本质:把你传入的8位数据,写入USART的DR数据保持寄存器。此时移位寄存器是空的,硬件自动把DR里的数据搬到移位寄存器,开始一位位往外发送。

步骤2:TXE标志位置1

数据从DR搬走后,硬件自动置位USART_FLAG_TXE。含义:DR寄存器已经空了,可以再写下一个字节。

查询发送:while(!TXE)或者while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) ==RESET);就是等待这个标志,等它置1就可以发送下一个字节。

如果开启中断,USART_ITConfig(USART1,USART_IT_TXE,ENABLE),此时会触发 发送空 中断。

步骤3:移位寄存器自动串行输出

USART硬件自行处理时序:先发一位起始位(低电平)→依次发送8位数据位→1位停止位(高电平),波特率时钟由外设自己产生,CPU不用管。一帧数据全部发送完毕后,硬件置位 TC发送完成标志位。

整个流程:CPU写DR→DR数据送入移位寄存器→TXE置1→硬件串行移位输出引脚TX→全部发送完TC置1。

3、单片机接收过程(Rx引脚PA10,外设主动发送CPU)

步骤1、外部设备(电脑串口助手)往Rx引脚发高低电平,Rx引脚不断检测电平变化,当检测到一个下降沿(起始位),串口外设开始同步接收。

步骤2、硬件移位采样数据

USART按照波特率,连续采样8位数据,存入内部移位寄存器,8位数据全部收齐之后,硬件自动把移位寄存器的数据复制到DR数据寄存器。

步骤3、硬件置位RXNE标志位。DR寄存器装入新数据后,硬件自动置位USART_FLAG_RXNE,即:接收寄存器非空,有新数据,CPU要来读取。此时2种方式可以实现:

1)只查询方式,没开中断(不用USART_ITConfig)

USART_GetFlagStatus(USART1,USART_FLAG_RXNE),查询到标志为1,就去读DR寄存器。

2)开启RXNE接收中断(使用USART_ITConfig)

RXNE置1,由于开启了中断,串口外设立即向NVIC发出中断请求,NVIC已经提前配置USART1中断,CPU暂停主循环程序,跳入固定中断函数:USART1_IRQHandler();

步骤4、CPU读取DR数据

函数:USART_ReceiveData()读取DR,这个读操作硬件会自动清除RXNE标志位。标志位清零后,串口外设才可以继续接收下一个字节。

注意:如果进入中断,没有用USART_ReceiveData()读取DR,RXNE标志一直是1,会一直不断重复进入中断,卡死程序。

综上:接收过程是:外部电平→Rx引脚硬件移位接收→数据存入DR→RXNE置1→开启中断就触发USART1中断→CPU进中断服务函数→读取DR→硬件自动清除RXNE标志位→等待下一个字节。

四、代码编写

配置:STM32F103C8T6,72MHz,9600bit/s,PA9-Tx,PA10-Rx

练习1、只发送数据Tx--PA9

1、开启时钟,GPIO配置为复用推挽输出

void Serial_Init()
{
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
	//GPIO初始化
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 ;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);

    //USART 初始化并使能
    USART_InitTypeDef USART_InitStructure;
	USART_InitStructure.USART_BaudRate = 9600;
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	USART_InitStructure.USART_Mode = USART_Mode_Tx;
	USART_InitStructure.USART_Parity = USART_Parity_No;
	USART_InitStructure.USART_StopBits = USART_StopBits_1;
	USART_InitStructure.USART_WordLength =USART_WordLength_8b;
	USART_Init(USART1,&USART_InitStructure);
	USART_Cmd(USART1,ENABLE);
}

2、发送一个字节和发送字符串函数(先发送低位)

void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1,Byte);
	while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) ==RESET);
}
//发送字符串
 void Serial_SendString(char* String)
 {
	while(*String )
	{
		Serial_SendByte(*String++);
	}
 
 }

3、测试

#include "stm32f10x.h"           
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
int main()
{
	OLED_Init();
	Serial_Init();
	Serial_SendByte(0x34);
	Serial_SendString("hello\r\n");
	
	while(1)
	{
		
	}

}

练习2:收发数据

1、开启时钟,GPIO配置PA9复用推挽输出,PA10浮空输入,

void Serial_Init()
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 ;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 ;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	USART_InitTypeDef USART_InitStructure;
	USART_InitStructure.USART_BaudRate = 9600;
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	USART_InitStructure.USART_Mode = USART_Mode_Tx |USART_Mode_Rx;
	USART_InitStructure.USART_Parity = USART_Parity_No;
	USART_InitStructure.USART_StopBits = USART_StopBits_1;
	USART_InitStructure.USART_WordLength =USART_WordLength_8b;
	USART_Init(USART1,&USART_InitStructure);
	USART_Cmd(USART1,ENABLE);

}

2、接收数据并发送,在OLED上显示

方式1:接步骤1的代码,采用查询的方式:不断检测USART_FLAG_RXNE标志位,置1就读取数据。

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"
uint8_t Data;

int main()
{
	OLED_Init();
	Serial_Init();
	
	while(1)
	{
		if(USART_GetFlagStatus(USART1,USART_FLAG_RXNE) ==SET)
		{
			Data =USART_ReceiveData(USART1);
			Serial_SendByte(Data);
			OLED_ShowHexNum(1,1,Data,2);	
		}		
	}
}

方法2、中断方式接收(项目常用)

原理:外设设备发来一个字节,硬件置位RXNE标志,触发USART1中断,进入中断函数读取数据。完整代码如下:

Serial.c代码

#include "Serial.h"

uint8_t Receive_Flag,Data;

void Serial_Init()
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9 ;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10 ;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStructure);
	
	USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);

	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
	NVIC_Init(&NVIC_InitStructure);
	
	USART_InitTypeDef USART_InitStructure;
	USART_InitStructure.USART_BaudRate = 9600;
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
	USART_InitStructure.USART_Mode = USART_Mode_Tx |USART_Mode_Rx;
	USART_InitStructure.USART_Parity = USART_Parity_No;
	USART_InitStructure.USART_StopBits = USART_StopBits_1;
	USART_InitStructure.USART_WordLength =USART_WordLength_8b;
	USART_Init(USART1,&USART_InitStructure);
	USART_Cmd(USART1,ENABLE);

}
void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1,Byte);
	while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) ==RESET);
}
 void Serial_SendString(char* String)
 {
	while(*String )
	{
		Serial_SendByte(*String++);
	}
 }
 
uint8_t Serial_RxFlag1()
{
	if(Receive_Flag ==1)
	{
		Receive_Flag =0;
		return 1;
	}
	return 0;
}
uint8_t Rx_GetData1()
{
	return Data;
}
 
 void USART1_IRQHandler()
 {
	if(USART_GetITStatus(USART1,USART_IT_RXNE) ==SET)
	{
		Data =USART_ReceiveData(USART1);
		Receive_Flag =1;
		USART_ClearITPendingBit(USART1,USART_IT_RXNE);		
	}
 }


 

main.c代码:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "Serial.h"

uint8_t GetData;
int main()
{
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
	OLED_Init();
	Serial_Init();
	
	while(1)
	{
		if(Serial_RxFlag1()==1)
		{
			GetData =Rx_GetData1();
			Serial_SendByte(GetData);
			OLED_ShowHexNum(1,1,GetData,2);
			
		}
		
	}
}

初始化时,也可以将波特率设为变量 uint32_t  BaudRate 。

总结:标准库配置流程(中断):

1、开启GPIO时钟、开串口外设时钟;

2、GPIO配置:Tx复用推挽,Rx浮空输入;

3、USART1结构体初始化:波特率、8N1格式;

4、开启外设中内部断使能(USART_ITConfig必写);

5、NVIC优先级分组,设置一次全局即可;

6、NVIC通道初始化,打开CPU中断响应;

7、使能USART外设;

8、编写中断服务函数;

五、关于中断USART_ITConfig

1、USART_ITConfig()是单独打开串口某一个中断开关,外设本身有中断功能,默认是关闭,这个函数就是打开对应的中断使能开关,不写的话,硬件不会触发串口中断。

STM32有2道开关,都得开启,缺一不可:

开关1:外设内部中断使能 USART_ITConfig(),有很多中断源,默认都关闭。(给串口外设开门)

比如:USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);就是告诉USART1外设,收到数据(RXNE事件发生),要向NVIC中断控制器发出中断请求信号。

开关2:NVIC总开关---NVIC_Init()   (3、给CPU开门)

NVIC是芯片统一中断控制器,配置NVIC,是允许CPU响应这个中断通道。

总之:USART_ITConfig()允许外设产生中断请求→NVIC_Init()CPU允许响应这个请求。

2、USART_ITConfig()第2个参数:

参数 含义
USART_IT_RXNE 接收数据寄存器非空 中断
USART_IT_TXE 发送数据寄存器空 中断
USART_IT_TC 一帧全部发送完成 中断
USART_IT_PE 奇偶检验错误 中断

3、三种情况要不要写USART_ITConfig()

1)查询方式接收(while轮询标志位),不用写;

2)开启串口Rx接收中断,必须写;

3)只用串口发送,不开启任何中断,不用写;

六、printf重定向

1、原理:printf()底层会调用一个标准库函数fputc(),默认这个函数往电脑控制台输出,我们要重写这个函数让它把每一个字符通过串口发送出去。

2、开启步骤

步骤1、在keil里,魔术棒→Target勾选UseMircroLIB,否则printf无法输出。

步骤2、在串口初始化的c文件中编写重定向即可。本案例是Serial.c,包含头文件<stdio.h>

#include <stdio.h>
int fputc(int ch,FILE*f)
{
	while(USART_GetFlagStatus(USART1,USART_FLAG_TXE) ==RESET);
	USART_SendData(USART1,ch);	
	return ch;
}

这样就定向到了串口1。

3、注意事项

1)printf内部是等待TXE标志逐个发送字符,会阻塞CPU允许。

2)不要在中断服务函数里调用printf。中断里运行printf会造成程序卡死、时序混乱。

3)主循环里随便使用printf,但打印大量数据会占用CPU时间。

4、printf重定向什么时候用?什么时候不用?

使用:

场景1、调试程序,查看变量数值,程序运行状态,比如,可直接写:printf("温度:%d\r\n",temp);

串口助手直接看到打印的数值等。

场景2:快速打印字符串+数字混合内容,不然打印数字很麻烦,要拆分百、十、个位等

场景3:工程复杂,多处需打印日志信息。程序模块多,采集、通讯、电机控制等多处需要输出信息,统一printf,代码简洁。

不使用:

场景1:产品已经调试完毕,关闭printf接收flash空间。

场景2:只用串口简单收发指令,只接收上位机命令,不需要打印日志。

场景3:单片机资源紧张。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐