【STM32 串口完全指南】从轮询到中断再到 DMA,一步步教你搞定串口收发!


前言

串口(UART/USART)是嵌入式开发中最常用的通信接口,不管是调试打印、传感器数据读取,还是上位机指令交互,都离不开它。

很多新手刚接触串口时,会被"轮询/中断/DMA"这三种模式搞懵:到底什么时候用轮询?中断和DMA又有什么区别?这篇文章会从基础到进阶,带你吃透STM32串口的三种工作模式,每个模式都会讲清原理、CubeMX配置、带注释的示例代码,以及测试验证方法,看完就能直接用到你的项目里!

本文基于STM32 HAL库编写,适用于STM32F1/F4/H7全系列芯片,可直接复制代码使用。


一、串口轮询模式(初识串口)

1.1 工作原理

轮询模式是串口最基础的工作方式,核心逻辑很简单:

  • CPU不断查询串口的状态寄存器,判断"数据是否发送完成"或"是否收到数据"
  • 只有当状态满足条件时,才会执行后续操作,否则一直阻塞在查询过程中
  • 简单来说:CPU全程被串口"占用",没法同时做其他事

适用场景:简单的调试打印、单次短数据收发,不涉及实时性要求高的场景。


1.2 CubeMX配置步骤

以USART1为例,配置步骤如下:

  1. 打开CubeMX,选择你的MCU(如STM32H723VGTX)
  2. 配置RCC时钟,确保系统时钟和APB总线时钟配置正确(串口波特率依赖总线时钟)
  3. 进入Connectivity → 选择USART1 → 模式选择Asynchronous(异步串口)
  4. 配置串口参数:波特率115200,数据位8,无校验位,停止位1(通用的8N1格式)
  5. 生成代码,选择HAL库,打开Keil工程

1.3 示例代码实现(带详细注释)

轮询模式下,我们用HAL_UART_Transmit(发送)和HAL_UART_Receive(接收)两个核心函数,实现"发送调试信息+接收数据回显"的功能:

c 复制代码
#include "stdio.h"
#include "string.h"
#include "main.h"

extern UART_HandleTypeDef huart1; // 引用CubeMX生成的USART1句柄

int main(void)
{
  // CubeMX自动生成的HAL初始化代码
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();

  // 定义发送缓冲区和接收缓冲区
  char send_buf[] = "【轮询模式】Hello UART!\r\n";
  uint8_t recv_buf[16] = {0}; // 接收缓冲区,最多存16个字节

  while (1)
  {
    // ---------------------- 轮询发送数据 ----------------------
    // 函数原型:HAL_UART_Transmit(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
    // 参数说明:
    // huart:串口句柄,这里用huart1
    // pData:要发送的数据缓冲区
    // Size:要发送的字节数
    // Timeout:超时时间(单位ms),超过这个时间没发送完成会返回HAL_TIMEOUT
    HAL_UART_Transmit(&huart1, (uint8_t*)send_buf, strlen(send_buf), 100);


    // ---------------------- 轮询接收数据 ----------------------
    // 函数原型:HAL_UART_Receive(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size, uint32_t Timeout)
    // 功能:阻塞等待,直到收到16个字节或超时1000ms
    HAL_UART_Receive(&huart1, recv_buf, 16, 1000);


    // ---------------------- 把收到的数据回发 ----------------------
    // 如果收到了数据(非空),就通过串口发回去
    if(strlen((char*)recv_buf) > 0)
    {
      HAL_UART_Transmit(&huart1, recv_buf, strlen((char*)recv_buf), 100);
      HAL_UART_Transmit(&huart1, (uint8_t*)"\r\n", 2, 100); // 换行,方便串口助手查看
    }

    memset(recv_buf, 0, sizeof(recv_buf)); // 清空接收缓冲区,避免残留数据影响下一次接收
    HAL_Delay(1000); // 延时1s,避免发送太频繁
  }
}

1.4 测试验证

  1. 编译下载程序到开发板
  2. 用USB转TTL模块连接开发板USART1的TX/RX引脚(注意交叉连接:开发板TX接模块RX,开发板RX接模块TX,GND必须共地)
  3. 打开串口助手,波特率115200,8N1,发送任意字符串,就能看到开发板的回显数据

二、串口中断模式(异步收发进阶)

2.1 工作原理

轮询模式最大的缺点是"阻塞CPU",而中断模式完美解决了这个问题:

  • 串口收到数据或发送完成时,会触发对应的中断信号
  • CPU平时可以正常执行主循环的其他任务(如LED翻转、电机控制),只有收到中断信号时,才会暂停当前任务,进入中断服务函数处理串口数据
  • 处理完数据后,CPU会回到之前被打断的地方继续执行

适用场景:异步接收传感器数据、上位机指令,需要CPU同时处理其他任务的场景。


2.2 CubeMX配置步骤

在轮询模式的基础上,额外开启串口中断:

  1. 进入USART1配置界面 → NVIC Settings 标签页
  2. 勾选USART1 global interrupt,设置中断优先级(一般抢占优先级2,子优先级2即可,避免被其他高优先级中断抢占)
  3. 生成代码

2.3 示例代码实现(带详细注释)

中断模式下,我们用HAL_UART_Receive_IT开启接收中断,用HAL_UART_RxCpltCallback回调函数处理收到的数据:

c 复制代码
#include "stdio.h"
#include "string.h"
#include "main.h"

extern UART_HandleTypeDef huart1;
uint8_t recv_byte; // 单字节接收缓冲区(每次接收1个字节)

int main(void)
{
  // CubeMX自动生成的初始化代码
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_USART1_UART_Init();

  char send_buf[] = "【中断模式】UART IT Ready!\r\n";
  HAL_UART_Transmit(&huart1, (uint8_t*)send_buf, strlen(send_buf), 100);

  // ---------------------- 开启串口接收中断 ----------------------
  // 函数原型:HAL_UART_Receive_IT(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
  // 功能:开启中断模式接收,收到1个字节后触发中断
  HAL_UART_Receive_IT(&huart1, &recv_byte, 1);

  while (1)
  {
    // 主循环可以正常执行其他任务,比如翻转LED
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
    HAL_Delay(500);
  }
}

// ---------------------- 串口接收完成回调函数 ----------------------
// 注意:这是HAL库的弱定义函数,收到数据后会自动调用,用户需要自己实现
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if(huart->Instance == USART1) // 判断是USART1触发的中断,避免多个串口时混淆
  {
    // 把收到的字节回发
    HAL_UART_Transmit(&huart1, &recv_byte, 1, 100);

    // 【重点】中断接收一次后会自动关闭,必须重新开启接收中断,才能继续接收下一个字节!
    HAL_UART_Receive_IT(&huart1, &recv_byte, 1);
  }
}

2.4 测试验证

  1. 编译下载程序,串口助手发送任意字符,开发板会实时回显你发送的内容
  2. 同时可以看到主循环里的LED在正常翻转,说明CPU没有被串口阻塞,能同时处理其他任务

三、串口DMA模式(高性能不定长收发)

3.1 工作原理

中断模式虽然解放了CPU,但每次收到数据都会触发一次中断,频繁中断还是会占用CPU资源。而DMA模式是串口的"终极形态":

  • DMA(直接存储器访问)是一个独立的外设,可以直接在串口外设和内存之间搬运数据,完全不需要CPU参与
  • 发送/接收数据时,CPU可以全程干别的事,只有在DMA传输完成或出现错误时,才会触发一次中断
  • 配合串口空闲中断(IDLE),还可以实现不定长数据接收,完美适配上位机指令、串口屏交互等场景

适用场景:大数据量收发、高波特率通信、不定长数据接收,对CPU占用率要求高的项目。


3.2 CubeMX配置步骤

在中断模式的基础上,配置串口DMA:

  1. 进入USART1配置界面 → DMA Settings 标签页
  2. 点击Add,添加两个DMA通道:
    • USART1_RX:模式选择Normal,勾选Memory Increment(内存地址自增)
    • USART1_TX:模式选择Normal,勾选Memory Increment
  3. 进入NVIC Settings,确认USART1和DMA的中断都已开启
  4. 生成代码

3.3 示例代码实现(不定长数据接收,带详细注释)

DMA模式最常用的场景就是不定长数据接收,我们结合"DMA接收+串口空闲中断"实现这个功能:

c 复制代码
#include "stdio.h"
#include "string.h"
#include "main.h"

extern UART_HandleTypeDef huart1;
extern DMA_HandleTypeDef hdma_usart1_rx;

#define USART1_RX_BUF_LEN 128 // DMA接收缓冲区最大长度,可根据项目需求调整
uint8_t usart1_rx_buf[USART1_RX_BUF_LEN]; // DMA接收缓冲区(必须定义为全局变量,不能是局部变量!)
uint16_t usart1_rx_len = 0; // 实际接收到的数据长度

int main(void)
{
  // CubeMX自动生成的初始化代码
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();

  char send_buf[] = "【DMA模式】UART DMA Ready!\r\n";
  HAL_UART_Transmit(&huart1, (uint8_t*)send_buf, strlen(send_buf), 100);

  // ---------------------- 开启DMA接收 ----------------------
  // 函数原型:HAL_UART_Receive_DMA(UART_HandleTypeDef *huart, uint8_t *pData, uint16_t Size)
  // 功能:开启DMA模式接收,数据会自动存入usart1_rx_buf,直到缓冲区满或触发空闲中断
  HAL_UART_Receive_DMA(&huart1, usart1_rx_buf, USART1_RX_BUF_LEN);

  // ---------------------- 开启串口空闲中断 ----------------------
  // 当串口总线空闲(没有数据传输)时,会触发IDLE中断,用来判断一帧数据接收完成
  __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);

  while (1)
  {
    // CPU完全不参与串口收发,主循环可以执行任何任务
    HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
    HAL_Delay(500);
  }
}

// ---------------------- USART1中断服务函数 ----------------------
// CubeMX会自动生成这个函数,我们只需要在里面添加空闲中断的处理逻辑
void USART1_IRQHandler(void)
{
  HAL_UART_IRQHandler(&huart1); // HAL库的通用中断处理函数

  // 判断是否是空闲中断触发
  if(__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE) != RESET)
  {
    __HAL_UART_CLEAR_IDLEFLAG(&huart1); // 清除空闲中断标志位(必须做,否则会一直触发中断)

    // ---------------------- 计算实际接收的数据长度 ----------------------
    // 暂停DMA传输,获取当前剩余的DMA传输计数
    HAL_UART_DMAStop(&huart1);
    // 实际接收长度 = 缓冲区总长度 - DMA剩余计数
    usart1_rx_len = USART1_RX_BUF_LEN - __HAL_DMA_GET_COUNTER(&hdma_usart1_rx);

    // ---------------------- 处理接收到的数据 ----------------------
    // 示例:把收到的不定长数据原封不动回发
    HAL_UART_Transmit(&huart1, usart1_rx_buf, usart1_rx_len, 100);
    HAL_UART_Transmit(&huart1, (uint8_t*)"\r\n", 2, 100);

    // 清空接收缓冲区(可选,根据需求决定是否需要)
    memset(usart1_rx_buf, 0, usart1_rx_len);

    // ---------------------- 重新开启DMA接收,准备下一次数据 ----------------------
    HAL_UART_Receive_DMA(&huart1, usart1_rx_buf, USART1_RX_BUF_LEN);
  }
}

3.4 测试验证

  1. 编译下载程序,串口助手发送任意长度的字符串(比如hello dma!),开发板会完整回显你发送的内容
  2. 发送超过128字节的数据,会自动截断(因为缓冲区最大长度是128,可根据需求调整)
  3. 主循环的LED正常翻转,说明CPU完全没有被串口占用

四、三种模式对比与选型建议

模式 CPU占用 适用场景 优点 缺点
轮询模式 高(阻塞CPU) 调试打印、单次短数据收发 实现简单,代码量少,无中断相关问题 阻塞主循环,无法处理异步数据,效率极低
中断模式 中(仅中断时占用) 异步接收、中等数据量交互 不阻塞主循环,CPU可同时执行其他任务 频繁中断会影响实时性,大数据量下效率不高
DMA模式 极低(几乎不占用CPU) 大数据量收发、高波特率、不定长数据 完全解放CPU,收发效率极高,支持不定长数据 配置稍复杂,需要配合空闲中断判断帧结束

选型建议

  • 纯调试打印用轮询模式即可
  • 接收传感器/上位机指令,数据量不大时用中断模式
  • 大数据量、高波特率、需要高性能时,优先用DMA模式

五、常见问题与避坑指南

  1. 串口乱码怎么解决?

    • 检查波特率配置:确认CubeMX里的串口波特率和串口助手一致,同时检查系统时钟树配置是否正确(波特率依赖APB总线时钟)
    • 检查硬件连接:TX/RX是否交叉连接,GND是否共地
    • 检查校验位/停止位:确保串口参数(8N1/9600等)完全匹配
  2. 中断模式接收不连续怎么办?

    • 检查回调函数里是否重新调用了HAL_UART_Receive_IT,中断接收一次后会自动关闭,不重新开启就不会触发下一次中断
    • 检查中断优先级:是否被更高优先级的中断抢占,导致串口中断无法及时响应
  3. DMA接收丢数据/长度计算错误?

    • 接收缓冲区必须定义为全局变量或静态变量,不能是局部变量(局部变量在函数退出后栈空间释放,DMA会写非法地址)
    • 确认开启了Memory Increment(内存地址自增),否则数据会一直写入缓冲区首地址,导致数据被覆盖
    • 计算长度时,必须先调用HAL_UART_DMAStop暂停DMA传输,再调用__HAL_DMA_GET_COUNTER获取剩余计数
  4. DMA空闲中断不触发?

    • 确认开启了UART_IT_IDLE中断,且在中断服务函数里清除了UART_FLAG_IDLE标志位
    • 确认串口总线确实处于空闲状态(没有持续发送数据)

END

串口是嵌入式开发的"基本功",从轮询到中断再到DMA,是一个从简单到高效的进阶过程。这篇文章的代码都是经过验证的,你可以直接复制到项目里使用,也可以根据自己的需求修改缓冲区大小、波特率等参数。

相关推荐
hrw_embedded1 小时前
STM32单片机增加全局内存增大导致ADC数据丢失,明明两个不相干的两个部分,为什么会相互干扰?
stm32·单片机·嵌入式硬件
余生皆假期-2 小时前
YuanHub 源码分析【六】MIT 模式
笔记·单片机·嵌入式硬件
玩转单片机与嵌入式2 小时前
别再只把 MCU 当控制器:新一代芯片正在把 AI 推理搬到设备端
人工智能·单片机·嵌入式硬件
三佛科技-134163842123 小时前
迷你除湿机方案开发,基于FT61E145-TRB单片机方案
单片机·嵌入式硬件·物联网·智能家居
czhaii3 小时前
STC15W408AS单片机不锈钢切割机C语言
单片机·嵌入式硬件
CHINA红旗下3 小时前
如何使用vscode开发STM32
ide·vscode·stm32
嵌入式小杰4 小时前
一阶低通滤波入门教程:从原理到单片机 C 代码实现
c语言·开发语言·stm32·单片机·算法
嵌入式小杰4 小时前
一阶卡尔曼滤波入门教程:从原理到单片机 C 代码实现
c语言·单片机