最近在嵌入式开发中经常需要处理大量数据在内存与外设如ADC、DAC、UART之间的搬运。如果全靠CPU来“手动”搬运不仅效率低下还会大量占用CPU资源导致系统响应变慢。DMA直接存储器访问技术就是为了解决这个痛点而生的。它就像一个“数据搬运工”能在不打扰CPU的情况下独立完成数据的高速传输。本文将围绕DMA特别是其一种常见的工作模式展开深入讲解其核心原理、配置步骤并通过一个完整的STM32实战案例模拟ADC数据搬运到内存带你从零到一掌握DMA的应用。无论你是刚接触嵌入式的新手还是想深化理解DMA工作机制的开发者都能从本文获得可直接复用到项目中的实用知识。1. DMA 核心概念与工作原理1.1 什么是 DMADMA全称 Direct Memory Access即直接存储器访问。它是一种允许特定硬件子系统外设直接读写系统内存而无需中央处理器CPU介入的技术。你可以把它想象成公司里的一个“专职快递员”。在没有DMA的情况下部门A外设要把一箱文件数据送到部门B内存需要先打电话给总经理CPU总经理停下手中的工作亲自跑来搬这箱文件。这显然效率很低。而有了DMA这个快递员部门A只需要把文件和送货地址告诉快递员快递员就会自己完成搬运总经理可以继续处理更重要的事务。DMA 的核心价值解放 CPU将CPU从繁琐的数据搬运工作中解脱出来使其能专注于计算、逻辑控制等核心任务。提高数据传输效率DMA控制器通常针对大数据块传输进行了优化传输速度远高于CPU通过软件循环搬运。实现高实时性在音频流、图像采集、高速通信等场景下DMA能确保数据按时、按量传输避免因CPU被占用而丢失数据。1.2 DMA 与 CPU 协同工作流程一次典型的DMA传输涉及以下几个角色CPU初始化DMA控制器配置源地址、目标地址、传输数据量等参数然后启动DMA传输之后便可以去执行其他任务。DMA 控制器一个独立的硬件模块负责执行具体的传输任务。它包含配置寄存器、地址计数器、数据计数器等。外设数据的生产者或消费者如ADC模数转换器、SPI、I2C等。它会向DMA控制器发出传输请求。内存数据的临时存储地如SRAM。其基本工作流程如下外设准备就绪例如ADC完成一次转换数据已存入其数据寄存器。外设发出 DMA 请求ADC向DMA控制器发送一个请求信号DMA Request。DMA 控制器申请总线DMA控制器收到请求后向总线仲裁器申请系统总线的使用权。总线仲裁总线仲裁器暂停CPU对总线的访问通常只占用几个时钟周期将总线控制权交给DMA控制器。执行 DMA 传输DMA控制器接管总线直接从ADC数据寄存器源地址读取数据然后写入指定的内存地址目标地址。完成后它会更新内部地址和计数器。释放总线单次传输完成或整个数据块传输完成后DMA控制器释放总线CPU重新获得控制权。传输完成中断当配置的数据量全部传输完毕DMA控制器会产生一个传输完成中断通知CPU“快递已送达”CPU可以进行后续处理如处理这批数据。1.3 常见 DMA 传输模式根据数据流的方向DMA传输主要有三种模式内存到外设例如将内存中准备好的音频数据发送到DAC进行播放。外设到内存例如将ADC采集的传感器数据存入内存数组。内存到内存用于在内存的两个区域之间高速复制或移动大块数据。根据数据传输的发起方式又可分为硬件触发传输由外设的硬件事件如ADC转换完成、UART收到数据自动发起。这是最常用、最高效的方式。软件触发通过软件设置标志位来启动一次DMA传输。2. 环境准备与开发平台说明本文的实战部分将以意法半导体的STM32F103C8T6俗称“蓝桥杯”或“最小系统板”为例进行讲解。选择它是因为其资源丰富、资料众多且DMA控制器具有代表性。开发环境MCUSTM32F103C8T6 (Cortex-M3内核)开发工具STM32CubeIDE (版本不限本文基于V1.13.2)固件库HAL库 (STM32Cube_FW_F1_V1.8.5)调试工具ST-LINK V2 仿真器软件依赖无特殊要求使用CubeIDE内置的CubeMX进行图形化配置。版本说明 本文重点在于阐述DMA的通用配置思路和编程模型。虽然示例基于STM32F1的HAL库但其概念和步骤初始化、配置、启动、中断处理适用于所有带有DMA功能的微控制器如GD32、AT32、ESP32等。不同厂商的库函数名称可能略有差异但核心原理相通。示例项目目标 我们将配置ADC1的通道1进行连续模拟信号采集并利用DMA将转换结果自动、循环地搬运到一个大小为100的uint16_t数组内存中。CPU无需干预此过程只需在需要时去读取这个数组即可获取最新的ADC数据。3. STM32 DMA 控制器与 ADC 配置详解3.1 STM32 DMA 控制器架构简介以STM32F103为例它有两个DMA控制器DMA1和DMA2每个控制器有多个通道Stream/Channel。不同外设被固定分配到特定的DMA通道上。例如ADC1的转换数据通常对应DMA1的通道1。在配置时我们需要根据数据手册或CubeMX的提示为外设选择正确的DMA通道。关键配置参数传输方向外设到内存还是内存到外设外设地址数据从哪里来/到哪里去例如ADC的数据寄存器地址ADC1-DR。内存地址数据搬到哪里/从哪里搬例如我们定义的数组adc_buffer的首地址。数据宽度外设和内存端的数据宽度8位16位32位。必须匹配例如ADC是12位结果放在16位寄存器中所以数据宽度应设为半字16位。传输模式单次模式DMA传输完指定数据量后停止需要软件重新启动。循环模式DMA传输完指定数据量后自动重置地址和计数器重新开始传输。非常适合ADC连续采集、UART持续收发等场景。传输数据量一共要传输多少个“数据项”其单位取决于数据宽度。中断使能是否在传输完成、半传输完成或传输错误时产生中断以通知CPU。3.2 ADC 的 DMA 请求对于STM32的ADC当使能了DMA模式后每次ADC转换完成都会自动产生一个DMA请求。DMA控制器响应这个请求将ADC数据寄存器DR中的值“搬走”。如果ADC配置为连续转换模式那么DMA请求也会连续产生配合DMA的循环模式就能实现“采集-搬运”的全自动流水线。3.3 使用 STM32CubeMX 进行图形化配置STM32CubeMX极大地简化了外设和DMA的初始化过程。以下是关键步骤选择 DMA 通道在Pinout Configuration标签页找到ADC1在DMA Settings选项卡点击Add选择ADC1。CubeMX会自动分配正确的通道如DMA1 Channel1。配置 DMA 参数Direction:Peripheral To Memory(外设到内存)Increment Address: 对于Peripheral端ADC地址固定选Disable。对于Memory端我们的数组地址需要递增以存放多个数据选Enable。Data Width: 根据ADC分辨率选择。对于12位ADC结果放在16位寄存器两边都选Half Word(16位)。Mode:Circular(循环模式)Priority: 根据系统需求选择如Medium。配置 ADCScan Conversion Mode:Enabled(如果使用多通道则启用)Continuous Conversion Mode:Enabled(连续转换)DMA Continuous Requests:Enabled(使能DMA连续请求)End Of Conversion Selection:EOC flag after each conversion(每次转换后产生EOC)在Parameter Settings中配置采样时间等。生成代码配置完成后点击GENERATE CODECubeIDE会自动生成包含DMA和ADC初始化代码的工程。4. 完整实战ADC 连续采集与 DMA 传输下面我们基于CubeMX生成的代码补充核心逻辑完成一个可运行的示例。4.1 创建工程与 CubeMX 配置首先在STM32CubeIDE中创建一个新的STM32项目选择MCU型号STM32F103C8Tx。然后按照上一节3.3的步骤完成ADC1通道1和DMA的图形化配置。记得在System Core-SYS中将Debug选为Serial Wire以便使用ST-LINK调试。配置完成后生成代码并打开工程。4.2 关键代码编写与解析CubeMX已经为我们生成了ADC和DMA的初始化函数MX_ADC1_Init()和MX_DMA_Init()并在main()中调用了它们。我们需要做的是启动ADC的DMA传输并处理数据。步骤一定义存储缓冲区在main.c文件顶部用户代码区0USER CODE BEGIN 0定义全局数组。/* USER CODE BEGIN 0 */ #define ADC_BUFFER_SIZE 100 uint16_t adc_buffer[ADC_BUFFER_SIZE]; // DMA将把ADC数据循环存入此数组 /* USER CODE END 0 */步骤二启动 ADC 的 DMA 传输在main()函数中初始化之后开始主循环之前USER CODE BEGIN 2启动ADC和DMA。/* USER CODE BEGIN 2 */ // 启动ADC的DMA传输将ADC转换结果搬运到adc_buffer // 参数ADC句柄目标内存地址传输数据长度以数据项为单位 if (HAL_ADC_Start_DMA(hadc1, (uint32_t*)adc_buffer, ADC_BUFFER_SIZE) ! HAL_OK) { // 如果启动失败可以在这里处理错误例如点亮错误指示灯 Error_Handler(); } // 此时ADC已经开始连续转换DMA开始自动搬运数据到adc_buffer // CPU可以继续执行其他任务 printf(ADC with DMA started.\r\n); /* USER CODE END 2 */关键函数解析HAL_ADC_Start_DMA()这个HAL库函数做了三件事启动ADC转换。启动指定的DMA流/通道并配置好传输参数。将DMA传输完成中断使能。调用此函数后ADC-DMA的“自动流水线”就建立起来了无需CPU干预。步骤三使用采集到的数据在主循环中我们可以随时访问adc_buffer数组来获取最新的ADC采样值。由于DMA配置为循环模式当数组填满后它会从数组开头重新开始填充因此adc_buffer中始终保存着最近ADC_BUFFER_SIZE个采样值。/* USER CODE BEGIN WHILE */ while (1) { // 示例计算最近100个ADC采样的平均值 uint32_t sum 0; for(int i 0; i ADC_BUFFER_SIZE; i) { sum adc_buffer[i]; } uint16_t average sum / ADC_BUFFER_SIZE; // 将平均值通过串口打印出来需要先配置好USART printf(ADC Average: %hu\r\n, average); // 延时一段时间避免打印太快 HAL_Delay(1000); /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */步骤四可选处理 DMA 传输完成中断如果我们想知道每一次“数组被完整填充一轮”的时刻可以使用DMA传输完成中断。CubeMX在配置DMA时默认可能没有使能传输完成中断。我们可以在CubeMX中DMA配置页面将Mode下的Interrupt Enable勾选TCIE传输完成中断使能。然后在stm32f1xx_it.c文件中找到DMA对应的中断服务函数如DMA1_Channel1_IRQHandler它内部会调用HAL_DMA_IRQHandler。我们需要在用户回调函数中处理传输完成事件。首先在main.c中重写弱定义的DMA传输完成回调函数/* USER CODE BEGIN 4 */ // 当DMA传输完成即adc_buffer被填满一次时此函数被调用 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) { if(hadc-Instance ADC1) { // 可以在这里设置一个标志位通知主循环新的一轮数据就绪 // 例如dma_transfer_complete 1; // 注意中断回调函数中应执行快速操作避免长时间占用 GPIO_PinState pinState HAL_GPIO_ReadPin(LED_GPIO_Port, LED_Pin); HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, !pinState); // 翻转LED指示一次传输完成 } } /* USER CODE END 4 */注意HAL_ADC_ConvCpltCallback是ADC转换完成通过DMA的回调它本质上是由DMA传输完成中断触发的。4.3 编译、下载与验证连接硬件将STM32F103C8T6的PA1ADC1通道1连接到一个可调电位器的中间引脚电位器另外两端分别接3.3V和GND。将ST-LINK与板子连接好。编译工程点击STM32CubeIDE的锤子图标进行编译确保无错误。下载程序点击绿色调试按钮将程序下载到单片机并运行。观察结果如果配置了LED翻转可以看到LED以固定的频率闪烁这个频率是ADC采样时间 * ADC_BUFFER_SIZE。通过串口助手如PuTTY可以看到每秒打印一次ADC采样的平均值。旋转电位器平均值会随之变化。至此一个完整的ADC DMA数据采集系统就搭建成功了。CPU只在启动时调用了一次HAL_ADC_Start_DMA之后的所有采集、搬运工作均由ADC和DMA硬件自动完成CPU仅在需要时去读取adc_buffer并计算平均值资源占用极低。5. 常见问题与排查思路在实际使用DMA时可能会遇到各种问题。下面列出一些常见现象及解决方法。问题现象可能原因排查思路与解决方案DMA 无法启动HAL_ADC_Start_DMA返回错误1. DMA或ADC时钟未使能。2. DMA通道选择错误。3. 内存缓冲区地址或长度配置非法。4. DMA或ADC未正确初始化。1. 检查CubeMX中相关外设的时钟是否已开启RCC配置。2. 核对数据手册确认该外设ADC1对应的DMA控制器和通道是否正确。3. 确保缓冲区地址是有效的内存地址如全局数组长度大于0。4. 单步调试查看MX_DMA_Init()和MX_ADC1_Init()是否成功执行。ADC 数据没有更新到缓冲区1. ADC未启动转换。2. DMA传输方向错误。3. 内存地址自增未使能。4. DMA模式配置为单次Normal且未重新启动。1. 确认调用了HAL_ADC_Start_DMA。2. 检查DMA配置方向应为Peripheral To Memory。3. 检查DMA配置中Memory Increment Address是否使能。4. 对于连续数据流DMA模式应设为Circular。数据缓冲区中的数据错乱或地址溢出1. 数据宽度不匹配。2. 缓冲区大小不足DMA写越界。3. 内存地址自增的步长错误。1. 确保Peripheral Data Width和Memory Data Width一致如都是Half Word。2. 确保Number of Data参数不大于缓冲区实际长度。3. 地址自增步长由数据宽度自动决定通常无需手动设置。系统运行一段时间后卡死1. DMA传输完成中断未及时清除。2. 中断服务函数处理时间过长导致其他中断被阻塞。3. 内存访问冲突如缓冲区定义在栈上函数返回后地址失效。1. HAL库通常会自动清除中断标志检查是否在别处误清了标志。2. 遵循中断服务函数“快进快出”原则只设置标志位复杂处理放到主循环。3.务必将DMA缓冲区定义为全局变量或静态变量切勿使用局部数组。ADC 采样率达不到预期1. ADC时钟分频过大。2. ADC采样周期设置过长。3. DMA传输本身也有开销在极高频率下可能成为瓶颈。1. 在RCC配置中降低ADC时钟分频系数。2. 在ADC参数配置中减少Sampling Time但可能影响精度。3. 查阅芯片数据手册的“ADCDMA”最大吞吐量章节。通用排查步骤检查 CubeMX 配置这是最常见的问题来源。逐项核对DMA和对应外设的每一个参数。检查时钟树确保DMA、总线AHB、以及相关外设的时钟都已使能且频率合理。使用调试器在HAL_ADC_Start_DMA处设断点检查返回值。查看DMA控制寄存器如CNDTR-剩余数据计数CPAR/CMAR-外设/内存地址的值是否符合预期。实时观察内存缓冲区adc_buffer的内容是否变化。简化测试先尝试最简单的配置单次触发、传输少量数据确保基础功能正常再逐步增加复杂度循环模式、中断等。6. 最佳实践与工程建议将DMA用于生产项目时除了使其工作更应关注其稳定性、可维护性和性能。6.1 内存管理与对齐缓冲区位置DMA缓冲区必须位于物理上连续的内存中。对于全局/静态数组编译器通常会将其分配到连续区域。使用malloc动态分配时需谨慎某些平台可能不保证连续性。内存对齐为了提高访问效率避免硬件错误建议将DMA缓冲区进行对齐。例如对于32位传输地址最好是4字节对齐。可以使用编译器指令__attribute__((aligned(4))) uint32_t buffer[100]; // GCC __align(4) uint32_t buffer[100]; // ARMCC双缓冲区技术在处理连续数据流时如音频可以使用两个缓冲区Ping-Pong Buffer。当DMA向缓冲区A填充时CPU处理缓冲区B的数据填充完成后两者角色互换。这能有效避免处理数据时发生冲突实现无缝数据流。HAL库的HAL_ADC_Start_DMA函数本身就支持传递一个双缓冲区。6.2 中断使用策略精简中断服务程序在DMA传输完成中断回调函数中只做最必要的操作如设置标志位、更新索引、通知任务等。绝对避免进行耗时操作如HAL_Delay、复杂计算、打印日志。使用 RTOS 信号量/队列在RTOS环境中可以在DMA完成中断中释放一个信号量或发送一个消息到队列让一个专门的处理任务线程去进行数据处理。这样中断上下文非常短。错误处理使能DMA传输错误中断TEIE并在错误回调函数HAL_ADC_ErrorCallback中记录错误执行安全恢复操作如停止DMA、重置外设。6.3 配置的健壮性参数检查在调用HAL_ADC_Start_DMA等函数后检查其返回值HAL_OK,HAL_ERROR,HAL_BUSY,HAL_TIMEOUT并做相应处理。超时机制对于需要等待DMA传输完成的场景非循环模式可以使用带超时的等待防止程序死锁。资源清理在应用程序结束或发生错误时确保正确停止DMA和外设HAL_ADC_Stop_DMA释放硬件资源。6.4 性能优化考量总线矩阵与仲裁了解芯片的总线架构如STM32的AHB、APB总线。将DMA的源和目标安排在不会与CPU核心访问产生严重冲突的总线上可以减少总线争用。使用内存到内存 DMA当需要复制或初始化大块内存时如memcpymemset使用内存到内存的DMA可以极大减轻CPU负担。许多MCU的DMA支持此功能。数据宽度与突发传输如果硬件支持配置更大的数据宽度如32位或突发传输模式可以提高单次总线事务的数据吞吐量。掌握DMA是嵌入式开发从“能用”到“高效”的关键一步。它不仅仅是配置几个寄存器更是一种设计思维——将系统视为由多个协同工作的硬件模块组成的整体让合适的硬件做它最擅长的事。从ADC数据采集、UART通信到SPI Flash读写DMA的应用无处不在。建议你在理解本文示例的基础上尝试将其应用到其他外设如TIM触发ADCDMA或探索更高级的特性如双缓冲区、链表模式。实践中遇到的每一个问题都会让你对芯片体系和系统设计有更深的理解。