v4l2_device
v4l2_device在v4l2 框架中充當所有 v4l2_subdev的 父設備,管理着註冊在其下的子設備。 以下是v4l2_device結構體原型( 去掉了無關的成員 ):
1 2 3 4 5 6 7 8 9 10 11 | struct v4l2_device { structlist_head subdevs; //用鏈表管理註冊的subdev charname[V4L2_DEVICE_NAME_SIZE]; //device 名字 structkref ref ; //引用計數 …… } |
可以看出 v4l2_device 的主要作用是 管理註冊在其下的子設備,方便系統查找引用到。
V4l2_device 的註冊和註銷:
1 2 3 | int v4l2_device_register( struct device*dev, struct v4l2_device *v4l2_dev) static void v4l2_device_release( struct kref * ref )
|
V4l2_subdev
V4l2_subdev 代表子設備,包含了子設備的相關屬性和操作。先來看下結構體原型:
1 2 3 4 5 6 7 8 9 10 11 12 13 1 | struct v4l2_subdev { struct v4l2_device *v4l2_dev; //指向父設備 const struct v4l2_subdev_ops *ops; //提供一些控制v4l2設備的接口 const struct v4l2_subdev_internal_ops *internal_ops; //向V4L2框架提供的接口函數 struct v4l2_ctrl_handler *ctrl_handler; //subdev控制接口 charname[V4L2_SUBDEV_NAME_SIZE]; /* name must be unique * struct video_device *devnode; /*subdev device node */ };
|
每個子設備驅動都需要 實現一個 v4l2_subdev 結構體,v4l2_subdev可以內嵌到其它結構體中,也可以獨立使用。
結構體中包含了對子設備操作的成員v4l2_subdev_ops 和 v4l2_subdev_internal_ops。
v4l2_subdev_ops結構體原型如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 | struct v4l2_subdev_ops { const struct v4l2_subdev_core_ops *core; //視頻設備通用的操作:初始化、加載FW、上電和RESET等 const struct v4l2_subdev_tuner_ops *tuner; //tuner特有的操作 const struct v4l2_subdev_audio_ops *audio; //audio特有的操作 const struct v4l2_subdev_video_ops *video; //視頻設備的特有操作:設置幀率、裁剪圖像、開關視頻流等…… }; |
視頻設備通常需要實現 core和video成員,這兩個OPS中的操作都是可選的,但是對於視頻流設備 video->s_stream(開啓或關閉流IO) 必須要實現。
v4l2_subdev_internal_ops 結構體原型如下:
v4l2_subdev_internal_ops 是向V4L2框架提供的接口,只能被V4L2框架層調用。 在註冊或打開子設備時,進行一些輔助性操作。
Subdev的註冊和註銷
當我們把 v4l2_subdev 需要實現的成員都已經實現,就可以調用以下函數把 子設備註冊到V4L2核心層 :
1 | int v4l2_device_register_subdev( struct v4l2_device*v4l2_dev, struct v4l2_subdev *sd) |
當卸載子設備時,可以調用以下函數進行註銷:
1 | void v4l2_device_unregister_subdev( struct v4l2_subdev*sd) |
video_device
video_device 結構體用於在/dev目錄下生成設備節點文件,把操作設備的接口暴露給用戶空間。
Video_device分配和釋放,用於分配和釋放video_device結構體:
1 2 3 | struct video_device *video_device_alloc( void ) void video_device_release( struct video_device *vdev) |
video_device註冊和註銷,實現video_device結構體的相關成員後,就可以調用下面的接口進行註冊:
1 2 3 4 5 | static inline int __must_checkvideo_register_device( struct video_device *vdev, int type, 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:
1 | void v4l2_fh_init( struct v4l2_fh *fh, structvideo_device *vdev) |
添加v4l2_fh到video_device,方便核心層調用到:
1 | void v4l2_fh_add( struct v4l2_fh *fh) |
v4l2_ctrl_handler
v4l2_ctrl_handler是用於保存子設備控制方法集的結構體,對於視頻設備這些ctrls包括設置亮度、飽和度、對比度和清晰度等,
用鏈表的方式來保存ctrls,可以通過v4l2_ctrl_new_std函數向鏈表添加ctrls。
1 2 3 4 5 | 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用來定義某操作對象的範圍。如:
1 | 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指令,然後通過查找此關係表找到對應回調函數。
以下是截取數組的兩項:
1 2 3 | 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_FNC和IOCTL_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,來看下這個結構體:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | struct vb2_queue { enum v4l2_buf_type type; //buffer類型 unsigned int io_modes; //訪問IO的方式:mmap、userptr etc const struct vb2_ops *ops; //buffer隊列操作函數集合 const struct vb2_mem_ops *mem_ops; //buffer memory操作集合 struct vb2_buffer *bufs[VIDEO_MAX_FRAME]; //代表每個buffer unsigned int num_buffers; //分配的buffer個數 …… }; |
Vb2_queue代表一個videobuffer隊列,vb2_buffer是這個隊列中的成員,vb2_mem_ops是緩衝內存的操作函數集,vb2_ops用來管理隊列。
vb2_mem_ops
vb2_mem_ops包含了內存映射緩衝區、用戶空間緩衝區的內存操作方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | struct vb2_mem_ops { void *(*alloc)( void *alloc_ctx, unsignedlong size); //分配視頻緩存 void (*put)( void *buf_priv); //釋放視頻緩存 //獲取用戶空間視頻緩衝區指針 void *(*get_userptr)( void *alloc_ctx,unsigned long vaddr, unsigned long 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); unsigned int (*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
vb2_ops是用來管理buffer隊列的函數集合,包括隊列和緩衝區初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | 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結構如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | struct v4l2_buffer { __u32 index; //buffer 序號 __u32 type; //buffer類型 __u32 bytesused; 緩衝區已使用byte數 __u32 flags; __u32 field; struct timeval timestamp; //時間戳,代表幀捕獲的時間 struct v4l2_timecode timecode; __u32 sequence; /*memory location */ __u32 memory; //表示緩衝區是內存映射緩衝區還是用戶空間緩衝區 union { __u32 offset; //內核緩衝區的位置 unsigned long userptr; //緩衝區的用戶空間地址 struct v4l2_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> 打開設備文件
1 | fd = open(dev_name, O_RDWR /* required */ | O_NONBLOCK, 0); |
dev_name[/dev/videoX]
2> 查詢設備支持的能力
1 2 3 | Struct v4l2_capability cap; ioctl(fd, VIDIOC_QUERYCAP, &cap) |
3> 設置視頻捕獲格式
1 2 3 4 5 6 7 8 9 10 11 | 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> 向驅動申請緩衝區
1 2 3 4 5 6 7 8 9 | 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> 獲取每個緩衝區的信息,映射到用戶空間
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | 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,開始視頻採集
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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" ); } type = V4L2_BUF_TYPE_VIDEO_CAPTURE; if (-1 == xioctl(fd, VIDIOC_STREAMON, & type)) |
7> 調用select監測文件描述符,緩衝區的數據是否填充好,然後對視頻數據
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | for (;;) { fd_set fds; struct timeval tv; int r; FD_ZERO(&fds); FD_SET(fd,&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> 取出已經填充好的緩衝,獲取到視頻數據的大小,然後對數據進行處理。這裏取出的緩衝只包含緩衝區的信息,並沒有進行視頻數據拷貝。
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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> 停止視頻採集
1 2 3 | type =V4L2_BUF_TYPE_VIDEO_CAPTURE; ioctl(fd,VIDIOC_STREAMOff, & type); |
10> 關閉設備
1 | Close(fd);
|