前言
__V412術語的意思是:video for linux version2,是Linux核心中關于視訊裝置的核心驅動架構,為上層的通路底層的視訊裝置提供了統一的接口.凡是核心中的子系統都有抽象底層硬體的差異,為上層提供統一的接口和提取出公共代碼避免代碼備援等好處.,我們的攝像頭驅動程式是屬于字元裝置驅動程式,對于複雜的字元裝置驅動程式它引入了分層的概念,在我們之前寫過的LCD驅動文章就是利用這種分層的概念,如下圖所示,上一層的fbmem.c構造了一個fops結構體,裡面有打開,讀寫函數,注冊該結構體,這一層核心檔案已經幫我們做好了,我們需要做的是硬體相關的一層,配置設定設定一個fb_info結構體然後注冊告訴fbmem.c,當我們的app調用open,read,write函數時就會根據注冊的fb_info結構體中的屬性來操作硬體了,可以看出這樣寫的好處就是我們隻需要專注于我們硬體層相關的東西即可,而且使得app使用統一的open,read,write函數調用,簡單來說就是使得app層可以通過統一的接口就能通路各種裝置
v4l2驅動架構如圖所示,圖中晶片子產品對應Soc的各個子子產品,video_device結構體主要用來控制Soc的video子產品,v4l2_device會包含多個v4l2_subdev,每個v4l2_subdev 用來控制各自的子子產品,某些驅動不需要v4l2_subdev,依靠video子產品就能實作功能
V4L2支援三類裝置:視訊輸入輸出裝置,VBI裝置和radio裝置(其實還支援更多類型的裝置,暫不讨論),分别會在/dev目錄下産生videoX,radioX和vbiX裝置節點,我們常見的視訊輸入裝置主要是攝像頭,下圖是V4L2在linux系統中的最簡要的結構圖
如上圖所示,V4L2系統主要由4部分組成
字元裝置驅動程式核心:V4L2本身就是一個字元裝置,具有字元裝置所有的特性,暴露接口給使用者空間
V4L2驅動核心:主要是建構一個核心中标準視訊裝置驅動的架構,為視訊操作提供統一的接口函數,是底層向上層注冊的核心
平台V4L2裝置驅動:在V4L2架構下,根據平台自身的特性實作與平台相關的V4L2驅動部分,包括注冊video_device和v4l2_dev
具體的sensor驅動:主要上電,提供工作時鐘,視訊圖像裁剪,流IO開啟等,實作各種裝置控制方法供上層調用并注v4l2_subdev.
從上圖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(攝像頭裝置)包括調節飽和度,對比度和白平衡等。
虛拟視訊驅動vivi.c分析:
vivi_init
vivi_create_instance
v4l2_device_register // 不是主要, 隻是用于初始化一些東西,比如自旋鎖、引用計數
video_device_alloc
//設定
1.vfd:
.fops = &vivi_fops,
.ioctl_ops = &vivi_ioctl_ops,
.release = video_device_release,
2.
vfd->v4l2_dev = &dev->v4l2_dev;//輔助作用,用于提供自旋鎖,引用計數
3. 設定"ctrl屬性"(用于APP的ioctl):
v4l2_ctrl_handler_init(hdl, 11);//初始化一個v4l2_ctrl_handler
dev->volume = v4l2_ctrl_new_std(hdl, &vivi_ctrl_ops,
V4L2_CID_AUDIO_VOLUME, 0, 255, 1, 200);//設定音量
dev->brightness = v4l2_ctrl_new_std(hdl, &vivi_ctrl_ops,
V4L2_CID_BRIGHTNESS, 0, 255, 1, 127);//設定亮度
dev->contrast = v4l2_ctrl_new_std(hdl, &vivi_ctrl_ops,
V4L2_CID_CONTRAST, 0, 255, 1, 16);
video_register_device(video_device, type:VFL_TYPE_GRABBER, nr)
/*
根據第二個參數type确定裝置名稱,也就是我們在/dev/下生成的video啊之類的,得到根據偏移值得到次裝置号,把vdev存進去以
該次裝置号為下标的video_device數組
注冊該vdev結構體,最終調用__video_register_device函數,該函數在v412_dev.c裡,也就是向上層注冊(v412_device.c),在
該函數中調用cdev_alloc()配置設定一個字元裝置到vdev->cdev中,這裡就設定了vdev->cdev->ops = &v4l2_fops,裡面就是
真正的使用者空間操作集合,最後調用cdev_init注冊該結構體,當app層調用read,write時就是調用v4l2_fops中的操作函數進
而轉向底層,調用底層裝置注冊的fops中的操作函數,比如上面的uvc_fop函數
這個實際的__video_register_device注冊函數是在drivers\media\video\v4l2-dev.c也就是它為下層提供注冊接口的
*/
__video_register_device//根據傳進來的type選擇不同的次裝置号
vdev->cdev = cdev_alloc();
vdev->cdev->ops = &v4l2_fops;
cdev_add
分析vivi.c的open,read,write,ioctl過程
當應用層open()/read()/write()操作/dev/videox時,先找到v4l2_fops,然後調用v4l2_open/v4l2_read/v4l2_write(drivers/media/v4l2-core/v4l2-dev.c),再通過video_devdata根據次裝置号從數組中得到video_device,再找到vivid_fops裡對應的操作函數,ioctl的前面流程類似,後面通過video_usercopy()擷取傳入的ioctl類型,找到video_device中對應ioctl_ops,調用不同的ioctl。
ioctl_ops函數分析
攝像頭驅動有衆多的ioctl,這些icctl實作對裝置的控制,比如在vivi.c中提供的ioctl函數如下所示
static const struct v4l2_ioctl_ops vivi_ioctl_ops = {
/*表示它是一個攝像頭裝置,必需的*/
.vidioc_querycap = vidioc_querycap,
/*攝像頭資料格式的操作,用于列舉,獲得,測試,設定攝像頭所提供的資料的格式,必需的*/
.vidioc_enum_fmt_vid_cap = vidioc_enum_fmt_vid_cap, //列舉格式,從設定好的format數組中得到格式
.vidioc_g_fmt_vid_cap = vidioc_g_fmt_vid_cap, //擷取格式
.vidioc_try_fmt_vid_cap = vidioc_try_fmt_vid_cap, //測試格式,與規定的資料格式進行比較
.vidioc_s_fmt_vid_cap = vidioc_s_fmt_vid_cap, //設定格式,該函數裡會先調用idioc_try_fmt_vid_cap該格式是否符合規格
/*緩沖區操作:申請/查詢/放入隊列/取出隊列,必需的*/
.vidioc_reqbufs = vidioc_reqbufs, //申請
.vidioc_querybuf = vidioc_querybuf, //查詢
.vidioc_qbuf = vidioc_qbuf, //放入
.vidioc_dqbuf = vidioc_dqbuf, //取出
.vidioc_streamon = vidioc_streamon, //啟動
.vidioc_streamoff = vidioc_streamoff, //停止
/* 用于制式操作,不是必需的 */
.vidioc_s_std = vidioc_s_std, //設定制式
/*輸入源操作,在xawtv的界面裡就是video source,不是必需的*/
.vidioc_enum_input = vidioc_enum_input, //枚舉輸入源
.vidioc_g_input = vidioc_g_input, //擷取輸入源
.vidioc_s_input = vidioc_s_input, //設定輸入源
/* 調試操作 */
.vidioc_log_status = v4l2_ctrl_log_status, //輸出裝置狀态到核心日志
.vidioc_subscribe_event = v4l2_ctrl_subscribe_event,//訂閱V4L2事件
.vidioc_unsubscribe_event = v4l2_event_unsubscribe,//取消訂閱V4L2事件
};
1.vidioc_querycap函數
static int vidioc_querycap(struct file *file, void *priv,
struct v4l2_capability *cap)
{
struct vivi_dev *dev = video_drvdata(file);
strcpy(cap->driver, "vivi");
strcpy(cap->card, "vivi");
strlcpy(cap->bus_info, dev->v4l2_dev.name, sizeof(cap->bus_info));
cap->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING |
V4L2_CAP_READWRITE;//V4L2_CAP_VIDEO_CAPTUR表示這是一個攝像頭裝置,V4L2_CAP_STREAMING表明這是一個流式攝像頭,V4L2_CAP_READWRITE表明應用程式可以通過read,write函數來獲得資料
cap->capabilities = cap->device_caps | V4L2_CAP_DEVICE_CAPS;
return 0;
}
2.vidioc_enum_fmt_vid_cap函數
static int vidioc_enum_fmt_vid_cap(struct file *file, void *priv,
struct v4l2_fmtdesc *f)
{
struct vivi_fmt *fmt;
if (f->index >= ARRAY_SIZE(formats))
return -EINVAL;
fmt = &formats[f->index];//從formats數組中找出支援的格式
strlcpy(f->description, fmt->name, sizeof(f->description));
f->pixelformat = fmt->fourcc;
return 0;
}
該函數主要是找到支援的格式的數組項賦給fmt,然後将fmt數組成員的id成員賦給f->pixelformat,傳回給使用者空間
…後面的函數就不一一介紹了,有興趣的可以自己看看源碼
攝像頭資料擷取過程分析
1.請求配置設定緩沖區
ioctl(4, VIDIOC_REQBUFS // 請求系統配置設定緩沖區
videobuf_reqbufs(隊列, v4l2_requestbuffers) // 隊列在open函數用videobuf_queue_vmalloc_init初始化
// 注意:這個IOCTL隻是配置設定緩沖區的頭部資訊,真正的緩存還沒有配置設定呢
2. 查詢映射緩沖區:
ioctl(4, VIDIOC_QUERYBUF // 查詢所配置設定的緩沖區
videobuf_querybuf // 獲得緩沖區的資料格式、大小、每一行長度、高度
使用者空間執行mmap
mmap(參數裡有"大小") // 在這裡才配置設定緩存
v4l2_mmap
vivi_mmap
videobuf_mmap_mapper
videobuf-vmalloc.c裡的__videobuf_mmap_mapper
mem->vmalloc = vmalloc_user(pages); // 在這裡才給緩沖區配置設定空間
3. 把緩沖區放入隊列
ioctl(4, VIDIOC_QBUF // 把緩沖區放入隊列
videobuf_qbuf
q->ops->buf_prepare(q, buf, field); // 調用驅動程式提供的函數做些預處理
list_add_tail(&buf->stream, &q->stream); // 把緩沖區放入隊列的尾部
q->ops->buf_queue(q, buf); // 調用驅動程式提供的"入隊列函數"
4. 啟動攝像頭
ioctl(4, VIDIOC_STREAMON
videobuf_streamon
q->streaming = 1;
5. 用select查詢是否有資料
// 驅動程式裡必定有: 産生資料、喚醒程序
v4l2_poll
vdev->fops->poll
vivi_poll
videobuf_poll_stream
// 從隊列的頭部獲得緩沖區
buf = list_entry(q->stream.next, struct videobuf_buffer, stream);
// 如果沒有資料則休眠
poll_wait(file, &buf->done, wait);
誰來産生資料、誰來喚醒它?
核心線程vivi_thread每30MS執行一次,它調用
vivi_thread_tick
vivi_fillbuff(fh, buf); // 構造資料
wake_up(&buf->vb.done); // 喚醒程序
6. 有資料後從隊列裡取出緩沖區
// 有那麼多緩沖區,APP如何知道哪一個緩沖區有資料?調用VIDIOC_DQBUF
ioctl(4, VIDIOC_DQBUF
vidioc_dqbuf
// 在隊列裡獲得有資料的緩沖區
retval = stream_next_buffer(q, &buf, nonblocking);
// 把它從隊列中删掉
list_del(&buf->stream);
// 把這個緩沖區的狀态傳回給APP
videobuf_status(q, b, buf, q->type);
7. 應用程式根據VIDIOC_DQBUF所得到緩沖區狀态,知道是哪一個緩沖區有資料
就去讀對應的位址(該位址來自前面的mmap)