拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Linux DMA驱动开发实战:dmaengine框架与Cache一致性解析

搞Linux下DMA开发这事儿说难也难说简单也简单。难在它不像普通驱动那样写寄存器-等中断-读数据就完事DMA牵涉到内存分配、cache一致性、描述符管理、中断上下文还有一堆dmaengine框架的API要理顺。简单在于只要吃透了通道申请、传输描述、映射同步、完成回调这四步主流程大部分DMA外设驱动都能拿下。这个系列我想按基础框架 - API实战 - 性能调优 - 疑难排查的顺序往下写第一篇先解决最核心的问题Linux DMA子系统是怎么组织的驱动里该怎么跟它打交道以及那些网上查不到但实际必踩的坑到底长什么样。适合刚接触Linux驱动开发、被dmaengine API绕晕的读者也适合已经在写DMA驱动、想系统梳理一遍的工程师。先说清楚这系列偏实操原理讲到位但不钻牛角尖目标是让你看完能上手把DMA跑起来。1. DMA基础概念与Linux驱动框架概览1.1 先搞清楚DMA到底解决了什么问题DMADirect Memory Access说白了就是帮CPU干搬运活儿的。CPU的强项是逻辑控制和计算不是把数据从外设搬到内存、再从内存搬到外设。没有DMA的年代CPU得守着状态寄存器data ready就搬一个字节再来一个再搬这对高速外设来说简直是灾难。比如一个每秒产生几百MB数据的采集设备CPU光做拷贝就占了大部分算力别的活儿全干不了。DMA的思路很直接外设要传数据时由DMA控制器直接操作总线把数据从源地址搬到目的地址搬完通过中断告诉CPU活干完了。CPU只管发起和收尾中间过程完全不用管。这个模型里涉及三个角色DMA控制器负责实际搬运通常是SoC内部的一个硬件模块也可能是外设自带的DMA能力。内存源或目的驱动负责准备缓冲区。外设想要传输数据的设备比如UART、SPI、I2C、以太网MAC。在Linux里这三者的关系被封装成了dmaengine子系统。它不是为某个具体设备服务的而是一个通用框架上接驱动称为DMA client下接各厂商的DMA控制器驱动DMA device driver。你写驱动时不需要关心底层DMA控制器是哪个厂商的、寄存器怎么配你只需要通过标准API把需求告诉dmaengine它来协调。这套设计跟Linux里其他子系统gpio、clk、pinctrl的思路完全一致定义好接口上层驱动和底层硬件解耦。1.2 dmaengine框架的三层结构和关键数据结构dmaengine框架分三层理解这三层你就能看懂代码了第一层是核心层drivers/dma/dmaengine.c提供统一API给驱动调用。这些API包括dma_request_chan、dmaengine_prep_slave_single、dmaengine_submit等驱动作者只管用这些接口不需要知道底层细节。第二层是DMA控制器驱动层比如drivers/dma/下各厂商的实现dw-axi-dmac、tegra210-adma、stm32-dma等。它们向核心层注册自己的dma_device结构体实现各种回调函数。这一层跟普通驱动开发者关系不大除非你是芯片厂商或者要写DMA控制器驱动。第三层就是DMA client层也就是我们要做的外设驱动。它通过dmaengine API发起传输请求配置参数等待完成。涉及的关键数据结构有这么几个struct dma_device { struct list_head channels; struct device *dev; unsigned int directions; enum dma_transaction_type cap_mask; int (*device_alloc_chan_resources)(struct dma_chan *chan); void (*device_free_chan_resources)(struct dma_chan *chan); struct dma_async_tx_descriptor *(*device_prep_slave_sg)( struct dma_chan *chan, struct scatterlist *sgl, unsigned int sg_len, enum dma_transfer_direction direction, unsigned long flags, void *context); struct dma_async_tx_descriptor *(*device_prep_dma_cyclic)( struct dma_chan *chan, dma_addr_t buf_addr, size_t buf_len, size_t period_len, enum dma_transfer_direction direction, unsigned long flags); int (*device_config)(struct dma_chan *chan, struct dma_slave_config *config); int (*device_terminate_all)(struct dma_chan *chan); };dma_device是控制器驱动注册的实体里面最重要的是那几个device_prep_*回调它们是具体传输的前置准备函数。struct dma_chan { struct dma_device *device; void *private; ... };dma_chan是通道句柄代表一个可用的DMA通道。我们从dma_request_chan拿到它后面所有操作都靠它。struct dma_async_tx_descriptor { dma_cookie_t cookie; enum dma_transaction_type tx_type; struct dma_chan *chan; dma_async_tx_callback callback; void *callback_param; ... };dma_async_tx_descriptor是描述符描述一次具体的传输。你调用dmaengine_prep_*系列函数时它会返回一个描述符指针然后你把描述符提交给通道执行。还有一个你一定会用到的结构体struct dma_slave_config { enum dma_transfer_direction direction; phys_addr_t src_addr; phys_addr_t dst_addr; enum dma_slave_buswidth src_addr_width; enum dma_slave_buswidth dst_addr_width; u32 src_maxburst; u32 dst_maxburst; u32 src_port_window_size; u32 dst_port_window_size; u32 slave_id; ... };这个结构体用来告诉DMA控制器数据从哪来、到哪去、总线宽度多少、每个burst传多少。1.3 为什么DMA驱动不能像普通IO那样直接读写很多从单片机转过来的开发者有个习惯分配一块内存拿到缓冲区地址直接把地址告诉DMA控制器指针寄存器然后启动。在Linux下这么做大概率会翻车原因在于cache。CPU在读写内存时不会每次都直接访问物理内存而是先经过cache。当DMA控制器直接访问物理内存时问题就来了如果CPU在cache里改了数据但还没写回内存DMA读到的就是旧数据反过来DMA往内存写了数据CPU的cache里如果还残留旧值CPU读到的也是旧值。这就是DMA开发里最常见的cache一致性问题。Linux为此提供了DMA映射API处理cache同步问题。这正是DMA驱动跟普通IO驱动最大的区别你不能再想当然地直接操作缓冲区必须先通过dma_map_single或dma_alloc_coherent让内核帮你处理好一致性。后面我专门用一节详细讲这个。2. DMA引擎API实战从申请通道到完成回调2.1 通道申请dma_request_chan怎么用最稳妥在驱动里要使用DMA第一步是获取DMA通道。现代内核推荐用设备树方式声明DMA资源驱动里通过dma_request_chan获取API原型是struct dma_chan *dma_request_chan(struct device *dev, const char *name);参数name对应设备树里dmas属性的dma_controller channel_name中的channel_name。比如设备树里有i2c1: i2c40012000 { compatible vendor,my-i2c; ... dmas dmamux1 2 0x400 0x01; dma-names rx, tx; };那么驱动里struct dma_chan *rx_chan; struct dma_chan *tx_chan; rx_chan dma_request_chan(dev, rx); if (IS_ERR(rx_chan)) { return PTR_ERR(rx_chan); } tx_chan dma_request_chan(dev, tx); if (IS_ERR(tx_chan)) { dma_release_channel(rx_chan); return PTR_ERR(tx_chan); }这里有个实际经验dma_request_chan可能返回EPROBE_DEFER表示DMA控制器驱动还没准备好框架建议你返回这个错误码让device模型稍后重新probe。写驱动时一定要处理这种情况不要直接返回错误终止probe。正确做法是把IS_ERR的返回值原样传给上层。注意老内核里常见的是dma_request_slave_channel它内部已经封装了设备树解析逻辑新内核里已经不推荐了统一用dma_request_chan就好。另外of_dma_request_slave_channel这类接口更不建议直接调除非你要处理的设备连struct device都没有。2.2 配置传输参数slave_config填不对后面全白搭拿到通道后下一步是告诉DMA控制器这次传输的物理布局。这里用到dmaengine_slave_configstruct dma_slave_config config {0}; int ret; config.direction DMA_MEM_TO_DEV; // 内存到设备 config.src_addr 0; // 内存方向不需要外设地址 config.dst_addr phys_addr; // 外设的物理寄存器地址 config.src_addr_width DMA_SLAVE_BUSWIDTH_4_BYTES; // 内存侧总线宽度 config.dst_addr_width DMA_SLAVE_BUSWIDTH_4_BYTES; // 外设侧总线宽度 config.src_maxburst 4; // 每次burst传输4个数据单元 config.dst_maxburst 4; ret dmaengine_slave_config(tx_chan, config); if (ret 0) { dev_err(dev, failed to configure dma channel\n); ... }这里有几个容易踩的坑一是src_addr和dst_addr填的是物理地址不是虚拟地址也不是总线地址。对大多数平台来说物理地址就等于总线地址但个别有IOMMU的平台会有地址映射这时候建议通过dmaengine_get_dma_device等相关接口确认别硬编码。二是src_addr_width和dst_addr_width不是随意填的。比如UART FIFO是1字节宽你填4字节DMA控制器按4字节搬运结果数据错位。有些DMA控制器会忽略这个配置自动适配但规范的驱动应该根据外设FIFO宽度显式配置。三是src_maxburst表示每次突发传输的数据单元个数不是字节数。例如UART FIFO深度16字节每次burst 8字节那maxburst 8/1 8假设宽度1字节。设置过大可能导致FIFO溢出设置过小又降低传输效率需要根据外设FIFO大小和DMA控制器能力综合评估。2.3 准备传输描述符single模式与cyclic模式怎么选配置完成后就要准备传输描述符。这里有两个高频API对应两种不同使用场景。第一种是dmaengine_prep_slave_single用于一次性传输一块数据struct dma_async_tx_descriptor *desc; desc dmaengine_prep_slave_single(tx_chan, buf_dma_addr, len, DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT); if (!desc) { dev_err(dev, failed to prepare dma transaction\n); ... }参数里的buf_dma_addr必须是经过DMA映射的地址后面细说len是字节数。最后一个DMA_PREP_INTERRUPT标志表示希望传输完成时触发中断回调。如果不置这个标志你没法收到完成通知。第二种是dmaengine_prep_dma_cyclic用于环形缓冲持续传输。音频设备是最典型的场景数据流源源不断需要周期性地刷新缓冲区desc dmaengine_prep_dma_cyclic(chan, buf_dma_addr, buf_len, period_len, direction, DMA_PREP_INTERRUPT);buf_len是整个环形缓冲区大小period_len是每个周期大小。DMA控制器传输完一个period_len就触发一次中断然后继续传下一段循环往复。这个模式特别适合音频、摄像头之类的连续数据流场景。2.4 提交、启动与回调这四步是DMA传输的灵魂描述符准备好后传输的发起分两步提交和启动。dma_cookie_t cookie; cookie dmaengine_submit(desc); ret dma_submit_error(cookie); if (ret) { dev_err(dev, failed to submit dma transaction\n); ... } dma_async_issue_pending(tx_chan);dmaengine_submit只是把描述符挂到通道的待处理队列里真正让DMA动起来的是dma_async_issue_pending。有些驱动新手在这里困惑为什么submit后没反应多半是忘了调用dma_async_issue_pending。完成回调在描述符里设置desc-callback my_dma_callback; desc-callback_param my_priv_data;回调在中断上下文执行不能做耗时操作。想处理数据正确姿势是把工作通过tasklet或workqueue推迟到进程上下文。这里推荐用tasklet或irq_work配合workqueue根据自己的需求选。完整的回调伪代码static void my_dma_callback(void *param) { struct my_priv *priv param; /* 在中断上下文标记完成并调度下半部 */ priv-dma_done true; tasklet_schedule(priv-tasklet); } static void my_tasklet_handler(unsigned long data) { struct my_priv *priv (struct my_priv *)data; /* 处理DMA搬运后的数据 */ dma_unmap_single(priv-dev, priv-buf_dma_addr, priv-buf_len, DMA_FROM_DEVICE); ... }2.5 完整的最小DMA传输流程伪代码把上面串起来一个最小可用的DMA发送流程大概是int my_dma_send(struct my_dev *dev, void *buf, size_t len) { struct dma_chan *chan dev-tx_chan; struct dma_async_tx_descriptor *desc; dma_addr_t dma_addr; dma_cookie_t cookie; int ret; /* 1. 映射缓冲区 */ dma_addr dma_map_single(dev-dev, buf, len, DMA_TO_DEVICE); ret dma_mapping_error(dev-dev, dma_addr); if (ret) { dev_err(dev-dev, dma_map_single failed\n); return -EIO; } /* 2. 准备描述符 */ desc dmaengine_prep_slave_single(chan, dma_addr, len, DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT); if (!desc) { dev_err(dev-dev, dmaengine_prep_slave_single failed\n); dma_unmap_single(dev-dev, dma_addr, len, DMA_TO_DEVICE); return -ENOMEM; } /* 3. 设置回调 */ desc-callback my_dma_callback; desc-callback_param dev; /* 4. 提交并启动 */ cookie dmaengine_submit(desc); ret dma_submit_error(cookie); if (ret) { dev_err(dev-dev, dmaengine_submit failed\n); dma_unmap_single(dev-dev, dma_addr, len, DMA_TO_DEVICE); return ret; } dma_async_issue_pending(chan); return 0; }这段代码别看简单里面每一步都有讲究尤其是dma_map_single的位置我见过不少人在准备描述符之后才做映射用起来也没报错但逻辑上是错的。映射必须发生在DMA控制器拿到地址之前否则可能拿到无效地址。3. DMA映射与Cache一致性最容易翻车的部分3.1 dma_map_single与方向选择的讲究上一节的例子用到了dma_map_single这一步是Linux DMA开发最容易出问题的环节。dma_map_single的原型dma_addr_t dma_map_single(struct device *dev, void *ptr, size_t size, enum dma_data_direction direction);direction有四种取值DMA_TO_DEVICECPU写数据DMA读。典型场景是内存到外设的发送。DMA_FROM_DEVICEDMA写数据CPU读。典型场景是外设到内存的接收。DMA_BIDIRECTIONAL双向传输数据可能被CPU和DMA反复读写。DMA_NONE调试用基本不出现。为什么要区分方向本质是给底层一个cache操作依据。DMA_TO_DEVICE时映射过程会把相应cache line写回内存确保DMA控制器能读到正确数据。DMA_FROM_DEVICE时映射过程会失效相应cache line确保CPU从cache读的是DMA写回的物理内存数据而不是过期的cache残留。注意DMA_BIDIRECTIONAL虽然方便但代价是一正一反两次cache操作性能比单向映射差不少。如果数据流方向明确尽量使用单向映射。3.2 流式映射 vs 一致性映射到底该用哪个dma_map_single属于流式映射streaming DMA mapping适合一次性、短时传输。它的特点是映射期间cache一致性有保证但映射结束后CPU访问这块内存前需要手动dma_unmap_single告诉内核DMA已经用完了cache状态可以恢复了。如果你在unmap之前直接访问缓冲区一样会遇到数据不一致问题。流式映射的另一个特性是它映射的缓冲区物理地址可能不连续如果原始缓冲区是从vmalloc或高端内存来的底层通过IOMMU或SWIOTLB解决。这也是为什么你拿到的是dma_addr_t而不是物理地址的原因不要假设它等于物理地址。另一种是dma_alloc_coherent它一次搞定分配和映射dma_addr_t dma_handle; void *cpu_addr; cpu_addr dma_alloc_coherent(dev, size, dma_handle, GFP_KERNEL);它返回的cpu_addr是CPU视角的虚拟地址dma_handle是DMA视角的地址。内核保证这块内存对CPU和DMA都是可见且cache一致的不需要手动sync操作。适合长期存在、频繁DMA传输的缓冲区。但要注意一致性映射的代价是每次访问都得绕过或刷cacheline性能不一定比流式映射好。而且它分配的是连续物理内存大块分配容易失败。实际项目中接收缓冲区常用dma_alloc_coherent因为要长期使用、随时可能被DMA写入一次性发送缓冲区常用dma_map_single。3.3 dma_sync_single_for_cpu与for_device流式映射下的手动同步流式映射的cache一致性只在映射期间有保证这在一次性发送场景没问题但有两个典型例外一是外设接收数据到缓冲区DMA传输完成后你要访问缓冲区内容。此时如果没有unmap比如你想复用缓冲区就必须在CPU访问前调用dma_sync_single_for_cpu(dev, dma_addr, len, DMA_FROM_DEVICE);让cache line失效确保CPU读到的是DMA写回的数据。二是缓冲区被CPU修改后要再次发送DMA开始前dma_sync_single_for_device(dev, dma_addr, len, DMA_TO_DEVICE);把CPU cache里的修改写回内存让DMA控制器能读到。这两个API是DMA驱动调优的重要工具因为频繁map/unmap的开销不小如果缓冲区复用率高很多驱动选择只做一次映射在每次传输前后做sync操作。这里分享一个我实际调过的案例某平台SPI控制器DMA接收数据偶尔出现几个字节乱码排查发现驱动在DMA完成回调里直接读了缓冲区没有做dma_sync_single_for_cpu。因为CPU的cache预取机制导致部分数据看起来是旧的加上DMA_FROM_DEVICE同步后问题消失。问题根源就是cache一致性跟SPI控制器本身没有关系。3.4 scatter-gather突破物理连续性的限制前面的API处理的都是物理连续的内存块。但复杂场景下数据可能要发往多个缓冲区或者缓冲区本身是分散的。这时候需要scatter-gatherSG模式。SG模式的核心是struct scatterlist数组每个节点描述一块内存struct scatterlist *sg; int nents, i; sg kcalloc(nents, sizeof(*sg), GFP_KERNEL); for (i 0; i nents; i) { sg_set_buf(sg[i], buf[i], len[i]); } nents dma_map_sg(dev, sg, nents, DMA_TO_DEVICE);dma_map_sg会遍历所有SG节点建立DMA映射并返回映射后的有效节点数。之后可以调用dmaengine_prep_slave_sg准备传输desc dmaengine_prep_slave_sg(chan, sg, nents, DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT);SG模式适合网络协议栈这类天然分片的数据流也适合缓冲区较大的场景。很多DMA控制器内部也支持SG通过硬件描述符链表实现而不是靠软件拼接性能很高。使用SG时有几个容易被忽略的细节SG节点数可能被合并或拆分dma_map_sg返回的nents可能小于传入值后续操作必须用返回值。每个SG节点的dma_address字段在映射后才是有效的不能在映射前读取。传输完成后必须dma_unmap_sg否则长时间运行会泄漏映射条目。3.5 dma_pool与预分配缓冲区降低DMA分配开销如果驱动频繁进行小尺寸DMA传输比如每次几百字节的UART数据包每次都dma_alloc_coherent再释放效率很低。内核为此提供了dma_pool机制专门管理固定大小的一致性缓冲区struct dma_pool *pool; void *cpu_addr; dma_addr_t dma_addr; pool dma_pool_create(my_pool, dev, size, align, 0); if (!pool) { dev_err(dev, dma_pool_create failed\n); ... } cpu_addr dma_pool_alloc(pool, GFP_KERNEL | GFP_DMA, dma_addr); ... dma_pool_free(pool, cpu_addr, dma_addr); dma_pool_destroy(pool);align参数指定对齐字节数通常设为cache line大小或外设的burst大小。alloc参数里的GFP_DMA标志用于限制在DMA可用区域内分配比如老x86的24MB限制区。现代大多数平台已经不需要GFP_DMA但保留也无妨。dma_pool的另一个隐藏好处是可以做对齐控制如果你想分配一个1KB的缓冲区并且要求起始地址128字节对齐普通kmalloc做不到这么精细dma_pool可以。4. 常见问题排查与避坑经验4.1 通道申请失败与EPROBE_DEFER的处理现象dma_request_chan返回错误有时是-EPROBE_DEFER有时是-ENODEV。排查思路先看设备树。dmas属性里引用的DMA控制器节点是否存在dma-names是否与驱动请求的名字一一对应。常见错误是dma-names顺序和dmas顺序不一致或者名字拼写错误导致通道匹配不上。再看DMA控制器驱动是否完成probe。如果DMA控制器本身依赖时钟、电源域而这些又没初始化好就会返回-EPROBE_DEFER。此时驱动应该把错误原样向上传递让设备模型延迟重试。经验不要在自己驱动里处理重试逻辑返回-EPROBE_DEFER是标准做法Linux设备模型会按依赖关系自动处理通常等DMA控制器probe完成后会自动重新probe你的设备。4.2 传输完成回调不触发现象DMA确实搬运了数据但desc-callback没被调用或者回调已经进了但状态不对。排查思路第一检查dmaengine_prep_*的flags参数。如果不带DMA_PREP_INTERRUPT标志底层DMA控制器驱动可能不会注册中断处理回调自然不触发。第二检查中断是否正常。有些DMA控制器有独立的中断号有些则合并到外设中断里。可以用cat /proc/interrupts确认DMA中断号有没有计数增加没增加说明中断根本没有触发。第三dma_async_issue_pending之后某些控制器要求等待一段时间才能完成如果你的测试代码在chain提交后立刻检查完成状态可能过早。正确做法等回调或者用completionstruct completion done; init_completion(done); desc-callback my_callback; desc-callback_param done; static void my_callback(void *param) { struct completion *done param; complete(done); } wait_for_completion_timeout(done, msecs_to_jiffies(1000));4.3 缓存一致性问题导致数据错乱现象DMA收到的数据和外设实际发送的不一致有时表现为首批数据正常、后续数据偶尔错位或者永远读不到最新数据。排查思路大部分情况下是cache sync时机错了。检查以下三点发送路径dma_map_single或dma_sync_single_for_device是否在数据写入缓冲区之后、dmaengine_submit之前完成。接收路径DMA完成回调里读缓冲区之前是否做了dma_sync_single_for_cpu或unmap。缓冲区是否被CPU和DMA同时操作。最常见的场景是一个缓冲区正在DMA传输CPU又往里面写数据导致数据覆盖或者cached脏数据。经验如果用了dma_alloc_coherent还出现数据错乱基本可以排除cache一致性问题转而检查DMA传输参数地址、长度、方向和外设的FIFO配置。4.4 环状缓冲与普通传输的选择误区不少项目里接收外设数据需要长期持续接收很容易想到用dmaengine_prep_dma_cyclic。但环状模式有个隐藏约束缓冲区必须是dma_alloc_coherent分配的连续内存且总长度是period_len的整数倍。不少人在这个上面翻车要么传入的地址不是DMA capable要么长度设置错误导致收发数据错位。如果数据流不要求严格实时性我建议普通驱动优先考虑dmaengine_prep_slave_single配合双缓冲切换。大致思路准备两个缓冲区A和B当前DMA用ACPU处理B传输完成时互换。这样代码逻辑清晰不容易出环状模式下的边界问题。环状模式更适合音频这类必须严格连续、不能有缝隙的场景。4.5 常见问题速查表问题现象可能原因排查/修复方法dma_request_chan返回-EPROBE_DEFERDMA控制器未就绪原样返回EPROBE_DEFER等待重新probedma_request_chan返回-ENODEV设备树dmas属性配置错误检查dmas/dma-names节点和名称传输不执行无中断缺DMA_PREP_INTERRUPT标志在prep阶段传入该标志CPU读到的数据是旧的DMA_FROM_DEVICE场景cache未失效dma_sync_single_for_cpu或unmapDMA读到的数据是旧的DMA_TO_DEVICE场景cache未写回dma_sync_single_for_device或unmap环状传输数据错位buf_len不是period_len的整数倍调整buf_len对齐缓冲区dma_addr为0分配失败或地址无效检查dma_mapping_error返回值大块dma_alloc_coherent失败物理内存碎片改用SG模式或减小分配尺寸SPI/UART数据偶尔乱码maxburst设置过大按FIFO深度/2设置maxburst4.6 调试技巧用ftrace和dmaengine debugfs定位问题排除DMA问题光靠printk效率太低。推荐两个工具第一个是/sys/kernel/debug/dmaengine/summary。内核配置CONFIG_DEBUG_FS和CONFIG_DMA_API_DEBUG打开后这个节点会列出所有DMA通道的信息包括通道名称、占用者、当前cookie等。可以快速确认通道有没有被正确申请、占用者是哪个驱动。第二个是dma_api_debug。这个模块会hook所有DMA API调用检测常见的映射错误比如未映射就使用DMA地址unmap的地址和map时的地址不一致map的数量和unmap的数量不平衡开启方法echo 1 /sys/kernel/debug/dma-api/error_clear如果驱动有DMA API滥用问题它会直接在内核log里输出警告。这个功能对DMA驱动联调非常有价值强烈建议打开。还有一个实用的方法在链路中故意关闭DMA用轮询方式跑同一组数据对比结果。如果轮询正常而DMA异常基本可以确定问题出在DMA配置或同步逻辑上。这招虽然笨但能快速划清问题边界。5. 实操经验与进阶方向5.1 设备树DMA属性到底怎么写前面提到设备树这里展开说一下规范写法。一个完整的使用DMA的外设节点一般需要两个属性dmas dma0 0 0x400, dma0 1 0x400; dma-names rx, tx;dmas属性中的每个条目包含三个值含义由DMA控制器驱动定义。以常见的dma-names 通道编号格式为例第一个值DMA控制器phandle第二个值通道编号或请求ID具体含义由控制器决定第三个值请求信号标志比如0x400表示突发长度或外设类型配置但这里没有统一标准不同DMA控制器驱动对这三个值的解析方式完全不同。比如STM32的DMA控制器驱动解析方式和TI的EDMA就完全不同写设备树前务必查阅对应DMA控制器驱动的bindings文档别想当然照抄别人的写法。另一种常见写法是dmas条目里配置slave_iddmas dma0 2 0x400 0x15, dma0 3 0x400 0x16;第四个值对应DMA_SLAVE_CONFIG的slave_id字段在一些需要硬件握手信号的控制器上需要这个。格式问题排查时Documentation/devicetree/bindings/dma/目录下每个DMA控制器的binding文档是唯一的权威。宁可多花两小时读文档也好过对着错误设备树调一整天。5.2 DMA中断合并与CPU负载小数据量场景的另一种选择DMA不是银弹。对于小数据量、低频次的外设传输DMA的开销有时候反而比中断轮询更高。为什么一次DMA传输的代价包括分配映射缓冲区、配置DMA控制器、发起传输、响应中断、清理映射。每一层都是开销。如果每次只传几十字节DMA的硬件初始化开销相对于中断方式没有优势还会增加驱动代码复杂度。我做过一个项目某低速串口波特率9600数据包平均32字节最开始直接上了DMA。调完发现CPU占用率和中断方式不相上下代码复杂度却翻了一倍。后来改成中断接收配合FIFO缓冲反而更简洁高效。所以判断一个外设是否需要DMA标准不是能不能用DMA而是数据量和频率是否值得DMA。带宽高、数据量大、传输频繁选DMA偶尔几个字节中断接收更合适。5.3 从dmaengine走向更多场景dmaengine API只是Linux DMA编程的一块拼图。实际项目里还会遇到其他DMA相关场景比如内核里大量使用DMA的网络栈、块设备层以及硬件加速相关的dma_buf框架。理解dmaengine是基础打好这个基础再往这些方向研究会顺利很多。后续这个系列我计划展开几个方向写几篇环形缓冲DMA的完整实现以及音频场景下的period边界处理。DMA性能调优包括burst方向调整、内存对齐优化、映射复用、缓存一致性对带宽的影响。SG模式的深入应用网络设备里DMA是怎么组织的。DMA与IOMMU的关系dma_map_sg在SMMU/IOMMU下的行为差异。5.4 最后一个实战建议写DMA驱动不要一次性写完再调试风险太高。我的习惯是分四步走第一步通道能否申请成功。先只调dma_request_chan确认通道和中断链路正常。第二步能否完成一次简单传输。用dmaengine_prep_slave_single做一次内存到内存或者内存到外设的搬运加上completion等待确认硬件链路通了。第三步在第二步基础上加上方向、中断和回调验证数据传输正确性。第四步才做完整的收发包逻辑。每一步尽量编译运行验证后再继续这样出问题时问题范围非常明确不用整个驱动里瞎猜。我在一开始带团队时有人一口气写完整个驱动结果调了三天最后发现是map方向写反了。如果按四步走这个错误第一步就暴露了。再补充一点DMA相关的内核日志用dev_dbg或dev_info打印时一定要把方向、长度、地址都打全。排查DMA问题日志里看不到len和direction会非常被动。我通常在每次dma_map_single和dmaengine_submit前各打一条完整信息虽然调试完会删掉大部分但在开发阶段这些日志是省时间的利器。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门