Linux 深入分析V4l2框架2(基于Linux6.6)---v4l2 device介绍
一、V4L2 框架
二、主设备
主设备使用 v4l2_device
进行抽象化表示。该例程使用了设备树来进行设备解析,使用平台驱动进行相应的驱动 probe。在文件 drivers/media/platform/omap3isp/isp.c
中有 isp_probe
函数,该函数的第一个参数定义即是 isp_device
,里面有提到大多数情况下,需要将 v4l2_device
结构体嵌入到一个更大的结构体里面使用,此为驱动自定义的结构体,那就是它了,那么它的定义如下:
struct isp_device {
struct v4l2_device v4l2_dev;
struct v4l2_async_notifier notifier;
struct media_device media_dev;
struct device *dev;
u32 revision;
/* platform HW resources */
unsigned int irq_num;
void __iomem *mmio_base[OMAP3_ISP_IOMEM_LAST];
unsigned long mmio_hist_base_phys;
struct regmap *syscon;
u32 syscon_offset;
u32 phy_type;
struct dma_iommu_mapping *mapping;
/* ISP Obj */
spinlock_t stat_lock; /* common lock for statistic drivers */
struct mutex isp_mutex; /* For handling ref_count field */
bool stop_failure;
struct media_entity_enum crashed;
int has_context;
int ref_count;
unsigned int autoidle;
#define ISP_CLK_CAM_ICK 0
#define ISP_CLK_CAM_MCLK 1
#define ISP_CLK_CSI2_FCK 2
#define ISP_CLK_L3_ICK 3
struct clk *clock[4];
struct isp_xclk xclks[2];
/* ISP modules */
struct ispstat isp_af;
struct ispstat isp_aewb;
struct ispstat isp_hist;
struct isp_res_device isp_res;
struct isp_prev_device isp_prev;
struct isp_ccdc_device isp_ccdc;
struct isp_csi2_device isp_csi2a;
struct isp_csi2_device isp_csi2c;
struct isp_ccp2_device isp_ccp2;
struct isp_csiphy isp_csiphy1;
struct isp_csiphy isp_csiphy2;
unsigned int sbl_resources;
unsigned int subclk_resources;
};
其中与本节有关的就属于是 struct v4l2_device v4l2_dev
这个了,它将作为串联管理整个 omap3isp 的管理者的存在。
struct isp_device
是驱动自定义的结构体,这个结构体可以看作是整个 omap3isp 的设备抽象化结构体,也就是说:它就代表了 omap3isp 这个大的设备。
2.1、注册 v4l2_device
在 isp.c 的 isp_probe
函数中有调用 isp_register_entities
函数,里面开头的内容大致如下:
drivers/media/platform/ti/omap3isp/isp.c
isp->v4l2_dev.mdev = &isp->media_dev;
ret = v4l2_device_register(isp->dev, &isp->v4l2_dev);
if (ret < 0) {
dev_err(isp->dev, "%s: V4L2 device registration failed (%d)\n",
__func__, ret);
goto done;
}
其实可以看到注册函数里面并没有把高设备加入到一个链表里面什么的,而是初始化结构体成员,比如子设备链表头初始化,增加 dev 的引用等。在一切子设备被串联起来之前首先要初始化注册 v4l2_device
这个总的设备。
三、v4l2_subdev
该结构体是抽象化的子设备,用于子设备管理之用。等几个操作。通常情况下的函数实例被定义在一个个的子设备驱动模块内部,注册子设备与注册子设备节点这两个函数的实体被定义在父设备模块内部。
drivers/media/platform/ti/omap3isp/isp.c
ret = isp_initialize_modules(isp);
if (ret < 0)
goto error_iommu;
ret = isp_register_entities(isp);
if (ret < 0)
goto error_modules;
下面首先看下 isp_initialize_modules
函数中的实现:
drivers/media/platform/ti/omap3isp/isp.c
static int isp_initialize_modules(struct isp_device *isp)
{
int ret;
ret = omap3isp_csiphy_init(isp);
ret = omap3isp_csi2_init(isp);
... ...
ret = omap3isp_h3a_aewb_init(isp);
ret = omap3isp_h3a_af_init(isp);
... ...
return 0;
... ...
再进去 omap3isp_xxxx_init
函数里面就可以看到有类似 v4l2_subdev_init
的函数调用了。
所以结构大体上是这样子的:
- 有一个输入设备 omap3isp,它的管理者列在一个单独的代码文件里面名为
isp.c
; - 定义一个自定义的抽象化结构体代表 omap3isp 这个设备,名为
isp_device
,并把v4l2_device
嵌入内部作为子设备管理工具; - 把子设备-类似 csi、preview、3a 等抽象化为一个个子设备,每个子设备一个代码文件,名为
ispxxx.c
,分别有自己的抽象化结构体,名为isp_xxx_dev
,内部嵌入了v4l2_subdev
作为子设备的抽象工具使用。同时实现自己的设备初始化函数,名为xxx_init_eneities
; - 在管理者
isp.c
的 probe 函数里面调用子设备的xxx_init_entities
,子设备初始化函数里面会做好v4l2_subdev
的初始化工作; - 管理者的 probe 函数里面注册
v4l2_device
,注册子设备,必要时注册子设备节点在用户空间生成/dev/nodeX
; - 可以通过
v4l2_device
来管理所有的子设备了,框架本身提供了很好用的管理方式与相关的回调函数、结构体成员等等。
四、设备的管理
4.1、主设备子设备互通
通过上面的步骤建立了连接之后怎么从主设备找到子设备呢?如何从子设备找到主设备?如何从 v4l2_device
到自定义的主设备抽象结构体?如何从 v4l2_subdev
到子设备自定义的结构体?
- 如何从主设备找到子设备
首先需要获取v4l2_device
结构体,然后可以使用list_for_each_entry
来对子设备进行遍历,其中子设备的结构体内部有一个name
成员,长度为32个字节,这个字段要求是整个v4l2_device
下属唯一的,所以要想找到某一个指定的子设备完全可以在遍历的时候对比子设备的name
字段看是不是自己想要找的。 - 如何从子设备找到主设备
v4l2_subdev
的结构体里面有一个v4l2_dev
的指针成员,该成员会在子设备被注册的时候指向v4l2_device
成员,注册函数为v4l2_device_register_subdev
。在该步骤执行完毕之后就可以通过获取子设备结构体内部的v4l2_dev
成员来获得主设备结构体。 - 如何从主设备到主设备实例化结构体
可以看到v4l2_device
内部并没有什么私有指针之类的东西,那怎么去找到主设备的实例化结构体呢,此时可以通过另一种偏门方法获取,比如在定义结构体的时候把v4l2_device
放在结构体成员的第一个,之后通过v4l2_subdev
获取到v4l2_device
之后就可以把其地址强制转换为主设备自定义的实例化结构体来实现访问。 - 如何从子设备到子设备实例化结构体
子设备内部有两个私有的指针:dev_priv
,host_priv
。前一个好理解也很好使用,使用的时候就调用v4l2_set_subdevdata
函数将dev_priv
指向子设备实例化结构体即可,然后就可以用v4l2_get_subdevdata
来从v4l2_subdev
获取到子设备结构体实例化的结构体数据了。后一个不是很好理解其用处,但是也可以通过v4l2_set_subdev_hostdata/v4l2_get_subdev_hostdata
来进行设置/获取,host 也即主控端,比如一个 camera sensor 的 SOC 端的控制器就可以作为主控端,再比如使用 I2C 进行通信的 camera sensor 的 SOC 端的 I2C 控制器就可以作为host_priv
,必要时通过 I2C 来控制子设备的行为。或者干脆把主设备实例化的结构体作为 host data 也可以。
4.2、主子设备信息交流
本节使用多个实际的用例来深入解释下各种信息交流方式与情景。比如:如何控制访问指定类型的子设备?子设备如何向主设备回返通知?
- 访问所有的 sensor 设备并关闭其数据流
- 子设备注册的时候应该要提供了相关的操作函数,那就是
v4l2_subdev_ops
这个结构体了,在此例中我们就仅仅设置其video
成员的s_stream
成员。 - 提供子设备组 id,也就是
v4l2_subdev
的grp_id
成员,此处我们设置为一个我自己假定的枚举类型(你只要保证这个枚举类型是整个v4l2_device
下属唯一的就行),我假定为OMAP3ISP_CAMSENSOR
。 - 初始化并注册子设备,就不再多说了,初始化以及注册的方式前面都有提到过了。
- 执行
v4l2_device_call_all(v4l2_device, OMAP3ISP_CAMSENSOR, video, s_stream, 0);
,此时会遍历挂在v4l2_device
名下的所有的OMAP3ISP_CAMSENSOR
组的子设备,调用其s_stream
的模块函数进行数据流的关闭。
- 子设备数据流关闭后向主设备回返通知
- 需要提供主设备的
notify
成员操作函数。 - 定义好
notification
的格式,比如我自己的定义,高8位表示哪个子设备,次8位表示哪种类型的操作(此处是 video 类型的 ops),再次8位表示具体的操作函数(s_stream),低8位表示操作值(0关闭)。 - 子设备调用
v4l2_subdev_notify
函数进行正式通知的发送,此时也可以带一些参数,只需要传递其地址就可以了,主子设备端商定好数据的格式即可。 - 主设备收到通知之后进行相关的操作。
五、交通枢纽 video_device
该结构体整合了数据流管理的终端模块功能,负责提供从内核空间到用户空间的数据交流,属于非常重要的一个功能了,必不可少的那种。
通常情况下所有的子设备都可以注册一个 video_device
结构体来在用户空间生成一个设备节点以供用户进行操作,但是区别在于只有负责真正传递视频数据的那个模块用得着注册 video 类型的设备节点名称(比如内核输入设备数据链的 DMA 数据终端),其它的使用 v4l-subdev 类型的就可以了。
video_device
只与 v4l2_device
进行绑定关联,通过后者这层关系可以访问到整个子设备网络的资源。
5.1、如何注册 video 类型节点
使用 video_register_device
配合 VFL_TYPE_GRABBER
参数进行注册,此时该函数执行完毕并返回的时候就可以在用户空间看到形如 /dev/videoX
的设备节点了。
注意需要提供其操作函数,类似下面的:
static struct v4l2_file_operations isp_video_fops = {
.owner = THIS_MODULE,
.unlocked_ioctl = video_ioctl2,
.open = isp_video_open,
.release = isp_video_release,
.poll = isp_video_poll,
.mmap = isp_video_mmap,
};
关于其成员如何实现本节不详细介绍,在后面的 videobuf2 一文中会进行详细介绍。
5.2、如何注册其它类型节点
对于 v4l2 输入设备来说,使用 v4l2_device_register_subdev_nodes
来进行批量的设备节点注册,它内部依然会调用 video_register_device
函数,只不过会使用 VFL_TYPE_SUBDEV
类型来代替上面 VFL_TYPE_GRABBER
,那么在用户空间生成的设备节点名称就是 v4l-subdevX
了。
这种情况下注册的设备节点的操作函数是在 v4l2-device.c
里面定义好的默认操作函数,它的定义如下:
drivers/media/v4l2-core/v4l2-subdev.c
const struct v4l2_file_operations v4l2_subdev_fops = {
.owner = THIS_MODULE,
.open = subdev_open,
.unlocked_ioctl = subdev_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl32 = subdev_compat_ioctl32,
#endif
.release = subdev_close,
.poll = subdev_poll,
};
六、举例应用
下面是一个使用 V4L2 接口的基本示例,展示了如何通过 video_device
打开视频设备、查询设备信息、配置视频流以及进行数据捕获。
1. 简单的 V4L2 视频设备应用示例
假设通过 V4L2 API 从视频设备(比如摄像头)捕获视频流。
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <string.h>
#include <errno.h>
#include <linux/videodev2.h>
#include <sys/mman.h>
#include <stdint.h>
#define VIDEO_DEVICE "/dev/video0"
int main() {
int fd = -1;
struct v4l2_capability cap;
struct v4l2_format fmt;
struct v4l2_buffer buf;
void *buffer;
int num_buffers = 1;
struct v4l2_requestbuffers reqbuf;
// 打开视频设备
fd = open(VIDEO_DEVICE, O_RDWR);
if (fd == -1) {
perror("Unable to open video device");
exit(1);
}
// 查询设备能力
if (ioctl(fd, VIDIOC_QUERYCAP, &cap) == -1) {
perror("VIDIOC_QUERYCAP failed");
close(fd);
exit(1);
}
printf("Driver: %s\n", cap.driver);
printf("Card: %s\n", cap.card);
printf("Bus info: %s\n", cap.bus_info);
printf("Capabilities: 0x%08x\n", cap.capabilities);
// 设置视频格式
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 640;
fmt.fmt.pix.height = 480;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_YUYV;
fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;
if (ioctl(fd, VIDIOC_S_FMT, &fmt) == -1) {
perror("VIDIOC_S_FMT failed");
close(fd);
exit(1);
}
// 请求缓冲区
memset(&reqbuf, 0, sizeof(reqbuf));
reqbuf.count = num_buffers;
reqbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
reqbuf.memory = V4L2_MEMORY_MMAP;
if (ioctl(fd, VIDIOC_REQBUFS, &reqbuf) == -1) {
perror("VIDIOC_REQBUFS failed");
close(fd);
exit(1);
}
// 映射缓冲区
memset(&buf, 0, sizeof(buf));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
buf.index = 0;
if (ioctl(fd, VIDIOC_QUERYBUF, &buf) == -1) {
perror("VIDIOC_QUERYBUF failed");
close(fd);
exit(1);
}
buffer = mmap(NULL, buf.length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, buf.m.offset);
if (buffer == MAP_FAILED) {
perror("mmap failed");
close(fd);
exit(1);
}
// 开始捕获
if (ioctl(fd, VIDIOC_STREAMON, &buf.type) == -1) {
perror("VIDIOC_STREAMON failed");
munmap(buffer, buf.length);
close(fd);
exit(1);
}
// 捕获一帧
if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {
perror("VIDIOC_QBUF failed");
munmap(buffer, buf.length);
close(fd);
exit(1);
}
// 等待并获取视频帧
if (ioctl(fd, VIDIOC_DQBUF, &buf) == -1) {
perror("VIDIOC_DQBUF failed");
munmap(buffer, buf.length);
close(fd);
exit(1);
}
// 处理捕获的视频帧(这里只是简单地打印出缓冲区地址)
printf("Captured frame at buffer address: %p\n", buffer);
// 停止视频流
if (ioctl(fd, VIDIOC_STREAMOFF, &buf.type) == -1) {
perror("VIDIOC_STREAMOFF failed");
}
// 释放资源
munmap(buffer, buf.length);
close(fd);
return 0;
}
代码说明:
-
打开设备: 使用
open()
打开视频设备文件(在这个示例中是/dev/video0
)。 -
查询设备能力: 使用
VIDIOC_QUERYCAP
获取视频设备的能力,如支持的视频格式、设备名称等。 -
设置视频格式: 通过
VIDIOC_S_FMT
设置捕获的视频格式(如分辨率、像素格式)。在这个示例中,我们将分辨率设置为640x480
,像素格式设置为V4L2_PIX_FMT_YUYV
。 -
请求缓冲区: 使用
VIDIOC_REQBUFS
请求缓冲区。这里设置为内存映射 (V4L2_MEMORY_MMAP
)。 -
映射缓冲区: 使用
mmap()
映射内存,分配缓冲区,用于存储捕获的帧数据。 -
启动视频流: 使用
VIDIOC_STREAMON
启动视频流。 -
捕获视频帧: 使用
VIDIOC_QBUF
将缓冲区放入队列,之后使用VIDIOC_DQBUF
获取捕获的帧数据。 -
停止视频流: 使用
VIDIOC_STREAMOFF
停止视频流。 -
资源清理: 使用
munmap()
释放映射的缓冲区,关闭视频设备。
2. 进一步的改进
-
处理视频帧: 在实际应用中,捕获到的视频数据可能需要进一步处理,比如保存为图像文件、传输或者进行图像分析。
-
多缓冲区支持: 代码中示例只使用了一个缓冲区。在高性能应用中,可能需要使用多个缓冲区进行循环队列管理,避免数据丢失。
-
错误处理: 在实际应用中,错误处理需要更全面,比如判断设备是否支持某些功能、内存映射是否成功等。
3. 使用 v4l-utils
测试设备
可以使用 v4l-utils
工具集来检查视频设备的配置和调试:
# 查看视频设备信息
v4l2-ctl --all
# 查看支持的视频格式
v4l2-ctl --list-formats
# 测试视频设备是否能正常工作
v4l2-ctl --device=/dev/video0 --stream-mmap --stream-count=100