Bootstrap

STM32-CubeMX学习使用记录8-DMA的使用

目录

 1.DMA的介绍

DMA的传输方式:

DMA传输参数

DMA资源

DMA的主要特征

DMA框图

DMA1控制器

DMA2控制器

DMA寄存器:

DMA寄存器配置流程:

2.DMA在CubeMX中的配置和在外设中的使用:

实验1:串口使用DMA发送1次数据

使用DMA模仿printf方式打印数据:

实验2:串口使用DMA一直发送数据

实验3:串口使用DMA触发发送完成的中断

实验4:串口使用DMA进行接收数据:

实验5:串口接收使用DMA触发半完成中断和全完成中断


 1.DMA的介绍

DMA用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU的干预,通过DMA数据可以快速地移动。这就节省了CPU的资源来做其他操作。

DMA的传输方式:

外设到内存、内存到外设、内存到内存、外设到外设

DMA传输参数

1 数据的源地址

2 数据传输位置的目标地址

3 传递数据多少的数据传输量

4 进行多少次传输的传输模式

DMA资源

对于大容量的STM32芯片有2个DMA控制器 两个DMA控制器,DMA1有7个通道,DMA2有5个通道。每个通道都可以配置一些外设的地址

DMA的主要特征

每个通道都直接连接专用的硬件DMA请求,每个通道都同样支持软件触发。这些功能通过软件来配置;

  1. 在同一个DMA模块上,多个请求间的优先权可以通过软件编程设置(共有四级:很高、高、中等和低),优先权设置相等时由硬件决定(请求0优先于请求1,依此类推);
  2. 独立数据源和目标数据区的传输宽度(字节、半字、全字),模拟打包和拆包的过程。源和目标地址必须按数据传输宽度对齐;
  3. 支持循环的缓冲器管理;
  4. 每个通道都有3个事件标志(DMA半传输、DMA传输完成和DMA传输出错),这3个事件标志逻辑或成为一个单独的中断请求;
  5. 存储器和存储器间的传输、外设和存储器、存储器和外设之间的传输;
  6. 闪存、SRAM、外设的SRAM、APB1、APB2和AHB外设均可作为访问的源和目标;
  7. 可编程的数据传输数目:最大为65535

DMA框图

DMA1控制器

DMA2控制器

DMA寄存器:

DMA中断状态寄存器(DMA_ISR):我们如果开启了 DMA_ISR 中这些中断,在达到条件后就会跳到中断服务函数里面去。注意此寄存器为只读寄存器,所以在这些位被置位之后,只 能通过其他的操作来清除

DMA中断标志清除寄存器(DMA_IFCR)
DMA_IFCR 的各位就是用来清除 DMA_ISR 的对应位的,通过写 0 清除。在 DMA_ISR 被置位后, 我们必须通过向该位寄存器对应的位写入 0 来清除

DMA通道x配置寄存器(DMA_CCRx)(x = 1…7)

该寄存器控制着 DMA 的很多相关 信息,包括数据宽度、外设及存储器的宽度、通道优先级、增量模式、传输方向、中断允许、 使能等都是通过该寄存器来设置的。所以 DMA_CCRx 是 DMA 传输的核心控制寄存器

DMA通道x传输数量寄存器(DMA_CNDTRx)(x = 1…7)

这个寄存器控制 DMA 通道 x 的每次 传输所要传输的数据量。其设置范围为 0~65535。并且该寄存器的值会随着传输的进行而减少, 当该寄存器的值为 0 的时候就代表此次数据传输已经全部发送完成了。所以可以通过这个寄存 器的值来知道当前 DMA 传输的进度

DMA通道x外设地址寄存器(DMA_CPARx)(x = 1…7)

该寄存器用来存储 STM32 外设的地 址,比如我们使用串口 1,那么该寄存器必须写入 0x40013804(其实就是&USART1_DR)。如果使 用其他外设,就修改成相应外设的地址就行了

DMA通道x存储器地址寄存器(DMA_CMARx)(x = 1…7)

该寄存器和 DMA_CPARx 差不多, 但是是用来放存储器的地址的。比如我们使用 SendBuf[5200]数组来做存储器,那么我们在 DMA_CMARx 中写入&SendBuff 就可以了

DMA寄存器配置流程:

  1. 在DMA_CPARx寄存器中设置外设寄存器的地址。发生外设数据传输请求时,这个地址将 是数据传输的源或目标。
  2. 在DMA_CMARx寄存器中设置数据存储器的地址。发生外设数据传输请求时,传输的数 据将从这个地址读出或写入这个地址。
  3. 在DMA_CNDTRx寄存器中设置要传输的数据量。在每个数据传输后,这个数值递减。
  4. 在DMA_CCRx寄存器的PL[1:0]位中设置通道的优先级。
  5. 在DMA_CCRx寄存器中设置数据传输的方向、循环模式、外设和存储器的增量模式、外 设和存储器的数据宽度、传输一半产生中断或传输完成产生中断。
  6. 设置DMA_CCRx寄存器的ENABLE位,启动该通道

一旦启动了DMA通道,它既可响应连到该通道上的外设的DMA请求。 当传输一半的数据后,半传输标志(HTIF)被置1,当设置了允许半传输中断位(HTIE)时,将产生 一个中断请求。在数据传输结束后,传输完成标志(TCIF)被置1,当设置了允许传输完成中断位 (TCIE)时,将产生一个中断请求

以上内容来自:

【STM32】 DMA原理,步骤超细详解,一文看懂DMA-CSDN博客


2.DMA在CubeMX中的配置和在外设中的使用:

因为HAL库中一些常用外设已经集成好了DMA模块,所以在使用时只需要调用DMA函数,外设的DMA即可实现传输。

以使用usart1为例,cubemx库的配置说明:这里我使用的是STM32F407VGT6,其他芯片步骤类似

在NVIC中可以配置是否开启DMA中断。

HAL库源代码可以利用AI帮助查看。

实验1:串口使用DMA发送1次数据

cubemx配置:就是上面图片的配置。

内存-->外设,这里的内存选择的是指针自加。因为是normal格式,当内存的数据全部传输完毕后,就会停止传输。结果就是字符串数据全部传输完毕后停止。

在mian函数中加入代码:

/* USER CODE BEGIN 2 */
  /* Configure DMA channel 1 to transfer data from USART1 to memory */
  uint8_t hello[] = "Hello World!\n";
  HAL_UART_Transmit_DMA(&huart1, hello, sizeof(hello)); // 设置发送为正常模式,只会发送一次

  /* USER CODE END 2 */

使用串口调试助手,按下复位键就可以获取一次数据

HAL_UART_Transmit_DMA的参数:

1.串口的句柄

2.数组的首地址

3.数组的大小

查看HAL_UART_Transmit_DMA函数:主要是这个函数

    HAL_DMA_Start_IT(huart->hdmatx, *(const uint32_t *)tmp, (uint32_t)&huart->Instance->DR, Size);

再打开 HAL_DMA_Start_IT函数,主要是下面:

    /*调用DMA_SetConfig函数配置DMA的源地址、目标地址和数据长度*/
    DMA_SetConfig(hdma, SrcAddress, DstAddress, DataLength);

    /*启用传输完成(TC)、传输错误(TE)和直接模式错误(DME)的中断。
    如果用户定义了半传输完成的回调,启用相关中断。*/
    hdma->Instance->CR  |= DMA_IT_TC | DMA_IT_TE | DMA_IT_DME;
    
    if(hdma->XferHalfCpltCallback != NULL)
    {
      hdma->Instance->CR  |= DMA_IT_HT;
    }
    
    /*启动DMA传输*/
    __HAL_DMA_ENABLE(hdma);

这些函数配置好了DMA和DMA的中断,并且使能了DMA。

既然DMA可以发送数据,那么也就可以使用模仿prntf的方法,使用DMA的方式。

使用DMA模仿printf方式打印数据:

来自:STM32CUBEMX配置教程(九)STM32串口DMA收发数据_cubemx选择引脚变黄-CSDN博客

代码如下:

#include <stdarg.h>
#include <stdio.h>
unsigned char UartTxBuf[128]; 
void Usart1Printf(const char *format,...)
{
	
	uint16_t len;
	va_list args;	
	va_start(args,format);
	len = vsnprintf((char*)UartTxBuf,sizeof(UartTxBuf),(char*)format,args);
	va_end(args);
	HAL_UART_Transmit_DMA(&huart1, UartTxBuf, len);
}

注意:unsigned char UartTxBuf[128]; 为全局变量。

该代码使用的是可变参函数

代码使用:

/* USER CODE BEGIN 2 */
  /* Configure DMA channel 1 to transfer data from USART1 to memory */
  uint8_t hello[] = "Hello World!\n";
  HAL_UART_Transmit_DMA(&huart1, hello, sizeof(hello)); // 设置发送为正常模式,只会发送一次
  HAL_Delay(50);
  if (huart1.hdmatx->State == HAL_DMA_STATE_READY)
  {
    Usart1Printf("Hello World!\n"); // 发送字符串
  }
  else
  {
    Usart1Printf("DMA channel 1 not ready!\n");
  }
  /* USER CODE END 2 */

要保证上一次的DMA数据传输完之后才可以再次发送数据。

效果:

实验2:串口使用DMA一直发送数据

把TX的DMA改为Circular:

将代码改为:

/* USER CODE BEGIN 2 */
  /* Configure DMA channel 1 to transfer data from USART1 to memory */
  uint8_t hello[] = "Hello World!\n";
  HAL_UART_Transmit_DMA(&huart1, hello, sizeof(hello)); // 设置发送为正常模式,只会发送一次
  /* USER CODE END 2 */

这样,就会不断的发送hello world了,具体流程就是发送完一组数据再重新发送,一直循环往复。

效果:

实验3:串口使用DMA触发发送完成的中断

因为使用HAL_UART_Transmit_DMA函数发送数据,该函数中已经开启了对应的中断(比如半传输完成,传输完成)。打开该函数可以看到:

/* Set the UART DMA transfer complete callback */
    huart->hdmatx->XferCpltCallback = UART_DMATransmitCplt;

    /* Set the UART DMA Half transfer complete callback */
    huart->hdmatx->XferHalfCpltCallback = UART_DMATxHalfCplt;

    /* Set the DMA error callback */
    huart->hdmatx->XferErrorCallback = UART_DMAError;

该函数注册了对应的中断回调函数,打开其中的回调函数可以发现:

static void UART_DMATransmitCplt(DMA_HandleTypeDef *hdma)
{
  UART_HandleTypeDef *huart = (UART_HandleTypeDef *)((DMA_HandleTypeDef *)hdma)->Parent;
  /* DMA Normal mode*/
  if ((hdma->Instance->CR & DMA_SxCR_CIRC) == 0U)
  {
    huart->TxXferCount = 0x00U;

    /* Disable the DMA transfer for transmit request by setting the DMAT bit
       in the UART CR3 register */
    ATOMIC_CLEAR_BIT(huart->Instance->CR3, USART_CR3_DMAT);

    /* Enable the UART Transmit Complete Interrupt */
    ATOMIC_SET_BIT(huart->Instance->CR1, USART_CR1_TCIE);

  }
  /* DMA Circular mode */
  else
  {
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
    /*Call registered Tx complete callback*/
    huart->TxCpltCallback(huart);
#else
    /*Call legacy weak Tx complete callback*/
    HAL_UART_TxCpltCallback(huart);
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */
  }
}

在传输完成数据时,可以发生传输完成中断,该中断就是在 HAL_UART_TxCpltCallback中产生的。

注意:normal模式下没有半传输完成中断。

实现代码:在usart.c中加入这段代码:

/* USER CODE BEGIN 1 */
 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
 {
   static uint8_t flag;
   if (huart->Instance == USART1 && flag == 0)
   {
     flag = 1;
     HAL_UART_Transmit_DMA(&huart1, "完全发送完成", sizeof("完全发送完成"));
   }
 }
/* USER CODE END 1 */

这样就可以在发送完hello world后触发发送完成中断,加入标志位防止不断触发中断。

效果:

实验4:串口使用DMA进行接收数据:

RX的模式配置为循环接收模式:

主要代码:

uint8_t buffer[128];
  HAL_UART_Receive_DMA(&huart1, buffer, sizeof(buffer)); // 设置接收为DMA模式,接收到的数据会自动放入buffer中

运行效果:

发送hhh

开启调试模式接收到数据,但是此时并不会触发中断,因为DMA并没有完成传输任务,要传输完128个字节的数据才会结束。若想不接受满就停止DMA传输,可以通过判断标志位的方式来结束DMA的传输,比如判断buffer接收到的最后一位数据是‘\n’。或者通过空闲中断(长时间接收不到数据触发)来结束当前的DMA传输。

实验5:串口接收使用DMA触发半完成中断和全完成中断

串口接收中断和发送中断一样,在没有自己注册回调函数的情况下,都是使用串口的回调函数。

打开HAL_UART_Receive_DMA函数可以看到注册好的回调函数:

 /* Set the UART DMA transfer complete callback */
  huart->hdmarx->XferCpltCallback = UART_DMAReceiveCplt;

  /* Set the UART DMA Half transfer complete callback */
  huart->hdmarx->XferHalfCpltCallback = UART_DMARxHalfCplt;

  /* Set the DMA error callback */
  huart->hdmarx->XferErrorCallback = UART_DMAError;

打开其中的一个函数:UART_DMAReceiveCplt

代码开头:对RxXferCount清零,表示串口接收数据计数完成。清除错误中断和禁用DMA接收请求,空闲中断,将UART设置为就绪状态。做这些都是为了保证资源清理、状态更新,以确保整个 UART 接收流程在完成一次 DMA 接收后的正确处理和后续衔接。

 UART_HandleTypeDef *huart = (UART_HandleTypeDef *)((DMA_HandleTypeDef *)hdma)->Parent;

  /* DMA Normal mode*/
  if ((hdma->Instance->CR & DMA_SxCR_CIRC) == 0U)
  {
    huart->RxXferCount = 0U;

    /* Disable RXNE, PE and ERR (Frame error, noise error, overrun error) interrupts */
    ATOMIC_CLEAR_BIT(huart->Instance->CR1, USART_CR1_PEIE);
    ATOMIC_CLEAR_BIT(huart->Instance->CR3, USART_CR3_EIE);

    /* Disable the DMA transfer for the receiver request by setting the DMAR bit
       in the UART CR3 register */
    ATOMIC_CLEAR_BIT(huart->Instance->CR3, USART_CR3_DMAR);

    /* At end of Rx process, restore huart->RxState to Ready */
    huart->RxState = HAL_UART_STATE_READY;

    /* If Reception till IDLE event has been selected, Disable IDLE Interrupt */
    if (huart->ReceptionType == HAL_UART_RECEPTION_TOIDLE)
    {
      ATOMIC_CLEAR_BIT(huart->Instance->CR1, USART_CR1_IDLEIE);
    }
  }

主要是这一段:

/* Check current reception Mode :
     If Reception till IDLE event has been selected : use Rx Event callback */
  if (huart->ReceptionType == HAL_UART_RECEPTION_TOIDLE)
  {
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
    /*Call registered Rx Event callback*/
    huart->RxEventCallback(huart, huart->RxXferSize);
#else
    /*Call legacy weak Rx Event callback*/
    HAL_UARTEx_RxEventCallback(huart, huart->RxXferSize);
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */
  }
  else
  {
    /* In other cases : use Rx Complete callback */
#if (USE_HAL_UART_REGISTER_CALLBACKS == 1)
    /*Call registered Rx complete callback*/
    huart->RxCpltCallback(huart);
#else
    /*Call legacy weak Rx complete callback*/
    HAL_UART_RxCpltCallback(huart);
#endif /* USE_HAL_UART_REGISTER_CALLBACKS */
  }

如果有自己注册的中断回调函数,则使用用户注册的回调函数,否则使用HAL_UARTEx_RxEventCallback或者HAL_UART_RxCpltCallback函数,这两个回调函数一个是在空闲截止时触发,一个是接受完固定数量的数据时触发。

cubemx配置:和上面的实验4是一样的

代码:在usart.c中的用户区添加:

/*串口DMA接收数据完成中断*/
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart->Instance == USART1)
  {
    /* code */
    HAL_UART_Transmit_DMA(&huart1, "全完成中断", sizeof("全完成中断")); // 将接收到的数据发送回去
  }
}
/*串口DMA接收数据半帧中断*/
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
  if (huart->Instance == USART1)
  {
    HAL_UART_Transmit_DMA(&huart1, "半完成中断", sizeof("半完成中断")); // 将接收到的数据发送回去
  }
}

在main中添加:

 uint8_t buffer[10];
  HAL_UART_Receive_DMA(&huart1, buffer, sizeof(buffer)); // 设置接收为DMA模式,接收到的数据会自动放入buffer中

效果就是发送一个hello就会触发一次半完成中断,

效果:

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;