Linux V4L2 - 消息机制

V4L2的消息同大部分的消息组织形式类似,可以理解为都是以队列的形式,有人往里面push,有人去pop。
kernel版本:V3.4。


一、V4L2消息队列的准备知识

1.1 struct v4l2_event

V4L2的单个消息用此结构体来表述的:

struct v4l2_event {
	__u32				  type;
	union {
		struct v4l2_event_vsync		    vsync;
		struct v4l2_event_ctrl		    ctrl;
		struct v4l2_event_frame_sync	frame_sync;
		__u8			                data[64];
	} u;
	__u32				  pending;
	__u32				  sequence;
	struct timespec		  timestamp;
	__u32				  id;
	__u32				  reserved[8];
};

type:消息类型。目前发现v4l2原生的类型包含下面这些,这些用户可以根据需要自己定制。最后这个V4L2_EVENT_PRIVATE_START就是为扩展做准备的。

#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_PRIVATE_START	0x08000000

data:用户自定义的消息实体/内容。

sequence:消息的序列号(不是index)。和struct v4l2_fh的成员sequence相同。(后面会结合代码分析)

timestamp:消息发送的时间戳。代码中没发现具体使用处,应该是用来判断消息生命周期吧。

pending:记录消息队列中尚未处理的消息数量。

id:命令ID。

1.2 struct v4l2_event_subscription

订阅消息发起时,订阅的消息用此结构体描述:

struct v4l2_event_subscription {
	__u32		type;
	__u32		id;
	__u32		flags;
	__u32		reserved[5];
};

订阅发起者既可以是内核模块的,也可以是用户空间的。强调一下:只有在订阅消息后,别的模块才能把消息放到该模块的消息队列中,没有定义的消息是无法queue进去的

1.3 struct v4l2_fh
消息队列就是用此结构体描述的:

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 list_head	         subscribed;  /* Subscribed events */
	struct list_head	         available;   /* Dequeueable events */
	unsigned int		         navailable;
	u32			                 sequence;
};

list:链表入口。因为一个设备可能有好几条消息队列。(一般情况下,打开1次设备,就会创建1个消息队列,不过一般情况下只会打开1次)

vdev:拥有此消息队列的设备。

ctrl_handler:其中牵扯很多其它的结构体,只需要了解这与控制元素相关就可以了。

prio:消息队列优先级。

wait:等待队列。由于可能有几个线程同时访问消息队列,所有等待消息队列可用的线程都会记录在该等待列表中。

subscribed:订阅消息链表。只有订阅的消息才能够queue到消息队列中,反之是无法queue到队列中的。

available:消息队列。可用的消息都以链表的形式保存在这里。

navailable:消息队列中可用消息的数量。

sequence:序列号。会一直自加下去,直到内核对象消亡。

上述几个结构体的组织结构大体如下所示:

注意:

<available>表示还没处理的消息。依然挂在链表中。
<subscribed>表示订阅的消息。单个订阅的消息仍然会保存几个子消息(毕竟一个消息可以发送多次)。

要特别注意消息queue进来时,先会保存到subscribed对应的订阅消息列表,然后才会链接到available链表上,上面蓝色和橙色之间的箭头不一定是一一对应的。具体为什么会保存多个自消息,可以继续了解一下订阅消息结构体。

1.4 struct v4l2_subscribed_event
订阅消息队列是以该结构体存在:

struct v4l2_subscribed_event {
	struct list_head	    list;
	u32			            type;
	u32			            id;
	u32			            flags;
	struct v4l2_fh		    *fh;
	struct list_head	    node;
	void	                (*replace)(struct v4l2_event *old, 
                                       const struct v4l2_event *new);
	void			        (*merge)(const struct v4l2_event *old, 
                                     struct v4l2_event *new);
	unsigned		        elems;
	unsigned		        first;
	unsigned		        in_use;
	struct v4l2_kevent	    events[];
};

list:将订阅消息插入到队列里的链接指针。
flags:一些标志位记录。
fh:可以理解成,该消息挂在哪个队列之上。
replace:替换消息的方法。当队列只能存放1个消息时,才会触发此方法。(此方法在队列已满以及使用struct v4l2_ctrl控制)
merge:当队列有多余2个容量时,会触发merge方法。(此方法在队列已满以及使用struct v4l2_ctrl控制)
elems:队列中能存放消息的最大数量。此变量在订阅消息下发过来时确定,然后会由内核分配对应的内存。
first:可以理解成游标指针,记录当前队列中入队时间最久且需要处理的消息。
in_use:入队的消息数量。
events:这里存放的只是一个指针,其大小由发起订阅时确定。
小结:此订阅消息队列,可以理解成一类消息的组合

二、Enqueue消息

这是消息入队的最重要的函数,此函数大概有如下几步:

1) 先检查消息是否在订阅消息队列中,不在的话,不允许该消息入队,反之允许入队。
2) 当该消息对应的订阅消息对象子消息满员之后,则将子消息队列中最老的那个替换掉,以留给新的消息。
3) 将新消息挂接到struct v4l2_fh可用链表中,以进行后续其它处理。

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;

	/* 这里注意:前面说过消息必须先订阅,才能queue进来,此处可以看到,
	 * 当检测到订阅消息列表中,没有当前消息,则直接return */
	sev = v4l2_event_subscribed(fh, ev->type, ev->id);
	if (sev == NULL)
		return;

	fh->sequence++;    // 消息序列号自动+1

	/* 显然当in_use和elems相等时,此时消息数组已经满员了,
	 * 此时需要将最旧的、没有处理的消息移除订阅消息队列 */
	if (sev->in_use == sev->elems) {
		// 队列中已经慢了,将第一个消息移除队列,腾出空间。
		kev = sev->events + sev_pos(sev, 0);
		list_del(&kev->list);
		sev->in_use--;        // 入队消息数目-1
		sev->first = sev_pos(sev, 1);     //将可用指针指向第2个消息实体
		fh->navailable--;      // 消息队列消息数量-1
		if (sev->elems == 1) {
		    /* 如果子消息数量只有1个,则替换该消息,
             * 注意由于都是同类消息,只需要改变状态即可 */
			if (sev->replace) {
				sev->replace(&kev->event, ev);
				copy_payload = false;
			}
		} else if (sev->merge) {
			struct v4l2_kevent *second_oldest;
            second_oldest = sev->events + sev_pos(sev, 0);
			sev->merge(&kev->event, &second_oldest->event);
		}
	}

	// 取出in_use所对应的消息实体,来存放具体的消息内容
	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->event.timestamp = *ts;
	kev->event.sequence = fh->sequence;
	sev->in_use++;        // 可用消息索引往后移动,相当于记录指针
	/* 上面已经把消息添加到订阅消息子消息列表中了,
     * 这里把消息再次插入具体设备的消息队列中 */
	list_add_tail(&kev->list, &fh->available);

	fh->navailable++;      // 需处理消息数量+1

	wake_up_all(&fh->wait);      // 唤醒所有等待消息的线程
}

下图是struct v4l2_subscribed_event结构体中的struct v4l2_kevent子消息在订阅消息结构体中的存放形态:

上面子消息队列中已经入队了2个,则 in_use = 2,而此时记录指针指向[1],则 first = 1,剩下的红色都是空闲子消息缓存。子消息的结构如下所示:

struct v4l2_kevent {
	struct list_head	          list;
	struct v4l2_subscribed_event  *sev;
	struct v4l2_event	          event;
};


三、Dequeue消息

dequeue就是消息出队(类似于取buffer时用VIDIOC_DQBUF),与enqueue消息相比,dequeue就简单多了,大概就如下几步:

1) 检查消息队列是否是为空,不为空则从中取出子消息。
2) 将上面子消息,从上面的消息队列中删除,并减少消息队列中消息的数量。
3) 将消息对应的订阅消息对象的记录指针后移,以及入队消息数量-1。

// 从参数中可以看到,v4l2_fh即消息队列,该队列被一个设备对象所拥有
static int 
__v4l2_event_dequeue(struct v4l2_fh *fh, struct v4l2_event *event) 
{
	struct v4l2_kevent *kev;
	unsigned long flags;

	spin_lock_irqsave(&fh->vdev->fh_lock, flags);
	
    // 判断队列中是否有可用的消息,没有就直接返回
	if (list_empty(&fh->available)) {
		spin_unlock_irqrestore(&fh->vdev->fh_lock, flags);
		return -ENOENT;
	}

	WARN_ON(fh->navailable == 0);

	// 从可用列表中取出第1个消息的入口,并将该消息从消息列表中删除
	kev = list_first_entry(&fh->available, struct v4l2_kevent, list);
	list_del(&kev->list);
	fh->navailable--;      // 消息数量-1

	// 将当前还需处理的消息列表,复制给刚才取出来的消息,以便它做其他操作
	kev->event.pending = fh->navailable;
	*event = kev->event;
	
    // 将消息对应的订阅消息对象中的记录指针向后移1个
	kev->sev->first = sev_pos(kev->sev, 1);
	kev->sev->in_use--;    // 入队消息数量-1

	spin_unlock_irqrestore(&fh->vdev->fh_lock, flags);

	return 0;
}

四、案例学习

以高通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已经实现了对应的接口。

// v4l2-ioctl.c
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;

	sev = v4l2_event_subscribed(fh, ev->type, ev->id);
	if (sev == NULL) {    // 如果订阅过的话,就不会进这里来
		return;
    }
}


五、总结

看了半天代码就总结一句话“消息必须先订阅,才允许入队列”。同时也需要知道下面这点:

订阅消息对象默认元素数量为1,如果设置的话就按设置的来分配内存。

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux中,使用V4L2来进行视频采集和处理。V4L2是Video4Linux2的简称,是一个内核框架,它提供了摄像头驱动程序的内核API,允许用户空间应用程序访问这些设备并进行配置。这里我们来介绍一种使用Linux V4L2-ctrl抓取单张图片的方法。 在这种情况下,用户可以使用V4L2控件库,通过V4L2控件库来控制摄像头设备驱动程序,来实现视频采集和图像截取的操作。 1. 安装V4L2-ctrl库 在Ubuntu中,可以使用以下命令安装V4L2-ctrl控件库: ``` sudo apt-get install libv4l-dev ``` 在其他Linux发行版中,也可以使用类似的命令进行安装。 2. 打开摄像头设备 使用以下命令打开摄像头设备: ``` $ v4l2-ctl --device=/dev/video0 --stream-mmap --stream-count=1 --stream-to=snapshot.raw ``` 以上命令将打开/dev/video0设备,并设置为内存映射流模式,采集一帧图像并将其保存到snapshot.raw文件中。 3. 转换图像 将采集到的原始图像转换为JPEG格式: ``` $ raw2jpeg snapshot.raw snapshot.jpeg ``` 这里使用了一个名为raw2jpeg的工具,它可以将原始图像数据转换为JPEG格式。 4. 查看抓取的图片 最后,可以使用图像查看器来查看抓取的JPEG图片。在Ubuntu中,可以使用以下命令来安装图像查看器: ``` sudo apt-get install eog ``` 完成安装后,可以使用eog来打开JPEG图像: ``` $ eog snapshot.jpeg ``` 这样,就可以使用Linux V4L2-ctrl抓取单张图片了。 总结:通过V4L2控件库,我们可以控制摄像头设备驱动程序进行视频采集和图像截取的操作。实现这个功能需要安装V4L2-ctrl库,打开摄像头设备,转换采集到的原始图像数据,并查看抓取的JPEG图片。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值