UART基础与HAL库轮询模式

各位同学,咱们今天聊点实在的。串口通信,说白了就是嵌入式工程师的"嘴"和"耳朵"。你想想看,一个单片机要是不能跟外界说话,那跟一块砖头有什么区别?我刚开始做嵌入式那会儿,第一个调通的外设就是UART,当时看着串口助手上一行行打印出来的数据,那种成就感,到现在还记得。

这一章,咱们就把UART的底裤扒干净。从协议基础到HAL库的轮询收发,一步步来。

UART协议基础

UART全称是Universal Asynchronous Receiver/Transmitter,通用异步收发器。注意"异步"这两个字——收发双方没有独立的时钟线,全靠约定好的参数来对齐数据。

一条UART通信线路上,默认是空闲高电平。起始位拉低,然后从最低位开始传数据,最后是停止位拉高。就这么简单。

参数 常见取值 我的建议
波特率 9600、115200、460800 调试用115200,量产看需求
数据位 8位(最常用) 除非特殊协议,否则用8位
停止位 1位、2位 1位就够了,2位浪费带宽
校验位 无、奇校验、偶校验 大多数场景用无校验
注意:收发双方的波特率误差不能超过±2%。我遇到过一块劣质USB转串口模块,标称115200实际只有112000,折腾了我一整天。后来换了FT232芯片的模块,问题立刻解决。

波特率怎么算?以STM32F103为例,APB2总线时钟72MHz,USART1挂在这个总线上。波特率计算公式是:

波特率 = 时钟频率 / (16 × USARTDIV)

USARTDIV是一个小数分频系数,HAL库的HAL_UART_Init()会帮你算好。你只需要在CubeMX里选个波特率就行。

HAL_UART_Transmit 与 HAL_UART_Receive 详解

这两个函数,是HAL库UART轮询模式的核心。咱们先看发送:

HAL_StatusTypeDef HAL_UART_Transmit(
    UART_HandleTypeDef *huart,
    uint8_t *pData,
    uint16_t Size,
    uint32_t Timeout
);

参数不多,但每个都有讲究:

  • huart:UART句柄指针,CubeMX生成的&huart1这种
  • pData:要发送的数据缓冲区指针
  • Size:发送的字节数
  • Timeout:超时时间,单位ms。设为HAL_MAX_DELAY表示一直等

返回值是HAL_OK、HAL_ERROR、HAL_BUSY或HAL_TIMEOUT。我个人习惯每次调用后都检查返回值,哪怕是在demo代码里。

小技巧:Timeout参数别设太大。调试时设1000ms就够了,如果1秒还没发完,大概率是硬件有问题。我曾经把Timeout设成HAL_MAX_DELAY,结果硬件没接好,程序卡死在发送函数里,连调试器都连不上。

接收函数长这样:

HAL_StatusTypeDef HAL_UART_Receive(
    UART_HandleTypeDef *huart,
    uint8_t *pData,
    uint16_t Size,
    uint32_t Timeout
);

参数跟发送一模一样。但有个坑——这个函数必须收到正好Size个字节才会返回。少一个字节都不行,会一直等直到超时。

为什么会这样?因为HAL库的轮询接收是"阻塞式"的。它在内部用一个while循环,不断检查RXNE标志位,收一个字节存一个,直到收满Size个。

轮询模式实现数据收发

咱们写个最简单的例子。STM32F103C8T6,USART1,PA9-TX,PA10-RX,115200-8N1。

初始化代码CubeMX帮你生成了,咱们直接看收发:

// 发送一个字符串
uint8_t tx_buf[] = "Hello, UART!\r\n";
HAL_UART_Transmit(&huart1, tx_buf, sizeof(tx_buf)-1, 1000);

// 接收5个字节
uint8_t rx_buf[5];
if(HAL_UART_Receive(&huart1, rx_buf, 5, 1000) == HAL_OK)
{
    // 收到5个字节,处理数据
    // 注意:如果只收到3个字节,函数会超时返回HAL_TIMEOUT
}

这段代码看着简单,但实际项目里很少这么用。为什么?因为轮询模式会阻塞CPU。在等待接收的1000ms里,CPU啥也干不了。你想想看,如果主循环里还有其他任务——比如读传感器、控制电机——那整个系统就卡死了。

轮询模式的适用场景:
  • 调试阶段,临时打印日志
  • 与PC端简单交互,数据量小
  • 系统空闲,没有其他实时任务

实战:PC端串口助手收发数据

咱们来做个实验。用STM32开发板,接上USB转串口模块,打开串口助手。

代码逻辑很简单:

  1. 上电后发送"UART Ready!\r\n"
  2. 等待接收1个字节
  3. 收到后,把收到的字节加1,再发回去
  4. 循环
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_USART1_UART_Init();

    uint8_t tx_ready[] = "UART Ready!\r\n";
    HAL_UART_Transmit(&huart1, tx_ready, sizeof(tx_ready)-1, 100);

    uint8_t rx_byte;
    uint8_t tx_byte;

    while (1)
    {
        // 等待接收1个字节
        if(HAL_UART_Receive(&huart1, &rx_byte, 1, 5000) == HAL_OK)
        {
            // 收到数据,加1后回发
            tx_byte = rx_byte + 1;
            HAL_UART_Transmit(&huart1, &tx_byte, 1, 100);
        }
        else
        {
            // 超时,打印提示
            uint8_t timeout_msg[] = "Timeout!\r\n";
            HAL_UART_Transmit(&huart1, timeout_msg, sizeof(timeout_msg)-1, 100);
        }
    }
}

在串口助手里发送字符'A'(0x41),应该收到'B'(0x42)。如果收不到,别急,按下面的步骤排查:

  • 检查串口助手的波特率、数据位、停止位是否跟代码一致
  • 检查TX/RX有没有接反——我犯过这个错不下五次
  • 检查共地——两个设备的GND必须连在一起
  • 用示波器或逻辑分析仪看TX引脚有没有波形
避坑指南:我曾经在调试一个量产项目时,发现10块板子里有2块串口不通。查了半天,发现是USB转串口模块的TX引脚电平是3.3V,但STM32的RX引脚容忍5V——问题出在模块的TX驱动能力不足。后来换了带电平转换的模块,问题解决。所以,硬件选型一定要看datasheet,别想当然。

好了,咱们用一张图来总结本章的核心知识:

UART轮询模式知识体系 UART轮询模式 协议基础 波特率:9600/115200 数据位:8位 停止位:1位 HAL库函数 HAL_UART_Transmit HAL_UART_Receive 参数:pData/Size/Timeout 实战要点 TX/RX不能接反 GND必须共地 检查波特率匹配 核心要点 轮询模式:阻塞CPU,适合调试和简单交互 必须检查函数返回值,不能假设每次调用都成功

轮询模式虽然简单,但它是理解UART通信的基础。你把这个搞透了,后面学中断和DMA就会轻松很多。记住:先跑通轮询,再谈优化。