DMA概述
DMA为直接存储访问,其作用是代替CPU开进行外设和存储器之间数据的迁移。

之前数据的搬运是靠CPU来实现的,现在有了DMA,就可以通过CPU配置DMA,然后数据搬运就由DMA来实现。

DMA的存在,使得CPU可以去处理自己擅长的计算工作,而把一部分工作交由DMA来进行处理。

之前数据搬运全部由CPU来处理,现在是CPU告诉DMA例如要接收多少数据,但是具体的数据搬运的工作是由DMA自己来完成的。DMA是属于MCU的一个片上外设,不过它的地位要比一般的片上外设高,但是肯定比CPU的地位要低。

宏观理解
我们把上面的图简化一下



第一步是CPU给DMA下发任务。

第二步是由外设向DMA发送请求。

第三步是DMA先对外设进行响应后完成数据交换。

最后一步通知CPU处理完成。
总线共享

这里CPU和DMA共用总线,这里MCU内部会进行协调,分配带宽。这里保证CPU至少有一半带宽。
DMA通道
真正实现数据搬迁的其实是DMA内部的通道。提供的多个通道是并发执行的,不同通道处理的外设不同,数据搬迁方向不同,但是为了实现多个外设的数据搬迁,使用了多通道,并发来进行处理。
通道优先级
这里不是说并发?为什么还有优先级?这是因为优先级高也不是时刻都要搬迁数据。当高优先级空闲或者等待数据,低优先级也可以搬迁数据,这里和CPU处理的差别就很大了。
分为软件优先级和硬件优先级
软件优先级有四个等级,硬件优先级按照管道顺序来拍,先比较软件优先级,软件优先级相等才会比较硬件优先级。

我们可以看到,外设对应哪个通道这个是已经约定好的,然后多个外设接入同一个通道只能选择一个。

这里是决定出哪一个进行发送,但是这里是并发的。
寄存器讲解
拿DMA1举例一共有(2+4*7)个寄存器。2是七个通道共用的寄存器,4是每一个通道都有的单独的寄存器。

这个寄存器用于配置每一个通道,包含模式,软件优先级,存储器数据宽度,外设数据宽度,两个地址递增模式,循环模式,传输方向,三个中断位,一个使能开关。

表示要传输的数据大小。

这两个表示,传输两端的起始地址。
DMA与中断

三个中断标志分别为,出错中断,传输结束中断和传输一半中断。上图是传输一半的例子。DMA在产生中断,其实是内部的通道产生的中断。
cpp
DMA1_Channel1_IRQn = 11, /*!< DMA1 Channel 1 global Interrupt */
DMA1_Channel2_IRQn = 12, /*!< DMA1 Channel 2 global Interrupt */
DMA1_Channel3_IRQn = 13, /*!< DMA1 Channel 3 global Interrupt */
DMA1_Channel4_IRQn = 14, /*!< DMA1 Channel 4 global Interrupt */
DMA1_Channel5_IRQn = 15, /*!< DMA1 Channel 5 global Interrupt */
DMA1_Channel6_IRQn = 16, /*!< DMA1 Channel 6 global Interrupt */
DMA1_Channel7_IRQn = 17, /*!< DMA1 Channel 7 global Interrupt */
但是这里一个通道对应一个中断,是如何区分哪一类型的中断呢,就靠剩余的两个DMA共用的寄存器来控制。


这两个寄存器,通过给每一个通道四位,来控制三种中断和总的是否有中断的表示,下面寄存器控制上面寄存器。

这是整体的流程图
初始化代码
cpp
UART_HandleTypeDef huart1;
DMA_HandleTypeDef hdma_usart1_rx;
DMA_HandleTypeDef hdma_usart1_tx;
首先可以看到的是其对应的句柄结构
cpp
typedef struct __DMA_HandleTypeDef
{
DMA_Channel_TypeDef *Instance; /*!< Register base address */
DMA_InitTypeDef Init; /*!< DMA communication parameters */
HAL_LockTypeDef Lock; /*!< DMA locking object */
__IO HAL_DMA_StateTypeDef State; /*!< DMA transfer state */
void *Parent; /*!< Parent object state */
void (* XferCpltCallback)( struct __DMA_HandleTypeDef * hdma); /*!< DMA transfer complete callback */
void (* XferHalfCpltCallback)( struct __DMA_HandleTypeDef * hdma); /*!< DMA Half transfer complete callback */
void (* XferErrorCallback)( struct __DMA_HandleTypeDef * hdma); /*!< DMA transfer error callback */
void (* XferAbortCallback)( struct __DMA_HandleTypeDef * hdma); /*!< DMA transfer abort callback */
__IO uint32_t ErrorCode; /*!< DMA Error code */
DMA_TypeDef *DmaBaseAddress; /*!< DMA Channel Base Address */
uint32_t ChannelIndex; /*!< DMA Channel Index */
} DMA_HandleTypeDef;
句柄中包含的就是这个通道地址,和初始化寄存器的数据,和指向哪个DMA的这些信息。
cpp
typedef struct
{
__IO uint32_t CCR;
__IO uint32_t CNDTR;
__IO uint32_t CPAR;
__IO uint32_t CMAR;
} DMA_Channel_TypeDef;
typedef struct
{
__IO uint32_t ISR;
__IO uint32_t IFCR;
} DMA_TypeDef;
这样可以找到具体通道的寄存器,也可以找到DMA共用的寄存器。
cpp
typedef struct
{
uint32_t Direction; /*!< Specifies if the data will be transferred from memory to peripheral,
from memory to memory or from peripheral to memory.
This parameter can be a value of @ref DMA_Data_transfer_direction */
uint32_t PeriphInc; /*!< Specifies whether the Peripheral address register should be incremented or not.
This parameter can be a value of @ref DMA_Peripheral_incremented_mode */
uint32_t MemInc; /*!< Specifies whether the memory address register should be incremented or not.
This parameter can be a value of @ref DMA_Memory_incremented_mode */
uint32_t PeriphDataAlignment; /*!< Specifies the Peripheral data width.
This parameter can be a value of @ref DMA_Peripheral_data_size */
uint32_t MemDataAlignment; /*!< Specifies the Memory data width.
This parameter can be a value of @ref DMA_Memory_data_size */
uint32_t Mode; /*!< Specifies the operation mode of the DMAy Channelx.
This parameter can be a value of @ref DMA_mode
@note The circular buffer mode cannot be used if the memory-to-memory
data transfer is configured on the selected Channel */
uint32_t Priority; /*!< Specifies the software priority for the DMAy Channelx.
This parameter can be a value of @ref DMA_Priority_level */
} DMA_InitTypeDef;
这里包含的就是我们要给寄存器配置的值。
cpp
static void MX_DMA_Init(void)
{
/* DMA controller clock enable */
__HAL_RCC_DMA1_CLK_ENABLE();
/* DMA interrupt init */
/* DMA1_Channel4_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA1_Channel4_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel4_IRQn);
/* DMA1_Channel5_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA1_Channel5_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(DMA1_Channel5_IRQn);
}
我们可以看到在初始化函数这里,打开的是NVIC的中断,但是具体DMA的配置在哪里呢。
cpp
/* USART1 DMA Init */
/* USART1_RX Init */
hdma_usart1_rx.Instance = DMA1_Channel5;
hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_rx.Init.Mode = DMA_NORMAL;
hdma_usart1_rx.Init.Priority = DMA_PRIORITY_LOW;
if (HAL_DMA_Init(&hdma_usart1_rx) != HAL_OK)
{
Error_Handler();
}
__HAL_LINKDMA(huart,hdmarx,hdma_usart1_rx);
/* USART1_TX Init */
hdma_usart1_tx.Instance = DMA1_Channel4;
hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_usart1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE;
hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_usart1_tx.Init.Mode = DMA_NORMAL;
hdma_usart1_tx.Init.Priority = DMA_PRIORITY_LOW;
if (HAL_DMA_Init(&hdma_usart1_tx) != HAL_OK)
{
Error_Handler();
}
__HAL_LINKDMA(huart,hdmatx,hdma_usart1_tx);
这里便配置了DMA中通道的寄存器。本质就是向这些寄存器中配置数据。

这里就是对CCR寄存器进行配置。

这里确定了哪个DMA,然后准备好指向共用寄存器的数组下标。
这里注意通道一个占20字节,但是只有四个寄存器,所以空四个出来,共用寄存器放在DMA最前面。

cpp
#define __HAL_LINKDMA(__HANDLE__, __PPP_DMA_FIELD__, __DMA_HANDLE__) \
do{ \
(__HANDLE__)->__PPP_DMA_FIELD__ = &(__DMA_HANDLE__); \
(__DMA_HANDLE__).Parent = (__HANDLE__); \
} while(0U)
这个作用是把DMA和外设建立起链接。
