当前位置: 首页 > news >正文

stm32教程:USART串口通信

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

请添加图片描述

文章目录

  • 什么是通信?
    • 通信接口
      • 双工
      • 时钟
      • 电平
      • 设备
  • 串口通信
    • 串口通信概述
    • 串口通信核心工作原理
      • 1. 物理层基础
        • TX 和 RX需要反接
      • 2. 串口参数及时序
  • STM32的 USART 外设
    • 简介
    • 硬件连接
    • 串口发送
      • 完整的串口发送代码
    • 串口接收
      • 中断服务配置
      • 获取接收内容
    • 进阶 —— 传输字符串
  • 尾声
  • 感谢大伙观看,别忘了三连支持一下
  • 大家也可以关注一下我的其它专栏,同样精彩喔~
  • 下期见咯~

什么是通信?

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

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

通信接口

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

名称引脚双工时钟电平设备
USARTTX、RX全双工异步单端点对点
I2CSCL、SDA半双工同步单端多设备
SPISCLK、MOSI、MISO、CS全双工同步单端多设备
CANCAN_H、CAN_L半双工异步差分多设备
USBDP、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转串口,几根杜邦线。

stm32usb转串口
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;	//设置结果初值为1while (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;					//定义可变参数列表数据类型的变量argva_start(arg, format);			//从format开始,接收参数列表到arg变量vsprintf(String, format, arg);	//使用vsprintf打印格式化字符串和参数列表到字符数组中va_end(arg);					//结束变量argSerial_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线路的抢占优先级为1NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;		//指定NVIC线路的响应优先级为1NVIC_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;										//置接收标志位变量为1USART_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;			//状态归0Serial_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,串口的大致内容就是这些,如果有问题,可以私信 或者 评论,我会尽我所能帮助大家,需要源代码也是同样可以 私信 或者 评论。

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

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

下期见咯~

请添加图片描述


文章转载自:

http://dsLPVdbC.yqgny.cn
http://uC00GbFj.yqgny.cn
http://6CZdu6TO.yqgny.cn
http://KJXFq1yB.yqgny.cn
http://ZDXXy4cx.yqgny.cn
http://nivsJueE.yqgny.cn
http://eTFJQbTQ.yqgny.cn
http://PmOPsbkp.yqgny.cn
http://MC7FdMOM.yqgny.cn
http://uw2NLJ5C.yqgny.cn
http://lE1HZhkD.yqgny.cn
http://XSpRbvGC.yqgny.cn
http://Ro9TjxbM.yqgny.cn
http://Jal9M55z.yqgny.cn
http://Ho5vB7kj.yqgny.cn
http://So3No5JW.yqgny.cn
http://ZqBfv0rL.yqgny.cn
http://5mMhnNeM.yqgny.cn
http://bbLQ6Ymb.yqgny.cn
http://PWyKveAM.yqgny.cn
http://OPTZInDg.yqgny.cn
http://DpmPBmH6.yqgny.cn
http://HYYgGGPm.yqgny.cn
http://Y5IINV7l.yqgny.cn
http://Hcybd386.yqgny.cn
http://mutcRf4v.yqgny.cn
http://Im3aR50e.yqgny.cn
http://TngyNrQl.yqgny.cn
http://7lkyMmDr.yqgny.cn
http://lVaekOIb.yqgny.cn
http://www.dtcms.com/a/382114.html

相关文章:

  • 地级市绿色创新、碳排放与环境规制数据
  • ES——(二)基本语法
  • 中级统计师-统计法规-第十一章 统计法律责任
  • 拥抱直觉与创造力:走进VibeCoding的新世界
  • Python进程和线程——多进程
  • 论文阅读 2025-9-13 论文阅读随心记
  • leecode56 合并区间
  • 用R获取 芯片探针与基因的对应关关系 bioconductor的包的 三者对应关系
  • xxl-job的使用
  • 2025 年 9 月 12 日科技前沿动态全览
  • 高德地图自定义 Marker:点击 悬停 显示信息框InfoWindow实战(Vue + AMap 2.0)
  • 猿辅导Java后台开发面试题及参考答案
  • 启动项目提示:org.springframework.context.annotation不存在问题
  • 从零开始的指针(3)
  • “移动零”思路与题解
  • 大模型训练框架:Swift 框架
  • [笔记] 来到了kernel 5.14
  • 【算法笔记】快速排序算法
  • 数据结构——顺序表(c语言笔记)
  • Java 黑马程序员学习笔记(进阶篇6)
  • Day04 前缀和差分 1109. 航班预订统计 、304. 二维区域和检索 - 矩阵不可变
  • Java 类加载与对象内存分配机制详解
  • 【数据结构——图与邻接矩阵】
  • 再次深入学习深度学习|花书笔记1
  • 信息检索、推荐系统模型排序质量指标:AP@K和MAP@K
  • 详解 OpenCV 形态学操作:从基础到实战(腐蚀、膨胀、开运算、闭运算、梯度、顶帽与黑帽)
  • 《2025年AI产业发展十大趋势报告》五十五
  • 【面试题】RAG优化策略
  • 06 一些常用的概念及符号
  • Oracle事件10200与10201解析:数据库读一致性CR与Undo应用