1.DMA简介
-
外设一般指的是数据寄存器DR( Data Register),比如ADC的数据寄存器,串口的数据寄存器等等,这里存储器指的就是运行内存SRAM和程序存储器flash,是我们存储变量数组和程序代码的地方,在外设和存储器或者存储器和存储器之间进行数据转运,就可以使用DMA来完成,并且在转运的过程中无需CPU的参与,节省了CPU的资源
-
如果执行的是存储器到存储器的转运,比如我们想把flash里的一批数据转运到SRAM里去,那就需要软件触发,使用软件触发之后,DMA就会迅速地把这批数据部转运完成;如果DMA进行的是外设到存储器的数据转运,就不能一股脑的转运了,因为外设的数据是有一定时机的,所以这时我们就需要用硬件触发,比如转运ADC的数据,那就得ADC每个通道AD转换完成后,硬件触发一次DMA,之后DMA再转运,触发一次,转运一次。总之就是:存储器到存储器的数据转运,我们一般使用软件触发,外设到存储器的数据转运,我们一般使用硬件触发
2.存储器映像
- ROM就是只读存储器,是一种非易失性、掉电不丢失的存储器
RAM是随机存储器(可读可写),是一种易失性、掉电丢失的存储器
3.DMA框图
-
仲裁器:虽然多个通道可以独立转运数据,但是最终DMA总线只有一条,所以所有的通道都只能分时复用这一条DMA总线。如果产生了冲突,那就会由仲裁器,根据通道的优先级来决定谁先使用和后使用。另外在总线矩阵这里,也会有个仲裁器,如果DMA和CPU都要访问同一个目标,那么DMA就会暂停CPU的访问,以防止冲突。不过总线仲裁器,仍然会保证CPU得到一半的总线带宽,使CPU也能正常的工作
-
下面这里是AHB从设备,也就是DMA自身的寄存器,因为DMA作为一个外设,它自己也会有相应的配置寄存器,这里连接在了总线右边的AHB总线上,所以DMA,既是总线矩阵的主动单元,可以读写各种存储器,也是AHB总线上的被动单元。CPU通过这一条线路,就可以对DMA进行配置了
-
接着继续看这里,是DMA请求,请求就是触发的意思,这条线路右边的触发源,是各个外设,所以这个DMA请求就是DMA的硬件触发源。比如ADC转换完成、串口接收到数据,需要触发DMA转运数据的时候,就会通过这条线路,向DMA发出硬件触发信号,之后DMA就可以执行数据转运的工作了。这就是DMA请求的作用。
-
注意:右上角的Flash是ROM只读存储器的一种,如果通过总线直接访问的话,无论是CPU,还是DMA,都是只读的,只能读取数据,而不能写入,如果你DMA的目的地址,填了Flash的区域,那转运时,就会出错。然后SRAM是运行内存,可以任意读写。
4.DMA基本结构
- 第一个是起始地址,有外设端的起始地址和存储器端的起始地址,这两个参数决定了数据是从哪里来到哪里去的,之后第二个参数是数据宽度,这个参数的作用是指定一次转运要按多大的数据宽度来进行,他可以选择字节byte、半字HalfWord和字word,字节是8位就是一次转运一个uint8_t这么大的数据,半字是16位就是一次转运一个uint16_t这么大的数据,字是32位就是一次转运unit32_t这么大的数据。比如转运ADC的数据,ADC的结果是unit16_t这么大,所以这个参数就要选择半字,一次转运一个unit16_t,然后第三个参数是地址是否自增,这个参数的作用是指定一次转移完成后,下一次转运是不是要把地址移动到下一个位置去,这就相当于是指针p++这个意思,比如ADC扫描模式,用DMA进行数据转运,外设地址是ADC_DR寄存器,寄存器这边显然地址是不用指针的,如果自增,那下一次转运就跑到别的寄存器那里去了,存储器这边地址就需要指针,每转运一个数据后就往后挪个坑,要不然下次再转就把上次的覆盖掉了,这就是地址是否自增的作用,就是指定是不是要转运一次挪个坑这个意思,这就是外设站点和存储基站点各自的三个参数了。
-
传输计数器是用来指定我总共需要转运几次的,这个传输计数器是一个自减计数器,比如你给他写个5,那DMA就只能进行5次数据转运,转运过程中每转运一次计数器的数就会减一,当传输计数器减到零之后,DMA就不会再进行数据转运了,另外它减到零之后,之前自增的地址也会恢复到起始地址的位置,以方便之后DMA开始新一轮的转换,在传输计数器的右边有一个自动重装器,这个自动重装器的作用就是传输计数器减到零之后,是否要自动恢复到最初的值,比如最初传输计数器给5,如果不使用自动重装器,那转运5次后DMA就结束了,如果使用自动重装器,那转运5次计数器减到零后就会立即重装到初始值5,这个就是自动重装器,它决定了转运的模式,如果不重装就是正常的单次模式,如果重装就是循环模式,比如如果你想转运一个数组,那一般就是单次模式转运一轮就结束了,如果是ADC扫描模式加连续转换,那为了配合ADC,DMA也需要使用循环模式,所以这个循环模式和ADC的连续模式差不多,都是指定一轮工作完成后,是不是立即开始下一轮工作。
-
触发就是决定DMA需要在什么时机进行转运的。触发源有硬件触发和软件触发,具体选择哪个,由M2M这个参数决定,M2M就是memory to memory因为2的英文two和to同音,所以M2M就是m to m存储器到存储器的意思,当我们给M2M位1时,DMA就会选择软件触发,这个软件触发并不是调用某个函数一次触发一次,这个软件触发的执行逻辑是,以最快的速度,连续不断地触发DMA,争取早日把传输计数器清零,完成这一轮的转换,所以这里的软件触发和我们之前外部中断和ADC的软件出发,可能不太一样,你可以把它理解成连续触发,那这个软件触发和循环模式不能同时用,因为软件触发就是想把传输计数器清零,循环模式是清零后自动重装,如果同时用的话,那DMA就停不下来了,这就是软件触发,软件触发一般适用于存储器到存储器的转运,因为存储器到存储器的转运,是软件启动不需要时机,并且想尽快完成的任务,所以上面这里M2M位给1就是软件触发,就是应用在存储器到存储器转运的情况,M2M位给0,那就是使用硬件触发,硬件触发源可以选择ADC、串口、定时器等等,使用硬件触发的转运一般都是与外设有关的转运,这些转运需要一定的时机,比如ADC转换完成、串口收到数据、定时时间到等等,所以需要使用硬件触发,在硬件达到这些时机时,传个信号过来来触发DMA进行转运,这就是硬件触发。
-
DMA进行转运有几个条件:第一就是开关控制,DMA_Cmd必须使能,第二就是传输计数器必须大于0,第三就是触发源必须有触发信号,触发一次转运一次,传输计数器自减一次,当传输计数器等于0,且没有自动重装时,这时无论是否触发,DMA都不会再进行转运了,此时就需要DMA_CMD给DISABLE,关闭DMA,再为传输计数器写一个大于0的数,再DMA_Cmd,给ENABLE,开启DMA,DMA才能继续工作,注意一下,写传输计数器时,必须要先关闭DMA再进行,不能在DMA开启时写传输计数器,这是手册里的规定。
-
选择硬件触发 需要在对应外设调用XXX_MDACmd 开启触发信号的输出 需要DMA中断 调用DMA_ITConfig 开启中断输出 再在NVIC里配置相应的中断通道 写入中断函数即可
5.DMA请求
- EN位是开关控制,EN等于0时不工作,EN等于1时工作。
- 每个通道的硬件触发源都是不同的,如果你需要用ADC1来触发的话,那就必须选择通道一,如果需要定时器二的更新事件来触发的话,那就必须选择通道二
6.数据宽度与对齐
7.数据转运+DMA
-
首先是外设站点和存储器站点的起始地址、数据宽度、地址是否自增这三个参数。那在这个任务里,外设地址显然应该填DataA数组的首地址,存储器地址给DataB数组的首地址,然后数据宽度,两个数组的类型都是uint8_t,所以数据宽度都是按8位的字节传输。之后地址是否自增,在中间可以看到,我们想要的效果是DataA[0]转到DataB[0],DataA[1]转DataB[1],等等。所以转运完DataA[0]和DataB[0]之后,两个站点的地址都应该自增,都移动到下一个数据的位置,继续转运DataA[1]和DataB[1],这样来进行。
8.ADC扫描模式+DMA
-
左边是ADC扫描模式的执行流程,在这里有7个通道,触发一次后,7个通道依次进行AD转换,然后转换结果都放到ADC_DR数据寄存器里面。那我们要做的就是,在每个单独的通道转换完成后,进行一个DMA数据转运,并且目的地址进行自增,这样数据就不会被覆盖了。所以在这里DMA的配置就是,外设地址,写入ADC_DR这个寄存器的地址;存储器的地址,可以在SRAM中定义一个数组ADValue,然后把ADValue的地址当做存储器的地址。
9.相关API
9.1 DMA_Init
void DMA_Init(DMA_Channel_TypeDef* DMAy_Channelx, DMA_InitTypeDef* DMA_InitStruct);
功能:根据 DMA_InitStruct 中指定的参数初始化 DMA 的通道 x 寄存器
参数:DMA Channelx:x 可以是 1,2…,或者 7 来选择 DMA 通道 xDMA_InitStruct:指向结构 DMA_InitTypeDef 的指针,包含了 DMA 通道 x 的配置信息
返回值:无
9.2 DMA_Cmd
void DMA_Cmd(DMA_Channel_TypeDef* DMAy_Channelx, FunctionalState NewState);
功能:使能或者失能指定的通道 x
参数:DMA Channelx:x 可以是 1,2…,或者 7 来选择 DMA 通道 xNewState:DMA 通道 x 的新状态 这个参数可以取:ENABLE 或者 DISABLE
返回值:无
9.3 DMA_SetCurrDataCounter
void DMA_SetCurrDataCounter(DMA_Channel_TypeDef* DMAy_Channelx, uint16_t DataNumber);
功能:设置DMA转换数据个数
参数:DMA Channelx:x 可以是 1,2…,或者 7 来选择 DMA 通道 xDataNumber:数据个数
返回值:无
9.4 DMA_GetFlagStatus
FlagStatus DMA_GetFlagStatus(uint32_t DMAy_FLAG);
功能:检查指定的 DMA 通道 x 标志位设置与否
参数:DMA_FLAG:待检查的 DMA 标志位
返回值:DMA_FLAG 的新状态(SET 或者 RESET)
9.5 DMA_ClearFlag
void DMA_ClearFlag(uint32_t DMAy_FLAG);
功能:清除 DMA 通道 x 待处理标志位
参数:DMA_FLAG:待清除的 DMA 标志位,使用操作符“|”可以同时选中多个DMA 标志位
返回值:无
10.DMA数据转运
10.1 接线图
10.2 程序调试
//存储器映像的调试
const uint16_t aa = 0x66;//const是C语言的关键字 为定义一个常量 常量就无法改变 对应Flash主闪存(可读不可写)//若这里没加const 在第13行代码执行完OLED显示的是2000开头的 即运行内存SRAM//这里加了const 在第13行代码执行完OLED显示的是0800开头的 即程序存储器FlashOLED_ShowHexNum(1,1,aa,2);
OLED_ShowHexNum(2,1,(uint32_t)&aa,8);
寄存器的实际地址 = 起始地址 + 偏移
起始地址表
偏移表
10.3 相关代码
MyDMA.c
#include "stm32f10x.h" // Device headeruint16_t MyDMA_Size;//定义全局变量,用于记住Init函数的Size,供Transfer函数使用void MyDMA_Init(uint32_t AddrA,uint32_t AddrB,uint16_t Size)
{MyDMA_Size = Size;/*开启时钟*/RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1,ENABLE);//开启DMA的时钟 DMA是AHB的总线设备 要用AHB开启时钟的函数 开启DMA的时钟//互联型是STM32F105/107的型号 本次使用的是F103 所以在下面参数表选DMA_InitTypeDef DMA_InitStructure;DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;//外设基地址,给定形参AddrADMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;//外设数据宽度,选择字节(uint8_t)DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;//外设地址自增,选择使能DMA_InitStructure.DMA_MemoryBaseAddr = AddrB;//存储器基地址,给定形参AddrBDMA_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;//指定传输计数器是否要自动重装 (模式,选择正常模式)自减到0后停下来//注意:自动重装与软件触发不能调试使用 若同时使用 则DMA不会停下来 会连续触发 DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;//存储器到存储器(软件触发),选择使能DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;//优先级,选择中等DMA_Init(DMA1_Channel1,&DMA_InitStructure);//将结构体变量交给DMA_Init,配置DMA1的通道1/*DMA使能*/DMA_Cmd(DMA1_Channel1, DISABLE); //这里先不给使能,初始化后不会立刻工作,等后续调用Transfer后,再开始
}void MyDMA_Transfer(void)
{DMA_Cmd(DMA1_Channel1,DISABLE);//DMA失能,在写入传输计数器之前,需要DMA暂停工作DMA_SetCurrDataCounter(DMA1_Channel1,MyDMA_Size);//写入传输计数器,指定将要转运的次数DMA_Cmd(DMA1_Channel1,ENABLE);//DMA使能,开始工作while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET); //等待DMA工作完成DMA_ClearFlag(DMA1_FLAG_TC1);
}
MyDMA.h
#ifndef __MYDME_H
#define __MYDMA_Hvoid MyDMA_Init(uint32_t AddrA,uint32_t AddrB,uint16_t Size);
void MyDMA_Transfer(void);#endif
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyDMA.h"/*存储器映像的调试
const uint16_t aa = 0x66;//const是C语言的关键字 为定义一个常量 常量就无法改变 对应Flash主闪存(可读不可写)//若这里没加const 在第13行代码执行完OLED显示的是2000开头的 即运行内存SRAM//这里加了const 在第13行代码执行完OLED显示的是0800开头的 即程序存储器Flash
*/uint8_t DataA[] = {0x01,0x02,0x03,0x04};
uint8_t DataB[] = {0,0,0,0};int main(void)
{OLED_Init();// OLED_ShowHexNum(1,1,aa,2);
// OLED_ShowHexNum(2,1,(uint32_t)&aa,8);
// OLED_ShowHexNum(2,1,(uint32_t)&ADC1->DR,8);MyDMA_Init((uint32_t)DataA,(uint32_t)DataB,4);//DMA初始化,把源数组和目的数组的地址传入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);while (1){DataA[0]++;//变换测试数据DataA[1]++;DataA[2]++;DataA[3]++;OLED_ShowHexNum(2, 1, DataA[0], 2); //显示数组DataAOLED_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); //显示数组DataBOLED_ShowHexNum(4, 4, DataB[1], 2);OLED_ShowHexNum(4, 7, DataB[2], 2);OLED_ShowHexNum(4, 10, DataB[3], 2);Delay_ms(1000); //延时1s,观察转运前的现象MyDMA_Transfer(); //使用DMA转运数组,从DataA转运到DataBOLED_ShowHexNum(2, 1, DataA[0], 2); //显示数组DataAOLED_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); //显示数组DataBOLED_ShowHexNum(4, 4, DataB[1], 2);OLED_ShowHexNum(4, 7, DataB[2], 2);OLED_ShowHexNum(4, 10, DataB[3], 2);Delay_ms(1000); //延时1s,观察转运后的现象}
}
现象:DataA数组中的数每隔1us自增一次 并将数据通过DMA数据传输到DataB DataB的数据也随之自增比同步
11.DMA+AD多通道
11.1 接线图
11.2 相关代码
AD2.c
#include "stm32f10x.h" // Device headeruint16_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的位置,配置为通道0ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); //规则组序列2的位置,配置为通道1ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); //规则组序列3的位置,配置为通道2ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5); //规则组序列4的位置,配置为通道3/*ADC初始化*/ADC_InitTypeDef ADC_InitStructure; //定义结构体变量ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //模式,选择独立模式,即单独使用ADC1ADC_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; //外设基地址,给定形参AddrADMA_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_ValueDMA_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就可以一直连续不断地工作
}
AD2.h
#ifndef __AD_H
#define __AD_Hextern uint16_t AD_Value[4];void AD_Init(void);#endif
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD2.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多通道一样 上电之后光敏传感器、热敏传感器、反射式红外传感器以及电位器随着调试其数值会发生变化