目录
[(3)DMA 请求与应答握手流程](#(3)DMA 请求与应答握手流程)
一、DMA简介
DMA(direct memory access)直接存储器访问,提供了一种外设和存储器之间直接高速的数据传输通道。通过DMA通道传输数据,可以无需CPU干预,从而节省CPU资源,用于更重要的工作中,提升CPU性能。
在普通的情况下,当外设需要访问内存时,需要先向CPU发送申请,由CPU代为向内存中读写数据。在整个过程中,CPU的速度很快,外设的速度则较为慢,CPU需要浪费大量的时间等待外设。
在DMA模式下,外设和内存之间的数据交换可以完全不经过CPU,这样就节省了CPU的宝贵的时间。
二、DMA的硬件资源
以STM32F10X系列芯片为例(其它系列芯片的DMA资源需要查手册确认)在大容量产品和互联型产品中,有两个DMA控制器:DMA1和DMA2。该系列的其他产品仅有一个:DMA1,本文主要介绍DMA1。其中DMA1和DMA2的通道数量不一致,具体如下:
DMA1有7个通道
DMA2有5个通道
芯片中的每个外设连接到了固定的DMA通道,由于外设数量较多,每个DMA通道上分配了多个外设,这就意味着:在一个DMA通道中,在同一时刻,仅可以有一个请求生效。具体的分配图如下:
(1)DMA1通道分配:

(2)DMA2通道分配

三、DMA优先级
STM32F10x 的每个 DMA 通道可被多个外设 DMA 请求复用,当多个 DMA 通道同时发起传输请求时,需要通过优先级仲裁决定响应顺序。
DMA 优先级分为 软件优先级 和 硬件优先级两级仲裁机制:
- 软件优先级 由 DMA 仲裁器统一仲裁配置,可编程设置 4 个等级:非常高、高、中等、低。软件优先级可由用户在程序中自行配置。
- 硬件优先级 当多个 DMA 通道软件优先级设置相同时 ,自动启用硬件优先级裁决。硬件优先级由通道编号 决定:通道号越小,硬件优先级越高。
四、CPU和DMA的总线调度策略
STM32F10x 采用 Cortex-M3 内核,CPU 内核与 DMA 控制器共享系统数据总线,同一时刻总线只能被一方占用,因此需要硬件总线仲裁机制进行调度。
(1)总线仲裁规则
硬件默认配置下, DMA 控制器的总线请求优先级高于 CPU。原因是外设数据流具有实时性、内部缓存容量小,若总线响应延迟,极易造成数据丢失;而 CPU 程序执行可短暂等待,不会立即丢失关键数据
(2)周期窃取与分时复用机制
系统采用 周期窃取 的总线调度方式:DMA 并不会长期独占总线,而是每次仅占用少数总线周期完成一次数据传输,传输间隙主动释放总线给 CPU 使用。通过分时复用总线资源,既保证外设 DMA 实时传输,又 避免 CPU 长期得不到总线使用权而产生 "饥饿" 现象。
(3)DMA 请求与应答握手流程
外设需要以 DMA 方式收发数据时,首先向 DMA 控制器发送 DMA 请求信号 ;DMA 控制器经通道配置与总线仲裁通过后,向外设返回 DMA 应答信号,随后正式启动总线数据传输。
五、数据传输
STM32 DMA 支持三种数据传输方向:
- 外设 → 内存外设数据通过 DMA 搬运至内部存储器。
- 内存 → 外设内存中数据通过 DMA 发送到外设。
- 内存 → 内存无需外设参与,DMA 实现存储器之间直接搬运,仅 DMA1 支持该模式。
ROM 属于只读程序存储器,仅允许 ROM → RAM 的 DMA 读取;硬件上 禁止 RAM → ROM 写入操作,无法通过 DMA 向 ROM 写数据。
六、寄存器介绍
(1)DMA通道配置寄存器(DMA_CCRx)

MEM2MEM:使用存储器到存储器模式时置位该位
PL:优先级配置

MSIZE:存储器数据宽度,设置从存储器中一次获取的数据的数据大小,有8、16、32三个长度

PSIZE:外设数据宽度,设置向外设中一次发送的数据的数据大小,有8、16、32三个长度,除特殊需求,PSIZE和MSIZE应保持一致。

MINC:存储器地址自增,需要传输多个数据、且数据都在连续地址中存储时,可以使用该功能。

PINC:外设地址自增,当外设的数据连续存储在外设的内存中时,使用该功能。注意,如果外设的数据仅在同一个寄存器中不断刷新时,禁用该功能。

CIRC:循环模式,当设定了数据传输数量CNDTR后,DMA每传输一个数据,CNDTR的值会减1,直到减为零。当循环模式打开时,会再次回到初始位置重新开始传输,如果不使能循环模式,当CNDTR为0后传输停止。

DIR:数据传输方向

除此之外,还有三个中断控制位

(2)DMA通道传输数量寄存器(DMA_CNDTRx)
数据传输数量
数据传输数量为0至65535。这个寄存器只能在通道不工作(DMA_CCRx的EN=0)时写入。通道开启后该寄存器变为只读,指示剩余的待传输字节数目。寄存器内容在每次DMA传输后递减。
数据传输结束后,寄存器的内容或者变为0;或者当该通道配置为自动重加载模式时,寄存器的内容将被自动重新加载为之前配置时的数值。
当寄存器的内容为0时,无论通道是否开启,都不会发生任何数据传输。
(3)DMA通道外设地址寄存器(DMA_CPARx)
外设数据寄存器的基地址,作为数据传输的源或目标。
当PSIZE='01'(16位),不使用PA[0]位。操作自动地与半字地址对齐。
当PSIZE='10'(32位),不使用PA[1:0]位。操作自动地与字地址对齐。
(4)DMA通道存储器地址寄存器(DMA_CMARx)
存储器地址作为数据传输的源或目标。
当MSIZE='01'(16位),不使用MA[0]位。操作自动地与半字地址对齐。
当MSIZE='10'(32位),不使用MA[1:0]位。操作自动地与字地址对齐。
(5)DMA中断状态寄存器(DMA_ISR)
该寄存器的控制位分为三类:
TEIF:传输错误标志位
HTIF:传输过半标志位
TCIF:传输完成标志位
该寄存器为只读,由硬件置位,清零需要通过IFCR寄存器操作
(6)DMA中断标志清除寄存器(DMA_IFCR)
该寄存器的控制位分为三类:
CTEIF:清除传输错误标志位
CHTIF:清除传输过半标志位
CTCIF:清除传输完成标志位
七、使用寄存器操作DMA功能时的配置思路
(1)封装初始化函数:
1.开启时钟
2.DMA配置传输设置
数据传输方向配置
数据宽度配置
地址自增配置
3.配置中断,传输结束后进行处理,控制位:TCIE
4.NVIC中断配置
(2)封装数据传输函数
1.配置前先停止DMA
2.设置外设地址
3.设置目的地址
4.设置数据长度
5.使能DMA,开始传输
(3)寄存器操作DMA的使用示例
文件名:dma.c
cpp
#include "dma.h"
void DMA1_Init(void)
{
// 1.打开时钟模块
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
// 2.DMA配置
DMA1_Channel4->CCR = 0;
// 2.1.数据宽度配置00-8位宽度
DMA1_Channel4->CCR &= ~DMA_CCR4_MSIZE;
DMA1_Channel4->CCR &= ~DMA_CCR4_PSIZE;
// 2.2.地址自增配置
DMA1_Channel4->CCR |= DMA_CCR4_MINC;
DMA1_Channel4->CCR &= ~DMA_CCR4_PINC; // 串口不自增
// 2.3.数据传输方向
DMA1_Channel4->CCR |= DMA_CCR4_DIR; // 从存储器读
// 3.开启DMA中断,处理传输完成后的操作
DMA1_Channel4->CCR |= DMA_CCR4_TCIE;
// 4.中断配置
NVIC_SetPriorityGrouping(3);
NVIC_SetPriority(DMA1_Channel4_IRQn, 2);
NVIC_EnableIRQ(DMA1_Channel4_IRQn);
}
void DMA1_TO_USART1(uint32_t src, uint32_t dst, uint16_t dataLen)
{
// 传输前关闭DMA通道
DMA1_Channel4->CCR &= ~DMA_CCR4_EN;
// 设置源数据地址,内存地址
DMA1_Channel4->CMAR = src;
// 设置目的地址,外设地址
DMA1_Channel4->CPAR = dst;
// 设置数据长度
DMA1_Channel4->CNDTR = dataLen;
// 开启DMA通道
DMA1_Channel4->CCR |= DMA_CCR4_EN;
}
void DMA1_Channel4_IRQHandler(void)
{
if(DMA1->ISR & DMA_ISR_TCIF4)
{
// 关闭DMA通道4
/*
在中断服务函数中,常规的操作顺序是:
第一步:立刻关闭 / 屏蔽 能再次触发本次同类型中断的硬件使能
第二步:清除中断标志位
第三步:执行正常业务逻辑处理
*/
DMA1_Channel4->CCR &= ~DMA_CCR4_EN;
// 清除中断标志位
DMA1->IFCR |= DMA_IFCR_CTCIF4;
}
}
文件名:dma.h
cpp
#ifndef __DMA_H
#define __DMA_H
#include "stm32f10x.h"
// DMA初始化
void DMA1_Init(void);
// DMA通道传输数据到UART1
void DMA1_TO_USART1(uint32_t src, uint32_t dst, uint16_t dataLen);
#endif
文件名:usart.c
cpp
#include "usart.h"
// 1.串口初始化
void USART1_Init(void)
{
// 1.打开时钟模块
RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;
// 2.串口收发引脚配置
/*
GPIOA9 -TX 复用功能推挽输出;CNF-10;MODE-11
GPIOA10-RX 浮空输入;CNF-01;MODE-00
*/
GPIOA->CRH |= GPIO_CRH_CNF9_1;
GPIOA->CRH &= ~GPIO_CRH_CNF9_0;
GPIOA->CRH |= GPIO_CRH_MODE9;
GPIOA->CRH &= ~GPIO_CRH_CNF10_1;
GPIOA->CRH |= GPIO_CRH_CNF10_0;
GPIOA->CRH &= ~GPIO_CRH_MODE10;
// 3.串口参数配置,波特率115200
USART1->BRR = 0X271;
USART1->CR1 |= USART_CR1_UE;
USART1->CR1 |= USART_CR1_TE;
USART1->CR1 |= USART_CR1_RE;
// 开启DMA发送
USART1->CR3 |= USART_CR3_DMAT;
}
// 2.发送一字节数据
void USART1_Send_Byte(int ch)
{
while((USART1->SR & USART_SR_TXE) == 0); // 等待上一字节发送完成
USART1->DR = ch; // 发送当前字节
}
// 3.模拟实现fputc
int fputc(int ch, FILE* file)
{
USART1_Send_Byte(ch);
return ch;
}
文件名:usart.h
cpp
#ifndef __USART_H
#define __USART_H
#include "stm32f10x.h"
#include <stdio.h>
// 1.串口初始化
void USART1_Init(void);
// 2.发送一字节数据
void USART1_Send_Byte(int ch);
// 3.模拟实现fputc
int fputc(int ch, FILE* file);
#endif
文件名:main.c
cpp
#include "main.h"
uint8_t data_in_ram[4] = {"ABC"};
int main(void)
{
USART1_Init();
DMA1_Init();
printf("test.\n");
DMA1_TO_USART1((uint32_t)data_in_ram, (uint32_t)&(USART1->DR), 4);
while(1)
{
}
}
八、使用HAL库配置DMA的方法
通过STM32cubemx图形化配置界面做基础配置
1.时钟配置
HSE和LSE都选择选择外部晶振


2.串口基础配置
开启串口通讯功能,选择异步通讯,其余配置默认即可。

在串口配置界面配置DMA(也可以开启串口后再DMA中配置)

在这里选择UART-TX

其余配置保持默认即可

3.生成代码调用HAL库实现DMA传输数据
cpp
int main(void)
{
/* USER CODE BEGIN 1 */
// 准备要发送的数据
uint8_t data_in_ram[4] = {"abc"};
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init();
MX_USART1_UART_Init();
/* USER CODE BEGIN 2 */
// 调用HAL库,通过DMA发送数据
HAL_UART_Transmit_DMA(&huart1, data_in_ram, 4);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
以上是通过HAL库的实现方法,这些基础的操作全部由图形化界面配置完成,快速、稳定,在工作中,做产品开发时使用HAL库会大大提高开发效率,寄存器仅用于学习原理。