在嵌入式开发中,串口通信是最常用的调试与数据传输方式之一。UART(Universal Asynchronous Receiver/Transmitter,通用异步收发传输器)作为一种简单、可靠的异步通信协议,被广泛应用于STM32与传感器、上位机、蓝牙模块等外设的交互场景。本文将从协议基础到STM32实战,全面解析UART协议在STM32中的应用,包含硬件设计、软件配置、实战案例及调试技巧,适合嵌入式开发者入门与进阶参考。
UART是一种通用的异步串行通信协议,用于在两个设备之间实现全双工或半双工数据传输。其核心特点是“异步”——通信双方无需共享统一的时钟信号,而是通过约定的波特率(数据传输速率)同步数据帧,因此硬件上只需2根线(TX发送、RX接收)即可实现双向通信(全双工)。
与UART容易混淆的是USART(Universal Synchronous/Asynchronous Receiver/Transmitter),两者的区别在于:
STM32芯片中集成的是USART外设,但在大多数场景下,我们使用其异步模式(即UART功能),因此本文统一以“UART”代指STM32中基于USART外设的异步通信。
UART的数据以“帧”为单位传输,每帧包含以下部分(从左到右传输):
(示意图:起始位+数据位+校验位+停止位)
波特率(Baud Rate)是UART通信的核心参数,定义为每秒传输的“码元数”(对于UART,每个码元对应1位数据),单位为bps(比特每秒)。例如:
通信双方必须使用相同的波特率,否则会出现数据解析错误。实际应用中,常用波特率为9600、19200、38400、57600、115200、256000等。
STM32系列芯片(如F1、F4、H7等)通常集成多个USART外设(如F103有3个USART和2个UART,F407有6个USART),不同型号资源不同,需参考对应的数据手册。
STM32的USART外设支持以下关键特性(以F103为例):
USART外设的TX/RX引脚需通过“复用功能”配置,不同型号的引脚映射不同。以STM32F103C8T6为例,USART1的默认引脚为:
若默认引脚被占用,可通过“重映射”功能切换到其他引脚(如USART1可重映射到PB6/PB7),具体需参考数据手册的“复用功能映射表”。
USART的时钟来自APB总线(APB1或APB2):
时钟频率直接影响波特率精度,需根据总线时钟计算波特率寄存器的值。
USART的配置主要通过以下寄存器实现(以寄存器级编程为例):
USART_CR1(控制寄存器1):
USART_CR2(控制寄存器2):
USART_CR3(控制寄存器3):
USART_BRR(波特率寄存器):
USART_SR(状态寄存器):
USART_DR(数据寄存器):
波特率由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通信的硬件电路简单,核心是“电平匹配”和“抗干扰”。
STM32的GPIO为3.3V电平,而PC的串口(RS232)为±15V电平,直接连接会损坏芯片,因此需通过“电平转换芯片”(如CH340、PL2303)实现3.3V与RS232/USB的转换。
若需硬件流控制(防止数据溢出),需增加RTS(请求发送)和CTS(清除发送)引脚:
大多数场景下无需流控制,可省略这两根线。
本节以STM32F103为例,分别介绍寄存器级和HAL库的配置方法,实现UART基本通信。
“8N1”是最常用的配置:8位数据位,无校验位,1位停止位。
// 使能GPIOA和USART1时钟(APB2总线)
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN;
// 配置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; // 浮空输入
// 复位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;
// 发送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++);
}
}
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);
}
功能: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);
}
}
测试方法:
查询方式会阻塞CPU,中断方式更高效。配置步骤:
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) {
// 主循环可执行其他任务
}
}
DMA(直接存储器访问)可实现数据在USART与内存间的直接传输,无需CPU干预,适合大数据量传输。
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) {
// 发送完成后可执行其他操作
}
}
}
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);
// 循环模式下无需手动重启,自动重新填充缓冲区
}
}
将标准库的printf函数重定向到UART,方便输出调试信息。
#include
// 重定向printf到USART1
int fputc(int ch, FILE *f) {
HAL_UART_Transmit(&huart1, (uint8_t*)&ch, 1, 100);
return ch;
}
int main(void) {
// 初始化代码...
int temp = 25;
printf("Temperature: %d℃\r\n", temp); // 输出到串口
}
在资源紧张时,可通过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;
发送时直接调用发送函数,接收时需先切换为输入模式(或通过中断自动切换)。
多个从机通过UART总线连接到主机,主机通过地址帧选择目标从机。
配置步骤:
USART1->CR1 |= USART_CR1_MME;
(多机模式使能);波特率错误:
引脚配置错误:
校验位/停止位不匹配:
中断未使能:
接地问题:
串口助手:
示波器/逻辑分析仪:
printf调试:
DMA调试:
HAL_DMA_GetState()
查看DMA状态。添加帧头帧尾:
0xAA + 长度 + 数据 + 校验和 + 0x55
,避免数据粘连。软件滤波:
超时处理:
使用中断+缓冲区:
UART作为STM32中最基础的通信方式,是嵌入式开发的必备技能。本文从协议基础到实战案例,覆盖了UART的核心知识点,包括:
实际开发中,需根据场景选择合适的传输方式(查询/中断/DMA),并注重通信的可靠性设计。进阶学习可探索:
掌握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;
}