3. Sensor驱动框架:Linux V4L2驱动框架解析
做Sensor驱动,绕不开V4L2。说白了,它就是Linux内核里给视频设备定的一套标准接口。我刚开始接触时也觉得这东西很绕,又是video_device又是v4l2_subdev,关系理不清。后来调了两年驱动,慢慢就摸出门道了。
V4L2框架分两层:上层是video_device,面向用户空间的/dev/videoX节点;下层是v4l2_subdev,面向具体的硬件设备,比如Sensor、ISP、MIPI桥接芯片。Sensor驱动就属于subdev这一层。
核心思路:Sensor驱动本质上是向V4L2框架注册一个subdev,然后通过控制接口(I2C/SPI)操作寄存器,配置输出格式、帧率、增益等参数。
3.1 V4L2框架的分层结构
先看一张整体框架图,我画了张SVG帮你理清关系:
这张图你看懂了吗?用户空间通过/dev/videoX发ioctl,V4L2核心层把请求路由到对应的video_device,video_device再调用注册好的subdev回调函数。Sensor驱动就在最下面的subdev位置。
3.2 Sensor子设备注册流程
Sensor驱动的入口是probe函数。我习惯把注册流程拆成5步,每一步都有对应的内核API:
| 步骤 | 操作 | 关键API/结构体 | 说明 |
|---|---|---|---|
| 1 | 分配subdev | v4l2_subdev_alloc() | 分配并初始化v4l2_subdev结构体 |
| 2 | 设置subdev ops | v4l2_subdev_core_ops v4l2_subdev_video_ops v4l2_subdev_pad_ops |
注册回调函数,核心是s_stream、s_fmt、g_frame_interval |
| 3 | 初始化控制 | v4l2_ctrl_handler_init() v4l2_ctrl_new_std() |
注册曝光、增益、白平衡等控制项 |
| 4 | 注册subdev | v4l2_async_register_subdev() | 异步注册,等待与bridge绑定 |
| 5 | 媒体实体注册 | media_entity_pads_init() | 定义sink/source pad,用于pipeline管理 |
嗯,这里要注意第4步。早期内核用v4l2_i2c_subdev_init()同步注册,现在主流平台都用异步方式。为什么?因为Sensor上电时序和MIPI时钟可能依赖其他设备先就绪,异步注册能解耦这种依赖关系。
我的经验:我在调试IMX335时遇到过一个问题——probe成功了,但s_stream就是报错。查了两天才发现是异步注册时,bridge还没准备好就调了s_stream。后来在subdev的registered回调里加了个标志位,等bridge绑定完成再允许stream on,问题解决。
3.3 I2C/SPI控制接口实现
Sensor芯片的控制接口,99%是I2C,少数高速或特殊场景用SPI。我主要讲I2C,SPI原理类似。
I2C读写函数是Sensor驱动的基石。每个寄存器操作都要通过它。看一个典型实现:
// I2C写寄存器函数
static int sensor_write_reg(struct i2c_client *client, u16 reg, u8 val)
{
struct i2c_msg msg;
u8 buf[3];
int ret;
buf[0] = (reg >> 8) & 0xFF; // 寄存器高字节
buf[1] = reg & 0xFF; // 寄存器低字节
buf[2] = val; // 写入值
msg.addr = client->addr;
msg.flags = 0; // 写操作
msg.len = 3;
msg.buf = buf;
ret = i2c_transfer(client->adapter, &msg, 1);
if (ret < 0) {
dev_err(&client->dev, "write reg 0x%04x failed: %d\n", reg, ret);
return ret;
}
return 0;
}
// I2C读寄存器函数
static int sensor_read_reg(struct i2c_client *client, u16 reg, u8 *val)
{
struct i2c_msg msg[2];
u8 reg_buf[2];
int ret;
reg_buf[0] = (reg >> 8) & 0xFF;
reg_buf[1] = reg & 0xFF;
msg[0].addr = client->addr;
msg[0].flags = 0; // 写寄存器地址
msg[0].len = 2;
msg[0].buf = reg_buf;
msg[1].addr = client->addr;
msg[1].flags = I2C_M_RD; // 读数据
msg[1].len = 1;
msg[1].buf = val;
ret = i2c_transfer(client->adapter, msg, 2);
if (ret < 2) {
dev_err(&client->dev, "read reg 0x%04x failed: %d\n", reg, ret);
return ret < 0 ? ret : -EIO;
}
return 0;
}
这段代码看着简单,但坑不少。我列几个常见的:
- 寄存器地址宽度:有些Sensor是8位地址,有些是16位。我遇到过一款芯片手册写16位,实际是8位,读出来的值全错。
- I2C速率:默认100kHz,但很多Sensor支持400kHz甚至1MHz。速率不匹配会导致通信不稳定。
- 批量读写:初始化时往往要写几十个寄存器,用单次读写效率太低。我习惯加一个
sensor_write_array()函数,把reg_addr + reg_val做成表,一次i2c_transfer搞定。
我曾经踩过的坑:有一次Sensor在低温下初始化失败,查了三天发现是I2C时序在低温下变差,需要调整I2C总线的上升时间。后来在设备树里加了i2c-scl-rising-time-ns参数,问题解决。所以,别小看I2C时序,它跟温度、PCB走线都有关系。
3.4 设备树与驱动绑定
现代Linux驱动都走设备树(Device Tree)。Sensor的设备树节点长这样:
// imx335设备树节点示例
&i2c2 {
status = "okay";
clock-frequency = <400000>; // I2C速率400kHz
imx335: camera-sensor@1a {
compatible = "sony,imx335";
reg = <0x1a>; // I2C地址
clocks = <&clk IMX335_XVCLK>;
clock-names = "xvclk";
reset-gpios = <&gpio3 15 GPIO_ACTIVE_LOW>;
powerdown-gpios = <&gpio3 16 GPIO_ACTIVE_HIGH>;
// MIPI CSI-2 配置
port {
imx335_ep: endpoint {
remote-endpoint = <&csi2_ep>;
data-lanes = <1 2 3 4>;
link-frequencies = /bits/ 64 <594000000>;
};
};
};
};
驱动里通过of_match_table匹配设备树节点:
static const struct of_device_id sensor_of_match[] = {
{ .compatible = "sony,imx335" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, sensor_of_match);
static struct i2c_driver sensor_i2c_driver = {
.driver = {
.name = "imx335",
.of_match_table = sensor_of_match,
},
.probe = sensor_probe,
.remove = sensor_remove,
.id_table = sensor_i2c_id,
};
module_i2c_driver(sensor_i2c_driver);
probe函数被调用时,i2c_client已经帮你配好了设备地址、时钟、GPIO等信息。你只需要从设备树里解析出这些资源,然后初始化Sensor硬件。
3.5 控制接口的封装策略
我见过很多新手把I2C读写函数直接暴露给上层调用。这不好。我建议做一层封装:
| 层级 | 函数 | 职责 |
|---|---|---|
| 底层 | sensor_read_reg / sensor_write_reg | 直接操作I2C/SPI总线,单寄存器读写 |
| 中间层 | sensor_set_gain / sensor_set_exposure | 封装增益、曝光等具体功能的寄存器操作序列 |
| 上层 | v4l2_ctrl_ops.s_ctrl | V4L2控制框架回调,调用中间层函数 |
这样做的好处是:哪天换Sensor了,只需要改中间层和底层,上层V4L2接口完全不用动。我在项目中用这个分层结构,换过三次Sensor型号,每次只改不到200行代码。
总结一下:Sensor驱动框架的核心就是V4L2 subdev + I2C控制接口。subdev负责对接V4L2框架,I2C负责操作硬件。把这两块搞明白,Sensor驱动就拿下了一半。另一半是MIPI CSI-2的配置和同步,我们后面章节会详细讲。