4. UART初始化配置:从时钟到寄存器,一步都不能少

UART要能正常工作,初始化配置是关键。说白了,就是告诉单片机:「我要用哪个引脚、跑多快、数据怎么传」。我刚开始做嵌入式那会儿,总觉得初始化就是抄几个寄存器值,结果经常遇到收不到数据、乱码、甚至死机的情况。后来才明白——每一步都有它的道理。

这一节,我们就把UART初始化的四个核心步骤拆开揉碎了讲。你跟着我的思路走一遍,以后遇到任何MCU的UART配置,都能举一反三。

核心要点:UART初始化 = 时钟使能 + GPIO复用 + 波特率计算 + 帧格式配置。顺序不能乱,参数不能错。

UART 初始化配置流程 ① 时钟使能 开启UART模块时钟 ② GPIO复用 配置TX/RX引脚功能 ③ 波特率设置 计算并写入BRG寄存器 ④ 帧格式 数据/停止/校验 ⚠️ 关键提醒 • 时钟使能必须在GPIO配置之前,否则写寄存器无效 • 波特率误差应控制在 ±2% 以内,否则通信会丢数据 • 收发双方的帧格式必须完全一致,否则收不到正确数据

4.1 时钟使能——先让UART模块「通电」

很多人一上来就配GPIO、写波特率寄存器,结果发现怎么配都不起作用。为什么?因为UART模块的时钟根本没打开!

在CMS32系列中,UART挂载在APB总线上。你需要先找到对应的时钟控制寄存器,把UART模块的时钟使能位拉高。我习惯的做法是:先查数据手册的时钟树,确认UART挂在哪条总线上,再去找对应的RCC寄存器。

// 以 CMS32 为例,使能 UART0 时钟
// 假设 UART0 挂载在 APB1 总线,时钟使能寄存器为 RCC_APB1ENR
RCC->APB1ENR |= (1 << 4);  // 第4位对应 UART0 时钟使能

我的经验:写完时钟使能后,建议读一下寄存器确认位确实被置1了。有些MCU有写保护机制,需要先解锁才能写。我曾经在一个项目里被这个坑了整整半天——寄存器写进去读出来还是0,后来发现是没解锁。

4.2 GPIO复用功能配置——把引脚「借」给UART

时钟有了,接下来要让物理引脚和UART模块「对接」。CMS32的GPIO默认是普通I/O模式,你需要把TX、RX两个引脚切换到复用功能模式。

这里有个容易混淆的点:TX是输出,RX是输入。配置时别搞反了。我见过有人把两个引脚都配成输出,结果自发自收都做不了。

// 配置 PA9 为 UART0_TX (复用推挽输出)
// 配置 PA10 为 UART0_RX (复用浮空输入)
GPIOA->CRH &= ~(0xFF << 4);   // 先清除配置位
GPIOA->CRH |= (0x0B << 4);    // PA9: 复用推挽输出 (AFPP)
GPIOA->CRH |= (0x04 << 8);    // PA10: 复用浮空输入 (AF_INPUT)
引脚 方向 配置模式 说明
TX 输出 复用推挽输出 (AFPP) MCU通过此引脚发送数据
RX 输入 复用浮空输入 (AF_INPUT) MCU通过此引脚接收数据

注意:配置GPIO复用功能时,一定要确认该引脚是否被其他外设占用。比如同一个引脚可能同时映射到UART和SPI,你只能选一个用。查数据手册的「引脚功能复用表」是最稳妥的办法。

4.3 波特率计算与设置——速度匹配是通信的基础

波特率就是每秒传输的比特数。收发双方的波特率必须一致,误差还不能太大。我一般要求误差控制在±2%以内,超过这个范围,长时间通信就会出现丢帧。

CMS32的波特率计算公式如下:

// 波特率计算公式
// BRG = (UART_CLK / (16 * BaudRate)) - 1
// 其中 UART_CLK 是UART模块的输入时钟频率

// 示例:UART_CLK = 48MHz,目标波特率 115200
uint32_t brg_value = (48000000 / (16 * 115200)) - 1;
// 计算结果: brg_value = 25.04 → 取整为 25
// 实际波特率 = 48000000 / (16 * (25 + 1)) = 115384
// 误差 = (115384 - 115200) / 115200 ≈ 0.16% ✅ 完全可用

你看,取整后实际波特率是115384,误差只有0.16%,远小于2%的容忍线。但如果你的时钟频率比较低,比如8MHz,想跑115200:

// UART_CLK = 8MHz,目标 115200
uint32_t brg_value = (8000000 / (16 * 115200)) - 1;
// 计算结果: brg_value = 3.34 → 取整为 3
// 实际波特率 = 8000000 / (16 * (3 + 1)) = 125000
// 误差 = (125000 - 115200) / 115200 ≈ 8.5% ❌ 通信会丢数据

误差8.5%,这肯定不行。怎么办?要么降低目标波特率到9600,要么提高UART模块的时钟频率。我在一个项目中就遇到过这种情况,最后把UART的时钟源从内部RC切换到了PLL输出,频率从8MHz提到了48MHz,问题就解决了。

实用建议:写代码时不要硬编码BRG值。用宏定义或者函数动态计算,这样换时钟频率时只需要改一个参数。我自己的代码里永远是这样写的:

#define UART_CLK     48000000
#define TARGET_BAUD  115200
#define BRG_VAL      ((UART_CLK / (16 * TARGET_BAUD)) - 1)

4.4 数据位/停止位/校验位配置——统一「语言」才能沟通

波特率对了,但数据格式不对,照样白搭。UART的帧格式包括:

  • 数据位:通常是8位,也有7位或9位的。CMS32默认支持8位数据位。
  • 停止位:1位、1.5位或2位。最常用的是1位停止位。
  • 校验位:无校验、奇校验、偶校验。如果通信环境噪声大,建议开启校验。

这些参数配置在UART的控制寄存器里。以CMS32为例:

// 配置 UART0 帧格式:8数据位 + 1停止位 + 无校验
UART0->CR1 &= ~(1 << 12);   // M位=0 → 8位数据位
UART0->CR2 &= ~(3 << 12);   // STOP位=00 → 1位停止位
UART0->CR1 &= ~(1 << 10);   // PCE位=0 → 无校验
参数 常用值 配置寄存器 说明
数据位 8位 CR1.M M=0: 8位; M=1: 9位
停止位 1位 CR2.STOP 00: 1位; 01: 0.5位; 10: 2位; 11: 1.5位
校验位 无校验 CR1.PCE + PS PCE=0: 无校验; PCE=1: 奇/偶校验由PS决定

避坑指南:我曾经在一个项目中,设备A用8位数据位+偶校验,设备B用8位数据位+无校验。两边波特率一样,但就是通不了。查了半天才发现是校验位不匹配。记住:收发双方的帧格式必须完全一致,包括数据位、停止位、校验位,一个都不能差。

4.5 完整的初始化函数示例

把上面四步串起来,就是一个完整的UART初始化函数。我习惯把每一步都加上注释,方便以后维护:

void UART0_Init(void)
{
    // 1. 时钟使能
    RCC->APB1ENR |= (1 << 4);          // 使能 UART0 时钟
    
    // 2. GPIO复用功能配置
    GPIOA->CRH &= ~(0xFF << 4);       // 清除 PA9, PA10 配置
    GPIOA->CRH |= (0x0B << 4);        // PA9: TX → 复用推挽输出
    GPIOA->CRH |= (0x04 << 8);        // PA10: RX → 复用浮空输入
    
    // 3. 波特率设置 (48MHz, 115200)
    UART0->BRR = 25;                    // BRG = (48M/16/115200) - 1 ≈ 25
    
    // 4. 帧格式配置: 8数据位 + 1停止位 + 无校验
    UART0->CR1 &= ~(1 << 12);         // 8位数据位
    UART0->CR2 &= ~(3 << 12);         // 1位停止位
    UART0->CR1 &= ~(1 << 10);         // 无校验
    
    // 5. 使能 UART (UE位)
    UART0->CR1 |= (1 << 0);           // 使能 UART0
    UART0->CR1 |= (1 << 2);           // 使能发送器 (TE)
    UART0->CR1 |= (1 << 3);           // 使能接收器 (RE)
}

嗯,这里要注意:使能UART的UE位之后,才能配置TE和RE。顺序反了的话,发送器和接收器可能无法正常工作。这是我踩过的坑,分享给你。

最后提醒:初始化完成后,建议先发一个测试字节(比如0x55),用示波器或逻辑分析仪抓一下TX引脚的波形。看起始位、数据位、停止位是否和配置一致。这一步能帮你快速发现配置错误,比调试半天代码效率高得多。