DMA知识介绍
DMA,又叫做直接存储器访问。是一种硬件机制,在单片机系统中常用于数据传输或存储器操作等场合,其目的是在不占用 CPU 时间的情况下,通过直接操作内存和外设实现数据的高速传输。相比于传统的 CPU 访问方式,DMA 机制的数据传输速度更高,能够提高系统的性能和效率。在单片机中,DMA 通常用于高速数据传输、缓存清空、内存拷贝、外设数据处理等场合。例如,当需要将传感器数据从 ADC 外设存取到存储器中时,可以使用 DMA 实现高速数据传输,从而提高系统的性能和效率。
DMA就是把数据从地址A转移到地址B,因此使用 DMA 需要配置 DMA 控制器,包括设置源、目的地址、数据传输长度、传输方向等参数,并启动 DMA 控制器执行数据传输。同时,为了保证数据的正确性和完整性,需要进行一定的数据校验和错误处理。
代码展示
下面我将以图文结合的方式来进行DMA实验的代码分析,我这里最终实现的效果是软件触发转换的ADC值通过DMA将数据运输至内存当中,采用的传感器是单片机的内部温度传感器。
main.c
extern u32 SendBuff; //发送数据缓冲区
int main(void)
{
u16 i=0;
u16 adc1;
delay_init(); //延时函数初始化
uart_init(115200); //串口初始化为115200
LED_Init(); //初始化与LED连接的硬件接口
MYDMA_Config(DMA1_Channel1,(u32)&ADC1->DR,(u32)&SendBuff,1);
Adc_Init(); //ADC初始化
while(1)
{
adc1=T_Get_Adc(ADC_Channel_16);
printf("adc1=%d\r\n",adc1); //得到真实的adc数据
printf("DMA DATA:\r\n");
ADC_DMACmd(ADC1,ENABLE); //使能ADC1的DMA发送
DMA_Cmd(DMA1_Channel1, ENABLE); //开始一次DMA传输
printf("传输后dma=%d\r\n",SendBuff); //输出传输过来的数据,对比真实数据判断是否正确
SendBuff=0;
//系统指示灯
i++;
delay_ms(10);
if(i==20)
{
LED0=!LED0;//提示系统正在运行
i=0;
}
printf("--------------------\r\n");
}
}
首先进行一些必要的初始化,根据查表得知ADC1使用的是DMA1的通道1,因此在DMA初始化中我们需要填入DMA1的通道1、ADC1的DR寄存器地址、传输后内存中变量的地址、传输数据量这几个参数,本次实验每次传输只传输1个32位长度的数据,因此数量为1。
接着主循环中因为我们需要验证传输前后的准确性,所以在传输前将ADC的DR寄存器中的值输出出来,DMA传输后将SendBuff的值输出出来作对比。
函数T_Get_Adc()的作用是启动一次模数转换,并返回ADC1中DR寄存器中的数值,接着是进行一次DMA传输。
dma.c
u16 DMA1_MEM_LEN;//保存DMA每次数据传送的长度
//DMA1的各通道配置
//从ADC->内存模式/8位数据宽度/存储器增量模式
//DMA_CHx:DMA通道CHx
//cpar:外设地址
//cmar:存储器地址
//cndtr:数据传输量
void MYDMA_Config(DMA_Channel_TypeDef* DMA_CHx,u32 cpar,u32 cmar,u16 cndtr)
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //使能DMA传输
// DMA1通道配置
DMA_DeInit(DMA_CHx); //将DMA的通道1寄存器重设为缺省值
DMA1_MEM_LEN=cndtr;
DMA_InitStructure.DMA_PeripheralBaseAddr = cpar; //DMA外设基地址
DMA_InitStructure.DMA_MemoryBaseAddr = cmar; //DMA内存基地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,从外设读取发送到内存
DMA_InitStructure.DMA_BufferSize = cndtr; //DMA通道的DMA缓存的大小
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址寄存器不变
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Disable; //内存地址寄存器不变
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //数据宽度为16位
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //数据宽度为16位
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //工作在循环模式
DMA_InitStructure.DMA_Priority = DMA_Priority_High; //DMA通道 x拥有中优先级
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //DMA通道x没有设置为内存到内存传输
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //根据DMA_InitStruct中指定的参数初始化DMA的通道USART1_Tx_DMA_Channel所标识的寄存器
}
//开启一次DMA传输
void MYDMA_Enable(DMA_Channel_TypeDef*DMA_CHx)
{
DMA_Cmd(DMA_CHx, DISABLE ); //关闭USART1 TX DMA1 所指示的通道
DMA_SetCurrDataCounter(DMA_CHx,DMA1_MEM_LEN);//DMA通道的DMA缓存的大小
DMA_Cmd(DMA_CHx, ENABLE); //使能USART1 TX DMA1 所指示的通道
}
本函数中主要是对DMA的配置,传入外设基地址和内存基地址,然后设置传输方向为外设到内存;缓存大小即每次数据传输的数据量,此处为1;然后设置内存和外设地址都不变;因为ADC数据大小为0~4095,所以设置数据宽度为半字(范围0~65535);工作在循环模式,优先级设置中优先级,此处不重要,因为只传输一个;非内存到内存。
函数开启一次DMA传输和之前main函数中直接调用的那一句话效果相同,只不过先使能再使能更稳定一点。
adc.c
//初始化ADC
//这里我们仅以规则通道为例
//我们默认将开启通道0~3
u32 SendBuff;
void Adc_Init(void) //ADC通道初始化
{
ADC_InitTypeDef ADC_InitStructure;
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA |RCC_APB2Periph_ADC1, ENABLE ); //使能GPIOA,ADC1通道时钟
// ADC1配置
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //分频因子6时钟为72M/6=12MHz
ADC_DeInit(ADC1); //将外设 ADC1 的全部寄存器重设为缺省值
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //ADC工作模式:ADC1和ADC2工作在独立模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; //模数转换工作在单通道模式
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //模数转换工作在循环转换模式
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //转换由软件而不是外部触发启动
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //ADC数据右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1; //顺序进行规则转换的ADC通道的数目
ADC_Init(ADC1, &ADC_InitStructure); //根据ADC_InitStruct中指定的参数初始化外设ADCx的寄存器
ADC_Cmd(ADC1, ENABLE); //使能指定的ADC1
ADC_TempSensorVrefintCmd(ENABLE); //开启内部温度传感器
ADC_ResetCalibration(ADC1); //重置指定的ADC1的复位寄存器
while(ADC_GetResetCalibrationStatus(ADC1)); //获取ADC1重置校准寄存器的状态,设置状态则等待
ADC_StartCalibration(ADC1); //
while(ADC_GetCalibrationStatus(ADC1)); //获取指定ADC1的校准程序,设置状态则等待
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
u16 T_Get_Adc(u8 ch)
{
ADC_RegularChannelConfig(ADC1, ch, 1, ADC_SampleTime_239Cycles5 ); //ADC1,ADC通道3,第一个转换,采样时间为239.5周期
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //使能指定的ADC1的软件转换启动功能
while(!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC ));//等待转换结束
return ADC_GetConversionValue(ADC1); //返回最近一次ADC1规则组的转换结果
}
首先对ADC时用到的硬件进行时钟使能;然后设置分频因子;设置ADC参数,设置ADC1和ADC2工作在独立模式,模数转换在单通道,循环转换,软件触发启动转换,数据右对齐,每次只转换1个通道的数据;将ADC1使能并开启内部温度传感器,最后就是对ADC1进行校准。
在函数T_Get_Adc()中我们也可以清楚的看到,本函数的作用是进行一次模数转换,关键语句时第43行,接着44行判断转换是否结束。
最终效果