1.概述
在V4L2子系统中,Video设备是一个字符设备,设备节点为/dev/videoX
,主设备号为81,次设备号范围为0-63。在用户空间,应用可以通过open/close/ioctl/mmap/read/write
系统调用操作Video设备。在内核空间中,Video设备的具体操作方法由驱动中的struct video_device
提供。驱动使用video_register_device
函数将struct video_device
注册到V4L2的核心层,然后V4L2的核心层在向上注册一个字符设备,该字符设备实现了虚拟文件系统要求的方法。这样应用就可以使用系统调用访问虚拟文件系统中Video设备提供的方法,然后进一步访问V4L2核心层提供的v4l2_fops
方法集合,最后通过struct video_device
结构体中的fops
和ioctl_ops
方法集合访问Video主设备。Video主设备通过v4l2_subdev_call
方法访问Video从设备,同时Video从设备可以通过notify
回掉方法通知主设备发生了事件。Camera Host控制器为Video主设备,Camear Sensor(摄像头)为Video从设备,一般为I2C设备。
2.数据结构介绍
Video设备使用struct video_device
结构体表示。设备的操作方法由fops
和ioctl_ops
函数指针集合指定,fops
是文件操作方法,实现常见的open、read、write
方法,ioctl_ops
实现应用层的ioctl
函数,功能较多。具体的设备类型由vfl_type
指定,可用VFL_TYPE_XXXX表示。Video设备是一个字符设备,由cdev
指向创建的字符设备结构体。使用video_device_alloc
函数动态分配video_device
结构体内存,使用video_device_release
释放分配的video_device
结构体内存,video_device
的release
函数可以初始化为video_device_release
,当设备注销时释放内存。使用video_register_device
注册该结构体,使用video_unregister_device
注销该结构体。
[include/media/v4l2-dev.h]
// video设备类型,由video_device的vfl_type成员指定
#define VFL_TYPE_GRABBER 0 // 图像采集设备,包括摄像头、调谐器等
#define VFL_TYPE_VBI 1 // 从视频消隐的时间段取得信息的设备
#define VFL_TYPE_RADIO 2 // 无线电设备,如收音机等
#define VFL_TYPE_SUBDEV 3 // v4l2从设备
#define VFL_TYPE_SDR 4 // Software Defined Radio
#define VFL_TYPE_MAX 5
// 传输方向,设备类型为VFL_TYPE_SUBDEV时忽略,由video_device的vfl_dir成员指定
#define VFL_DIR_RX 0 // 接收
#define VFL_DIR_TX 1 // 发送
#define VFL_DIR_M2M 2 // 内存到内存
// video_device注册状态掩码,若注册成功则video_device的flags的bit0为1,
// 注销时video_device的flags的bit0清零
#define V4L2_FL_REGISTERED (0)
// video_device的flags的bit1为1,则file->private_data指向struct v4l2_fh
#define V4L2_FL_USES_V4L2_FH (1)
struct video_device
{
#if defined(CONFIG_MEDIA_CONTROLLER) // 多媒体设备配置选项
struct media_entity entity; // 用于运行时数据流的管理
#endif
/* video设备操作函数集合,实现read、write等函数 */
const struct v4l2_file_operations *fops;
// video_device对应的设备结构体
struct device dev;
// 字符设备指针,注册时创建字符设备结构体
struct cdev *cdev;
struct v4l2_device *v4l2_dev; /* v4l2_device parent */
/* Only set parent if that can't be deduced from v4l2_dev */
struct device *dev_parent; /* device parent */
/* Control handler associated with this device node. May be NULL. */
struct v4l2_ctrl_handler *ctrl_handler;
/* vb2_queue associated with this device node. May be NULL. */
struct vb2_queue *queue;
/* Priority state. If NULL, then v4l2_dev->prio will be used. */
struct v4l2_prio_state *prio;
/* 设备名称 */
char name[32];
int vfl_type; /* 设备类型 */
int vfl_dir; /* 传输方向 */
int minor; // 次设备号,如果注册失败,将被设置为-1
u16 num; // 设备数量
// 标志位,可以使用set/clear/test操作
unsigned long flags;
// 物理设备的索引,用来区分不同的物理设备
int index;
/* V4L2 file handles */
spinlock_t fh_lock; /* Lock for all v4l2_fhs */
struct list_head fh_list; /* List of struct v4l2_fh */
/* Video standard vars */
v4l2_std_id tvnorms; /* Supported tv norms */
// 设备引用计数为0时被调用,用来释放资源,如释放动态分配的struct video_device结构体
void (*release)(struct video_device *vdev);
// 控制函数,应用层的ioctl都在这里实现,比较重要
const struct v4l2_ioctl_ops *ioctl_ops;
DECLARE_BITMAP(valid_ioctls, BASE_VIDIOC_PRIVATE);
/* serialization lock */
DECLARE_BITMAP(disable_locking, BASE_VIDIOC_PRIVATE);
struct mutex *lock;
};
// 动态分配video_device结构体,返回动态分配的video_device结构体指针,若失败返回NULL
struct video_device *video_device_alloc(void);
// 释放动态分配的video_device结构内存,release可以初始化为该函数
void video_device_release(struct video_device *vdev);
// 注册video_device结构体,注册失败时不会调用release函数
// vdev-video_device结构体指针
// type-设备类型,由VFL_TYPE_XXXX宏指定
// nr-生成的设备节点数量(0 == /dev/video0, 1 == /dev/video1, ...-1 == first free)
// 返回值-0成功,小于0失败
static inline int video_register_device(struct video_device *vdev, int type, int nr)
// 注销video_device结构体
void video_unregister_device(struct video_device *vdev);
Video设备的文件操作方法由struct v4l2_file_operations
结构体表示,指定了read、write、ioctl、mmap
等常见的文件操作方法。应用层对Video设备发起IO操作,最终会调用到该结构体中的函数。
[include/media/v4l2-dev.h]
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 *);
// poll函数
unsigned int (*poll) (struct file *, struct poll_table_struct *);
// ioctl函数
long (*ioctl) (struct file *, unsigned int, unsigned long);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
#ifdef CONFIG_COMPAT
long (*compat_ioctl32) (struct file *, unsigned int, unsigned long);
#endif
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 *);
};
视频设备的应用层ioctl
功能由struct v4l2_ioctl_ops
结构体来表示。ioctl
命令在内核include/uapi/linux/videodev2.h
文件中定义。比较常用的命令如下面的宏定义所示。
[include/uapi/linux/videodev2.h]
// 查询底层驱动支持的功能
#define VIDIOC_QUERYCAP _IOR('V', 0, struct v4l2_capability)
// 获取底层驱动支持的视频格式
#define VIDIOC_ENUM_FMT _IOWR('V', 2, struct v4l2_fmtdesc)
// 获取底层驱动当前的视频捕获格式
#define VIDIOC_G_FMT _IOWR('V', 4, struct v4l2_format)
// 设置底层驱动当前的视频捕获格式
#define VIDIOC_S_FMT _IOWR('V', 5, struct v4l2_format)
// 申请数据缓存内存
#define VIDIOC_REQBUFS _IOWR('V', 8, struct v4l2_requestbuffers)
// 获取VIDIOC_REQBUFS中分配的缓冲区信息,包括长度、偏移等信息
#define VIDIOC_QUERYBUF _IOWR('V', 9, struct v4l2_buffer)
// 图像采集模式-将读取完数据的空缓存返还给驱动的缓存队列
// 图像输出模式-将填充满数据的缓存返还给驱动的缓存队列
#define VIDIOC_QBUF _IOWR('V', 15, struct v4l2_buffer)
// 图像采集模式-将填充满数据的缓存从驱动中返回给应用
// 图像输出模式-将读取完数据的空显示缓存从驱动中返回给应用
#define VIDIOC_DQBUF _IOWR('V', 17, struct v4l2_buffer)
// 开始采集视频
#define VIDIOC_STREAMON _IOW('V', 18, int)
// 停止采集视频
#define VIDIOC_STREAMOFF _IOW('V', 19, int)
// 获取视频流的参数
#define VIDIOC_G_PARM _IOWR('V', 21, struct v4l2_streamparm)
// 设置视频流的参数
#define VIDIOC_S_PARM _IOWR('V', 22, struct v4l2_streamparm)
// 查询当前视频输入设备采用的标准
#define VIDIOC_G_STD _IOR('V', 23, v4l2_std_id)
// 设置当前视频输入设备的标准
#define VIDIOC_S_STD _IOW('V', 24, v4l2_std_id)
// 列出所有视频输入源
#define VIDIOC_ENUMINPUT _IOWR('V', 26, struct v4l2_input)
// 获取目前使用的视频输入源编号
#define VIDIOC_G_INPUT _IOR('V', 38, int)
// 选择视频输入源,输入源编号通过参数传入
#define VIDIOC_S_INPUT _IOWR('V', 39, int)
// 查询底层驱动的修剪能力
#define VIDIOC_CROPCAP _IOWR('V', 58, struct v4l2_cropcap)
// 获取视频信号的矩形边框
#define VIDIOC_G_CROP _IOWR('V', 59, struct v4l2_crop)
// 设置视频信号的矩形边框
#define VIDIOC_S_CROP _IOW('V', 60, struct v4l2_crop)
// 获取当前视频设备支持的标准,如PAL或NTSC
#define VIDIOC_QUERYSTD _IOR('V', 63, v4l2_std_id)
// 验证底层驱动的显示格式
#define VIDIOC_TRY_FMT _IOWR('V', 64, struct v4l2_format)
// 列出在给定的像素格式下视频帧的尺寸
#define VIDIOC_ENUM_FRAMESIZES _IOWR('V', 74, struct v4l2_frmsizeenum)
// 列出在给定的像素格式和帧尺寸下视频帧的间隔
#define VIDIOC_ENUM_FRAMEINTERVALS _IOWR('V', 75, struct v4l2_frmivalenum)
[include/media/v4l2-ioctl.h]
struct v4l2_ioctl_ops {
/* VIDIOC_QUERYCAP handler */
int (*vidioc_querycap)(struct file *file,
void *fh, struct v4l2_capability *cap);
/* VIDIOC_ENUM_FMT handlers */
int (*vidioc_enum_fmt_vid_cap)(struct file *file,
void *fh, struct v4l2_fmtdesc *f);
......
/* VIDIOC_G_FMT handlers */
int (*vidioc_g_fmt_vid_cap)(struct file *file, void *fh,struct v4l2_format *f);
......
/* VIDIOC_S_FMT handlers */
int (*vidioc_s_fmt_vid_cap)(struct file *file, void *fh,struct v4l2_format *f);
......
/* VIDIOC_TRY_FMT handlers */
int (*vidioc_try_fmt_vid_cap)(struct file *file, void *fh,struct v4l2_format *f);
......
/* Buffer handlers */
int (*vidioc_reqbufs) (struct file *file,
void *fh, struct v4l2_requestbuffers *b);
int (*vidioc_querybuf)(struct file *file, void *fh, struct v4l2_buffer *b);
int (*vidioc_qbuf) (struct file *file, void *fh, struct v4l2_buffer *b);
int (*vidioc_dqbuf) (struct file *file, void *fh, struct v4l2_buffer *b);
......
/* Stream on/off */
int (*vidioc_streamon) (struct file *file, void *fh, enum v4l2_buf_type i);
int (*vidioc_streamoff)(struct file *file, void *fh, enum v4l2_buf_type i);
/* Standard handling, ENUMSTD is handled by videodev.c */
int (*vidioc_g_std) (struct file *file, void *fh, v4l2_std_id *norm);
int (*vidioc_s_std) (struct file *file, void *fh, v4l2_std_id norm);
int (*vidioc_querystd) (struct file *file, void *fh, v4l2_std_id *a);
/* Input handling */
int (*vidioc_enum_input)(struct file *file, void *fh,struct v4l2_input *inp);
int (*vidioc_g_input) (struct file *file, void *fh, unsigned int *i);
int (*vidioc_s_input) (struct file *file, void *fh, unsigned int i);
/* Crop ioctls */
int (*vidioc_cropcap)(struct file *file, void *fh,struct v4l2_cropcap *a);
int (*vidioc_g_crop)(struct file *file, void *fh,struct v4l2_crop *a);
int (*vidioc_s_crop)(struct file *file, void *fh,const struct v4l2_crop *a);
......
/* Stream type-dependent parameter ioctls */
int (*vidioc_g_parm)(struct file *file, void *fh,struct v4l2_streamparm *a);
int (*vidioc_s_parm)(struct file *file, void *fh,struct v4l2_streamparm *a);
......
/* Debugging ioctls */
......
int (*vidioc_enum_framesizes)(struct file *file,
void *fh,struct v4l2_frmsizeenum *fsize);
int (*vidioc_enum_frameintervals)(struct file *file,
void *fh,struct v4l2_frmivalenum *fival);
};
struct video_device
数据结构中fops
和ioctl_ops
成员最重要,video_device
数据结构示意图如下图所示。
Video从设备的操作方法由struct v4l2_subdev_video_ops
结构体表示。Video主设备可以通过v4l2_subdev_call
调用这些方法。
struct v4l2_subdev_video_ops {
int (*s_routing)(struct v4l2_subdev *sd, u32 input, u32 output, u32 config);
// 设置时钟频率,单位为Hz,额外的标志位可以记录分频系数,如果没有使用,
// 则设置为0,如果不支持设置频率,则返回-EINVAL
int (*s_crystal_freq)(struct v4l2_subdev *sd, u32 freq, u32 flags);
int (*g_std)(struct v4l2_subdev *sd, v4l2_std_id *norm);
int (*s_std)(struct v4l2_subdev *sd, v4l2_std_id norm);
// 设置视频输出设备的标准ID,视频输入设备忽略
int (*s_std_output)(struct v4l2_subdev *sd, v4l2_std_id std);
// 获取视频输出设备当前采用的标准,视频输入设备忽略
int (*g_std_output)(struct v4l2_subdev *sd, v4l2_std_id *std);
int (*querystd)(struct v4l2_subdev *sd, v4l2_std_id *std);
// 获取视频采集设备支持的所有标准ID,视频输出设备忽略
int (*g_tvnorms)(struct v4l2_subdev *sd, v4l2_std_id *std);
// 获取视频输出设备支持的所有标准ID,视频采集设备忽略
int (*g_tvnorms_output)(struct v4l2_subdev *sd, v4l2_std_id *std);
// 获取输入状态,和v4l2_input结构体中的status字段一样
int (*g_input_status)(struct v4l2_subdev *sd, u32 *status);
int (*s_stream)(struct v4l2_subdev *sd, int enable);
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, const 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 (*g_frame_interval)(struct v4l2_subdev *sd,
struct v4l2_subdev_frame_interval *interval);
int (*s_frame_interval)(struct v4l2_subdev *sd,
struct v4l2_subdev_frame_interval *interval);
// 设置传统摄像机图像采集的时序
int (*s_dv_timings)(struct v4l2_subdev *sd,struct v4l2_dv_timings *timings);
// 获取传统摄像机图像采集的时序
int (*g_dv_timings)(struct v4l2_subdev *sd, struct v4l2_dv_timings *timings);
int (*query_dv_timings)(struct v4l2_subdev *sd,struct v4l2_dv_timings *timings);
// 列举像素格式
int (*enum_mbus_fmt)(struct v4l2_subdev *sd, unsigned int index,u32 *code);
// 获取像素格式
int (*g_mbus_fmt)(struct v4l2_subdev *sd,struct v4l2_mbus_framefmt *fmt);
// 尝试设置像素格式
int (*try_mbus_fmt)(struct v4l2_subdev *sd,struct v4l2_mbus_framefmt *fmt);
// 设置像素格式
int (*s_mbus_fmt)(struct v4l2_subdev *sd,struct v4l2_mbus_framefmt *fmt);
// 获取多媒体总线设置
int (*g_mbus_config)(struct v4l2_subdev *sd,struct v4l2_mbus_config *cfg);
// 设置明确的媒体总线设置
int (*s_mbus_config)(struct v4l2_subdev *sd,const struct v4l2_mbus_config *cfg);
// 设置主机给从设备分配的缓存,从设备可以调整缓冲区的size到较低的水平
int (*s_rx_buffer)(struct v4l2_subdev *sd, void *buf,unsigned int *size);
};
3.注册与注销过程分析
视频设备的主要功能都由struct video_device
结构体实现,video_device
结构体由video_register_device
函数注册。video_register_device
函数内部只调用了__video_register_device
函数,下面重点分析一下__video_register_device
函数。
[include/media/v4l2-dev.h]
// 注册video_device结构体
// vdev-video_device结构体指针
// type-注册的设备类型,有效的设备类型如下:
// VFL_TYPE_GRABBER - A frame grabber
// VFL_TYPE_VBI - Vertical blank data (undecoded)
// VFL_TYPE_RADIO - A radio card
// VFL_TYPE_SUBDEV - A subdevice
// VFL_TYPE_SDR - Software Defined Radio
// nr-生成的设备节点数量(0 == /dev/video0, 1 == /dev/video1, ...-1 == first free)
// warn_if_nr_in_use-若设备节点编号已被占用则发出警告,同时会选择其他设备节点编号
// owner-video设备节点所属的模块
// 返回值-0成功,小于0失败
int __video_register_device(struct video_device *vdev, int type, int nr,
int warn_if_nr_in_use, struct module *owner)
__video_register_device
函数的执行流程如下图所示,可总结如下:
(1)在注册之前必须设置release
和v4l2_dev
成员,前者用于设备注销时回调释放资源,后者指向了管理video_device
的v4l2_device
结构体。
(2)检查设备类型并确定设备节点基本名称。
(3)设置设备类型、次设备号(由设备类型和全局video_device
数组决定)及设备节点数量。
(4)将要注册的video_device
结构体指针保存到全局的video_device
数组中。
(5)根据设备类型验证那些ioctl函数可以使用。
(6)分配字符设备结构体。
(7)设置字符设备的操作函数集合为v4l2_fops
。
(8)将video设备注册为字符设备。
(9)注册设备。
(10)设置设备引用计数为0时的回调函数,回调函数为v4l2_device_release
。v4l2_device_release
主要的工作是删除注册的字符设备,回调v4l2_device
中的release
函数(通常是video_device_release
函数)释放video_device
结构体内存,最后减少v4l2_device
的引用计数(一个v4l2_device
可管理多个video_device
,当v4l2_device
的引用计数为0时,将进行注销工作)。
(11)增加video_device
所属v4l2_device
的引用计数。
(12)设置已注册标志V4L2_FL_REGISTERED
。
调用video_unregister_device
函数注销注册的video_device
,主要执行流程如下:
void video_unregister_device(struct video_device *vdev)
{
/* 如果没有注册,则直接返回 */
if (!vdev || !video_is_registered(vdev))
return;
mutex_lock(&videodev_lock);
// 清楚已注册标志
clear_bit(V4L2_FL_REGISTERED, &vdev->flags);
mutex_unlock(&videodev_lock);
// 注销设备
device_unregister(&vdev->dev);
}
4.Video设备访问流程总结
Video设备访问流程如下图所示。总结如下:
(1)首先通过系统调用访问/dev/videoX
用户空间设备节点。
(2)进入到内核空间,访问字符设备struct file_operations
中的方法。对于Vedio设备,该操作集合被V4L2子系统初始化为v4l2_fops
集合。
(3)通过V4L2子系统提供的v4l2_fops
集合,可直接调用底层驱动实现的Video主设备struct v4l2_file_operations
方法,对于ioctl
方法,则需要借助中间函数__video_do_ioctl
调用底层驱动实现的struct v4l2_ioctl_ops
中的ioctl
功能。struct v4l2_file_operations
方法和struct v4l2_ioctl_ops
方法属于主设备方法,需要主设备的驱动实现。
(4)struct v4l2_file_operations
和struct v4l2_ioctl_ops
中的函数都可以通过v4l2_subdev_call
调用Video从设备struct v4l2_subdev_core_ops
、struct v4l2_subdev_video_ops
、struct v4l2_subdev_pad_ops
等方法,这些方法都要在从设备驱动中实现。
5.Video设备访问流程实例分析
下图是Linux 4.1版本中imx6ull CMOS Sensor Interface (CSI)和ov5640 Image Sensor设备的访问流程,imx6ull CMOS Sensor Interface (CSI)是主设备,ov5640 Image Sensor是从设备,ov5640通过CSI接口连接到CPU上。imx6ull CSI驱动实现了主设备的操作方法,分别为struct v4l2_file_operations mx6s_csi_fops
和struct v4l2_ioctl_ops mx6s_csi_ioctl_ops
。ov5640驱动实现了从设备的操作方法,分别为struct v4l2_subdev_core_ops
、struct v4l2_subdev_video_ops ov5640_subdev_video_ops
、struct v4l2_subdev_pad_ops ov5640_subdev_pad_ops
。后面会具体分析这些方法的执行流程。
参考资料
- 内核文档-v4l2-framework_zh_CN.txt
- Linux内核4.1版本源码
- Android驱动开发权威指南
- Android驱动开发与移植实战详解
- http://blog.sina.com.cn/s/blog_602f87700101a52s.html
- https://www.kernel.org/doc/html/v4.11/media/uapi/v4l/v4l2.html
- IMX6ULL参考手册
- ov5640参考手册