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,这个注意数据手册。
既然 DMA 是在存储器之间进行数据转运的,那我们就应该要了解一下,STM32 中都有哪些存储器,这些存储器又是被安排到了哪些地址上,这就是存储器映像的内容。
计算机系统的 5 大组成部分是 运算器、控制器、存储器、输入设备和输出设备。其中运算器和控制器一般会合在一起,叫做 CPU,所以计算机的核心关键部分就是 CPU 和存储器。存储器又有两个重要知识点,一个是存储器的内容,另一个就是存储器的地址,那 STM32 也不例外。
这个表是 STM32 中所有类型的存储器和它们所被安排的地址,在 STM32 的数据手册里,也会有个存储器映像的图,此表是从图里总结出来的,都是一个意思。
在这个表里,无论是 Flash,还是 SRAM,还是外设寄存器,它们都是存储器的一种,包括外设寄存器,实际上也是存储器。
所以DMA 转运本质上都是存储器之间的数据转运,说成外设到存储器只不过是 STM32 它特别指定了可以转运外设的存储器而已。
在这个表里,存储器总共分为两大类:
闪存模块中还有闪存接口寄存器,这是外设的一部分,地址是 40 开头的,这个外设可以对闪存进行读写。
在 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 开头的区域,需要用的话了解一下。
有关 DMA 结构总结而言,其中包括用于访问各个存储器的 DMA 总线;内部的多个通道,可以进行独立的数据转运;仲裁器,用于调度各个通道,防止产生冲突;AHB 从设备,用于配置 DMA 参数;DMA 请求,用于硬件触发 DMA 的数据转运。这就是这个 DMA 的各个部分和作用。
Flash 是 ROM 只读存储器的一种,如果通过总线直接访问的话,无论是 CPU,还是 DMA,都是只读的,只能读取数据,而不能写入。如果 DMA 的目的地址,填了 Flash 的区域,那转运时,就会出错,这个注意一下。(当读写一个保留的地址区域,将会产生 DMA 传输错误)
当然 Flash 也不是绝对的不可写入,我们可以配置这个 Flash 接口控制器,对 Flash 进行写入,这个流程就比较麻烦了,要先对 Flash 按页进行擦除,再写入数据,不过这是另一个课题了,这里不再讨论。
总之就是 CPU 或者 DMA 直接访问 Flash 的话,是只可以读而不可以写的,然后 SRAM 是运行内存,可以任意读写,没有问题,外设寄存器的话,得看参考手册里面的描述,有的寄存器是只读的,有的寄存器是只写的,不过我们主要用的是数据寄存器,数据寄存器都是可以正常读写的。
如果想编写代码实际去控制 DMA 的话,那 DMA 基本结构图就是必不可少的。
DMA 框图只是一个笼统的结构图,对于 DMA 内部的执行细节,它还是没体现出来,所以我们再来分析一下这个图,看看 DMA 具体是怎么工作的。
在这个图里,左边是外设寄存器站点,右边是存储器站点,包括 Flash 和 SRAM,这两就是数据转运的两大站点。
在 STM32 手册里,他所说的存储器,一般是特指 Flash 和 SRAM,不包含外设寄存器,外设寄存器一般直接称作外设,虽然我们刚才说了,寄存器也是存储器的一种,但是 STM32 还是使用了外设和存储器来作为区分。
这就是外设站点和存储器站点各自的 3 个参数了,如果要进行存储器到存储器的数据转运,那我们就需要把其中一个存储器的地址,放在外设的这个站点,这样就能进行存储器到存储器的转运了。只要你在外设起始地址里写 Flash 或者 SRAM 的地址,那它就会去 Flash 或 SRAM 找数据,这个站点虽然叫外设寄存器,但是它就只是个名字而已,并不是说这个地址只能写寄存器的地址,如果写 Flash 的地址,那它就会去 Flash 里找;写 SRAM 的地址,那它就会去 SRAM 里找,这个没有限制,甚至你可以在外设站点写存储器的地址,存储器站点写外设的地址,然后方向参数给反过来,这样也是可以的,只是 ST 公司给它起了这样的名字而已,你也可以把它叫做站点 A、站点 B,从 A 到 B 或者 从 B 到 A 转运数据,不必拘泥于它写的外设站点、存储器站点这个名字。
传输计数器只有低 16 位有效,范围是 0~65535,所以传输个数最多是 65535 个。
触发一次,转运一次,传输计数器自减一次,当传输计数器等于 0,且没有自动重装时,这时无论是否触发,DMA 都不会再进行转运了,此时就需要 DMA_Cmd,给 DISABLE,关闭 DMA,再为传输计数器写入一个大于 0 的数,再 DMA_Cmd,给 ENABLE,开启 DMA,DMA 才能进行工作。注意一下,写传输计数器时,必须要先关闭 DMA,再进行,不能在 DMA 开启时,写传输计数器,这是手册里的规定
每个 DMA 通道都可以在 DMA 传输过半、传输完成和传输错误时产生中断。发送中断后,在中断状态寄存器里置标志位,如果配置了 NVIC,就会向 CPU 申请中断。之后自动标志位清除,用于清除中断标志位。
两个站点都有数据宽度的参数,如果数据宽度都一样,那就是正常的一个个转运。如果数据宽度不一样,那会怎么处理呢?这个表就是来说明这个问题的;第一列是源端宽度,第二列是目标宽度,第三列是传输数目。
总之,这个表的意思就是,如果你把小的数据转到大的里面去,高位就会补 0;如果把大的数据转到小的里面去,高位就会舍弃掉,如果数据宽度一样,那就没事。就是跟 uint8_t、uint16_t 和 uint32_t 变量之间相互赋值一样,不够就补 0,超了就舍弃高位。
任务:存储器到存储器的数据转运,将 SRAM 里的数据 DataA,转运到另一个数组 DataB 中。这种情况下,DMA 基本结构里的各个参数该如何配置?
转运 7 次之后,传输计数器自减到 0,DMA 停止,转运完成。这里的数据转运是一种复制转运,转运完成后 DataA 的数据并不会消失,这个过程相当于把 DataA 的数据复制到了 DataB 的位置。
任务:ADC 扫描模式和 DMA 配合使用的流程。一般来说,DMA 最常见的用途就是配合 ADC 的扫描模式,因为 ADC 扫描模式有个数据覆盖的特征,或者可以说这个数据覆盖的问题是 ADC 固有的缺陷,这个缺陷使 ADC 和 DMA 成为了最常见的伙伴,ADC 对 DMA 的需求是非常强烈的。像其他的一些外设,使用 DMA 可以提高效率,是锦上添花的操作,但是不使用也是可以的,顶多是损失一些性能。但是这个 ADC 的扫描模式,如果不使用 DMA,功能都会受到很大的限制,所以 ADC 和 DMA 的结合最为常见
左边是 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 转运,这部分内容,手册里并没有详细描述。
数据转运都是在 STM32 内部进行,所以外围电路就在一个 OLED 显示一下就行了。其他的模块都不需要。
看一下我们定义的数据是不是真的存储在了这相应的地址区间里。
本节是有关存储器地址、常量和变量、结构体访问寄存器这些知识点的一些扩展内容。
#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 寄存器的地址的呢?这种结构体的方式,又是如何访问到寄存器的呢?
- 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)。
- 回过来看,外设基地址(4000 0000) + 10000 = 4001 0000 就是 APB2 外设基地址,再回过来,再 + 2400 = 4001 2400 就得到了 ADC1 的基地址。
- 再回过来,现在基地址有了,但是基地址 + 偏移,才是寄存器的实际地址,在这里,它使用了一个非常巧妙的方法来实现这个偏移,就是使用结构体,来实现我们跳转到结构体的定义,可以看到依次定义了各种寄存器,这个结构体成员的顺序,看一下手册,和寄存器实际存放的顺序,是一一对应的,SR、CR1、CR2 等等,在结构体,也是 SR、CR1、CR2 等等,所以说,如果我们定义一个 ADC 结构体的指针,并且指针的地址就是这个外设的起始地址,那这个结构体的每个成员,就会正好映射实际的每个寄存器,那么这个结构体的内存和外设寄存器的内存就会完美重合,我在访问结构体的每个成员,就相当于访问这个外设的某个寄存器,这就是 STM32 中是用结构体来访问寄存器的流程。
所以 ADC1 是结构体指针,指向的是 ADC1 外设的起始地址,访问结构体成员,就相当于是加一个地址偏移,起始地址加偏移,就是指定的寄存器。
不得不说,STM32 这个库函数,把访问一个寄存器做的还是比较麻烦的,其实如果你想简单点的话,直接用指针访问某个物理地址就行了。
#define ADC1_DR (uint32_t*)0x4001244C
*ADC1_DR
,这样也是可以访问 ADC1 的 DR 寄存器,和这个结构体访问的效果,是一模一样的。
DMA 不涉及外围硬件电路,所以可以在 System 里添加。(为了防止与库函数里面的 DMA 重复,起名 MyDMA)
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);//清除中断挂起位
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);//DMA 是 AHB 总线的设备,所以要用 AHB 开启时钟的函数
第一个参数(互联型设备参数表/其他设备参数表):互联型设备是 STM32F105/107 的型号,我们使用的是 F103,所以在其他设备的参数表里选
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
注意事项:
- 对于 SRAM 的数组,它的地址是编译器分配的,并不是固定的,所以我们一般不会写绝对地址,而是通过数组名来获取地址。
- 缓存区大小,说简单点其实就是传输计数器,指定传输几次。在源码中这个参数直接赋值给了传输计数器的寄存器,它的取值是 0 ~65535。
- 循环模式不能应用在存储器到存储器的情况下,也就是自动重装和软件触发不能同时使用,如果同时使用,DMA 就会连续触发,永远也不会停下来。
- DMA_Init 的第一个参数,既选择了是哪个 DMA,也选择了是 DMA 的哪个通道。这里因为是存储器到存储器的转运,用的是软件触发,所以通道可以任意选择。这里就给通道 1 吧
DMA 转运三个条件:传输计数器 > 0,触发源有触发信号,DMA 使能,三个条件缺一不可
- 传一个 > 0 的 size,第一个条件满足
- 触发源为软件触发,所以一直都有触发信号,第二个条件满足
- DMA 还没有使能,第三个条件不满足,所以 DMA 还不会工作
- 如果选择硬件触发,不要忘记在对应的外设调用一下 XXX_DMACmd 开启一下触发信号的输出
- 如果你需要 DMA 的中断,那就调用 DMA_ITConfig,开启中断输出,再在 NVIC 里,配置相应的中断通道,然后写中断函数就行了,中断的配置各个外设都一样
DMA_Cmd(DMA1_Channel1, ENABLE);
如果想在初始化之后就立刻工作,可以在初始化函数最后加上。这样的话初始化就会后立刻开始转运,并且转运一次之后,DMA 就停止了
使能 DMA 之后,三个条件满足,DMA 就会进行数据转运了,转运一次,传输计数器自减一次,当传输计数器减到 0 之后,转运完成,同时第一个条件就不满足了,转运停止。这样就完成了一次数组之间的数据转运,最后在运行的过程中,如果转运完成,传输计数器清 0 了,这时再想给传输计数器赋值的话,就 DMA 失能,写传输计数器,DMA 使能,这样就行了,这就是 DMA 的编程思路。
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 种标志位:全局标志位/转运完成标志位/转运过半标志位/转运错误标志位。
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 个就行了
这里的接线图和 AD 多通道是一样的,也是 PA0 接一个电位器,PA1~PA3 接三个传感器模块的 AO 输出。
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 单次转运的模式。
那我们还可以配置成 ADC 连续扫描 + DMA 循环转运的模式,这样代码就更加方便。具体步骤是:
- ADC 连续模式 打开
- DMA 循环模式 打开
- ADC 软件触发直接放在初始化最后一行
- 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 进行存储器到外设的转运。