第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个独立可配置的通道: DMA1(7个通道), DMA2(5个通道)。
这个通道就是数据转运的路径,从一个地方移动到另一个地方,就需要占用一个通道,如果有多个通道进行转运,那它们之间各转各的,互不干扰。
每个通道都支持软件触发和特定的硬件触发。
如果DMA进行的是存储器到存储器的数据转运,比如想把Flash里的一批数据,转运到SRAM里去,那就需要软件触发了,使用软件触发,DMA会一股脑地,把这批数据,以最快的速度,全部转运完成。若果DMA进行的是外设到存储器的数据转运,就不能一股脑的转运了,因为外设的数据是有一定时机的,所以这时候需要用硬件触发,比如转运ADC的数据,就得ADC每个通道AD转换完成后,硬件触发一次DMA,之后DMA再转运,触发一次,转运一次,这样数据才是正确的。所以存储器到存储器的数据转运,我们一般使用软件触发;外设到存储器的数据转运,我们一般使用硬件触发。特定的硬件触发指的是每个DMA的通道,它的硬件触发源是不一样的,如果要使用某个外设的的硬件触发源,就得使用它连接的那个通道,而不能任意选择通道。
STM32F103C8T6 DMA资源:DMA1(7个通道)。
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);
}
}