ARTICLE DETAIL

资讯详情

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

DMA驱动开发实战:从原理到排错,释放CPU红利

DMA驱动开发实战:从原理到排错,释放CPU红利 做嵌入式这些年DMA是我最早接触、也最晚真正理解的一个外设。前几年调试一台需要24小时不间断跑串口数据记录的设备CPU占用率居高不下系统响应越来越慢后来在同事的提醒下把串口接收从“每字节中断”改成DMA搬运CPU占用直接降了一个数量级。那一刻我才意识到想做好嵌入式驱动开发DMA不是可选项而是绕不开的必修课。这篇文章就结合我这几年调过的板子把DMA驱动从原理、配置、实战到排错的经验完整拆一遍。适合正在做嵌入式底层开发、被串口阻塞/ADC采样/大块数据搬运折磨过的工程师也适合准备嵌入式岗位面试的朋友参考DMA是面试里被问得极多的一个点。1. DMA到底在解决什么问题1.1 数据搬运这件事比想象中更费CPU先说一个很多新手容易忽略的事实CPU处理一字节串口数据远不止“读一次寄存器”那么简单。数据到达会产生中断CPU要压栈保存现场跳转到中断服务函数读数据寄存器把数据写进缓冲区更新读写指针最后恢复现场。这一整套过程下来几十个CPU周期是跑不掉的。拿115200bps的串口来算一笔账一帧数据通常10位8位数据起始位停止位有时还有校验位也就是说每秒大约能传11520个字节。如果用每字节中断的方式接收CPU每秒至少要被打断11520次。平均下来大概每87微秒就被打断一次这还没算上协议解析等其他逻辑。这种情况下哪怕CPU主频再高软件层面的中断开销也会把系统拖得半死。当时我们那台设备就是这个状况串口数据一跑起来其它任务的调度延迟肉眼可见地变长LED闪烁都开始不顺畅。用DMA之前我一直以为是主频不够后来把串口改成DMA接收才发现CPU占用率掉了一大截系统立刻“活”过来了。这件事给我的教训就是排查性能问题先看数据是怎么“搬”的再看算法怎么“算”的。我习惯用一个类比来解释CPU就像公司里的项目经理本来应该专心写方案结果每隔几分钟就有员工敲门进来让他签字盖章方案自然写得支离破碎。DMA相当于给项目经理配了一个专职秘书文件先在秘书手里分好类、排好序攒到一定量再一次性交给经理。经理还是得看文件但再也不用被“高频打断”这件事折腾了。1.2 DMA控制器的本质一个能自己搬数据的总线主设备抛开各种芯片手册里绕来绕去的术语DMA控制器的本质就一句话它是一个可以独立读写内存和外设寄存器的总线主设备。普通外设没有总线主控能力数据进来只能先放在自己的寄存器里然后发中断通知CPU来取。DMA控制器不一样它有自己的一套“搬运流程”CPU先把源地址、目标地址、数据长度、传输方向这些参数写进DMA控制器的寄存器然后DMA就开始自动干活了。搬运过程中每搬一个字节/字它自己会更新地址计数搬到指定长度后产生一个完成中断通知CPU“活干完了你来看看”。关键点在于DMA并没有让数据变得更少它只是把“搬运过程”中原本属于CPU的时间给释放出来了。CPU现在只需要在全部数据传输完成后做一次善后而不是每一字节都亲力亲为。这也解释了为什么DMA对大数据块传输特别有效——数据传输长度越长CPU省下来的时间越可观。1.3 DMA不是万能钥匙别乱用很多初学者学会DMA之后容易走极端什么东西都想挂DMA其实这并不对。DMA本身也有固定开销初始化DMA通道、配置外设请求、等待通道就绪、处理完成中断这些都是成本。如果一次只传几个字节配置DMA的功夫可能用普通中断早就传完了。我的经验是下面这些场景DMA收益非常大高速串口收发波特率≥460800或者虽然波特率不高但数据量持续不断SPI接口的Flash读写、SD卡底层数据搬移ADC连续采样尤其是多通道、高采样率的场合显示屏的帧缓冲刷新大块像素数据从内存搬到LCD接口I2S音频数据流数据量稳定且持续内存到内存的大块拷贝比如图像处理前的数据预处理而下面这些场景用DMA纯属给自己找麻烦低速、少量、命令字类型的通信比如I2C读一个传感器寄存器按键扫描、GPIO状态读取这种典型的中断轮询活数据长度不固定、且靠MCU逐字节做协议解析的输入流——当然这种可以用“DMA搬到缓冲空闲中断区分帧”的方式来解决后面细说一句话总结DMA是给“批量、持续、数据量大”的场景用的不是给“零星、随机、单次”的场景用的。2. DMA驱动接入时的核心配置决策2.1 通道映射表是第一个坑真正动手写DMA驱动第一个要查的就是你所用的MCU里外设和DMA通道之间的映射关系。以常见的STM32为例DMA1、DMA2各有若干通道每个通道又能接收若干个外设的DMA请求但不是随便哪个外设都能接到任意通道上。串口1的发送通常固定接某个通道串口3的接收固定接另外一个通道ADC触发又是另一个映射具体要看芯片参考手册里的DMA request mapping table。我在这上面吃过亏。当时移植一个工程看到前辈代码里用的是DMA1_Channel4做串口发送我照着写结果功能不正常。后来查手册才发现换了一个具体型号之后串口的DMA映射位置不一样了我照着旧代码配置的通道根本接收不到外设的请求。从那之后我养成了一个习惯每拿到一块新板子第一件事就是打开参考手册的DMA映射表把要用的外设请求、DMA控制器编号、通道编号做成一张小表贴在本子上。还有一个细节容易被忽略有些MCU的DMA请求是可以重映射的比如通过复用功能选择器remap把某个外设的请求从一个通道换到另一个通道。这时候就要格外小心因为驱动代码里配置的“外设DMA使能”和“DMA通道选择”必须同时正确否则外设发起了请求DMA通道却收不到现象就是数据永远不动。2.2 方向、数据宽度、地址自增三个必须一起想清楚的参数DMA传输方向有三种外设到内存、内存到外设、内存到内存。这个方向决定了DMA控制器内部地址的更新逻辑也决定了源地址和目标地址的配置方式。数据宽度是第二个关键参数。外设寄存器通常是8位比如UART数据寄存器或16位/32位比如ADC数据寄存器内存侧的缓冲区宽度要和它匹配。如果ADC是16位的DMA传输内存宽度也配成16位半字这时候缓冲区必须按半字对齐如果外设是8位内存宽度也配成8位字节就不需要强对齐。宽度不匹配的后果很隐蔽常见的是数据错位、高低字节乱掉而且不是每次都错偶尔对偶尔错极难排查。地址自增更需要注意内存侧通常要自增因为搬过来的数据要一个挨一个地放外设侧通常不自增因为外设始终只有一个寄存器。比如串口收数据源地址是固定的数据寄存器不自增目标地址是内存缓冲区自增。有些驱动新人把所有地址都配成自增结果源地址也往外跳数据直接从奇怪的地方读出来。2.3 普通模式、循环模式与“半满中断全满中断”的经典组合DMA传输模式的选择直接影响驱动架构。普通模式Normal Mode下DMA搬到指定长度后自动停止要再次传输必须重新设置控制寄存器并重新使能。这种模式适合“一次读取固定长度”的场景比如MCU读取传感器的固定长度样本、后台一次性搬运一块数据。循环模式Circular Mode则是为连续数据流准备的。搬完一轮后DMA自动回到缓冲区起点继续搬CPU根本不用管缓冲区被当作一个环形缓冲一直在被刷新。这种模式下最常用的方案就是“半满中断全满中断HAL库回调”把缓冲区切成两半DMA搬完前半段触发半满中断CPU处理前半段数据DMA继续搬后半段搬完后触发全满中断CPU处理后半段数据两个中断交替触发等于CPU永远在处理“刚被DMA填满的那一段”而DMA永远不会停这个设计在高速串口接收和ADC连续采样里简直是标配。它的精髓在于把数据流切成块CPU以块为粒度消费而不是以字节为粒度消费处理效率和实时性都得到了兼顾。循环模式还有个变体叫“暂停-恢复”模式有些芯片支持在循环模式下通过软件控制DMA暂停把当前缓冲区数据取走后再恢复。这在数据速率不高、但需要精确处理每一帧数据的场景里很实用。2.4 突发传输、FIFO和带宽分配新手最容易忽视突发传输Burst Transfer与FIFO经常一起出现在高级DMA控制器里。简单说普通DMA搬一个数据单元就要做一次总线仲裁突发传输是一次总线事务里连续搬多个数据单元能显著减少总线仲裁次数、提高吞吐率。听起来很美好但突发传输有代价一次突发会持续占用总线如果系统里多个DMA通道同时工作优先级配置不合理的话高优先级通道的突发可能把低优先级通道饿死。0x80000000和0x00000000一个未初始化一个飞了现象又千奇百怪。我调过一台设备现象是SPI DMA接收的数据偶尔会错几个字节但又不是每次都错搞得人非常崩溃。检查了一整天的时序、GPIO配置、SPI模式最后发现是缓存一致性问题CPU把命令写到缓冲区后没有刷CacheDMA直接去内存读读到的是旧的而接收数据时CPU从缓冲区读数据又没失效Cache读到的是之前的数据。处理方式没有统一标准裸机工程里可以直接在DMA传输前调用__HAL_DMA_DISABLE并加上屏障指令Linux驱动里则必须老老实实用一致性API。这里顺便提一个Linux下很容易踩的坑如果用kmalloc申请普通内存当DMA缓冲区DMA和CPU之间会有一堆Cache同步问题。标准做法是用dma_alloc_coherent申请一致性内存框架会帮你处理好写回和失效如果出于某些原因只能用普通内存就必须用dma_map_single配合方向的dma_sync_single_for_cpu/for_device手动同步。两者的语义完全不同网上很多教程混着讲实操时很坑。4.5 缓冲区对齐与大小的选择不然后续全是坑很多DMA控制器要求缓冲区地址按传输数据的自然边界对齐8位传输不需要特殊对齐16位要求半字对齐32位要求字对齐。如果缓冲区定义成uint8_t buf[512]但DMA配置成了32位宽度某些芯片上直接不工作某些芯片上工作正常但极不稳定。解决办法有两个一是把缓冲区声明为联合体强制对齐二是用aligned(4)之类的属性修饰。裸机工程里我用的是第一种方案简单直接。缓冲区大小也要结合传输场景想清楚。循环模式下缓冲区太小会导致CPU来不及处理数据就被覆盖表现为数据丢失、进程卡顿缓冲区太大又浪费内存尤其MCU内存本来就紧张。推荐的做法是根据最大传输速率和CPU消费速度来计算缓冲区至少能容纳“两个中断间隔内到达的数据的两倍”这样才能保证CPU消费一半时DMA还在写另一半。面试里经常问“DMA缓冲区怎么定”其实核心就是这个预算过程。5. 写在最后DMA驱动设计的几个前置问题我个人的体会是真正做DMA驱动设计时要先回答三个问题再动手写代码要搬运多少数据数据什么时候来多久来一次这三个问题决定了DMA通道选择、缓冲区大小、中断服务函数设计也决定了你是用普通模式、循环模式还是双缓冲。先想清楚再写代码事半功倍急着调代码后面八成要返工。DMA调试的路子我走过不少弯路最后还是回到一个习惯先跑普通模式小数据量测通再上循环模式先不优化Cache稳定了再动同步逻辑。分享一个小技巧调DMA时在传输完成中断里翻转一个GPIO用示波器量脉宽你就能比任何测试工具都直观地看到DMA到底有多快、中断延迟到底有多大。祝大家在用DMA搬数据的路上少踩坑多省CPU。
返回列表