3. 寄存器编程入门:从数据手册到UART寄存器
好,咱们进入第三章。这一章很关键,说白了就是教你「怎么跟单片机硬件对话」。
很多初学者拿到一块新芯片,第一反应是上网找现成的库函数。我个人习惯正好相反——先翻数据手册,找到寄存器映射表,再动手写驱动。为什么?因为只有看懂寄存器,你才能真正理解硬件在干什么。
3.1 寄存器映射原理:内存地址背后的硬件逻辑
先问一个问题:你写 *(volatile uint32_t *)0x40004400 = 0x01; 的时候,到底发生了什么?
答案很简单——你在给一个「硬件寄存器」赋值。这个寄存器不是内存里的变量,它背后连着一堆逻辑门、触发器和总线接口。你写进去的每一位,都可能控制一个引脚的电平、一个中断的使能、或者一个波特率分频器的系数。
核心概念:寄存器映射,就是把芯片内部的功能模块(UART、GPIO、定时器等)的控制接口,映射到CPU的地址空间上。每个寄存器都有一个唯一的基地址 + 偏移量。
举个例子,STM32F103的USART1数据寄存器(DR)地址是 0x40013800 + 0x04。这个地址不是随便定的,芯片设计时就在总线上固定死了。你往这个地址写数据,数据就会通过APB总线送到UART模块的发送移位寄存器里。
我在项目中遇到过有人直接用 #define USART1_DR (*(volatile uint32_t *)0x40013804) 然后疯狂报错——后来发现他忘了加 volatile。编译器优化直接把写操作给吞了。嗯,这里要注意:硬件寄存器的值随时可能被外设改变,必须用 volatile 告诉编译器不要优化。
3.2 数据手册阅读方法:别被几百页吓到
一本数据手册动辄上千页,怎么读?我教你一个「三遍法」:
- 第一遍:扫目录,找寄存器章节。 通常叫「Memory Map」或「Register Description」。先找到UART相关的基地址和寄存器列表。
- 第二遍:看寄存器位定义。 每个寄存器都有位图(Bit Map),告诉你每一位或每一段的功能。比如控制寄存器的bit 3是使能位,bit 5:4是模式选择。
- 第三遍:对照参考代码验证。 写一小段测试代码,读回寄存器的值,跟手册预期对比。这一步能帮你发现手册理解偏差。
我的小技巧: 拿到新芯片,先打印出UART相关寄存器的复位值。如果读出来全是0xFF或者0x00,说明你的地址映射或者时钟没开。这招帮我省了至少一半的调试时间。
你想想看,数据手册里最常用的几个部分:
- 第1章:产品概述 —— 看特性列表,确认UART数量、支持的最高波特率、FIFO深度。
- 第2章:引脚定义 —— 找到UART的TX、RX、RTS、CTS对应哪个GPIO口。
- 第9章(大概):寄存器描述 —— 核心内容,逐位解释每个寄存器的功能。
- 第24章(大概):电气特性 —— 看时序图,了解数据采样点、建立保持时间。
3.3 UART相关寄存器概览
不管哪家厂商的MCU,UART寄存器基本都逃不出这三类:数据寄存器、控制寄存器、状态寄存器。咱们一个一个说。
3.3.1 数据寄存器(DR / DATA / RBR / THR)
这是最直接的寄存器。发送时你往里面写数据,接收时你从里面读数据。有些芯片把发送和接收分开(如STM32的DR寄存器读写地址相同,但物理上是两个寄存器),有些则共用。
| 位域 | 名称 | 描述 |
|---|---|---|
| [7:0] | DATA | 发送/接收的数据字节(8位模式) |
| [8] | PARITY | 奇偶校验位(如果使能) |
| [15:9] | RESERVED | 保留位,写0 |
我曾经踩过的坑: 读数据寄存器之前,一定要先检查状态寄存器的RXNE(接收数据非空)位。否则你读到的可能是上一次的数据,或者全是0。我曾经因为这个bug调了整整一个下午。
3.3.2 控制寄存器(CR1 / CR2 / CR3 / LCR / MCR)
控制寄存器负责配置UART的工作模式。通常有多个控制寄存器,每个管不同的功能组:
- CR1(控制寄存器1): 使能位(UE)、发送使能(TE)、接收使能(RE)、中断使能(RXNEIE、TCIE等)。
- CR2(控制寄存器2): 停止位长度选择(1位、1.5位、2位)、同步模式配置。
- CR3(控制寄存器3): 流控制使能(CTS、RTS)、单线半双工模式、DMA使能。
举个例子,配置一个典型的8N1(8数据位、无校验、1停止位)模式:
// 假设基地址为 USART1_BASE
// 使能UART、发送、接收
*(volatile uint32_t *)(USART1_BASE + 0x00) |= (1 << 13) | (1 << 3) | (1 << 2);
// 设置1位停止位(默认就是1位,但显式配置更安全)
*(volatile uint32_t *)(USART1_BASE + 0x04) &= ~(3 << 12);
// 配置8位数据模式(通常默认就是8位,但不同芯片可能不同)
*(volatile uint32_t *)(USART1_BASE + 0x00) &= ~(1 << 12); // M位=0表示8位
3.3.3 状态寄存器(SR / USR / LSR)
状态寄存器告诉你UART当前在干什么。这是你写驱动时最常查询的寄存器:
| 位 | 标志 | 含义 | 使用场景 |
|---|---|---|---|
| 5 | RXNE | 接收数据寄存器非空 | 读数据前检查,有数据再读 |
| 6 | TC | 发送完成 | 发送完一帧后等待此位,再发下一帧 |
| 7 | TXE | 发送数据寄存器空 | 可以写入下一个数据 |
| 3 | FE | 帧错误 | 检测到停止位错误,通常需要复位接收 |
| 2 | ORE | 溢出错误 | 数据被覆盖,说明CPU来不及读 |
我习惯在调试时把状态寄存器的值实时打印出来。有一次发现ORE位频繁置1,排查后发现是中断优先级太低,被其他高优先级中断抢占了CPU时间。调高UART中断优先级后问题解决。
3.4 知识结构图:UART寄存器体系
下面这张图帮你理清三类寄存器之间的关系:
3.5 实战:从零配置一个UART寄存器
光说不练假把式。咱们拿一个实际例子走一遍流程。假设芯片是STM32F103,目标:配置USART1为115200波特率、8N1模式、使能发送和接收。
第一步:查手册找到寄存器地址
从数据手册的Memory Map章节找到:USART1基地址 = 0x40013800。各寄存器偏移量:
- SR(状态寄存器):偏移 0x00
- DR(数据寄存器):偏移 0x04
- BRR(波特率寄存器):偏移 0x08
- CR1(控制寄存器1):偏移 0x0C
- CR2(控制寄存器2):偏移 0x10
- CR3(控制寄存器3):偏移 0x14
第二步:计算波特率
STM32的波特率公式:Baud = Fck / (16 * USARTDIV)。假设系统时钟72MHz,目标115200:
USARTDIV = 72000000 / (16 * 115200) = 39.0625
// 取整:DIV_Mantissa = 39, DIV_Fraction = 0.0625 * 16 = 1
// BRR寄存器值 = (39 << 4) | 1 = 0x0271
第三步:写代码配置
// 1. 使能USART1时钟(RCC寄存器操作)
*(volatile uint32_t *)0x40021018 |= (1 << 14); // RCC_APB2ENR 的 USART1EN 位
// 2. 配置GPIO为复用推挽输出(TX)和浮空输入(RX)
// 这部分属于GPIO寄存器,咱们后面章节细讲
// 3. 配置波特率
*(volatile uint32_t *)(0x40013800 + 0x08) = 0x0271; // BRR
// 4. 配置控制寄存器:8位数据、1位停止位、使能UE/TE/RE
*(volatile uint32_t *)(0x40013800 + 0x0C) = (1 << 13) | (1 << 3) | (1 << 2);
// bit13=UE, bit3=TE, bit2=RE
// 5. 发送一个字节 'A' (0x41)
while(!(*(volatile uint32_t *)(0x40013800 + 0x00) & (1 << 7))); // 等待TXE
*(volatile uint32_t *)(0x40013800 + 0x04) = 0x41; // 写DR
调试建议: 第一次写寄存器驱动时,每配置一个寄存器就读回来验证。比如配置完BRR后,读一下看看是不是0x0271。如果读出来是0,说明时钟没开或者地址写错了。
3.6 本章小结
寄存器编程没那么神秘。你只需要记住三件事:
- 地址要准 —— 基地址 + 偏移量,一个字节都不能错
- 位定义要清 —— 每个位是干什么的,手册里写得明明白白
- 顺序要对 —— 先开时钟,再配GPIO,再配UART控制寄存器,最后读写数据
我刚开始做嵌入式那会儿,觉得寄存器编程很麻烦,总想找现成的库。后来发现,真正出问题的时候,能救你的只有对寄存器的理解。库函数封装得再好,也挡不住硬件上的坑。