DMA
DMA简介
- DMA(Direct Memory Access)直接存储器存取——存储数据小助手,主要是协助CPU,完成数据转运的工作
- DMA可以提供外设(DR:比如ADC的数据寄存器、串口的数据寄存器)和存储器(运行内存SRAM和程序寄存器Flash)或者存储器和存储器之间的高速数据传输,无须CPU干预,节省了CPU的资源
- 12个独立可配置的通道: DMA1(7个通道), DMA2(5个通道)
- 每个通道都支持软件触发(存储器到存储器的转运)和特定的硬件触发(外设到存储器的转运:因为需要考虑时机)
- STM32F103C8T6 DMA资源:DMA1(7个通道)
存储器映像
ROM区:系统存储器:Bootloader一般是厂家更改,不易配置;
选项字节:一般是配置看门狗,flash的读写保护
RAM区:内核外设储存器:一般配置NVIC和SysTick
DMA框图
Flash是默认只读的闪存,可配置
为了使访问变得有序,中间设置了一个总线矩阵,左端的是主动单元,拥有主动存储器的访问权;右端是被动单元,对应的存储器只能被左边的主动单元读写
主动单元:Dcode总线是专门访问Flash的;
系统总线是访问其他东西的;
另外:DMA总线1,2,3:第三根是以太网私有的DMA,一般不用;
在DMA1和DMA2中,DMA有7个通道,而DMA2有5个通道,各个通道可以分别设置它们转运数据的源地址和目的地址,其中有个仲裁器 :虽然通道可以独立的运行,但是最终的DMA总线只有一条,所以由仲裁器根据通道的优先级,决定顺序,同时在总线矩阵中也存在仲裁器,作用于当DMA和CPU访问为同一目标时,DMA会停止访问。
在仲裁器下面有个AHB从设备 :DMA的自身寄存器,它连接在了总线矩阵右边,证明了DMA既是总线矩阵的主动单元,可以读写各种存储器,也是AHB总线上的被动单元,CPU可以通过此线路对DMA进行配置。
DMA请求:就是触发的意思,是通过外设输入的信号源引起的触发,一般是硬件触发源。
DMA基本结构
数据转运:
方向由一个参数控制:由于Flash默认为只读,所以不存在SRAM读写Flash和Flash读写Flash的情况
两端的对应参数,这两块配置可以随意配置,与它的命名无关,故可认为是站点A的配置参数和站点B的配置参数:
进行数据转运的3个参数配置:
数据宽度:决定一次转运的数据大小
地址是否自增:指定一次转运后,确定下一个转运的寄存器位置:对于外设寄存器来说要多次转运,所以不必增值;对于存储器来说,一个萝卜一个坑,需自增;
传输计数器:自减计数器,每转运一次,计数器就减1;当计数器减为0后,停止转运同时之前自增的地址,恢复到起始地址的位置;
旁边的自动重装器:设周期,当计数器减到0后,是否要自动恢复到最初的值;循环模式可对ADC多次+扫描模式
触发控制:决定DMA需要在什么时机进行转运的
触发源有:硬件触发和软件触发——由M2M参数决定
软件触发:不是以前的用程序触发的意思,而以最快的速度,连续不断地触发DMA,争取把传输计数器清0,所以当为软件触发时,不能开启自动重装器;一般使用与存储器到存储器的转运:因为是软件启动、不需要时机,越快越好。
硬件触发:反之。与外设有关。
DMA转运的条件:
- 开关控制:DMA_Cmd()必须使能;
- 传输计数器必须大于0
- 触发源必须有触发信号
配置传输计数器,必须关闭DMA
DMA请求——DMA触发部分
这个图不好理解,要对应基本结构;
这么多触发源如何选择:对应的外设是否开启了DMA是输出来决定的,比如要使用ADC1,那会有库函数叫ADC_DMACmd,通过此函数开启ADC1的一路输出。
优先级:默认通道越小越高,也可以配置
数据宽度与对齐
当对应的数据宽度不一样时:
例子:
1.数据转运+DMA
站点配置:
1.起始地址:DataA DataB
2.数据宽度:uint_8 uint_8
3.是否自增:自增 自增
4.方向参数:DataA——》Data2
5.传输计数器:设置为7,自动重装不需要
6.选择触发源:软件触发——这个存储器对存储器的数据转运
7.使能
此功能相当于复制功能
2.ADC扫描模式+DMA
站点配置:
1.起始地址:ADC_DR ADValue
2.数据宽度:uint_16 uint_16
3.是否自增:不自增 自增
4.方向参数:DataA——》Data2
5.传输计数器:设置为7,自动重装不需要(需要额外判断,ADC是否为连续模式)
6.选择触发源:硬件触发——这里是需要ADC单个通道转换的时机,来完成DMA的
在单个通道转运完成后,要触发DMA请求
7.使能
接线图
8-1 DMA数据转运
首先需要确定定义的数据,是不是真的存储在相应的地址区间——存储器映像
uint8_t aa=0x66;
const uint8_t aa1=0x66;
OLED_ShowHexNum(1,1,aa,2);
OLED_ShowHexNum(2,1,(uint32_t)&aa,8); //对应地址为20000000 对应地址在SRAM区
OLED_ShowHexNum(3,1,(uint32_t)&aa1,8);//对应地址为08000E30 对应地址在Flash地址
OLED_ShowHexNum(4,1,(uint32_t)&ADC1->DR,8);//对应地址为4001244C 对应地址在外设地址区中,是ADC1的数据寄存器地址,此地址是固定的
外设基地址:0x40000000
APB2外设基地址:0x40000000+10000
ADC1的基地址:0x40000000+10000+0x2400
在ADC1的基地址上+偏移就能找到DR的地址
——偏移用的是结构体指针
- 初始化RCC开启DMA时钟
- 直接调用DMA_Init,初始化对应参数
- 开关控制DMA_Cmd
- 设置触发源,如果想更改传输计数器,需先失能
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);
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); //DMA设置当前数据寄存器,给传输计数器写数据的
uint16_t DMA_GetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx); //DMA获取当前数据寄存器,返回传输计数器的值
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); //清除中断状态
注:需要使用AHB的时钟开启——多用手册或网络去查看
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyDMA.h"
//#include "OLED_Font.h"
//DMA数据转运
//定义DMA的配置——属于系统里头
//配置DMA转运的源端数组和目的数组
uint8_t DataA[]={0x01,0x02,0x03,0x04};
uint8_t DataB[]={0,0,0,0};
int main(void){
OLED_Init();
// //首先需要确定定义的数据,是不是真的存储在相应的地址区间——存储器映像
//uint8_t aa=0x66;
//const uint8_t aa1=0x66;
// OLED_ShowHexNum(1,1,aa,2);
// OLED_ShowHexNum(2,1,(uint32_t)&aa,8); //对应地址为20000000 对应地址在SRAM区
// OLED_ShowHexNum(3,1,(uint32_t)&aa1,8);//对应地址为08000E30 对应地址在Flash地址
// OLED_ShowHexNum(4,1,(uint32_t)&ADC1->DR,8);//对应地址为4001244C 对应地址在外设地址区中,是ADC1的数据寄存器地址,此地址是固定的
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);
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_ShowHexNum(4,1,DataB[0],2);
OLED_ShowHexNum(4,4,DataB[1],2);
OLED_ShowHexNum(4,7,DataB[2],2);
OLED_ShowHexNum(4,10,DataB[3],2);
while(1){
DataA[0]++;
DataA[1]++;
DataA[2]++;
DataA[3]++;
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_ShowHexNum(4,1,DataB[0],2);
OLED_ShowHexNum(4,4,DataB[1],2);
OLED_ShowHexNum(4,7,DataB[2],2);
OLED_ShowHexNum(4,10,DataB[3],2);
Delay_ms(1000);
MyDMA_Tranfer();
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_ShowHexNum(4,1,DataB[0],2);
OLED_ShowHexNum(4,4,DataB[1],2);
OLED_ShowHexNum(4,7,DataB[2],2);
OLED_ShowHexNum(4,10,DataB[3],2);
Delay_ms(1000);
}
}
MyDMA.c
#include "stm32f10x.h" // Device header
uint16_t My_Size;
void MyDMA_Init(uint32_t ADDrA,uint32_t ADDrB,uint16_t Size){
My_Size=Size;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);
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_DIR_PeripheralDST:存储器站点作为目的地 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);//第一个参数DMAy_Channelx——即选择了是哪个DMA,也选择了是哪个DMA的通道
DMA_Cmd(DMA1_Channel1,DISABLE);
}
//此时我们想让DataA的数据变化,使再转运一次
void MyDMA_Tranfer(void){
DMA_Cmd(DMA1_Channel1,DISABLE);
DMA_SetCurrDataCounter(DMA1_Channel1,My_Size);
DMA_Cmd(DMA1_Channel1,ENABLE);
//等待转运完成
while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);
//手动清除标志位
DMA_ClearFlag(DMA1_FLAG_TC1);
}
8-2 DMA+AD多通道
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
AD_Init(); //AD初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "AD0:");
OLED_ShowString(2, 1, "AD1:");
OLED_ShowString(3, 1, "AD2:");
OLED_ShowString(4, 1, "AD3:");
while (1)
{
OLED_ShowNum(1, 5, AD_Value[0], 4); //显示转换结果第0个数据
OLED_ShowNum(2, 5, AD_Value[1], 4); //显示转换结果第1个数据
OLED_ShowNum(3, 5, AD_Value[2], 4); //显示转换结果第2个数据
OLED_ShowNum(4, 5, AD_Value[3], 4); //显示转换结果第3个数据
Delay_ms(100); //延时100ms,手动增加一些转换的间隔时间
}
}
AD.c
#include "stm32f10x.h" // Device header
uint16_t AD_Value[4]; //定义用于存放AD转换结果的全局数组
/**
* 函 数:AD初始化
* 参 数:无
* 返 回 值:无
*/
void AD_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //开启ADC1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启DMA1的时钟
/*设置ADC时钟*/
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
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); //将PA0、PA1、PA2和PA3引脚初始化为模拟输入
/*规则组通道配置*/
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); //规则组序列1的位置,配置为通道0
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); //规则组序列2的位置,配置为通道1
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); //规则组序列3的位置,配置为通道2
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5); //规则组序列4的位置,配置为通道3
/*ADC初始化*/
ADC_InitTypeDef ADC_InitStructure; //定义结构体变量
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //模式,选择独立模式,即单独使用ADC1
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_NbrOfChannel确定
ADC_InitStructure.ADC_NbrOfChannel = 4; //通道数,为4,扫描规则组的前4个通道
ADC_Init(ADC1, &ADC_InitStructure); //将结构体变量交给ADC_Init,配置ADC1
/*DMA初始化*/
DMA_InitTypeDef DMA_InitStructure; //定义结构体变量
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; //外设基地址,给定形参AddrA
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //外设数据宽度,选择半字,对应16为的ADC数据寄存器
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址自增,选择失能,始终以ADC数据寄存器为源
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value; //存储器基地址,给定存放AD转换结果的全局数组AD_Value
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //存储器数据宽度,选择半字,与源数据宽度对应
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址自增,选择使能,每次转运后,数组移到下一个位置
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,选择由外设到存储器,ADC数据寄存器转到数组
DMA_InitStructure.DMA_BufferSize = 4; //转运的数据大小(转运次数),与ADC通道数一致
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //模式,选择循环模式,与ADC的连续转换一致
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //存储器到存储器,选择失能,数据由ADC外设触发转运到存储器
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级,选择中等
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //将结构体变量交给DMA_Init,配置DMA1的通道1
/*DMA和ADC使能*/
DMA_Cmd(DMA1_Channel1, ENABLE); //DMA1的通道1使能
ADC_DMACmd(ADC1, ENABLE); //ADC1触发DMA1的信号使能
ADC_Cmd(ADC1, ENABLE); //ADC1使能
/*ADC校准*/
ADC_ResetCalibration(ADC1); //固定流程,内部有电路会自动执行校准
while (ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1) == SET);
/*ADC触发*/
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //软件触发ADC开始工作,由于ADC处于连续转换模式,故触发一次后ADC就可以一直连续不断地工作
}
////启动ADC,获取结果
//void ADC_Get(void){
// DMA_Cmd(DMA1_Channel1,DISABLE);
//
// DMA_SetCurrDataCounter(DMA1_Channel1,4);
//
// DMA_Cmd(DMA1_Channel1,ENABLE);
//
// ADC_SoftwareStartConvCmd(ADC1, ENABLE);//用软件转换
// while(DMA_GetFlagStatus(DMA1_FLAG_TC1)==RESET);
// DMA_ClearFlag(DMA1_FLAG_TC1);
//}