Linux V4L2子系统-videobuf2框架分析(三)_v4l2 memory dma

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前在阿里

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化的资料的朋友,可以点击这里获取!

用户指针模式。内存由应用程序分配,并将内存地址传递到内核V4L2驱动程序中,然后由V4L2驱动程序将数据填充到用户空间的内存中。

    [include/uapi/linux/videodev2.h]
    // videobuf2类型枚举
    enum v4l2_buf_type {
        V4L2_BUF_TYPE_VIDEO_CAPTURE        = 1,  // 图像采集
        V4L2_BUF_TYPE_VIDEO_OUTPUT         = 2,  // 图像输出
        V4L2_BUF_TYPE_VIDEO_OVERLAY        = 3,
        V4L2_BUF_TYPE_VBI_CAPTURE          = 4,
        V4L2_BUF_TYPE_VBI_OUTPUT           = 5,
        V4L2_BUF_TYPE_SLICED_VBI_CAPTURE   = 6,
        V4L2_BUF_TYPE_SLICED_VBI_OUTPUT    = 7,
    #if 1
        V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY = 8, /\* Experimental \*/
    #endif
        V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE = 9,
        V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE  = 10,
        V4L2_BUF_TYPE_SDR_CAPTURE          = 11,
    };
    // 缓冲区类型
    enum v4l2_memory {
        V4L2_MEMORY_MMAP             = 1,  // 内存映射
        V4L2_MEMORY_USERPTR          = 2,  // 用户空间指针
        V4L2_MEMORY_OVERLAY          = 3,  // 内存重叠
        V4L2_MEMORY_DMABUF           = 4,  // DMA内存
    };
    /\* Timestamp type \*/
    #define V4L2\_BUF\_FLAG\_TIMESTAMP\_MASK 0x0000e000
    #define V4L2\_BUF\_FLAG\_TIMESTAMP\_UNKNOWN 0x00000000
    // 递增类型时间戳,在内核的 monotonic 时钟时间轴上面生成的时间戳
    #define V4L2\_BUF\_FLAG\_TIMESTAMP\_MONOTONIC 0x00002000
    #define V4L2\_BUF\_FLAG\_TIMESTAMP\_COPY 0x00004000
    /\* Timestamp sources. \*/
    #define V4L2\_BUF\_FLAG\_TSTAMP\_SRC\_MASK 0x00070000
    #define V4L2\_BUF\_FLAG\_TSTAMP\_SRC\_EOF 0x00000000
    #define V4L2\_BUF\_FLAG\_TSTAMP\_SRC\_SOE 0x00010000

    [include/media/videobuf2-core.h]
    enum vb2_io_modes {
        VB2_MMAP	= (1 << 0),  // 驱动程序支持mmap的流式API
        VB2_USERPTR	= (1 << 1),  // 驱动程序支持用户指针模式的流式API
        VB2_READ	= (1 << 2),  // 驱动程序支持read()方式访问
        VB2_WRITE	= (1 << 3),  // 驱动程序支持write()方式访问
        VB2_DMABUF	= (1 << 4),  // 驱动程序支持DMABUF的流式API
    };
    struct vb2_queue {
        enum v4l2_buf_type  type;      // videobuf2类型,见enum v4l2\_buf\_type枚举
        unsigned int  io_modes;        // 支持的IO模式,见enum vb2\_io\_modes枚举
        unsigned  fileio_read_once:1;  // 读取第一个缓冲区后报告EOF 
        unsigned  fileio_write_immediately:1;  // write写入的数据都添加到缓冲队列中
        unsigned  allow_zero_bytesused:1;
        // 保护struct vb2\_queue的互斥锁,使缓冲队列的操作串行化,若驱动实有互斥锁,
        // 则可设置为NULL,videobuf2核心层API不使用此锁
        struct mutex  \*lock;         
        const struct vb2_ops  \*ops;          // 驱动实现的回调函数
        const struct vb2_mem_ops  \*mem_ops;  // 内存分配器需要的函数
        void				\*drv_priv;       // 驱动私有数据
        // 驱动的缓冲结构体大小,若为0表示驱动不定义自己缓冲结构,则使用sizeof(struct vb2\_buffer)
        unsigned int  buf_struct_size;       
        // 时间戳标志,见V4L2\_BUF\_FLAG\_TIMESTAMP\_XX和V4L2\_BUF\_FLAG\_TSTAMP\_SRC\_XX宏定义
        u32				timestamp_flags;
        // 分配缓冲区时的内存标志,通常为0,也有使用GFP\_DMA或\_\_GFP\_DMA32强制将内存分配到明确的内存区域
        gfp_t				gfp_flags;
        u32				min_buffers_needed;  // 在处理数据流之前,需要最小的缓冲区数目
        // 私有锁,保护缓冲区的分配、释放、映射
        struct mutex			mmap_lock;
        // 目前使用的缓冲区类型,取值为enum v4l2\_memory枚举
        enum v4l2_memory		memory;
        struct vb2_buffer  \*bufs[VIDEO_MAX_FRAME];  // 保存分配缓冲区的地址
        unsigned int			num_buffers;  // 分配的缓冲区数量
        struct list_head		queued_list;  // 用户空间入队的缓冲区链表
        unsigned int			queued_count; // 入队的就绪缓冲区数量
        atomic_t  owned_by_drv_count;         // 属于驱动的缓冲区数量
        struct list_head  done_list;  // 此链表中的缓冲区已填充数据,可以出队被用户空间使用 
        spinlock_t  done_lock;        // 保护done\_list链表的自旋锁
        wait_queue_head_t  done_wq;   // 等待缓冲区出队的等待队列
        void  \*alloc_ctx[VIDEO_MAX_PLANES]; 
        unsigned int  plane_sizes[VIDEO_MAX_PLANES];
        unsigned int  streaming:1;  // 当前流的状态
        unsigned int  start_streaming_called:1;  // start\_streaming()被成功调用
        unsigned int  error:1;  // struct vb2\_queue发生了致命错误
        unsigned int  waiting_for_buffers:1;  // 在poll函数中使用,以检查是否还在等待数据
        ......
    };

驱动需要实现struct vb2_ops中的函数,当然也可以实现一部分,也可以直接使用内核提供的函数。

queue_setup由ioctl命令VIDIOC_REQBUFSVIDIOC_CREATE_BUFS调用,经常使用的是VIDIOC_REQBUFS,在实际分配内存之前调用一次,若分配的buffer数量不足num_buffers,则会再次调用。该函数需要将分配的buffer数量保存到num_buffers,将buffer的planes的数量保存到num_planes中,每个plans的大小存放在sizes数组中。planes和视频的像素格式有关,如YUV420SP格式的planes为2。alloc_ctxs数组保存每一个plane的特定数据。

wait_preparewait_finish函数内核提供了默认的实现,可以直接使用,分别对应函数vb2_ops_wait_preparevb2_ops_wait_finish。这两个函数实现很简单,vb2_ops_wait_prepare释放互斥锁vb2_ops_wait_finish获取互斥锁。用户调用ioctl并使用VIDIOC_QBUF命令时,内核会判断是否是阻塞调用,如果是阻塞调用并且没有准备好数据,内核此时会调用wait_prepare释放锁并进行休眠等待,直到数据准备好被唤醒,然后再调用wait_finish重新持有锁。

buf_init在分配缓冲区之后调用或获取了新的USERPTR之后调用(in MMAP case),驱动需要完成一些缓冲区初始化的工作,若初始化失败,则返回不为0的数,此时queue_setup将失败,一般用不到。

buf_prepare缓冲区每次从用户空间入队都需要调用或被ioctl的VIDIOC_PREPARE_BUF命令调用,驱动需要执行一些初始化工作或获取、修改缓冲区,若驱动支持VIDIOC_CREATE_BUFS,则需要验证缓冲区的大小,若有错误发生,则缓冲区不会入队。

start_streaming调用后流进入开启状态,在调用之前驱动必须先调用buf_queue接收缓冲区。如果发生硬件错误,驱动可以通过该回调返回一个错误,此时所有的buffer都会被归还给videobuf2(调用vb2_buffer_done(*vb, VB2_BUF_STATE_QUEUED))。如果需要设置start_streaming时buffer的最小数量,那么应该在调用该函数之前设置最少的buffer数量值vb2_queue->min_buffers_needed,只有buffer数量大于vb2_queue->min_buffers_neededstart_streaming才能被成功调用。

stop_streaming调用后流进入关闭状态,驱动需要停止DMA传输或等待工作完成和归还全部buffers(还给videobuf2)。调用vb2_buffer_done来归还所有驱动持有的buffers,可使用VB2_BUF_STATE_DONE(操作完成)和VB2_BUF_STATE_ERR(操作出错)参数。若要等待完成,可使用vb2_wait_for_all_buffers

    // 驱动需要明确定义的函数,用于操作vb2\_queue
    [include/media/videobuf2-core.h]
    struct vb2_ops {
        // 设置缓冲区队列相关参数
        int (\*queue_setup)(struct vb2_queue \*q, const struct v4l2_format \*fmt,
                unsigned int \*num_buffers, unsigned int \*num_planes,
                unsigned int sizes[], void \*alloc_ctxs[]);
        // 在调用ioctl等待新的缓冲区时释放所有锁,避免阻塞时产生死锁
        void (\*wait_prepare)(struct vb2_queue \*q);
        // 重新获取在前一个回调函数中释放的锁
        void (\*wait_finish)(struct vb2_queue \*q);
        // buffer初始化
        int (\*buf_init)(struct vb2_buffer \*vb);
        // buffer准备好
        int (\*buf_prepare)(struct vb2_buffer \*vb);
        // 缓冲区每次出队到用户空间都需要调用,驱动可以访问或修改缓冲区。
        void (\*buf_finish)(struct vb2_buffer \*vb);
        // 调用后缓冲区被释放,驱动可以做一些清理工作
        void (\*buf_cleanup)(struct vb2_buffer \*vb);
        // 调用后流进入开启状态,在调用之前驱动必须先调用buf\_queue接收缓冲区,
        int (\*start_streaming)(struct vb2_queue \*q, unsigned int count);
        // 调用后流进入关闭状态,驱动需要停止DMA传输或等待工作完成和缓冲区全部入队。
        void (\*stop_streaming)(struct vb2_queue \*q);
        // 缓冲区加入驱动的队列中
        void (\*buf_queue)(struct vb2_buffer \*vb);
    };

struct vb2_buffer是保存视频数据和信息的核心结构体,每一帧的图像都对应一个struct vb2_buffer结构体,图像信息保存在struct v4l2_buffer结构中,如时间戳、编号、序列号等信息。应用使用ioctl的VIDIOC_REQBUFS命令请求缓冲区时,分配struct vb2_bufferstruct vb2_buffer的状态由enum vb2_buffer_state枚举定义描述。若是V4L2_MEMORY_MMAP类型,则会额外分配内存,图像数据则保存在额外分配的内存中,额外分配的内存指针保存在planes[VIDEO_MAX_PLANES]数组中。

    [include/media/videobuf2-core.h]
    enum vb2_buffer_state {  // 缓冲区状态枚举
        VB2_BUF_STATE_DEQUEUED,  // 缓冲区出队,处于用户空间的控制下,默认状态
        VB2_BUF_STATE_PREPARING, // videobuf2正在准备缓冲区
        VB2_BUF_STATE_PREPARED,  // 缓冲区已准备好
        VB2_BUF_STATE_QUEUED,    // 缓冲区入队,处于videobuf2中,不处于驱动中
        VB2_BUF_STATE_ACTIVE,    // 缓冲区位于驱动中
        VB2_BUF_STATE_DONE,      // 缓冲区从驱动返回到videobuf2,但还没出队到用户空间
        VB2_BUF_STATE_ERROR,     // 出错,dequeued到用户空间会报错
    };
    struct vb2_buffer {  // video缓冲区描述符
        struct v4l2_buffer	v4l2_buf;  // 关联的缓冲区,可以被驱动read
        // 可以由driver读写,(byteused)即使是对于single-planar类型,v4l2\_planes[0]应该被
        // 使用而不是仅仅使用v4l2\_buf的byteused。
     // 驱动使用vb2\_set\_plane\_payload()设置byteused.
        struct v4l2_plane	v4l2_planes[VIDEO_MAX_PLANES];
        struct vb2_queue	\*vb2_queue;  // 该vb2\_buffer所属的vb2\_queue
        unsigned int		num_planes;  // 该buffer有多少个planes
        enum vb2_buffer_state	state;   // buffer的当前状态
        // queued buffer链表,保存所有从userspace queued进的buffers
        struct list_head	queued_entry;
        // 保存所有准备dequeued到userspace的buffers链表 
        struct list_head	done_entry;
        // 私有的per-plane信息,驱动禁止修改
        struct vb2_plane	planes[VIDEO_MAX_PLANES];
        ......
    };
    // 分配struct vb2\_buffer时,若是V4L2\_MEMORY\_MMAP类型,则会额外分配内存,
    // 图像数据则保存在额外分配的内存中,额外分配的内存指针保存在该结构体当中
    struct vb2_plane {
        void  \*mem_priv;        //存放一帧图片数据(针对MMAP类型模式)
        struct  dma_buf \*dbuf;  //(针对DMA类型模式)
        unsigned int  dbuf_mapped;
    };

    // struct v4l2\_buffer用来指定与描述一帧帧缓冲,应用可以设置
    struct v4l2_buffer {
        __u32			index;  // buffer的编号
        __u32			type;   // buffer的类型,由enum v4l2\_buf\_type定义
        // 数据在缓冲区(有效负载)中所占的字节数,对于多平面缓冲区未使用(设置为0)
        __u32			bytesused;
        // 标志位,见V4L2\_BUF\_FLAG\_XX宏定义,常见值有V4L2\_BUF\_FLAG\_MAPPED、 
        // V4L2\_BUF\_FLAG\_QUEUED、V4L2\_BUF\_FLAG\_DONE,分别代表当前缓存已经映射、
        // 缓存可以采集数据、缓存可以提取数据
        __u32			flags;  
        __u32			field;
        struct timeval		timestamp;  // 视频帧时间戳
        struct v4l2_timecode timecode;  // 时间码
        __u32			sequence;  // 该帧的序列号
        __u32			memory;  // enum v4l2\_memory枚举定义
        union {
            // V4L2\_MEMORY\_MMAP,从将要mapping的device memory头到数据头的offset
            __u32           offset;
            // V4L2\_MEMORY\_USERPTR,用户空间指针指向此buffer
            unsigned long   userptr;
            // for multiplanar buffers; userspace pointer to the array of plane
            // info structs for this buffer
            struct v4l2_plane \*planes;
            // V4L2\_MEMORY\_DMABUF,用户空间的描述符关联此描述符
            __s32		fd;
        } m;
        // 对于single-plane,表示buffer的字节数
        // 对于multi-plane buffers,则表示planes array中的元素数量
        __u32			length;
        __u32			reserved2;
        __u32			reserved;
    };

struct vb2_mem_ops是buffer内存分配和处理的操作函数集合,这些函数和buffer的类型有关系,即enum v4l2_memory枚举定义的类型。具体如下。
(1)get_userptrput_userptr函数用于处理USERPTR类型的buffer。
(2)allocputnum_usersmmap函数用于处理MMAP类型的buffer。
(3)allocputnum_usersvaddr函数用于处理read/write访问类型的buffer。
(4)attach_dmabufdetach_dmabufmap_dmabufunmap_dmabuf函数用于处理DMABUF类型的buffer。
struct vb2_mem_ops结构体通常被初始化为vb2_dma_contig_memops结构体,该结构体是内核提供的,可以直接使用。

    [include/linux/dma-direction.h]
    enum dma_data_direction {   // DMA数据传输方向
        DMA_BIDIRECTIONAL = 0,  // 双向
        DMA_TO_DEVICE = 1,      // 传输到设备
        DMA_FROM_DEVICE = 2,    // 从设备往外传输
        DMA_NONE = 3,
    };    
    [include/media/videobuf2-core.h]
    // 缓冲区内存处理和分配操作函数集合 
    struct vb2_mem_ops {
        // 分配video内存和私有数据(可选)分配器
        void \*(\*alloc)(void \*alloc_ctx, unsigned long size,
                enum dma_data_direction dma_dir, gfp_t gfp_flags);
        // 告诉分配器此缓冲区不再使用,若没有其他使用者使用,则分配器会释放此块内存
        void (\*put)(void \*buf_priv);
        struct dma_buf \*(\*get_dmabuf)(void \*buf_priv, unsigned long flags);
        // 获取用户空间指针指向的内存,在V4L2\_MEMORY\_USERPTR模式中使用
        void \*(\*get_userptr)(void \*alloc_ctx, unsigned long vaddr,
                unsigned long size, enum dma_data_direction dma_dir);
        // 告诉分配器USERPTR缓冲区不再使用
        void (\*put_userptr)(void \*buf_priv);
        // 缓冲区每次从用户空间添加到队列中就会被调用,对缓存同步很有用
        void (\*prepare)(void \*buf_priv);
        // 缓冲区每次从内核队列添加到用户空间就会被调用
        void (\*finish)(void \*buf_priv);
        // 为硬件操作添加共享的struct dma\_buf,在V4L2\_MEMORY\_DMABUF模式中使用
        // alloc\_ctx-分配上下文,dbuf-共享的dma\_buf
        void \*(\*attach_dmabuf)(void \*alloc_ctx, struct dma_buf \*dbuf,
                unsigned long size, enum dma_data_direction dma_dir);
        // 通知缓冲区的exporter目前的DMABUF不再使用
        void (\*detach_dmabuf)(void \*buf_priv);
        // 从分配器请求访问DMABUF,此DMABUF的分配器将通知驱动该DMABUF将要被使用
        int (\*map_dmabuf)(void \*buf_priv);
        // 释放访问DMABUF的控制权,此DMABUF的分配器将通知驱动该DMABUF已经使用完毕
        void (\*unmap_dmabuf)(void \*buf_priv);
        // 返回给定缓冲区的内核虚拟地址,该缓冲区与私有数据结构向关联
        void \*(\*vaddr)(void \*buf_priv);
        // 返回给定缓冲区的分配器定义的cookie
        void \*(\*cookie)(void \*buf_priv);    
        // 返回此缓冲区的当前使用者,若只有videobuf2层使用,则返回1
        unsigned int (\*num_users)(void \*buf_priv);
        // 建立用户空间到给定缓冲区虚拟地址区域的映射
        int (\*mmap)(void \*buf_priv, struct vm_area_struct \*vma);
    };

4.使用方法分析

videobuf2的使用方法复杂,需要结合具体的驱动实例进行说明,这样比较好理解。下图是imx6ull平台上,CSI控制器的videobuf2使用方法总结。应用可以通过调用opencloseioctlmmapread系统调用访问Video设备,内核根据不同的系统调用采用相对应的方法访问videobuf2。下面从这些系统调用入手,分析内核中videobuf2的使用方法。

videobuf2使用方法示意图

4.1.open

应用调用open打开Video设备,获取设备的描述符。内核中首先调用v4l2_open,然后调用驱动提供的mx6s_csi_open函数。缓冲区队列vb2_queue就是在mx6s_csi_open函数中完成初始化。缓冲区队列数据结构vb2_queue一般嵌入到其他结构体中,由驱动进行动态分配。首先必须设置缓冲区类型type、I/O模型io_modes、缓冲区操作函数集合ops、缓冲区内存管理函数集合mem_ops、时间戳类型timestamp_flags(通常设置为V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC),其他成员可根据实际情况设置,最后调用vb2_queue_init完成缓冲区队列vb2_queue的初始化。

    [include/media/videobuf2-core.h]
    q-缓冲区队列数据结构struct vb2_queue指针
    返回值-0成功,小于0失败
    int vb2\_queue\_init(struct vb2_queue \*q)
    {
        // 必须设置下面的成员,否则返回-EINVAL的错误
        if (WARN\_ON(!q) || WARN\_ON(!q->ops) || WARN\_ON(!q->mem_ops) ||
            WARN\_ON(!q->type) || WARN\_ON(!q->io_modes) ||
            WARN\_ON(!q->ops->queue_setup) || WARN\_ON(!q->ops->buf_queue) ||
            WARN\_ON(q->timestamp_flags & ~(V4L2_BUF_FLAG_TIMESTAMP_MASK |
                V4L2_BUF_FLAG_TSTAMP_SRC_MASK)))
            return -EINVAL;

        // 驱动必须选择合适的时间戳,否则内核会发出警告
        WARN\_ON((q->timestamp_flags & V4L2_BUF_FLAG_TIMESTAMP_MASK) ==
            V4L2_BUF_FLAG_TIMESTAMP_UNKNOWN);

        INIT\_LIST\_HEAD(&q->queued_list);  // 初始化queued\_list链表节点
        INIT\_LIST\_HEAD(&q->done_list);    // 初始化done\_list链表节点
        spin\_lock\_init(&q->done_lock);    // 初始化自旋锁
        mutex\_init(&q->mmap_lock);        // 初始化互斥锁
        init\_waitqueue\_head(&q->done_wq); // 初始化等待队列头
        // 若缓冲区大小驱动没有设置,则内核使用默认值初始化
        if (q->buf_struct_size == 0)
            q->buf_struct_size = sizeof(struct vb2_buffer);
        return 0;
    }

4.2.ioctl

V4L2子系统定义了很多ioctl命令供应用程序使用。VIDIOC_REQBUFS命令用于向内核申请缓冲区,VIDIOC_QUERYBUF命令用于获取缓冲区信息,VIDIOC_QBUF命令将读取完数据的空缓存返还给驱动的缓存队列,VIDIOC_DQBUF命令将填充满数据的缓存从驱动中返回给应用,VIDIOC_STREAMOFF命令用于关闭流,即停止图像采集,VIDIOC_STREAMON命令用于开启流,即开启图像采集。内核中的调用流程为v4l2_ioctl->video_ioctl2->__video_do_ioctl->根据不同的命令调用不同的驱动函数->调用对应的videobuf2处理函数,具体调用流程参考上图。下面具体分析一下ioctl调用的videobuf2处理函数。

4.2.1.VIDIOC_REQBUFS

使用VIDIOC_REQBUFS命令调用ioctl,最终会调用到vb2_reqbufs函数,内核使用vb2_reqbufs函数创建缓冲区。

    [include/media/videobuf2-core.h]
    // q-缓冲区队列数据结构struct vb2\_queue指针
    // req-申请缓冲区所需信息存放的结构体,应用需要设置里面的成员
    // 返回值-0成功,小于0失败
    int vb2\_reqbufs(struct vb2_queue \*q, struct v4l2_requestbuffers \*req);
    [include/uapi/linux/videodev2.h]
    struct v4l2_requestbuffers {
        __u32  count;       // 申请缓冲区的数量,一帧图像对应一个缓冲区
        __u32  type;		// 缓冲区类型,摄像头为V4L2\_BUF\_TYPE\_VIDEO\_CAPTURE
        __u32  memory;		// 通常为V4L2\_MEMORY\_MMAP或V4L2\_MEMORY\_USERPTR
        __u32  reserved[2]; // 保留
    };

vb2_reqbufs调用流程可总结如下:
(1)验证缓冲区的memory type和buffer type是否正确。
__verify_memory_type函数用于校验缓冲区类型和缓冲区内存类型。vb2_queue中的缓冲区类型typev4l2_requestbuffers中的缓冲区类型type需一致。缓冲区内存类型必须是V4L2_MEMORY_MMAPV4L2_MEMORY_USERPTRV4L2_MEMORY_DMABUF其中之一。
若是V4L2_MEMORY_MMAP类型,则q->io_modes必须设置为VB2_MMAPmem_ops->allocq->mem_ops->putq->mem_ops->mmap的函数指针也必须设置。
若是V4L2_MEMORY_USERPTR类型,则q->io_modes必须设置为VB2_USERPTRmem_ops->get_userptrq->mem_ops->put_userptr的函数指针也必须设置。
若是V4L2_MEMORY_DMABUF类型,则q->io_modes必须设置为VB2_DMABUFmem_ops->attach_dmabufq->mem_ops->detach_dmabufq->mem_ops->map_dmabufq->mem_ops->unmap_dmabuf的函数指针也必须设置。
(2)判断缓冲区参数是否正确,若不正确,则需要做一些处理。
申请的缓冲区数量为0或缓冲区队列中缓冲区数量不为0或申请的缓冲区内存类型和缓冲区队列中缓冲区内存类型不一致,则进入额外的处理逻辑。若内存类型为V4L2_MEMORY_MMAP且缓冲区正在使用,则直接返回错误。清理处于PREPARED或QUEUED状态的缓冲区并释放缓冲区内存。
(3)计算需要分配的缓冲区数量。
(4)调用驱动实现的函数queue_setup,驱动函数需要设置num_buffers、num_buffers、q->plane_sizes和q->alloc_ctx。
(5)调用__vb2_queue_alloc分配缓冲区内存。此时缓冲区的状态为VB2_BUF_STATE_DEQUEUED。后面详细说明该函数。
(6)若分配的缓冲区数量小于需要分配的数量,需要再次调用驱动提供的queue_setup函数。
(7)设置分配的缓冲区数量并向应用返回分配的缓冲区数量。

    vb2_reqbufs
      // 验证缓冲区的memory type和buffer type是否正确
        ->__verify_memory_type
        ->__reqbufs
            // 申请的缓冲区数量为0或缓冲区队列中缓冲区数量不为0或
            // 申请的缓冲区内存类型和缓冲区队列中缓冲区内存类型不一致,则需要则额外的处理
            if (req->count == 0 || q->num_buffers != 0 || q->memory != req->memory) {
                mutex\_lock(&q->mmap_lock);
                // 内存类型为V4L2\_MEMORY\_MMAP且缓冲区正在使用,则直接返回错误
                if (q->memory == V4L2_MEMORY_MMAP && \_\_buffers\_in\_use(q)) {
                    mutex\_unlock(&q->mmap_lock);
                    return -EBUSY;
                }
                // 若缓冲区处于PREPARED或QUEUED状态,则需要清理,
                // 一般在申请了缓冲区,但没调用STREAMON,会出现这种情况
                \_\_vb2\_queue\_cancel(q);
                // 释放已经分配内存的缓冲区
                ret = \_\_vb2\_queue\_free(q, q->num_buffers);
                mutex\_unlock(&q->mmap_lock);
            }
            // 缓冲区数量取应用申请的数量和最大数量中的较小值,VIDEO\_MAX\_FRAME定义为32
            num_buffers = min\_t(unsigned int, req->count, VIDEO_MAX_FRAME);
            // 缓冲区数量取num\_buffers和需要最少的缓冲区数量的较大值
            num_buffers = max\_t(unsigned int, num_buffers, q->min_buffers_needed);
            // 设置缓冲区队列中缓冲区的内存模型,内存模型应该和应用申请的模型保持一致
            q->memory = req->memory;
            // 回调用驱动提供的queue\_setup函数,imx6ull提供的函数为mx6s\_videobuf\_setup
            // 驱动函数需要设置num\_buffers、num\_buffers、q->plane\_sizes和q->alloc\_ctx
            call\_qop(...queue_setup...)
            // 分配缓冲区内存
            ->__vb2_queue_alloc
            // 若分配的缓冲区数量小于需要分配的数量,需要再次调用驱动提供的queue\_setup函数
            if (!ret && allocated_buffers < num_buffers) {
                num_buffers = allocated_buffers;
                // 若驱动能处理这种情况,则不会返回错误,若无法处理,则会返回错误
                // mx6s\_videobuf\_setup不具备这种功能
                ret = call\_qop(q, queue_setup, q, NULL, &num_buffers,
                        &num_planes, q->plane_sizes, q->alloc_ctx);
                if (!ret && allocated_buffers < num_buffers)
                    ret = -ENOMEM;
            }
            ->mutex\_lock(&q->mmap_lock);  // 缓冲区共享成员访问需要同步
            // 设置分配的缓冲区数量
            q->num_buffers = allocated_buffers;
            ->mutex\_unlock(&q->mmap_lock);
            req->count = allocated_buffers  // 向应用返回分配的缓冲区数量

queue_setup函数需要驱动提供。imx6ull提供的函数为mx6s_videobuf_setup,主要的作用是设置__reqbufs函数中的缓冲区数量num_buffers、plane的数量num_planes、plane的大小q->plane_sizesq->alloc_ctx。上述设置的变量都是调用函数以指针的形式传入。

    static int mx6s\_videobuf\_setup(struct vb2_queue \*vq,
                const struct v4l2_format \*fmt,
                unsigned int \*count, unsigned int \*num_planes,
                unsigned int sizes[], void \*alloc_ctxs[])
    {
        struct mx6s_csi_dev \*csi_dev = vb2\_get\_drv\_priv(vq);
        alloc_ctxs[0] = csi_dev->alloc_ctx;  // 设置alloc\_ctxs
        sizes[0] = csi_dev->pix.sizeimage;   // 设置plane\_sizes为图像的大小
        if (0 == \*count)  // 如果传入的缓冲区数量为0,则设置为32
            \*count = 32;
        // 如果num\_planes为0且缓冲区占用的总内存超过了规定的最大值,则要重新计算缓冲区数量
        // 最大值为64MB,MAX\_VIDEO\_MEM定义为64
        if (!\*num_planes && sizes[0] \* \*count > MAX_VIDEO_MEM \* 1024 \* 1024)
            // 则缓冲区数量按最大内存计算
            \*count = (MAX_VIDEO_MEM \* 1024 \* 1024) / sizes[0];
        \*num_planes = 1;  // 设置plane的数量为1
        return 0;
    }

缓冲区分配主要由__vb2_queue_alloc函数实现。分配num_buffers个缓冲区,即分配num_buffersstruct vb2_buffer结构退。所有的缓冲区内存地址都保存到vb2_queue结构体中的bufs数组中。若是缓冲区内存是V4L2_MEMORY_MMAP类型,则还需要额外分配保存图像的缓冲区,一个缓冲区分配num_planes个保存图像的缓冲区。此缓冲区由vb2_dc_alloc分配。分配完后缓冲器的结构示意如下图所示。V4L2_MEMORY_MMAP类型的缓冲区需要分配额外的内存空间用于存储图像数据,如图中绿框所属,首选分配一个管理的结构体struct vb2_dc_buf,再分配真正存储图像数据的缓冲区,存储图像的缓冲区物理地址和虚拟地址一致,其虚拟地址保存到管理结构体的vaddr成员中,虚拟地址保存到管理结构体的dma_addr成员中,缓冲区大小保存到管理结构体的size成员中。

缓冲区结构示意图

    static int \_\_vb2\_queue\_alloc(struct vb2_queue \*q, enum v4l2_memory memory,
                    unsigned int num_buffers, unsigned int num_planes)
    {
        unsigned int buffer;
        struct vb2_buffer \*vb;
        int ret;
        // 总共分配num\_buffers个缓冲区
        for (buffer = 0; buffer < num_buffers; ++buffer) {
            // 分配缓冲区内存,缓冲区大小为buf\_struct\_size,一般由驱动设置,
            // imx6ull平台设置为sizeof(struct mx6s\_buffer)
            vb = kzalloc(q->buf_struct_size, GFP_KERNEL);

            // 如果是multiplanar buffers,则缓冲区长度length保存的是plane的数量
            if (V4L2\_TYPE\_IS\_MULTIPLANAR(q->type))
                vb->v4l2_buf.length = num_planes;

            vb->state = VB2_BUF_STATE_DEQUEUED;  // 设置缓冲区状态
            vb->vb2_queue = q;  // 设置管理缓冲区的缓冲区队列
            vb->num_planes = num_planes;  // 设置plane数量
            vb->v4l2_buf.index = q->num_buffers + buffer;  // 设置缓冲区编号
            vb->v4l2_buf.type = q->type;   // 设置缓冲区类型
            vb->v4l2_buf.memory = memory;  // 设置缓冲区内存类型

            // 对于V4L2\_MEMORY\_MMAP类型,则还需要分配额外的内存用于保存图像数据
            // 然后映射到用户空间,用户可以直接读取额外内存中的数据
            if (memory == V4L2_MEMORY_MMAP) {
                
                // \*\*\*\*\*\*分配存储图像数据内存的函数\*\*\*\*\*\*\*
                ->\_\_vb2\_buf\_mem\_alloc(vb);
                    // 每一个缓冲区,分配num\_planes块额外的内存
                    for (plane = 0; plane < vb->num_planes; ++plane) {
                        // 分配的内存大小按页对齐
                        unsigned long size = PAGE\_ALIGN(q->plane_sizes[plane]);
                        // 调用驱动提供的alloc函数进行内存分配,
                        // imx6ull平台调用vb2\_dc\_alloc函数
                        mem_priv = call\_ptr\_memop(vb, alloc, q->alloc_ctx[plane],
                                    size, dma_dir, q->gfp_flags);
                        // 将额外分配的内存保存到mem\_priv成员中
                        vb->planes[plane].mem_priv = mem_priv;
                        // 保存长度
                        vb->v4l2_planes[plane].length = q->plane_sizes[plane];
                    }

                // 调用驱动提供的buf\_init函数进行初始化,imx6ull没有提供
                ->call\_vb\_qop(vb, buf_init, vb);
            }
            // 保存缓冲区地址
            q->bufs[q->num_buffers + buffer] = vb;
        }
        // 设置所有缓冲区的每个plane的长度
        ->\_\_setup\_lengths(q, buffer);
            vb->v4l2_planes[plane].length = q->plane_sizes[plane]
        // MMAP类型还要设置偏移,每个buffer的每个plane偏移都不一样
        if (memory == V4L2_MEMORY_MMAP)
            ->\_\_setup\_offsets(q, buffer);
        return buffer;
    }

vb2_dc_alloc由驱动提供,其分配的缓冲区虚拟地址和物理地址都连续,属于第三种类型的videobuf2。

    [drivers\media\v4l2-core\videobuf2-dma-contig.c]
    struct vb2_dc_buf {
        struct device  \*dev;
        void  \*vaddr;          // 内存虚拟地址
        unsigned long  size;   // 内存大小
        dma_addr_t  dma_addr;  // 内存物理地址
        enum dma_data_direction  dma_dir;  // DMA传输方向
        struct sg_table  \*dma_sgt;  // SG DMA相关
        /\* MMAP相关变量 \*/
        struct vb2_vmarea_handler  handler;
        atomic_t  refcount;
        struct sg_table  \*sgt_base;
        struct vm_area_struct  \*vma; // USERPTR相关变量
        struct dma_buf_attachment  \*db_attach;  // DMABUF相关变量
    };
    static void \*vb2\_dc\_alloc(void \*alloc_ctx, unsigned long size,
                enum dma_data_direction dma_dir, gfp_t gfp_flags)
    {
        struct vb2_dc_conf \*conf = alloc_ctx;
        struct device \*dev = conf->dev;
        struct vb2_dc_buf \*buf;
        // 首先分配一个struct vb2\_dc\_buf结构体
        buf = kzalloc(sizeof \*buf, GFP_KERNEL);
        // 然后分配存储图像数据的缓冲区,此缓冲区的物理地址和虚拟地址都连续
        buf->vaddr = dma\_alloc\_coherent(dev, size, &buf->dma_addr, GFP_KERNEL | gfp_flags);
        buf->dev = get\_device(dev);  // 设置父设备指针
        buf->size = size;  // 保存图像数据缓冲区的大小
        buf->dma_dir = dma_dir;  // 记录DMA传输方向
        // 设置struct vb2\_vmarea\_handler结构体
        buf->handler.refcount = &buf->refcount;
        buf->handler.put = vb2_dc_put;  // 回调函数
        buf->handler.arg = buf;  // 回调函数参数
        atomic\_inc(&buf->refcount);  // 增加引用计数,引用计数为0时释放缓冲区
        return buf;  // 返回分配的vb2\_dc\_buf地址
    }


4.2.2.VIDIOC_QUERYBUF

使用VIDIOC_QUERYBUF命令调用ioctl,最终会调用到vb2_querybuf函数,内核使用vb2_querybuf函数将缓冲区信息拷贝到用户空间,主要有时间戳timestamp、标志flags、缓冲区长度length、缓冲区偏移offset等信息。

    [include/media/videobuf2-core.h]
    // q-缓冲区队列数据结构struct vb2\_queue指针
    // b-v4l2\_buffer结构体指针,内核将缓冲区信息存放到里面
    // 返回值-0成功,小于0失败
    int vb2\_querybuf(struct vb2_queue \*q, struct v4l2_buffer \*b);

4.2.3.VIDIOC_QBUF

使用VIDIOC_QBUF命令调用ioctl,最终会调用到vb2_qbuf函数,内核使用vb2_qbuf函数将读取完数据的空缓存返还给驱动的缓存队列。

    [include/media/videobuf2-core.h]
    // q-缓冲区队列数据结构struct vb2\_queue指针
    // b-v4l2\_buffer结构体指针
    // 返回值-0成功,小于0失败
    int vb2\_qbuf(struct vb2_queue \*q, struct v4l2_buffer \*b);

vb2_qbuf的主要工作如下:
(1)通过bufs数组获取对应编号的缓冲区地址。
(2)根据缓冲区的不同状态做不同的处理。
VB2_BUF_STATE_DEQUEUED状态的缓冲区需要调用__buf_prepare函数执行一些准备工作。首先将缓冲区状态设置为VB2_BUF_STATE_PREPARINGV4L2_MEMORY_MMAP类型调用buf_prepare,imx6ull平台调用mx6s_videobuf_prepareV4L2_MEMORY_USERPTR调用get_userptr,imx6ull平台调用vb2_dc_get_userptrVB2_BUF_STATE_PREPARED状态的缓冲区无需准备工作。其他状态的缓冲区直接返回错误,不能进行VIDIOC_QBUF操作。
(3)将缓冲区挂到queued_list链表中。
(4)设置缓冲区状态为VB2_BUF_STATE_QUEUED
(5)若VIDIOC_STREAMON已经被调用,说明流已经打开,则需要调用__enqueue_in_driver函数将缓冲区添加到驱动的队列中。

    vb2_qbuf
        ->vb2_internal_qbuf
            // 获取对应编号的缓冲区地址
            vb = q->bufs[b->index];
            // 检查缓冲区状态,只有VB2\_BUF\_STATE\_DEQUEUED和
            // VB2\_BUF\_STATE\_PREPARED状态才能被加入到驱动的缓存队列
            switch (vb->state) {
            case VB2_BUF_STATE_DEQUEUED:

                -> \_\_buf\_prepare(vb, b);
                    ->__verify_length;  // 验证数据长度
                    vb->state = VB2_BUF_STATE_PREPARING;  // 设置缓冲区状态为PREPARING
                    vb->v4l2_buf.timestamp.tv_sec = 0;  // 清空时间戳
                    vb->v4l2_buf.timestamp.tv_usec = 0;
                    vb->v4l2_buf.sequence = 0;  // 设置序列号为0

                    ->\_\_qbuf\_mmap(vb, b);    // V4L2\_MEMORY\_MMAP类型调用
                        ->__fill_vb2_buffer  // 填充信息并校验
                        // 调用buf\_prepare函数,imx6ull平台调用mx6s\_videobuf\_prepare函数
                        // 主要的作用设置payload和校验缓冲区虚拟地址是否正确
                        call\_vb\_qop(vb, buf_prepare, vb) 
                    ->\_\_qbuf\_userptr(vb, b);  // V4L2\_MEMORY\_USERPTR类型调用
                        ->__fill_vb2_buffer   // 填充信息并校验
                        // 调用驱动提供的get\_userptr回调函数,
                        // imx6ull平台调用vb2\_dc\_get\_userptr回调函数
                        call\_ptr\_memop(...get_userptr...);
                        // 调用buf\_prepare函数,imx6ull平台调用mx6s\_videobuf\_prepare函数
                        call\_vb\_qop(vb, buf_prepare, vb);
                    // dmabuf目前没有接触到,不讨论
                    ->\_\_qbuf\_dmabuf(vb, b);  // V4L2\_MEMORY\_DMABUF类型调用
                break;

            case VB2_BUF_STATE_PREPARED:
                break;
            // videobuf2正在准备缓冲区,不能被加入到驱动的缓存队列
            // \_\_buf\_prepare函数内部会设置VB2\_BUF\_STATE\_PREPARING状态
            case VB2_BUF_STATE_PREPARING:
                return -EINVAL;
            default:
                return -EINVAL;
            }
            // 将应用传入的缓冲区挂到queued\_list链表中
            list\_add\_tail(&vb->queued_entry, &q->queued_list)
            q->queued_count++;  // queued\_list链表中的缓冲区数量加1
            vb->state = VB2_BUF_STATE_QUEUED;

            // 如果VIDIOC\_STREAMON已经被调用,则需要将缓冲区添加到驱动的缓冲区队列中
            if (q->start_streaming_called)  
                // 将缓冲区挂到imx6ull CSI设备驱动结构体的capture链表上
                ->\_\_enqueue\_in\_driver(vb);

            \_\_fill\_v4l2\_buffer(vb, b);  // 向用户空间填充信息

imx6ull平台buf_prepare的回调函数为mx6s_videobuf_prepare,主要的作用是设置payload,检查缓冲区虚拟地址是否存在和payload是否正确设置。

    static int mx6s\_videobuf\_prepare(struct vb2_buffer \*vb)
    {
        struct mx6s_csi_dev \*csi_dev = vb2\_get\_drv\_priv(vb->vb2_queue);
        int ret = 0;
        // 设置payload,payload为图像大小
        vb2\_set\_plane\_payload(vb, 0, csi_dev->pix.sizeimage);
            // 缓冲区的有效字节数为图像大小
            vb->v4l2_planes[plane_no].bytesused = size
        // 检查缓冲区虚拟地址是否存在和payload是否正确设置
        if (vb2\_plane\_vaddr(vb, 0) &&
            vb2\_get\_plane\_payload(vb, 0) > vb2\_plane\_size(vb, 0)) {
            ret = -EINVAL;
            goto out;
        }
        return 0;
    out:
        return ret;
    }

    // 获取plane的虚拟地址
    vb2\_plane\_vaddr(vb, 0);
        // 调用驱动提供的vaddr函数,imx6ull平台调用vb2\_dc\_vaddr
        call\_ptr\_memop(vb, vaddr, vb->planes[plane_no].mem_priv)
        ->vb2_dc_vaddr
            return buf->vaddr  // 返回存储图像内存的虚拟地址

4.2.4.VIDIOC_STREAMON

使用VIDIOC_STREAMON命令调用ioctl,最终会调用到vb2_streamon函数,内核使用vb2_streamon函数将开启视频流,对于图像采集设备而言,则设备开始采集图像,并将图像数据保存到缓冲区中。

    [include/media/videobuf2-core.h]
    // q-缓冲区队列数据结构struct vb2\_queue指针
    // type-缓冲区类型
    // 返回值-0成功,小于0失败
    int vb2\_streamon(struct vb2_queue \*q, enum v4l2_buf_type type);

vb2_streamon主要的工作如下:
(1)遍历queued_list链表,首先将缓冲区状态设置为VB2_BUF_STATE_ACTIVE,然后将入队的缓冲区都添加到驱动的队列中。imx6ull平台调用mx6s_videobuf_queue将缓冲区添加到capture链表中。
(2)调用start_streaming开始视频流,imx6ull平台调用mx6s_start_streaming函数使能设备,开始图像采集。
(3)流开启标志设置streaming设置为1。

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前在阿里

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化的资料的朋友,可以点击这里获取!

加到capture链表中。
(2)调用start_streaming开始视频流,imx6ull平台调用mx6s_start_streaming函数使能设备,开始图像采集。
(3)流开启标志设置streaming设置为1。

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前在阿里

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
[外链图片转存中…(img-6YTYe4TR-1715762430224)]
[外链图片转存中…(img-YivCWrUH-1715762430224)]
[外链图片转存中…(img-W8ASl7ur-1715762430225)]
[外链图片转存中…(img-fRBhvTMI-1715762430225)]
[外链图片转存中…(img-da6YgFZK-1715762430225)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

需要这份系统化的资料的朋友,可以点击这里获取!

  • 5
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值