USART串口通信

USART串口通信

通信概述

通信目的:将一个设备的数据传送到另一个设备,扩展硬件系统
通信协议:制定通信的规则,通信双方按照协议规则进行数据收发

image
USART/I2C/SPI:需要接GND引脚才能正常通信

UART:单片机和电脑/单片机和模块点对点通信
image
I2C:一个单片机主控、外挂多个被动传感器/存储器 -> 一主多从
image
SPI:高速通信 -> 一主多从
image
CAN多个主控互相通信

※双工
全双工:可以同时互相通信
半双工:只能同时走一个方向 一个方向走完了才能走另一个方向
单工:只能走一边

※时钟:我如何知道信号是 10 还是 1100
异步:需要确定起始点,帧数等
同步

※电平:两个设备的电压是否相同
单端:不相同
差分:相同 抗干扰能力强

※设备
点对点:一个设备对一个设备
多设备:有总线的区别 需要有寻址的过程来确定通信对象

串口通信:实现两个设备的互相通信(转协议)

image
USB转串口 CH340

image
CH340G 实现两个芯片的通信

image
串口转蓝牙

硬件电路

发送端TX接收端RX
image
(1)简单双向串口通信有两根通信线
(2)TX与RX要交叉连接
(3)只需单向的数据传输时,可以只接一根通信线
(4)电平标准不一致时:需要加电平转换芯片

电平标准:数据1和数据0的表达方式

TTL电平【常用】+3.3V或+5V表示1,0V表示0

RS232电平:-3 ~ -15V表示1,+3 ~ +15V表示0
一般大型机器使用

RS485电平【差分信号】:两线压差+2 ~ +6V表示1,-2 ~ -6V表示0

串口参数及时序

波特率/比特率:串口通信速率
——————————————————————
【科普】
波特率:单位时间内传送二进制数据的位数 单位bps(位/秒)
比特率:单位时间内传送二进制有效数据的位数 单位bps(位/秒)

比特率 = 波特率 x 单个调制状态对应的二进制位数
eg
1个起始位,8个数据位、1个校验位、2个终止位,每秒传送120个字符。
->有效数据为8位,一帧包含1+8+1+2=12位
波特率为:120*(1+8+1+2)=1440 bps=1440波特
又因为有效数据位为8位,而传送一个字符需1+8+1+2=12位
故比特率为:1440 * (8/12)=960 bps
(比特率还可以直接求:8 * 120=960 bps)
——————————————————————
空闲状态:引脚必须设高电平

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

数据位:数据帧的有效载荷,1为高电平,0为低电平
低位先行:0x55->01010101->低位先行->故波形:10101010
image

校验位:用于数据验证,根据数据位计算得来
无校验一般使用 奇校验和偶校验 在1个信号有2个及以上问题时精度较低
奇校验:看1的个数 如果不为奇数->认为是信号传输有问题
数据位00001111 校验位1 -> 5个1
数据位00001110 校验位0 -> 3个1
偶校验:看1的个数是否为偶数
※更高的检出率:CRC校验

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

举一些栗子

改变波特率 可以看到时间不一样
image
image

偶校验
image

连着两个0x55数据
image

中间有停止
image

USART简介

USART通用同步收发器(很少使用)/UART通用异步收发器

【常用参数】
波特率 9600/115200
数据位长度 常用8
停止位长度 常用1
校验位 常用 无校验

【STM32F103C8T6 USART资源】
APB2:USART1
APB1:USART2、USART3

USART基本结构

image

USART框图

image

(1)接受移位寄存器(RDR)没准备好接收:高电平 准备好:低电平
TX接收反馈信号 准备好了再发数据
image

(2)判断发送状态和接收状态的必要标志位
TXE:发送寄存器非空
RXNE:接受寄存器非空

(3)波特率发生器:分频器
利用APB时钟进行分频,得到发送和接收移位的时钟

数据帧

最好选择9位字长 有校验
或者8位字长 无校验
image
image

【检测噪声】:NE标志位

起始位侦测

image

数据采样:1个数据位16个采样时钟

image

波特率发生器

波特率由波特率寄存器BRR里的DIV确定
计算公式:波特率 = fPCLK2/1 / (16 * DIV)
image

CH340原理图

image
GND与单片机互接

寄存器经典分类【基本每个外设都有】

SR 状态寄存器 : 存放各种标志位
DR 数据寄存器 : 存放关键数据
CR 配置寄存器 : 存放配置参数

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_SendData(USART_TypeDef* USARTx, uint16_t Data);//发送数据
uint16_t USART_ReceiveData(USART_TypeDef* USARTx);//接受数据

【初始化】

void Serial_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);	//开启USART1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
	//将PA9引脚初始化为复用推挽输出【有外设】
	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_Mode_Tx 发送模式
	USART_Mode_Rx 接收模式
	
	USART_InitStructure.USART_Parity = USART_Parity_No;//奇偶校验,不需要
	
	USART_Parity_No 无校验
	USART_Parity_Odd 奇校验
	USART_Parity_Even 偶校验
	
	USART_InitStructure.USART_StopBits = USART_StopBits_1;//停止位,选择1位

    USART_StopBits_1
    USART_StopBits_0_5
    USART_StopBits_2
    USART_StopBits_1_5

	USART_InitStructure.USART_WordLength = USART_WordLength_8b;//字长,选择8位

    USART_WordLength_8b
    USART_WordLength_9b

	USART_Init(USART1, &USART_InitStructure);
	//将结构体变量交给USART_Init,配置USART1

	/*USART使能*/
	USART_Cmd(USART1, ENABLE);//使能USART1,串口开始运行
}

【接收模式】

GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);//将PA10引脚初始化为上拉输入

USART_InitStructure.USART_Mode = USART_Mode_Tx | USART_Mode_Rx;	//模式,发送模式和接收模式均选择

串口中断

/*中断输出配置*/
	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接收数据的中断
	
	/*NVIC中断分组*/
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);	//配置NVIC为分组2
	
	/*NVIC配置*/
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
	//选择配置NVIC的USART1线
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
	NVIC_Init(&NVIC_InitStructure);

【串口发送字节】:发送之后要等待标志位!

void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1, Byte);
	//将字节数据写入数据寄存器,写入后USART自动生成时序波形
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);//等待发送完成
	/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}

各种发送

//【发送数组】
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
	uint16_t i;
	for (i = 0; i < Length; i ++)//遍历数组
	{
		Serial_SendByte(Array[i]);
	}
// 【发送字符串】
void Serial_SendString(char *String)
{
	uint8_t i;
	for (i = 0; String[i] != '\0'; i ++)
	//遍历字符数组(字符串),遇到字符串结束标志位后停止
	{
		Serial_SendByte(String[i]);
	}
}
// 【发送数据】
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
	uint8_t i;
	for (i = 0; i < Length; i ++)//根据数字长度遍历数字的每一位
	{
		Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');	//依次调用Serial_SendByte发送每位数字
	}
}

重定向printf

int fputc(int ch, FILE *f)
{
	Serial_SendByte(ch);			//将printf的底层重定向到自己的发送字节函数
	return ch;
}

封装Serial_printf

void Serial_Printf(char *format, ...)//可变参数
{
	char String[100];//定义字符数组
	va_list arg;//定义可变参数列表数据类型的变量arg
	va_start(arg, format);//从format开始,接收参数列表到arg变量
	vsprintf(String, format, arg);
	//使用vsprintf打印格式化字符串和参数列表到字符数组中
	va_end(arg);//结束变量arg
	Serial_SendString(String);//串口发送字符数组(字符串)
}

USART串口接收

配置串口中断!
【USART有专门的中断】

/*中断输出配置*/
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);//开启串口接收数据的中断

image
当USART_IT_RXNE为1时向NVIC申请中断

其余和NVIC相同

/*NVIC中断分组*/
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//配置NVIC为分组2
	
/*NVIC配置*/
NVIC_InitTypeDef NVIC_InitStructure;//定义结构体变量
NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;//选择配置NVIC的USART1线
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;//指定NVIC线路使能
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;//指定NVIC线路的抢占优先级为1
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;//指定NVIC线路的响应优先级为1
NVIC_Init(&NVIC_InitStructure);//将结构体变量交给NVIC_Init,配置NVIC外设

/*USART使能*/
USART_Cmd(USART1, ENABLE);//使能USART1,串口开始运行

当Rx接收到数据后进入中断

void USART1_IRQHandler(void)
{
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
	//判断是否是USART1的接收事件触发的中断
	{
		Serial_RxData = USART_ReceiveData(USART1);
		//读取数据寄存器,存放在接收的数据变量
		Serial_RxFlag = 1;
		//置接收标志位变量为1
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);
		//清除USART1的RXNE标志位
		//读取数据寄存器会自动清除此标志位
		//如果已经读取了数据寄存器,也可以不执行此代码
	}
}

【检测标志位】 是否为1,如为1清零标志位

uint8_t Serial_GetRxFlag(void)
{
	if (Serial_RxFlag == 1)//如果标志位为1
	{
		Serial_RxFlag = 0;
		return 1;//则返回1,并自动清零标志位
	}
	return 0;//如果标志位为0,则返回0
}

【获取接收数据】

uint8_t Serial_GetRxData(void)
{
	return Serial_RxData;			//返回接收的数据变量
}

main函数

while (1)
{
	if (Serial_GetRxFlag() == 1)//检查串口接收数据的标志位
	{
		//先进USART中断!!!

		RxData = Serial_GetRxData();//获取串口接收的数据
		Serial_SendByte(RxData);//串口将收到的数据回传回去,用于测试
		OLED_ShowHexNum(1, 8, RxData, 2);//显示串口接收的数据
	}
}

如果要显示汉字呢?
※MicroLIB是Keil为嵌入式平台优化的精简库

UTF-8不乱码
小锤子C/C++内写上
-no-multibyte-chars
image

Serial_Printf("你好,世界");

切换GB2312编码
image
右上角打开配置(小扳手)
image
然后把汉字删掉,把文件关掉再打开->字体变为宋体
串口助手选GBK编码

USART串口数据包

数据模式

image
ASCII码表
image

HEX数据包

包头 0xFF
包尾 0xFE

固定包长
image
可变包长
image

【优点】
传输直接,解析数据简单
适合模块发送原始数据
eg 使用串口通信的陀螺仪 湿温度传感器
【缺点】
灵活性不足
载荷容易和包头包尾重复

文本数据包

包头 '@'
包尾 '\r''\n'

固定包长
image
可变包长
image

【优点】
数据直观易理解,灵活
适合输入指令进行人机交互的场景
eg 蓝牙模块常用AT指令

接收数据包

S:状态机
记住不同状态
在不同状态执行不同操作,进行状态合理转移

接收HEX数据包:固定包长4

image

接收文本数据包:可变包长

image

uint8_t Serial_TxPacket[4];				//定义发送数据包数组,数据包格式:FF 01 02 03 04 FE
uint8_t Serial_RxPacket[4];				//定义接收数据包数组
uint8_t Serial_RxFlag;					//定义接收数据包标志位

.h里写声明全局变量

extern uint8_t Serial_TxPacket[];
extern uint8_t Serial_RxPacket[];

发送HEX数据包

void Serial_SendPacket(void)
{
	Serial_SendByte(0xFF);
	Serial_SendArray(Serial_TxPacket, 4);
	Serial_SendByte(0xFE);
}

串口中断发送数据包

当RXNE为非空时触发

注意静态变量【内存管理】

void USART1_IRQHandler(void)
{
	//只能被初始化一次0 具有全局变量的性质但只能在本函数使用:内存管理
	static uint8_t RxState = 0;//定义表示当前状态机状态的静态变量
	static uint8_t pRxPacket = 0;//定义表示当前接收数据位置的静态变量
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
	//判断是否是USART1的接收事件触发的中断
	{
		uint8_t RxData = USART_ReceiveData(USART1);
		//读取数据寄存器,存放在接收的数据变量

		/*使用状态机的思路,依次处理数据包的不同部分*/

		/*当前状态为0,接收数据包包头*/
		if(RxState == 0)
		{
			if (RxData == 0xFF)//如果数据确实是包头
			{
				RxState = 1;//置下一个状态
				pRxPacket = 0;//数据包的位置归零
			}
		}
		
		/*当前状态为1,接收数据包数据*/
		else if (RxState == 1)
		{
			Serial_RxPacket[pRxPacket++] = RxData;
			//将数据存入数据包数组的指定位置
			if (pRxPacket >= 4)//如果收够4个数据
			{
				RxState = 2;//置下一个状态
			}
		}
		
		/*当前状态为2,接收数据包包尾*/
		else if (RxState == 2)
		{
			if (RxData == 0xFE)//如果数据确实是包尾部
			{
				RxState = 0;//状态归0
				Serial_RxFlag = 1;//接收数据包标志位置1,成功接收一个数据包
			}
		}
		
		USART_ClearITPendingBit(USART1, USART_IT_RXNE);//清除标志位
	}
}

※一个小问题
Serial_RxPacket数组兼具写入和写出
数据包之间可能会混在一起
-> 解决方法:在接收部分加入判断

发送文本数据包

※防止处理太快导致的数据包混在一起
分开
【写数据】 处理数据结束后(OLED屏显示后):再将Flag置0
【读数据】 在处理包头时:等待标志位置0再执行

【中断函数】

void USART1_IRQHandler(void)
{
	static uint8_t RxState = 0;
	static uint8_t pRxPacket = 0;
	if (USART_GetITStatus(USART1, USART_IT_RXNE) == SET)
	{
		uint8_t RxData = USART_ReceiveData(USART1);

		/*当前状态为0,接收数据包包头*/
		if (RxState == 0)
		{
			if (RxData == '@' && Serial_RxFlag == 0)
//如果数据确实是包头,并且上一个数据包已处理完毕:可变包长,故对上一个状态进行判断
			{
				RxState = 1;//置下一个状态
				pRxPacket = 0;//数据包的位置归零
			}
		}
		/*当前状态为1,接收数据包数据,同时判断是否接收到了第一个包尾*/
		else if (RxState == 1)
		{
			if (RxData == '\r')  //如果收到第一个包尾
			{
				RxState = 2;//置下一个状态
			}
			else  //接收到了正常的数据
			{
				Serial_RxPacket[pRxPacket++] = RxData;
				//将数据存入数据包数组的指定位置
			}
		}
		/*当前状态为2,接收数据包第二个包尾*/
		else if (RxState == 2)
		{
			if (RxData == '\n') //如果收到第二个包尾
			{
				RxState = 0;//状态归0
				Serial_RxPacket[pRxPacket] = '\0';
				//将收到的字符数据包添加一个字符串结束标志
				Serial_RxFlag = 1; //接收数据包标志位置1,成功接收一个数据包
			}
		}

		USART_ClearITPendingBit(USART1, USART_IT_RXNE);		//清除标志位
	}
}

【主函数】
注意在最后有将Serial_RxFlag清0

while (1)
	{
		//会先进中断!
		if (Serial_RxFlag == 1)//如果接收到数据包
		{
			OLED_ShowString(4, 1, "                ");
			OLED_ShowString(4, 1, Serial_RxPacket);
			//OLED清除指定位置,并显示接收到的数据包

			/*将收到的数据包与预设的指令对比,以此决定将要执行的操作*/
			if (strcmp(Serial_RxPacket, "LED_ON") == 0)
			//如果收到LED_ON指令
			{
				LED1_ON();
				Serial_SendString("LED_ON_OK\r\n");
				OLED_ShowString(2, 1, "                ");
				OLED_ShowString(2, 1, "LED_ON_OK");
			}
			else if (strcmp(Serial_RxPacket, "LED_OFF") == 0)
			//如果收到LED_OFF指令
			{
				LED1_OFF();
				Serial_SendString("LED_OFF_OK\r\n");
				OLED_ShowString(2, 1, "                ");
				OLED_ShowString(2, 1, "LED_OFF_OK");
			}
			else //上述所有条件均不满足,即收到了未知指令
			{
				Serial_SendString("ERROR_COMMAND\r\n");
				//串口回传一个字符串ERROR_COMMAND
				OLED_ShowString(2, 1, "                ");
				OLED_ShowString(2, 1, "ERROR_COMMAND");
			}

			Serial_RxFlag = 0;
			//处理完成后,需要将接收数据包标志位清零,否则将无法接收后续数据包
		}
	}
posted @ 2024-12-05 21:30  White_ink  阅读(598)  评论(0)    收藏  举报