4. UART 初始化编程:从时钟到使能,一步都不能少

UART 初始化,说白了就是给这个外设“上电”、“接好线”、“定好规矩”,最后让它开始工作。这四步缺一不可,顺序也不能乱。我见过不少新手,上来就配波特率,结果发现数据根本发不出去——回头一看,时钟都没开。

好,咱们一步步来。这一章,我会带着你手写寄存器,把每个配置位都讲清楚。

4.1 第一步:时钟使能——让 UART 先“通电”

任何外设要工作,第一步就是打开它的时钟。在 STM32 里,这由 RCC(复位与时钟控制)模块负责。你想想看,外设的时钟都不给,它怎么干活?

我个人习惯,在初始化任何外设之前,先看一眼参考手册的 RCC 章节,找到对应外设的时钟使能位。

核心操作: 设置 RCC->APB1ENR 或 RCC->APB2ENR 寄存器中的对应位为 1。

以 STM32F103 的 USART1 为例,它挂在 APB2 总线上。使能代码很简单:

// 使能 USART1 时钟(APB2 总线)
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;

如果是 USART2 或 USART3,它们挂在 APB1 总线上:

// 使能 USART2 时钟(APB1 总线)
RCC->APB1ENR |= RCC_APB1ENR_USART2EN;
小提示: 不同系列的单片机,UART 挂载的总线可能不同。比如 STM32L4 系列,USART1 可能在 APB2 上,但 LPUART1 却在 APB1 上。写代码前,一定先确认。

4.2 第二步:GPIO 复用配置——把引脚“借”给 UART

时钟打开了,但 UART 的 TX 和 RX 引脚还没接好。GPIO 引脚默认是普通 I/O 口,你得告诉它:“嘿,你现在归 UART 管了。” 这就是复用功能配置。

这里有两个关键点:

  • 模式: 设置为复用推挽输出(AF_PP)或复用开漏输出(AF_OD)。TX 通常用复用推挽输出。
  • 速度: 一般设置为 50 MHz 或更高,保证高速通信时信号不畸变。

我记得有一次,一个同事调试 UART 死活收不到数据,折腾了半天。最后发现是 RX 引脚没配置成浮空输入,而是配成了推挽输出——直接把对方的信号给“掐死”了。

以 USART1 的 TX(PA9)和 RX(PA10)为例:

// 1. 开启 GPIOA 时钟
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

// 2. 配置 PA9 (TX) 为复用推挽输出,50 MHz
GPIOA->CRH &= ~(0xF << 4);   // 先清空 CNF9 和 MODE9 位
GPIOA->CRH |= (0xB << 4);    // 0xB = 1011: CNF=10(复用推挽), MODE=11(50MHz)

// 3. 配置 PA10 (RX) 为浮空输入
GPIOA->CRH &= ~(0xF << 8);   // 先清空 CNF10 和 MODE10 位
GPIOA->CRH |= (0x4 << 8);    // 0x4 = 0100: CNF=01(浮空输入), MODE=00(输入模式)
注意: 不同型号的 MCU,GPIO 寄存器结构可能不同。比如 STM32F4 系列用的是 GPIOx->MODER、GPIOx->AFR 等寄存器,配置方式完全不同。别把 F1 的代码直接往 F4 上套。

4.3 第三步:波特率发生器配置——BRR 寄存器的计算

波特率,就是每秒传输的位数。UART 通信双方必须用同一个波特率,否则就是“鸡同鸭讲”。

波特率发生器本质上是一个分频器。它从外设时钟(PCLK)出发,通过一个分频系数,得到目标波特率。这个分频系数就写在 USART_BRR 寄存器里。

计算公式如下:

公式: BRR = PCLK / (16 * 目标波特率)

举个例子,假设 PCLK = 72 MHz,目标波特率 = 115200:

BRR = 72000000 / (16 * 115200) = 72000000 / 1843200 ≈ 39.0625

BRR 寄存器是 16 位的,高 4 位放小数部分(DIV_Fraction),低 12 位放整数部分(DIV_Mantissa)。39.0625 怎么拆?

  • 整数部分: 39 (0x27)
  • 小数部分: 0.0625 * 16 = 1 (0x1)

所以 BRR 的值 = (39 << 4) | 1 = 0x271。代码实现:

// 假设 PCLK = 72 MHz,目标波特率 = 115200
uint32_t pclk = 72000000;
uint32_t baud = 115200;

uint32_t brr_value = (pclk + (baud * 8)) / (baud * 16);  // 四舍五入优化

USART1->BRR = brr_value;
避坑指南: 我曾经在一个项目中,PCLK 是 36 MHz,但忘了除以 2,直接用了 72 MHz 去算 BRR。结果波特率差了一倍,通信全是乱码。后来用示波器一看,才发现位宽不对。所以,一定要确认 UART 挂载的总线时钟频率。

4.4 第四步:数据格式设置——字长、停止位、校验位

波特率定好了,接下来要约定“数据包”的格式。这由 USART_CR1 和 USART_CR2 寄存器控制。

参数 寄存器位 说明
字长 CR1 的 M 位 0 = 8 位数据,1 = 9 位数据
停止位 CR2 的 STOP[1:0] 位 00 = 1 位,01 = 0.5 位,10 = 2 位,11 = 1.5 位
校验位 CR1 的 PCE 和 PS 位 PCE=1 使能校验,PS=0 偶校验,PS=1 奇校验

最常用的配置是:8 位数据、1 位停止位、无校验。也就是所谓的 8N1。

// 配置 USART1 为 8N1 模式
// 字长:8 位(M=0)
USART1->CR1 &= ~USART_CR1_M;

// 停止位:1 位(STOP=00)
USART1->CR2 &= ~USART_CR2_STOP;

// 校验位:无校验(PCE=0)
USART1->CR1 &= ~USART_CR1_PCE;

如果你需要 9 位数据 + 偶校验 + 1 位停止位:

// 字长:9 位(M=1)
USART1->CR1 |= USART_CR1_M;

// 停止位:1 位
USART1->CR2 &= ~USART_CR2_STOP;

// 使能校验,偶校验
USART1->CR1 |= USART_CR1_PCE;   // 使能校验
USART1->CR1 &= ~USART_CR1_PS;   // 偶校验(PS=0)
注意: 当使能校验位时,数据域会减少 1 位。比如 8 位数据 + 校验,实际有效数据只有 7 位,第 8 位被校验位占用。这一点很容易被忽略,我在一个 Modbus 协议的项目里就踩过这个坑。

4.5 第五步:使能 UART 模块——最后的“点火”

所有配置都写好了,最后一步就是使能 UART。这由 CR1 寄存器的 UE(USART Enable)位控制。

// 使能 USART1
USART1->CR1 |= USART_CR1_UE;

使能之后,UART 就开始工作了。但注意,这时候发送器和接收器还没打开。你还需要分别使能它们:

// 使能发送器(TE 位)
USART1->CR1 |= USART_CR1_TE;

// 使能接收器(RE 位)
USART1->CR1 |= USART_CR1_RE;

我个人习惯,在使能 UE 之前,先把 TE 和 RE 都配好,然后一次性使能 UE。这样能避免一些奇怪的时序问题。

完整的初始化顺序:
  1. 使能 UART 时钟(RCC)
  2. 配置 GPIO 复用功能
  3. 计算并设置 BRR 寄存器(波特率)
  4. 配置数据格式(CR1、CR2)
  5. 使能发送器和接收器(TE、RE)
  6. 使能 UART 模块(UE)

4.6 本章知识体系总览

下面这张图,把 UART 初始化的整个流程串起来了。你可以把它当作一个检查清单,每次写初始化代码时对照着看。

UART 初始化流程与寄存器配置总览 UART 初始化流程与寄存器配置总览 1. 时钟使能 RCC->APBxENR 2. GPIO 复用 GPIOx->CRH/CRL 3. 波特率配置 USART->BRR 4. 数据格式 USART->CR1/CR2 5. 使能 UART USART->CR1 (UE) 关键参数速查 • 字长:M 位 (CR1) • 停止位:STOP 位 (CR2) • 校验:PCE/PS 位 (CR1) • 波特率:BRR 寄存器 • 发送/接收:TE/RE 位 • 使能:UE 位 (CR1) ⚠️ 常见错误 1. 忘记使能 GPIO 时钟,导致引脚配置无效 2. BRR 计算时用了错误的 PCLK 频率(未确认总线分频) 3. 使能 UE 前未配置 TE/RE,导致无法收发数据

嗯,到这里,UART 初始化的五个步骤就全讲完了。从时钟到 GPIO,从波特率到数据格式,最后使能模块——每一步都有对应的寄存器操作。你写代码的时候,按这个顺序来,基本不会出错。