前言
在嵌入式开发中,串口通信是最常用的调试与数据传输方式之一。UART(Universal Asynchronous Receiver/Transmitter,通用异步收发传输器)作为一种简单、可靠的异步通信协议,被广泛应用于STM32与传感器、上位机、蓝牙模块等外设的交互场景。本文将从协议基础到STM32实战,全面解析UART协议在STM32中的应用,包含硬件设计、软件配置、实战案例及调试技巧,适合嵌入式开发者入门与进阶参考。
一、UART协议基础
1.1 什么是UART?
UART是一种通用的异步串行通信协议,用于在两个设备之间实现全双工或半双工数据传输。其核心特点是“异步”——通信双方无需共享统一的时钟信号,而是通过约定的波特率(数据传输速率)同步数据帧,因此硬件上只需2根线(TX发送、RX接收)即可实现双向通信(全双工)。
与UART容易混淆的是USART(Universal Synchronous/Asynchronous Receiver/Transmitter),两者的区别在于:
- UART仅支持异步通信;
- USART既支持异步通信,也支持同步通信(需额外时钟线SCLK)。
STM32芯片中集成的是USART外设,但在大多数场景下,我们使用其异步模式(即UART功能),因此本文统一以“UART”代指STM32中基于USART外设的异步通信。
1.2 UART通信帧结构
UART的数据以“帧”为单位传输,每帧包含以下部分(从左到右传输):
(示意图:起始位+数据位+校验位+停止位)
- 空闲状态:通信线默认处于高电平(逻辑1),表示无数据传输。
- 起始位:1位低电平(逻辑0),标志一帧数据的开始,用于唤醒接收设备同步。
- 数据位:5~9位,存储实际传输的数据(通常为8位,即1字节),可选择高位在前或低位在前(STM32默认低位在前)。
- 校验位(可选):1位,用于数据校验,确保传输准确性:
- 无校验(None):不使用校验位;
- 奇校验(Odd):数据位+校验位中1的总数为奇数;
- 偶校验(Even):数据位+校验位中1的总数为偶数;
- 标记校验(Mark):校验位固定为1;
- 空格校验(Space):校验位固定为0。
- 停止位:1~2位高电平(逻辑1),标志一帧数据的结束,可根据需求选择1位、1.5位(仅异步模式)或2位。
1.3 波特率与通信速率
波特率(Baud Rate)是UART通信的核心参数,定义为每秒传输的“码元数”(对于UART,每个码元对应1位数据),单位为bps(比特每秒)。例如:
- 9600 bps:每秒传输9600位数据;
- 115200 bps:每秒传输115200位数据(常用调试速率)。
通信双方必须使用相同的波特率,否则会出现数据解析错误。实际应用中,常用波特率为9600、19200、38400、57600、115200、256000等。
1.4 UART通信方式
- 全双工:TX和RX独立工作,发送和接收可同时进行(如STM32与PC通过USB转串口通信)。
- 半双工:同一时刻只能发送或接收,需通过一根线实现(如STM32的单线半双工模式)。
二、STM32 USART外设介绍
STM32系列芯片(如F1、F4、H7等)通常集成多个USART外设(如F103有3个USART和2个UART,F407有6个USART),不同型号资源不同,需参考对应的数据手册。
2.1 USART外设主要特性
STM32的USART外设支持以下关键特性(以F103为例):
- 支持异步通信(UART)和同步通信;
- 波特率范围:1200 bps ~ 4.5 Mbps(F103,取决于时钟频率);
- 支持5/6/7/8/9位数据位;
- 支持奇校验、偶校验或无校验;
- 支持1/1.5/2位停止位;
- 支持硬件流控制(RTS/CTS,需额外引脚);
- 支持中断和DMA传输(减少CPU占用);
- 支持多机通信(地址检测模式);
- 支持单线半双工模式;
- 支持智能卡协议和IrDA红外通信(部分型号)。
2.2 引脚映射
USART外设的TX/RX引脚需通过“复用功能”配置,不同型号的引脚映射不同。以STM32F103C8T6为例,USART1的默认引脚为:
- TX:PA9(复用推挽输出);
- RX:PA10(浮空输入或上拉输入)。
若默认引脚被占用,可通过“重映射”功能切换到其他引脚(如USART1可重映射到PB6/PB7),具体需参考数据手册的“复用功能映射表”。
2.3 时钟源
USART的时钟来自APB总线(APB1或APB2):
- 高速USART(如USART1)通常挂载在APB2总线上,时钟频率较高(F103中APB2最高72MHz);
- 其他USART(如USART2、3)挂载在APB1总线上(F103中APB1最高36MHz)。
时钟频率直接影响波特率精度,需根据总线时钟计算波特率寄存器的值。
2.4 核心寄存器
USART的配置主要通过以下寄存器实现(以寄存器级编程为例):
USART_CR1(控制寄存器1):
- UE:USART使能位(1=使能);
- M:数据位长度(0=8位,1=9位);
- PCE:校验位使能(1=使能);
- PS:校验类型(0=偶校验,1=奇校验);
- TE:发送使能(1=使能);
- RE:接收使能(1=使能);
- RXNEIE:接收非空中断使能(1=使能)。
USART_CR2(控制寄存器2):
- STOP:停止位配置(00=1位,01=0.5位,10=2位,11=1.5位)。
USART_CR3(控制寄存器3):
- CTSIE:CTS中断使能;
- DMAT:发送DMA使能;
- DMAR:接收DMA使能。
USART_BRR(波特率寄存器):
- 由16位整数部分(DIV_Mantissa)和4位小数部分(DIV_Fraction)组成,用于计算波特率。
USART_SR(状态寄存器):
- TXE:发送数据寄存器空(1=可发送下一字节);
- TC:发送完成(1=一帧数据发送完毕);
- RXNE:接收数据寄存器非空(1=有数据待读取)。
USART_DR(数据寄存器):
- 发送时写入数据,接收时读取数据(8位或9位)。
2.5 波特率计算
波特率由USART_BRR寄存器的值和APB总线时钟共同决定,公式如下:
波特率 = APB总线时钟频率 / (16 * USARTDIV)
其中,USARTDIV = DIV_Mantissa + DIV_Fraction / 16(DIV_Mantissa为整数部分,DIV_Fraction为小数部分,0~15)。
例如,若APB2时钟为72MHz,需配置波特率为115200:
USARTDIV = 72,000,000 / (16 * 115200) ≈ 39.0625
→ DIV_Mantissa = 39(0x27),DIV_Fraction = 1(0x1)
→ USART_BRR = 0x271
STM32CubeMX会自动计算BRR值,手动配置时需确保误差≤3%(否则可能通信失败)。
三、硬件电路设计
UART通信的硬件电路简单,核心是“电平匹配”和“抗干扰”。
3.1 电平标准
STM32的GPIO为3.3V电平,而PC的串口(RS232)为±15V电平,直接连接会损坏芯片,因此需通过“电平转换芯片”(如CH340、PL2303)实现3.3V与RS232/USB的转换。
典型电路如下:
- STM32的TX(3.3V)连接到CH340的RXD;
- STM32的RX(3.3V)连接到CH340的TXD;
- CH340通过USB线连接到PC;
- 共地:STM32与CH340的GND必须连接(确保电平参考一致)。
3.2 抗干扰设计
- 若通信距离较长(>1米),可在TX/RX线上串联100Ω电阻(限流保护);
- 可并联100pF电容(滤除高频噪声);
- 远距离通信建议使用差分信号(如RS485),通过MAX485等芯片转换。
3.3 流控制(可选)
若需硬件流控制(防止数据溢出),需增加RTS(请求发送)和CTS(清除发送)引脚:
- STM32的RTS(输出)连接到外设的CTS(输入);
- STM32的CTS(输入)连接到外设的RTS(输出)。
大多数场景下无需流控制,可省略这两根线。
四、软件配置步骤
本节以STM32F103为例,分别介绍寄存器级和HAL库的配置方法,实现UART基本通信。
4.1 寄存器级配置(USART1,115200 8N1)
“8N1”是最常用的配置:8位数据位,无校验位,1位停止位。
步骤1:使能时钟
// 使能GPIOA和USART1时钟(APB2总线)
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN;
步骤2:配置GPIO
// 配置PA9为复用推挽输出(TX)
GPIOA->CRH &= ~(GPIO_CRH_MODE9 | GPIO_CRH_CNF9);
GPIOA->CRH |= GPIO_CRH_MODE9_1 | GPIO_CRH_MODE9_0; // 输出速率50MHz
GPIOA->CRH |= GPIO_CRH_CNF9_1; // 复用推挽
// 配置PA10为浮空输入(RX)
GPIOA->CRH &= ~(GPIO_CRH_MODE10 | GPIO_CRH_CNF10);
GPIOA->CRH |= GPIO_CRH_CNF10_0; // 浮空输入
步骤3:配置USART参数
// 复位USART1(可选,确保初始状态)
USART1->CR1 &= ~USART_CR1_UE;
// 配置数据位(8位)、无校验、使能发送和接收
USART1->CR1 &= ~(USART_CR1_M | USART_CR1_PCE); // 8位数据,无校验
USART1->CR1 |= USART_CR1_TE | USART_CR1_RE; // 使能发送和接收
// 配置停止位(1位)
USART1->CR2 &= ~USART_CR2_STOP; // 1位停止位
// 配置波特率(115200,APB2时钟72MHz)
USART1->BRR = 0x271; // 计算值39.0625
// 使能USART1
USART1->CR1 |= USART_CR1_UE;
步骤4:发送与接收函数
// 发送1字节
void UART1_SendByte(uint8_t data) {
while (!(USART1->SR & USART_SR_TXE)); // 等待发送寄存器为空
USART1->DR = data; // 写入数据
}
// 接收1字节(查询方式)
uint8_t UART1_RecvByte(void) {
while (!(USART1->SR & USART_SR_RXNE)); // 等待接收数据
return USART1->DR; // 读取数据
}
// 发送字符串
void UART1_SendString(uint8_t *str) {
while (*str) {
UART1_SendByte(*str++);
}
}
4.2 HAL库配置(基于STM32CubeMX)
步骤1:创建工程
- 打开STM32CubeMX,选择芯片型号(如STM32F103C8T6);
- 配置RCC:选择HSE时钟(外部晶振),配置系统时钟为72MHz。
步骤2:配置USART1
- 在“Pinout & Configuration”中,左侧选择“Connectivity”→“USART1”;
- 模式选择“Asynchronous”(异步模式);
- 参数配置:
- Baud Rate:115200;
- Word Length:8 Bits;
- Parity:None;
- Stop Bits:1;
- Flow Control:None;
- 自动生成引脚(PA9/TX,PA10/RX),若需重映射可在“System Core”→“GPIO”中修改。
步骤3:生成代码
- 配置工程路径和IDE(如Keil MDK);
- 勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”;
- 点击“GENERATE CODE”生成工程。
步骤4:发送与接收函数
HAL库提供了以下核心函数:
// 发送1字节(阻塞式)
HAL_UART_Transmit(&huart1, &data, 1, 100); // 超时100ms
// 接收1字节(阻塞式)
HAL_UART_Receive(&huart1, &data, 1, 100); // 超时100ms
// 发送字符串
void UART1_SendString(uint8_t *str) {
HAL_UART_Transmit(&huart1, str, strlen((char*)str), 100);
}
五、实战案例
5.1 案例1:串口回环测试(查询方式)
功能:STM32接收PC发送的数据,并原样返回。
int main(void) {
HAL_Init();
SystemClock_Config(); // 系统时钟配置(CubeMX生成)
MX_USART1_UART_Init(); // USART1初始化(CubeMX生成)
uint8_t data;
while (1) {
// 接收1字节
HAL_UART_Receive(&huart1, &data, 1, 1000);
// 发送接收到的字节
HAL_UART_Transmit(&huart1, &data, 1, 100);
}
}
测试方法:
- 用USB线连接CH340到PC,打开串口助手(如XCOM);
- 配置波特率115200,无校验,1停止位;
- 发送任意字符,应收到相同字符。
5.2 案例2:中断接收(非阻塞)
查询方式会阻塞CPU,中断方式更高效。配置步骤:
步骤1:使能接收中断(CubeMX)
- 在USART1配置中,勾选“NVIC Settings”→“Enabled”(使能中断);
- 生成代码后,HAL库会自动配置中断向量表。
步骤2:重写中断回调函数
uint8_t rx_data; // 全局变量,存储接收数据
// 中断回调函数(HAL库自动调用)
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart == &huart1) {
// 发送接收到的数据(回显)
HAL_UART_Transmit(&huart1, &rx_data, 1, 100);
// 重新开启中断接收(单次中断需手动重启)
HAL_UART_Receive_IT(&huart1, &rx_data, 1);
}
}
int main(void) {
HAL_Init();
SystemClock_Config();
MX_USART1_UART_Init();
// 开启中断接收
HAL_UART_Receive_IT(&huart1, &rx_data, 1);
while (1) {
// 主循环可执行其他任务
}
}
5.3 案例3:DMA传输(高效批量收发)
DMA(直接存储器访问)可实现数据在USART与内存间的直接传输,无需CPU干预,适合大数据量传输。
步骤1:配置DMA(CubeMX)
- 在USART1配置中,“DMA Settings”→“Add”:
- 发送:Stream选择“DMA1 Stream4”(USART1_TX对应DMA1_Stream4),方向“Memory to Peripheral”;
- 接收:Stream选择“DMA1 Stream5”(USART1_RX对应DMA1_Stream5),方向“Peripheral to Memory”;
- 模式选择“Normal”(单次传输)或“Circular”(循环传输)。
步骤2:DMA发送示例
uint8_t tx_buf[] = "Hello, DMA!\r\n";
int main(void) {
HAL_Init();
SystemClock_Config();
MX_USART1_UART_Init();
MX_DMA_Init(); // CubeMX生成的DMA初始化
// DMA发送(非阻塞)
HAL_UART_Transmit_DMA(&huart1, tx_buf, sizeof(tx_buf)-1);
while (1) {
// 等待发送完成(可选)
if (HAL_UART_GetState(&huart1) == HAL_UART_STATE_READY) {
// 发送完成后可执行其他操作
}
}
}
步骤3:DMA循环接收(固定长度)
uint8_t rx_buf[10]; // 接收缓冲区
int main(void) {
HAL_Init();
SystemClock_Config();
MX_USART1_UART_Init();
MX_DMA_Init();
// 开启DMA循环接收(每次接收10字节)
HAL_UART_Receive_DMA(&huart1, rx_buf, 10);
while (1) {
// 若需处理数据,可在回调函数中实现
}
}
// DMA接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
if (huart == &huart1) {
// 处理接收数据(如打印)
HAL_UART_Transmit_DMA(&huart1, rx_buf, 10);
// 循环模式下无需手动重启,自动重新填充缓冲区
}
}
5.4 案例4:printf重定向(调试信息输出)
将标准库的printf函数重定向到UART,方便输出调试信息。
步骤1:重写fputc函数(HAL库)
#include <stdio.h>
// 重定向printf到USART1
int fputc(int ch, FILE *f) {
HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, 100);
return ch;
}
步骤2:配置Keil(可选)
- 勾选“Use Micro LIB”(工程→Options→Target→Use Micro LIB),否则可能编译报错。
步骤3:使用示例
int main(void) {
// 初始化代码...
int temp = 25;
printf("Temperature: %d℃\r\n", temp); // 输出到串口
}
六、高级功能应用
6.1 单线半双工模式
在资源紧张时,可通过1根线实现发送和接收(同一时刻只能单向传输)。
配置步骤(寄存器级):
// 使能单线半双工模式(CR3寄存器)
USART1->CR3 |= USART_CR3_HDSEL; // 单线模式使能
// 引脚配置为复用推挽输出(同一引脚既作TX也作RX)
GPIOA->CRH &= ~(GPIO_CRH_MODE9 | GPIO_CRH_CNF9);
GPIOA->CRH |= GPIO_CRH_MODE9_1 | GPIO_CRH_MODE9_0 | GPIO_CRH_CNF9_1;
发送时直接调用发送函数,接收时需先切换为输入模式(或通过中断自动切换)。
6.2 多机通信(地址检测)
多个从机通过UART总线连接到主机,主机通过地址帧选择目标从机。
配置步骤:
- 从机使能地址检测:
USART1->CR1 |= USART_CR1_MME;
(多机模式使能); - 主机发送地址帧(第9位为1),从机仅响应匹配的地址;
- 地址匹配后,从机进入数据接收模式。
七、常见问题与调试技巧
7.1 通信失败的常见原因
波特率错误:
- 检查时钟频率是否正确(如系统时钟配置错误);
- 重新计算BRR值(误差需≤3%)。
引脚配置错误:
- 确认TX/RX引脚是否接反;
- 复用功能是否正确(未配置为复用模式会导致无输出);
- 输入引脚是否为浮空或上拉(下拉可能导致误触发)。
校验位/停止位不匹配:
- 双方必须配置相同的校验位和停止位(如主机8N1,从机也需8N1)。
中断未使能:
- 中断接收需使能RXNEIE(CR1寄存器)和NVIC中断。
接地问题:
- 未共地会导致电平参考不一致,通信不稳定。
7.2 调试工具与方法
串口助手:
- 用XCOM、SSCOM等工具发送数据,验证STM32的接收功能;
- 查看STM32发送的数据是否正确。
示波器/逻辑分析仪:
- 测量TX引脚波形,检查是否有信号输出;
- 观察波特率是否正确(1位时间=1/波特率,如115200对应约8.68μs)。
printf调试:
- 在关键步骤输出变量值(如“已进入中断”“接收数据:0xXX”)。
DMA调试:
- 检查DMA通道是否正确;
- 用
HAL_DMA_GetState()
查看DMA状态。
7.3 提升通信可靠性的技巧
添加帧头帧尾:
- 数据帧格式:
0xAA + 长度 + 数据 + 校验和 + 0x55
,避免数据粘连。
- 数据帧格式:
软件滤波:
- 对连续接收的相同数据进行校验(如连续3次相同才确认有效)。
超时处理:
- 接收时设置超时时间,超过时间未收到完整数据则丢弃。
使用中断+缓冲区:
- 中断接收数据到环形缓冲区,主程序从缓冲区读取,避免数据丢失。
八、总结与扩展
UART作为STM32中最基础的通信方式,是嵌入式开发的必备技能。本文从协议基础到实战案例,覆盖了UART的核心知识点,包括:
- UART协议的帧结构与波特率;
- STM32 USART外设的配置与寄存器;
- 硬件电路设计与电平转换;
- 寄存器级、HAL库、中断、DMA的实现方法;
- 常见问题与调试技巧。
实际开发中,需根据场景选择合适的传输方式(查询/中断/DMA),并注重通信的可靠性设计。进阶学习可探索:
- 低功耗模式下的UART唤醒;
- 高速波特率(如2Mbps)的稳定性优化;
- 多UART外设的并发管理。
掌握UART后,可进一步学习I2C、SPI等其他通信协议,构建更复杂的嵌入式系统。
附录:常用代码片段
- 环形缓冲区(中断接收用):
#define BUF_SIZE 128
uint8_t uart_buf[BUF_SIZE];
uint8_t buf_head = 0, buf_tail = 0;
// 中断回调函数中写入缓冲区
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
uart_buf[buf_head] = rx_data;
buf_head = (buf_head + 1) % BUF_SIZE;
HAL_UART_Receive_IT(&huart1, &rx_data, 1);
}
// 读取缓冲区数据
uint8_t UART1_ReadBuf(uint8_t *data) {
if (buf_head == buf_tail) return 0; // 空
*data = uart_buf[buf_tail];
buf_tail = (buf_tail + 1) % BUF_SIZE;
return 1;
}
- 校验和计算:
// 计算字节数组的校验和(简单累加)
uint8_t CheckSum(uint8_t *data, uint8_t len) {
uint8_t sum = 0;
for (uint8_t i = 0; i < len; i++) {
sum += data[i];
}
return sum;
}