ov9650摄像头驱动之——linux内核v4l2架构分析1
本系列准备分为3-4篇来讲,因为说的太多会比较乱
v4l2视频驱动主要涉及几个知识点:
摄像头方面的知识(摄像头厂家提供的芯片手册可以查看)
要了解选用的摄像头的特性,包括访问控制方法、各种参数的配置方法、信号输出类型等。
Camera解码器、控制器(主控芯片的芯片手册里面有摄像头相关的寄存器设置,比如2410里,里面主要是设置相关控制功能使能,芯片内部自己的架构)
如果摄像头是模拟量输出的,要熟悉解码器的配置。最后数字视频信号进入camera控制器后,还要熟悉camera控制器的操作。
V4L2的API和数据结构控制(主要是用户空间需要的一些v4l2的操作,然后针对这些操作必须在底层实现相应的驱动)
编写驱动前要熟悉应用程序访问V4L2的方法及设计到的数据结构。
V4L2的驱动架构(这个是在底层写驱动,为用户空间提供相应的访问接口,可以参照内核里面的/drivers/media/video//zc301/zc301_core.c 中的ZC301视频驱动代码
它是内核提供的非常完善的v4l2架构的例子,基本上都可以在它的基础上进行修改!)
最后编写出符合V4L2规范的视频驱动。
NO.1 摄像头方面的知识
ov9650摄像头,暂时先不说,先了解一下camera解码器、控制器,不同的主控芯片的camera控制器都差不多
static struct ov9650_reg { unsigned char subaddr; unsigned char value; }regs[] = { /* OV9650 intialization parameter table for VGA application */ {0x12, 0x40}, // Camera Soft reset. Self cleared after reset. {CHIP_DELAY, 10}, {0x11,0x81},{0x6a,0x3e},{0x3b,0x09},{0x13,0xe0},{0x01,0x80},{0x02,0x80},{0x00,0x00},{0x10,0x00}, {0x13,0xe5},{0x39,0x43},{0x38,0x12},{0x37,0x91},{0x35,0x91},{0x0e,0xa0},{0x1e,0x04},{0xA8,0x80}, {0x14,0x40},{0x04,0x00},{0x0c,0x04},{0x0d,0x80},{0x18,0xc6},{0x17,0x26},{0x32,0xad},{0x03,0x00}, {0x1a,0x3d},{0x19,0x01},{0x3f,0xa6},{0x14,0x2e},{0x15,0x10},{0x41,0x02},{0x42,0x08},{0x1b,0x00}, {0x16,0x06},{0x33,0xe2},{0x34,0xbf},{0x96,0x04},{0x3a,0x00},{0x8e,0x00},{0x3c,0x77},{0x8B,0x06}, {0x94,0x88},{0x95,0x88},{0x40,0xc1},{0x29,0x3f},{0x0f,0x42},{0x3d,0x92},{0x69,0x40},{0x5C,0xb9}, {0x5D,0x96},{0x5E,0x10},{0x59,0xc0},{0x5A,0xaf},{0x5B,0x55},{0x43,0xf0},{0x44,0x10},{0x45,0x68}, {0x46,0x96},{0x47,0x60},{0x48,0x80},{0x5F,0xe0},{0x60,0x8c},{0x61,0x20},{0xa5,0xd9},{0xa4,0x74}, {0x8d,0x02},{0x13,0xe7},{0x4f,0x3a},{0x50,0x3d},{0x51,0x03},{0x52,0x12},{0x53,0x26},{0x54,0x38}, {0x55,0x40},{0x56,0x40},{0x57,0x40},{0x58,0x0d},{0x8C,0x23},{0x3E,0x02},{0xa9,0xb8},{0xaa,0x92}, {0xab,0x0a},{0x8f,0xdf},{0x90,0x00},{0x91,0x00},{0x9f,0x00},{0xa0,0x00},{0x3A,0x01},{0x24,0x70}, {0x25,0x64},{0x26,0xc3},{0x2a,0x00},{0x2b,0x00},{0x6c,0x40},{0x6d,0x30},{0x6e,0x4b},{0x6f,0x60}, {0x70,0x70},{0x71,0x70},{0x72,0x70},{0x73,0x70},{0x74,0x60},{0x75,0x60},{0x76,0x50},{0x77,0x48}, {0x78,0x3a},{0x79,0x2e},{0x7a,0x28},{0x7b,0x22},{0x7c,0x04},{0x7d,0x07},{0x7e,0x10},{0x7f,0x28}, {0x80,0x36},{0x81,0x44},{0x82,0x52},{0x83,0x60},{0x84,0x6c},{0x85,0x78},{0x86,0x8c},{0x87,0x9e}, {0x88,0xbb},{0x89,0xd2},{0x8a,0xe6}, }; |
上面是需要顺序写ov9650的寄存器的地址和写入的数值(采用I2C子系统传输)
I2C子系统传输已经分析过,平台设备的资源可以在板文件中初始化:
1.修改vi drivers/i2c/busses/Kconfig
修改
config I2C_S3C2410
tristate "S3C2410 I2C Driver"
depends on ARCH_S3C2410 || ARCH_S3C64XX
help
Say Y here to include support for I2C controller in the
Samsung S3C2410 based System-on-Chip devices.
为:
config I2C_S3C2410
tristate "S3C2410 I2C Driver"
depends on ARCH_S3C2410 || ARCH_S3C64XX || ARCH_S5PC100
help
Say Y here to include support for I2C controller in the
Samsung S3C2410 based System-on-Chip devices.
2.内核配置并重新编译内核
$ make menuconfig
Device Drivers --->
<*> I2C support --->
<*> I2C device interface
I2C Hardware Bus support --->
<*> S3C2410 I2C Driver
3.修改vi arch/arm/mach-s5pc100/mach-smdkc100.c
查看原理图可以知道摄像头是接在I2C-0或1上,假设在1上,根据原理图修改i2c_devs1添加ov9650的内容,主要是ov9650的地址,这个在芯片手册上可以查到是0x60
而下面为什么是0x30呢?在我的另外一篇I2C子系统分析里面讲过。给个链接解释
修改:
static struct i2c_board_info i2c_devs1[] __initdata = {
};
为:
static struct i2c_board_info i2c_devs1[] __initdata = {
{
I2C_BOARD_INFO("ov9650", 0x30),
},
};
添加s5pc100 摄像头控制器平台设备相关内容,这些内容我们可以通过查看S5PC100的芯片手册查到
static struct resource s3c_camif_resource[] = {
[0] = {
.start = 0xEE200000,
.end = 0xEE200000 + SZ_1M - 1,
.flags = IORESOURCE_MEM,
},
[1] = {
.start = IRQ_FIMC0,
.end = IRQ_FIMC0,
.flags = IORESOURCE_IRQ,
}
};
static u64 s3c_device_camif_dmamask = 0xffffffffUL;
struct platform_device s3c_device_camif = {
.name = "s5pc100-camif",
.id = 0,
.num_resources = ARRAY_SIZE(s3c_camif_resource),
.resource = s3c_camif_resource,
.dev = {
.dma_mask = &s3c_device_camif_dmamask,
.coherent_dma_mask = 0xffffffffUL
}
};
EXPORT_SYMBOL(s3c_device_camif);
注册摄像头控制平台设备:
在smdkc100_devices中添加s3c_device_camif
static struct platform_device *smdkc100_devices[] __initdata = {
&s3c_device_camif, //添加内容
};
4. 添加驱动(video)
Make menuconfig
Device Drivers --->
<*> Multimedia support --->
<*> Video For Linux
[*] Enable Video For Linux API 1 (DEPRECATED) (NEW)
[*] Video capture adapters (NEW) --->
[*] V4L USB devices (NEW) --->
<*> USB Video Class (UVC)
[*] UVC input events device support (NEW)
<*> USB ZC0301[P] webcam support (DEPRECATED)
这样device已经注册好了!
/* write a register */
static int ov9650_reg_write(struct i2c_client *client, u8 reg, u8 val)
{
int ret;
u8 _val;
unsigned char data[2] = { reg, val };
struct i2c_msg msg = {
.addr= client->addr,
.flags= 0,
.len= 2,
.buf= data,
};
//构建i2c_msg
ret = i2c_transfer(client->adapter, &msg, 1); //I2C适配器和I2C设备之间的一组消息的交换
return 0;
}
static void ov9650_init_regs(void)
{
int i;
for (i=0; i<ARRAY_SIZE(regs); i++)
{
if (regs[i].subaddr == 0xff)
{
mdelay(regs[i].value);
continue;
}
ov9650_reg_write(ov9650_client, regs[i].subaddr, regs[i].value);
}
}
至此,通过I2C总线已经将摄像头的寄存器初始化好了。
ov9650摄像头驱动之——linux内核v4l2架构分析2
NO.2 Camera解码器、控制器
1.根据camera控制器的描述,图像传输有两个DMA通道,我们用的是C通道,所以先将DMA内存初始化,因为在V4L2操作中有把VIDIOC_REQBUFS中分配的数据缓存转换成物理地址的操作
所以DMA在用之前要初始化,包括实际物理地址的计算
init_image_buffer(camera_dev);// 初始化
static int __inline__ init_image_buffer(struct s5pc100_camera_device *cam){unsigned long size;unsigned int order;cam->frame = img_buff;size = MAX_WIDTH * MAX_HEIGHT * formats[3].depth / 8; //sizeof image buffer is 600KBytes printk("each image buffer is %dKBytes.\n", (int)(size/1024));order = get_order(size); //系统函数,size应该是2的n次幂,内存按页分配 img_buff[0].order = order;img_buff[0].virt_base = __get_free_pages(GFP_KERNEL|GFP_DMA, img_buff[0].order);//申请DMA空间,该函数可分配多个页并返回分配内存的首地址,分配的页数为2的order次幂,分配的页也不清零。order允许的最大值是10(即1024页)或者11(即2048页),具体依赖于硬件平台。 img_buff[0].img_size = size;img_buff[0].phy_base = img_buff[0].virt_base - PAGE_OFFSET + PHYS_OFFSET;// the DMA address.申请的DMA的物理地址,怎么计算的呢?首先要减去PAGE_OFFSET why?因为在linux系统中,进程的4G空间被分为用户空间和内核空间两部分,用户空间的地址一般分布为0-3G(即RAGE_OFFSET),这样剩下的3-4G为内核空间,然后再加上 +PHYS_OFFSET(这个是由具体的cpu决定的,RAM的物理起始地址),这样的话phy_base就对应上了真正的物理地址printk("get pages for img_buff[0..3] done.\n");return 0;error0:return -ENOMEM;}
2.camera控制器的初始化
- 图像源的格式设置
- window cut的设置
- 目标图像格式的设置
- 图像的缩放、旋转设置
- (可选,如果是用本地LCD显示的话)将输出buffer地址定位在Framebuffer显存地址中(即内存重叠,这样的话LCD就能直接显示了),因为这里没用到LCD,所以这个就省略
具体代码:
init_camif_config(camera_dev);static void init_camif_config(struct s5pc100_camera_device* c){struct s5pc100_camera_device*cam = c;cam->format = 3;// FIXME, C-path default format, see formats[] for detail.选择C通道 cam->srcHsize = 640;// FIXME, the OV9650's horizontal output pixels.设置图像源的大小 cam->srcVsize = 480;// FIXME, the OV9650's verical output pixels. 设置图像源的大小cam->wndHsize = 640;cam->wndVsize = 480; //window cut的设置 cam->targetHsize = cam->wndHsize;// 目标图像格式的设置,与window图像重叠,全覆盖cam->targetVsize = cam->wndVsize;旋转没有设置到目前为止,只是填充了cam的数据,但是camera控制器的源地址寄存器、目的地址寄存器都还没有配置这两个寄存器的配置依赖于上面初始化的参数update_camera_config(cam, (u32)-1);//这个函数中集成了一个函数,这个函数就是配置两个寄存器的操作 }static void update_camera_config (struct s5pc100_camera_device *c, u32 cmdcode){struct s5pc100_camera_device *cam = c;update_camera_regs(cam);// config the regs directly.封装了下面的两个函数,其实没必要 }static void __inline__ update_camera_regs(struct s5pc100_camera_device * cam){update_source_fmt_regs(cam);update_target_fmt_regs(cam);}
初始化source寄存器
static void __inline__ update_source_fmt_regs(struct s5pc100_camera_device *c) { struct s5pc100_camera_device *cam = c; u32 cfg;cfg = (1<<31)// ITU-R BT.601 YCbCr 8-bit mode |(0<<30)// CB,Cr value offset cntrol for YCbCr |(640<<16)// target image width |(0<<14)// input order is YCbYCr |(640<<0);// source image height writel(cfg, cam->reg_base + S5PC100_CISRCFMT); //0xEE20_0000 + 0000_0000 图像源地址 printk("S5PC100_CIGCFMT = %x\n", readl(cam->reg_base + S5PC100_CISRCFMT));cfg = (1<<15) |(1<<14) |(1<<30) |(1<<29);writel(cfg, cam->reg_base + S5PC100_CIWDOFST);///0xEE20_0000 + 0000_0004 清缓存fifo cfg = (1<<26) |(1<<29) |(1<<16) |(1<<7) |(0<<0); writel(cfg, cam->reg_base + S5PC100_CIGCTRL);///0xEE20_0000 + 0000_0008全局变量控制寄存器,包含了使能IRQ中断等操作 printk("S5PC100_CIGCTRL = %x\n", readl(cam->reg_base + S5PC100_CIGCTRL)); writel(0, cam->reg_base + S5PC100_CIWDOFST2);//0xEE20_0000 + 0000_0014窗口偏移寄存器 printk("OV9650_VGA mode\n"); }
static void __inline__ update_target_fmt_regs(struct s5pc100_camera_device * cam) { u32 cfg; u32 h_shift; u32 v_shift; u32 prescaler_v_ratio; u32 prescaler_h_ratio; u32 main_v_ratio; u32 main_h_ratio; switch (formats[cam->format].pixelformat) { case V4L2_PIX_FMT_RGB565: case V4L2_PIX_FMT_RGB24: case V4L2_PIX_FMT_YUV420: case V4L2_PIX_FMT_YUYV: /* YCbCr 1 plane*/ printk("format V4L2_PIX_FMT_YUYV"); writel(img_buff[0].phy_base, cam->reg_base + S5PC100_CIOYSA1);// 0xEE20_0000 + 0000_0018 DMAY1输出开始地址寄存器将配置好的DMA物理开始地址赋给上述寄存器 /* CIPRTRGFMT. */ cfg = (2 << 29) | (cam->targetHsize << 16)| (cam->targetVsize << 0)|(1<<13)|(1<<14)|(1<<15); 将cam里已经初始化好的大小信息移位,写入对应的位置 writel(cfg, cam->reg_base + S5PC100_CITRGFMT); // 0xEE20_0000 + 0000_0048 目标格式寄存器 /* CISCPRERATIO. */ calculate_prescaler_ratio_shift(cam->srcHsize, cam->targetHsize, &prescaler_h_ratio, &h_shift);//将源的横坐标进行压缩,返回 压缩率和移位数 calculate_prescaler_ratio_shift(cam->srcVsize, cam->targetVsize, &prescaler_v_ratio, &v_shift);//将源的纵坐标进行压缩 main_h_ratio = (cam->srcHsize << 8) / (cam->targetHsize << h_shift); main_v_ratio = (cam->srcVsize << 8) / (cam->targetVsize << v_shift);cfg = ((10 - (h_shift + v_shift)) << 28) | (prescaler_h_ratio << 16) | (prescaler_v_ratio << 0); //移位因子,即共移位多少次 writel(cfg, cam->reg_base + S5PC100_CISCPRERATIO);// 0xEE20_0000 + 0000_0050缩放比例寄存器,实现了图像的缩放处理cfg = (cam->targetHsize << 16) | (cam->targetVsize << 0); writel(cfg, cam->reg_base + S5PC100_CISCPREDST); // 0xEE20_0000 + 0000_0054 最初的目的定位寄存器cfg = (main_h_ratio << 16) | (main_v_ratio << 0); writel(cfg, cam->reg_base + S5PC100_CISCCTRL); //main-scaler control Reg的配置 cfg = cam->targetVsize * cam->targetHsize; //长*宽,0-27位,满足了 writel(cfg, cam->reg_base + S5PC100_CITAREA);//输出目标区域大小寄存器 cfg = (cam->targetVsize << 0) | (cam->targetHsize << 16); writel(cfg, cam->reg_base + S5PC100_ORGOSIZE); // 0xEE20_0000 + 0000_0184 DMA图像开始坐标寄存器 break;} } 下面的函数的意思是:传进来两个参数,一个是源的大小,另个是目的的大小,如果源是目标的64倍以上就错了,否则进行缩放,即源的大小是目标的32-64倍之间,就返回ratio(缩放比例)和shift(2的多少次幂),缩放比例是2的多少次幂,这样做的目的是方便移位,因为移位都是2的倍数 int calculate_prescaler_ratio_shift(unsigned int SrcSize, unsigned int DstSize, unsigned int*ratio,unsigned int *shift) { if(SrcSize>=64*DstSize) { return -EINVAL; } else if(SrcSize>=32*DstSize) { *ratio=32; *shift=5; } else if(SrcSize>=16*DstSize) { *ratio=16; *shift=4; } else if(SrcSize>=8*DstSize) { *ratio=8; *shift=3; } else if(SrcSize>=4*DstSize) { *ratio=4; *shift=2; } else if(SrcSize>=2*DstSize) { *ratio=2; *shift=1; } else { *ratio=1; *shift=0; } return 0; }
ov9650摄像头驱动之——linux内核v4l2架构分析3
NO.3 V4L2的API和数据结构
V4L2是V4L的升级版本,为linux下视频设备程序提供了一套接口规范。包括一套数据结构和底层V4L2驱动接口。
1、常用的结构体在内核目录include/linux/videodev2.h中定义
struct v4l2_requestbuffers //申请帧缓冲,对应命令VIDIOC_REQBUFS
struct v4l2_capability //视频设备的功能,对应命令VIDIOC_QUERYCAP
struct v4l2_input //视频输入信息,对应命令VIDIOC_ENUMINPUT
struct v4l2_standard //视频的制式,比如PAL,NTSC,对应命令VIDIOC_ENUMSTD
struct v4l2_format //帧的格式,对应命令VIDIOC_G_FMT、VIDIOC_S_FMT等
struct v4l2_buffer //驱动中的一帧图像缓存,对应命令VIDIOC_QUERYBUF
struct v4l2_crop //视频信号矩形边框
v4l2_std_id //视频制式
2、常用的IOCTL接口命令也在include/linux/videodev2.h中定义
VIDIOC_REQBUFS //分配内存
VIDIOC_QUERYBUF //把VIDIOC_REQBUFS中分配的数据缓存转换成物理地址
VIDIOC_QUERYCAP //查询驱动功能
VIDIOC_ENUM_FMT //获取当前驱动支持的视频格式
VIDIOC_S_FMT //设置当前驱动的频捕获格式
VIDIOC_G_FMT //读取当前驱动的频捕获格式
VIDIOC_TRY_FMT //验证当前驱动的显示格式
VIDIOC_CROPCAP //查询驱动的修剪能力
VIDIOC_S_CROP //设置视频信号的矩形边框
VIDIOC_G_CROP //读取视频信号的矩形边框
VIDIOC_QBUF //把数据从缓存中读取出来
VIDIOC_DQBUF //把数据放回缓存队列
VIDIOC_STREAMON //开始视频显示函数
VIDIOC_STREAMOFF //结束视频显示函数
VIDIOC_QUERYSTD //检查当前视频设备支持的标准,例如PAL或NTSC。
3、操作流程
V4L2提供了很多访问接口,你可以根据具体需要选择操作方法。需要注意的是,很少有驱动完全实现了所有的接口功能。所以在使用时需要参考驱动源码,或仔细阅读驱动提供者的使用说明。
下面列举出一种操作的流程,供参考。
(1)打开设备文件
int fd = open(Devicename,mode);
Devicename:/dev/video0、/dev/video1 ……
Mode:O_RDWR [| O_NONBLOCK]
如果使用非阻塞模式调用视频设备,则当没有可用的视频数据时,不会阻塞,而立刻返回。
(2)取得设备的capability
struct v4l2_capability capability;
int ret = ioctl(fd, VIDIOC_QUERYCAP, &capability);
看看设备具有什么功能,比如是否具有视频输入特性。
(3)选择视频输入
struct v4l2_input input;
……初始化input
int ret = ioctl(fd, VIDIOC_QUERYCAP, &input);
一个视频设备可以有多个视频输入。如果只有一路输入,这个功能可以没有。
(4)检测视频支持的制式
v4l2_std_id std;do {ret = ioctl(fd, VIDIOC_QUERYSTD, &std);} while (ret == -1 && errno == EAGAIN);switch (std) {case V4L2_STD_NTSC: //……case V4L2_STD_PAL://…… }
(5)设置视频捕获格式
struct v4l2_format fmt;fmt.type = V4L2_BUF_TYPE_VIDEO_OUTPUT;fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_UYVY;fmt.fmt.pix.height = height;fmt.fmt.pix.width = width;fmt.fmt.pix.field = V4L2_FIELD_INTERLACED;ret = ioctl(fd, VIDIOC_S_FMT, &fmt);if(ret) {perror("VIDIOC_S_FMT/n");close(fd);return -1;}
(6)向驱动申请帧缓存
struct v4l2_requestbuffers req;
if (ioctl(fd, VIDIOC_REQBUFS, &req) == -1) {
return -1;
}
v4l2_requestbuffers结构中定义了缓存的数量,驱动会据此申请对应数量的视频缓存。多个缓存可以用于建立FIFO,来提高视频采集的效率。
(7)获取每个缓存的信息,并mmap到用户空间
typedef struct VideoBuffer {void *start;size_t length;} VideoBuffer;VideoBuffer* buffers = calloc( req.count, sizeof(*buffers) );struct v4l2_buffer buf;for (numBufs = 0; numBufs < req.count; numBufs++) {//映射所有的缓存 memset( &buf, 0, sizeof(buf) );buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;buf.memory = V4L2_MEMORY_MMAP;buf.index = numBufs;if (ioctl(fd, VIDIOC_QUERYBUF, &buf) == -1) {//获取到对应index的缓存信息,此处主要利用length信息及offset信息来完成后面的mmap操作。return -1;}buffers[numBufs].length = buf.length;// 转换成相对地址 buffers[numBufs].start = mmap(NULL, buf.length,PROT_READ | PROT_WRITE,MAP_SHARED,fd, buf.m.offset);if (buffers[numBufs].start == MAP_FAILED) {return -1;}
(8)开始采集视频
int buf_type= V4L2_BUF_TYPE_VIDEO_CAPTURE;
int ret = ioctl(fd, VIDIOC_STREAMON, &buf_type);
(9)取出FIFO缓存中已经采样的帧缓存
struct v4l2_buffer buf;
memset(&buf,0,sizeof(buf));
buf.type=V4L2_BUF_TYPE_VIDEO_CAPTURE;
buf.memory=V4L2_MEMORY_MMAP;
buf.index=0;//此值由下面的ioctl返回
if (ioctl(fd, VIDIOC_DQBUF, &buf) == -1)
{
return -1;
}
根据返回的buf.index找到对应的mmap映射好的缓存,取出视频数据。
(10)将刚刚处理完的缓冲重新入队列尾,这样可以循环采集
if (ioctl(fd, VIDIOC_QBUF, &buf) == -1) {
return -1;
}
(11)停止视频的采集
int ret = ioctl(fd, VIDIOC_STREAMOFF, &buf_type);
(12)关闭视频设备
close(fd);
NO.4 V4L2的驱动架构
上述流程的各个操作都需要有底层V4L2驱动的支持。内核中有一些非常完善的例子。
比如:linux-2.6.26内核目录/drivers/media/video//zc301/zc301_core.c 中的ZC301视频驱动代码。上面的V4L2操作流程涉及的功能在其中都有实现。
1、V4L2驱动注册、注销函数
Video核心层(drivers/media/video/videodev.c)提供了注册函数
int video_register_device(struct video_device *vfd, int type, int nr)
video_device: 要构建的核心数据结构
Type: 表示设备类型,此设备号的基地址受此变量的影响
Nr: 如果end-base>nr>0 :次设备号=base(基准值,受type影响)+nr;
否则:系统自动分配合适的次设备号
具体驱动只需要构建video_device结构,然后调用注册函数既可。
如:zc301_core.c中的
err = video_register_device(cam->v4ldev, VFL_TYPE_GRABBER,
video_nr[dev_nr]);
Video核心层(drivers/media/video/videodev.c)提供了注销函数
void video_unregister_device(struct video_device *vfd)
2、struct video_device 的构建
video_device结构包含了视频设备的属性和操作方法。参见zc301_core.c
strcpy(cam->v4ldev->name, "ZC0301[P] PC Camera");cam->v4ldev->owner = THIS_MODULE;cam->v4ldev->type = VID_TYPE_CAPTURE | VID_TYPE_SCALES;cam->v4ldev->fops = &zc0301_fops;cam->v4ldev->minor = video_nr[dev_nr];cam->v4ldev->release = video_device_release;video_set_drvdata(cam->v4ldev, cam);
大家发现在这个zc301的驱动中并没有实现struct video_device中的很多操作函数,如:vidioc_querycap、vidioc_g_fmt_cap等。主要原因是struct file_operations zc0301_fops中的zc0301_ioctl实现了前面的所有ioctl操作。所以就不需要在struct video_device再实现struct video_device中的那些操作了。
另一种实现方法如下:
static struct video_device camif_dev ={.name = "s3c2440 camif",.type = VID_TYPE_CAPTURE|VID_TYPE_SCALES|VID_TYPE_SUBCAPTURE,.fops = &camif_fops,.minor = -1,.release = camif_dev_release,.vidioc_querycap = vidioc_querycap,.vidioc_enum_fmt_cap = vidioc_enum_fmt_cap,.vidioc_g_fmt_cap = vidioc_g_fmt_cap,.vidioc_s_fmt_cap = vidioc_s_fmt_cap,.vidioc_queryctrl = vidioc_queryctrl,.vidioc_g_ctrl = vidioc_g_ctrl,.vidioc_s_ctrl = vidioc_s_ctrl,};static struct file_operations camif_fops ={.owner = THIS_MODULE,.open = camif_open,.release = camif_release,.read = camif_read,.poll = camif_poll,.ioctl = video_ioctl2, /* V4L2 ioctl handler */.mmap = camif_mmap,.llseek = no_llseek,};
注意:video_ioctl2是videodev.c中是实现的。video_ioctl2中会根据ioctl不同的cmd来
调用video_device中的操作方法。
3、Video核心层的实现
参见内核/drivers/media/videodev.c
(1)注册256个视频设备
static int __init videodev_init(void){int ret;if (register_chrdev(VIDEO_MAJOR, VIDEO_NAME, &video_fops)) {return -EIO;}ret = class_register(&video_class);……}
上面的代码注册了256个视频设备,并注册了video_class类。video_fops为这256个设备共同的操作方法。
(2)V4L2驱动注册函数的实现
int video_register_device(struct video_device *vfd, int type, int nr){int i=0;int base;int end;int ret;char *name_base;switch(type) //根据不同的type确定设备名称、次设备号 {case VFL_TYPE_GRABBER:base=MINOR_VFL_TYPE_GRABBER_MIN;end=MINOR_VFL_TYPE_GRABBER_MAX+1;name_base = "video";break;case VFL_TYPE_VTX:base=MINOR_VFL_TYPE_VTX_MIN;end=MINOR_VFL_TYPE_VTX_MAX+1;name_base = "vtx";break;case VFL_TYPE_VBI:base=MINOR_VFL_TYPE_VBI_MIN;end=MINOR_VFL_TYPE_VBI_MAX+1;name_base = "vbi";break;case VFL_TYPE_RADIO:base=MINOR_VFL_TYPE_RADIO_MIN;end=MINOR_VFL_TYPE_RADIO_MAX+1;name_base = "radio";break;default:printk(KERN_ERR "%s called with unknown type: %d/n",__func__, type);return -1;}/* 计算出次设备号 */mutex_lock(&videodev_lock);if (nr >= 0 && nr < end-base) {/* use the one the driver asked for */i = base+nr;if (NULL != video_device[i]) {mutex_unlock(&videodev_lock);return -ENFILE;}} else {/* use first free */for(i=base;i<end;i++)if (NULL == video_device[i])break;if (i == end) {mutex_unlock(&videodev_lock);return -ENFILE;}}video_device[i]=vfd; //保存video_device结构指针到系统的结构数组中,最终的次设备号和i相关。 vfd->minor=i;mutex_unlock(&videodev_lock);mutex_init(&vfd->lock);/* sysfs class */memset(&vfd->class_dev, 0x00, sizeof(vfd->class_dev));if (vfd->dev)vfd->class_dev.parent = vfd->dev;vfd->class_dev.class = &video_class;vfd->class_dev.devt = MKDEV(VIDEO_MAJOR, vfd->minor);sprintf(vfd->class_dev.bus_id, "%s%d", name_base, i - base);//最后在/dev目录下的名称 ret = device_register(&vfd->class_dev);//结合udev或mdev可以实现自动在/dev下创建设备节点 ……}
从上面的注册函数中可以看出V4L2驱动的注册事实上只是完成了设备节点的创建,如:/dev/video0。和video_device结构指针的保存。
(3)视频驱动的打开过程
当用户空间调用open打开对应的视频文件时,如:
int fd = open(/dev/video0, O_RDWR);
对应/dev/video0的文件操作结构是/drivers/media/videodev.c中定义的video_fops。
static const struct file_operations video_fops=
{
.owner = THIS_MODULE,
.llseek = no_llseek,
.open = video_open,
};
奇怪吧,这里只实现了open操作。那么后面的其它操作呢?还是先看看video_open吧。
static int video_open(struct inode *inode, struct file *file){unsigned int minor = iminor(inode);int err = 0;struct video_device *vfl;const struct file_operations *old_fops;if(minor>=VIDEO_NUM_DEVICES)return -ENODEV;mutex_lock(&videodev_lock);vfl=video_device[minor];if(vfl==NULL) {mutex_unlock(&videodev_lock);request_module("char-major-%d-%d", VIDEO_MAJOR, minor);mutex_lock(&videodev_lock);vfl=video_device[minor]; //根据次设备号取出video_device结构if (vfl==NULL) {mutex_unlock(&videodev_lock);return -ENODEV;}}old_fops = file->f_op;file->f_op = fops_get(vfl->fops);//替换此打开文件的file_operation结构。后面的其它针对此文件的操作都由新的结构来负责了。也就是由每个具体的video_device的fops负责。if(file->f_op->open)err = file->f_op->open(inode,file);if (err) {fops_put(file->f_op);file->f_op = fops_get(old_fops);}……}