Linux 驱动框架中 Class 机制完整讲解(以 ov13855 摄像头为例)
一、从 ov13855 摄像头硬件开始
1. 硬件是什么
ov13855 是一颗 1300 万像素的 CMOS 图像传感器芯片。它有:
- I2C 接口:用来配置寄存器(分辨率、曝光、增益等)
- MIPI CSI-2 接口:输出图像数据
- 电源引脚
- 时钟引脚
2. 驱动要做什么
驱动需要完成这些工作:
-
硬件初始化
- 配置 I2C 总线,找到 ov13855 这颗芯片(地址 0x10)
- 写寄存器序列,配置输出格式、分辨率
- 控制电源、时钟
-
提供操作接口
- 上层要拍照时,驱动启动数据流
- 上层要调整曝光时,驱动写对应寄存器
- 上层要关闭摄像头时,驱动停止数据流
二、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. 问题分析
用户空间的应用程序(比如拍照软件)需要:
- 找到摄像头设备
- 打开设备
- 配置参数(分辨率、格式)
- 开始采集数据
- 读取图像数据
但现在只有 device 和 driver,它们都在内核空间,用户空间无法直接访问。
2. 解决方案:创建字符设备文件
内核需要做这些事:
-
分配设备号
cdev_t devno; alloc_chrdev_region(&devno, 0, 1, "ov13855"); // 得到 devno = (主设备号 249, 次设备号 0) -
注册字符设备
cstruct cdev *cdev = cdev_alloc(); cdev->ops = &ov13855_fops; // 文件操作函数 cdev_add(cdev, devno, 1); -
创建设备文件
- 内核需要在 /dev 目录下创建一个文件
- 这个文件的设备号是 (249, 0)
- 用户 open("/dev/ov13855") 时,内核根据设备号找到 cdev,调用 ov13855_fops 里的函数
3. 新的问题
每个驱动都要自己:
- 分配设备号
- 注册字符设备
- 创建 /dev 节点
这些代码重复且容易出错。而且,ov13855 驱动的 probe 函数现在要做很多和摄像头本身无关的事情。
四、V4L2 子系统的作用
1. V4L2 要解决什么问题
内核中有成百上千种摄像头:
- ov5640, ov8858, ov13855, imx219, imx477 ...
每种摄像头的驱动都要:
- 自己分配设备号
- 自己创建 /dev 节点
- 自己实现文件操作函数(open, close, ioctl)
这些代码高度重复。而且用户空间的应用程序要适配每种摄像头的接口,非常麻烦。
2. V4L2 提供什么
V4L2(Video for Linux 2)是内核的视频设备子系统,它做这些事:
-
统一的用户空间接口
- 所有摄像头都用同样的 ioctl 命令
- VIDIOC_QUERYCAP:查询能力
- VIDIOC_S_FMT:设置格式
- VIDIOC_STREAMON:开始采集
-
统一的设备注册
- 驱动不需要自己分配设备号
- 驱动不需要自己创建 /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 会:
- 自动分配设备号(比如 81:0)
- 自动创建 /dev/video0
- 自动处理用户空间的 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. 更好的方法
应用希望能够:
- 查询系统有多少个视频设备
- 获取每个设备的信息(名字、能力)
- 根据需求选择合适的设备
但是,应用程序在用户空间,无法直接访问内核的 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 要解决的问题
总结前面的问题:
-
内核角度
- 系统有很多视频设备(ov13855, ov8858, imx219...)
- 这些设备分散在不同的硬件路径下
- V4L2 子系统需要快速找到所有视频设备
-
用户空间角度
- 应用需要找到所有摄像头
- 不关心硬件在哪条总线上
- 只关心这是不是视频设备
-
驱动角度
- 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 创建后,内核会:
- 在全局 class 链表里注册这个 class
- 创建 /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 驱动只做两件事:
- 初始化硬件
- 调用 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:
- RK3588 的 I2C-4 控制器启动
- 扫描总线或解析设备树
- 发现地址 0x10 的设备
- 创建
struct i2c_client,addr = 0x10 - 加入 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 device 的 knode_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 device的knode_class成员 - 间接保存 :可以通过
knode_class找到完整的struct device * - 最终对应 :这个
struct device是struct 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 保存什么类型的指针?
答案:
- 直接类型 :链表节点类型(
struct klist_node) - 对应的结构体 :
struct device的knode_class成员 - 最终指向 :
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 个)
struct device- 通用设备struct device_driver- 通用驱动struct bus_type- 总线struct class- 设备类
I2C 层(2 个)
struct i2c_client- I2C 设备(包含 device)struct i2c_driver- I2C 驱动(包含 device_driver)
V4L2 层(2 个)
struct v4l2_subdev- V4L2 子设备struct video_device- 视频设备文件(包含 device)
驱动私有(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