RK3588:MIPI底层驱动学习——入门第三篇(IIC与V4L2如何共存?)
本章主要解析具体的OV13855驱动代码
代码位置路径:drivers/media/i2c/ov13855.c
逻辑承接:有了框架,具体驱动如何实现?
前一章我们学习了V4L2框架的原理,知道了内核通过v4l2_device和v4l2_subdev这套架构来管理摄像头设备。那么问题来了:具体的OV13855摄像头驱动程序是如何在这个框架下实际工作的?它又是如何将设备树中描述的硬件信息转化为可操作的设备对象的?
这就引出了我们本章要深入探讨的核心问题:驱动程序作为硬件和框架之间的桥梁,究竟是如何设计和实现的?
1. 驱动程序的双重身份
在深入代码之前,我们需要理解一个重要概念:OV13855驱动程序具有双重身份。
为什么会有双重身份? 这源于硬件的物理特性:
- OV13855摄像头通过I2C总线进行控制通信(配置寄存器、读取状态)
- 同时它又是一个视频子设备(产生图像数据流)
这种物理特性决定了驱动程序必须同时扮演两个角色:
第一重身份:I2C设备驱动
- 负责通过I2C总线与OV13855芯片通信
- 处理设备的发现、初始化、电源管理等基础操作
- 这部分功能由Linux的I2C子系统提供支持
第二重身份:V4L2子设备
- 负责向V4L2框架注册为摄像头子设备
- 处理图像格式、控制参数、数据流等摄像头相关操作
- 这部分功能由V4L2子系统提供支持
2. 驱动文件的组织结构
OV13855的完整驱动实现位于:
drivers/media/i2c/ov13855.c
为什么放在这个路径下?
drivers/media/
表明这是媒体设备驱动i2c/
表明这是通过I2C总线控制的设备- 这种路径组织体现了驱动的双重身份特征
3. 驱动的三大核心部分
基于双重身份的特点,OV13855驱动被组织成三个核心部分,每个部分承担不同的职责:
第一部分:probe初始化
这部分解决什么问题? 当系统启动时,如何将设备树中描述的硬件信息转化为可操作的设备对象?
第二部分:寄存器配置
这部分解决什么问题? 如何通过I2C总线向OV13855芯片发送具体的配置指令?
第三部分:V4L2操作接口
这部分解决什么问题? 如何响应上层应用的各种控制请求?
4. 那么这种双重身份在代码中是如何体现的呢?
上面我们从理论层面理解了双重身份,但具体到代码实现时,Linux内核是如何让一个驱动程序同时具备两种不同的能力的?这就需要深入理解Linux驱动框架的设计哲学。
Linux驱动框架的核心思想:每种硬件接口都有对应的子系统框架,驱动程序通过向不同的子系统注册来获得相应的能力。
对于OV13855这样的摄像头设备:
- 要获得I2C通信能力,就必须向I2C子系统注册为I2C设备驱动
- 要获得视频处理能力,就必须向V4L2子系统注册为V4L2子设备
那么一个驱动程序如何同时向两个不同的子系统注册呢? 这就需要在代码中定义两套不同的接口结构。
首先我们来看I2C设备驱动的注册接口:
// drivers/media/i2c/ov13855.c
static struct i2c_driver ov13855_i2c_driver = {.driver = {.name = OV13855_NAME, // 驱动名称:"ov13855".pm = &ov13855_pm_ops, // 电源管理操作.of_match_table = of_match_ptr(ov13855_of_match), // 设备树匹配表},.probe = &ov13855_probe, // 设备发现时的初始化函数.remove = &ov13855_remove, // 设备移除时的清理函数.id_table = ov13855_match_id, // I2C设备ID匹配表
};
这个结构体告诉I2C子系统:“我是一个名为ov13855的I2C设备驱动,当你在I2C总线上发现compatible属性为’ovti,ov13855’的设备时,请调用我的probe函数来处理它。”
但是仅有I2C驱动注册还不够,因为这只能让驱动获得与硬件通信的能力,还无法让上层应用程序通过V4L2接口来使用摄像头。所以驱动还必须向V4L2子系统注册。
这就引出了一个关键问题:V4L2子系统的注册不是通过独立的驱动结构体来完成的,而是在I2C驱动的probe函数中动态创建V4L2子设备对象。换句话说,I2C驱动是主体,V4L2子设备是在I2C驱动初始化过程中创建的附属对象。
5. 驱动程序需要管理哪些信息?
理解了双重身份的实现机制后,我们面临下一个问题:为了同时管理I2C通信和V4L2功能,驱动程序需要维护哪些信息?
这些信息可以分为几个层面:
硬件访问层面的信息:
- I2C客户端对象:用于与OV13855芯片进行寄存器读写通信
- 硬件资源描述:时钟、电源、GPIO等物理资源的控制句柄
- 硬件状态信息:当前电源状态、工作模式等
V4L2框架层面的信息:
- V4L2子设备对象:向V4L2框架注册的子设备实例
- 媒体实体信息:在媒体设备拓扑中的连接关系
- 控制接口:曝光、增益等参数的控制句柄
并发控制层面的信息:
- 互斥锁:保护共享资源不被并发访问破坏
- 运行状态标志:数据流是否启动、设备是否已初始化等
配置参数层面的信息:
- 当前工作模式:分辨率、帧率、像素格式等
- 设备树配置:模块索引、朝向、名称等静态配置信息
现在的问题是:这些不同层面、不同类型的信息如何在代码中组织和管理?
6. 驱动私有数据结构的设计思路
面对如此复杂的信息管理需求,Linux驱动开发采用了一个重要的设计模式:将所有驱动相关的信息封装在一个私有数据结构中。
为什么需要私有数据结构? 因为Linux内核中的各种回调函数(probe、remove、控制接口等)在被调用时,内核只会传递标准的参数(如i2c_client指针、v4l2_subdev指针等),但驱动程序往往需要访问更多的上下文信息。私有数据结构就是用来保存这些额外信息的容器。
私有数据结构的本质:它是驱动程序的"大脑",存储着驱动运行所需的所有状态信息和资源句柄。
让我们来看OV13855驱动的私有数据结构定义:
// drivers/media/i2c/ov13855.c
struct ov13855 {// === I2C设备身份相关 ===struct i2c_client *client; // I2C客户端对象,用于硬件通信// === V4L2子设备身份相关 ===struct v4l2_subdev subdev; // V4L2子设备对象struct media_pad pad; // 媒体实体的连接垫片struct v4l2_ctrl_handler ctrl_handler; // V4L2控制参数处理器struct v4l2_ctrl *exposure; // 曝光控制struct v4l2_ctrl *anal_gain; // 模拟增益控制struct v4l2_ctrl *digi_gain; // 数字增益控制struct v4l2_ctrl *hblank; // 水平消隐控制struct v4l2_ctrl *vblank; // 垂直消隐控制struct v4l2_ctrl *pixel_rate; // 像素速率控制struct v4l2_ctrl *link_freq; // 链路频率控制struct v4l2_ctrl *test_pattern; // 测试图案控制// === 硬件资源管理 ===struct clk *xvclk; // 外部时钟(24MHz)struct gpio_desc *power_gpio; // 电源控制GPIOstruct gpio_desc *reset_gpio; // 复位控制GPIOstruct gpio_desc *pwdn_gpio; // 掉电控制GPIOstruct regulator_bulk_data supplies[OV13855_NUM_SUPPLIES]; // 电源调节器阵列// === 引脚控制 ===struct pinctrl *pinctrl; // 引脚控制器struct pinctrl_state *pins_default; // 默认引脚状态struct pinctrl_state *pins_sleep; // 休眠引脚状态// === 运行状态管理 ===struct mutex mutex; // 并发保护锁bool streaming; // 数据流状态标志bool power_on; // 电源状态标志// === 配置参数管理 ===const struct ov13855_mode *cur_mode; // 当前工作模式u32 module_index; // 模块索引号const char *module_facing; // 模块朝向(前置/后置)const char *module_name; // 模块名称const char *len_name; // 镜头名称
};
这个结构体的设计体现了什么思想?
分层管理思想:不同类型的信息被清晰地分组,每一组对应驱动的一个功能层面。注释中的"==="分隔符不仅仅是美观,它体现了代码的组织逻辑。
资源集中管理思想:所有驱动需要使用的硬件资源(时钟、GPIO、电源等)都集中存储在这个结构体中,避免了分散管理导致的混乱。
状态封装思想:驱动的运行状态(是否上电、是否在传输数据、当前工作模式等)被封装在结构体中,便于统一管理和状态同步。
7. 私有数据结构与框架对象的关联机制
有了私有数据结构,我们面临一个实际的技术问题:在各种回调函数中,如何获取到这个包含完整驱动信息的结构体?
问题的具体描述:Linux内核在调用驱动的各种回调函数时,传递的参数是标准化的框架对象指针。比如:
- 调用V4L2相关回调时,传递的是
struct v4l2_subdev *
指针 - 调用I2C相关回调时,传递的是
struct i2c_client *
指针
但是我们真正需要的是访问包含所有驱动状态的struct ov13855 *
指针。
具体场景举例:假设上层应用要调整摄像头的曝光参数,V4L2框架会调用驱动的控制回调函数。这个回调函数的函数签名是固定的:
static int some_control_callback(struct v4l2_ctrl *ctrl)
在这个函数内部,我们需要:
- 获取当前的设备状态(比如是否已上电)
- 通过I2C总线向OV13855芯片写入寄存器
- 更新内部状态信息
但是,这个回调函数只能拿到v4l2_ctrl
指针,如何从这个指针获取到包含I2C客户端对象和设备状态的完整ov13855
结构体呢?
Linux内核的解决方案:容器嵌入机制(container embedding)。
核心思路:既然我们在struct ov13855
中嵌入了struct v4l2_subdev subdev
成员,那么就可以通过指针算术从subdev的地址反推出整个ov13855结构体的地址。
Linux内核为此提供了一个通用的宏:
// drivers/media/i2c/ov13855.c
#define to_ov13855(sd) container_of(sd, struct ov13855, subdev)
这个宏的工作原理:
sd
是指向v4l2_subdev
成员的指针struct ov13855
是包含该成员的结构体类型subdev
是该成员在结构体中的名称container_of
宏会计算出包含这个成员的结构体实例的起始地址
为什么这种计算是可行的? 因为C语言中结构体成员的布局是确定的。编译器保证subdev
成员在ov13855
结构体中的偏移量是固定的。
现在我们来看一个真实的应用场景。在V4L2控制参数的回调函数中,我们需要根据传入的控制对象来调整摄像头的实际硬件参数:
// drivers/media/i2c/ov13855.c - 真实的源码示例
static int ov13855_set_ctrl(struct v4l2_ctrl *ctrl)
{// 已知条件:// 1. ctrl是V4L2框架传递的控制对象指针// 2. ctrl->handler指向我们在ov13855结构体中的ctrl_handler成员// 3. 我们需要获取完整的ov13855结构体来进行硬件操作// 第一步:从ctrl->handler获取到包含它的ov13855结构体struct ov13855 *ov13855 = container_of(ctrl->handler,struct ov13855, ctrl_handler);// 第二步:获取I2C客户端对象用于硬件通信struct i2c_client *client = ov13855->client;int ret = 0;// 第三步:检查设备状态,确保可以进行硬件操作if (!ov13855->power_on) {// 设备未上电,无法进行寄存器操作return 0;}// 第四步:根据不同的控制参数类型,执行相应的硬件操作switch (ctrl->id) {case V4L2_CID_EXPOSURE:// 设置曝光参数需要向特定寄存器写入数值ret = ov13855_write_reg(client, OV13855_REG_EXPOSURE,OV13855_REG_VALUE_24BIT, ctrl->val);break;case V4L2_CID_ANALOGUE_GAIN:// 设置模拟增益参数ret = ov13855_write_reg(client, OV13855_REG_GAIN_H,OV13855_REG_VALUE_08BIT,(ctrl->val >> OV13855_GAIN_H_SHIFT) & OV13855_GAIN_H_MASK);ret |= ov13855_write_reg(client, OV13855_REG_GAIN_L,OV13855_REG_VALUE_08BIT,ctrl->val & OV13855_GAIN_L_MASK);break;default:dev_warn(&client->dev, "%s Unhandled id:0x%x, val:0x%x\n",__func__, ctrl->id, ctrl->val);break;}return ret;
}
这个例子展示了什么?
明确的数据流向:
- 用户应用通过V4L2接口调整曝光参数
- V4L2框架调用
ov13855_set_ctrl
回调函数,传入v4l2_ctrl
指针 - 驱动通过
container_of
获取完整的ov13855
结构体 - 驱动使用结构体中的
client
成员进行I2C通信 - 通过I2C向OV13855芯片的寄存器写入实际数值
关键的设计思想:通过容器嵌入机制,驱动可以在任何回调函数中访问到完整的上下文信息,从而实现从高层抽象接口到底层硬件操作的无缝转换。
类似地,在其他类型的回调函数中也采用相同的机制。比如在处理数据流控制时:
// drivers/media/i2c/ov13855.c - 数据流控制的真实场景
static int ov13855_s_stream(struct v4l2_subdev *sd, int on)
{// 已知条件:// 1. sd是V4L2框架传递的子设备指针// 2. on表示启动(1)或停止(0)数据流// 3. 我们需要通过I2C配置OV13855的流控制寄存器// 获取完整的驱动上下文struct ov13855 *ov13855 = to_ov13855(sd);struct i2c_client *client = ov13855->client;int ret = 0;// 使用互斥锁保护并发访问mutex_lock(&ov13855->mutex);// 检查当前数据流状态,避免重复操作if (ov13855->streaming == on) {mutex_unlock(&ov13855->mutex);return 0;}if (on) {// 启动数据流:向OV13855写入流控制寄存器ret = ov13855_write_reg(client, OV13855_REG_CTRL_MODE,OV13855_REG_VALUE_08BIT,OV13855_MODE_STREAMING);} else {// 停止数据流:向OV13855写入待机模式寄存器ret = ov13855_write_reg(client, OV13855_REG_CTRL_MODE,OV13855_REG_VALUE_08BIT,OV13855_MODE_SW_STANDBY);}if (!ret)ov13855->streaming = on; // 更新内部状态mutex_unlock(&ov13855->mutex);return ret;
}
通过这两个真实的代码示例可以看出:无论是参数控制还是数据流管理,驱动都遵循相同的模式:通过容器嵌入机制获取完整上下文,然后利用其中的硬件资源进行实际操作。这种设计保证了驱动在各种不同的回调场景下都能够访问到所需的全部信息。
8. i2c_driver与v4l2_subdev的协同工作机制
理解了私有数据结构和指针转换机制后,我们来看看I2C驱动和V4L2子设备是如何协同工作的。
工作流程的本质:I2C驱动负责设备的生命周期管理(发现、初始化、清理),V4L2子设备负责功能接口的提供(格式设置、参数控制、数据流管理)。
第一阶段:设备发现和I2C驱动加载
当内核扫描设备树时,会发现compatible = "ovti,ov13855"的设备节点,然后加载对应的I2C驱动:
// drivers/media/i2c/ov13855.c
static const struct of_device_id ov13855_of_match[] = {{ .compatible = "ovti,ov13855" }, // 与设备树中的compatible属性匹配{},
};
MODULE_DEVICE_TABLE(of, ov13855_of_match);
第二阶段:probe函数创建完整的驱动实例
当I2C子系统发现匹配的设备时,会调用ov13855_probe函数。这个函数是整个驱动的核心,它负责:
- 分配私有数据结构:为ov13855结构体分配内存空间
- 解析设备树配置:获取GPIO、时钟、电源等硬件资源信息
- 初始化硬件资源:配置时钟、电源、GPIO等
- 创建V4L2子设备:将摄像头注册到V4L2框架中
- 建立关联关系:让I2C客户端和V4L2子设备能够相互引用
第三阶段:V4L2操作接口的实现
一旦V4L2子设备创建成功,上层应用就可以通过标准的V4L2接口来控制摄像头:
// drivers/media/i2c/ov13855.c
static const struct v4l2_subdev_ops ov13855_subdev_ops = {.core = &ov13855_core_ops, // 核心操作(上电、复位等).video = &ov13855_video_ops, // 视频操作(数据流控制).pad = &ov13855_pad_ops, // 媒体垫片操作(格式协商)
};
每个操作集合都包含多个具体的回调函数,例如:
static const struct v4l2_subdev_video_ops ov13855_video_ops = {.s_stream = ov13855_s_stream, // 启动/停止数据流.g_frame_interval = ov13855_g_frame_interval, // 获取帧间隔.s_frame_interval = ov13855_s_frame_interval, // 设置帧间隔
};
关键理解:这些V4L2回调函数在执行时,都会通过前面讲的指针转换机制获取到完整的ov13855结构体,然后利用其中的i2c_client成员来进行实际的硬件操作。
这样就形成了一个完整的闭环:I2C驱动负责硬件管理,V4L2接口负责功能暴露,私有数据结构负责信息传递和状态维护。
通过这种精巧的设计,OV13855驱动成功地将复杂的双重身份管理隐藏在框架之下,为上层应用提供了简洁统一的摄像头控制接口。
9. 驱动在Linux内核中的精确层次位置
理解了i2c_driver与v4l2_subdev的协同工作后,我们需要进一步明确OV13855驱动在整个Linux内核架构中的确切位置。这个层次理解将帮助我们更好地把握驱动的职责边界。
OV13855驱动在Linux内核中处于三个层次的交汇点:
应用层 (用户程序)↓ (V4L2 API调用)
V4L2核心层 (内核框架) ↓ (v4l2_subdev_ops调用)
OV13855驱动 (承上启下的关键层)↓ (I2C协议 + GPIO/Clock/Regulator API)
硬件资源管理层↓ (物理信号)
OV13855芯片 (实际硬件)
这种"承上启下"的位置特性,直接决定了驱动必须同时满足上层V4L2框架的标准接口要求,又要正确控制底层硬件资源。
10. 完整的结构体组织关系图解
基于前面学习的指针转换机制,我们现在可以完整地理解OV13855驱动中各个结构体之间的组织关系:
// drivers/media/i2c/ov13855.c - 静态全局变量(编译时确定)
static struct i2c_driver ov13855_i2c_driver; // I2C驱动入口
static const struct v4l2_subdev_ops ov13855_subdev_ops; // V4L2操作集合
static const struct v4l2_subdev_core_ops ov13855_core_ops; // 核心操作
static const struct v4l2_subdev_video_ops ov13855_video_ops; // 视频操作
static const struct v4l2_subdev_pad_ops ov13855_pad_ops; // 媒体垫片操作
static const struct v4l2_ctrl_ops ov13855_ctrl_ops; // 控制操作
static const struct ov13855_mode supported_modes[]; // 支持的模式数组// 运行时动态分配的结构(probe时创建)
struct ov13855 *ov13855; // 驱动核心结构体(动态分配)
struct v4l2_subdev subdev; // 嵌入在ov13855中
struct v4l2_ctrl_handler ctrl_handler; // 嵌入在ov13855中
struct media_pad pad; // 嵌入在ov13855中
struct i2c_client *client; // I2C框架创建,ov13855保存指针
关键的关联关系建立:
// probe函数中建立的关联关系
ov13855->client = client; // ov13855 → client
i2c_set_clientdata(client, ov13855); // client → ov13855
ov13855->subdev.ops = &ov13855_subdev_ops; // subdev → 操作集合
ov13855->subdev.ctrl_handler = &ov13855->ctrl_handler; // subdev → 控制处理器
ov13855->cur_mode = &supported_modes[0]; // ov13855 → 当前模式
11. 操作集合的三层分级设计
V4L2框架要求驱动提供标准化的操作接口,OV13855驱动采用了三层分级的操作集合设计来满足这个要求:
顶层:统一操作集合入口
// drivers/media/i2c/ov13855.c
static const struct v4l2_subdev_ops ov13855_subdev_ops = {.core = &ov13855_core_ops, // 核心操作(电源、复位).video = &ov13855_video_ops, // 视频操作(数据流控制).pad = &ov13855_pad_ops, // 媒体垫片操作(格式协商)
};
中层:功能分类的操作集合
// 核心操作 - 设备基本控制
static const struct v4l2_subdev_core_ops ov13855_core_ops = {.s_power = ov13855_s_power, // 电源管理.ioctl = ov13855_ioctl, // 自定义命令
#ifdef CONFIG_COMPAT .compat_ioctl32 = ov13855_compat_ioctl32, // 32位兼容
#endif
};// 视频操作 - 数据流相关控制
static const struct v4l2_subdev_video_ops ov13855_video_ops = {.s_stream = ov13855_s_stream, // 启动/停止数据流.g_frame_interval = ov13855_g_frame_interval, // 获取帧间隔
};// 媒体垫片操作 - 格式协商和连接
static const struct v4l2_subdev_pad_ops ov13855_pad_ops = {.enum_mbus_code = ov13855_enum_mbus_code, // 枚举数据格式.enum_frame_size = ov13855_enum_frame_sizes, // 枚举分辨率.get_fmt = ov13855_get_fmt, // 获取当前格式.set_fmt = ov13855_set_fmt, // 设置格式.get_mbus_config = ov13855_g_mbus_config, // 获取MIPI配置
};
底层:具体的功能实现函数
每个操作集合中的函数指针都指向具体的实现函数,这些函数完成实际的I2C寄存器操作。
12. 关键疑惑解答:I2C操作的真正执行位置
在理解驱动结构时,一个常见的疑惑是:为什么摄像头的实际控制操作都在V4L2的ops函数里,而不是在I2C驱动的probe函数里?
根本原因分析:I2C驱动不是字符设备驱动,它没有提供用户空间可访问的接口:
// I2C驱动只有设备管理接口
struct i2c_driver {int (*probe)(struct i2c_client *client, ...); // 设备发现int (*remove)(struct i2c_client *client); // 设备移除 // 没有file_operations结构!// 没有用户可调用的read/write/ioctl接口!
};// 只有字符设备才有用户接口(V4L2核心提供)
struct file_operations {long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);// 这是用户程序真正调用的接口
};
完整的调用链路:
用户程序: ioctl(fd, VIDIOC_S_FMT, &fmt)↓ (系统调用)
/dev/video0 file_operations (V4L2核心提供)↓ (V4L2核心处理)
V4L2框架: v4l2_subdev_call(sd, pad, set_fmt, ...) ↓ (展开为函数指针调用)
sd->ops->pad->set_fmt(sd, ...)↓ (调用驱动实现)
ov13855_set_fmt() 函数↓ (I2C寄存器操作)
ov13855_write_reg(client, register, value)
各层职责的明确分工:
- I2C层:只负责设备发现和生命周期管理
- V4L2子设备层:提供具体的摄像头功能实现
- V4L2核心层:提供标准的字符设备接口
- 应用层:通过系统调用使用摄像头功能
13. I2C操作的实际分布:probe vs 运行时
让我们通过实际代码来验证一个重要事实:几乎所有的I2C寄存器操作都发生在V4L2操作函数中,probe函数中的I2C操作极少。
probe函数中的有限I2C操作:
static int ov13855_probe(struct i2c_client *client, ...)
{struct ov13855 *ov13855;// 主要工作:建立驱动框架ov13855 = devm_kzalloc(...);ov13855->client = client;// 可能的唯一I2C操作:验证芯片IDret = ov13855_check_sensor_id(ov13855, client);// 注册到V4L2框架v4l2_async_register_subdev_sensor_common(sd);return 0; // probe结束,几乎没有其他I2C操作
}
V4L2操作函数中的密集I2C操作:
// 启动数据流时的大量I2C写操作
static int ov13855_s_stream(struct v4l2_subdev *sd, int on)
{struct ov13855 *ov13855 = to_ov13855(sd);struct i2c_client *client = ov13855->client;if (on) {// 1. 写入完整的寄存器配置表(数百个寄存器)ret = ov13855_write_array(client, ov13855->cur_mode->reg_list);// 2. 设置曝光参数的I2C写入ret = ov13855_write_reg(client, OV13855_REG_EXPOSURE,OV13855_REG_VALUE_24BIT,ov13855->exposure->val);// 3. 设置增益参数的I2C写入ret = ov13855_write_reg(client, OV13855_REG_GAIN_H, ...);// 4. 启动数据流的I2C写入ret = ov13855_write_reg(client, OV13855_REG_CTRL_MODE,OV13855_REG_VALUE_08BIT,OV13855_MODE_STREAMING);}return ret;
}// 参数控制时的I2C写操作
static int ov13855_set_ctrl(struct v4l2_ctrl *ctrl)
{struct ov13855 *ov13855 = container_of(ctrl->handler,struct ov13855, ctrl_handler);switch (ctrl->id) {case V4L2_CID_EXPOSURE:// 每次调整曝光都需要I2C写入ret = ov13855_write_reg(client, OV13855_REG_EXPOSURE, ...);break;case V4L2_CID_ANALOGUE_GAIN:// 每次调整增益都需要I2C写入 ret = ov13855_write_reg(client, OV13855_REG_GAIN_H, ...);break;}return ret;
}
这种设计的深层考虑:
时机匹配原则:只有在实际需要使用摄像头功能时才进行硬件操作,避免不必要的电源消耗和总线占用。
状态同步保证:硬件寄存器操作与软件状态管理在同一个函数中进行,确保状态的一致性。
电源管理配合:I2C寄存器操作只在设备上电状态下进行,避免在芯片掉电时进行无效的总线传输。