1. 项目概述
USART/UART作为嵌入式开发中最基础的通信接口之一,几乎出现在所有MCU项目中。但很多新手在第一次接触用户手册中的示例代码时,往往会被各种寄存器配置和时序要求搞得晕头转向。我自己在STM32F103上调试UART时,就曾经因为一个波特率配置错误浪费了整整两天时间。
这个项目就是要带大家用最直白的方式,拆解用户手册中的USART/UART示例代码。我们会从最基础的串口通信原理开始,一步步分析寄存器配置背后的逻辑,最后实现一个可靠的收发程序。不同于官方手册的"配置步骤列表",我会重点解释每个设置项为什么要这样做,以及在真实项目中可能遇到的坑。
2. 核心概念解析
2.1 USART与UART的区别
虽然经常被混为一谈,但USART(Universal Synchronous/Asynchronous Receiver/Transmitter)和UART(Universal Asynchronous Receiver/Transmitter)有个关键区别:同步通信支持。USART可以工作在同步模式(需要时钟线)和异步模式,而UART只能异步通信。
在大多数嵌入式场景中,我们其实都在使用异步模式。所以如果你看到手册上写的是USART,但实际只用到了TX/RX两根线,那就是在当UART用。以STM32为例,它的USART1外设就同时支持两种模式。
2.2 异步串口的关键参数
理解下面这些参数是正确配置寄存器的前提:
-
波特率(Baud Rate):每秒传输的符号数。常见值有9600、115200等。需要特别注意波特率误差要控制在允许范围内(通常<3%)。
-
数据位(Data Bits):每个字符的位数,通常是8位(一个字节),但也有7位的情况(比如某些老式设备)。
-
停止位(Stop Bits):标志一个字符传输结束,可以是1、1.5或2位。大多数情况下用1位就够了。
-
校验位(Parity Bit):简单的错误检测机制,可选奇校验、偶校验或无校验。在干扰较强的环境中建议启用。
3. 寄存器配置详解
3.1 波特率设置
以STM32F4系列为例,波特率寄存器(USART_BRR)的计算公式是:
code复制BRR = (APB时钟频率)/(16 × 期望波特率)
假设APB2时钟是84MHz,想要115200波特率:
code复制BRR = 84,000,000/(16×115200) = 45.5729
整数部分45写入BRR[15:4],小数部分0.5729×16≈9,所以最终BRR值应该是0x2D9(45<<4 | 9)。
注意:实际计算时建议使用宏定义或常量表达式,避免硬编码。例如:
c复制#define APB2_CLK 84000000 #define BAUD_RATE 115200 USART1->BRR = (APB2_CLK + BAUD_RATE/2) / BAUD_RATE; // 四舍五入
3.2 控制寄存器配置
USART_CR1是最关键的控制寄存器,主要配置项包括:
- UE:USART使能位,必须先开启才能配置其他参数
- M:字长选择,0表示8数据位,1表示9数据位
- PCE:校验控制使能
- PS:校验选择,0表示偶校验,1表示奇校验
- TE/RE:发送/接收使能
典型配置代码:
c复制USART1->CR1 = 0; // 先清零
USART1->CR1 |= USART_CR1_UE; // 开启USART
USART1->CR1 |= USART_CR1_TE | USART_CR1_RE; // 使能收发
3.3 中断配置
如果需要使用中断接收数据,还需要配置:
- 在CR1中开启接收中断(RXNEIE)
- 在NVIC中使能USART中断
- 编写中断服务程序(ISR)
示例:
c复制// 使能接收中断
USART1->CR1 |= USART_CR1_RXNEIE;
// NVIC配置
NVIC_SetPriority(USART1_IRQn, 0);
NVIC_EnableIRQ(USART1_IRQn);
// 中断处理函数
void USART1_IRQHandler(void) {
if(USART1->SR & USART_SR_RXNE) {
uint8_t data = USART1->DR; // 读取数据会自动清除RXNE标志
// 处理接收到的数据
}
}
4. 完整示例代码解析
4.1 初始化函数
下面是一个完整的USART初始化示例(以STM32F407为例):
c复制void USART1_Init(uint32_t baudrate) {
// 1. 使能时钟
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
// 2. GPIO配置(PA9-TX, PA10-RX)
GPIOA->MODER &= ~(GPIO_MODER_MODER9 | GPIO_MODER_MODER10);
GPIOA->MODER |= (2 << GPIO_MODER_MODER9_Pos) | (2 << GPIO_MODER_MODER10_Pos); // 复用模式
GPIOA->AFR[1] &= ~(GPIO_AFRH_AFSEL9 | GPIO_AFRH_AFSEL10);
GPIOA->AFR[1] |= (7 << GPIO_AFRH_AFSEL9_Pos) | (7 << GPIO_AFRH_AFSEL10_Pos); // AF7-USART1
// 3. USART配置
USART1->BRR = (84000000 + baudrate/2) / baudrate; // 四舍五入
USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE;
USART1->CR2 = 0; // 1停止位
USART1->CR3 = 0; // 无流控
}
4.2 发送与接收函数
基础版(轮询方式):
c复制void USART1_SendByte(uint8_t data) {
while(!(USART1->SR & USART_SR_TXE)); // 等待发送缓冲区空
USART1->DR = data;
}
uint8_t USART1_ReceiveByte(void) {
while(!(USART1->SR & USART_SR_RXNE)); // 等待接收到数据
return USART1->DR;
}
增强版(带超时检测):
c复制#define USART_TIMEOUT 1000 // 1ms超时
bool USART1_SendByte(uint8_t data) {
uint32_t timeout = USART_TIMEOUT;
while(!(USART1->SR & USART_SR_TXE) && timeout--);
if(timeout == 0) return false;
USART1->DR = data;
return true;
}
bool USART1_ReceiveByte(uint8_t *data) {
uint32_t timeout = USART_TIMEOUT;
while(!(USART1->SR & USART_SR_RXNE) && timeout--);
if(timeout == 0) return false;
*data = USART1->DR;
return true;
}
5. 常见问题与调试技巧
5.1 数据收发异常排查步骤
-
检查时钟配置:
- 确认APB总线时钟是否正确
- 确认USART外设时钟已使能
- 使用示波器测量实际波特率
-
检查GPIO配置:
- 确认TX/RX引脚模式设置为复用功能
- 确认复用功能编号正确(AF7 for USART1)
- 检查引脚是否被其他外设占用
-
寄存器配置验证:
- 使用调试器查看BRR寄存器值是否符合计算
- 确认CR1中的UE、TE、RE位已置位
- 检查CR2中的停止位设置
5.2 典型问题解决方案
问题1:能发送但不能接收
- 检查RE位是否使能
- 检查RX引脚配置
- 检查对方设备的TX信号是否正常
问题2:接收数据错位
- 检查双方波特率是否一致
- 用示波器测量实际波特率误差
- 检查时钟源精度(特别是内部RC振荡器)
问题3:高波特率下数据错误
- 降低线缆长度或使用屏蔽线
- 在TX/RX线上加33Ω串联电阻
- 检查PCB布局,避免高速信号线过长
5.3 高级调试技巧
- 利用空闲中断实现帧检测:
c复制USART1->CR1 |= USART_CR1_IDLEIE; // 使能空闲中断
void USART1_IRQHandler(void) {
if(USART1->SR & USART_SR_IDLE) {
(void)USART1->DR; // 清除IDLE标志
// 处理一帧完整数据
}
}
- DMA配合使用减轻CPU负担:
c复制// 配置DMA从内存到USART的发送
DMA1_Stream4->PAR = (uint32_t)&USART1->DR;
DMA1_Stream4->M0AR = (uint32_t)tx_buffer;
DMA1_Stream4->NDTR = tx_length;
DMA1_Stream4->CR |= DMA_SxCR_EN;
// USART中使能DMA发送
USART1->CR3 |= USART_CR3_DMAT;
- 使用硬件流控(CTS/RTS):
当通信双方速度不匹配时,可以启用硬件流控:
c复制// 配置CTS/RTS引脚
GPIOA->MODER |= (2 << GPIO_MODER_MODER11_Pos) | (2 << GPIO_MODER_MODER12_Pos);
GPIOA->AFR[1] |= (7 << GPIO_AFRH_AFSEL11_Pos) | (7 << GPIO_AFRH_AFSEL12_Pos);
// 使能硬件流控
USART1->CR3 |= USART_CR3_CTSE | USART_CR3_RTSE;
6. 实际项目中的应用建议
在真实项目中,建议采用以下架构设计USART驱动:
-
分层设计:
- 底层硬件抽象层(HAL):处理寄存器直接操作
- 中间驱动层:实现环形缓冲区、协议解析等
- 应用层:处理具体业务逻辑
-
环形缓冲区实现:
c复制#define BUF_SIZE 256
typedef struct {
uint8_t buffer[BUF_SIZE];
volatile uint16_t head;
volatile uint16_t tail;
} ring_buffer_t;
void put_char(ring_buffer_t *buf, uint8_t c) {
buf->buffer[buf->head++] = c;
if(buf->head >= BUF_SIZE) buf->head = 0;
}
uint8_t get_char(ring_buffer_t *buf) {
uint8_t c = buf->buffer[buf->tail++];
if(buf->tail >= BUF_SIZE) buf->tail = 0;
return c;
}
-
协议设计建议:
- 使用固定帧头帧尾(如0xAA 0x55)
- 包含长度字段和校验字段(CRC8/CRC16)
- 定义超时机制(如50ms无新数据视为一帧结束)
-
性能优化技巧:
- 对于高频小数据包,禁用中断改用DMA
- 使用__attribute__((packed))确保结构体对齐
- 关键代码放在RAM中执行(通过__attribute__((section(".ramcode")))
我在多个工业项目中总结出一个经验:USART看似简单,但要实现稳定可靠的通信,必须处理好每一个细节。特别是在电磁环境复杂的场合,除了软件层面的健壮性设计,硬件上的滤波、隔离等措施同样重要。
