STM32中的DMA
DMA介绍
什么是DMA?
DMA(Direct Memory Access,直接存储器访问)提供在外设与内存、存储器和存储器之间的高速数据传输使用。它允许不同速度的硬件装置来沟通,而不需要依赖于CPU,在这个时间中,CPU对于内存的工作来说就无法使用。
简单描述: 就是一个数据搬运工!
DMA的意义
代替 CPU 搬运数据,为 CPU 减负。
1.数据搬运的工作比较耗时间;
2. 数据搬运工作时效要求高(有数据来就要搬走);
3. 没啥技术含量(CPU 节约出来的时间可以处理更重要的事)。
搬运数据的方式
有三种方式:存储器到存储器、存储器到外设、外设到存储器
- 存储器→存储器(例如:复制某特别大的数据buf)
- 存储器→外设 (例如:将某数据buf写入串口TDR寄存器)
- 外设→存储器 (例如:将串口RDR寄存器写入某数据buf)。
这里的外设指的是spi、usart、iic、adc 等基于APB1 、APB2或AHB时钟的外设,而这里的存储器包括自身的闪存(flash)或者内存(SRAM)以及外设的存储设备都可以作为访问地源或者目的地。
存储器→存储器
存储器→外设
外设→存储器
DMA框图
说明:利用DMA进行外设的数据搬运,首先,外设需向DMA1进项请求,然后,经过DMA的仲裁之后,DMA访问外设的数据进行搬运。
DMA控制器
注意:
一个通道每次只能搬运一个外设的数据!! 如果同时有多个外设的 DMA 请求,则按照优先级进行响应。 STM32F103C8T6 只有 DMA1 !
- DMA1有7个通道:
- DMA2 有 5 个通道:
DMA优先级管理
- 优先级管理采用软件+硬件:
软件: 每个通道的优先级可以在DMA_CCRx寄存器中设置,有4个等级:最高级>高级>中级>低级。
硬件: 如果2个请求,它们的软件优先级相同,则较低编号的通道比较高编号的通道有较高的优先权。比如:如果软件优先级相同,通道2优先于通道4。
DMA传输方式与指针递增模式
- 传输方式
DMA_Mode_Normal (正常模式) | 一次DMA数据传输完后,停止DMA传送 ,也就是只传输一次。 |
DMA_Mode_Circular (循环传输模式) | 当传输结束时,硬件自动会将传输数据量寄存器进行重装,进行下一轮的数据传输。 也就是多次传输模式。 |
- 指针递增模式
外设和存储器指针在每次传输后可以自动向后递增或保持常量。当设置为增量模式时,下一个要传输的地址将是前一个地址加上增量值。
情况1:
DMA数据对齐方式
- 数据宽度大的转移到数据宽度小的时候,低位保留高位截断
DMA寄存器
- DMA中断状态寄存器(DMA_ISR)
- DMA中断标志清除寄存器(DMA_IFCR)
- DMA通道x配置寄存器(DMA_CCRx)(x = 1…7)
- DMA通道x传输数量寄存器(DMA_CNDTRx)(x = 1…7)
16位寄存器,最多可以传输数量65536。
- DMA通道x外设地址寄存器(DMA_CPARx)(x = 1…7)
- DMA通道x存储器地址寄存器(DMA_CMARx)(x = 1…7)
DMA的库函数
在hal.dma.c文件中的一些常用的函数:
打开dma1时钟的函数:
在hal.dma.h文件中的一些常用的宏函数:
在hal_dma_ex.h文件中,获取 传输完成标志位
在hal.def.h文件中还存在所需的下面的函数:
若要读取外设的数据还需要相关串口的函数,如下:
小实验1:DMA内存到内存数据搬运
实验目的
使用DMA将一个大数组的数组搬运到另一个位置。
硬件清单
开发板、ST-Link、USB转TTL
配置流程
文件代码
- dma.c文件代码
#include "dma.h"
#include "stdio.h"#define BUF_SIZE 16uint32_t src_buf[BUF_SIZE] = {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15};uint32_t dst_buf[BUF_SIZE] = {0};/**
* @breif DMA的初始化函数
* @note 打开时钟,配置相关参数
* @param 无
* @retval 无
*/
DMA_HandleTypeDef dma_handle = {0};
void dma_init(void){__HAL_RCC_DMA1_CLK_ENABLE();dma_handle.Instance = DMA1_Channel1;dma_handle.Init.Direction = DMA_MEMORY_TO_MEMORY; /* 搬运数据的方式:内存到内存,外设到内存,内存到外设 *///内存相关的配置dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; /* 源:内存数据对齐模式:一般是8位 */dma_handle.Init.MemInc = DMA_MINC_ENABLE; /* 源:内存数据指针递增的方式 */dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; /* 目标:外设数据对齐模式:一般是8位 */dma_handle.Init.PeriphInc = DMA_PINC_ENABLE; /* 目标:外设数据指针递增的方式 */dma_handle.Init.Mode = DMA_NORMAL; /* 传输的模式:循环,不循环。内存到内存不支持循环模式 */dma_handle.Init.Priority = DMA_PRIORITY_MEDIUM; /* 通道的优先级设置:有四种*/HAL_DMA_Init(&dma_handle);}/**
* @breif 封装一个函数进行数据的转运
* @note 利用DMA_Start函数进行数据的搬运,当搬运完成后查看标志位是否置1,然后进行打印
* @note 在DMA_Start()函数中,数据的长度要写成sizeof(uint32_t)*BUF_SIZE,不能写成BUF_SIZE
* @param 无
* @retval 无
*/
void dma_transmit(void){HAL_DMA_Start(&dma_handle,(uint32_t)src_buf,(uint32_t)dst_buf,sizeof(uint32_t)*BUF_SIZE);while(__HAL_DMA_GET_FLAG(&dma_handle,DMA_FLAG_TC1) == RESET);for(uint16_t i = 0;i < BUF_SIZE;i++){printf("传输的数据是:%d \r\n",dst_buf[i]);}
}
- dma.h文件代码
#ifndef __DMA_H__
#define __DMA_H__
#include "stm32f1xx.h"void dma_init(void);
void dma_transmit(void);#endif
- mian.c文件代码
#include "sys.h"
#include "led.h"
#include "delay.h"
#include "uart1.h"
#include "dma.h"int main(void)
{HAL_Init(); /* 初始化HAL库 */stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */led_init(); /* LED初始化 */uart1_init(115200);printf("hello,world");dma_init();dma_transmit();while(1){ }
}
注意事项:
- 关于数据传输中最后一行出现异常值(如从15突然跳变到1073872904),如下所示:
原因:缓冲区溢出或内存越界
解决方式:将上面的等号去掉,就会正常。
- *****关于dma.c文件中的while循环中的判断条件:==RESET,而不能用!=SET*****
原因:!= SET 还可能代表其他未定义的状态,例如:硬件错误、无效参数,导致条件判断不准确。
因此,要直接使用官方推荐条件,可避免兼容性的问题。
小实验2:内存到外设数据转运
实验目的
使用DMA将一个大数据通过串口1发送
硬件清单
开发板、ST-Link、USB转TTL
配置流程
文件代码
- dma.c文件代码
#include "dma.h"
#include "stdio.h"
extern UART_HandleTypeDef uart1_handle;/**
* @breif DMA的初始化函数
* @note 打开时钟,配置相关参数
* @param 无
* @retval 无
*/
DMA_HandleTypeDef dma_handle = {0};
void dma_init(void){__HAL_RCC_DMA1_CLK_ENABLE();dma_handle.Instance = DMA1_Channel4; /* 查看表格:看所需的DMA通道:通道4, */dma_handle.Init.Direction = DMA_MEMORY_TO_PERIPH; /* 搬运数据的方式:内存到内存,外设到内存,内存到外设 *///内存相关的配置dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; /* 源:内存数据对齐模式:一般是8位 */dma_handle.Init.MemInc = DMA_MINC_ENABLE; /* 源:内存数据指针递增的方式 */dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; /* 目标:外设数据对齐模式:一般是8位 */dma_handle.Init.PeriphInc = DMA_PINC_DISABLE; /* 目标:串口发送寄存器数据指针是不能递增的 */dma_handle.Init.Mode = DMA_NORMAL; /* 传输的模式:循环,不循环。内存到内存不支持循环模式 */dma_handle.Init.Priority = DMA_PRIORITY_MEDIUM; /* 通道的优先级设置:有四种*/HAL_DMA_Init(&dma_handle);__HAL_LINKDMA(&uart1_handle,hdmatx,dma_handle); /* 将内存和外设的地址进行连接,注意:dma句柄前面不用加&*/
}/**
* @breif 封装一个函数进行数据的转运
* @note 利用DMA_Start函数进行数据的搬运,当搬运完成后查看标志位是否置1,然后进行打印
* @note 在DMA_Start()函数中,数据的长度要写成sizeof(uint32_t)*BUF_SIZE,不能写成BUF_SIZE
* @param 无
* @retval 无
*/
dma.h文件代码
#ifndef __DMA_H__
#define __DMA_H__
#include "stm32f1xx.h"void dma_init(void);#endif
- main.c文件代码
#include "sys.h"
#include "led.h"
#include "delay.h"
#include "uart1.h"
#include "dma.h"extern UART_HandleTypeDef uart1_handle;
uint8_t send_buf[1000] = {0};int main(void)
{HAL_Init(); /* 初始化HAL库 */stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */led_init(); /* LED初始化 */uart1_init(115200);
// printf("hello,world");dma_init();int i = 0;for(i = 0; i < 1000;i++){send_buf[i] = 'A';}HAL_UART_Transmit_DMA(&uart1_handle,send_buf,1000);while(1){ }
}
总结:
- 本代码是将内存中的数据转运到串口的发送数据的寄存器中,利用的函数HAL_UART_Transmit_DMA()函数,然后通过串口打印到串口调试助手的界面上。
- 与利用printf函数不同;
- 在将数据由内存转运到内存中时,利用的是HAL_DMA_Start()函数,通过判断传输完成标志位的函数__HAL_DMA_GET_FLAG()函数 ==SET(表明数据传输完成)。,然后利用printf()函数将数据打印出来。
- 在dma.c和main.c文件中用到串口初始化函数的句柄,所以,要注意利用extern声明一下外部变量。
- 实验现象
小实验3:DMA外设到寄存器内存数据搬运
实验目的
使用DMA接收串口的数据
硬件清单
开发板、ST-Link、USB转TTL
函数的意义:声明一个接收缓冲区(数组);这个函数返回接收完数据后剩余的数组长度。
可以用来计算,传输数据的长度:
例:
uart1_rx_len = UART1_RX_BUF_SIZE-__HAL_DMA_GET_COUNTER(&dma_handle);
配置流程
文件代码
- dma.c文件代码
#include "dma.h"
#include "stdio.h"
extern UART_HandleTypeDef uart1_handle;extern uint8_t uart1_rx_buf[UART1_RX_BUF_SIZE]; /* UART1接收缓冲区 *//**
* @breif DMA的初始化函数
* @note 打开时钟,配置相关参数
* @param 无
* @retval 无
*/
DMA_HandleTypeDef dma_handle = {0};
void dma_init(void){__HAL_RCC_DMA1_CLK_ENABLE();dma_handle.Instance = DMA1_Channel5; /* 查看表格:UART1_RX的DMA通道:通道5 */dma_handle.Init.Direction = DMA_PERIPH_TO_MEMORY; /* 搬运数据的方式:内存到内存,外设到内存,内存到外设 *///内存相关的配置dma_handle.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; /* 源:内存数据对齐模式:一般是8位 */dma_handle.Init.MemInc = DMA_MINC_ENABLE; /* 源:内存数据指针递增的方式 */dma_handle.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; /* 目标:外设数据对齐模式:一般是8位 */dma_handle.Init.PeriphInc = DMA_PINC_DISABLE; /* 目标:串口发送寄存器数据指针是不能递增的 */dma_handle.Init.Mode = DMA_NORMAL; /* 传输的模式:循环,不循环。内存到内存不支持循环模式 */dma_handle.Init.Priority = DMA_PRIORITY_MEDIUM; /* 通道的优先级设置:有四种*/HAL_DMA_Init(&dma_handle);__HAL_LINKDMA(&uart1_handle,hdmarx,dma_handle); /* 将内存和外设的地址进行连接,注意:dma句柄前面不用加&*/ HAL_UART_Receive_DMA(&uart1_handle,uart1_rx_buf,UART1_RX_BUF_SIZE); /* 打开串口的DMA数据转运,*/
}
- dma.h文件代码
#ifndef __DMA_H__
#define __DMA_H__
#include "stm32f1xx.h"#define UART1_RX_BUF_SIZE 128
void dma_init(void);#endif
- uart1.c文件代码
- main.c文件代码
#include "sys.h"
#include "led.h"
#include "delay.h"
#include "uart1.h"
#include "dma.h"int main(void)
{HAL_Init(); /* 初始化HAL库 */stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */led_init(); /* LED初始化 */uart1_init(115200);printf("hello,world\r\n");dma_init();while(1){ }
}
注意事项:
- 注意在串口中断函数中书写的DMA转运数据的流程。