stm32f103c8t6学习笔记(学习B站up江科大自化协)-DMA

DMA简介

        ·DMA主要用于协助CPU完成数据转运的工作

        ·DMA,英文全称Direct Memory Access,DMA这个外设是可以直接访问STM32内部存储器的,包括运行内存SRAM,程序存储器flash和寄存器等等,DMA都有权限访问,所以DMA能完成数据转运的工作

        ·第二行的外设指的是外设寄存器,一般是外设的数据寄存器DR(Data Register),比如ADC的数据寄存器和串口的数据寄存器等等,存储器指的是运行内存sram和程序存储器flash,是存储变量数组和程序代码的地方。

        ·可配置的通道,指的是数据转运的路径,从一个地方转运到另一个地方需要占用一个通道,如果有多个通道进行转运,可以各转各的,互不干扰,

        ·如果DMA进行的是存储器到存储器的数据转运,比如想把flash里的数据转运到sram里去,那就需要软件触发,DMA会一股脑将数据以最快速度转运完成。

        如果DMA进行的是外设到存储器的数据转运,就不能一股脑转运。外设的数据转运是有时机的,需要使用硬件触发,比如转运ADC的数据,得等ADC每个通道AD转换完成之后,硬件触发一次DMA,之后DMA再进行转运,触发一次转运一次,数据才是正确的。

        存储器到存储器的转运一般使用软件触发,外设到存储器的转运一般使用硬件触发。特定的硬件触发意味着每个DMA的硬件触发源是不一样的,要使用某个外设的硬件触发源,就得使用他连接的那个通道,而不能任意选择通道

        ·c8t6这个芯片只有7个通道,没有DMA2

存储器映像

        ·ROM,只读存储器,是一种非易失性、掉电不丢失的存储器

ROM分为三块:
        第一块是程序存储器flash,也就是主闪存,用途是存储c语言编译后的代码,也就是下载程序的位置。运行程序一般是从主闪存里面开始运行。这一块存储器STM32分配的地址是0x0800 0000,起始地址,也就是第一个字节的地址0800,然后剩余字节的地址依次增长,每个字节都分配一个独一无二的地址,之后程序才能精准的访问这个寄存器。终止地址取决于他的容量,编到哪里,哪里就是终止地址。如果在软件里看到,某个数据的地址是0800开头的,一般可以确定为主闪存的数据。
        第二块和第三块也是掉电不丢失,不难看出这两块存储器的位置是在ROM区的最后面,实际上存储介质也是flash,只不过一般flash指的是主闪存flash,而不是指这两个区域。对于这两块区域的地址都是1FFF开头的。系统存储器的用途是存储bootloader,用于串口下载。bootloader一般是芯片出厂自动写入的,一般不允许修改。选项字节的存储器一般用于存储一些独立于程序代码的配置参数,位置是在ROM的最后面,下载程序可以不刷新选项字节的内容,这样选项字节的配置就可以保持不变,选项字节里主要存储的是flash的读保护和写保护,还有看门狗等等的配置。

        ·RAM,随机存储器,是一种    易失性,掉电    丢失的存储器
        2000开头的是ram区域,首先是运行内存sram,分配地址是0x2000 0000,用途是用于存储运行过程的临时变量,也就是在程序中定义变量、数组、结构体的地方,类比电脑的话,运行内存就是内存条。
        外设寄存器,地址是0x4000 0000,用途是存储各个外设的配置参数,也就是初始化各个外设最终读写的东西。外设寄存器也是存储器的一种,存储介质其实也是sram,不过一般习惯把运行内存叫做sram,外设寄存器直接叫做寄存器。
        内核外设寄存器,地址是0xE000 0000,用途是存储内核各个外设的配置参数,内核外设就是NVIC和systick,因为内核外设和其他外设不是一个厂家设计的,所以地址被分开。

DMA基本结构

        ·图中的外设寄存器和存储器(flash、sram)这两个部分是数据转运的两大站点,在STM32里,一般特指的是flash和sram,不包含外设寄存器。外设寄存器一般直接称作外设,寄存器也是存储器的一种,DMA的数据转运可以从外设到存储器,也可以从存储器到外设,具体转运方向有一个方向的参数可以进行控制。还有一种转运方式是从存储器到存储器,比如flash到sram或sram到sram这两种方式。由于flash是只读的,所以DMA不可以进行从sram到flash或flash到flash的转运操作。

        ·左右两边分别是外设和存储器的三个参数,要进行数据转运首先要指定从哪到哪,以及怎么转。所以外设和存储器都有三个参数
                第一个是起始地址,有外设端的起始地址和存储器端的起始地址,这两个参数决定了数据是从何来到何去,
                第二个参数是数据宽度,这个参数的作用是指定一次转运要按多大的数据宽度来进行,可以选择字节byte、半字halfword和字word。字节是八位,也就是一次转运一个uint8_t大小的数据,半字是uint16_t大小,字是uint32_t大小
                第三个参数是地址是否自增,这个参数的作用是指定一次转运完成之后,下一次转运是否要把地址移动到下一个地址去,相当于指针p++。比如ADC扫描模式,用DMA进行数据转运,外设地址是ADC_DR寄存器,寄存器这边显示地址是不需要自增的,原因是自增的话下次转运就跑到别的寄存器去了,但是存储器这边地址需要自增,每转运一次数据后都往后挪个坑,否则下次在转运的时候就会把上次的数据覆盖,这就是地址是否自增的作用。

        ·如果要进行存储器到存储器的数据转运,就需要把其中一个存储器的地址放在外设的站点。只要在外设起始地址里写入flash或sram的地址,那就会去flash和sram里面找数据。虽然这个站点叫做外设寄存器,仅是名字而已,并不代表这个地址只能写寄存器的地址,如果写flash就会去flash里面找,写sram就会去sram里面找,没有限制,甚至可以在外设站点写存储器的地址,存储器站点写外设的地址,然后方向参数反过来即可。

        ·下面有个传输计数器,用来指定总共需要转运几次,这个传输计数器是一个递减计数器,比如给他写一个5,那么DMA就只能进行5次数据转运,转运过程中,每转运一次计数器的数就会减1,当传输技术器减到0之后就不会进行数据转运,同时减到0之后,前边自增的地址也会恢复到起始的位置,以方便后边DMA开始新一轮的转运。

        ·在传输计数器的右边有一个自动重装器,作用是传输计数器减到0之后是否要恢复到最初的值,比如传输计数器初值给5,不使用自动重装器,那转运5次之后DMA就结束了,如果使用自动重装器,那重装5次计数器减到0之后就会立刻重装到初始值5。自动重装器决定了转运的模式,如果不重装那就是正常的单次模式,如果重装就是循环模式,如果想转运一个数组一般就是单次模式,转运一轮就结束;如果是ADC扫描+连续转换,为了配合ADC,DMA也需要使用循环模式。这个循环模式和ADC的连续模式相似,都用于指定一轮工作之后是否立刻开启下一轮工作。

        ·最下边是DMA的触发控制,触发决定了DMA是在什么时候进行转运。触发源有硬件触发和软件触发,具体选择哪个由M2M(Memory to Memory,由于2和two同音,意思是存储器到存储器)这个参数来决定。

        当给M2M时DMA会选择软件触发,这个软件触发并不是调用某一个函数触发一次,这个软件触发的逻辑是:以最快的速度不断地触发DMA,争取快速把传输寄存器清零,完成本轮转换。这里的软件触发和之前外部中断、ADC的软件触发不太一样,可以理解成连续触发,但是软件触发和循环模式不能同时使用,因为软件触发是为了将传输计数器清零,而循环模式是将其清零后自动重装,同时使用的话DMA停不下来。软件触发一般用于存储器到存储器之间的转运,是软件启动,不需要时机。

        当M2M位给0那就是使用硬件触发,硬件触发源可以选择ADC、串口、定时器等等,使用硬件触发的转运一般是和外设有关的转运,这些转运需要一定的时机,比如ADC转换完成、串口发送数据、定时时间到等等,当硬件达到这些时机时,传一个信号来触发DMA进行转运。

        ·开关控制,也就是DMA_CMD函数,当给DMA使能之后,DMA准备就绪,可以进行转运。DMA进行转运有几个条件:

        第一就是开关控制,DMA_Cmd必须使能

        第二就是传输计数器必须大于0

        第三就是触发源必须有触发信号,触发一次转运一次,传输计数器自减一次。当传输计数器等于0且没有自动重装时,此时无论是否触发DMA都不会进行转运,此时需要将DMA_Cmd给disable关闭DMA,再为传输计数器写一个大于0的数,在DMA_Cmd给enable开启DMA,DMA才能继续工作。

        注意:手册规定,写传输计数器时必须先关闭DMA再进行,不能在DMA开启时写传输计数器

DMA请求

        这张图是DMA1的请求映像,下面是DMA的7个通道,每个通道都有一个数据选择器,可以选择硬件触发或软甲触发

        ·图的左侧列外设请求信号,每个通道的硬件触发源都是不同的,如果需要使用ADC1来触发的话就需要选择通道1,如果需要定时器2来更新事件的话那就需要选择通道2。由于每个通道的硬件触发源不同,如果想使用某个硬件触发源的话,就必须使用其所在的通道,这是关于使用硬件触发的注意事项;如果使用软件触发的话通道可以任意选择,因为每个通道的软件触发都是一样的。
        ·图中通道1的硬件触发是ADC1,定时器2的通道3和定时器4的通道1,具体选择哪个触发源是根据对应的外设是否开启了DMA输出来决定的。比如要使用ADC1,那就会有个库函数叫ADC_DMACmd,必须使用这个库函数开启ADC1的这一路输出才有效;如果想使用定时器2的通道3,会有个TIM_DMACmd的函数进行DMA输出控制。关于这三个触发源使用哪个,取决于把哪个外设的DMA输出开启了,如果都开启了,那就变成了一个或门,三个硬件都可以进行触发,一般情况下只开启一个。
        ·最终7个触发源进入到仲裁器,进行优先级判断,最终产生内部的DMA1请求,优先级的判断类似于中断优先级。默认优先级是通道号越小优先级越高,可在程序中进行配置。
 

        这里的意思应该是,EN并不是数据选择器的控制位,而是选择数据寄存器是否工作,EN=0数据寄存器不工作,EN=1数据寄存器工作。软件触发后面跟着M2M位的意思是当M2M=1时选择软件触发。

数据宽度与对齐

        数据宽度与对齐,在前面的数据转运的两个站点都有一个数据宽度的参数,如果数据宽度都一样那就是一个一个正常的转运,如果数据宽度不一样,需要参考操作表格。表格中第一列是源端宽度,第二列是目标宽度,第三列是传输数目,当源端和目标都是8位时,转运第一步在源端的0位置读数据B0,在目标的0位置写数据B0,就是把这个B0从表格左边移到右边,接着就是把B1移动到右边,接着就是B2B3。源端是8位目标是16位,这个的操作是源端读B0,目标写入00B0,读B1写入00B1,如果目标的数据宽度比源端的数据宽度大,那就在目标前面多出来的空位补0。在8位转到32位同理,将前边空出来的都补0。当目标数据宽度比源端数据宽度小的时候,比如16位转8位,现象就是读B1和B0,只写入B0,读B3B2,只写入B2,写就是把多出来的高位舍弃,后边的也是类似操作。总结就是如果把小的数据转到大的里边就会补0,如果把大的数据转到小的里面高位就会舍弃。
 

DMA数据转运

        .使用DMA,进行存储器到存储器的数据转运,也就是把一个数组里的数据复制到另一个数组里。

        ·数据转运+DMA,将sram里面的数组DataA转运到另一个数组DataB中的配置方式如下,首先是外设站点和存储器站点的起始地址、数据宽度、地址是否自增这三个参数。在这个任务里外设数组(发送)应该填写DataA的数组的首地址,存储器地址(接收)给DataB数组的地址,数据宽度两个数组类型都是uint8_t,所以数据宽度都是按8位的字节传输,由图可见将数组A0转到数组B0,然后A1到B1等等,所以两个站点的地址都需要自增,都移动到下一个数据的位置,才能继续进行转运。如果左边自增右边补自增,那么最后只会有B0接收到A8的数据,如果左边不自增右边自增,最后B0到B8都是A0的数据,如果都不自增那么只有B0有A0的数据。由于将外设站点转运到存储器站点,方向参数需正向。数组中有7个数据,所以传输计数器给7,自动重装暂不需要。触发部分要使用软件触发,因为这是存储器到存储器的数据转运,无需等待时机,最后调用ADC_Cmd给DMA使能。这里的转运是复制转运,转运完成之后DataA的数据不会消失,仅是把DataA的数据复制到DataB的位置

接线图

        

        这个部分的数据转运是在STM32内部进行的,无需其他外加模块,首先印证一下定义的数据是否储存在相应的地址区间里。

        在主函数写入如下内容,编译烧录后可见OLED显示屏上边显示第一行66是aa的内容,第二行是2000 0000是aa被存储的地址,对照存储器映像的表就知道aa存储的位置是sram区。在sram区的地址必定为20开头,但是具体地址是由编译器决定的。由于现在sram没有其他变量,所以sram就把aa放在sram的第一个位置。

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

uint16_t aa = 0x66;

int main()
{
	OLED_Init();
	
	OLED_ShowHexNum(1,1,aa,3);
	OLED_ShowHexNum(2,1,(uint32_t)&aa,8);	//长度为8 是8个16进制数 表示的是32位
	//显示地址一般使用16进制 对于一个变量取地址之后应该存放在一个指针变量里
	//如果想当做指针变量进行显示的话 需要在前边加上强制类型转换 如果不加强制类型转换 就是指针跨级赋值 会报错
	
	while(1)
	{

	}
}

        在代码中加入const关键字,const uint16_t aa = 0x66;,将aa表示为只能读不能写的常量,由于flash里边的数据也是只能读不能写,因此也对应起来,stm32中用const定义的变量(实际上变成了常量)是存储在flash里边的。编译烧录后发现第二行变成了0800 0808,对照存储器映像表可以发现aa被储存在了flash里边。flash里存储的是程序代码和常量数据。

        0800 0808地址尾部有偏移,没有和前边sram一样安排在第一个位置,这是因为flash里边还有程序代码在前边,所以编译器给这个常量安排的地址相对靠后。

        程序出现大量数据是无需修改时可以用const定义,节省sram空间,比如查找表或字库数据等。

外设寄存器地址

        对于变量或常量来说,地址是由编译器决定的,不同的程序地址可能不一样,非固定。对于外设寄存器地址来说是固定的,可以在手册查询,也可以在程序中直接访问寄存器,比如访问ADC1的DR寄存器,写ADC1->DR即可,代码如下

int main()
{
	OLED_Init();
	
	OLED_ShowHexNum(2,1,(uint32_t)&ADC1->DR,8);	//长度为8 是8个16进制数 表示的是32位
	
	while(1)
	{

	}
}

        烧录编译后发现,显示的内容为4001 244C,对应寄存器映像表,是属于外设寄存器的地址,这个地址是一个固定的,查询数据手册可以查到ADC1的起始地址是4001 2400,DR的偏移地址是4C,所以ADC1的DR地址就是4001 244C,与显示结果相同。

        寄存器的实际地址 =  起始地址 + 偏移地址,这里的ADC1 - > DR,ADC1是结构体指针,指向的是ADC1外设的起始地址,访问结构体成员,相当于是加一个地址偏移,指定地址加偏移就是指定的寄存器,由于ADC1是结构体指针,需要用 - > 取成员。如果希望简单点的操作也可以直接指针访问某个物理地址即可。比如#define ADC1_DR     (uint32_t *)0x4001 244C,然后使用*ADC1_DR取指针内容也可以直接进行访问

        在MyDMA里边写代码,但目前为止DMA还无法正常工作

#include "stm32f10x.h"


void MyDMA_Init(uint32_t AddrA,uint32_t AddrB,uint16_t Size)	//将地址提取成初始化函数的参数 在初始化的时候传递想转运的数组即可
{
//DMA是AHB中的设备,需要开启AHB的时钟
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	
	DMA_InitTypeDef DMA_InitStructure;	
	//外设站点、存储器站点的起始地址、数据宽度、是否自增
	DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;
	//对于sram的数组 地址是编译器分配的 不固定 一般不写绝对地址 而是通过数组名获取地址
	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;
	//缓存区大小,传输计数器 指定传输几次 取值范围0-65535
	DMA_InitStructure.DMA_BufferSize = Size;
	//传输方向	指定地址是源端还是目的地 
	//参数DST是外设站点做目的地(destination)方向就是从存储器站点到外设站点
	//参数SRC是外设站点做源头(source)方向从外设站点到存储器站点
	//这里将外设站点A数据转运到存储器站点B 选择DMA_DIR_PeripheralSRC
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
	//M2M选择是否存储器到存储器 选择软件触发(enable)or硬件触发
	DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;
	//传输模式 是否使用自动重装 自动重装不能和软件触发同时使用 否则会连续触发永远停不下
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
	//按照参数要求给一个优先级即可	如果多个通道可以指定紧急的转运更有优先级 这里随意选择
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
	DMA_Init(DMA1_Channel1,&DMA_InitStructure);//DMAy_Channelx y是选择DMA几 x选择通道 由于软件触发由存储器到存储器 可任意选择	
}

        DMA转运有三个条件:

        ·传输计数器大于0

        ·触发源有触发信号

        ·DMA使能

目前如果传入一个大于0的size的话条件1满足;触发源为软件触发,第二个条件也满足;DMA没有使能,第三个条件不满足。如果想在初始化之后立即工作的话,可以在初始化的末尾加上    DMA_Cmd(DMA1_Channel1,ENABLE);进行使能,转运一次传输计数器自减一次,当传输计数器减到0之后转运完成,同时第一个条件不满足,转运停止。        main.c部分的代码,

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

//DMA 转运 源端数组
uint8_t DataA[] = {0x01,0x02,0x03,0x04};
//DMA 转运 
uint8_t DataB[] = {0,0,0,0};

int main()
{
	OLED_Init();
	
	OLED_ShowHexNum(1,1,DataA[0],2);
	OLED_ShowHexNum(1,4,DataA[1],2);
	OLED_ShowHexNum(1,7,DataA[2],2);
	OLED_ShowHexNum(1,10,DataA[3],2);
	
	OLED_ShowHexNum(2,1,DataB[0],2);
	OLED_ShowHexNum(2,4,DataB[1],2);
	OLED_ShowHexNum(2,7,DataB[2],2);
	OLED_ShowHexNum(2,10,DataB[3],2);
	
	MyDMA_Init((uint32_t)DataA,(uint32_t)DataB,4);
	//传入DataA数组首地址 由于数组名就是取地址 无需加取地址符号 只需强制转换为uint32_t类型
	
	OLED_ShowHexNum(3,1,DataA[0],2);
	OLED_ShowHexNum(3,4,DataA[1],2);
	OLED_ShowHexNum(3,7,DataA[2],2);
	OLED_ShowHexNum(3,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)
	{

	}
}

        烧录后显示结果为

01 02 03 04
00 00 00 00
01 02 03 04
01 02 03 04

        现在是初始化之后立刻开始转运,并且转运一次之后DMA就停止,如果DataA的数据发生变化想再次转运,则需要给传输计数器重新赋值。创建一个新函数MyDMA_Transfer(),调用这个函数,会再次启动一次DMA转运,在里边需要给传输计数器赋值,赋值前需将DMA失能,需要将cmd函数中enable改成disable,

void MyDMA_Transfer(void)
{
	DMA_Cmd(DMA1_Channel1,DISABLE);
	DMA_SetCurrDataCounter(DMA1_Channel1,MyDMA_Size);
	DMA_Cmd(DMA1_Channel1,ENABLE);
	
	while(DMA_GetFlagStatus(DMA1_FLAG_GL1) == RESET);
	DMA_ClearFlag(DMA1_FLAG_TC1);//需要手动清除标志位
}
#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "MyDMA.h"
#include "OLED.h"

//DMA 转运 源端数组
uint8_t DataA[] = {0x01,0x02,0x03,0x04};
//DMA 转运 
uint8_t DataB[] = {0,0,0,0};


int main()
{
	OLED_Init();
	
	MyDMA_Init((uint32_t)DataA,(uint32_t)DataB,4);
	//传入DataA数组首地址 由于数组名就是取地址 无需加取地址符号 只需强制转换为uint32_t类型
	
	
	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);	
	                                    
	
	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);
		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);		
	}
}

DMA+数据多通道

        ·ADC扫描模式+DMA,左边是ADC扫描模式的执行流程,有七个通道,触发一次之后7个通道依次进行AD转换,然后转换结果放到ADC_DR寄存器里面。在每个单独的通道转换完成之后,进行一个DMA数据转运,并且目的的地址进行自增,防止数据被覆盖。外设地址写入ADC_DR这个寄存器的地址,存储器的地址可以在sram中定义一个数组ADValue,然后把ADValue的地址当做存储器的地址,之后数据宽度由于ADC_DR和sram数组都是uint16_t的数据,所以数据宽度都是16位的半字传输。在图中可见外设地址不自增存储器地址自增,传输方向是外设站点到存储器站点。传输通道有7个所以传输计数器设置为7。ADC如果是单次扫描,那么ADC的传输计数器可以不自动重装,转换一轮即停,如果ADC是连续扫描,可以使用自动重装,在ADC启动下一轮转换的时候,DMA也启动下一轮转运,ADC和DMA同步工作。ADC_DR的值是在ADC单个转换完成之后才有效的,所以DMA转运的时机,需要和ADC单个通道转换完成同步,所以ADC的触发要选择硬件触发。

        ·DMA最常用的用途就是配合ADC的扫描模式,因为ADC扫描模式有数据覆盖的特征,ADC对于DMA的需求非常强烈,其他的外设使用DMA将锦上添花,ADC没有的话功能会受到极大的限制。

接线图

         

        ADC单次扫描+DMA单次转运模式

AD.c部分代码

#include "stm32f10x.h"

uint16_t AD_Value[4];

void AD_Init(void)
{
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);	//6分频 分频之后ADCCLK = 72MHz / 6 = 12MHz
	
	GPIO_InitTypeDef GPIO_InitStructure;
	GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AIN;	
	//在AIN模式下 GPIO口是无效的 断开GPIO口防止输入输出对模拟电压造成干扰 AIN模式算是ADC的专属模式
	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);	
		
	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);

	//初始化ADC
	ADC_InitTypeDef ADC_InitStructure;
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
	//选择连续转换or单次转换 enable是连续模式 disable是单次模式
	ADC_InitStructure.ADC_DataAlign	= ADC_DataAlign_Right;	//数据对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
	//外部触发转换选择 这里不适用外部触发 none 使用软件触发
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;//选择工作在独立模式还是双ADC模式
	ADC_InitStructure.ADC_NbrOfChannel = 4;	//通道数目 指定在扫描模式下会用到几个通道 
	//这个参数在扫描模式下需要用 非扫描模式下整个列表只有第一个序列有效 写多少都没用
	ADC_InitStructure.ADC_ScanConvMode = ENABLE; 
	//扫描模式or非扫描模式 enable是扫描模式 disable是非扫描模式
	ADC_Init(ADC1,&ADC_InitStructure);
	
	
	DMA_InitTypeDef DMA_InitStructure;	
	//外设站点、存储器站点的起始地址、数据宽度、是否自增
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
	//对于sram的数组 地址是编译器分配的 不固定 一般不写绝对地址 而是通过数组名获取地址
	DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;//获取DR寄存器低16位的数据
	DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
	DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;
	DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
	DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;//存储器地址自增
	
	//有4个ADC通道 传输四次
	DMA_InitStructure.DMA_BufferSize = 4;
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
	DMA_Init(DMA1_Channel1,&DMA_InitStructure);
	//ADC1的硬件触发只接在DMA1的通道1上
	ADC_DMACmd(ADC1,ENABLE);
	DMA_Cmd(DMA1_Channel1,ENABLE);
		//开启ADC的电源
	ADC_Cmd(ADC1,ENABLE);
	
	
	ADC_ResetCalibration(ADC1);	//复位校准
	while(ADC_GetResetCalibrationStatus(ADC1) == SET);	//获取复位校准状态
	//加上while循环 如果没有校准完成就在while空循环里等待
	//一旦标志位被硬件清0 这个空循环会自动跳出
	ADC_StartCalibration(ADC1);	//开始校准
	while(ADC_GetCalibrationStatus(ADC1) == SET);	//获取开始校准状态
	//这部分函数在ADC初始化完成之后依次调用即可
}

void AD_GetValue(void)	
{
	//ADC是单次模式 触发之前需要重新写入传输计数器
	DMA_Cmd(DMA1_Channel1,DISABLE);
	DMA_SetCurrDataCounter(DMA1_Channel1,4);
	DMA_Cmd(DMA1_Channel1,ENABLE);
	
	
	ADC_SoftwareStartConvCmd(ADC1,ENABLE);//ADC是单次模式 软件触发转换函数
	
	//转运是在转换完成之后的 需等待转换完成
	while(DMA_GetFlagStatus(DMA1_FLAG_GL1) == RESET);
	DMA_ClearFlag(DMA1_FLAG_TC1);//需要手动清除标志位
}

        main.c的代码

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

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

ADC连续扫描+DMA循环转运模式

需要修改的代码如下

        1.ADC连续模式打开,将disable改成enable

	ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;

        2.DMA循环模式打开

	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;

        3.将ADC触发放在初始化最后一行

	ADC_SoftwareStartConvCmd(ADC1,ENABLE);//ADC是单次模式 软件触发转换函数

当ADC触发之后,ADC连续转换,DMA循环转运,两者一直在工作,刷新到sram数组里,需要数据时直接取即可,将void AD_GetValue(void)    函数删除,主循环中也删去调用,直接从数组中读数据即可。

  • 128
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值