2024年最全V4L2框架概述,2024-2024京东物联网嵌入式开发面试真题解析

img
img

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

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

如果你需要这些资料,可以戳这里获取

有些时候需要对驱动的所有设备进行迭代,这种情况通常发生在多个设备驱动使用同一个硬件设备的情况下,比如 ivtvfb 驱动就是个 framebuffer 驱动,它用到了 ivtv 这个硬件设备。可以使用以下方法来迭代所有的已注册设备:

static int callback(struct device \*dev, void \*p)
{
	struct v4l2\_device \*v4l2_dev = dev\_get\_drvdata(dev);

	/\* test if this device was inited \*/
	if (v4l2_dev == NULL)
		return 0;
	...
	return 0;
}

int iterate(void \*p)
{
	struct device\_driver \*drv;
	int err;

    /\* Find driver 'ivtv' on the PCI bus.
 \* pci\_bus\_type is a global. For USB busses use usb\_bus\_type.
 \*/
	drv = driver\_find("ivtv", &pci_bus_type);
	/\* iterate over all ivtv device instances \*/
	err = driver\_for\_each\_device(drv, NULL, p, callback);
	put\_driver(drv);
	return err;
}

有时候需要对设备实例进行计数以将设备实例映射到模块的全局数组里面,可以使用以下步骤来完成计数操作:

static atomic\_t drv_instance = ATOMIC\_INIT(0);

static int drv\_probe(struct pci\_dev \*pdev, const struct pci\_device\_id \*pci_id)
{
	...
	state->instance = atomic\_inc\_return(&drv_instance) - 1;
}

如果一个热拔插设备有很多个设备节点(比如一个USB摄像头可以产生多路视频输出,虽然它的视频源是一个),那么很难知道在什么时候才能够安全地卸载 v4l2_device 设备。基于以上问题, v4l2_device 引入了引用计数机制,当 video_register_device 函数被调用的时候,引用计数会加一,当 video_device 被释放的时候,引用计数会减一,直到 v4l2_device 的引用计数到0的时候,v4l2_devicerelease 回调函数就会被调用,可以在该回调函数里面做一些清理工作。当其它的设备(alsa,因为这个不属于 video 设备,所以也就不能使用上面的 video 函数进行计数的加减操作)节点被创建的时候,可以人为调用以下函数对引用计数进行增减操作:

	void v4l2_device_get(struct v4l2_device *v4l2_dev);
	int v4l2_device_put(struct v4l2_device *v4l2_dev);

需要注意的是,v4l2_device_register 函数将引用计数初始化为1,所以需要在 remove 或者 disconnect 回调方法里面调用 v4l2_device_put 来减少引用计数,否则引用计数将永远不会达到0。


v4l2_subdev 结构体

很多设备都需要与子设备进行交互,通常情况下子设备用于音视频的编解码以及混合处理,对于网络摄像机来说子设备就是 sensors 和 camera 控制器。通常情况下它们都是 I2C 设备,但也有例外。v4l2_subdev 结构体被用于子设备管理。

每一个子设备驱动都必须有一个 v4l2_subdev 结构体,这个结构体可以作为独立的简单子设备存在,也可以嵌入到更大的结构体(自定义的子设备结构体)里面。通常会有一个由内核设置的低层次结构体(i2c_client,也就是上面说的 i2c 设备),它包含了一些设备数据,要调用 v4l2_set_subdevdata 来设置子设备私有数据指针指向它,这样的话就可以很方便的从 subdev 找到相关的 I2C 设备数据(这个要编程实现的时候才能够了解它的用意)。另外也需要设置低级别结构的私有数据指针指向 v4l2_subdev 结构体,方便从低级别的结构体访问 v4l2_subdev 结构体,达到双向访问的目的,对于 i2c_client 来说,可以用 i2c_set_clientdata 函数来设置,其它的需使用与之相应的函数来完成设置。

桥驱动器需要存储每一个子设备的私有数据,v4l2_subdev 结构体提供了主机私有数据指针成员来实现此目的,使用以下函数可以对主机私有数据进行访问控制:

	v4l2\_get\_subdev\_hostdata();
	v4l2\_set\_subdev\_hostdata();

从桥驱动器的角度来看,我们加载子设备模块之后可以用某种方式获取子设备指针。对于 i2c 设备来说,调用 i2c_get_clientdata 函数即可完成,其它类型的设备也有与之相似的操作,在内核里面提供了不少的帮助函数来协助完成这部分工作,编程时可以多多使用。

每个 v4l2_subdev 结构体都包含有一些函数指针,指向驱动实现的回调函数,内核对这些回调函数进行了分类以避免出现定义了一个巨大的回调函数集,但是里面只有那么几个用得上的尴尬情况。最顶层的操作函数结构体内部包含指向各个不同类别操作函数结构体的指针成员,如下所示:

	struct v4l2_subdev_core_ops {
		int (*log_status)(struct v4l2_subdev *sd);
		int (*init)(struct v4l2_subdev *sd, u32 val);
		...
	};

	struct v4l2_subdev_tuner_ops {
		...
	};

	struct v4l2_subdev_audio_ops {
		...
	};

	struct v4l2_subdev_video_ops {
		...
	};

	struct v4l2_subdev_pad_ops {
		...
	};

	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;
	};

这部分的设计我个人觉得是非常实用的,linux 要想支持大量的设备的同时又要保持代码的精简就必须得这样去实现。core ops成员对于所有的子设备来说都是通用的,其余的成员不同的驱动会有选择的去使用,例如:video 设备就不需要支持 audio 这个 ops 成员。子设备驱动的初始化使用 v4l2_subdev_init 函数来完成(该函数只是初始化一些 v4l2_subdev 的成员变量,内容比较简单),在初始化之后需要设置子设备结构体的 nameowner 成员(如果是 i2c 设备的话,这个在 i2c helper 函数里面就会被设置)。该部分 ioctl 可以直接通过用户空间的 ioctl 命令访问到(前提是该子设备在用户空间生成了子设备节点,这样的话就可以操作子设备节点来进行 ioctl)。内核里面可以使用 v4l2_subdev_call 函数来对这些回调函数进行调用,这个在 pipeline 管理的时候十分受用。

如果需要与 media framework 进行集成,必须初始化 media_entity 结构体并将其嵌入到 v4l2_subdev 结构体里面,操作如下所示:

	struct media\_pad \*pads = &my_sd->pads;
	int err;

	err = media\_entity\_init(&sd->entity, npads, pads, 0);

其中 pads 结构体变量必须提前初始化,media_entityflagsnametypeops 成员需要设置。entity 的引用计数在子设备节点被打开/关闭的时候会自动地增减。在销毁子设备的时候需使用 media_entity_cleanup 函数对 entity 进行清理。如果子设备需要处理 video 数据,就需要实现 v4l2_subdev_video_ops 成员,如果要集成到 media_framework 里面,就必须要实现 v4l2_subdev_pad_ops 成员,此时使用 pad_ops 中与 format 有关的成员代替 v4l2_subdev_video_ops 中的相关成员。

子设备驱动需要设置 link_validation 成员来提供自己的 link validation 函数,该回调函数用来检查 pipeline 上面的所有的 link 是否有效(是否有效由自己来做决定),该回调函数在 media_entity_pipeline_start 函数里面被循环调用。如果该成员没有被设置,那么 v4l2_subdev_link_validate_default 将会作为默认的回调函数被使用,该函数确保 link 的 source pad 和 sink pad 的宽、高、media 总线像素码是一致的,否则就会返回错误。

有两种方法可以注册子设备(注意是设备,不是设备驱动,常用的方式是通过设备树来注册),第一种(旧的方法,比如使用 platform_device_register 来进行注册)是使用桥驱动去注册设备。这种情况下,桥驱动拥有连接到它的子设备的完整信息,并且知道何时去注册子设备,内部子设备通常属于这种情况。比如 SOC 内部的 video 数据处理单元,连接到 USB 或 SOC 的相机传感器。另一种情况是子设备必须异步地被注册到桥驱动上,比如基于设备树的系统,此时所有的子设备信息都独立于桥驱动器。使用这两种方法注册子设备的区别是 probing 的处理方式不同。也就是一种是设备信息结构体由驱动本身持有并注册,一种是设备信息结构体由设备树持有并注册。

设备驱动需要用 v4l2_device 信息来注册 v4l2_subdev ,如下所示:

	int err = v4l2\_device\_register\_subdev(v4l2_dev, sd);

如果子设备模块在注册之前消失的话,该操作就会失败,如果成功的话就会使得 subdev->dev 指向 v4l2_device。如果 v4l2_device 父设备的 mdev 成员不为空的话,子设备的 entity 就会自动地被注册到 mdev 指向的 media_device 里面。在子设备需要被卸载并且 sd->dev 变为NULL之后,使用如下函数来卸载子设备:

	v4l2\_device\_unregister\_subdev(sd);

如果子设备被注册到上层的 v4l2_device 父设备中,那么 v4l2_device_unregister 函数就会自动地把所有子设备卸载掉。但为了以防万一以及保持代码的风格统一,需要注册与卸载结对使用。可以用以下方式直接调用ops成员:err = sd->ops->core->g_std(sd, &norm); 使用下面的宏定义可以简化书写:err = v4l2_subdev_call(sd, core, g_std, &norm);该操作会检查 sd->dev 指针是否为空,如果是,返回 -ENODEV,同时如果 ops->core 或者 ops->core->g_std 为空,则返回 -ENOIOCTLCMD 。也可以通过以下函数调用来对 V4l2 下面挂载的所有子设备进行回调:

	v4l2\_device\_call\_all(v4l2_dev, 0, core, g_std, &norm);

该函数会跳过所有不支持该 ops 的子设备,并且所有的错误信息也被忽略,如果想捕获错误信息,可以使用下面的函数:

	err = v4l2_device_call_until_err(v4l2_dev, 0, core, g_std, &norm);

该函数的第二个参数如果为 0,则所有的子设备都会被访问,如果非 0,则指定组的子设备会被访问。

组ID使得桥驱动能够更加精确的去调用子设备操作函数,例如:在一个单板上面有很多个声卡,每个都能够改变音量,但是通常情况下只访问一个,这时就可以设置子设备的组 ID 为 AUDIO_CONTROLLER 并指定它的值,这时 v4l2_device_call_all 函数就会只去访问指定组的子设备,提高效率。

如果子设备需要向 v4l2_device 父设备发送事件通知的话,就可以调用 v4l2_subdev_notify 宏定义来回调 v4l2->notify 成员(前文有提到过)。

使用 v4l2_subdev 的优点是不包含任何底层硬件的信息,它是对底层硬件的一个抽象,因此一个驱动可能包含多个使用同一条 I2C 总线的子设备,也可能只包含一个使用 GPIO 管脚控制的子设备,只有在驱动设置的时候才有这些差别,而一旦子设备被注册之后,底层硬件对驱动来说就是完全透明的。

不清楚异步模式的用途
在异步模式下,子设备 probing 可以被独立地被调用以检查桥驱动是否可用,子设备驱动必须确认所有的 probing 请求是否成功,如果有任意一个请求条件没有满足,驱动就会返回 -EPROBE_DEFER 来继续下一次尝试,一旦所有的请求条件都被满足,子设备就需要调用 v4l2_async_register_subdev 函数来进行注册(用 v4l2_async_unregister_subdev 卸载)。桥驱动反过来得注册一个 notifier 对象(v4l2_async_notifier_register),该函数的第二个参数类型是 v4l2_async_notifier 类型的结构体,里面包含有一个指向指针数组的指针成员,指针数组每一个成员都指向 v4l2_async_subdev 类型结构体。v4l2 核心层会利用上述的异步子设备结构体描述符来进行子设备的匹配,如果成功匹配,.bound()notifier回调函数将会被调用,当所有的子设备全部被加载完毕之后,.complete() 回调函数就会被调用,子设备被移除的时候 .unbind() 函数就会被调用。

另外子设备还提供了一组内部操作函数,该内部函数的调用时机在下面有描述,原型如下所示:

struct v4l2\_subdev\_internal\_ops {
	int (\*registered)(struct v4l2\_subdev \*sd);
	void (\*unregistered)(struct v4l2\_subdev \*sd);
	int (\*open)(struct v4l2\_subdev \*sd, struct v4l2\_subdev\_fh \*fh);
	int (\*close)(struct v4l2\_subdev \*sd, struct v4l2\_subdev\_fh \*fh);
};

这些函数仅供 v4l2 framework 使用,驱动程序不应该显式的去调用这些回调

  • registered/unregister:在子设备被注册(v4l2_device_register_subdev)/反注册的时候被调用。
  • open/close:如果子设备在用户空间创建了设备节点,那么这两个函数就会在用户空间的设备节点被打开/关闭的时候调用到,主要是用来创建/关闭v4l2_fh以供v4l2_ctrl_handler等的使用。

v4l2子设备用户空间API

可以在 /dev 文件夹下创建 v4l-subdevX 设备节点以供用户直接操作子设备硬件。如果需要在用户空间创建设备节点的话,就需要在子设备节点注册之前设置 V4L2_SUBDEV_FL_HAS_DEVNODE 标志,然后调用 v4l2_device_register_subdev_nodes() 函数,就可以在用户空间创建设备节点,设备节点会在子设备卸载的时候自动地被销毁。

	VIDIOC_QUERYCTRL
	VIDIOC_QUERYMENU
	VIDIOC_G_CTRL
	VIDIOC_S_CTRL
	VIDIOC_G_EXT_CTRLS
	VIDIOC_S_EXT_CTRLS
	VIDIOC_TRY_EXT_CTRLS

上述 ioctls 可以通过设备节点访问,也可以直接在子设备驱动里面调用。

	VIDIOC_DQEVENT
	VIDIOC_SUBSCRIBE_EVENT
	VIDIOC_UNSUBSCRIBE_EVENT

要使用上述事件,就必须设置 v4l2_subdevV4L2_SUBDEV_USES_EVENTS 标志位,实现 core_opssubscribe 相关的回调函数,回调函数里面需要初始化 events,然后注册 v4l2_subdev。一些私有的 ioctls 可以在 v4l2_subdevops->core->ioctl 里面实现。

I2C子设备驱动

要想在 I2C 驱动里面添加 v4l2_subdev 支持,就需要把 v4l2_subdev 结构体嵌入到每个 I2C 实例结构体里面,有一些比较简单的 I2C 设备不需要自定义的状态结构体,此时只需要创建一个单独的 v4l2_subdev 结构体即可。一个典型的驱动自定义状态结构体如下所示:

	struct chipname\_state {
		struct v4l2\_subdev sd;
		...  /\* additional state fields \*/
	};

使用 v4l2_i2c_subdev_init 去初始化一个 I2C 子设备,该函数会填充 v4l2_subdev 的所有成员并确保 v4l2_subdevi2c_client 互相指向对方。也可以添加内联函数来从 v4l2_subdev 的指针获取到 i2c_client 结构体:

struct i2c\_client \*client = v4l2\_get\_subdevdata(sd);
也可以从i2c_client结构体指针获取到v4l2_subdev结构体:
struct v4l2\_subdev \*sd = i2c\_get\_clientdata(client);
桥驱动可以使用以下帮助函数来创建一个I2C子设备:
struct v4l2\_subdev \*sd = v4l2\_i2c\_new\_subdev
	(v4l2_dev, adapter,"module\_foo", "chipid", 0x36, NULL);

该函数会加载给定的模块(可以为空)并且调用 i2c_new_device 根据传入的参数创建子设备结构体,最后注册 v4l2_subdev

video_device 结构体

video_device 可以动态的分配:

struct video\_device \*vdev = video\_device\_alloc();
if (vdev == NULL)
	return -ENOMEM;
vdev->release = video_device_release;

如果需要将 video_device 结构体嵌入到更大的结构体里面的话,就需要设置 vdevrelease 成员。内核提供了两个默认的 release 回调函数,如下:

video\_device\_release()       // 仅仅调用kfree释放分配的内存,用于动态分配情况下
video\_device\_release\_empty() // 不做任何事情,静态变量

以下的函数成员必须被设置:

  • v4l2_dev:必须指向v4l2_device父设备
  • vfl_dir:VFL_DIR_RX(capture设备)、VFL_DIR_TX(输出设备)、VFL_DIR_M2M(codec设备)
  • fops:设置v4l2_file_operations结构体
  • ioctl_ops:ioctls,可以通过设备节点被用户空间程序访问,需设置fops的.unlocked_ioctl指向video_ioctl2
  • lock:如果想要在驱动空间里做锁操作,可以设置为NULL。否则需要指向一个已经初始化的mutex_lock结构体
  • queue:指向一个vb2_queue结构体,如果queue->lock不为空,那么与队列相关的ioctls就会使用queue内部的锁,这样的话就不用等待其它类型的ioctls操作
  • prio:对优先级进行跟踪,用在VIDIOC_G/S_PRIORITY上,如果为空的话就会使用v4l2_device里面的v4l2_prio_state
  • dev_parent:指向v4l2_device即可

如果想忽略 ioctl_ops 中某个 ioctls 的话可以调用下面的函数:

	void v4l2\_disable\_ioctl(struct video\_device \*vdev, unsigned int cmd);

如果要集成到 media_framework 里面,就需要设置 video_device 里面的 media_entity 成员,同时需要提供 media_pad

	struct media\_pad \*pad = &my_vdev->pad;
	int err;
	err = media\_entity\_init(&vdev->entity, 1, pad, 0);

  • video_device 的注册
    video_device 的注册函数如下:
	err = video\_register\_device(vdev, VFL_TYPE_GRABBER, -1);

该段代码会注册一个字符设备驱动程序并在用户空间生成一个设备节点。如果 v4l2_device 父设备的 mdev 成员不为空的话,video_deviceentity 会被自动的注册到 media framework 里面。函数最后一个参数是设备节点索引号,如果是 -1 的话就取用第一个内核中可用的索引号值。注册的设备类型以及用户空间中的节点名称取决于以下标识:

	VFL_TYPE_GRABBER: videoX 输入输出设备
	VFL_TYPE_VBI: vbiX 
	VFL_TYPE_RADIO: radioX 硬件定义的音频调谐设备
	VFL_TYPE_SDR: swradioX 软件定义的音频调谐设备

当一个设备节点被创建时,相关属性也会被创建,可以在 /sys/class/video4linux 里面看到这些设备文件夹,在文件夹里面可以看到 'name','dev_debug','index','uevent'等属性,可以使用 cat 命令查看。‘dev_debug’ 可以用于 video 设备调试,每个 video 设备都会创建一个 ‘dev_debug’ 属性,该属性以文件夹的形式存在与 /sys/class/video4linux/<devX>/ 下面以供使能 log file operation。'dev_debug’是一个位掩码,以下位可以被设置:

	0x01:记录ioctl名字与错误码。设置0x08位可以只记录VIDIOC\_(D)QBUF
	0x02:记录ioctl的参数与错误码。设置0x08位可以只记录VIDIOC\_(D)QBUF
	0x04:记录file ops操作。设置0x08位可以只记录read&write成员的操作
	0x08:如上所示
	0x10:记录poll操作

当以上的位被设置的时候,发生相关的调用或者操作的时候内核就会打印出来相关的调用信息到终端上面。类似于

[173881.402120] video4: VIDIOC_DQEVENT: error -2
[173884.906633] video4: VIDIOC_UNSUBSCRIBE_EVENT

  • video设备的清理
    当 video 设备节点需要被移除或者USB设备断开时,需要执行以下函数:


![img](https://img-blog.csdnimg.cn/img_convert/b3a17890067e40de422fb0ddfe4ce41f.png)
![img](https://img-blog.csdnimg.cn/img_convert/5def5884bb68f0b32d9f077a7a255beb.png)

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

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

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618679757)**

73884.906633] video4: VIDIOC_UNSUBSCRIBE_EVENT

  • video设备的清理
    当 video 设备节点需要被移除或者USB设备断开时,需要执行以下函数:


[外链图片转存中...(img-4GhlvlEY-1715634011493)]
[外链图片转存中...(img-vYKj3ZBU-1715634011494)]

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

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

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618679757)**

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值