当前位置: 首页 > news >正文

实用指南:Linux驱动之V4L2

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
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);
#elsetimer_setup(&g_virtual_timer, virtual_timer_expire, 0);
#endifg_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 摄像头)有两个主要接口:

  1. VideoControl Interface (VC)

    • 用来描述摄像头的功能结构(拓扑),相当于摄像头的“控制平面”。

    • 包含 Terminal(输入/输出终端)和 Unit(功能单元)。

    • Terminal 代表视频源/输出端口(如 Camera Terminal、Output Terminal)。

    • Unit 则是中间的处理模块(如 Processing Unit、Extension Unit)。

  2. VideoStreaming Interface (VS)

    • 用来传输视频流数据,比如 MJPEG、H.264、YUYV。

    • 包含视频格式描述(format descriptor)、分辨率、帧率等信息。

 如果公司需要设置自定义的操作,可以使用下列代码:

2.1 编写APP(参考mjpg-streamer)

2.1.1 列出帧格式/帧大小(分辨率)

这里其实就是两个循环,一个循环获取帧的格式,然后另外一个循环获取该格式下支持的分辨率。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
/* ./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)
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)
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映射)
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。

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)
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取出,保存内容之后再放入空闲链表
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 关闭摄像头
if(ioctl(fd, VIDIOC_STREAMOFF , &type) != 0){printf("can not stop capture\n");return -1;
}

2.1.3 控制亮度

这个需要在中间起一个线程进行控制亮度。

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库。

整体代码如下:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
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结构体。

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 框架

框架部分只是搭出了一个最简框架,当然里面有关硬件相关的函数都还未填写,之后慢慢填充里面有关硬件操作的相关函数。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
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结构体,那我们要提供该结构体中的函数。

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直接返回。

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。(但是一般的会支持多种帧格式大小,一般会根据传进来的值做一个判断,如果传进来的值没有刚好匹配上,会自动将值调整为支持的最近的那个帧大小)

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 设置格式
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。

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(硬件提供)

用户态 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 放到驱动的等待队列里,等“虚拟硬件”准备好数据后,再出队完成它。

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放入完成链表。

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 代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
/* 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是我们生成的节点。

六、完整代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
/* 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");

http://www.hskmm.com/?act=detail&tid=24256

相关文章:

  • 儿童与青少年数据安全及体育发展新方向会议
  • 威联通NAS Emby-Server 的SQLite数据库损坏和程序损坏修复
  • Embarcadero Dev-C++ 6.3 中文乱码问题 - 教程
  • 2025.10.4——2绿
  • 十月四日就听《10월 4일》
  • windows上的实用小软件
  • 比赛题2
  • ZR 2025 十一集训 Day 4
  • 价值处理单元(VPU)专题研究:从价值危机到透明决策的计算革命——声明Ai研究
  • 13-Neo4j Desktop
  • 中兴ZXHN F450光猫关闭TR069实录
  • 完整教程:六款智能证照工具盘点,打造个性化“数字身份档案”
  • 随机化学习笔记
  • PWN手的从成长之路-08-not_the_same_3dsctf_2016-溢出+函数调用劫持
  • 12-windows11的WSL详解
  • 完整教程:如何将文件从电脑传输到安卓设备
  • [vmware+openeuler22.03]创建软RAID
  • C++右值引用
  • 价值处理单元(VPU)专题研究:构建可信AI的基石
  • NOIP模拟赛记录
  • 软件工程第一次作业--关于未来规划和自我发展
  • 2025太阳能厂家推荐天津龙腾,太阳能热水系统,发电系统,光伏热系统,热水工程系统,预加热系统,中央热水系统,彩图发电系统,分户储水系统,分户计量系统推荐
  • 集训模拟赛日志
  • 1688 商品采集 API 调用全流程分享:从准备到实操 - 实践
  • 2025最新推荐化妆品代工公司排行榜:含 OEM / ODM / 一站式服务企业,助力品牌方精准选合作方
  • 悟空博弈单元(WBUC)专题研究:面向可能性计算的结构化创新架构
  • 访问控制、用户认证、https - 实践
  • GO_基础
  • sg.完整布局演示
  • sg.justification用法