stm32学习笔记:DMA

 每个DMA通道硬件触发源是不一样的,要使用某个外设的硬件触发源,就必须使用它连接的那个通道

12个独立可配置的通道:DMA1(7个通道),DMA2(5个通道)

每个通道都支持软件触发和特定的硬件触发

C8T6 DMA资源:DMA1

 ( 比如想把Flash里的一批数据转运到SRAM里,需要软件触发,使用软件触发之后,DMA会以最快都速度把这批数据转运)如果进行外设到存储器的转运,就不能一股脑地转运了,因为外设的数据是有一定的时机的,这时就需要用硬件触发   (比如转运ADC的数据,就需要每个ADC通道转换完成后,硬件触发一次DMA再转运)触发一次转运一次,这样数据才是正确的,才是我们想要的效果,所以存储器到存储器的数据转运一般使用软件触发,外设到存储器要硬件触发)

运算器和控制器一般会合在一起,叫做CPU,所以计算机的核心关键部分就是CPU和存储器

存储器的重要知识点:存储的内容和存储器的地址。外设也是存储器

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

RAM:随机存储器,易失,掉电丢失

ROM 分为3块,分别为程序存储器Flash(主闪存),系统存储器和选项字节

RAM:运行内存SRAM,外设寄存器,内核外设寄存器。

1.寄存器是一种特殊的存储器,一方面,CPU可以对寄存器进行读写,就像读写运行内存一样。另一方面,寄存器的每一位背后,都连接了一根导线,可以用于控制外设电路的状态
2.比如:置引脚的高低电平,导通和断开开关,切换数据选择器,或者多位结合起来,当作计数器,数据寄存器等。所以寄存器是连接软件和硬件的桥梁,软件读写寄存器,就相当于再控制硬件的执行。
3.所以使用DMA进行数据转运就相当于:从某个地址取内容,再放到另一个地址里。为了高效有条理地访问存储器,STM32设计了总线矩阵,左端是主动单元,也就是存储器的访问权。右边是被动单元,只能被左边的主动单元读取。主动单元包括内核的DCode和系统总线,可以访问右边的存储器,DCode专门访问Flash,系统总线访问其他东西。
4.由于DMA要访问数据,所以DMA也必须要有访问的主动权主动单元除了内核CPU,剩下的就是DMA了,每个DMA通道可以分别设置他们转运数据的源地址和目的地址,这样他们就可以各自独立的进行工作了。
5.仲裁器的作用:虽然每个通道可以独立进行设置,但是DMA总线只有一条,所以所有的通道都只能分时复用这一条DMA总线。如果产生了冲突,就会由仲裁器,根据通道的优先级决定谁先用谁后用。
6.在总线矩阵里,也会有仲裁器,如果DMA和CPU都要访问同一个目标,那么DMA就会暂停CPU的访问,防止冲突。不过。此时总线仲裁器仍然会保证CPU得到一般的总线带宽,使CPU能够正常的工作。
7.AHB从设备:DMA自身的寄存器,因为DMA作为一个外设,它自己也会有相应的配置寄存器,上面连接在总线右边的AHB总线上,所以DMA既是总线矩阵的主动单元,可以读写各种存储器,也是AHB总线上的被动单元

 

  1.  DMA请求就是触发的意思,右边的触发源就是各个外设,DMA请求就是DMA的硬件触发源,比如ADC转换完成,串口就受到数据等。
  2. 需要触发DMA转运数据的时候就通过DMA请求线路向DMA发出硬件触发信号,之后DMA就可以执行数据转运的动作了。

DMA的各个部分和作用:

DMA总线:用于访问各个存储器,内部的多个通道可以进行独立的数据转运。

仲裁器:用于调度各个通道,防止产生冲突

AHB从设备:用于配置DMA参数

DMA请求:用于硬件触发DMA的数据转运

DMA基本结构 

画圈的是数据转换的站点,左边是外设寄存器站点,右边是存储器站点包括Flash和SRAM。

STM32存储器一般特指Flash和SRAM,不包含外设寄存器,外设寄存器一般直接称为外设,所以就是外设到存储器,存储器到存储器这样来描述。

 外设和存储器参数的作用:

外设和存储器起始地址作用:决定了数据从哪里来,到哪里去。

数据宽度:作用是指定一次转运要按多大的数据宽度来进行,可以选择字节Byte,半字HalhWord和字Word。字节是8位,半字是16位

地址是否自增作用:指定一次转运完成后,下一次转运要把地址移动到下一个位置去,相当于指针P++ 。比如ADC扫描模式,用DMA进行数据转运,外设地址是ADC_DR寄存器,寄存器的地址不用自增,不然就会移位到其他寄存器里面了。存储器地址需要自增,每转运一次,就往后移动一位,不然会覆盖上一个数据。

进行存储器到存储器的转运,就需要把其中一个存储器的地址放在外设的这个站点。只要在存储器里写Flash或SRAM的地址,他就会去这两个地方找数据。

站点虽然叫外设存储器,并不是说只能写寄存器的地址只是一个名字而已。

 传输计数器:用来指定总共需要转运几次,它是自减计数器,比如你给他写个5,那DMA就只能进行5次数据转运,转运过程中,每转运一次,计数器的数就会减1。当传输计数器减到0后,DMA不再进行数据转运。 另外,它减到0之后,之前自增的地址,也会恢复到起始地址的位置,以方便之后DMA开始新一轮的转运。
 

自动重装器:

作用:传输计数器减到0之后,是否要恢复到最初的值,比如最初的值是5,如果不使用自动重装器,那么转运5次之后就DMA结束。使用自动重装器,转运5次,计数器减到0之后,计数器就会立即重装到初始值5. 不重装,就是单次模式,重装就是循环模式

比如说要转运一个数组,一般是单次模式,转运一轮就结束了。如果是ADC扫描模式+连续转换,为了配合ADC。DMA也需要使用循环模式。和ADC差不多,都是指定一轮工作完成后,是不是立即开始下一轮工作
 

DMA触发控制:

决定DMA在什么时机进行转运,触发源有硬件触发和软件触发,具体选择哪个有M2M参数决定。

M2M就是(Memory to Memory)即存储器到存储器,选择1DMA就会选择软件触发

软件触发执行逻辑:以最快的速度,连续不断地触发DMA,争取快速完成传输计数器清零,完成这一轮的转换工作,给0就是硬件触发。

触发源可以选择:ADC,串口,定时器等,一般都是与外设有关的转运,需要一定的时机,比如ADC转换完成,串口收到数据,定时时间到等。所以需要使用硬件触发,在硬件达到这些时机时,传一个信号过来,触发DMA进行转运。

开关控制:

使用 DMA_Cmd函数,给DMA使能后,DMA准备就绪,可以进行转运

DMA转运的条件:

1.开关控制,DMA_Cmd必须使能

2.传输计数器必须大于0

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

硬件触发注意事项:使用相应的硬件触发需要选择对应的通道,不然就触发不了,选择哪个触发源由对应的外设是否开启DMA输出决定的。

比如要使用ADC1,就需要使用ADC_DMACmd函数进行使能,必须使用这个库函数开启ADC1这一路输出才有效。使用定时器3,会有TIM_DMACmd函数用来进行DMA输出控制,使用哪一个外设触发源,取决于把哪个通道开启了,全开启理论上都可以使用

开关控制开启之后,这七个触发源进入到仲裁器进行优先级判断,最终产生内部DMA1请求,优先级判断与中断类似,序号越小优先级越高,也可以在程序中配置优先级。

DMA进行转运的三个条件

DMA进行转运的三个条件:

1、开关控制,DMA_Cmd必须使能

2、传输计数器必须大于0

3、触发源,必须有触发信号。
注意写传输计数器时,必须要先关闭DMA,在进行,不能再DMA开启时,写传输计数器。

//在里面需要重新给传输计数器赋值,传输计数器赋值,必须要先给DMA失能,
	DMA_Cmd(DMA1_Channel1, DISABLE);
	//给传输计数器赋值
	DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);
	//使能,此时DMA再次进行转运
	DMA_Cmd(DMA1_Channel1, ENABLE);

每个通道都有一个数据选择器,可以选择硬件触发和软件触发。EN并不是数据选择器的控制位,而是决定这个选择器要不要工作。EN=0,数据选择器不工作,EN=1,数据选择器工作。软件触发后面跟(M2M)的意思是当M2M位=1时,选择软件触发。每个通道的硬件触发源都不同,若想使用某个硬件触发源的话,必须使用它所在的通道,这就是硬件触发的注意事项。如你要使用ADC1来触发,就必须选择通道1.如果使用软件触发,那么通道就可以任意选择。

数据宽度与对齐:

如果转运的数据宽度一样,就是正常的一个个转运,如果数据宽度不一样,

比如源宽度是8位,目标宽度是16位,传输B0时,需要在前面高位补上0,传输结果就是00B0.

比如源宽度是16位,目标宽度是8位,传输B1B0时,需要把高位舍去,传输结果就是B0.

 

 具体工作流程:左边是ADC扫描模式的执行流程,在这里有7个通道,触发一次后,7个通道依次进行AD转换,然后将转换结果都放到ADC_DR数据寄存器里面,ADC是连续扫描,那DMA就可以使用自动重装,在ADC启动下一轮转换的时候,DMA也启动下一轮的转换。ADC和DMA同步工作。ADC扫描,在每隔单独的通道转换完成后,没有任何标志位,也不会触发中断。所以我们程序不太好判断某一个通道转换完成的时机是什么时候,但他会产生DMA请求,去触发DMA转运。

DMA库函数

void DMA_DeInit(DMA_Channel_TypeDef* DMAy_Channelx);  //恢复缺省配置
void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct);  //初始化
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);//清除中断挂起位

初始化后立刻转运

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;
	//RCC开启DMA的时钟,DMA是AHB总线的设备,所以用AHB开启时钟的函数
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	//初始化DMA
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;  //外设站点
	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;  //缓存区大小,传输计数器
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;   //传输模式,是否使用自动重装,注意,循环模式和软件触发不能够同时使用,如果同时使用,DMA就会连续触发
	DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;  //选择是否是存储器到存储器,其实就是选择硬件触发还是软件触发,这里选择软件触发
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;  //优先级
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);   //DMA1_Channel1选择是哪个DMA,也选择了DMA的哪个通道(软件触发,所以任意通道都可以)
	//因为这里是存储器到存储器,所以通道可任意选择
	//此处选择DMA1,通道1
	DMA_Cmd(DMA1_Channel1, DISENABLE);
	/*如果选择的是硬件出发,记得在对应的外设调用xxx_DMACmd,开启触发信号的输出*/
	/*如果你需要DMA的中断,就调用DMA_ITConfig,开启中断输出,再在NVIC,配置相应的中断通道,然后写中断服务函数*/
	/*在运行的过程中,如果转运完成,传输计数器清零,这是想再给传输计数器赋值,则需要DMA失能,写传输计数器,DMA使能*/
}
/*DMA转运的三个条件
1、开关控制,DMA_Cmd必须使能

2、传输计数器必须大于0

3、触发源,必须有触发信号。
*/
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_TC1) == RESET);
	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"

//DataA,作为待转运的原数据
uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};
//DataB,作为转运数据的目的地(全局变量默认初始值为0)
uint8_t DataB[] = {0, 0, 0, 0};



int main(void)
{
	OLED_Init();
		//显示dataA
		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);
		//显示dataB
		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);  //因为需要传输4个数据,所以为4
		//转运后
		//显示dataA
		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);
		//显示dataB
		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)
	{

	}
}

程序现象

转运多次


初始化后立刻转运的代码,并且转运一次之后,DMA停止工作,如果DataA的数据变化,需要再转运一次,怎么做呢? 此时需要给传输计数器赋值,调用一次MyDMA_Transfer(void),就再次启动一次DMA转运,在函数里面需要重新给传输计数器赋值,传输计数器赋值,必须要先给DMA失能,然后给传输计数器赋值,DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);,然后给DMA使能。开始转运,但需要等待转运完成, while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);,然后清除标志位。

MyDMA.c

#include "stm32f10x.h"                  // Device header

//全局变量
uint16_t MyDMA_Size;

void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
	//将Size赋值给全局变量
	MyDMA_Size = Size;
	//RCC开启DMA的时钟,DMA是AHB总线的设备,所以用AHB开启时钟的函数
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	//初始化DMA
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;  //外设站点
	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;  //缓存区大小,传输计数器
	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);   //DMA1_Channel1选择是哪个DMA,也选择了DMA的哪个通道(软件触发,所以任意通道都可以)
	//不让DMA初始化后就立刻进行转运,而是等调用Transfer函数后才进行转运
	DMA_Cmd(DMA1_Channel1, DISENABLE);
	/*如果选择的是硬件出发,记得在对应的外设调用xxx_DMACmd,开启触发信号的输出*/
	/*如果你需要DMA的中断,就调用DMA_ITConfig,开启中断输出,再在NVIC,配置相应的中断通道,然后写中断服务函数*/
	/*在运行的过程中,如果转运完成,传输计数器清零,这是想再给传输计数器赋值,则需要DMA失能,写传输计数器,DMA使能*/
}
/*DMA转运的三个条件
1、开关控制,DMA_Cmd必须使能

2、传输计数器必须大于0

3、触发源,必须有触发信号。
*/

//转运多次,需要给传输计数器重新赋值
//调用该函数,就再次启动一次DMA转运
//调用一次,再转运一次
void MyDMA_Transfer(void)
{
	//在里面需要重新给传输计数器赋值,传输计数器赋值,必须要先给DMA失能,
	DMA_Cmd(DMA1_Channel1, DISABLE);
	//给传输计数器赋值
	DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);
	//使能,此时DMA再次进行转运
	DMA_Cmd(DMA1_Channel1, ENABLE);
	//等待转运完成,TC1为转运完成标志位
	while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
	//标志位置1后需要手动清除
	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"

//DataA,作为待转运的原数据
uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};
//DataB,作为转运数据的目的地(全局变量默认初始值为0)
uint8_t DataB[] = {0, 0, 0, 0};



int main(void)
{
	OLED_Init();

	//将原数组和目的数组的地址传递进函数,转运数据长度为4
	MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4);
	
	OLED_ShowString(1, 1, "DataA");
	OLED_ShowString(3, 1, "DataB");
	OLED_ShowHexNum(1, 8, (uint32_t)DataA, 8);//之所以要强转类型,是因为DataA是指针类型
	OLED_ShowHexNum(3, 8, (uint32_t)DataB, 8);
	
	

		
	while (1)
	{
		//第一步,自增,变化一下原数组DataA的测试数据
		/*在给 DataA 数组的元素递增的操作中,每次循环都对 DataA 数组的不同元素进行加一操作,即 DataA[0]++、DataA[1]++、DataA[2]++、DataA[3]++。这样做的目的是为了改变 DataA 数组中的值,以便在后续的代码中能够观察到数据传输的效果。通过递增操作,每次循环 DataA 数组的不同元素的值都会增加,这样在每次数据传输之前和之后都可以通过 OLED 显示来观察到数据的变化。*/
		DataA[0] ++;
		DataA[1] ++;
		DataA[2] ++;
		DataA[3] ++;
		//显示DataA和DataB(转运前)
		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);
		//延时1秒,方便观看
		Delay_ms(1000);
		//第三步 调用MyDMA_Transfer()函数,使用DMA进行数据转运
		MyDMA_Transfer();
		//显示DataA和DataB,检测数据是不是从DataA转运到DataB(转运后)
		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);
		//延时1秒,方便观看
		Delay_ms(1000);
	}
}

程序现象

ADC+DMA应用(两个不同的方法,最后实现同一功能)

ADC扫描模式 DMA数据转运
(使用ADC的扫描模式来实现多通道采集,然后使用DMA进行数据转运)

开启ADC到DMA的输出 

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

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

uint16_t AD_Value[4];

/* ADC扫描模式 + DMA数据转运 */
void AD_Init(void)
{
	//开启ADC1时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
	//开启GPIOA时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	//RCC开启DMA的时钟,DMA是AHB总线的设备,所以用AHB开启时钟的函数
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	
	//该函数用来配置ADCCLK分频器的,他可以对APB2的72Mhz时钟选择2、4、6、8分频,输入到ADCCLK
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	//PA0被初始化成模拟输入的引脚
	GPIO_InitTypeDef GPIO_InitStructure;
	//模拟输入模式,在AIN模式下,GPIO口是无效的,断开GPIO,防止GPIO口的输入输出对模拟电压造成干扰
	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);
	//菜单上的1~4号空位,我填上了0~3这四个通道
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);  //通道0放到序列1
	ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);  //通道1放到序列2
	ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);  //通道2放到序列3
	ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);  //通道3放到序列4
	
	//用结构体初始化ADC
	ADC_InitTypeDef ADC_InitStructure;
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;  // 独立模式
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;  //右对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;  // 不使用外部触发,也就是使用内部软件触发
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; //单次转换还是连续转换
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;  //扫描
	ADC_InitStructure.ADC_NbrOfChannel = 4;  //通道数目,我点了4个菜,你看前4个位置就可以了
	ADC_Init(ADC1, &ADC_InitStructure);
	
	/*配置DMA*/

/* DMA 可以想象为服务员 ADC厨师把菜做好后,DMA这个服务员要尽快把菜端出来,防止覆盖  */
	//初始化DMA
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;  //(端菜的)源头地址
	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;  //存储器地址自增
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;  //传输方向,外设站点是源
	DMA_InitStructure.DMA_BufferSize = 4;  //缓存区大小,传输计数器
	DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;   //传输模式,是否使用自动重装
	//触发源为ADC1,厨师每个菜做好了,教我一下,我再去端菜,这样才是合适的时机
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;  //选择是否是存储器到存储器,其实就是选择硬件触发还是软件触发,此处选择硬件触发
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;  //优先级
	//必须使用DMA1的通道1
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);   //DMA1_Channel1选择是哪个DMA,也选择了DMA的哪个通道
	
	//开启ADC到DMA的输出
	ADC_DMACmd(ADC1, ENABLE);
	
	//不让DMA初始化后就立刻进行转运,而是等调用Transfer函数后才进行转运
	DMA_Cmd(DMA1_Channel1, ENABLE);
	//传输计数器不为零
	//DMA使能
	//但是触发源有信号,目前不满足,因为这里是硬件触发,ADC还没启动,不会有触发信号,所以dma是能后不会立刻工作
	
	
	//开启ADC电源
	ADC_Cmd(ADC1, ENABLE);
	//对ADC进行校准
	ADC_ResetCalibration(ADC1);  //复位校准
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);  //等待复位校准完成(0位复位校准完成,1为初始化复位校准)
	ADC_StartCalibration(ADC1);  //开始校准
	while (ADC_GetCalibrationStatus(ADC1) == SET);  //等待校准完成
	/*至此,ADC初始化已经完成,ADC处于准备就绪的状态*/
}

//因为ADC到DMA通道设定好了,而且自动运行,不需要判断标志位,使用DMA后,不需要其他方式转运
//此函数:调用该函数,ADC开始转换,连续扫描4个通道,DMA也同步进行转运,AD转换结果依次放在上面的AD_Value数组里
void AD_GetValue(void)
{
	
	//在里面需要重新给传输计数器赋值,传输计数器赋值,必须要先给DMA失能,
	DMA_Cmd(DMA1_Channel1, DISABLE);
	//给传输计数器赋值
	DMA_SetCurrDataCounter(DMA1_Channel1, 4);
	//使能,此时DMA再次进行转运
	DMA_Cmd(DMA1_Channel1, ENABLE);

	//ADC还是单次模式,还需要软件触发ADC转换
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
	
	//最后等待ADC转换和DMA转运完成,转运总是在转换之后,等待ADC转换完成的代码就不需要了
	//等待转运完成,TC1为转运完成标志位
	while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
	//标志位置1后需要手动清除
	DMA_ClearFlag(DMA1_FLAG_TC1);

}

AD.h
#ifndef __AD_H
#define __AD_H

//把数据存到SRAM数组里,外部可调用
extern uint16_t AD_Value[4];

void AD_Init(void);
void AD_GetValue(void);

#endif

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)
	{
		//调用getvalue,之后数据就直接跑搭配AD_Value数组里
		AD_GetValue();
		
		//显示结果
		OLED_ShowNum(1,5,AD_Value[0],4);  //通道1
		OLED_ShowNum(2,5,AD_Value[1],4);  //通道2
		OLED_ShowNum(3,5,AD_Value[2],4);  //通道3
		OLED_ShowNum(4,5,AD_Value[3],4);  //通道4
		
		Delay_ms(100);  //让他刷新慢些
	}
}

ADC连续扫描+DMA循环转运

总结:ADC连续扫描+DMA循环转运,此时硬件外设已经实现了相互配合和高度自动化,各种操作都是硬件自己完成,极大地减轻了软件负担,软件什么都不需要做,也不需要进行任何中断。硬件自动就把活干完。

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

uint16_t AD_Value[4];

/* ADC扫描模式 + DMA数据转运 */
void AD_Init(void)
{
	//开启ADC1时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
	//开启GPIOA时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	
	//RCC开启DMA的时钟,DMA是AHB总线的设备,所以用AHB开启时钟的函数
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	
	//该函数用来配置ADCCLK分频器的,他可以对APB2的72Mhz时钟选择2、4、6、8分频,输入到ADCCLK
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);
	
	//PA0被初始化成模拟输入的引脚
	GPIO_InitTypeDef GPIO_InitStructure;
	//模拟输入模式,在AIN模式下,GPIO口是无效的,断开GPIO,防止GPIO口的输入输出对模拟电压造成干扰
	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);
	//菜单上的1~4号空位,我填上了0~3这四个通道
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);  //通道0放到序列1
	ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);  //通道1放到序列2
	ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5);  //通道2放到序列3
	ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5);  //通道3放到序列4
	
	//用结构体初始化ADC
	ADC_InitTypeDef ADC_InitStructure;
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;  // 独立模式
	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_InitStructure.ADC_NbrOfChannel = 4;  //通道数目,我点了4个菜,你看前4个位置就可以了
	ADC_Init(ADC1, &ADC_InitStructure);
	
	/*配置DMA*/

/* DMA 可以想象为服务员 ADC厨师把菜做好后,DMA这个服务员要尽快把菜端出来,防止覆盖  */
	//初始化DMA
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;  //(端菜的)源头地址
	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;  //存储器地址自增
	DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;  //传输方向,外设站点是源
	DMA_InitStructure.DMA_BufferSize = 4;  //缓存区大小,传输计数器
	DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;   //传输模式,是否使用自动重装
	//触发源为ADC1,厨师每个菜做好了,教我一下,我再去端菜,这样才是合适的时机
	DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;  //选择是否是存储器到存储器,其实就是选择硬件触发还是软件触发,此处选择硬件触发
	DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;  //优先级
	//必须使用DMA1的通道1
	DMA_Init(DMA1_Channel1, &DMA_InitStructure);   //DMA1_Channel1选择是哪个DMA,也选择了DMA的哪个通道
	
	//开启ADC到DMA的输出
	ADC_DMACmd(ADC1, ENABLE);
	
	//不让DMA初始化后就立刻进行转运,而是等调用Transfer函数后才进行转运
	DMA_Cmd(DMA1_Channel1, ENABLE);
	
	//开启ADC电源
	ADC_Cmd(ADC1, ENABLE);
	//对ADC进行校准
	ADC_ResetCalibration(ADC1);  //复位校准
	while (ADC_GetResetCalibrationStatus(ADC1) == SET);  //等待复位校准完成(0位复位校准完成,1为初始化复位校准)
	ADC_StartCalibration(ADC1);  //开始校准
	while (ADC_GetCalibrationStatus(ADC1) == SET);  //等待校准完成
	/*至此,ADC初始化已经完成,ADC处于准备就绪的状态*/
	//ADC触发后,ADC连续转换,DMA循环转运,两者一直在工作,始终把最新的转换结果刷新到SRAM数组里
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}



AD.h
#ifndef __AD_H
#define __AD_H

//把数据存到SRAM数组里,外部可调用
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();
	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);  //通道1
		OLED_ShowNum(2,5,AD_Value[1],4);  //通道2
		OLED_ShowNum(3,5,AD_Value[2],4);  //通道3
		OLED_ShowNum(4,5,AD_Value[3],4);  //通道4
		
		Delay_ms(100);  //让他刷新慢些
	}
}

程序现象:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/317214.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

陶瓷碗口缺口检测-图像形态学

图像形态学 对得到的灰度图像,需要进行二值化处理和区域填充。二值化涉及两个步骤,第一,对图像行图像分割,将图像分割成目标和和背景;第二,对分割后图像进行区域填充。本例中的背景为黑色,可以…

前端工程化相关

工具方法: 知道软件包名,拿到源码或者路径的方法 在浏览器输入以下内容,就可以找到你想要的。。。 unpkg.com/输入包名 一、模块化 ESM特性清单: 自动采取严格模式,忽略“use strict”每个ESM模块都是单独的私有作用…

x-cmd pkg | csvkit - csv 实用工具集

目录 介绍首次用户功能特点类似工具与竞品进一步阅读 介绍 csvkit 是一个用Python编写的工具包,用于处理CSV文件。该工具包提供了一组命令行工具,可用于转换、查询和分析CSV文件。csvkit的主要命令包括csvcut(用于选择特定列)、c…

又涨了:net的域名铁定涨价

关注卢松松,会经常给你分享一些我的经验和观点。 又要涨价了,又要涨价了,又要涨价了!继.com域名涨价后,.net的域名也逐步涨价。最近一年来域名疯狂涨价,几个月内已经几乎翻番。 阿里云2月1日起上调.net英文域名价格…

allegro PCB设计心得笔记(二) PCB板框设计心得

Cadence Allegro软件设计PCB板框时,使用Add -> line,在Option选择Board Geometry/Outline,根据PCB需要输入对应坐标,设计好板框。 使用Z-Copy命令设计Route Keepin和Package Keepin时,需要先使用使用Shape -> Co…

仿真验证方法(3)——物理验证

目录 一、物理验证的分类 二、DRC 2.1 设计规则 2.2 规则示例 2.3 线宽违例 2.4 间距违例 2.5 交叠违例 三、金属覆盖图形密度检查 四、天线比率检查 4.1 起因 4.2 计算 4.3 改进 五、LVS检查 六、物理验证常用的EDA工具 七、总结 一、物理验证的分类 对于物理验…

哪里能找到好用的PPT模板?12个免费模板网站让你畅快办公!

你是否有过这样的经历,在准备重要会议或者演讲的时候,为找不到合适的PPT模板而困扰?或是在网上漫无目的地搜寻,结果收获的是设计平淡无奇的PPT模板? 如果你有同样的疑问,那么你来对地方了!在这…

什么是有机搜索引擎优化以及如何入门

什么是有机搜索引擎优化? 有机搜索引擎优化,简称 SEO,是指从搜索引擎的无偿搜索结果中增加网站流量的做法。 未付费搜索结果是获得的列表,而不是付费的。 这样做的目的是让您的网页在与您业务相关的未付费搜索结果中排名靠前。…

高级路由技术案例

文章目录 案例项目一:1、静态ECMP和浮动静态路由配置实验2、浮动静态路由配置 案例项目二:使用filter-policy过滤路由案例项目三:IS-IS基本配置案例项目四:OSPF基本配置案例任务一:OSPF单区域配置案例任务二&#xff1…

蓝桥杯单片机进阶教程4——需要分配进程的模块

前言: 【蓝桥杯单片机保姆级教学】 https://www.bilibili.com/video/BV1h3411m7Aw/?p117&share_sourcecopy_web&vd_sourcec4fc67867c5218768e783d04475bc5a2 P117-118 比赛的时候也按照这个顺序来写 一、数码管显示 分析考题 (1)…

【LeetCode每日一题】2182. 构造限制重复的字符串

2024-1-13 文章目录 [2182. 构造限制重复的字符串](https://leetcode.cn/problems/construct-string-with-repeat-limit/)思路: 2182. 构造限制重复的字符串 思路: 按照字符出现次数从高到低的顺序进行重复,通过维护一个指针 j 来寻找下一个…

【python】进阶--->MySQL数据库(二)

一、sql语句(结构化查询语言) 要和数据库进行交互,需要使用到数据库认识的语言 : sql语句 是关系型数据库都需要遵循的规范。不同数据库都支持sql语句,但是都有特有内容。 二、sql语句分类 数据定义语言 : 用来定义数据库–数据库,表,列. 数据操作语言 : 对数据库表中的记录进…

Modbus协议学习第一篇之基础概念

什么是“协议” 大白话解释:协议是用来正确传递消息数据而设立的一种规则。传递消息的双方(两台计算机)在通信时遵循同一种协议,即可理解彼此传递的消息数据。 Modbus协议模型 Modbus协议模型较为简单,使用一种称为应用…

Colab 谷歌免费的云端Python编程环境初体验

最新在学习AIGC的过程中,发现很多教程,demo使用到了Colab这个谷歌工具。 Colab 是什么? Google Colab是一个强大且免费的云端Python编程环境,为学生、研究人员和开发者提供了一个便捷的平台来开展数据科学、机器学习和深度学习项…

力扣(leetcode)第500题键盘行(Python)

500.键盘行 题目链接:500.键盘行 给你一个字符串数组 words ,只返回可以使用在 美式键盘 同一行的字母打印出来的单词。键盘如下图所示。 美式键盘 中: 第一行由字符 “qwertyuiop” 组成。 第二行由字符 “asdfghjkl” 组成。 第三行由字…

桌面显示器type-c接口方案6020

TYPE-C接口桌面显示器,与传统的显示器不同的是 新一类的显示器不仅仅支持视频传输,还可以利用显示器的DC电源转成PD协议充电给设备端(笔记本,任天堂等HOST设备)充电。 这种新型的TYPE-C接口桌面显示器,不仅…

C++核心编程之类和对象---C++面向对象的三大特性--继承

目录 一、继承 1. 继承的概念 2. 继承的定义 3. 类与类之间的关系 4. 继承的两类关系 二、继承方式的基本语法 总而言之,父类的私有内容,子类是访问不到的。 三、继承中的对象模型 父类中的私有属性被编译器隐藏,访问不到&#xff0c…

使用FreeBASIC设计8051单片机汇编编译器

在STC论坛上看到有人用C语言实现8051汇编编译器(源码),好奇下,试着用FB写了一下。 基本原理就是通过分析汇编文件然后转换为机器码。以下是51汇编与机器码对应的表格(数据来自网络,如果发现有误请联系QQ149…

记录:排查create_ap偶发无法开启自发AP的问题

背景说明: 系统:Xubuntu16.04;内核:4.14;无线网卡:EDIMAX EW-7822UAC 关于无线网卡的驱动安装和create_ap配置参考博文:Xubuntu16.04系统中使用EDIMAX EW-7822UAC无线网卡开启5G自发AP 目录 问题…

【Linux笔记】自定义一个简单的shell

一、命令行解释器shell的原理 我们已经知道Linux给我们提供了一系列由exec开头的系统调用接口,可以让我们在自己所写的程序中调用各种指令或者我们自己写的其他程序: 而我们的shell命令行解释器也是接收用户输入的指令,然后执行:…