深入理解l内核v4l2框架之video for linux 2(一)
在看了很多關于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;
/*do something*/
return 0;
}
int iterate(void *p)
{
struct device_driver *drv;
int err = 0;
/*Find driver 'vivi' on the PCI bus*/
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_device
struct media_entity entity;
#endif
struct 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);
[html]?view plain?copypads數組必須在之前就已經初始化好。這里不需要進行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文件。
總結
以上是生活随笔為你收集整理的深入理解l内核v4l2框架之video for linux 2(一)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: V4L2 driver(一). 整体框架
- 下一篇: Linux 设备模型之 (kobject