1. 程式人生 > >v4l2驅動架構分析

v4l2驅動架構分析

1、概述

Video4Linux2是Linux核心中關於視訊裝置的核心驅動框架,為上層的訪問底層的視訊裝置提供了統一的介面。凡是核心中的子系統都有抽象底層硬體的差異,為上層提供統一的介面和提取出公共程式碼避免程式碼冗餘等好處。就像公司的老闆一般都不會直接找底層的員工談話,而是找部門經理了解情況,一個是因為底層屌絲人數多,意見各有不同,措辭也不準,部門經理會把情況彙總後再向上彙報;二個是老闆時間寶貴。

         V4L2支援三類裝置:視訊輸入輸出裝置、VBI裝置和radio裝置(其實還支援更多型別的裝置,暫不討論),分別會在/dev目錄下產生videoX、radioX和vbiX裝置節點。我們常見的視訊輸入裝置主要是攝像頭,也是本文主要分析物件。下圖V4L2在

linux系統中的結構圖:



Linux系統中視訊輸入裝置主要包括以下四個部分:

字元裝置驅動程式核心:V4L2本身就是一個字元裝置,具有字元裝置所有的特性,暴露介面給使用者空間;

V4L2驅動核心:主要是構建一個核心中標準視訊裝置驅動的框架,為視訊操作提供統一的介面函式;

平臺V4L2裝置驅動:在V4L2框架下,根據平臺自身的特性實現與平臺相關的V4L2驅動部分,包括註冊video_device和v4l2_dev。

具體的sensor驅動:主要上電、提供工作時鐘、視訊影象裁剪、流IO開啟等,實現各種裝置控制方法供上層呼叫並註冊v4l2_subdev。

V4L2的核心原始碼位於drivers/media/v4l2-core,原始碼以實現的功能可以劃分為四類:

核心模組實現:由v4l2-dev.c實現,主要作用申請字元主裝置號、註冊class和提供video device註冊登出等相關函式;

V4L2框架:由v4l2-device.c、v4l2-subdev.c、v4l2-fh.c、v4l2-ctrls.c等檔案實現,構建V4L2框架;

Videobuf管理:由videobuf2-core.c、videobuf2-dma-contig.c、videobuf2-dma-sg.c、videobuf2-memops.c、videobuf2-vmalloc.c、v4l2-mem2mem.c等檔案實現,完成videobuffer的分配、管理和登出。

Ioctl框架:由v4l2-ioctl.c檔案實現,構建V4L2ioctl的框架。

2、V4L2框架

         結構體v4l2_device、video_device、v4l2_subdev和v4l2_fh是搭建框架的主要元素。下圖是V4L2框架的結構圖:


從上圖V4L2框架是一個標準的樹形結構,v4l2_device充當了父裝置,通過連結串列把所有註冊到其下的子裝置管理起來,這些裝置可以是GRABBER、VBI或RADIO。V4l2_subdev是子裝置,v4l2_subdev結構體包含了對裝置操作的ops和ctrls,這部分程式碼和硬體相關,需要驅動工程師根據硬體實現,像攝像頭裝置需要實現控制上下電、讀取ID、飽和度、對比度和視訊資料流開啟關閉的介面函式。Video_device用於建立子裝置節點,把操作裝置的介面暴露給使用者空間。V4l2_fh是每個子裝置的檔案控制代碼,在開啟裝置節點檔案時設定,方便上層索引到v4l2_ctrl_handler,v4l2_ctrl_handler管理裝置的ctrls,這些ctrls(攝像頭裝置)包括調節飽和度、對比度和白平衡等。

v4l2_device

v4l2_device在v4l2框架中充當所有v4l2_subdev的父裝置,管理著註冊在其下的子裝置。以下是v4l2_device結構體原型(去掉了無關的成員):

struct v4l2_device {

         structlist_head subdevs;    //用連結串列管理註冊的subdev

         charname[V4L2_DEVICE_NAME_SIZE];    //device 名字

         structkref ref;      //引用計數

         ……

};

可以看出v4l2_device的主要作用是管理註冊在其下的子裝置,方便系統查詢引用到。

V4l2_device的註冊和登出:

int v4l2_device_register(struct device*dev, struct v4l2_device *v4l2_dev)

static void v4l2_device_release(struct kref *ref)

V4l2_subdev

V4l2_subdev代表子裝置,包含了子裝置的相關屬性和操作。先來看下結構體原型:

struct v4l2_subdev {

         structv4l2_device *v4l2_dev;  //指向父裝置

         //提供一些控制v4l2裝置的介面

         conststruct v4l2_subdev_ops *ops;

         //V4L2框架提供的介面函式

         conststruct v4l2_subdev_internal_ops *internal_ops;

         //subdev控制介面

         structv4l2_ctrl_handler *ctrl_handler;

         /* namemust be unique */

         charname[V4L2_SUBDEV_NAME_SIZE];

         /*subdev device node */

         structvideo_device *devnode;  

};

每個子裝置驅動都需要實現一個v4l2_subdev結構體,v4l2_subdev可以內嵌到其它結構體中,也可以獨立使用。結構體中包含了對子裝置操作的成員v4l2_subdev_opsv4l2_subdev_internal_ops

v4l2_subdev_ops結構體原型如下:

struct v4l2_subdev_ops {

//視訊裝置通用的操作:初始化、載入FW、上電和RESET

         conststruct v4l2_subdev_core_ops        *core;

//tuner特有的操作

         conststruct v4l2_subdev_tuner_ops      *tuner;

//audio特有的操作

         conststruct v4l2_subdev_audio_ops      *audio;

//視訊裝置的特有操作:設定幀率、裁剪影象、開關視訊流等

……

};

視訊裝置通常需要實現core和video成員,這兩個OPS中的操作都是可選的,但是對於視訊流裝置video->s_stream(開啟或關閉流IO)必須要實現。

v4l2_subdev_internal_ops結構體原型如下:

    //subdev註冊時被呼叫,讀取ICID來進行識別

         int(*registered)(struct v4l2_subdev *sd);

         void(*unregistered)(struct v4l2_subdev *sd);

//當裝置節點被開啟時呼叫,通常會給裝置上電和設定視訊捕捉FMT

         int(*open)(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh);

         int(*close)(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh);

};

v4l2_subdev_internal_ops是向V4L2框架提供的介面,只能被V4L2框架層呼叫。在註冊或開啟子裝置時,進行一些輔助性操作。

Subdev的註冊和登出

當我們把v4l2_subdev需要實現的成員都已經實現,就可以呼叫以下函式把子設備註冊到V4L2核心層:

int v4l2_device_register_subdev(struct v4l2_device*v4l2_dev, struct v4l2_subdev *sd)

當解除安裝子裝置時,可以呼叫以下函式進行登出:

void v4l2_device_unregister_subdev(struct v4l2_subdev*sd)

video_device

         video_device結構體用於在/dev目錄下生成裝置節點檔案,把操作裝置的介面暴露給使用者空間

struct video_device

{

         conststruct v4l2_file_operations *fops;  //V4L2裝置操作集合

         /*sysfs */

         structdevice dev;             /* v4l device */

         structcdev *cdev;            //字元裝置

         /* Seteither parent or v4l2_dev if your driver uses v4l2_device */

         structdevice *parent;              /* deviceparent */

         structv4l2_device *v4l2_dev;          /*v4l2_device parent */

         /*Control handler associated with this device node. May be NULL. */

         structv4l2_ctrl_handler *ctrl_handler;

         /* 指向video buffer佇列*/

         structvb2_queue *queue;

         intvfl_type;      /* device type */

         intminor;  //次裝置號

         /* V4L2file handles */

         spinlock_t                  fh_lock; /* Lock for allv4l2_fhs */

         structlist_head        fh_list; /* List ofstruct v4l2_fh */

         /*ioctl回撥函式集,提供file_operations中的ioctl呼叫 */

         conststruct v4l2_ioctl_ops *ioctl_ops;

         ……

};

Video_device分配和釋放,用於分配和釋放video_device結構體:

struct video_device *video_device_alloc(void)

void video_device_release(struct video_device *vdev)

video_device註冊和登出,實現video_device結構體的相關成員後,就可以呼叫下面的介面進行註冊:

static inline int __must_checkvideo_register_device(struct video_device *vdev,

                   inttype, int nr)

void video_unregister_device(struct video_device*vdev);

vdev:需要註冊和登出的video_device;

type:裝置型別,包括VFL_TYPE_GRABBER、VFL_TYPE_VBI、VFL_TYPE_RADIO和VFL_TYPE_SUBDEV。

nr:裝置節點名編號,如/dev/video[nr]。

v4l2_fh

         v4l2_fh是用來儲存子裝置的特有操作方法,也就是下面要分析到的v4l2_ctrl_handler,核心提供一組v4l2_fh的操作方法,通常在開啟裝置節點時進行v4l2_fh註冊。

初始化v4l2_fh,新增v4l2_ctrl_handler到v4l2_fh:

void v4l2_fh_init(struct v4l2_fh *fh, structvideo_device *vdev)

新增v4l2_fh到video_device,方便核心層呼叫到:

void v4l2_fh_add(struct v4l2_fh *fh)

v4l2_ctrl_handler

v4l2_ctrl_handler是用於儲存子裝置控制方法集的結構體,對於視訊裝置這些ctrls包括設定亮度、飽和度、對比度和清晰度等,用連結串列的方式來儲存ctrls,可以通過v4l2_ctrl_new_std函式向連結串列新增ctrls。

struct v4l2_ctrl *v4l2_ctrl_new_std(structv4l2_ctrl_handler *hdl,

                            conststruct v4l2_ctrl_ops *ops,

                            u32id, s32 min, s32 max, u32 step, s32 def)

hdl是初始化好的v4l2_ctrl_handler結構體;

ops是v4l2_ctrl_ops結構體,包含ctrls的具體實現;

id是通過IOCTL的arg引數傳過來的指令,定義在v4l2-controls.h檔案;

min、max用來定義某操作物件的範圍。如:

v4l2_ctrl_new_std(hdl, ops, V4L2_CID_BRIGHTNESS,-208, 127, 1, 0);

使用者空間可以通過ioctl的VIDIOC_S_CTRL指令呼叫到v4l2_ctrl_handler,id透過arg引數傳遞。

3、ioctl框架

         你可能觀察到使用者空間對V4L2裝置的操作基本都是ioctl來實現的,V4L2裝置都有大量可操作的功能(配置暫存器),所以V4L2的ioctl也是十分龐大的。它是一個怎樣的框架,是怎麼實現的呢?

         Ioctl框架是由v4l2_ioctl.c檔案實現,檔案中定義結構體陣列v4l2_ioctls,可以看做是ioctl指令和回撥函式的關係表。使用者空間呼叫系統呼叫ioctl,傳遞下來ioctl指令,然後通過查詢此關係表找到對應回撥函式。

以下是擷取陣列的兩項:

IOCTL_INFO_FNC(VIDIOC_QUERYBUF, v4l_querybuf,v4l_print_buffer, INFO_FL_QUEUE | INFO_FL_CLEAR(v4l2_buffer, length)),

IOCTL_INFO_STD(VIDIOC_G_FBUF, vidioc_g_fbuf,v4l_print_framebuffer, 0),

         核心提供兩個巨集(IOCTL_INFO_FNCIOCTL_INFO_STD)來初始化結構體,引數依次是ioctl指令、回撥函式或者v4l2_ioctl_ops結構體成員、debug函式、flag。如果回撥函式是v4l2_ioctl_ops結構體成員,則使用IOCTL_INFO_STD;如果回撥函式是v4l2_ioctl.c自己實現的,則使用IOCTL_INFO_FNC

IOCTL呼叫的流程圖如下:


  使用者空間通過開啟/dev/目錄下的裝置節點,獲取到檔案的file結構體,通過系統呼叫ioctl把cmd和arg傳入到核心。通過一系列的呼叫後最終會呼叫到__video_do_ioctl函式,然後通過cmd檢索v4l2_ioctls[],判斷是INFO_FL_STD還是INFO_FL_FUNC。如果是INFO_FL_STD會直接呼叫到視訊裝置驅動中video_device->v4l2_ioctl_ops函式集。如果是INFO_FL_FUNC會先呼叫到v4l2自己實現的標準回撥函式,然後根據arg再呼叫到video_device->v4l2_ioctl_ops或v4l2_fh->v4l2_ctrl_handler函式集。

4、IO訪問

V4L2支援三種不同IO訪問方式(核心中還支援了其它的訪問方式,暫不討論):

read和write,是基本幀IO訪問方式,通過read讀取每一幀資料,資料需要在核心和使用者之間拷貝,這種方式訪問速度可能會非常慢;

記憶體對映緩衝區(V4L2_MEMORY_MMAP),是在核心空間開闢緩衝區,應用通過mmap()系統呼叫對映到使用者地址空間。這些緩衝區可以是大而連續DMA緩衝區、通過vmalloc()建立的虛擬緩衝區,或者直接在裝置的IO記憶體中開闢的緩衝區(如果硬體支援);

使用者空間緩衝區(V4L2_MEMORY_USERPTR),是使用者空間的應用中開闢緩衝區,使用者與核心空間之間交換緩衝區指標。很明顯,在這種情況下是不需要mmap()呼叫的,但驅動為有效的支援使用者空間緩衝區,其工作將也會更困難。

Read和write方式屬於幀IO訪問方式,每一幀都要通過IO操作,需要使用者和核心之間資料拷貝,而後兩種是流IO訪問方式,不需要記憶體拷貝,訪問速度比較快。記憶體對映緩衝區訪問方式是比較常用的方式。

記憶體對映快取區方式

         硬體層的資料流傳輸

         Camerasensor捕捉到影象資料通過並口或MIPI傳輸到CAMIF(camera interface),CAMIF可以對影象資料進行調整(翻轉、裁剪和格式轉換等)。然後DMA控制器設定DMA通道請求AHB將影象資料傳到分配好的DMA緩衝區。


         待影象資料傳輸到DMA緩衝區之後,mmap操作把緩衝區對映到使用者空間,應用就可以直接訪問緩衝區的資料。

vb2_queue

為了使裝置支援流IO這種方式,驅動需要實現struct vb2_queue,來看下這個結構體:

struct vb2_queue {

         enumv4l2_buf_type                  type;  //buffer型別

         unsignedint                        io_modes;  //訪問IO的方式:mmapuserptr etc

         conststruct vb2_ops                 *ops;   //buffer佇列操作函式集合

         conststruct vb2_mem_ops     *mem_ops;  //buffer memory操作集合

         structvb2_buffer              *bufs[VIDEO_MAX_FRAME];  //代表每個buffer

         unsignedint                        num_buffers;    //分配的buffer個數

……

};

Vb2_queue代表一個videobuffer佇列,vb2_buffer是這個佇列中的成員,vb2_mem_ops是緩衝記憶體的操作函式集,vb2_ops用來管理佇列。

vb2_mem_ops

         vb2_mem_ops包含了記憶體對映緩衝區、使用者空間緩衝區的記憶體操作方法:

         void           *(*alloc)(void *alloc_ctx, unsignedlong size);  //分配視訊快取

         void           (*put)(void *buf_priv);            //釋放視訊快取

         void           *(*get_userptr)(void *alloc_ctx,unsigned long vaddr, 

                                               unsignedlong size, int write);

         void           (*put_userptr)(void *buf_priv);       //釋放使用者空間視訊緩衝區指標

//用於快取同步

         void           (*prepare)(void *buf_priv);

         void           (*finish)(void *buf_priv);

         void           *(*vaddr)(void *buf_priv);

         void           *(*cookie)(void *buf_priv);

         unsignedint     (*num_users)(void *buf_priv);         //返回當期在使用者空間的buffer

         int              (*mmap)(void *buf_priv, structvm_area_struct *vma);  //把緩衝區對映到使用者空間

};

         這是一個相當龐大的結構體,這麼多的結構體需要實現還不得累死,幸運的是核心都已經幫我們實現了。提供了三種類型的視訊快取區操作方法:連續的DMA緩衝區、集散的DMA緩衝區以及vmalloc建立的緩衝區,分別由videobuf2-dma-contig.c、videobuf2-dma-sg.c和videobuf-vmalloc.c檔案實現,可以根據實際情況來使用。

         vb2_ops是用來管理buffer佇列的函式集合,包括佇列和緩衝區初始化

struct vb2_ops {

         //佇列初始化

         int(*queue_setup)(struct vb2_queue *q, const struct v4l2_format *fmt,

                               unsigned int *num_buffers, unsigned int*num_planes,

                               unsigned int sizes[], void *alloc_ctxs[]);

         //釋放和獲取裝置操作鎖

         void(*wait_prepare)(struct vb2_queue *q);

         void(*wait_finish)(struct vb2_queue *q);

         //buffer的操作

         int(*buf_init)(struct vb2_buffer *vb);

         int(*buf_prepare)(struct vb2_buffer *vb);

         int(*buf_finish)(struct vb2_buffer *vb);

         void(*buf_cleanup)(struct vb2_buffer *vb);

//開始視訊流

         int(*start_streaming)(struct vb2_queue *q, unsigned int count);

//停止視訊流

         int(*stop_streaming)(struct vb2_queue *q);

//VB傳遞給驅動

         void(*buf_queue)(struct vb2_buffer *vb);

};

vb2_buffer是快取佇列的基本單位,內嵌在其中v4l2_buffer是核心成員。當開始流IO時,幀以v4l2_buffer的格式在應用和驅動之間傳輸。一個緩衝區可以有三種狀態:

在驅動的傳入佇列中,驅動程式將會對此佇列中的緩衝區進行處理,使用者空間通過IOCTL:VIDIOC_QBUF把緩衝區放入到佇列。對於一個視訊捕獲裝置,傳入佇列中的緩衝區是空的,驅動會往其中填充資料;

在驅動的傳出佇列中,這些緩衝區已由驅動處理過,對於一個視訊捕獲裝置,快取區已經填充了視訊資料,正等使用者空間來認領;

使用者空間狀態的佇列,已經通過IOCTL:VIDIOC_DQBUF傳出到使用者空間的緩衝區,此時緩衝區由使用者空間擁有,驅動無法訪問。

這三種狀態的切換如下圖所示:

v4l2_buffer結構如下:

struct v4l2_buffer {

         __u32                          index;  //buffer 序號

         __u32                          type;   //buffer型別

         __u32                          bytesused;  緩衝區已使用byte

         __u32                          flags;

         __u32                          field;

         structtimeval           timestamp;  //時間戳,代表幀捕獲的時間

         structv4l2_timecode       timecode;

         __u32                          sequence;

         /*memory location */

         __u32                          memory;  //表示緩衝區是記憶體對映緩衝區還是使用者空間緩衝區

         union {

                   __u32           offset;  //核心緩衝區的位置

                   unsignedlong   userptr;   //緩衝區的使用者空間地址

                   structv4l2_plane *planes;

                   __s32                 fd;

         } m;

         __u32                          length;   //緩衝區大小,單位byte

};

當用戶空間拿到v4l2_buffer,可以獲取到緩衝區的相關資訊。Byteused是影象資料所佔的位元組數,如果是V4L2_MEMORY_MMAP方式,m.offset是核心空間影象資料存放的開始地址,會傳遞給mmap函式作為一個偏移,通過mmap對映返回一個緩衝區指標p,p+byteused是影象資料在程序的虛擬地址空間所佔區域;如果是使用者指標緩衝區的方式,可以獲取的影象資料開始地址的指標m.userptr,userptr是一個使用者空間的指標,userptr+byteused便是所佔的虛擬地址空間,應用可以直接訪問。

5、使用者空間訪問裝置

下面通過核心對映緩衝區方式訪問視訊裝置(capturedevice)的流程。

1>    開啟裝置檔案

fd = open(dev_name, O_RDWR /* required */ | O_NONBLOCK, 0);

dev_name[/dev/videoX]

2>    查詢裝置支援的能力

Struct v4l2_capability  cap;

ioctl(fd, VIDIOC_QUERYCAP, &cap)

3>    設定視訊捕獲格式

fmt.type= V4L2_BUF_TYPE_VIDEO_CAPTURE;

fmt.fmt.pix.width       = 640;

fmt.fmt.pix.height      = 480;

fmt.fmt.pix.pixelformat= V4L2_PIX_FMT_YUYV;  //畫素格式

fmt.fmt.pix.field       = V4L2_FIELD_INTERLACED;

ioctl(fd,VIDIOC_S_FMT, & fmt)

4>    向驅動申請緩衝區

Struct  v4l2_requestbuffers req;

req.count= 4;  //緩衝個數

req.type= V4L2_BUF_TYPE_VIDEO_CAPTURE;

req.memory= V4L2_MEMORY_MMAP;

if(-1 == xioctl(fd, VIDIOC_REQBUFS, &req))

5>    獲取每個緩衝區的資訊,對映到使用者空間

structbuffer {

        void  *start;

        size_t length;

} *buffers;

buffers = calloc(req.count, sizeof(*buffers));

for (n_buffers= 0; n_buffers < req.count; ++n_buffers) {

struct  v4l2_buffer buf;

buf.type        = V4L2_BUF_TYPE_VIDEO_CAPTURE;

buf.memory      = V4L2_MEMORY_MMAP;

buf.index       = n_buffers;

if (-1 ==xioctl(fd, VIDIOC_QUERYBUF, & buf))

                       errno_exit(“VIDIOC_QUERYBUF”);

buffers[n_buffers].length= buf.length;

buffers[n_buffers].start=

        mmap(NULL /* start anywhere */,

        buf.length,

        PROT_READ | PROT_WRITE /* required */,

        MAP_SHARED /* recommended */,

        fd, buf.m.offset);

 }

6>    把緩衝區放入到傳入佇列上,開啟流IO,開始視訊採集

for (i =0; i < n_buffers; ++i) {

    struct v4l2_buffer buf;

    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;

    buf.memory = V4L2_MEMORY_MMAP;

    buf.index = i;

    if (-1 == xioctl(fd, VIDIOC_QBUF, &buf))

          errno_exit(“VIDIOC_QBUF”);

 }

 if (-1 == xioctl(fd, VIDIOC_STREAMON, & type))

7>  呼叫select監測檔案描述符,緩衝區的資料是否填充好,然後對視訊資料

        for (;;) {

                        fd_set fds;

                        struct timeval tv;

                        int r;

                        FD_ZERO(&amp;fds);

                        FD_SET(fd,&amp;fds);

                        /* Timeout. */

                        tv.tv_sec = 2;

                        tv.tv_usec = 0;

                                                        //監測檔案描述是否變化

                        r = select(fd + 1,& fds, NULL, NULL, & tv);

                        if (-1 == r) {

                                if (EINTR ==errno)

                                       continue;

                               errno_exit(“select”);

                        }

                        if (0 == r) {

                                fprintf(stderr,”select timeout\n”);

                               exit(EXIT_FAILURE);

                        }

                                                        //對視訊資料進行處理

                        if (read_frame())

                                break;

                        /* EAGAIN - continueselect loop. */

               }

8>    取出已經填充好的緩衝,獲取到視訊資料的大小,然後對資料進行處理。這裡取出的緩衝只包含緩衝區的資訊,並沒有進行視訊資料拷貝。

buf.type= V4L2_BUF_TYPE_VIDEO_CAPTURE;

buf.memory= V4L2_MEMORY_MMAP;

if (-1 ==ioctl(fd, VIDIOC_DQBUF, & buf))    //取出緩衝

           errno_exit(“VIDIOC_QBUF”);

process_image(buffers[buf.index].start,buf.bytesused);   //視訊資料處理

if (-1 ==xioctl(fd, VIDIOC_QBUF, & buf))  //然後又放入到傳入佇列

     errno_exit(“VIDIOC_QBUF”);

9>    停止視訊採集

type =V4L2_BUF_TYPE_VIDEO_CAPTURE;

ioctl(fd,VIDIOC_STREAMOff, & type);

10> 關閉裝置

Close(fd);

暫時分析到這裡,後續在更新!

Reference:

本文轉自:http://blog.csdn.NET/rubyboss/article/details/14053523