V4L2視頻輸入框架概述

原文連接node

本文開啓 linux 內核 V4L2 框架部分的學習之旅,本文僅先對 V4L2 的框架作一個綜述性的歸納介紹,而後接下來的文章中會對 V4L2 框架的各個子模塊進行一個全面的介紹,包括每一部分的實現原理,如何使用,用在什麼地方等等。預計接下來的文章大概有5篇(不帶本篇)。坑已經挖好了,開始吧。

<!-- more -->linux

導讀:V4L2 是專門爲 linux 設備設計的一套視頻框架,其主體框架在 linux 內核,能夠理解爲是整個 linux 系統上面的視頻源捕獲驅動框架。其普遍應用在嵌入式設備以及移動端、我的電腦設備上面,市面上的編碼產品類如:SDV、手機、IPC、行車記錄儀都會用到這個框架來進行視頻採集,固然,有比較厲害的廠家直接就使用本身實現的一套視頻採集框架,這種屬因而廠家中戰鬥機了。下文主要參考linux-4.4內核文檔對V4L2框架進行一次全局的介紹。編程

V4L2框架簡介

幾乎全部的設備都有多個 IC 模塊,它們多是實體的(例如 USB 攝像頭裏麪包含 ISP、sensor 等)、也多是抽象的(如 USB 設備裏面的抽象拓撲結構),它們在 /dev 目錄下面生成了多個設備節點,而且這些 IC 模塊還建立了一些非 v4l2 設備:DVB、ALSA、FB、I2C 和輸入設備。正是因爲硬件的複雜性,v4l2 的驅動也變得很是複雜。數組

特別是 v4l2 驅動要支持 IC 模塊來進行音/視頻的混合/編解碼操做,這就更加使得 v4l2 驅動變得異常複雜。一般狀況下,有些IC模塊經過一個或者多個 I2C 總線鏈接到主橋驅動上面,同時其它的總線仍然可用,這些 IC 就稱爲 ‘sub-devices’,好比攝像頭設備裏面的 sensor 傳感器就是使用 I2C 來進行命令溝通,同時使用 MIPI 或者 LVDS 等接口進行圖像數據傳輸。安全

在很長一段時間內,該框架(指老舊的 V4L2 框架)僅限於經過 video_device 結構體建立 v4l 設備節點和 video_buf 來處理視頻數據。這意味着全部的驅動都必須對設備實例進行設置並將其映射到子設備上。有些時候這些操做步驟十分複雜,很難正確完成,而且有些驅動程序歷來沒有正確的按照這些操做步驟編寫。因爲缺乏一個框架,有不少通用代碼就沒有辦法被重構,從而致使這部分代碼被重複編寫,效率比較低下。網絡

所以,本框架抽象構建了全部驅動都須要的代碼並封裝爲一個個的模塊,簡化了設備驅動通用代碼的重構。v4l2-pci-skeleton.c 是個很是好的參考例程,它是一個PCI採集卡的驅動框架。該例程演示瞭如何使用 v4l2 驅動框架,而且該例程能夠做爲一個 PCI 視頻採集卡的驅動模板使用。在最開始的時候也能夠參照這個代碼編寫方式進行聯繫,固然最適合的代碼仍是 <font color='red'>drivers/media/video/omap3isp</font> 文件夾裏面的代碼,這個代碼基本上能夠做爲一個完整的輸入設備實例代碼(由於它包含了 ISP、CSI、video 等設備,而且有着一個完整的數據流 pipeline,幾乎用到了 V4L2 框架的方方面面,參考價值極大)來進行參考編寫本身的設備驅動代碼。app

V4L2框架藍圖

藍圖解構

這是一張很是大的圖,可是我只選取了其中的一個,這張圖對 V4L2 裏面的子模塊進行簡化(簡化到只有子模塊的名字,沒有內部實現的介紹),大圖以下:
V4L2 設備拓撲
<center>V4L2 設備拓撲</center>框架

這張圖怎麼看呢?它有如下幾個關鍵因素:異步

  • v4l2_device:這個是整個輸入設備的總結構體,能夠認爲它是整個 V4L2 框架的入口,充當驅動的管理者以及入口監護人。由該結構體引伸出來 v4l2_subdev。用於視頻輸入設備總體的管理,有多少輸入設備就有多少個v4l2_device抽象(好比一個USB攝像頭總體就能夠看做是一個 V4L2 device)。再往下分是輸入子設備,對應的是例如 ISP、CSI、MIPI 等設備,它們是從屬於一個 V4L2 device 之下的。
  • media_device:用於運行時數據流的管理,嵌入在 V4L2 device 內部,運行時的意思就是:一個 V4L2 device 下屬可能有很是多同類型的子設備(兩個或者多個 sensor、ISP 等),那麼在設備運行的時候我怎麼知道個人數據流須要用到哪個類型的哪個子設備呢。這個時候就輪到 media_device 出手了,它爲這一坨的子設備創建一條虛擬的連線,創建起來一個運行時的 pipeline(管道),而且能夠在運行時動態改變、管理接入的設備。
  • v4l2_ctrl_handler:控制模塊,提供子設備(主要是 video 和 ISP 設備)在用戶空間的特效操做接口,好比你想改變下輸出圖像的亮度、對比度、飽和度等等,均可以經過這個來完成。
  • vb2_queue:提供內核與用戶空間的 buffer 流轉接口,輸入設備產生了一坨圖像數據,在內核裏面應該放在哪裏呢?能放幾個呢?是整段連續的仍是仍是分段連續的又或者是物理不連續的?用戶怎麼去取用呢?都是它在管理。

層級解構

  1. 能夠看到圖中的入口 custom_v4l2_dev,它是由用戶定義的一個結構體,重要的不是它怎麼定義的,重要的是它裏面有一個 v4l2_device 結構體,上文說到,這個結構體總覽全局,指揮若定,至關於中央管理處的位置,那麼中央決定了,它就是整個輸入設備總體的抽象(好比整個 USB 攝像頭輸入設備,好比整個 IPC 攝像頭輸入設備)。它還有一個 media_device 結構體,上文也說道,,它是管數據流線路的,屬於搞結構路線規劃管理的。
  2. 日後 v4l2_device 裏面有一個鏈表,它維護了一個巨大的子設備鏈,全部的子設備都經過內核的雙向循環鏈表結構以 v4l2_device 爲中心牢牢團結在一塊兒。另外 media_device 在往裏面去就是一個個的 media_entity(如今不須要了解它的具體含義,只須要知道它就是相似電路板上面的元器件同樣的抽象體),media_entity 之間創建了本身的小圈子,在它們這個小圈子裏面數據流按照必定的順序暢通無阻,恣意遨遊。
  3. 到結尾處,抽象出來了 /dev/videoX 設備節點,這個就是外交部的角色,它負責提供了一個內核與用戶空間的交流樞紐。須要注意的是,該設備節點的本質仍是一個字符設備,其內部的一套操做與字符設備是同樣的,只不過是進行了一層封裝而已。
  4. 到此爲止,一個 V4L2 大概的四層結構就抽象出來了,以下圖所示:

V4L2 層次結構
<center>V4L2 層次結構</center>async

驅動結構體

全部的 V4L2 驅動都有如下結構體類型:

  • 每一個設備都有一個設備實例結構體(上面的 custom_v4l2_dev),裏面包含了設備的狀態;
  • 一種初始化以及控制子設備(v4l2_subdev)的方法;
  • 建立v4l2設備節點而且對設備節點的特定數據(media_device)保持跟蹤;
  • 含有文件句柄的文件句柄結構體(v4l2_fh 文件句柄與句柄結構體一一對應);
  • 視頻數據處理(vb2_queue);

結構體實例

  • 框架結構體(media_device

與驅動結構體很是相似,參考上面的解釋,這裏再也不贅述。v4l2 框架也能夠整合到 media framework 裏面。若是驅動程序設置了 v4l2_devicemdev 成員,那麼子設備與 video 節點都會被自動看成 media framework 裏的 entitiy 抽象。

  • v4l2_device 結構體

每個設備實例都被抽象爲一個 v4l2_device 結構體。一些簡單的設備能夠僅分配一個 v4l2_device 結構體便可,可是
大多數狀況下須要將該結構體嵌入到一個更大的結構體(custom_v4l2_dev)裏面。必須用 v4l2_device_register(struct device *dev, struct v4l2_device *v4l2_dev); 來註冊設備實例。該函數會初始化傳入的 v4l2_device 結構體,若是 dev->driver_data 成員爲空的話,該函數就會設置其指向傳入的 v4l2_dev 參數。

  • 集成 media framework

若是驅動想要集成 media framework 的話,就須要人爲地設置 dev->driver_data 指向驅動適配的結構體(該結構體由
驅動自定義- custom_v4l2_dev,裏面嵌入 v4l2_device 結構體)。在註冊 v4l2_device 以前就須要調用 dev_set_drvdata 來完成設置。而且必須設置 v4l2_decicemdev 成員指向註冊的 media_device 結構體實例。

  • 設備節點的命名

若是 v4l2_devicename 成員爲空的話,就按照 dev 成員的名稱來命名,若是 dev 成員也爲空的話,就必須在註冊 v4l2_device 以前設置它的 name 成員。可使用 v4l2_device_set_name 函數來設置 name 成員,該函數會基於驅動名以及驅動實例的索引號來生成 name 成員的名稱,相似於 ivtv0、ivtv1 等等,若是驅動名的最後一個字母是整數的話,生成的名稱就相似於cx18-0、cx18-1等等,該函數的返回值是驅動實例的索引號。

  • 回調函數與設備卸載

還能夠提供一個 notify() 回調函數給 v4l2_device 接收來自子設備的事件通知。固然,是否須要設置該回調函數取決於子設備是否有向主設備發送通知事件的需求。v4l2_device 的卸載需調用到 v4l2_device_unregister 函數。在該函數被調用以後,若是 dev->driver_data 指向 v4l2_device 的話,該指針將會被設置爲NULL。該函數會將全部的子設備所有卸載掉。若是設備是熱拔插屬性的話,當 disconnect 發生的時候,父設備就會失效,同時 v4l2_device 指向父設備的指針也必須被清除,能夠調用 v4l2_device_disconnect 函數來清除指針,該函數並不卸載子設備,子設備的卸載仍是須要調用到 v4l2_device_unregister 來完成。若是不是熱拔插設備的話,就沒必要關注這些。

驅動設備使用

有些時候須要對驅動的全部設備進行迭代,這種狀況一般發生在多個設備驅動使用同一個硬件設備的狀況下,好比 ivtvfb 驅動就是個 framebuffer 驅動,它用到了 ivtv 這個硬件設備。可使用如下方法來迭代全部的已註冊設備:

static int callback(struct device *dev, void *p)
{
    struct v4l2_device *v4l2_dev = dev_get_drvdata(dev);

    /* test if this device was inited */
    if (v4l2_dev == NULL)
        return 0;
    ...
    return 0;
}

int iterate(void *p)
{
    struct device_driver *drv;
    int err;

    /* Find driver 'ivtv' on the PCI bus.
    * pci_bus_type is a global. For USB busses use usb_bus_type.
    */
    drv = driver_find("ivtv", &pci_bus_type);
    /* iterate over all ivtv device instances */
    err = driver_for_each_device(drv, NULL, p, callback);
    put_driver(drv);
    return err;
}

有時候須要對設備實例進行計數以將設備實例映射到模塊的全局數組裏面,可使用如下步驟來完成計數操做:

static atomic_t drv_instance = ATOMIC_INIT(0);

static int drv_probe(struct pci_dev *pdev, const struct pci_device_id *pci_id)
{
    ...
    state->instance = atomic_inc_return(&drv_instance) - 1;
}

若是一個熱拔插設備有不少個設備節點(好比一個USB攝像頭能夠產生多路視頻輸出,雖然它的視頻源是一個),那麼很難知道在何時纔可以安全地卸載 v4l2_device 設備。基於以上問題, v4l2_device 引入了引用計數機制,當 video_register_device 函數被調用的時候,引用計數會加一,當 video_device 被釋放的時候,引用計數會減一,直到 v4l2_device 的引用計數到0的時候,v4l2_devicerelease 回調函數就會被調用,能夠在該回調函數裏面作一些清理工做。當其它的設備(alsa,由於這個不屬於 video 設備,因此也就不能使用上面的 video 函數進行計數的加減操做)節點被建立的時候,能夠人爲調用如下函數對引用計數進行增減操做:

void v4l2_device_get(struct v4l2_device *v4l2_dev);
    int v4l2_device_put(struct v4l2_device *v4l2_dev);
須要注意的是, v4l2_device_register 函數將引用計數初始化爲1,因此須要在 remove 或者 disconnect 回調方法裏面調用 v4l2_device_put 來減小引用計數,不然引用計數將永遠不會達到0。

v4l2_subdev 結構體

不少設備都須要與子設備進行交互,一般狀況下子設備用於音視頻的編解碼以及混合處理,對於網絡攝像機來講子設備就是 sensors 和 camera 控制器。一般狀況下它們都是 I2C 設備,但也有例外。v4l2_subdev 結構體被用於子設備管理。

每個子設備驅動都必須有一個 v4l2_subdev 結構體,這個結構體能夠做爲獨立的簡單子設備存在,也能夠嵌入到更大的結構體(自定義的子設備結構體)裏面。一般會有一個由內核設置的低層次結構體(i2c_client,也就是上面說的 i2c 設備),它包含了一些設備數據,要調用 v4l2_set_subdevdata 來設置子設備私有數據指針指向它,這樣的話就能夠很方便的從 subdev 找到相關的 I2C 設備數據(這個要編程實現的時候纔可以瞭解它的用意)。另外也須要設置低級別結構的私有數據指針指向 v4l2_subdev 結構體,方便從低級別的結構體訪問 v4l2_subdev 結構體,達到雙向訪問的目的,對於 i2c_client 來講,能夠用 i2c_set_clientdata 函數來設置,其它的需使用與之相應的函數來完成設置。

橋驅動器須要存儲每個子設備的私有數據,v4l2_subdev 結構體提供了主機私有數據指針成員來實現此目的,使用如下函數能夠對主機私有數據進行訪問控制:

v4l2_get_subdev_hostdata();
    v4l2_set_subdev_hostdata();

從橋驅動器的角度來看,咱們加載子設備模塊以後能夠用某種方式獲取子設備指針。對於 i2c 設備來講,調用 i2c_get_clientdata 函數便可完成,其它類型的設備也有與之類似的操做,在內核裏面提供了很多的幫助函數來協助完成這部分工做,編程時能夠多多使用。

每一個 v4l2_subdev 結構體都包含有一些函數指針,指向驅動實現的回調函數,內核對這些回調函數進行了分類以免出現定義了一個巨大的回調函數集,可是裏面只有那麼幾個用得上的尷尬狀況。最頂層的操做函數結構體內部包含指向各個不一樣類別操做函數結構體的指針成員,以下所示:

struct v4l2_subdev_core_ops {
        int (*log_status)(struct v4l2_subdev *sd);
        int (*init)(struct v4l2_subdev *sd, u32 val);
        ...
    };

    struct v4l2_subdev_tuner_ops {
        ...
    };

    struct v4l2_subdev_audio_ops {
        ...
    };

    struct v4l2_subdev_video_ops {
        ...
    };

    struct v4l2_subdev_pad_ops {
        ...
    };

    struct v4l2_subdev_ops {
        const struct v4l2_subdev_core_ops    *core;
        const struct v4l2_subdev_tuner_ops    *tuner;
        const struct v4l2_subdev_audio_ops    *audio;
        const struct v4l2_subdev_video_ops    *video;
        const struct v4l2_subdev_vbi_ops    *vbi;
        const struct v4l2_subdev_ir_ops        *ir;
        const struct v4l2_subdev_sensor_ops    *sensor;
        const struct v4l2_subdev_pad_ops    *pad;
    };

<font color='red'>這部分的設計我我的以爲是很是實用的,linux 要想支持大量的設備的同時又要保持代碼的精簡就必須得這樣去實現。</font>core ops成員對於全部的子設備來講都是通用的,其他的成員不一樣的驅動會有選擇的去使用,例如:video 設備就不須要支持 audio 這個 ops 成員。子設備驅動的初始化使用 v4l2_subdev_init 函數來完成(該函數只是初始化一些 v4l2_subdev 的成員變量,內容比較簡單),在初始化以後須要設置子設備結構體的 nameowner 成員(若是是 i2c 設備的話,這個在 i2c helper 函數裏面就會被設置)。該部分 ioctl 能夠直接經過用戶空間的 ioctl 命令訪問到(前提是該子設備在用戶空間生成了子設備節點,這樣的話就能夠操做子設備節點來進行 ioctl)。內核裏面可使用 v4l2_subdev_call 函數來對這些回調函數進行調用,這個在 pipeline 管理的時候十分受用。

若是須要與 media framework 進行集成,必須初始化 media_entity 結構體並將其嵌入到 v4l2_subdev 結構體裏面,操做以下所示:

struct media_pad *pads = &my_sd->pads;
    int err;

    err = media_entity_init(&sd->entity, npads, pads, 0);

其中 pads 結構體變量必須提早初始化,media_entityflagsnametypeops 成員須要設置。entity 的引用計數在子設備節點被打開/關閉的時候會自動地增減。在銷燬子設備的時候需使用 media_entity_cleanup 函數對 entity 進行清理。若是子設備須要處理 video 數據,就須要實現 v4l2_subdev_video_ops 成員,若是要集成到 media_framework 裏面,就必需要實現 v4l2_subdev_pad_ops 成員,此時使用 pad_ops 中與 format 有關的成員代替 v4l2_subdev_video_ops 中的相關成員。

子設備驅動須要設置 link_validation 成員來提供本身的 link validation 函數,該回調函數用來檢查 pipeline 上面的全部的 link 是否有效(是否有效由本身來作決定),該回調函數在 media_entity_pipeline_start 函數裏面被循環調用。若是該成員沒有被設置,那麼 v4l2_subdev_link_validate_default 將會做爲默認的回調函數被使用,該函數確保 link 的 source pad 和 sink pad 的寬、高、media 總線像素碼是一致的,不然就會返回錯誤。

有兩種方法能夠註冊子設備(注意是設備,不是設備驅動,經常使用的方式是經過設備樹來註冊),第一種(舊的方法,好比使用 platform_device_register 來進行註冊)是使用橋驅動去註冊設備。這種狀況下,橋驅動擁有鏈接到它的子設備的完整信息,而且知道什麼時候去註冊子設備,內部子設備一般屬於這種狀況。好比 SOC 內部的 video 數據處理單元,鏈接到 USB 或 SOC 的相機傳感器。另外一種狀況是子設備必須異步地被註冊到橋驅動上,好比基於設備樹的系統,此時全部的子設備信息都獨立於橋驅動器。使用這兩種方法註冊子設備的區別是 probing 的處理方式不一樣。也就是一種是設備信息結構體由驅動自己持有並註冊,一種是設備信息結構體由設備樹持有並註冊。

設備驅動須要用 v4l2_device 信息來註冊 v4l2_subdev ,以下所示:

int err = v4l2_device_register_subdev(v4l2_dev, sd);

若是子設備模塊在註冊以前消失的話,該操做就會失敗,若是成功的話就會使得 subdev->dev 指向 v4l2_device。若是 v4l2_device 父設備的 mdev 成員不爲空的話,子設備的 entity 就會自動地被註冊到 mdev 指向的 media_device 裏面。在子設備須要被卸載而且 sd->dev 變爲NULL以後,使用以下函數來卸載子設備:

v4l2_device_unregister_subdev(sd);

若是子設備被註冊到上層的 v4l2_device 父設備中,那麼 v4l2_device_unregister 函數就會自動地把全部子設備卸載掉。但爲了以防萬一以及保持代碼的風格統一,須要註冊與卸載結對使用。能夠用如下方式直接調用ops成員:err = sd->ops->core->g_std(sd, &norm); 使用下面的宏定義能夠簡化書寫:err = v4l2_subdev_call(sd, core, g_std, &norm);該操做會檢查 sd->dev 指針是否爲空,若是是,返回 -ENODEV,同時若是 ops->core 或者 ops->core->g_std 爲空,則返回 -ENOIOCTLCMD 。也能夠經過如下函數調用來對 V4l2 下面掛載的全部子設備進行回調:

v4l2_device_call_all(v4l2_dev, 0, core, g_std, &norm);

該函數會跳過全部不支持該 ops 的子設備,而且全部的錯誤信息也被忽略,若是想捕獲錯誤信息,可使用下面的函數:

err = v4l2_device_call_until_err(v4l2_dev, 0, core, g_std, &norm);

該函數的第二個參數若是爲 0,則全部的子設備都會被訪問,若是非 0,則指定組的子設備會被訪問。

組ID使得橋驅動可以更加精確的去調用子設備操做函數,例如:在一個單板上面有不少個聲卡,每一個都可以改變音量,可是一般狀況下只訪問一個,這時就能夠設置子設備的組 ID 爲 AUDIO_CONTROLLER 並指定它的值,這時 v4l2_device_call_all 函數就會只去訪問指定組的子設備,提升效率。

若是子設備須要向 v4l2_device 父設備發送事件通知的話,就能夠調用 v4l2_subdev_notify 宏定義來回調 v4l2->notify 成員(前文有提到過)。

使用 v4l2_subdev 的優勢是不包含任何底層硬件的信息,它是對底層硬件的一個抽象,所以一個驅動可能包含多個使用同一條 I2C 總線的子設備,也可能只包含一個使用 GPIO 管腳控制的子設備,只有在驅動設置的時候纔有這些差異,而一旦子設備被註冊以後,底層硬件對驅動來講就是徹底透明的。

不清楚異步模式的用途
在異步模式下,子設備 probing 能夠被獨立地被調用以檢查橋驅動是否可用,子設備驅動必須確認全部的 probing 請求是否成功,若是有任意一個請求條件沒有知足,驅動就會返回 -EPROBE_DEFER 來繼續下一次嘗試,一旦全部的請求條件都被知足,子設備就須要調用 v4l2_async_register_subdev 函數來進行註冊(用 v4l2_async_unregister_subdev 卸載)。橋驅動反過來得註冊一個 notifier 對象( v4l2_async_notifier_register),該函數的第二個參數類型是 v4l2_async_notifier 類型的結構體,裏面包含有一個指向指針數組的指針成員,指針數組每個成員都指向 v4l2_async_subdev 類型結構體。v4l2 核心層會利用上述的異步子設備結構體描述符來進行子設備的匹配,若是成功匹配, .bound()notifier回調函數將會被調用,當全部的子設備所有被加載完畢以後, .complete() 回調函數就會被調用,子設備被移除的時候 .unbind() 函數就會被調用。

另外子設備還提供了一組內部操做函數,該內部函數的調用時機在下面有描述,原型以下所示:

struct v4l2_subdev_internal_ops {
    int (*registered)(struct v4l2_subdev *sd);
    void (*unregistered)(struct v4l2_subdev *sd);
    int (*open)(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh);
    int (*close)(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh);
};

這些函數僅供 v4l2 framework 使用,驅動程序不該該顯式的去調用這些回調

  • registered/unregister:在子設備被註冊(v4l2_device_register_subdev)/反註冊的時候被調用。
  • open/close:若是子設備在用戶空間建立了設備節點,那麼這兩個函數就會在用戶空間的設備節點被打開/關閉的時候調用到,主要是用來建立/關閉v4l2_fh以供v4l2_ctrl_handler等的使用。

v4l2子設備用戶空間API

能夠在 /dev 文件夾下建立 v4l-subdevX 設備節點以供用戶直接操做子設備硬件。若是須要在用戶空間建立設備節點的話,就須要在子設備節點註冊以前設置 V4L2_SUBDEV_FL_HAS_DEVNODE 標誌,而後調用 v4l2_device_register_subdev_nodes() 函數,就能夠在用戶空間建立設備節點,設備節點會在子設備卸載的時候自動地被銷燬。

VIDIOC_QUERYCTRL
    VIDIOC_QUERYMENU
    VIDIOC_G_CTRL
    VIDIOC_S_CTRL
    VIDIOC_G_EXT_CTRLS
    VIDIOC_S_EXT_CTRLS
    VIDIOC_TRY_EXT_CTRLS

上述 ioctls 能夠經過設備節點訪問,也能夠直接在子設備驅動裏面調用。

VIDIOC_DQEVENT
    VIDIOC_SUBSCRIBE_EVENT
    VIDIOC_UNSUBSCRIBE_EVENT

要使用上述事件,就必須設置 v4l2_subdevV4L2_SUBDEV_USES_EVENTS 標誌位,實現 core_opssubscribe 相關的回調函數,回調函數裏面須要初始化 events,而後註冊 v4l2_subdev。一些私有的 ioctls 能夠在 v4l2_subdevops->core->ioctl 裏面實現。

I2C子設備驅動

要想在 I2C 驅動裏面添加 v4l2_subdev 支持,就須要把 v4l2_subdev 結構體嵌入到每一個 I2C 實例結構體裏面,有一些比較簡單的 I2C 設備不須要自定義的狀態結構體,此時只須要建立一個單獨的 v4l2_subdev 結構體便可。一個典型的驅動自定義狀態結構體以下所示:

struct chipname_state {
        struct v4l2_subdev sd;
        ...  /* additional state fields */
    };

使用 v4l2_i2c_subdev_init 去初始化一個 I2C 子設備,該函數會填充 v4l2_subdev 的全部成員並確保 v4l2_subdevi2c_client 互相指向對方。也能夠添加內聯函數來從 v4l2_subdev 的指針獲取到 i2c_client 結構體:

struct i2c_client *client = v4l2_get_subdevdata(sd);
也能夠從i2c_client結構體指針獲取到v4l2_subdev結構體:
struct v4l2_subdev *sd = i2c_get_clientdata(client);
橋驅動可使用如下幫助函數來建立一個I2C子設備:
struct v4l2_subdev *sd = v4l2_i2c_new_subdev
    (v4l2_dev, adapter,"module_foo", "chipid", 0x36, NULL);

該函數會加載給定的模塊(能夠爲空)而且調用 i2c_new_device 根據傳入的參數建立子設備結構體,最後註冊 v4l2_subdev

video_device 結構體

video_device 能夠動態的分配:

struct video_device *vdev = video_device_alloc();
if (vdev == NULL)
    return -ENOMEM;
vdev->release = video_device_release;

若是須要將 video_device 結構體嵌入到更大的結構體裏面的話,就須要設置 vdevrelease 成員。內核提供了兩個默認的 release 回調函數,以下:

video_device_release()       // 僅僅調用kfree釋放分配的內存,用於動態分配狀況下
video_device_release_empty() // 不作任何事情,靜態變量

如下的函數成員必須被設置:

  • v4l2_dev:必須指向v4l2_device父設備
  • vfl_dir:VFL_DIR_RX(capture設備)、VFL_DIR_TX(輸出設備)、VFL_DIR_M2M(codec設備)
  • fops:設置v4l2_file_operations結構體
  • ioctl_ops:ioctls,能夠經過設備節點被用戶空間程序訪問,需設置fops的.unlocked_ioctl指向video_ioctl2
  • lock:若是想要在驅動空間裏作鎖操做,能夠設置爲NULL。不然須要指向一個已經初始化的mutex_lock結構體
  • queue:指向一個vb2_queue結構體,若是queue->lock不爲空,那麼與隊列相關的ioctls就會使用queue內部的鎖,這樣的話就不用等待其它類型的ioctls操做
  • prio:對優先級進行跟蹤,用在VIDIOC_G/S_PRIORITY上,若是爲空的話就會使用v4l2_device裏面的v4l2_prio_state
  • dev_parent:指向v4l2_device便可

若是想忽略 ioctl_ops 中某個 ioctls 的話能夠調用下面的函數:

void v4l2_disable_ioctl(struct video_device *vdev, unsigned int cmd);

若是要集成到 media_framework 裏面,就須要設置 video_device 裏面的 media_entity 成員,同時須要提供 media_pad

struct media_pad *pad = &my_vdev->pad;
    int err;
    err = media_entity_init(&vdev->entity, 1, pad, 0);
  • video_device 的註冊

video_device 的註冊函數以下:

err = video_register_device(vdev, VFL_TYPE_GRABBER, -1);

該段代碼會註冊一個字符設備驅動程序並在用戶空間生成一個設備節點。若是 v4l2_device 父設備的 mdev 成員不爲空的話,video_deviceentity 會被自動的註冊到 media framework 裏面。函數最後一個參數是設備節點索引號,若是是 -1 的話就取用第一個內核中可用的索引號值。註冊的設備類型以及用戶空間中的節點名稱取決於如下標識:

VFL_TYPE_GRABBER: videoX 輸入輸出設備
    VFL_TYPE_VBI: vbiX 
    VFL_TYPE_RADIO: radioX 硬件定義的音頻調諧設備
    VFL_TYPE_SDR: swradioX 軟件定義的音頻調諧設備

當一個設備節點被建立時,相關屬性也會被建立,能夠在 /sys/class/video4linux 裏面看到這些設備文件夾,在文件夾裏面能夠看到 'name','dev_debug','index','uevent'等屬性,可使用 cat 命令查看。'dev_debug' 能夠用於 video 設備調試,每一個 video 設備都會建立一個 'dev_debug' 屬性,該屬性以文件夾的形式存在與 /sys/class/video4linux/<devX>/ 下面以供使能 log file operation。'dev_debug'是一個位掩碼,如下位能夠被設置:

0x01:記錄ioctl名字與錯誤碼。設置0x08位能夠只記錄VIDIOC_(D)QBUF
    0x02:記錄ioctl的參數與錯誤碼。設置0x08位能夠只記錄VIDIOC_(D)QBUF
    0x04:記錄file ops操做。設置0x08位能夠只記錄read&write成員的操做
    0x08:如上所示
    0x10:記錄poll操做

當以上的位被設置的時候,發生相關的調用或者操做的時候內核就會打印出來相關的調用信息到終端上面。相似於

[173881.402120] video4: VIDIOC_DQEVENT: error -2
[173884.906633] video4: VIDIOC_UNSUBSCRIBE_EVENT
  • video設備的清理

當 video 設備節點須要被移除或者USB設備斷開時,須要執行如下函數:

video_unregister_device(vdev);

來進行設備的卸載,該函數會移除 /dev 下的設備節點文件,同時不要忘記調用 media_entity_cleanup 來清理 entity。

ioctls 與 locking

V4L 核心層提供了可選的鎖服務,最主要的就是 video_device 裏面的鎖,用來進行 ioctls 的同步。若是使用了 videobuf2 框架,那麼 video_device->queue->lock 鎖也會被用來作 queue 相關的 ioctls 同步。使用不一樣的鎖有不少優勢,好比一些設置相關的 ioctls 花費的時間比較長,若是使用獨立的鎖,VIDIOC_DQBUF就不用等待設置操做的完成就能夠執行,這個在網絡攝像機驅動中很常見。固然,也能夠徹底由驅動自己去完成鎖操做,這時能夠設置全部的鎖成員爲NULL並實現一個驅動本身的鎖。

若是使用舊的 videobuf,須要將 video_device 的鎖傳遞給 videobuf queue 初始化函數,若是 videobuf 正在等待一幀數據的到達,此時會將鎖暫時釋放,等數據到達以後再次加鎖,不然別的處理程序就沒法訪問。因此不推薦使用舊的 videobuf。若是是在 videobuf2 框架下,須要實現 wait_preparewait_finish 回調函數去釋放或者獲取鎖,若是使用了 queue->lock,可使用 V4L2 提供的回調 vb2_ops_wait_prepare/finish 幫助函數來完成加鎖與解鎖的操做,它們會使用 queue->lock這個鎖(此時必定要將該鎖初始化)。

v4l2_fh 結構體

該結構體提供了一種簡單的保存文件句柄特定數據的方法。v4l2_fh 的使用者-v4l2 framework 能夠經過檢查 video_device->flagsV4L2_FL_USES_V4L2_FH 位來知道驅動是否使用 v4l2_fh 做爲 file->private_data 指針,該標誌位經過調用函數 v4l2_fh_init 來設置。

v4l2_fh 結構體做爲驅動本身的文件句柄存在,而且在驅動的 open 函數裏面設置 file->private_data 指向它,v4l2_fh 有多個的時候會做爲一個鏈表存在於 file->private_data 中,能夠遍歷訪問。在大多數狀況下 v4l2_fh 結構體都被嵌入到更大的結構體裏面,此時須要在 open 函數裏面調用
v4l2_fh_init+v4l2_fh_add 進行添加,在 release 函數裏面調用 v4l2_fh_del+v4l2_fh_exit 進行退出。驅動可使用 container_of 來訪問本身的文件句柄結構體,以下所示:

struct my_fh {
    int blah;
    struct v4l2_fh fh;
};

int my_open(struct file *file)
{
    struct my_fh *my_fh;
    struct video_device *vfd;
    int ret;

    my_fh = kzalloc(sizeof(*my_fh), GFP_KERNEL);

    v4l2_fh_init(&my_fh->fh, vfd);

    file->private_data = &my_fh->fh;
    v4l2_fh_add(&my_fh->fh);
    return 0;
}

int my_release(struct file *file)
{
    struct v4l2_fh *fh = file->private_data;
    struct my_fh *my_fh = container_of(fh, struct my_fh, fh);

    v4l2_fh_del(&my_fh->fh);
    v4l2_fh_exit(&my_fh->fh);
    kfree(my_fh);
    return 0;
}

如以上代碼所示,因爲 open 函數可能會被多個應用 app 所調用,因此 fh 也會有多個,可是 file->private 永遠指向最新的一個 v4l2_fh ,經過這個 v4l2_fh 能夠找到整個 v4l2_fh 鏈表中的全部元素。一些驅動須要在第一個文件句柄打開後以及最後一個文件句柄關閉前的時候作一些其它的工做,下面兩個幫助函數能夠檢查 v4l2_fh 結構體是否只剩下一個 entry:

int v4l2_fh_is_singular(struct v4l2_fh *fh)
若是是隻有一個entry,返回1,不然返回0,若是fh爲空也返回0。

int v4l2_fh_is_singular_file(struct file *filp)
和上面差很少,可是使用 filp->private_data 這一數據源,實際上它是指向最新的一個v4l2_fh的。

V4L2 events

V4L2 events 提供一種通用的方法來傳遞 events 到用戶空間,驅動程序必須使用 v4l2_fh(設置 video_deviceflags 位)纔可以實現對 V4L2 events 的支持。events 用類型和 ID 做爲區分標識,沒有使用到的 events 的ID就是0。

當用戶訂閱 event 時,用戶空間會相應地爲每一個 event 分配一個 kevent 結構體(若是 elems 參數爲0的話只有一個,不爲0就按照指定的數量分配),因此每一個 event 都有一個或多個屬於本身的 kevent 結構體,這就保證了若是驅動短期內生成了很是多的 events 也不會覆蓋到其它的同類型 events,能夠看做是分了好幾個籃子來放不一樣類型的水果。event 結構體是 v4l2_subscribed_event 結構體的最後一個成員,以數組的形式存在,而且是一個柔性數組(struct v4l2_kevent events[]),也就是說在分配 v4l2_subscribed_event 結構體空間的時候,events 並不佔用空間,須要額外爲指定數量的 events 分配空間,kzalloc(siezof(struct v4l2_subscribed_event) + sizeof(struct v4l2_kevent) * num, GFP_KERNEL);在使用的時候,徹底能夠按照數組的方式去對 kevent 進行尋址,很方便。

若是得到的 event 數量比 kevent 的還要多,那麼舊的 events 就會被丟棄。能夠設置結構體 v4l2_subscribed_eventmerge、replace 回調函數(其實默認的函數就足夠用了),它們會在 event 被捕獲而且沒有更多的空間來存放 event 時被調用。在 v4l2_event.c 裏面有一個很好的關於 replace/merge 的例子,ctrls_replace()與ctrls_merge() 被做爲回調函數使用。因爲這兩個函數能夠在中斷上下文被調用,所以必須得快速執行完畢並返回。

關於events的循環是一個比較有意思的操做,入隊時:三個變量(first-下一個準備被dequeue的eventm,elems-總kevent數量,in_use-已經使用的kevent數量)

  1. 若elems == in_use,說明隊列成員已經用完。
  2. 取出第一個kevent,從available隊列中刪掉,first指向數組的下一個成員,in_use --。
  3. 找到上一步中(first指向數組的下一個成員),將上一步(取出第一個kevent)的changes位進行合併賦值給前者。

由於後者比前者更新,因此數值徹底能夠覆蓋前者,同時又保留了前者的變化。

  1. 取出第in_use + first >= elems ? in_use + first - elems : in_use + first;個數組kevent項做爲新的填充項。
  2. in_use ++

一些有用的函數:

int v4l2_event_subscribe(struct v4l2_fh *fh, struct v4l2_event_subscription *sub, 
        unsigned elems, const struct v4l2_subscribed_event_ops *ops)

當用戶空間經過 ioctl 發起訂閱請求以後,video_device->ioctl_ops->vidioc_subscribe_event
須要檢查是否支持請求的 event,若是支持的話就調用上面的函數進行訂閱。通常能夠將 video 的相關 ioctl 指向內核默認的 v4l2_ctrl_subscribe_event() 函數。

int v4l2_event_unsubscribe(struct v4l2_fh *fh, struct v4l2_event_subscription *sub)
取消一個事件的訂閱,V4L2_EVENT_ALL類型能夠用於取消全部事件的訂閱。通常能夠將video的相關ioctl指向該函數。

void v4l2_event_queue(struct video_device *vdev, const struct v4l2_event *ev)
該函數用做events入隊操做(由驅動完成),驅動只須要設置type以及data成員,其他的交由V4L2來完成。

int v4l2_event_dequeue(struct v4l2_fh *fh, struct v4l2_event *event,
               int nonblocking)
events出隊操做,發生於用戶空間的VIDIOC_DQEVENT調用,做用是從available隊列中取出一個events。

v4l2_subscribed_event_ops 參數容許驅動程序設置如下四個回調函數成員:

  • add:添加一個事件訂閱時被調用
  • del:取消一個事件訂閱時被調用
  • replace:event以新換舊,隊列滿時被調用,下同,經常使用於只有一個elems的狀況下,拷貝kevent.u.ctrl項。
  • merge:將舊的event合併到新的event中,用於多個elems的狀況下,只合並changes項,緣由見上面event循環過程描述。

events 經過 poll 系統調用傳遞到用戶空間,驅動能夠將 v4l2_fh->wait 做爲 poll_wait() 的參數。子設備能夠直接經過 notify 函數向 v4l2_device 發送 events(使用V4L2_DEVICE_NOTIFY_EVENT)。drivers/media/platform/omap3isp給出瞭如何使用event的實例。

注意事項:

  • 注意v4l2_event_subscribe的elems參數,若是爲0,則內核就默認分配爲1,不然按照指定的參數值分配。
  • 最好不要使用內核默認的v4l2_subscribed_event_ops,由於它的add函數會嘗試在v4l2_ctrl裏面查找相應id的ctrl,若是

是自定義的event id的話,有可能找不到相關的ctrl項,這樣的話用戶空間的VIDIOC_SUBSCRIBE_EVENT就會返回失敗。

  • 用戶空間dqevent以後沒必要關心還回的操做,由於內核會自動獲取用過的kevent,用柔性數組去管理而不是分散的鏈表。
  • 子設備能夠經過v4l2_subdev_notify_event函數調用來入隊一個event並通知v4l2設備的notify回調。
  • v4l2_event_queue函數會遍歷video_device上面全部的v4l2_fh,將event入隊到每個fh的列表當中。fh由用戶打開video

設備節點的時候產生,每個用戶打開video節點時都會爲其分配一個單獨的v4l2_fh。

  • file->private永遠指向最新的一個v4l2_fh,經過這個v4l2_fh能夠找到整個v4l2_fh鏈表中的全部元素。
  • v4l2_fh_release函數會將全部掛載該fh上面的事件所有取消訂閱。

寫到這裏,本文就算結束了,這部分會發現不少東西都是點到即撤,沒有深刻去解釋,深刻的這部分放在後面來完成,還有一個就是可能會感受裏面有不少東西看着可能知道是什麼,可是反應到實際代碼裏面,實際應用裏面就不知道是什麼了,這個時候就必須結合代碼來進行實際操做實驗纔可以確切瞭解。還有一種狀況就是可能需求比較簡單,一些特性永遠用不到,這個時候也不要緊,那就用到的時候再去翻看就好。


<center>想作的事就去作吧</center>
<center><img src="http://www.yellowmax2001.com/...;>

相關文章
相關標籤/搜索