轉自:https://blog.csdn.net/Guet_Kite/article/details/78574781
權聲明:本文為 風箏 博主原創文章,未經博主允許不得轉載!!!!!!謝謝合作 https://blog.csdn.net/Guet_Kite/article/details/78574781
你好!這里是風箏的博客,
歡迎和我一起交流。
上一章寫了V4L2框架:嵌入式Linux驅動筆記(十七)——詳解V4L2框架(UVC驅動)
現在來寫V4L2的重點,他的用戶空間操作函數集合:
const struct v4l2_file_operations uvc_fops = {
.owner = THIS_MODULE, .open = uvc_v4l2_open, .release = uvc_v4l2_release, .unlocked_ioctl = video_ioctl2, #ifdef CONFIG_COMPAT .compat_ioctl32 = uvc_v4l2_compat_ioctl32, #endif .read = uvc_v4l2_read, .mmap = uvc_v4l2_mmap, .poll = uvc_v4l2_poll, #ifndef CONFIG_MMU .get_unmapped_area = uvc_v4l2_get_unmapped_area, #endif };
看下open函數:
static int uvc_v4l2_open(struct file *file) { /*部分函數省略*/ struct uvc_streaming *stream; struct uvc_fh *handle; stream = video_drvdata(file);//獲取uvc視頻流 ret = usb_autopm_get_interface(stream->dev->intf);//喚醒設備 handle = kzalloc(sizeof *handle, GFP_KERNEL);//創建uvc句柄 if (stream->dev->users == 0) {//第一次時 ret = uvc_status_start(stream->dev, GFP_KERNEL);//uvc狀態開始,里面提交urb } stream->dev->users++; v4l2_fh_init(&handle->vfh, &stream->vdev); v4l2_fh_add(&handle->vfh); handle->chain = stream->chain;//捆綁uvc句柄和uvc視頻鏈 handle->stream = stream;//捆綁uvc句柄和uvc視頻流 handle->state = UVC_HANDLE_PASSIVE;//設置uvc狀態為未激活 file->private_data = handle;//將uvc句柄作為文件的私有數據 return 0; }
open函數不是我們這章的重點,用戶空間對V4L2設備的操作基本都是ioctl來實現的,我們看下ioctl函數:
long video_ioctl2(struct file *file, unsigned int cmd, unsigned long arg) { return video_usercopy(file, cmd, arg, __video_do_ioctl); }
可以看出video_usercopy函數就是從user空間copy復制ioctl的cmd和arg參數,然后進入__video_do_ioctl函數:
static long __video_do_ioctl(struct file *file, unsigned int cmd, void *arg) { /*部分內容省略*/ struct video_device *vfd = video_devdata(file); const struct v4l2_ioctl_ops *ops = vfd->ioctl_ops; const struct v4l2_ioctl_info *info; if (v4l2_is_known_ioctl(cmd)) { info = &v4l2_ioctls[_IOC_NR(cmd)];//判斷是INFO_FL_STD還是INFO_FL_FUNC } if (info->flags & INFO_FL_STD) {//如果是INFO_FL_STD typedef int (*vidioc_op)(struct file *file, void *fh, void *p); const void *p = vfd->ioctl_ops;//調用到ioctl_ops真正的ioctrl操作集 const vidioc_op *vidioc = p + info->u.offset;//通過偏移值找到要執行函數的地址 ret = (*vidioc)(file, fh, arg);//直接調用到視頻設備驅動中video_device->ioctl_ops } else if (info->flags & INFO_FL_FUNC) {//如果是INFO_FL_FUNC ret = info->u.func(ops, file, fh, arg);//調用到v4l2自己實現的標准回調函數 } }
我們可以看出,如果info->flags是INFO_FL_FUNC,會調用vfd->ioctl_ops函數集合里的某個函數(通過info->u.offset偏移值確定),那vfd->ioctl_ops是什么呢?其實就是上一章說的,uvc_register_video函數里的vdev->ioctl_ops = &uvc_ioctl_ops了。
如果info->flags是INFO_FL_FUNC,直接調用info->u.func(ops, file, fh, arg)函數
那info又是怎么確定的呢?當然是:info = &v4l2_ioctls[_IOC_NR(cmd)];
static struct v4l2_ioctl_info v4l2_ioctls[] = {//.ioctl, .u.func, .debug, .flags IOCTL_INFO_FNC(VIDIOC_QUERYCAP, v4l_querycap, v4l_print_querycap, 0),//列舉性能 IOCTL_INFO_FNC(VIDIOC_ENUM_FMT, v4l_enum_fmt, v4l_print_fmtdesc, INFO_FL_CLEAR(v4l2_fmtdesc, type)),//列舉格式 IOCTL_INFO_FNC(VIDIOC_G_FMT, v4l_g_fmt, v4l_print_format, 0), IOCTL_INFO_FNC(VIDIOC_S_FMT, v4l_s_fmt, v4l_print_format, INFO_FL_PRIO),//設置攝像頭使用某種格式 IOCTL_INFO_FNC(VIDIOC_REQBUFS, v4l_reqbufs, v4l_print_requestbuffers, INFO_FL_PRIO | INFO_FL_QUEUE),//請求系統分配緩沖區 IOCTL_INFO_FNC(VIDIOC_QUERYBUF, v4l_querybuf, v4l_print_buffer, INFO_FL_QUEUE | INFO_FL_CLEAR(v4l2_buffer, length)),//查詢所分配的緩沖區 /*太長了,省略后續*/ }
其實,雖然是調用info->u.func(ops, file, fh, arg)函數,但是ops是vfd->ioctl_ops;,以v4l2_ioctls[]數組里的v4l_querycap函數(就是u.func字段)為例,里面也會調用:ops->vidioc_querycap(file, fh, cap);
所以,最終還是會回到uvc_ioctl_ops函數集合里。其實和info->flags 是 INFO_FL_STD的情況沒什么大的差別。
我們看下uvc_ioctl_ops 這個真正的ioctl操作函數集合:
const struct v4l2_ioctl_ops uvc_ioctl_ops = {
.vidioc_querycap = uvc_ioctl_querycap, .vidioc_enum_fmt_vid_cap = uvc_ioctl_enum_fmt_vid_cap,//列舉支持哪種格式 .vidioc_enum_fmt_vid_out = uvc_ioctl_enum_fmt_vid_out, .vidioc_g_fmt_vid_cap = uvc_ioctl_g_fmt_vid_cap,//獲取格式、分辨率 .vidioc_g_fmt_vid_out = uvc_ioctl_g_fmt_vid_out, .vidioc_s_fmt_vid_cap = uvc_ioctl_s_fmt_vid_cap,//先try測試,然后把要設置的格式/分辨率存起來 .vidioc_s_fmt_vid_out = uvc_ioctl_s_fmt_vid_out, .vidioc_try_fmt_vid_cap = uvc_ioctl_try_fmt_vid_cap,//檢測是否支持用戶輸入的格式 .vidioc_try_fmt_vid_out = uvc_ioctl_try_fmt_vid_out, .vidioc_reqbufs = uvc_ioctl_reqbufs,//請求分配緩存,APP將從這些緩存中讀到視頻數據 .vidioc_querybuf = uvc_ioctl_querybuf,//查詢緩存狀態, 比如地址信息(APP可以用mmap進行映射) .vidioc_qbuf = uvc_ioctl_qbuf,//把緩沖區放入隊列,底層的硬件操作函數將會把數據放入這個隊列的緩存 .vidioc_expbuf = uvc_ioctl_expbuf, .vidioc_dqbuf = uvc_ioctl_dqbuf,//APP通過poll/select確定有數據后,把緩存從隊列中取出來 .vidioc_create_bufs = uvc_ioctl_create_bufs, .vidioc_streamon = uvc_ioctl_streamon,//啟動視頻傳輸 .vidioc_streamoff = uvc_ioctl_streamoff, .vidioc_enum_input = uvc_ioctl_enum_input, .vidioc_g_input = uvc_ioctl_g_input, .vidioc_s_input = uvc_ioctl_s_input, /*太長了,后續省略......*/ };
可以看到非常的多,帶_cap的是捕獲設備,帶_out的是輸出設備。
雖然這些ioctl非常多,但是在韋東山第三期視頻里說道,簡化的攝像頭驅動程序至少需要11個ioctl,我能力不大,所以也就按照這個來分析:
.vidioc_querycap = uvc_ioctl_querycap,
.vidioc_enum_fmt_vid_cap = uvc_ioctl_enum_fmt_vid_cap,
.vidioc_g_fmt_vid_cap = uvc_ioctl_g_fmt_vid_cap,
.vidioc_try_fmt_vid_cap = uvc_ioctl_try_fmt_vid_cap,
.vidioc_s_fmt_vid_cap = uvc_ioctl_s_fmt_vid_cap,
.vidioc_reqbufs = uvc_ioctl_reqbufs,
.vidioc_querybuf = uvc_ioctl_querybuf,
.vidioc_qbuf = uvc_ioctl_qbuf,
.vidioc_dqbuf = uvc_ioctl_dqbuf,
.vidioc_streamon = uvc_ioctl_streamon,
.vidioc_streamoff = uvc_ioctl_streamoff,
我們先看第【1】個ioctl:.vidioc_querycap = uvc_ioctl_querycap函數:
static int uvc_ioctl_querycap(struct file *file, void *fh, struct v4l2_capability *cap) { struct video_device *vdev = video_devdata(file); struct uvc_fh *handle = file->private_data; struct uvc_video_chain *chain = handle->chain; struct uvc_streaming *stream = handle->stream; strlcpy(cap->driver, "uvcvideo", sizeof(cap->driver)); strlcpy(cap->card, vdev->name, sizeof(cap->card)); usb_make_path(stream->dev->udev, cap->bus_info, sizeof(cap->bus_info)); cap->capabilities = V4L2_CAP_DEVICE_CAPS | V4L2_CAP_STREAMING | chain->caps;//獲取這是一個什么設備: if (stream->type == V4L2_BUF_TYPE_VIDEO_CAPTURE)//如果是視頻捕獲設備 cap->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING; else cap->device_caps = V4L2_CAP_VIDEO_OUTPUT | V4L2_CAP_STREAMING; return 0; }
很簡單的函數,.vidioc_querycap 里的回調函數主要就是說明這個是什么設備而已,輸入設備?輸出設備?
接下來看第【2】個ioctl:.vidioc_enum_fmt_vid_cap = uvc_ioctl_enum_fmt_vid_cap函數:
uvc_ioctl_enum_fmt_vid_cap函數里又會調用uvc_ioctl_enum_fmt函數:
static int uvc_ioctl_enum_fmt(struct uvc_streaming *stream,struct v4l2_fmtdesc *fmt) { /*省略部分內容*/ struct uvc_format *format; format = &stream->format[fmt->index];//從uvc_fmts找出支持的格式 strlcpy(fmt->description, format->name, sizeof(fmt->description));//存放到description fmt->description[sizeof(fmt->description) - 1] = 0; fmt->pixelformat = format->fcc; }
.vidioc_enum_fmt_vid_cap 里的回調函數主要就是列舉出支持的格式,把他放到fmt->description描述符中,然后返回給用戶空間傳進來的fmt。
繼續看第【3】個ioctl:.vidioc_g_fmt_vid_cap = uvc_ioctl_g_fmt_vid_cap函數:
uvc_ioctl_g_fmt_vid_cap函數里又會調用uvc_v4l2_get_format函數:
static int uvc_v4l2_get_format(struct uvc_streaming *stream,struct v4l2_format *fmt)
{
/*部分內容省略*/ format = stream->cur_format;//從stream獲取當前格式 frame = stream->cur_frame;//從stream獲取當前分辨率 fmt->fmt.pix.pixelformat = format->fcc; fmt->fmt.pix.width = frame->wWidth; fmt->fmt.pix.height = frame->wHeight; fmt->fmt.pix.field = V4L2_FIELD_NONE; fmt->fmt.pix.bytesperline = uvc_v4l2_get_bytesperline(format, frame); fmt->fmt.pix.sizeimage = stream->ctrl.dwMaxVideoFrameSize; fmt->fmt.pix.colorspace = format->colorspace; fmt->fmt.pix.priv = 0; }
.vidioc_g_fmt_vid_cap里的回調函數主要是獲取格式、分辨率,把他存放在fmt->fmt.pix里。
第【4】個ioctl:.vidioc_try_fmt_vid_cap = uvc_ioctl_try_fmt_vid_cap函數:
uvc_ioctl_try_fmt_vid_cap函數里面又調用uvc_v4l2_try_format函數:
static int uvc_v4l2_try_format(struct uvc_streaming *stream, struct v4l2_format *fmt, struct uvc_streaming_control *probe, struct uvc_format **uvc_format, struct uvc_frame **uvc_frame) { /*部分內容省略*/ for (i = 0; i < stream->nformats; ++i) {//在format查找支持的格式是否有請求的格式 format = &stream->format[i]; if (format->fcc == fmt->fmt.pix.pixelformat) break; } if (i == stream->nformats) {//如果沒有 format = stream->def_format;//使用默認的格式 fmt->fmt.pix.pixelformat = format->fcc; } rw = fmt->fmt.pix.width; rh = fmt->fmt.pix.height; maxd = (unsigned int)-1; for (i = 0; i < format->nframes; ++i) {//查找最接近的圖像分辨率 __u16 w = format->frame[i].wWidth; __u16 h = format->frame[i].wHeight; d = min(w, rw) * min(h, rh); d = w*h + rw*rh - 2*d; if (d < maxd) { maxd = d; frame = &format->frame[i]; } if (maxd == 0) break; } interval = frame->dwDefaultFrameInterval;//每一幀的間隙 probe->bmHint = 1; /* dwFrameInterval */ probe->bFormatIndex = format->index; probe->bFrameIndex = frame->bFrameIndex; probe->dwFrameInterval = uvc_try_frame_interval(frame, interval); ret = uvc_probe_video(stream, probe);//里面調用uvc_set_video_ctrl fmt->fmt.pix.width = frame->wWidth;//設置具體的參數 fmt->fmt.pix.height = frame->wHeight;//分辨率 fmt->fmt.pix.field = V4L2_FIELD_NONE; fmt->fmt.pix.bytesperline = uvc_v4l2_get_bytesperline(format, frame); fmt->fmt.pix.sizeimage = probe->dwMaxVideoFrameSize;//一楨大小 fmt->fmt.pix.colorspace = format->colorspace; fmt->fmt.pix.priv = 0; }
我們看下.vidioc_try_fmt_vid_cap的回調函數做了什么:
1.檢測硬件是否支持請求的格式,否則使用默認格式
2.然后在format->frame[i]查找最近的分辨率來使用
3.設置每一幀的時間間隙,也就是一秒多少幀
4.填充probe,然后在uvc_probe_video里設置video控制
5.最后把嘗試好的數據填充到fmt->fmt.pix
所以.vidioc_try_fmt_vid_cap的回調函數主要是起一個測試作用。
第【5】個ioctl:.vidioc_s_fmt_vid_cap = uvc_ioctl_s_fmt_vid_cap函數:
uvc_ioctl_s_fmt_vid_cap函數里調用uvc_v4l2_set_format函數:
static int uvc_v4l2_set_format(struct uvc_streaming *stream, struct v4l2_format *fmt) { /*部分內容省略*/ ret = uvc_v4l2_try_format(stream, fmt, &probe, &format, &frame);//測試好參數 /*把參數保存在stream,供uvc_v4l2_get_format函數調用*/ stream->ctrl = probe; stream->cur_format = format;//把格式foramt保存起來 stream->cur_frame = frame;//把分辨率frame保存起來 }
其實我感覺這個函數都沒啥啥,大部分都在uvc_v4l2_try_format函數里做了,最后就是把參數存起來到stream里而已。
第【6】個ioctl:.vidioc_reqbufs = uvc_ioctl_reqbufs函數:
調用關系:
uvc_ioctl_reqbufs
uvc_request_buffers
vb2_reqbufs
vb2_core_reqbufs
int vb2_core_reqbufs(struct vb2_queue *q, enum vb2_memory memory,unsigned int *count) { /*部分內容省略*/ __vb2_queue_cancel(q);//清理任何緩沖的准備或排隊隊列狀態 memset(q->alloc_devs, 0, sizeof(q->alloc_devs));//初始化清零 q->memory = memory;//指向內存 ret = call_qop(q, queue_setup, q, &num_buffers, &num_planes, plane_sizes, q->alloc_devs);//查詢需要多少緩沖區buffers,設置緩存區隊列 allocated_buffers = __vb2_queue_alloc(q, memory, num_buffers, num_planes, plane_sizes);//申請緩存區 }
其中call_qop(q, queue_setup, q, &num_buffers, &num_planes,plane_sizes, q->alloc_devs)里面其實就是調用:(q)->ops->queue_setup(q, &num_buffers, &num_planes,plane_sizes, q->alloc_devs)
那這個(q)->ops又是在哪設置呢?
其實就是我們上一章里的uvc_queue_init函數里:
queue->queue.ops = &uvc_queue_qops;
static struct vb2_ops uvc_queue_qops = {
.queue_setup = uvc_queue_setup, .buf_prepare = uvc_buffer_prepare, .buf_queue = uvc_buffer_queue, .buf_finish = uvc_buffer_finish, .wait_prepare = vb2_ops_wait_prepare, .wait_finish = vb2_ops_wait_finish, .start_streaming = uvc_start_streaming, .stop_streaming = uvc_stop_streaming, };
這個結構體很重要,之后我們還會調用到。
知道需要多少緩存之后,就會調用__vb2_queue_alloc函數申請空間:
static int __vb2_queue_alloc(struct vb2_queue *q, enum vb2_memory memory, unsigned int num_buffers, unsigned int num_planes, const unsigned plane_sizes[VB2_MAX_PLANES]) { /*部分內容省略*/ unsigned int buffer, plane; struct vb2_buffer *vb; for (buffer = 0; buffer < num_buffers; ++buffer) { vb = kzalloc(q->buf_struct_size, GFP_KERNEL);//分配空間 vb->state = VB2_BUF_STATE_DEQUEUED;//設置狀態 vb->vb2_queue = q; vb->num_planes = num_planes; vb->index = q->num_buffers + buffer; vb->type = q->type;//設置類型,比如捕獲之類的 vb->memory = memory;//設置內存 for (plane = 0; plane < num_planes; ++plane) { vb->planes[plane].length = plane_sizes[plane]; vb->planes[plane].min_length = plane_sizes[plane]; } q->bufs[vb->index] = vb;//申請的空間裝入bufs if (memory == VB2_MEMORY_MMAP) {//如果使用的是VB2_MEMORY_MMAP類型 ret = __vb2_buf_mem_alloc(vb);//mmap出來 if (ret) { dprintk(1, "failed allocating memory for " "buffer %d\n", buffer); q->bufs[vb->index] = NULL; kfree(vb); break; } __setup_offsets(vb);//設置偏移量 ret = call_vb_qop(vb, buf_init, vb);//buf初始化 } } }
這里面就是申請num_buffers個緩存了,設置好然后放到q->bufs[]里,再mmap到用戶空間。
為什么mmap呢?
read和write,是基本幀IO訪問方式,每一幀都要通過IO操作,通過read讀取每一幀數據,數據需要在內核和用戶之間拷貝,這種方式訪問速度可能會非常慢;
但是通過mmap在內核空間開辟緩沖區,這些緩沖區可以是大而連續DMA緩沖區、通過vmalloc()創建的虛擬緩沖區,或者直接在設備的IO內存中開辟的緩沖區(如果硬件支持);是流IO訪問方式,不需要內存拷貝,訪問速度比較快。
設置好偏移值就對buf進行初始化了。
所以.vidioc_reqbufs的回調函數主要是請求分配緩存。
第【7】個.ioctl:.vidioc_querybuf = uvc_ioctl_querybuf函數:
調用關系:
uvc_query_buffer vb2_querybuf vb2_core_querybuf call_void_bufop(q, fill_user_buffer, q->bufs[index], pb);
- 1
- 2
- 3
- 4
可以看出,這里根據傳進來的參數,查找到使用到的緩存,會調用fill_user_buffer返回bufs[]給用戶空間。
所以.vidioc_querybuf 的回調函數主要是查詢緩存狀態,把他返回給用戶空間。
第【8】個.vidioc_qbuf = uvc_ioctl_qbuf函數:
調用關系為:
uvc_ioctl_qbuf
uvc_queue_buffer
vb2_qbuf
vb2_core_qbuf
- 1
- 2
- 3
- 4
int vb2_core_qbuf(struct vb2_queue *q, unsigned int index, void *pb) { /*部分內容省略*/ struct vb2_buffer *vb; vb = q->bufs[index]; list_add_tail(&vb->queued_entry, &q->queued_list);//添加到queued_list鏈表 q->queued_count++;//隊列數目加一 q->waiting_for_buffers = false; vb->state = VB2_BUF_STATE_QUEUED;//標記已入隊列 if (q->start_streaming_called)//如果調用過vb2_start_streaming __enqueue_in_driver(vb);//將vb->planes[plane].mem_priv(即是我們申請的mmap)調入我們寫的驅動中 if (pb) call_void_bufop(q, fill_user_buffer, vb, pb);//填充buffers到用戶空間 if (q->streaming && !q->start_streaming_called && q->queued_count >= q->min_buffers_needed) { ret = vb2_start_streaming(q); } }
這里面,將對應的vb2_buffer 添加到 q->queued_list 鏈表中,並改變state狀態。
然后調用__enqueue_in_driver把緩沖區放入隊列。
然后就是ret = vb2_start_streaming(q)函數,這個函數在uvc_ioctl_streamon函數里也會調用有,所以放在后面寫吧,這里雖然也會調用,但是是有條件的:
只有start_streaming沒被調用且到達最小需要的buffers數目,才嘗試啟動
第【9】個.vidioc_dqbuf = uvc_ioctl_dqbuf,函數:
調用關系為:
uvc_ioctl_dqbuf
uvc_dequeue_buffer
vb2_dqbuf
vb2_core_dqbuf
int vb2_core_dqbuf(struct vb2_queue *q, unsigned int *pindex, void *pb, bool nonblocking) { /*部分內容省略*/ struct vb2_buffer *vb = NULL; ret = __vb2_get_done_vb(q, &vb, pb, nonblocking); call_void_vb_qop(vb, buf_finish, vb); call_void_bufop(q, fill_user_buffer, vb, pb); list_del(&vb->queued_entry); q->queued_count--; }
這里面,就是在__vb2_get_done_vb函數里,將q->done_list 中的vb2_buffer中提出來,然后 將vb2_buffer中的v4l2_buffer信息返回,並將其從q->done_list 中刪除。
然后就調用(q)->vb2_queue->ops->buf_finish代表buf操作完成。
接着調用fill_user_buffer函數把把數據返回給用戶空間,最后list_del(&vb->queued_entry)把他移除掉。
第【10】個.vidioc_streamon = uvc_ioctl_streamon,,函數:
調用關系為:
uvc_ioctl_streamon
uvc_queue_streamon
vb2_streamon
vb2_core_streamon
vb2_start_streaming
static int vb2_start_streaming(struct vb2_queue *q) { /*部分內容省略*/ struct vb2_buffer *vb; q->start_streaming_called = 1;//標志以使用streaming ret = call_qop(q, start_streaming, q, atomic_read(&q->owned_by_drv_count));//調用q->ops->start_streaming q->start_streaming_called = 0; for (i = 0; i < q->num_buffers; ++i) { vb = q->bufs[i]; if (vb->state == VB2_BUF_STATE_ACTIVE) vb2_buffer_done(vb, VB2_BUF_STATE_QUEUED);//數據完成后 } }
函數里面q->ops->start_streaming,也就是queue->queue.ops = &uvc_queue_qops里的函數:uvc_start_streaming
但是uvc_start_streaming函數里又會調用uvc_video_enable(stream, 1);
uvc_video_enable函數里面就有這兩句:
ret = uvc_commit_video(stream, &stream->ctrl);//uvc提交視頻參數 ret = uvc_init_video(stream, GFP_KERNEL);//uvc初始化視頻,同時調用uvc_init_video_isoc分配urb
這樣就成功的啟動視頻傳輸了。
然后數據完成后就是調用vb2_buffer_done函數:函數里面就是:
list_add_tail(&vb->done_entry, &q->done_list);//將數據放入q->done_list中 vb->state = state; wake_up(&q->done_wq);//喚醒poll休眠的進程
數據完成就要把buffers放到done_list這個完成的鏈表中,然后改變狀態,最后就喚醒poll函數里的休眠進程。
第【11】個.vidioc_streamoff = uvc_ioctl_streamoff,,函數:
其實就和uvc_ioctl_streamoff是相反的,uvc_ioctl_streamoff是調用uvc_video_enable(stream, 1);
uvc_ioctl_streamoff就是調用uvc_video_enable(stream, 0)了,進行關閉視頻傳輸。
好了,十一個ioctl就馬馬虎虎的講完了。還有一些其他的ioctl,比如設置亮度之類的,就自己去看了,是在太多了。
其實一路寫來,我對這個buffers這塊輸出還是很模糊的,不知道怎么寫,,,,,,,,,還在研究中。不過大致的流程還是了解了一點。
最后,附上一位大佬的blog:http://www.cnblogs.com/surpassal/archive/2012/12/22/zed_webcam_lab2.html