2.1.1.4 节_摄像头驱动_从零写一个虚拟驱动之示例

 

一. vivi驱动应用程序调用过程

上节对xawtv对vivi程序调用过程进行了详细分析,可总结为以下流程:

二、仿照vivi.c编写myvivi.c驱动程序

#include <linux/module.h>
#include <linux/delay.h>
#include <linux/errno.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/mm.h>
#include <linux/ioport.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/pci.h>
#include <linux/random.h>
#include <linux/version.h>
#include <linux/mutex.h>
#include <linux/videodev2.h>
#include <linux/dma-mapping.h>
#include <linux/interrupt.h>
#include <linux/kthread.h>
#include <linux/highmem.h>
#include <linux/freezer.h>
#include <media/videobuf-vmalloc.h>
#include <media/v4l2-device.h>
#include <media/v4l2-ioctl.h>


static struct v4l2_format myvivi_format;

/* 队列操作1: 定义 */
static struct videobuf_queue myvivi_vb_vidqueue;
static spinlock_t myvivi_queue_slock;

static struct list_head myvivi_vb_local_queue; //定义本地队列,用于把videobuf放入该队列的尾部

static struct timer_list myvivi_timer; //定义定时器

/* ------------------------------------------------------------------
    Videobuf operations
   ------------------------------------------------------------------*/
/* APP调用ioctl VIDIOC_REQBUFS时会导致此函数被调用,
 * 它重新调整count和size
 */
static int myvivi_buffer_setup(struct videobuf_queue *vq, unsigned int *count, unsigned int *size)
{

    *size = myvivi_format.fmt.pix.sizeimage;

    if (0 == *count)
        *count = 32;

    return 0;
}

/* APP调用ioctlVIDIOC_QBUF时导致此函数被调用,
 * 它会填充video_buffer结构体并调用videobuf_iolock来分配内存
 * 
 */
static int myvivi_buffer_prepare(struct videobuf_queue *vq, struct videobuf_buffer *vb,
                        enum v4l2_field field)
{
    /* 0. 设置videobuf */
    vb->size = myvivi_format.fmt.pix.sizeimage;
    vb->bytesperline = myvivi_format.fmt.pix.bytesperline;
    vb->width  = myvivi_format.fmt.pix.width;
    vb->height = myvivi_format.fmt.pix.height;
    vb->field  = field;
    
    /* 1. 做些准备工作 */

#if 0
    /* 2. 调用videobuf_iolock为类型为V4L2_MEMORY_USERPTR的videobuf分配内存 */
    if (VIDEOBUF_NEEDS_INIT == buf->vb.state) {
        rc = videobuf_iolock(vq, &buf->vb, NULL);
        if (rc < 0)
            goto fail;
    }
#endif
    /* 3. 设置状态 */
    vb->state = VIDEOBUF_PREPARED;

    return 0;
}


/* APP调用ioctl VIDIOC_QBUF时:
 * 1. 先调用buf_prepare进行一些准备工作
 * 2. 把buf放入stream队列
 * 3. 调用buf_queue(起通知、记录作用)
 */
static void myvivi_buffer_queue(struct videobuf_queue *vq, struct videobuf_buffer *vb)
{
    vb->state = VIDEOBUF_QUEUED;

    /* 把videobuf放入本地一个队列尾部
     * 定时器处理函数就可以从本地队列取出videobuf
     */
    list_add_tail(&vb->queue, &myvivi_vb_local_queue);
}

/* APP不再使用队列时, 用它来释放内存 */
static void myvivi_buffer_release(struct videobuf_queue *vq,
               struct videobuf_buffer *vb)
{
    videobuf_vmalloc_free(vb);
    vb->state = VIDEOBUF_NEEDS_INIT;
}

static struct videobuf_queue_ops myvivi_video_qops = {
    .buf_setup      = myvivi_buffer_setup, /* 计算大小以免浪费 */
    .buf_prepare    = myvivi_buffer_prepare,
    .buf_queue      = myvivi_buffer_queue,
    .buf_release    = myvivi_buffer_release,
};

/* ------------------------------------------------------------------
    File operations for the device
   ------------------------------------------------------------------*/

static int myvivi_open(struct file *file)
{
    /* 队列操作2: 初始化 */
    videobuf_queue_vmalloc_init(&myvivi_vb_vidqueue, &myvivi_video_qops,
            NULL, &myvivi_queue_slock, V4L2_BUF_TYPE_VIDEO_CAPTURE, V4L2_FIELD_INTERLACED,
            sizeof(struct videobuf_buffer), NULL); /* 倒数第2个参数是buffer的头部大小 */

    myvivi_timer.expires = jiffies + 1;
    add_timer(&myvivi_timer);

    return 0;
}


static int myvivi_close(struct file *file)
{
    del_timer(&myvivi_timer);
    videobuf_stop(&myvivi_vb_vidqueue);
    videobuf_mmap_free(&myvivi_vb_vidqueue);
    
    return 0;
}

static int myvivi_mmap(struct file *file, struct vm_area_struct *vma)
{
    return videobuf_mmap_mapper(&myvivi_vb_vidqueue, vma);
}

static unsigned int myvivi_poll(struct file *file, struct poll_table_struct *wait)
{
    return videobuf_poll_stream(file, &myvivi_vb_vidqueue, wait);
}

static int myvivi_vidioc_querycap(struct file *file, void  *priv,
                    struct v4l2_capability *cap)
{
    strcpy(cap->driver, "myvivi");
    strcpy(cap->card, "myvivi");
    cap->version = 0x0001;
    cap->capabilities =    V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
    return 0;
}

/* 列举支持哪种格式 */
static int myvivi_vidioc_enum_fmt_vid_cap(struct file *file, void  *priv,
                    struct v4l2_fmtdesc *f)
{
    if (f->index >= 1)
        return -EINVAL;

    strcpy(f->description, "4:2:2, packed, YUYV");
    f->pixelformat = V4L2_PIX_FMT_YUYV;
    return 0;
}

/* 返回当前所使用的格式 */
static int myvivi_vidioc_g_fmt_vid_cap(struct file *file, void *priv,
                    struct v4l2_format *f)
{
    memcpy(f, &myvivi_format, sizeof(myvivi_format));
    return (0);
}

/* 测试驱动程序是否支持某种格式 */
static int myvivi_vidioc_try_fmt_vid_cap(struct file *file, void *priv,
            struct v4l2_format *f)
{
    unsigned int maxw, maxh;
    enum v4l2_field field;

    if (f->fmt.pix.pixelformat != V4L2_PIX_FMT_YUYV)
        return -EINVAL;

    field = f->fmt.pix.field;

    if (field == V4L2_FIELD_ANY) {
        field = V4L2_FIELD_INTERLACED;
    } else if (V4L2_FIELD_INTERLACED != field) {
        return -EINVAL;
    }

    maxw  = 1024;
    maxh  = 768;

    /* 调整format的width, height, 
     * 计算bytesperline, sizeimage
     */
    v4l_bound_align_image(&f->fmt.pix.width, 48, maxw, 2,
                  &f->fmt.pix.height, 32, maxh, 0, 0);
    f->fmt.pix.bytesperline =
        (f->fmt.pix.width * 16) >> 3;
    f->fmt.pix.sizeimage =
        f->fmt.pix.height * f->fmt.pix.bytesperline;

    return 0;
}

static int myvivi_vidioc_s_fmt_vid_cap(struct file *file, void *priv,
                    struct v4l2_format *f)
{
    int ret = myvivi_vidioc_try_fmt_vid_cap(file, NULL, f);
    if (ret < 0)
        return ret;

    memcpy(&myvivi_format, f, sizeof(myvivi_format));
    
    return ret;
}

static int myvivi_vidioc_reqbufs(struct file *file, void *priv,
              struct v4l2_requestbuffers *p)
{
    return (videobuf_reqbufs(&myvivi_vb_vidqueue, p));
}

static int myvivi_vidioc_querybuf(struct file *file, void *priv, struct v4l2_buffer *p)
{
    return (videobuf_querybuf(&myvivi_vb_vidqueue, p));
}

static int myvivi_vidioc_qbuf(struct file *file, void *priv, struct v4l2_buffer *p)
{
    return (videobuf_qbuf(&myvivi_vb_vidqueue, p));
}

static int myvivi_vidioc_dqbuf(struct file *file, void *priv, struct v4l2_buffer *p)
{
    return (videobuf_dqbuf(&myvivi_vb_vidqueue, p,
                file->f_flags & O_NONBLOCK));
}

static int myvivi_vidioc_streamon(struct file *file, void *priv, enum v4l2_buf_type i)
{
    return videobuf_streamon(&myvivi_vb_vidqueue);
}

static int myvivi_vidioc_streamoff(struct file *file, void *priv, enum v4l2_buf_type i)
{
    videobuf_streamoff(&myvivi_vb_vidqueue);
    return 0;
}


static const struct v4l2_ioctl_ops myvivi_ioctl_ops = {
        // 表示它是一个摄像头设备
        .vidioc_querycap      = myvivi_vidioc_querycap,

        /* 用于列举、获得、测试、设置摄像头的数据的格式 */
        .vidioc_enum_fmt_vid_cap  = myvivi_vidioc_enum_fmt_vid_cap,
        .vidioc_g_fmt_vid_cap     = myvivi_vidioc_g_fmt_vid_cap,
        .vidioc_try_fmt_vid_cap   = myvivi_vidioc_try_fmt_vid_cap,
        .vidioc_s_fmt_vid_cap     = myvivi_vidioc_s_fmt_vid_cap,
        
        /* 缓冲区操作: 申请/查询/放入队列/取出队列 */
        .vidioc_reqbufs       = myvivi_vidioc_reqbufs,
        .vidioc_querybuf      = myvivi_vidioc_querybuf,
        .vidioc_qbuf          = myvivi_vidioc_qbuf,
        .vidioc_dqbuf         = myvivi_vidioc_dqbuf,
        
        // 启动/停止
        .vidioc_streamon      = myvivi_vidioc_streamon,
        .vidioc_streamoff     = myvivi_vidioc_streamoff,   
};

static const struct v4l2_file_operations myvivi_fops = {
    .owner        = THIS_MODULE,
    .open       = myvivi_open,
    .release    = myvivi_close,
    .mmap       = myvivi_mmap,
    .ioctl      = video_ioctl2, /* V4L2 ioctl handler */
    .poll       = myvivi_poll,
};

static struct video_device *myvivi_device;

static void myvivi_release(struct video_device *vdev)
{
}

static void myvivi_timer_function(unsigned long data)
{
    struct videobuf_buffer *vb;
    void *vbuf;
    struct timeval ts;
    
    /* 1. 构造数据: 从队列头部取出第1个videobuf, 填充数据
     */

    /* 1.1 从本地队列取出第1个videobuf */
    if (list_empty(&myvivi_vb_local_queue)) { //已在myvivi_buffer_queue函数中放入的
        goto out;
    }
    
    vb = list_entry(myvivi_vb_local_queue.next,
             struct videobuf_buffer, queue);
    
    /* Nobody is waiting on this buffer, return */
    if (!waitqueue_active(&vb->done))
        goto out;
    

    /* 1.2 填充数据 */
    vbuf = videobuf_to_vmalloc(vb);
    memset(vbuf, 0, vb->size); // 写入0;
    vb->field_count++;
    do_gettimeofday(&ts);
    vb->ts = ts;
    vb->state = VIDEOBUF_DONE;

    /* 1.3 把videobuf从本地队列中删除 *****注意:这不是myvivi_vb_local_queue队列*/
    list_del(&vb->queue);

    /* 2. 唤醒进程: 唤醒videobuf->done上的进程 */
    wake_up(&vb->done);
    
out:
    /* 3. 修改timer的超时时间 : 30fps, 1秒里有30帧数据
     *    每1/30 秒产生一帧数据
     */
    mod_timer(&myvivi_timer, jiffies + HZ/30);
}

static int myvivi_init(void)
{
    int error;
    
    /* 1. 分配一个video_device结构体 */
    myvivi_device = video_device_alloc();

    /* 2. 设置 */

    /* 2.1 */
    myvivi_device->release = myvivi_release;

    /* 2.2 */
    myvivi_device->fops    = &myvivi_fops;

    /* 2.3 */
    myvivi_device->ioctl_ops = &myvivi_ioctl_ops;

    /* 2.4 队列操作
     *  a. 定义/初始化一个队列(会用到一个spinlock)
     */
    spin_lock_init(&myvivi_queue_slock);

    /* 3. 注册 */
    error = video_register_device(myvivi_device, VFL_TYPE_GRABBER, -1);

    /* 用定时器产生数据并唤醒进程 */
    init_timer(&myvivi_timer);
    myvivi_timer.function  = myvivi_timer_function;

    INIT_LIST_HEAD(&myvivi_vb_local_queue);
    
    return error;
}

static void myvivi_exit(void)
{
    video_unregister_device(myvivi_device);
    video_device_release(myvivi_device);
}

module_init(myvivi_init);
module_exit(myvivi_exit);
MODULE_LICENSE("GPL");

 

1 关键点分析

1)摄像头驱动程序结构

1. 分配结构体: video_device 结构体,通过 video_device_alloc 函数来分配
2. 设置
.fops 基本的操作函数
.ioctl_ops (里面需要设置 11 项)(与摄像头操作密切相关的函数)
如果要用内核提供的缓冲区操作函数,还需要构造一个 videobuf_queue_ops
3. 注册: video_register_device
 

2) v4l2 框架之 videobuf

videobuf 是应用程序和 v4l2 驱动程序的一个中间层,用它来进行视频数据缓冲区的分配和管理。 说白了就是提供一种功能框
架,用来分配和管理视频缓冲区,它相对独立,却又被 v4l2 驱动使用
。 它有一组功能函数集用来实现许多标准的 POSIX 系统
调用,包括 read(),poll()和 mmap()等等,还有一组功能函数集用来实现流式(streaming)IO 的 v4l2_ioctl 调用,包括缓冲区
的分配,入队和出队以及数据流控制等操作。使用 videobuf 需要驱动程序作者遵从一些强制的设计规则,但带来的好处是代
码量的减少和 v4l2 框架 API 的一致。
它根据应用程序的需求(缓冲区的数量的大小),分配相应的视频缓冲区,这个缓冲区是在内核空间分配的,并通过 mmap 方法
映射到用户空间,在内核空间形成一个缓冲区队列,在应用程序中有相应的缓冲区数组对应,它们指向的内存地址是一样的。
在驱动程序中,根据配置的硬件参数( FIFO 阈值),将 vip 硬件图像存储器中的数据放到缓冲区队列中的 每个缓冲区,然后等
待应用程序来读取该缓冲区的数据。 videobuf 主要由一些特殊的数据结构和 ioctl 调用组成. 下边对其做整体分析
 

A 缓冲类型

并不是所有的视频设备都使用相同的缓冲类型。实际上,有三种通用的类型:
1: 被分散在物理和内核虚拟地址空间的缓冲,几乎所有的用户空间缓冲都是这种类型,
如果可能的话分配内核空间的缓冲也很有意义,但是不幸的是,这个通常需要那些支持离散聚合 DMA 操作的硬件设备。
2: 物理上离散的但是虚拟地址是连续的,换句话说,就是用 vmalloc 分配的内核缓冲。这些缓冲很难用于 DMA 操作。
3: 物理上连续的缓冲。
videobuf 可以很好地处理这三种类型的缓冲,但是在此之前,驱动程序作者必须选择一种类型,并且以此类型为基础设计驱
动。
根据选择缓冲不同的类型,包含不同的头文件,这些头文件在 include/media/下面
<media/videobuf-dma-sg.h>
<media/videobuf-vmalloc.h>
<media/videobuf-dma-contig.h>
 

B 数据结构,回调函数

v4l2 驱动需要包含一个 videobuf_queue 的实例用来管理缓冲队列,同时还要一个链表来维护这个队列,另外还要一个中断
安全的 spin_lock 来保护队列的操作。
 

videobuf_queue_ops
下一步就是要填充一个回调函数集来处理实际的缓冲区队列,这个函数集用 videobuf_queue_ops 来描述:

struct videobuf_queue_ops {
	int *(buf_setup)(struct videobuf_queue*q, uint *count, uint *size);
	int *(buf_prepare)(struct videobuf_queue *q, struct videobuf_buffer *vb,
	enum v4l2_field field);
	void *(buf_queue)(struct videobuf_queue*q,struct videobuf_buffer *vb);
	void *(buf_release)(...);
}


 

buf_setup 函数
在 IO 处理请求之前被调用。 目的是告诉 videobuf 关于 IO 的信息。 count 参数提供一个缓冲区个数的参考,驱动必须检查它
的合理性,一个经验是大于等于 2,小于等于 32 个。 Size 参数指定了每一帧数据的大小。
何时,被谁调用?

static int vidioc_reqbufs(struct file *file, void *priv,struct v4l2_requestbuffers *p)
--> videobuf_reqbufs(&dev->vb_vidq, p);
------->q->ops->buf_setup(q, &count, &size);


buf_prepare 函数
每一个缓冲(videobuf_buffer 结构描述的)将被传递给该回调函数, 用来配置缓冲的 height,width 和 fileds。如果 field
参数被设置为 VIDEOBUF_NEEDS_INIT,那么驱动将把 vb 传递给 videobuf_iolock()这个函数。除此之外,该回调函数通常
也将为 vb 分配内存,最后把 vb 的状态置为 VIDEOBUF_PREPARED。
buf_queue 函数
当一个 vb 需要被放入 IO 请求队列时,调用该回调。它将把这个 buffer 放到可用的 buffer 链表当中去,然后把状态置为
VIDEOBUF_QUEUED。
何时,被谁调用?

static int vidioc_qbuf(struct file *file, void *priv, struct v4l2_buffer *p)
-->int videobuf_qbuf(struct videobuf_queue *q, struct v4l2_buffer *b)
------> retval = q->ops->buf_prepare(q, buf, field);
------> q->ops->buf_queue(q, buf);


buf_release 函数
当一个 buffer 不再使用的时候,调用该回调函数。驱动必须保证 buffer 上没有活跃的 IO 请求,之后就可以将这个 buffer
传递给合适的 free 函数,根据申请的 buffer 类型调用对应的释放函数:
1:scatter/gather 类型的调用
videobuf_dma_unmap(structvideobuf_queue, videobuf_dmabuf)
videobuf_dma_free(videobuf_dmabuf)
2:vmalloc 类型的调用
videobuf_vmalloc_free(videobuf_buffer)
3:contiguous 类型的调用
videobuf_dma_contig_free(videobuf_queue,videobuf_buffer)
有一种方法可以保证 buffer 上没有 IO 请求,调用函数
videobuf_waiton(videobuf_buffer,non_blocking, intr)
被谁调用?

static int vidioc_streamoff(struct file *file, void *priv, enum v4l2_buf_type i)
   videobuf_streamoff(&fh->vb_vidq)
     retval = __videobuf_streamoff(q);
         videobuf_queue_cancel(q);
              q->ops->buf_release(q, q->bufs[i]);


 

C 文件操作( v4l2_file_operations)

到了这儿,很多工作也就做完了,剩下的事情就是将对 videobuf 的调用传递给具体的驱动实现了。
我们 vivi.c 中的 v4l2_file_operations 中的成员很多都是最终都是由 videobuf 层来实现的
1: v4l2_file_operations.open 函数实现
首先就是打开操作(一般队列的初始化都在 open 函数中调用),这个操作要先对 videobuf_queue 进行初始化,初始化取决于
申请的 buffer 是什么类型,有如下三种初始化函数可供调用:

 

a: videobuf_queue_sg_init
 

void videobuf_queue_sg_init(structvideobuf_queue *q,
struct videobuf_queue_ops *ops,
struct device *dev,
spinlock_t *irqlock,
enum v4l2_buf_type type,
enum v4l2_field_ field,
unsigned int msize,
链接地址 void *priv,
struct mutex *ext_lock)

 

b: videobuf_queue_vmalloc_init
void videobuf_queue_vmalloc_init(structvideobuf_queue *q,
struct videobuf_queue_ops *ops,
struct device *dev,
spinlock_t *irqlock,
enum v4l2_buf_type type,
enum v4l2_field field,
unsigned int mszie,
void *priv,
struct mutex *ext_lock);


c: videobuf_queue_dma_contig_init
Void videobuf_queue_dma_contig_init(struct videobuf_queue *q,
struct videobuf_queue_ops *ops,
struct device *dev,
spinlock_t *irqlock,
enum v4l2_buf_type type,
enum v4l2_field field,
unsigned int mszie,
void *priv,
struct mutex *ext_lock);
以上三种初始化函数,有相同的参数,这些参数的从他们的名称就可以看出来锁代表的意义是什么。
参数 enum v4l2_buf_type type

V4L2_BUF_TYPE_VIDEO_CAPTURE 指定 buf 的类型为 capture,用于视频捕获设备
V4L2_BUF_TYPE_VIDEO_OUTPUT 指定 buf 的类型 output,用于视频输出设备
V4L2_BUF_TYPE_VIDEO_OVERLAY 指定 buf 的类型为 overlay,用于 overlay 设备
V4L2_BUF_TYPE_VBI_CAPTURE 用于 vbi 捕获设备
V4L2_BUF_TYPE_VBI_OUTPUT 用于 vbi 输出设备
V4L2_BUF_TYPE_SLICED_VBI_CAPTURE 用于切片 vbi 捕获设备
V4L2_BUF_TYPE_SLICED_VBI_OUTPUT 用于切片 vbi 输出设备
V4L2_BUF_TYPE_VIDEO_OUTPUT_OVERLAY 用于视频输出 overlay 设备
V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE 用于多平面存储格式的视频捕获设备
V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE 用于多平面存储格式的视频输出设备


参数 enum v4l2_field field
v4l2_field 指定 video 的 field,也就是说 interleaved 或者 progressive 的,一般都是指定为 V4L2_FIELD_NONE,用于逐行扫
描的设备。
 

2: v4l2_file_operations. read 函数实现
V4L2 捕获设备驱动可以支持两种 API: read()系统调用和更为复杂的流机制。一般的做法是两种都支持以确保所有的应用都可
以使用该设备。 videobuf 框架使得这种驱动的编写变得更为简单。
(我的理解,我们应用层可以通过队列的几个 ioctl 来实现获取 video 的数据,也可以通过 read 的方式来获取)
比如说要实现 read()系统调用,那么驱动程序只需要调用

ssize_t videobuf_read_one(structvideobuf_queue *q,
char __user *data, size_t count,loff_t *ppos, int nonblocking)
ssize_t videobuf_read_streaming(structvideobuf_queue *q, char __user *data, size_t count, loff_t *ppos,int
vbihack, int nonblocking)


这两个函数都是把帧数据读入到 data 当中,然后返回实际上读取的字节数。不同的是前者只读取一帧数据,而后者可以选择
读取多帧。一个典型的应用 read()系统调用必须开启捕获设备,然后返回之前停止该设备。
 

3: v4l2_file_operations. poll 函数实现
poll()系统调用通常由以下函数来实现: (我们 vivi.c 中的 v4l2_file_operations.poll 函数最终也是由 videobuf 层实现的)
unsigned int videobuf_poll_stream(struct file *file, struct videobuf_queue *q,poll_table *wait)//由
v4l2_file_operations.poll 调用
注意,实际最终使用的 q 是可用的第一个 buffer
 

。。。。。。。。。。。。

 

2 Linux 内核定时器


2.1 定时器的几个概念


jiffies 在内核中是一个全局变量,它用来统计系统启动以来系统中产生的总节拍数,这个变量定义在
include/linux/jiffies.h 中,定义形式如下。
unsigned long volatile jiffies;
想要理解 jiffies 的含义,我们需要首先理解时钟 节拍率 节拍的概念。
一、时钟
时钟应用于处理器的定时信号,它使得处理器在时钟中运行,依靠信号时钟,处理器便知道什么时候能够执行它的下一个功
能。在 Linux 系统中,时钟分为硬件时钟(又叫实时时钟)和软件时钟(又叫系统时钟)。在对内核编程中,我们经常用到的
是系统时钟,系统时钟的主要任务有如下三点:
1.保证系统时间的正确性。
2.防止进程超额使用 CPU。
3.记录 CPU 和资源消耗的统计时间。
系统时钟的初始值在系统启动时,通过读取硬件时钟获得,然后由 Linux 内核来维护。在系统运行中,系统时钟的更新是根据
系统启动后的时钟滴答数来更新的。
实时时钟的主要作用是提供计时和产生精确的时钟中断。实时时钟是用来持久存放系统时间的设备,即便系统关闭后,它也可
以靠主板上的微型电池提供的电力保持系统的计时。
二、节拍率
节拍率其实就是系统定时器产生中断的频率,所谓频率即指中断每秒钟产生多少次,即 Hz(赫兹)。不同的体系结构的系统而
言,节拍率不一定相同。
节拍率( Hz)的值可以在文件 include/asm-x86/param.h 中看到,定义如下。
#define Hz 1000
在 Linux 2.6 中,系统时钟每 1 毫秒中断一次(时钟频率,用 HZ 宏表示,定义为 1000,即每秒中断 1000 次, 2.4 中定
义为 100,很多应用程序也仍然沿用 100 的时钟频率),这个时间单位称为一个 jiffie
 

三、节拍
节拍就是指系统中连续两次时钟中断的间隔时间,该值等于节拍率分之一,即 1/Hz。因为系统再启动时已经设置了 Hz,所以
系统的节拍也可以确定。内核正是利用节拍来计算系统时钟和系统运行时间的。
三、 jiffies 变量
jiffies 用来统计系统启动以来系统中产生的总节拍数。该变量在系统启动时被初始化为 0,接下来没进行一次时钟中断,
jiffies 自动加 1。因此,知道了总的节拍数,然后再除以 Hz,即可知系统的运行时间( jiffies/Hz)。
对于 jiffies+Hz 的含义, jiffies 表示当前的系统时钟中断数, Hz 表示一秒后的时钟中断的增加量,假设 time=jiffies+Hz,
正如上面所说 ,内核正是利用节拍数来计算系统时钟和系统运行时间的,则通过 jiffies+Hz 即可间接表示一秒钟。
 

2.2 Linux 内核定时器的实现


1. timer_list
在 Linux 内核中, timer_list 结构体的一个实例对应一个定时器

2.初始化定时器


void init_timer(struct timer_list * timer);
函数分析:
void init_timer(struct timer_list * timer);
static void __init_timer(struct timer_list *timer,const char *name,struct lock_class_key *key)
timer->entry.next = NULL;
timer->base = __raw_get_cpu_var(tvec_bases);
videobuf_queue_ops 结构体
 

3.增加定时器


void add_timer(struct timer_list * timer);
上述函数用于注册内核定时器,将定时器加入到内核动态定时器链表中
我们最开始说我们的每个 timer_list 都对应一个定时器的实例,内核就是将所有的定时器都加入到一个链表中,当定时时间
到了就去对应的链表节点中找到该定时器,执行相应的函数,当定时器开始加入内核定时器链表时,开始计时,到了定时的时
间,就去执行响应的定时器处理函数,如果定时器处理函数里面没有重新设置超时时间,就不会再次定时
 

4.删除定时器


int del_timer(struct timer_list * timer);
上述函数用于删除定时器。
del_timer_sync()是 del_timer()的同步版,主要在多处理器系统中使用,如果编译
内核时不支持 SMP, del_timer_sync()和 del_timer()等价。
 

5.修改定时器的 expire


int mod_timer(struct timer_list *timer, unsigned long expires);
上述函数用于修改定时器的到期时间,在新的被传入的 expires 到来后才会执行定时器函数。
 

 

3 自己写驱动:


 

这是自写记录

myvivi.c

#include <linux/module.h>
#include <linux/delay.h>
#include <linux/errno.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/slab.h>
#include <linux/mm.h>
#include <linux/ioport.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/pci.h>
#include <linux/random.h>
#include <linux/version.h>
#include <linux/mutex.h>
#include <linux/videodev2.h>
#include <linux/dma-mapping.h>
#include <linux/interrupt.h>
#include <linux/kthread.h>
#include <linux/highmem.h>
#include <linux/freezer.h>
#include <media/videobuf-vmalloc.h>
#include <media/v4l2-device.h>

static struct video_device *myvivi_device;


static int __init myvivi_init(void)
{
	int iError = -1;
	//1.分配一个video_device结构体
	myvivi_device = video_device_alloc();
	
	//2.设置
	/* 2.1 */
	 

	/* */
	
	//3.注册
	iError = video_register_device(myvivi_device, VFL_TYPE_GRABBER, -1);
	
	
	return iError;

}


static void __exit myvivi_exit(void)
{
	video_unregister_device(myvivi_device);
	video_device_release(myvivi_device);
}


module_init(myvivi_init);
module_exit(myvivi_exit);
MODULE_LICENSE("GPL");



Makefile

KERN_DIR = /usr/src/linux-headers-2.6.31-14-generic



all:

	make -C $(KERN_DIR) M=`pwd` modules 



clean:

	make -C $(KERN_DIR) M=`pwd` modules clean

	rm -rf modules.order



obj-m	+= myvivi.o

如果之前有安装驱动的话,先卸载:sudo rmmod vivi

编译测试:

book@book-desktop:/work/projects/myvivi$ sudo insmod myvivi.ko 
insmod: error inserting 'myvivi.ko': -1 Unknown symbol in module

还缺少一些东西,进去video_register_device看一下,没有release函数,所以马上返回一个错误,从dmesg也可以看一下

视频中是

正好就是文件的395行,我估计他这里又是在后面改了什么,然后这里又不说

 

我的3.4.2

 

所以加上release函数,怎么加,先看怎么定义的

这里是缺少了依赖:

sudo modprobe vivi
sudo rmmod vivi

sudo myvivi.ko

这样就可以了

3.1:搭建框架

3.2:入口函数 myvivi_init

思路:入口函数就是分配设置和注册一个 video_device 结构体变量

 

3.2.1 分配:

3.2.2 设置

注意 1:

的参数 3 为什么是-1:内核中该参数的说明如下: -1,代表如果内核自动给我们分配第一个可以用的数字来构成设备节点

注意 2:
设置的时候要设置哪些内容?
参照 vivi.c
它的实现机制是:通过将 vivi_template 的内容赋值给我们的分配的结构体

所以 vivi_template 的内容就是要设置的内容

所以我们要
设置 1: video_device.release

设置 2: fops 成员

参照 vivi.c 中,看他实现了哪些 fops 里面的成员

1: v4l2_file_operations. Open


1.1:初始化缓冲区队列


Open 函数里面的内容我们要根据之前的分析或者去看 vivi.c 里面的源码,我们的摄像头之后要分配缓冲区,并把这些缓冲区
放入队列中,而队列的初始化我们可以放到 open 里面实现:
先定义一个 videobuf_queue 变量,然后在 open 函数中初始化,供给队列的管理和操作函数使用

参数解释在本章的 关键点分析 一节中有部分描述,大部分可以参照 vivi.c 来设置
参数 1:是个队列,所以我们在本地声明了一个队列

参数 2:是对缓冲区的管理操作函数
要我们自己实现:
1:声明一个 videobuf_queue_ops 变量,供给 open 函数里面初始化队列使用

1.2:实现管理缓冲区四个函数:


videobuf_queue_ops. buf_setup
 

videobuf_queue_ops. buf_prepare

videobuf_queue_ops. buf_queue
 

videobuf_queue_ops. buf_release

参照 vivi.c,发现代码如下:

发现 vivi.c 是通过一个 free_buffer 函数来实现的,

也就是主要通过 videobuf_vmalloc_free 来实现的,传入的参数就是 videobuf_buffer


1.3:初始化定时器

2: v4l2_file_operations. release

这个直接参照 vivi

由于我们用了定时器机制,我们 close 的时候还要在这里删除定时器

3: v4l2_file_operations. mmap

 

4: v4l2_file_operations.ioctl

5: v4l2_file_operations. poll

设置 3: ioctl_ops


1:分配一个 v4l2_ioctl_ops 变量

我们这里的成员比 vivi.c 中的个数少了一些,是因为我们之前分析过只有 11 个 ioctl 是必要的


2:将我们的 video_device 的成员 ioctl_ops 设置为刚才分配的 v4l2_ioctl_ops 变量
 

这样理论上这个成员就设置完了,只不过要我们去完善 v4l2_ioctl_ops 里面的 11 个 ioctl。
但是我们之前分析过 vivi.c 的 ioctl 的调用过程:
用户先发送命令,调用我们 v4l2 的 ioctl,从而调用我们 vivi.c 中的 ioctl 函数,然后

下面我们来实现各个 11 个 ioctl


1: vidioc_querycap

这里的 capabilities 在 vivi.c 中是

表面它是一个摄像头捕获设备,可以通过数据流或者读写来获取数据
我们知道我们摄像头可以通过 ioctl 来读取摄像头缓冲区的数据,其实也可以不通过 ioctl,也可以通过 read write 函数,
我们这里就让他只能通过 ioctl 来获取数据
 

2: vidioc_enum_fmt_vid_cap

用于列举摄像头的数据的格式
我们这里就让他只能支持一种摄像头的格式

3: vidioc_g_fmt_vid_cap

其实就是把本地的一种摄像头数据格式返回给应用层,我们这里只是定义了一个 v4l2_format 结构体变量,并没有初始化,
然后就在 vidioc_g_fmt_vid_cap 函数中使用,是因为应用层在调用 ioctl 的命令的过程是先 vidioc_s_fmt_vid_cap,然
后再 vidioc_g_fmt_vid_cap,所以不会报错
 

4: vidioc_try_fmt_vid_cap


当我们 set 摄像头格式的时候,要先去 try,因为担心我们的摄像头不支持我们应用层要 set 的摄像头格式

5: vidioc_s_fmt_vid_cap

这里我们传入的给 try 的第二个参数设置为 NULL,我们没有用到
 

6: vidioc_reqbufs

 

7: vidioc_querybuf

8: vidioc_qbuf

9: vidioc_dqbuf

10: myvivi_vidioc_streamon

11: myvivi_vidioc_streamoff

设置 4:队列操作


我们前面 3 个设置部分都可以从 vivi_template 中知道要做哪些步骤,但是为何要设置队列操作是因为我们分析过代码,知
道我们如果摄像头需要请求分配缓冲区话,我们就要通过一个队列和一个自旋锁还有队列的管理操作函数来进行对缓冲区的管
理分配工作。
队列的初始化我们在 v4l2_file_operations.Open 里面实现了
自旋锁的初始化我们就在这里初始化,因为在 open 函数里面初始化队列的时候会用到这把锁
对缓冲区的管理操作函数我们也是在 open 函数中初始化队列里有用到
所以我们 init 函数里面只要先初始化一把锁就好:
 

 

3.2.3 注册

 

3.2.4 设置定时器产生数据并唤醒进程


在 vivi.c 中由于我们没有实际的数据,所以我们只能自己产生数据,并去唤醒进程,因为应用层用 select 一直在查询缓冲
区是否有数据,所以我们这里产生了数据之后要去唤醒进程
Vivi.c 中采用了一个线程专门去产生数据和唤醒,我们这里利用定时器, 1/30s 产生一次数据,并去唤醒


1: 首先我们要设置一个定时器

2: 初始化定时器

 

3:加入内核定时器链表


我们这里只在 init 函数里面初始化,先不把它加入内核定时器链表,因为加入链表就代表定时器开始计时,开始使用了,我
们在 open 的时候做这一步, linux 写代码的原则,用到了该资源再分配该资源:
在 open 里面去加入内核定时器链表


4:在 close 里面要删除定时器

5: 产生数据


我们要在定时器的操作函数里面去产生数据。
我们要产生数据,首先要能提取到缓冲区,我们可以设置一个本地队列,当应用层调用 VIDIOC_QBUF 将缓冲区入队列的时
候,我们同时将缓冲区也加入到我们本地的一个队列中,然后定时器时间到,从本地队列中取出一个缓冲区然后赋数据


5.1: 设置本地队列

5.2:在 init 函数中初始化队列:

5.3: 当应用层调用 VIDIOC_QBUF 将缓冲区入队列的时候,我们同时将缓冲区也加入到我们本地的一个队列中

5.4:构造数据,唤醒进程

 

3.2:出口函数 myvivi_exit


思路:我们在入口函数 myvivi_init 中分配了一个结构体,并且注册了这个结构体,那么我们出口函数就要注销这个结构
体,并且好释放这个结构体,只不过顺序要和 myvivi_init 相反,先注销再释放

Q:疑惑:
Q1:在写虚拟驱动中,我们构造数据是通过定时器来实现的,那么定时器是什么时候启动的?我们装载驱动的时候定时器已经运行
了么?还是等我们运行了相关的应用程序才会启动?逻辑是怎么样的?
Q2:VIVI.C 中是通过创建了一个新的线程来实现的,分析下
Q3:在分析 USB 驱动的时候,有用到中断,学习下中断
Q4:在宋宝华一书中,中断是放在了处理并发的情况的,处理并发, 中断屏蔽、原子操作、自
旋锁和信号量等并发控制机制这些操作和线程的进程的通信的手段的联系是什么?
Q1:我们在写 11 个必要的 ioctl 的时候,其中有 vidioc_g_fmt_vid_cap 和 vidioc_s_fmt_vid_cap 用来获取和设置摄像头的数
据格式,我们这两个函数的实现方法都是:
static struct v4l2_format myvivi_format;
/* 返回当前所使用的格式 */
static int myvivi_vidioc_g_fmt_vid_cap(struct file *file, void *priv,struct v4l2_format *f)
{
memcpy(f, &myvivi_format, sizeof(myvivi_format));
return (0);}
static int myvivi_vidioc_s_fmt_vid_cap(struct file *file, void *priv,struct v4l2_format *f)
{
int ret = myvivi_vidioc_try_fmt_vid_cap(file, NULL, f);
if (ret < 0)
return ret;
memcpy(&myvivi_format, f, sizeof(myvivi_format));
return ret;
}
我们自己定义的 static struct v4l2_format myvivi_format 并没有看到在哪里有初始化,然后我们的 vivi.c 就直接使用了
myvivi_vidioc_g_fmt_vid_cap 函数,难道不会报错么?然后我查阅了之前 xawtv 的系统调用,发现并没有调用
VIDIOC_G_FMT,只是有调用 VIDIOC_G_FMT,所以我在想是不是因为这个原因,才没有导致我们的程序出错的。
Q2:
我自己通过看内核原本的 vivi.c 的实现过程如下:

Vivi.c 中是在 open 函数里面将一个 vivi_fh 的信息记录到了 file 结构体的私有数据段中: file->private_data
下面的这个的私有 priv 成员我看貌似基本所有 vivi 的函数都有涉及。
static int vidioc_g_fmt_vid_cap(struct file *file, void *priv,
struct v4l2_format *f)
{
struct vivi_fh *fh = priv;
f->fmt.pix.width = fh->width;
f->fmt.pix.height = fh->height;
f->fmt.pix.field = fh->vb_vidq.field;
f->fmt.pix.pixelformat = fh->fmt->fourcc;
f->fmt.pix.bytesperline =
(f->fmt.pix.width * fh->fmt->depth) >> 3;
f->fmt.pix.sizeimage =
f->fmt.pix.height * f->fmt.pix.bytesperline;
return (0);
}
内核通过 struct vivi_fh *fh = priv;将 open 函数中已经初始化好了参数就可以直接返回给应用层了,但是有点没明白,
这个系统调用函数中的 void *priv 参数和我们之前 open 函数中初始化好了的 file->private_data 是怎么对应起来的呢?
这一点请老师帮忙仔细分析下
An:
正确的使用流程是:先 vidioc_enum_fmt_vid_cap,再 set, 后 get,如果先 get 再 set 确实有问题
Q: 我看内核 vivi 里面涉及的队列都很多,我先把我理解的队列的作用说下,不理解的和错
的请老师说明下:
1: struct vivi_dev xxx.vivi_dmaqueue.active :
这个队列的作用应该是和老师代码中的本地队列 myvivi_vb_local_queue 的作用是一样的:
通过 v4l2 buffer 层的 buffer_queue 函数来实现有有数据时入队列,只不过这个队列是给本
地队列,为了通知定时器(如果我们实现虚假摄像头数据的方式是通过定时器的话)来产
生数据的
2: struct vivi_fh xxx.videobuf_queue.stream
这个应该就是我们的应用层调用入队列命令时,将分配的缓冲区入队列的那个队列了,也
就是说我们的分配的缓冲区和新增的缓冲区都会在这个队列上
3: struct vivi_fh xxx.videobuf_queue.videobuf_buffer.list_head
这个队列不理解什么作用,我没懂缓冲区里面为什么要有一个队列。我们的缓冲区与缓冲
区之间是通过 struct vivi_fh xxx.videobuf_queue.stream 这个队列来连接的,那么缓冲区里面
的队列有什么用?
其实还是回到了下面这个函数过程:
int videobuf_qbuf(struct videobuf_queue *q,struct v4l2_buffer *b)
struct videobuf_buffer *buf;
buf = q->bufs[b->index];
list_add_tail(&buf->stream, &q->stream);
q->ops->buf_queue(q, buf);
上面的过程我都能懂,就是这个入队列看的很别扭,
上门的过程最后的队列是:
q->stream --》 buf->stream
我们说链表的节点每个成员的数据类型不应该是一样的么?
这里的 q 和 buf 的数据类型不一样,而且 buf 还是 q 的成员,怎么理解?
额外理解:

也就是说当应用层调用 VIDIOC_QBUF 的时候,缓冲区会通过 videobuf_qbuf 函数放入 q->stream 的队列
放入之后我们会调用 buf_queue 函数,在这个函数里面我们又会将这个缓冲区放入本地队列

本地队列是为了给定时器产生数据用的
比如说定时时间到,由于我们的缓冲区可以通过本地队列找到(在上面我们已经把缓冲区放入到本地队列中了),我们在定时
器处理函数中从本地队列取出第一个缓冲区,然后给缓冲区赋值,然后从本地队列中删除这个缓冲区。应用层他是从 stream
队列中取到了缓冲区数据后,也会把他从队列中删除, 操作完之后,又会调用 VIDOC_QBUF 命令再次入队列
 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是一个驱动Xilinx FPGA板子中光口的示例代码。这个示例使用了Vivado中的Xilinx AXI Ethernet Subsystem IP来驱动光口。 ```verilog module top( // 输入时钟 input wire clk, // 输入复位信号 input wire rstn, // 光口接收数据 input wire [7:0] rx_data, // 光口接收数据有效信号 input wire rx_dv, // 光口接收错误信号 input wire rx_err, // 光口发送使能信号 output wire tx_en, // 光口发送数据 output wire [7:0] tx_data, // 光口发送数据使能信号 output wire tx_dv ); // Xilinx AXI Ethernet Subsystem IP实例化 axis_ethernet_subsystem ethernet_subsystem( // 时钟和复位信号 .s_axis_aclk(clk), .s_axis_aresetn(rstn), // 光口接收数据 .s_axis_tdata(rx_data), .s_axis_tvalid(rx_dv), .s_axis_tlast(1'b0), .s_axis_tuser(1'b0), .s_axis_tkeep(8'b11111111), .s_axis_tready(1'b1), .s_axis_tdest(8'h00), .s_axis_tid(8'h00), .s_axis_tdest(8'h00), .m_axis_tdata(tx_data), .m_axis_tvalid(tx_dv), .m_axis_tlast(1'b0), .m_axis_tuser(1'b0), .m_axis_tkeep(8'b11111111), .m_axis_tready(tx_en), .m_axis_tdest(8'h00), .m_axis_tid(8'h00), .m_axis_tdest(8'h00), .s_axis_terror(rx_err) ); endmodule ``` 这个示例中,我们实例化了一个名为`ethernet_subsystem`的Xilinx AXI Ethernet Subsystem IP,并将它的输入和输出端口与光口的信号相连接。这样,当我们将这个设计生成比特流文件并下载到FPGA板子中后,它就能够驱动光口了。 需要注意的是,这个示例代码只是一个简单的例子,具体的设计和代码实现会根据你的具体需求和板子型号有所不同。你需要根据相应的使用手册和参考资料进行具体的实现和调试。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值