cpp
#include <linux/version.h>
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/usb.h>
#include <media/v4l2-device.h>
#include <media/v4l2-ioctl.h>
#include <media/v4l2-ctrls.h>
#include <media/v4l2-event.h>
#include <media/videobuf2-v4l2.h>
#include <media/videobuf2-vmalloc.h>
extern unsigned char red[8230];
extern unsigned char blue[8267];
extern unsigned char green[8265];
static int g_copy_cnt = 0;
static struct list_head g_queued_bufs;
static struct timer_list g_virtual_timer;
static struct mutex g_vb_queue_lock; /* Protects vb_queue and capt_file */
static struct mutex g_v4l2_lock; /* Protects everything else */
/* intermediate buffers with raw data from the USB device */
struct virtual_frame_buf {
/* common v4l buffer stuff -- must be first */
struct vb2_v4l2_buffer vb;
struct list_head list;
};
/* Private functions */
static struct virtual_frame_buf *virtual_get_next_buf(void)
{
//unsigned long flags;
struct virtual_frame_buf *buf = NULL;
//spin_lock_irqsave(&s->queued_bufs_lock, flags);
if (list_empty(&g_queued_bufs))
goto leave;
buf = list_entry(g_queued_bufs.next,
struct virtual_frame_buf, list);
list_del(&buf->list);
leave:
//spin_unlock_irqrestore(&s->queued_bufs_lock, flags);
return buf;
}
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 15, 0)
static void virtual_timer_expire(unsigned long data)
#else
static void virtual_timer_expire(struct timer_list *t)
#endif
{
/* 从硬件上读到数据(使用red/green/blue数组来模拟) */
/* 获得第1个空闲buffer */
struct virtual_frame_buf *buf = virtual_get_next_buf();
void *ptr;
if (buf)
{
/* 写入buffer */
ptr = vb2_plane_vaddr(&buf->vb.vb2_buf, 0);
if (g_copy_cnt <= 60)
{
memcpy(ptr, red, sizeof(red));
vb2_set_plane_payload(&buf->vb.vb2_buf, 0, sizeof(red));
}
else if(g_copy_cnt <= 120)
{
memcpy(ptr, green, sizeof(green));
vb2_set_plane_payload(&buf->vb.vb2_buf, 0, sizeof(green));
}
else
{
memcpy(ptr, blue, sizeof(blue));
vb2_set_plane_payload(&buf->vb.vb2_buf, 0, sizeof(blue));
}
/* vb2_buffer_done */
vb2_buffer_done(&buf->vb.vb2_buf, VB2_BUF_STATE_DONE);
}
g_copy_cnt++;
if (g_copy_cnt > 180)
g_copy_cnt = 0;
/* 再次设置timer的超时时间 */
mod_timer(&g_virtual_timer, jiffies + HZ/30);
}
static int virtual_querycap(struct file *file, void *fh,
struct v4l2_capability *cap)
{
strlcpy(cap->driver, "virtual_video_drv", sizeof(cap->driver));
strlcpy(cap->card, "no-card", sizeof(cap->card));
cap->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING | V4L2_CAP_READWRITE ;
cap->capabilities = cap->device_caps | V4L2_CAP_DEVICE_CAPS;
return 0;
}
static int virtual_enum_fmt_cap(struct file *file, void *priv,
struct v4l2_fmtdesc *f)
{
if (f->index > 0)
return -EINVAL;
strlcpy(f->description, "motion_jpeg", sizeof(f->description));
f->pixelformat = V4L2_PIX_FMT_MJPEG;
return 0;
}
static int virtual_s_fmt_cap(struct file *file, void *priv,
struct v4l2_format *f)
{
/* 分辨用户传入的参数是否可用
* 如果不可用, 给APP提供最接近的、硬件支持的参数
*/
if (f->type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
return -EINVAL;
if (f->fmt.pix.pixelformat != V4L2_PIX_FMT_MJPEG)
return -EINVAL;
f->fmt.pix.width = 800;
f->fmt.pix.height = 600;
return 0;
}
static int virtual_enum_framesizes(struct file *file, void *fh,
struct v4l2_frmsizeenum *fsize)
{
if (fsize->index > 0)
return -EINVAL;
fsize->type = V4L2_FRMSIZE_TYPE_DISCRETE;
fsize->discrete.width = 800;
fsize->discrete.height = 600;
return 0;
}
static int virtual_g_fmt(struct file *file, void *priv, struct v4l2_format *f)
{
struct v4l2_pix_format *pix = &f->fmt.pix;
pix->width = 800;
pix->height = 600;
pix->field = V4L2_FIELD_NONE;
pix->pixelformat = V4L2_PIX_FMT_MJPEG;
pix->bytesperline = 0;
return 0;
}
static const struct v4l2_file_operations virtual_fops = {
.owner = THIS_MODULE,
.open = v4l2_fh_open,
.release = vb2_fop_release,
.read = vb2_fop_read,
.poll = vb2_fop_poll,
.mmap = vb2_fop_mmap,
.unlocked_ioctl = video_ioctl2,
};
static const struct v4l2_ioctl_ops virtual_ioctl_ops = {
.vidioc_querycap = virtual_querycap,
.vidioc_enum_fmt_vid_cap = virtual_enum_fmt_cap,
.vidioc_s_fmt_vid_cap = virtual_s_fmt_cap,
.vidioc_enum_framesizes = virtual_enum_framesizes,
.vidioc_g_fmt_vid_cap = virtual_g_fmt,
.vidioc_reqbufs = vb2_ioctl_reqbufs,
.vidioc_create_bufs = vb2_ioctl_create_bufs,
.vidioc_prepare_buf = vb2_ioctl_prepare_buf,
.vidioc_querybuf = vb2_ioctl_querybuf,
.vidioc_qbuf = vb2_ioctl_qbuf,
.vidioc_dqbuf = vb2_ioctl_dqbuf,
.vidioc_streamon = vb2_ioctl_streamon,
.vidioc_streamoff = vb2_ioctl_streamoff,
};
static struct video_device g_vdev = {
.name = "xupt_vir_drv",
.release = video_device_release_empty,
.fops = &virtual_fops,
.ioctl_ops = &virtual_ioctl_ops,
};
static struct v4l2_device g_v4l2_dev;
static struct vb2_queue g_vb_queue;
/* Videobuf2 operations */
static int virtual_queue_setup(struct vb2_queue *vq,
unsigned int *nbuffers,
unsigned int *nplanes, unsigned int sizes[], struct device *alloc_devs[])
{
/* 假装:至少需要8个buffer, 每个buffer只有1个plane */
/* Need at least 8 buffers */
if (vq->num_buffers + *nbuffers < 8)
*nbuffers = 8 - vq->num_buffers;
*nplanes = 1;
sizes[0] = PAGE_ALIGN(800*600*2);
return 0;
}
static void virtual_buf_queue(struct vb2_buffer *vb)
{
/* 把这个buffer告诉硬件相关的驱动程序 */
struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
struct virtual_frame_buf *buf =
container_of(vbuf, struct virtual_frame_buf, vb);
//unsigned long flags;
//spin_lock_irqsave(&s->queued_bufs_lock, flags);
list_add_tail(&buf->list, &g_queued_bufs);
//spin_unlock_irqrestore(&s->queued_bufs_lock, flags);
}
static int virtual_start_streaming(struct vb2_queue *vq, unsigned int count)
{
/* 启动硬件传输 */
/* 使用timer来模拟硬件中断
* 创建timer
* 启动timer
*/
#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 15, 0)
setup_timer(&g_virtual_timer, virtual_timer_expire, 0);
#else
timer_setup(&g_virtual_timer, virtual_timer_expire, 0);
#endif
g_virtual_timer.expires = jiffies + HZ/30;
add_timer(&g_virtual_timer);
return 0;
}
static void virtual_stop_streaming(struct vb2_queue *vq)
{
/* 停止硬件传输 */
del_timer(&g_virtual_timer);
while (!list_empty(&g_queued_bufs)) {
struct virtual_frame_buf *buf;
buf = list_entry(g_queued_bufs.next,
struct virtual_frame_buf, list);
list_del(&buf->list);
vb2_buffer_done(&buf->vb.vb2_buf, VB2_BUF_STATE_ERROR);
}
}
static const struct vb2_ops virtul_vb2_ops = {
.queue_setup = virtual_queue_setup,
.buf_queue = virtual_buf_queue,
.start_streaming = virtual_start_streaming,
.stop_streaming = virtual_stop_streaming,
.wait_prepare = vb2_ops_wait_prepare,
.wait_finish = vb2_ops_wait_finish,
};
static void virtual_video_release(struct v4l2_device *v)
{
}
static int virtual_video_drv_init(void)
{
int ret;
/* 分配/设置/注册video_device */
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
/* 设置
* 1. 函数调用(比如ioctl)
* 2. 队列/buffer的管理
*/
g_vb_queue.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
g_vb_queue.io_modes = VB2_MMAP | VB2_USERPTR | VB2_READ;
g_vb_queue.drv_priv = NULL;
g_vb_queue.buf_struct_size = sizeof(struct virtual_frame_buf); /* 分配vb时, 分配的空间大小为buf_struct_size */
g_vb_queue.ops = &virtul_vb2_ops;
g_vb_queue.mem_ops = &vb2_vmalloc_memops;
g_vb_queue.timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
ret = vb2_queue_init(&g_vb_queue);
if (ret) {
printk("Could not initialize vb2 queue\n");
return -1;;
}
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
mutex_init(&g_vb_queue_lock);
g_vdev.queue = &g_vb_queue;
g_vdev.queue->lock = &g_vb_queue_lock;
/* Register the v4l2_device structure(辅助作用) */
g_v4l2_dev.release = virtual_video_release;
strcpy(g_v4l2_dev.name, "virtual_v4l2");
ret = v4l2_device_register(NULL, &g_v4l2_dev);
if (ret) {
printk("Failed to register v4l2-device (%d)\n", ret);
return -1;
}
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
g_vdev.v4l2_dev = &g_v4l2_dev;
g_vdev.lock = &g_v4l2_lock;
mutex_init(&g_v4l2_lock);
ret = video_register_device(&g_vdev, VFL_TYPE_GRABBER, -1);
if (ret) {
printk("Failed to register as video device (%d)\n", ret);
return -1;
}
printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);
INIT_LIST_HEAD(&g_queued_bufs);
return 0;
}
static void virtual_video_drv_exit(void)
{
/* 反注册/释放video_device */
v4l2_device_unregister(&g_v4l2_dev);
video_unregister_device(&g_vdev);
}
module_init(virtual_video_drv_init);
module_exit(virtual_video_drv_exit);
MODULE_LICENSE("GPL");
一、V4L2应用程序开发
Video for linux two(Video4Linux2)简称V4L2,是V4L的改进版。V4L2支持三种方式来采集图像:内存映射方式(mmap)、直接读取方式(read)和用户指针。内存映射的方式采集速度较快,一般用于连续视频数据的采集,实际工作中的应用概率更高;直接读取的方式相对速度慢一些,所以常用于静态图片数据的采集;用户指针使用较少,如有兴趣可自行研究。
1.1 处理流程框架
一般来说,摄像头采集到数据之后,会产生中断,驱动程序将数据保存到一个空闲链表的buf中,并将其挂在完成链表的链表中。这时候应用程序会从完成链表中获取buf进行处理,再把buf放入空闲链表的尾部。

1.2 控制流程
二、UVC 设备(USB 摄像头)

UVC 设备(USB 摄像头)有两个主要接口:
-
VideoControl Interface (VC)
-
用来描述摄像头的功能结构(拓扑),相当于摄像头的"控制平面"。
-
包含 Terminal (输入/输出终端)和 Unit(功能单元)。
-
Terminal 代表视频源/输出端口(如 Camera Terminal、Output Terminal)。
-
Unit 则是中间的处理模块(如 Processing Unit、Extension Unit)。
-
-
VideoStreaming Interface (VS)
-
用来传输视频流数据,比如 MJPEG、H.264、YUYV。
-
包含视频格式描述(format descriptor)、分辨率、帧率等信息。
-
如果公司需要设置自定义的操作,可以使用下列代码:

2.1 编写APP(参考mjpg-streamer)
2.1.1 列出帧格式/帧大小(分辨率)
这里其实就是两个循环,一个循环获取帧的格式,然后另外一个循环获取该格式下支持的分辨率。
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <linux/types.h>
#include <linux/videodev2.h>
/* ./video_test /dev/video* */
int main(int argc , char * argv[])
{
int fd,j;
int fmt_index = 0;
int fream_index = 0;
struct v4l2_fmtdesc fmtdesc;
struct v4l2_frmsizeenum fsenum;
if(argc != 2){
printf("Usage: %s [/dev/video*]\n" , argv[0]);
return -1;
}
/* open file */
fd = open(argv[1] , O_RDWR);
if(fd < 0){
printf("can not open %s\n" , argv[1]);
return -1;
}
while(1){
/* 枚举格式 */
fmtdesc.index = fmt_index; //从0开始
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //指定type为捕获
if(0 != ioctl(fd , VIDIOC_ENUM_FMT, &fmtdesc))
break;
fream_index = 0;
while(1){
/* 枚举这种格式所支持的帧大小 */
/* 先清空 */
memset(&fsenum, 0, sizeof(struct v4l2_frmsizeenum));
fsenum.pixel_format = fmtdesc.pixelformat;
fsenum.index = fream_index;
if(ioctl(fd , VIDIOC_ENUM_FRAMESIZES, &fsenum) == 0)
{
printf("format %s,%d, framesize %d: %d * %d\n",
fmtdesc.description , fmtdesc.pixelformat , fream_index , fsenum.discrete.width , fsenum.discrete.height);
}
else{
break;
}
fream_index++;
}
fmt_index++;
}
return 0;
}


2.1.2 获取数据
流程如下:打开设备-->查询能力(ioctl VIDIOC_QUERYCAP)-->申请buffer(ioctl VIDIOC_REQBUF)-->查询buffer信息(ioctl VIDIOC_QUERYBUF)、映射-->把buffer放入空闲链表(ioctl VIDIOC_QBUF)-->启动摄像头(ioctl STREAMON)-->使用poll/select监测buffer,然后从完成链表中取出buffer(ioctl DQBUF)-->处理后再放入空闲链表
2.1.2.1 查询能力(VIDIOC_QUERYCAP)
cpp
struct v4l2_capability capability;
memset(&capability , 0 , sizeof(struct v4l2_capability));
for(ioctl(fd , VIDIOC_QUERYCAP , &capability) == 0){
if((capability.capabilities & V4L2_CAP_VIDEO_CAPTURE) == 0){
/* 如果不相等,说明其不具有捕获能力,直接退出 */
printf("this device has not capabilities\n");
return -1;
}
if((capability.capabilities & V4L2_CAP_STREAMING) == 0){
/* 设备是否支持流式传输,如果不支持就只能使用read,而不能用mmap */
/* 本文主要是想用mmap来读取图片,如果设备不支持直接返回 */
printf("this device can not use mmaping\n");
return -1;
}
}
2.1.2.2 设置格式(VIDIOC_S_FMT)
cpp
struct v4l2_format fmt;
memset(&fmt , 0 , sizeof(struct v4l2_format));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 1024;
fmt.fmt.pix.height = 768;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
fmt.fmt.pix.field = V4L2_FIELD_ANY;
if(ioctl(fd , VIDIOC_S_FMT , &fmt) == 0){
printf("set format ok : %d * %d\n" , fmt.fmt.pix.width , fmt.fmt.pix.height);
}else{
printf("can not set format\n");
return -1;
}
2.1.2.3 申请buffer(VIDIOC_REQBUFS申请buffer、VIDIOC_QUERYBUF映射)
cpp
struct v4l2_requestbuffers buffer;
struct v4l2_buffer vbuf;
void * bufs[32];
int buf_count = 0;
memset(&buffer , 0 , sizeof(struct v4l2_requestbuffers));
buffer.count = 32;
buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buffer.memory = V4L2_MEMORY_MMAP; //用作MMAP映射
if(ioctl(fd, VIDIOC_REQBUFS , &buffer) == 0){
buf_count = buffer.count;
/* 申请内存,对申请到的内存进行mmap,使得应用程序可以直接使用这些映射的内存 */
for(i = 0 ; i < buf_count ; i++){
memset(&vbuf , 0 , sizeof(struct v4l2_buffer));
vbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
vbuf.index = i;
vbuf.memory = V4L2_MEMORY_MMAP; //buf是用于mmap的
if(ioctl(fd , VIDIOC_QUERYBUF , &vbuf) == 0){
bufs[i] = mmap(0 , vbuf.length , PROT_READ | PROT_WRITE , MAP_SHARED ,
fd , vbuf.m.offset);
if(bufs[i] == MAP_FAILED){
printf("unable to map buffer\n");
return -1;
}
}else{
printf("can not query buf\n");
return -1;
}
}
printf("map %d buffers ok\n" , buf_count);
}
2.1.2.4 buffer放到空闲链表(VIDIOC_QBUF)
用户把一个空 buffer(还没有数据)交还给内核, 内核把它挂到空闲链表, 驱动随后会在采集过程中,把数据写进这个 buffer。
cpp
for(i = 0 ; i < buf_count ; ++i){
struct v4l2_buffer buf;
memset(&buf, buf_count, sizeof(struct v4l2_buffer));
buf.index = i;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if(ioctl(fd , VIDIOC_QBUF , buf) != 0){
printf("can not queue buf\n");
return -1;
}
}
printf("queue buf ok\n");
2.1.2.5 启动摄像头(VIDIOC_STREAMON)
cpp
if(ioctl(fd, VIDIOC_STREAMON , &type) != 0){
printf("can not start capture\n");
return -1;
}
printf("start capture ok\n");
2.1.2.6 poll查询队列,查询到把buf取出,保存内容之后再放入空闲链表
cpp
struct pollfd fds[1];
char file_name[32];
int file_cnt = 0;
int file_fd;
while(1){
memset(&fds , 0 , sizeof(fds));
fds[0].fd = fd;
fds[0].events = POLLIN;
if(poll(fds , 1 , -1) == 1){
/* 有事件发生,需要从buf中取出,将buf数据存为文件,在把buf放入队列 */
struct v4l2_buffer buf;
memset(&buf , 0 , sizeof(struct v4l2_buffer));
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if(ioctl(fd , VIDIOC_DQBUF , &buf) != 0){
printf("can not dequeue buffer\n");
return -1;
}
sprintf(file_name , "video_raw_data_%04d.jpeg" , file_cnt++);
file_fd = open(file_name , O_RDWR | O_CREAT , 0777);
if(file_fd < 0){
printf("create file error\n");
return -1;
}
write(file_fd , bufs[buf.index] , buf.bytesused);
close(file_fd);
if(ioctl(fd , VIDIOC_QBUF , &buf) != 0){
printf("can not queue buf\n");
return -1;
}
}
}
2.1.2.6 关闭摄像头
cpp
if(ioctl(fd, VIDIOC_STREAMOFF , &type) != 0){
printf("can not stop capture\n");
return -1;
}
2.1.3 控制亮度
这个需要在中间起一个线程进行控制亮度。
cpp
static void* brightness_ctl (void *args){
int fd = *(int *)args;
unsigned char c;
int delta;
/* 查询亮度信息 */
struct v4l2_queryctrl query_c;
memset(&query_c, 0, sizeof(struct v4l2_queryctrl));
query_c.id = V4L2_CID_BRIGHTNESS;
if(ioctl(fd , VIDIOC_QUERYCTRL , &query_c) != 0){
printf("can not get brightness\n");
return NULL;
}
printf("brightness min = %d , max = %d\n" , query_c.minimum , query_c.maximum);
delta = (query_c.maximum - query_c.minimum)/10;
/* 获取当前亮度值 */
struct v4l2_control ctl;
memset(&ctl, 0, sizeof(struct v4l2_control));
ctl.id = V4L2_CID_BRIGHTNESS;
ioctl(fd , VIDIOC_G_CTRL , &ctl);
while(1){
c = getchar();
if(c == 'u' || c == 'U'){
ctl.value += delta;
}else if (c == 'd' || c == 'D') {
ctl.value -= delta;
}else {
continue;
}
if(ctl.value > query_c.maximum){
ctl.value = query_c.maximum;
}else if (ctl.value < query_c.minimum) {
ctl.value = query_c.minimum;
}
ioctl(fd , VIDIOC_S_CTRL , &ctl);
}
return NULL;
}
编译程序的时候必须要加上-lpthread库。
整体代码如下:
cpp
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <linux/types.h>
#include <linux/videodev2.h>
#include <poll.h>
#include <sys/mman.h>
#include <pthread.h>
static void* brightness_ctl (void *args)
{
int fd = *(int*)args;
unsigned char c;
int delta;
/* 获取亮度最大最小值 */
struct v4l2_queryctrl query_c;
memset(&query_c, 0, sizeof(struct v4l2_queryctrl));
query_c.id = V4L2_CID_BRIGHTNESS;
if(ioctl(fd , VIDIOC_QUERYCTRL , &query_c) != 0){
printf("can not get brightness\n");
return NULL;
}
printf("brightness min = %d , max = %d\n" , query_c.minimum , query_c.maximum);
delta = (query_c.maximum - query_c.minimum)/10;
/* 获取当前亮度值 */
struct v4l2_control ctl;
memset(&ctl, 0, sizeof(struct v4l2_control));
ctl.id = V4L2_CID_BRIGHTNESS;
ioctl(fd , VIDIOC_G_CTRL , &ctl);
while(1){
c = getchar();
if(c == 'u' || c == 'U'){
ctl.value += delta;
}else if (c == 'd' || c == 'D') {
ctl.value -= delta;
}else {
continue;
}
if(ctl.value > query_c.maximum){
ctl.value = query_c.maximum;
}else if (ctl.value < query_c.minimum) {
ctl.value = query_c.minimum;
}
ioctl(fd , VIDIOC_S_CTRL , &ctl);
}
return NULL;
}
/* ./video_test /dev/video* */
int main(int argc , char * argv[])
{
int fd , j , i ;
int fmt_index = 0;
int fream_index = 0;
struct v4l2_fmtdesc fmtdesc;
struct v4l2_frmsizeenum fsenum;
struct v4l2_format fmt;
struct v4l2_capability capability;
struct v4l2_requestbuffers buffer;
struct v4l2_buffer vbuf;
void * bufs[32];
char filename[32];
int file_cnt = 0;
int buf_count;
int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
struct pollfd fds[1];
pthread_t thread;
if(argc != 2){
printf("Usage: %s [/dev/video*]\n" , argv[0]);
return -1;
}
/* open file */
fd = open(argv[1] , O_RDWR);
if(fd < 0){
printf("can not open %s\n" , argv[1]);
return -1;
}
/* 查询能力 */
memset(&capability, 0, sizeof(struct v4l2_capability));
if(ioctl(fd , VIDIOC_QUERYCAP , &capability) == 0){
if((capability.capabilities & V4L2_CAP_VIDEO_CAPTURE) == 0){
printf("this device is not capabilities\n");
return -1;
}
if((capability.capabilities & V4L2_CAP_STREAMING) == 0){
printf("this device can not use mmaping\n");
return -1;
}
}else {
printf("can not get capabilities\n");
return -1;
}
while(1){
/* 枚举格式 */
fmtdesc.index = fmt_index; //从0开始
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; //指定type为捕获
if(0 != ioctl(fd , VIDIOC_ENUM_FMT, &fmtdesc))
break;
fream_index = 0;
while(1){
/* 枚举这种格式所支持的帧大小 */
/* 先清空 */
memset(&fsenum, 0, sizeof(struct v4l2_frmsizeenum));
fsenum.pixel_format = fmtdesc.pixelformat;
fsenum.index = fream_index;
if(ioctl(fd , VIDIOC_ENUM_FRAMESIZES, &fsenum) == 0)
{
printf("format %s,%d, framesize %d: %d * %d\n",
fmtdesc.description , fmtdesc.pixelformat , fream_index , fsenum.discrete.width , fsenum.discrete.height);
}
else{
break;
}
fream_index++;
}
fmt_index++;
}
/* 设置格式 */
memset(&fmt, 0, sizeof(struct v4l2_format));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 1024;
fmt.fmt.pix.height = 768;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
fmt.fmt.pix.field = V4L2_FIELD_ANY;
if(ioctl(fd , VIDIOC_S_FMT , &fmt) == 0){
printf("set format ok : %d * %d\n" , fmt.fmt.pix.width , fmt.fmt.pix.height);
}else{
printf("can not set format\n");
return -1;
}
/* 申请buf */
memset(&buffer, 0, sizeof(struct v4l2_requestbuffers));
buffer.count = 32; /* 申请buffer个数,具体多少由驱动程序决定 */
buffer.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buffer.memory = V4L2_MEMORY_MMAP;
if(ioctl(fd, VIDIOC_REQBUFS , &buffer) == 0){
buf_count = buffer.count;
/* 申请成功之后到底有多少buf由内核决定,申请成功之后,mmap这些buffer,应用程序可以直接使用这些buffer */
for(i = 0; i < buffer.count ; i++){
/* 获取buf的信息 */
memset(&vbuf, 0, sizeof(struct v4l2_buffer));
vbuf.index = i;
vbuf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
vbuf.memory = V4L2_MEMORY_MMAP; //buf是用于mmap的
if(ioctl(fd , VIDIOC_QUERYBUF , &vbuf) == 0){
/* map the buffer */
bufs[i] = mmap(0 , vbuf.length , PROT_READ | PROT_WRITE , MAP_SHARED ,
fd , vbuf.m.offset);
if(bufs[i] == MAP_FAILED){
printf("unable to map buffer\n");
return -1;
}
}else{
printf("can not query buf\n");
return -1;
}
}
printf("map %d buffers ok\n" , buf_count);
}
else{
printf("can not request buffer\n");
return -1;
}
/* 把所有buf放入空闲链表 */
for(i = 0 ; i < buf_count ; ++i){
struct v4l2_buffer buf;
memset(&buf, 0, sizeof(struct v4l2_buffer));
buf.index = i;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
/* 用户把一个空 buffer(还没有数据)交还给内核, 内核把它挂到空闲链表, 驱动随后会在采集过程中,把数据写进这个 buffer */
if(ioctl(fd , VIDIOC_QBUF , &buf) != 0)
{
printf("can not queue buf\n");
return -1;
}
}
printf("queue buf ok\n");
/* 启动摄像头 */
if(ioctl(fd, VIDIOC_STREAMON , &type) != 0){
printf("can not start capture\n");
return -1;
}
printf("start capture ok\n");
/* 开启一个线程调整亮度 */
pthread_create(&thread , NULL , brightness_ctl , &fd);
/* poll 查询数据,并把buf取出队列,把buf的数据存为文件,把buf放入队列 */
while(1){
memset(fds, 0, sizeof(fds));
fds[0].fd = fd;
fds[0].events = POLLIN;
if(poll(fds , 1 , -1) == 1){
/* 把buf取出队列 */
struct v4l2_buffer buf;
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory = V4L2_MEMORY_MMAP;
if(ioctl(fd , VIDIOC_DQBUF , &buf) != 0){
printf("can not dequeue buffer\n");
return -1;
}
/* 把buf的数据存为文件 */
sprintf(filename, "video_raw_data_%04d.jpeg" , file_cnt++);
int fd_file = open(filename , O_RDWR | O_CREAT , 0777);
if(fd_file < 0){
printf("can not create or open file\n");
return -1;
}
printf("capture to %s\n" , filename);
write(fd_file , bufs[buf.index] , buf.bytesused);
close(fd_file);
/* 把buf放入队列 */
if(ioctl(fd , VIDIOC_QBUF , &buf) != 0)
{
printf("can not queue buf\n");
return -1;
}
}
}
/* 关闭摄像头 */
if(ioctl(fd, VIDIOC_STREAMOFF , &type) != 0){
printf("can not stop capture\n");
return -1;
}
printf("stop capture ok\n");
close(fd);
return 0;
}
三、V4L2驱动框架之videobuf2
APP想使用固定的接口来使用摄像头,那么如何让驱动工程师也按照这种方式提供呢?写一份文档,人家也不愿意看,这里就要采用Linux中的分层思想,系统提供好了接口层的代码(v4l2-dev.c,该程序无法支持硬件操作),驱动工程师就只需要提供硬件相关层即可。

3.1 怎么写一个摄像头驱动?
3.1.1 从上往下看
APP调用open/write/read/ioctl函数,进入到接口层函数。找到v4l2_fops结构体,通过传进来的filp结构体,找到次设备号,根据次设备号打开video_device。使用video_device中的fops去操作硬件相关的信息。


3.1.2 从下往上看
从典型的驱动程序中来看,例如airspy.c中。

在注册函数video_register_device中会用到vdev中的次设备号,注册一个字符设备,将这个字符设备的fops指向v4l2_dev.c中的v4l2_fops。

接下来我们来总结一下总体的流程:a、分配设置一个video_device,b、注册一个(其实应该是初始化)v4l2_dev起到辅助作用,里面有各种的锁,c、将vdev中的v4l2_dev->注册的v4l2_dev,d、最后去注册这个video_device,里面那个宏应该使用VFL_TYPE_GRABBER。
3.2 ioctl的调用分析
在驱动函数中,这些ioctI被分为2类:
INFO_FL_STD:标准的,无需特殊的代码来处理,APP的调用可以直达这些处理函数。
INFO_FL_FUNC:这类ioctl需要特殊处理,比如对于VIDIOC_ENUM_FMT,它需要根据设备的类型分别枚举。

3.3 buffer的内核实现和管理
3.3.1 回顾调用流程


自从2019年之后基本使用的都是videobuffer2。
3.3.2 videobuffer2缓冲区结构体

每一个摄像头驱动中都会有一个vb2_queue结构体,在这个队列中会有一个或者多个vb2_buffer,这个vb2_buf会根据硬件的不同,可能会有多个planes,一个plane就代表一个平面的意思(多个plane就相当于多平面相机),每一个planes中会记录信息(plane当前的信息),那么buffer如何表示呢,在vb2_plane中有一个mem_priv指针,该指针会指向vb2_vmalloc_buf结构体,这个结构体中含有vaddr,就把数据放到这个vaddr中。
分配流程:
驱动程序初始化时,就构造了vb2_queue,这是"buffer的队列",一开始里面没有"buffer。APP调用ioctI VIDIOC REQBUFS向驱动申请N个buffer,驱动程序分配n(n<=N)个vb2 buffer结构体,然后对于普通摄像头,还分配一个vb2_plane结构体、vb2_vmalloc buf结构体,最后分配存数据buffer。对于多平面摄像头,给每个vb2 buffer分配多个"vb2 plane结构体、vb2_vmalloc buf结构体、存数据的buffer。
入队列流程: APP调用ioctl VIDIOC_QBUF,驱动程序根据其index找到vb2_buffer把这个vb2_buffer放入链表vb2_queue.queued_list。硬件驱动接收到数据后,比如URB传输完成后从链表vb2_queue.queued_list取出vb2_buffer,把硬件数据存入vb2 buffer,把vb2_buffer放入链表vb2_queue.done_list。
出队列流程:APP调用ioctl VIDIOC DQBUF,驱动程序从链表vb2_queue.done_list取出并移除第1个vb2_buffer,驱动程序也把这个vb2_buffer从链表vb2_queue.queued_list移除。
3.3.3 videobuffer2中的3个ops

3.3.3.1 v4l2_buf_ops
videobuffer 和 应用程序的v4l2_buf 如何交互信息(videobuf2_v4l2.c):

在这个结构体中fill_v4l2_buffer是用来使用vb_buf构建v4l2_buf的,将信息返回给上层应用,然后fill_vb2_buf是使用v4l2_buf来构建vb_buf。这里的vb2_buf_ops中的buf可以理解成buf的描述符。
用来描述buf的结构体,这样比较好理解。

3.3.3.2 vb2_mem_ops(videobuf2-vmalloc.c)
用户申请buf,内核态如何做呢?如何将buf映射到用户空间呢?均是由vb2_mem_ops结构体决定的。如果没有特殊的需求,直接使用内核提供的即可。

用法如下:

DMABUF可以由其他驱动分配,直接将数据传给v4l2,无需通过CPU来进行分配。

3.3.3.3 vb2_ops(具体硬件操作所需要的信息,如airspy驱动函数所示,就在该驱动程序下)
硬件相关的代码:

用法如下:

APP到驱动程序,从上到下,含有两个辅助结构体。第一个结构体是用来在用户空间和用户空间传递数据的,第二个结构体提供内存映射,vb2_ops提供硬件操作相关的函数。

3.4 完整的调用流程
(以airspy.c为例)probe函数中所做的事情:前提是先进入了v4l2_dev.c中提供的file_operations结构体。



这里宏call_qop会被调用两次,第一次是分配内存之前,询问底层硬件相关的函数,需要分配几个buffer,每个buffer中含有多少个plane,第二次调用时分配内存结束,驱动想得到M个buffer,但是实际分配了N个buffer,这时候再次调用queue_setup,确认目前分配的buffer个数和信息是否符合硬件需求。
四、从0编写虚拟摄像头驱动
现在我们来回顾一下流程,就拿应用程序调用ioctl函数来说,首先会进入v4l2_dev.c函数中的file_operations结构体,调用v4l2_ioctl,在该ioctl函数中从file结构体中获取video_device,并且调用这个device的unlocked_iosct。在这个函数中会调用video_usercopy,在该函数中使用到v4l2_is_known_ioctl,去分辨最上层的ioctl属于v4l2_ioctl_info数组中的哪一项,再去调用到硬件相关的v4l2_ioctl_ops结构体。
bash
userspace ioctl(fd, cmd, arg)
↓
v4l2_dev.c → file_operations->unlocked_ioctl
↓
v4l2_ioctl
↓
video_usercopy
↓
__video_do_ioctl
↓
v4l2_is_known_ioctl → v4l2_ioctl_info[]
↓
v4l2_ioctl_ops (driver实现)
↓
硬件操作 (sensor, ISP, codec ...)
有关内存交互部分的结构体有vb2_buf_ops、vb2_mem_ops、vb2_ops,但是我们写摄像头驱动只需要提供vb2_ops这个有关硬件的部分即可,其余有关的均可以使用内核自带的函数。(vb2_ops
是驱动与硬件的 唯一绑定点,驱动开发者只需要实现它,就能让整个 videobuf2 框架跑起来。)
4.1 框架
框架部分只是搭出了一个最简框架,当然里面有关硬件相关的函数都还未填写,之后慢慢填充里面有关硬件操作的相关函数。
cpp
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/usb.h>
#include <media/v4l2-device.h>
#include <media/v4l2-ioctl.h>
#include <media/v4l2-ctrls.h>
#include <media/v4l2-event.h>
#include <media/videobuf2-v4l2.h>
#include <media/videobuf2-vmalloc.h>
static const struct v4l2_ioctl_ops vir_ioctl_ops = {
.vidioc_querycap = airspy_querycap,
.vidioc_enum_fmt_sdr_cap = airspy_enum_fmt_sdr_cap,
.vidioc_g_fmt_sdr_cap = airspy_g_fmt_sdr_cap,
.vidioc_s_fmt_sdr_cap = airspy_s_fmt_sdr_cap,
.vidioc_try_fmt_sdr_cap = airspy_try_fmt_sdr_cap,
.vidioc_reqbufs = vb2_ioctl_reqbufs,
.vidioc_create_bufs = vb2_ioctl_create_bufs,
.vidioc_prepare_buf = vb2_ioctl_prepare_buf,
.vidioc_querybuf = vb2_ioctl_querybuf,
.vidioc_qbuf = vb2_ioctl_qbuf,
.vidioc_dqbuf = vb2_ioctl_dqbuf,
.vidioc_streamon = vb2_ioctl_streamon,
.vidioc_streamoff = vb2_ioctl_streamoff,
.vidioc_g_tuner = airspy_g_tuner,
.vidioc_s_tuner = airspy_s_tuner,
.vidioc_g_frequency = airspy_g_frequency,
.vidioc_s_frequency = airspy_s_frequency,
.vidioc_enum_freq_bands = airspy_enum_freq_bands,
.vidioc_subscribe_event = v4l2_ctrl_subscribe_event,
.vidioc_unsubscribe_event = v4l2_event_unsubscribe,
.vidioc_log_status = v4l2_ctrl_log_status,
};
static const struct v4l2_file_operations vir_fops = {
.owner = THIS_MODULE,
.open = v4l2_fh_open,
.release = vb2_fop_release,
.read = vb2_fop_read,
.poll = vb2_fop_poll,
.mmap = vb2_fop_mmap,
.unlocked_ioctl = video_ioctl2,
};
static struct video_device g_vir_dev = {
.name = "virtual_video_drv",
.release = video_device_release_empty,
.fops = &vir_fops,
.ioctl_ops = &vir_ioctl_ops,
};
struct vb2_queue g_vb_queue;
struct v4l2_device g_v4l2_dev;
static int virtual_video_drv_init(void){
int ret;
/* 重点:分配/设置/注册一个video_device */
/* 1、设置
* 2、queue/buffer的管理
*/
g_vb_queue.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
g_vb_queue.io_modes = VB2_MMAP | VB2_USERPTR | VB2_READ;
g_vb_queue.drv_priv = s;
g_vb_queue.buf_struct_size = sizeof(struct airspy_frame_buf);
g_vb_queue.ops = &airspy_vb2_ops; //vb2_ops,硬件相关的操作函数
g_vb_queue.mem_ops = &vb2_vmalloc_memops; //vb2_mem_ops 辅助结构体,用于mmap等等,映射函数
g_vb_queue.timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
ret = vb2_queue_init(&g_vb_queue); //vb2_buf_ops用于APP和驱动传递参数
if (ret) { //init函数中q->buf_ops = &v4l2_buf_ops;
printk("Could not initialize vb2 queue\n");
return -1;
}
g_vir_dev.queue = &g_vb_queue; //指向前面构造的vb2_queue结构体
g_vir_dev.queue->lock = &g_vb_queue;
/* Register the v4l2_device structure */
g_v4l2_dev.release = airspy_video_release;
ret = v4l2_device_register(NULL, &g_v4l2_dev);
if (ret) {
printk("Failed to register v4l2-device (%d)\n", ret);
return -1;
}
g_vir_dev.v4l2_dev = &g_v4l2_dev;
g_vir_dev.lock = &g_v4l2_dev;
ret = video_register_device(&g_vir_dev, VFL_TYPE_SDR, -1);
if (ret) {
printk("Failed to register as video device (%d)\n",
ret);
return -1;
}
return 0;
}
static void virtual_video_drv_exit(void){
v4l2_device_unregister(&g_v4l2_dev);
video_unregister_device(&g_vir_dev);
return ;
}
module_init(virtual_video_drv_init);
module_exit(virtual_video_drv_exit);
MODULE_LICENSE("GPL");
4.2 编写硬件相关的代码
编写这部分需要提前了解该虚拟的摄像头驱动支持什么功能:
1、MJPG格式
2、在驱动程序中预先构造3幅图片,纯红绿蓝,供应用程序读取
4.2.1 ioctl部分代码
这里主要是将APP的代码程序实现的功能,我们要在驱动程序中提供这些功能:
4.2.1.1 查询能力
由上面的调用过程,可以知道,当用户态调用ioctl的时候,会使用到驱动函数中硬件部分代码提供的v4l2_ioctl_ops结构体,那我们要提供该结构体中的函数。
cpp
static int vir_querycap(struct file *file, void *fh,
struct v4l2_capability *cap)
{
strlcpy(cap->driver, "XUPT_VIRTUAL", sizeof(cap->driver));
strlcpy(cap->card, "no-cart", sizeof(cap->card));
cap->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING | V4L2_CAP_READWRITE;
cap->capabilities = cap->device_caps | V4L2_CAP_DEVICE_CAPS;
return 0;
}
4.2.1.2 枚举格式
这里我们的虚拟摄像头驱动只支持一种MJPEG格式,所以index只会为0,不为0直接返回。
cpp
static int vir_enum_fmt_cap(struct file *file, void *priv,
struct v4l2_fmtdesc *f)
{
/* 枚举支持的格式 */
if (f->index > 0)
return -EINVAL;
strlcpy(f->description, "Motion_JPEG", sizeof(f->description));
f->pixelformat = V4L2_PIX_FMT_MJPEG;
return 0;
}
4.2.1.3 获取帧格式大小
该驱动只支持大小为800*600的格式,所以不论传进来多少的值,都会变成800*600。(但是一般的会支持多种帧格式大小,一般会根据传进来的值做一个判断,如果传进来的值没有刚好匹配上,会自动将值调整为支持的最近的那个帧大小)
cpp
static int vir_enum_framesizes(struct file *file, void *fh,
struct v4l2_frmsizeenum *fsize)
{
/* 获取帧格式大小 */
if(fsize->index > 0){
return -EINVAL;
}
fsize->type = V4L2_FRMSIZE_TYPE_DISCRETE;
fsize->discrete.width = 800;
fsize->discrete.height = 600;
return 0;
}
4.2.1.4 设置格式
cpp
static int vir_s_fmt_cap(struct file *file, void *priv,
struct v4l2_format *f)
{
/* 设置格式,只能支持分辨率为800*600,并且只支持MJPEG格式*/
if(f->fmt.pix.pixelformat != V4L2_PIX_FMT_MJPEG){
return -EINVAL;
}
if(f->type != V4L2_BUF_TYPE_VIDEO_CAPTURE){
return -EINVAL;
}
f->fmt.pix.width = 800;
f->fmt.pix.height = 600;
return 0;
}
4.2.2 与buffer相关的代码
4.2.2.1 申请buffer
同上述流程,会进入到v4l2_ioctl_ops结构体中的vb2_ioctl_reqbufs,在该函数中使用vb2_core_reqbufs,这个函数会调用到call_qop函数,调用硬件相关的vb2_ops.queue_setup函数,确认需要多少的buffer、每个buffer中有多少个plane。
cpp
static int vir_queue_setup(struct vb2_queue *vq,
unsigned int *nbuffers,
unsigned int *nplanes, unsigned int sizes[], struct device *alloc_devs[])
{
/* 至少需要8个buffer */
/* Need at least 8 buffers */
if (vq->num_buffers + *nbuffers < 8)
*nbuffers = 8 - vq->num_buffers;
*nplanes = 1;
sizes[0] = PAGE_ALIGN(800*600);
return 0;
}
4.2.2.2 将空闲buffer放入队列
调用流程:vb2_ioctl_qbuf-->vb2_qbuf-->vb2_core_qbuf-->准备好buffer之后-->__enqueue_in_driver-->call_void_vb_qop-->buf_queue(硬件提供)
cpp
用户态 ioctl(QBUF)
↓
v4l2_ioctl
↓
vb2_ioctl_qbuf
↓
vb2_qbuf
↓
vb2_core_qbuf
↓ (准备 buffer: buf_prepare)
__enqueue_in_driver
↓
call_void_vb_qop
↓
buf_queue (驱动实现,提交给硬件)
buf_queue()
是 vb2 框架与具体硬件驱动的交汇点 ,它的作用可以一句话概括:👉 把用户提交的 buffer 放到驱动的等待队列里,等"虚拟硬件"准备好数据后,再出队完成它。
cpp
static void vir_buf_queue(struct vb2_buffer *vb)
{
/* 把这个buffer告诉硬件相关的驱动程序,应用程序传入多个buffer,驱动程序使用链表来管理 */
struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
struct vir_frame_buf *buf =
container_of(vbuf, struct vir_frame_buf, vb);
// unsigned long flags;
// spin_lock_irqsave(&s->queued_bufs_lock, flags);
/* 这个链表是硬件驱动相关的链表
* 这个空闲的buffer先被放入上层驱动vb2_queue的queued_list链表
* 然后才会被放入这个底层硬件驱动的链表
*/
list_add_tail(&buf->list, &g_vir_queued_bufs);
// spin_unlock_irqrestore(&s->queued_bufs_lock, flags);
}
4.2.3 数据传输
正常的摄像头驱动在启动传输的时候应该会去写相机的寄存器等等,并且当camera有数据了会产生中断,在驱动函数的中断处理函数去读取数据,记录在buffer中。流程如下图所示:

在我们的虚拟摄像头驱动程序中,没有相关的相机硬件设备,那么我们需要使用Timer定时器来模仿camera产生中断。在定时器超时时候调用超时处理函数,在该函数调用vir_get_next_buf,会去链表中取出第一个buffer,并且将这个buffer在链表中删除,同时返回这个buffer给内核进行数据传递。然后循环将三种颜色的图片输出给ptr,这里是去buffer中取出第一个plane的地址进行拷贝数据的。拷贝数据完之后会调用vb2_buffer_done将buffer放入完成链表。
cpp
static struct vir_frame_buf *vir_get_next_buf(void)
{
unsigned long flags;
struct vir_frame_buf *buf = NULL;
/* 从链表中取出第一项,然后删除并且返回这一项 */
// spin_lock_irqsave(&s->queued_bufs_lock, flags);
if (list_empty(&g_vir_queued_bufs))
goto leave;
buf = list_entry(g_vir_queued_bufs.next,
struct vir_frame_buf, list);
list_del(&buf->list);
leave:
// spin_unlock_irqrestore(&s->queued_bufs_lock, flags);
return buf;
}
static void vir_timer_expire(struct timer_list * t)
{
void *ptr;
/* 从硬件上读到数据(使用red/green/blue数组)来模仿 */
/* 获取第一个空闲的buffer */
struct vir_frame_buf *buf = vir_get_next_buf();
if(buf){
/* 写入buffer */
/* 从buf里面得到第0个plane的地址,随后就可以填入参数了 */
ptr = vb2_plane_vaddr(&buf->vb.vb2_buf, 0);
if(g_count <= 60){
memcpy(ptr, red, sizeof(red));
vb2_set_plane_payload(&buf->vb.vb2_buf, 0, sizeof(red));
}
else if (g_count < 120) {
memcpy(ptr, green, sizeof(green));
vb2_set_plane_payload(&buf->vb.vb2_buf, 0, sizeof(green));
}else{
memcpy(ptr, blue, sizeof(blue));
vb2_set_plane_payload(&buf->vb.vb2_buf, 0, sizeof(blue));
}
/* vb2_buffer_done放入完成链表 */
vb2_buffer_done(&buf->vb.vb2_buf, VB2_BUF_STATE_DONE);
}
g_count++;
if(g_count > 180)
g_count = 0;
/* 再次设置timer的超时时间 */
mod_timer(&g_vir_timer, jiffies + HZ/30);
}
/* 这里的count值表示vb2_queue中有多少个buffer */
static int vir_start_streaming(struct vb2_queue *vq, unsigned int count)
{
/* 启动硬件传输 */
/* 使用timer来模拟硬件中断
* 创建timer
* 启动timer
*/
timer_setup(&g_vir_timer, vir_timer_expire, 0);
/* 每秒传输30帧,那么超时时间就是1/30 = 30ms */
g_vir_timer.expires = jiffies + HZ/30;
add_timer(&g_vir_timer);
return 0;
}
static void vir_stop_streaming(struct vb2_queue *vq)
{
/* 停止硬件传输 */
del_timer(&g_vir_timer);
}
一套驱动程序对应多个设备节点时才需要这些锁,比如某个驱动生成了设备节点/dev/video0,/dev/video1,APP1访问/dev/video0,APP2访间/dev/video1, APP1、APP2有可能同时调用驱动里同一套代码这时,驱动里就要进行互斥操作。但是我们这个虚拟驱动程序只会产生一个设备节点,所以无需实现互斥操作。
五、上传板子实验
5.1 代码
cpp
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/usb.h>
#include <linux/types.h>
#include <media/v4l2-device.h>
#include <media/v4l2-ioctl.h>
#include <media/v4l2-ctrls.h>
#include <media/v4l2-event.h>
#include <media/videobuf2-v4l2.h>
#include <media/videobuf2-vmalloc.h>
/* intermediate buffers with raw data from the USB device */
struct vir_frame_buf {
/* common v4l buffer stuff -- must be first */
struct vb2_v4l2_buffer vb;
struct list_head list;
};
struct vb2_queue g_vb_queue;
struct v4l2_device g_v4l2_dev;
static struct timer_list g_vir_timer;
static struct list_head g_vir_queued_bufs;
static struct mutex g_vb_queue_lock; /* Protects vb_queue and capt_file */
static struct mutex g_v4l2_lock; /* Protects everything else */
/* 构建图片数据 */
extern unsigned char red[8341];
extern unsigned char green[8229];
extern unsigned char blue[8267];
static int g_count = 0;
static int vir_querycap(struct file *file, void *fh,
struct v4l2_capability *cap)
{
/* 查询能力 ok*/
strlcpy(cap->driver, "XUPT_VIRTUAL", sizeof(cap->driver));
strlcpy(cap->card, "no-card", sizeof(cap->card));
cap->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING | V4L2_CAP_READWRITE;
cap->capabilities = cap->device_caps | V4L2_CAP_DEVICE_CAPS;
return 0;
}
static int vir_enum_fmt_cap(struct file *file, void *priv,
struct v4l2_fmtdesc *f)
{
/* 枚举支持的格式 ok*/
if (f->index > 0)
return -EINVAL;
strlcpy(f->description, "Motion_JPEG", sizeof(f->description));
f->pixelformat = V4L2_PIX_FMT_MJPEG;
return 0;
}
static int vir_s_fmt_cap(struct file *file, void *priv,
struct v4l2_format *f)
{
/* 设置格式,只能支持分辨率为800*600,并且只支持MJPEG格式 ok*/
if(f->type != V4L2_BUF_TYPE_VIDEO_CAPTURE){
return -EINVAL;
}
if(f->fmt.pix.pixelformat != V4L2_PIX_FMT_MJPEG){
return -EINVAL;
}
f->fmt.pix.width = 800;
f->fmt.pix.height = 600;
return 0;
}
static int vir_enum_framesizes(struct file *file, void *fh,
struct v4l2_frmsizeenum *fsize)
{
/* 获取帧格式大小 ok*/
if(fsize->index > 0){
return -EINVAL;
}
fsize->type = V4L2_FRMSIZE_TYPE_DISCRETE;
fsize->discrete.width = 800;
fsize->discrete.height = 600;
return 0;
}
static int vir_g_fmt(struct file *file, void *priv, struct v4l2_format *f)
{
struct v4l2_pix_format *pix = &f->fmt.pix;
pix->width = 800;
pix->height = 600;
pix->field = V4L2_FIELD_NONE;
pix->pixelformat = V4L2_PIX_FMT_MJPEG;
pix->bytesperline = 0;
return 0;
}
static void vir_buf_queue(struct vb2_buffer *vb)
{
/* 把这个buffer告诉硬件相关的驱动程序,应用程序传入多个buffer,驱动程序使用链表来管理 */
struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
struct vir_frame_buf *buf =
container_of(vbuf, struct vir_frame_buf, vb);
// unsigned long flags;
// spin_lock_irqsave(&s->queued_bufs_lock, flags);
/* 这个链表是硬件驱动相关的链表
* 这个空闲的buffer先被放入上层驱动vb2_queue的queued_list链表
* 然后才会被放入这个底层硬件驱动的链表
*/
list_add_tail(&buf->list, &g_vir_queued_bufs);
// spin_unlock_irqrestore(&s->queued_bufs_lock, flags);
}
static int vir_queue_setup(struct vb2_queue *vq,
unsigned int *nbuffers,
unsigned int *nplanes, unsigned int sizes[], struct device *alloc_devs[])
{
/* 至少需要8个buffer */
/* Need at least 8 buffers */
if (vq->num_buffers + *nbuffers < 8)
*nbuffers = 8 - vq->num_buffers;
*nplanes = 1;
sizes[0] = PAGE_ALIGN(800*600);
return 0;
}
static struct vir_frame_buf *vir_get_next_buf(void)
{
// unsigned long flags;
struct vir_frame_buf *buf = NULL;
/* 从链表中取出第一项,然后删除并且返回这一项 */
// spin_lock_irqsave(&s->queued_bufs_lock, flags);
if (list_empty(&g_vir_queued_bufs))
goto leave;
buf = list_entry(g_vir_queued_bufs.next,
struct vir_frame_buf, list);
list_del(&buf->list);
leave:
// spin_unlock_irqrestore(&s->queued_bufs_lock, flags);
return buf;
}
static void vir_timer_expire(struct timer_list * t)
{
void *ptr;
/* 从硬件上读到数据(使用red/green/blue数组)来模仿 */
/* 获取第一个空闲的buffer */
struct vir_frame_buf *buf = vir_get_next_buf();
if(buf){
/* 写入buffer */
/* 从buf里面得到第0个plane的地址,随后就可以填入参数了 */
ptr = vb2_plane_vaddr(&buf->vb.vb2_buf, 0);
if(g_count <= 60){
memcpy(ptr, red, sizeof(red));
vb2_set_plane_payload(&buf->vb.vb2_buf, 0, sizeof(red));
}
else if (g_count < 120) {
memcpy(ptr, green, sizeof(green));
vb2_set_plane_payload(&buf->vb.vb2_buf, 0, sizeof(green));
}else{
memcpy(ptr, blue, sizeof(blue));
vb2_set_plane_payload(&buf->vb.vb2_buf, 0, sizeof(blue));
}
/* vb2_buffer_done放入完成链表 */
vb2_buffer_done(&buf->vb.vb2_buf, VB2_BUF_STATE_DONE);
}
g_count++;
if(g_count > 180)
g_count = 0;
/* 再次设置timer的超时时间 */
mod_timer(&g_vir_timer, jiffies + HZ/30);
}
/* 这里的count值表示vb2_queue中有多少个buffer */
static int vir_start_streaming(struct vb2_queue *vq, unsigned int count)
{
/* 启动硬件传输 */
/* 使用timer来模拟硬件中断
* 创建timer
* 启动timer
*/
timer_setup(&g_vir_timer, vir_timer_expire, 0);
/* 每秒传输30帧,那么超时时间就是1/30 = 30ms */
g_vir_timer.expires = jiffies + HZ/30;
add_timer(&g_vir_timer);
return 0;
}
static void vir_stop_streaming(struct vb2_queue *vq)
{
/* 停止硬件传输 */
del_timer(&g_vir_timer);
}
static const struct v4l2_ioctl_ops vir_ioctl_ops = {
.vidioc_querycap = vir_querycap,
.vidioc_enum_fmt_vid_cap = vir_enum_fmt_cap,
.vidioc_s_fmt_vid_cap = vir_s_fmt_cap,
.vidioc_enum_framesizes = vir_enum_framesizes,
.vidioc_g_fmt_vid_cap = vir_g_fmt,
.vidioc_reqbufs = vb2_ioctl_reqbufs,
.vidioc_create_bufs = vb2_ioctl_create_bufs,
.vidioc_prepare_buf = vb2_ioctl_prepare_buf,
.vidioc_querybuf = vb2_ioctl_querybuf,
.vidioc_qbuf = vb2_ioctl_qbuf,
.vidioc_dqbuf = vb2_ioctl_dqbuf,
.vidioc_streamon = vb2_ioctl_streamon,
.vidioc_streamoff = vb2_ioctl_streamoff,
};
static const struct v4l2_file_operations vir_fops = {
.owner = THIS_MODULE,
.open = v4l2_fh_open,
.release = vb2_fop_release,
.read = vb2_fop_read,
.poll = vb2_fop_poll,
.mmap = vb2_fop_mmap,
.unlocked_ioctl = video_ioctl2,
};
static struct video_device g_vir_dev = {
.name = "virtual_video_drv",
.release = video_device_release_empty,
.fops = &vir_fops,
.ioctl_ops = &vir_ioctl_ops,
};
/* buf_queue 的作用是:把buffer传给驱动,驱动获取数据填充好buffer后会调用
* vb2_buffer_done函数返还buffer。
*/
static const struct vb2_ops vir_vb2_ops = {
.queue_setup = vir_queue_setup,
.buf_queue = vir_buf_queue,
.start_streaming = vir_start_streaming,
.stop_streaming = vir_stop_streaming,
.wait_prepare = vb2_ops_wait_prepare,
.wait_finish = vb2_ops_wait_finish,
};
static void vir_video_release(struct v4l2_device *v)
{
}
static int virtual_video_drv_init(void){
int ret;
/* 重点:分配/设置/注册一个video_device */
/* 1、设置
* 2、queue/buffer的管理
*/
printk("%s %s %d\n" , __FILE__ , __FUNCTION__ , __LINE__);
g_vb_queue.type = V4L2_BUF_TYPE_SDR_CAPTURE;
g_vb_queue.io_modes = VB2_MMAP | VB2_USERPTR | VB2_READ;
g_vb_queue.drv_priv = NULL;
g_vb_queue.buf_struct_size = sizeof(struct vir_frame_buf); /* 分配vb时,分配的空间大小为buf_struct_size */
g_vb_queue.ops = &vir_vb2_ops; //vb2_ops,硬件相关的操作函数
g_vb_queue.mem_ops = &vb2_vmalloc_memops; //vb2_mem_ops 辅助结构体,用于mmap等等,映射函数
g_vb_queue.timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
ret = vb2_queue_init(&g_vb_queue); //vb2_buf_ops用于APP和驱动传递参数
if (ret) { //init函数中q->buf_ops = &v4l2_buf_ops;
printk("Could not initialize vb2 queue\n");
return -1;
}
printk("%s %s %d\n" , __FILE__ , __FUNCTION__ , __LINE__);
mutex_init(&g_vb_queue_lock);
mutex_init(&g_v4l2_lock);
g_vir_dev.queue = &g_vb_queue; //指向前面构造的vb2_queue结构体
g_vir_dev.queue->lock = &g_vb_queue_lock;
/* Register the v4l2_device structure */
g_v4l2_dev.release = vir_video_release;
strcpy(g_v4l2_dev.name, "vir_v4l2");
ret = v4l2_device_register(NULL, &g_v4l2_dev);
if (ret) {
printk("Failed to register v4l2-device (%d)\n", ret);
return -1;
}
printk("%s %s %d\n" , __FILE__ , __FUNCTION__ , __LINE__);
g_vir_dev.v4l2_dev = &g_v4l2_dev;
g_vir_dev.lock = &g_v4l2_lock;
ret = video_register_device(&g_vir_dev, VFL_TYPE_GRABBER, -1);
if (ret) {
printk("Failed to register as video device (%d)\n",
ret);
return -1;
}
printk("%s %s %d\n" , __FILE__ , __FUNCTION__ , __LINE__);
INIT_LIST_HEAD(&g_vir_queued_bufs);
printk("%s %s %d\n" , __FILE__ , __FUNCTION__ , __LINE__);
return 0;
}
static void virtual_video_drv_exit(void)
{
printk("%s %s %d\n" , __FILE__ , __FUNCTION__ , __LINE__);
v4l2_device_unregister(&g_v4l2_dev);
video_unregister_device(&g_vir_dev);
return ;
}
module_init(virtual_video_drv_init);
module_exit(virtual_video_drv_exit);
MODULE_LICENSE("GPL");
5.2 结果

通过dmseg我们发现,上述程序在register v4l2-device的时候出现了错误。这里其实是由于没有给v4l2_dev设置名字,我们只要给 strcpy(g_v4l2_dev.name, "vir_v4l2");设置名字即可。
insmod 我们的驱动程序后会发现,ls /dev/video* -l 下面没有生成设备节点,但是在 ls /dev/swradio0 这个设备生成了节点,这是因为代码在 ret = video_register_device(&g_vir_dev, VFL_TYPE_SDR, -1);的时候传的type不对,生成错了设备节点,所以我们要将这个type改成VFL_TYPE_GRABBER 或者直接传0即可。结果如下图所示,video11是我们生成的节点。

六、完整代码
cpp
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/usb.h>
#include <linux/types.h>
#include <media/v4l2-device.h>
#include <media/v4l2-ioctl.h>
#include <media/v4l2-ctrls.h>
#include <media/v4l2-event.h>
#include <media/videobuf2-v4l2.h>
#include <media/videobuf2-vmalloc.h>
/* intermediate buffers with raw data from the USB device */
struct vir_frame_buf {
/* common v4l buffer stuff -- must be first */
struct vb2_v4l2_buffer vb;
struct list_head list;
};
struct vb2_queue g_vb_queue;
struct v4l2_device g_v4l2_dev;
static struct timer_list g_vir_timer;
static struct list_head g_vir_queued_bufs;
static struct mutex g_vb_queue_lock; /* Protects vb_queue and capt_file */
static struct mutex g_v4l2_lock; /* Protects everything else */
/* 构建图片数据 */
extern unsigned char red[8341];
extern unsigned char green[8229];
extern unsigned char blue[8267];
static int g_count = 0;
static int vir_querycap(struct file *file, void *fh,
struct v4l2_capability *cap)
{
/* 查询能力 ok*/
strlcpy(cap->driver, "XUPT_VIRTUAL", sizeof(cap->driver));
strlcpy(cap->card, "no-card", sizeof(cap->card));
cap->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING | V4L2_CAP_READWRITE;
cap->capabilities = cap->device_caps | V4L2_CAP_DEVICE_CAPS;
return 0;
}
static int vir_enum_fmt_cap(struct file *file, void *priv,
struct v4l2_fmtdesc *f)
{
/* 枚举支持的格式 ok*/
if (f->index > 0)
return -EINVAL;
strlcpy(f->description, "Motion_JPEG", sizeof(f->description));
f->pixelformat = V4L2_PIX_FMT_MJPEG;
return 0;
}
static int vir_s_fmt_cap(struct file *file, void *priv,
struct v4l2_format *f)
{
/* 设置格式,只能支持分辨率为800*600,并且只支持MJPEG格式 ok*/
if(f->type != V4L2_BUF_TYPE_VIDEO_CAPTURE){
return -EINVAL;
}
if(f->fmt.pix.pixelformat != V4L2_PIX_FMT_MJPEG){
return -EINVAL;
}
f->fmt.pix.width = 800;
f->fmt.pix.height = 600;
return 0;
}
static int vir_enum_framesizes(struct file *file, void *fh,
struct v4l2_frmsizeenum *fsize)
{
/* 获取帧格式大小 ok*/
if(fsize->index > 0){
return -EINVAL;
}
fsize->type = V4L2_FRMSIZE_TYPE_DISCRETE;
fsize->discrete.width = 800;
fsize->discrete.height = 600;
return 0;
}
static int vir_g_fmt(struct file *file, void *priv, struct v4l2_format *f)
{
/* 该函数用于得到摄像头当前正在使用得格式,通过设置v4l2_format结构体把格式返回给用户态 */
struct v4l2_pix_format *pix = &f->fmt.pix;
pix->width = 800;
pix->height = 600;
pix->field = V4L2_FIELD_NONE;
pix->pixelformat = V4L2_PIX_FMT_MJPEG;
pix->bytesperline = 0;
return 0;
}
static void vir_buf_queue(struct vb2_buffer *vb)
{
/* 把这个buffer告诉硬件相关的驱动程序,应用程序传入多个buffer,驱动程序使用链表来管理 */
struct vb2_v4l2_buffer *vbuf = to_vb2_v4l2_buffer(vb);
struct vir_frame_buf *buf =
container_of(vbuf, struct vir_frame_buf, vb);
// unsigned long flags;
// spin_lock_irqsave(&s->queued_bufs_lock, flags);
/* 这个链表是硬件驱动相关的链表
* 这个空闲的buffer先被放入上层驱动vb2_queue的queued_list链表
* 然后才会被放入这个底层硬件驱动的链表
*/
list_add_tail(&buf->list, &g_vir_queued_bufs);
// spin_unlock_irqrestore(&s->queued_bufs_lock, flags);
}
static int vir_queue_setup(struct vb2_queue *vq,
unsigned int *nbuffers,
unsigned int *nplanes, unsigned int sizes[], struct device *alloc_devs[])
{
/* 至少需要8个buffer */
/* Need at least 8 buffers */
if (vq->num_buffers + *nbuffers < 8)
*nbuffers = 8 - vq->num_buffers;
*nplanes = 1;
sizes[0] = PAGE_ALIGN(800*600);
return 0;
}
static struct vir_frame_buf *vir_get_next_buf(void)
{
// unsigned long flags;
struct vir_frame_buf *buf = NULL;
/* 从链表中取出第一项,然后删除并且返回这一项 */
// spin_lock_irqsave(&s->queued_bufs_lock, flags);
if (list_empty(&g_vir_queued_bufs))
goto leave;
buf = list_entry(g_vir_queued_bufs.next,
struct vir_frame_buf, list);
list_del(&buf->list);
leave:
// spin_unlock_irqrestore(&s->queued_bufs_lock, flags);
return buf;
}
static void vir_timer_expire(struct timer_list * t)
{
void *ptr;
/* 从硬件上读到数据(使用red/green/blue数组)来模仿 */
/* 获取第一个空闲的buffer */
struct vir_frame_buf *buf = vir_get_next_buf();
if(buf){
/* 写入buffer */
/* 从buf里面得到第0个plane的地址,随后就可以填入参数了 */
ptr = vb2_plane_vaddr(&buf->vb.vb2_buf, 0);
if(g_count <= 60){
memcpy(ptr, red, sizeof(red));
vb2_set_plane_payload(&buf->vb.vb2_buf, 0, sizeof(red));
}
else if (g_count < 120) {
memcpy(ptr, green, sizeof(green));
vb2_set_plane_payload(&buf->vb.vb2_buf, 0, sizeof(green));
}else{
memcpy(ptr, blue, sizeof(blue));
vb2_set_plane_payload(&buf->vb.vb2_buf, 0, sizeof(blue));
}
/* vb2_buffer_done放入完成链表 */
vb2_buffer_done(&buf->vb.vb2_buf, VB2_BUF_STATE_DONE);
}
g_count++;
if(g_count > 180)
g_count = 0;
/* 再次设置timer的超时时间 */
mod_timer(&g_vir_timer, jiffies + HZ/30);
}
/* 这里的count值表示vb2_queue中有多少个buffer */
static int vir_start_streaming(struct vb2_queue *vq, unsigned int count)
{
/* 启动硬件传输 */
/* 使用timer来模拟硬件中断
* 创建timer
* 启动timer
*/
timer_setup(&g_vir_timer, vir_timer_expire, 0);
/* 每秒传输30帧,那么超时时间就是1/30 = 30ms */
g_vir_timer.expires = jiffies + HZ/30;
add_timer(&g_vir_timer);
return 0;
}
static void vir_stop_streaming(struct vb2_queue *vq)
{
/* 停止硬件传输 */
del_timer(&g_vir_timer);
while (!list_empty(&g_vir_queued_bufs)) {
struct vir_frame_buf *buf;
buf = list_entry(g_vir_queued_bufs.next,
struct vir_frame_buf, list);
list_del(&buf->list);
vb2_buffer_done(&buf->vb.vb2_buf, VB2_BUF_STATE_ERROR);
}
}
static const struct v4l2_ioctl_ops vir_ioctl_ops = {
.vidioc_querycap = vir_querycap,
.vidioc_enum_fmt_vid_cap = vir_enum_fmt_cap,
.vidioc_s_fmt_vid_cap = vir_s_fmt_cap,
.vidioc_enum_framesizes = vir_enum_framesizes,
.vidioc_g_fmt_vid_cap = vir_g_fmt,
.vidioc_reqbufs = vb2_ioctl_reqbufs,
.vidioc_create_bufs = vb2_ioctl_create_bufs,
.vidioc_prepare_buf = vb2_ioctl_prepare_buf,
.vidioc_querybuf = vb2_ioctl_querybuf,
.vidioc_qbuf = vb2_ioctl_qbuf,
.vidioc_dqbuf = vb2_ioctl_dqbuf,
.vidioc_streamon = vb2_ioctl_streamon,
.vidioc_streamoff = vb2_ioctl_streamoff,
};
static const struct v4l2_file_operations vir_fops = {
.owner = THIS_MODULE,
.open = v4l2_fh_open,
.release = vb2_fop_release,
.read = vb2_fop_read,
.poll = vb2_fop_poll,
.mmap = vb2_fop_mmap,
.unlocked_ioctl = video_ioctl2,
};
static struct video_device g_vir_dev = {
.name = "virtual_video_drv",
.release = video_device_release_empty,
.fops = &vir_fops,
.ioctl_ops = &vir_ioctl_ops,
};
/* buf_queue 的作用是:把buffer传给驱动,驱动获取数据填充好buffer后会调用
* vb2_buffer_done函数返还buffer。
*/
static const struct vb2_ops vir_vb2_ops = {
.queue_setup = vir_queue_setup,
.buf_queue = vir_buf_queue,
.start_streaming = vir_start_streaming,
.stop_streaming = vir_stop_streaming,
.wait_prepare = vb2_ops_wait_prepare,
.wait_finish = vb2_ops_wait_finish,
};
static void vir_video_release(struct v4l2_device *v)
{
}
static int virtual_video_drv_init(void){
int ret;
/* 重点:分配/设置/注册一个video_device */
/* 1、设置
* 2、queue/buffer的管理
*/
printk("%s %s %d\n" , __FILE__ , __FUNCTION__ , __LINE__);
g_vb_queue.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
g_vb_queue.io_modes = VB2_MMAP | VB2_USERPTR | VB2_READ;
g_vb_queue.drv_priv = NULL;
g_vb_queue.buf_struct_size = sizeof(struct vir_frame_buf); /* 分配vb时,分配的空间大小为buf_struct_size */
g_vb_queue.ops = &vir_vb2_ops; //vb2_ops,硬件相关的操作函数
g_vb_queue.mem_ops = &vb2_vmalloc_memops; //vb2_mem_ops 辅助结构体,用于mmap等等,映射函数
g_vb_queue.timestamp_flags = V4L2_BUF_FLAG_TIMESTAMP_MONOTONIC;
ret = vb2_queue_init(&g_vb_queue); //vb2_buf_ops用于APP和驱动传递参数
if (ret) { //init函数中q->buf_ops = &v4l2_buf_ops;
printk("Could not initialize vb2 queue\n");
return -1;
}
printk("%s %s %d\n" , __FILE__ , __FUNCTION__ , __LINE__);
mutex_init(&g_vb_queue_lock);
mutex_init(&g_v4l2_lock);
g_vir_dev.queue = &g_vb_queue; //指向前面构造的vb2_queue结构体
g_vir_dev.queue->lock = &g_vb_queue_lock;
/* Register the v4l2_device structure */
g_v4l2_dev.release = vir_video_release;
strcpy(g_v4l2_dev.name, "vir_v4l2");
ret = v4l2_device_register(NULL, &g_v4l2_dev);
if (ret) {
printk("Failed to register v4l2-device (%d)\n", ret);
return -1;
}
printk("%s %s %d\n" , __FILE__ , __FUNCTION__ , __LINE__);
g_vir_dev.v4l2_dev = &g_v4l2_dev;
g_vir_dev.lock = &g_v4l2_lock;
ret = video_register_device(&g_vir_dev, VFL_TYPE_GRABBER, -1);
if (ret) {
printk("Failed to register as video device (%d)\n",
ret);
return -1;
}
printk("%s %s %d\n" , __FILE__ , __FUNCTION__ , __LINE__);
INIT_LIST_HEAD(&g_vir_queued_bufs);
printk("%s %s %d\n" , __FILE__ , __FUNCTION__ , __LINE__);
return 0;
}
static void virtual_video_drv_exit(void)
{
printk("%s %s %d\n" , __FILE__ , __FUNCTION__ , __LINE__);
v4l2_device_unregister(&g_v4l2_dev);
video_unregister_device(&g_vir_dev);
return ;
}
module_init(virtual_video_drv_init);
module_exit(virtual_video_drv_exit);
MODULE_LICENSE("GPL");