【驱动】I2C驱动分析(四)-关键API解析

简介

在Linux内核源代码中的driver目录下包含一个i2c目录

i2c-core.c这个文件实现了I2C核心的功能以及/proc/bus/i2c*接口。
  i2c-dev.c实现了I2C适配器设备文件的功能,每一个I2C适配器都被分配一个设备。通过适配器访设备时的主设备号都为89,次设备号为0-255。I2c-dev.c并没有针对特定的设备而设计,只是提供了通用的read(),write(),和ioctl()等接口,应用层可以借用这些接口访问挂接在适配器上的I2C设备的存储空间或寄存器,并控制I2C设备的工作方式。

busses文件夹这个文件中包含了一些I2C总线的驱动,如针对S3C2410,S3C2440,S3C6410等处理器的I2C控制器驱动为i2c-s3c2410.c.
  algos文件夹实现了一些I2C总线适配器的algorithm.

I2C Core

i2c_new_device

i2c_new_device用于创建一个新的I2C设备,这个函数将会使用info提供的信息建立一个i2c_client并与第一个参数指向的i2c_adapter绑定。返回的参数是一个i2c_client指针。驱动中可以直接使用i2c_client指针和设备通信了。

struct i2c_client *
i2c_new_device(struct i2c_adapter *adap, struct i2c_board_info const *info)
{struct i2c_client	*client;int			status;client = kzalloc(sizeof *client, GFP_KERNEL);if (!client)return NULL;client->adapter = adap;client->dev.platform_data = info->platform_data;if (info->archdata)client->dev.archdata = *info->archdata;client->flags = info->flags;client->addr = info->addr;client->irq = info->irq;strlcpy(client->name, info->type, sizeof(client->name));status = i2c_check_addr_validity(client->addr, client->flags);if (status) {dev_err(&adap->dev, "Invalid %d-bit I2C address 0x%02hx\n",client->flags & I2C_CLIENT_TEN ? 10 : 7, client->addr);goto out_err_silent;}/* Check for address business */status = i2c_check_addr_ex(adap, i2c_encode_flags_to_addr(client));if (status != 0)dev_err(&adap->dev, "%d i2c clients have been registered at 0x%02x",status, client->addr);client->dev.parent = &client->adapter->dev;client->dev.bus = &i2c_bus_type;client->dev.type = &i2c_client_type;client->dev.of_node = info->of_node;client->dev.fwnode = info->fwnode;i2c_dev_set_name(adap, client, status);status = device_register(&client->dev);if (status)goto out_err;dev_dbg(&adap->dev, "client [%s] registered with bus id %s\n",client->name, dev_name(&client->dev));return client;out_err:dev_err(&adap->dev, "Failed to register i2c client %s at 0x%02x ""(%d)\n", client->name, client->addr, status);
out_err_silent:kfree(client);return NULL;
}
  • 函数通过调用kzalloc函数为client变量分配了一个大小为sizeof *client的内存块,并将其初始化为零。
  • adap参数赋值给client->adapter,表示新创建的设备将使用该I2C适配器。
  • 板级信息中的平台数据,标志位,I2C设备地址,中断号等传给client结构体。
  • 调用i2c_check_addr_validity函数检查I2C设备地址的有效性
  • 调用i2c_check_addr_ex函数检查地址的有效性,如果返回值不为0,表示已经有其他I2C设备注册在相同的地址上。
  • 设置新创建的设备的父设备为适配器的设备。将设备总线类型设置为I2C总线类型,并将设备类型设置为I2C客户端类型。
  • 将与设备树相关的节点of_node,与固件节点相关的节点fwnode传递给新创建的设备。
  • 调用i2c_dev_set_name函数为设备设置名称。
  • 最后会调用device_register函数注册新创建的设备。返回指向新创建的I2C设备的指针。

i2c_device_match

i2c_device_match 函数根据设备和设备驱动程序之间的不同匹配方式,检查它们之间是否存在匹配关系。这个函数通常在 I2C 子系统的设备驱动程序注册过程中使用,以确定哪个驱动程序适用于给定的设备。

static int i2c_device_match(struct device *dev, struct device_driver *drv)
{struct i2c_client	*client = i2c_verify_client(dev);struct i2c_driver	*driver;if (!client)return 0;/* Attempt an OF style match */if (of_driver_match_device(dev, drv))return 1;/* Then ACPI style match */if (acpi_driver_match_device(dev, drv))return 1;driver = to_i2c_driver(drv);/* match on an id table if there is one */if (driver->id_table)return i2c_match_id(driver->id_table, client) != NULL;return 0;
}
  • i2c_verify_client 函数用于验证设备是否是有效的 I2C 客户端设备。

  • 进行 OF (Open Firmware) 风格的匹配,通过调用 of_driver_match_device 函数来检查设备和驱动程序之间是否存在匹配关系。

  • 如果 OF 风格的匹配失败,则尝试进行 ACPI (Advanced Configuration and Power Interface) 风格的匹配,通过调用 acpi_driver_match_device 函数来检查设备和驱动程序之间是否存在匹配关系。

  • 如果 OF 和 ACPI 风格的匹配都失败了,代码将继续执行。调用to_i2c_driver 宏将其转换为 `struct i2c_driver 结构体指针。

  • 代码检查 driverid_table 字段是否为空。id_table 是一个指向 I2C 驱动程序支持的设备 ID 表的指针。如果 id_table 不为空,则调用 i2c_match_id 函数,将 driver->id_tableclient 作为参数进行匹配。如果匹配成功,即找到了匹配的设备 ID,函数返回的结果不为空,表示找到了匹配的设备驱动程序。

i2c_device_probe

i2c_device_probe 函数执行了 I2C 设备的探测操作。它设置中断信息、处理唤醒功能、设置时钟、关联功耗域,并调用驱动程序的 probe 函数进行设备特定的探测操作。

static int i2c_device_probe(struct device *dev)
{struct i2c_client	*client = i2c_verify_client(dev);struct i2c_driver	*driver;int status;if (!client)return 0;if (!client->irq) {int irq = -ENOENT;if (dev->of_node) {irq = of_irq_get_byname(dev->of_node, "irq");if (irq == -EINVAL || irq == -ENODATA)irq = of_irq_get(dev->of_node, 0);} else if (ACPI_COMPANION(dev)) {irq = acpi_dev_gpio_irq_get(ACPI_COMPANION(dev), 0);}if (irq == -EPROBE_DEFER)return irq;if (irq < 0)irq = 0;client->irq = irq;}driver = to_i2c_driver(dev->driver);if (!driver->probe || !driver->id_table)return -ENODEV;if (client->flags & I2C_CLIENT_WAKE) {int wakeirq = -ENOENT;if (dev->of_node) {wakeirq = of_irq_get_byname(dev->of_node, "wakeup");if (wakeirq == -EPROBE_DEFER)return wakeirq;}device_init_wakeup(&client->dev, true);if (wakeirq > 0 && wakeirq != client->irq)status = dev_pm_set_dedicated_wake_irq(dev, wakeirq);else if (client->irq > 0)status = dev_pm_set_wake_irq(dev, client->irq);elsestatus = 0;if (status)dev_warn(&client->dev, "failed to set up wakeup irq");}dev_dbg(dev, "probe\n");status = of_clk_set_defaults(dev->of_node, false);if (status < 0)goto err_clear_wakeup_irq;status = dev_pm_domain_attach(&client->dev, true);if (status == -EPROBE_DEFER)goto err_clear_wakeup_irq;status = driver->probe(client, i2c_match_id(driver->id_table, client));if (status)goto err_detach_pm_domain;return 0;err_detach_pm_domain:dev_pm_domain_detach(&client->dev, true);
err_clear_wakeup_irq:dev_pm_clear_wake_irq(&client->dev);device_init_wakeup(&client->dev, false);return status;
}
  • i2c_verify_client 函数用于验证设备是否是有效的 I2C 客户端设备。如果 client 为空,则说明设备不是有效的 I2C 客户端设备。

  • 检查 client 的中断(IRQ)是否已经设置。如果 client->irq 为 0,说明中断尚未设置。代码尝试从设备的设备树(device tree)或 ACPI 中获取中断信息,并将其赋值给 client->irq

  • 获取设备的驱动程序,并将其赋值给 driver 变量。这里使用了 to_i2c_driver 宏将 dev->driver 转换为 struct i2c_driver 结构体指针。

  • 检查 driverprobeid_table 字段是否为空。如果其中任何一个为空,表示驱动程序不支持探测操作或设备 ID 表,函数返回 -ENODEV(设备不存在)。

  • 如果 I2C 客户端设备的标志(client->flags)中包含 I2C_CLIENT_WAKE,表示该设备支持唤醒功能。代码尝试获取唤醒中断(wakeup IRQ)并进行设置。首先,通过设备的设备树或 ACPI 获取唤醒中断信息。然后,使用 dev_pm_set_dedicated_wake_irqdev_pm_set_wake_irq 函数设置唤醒中断。如果设置失败,会发出警告信息。

  • 调用 of_clk_set_defaults 函数为设备的时钟设置默认值。如果设置失败,跳转到 err_clear_wakeup_irq 标签处进行清理操作。

  • 调用 dev_pm_domain_attach 函数将设备与功耗域(power domain)关联起来。如果探测操作被推迟(deferred),跳转到 err_clear_wakeup_irq 标签处进行清理操作。

  • 调用驱动程序的 probe 函数,并传递 clienti2c_match_id(driver->id_table, client) 作为参数。i2c_match_id 函数用于在设备 ID 表中查找与给定设备匹配的条目。如果 probe 函数返回非零值,表示探测操作失败,跳转到 err_detach_pm_domain 标签处进行清理操作。

i2c_device_remove

i2c_device_remove 函数执行了 I2C 设备的移除操作。它调用驱动程序的 remove 函数,并进行功耗域的分离、唤醒中断的清除以及设备唤醒状态的设置。

static int i2c_device_remove(struct device *dev)
{struct i2c_client	*client = i2c_verify_client(dev);struct i2c_driver	*driver;int status = 0;if (!client || !dev->driver)return 0;driver = to_i2c_driver(dev->driver);if (driver->remove) {dev_dbg(dev, "remove\n");status = driver->remove(client);}dev_pm_domain_detach(&client->dev, true);dev_pm_clear_wake_irq(&client->dev);device_init_wakeup(&client->dev, false);return status;
}
  • i2c_verify_client 函数用于验证设备是否是有效的 I2C 客户端设备。如果 client 为空,则说明设备不是有效的 I2C 客户端设备。

  • 使用 to_i2c_driver 宏将 dev->driver 转换为 struct i2c_driver 结构体指针。

  • 如果驱动程序的 remove 字段不为空,表示驱动程序支持移除操作。开始进行移除操作,并调用驱动程序的 remove 函数。

  • 调用 dev_pm_domain_detach 函数分离设备和功耗域的关联。

  • 调用 dev_pm_clear_wake_irq 函数清除唤醒中断设置。

  • 调用 device_init_wakeup 函数将设备的唤醒状态设置为 false。

i2c_register_adapter

i2c_register_adapter 函数用于注册一个 I2C 适配器。它进行了一系列的完整性检查和初始化操作,并注册适配器设备。然后,注册与适配器相关的设备节点、ACPI 设备和空间处理器。最后,遍历所有的 I2C 驱动程序,并通知它们有新的适配器注册了。

static int i2c_register_adapter(struct i2c_adapter *adap)
{int res = 0;/* Can't register until after driver model init */if (unlikely(WARN_ON(!i2c_bus_type.p))) {res = -EAGAIN;goto out_list;}/* Sanity checks */if (unlikely(adap->name[0] == '\0')) {pr_err("i2c-core: Attempt to register an adapter with ""no name!\n");return -EINVAL;}if (unlikely(!adap->algo)) {pr_err("i2c-core: Attempt to register adapter '%s' with ""no algo!\n", adap->name);return -EINVAL;}rt_mutex_init(&adap->bus_lock);mutex_init(&adap->userspace_clients_lock);INIT_LIST_HEAD(&adap->userspace_clients);/* Set default timeout to 1 second if not already set */if (adap->timeout == 0)adap->timeout = HZ;dev_set_name(&adap->dev, "i2c-%d", adap->nr);adap->dev.bus = &i2c_bus_type;adap->dev.type = &i2c_adapter_type;res = device_register(&adap->dev);if (res)goto out_list;dev_dbg(&adap->dev, "adapter [%s] registered\n", adap->name);pm_runtime_no_callbacks(&adap->dev);#ifdef CONFIG_I2C_COMPATres = class_compat_create_link(i2c_adapter_compat_class, &adap->dev,adap->dev.parent);if (res)dev_warn(&adap->dev,"Failed to create compatibility class link\n");
#endif/* bus recovery specific initialization */if (adap->bus_recovery_info) {struct i2c_bus_recovery_info *bri = adap->bus_recovery_info;if (!bri->recover_bus) {dev_err(&adap->dev, "No recover_bus() found, not using recovery\n");adap->bus_recovery_info = NULL;goto exit_recovery;}/* Generic GPIO recovery */if (bri->recover_bus == i2c_generic_gpio_recovery) {if (!gpio_is_valid(bri->scl_gpio)) {dev_err(&adap->dev, "Invalid SCL gpio, not using recovery\n");adap->bus_recovery_info = NULL;goto exit_recovery;}if (gpio_is_valid(bri->sda_gpio))bri->get_sda = get_sda_gpio_value;elsebri->get_sda = NULL;bri->get_scl = get_scl_gpio_value;bri->set_scl = set_scl_gpio_value;} else if (!bri->set_scl || !bri->get_scl) {/* Generic SCL recovery */dev_err(&adap->dev, "No {get|set}_gpio() found, not using recovery\n");adap->bus_recovery_info = NULL;}}exit_recovery:/* create pre-declared device nodes */of_i2c_register_devices(adap);acpi_i2c_register_devices(adap);acpi_i2c_install_space_handler(adap);if (adap->nr < __i2c_first_dynamic_bus_num)i2c_scan_static_board_info(adap);/* Notify drivers */mutex_lock(&core_lock);bus_for_each_drv(&i2c_bus_type, NULL, adap, __process_new_adapter);mutex_unlock(&core_lock);return 0;out_list:mutex_lock(&core_lock);idr_remove(&i2c_adapter_idr, adap->nr);mutex_unlock(&core_lock);return res;
}
  • 检查 i2c_bus_type 的状态,确保驱动模型已经初始化。

  • 完整性检查。它检查适配器的名称 adap->name 和适配器的算法 adap->algo 是否为空。

  • 初始化适配器的互斥锁 adap->bus_lock、用户空间客户端锁 adap->userspace_clients_lock,以及用户空间客户端列表 adap->userspace_clients

  • 如果适配器的超时时间 adap->timeout 为 0,则将其设置为默认值 1 秒(HZ 表示秒数)。

  • 设置适配器设备的名称为 "i2c-%d",其中 %d 会被适配器的编号 adap->nr 替换。然后,它设置适配器设备的总线类型为 &i2c_bus_type,设备类型为 &i2c_adapter_type

  • 代码调用 device_register 函数注册适配器设备。如果注册失败,会返回相应的错误码,并进行清理操作。

  • 代码调用 pm_runtime_no_callbacks 函数设置适配器设备的电源管理运行时回调。

  • 如果定义了宏 CONFIG_I2C_COMPAT,代码调用 class_compat_create_link 函数创建适配器设备与兼容性类之间的链接。

  • 如果适配器具有 bus_recovery_info 字段,表示支持总线恢复功能。代码进行一些适配器恢复功能相关的初始化,包括检查恢复函数的有效性以及设置 GPIO 相关的函数。

  • 如果适配器的编号小于预定义的动态总线编号 __i2c_first_dynamic_bus_num,则调用 i2c_scan_static_board_info 函数扫描静态板级信息。

  • 代码通过调用 bus_for_each_drv 函数遍历所有注册的 I2C 驱动程序,并调用 __process_new_adapter 函数处理每个驱动程序。

  • 最后,返回 0 表示注册成功。

i2c_add_adapter

i2c_add_adapter 函数用于添加一个新的 I2C 适配器。它先尝试从设备树节点中获取适配器的编号,如果成功则使用指定的编号添加适配器。如果没有相关的设备树节点或获取编号失败,函数会在动态范围内分配一个适配器 ID,并将适配器与该 ID 相关联。然后,函数调用 i2c_register_adapter 函数注册适配器,并返回注册函数的返回值。

int i2c_add_adapter(struct i2c_adapter *adapter)
{struct device *dev = &adapter->dev;int id;if (dev->of_node) {id = of_alias_get_id(dev->of_node, "i2c");if (id >= 0) {adapter->nr = id;return __i2c_add_numbered_adapter(adapter);}}mutex_lock(&core_lock);id = idr_alloc(&i2c_adapter_idr, adapter,__i2c_first_dynamic_bus_num, 0, GFP_KERNEL);mutex_unlock(&core_lock);if (id < 0)return id;adapter->nr = id;return i2c_register_adapter(adapter);
}
  • 首先,代码获取适配器的设备结构体指针 dev,并将其赋值给变量 dev

  • 如果适配器设备具有 of_node 字段,表示设备与设备树节点相关联。代码通过调用 of_alias_get_id 函数从设备树节点中获取 “i2c” 别名的 ID。如果获取的 ID 大于等于 0,则将其赋值给适配器的编号 adapter->nr,并调用 __i2c_add_numbered_adapter 函数以指定的编号添加适配器。然后,函数返回该函数的返回值。

  • 如果设备没有相关的设备树节点或没有获取到别名的 ID,代码通过调用 mutex_lock 函数锁定 core_lock 互斥锁,以确保在添加适配器时不会发生竞争条件。

  • 代码调用 idr_alloc 函数从 i2c_adapter_idr 中分配一个适配器 ID,并将适配器结构体指针 adapter 与该 ID 相关联。参数 __i2c_first_dynamic_bus_num 表示分配的 ID 必须大于或等于该值。如果分配失败,即返回的 ID 小于 0,代码将返回该错误码。

  • 如果成功分配了适配器 ID,代码将该 ID 赋值给适配器的编号 adapter->nr

  • 代码通过调用 i2c_register_adapter 函数注册适配器并返回注册函数 i2c_register_adapter 的返回值。

i2c_detect_address

i2c_detect_address 函数用于检测指定地址上是否存在 I2C 设备,并执行自定义的设备检测函数。它会进行一系列的检查,包括地址的有效性、地址是否已被占用以及地址上是否存在设备。如果检测成功,函数会调用自定义的检测函数并根据检测结果进行相应的处理,包括创建新的设备实例并添加到驱动程序的客户端列表中。

static int i2c_detect_address(struct i2c_client *temp_client,struct i2c_driver *driver)
{struct i2c_board_info info;struct i2c_adapter *adapter = temp_client->adapter;int addr = temp_client->addr;int err;/* Make sure the address is valid */err = i2c_check_7bit_addr_validity_strict(addr);if (err) {dev_warn(&adapter->dev, "Invalid probe address 0x%02x\n",addr);return err;}/* Skip if already in use (7 bit, no need to encode flags) */if (i2c_check_addr_busy(adapter, addr))return 0;/* Make sure there is something at this address */if (!i2c_default_probe(adapter, addr))return 0;/* Finally call the custom detection function */memset(&info, 0, sizeof(struct i2c_board_info));info.addr = addr;err = driver->detect(temp_client, &info);if (err) {/* -ENODEV is returned if the detection fails. We catch ithere as this isn't an error. */return err == -ENODEV ? 0 : err;}/* Consistency check */if (info.type[0] == '\0') {dev_err(&adapter->dev, "%s detection function provided ""no name for 0x%x\n", driver->driver.name,addr);} else {struct i2c_client *client;/* Detection succeeded, instantiate the device */if (adapter->class & I2C_CLASS_DEPRECATED)dev_warn(&adapter->dev,"This adapter will soon drop class based instantiation of devices. ""Please make sure client 0x%02x gets instantiated by other means. ""Check 'Documentation/i2c/instantiating-devices' for details.\n",info.addr);dev_dbg(&adapter->dev, "Creating %s at 0x%02x\n",info.type, info.addr);client = i2c_new_device(adapter, &info);if (client)list_add_tail(&client->detected, &driver->clients);elsedev_err(&adapter->dev, "Failed creating %s at 0x%02x\n",info.type, info.addr);}return 0;
}
  • 调用 i2c_check_7bit_addr_validity_strict 函数检查地址的有效性。

  • 代码调用 i2c_check_addr_busy 函数检查地址是否已经在使用。

  • 代码调用 i2c_default_probe 函数检查该地址上是否存在设备。

  • 代码初始化一个 struct i2c_board_info 结构体变量 info,并将地址 addr 赋值给 info.addr 字段。

  • 代码调用设备驱动程序的 detect 函数,传入临时客户端 temp_clientinfo 结构体变量。这个自定义的检测函数用于检测设备是否存在,并填充 info 结构体中的其他字段。如果检测函数返回错误码,函数会检查错误码是否为 -ENODEV(表示设备检测失败),如果是则返回 0 表示不需要进行进一步的设备检测。

  • 如果设备检测成功,函数会检查 info 结构体中的设备名称字段 info.type 是否为空。如果为空,表示自定义的检测函数没有提供设备名称。否则,函数会创建一个新的 I2C 客户端设备,并将其添加到驱动程序的客户端列表中。

  • 如果适配器的 class 字段包含 I2C_CLASS_DEPRECATED 标志,表示该适配器将来会停止使用基于类的设备实例化方法。函数会打印警告信息,提醒开发者使用其他方式实例化设备。

i2c_detect

i2c_detect函数根据给定的适配器和驱动程序,通过遍历地址列表并调用i2c_detect_address函数,检测I2C适配器上连接的设备是否存在。

static int i2c_detect(struct i2c_adapter *adapter, struct i2c_driver *driver)
{const unsigned short *address_list;struct i2c_client *temp_client;int i, err = 0;int adap_id = i2c_adapter_id(adapter);address_list = driver->address_list;if (!driver->detect || !address_list)return 0;/* Warn that the adapter lost class based instantiation */if (adapter->class == I2C_CLASS_DEPRECATED) {dev_dbg(&adapter->dev,"This adapter dropped support for I2C classes and ""won't auto-detect %s devices anymore. If you need it, check ""'Documentation/i2c/instantiating-devices' for alternatives.\n",driver->driver.name);return 0;}/* Stop here if the classes do not match */if (!(adapter->class & driver->class))return 0;/* Set up a temporary client to help detect callback */temp_client = kzalloc(sizeof(struct i2c_client), GFP_KERNEL);if (!temp_client)return -ENOMEM;temp_client->adapter = adapter;for (i = 0; address_list[i] != I2C_CLIENT_END; i += 1) {dev_dbg(&adapter->dev, "found normal entry for adapter %d, ""addr 0x%02x\n", adap_id, address_list[i]);temp_client->addr = address_list[i];err = i2c_detect_address(temp_client, driver);if (unlikely(err))break;}kfree(temp_client);return err;
}

这段代码是一个用于检测I2C适配器上连接的设备的函数。下面是对代码的详细解释:

  • i2c_adapter_id(adapter)用于获取适配器的ID,并将其赋值给adap_id变量。

  • 如果adapterclassdriverclass不匹配,则返回0,表示不进行设备检测。

  • 创建一个临时的i2c_client结构体对象temp_client,并将其分配到内存中。

  • 使用一个循环遍历address_list数组中的地址,直到遇到I2C_CLIENT_END为止。在循环中,打印适配器的ID和当前地址,然后将当前地址赋值给temp_clientaddr成员。

  • 调用i2c_detect_address函数来检测指定地址上是否存在设备。

I2C dev

i2c_dev_init

i2c_dev_init执行了一系列操作,包括注册字符设备、创建设备类、注册总线通知器以及绑定已经存在的适配器。它在初始化过程中处理了可能发生的错误,并返回相应的错误码。

static int __init i2c_dev_init(void)
{int res;printk(KERN_INFO "i2c /dev entries driver\n");res = register_chrdev(I2C_MAJOR, "i2c", &i2cdev_fops);if (res)goto out;i2c_dev_class = class_create(THIS_MODULE, "i2c-dev");if (IS_ERR(i2c_dev_class)) {res = PTR_ERR(i2c_dev_class);goto out_unreg_chrdev;}i2c_dev_class->dev_groups = i2c_groups;/* Keep track of adapters which will be added or removed later */res = bus_register_notifier(&i2c_bus_type, &i2cdev_notifier);if (res)goto out_unreg_class;/* Bind to already existing adapters right away */i2c_for_each_dev(NULL, i2cdev_attach_adapter);return 0;out_unreg_class:class_destroy(i2c_dev_class);
out_unreg_chrdev:unregister_chrdev(I2C_MAJOR, "i2c");
out:printk(KERN_ERR "%s: Driver Initialisation failed\n", __FILE__);return res;
}
  • 调用register_chrdev函数注册字符设备,使用I2C_MAJOR作为主设备号,"i2c"作为设备名称,&i2cdev_fops是指向字符设备操作函数的指针。如果注册失败,跳转到out标签处。

  • 使用class_create函数创建一个设备类对象i2c_dev_class,"i2c-dev"作为类名称。如果创建失败,跳转到out_unreg_chrdev标签处。

  • 使用bus_register_notifier函数注册总线通知器,将i2cdev_notifier作为通知回调函数,以便跟踪稍后要添加或删除的适配器。如果注册失败,跳转到out_unreg_class标签处。

  • 调用i2c_for_each_dev函数来绑定已经存在的适配器。该函数使用i2cdev_attach_adapter作为回调函数,在设备上执行绑定操作。

  • 如果有任何错误发生,跳转到out_unreg_classout_unreg_chrdevout标签处执行相应的清理操作。

i2cdev_attach_adapter

i2cdev_attach_adapter作用是将I2C适配器注册到Linux内核中,以便在系统中使用I2C总线。它会获取一个空闲的struct i2c_dev结构体,然后使用device_create函数创建一个I2C设备,并将其与驱动核心相关联。

static int i2cdev_attach_adapter(struct device *dev, void *dummy)
{struct i2c_adapter *adap;struct i2c_dev *i2c_dev;int res;if (dev->type != &i2c_adapter_type)return 0;adap = to_i2c_adapter(dev);i2c_dev = get_free_i2c_dev(adap);if (IS_ERR(i2c_dev))return PTR_ERR(i2c_dev);/* register this i2c device with the driver core */i2c_dev->dev = device_create(i2c_dev_class, &adap->dev,MKDEV(I2C_MAJOR, adap->nr), NULL,"i2c-%d", adap->nr);if (IS_ERR(i2c_dev->dev)) {res = PTR_ERR(i2c_dev->dev);goto error;}pr_debug("i2c-dev: adapter [%s] registered as minor %d\n",adap->name, adap->nr);return 0;
error:return_i2c_dev(i2c_dev);return res;
}
  • 检查传入的设备是否为I2C适配器类型。如果不是,则直接返回0。

  • 调用get_free_i2c_dev函数获取一个空闲的i2c_dev结构体,并将其赋值给i2c_dev变量。

  • device_create创建一个I2C设备,并将其与驱动核心相关联。设备的主设备号为I2C_MAJOR,次设备号为adap->nr,设备名为i2c-%d,其中%d会被替换为适配器的编号。

  • 检查设备创建过程是否出错。如果出错,则将错误码赋值给res变量,并跳转到error标签处进行错误处理。

i2cdev_open

i2cdev_open通过次设备号获取对应的i2c_dev结构体和适配器,然后分配并初始化一个i2c_client结构体,最后将其赋值给文件的私有数据。

static int i2cdev_open(struct inode *inode, struct file *file)
{unsigned int minor = iminor(inode);struct i2c_client *client;struct i2c_adapter *adap;struct i2c_dev *i2c_dev;i2c_dev = i2c_dev_get_by_minor(minor);if (!i2c_dev)return -ENODEV;adap = i2c_get_adapter(i2c_dev->adap->nr);if (!adap)return -ENODEV;/* This creates an anonymous i2c_client, which may later be* pointed to some address using I2C_SLAVE or I2C_SLAVE_FORCE.** This client is ** NEVER REGISTERED ** with the driver model* or I2C core code!!  It just holds private copies of addressing* information and maybe a PEC flag.*/client = kzalloc(sizeof(*client), GFP_KERNEL);if (!client) {i2c_put_adapter(adap);return -ENOMEM;}snprintf(client->name, I2C_NAME_SIZE, "i2c-dev %d", adap->nr);client->adapter = adap;file->private_data = client;return 0;
}
  • 通过次设备号获取相应的i2c_dev结构体,该结构体表示I2C设备。
  • 通过适配器编号获取相应的i2c_adapter结构体,该结构体表示I2C适配器。
  • 使用kzalloc函数分配一块内存,大小为sizeof(*client),用于存储i2c_client结构体的信息。
  • 使用snprintf函数将适配器编号格式化为字符串,存储在client->name中。
  • i2c_adapter结构体赋值给i2c_client结构体的adapter成员变量。将client指针赋值给文件的私有数据,以便在后续的文件操作中使用。

i2cdev_write

i2cdev_write函数将用户空间的数据复制到内核空间,并使用i2c_master_send函数将数据发送到之前打开的I2C设备中。

static ssize_t i2cdev_write(struct file *file, const char __user *buf,size_t count, loff_t *offset)
{int ret;char *tmp;struct i2c_client *client = file->private_data;if (count > 8192)count = 8192;tmp = memdup_user(buf, count);if (IS_ERR(tmp))return PTR_ERR(tmp);pr_debug("i2c-dev: i2c-%d writing %zu bytes.\n",iminor(file_inode(file)), count);ret = i2c_master_send(client, tmp, count);kfree(tmp);return ret;
}
  • 首先从文件的私有数据中获取之前打开的I2C设备的i2c_client结构体指针。

  • 限制写入的数据长度不超过8192字节,如果超过了则将其截断为8192字节。

  • 使用memdup_user函数将用户空间的buf中的数据复制到内核空间,并将复制后的数据的指针赋值给tmp

  • 使用i2c_master_send函数将数据发送到I2C设备,返回值存储在ret中。

i2cdev_read

i2cdev_read函数在内核中分配一个缓冲区,使用i2c_master_recv函数从I2C设备中接收数据,并将接收到的数据复制到用户空间。

static ssize_t i2cdev_read(struct file *file, char __user *buf, size_t count,loff_t *offset)
{char *tmp;int ret;struct i2c_client *client = file->private_data;if (count > 8192)count = 8192;tmp = kmalloc(count, GFP_KERNEL);if (tmp == NULL)return -ENOMEM;pr_debug("i2c-dev: i2c-%d reading %zu bytes.\n",iminor(file_inode(file)), count);ret = i2c_master_recv(client, tmp, count);if (ret >= 0)ret = copy_to_user(buf, tmp, count) ? -EFAULT : ret;kfree(tmp);return ret;
}
  • 从文件的私有数据中获取之前打开的I2C设备的i2c_client结构体指针。

  • 限制读取的数据长度不超过8192字节,如果超过了则将其截断为8192字节。

  • 在内核中分配一个大小为count字节的缓冲区,用于存储从I2C设备读取的数据。

  • 使用i2c_master_recv函数从I2C设备中接收数据,返回值存储在ret中。

  • 如果数据接收成功,使用copy_to_user将数据从内核空间复制到用户空间的buf中。

i2cdev_ioctl

static long i2cdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{struct i2c_client *client = file->private_data;unsigned long funcs;dev_dbg(&client->adapter->dev, "ioctl, cmd=0x%02x, arg=0x%02lx\n",cmd, arg);switch (cmd) {case I2C_SLAVE:case I2C_SLAVE_FORCE:if ((arg > 0x3ff) ||(((client->flags & I2C_M_TEN) == 0) && arg > 0x7f))return -EINVAL;if (cmd == I2C_SLAVE && i2cdev_check_addr(client->adapter, arg))return -EBUSY;/* REVISIT: address could become busy later */client->addr = arg;return 0;case I2C_TENBIT:if (arg)client->flags |= I2C_M_TEN;elseclient->flags &= ~I2C_M_TEN;return 0;case I2C_PEC:/** Setting the PEC flag here won't affect kernel drivers,* which will be using the i2c_client node registered with* the driver model core.  Likewise, when that client has* the PEC flag already set, the i2c-dev driver won't see* (or use) this setting.*/if (arg)client->flags |= I2C_CLIENT_PEC;elseclient->flags &= ~I2C_CLIENT_PEC;return 0;case I2C_FUNCS:funcs = i2c_get_functionality(client->adapter);return put_user(funcs, (unsigned long __user *)arg);case I2C_RDWR:return i2cdev_ioctl_rdwr(client, arg);case I2C_SMBUS:return i2cdev_ioctl_smbus(client, arg);case I2C_RETRIES:if (arg > INT_MAX)return -EINVAL;client->adapter->retries = arg;break;case I2C_TIMEOUT:if (arg > INT_MAX)return -EINVAL;/* For historical reasons, user-space sets the timeout* value in units of 10 ms.*/client->adapter->timeout = msecs_to_jiffies(arg * 10);break;default:/* NOTE:  returning a fault code here could cause trouble* in buggy userspace code.  Some old kernel bugs returned* zero in this case, and userspace code might accidentally* have depended on that bug.*/return -ENOTTY;}return 0;
}
  • I2C_SLAVEI2C_SLAVE_FORCE:设置I2C设备的从设备地址。首先检查地址是否合法,如果地址超出范围或者设备不支持10位地址且地址超过7位,则返回错误码-EINVAL。如果是I2C_SLAVE命令并且地址已经被占用,则返回错误码-EBUSY。将client->addr设置为参数arg的值,表示从设备地址已经设置成功。
  • I2C_TENBIT:设置I2C设备是否使用10位地址。如果参数arg为非零值,则设置client->flagsI2C_M_TEN标志位,表示使用10位地址。否则,清除该标志位,表示使用7位地址。
  • I2C_PEC:设置I2C设备是否启动PEC(奇偶校验)。如果参数arg为非零值,则设置client->flagsI2C_CLIENT_PEC标志位,表示启用PEC。否则,清除该标志位,表示禁用PEC。
  • I2C_FUNCS:获取I2C设备支持的功能并将其值存储在funcs变量中。然后使用put_user函数将funcs的值复制到用户空间的arg指定的地址上,并返回操作结果。
  • I2C_RDWR:调用i2cdev_ioctl_rdwr函数处理I2C读写操作。
  • I2C_SMBUS:调用i2cdev_ioctl_smbus函数处理I2C SMBus操作。
  • I2C_RETRIES:设置I2C总线的重试次数。首先检查参数arg是否超过了整型的最大值,如果超过则返回错误码-EINVAL。然后将client->adapter->retries设置为参数arg的值,表示重试次数已经设置成功。
  • I2C_TIMEOUT:设置I2C总线的超时时间。首先检查参数arg是否超过了整型的最大值,如果超过则返回错误码-EINVAL。然后将client->adapter->timeout设置为参数arg乘以10得到的值(以毫秒为单位),表示超时时间已经设置成功。

i2c_driver

i2c_register_driver

i2c_register_driver将驱动程序注册到I2C驱动核心,并在注册完成后处理所有已经存在的适配器。注册完成后,驱动核心会调用probe()函数来匹配并初始化所有匹配的但未绑定的设备。

int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
{int res;/* Can't register until after driver model init */if (unlikely(WARN_ON(!i2c_bus_type.p)))return -EAGAIN;/* add the driver to the list of i2c drivers in the driver core */driver->driver.owner = owner;driver->driver.bus = &i2c_bus_type;INIT_LIST_HEAD(&driver->clients);/* When registration returns, the driver core* will have called probe() for all matching-but-unbound devices.*/res = driver_register(&driver->driver);if (res)return res;pr_debug("i2c-core: driver [%s] registered\n", driver->driver.name);/* Walk the adapters that are already present */i2c_for_each_dev(driver, __process_new_driver);return 0;
  • 检查是否可以在驱动模型初始化之后进行注册。如果驱动总线类型i2c_bus_type未初始化(i2c_bus_type.p为空),则打印警告信息,并返回错误码-EAGAIN

  • 将驱动程序的所有者(owner)设置为参数owner指定的模块,并将驱动程序的总线(bus)设置为&i2c_bus_type,表示使用I2C总线。

  • 初始化驱动程序的clients链表头。

  • 调用driver_register函数将驱动程序注册到驱动核心(driver core)。注册完成后,驱动核心会为所有匹配但未绑定的设备调用probe()函数。

  • 遍历已经存在的适配器(adapters),对每个适配器调用__process_new_driver函数进行处理。

I2C 传输

i2c_transfer

i2c_transfer用于执行I2C传输操作。它首先检查是否支持主控制器,如果支持,则打印调试信息,尝试对适配器进行锁定,然后调用__i2c_transfer函数执行传输操作,并在完成后解锁适配器并返回传输的结果。如果不支持主控制器,则返回不支持的错误码。

int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{int ret;/* REVISIT the fault reporting model here is weak:**  - When we get an error after receiving N bytes from a slave,*    there is no way to report "N".**  - When we get a NAK after transmitting N bytes to a slave,*    there is no way to report "N" ... or to let the master*    continue executing the rest of this combined message, if*    that's the appropriate response.**  - When for example "num" is two and we successfully complete*    the first message but get an error part way through the*    second, it's unclear whether that should be reported as*    one (discarding status on the second message) or errno*    (discarding status on the first one).*/if (adap->algo->master_xfer) {
#ifdef DEBUGfor (ret = 0; ret < num; ret++) {dev_dbg(&adap->dev, "master_xfer[%d] %c, addr=0x%02x, ""len=%d%s\n", ret, (msgs[ret].flags & I2C_M_RD)? 'R' : 'W', msgs[ret].addr, msgs[ret].len,(msgs[ret].flags & I2C_M_RECV_LEN) ? "+" : "");}
#endifif (in_atomic() || irqs_disabled()) {ret = i2c_trylock_adapter(adap);if (!ret)/* I2C activity is ongoing. */return -EAGAIN;} else {i2c_lock_adapter(adap);}ret = __i2c_transfer(adap, msgs, num);i2c_unlock_adapter(adap);return ret;} else {dev_dbg(&adap->dev, "I2C level transfers not supported\n");return -EOPNOTSUPP;}
}
  • 检查是否支持master_xfer函数指针,即I2C适配器的主控制器(master controller)是否存在。

  • 如果支持主控制器,则打印调试信息,显示每个消息的属性,如读写方向、地址、长度等。

  • 如果当前处于原子操作或中断被禁止的情况下,尝试对适配器进行非阻塞锁定。如果锁定失败,表示I2C活动正在进行中,函数返回错误码-EAGAIN

  • 如果不处于原子操作或中断被禁止的情况下,对适配器进行阻塞锁定。

  • 调用__i2c_transfer函数执行实际的I2C传输。

  • 解锁适配器,返回传输的结果。

  • 如果不支持主控制器,则打印调试信息,表示不支持I2C级别的传输。

i2c_master_send

i2c_master_send通过I2C主控制器向从设备发送数据。它构建一个i2c_msg结构,设置消息的地址、标志、长度和缓冲区,并将其传递给i2c_transfer函数执行实际的传输操作。函数的返回值是发送的字节数或错误码,用于指示传输是否成功。

int i2c_master_send(const struct i2c_client *client, const char *buf, int count)
{int ret;struct i2c_adapter *adap = client->adapter;struct i2c_msg msg;msg.addr = client->addr;msg.flags = client->flags & I2C_M_TEN;msg.len = count;msg.buf = (char *)buf;ret = i2c_transfer(adap, &msg, 1);/** If everything went ok (i.e. 1 msg transmitted), return #bytes* transmitted, else error code.*/return (ret == 1) ? count : ret;
}
  • client结构中获取I2C适配器指针adap

  • 定义i2c_msg结构变量msg,并设置其成员变量:

    • addr:设置为从设备的地址。

    • flags:设置为client结构中的flags成员与I2C_M_TEN按位与的结果。

    • len:设置为要发送的数据字节数count

    • buf:设置为要发送的数据缓冲区指针buf

  • 调用i2c_transfer函数执行I2C传输,将适配器指针和msg结构的地址作为参数传递。

  • 根据返回值判断传输是否成功:

  • 如果返回值为1,表示成功传输了1个消息,返回发送的字节数count

  • 否则,返回错误码。

i2c_master_recv

i2c_master_recv通过I2C主控制器从从设备接收数据。它构建一个i2c_msg结构,设置消息的地址、标志、长度和缓冲区,并将其传递给i2c_transfer函数执行实际的传输操作。函数的返回值是接收的字节数或错误码,用于指示传输是否成功。

int i2c_master_recv(const struct i2c_client *client, char *buf, int count)
{struct i2c_adapter *adap = client->adapter;struct i2c_msg msg;int ret;msg.addr = client->addr;msg.flags = client->flags & I2C_M_TEN;msg.flags |= I2C_M_RD;msg.len = count;msg.buf = buf;ret = i2c_transfer(adap, &msg, 1);/** If everything went ok (i.e. 1 msg received), return #bytes received,* else error code.*/return (ret == 1) ? count : ret;
}
  • client结构中获取I2C适配器指针adap

  • 定义i2c_msg结构变量msg,并设置其成员变量:

    • addr:设置为从设备的地址。

    • flags:设置为client结构中的flags成员与I2C_M_TEN按位与的结果,表示读取数据。

    • flags还通过按位或操作符|=设置为I2C_M_RD,以指示读取操作。

    • len:设置为要接收的数据字节数count

    • buf:设置为用于接收数据的缓冲区指针buf

  • 调用i2c_transfer函数执行I2C传输,将适配器指针和msg结构的地址作为参数传递。

  • 根据返回值判断传输是否成功:返回值是接收的字节数或错误码,用于指示传输是否成功。

本文参考

https://blog.csdn.net/qq_45172832/article/details/131221971

https://hello2mao.github.io/2015/12/02/Linux_I2C_driver/

https://blog.csdn.net/m0_46577050/article/details/122315569

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/632720.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PBR材质纹理下载

03:10 按照视频里的顺序 我们从第6个网站开始倒数 点击本行文字或下方链接 进入查看 6大网站地址 网址查看链接&#xff1a; http://www.uzing.net/community_show-1962-48-48-35.html 06 Tectures Wood Fence 001 | 3D TEXTURES 简介&#xff1a;最大的纹理网站之一&#x…

机器学习:BootStrapping(Python)

import numpy as np import pandas as pd from sklearn.neighbors import KNeighborsClassifier from sklearn.decomposition import PCA # 主成分分析 from sklearn.preprocessing import LabelEncoder, StandardScaler # 类别标签编码&#xff0c;标准化处理 import matplo…

推荐算法常见的评估指标

推荐算法评估指标比较复杂&#xff0c;可以分为离线和在线两部分。召回、粗排、精排和重排由于定位区别&#xff0c;其评估指标也会有一定区别&#xff0c;下面详细讲解。 1 召回评价体系 召回结果并不是最终推荐结果&#xff0c;其本质是为后续排序层服务的&#xff0c;故核…

中国联通助力吴江元荡生态岸线打造5G+自动驾驶生态长廊

吴江&#xff0c;素有“鱼米之乡”“丝绸之府”的美誉&#xff0c;其地理位置优越&#xff0c;地处太湖之滨。近年来&#xff0c;随着长三角生态绿色一体化发展示范区&#xff08;以下简称“示范区”&#xff09;的建立&#xff0c;元荡更是声名大噪&#xff0c;成为众多游客心…

GNU Radio简介及流程图搭建

文章目录 前言一、GNU Radio 是什么&#xff1f;二、GNU Radio 安装三、搭建第一个流程图1、创建 GRC 文件2、添加块3、运行流程图 前言 欢迎来到无线通信的世界&#xff0c;初步接触 GNU Radio&#xff0c;对其学习进行一个记录。 一、GNU Radio 是什么&#xff1f; GNU Rad…

el-tree获取当前选中节点及其所有父节点的id(包含半选中父节点的id)

如下图,我们现在全勾中的有表格管理及其下的子级,而半勾中的有工作台和任务管理及其子级 现在点击保存按钮后,需要将勾中的节点id及该节点对应的父节点,祖先节点的id(包含半选中父节点的id)也都一并传给后端,那这个例子里就应该共传入9个id,我们可以直接将getCheckedK…

SpringMVC 拦截器

文章目录 1、拦截器的配置2、拦截器的三个抽象方法3、多个拦截器的执行顺序总结 Spring MVC 拦截器是Spring框架中的一种机制&#xff0c;用于在请求到达处理器之前和渲染视图之前拦截请求&#xff0c;并允许开发者在这两个时间点进行自定义的处理逻辑。拦截器与过滤器&#xf…

openlayers [七] 地图控件controls详解

文章目录 controls 简述常见的 controls 控件使用方法fullscreen 全屏mouseposition 鼠标位置控件overviewmap 鹰眼图scaleline 比例尺控件zoomslider 缩放滑块刻度控件 总结 controls 简述 上篇文章我们将了在地图上的交互(interaction)&#xff0c;那些都是一些隐性的需要去…

css3轮播图案例

轮播图案例 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title><style>…

数据结构——二叉树的遍历与应用

目录 一.前言 二. 二叉树链式结构的实现 2.1 前置说明 2.2 二叉树的遍历 2.2.1 前序、中序以及后序遍历 前序遍历&#xff1a; 中序遍历递归图&#xff1a; 后序遍历&#xff1a; 2.3节点个数 2.4叶子节点个数 2.5第K层的节点个数 2.6 二叉树查找值为x的节点 2.7 …

【计算机网络】子网划分(经典基础练习题)

一、某主机IP地址为110.35.2.68&#xff0c;子网掩码为255.255.255.128&#xff0c;求网络地址&#xff1f; 二、有A类网络18.0.0.0的子网掩码为255.128.0.0&#xff0c;请问可以划分为多少个子网&#xff1f; 并写出每个子网的子网号&#xff1f; 三、将C类网119.181.25.0划分…

.NetCore Flurl.Http 4.0.0 以上管理客户端

参考原文地址&#xff1a;Managing Clients - Flurl 管理客户端 Flurl.Http 构建在堆栈之上System.Net.Http。如果您熟悉HttpClient&#xff0c;那么您可能听说过这个建议&#xff1a;不要为每个请求创建一个新客户端&#xff1b;重复使用它们&#xff0c;否则将面临后…

MySQL篇—性能压测工具mysqlslap介绍

☘️博主介绍☘️&#xff1a; ✨又是一天没白过&#xff0c;我是奈斯&#xff0c;DBA一名✨ ✌✌️擅长Oracle、MySQL、SQLserver、Linux&#xff0c;也在积极的扩展IT方向的其他知识面✌✌️ ❣️❣️❣️大佬们都喜欢静静的看文章&#xff0c;并且也会默默的点赞收藏加关注❣…

Docker 安装 MongoDb4

Docker 安装mongoDb 获取mongodb安装参考 获取mongodb 注意&#xff1a; WARNING: MongoDB 5.0 requires a CPU with AVX support, and your current system does not appear to have that! **hub官网&#xff08;需要梯子&#xff09;&#xff1a;**https://hub.docker.com/_…

WAF攻防相关知识点总结1--信息收集中的WAF触发及解决方案

什么是WAF WAF可以通过对Web应用程序的流量进行过滤和监控&#xff0c;识别并阻止潜在的安全威胁。WAF可以检测Web应用程序中的各种攻击&#xff0c;例如SQL注入、跨站点脚本攻击&#xff08;XSS&#xff09;、跨站请求伪造&#xff08;CSRF&#xff09;等&#xff0c;并采取相…

Node.js基础---fs文件系统 读取和写入

什么是nodejs? 脚本语言需要一个解析器才能运行&#xff0c;JavaScript是脚本语言&#xff0c;在不同的位置有不一样的解析器&#xff0c;如写入html的js语言&#xff0c;浏览器是它的解析器角色。而对于需要独立运行的JS&#xff0c;nodejs就是一个解析器。 每一种解析器都是…

详细介绍IP 地址、网络号和主机号、ABC三类、ip地址可分配问题、子网掩码、子网划分

1、 IP 地址: 网络之间互连的协议&#xff0c;是由4个字节(32位二进制)组成的逻辑上的地址。 将32位二进制进行分组&#xff0c;分成4组&#xff0c;每组8位(1个字节)。【ip地址通常使用十进制表示】ip地址分成四组之后&#xff0c;在逻辑上&#xff0c;分成网络号和主机号 2…

适合初学者的机器学习开源项目合集(已加入Github加速计划)

目录 开源项目合集[>> 机器学习路线图&#xff1a;mrdbourke/machine-learning-roadmap](https://gitcode.com/mrdbourke/machine-learning-roadmap)[>> 机器学习资源的汇总&#xff1a;johnmyleswhite/ML_for_Hackers](https://gitcode.com/johnmyleswhite/ML_for…

vue+elementUI el-select 中 没有加clearable出现一个或者多个×清除图标问题

1、现象&#xff1a;下方截图多清除图标了 2、在全局common.scss文件中加一个下方的全局样式noClear 3、在多清除图标的组件上层div加noClear样式 4、清除图标去除成功

Linux centos中find命令的多种用途:按照具体应用来详细说明find的用法举例

目录 一、find命令 二、find命令的语法 &#xff08;一&#xff09;语法格式 &#xff08;二&#xff09;选项 1、选项(option)介绍 2、控制符号链接的option 3、调试选项debugopts 4、优化选项 &#xff08;三&#xff09;表达式expression 1、选项options 2、测试…