DMA 协助CPU,完成数据转运工作。
两个程序: DMA数据转运,DMA+AD多通道
DMA数据转运,将使用DMA,进行存储器到存储器的数据转运,也就是把一个数组里面的数据,复制到另一个数组里。
定义一个数组DateA ,里面存放1、2、3、4;然后再定义一个数组DateB,里面存的4个0,作为数据转运的目的地,我们会写一个模块,叫MyDMA,把源数组和目的数组地址传进去,再传入转运数据长度4,就这执行主循环的流程。
第一步:自增,变化一下源数组DataA 的测试数据,
第二步:显示一下DataA和DataB,然后延时一秒,方便观看
第三步:调用一下MyDMA_Transfer函数,使用DMA进行数据转运。和使用for循环使用CPU一个个手动地转运数据,效果是一样的
接着最后,再显示一下DataA和DataB,看一下数据是不是从DataA转运到了DataB
DMA+AD多通道程序:
用ADC的扫描模式来实现多通道采集,然后使用DMA来进行数据转运,最终,AD转换数据就会直接自动地跑到我们定义的数组里面,然后用OLED显示一下就可以了。和上一节AD多通道的现象是一样的,就只是在STM32端,使用了扫描模式,并且加了DMA转运数据。
DMA这个外设,是可以直接访问STM32内部的存储器的,包括运行内存SRAM、程序存储器Flash和寄存器等等,DMA都有权利直接访问它们。所以DMA才能完成数据转运工作。
这里外设指的是外设的寄存器,一般是外设的数据寄存器DR Data Register ,比如ADC的数据寄存器,串口的数据寄存器等等,这里存储器指的就是运行内存SRAM和程序存储器Flash,是我们存储变量数组和程序代码的地方,在外设和存储器,或者存储器和存储器之间,进行数据转运,就可以使用DMA来完成,并且在转运的过程中,无须CPU的参与,节省了CPU的资源,CPU省下时间,就可以干一些其他的,更加专业的事情,搬运数据这种杂活,交给DMA就行了。
从一个地方移动到另一个地方,就需要占用一个通道,如果有多个通道进行转运,那它们之间可以各转各的,互不干扰,这就是DMA通道。
如果DMA进行的是存储器到存储器的数据转运,比如我们想把Flash里的一批数据,转运到SRAM里去,使用软件触发之后,DMA就会一股脑地把这批数据,以最快的速度,全部转运完成,这也是我们想要的结果。那如果DMA进行的是外设到存储器的数据转运,因为外设的数据是有一定时机的,所有这时我们需要用硬件触发,那就得ADC每个通道AD转换完成后,硬件触发一次DMA,之后DMA再转运。触发一次转运一次,这样的数据才是正确的,才是我们想要的效果。特定的硬件触发,意思是每隔DMA的通道,它的硬件触发源是不一样的,你要使用某个外设的硬件触发源,就得使用它连接的那个通道,而不能任意选择通道,
运算器、控制器、存储器、输入设备和输出设备
其中运算器和控制器一般会合在一起叫做CPU,计算机核心关键部分就是CPU和存储器
存储器两个重要的知识点:内容和地址。
这个表是STM32中所有类型的存储器,和它们被安排的地址,STM32的数据手册里,也有存储器映像的图,一个意思。
外设到存储器实质上也是存储器到存储器之间的数据转运。
ROM第一块:程序存储器Flash 主闪存 也就是我们下载程序的位置 运行程序,一般也是从主闪存里面开始运行的。这一块存储器,STM32给它分配的地址是0x0800 0000 起始地址,也就是第一个字节的地址是0800这个,起始地址,也就是第一个字节的地址是0800这个,然后剩余字节的地址依次增长,每个字节都分配一个独一无二的地址,就像给每个住户编门牌号一样,程序才能精准地访问这个存储器,最终终止地址是多少呢,取决于它的容量。
如果在软件里看到,某个数据的地址是0800开头的,可以确定它属于主闪存的数据。
系统存储器和选项字节,两块存储器掉电不丢失,也是ROM的一种,实际上它们的存储介质也是Flash,只不过我们一般讲的Flash指的是主闪存Flash,而不指这两块区域,可以看出这两块区域位置是在ROM区的最后面。
系统存储器的用途:BootLoader程序是芯片出厂自动写入的,一般也不允许我们修改,
选项字节位置是在ROM区的最后面,下载程序可以不刷新选项字节的内容,存的主要是Flash的读保护,写保护,还有看门狗等等的配置。
运行内存SRAM 存放 我们在程序中定义 变量、数组、结构体的地方,类比电脑的话,运行内存就是内存条
外设寄存器 也就是我们初始化各个外设,最终所读写的东西。它的存储介质其实也是SRAM,只不过习惯把运行内存叫SRAM,外设寄存器直接叫寄存器
内核外设就是NVIC和SysTick,因为内核外设和其他外设不是一个厂家设计的,所以它们的地址也是被分开了。
在STM32中,所有的存储器都被安排到了0-8个F这个地址范围内,因为CPU是32位的,所以寻址范围就是32位的范围,最大可以支持4GB容量的存储器,而STM32中的存储器都是KB级别的,所以这个4GB的寻址空间,会有大量的地址是空的,算一下地址使用率还不到1%。
它这里写的是别名到Flash或者系统存储器,取决于Boot引脚,所以这里需要我们想要执行的程序,映射到0地址来。如果映射在Flash区,就是从Flash执行,如果映射再系统存储器区,就是从系统存储器运行BootLoader,如果映射到SRAM,就是从SRAM启动,怎么选择,由BOOT0和BOOT1两个引脚来决定。
具体的每个外设,又有它们自己的起始地址,比如TIM2的地址是4000 0000 又可以具体细分到每个寄存器的地址、寄存器里每个字节的地址。
左上角是M3的内核,里面包含了CPU和内核外设等等,剩下的所有东西,都可以把它看成是存储器,所以总共就CPU和存储器两个东西。
Flash是主闪存区,SRAM是运行内存,各个外设,都可以看成是寄存器,也是一种SRAM寄存器,一方面,CPU可以对寄存器进行读写,就像读写运行内存一样,另一方面,寄存器的每一位背后,都连接了一根导线,用于控制外设电路的状态,比如置引脚的高低电平,导通和断开开关,切换数据选择器。所以,寄存器是连接软件和硬件的桥梁。软件读写寄存器,就相当于控制硬件的执行。
既然外设是寄存器,寄存器就是存储器,那使用DMA进行数据转运,就都可以归结为一类问题了,就是从某个地址取内容,再放到另一个地址去。
为了高效有条理地访问存储器,设计了一个总线矩阵,总线矩阵的左端是主动段元,也就是拥有存储器的访问权,右边这些是被动单元,它们的存储器只能被左边的主动单元读写,主动单元这里,内核有DCode和系统总线,可以访问右边的存储器,其中DCode总线是专门访问Flash的,系统总线是访问其他东西的。另外,由于DMA要转运数据,所以DMA也要有访问的主动权,那主动单元除了内核CPU剩下的就是DMA总线了。这里DMA1有一条DMA总线,DMA2也有一条DMA总线,还有以太网外设自己私有的DMA,这个可以先不用管,在DMA1和DMA2里面,可以看到DAM1有7个通道,DMA2有5个通道,各个通道可以分别设置它们转运数据的源地址和目的地址,这样它们就可以各自独立地工作了。
下面的仲裁器是因为,虽然多个通道可以独立转运数据,但是DMA总线只有一条,所以所有的通道都只能分时复用这一条DMA总线,如果产生了冲突,那就会由仲裁器,根据通道的优先级,来决定谁先用,谁后用。另外总线矩阵里也有个仲裁器,如果DMA和CPU都要访问同一个目标,那么DMA就会暂停VPU的访问,以防止冲突,不过总线仲裁器,仍然会保证CPU得到一半的总线带宽,使CPU正常的工作,这就是仲裁器的作用。
DAM作为一个外设,也会有相应的配置寄存器,这里连接在总线右边的AHB总线上,所以DMA即便是总线矩阵的主动单元,可以读写各种存储器,也是AHB总线上的被动单元,CPU通过这一条线路,就可以对DMA进行配置了。
DMA请求就是DAM的硬件触发源,比如ADC转换完成,串口接收到数据,需要触发DMA转运数据的时候,会通过这条线路,向DMA发出硬件触发信号,之后DMA就可以执行数据转运的工作了,这就是DMA请求的作用。
Flash如果通过总线直接访问,无论是CPU还是DMA,都是只读的,不能写入,我们可以配置Flash接口控制器,对Flash进行写入,这个流程就比较复杂了,要先对Flash按页进行擦除,再写入数据,这是另一个课题。暂不讨论。
SRAM可以任意读写
外设寄存器得看手册说明是只读的还是只写的,还是读写的
在这个图里面,外设寄存器和Flash、SRAM是数据转运的两大站点,STM32手册里,所说的存储器,一般是特指Flash和SRAM,不包含外设寄存器,外设寄存器就会直接称为外设。转运方向有一个方向的参数,可以进行控制,另外还可以从存储器到存储器。由于Flash是只读的,所以DMA不可以进行SRAM到Flash,或者Flash到Flash的转运操作。
既然要进行数据转运,那肯定要指定从哪里转到哪里,具体怎么转了,所以外设和存储器两个站都有3个参数,第一个是起始地址,有外设端的起始地址,和存储器端的起始地址,这两个参数决定了数据是从哪里来,到哪里去的。
第二个是数据宽度,指定一次要转运要按多大的数据宽度来进行,它可以选择字节Byte,半字HalfWord和字Word,字节8位,半字16位,字是32位。
第三个参数,地址是否自增,参数的作用,指定一次转运完成之后,下一次转运是不是要把地址移动到下一个位置去,这相当于指针,p++,这个意思。比如ADC的扫描模式,用DMA进行数据转运,外设地址是ADC_DR寄存器,寄存器这边显然地址不用自增的,如果自增,那下一次转运就跑到别的寄存器那里去了。存储器这边地址就需要自增,每转运一个数据后,就往后挪个坑,要不然下次转运会覆盖掉。
传输计数器就是用来指定,总共需要转运几次。这是一个自减计数器,比如给它写5,那DMA就只能进行5次数据转运,每转运一次,计数器的数就会减1,当传输计数器减到0之后,DMA就不会再进行数据转运了,之前自增的地址,也会恢复到起始地址的位置,以方便DMA开始新一轮的转运。
自动重装器的作用是传输计数器减到0之后,是否要自动恢复到最初的值,比如传输计数器给5,如果不使用自动重装器,那转运5次之后,DMA就结束了,如果使用自动重装器,那就转运5次,计数器减到0后,就会立即重装到初始值5。决定转运的模式,如果不重装,就是正常的单次模式,如果重装就是循环模式。比如想转运一个数组,一般就是单次模式,转运一轮就结束了。如果是ADC扫描模式+连续转换,那为了配合ADC,DMA也要使用循环模式,所以这个循环模式和ADC的连续模式差不多,都是指定一轮工作结束后,是不是立即开始下一轮工作。
DMA触发控制,触发就是决定DMA需要在什么时候进行转运的,具体触发源选择由M2M这个参数决定Memory to Memory,to和2的英文同音,存储器到存储器的意思。
软件触发并不是调用某个函数触发一次,而是以最快的速度,连续不断地触发DMA,争取早日把传输计数器清零,完成这一轮的转换。所以这里的软件触发和之前外部中断和ADC的软件触发可能不太一样,可以把它理解为连续触发,那这个软件触发和循环模式,不能同时用,因为软件触发是想把传输计数器清零,循环模式是清零后自动重装,如果同时用的话,DMA就停不下来了。软件触发一般适用于存储器到存储器的转运。
硬件触发源可以选择ADC、串口、定时器等等,使用硬件触发的转运,一般都是与外设有关的转运,这些转运需要一定的时机,比如ADC转换完成、串口接收数据、定时时间到等等。
开关控制DMA_Cmd函数,当给DMA使能后,DMA准备就绪,就可以进行转运了。DMA转运的几个条件:
1.开关控制,DMA_Cmd必须使能
2.传输计数器必须大于0
3.触发源必须有触发信号
触发一次,转运一次,传输计数器自减一次,当传输计数器等于0,且没有自动重装时,这是无论是否触发,DAM都不会再进行转运了,此时就需要DMA_Cmd,给DISABLE,关闭DMA,再为传输计数器写一个大于0的数,再DMA_Cmd,给ENABLE,开启DMA,DMA才能继续工作。(不能在DMA开启时,写传输计数器,这是手册里的规定)
DMA请求:
这张图表示的是DMA的触发部分,DMA1的请求映像,7个通道。
每个通道都有一个数据选择器,可以选择硬件触发或软件触发,这个图不太好理解,因为一般数据选择器的侧边是输入选择控制位,图示的意思难道是EN给1,是硬件触发,EN给0是软件触发吗?显然不对,而且左边写的是软件触发,MEM2MEM位,难道M2M是软件触发吗,也不太好理解。所以看上面的PPT图才好理解。正确理解:EN位是开关控制,是决定这个数据选择器要不要工作,M2M选择1时软件触发。
可以看到每个通道的硬件触发源都是不同的,如果需要ADC1来触发的话,那就必须选择通道1,如果需要定时器2的更新事件来触发的话,那就必须选择通道2,剩下的同理,每个通道的硬件触发源都不同。
所以如果想使用某个硬件触发源的话,就必须使用它所在的通道,这就是硬件触发的注意事项。而如果使用软件触发的话,那通道就可以任意选择了,因为每个通道的软件触发都是一样的
优先级的判断类似中断的优先级。此处默认优先级是通道号越小,优先级越高
如果数据宽度一样,那就是正常的一个个转运,如果宽度不一样,那是怎么处理呢?
由表可知:如果目标的数据宽度比源端数据宽度大,那就在目标数据前面多出来的空位补0,否则就是把多出来的高位舍弃掉
这里的转运是一种复制转运,转运完成后DataA的数据并不会消失,
左边是ADC扫描模式的执行流程,有7个通道,触发一次之后,7个通道依次进行AD转换,然后转换结果都放到ADC_DR数据寄存器里面,我们要做的就是在每个单独的通道转换完成后,进行一个DMA数据转运,并且目的地址进行自增,这样数据就不会被覆盖了。
ADC扫描模式,在每个单独的通道转换完成后,没有任何标志位,也不会触发中断,所以程序不太好判断,某一个通道转换完成的时机是什么时候,但是根据UP的研究实战,虽然单个通道转换完成后,不产生任何的标志位和中断,但是应该会产生DMA请求,去触发DMA转运。这部分内容手册里没有详细描述。单个通道的DMA请求肯定是有的,不然实验就做不成了。
一般来说,DMA最常见的用途就是配合ADC的扫描模式,因为ADC扫描模式会有个数据覆盖的特征,或者可以说这个数据覆盖的问题是ADC固有的缺陷,这个缺陷使ADC和DMA成为了最常见的伙伴,ADC对DMA的需求是非常强烈的。像其他一些外设,使用DMA可以提高效率,是锦上添花的操作,但是不使用也可以,顶多是损失一些性能,但是这个ADC的扫描模式,如果不使用DMA,功能都会受到很大的限制。
代码编写:
查找表,字库数据可以把它们定义为常量,这样节省SRAM的空间,定义在Flash里面。地址由编译器确定,不同的程序,地址可能不一样,是不固定的,
外设寄存器的地址:地址是固定的,在程序里可以用结构体,很方便地访问寄存器。
如果想算某个寄存器的地址,就可以查手册计算一下,首先查这个寄存器所在外设的起始地址,然后再在外设的寄存器总表里,查一下偏移,起始地址+偏移,就是这个寄存器的实际地址,
如何知道ADC1->DR寄存器地址。在ADC1右键跳转到定义,
#define ADC1 ((ADC_TypeDef*)ADC1_BASE)
左边是一个强制类型转换,把ADC1_BASE转换为了ADC_TypeDef类型的指针,右边ADC1_BASE是ADC1的基地址,就是起始地址
ADC1的基地址就是APB2外设基地址+0x2400,APB2外设基地址就是外设基地址+0x10000,外设基地址就是0x4000 0000。基地址+偏移地址就是寄存器的实际地址,使用了非常巧妙的方法来实现这个偏移,那就是使用结构体,依次定义了各种寄存器,结构体成员的顺序和寄存器实际存放顺序是一一对应的。这就是STM32中,使用结构体来访问寄存器的流程
ADC1->DR 中ADC1是结构体指针,指向的是ADC1外设的起始地址,访问结构体成员,相当于加了一个地址偏移,起始地址+偏移,就是指定的寄存器,这里因为ADC1是一个结构体指针,所以要用->,也就是箭头这个符号来取成员。
步骤:
1.RCC开启DMA时钟,
2.直接调用DMA_Init,初始化各个参数,包括外设和存储器站点的起始地址、数据宽度、地址是否自增、方向、传输计数器、是否需要自动重装、选择触发源,还有通道优先级,开关控制,给指定的通道使能
恢复缺省配置
初始化
结构体初始化
使能
中断输出使能
DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber); DMA设置当前数据寄存器 这个函数是给传输数据寄存器写数据的,
DMA_GetCurrDataCounter DMA获取当前数据寄存器,这个函数返回传输计数器的值,
获取标志位状态
清除标志位
获取中断状态
清除中断挂起位
除了中间两个函数,其他的都是经典函数
键盘上的Alt键,然后按鼠标左键进行框选,就可以以方框的形式框选,如果不按Alt,就是一行一行连续运行的框选。
可以看到此时硬件外设,已经实现了相互配合,和高度的自动化,各种操作都是硬件自己完成的,极大地减轻了软件负担,软件什么都不需要做,也不需要任何中断,可以再加一个外设,用ADC单次扫描,再用定时器去定时触发,这样就是定时器触发ADC,ADC触发DMA,整个过程完全自动,不需要程序手动进行操作。这就是STM32中,硬件自动化的一大特色。各种外设相互连接,互相交织,不再是传统的一个CPU单独控制多个独立的外设,而是外设之间相互合作形成一个网状结构,这样在完成某些简单且繁琐的工作的时候,就不需要CPU来统一调度了,可以直接通过外设之间的相互配合,自动完成这些繁琐的工作,大大提高外设的性能