Linux V4L2驱动开发实战:手把手教你从零实现一个摄像头驱动(附完整代码)
·
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 性能优化建议
- 双缓冲机制:实现乒乓缓冲减少DMA等待时间
- 零拷贝优化:使用DMABUF实现用户空间直接访问
- 中断合并:适当合并中断减少CPU负载
- 时钟门控:空闲时关闭传感器时钟节省功耗
// 示例:零拷贝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驱动。实际开发中还需根据具体硬件调整寄存器配置和时序参数,建议参考芯片手册和内核已有驱动实现。
更多推荐

所有评论(0)