Linux V4L2驱动开发实战:从零构建CSI摄像头驱动框架

在嵌入式Linux开发领域,视频采集功能的实现离不开V4L2(Video for Linux 2)驱动框架的支持。本文将深入探讨如何为CSI(CMOS Sensor Interface)摄像头构建完整的V4L2驱动,涵盖从设备注册到数据流管理的全流程实现。

1. V4L2驱动开发环境搭建

开发V4L2驱动前需要准备适当的硬件和软件环境。典型的嵌入式开发板如树莓派、i.MX6ULL或RK3568都包含CSI接口,可连接OV5640等常见摄像头模组。

开发环境要求:

  • 目标板:ARM架构开发板(如树莓派4B)
  • 摄像头模组:OV5640 MIPI-CSI摄像头
  • 工具链:arm-linux-gnueabihf-gcc
  • 内核版本:Linux 4.19+(需开启V4L2相关配置)

内核配置关键选项:

# 在内核源码目录执行
make menuconfig

# 确保以下选项开启
Device Drivers --->
    Multimedia support --->
        <*> Video4Linux (V4L) support
        <*> V4L2 sub-device userspace API
        <*> Videobuf2 core
        <*> Videobuf2 memory allocator
        <*> Videobuf2 VM allocator

开发板设备树配置示例:

&csi {
    status = "okay";
    port {
        csi_ep: endpoint {
            remote-endpoint = <&camera_ep>;
            data-lanes = <1 2>;
            clock-lanes = <0>;
        };
    };
};

&i2c2 {
    status = "okay";
    camera: ov5640@3c {
        compatible = "ovti,ov5640";
        reg = <0x3c>;
        clocks = <&camera_clk>;
        clock-names = "xclk";
        pinctrl-names = "default";
        pinctrl-0 = <&camera_pins>;
        powerdown-gpios = <&gpio 4 GPIO_ACTIVE_LOW>;
        reset-gpios = <&gpio 5 GPIO_ACTIVE_LOW>;

        port {
            camera_ep: endpoint {
                remote-endpoint = <&csi_ep>;
                data-lanes = <1 2>;
                clock-lanes = <0>;
            };
        };
    };
};

2. V4L2驱动核心数据结构

V4L2驱动围绕几个核心数据结构构建,理解这些结构体是开发驱动的基础。

2.1 video_device结构体

video_device代表一个视频设备实例,主要包含:

struct video_device {
    const struct v4l2_file_operations *fops;  // 文件操作集合
    struct device *dev;                       // 关联的设备
    struct v4l2_device *v4l2_dev;            // 所属的V4L2设备
    struct vb2_queue *queue;                  // 视频缓冲区队列
    const struct v4l2_ioctl_ops *ioctl_ops;  // ioctl操作集合
    void (*release)(struct video_device *vdev); // 释放函数
    // ...其他成员
};

2.2 v4l2_ioctl_ops操作集

v4l2_ioctl_ops定义了驱动需要实现的ioctl命令处理函数:

struct v4l2_ioctl_ops {
    int (*vidioc_querycap)(struct file *file, void *fh, 
                          struct v4l2_capability *cap);
    int (*vidioc_enum_fmt_vid_cap)(struct file *file, void *fh,
                                  struct v4l2_fmtdesc *f);
    int (*vidioc_g_fmt_vid_cap)(struct file *file, void *fh,
                               struct v4l2_format *f);
    int (*vidioc_s_fmt_vid_cap)(struct file *file, void *fh,
                               struct v4l2_format *f);
    int (*vidioc_reqbufs)(struct file *file, void *fh,
                         struct v4l2_requestbuffers *b);
    int (*vidioc_querybuf)(struct file *file, void *fh,
                          struct v4l2_buffer *b);
    int (*vidioc_qbuf)(struct file *file, void *fh,
                      struct v4l2_buffer *b);
    int (*vidioc_dqbuf)(struct file *file, void *fh,
                       struct v4l2_buffer *b);
    int (*vidioc_streamon)(struct file *file, void *fh,
                          enum v4l2_buf_type i);
    int (*vidioc_streamoff)(struct file *file, void *fh,
                           enum v4l2_buf_type i);
    // ...其他操作
};

2.3 vb2_queue缓冲区管理

vb2_queue负责视频缓冲区的管理,驱动需要初始化的关键字段:

struct vb2_queue {
    unsigned int type;                 // 缓冲区类型(V4L2_BUF_TYPE_VIDEO_CAPTURE)
    unsigned int io_modes;            // 支持的IO模式(VB2_MMAP等)
    const struct vb2_mem_ops *mem_ops; // 内存操作函数集
    void *drv_priv;                   // 驱动私有数据
    int (*buf_init)(struct vb2_buffer *vb); // 缓冲区初始化回调
    int (*buf_prepare)(struct vb2_buffer *vb); // 缓冲区准备回调
    void (*buf_queue)(struct vb2_buffer *vb); // 缓冲区入队回调
    int (*start_streaming)(struct vb2_queue *q, unsigned int count); // 开始流式传输
    void (*stop_streaming)(struct vb2_queue *q); // 停止流式传输
    // ...其他成员
};

3. 驱动实现步骤详解

3.1 驱动初始化与注册

典型的CSI摄像头驱动初始化流程如下:

static int csi_driver_probe(struct platform_device *pdev)
{
    struct csi_device *csi_dev;
    struct v4l2_device *v4l2_dev;
    struct video_device *vdev;
    int ret;
    
    // 1. 分配驱动私有数据结构
    csi_dev = devm_kzalloc(&pdev->dev, sizeof(*csi_dev), GFP_KERNEL);
    
    // 2. 初始化V4L2设备
    v4l2_dev = &csi_dev->v4l2_dev;
    strscpy(v4l2_dev->name, CSI_DRV_NAME, sizeof(v4l2_dev->name));
    ret = v4l2_device_register(&pdev->dev, v4l2_dev);
    
    // 3. 初始化视频缓冲区队列
    csi_dev->queue.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
    csi_dev->queue.io_modes = VB2_MMAP | VB2_USERPTR | VB2_DMABUF;
    csi_dev->queue.drv_priv = csi_dev;
    csi_dev->queue.buf_struct_size = sizeof(struct csi_buffer);
    csi_dev->queue.ops = &csi_vb2_ops;
    csi_dev->queue.mem_ops = &vb2_dma_contig_memops;
    ret = vb2_queue_init(&csi_dev->queue);
    
    // 4. 初始化video_device
    vdev = &csi_dev->vdev;
    vdev->v4l2_dev = v4l2_dev;
    vdev->queue = &csi_dev->queue;
    strscpy(vdev->name, "CSI Camera", sizeof(vdev->name));
    vdev->release = video_device_release_empty;
    vdev->fops = &csi_fops;
    vdev->ioctl_ops = &csi_ioctl_ops;
    vdev->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
    
    // 5. 注册视频设备
    ret = video_register_device(vdev, VFL_TYPE_VIDEO, -1);
    
    // 6. 初始化摄像头传感器(通过I2C)
    ret = csi_init_sensor(csi_dev);
    
    return 0;
}

3.2 实现文件操作集合

驱动需要提供基本的文件操作函数:

static const struct v4l2_file_operations csi_fops = {
    .owner = THIS_MODULE,
    .open = csi_open,
    .release = csi_release,
    .read = csi_read,
    .poll = csi_poll,
    .unlocked_ioctl = video_ioctl2,
    .mmap = csi_mmap,
};

static int csi_open(struct file *file)
{
    struct csi_device *csi_dev = video_drvdata(file);
    
    if (mutex_lock_interruptible(&csi_dev->lock))
        return -ERESTARTSYS;
    
    file->private_data = csi_dev;
    csi_dev->users++;
    
    mutex_unlock(&csi_dev->lock);
    return 0;
}

static int csi_release(struct file *file)
{
    struct csi_device *csi_dev = video_drvdata(file);
    
    mutex_lock(&csi_dev->lock);
    csi_dev->users--;
    
    if (!csi_dev->users) {
        if (vb2_is_streaming(&csi_dev->queue))
            vb2_queue_release(&csi_dev->queue);
    }
    
    mutex_unlock(&csi_dev->lock);
    return 0;
}

3.3 实现缓冲区操作

Videobuf2框架需要驱动实现以下关键操作:

static const struct vb2_ops csi_vb2_ops = {
    .queue_setup = csi_queue_setup,
    .buf_init = csi_buf_init,
    .buf_prepare = csi_buf_prepare,
    .buf_queue = csi_buf_queue,
    .start_streaming = csi_start_streaming,
    .stop_streaming = csi_stop_streaming,
    .wait_prepare = vb2_ops_wait_prepare,
    .wait_finish = vb2_ops_wait_finish,
};

static int csi_queue_setup(struct vb2_queue *vq,
                          unsigned int *nbuffers,
                          unsigned int *nplanes,
                          unsigned int sizes[],
                          struct device *alloc_devs[])
{
    struct csi_device *csi_dev = vb2_get_drv_priv(vq);
    
    if (*nplanes)
        return sizes[0] < csi_dev->fmt.sizeimage ? -EINVAL : 0;
    
    *nplanes = 1;
    sizes[0] = csi_dev->fmt.sizeimage;
    return 0;
}

static int csi_start_streaming(struct vb2_queue *vq, unsigned int count)
{
    struct csi_device *csi_dev = vb2_get_drv_priv(vq);
    int ret;
    
    // 配置CSI硬件接口
    ret = csi_hw_setup(csi_dev);
    if (ret)
        return ret;
    
    // 启动传感器输出
    ret = v4l2_subdev_call(csi_dev->sensor, video, s_stream, 1);
    if (ret) {
        csi_hw_stop(csi_dev);
        return ret;
    }
    
    // 启动DMA传输
    csi_dma_start(csi_dev);
    return 0;
}

4. 关键IOCTL实现详解

4.1 查询设备能力(VIDIOC_QUERYCAP)

static int csi_querycap(struct file *file, void *fh,
                       struct v4l2_capability *cap)
{
    strscpy(cap->driver, CSI_DRV_NAME, sizeof(cap->driver));
    strscpy(cap->card, "CSI Camera", sizeof(cap->card));
    snprintf(cap->bus_info, sizeof(cap->bus_info),
            "platform:%s", CSI_DRV_NAME);
    
    cap->device_caps = V4L2_CAP_VIDEO_CAPTURE | V4L2_CAP_STREAMING;
    cap->capabilities = cap->device_caps | V4L2_CAP_DEVICE_CAPS;
    return 0;
}

4.2 设置/获取视频格式(VIDIOC_S_FMT/VIDIOC_G_FMT)

static int csi_g_fmt_vid_cap(struct file *file, void *fh,
                            struct v4l2_format *f)
{
    struct csi_device *csi_dev = video_drvdata(file);
    
    f->fmt.pix.width = csi_dev->fmt.width;
    f->fmt.pix.height = csi_dev->fmt.height;
    f->fmt.pix.pixelformat = csi_dev->fmt.pixelformat;
    f->fmt.pix.field = csi_dev->fmt.field;
    f->fmt.pix.bytesperline = csi_dev->fmt.bytesperline;
    f->fmt.pix.sizeimage = csi_dev->fmt.sizeimage;
    f->fmt.pix.colorspace = csi_dev->fmt.colorspace;
    
    return 0;
}

static int csi_s_fmt_vid_cap(struct file *file, void *fh,
                            struct v4l2_format *f)
{
    struct csi_device *csi_dev = video_drvdata(file);
    struct v4l2_subdev_format src_fmt;
    int ret;
    
    if (vb2_is_busy(&csi_dev->queue))
        return -EBUSY;
    
    // 验证格式是否支持
    ret = csi_try_fmt_vid_cap(file, fh, f);
    if (ret)
        return ret;
    
    // 配置传感器输出格式
    src_fmt.format.width = f->fmt.pix.width;
    src_fmt.format.height = f->fmt.pix.height;
    src_fmt.format.code = csi_get_mbus_code(f->fmt.pix.pixelformat);
    src_fmt.format.field = f->fmt.pix.field;
    src_fmt.format.colorspace = f->fmt.pix.colorspace;
    src_fmt.which = V4L2_SUBDEV_FORMAT_ACTIVE;
    
    ret = v4l2_subdev_call(csi_dev->sensor, pad, set_fmt, NULL, &src_fmt);
    if (ret)
        return ret;
    
    // 保存格式到驱动
    csi_dev->fmt.width = f->fmt.pix.width;
    csi_dev->fmt.height = f->fmt.pix.height;
    csi_dev->fmt.pixelformat = f->fmt.pix.pixelformat;
    csi_dev->fmt.field = f->fmt.pix.field;
    csi_dev->fmt.bytesperline = f->fmt.pix.bytesperline;
    csi_dev->fmt.sizeimage = f->fmt.pix.sizeimage;
    csi_dev->fmt.colorspace = f->fmt.pix.colorspace;
    
    return 0;
}

4.3 缓冲区管理实现

static int csi_reqbufs(struct file *file, void *fh,
                      struct v4l2_requestbuffers *b)
{
    struct csi_device *csi_dev = video_drvdata(file);
    
    if (b->memory != V4L2_MEMORY_MMAP)
        return -EINVAL;
    
    return vb2_reqbufs(&csi_dev->queue, b);
}

static int csi_querybuf(struct file *file, void *fh,
                       struct v4l2_buffer *b)
{
    struct csi_device *csi_dev = video_drvdata(file);
    return vb2_querybuf(&csi_dev->queue, b);
}

static int csi_qbuf(struct file *file, void *fh,
                   struct v4l2_buffer *b)
{
    struct csi_device *csi_dev = video_drvdata(file);
    return vb2_qbuf(&csi_dev->queue, file, b);
}

static int csi_dqbuf(struct file *file, void *fh,
                    struct v4l2_buffer *b)
{
    struct csi_device *csi_dev = video_drvdata(file);
    return vb2_dqbuf(&csi_dev->queue, b, file->f_flags & O_NONBLOCK);
}

5. 数据流控制与DMA实现

5.1 启动/停止视频流

static int csi_streamon(struct file *file, void *fh,
                       enum v4l2_buf_type type)
{
    struct csi_device *csi_dev = video_drvdata(file);
    
    if (type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
        return -EINVAL;
    
    return vb2_streamon(&csi_dev->queue, type);
}

static int csi_streamoff(struct file *file, void *fh,
                        enum v4l2_buf_type type)
{
    struct csi_device *csi_dev = video_drvdata(file);
    
    if (type != V4L2_BUF_TYPE_VIDEO_CAPTURE)
        return -EINVAL;
    
    return vb2_streamoff(&csi_dev->queue, type);
}

5.2 DMA传输实现

static void csi_dma_start(struct csi_device *csi_dev)
{
    struct vb2_buffer *vb;
    dma_addr_t dma_addr;
    
    // 获取第一个缓冲区
    vb = vb2_plane_cookie(&csi_dev->queue.bufs[0], 0);
    dma_addr = vb2_dma_contig_plane_dma_addr(vb, 0);
    
    // 配置DMA引擎
    writel(dma_addr, csi_dev->regs + CSI_DMA_ADDR_REG);
    writel(csi_dev->fmt.sizeimage, csi_dev->regs + CSI_DMA_SIZE_REG);
    writel(CSI_DMA_ENABLE, csi_dev->regs + CSI_DMA_CTRL_REG);
    
    // 启用CSI中断
    writel(CSI_INT_FRAME_DONE, csi_dev->regs + CSI_INT_ENABLE_REG);
}

static irqreturn_t csi_irq_handler(int irq, void *dev_id)
{
    struct csi_device *csi_dev = dev_id;
    u32 status = readl(csi_dev->regs + CSI_INT_STATUS_REG);
    
    if (status & CSI_INT_FRAME_DONE) {
        struct vb2_buffer *vb;
        
        // 获取当前活动的缓冲区
        vb = &csi_dev->queue.bufs[csi_dev->active_buf];
        
        // 标记缓冲区为完成
        vb2_buffer_done(vb, VB2_BUF_STATE_DONE);
        
        // 切换到下一个缓冲区
        csi_dev->active_buf = (csi_dev->active_buf + 1) %
                             csi_dev->queue.num_buffers;
        
        // 启动下一次DMA传输
        vb = &csi_dev->queue.bufs[csi_dev->active_buf];
        writel(vb2_dma_contig_plane_dma_addr(vb, 0),
              csi_dev->regs + CSI_DMA_ADDR_REG);
    }
    
    // 清除中断状态
    writel(status, csi_dev->regs + CSI_INT_STATUS_REG);
    return IRQ_HANDLED;
}

6. 调试与性能优化

6.1 常用调试技巧

内核日志输出:

// 在关键路径添加调试信息
dev_dbg(&csi_dev->pdev->dev, "Starting stream, format: %dx%d %4.4s\n",
       csi_dev->fmt.width, csi_dev->fmt.height,
       (char *)&csi_dev->fmt.pixelformat);

V4L2调试工具:

# 查看视频设备信息
v4l2-ctl --list-devices
v4l2-ctl --device=/dev/video0 --all

# 测试视频捕获
v4l2-ctl --device=/dev/video0 --set-fmt-video=width=640,height=480,pixelformat=YUYV
v4l2-ctl --device=/dev/video0 --stream-mmap --stream-count=10 --stream-to=frame.raw

6.2 性能优化建议

  1. 双缓冲机制:实现乒乓缓冲减少DMA等待时间
  2. 零拷贝优化:使用DMABUF实现用户空间直接访问
  3. 中断合并:适当合并中断减少CPU负载
  4. 时钟门控:空闲时关闭传感器时钟节省功耗
// 示例:零拷贝DMABUF实现
static int csi_dmabuf_ops_attach(struct dma_buf *dbuf, struct device *dev,
                                struct dma_buf_attachment *attach)
{
    struct csi_buffer *buf = dbuf->priv;
    
    attach->priv = buf;
    return 0;
}

static struct sg_table *csi_dmabuf_ops_map(
    struct dma_buf_attachment *attach,
    enum dma_data_direction dir)
{
    struct csi_buffer *buf = attach->priv;
    struct sg_table *sgt;
    int ret;
    
    sgt = kmalloc(sizeof(*sgt), GFP_KERNEL);
    if (!sgt)
        return ERR_PTR(-ENOMEM);
    
    ret = dma_get_sgtable(attach->dev, sgt, buf->vaddr, buf->dma_addr,
                         buf->size);
    if (ret < 0) {
        kfree(sgt);
        return ERR_PTR(ret);
    }
    
    return sgt;
}

通过以上步骤,开发者可以构建一个完整的CSI摄像头V4L2驱动。实际开发中还需根据具体硬件调整寄存器配置和时序参数,建议参考芯片手册和内核已有驱动实现。

更多推荐