DMA的作用
直接存储器访问(DMA)用于在外设与存储器之间以及存储器与存储器之间,提供高速数据传输。可以在无需任何CPU操作的情况下通过DMA快速移动数据。这样可以节省CPU的资源可以供其他操作使用。
一般数据搬运可以通过两个方式实现:CPU或者DMA,如果数据量很大,让CPU来负责这部分的工作的话,那CPU会长时间的忙于数据的搬运,对于其他的工作,就会延后执行,为了解决这个问题(CPU因忙于数据搬运而耽误其他任务的执行),于是就可以选择使用DMA,DMA这个外设是不需要CPU去干预的,可以根据用户的配置独立的自发的去进行数据的搬运。也可以这样理解:DMA就是CPU的助手、数据的搬运工
DMA的工作原理
从参考手册中DMA控制器的框图中我们可以发现,DMA外设在总线中的位置是一个独立的外设,独立于总线APB1、2之外,直接与总线矩阵相连接,它可以通过总线矩阵去直接的访问AHB系统总线或者是APB外设总线。所以DMA可以代替CPU做搬运数据的功能,
让我们来举个例子说明一下DMA是如何搬运数据的,比如说要将ADC1中的数据存放入SRAM中。
1:首先DMA会通过总线矩阵去通过总线桥访问APB1,在通过APB1去访问ADC1外设,并且将ADC1外设中的数据返回到DMA中
2.然后在将返回的数据通过总线矩阵发送到目的地址SRAM中。
这过程中不需要CPU的参与,这样就减轻了CPU的任务量,让其更好的去处理别的任务。其他型号的芯片DMA功能也是大同小异,可能略有不同,大家在开发每一款芯片的时候,需要仔细的阅读他的参考手册!!如果不仔细的看参考手册的话,可能开发起来会困难重重!!。
再来说说DMA内部是如何实现的:
首先要有外设的DMA请求(数据的来源),来告知DMA数据已经准备好啦,可以马上过来搬运。
然后DMA会去响应外设的DMA请求,然后DMA会去选择通道,来决定获取到的数据要放到DMA的哪个通道里,比如说ADC1的数据放到通道1,ADC2的数据放到通道2,TIM1的数据放到通道3等等…………,
最后,DMA会根据数据搬运的目标地址,然后把对应的通道链接到目标地址中,最终形成数据流的形式,将数据搬运到目的地址中。
当然,谁先使用DMA,谁先进行数据的传输,那就要看DMA仲裁器来判断哪个传输的优先级更加的高!!
总的来说,对于DMA这个器件,他的功能就是建立起一个数据传输的通道。
他既可以在存储器和存储器之间,也可以是外设和存储器、存储器和外设之间的传输。方向有限定!!!!外设到存储器、存储器到外设是不一样的。如串口(源)接收数据就可以通过DMA的方式之间将数据放到内存(目标)里,不需要CPU的干预。
再比如也可以将存储器(源)的数据通过DMA的方式通过串口外设(目标)来发送。
如何配置DMA及其配置原理
再总结一下手册里的DMA特性需要如何配置
1:建立(选取)的传输通道
{
(源)存储器--存储器(目标)
(源)外设---存储器(目标)
(源)存储器---外设(目标)
}
2:确定传输的具体对象
具体的功能:如ADC(源)---内存(目标)、内存(源)---UART(目标)
3:敲定传输的细节:
(1)通道优先级:当多个DMA请求同时发送的时候,哪一个通道先传输呢(因为DMA是串行的,而不能并行)
(2)确定传输数据双方的数据格式
(3)确定数据是否需要循环传输(搬运1次还是一直循环搬运也就是一直采集),如果选择搬运1次那就是一般传输模式,如果需要一直搬运那就需要循环传输模式
(4)是否需要传输标志/中断
DMA在HAL库的实现
为什么要在了解DMA在HAL库中的实现,主要为了以后学习外设的驱动有更深层次的认识和加深对HAL库的理解,提升工程代码的阅读能力,了解底层逻辑和对HAL库的封装有进一步了解。
我们先讲讲思路再来详细的看一下代码。HAL库中外设驱动的实现(任意外设都通用)
句柄结构体的概念及其成员分析
句柄结构体(XX_HandleTypeDef),句柄结构体无论是在HAL库还是RTOS,句柄词的意思是“(门的)把手;柄;可以这样理解,当我们使用一个函数时,我们要通过门把手才能开门,同理我们也需要句柄结构体才能去使用操作对象。他有两个重要的成员:Instance& Init
Instance:他指向了外设内,一个具体外设的成员,如ADC里的ADC1、UART里的UART1、DMA里的Channel2,他实际上是用指针指向一个外设的基地址(重点!!是基地址)。
Init:指向了一个具体外设的初始化结构体,用来配置外设的工作参数。
我们HAL库中会有很多很多的句柄结构体。让我们看看HAL库中的DMA句柄结构体DMA_HandleTypeDef吧。框起来的部分要重点理解,其他的可以查看参考手册!!
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;
回调函数的概念
图中的回调函数是一种特殊的函数,它作为参数传递给另一个函数,并在被调用函数执行完毕后被调用。回调函数通常用于事件处理、异步编程和处理各种操作系统和框架的API。
下图为Instance& Init他们的关系图:
2.初始化结构体(XX_InitTypeDef):根据外设的各种配置寄存器,组织起来的外设参数配置结构体,内附在XX_HandleTypeDef结构体中
那让我们再看看DMA_InitTypeDef结构体:
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;
结构体内定于的内容与上面我们在参考手册中了解到的DMA底层逻辑是有很大的关联性的。
我们细看一下结构体成员中的Direction,想要知道Direction是如何配置的,需要看注释中的说明,注释中说了是通过DMA_Data_transfer_direction这个函数来配置构体成员中的Direction,
我们再去查找DMA_Data_transfer_direction的定义,
我们可以看到DMA_Data_transfer_
#define DMA_PERIPH_TO_MEMORY 0x00000000U /*!< Peripheral to memory direction */
#define DMA_MEMORY_TO_PERIPH ((uint32_t)DMA_CCR_DIR) /*!< Memory to peripheral direction */
#define DMA_MEMORY_TO_MEMORY ((uint32_t)DMA_CCR_MEM2MEM) /*!< Memory to memory direction */
direction定义了三个方向分别是外设到内存、内存到外设、内存到内存。这与我们在之前说到的建立(选取)的传输通道相对应,其他的结构体成员也是用这种方法来查找和使用的。这里就不多说啦!!要学好HAL库就需要搞懂HAL的封装原理和HAL库的函数是如何实现的,这为我们后续的HAL库学习和RTOS的学习起着至关重要的作用。
那么HAL库就可以将我们的外设通过这一个句柄结构体和一个初始化结构体(初始化结构体是嵌入进句柄结构体中的)来进行组织和使用。
STM32CubeMX来配置DMA及其注意事项
接下来我们再讲解一下如何使用STM32CubeMX来配置DMA,以stm32f103为例。
在此注意:内存与内存之间的DMA源地址和目标地址都要递增
因为如果源地址递增而目标地址不递增就会出现:
源地址递增,目标地址不递增,导致源地址重复传输到一个目标地址上,这样会导致最后出现的结果是目标地址的第一个首地址的内容是源地址最后一个地址的内容,这样就会出现许许多多的错误!!!
而源地址不递增,目标递增递增,就会出现:
目标地址重复传输源地址首地址的内容,导致目标地址的内容全是源地址首地址的内容,这样也会诱发许许多多的错误!!
通常这种错误是不被允许的,特殊处理的时候才会用到,否则需要两个都递增!!!!
而对于外设---内存或内存---外设的情况来说,外设的地址是不需要递增的,因为外设只有一个地址,递增就会跑去别的外设地址去了,下列举个USART1的DMA设置:
HAL库实现DMA代码的解读
然后进行常规的工程配置,就可以生成代码了。可以看到生成的工程中已经覆盖了DMA和USART的文件了。
让我们一起来看看STM32CubeMX生成的文件里面的内容。打开dma.c就可以看见
代码在自己做工程的时候可以复制下来直接使用。
DMA_HandleTypeDef hdma_memtomem_dma1_channel1;
/**
* Enable DMA controller clock
* Configure DMA for memory to memory transfers
* hdma_memtomem_dma1_channel1
*/
void MX_DMA_Init(void)
{
/* DMA controller clock enable */
__HAL_RCC_DMA1_CLK_ENABLE();
/* Configure DMA request hdma_memtomem_dma1_channel1 on DMA1_Channel1 */
hdma_memtomem_dma1_channel1.Instance = DMA1_Channel1;
hdma_memtomem_dma1_channel1.Init.Direction = DMA_MEMORY_TO_MEMORY;
hdma_memtomem_dma1_channel1.Init.PeriphInc = DMA_PINC_ENABLE;
hdma_memtomem_dma1_channel1.Init.MemInc = DMA_MINC_ENABLE;
hdma_memtomem_dma1_channel1.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_memtomem_dma1_channel1.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_memtomem_dma1_channel1.Init.Mode = DMA_NORMAL;
hdma_memtomem_dma1_channel1.Init.Priority = DMA_PRIORITY_LOW;
if (HAL_DMA_Init(&hdma_memtomem_dma1_channel1) != HAL_OK)
{
Error_Handler();
}
/* 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);
}
中断的概念以后也会出一篇文章来仔细讲解。中断的配置如下
串口的代码配置也是如下:
/* 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(uartHandle,hdmarx,hdma_usart1_rx);
而_HAL_LINKDMA()我们可以发现他是宏定义实现的(这部分有点难和复杂,有兴趣的伙伴们可以再深入学习一下)
#define __HAL_LINKDMA(__HANDLE__, __PPP_DMA_FIELD__, __DMA_HANDLE__) \
do{ \
(__HANDLE__)->__PPP_DMA_FIELD__ = &(__DMA_HANDLE__); \
(__DMA_HANDLE__).Parent = (__HANDLE__); \
} while(0U)
我们再来验证一下串口的句柄下是否有hdmarx这一个成员
typedef struct __UART_HandleTypeDef
{
USART_TypeDef *Instance; /*!< UART registers base address */
UART_InitTypeDef Init; /*!< UART communication parameters */
const uint8_t *pTxBuffPtr; /*!< Pointer to UART Tx transfer Buffer */
uint16_t TxXferSize; /*!< UART Tx Transfer size */
__IO uint16_t TxXferCount; /*!< UART Tx Transfer Counter */
uint8_t *pRxBuffPtr; /*!< Pointer to UART Rx transfer Buffer */
uint16_t RxXferSize; /*!< UART Rx Transfer size */
__IO uint16_t RxXferCount; /*!< UART Rx Transfer Counter */
__IO HAL_UART_RxTypeTypeDef ReceptionType; /*!< Type of ongoing reception */
__IO HAL_UART_RxEventTypeTypeDef RxEventType; /*!< Type of Rx Event */
DMA_HandleTypeDef *hdmatx; /*!< UART Tx DMA Handle parameters */
DMA_HandleTypeDef *hdmarx; /*!< UART Rx DMA Handle parameters */
HAL_LockTypeDef Lock; /*!< Locking object */
__IO HAL_UART_StateTypeDef gState; /*!< UART state information related to global Handle management
and also related to Tx operations.
This parameter can be a value of @ref HAL_UART_StateTypeDef */
__IO HAL_UART_StateTypeDef RxState; /*!< UART state information related to Rx operations.
This parameter can be a value of @ref HAL_UART_StateTypeDef */
__IO uint32_t ErrorCode; /*!< UART Error code */
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
void (* TxHalfCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Tx Half Complete Callback */
void (* TxCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Tx Complete Callback */
void (* RxHalfCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Rx Half Complete Callback */
void (* RxCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Rx Complete Callback */
void (* ErrorCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Error Callback */
void (* AbortCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Abort Complete Callback */
void (* AbortTransmitCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Abort Transmit Complete Callback */
void (* AbortReceiveCpltCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Abort Receive Complete Callback */
void (* WakeupCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Wakeup Callback */
void (* RxEventCallback)(struct __UART_HandleTypeDef *huart, uint16_t Pos); /*!< UART Reception Event Callback */
void (* MspInitCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Msp Init callback */
void (* MspDeInitCallback)(struct __UART_HandleTypeDef *huart); /*!< UART Msp DeInit callback */
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */
} UART_HandleTypeDef;
结果很明显看到是有的。通过串口的句柄结构体,就可以使用到DMA的功能,因为实际初始化好的DMA句柄已经通过此函数,关联到了串口的句柄结构体中的DMA句柄指针了。
更多的底层设置可以去各个外设的“How to use this driver”仔细阅读。如HAL库的DMA外设的How to use this driver如下,大家想要知道HAL库的底层逻辑和封装,HAL库是如何驱动各个外设的,都要仔细的阅读各个部分的How to use this driver,这样可以提高大家对HAL库的了解和更好的使用HAL库!!
本文就到这里啦,希望大家通过本文可以对DMA有深入的了解,毕竟知道外设原理和代码的实现才能更好的去使用外设,如果有错大家可以指出哟,感兴趣或者有不明白的地方的小伙伴可以私信问我!!如果喜欢可以点赞+关注哟!!!