stm32教程:USART串口通信

发布于:2025-09-15 ⋅ 阅读:(21) ⋅ 点赞:(0)

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

请添加图片描述

什么是通信?

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

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

通信接口

在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 的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
  1. 配置 GPIO 引脚:将 TX 引脚设置为复用推挽输出
	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 参数:设置波特率、数据位、停止位等
	/*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. 实现发送功能:通过查询或中断方式发送数据
void Serial_SendByte(uint8_t Byte)
{
	USART_SendData(USART1, Byte);		//将字节数据写入数据寄存器,写入后USART自动生成时序波形
	while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);	//等待发送完成
	/*下次写入数据寄存器会自动清除发送完成标志位,故此循环后,无需清除标志位*/
}

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

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

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

发送字符数组:

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

完整的串口发送代码

serial.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

#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 标志会自动清零,或手动清零),否则会持续触发中断。

中断服务配置

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 为例,注意选择需要使用的中断服务函数命名

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 的值 。

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.c里面判断 Serial_RxFlag ,为 1 既是读取到了新的内容,再将 Serial_RxData 的值传输出来即可。

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

进阶 —— 传输字符串

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

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

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 中加入 ——

extern char Serial_RxPacket[];
extern uint8_t Serial_RxFlag;

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

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

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

尾声

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

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

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

下期见咯~

请添加图片描述