3. Sensor驱动框架:Linux V4L2框架剖析、Media Controller架构、设备树配置、驱动注册流程

做Sensor驱动,说白了就是跟Linux内核的几大框架打交道。V4L2、Media Controller、设备树,这三样东西你绕不开。我刚开始接触时也觉得头大,但摸清套路后会发现,它们其实分工很明确。

3.1 V4L2框架:视频设备的标准接口

V4L2(Video for Linux 2)是Linux内核里处理视频设备的通用框架。你的Sensor最终暴露给用户空间的,就是V4L2的设备节点——通常是/dev/videoX。

V4L2的核心思想是“桥接”。它把硬件能力抽象成一组标准IOCTL,用户空间程序通过调用这些IOCTL来控制Sensor。我习惯把V4L2框架分成三层来看:

层次 说明 我关注的点
V4L2设备核心 注册video_device,提供fops 注意v4l2_fh的初始化顺序
V4L2子设备 用v4l2_subdev表示Sensor本身 子设备的pad和ops必须配齐
V4L2控制框架 管理曝光、增益、白平衡等参数 控制ID不要跟内核已有的冲突

驱动里最核心的数据结构是v4l2_subdev。它代表一个独立的硬件单元——你的Sensor就是这样一个子设备。我写驱动时,第一步永远是填充这个结构体:

static struct v4l2_subdev_ops sensor_subdev_ops = {
    .core   = &sensor_core_ops,
    .video  = &sensor_video_ops,
    .pad    = &sensor_pad_ops,
};

static struct v4l2_subdev sensor_subdev = {
    .ops    = &sensor_subdev_ops,
    .owner  = THIS_MODULE,
};

嗯,这里要注意:pad_ops里的get_fmt和set_fmt是ISP调优的入口。你输出的格式不对,后面ISP拿到的数据就是错的。

我的习惯:在注册v4l2_subdev之前,先把所有控制(controls)初始化好。因为子设备注册后,用户空间可能立刻查询控制列表,你还没准备好就会出问题。

3.2 Media Controller架构:管好你的数据通路

V4L2管的是单个设备,但一个摄像头系统里不止Sensor——还有CSI控制器、ISP、DMA引擎。Media Controller就是用来描述这些硬件之间的连接关系的。

它的核心概念是“实体(entity)”和“链路(link)”。每个硬件模块是一个实体,实体之间有数据流方向的链路。我举个例子:

  • 实体A:Sensor(输出一个pad)
  • 实体B:CSI接收器(输入一个pad,输出一个pad)
  • 实体C:ISP(输入一个pad,输出一个pad)
  • 链路:A→B→C

用户空间通过MEDIA_IOC_ENUM_LINKS和MEDIA_IOC_SETUP_LINK来配置这些连接。我曾经在一个项目里遇到图像花屏,查了两天发现是Media Controller的链路没使能——数据根本没从Sensor流到ISP。

避坑指南:Media Controller的链路默认是禁用的。你必须在驱动初始化时显式调用media_create_pad_link()并设置MEDIA_LNK_FL_ENABLED。我曾经忘了加这个标志,结果图像一直黑屏。

Media Controller和V4L2是怎么配合的?看这张图就清楚了:

V4L2 + Media Controller 架构关系图 用户空间应用 /dev/videoX (V4L2设备节点) V4L2核心 video_device / vb2 Media Controller media_device / 链路 Sensor (v4l2_subdev) CSI接收器 (v4l2_subdev) ISP (v4l2_subdev) V4L2负责设备抽象和IOCTL接口,Media Controller负责实体间的数据通路管理

3.3 设备树配置:硬件信息的载体

设备树(Device Tree)是描述硬件拓扑的文本文件。你的Sensor挂在哪个I2C总线上?使用哪个CSI接口?电源时序怎么控制?这些信息都在设备树里。

一个典型的Sensor设备树节点长这样:

i2c@ff160000 {
    sensor@3c {
        compatible = "sony,imx290";
        reg = <0x3c>;
        clocks = <&clk_ext_camera>;
        clock-names = "xvclk";
        avdd-supply = <&reg_camera_avdd>;
        dvdd-supply = <&reg_camera_dvdd>;
        iovdd-supply = <&reg_camera_iovdd>;
        reset-gpios = <&gpio4 15 GPIO_ACTIVE_LOW>;
        port {
            sensor_out: endpoint {
                remote-endpoint = <&csi_in>;
                data-lanes = <1 2 3 4>;
                link-frequencies = /bits/ 64 <450000000>;
            };
        };
    };
};

我特别想强调port和endpoint这部分。它定义了Sensor的数据输出连接到哪个CSI接收器。你想想看,如果remote-endpoint指向错了,或者data-lanes配少了,图像数据就会丢通道。

关键字段速查:
  • compatible:驱动匹配的关键字符串,必须跟驱动里的of_match_table一致
  • reg:I2C从设备地址,注意7位还是8位地址格式
  • clocks:主时钟,频率不对Sensor不工作
  • reset-gpios:复位引脚,极性别搞反
  • data-lanes:MIPI数据通道数,跟硬件连线必须一致

3.4 驱动注册流程:从probe到运行

驱动注册的流程,说白了就是内核怎么把你的Sensor驱动加载起来。我把它拆成几个关键步骤:

  1. 匹配设备树:内核遍历设备树节点,找到compatible匹配的驱动,调用probe函数。
  2. 初始化硬件:在probe里,先获取时钟、电源、GPIO等资源,然后复位Sensor,读取芯片ID确认通信正常。
  3. 注册V4L2子设备:填充v4l2_subdev结构体,调用v4l2_i2c_subdev_init()或v4l2_spi_subdev_init()注册。
  4. 注册Media Controller实体:创建media_entity,设置pad数量,调用media_entity_pads_init()。
  5. 注册控制:用v4l2_ctrl_handler_init()和v4l2_ctrl_new_std()添加曝光、增益等控制。
  6. 建立链路:调用media_create_pad_link()连接Sensor和CSI接收器。

我见过不少新手在probe里一股脑把所有事情做完,结果资源没准备好就注册子设备,导致系统崩溃。我的建议是:先获取所有资源,确认硬件在线,最后再注册到框架里。

static int sensor_probe(struct i2c_client *client) {
    struct sensor_dev *sensor;
    int ret;

    // 1. 分配私有数据结构
    sensor = devm_kzalloc(&client->dev, sizeof(*sensor), GFP_KERNEL);
    if (!sensor)
        return -ENOMEM;

    // 2. 获取时钟和电源
    sensor->xvclk = devm_clk_get(&client->dev, "xvclk");
    if (IS_ERR(sensor->xvclk))
        return PTR_ERR(sensor->xvclk);

    // 3. 复位Sensor并读取芯片ID
    gpiod_set_value_cansleep(sensor->reset_gpio, 1);
    usleep_range(10000, 12000);
    gpiod_set_value_cansleep(sensor->reset_gpio, 0);
    usleep_range(10000, 12000);

    ret = sensor_read_reg(client, SENSOR_REG_CHIP_ID, &chip_id);
    if (ret || chip_id != SENSOR_EXPECTED_ID) {
        dev_err(&client->dev, "chip ID mismatch\n");
        return -ENODEV;
    }

    // 4. 初始化V4L2子设备
    v4l2_i2c_subdev_init(&sensor->sd, client, &sensor_subdev_ops);

    // 5. 初始化Media Controller实体
    sensor->sd.entity.function = MEDIA_ENT_F_CAM_SENSOR;
    ret = media_entity_pads_init(&sensor->sd.entity, 1, &sensor->pad);
    if (ret)
        return ret;

    // 6. 注册控制
    v4l2_ctrl_handler_init(&sensor->ctrl_handler, 5);
    // ... 添加具体控制

    return 0;
}
我曾经踩过的坑:在probe里调用v4l2_i2c_subdev_init()之后,又去修改v4l2_subdev的entity字段。这个函数内部已经初始化了entity,你再覆盖会导致内存泄漏。正确的做法是在调用之前就把entity配置好。

驱动注册完成后,用户空间通过open("/dev/videoX")就能访问Sensor了。但别忘了,这只是第一步。后面还有流式传输(stream on/off)、缓冲区管理(vb2)、参数调优等一系列工作等着你。