和你一起終身學習,這里是程序員 Android
經典好文推薦,通過閱讀本文,您將收獲以下知識點:
一、概覽
二、流程簡介
三、關鍵結構體
四、模塊初始化
五、處理用戶空間請求
相機驅動層–V4L2框架解析
一、概覽
相機驅動層位于HAL Moudle與硬件層之間,借助linux內核驅動框架,以文件節點的方式暴露接口給用戶空間,讓HAL Module通過標準的文件訪問接口,從而能夠將請求順利地下發到內核中,而在內核中,為了更好的支持視頻流的操作,早先提出了v4l視頻處理框架,但是由于操作復雜,并且代碼無法進行較好的重構,難以維護等原因,之后便衍生出了v4l2框架。
按照v4l2標準,它將一個數據流設備抽象成一個videoX節點,從屬的子設備都對應著各自的v4l2_subdev實現,并且通過media controller進行統一管理,整個流程復雜但高效,同時代碼的擴展性也較高。
而對高通平臺而言,高通整個內核相機驅動是建立在v4l2框架上的,并且對其進行了相應的擴展,創建了一個整體相機控制者的CRM,它以節點video0暴露給用戶空間,主要用于管理內核中的Session、Request以及與子設備,同時各個子模塊都實現了各自的v4l2_subdev設備,并且以v4l2_subdev節點暴露給用戶空間,與此同時,高通還創建了另一個video1設備Camera SYNC,該設備主要用于同步數據流,保證用戶空間和內核空間的buffer能夠高效得進行傳遞。
再往下與相機驅動交互的便是整個相機框架的最底層Camera Hardware了,驅動部分控制著其上下電邏輯以及寄存器讀取時序并按照I2C協議進行與硬件的通信,和根據MIPI CSI協議傳遞數據,從而達到控制各個硬件設備,并且獲取圖像數據的目的。
V4L2英文是Video for Linux 2,該框架是誕生于Linux系統,用于提供一個標準的視頻控制框架,其中一般默認會嵌入media controller框架中進行統一管理,v4l2提供給用戶空間操作節點,media controller控制對于每一個設備的枚舉控制能力,于此同時,由于v4l2包含了一定數量的子設備,而這一系列的子設備都是處于平級關系,但是在實際的圖像采集過程中,子設備之間往往還存在著包含于被包含的關系,所以為了維護并管理這種關系,media controller針對多個子設備建立了的一個拓撲圖,數據流也就按照這個拓撲圖進行流轉。
二、流程簡介
整個對于v4l2的操作主要包含了如下幾個主要流程:
a) 打開video設備
在需要進行視頻數據流的操作之前,首先要通過標準的字符設備操作接口open方法來打開一個video設備,并且將返回的字符句柄存在本地,之后的一系列操作都是基于該句柄,而在打開的過程中,會去給每一個子設備的上電,并完成各自的一系列初始化操作。
b) 查看并設置設備
在打開設備獲取其文件句柄之后,就需要查詢設備的屬性,該動作主要通過ioctl傳入VIDIOC_QUERYCAP參數來完成,其中該系列屬性通過v4l2_capability結構體來表達,除此之外,還可以通過傳入VIDIOC_ENUM_FMT來枚舉支持的數據格式,通過傳入VIDIOC_G_FMT/VIDIOC_S_FMT來分別獲取和獲取當前的數據格式,通過傳入VIDIOC_G_PARM/VIDIOC_S_PARM來分別獲取和設置參數。
c) 申請幀緩沖區
完成設備的配置之后,便可以開始向設備申請多個用于盛裝圖像數據的幀緩沖區,該動作通過調用ioctl并且傳入VIDIOC_REQBUFS命令來完成,最后將緩沖區通過mmap方式映射到用戶空間。
d) 將幀緩沖區入隊
申請好幀緩沖區之后,通過調用ioctl方法傳入VIDIOC_QBUF命令來將幀緩沖區加入到v4l2 框架中的緩沖區隊列中,靜等硬件模塊將圖像數據填充到緩沖區中。
e) 開啟數據流
將所有的緩沖區都加入隊列中之后便可以調用ioctl并且傳入VIDIOC_STREAMON命令,來通知整個框架開始進行數據傳輸,其中大致包括了通知各個子設備開始進行工作,最終將數據填充到V4L2框架中的緩沖區隊列中。
f) 將幀緩沖區出隊
一旦數據流開始進行流轉了,我們就可以通過調用ioctl下發VIDIOC_DQBUF命令來獲取幀緩沖區,并且將緩沖區的圖像數據取出,進行預覽、拍照或者錄像的處理,處理完成之后,需要將此次緩沖區再次放入V4L2框架中的隊列中等待下次的圖像數據的填充。
整個采集圖像數據的流程現在看來還是比較簡單的,接口的控制邏輯很清晰,主要原因是為了提供給用戶的接口簡單而且抽象,這樣方便用戶進行集成開發,其中的大部分復雜的業務處理都被V4L2很好的封裝了,接下來我們來詳細了解下V4L2框架內部是如何表達以及如何運轉的。
三、關鍵結構體
從上圖不難看出,v4l2_device作為頂層管理者,一方面通過嵌入到一個video_device中,暴露video設備節點給用戶空間進行控制,另一方面,video_device內部會創建一個media_entity作為在media controller中的抽象體,被加入到media_device中的entitie鏈表中,此外,為了保持對所從屬子設備的控制,內部還維護了一個掛載了所有子設備的subdevs鏈表。
而對于其中每一個子設備而言,統一采用了v4l2_subdev結構體來進行描述,一方面通過嵌入到video_device,暴露v4l2_subdev子設備節點給用戶空間進行控制,另一方面其內部也維護著在media controller中的對應的一個media_entity抽象體,而該抽象體也會鏈入到media_device中的entities鏈表中。
通過加入entities鏈表的方式,media_device保持了對所有的設備信息的查詢和控制的能力,而該能力會通過media controller框架在用戶空間創建meida設備節點,將這種能力暴露給用戶進行控制。
由此可見,V4L2框架都是圍繞著以上幾個主要結構體來進行的,接下來我們依次簡單介紹下:
v4l2_device 源碼如下:
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,
unsigned int notification, void *arg);
struct v4l2_ctrl_handler *ctrl_handler;
struct v4l2_prio_state prio;
struct kref ref;
void (*release)(struct v4l2_device *v4l2_dev);
};
該結構體代表了一個整個V4L2設備,作為整個V4L2的頂層管理者,內部通過一個鏈表管理著整個從屬的所有的子設備,并且如果將整個框架放入media conntroller進行管理,便在初始化的時候需要將創建成功的media_device賦值給內部變量 mdev,這樣便建立了于與media_device的聯系,驅動通過調用v4l2_device_register方法和v4l2_device_unregister方法分別向系統注冊和釋放一個v4l2_device。
v4l2_subdev源碼如下:
struct v4l2_subdev {
#if defined(CONFIG_MEDIA_CONTROLLER)
struct media_entity entity;
#endif
struct list_head list;
struct module *owner;
bool owner_v4l2_dev;
u32 flags;
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;
void *host_priv;
struct video_device *devnode;
struct device *dev;
struct fwnode_handle *fwnode;
struct list_head async_list;
struct v4l2_async_subdev *asd;
struct v4l2_async_notifier *notifier;
struct v4l2_subdev_platform_data *pdata;
};
該結構體代表了一個子設備,每一個子設備都需要在初始化的時候掛載到一個總的v4l2_device上,并且將該v4l2設備賦值給內部的v4l2_dev變量,之后將自身加入到v4l2_device中的子設備鏈表中進行統一管理,這種方式提高了遍歷訪問所有子設備的效率,同時為了表達不同硬件模塊的特殊操作行為,v4l2_subdev定義了一個v4l2_subdev_ops 結構體來進行定義,其實現交由不同的硬件模塊來具體完成。其中如果使能了CONFIG_MEDIA_CONTROLLER宏,便會在media_controller中生成一個對應的media_entity,來代表該子設備,而該entity便會存入子設備結構體中的entity變量中,最后,如果需要創建一個設備節點的話,通過video_device調用標準API接口進行實現,而相應的video_device便會存入其內部devnode變量中。
video_device源碼如下:
struct video_device
{
#if defined(CONFIG_MEDIA_CONTROLLER)
struct media_entity entity;
struct media_intf_devnode *intf_devnode;
struct media_pipeline pipe;
#endif
const struct v4l2_file_operations *fops;
u32 device_caps;
/* sysfs */
struct device dev;
struct cdev *cdev;
struct v4l2_device *v4l2_dev;
struct device *dev_parent;
struct v4l2_ctrl_handler *ctrl_handler;
struct vb2_queue *queue;
struct v4l2_prio_state *prio;
/* device info */
char name[32];
int vfl_type;
int vfl_dir;
int minor;
u16 num;
unsigned long flags;
int index;
/* V4L2 file handles */
spinlock_t fh_lock;
struct list_head fh_list;
int dev_debug;
v4l2_std_id tvnorms;
/* callbacks */
void (*release)(struct video_device *vdev);
const struct v4l2_ioctl_ops *ioctl_ops;
DECLARE_BITMAP(valid_ioctls, BASE_VIDIOC_PRIVATE);
DECLARE_BITMAP(disable_locking, BASE_VIDIOC_PRIVATE);
struct mutex *lock;
};
如果需要給v4l2_device或者v4l2_subdev在系統中創建節點的話,便需要實現該結構體,并且通過video_register_device方法進行創建,而其中的fops便是video_device所對應的操作方法集,在v4l2框架內部,會將video_device嵌入到一個具有特定主設備號的字符設備中,而其方法集會在操作節點時被調用到。除了這些標準的操作集外,還定義了一系列的ioctl操作集,通過內部ioctl_ops來描述。
media_device源碼如下:
struct media_device {
/* dev->driver_data points to this struct. */
struct device *dev;
struct media_devnode *devnode;
char model[32];
char driver_name[32];
char serial[40];
char bus_info[32];
u32 hw_revision;
u64 topology_version;
u32 id;
struct ida entity_internal_idx;
int entity_internal_idx_max;
struct list_head entities;
struct list_head interfaces;
struct list_head pads;
struct list_head links;
/* notify callback list invoked when a new entity is registered */
struct list_head entity_notify;
/* Serializes graph operations. */
struct mutex graph_mutex;
struct media_graph pm_count_walk;
void *source_priv;
int (*enable_source)(struct media_entity *entity,
struct media_pipeline *pipe);
void (*disable_source)(struct media_entity *entity);
const struct media_device_ops *ops;
};
如果使能了CONFIG_MEDIA_CONTROLLER宏,則當v4l2_device初始化的過程中便會去創建一個media_device,而這個media_device便是整個media controller的抽象管理者,每一個v4l2設備以及從屬的子設備都會對應的各自的entity,并且將其存入media_device中進行統一管理,與其它抽象設備一樣,media_device也具有自身的行為,比如用戶可以通過訪問media節點,枚舉出所有的從屬于同一個v4l2_device的子設備,另外,在開啟數據流的時候,media_device通過將各個media_entity按照一定的順序連接起來,實現了數據流向的整體控制。
vb2_queue源碼如下:
struct vb2_queue {
unsigned int type;
unsigned int io_modes;
struct device *dev;
unsigned long dma_attrs;
unsigned bidirectional:1;
unsigned fileio_read_once:1;
unsigned fileio_write_immediately:1;
unsigned allow_zero_bytesused:1;
unsigned quirk_poll_must_check_waiting_for_buffers:1;
struct mutex *lock;
void *owner;
const struct vb2_ops *ops;
const struct vb2_mem_ops *mem_ops;
const struct vb2_buf_ops *buf_ops;
void *drv_priv;
unsigned int buf_struct_size;
u32 timestamp_flags;
gfp_t gfp_flags;
u32 min_buffers_needed;
/* private: internal use only */
struct mutex mmap_lock;
unsigned int memory;
enum dma_data_direction dma_dir;
struct vb2_buffer *bufs[VB2_MAX_FRAME];
unsigned int num_buffers;
struct list_head queued_list;
unsigned int queued_count;
atomic_t owned_by_drv_count;
struct list_head done_list;
spinlock_t done_lock;
wait_queue_head_t done_wq;
struct device *alloc_devs[VB2_MAX_PLANES];
unsigned int streaming:1;
unsigned int start_streaming_called:1;
unsigned int error:1;
unsigned int waiting_for_buffers:1;
unsigned int is_multiplanar:1;
unsigned int is_output:1;
unsigned int copy_timestamp:1;
unsigned int last_buffer_dequeued:1;
struct vb2_fileio_data *fileio;
struct vb2_threadio_data *threadio;
#ifdef CONFIG_VIDEO_ADV_DEBUG
/*
* Counters for how often these queue-related ops are
* called. Used to check for unbalanced ops.
*/
u32 cnt_queue_setup;
u32 cnt_wait_prepare;
u32 cnt_wait_finish;
u32 cnt_start_streaming;
u32 cnt_stop_streaming;
#endif
};
在整個V4L2框架運轉過程中,最為核心的是圖像數據緩沖區的管理,而這個管理工作便是由vb2_queue來完成的,vb2_queue通常在打開設備的時候被創建,其結構體中的vb2_ops可以由驅動自己進行實現,而vb2_mem_ops代表了內存分配的方法集,另外,還有一個用于將管理用戶空間和內核空間的相互傳遞的方法集buf_ops,而該方法集一般都定義為v4l2_buf_ops這一標準方法集。除了這些方法集外,vb2_queue還通過一個vb2_buffer的數組來管理申請的所有數據緩沖區,并且通過queued_list來管理入隊狀態的所有buffer,通過done_list來管理被填充了數據等待消費的所有buffer。
vb2_buffer源碼如下:
struct vb2_buffer {
struct vb2_queue *vb2_queue;
unsigned int index;
unsigned int type;
unsigned int memory;
unsigned int num_planes;
struct vb2_plane planes[VB2_MAX_PLANES];
u64 timestamp;
/* private: internal use only
*
* state: current buffer state; do not change
* queued_entry: entry on the queued buffers list, which holds
* all buffers queued from userspace
* done_entry: entry on the list that stores all buffers ready
* to be dequeued to userspace
*/
enum vb2_buffer_state state;
struct list_head queued_entry;
struct list_head done_entry;
};
該結構體代表了V4L2框架中的圖像緩沖區,當處于入隊狀態時內部queued_entry會被鏈接到vb2_queue中的queued_list中,當處于等待消費的狀態時其內部done_entry會被鏈接到vb2_queue 中的done_list中,而其中的vb2_queue便是該緩沖區的管理者。
以上便是V4L2框架的幾個核心結構體,從上面的簡單分析不難看出,v4l2_device作為一個相機內核體系的頂層管理者,內部使用一個鏈表控制著所有從屬子設備v4l2_subdev,使用vb2_queue來申請并管理所有數據緩沖區,并且通過video_device向用戶空間暴露設備節點以及控制接口,接收來自用戶空間的控制指令,通過將自身嵌入media controller中來實現枚舉、連接子設備同時控制數據流走向的目的。
四、模塊初始化
整個v4l2框架是在linux內核中實現的,所以按照內核驅動的運行機制,會在系統啟動的過程中,通過標準的module_init方式進行初始化操作,而其初始化主要包含兩個方面,一個是v4l2_device的初始化,一個是子設備的初始化,首先我們來看下v4l2_device的初始化動作的基本流程。
由于驅動的實現都交由各個平臺廠商進行實現,所有內部邏輯都各不相同,這里我們抽離出主要方法來進行梳理:
首先對于v4l2_device的初始化而言,在系統啟動的過程中,linux內核會找到module_init聲明的驅動,調用其probe方法進行探測相應設備,一旦探測成功,便表示初始化工作完成。
而在probe方法內部,主要做了以下操作:
- 獲取dts硬件信息,初始化部分硬件設備。
- 創建v4l2_device結構體,填充信息,通過v4l2_device_register方法向系統注冊并且創建video設備節點。
- 創建media_device結構體,填充信息,通過media_device_register向系統注冊,并創建media設備節點,并將其賦值給v4l2_device中的mdev。
- 創建v4l2_device的media_entity,并將其添加到media controller進行管理。
類似于v4l2_device的初始化工作,子設備的流程如下:
- 獲取dts硬件信息,初始化子設備硬件模塊
- 創建v4l2_subdev結構體,填充信息,通過v4l2_device_register_subdev向系統注冊,并將其掛載到v4l2_device設備中
- 創建對應的media_entity,并通過media_device_register_entity方法其添加到media controller中進行統一管理。
- 最后調用v4l2_device_register_subdev_nodes方法,為所有的設置了V4L2_SUBDEV_FL_HAS_DEVNODE屬性的子設備創建設備節點。
五、處理用戶空間請求
系統啟動之后,初始化工作便已經完成,現在一旦用戶想要使用圖像采集功能,便會觸發整個視頻采集流程,會通過操作相應的video節點來獲取圖像數據,一般來講,標準的V4L2框架只需要通過操作video節點即可,但是由于現在的硬件功能越來越復雜,常規的v4l2_controller已經滿足不了采集需求,所以現在的平臺廠商通常會暴露子設備的設備節點,在用戶空間直接通過標準的字符設備控制接口來控制各個設備,而現在我們的目的是梳理V4L2框架,所以暫時默認不創建子設備節點,簡單介紹下整個流程。
在操作之前,還有一個準備工作需要做,那就是需要找到哪些是我們所需要的設備,而它的設備節點是什么,此時便可以通過打開media設備節點,并且通過ioctl注入MEDIA_IOC_ENUM_ENTITIES參數來獲取v4l2_device下的video設備節點,該操作會調用到內核中的media_device_ioctl方法,而之后根據傳入的命令,進而調用到media_device_enum_entities方法來枚舉所有的設備。
整個采集流程,主要使用三個標準字符設備接口來完成,分別是用于打開設備的open方法、用于控制設備的ioctl方法以及關閉設備的close方法。
1. 打開設備(open)
一旦確認了我們需要操作的video節點是哪一個,便可以通過調用字符設備標準接口open方法來打開設備,而這個方法會首先陷入內核空間,然后調用file_operations中的open方法,再到v4l2_file_operations中的open方法,而該方法由驅動自己進行實現,其中主要包括了給各個硬件模塊上電,并且調用vb2_queue_init方法創建并初始化一個vb2_queue用于數據緩沖區的管理。
2. 控制設備(ioctl)
在打開設備之后,接下來的大部分操作都是通過ioctl方法來完成的,而在該方法中,會首先陷入到內核空間,之后調用字符設備的v4l2_fops中的v4l2_ioctl方法,而在該方法中又會去調用video_device的video_ioctl2方法,video_ioctl2方法定義了一系列video標準的方法,通過不同的命令在v4l2_ioctls中找到相應的標準方法實現,同時為了滿足用戶自定義命令的實現,在video_ioctl2方法中會去調用到之前注冊video_device時賦予的ioctl_ops中的vidioc_default方法,在該方法中加入用戶自己的控制邏輯。
在整個控制流程中,首先通過命令VIDIOC_QUERYCAP來獲取設備所具有的屬性,通過VIDIOC_G_PARM/VIDIOC_S_PARM來分別獲取和設置設備參數,在這一系列操作配置完成之后,便需要向內核申請用于數據流轉的緩沖區(Buffer),該操作通過命令VIDIOC_REQBUFS來完成,在內核部分主要調用了標準方法vb2_reqbufs,進而調用__vb2_queue_alloc來向內核申請已知個數的Buffer,并且將其存入之前創建的vb2_queue中進行管理。
申請好了Buffer之后,便可以通過傳入VIDIOC_QBUF命令將申請的Buffer入隊,具體操作最終會調用vb2_qbuf方法,而在該方法中會從vb2_queue的bufs數組中取出Buffer,將其加入queued_list鏈表中,并且更新Buffer狀態,等待數據的填充或者來自用戶空間的出隊操作。
在完成上面的操作后,整個數據流并沒有開始流轉起來,所以需要下發VIDIOC_STREAMON命令來通知整個框架開始出數據,在驅動中主要會去調用vb2_streamon方法,進而調用vb2_start_streaming方法,其中該方法會去將隊列中的的Buffer放入到相應的驅動中,等待被填充,緊接著會去調用vb2_queue.ops.start_streaming方法來通知設備開始出圖,而該方法一般由驅動自己實現,最后會調用v4l2_subdev_call(subdev, video, s_stream, mode)方法通知各個子設備開始出圖。
當有圖像產生時,會填充到之前傳入的buffe中,并且調用vb2_buffer_done方法通知vb2_queue將buffer加入到done_list鏈表中,并更新狀態為VB2_BUF_STATE_DONE。
在整個數據流開啟之后,并不會自動的將圖像傳入用戶空間,必須通過VIDIOC_DQBUF命令來從設備中讀取一個幀圖像數據,具體操作是通過層層調用會調用到vb2_dqbuf方法,而在該方法中會調用__vb2_get_done_vb方法去從done_list中獲取Buffer,如果當前鏈表為空則會等待最終數據準備好,如果有準備好的buffer便直接從done_list取出,并且將其從queued_list中去掉,最后通過__vb2_dqbuf方法將Buffer返回用戶空間。
獲取到圖像數據之后,便可以進行后期的圖像處理流程了,在處理完成之后,需要下發VIDIOC_QBUF將此次buffer重新加入queued_list中,等待下一次的數據的填充和出隊操作。
但不需要進行圖像的采集時,可以通過下發VIDIOC_STREAMOFF命令來停止整個流程,具體流程首先會調用v4l2_subdev_call(subdev, video, s_stream, 0)通知所有子設備停止出圖操作,其次調用vb2_buffer_done喚醒可能的等待Buffer的線程,同時更新Buffer狀態為VB2_BUF_STATE_ERROR,然后調用vb2_streamoff取消所有的數據流并更新vb2_queue.streaming的為disable狀態。
3. 關閉設備(close)
但確認不使用當前設備進行圖像采集操作之后,便可以調用標準方法close來關閉設備。其中主要包括了調用vb2_queue_release方法釋放了vb2_queue以及設備下電操作和相關資源的釋放。
通過上面的介紹,我相信我們已經對整個V4L2框架有了一個比較深入的認識, 然而對于一個優秀的軟件架構而言,僅僅是支持現有的功能是遠遠不夠的,隨著功能的不斷完善,勢必會出現需要進行擴展的地方,而v4l2在設計之初便很好的考慮到了這一點,所以提供了用于擴展的方法集,開發者可以通過加入自定的命令來擴充整個框架,高通在這一點上做的非常好,在v4l2框架基礎上,設計出了一個獨特的KMD框架,提供給UMD CSL進行訪問的接口。
原文鏈接:https://blog.csdn.net/u012596975/article/details/107137555
至此,本篇已結束。轉載網絡的文章,小編覺得很優秀,歡迎點擊閱讀原文,支持原創作者,如有侵權,懇請聯系小編刪除,歡迎您的建議與指正。同時期待您的關注,感謝您的閱讀,謝謝!