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 四个寄存器的协作关系
这四个寄存器不是孤立的。我画了一张图,帮你理清它们之间的数据流和控制流:
从图上你能看到:
- BRR从系统时钟分频,产生通信速率
- CR配置工作模式,控制收发使能
- DR是数据进出的唯一通道
- SR把状态反馈给CPU,驱动下一步操作
总结一下:配置顺序是BRR→CR→DR,查询顺序是SR→DR。这个顺序记牢了,写驱动就不会乱。
4.6 寄存器编程的常见陷阱
最后分享几个我实际项目中遇到的坑:
- 读SR后立即读DR:有些芯片要求读SR和读DR之间不能有其他操作,否则RXNE标志清除会失效。
- BRR写入时机:必须在UE=0时写BRR,否则写入无效。我吃过这个亏,调了半天波特率就是不对。
- CR的位操作:不要直接赋值整个CR,要用位操作修改。否则一不小心就把其他配置清掉了。
- ORE标志清除:溢出错误发生后,要先读SR再读DR才能清除ORE。只读DR是清不掉的。
特别提醒:不同厂商的UART寄存器命名和位定义可能有差异。比如有些芯片把状态寄存器叫USART_SR,有些叫UART_SR。但核心逻辑是一样的——数据、状态、控制、波特率,这四个寄存器搞懂了,换任何芯片都能快速上手。