4. Linux V4L2框架与Sensor驱动

V4L2,全称 Video for Linux 2,是 Linux 内核里处理视频设备的统一框架。说白了,它就是一套标准接口,让上层应用不用关心底层 Sensor 是索尼的还是安森美的。

我刚开始接触 V4L2 时,觉得它层级多、概念杂。后来调了几年 Sensor 驱动,才真正理解这套设计的精妙之处。今天咱们就把它拆开揉碎了讲。

4.1 V4L2 核心架构

V4L2 框架由三个核心组件构成:Video Device、Subdev 和 Media Controller。它们各司其职,又相互配合。

一句话总结:Video Device 管数据流,Subdev 管硬件配置,Media Controller 管拓扑连接。

4.1.1 三个组件的职责

组件 角色 典型节点 核心功能
Video Device 数据通道 /dev/video0 buffer 管理、帧采集、流控制
Subdev 硬件抽象 /dev/v4l-subdev0 Sensor 寄存器配置、曝光/增益控制
Media Controller 拓扑管理 /dev/media0 链路连接、Pad 路由、实体管理

举个例子。你写一个摄像头应用,打开 /dev/video0 就能拿到图像数据。但图像参数怎么配?曝光、增益、帧率这些,得通过 Subdev 的 ioctl 去设置。那 Sensor 和 ISP 之间怎么连的?Media Controller 告诉你。

4.1.2 数据流路径

一张图看懂数据怎么从 Sensor 走到应用层:

V4L2 数据流路径 Sensor (Subdev 实体) ISP (Subdev 实体) Video Device /dev/video0 应用层 原始 RAW YUV/Bayer 帧数据 Media Controller 管理实体间的链路连接 配置 Pad 路由与数据格式

你看,数据从 Sensor 出来,经过 ISP 处理,再通过 Video Device 送到应用层。Media Controller 就像个调度员,告诉内核这些模块怎么连的。

4.2 编写 Sensor 驱动

写一个标准的 Sensor 驱动,核心就是实现 V4L2 Subdev 接口。我习惯把驱动分成几个模块来写,这样结构清晰,也方便后续维护。

4.2.1 驱动框架

一个完整的 Sensor 驱动包含以下部分:

  1. I2C 通信层:读写 Sensor 寄存器
  2. Subdev 核心结构:实现 v4l2_subdev_ops
  3. Pad 与格式配置:定义支持的输出格式和分辨率
  4. 控制接口:曝光、增益、白平衡等参数调节
  5. 电源管理:上电/下电时序

我的习惯:先搭好 I2C 读写函数,再填充 Subdev 回调。这样调试时可以直接用 i2c-tools 验证硬件通信,不用等驱动写完。

4.2.2 核心代码骨架

下面是一个简化的 Sensor 驱动框架。注意看 Subdev 的注册流程:

#include <linux/i2c.h>
#include <media/v4l2-subdev.h>

struct sensor_dev {
    struct i2c_client *client;
    struct v4l2_subdev sd;
    struct v4l2_ctrl_handler ctrl_handler;
    struct v4l2_mbus_framefmt fmt;
    int power_count;
};

/* I2C 读写封装 */
static int sensor_read_reg(struct i2c_client *client, u16 reg, u8 *val)
{
    struct i2c_msg msg[2];
    u8 buf[2] = { reg >> 8, reg & 0xFF };
    /* ... 实现 I2C 读取 ... */
    return 0;
}

static int sensor_write_reg(struct i2c_client *client, u16 reg, u8 val)
{
    /* ... 实现 I2C 写入 ... */
    return 0;
}

/* Subdev 回调:获取当前格式 */
static int sensor_get_fmt(struct v4l2_subdev *sd,
                          struct v4l2_subdev_pad_config *cfg,
                          struct v4l2_subdev_format *format)
{
    struct sensor_dev *sensor = v4l2_get_subdevdata(sd);
    format->format = sensor->fmt;
    return 0;
}

/* Subdev 回调:设置格式 */
static int sensor_set_fmt(struct v4l2_subdev *sd,
                          struct v4l2_subdev_pad_config *cfg,
                          struct v4l2_subdev_format *format)
{
    struct sensor_dev *sensor = v4l2_get_subdevdata(sd);
    /* 检查分辨率是否支持,调整到最接近的配置 */
    sensor->fmt = format->format;
    return 0;
}

/* Subdev 回调:启动流 */
static int sensor_s_stream(struct v4l2_subdev *sd, int enable)
{
    struct sensor_dev *sensor = v4l2_get_subdevdata(sd);
    if (enable) {
        /* 配置 Sensor 寄存器,开始输出图像 */
        sensor_write_reg(sensor->client, 0x0100, 0x01);
    } else {
        /* 停止输出 */
        sensor_write_reg(sensor->client, 0x0100, 0x00);
    }
    return 0;
}

/* Subdev 操作集 */
static const struct v4l2_subdev_video_ops sensor_video_ops = {
    .s_stream = sensor_s_stream,
};

static const struct v4l2_subdev_pad_ops sensor_pad_ops = {
    .get_fmt = sensor_get_fmt,
    .set_fmt = sensor_set_fmt,
};

static const struct v4l2_subdev_ops sensor_ops = {
    .video = &sensor_video_ops,
    .pad   = &sensor_pad_ops,
};

/* I2C 驱动 probe 函数 */
static int sensor_probe(struct i2c_client *client,
                        const struct i2c_device_id *id)
{
    struct sensor_dev *sensor;
    struct v4l2_subdev *sd;

    sensor = devm_kzalloc(&client->dev, sizeof(*sensor), GFP_KERNEL);
    if (!sensor)
        return -ENOMEM;

    sensor->client = client;
    v4l2_i2c_subdev_init(&sensor->sd, client, &sensor_ops);

    /* 初始化默认格式 */
    sensor->fmt.code = MEDIA_BUS_FMT_SBGGR10_1X10;
    sensor->fmt.width = 1920;
    sensor->fmt.height = 1080;

    /* 注册控制句柄(曝光、增益等) */
    v4l2_ctrl_handler_init(&sensor->ctrl_handler, 4);
    v4l2_ctrl_new_std(&sensor->ctrl_handler, NULL,
                      V4L2_CID_EXPOSURE, 0, 0xFFFF, 1, 0x0800);
    v4l2_ctrl_new_std(&sensor->ctrl_handler, NULL,
                      V4L2_CID_GAIN, 0, 0xFF, 1, 0x40);
    sd->ctrl_handler = &sensor->ctrl_handler;

    return 0;
}

static const struct i2c_device_id sensor_id[] = {
    { "imx290", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, sensor_id);

static struct i2c_driver sensor_driver = {
    .driver = {
        .name = "sensor_drv",
    },
    .probe    = sensor_probe,
    .id_table = sensor_id,
};

module_i2c_driver(sensor_driver);

4.2.3 关键点解析

这段代码看着不长,但有几个地方容易踩坑:

  • v4l2_i2c_subdev_init:这个函数把 I2C client 和 Subdev 绑定在一起。之后你就能通过 /dev/v4l-subdevX 访问 Sensor 了。
  • 格式协商:set_fmt 里要做分辨率检查。我曾经遇到一个项目,Sensor 只支持 1080p,但应用层传了个 720p 进来,驱动没做校验,结果图像全是花屏。
  • 流控制:s_stream 是开关。开启时配寄存器,关闭时清状态。注意加引用计数,防止重复开关。

避坑指南:我曾经在 s_stream 里忘了加电源管理。Sensor 上电后一直没下电,板子待机功耗多了 200mW。后来加了 power_count 引用计数,才把问题解决。

4.3 Media Controller 集成

如果你的系统里 Sensor 后面接了 ISP 或者 MIPI 桥接芯片,那就需要 Media Controller 来管理链路。

4.3.1 创建 Pad 和 Link

每个 Subdev 实体可以有多个 Pad(输入/输出)。Sensor 通常只有一个输出 Pad:

/* 在 probe 中注册 Pad */
static const struct media_entity_operations sensor_media_ops = {
    .link_validate = v4l2_subdev_link_validate,
};

sd->entity.function = MEDIA_ENT_F_CAM_SENSOR;
sd->entity.ops = &sensor_media_ops;

/* 创建 1 个输出 Pad */
sensor->pads[SENSOR_PAD_SOURCE].flags = MEDIA_PAD_FL_SOURCE;
media_entity_pads_init(&sd->entity, 1, sensor->pads);

然后在用户空间用 media-ctl 工具配置链路:

# 将 Sensor 输出连接到 ISP 输入
media-ctl -d /dev/media0 -l '"imx290 0-001a":0 -> "isp":0 [1]'

# 设置 Sensor 输出格式
media-ctl -d /dev/media0 -V '"imx290 0-001a":0 [fmt:SBGGR10_1X10/1920x1080]'

4.3.2 为什么需要 Media Controller?

你想想看,一个复杂的摄像头系统可能有多个 Sensor、多个 ISP、多个输出通道。没有 Media Controller,你怎么知道数据从哪来、到哪去?

我调过一款多目相机,4 个 Sensor 共用 1 个 ISP。如果没有 Media Controller 管理链路,光靠 Video Device 根本分不清哪个数据流对应哪个 Sensor。

4.4 调试与验证

驱动写完了,怎么验证它跑对了?我一般用这几个方法:

方法 命令/工具 验证内容
查看设备节点 ls /dev/v4l* 确认 Subdev 和 Video Device 已注册
枚举格式 v4l2-ctl --list-formats 检查 Sensor 支持的输出格式
抓取一帧 v4l2-ctl --stream-mmap --stream-to=frame.raw 验证图像数据是否正常输出
查看链路 media-ctl -p -d /dev/media0 确认 Pad 和 Link 配置正确

调试技巧:如果抓到的图像全是 0xFF 或 0x00,先别急着调驱动。用示波器量一下 MIPI 时钟和数据线,确认硬件链路通不通。我遇到过好几次,驱动代码没问题,结果是 FPC 排线松了。

4.5 小结

V4L2 框架虽然复杂,但核心逻辑很清晰:Video Device 管数据,Subdev 管配置,Media Controller 管拓扑。写 Sensor 驱动时,你只需要实现 Subdev 的回调函数,注册好 Pad 和格式,剩下的框架帮你搞定。

嗯,这里要注意一点:不同内核版本的 V4L2 API 有差异。比如 4.x 和 5.x 的 Subdev 初始化方式就不太一样。写驱动前先确认内核版本,省得踩坑。