STM32外设学习--DMA直接存储器读取--学习笔记。
上次我们完成了单片机的ADC模数转换的功能,实现了单通道,多通道ADC。这次我们来看DMA直接存储器读取的学习。
STM32F103C8T6的DMA(直接存储器存取) 是一个非常重要的外设,它可以在不占用CPU的情况下实现外设和存储器之间、存储器与存储器之间的高速数据传输。让我详细为你解释:
一. DMA核心概念
1.什么是DMA?
DMA就像一个"数据搬运工",可以在外设(如ADC、串口、SPI等)和内存(如变量、数组)之间直接传输数据,不需要CPU参与。
传统方式 vs DMA方式:
传统方式:外设 → 通知CPU → CPU读取 → CPU写入内存 → 继续执行程序
DMA方式:外设 → DMA直接写入内存 → 同时CPU正常执行程序
2.STM32F103C8T6的DMA资源
这款芯片有2个DMA控制器(DMA1、DMA2),共12个通道(DMA1有7个,DMA2有5个)
| DMA控制器 | 通道数量 | 支持的外设 |
|---|---|---|
| DMA1 | 7个通道 | ADC1、SPI1、I2C1、USART1/2/3、TIM1/2/3/4等 |
| DMA2 | 5个通道 | ADC2/3、SPI2、I2C2、USART1、TIM1/5/8等 |
3.DMA工作流程详解
DMA传输三要素:
源地址:数据从哪里来
目标地址:数据到哪里去
传输数量:传输多少数据
工作流程:
配置DMA → 启动传输 → DMA自动搬运数据 → 传输完成产生中断
↓
CPU继续执行其他任务
4.DMA寄存器关键配置
主要寄存器:
DMA_CPARx:外设地址寄存器
DMA_CMARx:存储器地址寄存器
DMA_CNDTRx:数据传输数量寄存器
DMA_CCRx:通道配置寄存器
二.DMA简介
1.详情了解

从我们名字可以看出。DMA这个外设,是可以直接访问STM32的存储器的。
包括运行内存:SRAM。程序存储器Flish和寄存器等等。
这里的外设只得就是外设寄存器。一般是外设的数据寄存器DR,Data Register。比如ADC的数据寄存器,串口的数据寄存器。等等
这里的存储器,指得就是运行内存SRAM。和程序存储器Flish。是我们存储变量数组和程序代码的地方。
在外设和存储器,存储器和存储器之间转运数据,就可以用DMA来完成,并且在转运的过程中,无需CPU参与。节省了CPU资源,CPU省下来了时间,就可以干一些其他的,更加专业的事情。搬运数据这种杂活,交给DMA就可以了。
12个DMA转运的通道,这里的通道就是数据转运的路径,如果有多个通道进行转运,拿他们之间各转各的不会互相干扰。
如果DMA进行存储器和存储器之间的转运,比如我们想把,Flish里面的一批数据,转移到SRMA里面去。那就需要软件触发了。使用软件触发之后DMA就可以把数据,一股脑的高速的,全部转运完成。
如果DMA进行的是外设到存储器之间的数据转运。就不能一股脑的转运了,因为外设的数据是有一定时机的。所以这时候我们要用硬件触发。比如转运ADC的数据,那就需要ADC每个通道AD转换完成后,硬件触发一次DMA,之后DMA在进行转运,触发一次,转运一次,这样子数据才是正确的。才是我们想要的效果。
所以存储器到存储器的数据转运,我们一般使用软件触发,外设到存储器数据的转运我们一般使用特定的硬件触发。
特定的硬件触发,意思就是每个DMA通道,他的硬件触发源是不一样的。你要使用某个外设的硬件触发源就得使用他连接的那个通道。
2.存储器映像

我们了解到,计算机系统的五大组成部分,运算器,控制器,存储器,输入设备和输出设备。其中运算器和控制器会合在一起叫CPU。所以计算机的核心部分就是CPU和存储器。
存储器又有两个重点,分为存储器内容,以及存储器地址。那么STM32也不例外。上图就是STM32中所有的存储器,以及他们的地址。
无论是Flish还是SROM还是外设寄存器都是存储器的一种。
3.ROM
ROM是只读存储器,是一种非易失性,掉电不丢失的存储器。
RAM就是随机存储器是一种易失性。掉电丢失的存储器。
(1)Flash
其中ROM分为了3快,第一块是程序存储器Flash,也就是主闪存他的用途就是存储C预言编译后的代码,也就是我们下载程序的位置,一般运行程序,也是从主闪存里面执行的

这里STM32给他的地址是0x0800 0000。这是起始地址,也就是第一个字节的地址,剩余字节的地址依次增长,每个字节都分配一个独一无二的地址。
(2)系统存储器和选项字节
这两块存储器也是我们ROM的一种,掉电不丢失。实际上他们的存储介质也是Flish,只不过我们一般说的Flish指主闪存Flash,而不指这两块区域。

他们的地址都是0x1FFF 开头的。这两块存储器的位置是在ROM的最后面。

可以看一下他们的说明。第一个适用于串口下载。BootLoader程序存储的位置就被放在了这里。
BootLoader是程序是芯片出场自动写入的,一般不允许我们修改。
之后是选项字节,他是在ROM区最后面,你下载程序可以选择不刷新选项字节,这样子选项字节的配置,就会保持不变。
选项字节里存放的主要是Flish的读保护和写保护,还有看门狗等程序的配置。
4.RAM
(1)运行内存SRAM

可以看到地址和功能。也就是我们在程序中定义变量,数组,结构体的地方。可以试一下,定义一个变量,然后再取地址显示出来,那么他的地址就是0x2000开头的。
类比于电脑的话,运行内存就是内存条。
(2)外设寄存器
![]()
这里面存储的是我们初始化各个外设,最终所读写的东西。外设寄存器也是存储器的一种,他的存储单元也是SRAM。只不过我们一般喜欢把运行内存叫SRAM,外设寄存器就直接叫寄存器了。
(3)内核外设寄存器
![]()
内核就是NVIC和SysTick,因为内核外设和其他外设不是一个厂家设计的,所以他们的地址也是被分开了。
5.对比数据手册

我们可以看到再STM32中,所有存储器的地址都被安排到了0x0000-0xFFFF这个地址范围内。
因为CPU是32位的,所以寻址范围也就是32位的。32位的寻址范围是非常大的,最大可支持4GB容量的存储器。而我们STM32的存储器都是KB级别的。
所以4GB的寻址空间,会有大部分的都是空的。算一下地址的使用率还不到1%。

这里面有灰色填充的,就是我们的保留区域,就是没有用到的。

然后这个0地址实际上也是没有存储器的。这里写的是别名到Flish或者系统存储器,取决于Boot引脚。因为程序是从0地址开始运行的。所以这里把我们想要执行的程序,映射到0地址来。如果映射到Flish区,那么就从Flish开始执行程序。如果映射到系统存储器区,那么就是从系统存储器区,运行BottLoader。如果映射到SRAM就是从SRAM启动。
怎么选择由Boot0和Boot1两个引脚进行选择。这就是0地址里面的别名区。

接着剩下的0x0800Flish区,用于存储程序代码。

0x1FFF开始的系统存储器和选项字节,是在ROM区的最后面。

之后0x2000开始的是SROM区。

0x4000开始的是外设寄存器区。

这里面可以展开就是右面的这些东西。具体到每个外设又有他们自己的起始地址。

比如TIM2的是0x4000 0000,TIM3的地址是0x4000 0400。
然后外设地址里面,又可以具体分到每个寄存器地址,寄存器里每个字节的地址。最终所有字节的地址,我们就都算出来了。

最后上面这里0xE000开始的区域。存放的就是内核里面的外设寄存器了。
6.DMA框图

左上角是cortex-M3内核,里面包含了CPU和内核外设等等。剩下的都可以看成存储器,所以总共就是CPU和存储器两个东西,Flash是主闪存,SRAM是运行内存。各个外设都可以看成是寄存器也是一种SRAM存储器,寄存器是一种特殊的存储器。
一方面CPU可以dui寄存器进行读写,就想读写运行内存一样,另一方面寄存器的每一个背后都链接了一根导线这些导线可以用来控制外设电路的状态。比如置引脚高低电平,导通和断开开关切换数据选择器,或者多位结合起来当作计数器,数据寄存器等等。
所以寄存器是连接软件和硬件的桥梁。软件读写寄存器就相当于在控制硬件的执行。

为了高效有条例的访问存储器,这里设置了一个总线矩阵。总线矩阵的左端是主动单元,拥有存储器的访问权。右边的是被动单元,他们的存储器,只能被左边的主动单元读写。
主动单元这里内核有DCode和系统总线。可以访问右边的存储器,其中Dcode总线是专门访问Flish的。系统总线是访问其他东西的。另外由于DMA要转运数据,所以DMA必须要有访问的主动权。

这里可以看到DMA1有一条总线,DMA2有一条,下面的以太网也有一条,这个可以先不管。
在DMA1里可以看到DMA1有七个通道,DMA2有五个通道。各个通道可以设置他们转运数据的源地址和目的地址。
下面有一个仲裁器,虽然,多个通道可以独立运转数据,但是最终DMA总线只有一条。所以所有通道只能分时复用这一条DMA总线,如果产生了冲突,那就会由仲裁器,根据通道的优先级,决定谁先用,谁后用。
另外在总线矩阵这里也会有一个仲裁器如果DMA和CPU都要访问一个目标,那么DMA就会暂停CPU的访问,防止冲突。不过总线仲裁器仍然会保持CPU得到一半的总线带宽,使CPU也能正常工作。

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

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

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

还有一点就是这里的Flish他是ROM只读存储器的一种,如果通过总线直接访问的话无论是CPU还是DMA都是只读的只能读取数据,而不能写入,如果DMA的目的地址,填写了Flish的区域,那么转运时候就会出错。

如果想要写入Flish的话,我们可以配置这个Flash接口控制器。对Flash进行写入。这个就比较麻烦了要对Flish按页进行擦除。再写入数据。
三.DMA的基本结构图
1.基本结构图


这两部分就是数据转运的两大站点了。左边是外设寄存器站点右边是存储器站点包括Flash和SRAM在STM32手册里他所说的存储器一般是特指Flash和SRAM,不包含外设寄存器。外设寄存器一般直接称为外设。
所以就是外设--存储器,存储器--存储器,这样子进行描述。
虽然我们知道,寄存器也是存储器的一种但是STM32还是使用了寄存器和存储器进行区分。
可以看到DMA的转运数据是双向的,外设到存储器,存储器到外设,具体的方向有一个函数可以控制。
还有一种转运方式就是,存储器到存储器比如Flash--SRAM,SRAM-SRAM,这两种方式。
由于Flish是只读的,所以DMA不可以进行Flash到Flash的转运操作。

我们可以看到,外设和存储器都有三个参数。
(1)第一个参数
是起始地址有外设起始地址,存储器起始地址,这两个参数决定了数据是从哪里来,到哪里去的。
(2)第二个参数
是数据宽度,指定一次转运要用多大的数据宽度来进行。它可以选择字节Byte,半字节HalfWord,和字Word。
字节:8位,一次转运一个uint8_t 这么大的数据。
半字:16位,一次转运一个uint16_t 这么大的数据。
字:32位,一次转运一个uint32_t 这么大的数据。
比如转运ADC的数据,ADC的结果是uint16_t那么大,所以这个参数就要选择半字。
(3)第三个参数
是地址是否自增。这个参数的作用是,指定一次转运完成后下一次转运,是不是要把地址移动到下一个位置去。这就相当于指针P++的意思。
比如ADC扫描模式,用DMA转运数据,外设地址是ADC_DR寄存器,寄存器这边显然地址是不用自增的。如果自增,那么下一次转运就跑到别的寄存器那里去了,因为每个寄存器地址是不一样的。存储器这边,地址就需要自增,每转运一个数据后,都要往后挪一个地址,要不然下一个转运的数据过来,就会覆盖掉这个数据。
(4)转运例子
如果要进行存储器到存储器的数据转运,那我们就需要把其中一个存储器的地址,放在外设的站点,这样就可以了,只要你在外设地址里写Flash或SRAM的地址,那么DMA就会到Flash或者SRAM里面去找数据。

这个站点虽然叫外设寄存器,并不是说只能写外设寄存器的地址,如果写Flash的地址1,那么DMA就会去Flash里面找,SRAM同理。甚至可以在外设站点写存储器地址,存储器站点写外设地址。方向参数给反过来,这样子也可以。
(5)传输计数器

这个东西就是用来指定,我总共需要转运几次的这个传输计数器是一个自减计数器。比如说给他写一个5,那么DMA就会只能进行五次数据转运。转运过程中每转运1次,计数就会-1。当传输计数器减到0之后,就不会进行数据转运了。另外他转到0之后,之前自增的地址,也会恢复到起始地址的位置,以方便之后DMA进行新一轮的转运。
在传输寄存器的右边有一个自动重装器,这个自动重装器的作用就是,传输计数器减到0之后,是否要自动恢复到最初的值。比如最初传输计数器给5,如果不使用自动重装器,那么转运5次后,DMA就结束了,如果使用自动重装器,那么转运5次,计数器转到0后,就会立即重装到初始值5。
这就是自动重装器,它决定了转运的模式,如果不重装,就是正常的单次模式,如果重装就是循环模式。比如说想要转运一个数组,那么一般就是单次模式,转运1伦就结束了,如果是ADC扫描模式+连续转换那么为了配合ADC,DMA也需要采用循环模式。
所以这个循环模式和ADC的连续模式差不多,都是执行完一轮后,是否要继续执行下一轮工作。
(6)触发控制

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

最后就是开关控制了,也就是使能DMA,和使能ADC,定时器是一个道理。也就是给DMA上电。
(8)DMA启动条件
1.开关控制,DMA必须使能。
2.传输计数器必须大于0。
3.触发源必须有触发信号。
当传输计数器等于0,这时候无论DMA有没有触发,都不会在转运。此时就需要DMA_Cmd给Disable,关闭DMA,在为传输计数器,写一个大于0的数。再DMA_Cmd给ENABLE1,开启DMA。DMA才能继续工作。
值得注意的是,写传输计数器时,必须要关闭DMA,不允许DMA再开启时写传输计数器,这是手册的规定。
2.DMA请求


这里表示的是我们这块的结构,DMA触发的部分。
这张图时DMA1的请求映像,一共有7个通道。

每个通道都有一个数据选择器,可以选择硬件触发或者软件触发。


他把EN位画在了数据选择器的侧面,一般数据选择器的层面是输入选择控制位。难到这里的意思是EN给1选择硬件触发,EN给0选择软件触发吗?显然不是,而且在左面这里写的是软件触发MEM2MEM位。

所以重新布局了,M2M是数据选择控制位,用于选择硬件触发,还是软件触发EN为,开关控制,EN=0时不工作,EN=1时工作。
那么这里就说通了,EN不是数据选择器的控制位,而是决定数据选择器是否工作。
然后软件触发后面跟个MEM2MEM的意思应该是当M2M位等于1时选择软件触发。

可以看左边的硬件触发源,这里是外设请求信号,可以看到,每个通道的硬件触发源都是不一样的。如果像哟用ADC1来触发的话,那么必须选择通道1,如果需要定时器2的更新事件来触发的话,就要选择通道2。
如果使用软件触发的话,那么就可以任意选择了,因为软件触发的通道都是一样的。

可以看到,有很多硬件触发源,那么该选择哪一个呢,这个是对应的外设是否开启了DMA来决定的,比如说要使用ADC1,那么会有一个对应的ADC_DMACmd,必须使用这个库函数,开启DMA的这一路输出,它才有效。如果三个都开启了,这里是一个或门,理论上三个都可以开启触发,但是我们通常只开启一个。

之后这七个通道进入仲裁器进行优先级判断,最终产生内部的DMA请求。类似于中断的优先级,默认通道号越小,优先级越高。也可以再程序中,配置优先级。
3.数据宽度与对齐


我们可以看到这两个站点,都有一个类似数据宽度的东西。如果数据宽度都一样,那么就是正常的一个接着一个转,如果数据宽度不一样,就按照表里的进行操作。

(1)同等宽度
第一列是源端宽度,第二列是目标宽度,第三列是传输数目。

当源端和目标都是八位时,转运第一步,再源端0的位置,读取数据B0,再目标的0位置写入数据B0。就是把B0从左面移动到右面。之后就是B1,B2,B3。
(2)目标大于源端

然后就是源端8位,目标16位那么就是在源端0的位置读取B0,在目标写入00B0。B1,B2,B3同理都是,00B1,00B2,00B3。
意思就是,目标的数据宽度,比源端的数据宽度大,那么就在目标数据前面多出来的空位补0。
(3)目标小于源端

源端16位,目标却只有8位,源端大于目标宽度了。从源端0位置读取B1B0,转运后目标写入B0,舍去了B1,读取B3B2,写入B2。也就是把多出来的高位舍弃掉。
总结:小数据传到大数据,大数据高位补0,大数据传给小数据,大数据高位舍去。
4.数据转运+DMA

这个例子的意思时,将SRAM里的数组DataA,转运到另一个数组DataB中。
(1)外设地址
那么外设地址因该填DataA数组的首地址,存储器地址给DataB数组首地址。
(2)数据宽度
然后数据宽度两个数组的类型都是uin8t_t。所以数据宽度都是按8位字节传输。
(3)是否自增
我们可以在中间看出我们想要的效果是DataA1转运到DataB1,DataA2转运到DataB2。两个数组的位置一一对应。所以说转运完DataA1和DataB1之后,两个站点的地址都因该自增。都移动到下一个数据的位置,入座左边不自增,转运完成后右边所有DataB的位置都会变成DataA0。如果左边自增,右边不自增,那么就会DataB0是DataA7,DataB其他位置不变,如果都不自增,那么就是DataA0一直等于DataB0。其他数据不变。
(4)传输方向
直接改变方向参数就好,存储器到存储器
(5)传输计数和是否重装
在这里显然要转运7次,所以传输计数器我们给7,自动重装暂时不需要
(6)触发选择
我们要使用软件触发,因为是存储器到存储器之间的数据。是不需要等待硬件时机的,尽快完成转运就行。
(7)DMA使能
最后给DMA上电,使能DMA这样子就可以搬运数据了。
这里的转运是一种复制转运,转运完成后,DataA的数据并不会消失而是复制到DataB。
5.ADC扫描模式+DMA

(1)流程
左面是ADC扫描模式的执行流程。触发一次后七个通道依次进行AD转换然后转换结果都放在ADC_DR数据寄存器里面,我们要做的就是,在每个单独的通道转换完成后进行一次DMA数据转运,并且目的地址进行自增。这样数据就不会被覆盖了。
(2)地址
所以在这里DMA的配置就是外设地址,写入ADC_DR这个寄存器的地址,存储器的地址,可以在SRAM中定义一个ADValue。然后把ADValue的地址,当作存储器的地址。
(3)数据宽度
之后数据宽度,因为ADC_DR和SRAM数组要的都是uint 16_t的数据,所以数据宽度都是16位的半字传输。
(4)是否自增
显然是外设地址不自增,存储器地址自增。
(5)传输方向
是外设站点到存储器站点。
(6)传输器
传输计数器,这里通道有7个所以计数7次。
(7)是否自动重装
这里可以看ADC配置,ADC如果是单次扫描,那么DMA传输计数器可以不自动重装。转换一轮就停止,如果ADC是连续扫描,那么DMA就可以使用自动重装。在ADC启动下一轮转换的时候,DMA也启动下一轮转运。ADC和DMA同步工作。
(8)触发选择
这里ADC_DR的值是在ADC单个通道转换完成后才会有效,所以DMA转运的时机,需要和ADC单个通道转换完成同步,所以DMA的触发要选择ADC的硬件触发。
最后因为ADC扫描模式在每个单独的通道转换完成后没有任何标志位,也不会触发中断,但是根基视频描述,虽然不会产生任何标志位,也不会触发中断,但是会有一个DMA请求会触发DMA转运,这一部分手册没有说名,根据实验,单个通道的MA请求肯定是有的。..
6.总结
一般来说,DMA最常见的用途就是配合ADC的扫描模式,因为ADC扫描有一个数据覆盖的特征。或者说数据覆盖的我呢提是ADC固有的缺陷。这个缺陷使得DMA和ADC成为了最常见的伙伴。
其他的外设使用DMA属于锦上添花,增加一下效率,但是ADC的扫描模式不使用DAM的话,功能就会出现问题。
7.查看手册
存储器映射的内容



运行内存。

位段区域,就相当于位寻址。把stm32中所有的外设和寄存器都分配了地址。操作这个新的地址,就相当于操作其中的某一个位。因为32位地址有99%都是空的,即使把每一位都单独编码,也是可以的。

然后就是闪存。下面的闪存接口寄存器地址是0x400开头的,说明是外设的一部分。这个外设可以对闪存进行读写。

启动配置引脚,来选择程序在哪里启动。

传输计数器只有低16位有效1所以传输范围是0-65535个。所以传输个数最大位65535个。
