尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

高速采集不掉数据:FPGA DMA与Linux零拷贝驱动框架设计

高速采集不掉数据:FPGA DMA与Linux零拷贝驱动框架设计 高速采集项目做多了你会发现ADC选型、前端电路、采样时钟这些硬件问题多数能靠成熟方案解决真正让项目反复返工的反而是数据从FPGA侧搬到Linux应用层这条链路。我之前做过一套触发式采集系统ADC以200MSPS采样、12bit精度单通道原始数据率折算下来接近400MB/s。最初的实现是FPGA每凑满一个4KB帧就触发一次中断驱动在中断处理函数里把数据拷贝到用户缓冲区。单个帧拷贝看起来只要几微秒但连续跑起来之后每过几秒就丢一帧。这个问题排查了整整两天最后定位到中断底半部的调度抖动和锁竞争——CPU偶尔没能在下一帧DMA回写之前取走上一帧数据缓冲区直接被覆盖了。后来我把整条通路重写了一遍FPGA端用描述符链表驱动DMA引擎采样数据沿着AXI总线直接写入预先分配好的DDR缓冲区Linux侧用环形缓冲加mmap零拷贝方式交给用户态驱动只在中断出现时更新读写指针。这套框架稳定运行在ARM64平台上兼顾了吞吐、时延和可移植性名字就叫hs_dma_framework。这篇文章把当初的设计思路、踩坑过程和最终实现细节完整写出来给正在做FPGA高速采集、Linux驱动或ARM64嵌入式平台数据搬运的工程师一个可直接参考的骨架。1. 为什么需要hs_dma_framework一次丢数据事故的复盘1.1 高速采集场景的原始诉求高速数据采集的场景比大多数人想得更普遍。通信基站的宽带数字中频、雷达回波采样、光谱仪、高带宽示波器、工业视觉在线检测都会面临同一个问题前端ADC或传感器产生的数据率从几十MB/s到几百MB/s不等连续不断地涌向处理器。拿比较典型的256MSPS、14bit ADC来说单通道数据率就是256M乘以2字节约512MB/s。就算后端做了DDC降速送到处理器这边的有效数据率也常常在100MB/s以上。这个量级下传统的中断逐包搬运方案会立刻暴露出问题——CPU的工作远不止搬数据它还要跑协议栈、响应控制指令、维护系统调度。一旦中断频率高到一定程度系统整体表现就变得很不稳定。纯轮询策略也不现实。轮询会持续占用CPU时间片而且为了不错过新数据轮询间隔必须很短导致CPU空转浪费。在ARM64嵌入式平台上CPU核数有限功耗和散热也有约束不能靠堆算力解决搬运问题。这个阶段就该让DMA上场让数据走硬件通道直接进入内存CPU只负责初始化描述符、响应必要的中断和处理指针。1.2 事故复盘中断延迟抖动是怎么拖垮吞吐的那次丢数据事故的排查链路现在回想起来非常典型。一开始我怀疑FPGA侧的FIFO溢出于是在FPGA逻辑里加了计数器对每个包编号并记录时间戳结果硬件侧一切正常数据完整无损。接着在Linux驱动里记录中断到达时刻和用户态实际读走数据的时刻发现两者之间的差值抖动得很厉害有时候只有十几微秒有时候冲到几十毫秒。问题出在数据传输链条过长。每完成一个4KB帧DMA回写描述符并触发中断ISR在中断上下文里做标记随后唤醒内核线程内核线程再从DMA缓冲区拷贝到用户缓冲区。每一步都有调度延迟、锁等待、cache miss的可能。当数据持续高速到达时只要用户态读取速度在某段时间低于DMA写入速度缓冲区就会被覆盖于是表现为周期性丢包。那次排查验证了一个结论中断加拷贝的模式只能在数据率低于某个阈值时工作比如几十MB/s以下。数据率一旦上去就必须让DMA硬件直接面对稳定分配的缓冲区驱动和应用层只在正确的时机消费数据而不是每次都搬运数据。1.3 这套框架要解决的三个核心问题hs_dma_framework的目标很明确就是要同时解决三个问题。第一是吞吐持续稳定。DMA引擎用描述符链表描述每段数据的去向硬件自己沿着链表搬运不依赖CPU逐包处理吞吐由总线带宽决定而不是由CPU占用率决定。第二是时延可控。数据到了DDR之后用户态通过mmap直接访问没有多余拷贝。驱动用水位线和中断合并策略控制通知频率让应用层既不会因为中断太频繁而空转也不会因为通知太迟而增加时延。第三是从一套代码部署到不同FPGA和不同ARM64主板的能力。FPGA端的DMA引擎封装成可配置的IP核驱动通过设备树来适配中断号、地址空间、通道数量这些差异换板卡时不改驱动源码只改dts。这三点拆开看都不算新奇但一体化放在同一个框架里并且把ARM64 Linux端的各种细节做扎实才是hs_dma_framework的价值所在。2. 整体架构与分层思路FPGA、DMA、Linux驱动各自该干什么2.1 三层数据通路采集、搬运、消费hs_dma_framework在逻辑上分成三层职责边界非常清楚。最前端是FPGA采集层。ADC、LVDS、MIPI这类接口信号进入FPGA之后先做帧同步、格式转换、数字滤波、降采样这些预处理。这部分工作在FPGA内部完成目的是把原始采样数据整理成后续处理和传输都更方便的形态。预处理后的数据被写入FPGA内部的AXI-Stream或AXI-MM接口等待DMA引擎搬运。中间是DMA传输层。DMA引擎从FPGA侧的数据FIFO中读出数据按照描述符指定的目标地址、长度把数据写入DDR物理内存。搬运完成后DMA引擎把状态回写到描述符并根据配置触发中断。这一层的核心是描述符链表管理、地址映射和中断控制。最上层是Linux消费层。驱动负责分配DMA缓冲区、注册中断、维护环形缓冲读写指针用户态程序通过mmap直接操作这些内存算法线程拿到数据后做FFT、解调、图像重建等上层应用。数据流方向可以用一句话概括ADC数据进入FPGA预处理逻辑沿AXI接口流向DMA引擎DMA按描述符写入DDR驱动映射给用户态应用直接消费。整个路径上除了初始化阶段CPU基本不触碰数据本身。2.2 硬件形态选型集成SoC方案还是FPGA加ARM64主控方案在ARM64平台上做FPGA高速采集硬件形态主要有两种选择hs_dma_framework对这两种都做了适配。一种是用带ARM硬核的FPGA芯片典型的是Zynq UltraScale MPSoC这类。FPGA可编程逻辑和ARM64 Cortex-A53/A72核心封装在同一颗芯片里PL到PS通过高性能AXI端口直连物理距离近描述符访问和数据搬运路径都短时延最低。缺点是整套系统绑在FPGA厂家的工具链里升级ARM侧算力只能换整颗芯片而且这类产品的采购周期和成本都比较敏感。另一种是独立FPGA加独立ARM64主控。FPGA做采集和DMA引擎ARM64主控跑Linux和应用两者通过PCIe或者类AXI桥接协议通信。这样FPGA和主控可以分别选型采集能力和计算能力解耦升级任意一侧都方便。缺点是链路中间多了一层桥接协议描述符访问可能需要通过PCIe BAR窗口间接映射中断走MSI或者桥接控制器调试复杂度上升。两种形态在框架里的差异集中在驱动初始化和地址映射部分。集成式SoC直接用物理地址和本地中断独立主控方案需要把DMA引擎的寄存器空间映射进PCIe BAR中断用MSI或者中断控制器转发。框架层面通过设备树字段区分两者驱动代码主体完全通用。方案时延灵活性成本适用场景FPGAARM64硬核SoC最低较低升级需换芯片较高对时延敏感、需要集成化的产品独立FPGA独立ARM64主控略高高可独立升级更可控需要反复迭代、算法升级频繁的平台实际项目里如果样机阶段用的是独立FPGA和ARM64主控后期定型时想换成集成SoC只要设备树里调整地址和中断描述驱动和用户态代码几乎不用动。这也是hs_dma_framework最初就把硬件差异抽象到设备树层面的原因。2.3 控制面与数据面分离、设备树里的关键描述hs_dma_framework在FPGA端把寄存器分成控制面和数据面两类。控制面寄存器包括启动、停止、复位、通道使能、描述符首地址这些由驱动通过总线直接读写。数据面则完全由DMA硬件自动运转驱动不干预。这个分离原则保证CPU只在起始和结束阶段介入稳态运行时CPU压力很小。设备树节点承担了硬件拓扑描述职责。一个典型的设备树节点大致长这样dma0: dmaa0000000 { compatible hs,dma-framework; reg 0x0 0xa0000000 0x0 0x1000; interrupts 0 46 4; dma-channel-count 4; dma-descriptor-size 64; dma-buffer-size 1048576; dma-buffer-count 16; dma-interrupt-coalesce 32; };reg字段给出DMA引擎寄存器组的物理地址和大小interrupts字段描述中断控制器类型、中断号和触发方式后面几个自定义字段告诉驱动应该分配多少个通道、描述符多大、每块数据缓冲区多大、每次中断至少合并多少个描述符。驱动在probe函数里通过of_irq_get、of_address_to_resource这些接口把这些信息解析出来再按照参数初始化内部数据结构。把通道数、缓冲大小这类参数放到设备树而不是编译进代码受益最大的其实是整机调试阶段。同一块FPGA逻辑驱动不用改一行代码只要改dts参数就能适配不同的缓存需求和中断合并策略。3. FPGA端DMA引擎设计描述符链表、门铃寄存器与状态机的取舍3.1 为什么选择描述符链表而不是固定地址块搬运DMA搬运数据最原始的做法是CPU告诉DMA引擎一个固定的源地址、目的地址和长度搬完一次再配置一次。这种方式在数据传输间隔较长的场景够用但在持续高速采集场景里CPU会被频繁打断总线也会因为配置寄存器而出现空闲气泡。hs_dma_framework采用描述符链表方式这是业内做批量搬运的主流做法。CPU预先在内存中建立一段描述符数组每个描述符描述一段数据的目标地址、长度、属性以及下一个描述符的地址。然后CPU只需要把第一个描述符的地址写入DMA引擎的门铃寄存器DMA引擎会自主读取描述符、搬运数据、更新状态然后沿着next指针继续处理后面的描述符。描述符结构大致如下struct hs_dma_desc { u64 src_addr; // 数据源地址FPGA端FIFO或AXI地址 u64 dst_addr; // 目标地址DDR物理地址 u32 length; // 数据长度 u32 flags; // 描述符状态与属性 u64 next_desc; // 下一个描述符地址 u32 user_tag; // 用户标记可以放序列号或时间戳 u32 reserved; // 对齐保留 };描述符在初始化阶段由驱动一次性分配好FPGA端的DMA引擎只负责读取。一个描述符长度为64字节刚好对齐到cache line读写效率比紧凑排列更好。环形链表的好处是驱动可以预先填充一整批描述符DMA从链表头跑到链表尾后重新回到头部形成连续搬运不需要CPU在每段数据之间介入。3.2 AXI DMA、DataMover与自研引擎的取舍FPGA端DMA引擎的实现方案hs_dma_framework迭代过三个版本。第一个版本用了FPGA厂商提供的AXI DMA IP核配置简单官方文档齐全支持SGDMA模式对Quick入门很友好。但用到深度定制时会发现描述符布局和中断控制都是固定的想加通道统计计数器或者调整回写机制都需要额外包逻辑资源消耗也不小。第二个版本换成AXI DataMover这个IP更底层把MM2S和S2MM两条通道分开描述符怎么组织完全由用户自己控制。DataMover的原始吞吐比AXI DMA高因为它省掉了多余的地址管理逻辑更贴近直接内存访问的本质。代价是所有描述符解析、状态机错误恢复、队列管理都要自己写调试门槛明显提高。第三个版本就是我们最终在hs_dma_framework里保留的自研DMA引擎。它的规模不大只实现本框架需要的功能描述符读取、数据搬运、状态回写、中断计数。因为只保留了必要的逻辑FPGA资源占用比前两个方案都少还能在主状态机旁边挂寄存器统计实际搬运字节数和错误计数。自研引擎的开发工作量集中在AXI总线事务的时序处理上但只要把一次burst读、一次burst写的时序跑对后面扩展通道只是复制实例的问题。方案易用性性能可裁剪性调试难度AXI DMA IP高中上低中AXI DataMover中高中高自研DMA引擎低高高高如果你没有特殊功能需求直接用AXI DMA IP能省很多时间如果吞吐指标紧张DataMover配合自研描述符处理是更性价比的选择。hs_dma_framework选择自研引擎主要考虑了后续向多通道、带时间戳场景扩展的空间。3.3 状态机、中断策略与Cache一致性处理自研DMA引擎的主状态机并不复杂核心循环是IDLE等启动读取描述符后进入转移状态按AXI burst逐个搬运数据搬运完成后把状态字段回写到描述符对应位置然后判断是否到达链表末尾没到就继续读取下一个描述符。中断策略决定了CPU被唤醒的频率。hs_dma_framework用一个中断计数器寄存器每搬运完一个描述符计数器加一到了预设阈值比如32才触发一次真实中断。这个阈值放在设备树的dma-interrupt-coalesce字段里可以在不重新编译驱动的情况下调整。中断合并的本质是拿时延换CPU开销阈值越小CPU越及时阈值越大系统吞吐越稳定。Cache一致性是FPGA端设计中最容易踩雷的部分。描述符要被DMA引擎写回状态又要被CPU读取回收数据缓冲区要被DMA写入又要被用户态读取。对于CPU侧如果这些内存被映射成cacheable就需要通过一致性接口或者在驱动里做cache flush操作。hs_dma_framework在FPGA端避免直接处理一致性问题把这块控制权交给驱动层FPGA侧只按照AXI协议完成读写。实践中的经验是描述符用dma_alloc_coherent分配保证CPU和硬件看到的内容始终一致数据缓冲区根据场景选择是否做一致映射高频写入的大块数据用非一致映射加显式同步减少每次传输前的cache刷写开销。4. Linux驱动层落地设备树、中断线程、mmap环形缓冲的配套实现4.1 驱动模块的职责边界与初始化流程Linux驱动在hs_dma_framework里承担的角色是硬件抽象和资源调度不碰数据内容。它的职责可以拆成四块探测并初始化DMA引擎、分配并管理缓冲区、响应中断并维护环形缓冲读写指针、向用户态提供mmap和ioctl接口。初始化的核心流程按照probe函数展开。第一步解析设备树拿到寄存器地址、中断号、通道数、缓冲参数。第二步用platform_get_resource和ioremap把DMA引擎寄存器映射到虚拟地址空间。第三步为每个通道分配DMA缓冲区缓冲区的数量和大小决定系统能容忍的延迟上限。第四步初始化描述符链表把所有描述符按顺序串起来写DMA引擎的门铃寄存器使能对应通道。第五步注册中断把中断处理函数挂到设备树指定的中断号上。中断处理建议用request_threaded_irq而不是传统的request_irq加tasklet组合。原因是高速采集场景下主中断处理函数只需要做极短的事比如读取状态寄存器、更新写指针、调用wake_up真正复杂的描述符回收和队列管理放到threaded irq的线程化上下文里执行。这样中断上半部的耗时短中断期间的关闭时间可控下半部又不会被中断上下文限制束缚可以安全地调用锁和内存分配。4.2 环形缓冲区的读写同步与内存屏障高速数据通路最怕生产者与消费者速度不匹配。hs_dma_framework用环形缓冲区来衔接硬件生产者和用户态消费者。生产者是DMA引擎它按照描述符连续写入DDR消费者是用户态应用它通过mmap读取同一段DDR。驱动内部维护两个核心变量hw_write_index表示DMA引擎已经写到的描述符位置sw_read_index表示用户态已经读到的位置。DMA引擎每搬运完一个描述符会把完成状态回写到描述符的flags字段。驱动在中断处理中扫描新完成的描述符把hw_write_index更新到最新位置。用户态读完数据后通过ioctl把新的sw_read_index告诉驱动。这里必须处理多核之间的内存可见性问题。ARM64处理器对内存访问做了重排优化如果没有适当的内存屏障CPU的某个核可能看不到另一个核刚刚更新的指针。实践中的处理方式是驱动更新hw_write_index后用smp_wmb保证写屏障用户态读取前用smp_rmb保证读屏障。对于驱动与DMA硬件之间的数据同步用的是dma_wmb和dma_rmb它们确保在DMA操作和CPU访问之间建立正确的顺序约束。环形缓冲区还有一个需要加强的细节预留一定的空位作为水位线。当hw_write_index追击sw_read_index到只剩一个描述符距离时驱动标记缓冲接近满状态此时如果用户态没有及时消费可以选择丢弃新数据或者触发更高优先级通知。hs_dma_framework的做法是允许配置taildrop策略在硬实时场景下优先保证已进入缓冲的数据完整而不是一味追新。4.3 用户态零拷贝访问mmap加文件描述符事件通知为什么不建议用户态用read系统调用因为read会把DMA缓冲区里的数据再拷贝一次到用户态几百MB/s的数据率下memcpy的开销非常可观而且会造成cache污染干扰同一CPU上的其他实时任务。hs_dma_framework通过mmap把DMA缓冲区和描述符元数据区映射到用户态。应用启动后打开设备节点调用ioctl获取缓冲区布局信息然后对数据区和元数据区分别执行mmap。之后用户态直接通过指针读写这些内存不再经过系统调用读数据就变成从环形缓冲区指针位置读取。为了让用户态知道什么时候有新数据到来驱动实现了poll接口用poll_wait把等待队列挂接到中断处理路径。用户态可以启动一个epoll循环或者单独的读取线程块在poll上一旦新数据完成中断处理函数更新hw_write_index并调用wake_uppoll立即返回可读事件应用马上处理新数据。这里的接口设计做了明确分层。ioctl命令包括启动DMA、停止DMA、查询缓冲区信息、更新读指针。mmap区域分成两个数据区映射DMA缓冲区本身元数据区映射描述符和指针结构。应用可以只通过访问元数据区的写指针来感知新数据而不必每次调用系统调用。把控制操作和数据访问分开之后稳态数据路径上几乎没有任何系统调用开销这套设计在高速场景下非常关键。5. ARM64平台性能调优与实测踩坑记录5.1 吞吐实测的完整过程和瓶颈定位方法框架跑通之后第一件事就是量化性能。实测环境是ARM64主控加FPGA采集板FPGA端持续产生已知模式的数据用户态应用负责校验数据连续性并统计吞吐。测试程序从启动DMA开始FPGA不断输出递增计数的pattern用户态通过mmap读取数据后校验计数是否连续同时用clock_gettime统计每个时间窗口内处理的数据量。用递增pattern的好处是一旦DMA搬运或者驱动处理有乱序、丢段计数序列立刻出现跳变问题可以精确定位到数据路径的某个环节。实测下来memcpy模式在数据率约200MB/s时开始出现丢包CPU占用率接近满核。改成mmap零拷贝之后数据率提升到600MB/s仍然稳定运行CPU占用率只消耗在应用校验逻辑上。瓶颈从CPU转移到了DDR带宽和AXI总线带宽这才是高速采集应该有的状态。瓶颈定位主要靠两端统计。FPGA端维护DMA引擎忙闲比寄存器如果引擎长时间处于等待总线状态问题出在总线竞争驱动端用perf统计中断次数和中断处理耗时如果中断合并阈值设得太低中断次数会飙升CPU整体负载被拉升。两端数据对齐后才能判断该调FPGA的burst长度还是调驱动的中断策略。5.2 ARM64特有的缓存一致性与地址映射坑在ARM64平台上踩得最深的坑是缓存一致性。有段时间数据偶尔乱序甚至读到旧数据查了FPGA时序又查了驱动程序逻辑都没发现问题。最后用调试寄存器看cache状态才发现用户态访问DMA缓冲区时命中了CPU cache里的旧副本而DMA硬件已经写入了新的数据到DDRCPU根本不知道缓存已经失效。解决思路分成两部分。描述符区域必须用dma_alloc_coherent分配它保证该区域的页缓存属性不允许CPU缓存CPU和硬件访问同一份物理内存时看到的始终是最新内容。数据缓冲区不一定非要一致映射但要在驱动里通过dma_map_single或dma_map_sg建立映射关系并在DMA传输前做sync操作。实际性能测试显示大块连续数据在非一致映射下略优因为每次传输前只需要做一次cached刷新而不是让缓存一直保持同步。还有一个容易忽视的坑是缓冲区页对齐。用户态mmap映射DMA内存时内核的remap_pfn_range要求起始地址按页对齐。如果缓冲区地址只按4KB对齐而缓冲区跨了多个页映射就会出现问题甚至访问异常。hs_dma_framework在分配缓冲区时强制以2MB或1MB对齐这样既满足页表映射要求也避免TLB在连续大流量下频繁失效。5.3 中断频率、CPU亲和性与批处理参数的搭配中断调优对ARM64平台至关重要。最初配置是每搬运一个描述符就触发一次中断中断频率高到每秒几十万次CPU大量时间花在中断入口和出口上吞吐反而下降。后来把中断合并阈值从1调到32中断次数降到每秒几千次吞吐明显回升CPU占用率也降下来了。中断绑定同样重要。ARM64的多核处理器上把DMA中断亲和性固定到一个专用核同时把用户态处理线程绑到另一个核可以避免中断处理和数据处理抢同一个CPU资源。具体操作是修改/proc/irq/对应中断号的smp_affinity列表把bit位设置到指定CPU。批处理参数还需要和缓冲区大小配合。缓冲区块大、描述符多单次中断可以处理的批数据就大系统倾向于批量搬运缓冲区块小即使中断合并阈值高一次合并的数据量也有限。这里没有一个固定最优值我一般在性能测试里做梯度扫描中断阈值从1、2、4、8一直测到128观察吞吐和CPU占用率的交叉点再结合应用允许的最大时延确定最终参数。6. hs_dma_framework的扩展空间与复用经验6.1 从ADC扩展到图像采集和相控阵应用hs_dma_framework的第一版主要面向高速ADC数据采集但框架本身并不绑定ADC。把FPGA前端逻辑替换成MIPI CSI-2接收或者LVDS图像接收模块数据格式从采样点改成像素行DMA传输和应用层消费的逻辑完全复用。图像类的数据天然适合描述符链表描述。一幅图像可以切成多个块每个块用一段描述符指定它在DDR中的位置FRAME同步信息放在最后一个描述符的user_tag字段里。用户态重组帧时只需要检查user_tag的变化而不必逐个像素判断边界。对这种应用FPGA端要额外处理行长对齐因为AXI burst长度通常要求事务边界对齐到一定字节数图像行宽不一定满足这个约束。相控阵这类多通道同步采集场景框架需要增加的是时间戳和同步触发机制。每个通道一组描述符链表FPGA端在每包数据末尾填入全局时间计数器的快照用户态按时间戳对齐多通道数据做相位计算。描述符保留的user_tag字段正好充当这个角色驱动只需要把它原样透传不需要理解具体含义。6.2 用QEMU模拟ARM64环境调试驱动框架驱动开发不总是能拿真板做调试hs_dma_framework从早期就引入了QEMU模拟环境。在x86开发机上用qemu-system-aarch64配合virt机器跑ARM64内核镜像把驱动模块加载进去至少可以验证probe流程、设备树解析、mmap映射和poll事件这些不依赖真实DMA硬件路径的逻辑。QEMU不会真正模拟FPGA DMA硬件但可以通过注册一个简单的platform设备驱动来模拟寄存器读写行为。这种做法在框架开发初期价值很大因为驱动代码的数据结构、环形缓冲逻辑、用户态接口可以先在模拟环境下调通等真实硬件到了之后只关注与总线时序相关的部分。调试内核模块时配合QEMU的gdb stub可以在驱动代码里打断点逐步查看描述符链表的更新过程比直接在板子上printk效率高得多。需要说明的是QEMU无法复现cache一致性和DMA性能问题所以它只能作为逻辑验证工具不能替代真机压测。框架的最终验收还是必须在ARM64实机上完成。6.3 哪些改动该留到FPGA哪些该留到驱动哪些该留给应用层使用hs_dma_framework一段时间之后我最大的体会是分层边界越清楚项目迭代越快。FPGA端只负责接口时序、数据格式和DMA引擎行为。换一种ADC修改采样接口和数据打包逻辑加一个预处理模块插入数字滤波或抽取。这些改动完全发生在FPGA内部不影响驱动接口。驱动层负责硬件资源抽象和性能参数。中断合并阈值、缓冲块大小、通道映射、设备树字段这些是驱动层面的参数。性能调优通常不涉及FPGA逻辑只在设备树里调整参数后重启驱动模块即可。应用层负责算法和业务流程。FFT、解调、图像重建、数据存储都在用户态完成通过标准接口与框架交互根本不需要知道硬件细节。如果你也准备做类似的高速采集平台先把描述符格式和环形缓冲的通信协议钉死再动FPGA和驱动代码。宁可先写一个低速但正确的最小闭环从ADC读几个数到用户态打印出来再逐步加并发、加预处理、加性能优化。这条路线虽然看起来慢但每一步都有确定的验证方法最终收敛到稳定版本的速度反而最快。
返回列表