DMA,全称 Direct Memory Access,即直接存储器访问。
DMA传输将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。

CPU负责执行指令、处理数据和控制系统流程,是嵌入式系统的运作核心。它无时不刻地在处理大量事务,但像大规模数据复制这类操作会消耗其宝贵的计算周期。如果能将这部分相对简单的任务分流出去,让CPU专注于更复杂的计算与控制逻辑,无疑能显著提升系统效率。
因此,数据传输(尤其是大量数据)可以无需CPU直接参与。例如,希望将外设A的数据拷贝到外设B,只需为两者提供一条直接的数据通路。

DMA正是基于这一理念设计的,它的核心作用就是解决大量数据搬移过度消耗CPU资源的问题。有了DMA,CPU得以从繁琐的数据搬运中解放出来,更专注于计算、控制等核心任务。
DMA定义
DMA用于在外设和存储器之间或者存储器和存储器之间提供高速数据传输。整个过程无需CPU干预,数据可以快速移动,从而节省CPU资源以执行其他操作。
DMA传输方式
DMA实现了数据的直接传输,省去了传统方式中需要CPU寄存器中转的环节。它主要处理四种情况的数据传输,其本质都是从内存的一个区域传输到另一个区域(外设的数据寄存器可视为一个特殊的内存存储单元):
DMA传输参数
任何数据传输都需要几个基本要素:源地址、目标地址、传输数据量以及传输模式。DMA控制器所需要的核心参数也正是这四个。
用户设置好参数(主要是源地址、目标地址和传输数据量)并启动DMA后,控制器便会开始传输。当剩余传输数据量减至0时,传输到达终点,DMA停止。此外,DMA还支持循环传输模式,到达终点时会自动重新加载参数并启动下一次传输。简而言之,只要DMA处于启动状态且剩余传输数据量不为0,数据传输就会持续进行。

DMA的主要特征
- 每个通道都直接连接专用的硬件DMA请求,同时也支持软件触发,这些功能可通过软件配置。
- 在同一个DMA模块上,多个请求的优先级可通过软件编程设置(共有四级:很高、高、中等和低),优先级相同时由硬件决定(例如,请求0优先于请求1)。
- 支持独立配置源和目标的数据传输宽度(字节、半字、全字),模拟数据的打包和拆包过程。源和目标地址必须按数据传输宽度对齐。
- 支持循环缓冲器管理。
- 每个通道都有3个事件标志(DMA半传输、DMA传输完成和DMA传输出错),这三个标志逻辑或后成为一个单独的中断请求。
- 支持存储器到存储器、外设到存储器、存储器到外设之间的传输。
- 闪存、SRAM、外设的SRAM、APB1、APB2和AHB外设均可作为访问的源或目标。
- 可编程的数据传输数目,最大为65535。
STM32的DMA资源
对于大容量型号的STM32芯片,共有2个DMA控制器:DMA1有7个通道,DMA2有5个通道。每个通道都可以配置给特定的外设使用。
① DMA1控制器
来自外设(TIMx[x=1、2、3、4]、ADC1、SPI1、SPI/I2S2、I2Cx[x=1、2]和USARTx[x=1、2、3])产生的7个请求通过逻辑或输入到DMA1控制器,每个通道对应具体的外设。


② DMA2控制器
来自外设(TIMx[5、6、7、8]、ADC3、SPI/I2S3、UART4、DAC通道1、2和SDIO)产生的5个请求,经逻辑或输入到DMA2控制器,每个通道对应具体的外设。


这些对应关系在下方的系统框图中也能清晰看到。
DMA工作系统框图

从上图框图中,我们可以看到STM32内核、存储器、外设及DMA之间的连接。这些硬件通过总线矩阵协同工作,所有硬件间的数据转移都经由总线矩阵的协调,确保各个外设能够和谐地使用总线传输数据。
我们来对比一下,在有和无DMA的情况下,ADC采集的数据是如何存放到SRAM中的。
没有DMA的情况:
如果没有DMA,CPU需要作为数据传输的中转站。例如,将ADC采集的数据转移到SRAM的过程是:内核通过DCode,在总线矩阵的协调下,从AHB总线上的ADC外设获取数据;然后,内核再次通过DCode,在总线矩阵协调下,将数据存放到SRAM中。

有DMA传输的情况:
- 外设(如ADC)向DMA控制器发出传输请求。
- DMA控制器收到请求,触发相应通道开始工作。
- DMA控制器直接从AHB外设(ADC)获取数据。
- DMA控制器的总线与总线矩阵协调,使用AHB将数据直接存放到SRAM中。整个过程完全不需要内核(CPU)参与。

我们可以将上述过程更专业地描述为:在一个事件(如ADC转换完成)发生后,外设向DMA控制器发送一个请求信号。DMA控制器根据通道优先级处理请求。当DMA控制器开始访问发出请求的外设时,会立即发送一个应答信号给该外设。外设收到应答信号后立即释放请求,随后DMA控制器也撤销应答信号。一次DMA传输就此结束。如果还有更多数据需要传输,外设可以启动下一个请求周期。
总之,每次DMA传送由3个操作组成:
- 从外设数据寄存器或存储器地址寄存器指示的源地址取数据(首次传输的地址由DMA_CPARx或DMA_CMARx寄存器指定)。
- 将数据存放到外设数据寄存器或存储器地址寄存器指示的目标地址(首次传输的地址由DMA_CPARx或DMA_CMARx寄存器指定)。
- 对DMA_CNDTRx寄存器(包含未完成的操作数目)执行一次递减操作。
DMA传输模式
- 正常模式 (DMA_Mode_Normal):当一次DMA数据传输完成后,停止DMA传送,即只传输一次。
- 循环传输模式 (DMA_Mode_Circular):当传输结束时,硬件会自动重装传输数据量寄存器,并开始下一轮的数据传输,即多次重复传输。
仲裁器

仲裁器的作用是确定各个DMA传输请求的优先级,并根据优先级来启动对外设或存储器的访问。
优先级管理分为2个阶段:
- 软件:每个通道的优先级可以在DMA_CCRx寄存器中设置,共有4个等级:最高、高、中等、低。
- 硬件:如果两个请求的软件优先级相同,则编号较低的通道拥有较高的硬件优先级(例如,通道2优先于通道4)。
注意:在大容量产品和互联型产品中,DMA1控制器拥有高于DMA2控制器的优先级。
DMA数据流(仅存在于STM32F4 / M4 内核)
在STM32F4等系列中,设置了DMA通道后,还需选择该通道对应的外设数据流。每个数据流都能在源和目标之间提供单向传输链路,并支持常规传输(内存到外设、外设到内存、内存到内存)或双缓冲区传输。
DMA_SxCR寄存器控制数据流使用哪个通道,每个数据流可从8个通道中选择其一进行DMA传输。

DMA 传输通道
每个通道都可以在外设寄存器和存储器地址之间执行DMA传输,传输的数据量最大可编程为65535。包含要传输数据项数量的寄存器在每次传输后递减。
传输数据宽度可通过DMA_CCRx寄存器中的PSIZE(外设数据宽度)和MSIZE(存储器数据宽度)位编程设置。
指针递增模式
根据DMA_SxCR寄存器中PINC和MINC位的状态,外设和存储器指针在每次传输后可以自动递增或保持常量。当设置为增量模式时,下一个传输地址将是前一个地址加上增量值(根据PSIZE/MSIZE设置的数据宽度,递增1、2或4个字节)。
对于通过单个寄存器访问的外设源或目标数据,禁止递增模式非常有用。
存储器到存储器模式
DMA通道的操作可以在没有外设请求的情况下进行,这种模式称为存储器到存储器模式。
当设置了DMA_CCRx寄存器中的MEM2MEM位,并通过软件设置EN位启动DMA通道后,DMA传输将立即开始。当DMA_CNDTRx寄存器变为0时,传输结束。
注意:仅DMA2的外设接口可以访问存储器,因此只有DMA2控制器支持存储器到存储器的传输,DMA1不支持。此外,存储器到存储器模式不能与循环模式同时使用。
DMA中断
每个DMA通道都可以在DMA传输过半、传输完成和传输错误时产生中断。通过设置相应寄存器的位可以独立开启这些中断。

即使没有开启中断,也可以通过查询这些状态位来获取当前DMA传输的进度。其中最常用的是TCIFx位,用于判断数据流x的DMA传输是否完成。
注意:在大容量产品中,DMA2通道4和通道5的中断被映射到同一个中断向量上。而在互联型产品中,它们有独立的中断向量。其他DMA通道通常都有自己的中断向量。
DMA的内存占用
在采用Cortex-MX架构的STM32中,DMA使用独立的地址总线,不会与CPU的系统总线发生冲突。因此,使用DMA通常不会影响CPU的运行速度。
但是需要注意:DMA控制器和Cortex-M3内核共享系统数据总线来执行直接存储器数据传输。当CPU和DMA同时访问相同的目标(RAM或外设)时,DMA请求可能会使CPU暂停访问系统总线若干个周期。总线仲裁器会执行循环调度,以确保CPU至少能得到一半的系统总线带宽。
DMA配置部分
此部分分为DMA寄存器和DMA库函数两方面进行介绍。
DMA寄存器
DMA的配置参数主要包括:通道地址、优先级、数据传输方向、存储器/外设数据宽度、地址是否增量、循环模式以及数据传输量。
DMA中断状态寄存器 (DMA_ISR)

如果开启了DMA_ISR中的相应中断,在条件满足后就会跳转到中断服务函数。即使未开启,也可以通过查询这些位来获得当前DMA传输的状态,最常用的是TCIFx(通道x传输完成标志)。
注意:此寄存器为只读寄存器,位被置位后,只能通过其他操作来清除。
DMA中断标志清除寄存器 (DMA_IFCR)

DMA_IFCR寄存器的各位用于清除DMA_ISR寄存器中的对应标志位,通过写1来清除(具体看手册,示例图中为写1清除)。在DMA_ISR的某位被置位后,必须通过向DMA_IFCR对应位写入特定值来清除它。
DMA通道x配置寄存器 (DMA_CCRx)

该寄存器控制着DMA的大部分核心配置,包括数据宽度、地址增量模式、传输方向、通道优先级、中断使能、传输模式使能等。因此,DMA_CCRx是DMA传输的核心控制寄存器。
DMA通道x传输数量寄存器 (DMA_CNDTRx)

这个寄存器控制DMA通道x每次传输所要传输的数据量,设置范围为0~65535。该寄存器的值会随着传输进行而递减,当值为0时代表数据传输全部完成。因此,可以通过读取此寄存器的值来了解当前DMA传输的进度。
DMA通道x外设地址寄存器 (DMA_CPARx)

该寄存器用于存储STM32外设的地址。例如,如果使用串口1进行发送,则该寄存器应写入&USART1_DR(即USART1数据寄存器的地址)。使用其他外设时,修改为相应外设的地址即可。
DMA通道x存储器地址寄存器 (DMA_CMARx)

该寄存器类似于DMA_CPARx,但用于存储存储器的地址。例如,如果使用数组SendBuff[5200]作为存储区,则在DMA_CMARx中写入&SendBuff即可。
DMA寄存器配置流程
配置DMA通道x的一般步骤如下:
- 在DMA_CPARx寄存器中设置外设寄存器的地址。发生传输请求时,此地址将是数据传输的源或目标。
- 在DMA_CMARx寄存器中设置数据存储器的地址。传输时,数据将从此地址读出或写入此地址。
- 在DMA_CNDTRx寄存器中设置要传输的数据量。每次传输后,此数值递减。
- 在DMA_CCRx寄存器的PL[1:0]位中设置通道的优先级。
- 在DMA_CCRx寄存器中设置数据传输方向、循环模式、地址增量模式、数据宽度以及中断使能位(如半传输中断、传输完成中断)。
- 设置DMA_CCRx寄存器的ENABLE位,启动该通道。
一旦启动,通道即可响应连接的外设发出的DMA请求。传输一半数据后,半传输标志(HTIF)置1;全部传输完成后,传输完成标志(TCIF)置1。如果对应中断使能位(HTIE/TCIE)已设置,将产生中断请求。
DMA库函数
1. DMA初始化函数
DMA_DeInit(DMAy_Channelx);
功能:将DMAy Channelx寄存器的初始化为其默认值。
注:因为RCC_ResetCmd中对DMA无定义,所以此函数采用直接操纵DMA寄存器的方式实现。
void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct)
功能:设置要开启的通道及其参数,包括外设基地址、存储器基地址、传输数据量、增量模式、数据宽度等。具体参数见下方结构体:
typedef struct {
uint32_t DMA_PeripheralBaseAddr; /* 设置DMA源外设地址 */
uint32_t DMA_MemoryBaseAddr; /* 设置DMA目的存储器地址 */
uint32_t DMA_DIR; /* 设置数据传输方向,决定是从外设读数据到内存,还是从内存写数据到外设 */
uint32_t DMA_BufferSize; /* 设置传输数据量大小 */
uint32_t DMA_PeripheralInc; /* 设置外设地址是否自增 */
uint32_t DMA_MemoryInc; /* 设置存储器地址是否自增 */
uint32_t DMA_PeripheralDataSize; /* 设置外设数据宽度:字节(8bits)、半字(16bits)或字(32bits) */
uint32_t DMA_MemoryDataSize; /* 设置存储器数据宽度 */
uint32_t DMA_Mode; /* 设置DMA模式:正常模式或循环模式 */
uint32_t DMA_Priority; /* 设置DMA通道优先级:低、中、高、超高 */
uint32_t DMA_M2M; /* 设置是否使能存储器到存储器模式 */
} DMA_InitTypeDef;
2. DMA使能函数
void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState);
功能:使能或失能指定的DMA通道。
示例:DMA_Cmd(DMA1_Channel1, ENABLE);
3. DMA中断使能函数
void DMA_ITConfig(DMA_Channel_TypeDef* DMAy_Channelx, uint32_t DMA_IT, FunctionalState NewState);
功能:配置指定DMA通道的中断。
注:DMA_IT参数可选 DMA_IT_TC(传输完成)、DMA_IT_HT(传输一半)、DMA_IT_TE(传输错误)。
示例:DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE);
4. 设置与获取当前传输数据量函数
void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber);
uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx);
作用:前者用于在DMA通道关闭时设置传输数据量;后者用于在DMA通道开启时获取剩余未传输的数据量。
DMA库函数配置流程:
- 使能DMA时钟:
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
- 初始化DMA通道:
DMA_Init();
- 设置通道、外设/存储器地址、传输方向、数据量、数据宽度、地址增量模式、传输模式、优先级、M2M模式等。
- 使能外设的DMA功能:
- 以串口为例,使能串口DMA发送:
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
- 使能DMA通道传输:
DMA_Cmd(DMA1_Channelx, ENABLE);
- (可选)查询DMA传输状态:
DMA_GetFlagStatus(DMA1_FLAG_TCx);
- (可选)获取当前剩余数据量:
DMA_GetCurrDataCounter(DMA1_Channelx);
UART DMA传输应用
DMA就像一个高效的数据搬运工。以UART接收为例,传统方式下,数据到达会触发UART中断,CPU需要介入,在中断服务函数中读取UART数据寄存器并存入内存。而使用DMA方式,UART接收数据时,DMA控制器会直接介入,自动将UART数据寄存器的值搬运到预设的内存缓冲区中,CPU只需在需要时去检查内存中的数据即可,大大提高了CPU的效率。
DMA代码配置实例
① DMA初始化配置
以下是一个使用DMA1通道4进行USART1发送的初始化示例:
#define DMA1_MEM_LEN 500
u8 SendBuff[DMA1_MEM_LEN];
#define USART1_DR_Base 0x40013804 //USART1数据寄存器地址
void dma_init(void)
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
/* DMA配置 */
DMA_InitStructure.DMA_PeripheralBaseAddr = USART1_DR_Base; //外设地址(串口数据寄存器)
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)SendBuff; //内存地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; //传输方向:内存到外设
DMA_InitStructure.DMA_BufferSize = DMA1_MEM_LEN; //传输数据量
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址固定
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_Medium; //优先级:中
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //禁用内存到内存模式
DMA_Init(DMA1_Channel4, &DMA_InitStructure); //初始化DMA1通道4
DMA_Cmd(DMA1_Channel4, ENABLE); //使能通道
DMA_SetCurrDataCounter(DMA1_Channel4, DMA1_MEM_LEN); //设置传输数据量
DMA_ITConfig(DMA1_Channel4, DMA_IT_TC, ENABLE); //使能传输完成中断
}
② DMA中断服务函数
void DMA1_Channel4_IRQHandler(void)
{
if(DMA_GetFlagStatus(DMA1_FLAG_TC4) == SET)
{
// 传输完成处理逻辑
DMA_ClearFlag(DMA1_FLAG_TC4); //清除标志位
}
}
③ 主函数示例
#define SEND_BUF_SIZE 500
u8 SendBuff[SEND_BUF_SIZE];
const u8 TEXT_TO_SEND[]={"STM32F1 DMA 串口实验"};
uint16_t i;
int main(void)
{
uart_init(115200); //串口初始化
// 填充发送缓冲区
for(i=0; i<SEND_BUF_SIZE; i++)
{
SendBuff[i] = 0xAF; //示例数据
}
dma_init(); //初始化DMA
USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); //使能串口的DMA发送请求
while(1)
{
// 主循环,DMA在后台自动发送数据
}
}
通过以上讲解与实例,我们深入探讨了DMA在嵌入式系统,特别是STM32平台上的工作原理、配置方法和应用场景。理解并熟练运用DMA对于进行高效的嵌入式计算机基础编程至关重要。希望本文能帮助你更好地掌握这一关键技术。更多技术讨论与资源分享,欢迎访问云栈社区。