STM32学习笔记(6)——USART串口通信

时间:2024-04-16 15:42:39

一、基础知识

本节主要写一下通信的一些基础知识,简单过一遍,防忘。

1. 通信基本知识

(1)数据传送方式

分类:串行和并行。


(2)数据通信方向

分类:全双工、半双工和单工。

(3)数据同步方式

分类:同步和异步。

(4)通信速率

比特率(Bitrate):每秒钟传输的二进制位数,单位为比特每秒(bit/s)。

波特率(Baudrate):表示每秒钟传输的码元个数,单位为波特(B),没有每秒!(常用的波特率:4800、9600、19200、38400、43000、56000、57600、115200)

码元:举个例子,当我们用一个二进制数(0或1)表示信息时,0或1为一个码元,它只有一个位数;当用两个二进制数(00,01,10,11)表示信息时,譬如01,就是一个码元,它有两个位数。

2. 串口通信协议

(1)物理层

物理层:规定通讯系统中具有机械、电子功能部分的特性,确保原始数据在物理媒体的传输。其实就是硬件部分

RS-232标准

RS232标准串口主要用于工业设备直接通信。电平转换芯片一般有MAX3232,SP3232。

TTL电平标准是5V表示逻辑1,0V表示逻辑0;而为了增加串口通信的远距离传输以及抗干扰能力,RS-232使用-15V表示逻辑1,+15V表示逻辑0。

信号通过DB9串口连接器传输入设备,使用的是RS-232标准电平,因此还有电平转换芯片将其转为TTL电平。

USB转串口

在这里插入图片描述

USB转串口主要用于设备跟电脑通信,电平转换芯片一般有CH340等,所以电脑要安装CH340电平转换芯片的驱动程序(之后我们会用到)。

原生串口转串口

原生的串口通信主要是控制器跟串口的设备或者传感器通信,不需要经过电平转换芯片来转换电平,直接用TTL电平通信。

(2)协议层

串口通信一般是以格式传输数据,即一帧一帧传输,每帧包含有起始信号、数据信息、停止信息,可能还有校验信息。

串口数据包的组成:

  • 起始位:1个逻辑0表示。
  • 有效数据:在起始位后紧接着的就是有效数据,有效数据的长度常被约定为5、 6 、 7 或 8 位长。
  • 校验位:用于数据的抗干扰性。

五种校验方法:

奇校验(odd,有效数据和校验位中“ 1”的个数为奇数)

偶校验(even,有效数据和校验位中“ 1”的个数为偶数)

0校验(space,校验位总为0)

1校验(mark,校验位总为1)

无校验(noparity,不包含校验位)

  • 结束位:由 0.5、 1、 1.5 或 2 个逻辑1的数据位表示。

二、USART串口通信详解

USART(Universal Synchronous/Asynchronous Receiver/Transmitter)为通用同步异步收发器,支持同步单向通信和半双工单线通信。还有一种“阉割版”——UART(Universal Asynchronous Receiver/Transmitter),去掉了同步,为异步收发器。

【以下内容来自STM32中文参考手册25章通用同步异步收发器(USART)】USART 满足外部设备对工业标准NRZ异步串行数据格式的要求,并且使用了小数波特率发生器,可以提供多种波特率,使得它的应用更加广泛。USART支持同步单向通信和半双工单线通信;还支持局域互连网络 LIN、智能卡(SmartCard)协议与lrDA(红外线数据协会) SIR ENDEC规范。USART支持使用DMA,可实现高速数据通信。

1. USART功能框图

本节内容可参考STM32中文参考手册25.3节USART功能概述。我们将框图分为4个部分进行讲解。

(1)引脚

  • TX:发送数据输出
  • RX:接收数据串行输入
  • SCLK(位于最右边):发送器时钟输出,仅同步通信时使用
  • nRTS:请求发送(Request To Send)
  • nCTS:允许发送(Clear To Send)
  • SW_RX:数据接收引脚,属于内部引脚。

引脚对应的编号如下:

(2)数据寄存器

数据寄存器(USART_DR)只有低9位有效,实际上它包含一个发送数据寄存器USART_TDR和一个接收数据寄存器USART_RDR。TDR和RDR都是介于系统总线和移位寄存器之间。这里比较特别:一个地址对应了两个物理内存

当进行发送操作时,往USART_DR写入数据会自动存储在 TDR内,然后把内容转移到发送移位寄存器,最后通过模块发送到TX引脚;

当进行读取操作时,信息从RX引脚进入,通过模块后存入接受移位寄存器,然后把内容转移到RDR内,最后USART_DR提取RDR数据。

(3)控制器

本部分内容可参考STM32中文参考手册25.6节USART寄存器描述。

USART有专门控制发送的发送器、控制接收的接收器,还有唤醒单元、中断控制等。

控制寄存器1(USART_CR1)的TE位负责使能发送器,发送器就会“叫醒”发送移位寄存器。

控制寄存器1(USART_CR1)的RX位负责使能接收器,接收器就会“叫醒”接收移位寄存器。

控制寄存器1的TXEIE或RXNEIE置1可以产生中断。

控制寄存器1其他位:TXE:发送移位寄存器为空,发送单个字节时使用。TC:发送完成,发送多个字节数据时候使用。TXIE:发送完成中断使能。

控制寄存器1的M位设置的是字长,该位定义了数据字的长度,由软件对其设置和清零。设置0:一个起始位,8个数据位;设置1:一个起始位,9个数据位。之前已经说过,数据寄存器(USART_DR)只有低9位有效,并且第9位数据是否有效要取决于控制寄存器1的M位设置。

控制寄存器2(USART_CR2)的STOP[1:0]位用于设置停止位,可选0.5个、1个、1.5个、2个停止位。默认使用1个停止位。2个停止位适用于正常USART模式、单线模式和调制解调器模式。0.5和1.5个停止位用于智能卡模式。

(4)波特率

通过波特率寄存器(USART_BRR)可设置波特率。由于计算出的分频因子有小数,因此寄存器分为两个部分:

DIV_Mantissa[11:0]:USARTDIV的整数部分,这12位定义了USART分频器除法因子(USARTDIV)的整数部分。

DIV_Fraction[3:0]:USARTDIV的小数部分,这4位定义了USART分频器除法因子(USARTDIV)的小数部分。

波特比率的计算公式为:

fCK:串口的时钟,需要注意不同USART有不同AHB时钟频率。

USARTDIV:分频器除法因子,为无符号的定点数,最后为写入寄存器的值。

例如设置波特率为115200的计算过程:

使用十进制小数转换为二进制小数的方法进行。

2. USART的结构体定义和相关库函数

本节内容位于头文件stm32f10x_usart.h中。

USART的初始化结构体定义:

typedef struct
{
    //波特率设置
  uint32_t USART_BaudRate;          
    //字长设置
  uint16_t USART_WordLength; 
    //停止位设置
  uint16_t USART_StopBits;
    //校验位设置
  uint16_t USART_Parity;             
    //USART模式设置
  uint16_t USART_Mode;               
    //硬件流控制选择
  uint16_t USART_HardwareFlowControl; 
} USART_InitTypeDef;

USART的相关常用且重要库函数(可查看库函数手册):

void USART_DeInit(USART_TypeDef* USARTx);
void USART_Init(USART_TypeDef* USARTx, USART_InitTypeDef* USART_InitStruct);
void USART_StructInit(USART_InitTypeDef* USART_InitStruct);
void USART_Cmd(USART_TypeDef* USARTx, FunctionalState NewState);
void USART_ITConfig(USART_TypeDef* USARTx, uint16_t USART_IT, FunctionalState NewState);

void 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_ClearFlag(USART_TypeDef* USARTx, uint16_t USART_FLAG);
ITStatus USART_GetITStatus(USART_TypeDef* USARTx, uint16_t USART_IT);
void USART_ClearITPendingBit(USART_TypeDef* USARTx, uint16_t USART_IT);

3. 配置USART的流程

void USART_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;
	
	// 1. 开启USART时钟和对应的引脚时钟
	// USART1
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
	
	// 2. 配置USART引脚
	// USART1-TX发送 GPIOA.9 复用推挽模式
	GPIO_InitStructure.GPIO_Pin = USART1_TX_GPIO_PIN;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(USART1_TX_GPIO_PORT, &GPIO_InitStructure);
	
	// USART1-RX接收 GPIOA.10 浮空输入模式
	GPIO_InitStructure.GPIO_Pin = USART1_RX_GPIO_PIN;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_Init(USART1_RX_GPIO_PORT, &GPIO_InitStructure);
	
	// 3. 配置USART1串口参数
	USART_InitStructure.USART_BaudRate = USART_BAUDRATE;			// 3.1 配置波特率
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;		// 3.2 配置数据字长
	USART_InitStructure.USART_StopBits = USART_StopBits_1;			// 3.3 配置停止位
	USART_InitStructure.USART_Parity = USART_Parity_No;				// 3.4 配置校验位
	USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;	// 3.5 配置工作模式
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;  // 3.6 配置硬件控制流
	
	USART_Init(USART1, &USART_InitStructure);						// 4. 初始化串口
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//USART中断使能
	USART_Cmd(USART1, ENABLE);	 									// 5. 使能串口
}

三、实例:单片机向PC机(上位机)发送数据

注意,电脑需要安装CH340驱动和串口调试助手工具,并在工具中设置好波特率、校验位等参数。单片机要用USB(注意是接到单片机USB232接口)连接电脑而不是用STLINK!!!

1. 发送一个字节

void USART_SendByte(USART_TypeDef* pUSARTx, uint8_t data)
{
	USART_SendData(pUSARTx, data); //发送数据
	while(USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET); //等待发送完成
}

在main函数中进行调用:

USART_Config();
USART_SendByte(USART1, 0x34);

若选择16进制显示,则串口软件会显示34。否则会按照ASCII码转换为相应的字符。

2. 发送两个字节

这里需要说明一下,由于一次只能发送一个字节(4位),所以需要把数据源分段发送。

void USART_SendHalfWord(USART_TypeDef* pUSARTx, uint16_t data)
{
	uint8_t temp_h, temp_l;
	temp_h = (data & 0xFF00) >> 8; //截取高4位
	temp_l = data & 0x00FF; //截取低4位
	
	USART_SendData(pUSARTx, temp_h);
	while(USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
	
	USART_SendData(pUSARTx, temp_l);
	while(USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
}

3. 串口发送8位数据的数组

void USART_SendArray(USART_TypeDef* pUSARTx, uint8_t* Array, uint8_t num)
{
	uint8_t i;
	for( i = 0; i < num; i++)
	{
		USART_SendData(pUSARTx, Array[i]);
		while(USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
	}
}

4. 串口发送字符串

中文字符也行。

void USART_SendStr(USART_TypeDef* pUSARTx, uint8_t* str)
{
	while( *(str) != \'\0\')
	{
		USART_SendData(pUSARTx, *(str++));
		while(USART_GetFlagStatus(pUSARTx, USART_FLAG_TXE) == RESET);
	}
}

5. 重定向printf函数输出到串口

注意需要在魔法棒Target勾选Use MicroLIB,然后重新编译一次。

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

//重定向c库函数printf到串口,重定向后可使用printf、putchar函数
int fputc(int ch, FILE *f)
{
	/* 发送一个字节数据到串口 */
	USART_SendData(USART1, (uint8_t)ch);
		
	/* 等待发送完毕 */
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);		
	
	return (ch);
}

在main函数中调用以下语句时会输出到串口软件:

printf("我永远喜欢伊蕾娜!!!\n");
putchar(\'A\');

四、实例:单片机从PC机(上位机)接收数据

1. 重定向scanf函数输入到串口

//重定向c库函数scanf到串口,重写向后可使用scanf、getchar函数
int fgetc(FILE *f)
{
	/* 等待串口输入数据 */
	while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == RESET);

	return (int)USART_ReceiveData(USART1);
}

2. 使用中断服务函数

//stm32f10x_it.c
void USART1_IRQHandler(void)
{
	uint8_t a;
	if( USART_GetFlagStatus(USART1, USART_FLAG_RXNE) != RESET ) //如果接收寄存器接收到数据
	{
		a = USART_ReceiveData(USART1); 
		USART_SendData(USART1, a); //将接收的数据发送
	}
}

需要配置NVIC:

void NVIC_Config(void)
{
	NVIC_InitTypeDef NVIC_InitStructure;
	
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); 
	
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQ;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; 
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);
}

在main函数调用scanf函数,PC输入数据时,单片机会回复同样的数据给PC。

3. 上位机控制LED

功能实现:输入1时实现LED0反转,输入2时实现LED1反转,输入其他向上位机提示输入有误。部分代码如下:

int main(void)
{	
	uint8_t ch;
	USART_Config();
	LED_Init();

	while(1)
	{
		ch = getchar();
		printf("你选择了%c灯!\n", ch);
		switch(ch)
		{
			case \'1\':
				LED0 = !LED0;
				break;
			case \'2\':
				LED1 = !LED1;
				break;
			default:
				printf("输入有误!请重新输入!\n");
				break;
		}
		
	}
}