一、串口通信原理
串口通信(Serial Communication)是一种逐位传输数据的通信方式,常用于 MCU 与外设或 PC 通信。
1. 工作模式
STM32 支持全双工通信(TX、RX):
-
TX(Transmit):发送引脚
-
RX(Receive):接收引脚
2. 参数配置
关键参数包括:
-
波特率(Baudrate):如 115200bps
-
数据位:一般为 8 位
-
停止位:1 位或 2 位
-
校验位:无校验、偶校验、奇校验
-
流控:无、硬件流控(RTS/CTS)
二、串口通信实现(使用 STM32 HAL 库)
1. 初始化步骤
a. 使能串口及 GPIO 时钟
__HAL_RCC_USART1_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
b. 配置 GPIO 引脚为复用功能
GPIO_InitTypeDef GPIO_InitStruct = {0};
// PA9 -> USART1_TX, PA10 -> USART1_RX
GPIO_InitStruct.Pin = GPIO_PIN_9 | GPIO_PIN_10;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
c. 初始化 USART 外设
UART_HandleTypeDef huart1;
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();
}
}
2. 数据收发示例
a. 发送数据
uint8_t msg[] = "Hello UART\r\n";
HAL_UART_Transmit(&huart1, msg, sizeof(msg) - 1, HAL_MAX_DELAY);
b. 接收数据(阻塞)
uint8_t rx_data;
HAL_UART_Receive(&huart1, &rx_data, 1, HAL_MAX_DELAY);
c. 使用中断接收(推荐)
uint8_t rx_buffer[10];
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
// 处理接收到的数据
HAL_UART_Transmit(&huart1, rx_buffer, 1, 10);
HAL_UART_Receive_IT(&huart1, rx_buffer, 1); // 继续接收
}
}
开启接收中断:
HAL_UART_Receive_IT(&huart1, rx_buffer, 1);
在 stm32f1xx_it.c
中加入:
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart1);
}
三、开发中需要注意的问题
-
波特率不匹配
MCU 与上位机/外设波特率不一致会导致乱码或收不到数据。 -
缓冲区溢出
中断模式下若处理不及时,可能导致接收缓冲区溢出。 -
时钟配置错误
HCLK/APB 时钟错误会导致串口实际波特率不准,建议在 CubeMX 中统一配置。 -
FIFO 与缓存清理问题
使用 DMA 模式时需正确处理空闲中断和接收长度。 -
中断优先级配置不合理
如果串口中断优先级太低,可能导致数据丢失或系统响应慢。
四、调试与问题排查
1. 常见问题排查
问题现象 | 可能原因 | 解决方案 |
---|---|---|
无数据发送/接收 | 1. 波特率不匹配 2. 线序错误 3. 未初始化 | 1. 检查波特率设置 2. 交换TX/RX 3. 检查初始化代码 |
接收数据不完整/乱码 | 1. 时钟配置错误 2. 缓冲区溢出 3. 电气干扰 | 1. 检查时钟树配置 2. 增加缓冲区大小 3. 添加滤波电容 |
通信一段时间后中断 | 1. 缓冲区未及时处理 2. 错误标志未清除 | 1. 优化数据处理速度 2. 清除错误标志 __HAL_UART_CLEAR_FLAG() |
DMA传输卡死 | 1. DMA通道冲突 2. 内存对齐问题 | 1. 检查DMA通道映射 2. 确保4字节对齐 |
低功耗模式下无法唤醒 | 1. 未配置唤醒功能 2. 时钟源错误 | 1. 启用串口唤醒功能 2. 使用HSI时钟 |
2. 调试技巧
-
使用
USARTx->DR
和USARTx->SR
直接调试底层寄存器可确认是否有数据发送/接收 -
使用示波器/逻辑分析仪观察串口波形是否正常
-
使用串口工具(如 SSCOM、RealTerm)查看接收数据是否符合预期
-
打印调试信息到串口,确认程序运行流程
-
CubeMX 生成代码时可自动配置 NVIC 和 GPIO,避免人工遗漏
一、STM32 串口 DMA + 空闲中断
1、DMA + 空闲中断接收的原理
在高频率或大数据量串口通信场景中,频繁进入中断处理不高效。此时可使用 DMA 接收 + 空闲中断识别帧尾 的方案。
原理流程:
-
启动 UART DMA 接收(HAL_UART_Receive_DMA)
-
每次数据帧传输完成,串口会进入 空闲中断
-
在中断中读取 DMA 接收的实际长度
-
将接收到的数据拷贝或处理(如加入环形缓冲区)
-
重新启动 DMA 接收
2、代码实现:DMA + 空闲中断接收帧
1、环境准备(CubeMX)
-
使能 USART1,勾选
USART1 global interrupt
和DMA Rx
-
选择 RX DMA 为
Normal Mode
或Circular
-
NVIC 里开启
USART1 global interrupt
2. 定义变量
#define UART_DMA_RX_BUF_SIZE 256
uint8_t uart1_dma_rx_buf[UART_DMA_RX_BUF_SIZE]; // DMA 接收缓冲区
uint8_t uart1_frame_buf[UART_DMA_RX_BUF_SIZE]; // 接收帧处理缓冲区
uint16_t uart1_frame_len = 0;
3、初始化 DMA 接收 + 空闲中断
HAL_UART_Receive_DMA(&huart1, uart1_dma_rx_buf, UART_DMA_RX_BUF_SIZE);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 开启 IDLE 中断
4、中断服务函数
void USART1_IRQHandler(void)
{
HAL_UART_IRQHandler(&huart1);
// 空闲中断处理
if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_IDLE))
{
__HAL_UART_CLEAR_IDLEFLAG(&huart1);
// 计算接收字节数
uint16_t len = UART_DMA_RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx);
// 拷贝接收到的数据
memcpy(uart1_frame_buf, uart1_dma_rx_buf, len);
uart1_frame_len = len;
// 处理数据(例如:Modbus)
Modbus_Parse(uart1_frame_buf, uart1_frame_len);
// 重启 DMA
HAL_UART_Receive_DMA(&huart1, uart1_dma_rx_buf, UART_DMA_RX_BUF_SIZE);
}
}
二、使用 DMA 时的注意事项
问题 | 原因 | 解决方案 |
---|---|---|
空闲中断不触发 | 没有启用 UART_IT_IDLE 中断 | __HAL_UART_ENABLE_IT(..., UART_IT_IDLE) |
DMA 重启失败或溢出 | 使用循环 DMA 且未读取数据 | 用 Normal 模式手动重启,及时处理数据 |
帧截断/拼接 | 没有判断帧头帧尾、没有超时机制 | 配合定时器、空闲中断、帧头帧尾标志同步判断 |
HAL_UART_Receive_DMA 崩溃 | DMA 未停止/数据未处理完成 | 重启 DMA 前应禁用或先清空状态 |
调试工具与技术
-
逻辑分析仪:验证信号波形、波特率、帧结构
-
串口调试助手:验证数据内容(推荐:Putty、Tera Term)
-
STM32CubeMonitor:实时监控变量和寄存器
-
ErrorCode分析:检查
huart->ErrorCode
的值 -
断点调试:在回调函数设置断点
三、总结
-
DMA + 空闲中断是高效、稳定的串口接收方案
-
配合帧协议(如 Modbus RTU)可以处理复杂应用场景
-
串口通信调试建议采用逐步验证法(先收,再校验 CRC,再解析)
STM32 串口使用 HAL 库 + RingBuffer 接收数据说明
一、什么是 RingBuffer(环形缓冲区)
RingBuffer(循环缓冲区)是一种先进先出(FIFO)的数据结构。适合持续接收数据的场景,如串口中断模式下收发数据不定长、速率不固定的通信。
特点:
-
固定大小内存,节省资源
-
高效地避免数据覆盖(通过头尾指针控制)
-
易于配合中断模式收数据
二、适用场景
-
使用
HAL_UART_Receive_IT()
接收单字节 -
在
HAL_UART_RxCpltCallback()
中将接收的字节写入 RingBuffer -
主循环中从 RingBuffer 中读取数据解析帧
三、RingBuffer 实现
1. 定义结构体与缓冲区
#define RINGBUF_SIZE 256
typedef struct {
uint8_t buffer[RINGBUF_SIZE];
volatile uint16_t head;
volatile uint16_t tail;
} RingBuffer_t;
RingBuffer_t uart1_rx_ringbuf;
uint8_t uart1_rx_temp; // 临时变量接收单字节
2. RingBuffer 操作函数
void RingBuffer_Init(RingBuffer_t *rb)
{
rb->head = rb->tail = 0;
}
int RingBuffer_IsEmpty(RingBuffer_t *rb)
{
return rb->head == rb->tail;
}
int RingBuffer_IsFull(RingBuffer_t *rb)
{
return ((rb->head + 1) % RINGBUF_SIZE) == rb->tail;
}
void RingBuffer_Put(RingBuffer_t *rb, uint8_t data)
{
if (!RingBuffer_IsFull(rb)) {
rb->buffer[rb->head] = data;
rb->head = (rb->head + 1) % RINGBUF_SIZE;
}
}
int RingBuffer_Get(RingBuffer_t *rb, uint8_t *data)
{
if (!RingBuffer_IsEmpty(rb)) {
*data = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) % RINGBUF_SIZE;
return 1;
}
return 0;
}
四、串口中断接收与 RingBuffer 结合
1. 初始化
在主函数中:
RingBuffer_Init(&uart1_rx_ringbuf);
HAL_UART_Receive_IT(&huart1, &uart1_rx_temp, 1); // 启动单字节接收中断
2. 接收回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1) {
RingBuffer_Put(&uart1_rx_ringbuf, uart1_rx_temp);
HAL_UART_Receive_IT(&huart1, &uart1_rx_temp, 1); // 继续接收
}
}
五、主循环读取与帧解析
你可以在主循环中这样读取数据:
void Process_UART1_Data(void)
{
static uint8_t frame[64];
static uint8_t index = 0;
uint8_t ch;
while (RingBuffer_Get(&uart1_rx_ringbuf, &ch)) {
if (ch == '\n') {
frame[index] = 0;
printf("接收到帧: %s\r\n", frame);
index = 0; // 重置接收
} else if (index < sizeof(frame) - 1) {
frame[index++] = ch;
} else {
index = 0; // 超出最大帧长度则丢弃
}
}
}
六、RingBuffer 的注意事项
问题 | 解决方法 |
---|---|
数据被覆盖 | RingBuffer 满时拒绝写入或覆盖最旧数据 |
多线程/中断冲突 | 在中断中写入,在主循环中读取,避免竞态 |
串口速度太快 | 增加 RingBuffer 大小,或改为 DMA 模式 |
丢帧或粘包 | 添加帧头/帧尾标志或固定长度协议 |
七、RingBuffer vs DMA 模式对比
特性 | RingBuffer + 中断 | DMA + 空闲中断 |
---|---|---|
实现复杂度 | 简单 | 较复杂 |
性能 | 一般 | 高效,适合大数据量 |
适合场景 | 命令帧/少量通信 | 视频流、大帧、Modbus RTU |
易维护性 | 高 | 中 |