目录

第一个代码:DMA数据转运

扩展知识

DMA的配置步骤

DMA的库函数

DMA_DeInit

DMA初始化和DMA结构体初始化函数

DMA_Cmd

DMA_ITConfig

DMA_SetCurrDataCounter

DMA_GetCurrDataCounter

四个获取标志位状态函数

代码实现

MyDMA.c

第一步,开启时钟

第二步,初始化DMA

第三步,开关控制

启动DMA转运的函数

MyDMA.h

Main.c

第二个代码:DMA+AD多通道

ADC和DMA配合起来的配置步骤

代码实现

第一步,开启RCC时钟

第二步,配置GPIO

第三步,配置多路开关

第四步,配置ADC转换器

第五步,配置DMA

第六步,就是开关控制

第七步,ADC校准

第八步,配置ADC触发

AD.c

AD.h

Main.c


声明:本专栏是本人跟着B站江科大的视频的学习过程中记录下来的笔记,我之所以记录下来是为了方便自己日后复习。如果你也是跟着江科大的视频学习的,可以配套本专栏食用,如有问题可以QQ交流群:963138186

本节我们来写一下DMA的代码。

第一个代码:DMA数据转运

连线图:

复制工程并改名:

扩展知识

先扩展一下知识点:

通常我们定义的普通变量是存储在SRAM区,此时该变量的地址是以20开头的。

而在变量前面加上const,将变量变成常量,只能读不能写,而我们上节说过Flash区的数据就是只读不写,所以加了const关键字的变量是存储在Flash区的,此时它的地址是以08开头。加了const关键字的变量只能在定义的时候给一个值,后面想要给它赋值的话程序就会报错,因为它已经变成了常量,不能被修改。

对于变量或者常量来说,它们的地址是由编译器决定的,不同的程序地址可能不一样,是不固定的。但是外设寄存器的地址是固定的是以40开头的,在手册里都能查得到。

在程序里也可以用结构体很方便访问寄存,比如要访问ADC1的DR寄存器,就可以写ADC1—>DR,这样就可以访问ADC的DR寄存器了。

我们可以显示一下这个ADC1的DR寄存器的地址,

结果是4001 244C

我们在手册中查到的ADC1的起始地址是4001 2400,

然后查到DR寄存器偏移是4C,

所以ADC1的DR地址就是4001 244C。

所以如果想算某个寄存器的地址,就可以查手册计算一下。

首先查一下这个寄存器所在外设的起始地址然后再在外设的寄存器器总表里查一下偏移起始地址加偏移就是这个寄存器的实际地址。

我们再来研究一下ADC1—>DR这个东西是如何知道ADC1_DR寄存器的地址,这种结构体的方式又是如何访问到寄存器的?

我们可以在ADC1处右键跳转到定义,可以看 ADC1就是这个东西

左边是一个强制类型转换,把ADC1_BASE转换为了ADC_TypeDef类型的指针。

ADC1_BASE就是ADC1的基地址基地址是起始地址的意思也就是我们刚才查表看到的4001 2400

在ADC1_BASE右键转到定义看到ADC1基地址就是APB2外设基地址+0x2400。

在APB2PERIPH_BASE右键再转到定义,APB2外设基地址就是外设基地址+0x10000。

再转到定义,外设基地址就是0x4000 0000,可以看到上面还有SRAM基地址,是2000,flash基地址是0800,和我们上面讲的都是一致的。

这里回过来看,

外设基地址0x4000 0000+0x10000=4001 0000,是APB2外设基地址;

APB2外设基地址4001 0000+0x2400=4001 2400,就得到了ADC1的基地址,也就是手册表里写那样。

现在基地址有了,但是基地址+偏移才是寄存器的实际地址,在这里,它使用了一个非常巧妙的方法来实现这个偏移:就是使用结构体来实现,我们跳转到结构体的定义。

可以看到,这里是依次定义的各种寄存器,

这个结构体成员的顺序,和手册上这个寄存器实际存放的顺序是一一对应的

所以说如果我们定义一个ADC结构体的指针并且指针的地址就是这个外设的起始地址。那这个结构体的每个成员就会正好映射实际的每个寄存器。

如果指定这个结构体的起始地址就是ADC1外设寄存器的起始地址,那么这个结构体的内存和外设寄存器的内存就会完美重合,再访问结构体的某个成员,就相当于是访问这个外设的某个寄存器。

这就是STM32中使用结构体来访问寄存器的流程。

那么回头看看ADC1—>DR,现在就应该明白它是什么意思了吧!

ADC1是结构体指针,指向的是ADC1外设的起始地址,访问结构体成员就相当于是加一个地址偏移,起始地址加偏移就是指定的寄存器。

STM32这个库函数把访问一个寄存器做的还是非常麻烦的,其实如果想简单点的话,直接用指针访问某个物理地址就行了,比如:

这样写也是可以访问ADC1的DR寄存器的,和这个结构体访问的效果是一模一样的。

到这里有关存储器地址、常量和变量、结构体访问寄存器这些知识点就讲完了。

接下来我们回到正题,看一下DMA的配置。

DMA的配置步骤

DMA初始化的步骤我们看这个基本结构图:

第一步,RCC开启DMA的时钟。

第二步,初始化DMA,就可以直接调用DMA_Init,初始化结构图中各个参数了,包括外设和存储器站点的起始地址、数据宽度、地址是否自增、方向、传输计数器、是否需要自动重装、选择触发源。当然还有一个通道优先级,这里没画出来。这所有的参数通过一个结构体就可以配置好了。

第三步,开关控制,DMA_Cmd给指定的通道使能就完成了。如果选择的是硬件触发,不要忘了在对应的外设调用一下XXX_DMACmd开启一下触发信号的输出。如果需要DMA的中断,那就调用DMA_ITConfig开启中断输出,再在NVIC里配置相应的中断通道,然后写中断函数就行了。

这里结构图没有画中断的部分.

最后在运行的过程中,如果转运完成传输计数器清零。这时再想给传输计数器赋值的话,就DMA失能,写传输计数器,DMA使能这样就行了,这就是DMA的编程思路。

DMA的库函数

打开dma.h拖到最后。

DMA_DeInit

恢复缺省配置。

DMA初始化和DMA结构体初始化函数

DMA_Cmd

DMA使能/失能

DMA_ITConfig

DMA中断输出使能

DMA_SetCurrDataCounter

DMA设置当前数据寄存器,这个函数就是给传输计数器写数据的。

DMA_GetCurrDataCounter

DMA获取当前数据寄存器,这个函数就是返回传输计数器的值。如果想看看还剩多少数据没有转运,就可以调用这个函数获取一下传输计数器。

四个获取标志位状态函数

获取标志位状态、清除标志位、获取中断状态、清除中断挂起位。

代码实现

首先我们定义一下DMA转运的源端数组和目的数组,我们目前总共有四个数据,当然实际情况可能会有成千上万个数据,这样才能发挥出DMA转运的优势。然后第二个目的数组,全给0。

接下来我们的任务就是初始化DMA,然后让DMA把这里DataA的数据转运到DataB里面去。

MyDMA.c

第一步,开启时钟

DMA是AHB总线的设备,所以要用AHB开启时钟的函数。

第一个参数,对于互联型设备,这个参数可以是下面这些值的组合

对于其它设备,这个参数是这下面的组合

互联型是STM32F105/107的型号,我们的芯片是F103,所以我们在下面这个参数表里选,选择DMA1的参数就行了。

第二个参数enable开启DMA1的时钟。

第二步,初始化DMA

然后接下来DMA的初始化,初始化结构体的成员比较多

前面六个成员:外设站点的起始地址、数据宽度、是否自增、存储器站点的起始地址、数据宽度、是否自增。

之后传输方向DIR、缓冲区大小(其实就是传输计数器)、传输模式(其实就是是否使用自动重装)、M2M(选择是否是存储器到存储器,其实就是选择硬件触发还是软件触发)、最后priority优先级这个按照参数要求给一个优先级就行了。

DMA_Init函数的第一个参数是DMAy_Channelx,其中y可以是12,用来选择是哪个DMA,x可以是17DMA1或者可以是15对应DMA2,可以选择是哪一个通道

所以这里第一个参数既选择了是哪个DMA,也选择了是DMA的哪个通道。

我们选择的是DMA1,y就改成1,x选择通道,这里因为是存储器到存储器的转运用的是软件触发,所以通道可以任意选择,就给通道1。

然后我们挨个看一下结构体的成员取值。

第一个成员是外设站点的起始地址,对于SRAM的数据,它的地址是编译器分配的,并不是固定的。所以我们一般不会写绝对地址,而是通过数组名来获取地址。那这里我们就把这个地址提取成初始化函数的参数,这样在初始化的时候,想转运哪个数组,就把哪个数组的地址传进来就行了。

第二个成员:数据宽度,取值:字节,半字,字

这里我们是以字节的方式传输。

第三个成员:地址是否自增,跳转定义看一下。取值的第一个自增enable就是自增,第二个自增disable就是不自增。根据上节的分析,这种数组之间的转运地址肯定是要自增的。所以选第一个。

第四个成员是存储器站点的基地址,我们也把它提取成参数

第五个成员是数据宽度,也选择字节。

第六个成员是存储器站点地址是否自增,我们选enable。

这样外设站点和存储器站点的参数就配置好了。

第七个成员是方向,是指定外设站点是源端还是目的地,参数取如下

这里有两个参数,第一个是外设站点,作为DST,即 destination目的地。外设站点作为目的地,其实就是传输方向是存储器到外设站点这样来传输的。第二个是外设站点作为SRC,即 source源头,也就是外设站点到存储器站点的传输方向。那我们打算是把DataA放在外设站点,DataB放在存储器站点,所以传输方向就是外设站点到存储器站点。所以这里选择第二个参数外设站点作为数据源

第八个成员是buffsize是以数据单元指定缓冲区的大小,就是说要传送几个数据单元,这个数据单元等于传输源端站点的Data size,说简单点就是buffer size就是传输计数器指定传输几次。这个buffsize其实就是直接赋值给传输计数器的寄存。它的取值是0到65535。我们把这个buffer size也提取到函数的参数。然后把它赋值给Buffersize成员

这样传输次数就完成了。

第九个成员指定传输计器是否要自动重装注意:循环模式(也就是自动重装)不能应用在存储器到存储器的情况下。也就是我们上节说的自动重装和软件触发不能同时使用。如果同时使用DMA就会连续触发,永远也不会停下来。这个成员的取值:

这里有两个参数,第一个是循环模式,就是传输计数器自动重装。第二个是正常模式,就是传输计数器,不自动重装,自减到0后停下来。这里我们转运数组是存储器到存储器的传输,转运一次停下来就行了。所以选择第二个正常模式。

第十个成员M2M,就是DMA是否应用于存储器到存储器的转运,存储器到存储器的模式,就是软件触发,取值:

enable就是使用软件触发,第二个disable就是不使用软件触发,也就是使用硬件触发,我们转运数组,所以选择第一个使用软件触发。

最后一个成员priority是指定通道的软件优先级。这里有四个优先级:

第一个是very high非常高,第二个是high高,第三个是medium中等,第四个是low低。如果有多个通道的话,可以指定一下,确保紧急的转运有更高的优先级。目前我们就一个通道,那优先级就随便,可以选择一个中等优先级

到这里,DMA的参数就配置完成了。

那到目前为止,DMA还暂时不会工作。

DMA转运有三个条件:

第一个条件:传输计数器大于零。

第二个条件:触发源有触发信号。

第三个条件:DMA使能。

三个条件缺一不可。

目前如果传一个大于零的数给size的话,第一个条件满足。触发源为软件触发,所以一直都有触发信号,第二个条件满足。最后一个条件DMA还没有使能,第三个条件不满足。

第三步,开关控制

所以到目前为止,DMA还不会工作。如果想在初始化之后就立刻工作的话。可以在这最后加上DMA_Cmd,第一个参数DMA1_Channel1,第二个enable。使能DMA之后,三个条件满足DMA就会进行数据转运了。

转运一次传输计数器自减一次,当传输计数减到零之后,转运完成。之后第一个条件就不满足了,转运停止,这样就完成了一次数组之间的数据转运。

现在我们是初始化之后,立刻就进行转运,并且转运一次之后,DMA就停止了。如果DataA的数据又变化了,我们想再转运一次,那该怎么办?

启动DMA转运的函数

这时我们就需要给传输计数器重新赋值了。我们可以在初始化之后再写个函数,调用一次这个函数就再次启动一次DMA转运。在里面我们需要重新给传输计数器赋值,传输计数器赋值必须要先给DMA失能。

然后就可以给传输计数器赋值了,我们需要用到这个函数

第一个参数是DMAy_Channelx,选择DMA和通道,第二个参数是指定要给传输计数器写入的值,这里我们需要获取一下初始化时的size参数。但是它俩不在一个函数,不能直接传递过来,所以我们可以在这上面定义一个全局变量,然后初始化的时候,把size往这个全局变量也存一份。

之后在这个函数里就可以使用全局变量MyDMA_Size了,这样就可以重新给传输计数器赋值了。

最后再次使能DMA,就会再次开始转运。

然后我们先在上面写Disable,不让DMA初始化之后就立刻进行转运,而是等调用Transfer函数之后,再进行转运。

在转运开始之后,我们还需要做一个工作,就是等待转运完成。

我们需要用到这个函数查看一下标志位:

总共就是四种标志位,所有的通道都是这四种标志位

这里我们需要检查DMA1通道1转运完成的标志位,所以选择这个:

转运完成之后,标志位置1,所以我们需要加一个while循环等待这个标志位,如果没有完成,就一直循环等待,这样就实现了等待转运完成的效果了。标志位置1之后,不要忘了清除标志位,这个标志位是需要手动清除的,清除要用到这个函数参数和上面的一样:

到这里,我们这个函数就全部写完了

MyDMA.c

#include "stm32f10x.h"                  // Device header

uint16_t MyDMA_Size;					//定义全局变量,用于记住Init函数的Size,供Transfer函数使用

/**
  * 函    数:DMA初始化
  * 参    数:AddrA 原数组的首地址
  * 参    数:AddrB 目的数组的首地址
  * 参    数:Size 转运的数据大小(转运次数)
  * 返 回 值:无
  */
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
	MyDMA_Size = Size;					//将Size写入到全局变量,记住参数Size
	
	/*开启时钟*/
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);						//开启DMA的时钟
	
	/*DMA初始化*/
	DMA_InitTypeDef DMA_InitStructure;										//定义结构体变量
	DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;						//外设基地址,给定形参AddrA
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;	//外设数据宽度,选择字节
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;			//外设地址自增,选择使能
	DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;							//存储器基地址,给定形参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;								//转运的数据大小(转运次数)
	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);							//将结构体变量交给DMA_Init,配置DMA1的通道1
	
	/*DMA使能*/
	DMA_Cmd(DMA1_Channel1, DISABLE);	//这里先不给使能,初始化后不会立刻工作,等后续调用Transfer后,再开始
}

/**
  * 函    数:启动DMA数据转运
  * 参    数:无
  * 返 回 值:无
  */
void MyDMA_Transfer(void)
{
	DMA_Cmd(DMA1_Channel1, DISABLE);					//DMA失能,在写入传输计数器之前,需要DMA暂停工作
	DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);	//写入传输计数器,指定将要转运的次数
	DMA_Cmd(DMA1_Channel1, ENABLE);						//DMA使能,开始工作
	
	while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);	//等待DMA工作完成
	DMA_ClearFlag(DMA1_FLAG_TC1);						//清除工作完成标志位
}

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

Main.c

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

uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};				//定义测试数组DataA,为数据源
uint8_t DataB[] = {0, 0, 0, 0};							//定义测试数组DataB,为数据目的地

int main(void)
{
	/*模块初始化*/
	OLED_Init();				//OLED初始化
	
	MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4);	//DMA初始化,把源数组和目的数组的地址传入
	
	/*显示静态字符串*/
	OLED_ShowString(1, 1, "DataA");
	OLED_ShowString(3, 1, "DataB");
	
	/*显示数组的首地址*/
	OLED_ShowHexNum(1, 8, (uint32_t)DataA, 8);
	OLED_ShowHexNum(3, 8, (uint32_t)DataB, 8);
		
	while (1)
	{
		DataA[0] ++;		//变换测试数据
		DataA[1] ++;
		DataA[2] ++;
		DataA[3] ++;
		
		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);		//显示数组DataB
		OLED_ShowHexNum(4, 4, DataB[1], 2);
		OLED_ShowHexNum(4, 7, DataB[2], 2);
		OLED_ShowHexNum(4, 10, DataB[3], 2);
		
		Delay_ms(1000);		//延时1s,观察转运前的现象
		
		MyDMA_Transfer();	//使用DMA转运数组,从DataA转运到DataB
		
		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);		//显示数组DataB
		OLED_ShowHexNum(4, 4, DataB[1], 2);
		OLED_ShowHexNum(4, 7, DataB[2], 2);
		OLED_ShowHexNum(4, 10, DataB[3], 2);

		Delay_ms(1000);		//延时1s,观察转运后的现象
	}
}

这样就能看到DataB数组每隔一秒后,被DMA转运进来的数据就和DataA数组的一样了

运行效果:

STM32-DMA数据转运

如果你想把Flash的数据转运到SRAM里的话,可以在DataA数组前面加一个const,即把DataA定义在Flash里面

然后这一段代码就不能要了,因为const数据不能重新更改

第二个代码:DMA+AD多通道

接线图:

这个和上节AD多通道的接线是一样的。

复制上一节AD多通道的工程并改名:

说明:我每次只说程序要修改的部分,其他复制方面的重复步骤会略过,但是最后我都会附上本次程序的完整代码,大家可以看着完成代码解读程序逻辑!

我们是在上节AD.c的基础上修改的。

上节ADC的配置步骤:

前面说的DMA的配置步骤:

把ADC和DMA给配合起来,我们要将ADC配置成连续扫描+DMA循环转运的模式。

配合起来的流程就看这个图

ADC和DMA配合起来的配置步骤

第一步,开启RCC时钟,开启ADC1、GPIOA和DMA1的时钟,另外这里ADC CLK的分频器也需要配置一下;

第二步,配置GPIO,把需要用的GPIO配置成模拟输入的模式。

第三步,配置多路开关,把通道接入到规则组列表里。这个过程就是我们之前说的点菜,把各个通道的菜列在菜单里。

第四步,配置ADC转换器,在库函数里是用结构体来配置的,要使能连续转换模式,每转换一次规则组序列后立刻开始下一次转换。

第五步,配置DMA,就可以直接调用DMA_Init,所有的参数通过一个结构体就可以配置好了,其中,模式要选择循环模式,与ADC的连续转换一致。

第六步,就是开关控制,调用一下ADC_Cmd和DMA_Cmd的函数开启ADC和DMA,但是不要忘了ADC1触发DMA1的信号使能,就是这里:

这里有三个硬件触发源,具体使用哪个,取决于把哪个的DMA输出给开启了,要调用ADC_DMACmd这个函数开启ADC1作为硬件触发源。

第七步,ADC校准,这样可以减小误差。

第八步,配置ADC触发,软件触发ADC开始工作,由于ADC处于连续转换模式,故触发一次后ADC就可以一直连续不断地工作,这里要用到ADC_SoftwareStartConvCmd这个函数,调用一下就能软件触发转换了,它控制AD的启动和结束,对应结构图中的START这里:

代码实现

第一步,开启RCC时钟

第二步,配置GPIO

第三步,配置多路开关

首先我们要扫描PA0到PA3这四个通道,所以点菜菜单放在这里

这样菜单就点好了,菜单上的1到4号的空位我填上了0到3这四个通道。当然这个通道和次序可以任意修改,这样最终结果存放的顺序也会相应变化。

第四步,配置ADC转换器

点菜完成之后继续往下看ADC扫描模式,这个参数要改成enable,告诉厨师,我们点了多个菜,不要指盯第一个菜看。然后number of channel改成4,告诉厨师,我点的是四个菜,看前四个位置就可以了。

连续模式可以是连续扫描也可以是单次扫描这里我们用连续扫描模式即Enable

到这里ADC扫描模式就配置完成了之后,我们来配置下DMA。

第五步,配置DMA

可以把DMA想象成服务员,ADC这个厨师把菜做好了,DMA这个服务员要尽快把菜端出来,防止被覆盖。

DMA的第一个参数,外设基地址,这里是端菜的源头,厨师把菜做好,就放在ADC_DR寄存器里,所以端菜的源头地址就填ADC_DR的地址。之前我们也算过ADC1的DR寄存器地址就是0x4001 244C,所以可以直接这样来填,不过我们一般都不自己算,因为库函数已经帮我们算好了,所以这里可以这样写 (uint32_t)&ADC1->DR。这样得到的结果,其实就是0x4001 244C,那这样外设地址就填好了。

数据宽度,我们想要DR寄存器低十六位的数据,所以数据宽度就是Halfworld以半字十六位来转运(高16位是ADC2的,所以不用管)。

外设地址是否这个成员给disable,不自增,始终转运同一个位置的数据。

接下来存储器站点,存储器地址,也就是端菜的目的地。我们想要把数据存在SRAM数组里,所以我们先在前面定义一个数组,

然后赋值给这个成员就可以了

数据宽度也是半字。

地址是否自增给enable存储储器的地址是自增的,每转运一次挪一个坑。

到这里DMA的源端和目的地的参数就配置好了。

传输方向外设站点是源。

传输数量给4个,因为有4个ADC通道,所以传输4次。

传输模式这个可以给正常的单次模式也可以给自动重装的循环模式。这里我们配置成循环模式

然后M2M要给disable不使用软件触发,我们需要硬件触发,触发源为ADC1。厨师每个菜做好了,叫我一下,我再去端菜,这样才是合适的时机。

最后所有的参数都配置到DMA1的通道1里面去。这里通道就不能任意选择了。

这里要上节讲过的DMA1请求映像那个框图,ADC1的硬件触发是只接在了DMA1的通道1上。

第六步,就是开关控制

接着DMA_Cmd可以直接使能。

这时DMA转运的三个条件,第一个传输计数器不为零,满足。第三个DMA使能,满足,但是第二个触发源有信号,目前是不满足的,因为这里是硬件,触发ADC还没启动,不会有触发信号。所以这里DMA使能之后不会立刻工作。

最后在ADC使能之前,还有一个事情需要做,就是开启ADC到DMA的输出。这个我们上一节说过。这里有三个硬件触发源,具体使用哪个,取决于把哪个的DMA输出给开启了。

这里我们在adc.h里面找这个函数,

这个函数就是用来开启DMA触发信号的。

注意:DMA一定要在ADC使能之前开启,否则可能会出错。

第七步,ADC校准

到目前为止,ADC和DMA配合工作的配置就完成了。

第八步,配置ADC触发

最后再触发ADC,ADC得手动开启才能自动触发DMA,,所以是软件触发。放在初始化的最后一行

当ADC触发之后,ADC连续转换,DMA循环转运,两者一直在工作。始终把最新的转换结果刷新到SRAM数组里。当我们想要数据的时候,随时去数组里取就行了。

AD.c

#include "stm32f10x.h"                  // Device header

uint16_t AD_Value[4];					//定义用于存放AD转换结果的全局数组

/**
  * 函    数:AD初始化
  * 参    数:无
  * 返 回 值:无
  */
void AD_Init(void)
{
	/*开启时钟*/
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);	//开启ADC1的时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//开启GPIOA的时钟
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);		//开启DMA1的时钟
	
	/*设置ADC时钟*/
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);						//选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz
	
	/*GPIO初始化*/
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	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);					//将PA0、PA1、PA2和PA3引脚初始化为模拟输入
	
	/*规则组通道配置*/
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);	//规则组序列1的位置,配置为通道0
	ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);	//规则组序列2的位置,配置为通道1
	ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);	//规则组序列3的位置,配置为通道2
	ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);	//规则组序列4的位置,配置为通道3
	
	/*ADC初始化*/
	ADC_InitTypeDef ADC_InitStructure;											//定义结构体变量
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;							//模式,选择独立模式,即单独使用ADC1
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;						//数据对齐,选择右对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;			//外部触发,使用软件触发,不需要外部触发
	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;							//连续转换,使能,每转换一次规则组序列后立刻开始下一次转换
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;								//扫描模式,使能,扫描规则组的序列,扫描数量由ADC_NbrOfChannel确定
	ADC_InitStructure.ADC_NbrOfChannel = 4;										//通道数,为4,扫描规则组的前4个通道
	ADC_Init(ADC1, &ADC_InitStructure);											//将结构体变量交给ADC_Init,配置ADC1
	
	/*DMA初始化*/
	DMA_InitTypeDef DMA_InitStructure;											//定义结构体变量
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;				//外设基地址,给定形参AddrA
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;	//外设数据宽度,选择半字,对应16为的ADC数据寄存器
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;			//外设地址自增,选择失能,始终以ADC数据寄存器为源
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;					//存储器基地址,给定存放AD转换结果的全局数组AD_Value
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;			//存储器数据宽度,选择半字,与源数据宽度对应
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;						//存储器地址自增,选择使能,每次转运后,数组移到下一个位置
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;							//数据传输方向,选择由外设到存储器,ADC数据寄存器转到数组
	DMA_InitStructure.DMA_BufferSize = 4;										//转运的数据大小(转运次数),与ADC通道数一致
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;								//模式,选择循环模式,与ADC的连续转换一致
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;								//存储器到存储器,选择失能,数据由ADC外设触发转运到存储器
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;						//优先级,选择中等
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);								//将结构体变量交给DMA_Init,配置DMA1的通道1
	
	/*DMA和ADC使能*/
	DMA_Cmd(DMA1_Channel1, ENABLE);							//DMA1的通道1使能
	ADC_DMACmd(ADC1, ENABLE);								//ADC1触发DMA1的信号使能
	ADC_Cmd(ADC1, ENABLE);									//ADC1使能
	
	/*ADC校准*/
	ADC_ResetCalibration(ADC1);								//固定流程,内部有电路会自动执行校准
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);
	ADC_StartCalibration(ADC1);
	while (ADC_GetCalibrationStatus(ADC1) == SET);
	
	/*ADC触发*/
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);	//软件触发ADC开始工作,由于ADC处于连续转换模式,故触发一次后ADC就可以一直连续不断地工作
}

AD.h

#ifndef __AD_H
#define __AD_H

extern uint16_t AD_Value[4];

void AD_Init(void);

#endif

Main.c

主循环啥都不干,直接读取数组的结果。

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

int main(void)
{
	/*模块初始化*/
	OLED_Init();				//OLED初始化
	AD_Init();					//AD初始化
	
	/*显示静态字符串*/
	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);		//显示转换结果第0个数据
		OLED_ShowNum(2, 5, AD_Value[1], 4);		//显示转换结果第1个数据
		OLED_ShowNum(3, 5, AD_Value[2], 4);		//显示转换结果第2个数据
		OLED_ShowNum(4, 5, AD_Value[3], 4);		//显示转换结果第3个数据
		
		Delay_ms(100);							//延时100ms,手动增加一些转换的间隔时间
	}
}

运行结果和上节的AD多通道的现象一样,就不视频展示了。

另外我们还可以再加一个外设,比如定时器。

ADC用单次扫描,再用定时器去定时触发。这样就是定时器触发ADC,ADC触发DMA,整个过程完全自动,不需要程序手动进行操作,节省软件资源。这就是STM32中硬件自动化的一大特色。各个外设互相连接,互相交织,不再是传统的这样一个CPU单独控制多个独立的外设这样的新型结构。而是外设之间互相连接,互相合作,形成一个网状结构。这样在完成某些简单且繁琐的工作的时候,就不需要CPU来统一调度了,可以直接通过外设之间的相互配合,自动完成这些繁琐的工作。这样不仅可以减轻CPU的负担,还可以大大提高外设的性能。

本节就到这里,下节继续。

QQ交流群:963138186

本篇就到这里,下篇继续!欢迎点击下方订阅本专栏↓↓↓

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐