江协科技STM32学习笔记(第07章 DMA直接存储器存取)

第07章 DMA直接存储器存取

DMA主要用来协助CPU完成数据转运的工作。

7.1 DMA直接存储器存储

7.1.1 DMA简介

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

DMA这个外设,可以直接访问STM32内部的存储器的,包括运行内存SRAM、程序存储器Flash和寄存器等等,DMA都有权限访问它们,所以DMA能完成数据转运的工作。

DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源。

外设指的是外设寄存器,一般是外设的数据寄存器DR、Data Register,比如ADC的输入寄存器,串口的数据寄存器等等。存储器指的就是运行内存SRAM和程序存储器Flash,是我们存储变量数组和程序代码的地方。

12个独立可配置的通道: DMA17个通道), DMA25个通道)。

这个通道就是数据转运的路径,从一个地方移动到另一个地方,就需要占用一个通道,如果有多个通道进行转运,那它们之间各转各的,互不干扰。

每个通道都支持软件触发和特定的硬件触发。

如果DMA进行的是存储器到存储器的数据转运,比如想把Flash里的一批数据,转运到SRAM里去,那就需要软件触发了,使用软件触发,DMA会一股脑地,把这批数据,以最快的速度,全部转运完成。若果DMA进行的是外设到存储器的数据转运,就不能一股脑的转运了,因为外设的数据是有一定时机的,所以这时候需要用硬件触发,比如转运ADC的数据,就得ADC每个通道AD转换完成后,硬件触发一次DMA,之后DMA再转运,触发一次,转运一次,这样数据才是正确的。所以存储器到存储器的数据转运,我们一般使用软件触发;外设到存储器的数据转运,我们一般使用硬件触发。特定的硬件触发指的是每个DMA的通道,它的硬件触发源是不一样的,如果要使用某个外设的的硬件触发源,就得使用它连接的那个通道,而不能任意选择通道。

STM32F103C8T6 DMA资源:DMA17个通道)。

7.1.2 存储器映像

计算机设备的五大组成部分:运算器、控制器、存储器、输入设备和输出设备,运算器和控制器合在一起叫做CPU,所以计算机的核心关键部分就是CPU和存储器。

类型

起始地址

存储器

用途

ROM

0x0800 0000

程序存储器Flash

存储C语言编译后的程序代码

0x1FFF F000

系统存储器

存储BootLoader,用于串口下载

0x1FFF F800

选项字节

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

RAM

0x2000 0000

运行内存SRAM

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

0x4000 0000

外设寄存器

存储各个外设的配置参数

0xE000 0000

内核外设寄存器

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

ROM:只读存储器,是一种非易失性、掉电不丢失的存储器。

RAM:随机存储器,是一种易失性、掉电丢失的存储器。

程序存储器Flash:主闪存,我们下载程序的位置。运行程序一般也是从主闪存里面开始运行的。如果再软件里看到,某个数据的地址是0800开头的,就可以确定,它是属于主闪存的数据。

系统存储器和选项字节:存储介质也是Flash,不过我们一般说Flash指的是主闪存Flash,而不指这两块区域。选项字节里寸的主要是Flash的读保护、写保护,还有看门狗等等的配置。

运行内存SRAM:我们在程序中定义变量、数组、结构体的地方。定义一个变量,读取它的地址显示出来,它的地址就是0x2000开头的,类比于电脑的话,运行内存就是内存条。

外设寄存器:我们初始化各个外设,最终所读写的东西。存储器也是SRAM。

内核外设寄存器:内核外设就是NVIC和SysTick。内核外设和其它外设不是一个厂家设计的,所以它们的地址也是被分开了。

 在STM32中,所有的存储器都被安排到了0到8个F这个地址的范围,这个地址范围内,因为CPU是32的,所以寻址范围就是32位的范围。32位的寻址范围是非常大的,最大可以支持4GB容量的存储器,而STM32的存储器都是KB级别的,所以这个4GB的寻址空间,会有大量的地址都是空的。

 上图里灰色部分是Reserved区域,也就是保留区与,没有使用到。

7.1.3 DMA框图

左上角是Cortex-M3内核,里面包含了CPU和内核外设等等。剩下的所有东西,都可以把它看成是存储器。所以总共是CPU和存储器两个东西,Flash是主闪存,SRAM是运行内存。各个外设都可以看成是寄存器,也是一种SRAM存储器。寄存器是一种特殊的存储器,一方面,CPU可以对寄存器进行读写,就像读写运行内存一样;另一方面,寄存器的每一位背后,都连接了一根导线,这些导线可以用于控制外设电路的状态,比如置引脚的高低电平、导通和断开开关、切换数据选择器,或者多位组合起来,当作计数器、数据寄存器等等。所以计数器是连接硬件和软件的桥梁,软件读写寄存器,就相当于在控制硬件的执行。既然外设就是寄存器,寄存器就是存储器,那使用DMA进行数据转运,就都可以归为一类问题了,就是从某个地址取内容,再放到另一个地址去,为了高效有条理地访问存储器,设计了一个总线矩阵,总线矩阵地左端,是主动单元,也就是拥有存储器地访问权。右边是被动单元,它们的存储器只能被左边的主动单元读写。主动单元这里,内核有Dcode和系统总线,可以访问右边的存储器,其中Dcode总线是专门访问Flash的,系统总线是访问其它东西的。另外,由于DMA要转运数据,所以DMA也必须要有访问的主动权,那主动单元,除了内核CPU,剩下的就是DMA总线了。DMA1有一条DMA总线,DMA2也有一条DMA总线;下面还有一条DMA总线,这是以太网外设自己私有的DMA,所以这里可以不用管。在DMA1和DMA2里,可以看到DMA1有7个通道,DMA2有5个通道,各个通道可以分别设置它们转运数据的源地址和目的地址,这样它们就可以各自独立地工作了。

仲裁器材:这是因为,虽然多个通道可以独立运转数据,但是最终DMA总线只有一条,所以所有的通道都只能分时复用这一条DMA总线,如果产生了冲突,那就会由仲裁器、根据通道的优先级决定谁先用,谁后用。另外在总线矩阵这里,也有个仲裁器,如果DMA和CPU都要访问同一个目标,那么DMA就会暂停CPU的访问,以防止冲突,不过总线仲裁器,仍然会保证CPU得到一半的总线带宽,使CPU也能正常工作。这就是仲裁器的作用。

AHB从设备,也就是DMA自身的寄存器,因为DMA作为一个外设,它自己会有相应的配置寄存器,这里连接在了总线右边的AHB总线上,所以DMA即是总线矩阵的主动单元,可以读写各种存储器;也是AHB总线上的被动单元,就可以对DMA进行配置了。

DMA请求就是触发,触发源是各个外设,所以这个DMA请求就是DMA的硬件触发源,比如ADC转换完成、串口接收到数据,需要触发DMA转运数据的时候,就会通过中间的DMA请求这条线路,向DMA发出硬件触发信号,之后DMA就可以执行数据转运的工作了,这就是DMA请求的作用。

DMA总线:用于访问各个存储器。

DMA内部多个通道:可以独立的进行数据转运。

仲裁器:用于调动各个通道,防止产生冲突。

AHB从设备:用于配置DMA参数。

DMA请求,用于硬件触发DMA的数据转运。

Flash:ROM只读存储器的一种,如果通过总线直接访问的话,无论是CPU还是DMA,都是只读的,只能读取数据,而不能写入,如果DMA的目的地址,填了Flash的区域,那转运时,就会出错。Flash也不是绝对的不可写入,可以配置Flash控制器,对Flash进行写入,这个流程比较麻烦,要先对Flash按页进行擦除,再写入数据。总之就是CPU或者DMA直接访问Flash的话,是只可以读而不可以写的。

SRAM是运行内存,可以任意读写,没有问题。

外设寄存器可以看参考手册,有的寄存器是只读的,有的寄存器是只写的,不过我们用的主要是数据寄存器,数据寄存器都是可以正常读写的。

7.1.4 DMA基本结构

 外设寄存器、Flash/SRAM:数据转运的两大站点,左边是外设寄存器站点,右边是存储器站点,包括Flash和SRAM。在STM32手册里所说的存储器一般特指Flash和SRAM,不包含外设寄存器。外设寄存器,一般直接称作外设,所以就是外设到存储器,存储器到存储器,这样来描述。

DMA的数据转运可以是从外设到存储器,也可以是从存储器到外设,具体向左还是向右,有一个方向的参数可以进行控制。另外还有一种转运方式,就是存储器到存储器,比图Flash到SRAM或者SRAM到SRAM两种方式。由于Flash是只读的,所以DMA不可以进行SRAM到Flash,或者Flash到Flash的转运操作。

两边的参数:既然要进行数据转运,那肯定就要指定从哪里转到哪里,具体怎么转。所以外设和存储器两个站点,就都有3个参数。第一个是其实地址,由外设端的起始地址和存储器端的起始地址。这两个参数决定了数据是从哪里来,到哪里去的;第二参数是数据宽度,这个参数的作用是指定一次转运要按多大的数据宽度来进行,可以选择字节Byte、半字HalfWord和字Word;字节就是8位、也就是一次转运一个uint8_t这么大的数据;半字是16位,也就是一次转运一个uint16_t这么大的数据;字是32位,就是一次转运uint32_t这么大的数据;比如转运ADC的数据,ADC的结果是uint16_t这么大,所以这个参数就要选择半字,一次转运一个uint16_t。第3个参数是地址是否自增,这个参数的作用是指定一次转运完成后,下一次转运是不是要把地址移动到下一个位置去,这就相当于是指针,P++这个意思;比如ADC扫描模式,用DMA进行数据转运,外设地址是ADC_DR寄存器,寄存器这边,显然地址是不用自增的,如果自增,那下一次转运就跑到别的寄存器那里去了,存储器这边,地址就需要自增,每转运一个数据之后,就往后挪个坑,要不然下次再转就把上次的覆盖掉了,这就是地址是否自增的作用。就是指定是不是要转运一次挪个坑这个意思。这就是外设站点和存储器站点各自的3个参数了。

如果要进行存储器到存储器的数据转运,那我们就需要把其中一个存储器的地址,放在外设的这个站点,这样就能进行存储器到存储器的转运了,只要在外设起始地址里写Flash或者SRAM的地址,那它就会去Flash或SRAM找数据。这个站点虽然叫外设寄存器,但是它就只是个名字而已,并不是说这个地址只能写寄存器的地址,如果写Flash的地址,就回去Flash里去找,写SRAM,它就会去SRAM里找,这个没有限制,甚至可以在外设站点写存储器的地址,甚至可以在外设站点写存储器的地址,存储器站点写外设的地址,方向给反过来,这样也是可以的。

传输计数器,这个东西是用来指定我们共需要转运几次的,这个传输计数器是一个自减计数器,比如给它写一个5,那DMA就只能进行5次数据转运,转运过程中,每转运一次,计数器的数就会减1;当传输计数器减到0之后,DMA就不会再进行数据转运了,另外,它减到0之后,之前自增的地址,也会恢复到起始地址的位置,以方便之后DMA开始新一轮的转运。

在传输计数器的右边,有一个自动重装器,这个自动重装器的作用就是,传输计数器减到0之后,是否要自动恢复到最初的值。比如最初传输计数器给5,若果不使用自动重装器,那转运5次后,DMA就结束了;如果使用自动重装器,那转运5次,计数器减到0后,就会立即重装到初始值5。这个就是自动重装器,它决定了转运的模式,如果不重装,就是正常的单次模式,如果重装就是循环模式。比如如果想转运一个数组,那一般就是单次模式,转运一轮就结束了。如果是ADC扫描模式+连续转换,那为了配合ADC,DMA也需要使用循环模式,所以这个循环模式和ADC的连续模式差不多,都是指定一轮工作完成后,是不是立即开始下一轮工作。

最下面就是ADC的触发控制了,触发就是决定DMA需要在什么时机进行转运的。触发源有硬件触发和软件触发,具体选择哪个由M2M(Memory to Memory,因为two和to同音,所以M2M就是M to M,存储器到存储器的意思)这个参数决定。当我们给M2M位1时,DMA就会选择软件触发,这个软件触发并不是调用某个函数一次,触发一次,这个软件触发的逻辑是,以最快的速度,连续不断地触发DMA,争取早日把传输计数器清零,完成这一轮的转换。所以这里的软件触发和之前外部中断、ADC的软件触发不太一样,可以把它理解为连续触发。软件触发和循环模式不能同时使用,因为软件触发就是想把传输计数器清零,循环模式是清零后自动重装,如果同时用的话,DMA就停不下来了。软件触发一般适用于存储器到存储器的转运,因为存储器到存储器的转运,是软件启动、不需要时机,并且想尽快完成的任务。所以上面这里,M2M位给1,就是软件触发,就是应用在存储器到存储器的情况。当M2M位给0,就是使用硬件触发了,硬件触发可以选择ADC、串口、定时器等等。使用硬件触发的转运,一般都是与外设有关的转运,这些转运需要一定的时机,比如ADC转换完成、串口收到数据、定时时间到等等,所以需要硬件触发,在硬件达到这些时机时,传一个信号过来,来触发DMA进行转运,这就是硬件触发。

最后就是开关控制了,也就是DMA_cmd函数了,当给DMA使能后,DMA就准备就绪,可以进行转运了。DMA进行转运,有几个条件:

第1,就是开关控制,DMA_Cmd必须使能;

第2,传输计数器必须大于0;

第3,触发源,必须有触发信号,触发一次,转运一次,传输计数器自减一次,当传输计数器等于0,且没有自动重装时,这时无论是否触发,DMA都不会再进行转运了,此时就需要DMA_Cmd给DISABLE,关闭DMA,再为传输计数器写一个大于0的数,再DMA_CMd,给ENABLE,开启DMA,DMA才能继续工作;注意一下,写传输计数器时,必须要先关闭DMA,再进行,不能在DMA开启时,写传输计数器,这是手册里的规定。

7.1.5 DMA请求

下图就是DMA基本结构里DMA触发的部分。

上图是DMA1的请求映像, 下面是DMA的7个通道,每个通道都有一个数据选择器,可以选择硬件触发或软件触发。

EN位:EN并不是数据选择器的控制位,而且决定这个数据选择器要不要工作,EN=0,数据选择器不工作;EN=1,数据选择器工作。

软件触发(MEM2MEM位)的意思是:当M2M位等于1时,选择软件触发。

每个通道的硬件触发源都是不同的,如果需要ADC1来触发的话,那必须选择通道1;如果需要定时器2的更新事件来触发的话,那就必须选择同道2。如果使用软件触发,那每个通道都可以选择了,因为每个通道的软件触发都是一样的

通道1的硬件触发有:ADC1、定时器2的通道3和定时器4的通道1,那到底选择哪个定时器的触发源呢,这个是对应的外设是否开启了DMA输出决定的。比如要使用ADC1,那会有个库函数叫ADC_DMACmd,必须使用这个库函数开启ADC1的这一路输出,它才有效。如果想选择定时器2的通道3,那也会有个TIM_DMACmd函数,用来进行DMA输出控制。所以这三个触发源,具体使用哪一个,取决于把哪个外设的DMA输出开启了。如果3个都开启了,右边是一个或门,理论上3个硬件都可以进行触发,不过一般情况下,我们都是开启其中一个。

最后,7个触发源进入到仲裁器,进行优先级判断,最终产生内部的DMA1请求。这个优先级的判断,类似于中断的优先级,默认优先级是通道号越小,优先级越高,当然也可以在程序中配置优先级。

7.1.6 数据宽度与对齐

数据转运的两个站点,都有一个数据宽度的参数, 如果数据宽度都一样,那就是正常的一个个转运,如果数据宽度不一样呢?上表就是说明这个问题的。

以第二行为例:源宽8位,目标宽16位,那就在目标数据前面多出来的空位补0;

以第四行为例:源宽16位,目标宽8位,现象就是读B1B0,只写入B0,舍弃高位。

7.1.7 数据转运+DMA

任务:将SRAM里的数组DataA转运到另一个数组DataB中。

如何配置:

1.首先是外设站点和存储器站点的起始地址、数据宽度、地址是否自增这三个参数。

在这个任务里,外设地址显然应该填DataA数组的首地址,存储器地址给DataB的首地址。两个数组的数据宽度都是uint8_t,所以数据宽度都是按8位的字节传输。转运完DataA[0]数据之后,两个站点的数据都应该自增,都移动到下一个数据的位置。

2.方向参数

外设单元转运到存储器单元。

3.传输计数器和是否要自动重装

转运7次,所以计数器给7,自动重装暂时不需要。

4.触发选择

软件触发,因为这是存储器到存储器的数据转运。是不需要等待硬件时机的,尽快转运完就行了。5.最后,调用DMA_Cmd,给DMA使能。这样数据就从DataA转运到DataB了。转运7次之后,传输计数器自减到0,DMA停止,转运完成。

这里的转运是一种复制转运,转运完成后DataA的数据并不会消失。这个过程相当于是把DataA的数据复制到了DataB的位置。

7.1.8 ADC扫描模式+DMA 

左边是ADC扫描模式的执行流程,触发一次后,7个通道依次进行AD转换, 然后转换结果都放到了ADC_DR数据寄存器里。我们要做的就是,在每个单独的通道转换完成之后,进行一次DMA数据转运,并且目的地址进行自增,这样数据地址就不会覆盖了。

所以在这里DMA的配置就是:外设地址,写入ADC_DR这个寄存器的地址;存储器的地址,可以在SRAM中定义一个数据ADValue,然后把ADValue的地址当作存储器的地址,之后数据宽度,因为ADC_DR和SRAM数组,我们要的都是uint16_t的数据,所以数据宽度都是16位的半字传输。继续是地址是否自增,这个图里显然是外设寄存器地址不自增,存储器地址自增。传输方向是外设站点到存储器站点。传输计数器,这里通道有7个,所以计数7次。计数器是否自动重装,可以看ADC的配置,ADC如果是单次扫描,那DMA的传输计数器可以不自动重装,转换一轮就停止。如果ADC是连续扫描,那DMA就可以使用自动重装,在ADC启动下一轮转换的时候,DMA也启动下一轮的转运。ADC和DMA同步工作。最后是触发选择,这里ADC_DR的值是在ADC单个通道转换完成后才会有效,所以DMA转运完成的时机需要和ADC单个通道转换完成同步,所以DMA的触发选择要选择ADC的硬件触发。

硬件触发这里需要说明一下:ADC扫描模式,在每个单独的通道转换完成后,没有任何标志位,也不会触发中断,所以程序不太好判断,某一个通道转换完成的时机是什么时候。虽然单个通道转换完成后,不产生任何标志位和中断,但是它应该会产生DMA请求,去触发DMA转运。

一般来说,DMA最常见的用途就是配合ADC的扫描模式,因为ADC的扫描模式有个数据覆盖的特征,或者说这个数据覆盖的问题是ADC固有的缺陷,这个缺陷使ADC和DMA成为了最常见的伙伴。ADC对DMA的需求是非常强烈的,像其它的一些外设,使用DMA可以提高效率,是锦上添花的操作,不使用也是可以的,顶多是损失一些性能。但是这个ADC的扫描模式,如果不使用DMA,功能都会受到很大的限制。

7.1.8 数据手册

7.2  DMA数据转运

7.2.1 硬件电路

7.2.2 软件部分

(1)复制《OLED显示屏》工程改名为《DMA数据转运》

(2)测试一下下表

类型

起始地址

存储器

用途

ROM

0x0800 0000

程序存储器Flash

存储C语言编译后的程序代码

0x1FFF F000

系统存储器

存储BootLoader,用于串口下载

0x1FFF F800

选项字节

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

RAM

0x2000 0000

运行内存SRAM

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

0x4000 0000

外设寄存器

存储各个外设的配置参数

0xE000 0000

内核外设寄存器

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

①main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"                      // 调用延时头文件
#include "OLED.h"

uint8_t aa = 0x66;

int main(void)
{
	OLED_Init();                                 // 初始化OLED屏幕
	OLED_ShowHexNum(1,1,aa,2);
	OLED_ShowHexNum(2,1,(uint32_t)&aa,8);        
	// 变量取地址之后,如果当作数字显示,要进行强制类型转换,如果不进行转换,就是指针跨级赋值了
	while(1)
	{	
                     
	}
}

运行后aa这个变量存储在0x20开头的地址,说明存储在SRAM区,具体地址是多少是由编译器决定的,目前SRAM区没什么东西,所以编译器就把这个变量放在了SRAM区的第一个位置。

 ②main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"                      // 调用延时头文件
#include "OLED.h"

const uint8_t aa = 0x66;

int main(void)
{
	OLED_Init();                                 // 初始化OLED屏幕
	OLED_ShowHexNum(1,1,aa,2);
	OLED_ShowHexNum(2,1,(uint32_t)&aa,8);        
	// 变量取地址之后,如果当作数字显示,要进行强制类型转换,如果不进行转换,就是指针跨级赋值了
	while(1)
	{	
                     
	}
}

aa变量前加上const关键字后,表示aa是一个常量, 被const修饰的变量在程序中只能读,不能写,Flash也是只能读不能写的,所以const就和flash联系起来了。在STM32中,使用const定义的变量,是存储在Flash里面的,当然这里就不应该说是变量了,而应该说是常量。运行后地址是0x08开头的,说明常量aa被存储在Flash里面了。

 ③main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"                      // 调用延时头文件
#include "OLED.h"

const uint16_t aa = 0x66;
int main(void)
{
	OLED_Init();                                 // 初始化OLED屏幕
	OLED_ShowHexNum(1,1,aa,2);
	OLED_ShowHexNum(2,1,(uint32_t)&ADC1->DR,8);        
	// 变量取地址之后,如果当作数字显示,要进行强制类型转换,如果不进行转换,就是指针跨级赋值了
	while(1)
	{	
                     
	}
}

对于变量或者常量来说,它的地址是由编译器决定的,对于外设寄存器来说,它的地址是固定的,在手册里能查到,在程序里可以使用结构体很方便地访问寄存器。比如要访问ADC1的DR寄存器,就可以写ADC1->DR,这样就可以访问ADC1的DR寄存器了。

(3)添加驱动文件

 (4)初始化DMA

void DMA_DeInit(DMA_Channel_TypeDef* DMAy_Channelx);        //恢复缺省配置
void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct); //初始化
void DMA_StructInit(DMA_InitTypeDef* DMA_InitStruct);    //结构体初始化
void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState);    //使能
void DMA_ITConfig(DMA_Channel_TypeDef* DMAy_Channelx, uint32_t DMA_IT, FunctionalState NewState);  //中断输出使能
void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber);   //设置当前数据寄存器,这个函数就是给传输计数器写数据的
uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx);   //DMA获取当前数据寄存器,返回传输计数器的值
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);      //清除中断挂起位

(5)MyDMA.c

 

#include "stm32f10x.h"                  // Device header

uint16_t MyDMA_Size;  //用于接收和存储传输次数的值,方便后续给传输计数器重新赋值
/*DMA初始化函数:
第1步:RCC开启DMA的时钟
第2步:直接调用DMA_Init,初始化里面的各个参数
*/
void MyDMA_Init(uint32_t AddrA,uint32_t AddrB,uint16_t Size)     //输入参数源地址、目的地址和传输次数
{
	MyDMA_Size = Size;
	/*第1步:RCC开启DMA的时钟*/
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	DMA_InitTypeDef DMA_InitStruct;
	DMA_InitStruct.DMA_PeripheralBaseAddr = AddrA;         //外设站点起始地址
	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;           //外设站点数据宽度
	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Enable;                   //外设站点是否自增,不自增
	DMA_InitStruct.DMA_MemoryBaseAddr = AddrB;                  //存储器站点起始地址
	DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;                  //存储器站点数据宽度 
	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;                       //存储器站点是否自增
	DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;                             //传输方向:外设站点到存储器
	DMA_InitStruct.DMA_BufferSize = Size;                      //缓存区大小,其实就是传输计数器
	DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;             //传输模式,是否使用自动重装:正常模式,不自动重装,转运一次就停止
	DMA_InitStruct.DMA_M2M = DMA_M2M_Enable;               //选择是否是存储器到存储器,就是选择软件触发还是硬件触发:使用软件触发
	DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;                        //优先级,按照参数要求给一个优先级即可:中等优先级
	DMA_Init(DMA1_Channel1,&DMA_InitStruct);
	//初始化DMA1的通道1,因为用的是软件触发,所以通道可以任意选择
	DMA_Cmd(DMA1_Channel1,DISABLE);   
	//不让DMA初始化之后立刻进行转运,而是执行了MyDMA_Transfer函数之后再进行转运
}

/*给传输计数器重新赋值函数*/
void MyDMA_Transfer(void)
{
	DMA_Cmd(DMA1_Channel1,DISABLE);   //失能DMA
	DMA_SetCurrDataCounter(DMA1_Channel1,MyDMA_Size);
	DMA_Cmd(DMA1_Channel1,ENABLE);   //重新使能DMA
/*
  *     @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.     //转运错误标志位
*/
	while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);                //等待转运完成
	DMA_ClearFlag(DMA1_FLAG_TC1);                                   //需要手动清除标志位
}

(6)MyDMA.h

#ifndef __MYDMA_H
#define __MYDMA_H
void MyDMA_Init(uint32_t AddrA,uint32_t AddrB,uint16_t Size);
void MyDMA_Transfer(void);
#endif

(7)main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"                      // 调用延时头文件
#include "OLED.h"
#include "MyDMA.h"

uint8_t DataA[] = {0x01,0x02,0x03,0x04};   //要转运的源数组
//如果想把Flash的数据转运到SRAM里,前面加一个SRAM,但后面就不允许修改这个数了
uint8_t DataB[] = {0,0,0,0};   //目的数组:不写0的话,全局变量也是0


int main(void)
{
	OLED_Init();                                 // 初始化OLED屏幕
	MyDMA_Init((uint32_t)DataA,(uint32_t)DataB,4);
	OLED_ShowString(1,1,"DataA");
	OLED_ShowString(3,1,"DataB");
	OLED_ShowHexNum(1,8,(uint32_t)DataA,8);
	OLED_ShowHexNum(3,8,(uint32_t)DataB,8);
	
	OLED_ShowHexNum(2,1,DataA[0],2);
	OLED_ShowHexNum(2,4,DataA[1],2);
	OLED_ShowHexNum(2,7,DataA[2],2);
	OLED_ShowHexNum(2,10,DataA[3],2);
	OLED_ShowHexNum(4,1,DataB[0],2);
	OLED_ShowHexNum(4,4,DataB[1],2);
	OLED_ShowHexNum(4,7,DataB[2],2);
	OLED_ShowHexNum(4,10,DataB[3],2);

//	OLED_ShowHexNum(3,1,DataA[0],2);
//	OLED_ShowHexNum(3,4,DataA[1],2);
//	OLED_ShowHexNum(3,7,DataA[2],2);
//	OLED_ShowHexNum(3,10,DataA[3],2);
//	OLED_ShowHexNum(4,1,DataB[0],2);
//	OLED_ShowHexNum(4,4,DataB[1],2);
//	OLED_ShowHexNum(4,7,DataB[2],2);
//	OLED_ShowHexNum(4,10,DataB[3],2);
	while(1)
	{	
		DataA[0]++;
		DataA[1]++;
		DataA[2]++;
		DataA[3]++;
		/*转运前*/
		OLED_ShowHexNum(2,1,DataA[0],2);
		OLED_ShowHexNum(2,4,DataA[1],2);
		OLED_ShowHexNum(2,7,DataA[2],2);
		OLED_ShowHexNum(2,10,DataA[3],2);
		OLED_ShowHexNum(4,1,DataB[0],2);
		OLED_ShowHexNum(4,4,DataB[1],2);
		OLED_ShowHexNum(4,7,DataB[2],2);
		OLED_ShowHexNum(4,10,DataB[3],2);
		Delay_ms(1000);
		MyDMA_Transfer();        //开始转运
		/*转运后*/
		OLED_ShowHexNum(2,1,DataA[0],2);
		OLED_ShowHexNum(2,4,DataA[1],2);
		OLED_ShowHexNum(2,7,DataA[2],2);
		OLED_ShowHexNum(2,10,DataA[3],2);
		OLED_ShowHexNum(4,1,DataB[0],2);
		OLED_ShowHexNum(4,4,DataB[1],2);
		OLED_ShowHexNum(4,7,DataB[2],2);
		OLED_ShowHexNum(4,10,DataB[3],2);
		Delay_ms(1000);
	}
}

7.3  DAM+AD多通道

7.3.1 硬件电路

7.3.2 软件部分 

(1)复制《AD多通道》工程并改名为《DMA+AD多通道》

(2)AD.c

#include "stm32f10x.h"                  // Device header

uint16_t AD_Value[4];
/*初始化函数:
第1步:开启RCC时钟,包括ADC和GPIO的时钟,ADCCLK的分频器,也需要配置一下;
第2步:配置GPIO,把需要用的GPIO配置成模拟输入的模式;
第3步:配置多路开关,把最左边的通道接入右边的规则组列表里,使用多个通道,再触发转换函数里实现;
第4步:配置ADC转换器,库函数里使用结构体配置,包括ADC是单次转换还是连续转换、
扫描还是非扫描、有几个通道、触发源是什么、数据对齐是左对齐还是右对齐

如果需要模拟看门狗,会有几个函数用来配置阈值和监测通道的;如果想开启中断,那就在中断输出控制
里用ITConfig函数开启对应的中断输出,然后再在NVIC里,配置中断优先级,这样就能触发中断了。
第5步:DMA初始化
第6步:开关控制,调用一下ADC_Cmd函数,开启ADC,这样ADC就配置完成了。开启ADC之后,
还可以对ADC进行校准、这样可以减小误差。
在ADC工作的时候,如果想要触发转换,会有函数可以触发;如果想读取结果,也会有函数可以读取结果。
*/
void AD_Init(void)
{
	/*第1步:开启RCC时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);           //开启ADC1的时钟控制
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);          //要使用PA0口采样
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);                             //配置APB2六分频,分频后ADCCLK=72MHz/6=12MHz
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);             //开启DMA时钟
	/*第2步:配置GPIO*/
	GPIO_InitTypeDef GPIO_InitStruct;
	GPIO_InitStruct.GPIO_Mode = GPIO_Mode_AIN;
	/*配置GPIO口是模拟输入模式,AIN模式下,GPIO是无效的,断开GPIO口,防止GPIO口的输入输出对模拟电压造成干扰;
	所以AIN模式是ADC的专属模式*/
	GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3;
	GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitStruct);
	GPIO_SetBits(GPIOA,GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3);     
	/*ADC_Channel_0:通道0,1:规则组序列里的次序,ADC_SampleTime_55Cycles5:采样时间55.5个ADCCLK的周期*/
	/*第4步:配置ADC转换器*/
	ADC_InitTypeDef ADC_InitStruct;
	ADC_InitStruct.ADC_Mode = ADC_Mode_Independent; //配置ADC工作在独立模式
	ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;  //配置ADC数据对齐为右对齐
	ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //配置不使用外部触发,使用内部软件触发
	ADC_InitStruct.ADC_ContinuousConvMode = ENABLE;     //配置转换模式为循环转换
	ADC_InitStruct.ADC_ScanConvMode = ENABLE; //配置扫描模式为扫描模式
	ADC_InitStruct.ADC_NbrOfChannel = 4;    //目前是4个通道扫描模式,整个列表只有第一个序列有用
	ADC_Init(ADC1,&ADC_InitStruct);
	
	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);  // 以变量形式传递通道
	
	/*第5步:DMA初始化*/
	DMA_InitTypeDef DMA_InitStruct;
	DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;                   //外设站点起始地址,ADC_DR的地址
	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;       //外设站点数据宽度,我们想要DR寄存器低16位的数据
	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;                   //外设站点是否自增,不自增
	DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)&AD_Value;                  //存储器站点起始地址
	DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;                  //存储器站点数据宽度 
	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;                       //存储器站点是否自增
	DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;                             //传输方向:外设站点到存储器
	DMA_InitStruct.DMA_BufferSize = 4;                      //缓存区大小,其实就是传输计数器
	DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;             //模式,是否使用自动重装:循环模式
	DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;               //选择是否是存储器到存储器,就是选择软件触发还是硬件触发:不使用软件触发,需要硬件触发
	DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;                        //优先级,按照参数要求给一个优先级即可:中等优先级
	DMA_Init(DMA1_Channel1,&DMA_InitStruct);
	//初始化DMA1的通道1,因为用的是软件触发,这里不能任意选择ADC1的硬件触发只接到了DMA1的通道1上
	DMA_Cmd(DMA1_Channel1,ENABLE);  
	ADC_DMACmd(ADC1,ENABLE); //开启ADC到DMA的输出
	/*第6步:开关控制,开启ADC的电源*/	
	//不让DMA初始化之后立刻进行转运,而是执行了MyDMA_Transfer函数之后再进行转运
	ADC_Cmd(ADC1,ENABLE);
	ADC_ResetCalibration(ADC1);           //复位校准
	ADC_GetResetCalibrationStatus(ADC1);  //返回复位校准的状态
	while(ADC_GetResetCalibrationStatus(ADC1) == SET); //读取这一位,如果它是1,那就需要一直空循环等待;如果它变为0,那就说明复位校准完成,可以跳出等待了	
	ADC_StartCalibration(ADC1);           //启动校准,之后内部电路会自动校准
	ADC_GetCalibrationStatus(ADC1);       //获取校准状态
	while(ADC_GetCalibrationStatus(ADC1) == SET); //判断校准状态是否完成
	
	ADC_SoftwareStartConvCmd(ADC1,ENABLE);     //软件触发转换,启动,使用连续转换的时候放到初始化函数里就可以了,这样后面的AD_GetValue就不需要了
}
	

//void AD_GetValue(void)
//{
//	DMA_Cmd(DMA1_Channel1,DISABLE);   //失能DMA
//	DMA_SetCurrDataCounter(DMA1_Channel1,4);
//	DMA_Cmd(DMA1_Channel1,ENABLE);   //重新使能DMA
//	//因为现在是单次模式,所以还需要软件触发一下ADC开始
//	
//	while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);                //等待转运完成
//	DMA_ClearFlag(DMA1_FLAG_TC1);                                   //需要手动清除标志位

//}

(3)AD.h

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

(4)main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"                      // 调用延时头文件
#include "OLED.h"
#include "AD.h" 

int main(void)
{
	OLED_Init();                                 // 初始化OLED屏幕
	AD_Init();
	OLED_ShowString(1,1,"AD0:");
	OLED_ShowString(2,1,"AD1:");
	OLED_ShowString(3,1,"AD2:");
	OLED_ShowString(4,1,"AD3:");
	while(1)
	{	
//		AD_GetValue();
		
		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);
	}
}

 

  • 12
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值