v4l2子系统学习(三)编写虚拟摄像头驱动

1、声明

本文是在学习韦东山《驱动大全》V4L2子系统时,为梳理知识点和自己回看而记录,全部内容高度复制粘贴。

韦老师的《驱动大全》:商品详情

其对应的讲义资料:https://e.coding.net/weidongshan/linux/doc_and_source_for_drivers.git

2、前言

前面两章,我们基于v4l2在应用层实现了获取摄像头数据,了解了应用层中获取摄像头数据应该有的流程。接着,借着应用层的每一步操作来了解其驱动是如何实现的,从而了解整个v4l2的驱动框架。

从宏观的角度来看,前期的所有准备,无非就是想知道在编写设备驱动程序时要去分配设置哪个结构体,实现哪些关键函数。对于其它子系统的学习也是如此,再者,如今编写驱动的机会只会越来越少,学习驱动的首要目的还是为了养兵千日,用兵一时。

3、虚拟摄像头驱动编写

3.1、编写硬件相关代码

ioctl相关:

1、分配设置注册video_device结构体。

2、实现相关的ioctl函数(如获取设备能力、枚举格式、枚举分辨率、设置分辨率等)。

buffer相关:

3、分配设置vb2_queue结构体。

4、实现相关的vb2_ops函数(如queue_setup()、buf_queue()、start_streaming()、stop_streaming()等函数)。

数据传输:

5、实现start_streaming()、stop_streaming()

3.2、程序示例

#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, "100ask_virtual_video", 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, "100ask 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                     = "100ask_virtual_video",
	.release                  = video_device_release_empty,
	.fops                     = &virtual_fops,			// v4l2接口层相关的ioctl操作函数
	.ioctl_ops                = &virtual_ioctl_ops,		// 与硬件相关的ioctl操作函数
};

static struct v4l2_device g_v4l2_dev;

static struct vb2_queue g_vb_queue;

static int virtual_queue_setup(struct vb2_queue *vq,
		unsigned int *nbuffers,
		unsigned int *nplanes, unsigned int sizes[], struct device *alloc_devs[])
{
    /* 假装:至少需要8buffer, 每个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);		//buffer放到驱动程序里的链表
    //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);
	}
}

/* buffer管理要用到的vb2_ops */
static const struct vb2_ops virtul_vb2_ops = {
	.queue_setup            = virtual_queue_setup,		// APP调用ioctl VIDIOC_REQBUFS或VIDIOC_CREATE_BUFS时,驱动程序在分配内存之前,会调用此函数。
	.buf_queue              = virtual_buf_queue,		//buffer传送给驱动
	.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、设置vb2_queue结构体 */
    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;			// 设置buffer管理要用到的vb2_ops
    g_vb_queue.mem_ops = &vb2_vmalloc_memops;	// 设置buffer管理要用到的vb2_mem_ops
    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);
	
	/* 2、设置video_device结构体 */
    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);
	
	/* 3、注册video_device结构体 */ 
	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");
<think>嗯,用户想了解Linux驱动开发的学习路线,我得好好理清楚思路。首先,用户可能是刚开始接触Linux驱动开发,或者是已经有了一些编程基础,想转行到这个领域。需要从基础开始讲起,逐步深入,确保他能跟上。 首先,Linux驱动开发需要哪些基础知识呢?操作系统原理、C语言应该是必须的。可能还需要了解一些硬件相关的知识,比如处理器架构、内存管理之类的。我得先列出这些基础知识,让用户打好基础。 然后,Linux系统本身的了解也很重要。比如内核的组成、模块化机制,还有编译和调试的方法。用户可能需要学习如何编译内核,如何编写简单的模块,这部分可以通过一些例子来讲解,比如经典的Hello World模块。 接下来是驱动开发的核心部分,字符设备、平台设备、设备树这些概念。用户需要理解这些驱动类型的工作原理,以及如何实际操作。比如,如何注册字符设备,如何使用设备树来描述硬件。这部分可能需要结合代码示例,但用户现在可能还不需要写代码,所以先解释概念。 另外,同步与互斥机制、中断处理、DMA这些高级主题也是必须的。用户可能会遇到多线程环境下的资源竞争问题,或者需要处理硬件中断,这些都需要详细说明。不过要分阶段,先掌握基础再深入高级内容。 调试和优化技巧也很关键,没有这些在实际开发中会遇到很多困难。比如如何使用printk和动态调试,还有性能优化的方法。用户需要知道如何排查问题,提高驱动效率。 最后,推荐一些学习资源和项目实践,帮助用户巩固知识。书籍、在线文档、社区论坛都是不错的资源。实际项目比如LED驱动、传感器驱动,能让他动手实践,加深理解。 可能用户还想知道学习的大致时间安排,或者有哪些常见的陷阱需要避免。不过目前的问题主要是学习路线,所以先按步骤来,确保每个阶段都有明确的目标和内容。还要注意语言要口语化,避免太技术化的术语,让用户容易理解。 总之,结构应该是先基础,再系统学习,然后核心开发,高级主题,最后调试和项目实践。这样用户能循序渐进,逐步掌握Linux驱动开发的各个方面。需要检查是否有遗漏的重要知识点,比如设备树现在在嵌入式系统中很常用,必须涵盖进去。另外,驱动模型和sysfs这类用户空间接口也很重要,应该提到。</think>Linux驱动开发学习路线可以分为以下阶段,结合理论与实践逐步深入: --- ### **一、基础准备阶段(1-3个月)** 1. **C语言基础** - 重点掌握:指针、结构体、内存管理(malloc/free)、位操作、函数指针 - 推荐书籍:《C和指针》《C Primer Plus》 2. **操作系统原理** - 理解进程/线程、内存管理、中断、I/O机制、内核态与用户态 - 推荐学习:《操作系统导论》《深入理解计算机系统》 3. **Linux系统基础** - 熟练使用Linux命令行(gcc/gdb/makefile) - 掌握Shell脚本编写 - 学习内核源码目录结构(如`drivers/`, `include/`, `arch/`) --- ### **二、Linux内核入门(2-4个月)** 1. **内核编译与模块开发** - 实践内核编译(配置选项、交叉编译) - 编写第一个内核模块(Hello World) - 学习模块加载/卸载(`insmod/rmmod`) 2. **内核关键机制** - 理解系统调用、Proc文件系统、Sysfs - 掌握内核日志(`printk`)与动态调试(`dmesg`) 3. **设备驱动模型** - 学习字符设备驱动(`cdev`)、平台设备驱动(`platform_device/platform_driver`) - 掌握设备树(Device Tree)语法与使用 --- ### **驱动开发核心(3-6个月)** 1. **硬件交互基础** - 内存映射(`ioremap`)、寄存器操作、GPIO控制 - 中断处理(`request_irq`)、延时机制(`mdelay`) 2. **典型驱动开发** - **字符设备**:按键、LED、ADC - **块设备**:RAMDisk模拟 - **网络设备**:虚拟网卡驱动 - **输入子系统**:触摸屏、键盘驱动 3. **同步与并发** - 自旋锁(`spinlock`)、互斥锁(`mutex`)、信号量(`semaphore`) - 原子操作(`atomic_t`)、等待队列(`wait_queue`) --- ### **四、进阶技术(3-6个月)** 1. **内核调试与优化** - 使用`ftrace`、`kprobe`分析性能 - 内存泄漏检测(`kmemleak`)、死锁排查 2. **高级驱动技术** - DMA传输(`dma_alloc_coherent`) - 内核线程(`kthread`)、工作队列(`workqueue`) - 电源管理(`PM Framework`) 3. **实战项目** - 实现SPI/I2C设备驱动(如传感器) - 开发USB摄像头驱动(结合V4L2框架) - 移植开源驱动(如WiFi模块) --- ### **五、学习资源推荐** 1. **书籍** - 《Linux设备驱动程序》(经典必读) - 《Linux内核设计与实现》 - 《精通Linux设备驱动程序开发》 2. **在线文档** - 内核文档(`Documentation/`目录) - [Linux Kernel Module Programming Guide](https://tldp.org/LDP/lkmpg/2.6/html/) 3. **社区与工具** - 订阅[LKML](https://lore.kernel.org/)邮件列表 - 使用QEMU模拟器调试驱动 - 参与开源项目(如Raspberry Pi驱动开发) --- ### **六、避坑指南** 1. **硬件依赖问题**:优先选择支持良好的开发板(如树莓派、BeagleBone)。 2. **内核版本兼容性**:注意API变化(如`register_chrdev` vs `cdev_init`)。 3. **调试技巧**:善用`CONFIG_DEBUG_KERNEL`和`objdump`反汇编。 --- **学习路线总结**: 基础 → 内核机制 → 驱动开发 → 调试优化 → 项目实战 建议每天投入2-3小时,配合开发板实践,1年左右可达到中级水平。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值