2、Camera子系统概述:V4L2框架简介、Camera硬件接口(MIPI CSI/DVP)、驱动分层结构

好,咱们进入第二章。这一章我打算把Camera子系统的大框架给你捋一遍。说白了,就是让你先看清整座山的轮廓,再带你爬具体的坡。

很多初学者一上来就盯着某个驱动文件死磕,结果越看越懵。为什么?因为你不知道这个文件在整个系统里扮演什么角色。我当年也犯过这个错,调一个OV5640的驱动调了三天,最后发现是V4L2的ioctl处理顺序搞反了。嗯,从那以后我就养成了一个习惯:先看框架,再写代码。

2.1 V4L2框架简介

V4L2,全称Video for Linux 2,是Linux内核里处理视频设备的统一框架。你可以把它理解成摄像头领域的“通用语言”。不管你是USB摄像头、MIPI摄像头还是模拟摄像头,只要你想在Linux下用,就得遵守V4L2的规矩。

我个人习惯把V4L2框架拆成三层来看:

  • 应用层:用户空间的程序,比如gstreamer、ffmpeg、或者你自己写的测试程序。它们通过/dev/videoX节点跟内核打交道。
  • 核心层:V4L2框架本身,提供统一的ioctl接口、buffer管理、设备注册等机制。这部分你基本不用改,内核已经帮你写好了。
  • 驱动层:这才是我们嵌入式工程师的主战场。每个具体的摄像头传感器、每个SoC的ISP控制器,都需要对应的驱动。

核心概念:V4L2把摄像头抽象为一个“视频设备”,通过open()close()ioctl()等标准文件操作来访问。关键ioctl命令包括:

  • VIDIOC_QUERYCAP:查询设备能力
  • VIDIOC_ENUM_FMT:枚举支持的格式
  • VIDIOC_S_FMT:设置格式
  • VIDIOC_REQBUFS:申请buffer
  • VIDIOC_QBUF / VIDIOC_DQBUF:buffer入队/出队
  • VIDIOC_STREAMON / VIDIOC_STREAMOFF:启动/停止流

你想想看,如果没有V4L2,每个摄像头厂商都搞一套自己的API,那应用层程序员不得疯掉?V4L2的价值就在这里——它定义了一套标准接口,让上层应用不用关心底层硬件是啥。

2.2 Camera硬件接口:MIPI CSI 与 DVP

搞清楚了软件框架,咱们再来看看硬件接口。嵌入式Camera最常用的两种接口就是MIPI CSI和DVP。

2.2.1 MIPI CSI(Camera Serial Interface)

MIPI CSI是目前的主流方案。它采用差分信号传输,用LVDS(低压差分信号)技术,说白了就是一对线传一个信号,抗干扰能力强,能跑高速。

MIPI CSI有几个关键特点:

  • 串行传输:数据通过差分对串行发送,需要的引脚少。一个4-lane的MIPI接口,只需要4对数据线+1对时钟线,总共10根线。
  • 高速率:每lane可以跑到1Gbps甚至更高,4-lane就是4Gbps,1080p@60fps绰绰有余。
  • 低功耗:因为是差分信号,电压摆幅小,功耗比DVP低不少。

我在项目中遇到过一个问题:MIPI信号布线没做好,导致摄像头在高温下出现花屏。后来查了半天,发现是差分对等长没控制好,信号歪了。嗯,这里要注意——MIPI的PCB布线,差分对等长是命根子。

2.2.2 DVP(Digital Video Port)

DVP是传统的并行接口。它用一组并行数据线(通常是8位或10位)加上行同步、场同步、像素时钟等信号来传输数据。

DVP的特点:

  • 并行传输:数据线多,占用引脚多。一个8位的DVP接口,至少需要8根数据线+3根控制线+1根时钟线,共12根。
  • 速率受限:并行信号容易受干扰,速率上不去。一般DVP做到720p@30fps就差不多了,再高就容易出问题。
  • 布线麻烦:所有数据线必须等长,而且时钟线要跟数据线保持特定相位关系。我调过一块DVP的板子,就因为时钟线长了2mm,导致数据采样出错。

我的建议:新项目尽量选MIPI CSI。DVP虽然便宜,但调试起来太折腾了。除非你做的产品对成本极度敏感,或者分辨率要求很低(比如VGA级别),否则别碰DVP。

2.3 驱动分层结构

好,现在咱们把软件和硬件串起来,看看Camera驱动到底是怎么分层的。我习惯把它画成四层:

层次 职责 典型文件
应用层 调用V4L2 API,获取图像数据 test_app.c, gstreamer
V4L2核心层 提供统一接口,管理video设备节点 videodev2.h, v4l2-dev.c
V4L2子设备层 管理传感器、ISP等子设备 v4l2-subdev.c, subdev_ops
硬件驱动层 直接操作硬件寄存器 ov5640.c, imx219.c, csi_host.c

这里我要重点说一下子设备层。V4L2从2.6.26版本开始引入了子设备(subdev)模型。为什么要搞这个?因为一个Camera模组里,往往不止一个芯片——有传感器(sensor)、有ISP、有闪光灯控制器、有马达驱动(用于自动对焦)。每个芯片都需要一个驱动,但它们又协同工作。子设备模型就是用来管理这种“多芯片协作”场景的。

举个例子,一个典型的MIPI Camera驱动,在设备树里会这样描述:

// 设备树中的Camera节点
i2c@ff3e0000 {
    camera_sensor: camera@3c {
        compatible = "sony,imx219";
        reg = <0x3c>;
        clocks = <&clk_ext_camera>;
        clock-names = "xvclk";
        
        // MIPI CSI-2 配置
        port {
            sensor_out: endpoint {
                remote-endpoint = <&csi_in>;
                data-lanes = <1 2>;  // 使用2-lane MIPI
                clock-noncontinuous;
            };
        };
    };
};

csi@ffb00000 {
    compatible = "rockchip,rk3288-csi";
    port {
        csi_in: endpoint {
            remote-endpoint = <&sensor_out>;
        };
    };
};

你看,设备树里把传感器和CSI控制器分别描述,然后用remote-endpoint把它们连接起来。驱动加载时,V4L2框架会自动解析这个连接关系,把sensor子设备和CSI控制器子设备绑定到一起。

避坑指南:我曾经在调试一个双摄像头方案时,发现两个sensor的i2c地址冲突了。原因是设备树里忘了给第二个sensor指定不同的reg地址。嗯,这种低级错误,查起来真要命。所以写设备树时,一定要仔细核对每个器件的地址、时钟、复位引脚这些基础信息。

驱动分层的好处是什么?说白了就是“各司其职,互不干扰”。sensor驱动只管配置传感器寄存器、输出图像数据;CSI控制器驱动只管接收MIPI信号、把数据送到内存;ISP驱动只管做图像处理。每一层都只关心自己的事,出了问题也容易定位。

我个人的经验是:调试Camera驱动时,先确认硬件层有没有数据出来(用示波器看MIPI时钟和数据线),再确认V4L2层能不能正确枚举格式和申请buffer,最后才去调应用层的显示效果。按这个顺序来,能省掉至少一半的排查时间。

好,这一章就到这里。下一章咱们会深入设备树,看看Camera相关的binding到底怎么写。到时候我会拿几个实际项目里的设备树片段来拆解,保证让你看完就能上手。