DMA数据转运+AD多通道

因为数据转运都是在STM32内部进行的,加一个led显示就行了



好,那在正式开始 DMA 代码之前,我们先花一段时间做个小实验啊,来验证一下这个存储器映像的内容,看一下我们定义的数据,它到底是不是真的存在了这相应的地址区间里

我们回到代码啊,在这上面先随便定一个变量啊,UINT8 杠 T a 等于 0X660X66,是一个十六进制的数啊,然后在主函数里 OLEDShow Hex number,一行一列显示 A A 长度为 2,这就是正常的定义变量引用变量啊,然后我们再显示一下这个变量的地址看看,也是十六进制显示地址,一般都习惯用十六进制表示啊,接着二行一列显示取地址 A A 这个与号放在一个变量前面啊,表示取这个变量的地址,
然后这个变量取地址之后应该要存在一个指针变量里,这里如果直接想当做一个数字显示的话,还得在前面加上强制类型转换括号,用 int 32 杠 t,如果不加强制类型,下班就是指针跨级复制的,编译的时候会给你报个警告啊。最后显示长度为 8,8 个十六进制的数,表示的是 32 位,32 位的系统地址都是 32 位的啊,有关指针和地址还不会的话,可以看一下我空间里的指针教程啊

那我们看现象编译下载看一下,现在第一行显示的是 AA 变量的内容,66 没问题,第二行显示的就是 AA 这个变量,它被存储的地址,可以看到这个地址是 200000000,地址是 20 开头的

那对照 PPT 的这个表就知道了,A A 这个变量存储的位置是 S RAM 区,在 S RAM 区它的地址肯定是 20 开头的,那它的具体地址是多少呢?这个是由编译器来确定的,目前 S RAM 区还没什么东西,所以编译器就把这个变量放在了 S RAM 区的第一个位置,也就是是二零零零零零零零。

那回到代码,我们可以在这个变量前面加一个 const 的关键字,const 的是 C 语言的关键字啊,表示的是常量的意思,被 const 的修饰的变量在程序中只能读不能写,那我们上一小节说了,Flash 里面的数据也是只能读不能写的,所以 const 和 Flash 就联系起来了,在 STM32 中,使用 const 的定义的变量是存储在 Flash 里面的,当然这里就不应该说是变量了,而应该说是常量,因为它是不能变的,这个变量的值只能在定义的时候给,如果你在程序这里尝试再次给它赋值,那就会报错啊,错误,意思就是不能给 const 的常量赋值


那我们下载看一下,在这里就可以看到 AA 这个变量的地址就变成 0800 开头的了

在 PPT 的表格里可以知道,现在 AA 是被存储在了 Flash 里,在 Flash 里存储的是程序代码,当然还有常量数据啊,这里没写出来,那这里的地址尾部有些偏移啊,不像 S RAM 里那样直接安排在第一个位置,这是因为 Flash 里还有程序代码,这些东西放在了前面,所以编译器给这个常量安排的地址就相对靠后了一些,

这就是定义变量和常量的方法,正常情况下我们使用的都是变量啊,直接定义就行,不需要加 const 的,那什么时候需要定义常量呢?这个是当我们程序中出现了一大批数据,并且不需要更改时,就可以把它定义成常量,这样能节省 S RAM 的空间,比如查找表,字库数据等等,
我们可以打开这个 O L E D font,点 h 文件,这里面就是 O L E D 显示英文的字库,这是一个数组哈,它里面的数据决定了每个字符应该显示哪些像素点,这个数组非常的长哈,而且是不需要更改的,所以在这里就可以加一个 const,把它定义在 Flash 里面,这样就可以节省 SRAM 的空间,这里如果你一不小心把这个 const 去掉了,那程序功能并不会有任何影响,但是 SRAM 里会有和这个数组一样大的空间被浪费掉了,如果数组很小,那影响也不大,如果数组很大,那就得考虑一下 SRAM 是不是消耗的起了,这就是 const 的关键字的用处,如果你有个很大的查找表或者字库,最好加个 const。

好,那我们继续,接下来我们再研究一下外设寄存器的地址,这些东西先删掉啊,那对于变量或者常量来说,它的地址是由编译器确定的,不同的程序地址可能不一样啊,是不固定的
对于外设寄存器来说,它的地址是固定的,在手册里都能查得到啊,在程序里也可以用结构体很方便的访问寄存器,比如要访问 ADC1 的 DR 寄存器,就可以写 ADC1 杠大于号 DR,这样就可以访问 ADC1 的 DR 寄存器了

我们把这个寄存器放在这里显示一下地址看看,给你看一下可以看到 ADC1 的 DR 寄存器

它的地址是 4001244C,是 40 开头的,对照 PPT 的表可以知道它确实是外设寄存器的区域哈,这个具体地址 4001244C 是固定的哈

在手册里也可以查到,我们看一下,首先打开第二张看一下这个存储器映像找一下,可以看到 ADC1 的起始地址是 40012400 嗯

接着再打开 ADC 的章节,看一下最后面这个寄存器总表找一下,可以看到 ADC DR 寄存器的地址偏移是 4C

之前 ADC1 的起始地址是 40012400,DR 偏移是 4C,所以 ADC1 的 DR 地址就是 4001244C,和我们 OLED 这里显示的是一样的哈
所以如果你想算某个寄存器的地址,就可以查手册计算一下,首先查下这个寄存器所在外设的起始地址,然后再在外式的寄存器总表里查下偏移,起始地址加偏移就是这个寄存器的实际地址。
那回到代码,我们再来研究下这个 ADC1 杠大于号 DR 这个东西是如何知道 ADC1DR 寄存器的地址的呢,这种结构体的方式又是如何访问到寄存器的呢

我们可以在 ADC1 处右键跳转到定义,可以看到 ADC1 就是这个东西,左边是一个强制类型转换,把 ADC1 base 转换为了 ADC type 底范类型的指针,右边 ADC1 base 就是 ADC1 的基地址哈,基地址就是起始地址的意思,也就是我们刚才查表看到的 40012400 哈,

转到定义看一下,ADC1 基地址就是 APB2 外设基地址加上 0X2400,

再转到定义 APB2 外设基地址,就是外设基地址加 0X10000,

再转到定义外设基地址就是 0X40000000,可以看到上面还有 SRAM 基地址啊,是 2000,Flash 基地址是 0800,和我们之前讲的都是一致的啊

那这里回过来看,外设基地址是 40000000,加上 100000 等于 40010000,是 APB2 外设的基地址,再回过来再加 2400,等于 40012400,就得到了 ADC1 的基地址,也就是手册表里写的那个哈,再回过来,现在基地址有了,但是基地址加偏移才是寄存器的实际地址,
在这里啊,他使用了一个非常巧妙的方法来实现这个偏移,就是使用结构体来实现,我们跳转到结构体的定义,

可以看到这里是依次定义的各种寄存器,这个结构体成员的顺序哈,看一下手册和这个寄存器实际存放的顺序是一一对应的,

SR 、CR1、CR2等等,在结构体里面也是 SR 、CR1、CR2等等。所以说回到这里,如果我们定义一个 ADC 结构体的指针,并且指针的地址就是这个外设的起始地址,那这个结构体的每个成员就会正好映射实际的每个寄存器,
画个内存图,就是在内存的 40012400 开始的位置,存放的是 ADC1 的寄存器,存放顺序是 SR、CR 1、CR 2等等,我们定义 ADC type 底这样一个结构体,那它在内存的存储情况啊,也是 SR、CR 1、CR 2等等这样来存储的,如果我指定这个结构体的起始地址就是 ADC1 外设寄存器的起始地址,那么这个结构体的内存和外设寄存器的内存就会完美重合,我再访问结构体的某个成员就相当于是访问这个外设的某个寄存器了,这就是 STM32 中使用结构体来访问寄存器的流程。

回到这里,现在这个 ADC1 杠大于号 DR 你就应该明白它是啥意思了吧,ADC1 是结构体指针,指向的是 ADC1 外设的起始地址,访问结构体成员就相当于是加一个地址偏移,起始地址加偏移就是指定的寄存器,这里因为 ADC1 是一个结构体指针啊,所以要用杠大于号也就是箭头啊这个符号来取成员,
不得不说 STM32 这个库函数啊,把访问一个寄存器做的还是非常麻烦的哈,其实如果你想简单点的话,直接用指针访问某个物理地址就行了,比如定义一下 define ADC1,一下划线 dr,它是 0X4001244C,然后把这个地址强转为一个 Unit 32 杠 t的指针。之后使用星号取 ADC1DR 指针内容,这样也是可以访问 ADC1 的 DR 寄存器哈,和这个结构体访问的效果是一模一样的,


好到这里有关这个存储器地址常量和变量,结构体访问寄存器啊这些知识点我就讲完了,算是给大家扩展了一些内容啊,那接下来我们回到正题看一下。
来我们回到正题,看一下 DMA 的配置。把这些先删掉。首先我们定义一下 DMA 转用的源端数组和目的数组。第一个, UINT 8-7, Data A, 这个是源端数组,里面的数据是 0X01,0X02,0X03,0X04。目前总共有 4 个数据啊。当然实际情况可能会有成千上万个数据,这样才能发挥出 DMA 转用的优势啊。目前测试的话,就先给 4 个就行了。然后第二个数组, UINT 8-题 A 还 B 里面的数据是 0 0 0 0。当然如果你不写 0 的话,全局变量默认也是 0 啊。接下来我们的任务就是初始化 DMA,然后让 DMA 把这里 Data A 的数据转运到 Data B 里面去。这就是我们第一个代码的任务。

那我们还是先给 DMA 建一个模块啊。这里 DMA 不涉及外围硬件电路,所以可以在 System 里添加。在这里右键快速添加下模块。




DMA数据转运
在这里面我们将初始化 DMA,初始化的步骤啊,我们还是看一下 PPT 的这个基本结构图,初始化第一步,RCC 开启 DMA 的时钟,这个没问题啊,
第二步就可以直接调用 DMA IT 的初始化这里的各个参数了,包括外设和存储器站点的起始地址,数据宽度地址是否自增方向,传输寄存器是否需要自动重装,选择出发源,当然还有个通道优先级哈,这里没画出来,那这所有的参数通过一个结构体就可以配置好了,之后就可以开关控制 DMA cmd 给指定的通道使能就完成了,那在这里如果你选择的是硬件触发,不要忘了在对应的外设调用一下叉叉叉 DMA cmd 开启一下触发端,如果你需要 DMA 的中断,那就调用 DMA IT config 开启中断输出,再在 NVIC 里配置相应的中断通道,然后写中断函数就行了,中断的配置啊,各个外设都一样,这里结构图我暂时没有画中断的部分啊,最后在运行的过程中,如果转运完成,传输寄存器清 0 了,这时再想给传输寄存器赋值的话,就 DMA 失能,写传输寄存器 DMA 使能,这样就行了,这就是 DMA 的编程思路,

接下来我们看一下 DMA 的库函数哈,找一下打开 DMA 点 c 去放到最后函数就这么多,看上去应该是比较简单的哈,

我们看一下 DMA_DeInit:恢复缺省配置,DMA_Init:初始化使的,DMA_StructInit:结构体初始化 ,DMA_Cmd: 使能,DMA_ITConfig:中断输出使能,这些都是经典函数啊,应该已经很熟悉了吧,
然后接下来 DMA_SetCurrDataCounter:设置当前数据寄存器,这个函数就是给这个传输寄存器写数据的哈,

之后DMA_GetCurrDataCounter:获取当前数据寄存器,这个函数就是返回传输寄存器的值,如果你想看看还剩多少数据没有转运,就可以调用这个函获取一下传输寄存器,这样就行了。
然后最后四个函数,DMA_GetFlagStatus:获取标志位状态,DMA_ClearFlag:清除标志位,DMA_GetITStatus:获取中断状态,DMA_ClearITPendingBit:清除中断挂起位,这些也是经典函数哈,不用多说
那到这里 DMA 的库函数就结束了,除了中间这两个函数,剩下的全是经典函数哈,应该说还是比较好理解的,

然后我们来开始写代码,第一步,开启时钟 RCC AHB 外设时钟控制 DMA 是 AHB 总线的设备哈,所以要用 AHB 开启时钟的函数,我们找到定义看一下第一个参数的解释在这里啊,这里说的是对于互联型设备,这个参数可以是下面这一组的组合,

对于其他设备,这个参数是这下面的组合,互联型是 STM32F105107 的型号哈,我们这个是 F103,所以我们在下面这个参数表里选,、

很简单,选择这个 DMA1 的参数就行了,复制放到这里,第二个参数以 enabel 开启 DMA 一的时钟,

然后接下来 DMA 一例的初始化,DMA 先定义初始化结构体啊,DMA 一例的 type,定义 DMA 一例的 structure 之后把结构体成员全部引出来,这里成员比较多啊,我们重新排个序好了,现在看一下,其实已经不难理解了啊,前面六个成员外设站点的起始地址数据宽度是否自增,存储器站点的起始地址数据宽度是否自增,对应的就是图里的这六个参数,


之后 dir 传输方向,buffer size 缓冲区大小,其实就是传输寄存器哈,mode 传输模式其实就是是否使用自动重装,m2m 选择是否是存储器到存储器,其实就是选择硬件触发还是软件触发哈,最后 priority 优先级,这个按照参数要求给一个优先级就行了哈,那对应这个图从上到下每个参数都是可以用结构体来配置的,这就是初始化结构体的参数,
然后 dma 一例的函数转到定义看一下哈,这里第一个参数就不只是 DMA 几了,这里写的是第一个参数是 DMA Y _Channel X,其中 Y 可以是一或二,用来选择是哪个 DMA, X 可以是一到七对于 DMA 一,或者可以是一到五对于 DMA 二,可以选择是哪一个通道,所以这里 DMA 一例的第一个参数既选择了是哪个 DMA,也选择了是 DMA 的哪个通道,
那么复制这个 DMA Y 千楼 X 放到这里,然后 Y 改成一选择 DMA 一,X 选择通道,这里因为是存储器到存储器的转,用的是软件触发,所以通道可以任意选择哈,这里就给通道一吧,之后复制一下结构体的名字,把结构体的地址放在第二个参数,这样就是把结构体指定的参数配置到 DMA 一的通道一里面去,


那接下来我们看一下结构体的参数啊,第一个是外设站点的基地址,在这里要写一个 32 位的地址哈,比如 0X20000000 这样的地址,对于 SRAM 的数组,它的地址是编译器分配的,并不是固定的,所以我们一般不会写绝对地址,而是通过数组名来获取地址,那这里我们就把这个地址提取成初始化函数的参数,这样在初始化的时候你想转运哪个数组,就把哪个数组的地址传进来就行了,所以这里 void 改成 int 32 杠 t ADDRA,然后把 ADDRA 放在这里,这样外侧站点的地址就完成了,

之后数据宽度跳转定义看一下介绍是指定数据宽度,这个参数可以是这里面的一个值,Ctrl F 搜索一下,这里可以看到有 Byte 字节就是 Unit 8 杠 t,HalfWord 半字就是 Unit 16 杠 t,Word 字就是 Unit 32 杠 t,那这里我们想要以字节的方式传输,所以就复制第一个,然后放到这里,



接着地址是否自增跳转定义看一下解释,是指定外设地址是自增或者不是参数取值,搜索一下,这里有两个选择,第一个自增 enable 就是自增,第二个自增 disable 就是不自增,根据上一小节的分析啊,我们这种数组之间的转运地址肯定是要自增的了,所以复制第一个放到这里,



这样外侧站点的三个参数就配置完成了,之后存储器站点的三个参数也是类似的东西啊,第一个存储器站点的基地址我们也把它提取成参数,在这里啊,加一个参数,Unit 三二杠七 ADDRA B,然后把 ADDRA B 放在这里,作为重组器站点的起始地址,

接着数据宽度转到定义哈,搜索一下这个参数列表,我们也选择这个 Byte 参数,以字节传输,然后放到这里,



接着继续存储器站点地址自增转到定义,搜索一下参数列表哈,这里也选择地址自增,复制放到这里,这样外设站点和存储器站点的参数就配到了,



然后继续看 dir 转到定义,看一下解释是指定外设站点是源端还是目的地,参数取值,搜索一下,这里有两个参数啊,第一个是外设站点作为 d s t destination 目的地,外设站点作为目的地,其实就是传输方向是存储器到外设站点这样来传输的,第二个是外设站点作为 S R C source 源头,也就是外设站点到存储器站点的传输方向,那我们打算是把 data A 放在外设站点,data B 放在存储器站点,传输方向就是外设站点到存储器站点,所以这里选择第二个参数,外设站点多为数据源,复制放到这里,这就是传输方向,



接着 buffer size 转到定义,看一下,这里解释是以数据单元指定缓冲区的大小,这个数据单元等于外设数据宽度或者存储器数据宽度,取决于传输方向啊,这个话有点难理解哈,以数据单元指定缓冲区大小,就是说你要传送几个数据单元,
这个数据单元等于你传出源端站点的 data size,说简单点就是 buffer size,就是传输寄存器哈,指定传输几次,这个你可以看一下 DMA 一例的函数的源码哈,这个 buffer size 参数其实就是直接赋值给它传输寄存器的寄存器它的取值是 0 到 65535,那回到这里,我们把这个 buffer size 也提取到函数的参数里来,加一个 uint 16 杠 t size,然后把 size 放到这个 buffer size 参数这里,这样传输次数就完成了。


之后 mode 指定传输寄存器是否要自动重装,转到定义看一下,这里解释是指定操作方式,下面这里是参数取值列表,然后下面还有一个 note 注意事项啊,写的是循环模式,也就是自动重装哈,不能应用在存储器到存储器的情况下,也就是我们上一小节说的自动重装和软件触发不能同时使用,如果同时使用,DMA 就会连续触发哈,永远也不会停下来,那搜出来参数列表,这里有两个参数,第一个是循环模式,就是传输寄存器自动重装,第二个是正常模式,就是传输寄存器不自动重装,自减到 0 后停下来,这里我们转移数组,是存储器到存储器的传输,转用一次停下来就行了,所以选择第二个正常模式放到这里



之后 F2M 转到定义看一下,这里解释是 DMA 是否应用于存储器到存储器的转用存储器到存储器的模式啊,就是软件触发啊,搜索一下参数表,这里第一个 m2m enable 就是使用软件触发,第二个 disable 就是不使用软件触发,也就是使用硬件触发,我们转移数组,所以选择第一个复制放到这里,



然后继续看最后一个参数 priority 转到定义,解释是指定通道的软件优先级,看一下参数取值表哈,这里有四个优先级,第一个是 very high 非常高,第二个是 high 高,第三个是 medium 中等,第四个是 low 低,这个优先级哈,如果你有多个通道的话,可以指定一下,确保紧急的转运有更高的优先级。目前一个通道,那优先级就随便了,这里就选择一个中等优先级复制放到这里,这样优先级就配置好了,



好,到这里这个 DMA 的参数就配置完成了,这个参数虽然比较多哈,但是对照着框图来理解的话应该还是不难的,那到目前为止 DMA 还暂时不会工作,DMA 转运有三个条件,
第一个条件传输计数器大于 0,
第二个条件触发源有触发信号,
第三个条件 DMA 使能,三个条件缺一不可,
目前如果传一个大于 0 的 size 的话,第一个条件满足,触发源为软件触发,所以一直都有触发信号,第二个条件满足,最后一个条件 DMA 还没有使能,第三个条件不满足,所以到目前为止 DMA 还不会工作,如果你想在初始化之后就立刻工作的话,可以在这最后加上 DMA cmd 参数,第一个 DMA1 通道一,第二个 enable 使能 DMA,之后三个条件满足,DMA 就会进行数据转运了,转运一次,传输计数器自减一次,当传输计数器减到 0 之后,转运完成,同时第一个条件就不满足了,转运停止,这样就完成了一次数组之间的数据转运,

我们现在就可以来试一下看看啊?那先把这个函数放在头文件声明一下。

然后回到 main 点 c 里面来 1 库路的买 DMA.H 然后在主循环之前调用My data_ init的这些编一下啊要不然没有参数提示然后看一下参数第一个是源端地址这里直接写 data a 给它传入 data a 数组的首地址因为数组名就是地址所以就不需要再加取地址符号了,然后强制类型转换转换成 Unit 32-t 类型的数据这样就行了
之后是 data b 目的地址也强制类型转换为 UINT32 杠 t最后是传输次数这里有 4 个数据所以传输 4 次这样就完事了当调用完这个函数之后 DMA 就会立刻工作把 Data A 数组的数据转运到 Data B 里面去那我们用 OID 显示看一下哈
首先转运之前我们显示一下 Data A 和 Data B 的内容 OID show hex number 一行一列显示 DataA0 长度为 2 之后复制一下
一行四列显示 DataA1
一行七列显示 DataA2
一行十列显示 DataA3
这是在第一行显示 DataA 的数据哈
然后继续复制显示一下 Data B 把行号都改成 2 啊这里给大家介绍一个小技巧先按住键盘上的 Alt 键然后按鼠标左键进行框选这样就可以像这样以方框的形式框选如果不按 Alt 键那就是一行一行连续的框选所以这里可以按住 Alt 键把一框选起来然后改成二这样改的就非常快哈
之后 Data A 也都改成 Data B 这样就是在第二行显示 Data B 的
数据接下来在转移之后我们再再显示一下 Data A 和 Data 把显示部分复制下来第三行显示转运后的对它 A 第四行显示转运后的对它 B 这样测试程序就完成了我们来试一下啊编译没问题



在这个屏幕上前两行是转运前的 DataA 和 DataB, 可以看到 DataA 是 1234, DataB 是 0000。之后调用 My_ DMA_ Init 初始化 DMA, 并且初始化之后就立刻开始了转运。转运之后可以看到 DataA 和 DataB 的数据都变成了 1234,这说明 DataA 的数据成功的转运到了 DataB。 在整个过程中,源端数据 DataA 是不会变化的,这就是目前程序的现象。
接着我们继续来写这个代码啊,现在我们是初始化之后立刻就进行转运并且转运一次之后 DMA 就停止了,如果 DataA 的数据又变化了我们想再转运一次那该怎么办呢?这时我们就需要给传输寄存器重新复制了。我们可以在初始化之后再写一个函数啊, void My_ DMA_ Transfer void 的 DMA 传输函数。
调用一次这个函数,就再次启动一次 DMA 转运。在里面我们需要重新给传输寄存器赋值。传输寄存器赋值必须要先给 DMA 使能,所以这里复制一下 DMA cmd 函数,把已 label 改成 disable, 然后就可以给传输寄存器赋值了。我们看一下库函数,复制这个 DMA set current data counter 放在这里。参数第一个是 DMA y 迁入 x,选择 DMA 和通道,那我们复制一下上面这个就行了。第二个是你指定要给传输寄存器写入的值,这里我们需要获取一下初始化这里的 size 参数啊,但是它俩不在一个函数,不能直接传递过来,所以我们可以在这上面定义一个全局变量,用 int 16 杠 t 买 DMA size, 然后初始化的时候把 size 往这个全局变量也存一份。

之后在这个函数里就可以使用全局变量的买 DMA size 了,这样就可以重新给传输寄存器赋值了。最后再次复制这个 CMD 函数。参数给 enable,这样 DMA 传输的三个条件又重新满足了, DMA 就会再次开始转运。然后上面这里我们先给 disable 吧,不让 DMA 触发之后就立刻进行转运哈,而是等调用了 transfer 函数之后再进行转运,调用一次转运一次这样来工作。

当然在转运开始之后我们还需要做一个工作,就是等待转运完成,因为转运也是要花一些时间的哈。那在这里等待转运完成我们可以找一下 DMA 的函数,复制这个 DMA 该 the flag status 函数查看一下标志位,然后放到这里。总共就一个参数哈,转到定义看一下。这里有这么多的标志位可以查看,总共就是四种标志位哈。第一个是全局标志位,第二个是转运完成标志位,第三个是转运过半标志位,第四个是转运错误标志位。然后所有的通道都是这四种标志位啊。这里我们需要检查 DMA1 通道 1 转运完成的标志位,所以选择这个 DMA1 flagTC1 参数复制。

放到这里,转运完成之后标志位置一,所以我们需要加一个 while 循环,等待这个标志位等于等于 reset。如果没有完成,就一直循环等待,这样就实现了等待转运完成的效果了。标志位置一之后,不要忘了清除标志位哈,这个标志位是需要手动清除的,所以使用这里的 DMA 可立压 12 个函数,放到这里。参数和上面的一样哈,这样就行了。

好到这里我们这个模块就全部写完了。然后我们测试一下。先把这个穿字符函数放到头文件声明一下。

然后在 main 点 c 里,我们来实现这个代码的最终功能。首先先显示一些基本信息哈,在这里 o r d 修 string 一行一列显示 Data A,复制一下,三行一列显示一下 Data B,然后再显示一下它们的地址看看,复制一下, OID, show hex number,一行八列,显示 Data A,强转为, Unit 的三二杠 t。 长度为八,三行八列,显示 Data B 的地址,八位。好,这些是基本的信息,


然后显示数据的位置,换一下哈,在第二行显示 Data A 的数据。第四行显示 Data B 的数据。把买 DMA 初始化放在前面,下面这些先删掉。现在先看一下现象哈,

编译,下载,看一下

目前显示的是 Data A, 地址是二零零零零零零二啊,现在可能是模块里的那个 size 变量被分配到了 0 的位置啊,所以这个地址就往后偏移了两个字节。然后数据是一二三四, A 它 B 地址是二零零零零零零六,数据是零零零零,现在还没有进行数据转移哈,
之后回到代码,我们在主循环里测试。先变换一下 Data A 的原端测试数据,都给它们自增一。之后把显示部分挪到这里,显示一下 Data A 和 Data B。

抵了一秒,看一下转运前的 Data A 和 Data B 啊。然后调用买 DMA transfer 函数,开始转运。函数执行完成后,转运就结束了。然后再复制一下显示函数,看一下转运后的 Data A 和 Data B。 再抵类一秒方便观看啊。

这样程序就完成了。我们编译一下来看一下。可以看到现在的现象就是我们最开始演示的那样啊。 Data A 的数据先变换一下,然后转运, Data B 的数据就和 Data A 相同了。这就是 DMA 转运数据的效果。
主次增加然后转运:


如果你想把 Flash 的数据转运到 SRAM 里的话,可以在这个 Data A 前面加一个 const,

把 Data A 定义在 Flash 里面。那下面这里 Data A 加加哈就不能要了,因为 const 的数据不能重新更改。

然后编译下载看一下。此时可以看到 Data A 的地址是 08 开头的,说明 Data A 是 Flash 里面的数据,下面 Data B 的数据和 Data A 一样,说明 Data A 的数据成功转运到 Data B 里面来了。好,这就是第一个程序的全部内容了。
ADC+DMA运用



到 ad 点 c 里面来,在这里我们给它加上 DMA 转运数据的功能,那 DMA 的配置都差不多了哈,我们回到上一个代码,把这个 DMA 的初始化部分全部都复制下来,然后在这里 ADC 使能之前粘贴,

把这个开启 DMA 时钟的函数啊挪到前面去开启时钟我们都放在一起

那这样我们 ADC 和 DMA 初始化的代码就都有了,接下来我们来修改下参数,把 ADC 和 DMA 给配合起来,这里代码我们使用的是 ADC 的扫描模式加 DMA 数据转运执行的流程哈,就是 PPT 最后一页这样的,首先我们要扫描 PA0 到 PA3 这四个通道,所以点菜菜单这里复制一下,放在这里复制四份哈,点四个菜,其中通道 0 放在序列一的位置,然后通道一放在序列二,通道二放在序列三,通道三放在序列四,这样菜单就点好了,
菜单上的一到四号的空位,我填上了零到三这四个通道,当然这个通道和次序啊,你可以任意修改,这样的话最终结果存放的顺序也会相应变化哈,

点菜完成之后继续往下看,ADC 这里扫描模式这个参数要改成一 enable,告诉厨师我点了多个菜,你不要只盯第一个菜看,然后 number of channel 改成四,告诉厨师我点的是四个菜,你看前四个位置就可以了,连续模式这里可以是连续扫描哈,也可以是单次扫描,这个我们等会都演示一下看看啊,这里先用单次扫描,到这里 ADC 扫描模式就配置完成了,

之后我们来配置下 DMA,这里 DMA 你可以想象成服务员 ADC 这个厨师把菜做好了,DMA 这个服务员要尽快把菜端出来,防止被覆盖,看一下 DMA 的参数啊第一个外设基地址这里是端菜的源头,厨师把菜做好就放在 ADC DR 寄存器里,所以端菜的源头地址就填 ADC DR 的地址,之前我们也算过哈,ADC1 的低压寄存器地址就是 0X4001244C,所以可以这样来填,
不过我们一般都不自己算哈,因为库函数已经帮我们算好了,所以这里可以这样写,ADC1 箭头 DR 取地址再强转为 Unit 32 杠 t 这样得到的结果其实就是刚才写的 4001244C 哈,那这样外设地址就填好了之后数据宽度我们想要DR寄存器低 16 位的数据,所以数据宽度就是 half word,以半字 16 位来转运,外设地址自增给 disable,不自增始终转运同一个位置的数据接下来存储器站点,存储器基地址也就是端菜的目的地啊,
我们想要把数据存在 sram 数组里,所以我们先在前面定一个数组,Unit 16 杠 t AD value 4 然后在这里把 AD value 数组的地址作为目的地,把数组的地址强转为 Unit 32 杠 7 这样就行了,


之后数据宽度也是半字 half word 地址是否自增给 disable 存储器的地址是自增的哈,每转运一次挪一个坑到这里 DMA 的远端和目的地的参数就配置好了,

之后传出方向外设站点是源没问题,传输数量给四个,因为有四个 ADC 通道,所以传输四次传输模式,这个可以给正常的单次模模式啊,也可以给自动重装的循环模式,这里先给正常模式啊,等会可以都试试啊,然后 M2M 要给 disable 不使用软件触发,我们需要硬件触发,触发源为 ADC1,厨师每个菜做好了叫我一下,我再去端菜,这样才是合适的时机,最后所有的参数都配置到 DMA1 的通道一里面去,这里通道就不能任意选择了啊,我们看一下 PPT 的,这里可以看到 ADC1 的硬件出发是只接在了 DMA1 的通道一上,所以这里通道必须要使用 DMA1 的通道一,其他的通道都不行,接着 DMA cmd,这里可以直接使能哈,

这时 DMA 转运的三个条件,第一个传输寄存器不为 0 满足,第三个 DMA 使能满足,但是第二个触发源有信号,目前是不满足的,因为这里是硬件出发,ADC 还没启动,不会有触发信号,所以这里 DMA 使能之后不会立刻工作,
最后在 ADC 使能之前还有一个事情需要做,就是开启 ADC 到到 DMA 的输出,这个我们上一小节说过哈,这里有三个硬件触发源,具体使用哪个取决于把哪个的 DMA 输出给开启了,那这里我们找下函数啊,打开 ADC 点 h 的库函数,可以看到这里啊有个 ADC DMA cmd,这个函数就是用来开启 DMA 触发信号的复制,我们把这个函数放到这里,参数是 ADC1 以enable,这样就行了,

到目前为止 ADC 和 DMA 配合工作的配置就完在哪,接下来看一下下面 ADC GetValue 参数和返回值都不需要了,函数里面因为现在 ADC 还是单次模式,所以还需要软件触发一下 ADC 开始,其他的不需要了,因为 DMA 也是正常的单次模式,所以在触发 ADC 之前需要再重新写入一下传输寄存器哈,我们到这里来复制一下这个代码,放在启动 ADC 之前,传输次数给 4,

最后等待 ADC 转换和 DMA 转运完成,这里因为转运总是在转换之后的,所以我们可以复制一下这里等待 DMA 完成的代码,然后放在这里等待 ADC 转换完成的代码就不需要了,

这样当我们调用一下 ad get value 函数,ADC 开始转换,连续扫描四个通道,DMA 也同步进行转运,ADC 转换结果依次存放在这上面的 ad value 数独里。好,我们来测试一下,先把这个 get value 放在头文件声明一下,另外这个 ad value 数组我们也放在头文件里声明一下,把这个数组作为一个外部可调用的数组,当然数组前面加一个 extern 声明一下啊,

那回到主函数,把这些删掉,

在主循环里先 ad get value 无参无返回值,之后数据就直接跑到 ad Value 数组里了,我们显示一下 ad Value 零是第一个序列通道零的转换结果,ad Value 一是通道一的结果,二是通道二,三是通道三,这样就完事了,

编译没问题,下载看一下,这里四个通道的转换结果就出来了,测试一下啊,也都是没问题的,

这就是 ADC 单次扫描加 DMA 单次转运的模式,那我们还可以配置成 ADC 连续扫描加 DMA 循环转运的模式,这样代码就会更加方便,我们在这里改一下,
ADC 连续模式打开,DMA 循环模式也打开,


最后把 ADC 触发直接放在初始化的最后一行,

当 ADC 触发之后,ADC 连续转换,DMA 循环转运,两者一直在工作,始终把最新的转换结果刷新到 SRAM 数组里,当我们想要数据的时候,随时去数组里取就行了,这样最后面的 get value 函数完全就不需要了,头文件这里删掉,主函数这里也删掉,


主循环啥都不干,直接读取 ad y 六数组就能得到结果,试下看看下载可以看到,这样也可以完成 ad 多通道转换的功能,

这就是 adc 连续扫描加 dma 循环转运的程序代码,可以看到此时硬件外设已经实现了相互配合和高度的自动化哈,各种操作都是硬件自己完成的,极大的减轻了软件负担,软件什么都不需要做,也不需要进任何中断,硬件自动就把活干完了。
另外这里你还可以再加一个外设哈,比如定时器,ADC 用单次扫描,再用定时器去定时触发,这样就是定时器触发 ADC,ADC 触发 DMA,整个过程完全自动,不需要让程序手动进行操作,节省软件资源,这就是 STM32 中硬件自动化的一大特色啊,各个外设互相连接,互相交织,不再是传统的这样一个 CPU 单独控制多个独立的外设这样的新型结构啊,而是外设之间互相连接,互相合作,形成一个网状结构,这样在完成某些简单且繁琐的工作的时候,就不需要 CPU 来统一调度了,可以直接通过外设之间的相互配合,自动完成这些繁琐的工作,这样不仅可以减轻 CPU 的负担,还可以大大提高外设的性能。
在我们之前的学习中也经常遇到过这样的设计哈,比如定时器的输出可以通向 ADC、DAC 或其他定时器,ADC 的触发源可以来自定时器或外部中断,DMA 的触发源可以来自 ADC、定时器串口等等,这就是这个 STM32 外设互相配合工作的特色。
好,到这里,本节课的内容就差不多了哈。本节课我们演示了 DMA 存储器到存储器,外设到存储器的情况,当然还有一个存储器到外设的情况啊,我们目前没有讲,比如串口发送一批数据,就可以使用 DMA 进行存储器到外设的转运,这个就留给大家以后自己去研究了啊。