探秘 STM32 串口通信:从原理到实战的全方位指南
通用:UART 在工控、电力等诸多领域广泛应用,凭借其简单易用和兼容性强的特点,成为嵌入式系统中数据通信的常用选择。串行:与并行通信不同,UART 通信时处理器与外设仅需一根信号线相连,数据按位依次传输,且从低位开始。并行通信虽传输速度快,但需多根数据线,抗干扰能力弱、传输距离受限,相比之下,串行通信在抗干扰和长距离传输方面更具优势。异步:在计算机系统中,处理器与外设的数据处理速度差异显著,数据同
引言
在嵌入式开发的广阔天地里,STM32 单片机凭借其强大的性能与丰富的外设,成为众多开发者的得力助手。而串口通信(USART),作为 STM32 与外界沟通的关键桥梁,承载着数据传输的重要使命。无论是与 PC 端的交互调试,还是与各类传感器、执行器的协同工作,串口通信都发挥着不可或缺的作用。
想象一下,智能家居系统中,STM32 如何接收手机 APP 发送的指令来控制家电设备?工业自动化场景里,传感器采集的数据又怎样准确无误地传递给 STM32 进行分析处理?这背后,串口通信如同一位默默耕耘的信使,确保信息的顺畅流转。今天,就让我们一同深入探索串口通信的奥秘,解锁 STM32 与外界高效对话的密码。
常见的几种通信接口
| 连接对象 | 通信接口 | 功能简述 |
|---|---|---|
| LED 灯 / 蜂鸣器 | GPIO | 通过通用输入输出接口,处理器可控制 LED 灯的亮灭、蜂鸣器的发声等操作 ,实现简单的信号指示或报警功能。 |
| BT 蓝牙 / GPS | UART | 利用通用异步收发传输器接口,处理器与蓝牙模块通信可实现无线数据传输、设备配对等功能;与 GPS 模块通信可获取位置、时间等信息 。 |
| 加速度传感器 | I2C 总线 | 借助 I2C(Inter-Integrated Circuit)总线,处理器可从加速度传感器获取物体的加速度数据,用于运动检测、姿态识别等场景 。 |
| DS18B20 温度传感器 | 1-Wired 一线式总线 | 通过 1-Wired 一线式总线,处理器能够读取 DS18B20 温度传感器测量的温度数据,实现温度监测功能 。 |
| WIFI/Norflash | SPI 总线 | 利用 SPI(Serial Peripheral Interface)总线,处理器与 WIFI 模块通信可实现无线网络连接、数据收发;与 Norflash 通信可进行数据的存储和读取 。 |
串口定义
通用串行异步收发器
-
通用:UART 在工控、电力等诸多领域广泛应用,凭借其简单易用和兼容性强的特点,成为嵌入式系统中数据通信的常用选择。
-
串行:与并行通信不同,UART 通信时处理器与外设仅需一根信号线相连,数据按位依次传输,且从低位开始。并行通信虽传输速度快,但需多根数据线,抗干扰能力弱、传输距离受限,相比之下,串行通信在抗干扰和长距离传输方面更具优势。
-
异步:在计算机系统中,处理器与外设的数据处理速度差异显著,数据同步至关重要。异步通信通过特定协议实现数据传输,无需额外时钟信号线,而同步通信则需借助时钟信号线来协调双方数据传输节奏,以确保数据准确无误。
-
收发器:收发器作为数据发送与接收的硬件单元,在数据传输过程中,根据角色不同,处理器和外设可分别作为发送器或接收器。
UART 数据传输的协议
协议要素详解
-
空闲位:当处理器与外设不进行数据传输时,数据线上维持高电平的空闲位,为后续数据传输做好准备。
-
起始位:数据传输开始时,以低电平的起始位作为信号,标志着数据帧的开端。
-
数据位:代表实际传输的数据内容,可设置为 5 - 8 位,一般选用 8 位,通信双方的数据位长度需保持一致。
-
奇偶校验位:用于检测数据传输过程中是否出现错误,有奇校验、偶校验和无校验三种方式,双方校验方式必须相同。
-
停止位:数据传输结束时,发送高电平的停止位,停止位位数可为 1 位或 2 位,双方的停止位设置需一致。
-
波特率:作为衡量数据传输速率的关键参数,常见的波特率有 115200bps、9600bps 等,通信双方的波特率必须匹配,否则将导致数据传输错误。
协议流程示例
以处理器通过 UART 向 BT 发送数据 0x95(10010101)为例,UART 工作参数为 115200(波特率)、8(数据位数)、e(偶校验)、1(停止位位数),即 115200 8e1。其时序图如下:

数据传输协议流程为:空闲位 -> 起始位 -> 数据位 -> (校验位) -> 停止位 -> 空闲位。若要传输多个字节数据,流程则为:空闲位 -> 起始位 -> 低字节的 8 位数据 -> (校验位) -> 停止位 -> 起始位 -> 高字节的 8 位数据 -> (奇偶校验) -> 停止位... -> 空闲位。
UART 的三种工作方式
-
单工:数据传输仅能朝一个固定方向进行,适用于数据流向单一的场景。
-
半双工:数据可双向传输,但同一时刻只能进行单向传输,常见于一些对通信实时性要求不高的设备通信中。
-
全双工:允许数据同时双向传输,STM32 的 UART 通常工作在全双工模式,能高效实现数据的双向交互,满足大多数应用场景的需求。
硬件设计
硬件结构示意图

UART 控制器工作原理
-
发送数据流程:由于 CPU 核向数据寄存器 DR 写入数据的速度远快于发送移位寄存器将数据发送到 TX 引脚的速度,因此发送数据时,需先判断 TC 位(发送完成标志位),当 TC 为 1 表示发送数据寄存器为空,CPU 方可发送下一个数据,否则需轮询等待。若 TC 为 1,CPU 将数据写入数据寄存器 DR,随后数据自动拷贝至发送移位器,发送移位器按波特率和 UART 协议将数据发送到 TX 引脚上。
-
接收数据流程:接收移位器依据波特率和 UART 协议从 RX 引脚上接收数据,接收完成后数据自动拷贝至数据寄存器 DR。当 RXNE 位(接收数据寄存器非空标志位)为 1 时,表明数据寄存器 DR 中有数据,CPU 核可从中读取数据,否则 CPU 核需轮询等待。
简化硬件示意图

代码演示
串口发送功能代码
//usart.c
#include "usart.h"
void UART_Init(void){
//1.打开GPIOA/USART1控制器时钟 - APB2
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
//2.配置PA9 - 推挽复用输出,50MHz
GPIO_InitTypeDef GPIO_Config;
GPIO_Config.GPIO_Pin=GPIO_Pin_9;
GPIO_Config.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Config.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_Init(GPIOA,&GPIO_Config);
//3.配置PA10 - 浮空输入
GPIO_Config.GPIO_Pin=GPIO_Pin_10;
GPIO_Config.GPIO_Mode=GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA,&GPIO_Config);
//4.配置工作参数:115200 8n1
USART_InitTypeDef UART_Config;
UART_Config.USART_BaudRate=115200;
UART_Config.USART_WordLength=USART_WordLength_8b;
UART_Config.USART_Parity=USART_Parity_No;
UART_Config.USART_StopBits=USART_StopBits_1;
UART_Config.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;
UART_Config.USART_HardwareFlowControl=USART_HardwareFlowControl_None;
USART_Init(USART1,&UART_Config);
//5.使能串口
USART_Cmd(USART1,ENABLE);
}
void UART_Putc(char c){
//循环的判断
//TC,SET,可以发送;RESET,不能发送
while(USART_GetFlagStatus(USART1,USART_FLAG_TC)==RESET);
//将字符c放入到串口1的DR中
USART_SendData(USART1,c);
}
void UART_Puts(char* pstr){
while(*pstr){
UART_Putc(*pstr);
pstr++;
}
}
// main.c
#include "stm32f10x.h"
#include "led.h"
#include "beep.h"
#include "system.h"
#include "systick.h"
#include "key.h"
#include "exti.h"
#include "usart.h"
int main(void) {
//优先级分组 --|--
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
LED_Init(); // led初始化
BEEP_Init(); // beep初始化
Systick_Init();//systick定时器初始化
KEY_Init(); //按键初始化
My_EXTI_Init();//EXTI初始化
UART_Init();//串口初始化
while(1) {
UART_Puts("my heart will go on\n");
delay_ms(1000);
}
}
中断方式串口接收代码
//usart.c
#include "usart.h"
void UART_Init(void){
//1.打开GPIOA/USART1控制器时钟 - APB2
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1,ENABLE);
//2.配置PA9 - 推挽复用输出,50MHz
GPIO_InitTypeDef GPIO_Config;
GPIO_Config.GPIO_Pin=GPIO_Pin_9;
GPIO_Config.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Config.GPIO_Mode=GPIO_Mode_AF_PP;
GPIO_Init(GPIOA,&GPIO_Config);
//3.配置PA10 - 浮空输入
GPIO_Config.GPIO_Pin=GPIO_Pin_10;
GPIO_Config.GPIO_Mode=GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOA,&GPIO_Config);
//4.配置工作参数:115200 8n1
USART_InitTypeDef UART_Config;
UART_Config.USART_BaudRate=115200;
UART_Config.USART_WordLength=USART_WordLength_8b;
UART_Config.USART_Parity=USART_Parity_No;
UART_Config.USART_StopBits=USART_StopBits_1;
UART_Config.USART_Mode=USART_Mode_Rx|USART_Mode_Tx;
UART_Config.USART_HardwareFlowControl=USART_HardwareFlowControl_None;
USART_Init(USART1,&UART_Config);
//5.使能串口
USART_Cmd(USART1,ENABLE);
//6.配置NVIC支持串口1的中断
NVIC_InitTypeDef NVIC_Config;
NVIC_Config.NVIC_IRQChannel=USART1_IRQn;//串口1的中断通道
NVIC_Config.NVIC_IRQChannelPreemptionPriority=0;
NVIC_Config.NVIC_IRQChannelSubPriority=2;
NVIC_Config.NVIC_IRQChannelCmd=ENABLE;
NVIC_Init(&NVIC_Config);
//7.配置USART控制器支持RXNE中断
USART_ITConfig(USART1,USART_IT_RXNE,ENABLE);
}
void UART_Putc(char c){
//循环的判断
//TC,SET,可以发送;RESET,不能发送
while(USART_GetFlagStatus(USART1,USART_FLAG_TC)==RESET);
//将字符c放入到串口1的DR中
USART_SendData(USART1,c);
}
void UART_Puts(char* pstr){
while(*pstr){
UART_Putc(*pstr);
pstr++;
}
}
char UART_Getc(void){
// RXNE
//RESET,DR没有数据,陷入循环;SET,有数据,跳出循环
while(USART_GetFlagStatus(USART1,USART_FLAG_RXNE)==RESET);
//从串口1中获取一个字符
return (char)USART_ReceiveData(USART1);
}
//char buf[len];
//上位机 - "xxxx\r\n" -> "xxxx"
//"hello\r\n" -> "hello\0\n" -> "hello"
void UART_Gets(char* buf,u32 len){
int i=0;
for(i=0;i<(len-1);i++){
buf[i]=UART_Getc();
if(buf[i]=='\n')
break;
}
buf[i-1]='\0';
}
//interrupt.c
#include "interrupt.h"
//声明串口1的接收数组
char UART_RxBuff[UART_RXBUFF_SIZE];
//用于计数/下标
int UART_RxCounter=0;
//声明变量表示数据是否读取结束
int UART_Send_Flag=0;
//串口1的中断处理函数
void USART1_IRQHandler(void){
char c;
//判断是否是RXNE中断
if(USART_GetITStatus(USART1,USART_IT_RXNE)!=RESET){
//清除中断到来位
USART_ClearITPendingBit(USART1,USART_IT_RXNE);
//从DR寄存器中读取数据
c=USART_ReceiveData(USART1);
UART_RxBuff[UART_RxCounter++]=c;
if('\n'==c){//读取结束
UART_RxBuff[UART_RxCounter-2]='\0';
//将UART_RxCounter清0
UART_RxCounter=0;
//数据读取结束
UART_Send_Flag=1;
}
}
}
// main.c
#include "stm32f10x.h"
#include "led.h"
#include "beep.h"
#include "system.h"
#include "systick.h"
#include "key.h"
#include "exti.h"
#include "usart.h"
#include "string.h"//strcmp函数
#include "init.h"
#include "cmd.h"
#include "interrupt.h"
static cmd_t* pcmd;//定义变量存储find_cmd函数返回值
int main(void) {
//优先级分组 --|--
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
//总初始化函数
DEVICE_Init();
while(1) {
if(UART_Send_Flag){
UART_Puts("received command\n");
//buf="led on"/"led off"/"beep on"/"beep off"
//命令匹配
pcmd=find_cmd(UART_RxBuff);
if(pcmd != NULL)//匹配成功
pcmd->callback();
else
UART_Puts("invalid command");
UART_Puts("\n");
UART_Send_Flag=0;//读取完成标志为0
}
//业务逻辑
}
}
总结
通过本文,我们从串口的定义出发,详细剖析了 UART 数据传输协议、工作方式以及硬件设计原理,并通过代码演示展示了串口发送及中断方式接收数据的具体实现。串口通信作为 STM32 与外部设备交互的基础方式,其稳定性和高效性在嵌入式系统开发中至关重要。
在实际应用中,合理配置串口参数、优化数据收发逻辑,能确保数据准确无误地传输,为系统的稳定运行提供有力保障。同时,结合中断机制,可进一步提高系统的实时响应能力,满足多样化的应用需求。
最后
作为技术分享者,我始终希望用通俗易懂的语言和详细的代码示例,为大家呈现技术知识的精髓。但由于技术的复杂性和个人知识的局限性,文中可能存在不足或疏漏之处。真诚期待大家在评论区提出宝贵意见和建议,无论是对内容的疑问,还是对代码优化的见解,都欢迎分享。让我们携手在技术学习的道路上不断探索,共同进步!
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐



所有评论(0)