3. UART驱动实战:STM32 HAL库轮询模式收发数据

UART,说白了就是串口。做嵌入式开发的人,没人离得开它。我最早接触单片机时,第一个调通的外设就是UART——看着串口助手打印出"Hello World",那种感觉,嗯,至今还记得。

这一章咱们就动手实战。用STM32的HAL库,把UART轮询模式跑起来。你跟着我一步步来,很快就能看到效果。

3.1 轮询模式是什么?

轮询模式,就是CPU一直盯着某个标志位,等它变化。比如发数据时,等发送完成标志位置位;收数据时,等接收完成标志位置位。

说白了,就是"死等"。CPU在这期间啥也不干,就干等。

轮询模式的特点:

  • 代码简单,容易理解
  • 适合短数据、低频通信
  • CPU占用率高,不能做其他事
  • 实时性差,不适合高速或大数据量场景

我在项目中遇到过一个小白同事,用轮询模式跑115200波特率,一次发几百字节,结果整个系统卡死了。后来改成中断模式才解决。所以你要记住:轮询模式适合小数据量,别贪多。

3.2 STM32CubeMX配置UART

咱们用STM32F103C8T6做演示。先打开CubeMX,配置UART1。

配置步骤:

  1. 选择芯片型号:STM32F103C8Tx
  2. 在Pinout & Configuration页面,找到USART1
  3. Mode选择Asynchronous(异步模式)
  4. Parameter Settings里设置:
    • Baud Rate:115200 Bits/s
    • Word Length:8 Bits
    • Parity:None
    • Stop Bits:1
  5. NVIC Settings里,暂时不使能中断(轮询模式不需要)
  6. 生成代码

小提示:波特率115200是常用值。如果通信不稳定,可以降到9600试试。我调试时经常先设9600,通了再往上提。

3.3 HAL库UART轮询API

HAL库给我们封装好了三个轮询函数。你记住它们就行:

函数名 功能 超时参数
HAL_UART_Transmit 发送数据 超时时间(ms)
HAL_UART_Receive 接收数据 超时时间(ms)
HAL_UART_Transmit_IT 中断发送(本章不用) —

函数原型长这样:

// 发送:pData是数据指针,Size是字节数,Timeout是超时时间
HAL_StatusTypeDef HAL_UART_Transmit(UART_HandleTypeDef *huart, 
                                     uint8_t *pData, 
                                     uint16_t Size, 
                                     uint32_t Timeout);

// 接收:同样参数结构
HAL_StatusTypeDef HAL_UART_Receive(UART_HandleTypeDef *huart, 
                                    uint8_t *pData, 
                                    uint16_t Size, 
                                    uint32_t Timeout);

返回值是HAL_OK表示成功,HAL_TIMEOUT表示超时,HAL_ERROR表示出错。

3.4 实战:发送"Hello UART"

好,咱们写代码。打开main.c,在main函数里加上发送代码。

#include "main.h"
#include "usart.h"
#include "gpio.h"

uint8_t txBuffer[] = "Hello UART!\r\n";

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();

    while (1)
    {
        // 发送数据,超时时间1000ms
        HAL_UART_Transmit(&huart1, txBuffer, sizeof(txBuffer) - 1, 1000);
        
        // 延时1秒
        HAL_Delay(1000);
    }
}

编译下载,打开串口助手。你应该能看到每隔1秒打印一次"Hello UART!"。

注意:sizeof(txBuffer) - 1是为了去掉字符串结尾的'\0'。如果你不减1,会多发一个空字符,串口助手可能显示乱码。我刚开始学时就踩过这个坑。

3.5 实战:接收并回显

光发不收,那是单工。咱们来个回显程序:收到什么,就发回什么。

uint8_t rxByte;

while (1)
{
    // 接收1个字节,超时时间1000ms
    if (HAL_UART_Receive(&huart1, &rxByte, 1, 1000) == HAL_OK)
    {
        // 收到数据,原样发回
        HAL_UART_Transmit(&huart1, &rxByte, 1, 1000);
    }
}

你往串口助手发一个字符,它就会回显一个字符。这就是最简单的"串口回环"。

为什么会这样?因为HAL_UART_Receive在收到数据前会一直等(轮询),直到超时。收到后立即发回,然后继续等下一个字节。

3.6 轮询模式的坑

我用轮询模式踩过的坑,给你列出来:

  • 超时时间设太短:如果对方回复慢,你的程序会反复超时退出。我建议设1000ms起步。
  • 接收卡死:如果对方不发数据,HAL_UART_Receive会一直等。整个程序就卡在那了。所以轮询模式不适合做"等待不确定时长"的接收。
  • 数据粘包:轮询模式一次收一个字节,如果对方连续发多个字节,你可能只收到第一个,后面的丢了。解决办法是用中断或DMA。

我的建议:轮询模式只用在"发"的场景,或者"收固定长度数据"的场景。不确定长度的接收,别用轮询。

3.7 知识结构图

下面这张图,帮你理清UART轮询模式的核心逻辑:

UART轮询模式收发流程 HAL初始化 HAL_UART_Transmit HAL_UART_Receive 等待TXE标志位 等待TC标志位 等待RXNE标志位 读取DR寄存器 超时判断:返回HAL_OK或HAL_TIMEOUT 轮询模式核心:CPU循环检查标志位,期间不能执行其他任务 适合小数据量、低频通信场景

3.8 完整示例代码

最后,给你一个完整的收发示例。这个程序会先发提示信息,然后等待用户输入,收到后回显并加上换行。

/* main.c - UART轮询模式收发示例 */

#include "main.h"
#include "usart.h"
#include "gpio.h"

uint8_t prompt[] = "Please input a character:\r\n";
uint8_t rxByte;
uint8_t txPrefix[] = "You sent: ";

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_USART1_UART_Init();

    // 发送提示信息
    HAL_UART_Transmit(&huart1, prompt, sizeof(prompt) - 1, 1000);

    while (1)
    {
        // 接收1个字节
        if (HAL_UART_Receive(&huart1, &rxByte, 1, 5000) == HAL_OK)
        {
            // 发送前缀
            HAL_UART_Transmit(&huart1, txPrefix, sizeof(txPrefix) - 1, 1000);
            
            // 发送收到的字符
            HAL_UART_Transmit(&huart1, &rxByte, 1, 1000);
            
            // 发送换行
            uint8_t newline[] = "\r\n";
            HAL_UART_Transmit(&huart1, newline, 2, 1000);
        }
        else
        {
            // 超时处理
            uint8_t timeoutMsg[] = "Timeout! No data received.\r\n";
            HAL_UART_Transmit(&huart1, timeoutMsg, sizeof(timeoutMsg) - 1, 1000);
        }
    }
}

调试技巧:如果串口助手收不到数据,先检查这几项:波特率是否一致?串口线是否接反(TX接RX,RX接TX)?地线是否共地?我调试时至少有一半的问题出在这些地方。