上一篇我們講解了如何編寫基于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架構包括哪些設備
-
Video設備又分為主設備和從設備對于Camera來說, 主設備: Camera Host控制器為主設備,負責圖像數據的接收和傳輸, 從設備: 從設備為Camera Sensor,一般為I2C接口,可通過從設備控制Camera采集圖像的行為,如圖像的大小、圖像的FPS等。
-
V4L2的主設備號是81,次設備號范圍0~255 這些次設備號又分為多類設備:
- 視頻設備(次設備號范圍0-63)
- Radio(收音機)設備(次設備號范圍64-127)
- Teletext設備(次設備號范圍192-223)
- VBI設備(次設備號范圍224-255)。
- 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);
這個函數主要做四件事:
- 檢查設備類型,賦予設備名稱
- 尋找一個空閑的設備位置,尋找合適的主設備號和次設號
- 初始化字符設備,使用v4l2_device的v4l2_fops初始化video_device的fops,release函數等
- 注冊字符設備,并生成/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的關系舉例
下面以我們手機的攝像頭來舉例:
- 假定一款CMOS攝像頭,有兩個接口:一個是攝像頭接口(數據),一個是I2C接口(控制命令)
攝像頭接口負責傳輸圖像數據,I2C接口負責傳輸控制信息,所以又可以將CMOS攝像頭看作是一個I2C模塊
-
在一款SoC芯片上面,攝像頭相關的有攝像頭控制器、攝像頭接口、I2C總線 SOC上可以有多個攝像頭控制器,多個攝像頭接口,多個I2C總線 攝像頭控制器負責接收和處理攝像頭數據,攝像頭接口負責傳輸圖像數據,I2C總線負責傳輸控制信息
-
對于手機而言,一般都有兩個攝像頭:一個前置攝像頭,一個后置攝像頭
如下圖所示:
我們可以選擇讓控制器去操作哪一個攝像頭(可以使用某個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
我們用一張圖來總結設備之間關系:
- video_device是一個字符設備,video_device內含一個cdev
- v4l2_device是一個v4l2實例,嵌入到video_device中
- v4l2_device維護者一個鏈表管理v4l2_subdev,v4l2_subdev表示攝像頭的I2C控制模塊
- 主設備可通過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,
拉你進高質量技術交流群。