4. Subdev 初始化实战:编写一个简单的 Subdev 驱动模板、probe 函数实现、设备树匹配

本章是课程的第一个实战环节。我们将从零构建一个标准的 V4L2 Subdev 驱动模板,重点实现 probe 函数与设备树(Device Tree)匹配机制。通过本节的动手实践,你将掌握 Subdev 驱动的注册骨架、内核模块生命周期管理以及如何通过设备树节点绑定驱动。

4.1 Subdev 驱动模板结构

一个完整的 Subdev 驱动通常包含以下核心组件:

组件 说明 关键 API / 结构体
模块入口/出口 内核模块的加载与卸载 module_init / module_exit
I2C 驱动结构体 注册到 I2C 子系统,绑定设备树 struct i2c_driver
Subdev 结构体 V4L2 子设备实例,包含 ops struct v4l2_subdev
probe 函数 设备发现时的初始化入口 probe 回调
设备树匹配表 定义 compatible 字符串 struct of_device_id
工程提示: Subdev 驱动通常挂载在 I2C 或 SPI 总线上。本实战以 I2C 子设备为例,这是嵌入式摄像头传感器最常用的连接方式。

4.2 编写 Subdev 驱动模板代码

以下是一个最小化的 Subdev 驱动模板,包含模块声明、I2C 驱动注册、probe 函数骨架和设备树匹配表。

// my_subdev.c — 最小化 Subdev 驱动模板
#include <linux/module.h>
#include <linux/i2c.h>
#include <media/v4l2-subdev.h>
#include <media/v4l2-device.h>

/* 设备结构体:保存每个实例的私有数据 */
struct my_subdev_dev {
    struct v4l2_subdev sd;
    struct i2c_client *client;
    int power_on;
};

/* Subdev 核心操作集(占位) */
static const struct v4l2_subdev_core_ops my_core_ops = {
    .log_status = NULL,   /* 后续章节实现 */
};

/* Subdev 视频操作集(占位) */
static const struct v4l2_subdev_video_ops my_video_ops = {
    .s_stream = NULL,     /* 后续章节实现 */
};

/* Subdev 操作集总表 */
static const struct v4l2_subdev_ops my_subdev_ops = {
    .core = &my_core_ops,
    .video = &my_video_ops,
};

/* probe 函数:设备匹配时调用 */
static int my_subdev_probe(struct i2c_client *client,
                           const struct i2c_device_id *id)
{
    struct my_subdev_dev *dev;
    int ret;

    /* 1. 分配私有数据结构体 */
    dev = devm_kzalloc(&client->dev, sizeof(*dev), GFP_KERNEL);
    if (!dev)
        return -ENOMEM;

    dev->client = client;
    dev->power_on = 0;

    /* 2. 初始化 v4l2_subdev 结构体 */
    v4l2_i2c_subdev_init(&dev->sd, client, &my_subdev_ops);

    /* 3. 设置 Subdev 内部名称(用于调试) */
    snprintf(dev->sd.name, sizeof(dev->sd.name),
             "my_subdev %d-%04x", client->adapter->nr,
             client->addr);

    /* 4. 注册 Subdev 到父设备(v4l2_device) */
    ret = v4l2_device_register_subdev(&client->dev, &dev->sd);
    if (ret < 0) {
        dev_err(&client->dev, "subdev registration failed\n");
        return ret;
    }

    dev_info(&client->dev, "my_subdev probed successfully\n");
    return 0;
}

/* remove 函数:设备移除时调用 */
static int my_subdev_remove(struct i2c_client *client)
{
    struct v4l2_subdev *sd = i2c_get_clientdata(client);
    v4l2_device_unregister_subdev(sd);
    return 0;
}

/* 设备树匹配表 */
static const struct of_device_id my_subdev_of_match[] = {
    { .compatible = "myvendor,my-sensor-v1" },
    { /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, my_subdev_of_match);

/* I2C 设备 ID 表(传统方式) */
static const struct i2c_device_id my_subdev_id[] = {
    { "my-sensor-v1", 0 },
    { }
};
MODULE_DEVICE_TABLE(i2c, my_subdev_id);

/* I2C 驱动结构体 */
static struct i2c_driver my_subdev_driver = {
    .driver = {
        .name   = "my_subdev",
        .of_match_table = my_subdev_of_match,
    },
    .probe    = my_subdev_probe,
    .remove   = my_subdev_remove,
    .id_table = my_subdev_id,
};

/* 模块入口/出口 */
module_i2c_driver(my_subdev_driver);

MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Minimal V4L2 Subdev Driver Template");
MODULE_LICENSE("GPL v2");
关键点解析:
  • v4l2_i2c_subdev_init 将 I2C client 与 v4l2_subdev 绑定,并设置 sd->owner。
  • v4l2_device_register_subdev 将 Subdev 注册到父设备,建立层级关系。
  • of_match_table 是设备树匹配的核心,必须与 DTS 中的 compatible 一致。

4.3 probe 函数实现详解

probe 函数是驱动初始化的核心入口。当内核发现 I2C 总线上的设备与驱动匹配时,调用此函数。其典型流程如下:

  1. 分配私有数据:使用 devm_kzalloc 自动管理内存生命周期。
  2. 初始化 Subdev:调用 v4l2_i2c_subdev_init 建立 I2C 与 V4L2 的桥梁。
  3. 设置标识信息:填充 sd.name 便于调试和日志输出。
  4. 注册到 V4L2 设备:v4l2_device_register_subdev 将 Subdev 加入父设备链表。
  5. 硬件初始化(可选):读取芯片 ID、配置寄存器、申请中断等。
常见错误: 忘记调用 v4l2_device_register_subdev 会导致 Subdev 无法被媒体框架发现;probe 失败时务必释放已分配的资源,使用 devm_ 系列 API 可简化错误处理。

4.4 设备树匹配机制

设备树(Device Tree)是嵌入式 Linux 描述硬件拓扑的标准方式。Subdev 驱动通过 compatible 字符串与设备树节点匹配。

以下是一个典型的设备树节点示例:

// my-board.dts
&i2c2 {
    status = "okay";
    clock-frequency = <400000>;

    my_sensor: my-sensor@36 {
        compatible = "myvendor,my-sensor-v1";
        reg = <0x36>;
        clocks = <&clk IMX8MM_CLK_24M>;
        reset-gpios = <&gpio1 13 GPIO_ACTIVE_LOW>;
    };
};

匹配过程的内核流程如下 SVG 图所示:

设备树匹配与 probe 调用流程 设备树匹配与 probe 调用流程 DTS 节点 compatible = "myvendor,my-sensor-v1" OF 核心 of_match_table 匹配 I2C 核心 i2c_device_match probe 回调 my_subdev_probe v4l2_device_register_subdev 驱动加载完成 匹配条件 DTS compatible == of_match_table I2C 地址 reg 匹配

4.5 编译与加载验证

完成代码编写后,按以下步骤编译并加载驱动:

  1. 添加 Kconfig 选项(可选):在驱动目录的 Kconfig 中添加 config VIDEO_MY_SUBDEV。
  2. 修改 Makefile:添加 obj-$(CONFIG_VIDEO_MY_SUBDEV) += my_subdev.o。
  3. 编译内核/模块:make modules 或 make M=drivers/media/i2c。
  4. 加载模块:sudo insmod my_subdev.ko。
  5. 验证注册:查看 dmesg | tail 确认 probe 成功,检查 /sys/bus/i2c/devices/ 下是否出现新设备。
调试技巧: 如果 probe 未被调用,首先检查设备树节点是否被正确编译进内核(查看 /proc/device-tree),其次确认 compatible 字符串是否完全一致(包括大小写和通配符)。

4.6 本章小结

通过本实战,你完成了以下关键任务:

  • 编写了一个完整的 Subdev 驱动模板,包含模块入口/出口、I2C 驱动结构体、Subdev 初始化。
  • 实现了 probe 函数,掌握了设备内存分配、Subdev 初始化与注册的标准流程。
  • 配置了设备树匹配表,理解了 compatible 字符串在驱动绑定中的核心作用。
  • 通过 SVG 流程图清晰梳理了从 DTS 到 probe 调用的完整链路。