1. 项目缘起与整体架构思路
1.1 为什么要在FPGA和Linux之间搭一座DMA桥
做过高速数据采集的人都有一个共同的痛:FPGA侧的数据吞吐量轻松跑到几百MB/s甚至上GB/s,但数据一旦要送到Linux用户态做后续处理,瓶颈立刻从采集前端转移到了数据搬运环节。传统的字符设备驱动配合copy_to_user,在ARM64平台上实测往往只能跑到200~400MB/s,CPU占用率还高得离谱,一个核心几乎被memcpy吃满。
hs_dma_framework要解决的就是这个断层。它的核心思路很直接:让FPGA通过DMA把数据直接写进Linux的物理内存,驱动层只负责管理缓冲区和中断,用户态通过mmap直接读取,整个链路里CPU只做三件事——配置DMA描述符、响应中断、通知用户态。数据本身不经过CPU搬运。
这个框架适合谁?如果你正在做FPGA数据采集卡、边缘计算网关、通信测试终端这类项目,主控是ARM64(比如Zynq UltraScale+的A53、瑞芯微RK3588、鲲鹏等),需要把FPGA采集的高速数据实时送到Linux应用层,那这套架构可以直接参考。它不依赖特定厂商的DMA IP,Xilinx的AXI DMA、Intel的mSGDMA、自己写的描述符链式DMA都能接进来。
1.2 三层架构的划分逻辑
整个框架分成三层,这个划分不是拍脑袋定的,而是根据数据流向和职责边界来的。
FPGA逻辑层负责数据源接入和DMA引擎控制。这一层要处理的是ADC采样、LVDS接收、SPI读取等前端时序,把数据整理成DMA能识别的流格式。关键点在于:DMA的位宽要和数据源匹配,比如ADC是16bit@100MSPS,那DMA至少要用64bit位宽、200MHz以上的时钟才能不丢数。
内核驱动层是桥梁。它要做的事情包括:申请连续物理内存(或者用CMA)、建立DMA描述符环、注册中断处理、实现mmap接口、管理多缓冲区轮转。这一层最考验功力的是内存管理和中断下半部的处理策略。
用户态接口层提供mmap映射和ioctl控制。用户程序拿到虚拟地址后直接读,配合环形缓冲区的读写指针做无锁消费。这一层要处理的是同步问题——怎么知道有新数据到了,怎么防止读越界。
三层之间的接口定义清晰:FPGA和驱动之间是描述符寄存器和中断线,驱动和用户态之间是mmap区域加eventfd或poll机制。这种分层的好处是每一层可以独立调试,FPGA侧用ILA抓波形,驱动侧用ftrace跟中断,用户态用perf看吞吐。
1.3 方案选型中的几个关键取舍
为什么不用UIO或者VFIO?UIO框架确实能快速把设备暴露给用户态,但它对DMA缓冲区的管理太粗糙,中断处理也不够灵活。VFIO更适合虚拟化场景,配置复杂。自己写字符设备驱动虽然工作量大,但对缓冲区的控制粒度最细,能针对采集场景做优化。
为什么选mmap而不是read?read系统调用每次都要经历内核态到用户态的拷贝,对于高速数据流来说这是致命的。mmap把内核申请的DMA缓冲区直接映射到用户空间,用户态读数据就是读内存,零拷贝。代价是用户态要自己管理缓冲区的消费进度。
ARM64平台的特殊考量。ARM64的缓存一致性和x86不同,DMA缓冲区的cache维护必须显式做。另外ARM64的页大小可能是4K也可能是64K,驱动里申请内存时要考虑对齐。中断控制器的差异也要注意,GIC的配置和x86的APIC完全不是一回事。
2. 核心细节解析与实操要点
2.1 DMA描述符环的设计与参数计算
描述符环是整个框架的心脏。它的设计直接决定了吞吐量、延迟和CPU占用率。
一个典型的描述符结构包含:源地址(FPGA侧)、目的地址(内存物理地址)、传输长度、控制字段(是否产生中断、是否最后一个描述符)。在FPGA侧,这些描述符通常存在BRAM里,由DMA引擎依次读取执行。
描述符数量的计算有个经验公式:描述符数量 = 最大延迟容忍时间 / 单个描述符传输时间。举个例子,假设单个描述符传输4KB,DMA带宽是1GB/s,那单个描述符耗时约4微秒。如果中断处理加用户态调度的延迟是50微秒,那至少需要13个描述符才能保证不丢数。实际项目中我一般会留一倍余量,用32个描述符。
缓冲区大小也有讲究。太小了中断太频繁,CPU受不了;太大了延迟高,而且需要更大的连续物理内存。实测下来,每个缓冲区4KB到64KB是比较甜的点。如果数据率特别高,可以用64KB;如果对延迟敏感,用4KB。
注意:描述符环的物理地址必须连续,而且要对齐到DMA引擎要求的边界。Xilinx AXI DMA要求描述符起始地址32字节对齐,这个在申请内存时就要保证。
2.2 内核驱动的内存申请策略
在ARM64 Linux上申请DMA内存有几种方式,各有适用场景。
CMA(Contiguous Memory Allocator)是最常用的。它在系统启动时预留一块连续物理内存,驱动通过dma_alloc_coherent申请。优点是保证物理连续,缺点是预留的内存如果不用就浪费了。对于采集卡这种专用设备,CMA是首选。
DMA池(dma_pool)适合小块内存的频繁申请释放,比如描述符本身的内存。它从CMA或者系统内存里切小块,管理开销小。
预留内存(reserved-memory)是在设备树里直接划一块内存给特定驱动用。这种方式最可控,但灵活性差,内存大小改不了。
我一般这样组合:用设备树的reserved-memory划一大块给DMA缓冲区,驱动启动时一次性申请好所有缓冲区,运行期间不再动态申请。描述符用dma_pool管理。这样既保证了性能,又避免了运行时的内存碎片问题。
代码上,申请DMA缓冲区的核心调用是:
buf->vaddr = dma_alloc_coherent(dev, buf_size, &buf->paddr, GFP_KERNEL); if (!buf->vaddr) { dev_err(dev, "Failed to allocate DMA buffer\n"); return -ENOMEM; }这里GFP_KERNEL表示可以睡眠等待,适合在probe函数里调用。如果在中断上下文里申请,要用GFP_ATOMIC,但DMA coherent内存一般不在中断里申请。
2.3 中断处理与下半部机制的选择
DMA传输完成中断的处理策略直接影响系统吞吐和延迟。Linux的中断处理分上半部和下半部,上半部要快进快出,下半部做实际的数据处理。
对于高速采集,我推荐用threaded IRQ。它的好处是中断处理跑在内核线程里,可以睡眠,可以用mutex,调试也方便。配置方式:
ret = devm_request_threaded_irq(dev, irq, hs_dma_hardirq, hs_dma_threadirq, IRQF_TRIGGER_RISING, "hs_dma", priv);上半部hs_dma_hardirq只做一件事:读中断状态寄存器,判断是哪个缓冲区完成了,返回IRQ_WAKE_THREAD。下半部hs_dma_threadirq做实际工作:更新缓冲区状态、唤醒等待的进程、重新提交描述符。
如果数据率特别高,中断太频繁,可以考虑NAPI或者中断合并。中断合并是在硬件层面设置一个阈值,比如累积4个描述符完成才产生一次中断。这能大幅降低中断次数,代价是延迟增加。
实操心得:在Zynq UltraScale+上,我实测过threaded IRQ和tasklet的差异。在数据率500MB/s的场景下,tasklet的CPU占用率比threaded IRQ低约15%,但threaded IRQ的代码可维护性好太多。如果CPU资源紧张,用tasklet;如果追求开发效率,用threaded IRQ。
2.4 mmap的实现细节与缓存一致性
mmap让用户态直接访问DMA缓冲区,这是零拷贝的关键。实现上,驱动要提供mmap文件操作:
static int hs_dma_mmap(struct file *filp, struct vm_area_struct *vma) { struct hs_dma_dev *dev = filp->private_data; unsigned long offset = vma->vm_pgoff << PAGE_SHIFT; unsigned long size = vma->vm_end - vma->vm_start; int i; for (i = 0; i < dev->num_bufs; i++) { if (offset == dev->bufs[i].paddr) { return dma_mmap_coherent(dev->dev, vma, dev->bufs[i].vaddr, dev->bufs[i].paddr, size); } } return -EINVAL; }dma_mmap_coherent会自动处理缓存属性,把内存标记为non-cacheable或者write-combine,这样用户态读到的就是DMA写入的最新数据,不需要手动flush cache。
但这里有个坑:如果用户态只是读数据,non-cacheable映射没问题。如果用户态还要写这块内存(比如做原地处理),non-cacheable会导致写性能极差。这时候要用dma_mmap_attrs配合DMA_ATTR_WRITE_COMBINE,或者干脆让用户态读完后拷贝到自己的缓冲区再处理。
ARM64上还要注意:如果DMA缓冲区是cacheable的,DMA写入后CPU读之前必须invalidate cache。dma_alloc_coherent返回的内存默认是non-cacheable的,所以不需要额外操作。但如果用dma_alloc_attrs配合DMA_ATTR_NON_CONSISTENT,那就要手动调dma_sync_single_for_cpu。
3. 实操过程与核心环节实现
3.1 FPGA侧DMA引擎的Verilog实现要点
FPGA侧的DMA引擎不需要太复杂,核心是一个描述符读取状态机加一个数据搬移状态机。
描述符读取状态机从BRAM里依次读出描述符,解析出源地址、目的地址、长度。数据搬移状态机根据这些信息发起AXI读请求(从数据源)和AXI写请求(到DDR)。关键是要处理好AXI的outstanding和反压。
一个简化的描述符结构定义:
typedef struct packed { logic [63:0] src_addr; logic [63:0] dst_addr; logic [31:0] length; logic [7:0] control; // bit0: 产生中断, bit1: 最后一个描述符 logic [31:0] reserved; } dma_desc_t;描述符环在BRAM里首尾相连,最后一个描述符的control字段标记为last,DMA引擎执行完后回到第一个描述符继续。这样就形成了一个无限循环的采集链路。
注意:描述符的更新要和DMA引擎的执行同步。如果驱动要修改某个描述符,必须先确保DMA引擎已经过了这个描述符。通常的做法是驱动只修改“已完成”状态的描述符,通过读硬件寄存器获取当前DMA执行到的位置。
3.2 设备树节点的配置
设备树是ARM64 Linux下硬件描述的入口。hs_dma_framework的设备树节点大概长这样:
hs_dma: hs_dma@a0000000 { compatible = "hs,hs-dma-1.0"; reg = <0x0 0xa0000000 0x0 0x10000>; interrupts = <0 89 4>; interrupt-parent = <&gic>; memory-region = <&dma_reserved>; dma-coherent; status = "okay"; }; reserved-memory { #address-cells = <2>; #size-cells = <2>; ranges; dma_reserved: dma_buffer@70000000 { compatible = "shared-dma-pool"; reg = <0x0 0x70000000 0x0 0x10000000>; // 256MB no-map; }; };dma-coherent属性告诉内核这个设备是缓存一致的,驱动里就不需要手动做cache维护。memory-region指向预留的CMA区域,驱动用of_reserved_mem_device_init来绑定。
中断号89和触发类型4(上升沿)要根据实际硬件连接来改。在Zynq MPSoC上,PL到PS的中断通常走pl_ps_irq,设备树里要对应修改。
3.3 驱动probe函数的完整流程
probe函数是驱动的入口,要做的事情包括:解析设备树、映射寄存器、申请中断、申请DMA内存、初始化描述符环、注册字符设备。
static int hs_dma_probe(struct platform_device *pdev) { struct hs_dma_dev *dev; struct resource *res; int ret; dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL); if (!dev) return -ENOMEM; platform_set_drvdata(pdev, dev); dev->dev = &pdev->dev; // 1. 映射寄存器 res = platform_get_resource(pdev, IORESOURCE_MEM, 0); dev->regs = devm_ioremap_resource(&pdev->dev, res); if (IS_ERR(dev->regs)) return PTR_ERR(dev->regs); // 2. 申请中断 dev->irq = platform_get_irq(pdev, 0); ret = devm_request_threaded_irq(&pdev->dev, dev->irq, hs_dma_hardirq, hs_dma_threadirq, IRQF_TRIGGER_RISING, "hs_dma", dev); if (ret) return ret; // 3. 初始化DMA内存 ret = hs_dma_alloc_buffers(dev); if (ret) return ret; // 4. 初始化描述符环 ret = hs_dma_init_desc_ring(dev); if (ret) return ret; // 5. 注册字符设备 ret = hs_dma_register_chrdev(dev); if (ret) return ret; // 6. 启动DMA hs_dma_start(dev); dev_info(&pdev->dev, "hs_dma initialized, %d buffers\n", dev->num_bufs); return 0; }每一步的顺序不能乱。寄存器映射必须在中断申请之前,因为中断处理里要读寄存器。DMA内存申请必须在描述符初始化之前,因为描述符里要填物理地址。字符设备注册必须在DMA启动之前,否则用户态可能在设备还没准备好时就打开。
3.4 用户态程序的编写与性能调优
用户态程序的核心逻辑是:打开设备、mmap缓冲区、poll等待数据、处理数据、更新读指针。
int fd = open("/dev/hs_dma0", O_RDWR); if (fd < 0) { perror("open"); return -1; } // 获取缓冲区信息 struct hs_dma_info info; ioctl(fd, HS_DMA_GET_INFO, &info); // mmap所有缓冲区 void *bufs[info.num_bufs]; for (int i = 0; i < info.num_bufs; i++) { bufs[i] = mmap(NULL, info.buf_size, PROT_READ, MAP_SHARED, fd, i * info.buf_size); } // 主循环 struct pollfd pfd = { .fd = fd, .events = POLLIN }; while (running) { int ret = poll(&pfd, 1, 1000); if (ret > 0 && (pfd.revents & POLLIN)) { int idx = ioctl(fd, HS_DMA_GET_READY_IDX); process_data(bufs[idx], info.buf_size); ioctl(fd, HS_DMA_RELEASE_IDX, idx); } }性能调优的关键点:poll的超时时间不要设0,否则会忙等;也不要设太大,否则延迟高。1000毫秒是个合理的默认值。process_data里不要做耗时操作,如果处理不过来,要么增加缓冲区数量,要么用多线程。
实操心得:用户态读数据时,如果只是做简单的协议解析,直接在mmap区域上操作最快。如果需要复杂计算,建议memcpy到自己的缓冲区再处理,避免长时间占用DMA缓冲区导致驱动侧无缓冲区可用。
4. 常见问题与排查技巧实录
4.1 数据丢包与缓冲区溢出
这是最常见的问题。现象是用户态读到的数据不连续,或者驱动打印“buffer overflow”日志。
排查思路分三步走。第一步,确认DMA是否真的在丢数。在FPGA侧加一个计数器,统计DMA写入的字节数;在驱动侧统计中断次数乘以缓冲区大小。两者对不上就是DMA侧的问题。
第二步,如果DMA侧没问题,看驱动侧的中断处理是否及时。用ftrace跟踪中断处理函数的执行时间,如果单次执行超过缓冲区传输时间的50%,就有溢出风险。
第三步,看用户态的消费速度。用perf看用户态程序的CPU占用,如果接近100%,说明处理不过来。
解决方案:增加缓冲区数量、减小缓冲区大小(降低单次处理延迟)、优化用户态处理逻辑、用多线程消费。
4.2 mmap返回EINVAL
这个错误通常是因为mmap的offset和驱动里注册的缓冲区物理地址对不上。检查两点:用户态传的offset是不是缓冲区物理地址;驱动里dma_mmap_coherent的size参数是不是和用户态请求的一致。
还有一个容易忽略的点:vma->vm_pgoff是页帧号,要左移PAGE_SHIFT才是字节偏移。如果驱动里直接拿vm_pgoff和物理地址比较,肯定对不上。
4.3 ARM64上的缓存一致性问题
现象是用户态读到的数据偶尔有“旧数据”,或者数据看起来是乱的。
ARM64的缓存一致性比x86复杂。如果DMA缓冲区是cacheable的,DMA写入DDR后,CPU的cache里可能还是旧数据。解决方法有两种:用dma_alloc_coherent申请non-cacheable内存,或者在每次读之前调dma_sync_single_for_cpu。
我推荐第一种,简单可靠。虽然non-cacheable内存的CPU访问速度慢一些,但对于采集场景来说,用户态主要是顺序读,性能损失可以接受。
4.4 中断不触发或触发过于频繁
中断不触发,先检查设备树里的中断号和触发类型。用cat /proc/interrupts看中断计数有没有增加。如果计数不增加,说明硬件中断没到GIC,检查FPGA侧的中断信号有没有正确连接到PS。
中断过于频繁,考虑中断合并。在FPGA侧设置一个阈值寄存器,累积N个描述符完成才拉高中断。N的计算:N = 中断处理时间 / 单个描述符传输时间。比如中断处理要10微秒,单个描述符传输2微秒,那N至少是5。
4.5 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 数据丢包 | 缓冲区不足 | 对比DMA计数和中断计数 | 增加缓冲区数量 |
| mmap失败 | offset不匹配 | 打印物理地址和offset | 修正offset计算 |
| 数据乱序 | 缓存不一致 | 检查内存属性 | 用non-cacheable内存 |
| 中断不触发 | 设备树配置错误 | 查看/proc/interrupts | 修正中断号和触发类型 |
| CPU占用高 | 中断太频繁 | perf top看热点 | 启用中断合并 |
| 吞吐量低 | 描述符太小 | 计算单次传输时间 | 增大描述符长度 |
避坑技巧:调试DMA问题时,先在FPGA侧用ILA抓AXI总线的波形,确认DMA读写正常。然后在驱动侧加printk,确认中断和缓冲区状态。最后在用户态用hexdump看数据。一层一层排查,不要跳步。
5. 性能实测与优化经验
5.1 不同配置下的吞吐量对比
我在Zynq UltraScale+ ZCU104上做过一组对比测试。FPGA侧用AXI DMA,数据源是模拟的递增计数器,DMA位宽128bit,时钟250MHz。ARM64是Cortex-A53@1.5GHz,Linux 5.15。
| 缓冲区大小 | 缓冲区数量 | 中断方式 | 吞吐量 | CPU占用 |
|---|---|---|---|---|
| 4KB | 16 | threaded IRQ | 680MB/s | 22% |
| 4KB | 32 | threaded IRQ | 920MB/s | 28% |
| 16KB | 16 | threaded IRQ | 1.1GB/s | 18% |
| 16KB | 16 | tasklet | 1.2GB/s | 12% |
| 64KB | 8 | tasklet | 1.35GB/s | 8% |
| 64KB | 8 | 中断合并(4) | 1.4GB/s | 5% |
从数据可以看出几个规律:缓冲区越大,吞吐量越高,CPU占用越低;tasklet比threaded IRQ省CPU;中断合并效果显著。
但缓冲区不是越大越好。64KB缓冲区在1.4GB/s下的传输时间是45微秒,如果用户态处理一帧数据需要100微秒,那8个缓冲区只够撑360微秒,用户态稍微卡一下就会溢出。实际选型要根据用户态的处理能力来定。
5.2 用户态零拷贝处理的实现技巧
用户态拿到mmap地址后,如果只是做协议解析,可以直接在mmap区域上操作。但要注意:mmap区域是只读的(如果驱动映射为只读),写操作会段错误。
如果需要修改数据,有两种方案。方案一:驱动映射为读写,但这样DMA写入和CPU写入可能冲突。方案二:用户态memcpy到自己的缓冲区再修改。我推荐方案二,安全且不影响DMA。
对于需要低延迟的场景,可以用ioctl直接触发一次DMA传输,而不是等中断。这种方式适合“请求-响应”模式,不适合连续采集。
5.3 多线程消费的设计
当单线程处理不过来时,可以用多线程。每个线程负责一个或多个缓冲区,通过ioctl获取缓冲区索引,处理完后释放。
关键是要避免锁竞争。驱动侧用原子变量管理缓冲区状态,用户态用无锁队列传递缓冲区索引。如果实在需要锁,用pthread_spinlock而不是pthread_mutex,减少上下文切换。
实操心得:多线程消费时,线程数不要超过CPU核心数。在A53上,4个线程基本能跑满DMA带宽。线程太多反而因为调度开销导致性能下降。
6. 框架的扩展与适配
6.1 适配不同厂商的DMA IP
hs_dma_framework的驱动层和FPGA逻辑层是解耦的,适配不同DMA IP只需要改FPGA侧的描述符格式和寄存器定义。
Xilinx AXI DMA:描述符格式是固定的,驱动里用xilinx_dma的寄存器偏移。Intel mSGDMA:描述符是prefetch架构,需要配置descriptor controller。自己写的DMA:完全自定义,灵活但工作量大。
适配的关键是抽象出统一的描述符操作接口。驱动里定义struct hs_dma_ops,包含submit_desc、get_completed、reset等函数指针。不同IP实现不同的ops,上层逻辑不变。
6.2 在国产ARM64平台上的移植
国产ARM64平台(如鲲鹏、飞腾、瑞芯微)的移植主要注意三点。
第一,设备树的中断控制器节点可能不同。鲲鹏用GICv3,飞腾用GICv2,设备树里的interrupt-parent要对应修改。
第二,DMA内存的物理地址范围可能受限。有些平台的PCIe设备只能访问特定地址范围的内存,CMA区域要划在那个范围内。
第三,内核版本差异。国产平台往往用较老的内核(4.19或5.10),dma_alloc_coherent的API可能有差异。移植时先确认内核版本,再查对应的API文档。
6.3 与ROS等中间件的集成
如果采集的数据要送给ROS做后续处理,可以在用户态程序里把数据封装成sensor_msgs::Image或自定义消息,通过ros::Publisher发布。
关键是要控制发布频率。ROS的发布是异步的,如果发布太快,消息队列会堆积。建议用ros::Publisher的getNumSubscribers判断有没有订阅者,没有订阅者就不发布,节省CPU。
对于高数据率场景,可以用nodelet把发布节点和订阅节点放在同一个进程里,避免进程间拷贝。
7. 调试工具与手段
7.1 FPGA侧的ILA调试
ILA(Integrated Logic Analyzer)是FPGA调试的利器。在DMA的AXI接口上挂一个ILA,抓取读写通道的握手信号、地址、数据。
触发条件可以设成“写地址通道握手且写数据通道握手”,这样能抓到实际的DMA传输。观察awvalid和awready是否同时为高,wvalid和wready是否同时为高。如果awready一直为低,说明DDR控制器反压了,要检查DDR的带宽配置。
7.2 驱动侧的ftrace和perf
ftrace可以跟踪中断处理函数的执行时间和调用次数。配置方法:
echo function_graph > /sys/kernel/debug/tracing/current_tracer echo hs_dma_threadirq > /sys/kernel/debug/tracing/set_graph_function cat /sys/kernel/debug/tracing/trace_pipeperf可以看CPU热点:
perf top -g perf record -g -a sleep 10 perf report如果memcpy或copy_to_user出现在热点里,说明还有拷贝操作没优化掉。
7.3 用户态的strace和gdb
strace看系统调用:
strace -T -e trace=ioctl,poll,mmap ./hs_dma_app-T显示每个系统调用的耗时。如果ioctl耗时超过10微秒,说明驱动里的操作太重,要优化。
gdb看用户态程序的卡点:
gdb -p $(pidof hs_dma_app) (gdb) bt (gdb) info threads如果线程卡在poll上,说明没有数据可读,检查DMA是否在运行。
8. 项目实战中的经验沉淀
8.1 从原型到产品的几个关键改进
原型阶段能跑通就行,但产品化要考虑更多。第一是错误恢复:DMA引擎卡死了怎么办?驱动要能检测到超时并复位DMA。第二是热插拔:设备重新加载时,用户态程序要能重新打开设备。第三是电源管理:系统休眠时DMA要暂停,唤醒后恢复。
错误恢复的实现:在驱动里加一个定时器,如果超过预期时间没有中断,就认为DMA卡死,执行复位流程。复位流程包括:停止DMA、清空描述符环、重新初始化、重启DMA。
8.2 长时间运行的稳定性测试
采集卡往往要连续运行几天甚至几周。稳定性测试要覆盖:内存泄漏(用kmemleak检测)、中断丢失(统计中断计数和预期值的偏差)、数据错误(用已知模式的数据源校验)。
我一般会跑一个72小时的连续采集测试,数据源用PRBS(伪随机二进制序列),用户态校验接收到的数据是否和PRBS匹配。如果有误码,说明链路有问题。
8.3 不同Linux发行版的适配
Ubuntu、Debian、CentOS、Kylin的驱动编译环境不同。主要差异在内核头文件的路径和编译选项。建议用DKMS(Dynamic Kernel Module Support)管理驱动模块,这样内核升级后驱动能自动重新编译。
DKMS的配置:
dkms add -m hs_dma -v 1.0 dkms build -m hs_dma -v 1.0 dkms install -m hs_dma -v 1.0dkms.conf里指定源码路径和编译命令。这样在不同发行版上都能用统一的方式安装驱动。
8.4 性能瓶颈的定位方法论
遇到性能问题,不要瞎猜,按数据流方向逐段测量。FPGA侧测DMA写入速率,驱动侧测中断响应时间和缓冲区周转时间,用户态测处理时间和消费速率。哪一段的速率最低,瓶颈就在哪。
我常用的测量点:FPGA的DMA字节计数器、驱动的中断时间戳、用户态的clock_gettime。三个时间戳一对比,瓶颈一目了然。
最后分享一个小技巧:在驱动里加一个
debugfs节点,实时显示缓冲区状态、中断计数、DMA错误计数。调试时cat一下就能看到关键信息,比printk方便得多。