stm32_DMA
DMA
1. 概念与基本原理
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,提高系统效率和吞吐量。
2. DMA内部结构(可图示)
想象一个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通道之间的优先级。
- 中断使能: 配置是否在传输完成、一半传输完成、传输错误时触发中断。
-
仲裁器: 当有多个DMA通道或CPU同时需要访问总线时,仲裁器负责决定哪个请求获得总线访问权,以避免冲突。
-
总线接口: DMA控制器通过这个接口与系统总线(如AHB/APB总线)连接,从而可以读写内存和外设寄存器。
-
中断逻辑: DMA传输完成、一半传输完成、传输错误等事件发生时,DMA控制器会生成中断请求,通知CPU进行后续处理。
3. DMA工作模式与传输类型(可图示)
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控制器自动停止并可以触发传输完成中断。
- 循环模式: 传输指定数量的数据后,DMA控制器会自动重新加载数据量寄存器并继续传输,形成一个循环。常用于连续采集传感器数据或连续发送波形数据。
-
地址增量模式:
- 源地址增量: 如果源地址是内存地址,通常会配置为增量,以读取连续的数据块。如果源地址是外设寄存器,通常配置为不增量,因为外设数据寄存器地址通常是固定的。
- 目标地址增量: 同理。
-
数据宽度: 指定每次DMA传输的数据单位是8位、16位还是32位。这必须与源和目标数据的实际宽度相匹配。
-
中断类型:
DMA控制器可以根据不同的事件触发中断,方便CPU进行处理,例如:
- 传输完成中断 (TC): 当所有数据传输完毕时触发。
- 半传输完成中断 (HT): 在循环模式下,当一半数据传输完毕时触发,常用于“乒乓操作”,即在一个缓冲器被DMA填充时,CPU可以处理另一个缓冲器的数据。
- 传输错误中断 (TE): 在传输过程中发生错误时触发。
4. DMA操作流程(可图示)
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
+-------------------+
| 结束 |
+-------------------+
流程详解:
- 初始化DMA外设: 使能DMA控制器自身的时钟。
- 配置DMA通道/流: 这是DMA的核心配置步骤,包括选择合适的通道、传输方向、数据宽度、地址增量方式、传输模式以及中断等。
- 关联DMA与外设: 这一步至关重要。DMA传输通常是由外设发起的。例如,ADC完成一次转换后会向DMA控制器发出一个请求信号,告知DMA可以传输数据了。因此,需要在外设的配置中使能DMA请求。
- 设置DMA传输参数: 明确本次DMA传输的数据源地址、目标地址以及总共要传输的数据量。
- 启动DMA传输: 启用DMA通道/流,使其处于准备就绪状态。一旦外设发出DMA请求,DMA传输就会自动开始。
- (可选)等待DMA传输完成/处理中断:
- 轮询: CPU周期性地检查DMA状态寄存器的标志位,判断传输是否完成。这种方式会占用CPU时间。
- 中断: 更高效的方式。当DMA传输完成、一半完成或发生错误时,DMA控制器会触发中断,CPU跳转到相应的中断服务函数进行处理,然后可以继续执行其他任务。
5. 完整流程代码案例 (以STM32为例)
这里以STM32微控制器为例,使用HAL库来实现一个简单的DMA功能:通过DMA将ADC采集到的数据自动传输到SRAM中的一个数组中。
硬件连接:
- ADC: 使用STM32F407的ADC1,连接到PA1引脚(模拟输入)。
- DMA: ADC1通常连接到DMA2的Stream0。
开发环境: Keil MDK, STM32CubeMX (用于生成初始化代码)
5.1 STM32CubeMX配置步骤
- 新建项目并选择芯片。
- 配置ADC1:
- Mode:
Independent Mode
- Scan Conversion Mode:
Disable
(这里只采集一个通道) - Continuous Conversion Mode:
Enable
(连续采集) - Discontinuous Conversion Mode:
Disable
- DMA Continuous Request:
Enable
(关键!使能ADC的DMA请求) - Channel Settings:
- Rank1:
PA1
(Channel 1) - Sampling Time:
3 Cycles
(或其他合适的值)
- Rank1:
- Mode:
- 配置DMA2:
- 在ADC1设置的DMA Settings中,点击
Add
。 - 选择
DMA2 Stream0
(通常ADC1连接到此Stream)。 - Mode:
Normal
或Circular
(这里我们用Circular
模式,实现连续数据采集)。 - Direction:
Peripheral to Memory
(ADC是外设,RAM是内存)。 - Data Width:
Half Word
(ADC是12位,所以选择16位半字传输)。 - Increment Address:
Memory
(目标地址是RAM数组,需要递增)。 - Increment Address:
Peripheral
(源地址是ADC数据寄存器,通常不递增,但在这里为了配置一致性,选择No Increment
更合理,因为ADC数据寄存器地址固定)。 - Priority:
Low
(或Default)。 - Enable Interrupts: 勾选
DMA2 Stream0 global interrupt
(传输完成中断)。
- 在ADC1设置的DMA Settings中,点击
- 时钟配置: 保持默认即可,确保ADC和DMA的时钟都被使能。
- 生成代码。
5.2 Keil MDK代码实现
在STM32CubeMX生成的基础代码上,我们主要修改 main.c
文件和实现DMA中断服务函数。
/* USER CODE BEGIN Includes */
#include "main.h"
#include <stdio.h> // 用于串口打印,可选
/* 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 */
5.3 代码解释
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)
:- 这是启动DMA传输的关键函数。它告诉HAL库:
&hadc1
: 使用哪个ADC(以及关联的DMA通道)。(uint32_t*)adc_values
: DMA的目标地址,即数据将传输到adc_values
数组。注意这里需要强制类型转换为uint32_t*
,因为HAL库的设计中,pData
参数是uint32_t*
,但实际传输的是uint16_t
。ADC_BUFFER_SIZE
: 传输的数据量。
- 一旦此函数被调用,并且ADC配置为连续转换模式和DMA请求使能,每当ADC完成一次转换,它就会自动触发DMA将数据从ADC数据寄存器 (
ADC1->DR
) 传输到adc_values
数组中。
- 这是启动DMA传输的关键函数。它告诉HAL库:
HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
:- 这是一个用户自定义的ADC转换完成回调函数。当DMA传输了指定数量的数据(
ADC_BUFFER_SIZE
)后,DMA会触发一个传输完成中断,进而由HAL库的DMA中断处理函数 (HAL_DMA_IRQHandler
) 调用这个回调函数。 - 在这个回调函数中,我们将
adc_dma_transfer_complete
标志设置为1,通知主循环有新的数据可用。
- 这是一个用户自定义的ADC转换完成回调函数。当DMA传输了指定数量的数据(
DMA2_Stream0_IRQHandler(void)
:- 这是DMA2 Stream0的中断服务函数。
- 它由Cortex-M处理器的中断向量表在DMA中断发生时调用。
HAL_DMA_IRQHandler(&hdma_adc1);
:这个函数是HAL库提供的通用DMA中断处理程序。它会检查DMA的状态标志,并根据配置调用相应的回调函数(例如本例中的HAL_ADC_ConvCpltCallback
)。