4. UART串口通信:从原理到实战

UART串口,可以说是嵌入式开发中最基础、也最常用的通信接口了。我刚开始做STM32项目时,第一个调通的通信外设就是UART。说白了,它就是让两个设备之间能“说上话”的最简单方式。

这一章,我会带你从UART的底层原理讲起,然后手把手配置CubeMX,最后用HAL库实现串口打印和数据回显。嗯,这些都是我实际项目中反复用到的套路。

4.1 UART协议原理:一帧数据是怎么传的?

UART全称是Universal Asynchronous Receiver/Transmitter,通用异步收发器。它用两根线就能实现全双工通信:TX发、RX收。

为什么叫“异步”?因为没有时钟线。收发双方靠约定的波特率来同步。你想想看,就像两个人约定好每秒说一个字,不用额外喊“开始”和“结束”。

UART一帧数据的标准格式:

  • 起始位(1位):拉低电平,告诉接收方“我要开始发了”
  • 数据位(5~9位):实际数据,常用8位
  • 校验位(可选):奇校验或偶校验,用于检错
  • 停止位(1/1.5/2位):拉高电平,表示一帧结束

我在项目中遇到过一个问题:两个设备波特率都是115200,但通信就是乱码。查了半天,发现是停止位设置不一致——一个设了1位,另一个设了2位。所以,收发双方的帧格式必须完全一致。

UART一帧数据格式 空闲 起始 D0 D1 D2 D3 D4 D5 D6 D7 校验 停止 关键参数说明: • 波特率:每秒传输的位数,常用9600、115200、921600 • 数据位:通常8位,对应一个字节 • 停止位:1位最常用,2位用于噪声环境 • 校验位:无校验最常用,奇偶校验用于低层检错

4.2 CubeMX配置UART:三步搞定

我个人习惯用CubeMX生成初始化代码,省时省力。配置UART其实就三步:选引脚、设参数、生成代码。

4.2.1 引脚选择

STM32的USART外设通常有多个,比如USART1、USART2、USART3。每个外设的TX/RX引脚是固定的。以STM32F103C8T6为例:

USART外设 TX引脚 RX引脚 常用场景
USART1 PA9 PA10 调试打印、与PC通信
USART2 PA2 PA3 蓝牙模块、GPS模块
USART3 PB10 PB11 RS485、传感器

我的习惯:调试打印用USART1,因为PA9/PA10正好对应板载的USB转串口芯片,插上USB线就能看打印信息。

4.2.2 参数配置

在CubeMX的Pinout & Configuration界面,选中USART1,然后在Configuration选项卡里设置:

  • Mode:Asynchronous(异步模式)
  • Baud Rate:115200 Bits/s
  • Word Length:8 Bits(含校验位时选9)
  • Parity:None
  • Stop Bits:1

为什么选115200?因为速度快,而且大多数串口工具都支持。如果线比较长或者环境有干扰,可以降到9600。

4.2.3 生成代码

配置完点生成代码,CubeMX会自动创建usart.c和usart.h。你会在main.c里看到类似这样的初始化:

// CubeMX生成的UART初始化代码
static void MX_USART1_UART_Init(void)
{
    huart1.Instance = USART1;
    huart1.Init.BaudRate = 115200;
    huart1.Init.WordLength = UART_WORDLENGTH_8B;
    huart1.Init.StopBits = UART_STOPBITS_1;
    huart1.Init.Parity = UART_PARITY_NONE;
    huart1.Init.Mode = UART_MODE_TX_RX;
    huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;
    huart1.Init.OverSampling = UART_OVERSAMPLING_16;
    if (HAL_UART_Init(&huart1) != HAL_OK)
    {
        Error_Handler();
    }
}

4.3 HAL库收发函数:核心API

HAL库把UART操作封装成了几个函数。我刚开始用的时候也觉得函数名太长,但用习惯了就会发现,命名很规范,一看就知道是干什么的。

4.3.1 发送函数

函数名 说明 阻塞/非阻塞
HAL_UART_Transmit 阻塞发送,发完才返回 阻塞
HAL_UART_Transmit_IT 中断发送,后台完成 非阻塞
HAL_UART_Transmit_DMA DMA发送,CPU零负担 非阻塞

调试阶段我一般用阻塞发送,简单可靠。产品中如果数据量大,我会用DMA。

// 阻塞发送示例:发送"Hello World"
char msg[] = "Hello World\r\n";
HAL_UART_Transmit(&huart1, (uint8_t *)msg, strlen(msg), HAL_MAX_DELAY);

4.3.2 接收函数

函数名 说明 阻塞/非阻塞
HAL_UART_Receive 阻塞接收,收够才返回 阻塞
HAL_UART_Receive_IT 中断接收,每收到一个字节触发回调 非阻塞
HAL_UART_Receive_DMA DMA接收,批量传输 非阻塞

我曾经踩过的坑:用HAL_UART_Receive阻塞接收时,如果对方发来的数据长度小于你指定的长度,程序会一直卡在那里。所以,不确定数据长度时,一定要用中断或DMA方式。

4.4 实现串口打印与数据回显

好了,理论讲完了,咱们直接上代码。我会实现两个功能:串口打印和串口回显。

4.4.1 重定向printf到串口

在STM32上,printf默认输出到标准输出,但标准输出没地方去。我们需要重定向_write函数,让printf的数据通过UART发出去。

/* 在usart.c或main.c中添加 */
#include <stdio.h>

#ifdef __GNUC__
  /* 使用GCC时 */
  int _write(int file, char *ptr, int len)
  {
      HAL_UART_Transmit(&huart1, (uint8_t *)ptr, len, HAL_MAX_DELAY);
      return len;
  }
#else
  /* 使用ARMCC时 */
  int fputc(int ch, FILE *f)
  {
      HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
      return ch;
  }
#endif

加上这段代码后,你就可以直接用printf了:

printf("系统启动成功!\r\n");
printf("当前温度:%.2f°C\r\n", temperature);

小技巧:记得在字符串末尾加\r\n,这样串口工具才会换行。不加的话,所有输出会挤在一行。

4.4.2 数据回显:你发什么,我回什么

回显功能很简单:收到一个字节,马上把它发回去。用中断方式实现:

/* 全局变量 */
uint8_t rx_byte;  // 接收缓冲区

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_USART1_UART_Init();
    
    printf("UART回显测试开始...\r\n");
    
    /* 启动中断接收,每次接收1个字节 */
    HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
    
    while (1)
    {
        /* 主循环可以干别的事 */
        HAL_Delay(1000);
        printf("主循环运行中...\r\n");
    }
}

/* 中断回调函数:收到数据后自动调用 */
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1)
    {
        /* 把收到的字节发回去(回显) */
        HAL_UART_Transmit(&huart1, &rx_byte, 1, HAL_MAX_DELAY);
        
        /* 重新启动中断接收 */
        HAL_UART_Receive_IT(&huart1, &rx_byte, 1);
    }
}

这段代码的逻辑很简单:

  1. 初始化UART后,启动中断接收
  2. 每次收到一个字节,回调函数HAL_UART_RxCpltCallback被触发
  3. 在回调里,把收到的字节发回去
  4. 重新启动中断接收,等待下一个字节

关键点:中断接收是一次性的。每次收到指定长度的数据后,中断就停了。所以回调里必须重新调用HAL_UART_Receive_IT,否则只能收到一次数据。

4.5 实战中的避坑指南

最后,分享几个我在项目中踩过的坑,希望能帮你少走弯路。

坑1:波特率误差

STM32的UART时钟来自APB总线。如果系统时钟不是波特率的整数倍,会产生误差。误差超过2%就可能丢数据。我建议用8MHz、72MHz、168MHz这些常见频率,它们对常用波特率支持最好。

坑2:中断优先级

如果系统里同时用了多个中断,UART中断优先级设置不当会导致数据丢失。我一般把UART中断优先级设得比较高,尤其是接收中断。

坑3:缓冲区溢出

中断接收时,如果数据来得太快,而你在回调里处理得太慢,下一个字节就会覆盖上一个。解决办法:用环形缓冲区(ring buffer)或者DMA。

嗯,UART串口通信的内容就这些。从原理到配置,再到代码实现,每一步我都结合项目经验给你讲透了。你可以在开发板上试试回显程序,用串口工具发个"Hello",看看它会不会回你一个"Hello"。