4. Sensor驱动框架解析(Linux Kernel)
好,我们进入第四章。这一章是驱动开发的骨架——V4L2子系统。说白了,你要让Camera Sensor在Linux下工作,就必须搞懂V4L2怎么组织、sensor驱动怎么挂上去。
我刚开始接触V4L2时,觉得它就是个复杂的视频框架。后来调了几个sensor,才明白它其实是个“万能插座”——应用层、内核层、硬件层,全靠它来对接。
4.1 V4L2子系统整体架构
V4L2(Video for Linux 2)是Linux内核中视频设备的统一框架。它把摄像头、TV调谐器、编解码器等设备抽象成统一的字符设备。
先看一张整体架构图,帮你建立全局认知:
从这张图你能看到,V4L2不是单层结构,而是分层的。从上到下依次是:应用层、系统调用层、V4L2核心层、Bridge驱动层、Sensor驱动层,最后是硬件。
我个人习惯把V4L2驱动分成两类:
- Bridge驱动:负责SoC端的Camera接口(CSI/MIPI/Parallel),管理DMA、时钟、电源。
- Subdev驱动:负责外设(Sensor、Flash、AF),通过I2C/SPI控制寄存器。
你写sensor驱动,主要工作在subdev这一层。
4.2 Sensor驱动的核心结构体
写sensor驱动,你绕不开两个结构体:v4l2_subdev 和 i2c_client。它们就像驱动的心脏和血管。
4.2.1 i2c_client:硬件通信的桥梁
i2c_client 是I2C设备在内核中的代表。每个挂在I2C总线上的sensor芯片,都会对应一个 i2c_client 实例。
struct i2c_client {
unsigned short flags; // 标志位
unsigned short addr; // I2C从设备地址(7位或10位)
char name[I2C_NAME_SIZE]; // 设备名称
struct i2c_adapter *adapter; // 所属I2C适配器
struct device dev; // 内核设备模型
int irq; // 中断号(如果有)
struct list_head detected; // 设备链表
};
嗯,这里要注意:addr 是7位地址,不是8位。我见过不少新手把8位地址填进去,结果I2C通信死活不通。你想想看,7位地址左移一位才是8位读写地址,这个坑我踩过。
4.2.2 v4l2_subdev:V4L2子设备的抽象
v4l2_subdev 是V4L2框架中所有子设备的统一抽象。不管是sensor、闪光灯还是自动对焦,都用它来管理。
struct v4l2_subdev {
struct v4l2_device *v4l2_dev; // 所属V4L2设备
const struct v4l2_subdev_ops *ops; // 操作函数集
const struct v4l2_subdev_internal_ops *internal_ops;
struct v4l2_ctrl_handler *ctrl_handler; // 控制句柄
struct device *dev; // 内核设备指针
struct list_head list; // 链表节点
char name[V4L2_SUBDEV_NAME_SIZE]; // 子设备名称
u32 flags; // 标志位
void *host_priv; // 私有数据
struct video_device *devnode; // 视频设备节点(如果有)
};
这里最关键的是 ops 字段。它指向 v4l2_subdev_ops,里面定义了sensor支持的所有操作:
| 操作集 | 主要回调函数 | 用途 |
|---|---|---|
| core | s_power, ioctl, init | 电源管理、通用控制 |
| video | s_stream, g_frame_interval | 视频流控制、帧率获取 |
| pad | get_fmt, set_fmt, enum_mbus_code | 媒体总线格式协商 |
| sensor | g_skip_frames, s_exposure | sensor专用控制(可选) |
4.3 Probe流程详解
Probe函数是驱动的入口点。当内核发现I2C设备与驱动匹配时,就会调用probe。我把它拆成6个步骤,每一步都有讲究。
4.3.1 步骤一:设备树匹配与I2C探测
设备树中定义sensor节点,驱动通过 of_match_table 匹配:
static const struct of_device_id ov5640_of_match[] = {
{ .compatible = "ovti,ov5640" },
{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, ov5640_of_match);
static const struct i2c_device_id ov5640_id[] = {
{ "ov5640", 0 },
{ }
};
MODULE_DEVICE_TABLE(i2c, ov5640_id);
static struct i2c_driver ov5640_i2c_driver = {
.driver = {
.name = "ov5640",
.of_match_table = ov5640_of_match,
},
.probe = ov5640_probe,
.remove = ov5640_remove,
.id_table = ov5640_id,
};
匹配成功后,内核调用 ov5640_probe,传入 i2c_client 指针。
4.3.2 步骤二:分配并初始化私有数据结构
每个sensor驱动都需要一个私有结构体来保存状态:
struct ov5640 {
struct v4l2_subdev sd; // 子设备
struct i2c_client *client; // I2C客户端
struct v4l2_ctrl_handler ctrl_handler; // 控制句柄
struct mutex lock; // 互斥锁
struct regulator *avdd; // 模拟电源
struct regulator *dvdd; // 数字电源
struct regulator *dovdd; // IO电源
struct clk *xclk; // 外部时钟
int power_count; // 电源引用计数
struct v4l2_mbus_framefmt fmt; // 当前格式
unsigned int fps; // 帧率
};
我习惯在probe开头用 devm_kzalloc 分配内存,省去手动释放的麻烦。
4.3.3 步骤三:获取并使能硬件资源
从设备树获取电源、时钟、复位GPIO等资源:
static int ov5640_probe(struct i2c_client *client)
{
struct ov5640 *sensor;
int ret;
sensor = devm_kzalloc(&client->dev, sizeof(*sensor), GFP_KERNEL);
if (!sensor)
return -ENOMEM;
sensor->client = client;
// 获取电源
sensor->avdd = devm_regulator_get(&client->dev, "avdd");
if (IS_ERR(sensor->avdd))
return PTR_ERR(sensor->avdd);
// 获取时钟
sensor->xclk = devm_clk_get(&client->dev, "xclk");
if (IS_ERR(sensor->xclk))
return PTR_ERR(sensor->xclk);
// 使能时钟
ret = clk_prepare_enable(sensor->xclk);
if (ret)
return ret;
// 上电
ret = ov5640_power_on(sensor);
if (ret)
goto err_clk;
}
4.3.4 步骤四:读取芯片ID验证硬件
通过I2C读取sensor的芯片ID,确认硬件连接正确:
// 读取芯片ID
ret = ov5640_read_reg(sensor, OV5640_REG_CHIP_ID_HIGH, &chip_id_high);
if (ret)
goto err_power;
ret = ov5640_read_reg(sensor, OV5640_REG_CHIP_ID_LOW, &chip_id_low);
if (ret)
goto err_power;
chip_id = (chip_id_high << 8) | chip_id_low;
if (chip_id != OV5640_CHIP_ID) {
dev_err(&client->dev, "chip ID mismatch: 0x%04x\n", chip_id);
ret = -ENODEV;
goto err_power;
}
这一步是硬性检查。如果ID读不到或不对,说明I2C通信有问题,或者sensor没上电。我曾经因为电源纹波太大,导致ID读取偶尔失败,排查了两天才找到原因。
4.3.5 步骤五:初始化V4L2子设备与控制句柄
这是核心步骤,把sensor注册到V4L2框架中:
// 初始化互斥锁
mutex_init(&sensor->lock);
// 初始化控制句柄
v4l2_ctrl_handler_init(&sensor->ctrl_handler, 8);
sensor->sd.ctrl_handler = &sensor->ctrl_handler;
// 添加V4L2控制
v4l2_ctrl_new_std(&sensor->ctrl_handler, &ov5640_ctrl_ops,
V4L2_CID_BRIGHTNESS, 0, 255, 1, 128);
v4l2_ctrl_new_std(&sensor->ctrl_handler, &ov5640_ctrl_ops,
V4L2_CID_CONTRAST, 0, 255, 1, 128);
// ... 更多控制
if (sensor->ctrl_handler.error) {
ret = sensor->ctrl_handler.error;
goto err_ctrl;
}
// 初始化子设备
v4l2_i2c_subdev_init(&sensor->sd, client, &ov5640_subdev_ops);
sensor->sd.flags |= V4L2_SUBDEV_FL_HAS_DEVNODE;
sensor->sd.entity.function = MEDIA_ENT_F_CAM_SENSOR;
v4l2_i2c_subdev_init 这个函数很关键,它把 i2c_client 和 v4l2_subdev 绑定在一起,同时设置 sd->dev 指向 &client->dev。
4.3.6 步骤六:注册子设备到媒体框架
最后一步,把子设备注册到V4L2设备中:
// 注册子设备
ret = v4l2_async_register_subdev(&sensor->sd);
if (ret)
goto err_ctrl;
dev_info(&client->dev, "OV5640 sensor registered\n");
return 0;
err_ctrl:
v4l2_ctrl_handler_free(&sensor->ctrl_handler);
err_power:
ov5640_power_off(sensor);
err_clk:
clk_disable_unprepare(sensor->xclk);
return ret;
}
这里用 v4l2_async_register_subdev 而不是 v4l2_device_register_subdev,是因为现代Camera驱动都采用异步探测。Bridge驱动先注册,sensor驱动后注册,两者通过异步框架匹配。
4.4 Probe流程全景图
我把整个probe流程画成一张图,方便你对照:
4.5 常见问题与避坑指南
问题1:Probe成功但s_stream失败
我遇到过几次,probe一切正常,但应用层调用stream on时sensor没反应。排查后发现是电源引用计数没处理好。s_power回调里要维护一个power_count,上电+1,下电-1,只有count从0变1时才真正操作硬件。
问题2:芯片ID读取不稳定
曾经有个项目,sensor在低温下ID读取偶尔失败。最后发现是I2C时序问题——上电后需要等待至少10ms才能访问I2C。datasheet里写了,但我没仔细看。嗯,从此以后我都在probe里加了个msleep(20)。
问题3:异步注册导致Bridge找不到sensor
如果你用 v4l2_async_register_subdev,一定要确保Bridge驱动中定义了正确的 asd(异步子设备描述符)。我曾经因为设备树中sensor节点的endpoint属性写错了,导致异步匹配失败,sensor注册了但Bridge就是找不到它。
4.6 小结
这一章我们拆解了V4L2子系统的架构,深入分析了 v4l2_subdev 和 i2c_client 两个核心结构体,然后一步步走通了probe流程。说白了,sensor驱动的骨架就是这些内容。
你写驱动时,记住这个流程:匹配 → 分配资源 → 验证硬件 → 注册框架。每一步都有对应的内核API,用熟了自然就顺了。