DMA简介
·DMA(Direct Memory Access)直接存储器存取
·DMA可以提供外设和存储器或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了 CPU的资源
·12个独立可配置的通道:DMA1(7个通道),DMA2(5个通道)
·每个通道都支持软件触发和特定的硬件触发
·STM32F103C8T6 DMA资源:DMA1(7个通道)
配置DMA基本结构
图1
左右两个绿色框的外设和存储器其实只是名字,外设里面可以放存储器的地址,存储器里可以放外设的地址,只要选好方向就行了。
传输计数器:比如我要传输一个数组,里面有4个元素,那么就填4,计数器就会自减到0,为0时停止工作
自动重装器:是否循环计数(自动重装不能和软件触发同时使用!!!)
M2M:选择软件触发还是硬件触发
初始化配置模板
void MyDMA_Init(uint32_t AddrA,uint32_t AddrB,uint16_t Size)
{
MyDMA_Size=Size;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);//DMA是AHB总线的设备,开启AHB外设时钟
//初始化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);
//使能DMA
DMA_Cmd(DMA1_Channel1,DISABLE);
}
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);//检查转运完成标志位 完成后标志位置1
DMA_ClearFlag(DMA1_FLAG_TC1);//需要手动清除标志位
}
(这个代码用于两个数组之间的数据传递)
在传输地址时,要将数据类型强转为32位无符号整型数据,以免出错
可见配置DMA大部分是用一个DMA_Init函数配置结构体的各个参数就行了
结构体参数DMA_PeripheralInc和DMA_MemoryInc决定地址是否自增,我们要进行数组之间的传递,见下图
图2
转运一次,源地址和目标地址应该都+1,才能一一对应,所以这里选择地址自增
DMA_DIR有两个参数: DMA_DIR_PeripheralSRC,选择外设地址为源地址和DMA_DIR_PeripheralDST,选择外设地址为目的地址,这里的外设只是个名字,只是为了区分,要怎么配置可以按照实际需要选择,我们要把数组A的元素传递给数组B,又把数组A的首地址放在了外设站点,所以选择外设地址为源地址。
DMA_M2M选择硬件或软件触发:ENABLE是软件触发,DISABLE是硬件触发,我们要实现数组之间的传递,因此不需要用到硬件触发,所以选择软件触发,那么自动重装就不能选了。
最后使能DMA,这里通道数可以选择1~7任意一个。
在同时满足触发源有触发信号(软件触发一直有触发信号)、传输计数器大于0,DMA使能这三个条件,DMA就会开始工作。这里还没给DMA使能和给传输计数器赋值。
在while循环里不断调用自定义的第二个函数,就能不断的将变化的数组A里的值传递到数组B里,比如每秒都将数组A里的元素+1,可以用OLED观察数组的变化情况。
注意写传输计数器时,必须要先关闭DMA,再进行,这也是第二个自定义函数先关闭DMA使能的原因。
ADC扫描模式+DMA
DMA最常用的组合就是ADC
图3
上一节我们用的是ADC的非扫描模式,主要是因为,ADC多通道扫描时,会不断把数据运到ADC的DR寄存器,但又没有人将数据运出去,就会导致前面的数据被覆盖,所以DMA就派上用场了。
下面是代码示例
void AD_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1,ENABLE);//ADC都是挂载在APB2总线上
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);//DMA是AHB总线的设备,开启AHB外设时钟
//配置ADC时钟
RCC_ADCCLKConfig(RCC_PCLK2_Div6);//选择6分频 ADC时钟最大频率为14MHz
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_AIN;//选择模拟输入模式 ADC专属
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);
ADC_RegularChannelConfig(ADC1,ADC_Channel_0,1,ADC_SampleTime_55Cycles5);
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);
//初始化ADC
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode=ADC_Mode_Independent;//选择独立模式还是双ADC模式
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;//选择通道数目(1~16)
ADC_Init(ADC1,&ADC_InitStructure);
//初始化DMA
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_PeripheralBaseAddr=(uint32_t)&ADC1->DR;//ADC转换结果放在DR寄存器里,防止数据被覆盖,从这里取数据
DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_HalfWord;//DR寄存器只有低16位有效,所以选择半字长度
DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Disable;//不自增,始终转运DR的数据
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;//传输模式(是否自动重装)
DMA_InitStructure.DMA_M2M=DMA_M2M_Disable;//硬件触发|软件触发
DMA_InitStructure.DMA_Priority=DMA_Priority_Medium;//优先级
DMA_Init(DMA1_Channel1,&DMA_InitStructure);
//使能DMA
DMA_Cmd(DMA1_Channel1,ENABLE);
//开启ADC到DMA的通道!!!
ADC_DMACmd(ADC1,ENABLE);
//上电ADC
ADC_Cmd(ADC1,ENABLE);
//对ADC进行校准
ADC_ResetCalibration(ADC1);//这里会将复位校准状态标志位置1
while(ADC_GetResetCalibrationStatus(ADC1));//获取复位校准状态,复位校准完后状态位标志为0,跳出循环
ADC_StartCalibration(ADC1);//开始校准
while(ADC_GetCalibrationStatus(ADC1));//等待校准完成
ADC_SoftwareStartConvCmd(ADC1,ENABLE);//触发转换,选择软件触发
}
这里使用的是ADC的连续转换+扫描模式
外部有四个模拟信号输入,初始化DMA时,前六个函数和传递数组时不同,外设地址(源地址)是ADC1的DR寄存器,DMA是从这里转运数据的,我们定义一个数组用来存放转换值,那么存储器地址(目的地址)就是数组名,注意数据类型要强转为32位无符号整型!
数据长度选择半字(16位长度)因为DR寄存器只有低16位有效,外设地址不自增、存储器地址自增(参考图3来配置)
传输计数器选择4,传输模式选择自动重装模式,那么对应只能选择硬件触发,DMA_M2M_Disable就是硬件触发
最重要的一点,要打开ADC1到DMA的通道,使用ADC_DMACmd函数,ADC就会硬件触发DMA,不需要我们手动检测标志位等操作。
注意:DMA初始化时只能选择通道1
DMA的外设触发源要想是ADC1,那么只能走通道1这条路(DMA1_Channel1)
回顾上节内容,ADC开始工作是需要一次软件触发的,所以在初始化的最后一条我们还需要加一个软件触发ADC,因为我们选择的是ADC的连续转换模式
以上就是全部代码示例,只需要在主函数里调用一次初始化代码,ADC和DMA就会开始工作了
重要函数
DMA_SetCurrDataCounter:设置当前数据寄存器(设置传输长度)
DMA_ITConfig:开启DMA中断