1 DMA介绍
1.1 什么是DMA
DMA:Direct Memory Access,直接存储器存取。是一种用于在外围设备和内存之间进行数据传输的机制。通常情况下,中央处理(CPU)负责管理数据的传输,但使用DMA可以让外围设备直接访问系统内存,而无需CPU的干预。
两个DMA控制器共有12个通道(DMA1有7个通道,DMA2有5个通道),每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个DMA请求的优先权。
两个DMA控制器都能实现从外设向存储器传输数据(P->M)、存储器向外设传输数据(M->P)、存储器向存储器传输数据(M->M)。
P:Peripheral; M:Memory,可以是Flash,也可以是SRAM。
1.2 DMA的优点
-
1.提高性能: DMA允许外围设备直接与内存进行数据传输,而不需要CPU的介入。这可以显著提高数据传输的效率,释放CPU的资源,使其能够专注于其他任务。
-
2.降低CPU负载: 由于DMA负责数据传输,CPU无需执行每个数据传输的步骤。这有助于减轻CPU的负载,使其能够处理其他任务。
-
3.减少延迟: 直接内存访问可以减少数据传输的延迟,因为它不需要等待CPU执行传输操作。
-
4.支持高速数据传输: 对于需要高速数据传输的设备,如硬盘驱动器或图形卡,DMA是至关重要的,因为它允许快速而有效地移动大量数据。
1.3 DMA工作的一般步骤
-
1.外围设备请求: 当外围设备需要将数据传输到内存或从内存读取数据时,它会发送一个DMA请求。
-
2.DMA控制器响应: DMA控制器收到外围设备的请求后,会协调数据传输。DMA控制器与外围设备和内存之间直接进行通信。
-
3.中断CPU: 为了确保DMA控制器能够获得对内存总线的访问权限,它需要暂时中断CPU。这是为了避免CPU和DMA控制器同时访问内存总线,导致冲突。
-
4.DMA控制器获得内存总线控制权: 一旦中断CPU,DMA控制器获得对内存总线的控制权,可以直接访问内存。
-
5.数据传输: DMA控制器开始在外围设备和内存之间传输数据,而无需CPU的干预。这可以是从外围设备到内存的写操作,也可以是从内存到外围设备的读操作。
-
6.DMA完成通知: 一旦数据传输完成,DMA控制器通常会发送一个中断信号给CPU,以便CPU可以继续执行其他任务。
2 功能框图讲解
2.1 DMA功能框图
DMA功能框图重点理解三个部分:DMA请求、通道与仲裁器。
1.大容量产品是指闪存存储器容量在256K至512K字节之间的STM32F101xx和STM32F103xx微控制器。
2.互联型产品是指STM32F105xx和STM32F107xx微控制器。
2.2 DMA请求
如果外设要想通过 DMA 来传输数据,必须先给 DMA 控制器发送 DMA 请求, DMA 收到请求信号之后,控制器会给外设一个应答信号,当外设应答后且 DMA 控制器收到应答信号之后,就会启动 DMA 的传输,直到传输完毕。
请求可以由双方中的任一方提出,可以由发送方提出,也可以由接收方提出。
2.2.1 DMA1请求映像
2.2.2 DMA2请求映像
2.3 通道
虽然每个通道可以接收多个外设的请求,但是同一时间只能接收一个,不能同时接收多个。
2.3.1 DMA1通道
2.3.2 DMA2通道
2.4 仲裁器
当多个DMA请求一起来时,应该怎么选择?
仲裁器根据通道请求的优先级来启动外设/存储器的访问。
优先级可分为2个阶段进行判断:
- 1.软件阶段:通过寄存器DMA_CCRx位13:12 PL[1:0]来判断。
如果软件阶段的优先级一样,再比较硬件优先级。
- 2.硬件阶段:通道编号小的优先级大,DMA1的优先级高于DMA2的优先级。
3 DMA初始化结构体
// stm32f10x_dma.h 文件
typedef struct
{
uint32_t DMA_PeripheralBaseAddr; /*!< Specifies the peripheral base address for DMAy Channelx. */
uint32_t DMA_MemoryBaseAddr; /*!< Specifies the memory base address for DMAy Channelx. */
uint32_t DMA_DIR; /*!< Specifies if the peripheral is the source or destination.
This parameter can be a value of @ref DMA_data_transfer_direction */
uint32_t DMA_BufferSize; /*!< Specifies the buffer size, in data unit, of the specified Channel.
The data unit is equal to the configuration set in DMA_PeripheralDataSize
or DMA_MemoryDataSize members depending in the transfer direction. */
uint32_t DMA_PeripheralInc; /*!< Specifies whether the Peripheral address register is incremented or not.
This parameter can be a value of @ref DMA_peripheral_incremented_mode */
uint32_t DMA_MemoryInc; /*!< Specifies whether the memory address register is incremented or not.
This parameter can be a value of @ref DMA_memory_incremented_mode */
uint32_t DMA_PeripheralDataSize; /*!< Specifies the Peripheral data width.
This parameter can be a value of @ref DMA_peripheral_data_size */
uint32_t DMA_MemoryDataSize; /*!< Specifies the Memory data width.
This parameter can be a value of @ref DMA_memory_data_size */
uint32_t DMA_Mode; /*!< Specifies the operation mode of the DMAy Channelx.
This parameter can be a value of @ref DMA_circular_normal_mode.
@note: The circular buffer mode cannot be used if the memory-to-memory
data transfer is configured on the selected Channel */
uint32_t DMA_Priority; /*!< Specifies the software priority for the DMAy Channelx.
This parameter can be a value of @ref DMA_priority_level */
uint32_t DMA_M2M; /*!< Specifies if the DMAy Channelx will be used in memory-to-memory transfer.
This parameter can be a value of @ref DMA_memory_to_memory */
}DMA_InitTypeDef;
3.1 数据从哪里来,存到哪里去?
uint32_t DMA_PeripheralBaseAddr; /* 外设地址 */
uint32_t DMA_MemoryBaseAddr; /* 存储器地址 */
uint32_t DMA_DIR; /* 传输方向 */
-
外设地址:由DMA_CPARx寄存器配置。
-
存储器地址:由DMA_CMARx寄存器配置。
-
传输方向:由DMA_CCRx寄存器位4 DIR决定;如果是 M->M 传输则需要用到DMA_CCRx寄存器位14 MEM2MEM。
假设数据从外设(比如从ADC-DR寄存器)来,就把(ADC-DR寄存器)地址写到DMA_CPARx寄存器中。
假设数据从存储器内部来,就要把地址写到DMA_CMARx寄存器中。
3.2 数据要传多少,传的单位是什么?
uint32_t DMA_BufferSize; /*!< 传输数目 */
uint32_t DMA_PeripheralInc; /*!< 外设地址增量模式 */
uint32_t DMA_MemoryInc; /*!< 存储器地址增量模式 */
uint32_t DMA_PeripheralDataSize; /*!< 外设数据宽度 */
uint32_t DMA_MemoryDataSize; /*!< 存储器数据宽度 */
-
传输数目:由DMA_CNDTR寄存器配置。
-
外设地址增量模式:由DMA_CCRx寄存器位6 PINC配置。
-
存储器地址增量模式:由DMA_CCRx寄存器位7 MINC配置。
-
外设数据宽度:DMA_CCRx寄存器位9:8 PSIZE[1:0]配置。
-
存储器数据宽度:DMA_CCRx寄存器位11:10 MSIZE[1:0]配置。
3.2.1 增量模式
假设现在我们要通过串口发送数据(M->P),我们应该将外设地址增量模式配置为不执行外设地址增量操作、存储器地址增量模式配置为执行存储器地址增量操作。
3.2.2 数据宽度
-
如果源端宽度为32位,目标宽度为8位,那么就会丢弃高24位,只存储低8位。
-
如果源端宽度为8位,目标宽度为32位,那么就会将这8位存储在低8位。
3.3 什么时候传输结束?
uint32_t DMA_Mode; /*!< 模式选择 */
- 模式选择:由DMA_CCRx寄存器位5 CIRC配置。
不循环操作(normal):数据全发送完就不发了。
循环操作(circular):数据全发送完后再从头开始发送,不断发送。
- 每个 DMA 通道在 DMA 传输过半、传输完成和传输错误时都会有相应的标志位(DMA_ISR),如果使能了该类型的中断后,则会产生中断。
4 DMA相关寄存器
在以下列举的所有寄存器中,所有与通道6和通道7相关的位,对DMA2都不适用,因为DMA2只有5个通道。
4.1 DMA中断状态寄存器(DMA_ISR)
4.2 DMA中断标志清除寄存器(DMA_IFCR)
4.3 DMA通道x配置寄存器(DMA_CCRx)(x = 1…7)
4.4 DMA通道x传输数量寄存器(DMA_CNDTRx)(x = 1…7)
4.5 DMA通道x外设地址寄存器(DMA_CPARx)(x = 1…7)
4.6 DMA通道x存储器地址寄存器(DMA_CMARx)(x = 1…7)
5 存储器到存储器传输
为了使工程更有条理,我们把DMA相关的代码独立分开存储,方便以后移植。在“工程模板”上新建 “bsp_dma_m2m.c” 文件和"bsp_dma_m2m.h" 文件。
5.1 编程要点
把内部FLASH的数据传输到内部的SRAM中。
-
1.初始化DMA初始化结构体。
-
2.熟读参考手册DMA章节(非常重要)。
5.2 步骤
-
1.在FLASH中定义好要传输的数据,在SRAM中定义好用来接收FLASH数据的变量。
-
2.初始化DMA,主要是配置DMA初始化结构体。
-
3.编写比较函数。
-
4.编写main函数。
// bsp_dma_m2m.c 文件
#include "bsp_dma_m2m.h"
/* 定义aSRC_Const_Buffer数组作为DMA传输数据源
* const关键字将aSRC_Const_Buffer数组变量定义为常量类型
* 表示数据存储在内部的FLASH中
*/
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传输目标存储器
* 存储在内部的SRAM中
*/
uint32_t aDST_Buffer[BUFFER_SIZE];
void dma_m2m_config(void)
{
DMA_InitTypeDef DMA_InitStruct;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)aSRC_Const_Buffer;
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)aDST_Buffer;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStruct.DMA_BufferSize = BUFFER_SIZE;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
DMA_InitStruct.DMA_Priority = DMA_Priority_High;
DMA_InitStruct.DMA_M2M = DMA_M2M_Enable;
DMA_Init(DMA1_Channel1, &DMA_InitStruct);
DMA_Cmd(DMA1_Channel1, ENABLE);
}
/**
* 判断指定长度的两个数据源是否完全相等,
* 如果完全相等返回1,只要其中一对数据不相等返回0
*/
uint8_t Buffercmp(const uint32_t* pBuffer, uint32_t* pBuffer1, uint16_t BufferLength)
{
/* 数据长度递减 */
while(BufferLength--)
{
/* 判断两个数据源是否对应相等 */
if(*pBuffer != *pBuffer1)
{
/* 对应数据源不相等马上退出函数,并返回0 */
return 0;
}
/* 递增两个数据源的地址指针 */
pBuffer++;
pBuffer1++;
}
/* 完成判断并且对应数据相对 */
return 1;
}
// bsp_dma_m2m.h 文件
#ifndef __BSP_DMA_M2M_H
#define __BSP_DMA_M2M_H
#include "stm32f10x.h"
// 要发送的数据大小
#define BUFFER_SIZE 32
void dma_m2m_config(void);
uint8_t Buffercmp(const uint32_t* pBuffer, uint32_t* pBuffer1, uint16_t BufferLength);
#endif /* __BSP_DMA_M2M_H */
// main.c 文件
#include "bsp_led.h"
#include "stm32f10x.h"
#include "bsp_dma_m2m.h"
extern const uint32_t aSRC_Const_Buffer[BUFFER_SIZE];
extern uint32_t aDST_Buffer[BUFFER_SIZE];
int main(void)
{
LED_G_GPIO_Config();
LED_B_GPIO_Config();
LED_R_GPIO_Config();
dma_m2m_config();
LED_B(1);
while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
LED_B(0);
if(Buffercmp(aSRC_Const_Buffer, aDST_Buffer, BUFFER_SIZE))
{
LED_G(1);
}
else
{
LED_R(1);
}
while(1)
{
}
}
6 存储器到外设传输
为了使工程更有条理,我们把DMA相关的代码独立分开存储,方便以后移植。在“工程模板”上新建 “bsp_dma_m2p.c” 文件和"bsp_dma_m2p.h" 文件。
6.1 编程要点
SRAM到串口。
-
1.初始化串口。
-
2.配置DMA初始化结构体。
-
3.编写main函数。
// bsp_dma_m2p.c 文件
#include "bsp_dma_m2p.h"
uint8_t SENDBUFF[SENDBUFF_SIZE];
/**
* @brief USART GPIO 配置,工作参数配置
* @param 无
* @retval 无
*/
void USART_Config(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
USART_InitTypeDef USART_InitStructure;
// 打开串口GPIO的时钟
RCC_APB2PeriphClockCmd(DEBUG_USART_GPIO_CLK, ENABLE);
// 打开串口外设的时钟
RCC_APB2PeriphClockCmd(DEBUG_USART_CLK, ENABLE);
RCC_APB1PeriphClockCmd(DEBUG_USART_CLK, ENABLE);
// 将USART Tx的GPIO配置为推挽复用模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_TX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(DEBUG_USART_TX_GPIO_PORT, &GPIO_InitStructure);
// 将USART Rx的GPIO配置为浮空输入模式
GPIO_InitStructure.GPIO_Pin = DEBUG_USART_RX_GPIO_PIN;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_Init(DEBUG_USART_RX_GPIO_PORT, &GPIO_InitStructure);
// 配置串口的工作参数
// 配置波特率
USART_InitStructure.USART_BaudRate = DEBUG_USART_BAUDRATE;
// 配置 针数据字长
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
// 配置停止位
USART_InitStructure.USART_StopBits = USART_StopBits_1;
// 配置校验位
USART_InitStructure.USART_Parity = USART_Parity_No ;
// 配置硬件流控制
USART_InitStructure.USART_HardwareFlowControl =
USART_HardwareFlowControl_None;
// 配置工作模式,收发一起
USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;
// 完成串口的初始化配置
USART_Init(DEBUG_USARTx, &USART_InitStructure);
// // 串口中断优先级配置
// NVIC_Configuration();
//
// // 使能串口接收中断
// USART_ITConfig(DEBUG_USARTx, USART_IT_RXNE, ENABLE);
// 使能串口
USART_Cmd(DEBUG_USARTx, ENABLE);
}
void dma_m2p_config(void)
{
DMA_InitTypeDef DMA_InitStruct;
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)(USART1_BASE+0x04);
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)SENDBUFF;
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST;
DMA_InitStruct.DMA_BufferSize = SENDBUFF_SIZE;
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
DMA_InitStruct.DMA_Priority = DMA_Priority_High;
DMA_InitStruct.DMA_M2M = DMA_M2M_Disable;
DMA_Init(DMA1_Channel4, &DMA_InitStruct);
DMA_Cmd(DMA1_Channel4, ENABLE);
}
// bsp_dma_m2p.h 文件
#ifndef __BSP_DMA_M2P_H
#define __BSP_DMA_M2P_H
#include "stm32f10x.h"
// 要发送的数据大小
#define SENDBUFF_SIZE 5000
// 串口1-USART1
#define DEBUG_USARTx USART1
#define DEBUG_USART_CLK RCC_APB2Periph_USART1
#define DEBUG_USART_APBxClkCmd RCC_APB2PeriphClockCmd
#define DEBUG_USART_BAUDRATE 115200
// USART GPIO 引脚宏定义
#define DEBUG_USART_GPIO_CLK (RCC_APB2Periph_GPIOA)
#define DEBUG_USART_GPIO_APBxClkCmd RCC_APB2PeriphClockCmd
#define DEBUG_USART_TX_GPIO_PORT GPIOA
#define DEBUG_USART_TX_GPIO_PIN GPIO_Pin_9
#define DEBUG_USART_RX_GPIO_PORT GPIOA
#define DEBUG_USART_RX_GPIO_PIN GPIO_Pin_10
#define DEBUG_USART_IRQ USART1_IRQn
#define DEBUG_USART_IRQHandler USART1_IRQHandler
void USART_Config(void);
void dma_m2p_config(void);
#endif /* __BSP_DMA_M2M_P */
// main.c 文件
#include "stm32f10x.h"
#include "bsp_led.h"
#include "bsp_dma_m2m.h"
#include "bsp_dma_m2p.h"
#include <string.h>
extern uint8_t SENDBUFF[SENDBUFF_SIZE];
int main(void)
{
LED_G_GPIO_Config();
LED_B_GPIO_Config();
LED_R_GPIO_Config();
USART_Config();
memset(SENDBUFF, 0, sizeof(SENDBUFF));
dma_m2p_config();
USART_DMACmd(DEBUG_USARTx, USART_DMAReq_Tx, ENABLE);
while(1)
{
}
}
以上为单片机复位后通过串口助手看到的数据。
6.3 M->M与M->P的区别
- M->M一旦使能就立刻进行数据传输,但是M->P不一样,需要外设给DMA发送一个请求。