4. UART核心寄存器详解:数据寄存器(DR)、状态寄存器(SR)、控制寄存器(CR)、波特率寄存器(BRR)

做UART驱动开发,说白了就是跟四个寄存器打交道。数据怎么收发、状态怎么查、参数怎么配、速率怎么设——全在这几个寄存器里。我刚开始学的时候,总觉得寄存器手册几百页看不下去,后来发现只要抓住这四个核心,其他都是细枝末节。

4.1 数据寄存器(DR)—— 收发数据的“中转站”

数据寄存器是UART通信中最直接的寄存器。你发数据,就往它里面写;你收数据,就从它里面读。但这里有个坑——发送和接收共用同一个地址。

核心要点:DR寄存器在物理上是两个独立的寄存器(发送DR和接收DR),但映射到同一个地址。读操作访问接收DR,写操作访问发送DR。

拿STM32来举例,DR寄存器通常是9位宽(bit0~bit8)。bit8是奇偶校验位,bit0~bit7才是真正的数据位。如果你配置了8位数据模式,那bit8就别管它。

// 发送一个字节
UART1->DR = 0x41;  // 发送字符 'A'

// 接收一个字节
uint8_t data = UART1->DR;  // 读取接收到的数据

我的习惯:读DR之前,一定先查状态寄存器里的RXNE位(接收非空标志)。我曾经在项目里直接读DR,结果读到的全是上次的旧数据——因为数据还没到呢。

4.2 状态寄存器(SR)—— 通信状态的“晴雨表”

状态寄存器告诉你UART当前在干什么。你想想看,如果没有状态寄存器,你怎么知道数据发完了?怎么知道有没有新数据到了?

SR里最常用的几个标志位:

标志位 名称 说明
TXE 发送数据寄存器空 DR可写入新数据,硬件自动置1
TC 发送完成 整个帧(含停止位)已发送完毕
RXNE 接收数据寄存器非空 DR中有新数据可读,硬件自动置1
ORE 溢出错误 数据还没读,新数据又来了
FE 帧错误 停止位没收到,多半是波特率不匹配

我曾经踩过的坑:只查TXE就发下一个数据,结果发现数据对不上。后来才明白——TXE只表示DR空了,但移位寄存器可能还在发。要等TC置位,才算真正发完。尤其是在关闭UART之前,一定要等TC=1。

读SR还有个细节:读SR再读DR,会自动清除RXNE标志。这是硬件设计的固定流程,你写代码时要注意这个顺序。

// 发送一个字节(带TC等待)
UART1->DR = data;
while (!(UART1->SR & UART_SR_TC));  // 等发送完成

// 接收一个字节(带超时保护)
uint32_t timeout = 100000;
while (!(UART1->SR & UART_SR_RXNE)) {
    if (--timeout == 0) {
        // 超时处理
        break;
    }
}
uint8_t recv = UART1->DR;

4.3 控制寄存器(CR)—— 通信参数的“总控台”

控制寄存器负责配置UART的工作模式。数据位、停止位、校验位、使能控制——全在这里配。我习惯把CR1、CR2、CR3分开记,但最常用的是CR1。

CR1里几个关键位:

  • UE(bit0):UART使能位。配完所有参数后,最后置1开启。
  • M(bit12):字长选择。0=8位,1=9位。
  • PCE(bit10):校验使能。置1开启奇偶校验。
  • PS(bit9):校验选择。0=偶校验,1=奇校验。
  • TE(bit3):发送使能。
  • RE(bit2):接收使能。
  • RXNEIE(bit5):接收中断使能。

配置顺序很重要:先关UE,再配M/PCE/PS,最后开UE。我见过有人开着UART改参数,结果通信直接乱掉。

// 配置8位数据、无校验、1位停止位
UART1->CR1 &= ~UART_CR1_UE;      // 先关闭UART
UART1->CR1 &= ~UART_CR1_M;       // 8位模式
UART1->CR1 &= ~UART_CR1_PCE;     // 无校验
UART1->CR2 &= ~UART_CR2_STOP;    // 1位停止位
UART1->CR1 |= UART_CR1_TE | UART_CR1_RE;  // 使能收发
UART1->CR1 |= UART_CR1_UE;       // 开启UART

4.4 波特率寄存器(BRR)—— 通信速率的“节拍器”

波特率决定了UART通信的快慢。两边必须配成一样的,否则数据全是乱码。BRR的值由系统时钟和目标波特率计算得出。

计算公式很简单:

BRR = 系统时钟频率 / 目标波特率

// 举例:系统时钟72MHz,目标波特率115200
BRR = 72000000 / 115200 = 625

但实际芯片里,BRR寄存器通常分两部分:

  • DIV_Mantissa(bit4~bit15):整数部分
  • DIV_Fraction(bit0~bit3):小数部分(4位精度)

所以实际计算要带小数:

// 72MHz, 115200波特率
float div = 72000000.0f / 115200.0f;  // = 625.0
uint16_t mantissa = (uint16_t)div;     // 625
uint16_t fraction = (uint16_t)((div - mantissa) * 16);  // 0

UART1->BRR = (mantissa << 4) | fraction;

我的经验:实际项目中,系统时钟往往不是整数。比如8MHz晶振倍频到72MHz,但有些芯片内部PLL有误差。这时候算出来的BRR值要四舍五入取整。我曾经因为没做小数处理,波特率差了0.5%,结果在高温下通信频繁出错。

4.5 四个寄存器的协作关系

这四个寄存器不是孤立的。我画了一张图,帮你理清它们之间的数据流和控制流:

UART核心寄存器协作关系图 系统时钟 BRR(波特率寄存器) CR(控制寄存器) DR(数据寄存器) SR(状态寄存器) CPU/中断控制器 协作流程说明: 1. 系统时钟输入BRR,BRR输出波特率时钟给收发器 2. CR配置数据格式、使能位,控制收发器工作模式 3. DR作为数据通道:写DR发送,读DR接收 4. SR实时反映收发状态,CPU通过查询或中断方式响应

从图上你能看到:

  • BRR从系统时钟分频,产生通信速率
  • CR配置工作模式,控制收发使能
  • DR是数据进出的唯一通道
  • SR把状态反馈给CPU,驱动下一步操作

总结一下:配置顺序是BRR→CR→DR,查询顺序是SR→DR。这个顺序记牢了,写驱动就不会乱。

4.6 寄存器编程的常见陷阱

最后分享几个我实际项目中遇到的坑:

  1. 读SR后立即读DR:有些芯片要求读SR和读DR之间不能有其他操作,否则RXNE标志清除会失效。
  2. BRR写入时机:必须在UE=0时写BRR,否则写入无效。我吃过这个亏,调了半天波特率就是不对。
  3. CR的位操作:不要直接赋值整个CR,要用位操作修改。否则一不小心就把其他配置清掉了。
  4. ORE标志清除:溢出错误发生后,要先读SR再读DR才能清除ORE。只读DR是清不掉的。

特别提醒:不同厂商的UART寄存器命名和位定义可能有差异。比如有些芯片把状态寄存器叫USART_SR,有些叫UART_SR。但核心逻辑是一样的——数据、状态、控制、波特率,这四个寄存器搞懂了,换任何芯片都能快速上手。