DMA(Direct Memory Access)直接存储器存取

DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源(其实外设也是存储器,这么说只是特定了单片机能够转运外设存储器的功能罢了)

12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道)

每个通道都支持软件触发(存储器到存储器之间,一股脑的把数据传输过去)和特定的硬件触发(外设到存储器之间,比如AD转换,每次AD转换之后,触发一次DMA转换数据,触发一次转运一次。之所以说特定的硬件触发,是因为不同的DMA对应不同的通道,触发源是不一样的。)

STM32F103C8T6 DMA资源:DMA1(7个通道)

计算机五大组成:运算器,控制器,存储器,输入设备,输出设备,其中运算器控制器合为一体,成为CPU。

存储器映像

image.png

ROM是只读存储器,非易失性,掉电不丢失。 RAM是随机存储器,易失性,掉电丢失。 

终止地址多大,取决于它是多大的存储器。

BootLoader程序一般是出厂自动写入,一般不允许更改,

选项字节配置的一般是对Flash的读保护以及写保护,还有看门狗等等配置。

运行内存SRAM:也就是存储变量,结构体,数组的地方

内核各个外设的配置参数:就是NVIC以及Systick,由于内核外设以及普通外设是两家不同公司设计的,所以地址是分开的。

image.png

 存储器的地址是从0x00000000到0xFFFFFFFF,也就是4GB左右,而STM32是KB级别的,所以存储器中大部分是空白部分,也就是图中的灰色部分。程序从0x00000000开启执行,在这里开始是没有存储器的,需要经过映射才能决定从哪个储存器启动,从哪里启动取决于BOOT0与BOOT1两个引脚决定,引脚配置。可以映射到SRAM,Flash,以及System menmory

图片右侧是每一个存储器展开的画面,具体到了哪个外设的地址。

image.png

 外设可以看成寄存器,也是SRAM存储器。

寄存器是一种特殊的存储器,CPU可以对寄存器进行读写,就像读写运行内存一样,寄存器每一位的背后都有一根导线,可以控制外设电路的状态,比如引脚的高低电平,开关导通断开,以及切换数据选择器。或者多位结合,当做计数器以及数据存储器。是连接硬件以及软件的桥梁。

DMA转运数据就是把某个地址的数据转运到另一个地址中,总线矩阵左边是主动单元,拥有存储器的访问权,右边的是被动单元,他们的存储器只能被左边的主动单元读写。内核部分有Dcode总线以及系统总线可以访问右边的存储器,其中DCode总线专门访问Flash存储器,系统总线访问其他。

有三条DMA总线,一个是DMA1以及DMA2,还有以太网私有的DMA总线。

虽然DMA中有多个通道,但是总线只有一条,所有通道只能分时复用一条总线。如果有冲突就用仲裁器盘点优先级。在总线矩阵那里也有一个仲裁器,当CPU以及DMA有冲突的时候,暂停CPU避免冲突,但是CPU还是会获得一半带宽,保证CPU运行。

AHB从设备,用来配置DMA,就是DMA自身的寄存器,DMA 作为一个外设,也有自己的配置寄存器,它连接到AHB总线上,所以DMA是总线矩阵的主动单元,可以读写寄存器。也是被动单元,CPU通过此路径对DMA进行配置。

DMA请求就是DMA的硬件触发信号,比如ADC在转换完数据之后,会向DMA发出请求,硬件触发,然后DMA开始工作了。

CPU或者DMA直接访问Flash的话,只能是只读模式,不能写入数据,写入数据只能通过Flash接口控制器控制写入。

SRAM可以进行任意读写。外设寄存器的话就能查看手册,有的只读有的只写。不过数据寄存器一般都能正常读写的。

image.png

 方向就是可以控制外设寄存器以及Flash以及SRAM之间,谁转换给谁。但是因为Flash是只读模式,只能从Flash把数据转运到SRAM中,而不能从SRAM中转换到Flash。

起始地址就是从哪里来到哪里去,数据宽度就是制定一次转换数据的大小,可以是字节Byte,半字Half Word以及字word,字节就是八位,一次转运uint8_t,半字是16位,一次转运uint16_t,字是32。

至于自增,以ADC转换来说,在每一次转换结束用DMA转运数据到存储器存储之时,到下一次转换,目标存储器要+1,移到下一个地址存储,以防数据被覆盖。

而外设寄存器这边,因为在转换完成的时候,就是先把转换结束的数值放进ADC_DR寄存器中的,然后再从中用DMA搬运到别的地方,如果+1,就会跑到别的寄存器中,所以一般不加。

并不是说外设的起始地址只能写写外设寄存器,存储器的地址只能写存储器,可以交换写的,没有限制,只是到时候设置方向的时候把方向设置对了就行。写什么就会去相应的寄存器中寻找数据。

传输计数器,设置多少就转运多少次,就是一个自减的运算器,没转运一次减1,到达0的时候,自增的大小变清零,回到起始地址,方便下次转运。

自动重装值就是转换变成0后重新回到与原来的数,这个适用于循环模式,如果仅仅需要转换数组,就没必要开启重装,如果是ADC的循环模式就需要。

M2M就是menmory to menmory,2就是tow,与to同音,存储器到存储器,两者作用就是选择哪一种触发。软件触发的执行逻辑是以最快的速度触发DMA,目的是以最快的速度进行把计数器清零,完成一轮转换,这里的软件触发跟外部中断软件触发以及ADC触发不太一样,前两者是有一定的时间要求的。软件触发以及自动重装循环模式不能同时使用,不然DMA就停不下来啦,软件触发一般用在存储器到存储器之间,不需要时机,而且想要快速转运完成的。M2M给1,就是软件触发。

给0,就是硬件触发,可以选择ADC,串口,定时器等等,一般与外设有关,需要一定时机,比如EOC,定时器到达中断,外部中断。

开关控制!

启动条件:触发源,开关控制使能,计数器不为0。

当计数器等于0,而且没有自动重装的话,只能把开关控制的关掉,给disable,然后再写入一个大于0的值,再启动开关ENABLE。

image.png

 硬件触发源不同选择不同的通道,软件就是都一样,每一个触发源的使用控制函数都是不一样,具体问题具体分析。

仲裁器中,默认通道号越小,优先级越高,当然也可以自行配置,

**数据宽度与对齐**

image.png

就是当两个传输之间的存储器数据宽度不一样的时候,怎么处理的。

小数据转到大数据,高位补零,大数据转到小数据,多读一位,只写一位。 

image.png

 结构里的各个参数该如何配置?首先是外设站点和存储器站点的起始地址、数据宽度、地址是否自增这三个参数。那在这个任务里,外设地址显然应该填对DataA数组的首地址。存储器地址给得DataB数组的首地址。然后数据宽度两个数组的类型都是uint8_t,所以数据宽度都是按八位的字节传出。我们想要的效果是得DataA0转到DataB0,DataB1转到DataB1等等,两个数组的位置一一对应呀,所以转运完DataA0转到DataB0之后,两个站点儿的地址都应该指针都移动到下一个数据的位置。继续转运塔A1和塔B1这样来进行。

方向参数,那显然就是外设站点转运到存储器站点了,如果你想把得塔B的数据转运到data塔一,那可以把方向参数换过来,这样就是反向转运了。然后是传输计数器和是否要自动重装。在这里啊,显然要转运七次,所以传输机游戏给七自动重装暂时不需要。之后触发选择部分。这里我们要使用软件出发,因为这是存储器到存储器的数据转移,是不需要等待硬件时机的,尽快转移完成就行了,那最后调用DMA给DMA使能,这样数据就会从data塔a转移到data塔B的,转运七次之后,传输进入器自检到0停止转运完成。这里的数据转运是一种复制转运,转运完之后贝塔a的数据并不会消失。

image.png

接着看第二个任务,ADC扫描模式加DMA,左边是ADC扫描模式的执行流程,在这里有七个通道触发仪,之后七个通道依次进行ad转换,然后转换结果都放到ADC_DR数据寄存器里面,那我们要做的就是在每个单独的通道转换完成后,进行一次dma数据转运,并且目的地址进行自增,这样数据就不会被覆盖了。所以在这里。DMA的配置就是外设地址写作ADC_DR这个寄存器地址,目标地址写入存储器的地址。可以在SRAM中定义一个数组然后把数组的地址当做存储器的地址。之后数据宽度,因为ADC_DR和数组,我们要的都是UNIT16_t的数据,所以数据宽度都是16位的半值.图中所示,外设地址(也就是起始地址)不自增,目标地址,也就是存储器地址自增。方向是外设站点到存储器站点,计数器这里有七个,计数为7。当使用单次扫描,就不适用重装,使用连续扫描的时候,就是用DMA重装。

最后是触发选择,这里ACD的值是在ADC单个通道转换完成后才会有效,所以dma转移的时机需要和ADC单个通道转换完成同步,所以dma的触发要选择ADC的硬件触发。ADC扫描模式在每个单独的通道转换完成后,没有任何标志位,也不会触发中断,所以我们程序不太好判断某一个通道转换完成的时机是什么时候。但是根据我的研究,虽然单个通道转换完成后不产生任何标志位和中断,但是他应该会产生DMA请求去触发d ma转运,这部分内容手圈儿里并没有详细描述,根据我实际实验啊,单个通道的d ma请求肯定是有的现在就做不成了。这些就是ADC扫描模式和DMA配合使用的流程。一般来说,D ma最常见的用途就是配合ADC的扫描模式。因为ADC扫描模式有个数据覆盖的特征,或者可以说这个数据覆盖的问题是ADC固有的缺陷,这个缺陷使ADC和DMA成了最常见的伙伴儿,ADC对DMA的需求是非常强烈的,像其他的一些外设,使用d ma可以提高效率,是锦上添花的操作,但是不使用也是可以的,顶多是损失一些性能啊,但是这个ADC的扫描模式如果不使用d ma,功能都会受到很大的限制,所以ADC和DMA的结合最为常见。

此部分内容在参考手册中的2.存储器与总线架构

10DMA

变量存储在SRAM存储器中,而变量加上一个const之后就是常量,常量只读不可更改,就存储在Flash储存器中,而在显示地址的时候,要在地址前main加上一个强制类型转换,变成特定的格式。(uint32_t)

当需要一大片不需要更改的数据的时候,就把他设置成常量,然后节省SRAM的空间。比如查找表以及字库数据等等。比如OLED_front的字库中一样。

对于变量或者常量来说,地址是不确定的,由编译器自行决定,而外设寄存器的地址是固定的,手册可查。2.3结合11.12.15来看。

也可以用构体很方便的访问寄存器,比如访问ADC1的DR寄存器,就ADC1->DR,就可以了。

假设要看adc的DR寄存器,那么就(uint32_t)&ADC1->DR。2.3中查到ADC1的起始地址是0x40012400,而11.12.15查到DR寄存器偏移了4c,所以DR的寄存器地址数值是0x4001244c。

也就是首先查一下寄存器外设所在起始地址,然后再在外设寄存器总表中具体查其中的偏移地址。起始地址加上偏移地址就是实际地址。

#define ADC1                ((ADC_TypeDef *) ADC1_BASE)//就是用ADC1代替 ((ADC_TypeDef *) ADC1_BASE)

而 ((ADC_TypeDef *) ADC1_BASE)意思就是定义一个结构体指针,指针指向ADC1_BASE,这时候结构体中的首地址对应的就是指向地址的首地址,定义的成员是一一对应的关系。算是指定结构体的起始地址就是外设的起始地址,故而结构体的内存与外设内存完美重合。访问结构体的成员就相当于访问外设寄存器了。

ADC1是指向外设寄存器起始地址的结构体指针,->成员就相当于访问其中结构体的成员。

#define PERIPH_BASE           ((uint32_t)0x40000000);//外设寄存器起始地址

#define APB2PERIPH_BASE       (PERIPH_BASE + 0x10000);//外设基地址+0x10000

#define ADC1_BASE             (APB2PERIPH_BASE + 0x2400);//APB2外设基地址+0x2400

#define ADC1                ((ADC_TypeDef *) ADC1_BASE);

OLED_ShowHexNum(1, 8, (uint32_t)&ADC1->DR, 8);//上面的由跳转所得。

初始化DMA的步骤:

1.RCC开启DMA的时钟

2.调用DMA_Init用来初始化各个参数了

3.开关控制,DMA_cmd

4.如果是硬件触发,别忘了用***_DMACmd开启,触发信号的输出。如果需要DMA中断,就用DMA_Itconfig中断输出,然后配置NVIC。

void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber); //配置计数器的值

uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx);//返回计数器的值

FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG);获取标志位状态

void DMA_ClearFlag(uint32_t DMAy_FLAG);清除标志位状态

ITStatus DMA_GetITStatus(uint32_t DMAy_IT);获取中段标志位状态

void DMA_ClearITPendingBit(uint32_t DMAy_IT);清除

uint16_t MyDMA_Size;

void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)

{

    MyDMA_Size = Size;

       RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

//跳转此函数,得到下图,互联性设备(105.107用上面的那一组参数,其他用下面的。)    

image.png

    DMA_InitTypeDef DMA_InitStructure;

    DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;

    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;

    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;

    DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;//起始地址,因为数组是变量存储一般不是绝对的,就不用绝对地址写入,一般都用数组名来表示,初始地址,在函数参数中传入。

    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//宽度

    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//自增

//上面留个函数就是配置两个储存器的参数的

    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//传输方向

跳转:

#define DMA_DIR_PeripheralDST              ((uint32_t)0x00000010)外设站点作为目的地destination

#define DMA_DIR_PeripheralSRC              ((uint32_t)0x00000000)源头soure

***********************

    DMA_InitStructure.DMA_BufferSize = Size;//传输给计数器的寄存器0~65535,这里是作为参数传输

    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;//是否重装

跳转得到如下:*********************

/** @defgroup DMA_circular_normal_mode 

  * @{

  */

#define DMA_Mode_Circular                  ((uint32_t)0x00000020)//循环模式,则是自动重装

#define DMA_Mode_Normal                    ((uint32_t)0x00000000)//正常模式,没有自动重装

**************************

    DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;//选择硬件触发还是软件触发,ENABLE软件,DISABLE就是硬件触发

    DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//优先级

    DMA_Init(DMA1_Channel1, &DMA_InitStructure);//这个函数的第一个参数选择了DMA以及通道

    

    DMA_Cmd(DMA1_Channel1, DISABLE);

}

 小技巧:ALT+鼠标左键,就是小方框形式框选,这样改数据就比较快。

DMA_GetFlagStatus(DMA1_FLAG_TC1);跳转得到如下:符合要求标志位置1

************

  *     @arg DMA1_FLAG_GL1: DMA1 Channel1 global flag.//全局标志位

  *     @arg DMA1_FLAG_TC1: DMA1 Channel1 transfer complete flag.//转运完成标志位

  *     @arg DMA1_FLAG_HT1: DMA1 Channel1 half transfer flag.//转运过半标志位

  *     @arg DMA1_FLAG_TE1: DMA1 Channel1 transfer error flag.//转运错误标志位

************

之后别忘了手动清除标志位:DMA_ClearFlag(DMA1_FLAG_TC1);

如果用DMA转运ADC的话,ADC结束之后,会把数值放在DR寄存器中,因此起始地址就需要是DR寄存器的初始地址:0x4001244c,或者用(uint32_t)&ADC1->DR。要读取DR寄存器的低十六位,宽度就设置为半字。但是选择ADC触发转运的话,就属于硬件触发,选择传输通道的时候是固定的。具体看手册。

另外,还需要把ADC到DMA的信号开启,这样在ADC好了时候,才会触发DMA转运。

void ADC_DMACmd(ADC_TypeDef* ADCx, FunctionalState NewState);

可以把.c文件定义的变量放在.h文件中,在前面加上一个extern,就变成了可调用函数。

image.png

传统的就是CPU统一调度外设的星状结构,而如今的这种,就是外设之间互相配合形成的网状结构。大大节省了CPU的资源。

**DMA转运ADC多通道**

代码:.c文件中

#include "stm32f10x.h"                  // Device header
 
uint16_t AD_Value[4];
 
void AD_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);
	
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);
	ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);
		
	ADC_InitTypeDef ADC_InitStructure;
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;
	ADC_InitStructure.ADC_NbrOfChannel = 4;
	ADC_Init(ADC1, &ADC_InitStructure);
	
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
	DMA_InitStructure.DMA_BufferSize = 4;
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);
	
	DMA_Cmd(DMA1_Channel1, ENABLE);
	ADC_DMACmd(ADC1, ENABLE);
	ADC_Cmd(ADC1, ENABLE);
	
	ADC_ResetCalibration(ADC1);
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1) == SET);
	
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

main文件中:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
 
int main(void)
{
	OLED_Init();
	AD_Init();
	
	OLED_ShowString(1, 1, "AD0:");
	OLED_ShowString(2, 1, "AD1:");
	OLED_ShowString(3, 1, "AD2:");
	OLED_ShowString(4, 1, "AD3:");
	
	while (1)
	{
		OLED_ShowNum(1, 5, AD_Value[0], 4);
		OLED_ShowNum(2, 5, AD_Value[1], 4);
		OLED_ShowNum(3, 5, AD_Value[2], 4);
		OLED_ShowNum(4, 5, AD_Value[3], 4);
		
		Delay_ms(100);
	}
}

.h文件中

#ifndef __AD_H
#define __AD_H
 
extern uint16_t AD_Value[4];
 
void AD_Init(void);
 
#endif