ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32 DMA存储器到存储器传输实战:从原理到代码配置详解

2026/8/4 19:04:12 拓冰建站 浏览量
STM32 DMA存储器到存储器传输实战:从原理到代码配置详解 最近在嵌入式开发中经常需要处理大量数据在内存与外设如ADC、DAC、UART之间的搬运。如果全靠CPU来“手动”搬运不仅效率低下还会严重占用CPU资源导致系统响应变慢。这时DMA直接存储器访问技术就成了解放CPU、提升系统性能的关键。本文将围绕STM32的DMA控制器以“19 DMA 19DMA-04”这一典型应用或学习模块为切入点手把手带你从零理解DMA原理并完成一个完整的实战项目使用DMA实现存储器到存储器的数据高效传输。无论你是刚接触STM32的新手还是想深化DMA应用的开发者这篇从概念到代码、从配置到排错的系统教程都能让你直接复用。1. DMA核心概念为什么它是嵌入式开发的“效率神器”在深入代码之前我们必须搞清楚DMA到底是什么以及它为何如此重要。DMADirect Memory Access中文译为直接存储器访问。它是一种允许特定硬件子系统外设直接读写系统内存而无需中央处理器CPU介入的技术。你可以把CPU想象成公司的总经理把数据搬运想象成搬运货物。没有DMA时总经理CPU需要亲自去仓库内存取货再搬到卡车外设上整个过程他什么别的活都干不了。有了DMA后总经理只需要雇佣一个专业的搬运队DMA控制器告诉搬运队“从A仓库搬100箱货到B卡车”然后就可以去处理其他重要会议执行核心算法、响应中断等了。搬运队独立完成工作完成后通知一下总经理即可。DMA解决的核心问题解放CPU将CPU从繁琐的、耗时的数据搬运工作中解脱出来专注于计算、逻辑控制和紧急事件响应。提高数据吞吐率DMA控制器通常针对数据块传输进行优化传输速度远高于CPU通过加载/存储指令的单个操作。实现高实时性在音频流、图像采集、高速通信等场景中数据必须按时到达DMA能确保数据流的连续性避免因CPU处理其他任务而导致的数据丢失。在STM32中的常见应用场景外设到存储器ADC采集数据直接存入数组、UART接收大量数据到缓冲区。存储器到外设从数组发送数据到DAC生成波形、通过UART发送大块数据。存储器到存储器内存内部的数据块复制、快速填充或搬移数据。这正是我们本文实战的重点19DMA-04通常指向此类实验。相关重要概念通道ChannelDMA控制器有多个通道每个通道服务于一个或多个特定的外设如ADC1、USART1_TX。对于存储器到存储器的传输可以选用未被外设占用的通道。仲裁器Arbiter当多个通道同时请求DMA传输时仲裁器根据优先级软件可配置决定哪个通道先被服务。传输计数器NDTR指定要传输的数据项数量。每完成一次传输计数器减1。外设/存储器地址PAR/MAR分别指向数据传输的源地址和目的地址。在存储器和存储器模式下这两个地址都指向内存空间。数据宽度PSIZE/MSIZE可以配置为字节8位、半字16位或字32位需与地址对齐要求匹配以提高效率。理解了这些我们就知道配置DMA本质上是“雇佣并指挥搬运队”的过程告诉它从哪里搬源地址、搬到哪里去目的地址、搬多少数据量、怎么搬数据宽度、是否循环等。2. 环境准备与硬件平台说明在开始编程前我们需要明确开发环境。不同的STM32系列如F1、F4、H7其DMA控制器如DMA1 DMA2和库函数标准库、HAL库、LL库略有差异但核心思想和配置流程高度相似。本文实战环境MCU型号STM32F103C8T6Blue Pill开发板性价比高资料丰富。其DMA控制器为DMA1拥有7个通道。开发环境Keil MDK-ARM (uVision5) 或 STM32CubeIDE。编程库STM32标准外设库Standard Peripheral Library。虽然ST主推HAL/LL库但标准库代码更贴近寄存器易于理解原理。理解了标准库迁移到HAL库将非常轻松。核心功能使用DMA1的某个通道例如通道1实现将一段内存区域数组srcBuffer的数据搬运到另一段内存区域数组dstBuffer。项目工程结构预览 在开始前你的工程应该包含以下关键文件Your_Project/ ├── CMSIS/ Cortex微控制器软件接口标准文件由Keil或CubeIDE自动添加 ├── FWlib/ STM32标准外设库文件如stm32f10x_dma.c/.h stm32f10x_rcc.c/.h等 ├── User/ │ ├── main.c 主程序包含DMA配置和测试逻辑 │ ├── stm32f10x_conf.h 外设库配置文件 │ └── ...其他用户文件 └── ...工程配置文件确保在stm32f10x_conf.h中已经取消了#define USE_FULL_ASSERT的注释用于调试并确保包含了DMA和外设时钟的头文件。3. DMA传输模式与关键配置结构体详解STM32标准库通过初始化结构体来配置DMA。我们以DMA_InitTypeDef为核心拆解每一个关键成员。// 文件路径User/main.c (部分代码) #include stm32f10x.h #include stm32f10x_dma.h #include stm32f10x_rcc.h #include stdio.h // 用于printf调试需重定向串口 #define BUFFER_SIZE 32 uint32_t srcBuffer[BUFFER_SIZE]; uint32_t dstBuffer[BUFFER_SIZE];DMA_InitTypeDef 结构体成员精讲DMA_InitTypeDef DMA_InitStructure;DMA_PeripheralBaseAddr (外设基地址)作用数据传输的源地址或目的地址之一取决于数据传输方向。在存储器到存储器模式下它作为源地址。配置设置为源数组的首地址例如(uint32_t)srcBuffer。注意地址必须是32位对齐的因为我们将数据宽度设为字。DMA_MemoryBaseAddr (存储器基地址)作用数据传输的另一个地址。在存储器到存储器模式下它作为目的地址。配置设置为目的数组的首地址例如(uint32_t)dstBuffer。DMA_DIR (传输方向)作用定义数据流的方向。取值DMA_DIR_PeripheralDST从存储器到外设。此时Peripheral是目的DMA_DIR_PeripheralSRC从外设到存储器。此时Peripheral是源对于存储器到存储器模式ST标准库中该模式是通过使能DMA_CCR寄存器中的MEM2MEM位单独控制的DMA_DIR通常设置为DMA_DIR_PeripheralSRC将Peripheral端视为源Memory端视为目的但关键在于开启MEM2MEM。更清晰的逻辑是源是DMA_PeripheralBaseAddr目的是DMA_MemoryBaseAddr。DMA_BufferSize (缓冲区大小)作用指定要传输的数据项数量。即传输计数器NDTR的初始值。配置例如BUFFER_SIZE。传输完成后该值会被硬件自动递减为0。DMA_PeripheralInc (外设地址递增)作用每次传输后外设地址是否自动递增。在存储器到存储器模式下“外设”端就是我们的源内存地址。取值DMA_PeripheralInc_Enable或DMA_PeripheralInc_Disable。配置如果要从一个连续的数组搬运数据必须设置为Enable否则每次都会从同一个地址读取数据。DMA_MemoryInc (存储器地址递增)作用每次传输后存储器地址是否自动递增。在存储器到存储器模式下“存储器”端就是我们的目的内存地址。配置同样为了将数据顺序存入目的数组必须设置为Enable。DMA_PeripheralDataSize (外设数据宽度)DMA_MemoryDataSize (存储器数据宽度)作用定义单次传输的数据单元大小字节、半字、字。两者必须保持一致。取值DMA_PeripheralDataSize_Byte/ HalfWord/ Word。配置根据数组的数据类型决定。我们的srcBuffer和dstBuffer是uint32_t数组所以设置为DMA_PeripheralDataSize_Word和DMA_MemoryDataSize_Word即32位/4字节。这能最大化总线利用效率。DMA_Mode (模式)作用选择DMA通道是工作在单次模式还是循环模式。取值DMA_Mode_Normal单次模式。传输完指定数量的数据后DMA通道自动停止需要软件重新使能才能再次传输。DMA_Mode_Circular循环模式。传输完成后硬件自动重装传输计数器NDTR和地址寄存器立即开始新一轮传输。适用于需要持续数据流的场景如ADC连续采样。本文示例使用Normal模式。DMA_Priority (优先级)作用当多个DMA通道同时请求时仲裁器根据此优先级决定服务顺序。取值DMA_Priority_VeryHigh/ High/ Medium/ Low。配置在存储器到存储器传输中通常设为DMA_Priority_High因为这种传输通常希望尽快完成释放总线。DMA_M2M (存储器到存储器模式)作用这是启用存储器到存储器传输的关键位取值DMA_M2M_Enable或DMA_M2M_Disable。配置必须设置为DMA_M2M_Enable。当此位使能时DMA传输将由软件触发通过DMA_Cmd使能通道而不是由外设硬件请求触发。4. 完整实战DMA存储器到存储器数据传输下面我们一步步构建一个完整的、可验证的DMA传输示例。4.1 初始化数据缓冲区首先我们准备源数据和清零目的缓冲区以便验证。// 文件路径User/main.c (续) int main(void) { // 1. 初始化缓冲区 for(int i 0; i BUFFER_SIZE; i) { srcBuffer[i] i * 2; // 源数据0, 2, 4, 6, ... dstBuffer[i] 0; // 目的数据清零 } // ... 后续代码 }4.2 配置系统时钟与DMA时钟DMA作为AHB总线上的外设其时钟由RCC复位和时钟控制模块管理。必须使能对应的时钟。// 2. 开启DMA1时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);为什么在STM32中任何外设在使用前都必须开启其时钟这是为了降低功耗。DMA1的时钟在AHB总线上。4.3 详细配置DMA初始化结构体根据第3部分的讲解我们填充结构体。// 3. 配置DMA初始化结构体 DMA_InitTypeDef DMA_InitStructure; DMA_DeInit(DMA1_Channel1); // 将DMA1通道1寄存器重置为默认值 DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)srcBuffer; // 源地址 DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)dstBuffer; // 目的地址 DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; // 传输方向外设为源 DMA_InitStructure.DMA_BufferSize BUFFER_SIZE; // 传输数据项数量 DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Enable; // 源地址递增 DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; // 目的地址递增 DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Word; // 数据宽度字 DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Word; DMA_InitStructure.DMA_Mode DMA_Mode_Normal; // 单次模式 DMA_InitStructure.DMA_Priority DMA_Priority_High; // 通道优先级高 DMA_InitStructure.DMA_M2M DMA_M2M_Enable; // 使能存储器到存储器模式 DMA_Init(DMA1_Channel1, DMA_InitStructure); // 将配置写入DMA1通道1的寄存器4.4 启动DMA传输并等待完成配置完成后使能通道即可启动传输。由于是软件触发M2M模式无需等待外部事件。// 4. 使能DMA通道启动传输 DMA_Cmd(DMA1_Channel1, ENABLE); // 5. 等待传输完成轮询方式 while(DMA_GetFlagStatus(DMA1_FLAG_TC1) RESET) { // 传输未完成在此等待。在实际应用中可以在此处执行其他低优先级任务。 // DMA1_FLAG_TC1 是 DMA1 通道1传输完成标志位。 } // 传输完成清除标志位 DMA_ClearFlag(DMA1_FLAG_TC1);轮询 vs 中断这里使用了最简单的轮询方式等待传输完成标志。在真实项目中如果传输数据量大或CPU需要及时响应其他事件应该使用DMA传输完成中断。在中断服务函数中处理完成后的逻辑如设置标志、通知任务等这样CPU在DMA工作时是完全自由的。4.5 验证传输结果最后我们检查目的数组的数据是否与源数组一致。// 6. 验证传输结果 uint8_t transferSuccess 1; for(int i 0; i BUFFER_SIZE; i) { if(dstBuffer[i] ! srcBuffer[i]) { transferSuccess 0; break; } } if(transferSuccess) { // 可以通过串口打印成功信息或者点亮LED // printf(DMA Memory-to-Memory Transfer Succeeded!\n); GPIO_SetBits(GPIOC, GPIO_Pin_13); // 假设LED连接在PC13低电平点亮 } else { // printf(DMA Transfer Failed!\n); // 错误处理... } while(1) { // 主循环 } }为了看到效果你需要初始化一个GPIO如PC13连接了Blue Pill板载LED并在验证成功后控制它。同时如果需要printf需提前重定向串口例如USART1到调试器。完整流程总结准备数据初始化源和目的缓冲区。开时钟使能DMA控制器时钟。配参数填充DMA_InitTypeDef结构体核心是设置源/目的地址、数据量、地址递增、数据宽度并务必使能DMA_M2M。启传输调用DMA_Cmd使能通道。等完成轮询或中断等待传输完成标志。验结果比对数据确认传输正确性。5. 常见问题与深度排查指南在实际操作中你可能会遇到DMA不工作、数据错误等问题。下面是一个系统的排查清单。问题现象可能原因排查步骤与解决方案DMA根本不启动标志位永不置位1. DMA时钟未开启。2.DMA_M2M模式未使能。3. 源/目的地址非法或未对齐。4. 缓冲区大小(BufferSize)为0。1. 检查RCC_AHBPeriphClockCmd(DMA1, ENABLE)是否已调用。2. 确认DMA_InitStructure.DMA_M2M DMA_M2M_Enable。3. 检查地址是否为有效的内存地址如全局数组。对于字传输地址必须是4字节对齐即地址低2位为0。4. 确保BUFFER_SIZE是一个大于0的整数。数据只复制了一部分或目的地址数据错误1. 地址递增配置错误。2. 数据宽度配置错误。3. 传输过程中被更高优先级中断打断且未处理好。1. 确认PeripheralInc和MemoryInc根据需求正确设置连续传输必须Enable。2. 确认PeripheralDataSize和MemoryDataSize一致且与数组数据类型匹配uint8_t用Byteuint16_t用HalfWorduint32_t用Word。3. 检查中断优先级。DMA传输本身不能被中断但CPU访问同一总线资源可能受影响。确保关键代码段或DMA配置过程的原子性。传输完成后源数据被破坏在存储器到存储器模式下DMA读操作不会破坏源数据。如果发生极可能是1. 源和目的地址区域发生重叠且传输模式/方向导致覆盖。2. 程序其他部分修改了源数据。1. 确保源和目的缓冲区是独立的、不重叠的内存区域。2. 使用调试器观察源数组地址在传输前后设置断点查看是否被其他代码修改。使用中断时中断服务函数不执行1. DMA通道的中断未使能。2. NVIC嵌套向量中断控制器未配置。3. 中断服务函数名写错或未实现。4. 中断标志未正确清除。1. 在DMA初始化后调用DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE)使能传输完成中断。2. 配置NVIC设置优先级并使能DMA1通道1中断NVIC_Init(...)。3. 在stm32f10x_it.c中正确定义中断函数void DMA1_Channel1_IRQHandler(void)。4. 在中断函数内检查标志位后必须调用DMA_ClearITPendingBit(DMA1_IT_TC1)清除中断标志。调试时发现DMA传输速度不如预期1. 系统时钟SYSCLK或AHB总线时钟HCLK配置过低。2. 使用了字节或半字传输而总线是32位的未能充分利用带宽。3. 内存访问冲突如同时访问Flash。1. 检查系统时钟配置确保运行在最高允许频率对于F103通常为72MHz。2. 在地址对齐允许的情况下尽量使用字Word传输。3. 对于需要极致速度的场合考虑将缓冲区放在SRAM中默认就是并避免在DMA传输期间进行大量的Flash读取操作。6. 工程最佳实践与进阶思考掌握了基础操作后将这些知识安全、高效地应用到实际项目中还需要遵循一些最佳实践。6.1 内存管理与地址对齐使用全局变量或静态变量确保DMA缓冲区的生命周期足够长避免使用栈上的局部变量函数退出后失效。地址对齐这是性能和安全的关键。对于字传输源和目的地址都应是4字节对齐。编译器通常会对齐全局数组。你可以使用GCC/ARMCC的特定属性如__attribute__((aligned(4)))来显式指定对齐。不对齐的访问在某些架构上会导致硬件错误HardFault。使用volatile关键字对于被DMA和CPU共同访问的缓冲区例如DMA写入CPU读取应使用volatile关键字声明防止编译器进行错误的优化如将读操作缓存到寄存器。volatile uint32_t dma_buffer[1024] __attribute__((aligned(4)));6.2 中断与事件驱动设计避免轮询在生产代码中强烈建议使用DMA完成中断TC、半传输中断HT或传输错误中断TE而不是轮询标志位。这能极大提高CPU效率。中断服务函数ISR要短小精悍在ISR中只做最必要的操作如设置事件标志、释放信号量、递增计数器等。复杂的处理应放到主循环或任务中。合理设置中断优先级DMA中断的优先级应根据业务紧急程度设置。注意如果DMA服务于一个高实时性外设如ADC其完成中断的优先级应高于系统中其他大部分中断。6.3 双缓冲区与循环DMA双缓冲区Ping-Pong Buffer在处理连续数据流如音频时可以设置两个缓冲区。当DMA向缓冲区A填充数据时CPU处理缓冲区B的数据DMA完成后自动切换到缓冲区BCPU则处理缓冲区A。这能实现无缝的数据处理避免丢失。循环DMA模式Circular Mode对于永不停止的数据流配置DMA_Mode_Circular。DMA在传输完指定数量数据后会自动重载计数器和地址重新开始传输。结合双缓冲区和半传输中断HT、传输完成中断TC可以高效地管理数据。6.4 安全与健壮性传输前校验参数在启动DMA前检查缓冲区大小是否为正地址是否有效非NULL。处理传输错误使能DMA传输错误中断DMA_IT_TE并在中断中记录错误、复位DMA通道进行错误恢复。防止缓冲区溢出确保BufferSize不大于实际分配的缓冲区大小。临界区保护如果DMA的配置如改变目标地址可能被中断或其他任务打断应在操作前后使用关中断或互斥锁进行保护。6.5 从标准库迁移到HAL库如果你使用STM32CubeMX和HAL库概念完全一致但API不同。核心步骤在CubeMX图形界面中启用DMA通道并配置参数模式、优先级、数据宽度等。生成的代码会初始化DMA句柄hdma_xxx。使用HAL_DMA_Start()或HAL_DMA_Start_IT()中断方式启动传输。传输完成回调函数是HAL_DMA_XferCpltCallback()。HAL库封装程度更高但原理相通。理解标准库的寄存器级操作能让你在使用HAL库时更从容地排查底层问题。通过以上从理论到实践从配置到排错再到最佳实践的系统学习你应该已经能够独立在STM32项目中驾驭DMA特别是存储器到存储器传输这一强大功能。它不仅是优化性能的工具更是构建高效、实时嵌入式系统的基石。