目录
一、DMA基本介绍
1、DMA的定义
DMA(Direct Memory Access)—直接存储器存取,DMA 控制器独立于内核,结构比较简单,是单片机的一个外设,它的主要功能是用来搬数据,但是不需要占用 CPU,即在传输数据的时候,CPU 可以干其他的事情,好像是多线程一样。数据传输支持从外设到存储器或者存储器到存储器,这里的存储器可以是 SRAM 或者是 FLASH,SRAM(默认存放变量)或者是FLASH(默认存放常量,被const修饰的全局变量可以看成是常量类型),而外设指的其实是外设的数据寄存器。
所以就是在需要传输大量数据的时候,我们可以给他们提供一条数据通道,这是是可以不用占用内核CPU,不经过CPU处理,对于实时性要求比较高的场合,我们可以利用DMA来减小CPU的负担。
2、DMA数据传输
我们知道,数据传输,首先需要配置的是,在后面结构体配置时仔细分开
- 数据传输的目标地址
- 数据传输数目大小及传输单位
- 传输数据结束的模式选择
主要是有三种形式传输:存储器和存储器之间(M->M)、外设到存储器(P->M)、存储器到外设之间的传输(M->P);
DMA 控制器包含了 DMA1 和 DMA2,其中 DMA1 有 7 个通道, DMA2 有 5 个通道,这里的通道可以理解为传输数据的一种管道。要注意的是 DMA2 只存在于大容量或互联型(f105/f107)的单片机中。
每个通道都有3个事件标志(DMA半传输、DMA传输完成和DMA传输出错),这3个事件标志逻辑或成为一个单独的中断请求;如果配置了中断使能,就会触发中断,然后通过中断标志位判断引起中断的事件类型,通常我们用来监测DMA是否传输完成。
闪存、SRAM、外设的SRAM、APB1、APB2和AHB外设均可作为访问的源和目标。
二、DMA功能框图
图中为DMA框图,主要是图中三个部分:
1、DMA请求
在发生一个事件后,外设向DMA控制器发送一个请求信号。DMA控制器根据通道的优先权处理请求。当DMA控制器开始访问发出请求的外设时,DMA控制器立即发送给它一个应答信号。当从DMA控制器得到应答信号时,外设立即释放它的请求。一旦外设释放了这个请求,DMA控制器同时撤销应答信号。这是就会启动数据传输直至结束。
不同的 DMA 控制器的通道对应着不同的外设请求,这决定了我们在软件编程上该怎么设置,比如ADC1对应DMA1的通道1,串口USART1的发送对应DMA1的通道4,具体见 DMA 请求映像表(图上没有的说明不具备DMA请求功能):
图片来源:STM32F10xxx参考手册>>>10 DMA控制器(DMA)>>>表60 各个通道的DMA请求一览
2、通道
DMA有两个DMA控制器一共有12个通道(DMA1有7个通道, DMA2有5个通道),每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。虽然每个通道可以接收多个外设的请求,但是同一时间只能接收一个,不能同时接收多个。
3、仲裁器
当发生多个 DMA 通道请求时,响应处理的顺序问题就由仲裁器管理。仲裁器管理 DMA 通道请求分为两个阶段。第一阶段属于软件阶段,可以在DMA_CCRx 寄存器中DMA通道寄存器PL中设置,有 4 个等级:非常高、高、中和低四个优先级。
第二阶段属于硬件阶段,如果两个或以上的 DMA 通道请求设置的优先级一样,则他们优先级取决于通道编号,编号越小优先级越高,比如通道 0 高于通道 1。在大容量产品和互联型产品中,
DMA1 控制器拥有高于 DMA2 控制器的优先级。
三、DMA数据配置
使用DMA,最重要的就是上面说的三点,从哪里来到哪里去,传输的数据单位多少,传输多少大小的数据,模式选择一次传输还是循环传输。
- 数据传输的目标地址
- 数据传输数目大小及传输单位
- 传输数据结束的模式选择
1、传输方向及地址
我们知道 DMA 传输数据的方向有三个:从外设到存储器,从存储器到外设,从存储器到存储器。数据传输方向 DMA_CCR 的位 4 DIR 配置: 0 表示从外设到存储器, 1 表示从存储器到外设。这里面涉及到的外设地址由 DMA_CPAR 配置,存储器地址由 DMA_CMAR 配置。
外设到存储器:当我们使用从外设到存储器传输时,以 ADC 采集为例。 DMA 外设寄存器的地址对应的就是 ADC 数据寄存器的地址, DMA 存储器的地址就是我们自定义的变量(用来接收存储 AD 采集的数据)的地址。方向我们设置外设为源地址。
存储器到外设:当我们使用从存储器到外设传输时,以串口向电脑端发送数据为例。 DMA 外设寄存器的地址对应的就是串口数据寄存器的地址, DMA 存储器的地址就是我们自定义的变量(相当于一个缓冲区,用来存储通过串口发送到电脑的数据)的地址。方向我们设置外设为目标地址。
存储器到存储器:当我们使用从存储器到存储器传输时,以内部 FLASH 向内部 SRAM 复制数据为例。 DMA 外设寄存器的地址对应的就是内部 FLASH(我们这里把内部 FALSH 当作一个外设来看)的地址, DMA 存储器的地址就是我们自定义的变量(相当于一个缓冲区,用来存储来自内部 FLASH 的数据)的地址。方向我们设置外设(即内部 FLASH)为源地址。跟上面两个不一样的是,这里需要把 DMA_CCR 位 14: MEM2MEM:存储器到存储器模式配置为 1,即启动 M2M 模式,在结构体配置时需开启。
2、传输数据大小及单位
当配置好数据传输方向后,我们还需要正确配置数据的大小,数据的单位,而在DMA 控制器的控制下,数据要想有条不紊的从一个地方搬到另外一个地方,还必须正确设置两边数据指针的增量模式。
以串口向电脑发送数据为例,我们可以一次性给电脑发送很多数据,具体多少由 DMA_CNDTR 配置,这是一个 32 位的寄存器,前16位为数据传输数量,一次最多只能传输 65535 个数据。
要想数据传输正确,源和目标地址存储的数据宽度还必须一致,串口数据寄存器是 8 位的,所以在定义发送数据的时候也必须定义成8位,外设的数据宽度由 DMA_CCR 的 PSIZE[1:0] 配置,可以是 8/16/32 位,存储器的数据宽度由 DMA_CCR 的 MSIZE[1:0] 配置,可以是 8/16/32 位。
我们必须正确配置增量模式,以串口向电脑发送数据为例,我们要发送的数据设置很多个,那我们每发送完一个,那么存储器的地址指针就应该加1,而(外设)串口数据寄存器的只有一个,地址指针也是固定不变的,由基础地址加上偏移地址 (USART1_BASE+0x04),具体数据指针的增量模式根据实际情况来配置。
3、传输完成
数据什么时候传输完成,我们可以通过查询标志位或者通过中断的方式来鉴别。每个 DMA 通道在 DMA 传输过半、传输完成和传输错误时都会有相应的标志位,如果使能了该类型的中断后,则会产生中断。有关各个标志位的详细描述请参考 DMA 中断状态寄存器 DMA_ISR 的详细描述。
传输完成还分两种模式,是一次传输还是循环传输,一次传输很好理解,即是传输一次之后就停止,要想再传输的话,必须关断 DMA 使能后再重新配置后才能继续传输。循环传输则是一次传输完成之后又恢复第一次传输时的配置循环传输,不断的重复。具体的由 DMA_CCR 寄存器的 CIRC 循环模式位控制。
三、DMA初始化结构体详解
结构体 xxx_InitTypeDef 和库函数 xxx_Init 配合使用是标准库精髓所在,理解了结构体 xxx_InitTypeDef 每个成员意义基本上就可以对该外设运用自如。所以我们在 stm32f10x_dma.h 文件中调用。
1、DMA_InitTypeDef初始化结构体配置
typedef struct
//数据输入与输出
uint32_t DMA_PeripheralBaseAddr; //外设地址
uint32_t DMA_MemoryBaseAddr; //存储器地址
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_M2M; //存储器到存储器模式
}DMA_InitTypeDef;
1、DMA_PeripheralBaseAddr:外设地址,设定 DMA_CPAR 寄存器的值;一般设置为外设的数据寄存器地址,如果是存储器到存储器模式则设置为其中一个存储器地址。
2、DMA_MemoryBaseAddr:存储器地址,设定 DMA_CMAR 寄存器值;一般设置为我们自定义存储区的首地址。
3、DMA_DIR :传输方向选择,可选外设到存储器、存储器到外设,可设置其中一个为起点。这里并没有存储器到存储器的方向选择,当使用存储器到存储器时,只需要把其中一个存储器当作外设使用即可。
4、DMA_BufferSize:设定待传输数据数目,初始化设置目标寄存器的值。
5、DMA_PeripheralInc:如果配置为 DMA_PeripheralInc_Enable,使能外设地址自动递增功能,它设定 DMA_CCR 寄存器的 PINC 位的值;一般外设都是只有一个数据寄存器,所以一般不会使能该位。
6、DMA_MemoryInc:如果配置为 DMA_MemoryInc_Enable,使能存储器地址自动递增功能,它设定 DMA_CCR 寄存器的 MINC 位的值;我们自定义的存储区一般都是存放多个数据的,所以要使能存储器地址自动递增功能。
7、DMA_PeripheralDataSize:外设数据宽度,可选字节Byte (8 位)、半字HalfWord (16 位) 和字Word (32 位)。
8、DMA_MemoryDataSize:存储器数据宽度,可选字节Byte (8 位)、半字HalfWord (16 位) 和字Word (32 位)。当外设和存储器之间传数据时,两边的数据宽度应该设置为一致大小。
9、DMA_Mode:DMA 传输模式选择,可选一次传输(DMA_Mode_Normal)或者循环传输(DMA_Mode_Circular)。例如我们的 ADC 采集是持续循环进行的,所以使用循环传输模式。
10、DMA_Priority:软件设置通道的优先级,有 4 个可选优先级分别为非常高、高、中和低,它设定 DMA_CCR 寄存器的 PL[1:0] 位的值。 DMA 通道优先级只有在多个 DMA 通道同时使用时才有意义,如果是单个通道,优先级可以随便设置。
11、DMA_M2M:存储器到存储器模式,使用存储器到存储器时用到,设定 DMA_CCR 的位 14 MEN2MEN 即可启动存储器到存储器模式。
2、程序设计
1、存储器到存储器
FLASH to SRAM 把内部FLASH的数据传输到内部的SRAM中,在这之中把 flash 定义为外设,sram 定义为存储器。
DMA.h
#include "stm32f10x.h"
#ifndef _DMA_H_
#define _DMA_H_
#define BUFFER_SIZE 16
#define SEND_SIZE 500
#define USART_DR_ADDR (USART1_BASE+0x04)
void DMA_MTM_Init(void);
uint8_t Buffercmp(const uint32_t *pBuffer1,uint32_t *pBuffer2,uint32_t *BufferLength);
void Usart_DMA_Init(void);
#endif
DMA.c
#include "stm32f10x.h"
#include "dma.h"
//const 关键字把数组Buffer定义为常量类型,表述数据存储在内部的Flash中
const uint32_t SRC_Buffer[BUFFER_SIZE] = {
0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
};
//定义DMA传输的目标存储器,变量存储在内部SRAM
uint32_t DST_Buffer[BUFFER_SIZE] ;
void DMA_MTM_Init(void)
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd( RCC_AHBPeriph_DMA1, ENABLE);
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)SRC_Buffer;
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)DST_Buffer;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;//外设作为起点
DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;
DMA_Init( DMA1_Channel6, &DMA_InitStructure ); //选取通道6
DMA_ClearFlag( DMA1_FLAG_TC6);
DMA_Cmd(DMA1_Channel6, ENABLE);
}
uint8_t Buffercmp(const uint32_t *pBuffer1,uint32_t *pBuffer2,uint32_t *BufferLength)
{
while (BufferLength --) //数据长度递减
{
if(*pBuffer1 != *pBuffer2) //判断两个数据源是否相等
{
return 0; //对应数据源不相等,返回0
}
pBuffer1++; //递增两个数据源的地址指针
pBuffer2++;
}
return 1; //完成判断并且对应数据相等
}
main.c
#include "stm32f10x.h"
#include "led.h"
#include "usart.h"
#include "dma.h"
extern const unsigned char BMP3[],BMP4[],BMP5[];
extern uint8_t SendBuffer[SEND_SIZE] ;
void delay(uint16_t time)
{
uint16_t i = 0;
while(time--)
{
i = 12000;
while(i--);
}
}
int main(void)
{
uint8_t status = 0;
extern const uint32_t SRC_Buffer[BUFFER_SIZE];
extern uint32_t DST_Buffer[BUFFER_SIZE];
LED_Init();
DMA_MTM_Init();
status = Buffercmp( SRC_Buffer, DST_Buffer ,BUFFER_SIZE);
if(status == 0)
{
GPIO_ResetBits( GPIOC, GPIO_Pin_13);
}
else
{
GPIO_SetBits( GPIOC, GPIO_Pin_13);
}
while(1)
{
}
}
2、存储器到外设
SRAM to USART把内部 sram 的数据传输到外部的 usart 中
usart串口为外设,sram 定义为存储器
DMA.h
#include "stm32f10x.h"
#ifndef _DMA_H_
#define _DMA_H_
#define BUFFER_SIZE 16
#define SEND_SIZE 500
#define USART_DR_ADDR (USART1_BASE+0x04)
void DMA_MTM_Init(void);
uint8_t Buffercmp(const uint32_t *pBuffer1,uint32_t *pBuffer2,uint32_t *BufferLength);
void Usart_DMA_Init(void);
#endif
USART.c
#include"usart.h"
#include"stm32f10x.h"
void usart_init(void)
{
GPIO_InitTypeDef gpio_init_Structure;
USART_InitTypeDef usart_init_Structure;
NVIC_InitTypeDef NVIC_init_Structure;
NVIC_PriorityGroupConfig( NVIC_PriorityGroup_2);
RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOA, ENABLE);
RCC_APB2PeriphClockCmd( RCC_APB2Periph_AFIO, ENABLE);
RCC_APB2PeriphClockCmd( RCC_APB2Periph_USART1, ENABLE);
//配置PA9 TX
gpio_init_Structure.GPIO_Mode = GPIO_Mode_AF_PP ;//复用推挽输出,IO内置外用GPIO_Mode_AF_PP
gpio_init_Structure.GPIO_Pin = GPIO_Pin_9;
gpio_init_Structure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init_Structure);
//配置PA10
gpio_init_Structure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
gpio_init_Structure.GPIO_Pin = GPIO_Pin_10;
//gpio_init_Structure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &gpio_init_Structure);
usart_init_Structure.USART_BaudRate =115200; //波特率
usart_init_Structure.USART_HardwareFlowControl =USART_HardwareFlowControl_None; //硬件流控制
usart_init_Structure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;//模式
usart_init_Structure.USART_Parity = USART_Parity_No;//检验位
usart_init_Structure.USART_StopBits = USART_StopBits_1; //停止位
usart_init_Structure.USART_WordLength = USART_WordLength_8b;//字长
USART_Init(USART1, &usart_init_Structure);
USART_ITConfig(USART1, USART_IT_RXNE, ENABLE );
USART_Cmd(USART1, ENABLE);
NVIC_init_Structure.NVIC_IRQChannel = USART1_IRQn;
NVIC_init_Structure.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_init_Structure.NVIC_IRQChannelSubPriority = 1;
NVIC_init_Structure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_init_Structure);
}
//发送字符
void usart_SendByte(USART_TypeDef* USARTx, uint16_t Data)
{
USART_SendData( USARTx, Data);
while(USART_GetFlagStatus( USARTx, USART_FLAG_TXE ) == RESET);
}
//发送字符串
void usart_SendStr(USART_TypeDef* USARTx, char *str)
{
uint16_t i =0;
do
{
usart_SendByte( USARTx, *(str + i) );
i++;
}while(*(str + i) != '\0');
while(USART_GetFlagStatus( USARTx, USART_FLAG_TC ) == RESET);
}
//利用printf发送字符串
int fputc(int ch, FILE *f)
{
USART_SendData( USART1, (uint8_t)ch );
while(USART_GetFlagStatus( USART1, USART_FLAG_TXE ) == RESET);
return (ch);
}
//接受字符串
int fgetc(FILE *F)
{
while(USART_GetFlagStatus( USART1, USART_FLAG_RXNE ) == RESET);
return (int) USART_ReceiveData( USART1 );
}
DMA.c
#include "stm32f10x.h"
#include "dma.h"
//const 关键字把数组Buffer定义为常量类型,表述数据存储在内部的Flash中
const uint32_t SRC_Buffer[BUFFER_SIZE] = {
0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
};
uint8_t SendBuffer[SEND_SIZE] ;
//内存发送到串口
void Usart_DMA_Init(void)
{
DMA_InitTypeDef DMA_InitStructure;
RCC_AHBPeriphClockCmd( RCC_AHBPeriph_DMA1, ENABLE);
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t) SendBuffer;
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t) USART_DR_ADDR;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStructure.DMA_BufferSize = SEND_SIZE;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //串口数据寄存器地址定义好了,不需要增量
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Priority = DMA_Priority_High;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_Init( DMA1_Channel4, &DMA_InitStructure );
DMA_ClearFlag( DMA1_FLAG_GL4);
DMA_Cmd(DMA1_Channel4 , ENABLE );
}
main.c
#include "stm32f10x.h"
#include "led.h"
#include "usart.h"
#include "dma.h"
extern uint8_t SendBuffer[SEND_SIZE] ;
void delay(uint16_t time)
{
uint16_t i = 0;
while(time--)
{
i = 12000;
while(i--);
}
}
int main(void)
{
uint16_t i = 0;
for(i=0;i<SEND_SIZE;i++)
{
SendBuffer[i] = '0';
}
usart_init();
Usart_DMA_Init();
USART_DMACmd( USART1, USART_DMAReq_Tx, ENABLE);
while(1)
{
}
}
3、问题总结
不同的 DMA 控制器的通道对应着不同的外设请求,我们需要按照DMA映像表正确的去配置通道口,比如本次中的 SARM接收到的数据放到 USART1_TX 中发送出去, USART1_TX 对应的就是 DMA1_Channel4(通道4)。然后需要根据实际情况配置数据大小和单位,根据需求配置模式,在多个通道的时候需要注意优先级。本次文章作为一个学习笔记。