V4L2框架概述

轉載自-YellowMax-的博文:https://blog.csdn.net/u013904227/article/details/80718831

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

導讀: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 視頻採集卡的驅動模板使用。在最開始的時候也可以參照這個代碼編寫方式進行聯繫,當然最適合的代碼還是 drivers/media/video/omap3isp 文件夾裏面的代碼,這個代碼基本上可以作爲一個完整的輸入設備實例代碼(因爲它包含了 ISP、CSI、video 等設備,並且有着一個完整的數據流 pipeline,幾乎用到了 V4L2 框架的方方面面,參考價值極大)來進行參考編寫自己的設備驅動代碼。

V4L2框架藍圖

藍圖解構

這是一張非常大的圖,但是我只選取了其中的一個,這張圖對 V4L2 裏面的子模塊進行簡化(簡化到只有子模塊的名字,沒有內部實現的介紹),大圖如下:
V4L2 設備拓撲

V4L2 設備拓撲

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

  • 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 層次結構
    V4L2 層次結構

驅動結構體

所有的 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;
}
  • 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

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

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

如果一個熱拔插設備有很多個設備節點(比如一個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);
  • 1
  • 2


*需要注意的是,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();
  • 1
  • 2

從橋驅動器的角度來看,我們加載子設備模塊之後可以用某種方式獲取子設備指針。對於 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;
    };
  • 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

這部分的設計我個人覺得是非常實用的,linux 要想支持大量的設備的同時又要保持代碼的精簡就必須得這樣去實現。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);
  • 1
  • 2
  • 3
  • 4

其中 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);
  • 1

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

    v4l2_device_unregister_subdev(sd);
  • 1

如果子設備被註冊到上層的 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);
  • 1

該函數會跳過所有不支持該 ops 的子設備,並且所有的錯誤信息也被忽略,如果想捕獲錯誤信息,可以使用下面的函數:

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

該函數的第二個參數如果爲 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);
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

這些函數僅供 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
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

上述 ioctls 可以通過設備節點訪問,也可以直接在子設備驅動裏面調用。

    VIDIOC_DQEVENT
    VIDIOC_SUBSCRIBE_EVENT
    VIDIOC_UNSUBSCRIBE_EVENT
  • 1
  • 2
  • 3

要使用上述事件,就必須設置 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 */
    };
  • 1
  • 2
  • 3
  • 4

使用 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);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

該函數會加載給定的模塊(可以爲空)並且調用 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;
  • 1
  • 2
  • 3
  • 4

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

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

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

  • 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);
  • 1

如果要集成到 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);
  • 1
  • 2
  • 3
  • video_device 的註冊
    video_device 的註冊函數如下:
    err = video_register_device(vdev, VFL_TYPE_GRABBER, -1);
  • 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 軟件定義的音頻調諧設備
  • 1
  • 2
  • 3
  • 4

當一個設備節點被創建時,相關屬性也會被創建,可以在 /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操作
  • 1
  • 2
  • 3
  • 4
  • 5

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

[173881.402120] video4: VIDIOC_DQEVENT: error -2
[173884.906633] video4: VIDIOC_UNSUBSCRIBE_EVENT
  • 1
  • 2
  • video設備的清理
    當 video 設備節點需要被移除或者USB設備斷開時,需要執行以下函數:
    video_unregister_device(vdev);
  • 1

來進行設備的卸載,該函數會移除 /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;
}
  • 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

如以上代碼所示,由於 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爲空也返回0int v4l2_fh_is_singular_file(struct file *filp)
和上面差不多,但是使用 filp->private_data 這一數據源,實際上它是指向最新的一個v4l2_fh的。
  • 1
  • 2
  • 3
  • 4
  • 5

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位進行合併賦值給前者。
因爲後者比前者更新,所以數值完全可以覆蓋前者,同時又保留了前者的變化。
4. 取出第in_use + first >= elems ? in_use + first - elems : in_use + first;個數組kevent項作爲新的填充項。
5. in_use ++
/**************************************************/

一些有用的函數:

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

當用戶空間通過 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。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

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上面的事件全部取消訂閱。

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


想做的事就去做吧

微信公衆號

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章