我估计大多数人学这么久连听说都没听说过DMA,更不用提知道它是干嘛的。其实DMA的本质就是一个数据的搬运工。平常的时候当我们没有配置的时候,一直都是CPU在搬运数据,但是这个活又累又没有技术含量,所以DMA的重要性还是有的。
目录
1.DMA的数据搬运:
1.DMA数据路径
简单来说,只有三种:
内存--内存 DMA_MEMORY_TO_MEMORY
外设--内存 DMA_PERIPH_TO_MEMORY
内存--外设 DMA_MEMORY_TO_PERIPH
这里的外设其实是指的STM32板子上的外设,比如GPIO口啊,串口啊等等。而所谓内存,从代码上来看其实就是一个变量。
2.DMA搬运模式
这里可配置的东西有:
优先级设定;指针是否递增;搬运模式是否循环;通道的选择。其实都没有什么难度,只不过看起来花里胡哨的,我依次讲解。
优先级的设定:
DMA的优先级采用的是硬件+软件,什么意思呢?其实本质是 通道号+软件优先级。在使用过程中,我们往往同时配置多个通道,当两个通道内的数据同时到达时,优先看软件优先级,再看通道号。注意:通道号越小优先级越。
通道:
DMA的通道其实和定时器的有点像,DMA通过通道连接各种外设,从而实现数据传输。在配置中,外设往往也配置了对应的DMA句柄来接应。所以这里对于通道只需要知道要按照表格对应来选择就行。
注意:通常情况下芯片都只有DMA1没有DMA2.
搬运是否循环:
DMA_Mode_Normal(正常模式):一次DMA数据传输完后,停止DMA传送 ,也就是只传输一次 DMA_Mode_Circular(循环传输模式) :当传输结束时,硬件自动会将传输数据量寄存器进行重装,进行下一轮的数据传输。 也就是多次传输。注意:当循环结束后假设给内存设定为了指针递增,此时指针会重置回到缓存最开始指向的地方。(假设缓存为arr[4]传输完成时指向arr[3],循环结束后指针会自动回归为arr[0])
指针是否递增:
数据源和目标缓存都有一个对应的指针来指向它从而使得数据知道存哪,在配置中也是分别配置是否递增的。不用想那么复杂,就一句话:数据来自或者要去内存则为递增,外设则不递增。
解释:因为外设来源的数据往往放在xx寄存器里,数据一直是更新覆盖的;所以不用递增。内存就不一样了,内存往往是数组或者一个xxbuf,不递增的话就覆盖掉了。
另外还有一项配置叫数据对齐模式,其实都一般配置为:DMA_PDATAALIGN_BYTE
2.DMA的框架
3.DMA函数与配置
函数
__HAL_RCC_DMA1_CLK_ENABLE(…) 使能DMA时钟的
HAL_DMA_Init(…) 跟TIM的INIT用法一样
HAL_DMA_Start(…) 搬运函数。
__HAL_LINKDMA(…) 连接内存到外设数据通道的。
__HAL_DMA_GET_FLAG(…) 获取DMA寄存器标志位的
配置:
基本的配置的步骤为:
DMA时钟使能:__HAL_RCC_DMA1_CLK_ENABLE();
|
DMA初始化(通道选择;优先级;指针递增;数据对齐;搬运模式:HAL_DMA_Init()
|
DMA搬运:如果是和外设进行交互,那么这一步会变化,在代码中详解。
|
查询DMA数据是否传输正常完成
内存-内存配置:
DMA_HandleTypeDef hdma_handle = {0};
void DMA_INIT(){
__HAL_RCC_DMA1_CLK_ENABLE();
//内存配置
hdma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_handle.Init.MemInc = DMA_MINC_ENABLE;
//外设配置(目标存储配置)
hdma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_handle.Init.PeriphInc = DMA_PINC_ENABLE;
//模式;优先级;转运方向配置
hdma_handle.Init.Mode = DMA_NORMAL;
hdma_handle.Init.Priority = DMA_PRIORITY_MEDIUM;
hdma_handle.Init.Direction = DMA_MEMORY_TO_MEMORY;
hdma_handle.Instance = DMA1_Channel1;
HAL_DMA_Init(&hdma_handle);
}
void DMA_Transport(){
HAL_DMA_Start(&hdma_handle,(uint32_t)Sroce_buf,(uint32_t)Target_buf,sizeof(uint32_t) * 16);
while(__HAL_DMA_GET_FLAG(hdma_handle,DMA_FLAG_TC1) == RESET){
int i = 0;
for(i = 0;i<16;i++){
printf("data[%d] = %X \r\n",i,Target_buf[i]);
}
}
}
内存-外设的配置:
#include "dma.h"
extern UART_HandleTypeDef uart1_handle;
DMA_HandleTypeDef hdma_handle = {0};
void DMA_INIT(){
__HAL_RCC_DMA1_CLK_ENABLE();
hdma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma_handle.Init.MemInc = DMA_MINC_ENABLE;
hdma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_handle.Init.PeriphInc = DMA_PINC_DISABLE; //外设内存不可递增。
hdma_handle.Init.Mode = DMA_NORMAL;
hdma_handle.Init.Priority = DMA_PRIORITY_MEDIUM;
hdma_handle.Init.Direction = DMA_MEMORY_TO_PERIPH; //内存到外设
hdma_handle.Instance = DMA1_Channel4;
HAL_DMA_Init(&hdma_handle);
//链接函数,这里是吧DMA和外设连接起来,中间的参数是外设句柄中的DMA成员变量
//可以理解为,每一个外设都配有DMA成员变量,为的就是和DMA连接。
__HAL_LINKDMA(&uart1_handle,hdmatx,hdma_handle);
}
在main中:
DMA_INIT();
HAL_UART_Transmit_DMA(&uart1_handle,Send_buf,1024);
这里注意 HAL_UART_Transmit_DMA(&uart1_handle,Send_buf,1024); 是使用了外设对应的API,利用传输过来的句柄进行传输。
外设-内存(重点)
#include "dma.h"
#include "stdio.h"
#include "uart1.h"
extern UART_HandleTypeDef uart1_handle;
extern uint8_t uart1_rx_buf[UART1_RX_BUF_SIZE];
DMA_HandleTypeDef dma_handle = {0};
void dma_init(void)
{
__HAL_RCC_DMA1_CLK_ENABLE();
dma_handle.Instance = DMA1_Channel5;
dma_handle.Init.Direction = DMA_PERIPH_TO_MEMORY;
dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
dma_handle.Init.MemInc = DMA_MINC_ENABLE;
dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
dma_handle.Init.PeriphInc = DMA_PINC_DISABLE;
dma_handle.Init.Priority = DMA_PRIORITY_MEDIUM;
dma_handle.Init.Mode = DMA_NORMAL;
HAL_DMA_Init(&dma_handle);
__HAL_LINKDMA(&uart1_handle, hdmarx, dma_handle);
HAL_UART_Receive_DMA(&uart1_handle, uart1_rx_buf, UART1_RX_BUF_SIZE);
}
基本配置中有两个重点,一个是extern外面串口的变量,另一个是链接到外设的函数,如果展开的来看的话,LINK函数内部是把该句柄赋值给了串口句柄中的成员变量DMA句柄。
第二个重点是: HAL_UART_Receive_DMA该函数配置的是 串口句柄 目标缓存 传输大小,其中,传输大小最为重要,这里传输大小配置为了缓存区的长度大小。
void USART1_IRQHandler(void)
{
if (__HAL_UART_GET_FLAG(&uart1_handle, UART_FLAG_IDLE) != RESET)
{
__HAL_UART_CLEAR_IDLEFLAG(&uart1_handle);
HAL_UART_DMAStop(&uart1_handle);
uart1_rx_len = UART1_RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(&dma_handle);
printf("recv: %s, recv_len: %d\r\n", uart1_rx_buf, uart1_rx_len);
uart1_rx_clear();
HAL_UART_Receive_DMA(&uart1_handle, uart1_rx_buf, UART1_RX_BUF_SIZE);
}
}
这里代码很短但是非常有机制可谈,其中长度的计算就非常有的说:
首先:DMA的传输长度被配置为了缓存器的长度
其次:DMA在传输过程中当没有传输足够配置的长度但数据源为空时会进入等待
然后:这里配置了HAL_UART_DMAStop函数使得DMA停止传输并保持了当前状态。
再然后:使用__HAL_DMA_GET_COUNTER会返回当前未传输任务中还未传输的长度
最后:使用缓存大小 - __HAL_DMA_GET_COUNTER的返回值 = 已传输的值。
我用数字再解释一遍:假设配置DMA传输长度为10,此时串口接收到了4,DMA传输进缓存区4的数据,此时数据源为空DMA进入等待;于此同时串口接收完成触发空闲中断,此时调用函数Stop使得DMA停止传输保持当前状态,GET_COUNTER返回的值为6,所以直接10 - 6 = 4也就是接收到的数据长度。
其中,Stop函数是必要的,因为如果长时间没有接收或者新的数据进入都会导致GET返回值变化。