文章目录
总体系统框架:
I2C 设备驱动层
i2c_client
(I2C客户端)
- 定义:
i2c_client
表示连接到I2C总线的一个从设备(也称为I2C设备),它是一个具体的I2C从属设备的抽象。 - 作用: 它封装了设备的地址和其他特性,并且与
i2c_driver
关联,用于与I2C适配器进行数据交换。每个连接到I2C总线的设备都有一个对应的i2c_client
结构体。 - 例子: 例如一个连接在I2C总线上的温度传感器就是一个
i2c_client
。
i2c_client
驱动
- 定义:
i2c_client
驱动程序(通常也称为I2C设备驱动程序)是为连接到I2C总线的特定从设备编写的驱动程序。 - 作用: 它负责处理特定I2C设备的高级操作,例如配置设备、读取传感器数据、发送控制命令等。每个I2C从设备通常对应一个特定的
i2c_client
驱动。 - 例子: 一个连接到I2C总线上的温度传感器可能有一个专门的驱动程序,如
lm75.c
用于温度传感器LM75。
i2c_client
驱动 是针对I2C从设备编写的驱动程序,负责与具体的I2C设备进行通信。i2c_client
驱动使用 i2c_adapter
驱动提供的接口与I2C设备进行通信,并实现设备特定的操作。
I2C 适配器驱动层
i2c_adapter
- 定义:
i2c_adapter
表示I2C控制器(或主控器),负责在I2C总线上发送和接收数据。 - 作用: 它是I2C硬件的抽象,负责与I2C设备(即
i2c_client
)进行通信。一个系统可以有多个I2C适配器,每个适配器都可以连接到一个或多个I2C设备。 - 例子: 典型的I2C适配器是SoC中的I2C控制器或PCI/USB等外部I2C控制器。
i2c_adapter
是I2C主控器的抽象,管理整个I2C总线的通信。
i2c_adapter
驱动
- 定义:
i2c_adapter
驱动程序是为I2C控制器(或I2C主控器)编写的驱动程序。 - 作用: 它负责控制I2C总线的低级操作,例如生成时钟信号、发送和接收数据、处理总线仲裁等。每个I2C控制器通常对应一个特定的
i2c_adapter
驱动。 - 例子: 如果你的设备有一个I2C控制器,这个控制器将有一个专门的驱动程序,如
i2c-bcm2835.c
用于树莓派的I2C控制器。
i2c_adapter
驱动提供了与I2C总线进行低级操作的功能,并将这些功能暴露给I2C子系统。
I2C 核心层
i2c_client
需要使用专门的函数与 i2c_adapter
以及 I2C 硬件通信。这些函数通常由 Linux 内核的 I2C 子系统提供,它们抽象了 I2C 通信的底层细节,使 i2c_client
驱动程序能够方便地与 I2C 总线上的硬件设备进行交互。
相关函数接口
i2c_smbus_read_byte_data
/ i2c_smbus_write_byte_data
-
功能: 读取或写入一个字节数据到从设备的特定寄存器地址。
-
用法:
int i2c_smbus_write_byte_data(struct i2c_client *client, u8 command, u8 value); int i2c_smbus_read_byte_data(struct i2c_client *client, u8 command);
-
应用场景: 适用于使用 SMBus 协议通信的设备。
i2c_master_send
/ i2c_master_recv
-
功能: 发送或接收原始的 I2C 数据(字节流)。
-
用法:
int i2c_master_send(struct i2c_client *client, const char *buf, int count); int i2c_master_recv(struct i2c_client *client, char *buf, int count);
-
应用场景: 适用于需要自定义 I2C 通信协议的设备。
i2c_transfer
-
功能: 执行更复杂的 I2C 传输,支持多个消息的批量发送和接收。
-
用法:
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num);
-
应用场景: 需要进行多次读写或混合操作的复杂设备。
i2c_smbus_read_i2c_block_data
/ i2c_smbus_write_i2c_block_data
-
功能: 读取或写入一块数据(多个字节)到 I2C 设备。
-
用法:
int i2c_smbus_read_i2c_block_data(struct i2c_client *client, u8 command, u8 length, u8 *values); int i2c_smbus_write_i2c_block_data(struct i2c_client *client, u8 command, u8 length, const u8 *values);
-
应用场景: 当设备需要传输较大块的数据时使用。
函数工作流程
i2c_client
驱动 使用上述函数与i2c_adapter
进行通信。i2c_adapter
驱动则根据i2c_client
的请求,通过硬件层面与实际的 I2C 设备通信。- 这些函数调用的底层实现最终会调用
i2c_adapter
提供的低级接口,以控制 I2C 硬件完成数据的发送和接收。
为什么不使用i2c_client直接操作硬件?
直接使用 i2c_client
直接操作 I2C 硬件在设计上是被避免的,主要原因如下:
-
抽象与模块化设计
- 抽象层次: Linux 内核使用
i2c_adapter
来抽象底层硬件细节,这种设计使得i2c_client
驱动程序可以独立于具体的硬件控制器。这样,即使在不同的硬件平台上,i2c_client
驱动程序也不需要为每个I2C控制器编写特定代码。 - 模块化:
i2c_adapter
提供了与硬件无关的接口,i2c_client
通过这些接口与 I2C 总线进行通信。这种模块化设计允许i2c_client
驱动程序专注于设备的高层功能,而无需处理底层硬件差异。
- 抽象层次: Linux 内核使用
-
代码可维护性
- 分离职责: 通过将
i2c_adapter
与i2c_client
分离,不同开发者可以专注于各自领域(I2C 控制器驱动 vs I2C 设备驱动)。这种分离使得代码更易于维护和理解。 - 硬件抽象:
i2c_adapter
驱动封装了硬件操作,任何对硬件的更新或变化都可以在i2c_adapter
层次中处理,而无需修改所有的i2c_client
驱动。
- 分离职责: 通过将
-
硬件差异与兼容性
- 适应不同硬件: I2C 硬件控制器可能具有不同的寄存器布局、时序要求、以及功能特性(如支持的I2C时钟速率、SMBus扩展等)。将这些复杂性封装在
i2c_adapter
中,可以使得i2c_client
驱动在各种硬件平台上运行而无需修改。 - 兼容性问题: 直接让
i2c_client
操作 I2C 硬件可能导致不兼容的问题,尤其是在不同的硬件平台上或在I2C总线上有多个设备时。
- 适应不同硬件: I2C 硬件控制器可能具有不同的寄存器布局、时序要求、以及功能特性(如支持的I2C时钟速率、SMBus扩展等)。将这些复杂性封装在
-
错误处理与重试机制
- 中央化错误处理:
i2c_adapter
驱动可以集中处理 I2C 通信中的错误,如仲裁失败、超时等,并且可以实现诸如自动重试的机制。这些机制在i2c_client
驱动中重复实现是低效的,并容易产生不一致的问题。 - 资源管理:
i2c_adapter
驱动可以统一管理 I2C 总线的资源,例如总线锁定、时序控制等,从而避免i2c_client
驱动之间的资源争夺。
- 中央化错误处理:
-
代码复用
- 重用性: 通过将 I2C 硬件操作封装在
i2c_adapter
驱动中,不同的i2c_client
驱动程序可以重用相同的接口进行通信,无需每个设备驱动都重复实现硬件访问逻辑。
- 重用性: 通过将 I2C 硬件操作封装在
i2c client 驱动示例
准备工作
本次I2C 驱动,基于8qm max9286解串器 mipi 驱动简化
设备树 由NXP官方提供的I2C 设备描述
进入系统 i2cdetect -l
查看当前所有的I2C总线
root@imx8qmmek:~# i2cdetect -l
i2c-10 i2c 58246000.i2c I2C adapter
i2c-8 i2c 57247000.i2c I2C adapter
i2c-6 i2c 5a830000.i2c I2C adapter
i2c-4 i2c 56226000.i2c I2C adapter
i2c-9 i2c 58226000.i2c I2C adapter
i2c-7 i2c 56247000.i2c I2C adapter
i2c-5 i2c 5a810000.i2c I2C adapter
已知i2c 9号总线只有一个MAX9286设备
因此可知9号总线对应的是i2c-9 的地址为58226000 ,定位到下方设备树
i2c@58226000 {
compatible = "fsl,imx8qm-lpi2c";
reg = <0x00 0x58226000 0x00 0x1000>;
interrupts = <0x08 0x04>;
interrupt-parent = <0xc5>;
clocks = <0x03 0x112 0x03 0x23b>;
clock-names = "per\0ipg";
assigned-clocks = <0x03 0x112>;
assigned-clock-rates = <0x16e3600>;
power-domains = <0xef>;
status = "okay";
#address-cells = <0x01>;
#size-cells = <0x00>;
clock-frequency = <0x186a0>;
max9286_mipi@6A {
compatible = "maxim,max9286_mipi_test";
reg = <0x6a>;
pinctrl-names = "default";
pinctrl-0 = <0xf0>;
clocks = <0x03 0x00>;
clock-names = "capture_mclk";
mclk = <0x19bfcc0>;
mclk_source = <0x00>;
pwn-gpios = <0xf1 0x1b 0x00>;
virtual-channel;
port {
endpoint {
remote-endpoint = <0xf2>;
data-lanes = <0x01 0x02 0x03 0x04>;
linux,phandle = <0xc6>;
phandle = <0xc6>;
};
};
};
};
直接copy,修改一下compatible,因为8QM有两个同样的MAX9286 ,其中一个已经加载到系统当中, 为了避免冲突,修改compatible指定一个新的驱动
后记:2024年8月12日09:39:34 淦 改掉9号总线驱动描述,10号总线的摄像头少探测两个,,,不想发生冲突还是发生了,(¦3」∠) (¦3」∠) (¦3」∠)
完整代码
#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/ctype.h>
#include <linux/types.h>
#include <linux/delay.h>
#include <linux/clk.h>
#include <linux/of_device.h>
#include <linux/i2c.h>
#include <linux/v4l2-mediabus.h>
#include <linux/of_gpio.h>
#include <linux/pinctrl/consumer.h>
#include <linux/regulator/consumer.h>
#include <media/v4l2-subdev.h>
/* Device-specific constants */
#define MAX9286_REG_STATUS 0x0F
#define ADDR_MAX9286 0x6A
/* Driver data structure */
struct sensor_data
{
struct i2c_client *client;
int pwn_gpio_0;
int pwn_gpio;
int pwn_gpio_1;
u32 mclk;
u8 mclk_source;
struct clk *sensor_clk;
};
/* Function to read a register */
static int max9286_read_reg(struct i2c_client *client, u8 reg)
{
int ret;
client->addr = ADDR_MAX9286;
ret = i2c_smbus_read_byte_data(client, reg);
if (ret < 0)
{
dev_err(&client->dev, "Failed to read reg 0x%02x: %d\n", reg, ret);
return ret;
}
return ret;
}
/* Function to write a register */
static int max9286_write_reg(struct i2c_client *client, u8 reg, u8 value)
{
int ret;
client->addr = ADDR_MAX9286;
ret = i2c_smbus_write_byte_data(client, reg, value);
dev_dbg(&client->dev,
"%s: addr %02x reg %02x val %02x\n",
__func__, client->addr, reg, value);
if (ret < 0)
{
dev_err(&client->dev, "Failed to write reg 0x%02x: %d\n", reg, ret);
return ret;
}
return 0;
}
// 重启设备
static void max9286_hw_reset(struct sensor_data *max9286_data)
{
printk("______________%s________________", __func__);
gpio_set_value(max9286_data->pwn_gpio, 0);
msleep(100);
gpio_set_value(max9286_data->pwn_gpio, 1);
msleep(50);
}
/* Probe function */
static int max9286_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
struct sensor_data *max9286_data;
int retval;
struct device *dev = &client->dev;
max9286_data = devm_kzalloc(dev, sizeof(*max9286_data), GFP_KERNEL);
if (!max9286_data)
return -ENOMEM;
max9286_data->client = client;
/* Set initial values for the sensor struct. */
max9286_data->sensor_clk = devm_clk_get(dev, "capture_mclk");
if (IS_ERR(max9286_data->sensor_clk))
{
/* assuming clock enabled by default */
max9286_data->sensor_clk = NULL;
dev_err(dev, "clock-frequency missing or invalid\n");
return PTR_ERR(max9286_data->sensor_clk);
}
retval = of_property_read_u32(dev->of_node, "mclk",
&(max9286_data->mclk));
if (retval)
{
dev_err(dev, "mclk missing or invalid\n");
return retval;
}
retval = of_property_read_u32(dev->of_node, "mclk_source",
(u32 *)&(max9286_data->mclk_source));
if (retval)
{
dev_err(dev, "mclk_source missing or invalid\n");
return retval;
}
// MAX9286 设备启动开始 copy 官方驱动写法
/* request power down pin */
max9286_data->pwn_gpio = of_get_named_gpio(dev->of_node, "pwn-gpios", 0);
if (!gpio_is_valid(max9286_data->pwn_gpio))
{
dev_err(dev, "no sensor pwdn 0 pin available\n");
return -ENODEV;
}
retval = devm_gpio_request_one(dev, max9286_data->pwn_gpio, GPIOF_OUT_INIT_HIGH,
"max9286_pwd");
if (retval < 0)
return retval;
max9286_hw_reset(max9286_data);
clk_prepare_enable(max9286_data->sensor_clk);
// MAX9286 设备启动结束
retval = max9286_read_reg(max9286_data->client, 0x1e);
if (retval != 0x40)
{
pr_warning("max9286 is not found, chip id reg 0x1e = 0x%x.\n", retval);
clk_disable_unprepare(max9286_data->sensor_clk);
// devm_gpio_free(dev, max9286_data->pwn_gpio_0);
return -ENODEV;
}
dev_info(&client->dev, "MAX9286 detected, status=0x%02x\n", retval);
return 0;
}
/* Remove function */
static int max9286_remove(struct i2c_client *client)
{
struct sensor_data *data = i2c_get_clientdata(client);
clk_disable_unprepare(data->sensor_clk);
return 0;
}
/* Compatible table for device tree matching */
static const struct of_device_id max9286_of_match[] = {
{
.compatible = "maxim,max9286_mipi_test",
},
{},
};
MODULE_DEVICE_TABLE(of, max9286_of_match);
/* I2C device ID table */
static const struct i2c_device_id max9286_id[] = {
{"max9286_mipi_test", 0},
{},
};
MODULE_DEVICE_TABLE(i2c, max9286_id);
/* I2C driver structure */
static struct i2c_driver max9286_driver = {
.driver = {
.name = "max9286_mipi_test",
.of_match_table = max9286_of_match,
},
.probe = max9286_probe,
.remove = max9286_remove,
.id_table = max9286_id,
};
module_i2c_driver(max9286_driver);
MODULE_AUTHOR("Marxist");
MODULE_DESCRIPTION("MAX9286 MIPI I2C Driver");
MODULE_LICENSE("GPL");
MODULE_ALIAS("CSI");
关键点解析
i2c驱动写法也遵循总线模型,基本思想一致,驱动ID与驱动名称与compatible一一对应
关键参数 i2c client ,具体的构成:
addr
:这个字段保存设备的7位 I²C 地址。I²C 控制器通过这个地址与总线上的设备通信。
adapter
:指向 i2c_adapter
结构体的指针,表示与该设备连接的 I²C 总线控制器。
dev
:表示设备在 Linux 设备模型中的结构体。它用于设备管理、电源管理和日志记录。
name
:一个包含 I²C 设备名称的字符串。
flags
:提供关于 I²C 设备的附加信息,如10位地址模式的使用等。
开始工作—匹配设备树
/* Compatible table for device tree matching */
static const struct of_device_id max9286_of_match[] = {
{
.compatible = "maxim,max9286_mipi_test",
},
{},
};
MODULE_DEVICE_TABLE(of, max9286_of_match);
/* I2C device ID table */
static const struct i2c_device_id max9286_id[] = {
{"max9286_mipi_test", 0},
{},
};
MODULE_DEVICE_TABLE(i2c, max9286_id);
/* I2C driver structure */
static struct i2c_driver max9286_driver = {
.driver = {
.name = "max9286_mipi_test",
.of_match_table = max9286_of_match,
},
.probe = max9286_probe,
.remove = max9286_remove,
.id_table = max9286_id,
};
module_i2c_driver(max9286_driver);
MODULE_AUTHOR("Marxist");
MODULE_DESCRIPTION("MAX9286 MIPI I2C Driver");
MODULE_LICENSE("GPL");
MODULE_ALIAS("CSI");
重心仍在在于probe函数
获取设备树设备描述参数
/* Set initial values for the sensor struct. */
max9286_data->sensor_clk = devm_clk_get(dev, "capture_mclk");
if (IS_ERR(max9286_data->sensor_clk))
{
/* assuming clock enabled by default */
max9286_data->sensor_clk = NULL;
dev_err(dev, "clock-frequency missing or invalid\n");
return PTR_ERR(max9286_data->sensor_clk);
}
retval = of_property_read_u32(dev->of_node, "mclk",
&(max9286_data->mclk));
if (retval)
{
dev_err(dev, "mclk missing or invalid\n");
return retval;
}
retval = of_property_read_u32(dev->of_node, "mclk_source",
(u32 *)&(max9286_data->mclk_source));
if (retval)
{
dev_err(dev, "mclk_source missing or invalid\n");
return retval;
}
根据GPIO 启动设备,作为一个合格的CV工程师,直接copy官方写法
// MAX9286 设备启动开始 copy 官方驱动写法
/* request power down pin */
max9286_data->pwn_gpio = of_get_named_gpio(dev->of_node, "pwn-gpios", 0);
if (!gpio_is_valid(max9286_data->pwn_gpio))
{
dev_err(dev, "no sensor pwdn 0 pin available\n");
return -ENODEV;
}
retval = devm_gpio_request_one(dev, max9286_data->pwn_gpio, GPIOF_OUT_INIT_HIGH,
"max9286_pwd");
if (retval < 0)
return retval;
max9286_hw_reset(max9286_data);
clk_prepare_enable(max9286_data->sensor_clk);
// MAX9286 设备启动结束
启动完成之后,就可以对i2c设备进行读写了
关键的两个API:i2c_smbus_read_byte_data``和i2c_smbus_write_byte_data
/* Function to read a register */
static int max9286_read_reg(struct i2c_client *client, u8 reg)
{
int ret;
client->addr = ADDR_MAX9286;
ret = i2c_smbus_read_byte_data(client, reg);
if (ret < 0)
{
dev_err(&client->dev, "Failed to read reg 0x%02x: %d\n", reg, ret);
return ret;
}
return ret;
}
/* Function to write a register */
static int max9286_write_reg(struct i2c_client *client, u8 reg, u8 value)
{
int ret;
client->addr = ADDR_MAX9286;
ret = i2c_smbus_write_byte_data(client, reg, value);
dev_dbg(&client->dev,
"%s: addr %02x reg %02x val %02x\n",
__func__, client->addr, reg, value);
if (ret < 0)
{
dev_err(&client->dev, "Failed to write reg 0x%02x: %d\n", reg, ret);
return ret;
}
return 0;
}
通过MAX9286的数据手册来看看,哪些能读一读
比如:读0x1E和0x1F 可以得到设备ID和修订
对应的代码就比较简单了
retval = max9286_read_reg(max9286_data->client, 0x1e);
if (retval != 0x40)
{
pr_warning("max9286 is not found, chip id reg 0x1e = 0x%x.\n", retval);
clk_disable_unprepare(max9286_data->sensor_clk);
// devm_gpio_free(dev, max9286_data->pwn_gpio_0);
return -ENODEV;
}
dev_info(&client->dev, "MAX9286 detected, status=0x%02x\n", retval);
主要是看数据手册 通过读写操作API 控制寄存器
读寄存器的例子
retval = max9286_read_reg(max9286_data->client, 0x1e);
if (retval != 0x40)
{
pr_warning("max9286 is not found, chip id reg 0x1e = 0x%x.\n", retval);
clk_disable_unprepare(max9286_data->sensor_clk);
// devm_gpio_free(dev, max9286_data->pwn_gpio_0);
return -ENODEV;
}
dev_info(&client->dev, "MAX9286 detected, status=0x%02x\n", retval);
实际开发 案例:设置MAX9286的时间同步
具体时间同步的原理这里不再赘述,对应到代码操作,就是写寄存器
根据数据手册来看,0x01这个寄存器地址便是控制时间同步的相关设置,左侧二进制为操作值,右侧二进制为设定的默认值
max9286_write_reg(max9286_data, 0x01, 0x00);
则代表往0x01寄存器写入 0x00 代表的含义为 帧内同步,且为手动模式
上述例子,只是描述相机开发中一个业务逻辑,普通的业务逻辑都是操作寄存器的值来完成相应的命令。
效果演示:
加载驱动,内核打印日志分析
i2c client 驱动深度解析
在这个I²C驱动中,I²C设备的读写操作是由驱动程序中的函数(如max9286_read_reg
和max9286_write_reg
)发起的,但实际执行I²C传输的命令是由I²C控制器负责的。(结合系统框图来看)
具体流程:
- 驱动程序发起I²C操作:
- 当驱动程序调用
i2c_smbus_read_byte_data
或i2c_smbus_write_byte_data
函数时,它实际上是在请求I²C控制器与I²C设备进行通信。 - 例如,
max9286_read_reg
函数中调用了i2c_smbus_read_byte_data
来读取寄存器的数据,这个函数是I²C子系统提供的API。
- 当驱动程序调用
- I²C控制器执行传输:
- 这些I²C API(如
i2c_smbus_read_byte_data
)通过I²C适配器(由I²C控制器驱动提供)与I²C控制器通信。 - I²C控制器通过I²C总线发送具体的读/写命令给目标I²C设备(这里是MAX9286),并处理I²C总线上所有的时序和信号。
- 这些I²C API(如
- 控制器与设备交互:
- I²C控制器根据驱动程序请求发送的命令与目标设备进行实际的数据传输,读取或写入设备的寄存器。
- 数据传输完成后,控制器将结果返回给驱动程序。
开发注意事项
对于大多数普通开发者,尤其是那些编写 I2C 设备驱动 (i2c_client
驱动) 的开发者来说,通常不需要深入了解 i2c_adapter
驱动的具体实现。这是因为 i2c_adapter
驱动负责处理底层的硬件操作,而 i2c_client
驱动通过标准的 I2C API 与它进行通信。
为什么普通开发者不需要关心 i2c_adapter
的实现:
- 抽象层次:
i2c_adapter
驱动将底层硬件的复杂性抽象化,提供统一的接口给i2c_client
驱动使用。因此,普通开发者只需要调用内核提供的 I2C API 函数,如i2c_smbus_read_byte_data
、i2c_master_send
等,来与 I2C 设备进行通信,而不需要了解这些函数背后的实现细节。
- 可移植性:
- 使用内核标准的 I2C API 编写的
i2c_client
驱动程序可以在不同的硬件平台上运行,而无需修改。这是因为i2c_adapter
驱动在不同的平台上已经处理了硬件特定的差异。开发者无需担心底层 I2C 控制器的差异。
- 使用内核标准的 I2C API 编写的
- 开发效率:
- 关注
i2c_client
的开发可以让开发者专注于设备的功能实现,而不必分散精力去理解复杂的底层硬件操作逻辑。通过使用已经定义好的 API,开发者能够更快地实现功能,减少开发周期。
- 关注
- 内核维护:
- Linux 内核开发团队通常会负责
i2c_adapter
驱动的开发和维护。它们保证了这些驱动的稳定性和兼容性。普通开发者通常只需要依赖已经存在的i2c_adapter
驱动,而不是自己编写或修改它们。
- Linux 内核开发团队通常会负责
何时需要关心 i2c_adapter
的实现:
尽管普通开发者通常不需要关心 i2c_adapter
的实现,但在以下一些特定场景下,了解 i2c_adapter
的实现可能是必要的:
- 开发新的 I2C 控制器驱动:
- 如果你在工作中涉及到为新的 I2C 硬件控制器编写驱动程序,或者需要修改现有的
i2c_adapter
驱动,那么你就需要深入理解i2c_adapter
的实现。
- 如果你在工作中涉及到为新的 I2C 硬件控制器编写驱动程序,或者需要修改现有的
- 调试复杂问题:
- 如果在调试 I2C 相关问题时发现问题可能出在 I2C 控制器或
i2c_adapter
驱动上,你可能需要了解一些i2c_adapter
的实现细节,以便更有效地定位和解决问题。
- 如果在调试 I2C 相关问题时发现问题可能出在 I2C 控制器或
- 性能优化:
- 在需要针对特定硬件平台进行性能优化时,理解
i2c_adapter
的实现有助于发现可能的优化机会,例如 I2C 传输速度、总线时序调整等。
- 在需要针对特定硬件平台进行性能优化时,理解