资讯动态

嵌入式DMA技术详解:原理、工作模式与串口收发实战

发布时间:2026/8/5 2:32:29 来源:尧图企业网站定制
1. DMA到底是什么为什么每个嵌入式开发者都绕不开它如果你在调试串口收发、SPI读写Flash或者驱动LCD屏幕时被CPU频繁进入中断、处理数据搬运搞得焦头烂额导致主程序卡顿、实时性变差那么你大概率需要深入了解DMA这个“外挂”了。DMA全称Direct Memory Access直接存储器访问。这个名字听起来有点抽象但它的作用非常直接让数据在内存和外设之间“自动”搬家完全不需要CPU这个“总经理”亲自插手。想象一下你CPU是一家公司的总经理每天要处理各种决策执行算法、逻辑判断。现在仓库内存和生产线外设如串口之间有一批原料数据需要频繁搬运。如果每次搬运都需要你亲自跑一趟那你基本就没时间干正事了。DMA就像一个你雇来的专业搬运队DMA控制器你只需要在第一次告诉它“从A仓库的X号货架搬100箱货到B生产线的Y号入口搬完了叫我一声。” 之后你就可以回去开会了。搬运队会自己完成所有重复性的体力活只在全部搬完或者遇到问题时才通知你。这样一来你的工作效率系统性能就大大提升了。在嵌入式领域DMA的应用无处不在。从最基础的串口USARTDMA收发到高速的SPI读写外部存储器如W25Q128再到音频处理的I2S DMA传输以及图形显示的LCD如LVGL使用DMA2D加速DMA都是提升系统吞吐量、降低CPU负载、保证实时性的关键角色。理解了DMA你就能从“资源紧张”的开发者进阶为能游刃有余设计高效系统的工程师。2. DMA核心原理与工作模式深度拆解要驾驭DMA不能只停留在“自动搬运”的概念上必须深入其内部工作机制。这能帮助你在遇到复杂场景如多外设共用DMA、环形缓冲、双缓冲时做出正确的设计和排错。2.1 DMA控制器的基本架构以常见的STM32系列微控制器为例其DMA控制器通常包含多个独立的通道Channel。你可以把每个通道看作一条独立的“搬运流水线”。一条流水线在同一时间只能为一个“客户”即一个外设或内存区域服务。这些客户包括ADC、SPI、I2C、USART、定时器等的外设寄存器。关键组件解析源地址寄存器Source Address告诉DMA数据从哪里来。可以是内存地址如数组buffer的首地址也可以是外设数据寄存器地址如USART1-DR。目标地址寄存器Destination Address告诉DMA数据搬到哪里去。同样可以是内存或外设地址。数据数量寄存器Data Number设定本次传输要搬运多少“单位”的数据。单位大小由数据宽度决定。数据宽度Data Width设定每次搬运操作的数据大小通常是字节8位、半字16位或字32位。必须注意源和目的的数据宽度设置要匹配否则会导致数据错乱。例如从32位宽的内存搬数据到8位宽的串口数据寄存器通常需要设置源数据宽度为字节或者使用DMA的“打包Packing”功能如果支持。传输模式Transfer Mode外设到内存模式典型应用是串口DMA接收。源地址是USART-DR目标地址是用户定义的数组。串口每收到一个字节就会向DMA控制器发出一个请求DMA随即将该字节从串口数据寄存器搬到目标数组。内存到外设模式典型应用是串口DMA发送。源地址是存放待发送数据的数组目标地址是USART-DR。DMA将数据逐个搬到串口串口硬件自动将其发送出去。内存到内存模式这是DMA能力的一个扩展不涉及外设。可以用于高效地拷贝、填充大块内存数据比如初始化显示缓冲区或进行数据块搬移速度远高于CPU用循环操作。2.2 关键工作模式普通模式 vs 循环模式这是DMA配置中最容易混淆也最重要的概念之一直接决定了数据搬运的持续性和缓冲区的管理方式。普通模式Normal Mode行为DMA在完成设定的数据数量Data Number传输后会自动停止并产生一个传输完成中断如HAL_DMA_XferCpltCallback。应用场景发送或接收一段固定长度的数据。例如通过串口发送一条已知长度的指令报文或者接收一个固定格式的数据包。注意事项在普通模式下一次传输完成后DMA通道会被禁用。如果你想再次启动传输必须重新配置数据数量并启动DMA通道。对于持续不断的流式数据如音频流、持续传感器数据普通模式就不太合适因为你需要不断重启DMA会产生间隙。循环模式Circular Mode行为DMA在完成设定的数据数量传输后不会停止而是自动将源/目标地址重置为初始值数据数量重置为初始值然后重新开始新一轮传输如此周而复始。应用场景处理连续、不间断的数据流。这是实现“环形缓冲区Ring Buffer”或“双缓冲Double Buffer”的硬件基础。应用实例串口DMA接收未知长度的数据。你可以设置一个足够大的数组作为接收缓冲区并启用循环模式。DMA会不停地往这个数组里填数据写满后就从头开始覆盖。你的主程序或中断服务程序只需要定期去检查这个缓冲区里有多少新数据通过计算DMA当前写入位置然后取走处理即可。这完美解决了“串口DMA接收完成中断只触发一次”的困扰因为循环模式根本没有“完成”的概念只有“半满”或“溢出”的事件需要关注。2.3 中断与标志位如何知道DMA干完活了DMA控制器在工作过程中会产生多种事件并通过中断或标志位通知CPU。合理利用这些事件是高效编程的关键。传输完成中断Transfer Complete Interrupt当DMA完成了全部Data Number个数据的传输时触发。在普通模式下这标志着一次搬运任务结束在循环模式下这标志着完成了一轮缓冲区填充。半传输完成中断Half Transfer Complete Interrupt当DMA完成了一半Data Number个数据的传输时触发。这是实现双缓冲Double Buffer技术的核心机制。双缓冲原理假设你设置了一个包含N个元素的数组作为DMA目标并开启了半传输和传输完成中断。DMA开始搬运。当它搬完前N/2个数据填满了缓冲区前半部分时触发半传输中断。此时CPU可以安全地处理这前半部分的数据因为DMA正在向后半部分写入。当DMA搬完后N/2个数据填满了整个缓冲区时触发传输完成中断。此时CPU可以处理后半部分数据而DMA在循环模式下会回头从数组开头重新写入覆盖前半部分。如此交替CPU和DMA各自操作不同的半区实现了无锁、高效的数据流水线避免了处理数据时和DMA写入的数据冲突。这在处理I2S音频流、摄像头数据时非常有用。传输错误中断Transfer Error Interrupt当发生配置错误如访问非法地址时触发。直接模式错误中断Direct Mode Error Interrupt在某些特定配置下发生错误时触发。实操心得在STM32的HAL库中使用HAL_DMA_Start_IT()启动带中断的DMA传输后需要重写对应的回调函数如HAL_DMA_XferCpltCallback来处理这些事件。务必注意中断的优先级设置尤其是当DMA服务于高速外设如SPI Flash时如果DMA中断被低优先级任务长时间阻塞可能导致数据丢失或外设错误。3. 典型场景实战串口DMA收发全解析串口通信是嵌入式开发中最基础也最常用的功能结合DMA可以极大解放CPU。下面我们以STM32为例拆解USART DMA收发的完整流程和关键细节。3.1 串口DMA发送如何可靠地发送一长串数据目标通过DMA将内存中的一个字符串或数据包发送出去。配置步骤以STM32CubeMX/HAL库为例CubeMX图形配置使能USART和对应的DMA通道Stream/Channel。在DMA设置中为USART_TX添加一个DMA请求。模式选择Normal普通模式发送完即停。方向Memory To Peripheral。数据宽度根据需求选择通常外设USART-DR是Byte内存侧也选Byte。关闭循环模式。代码实现// 1. 定义发送缓冲区 uint8_t tx_buffer[] Hello, DMA!\r\n; uint16_t tx_len sizeof(tx_buffer) - 1; // 去掉字符串结尾的\0 // 2. 启动DMA发送 HAL_UART_Transmit_DMA(huart1, tx_buffer, tx_len); // 3. 在传输完成中断回调函数中处理后续事宜 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if(huart-Instance USART1) { // 发送完成可以点亮一个LED或者准备下一包数据 // 注意在普通模式下此时DMA已停止缓冲区可以安全复用 } }关键问题与技巧如何判断串口DMA发送真正完成这是一个高频问题。HAL_UART_TxCpltCallback回调表示DMA已经把最后一个字节从内存搬到了USART的数据寄存器DR。但是USART的移位寄存器可能还在发送这最后一个字节如果此时你立即关闭串口、进入低功耗模式或复用TX引脚会导致最后一个字节发送不完整。解决方案在传输完成回调中不要立即进行敏感操作。可以等待USART状态寄存器中的TCTransmission Complete标志置位。HAL库提供了HAL_UART_GetState()函数但更直接的方法是使用while((huart1.Instance-SR USART_SR_TC) 0);进行短暂等待几个时钟周期。更优雅的做法是利用USART本身的TC中断。发送过程中能否修改缓冲区在DMA传输进行中绝对不能修改源地址指向的缓冲区内容否则发送出去的数据将是不可预知的。如果需要发送动态数据应确保在启动DMA前准备好完整数据或者使用双缓冲机制。3.2 串口DMA接收如何高效接收不定长数据这是DMA应用的难点和重点。核心思路是循环模式 空闲中断Idle Interrupt。配置步骤CubeMX图形配置使能USART和对应的DMA通道Stream/Channel。在DMA设置中为USART_RX添加一个DMA请求。模式选择Circular循环模式。方向Peripheral To Memory。数据宽度Byte。开启DMA传输完成中断用于计算数据非必需。在USART配置中开启空闲中断Idle Interrupt。代码实现原理// 定义接收缓冲区 #define RX_BUFFER_SIZE 256 uint8_t rx_buffer[RX_BUFFER_SIZE]; // 在main初始化部分启动DMA接收 HAL_UART_Receive_DMA(huart1, rx_buffer, RX_BUFFER_SIZE); // 注意这里长度是缓冲区总大小因为工作在循环模式 // 全局变量记录状态 volatile uint16_t dma_last_pos RX_BUFFER_SIZE; // 上次读取的位置空闲中断处理 串口空闲中断在检测到RX线上超过一个字符帧的时间如10个位时间没有新数据时触发。这是判断“一帧数据结束”的利器。// 在stm32fxx_it.c的中断服务函数中 void USART1_IRQHandler(void) { // ... 其他中断处理 if(__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE) ! RESET) { __HAL_UART_CLEAR_IDLE_FLAG(huart1); // 清除空闲中断标志 // 计算本次接收到的数据长度 uint16_t current_pos RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx); // __HAL_DMA_GET_COUNTER 返回DMA通道中剩余未传输的数据量 // 用总大小减去剩余量得到当前已写入的位置 uint16_t receive_len 0; if(current_pos dma_last_pos) { receive_len current_pos - dma_last_pos; } else { // 缓冲区已卷绕wrap around receive_len RX_BUFFER_SIZE - dma_last_pos current_pos; } if(receive_len 0) { // 处理从 rx_buffer[dma_last_pos] 开始的 receive_len 个字节数据 process_received_data(rx_buffer[dma_last_pos], receive_len); } // 更新上次读取位置 dma_last_pos current_pos; } // ... 其他中断处理 }避坑指南缓冲区大小循环缓冲区的大小要足够容纳两帧最大可能的数据包防止在处理一帧数据时新来的数据覆盖了还未处理完的旧数据。数据覆盖上述代码中如果process_received_data函数处理速度太慢而新数据源源不断dma_last_pos可能会被DMA的当前写入位置追上导致部分新数据被当作“未处理”数据重复处理或者更糟未处理的数据被覆盖。在高流量场景下需要设计更健壮的流控或使用双缓冲。DMA计数器复位在极少数情况下如通信错误后恢复可能需要停止并重启DMA接收。重启前务必先调用__HAL_DMA_GET_COUNTER获取并保存当前位置然后调用HAL_UART_DMAStop()再重新配置缓冲区地址和数据长度最后调用HAL_UART_Receive_DMA()重启。顺序错误可能导致数据混乱。4. 进阶应用与疑难杂症排查掌握了基础用法后面对更复杂的系统需求你需要了解DMA的一些高级特性和常见问题。4.1 多外设共享DMA通道像STM32F103这类资源紧张的芯片多个外设如USART1_TX, USART2_RX, SPI1_TX可能共享同一个DMA通道Channel。这意味着同一时间只能有一个外设使用该通道。管理策略分时复用在代码层面严格管理确保在使用一个外设的DMA前该通道没有被其他外设占用。使用前先HAL_DMA_Stop()停止可能存在的传输然后重新配置源/目标地址、数据量等参数再为新外设启动DMA。状态标志为每个使用共享DMA通道的外设设计一个状态标志如busy在启动DMA前检查。硬件限制核查仔细查阅芯片参考手册的DMA请求映射表明确哪些外设固定映射到哪些通道避免冲突设计。4.2 内存到内存传输这是DMA一个非常高效的功能常用于初始化大块内存如清零显存或拷贝数据。// 使用HAL库进行内存到内存拷贝 uint32_t src[100], dst[100]; // ... 填充src数据 HAL_DMA_Start(hdma_memtomem_dma_channel, (uint32_t)src, (uint32_t)dst, 100); // 等待传输完成 HAL_DMA_PollForTransfer(hdma_memtomem_dma_channel, HAL_DMA_FULL_TRANSFER, HAL_MAX_DELAY);注意内存到内存传输不涉及外设请求通常需要手动启动并等待完成。它的优先级可能低于外设触发的DMA请求。4.3 DMA传输中的常见问题与排查技巧问题DMA传输不启动或者只传输一部分数据。检查1外设时钟和DMA时钟是否使能这是最容易被忽略的。在CubeMX生成的代码中通常会帮你使能但如果是手动注册代码务必确认__HAL_RCC_DMAx_CLK_ENABLE()和对应外设的时钟已开启。检查2DMA通道与外设的映射是否正确对照数据手册确认你使用的DMA通道Stream确实支持该外设的请求。检查3外设是否已配置为产生DMA请求例如对于USART发送需要使能USART_CR3寄存器中的DMAT位对于接收需要使能DMA_R位。HAL库的HAL_UART_Transmit_DMA()函数内部会设置这些位。检查4缓冲区地址对齐问题如果源/目标地址是内存地址且数据宽度设置为字32位那么该地址最好是4字节对齐的。非对齐访问在某些芯片上可能导致传输错误或效率低下。可以使用__ALIGNED(4)关键字来定义数组。问题DMA传输完成中断不触发。检查1DMA中断是否使能使用HAL_DMA_Start_IT()或HAL_UART_Transmit_DMA()其内部调用了带中断的启动才能产生中断。检查2NVIC中断控制器是否配置正确在CubeMX中需要勾选对应的DMA通道全局中断并设置优先级。在代码中对应中断服务函数如DMA1_Channel1_IRQHandler需要正确实现并调用HAL_DMA_IRQHandler()。检查3传输完成中断标志TCIF是否被清除在中断服务函数中HAL库会处理标志位的清除。但如果自己编写中断服务程序务必在退出前清除相应的标志位__HAL_DMA_CLEAR_FLAG否则会连续进入中断。问题数据错乱收到或发送的数据不对。检查1数据宽度Data Width是否匹配这是最常见的原因。确保内存端和外设端的数据宽度设置一致。例如从32位宽的缓冲区发送到8位宽的串口如果DMA配置为每次传输32位那么一次操作会搬4个字节到串口数据寄存器但串口硬件只会取最低的8位导致数据顺序错乱。此时应配置为每次传输8位或者使能DMA的“字节打包”功能如果支持。检查2字节序Endianness问题当数据宽度大于8位时需要注意CPU的字节序大端/小端。DMA通常按照内存中的字节顺序原样搬运。如果你用DMA发送一个16位的整数0x1234在内存中可能是0x34低地址,0x12高地址小端模式。DMA会先搬0x34到外设再搬0x12。你需要确认接收端是否能正确解析这种顺序。检查3缓冲区在传输过程中被意外修改确保在DMA传输期间没有其他代码包括中断修改源或目标缓冲区。可以考虑使用const定义发送缓冲区或将缓冲区定义为volatile并配合关中断等保护措施。问题使用双缓冲时数据出现重叠或丢失。检查中断处理逻辑是否正确在半传输中断HT中你处理的是缓冲区的前半部分在传输完成中断TC中你处理的是后半部分。两个中断的处理函数必须非常快并且要确保在处理一个半区时DMA正在写入另一个半区。绝对不能在中断处理函数中进行长时间的操作如打印大量日志否则DMA可能已经写满了当前半区并开始覆盖你正在处理的半区。如果处理耗时应将数据拷贝到另一个临时缓冲区然后快速退出中断在主循环中处理。5. 不同芯片平台与生态下的DMA应用要点DMA是一个通用概念但在不同厂商、不同架构的MCU上其具体实现和API各有不同。STM32 (HAL/LL库)生态最完善CubeMX图形化配置极大简化了初始化。重点掌握HAL_DMA_Start(),HAL_DMA_Start_IT(), 以及各种Callback函数。注意LL库提供更底层的寄存器操作对时序要求苛刻时可能用到。GD32与STM32高度兼容但仍有细微差异。例如在GD32中使能USART的DMA接收可能需要额外操作一些寄存器位。需要仔细阅读对应型号的参考手册。N32 (国民技术)同样采用ARM Cortex-M内核DMA控制器设计与STM32类似但外设寄存器地址和位定义不同需使用其官方提供的SDK。Zynq (FPGAARM)这里的DMA通常指AXI DMA IP核用于在FPGA可编程逻辑PL和ARM处理系统PS的内存之间进行高速数据传输。配置更为复杂涉及Linux驱动、设备树Device Tree、用户空间API如read/write或XDMA驱动。核心是理解Scatter-GatherSG模式它允许DMA传输不连续的内存块。ESP32其DMA通常与I2S、SPI等外设绑定用于音频、显示屏数据传输。例如驱动WS2812/WS2811灯带时会利用I2S的DMA模式产生精确的PWM时序信号。FreeRTOS与DMA在RTOS环境下使用DMA需要特别注意资源共享和临界区保护。例如多个任务可能都想使用同一个DMA通道进行内存拷贝。通常使用互斥锁Mutex来保护DMA通道的访问。另外DMA传输完成中断服务程序ISR中应使用FromISR版本的RTOS API如xSemaphoreGiveFromISR来通知任务而不是在ISR中进行复杂处理。工具与调试技巧逻辑分析仪是调试DMA相关时序问题的终极利器。你可以同时抓取外设的时钟线、数据线以及DMA相关的控制信号或GPIO翻转信号直观地看到DMA请求、响应和数据搬运的时序关系。调试器与内存观察在IDE如Keil, IAR, STM32CubeIDE中设置断点观察DMA控制寄存器的值如CNDTR-剩余数据计数、源/目标地址寄存器的变化以及缓冲区内存内容的变化。GPIO翻转在DMA传输开始、中断触发等关键位置用一条空闲的GPIO口线进行电平翻转然后用示波器观察可以粗略测量代码执行时间和DMA传输耗时。DMA不是一个一学就会、一用就灵的黑盒子。它需要你清楚地知道数据流从哪里来、到哪里去、以何种形式搬运、何时开始、何时结束。每一次成功的DMA应用都是你对芯片架构、外设特性和系统数据流理解的一次深化。从最简单的串口收发开始逐步尝试SPI Flash读写、ADC多通道扫描、LCD刷新你会逐渐体会到这个“沉默的搬运工”带来的巨大性能红利。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价