首页 / 资讯中心 / 文章详情

嵌入式DMA驱动开发:从硬件原理到Linux框架的完整实践

嵌入式DMA驱动开发:从硬件原理到Linux框架的完整实践 ★ FEATURED ARTICLE
做嵌入式驱动开发这些年DMADirect Memory Access直接内存访问是我觉得最值得在项目早期就啃透的外设之一。串口、SPI、SDIO、以太网、ADC采样凡是和“批量数据传输”沾边的驱动最后几乎都要和DMA打交道。第6期就把DMA从硬件行为到Linux驱动框架、从Cache一致性到带宽测速的完整链路梳理一遍把我踩过的坑和验证过的方法一并写出来。这篇内容适合正在写Linux字符驱动、搞STM32外设驱动或者准备嵌入式驱动岗位面试的朋友无论你用的是什么内核版本这套分析思路基本通用。1. 为什么DMA是驱动开发的“硬骨头”1.1 先搞清楚DMA到底解决了什么问题很多新手看DMA的教材上来就是“DMA可以在不占用CPU的情况下搬运数据”这句话没错但容易让人忽略背后真正要命的东西DMA不是简单的“数据搬运工”它是一套独立的存储访问子系统拥有自己的总线仲裁逻辑、传输描述符、中断和错误状态。你在驱动里配置DMA本质上是把这个外设的数据通路整体托管给一个硬件代理CPU只在传输开始和结束时介入。嵌入式场景里DMA最常见的三种用途大家应该都熟外设到内存比如UART收数据、ADC采集结果、内存到外设比如SPI发送、DAC输出、内存到内存比如图像数据搬移、加密引擎前的数据重排。但这三种用途对驱动代码的要求完全不同。外设到内存最常见也最容易出问题因为你不知道硬件什么时候会丢数据、丢多少缓冲区管理稍有不慎就是踩内存查起来极其痛苦。我做过一个采集项目ADC在1MHz采样率下持续出数DMA每收满一帧就中断一次驱动里要保证在下一帧到来之前把当前帧的拷贝完成否则要么丢数据要么覆盖未处理的缓冲区。1.2 DMA让驱动架构复杂了一个维度没有DMA的时候驱动的数据通路是线性的CPU读寄存器或FIFO、判断状态、搬数据、再循环。有了DMA之后数据通路变成了并行的CPU在配置好DMA描述符之后就可以去干别的传输的完成由中断或者轮询标志位来通知。这个并行意味着驱动里天然需要处理异步、并发和时序竞争。举个最简单的例子你在UART接收驱动的中断里调用readl读FIFO数据还在硬件层。而DMA接收是硬件在后台往你指定的缓冲区写CPU完全不知道数据什么时候写入只有DMA的完成中断能告诉你“已经收了N个字节”。这中间就引出了DMA驱动开发的核心矛盾CPU视角的内存内容和DMA正在写入的内容之间存在时间差和一致性风险。这个矛盾延伸到Linux下就是Cache一致性、DMA屏障、dma_map和dma_unmap的语义问题在裸机环境下就是DMA缓冲区首地址对齐、总线上是否有其他主设备干扰、内存屏障指令是否要加。所以如果你只想“让数据能通”不一定会碰见这些坑但一旦你要做量产级的驱动要处理高吞吐、长时间运行、多通道并发DMA的每一个细节都会被放大成事故。这篇就以Linux内核的DMA engine框架为主线穿插裸机场景STM32那类作对比把驱动开发的完整逻辑串起来。2. DMA驱动框架的核心结构设计2.1 Linux内核DMA engine的层次拆解Linux的DMA子系统不是一上来就能request_irq这么简单它分成三层DMA控制器驱动层、DMA engine核心层、client使用层。控制器驱动层负责和具体硬件打交道比如PL330、DW_APB_DMAC、STM32的DMA1/DMA2这一层把硬件的通道、描述符、中断、寄存器操作抽象成struct dma_device。核心层提供通用接口和状态机管理维护通道的分配、释放、传输请求的排队。client层就是我们普通驱动开发者主要接触的通过dma_request_channel和dmaengine_prep_*来发起一次传输。理解这个层次特别重要因为它在设计上决定了驱动代码的职责边界。你在自己的驱动里不应该去手动修改DMA控制器的寄存器而是要调dmaengine_*的API把“怎么操作硬件”的细节交给控制器驱动。这样做带来的直接好处是代码可移植性换一颗SoC只要DMA控制器驱动适配好了你的业务驱动改动量很小。我在项目里换过主控业务驱动里只改了设备树里的DMA通道编号和请求号其余逻辑原封不动。设备树里通常这样描述一个DMA clientuart3 { dmas dma0 22 0x4000 0x0f; dma-names rx, tx; };这里dma0引用了DMA控制器节点22是DMA请求线编号0x4000是传输配置标志0x0f是突发长度。这一行的含义比看起来复杂得多后面我会专门展开突发长度和传输配置怎么选。2.2 从client视角看一次DMA传输的生命周期一次典型的DMA读外设到内存请求驱动里会按这个顺序调用接口struct dma_chan *chan dma_request_chan(dev, rx); struct dma_slave_config cfg { .direction DMA_DEV_TO_MEM, .src_addr (phys_addr_t)uart_phys_base UART_RX_FIFO, .src_addr_width DMA_SLAVE_BUSWIDTH_1_BYTE, .src_maxburst 4, }; dmaengine_slave_config(chan, cfg); struct dma_async_tx_descriptor *desc dmaengine_prep_slave_single(chan, buf_phys, len, DMA_DEV_TO_MEM, DMA_PREP_INTERRUPT); desc-callback rx_done_cb; desc-callback_param priv; dmaengine_submit(desc); dma_async_issue_pending(chan);这一步一步写下来逻辑很清楚但如果你把它和裸机场景对应着看就更好理解了dma_request_chan相当于选通道dmaengine_slave_config相当于设置源地址、目的地址、数据宽度、突发模式dmaengine_prep_slave_single相当于构建一个描述符对应硬件的DMA描述符表项dmaengine_submit是把描述符挂到通道的pending队列dma_async_issue_pending是真正触发硬件开始搬运。很多人在写的时候容易忽略desc-callback的执行上下文。DMA完成中断跑在硬中断上下文里回调函数也是在驱动层注册的那个tasklet或者线程化中断里执行。如果你直接用desc-callback它是在DMA控制器驱动的中断处理流程里被调用的不能在里面睡眠不能调用可能睡眠的函数比如mutex_lock、kmalloc带GFP_KERNEL、copy_to_user。我见过同事在回调里用wait_event_interruptible等用户进程结果一收数据整个中断就卡死系统看起来像是DMA吞了中断其实就是回调阻塞了。注意回调里只做轻量级操作比如把缓冲区挂到工作队列、置一个标志位、唤醒wait_queue。重量级的拷贝、解析、协议处理放到workqueue或者tasklet里做。2.3 裸机场景STM32的DMA配置思路Linux下的框架很成熟但裸机开发的思路其实是一样的只是没有现成的API给你调。以STM32F1系列为例DMA1和DMA2各有多个通道每个通道可以绑定不同的外设请求。比如USART1_TX对应DMA1通道4USART1_RX对应DMA1通道5ADC1对应DMA1通道1。配置时核心要决定这几组寄存器通道配置寄存器方向、外设/内存地址递增、数据宽度、中断使能、传输数量寄存器要搬运多少个数据单元、外设地址和内存地址寄存器。一个典型的USART1接收DMA配置代码骨架如下RCC-AHBENR | RCC_AHBENR_DMA1EN; DMA1_Channel5-CPAR (uint32_t)USART1-DR; DMA1_Channel5-CMAR (uint32_t)rx_buf; DMA1_Channel5-CNDTR RX_BUF_LEN; DMA1_Channel5-CCR DMA_CCR_MEMINC | DMA_CCR_PSIZE_0 | DMA_CCR_MSIZE_0 | DMA_CCR_PL_1 | DMA_CCR_TEIE | DMA_CCR_TCIE | DMA_CCR_EN; USART1-CR3 | USART_CR3_DMAR;这段配置里最容易忽略的是MEMINC这个位。外设地址CPAR固定内存地址CMAR每搬运一个数据单元自动递增这就是DMA接收能连续往缓冲区写数据的原理。如果你忘了置MEMINCDMA会一直往缓冲区的同一个地址写收到的数据全部互相覆盖。裸机开发里DMA的中断服务函数要自己做状态机判断void DMA1_Channel5_IRQHandler(void) { if (DMA1-ISR DMA_ISR_TCIF5) { DMA1-IFCR | DMA_IFCR_CTCIF5; received_len RX_BUF_LEN - DMA1_Channel5-CNDTR; // 处理接收数据 } }这里CNDTR寄存器是实时递减的传输完成后用缓冲区总长度减去剩余计数得到实际接收的字节数。这个寄存器在传输过程中可以随时读拿来判断当前收了多少字节是调试利器但在传输还没完成时读到的值仅代表“剩余多少个单元”不能直接当字节数用要乘上数据宽度。3. 理解DMA的硬件行为与边界条件3.1 数据宽度、突发长度和FIFO的匹配关系DMA控制器不是逐字节搬运的它可以从源端读取一批数据攒到自己的FIFO里再一次性写入目的端这批数据的单位叫“突发长度”burst。突发长度直接影响总线占用效率和传输带宽也和外设FIFO深度息息相关。外设的FIFO深度决定了DMA的burst不能大于FIFO能容纳的数据量否则数据会溢出丢失。举个实际参数计算的例子某SPI外设的RX FIFO深度是16字节DMA接收方向配置的数据宽度是1字节突发长度最多可以配16也就是每次从外设FIFO拿出来16个字节再写内存。但如果你突发长度配了32硬件行为就变成DMA一次性从外设搬32字节外设FIFO只有16必然导致FIFO溢出收到的数据错乱。在Linux下src_maxburst和dst_maxburst就是给控制器驱动提示这个限制的但最终还是要看DMA控制器驱动怎么解释这两个字段。有的驱动会把burst转换成分散/聚集描述符里的DMA_SLAVE_BUSWIDTH_*和MAX_BURST项有的直接就忽略。所以我的经验是配置burst的时候先查datasheet里外设FIFO深度和AHB/APB总线位宽再对着驱动源码看一眼硬件寄存器到底怎么映射burst字段不要凭感觉填。bus宽度和数据宽度也要匹配。比如外设寄存器是16位的你把src_addr_width配成4字节DMA会做非对齐的32位访问很多外设FIFO是16位映射到AHB总线上的直接按32位访问会把高16位读到错误的值。反过来内存端的宽度建议不小于外设端否则DMA一次只能挪半个字带宽上不去。3.2 缓冲区对齐和Cache一致性驱动开发最隐蔽的坑DMA是直接访问物理内存的这带来了两个基本约束缓冲区物理地址必须连续缓冲区在CPU视角的Cache内容必须和设备视角一致。第一个约束好理解驱动里要么用dma_alloc_coherent分配一致性内存保证物理连续且Cache相干要么用dma_map_single映射一块已有的内核缓冲区。第二个约束就没那么直观了。现代CPU都有多级CacheCPU写数据后会留在Cache里而DMA绕过Cache直接读内存那DMA看到的就是旧数据。反过来DMA往内存写了新数据Cache里还是旧副本CPU一读就读到脏数据。这就是DMA和Cache的一致性问题。解决思路有两条一是直接把缓冲区所在的地址空间配置为不可CacheIO内存或强序内存dma_alloc_coherent就是这么干的二是在合适的时间点手动做Cache的clean和invalidatedma_map_single配合dma_sync_single_for_device和dma_sync_single_for_cpu就是做这个事情。我见过最典型的错误是这样的内核驱动里用kmalloc分配了一块缓冲区然后直接把虚拟地址丢给dmaengine_prep_slave_single没做任何map操作。小数据量的时候跑得好好的一旦数据量大了、CPU把缓冲区一部分留在L2 Cache里DMA收到的数据就是花屏、乱码、偶尔丢字节的随机故障。这种bug的排查难度极高因为复现条件不稳定和数据量、CPU负载、Cache替换策略都相关。实操心得凡是DMA要读写的缓冲区统一用dma_alloc_coherent或者dma_pool分配少用kmalloc手动sync省心。只有对性能极度敏感、想靠Cache命中提速的场景才考虑手动同步但要做好每一处dma_sync_*的配对。3.3 描述符机制内存到内存传输的隐藏依赖现在的DMA控制器基本都是描述符驱动的裸机时代的寄存器式配置已经被替代了。描述符里包含源地址、目的地址、传输长度、突发配置、中断使能、下一个描述符指针。DMA控制器自动从内存里加载描述符执行完一个就跳下一个直到链尾。描述符放在普通内存里和普通数据缓冲区一样要保证一致性。Linux的dmaengine_prep_dma_memcpy会返回一个描述符提交之后硬件可能立即开始读这个描述符所以不要再修改描述符的内容除非你重新初始化一个。另一个坑是描述符池耗尽dmaengine_submit之后描述符是在队列里排队如果提交速度大于DMA执行速度描述符迟早用完再提交就会返回错误或者空指针。我在一个网络驱动里遇到过性能压测一跑dmaengine_prep_*返回NULL直接就oops了后来在提交前加了描述符数量和队列深度的检查才彻底稳定下来。裸机项目里如果DMA控制器不支持描述符链表就只有单次传输模式高频率外设中断场景下性能瓶颈明显所以选型时如果明确知道要做高吞吐优先选带多描述符FIFO的控制器比如STM32F4以上系列或带PL330的SoC。当然这是硬件选型问题驱动开发层面要做的就是先吃透控制器的描述符管理方式。4. 串口DMA与SPI DMA的驱动实操与验证4.1 串口DMA接收的环形缓冲区设计串口本身是低速外设但DMA接收能极大降低CPU中断频率尤其是在115200bps、每秒钟约1万字节的连续数据流场景下。常规思路是DMA接收方向配一个比较大的缓冲区比如4096字节DMA每收满一次触发中断驱动里把数据拷贝到协议解析缓冲区然后重新提交下一次DMA传输。这个方案有个明显问题DMA在缓冲区收满之前不会触发完成中断如果串口数据不是刚好凑够4096字节才停那最后一小段数据会一直滞留在DMA缓冲区里要等下一次数据到来把它顶出来。这个问题叫“DMA残留数据”。解决办法通常有两种一是开DMA的半传输中断和传输完成中断配合半中断一到来就把前一半拿走完成中断把后一半拿走但这样对缓冲区大小和数据包长度有匹配要求二是用定时器兜底每次收到DMA中断后启动一个短定时器如果在一段时间内没有新的DMA中断但缓冲区里还有数据就手动flush。第二种更通用。环形缓冲区的经典实现里读指针和写指针分别由CPU和DMA维护。CPU在DMA完成中断里更新读指针DMA硬件自己维护写指针通过CNDTR倒推。关键点在于CPU侧要时刻记得DMA还在往缓冲区写不能把读指针追到写指针之前。裸机开发里容易犯的错误是在DMA完成中断里直接把整个缓冲区拷贝走但DMA可能已经开始了下一轮写入。Linux下串口子系统其实已经帮我们把DMA的这些逻辑封装在serial_core里了。普通的tty驱动开发者通常不直接碰dmaengine接口而是改8250或者全志、瑞萨的串口驱动里的DMA回调。但我建议即使不写串口底层也可以把环形缓冲区的设计思路吃透这几乎是所有流式DMA外设驱动的共用模板。4.2 SPI DMA发送的高频踩坑点SPI用DMA发送有一个经典陷阱SPI主机在没有时钟时是不会主动输出的DMA往SPI的TX FIFO塞数据如果SPI没有启动传输数据会积压在FIFO里不动。反之SPI接收时要产生时钟才有数据进来如果DMA接收准备好之前主机已经开始时钟那前面的数据可能只发不收缓冲区里就是空洞。所以SPI DMA发送的驱动里使能DMA和外设的先后顺序非常关键。正确顺序通常是先配置好DMA描述符并提交再把SPI主机切到DMA模式并启动传输让SPI的时钟由FIFO空/满状态自然驱动。反过来如果先启动SPI、再等DMA那第一批数据会延迟时序就对不上了。另一个坑是SPI的DMA传输和数据宽度。很多SPI外设支持8位或16位模式DMA数据宽度要跟着配。8位模式下DMA每搬运1字节16位模式下每搬运2字节。如果数据宽度配错收到的数据会是错位的两个字节挤在一个16位单元里恢复起来费神。这个在逻辑分析仪上看得非常清楚但不少人第一反应是SPI相位极性配错了实际上SPI模式根本没问题。还有一种情况是SPI DMA发送完成后SPI外设的TX FIFO数据还没有完全移位出去你就直接关了SPI时钟或者切了GPIO导致最后一两个字节丢失。这类问题在发送完成中断里加一个小的延时或者轮询SPI状态寄存器的BSY标志位就能解决。我习惯的做法是DMA完成中断触发后先读SPI的忙状态位确认不忙了再close或者启动下一次DMA传输。4.3 DMA带宽测速的正确打开方式热词里反复出现“DMA测速”确实DMA的带宽不是datasheet峰值实测中受总线仲裁、Cache命中率、描述符开销、中断频率等多重因素影响。测速时常见的错误是把DMA搬运数据和CPU搬运混在一起量测出来的是“CPU总线”的综合结果不是纯粹的DMA能力。我推荐的测速方法是驱动里分配两块一致性内存缓冲区分别作为源和目的用dmaengine_prep_dma_memcpy提交一个固定大小的内存到内存传输同时记录起始ktime在完成回调里用ktime相减得到耗时反复跑多次取稳定值。关键点如下源和目的缓冲区都要物理连续、且地址对齐到DMA控制器的最大传输粒度和Cache line大小。传输大小要按DMA的最大burst对齐比如512字节对齐否则某些控制器会把末尾的小尾巴用单次传输完成带宽统计不准确。内存地址不要选择同一个page或者紧密相邻的地址否则DMA内存控制器可能有bank冲突带宽被压低这不是你驱动的问题。统计平均值之外还要看最大最小值的抖动。DMA带宽抖动过大说明总线上有其他主设备抢占比如LCD控制器在刷屏、SD卡在做DMA搬运这在多主设备系统里很常见。我在项目里实测过一片主控的DMA memcpy带宽峰值能到600MB/s左右但实际跑外设SPI时只有几十MB/s瓶颈在SPI时钟和外设FIFO深度不在DMA。所以“DMA测速失败”不代表DMA不行很可能是链路里的最短板在外设端。调试时要一级一级拆先测memcpy再测外设到内存逐层定位瓶颈。5. 常用调试手段与错误定位技巧5.1 从硬件寄存器入手确认DMA配置是否生效DMA出问题第一件事不是翻代码而是确认配置有没有真正写进硬件。Linux下可以用devmem直接读DMA控制器寄存器的值比如devmem 0x40030000 32对照datasheet的寄存器位定义确认SAR源地址、DAR目的地址、TCR传输计数和自己配置的值一致。如果SAR/TCR和你预期完全不一样说明DMA engine驱动在配置过程中出了问题如果一致说明配置本身没问题问题在中断或者缓冲区一致性上。裸机环境下更简单调试器里直接watch这几个寄存器的值单步走一遍配置流程看CNDTR的变化是否符合预期。CNDTR在执行过程中是递减的如果你发现它没动极可能是DMA没有实际传输检查外设的DMA请求信号是否生效。5.2 日志、Trace和中断计数定位丢数据问题DMA丢数据是驱动开发里最头疼的bug排查思路要按层级拆数据丢在源头外设侧丢在搬运路线上总线、DMA控制器侧还是丢在缓冲区到应用侧的拷贝上。每层用一个独立的观测点。内核驱动里可以在关键流程处加tracepoint或者简单的printk计数比如static void rx_done_cb(void *param) { struct my_dev *dev param; dev-stats.done_irq; // ... }配合中断计数和缓冲区长度统计能定位出到底是DMA没产生完成中断还是完成了中断但数据被覆写。我在串口DMA上遇到过一类问题DMA完成中断触发频率远高于预期导致每次只搬运了几个字节吞吐极低。查下来是DMA通道配置里burst没生效控制器把每个字节当成一次单次传输并且每传输一个字节就触发一次中断。配置burst之后中断频率立刻降到原来的百分之几。5.3 不用逻辑分析仪也能抓DMA时序问题的土办法很多嵌入式项目没有逻辑分析仪但不代表没法定位DMA时序问题。一个简单的方案是在驱动里用GPIO输出脉冲标记关键事件点。DMA提交完成dma_async_issue_pending之后、DMA完成中断刚开始时、数据处理完成时各翻转一次GPIO用示波器看这几个点的时间关系。这个方法在裸机和Linux下都适用。有次调试一个SPIDMA的音频输出驱动声音有周期性卡顿靠GPIO脉冲一眼就看出来了DMA完成中断到下一次dmaengine_prep_*提交之间间隔不稳定有一阵子长达几十毫秒那是因为驱动在中断里做了一次mutex_lock被另一个持锁进程阻塞了。优化掉锁之后脉冲间隔变得均匀问题消失。注意GPIOToggle本身会引入开销只适合定性看时序不适合做精确的时间测量。要精确测量还是用sched_clock或者内核的ftrace。6. 常见问题与排查技巧实录6.1 问题速查表我把嵌入式DMA开发里反复出现的问题整理成一个速查表每个问题都标记了典型特征和首选排查方向实战中可以直接对着这张表排雷。问题现象最可能的原因首选排查动作数据错位/花屏/乱码数据宽度配置不对或Cache未同步核对src_addr_width和外设寄存器位宽检查dma_alloc_coherent是否使用偶发丢数据且与CPU负载相关Cache一致性问题缓冲区未用一致性内存统一改用dma_alloc_coherent后观察是否稳定中断频率异常高吞吐极低burst未生效控制器按单次传输执行读寄存器确认MAXBURST值对照驱动源码确认映射关系数据停留在缓冲区处理不完环形缓冲区读指针追过写指针检查缓冲区剩余空间计算确认DMA描述符是否已重新提交DMA传输不启动CNDTR不动外设DMA请求信号未使能检查外设寄存器里的DMA使能位确认请求线编号中断丢失但数据实际到达ISR里睡眠或阻塞中断被长时间屏蔽检查回调函数是否有锁、kmalloc、msleep描述符返回NULL或提交失败描述符池耗尽检查提交速率和描述符队列深度加错误统计最后一个字节丢失外设移位寄存器数据未清空就关闭在完成中断里等待BSY位清零后再复位外设memcpy测速低于预期源地址未按Cache line对齐总线bank冲突改用dma_alloc_coherent分配并确保对齐6.2 Cache一致性冲突的经典排查案例我之前遇到一个很典型的案例SPIDMA收数数据量小时完全正常数据量大时随机出现几十个字节的旧数据。排查时我先怀疑SPI时序调了相位极性没用。后来用dma_map_single替换了kmalloc缓冲区在每次DMA提交前做dma_sync_single_for_device接收完成做dma_sync_single_for_cpu问题消失。事后来看就是CPU在缓冲区里写入控制参数后留了Cache脏数据DMA读到了旧的参数描述符导致描述符内容错乱。这个案例给我最大的教训是DMA的开发不要等出问题再补一致性应该在写第一版驱动的时候就按照“凡是DMA访问的内存都要显式管理”的标准来写。一致性管理的代码模板并不复杂但漏掉一个位置就是随机故障。6.3 面试八股里DMA最常被问的几个点因为是驱动岗位DMA几乎是必考点。面试官通常不会问API怎么调用而是喜欢追问这些点DMA传输需要满足哪些硬件条件——物理连续、地址对齐、方向明确、外设请求正确。为什么DMA能提升CPU利用率——减少中断次数数据搬运由硬件完成但这不是零成本描述符管理、中断处理、缓冲区拷贝仍然占用CPU。Cache一致性怎么解决——一致性DMA映射dma_alloc_coherent和非一致性映射dma_map_singlesync两条路线讲清楚适用场景。什么是突发传输和FIFO深度有什么关系——burst是DMA一次总线上连续读写的长度不能超过外设FIFO容量否则溢出。描述符在DMA中的作用——描述符装载源、目的、长度、下一个描述符指针实现链表式连续传输避免频繁CPU介入。多通道DMA同时工作会发生什么——总线仲裁、通道优先级、FIFO共享低优先级通道可能有延迟高优先级通道可能饿死低优先级。这些问题看着是知识题实际上是在考察你有没有在真实环境里被DMA坑过。回答的时候把自己的项目经历、调试手段、踩坑复盘讲清楚比背概念强很多。7. 从驱动框架到产品化的几点体会DMA这块内容如果只停在“能用”的程度和“靠谱”之间还有不少距离。真正做产品化的时候我建议把这几件事也纳入驱动设计的考量范围。第一DMA缓冲区的生命周期管理要提前规划。驱动里同时存在三种DMA内存一致性内存、流式映射缓冲区和描述符缓冲区。它们的分配时机、释放时机、错误处理路径都不同。我在项目里吃过亏的是在remove函数里先释放了DMA通道再释放缓冲区结果通道释放回调里还在访问缓冲区数据直接crash。现在我的做法是任何DMA通道释放之前先确保已经停掉所有传输、清空pending队列、等待所有完成回调执行完毕再释放缓冲区。第二电源管理和DMA的关系经常被忽视。系统休眠时DMA控制器进入低功耗状态如果外设在休眠前没有停掉DMA传输醒来后描述符和通道状态可能是脏的。Linux的dmaengine_pause、dmaengine_resume、device_prep_dma_*的幂等性要仔细测试休眠唤醒循环跑几百次才能放心。第三DMA驱动要主动暴露性能统计接口。哪怕只是debugfs里的一组计数器也能在客户现场出问题时快速定位。我习惯的统计项包括提交的传输次数、完成回调次数、超时次数、描述符分配失败次数、每次传输的最大/最小耗时。这些数据在问题面前比一千行日志都管用。DMA驱动的开发硬件的坑基本是死的最大的变量是CPU和总线的行为。把硬件行为吃透、把内存一致性管好、把自己的统计手段做扎实这套驱动就能站得住。做驱动的时间越长越觉得DMA不是“外设驱动的一部分”而是整个系统性能不可忽略的基础设施值得花时间把它彻底学透。
阅读完成 · 觉得有帮助?
咨询建站