Camera | 5.Linux v4l2架構(基於rk3568)

2023-03-03 06:00:52

上一篇我們講解了如何編寫基於V4L2的應用程式編寫,本文主要講解核心中V4L2架構,以及一些最重要的結構體、註冊函數。

廠家在實現自己的攝像頭控制器驅動時,總體上都遵循這個架構來實現,但是不同廠家、不同型號的SoC,具體的驅動實現仍然會有一些差別。

讀者可以通過本文了解各個結構體與對應的攝像頭模組、SoC上控制器模組、以及他們之間介面關係,並能夠了解這些硬體模組與V4L2架構之間關係。

下一張我們基於瑞芯微rk3568來詳細講解具體V4L2的實現。

一、V4L2架構

V4L2子系統是Linux核心中關於Video(視訊)裝置的API介面,是V4L(Video for Linux)子系統的升級版本。

V4L(Video for Linux)是Linux核心中關於視訊裝置的API介面,出現於Linux核心2.1版本,經過修改bug和新增功能,Linux核心2.5版本推出了V4L2(Video for Linux Two)子系統,功能更多且更穩定。

V4L2子系統向上為虛擬檔案系統提供了統一的介面,應用程式可通過虛擬檔案系統存取Video裝置。

V4L2子系統向下給Video裝置提供介面,同時管理所有Video裝置。

二、V4L2架構包括哪些裝置

  1. Video裝置又分為主裝置和從裝置對於Camera來說,
    主裝置:
    Camera Host控制器為主裝置,負責影象資料的接收和傳輸,
    從裝置:
    從裝置為Camera Sensor,一般為I2C介面,可通過從裝置控制Camera採集影象的行為,如影象的大小、影象的FPS等。

  2. V4L2的主裝置號是81,次裝置號範圍0~255
    這些次裝置號又分為多類裝置:

  • 視訊裝置(次裝置號範圍0-63)
  • Radio(收音機)裝置(次裝置號範圍64-127)
  • Teletext裝置(次裝置號範圍192-223)
  • VBI裝置(次裝置號範圍224-255)。
  1. V4L2裝置對應的裝置節點有/dev/videoX、/dev/vbiX、/dev/radioX
    本文只討論視訊裝置,視訊裝置對應的裝置節點是/dev/videoX,視訊裝置以高頻攝像頭或Camera為輸入源,Linux核心驅動該類裝置,接收相應的視訊資訊並處理。

V4L2框架的架構如下圖所示:

  • user space:
    應用程式主要通過libv4l庫來操作攝像頭
    也可以基於字元裝置/dev/videoX自己編寫應用程式
    guvcview:用於偵錯usb攝像頭(還有個軟體cheese也可以)
    v4l2 utilities: v4l2 的工具集(參考前面第3篇文章)

  • kernel space:
    sensor、ISP、VIPP、CSI、CCI都為從裝置
    從dphy物理層獲取視訊資料冊通過vb2子模組
    CCI :主要是通過GPIO(供電、片選)、I2C(下發設定命令給sensor)實現設定sensor
    EHCI/OHCI:USB型別攝像頭

  • hardware
    CSIC Controller:從dphy獲取mipi協定幀
    I2C Controller:與sensor的i2c block通訊
    GPIO Controller:sensor通常需要供電或者片選

  • external device
    sensror:攝像頭的介面主要有:USB,DVP.MIPI(CSI)

三、Linux核心中V4L2驅動程式碼

Linux系統中視訊輸入裝置主要包括以下四個部分:

  • 1.字元裝置驅動:
    V4L2本身就是一個字元裝置,具有字元裝置所有的特性,暴露介面給使用者空間;

  • 2.V4L2驅動核心:
    主要是構建一個核心中標準視訊裝置驅動的框架,為視訊操作提供統一的介面函數;

  • 3.平臺V4L2裝置驅動:
    在V4L2框架下,根據平臺自身的特性實現與平臺相關的V4L2驅動部分,包括註冊video_device和v4l2_device;

  • 4.具體的sensor驅動:
    主要上電、提供工作時鐘、視訊影象裁剪、流IO開啟等,實現各種裝置控制方法供上層呼叫並註冊v4l2_subdev。

V4L2核心原始碼位於drivers/media/v4l2-core,根據功能可以劃分為四類:

由上圖可知:

  • 1.字元裝置模組:
    由v4l2-dev.c實現,主要作用申請字元主裝置號、註冊class和提供video device註冊登出等相關函數。

  • 2.V4L2基礎框架:
    由v4l2-device.c、v4l2-subdev.c、v4l2-fh.c、v4l2-ctrls.c等檔案構建V4L2基礎框架。

  • 3.videobuf管理
    由videobuf2-core.c、videobuf2-dma-contig.c、videobuf2-dma-sg.c、videobuf2-memops.c、videobuf2-vmalloc.c、v4l2-mem2mem.c等檔案實現,完成videobuffer的分配、管理和登出。

  • 4.Ioctl框架:
    由v4l2-ioctl.c檔案實現,構建V4L2 ioctl的框架。

瑞芯微平臺還包括ISP的驅動框架,下面是rk3568對應的ISP相關程式碼:
Linux Kernel-4.19
	|-- arch/arm/boot/dts DTS組態檔
	|-- drivers/phy/rockchip
	|-- phy-rockchip-mipi-rx.c mipi dphy驅動
	|-- phy-rockchip-csi2-dphy-common.h
	|-- phy-rockchip-csi2-dphy-hw.c
	|-- phy-rockchip-csi2-dphy.c
	|-- drivers/media
		|-- v4l2-core
		|-- platform/rockchip/cif RKCIF驅動
		|-- platform/rockchip/isp RKISP驅動
			|-- dev.c 包含 probe、非同步註冊、clock、pipeline、 iommu及media/v4l2 framework
			|-- capture_v21.c 包含 mp/sp/rawwr的設定及 vb2,幀中斷處理
			|-- dmarx.c 包含 rawrd的設定及 vb2,幀中斷處理
			|-- isp_params.c 3A相關引數設定
			|-- isp_stats.c 3A相關統計
			|-- isp_mipi_luma.c mipi資料亮度統計
			|-- regs.c 暫存器相關的讀寫操作
			|-- rkisp.c isp subdev和entity註冊,包含從 mipi 接收資料,並有 crop 功能
			|-- csi.c csi subdev和mipi設定
			|-- bridge.c bridge subdev,isp和ispp互動橋樑
		|-- platform/rockchip/ispp rkispp驅動
			|-- dev.c 包含 probe、非同步註冊、clock、pipeline、 iommu及media/v4l2 framework
			|-- stream.c 包含 4路video輸出的設定及 vb2,幀中斷處理
			|-- rkispp.c ispp subdev和entity註冊
			|-- params.c TNR/NR/SHP/FEC/ORB引數設定
			|-- stats.c ORB統計資訊
		|-- i2c
			|-- ov13850.c CIS(cmos image sensor)驅動

四、結構體詳解

V4L2中有幾個最重要的幾個結構體,v4l2_device、video_device、v4l2_subdev等。
他們大致關係如下:

1.v4l2_device主裝置

V4L2主裝置範例使用struct v4l2_device結構體表示,v4l2_device是V4L2子系統的入口,管理著V4L2子系統的主裝置和從裝置;

v4l2_device用來描述一個v4l2裝置範例,可以包含多個子裝置,對應的是例如 I2C、CSI、MIPI 等裝置,它們是從屬於一個 V4L2 device 之下的;

簡單裝置可以僅分配這個結構體,但在大多數情況下,都會將這個結構體嵌入到一個更大的結構體中以提供v4l2框架的功能,比如struct isp_device

需要與媒體框架整合的驅動必須手動設定dev->driver_data,指向包含v4l2_device結構體範例的驅動特定裝置結構體。這可以在註冊V4L2裝置範例前通過dev_set_drvdata()函數完成。

同時必須設定v4l2_device結構體的mdev域,指向適當的初始化並註冊過的media_device範例。

 [include/media/v4l2-device.h]
    struct v4l2_device {
        struct device *dev;  // 父裝置指標
    #if defined(CONFIG_MEDIA_CONTROLLER)  // 多媒體裝置設定選項
        // 用於執行時資料流的管理,
        struct media_device *mdev;
    #endif
        // 註冊的子裝置的v4l2_subdev結構體都掛載此連結串列中
        struct list_head subdevs;
        // 同步用的自旋鎖
        spinlock_t lock;
        // 獨一無二的裝置名稱,預設使用driver name + bus ID
        char name[V4L2_DEVICE_NAME_SIZE];
        // 被一些子裝置回撥的通知函數,但這個設定與子裝置相關。子裝置支援的任何通知必須在
        // include/media/<subdevice>.h 中定義一個訊息頭。
        void (*notify)(struct v4l2_subdev *sd, unsigned int notification, void *arg);
        // 提供子裝置(主要是video和ISP裝置)在使用者空間的特效操作介面,
        // 比如改變輸出影象的亮度、對比度、飽和度等等
        struct v4l2_ctrl_handler *ctrl_handler;
        // 裝置優先順序狀態
        struct v4l2_prio_state prio;
        /* BKL replacement mutex. Temporary solution only. */
        struct mutex ioctl_lock;
        // struct v4l2_device結構體的參照計數,等於0時才釋放
        struct kref ref;
        // 參照計數ref為0時,呼叫release函數進行釋放資源和清理工作
        void (*release)(struct v4l2_device *v4l2_dev);
    };

註冊函數:

v4l2_device_register

使用v4l2_device_register註冊v4l2_device結構體.如果v4l2_dev->name為空,則它將被設定為從dev中衍生出的值(為了更加精確,形式為驅動名後跟bus_id)。

如果在呼叫v4l2_device_register前已經設定好了,則不會被修改。如果dev為NULL,則必須在呼叫v4l2_device_register前設定v4l2_dev->name。可以基於驅動名和驅動的全域性atomic_t型別的範例編號,通過v4l2_device_set_name()設定name。

這樣會生成類似ivtv0、ivtv1等名字。若驅動名以數位結尾,則會在編號和驅動名間插入一個破折號,如:cx18-0、cx18-1等。

dev引數通常是一個指向pci_dev、usb_interface或platform_device的指標,很少使其為NULL,除非是一個ISA裝置或者當一個裝置建立了多個PCI裝置,使得v4l2_dev無法與一個特定的父裝置關聯。

使用v4l2_device_unregister解除安裝v4l2_device結構體。如果dev->driver_data域指向 v4l2_dev,將會被重置為NULL。主裝置登出的同時也會自動登出所有子裝置。如果你有一個熱插拔裝置(如USB裝置),則當斷開發生時,父裝置將無效。

由於v4l2_device有一個指向父裝置的指標必須被清除,同時標誌父裝置已消失,所以必須呼叫v4l2_device_disconnect函數清理v4l2_device中指向父裝置的dev指標。v4l2_device_disconnect並不登出主裝置,因此依然要呼叫v4l2_device_unregister函數登出主裝置。

[include/media/v4l2-device.h]
    // 註冊v4l2_device結構體,並初始化v4l2_device結構體
    // dev-父裝置結構體指標,若為NULL,在註冊之前裝置名稱name必須被設定,
    // v4l2_dev-v4l2_device結構體指標
    // 返回值-0成功,小於0-失敗
    int v4l2_device_register(struct device *dev, struct v4l2_device *v4l2_dev)

    // 解除安裝註冊的v4l2_device結構體
    // v4l2_dev-v4l2_device結構體指標
    void v4l2_device_unregister(struct v4l2_device *v4l2_dev)

    // 設定裝置名稱,填充v4l2_device結構體中的name成員
    // v4l2_dev-v4l2_device結構體指標
    // basename-裝置名稱基本字串
    // instance-裝置計數,呼叫v4l2_device_set_name後會自加1
    // 返回值-返回裝置計數自加1的值
    int v4l2_device_set_name(struct v4l2_device *v4l2_dev, 
            const char *basename, atomic_t *instance)

    // 熱插拔裝置斷開時呼叫此函數
    // v4l2_dev-v4l2_device結構體指標
    void v4l2_device_disconnect(struct v4l2_device *v4l2_dev);
同一個硬體的情況下。如ivtvfb驅動是一個使用ivtv硬體的幀緩衝驅動,同時alsa驅動也使用此硬體。可以使用如下例程遍歷所有註冊的裝置:
    static int callback(struct device *dev, void *p)
    {
        struct v4l2_device *v4l2_dev = dev_get_drvdata(dev);

        /* 測試這個裝置是否已經初始化 */
        if (v4l2_dev == NULL)
            return 0;
        ...
        return 0;
    }

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

        /* 在PCI 匯流排上查詢ivtv驅動。
        pci_bus_type是全域性的. 對於USB匯流排使用usb_bus_type。 */
        drv = driver_find("ivtv", &pci_bus_type);
        /* 遍歷所有的ivtv裝置範例 */
        err = driver_for_each_device(drv, NULL, p, callback);
        put_driver(drv);
        return err;
    }

2. video_device

V4L2子系統使用v4l2_device結構體管理裝置,裝置的具體操作方法根據裝置型別決定,

前面說過管理的裝置分為很多種,

若是視訊裝置,則需要註冊video_device結構體,並提供相應的操作方法。

對於視訊裝置Camera而言,Camera控制器可以視為主裝置,接在Camera控制器上的攝像頭可以視為從裝置

struct video_device
{
	const struct v4l2_file_operations *fops;
	struct cdev *cdev;     //vdev->cdev->ops = &v4l2_fops;  字元裝置描述符
	struct v4l2_device *v4l2_dev;
	struct v4l2_ctrl_handler *ctrl_handler;

	struct vb2_queue *queue;  //q->ops = &dmarx_vb2_ops; buf操作真正驅動回撥函數
  …………
	const struct v4l2_ioctl_ops *ioctl_ops;//vdev->ioctl_ops = &rkisp_dmarx_ioctl; 
  …………
};

註冊函數:

[rk_android11.0_sdk_220718\kernel\drivers\media\v4l2-core\v4l2-dev.c]


static inline int __must_check video_register_device(struct video_device *vdev,
        int type, int nr)
{
    return __video_register_device(vdev, type, nr, 1, vdev->fops->owner);
}
int __video_register_device(struct video_device *vdev, int type, int nr,
        int warn_if_nr_in_use, struct module *owner)
{
    ····
    int minor_cnt = VIDEO_NUM_DEVICES;//次裝置個數預設為256
    const char *name_base;

    /* A minor value of -1 marks this video device as never
       having been registered */
    vdev->minor = -1;

    /* the release callback MUST be present 如果之前沒有宣告銷燬函數,則報錯*/
    if (WARN_ON(!vdev->release))
        return -EINVAL;
    /* the v4l2_dev pointer MUST be present 如果之前未註冊v4l2_device則報錯*/
    if (WARN_ON(!vdev->v4l2_dev))
        return -EINVAL;

    /* Part 1: check device type */
    switch (type) {
    //根據裝置型別類註冊裝置,攝像頭裝置為VFL_TYPE_GRABBER型別
    case VFL_TYPE_GRABBER:
        name_base = "video";
    ·····
    ·····
    vdev->vfl_type = type;
    vdev->cdev = NULL;
    if (vdev->dev_parent == NULL)
        vdev->dev_parent = vdev->v4l2_dev->dev;
    if (vdev->ctrl_handler == NULL)
        //設定video_device的ctrl_handler,存在v4l2_device結構體中
        vdev->ctrl_handler = vdev->v4l2_dev->ctrl_handler;
    /* Part 2: find a free minor, device node number and device index. */
    /*2.尋找空閒次裝置號,裝置個數和裝置下標*/

    /* Pick a device node number 尋找一個空項位置*/
    mutex_lock(&videodev_lock);
    nr = devnode_find(vdev, nr == -1 ? 0 : nr, minor_cnt);
    //
    if (nr == minor_cnt)
        nr = devnode_find(vdev, 0, minor_cnt);
    if (nr == minor_cnt) {
        printk(KERN_ERR "could not get a free device node number\n");
        mutex_unlock(&videodev_lock);
        return -ENFILE;
    }
#ifdef CONFIG_VIDEO_FIXED_MINOR_RANGES
    /* 1-on-1 mapping of device node number to minor number */
    i = nr;
#else
    /* The device node number and minor numbers are independent, so
       we just find the first free minor number. */
    for (i = 0; i < VIDEO_NUM_DEVICES; i++)
        if (video_device[i] == NULL)
            break;
    if (i == VIDEO_NUM_DEVICES) {
        mutex_unlock(&videodev_lock);
        printk(KERN_ERR "could not get a free minor\n");
        return -ENFILE;
    }
#endif
    vdev->minor = i + minor_offset;
    vdev->num = nr;
    devnode_set(vdev);

    /* Should not happen since we thought this minor was free */

    vdev->index = get_index(vdev);
    video_device[vdev->minor] = vdev;

    if (vdev->ioctl_ops)
        determine_valid_ioctls(vdev);

    /* Part 3: Initialize the character device */
    vdev->cdev = cdev_alloc();
    if (vdev->cdev == NULL) {
        ret = -ENOMEM;
        goto cleanup;
    }
    vdev->cdev->ops = &v4l2_fops;//設定字元裝置的系統呼叫函數
    vdev->cdev->owner = owner;

    //註冊字元裝置
    ret = cdev_add(vdev->cdev, MKDEV(VIDEO_MAJOR, vdev->minor), 1);

    /* Part 4: register the device with sysfs */
    vdev->dev.class = &video_class;
    vdev->dev.devt = MKDEV(VIDEO_MAJOR, vdev->minor);
    vdev->dev.parent = vdev->dev_parent;
    //設定video結點名稱,如果裝置型別為VFL_TYPE_GRABBER,名稱為videoX
    dev_set_name(&vdev->dev, "%s%d", name_base, vdev->num);

    //註冊device檔案,生成裝置檔案/dev/videoX
    ret = device_register(&vdev->dev);
    /* Register the release callback that will be called when the last
       reference to the device goes away. */
    //設定銷燬video裝置的回撥函數
    vdev->dev.release = v4l2_device_release;

    /* Increase v4l2_device refcount */
    v4l2_device_get(vdev->v4l2_dev);

這個函數主要做四件事:

  1. 檢查裝置型別,賦予裝置名稱
  2. 尋找一個空閒的裝置位置,尋找合適的主裝置號和次設號
  3. 初始化字元裝置,使用v4l2_device的v4l2_fops初始化video_device的fops,release函數等
  4. 註冊字元裝置,並生成/dev/videoX結點,註冊subdev時也會呼叫這個介面

3. v4l2_subdev從裝置

V4L2從裝置使用struct v4l2_subdev結構體表示,該結構體用於對子裝置進行抽象。

幾乎所有的裝置都有多個 IC 模組

  • 它們可能是實體的(例如 USB 攝像頭裡麵包含 ISP、sensor 等)
  • 也可能是抽象的(如 USB 裝置裡面的抽象拓撲結構)
  • 它們在 /dev 目錄下面生成了多個裝置節點,並且這些 IC 模組還建立了一些非 v4l2 裝置:DVB、ALSA、FB、I2C 和輸入裝置。

通常情況下,這些IC模組通過一個或者多個 I2C 匯流排連線到主橋驅動上面,同時其它的匯流排仍然可用,這些 IC 就稱為 ‘sub-devices’

一個V4L2主裝置可能對應多個V4L2從裝置,所有主裝置對應的從裝置都掛到v4l2_device結構體的subdevs連結串列中。

對於視訊裝置,從裝置就是攝像頭,通常情況下是I2C裝置,主裝置可通過I2C匯流排控制從裝置

例如控制攝像頭的焦距、閃光燈等,同時使用 MIPI 或者 LVDS 等介面進行影象資料傳輸。

struct v4l2_subdev中包含的struct v4l2_subdev_ops是一個完備的操作函數集,用於對接各種不同的子裝置,比如video、audio、sensor等;

同時還有一個核心的函數集struct v4l2_subdev_core_ops,提供更通用的功能。
子裝置驅動根據裝置特點實現該函數集中的某些函數即可。

 [include/media/v4l2-subdev.h]
    #define V4L2_SUBDEV_FL_IS_I2C        (1U << 0)  // 從裝置是I2C裝置
    #define V4L2_SUBDEV_FL_IS_SPI        (1U << 1)  // 從裝置是SPI裝置
    #define V4L2_SUBDEV_FL_HAS_DEVNODE    (1U << 2)  // 從裝置需要裝置節點
    #define V4L2_SUBDEV_FL_HAS_EVENTS    (1U << 3)  // 從裝置會產生事件

    struct v4l2_subdev {
    #if defined(CONFIG_MEDIA_CONTROLLER)  // 多媒體設定選項
        struct media_entity entity;
    #endif
        struct list_head list;  // 子裝置串聯連結串列
        struct module *owner;  // 屬於那個模組,一般指向i2c_lient驅動模組
        bool owner_v4l2_dev;
        // 標誌位,確定該裝置屬於那種裝置,由V4L2_SUBDEV_FL_IS_XX宏確定
        u32 flags;
        // 指向主裝置的v4l2_device結構體
        struct v4l2_device *v4l2_dev;
        // v4l2子裝置的操作函數集合
        const struct v4l2_subdev_ops *ops;
        // 提供給v4l2框架的操作函數,只有v4l2框架會呼叫,驅動不使用
        const struct v4l2_subdev_internal_ops *internal_ops;
        // 從裝置的控制介面
        struct v4l2_ctrl_handler *ctrl_handler;
        // 從裝置的名稱,必須獨一無二
        char name[V4L2_SUBDEV_NAME_SIZE];
        // 從裝置組的ID,由驅動定義,相似的從裝置可以編為一組,
        u32 grp_id;
        // 從裝置私有資料指標,一般指向i2c_client的裝置結構體dev
        void *dev_priv;
        // 主裝置私有資料指標,一般指向v4l2_device嵌入的結構體
        void *host_priv;
        // 指向video裝置結構體
        struct video_device *devnode;
        // 指向物理裝置
        struct device *dev;
        // 將所有從裝置連線到全域性subdev_list連結串列或notifier->done連結串列
        struct list_head async_list;
        // 指向struct v4l2_async_subdev,用於非同步事件
        struct v4l2_async_subdev *asd;
        // 指向管理的notifier,用於主裝置和從裝置的非同步關聯
        struct v4l2_async_notifier *notifier;
        /* common part of subdevice platform data */
        struct v4l2_subdev_platform_data *pdata;
    };
    // 提供給v4l2框架的操作函數,只有v4l2框架會呼叫,驅動不使用
    struct v4l2_subdev_internal_ops {
        // v4l2_subdev註冊時回撥此函數,使v4l2_dev指向主裝置的v4l2_device結構體
        int (*registered)(struct v4l2_subdev *sd);
        // v4l2_subdev解除安裝時回撥此函數
        void (*unregistered)(struct v4l2_subdev *sd);
        // 應用呼叫open開啟從裝置節點時呼叫此函數
        int (*open)(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh);
        // 應用呼叫close時呼叫此函數
        int (*close)(struct v4l2_subdev *sd, struct v4l2_subdev_fh *fh);
    };

使用v4l2_subdev_init初始化v4l2_subdev結構體。然後必須用一個唯一的名字初始化subdev->name,同時初始化模組的owner域。

若從裝置是I2C裝置,則可使用v4l2_i2c_subdev_init函數進行初始化,該函數內部會呼叫v4l2_subdev_init,同時設定flags、owner、dev、name等成員。

 [include/media/v4l2-subdev.h]
    // 初始化v4l2_subdev結構體
    // ops-v4l2子裝置的操作函數集合指標,儲存到v4l2_subdev結構體的ops成員中
    void v4l2_subdev_init(struct v4l2_subdev *sd,
            const struct v4l2_subdev_ops *ops);

    [include/media/v4l2-common.h]
    // 初始化V4L2從裝置為I2C裝置的v4l2_subdev結構體
    // sd-v4l2_subdev結構體指標
    // client-i2c_client結構體指標
    // ops-v4l2子裝置的操作函數集合指標,儲存到v4l2_subdev結構體的ops成員中
    void v4l2_i2c_subdev_init(struct v4l2_subdev *sd, 
        struct i2c_client *client,
        const struct v4l2_subdev_ops *ops);

從裝置必須向V4L2子系統註冊v4l2_subdev結構體,使用v4l2_device_register_subdev註冊,使用v4l2_device_unregister_subdev登出。

[include/media/v4l2-device.h]
    // 向V4L2子系統註冊v4l2_subdev結構體
    // v4l2_dev-主裝置v4l2_device結構體指標
    // sd-從裝置v4l2_subdev結構體指標
    // 返回值 0-成功,小於0-失敗
    int v4l2_device_register_subdev(struct v4l2_device *v4l2_dev,
                    struct v4l2_subdev *sd)

    // 從V4L2子系統登出v4l2_subdev結構體
    // sd-從裝置v4l2_subdev結構體指標    
    void v4l2_device_unregister_subdev(struct v4l2_subdev *sd);

V4L2從裝置驅動都必須有一個v4l2_subdev結構體。
這個結構體可以單獨代表一個簡單的從裝置,也可以嵌入到一個更大的結構體中,與更多裝置狀態資訊儲存在一起。通常有一個下級裝置結構體(比如:i2c_client)包含了核心建立的裝置資料。

建議使用v4l2_set_subdevdata()將這個結構體的指標儲存在v4l2_subdev的私有資料域(dev_priv)中。可以更方便的通過v4l2_subdev找到實際的低層匯流排特定裝置資料。

對於常用的i2c_client結構體,i2c_set_clientdata函數可用於儲存一個v4l2_subdev指標,i2c_get_clientdata可以獲取一個v4l2_subdev指標;對於其他匯流排可能需要使用其他相關函數。

[include/media/v4l2-subdev.h]
    // 將i2c_client的指標儲存到v4l2_subdev結構體的dev_priv成員中
    static inline void v4l2_set_subdevdata(struct v4l2_subdev *sd, void *p)
    {
        sd->dev_priv = p;
    }

    [include/linux/i2c.h]
    // 可以將v4l2_subdev結構體指標儲存到i2c_client中dev成員的driver_data中
    static inline void i2c_set_clientdata(struct i2c_client *dev, void *data)
    {
        dev_set_drvdata(&dev->dev, data);
    }
    // 獲取i2c_client結構體中dev成員的driver_data,一般指向v4l2_subdev
    static inline void *i2c_get_clientdata(const struct i2c_client *dev)
    {
        return dev_get_drvdata(&dev->dev);
    }

主裝置驅動中也應儲存每個子裝置的私有資料,比如一個指向特定主裝置的各裝置私有資料的指標。為此v4l2_subdev結構體提供主裝置私有資料域(host_priv),並可通過v4l2_get_subdev_hostdata和 v4l2_set_subdev_hostdata存取。

 [include/media/v4l2-subdev.h]
    static inline void *v4l2_get_subdev_hostdata(const struct v4l2_subdev *sd)
    {
        return sd->host_priv;
    }
    static inline void v4l2_set_subdev_hostdata(struct v4l2_subdev *sd, void *p)
    {
        sd->host_priv = p;
    }

每個v4l2_subdev都包含子裝置驅動需要實現的函數指標(如果對此裝置不適用,可為NULL),具體在v4l2_subdev_ops結構體當中。

由於子裝置可完成許多不同的工作,而在一個龐大的函數指標結構體中通常僅有少數有用的函數實現其功能肯定不合適。

所以,函數指標根據其實現的功能被分類,每一類都有自己的函數指標結構體,如v4l2_subdev_core_ops、v4l2_subdev_audio_ops、v4l2_subdev_video_ops等等。

頂層函數指標結構體包含了指向各類函數指標結構體的指標,如果子裝置驅動不支援該類函數中的任何一個功能,則指向該類結構體的指標為NULL。

 [include/media/v4l2-subdev.h]
    /* v4l2從裝置的操作函數集合,從裝置根據自身裝置型別選擇實現,
       其中core函數集通常可用於所有子裝置,其他類別的實現依賴於
       子裝置。如視訊裝置可能不支援音訊操作函數,反之亦然。這樣的
       設定在限制了函數指標數量的同時,還使增加新的操作函數和分類
       變得較為容易。 */
    struct v4l2_subdev_ops {
        // 從裝置的通用操作函數集合,進行初始化、reset、控制等操作
        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;    // VBI裝置
        const struct v4l2_subdev_ir_ops        *ir;
        const struct v4l2_subdev_sensor_ops    *sensor;
        const struct v4l2_subdev_pad_ops    *pad;
    };
    // 適用於所有v4l2從裝置的操作函數集合
    struct v4l2_subdev_core_ops {
        // IO引腳複用設定
        int (*s_io_pin_config)(struct v4l2_subdev *sd, size_t n,
                        struct v4l2_subdev_io_pin_config *pincfg);
        // 初始化從裝置的某些暫存器,使其恢復預設
        int (*init)(struct v4l2_subdev *sd, u32 val);
        // 載入韌體
        int (*load_fw)(struct v4l2_subdev *sd);
        // 復位
        int (*reset)(struct v4l2_subdev *sd, u32 val);
        // 設定GPIO引腳輸出值
        int (*s_gpio)(struct v4l2_subdev *sd, u32 val);
        // 設定從裝置的電源狀態,0-省電模式,1-正常操作模式
        int (*s_power)(struct v4l2_subdev *sd, int on);
        // 中斷函數,被主裝置的中斷函數呼叫
        int (*interrupt_service_routine)(struct v4l2_subdev *sd,
                            u32 status, bool *handled);
        ......
    };

使用v4l2_device_register_subdev註冊從裝置後,就可以呼叫v4l2_subdev_ops中的方法了。

可以通過v4l2_subdev直接呼叫,也可以使用核心提供的宏定義v4l2_subdev_call間接呼叫某一個方法。

若要呼叫多個從裝置的同一個方法,則可使用v4l2_device_call_all宏定義。

// 直接呼叫
    err = sd->ops->video->g_std(sd, &norm);

    // 使用宏定義呼叫,這個宏將會做NULL指標檢查,如果su為NULL,則返回-ENODEV;
    // 如果sd->ops->video或sd->ops->video->g_std為NULL,則返回-ENOIOCTLCMD;
    // 否則將返回sd->ops->video->g_std的呼叫的實際結果
    err = v4l2_subdev_call(sd, video, g_std, &norm);

    [include/media/v4l2-subdev.h]
    #define v4l2_subdev_call(sd, o, f, args...)                \
    (!(sd) ? -ENODEV : (((sd)->ops->o && (sd)->ops->o->f) ?    \
        (sd)->ops->o->f((sd) , ##args) : -ENOIOCTLCMD))


    v4l2_device_call_all(v4l2_dev, 0, video, g_std, &norm);
  
[include/media/v4l2-device.h]
    #define v4l2_device_call_all(v4l2_dev, grpid, o, f, args...)        \
    do {                                \
        struct v4l2_subdev *__sd;                \
        __v4l2_device_call_subdevs_p(v4l2_dev, __sd,        \
            !(grpid) || __sd->grp_id == (grpid), o, f ,    \
            ##args);                    \
    } while (0)

如果子裝置需要通知它的v4l2_device主裝置一個事件,可以呼叫v4l2_subdev_notify(sd,notification, arg)

這個宏檢查是否有一個notify回撥被註冊,如果沒有,返回-ENODEV。否則返回 notify呼叫結果。notify回撥函數由主裝置提供。

[include/media/v4l2-device.h]
    // 從裝置通知主裝置,最終回撥到v4l2_device的notify函數
    static inline void v4l2_subdev_notify(struct v4l2_subdev *sd,
                        unsigned int notification, void *arg)
    {
        if (sd && sd->v4l2_dev && sd->v4l2_dev->notify)
            sd->v4l2_dev->notify(sd, notification, arg);
    }

使用v4l2_subdev的好處在於它是一個通用結構體,且不包含任何底層硬體資訊。

所有驅動可以包含多個I2C匯流排的從裝置,但也有從裝置是通過GPIO控制。這個區別僅在設定裝置時有關係,一旦子裝置註冊完成,對於v4l2子系統來說就完全透明瞭。

4. v4l2_fh

檔案存取控制

5. v4l2_ctrl_handler

控制模組,提供子裝置(主要是 video 和 ISP 裝置)在使用者空間的特效操作介面

6. media_device

用於執行時資料流的管理,嵌入在 V4L2 device 內部

五、 video_device、v4l2_device和v4l2_subdev的關係舉例

下面以我們手機的攝像頭來舉例:

  1. 假定一款CMOS攝像頭,有兩個介面:一個是攝像頭介面(資料),一個是I2C介面(控制命令)

攝像頭介面負責傳輸影象資料,I2C介面負責傳輸控制資訊,所以又可以將CMOS攝像頭看作是一個I2C模組

  1. 在一款SoC晶片上面,攝像頭相關的有攝像頭控制器、攝像頭介面、I2C匯流排
    SOC上可以有多個攝像頭控制器,多個攝像頭介面,多個I2C匯流排
    攝像頭控制器負責接收和處理攝像頭資料,攝像頭介面負責傳輸影象資料,I2C匯流排負責傳輸控制資訊

  2. 對於手機而言,一般都有兩個攝像頭:一個前置攝像頭,一個後置攝像頭

如下圖所示:

我們可以選擇讓控制器去操作哪一個攝像頭(可以使用某個gpio供電,通過電平來選擇攝像頭),這就做到了使用一個攝像頭控制器來控制多個攝像頭,這就是多路複用

我們回到V4L2來,再來談v4l2_device和v4l2_subdev:

  • v4l2_device表示一個v4l2範例,在V4L2驅動中,使用v4l2_device來表示攝像頭控制器
  • 使用v4l2_subdev來表示具體的某一個攝像頭的I2C控制模組,進而通過其控制攝像頭
  • v4l2_device裡有一個v4l2_subdev連結串列,可以選擇v4l2_device去控制哪一個v4l2_subdev
    subdev的設計目的是為了多路複用,就是用一個v4l2_device可以服務多個v4l2_subdev

然而某些驅動是沒有v4l2_subdev,只有video_device

我們用一張圖來總結裝置之間關係:

  1. video_device是一個字元裝置,video_device內含一個cdev
  2. v4l2_device是一個v4l2範例,嵌入到video_device中
  3. v4l2_device維護者一個連結串列管理v4l2_subdev,v4l2_subdev表示攝像頭的I2C控制模組
  4. 主裝置可通過v4l2_subdev_call的宏呼叫從裝置提供的方法,反過來從裝置可以呼叫主裝置的notify方法通知主裝置某些事件發生了。

核心層(core)負責註冊字元裝置,然後提供video_device物件和相應的註冊介面給硬體相關層使用;

硬體相關層需要分配一個video_device並設定它,然後向核心層註冊,核心層會為其註冊字元裝置並且建立裝置節點(/dev/videox);

同時硬體相關層還需要分配和設定相應的v4l2_device和v4l2_subdev,其中v4l2_device的一個比較重要的意義就是管理v4l2_subdev,當然有一些驅動並不需要實現v4l2_subdev,此時v4l2_device的意義就不是很大了;

當應用層通過/dev/video來操作裝置的時候,首先會來到V4L2的核心層,核心層通過註冊進的video_device的回撥函數呼叫相應的操作函數,video_device可以直接操作硬體或者是通過v4l2_subdev來操作硬體。

一口君再把各個結構體與各回撥函數之間關係彙總到下面這個圖裡(rk3568):

主要架構部分Linux核心已經實現了,Camera控制器驅動,廠家一般都會實現,對於一般驅動工程師來說,我們只需要實現子裝置驅動即可。

六、videobuf2

從資料流角度來分析,V4L2框架可以分成兩個部分看:控制流+資料流

  • 控制流主要由v4l2_subdev的回撥函數實現(一般由攝像頭廠商提供),主要用於控制攝像
  • 資料流的部分就是video buffer,驅動部分通常由SoC廠商提供(比如瑞芯微rk3568平臺,對應到rkisp_rawrd0_m、rkisp_rawrd2_s子模組)。

V4L2的buffer管理是通過videobuf2來完成的,它充當使用者空間和驅動之間的中間層,並提供low-level,模組化的記憶體管理功能;

獲取攝像頭視訊流的主要步驟如下:

要獲取影象資訊需要執行VIDIOC_DQBUF、VIDIOC_QBUF命令。

瑞芯微rk3568平臺videobuf2相關結構體和ops回撥函數關係如下:

  • 其中struct rkisp_device是瑞芯微3568平臺用於管理Camera控制器的最重要的結構體

  • struct rkisp_capture_device 對應拓撲結構中的模組rkisp_rawrd0_m 、rkisp_rawrd2_s 。

  • 該模組是一個video裝置,用於獲取原始影象資訊,所以在struct rkisp_vdev_node vnode中包含了struct vb2_queue buf_queue、struct video_device vdev

  • struct vb2_queue中的回撥函數struct vb2_mem_ops *mem_ops、struct vb2_buf_ops *buf_ops、struct vb2_ops *ops就是videobuf2驅動。

videobuf2驅動部分相關結構體如下:

上圖大體包含了videobuf2的框架;

  • vb2_queue:
    核心的資料結構,用於描述buffer的佇列,其中struct vb2_buffer *bufs[]是存放buffer節點的陣列,該陣列中的成員代表了vb2 buffer,並將在queued_list和done_list兩個佇列中進行流轉;
  • struct vb2_buf_ops:
    buffer的操作函數集,由驅動來實現,並由框架通過call_bufop宏來對特定的函數進行呼叫;
  • struct vb2_mem_ops:
    記憶體buffer分配函數介面,buffer型別分為三種:
    1)虛擬地址和實體地址都分散,可以通過dma-sg來完成;
    2)實體地址分散,虛擬地址連續,可以通過vmalloc分配;
    3)實體地址連續,可以通過dma-contig來完成;三種型別也vb2框架中都有實現,框架可以通過call_memop來進行呼叫;
  • struct vb2_ops:
    vb2佇列操作函數集,由驅動來實現對應的介面,並在框架中通過call_vb_qop宏被呼叫;

呼叫流程:

                     通用介面    ----------isp ioctrl介面----------           驅動                        
字元裝置->v4l2_ioctl->v4l_qbuf->vb2_ioctl_qbuf->vb2_qbuf->vb2_core_qbuf->rkisp_buf_queue
  • 下面是VIDIOC_DQBUF命令執行的 log【在函數vb2_core_dqbuf入口呼叫stack_dump()】:
/* */
[  105.813743] vb2_core_dqbuf+0x54/0x5b8
[  105.813753] vb2_dqbuf+0x94/0xc8
[  105.813763] vb2_ioctl_dqbuf+0x50/0x60

[  105.813774] v4l_dqbuf+0x44/0x58
[  105.813785] __video_do_ioctl+0x1a0/0x348
[  105.813795] video_usercopy+0x228/0x740
[  105.813805] video_ioctl2+0x14/0x20
[  105.813815] v4l2_ioctl+0x44/0x68
[  105.813825] v4l2_compat_ioctl32+0x1d0/0x3a48

[  105.813836] __arm64_compat_sys_ioctl+0xbc/0x15b0
[  105.813847] el0_svc_common.constprop.0+0x64/0x178
[  105.813859] el0_svc_compat_handler+0x18/0x20
[  105.813869] el0_svc_compat+0x8/0x34
  • VIDIOC_QBUF命令執行的log:
[  105.944858] vb2_core_qbuf+0x28/0x338
[  105.944883] vb2_qbuf+0x6c/0x90
[  105.944904] vb2_ioctl_qbuf+0x48/0x58
[  105.944928] v4l_qbuf+0x44/0x58
[  105.944951] __video_do_ioctl+0x1a0/0x348
[  105.944972] video_usercopy+0x228/0x740
[  105.944993] video_ioctl2+0x14/0x20
[  105.945013] v4l2_ioctl+0x44/0x68
[  105.945036] v4l2_compat_ioctl32+0x1d0/0x3a48
[  105.945058] __arm64_compat_sys_ioctl+0xbc/0x15b0
[  105.945082] el0_svc_common.constprop.0+0x64/0x178
[  105.945105] el0_svc_compat_handler+0x18/0x20
[  105.945125] el0_svc_compat+0x8/0x34

七、v4l2拓撲結構

關於如何使用裝置樹節點描述拓撲結構,後續文章會詳細講解。

文中各種mipi技術檔案,後臺回覆關鍵字:mipi

後面還會繼續更新幾篇Camera文章,

建議大家訂閱本專題!

也可以後臺留言,加一口君好友yikoupeng,

拉你進高質量技術交流群。