【自学嵌入式:stm32单片机】DMA直接存储器存取

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

DMA简介

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

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

  • 每个通道都支持软件触发(比如把Flash里的一批数据,转运到SRAM)和特定的硬件触发(处理需要有一定时机才能传输的数据,比如转运ADC数据,那就等ADC每个通道转运完成后,硬件触发一次DMA,之后DMA再转运,触发一次,转运一次)

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

【注】存储器到存储器的数据转运一般使用软件触发,外设到存储器的数据转运,我们一般使用硬件触发,每个硬件触发它的触发源是不一样的,若使用某个外设的硬件触发源,就得使用它连接的那个通道,而不能任意选择通道。

存储器映像

类型 起始地址 存储器 用途
ROM 0x0800 0000 程序存储器Flash 存储C语言编译后的程序代码
ROM 0x1FFF F000 系统存储器 存储BootLoader(出场自动写入,一般不能修改),用于串口下载
ROM 0x1FFF F800 选项字节 存储一些独立于程序代码的配置参数
RAM 0x2000 0000 运行内存SRAM 存储运行过程中的临时变量
RAM 0x4000 0000 外设寄存器 存储各个外设的配置参数
RAM 0xE000 0000 内核外设寄存器 存储内核各个外设的配置参数

image

DMA框图

image

寄存器

左上角这里是Cortex-M3内核,里面包含了CPU和内核外设等等,剩下的这所有东西,你都可以把它看成是存储器,所以总共就是CPU和存储器两个东西,Flash是主闪存,SRAM是运行内存,各个外设,都可以看成是寄存器,也是一种SRAM存储器,寄存器是一种特殊的存储器。
image

如上图,一方面,CPU可以对寄存器进行读写,就像读写运行内存一样,另一方面,寄存器的每一位背后,都连接了一根导线,这些导线可以用于控制外设电路的状态,比如置引脚的高低电平、导通和断开开关、切换数据选择器,或者多位组合起来,当做计数器、数据寄存器等。所以,寄存器是连接软件和硬件的桥梁,软件读写寄存器,就相当于在控制硬件的执行。
image

既然外设就是寄存器。寄存器就是存储器,那使用DMA进行数据转运,就都可以旧为一类问题了,就是从某个地址取内容,再放到另一个地址去。外设寄存器是否能读写要看参考手册里的描述,有的寄存器是只读的,有的寄存器是只写的,不过我们主要用的是数据寄存器。数据寄存器都是可以正常读写的。

总线矩阵

image

如上图,为了高效有条理地访问存储器,这里设计了一个总线矩阵,总线矩阵的左端,是主动单元,也就是拥有存储器的访问权,右边这些,是被动单元。它们的存储器只能被左边的主动单元读写,主动单元这里,内核有DCode和系统总线,可以访问右边的存储器,其中DCode总线是专门访问Flash的,系统总线是访问其他东西的,另外,由于DMA要转运数据,所以DMA也必须要有访问的主动权,那主动单元,除了内核CPU,剩下的就是DMA总线了

DMA总线

image
如上图蓝色的线,DMA1,DMA2和下面各有一条DMA总线(3条总线),下面的DMA总线是以太网外设自己私有的DMA,在DMA1和DMA2里面,可以看到,DMA1有7个通道,DMA2有5个通道,各个通道可以分别设置它们转运数据的源地址和目的地址,这样它们就可以各自独立地工作了。

仲裁器

image

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

AHB从设备

image
如上图蓝色圈的是AHB从设备,也就是DMA自身的寄存器,因为DMA作为一个外设,它自己也会有相应的配置寄存器。
image

如上图,这里连接在了总线右边的AHB总线上,所以DMA,即是总线矩阵的主动单元,可以读写各种存储器,也是AHB总线上的被动单元。
image

如上图,CPU通过这一条线路,就可以对DMA进行配置了。

DMA请求

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

Flash

image
如上图,框图中的Flash是ROM只读存储器的一种,如果通过总线直接访问的话,无论是CPU,还是DMA,都是只读的,只能读取数据,而不能写入,如果你DMA的日的地址,填了Flash的区域,那转运时,就会出错
image

如上图,当然Flash也不是绝对的不可写入,我们可以配置这个Flash接口控制器,对Flash进行写入,这个流程就比较麻烦了,要先对Flash按页进行擦除,再写入数据

DMA基本结构

数据转运的两大站点

image
在这个图里,外设寄存器和存储器(包括Flash和SRAM)这两部分就是数据转运的两大站点了,在STM32手册里,他所说的存诸器,一般是特指Flash和SRAM,不包含外设寄存器,外设寄存器,他一般直接称作外设,所以就是外设到存储器,存储器到存储器。这样来描述。虽然我们刚才说了,寄存器也是存储器的一种,但是STM32还是使用了外设和存储器来作为区分。
image

如上图,那在这里可以看到,DMA的数据转运,可以是从外设到存储器,也可以从存储器到外设,具体是向左还是向右,有一个方向的参数,可以进行控制。
image

如上图,还有一种转运方式就是存储器到存储器,比如Flash到SRAM或者SRAM到SRAM,这两种方式,由于Flash是只读的,所以DMA不可以进行SRAM到Flash,或者Flash到Flash的转运操作。
image

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

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

传输计数器

image

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

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

DMA的触发控制

image
如上图,这就是DMA的触发控制,触发,就是决定DMA需要在什么时机进行转运的,触发源,有硬件触发,和软件触发,具体选择哪个,由M2M这个参数决定,M2M就是Memory to Memory。

软件触发DMA

当我们给M2M位1时,DMA就会选择软件触发,这个软件触发并不是调用某个函数一次,触发一次,它这个软件触发的执行逻辑是,以最快的速度,连续不断地触发DMA,争取早日把传输计数器清零,完成这一轮的转换,所以这里的软件触发和之前外部中断以及ADC的软件触发不一样,可以把这里的软件触发理解成连续触发,软件触发和循环模式不能同时使用,因为软件触发就是想把传输计数器清零,循环模式是清零后自动重装,如果同时用的话,那DMA就停不下来了,软件触发一般适用于存储器到存储器的转运,因为存储器到存储器的转运是软件启动、不需要时机,尽最大速度完成。

硬件触发DMA

当M2M位给0,就是硬件触发DMA,硬件触发源可以选择ADC、串口、定时器等等,使用硬件触发的转运,一般都是与外设有关的转运,这些转运需要一定的时机,比如ADC转换完成、串口收到数据、定时时问到等等,所以需要使用硬件触发,在硬件达到这些时机时,传一个信号过来,来触发DMA进行转运,这就是硬件触发。

开关控制

image

最后,如上图黑色圈的部分,就是开关控制,也就是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请求

image
上图对应下图的DMA触发的部分:
image

最上面的图是DMA1的请求映像,共7个通道,每个通道都有一个数据选择器,可以选择硬件触发或软件触发,这个图中不太好理解,EN位画在了数据选择器的侧边,一般数据选择器的侧边是输入选择控制位,难道这里的意思是,EN给1,选择硬件触发,EN给O,选择软件触发吗,那显然不对,而且第一个信号请求它左边这里写的是软件触发(MEM2MEM位),难道M2M位是软件触发吗,所以这个图重新布局为刚才那个图:
image
M2M位是数据选择器的控制位,用于选择是硬件触发还是软件触发,EN位是开关控制,EN=0时不工作,EN=1时工作,也就是在刚才这个图中:
image

EN并不是数据选择器的控制位,而且决定这个数据选择器要不要工作,EN=0,数据选择器不工作,EN1,数据选择器工作,然后软件触发后面跟个M2M位的意思立该是,当M2M位=1时,选择软件触发。
image

如上图,看左侧的硬件触发源,这里是外设请求信号,可以看到,每个通道的硬件触发源都是不同的,如果你需要用ADC1来触发的话,那就必须选择通道1,如果需要定时器2的更新事件来触发的话,那就必须选择通道2,剩下的也是同理,因为每个通道的硬件触发源都不同,所以如果你想便用某个硬件触发源的话,就必须使用它所在的通道,而如果使用软件触发的话,那通道就可以任意选择了,因为每个通道的软件触发都是一样的,这里通道1的硬件触发是ADC1、定时器2的通道3和定时器4的通道1,到底选择哪个触发源是对应的外设是否开启DMA所决定的,比如你要使用ADC1·那会有个库函数叫ADC_DMACmd,必须使用这个库函数开启ADC1的这一路输出,它才有效,如果想选择定时器2的通道3,那也会有个TIM_DMACmd函数,用来进行DMA输出控制,所以这三个触发源,具体使用哪个,取决于你把哪个外设的DMA输出开启了。
image

如上图,如果3个都开启了,那这边是一个或门,理论上3个硬件都可以进行触发,不过一般情况下我们都是开启其中一个。
image

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

数据宽度与对齐

可编程的数据传输宽度和大小端操作(当PINC = MINC = 1)

源端宽度 目标宽度 传输数目 源:地址/数据 传输操作 目标:地址/数据
8 8 4 0x0 / B0
0x1 / B1
0x2 / B2
0x3 / B3
1: 在0x0读B0[7:0],在0x0写B0[7:0]
2: 在0x1读B1[7:0],在0x1写B1[7:0]
3: 在0x2读B2[7:0],在0x2写B2[7:0]
4: 在0x3读B3[7:0],在0x3写B3[7:0]
0x0 / B0
0x1 / B1
0x2 / B2
0x3 / B3
8 16 4 0x0 / B0
0x1 / B1
0x2 / B2
0x3 / B3
1: 在0x0读B0[7:0],在0x0写0x00B0[15:0]
2: 在0x1读B1[7:0],在0x2写0x00B1[15:0]
3: 在0x2读B2[7:0],在0x4写0x00B2[15:0]
4: 在0x3读B3[7:0],在0x6写0x00B3[15:0]
0x0 / 0x00B0
0x2 / 0x00B1
0x4 / 0x00B2
0x6 / 0x00B3
8 32 4 0x0 / B0
0x1 / B1
0x2 / B2
0x3 / B3
1: 在0x0读B0[7:0],在0x0写0x000000B0[31:0]
2: 在0x1读B1[7:0],在0x4写0x000000B1[31:0]
3: 在0x2读B2[7:0],在0x8写0x000000B2[31:0]
4: 在0x3读B3[7:0],在0xC写0x000000B3[31:0]
0x0 / 0x000000B0
0x4 / 0x000000B1
0x8 / 0x000000B2
0xC / 0x000000B3
16 8 4 0x0 / B1B0
0x2 / B3B2
0x4 / B5B4
0x6 / B7B6
1: 在0x0读B1B0[15:0],在0x0写B0[7:0]
2: 在0x2读B3B2[15:0],在0x1写B2[7:0]
3: 在0x4读B5B4[15:0],在0x2写B4[7:0]
4: 在0x6读B7B6[15:0],在0x3写B6[7:0]
0x0 / B0
0x1 / B2
0x2 / B4
0x3 / B6
16 16 4 0x0 / B1B0
0x2 / B3B2
0x4 / B5B4
0x6 / B7B6
1: 在0x0读B1B0[15:0],在0x0写B1B0[15:0]
2: 在0x2读B3B2[15:0],在0x2写B3B2[15:0]
3: 在0x4读B5B4[15:0],在0x4写B5B4[15:0]
4: 在0x6读B7B6[15:0],在0x6写B7B6[15:0]
0x0 / B1B0
0x2 / B3B2
0x4 / B5B4
0x6 / B7B6
16 32 4 0x0 / B1B0
0x2 / B3B2
0x4 / B5B4
0x6 / B7B6
1: 在0x0读B1B0[15:0],在0x0写0x000B1B0[31:0]
2: 在0x2读B3B2[15:0],在0x4写0x000B3B2[31:0]
3: 在0x4读B5B4[15:0],在0x8写0x000B5B4[31:0]
4: 在0x6读B7B6[15:0],在0xC写0x000B7B6[31:0]
0x0 / 0x000B1B0
0x4 / 0x000B3B2
0x8 / 0x000B5B4
0xC / 0x000B7B6
32 8 4 0x0 / B3B2B1B0
0x4 / B7B6B5B4
0x8 / BBA9B8B7
0xC / BFBEBDBC
1: 在0x0读B3B2B1B0[31:0],在0x0写B0[7:0]
2: 在0x4读B7B6B5B4[31:0],在0x1写B4[7:0]
3: 在0x8读BBA9B8B7[31:0],在0x2写B7[7:0]
4: 在0xC读BFBEBDBC[31:0],在0x3写BC[7:0]
0x0 / B0
0x1 / B4
0x2 / B7
0x3 / BC
32 16 4 0x0 / B3B2B1B0
0x4 / B7B6B5B4
0x8 / BBA9B8B7
0xC / BFBEBDBC
1: 在0x0读B3B2B1B0[31:0],在0x0写B3B2B1B0[15:0]
2: 在0x4读B7B6B5B4[31:0],在0x2写B7B6B5B4[15:0]
3: 在0x8读BBA9B8B7[31:0],在0x4写BBA9B8B7[15:0]
4: 在0xC读BFBEBDBC[31:0],在0x6写BFBEBDBC[15:0]
0x0 / B3B2B1B0
0x2 / B7B6B5B4
0x4 / BBA9B8B7
0x6 / BFBEBDBC
32 32 4 0x0 / B3B2B1B0
0x4 / B7B6B5B4
0x8 / BBA9B8B7
0xC / BFBEBDBC
1: 在0x0读B3B2B1B0[31:0],在0x0写B3B2B1B0[31:0]
2: 在0x4读B7B6B5B4[31:0],在0x4写B7B6B5B4[31:0]
3: 在0x8读BBA9B8B7[31:0],在0x8写BBA9B8B7[31:0]
4: 在0xC读BFBEBDBC[31:0],在0xC写BFBEBDBC[31:0]
0x0 / B3B2B1B0
0x4 / B7B6B5B4
0x8 / BBA9B8B7
0xC / BFBEBDBC

之前提到的数据转运的两个站点,都有一个数据宽度的参数,如果数据宽度都一样,那就是正常的一个个转运,如果数据宽度不一样,就按这个表中相应的传输操作来处理,只说明一点,就是源端宽度大于目标宽度,舍弃高位,源端宽度小于目标宽度,高位补0.

补充内容

位段

位段区相当于位寻址,它把外设寄存器和SRAM中所有的位都分配了地址,你操作这个新的地址,就相当于操作其某一个位,因为32位的地址有99%都是空的,所以地址空间很充足,即使把每一位都单独编码那也毫无压力,所以就存在了这样一个位段,用于单独操作寄存器或SRAM的某一位,位段区是另找了一个地方,开辟了一段地址区域。

Cortex™-M3存储器映像包括两个位段(bit-band)区。这两个位段区将别名存储器区中的每个字映射到位段存储器区的一个位,在别名存储区写入一个字具有对位段区的目标位执行读-改-写操作的相同效果。

在STM32F10xxx里,外设寄存器和SRAM都被映射到一个位段区里,这允许执行单一的位段的写和读操作。

下面的映射公式给出了别名区中的每个字是如何对应位带区的相应位的:

\[\text{bit_word_addr} = \text{bit_band_base} + (\text{byte_offset} \times 32) + (\text{bit_number} \times 4) \]

其中:

  • bit_word_addr 是别名存储器区中字的地址,它映射到某个目标位。
  • bit_band_base 是别名区的起始地址。
  • byte_offset 是包含目标位的字节在位段里的序号。
  • bit_number 是目标位所在位置(0~31)。

例子
下面的例子说明如何映射别名区中SRAM地址为0x20000300的字节中的位2:

\[0x22006008 = 0x22000000 + (0x300 \times 32) + (2 \times 4) \]

0x22006008地址的写操作与对SRAM中地址0x20000300字节的位2执行读-改-写操作有着相同的效果。

posted @ 2025-08-18 15:46  秦瑞迁  阅读(79)  评论(0)    收藏  举报