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_create_pad_link()并设置MEDIA_LNK_FL_ENABLED。我曾经忘了加这个标志,结果图像一直黑屏。
Media Controller和V4L2是怎么配合的?看这张图就清楚了:
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 = <®_camera_avdd>;
dvdd-supply = <®_camera_dvdd>;
iovdd-supply = <®_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驱动加载起来。我把它拆成几个关键步骤:
- 匹配设备树:内核遍历设备树节点,找到
compatible匹配的驱动,调用probe函数。 - 初始化硬件:在
probe里,先获取时钟、电源、GPIO等资源,然后复位Sensor,读取芯片ID确认通信正常。 - 注册V4L2子设备:填充
v4l2_subdev结构体,调用v4l2_i2c_subdev_init()或v4l2_spi_subdev_init()注册。 - 注册Media Controller实体:创建
media_entity,设置pad数量,调用media_entity_pads_init()。 - 注册控制:用
v4l2_ctrl_handler_init()和v4l2_ctrl_new_std()添加曝光、增益等控制。 - 建立链路:调用
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)、参数调优等一系列工作等着你。