DMA全程Direct Memory Access,即直接存储器访问。简单来讲,它的功能是把数据从一个地址搬运到另一个地址。通常有三个传输方向,分别是内存到内存,内存到外设和外设到内存。
直接存储器存取(DMA)用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU干预,数据可以通过DMA快速地移动,这就节省了CPU的资源来做其他操作。
比如在串口接收或者发送时可以直接利用DMA将接收内容直接搬运到接收数组。或者利用DMA将准备发送的数据搬运到发送的缓冲区。再或者利用DMA把数据搬运到特定的地址,或者从特定的地址利用DMA搬运数据出来。总而言之,在平时的开发过程中,DMA是非常常用的。
STM32F103ZET6有两个DMA,12个通道(DMA1有7个通道,DMA2有5个通道),每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个DMA请求的优先权。
如果一个外设想要通过DMA传输数据,必须先给DMA控制器发送DMA请求。DMA控制器收到请求后,会给外设一个应答信号。当外设收到应答信号后,也会给DMA控制器一个应答信号。当DMA控制器收到外设的应答信号后,启动DMA传输。
前面介绍STM32F103ZET6有两个DMA,12个通道,同的 DMA 控制器的通道对应着不同的外设请求。根据中文参考手册,对应关系如下
DMA具有12个独立可编程的通道,每个通道对应不同外设的DMA请求。虽然每个通道可以接收多个外设的DMA请求,但是同一时间只能接收一个。
当有多个DMA请求时,需要仲裁器来决定响应的先后顺序。仲裁器决定相应顺序的方法有两种
这里以配置DMA,将ADC采集到的数据搬运到内存中的某一个数组中为例,讲解一下DMA的配置和使用方法。
ADC使用TIM4的通道4触发,具体配置可见本系列另一篇文章STM32速成笔记—ADC。这里在之前配置的基础上需要开启ADC的DMA传输,在初始化ADC时加上下面的程序
ADC_DMACmd(ADC1,ENABLE); // 使能ADC的DMA传输
ADC初始化程序如下
/*
*==============================================================================
*函数名称:ADC1_Init
*函数功能:初始化ADCx
*输入参数:无
*返回值:无
*备 注:TIM4通道4触发AD转换,使能了DMA
*==============================================================================
*/
void ADC1_Init(void)
{
// 结构体定义
GPIO_InitTypeDef GPIO_InitStructure;
ADC_InitTypeDef ADC_InitStructure;
// 开启时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1,ENABLE);
// 设置ADC分频因子6 72M/6=12,ADC最大时间不能超过14M
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
// 规则通道配置
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 1, ADC_SampleTime_239Cycles5);
// GPIO配置
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_1; //ADC1通道1
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN; // 模拟输入
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
// ADC参数配置
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式
ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 非扫描模式
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 关闭连续转换
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_T4_CC4; // TIM2通道2触发
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 右对齐
ADC_InitStructure.ADC_NbrOfChannel = 1; // 1个转换在规则序列中 也就是只转换规则序列1
ADC_Init(ADC1, &ADC_InitStructure); // ADC初始化
// 使能外部触发
ADC_ExternalTrigConvCmd(ADC1, ENABLE);
ADC_DMACmd(ADC1,ENABLE); // 使能ADC的DMA传输
ADC_Cmd(ADC1, ENABLE); // 开启AD转换器
// ADC校准
ADC_ResetCalibration(ADC1); // 重置指定的ADC的校准寄存器
while(ADC_GetResetCalibrationStatus(ADC1)); // 获取ADC重置校准寄存器的状态
ADC_StartCalibration(ADC1); // 开始指定ADC的校准状态
while(ADC_GetCalibrationStatus(ADC1)); // 获取指定ADC的校准程序
ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 使能或者失能指定的ADC的软件转换启动功能
}
由上面的介绍可知,ADC1是DMA1的通道1,我们配置一下DMA1的通道1,使能传输完成中断。
/*
*==============================================================================
*函数名称:DMA1_Init
*函数功能:DMA1初始化
*输入参数:souAddr:数据源地址;desAddr:数据目的地址
*返回值:无
*备 注:数据传输宽度为16位,外设到内存,循环传输,使能了传输完成中断
*==============================================================================
*/
void DMA1_Init (u32 souAddr,u32 desAddr)
{
// 结构体定义
DMA_InitTypeDef DMA_InitStructure;
NVIC_InitTypeDef NVIC_InitStructure;
// 使能DMA时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
//DMA1初始化
DMA_DeInit(DMA1_Channel1);
DMA_InitStructure.DMA_PeripheralBaseAddr = souAddr; // 数据源地址
DMA_InitStructure.DMA_MemoryBaseAddr = desAddr; // 目的地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 传输方向(外设到内存)
DMA_InitStructure.DMA_BufferSize = 128; // 一次传输数据大小
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不自增
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址自增
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 外设数据宽度选择
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 内存数据宽度选择
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // DMA模式:循环传输
DMA_InitStructure.DMA_Priority = DMA_Priority_High; // 优先级:高
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 禁止内存到内存的传输
DMA_Init(DMA1_Channel1, &DMA_InitStructure); // 配置DMA1
// 使能传输完成中断
DMA_ITConfig(DMA1_Channel1,DMA_IT_TC, ENABLE);
// NVIC配置
NVIC_InitStructure.NVIC_IRQChannel = DMA1_Channel1_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
// 使能DMA1通道1
DMA_Cmd(DMA1_Channel1,ENABLE);
}
// DMA1中断服务函数
void DMA1_Channel1_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_IT_TC1)!=RESET)
{
DMA_Cmd(DMA1_Channel1,DISABLE);
while (1)
{}
}
// 清除中断标志位
DMA_ClearITPendingBit(DMA1_IT_TC1);
}
定义一个存储AD转换结果的数组,初始化时,程序如下
u16 gAdcAdValue[128]; // 存储AD值
DMA1_Init((u32)(&ADC1->DR),(u32)&gAdcAdValue); // DMA1初始化
中断服务函数中将存储标志位置1表示存储完成
u8 gDmaAdcSaveFlag = 0; // ADC数据存储标志位
// DMA1中断服务函数
void DMA1_Channel1_IRQHandler(void)
{
if(DMA_GetITStatus(DMA1_IT_TC1)!=RESET)
{
gDmaAdcSaveFlag = 1; // 存储标志位置1,表示存储完成
}
// 清除中断标志位
DMA_ClearITPendingBit(DMA1_IT_TC1);
}
上面的配置就可以实现ADC采集,DMA将采集结果搬运到内存中的一个数组里面。