DMA简介-stm32入门

1. DMA简介

1.1 基本概念

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

DMA 是一个数据转运小助手,它主要是用来协助 CPU,完成数据转运的工作。

DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源

外设指的是外设寄存器,一般是外设的数据寄存器 DR,Data Register,比如 ADC 的数据寄存器、串口的数据寄存器等等。
存储器指的就是运行内存 SRAM 和程序存储器 Flash,是我们存储变量数组和程序代码的地方。

12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道)

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

每个通道都支持软件触发和特定的硬件触发

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

所以存储器到存储器的数据转运,我们一般使用软件触发,外设到存储器的数据转运,我们一般使用硬件触发。

  • 特定的硬件触发:意思就是每个 DMA 的通道,它的硬件触发源是不一样的,你要使用某个外设的硬件触发源,就得使用它连接的那个通道,而不能任意选择通道。这个之后我们再详细分析。

STM32F103C8T6 DMA资源:DMA1(7个通道)

我们这个芯片只有 DMA1 的 7 个通道,没有 DMA2,这个注意数据手册。

1.2 存储器映像

既然 DMA 是在存储器之间进行数据转运的,那我们就应该要了解一下,STM32 中都有哪些存储器,这些存储器又是被安排到了哪些地址上,这就是存储器映像的内容。

计算机系统的 5 大组成部分是 运算器、控制器、存储器、输入设备和输出设备。其中运算器和控制器一般会合在一起,叫做 CPU,所以计算机的核心关键部分就是 CPU 和存储器。存储器又有两个重要知识点,一个是存储器的内容,另一个就是存储器的地址,那 STM32 也不例外。

DMA简介-stm32入门_第1张图片
这个表是 STM32 中所有类型的存储器和它们所被安排的地址,在 STM32 的数据手册里,也会有个存储器映像的图,此表是从图里总结出来的,都是一个意思。

在这个表里,无论是 Flash,还是 SRAM,还是外设寄存器,它们都是存储器的一种,包括外设寄存器,实际上也是存储器。

所以DMA 转运本质上都是存储器之间的数据转运,说成外设到存储器只不过是 STM32 它特别指定了可以转运外设的存储器而已。

在这个表里,存储器总共分为两大类:

  • ROM:只读存储器,是一种非易失性、掉电不丢失的存储器。其中 ROM 分为了三块:
    1. 第一块是程序存储器 Flash,也就是主闪存,闪存被分为了很多页;它的用途就是,存储 C 语言编译后的程序代码,也就是我们下载程序的位置;运行程序,一般也是从主闪存里开始运行的。最终终止地址取决于它的容量,编到哪里,哪里就是终止地址,这就是主闪存的地址范围。之后如果在软件里看到某个数据的地址是 0800 开头的,那你就可以确定,它是属于主闪存的数据。
    2. 系统存储器和选项字节,这两块存储器统称为信息块,也是 ROM 的一种,掉电不丢失,实际上它们的存储介质也是 Flash,只不过我们一般说 Flash 指的是主闪存 Flash,而不指这两块区域。这两款存储器的位置是在 ROM 区的最后面,BootLoader 程序存储的位置被分配到系统存储器,BootLoader 程序是芯片出厂自动写入的,一般也不允许我们修改;你下载程序可以不刷新选项字节的内容,这样选项字节的配置就可以保持不变,选项字节里存的主要是 Flash 的读保护、写保护,还有看门狗等等的配置,这个如果需要的话,可以了解一下。

闪存模块中还有闪存接口寄存器,这是外设的一部分,地址是 40 开头的,这个外设可以对闪存进行读写。

  • RAM:随机存储器,是一种易失性、掉电丢失的存储器。
    1. 首先是运行内存 SRAM,也就是我们在程序中定义变量、数组、结构体的地方,定一个变量,再取它的地址显示出来,那这个地址肯定就是 2000 开头的。类比于电脑的话,运行内存就是内存条。
    2. 接下来是外设寄存器,也就是我们初始化各个外设,最终所读写的东西。之前我们说了,外设寄存器也是存储器的一种,它的存储介质其实也是 SRAM,只不过我们一般习惯把运行内存叫 SRAM,外设寄存器就直接叫寄存器了。具体到每个外设,又有它们具体的起始地址,然后外设地址里面又可以具体细分到每个寄存器的地址、寄存器里的每个字节的地址,最终所有字节的地址,就都可以算出来了。
    3. 最后是内核外设寄存器,内核外设就是 NVIC 和 SysTick,因为内核外设和其他外设不是一个厂家设计的,所以它们的地址也是被分开的。

在 STM32 中,所有的存储器都被安排到了 0 到 8 个 F,这个地址范围内,因为 CPU 是 32 位的,所以寻址范围就是 32 位的范围,32 位的寻址范围是非常大的,最大可以支持 4 GB 容量的存储器,而我们 STM32 的存储器都是 KB 级别的,所以这个 4 GB 的寻址空间,会有大量的地址都是空的,目前地址的使用率还不到 1%。

0 地址实际上也是没有存储器的,它这里写的是别名到 Flash 或者系统存储器,取决于 BOOT 引脚。因为程序是从 0 地址开始运行的,所以这里需要把我们想要执行的程序,映射到 0 地址来,如果映射在 Flash 区,就是从 Flash 执行;如果映射在系统存储器区,就是从系统存储器运行 BootLoader;如果映射到 SRAM,就是从 SRAM 启动。怎么选择,由 BOOT1 和 BOOT2 两个引脚来决定,这就是 0 地址里的别名区

存储器包含两个位段区域,这两个位段区,映射了外设寄存器和 SRAM 中全部的位,这个位段区就相当于是位寻址,它把外设寄存器和 SRAM 中所有的位都分配了地址,你操作了这个新的地址,就相当于操作其中某一个位,因为 32 位的地址有 99% 都是空的,所以地址空间很充足,即使把每一位都单独编码,那也毫无压力,所以就存在了这样一个位段,用于单独操作寄存器或 SRAM 的某一位,位段区是另找了一个地方,开辟了一段地址区域。
其中 SRAM 位段区是 2200 开头的区域,外设寄存器的位段区是 4200 开头的区域,需要用的话了解一下。

1.3 DMA 框图

DMA简介-stm32入门_第2张图片

  1. 左上角是 Cortex-M3 内核,里面包含了 CPU 和内核外设等等,剩下的这所有东西,你都可以把它看成是存储器,所以总共就是 CPU 和存储器两个东西。
  2. Flash 是主闪存,SRAM 是运行内存,各个外设都可以看成是寄存器,也是一种 SRAM 存储器,寄存器是一种特殊的存储器,一方面 CPU 可以对寄存器进行读写,就像读写运行内存一样;另一方面,寄存器的每一位背后,都连接了一根导线,这些导线可以用于控制外设电路的状态,比如置引脚的高低电平、导通和断开开关、切换数据选择器或者多位结合起来,当作计数器、数据寄存器等等。所以寄存器是连接软件和硬件的桥梁,软件读写寄存器,就相当于在控制硬件的执行。既然外设就是寄存器,寄存器就是存储器,那使用 DMA 进行数据转运,就都可以归为一类问题了,就是从某个地址取内容,再放到另一个地址去。
  3. 我们看图,为了高效有条理的访问存储器,设计了一个总线矩阵。总线矩阵的左端是主动单元,也就是拥有存储器的访问权;右边这些是被动单元,它们的存储器只能被左边的主动单元读写。主动单元这里,内核有 DCode 和系统总线,可以访问右边的存储器,其中 DCode 总线是专门访问 Flash 的,系统总线是访问其他东西的。另外,由于 DMA 要转运数据,所以 DMA 也必须要有访问的主动权,那主动单元,除了内核 CPU,剩下的就是 DMA 总线了,这里 DMA1 有一条 DMA 总线,DMA2 也有一条 DMA 总线,下面还有一条 DMA 总线,这是以太网外设自己私有的 DMA,可以不用管的。在 DMA1 和 DMA2 里面可以看到,DMA1 有 7 个通道,DMA2 有 5 个通道,各个通道可以分别设置它们转运数据的源地址和目的地址,这样它们就可以各自独立的工作了。
  4. 接着下面这里有个仲裁器,这是因为虽然多个通道可以独立转运数据,但是最终 DMA 总线只有一条,所以所有的通道都只能分时复用这一条 DMA 总线,如果产生了冲突,那就会由仲裁器,根据通道的优先级,来决定谁先用,谁后用。另外在总线矩阵这里,也会有个仲裁器,如果 DMA 和 CPU 都要访问同一个目标,那么 DMA 就会暂停 CPU 的访问,以防止冲突,不过总线仲裁器,仍然会保证 CPU 得到一半的总线带宽,使 CPU 也能正常工作,这就是仲裁器的作用
  5. 接着下面这里是 AHB 从设备,也就是 DMA 自身的寄存器,因为 DMA 作为一个外设,它自己也会有相应的配置寄存器,这里连接在了总线右边的 AHB 总线上。所以 DMA 既是总线矩阵的主动单元,可以读写各种存储器,也是 AHB 总线上的被动单元,CPU 通过这一条线路,就可以对 DMA 进行配置了。
  6. 接着继续看 DMA 请求,请求就是触发的意思,这条线路右边的触发源是各个外设,所以这个 DMA 请求就是 DMA 的硬件触发源,比如 ADC 转换完成、串口接收到数据,需要触发 DMA 转运数据的时候,就会通过这条线路,向 DMA 发出硬件触发信号,之后 DMA 就可以执行数据转运的工作了,这就是 DMA 请求的作用。

有关 DMA 结构总结而言,其中包括用于访问各个存储器的 DMA 总线;内部的多个通道,可以进行独立的数据转运;仲裁器,用于调度各个通道,防止产生冲突;AHB 从设备,用于配置 DMA 参数;DMA 请求,用于硬件触发 DMA 的数据转运。这就是这个 DMA 的各个部分和作用。

Flash 是 ROM 只读存储器的一种,如果通过总线直接访问的话,无论是 CPU,还是 DMA,都是只读的,只能读取数据,而不能写入。如果 DMA 的目的地址,填了 Flash 的区域,那转运时,就会出错,这个注意一下。(当读写一个保留的地址区域,将会产生 DMA 传输错误)
当然 Flash 也不是绝对的不可写入,我们可以配置这个 Flash 接口控制器,对 Flash 进行写入,这个流程就比较麻烦了,要先对 Flash 按页进行擦除,再写入数据,不过这是另一个课题了,这里不再讨论。
总之就是 CPU 或者 DMA 直接访问 Flash 的话,是只可以读而不可以写的,然后 SRAM 是运行内存,可以任意读写,没有问题,外设寄存器的话,得看参考手册里面的描述,有的寄存器是只读的,有的寄存器是只写的,不过我们主要用的是数据寄存器,数据寄存器都是可以正常读写的。

1.4 DMA 基本结构

DMA简介-stm32入门_第3张图片
如果想编写代码实际去控制 DMA 的话,那 DMA 基本结构图就是必不可少的。

DMA 框图只是一个笼统的结构图,对于 DMA 内部的执行细节,它还是没体现出来,所以我们再来分析一下这个图,看看 DMA 具体是怎么工作的。

在这个图里,左边是外设寄存器站点,右边是存储器站点,包括 Flash 和 SRAM,这两就是数据转运的两大站点。

  1. 可以看到,DMA 的数据转运,可以是从外设到存储器,也是可以从存储器到外设,具体是向左还是向右,有一个方向的参数,可以进行控制。另外还有一种转运方式,就是存储器到存储器,比如 Flash 到 SRAM 或者 SRAM 到 SRAM 这两种方式,由于 Flash 是只读的,所以 DMA 不可以进行SRAM 到 Flash,或者 Flash 到 Flash 的转运操作。

在 STM32 手册里,他所说的存储器,一般是特指 Flash 和 SRAM,不包含外设寄存器,外设寄存器一般直接称作外设,虽然我们刚才说了,寄存器也是存储器的一种,但是 STM32 还是使用了外设和存储器来作为区分。

  1. 然后我们继续看外设和存储器的参数,既然要进行数据转运,那肯定就要指定从哪里转到哪里,具体怎么转了,所以外设和存储器两个站点都有 3 个参数:
    1. 第一个是起始地址,有外设端的起始地址和存储器端的起始地址,这两个参数决定了数据是从哪里来的,到哪里去的;
    2. 之后第二个参数是数据宽度,这个参数的作用是,指定一次转运要按多大的数据宽度来进行,它可以选择字节 Byte、半字 HaldWord 和字 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 里找,这个没有限制,甚至你可以在外设站点写存储器的地址,存储器站点写外设的地址,然后方向参数给反过来,这样也是可以的,只是 ST 公司给它起了这样的名字而已,你也可以把它叫做站点 A、站点 B,从 A 到 B 或者 从 B 到 A 转运数据,不必拘泥于它写的外设站点、存储器站点这个名字。

  1. 接着往下面看有个传输计数器,这个东西就是用来指定,总共需要转运几次的,这个传输计数器是一个自减计数器,比如你给它写个 5,那 DMA 就只能进行 5 次数据转运。转运过程中,每转运一次,计数器的数就会减 1,当传输计数器减到 0 之后,DMA 就不会再进行数据转运了,另外,它减到 0 之后,之前自增的地址,也会恢复到起始地址的位置,以方便之后 DMA 开始新一轮的转运。在传输计数器的右边,有一个自动重装器,这个自动重装器的作用就是,传输计数器减到 0 之后,是否要自动恢复到最初的值,比如最初传输计数器给 5,如果不使用自动重装器,那转运 5 次后,DMA 就结束了;如果使用自动重装值,那转运 5 次,计数器减到 0 后,就会立刻重装到初始值 5,这个就是自动重装器,它决定了转运的模式。如果不重装,就是正常的单次模式,如果重载,就是循环模式,比如如果你想转运一个数组,那一般就是单次模式,转运一轮,就结束了;如果是 ADC 扫描模式 + 连续转换,那为了配合 ADC,DMA 也需要使用循环模式,所以这个循环模式和 ADC 的连续模式差不多,都是指定一轮工作完成后,是不是立即开始下一轮工作。

传输计数器只有低 16 位有效,范围是 0~65535,所以传输个数最多是 65535 个。

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

每个 DMA 通道都可以在 DMA 传输过半、传输完成和传输错误时产生中断。发送中断后,在中断状态寄存器里置标志位,如果配置了 NVIC,就会向 CPU 申请中断。之后自动标志位清除,用于清除中断标志位。

1.5 DMA 请求

对应于 DMA 基本结构中的 DMA 触发的部分。
DMA简介-stm32入门_第4张图片

  1. 这张图是 DMA1 的请求映像,下面是 DMA 的 7 个通道,每个通道都有一个数据选择器,可以选择硬件触发或软件触发。一般数据选择器的侧边的输入选择控制位,但是这里左边写的是软件触发(MEM2MEM位),M2M 位是数据选择器的控制位,用于选择是硬件触发还是软件触发,当 M2M 位 = 1 时,选择软件触发;而 EN 位是并不是数据选择器的控制位,而是开关控制,决定这个数据选择器要不要工作,EN = 0 时不工作,EN = 1 时工作。
  2. 继续看左边的硬件触发源,这里是外设请求信号,可以看到,每个通道的硬件触发源都是不同的,如果你需要用 ADC1 来触发的话,那就必须选择通道 1,如果需要定时器 2 的更新事件来触发的话,那就必须选择通道 2,剩下的也是同理。因为每个通道的硬件触发源都不同,所以如果你想使用某个硬件触发源的话,就必须使用它所在的通道,这就是硬件触发的注意事项。而如果使用软件触发的话,那通道就可以任意选择了,因为每个通道的软件触发都是一样的。选择哪个触发源是由对应的外设是否开启了 DMA 输出来决定的,比如你要使用 ADC1,那会有个库函数叫 ADC_DMACmd,必须使用这个库函数开启 ADC1 这一路的输出,它才有效;如果想选择定时器 2 的通道 3,那也会有个 TIM_DMACmd 函数,用来进行 DMA 输出控制,所以触发源使用哪个取决于你把哪个外设的 DMA 输出开启了,如果 3 个都开启了,那这边是一个或门,理论上 3 个硬件都可以进行触发,不过一般情况下,我们都是开启其中一个。。
  3. 之后,这 7 个触发源,进入到仲裁器,进行优先级判断,最终产生内部 DMA1 请求,这个优先级的判断,类似于中断的优先级,默认优先级是通道号越小,优先级越高;当然也可以在程序中配置优先级,这个其实影响并不是很大,大家了解一下即可。

1.6 数据宽度与对齐

DMA简介-stm32入门_第5张图片
两个站点都有数据宽度的参数,如果数据宽度都一样,那就是正常的一个个转运。如果数据宽度不一样,那会怎么处理呢?这个表就是来说明这个问题的;第一列是源端宽度,第二列是目标宽度,第三列是传输数目。

  1. 当源端和目标都是 8 位时,转运第一步,在源端的 0 位置,读数据 B0,在目标的 0 位置,写数据 B0,就是把这个 B0,从左边挪到右边。之后的步骤就是,把 B1 从左边挪到右边,接着 B2、B3,这是源端和目标都是 8 位的情况,操作也很正常。
  2. 当源端是 8 位,目标是 16 位时,转运第一步,在源端的 0 位置,读数据 B0,在目标的 0 位置,写数据 00B0。之后的步骤就是,读数据 B1,写数据 00B1,等等,如果你目标的数据宽度,比源端的数据宽度大,那就再目标数据前面多出来的空位补 0,之后 8 位 转运到 32 位,也是一样的处理,前面空出来的都补 0。
  3. 当目标数据宽度,比源端数据宽度小时,比如,由 16 位转到 8 位去,现象就是,读 B1B0,只写入 B0;读 B3B2,只写入 B2,也就是把多出来的高位舍弃掉,之后的各种情况,也都是类似的操作。

总之,这个表的意思就是,如果你把小的数据转到大的里面去,高位就会补 0;如果把大的数据转到小的里面去,高位就会舍弃掉,如果数据宽度一样,那就没事。就是跟 uint8_t、uint16_t 和 uint32_t 变量之间相互赋值一样,不够就补 0,超了就舍弃高位。

1.7 DMA 转运实例

1.7.1 数据转运 + DMA

任务:存储器到存储器的数据转运,将 SRAM 里的数据 DataA,转运到另一个数组 DataB 中。这种情况下,DMA 基本结构里的各个参数该如何配置?

DMA简介-stm32入门_第6张图片

  1. 首先是外设站点、存储器站点的起始地址、数据宽度、地址是否自增这三个参数。在这个任务里,外设地址显然应该填 DataA 数组的首地址,存储器地址,给 DataB 数组的首地址;然后数据宽度,两个数组的类型都是 uint8_t,所以数据宽度都是按 8 位的字节传输,之后地址是否自增,在中间可以看到,我们想要的效果是 DataA[0] 转到 DataB[0],DataA[1] 转到 DataB[1],等等,两个数组的位置一一对应,所以转运完 DataA[0] 和 DataB[0] 之后,两个站点的地址都应该自增,都移动到下一个数据的位置,继续转运 DataA[1] 和 DataB[1] ,这样来进行。如果左边不自增,右边自增,转运完成后,DataB 的所有数据都会等于 DataA[0];如果左边自增,右边不自增,转运完成后,DataB[0] 等于 DataA 的最后一个数,DataB 其他的数不变。如果左右都不自增,那就一直是 DataA[0] 转到 DataB[0],其他的数据不变,这就是地址是否自增的效果。
  2. 之后,方向参数显然是外设站点转运到存储器站点,如果你想要把 DataB 的数据转运到 DataA,那可以把方向参数换过来,这样就是反向转运了。
  3. 然后是传输计数器和是否要自动重装,这里显然要转运 7 次,所以传输计数器给 7,自动重装暂时不需要。
  4. 之后触发选择部分,这里我们要使用软件触发,因为这是存储器到存储器的数据转运,是不需要等待硬件时机的,尽快转运完成就行了。
  5. 最后,调用 DMA_Cmd 给 DMA 使能,这样数据就会从 DataA 转运到 DataB 了。

转运 7 次之后,传输计数器自减到 0,DMA 停止,转运完成。这里的数据转运是一种复制转运,转运完成后 DataA 的数据并不会消失,这个过程相当于把 DataA 的数据复制到了 DataB 的位置。

1.7.2 ADC 扫描模式 + DMA

任务:ADC 扫描模式和 DMA 配合使用的流程。一般来说,DMA 最常见的用途就是配合 ADC 的扫描模式,因为 ADC 扫描模式有个数据覆盖的特征,或者可以说这个数据覆盖的问题是 ADC 固有的缺陷,这个缺陷使 ADC 和 DMA 成为了最常见的伙伴,ADC 对 DMA 的需求是非常强烈的。像其他的一些外设,使用 DMA 可以提高效率,是锦上添花的操作,但是不使用也是可以的,顶多是损失一些性能。但是这个 ADC 的扫描模式,如果不使用 DMA,功能都会受到很大的限制,所以 ADC 和 DMA 的结合最为常见
DMA简介-stm32入门_第7张图片

左边是 ADC 扫描模式的执行流程,在这个有 7 个通道,触发一次后,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 转运,这部分内容,手册里并没有详细描述。

2. 两个 DMA 转运功能案例

2.1 DMA 数据转运

2.1.1 硬件电路图

DMA简介-stm32入门_第8张图片
数据转运都是在 STM32 内部进行,所以外围电路就在一个 OLED 显示一下就行了。其他的模块都不需要。

2.1.2 存储器映像验证

看一下我们定义的数据是不是真的存储在了这相应的地址区间里。

本节是有关存储器地址、常量和变量、结构体访问寄存器这些知识点的一些扩展内容。

  • 常量和变量
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"

const uint8_t aa = 0x66;
const uint8_t bb = 0x66;

int main(void) {
	OLED_Init();
	
	//正常的定义变量,引用变量
	OLED_ShowHexNum(1, 1, aa, 2);
	//显示变量的地址,8 个 16 进制的数表示 32 位(32 位系统)
	OLED_ShowHexNum(2, 1, (uint32_t)&aa, 8);//不加强制类型转换,就是指针跨级赋值了,编译的时候会给报警告
	OLED_ShowHexNum(3, 1, (uint32_t)&bb, 8);
	
	while(1){

	}
}
  • aa 变量的地址是 20000000,地址以 20 开头,所以 aa 变量存储的位置是 SRAM 区,在 SRAM 区,它的地址肯定是以 20 开头的,具体地址由编译器来确定。目前 SRAM 区没什么东西,所以编译器就把这个变量放在了 SRAM 区的第一个位置。
  • const 是 C 语言的关键字,表示的是常量的意思,被 const 修饰的变量,在程序中,只能读,不能写,而 Flash 里面的数据也是只能读,不能写,所以 const 和 Flash 就联系起来了。在 STM32 中,使用 const 修饰的常变量是存储在 Flash 里面的,这个常变量的值只能在定义的时候给 aa 常变量的地址,就变成了 08 开头的,所以 aa 常变量被存储在了 Flash 里,在 Flash 里存储的是程序代码,当然还有常量数据,这里地址尾部有些偏移,不像 SRAM 里那样,直接安排在第一个位置,这是因为 Flash 里还有程序代码的东西,放在了前面,所以编译器给这个常量安排的地址,就相对靠后了一些,这就是定义变量和常量的方法。
  • 正常情况下,我们使用的都是变量,直接定义就行,不需要加 const,那什么时候需要定义常量呢?这个是当我们程序中出现了一大批数据,并且不需要更改时,就可以把它定义成常量,这样能节省 SRAM 的空间,比如查找表、字库数据等等。字库是一个数组,它里面的数据决定了每个字符应该显示哪些像素点,这个数组非常长,而且是不需要更改的,所以可以加一个 const,把它定义在 Flash 里面,这样就可以节省 SRAM 的空间。如果一不小心,把 const 去掉了,那程序功能并不会有任何影响,但是 SRAM 里会有和这个数组一样大的空间被浪费掉,如果数组很小,那影响也不大,但是如果数组很大,那就得考虑一下,SRAM 是不是消耗的起了,这就是 const 关键字的用途。如果你有一个很大的查找表或者字符,最好加一个 const。
  • 寄存器
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"

int main(void) {
	OLED_Init();

	OLED_ShowHexNum(4, 1, (uint32_t)&ADC1->DR, 8);

	while(1){

	}
}
  • 接下来我们再研究下外设寄存器的地址,对于变量或者常量来说,它的地址是由编译器确定的,不同的程序,地址可能不一样,是不固定的。对于外设寄存器来说,它的地址是固定的,在手册里都能查得到,在程序里,也可以用结构体,很方便的访问寄存器。比如要访问 ADC1 的 DR 寄存器就可以写 ADC1->DR,这样就可以访问 ADC1 的 DR 寄存器了,可以看到它的地址是 4001 244C,是 40 开头的,对照 PPT 的表,可以知道它确实是外设寄存器的区域,这个具体地址 4001 244C 是固定的,在手册里可以查到,如果你想算某个寄存器的地址,就可以查手册计算一下(首先查一下这个寄存器所在外设的起始地址,然后再在外设的寄存器总表里,查一下偏移,起始地址 + 偏移就是这个寄存器的实际地址。

  • ADC1->DR 如何知道 ADC1_DR 寄存器的地址的呢?这种结构体的方式,又是如何访问到寄存器的呢?

    1. ADC1 是 ((ADC_TypeDef *) ADC1_BASE),左边是一个强制类型转换,把 ADC1_BASE 转换为了 ADC_TypeDef 类型的指针右边 ADC1_BASE 就是 ADC1 的基地址,基地址就是起始地址的意思,那就是查表看到的 4001 2400,转到定义看一下,ADC1 的基地址就是 APB2 外设基地址(APB2PERIPH_BASE) + 0x2400,再转到定义,APB2 外设基地址 就是 外设基地址(PERIPH_BASE) + 0x10000,再转到定义,外设基地址就是 ((uint32_t)0x40000000)。
    2. 回过来看,外设基地址(4000 0000) + 10000 = 4001 0000 就是 APB2 外设基地址,再回过来,再 + 2400 = 4001 2400 就得到了 ADC1 的基地址。
    3. 再回过来,现在基地址有了,但是基地址 + 偏移,才是寄存器的实际地址,在这里,它使用了一个非常巧妙的方法来实现这个偏移,就是使用结构体,来实现我们跳转到结构体的定义,可以看到依次定义了各种寄存器,这个结构体成员的顺序,看一下手册,和寄存器实际存放的顺序,是一一对应的,SR、CR1、CR2 等等,在结构体,也是 SR、CR1、CR2 等等,所以说,如果我们定义一个 ADC 结构体的指针,并且指针的地址就是这个外设的起始地址,那这个结构体的每个成员,就会正好映射实际的每个寄存器,那么这个结构体的内存和外设寄存器的内存就会完美重合,我在访问结构体的每个成员,就相当于访问这个外设的某个寄存器,这就是 STM32 中是用结构体来访问寄存器的流程。
  • 所以 ADC1 是结构体指针,指向的是 ADC1 外设的起始地址,访问结构体成员,就相当于是加一个地址偏移,起始地址加偏移,就是指定的寄存器。

不得不说,STM32 这个库函数,把访问一个寄存器做的还是比较麻烦的,其实如果你想简单点的话,直接用指针访问某个物理地址就行了。
#define ADC1_DR (uint32_t*)0x4001244C
*ADC1_DR,这样也是可以访问 ADC1 的 DR 寄存器,和这个结构体访问的效果,是一模一样的。

2.1.3 DMA 转运模块初始化

DMA 不涉及外围硬件电路,所以可以在 System 里添加。(为了防止与库函数里面的 DMA 重复,起名 MyDMA)

  • DMA 转运模块的库函数(在 dma.h 中)
void DMA_DeInit(DMA_Channel_TypeDef* DMAy_Channelx);//恢复缺省配置
void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct);//初始化,把结构体指定的参数,配置到 DMAx 的通道 y 里面去
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);//用于返回传输计数器的值,如果想看看,还剩多少数据没有转运,就可以调用这个函数获取传输计数器的值。
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);//清除中断挂起位
  • 配置 DMA 转运模块
  1. RCC 开启 DMA 的时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//DMA 是 AHB 总线的设备,所以要用 AHB 开启时钟的函数

第一个参数(互联型设备参数表/其他设备参数表):互联型设备是 STM32F105/107 的型号,我们使用的是 F103,所以在其他设备的参数表里选

  1. 直接调用 DMA_Init,初始化各个参数(包括外设和存储器站点的起始地址、数据宽度、地址是否自增,方向,传输计数器,是否需要自动重装,选择触发源,还有通道优先级),通过一个结构体,就可以配置好了。
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;//外设站点的起始地址(32 位地址),
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设站点的数据宽度
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;//外设站点是否自增
DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;//存储器站点的起始地址
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//存储器站点的数据宽度
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器站点是否自增
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//传输方向,指定外设站点是源端还是目的地,选择外设站点作为源端,传输方向是外设站点到存储器站点
DMA_InitStructure.DMA_BufferSize = size;//缓存区大小,以数据单元指定缓冲区,就是说你要传送几个数据单元,这个数据单元,等于你传输源端站点的 DataSize。
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;//传输模式(循环模式/正常模式),其实就是指定传输计数器是否使用自动重装
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;//选择是否应用于存储器到存储器的转运(软件触发),其实就是选择硬件触发还是软件触发
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//优先级(非常高/高/中等/低),按照参数要求,确定优先级即可。(如果有多个通道,可以指定一下优先级,确保紧急的转运有更高的优先级)
DMA_Init(DMA1_Channel1, &DMA_InitStructure);//第一个参数,DMAy_ChannelX,y 可以是 1 或 2,用来选择是哪个 DMA,x 用来选择是哪一个通道,对于 DMA 1 而言,可以是 1~7,对于 DMA 2 而言,可以是 1~5

注意事项:

  1. 对于 SRAM 的数组,它的地址是编译器分配的,并不是固定的,所以我们一般不会写绝对地址,而是通过数组名来获取地址。
  2. 缓存区大小,说简单点其实就是传输计数器,指定传输几次。在源码中这个参数直接赋值给了传输计数器的寄存器,它的取值是 0 ~65535。
  3. 循环模式不能应用在存储器到存储器的情况下,也就是自动重装和软件触发不能同时使用,如果同时使用,DMA 就会连续触发,永远也不会停下来。
  4. DMA_Init 的第一个参数,既选择了是哪个 DMA,也选择了是 DMA 的哪个通道。这里因为是存储器到存储器的转运,用的是软件触发,所以通道可以任意选择。这里就给通道 1 吧

DMA 转运三个条件:传输计数器 > 0,触发源有触发信号,DMA 使能,三个条件缺一不可

  1. 传一个 > 0 的 size,第一个条件满足
  2. 触发源为软件触发,所以一直都有触发信号,第二个条件满足
  3. DMA 还没有使能,第三个条件不满足,所以 DMA 还不会工作
  1. 进行开关控制,DMA_Cmd,给指定的通道使能,就完成了。
  • 如果选择硬件触发,不要忘记在对应的外设调用一下 XXX_DMACmd 开启一下触发信号的输出
  • 如果你需要 DMA 的中断,那就调用 DMA_ITConfig,开启中断输出,再在 NVIC 里,配置相应的中断通道,然后写中断函数就行了,中断的配置各个外设都一样
DMA_Cmd(DMA1_Channel1, ENABLE);

如果想在初始化之后就立刻工作,可以在初始化函数最后加上。这样的话初始化就会后立刻开始转运,并且转运一次之后,DMA 就停止了

使能 DMA 之后,三个条件满足,DMA 就会进行数据转运了,转运一次,传输计数器自减一次,当传输计数器减到 0 之后,转运完成,同时第一个条件就不满足了,转运停止。这样就完成了一次数组之间的数据转运,最后在运行的过程中,如果转运完成,传输计数器清 0 了,这时再想给传输计数器赋值的话,就 DMA 失能,写传输计数器,DMA 使能,这样就行了,这就是 DMA 的编程思路。

  1. 如果 DataA 的数据有变化了,我们想再转运一次,那该怎么办呢?这时我们就需要给传输计数器重新赋值了
void MyDMA_Transfer(void) {//DMA 传输函数,调用一次这个函数,就再次启动一次 DMA 转运
	//1. 我们需要重新给传输计数器赋值,必须要先给 DMA 失能
	DMA_Cmd(DMA1_Channel1, DISABLE);
	//2. 然后就可以给传输计数器赋值了
	DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);
	//3. 最后再次调用 Cmd 函数给 DMA 使能
	DMA_Cmd(DMA1_Channel1, ENABLE);//这样 DMA 转运的 3 个条件又重新满足了,DMA 就会再次开始转运
	
	//4. 等待转运完成
	while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);//获取标志位状态,选择转运完成标志位。
	
	//5. 不要忘了清除标志位,这个标志位是需要手动清除的。
	DMA_ClearFlag(DMA1_FLAG_TC1);//清除标志位
	
}

DMA 所有的通道都是这 4 种标志位:全局标志位/转运完成标志位/转运过半标志位/转运错误标志位。

2.1.4 示例代码

Alt 键 + 鼠标左键可以以方框的形式进行框选。如果不按 Alt 键,那就是一行一行连续的框选。

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

MyDMA.c

#include "stm32f10x.h"                  // Device header

uint16_t MyDMA_Size;

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 是 AHB 总线的设备,所以要用 AHB 开启时钟的函数
//2.直接调用 DMA_Init,初始化各个参数(包括外设和存储器站点的起始地址、数据宽度、地址是否自增,方向,传输计数器,是否需要自动重装,选择触发源,还有通道优先级),通过一个结构体,就可以配置好了
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;//外设站点的起始地址(32 位地址),
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设站点的数据宽度
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;//外设站点是否自增
	DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;//存储器站点的起始地址
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;//存储器站点的数据宽度
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器站点是否自增
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//传输方向,指定外设站点是源端还是目的地,选择外设站点作为源端,传输方向是外设站点到存储器站点
	DMA_InitStructure.DMA_BufferSize = size;//缓存区大小,说简单点其实就是传输计数器,指定传输几次。在源码中这个参数直接赋值给了传输计数器的寄存器,它的取值是 0 ~65535
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;//传输模式(循环模式/正常模式),其实就是指定传输计数器是否使用自动重装
	DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;//选择是否应用于存储器到存储器的转运(软件触发),其实就是选择硬件触发还是软件触发
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//优先级(非常高/高/中等/低),按照参数要求,确定优先级即可。(如果有多个通道,可以指定一下优先级,确保紧急的转运有更高的优先级)
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);//第一个参数,DMAy_ChannelX,y 可以是 1 或 2,用来选择是哪个 DMA,x 用来选择是哪一个通道,对于 DMA 1 而言,可以是 1~7,对于 DMA 2 而言,可以是 1~5

//3.进行开关控制,DMA_Cmd,给指定的通道使能,就完成了。
	
	DMA_Cmd(DMA1_Channel1, DISABLE);//不让 DMA 初始化后,就立刻开始转运,而是等调用了 Transfer 函数后,再进行转运,调用一次转运一次,这样来工作
	//如果想在初始化之后就立刻工作,可以在这最后加上 ENABLE
}

void MyDMA_Transfer(void) {//DMA 传输函数,调用一次这个函数,就再次启动一次 DMA 转运
	//1. 我们需要重新给传输计数器赋值,必须要先给 DMA 失能
	DMA_Cmd(DMA1_Channel1, DISABLE);
	//2. 然后就可以给传输计数器赋值了
	DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);
	//3. 最后再次调用 Cmd 函数给 DMA 使能
	DMA_Cmd(DMA1_Channel1, ENABLE);//这样 DMA 转运的 3 个条件又重新满足了,DMA 就会再次开始转运
	
	//4. 等待转运完成
	while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);//获取标志位状态,选择转运完成标志位。
	
	//5. 不要忘了清除标志位,这个标志位是需要手动清除的。
	DMA_ClearFlag(DMA1_FLAG_TC1);//清除标志位
	
}

DMA 初始化后,不开始转运,而是等调用了 Transfer 函数后,再进行转运,调用一次转运一次,这样来工作

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyDMA.h"

uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};
uint8_t DataB[] = {0x00, 0x00, 0x00, 0x00};

int main(void) {
	OLED_Init();
	
	MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4);//DMA 初始化后,不转运,而是等调用了 Transfer 函数后,再进行转运,调用一次转运一次,这样来工作
	
	OLED_ShowString(1, 1, "DataA");
	OLED_ShowString(3, 1, "DataB");
	OLED_ShowHexNum(1, 8, (uint32_t)DataA, 8);//可能是模块里的 Size 变量被分配到了 0 的位置,所以这个地址就往后偏移了 2 个字节
	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);
	
	
	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);//在整个转运过程中,源端数据 DataA 是不会变化的
		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);
	}
}

如果想把 Flash 的数据转运到 SRAM,可以在 DataA 前面加一个 const:const uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};,把 DataA 定义在 Flash 里面。因为 const 数据不能重新更改,所以 DataA 的++部分就不能要了。然后编译下载。

  • 此时可以看到,DataA 地址是 08 开头的,说明 DataA 是 Flash 里面的数据。
  • 下面 DataB 的数据和 DataA 一样,说明 DataA 的数据成功转运到 DataB 里面来了。

实际上 DataA 中可能会有成千上万个数据,这样才能发挥 DMA 转运的优势,目前测试的话,先给 4 个就行了

2.2 DMA + AD 多通道

2.2.1 硬件电路图

DMA简介-stm32入门_第9张图片
这里的接线图和 AD 多通道是一样的,也是 PA0 接一个电位器,PA1~PA3 接三个传感器模块的 AO 输出。

2.1.2 ADC 单次扫描 + DMA 单次转运的程序代码

AD.h

#ifndef __AD_H
#define __AD_H

extern uint16_t AD_Value[4];//放在头文件中声明,作为外部可调用的数组

void AD_Init(void);
void AD_GetValue(void);
	
#endif

AD.c

#include "stm32f10x.h"                  // Device header

uint16_t AD_Value[4];

void AD_Init(void) {
//1.开启 RCC 时钟,包括 ADC、DMA 和 GPIO 时钟,另外 ADCCLK 的分频器也需要配置一下,
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);//ADC 都是 APB2 上的设备
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//DMA 是 AHB 总线的设备,所以要用 AHB 开启时钟的函数
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);//配置 ADCCLK,PCLK2 就是 APB2时钟 的意思(72M / 6 = 12M)
//2.配置 GPIO,把需要用的 GPIO 配置成模拟输入的模式
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//模拟输入模式,AIN 模式是 ADC 的专属模式,在 AIN 模式下,GPIO 口是无效的,断开 GPIO,防止你 GPIO 口的输入输出对我模拟电压的造成干扰
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);//GPIO 配置好默认低电平
	
//3.在转换前指定了转换的通道(点菜,把各个通道的菜,列在菜单里)
	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);

//4.配置 ADC 转换器,在库函数里,用结构体来配置的(包括ADC转换模式,有几个通道,触发源、数据对齐等等)
	ADC_InitTypeDef ADC_InitStructure;
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;// ADC 工作模式(独立模式/双 ADC 模式)。选择独立模式
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//数据对齐。选择右对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//触发控制的触发源,定义用于启动规则组转换的外部触发源。不使用外部触发,使用内部软件触发
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;//连续转换模式,选择连续转换/单次转换(ENABLE/DISABLE)
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;//扫描转换模式,选择扫描模式/非扫描模式(ENABLE/DISABLE)
	ADC_InitStructure.ADC_NbrOfChannel = 4;//规则组转换列表里通道数目(参数在 1~16),指定在扫描模式下,总共会用到几个通道。在非扫描的模式下,这个参数其实是没有用的,无论写多少数目,最终都是只有序列1 的位置有效。
	ADC_Init(ADC1, &ADC_InitStructure);

//5.直接调用 DMA_Init,初始化各个参数
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;//外设站点的起始地址(32 位地址),这里就是 ADC_DR 的地址
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//外设站点的数据宽度,想要的是 ADC_DR 寄存器低 16 位的数据
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设站点是否自增,不自增
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;//存储器站点的起始地址,我们想把数据存储在 SRAM 的数组里,这里是 数组的地址
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//存储器站点的数据宽度,也是半字halfword
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器站点是否自增,这里存储器地址是自增的,每转运一次挪一个坑
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//传输方向,指定外设站点是源端还是目的地,选择外设站点作为源端,传输方向是外设站点到存储器站点
	DMA_InitStructure.DMA_BufferSize = 4;//缓存区大小,说简单点其实就是传输计数器,指定传输几次。因为有 4 个 ADC 通道,所以传输 4 次
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;//传输模式(循环模式/正常模式),其实就是指定传输计数器是否使用自动重装
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;//选择是否应用于存储器到存储器的转运(软件触发),其实就是选择硬件触发还是软件触发,我们需要硬件触发,触发源为 ADC1
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//优先级(非常高/高/中等/低),按照参数要求,确定优先级即可。(如果有多个通道,可以指定一下优先级,确保紧急的转运有更高的优先级)
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);//这里通道需要根据硬件触发源来进行选择的。这里必须要使用 DMA1 的通道 1,其他的通道都不行
	
//6.进行开关控制,DMA_Cmd,给指定的通道使能。
	DMA_Cmd(DMA1_Channel1, ENABLE);
	
//此时DMA转运的触发源有信号条件目前是不满足的,因为这里是硬件触发,ADC 还没启动,不会有触发信号。所以 DMA 使能之后不会立刻工作。
//7.开启 ADC 到 DMA 的输出(通道有多个触发源,具体使用哪个,取决于把哪个 DMA 输出给开启了)
	ADC_DMACmd(ADC1, ENABLE);//用来开启 DMA 触发信号的
//到目前为止,ADC 和 DMA 配合工作的配置就完成了

//8.开关控制,调用ADC_Cmd 函数,开启 ADC,这样 ADC 就配置完成了,就能正常工作了
	ADC_Cmd(ADC1, ENABLE);
	
//9.开启 ADC 之后,根据手册建议,对 ADC 进行一下校准,这样可以减小误差
	ADC_ResetCalibration(ADC1);//复位校准
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);//获取复位校准状态,空循环等待校准完成。
	//获取 CR2 寄存器里的 RSTCAL 标志位,软件置该位为 1,那硬件就会开始复位校准,当复位校准完成后,该位就会由硬件自动清 0。
	ADC_StartCalibration(ADC1);//开始校准
	while (ADC_GetCalibrationStatus(ADC1) == SET);//获取开始校准状态,空循环等待校准完成。
	
	
}

void AD_GetValue(void) {
	//1.因为 DMA 是正常的单次模式,所以在触发 ADC 之前,需要再重新写一下传输计数器
	DMA_Cmd(DMA1_Channel1, DISABLE);
	DMA_SetCurrDataCounter(DMA1_Channel1, 4);
	DMA_Cmd(DMA1_Channel1, ENABLE);
	//2.软件触发转换(因为 ADC 还是单次模式,所以还是需要软件触发一下 ADC 开始)
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
	//3.最后等待 ADC 转换和 DMA 转运完成,这里因为转运总是在转换之后,所以等待 DMA 转运完成,等待 ADC 转换完成就不需要了
	while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);//获取标志位状态,选择转运完成标志位。
	DMA_ClearFlag(DMA1_FLAG_TC1);//清除标志位
	
	
}

当我们调用 AD_GetValue 函数时,ADC 开始转换,连续扫描 4 个通道,DMA 也同步进行转运,AD 转换结果,依次放在 AD_Value 数组中。

  • 这里通道需要根据硬件触发源来进行选择的。这里必须要使用 DMA1 的通道 1,其他的通道都不行。
  • DMA_Cmd 给指定的通道使能以后,此时DMA转运的触发源有信号条件目前是不满足的,因为这里是硬件触发,ADC 还没启动,不会有触发信号。所以 DMA 使能之后不会立刻工作。
  • 只有 开启 ADC 到 DMA 的输出后,ADC 和 DMA 配合工作的配置才完成了。

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"

int main(void) {
	OLED_Init();
	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);
	}
}

这就是 ADC 单次扫描 + DMA 单次转运的模式。

2.1.3 ADC 连续扫描 + DMA 循环转运的程序代码

那我们还可以配置成 ADC 连续扫描 + DMA 循环转运的模式,这样代码就更加方便。具体步骤是:

  1. ADC 连续模式 打开
  2. DMA 循环模式 打开
  3. ADC 软件触发直接放在初始化最后一行
  4. AD_GetValue 完全不需要了,直接删掉

AD.h

#ifndef __AD_H
#define __AD_H

extern uint16_t AD_Value[4];//放在头文件中声明,作为外部可调用的数组

void AD_Init(void);
	
#endif

AD.c

#include "stm32f10x.h"                  // Device header

uint16_t AD_Value[4];

void AD_Init(void) {
//1.开启 RCC 时钟,包括 ADC、DMA 和 GPIO 时钟,另外 ADCCLK 的分频器也需要配置一下,
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);//ADC 都是 APB2 上的设备
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//DMA 是 AHB 总线的设备,所以要用 AHB 开启时钟的函数
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);//配置 ADCCLK,PCLK2 就是 APB2时钟 的意思(72M / 6 = 12M)
//2.配置 GPIO,把需要用的 GPIO 配置成模拟输入的模式
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;//模拟输入模式,AIN 模式是 ADC 的专属模式,在 AIN 模式下,GPIO 口是无效的,断开 GPIO,防止你 GPIO 口的输入输出对我模拟电压的造成干扰
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIO_InitStructure);//GPIO 配置好默认低电平
	
//3.在转换前指定了转换的通道(点菜,把各个通道的菜,列在菜单里)
	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);

//4.配置 ADC 转换器,在库函数里,用结构体来配置的(包括ADC转换模式,有几个通道,触发源、数据对齐等等)
	ADC_InitTypeDef ADC_InitStructure;
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;// ADC 工作模式(独立模式/双 ADC 模式)。选择独立模式
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;//数据对齐。选择右对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;//触发控制的触发源,定义用于启动规则组转换的外部触发源。不使用外部触发,使用内部软件触发
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;//连续转换模式,选择连续转换/单次转换(ENABLE/DISABLE)
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;//扫描转换模式,选择扫描模式/非扫描模式(ENABLE/DISABLE)
	ADC_InitStructure.ADC_NbrOfChannel = 4;//规则组转换列表里通道数目(参数在 1~16),指定在扫描模式下,总共会用到几个通道。在非扫描的模式下,这个参数其实是没有用的,无论写多少数目,最终都是只有序列1 的位置有效。
	ADC_Init(ADC1, &ADC_InitStructure);

//5.直接调用 DMA_Init,初始化各个参数
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;//外设站点的起始地址(32 位地址),这里就是 ADC_DR 的地址
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//外设站点的数据宽度,想要的是 ADC_DR 寄存器低 16 位的数据
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;//外设站点是否自增,不自增
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;//存储器站点的起始地址,我们想把数据存储在 SRAM 的数组里,这里是 数组的地址
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;//存储器站点的数据宽度,也是半字halfword
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器站点是否自增,这里存储器地址是自增的,每转运一次挪一个坑
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//传输方向,指定外设站点是源端还是目的地,选择外设站点作为源端,传输方向是外设站点到存储器站点
	DMA_InitStructure.DMA_BufferSize = 4;//缓存区大小,说简单点其实就是传输计数器,指定传输几次。因为有 4 个 ADC 通道,所以传输 4 次
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;//传输模式(循环模式/正常模式),其实就是指定传输计数器是否使用自动重装
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;//选择是否应用于存储器到存储器的转运(软件触发),其实就是选择硬件触发还是软件触发,我们需要硬件触发,触发源为 ADC1
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//优先级(非常高/高/中等/低),按照参数要求,确定优先级即可。(如果有多个通道,可以指定一下优先级,确保紧急的转运有更高的优先级)
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);//第一个参数,DMAy_ChannelX,y 可以是 1 或 2,用来选择是哪个 DMA,x 用来选择是哪一个通道,对于 DMA 1 而言,可以是 1~7,对于 DMA 2 而言,可以是 1~5
	//这里通道需要根据硬件触发源来进行选择的。这里必须要使用 DMA1 的通道 1,其他的通道都不行
//6.进行开关控制,DMA_Cmd,给指定的通道使能。
	DMA_Cmd(DMA1_Channel1, ENABLE);
	
//此时DMA转运的触发源有信号条件目前是不满足的,因为这里是硬件触发,ADC 还没启动,不会有触发信号。所以 DMA 使能之后不会立刻工作。
//7.开启 ADC 到 DMA 的输出(通道有多个触发源,具体使用哪个,取决于把哪个 DMA 输出给开启了)
	ADC_DMACmd(ADC1, ENABLE);//用来开启 DMA 触发信号的
//到目前为止,ADC 和 DMA 配合工作的配置就完成了

//8.开关控制,调用ADC_Cmd 函数,开启 ADC,这样 ADC 就配置完成了,就能正常工作了
	ADC_Cmd(ADC1, ENABLE);
	
//9.开启 ADC 之后,根据手册建议,对 ADC 进行一下校准,这样可以减小误差
	ADC_ResetCalibration(ADC1);//复位校准
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);//获取复位校准状态,空循环等待校准完成。
	//获取 CR2 寄存器里的 RSTCAL 标志位,软件置该位为 1,那硬件就会开始复位校准,当复位校准完成后,该位就会由硬件自动清 0。
	ADC_StartCalibration(ADC1);//开始校准
	while (ADC_GetCalibrationStatus(ADC1) == SET);//获取开始校准状态,空循环等待校准完成。
	
//10.软件触发转换(因为 ADC 是扫描模式,所以只需要软件触发一下 ADC 开始)
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}


main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"

int main(void) {
	OLED_Init();
	AD_Init();
	
	OLED_ShowString(1, 1, "AD0:");
	OLED_ShowString(2, 1, "AD1:");
	OLED_ShowString(3, 1, "AD2:");
	OLED_ShowString(4, 1, "AD3:");
	
	while(1){
		
		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);
	}
}

当 ADC 触发之后,ADC 连续转换,DMA 循环转运,两者一直在工作,始终把最新的转换结果,刷新到 SRAM 数组里,我们想要数据的时候,随时去数组里取就行了。主循环啥都不干,直接读取 AD_Value 数组,就能得到结果。这样也可以完成 AD 多通道转换的功能。

可以看到,此时硬件外设,已经实现了相互配合 和 高度的自动化,各种操作都是硬件自己完成的,极大地减轻了软件负担,软件什么都不需要做,也不需要进任何中断,硬件自动就把活干完了。这就是 STM32 中硬件自动化的一大特色,各个外设互相连接,互相交织,不再是传统的一个 CPU 单独控制多个独立的外设这样的星型结构,而是外设之间互相连接,互相合作,形成一个网状结构,这样在完成某些简单且繁琐的工作的时候,就不需要 CPU 来统一调度了,可以直接通过外设之间的相互配合,自动完成这些繁琐的工作,这样不仅可以减轻 CPU 的负担,还可以大大提高外设的性能。比如定时器的输出,可以通向 ADC、DAC 或其他定时器,ADC 的触发源可以来自定时器或外部中断,DMA 的触发源可以来自 ADC、定时器、串口等等。这就是 STM32 外设相互配合工作的特色。

另外这里,你还可以再加一个外设,比如定时器,ADC 用单次扫描,再用定时器去定时触发,这样就是定时器触发 ADC,ADC 触发 DMA,整个过程完全自动,不需要程序手动进行操作,节省软件资源。

本节演示了 DMA 存储器到存储器、外设到存储器的情况,当然还有一个存储器到外设的情况,我们目前还没有介绍,比如串口发送一批数据,就可以使用 DMA 进行存储器到外设的转运。

你可能感兴趣的:(stm32,stm32,嵌入式硬件,单片机)