目录

DMA

简介:

存储器映像

DMA框图:

DMA基本结构:

DMA请求:

数据宽度与对齐:

补充:

DMA库函数:

(DMA数据转换)代码:

MyDMA.h模块:

MyDMA.c模块:

main.c模块:

(DMA数据转换+AD多通道)代码:

AD.h模块:

AD.c模块:

main.c模块:


DMA

简介:

  • DMA(Direct Memory Access)直接存储器存取
  • DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源
  • 12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道)
  • 每个通道都支持软件触发和特定的硬件触发
  • STM32F103C8T6 DMA资源:DMA1(7个通道)

存储器和存储器之间用软件触发,软件可以用很快的速度一股脑的转运完成。

外设和存储器之间用硬件触发,因为外设是有一定的时机的,要等到ADC外设数模转运完成才可以转运

并且每个DMA的通道,对应的硬件触发源不一样。使用某个外设的硬件触发源,就要使用它连接的那个通道,而不能任意选择通道。

存储器映像

stm32有以上这些存储器

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

RAM就是随机存储器,是一种易失性、掉电丢失的存储器

(人话:RAM是手机运行内存,ROM是手机硬盘内存,例如:我的手机是IQOO Neo 9 16+256g的手机,那么RAM = 16g,ROM = 256g)

Flash储存程序代码和const定义的常量。

我们定义的变量储存在SRAM里。

DMA框图:

其中除了标红的区域包含了CPU和内核外设等等,其余的都可看做是存储器

寄存器是一种特殊的存储器

        一方面CPU可以对寄存器进行读写操作,就像读写运行内存一样。

        另一方面,寄存器的每一位背后都连接了一根导线,这些导线可以用于控制外设电路的状态。

所以寄存器是软件和硬件的桥梁,CPU读写寄存器就是在控制硬件电路的执行。

为了高效有条理的访问存储器,中间设计了一个总线矩阵。

        左边:主动单元,拥有存储器的访问权。

        右边:被动单元,它们的存储器只能被左边的主动单元读写。

左边:

        DCode总线:专门访问Flash的

        系统总线是访问其他的

DMA1和DMA2分别有一条线路连接到总线上。

其中,DMA1有7个通道,DMA2有5个通道。各个通道可以设置他们转运数据的源地址和目标地址。

        在DMA1和DMA2框图中,都显示一个仲裁器。因为虽然多个通道可以独立转运数据,但是最终DMA总线只有一条。所以所有的通道都只能分时复用这一条DMA总线。如果产生冲突,就会由仲裁器根据优先级决定谁先用,谁后用。

        在总线矩阵中也会有一个仲裁器,就是当CPU和DMA同时访问同一个目标,那么DMA就会停止CPU的访问,防止冲突。

        AHB从设备,也就是DMA自身的寄存器。CPU可以通过线路对DMA进行配置。

两个红框中的各个外设,引出两条DMA请求线路。这个DMA请求就是DMA的硬件触发源。比如:ADC转换完成,串口接收到数据。需要触发DMA转运数据的时候,发出请求。

注:图中的Flash是只读存储器,通过总线访问都是只读的,只能读取不能写入。但是可以通过配置Flash接口控制器来对Flash进行写入,只不过过程就会有点麻烦了

DMA基本结构:

参数1:方向:外设---->存储器 / 存储器---->存储器

参数2:外设和存储器的起始地址

参数3:数据宽度。指定一次转运要按多大的数据宽度来转运

参数4:地址是否自增。当一次转运完成后,地址是否++。(相当于指针指向数组第一个数,转运后指针++指向第二个数)

参数5:传输计数器:指定可以传输几次。当它的值为5时,每传输一次,它的值就-1,直到减为零,就不再进行数据传输。此时的地址就会变为新的起始地址,以方便新的一轮转换。

参数6:自动重装值:当传输计数器值减为0时,是否要让他自动恢复到最初的值。

参数7:M2M:决定触发源是由硬件触发还是有软件触发

软件触发:以最快的速度,连续不断的触发DMA,争取早日把传输计数器清零。完成这一轮的转换

开关控制:DMA_Cmd();

注意:写传输计数器时,必须要先关闭DMA_Cmd();

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);

/* 开启DMA中断 */
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);

(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

MyDMA.c模块:

#include "stm32f10x.h"                  // Device header

/*
 * 1.RCC开启DMA的时钟。
 * 2.调用DMA_Init()初始化各个参数。
 * 3.DMA_Cmd()。
 * (如果调用硬件触发,要在对应的外设调用相应的XXX_DMACMd,开启一下触发信号的输出)
 * (如果需要DMA的中断,那就调用DMA_ITConfig(),开启中断输出,再在NVIC里配置相应的终端通道,写相应的中断函数)
 *  结束后再想给传输计数器赋值的话,就DMA_Cmd()失能,赋值,DMA_Cmd()使能。
 */
 
 
/*
 *
 * DMA启动的三个条件:
 * 1.传输计数器 > 0;
 * 2.触发源有触发信号
 * 3.DMA使能
 *
 */


uint16_t MyDMA_size;

void MyDMA_Init(uint32_t ADDrA, uint32_t ADDrB, uint16_t size)
{
	MyDMA_size = size;
	
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	
	DMA_InitTypeDef DMA1_Initstructure;
	
	/* 外设 */
	DMA1_Initstructure.DMA_PeripheralBaseAddr = ADDrA;   //起始地址
	DMA1_Initstructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;   //数据宽度
	DMA1_Initstructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;   //地址是否自增
	
	/* 存储器 */
	DMA1_Initstructure.DMA_MemoryBaseAddr = ADDrB;   //起始地址
	DMA1_Initstructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;   //数据宽度
	DMA1_Initstructure.DMA_MemoryInc = DMA_MemoryInc_Enable;   //地址是否自增
	
	DMA1_Initstructure.DMA_DIR = DMA_DIR_PeripheralSRC;   //传输方向
	DMA1_Initstructure.DMA_BufferSize = size;   //传输计数器
	DMA1_Initstructure.DMA_Mode = DMA_Mode_Normal;   //传输模式,其实就是是否使用自动重装
	DMA1_Initstructure.DMA_M2M = DMA_M2M_Enable;   //选择是硬件触发还是软件触发
	DMA1_Initstructure.DMA_Priority = DMA_Priority_Medium;   //配置优先级
	
	DMA_Init(DMA1_Channel1, &DMA1_Initstructure);   //软件触发,任意1~7通道都可以
	
	
	DMA_Cmd(DMA1_Channel1, DISABLE);
}

//再启动一次DMA
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);  //没完成RESET,完成SET
	DMA_ClearFlag(DMA1_FLAG_TC1);
	
}

main.c模块:

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "Oled.h"
#include "MYDMA.h"

uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};
uint8_t DataB[] = {0, 0, 0, 0};

int main(void)
{
	OLED_Init();
	
	//开启转运
	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);
	OLED_ShowHexNum(3, 8, (uint32_t)DataB, 8);
	
	while(1)
	{
		DataA[0] ++;
		DataA[1] ++;
		DataA[2] ++;
		DataA[3] ++;
		
		//转运前的DataA[]
		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);
		
		//转运前的DataB[]
		OLED_ShowNum(4, 1, DataB[0], 2);
		OLED_ShowNum(4, 4, DataB[1], 2);
		OLED_ShowNum(4, 7, DataB[2], 2);
		OLED_ShowNum(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_ShowNum(4, 1, DataB[0], 2);
		OLED_ShowNum(4, 4, DataB[1], 2);
		OLED_ShowNum(4, 7, DataB[2], 2);
		OLED_ShowNum(4, 10, DataB[3], 2);
		
		Delay_ms(1000);
	}
}



(DMA数据转换+AD多通道)代码:

AD.h模块:
 

#ifndef __AD__H
#define __AD__H

extern uint16_t AD_Value[4];

void AD_Init(void);
void ADC_GetValue(void);

#endif

AD.c模块:

#include "stm32f10x.h"                  // Device header

uint16_t AD_Value[4];

void AD_Init(void)
{
	//1.开启RCC时钟,包括ADC和GPIO的时钟。ADCCLK的分频器也要配置
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);     //ADCCLK时钟最大14MHz 6分频后是12MHz
	
	
	//2.配置GPIO为模拟输入模式
	GPIO_InitTypeDef GPIOA_InitStructure;
	/*这里的模拟输入模式是ADC的专属模式,让GPIO的输入输出失效,信号直接进入板子里面的ADC外设*/
	GPIOA_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
	GPIOA_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
	GPIOA_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOA, &GPIOA_InitStructure);
	
	//3.配置多路开关,把左边的通道接入右边的规则组里面(点菜)
	ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
	/*ADC1的菜单里,菜品:通道0,排在第一位,采样时间是55.5个ADC周期*/
	
	//还想再点几个菜的话,就多调用计次这个函数
	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);

	//4.配置ADC转换器
	ADC_InitTypeDef ADC_InitStructure;
	ADC_InitStructure.ADC_Mode = ADC_Mode_Independent;                  //配置ADC的工作模式(独立模式/双ADC模式)
	ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;              //设置右对齐还是左对齐
	ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //外部触发转换选择
	ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;                 //选择 连续/单次 转换模式 ENABLE(连续)/DISABLE(单次)
	ADC_InitStructure.ADC_ScanConvMode = ENABLE;                       //选择扫描模式(ENABLE)还是非扫描模式(DISABLE)
	ADC_InitStructure.ADC_NbrOfChannel = 4;                             //通道数目:在扫描模式下,总共会用到几个通道
	ADC_Init(ADC1, &ADC_InitStructure);
	
	DMA_InitTypeDef DMA1_Initstructure;
	
	/* 外设 */
	DMA1_Initstructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;   //起始地址(ADC转换完成后放在ADC->DR中)
	DMA1_Initstructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;   //数据宽度
	DMA1_Initstructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;   //地址是否自增
	/* 这里选择不自增是因为,ADC的规则组里面的寄存器只有一个。多个菜品只在这个寄存器里面的多位自增 */
	
	/* 存储器 */
	DMA1_Initstructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;   //起始地址
	DMA1_Initstructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;   //数据宽度
	DMA1_Initstructure.DMA_MemoryInc = DMA_MemoryInc_Enable;   //地址是否自增
	
	DMA1_Initstructure.DMA_DIR = DMA_DIR_PeripheralSRC;   //传输方向
	DMA1_Initstructure.DMA_BufferSize = 4;   //传输计数器
	DMA1_Initstructure.DMA_Mode = DMA_Mode_Normal;   //传输模式,其实就是是否使用自动重装
	DMA1_Initstructure.DMA_M2M = DMA_M2M_Disable;   //选择是硬件触发还是软件触发,这里是硬件出发
	DMA1_Initstructure.DMA_Priority = DMA_Priority_Medium;   //配置优先级
	
	DMA_Init(DMA1_Channel1, &DMA1_Initstructure);   //软件触发,ADC外设接在了通道1上,所以这里只能使用通道1
	
	DMA_Cmd(DMA1_Channel1, ENABLE);
	
	ADC_DMACmd(ADC1, ENABLE);//开启ADC到DMA的输出
	
	ADC_Cmd(ADC1, ENABLE);
	
	//5.对ADC进行校准
	ADC_ResetCalibration(ADC1);                   //复位校准
	/*软件置1,开启复位校准,复位校准完成后,硬件自动清零*/
	while(ADC_GetResetCalibrationStatus(ADC1) == SET);   //等待复位校准完成(未完成则一直开始等待)
	ADC_StartCalibration(ADC1);                   //开始校准
	/*软件置1,开启校准,校准完成后,硬件自动清零*/
	while(ADC_GetCalibrationStatus(ADC1) == SET);               //等待校准完成
}


//多通道简易实现,将函数的参数改为ADC规则通道配置的参数;
void ADC_GetValue(void)
{
	//写入传输计数器
	DMA_Cmd(DMA1_Channel1, DISABLE);
	DMA_SetCurrDataCounter(DMA1_Channel1, 4);
	DMA_Cmd(DMA1_Channel1, ENABLE);
	
	ADC_SoftwareStartConvCmd(ADC1, ENABLE);   //软件开启转换命令
	
	//等待DMA完成
	while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);  //没完成RESET,完成SET
	DMA_ClearFlag(DMA1_FLAG_TC1);
}


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)
	{
		ADC_GetValue();
		
		OLED_ShowNum(1,5,AD_Value[0],5);
		OLED_ShowNum(2,5,AD_Value[1],5);
		OLED_ShowNum(3,5,AD_Value[2],5);
		OLED_ShowNum(4,5,AD_Value[3],5);
		
		Delay_ms(500);
	}
}

DMA数据转换+AD多通道:

扫描模式可以对多个进行转换,但是转换后的数据存在ADC_DR寄存器中,不及时取出数据就会导致数被下一个通道转换的数据覆盖。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐