Linux V4l2子系统分析3(基于Linux6.6)---消息机制介绍
一、概述
在 Linux 的 V4L2 (Video for Linux 2) 框架中,消息机制是用于设备和应用之间进行通信的一个重要组成部分。V4L2 的消息机制通过特定的接口和事件机制,允许应用程序与内核驱动交互,获取视频设备的状态、控制设置、或者在特定事件发生时接收通知。
1. V4L2 的消息机制概述
V4L2 提供了两种主要的消息机制来支持用户空间应用程序与内核之间的通信:
-
事件机制 (Event Notification):V4L2 允许设备驱动通过事件向用户空间应用程序发送通知。当设备状态发生变化时,内核可以通过事件机制通知用户空间。
-
控制机制 (Control Mechanism):V4L2 使用控制项来提供设备设置和状态的获取/修改机制,允许应用程序动态调整视频设备的参数。
2. V4L2 事件机制 (Event Notification)
V4L2 的事件机制允许内核在发生特定事件时,通过文件描述符的 poll()
或 select()
系统调用将事件通知到用户空间。事件可以通过 事件队列 进行传递,设备在发生特定事件时会将其写入事件队列,用户空间应用程序可以从事件队列中读取这些事件。
常见的事件类型:
- V4L2_EVENT_CTRL:控制变化事件。当控制项(如亮度、对比度、饱和度等)发生变化时会触发此事件。
- V4L2_EVENT_FRAME_SYNC:帧同步事件,用于通知应用程序视频帧同步的信息。
- V4L2_EVENT_EOS:文件结尾事件,通常在流媒体或录像结束时触发。
- V4L2_EVENT_VSYNC:垂直同步事件,在视频显示的垂直同步信号到达时触发。
3. V4L2 控制机制 (Control Mechanism)
V4L2 的控制机制允许应用程序获取和设置设备的控制项。控制项是设备的各种配置参数,如亮度、对比度、饱和度、音量等。V4L2 提供了通用的接口来管理这些控制项,支持单独控制、查询控制值等。
控制项的操作:
- 设置控制项:应用程序可以使用
ioctl()
调用VIDIOC_S_CTRL
命令来设置设备的控制项。 - 获取控制项的值:应用程序可以使用
ioctl()
调用VIDIOC_G_CTRL
命令来获取当前控制项的值。
常用控制项:
V4L2 定义了大量的控制项 ID 来表示不同的视频参数。常见的控制项包括:
- V4L2_CID_BRIGHTNESS:亮度
- V4L2_CID_CONTRAST:对比度
- V4L2_CID_SATURATION:饱和度
- V4L2_CID_GAIN:增益
- V4L2_CID_EXPOSURE:曝光时间
- V4L2_CID_WHITE_BALANCE:白平衡
- V4L2_CID_SHARPNESS:锐度
二、v4l2消息队列
v4l2消息结构体了解之前,仍然需要了解几个结构体。结构体中的数据域最好结合代码分析一下作用。
2.1、struct v4l2_event
v4l2的单个消息用下面结构体来表述的。
include/uapi/linux/videodev2.h
struct v4l2_event {
__u32 type;
union {
struct v4l2_event_vsync vsync;
struct v4l2_event_ctrl ctrl;
struct v4l2_event_frame_sync frame_sync;
struct v4l2_event_src_change src_change;
struct v4l2_event_motion_det motion_det;
__u8 data[64];
} u;
__u32 pending;
__u32 sequence;
#ifdef __KERNEL__
struct __kernel_timespec timestamp;
#else
struct timespec timestamp;
#endif
__u32 id;
__u32 reserved[8];
};
这里主要说明下面几个数据域,其它用得少,暂时不记录。
- type:毋庸置疑这是消息的类型,目前发现v4l2原生的类型包含下面这些,这些用户可以根据需要自己定制,最后一个私有的
V4L2_EVENT_PRIVATE_START
就是为扩展做准备的。
include/uapi/linux/videodev2.h
/*
* E V E N T S
*/
#define V4L2_EVENT_ALL 0
#define V4L2_EVENT_VSYNC 1
#define V4L2_EVENT_EOS 2
#define V4L2_EVENT_CTRL 3
#define V4L2_EVENT_FRAME_SYNC 4
#define V4L2_EVENT_SOURCE_CHANGE 5
#define V4L2_EVENT_MOTION_DET 6
#define V4L2_EVENT_PRIVATE_START 0x08000000
-
data[64]:这里用户可以根据实际情况需要,用这64个字节定义自己的消息实体。
-
sequence:这个用来记录该消息的序列号(并不是index),其和
struct v4l2_fh
中的sequence是一致的,后面会结合代码分析。 -
timestamp:消息发送的时间戳,代码中也没发现具体使用的地方,应该是用来判断消息生命周期准备吧。暂且不关心
-
pending:用来记录消息队列中尚未处理的消息数量。
-
id:命令id.
2.2、struct v4l2_event_subscription
订阅消息发起时,订阅的消息用此结构体描述。此订阅发起者既可以是kenle模块,也可以是用户空间的管理者。这里第一次情调一下消息只有订阅后,别的模块才能把消息放到模块的消息队列中,没有定义的消息是无法queue进去的。
include/uapi/linux/videodev2.h
struct v4l2_event_subscription {
__u32 type;
__u32 id;
__u32 flags;
__u32 reserved[5];
};
2.3、struct v4l2_fh
消息队列是用此结构体描述的。
include/media/v4l2-fh.h
struct v4l2_fh {
struct list_head list;
struct video_device *vdev;
struct v4l2_ctrl_handler *ctrl_handler;
enum v4l2_priority prio;
/* Events */
wait_queue_head_t wait;
struct mutex subscribe_lock;
struct list_head subscribed;
struct list_head available;
unsigned int navailable;
u32 sequence;
struct v4l2_m2m_ctx *m2m_ctx;
};
- list:链表入口,因为一个设备可能有好几条消息队列。(一般情况下打开一次设备会创建一个消息队列,不过一般请看下只会打开一次)
- vdev:拥有此消息队列的设备
- ctrl_handler:这其中也牵扯很多其它结构体,只需要了解这与控制元素相关就可以了。
- prio:消息队列优先级。
- wait:等待队列,由于可能有几个线程同时访问消息队列,所有等待消息队列可用的线程都会记录在这个等待列表中。
- subscribed:这个是订阅消息链表。只有订阅的消息才能够queue到消息队列中,反之是无法queue到队列中的。
- available:消息队列,可用的消息都以链表的形式保存在这里。
- navailable:消息队列中可用消息的数量。
- sequence:序列号,此数据域会一直自加下去,直到内核对象消亡。
写到这里消息队列、订阅消息、就绪消息的存在形式和依赖关系应该初步了解了,他们的组织结构大体如下所示:
注意:
- available表示的是还没处理的消息,依然挂在链表中。
- subscribed表示的是订阅的消息,单个订阅消息仍然会保存几个子消息(毕竟一个消息可以发送多次)。这里要特别注意消息queue进来时,先会保存到subscribed对应的订阅消息列表,然后才会链接到available链表上,上面蓝色和橙色之间的箭头不一定是一一对应的。具体为什么会保存多个自消息,可以继续了解一下订阅消息结构体。
2.4、struct v4l2_subscribed_event
订阅消息是以struct v4l2_subscribed_event
结构存在,其成员详细分布如下所示:
include/media/v4l2-event.h
struct v4l2_subscribed_event {
struct list_head list;
u32 type;
u32 id;
u32 flags;
struct v4l2_fh *fh;
struct list_head node;
const struct v4l2_subscribed_event_ops *ops;
unsigned int elems;
unsigned int first;
unsigned int in_use;
struct v4l2_kevent events[];
};
这了简单介绍几个重要变量
- list:将订阅消息插入到消息队列里的链接指针
- flags:一些标志位记录
- fh:可以理解成,该消息挂在哪个消息队列之上。
- replace:替换消息的方法,当订阅消息队列只能存放1个消息时,才会触发此替换方法。(此方法在订阅消息队列满以及使用
struct v4l2_ctrl
控制) - merge:当订阅消息队列有多余2个容量时,会触发merge方法(此方法在订阅消息队列满以及使用
struct v4l2_ctrl
控制) - elems:订阅消息队列中最多能存放消息的数量,此变量在订阅消息下发过来时确定,然后会由kernel分配对应的内存。
- first:这个可以理解成游标指针,记录当前订阅消息队列中入队时间最久且需要处理的消息。
- in_use:入队的消息数量
- events:这里存放的只是一个指针,其大小由发起订阅时确定。
小结:此订阅消息,可以理解成一类消息的组合
三、Enqueue消息
drivers/media/v4l2-core/v4l2-event.c
static void __v4l2_event_queue_fh(struct v4l2_fh *fh,
const struct v4l2_event *ev, u64 ts)
{
struct v4l2_subscribed_event *sev;
struct v4l2_kevent *kev;
bool copy_payload = true;
/* Are we subscribed? */
sev = v4l2_event_subscribed(fh, ev->type, ev->id);
if (sev == NULL)
return;
/* Increase event sequence number on fh. */
fh->sequence++;
/* Do we have any free events? */
if (sev->in_use == sev->elems) {
/* no, remove the oldest one */
kev = sev->events + sev_pos(sev, 0);
list_del(&kev->list);
sev->in_use--;
sev->first = sev_pos(sev, 1);
fh->navailable--;
if (sev->elems == 1) {
if (sev->ops && sev->ops->replace) {
sev->ops->replace(&kev->event, ev);
copy_payload = false;
}
} else if (sev->ops && sev->ops->merge) {
struct v4l2_kevent *second_oldest =
sev->events + sev_pos(sev, 0);
sev->ops->merge(&kev->event, &second_oldest->event);
}
}
/* Take one and fill it. */
kev = sev->events + sev_pos(sev, sev->in_use);
kev->event.type = ev->type;
if (copy_payload)
kev->event.u = ev->u;
kev->event.id = ev->id;
kev->ts = ts;
kev->event.sequence = fh->sequence;
sev->in_use++;
list_add_tail(&kev->list, &fh->available);
fh->navailable++;
wake_up_all(&fh->wait);
}
这里就是如队列消息的最重要的函数,此函数大概有下面几个流程。
- 1.先检查消息是否存在订阅消息列表中,不存在的话不允许该消息入队列,反之允许消息入队列。
- 2.当该消息对应的订阅消息对象子消息满员之后,则将子消息队列中最老的那个替换掉,以留给新的消息。
- 3.将新消息挂接到v4l2_fh可用链表中,以进行后续其它处理
注意:下图是struct v4l2_subscribed_event
结构体中的struct v4l2_kevent
子消息在订阅消息结构体中的存放形态。
上面子消息队列中已经入队了2个消息,此时in_use=2,而此时记录指针指向1,则first=1,剩下的红色都是空闲子消息缓存。子消息的结构如下所示:
include/media/v4l2-event.h
struct v4l2_kevent {
struct list_head list;
struct v4l2_subscribed_event *sev;
struct v4l2_event event;
u64 ts;
};
四、Dequeue消息
Dequeue操作就是取消息的操作(类似于取buffer时用dequeue_buffer
),相比较消息的enqueue操作,dequeue操作简单多了。这里仍然根据代码流程来分析。
drivers/media/v4l2-core/v4l2-event.c
int v4l2_event_dequeue(struct v4l2_fh *fh, struct v4l2_event *event,
int nonblocking)
{
int ret;
if (nonblocking)
return __v4l2_event_dequeue(fh, event);
/* Release the vdev lock while waiting */
if (fh->vdev->lock)
mutex_unlock(fh->vdev->lock);
do {
ret = wait_event_interruptible(fh->wait,
fh->navailable != 0);
if (ret < 0)
break;
ret = __v4l2_event_dequeue(fh, event);
} while (ret == -ENOENT);
if (fh->vdev->lock)
mutex_lock(fh->vdev->lock);
return ret;
}
EXPORT_SYMBOL_GPL(v4l2_event_dequeue);
取消息相对简单多了,大概就下面几步
- 1.检查消息列表是否是为空,不为空则从中取出子消息。
- 2.将上面子消息,从上面的消息列表中删除,并减少消息队列中消息的数量
- 3.将消息对应的订阅消息对象的记录指针后移,以及如队列消息-1.
四、案例学习
- 上层
这里以高通daemon进程起来后,server就向video0 node中订阅了NEW_SESSION、DEL_SESSION等消息。
subscribe.type = MSM_CAMERA_V4L2_EVENT_TYPE;
for (i = MSM_CAMERA_EVENT_MIN + 1; i < MSM_CAMERA_EVENT_MAX; i++) {
subscribe.id = i;
if (ioctl(hal_fd->fd[0], VIDIOC_SUBSCRIBE_EVENT, &subscribe) < 0)
goto subscribe_failed;
}
其中子消息的类型:
#define MSM_CAMERA_EVENT_MIN 0
#define MSM_CAMERA_NEW_SESSION (MSM_CAMERA_EVENT_MIN + 1)
#define MSM_CAMERA_DEL_SESSION (MSM_CAMERA_EVENT_MIN + 2)
#define MSM_CAMERA_SET_PARM (MSM_CAMERA_EVENT_MIN + 3)
#define MSM_CAMERA_GET_PARM (MSM_CAMERA_EVENT_MIN + 4)
#define MSM_CAMERA_MAPPING_CFG (MSM_CAMERA_EVENT_MIN + 5)
#define MSM_CAMERA_MAPPING_SES (MSM_CAMERA_EVENT_MIN + 6)
#define MSM_CAMERA_MSM_NOTIFY (MSM_CAMERA_EVENT_MIN + 7)
#define MSM_CAMERA_EVENT_MAX (MSM_CAMERA_EVENT_MIN + 8)
上面是上层对应的代码,ioctl会调用到对应设备驱动实现的vidioc_subscribe_event
,这里kernel已经实现了对应的接口。
case VIDIOC_SUBSCRIBE_EVENT:
{
struct v4l2_event_subscription *sub = arg;
if (!ops->vidioc_subscribe_event)
break;
ret = ops->vidioc_subscribe_event(fh, sub);
if (ret < 0) {
dbgarg(cmd, "failed, ret=%ld", ret);
break;
}
dbgarg(cmd, "type=0x%8.8x", sub->type);
break;
}
上面执行完成后,订阅消息NEW_SESSION、DEL_SESSION等消息会挂接到设备的fh->subscribed
订阅链表上。下一个操作则从订阅列表上查找入队列的消息,存在的话允许如队列,反之不允许。
static void __v4l2_event_queue_fh(struct v4l2_fh *fh, const struct v4l2_event *ev,
const struct timespec *ts)
{
struct v4l2_subscribed_event *sev;
struct v4l2_kevent *kev;
bool copy_payload = true;
/* Are we subscribed? */
sev = v4l2_event_subscribed(fh, ev->type, ev->id);
if (sev == NULL)//如果订阅过的话,这里就不会进来
return;
小结:“消息必须先订阅,才允许入队列” 订阅消息对象默认元素数量为1,如果设置的话就按设置的来分配内存。
五、举例应用
Rockchip 平台与 V4L2 消息机制的应用
Rockchip 平台,作为一个广泛应用于嵌入式设备、智能电视、平板电脑等领域的系统平台,通常会用到 V4L2 来处理摄像头、视频编码解码、图像处理等任务。V4L2 提供的消息机制可以有效支持这些功能,特别是在事件驱动和设备控制方面。
下面通过一个具体的示例来介绍如何在 Rockchip 平台上使用 V4L2 消息机制。
1. V4L2 消息机制在 Rockchip 平台的常见应用场景
- 事件通知:通过 V4L2 事件机制,设备可以向应用程序发送事件通知,告知应用程序有关设备状态变化、视频帧同步、视频流结束等信息。
- 控制项调整:应用程序可以使用 V4L2 的控制项接口来修改摄像头或视频流的参数,如曝光、白平衡、亮度等。
- 视频捕获与流控制:利用 V4L2 的事件机制和控制机制,开发者可以实现视频流的控制与管理,确保视频捕获和显示与硬件同步。
2. Rockchip 平台使用 V4L2 事件机制的示例
假设我们正在开发一个基于 Rockchip 平台的摄像头应用程序,应用程序需要监听与摄像头相关的事件(例如,视频帧同步、垂直同步等),并根据这些事件进行相应的处理。
步骤 1: 打开设备并设置事件监听
首先,我们打开摄像头设备文件(/dev/video0
),并通过 poll()
来监听设备的事件。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <poll.h>
#include <sys/ioctl.h>
#include <linux/videodev2.h>
int main() {
int fd = open("/dev/video0", O_RDWR);
if (fd == -1) {
perror("Failed to open video device");
return -1;
}
// 监听事件
struct pollfd pfd;
pfd.fd = fd;
pfd.events = POLLIN; // 监听设备可读事件
while (1) {
int ret = poll(&pfd, 1, -1); // 阻塞等待事件
if (ret > 0 && (pfd.revents & POLLIN)) {
// 有事件发生,读取事件
struct v4l2_event event;
if (ioctl(fd, VIDIOC_DQEVENT, &event) == -1) {
perror("Failed to dequeue event");
break;
}
// 处理不同类型的事件
switch (event.type) {
case V4L2_EVENT_FRAME_SYNC:
printf("Frame sync event received\n");
break;
case V4L2_EVENT_VSYNC:
printf("Vertical sync event received\n");
break;
default:
printf("Unknown event type: %u\n", event.type);
break;
}
}
}
close(fd);
return 0;
}
代码解释:
- 打开设备:首先,应用程序打开摄像头设备文件
/dev/video0
。 - 监听事件:我们使用
poll()
函数监听文件描述符的可读事件。当事件发生时,通过VIDIOC_DQEVENT
获取事件。 - 处理事件:根据事件类型(如
V4L2_EVENT_FRAME_SYNC
、V4L2_EVENT_VSYNC
等),我们可以采取不同的处理策略。
常见事件类型:
- V4L2_EVENT_FRAME_SYNC:通知应用程序视频帧同步,通常用于同步显示或处理视频帧。
- V4L2_EVENT_VSYNC:垂直同步事件,通常用于视频显示的同步控制。
- V4L2_EVENT_EOS:视频流结束事件,通常用于通知应用程序视频录制或播放已结束。
步骤 2: 控制摄像头参数
通过 V4L2 控制机制,应用程序可以动态地调整摄像头的参数,如曝光、亮度、对比度等。
#include <linux/videodev2.h>
int set_camera_brightness(int fd, int brightness) {
struct v4l2_control control;
control.id = V4L2_CID_BRIGHTNESS; // 设置亮度控制项
control.value = brightness;
if (ioctl(fd, VIDIOC_S_CTRL, &control) == -1) {
perror("Failed to set brightness");
return -1;
}
return 0;
}
代码解释:
- 控制项设置:通过
VIDIOC_S_CTRL
设置摄像头的亮度(V4L2_CID_BRIGHTNESS
)。 - 执行控制:通过
ioctl()
调用来提交设置。
3. 集成到 Rockchip 平台的视频应用
在 Rockchip 平台上,我们通常会使用硬件加速的摄像头模块,并通过 V4L2 接口来访问这些硬件资源。V4L2 的事件机制可以帮助我们有效地处理硬件事件,如帧捕获同步、图像处理等。而控制机制则可以用来优化视频流参数,以适应不同的环境条件。
例如,在 Rockchip 平台上,如果摄像头支持硬件加速的曝光调节,应用程序可以通过 V4L2 控制机制动态调整曝光值,确保在光线变化的环境中保持图像质量。