stm32学习笔记---DMA直接存储器存取(代码部分)DMA数据转运/DMA+AD多通道
ADC和DMA配合起来的配置步骤第一步,开启RCC时钟,开启ADC1、GPIOA和DMA1的时钟,另外这里ADC CLK的分频器也需要配置一下;第二步,配置GPIO,把需要用的GPIO配置成模拟输入的模式。第三步,配置多路开关,把通道接入到规则组列表里。这个过程就是我们之前说的点菜,把各个通道的菜列在菜单里。第四步,配置ADC转换器,在库函数里是用结构体来配置的,要使能连续转换模式,每转换一次规
目录
声明:本专栏是本人跟着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可以是1或2,用来选择是哪个DMA,x可以是1到7对应DMA1,或者可以是1到5对应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
本篇就到这里,下篇继续!欢迎点击下方订阅本专栏↓↓↓
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)