1.MDA简介
DMA全称Direct Memory Access,直接存储区访问。
DMA传输将数据从一个地址空间复制到另一个地址空间。当CPU初始化这个传输动作,传输动作本身是由DMA控制器来实现和完成的。DMA传输方式无需CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为RAM和IO设备开辟一个直接传输数据的通道,使得CPU的效率大大提高。
STM32F4xx系列的DMA支持外设到存储器传输、存储器到外设传输和存储器到存储器传输三种传输模式。 这里的外设一般指外设的数据寄存器,比如ADC、SPI、I2C、DCMI等等外设的数据寄存器,存储器一般是指片内SRAM、外部存储器、片内Flash等等。
外设到存储器传输就是把外设数据寄存器内容转移到指定的内存空间。比如进行ADC采集时我们可以利用DMA传输把AD转换数据转移到我们定义的存储区中, 这样对于多通道采集、采样频率高、连续输出数据的AD采集是非常高效的处理方法。
存储区到外设传输就是把特定存储区内容转移至外设的数据寄存器中,这种多用于外设的发送通信。
存储器到存储器传输就是把一个指定的存储区内容拷贝到另一个存储区空间。功能类似于C语言内存拷贝函数memcpy, 利用DMA传输可以达到更高的传输效率,特别是DMA传输是不占用CPU的,可以节省很多CPU资源。
1.1外设通道选择
STM32F4xx系列资源丰富,具有两个DMA控制器,同时外设繁多,为实现正常传输,DMA需要通道选择控制。每个DMA控制器具有8个数据流, 每个数据流对应8个外设请求。在实现DMA传输之前,DMA控制器会通过DMA数据流x配置寄存器DMA_SxCR(x为0~7,对应8个DMA数据流)的CHSEL[2:0]位选择对应的通道作为该数据流的目标外设。
外设通道选择要解决的主要问题是决定哪一个外设作为该数据流的源地址或者目标地址。
DMA1各个通道的请求映像
DMA2各个通道的请求映像每个外设请求都占用一个数据流通道,相同外设请求可以占用不同数据流通道。
1.2仲裁器
一个DMA控制器对应8个数据流,数据流包含要传输数据的源地址、目标地址、数据等等信息。如果我们需要同时使用同一个DMA控制器(DMA1或DMA2)多个外设请求时, 那必然需要同时使用多个数据流,那究竟哪一个数据流具有优先传输的权利呢?这就需要仲裁器来管理判断了。
仲裁器管理数据流方法分为两个阶段。第一阶段属于软件阶段,我们在配置数据流时可以通过寄存器设定它的优先级别, 具体配置DMA_SxCR寄存器PL[1:0]位,可以设置为非常高、高、中和低四个级别。第二阶段属于硬件阶段,如果两个或以上数据流软件设置优先级一样, 则他们优先级取决于数据流编号,编号越低越具有优先权,比如数据流2优先级高于数据流3。
1.3FIFO
每个数据流都独立拥有四级32位FIFO(先进先出存储器缓冲区)。DMA传输具有FIFO模式和直接模式。
直接模式在每个外设请求都立即启动对存储器传输。在直接模式下,如果DMA配置为存储器到外设传输那DMA会见一个数据存放在FIFO内, 如果外设启动DMA传输请求就可以马上将数据传输过去。
FIFO用于在源数据传输到目标地址之前临时存放这些数据。可以通过DMA数据流xFIFO控制寄存器DMA_SxFCR的FTH[1:0]位来控制FIFO的阈值, 分别为1/4、1/2、3/4和满。如果数据存储量达到阈值级别时,FIFO内容将传输到目标中。
FIFO对于要求源地址和目标地址数据宽度不同时非常有用,比如源数据是源源不断的字节数据,而目标地址要求输出字宽度的数据, 即在实现数据传输时同时把原来4个8位字节的数据拼凑成一个32位字数据。此时使用FIFO功能先把数据缓存起来,分别根据需要输出数据。
FIFO另外一个作用使用于突发(burst)传输。
2.DMA数据配置
2.1DMA传输模式
DMA2支持全部三种传输模式,而DMA1只有外设到存储器和存储器到外设两种模式。模式选择可以通过DMA_SxCR寄存器的DIR[1:0]位控制, 进而将DMA_SxCR寄存器的EN位置1就可以使能DMA传输。
在DMA_SxCR寄存器的PSIZE[1:0]和MSIZE[1:0]位分别指定外设和存储器数据宽度大小,可以指定为字节(8位)、半字(16位)和字(32位), 我们可以根据实际情况设置。直接模式要求外设和存储器数据宽度大小一样,实际上在这种模式下DMA数据流直接使用PSIZE,MSIZE不被使用。
2.2源地址和目标地址
DMA数据流x外设地址DMA_SxPAR(x为0~7)寄存器用来指定外设地址,它是一个32位数据有效寄存器。 DMA数据流x存储器0地址DMA_SxM0AR(x为0~7) 寄存器和DMA数据流x存储器1地址DMA_SxM1AR(x为0~7)寄存器用来存放存储器地址, 其中DMA_SxM1AR只用于双缓冲模式,DMA_SxM0AR和DMA_SxM1AR都是32位数据有效的。
当选择外设到存储器模式时,即设置DMA_SxCR寄存器的DIR[1:0] 位为“00”,DMA_SxPAR寄存器为外设地址,也是传输的源地址, DMA_SxM0AR寄存器为存储器地址,也是传输的目标地址。对于存储器到存储器传输模式,即设置DIR[1:0]位为“10”时, 采用与外设到存储器模式相同配置。而对于存储器到外设,即设置DIR[1:0]位为“01”时,DMA_SxM0AR寄存器作为为源地址,DMA_SxPAR寄存器作为目标地址。
2.3流控制器
流控制器主要涉及到一个控制DMA传输停止问题。DMA传输在DMA_SxCR寄存器的EN位被置1后就进入准备传输状态,如果有外设请求DMA传输就可以进行数据传输。 很多情况下,我们明确知道传输数据的数目,比如要传1000个或者2000个数据,这样我们就可以在传输之前设置DMA_SxNDTR寄存器为要传输数目值, DMA控制器在传输完这么多数目数据后就可以控制DMA停止传输。
DMA数据流x数据项数DMA_SxNDTR(x为0~7)寄存器用来记录当前仍需要传输数目,它是一个16位数据有效寄存器,即最大值为65535, 这个值在程序设计是非常有用也是需要注意的地方。我们在编程时一般都会明确指定一个传输数量,在完成一次数目传输后DMA_SxNDTR计数值就会自减,当达到零时就说 明传输完成。
如果某些情况下在传输之前我们无法确定数据的数目,那DMA就无法自动控制传输停止了,此时需要外设通过硬件通信向DMA控制器发送停止传输信号。 这里有一个大前提就是外设必须是可以发出这个停止传输信号,只有SDIO才有这个功能,其他外设不具备此功能。
2.4循环模式
循环模式相对应于一次模式。一次模式就是传输一次就停止传输,下一次传输需要手动控制,而循环模式在传输一次后会自动按照相同配置重新传输,周而复始直至被控制停止或传输发生错误。
通过DMA_SxCR寄存器的CIRC位可以使能循环模式。
2.5传输类型
DMA传输类型有单次(Single)传输和突发(Burst)传输。突发传输就是用非常短时间结合非常高数据信号率传输数据,相对正常传输速度, 突发传输就是在传输阶段把速度瞬间提高,实现高速传输,在数据传输完成后恢复正常速度,有点类似达到数据块“秒传”效果。为达到这个效果突发传输过程要占用AHB总线, 保证要求每个数据项在传输过程不被分割,这样一次性把数据全部传输完才释放AHB总线;而单次传输时必须通过AHB的总线仲裁多次控制才传输完成。
2.6直接模式
默认情况下,DMA工作在直接模式,不使能FIFO阈值级别。
直接模式在每个外设请求都立即启动对存储器传输的单次传输。直接模式要求源地址和目标地址的数据宽度必须一致, 所以只有PSIZE控制,而MSIZE值被忽略。突发传输是基于FIFO的所以直接模式不被支持。另外直接模式不能用于存储器到存储器传输。
在直接模式下,如果DMA配置为存储器到外设传输那DMA会见一个数据存放在FIFO内,如果外设启动DMA传输请求就可以马上将数据传输过去。
2.7双缓冲模式
设置DMA_SxCR寄存器的DBM位为1可启动双缓冲传输模式,并自动激活循环模式。双缓冲不应用与存储器到存储器的传输。双缓冲模式下, 两个存储器地址指针都有效,即DMA_SxM1AR寄存器将被激活使用。开始传输使用DMA_SxM0AR寄存器的地址指针所对应的存储区, 当这个存储区数据传输完DMA控制器会自动切换至DMA_SxM1AR寄存器的地址指针所对应的另一块存储区, 如果这一块也传输完成就再切换至DMA_SxM0AR寄存器的地址指针所对应的存储区,这样循环调用。
当其中一个存储区传输完成时都会把传输完成中断标志TCIF位置1,如果我们使能了DMA_SxCR寄存器的传输完成中断,则可以产生中断信号, 这个对我们编程非常有用。另外一个非常有用的信息是DMA_SxCR寄存器的CT位,当DMA控制器是在访问使用DMA_SxM0AR时CT=0, 此时CPU不能访问DMA_SxM0AR,但可以向DMA_SxM1AR填充或者读取数据;当DMA控制器是在访问使用DMA_SxM1AR时CT=1,此时CPU不能访问DMA_SxM1AR, 但可以向DMA_SxM0AR填充或者读取数据。另外在未使能DMA数据流传输时,可以直接写CT位,改变开始传输的目标存储区。
双缓冲模式应用在需要解码程序的地方是非常有效的。比如MP3格式音频解码播放,MP3是被压缩的文件格式, 我们需要特定的解码库程序来解码文件才能得到可以播放的PCM信号,解码需要一定的实际,按照常规方法是读取一段原始数据到缓冲区, 然后对缓冲区内容进行解码,解码后才输出到音频播放电路,这种流程对CPU运算速度要求高,很容易出现播放不流畅现象。 如果我们使用DMA双缓冲模式传输数据就可以非常好的解决这个问题,达到解码和输出音频数据到音频电路同步进行的效果。
2.8DMA中断
每个DMA数据流可以在发送以下事件时产生中断:
-
达到半传输:DMA数据传输达到一半时HTIF标志位被置1, 如果使能HTIE中断控制位将产生达到半传输中断;
-
传输完成:DMA数据传输完成时TCIF标志位被置1, 如果使能TCIE中断控制位将产生传输完成中断;
-
传输错误:DMA访问总线发生错误或者在双缓冲模式下试图访问“受限”存储器地址寄存器时TEIF标志位被置1, 如果使能TEIE中断控制位将产生传输错误中断;
-
FIFO错误:发生FIFO下溢或者上溢时FEIF标志位被置1, 如果使能FEIE中断控制位将产生FIFO错误中断;
-
直接模式错误:在外设到存储器的直接模式下,因为存储器总线没得到授权,使得先前数据没有完成被传输到存储器空间上, 此时DMEIF标志位被置1,如果使能DMEIE中断控制位将产生直接模式错误中断。
3.DMA_InitTypeDef初始化结构体
typedef struct {
uint32_t DMA_Channel; //通道选择
uint32_t DMA_PeripheralBaseAddr; //外设地址
uint32_t DMA_Memory0BaseAddr; //存储器0地址
uint32_t DMA_DIR; //传输方向
uint32_t DMA_BufferSize; //数据数目
uint32_t DMA_PeripheralInc; //外设递增
uint32_t DMA_MemoryInc; //存储器递增
uint32_t DMA_PeripheralDataSize; //外设数据宽度
uint32_t DMA_MemoryDataSize; //存储器数据宽度
uint32_t DMA_Mode; //模式选择
uint32_t DMA_Priority; //优先级
uint32_t DMA_FIFOMode; //FIFO模式
uint32_t DMA_FIFOThreshold; //FIFO阈值
uint32_t DMA_MemoryBurst; //存储器突发传输
uint32_t DMA_PeripheralBurst; //外设突发传输
} DMA_InitTypeDef;
-
DMA_Channel:DMA请求通道选择,可选通道0至通道7,每个外设对应固定的通道, 具体设置值需要查表 DMA1各个通道的请求映像 和表 DMA2各个通道的请求映像 ; 它设定DMA_SxCR寄存器的CHSEL[2:0]位的值。例如,我们使用模拟数字转换器ADC3规则采集4个输入通道的电压数据,查表 DMA2各个通道的请求映像 可知使用通道2。
-
DMA_PeripheralBaseAddr:外设地址,设定DMA_SxPAR寄存器的值;一般设置为外设的数据寄存器地址, 如果是存储器到存储器模式则设置为其中一个存储区地址。ADC3的数据寄存器ADC_DR地址为((uint32_t)ADC3+0x4C)。
-
DMA_Memory0BaseAddr:存储器0地址,设定DMA_SxM0AR寄存器值;一般设置为我们自定义存储区的首地址。 我们程序先自定义一个16位无符号整形数组ADC_ConvertedValue[4]用来存放每个通道的ADC值, 所以把数组首地址(直接使用数组名即可)赋值给DMA_Memory0BaseAddr。
-
DMA_DIR:传输方向选择,可选外设到存储器、存储器到外设以及存储器到存储器。 它设定DMA_SxCR寄存器的DIR[1:0]位的值。ADC采集显然使用外设到存储器模式。
-
DMA_BufferSize:设定待传输数据数目,初始化设定DMA_SxNDTR寄存器的值。 这里ADC是采集4个通道数据,所以待传输数目也就是4。
-
DMA_PeripheralInc:如果配置为DMA_PeripheralInc_Enable,使能外设地址自动递增功能,它设定DMA_SxCR寄存器的PINC位的值; 一般外设都是只有一个数据寄存器,所以一般不会使能该位。ADC3的数据寄存器地址是固定并且只有一个所以不使能外设地址递增。
-
DMA_MemoryInc:如果配置为DMA_MemoryInc_Enable,使能存储器地址自动递增功能,它设定DMA_SxCR寄存器的MINC位的值; 我们自定义的存储区一般都是存放多个数据的,所以使能存储器地址自动递增功能。我们之前已经定义了一个包含4个元素的数字用来存放数据, 使能存储区地址递增功能,自动把每个通道数据存放到对应数组元素内。
-
DMA_PeripheralDataSize:外设数据宽度,可选字节(8位)、半字(16位)和字(32位),它设定DMA_SxCR寄存器的PSIZE[1:0]位的值。 ADC数据寄存器只有低16位数据有效,使用半字数据宽度。
-
DMA_MemoryDataSize:存储器数据宽度,可选字节(8位)、半字(16位)和字(32位),它设定DMA_SxCR寄存器的MSIZE[1:0]位的值。 保存ADC转换数据也要使用半字数据宽度,这跟我们定义的数组是相对应的。
-
DMA_Mode:DMA传输模式选择,可选一次传输或者循环传输,它设定DMA_SxCR寄存器的CIRC位的值。 我们希望ADC采集是持续循环进行的,所以使用循环传输模式。
-
DMA_Priority:软件设置数据流的优先级,有4个可选优先级分别为非常高、高、中和低,它设定DMA_SxCR寄存器的PL[1:0]位的值。 DMA优先级只有在多个DMA数据流同时使用时才有意义,这里我们设置为非常高优先级就可以了。
-
DMA_FIFOMode:FIFO模式使能,如果设置为DMA_FIFOMode_Enable表示使能FIFO模式功能; 它设定DMA_SxFCR寄存器的DMDIS位。ADC采集传输使用直接传输模式即可,不需要使用FIFO模式。
-
DMA_FIFOThreshold:FIFO阈值选择,可选4种状态分别为FIFO容量的1/4、1/2、3/4和满;它设定DMA_SxFCR寄存器的FTH[1:0]位; DMA_FIFOMode设置为DMA_FIFOMode_Disable,那DMA_FIFOThreshold值无效。ADC采集传输不使用FIFO模式,设置改值无效。
-
DMA_MemoryBurst:存储器突发模式选择,可选单次模式、4节拍的增量突发模式、8节拍的增量突发模式或16节拍的增量突发模式, 它设定DMA_SxCR寄存器的MBURST[1:0]位的值。ADC采集传输是直接模式,要求使用单次模式。
-
DMA_PeripheralBurst:外设突发模式选择,可选单次模式、4节拍的增量突发模式、8节拍的增量突发模式或16节拍的增量突发模式, 它设定DMA_SxCR寄存器的PBURST[1:0]位的值。ADC采集传输是直接模式,要求使用单次模式。
4.DMA存储器到存储器模式实验
存储器到存储器模式可以实现数据在两个内存的快速拷贝。我们先定义一个静态的源数据,然后使用DMA传输把源数据拷贝到目标地址上,最后对比源数据和目标地址的数据,看看是否传输准确。
#ifndef __BSP_DMA_H
#define __BSP_DMA_H
#ifdef __cplusplus
extern "C"{
#endif
#include "stm32f4xx.h"
/* 相关宏定义,使用存储器到存储器传输必须使用DMA2 */
#define DMA_STREAM DMA2_Stream0
#define DMA_CHANNEL DMA_Channel_0
#define DMA_STREAM_CLOCK RCC_AHB1Periph_DMA2
#define DMA_IT_TCIF DMA_IT_TCIF0
#define DMA_IT_HTIF DMA_IT_HTIF0
#define DMA_FLAG_TCIF DMA_FLAG_TCIF0
#define DMA_FLAG_HTIF DMA_FLAG_HTIF0
#define DMA_STREAM_IRQn DMA2_Stream0_IRQn
#define DMA_STREAM_IRQHandler DMA2_Stream0_IRQHandler
#define BUFFER_SIZE 32
void Init_M2M_DMA(void);
#ifdef __cplusplus
}
#endif
#endif
#include "bsp_dma.h"
#include "stdio.h"
#include "string.h"
/* 定义aSRC_Const_Buffer数组作为DMA传输数据源
const关键字将aSRC_Const_Buffer数组变量定义为常量类型 */
const uint32_t aSRC_Const_Buffer[BUFFER_SIZE]= {
0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
0x41424344,0x45464748,0x494A4B4C,0x4D4E4F50,
0x51525354,0x55565758,0x595A5B5C,0x5D5E5F60,
0x61626364,0x65666768,0x696A6B6C,0x6D6E6F70,
0x71727374,0x75767778,0x797A7B7C,0x7D7E7F80
};
/* 定义DMA传输目标存储器 */
uint32_t aDST_Buffer[BUFFER_SIZE];
void Init_M2M_DMA(void)
{
/* 使能DMA时钟 */
RCC_AHB1PeriphClockCmd(DMA_STREAM_CLOCK, ENABLE);
/* 复位初始化DMA数据流 */
DMA_DeInit(DMA_STREAM);
/* 确保DMA数据流复位完成 */
while (DMA_GetCmdStatus(DMA_STREAM) != DISABLE) {
}
DMA_InitTypeDef DMA_InitStructure;
DMA_InitStructure.DMA_BufferSize=BUFFER_SIZE;//一次DMA事务传输的数据个数
DMA_InitStructure.DMA_Channel=DMA_CHANNEL;
DMA_InitStructure.DMA_DIR=DMA_DIR_MemoryToMemory;
DMA_InitStructure.DMA_FIFOMode=DMA_FIFOMode_Disable;
DMA_InitStructure.DMA_FIFOThreshold=DMA_FIFOThreshold_Full;
DMA_InitStructure.DMA_Memory0BaseAddr= (uint32_t)aDST_Buffer;
DMA_InitStructure.DMA_MemoryBurst=DMA_MemoryBurst_Single;
DMA_InitStructure.DMA_MemoryDataSize=DMA_MemoryDataSize_Word;
DMA_InitStructure.DMA_MemoryInc=DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_Mode=DMA_Mode_Normal;
DMA_InitStructure.DMA_PeripheralBaseAddr=(uint32_t)aSRC_Const_Buffer;
DMA_InitStructure.DMA_PeripheralBurst=DMA_PeripheralBurst_Single;
DMA_InitStructure.DMA_PeripheralDataSize=DMA_PeripheralDataSize_Word;
DMA_InitStructure.DMA_PeripheralInc=DMA_PeripheralInc_Enable;
DMA_InitStructure.DMA_Priority=DMA_Priority_Low;
DMA_Init(DMA_STREAM,&DMA_InitStructure);
//配置中断控制器并使能中断
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel=DMA_STREAM_IRQn;
NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority=0;
NVIC_Init(&NVIC_InitStruct);
DMA_ITConfig(DMA_STREAM,DMA_IT_TCIF|DMA_IT_HTIF,ENABLE);
DMA_ClearITPendingBit(DMA_STREAM,DMA_IT_TCIF|DMA_IT_HTIF);
DMA_Cmd(DMA_STREAM,ENABLE);
}
void DMA_STREAM_IRQHandler(void)
{
if(SET== DMA_GetFlagStatus(DMA_STREAM,DMA_FLAG_HTIF))
{
printf("half transfer\r\n");
DMA_ClearFlag(DMA_STREAM,DMA_FLAG_HTIF);
}
else if(SET== DMA_GetFlagStatus(DMA_STREAM,DMA_FLAG_TCIF))
{
printf("transfer complete\r\n");
DMA_ClearFlag(DMA_STREAM,DMA_FLAG_TCIF);
}
// if(SET==DMA_GetITStatus(DMA_STREAM,DMA_IT_HTIF))
// {
// //half transfer complete
// printf("half transfer\r\n");
// DMA_ClearITPendingBit(DMA_STREAM,DMA_IT_HTIF);
//
// }
// else if(SET==DMA_GetITStatus(DMA_STREAM,DMA_IT_TCIF))
// {
// //transfer complete
//
// if(0== memcmp(aSRC_Const_Buffer,aDST_Buffer,BUFFER_SIZE))
// {
// printf("transfer complete\r\n");
// }
// DMA_ClearITPendingBit(DMA_STREAM,DMA_IT_TCIF);
// }
}
编程注意事项:
- 对于存储器到存储器传输模式,源地址和目标地址的设置, 采用与外设到存储器模式相同配置。也就是源存储器地址当作外设地址。
- 我们在中断服务函数中检查ITStatus时发现,没有检测到half transfer的IT标志位,但是可以检测到transfer complete的IT标志位。但是FlagStatus都可以获取到,也不知道是啥问题。
- 如果我换为其他的DMA2_STREAM,比如DMA2_Stream1。甚至transfer complete的IT标志位都没有检测到。也不知道是啥原因。
- 对于存储器到存储器传输模式,只能选择DMA2。