DMA,全称Direct Memory Access,即直接存储器访问。它是微控制器(MCU)、嵌入式处理器中的一个独立硬件模块,用于在无需CPU干预的情况下,在不同内存区域(包括外设寄存器和SRAM、Flash等)之间进行数据传输。
基本原理: 在没有DMA的情况下,CPU负责所有的数据传输。例如,从ADC读取数据,CPU需要逐个读取ADC寄存器并将数据拷贝到RAM;向UART发送数据,CPU需要逐个将数据从RAM拷贝到UART发送寄存器。这种方式会占用大量CPU时间,尤其是在高速数据传输或大量数据传输的场景下,会严重影响CPU处理其他任务的效率。
DMA模块的出现,就是为了解决这个问题。当DMA被配置并启动后,CPU只需告诉DMA控制器需要传输的数据源、数据目标、数据量和传输方向,DMA控制器就会接管数据传输任务。CPU可以自由地执行其他指令,只有当DMA传输完成(或发生错误)时,DMA控制器才会通过中断通知CPU。
核心思想: 解放CPU,提高系统效率和吞吐量。
想象一个DMA控制器的内部结构,它通常包含以下几个关键部分:
+-----------------------------------------------------+
| DMA控制器 |
+-----------------------------------------------------+
| |
| 1. DMA通道/流 (DMA Channel/Stream) | <-- 每个通道处理一个独立的传输任务
| +-----------------------------------------+ |
| | | |
| | 源地址寄存器 (Source Address Register) | | <-- 数据源地址 (如:外设数据寄存器或RAM地址)
| | 目标地址寄存器 (Destination Address Register)| | <-- 数据目标地址 (如:RAM地址或外设数据寄存器)
| | 数据量寄存器 (Number of Data Register) | | <-- 待传输的数据量
| | 控制寄存器 (Control Register) | | <-- 配置传输模式、数据宽度、增量模式等
| +-----------------------------------------+ |
| |
| 2. 仲裁器 (Arbiter) | <-- 解决多个DMA通道同时请求总线访问时的冲突
| |
| 3. 总线接口 (Bus Interface) | <-- 连接到系统总线,进行实际的数据读写
| |
| 4. 中断逻辑 (Interrupt Logic) | <-- 传输完成/错误时生成中断请求
| |
+-----------------------------------------------------+
^ ^
| |
| |
+-------+-----+ +-------+-----+
| 外设总线 | | 内存总线 |
| (Peripherals) | | (SRAM/Flash)|
+---------------+ +---------------+
关键部件解释:
DMA通道/流: 大多数微控制器会提供多个独立的DMA通道或流(Stream),每个通道可以独立地配置和启动一个DMA传输任务。例如,一个通道可以用于ADC到RAM的传输,另一个通道用于RAM到UART的传输。
源地址寄存器 (Source Address Register - SAR/PAR): 存储数据传输的起始源地址。这可以是内存地址,也可以是外设的数据寄存器地址。
目标地址寄存器 (Destination Address Register - DAR/MAR): 存储数据传输的起始目标地址。同样可以是内存地址或外设的数据寄存器地址。
数据量寄存器 (Number of Data Register - NDTR/CR): 存储本次DMA传输需要传输的数据单位数量。每传输一个数据单位,此寄存器会自动减1,直到减为0时传输完成。
控制寄存器 (Control Register - CR/CCR):
这是DMA配置的核心。它包含了以下重要配置:
仲裁器: 当有多个DMA通道或CPU同时需要访问总线时,仲裁器负责决定哪个请求获得总线访问权,以避免冲突。
总线接口: DMA控制器通过这个接口与系统总线(如AHB/APB总线)连接,从而可以读写内存和外设寄存器。
中断逻辑: DMA传输完成、一半传输完成、传输错误等事件发生时,DMA控制器会生成中断请求,通知CPU进行后续处理。
DMA的工作模式和传输类型决定了DMA如何进行数据传输:
+-------------------------------------------------------+
| DMA工作模式/传输类型 |
+-------------------------------------------------------+
| |
| 1. 传输方向 (Transfer Direction) |
| +-----------------------------------------+ |
| | | |
| | 外设到内存 (Peripheral to Memory) | |
| | - e.g., ADC数据采集到RAM | |
| | | |
| | 内存到外设 (Memory to Peripheral) | |
| | - e.g., RAM数据发送到UART | |
| | | |
| | 内存到内存 (Memory to Memory) | |
| | - e.g., 快速拷贝内存块 | |
| +-----------------------------------------+ |
| |
| 2. 传输模式 (Transfer Mode) |
| +-----------------------------------------+ |
| | | |
| | 普通模式 (Normal Mode) | |
| | - 完成一次指定数量的传输后停止 | |
| | | |
| | 循环模式 (Circular Mode) | |
| | - 传输完成后自动重置数据量寄存器 | |
| | - 持续循环传输,无需CPU干预 | |
| | - 适用于连续数据流采集或输出 | |
| +-----------------------------------------+ |
| |
| 3. 地址增量模式 (Address Increment Mode) |
| +-----------------------------------------+ |
| | | |
| | 源地址增量 (Peripheral Increment / Memory Increment) |
| | - 传输后源地址是否递增 | |
| | | |
| | 目标地址增量 (Memory Increment / Peripheral Increment) |
| | - 传输后目标地址是否递增 | |
| | | |
| | 不增量 (No Increment) | |
| | - 地址保持不变,用于读写固定寄存器 | |
| +-----------------------------------------+ |
| |
| 4. 数据宽度 (Data Width) |
| +-----------------------------------------+ |
| | | |
| | 字节 (Byte - 8-bit) | |
| | 半字 (Half-Word - 16-bit) | |
| | 字 (Word - 32-bit) | |
| +-----------------------------------------+ |
| |
| 5. 中断类型 (Interrupt Type) |
| +-----------------------------------------+ |
| | | |
| | 传输完成中断 (Transfer Complete - TC) | |
| | 半传输完成中断 (Half Transfer - HT) | |
| | 传输错误中断 (Transfer Error - TE) | |
| | 直接模式错误中断 (Direct Mode Error - DME) |
| +-----------------------------------------+ |
+-------------------------------------------------------+
详细说明:
传输方向: 定义数据从哪里来到哪里去。
传输模式:
地址增量模式:
数据宽度: 指定每次DMA传输的数据单位是8位、16位还是32位。这必须与源和目标数据的实际宽度相匹配。
中断类型:
DMA控制器可以根据不同的事件触发中断,方便CPU进行处理,例如:
DMA的操作通常遵循以下流程:
+-------------------+
| 开始 |
+-------------------+
|
V
+-------------------+
| 1. 初始化DMA外设 |
| - 使能DMA时钟 |
+-------------------+
|
V
+-------------------+
| 2. 配置DMA通道/流 |
| - 选择DMA通道/流号 |
| - 配置传输方向 |
| - 配置数据宽度 |
| - 配置地址增量模式 |
| - 配置传输模式(普通/循环)|
| - 配置优先级 |
| - 配置中断使能(可选)|
+-------------------+
|
V
+-------------------+
| 3. 关联DMA与外设 |
| - 例如:配置ADC的DMA请求使能位 |
| - 例如:配置UART的DMA发送/接收使能位 |
+-------------------+
|
V
+-------------------+
| 4. 设置DMA传输参数 |
| - 设置源地址 |
| - 设置目标地址 |
| - 设置传输数据量 |
+-------------------+
|
V
+-------------------+
| 5. 启动DMA传输 |
| - 使能DMA通道/流 |
+-------------------+
|
V
+-------------------+
| 6. (可选)等待DMA传输完成/处理中断 |
| - 轮询DMA状态标志 |
| - 或,等待DMA中断并执行中断服务函数 |
+-------------------+
|
V
+-------------------+
| 结束 |
+-------------------+
流程详解:
这里以STM32微控制器为例,使用HAL库来实现一个简单的DMA功能:通过DMA将ADC采集到的数据自动传输到SRAM中的一个数组中。
硬件连接:
开发环境: Keil MDK, STM32CubeMX (用于生成初始化代码)
Independent Mode
Disable
(这里只采集一个通道)Enable
(连续采集)Disable
Enable
(关键!使能ADC的DMA请求)PA1
(Channel 1)3 Cycles
(或其他合适的值)Add
。DMA2 Stream0
(通常ADC1连接到此Stream)。Normal
或 Circular
(这里我们用Circular
模式,实现连续数据采集)。Peripheral to Memory
(ADC是外设,RAM是内存)。Half Word
(ADC是12位,所以选择16位半字传输)。Memory
(目标地址是RAM数组,需要递增)。Peripheral
(源地址是ADC数据寄存器,通常不递增,但在这里为了配置一致性,选择No Increment
更合理,因为ADC数据寄存器地址固定)。Low
(或Default)。DMA2 Stream0 global interrupt
(传输完成中断)。在STM32CubeMX生成的基础代码上,我们主要修改 main.c
文件和实现DMA中断服务函数。
/* USER CODE BEGIN Includes */
#include "main.h"
#include // 用于串口打印,可选
/* USER CODE END Includes */
/* Private variables ---------------------------------------------------------*/
ADC_HandleTypeDef hadc1; // ADC句柄
DMA_HandleTypeDef hdma_adc1; // DMA句柄
/* USER CODE BEGIN PV */
/* Private variables ---------------------------------------------------------*/
#define ADC_BUFFER_SIZE 10 // 定义ADC数据缓冲区大小
uint16_t adc_values[ADC_BUFFER_SIZE]; // 存储ADC采集数据的数组
volatile uint8_t adc_dma_transfer_complete = 0; // DMA传输完成标志
/* USER CODE END PV */
/* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_DMA_Init(void);
static void MX_ADC1_Init(void);
/* USER CODE BEGIN PFP */
/* Private function prototypes -----------------------------------------------*/
/* USER CODE END PFP */
/* USER CODE BEGIN 0 */
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)
{
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
/* MCU Configuration--------------------------------------------------------*/
/* Reset of all peripherals, Initializes the Flash interface and the Systick. */
HAL_Init();
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Configure the system clock */
SystemClock_Config();
/* USER CODE BEGIN SysInit */
/* USER CODE END SysInit */
/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_DMA_Init(); // DMA初始化必须在ADC初始化之前,因为ADC初始化会用到DMA句柄
MX_ADC1_Init();
/* USER CODE BEGIN 2 */
// 启动ADC的DMA传输
// HAL_ADC_Start_DMA(hadc, pData, Length)
// hadc: ADC句柄
// pData: 目标数据缓冲区地址
// Length: 传输数据量
if (HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_values, ADC_BUFFER_SIZE) != HAL_OK)
{
/* Start Error */
Error_Handler();
}
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
if (adc_dma_transfer_complete)
{
// DMA传输完成,可以处理采集到的数据了
// 注意:在循环模式下,这个标志会持续被设置,因为它每次完成一个完整的缓冲区传输就会触发
// 这里只是简单打印,实际应用中可以进行数据处理、滤波等
printf("ADC Values: ");
for (int i = 0; i < ADC_BUFFER_SIZE; i++)
{
printf("%d ", adc_values[i]);
}
printf("\r\n");
adc_dma_transfer_complete = 0; // 清除标志
}
HAL_Delay(500); // 主循环可以做其他事情,等待DMA中断
}
/* USER CODE END 3 */
}
/**
* @brief System Clock Configuration
* @retval None
*/
void SystemClock_Config(void)
{
/* ... (CubeMX生成的时钟配置代码) ... */
}
/**
* @brief ADC1 Initialization Function
* @param None
* @retval None
*/
static void MX_ADC1_Init(void)
{
/* ... (CubeMX生成的ADC1初始化代码) ... */
// 注意,CubeMX会在这个函数中关联DMA句柄:
// hadc1.DMA_Handle = &hdma_adc1;
// hdma_adc1.Parent = &hadc1;
}
/**
* @brief DMA Initialization Function
* @param None
* @retval None
*/
static void MX_DMA_Init(void)
{
/* USER CODE BEGIN DMA_Init_First */
/* USER CODE END DMA_Init_First */
/* DMA controller clock enable */
__HAL_RCC_DMA2_CLK_ENABLE(); // 这一行是CubeMX生成的,使能DMA2时钟
/* DMA interrupt init */
/* DMA2_Stream0_IRQn interrupt configuration */
HAL_NVIC_SetPriority(DMA2_Stream0_IRQn, 0, 0); // 设置DMA中断优先级
HAL_NVIC_EnableIRQ(DMA2_Stream0_IRQn); // 使能DMA中断
/* USER CODE BEGIN DMA_Init_Last */
/* USER CODE END DMA_Init_Last */
}
/**
* @brief GPIO Initialization Function
* @param None
* @retval None
*/
static void MX_GPIO_Init(void)
{
/* ... (CubeMX生成的GPIO初始化代码) ... */
}
/* USER CODE BEGIN 4 */
// DMA传输完成中断回调函数
// 这个函数会在HAL_DMA_IRQHandler中被调用
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
/* Prevent unused argument(s) compilation warning */
UNUSED(hadc); // 避免编译器警告
/* NOTE : This function Should not be modified, when the callback is needed,
the HAL_ADC_ConvCpltCallback could be implemented in the user file.
*/
adc_dma_transfer_complete = 1; // 设置DMA传输完成标志
}
/* USER CODE END 4 */
/**
* @brief This function is executed in case of error occurrence.
* @retval None
*/
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
while(1)
{
}
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
/**
* @brief Reports the name of the source file and the source line number
* where the assert_param error has occurred.
* @param file: pointer to the source file name
* @param line: assert_param error line source number
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* USER CODE BEGIN 6 */
/* User can add his own implementation to report the file name and line number,
tex: printf("Wrong parameters value: file %s on line %d\r\n", file, line) */
/* USER CODE END 6 */
}
#endif /* USE_FULL_ASSERT */
/************************ (C) COPYRIGHT STMicroelectronics *****END OF FILE****/
中断服务函数 (在 stm32f4xx_it.c
中)
CubeMX会自动生成DMA中断服务函数的框架,你需要在 DMA2_Stream0_IRQHandler
中调用HAL库的DMA处理函数。
/* USER CODE BEGIN Includes */
/* USER CODE END Includes */
/* External variables --------------------------------------------------------*/
extern DMA_HandleTypeDef hdma_adc1; // 声明在main.c中定义的DMA句柄
/* USER CODE BEGIN EV */
/* USER CODE END EV */
/******************************************************************************/
/* Cortex-M4 Processor Interruption and Exception Handlers */
/******************************************************************************/
/**
* @brief This function handles DMA2 Stream0 global interrupt.
*/
void DMA2_Stream0_IRQHandler(void)
{
/* USER CODE BEGIN DMA2_Stream0_IRQn 0 */
/* USER CODE END DMA2_Stream0_IRQn 0 */
HAL_DMA_IRQHandler(&hdma_adc1); // 调用HAL库的DMA中断处理函数
/* USER CODE BEGIN DMA2_Stream0_IRQn 1 */
/* USER CODE END DMA2_Stream0_IRQn 1 */
}
/* USER CODE BEGIN 1 */
/* USER CODE END 1 */
ADC_HandleTypeDef hadc1; DMA_HandleTypeDef hdma_adc1;
: 定义了ADC和DMA的HAL库句柄,用于操作对应的外设。uint16_t adc_values[ADC_BUFFER_SIZE];
: 定义了一个数组,用于存储DMA从ADC传输过来的12位(实际存储在16位uint16_t中)数据。volatile uint8_t adc_dma_transfer_complete = 0;
: 一个标志位,用于在中断中通知主循环DMA传输完成。volatile
关键字很重要,因为它告诉编译器这个变量的值可能在程序执行流程之外(例如中断)被改变。MX_DMA_Init();
: STM32CubeMX生成的DMA初始化函数,负责使能DMA时钟,配置DMA通道/流的各项参数(方向、数据宽度、地址增量、模式、优先级),并使能中断。MX_ADC1_Init();
: STM32CubeMX生成的ADC初始化函数,其中会配置ADC的通道、采样时间,并且最重要的是,会关联DMA句柄到ADC句柄 (hadc1.DMA_Handle = &hdma_adc1;
)。if (HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_values, ADC_BUFFER_SIZE) != HAL_OK)
:
&hadc1
: 使用哪个ADC(以及关联的DMA通道)。(uint32_t*)adc_values
: DMA的目标地址,即数据将传输到adc_values
数组。注意这里需要强制类型转换为uint32_t*
,因为HAL库的设计中,pData
参数是uint32_t*
,但实际传输的是uint16_t
。ADC_BUFFER_SIZE
: 传输的数据量。ADC1->DR
) 传输到adc_values
数组中。HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
:
ADC_BUFFER_SIZE
)后,DMA会触发一个传输完成中断,进而由HAL库的DMA中断处理函数 (HAL_DMA_IRQHandler
) 调用这个回调函数。adc_dma_transfer_complete
标志设置为1,通知主循环有新的数据可用。DMA2_Stream0_IRQHandler(void)
:
HAL_DMA_IRQHandler(&hdma_adc1);
:这个函数是HAL库提供的通用DMA中断处理程序。它会检查DMA的状态标志,并根据配置调用相应的回调函数(例如本例中的HAL_ADC_ConvCpltCallback
)。