STM32 DMA

简介

DMA的概念

​ 直接存储器访问 (DMA) :用于在外设与存储器之间以及存储器与存储器之间进行高速数据传输。DMA传输过程的初始化和启动由CPU完成,传输过程由DMA控制器来执行,无需CPU参与,从而节省CPU资源,提高利用率。

DMA数据传输的四个要素:

  1. 传输源 :DMA数据传输的来源
  2. 传输目标:DMA数据传输的目的
  3. 传输数量:DMA传输数据的数量
  4. 触发信号:启动一次DMA数据传输的动作

​ DMA在很多情况下都能够帮助提高系统的性能,尤其是在处理大量数据传输时。它可以用来传输数据到/从外设,例如ADC、DAC、SPI和UART等,也可以用来进行内存间的数据传输。

DMA传输方式

  • 外设到内存
  • 内存到外设
  • 内存到内存
  • 外设到外设

DMA的循环模式和正常模式

​ BDMA主要有两种模式,一个是Normal正常模式,传输一次后就停止传输;另一种是Circular循环模式,会一直循环的传输下去,即使有DMA中断,传输也是一直在进行的。

这两种模式各有用途。

Normal正常模式

​ 适合用于单次传输,比如存储器到存储器的数据复制粘贴,又比如串口的数据单次发送,下次还需要发送的时候,使能下即可。

Circular循环模式

​ 适合用于需要连续传输的场合,比如定时器触发BDMA实现任意IO的PWM输出。

指针递增模式

源地址指针递增 目标地址指针同步递增

适用于:内存到内存 外设到内存

源地址递增 目标指针不动

适用于 内存到外设 如 串口

HAL库函数介绍

HAL_DMA_Start : 开始DMA传输

1
HAL_StatusTypeDef HAL_DMA_Start(DMA_HandleTypeDef *hdma, uint32_t SrcAddress, uint32_t DstAddress, uint32_t DataLength)

第一个参数: DMA句柄

第二个参数: 源内存地址

第三个参数: 目标内存地址

第四个参数: 传输数据的长度 需要乘以siezof(uint32_t)

__HAL_DMA_GET_FLAG: 判断DMA状态

1
#define __HAL_DMA_GET_FLAG(__HANDLE__, __FLAG__)

参数一: DMA句柄

参数二:要查看的状态

有以下几种状态:

DMA_FLAG_TCIFx:传输完成标志。

DMA_FLAG_TIFx:半传输完成标志。

DMA_FLAG_EIFx:传输错误标志。

DMA_FLAG_DMEIFx:直接模式错误标志。

DMA_FLAG_FEIFx:FIFO错误标志。

这里的X需要换成DMA对应的通道

任务1:内存到内存

​ 任务要求:将数组A的内容 通过DMA搬运到数组B 并在串口打印数组B

配置DMA

选择第二个选项卡 MenToMem (内存到内存)

image-20230109223949403

点击Add

image-20230118225817635

配置模式为 正常模式

使用DMA1 通道0

选择指针的递增模式 同步递增

Data Width 为数据宽度 这里使用word 字

其他 暂时不要管

在 STM32F103 系列没有 Fifo阈值的设置

代码编写:

以下操作仅在H7系列需要操作

F1 F4 系列不需要

attribute((section(".DisplayBuffer"))) 这段话只在H7 需要加

H7 使用的时候可以在DMA.c 文件里 将Filo 关闭

hdma_memtomem_dma1_stream0.Init.FIFOMode = DMA_FIFOMODE_DISABLE;

我使用的是 H7 系列 DMA比较复杂 需要额外设置 如下: 其他系列 F1 F4 不需要做

这是总线访问权限的图

image-20230117191212561
image-20230117191224657

可以看到 DMA1 DMA2 可以访问SRAM1 -SRAM3 的内容

所以需要 指定内存

我使用的是Clion+CubaMX 需要做如下设置

image-20230117191244044

首先找到xxx.Id添加如下代码

image-20230117191256242
1
2
3
4
5
._User_data :
{
. = ALIGN(32);
. = ALIGN(32);
} >RAM_D2

准备工作完成 指定变量存储去见下方

准备原始数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* USER CODE BEGIN PM */
#define BUF_SIZE 1024 //数组大小
/* USER CODE END PM */


/* USER CODE BEGIN PFP */
count uint32_t srcBuf[BUF_SIZE] ={ //原始数据
0x01020304,0x05060708,0x090A0B0C,0x0D0E0F10,
0x11121314,0x15161718,0x191A1B1C,0x1D1E1F20,
0x21222324,0x25262728,0x292A2B2C,0x2D2E2F30,
0x31323334,0x35363738,0x393A3B3C,0x3D3E3F40,
0x41424344,0x44564748,0x494A4B4C,0x4D4E4F50,
0x51525345,0x55565758,0x595A5B5C,0x5D5E5F60,
0x61626364,0x65666768,0x696A6B6C,0x6D6E6F70,
0x71727374,0x75767778,0x797A7B7C,0x7D7E7F80
};
__attribute__((section("._User_data"))) uint32_t desBuf[BUF_SIZE] = {0}; // 目标数组
/* USER CODE END PFP */

发送数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* USER CODE BEGIN 2 */
printf("DMA M to M Test \r\n ");
if (HAL_DMA_Start(&hdma_memtomem_dma1_stream0, (uint32_t) srcBuf, (uint32_t) desBuf, BUF_SIZE) == HAL_OK){ //dma开始搬运数据
printf("OK\r\n"); // 正常串口显示 OK 反之 显示no
}else{
printf("no\r\n");
}
while (__HAL_DMA_GET_FLAG(&hdma_memtomem_dma1_stream0,DMA_FLAG_TCIF0_4) == ENABLE); // 等待发送完毕
for (int i = 0; i < sizeof(srcBuf) / sizeof(srcBuf[0]); ++i){
printf("srcBuf[%d] = %lX\r\n",i,desBuf[i]);

}

/* USER CODE END 2 */

程序结果:

image-20230117192215142

任务2:内存到外设

使用DMA 将内存的数据发送到串口

使用到的库函数

串口DMA方式发送函数:HAL_UART_Transmit_DMA

image-20230117192252949

Cuba MX的设置

image-20230117192413741

打开Usart1 的DMA 所有参数默认即可 这里 Mode 选择单次 只发送一次

最后的效果图 可以看出两种 mode的区别 这里 像是串口类型的只需要发送一次选择Normal 就可以

像adc需要持续采集数据的需要设置为循环 Circular

image-20230118225333796

检测NVIC的 DMA1 的中断是否打开

程序编写

准备数据

1
2
3
4
5
6
7
8
9
10
/* USER CODE BEGIN PM */
#define BUF_SIZE 50 //数组大小
/* USER CODE END PM */

/* USER CODE BEGIN PV */

__attribute__((section("._User_data")))uint8_t SendBuf[BUF_SIZE] = {0}; //因为H7的DMA只能访问 ASRM的数据 需要将发送数据的变量放到ASRM的区域


/* USER CODE END PV */

发送数据

1
2
3
4
5
6
7
8
9
10
11
/* USER CODE BEGIN 2 */

printf("DMA M to P Test \r\n ");
for (int i = 0; i < sizeof(SendBuf) / sizeof(SendBuf[0]); ++i){ //填充数据
SendBuf[i] = 'B';
}


HAL_UART_Transmit_DMA(&huart2,SendBuf,BUF_SIZE);//发送数据

/* USER CODE END 2 */

程序效果

Mode 为单次
image-20230118223101377
Mode 为循环
image-20230118223312438

任务3: 外设到内存

使用到的库函数

串口DMA方式接收函数:HAL_UART_Receive_DMA

image-20230118223447023

**获取未传输数据个数函数:__HAL_DMA_GET_COUNTER**

image-20230118223459503

**关闭DMA数据流:__HAL_DMA_DISABLE**

image-20230118223508600

任务目标

实现不定长数据的收发

任务内容

利用串口调试助手,从PC上发送任意长度的字符到开发板,开发板收到后原样发回到PC。

设计思路

使能IDLE中断

在串口1的中断服务程序USART1_IRQHandler中添加对IDLE中断的判断,该函数位于stm32h7xx_it.c文件;

设置传输模式为普通模式,启动DMA传输。串口一旦接收到数据,则触发DMA操作,将数据存放到用户定义的接收缓冲区;

当一帧数据发送完成后,线路处于IDLE状态,将触发IDLE中断,调用IDLE中断回调函数,设置数据接收完成标志;

主程序检测到接收完成标志置位后,将接收的一帧数据原样发回到PC,并禁能DMA,以触发DMA中断。DMA中断将调用接收中断回 调函数,在回调函数中重新启动DMA传输

CubaMx 的 配置

image-20230118223520009

添加接收的DMA

image-20230118223532827

检查中断 将 串口中断 和 DMA的中断打开

程序编写

准备数据接收区

在main.c下编写

1
2
3
4
5
6
/* USER CODE BEGIN PV */

__attribute__((section("._User_data"))) uint8_t rcvBuf[BUF_SIZE]; // 定义接受数组
uint8_t rcvLen; // 定义接受数据长度
uint8_t Rx_Flag; //接受标志
/* USER CODE END PV */

在 main.h 下编写

1
#define BUF_SIZE 256 //数组大小

在stm32h7xx_it.c 下编写

1
2
3
4
5
/* USER CODE BEGIN PV */
extern __attribute__((section("._User_data"))) uint8_t rcvBuf[BUF_SIZE]; // 定义接受数组
extern uint8_t rcvLen; // 定义接受数据长度
extern uint8_t Rx_Flag; //接受标志
/* USER CODE END PV */

准备接受数据

开启空闲中断 在main.c 文件下

H750 上电就进入空闲中断 就会打印一次 长度为0 的数据 一下两种方法可以解决

while(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE)==RESET)

__HAL_UART_CLEAR_IDLEFLAG(&huart1); __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);

1
2
3
4
5
6
7
8
9
10
 /* USER CODE BEGIN 2 */
memset(rcvBuf,0,BUF_SIZE);
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);//开启IDLE中断

printf("DMA P to M Test \r\n ");
HAL_UART_Receive_DMA(&huart1,rcvBuf,BUF_SIZE);//开启DMA接收
__HAL_UART_CLEAR_IDLEFLAG(&huart1);//防止单片机上电就进入空闲中断
__HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE);//防止单片机上电就进入空闲中断
Rx_Flag = 0;//传输完成 标志置零
/* USER CODE END 2 */

开始数据接受 在stm32h7xx_it.c 下编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
if(__HAL_UART_GET_FLAG(&huart1,UART_FLAG_IDLE) == SET){ //判断IDLE的标志位
__HAL_UART_CLEAR_IDLEFLAG(&huart1); //清除IDLE标志位置
//计算获得了多少数据
uint8_t temp_len = __HAL_DMA_GET_COUNTER(&hdma_usart1_rx); // 获取 剩余的位数
HAL_UART_DMAStop(&huart1); //停止DMA传输 防止干扰
rcvLen = BUF_SIZE - temp_len ; //总位数减去剩余位数 得到接收位数
//HAL_UART_Transmit_DMA(&huart1,rcvBuf,rcvLen); //原样发回串口
Rx_Flag = 1 ;//传输完成标志 置为1 表示接受完毕

}
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */

/* USER CODE END U SART1_IRQn 1 */
}

数据处理

在main.c 编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
if(Rx_Flag == 1 ){
Rx_Flag = 0;//接受标志 置零
printf("data is %s\r\n",rcvBuf);//原样输出串口
memset(rcvBuf,0,BUF_SIZE);//清空接收区
HAL_UART_Receive_DMA(&huart1,rcvBuf,BUF_SIZE);//重新开启串口DMA接收
}
}
/* USER CODE END 3 */

程序效果

image-20230118225031500

参考文献:

https://blog.csdn.net/wallace89/article/details/117001405

(132条消息) STM32H743不能使用DMA的问题_Jaken5213的博客-CSDN博客_stm32h743dma指定地址

(132条消息) STM32_H750串口接收不定长数据(IDLE+DMA)及初始化之后便进入idle中断的解决方法_MY_QuinTA的博客-CSDN博客_h750 串口空闲+dma 一直进接收中断