stm32教程:USART串口通信

按我目前接触下来的感受,通信一般都是比较常用的,在设计一个项目的时候一般用到的开发板都不止一个,但是最后要将功能联合起来,那么就需要通过通信了, 而其中比较方便的就是串口通信,简单好用。

文章目录

什么是通信?

在硬件领域中,通信就是将一个设备的数据传送到另一个设备,扩展硬件系统。

根据不同的通信规则衍生出了很多种通信协议,根据这些通信协议,通信双方按照协议规则进行数据收发。

通信接口

在stm32f103c8t6这个最小系统板中,实现了很多种通信协议,如下表所示:

名称 引脚 双工 时钟 电平 设备
USART TX、RX 全双工 异步 单端 点对点
I2C SCL、SDA 半双工 同步 单端 多设备
SPI SCLK、MOSI、MISO、CS 全双工 同步 单端 多设备
CAN CAN_H、CAN_L 半双工 异步 差分 多设备
USB DP、DM 半双工 异步 差分 点对点

双工

全双工 ,就是指通信双方能够同时 进行双向通信 。一般全双工的都会有两根通信线,发送和接收互不影响

半双工,这种方式一般只有一根数据线。

单工, 例如说把 USART 的RX去掉,那它就不能实现数据的接收,那它就是一个单工的。

时钟

同步 , 同步时钟能够通过时钟线来进行采样。
异步, 通过特定的采样频率来进行采样,并且需要设置帧头帧尾来进行数据的对齐。

电平

单端,引脚的高低电平是相对于GND而言的,所以通信双方需要进行共地才能进行通信。

双端 , 是靠两个差分引脚的电压差来进行信号传输的。一般双端通信具有比较好的抗干扰特性传输速度

设备

点对点,就是两个设备一对一通信。

多设备, 一个设备同时对多个设备进行传输,需要一个寻址的过程,来确定接收对象。

串口通信

串口通信概述

串口通信(Serial Communication)是嵌入式系统中最常用的通信方式之一,其核心思想是通过逐位传输实现设备间的数据交互。在STM32微控制器中,UART(Universal Asynchronous Receiver/Transmitter)模块承担了异步串行通信的核心功能,具有全双工、异步、高可靠性等特点,广泛应用于传感器通信、模块调试、设备控制等场景。

串口通信核心工作原理

1. 物理层基础

电平标准:常见的有TTL电平(3.3V / 5V)和RS-232电平(±12V)

连接方式:采用三线制(TX发送、RX接收、GND地线 )实现全双工通信

TX 和 RX需要反接

2. 串口参数及时序

波特率: 串口通信的速率

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

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

校验位: 用于数据验证,根据数据位计算得来

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

再额外说一下校验位,它与数据有关.

例如说选择奇校验数据位加上校验位的 1 数量为奇数

数据位:10001110 ------ 1有4个,是偶数 ------那么校验位为 1

数据位:10001100 ------ 1有3个,是奇数 ------那么校验位为 0

偶校验则反之。

STM32的 USART 外设

前面所说的都是 USART协议 ,然后下面来详细说一下在stm32中的 USART外设 ,来看看在stm32中怎么去实现和使用USART来进行通讯。

简介

USART是STM32内部集成的硬件外设,可根据数据寄存器的一个字节数据自动生成数据帧时序,从TX引脚发送出去,也可自动接收RX引脚的数据帧时序,拼接为一个字节数据,存放在数据寄存器里。

自带波特率发生器,最高达4.5Mbits/s

可配置数据位长度(8/9)、停止位长度(0.5/1/1.5/2)

可选校验位(无校验/奇校验/偶校验)

支持同步模式、硬件流控制、DMA、智能卡、IrDA、LIN

STM32F103C8T6 USART资源: USART1、 USART2、 USART3

硬件连接

这里就以 STM32 与 PC通信为例。

需要使用的硬件设备有: stm32f103c8t6最小系统板,一台电脑, st-link, USB转串口,几根杜邦线。

stm32 usb转串口
PA9(TX) RX
PA10(RX) TX

也就是stm32的 USART1 的两个引脚连到 usb转串口上,记得要反接。

串口发送

  1. 使能外设时钟:开启 USART 和对应 GPIO 的时钟
c 复制代码
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
  1. 配置 GPIO 引脚:将 TX 引脚设置为复用推挽输出
c 复制代码
	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);	
  1. 初始化 USART 参数:设置波特率、数据位、停止位等
c 复制代码
	/*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;	//停止位,选择1位
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;		//字长,选择8位
	USART_Init(USART1, &USART_InitStructure);				//将结构体变量交给USART_Init,配置USART1
  1. 实现发送功能:通过查询或中断方式发送数据
c 复制代码
void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1, Byte);		//将字节数据写入数据寄存器,写入后USART自动生成时序波形
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);	//等待发送完成
	/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}

因为stm库函数实现了UART发送一个字符,我们直接调用就能直接实现需求,只需要等到标志位变回SET,即可。

然后我们要发送数组只需要套一层循环即可。

c 复制代码
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
	uint16_t i;
	for (i = 0; i < Length; i ++)		//遍历数组
	{
		Serial_SendByte(Array[i]);		//依次调用Serial_SendByte发送每个字节数据
	}
}

发送字符数组:

c 复制代码
void Serial_SendString(char *String)
{
	uint8_t i;
	for (i = 0; String[i] != '\0'; i ++)//遍历字符数组(字符串),遇到字符串结束标志位后停止
	{
		Serial_SendByte(String[i]);		//依次调用Serial_SendByte发送每个字节数据
	}
}

完整的串口发送代码

serial.c

c 复制代码
#include "stm32f10x.h"                  // Device header
#include <stdio.h>
#include <stdarg.h>

/**
  * 函    数:串口初始化
  * 参    数:无
  * 返 回 值:无
  */
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;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA9引脚初始化为复用推挽输出
	
	/*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;	//停止位,选择1位
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;		//字长,选择8位
	USART_Init(USART1, &USART_InitStructure);				//将结构体变量交给USART_Init,配置USART1
	
	/*USART使能*/
	USART_Cmd(USART1, ENABLE);								//使能USART1,串口开始运行
}

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

/**
  * 函    数:串口发送一个数组
  * 参    数:Array 要发送数组的首地址
  * 参    数:Length 要发送数组的长度
  * 返 回 值:无
  */
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
	uint16_t i;
	for (i = 0; i < Length; i ++)		//遍历数组
	{
		Serial_SendByte(Array[i]);		//依次调用Serial_SendByte发送每个字节数据
	}
}

/**
  * 函    数:串口发送一个字符串
  * 参    数:String 要发送字符串的首地址
  * 返 回 值:无
  */
void Serial_SendString(char *String)
{
	uint8_t i;
	for (i = 0; String[i] != '\0'; i ++)//遍历字符数组(字符串),遇到字符串结束标志位后停止
	{
		Serial_SendByte(String[i]);		//依次调用Serial_SendByte发送每个字节数据
	}
}

/**
  * 函    数:次方函数(内部使用)
  * 返 回 值:返回值等于X的Y次方
  */
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
	uint32_t Result = 1;	//设置结果初值为1
	while (Y --)			//执行Y次
	{
		Result *= X;		//将X累乘到结果
	}
	return Result;
}

/**
  * 函    数:串口发送数字
  * 参    数:Number 要发送的数字,范围:0~4294967295
  * 参    数:Length 要发送数字的长度,范围:0~10
  * 返 回 值:无
  */
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;
}

/**
  * 函    数:自己封装的prinf函数
  * 参    数:format 格式化字符串
  * 参    数:... 可变的参数列表
  * 返 回 值:无
  */
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);		//串口发送字符数组(字符串)
}

serial.h

c 复制代码
#ifndef __SERIAL_H
#define __SERIAL_H

#include <stdio.h>

void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array, uint16_t Length);
void Serial_SendString(char *String);
void Serial_SendNumber(uint32_t Number, uint8_t Length);
void Serial_Printf(char *format, ...);

#endif

串口接收

当外部设备通过串口发送 1 个字节到 STM32 时,数据会被存入 USART 的接收数据寄存器(RDR),此时 RXNE 标志自动置 1。

若提前配置了 "允许 RXNE 中断",则 RXNE=1 时会触发中断请求,CPU 会跳转到对应的中断服务函数执行。

在中断服务函数中,需读取 RDR 中的数据(读取后 RXNE 标志会自动清零,或手动清零),否则会持续触发中断。

中断服务配置

c 复制代码
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;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);					//将PA9引脚初始化为复用推挽输出
	
	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初始化*/
	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;	//停止位,选择1位
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;		//字长,选择8位
	USART_Init(USART1, &USART_InitStructure);				//将结构体变量交给USART_Init,配置USART1
	
	/*中断输出配置*/
	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线路使能
	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,串口开始运行
}

在原本发送的初始化中添加NVIC配置,也就是配置中断。

下面以 USART1 为例,注意选择需要使用的中断服务函数命名

c 复制代码
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标志位
																//读取数据寄存器会自动清除此标志位
																//如果已经读取了数据寄存器,也可以不执行此代码
	}
}

这时候我们的接收内容就存储到了Serial_RxData 里了。

获取接收内容

下面我们只需要在主函数中读取Serial_RxData 的内容。

在Serial.c里写两个函数,分别返回Serial_RxData 和Serial_RxFlag 的值 。

c 复制代码
uint8_t Serial_GetRxFlag(void)
{
	if (Serial_RxFlag == 1)			//如果标志位为1
	{
		Serial_RxFlag = 0;
		return 1;					//则返回1,并自动清零标志位
	}
	return 0;						//如果标志位为0,则返回0
}
c 复制代码
uint8_t Serial_GetRxData(void)
{
	return Serial_RxData;			//返回接收的数据变量
}

然后再main.c里面判断 Serial_RxFlag ,为 1 既是读取到了新的内容,再将 Serial_RxData 的值传输出来即可。

c 复制代码
	while (1)
	{
		if (Serial_GetRxFlag() == 1)			//检查串口接收数据的标志位
		{
			RxData = Serial_GetRxData();		//获取串口接收的数据
			Serial_SendByte(RxData);			//串口将收到的数据回传回去,用于测试
			OLED_ShowHexNum(1, 8, RxData, 2);	//显示串口接收的数据
		}
	}

进阶 ------ 传输字符串

传输字符串和单个字符的差别就在于数量,我们只需要 判定字符串头尾,然后将字符串内容都保存下来,然后传递给主函数即可。

首先,修改中断服务函数。

c 复制代码
void USART1_IRQHandler(void)
{
	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 == '@' && Serial_RxFlag == 0)		//如果数据确实是包头,并且上一个数据包已处理完毕
			{
				RxState = 1;			//置下一个状态
				pRxPacket = 0;			//数据包的位置归零
			}
		}
		/*当前状态为1,接收数据包数据,同时判断是否接收到了第一个包尾*/
		else if (RxState == 1)
		{
			if (RxData == '\r')			//如果收到第一个包尾
			{
				RxState = 2;			//置下一个状态
			}
			else						//接收到了正常的数据
			{
				Serial_RxPacket[pRxPacket] = RxData;		//将数据存入数据包数组的指定位置
				pRxPacket ++;			//数据包的位置自增
			}
		}
		/*当前状态为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);		//清除标志位
	}
}

然后在 Serail.h 中加入 ------

c 复制代码
extern char Serial_RxPacket[];
extern uint8_t Serial_RxFlag;

就是共同使用同一个变量。

同样,也要在Serial.c 中加入变量声明 ------

c 复制代码
char Serial_RxPacket[100];				//定义接收数据包数组,数据包格式"@MSG\r\n"
uint8_t Serial_RxFlag;					//定义接收数据包标志位

尾声

OK,串口的大致内容就是这些,如果有问题,可以私信 或者 评论,我会尽我所能帮助大家,需要源代码也是同样可以 私信 或者 评论。

感谢大伙观看,别忘了三连支持一下

大家也可以关注一下我的其它专栏,同样精彩喔~

下期见咯~

相关推荐
sheepwjl4 小时前
《嵌入式硬件(十二):基于IMX6ULL的时钟操作》
汇编·arm开发·单片机·嵌入式硬件·时钟·.s编译
智者知已应修善业5 小时前
【51单片机单按键控制2个LED循环闪烁】2022-12-7
c语言·经验分享·笔记·嵌入式硬件·51单片机
物随心转6 小时前
ARM的TrustZone
嵌入式硬件
风_峰6 小时前
PuTTY软件访问ZYNQ板卡的Linux系统
linux·服务器·嵌入式硬件·fpga开发
田甲7 小时前
【STM32】串口的阻塞、中断、DMA收发
stm32·单片机·嵌入式硬件
酷~8 小时前
单片机启动文件——数据段重定位,BSS段清零
单片机·嵌入式硬件
wotaifuzao8 小时前
单片机的RAM与ROM概念
单片机·嵌入式硬件
jz-炸芯片的zero8 小时前
【Zephyr电源与功耗专题】14_BMS电池管理算法(三重验证机制实现高精度电量估算)
单片机·物联网·算法·zephyr·bms电源管理算法
三佛科技-134163842128 小时前
蒸面器/蒸脸仪方案开发,蒸面器/蒸脸仪MCU控制方案分析
单片机·嵌入式硬件