虚拟视频驱动程序vivi.c源码分析
2014-07-14 20:08
399 查看
虚拟视频驱动程序vivi.c源码分析
以下先把上一篇文章中的最后一段,放在这里利于程序源码的分析:
vivi.c 虚拟视频驱动程序----- 此代码模拟一个真正的视频设备V4L2 API (位于drivers/media/video目录下)
入口:+int __init vivi_init(void)
+ vivi_create_instance(i) /*创建设备*//**/。
+ 分配一个vivi_dev的结构体 /*它嵌套这结构体v4l2_device 和video_device*/
+ v4l2_device_register(NULL, &dev->v4l2_dev);/*注册vivi_dev中的V4l2_device*/
+ 初始化视频的DMA队列
+ 初始化锁
+ video_device_alloc(); 动态分配video_device结构体
+ 构建一个video_device结构体 vivi_template 并赋给上面分配的video_device
static struct video_device vivi_template = {
.name = "vivi",
.fops = &vivi_fops,
.ioctl_ops = &vivi_ioctl_ops,
.minor = -1,
.release = video_device_release,
.tvnorms = V4L2_STD_525_60,
.current_norm = V4L2_STD_NTSC_M,
};
+ video_set_drvdata(vfd, dev);设置驱动程序专有数据
+ 所有控件设置为其默认值
+ list_add_tail(&dev->vivi_devlist, &vivi_devlist);添加到设备列表
+ 构建 v4l2_file_operations 结构体vivi_fops 并实现.open .release .read .poll .mmap函数----- .ioctl 用标准的v4l2控制处理程序
+ 构建 v4l2_ioctl_ops结构体 vivi_ioctl_ops
static const struct v4l2_ioctl_ops vivi_ioctl_ops = {
.vidioc_querycap = vidioc_querycap,
.vidioc_enum_fmt_vid_cap = vidioc_enum_fmt_vid_cap,
.vidioc_try_fmt_vid_cap = vidioc_try_fmt_vid_cap,
.vidioc_s_fmt_vid_cap = vidioc_s_fmt_vid_cap,
.vidioc_reqbufs = vidioc_reqbufs,
.vidioc_querybuf = vidioc_querybuf,
.vidioc_qbuf = vidioc_qbuf,
.vidioc_dqbuf = vidioc_dqbuf,
.vidioc_s_std = vidioc_s_std,
.vidioc_enum_input = vidioc_enum_input,
.vidioc_g_input = vidioc_g_input,
.vidioc_s_input = vidioc_s_input,
.vidioc_queryctrl = vidioc_queryctrl,
.vidioc_g_ctrl = vidioc_g_ctrl,
.vidioc_s_ctrl = vidioc_s_ctrl,
.vidioc_streamon = vidioc_streamon,
.vidioc_streamoff = vidioc_streamoff,
#ifdef CONFIG_VIDEO_V4L1_COMPAT
.vidiocgmbuf = vidiocgmbuf,
#endif
};
+ int vivi_open(struct file *file)
+ vivi_dev *dev = video_drvdata(file); 访问驱动程序专用数据
+ 分配+初始化句柄(vivi_fh)数据
+ 重置帧计数器
+ videobuf_queue_vmalloc_init(); 初始化视频缓冲队列
+ 开启一个新线程用于开始和暂停
+ 实现自定义的v4l2_ioctl_ops 函数
现在开始分析程序源码,利于之后对V4L2驱动的开发,学习
首先就行驱动的入口开始:
static int __init vivi_init(void)
{
const struct font_desc *font = find_font("VGA8x16");
int ret = 0, i;
if (font == NULL) {
printk(KERN_ERR "vivi: could not find font\n");
return -ENODEV;
}
font8x16 = font->data;
if (n_devs <= 0)
n_devs = 1;
for (i = 0; i < n_devs; i++) {
//Here is the most important
ret = vivi_create_instance(i);
if (ret) {
/* If some instantiations succeeded, keep
driver */
if (i)
ret = 0;
break;
}
}
if (ret < 0) {
printk(KERN_ERR "vivi: error %d while loading driver\n", ret);
return ret;
}
printk(KERN_INFO "Video Technology Magazine Virtual Video "
"Capture Board ver %u.%u.%u successfully loaded.\n",
(VIVI_VERSION >> 16) & 0xFF, (VIVI_VERSION >> 8) & 0xFF,
VIVI_VERSION & 0xFF);
/* n_devs will reflect the actual number of allocated devices */
n_devs = i;
return ret;
}
static void __exit vivi_exit(void)
{
vivi_release();
}
module_init(vivi_init);
module_exit(vivi_exit);
这其实最重要的就是上面标注备份,下面重点分析vivi_create_instance方法:
static int __init vivi_create_instance(int inst)
{
struct vivi_dev *dev;
struct video_device *vfd;
struct v4l2_ctrl_handler *hdl;
struct vb2_queue *q;
int ret;
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
// set the v4l2_device(the
name)
snprintf(dev->v4l2_dev.name, sizeof(dev->v4l2_dev.name),
"%s-%03d", VIVI_MODULE_NAME, inst);
/*
* register the v4l2_device, but you should pay attention here
* the "dev == NULL" it means v4l2_device.dev == NULL
* You did't set the v4l2_device.dev, you
will set it later
*/
ret = v4l2_device_register(NULL, &dev->v4l2_dev);
if (ret)
goto free_dev;
/* init the handle, learn it later */
dev->fmt = &formats[0];
dev->width = 640;
dev->height = 480;
hdl = &dev->ctrl_handler;
v4l2_ctrl_handler_init(hdl, 11);
dev->volume = v4l2_ctrl_new_std(hdl, &vivi_ctrl_ops,
V4L2_CID_AUDIO_VOLUME, 0, 255, 1, 200);
dev->brightness = v4l2_ctrl_new_std(hdl, &vivi_ctrl_ops,
V4L2_CID_BRIGHTNESS, 0, 255, 1, 127);
dev->contrast = v4l2_ctrl_new_std(hdl, &vivi_ctrl_ops,
V4L2_CID_CONTRAST, 0, 255, 1, 16);
dev->saturation = v4l2_ctrl_new_std(hdl, &vivi_ctrl_ops,
V4L2_CID_SATURATION, 0, 255, 1, 127);
dev->hue = v4l2_ctrl_new_std(hdl, &vivi_ctrl_ops,
V4L2_CID_HUE, -128, 127, 1, 0);
dev->button = v4l2_ctrl_new_custom(hdl, &vivi_ctrl_button, NULL);
dev->int32 = v4l2_ctrl_new_custom(hdl, &vivi_ctrl_int32, NULL);
dev->int64 = v4l2_ctrl_new_custom(hdl, &vivi_ctrl_int64, NULL);
dev->boolean = v4l2_ctrl_new_custom(hdl, &vivi_ctrl_boolean, NULL);
dev->menu = v4l2_ctrl_new_custom(hdl, &vivi_ctrl_menu, NULL);
dev->string = v4l2_ctrl_new_custom(hdl, &vivi_ctrl_string, NULL);
if (hdl->error) {
ret = hdl->error;
goto unreg_dev;
}
dev->v4l2_dev.ctrl_handler = hdl;
/* initialize locks */
spin_lock_init(&dev->slock);
/* initialize queue, learn it later */
q = &dev->vb_vidq;
memset(q, 0, sizeof(dev->vb_vidq));
q->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
q->io_modes = VB2_MMAP | VB2_USERPTR | VB2_READ;
q->drv_priv = dev;
q->buf_struct_size = sizeof(struct
vivi_buffer);
q->ops = &vivi_video_qops;
q->mem_ops = &vb2_vmalloc_memops;
vb2_queue_init(q);
mutex_init(&dev->mutex);
/* init video dma queues */
INIT_LIST_HEAD(&dev->vidq.active);
init_waitqueue_head(&dev->vidq.wq);
/* before register the video_device, init the video_device data*/
ret = -ENOMEM;
vfd = video_device_alloc();
if (!vfd)
goto unreg_dev;
*vfd = vivi_template;/* the
most important struct */
vfd->debug = debug;
vfd->v4l2_dev = &dev->v4l2_dev; /* here set the
v4l2_device, you have already registered it */
set_bit(V4L2_FL_USE_FH_PRIO, &vfd->flags);
/*
* Provide a mutex to v4l2 core. It will be used to protect
* all fops and v4l2 ioctls.
*/
vfd->lock = &dev->mutex;
ret = video_register_device(vfd, VFL_TYPE_GRABBER, video_nr);
if (ret < 0)
goto rel_vdev;
/*
* You should pay attention to this method
* here you set the vivi_dev into the vedio_device for the later use in fops
* When you want to use the vivi_dev, you use vedio_get_drvdata() to get
*/
video_set_drvdata(vfd, dev);
/* Now that everything is fine, let's
add it to device list */
list_add_tail(&dev->vivi_devlist, &vivi_devlist);
if (video_nr != -1)
video_nr++;
dev->vfd = vfd;
/* the debug message*/
v4l2_info(&dev->v4l2_dev, "V4L2
device registered as %s\n",
video_device_node_name(vfd));
return 0;
rel_vdev:
video_device_release(vfd);
unreg_dev:
v4l2_ctrl_handler_free(hdl);
v4l2_device_unregister(&dev->v4l2_dev);
free_dev:
kfree(dev);
return ret;
}
vivi_create_instance方法中主要完成以下工作;
1.首先通过v4l2_device_register() 方法注册 v4l2_device
2.ctrl_handler初始化
3.互斥锁,自旋锁等初始化
4.vb2_quene初始化
5.init video dma queues
6.填充video_device,并且调用video_register_device注册video_device
7.把vivi_dev结构set进video_device中,方便之后使用,使用video_set_drvdata(vfd, dev);
8.设备信息加入的链表结构
下面针对以上步骤做详细分析:
1.首先通过v4l2_device_register() 方法注册 v4l2_device
int v4l2_device_register(struct device *dev, struct
v4l2_device *v4l2_dev)
{
if (v4l2_dev == NULL)
return -EINVAL;
INIT_LIST_HEAD(&v4l2_dev->subdevs);
spin_lock_init(&v4l2_dev->lock);
mutex_init(&v4l2_dev->ioctl_lock);
/* initial the global priotity*/
v4l2_prio_init(&v4l2_dev->prio);
kref_init(&v4l2_dev->ref);
v4l2_dev->dev = dev;
if (dev == NULL) {
/* If dev == NULL, then name
must be filled in by the caller */
WARN_ON(!v4l2_dev->name[0]);
/* Here give the caller a WARN, tell the caller to set the
dev*/
return 0;
}
/* Set name to driver
name + device name if it is empty. */
if (!v4l2_dev->name[0])
snprintf(v4l2_dev->name, sizeof(v4l2_dev->name), "%s
%s",
dev->driver->name, dev_name(dev));
/* Here is also very important, you
can get v4l2_device by use dev_get_drvdata*/
if (!dev_get_drvdata(dev))
dev_set_drvdata(dev, v4l2_dev);
return 0;
}
EXPORT_SYMBOL_GPL(v4l2_device_register);
在v4l2_device_register方法中,进行v4l2设备优先级的初始化,这里把v4l2_device中记录优先级状态的v4l2_prio_state结构变量prio清空,
另外这里说一下kref结构定义的ref变量,这个变量保存打开设备的计数,这里第一次注册设备,初始化ref为1,若之后重复注册则会先检查ref这个变量,
如果ref为1,则表示已经注册过了,避免重复注册v4l2_device,这个初始化在kref_init方法中实现,这是我的理解,可是我暂时还没有找到检查ref的地方,暂且跳过
最后一步根据dev 结构体决定,如果dev不为空,则这里setv4l2_device的name,并且将v4l2_device结构set进dev中,方便后面获取使用
2.ctrl_handler初始化
/* Initialize the handler */
int v4l2_ctrl_handler_init(struct v4l2_ctrl_handler *hdl,
unsigned nr_of_controls_hint)
{
mutex_init(&hdl->lock);
INIT_LIST_HEAD(&hdl->ctrls);
INIT_LIST_HEAD(&hdl->ctrl_refs);
hdl->nr_of_buckets = 1 + nr_of_controls_hint / 8;
hdl->buckets = kzalloc(sizeof(hdl->buckets[0]) * hdl->nr_of_buckets,
GFP_KERNEL);
hdl->error = hdl->buckets ? 0 : -ENOMEM;
return hdl->error;
}
这里还是很有必要了解一下v4l2_ctrl_handler这个结构体到底是干什么用的
/** struct v4l2_ctrl_handler - The
control handler keeps track of all the
* controls: both the controls owned by the handler and those inherited
* from other handlers.
* @lock: Lock to control access to this
handler and its controls.
* @ctrls: The list of controls owned by this handler.
* @ctrl_refs: The list of control references.
* @cached: The last found control reference. It is common
that the same
* control is needed multiple times, so this is a
simple
* optimization.
* @buckets: Buckets for the hashing. Allows for quick
control lookup.
* @nr_of_buckets: Total number of buckets in the array.
* @error: The error code
of the first failed control addition.
*/
struct v4l2_ctrl_handler {
struct mutex lock;
struct list_head ctrls;
struct list_head ctrl_refs;
struct v4l2_ctrl_ref *cached;
struct v4l2_ctrl_ref **buckets;
u16 nr_of_buckets;
int error;
};
在v4l2_ctrl_handler_init方法中,主要通过nr_of_controls_hint变量的大小,计算nr_of_buckets,并为buckets申请空间,并将申请结果保存在error变量中,我感觉可以是用于以后方便check的
dev->v4l2_dev.ctrl_handler = hdl;最后,关联vivi_dev
问题点:
1.v4l2_ctrl_new_std
2.v4l2_ctrl_new_custom
以上两个方法不是很理解,待以后研究
3.互斥锁,自旋锁等初始化
这个比较简单,就不在做说明了
4.vb2_quene初始化
首先还是很有必要看一下这个结构体
/**
* struct vb2_queue - a videobuf queue
*
* @type: queue type (see V4L2_BUF_TYPE_* in linux/videodev2.h
* @io_modes: supported io methods (see vb2_io_modes enum)
* @io_flags: additional io flags (see vb2_fileio_flags enum)
* @ops: driver-specific callbacks
* @mem_ops: memory allocator specific callbacks
* @drv_priv: driver private data
* @buf_struct_size: size of the driver-specific buffer structure;
* "0" indicates the driver doesn't want to use
a custom buffer
* structure type, so sizeof(struct vb2_buffer) will is used
*
* @memory: current memory type used
* @bufs: videobuf buffer structures
* @num_buffers: number of allocated/used buffers
* @queued_list: list of buffers currently queued from userspace
* @queued_count: number of buffers owned by the driver
* @done_list: list of buffers ready to be dequeued to userspace
* @done_lock: lock to protect done_list list
* @done_wq: waitqueue for processes waiting for buffers
ready to be dequeued
* @alloc_ctx: memory type/allocator-specific
contexts for each plane
* @streaming: current streaming state
* @fileio: file io emulator internal data, used only if emulator is active
*/
struct vb2_queue {
enum v4l2_buf_type type;
unsigned int io_modes;
unsigned int io_flags;
const struct vb2_ops *ops;
const struct vb2_mem_ops *mem_ops;
void *drv_priv;
unsigned int buf_struct_size;
/* private: internal
use only */
enum v4l2_memory memory;
struct vb2_buffer *bufs[VIDEO_MAX_FRAME];
unsigned int num_buffers;
struct list_head queued_list;
atomic_t queued_count;
struct list_head done_list;
spinlock_t done_lock;
wait_queue_head_t done_wq;
void *alloc_ctx[VIDEO_MAX_PLANES];
unsigned int streaming:1;
struct vb2_fileio_data *fileio;
};
在v4l2_ctrl_handler_init方法中,首先对vb2_quene其中的重要数据进行填充,最最重要的就是
q->ops = &vivi_video_qops;
q->mem_ops = &vb2_vmalloc_memops;
这两条简单的复制语句责任重大啊,这里先知道有这么一回事,后面补充
/**
* struct vb2_ops - driver-specific callbacks
*
* @queue_setup: called from a VIDIOC_REQBUFS handler, before
* memory allocation; driver should return the required
* number of buffers in num_buffers, the required number
* of planes per buffer in num_planes; the size of each
* plane should be set in the sizes[] array and optional
* per-plane allocator specific context in alloc_ctxs[]
* array
* @wait_prepare: release any locks taken while calling vb2 functions;
* it is called before an ioctl needs to wait for a
new
* buffer to arrive; required to avoid
a deadlock in
* blocking access type
* @wait_finish: reacquire all locks released in the previous callback;
* required to continue operation after sleeping while
* waiting for a new buffer to arrive
* @buf_init: called once after allocating a buffer (in MMAP case)
* or after acquiring a new USERPTR buffer; drivers may
* perform additional buffer-related initialization;
* initialization failure (return != 0) will
prevent
* queue setup from completing successfully; optional
* @buf_prepare: called every time the buffer is queued
from userspace;
* drivers may perform any initialization required before
* each hardware operation in this callback;
* if an error is returned, the
buffer will not be queued
* in driver; optional
* @buf_finish: called before every dequeue of the buffer back to
* userspace; drivers may perform any operations required
* before userspace accesses the buffer; optional
* @buf_cleanup: called once before the buffer is freed; drivers
may
* perform any additional cleanup; optional
* @start_streaming: called once before entering 'streaming' state; enables
* driver to receive buffers over buf_queue() callback
* @stop_streaming: called when 'streaming' state
must be disabled; driver
* should stop any DMA transactions or wait until they
* finish and give back all buffers it got from buf_queue()
* callback; may use vb2_wait_for_all_buffers() function
* @buf_queue: passes buffer vb to the driver; driver
may start
* hardware operation on this buffer; driver should give
* the buffer back by calling vb2_buffer_done() function
*/
struct vb2_ops {
int (*queue_setup)(struct
vb2_queue *q, unsigned int *num_buffers,
unsigned int *num_planes, unsigned long sizes[],
void *alloc_ctxs[]);
void (*wait_prepare)(struct
vb2_queue *q);
void (*wait_finish)(struct
vb2_queue *q);
int (*buf_init)(struct
vb2_buffer *vb);
int (*buf_prepare)(struct
vb2_buffer *vb);
int (*buf_finish)(struct
vb2_buffer *vb);
void (*buf_cleanup)(struct
vb2_buffer *vb);
int (*start_streaming)(struct
vb2_queue *q);
int (*stop_streaming)(struct
vb2_queue *q);
void (*buf_queue)(struct
vb2_buffer *vb);
};
/**
* struct vb2_mem_ops - memory handling/memory allocator operations
* @alloc: allocate video memory and, optionally, allocator private data,
* return NULL on failure or a
pointer to allocator private,
* per-buffer data on success; the
returned private structure
* will then be passed as buf_priv argument to other ops in this
* structure
* @put: inform the allocator that the buffer will no longer be used;
* usually will result in the allocator freeing the buffer (if
* no other users of this buffer are present); the buf_priv
* argument is the allocator private per-buffer
structure
* previously returned from the alloc callback
* @get_userptr: acquire userspace memory for a hardware operation; used for
* USERPTR memory types; vaddr is the address passed to the
* videobuf layer when queuing a video buffer of USERPTR type;
* should return an allocator private per-buffer structure
* associated with the buffer on success, NULL on failure;
* the returned private structure will then be passed as buf_priv
* argument to other ops in this structure
* @put_userptr: inform the allocator that a USERPTR buffer will no longer
* be used
* @vaddr: return a kernel virtual address to a given memory buffer
* associated with the passed private structure or NULL if no
* such mapping exists
* @cookie: return allocator specific cookie for a given memory buffer
* associated with the passed private structure or NULL if not
* available
* @num_users: return the current number of users of a memory buffer;
* return 1 if the videobuf layer (or actually
the driver using
* it) is the only user
* @mmap: setup a userspace mapping for a given memory buffer under
* the provided virtual memory region
*
* Required ops for USERPTR types: get_userptr, put_userptr.
* Required ops for MMAP types: alloc, put, num_users, mmap.
* Required ops for read/write access types: alloc, put, num_users, vaddr
*/
struct vb2_mem_ops {
void *(*alloc)(void *alloc_ctx, unsigned
long size);
void (*put)(void *buf_priv);
void *(*get_userptr)(void *alloc_ctx, unsigned
long vaddr,
unsigned long size, int write);
void (*put_userptr)(void *buf_priv);
void *(*vaddr)(void *buf_priv);
void *(*cookie)(void *buf_priv);
unsigned int (*num_users)(void *buf_priv);
int (*mmap)(void *buf_priv, struct
vm_area_struct *vma);
};
最后调用vb2_queue_init方法,进行初始化
/**
* vb2_queue_init() - initialize
a videobuf2 queue
* @q: videobuf2 queue; this structure should be allocated in driver
*
* The vb2_queue structure should be allocated by the driver. The driver is
* responsible of clearing it's content and setting initial values for some
* required entries before calling this function.
* q->ops, q->mem_ops, q->type and q->io_modes
are mandatory. Please refer
* to the struct vb2_queue description in include/media/videobuf2-core.h
* for more information.
*/
int vb2_queue_init(struct vb2_queue *q)
{
BUG_ON(!q);
BUG_ON(!q->ops);
BUG_ON(!q->mem_ops);
BUG_ON(!q->type);
BUG_ON(!q->io_modes);
BUG_ON(!q->ops->queue_setup);
BUG_ON(!q->ops->buf_queue);
INIT_LIST_HEAD(&q->queued_list);
INIT_LIST_HEAD(&q->done_list);
spin_lock_init(&q->done_lock);
init_waitqueue_head(&q->done_wq);
if (q->buf_struct_size == 0)
q->buf_struct_size = sizeof(struct
vb2_buffer);
return 0;
}
EXPORT_SYMBOL_GPL(vb2_queue_init);
这里方法很简单,只是进行check,然后最最简单的初始化,这里不再多说了
5.init
video dma queues
只有两条语句进行初始化
INIT_LIST_HEAD(&dev->vidq.active);
init_waitqueue_head(&dev->vidq.wq);
6.填充video_device,并且调用video_register_device注册video_device
这里终于到了重点了,很重要,开始了
开始简单,申请内存空间并进行填充,然后才真正调用用video_register_device这个方法,开始了
但是在调用之前的这条语句你必须关注vfd->v4l2_dev = &dev->v4l2_dev;从这里也可以知道v4l2_device和video_device的注册顺序
int __video_register_device(struct video_device *vdev, int type, int nr,
int warn_if_nr_in_use, struct module *owner)
{
int i = 0;
int ret;
int minor_offset = 0;
int minor_cnt = VIDEO_NUM_DEVICES;
const char *name_base;
/* A minor value of -1 marks this video device as never
having been registered */
vdev->minor = -1;
/* the release callback MUST be present */
WARN_ON(!vdev->release);
if (!vdev->release)
return -EINVAL;
/* v4l2_fh support */
spin_lock_init(&vdev->fh_lock);
INIT_LIST_HEAD(&vdev->fh_list);
/* Part 1: check device type */
/* after here, you can see videx ...the
char device in /dev */
//这里还是单独说一下吧,最终你在/dev目录下看到的video0就是在这里决定的,大家可以知道,可不是一定名字叫video的
switch (type) {
case VFL_TYPE_GRABBER:
name_base = "video";
break;
case VFL_TYPE_VBI:
name_base = "vbi";
break;
case VFL_TYPE_RADIO:
name_base = "radio";
break;
case VFL_TYPE_SUBDEV:
name_base = "v4l-subdev";
break;
default:
printk(KERN_ERR "%s called with unknown type: %d\n",
__func__, type);
return -EINVAL;
}
vdev->vfl_type = type;
vdev->cdev = NULL;
if (vdev->v4l2_dev) {
if (vdev->v4l2_dev->dev)
//这里说明vdev和保存在v4l2_device的dev具有共同的parent,对之后sys 接口那里有用
vdev->parent = vdev->v4l2_dev->dev;
if (vdev->ctrl_handler == NULL)
//上面初始化的ctrl_handler在这里要派上用处了,而且同时指向video_device和v4l2_device的化ctrl_handler
vdev->ctrl_handler = vdev->v4l2_dev->ctrl_handler;
/* If the prio state pointer is NULL, then use
the v4l2_device
prio state. */
if (vdev->prio == NULL)
//上面初始化的prio在这里要派上用处了,而且同时指向video_device和v4l2_device的化prio
vdev->prio = &vdev->v4l2_dev->prio;
}
//从这里往下挺长一段代码是在为要申请的字符设备寻找一个合适的设备号,这里不去深入追究了,有时间可以可虑回来看看
/* Part 2: find a free minor, device
node number and device index. */
#ifdef CONFIG_VIDEO_FIXED_MINOR_RANGES
/* Keep the ranges for the first four types for historical
* reasons.
* Newer devices (not yet in place) should
use the range
* of 128-191 and just pick the first free minor there
* (new style). */
switch (type) {
case VFL_TYPE_GRABBER:
minor_offset = 0;
minor_cnt = 64;
break;
case VFL_TYPE_RADIO:
minor_offset = 64;
minor_cnt = 64;
break;
case VFL_TYPE_VBI:
minor_offset = 224;
minor_cnt = 32;
break;
default:
minor_offset = 128;
minor_cnt = 64;
break;
}
#endif
/* Pick a device node number */
mutex_lock(&videodev_lock);
nr = devnode_find(vdev, nr == -1 ? 0 : nr, minor_cnt);
if (nr == minor_cnt)
nr = devnode_find(vdev, 0, minor_cnt);
if (nr == minor_cnt) {
printk(KERN_ERR "could not get a free device node number\n");
mutex_unlock(&videodev_lock);
return -ENFILE;
}
#ifdef CONFIG_VIDEO_FIXED_MINOR_RANGES
/* 1-on-1
mapping of device node number to minor number */
i = nr;
#else
/* The device node number and minor numbers are independent, so
we just find the first free minor number. */
for (i = 0; i < VIDEO_NUM_DEVICES; i++)
if (video_device[i] == NULL)
break;
if (i == VIDEO_NUM_DEVICES) {
mutex_unlock(&videodev_lock);
printk(KERN_ERR "could not get a free minor\n");
return -ENFILE;
}
#endif
vdev->minor = i + minor_offset;
vdev->num = nr;
devnode_set(vdev);
/* Should not happen since we thought this minor was free */
WARN_ON(video_device[vdev->minor] != NULL);
vdev->index = get_index(vdev);
mutex_unlock(&videodev_lock);
//上面的方法获取到了那个合适的设备号,现在要开始注册我们的字符设备了
/* Part 3: Initialize the character device */
vdev->cdev = cdev_alloc();
if (vdev->cdev == NULL) {
ret = -ENOMEM;
goto cleanup;
}
vdev->cdev->ops = &v4l2_fops;//most
important part,操作设备的通道
vdev->cdev->owner = owner;
ret = cdev_add(vdev->cdev, MKDEV(VIDEO_MAJOR, vdev->minor), 1);
if (ret < 0) {
printk(KERN_ERR "%s: cdev_add failed\n", __func__);
kfree(vdev->cdev);
vdev->cdev = NULL;
goto cleanup;
}
//这里我们也大可先不用关注,主要是在sysfs的一些设备添加等等
/* Part 4: register the device with sysfs */
vdev->dev.class = &video_class;
vdev->dev.devt = MKDEV(VIDEO_MAJOR, vdev->minor);
if (vdev->parent)
vdev->dev.parent = vdev->parent;
dev_set_name(&vdev->dev, "%s%d", name_base, vdev->num);
ret = device_register(&vdev->dev);
if (ret < 0) {
printk(KERN_ERR "%s: device_register failed\n", __func__);
goto cleanup;
}
/* Register the release callback that will be called when the last
reference to the device goes away. */
vdev->dev.release = v4l2_device_release;
if (nr != -1 && nr != vdev->num && warn_if_nr_in_use)
printk(KERN_WARNING "%s: requested %s%d, got %s\n", __func__,
name_base, nr, video_device_node_name(vdev));
/* Increase v4l2_device refcount */
if (vdev->v4l2_dev)
v4l2_device_get(vdev->v4l2_dev);
#if defined(CONFIG_MEDIA_CONTROLLER)
//这里其实还是比较重要的,不过不是所以的驱动都要添加这一个步骤,这也是为什么有一个if define
的原因了
//意思就是如果这个驱动中需要用到media controler的时候就需要在这里注册media_device
//这里同样先不做深入研究,media_device和media_entity这两个重要结构体之后还要研究
/* Part 5: Register the entity. */
if (vdev->v4l2_dev && vdev->v4l2_dev->mdev &&
vdev->vfl_type != VFL_TYPE_SUBDEV) {
vdev->entity.type = MEDIA_ENT_T_DEVNODE_V4L;
vdev->entity.name = vdev->name;
vdev->entity.v4l.major = VIDEO_MAJOR;
vdev->entity.v4l.minor = vdev->minor;
ret = media_device_register_entity(vdev->v4l2_dev->mdev,
&vdev->entity);
if (ret < 0)
printk(KERN_WARNING
"%s: media_device_register_entity failed\n",
__func__);
}
#endif
//保存注册成功标记,并将注册成功的video_device保存到全局数组video_device中,大功告成
/* Part 6: Activate this minor. The
char device can now be used. */
set_bit(V4L2_FL_REGISTERED, &vdev->flags);//设置标志位,之后还会遇到test_bit方法用来check
flags的第nr位是否为1.
//这里还是多说一点,另外还有两中标志位需要知道:V4L2_FL_USES_V4L2_FH, V4L2_FL_USE_FH_PRIO
mutex_lock(&videodev_lock);
video_device[vdev->minor] = vdev;
mutex_unlock(&videodev_lock);
return 0;
//这里是出错处理函数
cleanup:
mutex_lock(&videodev_lock);
if (vdev->cdev)
cdev_del(vdev->cdev);
devnode_clear(vdev);
mutex_unlock(&videodev_lock);
/* Mark this video device as never having been registered. */
vdev->minor = -1;
return ret;
}
EXPORT_SYMBOL(__video_register_device);
7.把vivi_dev结构set进video_device中,方便之后使用,设备信息加入的链表结构
最后结尾的这段代码这里我决定单独放在下面分析,也算妥善收尾吧
/*
* You should pay attention to this method
* here you set the vivi_dev into the vedio_device for the later use in fops
* When you want to use the vivi_dev, you use vedio_get_drvdata() to get
*/
video_set_drvdata(vfd, dev);
/* Now that everything is fine, let's
add it to device list */
list_add_tail(&dev->vivi_devlist, &vivi_devlist);//添加的device
list当中
if (video_nr != -1)
video_nr++;//用于计数,找到设备
dev->vfd = vfd;关联video_device和vivi_dev
短短的几条语句,但我看来,个个都短小精悍
首先说说这个方法,他有什么用处呢?其实用处可大了,就行我上面注释的那样,这里把vivi_dev设置到vedio_device中,是为了之后字符设备访问接口中使用
这里多说一点,也算顺便说一下用户空间操作设备的流程了
首先当时用户空间访问设备了,这个做驱动的不懂那可糗大了,用户空间open时,也就是启动了上面video_device_register方法中的很重要的下面结构中的open方法
static const struct file_operations v4l2_fops = {
.owner = THIS_MODULE,
.read = v4l2_read,
.write = v4l2_write,
.open = v4l2_open,
.get_unmapped_area = v4l2_get_unmapped_area,
.mmap = v4l2_mmap,
.unlocked_ioctl = v4l2_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = v4l2_compat_ioctl32,
#endif
.release = v4l2_release,
.poll = v4l2_poll,
.llseek = no_llseek,
};
我们来看一下这个open方法
/* Override for the
open function */
static int v4l2_open(struct inode *inode, struct
file *filp)
{
struct video_device *vdev;
int ret = 0;
/* Check if the video device is available */
mutex_lock(&videodev_lock);
vdev = video_devdata(filp);
/* return ENODEV if the video device has already been removed. */
if (vdev == NULL || !video_is_registered(vdev)) {
mutex_unlock(&videodev_lock);
return -ENODEV;
}
/* and increase the device refcount */
video_get(vdev);//这里是用来计数的
mutex_unlock(&videodev_lock);
/*
* Here using the API you get the method you get the open() method
write
* The other methods in fops use the same method to use you own code
*/
if (vdev->fops->open) {
if (vdev->lock && mutex_lock_interruptible(vdev->lock)) {
ret = -ERESTARTSYS;
goto err;
}
if (video_is_registered(vdev))
ret = vdev->fops->open(filp);
else
ret = -ENODEV;
if (vdev->lock)
mutex_unlock(vdev->lock);
}
err:
/* decrease the refcount in case of
an error */
if (ret)
video_put(vdev);
return ret;
}
只有最下面的那个标准才是重点,经过那么多的check,最后走的了最后这一步,ret = vdev->fops->open(filp);
这个open方法在哪里呢?那就沿着箭头方向找吧,是video_device内部的fops中的open方法,这个方法不是有在哪里呢?
接着找,在video_device_register方法之前
*vfd
= vivi_template;/* the most important struct */我还特意在这里写了最重要的结构体
所以最终调用的是vivi_template中fops中定义的open方法
static const struct v4l2_file_operations vivi_fops = {
.owner = THIS_MODULE,
.open = v4l2_fh_open,
.release = vivi_close,
.read = vivi_read,
.poll = vivi_poll,
.unlocked_ioctl = video_ioctl2, /* V4L2
ioctl handler */
.mmap = vivi_mmap,
};
static const struct v4l2_ioctl_ops vivi_ioctl_ops = {
.vidioc_querycap = vidioc_querycap,
.vidioc_enum_fmt_vid_cap = vidioc_enum_fmt_vid_cap,
.vidioc_g_fmt_vid_cap = vidioc_g_fmt_vid_cap,
.vidioc_try_fmt_vid_cap = vidioc_try_fmt_vid_cap,
.vidioc_s_fmt_vid_cap = vidioc_s_fmt_vid_cap,
.vidioc_reqbufs = vidioc_reqbufs,
.vidioc_querybuf = vidioc_querybuf,
.vidioc_qbuf = vidioc_qbuf,
.vidioc_dqbuf = vidioc_dqbuf,
.vidioc_s_std = vidioc_s_std,
.vidioc_enum_input = vidioc_enum_input,
.vidioc_g_input = vidioc_g_input,
.vidioc_s_input = vidioc_s_input,
.vidioc_streamon = vidioc_streamon,
.vidioc_streamoff = vidioc_streamoff,
};
static struct video_device vivi_template = {
.name = "vivi",
.fops = &vivi_fops,
.ioctl_ops = &vivi_ioctl_ops,
.release = video_device_release,
.tvnorms = V4L2_STD_525_60,
.current_norm = V4L2_STD_NTSC_M,
};
我们找到了,就是v4l2_fh_open这个方法,这个方法与我们之前写字符驱动时遇到的情况很是不同,他的open方法其实是有内核API写好的,我们先看看
int v4l2_fh_open(struct file *filp)
{
struct video_device *vdev = video_devdata(filp);
struct v4l2_fh *fh = kzalloc(sizeof(*fh), GFP_KERNEL);
/*
* IN the open method, do only
one job
* set v4l2_fh into filp->private_data for later
use, and initial v4l2_fh
*/
filp->private_data = fh;
if (fh == NULL)
return -ENOMEM;
v4l2_fh_init(fh, vdev);
v4l2_fh_add(fh);
return 0;
}
EXPORT_SYMBOL_GPL(v4l2_fh_open);
这个open方法将v4l2_fh 这个同样很重要的结构体保存到filp->private中,并做一些初始化,具体过程暂且不说
这里fops中的其他接口的实现比起open,方法是一样的,而且更简单
但是,这里我真正想表达的东西还没有出现,大家看到这里的filp->private_data = fh;//问题就在这里了,我们真正在read,write中需要传递的想要使用的是vivi_dev其实,
而不是v4l2_fh这个数据结构,我们还是直接看看vivi_template中fops中定义的read方法吧
static ssize_t
vivi_read(struct file *file, char __user *data, size_t
count, loff_t *ppos)
{
struct vivi_dev *dev = video_drvdata(file);
dprintk(dev, 1, "read
called\n");
return vb2_read(&dev->vb_vidq, data, count, ppos,
file->f_flags & O_NONBLOCK);
}
这里大家看到了,方法获取到vivi_dev这个数据结构,并传递到vb2_read这个方法中,这个方法时驱动中数据传输的关键,之后再慢慢研究,这里同样不深究
而这个vivi_dev是通过video_drvdata方法获得的,这就是为什么在上面我强调的要用
video_set_drvdata(vfd, dev);把vivi_dev装载到video_device中
到这里基本的过程都整理完了,其实大头还在后面呢,待续。。。。。。
以下先把上一篇文章中的最后一段,放在这里利于程序源码的分析:
vivi.c 虚拟视频驱动程序----- 此代码模拟一个真正的视频设备V4L2 API (位于drivers/media/video目录下)
入口:+int __init vivi_init(void)
+ vivi_create_instance(i) /*创建设备*//**/。
+ 分配一个vivi_dev的结构体 /*它嵌套这结构体v4l2_device 和video_device*/
+ v4l2_device_register(NULL, &dev->v4l2_dev);/*注册vivi_dev中的V4l2_device*/
+ 初始化视频的DMA队列
+ 初始化锁
+ video_device_alloc(); 动态分配video_device结构体
+ 构建一个video_device结构体 vivi_template 并赋给上面分配的video_device
static struct video_device vivi_template = {
.name = "vivi",
.fops = &vivi_fops,
.ioctl_ops = &vivi_ioctl_ops,
.minor = -1,
.release = video_device_release,
.tvnorms = V4L2_STD_525_60,
.current_norm = V4L2_STD_NTSC_M,
};
+ video_set_drvdata(vfd, dev);设置驱动程序专有数据
+ 所有控件设置为其默认值
+ list_add_tail(&dev->vivi_devlist, &vivi_devlist);添加到设备列表
+ 构建 v4l2_file_operations 结构体vivi_fops 并实现.open .release .read .poll .mmap函数----- .ioctl 用标准的v4l2控制处理程序
+ 构建 v4l2_ioctl_ops结构体 vivi_ioctl_ops
static const struct v4l2_ioctl_ops vivi_ioctl_ops = {
.vidioc_querycap = vidioc_querycap,
.vidioc_enum_fmt_vid_cap = vidioc_enum_fmt_vid_cap,
.vidioc_try_fmt_vid_cap = vidioc_try_fmt_vid_cap,
.vidioc_s_fmt_vid_cap = vidioc_s_fmt_vid_cap,
.vidioc_reqbufs = vidioc_reqbufs,
.vidioc_querybuf = vidioc_querybuf,
.vidioc_qbuf = vidioc_qbuf,
.vidioc_dqbuf = vidioc_dqbuf,
.vidioc_s_std = vidioc_s_std,
.vidioc_enum_input = vidioc_enum_input,
.vidioc_g_input = vidioc_g_input,
.vidioc_s_input = vidioc_s_input,
.vidioc_queryctrl = vidioc_queryctrl,
.vidioc_g_ctrl = vidioc_g_ctrl,
.vidioc_s_ctrl = vidioc_s_ctrl,
.vidioc_streamon = vidioc_streamon,
.vidioc_streamoff = vidioc_streamoff,
#ifdef CONFIG_VIDEO_V4L1_COMPAT
.vidiocgmbuf = vidiocgmbuf,
#endif
};
+ int vivi_open(struct file *file)
+ vivi_dev *dev = video_drvdata(file); 访问驱动程序专用数据
+ 分配+初始化句柄(vivi_fh)数据
+ 重置帧计数器
+ videobuf_queue_vmalloc_init(); 初始化视频缓冲队列
+ 开启一个新线程用于开始和暂停
+ 实现自定义的v4l2_ioctl_ops 函数
现在开始分析程序源码,利于之后对V4L2驱动的开发,学习
首先就行驱动的入口开始:
static int __init vivi_init(void)
{
const struct font_desc *font = find_font("VGA8x16");
int ret = 0, i;
if (font == NULL) {
printk(KERN_ERR "vivi: could not find font\n");
return -ENODEV;
}
font8x16 = font->data;
if (n_devs <= 0)
n_devs = 1;
for (i = 0; i < n_devs; i++) {
//Here is the most important
ret = vivi_create_instance(i);
if (ret) {
/* If some instantiations succeeded, keep
driver */
if (i)
ret = 0;
break;
}
}
if (ret < 0) {
printk(KERN_ERR "vivi: error %d while loading driver\n", ret);
return ret;
}
printk(KERN_INFO "Video Technology Magazine Virtual Video "
"Capture Board ver %u.%u.%u successfully loaded.\n",
(VIVI_VERSION >> 16) & 0xFF, (VIVI_VERSION >> 8) & 0xFF,
VIVI_VERSION & 0xFF);
/* n_devs will reflect the actual number of allocated devices */
n_devs = i;
return ret;
}
static void __exit vivi_exit(void)
{
vivi_release();
}
module_init(vivi_init);
module_exit(vivi_exit);
这其实最重要的就是上面标注备份,下面重点分析vivi_create_instance方法:
static int __init vivi_create_instance(int inst)
{
struct vivi_dev *dev;
struct video_device *vfd;
struct v4l2_ctrl_handler *hdl;
struct vb2_queue *q;
int ret;
dev = kzalloc(sizeof(*dev), GFP_KERNEL);
if (!dev)
return -ENOMEM;
// set the v4l2_device(the
name)
snprintf(dev->v4l2_dev.name, sizeof(dev->v4l2_dev.name),
"%s-%03d", VIVI_MODULE_NAME, inst);
/*
* register the v4l2_device, but you should pay attention here
* the "dev == NULL" it means v4l2_device.dev == NULL
* You did't set the v4l2_device.dev, you
will set it later
*/
ret = v4l2_device_register(NULL, &dev->v4l2_dev);
if (ret)
goto free_dev;
/* init the handle, learn it later */
dev->fmt = &formats[0];
dev->width = 640;
dev->height = 480;
hdl = &dev->ctrl_handler;
v4l2_ctrl_handler_init(hdl, 11);
dev->volume = v4l2_ctrl_new_std(hdl, &vivi_ctrl_ops,
V4L2_CID_AUDIO_VOLUME, 0, 255, 1, 200);
dev->brightness = v4l2_ctrl_new_std(hdl, &vivi_ctrl_ops,
V4L2_CID_BRIGHTNESS, 0, 255, 1, 127);
dev->contrast = v4l2_ctrl_new_std(hdl, &vivi_ctrl_ops,
V4L2_CID_CONTRAST, 0, 255, 1, 16);
dev->saturation = v4l2_ctrl_new_std(hdl, &vivi_ctrl_ops,
V4L2_CID_SATURATION, 0, 255, 1, 127);
dev->hue = v4l2_ctrl_new_std(hdl, &vivi_ctrl_ops,
V4L2_CID_HUE, -128, 127, 1, 0);
dev->button = v4l2_ctrl_new_custom(hdl, &vivi_ctrl_button, NULL);
dev->int32 = v4l2_ctrl_new_custom(hdl, &vivi_ctrl_int32, NULL);
dev->int64 = v4l2_ctrl_new_custom(hdl, &vivi_ctrl_int64, NULL);
dev->boolean = v4l2_ctrl_new_custom(hdl, &vivi_ctrl_boolean, NULL);
dev->menu = v4l2_ctrl_new_custom(hdl, &vivi_ctrl_menu, NULL);
dev->string = v4l2_ctrl_new_custom(hdl, &vivi_ctrl_string, NULL);
if (hdl->error) {
ret = hdl->error;
goto unreg_dev;
}
dev->v4l2_dev.ctrl_handler = hdl;
/* initialize locks */
spin_lock_init(&dev->slock);
/* initialize queue, learn it later */
q = &dev->vb_vidq;
memset(q, 0, sizeof(dev->vb_vidq));
q->type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
q->io_modes = VB2_MMAP | VB2_USERPTR | VB2_READ;
q->drv_priv = dev;
q->buf_struct_size = sizeof(struct
vivi_buffer);
q->ops = &vivi_video_qops;
q->mem_ops = &vb2_vmalloc_memops;
vb2_queue_init(q);
mutex_init(&dev->mutex);
/* init video dma queues */
INIT_LIST_HEAD(&dev->vidq.active);
init_waitqueue_head(&dev->vidq.wq);
/* before register the video_device, init the video_device data*/
ret = -ENOMEM;
vfd = video_device_alloc();
if (!vfd)
goto unreg_dev;
*vfd = vivi_template;/* the
most important struct */
vfd->debug = debug;
vfd->v4l2_dev = &dev->v4l2_dev; /* here set the
v4l2_device, you have already registered it */
set_bit(V4L2_FL_USE_FH_PRIO, &vfd->flags);
/*
* Provide a mutex to v4l2 core. It will be used to protect
* all fops and v4l2 ioctls.
*/
vfd->lock = &dev->mutex;
ret = video_register_device(vfd, VFL_TYPE_GRABBER, video_nr);
if (ret < 0)
goto rel_vdev;
/*
* You should pay attention to this method
* here you set the vivi_dev into the vedio_device for the later use in fops
* When you want to use the vivi_dev, you use vedio_get_drvdata() to get
*/
video_set_drvdata(vfd, dev);
/* Now that everything is fine, let's
add it to device list */
list_add_tail(&dev->vivi_devlist, &vivi_devlist);
if (video_nr != -1)
video_nr++;
dev->vfd = vfd;
/* the debug message*/
v4l2_info(&dev->v4l2_dev, "V4L2
device registered as %s\n",
video_device_node_name(vfd));
return 0;
rel_vdev:
video_device_release(vfd);
unreg_dev:
v4l2_ctrl_handler_free(hdl);
v4l2_device_unregister(&dev->v4l2_dev);
free_dev:
kfree(dev);
return ret;
}
vivi_create_instance方法中主要完成以下工作;
1.首先通过v4l2_device_register() 方法注册 v4l2_device
2.ctrl_handler初始化
3.互斥锁,自旋锁等初始化
4.vb2_quene初始化
5.init video dma queues
6.填充video_device,并且调用video_register_device注册video_device
7.把vivi_dev结构set进video_device中,方便之后使用,使用video_set_drvdata(vfd, dev);
8.设备信息加入的链表结构
下面针对以上步骤做详细分析:
1.首先通过v4l2_device_register() 方法注册 v4l2_device
int v4l2_device_register(struct device *dev, struct
v4l2_device *v4l2_dev)
{
if (v4l2_dev == NULL)
return -EINVAL;
INIT_LIST_HEAD(&v4l2_dev->subdevs);
spin_lock_init(&v4l2_dev->lock);
mutex_init(&v4l2_dev->ioctl_lock);
/* initial the global priotity*/
v4l2_prio_init(&v4l2_dev->prio);
kref_init(&v4l2_dev->ref);
v4l2_dev->dev = dev;
if (dev == NULL) {
/* If dev == NULL, then name
must be filled in by the caller */
WARN_ON(!v4l2_dev->name[0]);
/* Here give the caller a WARN, tell the caller to set the
dev*/
return 0;
}
/* Set name to driver
name + device name if it is empty. */
if (!v4l2_dev->name[0])
snprintf(v4l2_dev->name, sizeof(v4l2_dev->name), "%s
%s",
dev->driver->name, dev_name(dev));
/* Here is also very important, you
can get v4l2_device by use dev_get_drvdata*/
if (!dev_get_drvdata(dev))
dev_set_drvdata(dev, v4l2_dev);
return 0;
}
EXPORT_SYMBOL_GPL(v4l2_device_register);
在v4l2_device_register方法中,进行v4l2设备优先级的初始化,这里把v4l2_device中记录优先级状态的v4l2_prio_state结构变量prio清空,
另外这里说一下kref结构定义的ref变量,这个变量保存打开设备的计数,这里第一次注册设备,初始化ref为1,若之后重复注册则会先检查ref这个变量,
如果ref为1,则表示已经注册过了,避免重复注册v4l2_device,这个初始化在kref_init方法中实现,这是我的理解,可是我暂时还没有找到检查ref的地方,暂且跳过
最后一步根据dev 结构体决定,如果dev不为空,则这里setv4l2_device的name,并且将v4l2_device结构set进dev中,方便后面获取使用
2.ctrl_handler初始化
/* Initialize the handler */
int v4l2_ctrl_handler_init(struct v4l2_ctrl_handler *hdl,
unsigned nr_of_controls_hint)
{
mutex_init(&hdl->lock);
INIT_LIST_HEAD(&hdl->ctrls);
INIT_LIST_HEAD(&hdl->ctrl_refs);
hdl->nr_of_buckets = 1 + nr_of_controls_hint / 8;
hdl->buckets = kzalloc(sizeof(hdl->buckets[0]) * hdl->nr_of_buckets,
GFP_KERNEL);
hdl->error = hdl->buckets ? 0 : -ENOMEM;
return hdl->error;
}
这里还是很有必要了解一下v4l2_ctrl_handler这个结构体到底是干什么用的
/** struct v4l2_ctrl_handler - The
control handler keeps track of all the
* controls: both the controls owned by the handler and those inherited
* from other handlers.
* @lock: Lock to control access to this
handler and its controls.
* @ctrls: The list of controls owned by this handler.
* @ctrl_refs: The list of control references.
* @cached: The last found control reference. It is common
that the same
* control is needed multiple times, so this is a
simple
* optimization.
* @buckets: Buckets for the hashing. Allows for quick
control lookup.
* @nr_of_buckets: Total number of buckets in the array.
* @error: The error code
of the first failed control addition.
*/
struct v4l2_ctrl_handler {
struct mutex lock;
struct list_head ctrls;
struct list_head ctrl_refs;
struct v4l2_ctrl_ref *cached;
struct v4l2_ctrl_ref **buckets;
u16 nr_of_buckets;
int error;
};
在v4l2_ctrl_handler_init方法中,主要通过nr_of_controls_hint变量的大小,计算nr_of_buckets,并为buckets申请空间,并将申请结果保存在error变量中,我感觉可以是用于以后方便check的
dev->v4l2_dev.ctrl_handler = hdl;最后,关联vivi_dev
问题点:
1.v4l2_ctrl_new_std
2.v4l2_ctrl_new_custom
以上两个方法不是很理解,待以后研究
3.互斥锁,自旋锁等初始化
这个比较简单,就不在做说明了
4.vb2_quene初始化
首先还是很有必要看一下这个结构体
/**
* struct vb2_queue - a videobuf queue
*
* @type: queue type (see V4L2_BUF_TYPE_* in linux/videodev2.h
* @io_modes: supported io methods (see vb2_io_modes enum)
* @io_flags: additional io flags (see vb2_fileio_flags enum)
* @ops: driver-specific callbacks
* @mem_ops: memory allocator specific callbacks
* @drv_priv: driver private data
* @buf_struct_size: size of the driver-specific buffer structure;
* "0" indicates the driver doesn't want to use
a custom buffer
* structure type, so sizeof(struct vb2_buffer) will is used
*
* @memory: current memory type used
* @bufs: videobuf buffer structures
* @num_buffers: number of allocated/used buffers
* @queued_list: list of buffers currently queued from userspace
* @queued_count: number of buffers owned by the driver
* @done_list: list of buffers ready to be dequeued to userspace
* @done_lock: lock to protect done_list list
* @done_wq: waitqueue for processes waiting for buffers
ready to be dequeued
* @alloc_ctx: memory type/allocator-specific
contexts for each plane
* @streaming: current streaming state
* @fileio: file io emulator internal data, used only if emulator is active
*/
struct vb2_queue {
enum v4l2_buf_type type;
unsigned int io_modes;
unsigned int io_flags;
const struct vb2_ops *ops;
const struct vb2_mem_ops *mem_ops;
void *drv_priv;
unsigned int buf_struct_size;
/* private: internal
use only */
enum v4l2_memory memory;
struct vb2_buffer *bufs[VIDEO_MAX_FRAME];
unsigned int num_buffers;
struct list_head queued_list;
atomic_t queued_count;
struct list_head done_list;
spinlock_t done_lock;
wait_queue_head_t done_wq;
void *alloc_ctx[VIDEO_MAX_PLANES];
unsigned int streaming:1;
struct vb2_fileio_data *fileio;
};
在v4l2_ctrl_handler_init方法中,首先对vb2_quene其中的重要数据进行填充,最最重要的就是
q->ops = &vivi_video_qops;
q->mem_ops = &vb2_vmalloc_memops;
这两条简单的复制语句责任重大啊,这里先知道有这么一回事,后面补充
/**
* struct vb2_ops - driver-specific callbacks
*
* @queue_setup: called from a VIDIOC_REQBUFS handler, before
* memory allocation; driver should return the required
* number of buffers in num_buffers, the required number
* of planes per buffer in num_planes; the size of each
* plane should be set in the sizes[] array and optional
* per-plane allocator specific context in alloc_ctxs[]
* array
* @wait_prepare: release any locks taken while calling vb2 functions;
* it is called before an ioctl needs to wait for a
new
* buffer to arrive; required to avoid
a deadlock in
* blocking access type
* @wait_finish: reacquire all locks released in the previous callback;
* required to continue operation after sleeping while
* waiting for a new buffer to arrive
* @buf_init: called once after allocating a buffer (in MMAP case)
* or after acquiring a new USERPTR buffer; drivers may
* perform additional buffer-related initialization;
* initialization failure (return != 0) will
prevent
* queue setup from completing successfully; optional
* @buf_prepare: called every time the buffer is queued
from userspace;
* drivers may perform any initialization required before
* each hardware operation in this callback;
* if an error is returned, the
buffer will not be queued
* in driver; optional
* @buf_finish: called before every dequeue of the buffer back to
* userspace; drivers may perform any operations required
* before userspace accesses the buffer; optional
* @buf_cleanup: called once before the buffer is freed; drivers
may
* perform any additional cleanup; optional
* @start_streaming: called once before entering 'streaming' state; enables
* driver to receive buffers over buf_queue() callback
* @stop_streaming: called when 'streaming' state
must be disabled; driver
* should stop any DMA transactions or wait until they
* finish and give back all buffers it got from buf_queue()
* callback; may use vb2_wait_for_all_buffers() function
* @buf_queue: passes buffer vb to the driver; driver
may start
* hardware operation on this buffer; driver should give
* the buffer back by calling vb2_buffer_done() function
*/
struct vb2_ops {
int (*queue_setup)(struct
vb2_queue *q, unsigned int *num_buffers,
unsigned int *num_planes, unsigned long sizes[],
void *alloc_ctxs[]);
void (*wait_prepare)(struct
vb2_queue *q);
void (*wait_finish)(struct
vb2_queue *q);
int (*buf_init)(struct
vb2_buffer *vb);
int (*buf_prepare)(struct
vb2_buffer *vb);
int (*buf_finish)(struct
vb2_buffer *vb);
void (*buf_cleanup)(struct
vb2_buffer *vb);
int (*start_streaming)(struct
vb2_queue *q);
int (*stop_streaming)(struct
vb2_queue *q);
void (*buf_queue)(struct
vb2_buffer *vb);
};
/**
* struct vb2_mem_ops - memory handling/memory allocator operations
* @alloc: allocate video memory and, optionally, allocator private data,
* return NULL on failure or a
pointer to allocator private,
* per-buffer data on success; the
returned private structure
* will then be passed as buf_priv argument to other ops in this
* structure
* @put: inform the allocator that the buffer will no longer be used;
* usually will result in the allocator freeing the buffer (if
* no other users of this buffer are present); the buf_priv
* argument is the allocator private per-buffer
structure
* previously returned from the alloc callback
* @get_userptr: acquire userspace memory for a hardware operation; used for
* USERPTR memory types; vaddr is the address passed to the
* videobuf layer when queuing a video buffer of USERPTR type;
* should return an allocator private per-buffer structure
* associated with the buffer on success, NULL on failure;
* the returned private structure will then be passed as buf_priv
* argument to other ops in this structure
* @put_userptr: inform the allocator that a USERPTR buffer will no longer
* be used
* @vaddr: return a kernel virtual address to a given memory buffer
* associated with the passed private structure or NULL if no
* such mapping exists
* @cookie: return allocator specific cookie for a given memory buffer
* associated with the passed private structure or NULL if not
* available
* @num_users: return the current number of users of a memory buffer;
* return 1 if the videobuf layer (or actually
the driver using
* it) is the only user
* @mmap: setup a userspace mapping for a given memory buffer under
* the provided virtual memory region
*
* Required ops for USERPTR types: get_userptr, put_userptr.
* Required ops for MMAP types: alloc, put, num_users, mmap.
* Required ops for read/write access types: alloc, put, num_users, vaddr
*/
struct vb2_mem_ops {
void *(*alloc)(void *alloc_ctx, unsigned
long size);
void (*put)(void *buf_priv);
void *(*get_userptr)(void *alloc_ctx, unsigned
long vaddr,
unsigned long size, int write);
void (*put_userptr)(void *buf_priv);
void *(*vaddr)(void *buf_priv);
void *(*cookie)(void *buf_priv);
unsigned int (*num_users)(void *buf_priv);
int (*mmap)(void *buf_priv, struct
vm_area_struct *vma);
};
最后调用vb2_queue_init方法,进行初始化
/**
* vb2_queue_init() - initialize
a videobuf2 queue
* @q: videobuf2 queue; this structure should be allocated in driver
*
* The vb2_queue structure should be allocated by the driver. The driver is
* responsible of clearing it's content and setting initial values for some
* required entries before calling this function.
* q->ops, q->mem_ops, q->type and q->io_modes
are mandatory. Please refer
* to the struct vb2_queue description in include/media/videobuf2-core.h
* for more information.
*/
int vb2_queue_init(struct vb2_queue *q)
{
BUG_ON(!q);
BUG_ON(!q->ops);
BUG_ON(!q->mem_ops);
BUG_ON(!q->type);
BUG_ON(!q->io_modes);
BUG_ON(!q->ops->queue_setup);
BUG_ON(!q->ops->buf_queue);
INIT_LIST_HEAD(&q->queued_list);
INIT_LIST_HEAD(&q->done_list);
spin_lock_init(&q->done_lock);
init_waitqueue_head(&q->done_wq);
if (q->buf_struct_size == 0)
q->buf_struct_size = sizeof(struct
vb2_buffer);
return 0;
}
EXPORT_SYMBOL_GPL(vb2_queue_init);
这里方法很简单,只是进行check,然后最最简单的初始化,这里不再多说了
5.init
video dma queues
只有两条语句进行初始化
INIT_LIST_HEAD(&dev->vidq.active);
init_waitqueue_head(&dev->vidq.wq);
6.填充video_device,并且调用video_register_device注册video_device
这里终于到了重点了,很重要,开始了
开始简单,申请内存空间并进行填充,然后才真正调用用video_register_device这个方法,开始了
但是在调用之前的这条语句你必须关注vfd->v4l2_dev = &dev->v4l2_dev;从这里也可以知道v4l2_device和video_device的注册顺序
int __video_register_device(struct video_device *vdev, int type, int nr,
int warn_if_nr_in_use, struct module *owner)
{
int i = 0;
int ret;
int minor_offset = 0;
int minor_cnt = VIDEO_NUM_DEVICES;
const char *name_base;
/* A minor value of -1 marks this video device as never
having been registered */
vdev->minor = -1;
/* the release callback MUST be present */
WARN_ON(!vdev->release);
if (!vdev->release)
return -EINVAL;
/* v4l2_fh support */
spin_lock_init(&vdev->fh_lock);
INIT_LIST_HEAD(&vdev->fh_list);
/* Part 1: check device type */
/* after here, you can see videx ...the
char device in /dev */
//这里还是单独说一下吧,最终你在/dev目录下看到的video0就是在这里决定的,大家可以知道,可不是一定名字叫video的
switch (type) {
case VFL_TYPE_GRABBER:
name_base = "video";
break;
case VFL_TYPE_VBI:
name_base = "vbi";
break;
case VFL_TYPE_RADIO:
name_base = "radio";
break;
case VFL_TYPE_SUBDEV:
name_base = "v4l-subdev";
break;
default:
printk(KERN_ERR "%s called with unknown type: %d\n",
__func__, type);
return -EINVAL;
}
vdev->vfl_type = type;
vdev->cdev = NULL;
if (vdev->v4l2_dev) {
if (vdev->v4l2_dev->dev)
//这里说明vdev和保存在v4l2_device的dev具有共同的parent,对之后sys 接口那里有用
vdev->parent = vdev->v4l2_dev->dev;
if (vdev->ctrl_handler == NULL)
//上面初始化的ctrl_handler在这里要派上用处了,而且同时指向video_device和v4l2_device的化ctrl_handler
vdev->ctrl_handler = vdev->v4l2_dev->ctrl_handler;
/* If the prio state pointer is NULL, then use
the v4l2_device
prio state. */
if (vdev->prio == NULL)
//上面初始化的prio在这里要派上用处了,而且同时指向video_device和v4l2_device的化prio
vdev->prio = &vdev->v4l2_dev->prio;
}
//从这里往下挺长一段代码是在为要申请的字符设备寻找一个合适的设备号,这里不去深入追究了,有时间可以可虑回来看看
/* Part 2: find a free minor, device
node number and device index. */
#ifdef CONFIG_VIDEO_FIXED_MINOR_RANGES
/* Keep the ranges for the first four types for historical
* reasons.
* Newer devices (not yet in place) should
use the range
* of 128-191 and just pick the first free minor there
* (new style). */
switch (type) {
case VFL_TYPE_GRABBER:
minor_offset = 0;
minor_cnt = 64;
break;
case VFL_TYPE_RADIO:
minor_offset = 64;
minor_cnt = 64;
break;
case VFL_TYPE_VBI:
minor_offset = 224;
minor_cnt = 32;
break;
default:
minor_offset = 128;
minor_cnt = 64;
break;
}
#endif
/* Pick a device node number */
mutex_lock(&videodev_lock);
nr = devnode_find(vdev, nr == -1 ? 0 : nr, minor_cnt);
if (nr == minor_cnt)
nr = devnode_find(vdev, 0, minor_cnt);
if (nr == minor_cnt) {
printk(KERN_ERR "could not get a free device node number\n");
mutex_unlock(&videodev_lock);
return -ENFILE;
}
#ifdef CONFIG_VIDEO_FIXED_MINOR_RANGES
/* 1-on-1
mapping of device node number to minor number */
i = nr;
#else
/* The device node number and minor numbers are independent, so
we just find the first free minor number. */
for (i = 0; i < VIDEO_NUM_DEVICES; i++)
if (video_device[i] == NULL)
break;
if (i == VIDEO_NUM_DEVICES) {
mutex_unlock(&videodev_lock);
printk(KERN_ERR "could not get a free minor\n");
return -ENFILE;
}
#endif
vdev->minor = i + minor_offset;
vdev->num = nr;
devnode_set(vdev);
/* Should not happen since we thought this minor was free */
WARN_ON(video_device[vdev->minor] != NULL);
vdev->index = get_index(vdev);
mutex_unlock(&videodev_lock);
//上面的方法获取到了那个合适的设备号,现在要开始注册我们的字符设备了
/* Part 3: Initialize the character device */
vdev->cdev = cdev_alloc();
if (vdev->cdev == NULL) {
ret = -ENOMEM;
goto cleanup;
}
vdev->cdev->ops = &v4l2_fops;//most
important part,操作设备的通道
vdev->cdev->owner = owner;
ret = cdev_add(vdev->cdev, MKDEV(VIDEO_MAJOR, vdev->minor), 1);
if (ret < 0) {
printk(KERN_ERR "%s: cdev_add failed\n", __func__);
kfree(vdev->cdev);
vdev->cdev = NULL;
goto cleanup;
}
//这里我们也大可先不用关注,主要是在sysfs的一些设备添加等等
/* Part 4: register the device with sysfs */
vdev->dev.class = &video_class;
vdev->dev.devt = MKDEV(VIDEO_MAJOR, vdev->minor);
if (vdev->parent)
vdev->dev.parent = vdev->parent;
dev_set_name(&vdev->dev, "%s%d", name_base, vdev->num);
ret = device_register(&vdev->dev);
if (ret < 0) {
printk(KERN_ERR "%s: device_register failed\n", __func__);
goto cleanup;
}
/* Register the release callback that will be called when the last
reference to the device goes away. */
vdev->dev.release = v4l2_device_release;
if (nr != -1 && nr != vdev->num && warn_if_nr_in_use)
printk(KERN_WARNING "%s: requested %s%d, got %s\n", __func__,
name_base, nr, video_device_node_name(vdev));
/* Increase v4l2_device refcount */
if (vdev->v4l2_dev)
v4l2_device_get(vdev->v4l2_dev);
#if defined(CONFIG_MEDIA_CONTROLLER)
//这里其实还是比较重要的,不过不是所以的驱动都要添加这一个步骤,这也是为什么有一个if define
的原因了
//意思就是如果这个驱动中需要用到media controler的时候就需要在这里注册media_device
//这里同样先不做深入研究,media_device和media_entity这两个重要结构体之后还要研究
/* Part 5: Register the entity. */
if (vdev->v4l2_dev && vdev->v4l2_dev->mdev &&
vdev->vfl_type != VFL_TYPE_SUBDEV) {
vdev->entity.type = MEDIA_ENT_T_DEVNODE_V4L;
vdev->entity.name = vdev->name;
vdev->entity.v4l.major = VIDEO_MAJOR;
vdev->entity.v4l.minor = vdev->minor;
ret = media_device_register_entity(vdev->v4l2_dev->mdev,
&vdev->entity);
if (ret < 0)
printk(KERN_WARNING
"%s: media_device_register_entity failed\n",
__func__);
}
#endif
//保存注册成功标记,并将注册成功的video_device保存到全局数组video_device中,大功告成
/* Part 6: Activate this minor. The
char device can now be used. */
set_bit(V4L2_FL_REGISTERED, &vdev->flags);//设置标志位,之后还会遇到test_bit方法用来check
flags的第nr位是否为1.
//这里还是多说一点,另外还有两中标志位需要知道:V4L2_FL_USES_V4L2_FH, V4L2_FL_USE_FH_PRIO
mutex_lock(&videodev_lock);
video_device[vdev->minor] = vdev;
mutex_unlock(&videodev_lock);
return 0;
//这里是出错处理函数
cleanup:
mutex_lock(&videodev_lock);
if (vdev->cdev)
cdev_del(vdev->cdev);
devnode_clear(vdev);
mutex_unlock(&videodev_lock);
/* Mark this video device as never having been registered. */
vdev->minor = -1;
return ret;
}
EXPORT_SYMBOL(__video_register_device);
7.把vivi_dev结构set进video_device中,方便之后使用,设备信息加入的链表结构
最后结尾的这段代码这里我决定单独放在下面分析,也算妥善收尾吧
/*
* You should pay attention to this method
* here you set the vivi_dev into the vedio_device for the later use in fops
* When you want to use the vivi_dev, you use vedio_get_drvdata() to get
*/
video_set_drvdata(vfd, dev);
/* Now that everything is fine, let's
add it to device list */
list_add_tail(&dev->vivi_devlist, &vivi_devlist);//添加的device
list当中
if (video_nr != -1)
video_nr++;//用于计数,找到设备
dev->vfd = vfd;关联video_device和vivi_dev
短短的几条语句,但我看来,个个都短小精悍
首先说说这个方法,他有什么用处呢?其实用处可大了,就行我上面注释的那样,这里把vivi_dev设置到vedio_device中,是为了之后字符设备访问接口中使用
这里多说一点,也算顺便说一下用户空间操作设备的流程了
首先当时用户空间访问设备了,这个做驱动的不懂那可糗大了,用户空间open时,也就是启动了上面video_device_register方法中的很重要的下面结构中的open方法
static const struct file_operations v4l2_fops = {
.owner = THIS_MODULE,
.read = v4l2_read,
.write = v4l2_write,
.open = v4l2_open,
.get_unmapped_area = v4l2_get_unmapped_area,
.mmap = v4l2_mmap,
.unlocked_ioctl = v4l2_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = v4l2_compat_ioctl32,
#endif
.release = v4l2_release,
.poll = v4l2_poll,
.llseek = no_llseek,
};
我们来看一下这个open方法
/* Override for the
open function */
static int v4l2_open(struct inode *inode, struct
file *filp)
{
struct video_device *vdev;
int ret = 0;
/* Check if the video device is available */
mutex_lock(&videodev_lock);
vdev = video_devdata(filp);
/* return ENODEV if the video device has already been removed. */
if (vdev == NULL || !video_is_registered(vdev)) {
mutex_unlock(&videodev_lock);
return -ENODEV;
}
/* and increase the device refcount */
video_get(vdev);//这里是用来计数的
mutex_unlock(&videodev_lock);
/*
* Here using the API you get the method you get the open() method
write
* The other methods in fops use the same method to use you own code
*/
if (vdev->fops->open) {
if (vdev->lock && mutex_lock_interruptible(vdev->lock)) {
ret = -ERESTARTSYS;
goto err;
}
if (video_is_registered(vdev))
ret = vdev->fops->open(filp);
else
ret = -ENODEV;
if (vdev->lock)
mutex_unlock(vdev->lock);
}
err:
/* decrease the refcount in case of
an error */
if (ret)
video_put(vdev);
return ret;
}
只有最下面的那个标准才是重点,经过那么多的check,最后走的了最后这一步,ret = vdev->fops->open(filp);
这个open方法在哪里呢?那就沿着箭头方向找吧,是video_device内部的fops中的open方法,这个方法不是有在哪里呢?
接着找,在video_device_register方法之前
*vfd
= vivi_template;/* the most important struct */我还特意在这里写了最重要的结构体
所以最终调用的是vivi_template中fops中定义的open方法
static const struct v4l2_file_operations vivi_fops = {
.owner = THIS_MODULE,
.open = v4l2_fh_open,
.release = vivi_close,
.read = vivi_read,
.poll = vivi_poll,
.unlocked_ioctl = video_ioctl2, /* V4L2
ioctl handler */
.mmap = vivi_mmap,
};
static const struct v4l2_ioctl_ops vivi_ioctl_ops = {
.vidioc_querycap = vidioc_querycap,
.vidioc_enum_fmt_vid_cap = vidioc_enum_fmt_vid_cap,
.vidioc_g_fmt_vid_cap = vidioc_g_fmt_vid_cap,
.vidioc_try_fmt_vid_cap = vidioc_try_fmt_vid_cap,
.vidioc_s_fmt_vid_cap = vidioc_s_fmt_vid_cap,
.vidioc_reqbufs = vidioc_reqbufs,
.vidioc_querybuf = vidioc_querybuf,
.vidioc_qbuf = vidioc_qbuf,
.vidioc_dqbuf = vidioc_dqbuf,
.vidioc_s_std = vidioc_s_std,
.vidioc_enum_input = vidioc_enum_input,
.vidioc_g_input = vidioc_g_input,
.vidioc_s_input = vidioc_s_input,
.vidioc_streamon = vidioc_streamon,
.vidioc_streamoff = vidioc_streamoff,
};
static struct video_device vivi_template = {
.name = "vivi",
.fops = &vivi_fops,
.ioctl_ops = &vivi_ioctl_ops,
.release = video_device_release,
.tvnorms = V4L2_STD_525_60,
.current_norm = V4L2_STD_NTSC_M,
};
我们找到了,就是v4l2_fh_open这个方法,这个方法与我们之前写字符驱动时遇到的情况很是不同,他的open方法其实是有内核API写好的,我们先看看
int v4l2_fh_open(struct file *filp)
{
struct video_device *vdev = video_devdata(filp);
struct v4l2_fh *fh = kzalloc(sizeof(*fh), GFP_KERNEL);
/*
* IN the open method, do only
one job
* set v4l2_fh into filp->private_data for later
use, and initial v4l2_fh
*/
filp->private_data = fh;
if (fh == NULL)
return -ENOMEM;
v4l2_fh_init(fh, vdev);
v4l2_fh_add(fh);
return 0;
}
EXPORT_SYMBOL_GPL(v4l2_fh_open);
这个open方法将v4l2_fh 这个同样很重要的结构体保存到filp->private中,并做一些初始化,具体过程暂且不说
这里fops中的其他接口的实现比起open,方法是一样的,而且更简单
但是,这里我真正想表达的东西还没有出现,大家看到这里的filp->private_data = fh;//问题就在这里了,我们真正在read,write中需要传递的想要使用的是vivi_dev其实,
而不是v4l2_fh这个数据结构,我们还是直接看看vivi_template中fops中定义的read方法吧
static ssize_t
vivi_read(struct file *file, char __user *data, size_t
count, loff_t *ppos)
{
struct vivi_dev *dev = video_drvdata(file);
dprintk(dev, 1, "read
called\n");
return vb2_read(&dev->vb_vidq, data, count, ppos,
file->f_flags & O_NONBLOCK);
}
这里大家看到了,方法获取到vivi_dev这个数据结构,并传递到vb2_read这个方法中,这个方法时驱动中数据传输的关键,之后再慢慢研究,这里同样不深究
而这个vivi_dev是通过video_drvdata方法获得的,这就是为什么在上面我强调的要用
video_set_drvdata(vfd, dev);把vivi_dev装载到video_device中
到这里基本的过程都整理完了,其实大头还在后面呢,待续。。。。。。
相关文章推荐
- 虚拟视频驱动程序vivi.c源码分析
- 虚拟视频驱动程序vivi.c源码分析
- 虚拟视频驱动程序vivi.c源码分析
- 虚拟视频驱动程序vivi.c源码分析[转]
- 虚拟视频驱动程序vivi.c源码分析
- 虚拟视频驱动程序vivi.c源码分析
- 虚拟视频驱动vivi.c分析(linux-3.4.2版本)
- WebRTC源码分析1视频显示
- vivi源码分析 - xgc94418297的日志 - 网易博客
- Linux设备驱动程序第三版学习(1)-字符设备驱动程序源码分析
- v4l2视频驱动中关于vivi.c的个人分析(菜鸟入门,请轻拍!)
- LDD3源码分析之字符设备驱动程序(加上测试代码)
- Linux设备驱动程序第三版学习(1)-字符设备驱动程序源码分析
- 基于mini6410的linux驱动学习总结(五 字符设备驱动程序实例分析(虚拟设备驱动))
- LDD3源码分析之字符设备驱动程序
- LDD3源码分析之字符设备驱动程序
- 转载—PC键盘驱动程序源码分析
- ASP.NET上传视频文件同时转换为flv并且抓取第一帧生面图片源码分析