上一节我们把虚拟DMA驱动的骨架搭起来了,能申请通道、能配置传输参数。但说实话,那只是个空壳子,真正干活儿的部分——数据传输、中断响应、还有高性能场景必备的SG(Scatter-Gather)模式,都还没动。今天咱们就把这些硬骨头啃下来。
我个人习惯,写驱动一定要先把中断处理想清楚。为什么?因为DMA的精髓就是异步,CPU把活儿派下去,DMA干完了得通知你。这个通知机制要是搞砸了,整个系统就卡死了。我在项目中遇到过好几次,中断处理函数写得不对,导致数据丢包或者系统死锁,排查起来特别痛苦。
中断处理:让DMA学会“喊报告”
先看中断处理的核心逻辑。当DMA传输完成,硬件会触发一个中断。我们的驱动要做三件事:
- 从硬件寄存器读取中断状态,确认是哪个通道完成的
- 清理中断标志位,防止重复触发
- 调用注册的回调函数,通知上层数据准备好了
代码实现大概是这样的:
static irqreturn_t virtual_dma_irq_handler(int irq, void *dev_id) { struct virtual_dma_device *vdma = dev_id; u32 status; int i; // 读取中断状态寄存器 status = readl(vdma->base + VIRTUAL_DMA_IRQ_STATUS); for (i = 0; i < vdma->num_channels; i++) { if (status & (1 << i)) { struct virtual_dma_channel *chan = &vdma->channels[i]; // 清理中断标志 writel(1 << i, vdma->base + VIRTUAL_DMA_IRQ_CLEAR); // 更新传输状态 chan->state = VIRTUAL_DMA_STATE_COMPLETE; // 回调通知上层 if (chan->callback) chan->callback(chan->callback_param); // 性能统计:记录完成时间 do_gettimeofday(&chan->stat.end_time); chan->stat.transfer_count++; } } return IRQ_HANDLED; }注意:中断处理函数里不要做耗时操作,比如打印日志、分配内存。我曾经见过有人直接在中断里用printk,结果系统响应变得极慢。回调函数也要设计成轻量级的,真正耗时的处理应该放到工作队列或tasklet里。
SG传输:解决大块数据搬运的痛点
你想想看,如果一次要传输1MB的数据,但系统内存是碎片化的,物理地址不连续怎么办?传统的DMA只能处理连续物理地址,这就尴尬了。SG传输就是为此而生——它允许你把多个不连续的物理内存块,通过一个描述符链表串起来,DMA硬件会自动遍历这个链表,完成所有数据块的搬运。
说白了,SG就是DMA的“批处理模式”。
我们的虚拟DMA驱动要实现SG,需要准备一个描述符数组:
struct virtual_dma_sg_entry { dma_addr_t src_addr; // 源物理地址 dma_addr_t dst_addr; // 目的物理地址 size_t length; // 本段长度 }; struct virtual_dma_sg_desc { struct virtual_dma_sg_entry *entries; int num_entries; int current_entry; // 当前正在传输的条目索引 };启动SG传输的代码逻辑:
static int virtual_dma_start_sg(struct virtual_dma_channel *chan, struct virtual_dma_sg_desc *desc) { unsigned long flags; int ret = 0; spin_lock_irqsave(&chan->lock, flags); // 检查通道状态 if (chan->state != VIRTUAL_DMA_STATE_IDLE) { dev_err(chan->dev, "Channel %d is busy\n", chan->id); ret = -EBUSY; goto out; } // 保存SG描述符 chan->sg_desc = desc; chan->state = VIRTUAL_DMA_STATE_RUNNING; // 配置第一个SG条目到硬件寄存器 writel(desc->entries[0].src_addr, chan->base + VIRTUAL_DMA_SRC_ADDR); writel(desc->entries[0].dst_addr, chan->base + VIRTUAL_DMA_DST_ADDR); writel(desc->entries[0].length, chan->base + VIRTUAL_DMA_LENGTH); // 设置SG模式标志 writel(1, chan->base + VIRTUAL_DMA_SG_MODE); // 启动传输 writel(1, chan->base + VIRTUAL_DMA_START); // 性能统计:记录开始时间 do_gettimeofday(&chan->stat.start_time); out: spin_unlock_irqrestore(&chan->lock, flags); return ret; }经验之谈:SG传输的关键在于描述符链表的维护。我在MTK平台上调试过一款摄像头驱动,SG列表有128个条目,结果因为描述符地址没有对齐到16字节,硬件直接罢工了。记住,很多DMA控制器对描述符有对齐要求,务必查阅芯片手册。
性能统计:用数据说话
驱动写得好不好,不能光靠感觉。我们需要一套性能统计机制,量化DMA的传输效率。我习惯在通道结构体里加一个统计子结构:
struct virtual_dma_stats { struct timeval start_time; // 传输开始时间 struct timeval end_time; // 传输结束时间 unsigned long transfer_count; // 总传输次数 unsigned long total_bytes; // 总传输字节数 unsigned long max_latency; // 最大延迟(us) unsigned long min_latency; // 最小延迟(us) unsigned long avg_latency; // 平均延迟(us) };每次传输完成后,更新统计信息:
static void update_stats(struct virtual_dma_channel *chan, size_t bytes) { unsigned long latency; struct timeval now; do_gettimeofday(&now); latency = (now.tv_sec - chan->stat.end_time.tv_sec) * 1000000 + (now.tv_usec - chan->stat.end_time.tv_usec); chan->stat.total_bytes += bytes; chan->stat.transfer_count++; // 更新最大/最小延迟 if (latency > chan->stat.max_latency) chan->stat.max_latency = latency; if (latency < chan->stat.min_latency || chan->stat.min_latency == 0) chan->stat.min_latency = latency; // 更新平均延迟 chan->stat.avg_latency = (chan->stat.avg_latency * (chan->stat.transfer_count - 1) + latency) / chan->stat.transfer_count; }通过sysfs接口把这些数据暴露给用户空间,方便调试:
static ssize_t stats_show(struct device *dev, struct device_attribute *attr, char *buf) { struct virtual_dma_channel *chan = dev_get_drvdata(dev); return sprintf(buf, "Transfer count: %lu\n" "Total bytes: %lu\n" "Max latency: %lu us\n" "Min latency: %lu us\n" "Avg latency: %lu us\n", chan->stat.transfer_count, chan->stat.total_bytes, chan->stat.max_latency, chan->stat.min_latency, chan->stat.avg_latency); }测试用例:验证驱动的正确性
驱动写完了,不测试等于没写。我建议至少写三个层次的测试用例:
| 测试级别 | 测试内容 | 预期结果 |
|---|---|---|
| 单元测试 | 单次DMA传输,4字节对齐 | 数据正确,中断触发 |
| SG测试 | 5个不连续内存块,总大小64KB | 所有数据正确搬运 |
| 压力测试 | 连续1000次传输,随机大小 | 无数据丢失,无内存泄漏 |
这里给一个简单的测试代码片段:
static int test_single_transfer(struct virtual_dma_device *vdma) { struct virtual_dma_channel *chan; dma_addr_t src, dst; void *src_buf, *dst_buf; int ret; // 分配DMA缓冲区 src_buf = dma_alloc_coherent(vdma->dev, 4096, &src, GFP_KERNEL); dst_buf = dma_alloc_coherent(vdma->dev, 4096, &dst, GFP_KERNEL); if (!src_buf || !dst_buf) return -ENOMEM; // 填充源数据 memset(src_buf, 0xAA, 4096); memset(dst_buf, 0x00, 4096); // 请求通道 chan = virtual_dma_request_channel(vdma, 0); if (!chan) { ret = -EBUSY; goto out_free; } // 启动传输 ret = virtual_dma_start_transfer(chan, src, dst, 4096); if (ret) goto out_release; // 等待完成(实际应用应该用异步回调) msleep(100); // 验证数据 if (memcmp(src_buf, dst_buf, 4096) == 0) pr_info("Test PASS: data match\n"); else pr_err("Test FAIL: data mismatch\n"); out_release: virtual_dma_release_channel(chan); out_free: dma_free_coherent(vdma-dev, 4096, src_buf, src); dma_free_coherent(vdma->dev, 4096, dst_buf, dst); return ret; }核心要点回顾:
- 中断处理要快进快出,清理标志位后尽快调用回调
- SG传输解决了物理地址不连续的问题,但要注意描述符对齐
- 性能统计是驱动优化的基础,用sysfs暴露给用户空间
- 测试用例要覆盖单次传输、SG传输、压力测试三个层次
嗯,到这里,虚拟DMA驱动的核心逻辑就完整了。从通道管理到中断处理,从SG传输到性能统计,再到测试验证,这一套流程在MTK平台上我已经验证过多次。你按照这个思路去写,基本不会出大问题。下一节咱们聊聊如何把这个驱动集成到内核的DMA Engine框架中,让上层应用可以更方便地调用。