Linux 深入分析V4l2框架5(基于Linux6.6)---control介绍
一、简介
1.1、V4L2 控制的作用
V4L2 控制提供了一种标准化的方法,用于访问和修改视频设备的属性和设置。这些设置通常包括视频格式、分辨率、增益、曝光、白平衡等参数。设备的这些控制项可以通过 V4L2 提供的 API 进行读取和修改。
1.2、V4L2 控制分类
V4L2 控制通常可以分为以下几类:
-
标准控制: 这些控制项适用于所有视频设备,如视频格式、图像尺寸等。
- V4L2_CID_BASE:基础控制项的起始点。
- V4L2_CID_BRIGHTNESS:亮度控制。
- V4L2_CID_CONTRAST:对比度控制。
- V4L2_CID_SATURATION:饱和度控制。
- V4L2_CID_HUE:色调控制。
- V4L2_CID_GAIN:增益控制。
- V4L2_CID_EXPOSURE:曝光控制。
-
设备特定控制: 某些设备可能有特定的控制项,这些控制项通常与设备的硬件特性紧密相关。例如,某些视频设备可能支持特殊的色彩校正、镜像、旋转等控制。
-
调节控制: 用于动态调整视频设备参数,这些参数通常在视频流传输过程中会频繁变化。
- V4L2_CID_AUTO_WHITE_BALANCE:自动白平衡控制。
- V4L2_CID_EXPOSURE_AUTO:自动曝光控制。
-
调试控制: 某些设备可能提供调试控制,用于开发和测试时的参数调整和状态查看。
1.3、控制操作方式
V4L2 控制项的操作通常是通过 ioctl
系统调用来实现的。常见的操作有:
- 查询控制:获取控制项的当前值。
- 设置控制:设置控制项的新值。
- 获取控制的范围和属性:查询控制项的取值范围、步长等信息。
主要的 ioctl
调用:
-
VIDIOC_QUERYCTRL
用于查询某个控制项的基本信息(如控制项的范围、步长、默认值等)。
-
struct v4l2_queryctrl queryctrl; queryctrl.id = V4L2_CID_BRIGHTNESS; // 控制项ID ioctl(fd, VIDIOC_QUERYCTRL, &queryctrl);
-
VIDIOC_G_CTRL
用于获取某个控制项的当前值。 -
struct v4l2_control control; control.id = V4L2_CID_BRIGHTNESS; ioctl(fd, VIDIOC_G_CTRL, &control); printf("Current brightness: %d\n", control.value);
-
VIDIOC_S_CTRL
用于设置某个控制项的值。struct v4l2_control control; control.id = V4L2_CID_BRIGHTNESS; control.value = 128; // 设置亮度为128 ioctl(fd, VIDIOC_S_CTRL, &control);
1.4、常用控制项
以下是一些常见的 V4L2 控制项及其作用:
-
亮度控制 (V4L2_CID_BRIGHTNESS)
调整视频图像的亮度值,范围通常是 -128 到 127。 -
对比度控制 (V4L2_CID_CONTRAST)
控制图像的对比度,范围通常是 0 到 255。 -
饱和度控制 (V4L2_CID_SATURATION)
调整视频图像的饱和度,范围通常是 0 到 255。 -
色调控制 (V4L2_CID_HUE)
控制视频图像的色调,范围通常是 -180 到 180。 -
增益控制 (V4L2_CID_GAIN)
调整图像的增益,用于增加图像的亮度,通常是 0 到 255。 -
自动白平衡 (V4L2_CID_AUTO_WHITE_BALANCE)
启用或禁用自动白平衡调整。 -
曝光控制 (V4L2_CID_EXPOSURE)
调整曝光级别,范围通常是 0 到 255。 -
自动曝光 (V4L2_CID_EXPOSURE_AUTO)
控制自动曝光的启用与禁用。
control 有两个主要的结构体对象:
v4l2_ctrl:描述 control 属性, control 的结构体抽象表示,跟踪 control 的值
v4l2_ctrl_handler:跟踪 control,包含一个 `v4l2_ctrl` 列表,该列表就是你需要的 control 项的集合。
二、control使用
2.1、准备驱动
这个结构体就是驱动自定义的结构体,还记得这个结构体它也会包含 v4l2_device
或者 v4l2_subdev
吗?(前面的文章里面还有描述)对,就是它,它还应该包含了 v4l2_ctrl_handler
结构体,实例如下:
struct foo_dev {
...
struct v4l2_ctrl_handler ctrl_handler;
...
};
struct foo_dev *foo;
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,该操作主要是为了兼容旧的驱动,新的驱动就不要采用这种写法了。
v4l2_ctrl_handler_free(&foo->ctrl_handler);
2.2 为 v4l2_ctrl_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;
}
可以看到上面有种类繁多的函数调用,它们的功能都会在下面一一描述。
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,它们是驱动特定的(函数内部不会自行取值填充,这些需要函数调用的时候在参数里面指定)。每个驱动的上述成员值范围大小都是不固定的,但是 type
、name
、flags
(这三个变量可以在上面那个函数内部实现看到)这些是是全局的,它们被 v4l2 驱动核心固化,每个 ID 都有自己特定的 type,name,flags,这些不需要驱动模块去关心,交给 V4L2 框架去做好了。但是我们仍然需要关注一个点,那就是 flag
变量,里面有很多类型都有自己特定的特性,比如 V4L2_CTRL_FLAG_WRITE_ONLY
表明该 ID 对应的 control 只能够从用户空间往下面写入值,无法读出,V4L2_CTRL_FLAG_VOLATILE
表明值可能会被硬件本身改变,也是只读的,每次写入的时候会被忽略,这些对我们编程是至关重要的,我们要弄清楚我们的 control 是什么类型的(尤其在需要自定义 control 的时候)。该函数调用之后相关的控制 ID 对应的 ctrl 初始值被设置为 def
参数指定的值。搞不清楚就会造成命名调用了相关的 ctrl,内核驱动却收不到消息的状况。
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个被加入的菜单项将会被跳过。
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
是一个很好的参考例程。
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 数值与上一次一致,那么在进入该回调函数之前内核相关模块就会直接返回成功了,如果数值的范围不对,则会按照就近原则进行数值的重新调整。
2.3 添加自定义以及标准 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);
- 添加自定义的
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
结构体来进行配置。
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_ops
的 s_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 里面实现,驱动编写者不必关心内部具体实现。
三、应用举例
1. 简单的 V4L2 控制应用:获取和设置亮度(Brightness)
目标
使用 V4L2 控制接口,获取和设置视频设备的亮度(Brightness)。这个例子将展示如何打开设备、获取当前的亮度值、设置新的亮度值,并关闭设备。
步骤
-
打开视频设备
使用open()
打开视频设备文件(如/dev/video0
)。 -
查询和获取亮度控制项的当前值
使用VIDIOC_G_CTRL
获取当前亮度。 -
设置新的亮度值
使用VIDIOC_S_CTRL
设置新的亮度值。 -
关闭设备
使用close()
关闭设备。
示例代码
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
int main() {
int fd;
struct v4l2_control control;
struct v4l2_queryctrl queryctrl;
// 打开视频设备(例如 /dev/video0)
fd = open("/dev/video0", O_RDWR);
if (fd == -1) {
perror("打开视频设备失败");
return -1;
}
// 查询亮度控制项的基本信息
queryctrl.id = V4L2_CID_BRIGHTNESS;
if (ioctl(fd, VIDIOC_QUERYCTRL, &queryctrl) == -1) {
perror("查询控制项失败");
close(fd);
return -1;
}
// 获取当前亮度值
control.id = V4L2_CID_BRIGHTNESS;
if (ioctl(fd, VIDIOC_G_CTRL, &control) == -1) {
perror("获取当前亮度失败");
close(fd);
return -1;
}
printf("当前亮度值: %d\n", control.value);
// 设置新的亮度值
control.value = 128; // 设置为新的亮度值(0-255 范围)
if (ioctl(fd, VIDIOC_S_CTRL, &control) == -1) {
perror("设置亮度失败");
close(fd);
return -1;
}
printf("已设置新亮度值: %d\n", control.value);
// 关闭视频设备
close(fd);
return 0;
}
说明
-
打开设备: 使用
open("/dev/video0", O_RDWR)
打开视频设备。确保设备存在,并且具有读写权限。 -
查询控制项: 使用
VIDIOC_QUERYCTRL
查询亮度控制项(V4L2_CID_BRIGHTNESS
)。该操作返回控制项的范围、步长和默认值等信息。 -
获取当前亮度值: 使用
VIDIOC_G_CTRL
获取当前亮度的值,并打印出来。 -
设置新的亮度值: 使用
VIDIOC_S_CTRL
设置亮度值。例如,我们将亮度值设置为 128。 -
关闭设备: 使用
close(fd)
关闭视频设备。
运行结果示例
当前亮度值: 90
已设置新亮度值: 128
2. V4L2 控制应用:调整白平衡(Auto White Balance)
在实际应用中,您可能需要控制白平衡(例如,自动白平衡)。以下是如何使用 V4L2 控制项来启用或禁用自动白平衡功能。
示例代码:启用/禁用自动白平衡
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
int main() {
int fd;
struct v4l2_control control;
// 打开视频设备(例如 /dev/video0)
fd = open("/dev/video0", O_RDWR);
if (fd == -1) {
perror("打开视频设备失败");
return -1;
}
// 获取当前自动白平衡状态
control.id = V4L2_CID_AUTO_WHITE_BALANCE;
if (ioctl(fd, VIDIOC_G_CTRL, &control) == -1) {
perror("获取自动白平衡状态失败");
close(fd);
return -1;
}
printf("当前自动白平衡状态: %s\n", control.value ? "启用" : "禁用");
// 禁用自动白平衡
control.value = 0; // 0 表示禁用自动白平衡
if (ioctl(fd, VIDIOC_S_CTRL, &control) == -1) {
perror("禁用自动白平衡失败");
close(fd);
return -1;
}
printf("自动白平衡已禁用\n");
// 关闭视频设备
close(fd);
return 0;
}
说明
-
查询当前状态: 通过
VIDIOC_G_CTRL
获取当前自动白平衡控制项的状态,control.value
为 1 时表示启用,0 时表示禁用。 -
设置控制项: 使用
VIDIOC_S_CTRL
设置V4L2_CID_AUTO_WHITE_BALANCE
为 0,禁用自动白平衡。
3. V4L2 控制应用:调整曝光(Exposure)
曝光控制(V4L2_CID_EXPOSURE
)允许开发者调整相机的曝光值,控制图像的亮度。以下是如何使用 V4L2 控制接口调整曝光。
示例代码:调整曝光值
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
int main() {
int fd;
struct v4l2_control control;
// 打开视频设备(例如 /dev/video0)
fd = open("/dev/video0", O_RDWR);
if (fd == -1) {
perror("打开视频设备失败");
return -1;
}
// 获取当前曝光值
control.id = V4L2_CID_EXPOSURE;
if (ioctl(fd, VIDIOC_G_CTRL, &control) == -1) {
perror("获取曝光值失败");
close(fd);
return -1;
}
printf("当前曝光值: %d\n", control.value);
// 设置新的曝光值
control.value = 128; // 设置为新的曝光值(0-255 范围)
if (ioctl(fd, VIDIOC_S_CTRL, &control) == -1) {
perror("设置曝光值失败");
close(fd);
return -1;
}
printf("已设置新曝光值: %d\n", control.value);
// 关闭视频设备
close(fd);
return 0;
}