目录
参考韦东山驱动大全
一、V4L2应用程序开发
1. 数据采集流程
- Video for Linux two(Video4Linux2)简称V4L2,是V4L的改进版。
- V4L2支持三种方式来采集图像:内存映射方式(mmap)、直接读取方式(read)和用户指针。
- 内存映射的方式采集速度较快,一般用于连续视频数据的采集,实际工作中的应用概率更高;
- 直接读取的方式相对速度慢一些,所以常用于静态图片数据的采集;
- 用户指针使用较少。
1.1 buffer的管理
-
使用摄像头时,核心是"获得数据"。所以先讲如何获取数据,即如何得到buffer。
-
摄像头采集数据时,是一帧又一帧地连续采集。所以需要申请若干个buffer,驱动程序把数据放入buffer,APP从buffer得到数据。这些buffer可以使用链表来管理。
-
由于摄像头在驱动编写后不一定会调用,如果在驱动中申请buffer,那么可能会导致一部分的内存空间被浪费。因此buffer的申请是在应用程序中进行的。
驱动程序周而复始地做如下事情:
- 从硬件采集到数据
- 把"空闲链表"取出buffer,把数据存入buffer
- 把含有数据的buffer放入"完成链表"
APP也会周而复始地做如下事情:
- 监测"完成链表",等待它含有buffer
- 从"完成链表"中取出buffer
- 处理数据
- 把buffer放入"空闲链表"
链表操作示意图如下:
1.2 完整的使用流程
参考mjpg-streamer和video2lcd,总结了摄像头的使用流程,如下:
- open:打开设备节点/dev/videoX
- ioctl VIDIOC_QUERYCAP:Query Capbility,查询能力,比如
- 确认它是否是"捕获设备",因为有些节点是输出设备
- 确认它是否支持mmap操作,还是仅支持read/write操作
- ioctl VIDIOC_ENUM_FMT:枚举它支持的格式
- ioctl VIDIOC_S_FMT:在上面枚举出来的格式里,选择一个来设置格式
- ioctl VIDIOC_REQBUFS:申请buffer,APP可以申请很多个buffer,但是驱动程序不一定能申请到
- ioctl VIDIOC_QUERYBUF和mmap:查询buffer信息(长度)、映射
- 如果申请到了N个buffer,这个ioctl就应该执行N次(for循环)
- 执行mmap后,APP就可以直接读写这些buffer
- ioctl VIDIOC_QBUF:把buffer放入"空闲链表"
- 如果申请到了N个buffer,这个ioctl就应该执行N次
- ioctl VIDIOC_STREAMON:启动摄像头
- 这里是一个循环:使用poll/select监测buffer,然后从"完成链表"中取出buffer,处理后再放入"空闲链表"
- poll/select,阻塞等待数据
- ioctl VIDIOC_DQBUF:从"完成链表"中取出buffer
- 处理:前面使用mmap映射了每个buffer的地址,处理时就可以直接使用地址来访问buffer
- ioclt VIDIOC_QBUF:把buffer放入"空闲链表"
- ioctl VIDIOC_STREAMOFF:停止摄像头
2. 控制流程
使用摄像头时,我们可以调整很多参数,比如:
-
对于视频流本身:
- 设置格式:比如V4L2_PIX_FMT_YUYV、V4L2_PIX_FMT_MJPEG、V4L2_PIX_FMT_RGB565
- 设置分辨率:1024*768等
-
对于控制部分:
- 调节亮度
- 调节对比度
- 调节色度
2.1 APP接口
就APP而言,对于这些参数有3套接口:查询或枚举(Query/Enum)、获得(Get)、设置(Set)。
2.1.1 数据格式
以设置数据格式为例,可以先枚举:
struct v4l2_fmtdesc fmtdesc;
fmtdesc.index = 0; // 比如从0开始
fmtdesc.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; // 指定type为"捕获"
ioctl(vd->fd, VIDIOC_ENUM_FMT, &fmtdesc);
#if 0
/*
* F O R M A T E N U M E R A T I O N
*/
struct v4l2_fmtdesc {
__u32 index; /* Format number */
__u32 type; /* enum v4l2_buf_type */
__u32 flags;
__u8 description[32]; /* Description string */
__u32 pixelformat; /* Format fourcc */
__u32 reserved[4];
};
#endif
还可以获得当前的格式:
struct v4l2_format currentFormat;
memset(¤tFormat, 0, sizeof(struct v4l2_format));
currentFormat.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
ioctl(vd->fd, VIDIOC_G_FMT, ¤tFormat);
#if 0
struct v4l2_format {
__u32 type;
union {
struct v4l2_pix_format pix; /* V4L2_BUF_TYPE_VIDEO_CAPTURE */
struct v4l2_pix_format_mplane pix_mp; /* V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE */
struct v4l2_window win; /* V4L2_BUF_TYPE_VIDEO_OVERLAY */
struct v4l2_vbi_format vbi; /* V4L2_BUF_TYPE_VBI_CAPTURE */
struct v4l2_sliced_vbi_format sliced; /* V4L2_BUF_TYPE_SLICED_VBI_CAPTURE */
struct v4l2_sdr_format sdr; /* V4L2_BUF_TYPE_SDR_CAPTURE */
__u8 raw_data[200]; /* user-defined */
} fmt;
};
/*
* V I D E O I M A G E F O R M A T
*/
struct v4l2_pix_format {v4l2_format
__u32 width;
__u32 height;
__u32 pixelformat;
__u32 field; /* enum v4l2_field */
__u32 bytesperline; /* for padding, zero if unused */
__u32 sizeimage;
__u32 colorspace; /* enum v4l2_colorspace */
__u32 priv; /* private data, depends on pixelformat */
__u32 flags; /* format flags (V4L2_PIX_FMT_FLAG_*) */
__u32 ycbcr_enc; /* enum v4l2_ycbcr_encoding */
__u32 quantization; /* enum v4l2_quantization */
__u32 xfer_func; /* enum v4l2_xfer_func */
};
#endif
也可以设置当前的格式:
struct v4l2_format fmt;
memset(&fmt, 0, sizeof(struct v4l2_format));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
fmt.fmt.pix.width = 1024;
fmt.fmt.pix.height = 768;
fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_MJPEG;
fmt.fmt.pix.field = V4L2_FIELD_ANY;
int ret = ioctl(vd->fd, VIDIOC_S_FMT, &fmt);
2.1.2 选择输入源
可以获得当期输入源、设置当前输入源:
int value;
ioctl(h->fd,VIDIOC_G_INPUT,&value); // 读到的value从0开始, 0表示第1个input源
int value = 0; // 0表示第1个input源
ioctl(h->fd,VIDIOC_S_INPUT,&value)
2.1.3 其他参数
如果每一参数都提供一系列的ioctl cmd,那使用起来很不方便。
对于这些参数,APP使用对应ID来选中它,然后使用VIDIOC_QUERYCTRL(查询控制)、VIDIOC_G_CTRL(获得当前设置控制)、VIDIOC_S_CTRL(设置控制)来操作它。
不同参数的ID值不同。
以亮度Brightness为例,有如下调用方法:
- 查询:
struct v4l2_queryctrl qctrl;
memset(&qctrl, 0, sizeof(qctrl));
qctrl.id = V4L2_CID_BRIGHTNESS; // V4L2_CID_BASE+0;
ioctl(fd, VIDIOC_QUERYCTRL, &qctrl);
/* Used in the VIDIOC_QUERYCTRL ioctl for querying controls */
struct v4l2_queryctrl {
__u32 id;
__u32 type; /* enum v4l2_ctrl_type */
__u8 name[32]; /* Whatever */
__s32 minimum; /* Note signedness */
__s32 maximum;
__s32 step;
__s32 default_value;
__u32 flags;
__u32 reserved[2];
};
- 获得当前值
struct v4l2_control c;
c.id = V4L2_CID_BRIGHTNESS; // V4L2_CID_BASE+0;
ioctl(h->fd, VIDIOC_G_CTRL, &c);
/*
* C O N T R O L S
*/
struct v4l2_control {
__u32 id;
__s32 value;
};
- 设置
struct v4l2_control c;
c.id = V4L2_CID_BRIGHTNESS; // V4L2_CID_BASE+0;
c.value = 99;
ioctl(h->fd, VIDIOC_S_CTRL, &c);
2.2 理解接口
2.2.1 概念
以USB摄像头为例,它的内部结构如下:
- 一个USB摄像头必定有一个VideoControl接口,用于控制。
- 有0个或多个VideoStreaming接口,用于传输视频。
摄像头使用流程中的
ioctl VIDIOC_STREAMON:启动摄像头
和ioctl VIDIOC_STREAMOFF:关闭摄像头
都是对VideoStreaming Interface的操控
在VideoControl内部,有多个实体Entity,即Unit或Terminal,CT、IT、SU、PU、OT称为Unit。上一个Unit或Terminal的数据,流向下一个Unit或Terminal,多个Unit或Terminal组成一个完整的UVC功能设备。
- 只有一个输出引脚
- 可以Fan-out,不能Fan-in
-
Terminal:位于边界,用于联通外界。有:IT(Input Terminal)、OT(Output Terminal)、CT(Camera Terminal)。模型如下,有一个输出引脚:
IT不关心后续输出,OT不关心前面输入
-
Unit:位于VideoControl内部,用来进行各种控制
-
SU:Selector Unit(选择单元),从多路输入中选择一路,比如设备支持多种输入源,可以通过SU进行选择切换。模型如下
-
PU:Porocessing Unit(处理单元),用于调整亮度、对比度、色度等,有如下控制功能:
- User Controls
- Brightness 背光
- Hue 色度
- Saturation 饱和度
- Sharpness 锐度
- Gamma 伽马
- Digital Multiplier (Zoom) 数字放大
- Auto Controls
- White Balance Temperature 白平衡色温
- White Balance Component 白平衡组件
- Backlight Compensation 背光补偿
- Contrast 对比度
- Other
- Gain 增益
- Power Line Frequency 电源线频率
- Analog Video Standard 模拟视频标准
- Analog Video Lock Status 模拟视频锁状态
- 模型如下
- User Controls
-
EU:Encoding Unit(编码单元),对采集所得的数据进行个性化处理的功能。编码单元控制编码器的属性,该编码器对通过它流式传输的视频进行编码。它具有如下功能:
-
模型如下
-
-
XU:Extension Unit(扩展单元),厂家可以在XU上提供自定义的操作,模型如下:
2.2.2 操作方法
我们使用ioctl操作设备节点"/dev/video0"时,不同的ioctl操作的可能是VideoControl接口,或者VideoStreaming接口。
跟视频流相关的操作,比如:VIDIOC_ENUM_FMT、VIDIOC_G_FMT、VIDIOC_S_FMT、VIDIOC_STREAMON、VIDIOC_STREAMOFF,是操作VideoStreaming接口。
其他ioctl,大多都是操作VideoControl接口。
从底层驱动和硬件角度看,要操作VideoControl接口,需要指明:
- entity:你要操作哪个Terminal或Unit,比如PU
- Control Selector:你要操作entity里面的哪个控制项?比如亮度PU_BRIGHTNESS_CONTROL
- 控制项里哪些位:比如CT(Camera Terminal)里的CT_PANTILT_RELATIVE_CONTROL控制项对应32位的数据,其中前16位对应PAN控制(左右转动),后16位对应TILE控制(上下转动)
但是APP不关注这些细节,使用一个ID来指定entity、Control Selector、哪些位:
/*
* C O N T R O L S
*/
struct v4l2_control {
__u32 id;
__s32 value;
};
驱动程序里,会解析APP传入的ID,找到entity、Control Selector、哪些位。
自己扩展Unit:
3. 应用程序编写
用的USB摄像头
二、V4L2驱动
在任何APP中都是使用open/read/write
,而在驱动中就提供相应的drv_open/read/write
,以面向对象的思想来看,将drv_open/read/write
集合到file_operations
中并且集合成一个接口文件,根据次设备号找到连接对应的硬件相关层
1. 整体框架
- 字符设备驱动程序的核心是:file_operations结构体
- V4L2设备驱动程序的核心是:
video_device结构体
,它里面有2大成员- v4l2_file_operations结构体:实现具体的open/read/write/ioctl/mmap操作
- v4l2_ioctl_ops结构体:v4l2_file_operations结构体一般使用video_ioctl2函数,它要调用v4l2_ioctl_ops结构体
1.1注册流程
- 初始化一个video_device结构体,并设置release、fops、ioctl_fops
- 分配video_device结构体
- 初始化一个v4l2_device结构体,并注册
- 建立video_device和v4l2_device的联系
- 注册video_device结构体
参考drivers\media\usb\airspy\airspy.c
:
static struct video_device airspy_template = {
.name = "AirSpy SDR",
.release = video_device_release_empty,
.fops = &airspy_fops,
.ioctl_ops = &airspy_ioctl_ops,
};
// 分配/设置video_device结构体
s->vdev = airspy_template;
// 初始化一个v4l2_device结构体(起辅助作用)
/* Register the v4l2_device structure */
s->v4l2_dev.release = airspy_video_release;
ret = v4l2_device_register(&intf->dev, &s->v4l2_dev);
// video_device和4l2_device建立联系
s->vdev.v4l2_dev = &s->v4l2_dev;
// 注册video_device结构体
ret = video_register_device(&s->vdev, VFL_TYPE_SDR, -1);
__video_register_device
// 根据次设备号把video_device结构体放入数组
video_device[vdev->minor] = vdev;
// 注册字符设备驱动程序
vdev->cdev->ops = &v4l2_fops;
vdev->cdev->owner = owner;
ret = cdev_add(vdev->cdev, MKDEV(VIDEO_MAJOR, vdev->minor), 1);
1.2 ioctl调用流程分析
1.2.1 两类ioctl
底层驱动程序提供了很多ioctl的处理函数,比如: