Linux 驱动框架中 Class 机制完整讲解(以 ov13855 摄像头为例)

Linux 驱动框架中 Class 机制完整讲解(以 ov13855 摄像头为例)

一、从 ov13855 摄像头硬件开始

1. 硬件是什么

ov13855 是一颗 1300 万像素的 CMOS 图像传感器芯片。它有:

  • I2C 接口:用来配置寄存器(分辨率、曝光、增益等)
  • MIPI CSI-2 接口:输出图像数据
  • 电源引脚
  • 时钟引脚

2. 驱动要做什么

驱动需要完成这些工作:

  1. 硬件初始化

    • 配置 I2C 总线,找到 ov13855 这颗芯片(地址 0x10)
    • 写寄存器序列,配置输出格式、分辨率
    • 控制电源、时钟
  2. 提供操作接口

    • 上层要拍照时,驱动启动数据流
    • 上层要调整曝光时,驱动写对应寄存器
    • 上层要关闭摄像头时,驱动停止数据流

二、Linux 内核如何管理 ov13855 驱动

1. 内核已有的基础结构

Linux 内核用三个结构来管理硬件:

结构1:struct device

  • 代表一个实际存在的硬件
  • 记录这个硬件在哪条总线上(I2C 总线)
  • 记录这个硬件的地址(0x10)

对于 ov13855:

c 复制代码
struct device ov13855_dev = {
    .bus = &i2c_bus_type,      // 在 I2C 总线上
    .parent = i2c_adapter_dev, // 挂在某个 I2C 控制器下
    // ... 其他信息
};

结构2:struct device_driver

  • 包含驱动代码
  • 包含 probe 函数:硬件初始化代码
  • 包含匹配表:说明这个驱动能驱动哪些硬件

对于 ov13855:

c 复制代码
struct i2c_driver ov13855_driver = {
    .probe = ov13855_probe,      // 初始化函数
    .id_table = ov13855_id,      // 匹配表
    // ... 其他函数
};

结构3:bus(总线)

  • 维护 device 链表
  • 维护 driver 链表
  • 负责匹配:当有新 device 或新 driver 加入时,遍历链表找匹配项
  • 匹配成功后调用 driver->probe()

对于 ov13855:

  • I2C 总线看到地址 0x10 的设备
  • 遍历已注册的 i2c_driver
  • 发现 ov13855_driver 的匹配表里有 0x10
  • 调用 ov13855_probe()

2. probe 成功后的状态

probe 函数执行完成后:

c 复制代码
static int ov13855_probe(struct i2c_client *client)
{
    // 申请内存,保存驱动私有数据
    struct ov13855 *ov13855 = kzalloc(...);
    
    // 写寄存器,初始化硬件
    ov13855_write_reg(client, 0x0103, 0x01); // 软复位
    ov13855_write_reg(client, 0x0100, 0x00); // 待机模式
    // ... 写几百个寄存器
    
    // 到这里,硬件已经能工作了
    return 0;
}

此时内核状态:

  • device 和 driver 已经绑定
  • 硬件已经初始化完成
  • 但是,没有任何路径可以让用户空间使用这个摄像头

三、第一个问题:用户空间怎么操作摄像头

1. 问题分析

用户空间的应用程序(比如拍照软件)需要:

  1. 找到摄像头设备
  2. 打开设备
  3. 配置参数(分辨率、格式)
  4. 开始采集数据
  5. 读取图像数据

但现在只有 device 和 driver,它们都在内核空间,用户空间无法直接访问。

2. 解决方案:创建字符设备文件

内核需要做这些事:

  1. 分配设备号

    c 复制代码
    dev_t devno;
    alloc_chrdev_region(&devno, 0, 1, "ov13855");
    // 得到 devno = (主设备号 249, 次设备号 0)
  2. 注册字符设备

    c 复制代码
    struct cdev *cdev = cdev_alloc();
    cdev->ops = &ov13855_fops;  // 文件操作函数
    cdev_add(cdev, devno, 1);
  3. 创建设备文件

    • 内核需要在 /dev 目录下创建一个文件
    • 这个文件的设备号是 (249, 0)
    • 用户 open("/dev/ov13855") 时,内核根据设备号找到 cdev,调用 ov13855_fops 里的函数

3. 新的问题

每个驱动都要自己:

  1. 分配设备号
  2. 注册字符设备
  3. 创建 /dev 节点

这些代码重复且容易出错。而且,ov13855 驱动的 probe 函数现在要做很多和摄像头本身无关的事情。

四、V4L2 子系统的作用

1. V4L2 要解决什么问题

内核中有成百上千种摄像头:

  • ov5640, ov8858, ov13855, imx219, imx477 ...

每种摄像头的驱动都要:

  • 自己分配设备号
  • 自己创建 /dev 节点
  • 自己实现文件操作函数(open, close, ioctl)

这些代码高度重复。而且用户空间的应用程序要适配每种摄像头的接口,非常麻烦。

2. V4L2 提供什么

V4L2(Video for Linux 2)是内核的视频设备子系统,它做这些事:

  1. 统一的用户空间接口

    • 所有摄像头都用同样的 ioctl 命令
    • VIDIOC_QUERYCAP:查询能力
    • VIDIOC_S_FMT:设置格式
    • VIDIOC_STREAMON:开始采集
  2. 统一的设备注册

    • 驱动不需要自己分配设备号
    • 驱动不需要自己创建 /dev 节点
    • 驱动只需要填一个结构体,调用一个注册函数

3. ov13855 驱动怎么使用 V4L2

驱动的 probe 函数改成这样:

c 复制代码
static int ov13855_probe(struct i2c_client *client)
{
    struct ov13855 *ov13855 = kzalloc(...);
    
    // 初始化硬件(和之前一样)
    ov13855_write_reg(...);
    
    // 创建 V4L2 子设备结构
    struct v4l2_subdev *sd = &ov13855->subdev;
    v4l2_subdev_init(sd, &ov13855_subdev_ops);
    
    // 注册到 V4L2 子系统
    v4l2_async_register_subdev(sd);
    
    return 0;
}

注册成功后,V4L2 会:

  1. 自动分配设备号(比如 81:0)
  2. 自动创建 /dev/video0
  3. 自动处理用户空间的 open/ioctl 调用

五、第二个问题:系统里有多个摄像头怎么办

1. 具体场景

Orange Pi 5 开发板上同时接了:

  • ov13855 摄像头(前摄)
  • ov8858 摄像头(后摄)

它们都注册到 V4L2,会创建:

  • /dev/video0(ov13855)
  • /dev/video1(ov8858)

2. 应用程序的困惑

拍照应用启动时,需要找到所有摄像头:

c 复制代码
// 应用程序代码
int fd0 = open("/dev/video0", O_RDWR);
int fd1 = open("/dev/video1", O_RDWR);
int fd2 = open("/dev/video2", O_RDWR);  // 可能不存在

问题:

  • 应用不知道系统有几个摄像头
  • 要一个个尝试打开 /dev/video0, /dev/video1, ...
  • 而且不知道 video2、video3 是否存在

3. 更好的方法

应用希望能够:

  1. 查询系统有多少个视频设备
  2. 获取每个设备的信息(名字、能力)
  3. 根据需求选择合适的设备

但是,应用程序在用户空间,无法直接访问内核的 device 链表。

六、sysfs 的作用

1. sysfs 是什么

sysfs 是一个虚拟文件系统,挂载在 /sys 目录下。内核把设备信息导出到 /sys,用户空间可以读取。

2. V4L2 在 sysfs 里导出什么

当 ov13855 注册成功后,V4L2 会在 /sys 里创建:

bash 复制代码
/sys/devices/platform/...../ov13855/
    ├── name         # 设备名
    ├── dev          # 设备号 "81:0"
    └── ...

但是,这个路径太深了:

  • 路径依赖硬件树结构
  • 不同平台路径不同(RK3588 和 i.MX8 路径完全不同)
  • 应用程序无法写通用代码

3. 问题本质

应用程序需要的是:按照功能分类的设备列表,而不是按照硬件拓扑结构的设备树。

对于视频设备,应用希望:

bash 复制代码
/sys/video/
    ├── video0 -> 指向 ov13855
    └── video1 -> 指向 ov8858

对于声卡,应用希望:

bash 复制代码
/sys/sound/
    ├── card0 -> 指向某个声卡
    └── card1 -> 指向另一个声卡

七、Class 的引入

1. Class 要解决的问题

总结前面的问题:

  1. 内核角度

    • 系统有很多视频设备(ov13855, ov8858, imx219...)
    • 这些设备分散在不同的硬件路径下
    • V4L2 子系统需要快速找到所有视频设备
  2. 用户空间角度

    • 应用需要找到所有摄像头
    • 不关心硬件在哪条总线上
    • 只关心这是不是视频设备
  3. 驱动角度

    • ov13855 驱动不想知道 V4L2 子系统的存在
    • 只想说"我是一个视频设备"
    • 后续的事情自动完成

2. Class 的定义

内核需要一个机制:

  • 把功能相同的设备归类管理
  • 自动创建统一的 sysfs 目录
  • 提供查询接口给上层子系统

这个机制就叫做 class(类)。

class 是一个结构体,包含:

c 复制代码
struct class {
    const char *name;           // 类名,比如 "video4linux"
    struct list_head devices;   // 属于这个类的设备链表
    // ... 其他字段
};

3. V4L2 的 class

V4L2 子系统在初始化时创建一个 class:

c 复制代码
// V4L2 子系统初始化代码
static int __init videodev_init(void)
{
    // 创建 video4linux 类
    video_class = class_create(THIS_MODULE, "video4linux");
    
    return 0;
}

这个 class 创建后,内核会:

  1. 在全局 class 链表里注册这个 class
  2. 创建 /sys/class/video4linux/ 目录

现在 video_class 是一个空的类,devices 链表是空的。

八、设备如何加入 Class

1. ov13855 注册流程

回到 ov13855 的 probe 函数:

c 复制代码
static int ov13855_probe(struct i2c_client *client)
{
    // 1. 初始化硬件
    ov13855_write_reg(...);
    
    // 2. 注册到 V4L2
    v4l2_async_register_subdev(sd);
    
    return 0;
}

v4l2_async_register_subdev() 内部会做这些事:

c 复制代码
// V4L2 内部代码
int v4l2_async_register_subdev(struct v4l2_subdev *sd)
{
    // ... 其他处理
    
    // 关键:创建一个 video_device
    struct video_device *vdev = video_device_alloc();
    
    // 设置 vdev 的 class
    vdev->dev.class = video_class;  // ← 这里赋值
    
    // 调用 device_add
    device_add(&vdev->dev);
    
    return 0;
}

2. device_add 做什么

device_add() 函数看到 dev->class != NULL,会执行这些操作:

操作1:加入 class 设备链表

c 复制代码
// device_add 内部代码
if (dev->class) {
    // 把这个 device 加入 class->devices 链表
    list_add_tail(&dev->knode_class, &dev->class->devices);
}

执行后,video_class 的 devices 链表里有了 ov13855 对应的 video_device。

操作2:创建 sysfs 目录

c 复制代码
if (dev->class) {
    // 在 /sys/class/video4linux/ 下创建目录
    sysfs_create_link(&dev->class->dir, &dev->kobj, dev_name(dev));
}

执行后,文件系统里出现:

bash 复制代码
/sys/class/video4linux/video0 -> /sys/devices/.../ov13855

操作3:发送 uevent

c 复制代码
if (dev->class) {
    // 通知用户空间:有新设备加入
    kobject_uevent(&dev->kobj, KOBJ_ADD);
}

udev 守护进程收到 uevent 后,会自动创建 /dev/video0。

3. 完整流程

text 复制代码
ov13855 驱动加载
    ↓
i2c_add_driver(&ov13855_driver)
    ↓
I2C 总线发现地址 0x10 的设备
    ↓
调用 ov13855_probe()
    ↓
probe 内部调用 v4l2_async_register_subdev()
    ↓
V4L2 创建 video_device 结构
    ↓
设置 vdev->dev.class = video_class  ← 关键步骤
    ↓
调用 device_add(&vdev->dev)
    ↓
device_add 看到 dev->class 不为空
    ├─ 加入 video_class->devices 链表
    ├─ 创建 /sys/class/video4linux/video0
    └─ 发送 uevent
    ↓
udev 收到 uevent
    ↓
创建 /dev/video0

九、Class 的具体功能

1. 功能1:维护设备链表

video_class 内部有一个链表:

c 复制代码
struct class video_class = {
    .name = "video4linux",
    .devices = LIST_HEAD_INIT(video_class.devices),
};

每当有新的视频设备 device_add:

  • 如果 dev->class == video_class
  • device_add 自动执行 list_add_tail(&dev->knode_class, &video_class.devices)

结果:video_class.devices 里保存了所有视频设备的指针。

2. 功能2:提供查询接口

V4L2 可以遍历所有视频设备:

c 复制代码
// V4L2 内部代码
struct device *dev;
list_for_each_entry(dev, &video_class.devices, knode_class) {
    struct video_device *vdev = to_video_device(dev);
    // 对每个视频设备做处理
}

或者查找特定设备:

c 复制代码
// 查找名字为 "ov13855" 的设备
struct device *dev;
dev = class_find_device(video_class, NULL, "ov13855", device_match_name);

3. 功能3:创建统一的 sysfs 目录

所有视频设备自动出现在:

bash 复制代码
/sys/class/video4linux/
    ├── video0 -> ../../devices/platform/.../ov13855
    └── video1 -> ../../devices/platform/.../ov8858

应用程序只需要:

c 复制代码
DIR *dir = opendir("/sys/class/video4linux");
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
    printf("找到视频设备: %s\n", entry->d_name);
}

4. 功能4:自动清理

当 ov13855 驱动卸载时:

c 复制代码
static void ov13855_remove(struct i2c_client *client)
{
    v4l2_async_unregister_subdev(sd);
}

内部会调用 device_del(&vdev->dev)

c 复制代码
// device_del 内部代码
if (dev->class) {
    // 从 class 链表删除
    list_del(&dev->knode_class);
    
    // 删除 sysfs 链接
    sysfs_remove_link(&dev->class->dir, dev_name(dev));
    
    // 发送 uevent
    kobject_uevent(&dev->kobj, KOBJ_REMOVE);
}

结果:

  • video_class.devices 链表里移除了这个设备
  • /sys/class/video4linux/video0 消失
  • udev 删除 /dev/video0

十、关键点总结

1. dev->class 何时赋值

时机:device_add() 之前

对于 ov13855:

c 复制代码
v4l2_async_register_subdev(sd) {
    vdev->dev.class = video_class;  // 先赋值
    device_add(&vdev->dev);         // 再 add
}

规则:

  • 如果 device_add 时 dev->class 为 NULL,不会加入任何 class
  • 如果 device_add 时 dev->class 有值,自动加入对应 class

2. device_add 何时被调用

三种情况:

情况1:驱动 probe 成功后

  • I2C 总线匹配到 ov13855
  • 调用 ov13855_probe()
  • probe 内部调用 v4l2_async_register_subdev()
  • 内部执行 device_add()

情况2:子系统主动创建虚拟设备

c 复制代码
// 某些驱动会主动创建设备
vdev = video_device_alloc();
vdev->dev.class = video_class;
device_add(&vdev->dev);

情况3:热插拔

  • USB 摄像头插入
  • USB 总线创建 device
  • USB 摄像头驱动 probe
  • probe 里调用 device_add

3. Class 和 driver 的关系

driver 不需要知道 class 的存在

ov13855 驱动只做两件事:

  1. 初始化硬件
  2. 调用 v4l2_async_register_subdev()

所有 class 相关的事情都是 V4L2 子系统内部处理的。

V4L2 负责

  • 创建 video_class
  • 在注册设备时设置 dev->class
  • 调用 device_add

好处

  • ov13855 驱动不依赖 V4L2 内部实现
  • V4L2 可以随意修改 class 管理方式
  • 驱动代码简洁

十一、实际例子验证

1. 查看 class

bash 复制代码
ls /sys/class/

输出:

复制代码
video4linux/  # 视频设备类
sound/        # 声卡类
input/        # 输入设备类
net/          # 网络设备类
...

2. 查看 video4linux 类的设备

bash 复制代码
ls -l /sys/class/video4linux/

输出:

复制代码
video0 -> ../../devices/platform/fdd90000.i2c/i2c-4/4-0010/video4linux/video0

这个软链接指向 ov13855 的实际 device 路径。

3. 查看设备属性

bash 复制代码
cat /sys/class/video4linux/video0/name

输出:

复制代码
ov13855
bash 复制代码
cat /sys/class/video4linux/video0/dev

输出:

复制代码
81:0

这是设备号,主设备号 81,次设备号 0。

4. 应用程序使用

c 复制代码
// 用户程序
#include <dirent.h>
#include <stdio.h>

int main() {
    DIR *dir = opendir("/sys/class/video4linux");
    struct dirent *entry;
    
    while ((entry = readdir(dir)) != NULL) {
        if (entry->d_name[0] == '.') continue;
        
        char path[256];
        snprintf(path, sizeof(path), 
                 "/sys/class/video4linux/%s/name", entry->d_name);
        
        FILE *f = fopen(path, "r");
        char name[64];
        fgets(name, sizeof(name), f);
        fclose(f);
        
        printf("设备: /dev/%s, 名称: %s", entry->d_name, name);
    }
    
    closedir(dir);
    return 0;
}

输出:

复制代码
设备: /dev/video0, 名称: ov13855

十二、为什么需要 Class

1. 没有 class 会怎样

假设内核没有 class 机制:

V4L2 要遍历所有视频设备

c 复制代码
// 必须遍历整个 device 树
for_each_device(dev) {
    // 判断是不是视频设备
    if (dev->bus == &i2c_bus_type) {
        struct i2c_client *client = to_i2c_client(dev);
        // 怎么判断这是摄像头?读设备树?检查驱动名?
    }
}

问题:

  • 逻辑复杂
  • 性能差(要遍历所有设备)
  • 容易出错(判断规则不统一)

应用程序要找摄像头

c 复制代码
// 必须遍历整个 /sys/devices 树
find /sys/devices -name "*video*"

问题:

  • 路径不固定
  • 不同平台路径不同
  • 没有统一规则

驱动卸载时

  • V4L2 不知道哪些设备属于它
  • 需要维护自己的设备链表
  • 容易内存泄漏

2. 有了 class 之后

V4L2 遍历设备

c 复制代码
list_for_each_entry(dev, &video_class.devices, knode_class) {
    // 直接遍历,保证都是视频设备
}

应用程序找摄像头

bash 复制代码
ls /sys/class/video4linux/

驱动卸载

  • device_del 自动从 class 链表删除
  • class 自动维护链表

十三、完整流程图

text 复制代码
系统启动
    ↓
V4L2 子系统初始化
    └─ video_class = class_create("video4linux")
        └─ 创建 /sys/class/video4linux/

ov13855 驱动加载
    ↓
i2c_add_driver(&ov13855_driver)
    └─ 注册到 I2C 总线

I2C 总线扫描
    ↓
发现地址 0x10 的设备
    └─ 创建 i2c_client (device)
        └─ device.bus = &i2c_bus_type

I2C 总线匹配
    ↓
ov13855_driver 匹配成功
    ↓
调用 ov13855_probe(client)
    ├─ 初始化硬件(写寄存器)
    └─ v4l2_async_register_subdev(sd)
        └─ 创建 video_device
            ├─ vdev->dev.class = video_class  ← 关键
            └─ device_add(&vdev->dev)
                ├─ 加入 video_class.devices 链表
                ├─ 创建 /sys/class/video4linux/video0
                └─ 发送 uevent

udev 守护进程
    ↓
收到 KOBJ_ADD uevent
    └─ 读取设备号 81:0
        └─ 创建 /dev/video0 (字符设备文件)

应用程序
    ↓
ls /sys/class/video4linux/
    └─ 发现 video0
open("/dev/video0")
    └─ 内核根据设备号找到 vdev
        └─ 调用 vdev->fops->open()

这就是 Linux class 机制在 ov13855 摄像头驱动中的完整过程。

好的,我明白你的困惑了。让我用 ov13855 摄像头的完整流程,把所有结构体都梳理清楚。


Linux 驱动框架结构体完整梳理(ov13855 案例)

一、核心问题:video_class.devices 保存的是什么指针?

答案:保存的是 struct device \* 指针

但这个 device 实际上是 struct video_device 里面的一个成员。

具体来说:

c 复制代码
// video_class 的定义
struct class video_class = {
    .name = "video4linux",
    .devices = ...  // 这是一个链表头
};

// 链表里保存的是什么?
// 保存的是 struct device 的 knode_class 成员
// 通过 knode_class 可以找到完整的 struct device
// 而这个 struct device 又是 struct video_device 的成员

所以完整的关系是:

复制代码
video_class.devices (链表)
    ↓ 包含
struct device 的 knode_class (链表节点)
    ↓ 属于
struct device (完整结构体)
    ↓ 是谁的成员?
struct video_device 的 dev 成员

二、所有结构体分层讲解

第 1 层:通用设备模型结构体

这是 Linux 设备模型的基础,所有设备驱动都要用。

结构体 1:struct device

作用:代表一个硬件设备或虚拟设备

主要成员

c 复制代码
struct device {
    struct device *parent;         // 父设备指针
    struct bus_type *bus;          // 所属总线
    struct device_driver *driver;  // 绑定的驱动
    struct class *class;           // 所属类
    
    struct kobject kobj;           // 用于 sysfs 和引用计数
    struct list_head knode_class;  // 链表节点,链入 class->devices
    
    void *driver_data;             // 驱动私有数据指针
    dev_t devt;                    // 设备号(主设备号+次设备号)
};

何时创建

  • 方式1:总线扫描到硬件时自动创建(比如 I2C 总线扫描到地址 0x10 的设备)
  • 方式2:驱动代码主动创建(比如虚拟设备)
  • 方式3:子系统封装的 API 内部创建(比如 V4L2)

对于 ov13855

  • I2C 总线扫描到地址 0x10 的设备,创建一个 struct i2c_client
  • i2c_client 里包含一个 struct device 成员
结构体 2:struct device_driver

作用:代表驱动程序

主要成员

c 复制代码
struct device_driver {
    const char *name;              // 驱动名
    struct bus_type *bus;          // 所属总线
    
    int (*probe)(struct device *); // 设备初始化函数
    int (*remove)(struct device *);// 设备移除函数
    
    struct list_head devices;      // 该驱动绑定的所有设备
};

何时创建

  • 驱动加载时(insmod 或内核启动时)
  • 驱动代码里定义一个全局变量

对于 ov13855

  • ov13855 驱动定义了一个 struct i2c_driver
  • i2c_driver 里包含一个 struct device_driver 成员
结构体 3:struct bus_type

作用:代表一条总线(I2C、SPI、Platform、USB 等)

主要成员

c 复制代码
struct bus_type {
    const char *name;              // 总线名 "i2c"
    
    struct list_head devices;      // 该总线上的所有设备
    struct list_head drivers;      // 该总线上的所有驱动
    
    int (*match)(struct device *, struct device_driver *);  // 匹配函数
    int (*probe)(struct device *); // 可选的 probe
};

何时创建

  • 内核启动时,各个总线子系统创建
  • 比如 I2C 子系统创建 i2c_bus_type

对于 ov13855

  • 使用的是 i2c_bus_type,这是内核已经创建好的
结构体 4:struct class

作用:按功能分类管理设备

主要成员

c 复制代码
struct class {
    const char *name;              // 类名 "video4linux"
    
    struct list_head devices;      // 该类的所有设备(实际是 klist)
    
    struct kobject *dev_kobj;      // /sys/class/<name>/ 目录
};

何时创建

  • 子系统初始化时
  • 比如 V4L2 初始化时创建 video_class

对于 ov13855

  • 使用的是 video_class,V4L2 子系统创建的

第 2 层:I2C 总线层结构体

I2C 是一种具体的总线,它在通用设备模型的基础上添加了自己的特性。

结构体 5:struct i2c_client

作用:代表一个 I2C 设备

主要成员

c 复制代码
struct i2c_client {
    unsigned short addr;           // I2C 地址(比如 0x10)
    struct i2c_adapter *adapter;   // 所属的 I2C 控制器
    
    struct device dev;             // ← 嵌入的通用 device 结构体
    
    char name[I2C_NAME_SIZE];      // 设备名
};

包含关系

复制代码
struct i2c_client
    ├── addr = 0x10
    ├── adapter = I2C-4 控制器
    └── dev (struct device)
            ├── parent = I2C-4 控制器的 device
            ├── bus = &i2c_bus_type
            ├── driver = NULL(初始为空,匹配后指向驱动)
            └── class = NULL(初始为空)

何时创建

  • I2C 控制器扫描总线时
  • 或者设备树解析时
  • 创建后加入 i2c_bus_type.devices 链表

对于 ov13855

  1. RK3588 的 I2C-4 控制器启动
  2. 扫描总线或解析设备树
  3. 发现地址 0x10 的设备
  4. 创建 struct i2c_client,addr = 0x10
  5. 加入 I2C 总线的设备链表
结构体 6:struct i2c_driver

作用:代表一个 I2C 驱动

主要成员

c 复制代码
struct i2c_driver {
    int (*probe)(struct i2c_client *, const struct i2c_device_id *);
    int (*remove)(struct i2c_client *);
    
    const struct i2c_device_id *id_table;  // 匹配表
    
    struct device_driver driver;  // ← 嵌入的通用 driver 结构体
};

包含关系

复制代码
struct i2c_driver ov13855_driver
    ├── probe = ov13855_probe
    ├── id_table = { { "ov13855", 0 }, ... }
    └── driver (struct device_driver)
            ├── name = "ov13855"
            ├── bus = &i2c_bus_type
            └── devices (链表,保存绑定的设备)

何时创建

  • 驱动代码里静态定义
  • insmod 时注册到内核

对于 ov13855

c 复制代码
// 驱动源码里定义
static struct i2c_driver ov13855_driver = {
    .probe = ov13855_probe,
    .remove = ov13855_remove,
    .id_table = ov13855_id,
    .driver = {
        .name = "ov13855",
    },
};

// 驱动加载时注册
i2c_add_driver(&ov13855_driver);

第 3 层:V4L2 子系统结构体

V4L2 是视频设备子系统,它在 I2C 设备的基础上添加视频相关功能。

结构体 7:struct v4l2_subdev

作用:代表一个视频子设备(通常是摄像头传感器)

主要成员

c 复制代码
struct v4l2_subdev {
    struct list_head list;         // 链入 V4L2 的子设备链表
    
    const struct v4l2_subdev_ops *ops;  // 操作函数集
    
    struct device *dev;            // ← 指向对应的 i2c_client.dev
    
    char name[V4L2_SUBDEV_NAME_SIZE];
};

何时创建

  • ov13855_probe 函数里

对于 ov13855

c 复制代码
static int ov13855_probe(struct i2c_client *client)
{
    struct ov13855 *ov13855;  // 驱动私有数据
    
    // 1. 分配内存
    ov13855 = devm_kzalloc(&client->dev, sizeof(*ov13855), GFP_KERNEL);
    
    // 2. 初始化 v4l2_subdev
    struct v4l2_subdev *sd = &ov13855->subdev;
    v4l2_subdev_init(sd, &ov13855_subdev_ops);
    sd->dev = &client->dev;  // ← 指向 i2c_client 的 dev
    
    // 3. 注册到 V4L2
    v4l2_async_register_subdev(sd);
    
    return 0;
}
结构体 8:struct video_device

作用:代表一个视频设备文件(对应 /dev/videoX)

主要成员

c 复制代码
struct video_device {
    const struct v4l2_file_operations *fops;  // 文件操作
    
    struct device dev;             // ← 嵌入的通用 device 结构体
    
    struct cdev *cdev;             // 字符设备
    
    struct v4l2_device *v4l2_dev;  // 所属的 V4L2 设备
    
    int minor;                     // 次设备号
};

包含关系

复制代码
struct video_device
    ├── fops = &ov13855_fops
    ├── cdev = 字符设备结构
    ├── minor = 0(对应 video0)
    └── dev (struct device)
            ├── parent = &i2c_client.dev
            ├── bus = NULL(虚拟设备,不在总线上)
            ├── class = &video_class  ← 关键!
            ├── devt = (81, 0)  // 设备号
            └── knode_class(链表节点,链入 video_class.devices)

何时创建

  • V4L2 注册子设备时,内部自动创建

对于 ov13855

c 复制代码
// v4l2_async_register_subdev 内部
int v4l2_async_register_subdev(struct v4l2_subdev *sd)
{
    // 创建 video_device
    struct video_device *vdev;
    vdev = video_device_alloc();
    
    // 设置 class
    vdev->dev.class = &video_class;  // ← 这里设置
    
    // 设置设备号
    vdev->dev.devt = MKDEV(81, 0);   // 主设备号 81,次设备号 0
    
    // 设置父设备
    vdev->dev.parent = sd->dev;      // 指向 i2c_client.dev
    
    // 调用 device_add
    device_add(&vdev->dev);
    
    return 0;
}

三、完整的结构体关系图

从底层到顶层:

复制代码
硬件层:ov13855 芯片(地址 0x10)
    ↓
I2C 总线层:
    struct i2c_client (I2C 设备结构体)
        ├── addr = 0x10
        └── dev (struct device)
                ├── bus = &i2c_bus_type
                ├── driver = &ov13855_driver.driver
                └── class = NULL
    
    struct i2c_driver (I2C 驱动结构体)
        ├── probe = ov13855_probe
        └── driver (struct device_driver)
                ├── name = "ov13855"
                └── bus = &i2c_bus_type
    ↓
V4L2 子系统层:
    struct v4l2_subdev (V4L2 子设备结构体)
        ├── ops = &ov13855_subdev_ops
        └── dev = &i2c_client.dev (指针)
    
    struct video_device (视频设备结构体)
        ├── fops = &ov13855_fops
        ├── minor = 0
        └── dev (struct device)
                ├── parent = &i2c_client.dev
                ├── bus = NULL
                ├── class = &video_class  ← 关键
                └── devt = (81, 0)
    ↓
Class 层:
    struct class video_class
        ├── name = "video4linux"
        └── devices (链表)
                └── 包含 video_device.dev.knode_class

四、链表关系详解

链表 1:i2c_bus_type.devices

保存什么:所有 I2C 设备

链表节点在哪struct device 有一个成员用于链入总线

对于 ov13855

c 复制代码
i2c_bus_type.devices (链表头)
    ↓ 包含
i2c_client.dev (的链表节点)

链表 2:i2c_bus_type.drivers

保存什么:所有 I2C 驱动

链表节点在哪struct device_driver 有一个成员用于链入总线

对于 ov13855

c 复制代码
i2c_bus_type.drivers (链表头)
    ↓ 包含
ov13855_driver.driver (的链表节点)

链表 3:video_class.devices

保存什么 :所有视频设备的 struct device

链表节点在哪struct deviceknode_class 成员

对于 ov13855

c 复制代码
video_class.devices (链表头,类型是 struct klist)
    ↓ 包含
video_device.dev.knode_class (链表节点)
    ↓ 通过 container_of 找到
video_device.dev (完整的 struct device)
    ↓ 通过 container_of 找到
video_device (完整的 struct video_device)

回答第一个问题

video_class.devices 链表保存的是:

  • 直接保存struct deviceknode_class 成员
  • 间接保存 :可以通过 knode_class 找到完整的 struct device *
  • 最终对应 :这个 struct devicestruct video_device 的成员

遍历方法

c 复制代码
// V4L2 内部代码
struct device *dev;
struct video_device *vdev;

list_for_each_entry(dev, &video_class.devices, knode_class) {
    // dev 是 struct device *
    
    // 通过 container_of 找到外层的 video_device
    vdev = container_of(dev, struct video_device, dev);
    
    // 现在可以访问 vdev 的所有成员
    printk("视频设备: %s\n", video_device_node_name(vdev));
}

五、时序图:从硬件到用户空间

复制代码
时刻 0:系统启动
    ├─ I2C 子系统初始化,创建 i2c_bus_type
    ├─ V4L2 子系统初始化,创建 video_class
    └─ /sys/class/video4linux/ 目录出现

时刻 1:I2C 控制器初始化
    ├─ RK3588 的 I2C-4 控制器启动
    ├─ 扫描总线,发现地址 0x10 的设备
    ├─ 创建 struct i2c_client
    │       ├── addr = 0x10
    │       └── dev.bus = &i2c_bus_type
    └─ 加入 i2c_bus_type.devices 链表

时刻 2:ov13855 驱动加载
    ├─ insmod ov13855.ko
    ├─ 调用 i2c_add_driver(&ov13855_driver)
    ├─ ov13855_driver 加入 i2c_bus_type.drivers 链表
    └─ I2C 总线开始匹配

时刻 3:匹配成功
    ├─ I2C 总线调用 match 函数
    ├─ 发现 ov13855_driver.id_table 里有 "ov13855"
    ├─ 发现 i2c_client 的设备树兼容性匹配
    ├─ 设置 i2c_client.dev.driver = &ov13855_driver.driver
    └─ 调用 ov13855_probe(i2c_client)

时刻 4:probe 函数执行
    ├─ 分配 struct ov13855(驱动私有数据)
    ├─ 初始化硬件(写寄存器)
    ├─ 创建 struct v4l2_subdev
    │       └── sd.dev = &i2c_client.dev
    └─ 调用 v4l2_async_register_subdev(sd)

时刻 5:V4L2 注册
    ├─ V4L2 内部创建 struct video_device
    │       ├── vdev.dev.class = &video_class  ← 关键
    │       ├── vdev.dev.parent = &i2c_client.dev
    │       ├── vdev.dev.devt = (81, 0)
    │       └── vdev.minor = 0
    ├─ 调用 device_add(&vdev.dev)
    └─ device_add 看到 dev.class != NULL

时刻 6:device_add 执行
    ├─ 将 vdev.dev.knode_class 加入 video_class.devices 链表
    ├─ 创建 /sys/class/video4linux/video0 软链接
    └─ 发送 KOBJ_ADD uevent

时刻 7:udev 响应
    ├─ udev 收到 uevent
    ├─ 读取 /sys/class/video4linux/video0/dev → "81:0"
    ├─ 创建 /dev/video0(主设备号 81,次设备号 0)
    └─ 设置权限

时刻 8:用户空间可用
    ├─ ls /dev/video0 → 存在
    ├─ ls /sys/class/video4linux/ → 看到 video0
    └─ 应用程序可以 open("/dev/video0")

六、总结归纳

问题1:video_class.devices 保存什么类型的指针?

答案

  1. 直接类型 :链表节点类型(struct klist_node
  2. 对应的结构体struct deviceknode_class 成员
  3. 最终指向struct video_device 里的 dev 成员

为什么这样设计

  • Linux 内核的链表是侵入式链表
  • 不是保存指针,而是把链表节点嵌入到结构体里
  • 通过 container_of 宏从链表节点找到外层结构体

具体代码

c 复制代码
// 定义
struct class {
    struct klist devices;  // 不是 struct device *,而是 klist
};

struct device {
    struct klist_node knode_class;  // 链表节点
    struct class *class;            // 指向所属的 class
};

// 加入链表
device_add(struct device *dev) {
    if (dev->class) {
        // 将 dev->knode_class 加入 dev->class->devices
        klist_add_tail(&dev->knode_class, &dev->class->devices);
    }
}

// 遍历链表
struct klist_node *n;
klist_for_each(n, &video_class.devices) {
    // 从链表节点找到 struct device
    struct device *dev = container_of(n, struct device, knode_class);
    
    // 从 struct device 找到 struct video_device
    struct video_device *vdev = container_of(dev, struct video_device, dev);
}

问题2:一共有几种结构体?

总结一下 ov13855 涉及的所有结构体:

通用层(4 个)
  1. struct device - 通用设备
  2. struct device_driver - 通用驱动
  3. struct bus_type - 总线
  4. struct class - 设备类
I2C 层(2 个)
  1. struct i2c_client - I2C 设备(包含 device)
  2. struct i2c_driver - I2C 驱动(包含 device_driver)
V4L2 层(2 个)
  1. struct v4l2_subdev - V4L2 子设备
  2. struct video_device - 视频设备文件(包含 device)
驱动私有(1 个)
  1. struct ov13855 - 驱动自定义的私有数据结构

包含关系

复制代码
struct ov13855 (驱动私有)
    └── subdev (struct v4l2_subdev)

struct video_device (V4L2)
    └── dev (struct device) ← 这个 dev.class = &video_class

struct i2c_client (I2C)
    └── dev (struct device)

struct i2c_driver (I2C)
    └── driver (struct device_driver)

指针关系

复制代码
v4l2_subdev.dev → 指向 i2c_client.dev
video_device.dev.parent → 指向 i2c_client.dev
i2c_client.dev.driver → 指向 ov13855_driver.driver
相关推荐
yiSty2 小时前
linux命令行下使用百度云网盘【自用】
linux·运维·百度云
超绝振刀怪2 小时前
【Linux工具】环境基石:软件包管理器 yum 与 Vim 编辑器详解
linux·编辑器·vim
福尔摩斯张3 小时前
插件式架构:解耦与扩展的艺术与实践(超详细)
linux·服务器·网络·网络协议·tcp/ip
txzz88883 小时前
CentOS-Stream-10 搭建YUM源Web服务器
linux·运维·centos·yum源·linux系统更新·centos系统更新·自建web yum源
Molesidy3 小时前
【Linux】基于Imx6ull Pro开发板和platform_device+platform_driver框架的LED驱动设计以及上机测试
linux·驱动开发
我科绝伦(Huanhuan Zhou)3 小时前
Linux系统硬件时钟与系统时钟深度解析及同步实操指南
linux·运维·服务器
k***92164 小时前
【Linux】进程概念(六):地址空间核心机制
linux·运维·算法
李白同学4 小时前
Linux:调试器-gdb/cgdb使用
linux·服务器·c语言·c++
保持低旋律节奏4 小时前
linux——进程调度(时间片+优先级轮转调度算法O(1))
linux·运维·算法