前言
串口(UART/USART)是嵌入式开发中最常用的通信接口,不管是调试打印、传感器数据读取,还是上位机指令交互,都离不开它。
很多新手刚接触串口时,会被"轮询/中断/DMA"这三种模式搞懵:到底什么时候用轮询?中断和DMA又有什么区别?这篇文章会从基础到进阶,带你吃透STM32串口的三种工作模式,每个模式都会讲清原理、CubeMX配置、带注释的示例代码,以及测试验证方法,看完就能直接用到你的项目里!
本文基于STM32 HAL库编写,适用于STM32F1/F4/H7全系列芯片,可直接复制代码使用。
一、串口轮询模式(初识串口)
1.1 工作原理
轮询模式是串口最基础的工作方式,核心逻辑很简单:
- CPU不断查询串口的状态寄存器,判断"数据是否发送完成"或"是否收到数据"
- 只有当状态满足条件时,才会执行后续操作,否则一直阻塞在查询过程中
- 简单来说:CPU全程被串口"占用",没法同时做其他事
适用场景:简单的调试打印、单次短数据收发,不涉及实时性要求高的场景。
1.2 CubeMX配置步骤
以USART1为例,配置步骤如下:
- 打开CubeMX,选择你的MCU(如STM32H723VGTX)
- 配置RCC时钟,确保系统时钟和APB总线时钟配置正确(串口波特率依赖总线时钟)
- 进入
Connectivity→ 选择USART1→ 模式选择Asynchronous(异步串口) - 配置串口参数:波特率
115200,数据位8,无校验位,停止位1(通用的8N1格式) - 生成代码,选择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 测试验证
- 编译下载程序到开发板
- 用USB转TTL模块连接开发板USART1的TX/RX引脚(注意交叉连接:开发板TX接模块RX,开发板RX接模块TX,GND必须共地)
- 打开串口助手,波特率115200,8N1,发送任意字符串,就能看到开发板的回显数据
二、串口中断模式(异步收发进阶)
2.1 工作原理
轮询模式最大的缺点是"阻塞CPU",而中断模式完美解决了这个问题:
- 串口收到数据或发送完成时,会触发对应的中断信号
- CPU平时可以正常执行主循环的其他任务(如LED翻转、电机控制),只有收到中断信号时,才会暂停当前任务,进入中断服务函数处理串口数据
- 处理完数据后,CPU会回到之前被打断的地方继续执行
适用场景:异步接收传感器数据、上位机指令,需要CPU同时处理其他任务的场景。
2.2 CubeMX配置步骤
在轮询模式的基础上,额外开启串口中断:
- 进入USART1配置界面 →
NVIC Settings标签页 - 勾选
USART1 global interrupt,设置中断优先级(一般抢占优先级2,子优先级2即可,避免被其他高优先级中断抢占) - 生成代码
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 测试验证
- 编译下载程序,串口助手发送任意字符,开发板会实时回显你发送的内容
- 同时可以看到主循环里的LED在正常翻转,说明CPU没有被串口阻塞,能同时处理其他任务
三、串口DMA模式(高性能不定长收发)
3.1 工作原理
中断模式虽然解放了CPU,但每次收到数据都会触发一次中断,频繁中断还是会占用CPU资源。而DMA模式是串口的"终极形态":
- DMA(直接存储器访问)是一个独立的外设,可以直接在串口外设和内存之间搬运数据,完全不需要CPU参与
- 发送/接收数据时,CPU可以全程干别的事,只有在DMA传输完成或出现错误时,才会触发一次中断
- 配合串口空闲中断(IDLE),还可以实现不定长数据接收,完美适配上位机指令、串口屏交互等场景
适用场景:大数据量收发、高波特率通信、不定长数据接收,对CPU占用率要求高的项目。
3.2 CubeMX配置步骤
在中断模式的基础上,配置串口DMA:
- 进入USART1配置界面 →
DMA Settings标签页 - 点击
Add,添加两个DMA通道:USART1_RX:模式选择Normal,勾选Memory Increment(内存地址自增)USART1_TX:模式选择Normal,勾选Memory Increment
- 进入
NVIC Settings,确认USART1和DMA的中断都已开启 - 生成代码
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 测试验证
- 编译下载程序,串口助手发送任意长度的字符串(比如
hello dma!),开发板会完整回显你发送的内容 - 发送超过128字节的数据,会自动截断(因为缓冲区最大长度是128,可根据需求调整)
- 主循环的LED正常翻转,说明CPU完全没有被串口占用
四、三种模式对比与选型建议
| 模式 | CPU占用 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 轮询模式 | 高(阻塞CPU) | 调试打印、单次短数据收发 | 实现简单,代码量少,无中断相关问题 | 阻塞主循环,无法处理异步数据,效率极低 |
| 中断模式 | 中(仅中断时占用) | 异步接收、中等数据量交互 | 不阻塞主循环,CPU可同时执行其他任务 | 频繁中断会影响实时性,大数据量下效率不高 |
| DMA模式 | 极低(几乎不占用CPU) | 大数据量收发、高波特率、不定长数据 | 完全解放CPU,收发效率极高,支持不定长数据 | 配置稍复杂,需要配合空闲中断判断帧结束 |
选型建议:
- 纯调试打印用轮询模式即可
- 接收传感器/上位机指令,数据量不大时用中断模式
- 大数据量、高波特率、需要高性能时,优先用DMA模式
五、常见问题与避坑指南
-
串口乱码怎么解决?
- 检查波特率配置:确认CubeMX里的串口波特率和串口助手一致,同时检查系统时钟树配置是否正确(波特率依赖APB总线时钟)
- 检查硬件连接:TX/RX是否交叉连接,GND是否共地
- 检查校验位/停止位:确保串口参数(8N1/9600等)完全匹配
-
中断模式接收不连续怎么办?
- 检查回调函数里是否重新调用了
HAL_UART_Receive_IT,中断接收一次后会自动关闭,不重新开启就不会触发下一次中断 - 检查中断优先级:是否被更高优先级的中断抢占,导致串口中断无法及时响应
- 检查回调函数里是否重新调用了
-
DMA接收丢数据/长度计算错误?
- 接收缓冲区必须定义为全局变量或静态变量,不能是局部变量(局部变量在函数退出后栈空间释放,DMA会写非法地址)
- 确认开启了
Memory Increment(内存地址自增),否则数据会一直写入缓冲区首地址,导致数据被覆盖 - 计算长度时,必须先调用
HAL_UART_DMAStop暂停DMA传输,再调用__HAL_DMA_GET_COUNTER获取剩余计数
-
DMA空闲中断不触发?
- 确认开启了
UART_IT_IDLE中断,且在中断服务函数里清除了UART_FLAG_IDLE标志位 - 确认串口总线确实处于空闲状态(没有持续发送数据)
- 确认开启了
END
串口是嵌入式开发的"基本功",从轮询到中断再到DMA,是一个从简单到高效的进阶过程。这篇文章的代码都是经过验证的,你可以直接复制到项目里使用,也可以根据自己的需求修改缓冲区大小、波特率等参数。