Bootstrap

江协科技STM32学习- P24 DMA数据转运&DMA+AD多通道

      🚀write in front🚀  
🔎大家好,我是黄桃罐头,希望你看完之后,能对你有所帮助,不足请指正!共同学习交流
🎁欢迎各位→点赞👍 + 收藏⭐️ + 留言📝​ 

💬本系列哔哩哔哩江科大STM32的视频为主以及自己的总结梳理📚 

🚀Projeet source code🚀   

💾工程代码放在了本人的Gitee仓库:iPickCan (iPickCan) - Gitee.com

引用:

STM32入门教程-2023版 细致讲解 中文字幕_哔哩哔哩_bilibili

Keil5 MDK版 下载与安装教程(STM32单片机编程软件)_mdk528-CSDN博客

STM32之Keil5 MDK的安装与下载_keil5下载程序到单片机stm32-CSDN博客

0. 江协科技/江科大-STM32入门教程-各章节详细笔记-查阅传送门-STM32标准库开发_江协科技stm32笔记-CSDN博客

【STM32】江科大STM32学习笔记汇总(已完结)_stm32江科大笔记-CSDN博客

江科大STM32学习笔记(上)_stm32博客-CSDN博客

STM32学习笔记一(基于标准库学习)_电平输出推免-CSDN博客

STM32 MCU学习资源-CSDN博客

stm32学习笔记-作者: Vera工程师养成记

stem32江科大自学笔记-CSDN博客

术语:

英文缩写描述
GPIO:General Purpose Input Onuput通用输入输出
AFIO:Alternate Function Input Output复用输入输出
AO:Analog Output模拟输出
DO:Digital Output数字输出
内部时钟源 CK_INT:Clock Internal内部时钟源
外部时钟源 ETR:External clock 时钟源 External clock 
外部时钟源 ETR:External clock mode 1外部时钟源 Extern Input pin 时钟模式1
外部时钟源 ETR:External clock mode 2外部时钟源 Extern Trigger 时钟模式2
外部时钟源 ITRx:Internal trigger inputs外部时钟源,ITRx (Internal trigger inputs)内部触发输入
外部时钟源 TIx:external input pin 外部时钟源 TIx (external input pin)外部输入引脚
CCR:Capture/Comapre Register捕获/比较寄存器
OC:Output Compare输出比较
IC:Input Capture输入捕获
TI1FP1:TI1 Filter Polarity 1Extern Input 1 Filter Polarity 1,外部输入1滤波极性1
TI1FP2:TI1 Filter Polarity 2Extern Input 1 Filter Polarity 2,外部输入1滤波极性2
DMA:Direct Memory Access直接存储器存取

正文:

0. 概述

从 2024/06/12 定下计划开始学习下江协科技STM32课程,接下来将会按照哔站上江协科技STM32的教学视频来学习入门STM32 开发,本文是视频教程 P2 STM32简介一讲的笔记。

引用:

什么是DMA以及DMA的意义-CSDN博客

一,什么是DMA?

  • 1.1 DMA(Direct Memory Access,直接存储器访问)提供在外设与内存、存储器和存储器之间的高速数据传输使用。它允许不同速度的硬件装置来沟通,而不需要依赖于CPU,在这个时间中,CPU对于内存的工作来说就无法使用。
  • 1.2 DMA的意义
     代替 CPU 搬运数据,为 CPU 减负。
    1. 数据搬运的工作比较耗时间;
    2. 数据搬运工作时效要求高(有数据来就要搬走);
    3. 没啥技术含量(CPU 节约出来的时间可以处理更重要的事)。

1.🚢第一个代码: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

在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_ITConfig

DMA中断输出使能

DMA_SetCurrDataCounter

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

DMA_GetCurrDataCounte

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

四个获取标志位状态函数

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

代码实现

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

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

MyDMA.c
第一步,开启时钟

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

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

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

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

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

第二步,初始化DMA

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

前面六个成员:

  • 外设站点的起始地址、
  • 数据宽度、
  • 是否自增、
  • 存储器站点的起始地址、
  • 数据宽度、
  • 是否自增。

之后是

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

MA_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之后,不要忘了清除标志位,这个标志位是需要手动清除的,清除要用到这个函数参数和上面的一样:

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

 

实验1源码

MyDMA.c

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

uint16_t MyDMA_Size;

void MyDMA_Init(uint32_t ArrayA, uint32_t ArrayB, uint16_t Size)
{
	MyDMA_Size = Size;
	
	//RCC AHB外设DMA1时钟使能
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	
	//DMA初始化
	DMA_InitTypeDef DMA_InitStruct;
	//DMA_StructInit(&DMA_InitStruct);
	
	//DMA通道的配置
	DMA_InitStruct.DMA_PeripheralBaseAddr = ArrayA; 					 //DMA外设起始地址
	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //DMA转运长度,字节/半字/字
	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Enable;		 //DMA外设地址是否自增
	DMA_InitStruct.DMA_MemoryBaseAddr = ArrayB;							 //存储器起始地址
	//DMA_InitStruct.DMA_MemoryDataSize = DMA_PeripheralDataSize_Byte;	 //存储器DMA转运长度,字节/半字/字
	//DMA_InitStruct.DMA_MemoryInc = DMA_PeripheralInc_Enable;			 //存储器地址是否自增
	DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;		
	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
	DMA_InitStruct.DMA_BufferSize = Size;								 //DMA计数器
	DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;						 //DMA方向,从外设到存储器/从存储器到外设
	DMA_InitStruct.DMA_M2M = DMA_M2M_Enable;							 //DMA M2M存储器到存储器转运,软件触发(不需要硬件触发源)
	DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;							 //DMA非循环模式,DMA单次执行非自动装载
	DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;					 //DMA通道的的优先级
	DMA_Init(DMA1_Channel1, &DMA_InitStruct);							 //使用DMA1 channel_1
	
	//使能DMA
	DMA_Cmd(DMA1_Channel1, DISABLE);
}

void MyDMA_Transfer(void)
{
	
	DMA_Cmd(DMA1_Channel1, DISABLE);					//去使能DMA
	DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size); 	//设置DMA计数器
	DMA_Cmd(DMA1_Channel1, ENABLE);						//使能DMA
	
	//等待DMA完成
	while(DMA_GetFlagStatus(DMA1_FLAG_TC1) ==  RESET);
	DMA_ClearFlag(DMA1_FLAG_TC1);	//清除DMA完成标记
}

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 "oled.h"
#include "Countersensor.h"
#include "Encoder.h"
#include "Timer.h"
#include "AD.h"
#include "Delay.h"
#include "MyDMA.h"

extern uint16_t Num;

uint8_t ArrayA[4];
uint8_t ArrayB[4];

int main(int argc, char *argv[])
{	uint16_t ADValue;
	float voltage;
	
	OLED_Init();
	MyDMA_Init((uint32_t)ArrayA, (uint32_t)ArrayB, 4);
	
	OLED_ShowString(1, 1, "ArryA");
	OLED_ShowHexNum(1,7,(uint32_t)ArrayA, 8);
	
	OLED_ShowString(3, 1, "ArryB");
	OLED_ShowHexNum(3,7,(uint32_t)ArrayB, 8);
	
	while(1)
	{
		ArrayA[0] += 1;
		ArrayA[1] += 1;
		ArrayA[2] += 1;
		ArrayA[3] += 1;
		
		
		MyDMA_Transfer();

		OLED_ShowHexNum(2, 1, ArrayA[0], 2);
		OLED_ShowHexNum(2, 4, ArrayA[1], 2);
		OLED_ShowHexNum(2, 7, ArrayA[2], 2);
		OLED_ShowHexNum(2, 10, ArrayA[3], 2);
		
		Delay_ms(1000);
		
		OLED_ShowHexNum(4, 1, ArrayB[0], 2);
		OLED_ShowHexNum(4, 4, ArrayB[1], 2);
		OLED_ShowHexNum(4, 7, ArrayB[2], 2);
		OLED_ShowHexNum(4, 10, ArrayB[3], 2);
		Delay_ms(1000);
	}
	
	return 1;
}

实验1实验结果

实验1问题记录 

🙂🙂DMA的外设地址增长方向,外设地址是否自增,参数宏定义和DMA存储设备地址增长方向,存储设备地址是否自增的宏定义不是同一个。

使用错误的话,DMA会工作异常。

查看ad.h宏定义,也可以看到实际的定义值不一样

🙂🙂 DMA使用之前不要忘记初始化DMA

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

接线图:

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

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

上节ADC的配置步骤:

 前面说的DMA的配置步骤:

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

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

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

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

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

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

第四步,配置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数组里。当我们想要数据的时候,随时去数组里取就行了。

实验2源码

AD.c

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

uint16_t MyADValue[4];

void AD_Init(void)
{
	//开启RCC时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);	//RCC APB2 GPIOA时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);	//RCC APB2 ADC1时钟
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);		//RCC AHB  DMA1时钟
	
	//RCC_ADC Clock预分频值
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);	//RCC_ADC_Clock=72Mhz/6=12Mhz
	
	//GPIOA_Pin0配置为模拟输入
	//GPIOA_Pin0作为ADC1的输入通道1,2,3,4
	GPIO_InitTypeDef gpioInitStructure;
	gpioInitStructure.GPIO_Mode = GPIO_Mode_AIN;
	gpioInitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
	gpioInitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &gpioInitStructure);
	
	
	
	//ADC模拟输入多路开关
	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_Init初始化
	ADC_InitTypeDef ADC_InitStruct;
	ADC_StructInit(&ADC_InitStruct);
	
	ADC_InitStruct.ADC_Mode = ADC_Mode_Independent;
	ADC_InitStruct.ADC_DataAlign = ADC_DataAlign_Right;
	ADC_InitStruct.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;	//非外部触发,软件触发方式
	ADC_InitStruct.ADC_ScanConvMode = ENABLE;							//ADC使用扫描模式,配合DMA自动转运
	ADC_InitStruct.ADC_ContinuousConvMode = ENABLE;						//ADC使用连续转换模式
	ADC_InitStruct.ADC_NbrOfChannel = 4;								//ADC规则组里扫描数目
	ADC_Init(ADC1, &ADC_InitStruct);
	
	
	//DMA初始化
	DMA_InitTypeDef DMA_InitStruct;
	DMA_StructInit(&DMA_InitStruct);
	//DMA通道的配置
	DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; 				//DMA外设起始地址
	DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; 	//DMA转运长度,字节/半字/字
	DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;		 		//DMA外设地址是否自增,不自增
	DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)MyADValue;						//存储器起始地址
	DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;		 	//存储器DMA转运长度,字节/半字/字	
	DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;					 	//存储器地址是否自增
	DMA_InitStruct.DMA_BufferSize = 4;								 			//DMA计数器
	DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;						 //DMA方向,从外设到存储器/从存储器到外设
	DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;							 //M2M=0,DMA硬件触发源
	DMA_InitStruct.DMA_Mode = DMA_Mode_Circular;						 //DMA循环模式,自动重装
	DMA_InitStruct.DMA_Priority = DMA_Priority_Medium;					 //DMA通道的的优先级
	DMA_Init(DMA1_Channel1, &DMA_InitStruct);							 //使用DMA1 channel_1
	
	//使能DMA
	DMA_Cmd(DMA1_Channel1, ENABLE);
	
	
	//必须先使能DMA请求,再使能DMA开关,否则可能会出错
	//ADC使能DMA请求
	ADC_DMACmd(ADC1, ENABLE);
	
	//ADC开关
	ADC_Cmd(ADC1, ENABLE);
	
	//ADC校准
	ADC_ResetCalibration(ADC1);							//软件置标志位
	while(ADC_GetResetCalibrationStatus(ADC1) == SET);	//当校准完成之后,硬件自动清除标志位
	ADC_StartCalibration(ADC1);							//软件置标志位
	while(ADC_GetCalibrationStatus(ADC1) == SET);		//当校准完成之后,硬件自动清除标志位
	
	
	//软件ADC触发转换
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

uint16_t GetValue(uint8_t ADC_Channel)
{
	return 0;
}

AD.h

#ifndef __AD_H__
#define __AD_H__

extern uint16_t MyADValue[4];

void AD_Init(void);
uint16_t GetValue(uint8_t ADC_Channel);

#endif

Main.c

#include "stm32f10x.h"                  // Device header
#include "oled.h"
#include "Countersensor.h"
#include "Encoder.h"
#include "Timer.h"
#include "AD.h"
#include "Delay.h"
#include "MyDMA.h"

extern uint16_t Num;

uint8_t ArrayA[4];
uint8_t ArrayB[4];

int main(int argc, char *argv[])
{	uint16_t ADValue;
	float voltage;
	
	OLED_Init();
	//MyDMA_Init((uint32_t)ArrayA, (uint32_t)ArrayB, 4);
	AD_Init();
	
	OLED_ShowString(1, 1, "AD1:");
	OLED_ShowString(2, 1, "AD2:");
	OLED_ShowString(3, 1, "AD3:");
	OLED_ShowString(4, 1, "AD4:");

	while(1)
	{
		OLED_ShowNum(1, 5, MyADValue[0], 4);
		OLED_ShowNum(2, 5, MyADValue[1], 4);
		OLED_ShowNum(3, 5, MyADValue[2], 4);
		OLED_ShowNum(4, 5, MyADValue[3], 4);
		
		Delay_ms(1000);
		

	}
	
	return 1;
}

实验2结果

;