V4L2框架-control

阅读原文

本文介绍在 v4l2 框架下面的 control 控制接口,这些接口通常用来实现一些特效控制、菜单控制等等。

03 - V4L2框架-videobuf2
02 - V4L2框架-media-device
01 - V4L2框架-v4l2 device
00 - V4L2框架概述

简介

既然涉及到视频输入,就会有很多与 ISP 相关的效果,比如对比度、饱和度、色温、白平衡等等,这些都是通用的、必须的控制项,并且大多数仅需要设置一个整数值即可。V4L2 很贴心地为我们提供了这样一些接口以供使用(可以说是非常贴心的了),在内核里面,这些控制项被抽象为一个个的控制 ID,分别以 V4L2_CID_XXX 来命名。

有许多控制函数并不是单个驱动特定的,这些通用的控制 API 可以挪到 V4L2 内核框架里面。而留给驱动开发者的问题只是以下几个点:

  • 明确自己需要什么样的 control
  • 怎样去添加一个 control
  • 怎么去设置 control 的值,后面会知道设置接口是 s_ctrl
  • 怎么去获取 control 的值,g_volatile_ctrl
  • 怎么去验证用户的 control 的值是否合法(try_ctrl),control 框架依赖于 v4l2_device 以及 v4l2_subdev

control 有两个主要的结构体对象:

v4l2_ctrl:描述 control 属性, control 的结构体抽象表示,跟踪 control 的值
v4l2_ctrl_handler:跟踪 control,包含一个 `v4l2_ctrl` 列表,该列表就是你需要的 control 项的集合

V4L2 ctrl 模块内部实现

V4L2 ctrl 模块内部实现

如何使用

准备驱动

1. 添加 handler 到驱动顶层结构体里面

这个结构体就是驱动自定义的结构体,还记得这个结构体它也会包含 v4l2_device 或者 v4l2_subdev 吗?(前面的文章里面还有描述)对,就是它,它还应该包含了 v4l2_ctrl_handler 结构体,实例如下:

	struct foo_dev {
		...
		struct v4l2_ctrl_handler ctrl_handler;
		...
	};

	struct foo_dev *foo;

2. 初始化 handler

v4l2_ctrl_handler_init(&foo->ctrl_handler, nr_of_controls);

第二个参数指明期望创建的 control 的数量,函数根据这个值来创建一个哈希表,这个数量只是作为一个指导,实际上的 control 数量不一定跟这个数字一致。对于 `video_device` 或者 `v4l2_subdev` 设备来说,需要显式的设置他们的 `ctrl_handler` 成员(去内核代码里面看一下就能看到它)指向驱动结构体的 `ctrl_handler`,否则打开video节点并不能进行相关的控制。

3. 把控制 handler 挂接到驱动中

  • 对于v4l2 驱动来说
	struct foo_dev {
		...
		struct v4l2_device v4l2_dev;
		...
		struct v4l2_ctrl_handler ctrl_handler;
		...
	};
	foo->v4l2_dev.ctrl_handler = &foo->ctrl_handler;

这里移除了 V4L 一代目原有 v4l2_ctrl_ops 里面的 vidioc_queryctrl, vidioc_query_ext_ctrl, vidioc_querymenu, vidioc_g_ctrl, vidioc_s_ctrl, vidioc_g_ext_ctrls, vidioc_try_ext_ctrls and vidioc_s_ext_ctrls 操作函数,这些在使用了 control 框架之后都不再需要。

  • 对于子设备驱动来说
	struct foo_dev {
		...
		struct v4l2_subdev sd;
		...
		struct v4l2_ctrl_handler ctrl_handler;
		...
	};
	foo->sd.ctrl_handler = &foo->ctrl_handler;

然后设置 v4l2_subdev_core_ops 里面的所有成员指向帮助函数。

		.queryctrl = v4l2_subdev_queryctrl,
		.querymenu = v4l2_subdev_querymenu,
		.g_ctrl = v4l2_subdev_g_ctrl,
		.s_ctrl = v4l2_subdev_s_ctrl,
		.g_ext_ctrls = v4l2_subdev_g_ext_ctrls,
		.try_ext_ctrls = v4l2_subdev_try_ext_ctrls,
		.s_ext_ctrls = v4l2_subdev_s_ext_ctrls,

这是一个暂时性的解决方案,等到所有依赖子设备驱动的 v4l2 驱动都转为使用 control 框架之后这些函数将不再需要,也就是说有的 v4l2 驱动仍然可能通过上面的回调去进行 ctrl 控制,上面的帮助函数只是做一个中转,把 v4l2 驱动的回调重新定位,指向 control 框架中的 ctrl,该操作主要是为了兼容旧的驱动,新的驱动就不要采用这种写法了。

4. 最后清理 handler

v4l2_ctrl_handler_free(&foo->ctrl_handler);

说清理是在模块卸载或者使用完毕的时候才去清理,不要刚初始化完就去清理了,那样还用个啥。

v4l2_ctrl_handler 添加控制

1. 初始化控制 handler

	static const s64 exp_bias_qmenu[] = {
	       -2, -1, 0, 1, 2
	};
	static const char * const test_pattern[] = {
		"Disabled",
		"Vertical Bars",
		"Solid Black",
		"Solid White",
	};

	v4l2_ctrl_handler_init(&foo->ctrl_handler, nr_of_controls);
	v4l2_ctrl_new_std(&foo->ctrl_handler, &foo_ctrl_ops,
			V4L2_CID_BRIGHTNESS, 0, 255, 1, 128);
	v4l2_ctrl_new_std(&foo->ctrl_handler, &foo_ctrl_ops,
			V4L2_CID_CONTRAST, 0, 255, 1, 128);
	v4l2_ctrl_new_std_menu(&foo->ctrl_handler, &foo_ctrl_ops,
			V4L2_CID_POWER_LINE_FREQUENCY,
			V4L2_CID_POWER_LINE_FREQUENCY_60HZ, 0,
			V4L2_CID_POWER_LINE_FREQUENCY_DISABLED);
	v4l2_ctrl_new_int_menu(&foo->ctrl_handler, &foo_ctrl_ops,
			V4L2_CID_EXPOSURE_BIAS,
			ARRAY_SIZE(exp_bias_qmenu) - 1,
			ARRAY_SIZE(exp_bias_qmenu) / 2 - 1,
			exp_bias_qmenu);
	v4l2_ctrl_new_std_menu_items(&foo->ctrl_handler, &foo_ctrl_ops,
			V4L2_CID_TEST_PATTERN, ARRAY_SIZE(test_pattern) - 1, 0,
			0, test_pattern);
	...
	if (foo->ctrl_handler.error) {
		int err = foo->ctrl_handler.error;

		v4l2_ctrl_handler_free(&foo->ctrl_handler);
		return err;
	}

可以看到上面有种类繁多的函数调用,它们的功能都会在下面一一描述。

2. 非菜单控制项

	struct v4l2_ctrl *v4l2_ctrl_new_std(struct v4l2_ctrl_handler *hdl,
		const struct v4l2_ctrl_ops *ops,
		u32 id, s32 min, s32 max, u32 step, s32 def);

v4l2_ctrl_new_std 函数将会基于 control 的 ID 来填充所有成员,但是除了 min, max, step 以及 default values,它们是驱动特定的(函数内部不会自行取值填充,这些需要函数调用的时候在参数里面指定)。每个驱动的上述成员值范围大小都是不固定的,但是 typenameflags (这三个变量可以在上面那个函数内部实现看到)这些是是全局的,它们被 v4l2 驱动核心固化,每个 ID 都有自己特定的 type,name,flags,这些不需要驱动模块去关心,交给 V4L2 框架去做好了。但是我们仍然需要关注一个点,那就是 flag 变量,里面有很多类型都有自己特定的特性,比如 V4L2_CTRL_FLAG_WRITE_ONLY 表明该 ID 对应的 control 只能够从用户空间往下面写入值,无法读出,V4L2_CTRL_FLAG_VOLATILE 表明值可能会被硬件本身改变,也是只读的,每次写入的时候会被忽略,这些对我们编程是至关重要的,我们要弄清楚我们的 control 是什么类型的(尤其在需要自定义 control 的时候)。该函数调用之后相关的控制 ID 对应的 ctrl 初始值被设置为 def 参数指定的值。搞不清楚就会造成命名调用了相关的 ctrl,内核驱动却收不到消息的状况。

3. 菜单控制项

	struct v4l2_ctrl *v4l2_ctrl_new_std_menu(struct v4l2_ctrl_handler *hdl,
		const struct v4l2_ctrl_ops *ops,
		u32 id, s32 max, s32 skip_mask, s32 def);

v4l2_ctrl_new_std_menu 函数与 v4l2_ctrl_new_std 很像,只是用在菜单控制上面,对于菜单控制来说,默认 min 都为 0。如果 skip_mask 的第X位为1,那么第X个被加入的菜单项将会被跳过。

4. 驱动特定菜单控制项

   struct v4l2_ctrl *v4l2_ctrl_new_std_menu_items(
	   struct v4l2_ctrl_handler *hdl,
	   const struct v4l2_ctrl_ops *ops, u32 id, s32 max,
	   s32 skip_mask, s32 def, const char * const *qmenu);

该函数与 v4l2_ctrl_new_std_menu 比较相似,它有一个额外的参数 qmenu,这是驱动特定的 menu,它是一个二维的数组,数组的每一项是一个字符串,里面就是下拉菜单里面的项目对应的实际值。camif-capture.c 是一个很好的参考例程。

5. 驱动特定的整数菜单控制项

	struct v4l2_ctrl *v4l2_ctrl_new_int_menu(struct v4l2_ctrl_handler *hdl,
		const struct v4l2_ctrl_ops *ops,
		u32 id, s32 max, s32 def, const s64 *qmenu_int);

该函数会根据驱动特定的 item 来创建一个标准的 integer 类型的菜单控制项,该函数的最后一个参数 qmenu_int 是一个有符号的64位整形菜单 item 列表。该函数与上一个非常相似,只不过菜单的内容一个是字符串,一个是整形。

上述所有的函数一般都在 v4l2_ctrl_handler_init 之后被调用,所有的函数都会返回一个 v4l2_ctrl 类型的指针,如果需要存放相关的 ctrl 的话就可以获取函数的返回值存放起来。值得注意的是,当这些函数调用出错时,就会返回 NULL 或者错误码,并且设置 ctrl_handler->error 指向错误码,该错误码指向 ctrl_handler 的 ctrl 列表中第一个出错的 ctrl 的错误码。这些对 v4l2_ctrl_handler_init 函数来说也适用。

v4l2_ctrl_cluster 函数可以将指定的 ctrl 列表中一定数量的 ctrl 进行合并,所有合并之后的控制 ID 均被指向合并列表中的第一个控制 ID 项,也就是说,一旦 ID 合并,不管你执行哪一个 ID,只要它处于被合并的 ID 列表中,最终执行的控制 ID 都是列表中的第一项 ID 所在的控制项,常用于多种控制通过同一个硬件来完成的情况下。

举个例子:假设大部分的硬件中,亮度与色温是属于两个模块控制的,其值的大小也都不一致,这个时候就没有必要去合并,只管各自控制各自的就好,而有的控制器会把亮度与色温进行绑定,由同一个模块进行控制,亮度与
色温有一个对照表(一一对应),此时这两个就可以进行合并,合并之后只需要执行其中一个控制即可。那为什么一样的话不直接就用其中一个好了,因为有些应用程序就是分开控制的,但是移植到了一个新的硬件平台上面,我改应用好麻烦的(尤其是业务逻辑),所以在驱动里面去适配是最好不过了。

6. 可选的强制初始化控制设置

v4l2_ctrl_handler_setup(&foo->ctrl_handler);

该函数将会为所有的 controls 调用 s_ctrl,一般用来设置硬件为默认配置。如果需要同步硬件与内部数据结构体,那么就可以运行该函数完成同步操作,常用于防止硬件的初始化值与控制 ID 指定的初始化默认值不一致的情况,比如在系统刚刚起来的时候设置一遍默认的值用来初始化整个系统效果,很有用。

7. 实现 v4l2_ctrl_ops 结构体

static const struct v4l2_ctrl_ops foo_ctrl_ops = {
	.s_ctrl = foo_s_ctrl,
};

通常情况下需要设置 s_ctrl 成员函数为:

	static int foo_s_ctrl(struct v4l2_ctrl *ctrl)
	{
		struct foo *state = container_of(ctrl->handler, struct foo, ctrl_handler);

		switch (ctrl->id) {
		case V4L2_CID_BRIGHTNESS:
			write_reg(0x123, ctrl->val);
			break;
		case V4L2_CID_CONTRAST:
			write_reg(0x456, ctrl->val);
			break;
		}
		return 0;
	}

需要注意的是,一旦整个ctrl流程进入到 s/g_ctrl 回调函数里面了,就说明传入的 ctrl 数值是正确的(前提是 ctrl 初始化的时候数值范围等设置正确), control 框架会根据设置的初始值来判断值的合理性,如果可以走到函数内部的调用,那此时只管进行寄存器操作,不必关心数值的正确与否(它肯定是对的)。如果该次的 ctrl 数值与上一次一致,那么在进入该回调函数之前内核相关模块就会直接返回成功了,如果数值的范围不对,则会按照就近原则进行数值的重新调整。

添加自定义以及标准 ctrl

1. 标准的ctrl

  • 实现 v4l2_ctrl_ops 结构体
	static const struct v4l2_ctrl_ops foo_ctrl_ops = {
		.s_ctrl = foo_s_ctrl,
	};

通常情况下需要设置 s_ctrl 成员函数为:

	static int foo_s_ctrl(struct v4l2_ctrl *ctrl)
	{
		struct foo *state = container_of(ctrl->handler, struct foo, ctrl_handler);

		switch (ctrl->id) {
		case V4L2_CID_BRIGHTNESS:
			write_reg(0x123, ctrl->val);
			break;
		case V4L2_CID_CONTRAST:
			write_reg(0x456, ctrl->val);
			break;
		}
		return 0;
	}
  • 添加至 ctrl_handler 里面
	v4l2_ctrl_handler_init(&foo->ctrl_handler, nr_of_controls);
	v4l2_ctrl_new_std(&foo->ctrl_handler, &foo_ctrl_ops,
			V4L2_CID_BRIGHTNESS, 0, 255, 1, 128);
	v4l2_ctrl_new_std(&foo->ctrl_handler, &foo_ctrl_ops,
			V4L2_CID_CONTRAST, 0, 255, 1, 128);

2. 自定义的 ctrl

  • 添加自定义的 v4l2_ctrl_config 配置结构体
	static const struct v4l2_ctrl_config ctrl_filter = {
		.ops = &foo_ctrl_ops,
		.id = V4L2_CID_CUSTOM_ID,
		.name = "Spatial Filter",
		.type = V4L2_CTRL_TYPE_INTEGER,
		.flags = V4L2_CTRL_FLAG_SLIDER, // 必要的
		.max = 15,
		.step = 1,
	};

	ctrl = v4l2_ctrl_new_custom(&foo->ctrl_handler, &ctrl_filter, NULL);

其中菜单类型、普通类型、不同标志位的 ctrl 都可以通过 v4l2_ctrl_config 结构体来进行配置。

3. 用户空间使用

	struct v4l2_control ctrl;

	ctrl.id = V4L2_CID_CUSTOM_ID;
	ctrl.calue = 200;
	ioctl(fd, VIDIOC_S_CTRL, &ctrl);

VIDIOC_S_CTRL:该 ioctl 调用可能(与 ctrl 的 flag 以及 value 有关,如果 value 值与上次一致,那么就会直接返回)会导致 foo_ctrl_opss_ctrl 成员被调用,其它的成员函数有相应的调用 ID,ctrl 的 id 成员应该填充自己想要控制的 ctrl 项,value 就是设置 ctrl 相应项的值。

对于 /dev/videoX 类型的设备节点来说,会按照 v4l2_fh.ctrl_handler->video_device.ctrl_handler->v4l2_ioctl_ops.vidioc_s_ctrl->v4l2_ioctl_ops.vidioc_s_ext_ctrls 顺序来查找是否有可用的相关的 ctrl 调用。对于子设备类型的节点(/dev/v4l-subdevX)来说,会自动选择 v4l2_fh.ctrl_handler 来进行控制操作,这个在子设备 framework 里面实现,驱动编写者不必关心内部具体实现。

扩展项

  • ctrl 的继承
    当一个子设备被注册的时候(v4l2_device_register_subdev),同时 v4l2_device 以及 v4l2_subdevctrl_handler 成员都被设置,那么 V4L2 驱动也可以使用到子设备的 ctrl,如果两者的 ctrl 有重复的话,子设备的 ctrl 将会被跳过(对于V4L2 主设备驱动来说,子设备的仍然会执行自己的 ctrl),等于说是一个由子设备到 V4L2 父设备的自下而上的继承关系。在子设备注册函数中 v4l2_ctrl_add_handler() 函数会被调用以添加子设备的 ctrl 到 V4L2 设备当中(子设备彼此不可共享访问)。如果想在子设备之间共享 ctrl_handler 的话,可以显式地调用 v4l2_ctrl_add_handler 函数来完成。设置 ctrl 的 is_private 位为1可以避免 ctrl_handler 的合并导致 ctrl 共享。

  • 访问控制值(access value)
    v4l2_ctrl 结构体为驱动保存了当前的以及新的 value,以供对比,并且只有在合适的时候才能够成功把值从用户空间传递到内核驱动。在 v4l2_ctrl 结构体里面有以下的成员:

	s32 val;
	struct {
		s32 val;
	} cur;
	union v4l2_ctrl_ptr p_new;
	union v4l2_ctrl_ptr p_cur;

这几个成员用于保存对应控制的新旧数值,如果控制数值是简单的32位有符号整形的话,他们之间如下的对应关系(只有 s_ctrl 返回为0的时候 ctrl 核心模块才会把新设置的值拷贝到缓存成员当中去,执行失败理所当然是不需要保存的):

	&ctrl->val == ctrl->p_new.p_s32
	&ctrl->cur.val == ctrl->p_cur.p_s32

猜想一下,如果这个里面已经保存了新旧的数值的话,那么 g_volatile_ctrl 就没必要去实现了(因为当我需要读取相关的值的时候我直接从值缓存里面拿取数据就好了)。而该函数的实现是专门用于那些可能由于硬件原因而导致值不断变化的 ctrl 的(所以才命名为 g_volatile_ctrl),此时需要设置 ctrl 的 flag 属性为易变的状态:flag |= V4L2_CTRL_FLAG_VOLATILE;此时所有的来自用户空间的写入操作都会被忽略。表明相关 ctrl 的值可能会由硬件本身来改变,所以每次 get ctrl 的时候都乖乖调用相关的回调函数,在回调函数里面读取寄存器的值,重新返回新读取的值。通常情况下,设置为 volatile 的 ctrl 都是只读的,但是仍然可以在 g_volatile_ctrl 回调函数里面去访问值缓存结构体成员(这个访问是由驱动本身发起的),需要注意的是,如果一个 ctrl 是 volatile 的同时又不是只读的话,就不会产生 V4L2_EVENT_CTRL_CH_VALUE 这一事件消息。

在驱动内部可以使用以下的函数来进行相关 ctrl 的数值设定(切记不要在 v4l2_ctrl_ops 回调内部使用下面的函数,否则会造成死锁,因为本身进入那些回调函数的时候就加锁了,再次调用下面的函数会导致重复加锁):

	s32 v4l2_ctrl_g_ctrl(struct v4l2_ctrl *ctrl);
	int v4l2_ctrl_s_ctrl(struct v4l2_ctrl *ctrl, s32 val);
  • 菜单控制
    v4l2_ctrl 结构体内部有以下成员:
	union {
		u32 step;
		u32 menu_skip_mask;
	};

对于菜单控制来说,该共用体使用 menu_skip_mask 成员,该成员指定了哪些菜单项将会被跳过,如果为0,则表明所有的菜单项都可用,值得注意的是,VIDIOC_QUERYCTRL 调用将会永远返回步长1。一个比较好的例子是 MPEG Audio Layer II 的比特率菜单控制项,实际上,大部分硬件只用到了其中的一部分。

  • 激活与锁定 control
    在实际使用当中,有的 control 之间具有一定的强相关性,比如 Chroma AGC 控制打开的时候, Chroma Gain 就无效了(硬件层面属于互斥),当这种情况发生的时候,如果用户接着去设置 Chroma Gain 的相关控制,虽然软件层面是可以设置的,但是硬件是没有办法响应的,这个时候就需要将 Chroma Gain 的相关控制禁止掉,也就是互斥。内核可以调用 v4l2_ctrl_activate() 函数来发出激活/去激活信号,注意,控制 framework 并不会去检查这个标志,这个仅用于 GUI 层面,当函数被调用的时候,用户空间会收到一个事件,通过该事件,GUI 可以进行相关的动作。该函数通常用于 s_ctrl 回调中。该标志位是 ‘active’

另一个标志位是 ‘grabbed’,代表锁定,比如 MPEG 中,在视频流的传输过程当中,比特率是不能改变的,也就是说,视频传输过程中相关的控制应该是处于锁定状态的,可以使用 v4l2_ctrl_grab() 函数来设置锁定/解锁相关的控制项,控制 framework 会检查该标志位,如果被设置,那么该控制调用就会返回 -EBUSY 信息,通常情况下该函数在码流的开启/关闭函数中被调用。

  • 为ctrl添加回调
void v4l2_ctrl_notify(struct v4l2_ctrl *ctrl,
	void (*notify)(struct v4l2_ctrl *ctrl, void *priv), void *priv);

该函数可以为一个 ctrl 添加回调,当相关 ctrl 的值被改变的时候,该回调就会产生作用,一个 ctrl 只能够添加一个回调,如果尝试添加多个 ctrl 的话会导致 WARN_ON 的产生。注意在该回调产生的时候,ctrl 的锁仍然被保持,直到相关的 s_ctrl 返回时才进行解锁。

  • 与 V4L2 event 的联合使用
    在内核 control 模块初始化的时候会为每一个 ctrl 设置其初始值,并且为每一个 ctrl 产生一个 V4L2_EVENT_CTRL 的事件。此后在每一次成功设置了相应 ctrl 的值之后都会产生一个 V4L2_EVENT_CTRL 事件,事件的内容包含了 ctrl 的 ID 以及它的值。那么这个东西有什么用呢?比如你有两个进程,一个进程是控制 ISP 效果的,另一个进程需要根据不同的 ISP 效果做出不同的响应,那么就可以在另一个进程里面订阅这个类型的事件,每次值发生改变的时候就可以依据相关的 ctrl 值进行对应的动作。

关于 V4L2 event 的介绍在第一篇文章的结尾部分就有描述了:00 - V4L2框架概述

到本文为止,基本上 V4L2 框架的驱动部分被介绍地差不多了,还有用户空间操作层面的东西,比如应该按照什么样的序列来进行数据流的开启与关闭,应该如何正确、规范地使用 control,如何配合事件使用,以及如何在一个具体的案例里面组合这些模块进行统一的规划编程等等。这些虽然目前为止没有专门的一篇文章来去介绍,但是看完前面几片文章之后相信已经有一个大体的使用框架了,后续是否会更新应用空间的操作待定,大概率不会。


想做的事情就去做吧
已标记关键词 清除标记
这是我调曝光的方法,试了v4l2_control的两种方式,这种可以调,但是保存图片发现不是每次曝光都正常。这是为什么,是这个V4L2_CID_EXPOSURE_ABSOLUTE参数不好还是摄像头问题。 ``` int set_exposure(int cam_fd, int exp_value) { struct v4l2_control ctrl; struct v4l2_queryctrl setting; int min, max, current, step, val_def; if (isv4l2Control(cam_fd, V4L2_CID_EXPOSURE_ABSOLUTE, &setting) < 0) return -1; min = setting.minimum; max = setting.maximum; step = setting.step; val_def = setting.default_value; current = v4l2GetControl(cam_fd, V4L2_CID_EXPOSURE_ABSOLUTE); printf("max %d, min %d, default %d, current %d \n", max, min, val_def, current); printf("start set exp\n"); int ret; //设置曝光绝对值 ctrl.id = V4L2_CID_EXPOSURE_ABSOLUTE; ctrl.value = exp_value; ret = ioctl(cam_fd, VIDIOC_S_CTRL, &ctrl); if (ret < 0) { printf("Set exposure failed (%d)\n", ret); return FALSE; } else printf("Control name:%s set to value:%d\n", setting.name, ctrl.value); return TRUE; } ``` 这是我尝试过不行的方式 ``` int ret; struct v4l2_controlctrl; ctrl.id = V4L2_CID_EXPOSURE; //得到曝光档次,A20接受从 -44 共9个档次 ret = ioctl(Handle, VIDIOC_G_CTRL,&ctrl); if (ret < 0) { printf("Get exposure failed (%d)\n",ret); returnV4L2_UTILS_GET_EXPSURE_ERR; } printf("\nGet Exposure :[%d]\n",ctrl.value); //设置曝光档次 ctrl.id = V4L2_CID_EXPOSURE; ctrl.value = -4; ret = ioctl(Handle, VIDIOC_S_CTRL,&ctrl); if (ret < 0) { printf("Set exposurefailed (%d)\n", ret); return V4L2_UTILS_SET_EXPSURE_ERR;} ```
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页