在看了很多關于v4l2驅動的例程之後,想深入研究下linux核心的v4l2架構,順便把這些記錄下來,以備查用。
Video for Linux 2
随着一些視訊或者圖像硬體的複雜化,V4L2驅動也越來越趨于複雜。許多硬體有多個IC,在/dev下生成多個video裝置或者其他的諸如,DVB,ALSA,FB,I2C ,IR等等非V4L2的裝置。是以,V4L2驅動程式就要為這些硬體裝置提供音視訊的合成以及編解碼的功能接口,另外,通常這些裝置都通過多個I2C總線實作和CPU的通訊,不僅是I2C總線,其他的也有可能被使用,比如SPI,1-wire,等等。挂在這些總線上的裝置叫做sub-devices,即V4L2裝置的子裝置。
之前相當長的一段時間内,V4L2被限制在使用video_device來建立V4L2裝置節點,使用videobuf來處理視訊緩存。這就意味着,所有的驅動驅動程式除了建立一個裝置執行個體外,還要單獨實作連接配接”子裝置”的步驟。這個過程比較複雜,也容易産生錯誤。正是缺少這樣一種架構,使得在代碼重用方面做得不夠好,驅動程式看起來很臃腫。
是以V4L2架構才被整理出來,提供一些基礎的元件,通過一些共享的功能函數簡化驅動的編寫,使代碼的重用性增強,硬體裝置驅動隻需要實作相關的操作而不必關心互動式的應用,同時應用可以更加透明地使用硬體來驅動音視訊的處理。而且這個架構也在不斷地更新擴充,基礎部分就是提供的v4l2API。但這裡不讨論V4L2提供了哪些API和他們如何被使用,我們隻讨論和v4l2核心及驅動相關的知識。
首先來看看所有的v4l2驅動都必須要有的幾個組成部分:
– 用來描述每一個v4l2裝置執行個體狀态的結構(structv4l2_device)。
– 用來初始化和控制子裝置的方法(structv4l2_subdev)。
– 要能建立裝置節點并且能夠對該節點所持有的資料進行跟蹤(structvideo_device)。
– 為每一個被打開的節點維護一個檔案句柄(structv4l2_fh)。
– 視訊緩沖區的處理(videobuf或者videobuf2 framework)。
在linux3.0以上的核心對這些結構的定義,從定義當中就可以窺探整個v4l2的架構。這些結構體有:
struct v4l2_device; 用來描述一個v4l2裝置執行個體
struct v4l2_subdev, 用來描述一個v4l2的子裝置執行個體
struct video_device; 用來建立裝置節點/dev/videoX
struct v4l2_fh; 用來跟蹤檔案句柄執行個體
我們把videobuf及videobuf2架構放到後面的系列來讨論。
用一個比較粗糙的圖來表現他們之間的關系,大緻為:
裝置執行個體(v4l2_device)
|______子裝置執行個體(v4l2_subdev)
|______視訊裝置節點(video_device)
|______檔案通路控制(v4l2_fh)
|______視訊緩沖的處理(videobuf/videobuf2)
好了,接下來我們一一分析一下這些結構的定義。
1、v4l2_device
這個定義在linux/media/v4l2-device.h當中定義
struct v4l2_device {
//指向裝置模型的指針struct device *dev;#if defined(CONFIG_MEDIA_CONTROLLER)//指向一個媒體控制器的指針struct media_device *mdev;#endif//管理子裝置的雙向連結清單,所有注冊到的子裝置都需要加入到這個連結清單當中struct list_head subdevs;//全局鎖spinlock_t lock;//裝置名稱char name[V4L2_DEVICE_NAME_SIZE];//通知回調函數,通常用于子裝置傳遞事件,這些事件可以是自定義事件void (*notify)(struct v4l2_subdev*sd, uint notification, void *arg);//控制句柄struct v4l2_ctrl_handler*ctrl_handler;//裝置的優先級狀态,一般有背景,互動,記錄三種優先級,依次變高struct v4l2_prio_state prio;//ioctl操作的互斥量struct mutex ioctl_lock;//本結構體的引用追蹤struct kref ref;//裝置釋放函數void (*release)(struct v4l2_device*v4l2_dev);
};
要注冊一個執行個體,需要使用函數
v4l2_device_register(struct device*dev, struct v4l2_device *v4l2_dev);
該函數将會初始化v4l2_device結構,如果dev->driver_data為空,那麼将把v4l2_dev指派給這個driver_data。
如果驅動要內建媒體裝置架構,就需要手動設定dev->driver_data來指向一個嵌入了v4l2_device結構的媒體裝置結構,這個結構可以是指定驅動的狀态描述。在注冊函數之前調用dev_set_drvdata來完成,這些都要在調用register函數之前就要設定好,同樣,如果有媒體裝置的話,必須也要在此之前就要初始化好,并且設定v4l2_device的mdev域來指向一個已經初始化過的媒體裝置執行個體。
如果v4l2_dev->name是空,注冊函數也将根據dev->name來設定v4l2_dev的name,如果已經設定,那麼注冊函數将不再過問。如果dev是空,那麼就必須在調用注冊函數之前設定v4l2_dev->name。當然,也可以使用v4l2_device_set_name()來設定裝置執行個體名稱。
要移除注冊的話,調用函數:
v4l2_device_unregister(structv4l2_device *vd)
如果是可熱插拔的裝置,那麼還需要調用
v4l2_device_disconnect(structv4l2_device *vd)
來斷開裝置的連接配接,否則會産生空指針的問題。
有的時候需要疊代驅動注冊的所有裝置,這個通常出現在多個裝置驅動使用相同的硬體的情況,比如說ivtvfb是一個framebuffer驅動,它使用ivtv硬體,同時也是一個tv驅動。相同的情況對于alsa驅動也是适用的。那麼,疊代該怎麼完成呢?看下面這個例程:
static int callback(struce device *dev,void *p)
{
struct v4l2_device *vdev =dev_get_drvdata(dev);if (vdev == NULL) return 0;return 0;
}
int iterate(void *p)
{
struct device_driver *drv;int err = 0;drv = driver_find(“vivi”,&pci_bus_type);err = driver_for_each_device(drv,NULL, p, callback);put_driver(drv);return err;
}
有時候還需要維護一個運作時的裝置執行個體計數,定義一個原子變量就可以了。如果一個v4l2裝置上注冊了很多裝置節點,我們在移除注冊v4l2_device的時候,就要等到所有的裝置節點移除之後,ref成員幫助我們記錄v4l2_device的節點注冊數,每次調用video_register_device都會加1,反之則減一。一旦這個值為0的時候,我們才可以調用v4l2_device_unregister。如果不是視訊節點,那麼手動調用這兩個函數來計數:
void v4l2_device_get(struct v4l2_device*vd) //ref +1
int v4l2_device_put(struct v4l2_device*vd) // ref -1
2、v4l2_subdev
struct v4l2_subdev {
#if defined(CONFIG_MEDIA_CONTROLLER)//媒體控制器的實體,和v4l2_devicestruct media_entity entity;#endifstruct list_head list;struct module *owner;u32 flags;//指向一個v4l2裝置struct v4l2_device *v4l2_dev;//子裝置的操作函數集const struct v4l2_subdev_ops *ops;//子裝置的内部操作函數集const struct v4l2_subdev_internal_ops*internal_ops;//控制函數處理器struct v4l2_ctrl_handler*ctrl_handler;//子裝置的名稱char name[V4L2_SUBDEV_NAME_SIZE];//子裝置所在的組辨別u32 grp_id;//子裝置私有資料指針,一般指向總線接口的用戶端void *dev_priv;//子裝置私有的資料指針,一般指向總線接口的host端void *host_priv;//裝置節點struct video_device devnode;//子裝置的事件unsigned int nevents;
};
很多v4l2 驅動程式都需要和子裝置(sub_device) 來進行通訊,這些裝置實際上完成了所有的任務,比如說音視訊的合成,編碼,解碼。對于webcam 來說,子裝置就是sensor 和camera 控制器。通常這些都是I2C 裝置,但也不是必須的。為了給這些子裝置提供一個一緻的接口,v4l2_subdev 結構才應運而生。
每一個子裝置都必須有一個v4l2_subdev結構。這個結構可以單獨地使用或者被嵌入一個更大的結構。通常有一個更低級的裝置結構(比如i2c_client),它包含裝置的一些初始化資料,是以建議v4l2_subdev->dev_priv指向該資料,可以通過函數:
v4l2_set_subdevdata()
v4l2_get_subdevdata()
來設定,然後調用v4l2_get_subdevdata() 這樣就會很友善的從v4l2_subdev 找到實際的總線相關的裝置資料。總之是一些私有的資料,可以是平台相關的資料,可以是自己定義的包含了v4l2_subdev 結構的裝置執行個體等等。
同樣也需要一個從總線相關的裝置友善的找到v4l2_subdev,可以這樣來實作例如:i2c_set_clientdata().調用這個函數來把v4l2_subdev結構指針賦給i2c_client的private資料。然後調用i2c_get_clientdata()獲得v4l2_subdev的指針。當然也可以通過container_of來操作,但是核心既然提供了這樣的api,用之何樂不為呢?
每一個v4l2_subdev包含了子裝置可以實作的函數指針。這些函數可以做很多很多不同的事情,它們根據不同的操作類别被放在不同的結構當中。最高一級的操作函數集涵蓋了各種操作大類。比如:
struct v4l2_subdev_core_ops {int (*g_chip_ident)(struct v4l2_subdev*, struct v4l2_dbg_chip_ident *);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_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;
};
core操作對于所有子裝置來說是共通的,其他的類型可以根據子裝置的需要來實作,比如一個視訊子裝置不太可能實作音頻的操作等等。
至此我們介紹了v4l2_subdev的一些成員及操作函數,那麼下面就可以進行初始化了,初始化函數調用:
v4l2_subdev_init(sd, &ops);
之後就需要初始化子裝置的名字和owner等等。如果需要內建媒體架構,那麼我們就必須初始化這個media_entity結構,并将其嵌入到v4l2_subdev的結構當中,這個通過調用media_entity_init()來實作。
struct media_pad *pads = &my_sd->pads;
media_entity_init(&sd->entity,npads, pads, 0);
pads數組必須在之前就已經初始化好。這裡不需要進行media_entity的類型和名字的手動初始化,但是revision域如果需要的話就必須要初始化。Entity的引用參數會自動在子裝置節點被打開和關閉的時候進行加減。在子裝置被銷毀之前不要忘了cleanup這個mediaentity.調用media_entity_cleanup(&sd->entity)。關于media_entity的相關知識這裡不做讨論。下面繼續讨論v4l2_subdev的注冊。
注冊v4l2_subdev子裝置執行個體到v4l2_device裝置系統當中,用這個函數:
v4l2_device_unregister_subdev(sd)
這個函數執行成功之後,subdev->dev将指向v4l2_device,如果v4l2_device的mdev是一個非空的值,那麼subdev->entity也将會被自動注冊為mdev。要移除注冊的子裝置,調用:
v4l2_device_unregister_subdev(sd)
接下來介紹子裝置提供的功能調用,如果要使用子裝置提供的接口函數,有兩種方法,第一種就是直接使用ops中的回調函數,但是不推薦這樣做,一般是用第二種方法,調用函數:
v4l2_subdev_call(sd, o, f, arg...)
來擷取子裝置晶片的辨別。其中,sd就是子裝置執行個體,o是子裝置下操作函數的大類,例如可以是core/video/audio/tuner,f是大類下面的功能回調函數,arg是傳入的參數。另外,還可以通過v4l2裝置執行個體調用全部子裝置的功能回調函數,使用這個函數:
v4l2_device_call_all(v4l2, grp_id, o,f, arg...)
其中grp_id就是子裝置的組辨別。舉個例子:
v4l2_subdev_call(sd, video,g_chip_cap, &cap);
v4l2_device_call_all(v4l2, 0, core,g_chip_id, &cap);
前者是調用子裝置sd的video類下的g_chip_cap功能回調函數;後者是v4l2裝置調用所有子裝置的core類下的g_chip_id功能回調函數。grp_id非0則指定調用相同組辨別的該方法。子裝置還需要通知它的v4l2父裝置發生了什麼事件,這個通過調用下面這個函數實作。
v4l2_subdev_notify(sd, notification,arg)
但是父裝置必須要有能夠處理這些事件的能力,就是實作v4l2_device的notify功能。
除了通過v4l2_subdev_ops結構暴露給核心的API之外,v4l2子裝置也同樣可以被使用者程式直接控制。裝置節點名為v4l-subdevX建立在/dev目錄下,這樣就可以通過打開裝置檔案來直接通路子裝置。如果一個子裝置支援直接的使用者空間通路,那麼它就必須在被注冊之前就設定V4L2_SUBDEV_FL_HAS_DEVNODE标志。注冊子裝置之後,v4l2_device驅動就會為所有持此标志的子裝置建立裝置節點。這個通過v4l2_device_register_subdev_nodes()來實作。這個裝置節點可以處理一組标準的V4l2API子集,如下:
VIDIOC_QUERYCTRL
VIDIOC_QUERYMENU
VIDIOC_G_CTRL
VIDIOC_S_CTRL
VIDIOC_G_EXT_CTRLS
VIDIOC_S_EXT_CTRLS
VIDIOC_TRY_EXT_CTRLS
所有上面這些控制調用都可以通過core:ioctl操作來完成。至此,關于v4l2子裝置相關的知識就介紹完畢。
備注:
核心為我們提供了很多幫助函數,比如v4l2_i2c子裝置驅動架構,使編寫此類驅動變得容易很多。因為此類驅動有很多共通之處,是以可以将其抽象出來以便易于使用。這個抽象在v4l2_common.h當中。
給一個I2C驅動添加v4l2_subdev支援的推薦方法是将v4l2_subdev結構嵌入I2C裝置執行個體的state結構當中。非常簡單的裝置沒有這個結構,那麼就直接建立一個v4l2_subdev執行個體就好了。一個典型的state結構就像這樣:
struct chipname_state {
struct v4l2_subdev sd;…..
};
初始化一個v4l2_subdev并且将其和i2c總線裝置連接配接起來
v4l2_i2c_subdev_init(&state->sd,client, subdev_ops);
這個函數将填充所有v4l2_subdev結構的域,并且保證v4l2_subdev和i2c_client能夠互相找到。最好是能夠實作一個state和subdev互訪的inline函數:
static inline struct chipname_state*to_state(struct v4l2_subdev *sd)
{
return container_of(sd, structchipname_state, sd);
}
然後通過如下函數實作v4l2_subdev和i2c_client的互訪:
struct i2c_client *client =v4l2_get_subdevdata(sd);
struct v4l2_subdev *sd =i2c_get_clientdata(client);
要確定在subdev的驅動被remove的時候調用如下函數:
v4l2_device_unregister_subdev(sd);
另外還有一些幫助函數可以使用:
struct v4l2_subdev *sd =v4l2_i2c_new_subdev(v4l2_dev, adapter, “module_foo”,“chipid”, 0x36, NULL);
這個函數會load一個i2c的adapter然後調用i2d_new_device并且根據chipid和i2c的位址(0x36)來建立一個新的i2c裝置,之後會将子產品名為module_foo的subdev注冊到v4l2_dev裡面去。關于其他的幫助函數,請查閱v4l2-common.h檔案。
未完待續。。。