STM32学习笔记(8_1)- DMA直接存储器存取

发布于:2024-03-29 ⋅ 阅读:(20) ⋅ 点赞:(0)

无人问津也好,技不如人也罢,都应静下心来,去做该做的事。

最近在学STM32,所以也开贴记录一下主要内容,省的过目即忘。视频教程为江科大(改名江协科技),网站jiangxiekeji.com

本期开始学习DMA,直接存储器存取。DMA是一个数据转运小助手,它主要是用来协助CPU,完成数据转运的工作。

DMA简介

DMA这个外设,是可以直接访问STM32内部的存储器的,包括运行内存SRAM、程序存储器Flash和寄存器等等。DMA都有权限访问它们,所以DMA才能完成数据转运的工作。

下图这里外设,指的就是外设的寄存器。一般是外设的数据寄存器DR,Data Register,比如ADC的数据寄存器、串口的数据寄存器等等;这里存储器,指的就是是运行内存SRAM和程序存储器Flash,是我们存储变量数组和程序代码的地方。在外设和存储器或者存储器和存储器之间,进行数据转运,就可以使用DMA来完成。并且在转运的过程中,无须CPU的参与,CPU省下时间,就可以干一些其他的、更加专业的事情。搬运数据这种杂货,交给DMA就行了。

这个通道就是数据转运的路径,从一个地方移动到另一个地方,就需要占用一个通道。如果有多个通道进行转运,那它们之间可以各转各的,互不干扰。这就是DMA的通道。

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

存储器映像

既然DMA是在存储器之间进行数据转运的,那我们就应该要了解一下,STM32中都有哪些存储器以及这些存储器被安排到的起始地址。这里有个表,这个表就是STM32中所有类型的存储器。ROM就是只读存储器,是一种非易失性、掉电不丢失的存储器。RAM就是随机存储器,是一种易失性、掉电丢失的存储器。

先看ROM区,运行程序,一般也是从主闪存即Flash里面开始运行的,起始地址,也就是第一个字节的地址是0800这个。最终终地址是多少呢,这取决于它的容量,程序编到哪里,哪里就是最终地址。这就是主闪存的地址范围,你之后如果在软件里看到,某个数据的地址是0800开头的,那你就可以确定,它是属于主闪存的数据。

BootLoader程序是芯片出厂自动写入的,一般也不允许我们修改。

选项字节的位置是在ROM☒的最后面,你下载程序可以不刷新选项字节的内容,这样它里面的内容就不会变。选项字节里,存的主要是Flash的读保护、写保护,还有看门狗等等的配置。

再看RAM区,SRAM,地址开头是0x2000,也就是我们在程序中定义变量、数组、结构体的地方。

外设寄存器,也就是我们初始化各个外设,最终所读写的东西。它的存储介质其实也是SRAM。

内核外设就是NVIC和SysTick,内核外设和其他外设不是一个厂家设计的,所以它们的地址也是分开的。

DMA框图

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

使用DMA进行数据转运,就是从某个地址取内容,再放到另一个地址去

框图中,总线矩阵的左端,是主动单元,也就是拥有存储器的访问权。右边这些,是被动单元,它们的存储器只能被左边的主动单元读写。主动单元这里,内核有DCode和系统总线,可以访问右边的存储器。其中DCode总线是专门访问Flash的,系统总线是访问其他东西的。

可以看到DMA1有7个通道,各个通道可以分别设置它们转运数据的源地址和目的地址,这样它们就可以独立工作了。

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

DMA1里还有个AHB从设备,因为DMA作为一个外设,它自己也会有相应的配置寄存器,这里连接在了总线右边的AHB总线上。所以DMA,即是总线矩阵的主动单元,可以读写各种存储器;也是AHB总线上的被动单元。CPU可以通过AHB总线对DMA进行控制。

DMA请求,用手硬件触发DMA的数据转运。

总之就是CPU或者DMA直接访问Flash的话,是只可以读而不可以写的。然后SRAM是运行内存,可以任意读写,没有问题。外设寄存器的话,得看参考手册里面的描述。有的寄存器是只读的,有的寄存器是只写的,不过我们主要用的是数据寄存器,数据寄存器都是可以正常读写的。

DMA基本结构

 如果想编写代码实际去控制DMA的话,那这个图就是必不可少的了,上面的DMA框图只是笼统的介绍。

下图左边是外设寄存器(后面简称外设)站点,右边是存储器站点,包括Flash和SRAM。那在这里可以看到,DMA的数据转运,可以是从外设到存储器,也可以从存储器到外设,具体是向左还是向右,有一个方向的参数,可以进行控制。另外,还有一种转运方式,就是存储器到存储器。比如FIash到SRAM或者SRAM到SRAM,这两种方式。因为Flash是只读的,所以DMA不可以进行SRAM到Flash,或者Flash到Flash的转运操作。

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

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

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

最下面是DMA的触发部分,触发就是决定DMA需要在什么时机进行转运的。触发源,有硬件触发和软件触发,具体选择哪个,由M2M这个参数决定。M2M就是Memory to Memory,因为2的英文two和to同音,所以M2M就是M To M,存储器到存储器的意思。当我们给M2M位1时,DMA就会选择软件触发,这个软件触发并不是调用某个函数一次,触发一次,它这个软件触发的执行逻辑是,以最快的速度,连续不断地触发DMA,争取早曰把传输计数器清零,完成这一轮的转换。和我们之前外部中断和ADC的软件触发可能不太一样。那这里的软件触发和循环模式,不能同时用,因为软件触发就是想把传输计数器清零,循环模式是清零后自动重装传输计数器,如果同时用的话,那DMA就停不下来了。软件触发一般适用于存储器到存储器的转运,硬件触发源可以选择ADC、串口、定时器等等,使用硬件触发的转运,一般都是与外设有关的转运。硬件触发一般需要一定时机,比如ADC转换完成、串口收到数据、定时时间到等等,传一个信号过来,来触发DMA进行转运。

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

DMA转运有几个条件:第一,就是开关控制,DMA_Cmd必须使能;第二,就是传输计数器必须大于0;第三,就是触发源,必须有触发信号。触发一次,转运一次,传输计数器自减一次。当传输计数器等手0,且没有自动重装时,这时无论是否触发,DMA都不会再进行转运了。此时就需要DMA_Cmd,给DISABLE,关闭DMA,再为传输计数器写入一个大于0的数,再DMA_Cmd,给ENABLE,开启DMA,DMA才能继续工作。注意一下,写传输计数器时,必须要先关闭DMA,再进行,不能在DMA开启时,写传输计数器,这是手册里的规定。

DMA请求(即触发)

 请求源和通道都是一一对应的,也就是说通道几和外部的GPIO口是固定的。

因此使用硬件触发的话,只能根据外设选择特定的通道,比做如你要使用ADC,那会有库函数叫ADC_DMACmd,必须使用这个库函数开启ADC1的这路输出,它才有效。如果想选择定时器2的通道3,那也会有个TIM_DMACmd函数,用来进行DMA输出控制; 

而如果使用软件触发的话,那通道就可以任意选择了,因为每个通道的软件触发都是一样的。

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

数据宽度与对齐

DMA转运的源数据和目标数据不一样时,怎么处理?

如果你把小的数据转到大的里面去高位就会补0,如果把大的数据转到小的里面去那高位就会舍弃掉。

例子(数据转运+DMA)

那在这个任务里,外设地址显然应填DataA数组的首地址,存储器地址给DataB数组的首地址,然后数据宽度,两个数组的类型都是uint8_t,所以数据宽度都是按8位的字节传输。两个数组的位置一一对应,所以地址要自增。

那这里显然就是外设站点转运到存储器站点了,当然如果你想把DataB的数据转运到DataA,那可以把方向参数换过来。然后是传输计数器和是否要自动重装,在这里,显然要转运7次,所以传输计数器给7,自动重装暂时不需要。之后触发选择部分,选择软件触发,因为这是存储器到存储器的数据转运,是不需要等待硬件时机的,尽快转运完成就行了。那最后,调用DMA_Cmd,给DMA使能。

这样数据就会从DataA转运到DataB了,转运7次之后,传输计数器自减到0,DMA停止,转运完成,这里的数据转运是一种复制转运,转运完成后DataA的数据并不会消失。

例子(ADC扫描模式+DMA)

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

所以这里DMA的配置就是:外设地址,写入ADC_DR这个寄存器的地址;存储器的地址,可以在SRAM中定义一个数组ADValue,然后把ADValuef的地址当做存储器的地址;之后数据宽度,因为ADC_DR和SRAM数组,我们要的都是uint_16的数据,所以数据宽度都是16位的半字传输;地址是否自增,那从这个图里,显然是外设地址不自增,存储器地址自增;传输方向,是外设站点到存储器站点;传输计数器,这里通道有7个,所以计数7次;计数器是否自动重装,这里可以看ADC的配置,ADC如果是单次扫描,那DMA的传输计数器可以不自动重装,转换一轮就停止。如果ADC是连续扫描,那DMA就可以使用自动重装,在ADC启动下一轮转换的时候,DMA也启动下一轮的转运;最后是触发选择,这里ADC_DRI的值是在ADC单个通道转换完成后才会有效,所以DMA转运的时机,需要和ADC单个通道转换完成同步,所以DMA的触发要选择ADC的硬件触发,在ADC扫描模式下,虽然单个通道转换完成后,不产生任何标志位和中断,但是它会产生DMA请求,去触发DMA转运。这就是大致流程。

一般来说,DMA最常见的用途就是配合ADC的扫描模式,因为ADC扫描模式有个数据覆盖的特征,或者可以说这个数据覆盖的问题是ADC固有的缺陷,这个缺陷使ADC和DMA成为了最常见的伙伴。ADC对DMA的需求是很强烈的,DMA对其他外设可能只是锦上添花。

两个程序现象 

DMA数据转运

在这个程序里,我们将使用DMA,进行存储器到存储器的数据转运。也就是把一个数组里面的数据,复制到另一个数组里。这里先定义了一个数组DataA,里面存的是1、2、3、4,作为待转运的源数据。然后下面再定义一个数组DataB,里面存的是4个0,作为转运数据的目的地。之后我们将会写一个模块,叫MyDMA。然后MyDMA初始化,把源数组和目的数组的地址传进去,再传入转运数据的长度4,接着执行主循环的流程。

第一步,自增,变化一下源数组DataA的测试数据。

第二步,显示一下DataA和DataB,然后延时一秒,方便观看。

第三步,调用一下MyDMA_Transfer函数,使用DMA进行数据转运。和主程序里直接使用for循环,使用CPU一个个手动地转运数据,效果是一样的。

接着最后,再显示一下DataA和DataB,看一下数据是不是从DataA转运到了DataB。

这里第一行是DataA,右边是DataA数组的地址。第二行就是DataAl的源数据了,每隔两秒变一次。第三行是DataB,右边是DataB数组的地址。最后一行,是DataB的目的地数据。

可以看到,DataA每变一次,Delay1秒后,数据就转运到了DataB。这个转运过程,就是由DMA来完成的。你也可以定义100个、1000个等等数据,然后使用DMA来进行转运,都是可以的。这是第一个程序的现象。

DMA+AD多通道

用ADC的扫描模式来实现多通道采集,然后使用DMA来进行数据转运。最终,AD转换的数据就会直接自动地跑到我们定义的数组里面来,之后我们就只需要用OLED显示一下就行了。看上去就很方便。

这个硬件电路和程序现象和上一节的AD多通道都是一模一样的,也是测量PA0~PA3这4个通道的模拟量。就只是在STM32端,使用了扫描模式,并且加了DMA转运数据。

本文含有隐藏内容,请 开通VIP 后查看