做过视频采集驱动的人对videobuf2和DMA内存管理这两个词应该都不陌生。VB2是Linux内核V4L2框架里负责缓冲区管理的中枢几乎所有现代摄像头、采集卡、ISP驱动都建立在它之上DMA内存管理则是这些驱动里最容易出问题的部分——缓存、映射、同步一环出错就是花屏或者直接死机。我没少在这种问题上熬到半夜后来把VB2的源码通读了一遍再把MMAP、USERPTR、DMABUF三种模式都实际跑过才算真正理顺了里面门道。这篇文章把我理解的videobuf2机制从头到尾讲一遍vb2_queue、vb2_buffer、vb2_mem_ops这些核心对象怎么协同三种内存模式在DMA层面分别是怎么运作的以及我接入PCIe采集卡驱动时做DMA映射和零拷贝共享的实操经验。适合正在写采集类驱动的内核工程师想弄明白V4L2缓冲区生命周期和DMA同步原理的应用开发者也值得看看。1. 为什么驱动都在往videobuf2上靠1.1 老videobuf的局限耦合太深在V4L2还是早期体系的年代每个视频驱动都要自己维护缓冲区状态、mmap映射关系和ioctl分发逻辑。后来内核社区抽象出一个videobuf框架也就是大家口中的videobuf1把不少通用逻辑收拢了但它的设计有个先天问题缓冲区分配和用户态交互方式耦合得太死框架内部实现也偏僵化。当视频设备从隔行模拟采集走向高清、多平面格式的时候老框架就顶不住了。多plane格式意味着一个buffer里要管理多块独立内存Y平面、UV平面每块内存的偏移、长度、DMA地址都要单独处理更麻烦的是零拷贝需求设备之间要直接共享缓冲区这需要一套标准化的跨设备内存共享机制老videobuf完全没有这个能力。想给它打补丁改动量跟重写差不多而且不同厂商驱动实现出来的行为还不一致上层应用根本没法依赖。所以内核社区干脆推倒重来设计videobuf2VB2核心思路就是把“内存从哪来”和“硬件怎么用”彻底解耦。这个决策后来被证明非常正确今天大家看到的DMABUF、request机制、各种多平面格式都是在这个解耦基础之上长出来的。1.2 职责分离驱动接住自己关心的那部分VB2把缓冲区的完整生命周期拆成了几个递进阶段分配与释放alloc/put、映射与同步map/sync、入队与出队qbuf/dqbuf、硬件流转streamon/streamoff。每个阶段都对应一组回调驱动接住自己关心的那部分其余通用逻辑全部交给框架。这样做的好处是实打实的同一套驱动代码只要换一个mem_ops就能支持完全不同的内存来源。比如驱动硬件DMA逻辑时你不需要操心这块内存是内核分配的、用户态传进来的、还是某个GPU导出的DMABUF你拿到的就是一个已经映射好的DMA地址DMA控制器视角的地址不一定等于物理地址。新平台特性比如IOMMU、cache coherent、安全DMA区域也都能在框架层统一处理不用每个驱动各写一套。所以当你看到某个驱动代码里只有几个简单的回调却能正常工作别觉得它简单。框架帮它扛了大部分复杂度你要做的就是在正确的地方填正确的逻辑。2. 核心数据结构全景2.1 vb2_queue一切操作的大门驱动跟VB2打交道最先遇到的就是struct vb2_queue。这个结构体描述了一块视频DMA通道的所有配置从缓冲区类型、支持的内存模式、内存操作符到驱动自己的回调函数全在里头。我贴一段采集驱动里常用的初始化代码注释直接标了关键字段static struct vb2_queue my_vb2_q; static struct my_drv_ctx ctx; my_vb2_q.type V4L2_BUF_TYPE_VIDEO_CAPTURE; my_vb2_q.io_modes VB2_MMAP | VB2_USERPTR | VB2_DMABUF; my_vb2_q.mem_ops vb2_dma_contig_memops; my_vb2_q.ops my_vb2_ops; my_vb2_q.buf_struct_size sizeof(struct my_buffer); my_vb2_q.min_buffers_needed 4; my_vb2_q.gfp_flags GFP_DMA32; my_vb2_q.timestamp_flags V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC; my_vb2_q.drv_priv ctx; my_vb2_q.lock ctx.mutex; my_vb2_q.dev pdev-dev; vb2_queue_init(my_vb2_q);type字段好理解告诉你这是视频采集还是输出。io_modes声明驱动支持哪几种内存模式VB2会把不支持的模式直接挡在门外。mem_ops特别关键它决定缓冲区怎么分配和映射比如vb2_dma_contig_memops要求分配物理连续内存vb2_dma_sg_memops在非连续情况下也能干活。buf_struct_size是给每个buffer额外分配的自定义结构体大小。如果你的驱动需要在每个缓冲区上记录硬件描述符、映射的sg_table、帧序号这些信息就定义自己的struct my_buffer把大小填这里。VB2分配vb2_buffer结构时会连带把你自定义的那块内存分配好然后用container_of互转。还有一个容易被忽略的字段是min_buffers_needed。streamon后驱动需要多少缓冲区才能开始硬件传输就填多少。如果应用只入队一个buffer就想streamonVB2会一直憋着直到数量够才开始调用start_streaming。这个阈值不是随便填的填少了DMA深度不够容易丢帧填多了低内存场景会初始化失败。2.2 vb2_buffer与缓冲区状态机用户态看到的buffer在内核里就是struct vb2_buffer。一个buffer包含若干plane每个plane对应一块独立内存。对单平面来说整个帧就是一块内存多平面格式则分Y和UV两块甚至更多。struct vb2_buffer { struct vb2_queue *vb2_queue; unsigned int index; unsigned int type; unsigned int memory; unsigned int num_planes; struct vb2_plane planes[VB2_MAX_PLANES]; enum vb2_buffer_state state; /* ... */ };vb2_plane里存放的是应用层需要的bytesused、length这类信息以及驱动在内核侧使用的私有映射结果比如DMA地址、sg_table指针。驱动在buf_prepare回调里填充这些内容硬件描述符用的地址就从这里拿。缓冲区状态机是理解VB2的核心。一帧buffer从用户态dqbuf得到、qbuf入队到硬件开始使用再到中断完成后回队列始终在一个明确的state里流转DEQUEUED已出队用户持有→ PREPARING正在准备→ QUEUED已入队排队等硬件→ ACTIVE硬件正在使用→ DONE硬件完成等用户出队→ DEQUEUED。如果硬件出错状态会变成ERROR用户dqbuf时拿到的不再是正常帧而是一个带出错标记的空buffer。这个状态机不是摆设。在调试时如果发现一帧数据反复出现在不同阶段或者应用dqbuf回来buffer里的内容永远是脏的第一件事就是打印每个buffer的state看是不是某次vb2_buffer_done调用漏了、重复了或者state被硬改错位。2.3 vb2_mem_ops内存操作的抽象层mem_ops是VB2最精华的抽象一套标准的函数指针集合涵盖了内存分配、映射、同步、DMA-BUF导入导出等所有底层操作alloc / put分配和释放一块适合DMA的内存mmap把内核侧缓冲区映射到用户态地址空间get_userptr / put_userptr处理用户态指针把用户页面固定住并准备DMA映射attach_dmabuf / detach_dmabuf / map_dmabuf / unmap_dmabufDMA-BUF导入导出的核心操作prepare / finish在DMA操作前后做cache同步vaddr / cookie返回内核虚拟地址或内存句柄内核提供了几个标准实现驱动直接拿来用就行mem_ops内存特性适用场景vb2_dma_contig_memops物理连续或IOMMU映射后设备连续绝大多数采集卡、ISP、显示控制器vb2_dma_sg_memops分散页聚合设备需支持SG支持分散-聚集DMA的设备vb2_vmalloc_memops虚拟内存分配适合模拟内存充足、DMA要求不高的场景我用的采集卡硬件不支持SG模式所以选了vb2_dma_contig_memops让内核分配物理连续内存。如果你的设备有IOMMU/SMMU其实分配连续物理内存的必要性就大大降低了因为IOMMU可以把离散物理页映射成连续的IOVA这时候用vb2_dma_sg_memops更灵活能避免高内存碎片下分配大块连续内存的失败率。3. DMA内存管理的底层逻辑3.1 先理解cache一致性问题DMA内存管理最核心的坑在cache一致性。CPU和设备看到的是两份不同的数据副本CPU访问的是cache里的数据设备通过DMA直接读写的是内存里的数据。如果两边数据不一致具体表现就是设备采集回来的数据CPU一读发现还是旧的或者CPU写好的命令设备DMA直接读到的却是乱码。打个比方cache是CPU的私人笔记本设备是个只管跟仓库内存打交道的人。CPU往笔记本上记了东西但还没誊到仓库设备直接从仓库拿货自然拿不到最新内容反过来设备把新货放进仓库CPU还盯着笔记本上的旧记录也看不到新东西。解决这个问题的关键就是dma_sync这类操作相当于“誊写”和“核对”的动作。Linux内核里处理cache一致性的正统方式是DMA API。简单场景下用dma_map_single/dma_unmap_single流式DMA场景下用dma_sync_single_for_device和dma_sync_single_for_cpu来切换所有权。对于视频采集设备持续写内存驱动在把buffer交给硬件前应该做dma_sync_single_for_device让CPU的脏数据先落盘硬件写完后dqbuf前做dma_sync_single_for_cpu把DMA写的内容刷回CPU可见的cache。还有一种更省心的方式是用dma_alloc_coherent分配一致性内存。这块内存在硬件设计上就是cache coherency的也就是CPU和设备访问同一份数据不需要显式同步。代价是分配和访问性能可能略低于普通流式DMA而且对地址范围有限制。VB2里vb2_dma_contig_memops分配buffer时用的正是这个路径所以用MMAP模式配合dma-contig同步问题最少。3.2 IOMMU把离散物理页变成连续IOVA提到DMA就得说IOMMU在移动SoC上一般叫SMMU。IOMMU做的事和CPU的MMU类似只不过它管的是设备视角的地址空间。没有IOMMU的设备DMA地址必须填真实物理地址这就要求buffer物理连续有IOMMU的设备设备看到的地址是IOVAIOMMU负责把IOVA翻译成物理地址物理页离散一点也问题不大。用vb2_dma_contig_memops到底分配的是什么取决于平台实现。没有IOMMU时它是物理连续内存DMA地址就是物理地址有IOMMU时底层可能走dma_alloc_attrs分配不经内核映射的分散页再通过IOMMU映射成连续的IOVA。所以驱动里拿到的dma_addr_t务必当作“设备DMA控制器视角的地址”来用别拿去直接当物理地址传给其他子系统。这个区别在调试时特别容易踩坑。我曾经在一个带SMMU的平台上把vb2_plane_dma_addr拿到的值打印出来看着挺正常实际它是IOVA。后来为了调试直接在驱动里把这个地址当物理地址去访问结果当然是访问了完全无关的内存整个系统直接挂掉。现在写驱动凡是涉及DMA地址的变量名字里一律带“dma”或“iova”前缀强制自己记住它不是物理地址。3.3 三种V4L2内存模式怎么选V4L2应用侧有三种内存模式配套的VB2实现各不相同选型的依据主要是产品和场景V4L2_MEMORY_MMAP内核分配buffer应用用mmap把它映射到用户态。最简单、最常用驱动负担小性能稳定。缺点是多了一层映射开销帧数据要在应用里搬一次才能到计算单元。V4L2_MEMORY_USERPTR应用自己分配内存把用户态指针传进来内核通过get_user_pages把页面固定再做DMA映射。省了内核分配和mmap但要求用户内存适配DMA约束对齐、连续、驻留而且管理不好容易换页崩掉。V4L2_MEMORY_DMABUF通过DMA-BUF文件描述符传递缓冲区是零拷贝的标准解法。生产者导出dmabuf消费者导入多个设备共享同一块内存。现代ISP、GPU、显示控制器之间的数据流转基本都走这条路。模式谁来分配内存典型优点典型坑点MMAP内核简单稳定驱动友好需要映射开销USERPTR应用省一次内核分配页面固定、对齐约束多DMABUF导出者零拷贝、多设备共享需要处理附加/同步时序实际项目里能用MMAP就用MMAP它是最不容易出错的。需要多设备共享数据就上DMABUF。USERPTR我建议少碰除非产品有必须的理由比如应用侧内存池已经存在且不想改架构。我见过太多在USERPTR里被用户内存生命周期坑死的项目调试成本远高于收益。4. 实操PCIe采集驱动接入vb24.1 约定缓冲区格式与数量驱动接入vb2第一步就是实现vb2_ops里的queue_setup。它的职责是跟应用协商缓冲区的格式、平面数量和大小。应用通过VIDIOC_S_FMT设置格式后queue_setup会被调用来确认每个plane的大小以及建议的缓冲区数量。static int my_queue_setup(struct vb2_queue *q, unsigned int *num_buffers, unsigned int *num_planes, unsigned int sizes[], struct device *alloc_devs[]) { struct my_drv_ctx *ctx vb2_get_drv_priv(q); if (*num_planes) { if (sizes[0] ctx-fmt.fmt.pix.sizeimage) return -EINVAL; return 0; } *num_planes 1; sizes[0] ctx-fmt.fmt.pix.sizeimage; if (*num_buffers 4) *num_buffers 4; return 0; }这里有个经验num_buffers是输入输出参数应用传一个期望值驱动可以往上调整。我一般会确保至少4个buffer打底因为采集流的DMA深度和VB2内部状态切换都需要几个buffer周转。设太少容易出现应用来不及dqbuf、驱动没有空闲buffer可以挂入队的情况表现为断断续续丢帧。设太多又浪费内存4到6个通常是比较折中的范围。alloc_devs数组用于多设备共享场景可以指定每个plane由哪个设备来分配内存比如ISP驱动的scaler输出和主线采集的plane归属不同硬件。单设备驱动直接留NULL就行。4.2 buf_prepare阶段完成DMA映射buf_prepare是每次qbueue入队前都会调用的回调在这里把buffer准备成硬件可以直接使用的状态。对dma-contig模式来说最关键的操作是从VB2拿到底层DMA地址填到硬件的描述符链表中。static int my_buf_prepare(struct vb2_buffer *vb) { struct my_buffer *buf to_my_buffer(vb); struct my_drv_ctx *ctx vb2_get_drv_priv(vb-vb2_queue); dma_addr_t dma_addr; dma_addr vb2_dma_contig_plane_dma_addr(vb, 0); if (!dma_addr) return -EINVAL; ctx-hw_desc[buf-index].dma_addr dma_addr; ctx-hw_desc[buf-index].size vb2_plane_size(vb, 0); /* 把新的待处理帧串入硬件链表 */ my_hw_queue_desc(ctx, buf-index); return 0; }注意buf_prepare每次入队都会被调用所以这一帧current被硬件占用时千万别在同一帧再次入队否则硬件链表里会挂两份相同的描述符DMA写同一块buffer两次画面会同时出现撕裂和覆盖。buf_prepare里还有一个容易被忽略的点驱动要在这个阶段检查buffer状态。如果VB2已经标记这个buffer是ERROR状态了直接返回错误比把它交给硬件要安全得多。我早期没做这个检查一帧出错的buffer继续入队硬件在异常状态上继续DMA中断处理里判定帧完成时数据早就错乱得没法看了。4.3 中断处理与vb2_buffer_done硬件完成一帧DMA后会触发中断。中断处理函数里除了清中断、读状态寄存器最核心的动作是调用vb2_buffer_done把这帧buffer从ACTIVE状态转成DONE并唤醒正在VIDIOC_DQBUF上等待的应用。static irqreturn_t my_irq_handler(int irq, void *data) { struct my_drv_ctx *ctx data; struct vb2_buffer *vb; u32 status readl(ctx-regs INT_STATUS); if (!(status FRAME_DONE)) return IRQ_NONE; vb ctx-active_vb; if (vb) { vb-planes[0].bytesused ctx-current_frame_size; vb2_buffer_done(vb, VB2_BUF_STATE_DONE); ctx-active_vb NULL; } /* 把之前准备好的一帧挂给硬件维持流水线 */ if (ctx-pending_vb) my_hw_start(ctx, ctx-pending_vb); writel(status, ctx-regs INT_CLEAR); return IRQ_HANDLED; }这里有一个非常容易犯的错误帧率不稳时中断可能来晚了或者丢中断前一个buffer还没DONE后一个又完成了。所以active_vb一定要在把buffer挂给硬件时记录好中断里按记录来收帧。如果中断没到要在超时机制里清掉硬件状态并把buffer标成ERROR否则应用永远等不到这帧dqbuf整个流就堵死了。start_streaming和stop_streaming回调里也要特别谨慎。start_streaming里拿到足够的buffer后要把所有入队buffer一次性挂给硬件之后才能开DMA。stop_streaming则相反必须先停硬件、清中断再回收所有还在ACTIVE的buffer用VB2_BUF_STATE_ERROR收尾。顺序一旦反了DMA还在写内存而驱动已经把buffer释放回给VB2那种内存损坏排查起来极其痛苦。4.4 DMABUF导出让数据零拷贝流转如果采集驱动不只是给自己用还要把帧共享给GPU、显示控制器或者另一个视频设备比如手机拍照流程里sensor → ISP → GPU的链路那驱动最好支持导出DMABUF。VB2的dma-contig memops自带导出能力驱动只要在vb2_queue配置里加上VB2_DMABUF标志再实现相应的ioctl处理。static const struct vb2_ops my_vb2_ops { .queue_setup my_queue_setup, .buf_prepare my_buf_prepare, .buf_finish my_buf_finish, .buf_cleanup my_buf_cleanup, .start_streaming my_start_streaming, .stop_streaming my_stop_streaming, .wait_prepare vb2_ops_wait_prepare, .wait_finish vb2_ops_wait_finish, };导出DMABUF时使用者会调用dma_buf_attach把自己设备挂上来然后map_attachment拿到一张sg_table这张sg_table里的地址已经是使用者设备视角的IOVA/DMA地址了。导出方要保证map/unmap期间buffer的DMA映射是稳定的不要在对方正在map时去释放底层内存。VB2会在buffer state为ACTIVE或DONE时拒绝释放操作这就是框架层帮你守住了安全底线。5. 常见问题与排查实录5.1 花屏和错位90%是cache一致性花屏、画面左半块是上一帧、右半块是这一帧这类症状我遇到最多的情况就是cache同步缺失。MMAP模式下如果没有正确做dma_sync硬件DMA写完内存CPU从cache里读到的还是老数据画面就会是新旧帧混合。排查思路是先确认buffer用的是不是dma_alloc_coherent。如果是那理论上不存在cache问题重点转向DMA地址是否写错、描述符size是否对。如果不是逐个检查在硬件事前有没有for_device、事后有没有for_cpu。在驱动里加几个临时打印把buffer的dma地址、size、sync方向打印出来再对比寄存器值用排除法很快能锁到问题点。我见过一个特别隐蔽的casedma_sync的函数调用都对但方向搞反了硬件写数据之前反倒把cache数据刷到内存把设备刚写回的内容覆盖掉了画面表现为一段全黑里夹着上一帧的残留。后来把方向改对问题立刻消失。记住规律设备写、CPU读用for_cpuCPU写、设备读用for_device别一门心思以为同步就是清cache。5.2 DMA地址为0或映射失败vb2_dma_contig_plane_dma_addr返回0或者硬件描述符里的DMA地址全是0这种问题通常指向分配阶段失败。常见原因有CONFIG_CMA没配导致连续内存不够、分配gfp_flags和硬件要求冲突、或者设备没有正确关联到内存分配器。如果平台有IOMMU/SMMU还要检查设备树里iommu-map是否正确。设备没有与SMMU关联dma_alloc_attrs可能不会走IOMMU通路拿到的要么是物理地址要么直接失败。驱动登记的dev字段必须是真实做DMA的总线设备不要随手填个杂散platform_device否则DMA API拿到的dma_ops和iommu配置都是错的。这类问题用dmesg一般能看到明确报错比如“DMA: out of SW-IOMMU space”或“allocation failed”。结合CONFIG_DMA_API_DEBUG打开dma-api debug还能检测到mismatched map/unmap、double unmap这些隐形问题。5.3 USERPTR模式性能突然恶化USERPTR模式最经典的问题是页面固定化和IOMMU映射被反复重建。如果应用传入的用户内存不是稳定的、生命周期短每次qbuf都要get_user_pages固定页面、建立映射dqbuf后可能又要解除映射开销全摊在热路径上帧率自然难看了。另外用户内存的排列方式也很关键。如果应用分配的是普通malloc内存页面颗粒度下物理页可能分散得厉害设备不支持SG模式时vb2底层还要做反弹缓冲区bounce buffer或者重新拷贝那USERPTR的意义就完全丧失了。这个模式还有一档看不见的坑用户内存被madvise主动释放或者页面被回收而驱动还拿着page引用在DMA轻则数据变脏重则内核panic。如果你必须用USERPTR确保应用侧内存足够“稳定”分配后不挪、不释放、不madvise最好页面对齐且为匿名页。碰到底层平台reset或swap事件USERPTR最容易被牵连。能换DMABUF就换DMABUF这是我从实战里得出的结论。5.4 零拷贝流水线中的时序抖帧DMABUF零拷贝流水线里最常被问的问题是“为什么GPU读到的帧时而新时而旧”。大多数情况是生产者和消费者之间的同步没有做好。DMA-BUF有自己的fence机制消费者要等生产者写完才能读但采集驱动在导出buffer后如果提前把buffer标记成DONE或者消费者的设备没有正确等待fence数据就可能被读早或读晚。处理办法是确保两端都遵守DMA-BUF的同步语义。采集驱动在完成DMA后要调用dma_buf_end_cpu_access等接口通知共享方“这块内容可以读了”消费者侧在map_attachment后、使用前要等待对应的fence而不是默认缓冲区内容随时有效。这类问题排查起来很难复现一旦出现建议在两端各加时间戳打印对比buffer完成时间和消费者读取时间排除掉fence后基本都会露出一个明显的先后错位。6. 调试工具与性能调优心得6.1 善用内核trace、debugfs和v4l2-compliance调试vb2相关问题时我最依赖的三个工具是内核tracepoint、debugfs和v4l2-compliance。内核事件里有一组videobuf2相关的tracepoint比如vb2_buf_queue、vb2_buffer_done可以在跑采集时跟踪每个buffer的qbuf/dqbuf时机和state变迁。打开的方式很简单直接往tracefs里写1就行echo 1 /sys/kernel/debug/tracing/events/videobuf2/vb2_buf_queue/enable echo 1 /sys/kernel/debug/tracing/events/videobuf2/vb2_buffer_done/enable cat /sys/kernel/debug/tracing/trace结合自己的驱动打点能非常清楚地看到一帧buffer在queue里的完整时间线。有时候帧率掉了一半trace里会直接显示dqbuf一直等DQ事件说明应用侧消费太慢或驱动只挂了一两个buffer在飞。v4l2-compliance是v4l2-utils里的测试套件所有声称支持VB2的驱动都应该先过一遍。它能帮你排查到很多驱动回调的边界问题比如num_buffers传0、plane size不一致、streamon前qbuf失败等。这是我的例行公事驱动写完第一件事就是跑v4l2-compliance没跑完前不碰业务逻辑。debugfs用于自定义信息最方便。我习惯在debugfs里导出一份寄存器dump、buffer状态表、硬件描述符链表的实时快照配合trace定位中断处理慢、描述符更新不及时这类问题。6.2 性能优化可以往哪儿使劲如果采集链路已经通了性能还差口气优先检查这几个点缓冲区数量min_buffers_needed太小时硬件DMA深度不足容易断流。加大缓冲池往往能直接提升稳定帧率。避免不必要的sync一致性内存和cache策略允许时尽量让DMA走不带sync的路径。每次sync都有开销特别是在高帧率场景下累加起来很可观。中断合并如果硬件支持状态寄存器批量查询考虑用NAPI或者合并中断方式降低CPU占用尤其4K分辨率下中断频率跟帧率一样高每帧都要进一次中断对CPU压力不小。DMA地址布局尽量让描述符地址和buffer地址对齐到硬件要求的边界避免DMA控制器做额外拆分。io_modes尽量少开不需要USERPTR就别声明VB2会为每种支持模式做额外的allocator初始化和检查路径声明少了路径短错误面也小。我个人实测下来很多“不稳定”不是硬件能力不够而是buffer个数不足和中断处理太慢叠加造成的调大min_buffers_needed、把中断处理里可以后置的活挪到tasklet/threaded irq症状往往立刻大幅缓解。写驱动这几年我对videobuf2最大的感触是它看起来是一堆回调的拼接实际上是一套很严谨的契约。每个阶段该做什么、不该做什么都划得清清楚楚你只要顺着契约走DMA内存管理就不会太折腾人。反过来任何想着“绕过框架”的操作短期看似痛快后续都是要加倍偿还的。最后再分享一个实用的习惯新写的vb2驱动第一步别急着调业务先跑一遍v4l2-compliance再跑一个循环的qbuf/dqbuf压力测试把框架层面的问题扫干净后面你会省下大量抓头发的夜晚。
阅读完成 · 觉得有帮助?