4. UART 裸机实战:发送与接收

好,咱们今天直接上手干。UART 协议的理论,说白了就是一根 TX、一根 RX,加上一个共同的地。但理论归理论,真到代码里,你会发现坑比想象的多。我最早用 STM32 做串口时,就因为在配置里少写了一句时钟使能,愣是盯着逻辑分析仪看了半小时——嗯,后来再也没犯过这种低级错误。

4.1 硬件连接与准备工作

先看看你需要什么:

  • 开发板:STM32F103C8T6(蓝板子)或 Arduino Uno
  • USB 转 TTL 模块:CH340G 或 CP2102 都行
  • 杜邦线:母对母,三根
  • 串口助手:我习惯用 SSCOM,或者 Putty 也可以
⚠️ 接线警告: 千万别把 TX 接 TX,RX 接 RX。正确接法是:
开发板 TX → 模块 RX
开发板 RX → 模块 TX
开发板 GND → 模块 GND
我见过有人接反了,然后跑来问我为什么收不到数据——其实就是交叉一下的事。

4.2 STM32 裸机发送:轮询方式

先写发送。STM32 的 USART 发送其实就三步:

  1. 使能 USART 时钟和 GPIO 时钟
  2. 配置 TX 引脚为复用推挽输出
  3. 配置 USART 参数(波特率、数据位、停止位)

下面是我常用的初始化代码,直接贴寄存器版本,不依赖 HAL 库——你想想看,用寄存器才能真的理解底层在干什么。

void USART1_Init(void)
{
    // 1. 使能时钟
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

    // 2. 配置 PA9 (TX) 为复用推挽输出,50MHz
    GPIOA->CRH &= ~(0xF << 4);   // 先清零
    GPIOA->CRH |= (0xB << 4);    // 1011: 复用推挽输出

    // 3. 配置 USART 参数
    USART1->BRR = 0x1D4C;        // 72MHz / 9600 = 7500 = 0x1D4C
    USART1->CR1 |= USART_CR1_TE; // 使能发送
    USART1->CR1 |= USART_CR1_UE; // 使能 USART
}
💡 波特率计算小技巧: 72MHz 时钟下,要得到 9600 波特率,分频系数 = 72000000 / (16 × 9600) = 468.75。取整后写入 BRR 寄存器的高 12 位和低 4 位。我一般直接用计算器算好十六进制值,省得调试时还要心算。

发送一个字节的函数更简单:

void USART1_SendByte(uint8_t data)
{
    // 等待发送数据寄存器空
    while (!(USART1->SR & USART_SR_TXE));
    USART1->DR = data;
}

这里有个细节:TXE 标志位表示数据寄存器已空,可以写入下一个字节。但如果你连续发送大量数据,最好也检查 TC(发送完成)标志,确保最后一个字节真的从移位寄存器送出去了。我在做 GPS 数据解析时就吃过这个亏——最后几个字节老是丢。

4.3 STM32 裸机接收:轮询方式

接收比发送多一个坑:你怎么知道数据来了?轮询方式就是不停地读 RXNE 标志位。

uint8_t USART1_ReceiveByte(void)
{
    // 等待接收数据寄存器非空
    while (!(USART1->SR & USART_SR_RXNE));
    return (uint8_t)(USART1->DR & 0xFF);
}

但轮询有个致命问题:它会卡死 CPU。如果你在主循环里只做接收,那没问题。但如果你还要处理其他任务,就得用中断了。这个咱们后面章节再讲。

🔑 核心要点: 轮询方式适合“发完等回”的场景,比如向传感器发指令然后等应答。不适合持续接收数据流。

4.4 Arduino 实现:更简单,但原理一样

Arduino 把底层封装好了,但咱们得知道它背后干了什么。看这段代码:

void setup() {
    Serial.begin(9600);  // 初始化,波特率 9600
}

void loop() {
    // 发送
    Serial.println("Hello, UART!");

    // 接收
    if (Serial.available() > 0) {
        char c = Serial.read();
        Serial.print("Received: ");
        Serial.println(c);
    }
    delay(1000);
}

Serial.begin() 背后做的事,跟咱们刚才写的寄存器配置一模一样:设置波特率、配置引脚、使能 USART。只不过 Arduino 帮你选了引脚(通常是 D0/RX 和 D1/TX)。

我个人习惯在 Arduino 上先快速验证协议,再移植到 STM32 上做产品。毕竟 Arduino 的调试效率高,出了问题换代码也快。

4.5 串口助手调试实战

好,代码写完了,怎么验证?打开串口助手,设置好参数:

参数 值 说明
波特率 9600 必须与代码一致
数据位 8 默认值
停止位 1 默认值
校验位 None 无校验
流控制 None 无流控

点击“打开串口”,你应该能看到开发板不断发送 "Hello, UART!"。然后在发送区输入字符,点发送,看能不能收到回显。

⚠️ 常见故障:
  • 收不到数据:先检查 TX/RX 是否交叉,再量一下 TX 引脚有没有波形(用示波器或逻辑分析仪)。我曾经有一次是杜邦线接触不良,折腾了半小时。
  • 数据乱码:波特率不匹配。检查代码里的时钟频率和分频系数对不对。
  • 只能收不能发:检查 TX 引脚配置是不是复用推挽输出,或者是不是被其他外设占用了。

4.6 知识体系:UART 裸机通信流程

下面这张图把整个流程串起来了,你一看就明白:

UART 裸机通信流程 配置时钟与GPIO 设置USART参数 等待TXE标志 写入DR寄存器 配置时钟与GPIO 设置USART参数 等待RXNE标志 读取DR寄存器 串口助手 显示/发送数据 TX→RX TX→RX 发送和接收是独立的两个路径,可以同时进行(全双工)

4.7 避坑指南:我踩过的那些雷

  • 时钟没使能:USART 和 GPIO 的时钟都要使能。我刚开始学 STM32 时,只使能了 USART 时钟,忘了 GPIO 时钟,结果 TX 引脚一直输出高电平,死活发不出数据。
  • 波特率误差:如果系统时钟不是整数倍,波特率会有误差。误差超过 2% 就可能丢数据。我建议用 72MHz 或 8MHz 这种常见频率,算出来的分频系数比较整。
  • 忘记接地:GND 不接,信号没有参考地,数据全是乱的。这听起来很基础,但我在培训新人时,十个人里有三个会犯这个错。
  • 串口助手缓冲区溢出:如果你一次性发太多数据,串口助手的接收缓冲区可能不够。我一般把接收缓冲区设到 10KB 以上。
🔧 调试利器: 逻辑分析仪是调试 UART 最好的工具。你直接看 TX 引脚的波形,有没有起始位、数据位对不对、停止位有没有,一目了然。我包里常备一个 8 通道的,几十块钱,但能省下几小时的排查时间。

好了,这一章的内容就到这儿。代码都给你了,图也画清楚了,剩下的就是你自己动手试。记住:看十遍不如写一遍,写十遍不如调一遍。去把串口调通,你会对 UART 有更深的理解。