4. 串口通信基础(UART)

UART,说白了就是串口通信的物理层协议。我刚开始做嵌入式时,总觉得这东西太简单,不就是两根线收发数据嘛。直到有一次调试Bootloader,串口死活收不到正确数据,折腾了两天才发现是波特率算错了……嗯,从那以后我再也不敢小看UART的基础知识了。

4.1 UART协议帧格式

UART的帧格式其实很固定。你想想看,它不像SPI或I2C有时钟线,全靠收发双方约定好速率来采样。所以帧结构里必须包含同步信息。

一个标准的UART数据帧长这样:

字段 位数 说明
起始位 1 bit 固定为低电平,标志传输开始
数据位 5~9 bits 常用8位,LSB先发
校验位 0/1 bit 可选奇校验/偶校验/无校验
停止位 1/1.5/2 bits 固定为高电平,标志帧结束

我个人习惯用8N1——8位数据、无校验、1位停止位。这是最通用的配置,兼容性最好。你在项目中如果遇到通信双方协议不一致,先检查这个帧格式配置。

关键点:UART是异步通信,没有时钟线。收发双方必须用相同的波特率、相同的帧格式,否则数据就是乱码。

4.2 波特率计算

波特率就是每秒传输的符号数。对于UART来说,1个符号就是1个bit。所以波特率 = 比特率。

CMS32的串口模块通常由系统时钟分频得到波特率。计算公式如下:

波特率 = 系统时钟频率 / (16 × (UBR + 1))

其中UBR是波特率寄存器值。反过来算:

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

举个例子。系统时钟24MHz,目标波特率115200:

UBR = (24000000 / (16 × 115200)) - 1
    = (24000000 / 1843200) - 1
    ≈ 13.02 - 1
    = 12.02

取整后UBR = 12。实际波特率 = 24000000 / (16 × 13) ≈ 115384。误差只有0.16%,完全可用。

注意:波特率误差超过2%就容易丢数据。我曾经在项目中用8MHz晶振配9600波特率,算出来误差3.1%,结果串口助手收的数据全是乱码。后来换了11.0592MHz晶振才解决。

常用波特率与UBR值对照表(系统时钟24MHz):

目标波特率 UBR计算值 UBR取整 实际波特率 误差
9600 155.25 155 9615 0.16%
19200 77.125 77 19231 0.16%
38400 38.0625 38 38462 0.16%
57600 25.04 25 57692 0.16%
115200 12.02 12 115385 0.16%

4.3 CMS32串口寄存器配置

CMS32的串口模块叫UARTn,n从0开始。配置串口,说白了就是操作几个关键寄存器。

我一般按这个顺序配置:

  1. 时钟使能:打开UART模块时钟
  2. 引脚复用:把GPIO配置为UART功能
  3. 波特率设置:写UBR寄存器
  4. 帧格式设置:数据位、校验位、停止位
  5. 中断使能:如果需要中断方式
  6. 使能UART:启动收发

核心寄存器一览:

寄存器 功能
UARTn_BRR 波特率寄存器,写入UBR值
UARTn_CR1 控制寄存器1,使能UART、设置数据位
UARTn_CR2 控制寄存器2,设置停止位、校验位
UARTn_SR 状态寄存器,查询收发完成标志
UARTn_DR 数据寄存器,读写收发数据

配置代码示例:

void UART1_Init(void)
{
    // 1. 时钟使能
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_UART1, ENABLE);
    
    // 2. 引脚复用:PA9 = TX, PA10 = RX
    GPIO_InitTypeDef GPIO_InitStruct;
    GPIO_InitStruct.GPIO_Pin = GPIO_Pin_9 | GPIO_Pin_10;
    GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AF_PP;
    GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_Init(GPIOA, &GPIO_InitStruct);
    
    // 3. 波特率设置:115200 @ 24MHz
    UART1->BRR = 12;
    
    // 4. 帧格式:8N1
    UART1->CR1 = 0x00;      // 8位数据
    UART1->CR2 = 0x00;      // 1位停止位,无校验
    
    // 5. 使能UART
    UART1->CR1 |= 0x2000;   // UE使能位
    UART1->CR1 |= 0x000C;   // TE和RE使能
}

小技巧:配置完寄存器后,先发一个0x55(二进制01010101)测试。如果示波器看到方波,说明配置没问题。我每次调串口都这么干,省了不少排查时间。

4.4 阻塞式发送与接收

阻塞式,说白了就是CPU死等。发送时等数据发完,接收时等数据到来。效率不高,但简单可靠。

发送一个字节:

void UART1_SendByte(uint8_t data)
{
    // 等待发送缓冲区空
    while (!(UART1->SR & 0x0080));
    // 写入数据
    UART1->DR = data;
}

接收一个字节:

uint8_t UART1_ReceiveByte(void)
{
    // 等待接收数据就绪
    while (!(UART1->SR & 0x0020));
    // 读取数据
    return (uint8_t)(UART1->DR & 0xFF);
}

发送字符串:

void UART1_SendString(const char *str)
{
    while (*str)
    {
        UART1_SendByte(*str++);
    }
}

注意:阻塞式接收有个大坑——如果对方不发数据,程序就卡死在while循环里。我在Bootloader项目中遇到过,上位机掉线了,下位机一直等接收,整个系统死机。解决方案是加超时机制,或者用中断+超时。

4.5 串口打印重定向(printf)

printf是调试利器。但嵌入式C库的printf默认输出到stdout,不会自动走串口。我们需要重定向。

重定向的核心就是实现fputc函数。C库的printf最终会调用fputc来输出每个字符。

int fputc(int ch, FILE *f)
{
    // 阻塞发送一个字符
    while (!(UART1->SR & 0x0080));
    UART1->DR = (uint8_t)ch;
    return ch;
}

然后在工程设置里勾选"Use MicroLIB"。MicroLIB是ARM提供的精简C库,体积小,支持printf重定向。

配置完成后,就可以愉快地用了:

printf("系统初始化完成,当前温度:%d°C\n", temperature);
printf("Bootloader版本:%s\n", VERSION_STRING);

经验之谈:printf默认是阻塞的。如果串口速率低(比如9600),打印大量数据会拖慢系统。我一般只在调试阶段用printf,正式发布前会加一个宏开关,把printf全部关掉。

4.6 本章知识结构

下面这张图把UART的核心知识点串起来了。从帧格式到寄存器配置,再到收发实现和printf重定向,一条线走通。

UART串口通信知识体系 UART通信 帧格式 波特率计算 寄存器配置 收发实现 起始位/停止位 数据位(8N1) 校验位(可选) 公式:BRR计算 误差控制<2% 常用速率表 BRR/CR1/CR2 SR状态寄存器 DR数据寄存器 阻塞式发送 阻塞式接收 超时机制 printf重定向 → fputc实现

这张图把UART的五个核心模块串起来了。从帧格式到波特率计算,再到寄存器配置和收发实现,最后用printf重定向收尾。每个模块之间都有依赖关系——帧格式决定了寄存器怎么配,波特率算错了收发全是乱码,收发函数又是printf的基础。