STM32 DMA实战指南:从CubeMX配置到HAL库编程,解放CPU提升性能
2026/8/4 13:20:43 网站建设 项目流程

最近在嵌入式开发中,经常需要处理大量数据在内存与外设(如ADC、DAC、SPI、UART)之间的搬运。如果全靠CPU来搬运,不仅会大量占用CPU时间,导致主程序效率低下,在高速数据流场景下还可能因处理不及时而丢失数据。这时,DMA(直接存储器访问)技术就成了解放CPU、提升系统性能的关键。本文将围绕STM32的DMA控制器,特别是其通道和流的概念,结合CubeMX配置与HAL库编程,提供一个从原理到实战的完整闭环方案。无论你是刚接触STM32的新手,还是需要在项目中快速集成DMA的开发者,都能从本文找到清晰的配置步骤、可复用的代码示例以及避坑指南。

1. DMA核心概念:为什么需要它?

在深入配置之前,我们必须理解DMA是什么,以及它解决了什么问题。

DMA(Direct Memory Access),即直接存储器访问。它是一种允许特定硬件子系统(外设)直接读写系统内存,而无需中央处理器(CPU)介入的技术。你可以把它想象成一个高效的“数据搬运工”。

它解决了什么问题?

  1. 解放CPU:在没有DMA的情况下,CPU需要亲自执行数据拷贝指令(如memcpy)。对于大量数据或高速数据流(如音频采样、图像传输),CPU会被困在简单的搬运工作中,无法执行更复杂的计算或逻辑任务。
  2. 提高数据吞吐率:DMA控制器是专为数据搬运设计的硬件,其传输效率通常高于CPU通过软件搬运。
  3. 满足实时性要求:在一些对时序要求严格的场景(如ADC定期采样),DMA可以确保数据在精确的时刻被搬运到指定位置,避免因CPU任务调度延迟导致的数据丢失。

STM32中的DMA控制器: 在STM32中,DMA控制器是一个独立的外设。以STM32F4系列为例,它通常有两个DMA控制器(DMA1和DMA2),每个控制器有多个流(Stream),每个流又包含多个通道(Channel)

  • 流(Stream):可以理解为一条数据传输的“管道”或“路径”。每个流是独立的,可以配置其传输的源地址、目标地址、数据量、传输模式等。一个DMA控制器(如DMA2)有8个流(Stream0~Stream7)。
  • 通道(Channel):每个流可以映射到不同的外设请求。通道号决定了这个流服务于哪个外设。例如,DMA2的Stream0,如果选择通道0(Channel 0),它就服务于ADC1;如果选择通道3(Channel 3),它就服务于SPI1的接收。通道是连接“流”这个管道和具体“外设”的接口。

简单类比:DMA控制器是一个物流中心(DMA1/DMA2),流是里面的传送带(Stream0-7),通道是传送带连接不同仓库(外设)的岔路口选择器(Channel 0-7)。你需要为特定的货物(数据)选择一条传送带,并设置好它通往哪个仓库。

2. 环境准备与开发工具

在开始实战前,请确保你的开发环境已就绪。

硬件环境:

  • 一块STM32开发板(本文以STM32F407系列为例,但其DMA配置思路通用)。
  • USB数据线,用于供电和程序下载调试。
  • 可选:逻辑分析仪或示波器,用于观察信号时序。

软件环境:

  1. IDE:STM32CubeIDE(推荐,集成了CubeMX和调试功能)或 Keil MDK / IAR。
  2. STM32CubeMX:用于图形化配置芯片引脚、时钟、外设和中间件。确保版本较新(如V6.11.0)。
  3. STM32 HAL库:通常随CubeMX或CubeIDE安装,本文使用HAL库进行编程。
  4. 串口调试助手:如SecureCRT、Putty、MobaXterm等,用于查看调试信息。

版本说明:本文示例基于以下环境,但核心配置逻辑适用于大多数STM32系列(如F1, F4, H7等),主要区别在于外设名称和可用流/通道的数量,请根据你的具体芯片型号调整。

  • STM32CubeIDE: 1.14.0
  • STM32CubeF4 Firmware Package: 1.28.0
  • 开发板主控:STM32F407VET6

3. DMA关键配置参数详解

使用CubeMX和HAL库配置DMA时,你会遇到一系列参数。理解它们至关重要。

3.1 传输方向(Direction)

定义数据搬运的方向。

  • 外设到存储器(Peripheral To Memory):例如,从ADC的数据寄存器搬运到内存中的数组。这是ADC采样的典型场景。
  • 存储器到外设(Memory To Peripheral):例如,从内存中的数组搬运到USART的数据寄存器,用于串口发送大量数据。
  • 存储器到存储器(Memory To Memory):在两个内存区域之间搬运数据。注意:并非所有DMA流都支持此模式,需要查阅芯片参考手册。

3.2 传输模式(Mode)

  • 普通模式(Normal):DMA传输完指定的数据量(NDTR寄存器值)后,便停止传输,需要软件重新使能才能进行下一次传输。
  • 循环模式(Circular):DMA传输完指定数据量后,自动重置传输计数器并重新开始,形成一个连续的传输循环。这对于需要持续数据流的场景(如双缓冲ADC采样、音频播放)非常有用。

3.3 数据宽度(Data Width)

指单次传输操作的数据单元大小。源和目标的宽度可以不同,DMA会自动处理打包/解包,但通常建议保持一致。

  • 字节(Byte):8位
  • 半字(Half Word):16位(STM32中通常为uint16_t
  • 字(Word):32位(STM32中通常为uint32_t重要原则:数据宽度应与外设数据寄存器宽度对齐。例如,STM32F4的ADC是12位分辨率,数据寄存器是16位的,所以通常使用半字(16位)宽度。

3.4 地址增量(Increment)

传输后,源或目标地址是否自动增加。

  • 不增量(Disable):地址不变。适用于源或目标是外设固定数据寄存器的情况(如&ADC1->DR)。
  • 增量(Enable):地址根据数据宽度自动增加。适用于源或目标是内存数组的情况。

经典配置组合

  • 外设到内存:外设地址不增量,内存地址增量。
  • 内存到外设:内存地址增量,外设地址不增量。
  • 内存到内存:源和目标地址都增量。

3.5 优先级(Priority)

当多个DMA流同时请求传输时,仲裁器根据优先级决定谁先使用总线。

  • 低(Low)
  • 中(Medium)
  • 高(High)
  • 最高(Very High)对于实时性要求高的传输(如ADC),应设置为高优先级。

3.6 FIFO与突发传输(Burst)

高级特性,用于优化大数据块传输性能。FIFO可以暂存数据,实现突发(Burst)传输,减少总线访问次数。在初始学习阶段,可以暂时使用直接模式(FIFO禁用),它更简单直观。

4. 实战案例一:DMA+ADC实现多通道连续扫描

我们将实现用DMA将ADC1的3个通道(CH0, CH1, CH2)连续扫描采样,并自动搬运到内存数组中。

4.1 CubeMX图形化配置

  1. 创建工程,选择芯片
  2. 配置时钟树:将系统时钟(HCLK)配置到最大频率(如STM32F407的168MHz),确保ADC和DMA时钟已使能。
  3. 配置ADC1
    • Analog->ADC1中,选择IN0,IN1,IN2作为扫描通道。
    • Scan Conversion ModeEnabled(扫描模式)。
    • Continuous Conversion ModeEnabled(连续转换模式)。
    • DMA Continuous RequestsEnabled(使能DMA连续请求,这样ADC转换完成就会自动触发DMA请求)。
    • End Of Conversion Selection:选择EOC flag at the end of all conversions(在所有通道转换完成后产生EOC信号)。
    • Parameter Settings->Regular Conversion中,设置Rank,顺序添加通道0,1,2,并设置采样时间(如15 Cycles)。
  4. 配置DMA
    • DMA Settings标签页,点击Add
    • DMA Request:选择ADC1。CubeMX会自动分配一个可用的流(如DMA2 Stream0)。
    • DirectionPeripheral To Memory
    • PriorityHigh
    • ModeCircular(循环模式,实现连续采样)。
    • Increment Address
      • PeripheralDisable(ADC数据寄存器地址固定)。
      • MemoryEnable(内存数组地址需要递增)。
    • Data Width
      • PeripheralHalf Word(ADC数据寄存器是16位)。
      • MemoryHalf Word(我们定义uint16_t数组)。
    • FIFO:默认Disable(直接模式)。

4.2 生成代码与用户代码编写

点击GENERATE CODE生成工程。我们主要修改main.c

/* 在USER CODE BEGIN PV 区域定义变量 */ #define ADC_BUFF_SIZE 300 // 每个通道采样100次,3个通道共300个数据 uint16_t adc_dma_buffer[ADC_BUFF_SIZE]; // DMA搬运目标数组 uint32_t adc_ch0_val, adc_ch1_val, adc_ch2_val; // 用于计算平均值的变量 /* USER CODE END PV */ /* 在USER CODE BEGIN 2 区域启动ADC和DMA */ // 启动ADC的DMA传输,将ADC数据寄存器连接到我们定义的内存数组 // 参数:ADC句柄,目标数组地址,数组长度(以数据单元为单位,这里是半字个数) if (HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_dma_buffer, ADC_BUFF_SIZE) != HAL_OK) { Error_Handler(); // 启动失败处理 } /* USER CODE END 2 */ /* 在USER CODE BEGIN 3 区域的主循环中处理数据 */ while (1) { // 简单演示:计算最近100组采样中,每个通道的平均值 // 注意:由于DMA在循环搬运,数组中的数据是最新的连续数据 adc_ch0_val = 0; adc_ch1_val = 0; adc_ch2_val = 0; for(int i = 0; i < 100; i++) // 假设取最新的100组 { // 数组排列顺序:CH0, CH1, CH2, CH0, CH1, CH2... adc_ch0_val += adc_dma_buffer[i * 3 + 0]; // 第i组的通道0 adc_ch1_val += adc_dma_buffer[i * 3 + 1]; // 第i组的通道1 adc_ch2_val += adc_dma_buffer[i * 3 + 2]; // 第i组的通道2 } adc_ch0_val /= 100; adc_ch1_val /= 100; adc_ch2_val /= 100; // 这里可以将平均值通过串口打印出来,或用于其他控制逻辑 // printf("CH0:%lu, CH1:%lu, CH2:%lu\r\n", adc_ch0_val, adc_ch1_val, adc_ch2_val); HAL_Delay(1000); // 每秒处理一次 } /* USER CODE END 3 */

代码解释

  1. HAL_ADC_Start_DMA函数启动了ADC的转换,并关联了DMA传输。ADC每完成一组扫描(3个通道),DMA就会自动将ADC数据寄存器(ADC1->DR)中的值搬运到adc_dma_buffer数组中,并自动递增内存地址。
  2. 由于DMA配置为循环模式,当数组被填满后,DMA会自动回到数组开头覆盖旧数据,实现连续不断的采样,无需CPU干预。
  3. 在主循环中,CPU可以自由地处理已经搬运到内存中的数据(如计算平均值),实现了采集与处理的并行。

5. 实战案例二:DMA+UART实现高效串口数据发送

使用DMA发送串口数据,可以避免CPU等待每个字节发送完成的延时。

5.1 CubeMX配置

  1. 配置USART1(或其他可用串口)为异步模式(Asynchronous),并设置合适的波特率(如115200)。
  2. 配置DMA
    • DMA Settings标签页,为USART1_TX添加一个DMA流。
    • DirectionMemory To Peripheral
    • PriorityMedium
    • ModeNormal(发送完一段数据就停止)。
    • Increment Address
      • MemoryEnable(我们要发送一个数组)。
      • PeripheralDisable(USART数据寄存器地址固定)。
    • Data Width:都设置为Byte(串口通常以字节为单位)。

5.2 代码实现

/* USER CODE BEGIN PV */ uint8_t tx_data[] = "Hello, CSDN! This message is sent by DMA.\r\n"; /* USER CODE END PV */ /* USER CODE BEGIN 2 */ // 先使用轮询方式发送一个启动信息,证明串口工作正常 HAL_UART_Transmit(&huart1, (uint8_t*)"UART DMA Demo Start\r\n", 22, 1000); /* USER CODE END 2 */ /* USER CODE BEGIN 3 */ while (1) { // 使用DMA发送数据,非阻塞,函数调用后立即返回 if(HAL_UART_Transmit_DMA(&huart1, tx_data, sizeof(tx_data) - 1) != HAL_OK) { // 发送失败处理(例如DMA忙) Error_Handler(); } // 此时CPU可以立即去做其他事情,无需等待发送完成 // 例如:闪烁LED,处理传感器数据等 HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); // 等待一段时间,或者通过回调函数知道DMA发送完成 // 这里简单延时,注意:如果上次DMA发送未完成就再次调用HAL_UART_Transmit_DMA,会返回BUSY错误。 HAL_Delay(1000); } /* USER CODE END 3 */ /* 可以在合适的地方定义发送完成回调函数(弱函数需要重写) */ void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if(huart->Instance == USART1) { // USART1的DMA发送完成,可以在这里置标志位或进行下一步操作 // 例如:点亮另一个LED,或者准备下一包数据 } }

关键点

  • HAL_UART_Transmit_DMA是非阻塞函数,调用后立即返回,数据传输由DMA后台完成。
  • 普通模式下,一次DMA传输完成后会自动停止。需要发送新数据时,再次调用该函数。
  • 可以通过重写HAL_UART_TxCpltCallback回调函数,在发送完成时得到通知,实现精确的流程控制。

6. 常见问题与排查思路(FAQ)

DMA配置看似简单,但实际调试中容易遇到各种问题。下表总结了常见现象和解决思路:

问题现象可能原因排查思路与解决方案
DMA不传输数据1. DMA或外设时钟未使能。
2. DMA流/通道未正确映射到外设。
3. DMA未使能或未启动传输。
4. 外设未产生DMA请求。
1. 在CubeMX的Clock Configuration确认时钟树,在代码中检查__HAL_RCC_DMAx_CLK_ENABLE()__HAL_RCC_xxx_CLK_ENABLE()是否被调用。
2. 核对《芯片参考手册》的DMA请求映射表,确认CubeMX配置的流和通道与外设匹配。
3. 确认调用了正确的HAL启动函数(如HAL_ADC_Start_DMA)。
4. 确认外设已配置为产生DMA请求(如ADC的DMA Continuous Requests已使能)。
数据错位或混乱1. 源/目标地址增量配置错误。
2. 数据宽度配置错误。
3. 内存缓冲区大小不足或溢出。
4. 数组排列顺序与扫描顺序不匹配。
1. 牢记“外设地址不增量,内存地址增量”等原则,检查CubeMX中Increment Address设置。
2. 确保Data Width与外设寄存器宽度及内存变量类型匹配(如ADC用Half Word,对应uint16_t数组)。
3. 计算所需缓冲区大小:通道数 * 每次采样组数 * 数据宽度。确保DMA传输长度NDTR设置正确。
4. 对于多通道ADC扫描,数据在数组中是按扫描顺序交替存储的,处理数据时索引计算要正确。
只能传输一次,不循环DMA传输模式配置为Normal而非Circular如果需求是连续传输,在CubeMX中将Mode改为Circular。注意,在普通模式下,传输完成后需要重新启动。
程序卡死在DMA传输完成中断1. 未清除传输完成中断标志位(TCIF)。
2. 在中断回调函数中进行了耗时操作或错误处理。
1. HAL库通常会自动清除标志位,但如果用户直接操作寄存器,需手动清除DMAx_LISRDMAx_HISR中的TCIFx位。
2. 中断回调函数应尽量简短,快速置标志位后退出,复杂处理放到主循环中。
使用DMA+UART发送,数据丢失最后几个字节在DMA传输完成前,函数退出导致发送缓冲区(tx_data)被释放或覆盖。确保存储待发送数据的数组(如tx_data)是全局变量或静态变量,其生命周期必须长于DMA传输时间。不能在函数内定义局部数组然后启动DMA,函数返回后数组内存无效。
内存到内存传输失败1. 所使用的DMA流不支持内存到内存模式。
2. 源/目标地址未按数据宽度对齐(如非4字节对齐地址进行字传输)。
1. 查阅数据手册,确认该DMA流支持Memory-to-Memory
2. 确保地址对齐,或使用字节传输模式。

7. 最佳实践与工程建议

将DMA用于实际项目时,遵循以下建议可以提升代码的健壮性和可维护性。

  1. 清晰的变量命名与注释

    • DMA缓冲区变量名应体现其用途,如adc1_dma_bufferuart_tx_dma_buffer
    • 在数组定义处注释其大小和排列格式,特别是多通道ADC扫描。
    // ADC DMA缓冲区:3个通道,循环存储,深度100组 // 排列:[CH0_0, CH1_0, CH2_0, CH0_1, CH1_1, CH2_1, ...] #define ADC_CH_NUM 3 #define ADC_SAMPLE_GROUP 100 uint16_t adc_dma_buf[ADC_CH_NUM * ADC_SAMPLE_GROUP];
  2. 双缓冲技术(Double Buffering): 对于高速连续数据采集(如音频),使用循环模式+DMA双缓冲可以避免数据处理时数据被覆盖的风险。

    • 原理:配置DMA传输长度为缓冲区总长的一半。当半传输完成(HT)和传输完成(TC)时,分别触发中断。在中断中,CPU处理另一半“安全”的数据。
    • 实现:在CubeMX中使能DMA的Half Transfer InterruptTransfer Complete Interrupt,并重写对应的回调函数。
  3. 错误处理: HAL库的DMA启动函数(如HAL_ADC_Start_DMA)会返回HAL_StatusTypeDef。务必检查返回值,并在错误时采取相应措施(如重试、记录日志、系统复位)。

    HAL_StatusTypeDef status = HAL_ADC_Start_DMA(&hadc1, ...); if(status != HAL_OK) { // 记录错误码 status // 尝试恢复或进入安全模式 }
  4. 资源冲突检查

    • 一个DMA流在同一时间只能用于一个外设。在项目多人协作或模块化开发时,应在文档或代码中明确记录各DMA流的使用情况。
    • 使用__HAL_DMA_GET_STREAM_STATE()宏可以检查DMA流的状态(如HAL_DMA_STATE_READY,HAL_DMA_STATE_BUSY),在启动新的传输前进行检查。
  5. 功耗考量: DMA传输本身消耗总线带宽,但相比CPU搬运数据,它允许CPU进入低功耗模式(如Sleep)。在电池供电设备中,可以配置DMA在后台搬运数据,同时让CPU休眠,由DMA传输完成中断唤醒CPU进行处理,从而大幅降低系统平均功耗。

  6. 调试技巧

    • 在调试初期,可以先用轮询模式(如HAL_ADC_Start+HAL_ADC_PollForConversion)验证外设基本功能是否正常,然后再切换到DMA模式。
    • 利用STM32CubeIDE的实时变量查看(Live Watch)功能,可以实时观察DMA缓冲区数组的值变化,这是调试DMA传输是否生效的最直观方法。
    • 在DMA传输完成中断回调函数中设置断点,可以确认传输是否被正确触发。

掌握DMA是提升STM32项目性能的关键一步。从理解流和通道的概念开始,通过CubeMX进行可视化配置,再结合HAL库的API,你可以快速将DMA应用到ADC、UART、SPI、I2C等各种外设的数据传输中。核心在于明确数据流向、正确配置地址增量和传输模式,并妥善处理中断与缓冲区。建议从本文的ADC多通道采样和UART发送两个经典案例入手,亲手实践一遍,再逐步应用到更复杂的场景,如SPI通信、DAC波形生成等。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询