1. 方案概述
在嵌入式开发中,串口通信是最常用的功能。在前面博客中讲述了通过轮询和中断实现方式,但是都有各自的缺点,在高速或大数据量时会被频繁打断CPU,效率低下。
本教程使用STM32F103实现以下功能:
- DMA接收:数据直接由DMA搬运到内存缓冲区,不占用CPU。
- IDLE中断:利用串口的空闲中断检测一帧数据的结束,实现不定长数据接收。
- DMA发送:发送数据同样使用DMA,发送完成后CPU才会介入(或完全不介入)。
- 回环测试:将接收到的数据通过DMA原样发回(ECHO)。
2. 硬件与引脚分配
以 STM32F103为例:使用USART3:
| 功能 | 引脚 | 模式 | DMA通道 |
|---|---|---|---|
| TX | PB10 | 复用推挽输出 | DMA1_Channel2 |
| RX | PB11 | 浮空输入 | DMA1_Channel3 |
3. 代码核心详解
我们将代码分为三个部分:配置头文件、驱动实现、主逻辑
3.1 头文件配置(usart3.h)
- volatile 关键字很重要,因为 g_u3_rx_flag 和 g_u3_rx_len 会在中断函数中被修改,并在主循环中被读取,防止编译器优化。
#ifndef __USART3_H
#define __USART3_H
#include "stm32f10x.h" // Device header
#include <string.h>
#define USART3_BUFFER_SIZE 256 // 定义最大接收、发送长度
extern uint8_t usart3_rx_buffer[USART3_BUFFER_SIZE]; // 接收缓冲
extern uint8_t usart3_tx_buffer[USART3_BUFFER_SIZE]; // 发送缓冲
extern volatile uint8_t g_u3_rx_flag; // 接收完成标志(1:完成;0:未完成)
extern volatile uint16_t g_u3_rx_len; // 接收到的数据长度
// 函数声明
void usart3_dma_init(void);
void usart3_dma_tx_data(uint8_t *buffer, uint16_t len);
void usart3_resume_rx(void);
#endif
3.2 核心驱动实现(usart3.c)
A. 初始化函数 usart3_dma_init
这个函数完成了时钟初始化、GPIO、USART和DMA的全部配置
关键点解析:
-
时钟开启:注意USART3挂载在APB1,而GPIO和DMA挂载在APB2和AHB。
-
IDLE中断:USART_ITConfig(USART3, USART_IT_IDLE, ENABLE);是检测不定长数据的核心。当总线空闲,一帧数据发完了时,触发中断。
-
DMA配置:
- RX(Channel3):DMA_Mode_Normal(普通模式),因为我们需要在中断中手动处理完数据后,再重新开启DMA,这样更可控。
- TX(Channel2):初始化时不开启,只有发送函数调用时才开启。
uint8_t usart3_rx_buffer[USART3_BUFFER_SIZE];
uint8_t usart3_tx_buffer[USART3_BUFFER_SIZE];
volatile uint8_t g_u3_rx_flag = 0;
volatile uint16_t g_u3_rx_len = 0;
/**
* @brief 初始化 usart3 + dma + nvic
*/
void usart3_dma_init(void) {
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
DMA_InitTypeDef DMA_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
// 1.开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 2.配置 GPIO (PB10 Tx,PB11 Rx)
// TX:复用推挽
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// RX:浮空输入
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_11;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(GPIOB, &GPIO_InitStructure);
// 3.配置 USART3
USART_InitStructure.USART_BaudRate = 115200;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
USART_Init(USART3, &USART_InitStructure);
// 开启IDLE(空闲)中断,用于检测不定长数据接收完成
USART_ITConfig(USART3, USART_IT_IDLE, ENABLE);
// 开启USART3 的DMA请求
USART_DMACmd(USART3, USART_DMAReq_Tx | USART_DMAReq_Rx, ENABLE);
// 4. 配置NVIC(中断优先级)
NVIC_InitStructure.NVIC_IRQChannel = USART3_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 5. 配置DMA1_Channel3(用于接收RX)
DMA_DeInit(DMA1_Channel3);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART3->DR; // 外设地址(数据寄存器)
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)usart3_rx_buffer; // 内存地址(接收缓冲区)
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设-》内存
DMA_InitStructure.DMA_BufferSize = USART3_BUFFER_SIZE;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // DR地址不变
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 单次模式(非循环)
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel3, &DMA_InitStructure);
// 立即开启接收 DMA
DMA_Cmd(DMA1_Channel3, ENABLE);
// 6. 配置 DMA1_Channel2(用于发送TX)
DMA_DeInit(DMA1_Channel2);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART3->DR;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)usart3_tx_buffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = 0;
DMA_Init(DMA1_Channel2, &DMA_InitStructure);
// 注意:TX DMA先不开启,要等到数据发送时再开
// 7. 最后使能串口
USART_Cmd(USART3, ENABLE);
}
B.DMA 发送函数 usart3_dma_tx_data
void usart3_dma_tx_data(uint8_t *buffer, uint16_t len) {
// 1. 等待上一次发送完成 (防止覆盖数据)
while(DMA_GetCurrDataCounter(DMA1_Channel2) != 0);
// 2. 关闭 DMA 才能修改配置
DMA_Cmd(DMA1_Channel2, DISABLE);
// 3. 更新数据源地址和长度
DMA1_Channel2->CMAR = (uint32_t)buffer; // 设置内存地址
DMA_SetCurrDataCounter(DMA1_Channel2, len); // 设置发送长度
// 4. 启动发送
DMA_Cmd(DMA1_Channel2, ENABLE);
}
C.重启接收函数 usart3_resume_rx
主函数处理完数据后,需要调用此函数让DMA准备好接收下一包数据
void usart3_resume_rx(void) {
DMA_Cmd(DMA1_Channel3, DISABLE); // 必须先关
DMA_SetCurrDataCounter(DMA1_Channel3, USART3_BUFFER_SIZE); // 重置接收长度计数器
// 清除所有中断标志,防止误触发
DMA_ClearFlag(DMA1_FLAG_TC3 | DMA1_FLAG_HT3 | DMA1_FLAG_TE3 | DMA1_FLAG_GL3);
DMA_Cmd(DMA1_Channel3, ENABLE); // 重新开
}
D.中断服务函数
void USART3_IRQHandler(void) {
uint32_t temp;
// 判断是否是空闲中断 (IDLE)
if(USART_GetITStatus(USART3, USART_IT_IDLE) != RESET) {
// 1. 清除 IDLE 标志位
// 查阅手册:清除 IDLE 需要先读 SR 寄存器,再读 DR 寄存器
temp = USART3->SR;
temp = USART3->DR;
(void)temp; // 防止编译器警告未使用变量
// 2. 关闭 DMA,防止数据处理期间有新数据进来导致混乱
DMA_Cmd(DMA1_Channel3, DISABLE);
// 3. 计算接收到的数据长度
// DMA_GetCurrDataCounter 返回的是“剩余”需要传输的数量
// 实际接收 = 总大小 - 剩余大小
g_u3_rx_len = USART3_BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel3);
// 4. 设置标志位,通知 main()
if (g_u3_rx_len > 0) {
g_u3_rx_flag = 1;
} else {
// 如果是异常触发,直接恢复接收
usart3_resume_rx();
}
}
}
3.3 主逻辑(main.c)、
int main(void)
{
// ... 初始化代码 ...
usart3_dma_init();
// 发送开机欢迎语
char *msg = "System Init OK. Echo Mode Start...\r\n";
strcpy((char*)usart3_tx_buffer, msg);
usart3_dma_tx_data(usart3_tx_buffer, strlen(msg));
while(1) {
// LED 心跳,证明系统未死机
bsp_led_toggle();
delay_ms(500);
// 轮询标志位
if (g_u3_rx_flag == 1) {
// 1. 处理数据 (这里是回显操作)
// 将接收 buffer 拷贝到发送 buffer
memcpy(usart3_tx_buffer, usart3_rx_buffer, g_u3_rx_len);
uint16_t current_len = g_u3_rx_len;
// 2. 清除标志位
g_u3_rx_flag = 0;
// 3. 关键:先恢复接收 DMA,再处理发送
// 这样能最大程度减少“盲区时间”,防止漏接下一包数据
usart3_resume_rx();
// 4. 发送数据
usart3_dma_tx_data(usart3_tx_buffer, current_len);
}
}
}
4. 注意事项:
- 清除 IDLE 标志位: 代码中 temp = USART3->SR; temp = USART3->DR; 是标准库清除 IDLE 标志的序列。如果只调用 USART_ClearITPendingBit 可能无法清除 IDLE 标志,导致一直进入中断。
- DMA 接收长度计算: DMA 寄存器 CNDTR 实际上是倒计数的。初始化时设为 256,接收到 10 个字节后,它会变成 246。所以 RxLen = 256 - 246 = 10。
- 中断优先级: 如果在使用了 FreeRTOS 等系统,或者有其他高频中断(如 ADC、定时器),务必合理设置 NVIC 的优先级。串口通信通常优先级设置为中等即可。
- Cache 一致性 (STM32F1) : 在 STM32F1 系列(Cortex-M3)上没有 D-Cache,所以不用担心 DMA 和 CPU 访问内存不一致的问题。如果是 F7 或 H7 系列,则需要考虑 Cache Clean/Invalidate 操作。