2.1.1.3 节_摄像头驱动程序彻底分析

目录

1 修复上节编译 vivi 遗留的 Bug

2 根据虚拟驱动 vivi 的使用过程彻底分析摄像头驱动

A:根据 strace 命令获取应用程序的系统调用

B:结合 xawtv 源码分析应用程序系统调用过程

总结: xawtv 的几大函数:

C:裁剪系统调用

总结: 11 个必须实现的 ioctl

D:分析应用程序获取摄像头数据的过程

3: 怎么写摄像头驱动程序:

v4l2 编程接口(一) — ioctl:

1、 struct v4l2_capability 与 VIDIOC_QUERYCAP


1 修复上节编译 vivi 遗留的 Bug

1、上一节测试的摄像头驱动程序的缺陷
(1)依次装载驱动程序,出现错误如下

2)用 dmesg 命令查看详细输出(某些函数没有识别,如下图列举的函数),可见我们的 vivi.ko 还依赖于其他驱动程序,
上一节直接使用这些命令没有问题,是因为在做虚拟摄像头 vivi 之前。我们先接上 USB 摄像头, ubuntu 里面自动给我们安装
了其他驱动程序。所以在使用 vivi 的时候,没有出现问题。

3) sudo modprobe vivi
这是安装 ubuntu 里面自带的 vivi 驱动程序,把它依赖的其他驱动程序也一起安装进来
(4)把内核自带的 vivi 去掉,安装上我们编译出来的 vivi.ko,用自己的编译程序是因为
我们以后做实验的时候要用到自己编译的代码,装载新的驱动成功后可以在 dev 下面发现我们新装载的设备 video0

2 根据虚拟驱动 vivi 的使用过程彻底分析摄像头驱动

这一节我们希望了解应用程序是如何调用我们的驱动程序来设置我们摄像头,并是如何去获取的到摄像头的数据的。
所以我们就根据应用程序去分析他的程序,至于应用程序的源码我们 xawtv 我们已经可以获得,我们可以一步步从 main 函数
开始分析,但是 xawtv 中并非只有关于摄像头的内容,所以这样分析会比较复杂,我们可以通过启动摄像头的应用程序,然后
获取摄像头在启动过程中的系统调用,从而可以快速分析到相关的内容。 这里要注意的是我们的应用程序和我们的 xawtv 源码
并不是完全匹配的,所以或得到的通过启动应用程序获得到的系统调用结合 xawtv 源码会有差异
这是基于 linux 内核 2.6.31.14 分析的

A:根据 strace 命令获取应用程序的系统调用

(1)分析使用过程,通过应用程序源码 xawtv 分析,建立 sourceinsight 工程
(2)快捷了解 xawtv 所涉及的调用,用 strace 工具获得所涉及的调用。把 xawtv 所涉及的系统调用记录在 xawtv.log 文件
里面。

接下来分析 xawtv.log

(3)打开 xawtv.log 文件,搜索关键词“/dev/video0”,结合内核版本的 vivi.c 来分析
由于我们刚刚在装载驱动的时候检查了下接入设备的设备名称就是/dev/video0,所以在 log 中去搜索/dev/video0
搜索到之后我们把对摄像头的系统调用提取出来保存到“xawtv 涉及的 vivi 驱动的系统调用.txt

B:结合 xawtv 源码分析应用程序系统调用过程


接下来我们去结合 xawtv 源码来分析这些系统调用在源码中是在应用程序中哪些函数中调用的
对于 xawtv 涉及的 vivi 驱动的系统调用.txt 中提取出来的系统调用逐个去查找对应源码
比如: xawtv 涉及的 vivi 驱动的系统调用.txt 最开始 open 的系统调用我们不好查找,直接看 open 下面紧接着的
VIDIOC_QUERYCAP 系统调用。

搜索后发现

刚好 VIDIOC_QUERYCAP 就在 open 函数中。
最终结合源码所有的系统调用对应在源码中的调用关系都如下:

// 1~7都是在v4l2_open里调用
1. open
2. ioctl(4, VIDIOC_QUERYCAP

// 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. 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        
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})
            ioctl(4, VIDIOC_DQBUF                // de-queue, 把缓冲区从队列中取出
            // 处理, 之以已经通过mmap获得了缓冲区的地址, 就可以直接访问数据        
            ioctl(4, VIDIOC_QBUF                 // 把缓冲区放入队列
        }



// 3~7 都是在 xawtv 源码中的 get_device_capabilities 函数(获得相关属性)里调用


// 8~10 都是通过 v4l2_read_attr 来调用的 ,我们发现源码中的如下这三种系统调用的顺序和之前日志中显示不一样,因为
代码中是 if else 结构,并不一定是顺序执行

总结: xawtv 的几大函数:

1. v4l2_open
2. v4l2_read_attr/v4l2_write_attr //读写属性
3. v4l2_start_streaming //启动流
4. v4l2_nextframe/v4l2_waiton //
 

C:裁剪系统调用

上面结合源码分析了应用程序是如何调用我们的系统调用的,最终发现有很多系统调用,就意味着我们的驱动就要实现相对应
的系统调用函数,那么这些系统调用是不是都是必须的呢?我们接下来结合 vivi.c 和刚才的分析结果一个个分析,看看能不能
裁剪
1、一边修改 vivi.c 一边测试
(1)执行 xawtv

出现以下画面

(2)点击画面右键出现属性菜单

上图是未经过裁剪的系统调用时应用程序对应可以获取,设置的操作界面
我们之前知道当我们应用程序调用对应的 ioctl 的时候就会调用 vivi.c 中的 vivi_ioctl_ops

而根据不同的 ioctl 命令就会调用 vivi_ioctl_ops 中对应的操作函数,具体的过程我们可以通过看到 1.1.2 节中的应用层调用
ioctl 的过程(最终调用__video_do_ioctl(v4l2-ioctl.c 文件中) 函数来根据不同的 cmd 来找到 vivi_ioctl_ops 对应的函数)

最终经过验证和裁剪或得到了如下 11 个必须的 ioctl

总结: 11 个必须实现的 ioctl

摄像头驱动程序必需的 11 个 ioctl: /drivers/media/video/vivi.c -> vivi_ioctl_ops(结构体),修改测试得知:
 

.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,

D:分析应用程序获取摄像头数据的过程


我们知道我们摄像头的数据都会保存在缓冲区中,所以在之前分析应用程序的系统调用中对数据的获取过程一般过程就是:
先请求分配缓冲区,然后将内核分配的缓冲区映射到应用层,以便应用层来操作缓冲区中的数据,之后将分配好的缓冲区加入
缓冲区队列中(要求分配的缓冲区可能有多个) ,之后就可以启动摄像头,然后不断查询每个缓冲区中是否有数据,查到哪个
缓冲区中有数据之后就将该缓冲区从缓冲区队列中取出来,然后来操作缓冲区中的数据。

1. 请求分配缓冲区: ioctl(4, VIDIOC_REQBUFS          // 请求系统分配缓冲区
                        videobuf_reqbufs(队列, v4l2_requestbuffers) // 队列在open函数用videobuf_queue_vmalloc_init初始化
                        // 注意:这个IOCTL只是分配缓冲区的头部信息,真正的缓存还没有分配呢
               //在驱动程序有一条原则,这些资源只有在我们用到的时候才进行分配
2. 查询映射缓冲区:
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);   // 在这里才给缓冲区分配空间

3. 把缓冲区放入队列:
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);           // 调用驱动程序提供的"入队列函数"
        

4. 启动摄像头
ioctl(4, VIDIOC_STREAMON
    videobuf_streamon
        q->streaming = 1;
        

5. 用select查询是否有数据
          // 驱动程序里必定有: 产生数据、唤醒进程
          v4l2_poll
                vdev->fops->poll
                    vivi_poll   
                        videobuf_poll_stream
                            // 从队列的头部获得缓冲区
                            buf = list_entry(q->stream.next, struct videobuf_buffer, stream);
                            
                            // 如果没有数据则休眠,在buf->done这里进行休眠                            
                            poll_wait(file, &buf->done, wait);

    谁来产生数据、谁来唤醒它?
    内核线程vivi_thread每30MS执行一次,它调用
    vivi_thread_tick
        vivi_fillbuff(fh, buf);  // 构造数据 
        wake_up(&buf->vb.done);  // 唤醒进程
          
6. 有数据后从队列里取出缓冲区
// 有那么多缓冲区,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);
        
7. 应用程序根据VIDIOC_DQBUF所得到缓冲区状态,知道是哪一个缓冲区有数据
   就去读对应的地址(该地址来自前面的mmap)

总结数据获取过程

===> vivi.c 缓冲区操作过程:
①VIDIOC_REQBUFS(分配头部信息)
->②VIDIOC_QUERYBUF(返回属性) / mmap(映射地址(把缓存映射到应用层空间,应用层就可以直接操作这块缓存),分配
实际空间)
->③ VIDIOC_QBUF(把缓冲区放入队列)
->④VIDIOC_STREAMON(启动摄像头)
->⑤用 select 查询是否有数据:在队列头一个 buf 上操作
->⑥ VIDIOC_DQBUF( 返回队列头的 buf 并从队列中删除)
->⑦ VIDIOC_DQBUF(重新放回队列-③)

3: 怎么写摄像头驱动程序:

1. 分配结构体: video_device:video_device_alloc
2. 设置
        .fops
        .ioctl_ops (里面需要设置 11 项)
        如果要用内核提供的缓冲区操作函数,还需要构造一个 videobuf_queue_ops
3. 注册: video_register_device

v4l2 编程接口(一) — ioctl:

在应用程序获取视频数据的流程中,都是通过 ioctl 命令与驱动程序进行交互,常见的 ioctl 命令有:

我们可以在 videodev2.h 中找到:

1、 struct v4l2_capability 与 VIDIOC_QUERYCAP

查询 V4L2 功能集: VIDIOC_QUERYCAP
struct v4l2_capability cap;
int rel = 0;
ioctl(Handle, VIDIOC_QUERYCAP, &cap);

使用 ioctl VIDIOC_QUERYCAP 来查询当前 driver 是否合乎规范。因为 V4L2 要求所有 driver 和 Device 都支持这个 Ioctl。
所以,可以通过这个 ioctl 是否成功来判断当前设备和 dirver 是否支持 V4L2 规范。当然,这样同时还能够得到设备足够的能
力信息。
。。。。。。。。。。。

发布了241 篇原创文章 · 获赞 28 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_34738528/article/details/105072060