news 2026/9/7 11:18:40

DMA详解:从工作原理到串口/ADC实战,一文搞懂嵌入式DMA

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DMA详解:从工作原理到串口/ADC实战,一文搞懂嵌入式DMA

搞嵌入式这行,跟串口、ADC、SPI打交道久了,迟早会撞上DMA这个词。尤其当你发现CPU一边要处理业务逻辑,一边还要吭哧吭哧搬运数据,系统一忙就丢包、卡顿、响应慢的时候,DMA就成了绕不开的救星。这玩意儿全称Direct Memory Access,直译就是直接存储器访问,核心作用一句话:让数据在外设和内存之间自己跑,别去打扰CPU。

我这几年调过的板子,从STM32到国产PY32,从裸机到RTOS,几乎每个项目都离不开DMA。这篇就把DMA的工作流程、传输模式和典型应用场景一次性讲透,包括那些网上翻半天也找不到的坑——比如串口DMA接收不定长数据怎么配合空闲中断、发送时到底要不要等上一轮发完、ADC多通道DMA采样为什么数据老是错位。不管你是刚入门的大学生,还是被项目逼着查DMA的工程师,这篇都值得你花十分钟看完,很多经验是真金白银踩出来的。

1. DMA到底是什么:它解决的从来不是“快”,而是“省”

1.1 没有DMA的日子,CPU是怎么被拖垮的

先算一笔账。假设你用串口以115200bps收数据,一个字节大约耗时86.8微秒。如果用中断方式接收,每个字节进来CPU都要停下手头的活,保存现场、进中断服务函数、把数据从数据寄存器搬到内存缓冲区、清标志位、恢复现场。这一套下来,即使效率高也要2到3微秒,看起来占比不高,但问题是:一旦数据速率上到1Mbps甚至更高,或者同时开了串口、SPI、ADC好几路数据流,CPU绝大部分时间就在做“从寄存器到内存”的纯搬运工作。

还有一个更隐蔽的问题:中断频繁触发会打乱CPU的执行流。你正在跑一个关键算法,突然被串口中断打断,等回来的时候缓存、流水线状态全变了,上下文切换的开销远不止那几微秒。我见过一个项目,就是因为串口中断太频繁,导致主循环里的实时控制任务周期性抖动,查了好久才发现罪魁祸首是中断风暴。

DMA的出发点不是“让传输更快”,而是“让CPU从传输中抽身”。数据要搬,但搬的人不该是CPU,而应该是一个专门干这活的硬件小工。这个小工就是DMA控制器。

1.2 DMA的核心思想:CPU只当管理者,不当搬运工

打个比方。一家餐厅里,CPU是厨师,数据是食材,外设和内存分别是仓库和灶台。没有DMA的时候,厨师每炒一道菜都要亲自跑一趟仓库取食材,菜越做越多,厨师全耗在路上了。DMA相当于请了一个传菜员,厨师只需要说一句“把仓库里那批食材搬到灶台”,然后继续专心炒菜,传菜员干完了再喊一声“搬完了”。

这个“喊一声”就是DMA传输完成中断。整个过程中,CPU只在开始的时候配置一下DMA控制器的源地址、目的地址、传输长度,然后该干嘛干嘛去,等DMA搬完了,通过中断或者轮询标志位来确认结果。如果是循环模式,甚至不需要CPU介入,DMA会自己一遍遍搬,搬完一轮自动从头再来,CPU连“确认完成”都可以省了。

1.3 DMA、中断、轮询:三者的定位完全不一样

很多新手搞不清什么时候用DMA,什么时候用中断,什么时候干脆轮询。我的经验是这么区分的:

  • 轮询:适用于极低速、极简单的场景,比如按键扫描、查询传感器状态。CPU闲着也是闲着,顺手读一下寄存器就行。
  • 中断:适用于事件型、偶发性的数据,比如收到一个命令、检测到一个边沿。数据量小,事件频率不高,中断完全扛得住。
  • DMA:适用于批量、连续、高频的数据流,比如串口收一帧几百字节的数据、ADC连续采样几千个点、SPI刷一块屏幕。这类场景数据量大且规律性强,是DMA的主场。

一句话总结:轮询是“盯着看”,中断是“被叫去处理”,DMA是“派个人去代办”。三者不是竞争关系,而是可以配合使用的。最常见的就是DMA做完一批传输后触发中断,CPU在中断里只做“结果处理”,不碰数据搬运,各司其职。

2. DMA工作流程拆解:从配置到完成的完整链路

2.1 一个DMA传输的四个阶段

DMA听起来玄乎,实际上一次完整的传输就四个阶段:请求、响应、传输、结束。我一个个拆开讲。

第一个阶段是请求。外设准备好数据了,或者内存里有一块数据等着搬走,谁来发起这个动作?如果是外设到内存,外设会拉高DMA请求信号,比如串口接收寄存器非空,就会向DMA控制器发出请求。如果是内存到内存,则由软件把DMA使能位置1,相当于软件发起请求。

第二个阶段是响应。DMA控制器收到请求后,会向总线仲裁器申请总线控制权。这里要注意,同一时间总线只有一个主人,CPU在用总线,DMA就得等着;DMA拿到总线,CPU就得暂停访问。好在DMA传输通常是突发式、短时间的,对CPU的影响微乎其微。仲裁器会根据优先级来决定先让谁用总线,这就是为什么高优先级通道能“插队”的原因。

第三个阶段是传输。这是核心动作。DMA控制器从源地址读一个数据单元,写入目的地址。数据单元的宽度可以是字节、半字(16位)、字(32位),具体由你配置。每传完一个单元,源地址和目的地址根据方向决定是自增、自减还是保持不变,传输计数器减一。这个“搬运一个单元”的动作会循环执行,直到计数器归零。

第四个阶段是结束。计数器归零后,DMA控制器会置位传输完成标志,如果使能了中断,还会向CPU发出中断请求。至此,一次完整的DMA传输结束。如果是循环模式,传输计数器会自动重载初始值,DMA又开始下一轮搬运,周而复始。

2.2 以STM32为例,看一次DMA传输的寄存器操作序列

光讲概念不过瘾,我以最常用的STM32 HAL库为例,展示一次内存到内存搬运的完整操作序列。这段代码是把一个数组的数据通过DMA复制到另一个数组:

uint32_t src_buf[64]; uint32_t dst_buf[64]; // 初始化DMA,需要先开启DMA1或DMA2的时钟 __HAL_RCC_DMA1_CLK_ENABLE(); DMA_HandleTypeDef hdma; hdma.Instance = DMA1_Channel1; hdma.Init.Direction = DMA_MEMORY_TO_MEMORY; hdma.Init.PeriphInc = DMA_PINC_ENABLE; hdma.Init.MemInc = DMA_MINC_ENABLE; hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; hdma.Init.MemDataAlignment = DMA_MDATAALIGN_WORD; hdma.Init.Mode = DMA_NORMAL; hdma.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma); // 启动DMA传输,src是源,dst是目的,长度64个字 HAL_DMA_Start(&hdma, (uint32_t)src_buf, (uint32_t)dst_buf, 64); // 等待传输完成 HAL_DMA_PollForTransfer(&hdma, HAL_DMA_FULL_TRANSFER, HAL_MAX_DELAY);

这里有个关键点很多人没意识到:在HAL库里,外设地址叫Periph,内存地址叫Mem,但方向是内存到内存时,PeriphIncMemInc必须都使能,否则数据搬完只有第一个字是有效的。我见过不止一个新手卡在这个坑里,搬完数据后发现目标数组只有第一个元素变了,其他全是0,就是因为忘了开地址自增。

2.3 关键状态位与中断标志:怎么判断DMA干完活了

DMA控制器内部有几个状态位,搞懂它们,排查问题就成功了一半。以常见MCU为例,几个最重要的标志是:

  • 传输完成标志:DMA搬完所有数据后置位,代表这一批任务结束。
  • 半传输标志:传输数量过半时置位,常用于双缓冲切换。比如ADC正在采后半段数据时,CPU可以处理前半段,两边不打架。
  • 传输错误标志:总线错误、非法地址等异常情况下置位,必须优先处理,否则DMA可能一直卡住。

使用中断方式时,进入DMA中断服务函数第一件事就是读取中断标志并清除。但要注意,有些芯片的标志清除顺序有讲究,比如必须先读状态寄存器再清标志,顺序反了会导致中断反复触发,系统直接卡死在中断里。这部分必须看芯片参考手册,不能想当然。

2.4 DMA连续请求与循环模式:两者是什么关系

热词里有个“DMA continuous requests”,中文叫连续请求或持续请求。这个概念在部分MCU的DMA配置中存在,比如有些芯片的DMA支持“循环缓冲区+连续请求”模式。简单说,普通模式下,外设每产生一个请求,DMA传一个数据单元,传完就等下一个请求。而在连续请求模式下,DMA只要启动就会不断搬数据,不再等外设请求。

连续请求和循环模式经常搭配使用。循环模式管的是“搬完一轮后自动回到起点继续搬”,连续请求管的是“不需要外设逐个触发”。两者叠加的效果就是DMA一直在后台搬数据,搬完一圈从头再来,CPU完全不操心。典型应用就是ADC连续采样:ADC不停采,DMA不停搬,数据在一个环形缓冲区里循环覆盖。

3. DMA传输模式全解析:模式选错,性能天差地别

3.1 按方向分:三种传输方向,对应三类场景

DMA传输方向是配置时最先要确定的参数,直接决定了你的源和目的是谁。

第一种是内存到内存,典型应用是大块数据拷贝、图像缓冲翻转、协议栈报文重组。这种模式下源和目的都在内存地址空间,数据经过DMA控制器内部的临时寄存器中转,从一个内存区域搬到另一个内存区域。特点是CPU完全不用参与,适合搬大块数据,但会占用总线带宽,如果和实时性要求高的任务抢总线,要谨慎评估时序。

第二种是外设到内存,这是MCU开发中最常见的。串口接收、SPI接收、ADC采样、I2C接收,本质都是外设的数据寄存器收到数据后,由DMA搬到内存缓冲区。这里有一个关键配置:外设地址固定不变,内存地址自增。外设地址永远是那个数据寄存器,每搬一个数据,内存地址往后挪一格,这样才能形成连续的数据序列。

第三种是内存到外设,典型应用是串口发送、SPI发送、DAC波形输出。DMA从内存缓冲区取出数据,写入外设的数据寄存器。这时候内存地址自增,外设地址固定。配上定时器触发,DMA还可以按固定节奏输出数据,比如产生PWM波形、播放音频采样点。

3.2 按工作模式分:单次、循环、突发,各有用武之地

单次模式是最基本的,配置好源地址、目的地址、长度,触发一次搬一次,搬完拉倒,计数器归零,DMA通道自动禁用。适合一次性的数据搬移,比如把传感器校准参数从Flash搬到RAM。

循环模式是单次模式的强化版,计数器归零后自动重载初始值,DMA通道一直保持使能。典型应用就是ADC连续采样和串口接收环形缓冲。但循环模式有个隐患:如果软件没有及时消费缓冲区里的数据,新数据会把旧数据覆盖掉。所以必须配合半传输中断、传输完成中断或空闲中断来分时处理。

突发模式则是以“块”为单位传输,而不是一个数据单元一个数据单元地搬。比如一次突发传输8个字,DMA会连续读8个字再连续写8个字。这种模式效率高,因为减少了总线仲裁次数,但需要源和目的地址都满足对齐要求,而且对总线的占用时间更长,可能在瞬间抢占大量总线带宽,影响系统的实时性。用突发模式前一定要算好带宽余量。

3.3 按触发方式分:硬件触发与软件触发

DMA传输需要触发源。软件触发就是写寄存器启动,适用于内存到内存搬运、或者定时批量处理。硬件触发则是外设产生事件来触发DMA搬运,比如串口收到字节、ADC转换完成、定时器更新事件。

硬件触发有个细节必须注意:DMA传输的数据宽度要和触发外设的数据宽度匹配。比如串口数据寄存器是8位的,你配置DMA搬运宽度却是32位,那每次触发实际搬入的数据会错位,收到的东西完全对不上。这类问题非常隐蔽,因为它不报错,只产生“脏数据”。

定时器触发DMA是非常实用的技巧。比如你要输出一组正弦波查表数据到DAC,简单粗暴的做法是CPU定时把下一个采样点写给DAC,但这样CPU会被绑死。换成定时器触发DMA,配置好DMA内存地址自增、DAC地址固定,定时器每次更新事件触发一次DMA搬运,CPU只需启动一次,剩下的交给硬件。采样率再高也不怕,CPU完全解放。

3.4 不同平台上的DMA架构差异:MCU、Linux、UFS这些都得知道

搞嵌入式不能只盯着STM32。Linux系统里有通用的DMA引擎框架,设备驱动程序通过DMA API申请DMA缓冲区、映射地址、提交传输请求,底层由平台相关的DMA控制器完成实际搬运。这里最核心的差异是:Linux场景下DMA缓冲区涉及到物理地址、虚拟地址和缓存一致性映射,代码里必须处理更复杂的地址转换。

热词里还有UFS DMA。UFS(Universal Flash Storage)作为一种高速存储接口,它的数据传输基本都要靠DMA来搬运,因为UFS的速率动辄每秒几百MB甚至上GB,CPU压根没法逐字节参与。UFS控制器内部集成了专用的DMA引擎,负责在UFS设备和系统内存之间搬数据,同时处理中断聚合、高级电源管理等。

分布式DMA则是另一个方向,常见于多核处理器或异构计算平台。每个计算核心拥有独立的DMA引擎,可以在局部内存之间搬运数据,减少跨核心访问总线的开销。这种架构在通信基带、AI推理芯片中很常见。对于大部分做MCU开发的人来说,可能用不到这么复杂的架构,但理解这个思路有助于你迁移到更高级的平台。

4. 典型应用场景与实操要点:串口、ADC、SPI一次讲清

4.1 UART DMA接收不定长数据:空闲中断才是关键

这是被问得最多的场景:串口接收的数据长度不固定,怎么用DMA接?方案是“DMA+空闲中断”。所谓空闲中断,就是串口在一段时间内没有收到新数据时触发的中断,它标志着一帧数据已经接收完整。

具体做法分三步。第一步,配置UART的DMA接收,让DMA把数据持续搬运到一个足够大的缓冲区,使用循环模式。第二步,开启UART的空闲中断。第三步,在空闲中断里计算本次接收的数据长度,然后处理数据。

// 配置UART接收DMA,循环模式 __HAL_RCC_DMA1_CLK_ENABLE(); hdma_uart_rx.Instance = DMA1_Channel5; hdma_uart_rx.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_uart_rx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_uart_rx.Init.MemInc = DMA_MINC_ENABLE; hdma_uart_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_uart_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_uart_rx.Init.Mode = DMA_CIRCULAR; // 循环模式 HAL_DMA_Init(&hdma_uart_rx); __HAL_LINKDMA(&huart1, hdmarx, hdma_uart_rx); HAL_UART_Receive_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE); // 开启空闲中断 __HAL_UART_ENABLE_IT(&huart1, UART_IT_IDLE); // 空闲中断回调中处理数据 void HAL_UART_IDLE_Callback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { __HAL_UART_CLEAR_IDLEFLAG(&huart1); uint16_t len = RX_BUFFER_SIZE - __HAL_DMA_GET_COUNTER(&hdma_uart_rx); process_frame(rx_buffer, len); } }

这段代码的核心是__HAL_DMA_GET_COUNTER,它返回DMA还剩多少数据没搬。用缓冲区总长度减去剩余计数,就是实际收到的数据长度。注意这里有个坑:如果使用循环模式,处理完一帧数据后不要傻傻地把DMA停掉重新配置再启动,那样会丢数据。正确做法是让DMA一直在循环跑,你只需要在空闲中断里计算长度并拷贝数据。

缓冲区大小有讲究。它必须大于项目里可能出现的最大一帧数据的长度,同时要考虑极端情况下收到的干扰数据。我习惯设成最大帧长的2到3倍,宁可浪费一点内存,也不能让数据溢出覆盖。

4.2 UART DMA发送:到底要不要等上一轮发完

热词里“dma串口发送需要等待上一轮数据发送完吗”这个问题,说明大家都在这上面栽过跟头。直接给结论:要等,但不等不等于一定出错,只是风险极大。

为什么?每次调用DMA发送时,你需要把源缓冲区地址和长度告诉DMA控制器。如果上一次发送还没结束,你就修改了缓冲区内容或者启动了新一轮发送,DMA可能正在读一个被修改了一半的缓冲区,发出去的数据就会错乱。还有一种更隐蔽的情况:配置新传输时DMA计数器被重置,上一轮没发完的尾巴突然断了。

我的处理习惯是维护一个“发送忙”标志。启动DMA发送时置位,在DMA发送完成中断里清除。下次要发送前先检查这个标志,如果还忙着,就进入等待队列或直接丢弃。如果你用的是类似HAL_UART_Transmit_DMA这样的接口,它内部其实会检查UART的状态,但裸机环境下依然建议自己做一层保护。

这里特别提醒一个细节:DMA发送完成后,缓冲区的数据还在,但DMA已经释放了总线。如果你的缓冲区是局部变量,函数返回后栈空间被释放,此时DMA还在搬数据就会读到垃圾值。所以DMA发送的源缓冲区必须保证在发送完成之前一直有效,最好用全局数组或静态数组。

4.3 ADC多通道DMA采样:数据错位问题怎么破

ADC多通道+DMA是另一个高频场景,热词里“stm32 hal库adc单通道dma多次采样”也直接命中了这个需求。ADC转换结果通过DMA自动搬运到内存数组,CPU只负责定期处理数组,这是标准做法。

单通道DMA多次采样比较简单,配置ADC为连续转换模式,DMA循环搬运,缓冲区里就是连续的一串采样值。但多通道就麻烦了,因为多个通道的转换结果会依次推进DMA缓冲区。比如你开了通道0、1、2三个通道,DMA缓冲区里的数据顺序是:ch0、ch1、ch2、ch0、ch1、ch2……你取数据时必须按组取,不能简单地认为数组第0到99个都是ch0。

我踩过的坑是通道顺序和DMA缓冲区初始化不一致导致的数据错位。解决思路有两个:一是严格按ADC的通道顺序手动编排DMA缓冲区,取数据用取模分组;二是启用ADC的DMA半字模式,确保每个采样值在缓冲区中的位置固定。实测下来,用双缓冲+DMA半传输中断最稳:后半段采样期间处理前半段,数据处理和采样完全并行。

关于采样频率和缓冲区大小,一定要算好。假设ADC采样率是1kHz,DMA缓冲区设1000个点,缓冲区每1秒才会被循环覆盖一遍。如果你在后续处理中耗时超过1秒,有些数据就被覆盖了。合理做法是根据处理耗时反推缓冲区大小,留足余量。

4.4 SPI刷屏、I2C读写、内存搬运:几个高频场景速查

SPI刷屏是DMA的经典应用。LCD屏幕刷新一帧数据动辄几百KB,用CPU逐像素发送,帧率上不去还卡界面。用SPI+DMA发送,只需把显存地址配成源地址,SPI数据寄存器配成目的地址,长度设为总像素数乘每个像素字节数,一帧刷完CPU完全无感。

I2C+DMA相对少用,因为I2C有ACK应答机制,每个字节传输后需要检查应答,DMA模式下出错了不容易恢复。如果必须用,建议只在无ACK校验的情况下使用,或者配合超时机制。实际项目中我更多用I2C中断方式,数据量不大时足够用。

内存到内存搬运在嵌入式场景也很有用。比如协议栈里需要把报文的头部和数据区拼接在一起再发送,用DMA搬运可以省一次CPU拷贝。还有一个容易被忽视的点:双缓冲音频播放。一段音频被分成两个缓冲区,一个在播放,另一个在填充新数据,DMA在两者之间切换,实现无缝播放。这个思路跟ADC双缓冲一模一样。

4.5 低成本MCU上的DMA实践:以PY32F003为例

热词里出现PY32F003,这是国产一款性价比很高的M0+内核MCU,资源不多,但DMA该有的功能都有。用它做串口DMA接收,思路和STM32完全一致,唯一需要注意的是它内部DMA通道数量有限,外设映射关系也跟STM32不完全相同,配之前必须翻芯片手册确认哪个DMA通道可以服务哪个外设。

我实际用PY32F003做过一个低功耗传感器采集节点,串口接收采用DMA+空闲中断,CPU大部分时间进入睡眠,只有空闲中断唤醒处理数据帧,整体功耗压得很低。这算是DMA在低功耗场景下的额外优势:数据接收根本不需要CPU醒来,攒够一帧再说。

4.6 进阶场景:FreeModbus+DMA、DMA测速验证

FreeModbus这种协议栈场景下用DMA要格外小心。Modbus RTU帧有严格的3.5字符时间间隔作为帧间隔判断,如果你用DMA+空闲中断接收,空闲中断的判定时间和3.5字符时间不一定匹配,可能导致粘帧或者断帧。我踩过的坑是:空闲中断的触发时机比Modbus要求的帧间隔略长,导致两个连续的Modbus帧被当成一帧接收,报文解析直接失败。后来做法是关闭DMA,改回中断接收,因为Modbus的帧速率本来就不高,中断接收足够用了。

DMA测速这个需求,核心是验证DMA实际吞吐量是否达到预期。简单做法是配置一个内存到内存的DMA搬运大块数据,用定时器记录搬运耗时,算出实际带宽。还可以对比同样操作下CPU逐字节搬运的耗时,效果直观震撼。我实测过一次,在72MHz主频的MCU上,DMA搬运1KB数据只需要几个微秒,CPU逐字节搬运要几百微秒,差距接近两个数量级。

5. DMA疑难杂症与排查技巧实录

5.1 常见问题速查表

我在多个项目里总结了一张高频问题速查表,每次调试DMA出问题先对着查一遍,往往能快速定位。

现象可能原因排查方向
接收到的数据全是0地址自增未使能;数据宽度不匹配检查PeriphInc/MemInc配置、对齐方式
数据错位、通道顺序错乱ADC多通道时缓冲区编排错误按通道顺序重新排列缓冲区或分组取数
DMA传输完成后数组没更新Cache未失效或未刷新检查缓存一致性,做Cache Clean/Invalidate
空闲中断频繁误触发串口波形有毛刺或DMA缓冲区太小检查硬件、增大缓冲区、调整空闲判定时长
DMA发送的数据只有前半段源缓冲区被提前释放确认缓冲区生命周期覆盖整个发送过程
DMA中断里死循环标志清除顺序不对按参考手册要求先读后清或先清后读
循环模式下数据被覆盖处理速度跟不上DMA速度改用双缓冲+半传输中断,或调大缓冲区

5.2 缓存一致性与对齐问题:高性能MCU上最容易翻车

如果你用的是带Cache的高性能MCU(比如Cortex-M7内核),DMA有一个大坑必须知道:Cache和DMA的数据一致性。CPU写数据到内存时,数据可能还停留在Cache里,没有真正落到内存;DMA搬运时直接读内存,读到的可能是旧数据。反过来,DMA从外设收到的数据写进内存,CPU读的时候可能命中Cache里的旧值,读到脏数据。

解决办法是在DMA操作前后做Cache维护操作。在STM32H7等系列上,HAL库有对应的接口,比如SCB_InvalidateDCache_by_AddrSCB_CleanDCache_by_Addr。规则很简单:DMA写入数据后,CPU读取前,做一次Invalidate(使Cache失效,强制从内存读);CPU写入数据后,DMA读取前,做一次Clean(把Cache内容刷到内存)。这一步漏了,数据就是你怀疑人生的根源。

对齐问题同样隐蔽。很多DMA控制器要求源地址、目的地址、传输长度满足特定对齐要求,比如按字对齐。如果不满足,轻则传输效率下降,重则直接触发总线错误进入HardFault。常规做法是:设计数据结构时就把缓冲区安排到对齐地址上,或者使用编译器的对齐属性声明。我习惯性把DMA缓冲区都用4字节对齐,养成习惯后能省掉一大半奇葩问题。

5.3 数据错位、中断丢失、DMA卡死的排查思路

数据错位的排查,第一步先关掉DMA的所有优化,用最简单的方式验证:固定一个源缓冲区,内容填递增序号,DMA搬运后检查目标缓冲区是否也是递增序号。如果序号错乱,说明地址步进方向或宽度配置有问题;如果序号正确但周期性地缺失,说明缓冲区长度和触发频率不匹配。

中断丢失的排查思路要先确认中断是否真的没触发。在中断服务函数入口设置一个GPIO翻转,用示波器看波形,比纯日志排查高效得多。如果GPIO有翻转但你的处理逻辑没执行,那就是中断优先级或标志位处理的问题;如果GPIO根本没翻转,那就是DMA到中断这一链路断了,查DMA使能、NVIC使能、中断标志是否被过早清除。

DMA卡死的排查要从“请求-响应”链路下手。DMA启动后一直没动静,先确认外设的DMA请求是否产生,信号路径上任何一环没打通都会卡死。我遇到过一个案例,DMA通道配置完全正确,但外设的DMA请求映射选错了通道,DMA控制器根本收不到请求,自然不干活。这种问题不查芯片手册的映射表,光看代码永远找不到原因。

5.4 调试DMA的几个实用手法

调试DMA,我最常用的工具不是调试器,而是示波器和逻辑分析仪。用GPIO引脚翻转来标记关键节点,比如DMA启动、传输完成、空闲中断触发,把这些信号引到示波器上,整个时序一目了然。这比盯着调试器的寄存器窗口直观多了,尤其适合定位时序相关的疑难问题。

还有一个技巧是用仿真器实时查看DMA计数器的变化。启动DMA后,在调试环境里观察剩余计数值是否在递减。如果递减,说明DMA在干活,问题出在数据内容上;如果纹丝不动,说明DMA根本没被触发,往回查请求源。

最后一个习惯:所有DMA缓冲区在调试阶段都先填上固定pattern,比如0xA5。这样只要看到数据不对,马上能判断是“没被写入”还是“写入但被覆盖”,排查方向完全不一样。这个习惯帮我节省了无数排查时间。

DMA这东西,用熟了之后你会发现自己再也回不去了。以前写串口接收,每收一个字节CPU都要响应一次中断;现在配好DMA和空闲中断,CPU该睡睡、该算算,数据自己安安静静地躺在缓冲区里等着处理。这种感觉,就像请了个勤快的传菜员,后厨的火候、切配、摆盘你都能专心照顾,再不用跑上跑下。

我个人在实际操作中最大的体会是:DMA的难点从来不在API怎么调,而在于你愿不愿意花时间把外设的请求路径、内存的对齐要求、中断的交互时序这些底层细节彻底搞清楚。只要啃下这块硬骨头,无论是串口不定长接收、ADC连续采样,还是SPI刷屏、双缓冲音频,在DMA面前都是同一个套路。希望这篇能把你的DMA之路铺平一点,少踩几个我已经替你踩过的坑。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 11:17:51

Notepad++主题定制全攻略:Stylers.xml与暗色高亮配色

简介:一套收藏已久的Notepad主题配置包,适合希望改善编辑器观感、降低长时间编码视觉疲劳的开发者使用。主题以KamiTheme.xml为主题文件,搭配txt配置说明,给出了字体、字号等推荐设置,便于用户快速上手,对N…

作者头像 李华
网站建设 2026/9/7 11:16:06

LLM服务性能评估:延迟、吞吐量与可用性测试实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 11:15:08

Knora One 字体兜底实测:从字体栈到 unicode-range 的完整验证指南

如果你做过网页、文档解析或者本地工具界面,一定遇到过这种场景:声明了一个字体,结果页面上全是方块,中文不出字,西文和数字正常,符号显示成豆腐块。这个现象叫“字体兜底”失败。Knora One 这个项目的内容…

作者头像 李华
网站建设 2026/9/7 11:14:11

水利水电地质CAD线型库搭建与调试实战

简介:面向水利水电工程地质领域的CAD制图人员,这套专用线型与图例资源包可解决地质图中线型表达不规范、地质信息传达不清晰的问题。压缩包共435个文件,以428个pat填充图案为主,辅以lin线型文件、shx形文件、txt说明文档、xls参数…

作者头像 李华
网站建设 2026/9/7 11:13:09

550MHz Cortex-M7 + 丰富连接外设:STM32H725ZGT6深度解析

如果让我用一句话概括 STM32H725ZGT6 这颗芯片,我会说:它把“能算”和“能连”这两件事做到了相当高的平衡。做嵌入式这些年,我见过太多项目在选型时纠结——想要性能得上更高端的 SoC,但成本、功耗、开发复杂度全上去了&#xff…

作者头像 李华
网站建设 2026/9/7 11:12:32

嵌入式启动流程深度拆解:从复位向量到OTA工程化实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华