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 走到应用层:
你看,数据从 Sensor 出来,经过 ISP 处理,再通过 Video Device 送到应用层。Media Controller 就像个调度员,告诉内核这些模块怎么连的。
4.2 编写 Sensor 驱动
写一个标准的 Sensor 驱动,核心就是实现 V4L2 Subdev 接口。我习惯把驱动分成几个模块来写,这样结构清晰,也方便后续维护。
4.2.1 驱动框架
一个完整的 Sensor 驱动包含以下部分:
- I2C 通信层:读写 Sensor 寄存器
- Subdev 核心结构:实现 v4l2_subdev_ops
- Pad 与格式配置:定义支持的输出格式和分辨率
- 控制接口:曝光、增益、白平衡等参数调节
- 电源管理:上电/下电时序
我的习惯:先搭好 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 初始化方式就不太一样。写驱动前先确认内核版本,省得踩坑。