介绍

存储器指的是运行内存SRAM程序存储器Flash;外设的数据是外设寄存器里的数据。

进行存储器之间的数据传输时使用软件触发,就会把数据全部快速转运;但进行外设寄存器和存储器之间的传输时,外设数据产生是需要时间的,比如AD转换,这时候就需要硬件触发,数据产生后才能触发,触发一次就转运一次。

特定的硬件触发意思是每个外设连接的DMA通道都是特定的,不能随意选择。

存储器映像

寄存器其实也是存储器,介质是SRAM;内核外设是NVICSysClock

DMA结构

左上角是CPU,剩下的都是存储器。在总线矩阵的左边是主动单元,拥有访问存储器的权利,右边是被动单元,只能被读取;主动单元中内核的有DCode和系统,其中DCode专门访问Flash。

因为要转运数据,DMA也是主动单元,一个DMA内部多个通道都可以转运数据,但是最终的DMA总线只有一个,所以需要内部的仲裁器根据优先级决定谁先用后用;在总线矩阵里也有仲裁器,处理CPU和DMA的访问顺序。

AHB从设备就是DMA的寄存器,可以配置DMA的参数;因为DMA是一个外设,挂在AHB总线上,所以DMA也是一个被动单元。DMA请求即触发,来自于硬件触发源,比如ADC转换完成、串口接收到数据,就可以向DMA发出触发信号,DMA就开始转运。

Flash是只读寄存器,CPU和DMA直接访问时只能读取其中数据,不能写入数据。想要写入就要配置Flash接口控制器才能写。

DMA转运流程

在流程中的外设和存储器站点只是一个名称,实际上里面的起始地址可以随便,比如你可以在外设的起始地址里写SRAM的起始地址,你写哪里的地址,CPU就会去哪里找;但是数据转运的目的地不能是Flash,因为Flash不能写入数据,上面已经说过。

写好两个起始地址之后转运方向可以配置。数据宽度是一次转运多大的数据,可以是一个字节,大小uint8_t;一个半字,大小uint16_t(ADC转运结果就这么大);一个,大小uint32_t。地址是否自增,就是第一次在初始地址转运完成后下一次转运时地址要不要向后移一位再转运。

传输计数器可以配置转运次数,范围0~65535,写入5就转运5次,从5开始每转运一次后数字就减一,减到0就停止转运了,这时候转运的地址会自动回到起始地址,方便下一次转运;自动重装器配置转运的模式,如果是单次模式,那么次数见到0就停止;如果是循环模式,次数减到0就会自动回到最初的次数继续转运。

触发源有硬件触发和软件触发M2M即memory to memory,存储器到存储器,它置1就是软件触发,这里的软件触发就不是靠程序触发了,是以最快速度连续触发DMA转运,让次数回到0,所以不能同时和循环模式一起用。置0就是硬件触发,硬件是需要时间的,如ADC转换,串口接收数据,定时器定时。

最后开关控制DMA,给传输计数器写值的时候DMA必须是关闭的,然后再打开,不能在DMA工作的时候写值,这是规定。

DMA工作的三个条件:传输计数器大于0、触发源有触发信号、DMA使能

其他

DMA请求(以DMA1为例)

可以看出,通道不同,硬件触发源也不同,软件触发时一样的,所以想使用某个硬件触发源就必须选择对应的通道,并且在这个硬件触发源配置中需要打开对应的DMA通道,比如使用ADC1,就要在ADC1配置中调用ADC_DMACmd()函数;

通道优先级一般是通道数越小优先级越高,也可以在程序中配置。

数据宽度对齐

当两个站点的数据宽度相同,就正常一位一位转运,类似于复制粘贴;当源端宽度大于目标宽度时,就将源端的数据高位补0写入目标端;源端宽度小于目标宽度时,就将数据高位删除再写入目标端

实例

DMA数据转运

把一个数组中的数据写入另一个数组中,配置好地址和数据宽度以及方向,配置两端地址都要自增,传输计数器写入7,自动重装器配置单次模式,因为两个都是SRAM存储器,所以使用软件触发即可

ADC+DMA

ADC转换结果都在ADC_DR寄存器中,地址不变,所以设置源端地址不自增,在SRAM中创建一个数组,设置自增,配置数据宽度和方向,转运7次,如果ADC是连续模式就设置DMA循环模式,反之就单次模式,选择硬件触发

DMA数据转运代码

这里实现将一个数组里的数据转运到另一个数组。

配置DMA,先开启AHB的RCC时钟,然后初始化配置地址、方向、模式这些,最后使能

初始化代码
uint16_t dma_count;
void dma_init(uint32_t addA,uint32_t addB,uint16_t count)
{
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
	dma_count=count;
	//初始化
	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_BufferSize=count;		//传输计数器
	DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC;	//方向,外设作为源端
	DMA_InitStructure.DMA_M2M=DMA_M2M_Enable;	//触发源,软件触发
	DMA_InitStructure.DMA_MemoryBaseAddr=addB;	 //存储器起始地址
	DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_Byte;	 //存储器数据宽度,字节
	DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable; 	//存储器地址是否自增,自增
	DMA_InitStructure.DMA_Mode=DMA_Mode_Normal; 	//传输模式,单次模式
	DMA_InitStructure.DMA_PeripheralBaseAddr=addA; 	//外设起始地址
	DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_Byte;	 //外设数据宽度,字节
	DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Enable;	 //外设地址是否自增,自增
	DMA_InitStructure.DMA_Priority=DMA_Priority_VeryHigh;	 //优先级
	DMA_Init(DMA1_Channel1,&DMA_InitStructure);
	
	//初始化结束先不使能,即不马上转运
	DMA_Cmd(DMA1_Channel1,DISABLE);
}

void dma_trans(void)//调用一次就转运一轮
{
		DMA_Cmd(DMA1_Channel1,DISABLE);//先失能
		DMA_SetCurrDataCounter(DMA1_Channel1,dma_count);//给传输计数器赋值
		DMA_Cmd(DMA1_Channel1,ENABLE);//再使能,开始转运
	while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);//等待转运完成
	DMA_ClearFlag(DMA1_FLAG_TC1);//清除标志位
}
主函数中
#include "stm32f10x.h"// Device header
#include "Delay.h"
#include "OLED.h"
#include "dma.h"

uint8_t A[]={0X01,0X02,0X03,0X04};
uint8_t B[]={0,0,0,0};


int main()
{
	OLED_Init();
	OLED_ShowString(1,1,"A");
	OLED_ShowString(3,1,"B");
	 OLED_ShowHexNum(2,1,A[0],2);
	OLED_ShowHexNum(2,4,A[1],2);
	OLED_ShowHexNum(2,7,A[2],2);
	OLED_ShowHexNum(2,10,A[3],2);
	
	OLED_ShowHexNum(4,1,B[0],2);
	OLED_ShowHexNum(4,4,B[1],2);
	OLED_ShowHexNum(4,7,B[2],2);
	OLED_ShowHexNum(4,10,B[3],2);
	dma_init((uint32_t) A,(uint32_t) B,4);//数组名就是数组首地址,配置完成,还没转运
	
 while(1)
 {
	 A[0]++;
	 A[1]++;
	 A[2]++;
	 A[3]++;

	 OLED_ShowHexNum(2,1,A[0],2);
	OLED_ShowHexNum(2,4,A[1],2);
	OLED_ShowHexNum(2,7,A[2],2);
	OLED_ShowHexNum(2,10,A[3],2);
	
	OLED_ShowHexNum(4,1,B[0],2);
	OLED_ShowHexNum(4,4,B[1],2);
	OLED_ShowHexNum(4,7,B[2],2);
	OLED_ShowHexNum(4,10,B[3],2);
	 Delay_ms(1000);
	 dma_trans();
	  OLED_ShowHexNum(2,1,A[0],2);
	OLED_ShowHexNum(2,4,A[1],2);
	OLED_ShowHexNum(2,7,A[2],2);
	OLED_ShowHexNum(2,10,A[3],2);
	
	OLED_ShowHexNum(4,1,B[0],2);
	OLED_ShowHexNum(4,4,B[1],2);
	OLED_ShowHexNum(4,7,B[2],2);
	OLED_ShowHexNum(4,10,B[3],2);
	 Delay_ms(1000);
	 
 }
}

ADC多通道+DMA代码

在使用DMA的情况下使用AD多通道,先开启AD、DMA和GPIO的时钟,再预分频得到ADCCLK,然后初始化GPIO,选择ADC通道,然后配置ADC,接着配置DMA,打开ADC的DMA通道,最后开启ADC,后面还需要校准

初始化
uint16_t ADC_value[4];//数组存转运来的数据,是目标端,所以类型必须和ADC转换结果相同,即数据宽度相同

void AD_init(void)
{
	//开启时钟
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
		RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);
	//预分频
	RCC_ADCCLKConfig(RCC_PCLK2_Div6);//将72MHZ 6分频了,因为ADCCLK最高频率是14MHZ
	
	//初始化GPIO
	GPIO_InitTypeDef GPIO_InitSturcture;
	GPIO_InitSturcture.GPIO_Mode=GPIO_Mode_AIN;//需要模拟输入模式
	GPIO_InitSturcture.GPIO_Pin=GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2|GPIO_Pin_3;
	GPIO_InitSturcture.GPIO_Speed=GPIO_Speed_50MHz;
	GPIO_Init(GPIOA,&GPIO_InitSturcture);
	
	//ADC通道
	ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);//通道0排在第一个转换
	ADC_RegularChannelConfig(ADC1,ADC_Channel_1,2,ADC_SampleTime_55Cycles5);//通道1排在第二个转换
	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;//是否开启连续转换模式,不开就是单次转换
	ADC_InitStructure.ADC_DataAlign=ADC_DataAlign_Right;//数据右对齐
	ADC_InitStructure.ADC_ExternalTrigConv=ADC_ExternalTrigConv_None;//不用外部触发源,使用软件触发
	ADC_InitStructure.ADC_Mode=ADC_Mode_Independent;//ADC独立模式,不使用双模式
	ADC_InitStructure.ADC_NbrOfChannel=4;//转换通道总数,转换排在前四个的通道
	ADC_InitStructure.ADC_ScanConvMode=ENABLE;//是否开启扫描模式,开启
	ADC_Init(ADC1,&ADC_InitStructure);
	
	//初始化DMA
	
	RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);

	DMA_InitTypeDef DMA_InitStructure;
	DMA_InitStructure.DMA_BufferSize=4;		//传输计数器
	DMA_InitStructure.DMA_DIR=DMA_DIR_PeripheralSRC;	//方向,外设作为源端
	DMA_InitStructure.DMA_M2M=DMA_M2M_Disable;	//触发源,硬件触发
	DMA_InitStructure.DMA_MemoryBaseAddr=(uint32_t)ADC_value;	 //存储器起始地址,这里给的数组的地址并强制类型转换
	DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_HalfWord;	 //存储器数据宽度,半字uint16_t
	DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable; 	//存储器地址是否自增,自增
	DMA_InitStructure.DMA_Mode=DMA_Mode_Normal; 	//传输模式,单次模式
	DMA_InitStructure.DMA_PeripheralBaseAddr=(uint32_t)&ADC1->DR; 	//外设起始地址,给的DR寄存器的地址
	DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;	 //外设数据宽度,半字uint16_t
	DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Disable;	 //外设地址是否自增,不自增
	DMA_InitStructure.DMA_Priority=DMA_Priority_VeryHigh;	 //优先级
	DMA_Init(DMA1_Channel1,&DMA_InitStructure);
	//使能DMA,但是仍然没有转运,因为还缺少触发信号
	DMA_Cmd(DMA1_Channel1,ENABLE);
	
	//打开ADC的DMA通道
	ADC_DMACmd(ADC1,ENABLE);
	
	//开启ADC
	ADC_Cmd(ADC1,ENABLE);
	
	//校准
	ADC_ResetCalibration(ADC1);
	while (ADC_GetResetCalibrationStatus(ADC1)==SET);
	ADC_StartCalibration(ADC1);
	while(ADC_GetCalibrationStatus(ADC1)==SET);
}

void ADC_get(void)
{
		DMA_Cmd(DMA1_Channel1,DISABLE);//先失能
		DMA_SetCurrDataCounter(DMA1_Channel1,4);//给传输计数器赋值
		DMA_Cmd(DMA1_Channel1,ENABLE);//再使能
	
	//ADC准备好了,接下来就触发转换,这里使用软件触发
	ADC_SoftwareStartConvCmd(ADC1,ENABLE);//转换完就有DMA触发信号了,DMA开始转运数据到数组中
	while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);//等待转运完成
	DMA_ClearFlag(DMA1_FLAG_TC1);//清除标志位
	
}
//这是ADC单次转换、DMA单次转运。如果要ADC连续转换、DMA循环转运,就配置ADC连续模式和DMA循环模式,把ADC软件触发转换的函数放在ADC初始化的最后,不需要ADC_get()函数了,就可以实现在主函数中一调用ADC初始化函数之后ADC就会自动连续不断的转换,一个通道转换完成就会触发DMA转运一次,一直循环转换循环转运
主函数中
#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)
 {
	
	 ADC_get();//转换并转运
	 OLED_ShowNum(1,5,ADC_value[0],4);
	 OLED_ShowNum(2,5,ADC_value[1],4);
	 OLED_ShowNum(3,5,ADC_value[2],4);
	 OLED_ShowNum(4,5,ADC_value[3],4);
	 Delay_ms(500);
 }
}
Logo

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

更多推荐