硬 件 :电脑、 JZ2440 开发板、 UVC 摄像头(推荐使用) 系 统 : Ubuntu9.10// 即光盘提供的那个虚拟机系统
Uboot : u-boot-2012.04.01 // 即毕业班移植新 uboot 那个
Kernel : linux-3.4.2 // 即毕业班移植新内核那个文件系统:
fs_mini_mdev_new.tar.bz2
① UVC 指 USB Video Class, UVC 摄像头的简单判断标准就是接到 Windows 电脑上后,不用安装驱动
程序就可以使用
② USB 摄像头输出的数据有多种格式,比如原始数据 RGB 或 YUV 格式,压缩的 MJPEG 格式。使用
网络传输视频时,应该传输压缩格式的,否则传输的数据量会非常大。
S3C2440 的主频只有 400M,如果使用的摄像头不支持 MJPEG 输出,就需要使用软件(mjpg-streamer)
来压缩,这极大的耗费 CPU 资源,导致远程视频不流畅
视频监控的Linux底层驱动程序属于字符设备驱动,回顾字符设备驱动:
这种分层的结构除了可以让我们更加专注于硬件相关的代码外,还可以让上层应用程序有一套统一的调用接口。
上图中所说的某结构体,指的是针对于某个具体的驱动的具体结构体,对于LCD来说是fb_info结构体,对于视频来说又是另外一套框架和结构体:
uvc_init -->uvc_driver --> uvc_probe
uvc_probe
v4l2_device_register // 不重要
uvc_register_chains
uvc_register_terms
uvc_register_video
video_device_alloc
video_register_device //重要的注册函数
__video_register_device
//最终调用到这个函数 (该函数位于:v4l-dev.c 这正是v4l2 框架的核心层)
其中,video_register_device 是重要的部分。
从 video_register_device 可以反推出哪个部分时核心层:
对于V4L2框架的细节部分,可以参看:linux-3.4.2_jz2440\Documentation\video4linux\v4l2-framework.txt
通过分析 vivi.c(Virtual Video driver) 来深入理解视频驱动框架:
1.分配video_device
2.设置
3.注册:video_register_device
vivi_init
vivi_create_instance
v4l2_device_register // 不是主要的, 只是用于初始化一些东西,比如自旋锁、引用计数,这里改为v4l2_device_init更为合适
vfd=video_device_alloc:
struct video_device *vfd;
*vfd = vivi_template;
// 设置
1. vfd:
.fops = &vivi_fops,
.ioctl_ops = &vivi_ioctl_ops,
.release = video_device_release,
2.
vfd->v4l2_dev = &dev->v4l2_dev;
// v4l2_device_register(NULL, &dev->v4l2_dev);
//该函数中没有做什么实质性的事,只是用于初始化一些东西,比如自旋锁、引用计数,这里改为v4l2_device_init更为合适
//因此这里的 struct v4l2_device v4l2_dev 不是最主要的
重要:3. 设置"ctrl属性"(用于APP的ioctl):
v4l2_ctrl_handler_init(hdl, 11);
dev->volume = v4l2_ctrl_new_std(hdl, &vivi_ctrl_ops,
V4L2_CID_AUDIO_VOLUME, 0, 255, 1, 200);
dev->brightness = v4l2_ctrl_new_std(hdl, &vivi_ctrl_ops,
V4L2_CID_BRIGHTNESS, 0, 255, 1, 127);
dev->contrast = v4l2_ctrl_new_std(hdl, &vivi_ctrl_ops,
V4L2_CID_CONTRAST, 0, 255, 1, 16);
``````
video_register_device(video_device, type:VFL_TYPE_GRABBER, nr)
__video_register_device
vdev->cdev = cdev_alloc();
vdev->cdev->ops = &v4l2_fops;
cdev_add()
video_device[vdev->minor] = vdev; //以次设备号为下标,将vdev存放到数组中
if (vdev->ctrl_handler == NULL) //设置ctrl_handler 结构体
vdev->ctrl_handler = vdev->v4l2_dev->ctrl_handler;
分析vivi.c的open,read,write,ioctl过程
1. open
app: open("/dev/video0",....)
---------------------------------------------------
drv: v4l2_fops.v4l2_open //vdev->cdev->ops = &v4l2_fops 中的open函数
vdev = video_devdata(filp); // 根据次设备号从数组中得到video_device 之后就可以使用video_device结构体所提供的各种函数了
// video_register_device 函数中会将video设备放入数组中
ret = vdev->fops->open(filp);
vivi_fops->open = v4l2_fh_open
2. read
app: read ....
---------------------------------------------------
drv: v4l2_fops.v4l2_read //vdev->cdev->ops = &v4l2_fops中的read函数
struct video_device *vdev = video_devdata(filp);
ret = vdev->fops->read(filp, buf, sz, off); // vivi_fops -> v4l2_fh_open
3. ioctl //较为复杂
app: ioctl
----------------------------------------------------
drv: v4l2_fops.unlocked_ioctl = v4l2_ioctl //vdev->cdev->ops = &v4l2_fops中的ioctl函数
实际上最终调用到:v4l2_ioctl
struct video_device *vdev = video_devdata(filp);
ret = vdev->fops->unlocked_ioctl(filp, cmd, arg);
vivi.c:video_ioctl2
video_usercopy(file, cmd, arg, __video_do_ioctl); // 从用户空间拷贝命令参数,然后调用__video_do_ioctl
__video_do_ioctl :
struct video_device *vfd = video_devdata(file); //同理都是先根据次设备号得到video_device结构体
根据APP传入的cmd来获得、设置"某些属性"
__video_do_ioctl
struct video_device *vfd = video_devdata(file);
case VIDIOC_QUERYCTRL:
{
struct v4l2_queryctrl *p = arg;
if (vfh && vfh->ctrl_handler)
ret = v4l2_queryctrl(vfh->ctrl_handler, p);
else if (vfd->ctrl_handler) // 在哪设置?在video_register_device
ret = v4l2_queryctrl(vfd->ctrl_handler, p);
// 根据ID在ctrl_handler里找到v4l2_ctrl,返回它的值
准备工作:安装xawtv
sudo apt-get install xawtv
源码xawtv-3.95.tar.gz: http://www.kraxel.org/releases/xawtv/
在这个网站创建新的sources.list
http://repogen.simplylinux.ch/
① 确实ubuntu的内核版本
uname -a
Linux book-desktop 2.6.31-14-generic #48-Ubuntu SMP Fri Oct 16 14:04:26 UTC 2009 i686 GNU/Linux
② 去www.kernel.org下载同版本的内核
解压后把drivers/media/video目录取出后放到服务器上面去(Ubuntu9.10)
KERN_DIR = /usr/src/linux-headers-2.6.31-14-generic
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += vivi.o
obj-m += videobuf-core.o
obj-m += videobuf-vmalloc.o
obj-m += v4l2-common.o
③ make (只加入obj-m += vivi.o时报错)
解决方法:在内核中搜索这些函数所在的c文件并将这些文件编译成内核模块:
④ insmod videobuf-core.ko
insmod videobuf-vmalloc.ko
insmod v4l2-common.ko
insmod vivi.ko
⑤ ls /dev/video*
⑥ xawtv -c /dev/videoX
在事先没有连接USB摄像头时,装载驱动程序会报错,是因为在连接了USB摄像头后,Linux会自动安装一些驱动程序。
解决方法:
下面分析xawtv 源码:
直接分析源码很麻烦,可以用strace工具来分析xawtv用到了哪些系统调用(open、read、write),就知道xawtv应用程序做了那些事:
//这部分代码是应用程序中读出来的,但是跟源码没有对应关系,说明应用程序跟源码还是有差别的
3. ioctl(4, VIDIOC_G_FMT
4. for()
ioctl(4, VIDIOC_ENUM_FMT
5. ioctl(4, VIDIOC_QUERYCAP // 列举性能
6. ioctl(4, VIDIOC_G_INPUT // 获得当前使用输入源
7. ioctl(4, VIDIOC_ENUMIN PUT // 列举输入源
8. ioctl(4, VIDIOC_QUERYCTRL // 查询属性,比如亮度、对比度
9. ioctl(4, VIDIOC_QUERYCAP
10. ioctl(4, VIDIOC_ENUMINPUT
xawtv源码中对应的ioctl操作:
// 1~7都是在v4l2_open里调用
1. open
2. ioctl(4, VIDIOC_QUERYCAP //open之后就开始查询性能,必不可少
// 3~7 都是在get_device_capabilities里调用
3. for()
ioctl(4, VIDIOC_ENUMINPUT // 列举输入源,VIDIOC_ENUMINPUT/VIDIOC_G_INPUT/VIDIOC_S_INPUT不是必需的
4. for()
ioctl(4, VIDIOC_ENUMSTD // 列举标准(制式), 不是必需的
5. for()
ioctl(4, VIDIOC_ENUM_FMT // 列举所支持的格式
6. ioctl(4, VIDIOC_G_PARM //获得参数
7. for()
ioctl(4, VIDIOC_QUERYCTRL // 查询属性(比如说亮度值最小值、最大值、默认值)
// 8~10都是通过v4l2_read_attr(读取属性)来调用的
8. ioctl(4, VIDIOC_G_STD // 获得当前使用的标准(制式), 不是必需的
9. ioctl(4, VIDIOC_G_INPUT
10. ioctl(4, VIDIOC_G_CTRL // 获得当前属性, 比如亮度是多少
// 11~12在v4l2_overlay中调用,暂时没有用到该功能
11. ioctl(4, VIDIOC_TRY_FMT // 试试能否支持某种格式
12. ioctl(4, VIDIOC_S_FMT // 如果支持的话就设置摄像头使用某种格式
// 13~16在v4l2_start_streaming
13. ioctl(4, VIDIOC_REQBUFS // 请求系统分配缓冲区
14. for()
ioctl(4, VIDIOC_QUERYBUF // 查询所分配的缓冲区
mmap //调用mmap来映射各个缓冲区的地址
//之后将所有的buf都放入队列中:v4l2_queue_all
15. for ()
ioctl(4, VIDIOC_QBUF // 把缓冲区放入队列
16. ioctl(4, VIDIOC_STREAMON // 启动摄像头
// 17里都是通过v4l2_write_attr来调用的
17. for ()
ioctl(4, VIDIOC_S_CTRL // 设置属性
ioctl(4, VIDIOC_S_INPUT // 设置输入源
ioctl(4, VIDIOC_S_STD // 设置标准(制式), 不是必需的
// v4l2_nextframe > v4l2_waiton
18. v4l2_queue_all
v4l2_waiton
for ()
{
select(5, [4], NULL, NULL, {5, 0}) = 1 (in [4], left {4, 985979}) //查询是否有数据
//如果一旦驱动程序有了数据,它就会将应用程序唤醒,唤醒之后应用程序调用DQBUF把数据取出获取buf的信息
ioctl(4, VIDIOC_DQBUF // de-queue, 一旦有数据就把缓冲区从队列中取出
// 处理, 之以已经通过mmap获得了缓冲区的地址, 就可以直接访问数据
ioctl(4, VIDIOC_QBUF // 把缓冲区放入队列
}
v4l2_open
打开摄像头设备,获取其性能参数
v4l2_read_attr/v4l2_write_attr
v4l2_start_streaming
其中会请求buf并mmap buf
v4l2_nextframe --> v4l2_waiton
v4l2_waiton 函数来等待查询摄像头数据。
接下来要做的是将众多的ioctl进行精简,留下最少的部分完成摄像头应用程序的基本功能:修改vivi.c
应用程序 --> ioctl --> video_ioctl2 (根据ioctl不同的CMD)–> v4l2_ioctl_ops 中的对应函数
摄像头驱动程序必需的11个ioctl:
// 表示它是一个摄像头设备
.vidioc_querycap = vidioc_querycap,
/* 用于列举、获得、测试、设置摄像头的数据的格式 */
.vidioc_enum_fmt_vid_cap = vidioc_enum_fmt_vid_cap,
.vidioc_g_fmt_vid_cap = vidioc_g_fmt_vid_cap,
.vidioc_try_fmt_vid_cap = vidioc_try_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,
ioctl(4, VIDIOC_REQBUFS // 请求系统分配缓冲区
videobuf_reqbufs(队列, v4l2_requestbuffers) // 队列在open函数用videobuf_queue_vmalloc_init初始化
// 注意:这个IOCTL只是分配缓冲区的头部信息,真正的缓存还没有分配呢
ioctl(4, VIDIOC_QUERYBUF // 查询所分配的缓冲区
videobuf_querybuf // 获得缓冲区的数据格式、大小、每一行长度、高度(此时还未分配缓存)
mmap(参数里有"大小") // 在这里才分配缓存
v4l2_mmap
vivi_mmap
videobuf_mmap_mapper
videobuf-vmalloc.c里的__videobuf_mmap_mapper
mem->vmalloc = vmalloc_user(pages); // 在这里才给缓冲区分配空间
ioctl(4, VIDIOC_QBUF // 把缓冲区放入队列
videobuf_qbuf
q->ops->buf_prepare(q, buf, field); // 调用驱动程序提供的函数做些预处理
list_add_tail(&buf->stream, &q->stream); // 把缓冲区放入队列的尾部
q->ops->buf_queue(q, buf); // 调用驱动程序提供的"入队列函数"
ioctl(4, VIDIOC_STREAMON
videobuf_streamon
q->streaming = 1;
// 驱动程序里必定有: 产生数据、唤醒进程
v4l2_poll
vdev->fops->poll
vivi_poll
videobuf_poll_stream
// 从队列的头部获得缓冲区
buf = list_entry(q->stream.next, struct videobuf_buffer, stream);
// 如果没有数据则休眠
poll_wait(file, &buf->done, wait);
谁来产生数据、谁来唤醒它?
内核线程vivi_thread每30MS执行一次,它调用
vivi_thread_tick //产生数据
vivi_fillbuff(fh, buf); // 构造数据
wake_up(&buf->vb.done); // 唤醒进程
注意:真实的摄像头是硬件摄像头产生数据,而vivi虚拟摄像头是由一个内核线程来产生数据:
// 有那么多缓冲区,APP如何知道哪一个缓冲区有数据?调用VIDIOC_DQBUF
ioctl(4, VIDIOC_DQBUF
vidioc_dqbuf
// 在队列里获得有数据的缓冲区
retval = stream_next_buffer(q, &buf, nonblocking);
// 把它从队列中删掉
list_del(&buf->stream);
// 把这个缓冲区的状态返回给APP
videobuf_status(q, b, buf, q->type);
UVC: USB Video Class
UVC驱动:drivers\media\video\uvc\
id_table 表示能够支持的所有USB设备,probe函数是当接入所能支持的设备后调用的函数,在probe函数中就会像上述描述的那样进行有关视频的设置。
uvc_driver.c分析:
1. usb_register(&uvc_driver.driver);
2. uvc_probe
uvc_register_video
vdev = video_device_alloc();
vdev->fops = &uvc_fops;
video_register_device
在www.usb.org下载 uvc specification,
UVC 1.5 Class specification.pdf : 有详细描述
USB_Video_Example 1.5.pdf : 有示例
通过VideoControl Interface来控制(比如亮度、白平衡等参数的控制)
通过VideoStreaming Interface来读视频数据,
VC里含有多个Unit/Terminal等功能模块,可以通过访问这些模块进行控制,比如调亮度
const struct v4l2_file_operations uvc_fops = {
.owner = THIS_MODULE,
.open = uvc_v4l2_open,
.release = uvc_v4l2_release,
.ioctl = uvc_v4l2_ioctl,
.read = uvc_v4l2_read,
.mmap = uvc_v4l2_mmap,
.poll = uvc_v4l2_poll,
};
之后一个一个来跟踪ioctl函数的操作过程:当应用程序调用到ioctl函数时就会调用到uvc_fops 结构体中的uvc_v4l2_ioctl函数:
(整个过程跟xawtv打开vivi虚拟摄像头的原理类似)
uvc_v4l2_ioctl函数中的video_usercopy函数就是将应用程序传来的cmd参数拷贝至内核态然后调用uvc_v4l2_do_ioctl函数。
uvc_v4l2_do_ioctl函数中有一系列的ioctl的调用。
2. VIDIOC_QUERYCAP // video->streaming->type 应该是在设备被枚举时分析描述符时设置的
if (video->streaming->type == V4L2_BUF_TYPE_VIDEO_CAPTURE)
cap->capabilities = V4L2_CAP_VIDEO_CAPTURE
| V4L2_CAP_STREAMING;
else
cap->capabilities = V4L2_CAP_VIDEO_OUTPUT
| V4L2_CAP_STREAMING;
3. VIDIOC_ENUM_FMT // format数组应是在设备被枚举时设置的
format = &video->streaming->format[fmt->index];
4. VIDIOC_G_FMT
uvc_v4l2_get_format // USB摄像头支持多种格式fromat, 每种格式下有多种frame(比如分辨率)
struct uvc_format *format = video->streaming->cur_format;
struct uvc_frame *frame = video->streaming->cur_frame;
5. VIDIOC_TRY_FMT
uvc_v4l2_try_format
/* Check if the hardware supports the requested format. */
/* Find the closest image size. The distance between image sizes is
* the size in pixels of the non-overlapping regions between the
* requested size and the frame-specified size.
*/
6. VIDIOC_S_FMT // 只是把参数保存起来,还没有发给USB摄像头
uvc_v4l2_set_format
uvc_v4l2_try_format
video->streaming->cur_format = format;
video->streaming->cur_frame = frame;
7. VIDIOC_REQBUFS //请求缓冲区
uvc_alloc_buffers
for (; nbuffers > 0; --nbuffers) {
mem = vmalloc_32(nbuffers * bufsize);
if (mem != NULL)
break;
}
8. VIDIOC_QUERYBUF //查询buf参数
uvc_query_buffer
__uvc_query_buffer
memcpy(v4l2_buf, &buf->buf, sizeof *v4l2_buf); // 复制参数
9. mmap
uvc_v4l2_mmap
10. VIDIOC_QBUF //数据入队列
uvc_queue_buffer
list_add_tail(&buf->stream, &queue->mainqueue);
list_add_tail(&buf->queue, &queue->irqqueue);
11. VIDIOC_STREAMON
uvc_video_enable(video, 1) // 把所设置的参数发给硬件,然后启动摄像头
/* Commit the streaming parameters. */ 把参数提交给硬件设备
uvc_commit_video
uvc_set_video_ctrl /* 设置格式fromat, frame */
ret = __uvc_query_ctrl(video->dev /* 哪一个USB设备 */, SET_CUR, 0,
video->streaming->intfnum /* 哪一个接口: VS 接口*/,
probe ? VS_PROBE_CONTROL : VS_COMMIT_CONTROL, data, size,
uvc_timeout_param);
/* 启动:Initialize isochronous/bulk URBs and allocate transfer buffers. */
uvc_init_video(video, GFP_KERNEL);
uvc_init_video_isoc / uvc_init_video_bulk
urb->complete = uvc_video_complete; (收到数据后此函数被调用,它又调用video->decode(urb, video, buf); ==> uvc_video_decode_isoc/uvc_video_encode_bulk => uvc_queue_next_buffer => wake_up(&buf->wait);)
usb_submit_urb
// 当应用程序将一个buf放入队列之后,调用streamon 来启动数据传输,在streamon 中做了一些初始化。
之后当应用程序调用poll函数来查询是否数据已经就绪,这时就会有poll_wait 在这里休眠等待数据,
当USB驱动程序获得了数据之后,每个USB请求块(urb)完成之后,其uvc_video_complete 函数被调用,
这个complete函数最终就会调用到wake_up 函数将应用程序唤醒。
12. poll //poll函数返回以后,数据就可以使用了,接下来就是从队列中取出数据
uvc_v4l2_poll
uvc_queue_poll
poll_wait(file, &buf->wait, wait); // 休眠等待有数据
13. VIDIOC_DQBUF
uvc_dequeue_buffer
list_del(&buf->stream);
14. VIDIOC_STREAMOFF
uvc_video_enable(video, 0);
usb_kill_urb(urb);
usb_free_urb(urb);
分析设置亮度过程:
ioctl: VIDIOC_S_CTRL
uvc_ctrl_set
uvc_ctrl_commit
__uvc_ctrl_commit(video, 0);
uvc_ctrl_commit_entity(video->dev, entity, rollback);
ret = uvc_query_ctrl(dev /* 哪一个USB设备 /, SET_CUR, ctrl->entity->id / 哪一个unit/terminal /,
dev->intfnum / 哪一个接口: VC interface */, ctrl->info->selector,
uvc_ctrl_data(ctrl, UVC_CTRL_DATA_CURRENT),
ctrl->info->size);
UVC设备有2个interface: VideoControl Interface, VideoStreaming Interface
VideoControl Interface用于控制,比如设置亮度。它内部有多个Unit/Terminal(在程序里Unit/Terminal都称为entity)
可以通过类似的函数来访问:
ret = uvc_query_ctrl(dev /* 哪一个USB设备 /, SET_CUR, ctrl->entity->id / 哪一个unit/terminal /,
dev->intfnum / 哪一个接口: VC interface */, ctrl->info->selector,
uvc_ctrl_data(ctrl, UVC_CTRL_DATA_CURRENT),
ctrl->info->size);
VideoStreaming Interface用于获得视频数据,也可以用来选择fromat/frame(VS可能有多种format, 一个format支持多种frame, frame用来表示分辨率等信息)
可以通过类似的函数来访问:
ret = __uvc_query_ctrl(video->dev /* 哪一个USB设备 /, SET_CUR, 0,
video->streaming->intfnum / 哪一个接口: VS */,
probe ? VS_PROBE_CONTROL : VS_COMMIT_CONTROL, data, size,
uvc_timeout_param);
我们在设置FORMAT时只是简单的使用video->streaming->format[fmt->index]等数据,
这些数据哪来的?
应是设备被枚举时设置的,也就是分析它的描述符时设置的。
UVC驱动的重点在于:
描述符的分析
属性的控制: 通过VideoControl Interface来设置
格式的选择:通过VideoStreaming Interface来设置
数据的获得:通过VideoStreaming Interface的URB来获得