【STM32】DMA直接存储器存储

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

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

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

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

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

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

存储器映像

类型

起始地址

存储器

用途

ROM

0x0800 0000

程序存储器Flash

存储C语言编译后的程序代码以及常量数据

(const)

0x1FFF F000

系统存储器

存储BootLoader程序,用于串口下载

0x1FFF F800

选项字节

存储一些独立于程序代码的配置参数

RAM

0x2000 0000

运行内存SRAM

存储运行过程中的临时变量

0x4000 0000

外设寄存器

存储各个外设的配置参数

0xE000 0000

内核外设寄存器

存储内核各个外设的配置参数

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

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

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

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

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

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

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

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

 外设可以看成寄存器,也是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可以进行任意读写。外设寄存器的话就能查看手册,有的只读有的只写。不过数据寄存器一般都能正常读写的。

 方向就是可以控制外设寄存器以及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。

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

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

**数据宽度与对齐**

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

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

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

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

 

接着看第二个任务,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用上面的那一组参数,其他用下面的。)    
    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,就变成了可调用函数。

传统的就是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

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值