当前位置: 首页 > news >正文

加强网站政务服务建设建设银行激活社保卡网站

加强网站政务服务建设,建设银行激活社保卡网站,邯郸论坛官网,网络服务商怎么查询文章目录一、/dev/videoX1. 查询设备能力2. 获取当前视频格式3. 设置视频格式4. 申请缓冲区1) mmap 方式2) user 分配5. 查询缓冲区信息6. 将缓冲区放入队1) fill_vb2_buffer2) buf_prepare3) get_userptr4) buf_init5) attach_dmabuf 和 map_dmabuf6) start_streaming7) 总结7… 文章目录一、/dev/videoX1. 查询设备能力2. 获取当前视频格式3. 设置视频格式4. 申请缓冲区1) mmap 方式2) user 分配5. 查询缓冲区信息6. 将缓冲区放入队1) fill_vb2_buffer2) buf_prepare3) get_userptr4) buf_init5) attach_dmabuf 和 map_dmabuf6) start_streaming7) 总结7. 启动视频流1) buf_queue2) vidioc_streamon3. vb2_buffer_done8. 从队列中取出缓冲9. mmap 映射内存10. 停止视频流11. 总结二、实现一个虚拟摄像头1. 最简单的虚拟摄像头2. 支持查询设备能力3. 支持申请 buffer 的能力4. 支持查询缓冲区信息5. 支持 mmap 功能6. 支持将队列放入缓冲区7. 添加启动视频流功能8. 从队列中取出缓冲三、/dev/subdevX1. subdev 的接口2. 实现简单的 subdev 驱动1) 注冊三個 subdev2) 注册 isp 对应的 video四、media controller1. media controller 的核心组件1) media_device2) entity 组件3) pad 组件4) media_link 组件2. 添加 media controller1) 给 isp 添加 media entity2) 给 subdev 添加 media entity3) 应用程序4) 验证结果五、设备树 ports 方法1. 常见接口1) fwnode_graph_get_port_parent2) fwnode_graph_get_remote_endpoint3) fwnode_graph_parse_endpoint4) fwnode_graph_get_remote_port5) fwnode_graph_get_port_parent2. 编程验证1) 设备 A 的验证程序2) 设备 B 的验证程序3. 总结作者: baron 博客: baron-z.cn V4L2(Video for Linux 2) 是 Linux 操作系统中的视频驱动框架, 它为视频设备提供了统一的处理接口. 本文基于内核版本 Linux 4.4.94 分析 linux 视频驱动框架. 它包括以下内容. /dev/videoX 视频流处理框架/dev/v4l-subdevX 的子设备框架Media Controller 框架设备树 ports 方法 /dev/videoX 节点对应的设备后续会称为 video 设备和 video 框架. /dev/v4l-subdevX 节点对应的设备则称为 subdev 设备和 subdev 框架. 无论是 video 设备还是 subdev 设备, 都是通过 v4l2_device 进行统一管理. video 设备和 subdev 设备 的整体框架如下所示. 注意: video 指代的是 /dev/videoX 节点对应的设备, 而不是内核中的 video_deivce 数据结构. 这个数据结构无论是 video 设备还是 subdev 设备都用到了. 一、/dev/videoX /dev/videoX 设备节点用于摄像头视频流的管理, 视频流数据传输, 摄像头到内存的 dma 传输, 视频流的控制, 启动停止, 内存分配, 自定义操作等. 它的整体框架如图所示. 可以看出 video 设备本质就是字符设备. 主要操作就是一堆 ioctl, 主要的宏如下. ioctl 宏功能含义VIDIOC_QUERYCAP查询设备能力如是否支持视频输入/输出、驱动信息等VIDIOC_G_FMT获取当前视频格式分辨率、像素格式等VIDIOC_S_FMT设置视频格式分辨率、像素格式等VIDIOC_REQBUFS申请缓冲区用于流式 I/O 传输VIDIOC_QUERYBUF查询缓冲区信息如内存地址、大小、状态等VIDIOC_QBUF将缓冲区放入队列用于数据采集VIDIOC_DQBUF从队列中取出缓冲区获取已采集的数据VIDIOC_STREAMON启动视频流开始采集数据VIDIOC_STREAMOFF停止视频流停止采集数据VIDIOC_DEFAULT处理未定义的 ioctl 请求默认操作对于以上的操作, 对应驱动需要提供三个操作函数 v4l2_ioctl_ops , vb2_ops, 以及 vb2_mem_ops. 1. 查询设备能力 应用需要返回的数据结构如下. struct v4l2_capability {__u8 driver[16]; // 驱动模块的名称__u8 card[32]; // 视频设备描述信息, 厂商型号等__u8 bus_info[32]; // 总线信息__u32 version; // 内核版本号, 通过 KERNEL_VERSION 宏获取__u32 capabilities; // 设备的整体功能__u32 device_caps; // 设备节点的访问能力__u32 reserved[3]; // 保留 };对应的 ioctl 宏为 VIDIOC_QUERYCAP #define VIDEO_DEVICE /dev/video0int main() {int fd open(VIDEO_DEVICE, O_RDWR);// 1. 查询设备能力struct v4l2_capability cap;ioctl(fd, VIDIOC_QUERYCAP, cap)...... // 省略部分代码close(fd); };调用流程如下图 对于该接口, 驱动需要实现 vidioc_querycap 接口. 主要用于提供 cap-driver, cap-card, cap-bus_info const struct v4l2_ioctl_ops xxx_ioctl_ops {.vidioc_querycap xxx_querycap, };2. 获取当前视频格式 应用需要返回的数据结构如下. /** 使用时根据 type 字段的值来选择合适的 fmt 字段格式* - 如果 type 是 V4L2_BUF_TYPE_VIDEO_CAPTURE则使用 pix* - 如果 type 是 V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE则使用 pix_mp* - 如果 type 是 V4L2_BUF_TYPE_VIDEO_OVERLAY则使用 win* - 如果 type 是 V4L2_BUF_TYPE_VBI_CAPTURE则使用 vbi* - 如果 type 是 V4L2_BUF_TYPE_SLICED_VBI_CAPTURE则使用 sliced* - 如果 type 是 V4L2_BUF_TYPE_SDR_CAPTURE则使用 sdr* - 如果 type 是 V4L2_BUF_TYPE_META_CAPTURE则使用 meta* - 如果需要自定义格式可以使用 raw_data*/ struct v4l2_format {__u32 type;union {// 单平面视频图像格式适用于大多数普通摄像头struct v4l2_pix_format pix;// 多平面视频图像格式适用于 YUV420 等多平面格式如现代摄像头或视频编解码器struct v4l2_pix_format_mplane pix_mp;// 视频叠加层格式定义视频叠加窗口的位置、混合方式等struct v4l2_window win;// 原始 VBI (Vertical Blanking Interval) 格式// 用于捕获模拟视频信号中的垂直消隐期数据如图文电视struct v4l2_vbi_format vbi;// 分片 VBI 格式结构化的 VBI 数据按行分片struct v4l2_sliced_vbi_format sliced;// 软件定义无线电 (SDR) 格式// 用于定义 SDR 设备的采样率、调制方式等参数 struct v4l2_sdr_format sdr;// 原始数据缓冲区(保留字段用于未来扩展或自定义格式)struct v4l2_meta_format meta; __u8 raw_data[200];} fmt; };对于普通的摄像头是会用到 v4l2_pix_format. 这里只以这个为例进行说明 // 描述单平面视频帧的格式适用于传统摄像头 struct v4l2_pix_format {// 图像宽度像素例如 1920__u32 width;// 图像高度像素例如 1080__u32 height;// 像素格式四字符编码如 V4L2_PIX_FMT_YUYV 表示 YUV422 格式__u32 pixelformat;// 扫描模式枚举 v4l2_field// V4L2_FIELD_NONE 逐行扫描// V4L2_FIELD_INTERLACED 隔行扫描__u32 field;// 每行像素数据的字节数包含内存对齐填充// 计算公式width * 每像素字节数 填充字节// 若为 0 表示无填充连续存储__u32 bytesperline;// 整个图像数据的总大小单位字节// 典型计算bytesperline * height__u32 sizeimage;// 颜色空间枚举 v4l2_colorspace// 例如V4L2_COLORSPACE_SRGBsRGB 颜色空间__u32 colorspace;// 私有数据含义取决于 pixelformat通常保留使用__u32 priv;// 格式标志位V4L2_PIX_FMT_FLAG_* 的位掩码组合// 例如V4L2_PIX_FMT_FLAG_PREMUL_ALPHA预乘 Alpha 通道__u32 flags;// 颜色编码联合体根据场景选择成员union {// YCbCr 编码标准如 ITU-R BT.601/709/2020__u32 ycbcr_enc;// HSV 编码方式极少使用场景__u32 hsv_enc;};// 量化范围枚举 v4l2_quantization// V4L2_QUANTIZATION_FULL_RANGE 0-255PC 范围// V4L2_QUANTIZATION_LIM_RANGE 16-235TV 范围YUV 默认__u32 quantization;// 传输函数枚举 v4l2_xfer_func// 定义光信号到电信号的转换曲线如 sRGB 伽马曲线__u32 xfer_func; };同样的应用调用如下. ...... // 省略无关代码struct v4l2_format fmt;fmt.type V4L2_BUF_TYPE_VIDEO_CAPTURE;ioctl(fd, VIDIOC_G_FMT, fmt) ......调用到驱动的调用链如下 也就是驱动要实现一个 vidioc_g_fmt_vid_cap 用于填充 v4l2_pix_format 数据结构. const struct v4l2_ioctl_ops xxx_ioctl_ops {.vidioc_g_fmt_vid_cap xxx_fmt_vid_cap, };3. 设置视频格式 struct v4l2_format new_fmt;new_fmt.type V4L2_BUF_TYPE_VIDEO_CAPTURE;new_fmt.fmt.pix.width 640;new_fmt.fmt.pix.height 480;new_fmt.fmt.pix.pixelformat V4L2_PIX_FMT_YUYV;new_fmt.fmt.pix.field V4L2_FIELD_INTERLACED;ioctl(fd, VIDIOC_S_FMT, new_fmt)调用流程如下 驱动需要实现 vidioc_s_fmt_vid_cap 函数, 用于设置摄像头格式. const struct v4l2_ioctl_ops xxx_ioctl_ops {.vidioc_s_fmt_vid_cap xxx_fmt_vid_cap, };4. 申请缓冲区 v4l2 支持 4 种分配内存的方式 mmap, user, overlay, dma-buf. 本文只讲前两种常用的, 在内核中使用 vb2_queue 来管理 buffer. 在内核中如下图所示 每一个 vb2_buffer 用于描述一帧图像一帧图像可以由一个或者多个 vb2_plane 组成, (例如可以将 YUV 数据分别存储于三个 vb2_plane), 大多数情况一个 vb2_plane 里面存放一帧数据每一个 vb2_plane 有一个 mem_priv 指向一个数据结构, 该结构用于保存实际分配的内存, videobuf2 为我们提供了一个数据结构 vb2_vmalloc_buf.由于每一帧图像的数据量是确定的, 分辨率一般是不会变的, 所以为了减少代码冗余, 使用 plane_sizes 数组用于保存每个 vb2_plane 的数据大小 1) mmap 方式 用于空间需要用到 v4l2_requestbuffers 用来要分配的内存类型. struct v4l2_requestbuffers {__u32 count; // 缓冲区的数量__u32 type; // 缓冲区的类型, 由 v4l2_buf_type 进行描述__u32 memory; // 内存分配的方式__u32 capabilities;__u32 reserved[1]; };// 可用的 type , 常用的就两个 enum v4l2_buf_type {V4L2_BUF_TYPE_VIDEO_CAPTURE 1, // 捕获视频流数据V4L2_BUF_TYPE_VIDEO_OUTPUT 2, // 输出视频流数据.....// 省略/* Deprecated, do not use */V4L2_BUF_TYPE_PRIVATE 0x80, };// memory 的类型有四种 enum v4l2_memory {V4L2_MEMORY_MMAP 1, // mmap 内核空间分配V4L2_MEMORY_USERPTR 2, // user 用户空间分配V4L2_MEMORY_OVERLAY 3, // 覆盖模式, 将数据直接映射到显存V4L2_MEMORY_DMABUF 4, // dma-buf };应用的调用方式如下所示 struct v4l2_requestbuffers req;req.count 4;req.type V4L2_BUF_TYPE_VIDEO_CAPTURE;req.memory V4L2_MEMORY_MMAP;ioctl(fd, VIDIOC_REQBUFS, req);驱动的调用流程如下. 从调用流程可以知道驱动要实现的接口有 queue_setup static const struct vb2_ops uvc_queue_qops {.queue_setup uvc_queue_setup,...... };传入的 count 被转换为 num_buffers, 但具体的数量由驱动决定, 以及每一个 vb2_buffer 的数量也是由驱动决定的. 因此 uvc_queue_setup 包含以下内容. 根据传入的 num_buffers 重新计算 vb2_buffer 的数量.指定 vb2_plane 的数量, 大部分情况为 1根据 vb2_plane 的数量, 将每个 vb2_plane 的大小填充到 vb2_queue-plane_sizes 数组中 可以参考 uvc 驱动的实现 drivers/usb/gadget/function/uvc_queue.c, 这里就不搬代码了.从上面的图可以看出 __vb2_queue_alloc 函数也是非常重要的函数我这里就直接附上源码. static int __vb2_queue_alloc(struct vb2_queue *q, enum vb2_memory memory,unsigned int num_buffers, unsigned int num_planes) {unsigned int buffer;struct vb2_buffer *vb;int ret;// 分配 num_buffers 个 vb2_bufferfor (buffer 0; buffer num_buffers; buffer) {vb kzalloc(q-buf_struct_size, GFP_KERNEL); // 分配 vb2_bufferif (!vb) {dprintk(1, memory alloc for buffer struct failed\n);break;}// 设置 buffer 的 state 为已出队状态// VB2_BUF_STATE_DEQUEUED: 已出队, 代表缓冲区已被用户空间取出当前由用户应用程序控制// VB2_BUF_STATE_PREPARING: 准备中, 缓冲还未完全初始化// VB2_BUF_STATE_PREPARED: videobuf 和驱动程序已完成缓冲区的准备缓冲区可用于硬件操作// VB2_BUF_STATE_QUEUED: 缓冲区已加入 videobuf 队列但尚未送至驱动程序处理// VB2_BUF_STATE_REQUEUEING: 缓冲区正在重新提交给驱动程序进行处理// VB2_BUF_STATE_ACTIVE: 缓冲区已经被驱动程序接收并可能正在用于硬件操作如视频采集、编码等// VB2_BUF_STATE_DONE: 硬件或驱动已处理完缓冲区并返回到 videobuf但尚未被用户空间取出, 操作完成等待用户获取// VB2_BUF_STATE_ERROR: 缓冲区处理过程中发生错误vb-state VB2_BUF_STATE_DEQUEUED;vb-vb2_queue q; // 设置所属的 vb2_queuevb-num_planes num_planes; // 设置 vb2_plane 的数量vb-index q-num_buffers buffer; // 设置 index , 该 index 也是数组标vb-type q-type; // 设置 typevb-memory memory; // 设置内存分配方式if (memory VB2_MEMORY_MMAP) {// 遍历 vb2_buffer 的 vb2_plane 为每一个 vb2_plane 分配内存// 分配的内存保存在 mem_priv 数据结构中, 具体内存的分配方式由b-vb2_queue-mem_ops-alloc 提供, vb2_plane 的大小更新到 length 成员变量ret __vb2_buf_mem_alloc(vb);if (ret) {dprintk(1, failed allocating memory for buffer %d\n, buffer);kfree(vb);break;}// 如果 ops 设置了 buf_init 则调用该函数对 buffer 进一步初始化.ret call_vb_qop(vb, buf_init, vb);if (ret) {dprintk(1, buffer %d %p initialization failed\n, buffer, vb);__vb2_buf_mem_free(vb);kfree(vb);break;}}// 将 vb2_buffer 保存到 vb2_queue-bufs[]数组中 中q-bufs[q-num_buffers buffer] vb;}// 更新 vb2_buffer 中 vb2_plane 的大小__setup_lengths(q, buffer);// 更新新分配的 vb2_plane.m.offset 的值, 它等于之前分配的 plane 的数据长度加上自己的数据长度. if (memory VB2_MEMORY_MMAP)__setup_offsets(q, buffer);dprintk(1, allocated %d buffers, %d plane(s) each\n,buffer, num_planes);return buffer; }这个函数的功能如下 分配 num_buffers 个 vb2_buffer , 设置所属的 vb2_queue, 更新 vb2_plane 的数量 num_planes, 设置数组索引 index, 设置内存分配方式 memory.如果内存分配方式是 VB2_MEMORY_MMAP, 则遍历 vb2_buffer 的 vb2_plane 为每一个 vb2_plane 分配内存, 配的内存保存在 mem_priv 数据结构中, 具体内存的分配方式由 b-vb2_queue-mem_ops-alloc 提供更新 vb2_buffer 中 vb2_plane 的大小即 length 成员变量.如果内存分配的方式是 VB2_MEMORY_MMAP , 更新新分配的 vb2_plane.m.offset 的值, 它等于之前分配的 vb2_plane 的数据长度加上自己的数据长度. 我们只需要知道 vb2_plane.m.offset 每一个 vb2_plane 都是唯一的, 它也是作为 plane 的索引. 其中驱动需要提供 alloc 函数, 用于分配实现内存分配接口. const struct vb2_mem_ops vb2_vmalloc_memops {.alloc vb2_vmalloc_alloc,...... }对于 vb2_mem_ops 我们一般使用默认的 vb2_buffer 提供的默认接口, 这里 alloc 则对应 vb2_vmalloc_alloc. static void *vb2_vmalloc_alloc(void *alloc_ctx, unsigned long size,enum dma_data_direction dma_dir, gfp_t gfp_flags) {struct vb2_vmalloc_buf *buf;buf kzalloc(sizeof(*buf), GFP_KERNEL | gfp_flags);if (!buf)return NULL;buf-size size; // 设置 buffer 的大小buf-vaddr vmalloc_user(buf-size); // 分配虚拟内存buf-dma_dir dma_dir; // 设置 dma_dirbuf-handler.refcount buf-refcount;buf-handler.put vb2_vmalloc_put;buf-handler.arg buf;if (!buf-vaddr) {pr_debug(vmalloc of size %ld failed\n, buf-size);kfree(buf);return NULL;}atomic_inc(buf-refcount);return buf; }这个函数就是用于分配内存的, vb2 提供了一个默认的数据结构. 这个数据结构被保存到了 vb2_buffer-planes[plane].mem_priv. struct vb2_vmalloc_buf {void *vaddr; // 用于保存分配的内存的虚拟地址.struct frame_vector *vec;enum dma_data_direction dma_dir;unsigned long size;refcount_t refcount;struct vb2_vmarea_handler handler;struct dma_buf *dbuf; // 如果是 dma_buf 则放到这里 };总结 驱动需要实现 vb2_ops-queue_setup 用于提供 vb2_buffer 的数量, vb2_plane 的数量和大小.驱动需要还需要实现另一个接口 vb2_mem_ops-alloc 用于分配实际的内存空间, 大多数情况, 使用 videobuf2-vmalloc.c 中提供的 vb2_vmalloc_alloc 函数, 当然也可以根据需求自己实现. 2) user 分配 user 分配和 mmap 分配完全一样只是不会调用 __vb2_buf_mem_alloc 分配触发真正的内存分配而已. 因为真正的内存由用户空间分配, 在 VIDIOC_QBUF 提交 buffer 到内核操作的时候会分配该 __vb2_buf_mem_alloc, 并将转换后的虚拟地址填充到该结构. 5. 查询缓冲区信息 查询缓冲区的作用就是返回前面分配的内存信息, 即返回内核里面的 v4l2_buffer. ......struct v4l2_buffer buf;buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE;buf.memory V4L2_MEMORY_MMAP;buf.index 0;ioctl(fd, VIDIOC_QUERYBUF, buf); ......调用流程如下所示 根据 index 返回分配的对应的 v4l2_buffer , 不同内存分配的方式也会返回不同的信息. mmap 分配的内存则会返回 b-m.offset 这个则是 mmap 的索引user 方式则返回 b.m-userptr 指向用户空间分配的内存dma-buf 方式则返回对应 b.m.fd 6. 将缓冲区放入队 struct v4l2_buffer buf;buf.index 0;buf.length dev-mem[i].length;buf.m.userptr (unsigned long)dev-mem[i].start;buf.type V4L2_BUF_TYPE_VIDEO_OUTPUT;if (ioctl(fd, VIDIOC_QBUF, buf) -1) {perror(将缓冲区放入队列失败);close(fd);return 1;}调用流程如下所示 从调用链可以看出, 对于 VIDIOC_QBUF 操作就是一些回调函数的实现, 主要需要实现的函数如下所示. 1) fill_vb2_buffer 非必须实现, 该接口用于更新用户传入的 vb2_buffer , 即用内核里面的 vb2_buffer 更新用户空间传入的 vb2_buffer. 内核为我们提供了一个默认的实现 __fill_vb2_buffer. 当调用内核的 vb2_queue_init 初始化 vb2_queue 时设置. //drivers/media/common/videobuf2/videobuf2-v4l2.c static const struct vb2_buf_ops v4l2_buf_ops {.fill_vb2_buffer __fill_vb2_buffer, };int vb2_queue_init(struct vb2_queue *q) {......q-buf_ops v4l2_buf_ops;...... };2) buf_prepare 该接口用于驱动对 buffer 在放入链表 vb2_queue-queued_lis 前作处理. static struct vb2_ops uvc_queue_qops {...... .buf_prepare uvc_buffer_prepare,...... };static int uvc_buffer_prepare(struct vb2_buffer *vb) {struct uvc_video_queue *queue vb2_get_drv_priv(vb-vb2_queue);struct vb2_v4l2_buffer *vbuf to_vb2_v4l2_buffer(vb);struct uvc_buffer *buf container_of(vbuf, struct uvc_buffer, buf);.... // 更新 buffer 状态buf-state UVC_BUF_STATE_QUEUED;buf-mem vb2_plane_vaddr(vb, 0); // 获取 buffer 的虚拟地址buf-length vb2_plane_size(vb, 0); // 获取 buffer 的大小if (vb-type V4L2_BUF_TYPE_VIDEO_CAPTURE)buf-bytesused 0;elsebuf-bytesused vb2_get_plane_payload(vb, 0);return 0; }对于 uvc 来说就是更新私有 uvc_buffer 的虚拟地址和 buffer 的大小 buf_prepare 会对 vb2_buffer 做预处理. 它和后面 buf_queue 是两个相互配合的接口. uvc 和一些常规的驱动, 会在这里拿到 vb2_buffer 的虚拟地址. 还有一些例如 rk 的驱动会在 buf_queue 中拿走虚拟地址. 就是 camer 控制器的图像传输用到的内存地址是从 buf_prepare或者 buf_queue 获取的. 具体取决于驱动的选择. 3) get_userptr 如果需要支持用户空间分配内存必须实现, 该接口用于将用户空间的分配的内存地址, 转换为内核空间的虚拟地址. 内核也提供了默认的实现 vb2_vmalloc_get_userptr, 一般情况初始化 vb2_queue 时设置, 参考 uvc const struct vb2_mem_ops vb2_vmalloc_memops {.......get_userptr vb2_vmalloc_get_userptr,...... };int uvcg_queue_init(struct uvc_video_queue *queue, enum v4l2_buf_type type,struct mutex *lock) {int ret;// ...... 省略部分代码queue-queue.ops uvc_queue_qops; // 初始化 vb2_ops//......queue-queue.mem_ops vb2_vmalloc_memops; // 初始化 vb2_mem_ops//......ret vb2_queue_init(queue-queue);// ......return 0; }vb2_vmalloc_get_userptr 会创建 vb2_vmalloc_buf 用来保存内存的内核虚拟地址, 如果是 mmap 则在申请内存操作 VIDIOC_REQBUFS 时分配, 用户分配则挪到了这里. static void *vb2_vmalloc_get_userptr(struct device *dev, unsigned long vaddr,unsigned long size,enum dma_data_direction dma_dir) {struct vb2_vmalloc_buf *buf;struct frame_vector *vec;int n_pages, offset, i;int ret -ENOMEM;buf kzalloc(sizeof(*buf), GFP_KERNEL);if (!buf)return ERR_PTR(-ENOMEM);buf-dma_dir dma_dir;offset vaddr ~PAGE_MASK;buf-size size;// ...... 省略部分代码if (frame_vector_to_pages(vec) 0) {buf-vaddr (__force void *)ioremap_nocache(__pfn_to_phys(nums[0]), size offset);} else {buf-vaddr vm_map_ram(frame_vector_pages(vec), n_pages, -1, PAGE_KERNEL);}buf-vaddr offset;return buf; }4) buf_init 如果一直没有分配 mem_priv 则会尝试调用 buf_init, 没看到有啥驱动用到, 不做分析. 5) attach_dmabuf 和 map_dmabuf dma-buf 中用获取 dma-buf 内存的回调接口. 这里也不详细描述. 后面可能会补充. 6) start_streaming 尝试开始取流, 如果 q-streaming 设置了且检测到 queued_count 小于 min_buffers_needed, 则尝试触发取流操作. 7) 总结 VIDIOC_QBUF 参数用于将缓冲区放入队列, 分三种情况 mmap、 user 和 dma-buf. 分别对应三种内存分配的方式. 不同的方式需要实现的回调不同.用户空间只需要提供内存的分配方式, 以及 buf.index. 缓冲区的分配要通过 VIDIOC_REQBUFS 来实现. 简单的操作就是分配之后用 VIDIOC_QUERYBUF 拿到分配的 vb2_buffer. 用这个 buffer 来实现 VIDIOC_QBUF 操作内核会更新用户传入的 vb2_buffer 信息, 同步为内核的 vb2_buffer.将 vb-queued_entry 链接到 q-queued_list, 同时增加 q-queued_count 的引用计数. 链表 q-queued_list 就是等待处理 buffer 的链表. 7. 启动视频流 if (ioctl(fd, VIDIOC_STREAMON, buf.type) -1) {perror(启动视频流失败);close(fd);return 1;}调用流程如下. 1) buf_queue 在启动视频流前遍历需要处理的 buffer, 做启动前的预处理. 部分驱动在这里获取 vb2_buffer 的地址给到 carme 控制器. static struct vb2_ops rkcif_vb2_ops {// .......wait_prepare vb2_ops_wait_prepare,// ...... };static void rkcif_buf_queue(struct vb2_buffer *vb) {// ......for (i 0; i fmt-mplanes; i) {void *addr vb2_plane_vaddr(vb, i); // 获取 vb2_buffer 的虚拟地址if (hw_dev-iommu_en) {struct sg_table *sgt vb2_dma_sg_plane_desc(vb, i);// 填充到 carmera 控制器的 buffercifbuf-buff_addr[i] sg_dma_address(sgt-sgl);} else {cifbuf-buff_addr[i] vb2_dma_contig_plane_dma_addr(vb, i);}}// ......2) vidioc_streamon 启动视频流的回调接口, 可以直接实现启动功能, 参考 uvc 驱动的实现 const struct v4l2_ioctl_ops uvc_v4l2_ioctl_ops {//....vidioc_streamon uvc_v4l2_streamon,// ... };或者这里使用内核的默认实现 vb2_ioctl_streamon, 然后在实现 start_streaming 作为驱动真正的流启动接口. 以 vimc 为例, 如下所示. static const struct v4l2_ioctl_ops vimc_cap_ioctl_ops {.......vidioc_streamon vb2_ioctl_streamon,...... };static const struct vb2_ops vimc_cap_qops {// .......start_streaming vimc_cap_start_streaming,// ...... };这里可以写寄存器启动 sensor 也可以通过 v4l2_subdev_call 接口调用 senor 的 subdev 对应的启动接口启动. 3. vb2_buffer_done 如果 buffer 已经处理完了, 则唤醒 done_wq 将 buffer 返回给用户空间. 8. 从队列中取出缓冲 if (ioctl(fd, VIDIOC_DQBUF, buf) -1) {perror(从队列中取出缓冲区失败);close(fd);return 1;}调用流程如下所示. 从缓冲区冲取出队列的关键函数是 __vb2_wait_for_done_vb, 他的功能如下 static int __vb2_wait_for_done_vb(struct vb2_queue *q, int nonblocking) {for (;;) {int ret;if (q-waiting_in_dqbuf) { // 当前有其他进程在等待缓冲区, 则直接返回return -EBUSY;}if (!q-streaming) { // 视频流停止了也直接返回return -EINVAL;}if (q-error) { // 队列中有错误也直接返回return -EIO;}if (q-last_buffer_dequeued) { // 最后一个缓冲区出队返回return -EPIPE;}if (!list_empty(q-done_list)) { // 如果已经有完成的缓冲区退出break;}if (nonblocking) { // 没有缓冲区直接返回return -EAGAIN;}q-waiting_in_dqbuf 1; // 设置占用该 vb2_queuecall_void_qop(q, wait_prepare, q); // 回调 vb2_queue-ops-wait_prepare 加锁// 进入等待, 等待缓冲区准备好.dprintk(3, will sleep waiting for buffers\n);ret wait_event_interruptible(q-done_wq,!list_empty(q-done_list) || !q-streaming ||q-error);call_void_qop(q, wait_finish, q); // 回调 vb2_queue-ops-wait_finish 解锁q-waiting_in_dqbuf 0; // 释放占用}return 0; }当前有其他进程在等待缓冲区 q-waiting_in_dqbuf、视频流停止了 !q-streaming、队列中有错误q-error、最后一个缓冲区已经出队 q-last_buffer_dequeued、都立即返回.调用 wait_prepare 默认实现为 vb2_ops_wait_prepare 为 vb2_queue 加锁. 设置 q-waiting_in_dqbuf 表示当前进程占用该 vb2_queue.wait_event_interruptible 进入等待, 当 buffer 准备好会触发中断, 在中断中调用 vb2_buffer_done 唤醒 q-done_wq 队列返回.调用 wait_finish 默认实现为 vb2_ops_wait_finish 为 vb2_queue 解锁. 清除 q-waiting_in_dqbuf 释放前进程占用 vb2_queue. 9. mmap 映射内存 void *buffer_start; buffer_start mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);映射内存就是通过 buf.m.offset 找到对应的 vb2_plane , 然后映射其内存到用户空间.内核为我们提供了默认实现, 调用流程如下所示. 这里贴一下关键代码, 这部分内存主要是内存映射想关, 不再这篇文章过多赘述. vb2_vmalloc_mmap() --remap_vmalloc_range() -- // 这里就把内核的虚拟内存映射到用户空间了vma-vm_start (unsigned long)(addr (pgoff PAGE_SHIFT));vma-vm_end vma-vm_start size;10. 停止视频流 if (ioctl(fd, VIDIOC_STREAMOFF, buf.type) -1) {perror(停止视频流失败);close(fd);return 1; }调用流程如下. 和启动流相同, 可以在 ops-vidioc_streamoff 中就直接实现停止操作, 例如 uvc const struct v4l2_ioctl_ops uvc_v4l2_ioctl_ops {// .......vidioc_streamoff uvc_v4l2_streamoff,// ...... };也可以调用通用的接口 vb2_ioctl_streamoff, 然后在实现 vb2_queue-ops-stop_streaming 接口用于真正的停止操作. 如下所示 static const struct v4l2_ioctl_ops vpif_ioctl_ops {// .......vidioc_streamoff vb2_ioctl_streamoff, // 调通通用接口// ...... };static const struct vb2_ops video_qops {// .......stop_streaming vpif_stop_streaming, // 真正的停止接口// ...... };11. 总结 完整的整理整个 /dev/videox 的操作流程可以发现, 其实就是对 vb2_buffer 的操作. 它聚焦于视频的流处理.下图完整的展示了一帧图像的处理流程(这图对应 mmap). 的这些步骤已近是很精简的步骤了. 知识回顾: 每一帧图像用一个 vb2_buffer 描述, 一帧图像可以由一个或者多个 vb2_plane 组成, 每一个 vb2_plane 有一个 mem_priv 指向一个数据结构, 该结构用于保存实际分配的内存, videobuf2 为我们提供了一个数据结构 vb2_vmalloc_buf 驱动需要实现的接口如下: 接口说明vb2_queue-ops-queue_setup用于确认 vb2_plane 的数量和大小。vb2_queue-mem_ops-alloc用于分配一帧图像传输需要的内存。vb2_queue-buf_ops-fill_user_buffer用于将 vb2_buffer 返回给用户空间。vb2_queue-ops-buf_prepare获取前面分配的 vb2_buffer 的 vb2_plane 中虚拟地址把它传给 camera 控制器或者对 plane 做预处理。它和 vb2_queue-ops-buf_queue 是二选其一如果在 buf_queue 中获取这里就不用获取。vb2_queue-ops-buf_queue获取前面分配的 vb2_buffer 的 vb2_plane 中虚拟地址把它传给 camera 控制器。它和 vb2_queue-ops-buf_prepare 是二选其一如果在 buf_prepare 中获取这里就不用获取。vb2_queue-ops-start_streaming启动摄像头传输。如果这里使用 vb2_ioctl_streamon 系统接口则需要实现 vb2_queue-ops-start_streaming。vb2_queue-ops-start_streaming如果前面的 vb2_queue-ops-start_streaming 使用了 vb2_ioctl_streamon 接口则需要实现这个接口。vb2_queue-mem_ops-mmap用于映射内核虚拟地址到用户空间一般使用默认实现 vb2_vmalloc_mmap 二、实现一个虚拟摄像头 有了前面的知识, 我们来逐步实现一个支持 mmap 的虚拟摄像头.为了减少代码的冗余, 除了第一个程序外, 后续的修改将以 patch 的形式给出. 这样也方便观察修改了哪些地方. 1. 最简单的虚拟摄像头 首先是实现一个简单的摄像头驱动, 它只需要具备 /dev/videox 节点就行了. #include linux/module.h #include linux/kernel.h #include linux/fs.h #include linux/cdev.h #include linux/device.h #include linux/uaccess.h #include linux/videodev2.h #include media/v4l2-device.hstatic struct v4l2_device v4l2_dev; static struct video_device vcam_vdev;static int vcam_open(struct file *file) {printk(KERN_INFO simple_vcam: device opened\n);return 0; }static int vcam_release(struct file *file) {printk(KERN_INFO simple_vcam: device closed\n);return 0; }static const struct v4l2_file_operations vcam_fops {.owner THIS_MODULE,.open vcam_open,.release vcam_release, };static int __init vcam_init(void) {int ret;printk(KERN_INFO simple_vcam: initializing\n);// 当第一个参数为 NULL 时必须设置 namesnprintf(v4l2_dev.name, sizeof(v4l2_dev.name), %s, vcam v4l2 dev);ret v4l2_device_register(NULL, v4l2_dev);if (ret) {printk(KERN_ERR simple_vcam: v4l2_device_register failed\n);return ret;}// 初始化 vcam_vdevstrscpy(vcam_vdev.name, Simple Virtual Camera, sizeof(vcam_vdev.name));vcam_vdev.v4l2_dev v4l2_dev; // 必须设置所属 v4l2_devvcam_vdev.fops vcam_fops; // 设置 fopsvcam_vdev.release video_device_release_empty; // 必须设置 relase 接口vcam_vdev.vfl_dir VFL_DIR_RX;vcam_vdev.device_caps V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_READWRITE;// 注册 video device.ret video_register_device(vcam_vdev, VFL_TYPE_GRABBER, -1);if (ret) {printk(KERN_ERR simple_vcam: video_register_device failed\n);v4l2_device_unregister(v4l2_dev);return ret;}printk(KERN_INFO simple_vcam: registered video device /dev/video%d\n, vcam_vdev.minor);return 0; }static void __exit vcam_exit(void) {printk(KERN_INFO simple_vcam: exiting\n);video_unregister_device(vcam_vdev);v4l2_device_unregister(v4l2_dev); }module_init(vcam_init); module_exit(vcam_exit);MODULE_LICENSE(GPL); MODULE_AUTHOR(baron); MODULE_DESCRIPTION(Simple virtual camera driver example);验证程序 #include stdio.h #include fcntl.h #include unistd.hint main() {int fd;// 打开设备 /dev/video9根据实际情况可能是 /dev/videoXfd open(/dev/video9, O_RDWR);if (fd -1) {perror(打开设备失败);return -1;}printf(设备已打开成功\n);// 关闭设备close(fd);printf(设备已关闭。\n);return 0; }验证结果: console:/cache # ./mytest [ 1777.977139] simple_vcam: device opened camera open camera close console:/cache # [ 1777.977359] simple_vcam: device closed2. 支持查询设备能力 驱动修改如下 Index: kernel/drivers/media/my_camera.c--- kernel.orig/drivers/media/my_camera.ckernel/drivers/media/my_camera.c-6,6 6,7 #include linux/uaccess.h#include linux/videodev2.h#include media/v4l2-device.h #include media/v4l2-ioctl.hstatic struct v4l2_device v4l2_dev;static struct video_device vcam_vdev;-20,8 21,34 static int vcam_release(struct file *filreturn 0;}// 驱动需要实现 querycap 用于返回 video 信息 // cap-driver: 设备的驱动信息 // cap-card: 设备的名称 // cap-bus_info: 设备的总线信息 static int vcam_v4l2_querycap(struct file *file, void *fh, struct v4l2_capability *cap) {struct video_device *vdev video_devdata(file);strlcpy(cap-driver, virtual_vcam, sizeof(cap-driver));strlcpy(cap-card, Virtual Camera, sizeof(cap-card));strlcpy(cap-bus_info, vdev-name, sizeof(vdev-name));cap-device_caps V4L2_CAP_VIDEO_OUTPUT | V4L2_CAP_STREAMING;cap-capabilities cap-device_caps | V4L2_CAP_DEVICE_CAPS;return 0; }const struct v4l2_ioctl_ops vcam_v4l2_ioctl_ops {.vidioc_querycap vcam_v4l2_querycap, // 实现 vidioc_querycap 用于支持 VIDIOC_QUERYCAP }; static const struct v4l2_file_operations vcam_fops {.owner THIS_MODULE,.unlocked_ioctl video_ioctl2, // 增加 ioctl 的支持 #ifdef CONFIG_COMPAT.compat_ioctl32 video_ioctl2, // 增加 ioctl 的支持 #endif.open vcam_open,.release vcam_release,};-43,6 70,7 static int __init vcam_init(void) {strscpy(vcam_vdev.name, Simple Virtual Camera, sizeof(vcam_vdev.name));vcam_vdev.v4l2_dev v4l2_dev; // 必须设置所属 v4l2_devvcam_vdev.fops vcam_fops; // 设置 fopsvcam_vdev.ioctl_ops vcam_v4l2_ioctl_ops;vcam_vdev.release video_device_release_empty; // 必须设置 relase 接口vcam_vdev.vfl_dir VFL_DIR_RX;vcam_vdev.device_caps V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_READWRITE;应用修改如下: Index: test/test.c--- test.orig/test.ctest/test.c-18,6 18,17 int main() {printf(camera open\n); // 1. 查询设备能力struct v4l2_capability cap;if (ioctl(fd, VIDIOC_QUERYCAP, cap) -1) {perror(查询设备能力失败);close(fd);return 1;}printf(card: %s\n, cap.card);printf(driver: %s\n, cap.driver);printf(bus_info: %s\n, cap.bus_info); // 关闭设备close(fd);printf(camera close\n);验证结果: console:/cache # ./mytest [ 888.990438] simple_vcam: device opened camera open card: Virtual Camera // 打印card 信息 driver: virtual_vcam // 打印 drvier 信息 bus_info: Simple Virtual Camera // 打印总线信息 camera close console:/cache # [ 888.996537] simple_vcam: device closed3. 支持申请 buffer 的能力 从前面的文章可以知道 camera 的 buffer 通过 vb2_queue 来管理, 因此我们需要添加对它的支持. Index: kernel/drivers/media/my_camera.c--- kernel.orig/drivers/media/my_camera.ckernel/drivers/media/my_camera.c-7,9 7,15 #include linux/videodev2.h#include media/v4l2-device.h#include media/v4l2-ioctl.h #include linux/spinlock.h #include media/videobuf2-core.h #include media/videobuf2-v4l2.h #include media/videobuf2-vmalloc.hstatic struct v4l2_device v4l2_dev;static struct video_device vcam_vdev; static struct vb2_queue vcam_queue; static struct mutex vcam_mutex;static int vcam_open(struct file *file) {printk(KERN_INFO simple_vcam: device opened\n);-21,7 27,7 static int vcam_release(struct file *filreturn 0;}-// 驱动需要实现 querycap 用于返回 video 信息 // 用于支持查询设备的能力// cap-driver: 设备的驱动信息// cap-card: 设备的名称// cap-bus_info: 设备的总线信息-39,8 45,41 static int vcam_v4l2_querycap(struct filreturn 0;}// 分配 buffer 时驱动的回调接口, 这里使用通用的 vb2_reqbufs 实现 // 虚拟摄像头这里就随便填写了. static int vcam_v4l2_reqbufs(struct file *file, void *fh, struct v4l2_requestbuffers *b) {int ret;if (b-type ! vcam_queue.type)return -EINVAL;printk(%s\n, __func__);ret vb2_reqbufs(vcam_queue, b);return ret ? ret : b-count; }// V4L2_BUF_TYPE_VIDEO_CAPTURE 必须实现这个接口用于返回支持的视频格式. static int vcam_v4l2_g_fmt_vid_cap(struct file *file, void *fh, struct v4l2_format *f) {f-fmt.pix.width 400;f-fmt.pix.height 800;f-fmt.pix.pixelformat V4L2_PIX_FMT_YUYV;f-fmt.pix.field V4L2_FIELD_NONE;f-fmt.pix.bytesperline f-fmt.pix.width * 2;f-fmt.pix.sizeimage f-fmt.pix.height * f-fmt.pix.bytesperline;f-fmt.pix.colorspace V4L2_COLORSPACE_SRGB;f-fmt.pix.priv 0;return 0; } const struct v4l2_ioctl_ops vcam_v4l2_ioctl_ops { - .vidioc_querycap vcam_v4l2_querycap, // 实现 vidioc_querycap 用于支持 VIDIOC_QUERYCAP.vidioc_querycap vcam_v4l2_querycap, // 用于返回设备信息.vidioc_reqbufs vcam_v4l2_reqbufs, // 用于支持分配 vb2_buffer.vidioc_g_fmt_vid_cap vcam_v4l2_g_fmt_vid_cap, // 用于支持返回驱动支持的视频格式};static const struct v4l2_file_operations vcam_fops {-53,6 92,48 static const struct v4l2_file_operations.release vcam_release,};// 用于设置 vb2_plane 的数量和大小 static int vcam_queue_setup(struct vb2_queue *vq,unsigned int *nbuffers, unsigned int *nplanes,unsigned int sizes[], struct device *alloc_devs[]) {printk(%s\n, __func__);*nplanes 1; // 设置数量sizes[0] 800*400; // 设置大小return 0; }static void vcam_buffer_queue(struct vb2_buffer *vb) {printk(%s\n, __func__); }static struct vb2_ops vcam_queue_qops {.queue_setup vcam_queue_setup, // 用于设置 vb2_plane 的数量和大小.buf_queue vcam_buffer_queue, // 必须实现 };// 初始化 vb2_queue int vcam_queue_init(struct vb2_queue *queue, enum v4l2_buf_type type,struct mutex *lock) {int ret;queue-type type;queue-io_modes VB2_MMAP | VB2_USERPTR | VB2_DMABUF;queue-ops vcam_queue_qops;queue-lock lock;queue-mem_ops vb2_vmalloc_memops;queue-timestamp_flags V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC| V4L2_BUF_FLAG_TSTAMP_SRC_EOF;ret vb2_queue_init(queue);if (ret)return ret;return 0; } static int __init vcam_init(void) {int ret; -83,6 164,9 static int __init vcam_init(void) {return ret;} // 初始化 vb2_queuevcam_queue_init(vcam_queue, V4L2_BUF_TYPE_VIDEO_CAPTURE, vcam_mutex); printk(KERN_INFO simple_vcam: registered video device /dev/video%d\n, vcam_vdev.minor);return 0;}应用程序修改. Index: test/test.c--- test.orig/test.ctest/test.c-29,6 29,19 int main() {printf(driver: %s\n, cap.driver);printf(bus_info: %s\n, cap.bus_info); // 2. 申请缓冲区struct v4l2_requestbuffers req;req.count 3;req.type V4L2_BUF_TYPE_VIDEO_CAPTURE;req.memory V4L2_MEMORY_MMAP;if (ioctl(fd, VIDIOC_REQBUFS, req) -1) {perror(申请缓冲区失败);close(fd);return 1;}printf(alloc 3 buffer\n); // 关闭设备close(fd);printf(camera close\n);验证结果: console:/cache # ./mytest [ 785.535608] simple_vcam: device opened camera open card: Virtual Camera driver: virtual_vcam bus_info: Simple Virtual Camera alloc 3 buffer camera close [ 785.541709] vcam_v4l2_reqbufs [ 785.541882] vcam_queue_setup [ 785.543306] simple_vcam: device closed4. 支持查询缓冲区信息 这个驱动需要添加对 vidioc_querybuf 的支持. Index: kernel/drivers/media/my_camera.c--- kernel.orig/drivers/media/my_camera.ckernel/drivers/media/my_camera.c-76,10 76,17 static int vcam_v4l2_g_fmt_vid_cap(strucreturn 0;}// 添加驱动用于支持查询缓冲区信息的功能 static int vcam_v4l2_querybuf(struct file *file, void *fh, struct v4l2_buffer *b) {return vb2_querybuf(vcam_queue, b); } const struct v4l2_ioctl_ops vcam_v4l2_ioctl_ops {.vidioc_querycap vcam_v4l2_querycap, // 用于返回设备信息.vidioc_reqbufs vcam_v4l2_reqbufs, // 用于支持分配 vb2_buffer.vidioc_g_fmt_vid_cap vcam_v4l2_g_fmt_vid_cap, // 用于支持返回驱动支持的视频格式.vidioc_querybuf vcam_v4l2_querybuf, // 添加驱动用于支持查询缓冲区信息的功能};static const struct v4l2_file_operations vcam_fops {应用程序 Index: test/test.c--- test.orig/test.ctest/test.c-42,6 42,18 int main() {printf(alloc 3 buffer\n); // 3. 查询缓冲区信息struct v4l2_buffer buf;buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE;buf.memory V4L2_MEMORY_MMAP;buf.index 0;if (ioctl(fd, VIDIOC_QUERYBUF, buf) -1) {perror(查询缓冲区信息失败);close(fd);return 1;}printf(缓冲区大小: %u bytes\n, buf.length); // 关闭设备close(fd);printf(camera close\n);验证结果 console:/cache # ./mytest [ 182.823177] simple_vcam: device opened camera open card: Virtual Camera driver: virtual_vcam bus_info: Simple Virtual Camera 缓冲区大小: 320000 bytes // 计算一下 800x400 320000 alloc 3 buffer camera close [ 182.829223] vcam_v4l2_reqbufs [ 182.829240] vcam_queue_setup [ 182.833253] simple_vcam: device closed5. 支持 mmap 功能 Index: kernel/drivers/media/my_camera.c--- kernel.orig/drivers/media/my_camera.ckernel/drivers/media/my_camera.c-89,13 89,20 const struct v4l2_ioctl_ops vcam_v4l2_io.vidioc_querybuf vcam_v4l2_querybuf, // 添加驱动用于支持查询缓冲区信息的功能};// 添加对 mmap 的支持 static int vcam_v4l2_mmap(struct file *file, struct vm_area_struct *vma) {return vb2_mmap(vcam_queue, vma); } static const struct v4l2_file_operations vcam_fops {.owner THIS_MODULE,.open vcam_open,.unlocked_ioctl video_ioctl2, // 增加 ioctl 的支持#ifdef CONFIG_COMPAT.compat_ioctl32 video_ioctl2, // 增加 ioctl 的支持#endif - .open vcam_open,.mmap vcam_v4l2_mmap, // 添加对 mmap 的支持.release vcam_release,};应用程序: Index: test/test.c--- test.orig/test.ctest/test.c-5,6 5,7 #include sys/ioctl.h#include linux/videodev2.h#include errno.h #include sys/mman.hint main() {int fd;-54,6 55,16 int main() {}printf(缓冲区大小: %u bytes\n, buf.length); // 4. mmap 映射内存void *buffer_start;buffer_start mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);if (buffer_start MAP_FAILED) {perror(mmap);exit(EXIT_FAILURE);}printf(mmap camera bufer %p\n, buffer_start); // 关闭设备close(fd);printf(camera close\n);验证结果: console:/cache # ./mytest [ 55.868859] simple_vcam: device opened camera open card: Virtual Camera driver: virtual_vcam bus_info: Simple Virtual Camera alloc 3 buffer 缓冲区大小: 320000 bytes mmap camera bufer 0x78780ac000 // 映射内存 camera close [ 55.874932] vcam_v4l2_reqbufs [ 55.874953] vcam_queue_setup [ 55.884138] simple_vcam: device closed6. 支持将队列放入缓冲区 我们在将 buffer 放入缓冲区前, 先给他写入 “hello vcam this is user” 然后在内核中打印出我们的修改. Index: kernel/drivers/media/my_camera.c--- kernel.orig/drivers/media/my_camera.ckernel/drivers/media/my_camera.c-16,6 16,8 static struct v4l2_device v4l2_dev;static struct video_device vcam_vdev;static struct vb2_queue vcam_queue;static struct mutex vcam_mutex; static void *vcam_mem; static unsigned int length;static int vcam_open(struct file *file) {printk(KERN_INFO simple_vcam: device opened\n);-87,6 89,7 const struct v4l2_ioctl_ops vcam_v4l2_io.vidioc_reqbufs vcam_v4l2_reqbufs, // 用于支持分配 vb2_buffer.vidioc_g_fmt_vid_cap vcam_v4l2_g_fmt_vid_cap, // 用于支持返回驱动支持的视频格式.vidioc_querybuf vcam_v4l2_querybuf, // 添加驱动用于支持查询缓冲区信息的功能.vidioc_qbuf vb2_ioctl_qbuf, // 用于支持将缓冲区放入队列};// 添加对 mmap 的支持-122,9 125,23 static void vcam_buffer_queue(struct vb2printk(%s\n, __func__);} // 获取前面分配的 buffer 的虚拟地址 // 打印出用户的修改, 这里应该能打印 hello vcam this is user static int vcam_buffer_prepare(struct vb2_buffer *vb) {vcam_mem vb2_plane_vaddr(vb, 0);length vb2_plane_size(vb, 0);printk(vcam_mem: %s, length:%d\n, (char*)vcam_mem, length);return 0; } static struct vb2_ops vcam_queue_qops {.queue_setup vcam_queue_setup, // 用于设置 vb2_plane 的数量和大小.buf_queue vcam_buffer_queue, // 必须实现.buf_prepare vcam_buffer_prepare, // 用于获取前面分配的 buffer 的虚拟地址};// 初始化 vb2_queue-179,6 196,7 static int __init vcam_init(void) {}// 初始化 vb2_queuevcam_vdev.queue vcam_queue; // vb2_ioctl_qbuf 需要把 queue 绑定到 video_devicevcam_queue_init(vcam_queue, V4L2_BUF_TYPE_VIDEO_CAPTURE, vcam_mutex);printk(KERN_INFO simple_vcam: registered video device /dev/video%d\n, vcam_vdev.minor);应用程序修改 Index: test/test.c--- test.orig/test.ctest/test.c-6,6 6,7 #include linux/videodev2.h#include errno.h#include sys/mman.h #include string.hint main() {int fd;-65,6 66,18 int main() {printf(mmap camera bufer %p\n, buffer_start); // 往映射内存中写入数据const char *msg hello vcam this is user;memcpy(buffer_start, msg, strlen(msg) 1);printf(write %s to kernel\n, (char*)buffer_start);if (ioctl(fd, VIDIOC_QBUF, buf) -1) {perror(将缓冲区放入队列失败);close(fd);return 1;} // 关闭设备close(fd);printf(camera close\n)验证结果: console:/cache # ./mytest [ 33.442391] simple_vcam: decvaimceer ao poepneendcard: Virtual Camera driver: virtual_vcam bus_info: Simple Virtual Camera alloc 3 buffer 缓冲区大小: 320000 bytes mmap camera bufer 0x7ef2c21000 write hello vcam this is user to kernel // 应用打印 camera close [ 33.448609] vcam_queue_setup [ 33.458952] vcam_mem: hello vcam this is user, length:320000 // 在内核中打印出应用写入的数据. [ 33.461500] simple_vcam: device closed7. 添加启动视频流功能 添加启动视频流的功能, 并且在启动的时候, 模拟 sensor 处理完视频并提交这帧. 模拟的方式很简单就是往 mem 中写入 “hello vcam this is kernel” Index: kernel/drivers/media/my_camera.c--- kernel.orig/drivers/media/my_camera.ckernel/drivers/media/my_camera.c-90,6 90,7 const struct v4l2_ioctl_ops vcam_v4l2_io.vidioc_g_fmt_vid_cap vcam_v4l2_g_fmt_vid_cap, // 用于支持返回驱动支持的视频格式.vidioc_querybuf vcam_v4l2_querybuf, // 添加驱动用于支持查询缓冲区信息的功能.vidioc_qbuf vb2_ioctl_qbuf, // 用于支持将缓冲区放入队列.vidioc_streamon vb2_ioctl_streamon, // 使用标准的 vb2_ioctl_streamon 接口};// 添加对 mmap 的支持-138,10 139,36 static int vcam_buffer_prepare(struct vbreturn 0;}// 直接在 steram on 中 模拟数据采集完成 static int vcam_start_streaming(struct vb2_queue *vq, unsigned int count) {struct vb2_buffer *vb;void *mem;if (list_empty(vq-queued_list)) {printk(vcam: queued_list is empty, cannot start streaming\n);return 0;}vb list_first_entry(vq-queued_list, struct vb2_buffer, queued_entry);// 模拟采集过程, 这里直接修改 buffer 内容mem vb2_plane_vaddr(vb, 0);memcpy(mem, hello vcam this is kernel, strlen(hello vcam this is kernel) 1);// 采集完成将 buffer 放到 vb2_queue-done_list 链表vb2_buffer_done(vb, VB2_BUF_STATE_DONE); // 将 buffer 放到完成链表printk(%s change mem to %s\n, __func__, (char*)mem);return 0; } static struct vb2_ops vcam_queue_qops {.queue_setup vcam_queue_setup, // 用于设置 vb2_plane 的数量和大小.buf_queue vcam_buffer_queue, // 必须实现.buf_prepare vcam_buffer_prepare, // 用于获取前面分配的 buffer 的虚拟地址.start_streaming vcam_start_streaming, // vb2_ioctl_streamon 的后续接口, 这个是真正的 stream on 接口};应用修改: Index: test/test.c--- test.orig/test.ctest/test.c-72,12 72,22 int main() {printf(write %s to kernel\n, (char*)buffer_start); // 5. 将缓冲区放入队列if (ioctl(fd, VIDIOC_QBUF, buf) -1) {perror(将缓冲区放入队列失败);close(fd);return 1;} // 6. 启动视频流if (ioctl(fd, VIDIOC_STREAMON, buf.type) -1) {perror(启动视频流失败);close(fd);return 1;}printf(vcam stream on\n); // 关闭设备close(fd);printf(camera close\n);验证结果: console:/cache # ./mytest [ 51.017063] simple_vcam: device opened camera open card: Virtual Camera driver: virtual_vcam bus_info: Simple Virtual Camera alloc 3 buffer 缓冲区大小: 320000 bytes mmap camera bufer 0x7a81864000 write hello vcam this is user to kernel vcam stream on // 启动摄像头 camera close [ 51.023179] vcam_queue_setup [ 51.033537] vcam_mem: hello vcam this is user, length:320000 [ 51.033591] vcam_start_streaming change mem to hello vcam this is kernel // 将 mem 的内容该成 hello vcam this is kernel [ 51.037391] simple_vcam: device closed 8. 从队列中取出缓冲 Index: kernel/drivers/media/my_camera.c--- kernel.orig/drivers/media/my_camera.ckernel/drivers/media/my_camera.c-91,6 91,7 const struct v4l2_ioctl_ops vcam_v4l2_io.vidioc_querybuf vcam_v4l2_querybuf, // 添加驱动用于支持查询缓冲区信息的功能.vidioc_qbuf vb2_ioctl_qbuf, // 用于支持将缓冲区放入队列.vidioc_streamon vb2_ioctl_streamon, // 使用标准的 vb2_ioctl_streamon 接口.vidioc_dqbuf vb2_ioctl_dqbuf, // 使用标准的 vb2_ioctl_dqbuf 接口};// 添加对 mmap 的支持-169,6 170,8 static struct vb2_ops vcam_queue_qops .buf_queue vcam_buffer_queue, // 必须实现.buf_prepare vcam_buffer_prepare, // 用于获取前面分配的 buffer 的虚拟地址.start_streaming vcam_start_streaming, // vb2_ioctl_streamon 的后续接口, 这个是真正的 stream on 接口.wait_prepare vb2_ops_wait_prepare, // 使用标准接口加锁.wait_finish vb2_ops_wait_finish, // 使用标准接口解锁};// 初始化 vb2_queue应用修改 Index: test/test.c--- test.orig/test.ctest/test.c-88,6 88,14 int main() {printf(vcam stream on\n); if (ioctl(fd, VIDIOC_DQBUF, buf) -1) {perror(从队列中取出缓冲区失败);close(fd);return 1;}printf(%s\n, (char*)buffer_start); // 关闭设备close(fd);printf(camera close\n);验证结果: console:/cache # ./mytest [ 99.390015] simple_vcam: device opened camera open card: Virtual Camera driver: virtual_vcam bus_info: Simple Virtual Camera alloc 3 buffer 缓冲区大小: 320000 bytes mmap camera bufer 0x741ca9f000 write hello vcam this is user to kernel vcam stream on hello vcam this is kernel // 内核读出处理过后的 buffer camera close [ 99.396111] vcam_queue_setup [ 99.406462] vcam_mem: hello vcam this is user, length:320000 [ 99.406512] vcam_start_streaming change mem to hello vcam this is kernel // 修改 buffer [ 99.412631] simple_vcam: device closed三、/dev/subdevX /dev/v4l-subd1evX 设备, 用于管理和配资子设备, sensor 的寄存器初始化等操作、 isp (Image Signal Processor)、视频编码器/解码器、桥接芯片(bridge). 他和 video 设备配合使用. // 可以是一对一的 摄像头设备(/dev/videoX)│└── subdevX (sensor)// 也可以是一对多的 摄像头设备(/dev/videoX)│├── subdev0 (Sensor传感器)├── subdev1 (ISP处理芯片)└── subdev2 (镜头控制器VCM模块)/dev/v4l-subd1evX 设备本质上也是通过一个字符设备的一系列 ioctl 来控制的. 为什么要单独抽取出来, 而不直接放到流处理部分中呢? 这是为了方便驱动的移植, 因为 /dev/video 流处理部分是通用的, 所有摄像头都会用到. 为减少代码冗余, 就将与摄像头控制器强耦合的部分单独抽取出来, 这部分由芯片厂家实现. 而 sensor 部分则通过 subdev 独立实现, 这样移植新 sensor 时, 只需更换对应的 subdev 即可. 整体框架如下图所示: 从上图中也可以看出 /dev/videox 和 /dev/subdevx 由 v4l2_device 统一管理. 注: 图中只画了一个 subdev 是因为图片大小限制, 一个vdieo 右边可以挂多个subdev. 1. subdev 的接口 注册的接口有两个. 第一个接口 v4l2_device_register_subdev 接口很简单, 用于将 subdev-list 链接到所属的 v4l2_dev-subdevs. 第二个接口 v4l2_device_register_subdev_nodes 接口的功能如下. 遍历 v4l2_dev-subdevs 上的 subdev 为每一个没有注册的设备创建一个 video_device 并注册进内核.设置对应的 video_device 的 fops 为 v4l2_subdev_fops, ioctl 的不同就是 /dev/subdev 设备 和 /dev/video 设备的核心差异点. const struct v4l2_file_operations v4l2_subdev_fops {.owner THIS_MODULE,.open subdev_open,.unlocked_ioctl subdev_ioctl, // 对于 video 设备这里是 video_ioctl2 #ifdef CONFIG_COMPAT.compat_ioctl32 subdev_compat_ioctl32, #endif.release subdev_close,.poll subdev_poll, }; EXPORT_SYMBOL_GPL(v4l2_subdev_fops);如下所示为 gc5035 摄像头的驱动实现. 到这里我们就可以发现, subdev 的核心就是一堆回调函数. struct v4l2_subdev_ops {const struct v4l2_subdev_core_ops *core; // 核心接口上下电等const struct v4l2_subdev_tuner_ops *tuner;const struct v4l2_subdev_audio_ops *audio;const struct v4l2_subdev_video_ops *video; // 视频相关的接口, 启动停止视频流等const struct v4l2_subdev_vbi_ops *vbi;const struct v4l2_subdev_ir_ops *ir;const struct v4l2_subdev_sensor_ops *sensor;const struct v4l2_subdev_pad_ops *pad; // pad 相关接口支持的分变率等. };sensor 支持什么就添加什么接口, 然后通过 v4l2_subdev_call 调用该接口. 驱动可以直接通过 v4l2_subdev_call 调用 subdev 支持的接口. #define v4l2_subdev_call(sd, o, f, args...) \({ \int __result; \if (!(sd)) \__result -ENODEV; \else if (!((sd)-ops-o (sd)-ops-o-f)) \__result -ENOIOCTLCMD; \else \__result (sd)-ops-o-f((sd), ##args); \__result; \})例如我们想要调用上图中的 gc5035_s_stream 启动视频流接口. 我们先看驱动的实现. static const struct v4l2_subdev_video_ops gc5035_video_ops {.s_stream gc5035_s_stream, // 对应的 s_stream 接口 };static const struct v4l2_subdev_ops gc5035_subdev_ops {.vido gc5035_core_ops, // 对应的 video 接口 };于是我们就可以使用以下接口调用 gc5035_s_stream v4l2_subdev_call(gc5035_subdev, video, s_stream, 1); // 传入 1 启动视频流subdev 总体上来说比较简单 首先通过 v4l2_device_register_subdev 添加到所属的 v4l2_dev然后使用 v4l2_device_register_subdev_nodes 注册 v4l2_dev 上的所有 subdev.最后通过 v4l2_subdev_call 调用 subdev 提供的接口. 2. 实现简单的 subdev 驱动 假设我们有以下摄像头硬件链路, 有两个sensor, sensor_A 和 sensor_B. 数据通过 csi 控制器后输入到 isp. 如果我们就需要实现三个 subdev 驱动, 分别用于描述 sensor_A 、sensorB、csi 控制器. 1) 注冊三個 subdev 注册 sensor_A 、sensorB、csi 控制器. 对应的 subdev, 本文的目的只是为了展示原理, 代码做错误处理, 以及只保留一个最简单的功能. 正常情况下分别对应三个驱动程序, 这里我就放到一起了. #include linux/module.h #include linux/kernel.h #include linux/fs.h #include linux/cdev.h #include linux/device.h #include linux/uaccess.h #include linux/videodev2.h #include media/v4l2-device.h #include media/v4l2-ioctl.h #include linux/spinlock.h #include media/videobuf2-core.h #include media/videobuf2-v4l2.h #include media/videobuf2-vmalloc.h #include media/media-entity.hextern struct v4l2_device my_v4l2_dev;static int sensor_s_stream(struct v4l2_subdev *sd, int on) {printk(%s: %s stream %s\n, __func__, sd-name, on ? on : off);return 0; }static const struct v4l2_subdev_video_ops sensor_video_ops {.s_stream sensor_s_stream, };static struct v4l2_subdev_ops sensor_ops {.video sensor_video_ops, };struct v4l2_subdev* my_sensor_register(const char *name) {struct v4l2_subdev *sd;int ret;sd kzalloc(sizeof(*sd), GFP_KERNEL);v4l2_subdev_init(sd, sensor_ops);sd-owner THIS_MODULE;sd-flags V4L2_SUBDEV_FL_HAS_DEVNODE;strscpy(sd-name, name, sizeof(sd-name));ret v4l2_device_register_subdev(my_v4l2_dev, sd);ret v4l2_device_register_subdev_nodes(my_v4l2_dev);return sd; }static int __init sensor_module_init(void) {struct v4l2_subdev *sensor_A;struct v4l2_subdev *sensor_B;struct v4l2_subdev *csi_control;sensor_A my_sensor_register(sensor_A);sensor_B my_sensor_register(sensor_B);csi_control my_sensor_register(csi_control);return 0; }module_init(sensor_module_init);MODULE_LICENSE(GPL); MODULE_AUTHOR(baron); MODULE_DESCRIPTION(V4L2 subdev example);2) 注册 isp 对应的 video 同样代码只做展示, 这这里我只保留了 stream_on 的功能. #include linux/module.h #include linux/kernel.h #include linux/fs.h #include linux/cdev.h #include linux/device.h #include linux/uaccess.h #include linux/videodev2.h #include media/v4l2-device.h #include media/v4l2-ioctl.h #include linux/spinlock.h #include media/videobuf2-core.h #include media/videobuf2-v4l2.h #include media/videobuf2-vmalloc.hstruct v4l2_device my_v4l2_dev; EXPORT_SYMBOL_GPL(my_v4l2_dev);static struct media_device media_dev;struct video_device isp_vdev; EXPORT_SYMBOL_GPL(isp_vdev);struct v4l2_subdev *get_remote_sensor(struct media_pad *local) {struct media_pad *remote;remote media_entity_remote_pad(local);return media_entity_to_v4l2_subdev(remote-entity); } EXPORT_SYMBOL_GPL(get_remote_sensor);static int isp_open(struct file *file) {printk(simple_isp: device opened\n);return 0; }static int isp_release(struct file *file) {printk(simple_isp: device closed\n);return 0; }static int isp_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type) {struct video_device *vdev video_devdata(file);struct v4l2_subdev * remote_sub;printk(%s\n, __func__);remote_sub get_remote_sensor(vdev-entity.pads[0]);v4l2_subdev_call(remote_sub, video, s_stream, 1);return 0; }const struct v4l2_ioctl_ops isp_v4l2_ioctl_ops {.vidioc_streamon isp_v4l2_streamon, };static const struct v4l2_file_operations isp_fops {.owner THIS_MODULE,.open isp_open,.unlocked_ioctl video_ioctl2, #ifdef CONFIG_COMPAT.compat_ioctl32 video_ioctl2, #endif.release isp_release, };static struct media_pad isp_pads[1] {{.flags MEDIA_PAD_FL_SINK,}, };static int __init isp_init(void) {int ret;printk(gsimple_isp: initializing\n);// 当第一个参数为 NULL 时必须设置 namesnprintf(my_v4l2_dev.name, sizeof(my_v4l2_dev.name), %s, isp v4l2 dev);ret v4l2_device_register(NULL, my_v4l2_dev);if (ret) {printk(simple_isp: v4l2_device_register failed\n);return ret;}// 初始化 isp_vdevstrscpy(isp_vdev.name, Simple Virtual Camera, sizeof(isp_vdev.name));isp_vdev.v4l2_dev my_v4l2_dev; // 必须设置所属 v4l2_devisp_vdev.fops isp_fops; // 设置 fopsisp_vdev.ioctl_ops isp_v4l2_ioctl_ops;isp_vdev.release video_device_release_empty; // 必须设置 relase 接口isp_vdev.vfl_dir VFL_DIR_RX;isp_vdev.device_caps V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_READWRITE;isp_vdev.entity.function MEDIA_ENT_F_CAM_SENSOR;ret media_entity_pads_init(isp_vdev.entity, 1, isp_pads);my_v4l2_dev.mdev media_dev;// 这个要在 video_register_device 前面snprintf(media_dev.model, sizeof(media_dev.model), My Virtual Media Device);snprintf(media_dev.driver_name, sizeof(media_dev.driver_name), My Virtual driver);snprintf(media_dev.serial, sizeof(media_dev.serial), My Virtual serial);snprintf(media_dev.bus_info, sizeof(media_dev.bus_info), My Virtual bus_info);media_device_init(media_dev);ret media_device_register(media_dev);if (ret) {pr_err(media_device_register failed\n);return ret;}// 注册 video device.ret video_register_device(isp_vdev, VFL_TYPE_GRABBER, -1);if (ret) {printk(simple_isp: video_register_device failed\n);return ret;}printk(gsimple_isp: registered video device /dev/video%d\n, isp_vdev.minor);return 0; }module_init(isp_init);MODULE_LICENSE(GPL); MODULE_AUTHOR(baron); MODULE_DESCRIPTION(Simple virtual camera driver example);注册完之后可以在系统中找到这些设备. console:/ # cat /sys/class/video4linux/v4l-subdev0/name sensor_A // sensor_A 设备 console:/ # cat /sys/class/video4linux/v4l-subdev1/name sensor_B // sensor_B 设备 console:/ # cat /sys/class/video4linux/v4l-subdev2/name isp conrtol // sensor_C 设备 console:/ # cat /sys/class/video4linux/video9/name Simple Virtual Camera四、media controller 前面我们模拟了一个简单的摄像头链路, 由sensor_A 、sensorB、csi 控制器 以及 isp video. 组成, 我们为他们创建了对应的设备. 现在引入一个问题: 只有一个 csi 控制器, 但是有两个 sensor 输入, 我们希望组成如下图的链路状态我们应该怎么实现. 当然我们可以在驱动写死, 如果我们一开始就知道硬件链接的话. 然而实际芯片的链路往往更加复杂, 可能存在多个csi控制器, isp 处理后有多个输出. 有没有一种方法可以动态的链接这些链路. 于是 v4l2 中的 media controller(MC) 框架闪亮登场. media controller(MC) 允许用户空间获取 v4l2_device 下的硬件设备信息, 以及对他们进行动态的管理. 1. media controller 的核心组件 media controller(MC)的核心组件有三个 entity、pad 以及 link 组成.这些组件由 media_deivce 进行统一的管理, 如下图所示. media controller(MC) 的本质也是一个字符设备, 提供了一组 ioctl 对 entity 等组件进行管理. 1) media_device media_device 向提供一个 cdev 字符设备, 用于用户空间与内核空间的 entity、pad、link 组件进行交互和管理. 它包括流媒体设备的整体描述, 以下描述字段是需要提供的. 字段作用media_devce.model[32]标识设备的硬件型号名称 (如USB Camera Model X)media_devce.driver_name[32]表示控制该设备的内核驱动名称(如uvcvideo)media_devce.serial[40]存储设备的唯一序列号用于区分同型号的不同设备。media_devce.bus_info[32]描述设备的总线连接信息(如usb-3-2.4),指明设备的物理连接位置media_device 通过 media_device_init 接口初始化然后通过 media_device_register 接口进行注册. 下面伪代码展示了注册流程, 注意 media_device 由 v4l2_dev 进行管理必须设置所属的 v4l2_dev. v4l2_dev.mdev media_dev; // media device 由 v4l2_dev 进行管理必须设置所属的 v4l2_dev// 设置 media device 的基本信息snprintf(media_dev.model, sizeof(media_dev.model), My Virtual Media Device);snprintf(media_dev.driver_name, sizeof(media_dev.driver_name), My Virtual driver);snprintf(media_dev.serial, sizeof(media_dev.serial), My Virtual serial);snprintf(media_dev.bus_info, sizeof(media_dev.bus_info), My Virtual bus_info);media_device_init(media_dev); // 初始化ret media_device_register(media_dev); // 注册2) entity 组件 需要链接的对象,比如我们的sensor_A 、sensorB、csi 控制器以及 isp vido 模块.当我们需要它作为链接对象时, 我们只需要内嵌 entity 就行了. subdev 和 video_device 已经帮我们嵌入了这个结构. 我们只需要在 kernel 中配置 CONFIG_MEDIA_CONTROLLER 这个宏就行了. struct v4l2_subdev { #if defined(CONFIG_MEDIA_CONTROLLER)struct media_entity entity; // 内嵌 entity 用于支持 media controller #endif // ...... }struct video_device { #if defined(CONFIG_MEDIA_CONTROLLER)struct media_entity entity; // 内嵌 entity 用于支持 media controllerstruct media_intf_devnode *intf_devnode;struct media_pipeline pipe; #endif // ......通过接口 media_device_register_entity 进行注册, 同样的对于 subdev 和 video_device 内核已经提供了对它的支持. // subdev 的支持 v4l2_device_register_subdev() --#if defined(CONFIG_MEDIA_CONTROLLER) // 通过宏 CONFIG_MEDIA_CONTROLLER 控制media_device_register_entity()--#endif// video device 的支持 __video_register_device() --video_register_media_controller() --#if defined(CONFIG_MEDIA_CONTROLLER) // 通过宏 CONFIG_MEDIA_CONTROLLER 控制ret media_device_register_entity(vdev-v4l2_dev-mdev, vdev-entity);#endifentity 通过 function 用于描述不同的 entity 的功能, 对于摄像头内核提供以下的 function #define MEDIA_ENT_F_CAM_SENSOR (MEDIA_ENT_F_OLD_SUBDEV_BASE 1) #define MEDIA_ENT_F_FLASH (MEDIA_ENT_F_OLD_SUBDEV_BASE 2) #define MEDIA_ENT_F_LENS (MEDIA_ENT_F_OLD_SUBDEV_BASE 3) #define MEDIA_ENT_F_TUNER (MEDIA_ENT_F_OLD_SUBDEV_BASE 5)在注册前我们需要设置 function, 如下面伪代码 static struct v4l2_subdev sensorA_sd; int xxx_init(void) {// 设置 MEDIA_ENT_F_CAM_SENSOR 摄像头常用的 functionsensorA_sd.entity.function MEDIA_ENT_F_CAM_SENSOR;// 注冊 v4l2_subdev 的时候就帮我们注册了 v4l2_devret v4l2_device_register_subdev(v4l2_dev, sensorA_sd); }3) pad 组件 entity 所描述的实体设备上面的端口. 每一个 entity 上面有一个到多个端口(pad), 端口的数量是固定好的存放与 entity-pads 数组中.数据结构如下所示 struct media_pad {struct media_gobj graph_obj; // 用于管理 padstruct media_entity *entity; // 所属 entityu16 index; // entity-pads 中对应的数组下标unsigned long flags; // pad 状态标志 };pad 中 index 表示所在的 entity-pads[index] 中的数组下标. pad 有三种标志 sink 接收端口、source 发送端口 和 must connect 必须连接. #define MEDIA_PAD_FL_SINK (1 0) // sink 接收端口 #define MEDIA_PAD_FL_SOURCE (1 1) // source 发送端口 #define MEDIA_PAD_FL_MUST_CONNECT (1 2) // connect 端口必须是连接状态通过 media_entity_pads_init 函数将 pads 添加到所属的 entity. 下面伪代码展示了添加过程. static struct v4l2_subdev sensorA_sd; // sensorA 只有 1 个 pad, 并且为发送端口. static struct media_pad sensorA_pads[1] {{.flags MEDIA_PAD_FL_SOURCE,}, };int xxx_init(void) {// ......ret media_entity_pads_init(sensorA_sd.entity, sizeof(sensorA_pads), sensorA_pads);// ...... }4) media_link 组件 media_link 用于连接 pad 和 pad. 如下图所示, link 有两个一个 link 一个 linkback, 设计两个 link 是为了方便链表的遍历. link 也是驱动提前注册的, 有哪些可以 link 在一起, 驱动需要提前规划好并且使用 media_create_pad_link 接口创建好这些 link. 用户空间则对这些 link 进行连接还是断开的控制.有了 link 之后就可以通过 pad 返回对面的设备. 下面代码展示了获取流程. struct v4l2_subdev *get_remote_sensor(struct media_pad *local) {struct media_pad *remote;remote media_entity_remote_pad(local);return media_entity_to_v4l2_subdev(remote-entity); } EXPORT_SYMBOL_GPL(get_remote_sensor);有了这个技巧, 我们就能在当前设备的接口里面通过 v4l2_subdev_call 调用远端设备的接口, 从而实现动态调用的功能. static int isp_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type) {struct video_device *vdev video_devdata(file);struct v4l2_subdev * remote_sub;remote_sub get_remote_sensor(vdev-entity.pads[0]);v4l2_subdev_call(remote_sub, video, s_stream, 1);return 0; }这样我们只需要在应用切换 link 就能实现动态切换了. 2. 添加 media controller 有了这些知识, 给我们的 sensor_A 、sensorB、csi 控制器以及 isp vido 模块 添加 media controller 吧. 这里的修改也是基于前面的 subdev 驱动修改的, 这里也只给出 patch. 1) 给 isp 添加 media entity 给我们的 /dev/video9 描述的 isp 添加 media entity, 按照前面的关系图, 我们需要添加一个 pad(sink) 用于接收 csi 的数据. Index: kernel/drivers/media/my_camera.c--- kernel.orig/drivers/media/my_camera.ckernel/drivers/media/my_camera.c-16,9 16,21 struct v4l2_device my_v4l2_dev;EXPORT_SYMBOL_GPL(my_v4l2_dev);static struct media_device media_dev; struct video_device isp_vdev;EXPORT_SYMBOL_GPL(isp_vdev);struct v4l2_subdev *get_remote_sensor(struct media_pad *local) {struct media_pad *remote;remote media_entity_remote_pad(local);return media_entity_to_v4l2_subdev(remote-entity); } EXPORT_SYMBOL_GPL(get_remote_sensor); static int isp_open(struct file *file){printk(simple_isp: device opened\n);-33,8 45,15 static int isp_release(struct file *filestatic int isp_v4l2_streamon(struct file *file, void *fh, enum v4l2_buf_type type){struct video_device *vdev video_devdata(file);struct v4l2_subdev * remote_sub; printk(%s\n, __func__); remote_sub get_remote_sensor(vdev-entity.pads[0]);v4l2_subdev_call(remote_sub, video, s_stream, 1); return 0;} -52,6 71,12 static const struct v4l2_file_operations.release isp_release,};static struct media_pad isp_pads[1] {{.flags MEDIA_PAD_FL_SINK,}, }; static int __init isp_init(void) {int ret; -74,6 99,22 static int __init isp_init(void) {isp_vdev.vfl_dir VFL_DIR_RX;isp_vdev.device_caps V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_READWRITE; isp_vdev.entity.function MEDIA_ENT_F_CAM_SENSOR;ret media_entity_pads_init(isp_vdev.entity, 1, isp_pads);my_v4l2_dev.mdev media_dev;// 这个要在 video_register_device 前面snprintf(media_dev.model, sizeof(media_dev.model), My Virtual Media Device);snprintf(media_dev.driver_name, sizeof(media_dev.driver_name), My Virtual driver);snprintf(media_dev.serial, sizeof(media_dev.serial), My Virtual serial);snprintf(media_dev.bus_info, sizeof(media_dev.bus_info), My Virtual bus_info);media_device_init(media_dev);ret media_device_register(media_dev);if (ret) {pr_err(media_device_register failed\n);return ret;} // 注册 video device.ret video_register_device(isp_vdev, VFL_TYPE_GRABBER, -1);if (ret) {2) 给 subdev 添加 media entity 给我们的 sensor_A 和 sensorB 分别添加一个 pad(source) 用于传送数据到 csi. 给 csi 控制器 添加两个 pad(source/link) 分别用于接收 sensor 的数据和发送数据给 isp. 最后给他们添加 link. ndex: kernel/drivers/media/my_sensorB.c--- kernel.orig/drivers/media/my_sensorB.ckernel/drivers/media/my_sensorB.c-15,11 15,21 extern struct v4l2_device my_v4l2_dev;extern struct v4l2_subdev *get_remote_sensor(struct media_pad *local); static int sensor_s_stream(struct v4l2_subdev *sd, int on){struct v4l2_subdev * remote_sub; printk(%s: %s stream %s\n, __func__, sd-name, on ? on : off); - - return 0;if(strcmp(sd-name, csi_control) 0){remote_sub get_remote_sensor(sd-entity.pads[0]);v4l2_subdev_call(remote_sub, video, s_stream, 1);}return 0;}static const struct v4l2_subdev_video_ops sensor_video_ops {-30,7 40,28 static struct v4l2_subdev_ops sensor_ops.video sensor_video_ops,};-struct v4l2_subdev* my_sensor_register(const char *name) static struct media_pad sensorA_pads[1] {{.flags MEDIA_PAD_FL_SOURCE,}, };static struct media_pad sensorB_pads[1] {{.flags MEDIA_PAD_FL_SOURCE,}, };static struct media_pad sensor_csi_pads[2] {{.flags MEDIA_PAD_FL_SINK,},{.flags MEDIA_PAD_FL_SOURCE,}, };struct v4l2_subdev* my_sensor_register(const char *name, unsigned int pad_num, struct media_pad *pads){struct v4l2_subdev *sd;int ret;-42,6 73,9 struct v4l2_subdev* my_sensor_register(csd-flags V4L2_SUBDEV_FL_HAS_DEVNODE;strscpy(sd-name, name, sizeof(sd-name)); sd-entity.function MEDIA_ENT_F_CAM_SENSOR;ret media_entity_pads_init(sd-entity, pad_num, pads); ret v4l2_device_register_subdev(my_v4l2_dev, sd);ret v4l2_device_register_subdev_nodes(my_v4l2_dev);-49,15 83,22 struct v4l2_subdev* my_sensor_register(creturn sd;}extern struct video_device isp_vdev; static int __init sensor_module_init(void){int ret;struct v4l2_subdev *sensor_A;struct v4l2_subdev *sensor_B;struct v4l2_subdev *csi_control;- sensor_A my_sensor_register(sensor_A); - sensor_B my_sensor_register(sensor_B); - csi_control my_sensor_register(csi_control);sensor_A my_sensor_register(sensor_A, 1, sensorA_pads);sensor_B my_sensor_register(sensor_B, 1, sensorB_pads);csi_control my_sensor_register(csi_control, 2, sensor_csi_pads);ret media_create_pad_link(sensor_A-entity, 0, csi_control-entity, 0, MEDIA_LNK_FL_ENABLED);ret media_create_pad_link(sensor_B-entity, 0, csi_control-entity, 0, 0);ret media_create_pad_link(csi_control-entity, 1, isp_vdev.entity, 0, MEDIA_LNK_FL_ENABLED);return 0;}3) 应用程序 #include stdio.h #include stdlib.h #include fcntl.h #include unistd.h #include sys/ioctl.h #include linux/videodev2.h #include errno.h #include sys/mman.h #include string.hint main() {int fd;// 打开设备 /dev/video9fd open(/dev/video9, O_RDWR);if (fd -1) {perror(打开设备失败);return -1;}printf(camera open\n);struct v4l2_buffer buf;buf.type V4L2_BUF_TYPE_VIDEO_CAPTURE;buf.memory V4L2_MEMORY_MMAP;buf.index 0;// 6. 启动视频流if (ioctl(fd, VIDIOC_STREAMON, buf.type) -1) {perror(启动视频流失败);close(fd);return 1;}printf(vcam stream on\n);// 关闭设备close(fd);printf(camera close\n);return 0; }4) 验证结果 验证需要用 media-ctl 工具, 命令 media-ctl -d /dev/media1 -p 用于打印基本信息. 如图所示默认 有两个关键的信息 entity id 和 pad id , 创建链接需要用到这两个 id. 驱动在注册的时候创建了一个默认的连接图中也可以看出来 sensor_A - csi 控制器 - isp vido 模块. 我们来验证一下这个链接. console:/cache # ./mytest [ 1224.326341] simple_isp: device opened camera open vcam stream on camera close console:/cache # [ 1224.326580] isp_v4l2_streamon [ 1224.326607] sensor_s_stream: csi_control stream on [ 1224.326626] sensor_s_stream: sensor_A stream on // 通过 isp 的 open 调用到 sensor_A [ 1224.327022] simple_isp: device closed 接下来我们用 media-ctl 修改连接为 sensorB - csi 控制器 - isp vido 模块. 130|console:/cache # media-ctl -d /dev/media1 --reset // 先清除原来的链接 // 前面的 9:0 就是 sensorB 的 entity id 和 pad id 130|console:/cache # media-ctl -d /dev/media1 -l 9:0 - 13:0 [1] // 创建 sensorB -- csi 控制器的连接 130|console:/cache # media-ctl -d /dev/media1 -l 13:1 - 1:0 [1] // 创建 csi 控制器 -- isp vido 模块的连接修改之后如图 再来编程验证, 发现 sensor 已经动态切换成 sensor_B 了 console:/cache # ./mytest [ 1728.598123] simple_isp: device opened camera open vcam stream on camera close console:/cache # [ 1728.598343] isp_v4l2_streamon [ 1728.598368] sensor_s_stream: csi_control stream on [ 1728.598387] sensor_s_stream: sensor_B stream on // 通过 isp 的 open 调用到 sensor_B [ 1728.598794] simple_isp: device closed五、设备树 ports 方法 linux 设备树中使用 port(端口) 和 endpoint(端点) 用来描述设备之间的连接关系. 这种连接关系主要用于多媒体, 音频, 视频等场景. 其实就是通过 port 和 endpoint 来找到 dts 描述的对方设备. 为了兼容 acpi linux 又整了一套 fwnode 接口封装了设备树相关的操作接口, 有两个关键的数据结构 fwnode_handle 和 fwnode_endpoint, 前者用于描述设备节点, 后者则用于记录端点信息. struct fwnode_endpoint {unsigned int port; // 端口号 idunsigned int id; // 端点 id, 用于区分同一端口下的多个端点const struct fwnode_handle *local_fwnode; };他们的接口实现在 drivers/base/property.c 和 drivers/of/property.c 中, 下面给出常见接口. 1. 常见接口 1) fwnode_graph_get_port_parent 获取设备节点写的 fwnode 的 prev 指向的端点的下一个端点, 如果 prev 为空则表示找第一个端点. 注意这里的 fwnode 必现为设备节点 device_node当 prev 为 NULL 时 device_node -- ports -- port -- endpiont当 prev 不为空时 prev -- parent_node -- endpoint struct fwnode_handle *fwnode_graph_get_next_endpoint(const struct fwnode_handle *fwnode, struct fwnode_handle *prev);2) fwnode_graph_get_remote_endpoint 获取 fwnode 描述的端点的远端端点, endpoint -- remote_endpoint -- endpoint. struct fwnode_handle *fwnode_graph_get_remote_endpoint(const struct fwnode_handle *fwnode);3) fwnode_graph_parse_endpoint 读取 fwnode 描述的 endpoint 的端点信息和端点信息. endpoint-port 设备树 port 的 reg 属性endpoint-id 设备树中的 endpoint 的 reg 属性 int fwnode_graph_parse_endpoint(const struct fwnode_handle *fwnode, struct fwnode_endpoint *endpoint);4) fwnode_graph_get_remote_port 获取 fwnode 描述的 endpoint 的远程 endpoint 端点的 port 节点. struct fwnode_handle *fwnode_graph_get_remote_port_parent(const struct fwnode_handle *fwnode);5) fwnode_graph_get_port_parent 获取 fwnode 描述的 endpoint 的远程 endpoint 端点的 device 设备节点. struct fwnode_handle *fwnode_graph_get_endpoint_by_id(const struct fwnode_handle *fwnode, u32 port, u32 endpoint, unsigned long flags);2. 编程验证 设备树的编写如下, 设计了两个设备, 然后通过上面的接口, 获取对方设备的信息. /{......device_A { // 设备 Acompatible device_A;status okay;ports {#address-cells 1;#size-cells 0;port11 {#address-cells 1;#size-cells 0;reg 11; // 设备 A 的端口号device_A_out: endpoint0 {reg 0; // 设备 A 的端点号remote-endpoint device_B_in;};};};};device_B { // 设备 Bcompatible device_B;status okay;ports {#address-cells 1;#size-cells 0;port12 {#address-cells 1; #size-cells 0;reg 12; // 设备 B 的端口号device_B_in: endpoint1 {reg 1; // 设备 B 的端点号remote-endpoint device_A_out;};};};};...... };1) 设备 A 的验证程序 #include linux/module.h #include linux/kernel.h #include linux/device.h #include linux/platform_device.h #include linux/firmware.h #include linux/fwnode.h #include linux/property.h #include linux/mod_devicetable.h #include linux/of.hstatic int device_A_probe(struct platform_device *pdev) {struct fwnode_handle *fwnode of_fwnode_handle(pdev-dev.of_node);struct fwnode_handle *endpoint;struct fwnode_handle *remote_endpoint;struct fwnode_endpoint ep;struct fwnode_handle *remote_port;struct fwnode_handle *remote_device;int ret;printk(Device A: probe begin\n);// 获取当前设备的端点endpoint fwnode_graph_get_next_endpoint(fwnode, NULL);if (endpoint){printk(endpiont name:%s\n, ((to_of_node(endpoint))-full_name));}// 解析端点信息ret fwnode_graph_parse_endpoint(endpoint, ep);if (!ret){// 打印端口号和端点 IDprintk(device A Port ID: %u\n, ep.port);printk(device A Endpoint ID: %u\n, ep.id);}// 获取当前端点的远程端点remote_endpoint fwnode_graph_get_remote_endpoint(endpoint);if (remote_endpoint) {printk(remote_endpiont name:%s\n, ((to_of_node(remote_endpoint))-full_name));}// 解析端点信息ret fwnode_graph_parse_endpoint(remote_endpoint, ep);if (!ret){// 打印远端端口和端点 IDprintk(device B Port ID: %u\n, ep.port);printk(device B Endpoint ID: %u\n, ep.id);}// 获取远端 device b 的 port 节点remote_port fwnode_graph_get_remote_port(endpoint);if (remote_port) {printk(remote port name:%s\n, ((to_of_node(remote_port))-full_name));}// 获取远端 device b 的设备节点remote_device fwnode_graph_get_port_parent(endpoint);if (remote_device){printk(remote device name:%s\n, ((to_of_node(remote_device))-full_name));}printk(Device A: probe end\n);return 0; }static const struct of_device_id device_A_of_match[] {{ .compatible device_A},{}, };struct platform_driver device_A_driver {.probe device_A_probe,.driver {.name device_A_driver,.of_match_table device_A_of_match,}, };static int __init device_A_init(void) {return platform_driver_register(device_A_driver); }static void __exit device_A_exit(void) {platform_driver_unregister(device_A_driver); }module_init(device_A_init); module_exit(device_A_exit);MODULE_LICENSE(GPL); MODULE_AUTHOR(baron); MODULE_DESCRIPTION(Device A Driver);验证结果: [ 1.206735] Device A: probe begin [ 1.206754] endpiont name:endpoint0 // 打印设备 A 的端点名 [ 1.206767] device A Port ID: 11 // 打印设备 A 的端口号 [ 1.206777] device A Endpoint ID: 0 // 打印设备 A 的端点号 [ 1.206791] remote_endpiont name:endpoint1 // 打印远程设备 B 的端点名 [ 1.206804] device B Port ID: 12 // 打印远程设备 B 的端口号 [ 1.206813] device B Endpoint ID: 1 // 打印远程设备 B 的端点号 [ 1.206820] remote port name:port12 // 打印远程设备 B 的端口名 [ 1.206830] remote device name:device_A // 打印远程设备名 [ 1.206840] Device A: probe end2) 设备 B 的验证程序 #include linux/module.h #include linux/kernel.h #include linux/device.h #include linux/platform_device.h #include linux/firmware.h #include linux/fwnode.h #include linux/property.h #include linux/mod_devicetable.h #include linux/of.hstatic int device_B_probe(struct platform_device *pdev) {struct fwnode_handle *fwnode of_fwnode_handle(pdev-dev.of_node);struct fwnode_handle *endpoint;struct fwnode_handle *remote_endpoint;struct fwnode_endpoint ep;struct fwnode_handle *remote_port;struct fwnode_handle *remote_device;int ret;printk(Device B: probe begin\n);// 获取当前设备的端点endpoint fwnode_graph_get_next_endpoint(fwnode, NULL);if (endpoint){printk(endpiont name:%s\n, ((to_of_node(endpoint))-full_name));}// 解析端点信息ret fwnode_graph_parse_endpoint(endpoint, ep);if (!ret){// 打印端口号和端点 IDprintk(device B Port ID: %u\n, ep.port);printk(device B Endpoint ID: %u\n, ep.id);}// 获取当前端点的远程端点remote_endpoint fwnode_graph_get_remote_endpoint(endpoint);if (remote_endpoint) {printk(remote_endpiont name:%s\n, ((to_of_node(remote_endpoint))-full_name));}// 解析端点信息ret fwnode_graph_parse_endpoint(remote_endpoint, ep);if (!ret){// 打印远端端口和端点 IDprintk(device A Port ID: %u\n, ep.port);printk(device A Endpoint ID: %u\n, ep.id);}// 获取远端 device a 的 port 节点remote_port fwnode_graph_get_remote_port(endpoint);if (remote_port) {printk(remote port name:%s\n, ((to_of_node(remote_port))-full_name));}// 获取远端 device a 的设备节点remote_device fwnode_graph_get_port_parent(endpoint);if (remote_device){printk(remote device name:%s\n, ((to_of_node(remote_device))-full_name));}printk(Device B: probe end\n);return 0; }static const struct of_device_id device_B_of_match[] {{ .compatible device_B},{}, };static struct platform_driver device_B_driver {.probe device_B_probe,.driver {.name device_B_driver,.of_match_table device_B_of_match,}, };static int __init device_B_init(void) {return platform_driver_register(device_B_driver); }static void __exit device_B_exit(void) {platform_driver_unregister(device_B_driver); }module_init(device_B_init); module_exit(device_B_exit);MODULE_LICENSE(GPL); MODULE_AUTHOR(baron); MODULE_DESCRIPTION(Device B Driver);验证结果: [ 1.207246] Device B: probe begin [ 1.207263] endpiont name:endpoint1 // 打印设备 B 的端点名 [ 1.207278] device B Port ID: 12 // 打印设备 B 的端口号 [ 1.207287] device B Endpoint ID: 1 // 打印设备 B 的端点号 [ 1.207303] remote_endpiont name:endpoint0 // 打印远程设备 A 的端点名 [ 1.207314] device A Port ID: 11 // 打印远程设备 A 的端号 [ 1.207323] device A Endpoint ID: 0 // 打印远程设备 A 的端点号 [ 1.207336] remote port name:port11 // 打印远程设备 A 的端口名 [ 1.207346] remote device name:device_B // 打印远程设备名 [ 1.207356] Device B: probe end3. 总结 通过这样的方式, 我们就能够通过设备树找到对方设备, 从而形成数据流, 因此也约定俗成数据要发给谁对应的 remote-endpoint 就设置为 xxx_in. 例如有两个摄像头 camera0 和 camera1, 两个摄像头都接到了 csi2_dphy 这个摄像头控制器, 然后通过 csi2_dphy 输出到 isp 处理. 就可以通过 dts 来实现设备的级联. 这样对于摄像头驱动来说, 只需要在 dts 节点里面创建一个 ports 然后把 remote-endpoint 指向我们需要输入的摄像头控制器节点的 endpoint 就可以完成模块的对接, 因为在摄像头控制器加载的时候, 会遍历设备树中 ports 节点上的摄像头设备, 这样就知道了接的是哪个摄像头了. 不需要的时候设置为 disable 或者直接注释掉就行了. 十分方便.
http://www.pierceye.com/news/766955/

相关文章:

  • 网站建设动漫网站模板怎么做的
  • 西安网站制作公司官网wordpress证书关闭
  • 北网站建设优化seo是什么意思
  • 中国seo网站长沙城乡建设网站
  • 没有相应营业执照怎么做网站重庆网站设计公司排名
  • 企业网站手机版商城网站有什么好处
  • 推荐一本学做网站的书温州阀门网站建设
  • 用户要承担暖气费的税吗太原优化排名推广
  • Wordpress外贸网站搭建公司宿迁建设网站
  • 学校网站建设的意义和应用山东淄博网站建设
  • 莱芜网站建设价格低网站vps
  • 长治做网站哪家好赣州注册公司
  • 网站开发从入门到精通做h5的网站哪个好
  • 免费公司网站如何建立设计个人网站好备案吗
  • 建网站和做微信哪个好在线识别图片百度识图
  • php网站开发如何实现删除功能大连大连建设工程信息网站
  • 表格模板免费下载网站wordpress 插件位置
  • wordpress小白能学会吗汕头做网站优化公司
  • 军队营房基础建设网站重庆做网站个人
  • 网站建设怎样中英文网站备案是空间备案还是域名备案
  • 陕西网站制作人力资源服务外包
  • 成都网站建设哪家售后好网站建设费可以计业务费吗
  • 做服装到哪个网站拿货品质好自己制作的网页别人如何访问
  • 榆林哪里做网站网页游戏网站那个好
  • 泰安口碑好的企业建站公司wordpress验证码无效
  • 圣矢网络重庆网站建设优化推广公司好听好记的网站域名
  • 如何做旅游小视频网站比较好的外贸公司
  • 图书馆建设投稿网站使用 ahrefs 进行 seo 分析
  • 校园网站建设 德育免费换ip软件
  • 排行网站模板凡科代理千万不要做