ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

嵌入式DMA实战:从32位DMA控制器配置到串口数据传输优化

嵌入式DMA实战:从32位DMA控制器配置到串口数据传输优化

在实际嵌入式开发、音视频处理或高性能数据传输场景中,直接内存访问(DMA)是提升系统效率、降低CPU负载的核心技术。当项目文档或芯片手册中出现类似“32 DMA 32DMA-17”这样的标识时,它通常指向一个具体的DMA控制器型号、通道配置或某个项目内部的模块代号。对于开发者而言,理解其背后的技术含义、掌握其配置方法并能在实际项目中应用,是打通硬件加速关键路径的必要技能。

本文将以一个典型的嵌入式DMA应用为背景,假设“32 DMA 32DMA-17”代表一个支持32位数据宽度、拥有多个通道的DMA控制器(例如DMA1的通道7),我们将从零开始,完成从概念理解、环境准备、寄存器配置到数据搬运验证的全过程。无论你是正在调试一块新的开发板,还是需要优化现有项目的内存拷贝性能,这篇文章都将提供一条清晰的实践路径。我们将重点关注如何配置DMA进行内存到外设(如串口)的数据传输,并解释每一步配置背后的硬件原理,最后给出完整的排错清单和性能优化建议。

1. 理解DMA:为什么需要它以及它是如何工作的

在深入配置之前,必须清楚DMA解决了什么问题,以及它是如何与CPU、内存、外设协同工作的。

1.1 DMA要解决的核心问题:解放CPU

在没有DMA的系统中,如果要将一片内存数据发送到串口(UART),CPU需要执行一个循环:从内存读取一个字节 -> 将这个字节写入串口的数据寄存器 -> 等待串口发送完成或就绪 -> 重复。这个过程占用了CPU大量的时间,使其无法处理其他更复杂的任务,如业务逻辑计算或响应其他中断。这种由CPU亲自搬运数据的方式称为“程序控制I/O”或“轮询”,效率低下。

DMA(Direct Memory Access,直接内存访问)控制器是一个独立的硬件模块。它的职责就是代替CPU,在内存与内存之间、内存与外设之间高效地搬运数据。一旦CPU初始化好DMA控制器(告诉它源地址、目标地址、数据量),DMA控制器就会在后台接管数据传输工作,而CPU可以继续执行其他代码。只有当整个数据块传输完成,DMA控制器才会通过中断通知CPU“任务完成”。这极大地提高了系统的整体吞吐量和实时性。

1.2 DMA的工作流程与关键概念

一次典型的DMA传输涉及以下几个核心角色和阶段:

  1. 发起者(Initiator):通常是CPU,通过软件配置DMA控制器。
  2. DMA控制器(Controller):执行传输任务的硬件。
  3. 源(Source):数据的起始地址,可以是内存(如SRAM中的数组)或外设寄存器(如ADC的数据寄存器)。
  4. 目标(Destination):数据的终点地址,同样可以是内存或外设寄存器。
  5. 通道(Channel):一个DMA控制器通常有多个独立的通道,每个通道可以处理一对源和目标的传输任务,并且可以绑定到特定的外设(如UART1的发送请求会固定由DMA1的通道4服务)。通道之间可以设置优先级。
  6. 传输计数器(Counter):需要传输的数据单元数量。
  7. 数据宽度(Data Width):每次传输操作的数据位数,如8位(字节)、16位(半字)、32位(字)。源和目标的数据宽度可以不同,控制器通常支持打包(Packing)和解包(Unpacking)。
  8. 仲裁器(Arbiter):当多个通道同时请求传输时,根据优先级决定服务顺序。
  9. 中断(Interrupt):传输完成、传输一半或发生错误时,DMA控制器可以产生中断,通知CPU处理。

工作流程简述:

  • 配置阶段:CPU设置DMA通道的源地址、目标地址、传输方向、数据宽度、传输模式、是否使能中断等。
  • 触发阶段:传输可以由软件触发(CPU写寄存器启动),也可以由硬件触发(外设如UART发送寄存器空时,自动向DMA发出请求)。
  • 传输阶段:DMA控制器接管总线,在源和目标之间搬运数据,每次搬运一个数据宽度单位,传输计数器递减。
  • 结束阶段:传输计数器归零,传输完成。如果使能了完成中断,则产生中断,CPU在中断服务程序中处理后续事宜(如准备下一批数据)。

1.3 解读“32 DMA 32DMA-17”

在缺乏具体项目文档的情况下,我们可以基于常见命名规则进行合理推测:

  • 第一个“32”:很可能指数据总线宽度或数据宽度为32位。这意味着DMA控制器一次可以搬运一个32位(4字节)的数据。
  • “DMA”:直接内存访问控制器。
  • 第二个“32”:可能指DMA控制器的编号(如DMA1, DMA2)或版本。
  • “DMA-17”:极有可能指具体的通道编号。在某些芯片中,通道编号是全局的,例如“通道17”。在另一些芯片(如STM32)中,它可能表示“DMA2 Stream 7”(因为DMA2的流7常用于某些外设)。在本文的后续示例中,我们将以一个假设的“DMA1通道7”作为我们的操作对象,这是一个常见的用于UART发送的通道配置。实际开发中,你必须查阅你所使用的芯片的《参考手册》来确定确切的映射关系。

2. 环境准备与项目基础配置

在开始写DMA代码之前,需要搭建一个可以编译、下载和调试的嵌入式开发环境。

2.1 硬件与软件环境清单

项目说明示例/备注
开发板/MCU支持DMA的微控制器如STM32F4系列、GD32系列等ARM Cortex-M内核芯片
调试器用于下载程序和调试J-Link, ST-Link, DAP-Link
IDE/工具链代码编辑、编译、调试环境Keil MDK, IAR Embedded Workbench, STM32CubeIDE, VSCode + ARM GCC
芯片支持包芯片寄存器定义、启动文件、外设驱动库STM32CubeMX生成的HAL库代码、标准外设库、或直接寄存器操作的头文件
串口工具用于验证DMA发送的数据PuTTY, Tera Term, SecureCRT

2.2 使用STM32CubeMX进行基础工程搭建(以HAL库为例)

为了快速构建一个包含DMA的工程框架,我们使用STM32CubeMX工具。这里假设我们的目标是使用DMA将内存中的一个字符串发送到UART1。

  1. 打开CubeMX,选择你的芯片型号
  2. 配置系统时钟(SYS):将Debug设为Serial Wire(如果使用ST-Link调试)。
  3. 配置时钟树(RCC):选择合适的外部晶振(HSE),并将系统时钟(SYSCLK)配置到芯片允许的最高频率(如168MHz for STM32F407),以获得最佳DMA性能。
  4. 配置UART1
    • Pinout & Configuration标签页找到USART1
    • 将模式设置为Asynchronous(异步通信)。
    • 配置波特率、字长、停止位、校验位(例如 115200 8-N-1)。
    • DMA Settings选项卡中,点击Add,为USART1_TX添加一个DMA请求。
    • 在弹出的DMA流/通道选择中,根据你的芯片手册选择正确的通道。对于STM32F4,UART1_TX通常使用DMA2 Stream7 Channel4这里就对应了我们假设的“DMA-17”场景。我们将流(Stream)和通道(Channel)都配置好。
    • 配置DMA参数:
      • Direction:Memory To Peripheral(内存到外设)。
      • Priority:Medium(可根据需要调整)。
      • Mode:Normal(传输一次后停止)。如需循环发送(如用于显示刷新),可选Circular
      • Increment Address: 对于Memory侧,需要设置为Enable,这样DMA会在每次传输后自动增加内存地址;对于Peripheral侧(UART数据寄存器地址固定),设置为Disable
      • Data Width: 将MemoryPeripheral都设置为Byte(8位),因为UART通常以字节为单位发送。但我们的DMA控制器是32位的,它内部会高效地处理字节传输。
  5. 生成代码
    • 转到Project Manager标签页,设置项目名称、路径、IDE(如MDK-ARM V5)。
    • Code Generator中,选择Copy only necessary library filesGenerate peripheral initialization as a pair of ‘.c/.h’
    • 点击GENERATE CODE,生成完整的工程。

现在,你得到了一个已经配置好UART1和DMA的Keil或IDE工程。CubeMX已经帮我们生成了DMA和UART的初始化代码(在main.cMX_DMA_Init()MX_USART1_UART_Init()函数中)。

3. 编写DMA传输的核心代码

工程生成后,我们需要在用户代码区域编写启动DMA传输的逻辑。

3.1 定义源数据缓冲区

main.c文件顶部,用户代码区(/* USER CODE BEGIN PV */)定义要发送的数据。

/* Private variables ---------------------------------------------------------*/ /* USER CODE BEGIN PV */ uint8_t dma_tx_buffer[] = "Hello, DMA! This data is sent by DMA.\r\n"; /* USER CODE END PV */

这是一个存储在内存(SRAM)中的字节数组,也是DMA传输的源数据。

3.2 启动DMA传输

main函数的while(1)循环之前或某个事件触发时,启动DMA传输。

/* USER CODE BEGIN 2 */ // 启动DMA传输,将内存数据发送到UART1 if (HAL_UART_Transmit_DMA(&huart1, dma_tx_buffer, sizeof(dma_tx_buffer)-1) != HAL_OK) { // 如果启动失败,可以进行错误处理,例如点亮一个错误LED Error_Handler(); } // 此时CPU已解放,可以执行其他任务 // 例如,可以在这里操作LED闪烁,证明CPU未被阻塞 /* USER CODE END 2 */

代码解释

  • HAL_UART_Transmit_DMA是HAL库提供的函数,用于启动UART的DMA发送。
  • 参数1:&huart1,UART1的句柄,由CubeMX生成。
  • 参数2:dma_tx_buffer,源数据缓冲区的首地址。
  • 参数3:sizeof(dma_tx_buffer)-1,要发送的数据长度。我们减1是为了不发送字符串末尾的\0
  • 函数会检查DMA通道状态,并启动传输。传输启动后立即返回,CPU不会等待发送完成

3.3 处理DMA传输完成中断(可选但重要)

为了知道数据何时发送完毕,或者处理传输错误,我们需要使能并实现DMA传输完成中断回调函数。

  1. 在CubeMX中使能中断:回到DMA Settings,选中为USART1_TX添加的DMA流,在下方NVIC Settings中勾选DMAx streamy global interrupt(例如DMA2 stream7 global interrupt)的使能复选框。重新生成代码。

  2. 实现回调函数:HAL库使用弱定义(__weak)的回调函数,我们需要在用户文件中重写它。通常在main.c或专门的dma.c文件中实现。

/* USER CODE BEGIN 4 */ /** * @brief Tx Transfer completed callback. * @param huart Pointer to a UART_HandleTypeDef structure. * @retval None */ void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { // 判断是哪个UART触发的回调 if (huart->Instance == USART1) { // 传输完成,可以在这里进行后续操作 // 例如,翻转一个LED,或者准备下一批数据再次启动DMA HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin); // 假设你定义了一个LED // 注意:不要在回调函数中进行耗时操作! } } /** * @brief DMA error callback. * @param hdma Pointer to a DMA_HandleTypeDef structure. * @retval None */ void HAL_DMA_ErrorCallback(DMA_HandleTypeDef *hdma) { // 处理DMA传输错误,例如读取错误标志并复位 // 对于生产环境,这里应有详细的错误日志或恢复机制 Error_Handler(); } /* USER CODE END 4 */

3.4 验证CPU未被阻塞

为了直观证明DMA工作期间CPU是自由的,我们可以在main函数的while(1)循环中让另一个LED以不同的频率闪烁。

/* Infinite loop */ /* USER CODE BEGIN WHILE */ while (1) { // 主循环任务,与DMA传输并行执行 HAL_GPIO_TogglePin(CPU_LED_GPIO_Port, CPU_LED_Pin); // 另一个LED HAL_Delay(200); // 延时200ms /* USER CODE END WHILE */ /* USER CODE BEGIN 3 */ } /* USER CODE END 3 */

如果DMA工作正常,你会看到CPU_LED以200ms的间隔稳定闪烁,而TxCpltCallback中控制的LED会在每次DMA发送完成后闪烁一次。这证明了数据传输是由DMA在后台完成的。

4. 关键配置详解与寄存器级理解

虽然HAL库简化了操作,但理解底层寄存器配置对于调试和优化至关重要。下面以STM32F4的DMA为例,解释关键寄存器。

4.1 DMA数据流配置寄存器(DMA_SxCR)

这是每个DMA流(Stream)的核心控制寄存器。我们通过HAL库函数设置的参数最终都写入了这里。

位域名称功能说明我们的配置(内存到外设,字节传输)
EN流使能1:使能该流。传输完成后由硬件清零。HAL_UART_Transmit_DMA置1
DIR[1:0]数据传输方向00:外设到内存;01:内存到外设;10:内存到内存01 (内存到外设)
PSIZE[1:0]外设数据宽度00:8位;01:16位;10:32位00 (字节)
MSIZE[1:0]内存数据宽度同上00 (字节)
PINC外设地址递增1:每次传输后外设地址递增0 (外设地址固定为UART->DR)
MINC内存地址递增1:每次传输后内存地址递增1 (内存地址递增)
CIRC循环模式1:传输完成后计数器自动重装,循环传输0 (普通模式)
PL[1:0]优先级00:低;01:中;10:高;11:最高01 (中等)
CHSEL[2:0]通道选择选择该流映射到哪个DMA通道(Channel)根据CubeMX选择(如Channel 4)

4.2 DMA数据流外设/内存地址寄存器(DMA_SxPAR, DMA_SxM0AR)

  • DMA_SxPAR:存储外设端数据寄存器的地址。对于UART发送,就是USART1->DR的地址。HAL库在初始化时已经设置好。
  • DMA_SxM0AR:存储内存缓冲区的起始地址。即我们定义的dma_tx_buffer的地址。

4.3 DMA数据流数据项数寄存器(DMA_SxNDTR)

这个寄存器存储要传输的数据项数量。每次传输完成一个数据宽度单位(在我们的例子里是一个字节),该值就减1。当减到0时,传输完成,如果使能了传输完成中断(TCIE),就会产生中断。

重要:在传输过程中,软件不应修改DMA_SxNDTR寄存器,除非先禁用该DMA流(EN=0)。HAL库的HAL_UART_Transmit_DMA函数在启动前会设置好这个值。

4.4 中断使能寄存器

DMA_SxCR寄存器中,还有几个中断使能位:

  • TCIE:传输完成中断使能。
  • HTIE:传输过半中断使能。
  • TEIE:传输错误中断使能。 CubeMX在使能中断后,会帮我们设置好TCIE

5. 运行验证与结果分析

将代码编译并下载到开发板后,按以下步骤验证:

  1. 硬件连接:使用USB转串口模块,将开发板的UART1_TX引脚连接到电脑的USB口。
  2. 打开串口助手:在电脑上打开串口调试工具(如PuTTY),选择正确的COM口,设置波特率为115200,8位数据,1位停止位,无校验。
  3. 复位开发板:你会看到串口助手立即收到字符串:Hello, DMA! This data is sent by DMA.
  4. 观察LEDCPU_LED会以200ms间隔持续闪烁,而传输完成LED只会在每次字符串发送完成后闪烁一次。
  5. 逻辑分析仪/示波器验证(进阶):如果用逻辑分析仪抓取UART1_TX引脚波形,你会看到数据字节连续、等间隔地发出,中间没有因为CPU处理其他任务而产生异常的延时,这正是DMA工作的特征。

成功标志:串口正确收到数据,且主循环的LED闪烁未受数据传输影响。这证明DMA成功地在后台完成了数据搬运任务,CPU在此期间是空闲的。

6. 常见问题排查清单

在实际项目中,DMA配置出错是常见问题。下面是一个按优先级排序的排查清单。

问题现象可能原因检查点与解决方案
编译通过,但串口无任何输出1. DMA/UART时钟未使能。
2. DMA通道映射错误。
3. 传输未启动或立即完成(长度0)。
4. 串口引脚配置错误。
1. 检查MX_DMA_InitMX_USART1_UART_Init是否被main调用。
2.核对芯片手册,确认UART1_TX使用的DMA和通道是否正确。这是最常见的错误!
3. 调试模式单步执行,查看HAL_UART_Transmit_DMA返回值。检查dma_tx_buffer地址和长度是否有效。
4. 使用CubeMX确认UART1_TX引脚(如PA9)已正确配置为复用推挽输出。
串口输出乱码或部分数据丢失1. 波特率不匹配。
2. 内存/外设数据宽度配置错误。
3. 内存地址递增(MINC)配置错误。
4. 缓冲区数据被意外修改。
1. 确认电脑串口助手和代码中的波特率设置完全一致。
2. 确认PSIZEMSIZE设置。如果外设是8位(如UART),但设置了32位宽度,DMA会一次写32位数据到UART->DR,导致数据错误。
3. 对于内存到外设,MINC必须使能,否则DMA会一直发送缓冲区的第一个字节。
4. 确保在DMA传输过程中,没有其他代码(如中断)修改dma_tx_buffer的内容。
程序卡死或进入HardFault1. 访问了非法内存地址。
2. DMA中断服务程序(ISR)处理不当。
3. 数组越界或指针错误。
1. 检查DMA_SxM0ARDMA_SxPAR寄存器值是否在合法的内存/外设地址范围内。
2. 检查DMA中断向量是否正确映射到HAL库的中断处理函数DMAx_Streamy_IRQHandler
3. 检查缓冲区长度NDTR是否设置过大,导致DMA访问了缓冲区之后的不属于你的内存区域。
只有第一次发送成功,后续发送失败1. DMA工作在Normal模式,传输完成后流被禁用(EN=0),但未重新启动。
2. 未等待上次传输完成就启动新传输。
1. 在HAL_UART_TxCpltCallback中或再次发送前,检查DMA流状态。如果需要连续发送,可以在回调中重新调用HAL_UART_Transmit_DMA
2. 使用HAL_UART_GetState()检查UART是否就绪,或者使用HAL_DMA_GetState()检查DMA状态。确保前一次传输完成后再启动新的。
DMA传输完成中断未触发1. DMA中断未使能(NVIC配置)。
2. 传输完成中断标志未使能(TCIE位)。
3. 中断服务函数(IRQHandler)未正确实现或未清除中断标志。
1. 在CubeMX的NVIC配置中确认DMA流全局中断已勾选。
2. 查看DMA_SxCR寄存器,确认TCIE位为1。
3. HAL库通常会自动清除标志。如果使用标准库或寄存器操作,必须在ISR中手动清除对应的中断标志位(如DMA_LIFCR中的CTCIFy)。

7. 最佳实践与性能优化建议

掌握了基础用法后,以下实践能让你的DMA应用更健壮、高效。

7.1 双缓冲区(Ping-Pong Buffer)技术

对于连续数据流(如音频采集、高速ADC),使用单个缓冲区时,DMA传输期间CPU不能访问缓冲区,否则会导致数据损坏。双缓冲区技术可以解决这个问题。

原理:准备两个缓冲区(BufferA和BufferB)。当DMA正在向UART发送BufferA的数据时,CPU可以处理已经接收完的BufferB的数据。当DMA发送完BufferA后,产生中断,在中断回调中,CPU将DMA的目标地址切换到BufferB,并开始处理BufferA的数据,如此循环往复。

// 示例框架 uint8_t buffer_a[BUFFER_SIZE]; uint8_t buffer_b[BUFFER_SIZE]; uint8_t *dma_current_target = buffer_a; void Start_DMA_Transfer(void) { HAL_UART_Transmit_DMA(&huart1, dma_current_target, BUFFER_SIZE); } void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { // 1. 处理刚刚发送完的缓冲区 (dma_current_target) ProcessBuffer(dma_current_target); // 2. 切换缓冲区 if (dma_current_target == buffer_a) { dma_current_target = buffer_b; } else { dma_current_target = buffer_a; } // 3. 启动下一次传输 Start_DMA_Transfer(); } }

7.2 合理选择数据宽度与突发传输

  • 对齐访问:如果源和目标都是32位对齐的内存地址,将数据宽度设置为32位(字),DMA单次传输效率最高,因为32位DMA控制器一次就能搬完4个字节。
  • 突发传输(Burst):一些高级DMA控制器支持突发传输模式,可以在获得总线控制权后连续传输多个数据项,进一步减少总线仲裁开销。在CubeMX的DMA配置中,可以设置FIFO模式和突发大小。

7.3 内存到内存传输的优化

当使用DMA进行内存间拷贝(如memcpy)时,性能远超CPU逐字节拷贝。配置时注意:

  • 将源和目标的Data Width都设置为32位(如果地址对齐)。
  • 使能源和目标的地址递增(PINCMINC)。
  • 对于大块数据,使用循环模式(CIRC)可以用于实现环形缓冲区。

7.4 生产环境注意事项

  1. 错误处理:务必实现HAL_DMA_ErrorCallback。在函数内读取DMA错误状态寄存器(如DMA_HISR/DMA_LISR),区分是传输错误(TEIF)、FIFO错误(FEIF)还是直接模式错误(DMEIF),并记录日志或执行安全恢复。
  2. 资源竞争:DMA和CPU共享总线(如AHB)。当两者同时访问同一内存区域(如SRAM)时,总线仲裁会导致等待。对于性能关键路径,可以考虑将DMA源/目标缓冲区放在不同的内存块(如使用CCM RAM,如果芯片支持)。
  3. 功耗管理:在低功耗模式下,DMA时钟可能被关闭。在进入低功耗模式前,确保所有DMA传输已完成并禁用DMA时钟。唤醒后需要重新初始化DMA。
  4. 代码可移植性:虽然HAL库提高了可移植性,但不同系列甚至不同型号的STM32,其DMA流/通道与外设的映射关系可能不同。将映射关系(如DMA2_Stream7_Channel4)定义为宏或放在配置文件中,便于移植。

通过从概念到实践,再到排错和优化的完整学习,你应该已经能够驾驭项目中“32 DMA 32DMA-17”这类标识背后的技术实质,并能够将其转化为稳定高效的代码。下一步,可以尝试将DMA应用于其他外设,如ADC扫描采集、SPI通信、或内存到内存的大块数据搬运,进一步巩固这项嵌入式开发的核心技能。

返回列表