4. UART串口通信驱动开发(上):协议基础与CMS32寄存器配置

各位同学,咱们今天正式进入UART串口通信的实战环节。UART这个东西,说简单也简单,说复杂也复杂。我做了这么多年嵌入式,可以说90%的调试工作都离不开它——打印日志、与传感器通信、甚至做Bootloader升级,全得靠这条串口线。

这一章我们先打好基础,把UART协议本身搞明白,再把CMS32芯片上的UART模块寄存器摸清楚。波特率怎么配、数据位/停止位/校验位怎么设,这些看似基础的东西,其实藏着不少坑。我当年刚入行时就在波特率偏差上吃过亏,后面会跟大家细说。

4.1 UART协议基础:串口通信的"老规矩"

UART全称是Universal Asynchronous Receiver/Transmitter,通用异步收发器。说白了,它就是一根发送线(TX)、一根接收线(RX),再加上地线(GND),就能实现两个设备之间的通信。

为什么叫"异步"?因为没有独立的时钟线。收发双方得事先约定好通信速率——也就是波特率,然后各自用自己的时钟去采样数据。这就好比两个人约好"每秒说一个字",然后各自掐表,谁也别依赖谁。

UART数据帧结构(标准格式):

  • 起始位:1位,逻辑低电平,表示"我要开始发数据了"
  • 数据位:5~9位,常见为8位(一字节)
  • 校验位:0或1位,可选奇校验/偶校验/无校验
  • 停止位:1位、1.5位或2位,逻辑高电平,表示"这一帧结束"

我给大家画个图,这样更直观:

UART 标准数据帧结构 空闲 起始 D0 D1 D2 D3 D4 D5 D6 D7 校验 停止 一帧数据(典型10位:1起始 + 8数据 + 1停止,无校验) 关键说明: • 空闲时TX线保持高电平 • 起始位是低电平,从高到低的跳变告诉接收方"准备接收" • 数据位LSB(最低位)先发,MSB最后发

这里有个细节我想强调一下:数据位是LSB first,也就是最低位先发。比如你要发0x55(二进制01010101),先发出去的是最低位的1,而不是最高位的0。这个顺序跟SPI、I2C不一样,别搞混了。

个人经验:我调试串口时,最常用的套路就是发0x55和0xAA这两个数。0x55是01010101,0xAA是10101010,用示波器一看波形,数据位顺序对不对、波特率准不准,一目了然。

4.2 CMS32 UART模块寄存器全景

好,协议层面搞清楚了,咱们来看看CMS32芯片上的UART模块长什么样。CMS32的UART模块,我习惯叫它"串口外设",它内部有一组寄存器来控制通信行为。

咱们不需要把所有寄存器都背下来,但核心的几个必须烂熟于心。我列个表,大家对照着看:

寄存器名称 偏移地址 功能描述 我常用的配置
UART_BR 0x00 波特率配置寄存器 根据时钟频率计算分频值
UART_CR 0x04 控制寄存器(使能、中断、模式) 使能TX/RX,开启接收中断
UART_LCR 0x08 线路控制寄存器(数据位/停止位/校验位) 8N1(8数据位、无校验、1停止位)
UART_SR 0x0C 状态寄存器(发送完成、接收就绪、溢出等) 轮询或中断中读取判断
UART_TDR 0x10 发送数据寄存器 写入要发送的字节
UART_RDR 0x14 接收数据寄存器 读取接收到的字节

这6个寄存器,就是咱们操作UART的全部家当。你想想看,一个串口收发,本质上就是:配波特率 → 设格式 → 写TDR发数据 → 读RDR收数据。就这么简单。

注意:CMS32的UART模块在配置寄存器时,建议先关闭UART使能(CR寄存器中的EN位清0),等所有参数配好后再打开。我曾经在项目里图省事,没关使能就直接改波特率,结果串口直接"哑"了,排查了半天才发现是配置时序的问题。

4.3 波特率配置:算不对就等着收乱码

波特率是UART通信的"节拍器"。收发双方必须用同一个速率,否则收到的数据全是乱码。

CMS32的波特率生成原理是这样的:系统时钟(比如48MHz)经过一个分频器,得到目标波特率。分频值计算公式为:

分频值 = 系统时钟频率 / (目标波特率 × 16) - 1

为什么要除以16?因为UART模块内部会用16倍波特率的时钟去采样每一位数据,这样能提高抗干扰能力。这个16是硬件固定的,咱们不用纠结。

举个例子,假设系统时钟是48MHz,想要115200bps的波特率:

分频值 = 48000000 / (115200 × 16) - 1
       = 48000000 / 1843200 - 1
       = 26.04 - 1
       ≈ 25(取整)

取整后实际波特率 = 48000000 / (16 × (25 + 1)) = 48000000 / 416 = 115384.6 bps

误差 = (115384.6 - 115200) / 115200 ≈ 0.16%

波特率误差容忍度:一般要求误差在±2%以内。0.16%完全没问题。但如果系统时钟是16MHz,想要921600bps,分频值算出来可能只有0点几,取整后误差就大了。这时候就得考虑换更高精度的时钟源,或者降低波特率。

代码实现也很直接:

/* 波特率配置函数 */
void UART_SetBaudRate(uint32_t sys_clk_hz, uint32_t target_baud) {
    uint32_t divisor;
    
    /* 计算分频值 */
    divisor = sys_clk_hz / (target_baud * 16) - 1;
    
    /* 写入波特率寄存器 */
    UART_BR = divisor & 0xFFFF;
    
    /* 我习惯在这里加个断言,检查误差是否在允许范围内 */
    /* 实际项目中可以加个调试打印 */
}

避坑指南:我曾经在一个项目里,系统时钟是24MHz,想要配个9600波特率。算出来分频值是155.5,取整156。结果实际波特率是9600.6,误差0.006%,看起来没问题对吧?但那个项目用的是外部晶振,精度只有±50ppm,加上温度漂移,实际误差就超了。所以,波特率配置一定要考虑时钟源的精度,不能只看理论计算。

4.4 数据位/停止位/校验位设置

这三个参数都在线路控制寄存器(UART_LCR)里配。咱们一个个说。

4.4.1 数据位长度

CMS32支持5、6、7、8位数据长度。现在绝大多数场景都用8位,因为一字节正好8位。但有些老设备或者特殊协议会用7位甚至5位,比如某些工业仪表用7位ASCII码传输。

LCR寄存器中,数据位由两个位控制(假设为LCR[1:0]):

LCR[1:0] 数据位长度 适用场景
00 5位 老式电传打字机(现在基本不用)
01 6位 极少见
10 7位 ASCII码传输(某些工业设备)
11 8位 最常用,默认选这个

4.4.2 停止位长度

停止位是帧结束的标志,高电平。CMS32支持1位、1.5位和2位停止位。

  • 1位停止位:最常用,效率最高
  • 1.5位:很少见,某些老设备会用
  • 2位停止位:给接收端更多处理时间,适合低速设备或长距离传输

我个人习惯,只要不是特殊要求,一律用1位停止位。2位停止位相当于每帧多浪费1位时间,在高速传输时影响挺明显的。

4.4.3 校验位

校验位用于简单的错误检测。有三种模式:

  • 无校验(None):不发送校验位,帧长度最短
  • 奇校验(Odd):数据位+校验位中1的总数为奇数
  • 偶校验(Even):数据位+校验位中1的总数为偶数

举个例子,数据是0x55(二进制01010101,有4个1):

  • 奇校验:校验位=1(让总1数变成5,奇数)
  • 偶校验:校验位=0(总1数保持4,偶数)

配置代码也很简单:

/* 配置UART为8N1模式:8数据位、无校验、1停止位 */
void UART_SetFormat_8N1(void) {
    uint32_t lcr_val = 0;
    
    /* 数据位:8位 -> LCR[1:0] = 11 */
    lcr_val |= (0x03 << 0);
    
    /* 停止位:1位 -> LCR[2] = 0 */
    lcr_val &= ~(1 << 2);
    
    /* 校验位:无校验 -> LCR[4:3] = 00 */
    lcr_val &= ~((1 << 3) | (1 << 4));
    
    /* 写入寄存器 */
    UART_LCR = lcr_val;
}

重要提醒:配置LCR寄存器时,一定要一次性写完所有位,不要分多次写。因为LCR的某些位变化会触发硬件重新初始化,分步写可能导致中间状态不稳定。我见过有同事先配数据位,再配停止位,结果中间那几微秒里UART发出了错误的帧,把对端设备搞"懵"了。

4.5 完整初始化流程总结

好了,知识点都讲完了。咱们把UART初始化的完整流程串一下:

  1. 使能UART模块时钟(在时钟管理寄存器里打开UART外设时钟门控)
  2. 配置GPIO引脚复用(把TX、RX引脚设为UART功能)
  3. 关闭UART使能(CR寄存器EN位清0)
  4. 配置波特率(写UART_BR寄存器)
  5. 配置数据格式(写UART_LCR寄存器,一次性完成)
  6. 配置中断(可选)(使能接收中断、发送完成中断等)
  7. 打开UART使能(CR寄存器EN位置1)

这个顺序是我多年调试总结出来的,每一步都有它的道理。尤其是第3步和第7步,先关再开,能保证所有配置在UART使能前就绪,避免出现不可预期的行为。

核心要点回顾:

  • UART是异步通信,收发双方靠约定好的波特率同步
  • 数据帧结构:起始位(1) + 数据位(5~8) + 校验位(0/1) + 停止位(1/1.5/2)
  • 波特率分频值 = 系统时钟 / (目标波特率 × 16) - 1,误差需控制在±2%以内
  • LCR寄存器必须一次性配置,避免中间态
  • 初始化流程:时钟→GPIO→关使能→配参数→开使能