2.22 DMA直接存储器存储

DMA简介

DMA是一个数据转运小助手,它主要是用来协助CPU来完成数据转运的工作。

DMA(Direct Memory Access)直接存储器存取(直接存储器访问)。从DMA的名字来看,这个DMA是可以直接访问STM32内部的存储器的,包括运行内存SRAM、程序存储器Flash和寄存器等等,DMA都有权限访问他们,所以DMA才能完成数据转运的工作。

DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源。外设就是指外设的寄存器,一般是外设的数据寄存器DR(Data Register),比如ADC的数据寄存器,串口的数据寄存器等等。存储器指的就是运行内存SRAM和程序存储器Flash,是我们存储变量数组和程序代码的地方。

STM32的DMA有12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道),这个通道就是数据转运的路径,从一个地方移动到另一个地方,就需要占用一个通道,如果有多个通道进行转运,那他们之间可以各转各的,互不干扰。

每个通道都支持软件触发和特定的硬件触发,这里如果DMA进行的是存储器到存储器的数据转运,比如我们想把Flash里的一批数据转运到SRAM中,那就需要软件触发了,使用软件触发之后,DMA就会一股脑的把这批数据以最快的速度全部转运完成。如果DMA进行的是外设到存储器的数据转运,就不能一股脑的转运了,因为外设的数据是有一定时机的,所以这时候我们就需要用硬件触发,比如转运ADC的数据,那就得ADC每个通道AD转换完成后,硬件触发一次DMA,之后DMA再转运,触发一次转运一次。所以存储器到存储器的数据转运,我们一般使用软件触发,外设到存储器的数据转运,我们一般使用硬件触发。

为什么是特定的硬件出发呢?

因为每个DMA的通道,它的硬件触发源是不一样的,你要使用某个外设的硬件触发源,就得使用它连接的那个通道,而不能任意选择通道。

STM32F103C8T6的DMA资源是DMA1(7个通道)。我们这个芯片只有DMA1的7个通道,没有DMA2。

STM32的存储器映像

既然DMA是在存储器之间进行数据转运的,那我们就应该了解一下STM32中都有哪些存储器,这些存储器又是被安排到了哪些地址上,这就是存储器映像的内容。

那我们知道,计算机系统的5大组成部分是运算器、控制器、存储器、输入设备和输出设备。其中运算器和控制器一般会合在一起,叫做CPU,所以计算机的核心关键部分就是CPU和存储器,存储器又有两个重要知识点,一个是存储器的内容,另一个就是存储器的地址,那STM32也不例外,上的表就是STM32中所有类型的存储器和它们所被安排的地址。

在这个表里,无论是Flash、还是SRAM,还是外设寄存器,它们都是存储器的一种,包括外设寄存器,实际上也是存储器。我们前面所说的外设到存储器,存储器到存储器,本质上实际上都是存储器之间的数据转运。至于说成外设的存储器,只不过是STM32它特别指定了可以转运的外设的存储器而已。

存储器共分成两大类,ROM和RAM:

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

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

ROM又分为三块:

第一块是程序存储器Flash,也就是主闪存,它的用途就是存储C语言编译后的程序代码,也就是我们下载程序的位置。运行程序一般也是从主闪存里面开始运行的。这一块存储器,STM32给他分配的地址的是0X08000000。起始地址,也就是第一个字节的地址是0800,然后剩余字节的地址依次增长,每个字节都分配一个独一无二的地址,就想给每个住户编门牌号一样,只有分配了独一无二的门牌号,程序才能精准的访问这个存储器。最终终止地址是多少呢?这取决于它的容量,编到哪里,哪里就是终止地址,这就是主闪存地址的范围。你之后如果在软件里看到,某个数据的地址开头是0800开头的,那你就可以确定,他是属于主闪存的数据。

剩下两块是系统存储器和选项字节,这两块存储器也是ROM的一种,掉电不丢失。实际上他们的存储介质也是Flash,只不过我们一般说Flash指的是主闪存Flash,而不是指这两块区域。系统存储器里是BootLoader程序,BootLoader程序是芯片出厂自动写入的,一般也不允许我们修改。选项字节这个存储器是用于存储一些独立于程序代码的参数配置,他的位置在ROM区的最后面,你下载程序可以不刷新选项字节的内容,这样选项字节的配置就可以保持不变,选项字节里存的主要是Flash的读保护、写保护,还有看门狗等配置。

RAM也分为三块:

第一块是运行内存SRAM,分配的地址是0X2000000,用于存储运行中的临时变量,也就是我们在程序中定义的变量、数组、结构体的地方。

第二块是外设寄存器,分配的地址是0X4000000,用途是配置各个外设的配置参数,也就是我们初始化各个外设,最终所读写的东西,刚才我们说了,外设寄存器也是存储器的一种,它的存储介质其实也是SRAM,只不过我们一般习惯把运行内存叫SRAM,外设寄存器直接叫寄存器了。

第三块是内核外设寄存器,分配的地址是0XE000000,用途是存储内核各个外设的配置参数,内核外设就是NVIC和Systick,因为内核外设和其他外设不是一个厂家设计的,所以他们的地址也是被分开了。

DMA框图

左上角是Cortex-M3内核,里面包含了CPU和内核外设等,剩下的所有东西,你都可以把他看成是存储器,所以总共就是CPU和存储器两种东西。Flash是主闪存,SRAM是运行内存。各个外设都可以看成是寄存器,也是一种SRAM存储器,寄存器是一种特殊的存储器,一方面,CPU可以对寄存器进行读写,就像读写运行内存一样;另一方面,寄存器的每一位背后,都连接了一根导线,这些导线可以用于控制外设电路的状态,比如置引脚的高低电平、导通和断开开关、切换数据选择器等等,所以寄存器是连接软件和硬件的桥梁,软件读写寄存器,就相当于在控制硬件的执行。既然外设就是寄存器,寄存器就是存储器,那使用DMA进行数据转运,就都可以归类为一类问题了,就是从某个地址取内容,再放到另一个地址去。

为了高效有条理的访问存储器,这里设计了一个总线矩阵,,总线矩阵的左端,是主动单元,也就是拥有存储器的访问权;总线矩阵的右,是被动单元,它们存储器只能被左边的主动单元读写。

主动单元这里,内核有DCode和系统总线,可以访问右边的存储器;其中Dcode总线是专门访问Flash的,系统总线是访问其他东西的。另外,由于DMA要转运数据,所以DMA也必须要有访问的主动权。那主动单元除了内核CPU,剩下的就是DMA总线了(DMA1有一条系统总线,DMA2也有一条系统总线)。最下面时以太网外设自己私有的DMA,这个可以不用管的。

在DMA1和DMA2里面,可以看到,DMA1有7个通道,DMA2有5个通道,各个通道可以分别设置他们转运数据的源地址和目的地址,这样他们就可以各自独立的工作了。接着下面有个仲裁器,这个是因为,虽然多个通道可以独立转运数据,但是最终DMA总线只有一条,所以所有的通道都只能分时复用这一条DMA总线,如果产生冲突,那就会由仲裁器根据通道的优先级决定谁先用谁后用。

另外,在总线矩阵里,也会有仲裁器,如果DMA和CPU都要访问同一个目标,那么DMA就会暂停CPU的访问,以防止冲突。不过总线仲裁器仍然会保证CPU得到一半的总线带宽,使CPU也能正常工作。

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

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

总结一下:

用于访问各个存储器的DMA总线。DMA的内部多个通道可以进行独立的数据转运。仲裁器,用于调度各通道,防止产生冲突。AHB从设备,用于配置DMA参数。DMA请求,用于硬件触发DMA的数据转运。

最后,额外说一下,Flash,它是ROM只读存储器的一种,如果通过总线直接访问的他的话,无论是CPU还是DMA,都是只读的,只能读取数据,而不能写入,如果你DMA的目的地址,填写了Flash的区域,那么转运时,就会出错。当然Flash也不是绝对的不可写入,我们可以配置Flash接口控制器,对Flash进行写入。这个流程就比较麻烦了,要先对Flash按页进行擦除,再写入数据。总之,CPU和DMA直接访问Flash的话,是只可以读而不可以写的,然后SRAM是运行内存,可以任意读写,没有问题。

DMA的基本结构

如果想编写代码实际去控制DMA的话,需要看一下DMA的内部执行细节,上面的图就把这些执行细节画出来了。

这两部分就是数据转运的两大站点了:。在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这么大,那就选择半字的数据宽度进行转运。第三个是地址是否自增,这个参数的作用是,指定一次参数转运完成后,是不是要把地址移动到下一个位置去,这就相当于指针p++的意思。比如ADC扫描模式,用DMA进行数据转运,外设地址是ADC_DR寄存器,寄存器这边显然地址是不用自增的,如果自增,那一次转运就跑到别的寄存器里去了,存储器这边,地址就需要自增,每转运一次数据,就往后挪一个坑,要不然下次再转就把上次的覆盖掉了。另外,如果要进行存储器到存储器的数据转运,那我们就需要把其中一个存储器的地址,放在外设的这个站点,这样就能存储器到存储器的转运了,只要你在外设起始地址里写Flash或者SRAM 的地址,那他就会去Flash或者SRAM找数据,这个站点虽然叫外设寄存器,但他就只是个名字而已,并不是说这个地址只能写寄存器的地址。甚至你可以在外设站点写存储器地址,在存储器站点写外设地址,这都是没问题的,只是ST公司写的名字而已。

 

接着我们看传输计数器,这个东西就是用来指定我总共需要转运几次的,这个传输计数器是一个自减计数器,比如你写一个5,那么DMA就只能进行5次数据转运,转运过程中,每转运一次,计数器的值就会减1。另外,在传输计数器的值为0后,之前自增的地址,也会恢复到起始的位置,以方便DMA之后开始新一轮的转运。在传输计数器的右边是一个自动重装器,这个自动重装器的作用就是传输计数器减到0之后,是否要恢复到最初的值,它决定了转运的模式,如果不重装,那就是正常的单次模式,如果重装,那就是循环模式。比如你想要转运一个数组,那就是单次模式;如果是ADC扫描+连续转换,那为了配合ADC,DMA也需要使用循环模式,所以这个循环模式和ADC的连续模式也差不多,都是指定一轮工作完成后,是不是要立即开始下一轮工作。

 

接着看下面,是DMA的触发控制,触发,就是DMA需要在什么时机进行转运的。触发源有硬件触发和软件触发,具体选择哪个?由M2M这个参数决定。M2M就是Memory to Memory,当我们给M2M位置1时,DMA就会选择软件触发,反之会进行硬件触发。这个软件触发并不是调用某个函数一次,触发一次,他这个软件触发的执行逻辑是,以最快的速度,连续不断地触发DMA,争取早日把传输计数器清零,完成这一轮的转换。所以这里的软件触发,和我们之前外部中断和ADC的软件触发不太一样,你可以把DMA的软件触发理解成连续触发,那这个软件触发和循环模式(就是自动重装器恢复初始值),不能同时使用,因为软件触发就是想把传输计数器清0,循环模式是清0后自动重装,如果同时用的话,那么DMA就无法停下了。M2M给0,那就是硬件触发,硬件触发源可以选择ADC、串口、定时器等等,使用硬件触发的转运,一般都是与外设有关的转运,这些转运需要一定的时机,比如ADC转换完成,串口收到数据、定时器时间到等等,所以需要使用硬件触发,在硬件达到这些时机时,传一个信号过来,来触发DMA进行转运。

 

最后,是开关控制,也就是DMA_Cmd函数,当你DMA使能后,DMA就准备就绪,可以进行转运了。

DMA进行转运,有几个条件:

1.开关控制必须使能。

2.传输计数器必须大于0。(有自动重装时,等于零会直接恢复至初始值)

3.触发源必须有触发信号。

如果没有上述三个条件,无论是否触发,DMA都不会再进行转运了。此时就需要DMA_Cmd给DISABLE,关闭DMA。再为传输计数器写入一个大于0的数,再DMA_Cmd,给ENABLE,开启DMA。DMA才能继续工作,注意一下,写传输计数器时,必须要先关闭DMA再进行,不能在DMA开启时,写传输计数器,这是手册里的规定。

细节的问题

DMA请求

DMA1的请求映像:

DMA1有7个通道,每个通道都有1个数据选择器,可以选择硬件触发还是软件触发,把EN位放在了数据选择器的侧边,一般数据选择器的侧边是输入选择控制位,难道这里的意思是,EN给1,选择硬件触发,EN给0,选择软件触发吗?那显然不对,而且它左边这里写的是软件触发(MEM2MEM位),难道M2M位是软件触发码?这个也不太好理解,所以这个图需要给他重新布局一下:

DMA1的请求映像就是这张图的DMA触发部分的内容。M2M位是数据选择器的控制位,用于选择硬件触发还是软件触发。EN位是开关控制。EN=0时数据选择器不工作,EN=1时数据选择器工作。

DMA1请求映像这样画的意思是应该是,EN并不是数据选择器的控制位,而是决定这个数据选择器要不要工作。EN=0时数据选择器不工作,EN=1时数据选择器工作。然后软件触发后面跟个M2M位的意思应该是,当M2M位=1时,选择软件触发。

,这里是外设请求信号,可以看到,每个外设的硬件触发源都是不同的,如果你需要用ADC1来触发的话,那就必须选择通道1。如果需要定时器2的更新事件(TIM2_UP)来触发的的话,那就必须要选择通道2。这里通道1的硬件触发是ADC1、定时器2的通道3和定时器4的通道1,到底是选择哪个触发源呢?这个是对应的外设是否开启了DMA输出来决定的,比如你要使用ADC1,那会有个库函数ADC_DMACmd(),必须使用这个库函数开启ADC1的这一路输出,它才有效。如果想选择定时器2的通道3,那也会有个TIM_DMACmd函数,用来进行DMA输出控制,所以这三个触发源,具体使用哪个,取决于你把哪个外设的DMA输出开启了,如果3个都开启了,那这边是一个或门,理论上3个硬件都可以进行触发,不过一般情况下我们都是开启其中一个。这7个通道的触发源进入到仲裁器,来进行优先级判断,最终产生内部的DMA1请求,这个优先级判断,类似于中断的优先级,默认优先级是通道号越小,优先级越高,当然也可以在程序中配置优先级,这个影响其实并不是很大,大家了解一下就行了。

数据宽度与对其

这个表是用来说明什么问题的呢?我们通过前面学习知道,两数据转运的三个参数分别是起始地址、数据宽度、地址是否自增;那么数据宽度都一样,那就是正常的一个转运,那要是数据宽度不一样,怎么办?这个表就是来说明这个问题的。看图可知,你把小的数据转移到大的数据中去,高位就会补0,你把大的数据转移到小的数据中去,高位就会被舍弃。

ADC扫描模式+DMA

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

外设地址:ADC_DR寄存器地址;存储器地址:在SRAM中定义一个叫ADValue的数组的地址。

存储器宽度:都写uint16_t。

地址书是否自增:外设地址不自增,存储器地址自增。

传输方向:外设站点到存储器站点。

传输计数器,这里通道有7个,所以计数7次。

计数器是否自动重装,这里可以看ADC的配置,ADC如果是单词扫描,那么DMA的自动重装器可以不重装,转换一轮就自动停止。

如果ADC是连续扫描,那么DMA就可以使用自动重装,在ADC启动下一轮转换的时候,DMA也启动下一轮的转运。ADC和DMA同步工作。

最后是触发选择,这里的ADC_DR的值是在单个通道转换完成后才会有效,所以DMA转运的时机,需要和ADC单个通道转换完成同步,所以DMA触发要选择ADC的硬件触发,硬件触发这里要说明一下,ADC在扫描模式单独通道完成后,没有任何标志位,也不会触发中断,所以我们程序不太好判断,某一个通道转换完成的时机是什么时候,但是根据我的研究,虽然单个通道转换完成之后,不产生任何标志位和中断,但是他应该会产生DMA请求,去触发DMA转运,这部分内容,手册中并没有详细描述,根据实际实验,单个通道的DMA请求肯定是有的,要不然这个实例就无法完成了,这些就是ADC扫描模式和DMA配合使用额流程。