一、DMA介绍
直接存储器存取(direct memory access,DMA)用来提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。无须CPU干预,数据可以通过DMA快速地移动,这就节省了CPU的资源来做其他操作。
简单描述,DMA充当了一个数据搬运工的角色。
DMA核心作用
- 解放 CPU:传统数据传输需要 CPU 逐条指令搬运(如读取外设数据→写入内存),而 DMA 可独立完成传输,CPU 可同时执行其他任务。
 - 提高效率:DMA 传输速度快,尤其适合大量数据(如音频、图像)的连续传输,避免 CPU 因等待传输而阻塞。
 - 支持多种场景:可实现外设→内存、内存→外设、内存→内存的数据传输。
 
DMA搬运什么数据
存储器、外设
这里的外设指的是spi、usart、iic、adc 等基于APB1 、APB2或AHB时钟的外设,而这里的存储器包括
自身的闪存(flash)或者内存(SRAM)以及外设的存储设备都可以作为访问地源或者目的。
三种搬运方式:
- 存储器→存储器(例如:复制某些特别大的数据buf)
 - 存储器→外设 (例如:将数据buf写入串口TDR寄存器)
 - 外设→存储器 (例如:将串口RDR寄存器写入数据buf)
 
1. DMA框图

2. stm32中DMA
STM32F103中有2个DMA控制器,有12个通道(DMA1有7个通道,DMA2有5个通道),每个通道专门用来管理来自于一个或多个外设对存储器访问的请求。还有一个仲裁器来协调各个DMA请求的优先权。
一个通道每次只能搬运一个外设的数据,如果同时有多个外设的 DMA 请求,则按照优先级进行响应。
要注意的是DMA2只存在于大容量产品和互联型产品中。


3. DMA优先级管理
优先级管理采用软件+硬件:
软件: 每个通道的优先级可以在DMA_CCRx寄存器中设置,有4个等级
最高级>高级>中级>低级
硬件: 如果2个请求,它们的软件优先级相同,则较低编号的通道比较高编号的通道有较高的优先权。
比如:如果软件优先级相同,通道2优先于通道4

二、实验:ROM到RAM
把ROM中的数据通过DMA传输到RAM,然后把数据通过printf发送到串口。
DMA传输不涉及外设,所以通道随便选。我们选DMA1的1通道。
注意DMA没有办法把数据从RAM传输到ROM(flash)。
1. ROM和RAM
STM32 的 ROM 主要用于存储程序代码、常量数据和固化的配置信息,其内容在断电后不会丢失。
- Flash 存储器 :STM32 中最核心的 ROM 类型,用于存储用户编写的程序代码(如
main函数、中断服务程序等)、常量(如const修饰的变量)以及程序运行所需的固定数据。
特点:可多次擦写(但擦写次数有限,通常数万次以上),掉电数据不丢失,访问速度较快。 - 系统存储器(System Memory) :一块特殊的 Flash 区域,由 ST 公司固化了Bootloader 程序(如 ISP 程序),用于通过串口、USB 等方式给单片机烧录程序。用户无法修改此区域内容。
 
STM32 的 RAM 用于存储程序运行时的临时数据,其内容在断电后会丢失,是程序运行的 "临时 workspace"。
- 
SRAM(静态随机存取存储器):STM32 的 RAM 均为 SRAM,无需刷新电路,访问速度快,适合高速数据读写。
特点:掉电数据丢失,读写速度远快于 Flash,容量通常小于 Flash
 
2. 寄存器实现
            
            
              c
              
              
            
          
          #include "dma.h"
void DMA1_init()
{
    // 1. 开启时钟
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;
    // 2. DMA相关配置
    // 2.1 DMA方向 存储器到存储器 ROM===>RAM
    DMA1_Channel1->CCR |= DMA_CCR1_MEM2MEM;
    DMA1_Channel1->CCR &= ~DMA_CCR1_DIR;
    // 2.2 数据宽度 8位
    DMA1_Channel1->CCR &= ~DMA_CCR1_MSIZE;
    DMA1_Channel1->CCR &= ~DMA_CCR1_PSIZE;
    // 2.3 地址自增
    DMA1_Channel1->CCR |= DMA_CCR1_MINC;
    DMA1_Channel1->CCR |= DMA_CCR1_PINC;
    // 2.4 传输完成中断
    DMA1_Channel1->CCR |= DMA_CCR1_TCIE;
    // 2.5 NVIC配置
    NVIC_SetPriorityGrouping(4);
    NVIC_SetPriority(DMA1_Channel1_IRQn, 1);
    NVIC_EnableIRQ(DMA1_Channel1_IRQn);
}
void DMA1_transmitData(uint32_t srcAddr, uint32_t destAddr, uint16_t dataLen)
{
    // 外设地址
    DMA1_Channel1->CPAR = srcAddr;
    // 存储器地址
    DMA1_Channel1->CMAR = destAddr;
    // 传输长度
    DMA1_Channel1->CNDTR = dataLen;
    // 开启通道
    DMA1_Channel1->CCR |= DMA_CCR1_EN;
}
void DMA1_Channel1_IRQHandler(void)
{
    if (DMA1->ISR & DMA_ISR_TCIF1)
    {
        // 清除中断标志位
        DMA1->IFCR |= DMA_IFCR_CTCIF1;
        // 关闭通道
        DMA1_Channel1->CCR &= ~DMA_CCR1_EN;
        is_finished = 1;
    }
}
        
            
            
              c
              
              
            
          
          #include "led.h"
#include "delay.h"
#include "usart.h"
#include "dma.h"
const uint8_t src_data[4] = {10, 20, 30, 40};
uint8_t dest_data[4] = {0};
uint8_t is_finished = 0;
int main(void)
{
	USART1_init();
	DMA1_init();
	printf("hello world!\r\n");
	DMA1_transmitData((uint32_t)src_data, (uint32_t)dest_data, 4);
	while (1)
	{
		if (is_finished)
		{
			is_finished = 0;
			for (uint8_t i = 0; i < 4; i++)
			{
				printf("%d\t", dest_data[i]);
			}
		}
	}
}
        3.HAL库实现
            
            
              c
              
              
            
          
          int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  printf("hello\r\n");
  // 注册回调函数
  HAL_DMA_RegisterCallback(&hdma_memtomem_dma1_channel1, HAL_DMA_XFER_CPLT_CB_ID, dma_compeleteCallback);
  HAL_DMA_Start_IT(&hdma_memtomem_dma1_channel1, (uint32_t)src_data, (uint32_t)dest_data, 4);
  /* USER CODE END 2 */
  while (1)
  {
    if (is_finished)
    {
      is_finished = 0;
      for (uint8_t i = 0; i < 4; i++)
      {
        printf("%d\t", dest_data[i]);
      }
    }
  }
}
void dma_compeleteCallback(DMA_HandleTypeDef *_hdma)
{
  // 清除中断标志位,失能通道
  HAL_DMA_Abort_IT(&hdma_memtomem_dma1_channel1);
  is_finished = 1;
}
        
            
            
              c
              
              
            
          
          HAL_StatusTypeDef HAL_DMA_RegisterCallback(DMA_HandleTypeDef *hdma, HAL_DMA_CallbackIDTypeDef CallbackID, void (*pCallback)(DMA_HandleTypeDef *_hdma))
        - pCallback : 指向用户定义的回调函数的指针。回调函数的原型应为 
void Callback(DMA_HandleTypeDef *_hdma)。 
三、实验:RAM到外设(串口)
把RAM中的数据直接传输到usart1的Tx引脚,然后数据被发送到电脑端。
由于不同的通道对应着不同的外设,查阅手册了解到应该选择DMA1的通道4
1. 寄存器实现
            
            
              c
              
              
            
          
          #include "dma.h"
void DMA1_init()
{
    // 1. 开启时钟
    RCC->AHBENR |= RCC_AHBENR_DMA1EN;
    // 2. DMA相关配置
    // 2.1 DMA方向 存储器到外设 ROM===>USART1
    DMA1_Channel4->CCR |= DMA_CCR4_DIR; // 从存储器读
    // 2.2 数据宽度 8位
    DMA1_Channel4->CCR &= ~DMA_CCR4_MSIZE;
    DMA1_Channel4->CCR &= ~DMA_CCR4_PSIZE;
    // 2.3 地址自增
    // 外设(串口)不自增
    DMA1_Channel4->CCR |= DMA_CCR4_MINC;
    DMA1_Channel4->CCR &= ~DMA_CCR4_PINC;
    // 2.4 传输完成中断
    DMA1_Channel4->CCR |= DMA_CCR4_TCIE;
    // 2.5 NVIC配置
    NVIC_SetPriorityGrouping(4);
    NVIC_SetPriority(DMA1_Channel4_IRQn, 1);
    NVIC_EnableIRQ(DMA1_Channel4_IRQn);
    // 2.6 串口DMA使能发送
    USART1->CR3 |= USART_CR3_DMAT;
}
// 源-存储器地址  目的-外设地址
void DMA1_transmitData(uint32_t srcAddr, uint32_t destAddr, uint16_t dataLen)
{
    // 外设地址
    DMA1_Channel4->CPAR = destAddr;
    // 存储器地址
    DMA1_Channel4->CMAR = srcAddr;
    // 传输长度
    DMA1_Channel4->CNDTR = dataLen;
    // 开启通道
    DMA1_Channel4->CCR |= DMA_CCR4_EN;
}
void DMA1_Channel4_IRQHandler(void)
{
    if (DMA1->ISR & DMA_ISR_TCIF4)
    {
        // 清除中断标志位
        DMA1->IFCR |= DMA_IFCR_CTCIF4;
        // 关闭通道
        DMA1_Channel4->CCR &= ~DMA_CCR4_EN;
    }
}
        
            
            
              c
              
              
            
          
          #include "led.h"
#include "delay.h"
#include "usart.h"
#include "dma.h"
uint8_t src_data[]= {'a','b','c','d'};
int main(void)
{
	USART1_init();
	DMA1_init();
	printf("hello world!\r\n");
	DMA1_transmitData((uint32_t)src_data, (uint32_t)&(USART1->DR), 4);
	while (1)
	{
	}
}
        
值得注意的是实验现象中hello word后面回车换行符号不见了,原因是进行DMA传输时,没有对串口缓冲区进行判空操作,直接将数据刷新进去,导致原来的数据没有发送完就直接覆盖掉。
为处理这个现象,可以在进行DMA传输之前适当延时。
2. HAL库实现
使用STM32CubeMX生成的代码,有关DMA的配置在串口配置文件中。
            
            
              c
              
              
            
          
          uint8_t src_data[] = {'a', 'b', 'c', 'd','e'};
int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_USART1_UART_Init();
  /* USER CODE BEGIN 2 */
  HAL_UART_Transmit_DMA(&huart1,src_data,5);
  /* USER CODE END 2 */
  while (1)
  {
  }
}