DMA 直接内存访问 控制器 STM32F4xx 系列 外设 内存 数据传输 CPU 资源 AHB 主总线 FIFO 流 通道 仲裁器 优先级 传输模式 外设到内存 内存到外设 内存到内存 双缓冲 配置 数据宽度 突发传输 循环模式 指针增量 错误管理 流控制器 配置流程 应用指南 嵌入式开发
一、DMA核心概念与价值
在嵌入式系统开发中,CPU的资源是宝贵的。当外设(如ADC、SPI、UART)需要与内存频繁交换大量数据时,如果每次传输都让CPU参与“搬运”,会严重消耗CPU的计算能力,导致系统响应变慢或无法处理其他关键任务。
直接内存访问(DMA)技术正是为了解决这一瓶颈而生。STM32F4系列的DMA控制器就像一个高度智能的“数据搬运工”,它能够在无需CPU干预的情况下,独立完成在外设与内存之间、或者在内存不同区域之间的高速数据传输。这极大地解放了CPU,使其可以专注于算法、逻辑控制等核心任务,从而提升整个系统的效率和性能。
本指南将带你从零开始,透彻理解STM32F4 DMA的工作原理,并掌握其配置与应用方法。
二、STM32F4 DMA控制器架构总览
STM32F4系列微控制器通常配备两个DMA控制器:DMA1 和 DMA2。每个控制器拥有相同但独立的结构。
-
核心组成:
-
流(Stream):每个DMA控制器有8个流(Stream 0-7)。流是数据传输的执行单位,你可以把它理解为一个独立的“传输通道”。
-
通道(Channel):每个流可以关联到最多8个可能的通道请求之一。通道决定了这个流服务于哪个具体的外设(例如,ADC1、SPI1_TX、USART1_RX)。通过软件配置选择。
-
仲裁器(Arbiter):负责管理多个流同时发出请求时的优先级冲突。
-
FIFO(先进先出缓冲区):每个流都有一个4字(16字节)深度的内部FIFO。它用于暂存数据,是实现灵活数据传输(如数据宽度转换、突发传输)的关键。
-
AHB总线接口:包含一个用于访问内存的AHB主端口、一个用于访问外设的AHB主端口,以及一个用于CPU配置DMA控制器自身的AHB从端口。
-
-
一个重要区别:
-
只有 DMA2 控制器的外设端口连接到了总线矩阵,因此 仅DMA2支持内存到内存的传输。DMA1的外设端口仅能访问外设数据寄存器。
-
三、DMA传输的三种基本模式
在配置一个DMA流时,你首先需要确定传输方向。
| 传输模式 | 数据流向 | 典型应用场景 | 可用控制器 |
|---|---|---|---|
| 外设到内存 | 从外设数据寄存器到内存数组 | ADC采集数据存入RAM, UART接收数据 | DMA1, DMA2 |
| 内存到外设 | 从内存数组到外设数据寄存器 | 从RAM发送数据到DAC, UART发送数据 | DMA1, DMA2 |
| 内存到内存 | 从一个内存区域到另一个内存区域 | 大数据块复制、快速数据搬移 | 仅DMA2 |
四、零基础配置DMA流:分步详解
以下步骤是配置和使用任何一个DMA流的通用流程。我们以“使用DMA2的Stream5,将ADC1转换的数据搬运到数组adc_buffer[]中”为例。
步骤1:前期准备与检查
-
确认你的外设(本例中是ADC1)支持DMA,并查找其对应的DMA请求映射表(参考芯片参考手册表43,44),确定应该使用哪个DMA控制器、哪个流、哪个通道。例如,ADC1对应 DMA2, Stream0/Stream4, Channel 0。
-
在代码中使能DMA控制器的时钟:
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE);
步骤2:禁用目标流
在对一个流进行任何关键配置之前,必须确保它处于禁用状态。
c
DMA_Cmd(DMA2_Stream0, DISABLE); // 先禁用流 while (DMA_GetCmdStatus(DMA2_Stream0) != DISABLE); // 等待确认流已停止
警告:在禁用连接到DMA流的外设之前,必须先执行此步骤禁用DMA流。
步骤3:清除中断标志
清除该流可能置起的所有旧状态标志,避免误触发中断。
c
DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_FEIF0 | DMA_IT_DMEIF0 | DMA_IT_TEIF0 | DMA_IT_HTIF0 | DMA_IT_TCIF0);
步骤4:配置流的基本参数
通过DMA_InitStructure结构体进行配置,这是最核心的一步。
c
DMA_InitTypeDef DMA_InitStructure; DMA_StructInit(&DMA_InitStructure); // 初始化为默认值 DMA_InitStructure.DMA_Channel = DMA_Channel_0; // 选择通道0 (对应ADC1) DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&(ADC1->DR); // 外设地址:ADC数据寄存器 DMA_InitStructure.DMA_Memory0BaseAddr = (uint32_t)adc_buffer; // 内存地址:目标数组 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralToMemory; // 传输方向:外设到内存 DMA_InitStructure.DMA_BufferSize = 1024; // 传输数据项数量:1024个 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址固定 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 内存地址递增 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 外设数据宽度:16位(ADC分辨率) DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 内存数据宽度:16位 DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 模式:循环模式(持续采集) DMA_InitStructure.DMA_Priority = DMA_Priority_High; // 流优先级:高 DMA_InitStructure.DMA_FIFOMode = DMA_FIFOMode_Enable; // 启用FIFO模式(推荐) DMA_InitStructure.DMA_FIFOThreshold = DMA_FIFOThreshold_HalfFull; // FIFO阈值:半满 DMA_InitStructure.DMA_MemoryBurst = DMA_MemoryBurst_Single; // 内存突发:单次传输 DMA_InitStructure.DMA_PeripheralBurst = DMA_PeripheralBurst_Single; // 外设突发:单次传输 DMA_Init(DMA2_Stream0, &DMA_InitStructure);
关键参数解读:
-
DMA_BufferSize:定义要传输的数据项数量。数据项的大小由DMA_PeripheralDataSize或DMA_MemoryDataSize决定。例如,两者均为HalfWord(2字节),则BufferSize=1024表示传输1024 * 2 = 2048字节。 -
DMA_PeripheralInc/MemoryInc:决定每次传输后地址是否自动增加。外设寄存器地址通常固定,内存地址通常递增。 -
DMA_Mode:-
DMA_Mode_Normal:普通模式。传输完指定数量数据后,DMA自动停止。 -
DMA_Mode_Circular:循环模式。传输完成后,NDTR寄存器自动重载初始值,地址指针复位(取决于是否增量),然后重新开始传输。适用于连续数据流(如音频、实时采集)。
-
-
DMA_FIFOMode:-
启用:允许使用FIFO,支持数据打包/解包(源和目标宽度不同),支持突发传输。
-
禁用:直接模式。每次请求立即传输,但要求源和目标数据宽度必须一致。
-
步骤5:配置中断(可选)
如果需要在数据传输一半或完成时得到通知,需配置NVIC。
c
DMA_ITConfig(DMA2_Stream0, DMA_IT_TC, ENABLE); // 使能传输完成中断 // DMA_ITConfig(DMA2_Stream0, DMA_IT_HT, ENABLE); // 可选:使能半传输中断 NVIC_InitStructure.NVIC_IRQChannel = DMA2_Stream0_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure);
步骤6:使能DMA流
配置完成后,启动DMA流,使其开始等待外设的请求。
c
DMA_Cmd(DMA2_Stream0, ENABLE);
步骤7:配置并启动外设
最后,去配置你的外设(本例是ADC1),使其在需要传输数据时向DMA发出请求。
c
ADC_DMACmd(ADC1, ENABLE); // 使能ADC1的DMA请求 ADC_SoftwareStartConv(ADC1); // 开始ADC转换(假设已配置好ADC)
现在,ADC每转换完一个数据,就会触发DMA请求,DMA控制器会自动将ADC1->DR中的数据搬运到adc_buffer数组中,并自动递增数组下标。当搬运完1024个数据后,由于我们设置了循环模式,DMA会重新开始,覆盖或继续填充数组(取决于应用逻辑)。
五、高级特性与实战技巧
-
双缓冲模式
-
原理:使用两个内存缓冲区(M0AR和M1AR)。当DMA向缓冲区A写入时,CPU可以处理缓冲区B中的数据,实现“乒乓操作”,有效避免数据处理和采集的冲突。
-
配置:设置
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular,并设置DMA_InitStructure.DMA_MemoryBurst = DMA_Memory1BaseAddr,然后使能DMA_DoubleBufferModeCmd(STREAMx, ENABLE)。通过DMA_GetCurrentMemoryTarget()判断当前DMA正在操作哪个缓冲区。
-
-
数据打包与解包
-
场景:当源和目标数据宽度不一致时(例如,从8位的外设接收数据存到32位的内存中),DMA的FIFO可以自动进行打包/解包。
-
条件:必须启用FIFO模式。
DMA_PeripheralDataSize和DMA_MemoryDataSize可设置为不同值。NDTR的值需要是宽度比值的整数倍(例如,外设8位,内存32位,NDTR必须是4的倍数)。
-
-
突发传输
-
目的:提高总线利用效率。通过一次请求传输一个数据块(4、8、16拍),而不是一个数据项。
-
配置:设置
DMA_PeripheralBurst和DMA_MemoryBurst。必须启用FIFO模式,且FIFO阈值大小必须能容纳整个突发传输的数据量(见表49)。例如,内存突发为4拍,内存数据宽度为字(4字节),则一次突发传输16字节。FIFO容量为16字节,因此阈值可以设为1/4(4字节)、半满(8字节)或全满(16字节),但不能设为3/4(12字节)。
-
-
流控制器与外设流控制
-
DMA作为流控制器(默认):传输总量由
DMA_BufferSize决定。 -
外设作为流控制器:设置
DMA_InitStructure.DMA_PFCTRL = DMA_PFCTRL_PERIPH。此时传输总数未知,由外设(如SDIO)通过硬件信号通知DMA传输结束。NDTR寄存器在启动后会被强制设为0xFFFF,并在传输中递减。
-
六、错误诊断与调试
DMA传输失败时,应检查以下状态标志(在DMA_LISR或DMA_HISR寄存器中):
-
TEIF (Transfer Error):传输错误。通常由于访问了非法地址(如未初始化的指针)或双缓冲模式下错误地写入了内存地址寄存器。
-
FEIF (FIFO Error):FIFO错误。可能因FIFO上溢/下溢,或初始配置中FIFO阈值与突发大小不匹配导致。
-
DMEIF (Direct Mode Error):直接模式错误。在直接模式下发生访问冲突。
调试建议:
-
始终在初始化前禁用流并等待其确认禁用。
-
仔细检查外设和内存的地址是否正确,是否已对齐(例如字传输时地址需4字节对齐)。
-
确认
DMA_BufferSize、数据宽度、地址增量模式的组合符合逻辑。 -
使用传输完成中断(TCIF)或半传输中断(HTIF)作为流程监控点。
-
对于复杂配置(如突发+双缓冲),先用简单配置(单次、直接模式)验证通路正确性。
通过本指南的系统学习,你应该已经掌握了STM32F4 DMA控制器的核心原理和配置方法。DMA是提升STM32系统性能的利器,熟练掌握它将使你设计的嵌入式系统更加高效和稳定。请结合具体的外设(如UART、SPI、ADC)参考手册和示例代码进行实践,以巩固理解。
2329

被折叠的 条评论
为什么被折叠?



