尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

嵌入式DMA技术详解:从原理到实战,串口、I2S、ADC应用全解析

嵌入式DMA技术详解:从原理到实战,串口、I2S、ADC应用全解析 1. DMA到底是什么为什么每个嵌入式工程师都绕不开它如果你在玩单片机尤其是像STM32、GD32这类ARM Cortex-M内核的芯片那么DMADirect Memory Access直接存储器访问绝对是一个你迟早要打交道的“老朋友”。我第一次接触DMA是在一个串口通信项目里当时需要高速、不间断地接收一长串传感器数据。用传统的中断方式每收到一个字节就进一次中断CPU忙得不可开交主程序卡顿明显数据还偶尔会丢。前辈甩过来一句“上DMA吧。” 从那时起我才真正体会到把CPU从繁琐的搬运数据工作中解放出来是多么爽快的一件事。简单来说DMA就是一个硬件搬运工。在没有DMA的世界里CPU是公司里唯一的员工既要处理核心业务逻辑你的应用程序又要亲自去仓库外设寄存器取货、打包、再送到另一个仓库内存。比如串口收到一个字节CPU需要暂停手头的计算去串口数据寄存器把这个字节读出来再存到数组里。这个过程虽然快但架不住活多——高速AD采样、摄像头数据、网络包、SD卡读写这些数据量一大CPU就彻底沦为“搬运工”核心业务反而没时间做了。DMA的出现就是给CPU配了一个专门的“物流部门”。你只需要告诉DMA货在哪里源地址要送到哪里去目标地址要送多少数据量然后就可以挥挥手让DMA去干了。在此期间CPU可以继续执行你的主程序处理更复杂的算法和逻辑。只有当DMA完成了一整批货物的搬运比如接收完1000个字节它才会举手报告“老板活干完了”触发一个传输完成中断。CPU只需要在这个中断里处理这批完整的数据即可效率提升不是一点半点。现在你搜索“串口DMA”、“SPI DMA”、“I2S DMA”会发现无数讨论和教程这恰恰说明了它的核心地位。从简单的串口收发到复杂的音频流I2S、图像传输、以太网通信DMA都是实现高性能、低功耗、实时性系统的基石技术。不理解DMA很多嵌入式性能优化就无从谈起。2. DMA核心工作机制与关键概念拆解要驾驭DMA不能只停留在“它是个搬运工”的模糊概念上必须深入其内部的工作机制。不同厂商的DMA控制器如STM32的DMAZynq的AXI DMAPL330 DMA架构虽有差异但核心思想相通。2.1 DMA传输的“三要素”源、目标和搬运量任何一次DMA传输都始于对这三个基本要素的配置源地址 (Source Address)数据从哪里来。可以是外设的数据寄存器地址如USART1-DR也可以是内存中的某个数组地址sourceBuffer[0]。目标地址 (Destination Address)数据到哪里去。同样可以是外设寄存器或内存地址。数据数量 (Data Number)要连续搬运多少个数据单元。这里的数据单元大小可以是字节8位、半字16位或字32位具体取决于外设数据宽度和你的配置。根据源和目标的类型DMA传输分为三种经典模式内存到外设 (Memory-to-Peripheral)最常见的是发送数据。例如将内存中一个字符串通过DMA搬运到串口的发送数据寄存器自动逐个字节发出。你配置好源字符串地址、目标串口DR寄存器和数量后启动DMA和串口发送就可以不管了。外设到内存 (Peripheral-to-Memory)最常见的是接收数据。例如配置DMA将串口接收数据寄存器的内容自动搬运到内存的一个接收缓冲区。串口每收到一个字节硬件就会触发一次DMA请求DMA控制器无需CPU干预就把数据搬走了。内存到内存 (Memory-to-Memory)这是纯内部数据搬运不涉及外设。例如需要快速拷贝或初始化一大段内存区域。STM32的DMA1通常不支持此模式而DMA2支持这在需要高效处理数据块时非常有用。2.2 数据流、通道与仲裁DMA的“交通管理系统”一个MCU内部往往有多个DMA控制器如DMA1, DMA2每个控制器下有多个数据流Stream或通道Channel。这是DMA资源管理的核心。通道 (Channel)每个通道通常与一个特定的外设请求线绑定。例如STM32F4中USART1的TX请求可能映射到DMA2的Stream7 Channel4。这意味着当USART1需要发送数据时它会通过这条专用的“请求线”向DMA2的Stream7发出搬运请求。你需要根据芯片数据手册的表格正确配置通道号DMA控制器才知道该响应哪个外设的呼叫。数据流 (Stream)你可以把每个Stream看作一条独立的“传输流水线”。每个Stream在同一时间只能服务于一个传输任务即一组源、目标、数量的配置。多个Stream之间可以并行工作。仲裁器 (Arbiter)当多个Stream同时向DMA控制器发出请求时谁先谁后这就由仲裁器根据优先级通常可配置为固定优先级或循环优先级来决定。高优先级的传输如实时音频数据能抢占低优先级的传输如内存拷贝确保关键任务的实时性。2.3 指针递增与循环模式让搬运更智能指针递增默认情况下DMA每搬运完一个数据单元源地址和目标地址会自动递增到下一个单元位置。这对于搬运连续内存数组是完美的。但在“外设到内存”传输中源地址是外设寄存器如USART-DR这个地址是固定的不应该递增。因此在配置时必须正确设置外设地址不递增内存地址递增。循环模式 (Circular Mode)这是实现“双缓冲”或“连续流”传输的关键。在此模式下当DMA搬运完指定数量的数据后不会停止而是自动将地址指针重新指向缓冲区开头并重新加载传输数量开始新一轮搬运。这对于需要不间断收发音频I2S DMA、摄像头数据等场景至关重要。结合半传输完成HT和传输完成TC中断可以实现“乒乓缓冲”CPU处理前半段数据时DMA正在填充后半段实现无缝数据流。注意开启循环模式时通常要配合使用双缓冲或确保你的数据处理速度能跟上DMA的填充速度否则新数据会覆盖未处理的数据造成丢失。2.4 中断事件DMA与CPU的“协同信号”DMA虽然独立工作但最终需要与CPU协同。这是通过中断实现的主要关注三个标志传输完成中断 (Transfer Complete, TC)当配置的数据量全部搬运完毕时触发。这是最常用的中断用于通知CPU“一批活干完了快来处理”。半传输完成中断 (Half Transfer, HT)当搬运完一半数据量时触发。这是实现双缓冲机制的核心。例如你设置了一个1000字节的缓冲区当DMA搬运完前500字节时触发HT中断CPU可以安全地处理这前半部分数据同时DMA继续填充后半部分。当TC中断触发时CPU再去处理后500字节。如此循环数据永不间断。传输错误中断 (Transfer Error, TE)当发生配置错误、访问冲突等问题时触发。一个健壮的程序必须处理错误中断进行错误恢复或报告。3. 实战精讲串口DMA收发全流程与避坑指南理论说再多不如一行代码。我们以STM32的HAL库为例拆解一个完整的串口UART DMA收发工程这里面包含了90%你会遇到的坑和技巧。3.1 硬件与软件环境准备假设我们使用STM32F407USART1用于通信TX用DMA2 Stream7 Channel4RX用DMA2 Stream5 Channel4。使用STM32CubeMX进行初始化配置是最高效的方式。CubeMX关键配置步骤使能USART1为异步模式设置波特率等参数。在DMA Settings标签页为USART1_TX添加DMA请求。方向选择Memory To Peripheral模式选择Normal单次或Circular循环优先级根据系统需求设定。内存地址递增外设地址不递增。数据宽度通常都选Byte与串口数据宽度匹配。为USART1_RX添加DMA请求。方向选择Peripheral To Memory模式强烈建议在连续接收场景下使用Circular。同样内存地址递增外设地址不递增。在NVIC Settings中使能USART1的全局中断用于处理IDLE中断等以及对应DMA Stream的传输完成中断TC和半传输完成中断HT如果用到双缓冲。生成代码。3.2 发送流程从启动到完成的细节生成的代码会初始化好DMA和串口但发送流程需要我们自己编写。// 1. 定义发送缓冲区 uint8_t txBuffer[] Hello, DMA!\r\n; uint16_t txLen strlen((char*)txBuffer); // 2. 确保上一次DMA传输已完成重要 while(HAL_DMA_GetState(hdma_usart1_tx) HAL_DMA_STATE_BUSY) { // 可以加入超时机制 } // 3. 启动DMA传输 if(HAL_UART_Transmit_DMA(huart1, txBuffer, txLen) ! HAL_OK) { // 错误处理 } // 4. 此时CPU已被释放可以执行其他任务 doSomethingElse(); // 5. 如何知道发送完成 // 方法一轮询标志位非阻塞但占用CPU while(__HAL_DMA_GET_FLAG(hdma_usart1_tx, DMA_FLAG_TCIF3_7) RESET) { // 等待发送完成标志Stream7对应TCIF7标志位 } __HAL_DMA_CLEAR_FLAG(hdma_usart1_tx, DMA_FLAG_TCIF3_7); // 清除标志 // 方法二使用发送完成回调函数推荐异步通知 // 需要在主程序中重写 HAL_UART_TxCpltCallback 函数 void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if(huart-Instance USART1) { // 发送完成可以准备下一包数据或通知任务 sendCompleteFlag 1; } }发送关键点启动前检查状态这是新手最常踩的坑。如果上一次DMA传输还没结束就再次启动会导致DMA控制器状态混乱数据发送异常。务必在HAL_UART_Transmit_DMA前检查DMA状态或等待上一次回调完成。缓冲区生命周期DMA传输是异步的。在DMA发送过程中必须确保txBuffer数组所在的内存区域有效且内容不变。如果txBuffer是局部变量函数返回后栈内存可能被覆盖导致发送乱码。通常将发送缓冲区定义为全局变量或静态变量。“发送完成”的判断串口DMA发送完成中断指的是DMA已经把最后一个字节从内存搬运到了串口的发送数据寄存器TDR。但这不意味着最后一个字节已经从串口TX引脚发出去了串口自己的移位寄存器还需要时间把字节一位一位地发出去。对于判断一帧数据是否完全从物理引脚发出通常不需要特别处理因为时间很短。但在需要严格同步如关闭串口、切换引脚模式时可以额外等待一下串口的“传输完成”TC标志位USART_FLAG_TC。3.3 接收流程IDLE中断 环形DMA的黄金组合单纯使用DMA接收你只知道收到了数据但不知道一帧数据何时结束。对于不定长数据IDLE中断串口总线空闲中断是完美搭档。// 1. 定义一个大容量的环形接收缓冲区 #define RX_BUFFER_SIZE 1024 uint8_t rxBuffer[RX_BUFFER_SIZE]; // 2. 在main初始化后启动串口DMA接收循环模式 HAL_UART_Receive_DMA(huart1, rxBuffer, RX_BUFFER_SIZE); // 3. 使能IDLE中断 __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE); // 4. 在USART1的全局中断服务函数中处理IDLE中断 void USART1_IRQHandler(void) { HAL_UART_IRQHandler(huart1); // 调用HAL库中断处理函数 } // 5. 重写HAL库的IDLE中断回调函数 void HAL_UART_IdleCallback(UART_HandleTypeDef *huart) { if(huart-Instance USART1) { // 临时关闭DMA防止在计算过程中数据被修改 __HAL_DMA_DISABLE(huart-hdmarx); // 计算本次接收到的数据长度 // 公式总缓存大小 - DMA寄存器中剩余未传输的数据量 uint16_t remainData __HAL_DMA_GET_COUNTER(huart-hdmarx); uint16_t receivedLen RX_BUFFER_SIZE - remainData; // 获取本次数据的起始位置需要考虑缓冲区环回 static uint16_t lastPos 0; // 上次处理到的位置 uint16_t currentPos RX_BUFFER_SIZE - remainData; // 当前DMA写指针位置 // 处理从 lastPos 到 currentPos 之间的数据 processReceivedData(rxBuffer[lastPos], receivedLen); // 更新上次处理位置 lastPos currentPos; // 重新使能DMA __HAL_DMA_ENABLE(huart-hdmarx); } }接收关键点IDLE中断原理当串口接收线上超过一个字节的时间具体取决于波特率没有新的数据时硬件就会产生IDLE中断。这标志着一帧数据的结束。计算接收长度这是核心技巧。在循环DMA模式下DMA的CNDTR寄存器保存着剩余未传输的数据量。用缓冲区总大小减去这个剩余量就得到了已经搬运到内存的数据量也就是自DMA启动以来累计接收的字节数。通过记录上次处理时的累计接收量就能算出本次IDLE期间新收到的数据长度。处理环回当DMA指针到达缓冲区末尾时会自动环回到开头。上面的currentPos计算和lastPos的比较需要做环回处理逻辑会稍复杂一些。一个更稳健的方法是不直接计算长度和位置而是在IDLE中断里将当前累计接收长度保存到一个变量中在主循环或任务中再与上一次的长度比较、计算差值并处理数据这样中断服务函数执行时间更短。双缓冲中断如果接收数据量非常稳定也可以使用DMA的半传输完成HT和传输完成TC中断来实现双缓冲逻辑更清晰实时性更高。4. 进阶应用与疑难杂症排查掌握了基础收发我们来看看更复杂的场景和那些让人头疼的问题。4.1 多串口共享DMA资源像“STM32三个串口共用同一个DMA”这种需求本质上是DMA通道/Stream的复用。一个DMA Stream在同一时间只能服务一个外设。所以“共用”不是同时使用而是分时复用。实现策略静态分配顺序使用为每个串口的TX/RX分配不同的DMA Stream。这是最理想的情况互不干扰。动态分配使用前配置如果Stream数量不足只能共享。那么必须在启动一个串口的DMA传输前确保该Stream当前是空闲的HAL_DMA_GetState()返回READY然后重新配置该Stream的通道对应新的外设、源/目标地址、数据量等所有参数再启动。关键点在重新配置前必须调用HAL_DMA_DeInit()或__HAL_DMA_RESET_HANDLE_STATE()来彻底重置DMA Stream的HAL库状态机否则配置可能不生效。使用内存到内存模式中转如果某个外设没有可用的专用DMA Stream可以考虑先用一个内存到内存的DMA Stream将数据从一个缓冲区搬到另一个缓冲区再配合中断由CPU将数据交给外设。但这牺牲了部分效率。4.2 DMA传输“只进一次中断”问题在STM32F407 IIS DMA双缓冲或类似场景中有时会遇到配置了双缓冲使能了HT和TC中断但DMA只进入一次中断通常是TC后就停止了。排查思路检查DMA模式双缓冲必须工作在循环模式Circular。如果模式是NormalDMA在完成一次传输无论是HT还是TC后就会自动关闭不会再次触发。检查缓冲区地址配置在双缓冲模式下需要正确设置M0AR内存0地址寄存器和M1AR内存1地址寄存器。在HAL库中使用HAL_DMAEx_MultiBufferStart_IT()等函数来启动。如果只配置了一个内存地址双缓冲机制不会生效。检查中断使能确认在CubeMX和代码中不仅使能了DMA全局中断__HAL_DMA_ENABLE_IT(hdma, DMA_IT_HT | DMA_IT_TC)也正确编写了对应的中断回调函数HAL_DMA_HT_Callback,HAL_DMA_TC_Callback。清除中断标志位在中断服务函数或回调函数中是否无意中清除了所有的DMA中断标志或者HAL库的默认处理有问题有时需要单步调试查看中断触发后相关标志位的状态。4.3 内存与缓存一致性问题Cortex-M7等带Cache内核这是高阶坑在STM32F7/H7等使用Cortex-M7内核的芯片上尤其突出。CPU和DMA都访问同一块内存缓冲区但CPU有数据缓存D-Cache。这会导致CPU写DMA读CPU修改了缓冲区数据但数据可能还留在Cache里没写回内存Write-Back策略。此时DMA直接从内存读走的就是旧数据。DMA写CPU读DMA将外设数据直接写入内存但CPU的Cache里可能还缓存着该地址的旧数据。CPU直接读到的就是Cache里的旧数据。解决方案使用非缓存内存通过MPU内存保护单元将DMA缓冲区所在的存储区如SRAM配置为Non-Cacheable。这是最根本的解决办法。手动维护缓存一致性在CPU操作DMA缓冲区前后使用SCB_CleanDCache_by_Addr()清理确保CPU写的数据刷到内存和SCB_InvalidateDCache_by_Addr()无效化确保CPU读取最新数据函数来手动维护缓存。这需要精确计算缓冲区地址和大小。HAL库的封装HAL库提供了一些封装函数如HAL_DMAEx_*系列函数内部可能包含了缓存维护操作。仔细阅读HAL库的源码和注释。4.4 常见问题速查表问题现象可能原因排查方向与解决方法DMA不启动数据不传输1. DMA或外设时钟未使能。2. DMA Stream未使能EN位。3. 源/目标地址配置错误特别是外设寄存器地址。4. 传输数据量NDTR为0。1. 检查__HAL_RCC_DMAx_CLK_ENABLE()和对应外设时钟。2. 单步调试查看DMA控制寄存器CR的EN位。3. 核对数据手册中的外设寄存器地址。4. 检查NDTR寄存器或HAL库传入的Size参数。数据发送/接收错乱、重复或丢失1. 内存缓冲区溢出接收或内容被修改发送。2. 指针递增配置错误外设地址误递增。3. 数据宽度不匹配外设8位DMA配置16位。4. 中断嵌套或优先级问题导致数据处理不及时。1. 确保缓冲区足够大生命周期正确。2. 检查CubeMX或代码中SrcInc/DstInc配置。3. 外设和DMA的数据宽度必须匹配。4. 优化中断服务函数或将数据处理移到主循环。只能传输一次无法连续传输1. DMA模式配置为Normal而非Circular。2. 传输完成中断中未重新启动DMA。3. DMA传输完成标志未清除导致状态机卡住。1. 连续传输需使用循环模式。2. 在TC回调函数中再次调用启动函数对于Normal模式。3. 检查并清除TCIF等标志位。进入DMA中断后卡死或频繁进入1. 中断标志未正确清除。2. 中断服务函数IRQHandler未正确调用HAL库处理函数。3. 中断优先级配置冲突如SysTick与DMA中断。1. 使用__HAL_DMA_CLEAR_FLAG()清除对应标志。2. 确保在stm32f4xx_it.c中调用了HAL_DMA_IRQHandler()。3. 合理配置NVIC优先级分组和子优先级。5. 不同场景下的DMA应用模式选型DMA的应用远不止串口。根据不同的数据流特性和性能要求需要选择不同的应用模式。5.1 低速不定长数据串口UART DMA IDLE中断正如前面详细讲解的这是最经典的组合。适用于Modbus、自定义串口协议、GPS模块、蓝牙AT指令等场景。核心是利用IDLE中断检测帧结束利用循环DMA实现零丢失接收把CPU占用率降到最低。实操心得对于高波特率如921600下的不定长数据缓冲区要设得足够大并且IDLE中断处理函数一定要快。建议只在中断内设置标志位、计算长度将实际的数据解析、拷贝等耗时操作放到主循环或RTOS任务中。5.2 高速等长数据流I2S/Audio DMA双缓冲播放或录制音频是典型的连续、高速、等长数据流场景。以I2S DAC播放为例模式必须使用循环模式和双缓冲。流程初始化两个音频缓冲区BufferA和BufferB。启动DMA将BufferA的数据发送给I2S DAC。当DMA发送完BufferA触发TC中断时I2S DAC正在播放BufferA的数据此时DMA会自动切换到BufferB继续发送。同时在TC中断回调函数中CPU应立刻用新的音频数据填充已经播放完毕的BufferA。当DMA发送完BufferB触发下一个TC中断时它又切换回BufferA发送而CPU则去填充BufferB。关键CPU填充缓冲区的速度必须快于DMA播放的速度否则会出现音频卡顿欠载。这通常需要精确计算缓冲区大小和音频帧率。5.3 块设备读写SPI/I2C for Flash/SD Card DMA读写W25Q128这类SPI Flash或SD卡数据量大且多为块操作。使用DMA可以极大提升吞吐量。发送命令阶段通常用CPU查询方式发送几个字节的命令和地址因为指令短且需要严格时序。数据传输阶段在发送读命令如0x03和地址后后续从Flash读出的数据流或者向Flash写入的数据块全部交给DMA搬运。配置为SPI的RX/TX DMA请求。注意事项SPI的时钟极性和相位必须与从设备严格匹配。DMA传输期间要确保SPI时钟稳定。对于写入操作在DMA传输完成后还需要查询Flash的状态寄存器等待内部编程真正完成。5.4 模拟信号处理ADC扫描模式 DMA多通道ADC采集是DMA的绝佳应用。将ADC配置为扫描模式连续转换多个通道并使能DMA请求。每转换完一个通道或所有通道ADC都会触发DMA将结果寄存器ADC-DR的值搬运到内存中的一个数组里。CPU只需要定期例如通过定时器去处理这个已经排列好的数组数据即可实现了多通道同步、无遗漏采集。配置技巧对于STM32注意ADC的DMA模式。有“单次”和“循环”模式。在循环模式下DMA会持续不断地搬运ADC也持续转换适合实时监控。此时结合DMA的半传输和传输完成中断可以实现对ADC数据的实时分块处理。6. 超越MCUFPGA与SoC中的DMA思想DMA的思想在更复杂的系统中无处不在。像“FPGA实现DMA”或“Zynq AXI DMA”其核心逻辑与单片机一脉相承但规模和控制方式不同。FPGA中的DMA通常使用软核如NIOS II配合Avalon或AXI总线来实现。你需要设计或调用DMA控制器IP核它包含读主端口从源地址读、写主端口向目标地址写和一个控制从端口供CPU配置。FPGA的灵活性在于你可以定制DMA的触发源不限于外设可以是自定义逻辑信号、传输规则复杂的数据打包、解包和总线位宽实现极高的吞吐量和定制化数据流处理。SoC中的AXI DMA如Zynq这是连接可编程逻辑PL和处理系统PS的高速数据通道。AXI DMA IP核负责在PS的DDR内存和PL端的AXI-Stream接口之间搬运数据。它通常提供更精细的控制如Scatter/GatherSG分散-聚集功能可以将物理上不连续的内存块通过一个描述符链表组织起来让DMA完成一次连续的“虚拟”传输这对操作系统管理内存池非常友好。在FreeRTOS等RTOS下使用AXI DMA关键是要处理好缓存一致性并正确使用中断通知任务进行数据处理。无论是简单的单片机还是复杂的SoCDMA的本质都是“用硬件代替软件以空间换时间将CPU从数据搬运的苦力活中解放出来专注于决策与控制”。吃透它的原理根据具体场景灵活运用和调试是嵌入式工程师向高性能系统设计迈进的必修课。我个人的经验是遇到任何DMA问题回归数据手册的寄存器描述结合调试器观察相关寄存器的实际值比盲目搜索代码更能快速定位根因。
返回列表