×
单片机 > 单片机程序设计 > 详情

STM32之DMA(直接存储器存储)

发布时间:2020-06-09 发布时间:
|

DMA简介: 
在硬件系统中,主要由CPU(内核),外设,内存(SRAM),总线等结构组成,数据就经常要在内存与外设之间传输转移,或者是从外设A转移到外设B. 
DMA(Direct Memory Access)直接存储器存储,是一种可以大大减轻CPU工作量的数据存储方式. 
数据转移的一般方式: 
例如当CPU需要处理由ADC外设采集回来的数据时,CPU首先要把数据从ADC外设的寄存器读取到内存中(变量),然后进行运算处理. 
(但是,因为在转移数据的过程中会占用CPU十分宝贵的资源,所以希望CPU更多地被用在数据运算或响应中断之中,而数据转移的工作交由其它部件完成。) 
DMA的方式: 
DMA可以为CPU分担了数据转移的工作。因为DMA的存在,CPU被解放出来,它可以在DMA转移数据的过程中同时进行数据运算,响应中断,大大提高效率.

DMA的工作: 
在STM32中文手册可以找到STM32的系统结构图,可以很清晰的看到STM32内核,存储器,外设以及DMA的连接 
 
所有这些硬件结构最终都通过各种各样的线连接到总线矩阵之中,硬件结构之间的数据转移都经过总线矩阵的协调,使各个外设都能够和谐地使用总线来传输数据.

例如: 
在不使用DMA的情况下,内核通过DCode经过总线矩阵协调,使用AHB把外设ADC采集的数据读取到内核,然后内核DCode再通过总线矩阵协调,把数据存放到内存SRAM中。 
而在使用DMA之后,由DMA控制器的DMA总线与总线矩阵协调,使用AHB把外设ADC的数据经由DMA通道存放到内存SRAM。在这个数据传输的过程中,不需要内核的全程参与,所以内核可以同时进行数据运算,而且DMA的方式是点到点的数据转移,而不使用DMA的方式还要以内核来作为中转站,显然是DMA的传输方式的效率更高。(所以”直接“是很强的东西!!)

DMA的控制参数: 
要使用DMA,需要确定一系列的控制参数: 
如外设数据的地址,内存地址,传输方向等,在开启DMA传输前还要先发出DMA请求。

DMA实例main函数: 
main函数功能: 
实际上是利用DMA把数据(数组)从内存转移到外设(串口)。外设工作的时候,除了转移数据,实质是不需要内核干预的,而数据转移的工作现在交给了DMA,所以在串口发送数据的时候,内核同时还可以进行其它操作,比如点亮LED灯(类似一个线程动作)。

头文件忽略……….. 
extern uint8_t SendBuff[SENDBUFF_SIZE]; 
uint16_t i;

int main(void) 

/USART1_Config 115200 8-N-1/ 
USART1_Config(); 
DMA_Config(); 
LED_GPIO_Config();

r(i=0;i

{

SendBuff[i] = 0xff;

}

/*串口向DMA发出请求 */

USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);//在DMA传送未完成时,CPU会继续执行main函数中的代码


LED1(ON);//先点亮LED,而同时DMA在向串口运送数据,当DMA发送完成时,在中断函数关闭LED

while(1);

}


main函数里面配置好了串口1,DMA,以及LED外设,使能DMA的发送请求.其中串口配置以及LEDGPIO的配置这里就不讲解了,前面的博客都有说,配置的内容都一样.


DMA的配置: 

void DMA_Config(void) 

DMA_InitTypeDef DMA_InitStructure; 

RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//开启 DMA 时钟

NVIC_Config(); //配置 DMA 中断


/*设置 DMA 源:内存地址&串口数据寄存器地址*/

DMA_InitStructure.DMA_PeripheralBaseAddr = USART1_DR_Base;

/*内存地址(要传输的变量的指针)*/

DMA_InitStructure.DMA_MemoryBaseAddr = (u32)SendBuff;

/*方向:从内存到外设*/

DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;

/*传输大小 DMA_BufferSize=SENDBUFF_SIZE*/

DMA_InitStructure.DMA_BufferSize = SENDBUFF_SIZE;

/*外设地址不增*/

DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;

/*内存地址自增*/

DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;

/*外设数据单位*/

DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;

/*内存数据单位 8bit*/

DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;

/*DMA 模式:一次传输,循环*/

DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;

/*优先级:中*/

DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;

/*禁止内存到内存的传输 */

DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;

/*配置 DMA1 的 4 通道*/

DMA_Init(DMA1_Channel4, &DMA_InitStructure);

DMA_Cmd (DMA1_Channel4,ENABLE); //使能

DMA_ITConfig(DMA1_Channel4,DMA_IT_TC,ENABLE); //配置 DMA 发送完之后产生中断

}

DMA_InitStructure.DMA_PeripheralBaseAddr = USART1_DR_Base; 
保存外设数据寄存器的基地址,这个地址作为传输的源或目标(DMA具有地址自增的功能,地址自增可以方便地读取连续的单元) 
这里面用的USART1_DR_Base宏实际是#define USART1_DR_Base 0x40013804,从《 STM32 参考手册》可知,串口外设会自动地把数据寄存器中的数据,送入它的移位寄存器,然后由硬件按照串口协议把该数据发送出去。 
在这个实例中,把数据寄存器的地址作为外设的地址,那么由DMA通道转移过来的内存数据就会被保护到这个寄存器中,然后串口就会自动进行发送了.

ps:外设数据寄存器的地址可以在《STM32参考手册》中找到一个存储器映射表(部分)(图截的不是很好看!!!) 
 
 
可以看到,USART1(串口1)的外设基地址为0x40013800,同时我们在查找《STM32数据手册》手册可以找到USART的数据寄存器,了解到偏移量是0x04,那么USART1的外设基地址加上数据寄存器的地址偏移就是在DMA传输中需要的目标地址了。 
0x4001 3804 = 0x4001 3800 + 0x04;

DMA_InitStructure.DMA_MemoryBaseAddr = (u32)SendBuff; 
保存了内存的基地址,这个地址也可以作为传输源或目标.在使用时通常会给这个成员赋值为某个数组的基地址,然后利用DMA的地址自增功能把数组一个个地填满. 
(在C语言中数组名就是该数组的基地址,而数组(变量)是被保存到内存(SRAM)上的,所以我们实质上给.DMA_MemoryBaseAddr 这个结构体成员赋予了一个内存地址。)

DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; 
DMA数据传输方向,可以选择是外设到内存还是内存到外设。DMA_DIR_PeripheralDST是内存到外设

DMA_InitStructure.DMA_BufferSize = SENDBUFF_SIZE; 
DMA要传输的数据总大小, DMA_BufferSize=SENDBUFF_SIZE=5000,这里要传输5000个数据

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; 
内存传输数据单元大小,可以为字节,半字节,字,这里是8bit.

DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; 
DMA的模式,可以为循环模式或者正常模式,在循环模式下传输完一轮数据之后再重新传输,适合ADC不断采集数据等场合,这里是正常模式,也就是一次.

DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; 
配置DMA通道的优先级,总线矩阵根据DMA通道的优先级进行总线协调分配,这里配置为DMA_Priority_Medium(中等优先级). 
(在使用1个DMA通道时,配置任何优先级都没有区别)

DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; 
使能内存到内存的DMA传输。DMA传输可以在外设与内存,外设与外设,还可以在内存与内存之间进行传输。 
这里用的是内存到外设 ,使用DMA_M2M_Disable(禁止内存到内存的传输).

DMA_Init(DMA1_Channel4, &DMA_InitStructure); 
DMA_Cmd (DMA1_Channel4,ENABLE);

填充好结构体后,就使能DMA了,这里用到DMA1_Channel4(DMA1的通道4),这个通道不是随便选择的,是根据DMA的请求映射来选择的,在《STM32参考手册》可以找到。 
(DMA请求是指外设在需要使用DMA前需要向DMA控制器发送请求信息,DMA在接收到请求后才会根据DMA配置进行数据转移。) 
 
PS: 
从图中可以看到即使同样是外设串口1,串口1的发送数据 DMA 请求和串口1的接收数据DMA请求通道都是不一样的,分别为DMA1通道4和DMA1的通道5。

DMA_ITConfig(DMA1_Channel4,DMA_IT_TC,ENABLE); 
把DMA配置成DMA_IT_TC(DMA发送完成标志)中断.DMA_ITConfig是用于外设中断的函数.

DMA的中断: 
中断配置: 
在DMA_Config()中调用了NVIC_Config()进行DMA中断配置 
static void NVIC_Config(void) 

NVIC_InitTypeDef NVIC_InitStructure;

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_1); //配置DMA通道的优先级组1


NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel4_IRQn;//中断通道

NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1;

NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;

NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

NVIC_Init(&NVIC_InitStructure);


这里的是调用了NVIC_PriorityGroupConfig()DMA的中断优先级为组1,中断通道配置为DMA1_Channel4_IRQn.


中断服务函数: 

void DMA1_Channel4_IRQHandler(void) 

{


if(DMA_GetFlagStatus(DMA1_FLAG_TC4)==SET)//判断是否为 DMA 发送完成中断

{


LED1(OFF);//LED 关闭

DMA_ClearFlag(DMA1_FLAG_TC4); //清除DMA中断标志位

}


这个中断服务函数名在启动文件startup_stm32f10x_hd.s中找到(以前说过,这里再说一次),这里就是检查中断标志位,关闭LED灯,清除中断标志位


在main函数里USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); 

使能或者关闭串口的DMA接口,这里配置的是串口1的USART_DMAReq_Tx(串口发送请求),也可以是USART_DMAReq_Rx(串口接收请求). 

调用这个库函数允许串口外设向 DMA 发出请求,请求DMA传输数据。调用了这个函数之后,DMA开始响应串口的请求,根据DMA配置,把数组中的数据一个个地转移到串口数据寄存器,并由串口向外发送这些数据。在调用了USART_DMACmd()函数之后,接下来在main函数就把LED点亮了.


PS: 

实际上,在DMA还没传输完成数据的时候,因为内核并不参与DMA数据传输的全过程,所以内核在这个时候执行了点亮LED1的代码。而当DMA传输完成时,进入了中断,再把LED1关闭.


关键字:STM32  DMA  直接存储器存储 


『本文转载自网络,版权归原作者所有,如有侵权请联系删除』

热门文章 更多
浅谈AVR中定时器几种工作模式