
简介围绕DMA技术与DMA控制器的PPT课件系统梳理了直接内存访问的工作原理与硬件实现。DMA技术通过让外设直接与内存交换数据显著降低CPU中转开销课件对这一机制及其应用场景作了清晰阐释。内容从DMA传送方式的特点、完整传送流程到DMAC的基本功能、一般结构和工作方式再到Intel 8237A控制器的特性与编程结构层层递进覆盖单字节传输、块传输、请求传输等关键模式适合计算机组成原理、微机接口技术等课程的学习者作为配套资料。资源为单个pptx文件共46页压缩包约452KB图文结合包含DMA请求/响应时序、操作过程示意图等能直观展示总线控制权切换和地址计数更新。目前已有156人学习下载对于想快速理解DMA机制、熟悉8237A寄存器与工作模式的学生或教师是一份实用的复习与备课材料。 做嵌入式开发到一定阶段DMA是个绕不开的话题。我见过不少工程师调串口、ADC、SPI的时候满脑子都是“数据到了没”“中断里怎么搬数据”结果一个主循环被高频中断挤得七零八落。DMA的全称是Direct Memory Access直接存储器访问核心就一句话不让CPU当数据搬运工。这篇内容围绕DMA技术和DMA控制器展开适合正在用STM32、GD32、ESP32这类MCU做外设数据流处理的嵌入式工程师也适合刚接触DMA控制器原理的硬件开发者。读完你会发现处理好一个DMA通道往往比优化十处中断代码更管用。1. 先搞懂DMA到底在解决什么问题1.1 CPU当搬运工到底有多亏很多人最开始学串口发送都是阻塞式轮询发送寄存器数据不多的时候感觉还行。一旦波特率拉高、数据量变大CPU的时间就开始被大量占用。举一个最常见的例子串口波特率115200理论上每秒能传大约11520字节按每个字节触发一次发送寄存器空或接收寄存器非空中断来算CPU每秒钟要响应上万次中断进中断、读数据、判断标志位、写数据、出中断这一套下来即使每次只有几十个周期累计起来也是非常可观的。这类场景里CPU做的其实就是一个“从A搬到B”的重复劳动完全没有技术含量。但CPU的算力本来应该拿去跑PID、跑算法、刷界面、维护网络协议栈结果全耗在搬运上这就显得非常浪费。DMA控制器的设计初衷就是把这部分劳动接管过来。只要配置好方向、地址、数据长度DMA就会像一台自动传送带一样把外设寄存器和内存之间的数据按指定节奏搬完搬完再通过中断告诉你一声。1.2 DMA的基本工作流程DMA控制器的本质是一个独立于CPU内核的总线主设备它有权访问系统总线能直接读写外设寄存器和内存。整个流程其实很简单外设比如UART、ADC、SPI、定时器发出DMA请求信号DMA控制器收到请求后按照预先配置的源地址、目标地址、传输宽度、传输数量去执行数据搬运。搬运完一个数据单元传输计数寄存器减一直到计数值归零产生传输完成事件再触发中断或事件标志。现实中的MCU里DMA通道往往会和一系列外设请求做映射。比如STM32F1系列有DMA1和DMA2两个控制器每个控制器若干通道每个通道可以响应几个固定的外设请求。这个“映射”关系非常关键通道选不对外设再勤奋地发请求DMA也不理你。我见过很多新手一上来就随便选一个DMA通道结果数据死活不动就是没查外设到DMA通道的映射表。这个在设计初期就一定要查清楚而不是等代码跑不通再去怀疑配置。1.3 DMA和中断、查询方式的定位差异有朋友会问DMA是不是就是中断的替代品严格说不是。中断负责的是“事件通知”DMA负责的是“数据搬运”。两者常常配合使用DMA搬完数据产生完成中断CPU在中断回调里只做数据解析和处理根本不去碰接收寄存器。这样中断服务函数可以写得非常短整个系统的实时性反而更好。查询方式适合低速、低频、数据量小的场合代价是CPU需要不断轮询状态位开销不可控。中断方式适合数据到达频率不高但要求及时响应的场景比如按键、边沿检测。DMA适合数据量大、频率高、周期性机械搬运的场景比如高速串口接收、ADC连续采样、SPI驱动屏幕刷缓存、定时器触发批量波形输出。三者的关系不是互斥的而是根据业务特性混合使用。实际工程里最常见的组合是“DMA搬运完成中断处理数据主循环跑逻辑”。2. DMA控制器的关键结构与参数解析2.1 通道、请求映射与句柄关联稍微深入一点看DMA控制器内部结构会发现它并不是一个“万能传送门”而是由若干通道组成的。每个通道可以理解为一条独立的运输线路有自己的源地址、目标地址、传输计数、配置寄存器。STM32系列中同一个DMA控制器下的不同通道对应固定的外设请求映射。比如某些芯片上USART1_RX只能走DMA1的通道3或通道5具体要看参考手册的DMA请求映射表。HAL库里用DMA_HandleTypeDefDMA句柄来管理这些通道配置同时需要和外设句柄关联。比如用DAC播放音频时HAL_DAC_Start_DMA(hdac, DAC_CHANNEL_1, (uint32_t)audio_buf, len, DAC_ALIGN_12B_R)这个函数就会把DAC句柄和DMA句柄绑定起来。实际配置时DMA句柄负责传输方向和缓冲区地址DAC句柄负责DAC通道、触发源和数据格式。两者是互相配合的两个角色不是同一个对象这个关系值得仔细体会。2.2 传输方向、数据宽度、优先级的选择DMA传输方向常见有三种外设到内存、内存到外设、内存到内存。前两个是外设和内存之间的搬运第三个是把一块内存搬到另一块内存主要用在数据搬移场景比如图像缓冲区翻转、协议栈缓冲整理。内存到内存模式会占用总线CPU访问总线的延迟会变高所以不建议随便用。数据宽度有字节、半字、字三种。选择原则很简单源和目标的宽度要匹配或者至少保证总线能正确拆分组合。比如ADC是12位数据存储在16位寄存器里对应的内存buffer也应该是uint16_t数组DMA宽度就设成半字。如果外设是8位数据寄存器就设成字节。宽度不匹配不只是数据错乱的问题还可能导致对齐异常尤其在开启总线上其他设备的高效传输时很容易出现难以排查的偶发错误。优先级决定多个DMA通道同时请求时谁先被服务。高优先级通道会被优先处理但这不等于高优先级数据更安全。优先级低的通道如果迟迟得不到服务就可能出现缓冲区覆盖。工程上建议把实时性要求最高、缓冲最浅的通道设为高优先级其他通道按数据量和缓冲深度合理分配。2.3 循环模式、双缓冲与连续请求DMA的模式选择是一个常用但容易糊涂的点。Normal模式搬完指定长度就停适合“一次收一包数据”的场景。Circular循环模式搬完指定长度后自动把计数器重置继续从起点搬运适合ADC连续采样、麦克风持续采集、串口不定长流式接收这类需要持续传输的场景。双缓冲机制是针对“边采集边处理”的优化方案。它把缓冲区拆成两块DMA在往A块写数据的时候CPU可以处理B块里的旧数据反过来也一样形成类似乒乓的效果。双缓冲能有效避免“CPU还没处理完DMA又覆盖了数据”的尴尬。在STM32里双缓冲模式和循环模式可以结合使用触发半传输中断与传输完成中断来切换处理A块和B块。DMA continuous requests连续请求这个词在有些参考手册中出现。它描述的是外设请求信号保持有效状态时DMA要不要持续响应。如果外设只有脉冲式的请求DMA按一次请求传一次如果外设是连续请求DMA会一直保持搬运直到传输计数归零或外设请求撤销。这个特性对由定时器持续触发、希望波形无间隙输出的场景很重要配置的时候要留意芯片手册里关于请求模式的定义别按默认理解。2.4 FIFO与突发传输的作用稍微高端一点的DMA控制器比如STM32F4、F7系列还带FIFO和突发burst传输。FIFO相当于数据搬运的缓冲蓄水池允许DMA先攒几个数据再一次性写出去减少总线访问次数。突发传输让DMA在一次总线仲裁中连续搬运若干数据单元而不是每传一次数据都去抢一次总线。对大多数应用来说不使用FIFO、直接采用默认模式已经足够。但在大量数据传输、对CPU总线占用有严格要求的场景合理使用FIFO和突发传输能明显减少总线冲突。FIFO也有个麻烦——配置阈值时要考虑源和目标宽度以及突发长度一旦配错数据会出现错位。我的建议是先跑通默认模式确认真实需求再去优化总线效率不要一上来就整突发传输。3. 高频应用场景实战拆解3.1 串口DMA接收不定长数据空闲中断DMA是经典做法无数人搜过“串口DMA接收不定长数据”核心思路其实是一致的DMA负责把串口接收寄存器里的数据搬到内存缓冲区同时开启串口空闲中断IDLE。当一帧数据发完串口线上空闲了硬件产生IDLE中断CPU在这个中断里读取DMA剩余的传输计数反推出本次实际收到的数据长度然后处理数据、重置DMA等待下一帧。以STM32 HAL库为例常见的框架是这样// 串口空闲中断回调中计算实际收到的数据长度 void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart-Instance USART1) { // Size是本次事件实际接收的字节数 process_packet(uart1_rx_buf, Size); // 重新开启下一次接收 HAL_UARTEx_ReceiveToIdle_DMA(huart1, uart1_rx_buf, UART1_RX_BUF_SIZE); } }这里有几个容易踩的坑。第一个是接收缓冲区分两次配置初始化时调用一次HAL_UARTEx_ReceiveToIdle_DMA设置DMA可以连续接收每一帧处理完之后要重新调用一次因为DMA计数器已经归零必须重置。第二个是在数据处理完成之前不要立刻把缓冲区的数据覆盖掉。如果处理耗时较长就需要双缓冲或者把数据快速拷贝走。第三个是回调里的Size字段不同HAL版本含义不同有的是本次收到的数据量有的是剩余量以你手上的HAL库源码为准。低资源MCU比如PY32F003上同样可以用串口DMA接收空闲中断判断接收结束。这类芯片DMA通道较少但原理完全一致只是库函数接口相对精简需要手动配置DMA中断和串口IDLE标志。我建议先在标准大芯片上跑通逻辑再移植到小资源芯片会省很多排查时间。3.2 ADC多通道多次采样扫描模式DMA正确姿势ADC连续采样最忌讳在主循环里反复调用读取函数。正确做法是把ADC设置成扫描模式开启DMA搬运让采样结果不断被搬运到内存数组里CPU只需要在适当时候去数组里取平均值或有效值。比如STM32HAL工程里我常用这样的配置思路// ADC1 多通道扫描DMA循环搬运 HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, ADC_BUF_LEN);这里的ADC_BUF_LEN是有讲究的。如果每个通道采样一次缓冲区长度等于通道数就够了如果想做多次采样再滤波缓冲区长度是通道数乘以每通道采样次数。DMA传完整段buffer后可以在传输完成中断或半传输中断中处理前半段和后半段数据。实际调试HAL库ADC单通道DMA多次采样时最常见的问题就是只采到第一次数据后面全不动。原因多半是ADC配置里ContinuousConvMode没开启或者DMA被设置成Normal模式搬完一次就停。连续转换DMA循环模式才能保证数据持续刷新。另一个隐蔽问题是对齐方式12位ADC数据可以左对齐或右对齐如果你的buffer是uint16_t但配置成左对齐值可能大得离谱。3.3 SPI DMA与PWMDMA的进阶玩法SPI配合DMA最典型的场景是驱动LCD屏幕。屏幕刷新一帧可能几十KB数据如果CPU逐字节刷写画面基本不用干别的。配置SPI DMA发送后只需要往SPI发送缓冲区地址里塞数据、启动传输、等完成中断CPU就可以去跑界面逻辑。要注意SPI的速率不能超过从设备支持上限同时DMA发送期间不要关闭SPI时钟否则数据会瞬断。PWMDMA的用法也值得提一嘴。通过定时器触发DMA把CCR比较寄存器或ARR自动重装载寄存器作为目标地址就可以批量更新PWM占空比或周期。比如STM32F103上做双路呼吸灯可以把两路占空比数据组织成一个数组用DMA按定时器更新事件逐个搬运到对应的CCR寄存器这样就能生成无级渐变波形几乎不占用CPU。这种方式适合需要周期性更新PWM占空比、做渐变色带、音频灯效的场景。配置的关键是DMA触发源要选对外设事件比如TIMx的更新事件而不是随便一个定时器溢出标志。触发源选错DMA就不会按节拍搬运。3.4 分布式DMA与测速评估热词里提到“分布式DMA”这通常是相对复杂的多核、多控制器系统中各个子系统都有独立DMA通道或DMA引擎协同工作的模式。在PCIE、多核SoC、高端FPGA领域比较常见。嵌入式单片机的DMA控制器相比起来简单很多但理解“数据搬运不要只依赖单一路径”的思维是通用的。做大型数据采集系统时多个外设的DMA通道可以同时工作只要优先级和总线占用协调好整体吞吐量会提升明显。提到“DMA测速软件”我们用单片机时当然没有现成软件可装但可以自己写一个DMA带宽测试程序准备一个较大的内存块配置DMA内存到内存搬运用定时器记录搬运耗时计算等效带宽。再调整FIFO、突发长度、数据宽度观察带宽变化。这是一个很好用的优化验证手段比拍脑袋决定配置靠谱得多。实测下来合理使用FIFO和突发传输DMA搬运带宽往往能有显著提升但代价是配置复杂度和出错概率同时上升。4. DMA常见问题与排查技巧4.1 Cache一致性问题带Cortex-M7或更高端内核的MCU普遍有指令Cache和数据Cache。CPU读内存时会优先读Cache而DMA是直接和物理内存打交道它搬进来的数据不一定和CPU看到的Cache内容保持一致。这就是经典的DMA与Cache一致性问题。如果外部RAM上的缓冲区被DMA写入而CPU读到的是Cache里的旧数据就会出现“数据没更新”的诡异现象。解决办法是DMA写入数据后、CPU读取前对缓冲区调用Cache无效化InvalidateCPU写入数据后、DMA读出前需要先Clean回写Cache。操作函数在CMSIS或HAL库里都有现成接口比如SCB_InvalidateDCache_by_Addr。这个坑在STM32F7、H7系列特别常见用F1系列的人大多不会遇到所以一旦从F1切到H7第一个震惊自己的就是这个问题。4.2 中断标志位与回调的混乱DMA传输完成、半传输、传输错误都有独立的中断标志。HAL库里对应不同回调函数比如XferCpltCallback、XferHalfCpltCallback、XferErrorCallback。实际使用有个细节在回调函数里做的事不要太重尽量避免在中断上下文里做耗时操作比如内存拷贝大量数据或打印耗时日志。如果确实需要快速处理建议拷贝关键数据到另一块内存标记一个事件标志主循环再处理。手动操作寄存器时中断标志的清除顺序容易让人抓狂。串口DMA接收场景里IDLE标志和DMA完成标志往往需要一起清除顺序错了可能导致下一次接收直接失效。通用的做法是读串口SR寄存器清IDLE标志再检查DMA状态最后重新使能接收。不同MCU细节有差异要看参考手册的中断清除描述。4.3 串口DMA接收不定长的边界判定接收不定长数据的核心难题是“什么时候算一帧结束”。IDLE中断是最常用的判定方式但有两个边界场景容易出问题。一个是半包如果发送方一字节一字节地慢慢发单片机的IDLE中断可能在每两个字节之间就触发一次导致一帧数据被拆成无数小包。另一个是长包丢失DMA缓冲区太小一帧还没发完DMA计数器就归零了数据自动停止接收。针对半包问题可以用超时机制也就是IDLE中断后先启动一个软件定时器等一小段时间再判定这帧是否真正结束。针对长包丢失常用双缓冲或循环缓冲配合半传输中断。比如DMA缓冲区1024字节传输超过512字节触发半传输中断把前半段数据搬走传满1024字节触发完成中断再把后半段搬走这样就不会丢长帧。我之前做过一个Modbus网关就靠这种方式把长达2048字节的协议帧完整接收下来稳得很。4.4 地址对齐与资源冲突DMA对地址对齐的要求在不同芯片上不一样。一部分MCU要求外设地址和内存地址按传输宽度对齐比如半字传输时地址必须是2字节对齐字传输时必须是4字节对齐。如果地址不对齐轻则性能下降重则总线错误。定义缓冲区时建议用编译器属性强制对齐比如在STM32上使用ALIGN_32BYTES宏避免手动数地址。资源冲突主要发生在两个外设抢同一个DMA通道或两个DMA控制器争抢总线。调试时如果发现数据传输偶发停顿可以看看每个通道的优先级配置以及外设请求是否被另一个通道占用。特别是STM32有些外设请求只能走特定DMA控制器换到另一个控制器时映射关系完全不同这一步查文档比调代码更高效。5. 学习路径与建议方向我给刚开始接触DMA朋友的建议路径是先用串口DMA发送固定字符串体会“写一次代码数据自己跑完”的感觉然后做串口DMA接收不定长数据结合空闲中断理解DMA与外设请求、中断回调的配合方式接着做ADC多通道连续采样把DMA循环模式用透最后再上双缓冲、半传输中断、FIFO和突发传输这些高级功能。调试DMA类问题时我的检查顺序一般是先确认外设请求是否真的发生再看DMA通道映射是否选对然后确认源地址、目标地址和数据长度最后检查中断标志是否正常置位。这个顺序能过滤掉大多数低级配置错误。另外强烈建议在代码里把DMA状态寄存器实时读出来哪怕只通过串口打印一次也能快速定位是没请求、没搬运还是请求到了但传错地方。我个人的习惯是拿到一个新MCU或新开发板先写一个最简单的DMA内存搬运程序确认DMA控制器本身工作正常再逐个加上外设请求源。这套思路帮我躲过无数个“数据不动”的深夜也让我在后续调ADC、串口、SPI时心里非常有底。本文还有配套的精品资源点击获取