原文地址: ov9650 摄像头驱动之——linux 内核 v4l2 架构分析 3
本系列准备分为 3-4 篇来讲, 因为说的太多会比较乱
v4l2 视频驱动主要涉及几个知识点:
- 摄像头方面的知识 (摄像头厂家提供的芯片手册可以查看), 要了解选用的摄像头的特性, 包括访问控制方法, 各种参数的配置方法, 信号输出类型等.
- Camera 解码器, 控制器 (主控芯片的芯片手册里面有摄像头相关的寄存器设置, 比如 2410 里, 里面主要是设置相关控制功能使能, 芯片内部自己的架构), 如果摄像头是模拟量输出的, 要熟悉解码器的配置. 最后数字视频信号进入 camera 控制器后, 还要熟悉 camera 控制器的操作.
- V4L2 的 API 和数据结构控制 (主要是用户空间需要的一些 v4l2 的操作, 然后针对这些操作必须在底层实现相应的驱动), 编写驱动前要熟悉应用程序访问 V4L2 的方法及设计到的数据结构.
- V4L2 的驱动架构 (这个是在底层写驱动, 为用户空间提供相应的访问接口, 可以参照内核里面的
/drivers/media/video/zc301/zc301_core.c
中的 ZC301 视频驱动代码, 它是内核提供的非常完善的 v4l2 架构的例子, 基本上都可以在它的基础上进行修改!) - 最后编写出符合 V4L2 规范的视频驱动.
摄像头方面的知识
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 子系统传输已经分析过, 平台设备的资源可以在板文件中初始化:
修改 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.
内核配置并重新编译内核
$ make menuconfig
Device Drivers --->
<*> I2C support --->
<*> I2C device interface
I2C Hardware Bus support --->
<*> S3C2410 I2C Driver
修改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, // 添加内容
};
添加驱动 (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 总线已经将摄像头的寄存器初始化好了.
下一部分将讲解 Camera 解码器, 控制器.
Camera 解码器, 控制器
DMA 内存初始化
根据 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;
// 申请 DMA 空间, 该函数可分配多个页并返回分配内存的首地址, 分配的页数为 2 的 order 次幂, 分配的页也不清零.
// order 允许的最大值是 10(即 1024 页) 或者 11(即 2048 页), 具体依赖于硬件平台.
img_buff[0].virt_base = __get_free_pages(GFP_KERNEL|GFP_DMA, img_buff[0].order);
img_buff[0].img_size = size;
// the DMA address. 申请的 DMA 的物理地址, 怎么计算的呢?
// 首先要减去 PAGE_OFFSET why? 因为在 linux 系统中, 进程的 4G 空间被分为用户空间和内核空间两部分,
// 用户空间的地址一般分布为 0-3G(即 RAGE_OFFSET),
// 这样剩下的 3-4G 为内核空间, 然后再加上 +PHYS_OFFSET(这个是由具体的 cpu 决定的, RAM 的物理起始地址), 这样的话 phy_base 就对应上了真正的物理地址
img_buff[0].phy_base = img_buff[0].virt_base - PAGE_OFFSET + PHYS_OFFSET;
printk("get pages for img_buff[0..3] done.\n");
return 0;
error0:
return -ENOMEM;
}
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;
}
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;iminor=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);
}
……
}
文章评论