V4L2 驅動隨著硬件的變化也越來越復雜,現在大部分設備有裡面包含了多個IC, 在/dev目錄下不僅要建立 V4L2 的節點,而且還需要建立如:DVB、ALSA、FB、I2C、input等設備節點。事實上 V4L2 驅動需要支持音頻/視頻的混音/編碼/解碼等IC所以比其他驅動都要復雜很多,通常這些IC通過 i2c 總線連接到主板,這些設備都統稱為sub-devices。在很長的一段時間裡 V4L2 被限制只能在 video_device 結構體裡面創建,並且用video_buf 控制視頻緩存,這意味著所有的驅動創建自己的實例都將連接到自己的sub-devices,這些工作通常很復雜並經常引起錯誤,許多常見的代碼因為缺乏一個框架而無法重構。因此這個框架建立起了基本的機制,所有的驅動都需要和這個框架結合以便共用其中的函數。因此 V4L2 框架作了相應的優化:它有一個 v4l2_device 結構作為設備實例,一個v4l2_subdev 結構作為子設備實例,video_device 結構包含了v4l2_device 節點,以後將會有一個 v4l2_fh 的結構作為與文件句柄的實例。每個設備都采用 v4l2_device 結構來表示。非常簡單的設備都可以申請這個結構,但通常會將這個結構嵌入一個更大的結構中。
1、 video_device
在 v4l2 中用 struct video_device 代表一個視頻設備,該結構說明如下:
[cpp]
struct video_device
{
/* 設備操作合集 */
const struct v4l2_file_operations *fops;
/* sysfs節點 */
struct device dev; /* v4l device */
struct cdev *cdev; /* 字符設備節點 */
/* Set either parent or v4l2_dev if your driver uses v4l2_device */
struct device *parent; /* 父設備 */
struct v4l2_device *v4l2_dev; /* v4l2_device parent */
/* 設備信息 */
char name[32];
int vfl_type;
/* 如果注冊失敗 minor 將被設置為 -1 */
int minor;
u16 num;
/* 需要用位操作 flags */
unsigned long flags;
/* attribute to differentiate multiple indices on one physical device */
int index;
int debug; /* Activates debug level*/
/* Video standard vars */
v4l2_std_id tvnorms; /* Supported tv norms */
v4l2_std_id current_norm; /* Current tvnorm */
/* 釋放設備的回調函數 */
void (*release)(struct video_device *vdev);
/* ioctl 回調函數 */
const struct v4l2_ioctl_ops *ioctl_ops;
};
其中
struct cdev {
struct kobject kobj; /* 內核對象 */
struct module *owner;
const struct file_operations *ops; /* 設備操作合集 */
struct list_head list;
dev_t dev;
unsigned int count;
};
struct v4l2_file_operations {
struct module *owner;
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); /* 讀數據 */
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *); /* 寫數據 */
unsigned int (*poll) (struct file *, struct poll_table_struct *); /* 同步操作 */
long (*ioctl) (struct file *, unsigned int, unsigned long); /* 特殊命令 */
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
unsigned long (*get_unmapped_area) (struct file *, unsigned long,
unsigned long, unsigned long, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *); /* 內存映射 */
int (*open) (struct file *); /* 打開設備 */
int (*release) (struct file *); /* 釋放設備 */
};
視頻設備在 linux 中作為字符設備出現,域 cdev 與 /dev/videox 節點關聯,打開節點就相當於執行cdev 的 open 函數,cdev 的 ops 域即 file_operations 的一些接口在經過一定的參數過濾後最終都調用了video_device 的 fops 域即v4l2_file_operations的成員,所以在編寫驅動程序的時候需要實現 v4l2_file_operations 的接口:其中 open 用於打開視頻設備, read 接口用於讀取視頻數據,poll 接口用於視頻流的同步,mmap 將視頻設備的保存數據的內存空間的物理地址映射到用戶空間,ioctl 用於向視頻設備發送命令並查詢相關信息(ioctl 一般設置為 v4l2 提供的 video_ioctl2 函數,並最終調用 video_device 的 ioctl_ops 域即 v4l2_ioctl_ops),通常需要實現的 ioctl 接口如下:
[cpp]
static const struct v4l2_ioctl_ops xxx_cam_ioctl_ops = {
.vidioc_querycap = vidioc_querycap,
.vidioc_enum_input = vidioc_enum_input,
.vidioc_g_input = vidioc_g_input,
.vidioc_s_input = vidioc_s_input,
.vidioc_queryctrl = vidioc_queryctrl,
.vidioc_s_ctrl = vidioc_s_ctrl,
.vidioc_g_ctrl = vidioc_g_ctrl,
.vidioc_enum_fmt_vid_cap = vidioc_enum_fmt_vid_cap,
.vidioc_try_fmt_vid_cap = vidioc_try_fmt_vid_cap,
.vidioc_g_fmt_vid_cap = vidioc_g_fmt_vid_cap,
.vidioc_s_fmt_vid_cap = vidioc_s_fmt_vid_cap,
.vidioc_reqbufs = vidioc_reqbufs,
.vidioc_querybuf = vidioc_querybuf,
.vidioc_qbuf = vidioc_qbuf,
.vidioc_dqbuf = vidioc_dqbuf,
.vidioc_streamon = vidioc_streamon,
.vidioc_streamoff = vidioc_streamoff,
.vidioc_default = vidioc_default,
};
該結構各域的作用如上篇文章所述。video_device 通過 video_register_device 函數注冊,函數原型如下:
[cpp]
/**
* video_register_device - 注冊一個 v4l2 設備
* @vdev: video_device 結構
* @type: v4l2 設備的類型
* @nr: 從設備號(0 == /dev/video0, 1 == /dev/video1, -1 == first free)
*
* 注冊代碼將會根據注冊設備的類型指派從設備號,如果沒有合適的從設備號將會返回錯誤值.
*
* 通常有如下幾種設備類型
*
* %VFL_TYPE_GRABBER - 視頻采集設備
*
* %VFL_TYPE_VTX - 圖文電視設備
*
* %VFL_TYPE_VBI - 場消隱區解碼設備(undecoded)
*
* %VFL_TYPE_RADIO - 無線設備
*/
int video_register_device(struct video_device *vdev, int type, int nr)
{
return __video_register_device(vdev, type, nr, 1);
}
EXPORT_SYMBOL(video_register_device);
該函數注冊流程較簡單:首先會根據設備類型確定在 /dev 目錄下的節點名稱以及從設備號的偏移和值,然後為 cdev 申請內存空間並注冊,將 vdev->cdev->ops 設置為內核提供的 v4l2_fops,最後將 vdev->dev 注冊到 sysfs 中。
2、v4l2_subdev
許多驅動需要與子設備通信,這些設備做的任務比較常見的是音視頻處理、編解碼等, 網絡攝像頭比較常見的子設備是傳感器和攝像頭控制器。為了提供一個統一的接口給這些子設備,內核將涉及到子設備控制的那部分(如 vidioc_s_ctrl、vidioc_s_frequency 等)獨立了出來,用 struct v4l2_subdev 來表示以方便用戶實現 v4l2 驅動程序:
[cpp]
struct v4l2_subdev {
struct list_head list; /* 鏈接至 v4l2_device */
struct module *owner;
u32 flags;
struct v4l2_device *v4l2_dev; /* 指向 v4l2_device */
const struct v4l2_subdev_ops *ops; /* subdev 操作合集 */
/* name must be unique */
char name[V4L2_SUBDEV_NAME_SIZE];
/* can be used to group similar subdevs, value is driver-specific */
u32 grp_id;
/* 私有數據 */
void *priv;
};
其中 list 域作為鏈表節點鏈接至 v4l2_dev 指向的 v4l2_device 結構中,這個結構中最重要的成員就是 struct v4l2_subdev_ops *ops,該域包含了 v4l2 設備支持的所有操作,定義如下:
[cpp]
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; /* 視頻操作合集 */
};
v4l2_subdev_core_ops 包含的操作合集是各種類型設備通用的:
[cpp]
struct v4l2_subdev_core_ops {
int (*g_chip_ident)(struct v4l2_subdev *sd, struct v4l2_dbg_chip_ident *chip); /* 獲取設備id */
int (*log_status)(struct v4l2_subdev *sd); /* 狀態消息 */
int (*s_config)(struct v4l2_subdev *sd, int irq, void *platform_data); /* 設置配置信息 */
int (*init)(struct v4l2_subdev *sd, u32 val); /* 初始化設備 */
int (*load_fw)(struct v4l2_subdev *sd); /* 加載firmware */
int (*reset)(struct v4l2_subdev *sd, u32 val); /* 重置設備 */
int (*s_gpio)(struct v4l2_subdev *sd, u32 val); /* 設置gpio */
int (*queryctrl)(struct v4l2_subdev *sd, struct v4l2_queryctrl *qc); /* 查詢設備支持的操作 */
int (*g_ctrl)(struct v4l2_subdev *sd, struct v4l2_control *ctrl); /* 獲取當前命令值 */
int (*s_ctrl)(struct v4l2_subdev *sd, struct v4l2_control *ctrl); /* 設置當前命令值 */
int (*g_ext_ctrls)(struct v4l2_subdev *sd, struct v4l2_ext_controls *ctrls); /* 獲取外置命令值 */
int (*s_ext_ctrls)(struct v4l2_subdev *sd, struct v4l2_ext_controls *ctrls); /* 設置外置命令值 */
int (*try_ext_ctrls)(struct v4l2_subdev *sd, struct v4l2_ext_controls *ctrls);
int (*querymenu)(struct v4l2_subdev *sd, struct v4l2_querymenu *qm); /* 查詢操作菜單 */
int (*s_std)(struct v4l2_subdev *sd, v4l2_std_id norm); /* 設置數據標准 */
long (*ioctl)(struct v4l2_subdev *sd, unsigned int cmd, void *arg); /* 處理特殊命令 */
#ifdef CONFIG_VIDEO_ADV_DEBUG
int (*g_register)(struct v4l2_subdev *sd, struct v4l2_dbg_register *reg); /* 獲取寄存器值 */
int (*s_register)(struct v4l2_subdev *sd, struct v4l2_dbg_register *reg); /* 設置寄存器值 */
#endif
};
v4l2_subdev_tuner_ops 包含的操作合集則是調諧器獨有的:
[cpp]
struct v4l2_subdev_tuner_ops {
int (*s_mode)(struct v4l2_subdev *sd, enum v4l2_tuner_type); /* 設置調諧器模式 */
int (*s_radio)(struct v4l2_subdev *sd); /* 設置無線設備信息 */
int (*s_frequency)(struct v4l2_subdev *sd, struct v4l2_frequency *freq); /* 設置頻率 */
int (*g_frequency)(struct v4l2_subdev *sd, struct v4l2_frequency *freq); /* 獲取頻率 */
int (*g_tuner)(struct v4l2_subdev *sd, struct v4l2_tuner *vt); /* 獲取調諧器信息 */
int (*s_tuner)(struct v4l2_subdev *sd, struct v4l2_tuner *vt); /* 設置調諧器信息 */
int (*g_modulator)(struct v4l2_subdev *sd, struct v4l2_modulator *vm); /* 獲取調幅器信息 */
int (*s_modulator)(struct v4l2_subdev *sd, struct v4l2_modulator *vm); /* 設置調幅器信息 */
int (*s_type_addr)(struct v4l2_subdev *sd, struct tuner_setup *type); /* 安裝調諧器 */
int (*s_config)(struct v4l2_subdev *sd, const struct v4l2_priv_tun_config *config); /* 設置配置信息 */
int (*s_standby)(struct v4l2_subdev *sd); /* 設置標准 */
};
v4l2_subdev_audio_ops 包含的操作合集則是音頻部分獨有的:
[cpp] view plaincopy
struct v4l2_subdev_audio_ops {
int (*s_clock_freq)(struct v4l2_subdev *sd, u32 freq); /* 設置音頻設備頻率 */
int (*s_i2s_clock_freq)(struct v4l2_subdev *sd, u32 freq); /* 設置i2s總線頻率 */
int (*s_routing)(struct v4l2_subdev *sd, u32 input, u32 output, u32 config); /* 設置音頻路由 */
};
v4l2_subdev_video_ops 包含的操作合集則是視頻部分獨有的:
[cpp]
struct v4l2_subdev_video_ops {
int (*s_routing)(struct v4l2_subdev *sd, u32 input, u32 output, u32 config); /* 設置視頻路由 */
int (*s_crystal_freq)(struct v4l2_subdev *sd, u32 freq, u32 flags); /* 設置設備頻率 */
int (*decode_vbi_line)(struct v4l2_subdev *sd, struct v4l2_decode_vbi_line *vbi_line); /* 消隱區信息解碼 */
int (*s_vbi_data)(struct v4l2_subdev *sd, const struct v4l2_sliced_vbi_data *vbi_data); /* 設置消隱區數據 */
int (*g_vbi_data)(struct v4l2_subdev *sd, struct v4l2_sliced_vbi_data *vbi_data); /* 獲取消隱區數據 */
int (*g_sliced_vbi_cap)(struct v4l2_subdev *sd, struct v4l2_sliced_vbi_cap *cap);
int (*s_std_output)(struct v4l2_subdev *sd, v4l2_std_id std); /* 設置標准輸出 */
int (*querystd)(struct v4l2_subdev *sd, v4l2_std_id *std); /* 查詢標准 */
int (*g_input_status)(struct v4l2_subdev *sd, u32 *status); /* 獲取輸入狀態 */
int (*s_stream)(struct v4l2_subdev *sd, int enable); /* 設置數據流 */
int (*enum_fmt)(struct v4l2_subdev *sd, struct v4l2_fmtdesc *fmtdesc); /* 枚舉視頻格式 */
int (*g_fmt)(struct v4l2_subdev *sd, struct v4l2_format *fmt); /* 獲取視頻格式 */
int (*try_fmt)(struct v4l2_subdev *sd, struct v4l2_format *fmt); /* 嘗試設置視頻格式 */
int (*s_fmt)(struct v4l2_subdev *sd, struct v4l2_format *fmt); /* 設置視頻格式 */
int (*cropcap)(struct v4l2_subdev *sd, struct v4l2_cropcap *cc); /* 視頻剪輯功能 */
int (*g_crop)(struct v4l2_subdev *sd, struct v4l2_crop *crop); /* 獲取剪輯功能 */
int (*s_crop)(struct v4l2_subdev *sd, struct v4l2_crop *crop); /* 設置剪輯功能 */
int (*g_parm)(struct v4l2_subdev *sd, struct v4l2_streamparm *param); /* 獲取參數 */
int (*s_parm)(struct v4l2_subdev *sd, struct v4l2_streamparm *param); /* 設置參數 */
int (*enum_framesizes)(struct v4l2_subdev *sd, struct v4l2_frmsizeenum *fsize); /* 枚舉幀大小 */
int (*enum_frameintervals)(struct v4l2_subdev *sd, struct v4l2_frmivalenum *fival); /* 枚舉幀間隔 */
};
因為 v4l2 設備一般用 i2c 總線通信,所以注冊函數需要提供 i2c_client,函數原型如下:
[cpp]
/**
* v4l2_i2c_subdev_init - 注冊一個 v4l2_subdev
* @sd: v4l2_subdev 結構
* @client: 通信用的i2c設備
* @ops: v4l2_subdev_ops 操作合集
*/
void v4l2_i2c_subdev_init(struct v4l2_subdev *sd, struct i2c_client *client, const struct v4l2_subdev_ops *ops)
當 video_device 中的接口需要調用 v4l2_subdev 的成員函數時一般采用如下宏定義:
[cpp]
/* 調用成員函數之前需要先檢查成員函數是否被設置
* v4l2_subdev_call - 調用 v4l2_subdev 成員函數
* @sd: v4l2_subdev 結構
* @o: v4l2_subdev_ops 成員名稱
* @f: v4l2_subdev 成員函數
* @args: v4l2_subdev 成員函數的參數
使用示例: err = v4l2_subdev_call(sd, core, g_chip_ident, &chip);
*/
#define v4l2_subdev_call(sd, o, f, args...) \
(!(sd) ? -ENODEV : (((sd) && (sd)->ops->o && (sd)->ops->o->f) ? \
(sd)->ops->o->f((sd) , ##args) : -ENOIOCTLCMD))