STM32中DMA的使用——串口数据的DMA传输
DMA 的使用 —— 以STM32 串口DMA传输为例
本笔记总结修改自正点原子的F103开发指南。原文中的教程没有使用到CubeMX,并且说的很乱,因此我就总结简化了一下,然后把封装的函数拆解分析了一下。
一、DMA 简介
DMA(Direct Memory Access),直接存储器访问。它的作用是在不需要 CPU 参与的情况下,由硬件在 RAM 和 I/O 设备之间直接传输数据。和中断方式不同,DMA 不需要保存和恢复现场,CPU 的效率更高。
STM32F103 的 DMA 控制器
- 内部有两个 DMA 控制器:DMA1 和 DMA2(DMA2 仅在大容量产品中存在)
- DMA1 有 7 个通道,DMA2 有 5 个通道
- 每个通道管理一个或多个外设对存储器的访问请求
- 有一个仲裁器,负责协调各通道的优先级
DMA 的特性
- 每个通道都有专用的硬件 DMA 请求,也支持软件触发
- 优先级可软件编程设置(四级:很高、高、中等、低),同等优先级由硬件决定(通道号越小优先级越高)
- 源和目标数据宽度独立设置(字节、半字、全字),支持打包和拆包
- 支持循环缓冲器管理
- 每个通道有 3 个事件标志:半传输完成、传输完成、传输出错,逻辑或成一个中断请求
- 支持存储器到存储器、外设到存储器、存储器到外设的传输
- 闪存、SRAM、APB1/APB2、AHB 外设均可作为源或目标
- 可编程传输数据量,最大 65536
二、DMA 框图分析

DMA 控制器框图主要有三部分:
1. DMA 请求
外设要使用 DMA 传输数据时,必须先向 DMA 控制器发送请求。DMA 收到请求后会给外设一个应答信号,外设应答后启动传输,直到完成。
DMA1 和 DMA2 的各通道对应不同的外设请求,编程时需要查表确认通道分配。例如串口 1 的发送对应 DMA1 的通道 4。
2. 通道
DMA 有 12 个独立可编程的通道(DMA1 的 7 个 + DMA2 的 5 个)。每个通道对应不同外设的 DMA 请求。一个通道可以接收多个外设的请求,但同一时间只能处理一个。
3. 仲裁器
当多个通道同时有请求时,仲裁器决定响应顺序。分两个阶段:
- 软件阶段:在
DMA_CCRx寄存器中设置优先级(非常高/高/中/低) - 硬件阶段:优先级相同时,通道编号越低优先级越高
DMA1 的优先级高于 DMA2。
每个通道对应着不同的外设请求,这个是固定的不可改变的,如果有冲突就通过仲裁器来判断哪个优先级更高。


三、DMA 寄存器
DMA_ISR(中断状态寄存器)

只读寄存器,查询当前 DMA 传输状态。常用的位是 TCIFx(通道 x 传输完成标志)。
DMA_IFCR(中断标志清除寄存器)

写 1 清除对应的标志位。
DMA_CNDTRx(传输数量寄存器)

控制通道 x 每次传输的数据量,范围 0~65535。传输过程中该寄存器的值会递减,到 0 表示传输完成。可通过读取这个寄存器获取当前传输进度。
DMA_CCRx(配置寄存器)

核心控制寄存器,配置内容包括:
- 数据宽度
- 外设及存储器宽度
- 通道优先级
- 增量模式
- 传输方向
- 中断使能
- DMA 使能
DMA_CPARx(外设地址寄存器)

存放外设地址。例如串口 1 要填入 0x40013804(&USART1_DR)。
DMA_CMARx(存储器地址寄存器)
存放存储器地址。例如用 g_sendbuf[5200] 做缓冲区,就填入 &g_sendbuf。
四、CubeMX 配置与程序设计
实验:按下 KEY0 按键后,CPU 把内存里准备好的一串数据,通过 DMA 自动送到串口 1 的发送寄存器,CPU 在传输过程中可以去做别的事。串口调试助手能看到接收到的数据。
实现这个流程,需要配置以下几个环节(从 HAL 库和寄存器的角度看):
-
DMA 时钟使能 —— DMA 挂在 AHB 总线上,工作前必须先打开 DMA 控制器的时钟。对应操作
RCC_AHBENR寄存器的 DMA1EN 位。如果这一步没做,DMA 寄存器写不进数据。 -
选 DMA 通道 —— 串口 1 的发送固定对应 DMA1 的通道 4(查 DMA1 请求映像表可知)。这一步要告诉 DMA 控制器用哪个通道来处理请求,实际上是选定了
DMA1_Channel4这一组寄存器(DMA_CCR4、DMA_CNDTR4、DMA_CPAR4、DMA_CMAR4)。 -
配置传输参数(DMA_CCRx) —— 核心配置寄存器,决定 DMA 怎么传。需要设置的内容包括:
- 传输方向:存储器到外设(数据从
g_sendbuf到串口的数据寄存器) - 增量模式:存储器地址递增(每发一个字节,源地址往后移一个位置),外设地址固定(串口数据寄存器地址就一个)
- 数据宽度:8 位,因为串口一次发一个字节
- 传输模式:普通模式(Normal),传完指定数量就停。如果用循环(Circular)模式,传完会自动从头再传,适合连续的数据流
- 优先级:多个 DMA 通道同时请求时的仲裁依据,这里只有一路,设 Medium 即可
- 传输方向:存储器到外设(数据从
-
填入源地址和目标地址 ——
DMA_CPAR4写入&USART1_DR(0x40013804),告诉 DMA 把数据送到串口的哪个寄存器;DMA_CMAR4写入&g_sendbuf,告诉 DMA 从哪里取数据。 -
设置传输数据量 ——
DMA_CNDTR4写入要发送的总字节数。传输过程中这个值递减,减到 0 表示传完了。可以实时读它来知道还剩多少没发。 -
关联外设和 DMA —— 前面几步把 DMA 配置好了,但 DMA 还不知道它服务的是哪个外设。在 HAL 库中通过
__HAL_LINKDMA()把串口句柄的hdmatx成员指向 DMA 句柄,这样串口的 HAL 函数(如HAL_UART_Transmit_DMA)才能正确地操作这个 DMA 通道。这只是软件层面的指针关联,没有寄存器操作。 -
开启串口的 DMA 发送请求 —— 串口端也有个开关:
USART_CR3寄存器的 DMAT 位。置 1 后,串口每发送完一个字节,会自动向 DMA 发出请求,DMA 收到请求就送下一个字节过来。这一步封装在HAL_UART_Transmit_DMA()里,调用时一并完成。 -
DMA 中断(可选) —— 使能 NVIC 中
DMA1_Channel4_IRQn的中断。DMA 传完后会产生中断,HAL 库的HAL_DMA_IRQHandler会收这个中断,然后调用对应的回调函数(比如HAL_UART_TxCpltCallback)。如果不想用中断,也可以轮询DMA_ISR的 TCIF4 位来判断传输是否完成。
以上 8 点就是完成一次串口 DMA 发送所需的全部配置。下面看 CubeMX 如何把这些配置自动生成出来。
4.1 CubeMX 中的配置操作
在 CubeMX 的 Pinout & Configuration 页面需要配置以下几项:
USART1
-
Mode:选择 Asynchronous(异步模式)
- 串口通信就是异步的,不需要时钟线,所以选这个。如果选 Single Wire(半双工)就只能发或只能收,这里收发都要用。
-
Parameter Settings:Baud Rate 设为 115200,其他默认
- 波特率是每秒传多少位,115200 是常用值,和串口调试助手那边保持一致就行。
-
NVIC Settings:勾选 Enabled(使能 USART1 全局中断)
- 如果不勾,串口收发的中断不会触发。虽然这个实验没用中断方式做 DMA,但 CubeMX 生成代码时如果发现 NVIC 没开,HAL 库函数会有断言报错,所以先勾上。
-
DMA Settings:点 Add,选择 USART1_TX
- Direction:Memory To Peripheral
- 数据从内存(
g_sendbuf)送到外设(串口 1 的发送数据寄存器),所以方向是存储器到外设。
- 数据从内存(
- Mode:Normal
- Normal 模式:传完指定数量的数据就停。还有一个 Circular(循环)模式,传完会自动从头再传,适合周期性的数据流(比如音频输出),这里只需要按一下传一次,所以用 Normal。
- Periph Data Width:Byte / Mem Data Width:Byte
- 数据宽度。发送缓冲区是
uint8_t数组,串口一次传一个字节,所以两边都设 Byte。如果设 Half Word(16位)或 Word(32位),DMA 会一次拿 2 或 4 个字节往外送,串口用不了。
- 数据宽度。发送缓冲区是
- Priority:Medium
- DMA 通道的优先级。如果还有其他 DMA 通道在跑,仲裁器会根据这个优先级决定谁先用总线。这里没有别的 DMA 通道,所以 Medium 就够了。
添加后 CubeMX 会自动分配到 DMA1 的通道 4。因为这个通道是串口 1 发送的固定映射(见前面框图里的 DMA1 请求映像表),不能自己随便改。



- Direction:Memory To Peripheral
PB5:Pinout 视图里点击 PB5,选 GPIO_Output(LED0)
- PB5 接了一个 LED(正点原子板子上是 LED0),低电平点亮。设成推挽输出、不用上下拉。
PE4:Pinout 视图里点击 PE4,选 GPIO_Input(KEY0)
- PE4 接了按键 KEY0,按下为低电平。设成上拉输入(平时高电平,按下拉低)。
时钟配置:RCC 里 HSE 选 Crystal/Ceramic Resonator,Clock Configuration 页里设好 PLL,让 SYSCLK 跑到 72MHz
- HSE 是外部高速晶振,板子上接了 8MHz 晶振,所以选这个。用它经过 PLL 倍频到 72MHz 作为系统时钟。AHB 分频默认 /1,这样 HCLK = 72MHz,DMA 也跑在 72MHz。不把 AHB 分频设得太低,否则 DMA 速度会受影响。

4.2 CubeMX 生成的代码
生成工程后,CubeMX 在 main.c 和相关文件里生成了以下初始化函数:
SystemClock_Config()
根据 Clock Configuration 页的设定生成时钟初始化代码,包括 HSE 起振、PLL 配置、SYSCLK 选择、AHB/APB1/APB2 分频系数。实质操作的是 RCC 寄存器组的时钟配置相关寄存器(RCC_CR、RCC_CFGR、RCC_PLLCFGR 等)。
MX_DMA_Init()
void MX_DMA_Init(void)
{
__HAL_RCC_DMA1_CLK_ENABLE(); // 操作 RCC_AHBENR 寄存器
HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn); // 操作 NVIC 相关寄存器
}
这个函数只做了两件事:开启 DMA1 时钟、使能 DMA 中断。DMA 的具体参数配置不在这里。
MX_USART1_UART_Init()
static 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;
HAL_UART_Init(&huart1);
}
调用 HAL_UART_Init 时,HAL 库内部会调用 HAL_UART_MspInit,这个函数由 CubeMX 自动生成,里面包含了 DMA 的完整初始化:
void HAL_UART_MspInit(UART_HandleTypeDef* uartHandle)
{
if (uartHandle->Instance == USART1)
{
__HAL_RCC_USART1_CLK_ENABLE(); // 使能 USART1 时钟
__HAL_RCC_GPIOA_CLK_ENABLE(); // 使能 GPIOA 时钟
// PA9(TX), PA10(RX) 的 GPIO 初始化...
// ====== DMA 配置(对应原来手写的 dma_init 函数)======
hdma_usart1_tx.Instance = DMA1_Channel4; // IRQ 和 DMA 请求映像见框图
hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_tx.Init.Mode = DMA_NORMAL;
hdma_usart1_tx.Init.Priority = DMA_PRIORITY_MEDIUM;
HAL_DMA_Init(&hdma_usart1_tx); // 写入 DMA_CCRx 等寄存器
__HAL_LINKDMA(uartHandle, hdmatx, hdma_usart1_tx); // 关联外设和 DMA
HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
}
}
手动方式里自己写的 dma_init() 做的事,全部被 CubeMX 生成到了 HAL_UART_MspInit 里了。
MX_GPIO_Init()
初始化 PB5(LED0)、PE4(KEY0),包括时钟使能、模式选择、上下拉、输出速度等。操作的是 GPIOx_CRL/CRH、GPIOx_ODR 等寄存器。
4.3 步骤封装说明
原来的寄存器操作步骤被封装到了 CubeMX 生成的各个函数里,对应关系如下:
| 原来手写的操作 | 对应寄存器 | 被封装在哪个函数 |
|---|---|---|
| 使能 DMA1 时钟 | RCC_AHBENR 的 DMA1EN 位 |
MX_DMA_Init() 中的 __HAL_RCC_DMA1_CLK_ENABLE() |
| 设 DMA 通道 | DMA_CPARx、DMA_CMARx |
HAL_UART_MspInit 中的 hdma_usart1_tx.Instance = DMA1_Channel4 |
| 设传输方向、数据宽度、增量模式、优先级等 | DMA_CCRx |
HAL_UART_MspInit 中的结构体赋值 + HAL_DMA_Init() |
| 关联外设句柄和 DMA 句柄 | 无 | HAL_UART_MspInit 中的 __HAL_LINKDMA() |
| 使能串口 DMA 发送 | USART_CR3 的 DMAT 位 |
用户代码调 HAL_UART_Transmit_DMA() |
| 查询传输完成标志 | DMA_ISR 的 TCIFx 位 |
用户代码调 __HAL_DMA_GET_FLAG() |
| 获取剩余数据量 | DMA_CNDTRx |
用户代码调 __HAL_DMA_GET_COUNTER() |
| 清除传输完成标志 | DMA_IFCR 的 CTCIFx 位 |
用户代码调 __HAL_DMA_CLEAR_FLAG() |
| DMA 中断处理 | NVIC + DMA 中断标志 | DMA1_Channel4_IRQHandler -> HAL_DMA_IRQHandler -> 用户回调 |
从表格可以看出来,CubeMX 自动生成的部分是时钟、通道选择、参数配置、关联和中断使能,而启动传输、查进度、清标志这几步仍由用户代码完成。
4.4 需要自己写的代码
(1)发送缓冲区
/* USER CODE BEGIN PV */
const uint8_t TEXT_TO_SEND[] = {"正点原子 STM32 DMA 串口实验"};
#define SEND_BUF_SIZE (sizeof(TEXT_TO_SEND) + 2) * 200
uint8_t g_sendbuf[SEND_BUF_SIZE];
uint8_t transmit_done = 0; // 中断方式用的标志
/* USER CODE END PV */
(2)缓冲区填充 写在 初始化函数全部调用之后
/* USER CODE BEGIN 2 */
uint16_t len = sizeof(TEXT_TO_SEND);
uint16_t k = 0;
uint8_t mask = 0;
for (uint16_t i = 0; i < SEND_BUF_SIZE; i++)
{
if (k >= len)
{
if (mask) { g_sendbuf[i] = 0x0a; k = 0; }
else { g_sendbuf[i] = 0x0d; mask++; }
}
else
{
mask = 0;
g_sendbuf[i] = TEXT_TO_SEND[k];
k++;
}
}
/* USER CODE END 2 */
(3)主循环
/* USER CODE BEGIN WHILE */
uint8_t key;
while (1)
{
key = key_scan(0);
if (key == KEY0_PRES)
{
HAL_UART_Transmit_DMA(&huart1, g_sendbuf, SEND_BUF_SIZE);
while (1)
{
if (__HAL_DMA_GET_FLAG(&hdma_usart1_tx, DMA_FLAG_TC4))
{
__HAL_DMA_CLEAR_FLAG(&hdma_usart1_tx, DMA_FLAG_TC4);
HAL_UART_DMAStop(&huart1);
break;
}
/* 等待传输完成,CPU 可以在这里做其他事 */
}
}
HAL_Delay(10);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
(4)中断回调(可选)
上面是轮询方式,CubeMX 已经开了 DMA 中断,也可以改成中断方式。把轮询的 while 去掉,在 /* USER CODE BEGIN 4 */ 里写回调:
/* USER CODE BEGIN 4 */
void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if (huart->Instance == USART1)
{
HAL_UART_DMAStop(&huart1);
transmit_done = 1;
}
}
/* USER CODE END 4 */
主循环里调完 HAL_UART_Transmit_DMA 就不用管了,传完会自动进回调。

浙公网安备 33010602011771号