IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 嵌入式 -> Linux内核:I2C设备驱动 -> 正文阅读

[嵌入式]Linux内核:I2C设备驱动

一、I2C协议

I2C和SMBus简介

? I2C(发音:I2C,在内核文档中写成I2C)是由Philips开发的协议。它是一种缓慢的两线协议(可变速度,最高可达400 kHz),具有高速扩展(3.4 MHz)。它提供了一种廉价的总线,用于连接需要不频繁或低带宽通信的多种设备。I2C广泛应用于嵌入式系统中。一些系统使用不符合品牌要求的变体,因此不标榜为I2C,而是使用不同的名称,例如TWI(两线接口),IIC。

最新的I2C官方规范是NXP Semiconductors发布的《I2C总线规范与用户手册》(UM10204)。但是,您需要登录到该站点才能访问PDF。此规范的旧版本(版本6)存档于此。

SMBus(系统管理总线)是基于I2C协议的,主要是I2C协议和信令的子集。许多I2C设备将在SMBus上工作,但是一些SMBus协议添加了超出实现I2C品牌所需的语义。现代PC主板依赖SMBus。通过SMBus连接的最常见设备是使用I2C eeprom配置的RAM模块和硬件监控芯片。

因为SMBus主要是广义I2C总线的一个子集,所以我们可以在许多I2C系统上使用它的协议。然而,有些系统不能同时满足SMBus和I2C的电气约束;以及其他不能实现所有通用SMBus协议语义或消息的组件。

术语

使用官方文档中的术语,I2C总线连接一个或多个主芯片和一个或多个从芯片。

在这里插入图片描述

? 简单的I2C总线

? 主芯片是启动与从芯片通信的节点。在Linux内核实现中,它被称为适配器或总线。适配器驱动程序在drivers/i2c/busses/子目录中。

? 一个算法包含通用代码,可用于实现整个I2C适配器类。每个特定的适配器驱动程序要么依赖于drivers/i2c/algos/子目录中的一个算法驱动程序,要么包含它自己的实现。

? 从芯片是当主芯片寻址时响应通信的节点。在Linux中,它被称为客户端。客户端驱动程序被保存在特定于它们提供的特性的目录中,例如用于gpio扩展器的驱动程序/media/gpio/和用于视频相关芯片的驱动程序/media/i2c/。

对于图中的示例配置,您需要I2C适配器的驱动程序和I2C设备的驱动程序(通常每个设备一个驱动程序)。

关键符号

S起始条件
P停止条件
Rd/Wr (1 bit)读/写。Rd等于1 Wr等于0。
A, NA (1 bit)确认(ACK)和不确认(NACK)位
Addr (7 bits)I2C 7位地址。注意,可以将其扩展为10位I2C地址。
Data (8 bits)纯数据字节。
[…]由I2C设备发送的数据,而不是由主机适配器发送的数据。

简单的发送传输

由i2c_master_send实现():

S Addr Wr [A] Data [A] Data [A]…A .数据[A] P

简单的接收传输

由i2c_master_recv实现():

S Addr Rd [A] [Data] A [Data] A…A[数据]NA P

合并传输

由i2c_transfer实现()。

它们就像上面的传输一样,但是发送的不是停止条件P,而是开始条件S,传输继续进行。一个读字节,然后写字节的例子:

S Addr Rd [A] [Data] NA S Addr Wr [A] Data [A] P

修改交互

通过为I2C消息设置这些标志,也可以对I2C协议进行以下修改。除了I2C_M_NOSTART,它们通常只需要解决设备问题:

I2C_M_IGNORE_NAK:

正常情况下,如果有[NA]来自客户端,消息会立即中断。设置该标志将任何[NA]视为[A],并发送所有消息。这些消息仍然可能在SCL lo->hi超时时失败。
I2C_M_NO_RD_ACK:

在已读消息中,跳过master a /NA位。
I2C_M_NOSTART:

在合并传输中,在某个点上没有生成“S Addr Wr/Rd [a]”。例如,在第二个部分消息上设置I2C_M_NOSTART会产生如下结果:

S Addr Rd [A] [Data] NA Data [A] P

如果您为第一个部分消息设置了I2C_M_NOSTART变量,我们不生成Addr,但是我们生成了启动条件s。这可能会混淆您总线上的所有其他客户端,所以不要尝试这样做。

这通常用于从系统内存中的多个数据缓冲区收集传输到I2C设备的单个传输,但也可以在一些罕见的设备的方向变化之间使用。
I2C_M_REV_DIR_ADDR:

这将切换Rd/Wr标志。也就是说,如果您想进行写入操作,但需要发出Rd而不是Wr,或者反之亦然,您可以设置此标志。例如:

数据[A]数据[A]…A .数据[A] P

I2C_M_STOP:

在消息之后强制一个停止条件§。一些I2C相关协议(如SCCB)需要这样做。通常,您不希望在一次传输的消息之间被中断。

如何实例化I2C设备

? 与PCI或USB设备不同,I2C设备不在硬件级别枚举。相反,软件必须知道每个I2C总线段上连接了哪些设备,以及这些设备使用的是什么地址。因此,内核代码必须显式地实例化I2C设备。有几种方法可以实现这一点,具体取决于上下文和需求。

方法1:静态声明I2C设备

? 这种方法适用于当I2C总线是一个系统总线时,就像许多嵌入式系统一样。在这样的系统中,每个I2C总线都有一个预先知道的数字。因此,可以预先声明总线上的I2C设备。

在不同的架构上,这些信息以不同的方式提供给内核:设备树、ACPI或板文件。

当I2C总线被注册后,I2C设备将被I2C -core自动实例化。当设备所在的I2C总线消失时(如果有的话),设备将自动解除绑定并销毁。

通过设备树声明I2C设备

? 在使用设备树的平台上,I2C设备的声明是在主控制器的子节点上完成的。

例如:

i2c1: i2c@400a0000 {
        /* ... master properties skipped ... */
        clock-frequency = <100000>;

        flash@50 {
                compatible = "atmel,24c256";
                reg = <0x50>;
        };

        pca9532: gpio@60 {
                compatible = "nxp,pca9532";
                gpio-controller;
                #gpio-cells = <2>;
                reg = <0x60>;
        };
};

? 在这里,两个设备以100kHz的速度连接到总线上。关于设置设备可能需要的其他属性,请参阅文档/设备树/bindings/中的设备树文档。

通过ACPI声明I2C设备

? ACPI也可以描述I2C设备。有关于这个的特殊文档,目前位于基于ACPI的设备枚举。

在板文件中声明I2C设备

? 在许多嵌入式架构中,设备树已经取代了基于板卡文件的硬件描述,但板卡文件在旧代码中仍然使用。通过board文件实例化I2C设备是通过调用i2c_register_board_info()来注册一个数组结构i2c_board_info来完成的。

例如(来自omap2h4):

static struct i2c_board_info h4_i2c_board_info[] __initdata = {
      {
              I2C_BOARD_INFO("isp1301_omap", 0x2d),
              .irq            = OMAP_GPIO_IRQ(125),
      },
      {       /* EEPROM on mainboard */
              I2C_BOARD_INFO("24c01", 0x52),
              .platform_data  = &m24c01,
      },
      {       /* EEPROM on cpu card */
              I2C_BOARD_INFO("24c01", 0x57),
              .platform_data  = &m24c01,
      },
};

static void __init omap_h4_init(void)
{
      (...)
      i2c_register_board_info(1, h4_i2c_board_info,
                      ARRAY_SIZE(h4_i2c_board_info));
      (...)
}

? 上面的代码在I2C总线1上声明了3个设备,包括它们各自的地址和它们的驱动程序所需的自定义数据。

方法2:显式实例化设备

? 这种方法适用于大型设备使用I2C总线进行内部通信的情况。典型的例子是电视适配器。它们可以有调谐器、视频解码器、音频解码器等,通常通过I2C总线连接到主芯片上。你不会提前知道I2C总线的编号,所以上面描述的方法1不能使用。相反,您可以显式地实例化I2C设备。这是通过填充结构i2c_board_info并调用i2c_new_client_device()来完成的。

示例(来自sfe4001网络驱动):

static struct i2c_board_info sfe4001_hwmon_info = {
      I2C_BOARD_INFO("max6647", 0x4e),
};

int sfe4001_init(struct efx_nic *efx)
{
      (...)
      efx->board_info.hwmon_client =
              i2c_new_client_device(&efx->i2c_adap, &sfe4001_hwmon_info);

      (...)
}

上面的代码实例化了网络适配器上的I2C总线上的一个I2C设备。

? 当你不确定一个I2C设备是否存在时(例如,一个可选的功能在一个廉价版本的电路板上不存在,但你没有办法区分它们),或者它可能在一个电路板到下一个电路板上有不同的地址(制造商在没有通知的情况下改变它的设计)。在这种情况下,可以调用i2c_new_scanned_device()而不是i2c_new_client_device()

示例(来自nxp OHCI驱动):

static const unsigned short normal_i2c[] = { 0x2c, 0x2d, I2C_CLIENT_END };

static int usb_hcd_nxp_probe(struct platform_device *pdev)
{
      (...)
      struct i2c_adapter *i2c_adap;
      struct i2c_board_info i2c_info;

      (...)
      i2c_adap = i2c_get_adapter(2);
      memset(&i2c_info, 0, sizeof(struct i2c_board_info));
      strscpy(i2c_info.type, "isp1301_nxp", sizeof(i2c_info.type));
      isp1301_i2c_client = i2c_new_scanned_device(i2c_adap, &i2c_info,
                                                  normal_i2c, NULL);
      i2c_put_adapter(i2c_adap);
      (...)
}

上面的代码实例化了在问题的OHCI适配器上的I2C总线上最多1个I2C设备。它首先尝试地址0x2c,如果在那里什么都没有找到,它尝试地址0x2d,如果仍然没有找到,它简单地放弃。

? 实例化I2C设备的驱动程序负责在清理过程中销毁它。这是通过对i2c_new_client_device()i2c_new_scanned_device()返回的指针调用i2c_unregister_device()来实现的。

方法3:探测特定设备的I2C总线

? 有时您没有关于I2C设备的足够信息,甚至不需要调用i2c_new_scanned_device()。典型的例子是PC主板上的硬件监控芯片。有几十种型号,可以居住在25个不同的地址。考虑到大量的主板在那里,它是几乎不可能建立一个详尽的清单的硬件监控芯片正在使用。幸运的是,大多数芯片都有制造商和设备ID寄存器,因此可以通过探测来识别它们。

? 在这种情况下,I2C设备既没有声明也没有显式实例化。相反,I2C -core将在加载这些设备的驱动程序后立即探测它们,如果找到任何驱动程序,I2C设备将自动实例化。为了防止该机制的任何不当行为,适用以下限制:

? I2C设备驱动程序必须实现detect()方法,该方法通过从任意寄存器读取来识别受支持的设备。

? 只有可能有支持的设备并同意被探测的总线才会被探测。例如,这避免了探测电视适配器上的硬件监控芯片。

? 示例:请参见“drivers/hwmon/lm90.c”文件中的“lm90_driver”和“lm90_detect()”

? 探测成功后实例化的I2C设备将在检测到它们的驱动程序被移除时自动销毁,或者底层I2C总线本身被销毁时自动销毁,无论哪种情况先发生。

熟悉2.4内核和早期2.6内核的I2C子系统的人会发现,这种方法3在本质上与那里所做的类似。两个显著的差异是:

探测只是现在实例化I2C设备的一种方法,而这在当时是唯一的方法。在可能的情况下,应首选方法1和2。方法3只能在没有其他方法的情况下使用,因为它可能会产生不良的副作用。

I2C总线现在必须显式地说明哪些I2C驱动程序类可以探测它们(通过类位域的方式),而当时所有I2C总线都是默认探测的。默认值是一个空类,这意味着没有探测发生。类位字段的目的是限制前面提到的不良副作用。

同样,应该尽可能避免使用方法3。显式设备实例化(方法1和2)更受欢迎,因为它更安全、更快。

方法4:从用户空间实例化

? 通常,内核应该知道连接了哪些I2C设备以及它们所在的地址。但是,在某些情况下却不是这样,因此添加了一个sysfs接口来让用户提供信息。该接口由在每个I2C总线目录下创建的两个属性文件new_devicedelete_device组成。这两个文件都是只写的,您必须向它们写入正确的参数,以便正确地实例化(分别删除)I2C设备。

文件new_device有两个参数:I2C设备的名称(一个字符串)和I2C设备的地址(一个数字,通常以0x开头的十六进制表示,但也可以用十进制表示)。

文件delete_device只有一个参数:I2C设备的地址。由于在一个给定的I2C网段上,不能有两个设备位于同一个地址上,所以这个地址足以唯一地标识要删除的设备。

例子:

#echo eeprom 0x50 > /sys/bus/i2c/devices/i2c-3/new_device

? 虽然这个接口只应该在无法进行内核内设备声明时使用,但在很多情况下它都是有用的:

I2C驱动程序通常检测设备(上面的方法3),但是设备所在的总线段没有设置正确的类位,因此检测不会触发。

I2C驱动程序通常检测设备,但您的设备位于一个意外的地址。

I2C驱动程序通常检测设备,但你的设备没有被检测到,要么是因为检测程序太严格,要么是因为你的设备还没有被官方支持,但你知道它是兼容的。

您正在一个测试板上开发一个驱动程序,其中您自己焊接了I2C设备。

这个接口是对某些I2C驱动实现的force_*模块参数的替换。它是在i2c-core中实现的,而不是单独在每个设备驱动程序中实现的,它的效率要高得多,而且它的优点是不需要重新加载驱动程序来更改设置。你也可以在驱动程序加载或可用之前实例化设备,你不需要知道设备需要什么驱动程序。

二、I2C重要的驱动接口

1. i2c_driver

I2C设备驱动结构

struct i2c_driver {
    /* 我们实例化哪种i2c设备(用于检测) */
	unsigned int class;

	/* 添加总线的回调函数(已弃用)。通知驱动程序出现了新的总线。你应该避免使用这个,它将在不久的将来被删除。*/
	int (*attach_adapter)(struct i2c_adapter *) __deprecated;

	/* 设备绑定/解绑时的回调函数,标准驱动模型接口 */
	int (*probe)(struct i2c_client *, const struct i2c_device_id *);
	int (*remove)(struct i2c_client *);

	/* 设备关闭的回调函数,与枚举无关的驱动程序模型接口 */
	void (*shutdown)(struct i2c_client *);

	/* 警报回调函数,例如SMBus警报协议。数据值的格式和含义取决于协议。对于SMBus警报
	 * 协议,有一个数据位作为警报响应的低位(“事件标志”)传递。*/
	void (*alert)(struct i2c_client *, unsigned int data);

	/* 总线范围信令的回调函数(可选):一个类似ioctl的命令,可用于执行设备的特定功能。*/
	int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);

    /* 设备驱动模型驱动 */
	struct device_driver driver;
    
    /* 此驱动程序支持的I2C设备列表 */
	const struct i2c_device_id *id_table;

	/* 用于自动创建设备的设备检测回调函数 */
	int (*detect)(struct i2c_client *, struct i2c_board_info *);
    
    /* 要探测的I2C地址(用于检测) */
	const unsigned short *address_list;
    
    /* 我们创建的检测到的客户机列表(仅用于i2c内核) */
	struct list_head clients;
};

driver.owner字段应该设置为该驱动程序的模块所有者。driver.name字段应该设置为该驱动程序的名称。

? 对于自动设备检测,必须同时定义@detect和@address_list。@class也应该设置,否则只有强制使用模块参数的设备才会被创建。detect函数必须至少填充成功检测时传递给它的i2c_board_info结构的name字段,也可能是flags字段。

? 如果@detect缺失,驱动程序仍然可以在枚举设备上正常工作。检测到的设备将不受支持。对于许多无法可靠检测的I2C/SMBus设备,以及在实践中总能列举出来的设备,这都是意料之中的。

传递给@detect回调的i2c_client结构并不是一个真正的i2c_client。它被初始化了,这样您就可以调用i2c_smbus_read_byte_data和它的友元函数。不要用它做任何其他事情。特别是,不允许在dev_dbg上调用dev_dbg和friends。

2. i2c_client :

? I2C从设备结构,i2c_client用于标识连接到i2c总线的单个设备(即芯片,如:AP3216C)。暴露在Linux中的行为是由管理设备的驱动程序定义的。

struct i2c_client {
    /* 标志,I2C_CLIENT_TEN表示设备使用10位芯片地址; I2C_CLIENT_PEC表示
     * 使用SMBus Packet Error Checking 
     */
	unsigned short flags;
    
    /* 连接到父适配器的I2C总线上使用的地址。 */
	unsigned short addr;
    /* 表示设备的类型,通常是一个通用的芯片名称,足以隐藏second-sourcing和compatible版本 */
	char name[I2C_NAME_SIZE];
    
    /* 管理承载此I2C设备的总线段 */
	struct i2c_adapter *adapter;
    
    /* 驱动模型设备节点为从属设备 */
	struct device dev;				/* the device structure		*/
	int irq;						/* irq issued by device		*/
	/* i2c_driver的成员。客户端列表或i2c-core的userspace_devices列表 */
    struct list_head detected;
	// ...
};

? i2c_driver对应一套驱动方法,其主要函数是attach_adapter()和detach_client(),i2c_client对应真实的i2c物理设备device,每个i2c设备都需要一个i2c_client来描述,i2c_driver与i2c_client的关系是一对多。一个i2c_driver上可以支持多个同等类型的i2c_client.

3. i2c_adapter

i2c_adapter是用于标识物理i2c总线以及访问它所需的访问算法的结构。

struct i2c_adapter {
	struct module *owner;
	unsigned int class;		  /* classes to allow probing for */
	const struct i2c_algorithm *algo; /* the algorithm to access the bus */
	void *algo_data;

	/* data fields that are valid for all devices	*/
	struct rt_mutex bus_lock;

	int timeout;			/* in jiffies */
	int retries;
	struct device dev;		/* the adapter device */

	int nr;
	char name[48];
	struct completion dev_released;

	struct mutex userspace_clients_lock;
	struct list_head userspace_clients;

	struct i2c_bus_recovery_info *bus_recovery_info;
	const struct i2c_adapter_quirks *quirks;
};

? i2c_adapter和i2c_client的关系与i2c硬件体系中适配器和设备的关系一致,即i2c_client依附于i2c_adapter,由于一个适配器上可以连接多个i2c设备,所以i2c_adapter中包含依附于它的i2c_client的链表。

4. i2c_algorithm

? i2c_algorithm是I2C传输方法的接口,可以使用相同的总线算法来解决,例如位碰撞或PCF8584,这是最常见的两种算法。@master_xfer字段的返回码应该表明在传输过程中发生的错误码的类型,这在内核文档文件Documentation/i2c/fault-codes中有记录。

struct i2c_algorithm {
	/* 如果适配器算法不能进行i2c级访问,请将master_xfer设置为NULL。
	 * 如果dapter算法可以进行SMBus访问,则设置smbus_xfer。
	 * 如果设置为NULL,则使用普通I2C消息模拟SMBus协议 
	 */
	/* master_xfer应该返回成功处理的消息数,或者在出错时返回负值 */
	int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,
			   int num);
	int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,
			   unsigned short flags, char read_write,
			   u8 command, int size, union i2c_smbus_data *data);

	/* To determine what the adapter supports */
	u32 (*functionality) (struct i2c_adapter *);

#if IS_ENABLED(CONFIG_I2C_SLAVE)
	int (*reg_slave)(struct i2c_client *client);
	int (*unreg_slave)(struct i2c_client *client);
#endif
};

? i2c_adapter对应与物理上的一个适配器,而i2c_algorithm对应一套通信方法,一个i2c适配器需要i2c_algorithm中提供的(i2c_algorithm中的又是更下层与硬件相关的代码提供)通信函数来控制适配器上产生特定的访问周期。缺少i2c_algorithm的i2c_adapter什么也做不了,因此i2c_adapter中包含其使用i2c_algorithm的指针。

? i2c_algorithm中的关键函数master_xfer()用于产生i2c访问周期需要的start stop ack信号,以i2c_msg(即i2c消息)为单位发送和接收通信数据。i2c_msg也非常关键,调用驱动中的发送接收函数需要填充该结构体。

5. i2c_msg

? i2c_msg是I2C传输的一个段的底层表示。它对@i2c_transfer()过程中的驱动程序、i2c-dev中的用户空间以及@i2c_adapter.@master_xfer()方法中的I2C适配器驱动程序都是可见的。

? 除了使用I2C“protocol mangling”之外,所有的I2C适配器都实现了I2C传输的标准规则。每个传输都从START开始。其后是从机地址,以及位编码读和写。然后跟着所有数据字节,可能包括一个带有SMBus PEC的字节。传输以一个NAK终止,或者当所有这些字节都被传输并被ACK后终止。如果这是组中的最后一条消息,则它后面会跟着STOP。否则,它后面是下一个@i2c_msg传输段,以(重复的)START开头。

? 另外,当适配器支持I2C_FUNC_PROTOCOL_MANGLING时,传递某些@flags可能会改变那些标准协议行为。这些标志只用于断开/不符合从机设备,以及已知支持它们需要的特定mangling选项的适配器(一个或多个IGNORE_NAK, NO_RD_ACK, NOSTART和REV_DIR_ADDR)。

struct i2c_msg {
	__u16 addr;	/* slave address			*/
	__u16 flags;
#define I2C_M_TEN		0x0010	/* this is a ten bit chip address */
#define I2C_M_RD		0x0001	/* read data, from slave to master */
#define I2C_M_STOP		0x8000	/* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NOSTART		0x4000	/* if I2C_FUNC_NOSTART */
#define I2C_M_REV_DIR_ADDR	0x2000	/* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK	0x1000	/* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NO_RD_ACK		0x0800	/* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_RECV_LEN		0x0400	/* length will be first received byte */
	__u16 len;		/* msg length				*/
	__u8 *buf;		/* pointer to msg data			*/
};

? 可以看出,linux内核对i2c架构抽象出了一个叫核心层core的中间件,它分离了设备驱动device driver和硬件控制的实现细节(如操作i2c的寄存器),core层不但为上面的设备驱动提供封装后的内核注册函数,而且还为下面的硬件实现提供注册接口(也就是i2c总线注册接口),可以说core层起到了承上启下的作用。

6. i2c_board_info

? 设备创建模板,i2c_board_info用于构建列出现有I2C设备的信息表。此信息用于生成驱动模型树。
对于主板,这是使用i2c_register_board_info()静态完成的; 总线号码标识了尚未可用的适配器。对于外接板,i2c_new_device()使用已知的适配器动态执行此操作。

struct i2c_board_info {
    /* 芯片类型,初始化i2c_client.name */
	char		type[I2C_NAME_SIZE];
    
    /* 芯片标志,初始化i2c_client.flags */
	unsigned short	flags;
    
    /* 芯片地址,初始化i2c_client.addr */
	unsigned short	addr;
    
    /* 保存在i2c_client.dev.platform_data */
	void		*platform_data;
    
    /* 复制到i2c_client.dev.archdata中 */
	struct dev_archdata	*archdata;
    
    /* 指针指向设备树设备节点 */
	struct device_node *of_node;
	struct fwnode_handle *fwnode; /* 平台固件提供的设备节点 */
	int		irq; /* 存储在i2c_client.irq中 */
};

? I2C实际上不支持硬件探测,尽管控制器和设备可能能够使用I2C_SMBUS_QUICK来判断给定地址是否有设备。驱动程序通常需要更多的信息,如芯片类型、配置、相关IRQ等。

三、I2C核心驱动调用流程

  1. i2c_init()函数主要工作有:创建./bus/devices./bus/drivers目录;初始化klist_devices设备链表和klist_drivers驱动链表;在/sys/class/目录下创建i2c-adapter兼容性子类目录(/sys/class/i2c-adapter);将i2c_driver空驱动挂接到klist_drivers驱动链表
static int __init i2c_init(void)
    /* 从aliases_look链表找出与i2c别名相同的节点,然后返回其序号 */
--> retval = of_alias_get_highest_id("i2c"); 
	--> list_for_each_entry(app, &aliases_lookup, link) 
        /* 【注1】【注2】等效并展开为下面的for循环 */
        for (app = list_head(&aliases_lookup, typeof(*app), link);
            &app->link != (&aliases_lookup); 
            app = list_next(app, link)) {
            if (strcmp(app->stem, stem) != 0)
                continue;

            if (app->id > id)
            	id = app->id;
        }
    /* 工作就是完成subsys_private的初始化.创建注册的这条总线需要的目录文件.
     * 在这条总线目录下创建/device  /driver 目录
     * 初始化这条总线上的设备链表:struct klist klist_devices;
     * 初始化这条总线上的驱动链表:struct klist klist_drivers; */
--> retval = bus_register(&i2c_bus_type);	/* 【注3】注册i2c驱动核心子系统 */
    --> /* 与上面i2c_bus_type的成员同类型,用来保存i2c_bus_type结构的驱动程序核心部分的私有部分 */
        struct subsys_private *priv;
		/* 为priv指针分配内核空间 */
        priv = kzalloc(sizeof(struct subsys_private), GFP_KERNEL);

		/* priv->bus指向i2c_bus_type */
        priv->bus = bus;
        bus->p = priv;    	/* i2c_bus_type->p = priv; p关联到priv */

        BLOCKING_INIT_NOTIFIER_HEAD(&priv->bus_notifier);
		/* kobject对应一个目录, 这个目录就是我们看到的总线名字/bus/i2c */

        retval = kobject_set_name(&priv->subsys.kobj, "%s", bus->name);		

		/* i2c目录在bus下, 即/bus/i2c:初始化kset的成员kobject,为kset_register()做准备. */
        priv->subsys.kobj.kset = bus_kset;

		/* static struct kobj_type bus_ktype = { .sysfs_ops = &bus_sysfs_ops, }; */
        priv->subsys.kobj.ktype = &bus_ktype;

		/* 设置该标志,当有driver注册时,会自动匹配devices上的设备并用probe初始化,当有device
		 * 注册时, 也同样找到driver并会初始化
		 * int bus_add_driver(struct device_driver *drv)
		 * 	   if (drv->bus->p->drivers_autoprobe) { 
		 *         error = driver_attach(drv); 
		 *     } */
        priv->drivers_autoprobe = 1;						
		
		/* 注册kset,创建目录结构,以及层次关系生成/bus/i2c */
		/* kset_register(&priv->subsys);
         *     kobject_add_internal(&k->kobj);
         *     error = create_dir(kobj); 
         *     error = sysfs_create_dir(kobj);
         *     &priv->subsys --> i2c_bus_type->p->subsys  		
         *     priv->subsys.kobj.kset = bus_kset; 
         */
        retval = kset_register(&priv->subsys);
		
		/* i2c目录下生成bus_attr_uevent属性文件 */
        retval = bus_create_file(bus, &bus_attr_uevent);			 

		/* 在i2c下面创建一个i2c/devices, 是i2c这条总线的devices的根目录. 为什么在i2c目录下?
		 * (device这个目录:kset->kobj) kset->kobj.parent = &priv->subsys.kobj; 在
		 * 某个目录下:kset->kobj.kset = x 这个目录在x目录下,这个目录下面“还可有”目录
		 * kobj.parent = y 这个目录在y目录下,这个目录下面“没有”有目录 */
        priv->devices_kset = kset_create_and_add("devices", NULL,		
                             &priv->subsys.kobj);		

		/* 在i2c下面创建一个i2c/drivers目录, 是i2c这条总线的driver的根目录. */
        priv->drivers_kset = kset_create_and_add("drivers", NULL,		
                             &priv->subsys.kobj);

		/* 初始化i2c_bus_type->p->klist_devices, 就是初始化device的list_head */
        klist_init(&priv->klist_devices, klist_devices_get, klist_devices_put);	

		/* 就是初始化driver的list_head device, driver注册都挂在对应的链表上. */
        klist_init(&priv->klist_drivers, NULL, NULL);				
        retval = add_probe_files(bus);		/* 创建文件 */

		/* 根据给定一个目录kobject(这里是i2c_bus_type->p->subsys.kobj),
		 * 创建一组属性组i2c_bus_type->bus_groups 
		 * 这个函数创建一系列属性组。如果在创建组时发生错误,则将删除以前创建的所有组,
		 * 并将所有组恢复到调用此函数时的原始状态。如果所创建的任何属性文件已经存在,
		 * 它将显式地发出警告和错误。*/
        retval = bus_add_groups(bus, bus->bus_groups);

    /* 在/sys/class/目录下创建i2c-adapter兼容性子类目录(/sys/class/i2c-adapter)
     * 兼容性类是指将类设备系列转换为总线设备时的临时用户空间兼容性工作区 */
--> i2c_adapter_compat_class = class_compat_register("i2c-adapter");
	--> struct class_compat *cls;
        cls = kmalloc(sizeof(struct class_compat), GFP_KERNEL);
        if (!cls)
            return NULL;
		/* 动态创建一个struct kobject并向sysfs注册, @name为kobject的名称(这里是i2c-adapter),
         * @parent: 此kobject的父kobject 这个函数动态地创建一个kobject结构,并向sysfs注册它。
         * 当您完成此结构时,调用kobject_put(),当不再使用该结构时,该结构将被动态释放。*/
        cls->kobj = kobject_create_and_add(name, &class_kset->kobj);

--> /* i2c_driver与一个或多个i2c_client(设备)节点一起使用,在与某个i2c_adapter相关联的
	 * 总线实例上访问i2c从芯片。*/
    retval = i2c_add_driver(&dummy_driver); /* i2c_register_driver(THIS_MODULE, driver) */
	--> /* 在驱动模型初始化之前不能注册 */
        if (unlikely(WARN_ON(!i2c_bus_type.p)))
            return -EAGAIN;

        /* 将驱动添加到驱动核心的i2c驱动列表中 */
        driver->driver.owner = owner;
        driver->driver.bus = &i2c_bus_type;

        /* 当注册返回时,驱动核心将调用所有匹配但未绑定的设备的probe()函数 */
        res = driver_register(&driver->driver);
        --> struct device_driver *other;
			/* 根据其名称查找总线上的驱动程序, 调用kset_find_obj()来遍历总线上的驱动列表,以按名称查找
			 * 驱动程序。如果找到驱动则返回。 */
            other = driver_find(drv->name, drv->bus);

			/* 向总线添加一个驱动程序 */
            ret = bus_add_driver(drv);

			/* 这个函数创建一系列属性组。如果在创建组时发生错误,则将删除以前创建的所有组,并将所有组恢复到
			 * 调用此函数时的原始状态。如果所创建的任何属性文件已经存在,它将显式地发出警告和错误。 */
            ret = driver_add_groups(drv, drv->groups);

            kobject_uevent(&drv->p->kobj, KOBJ_ADD);

        INIT_LIST_HEAD(&driver->clients);
        /* 遍历已经存在的适配器, __process_new_driver为函数指针 【注】4 */
        i2c_for_each_dev(driver, __process_new_driver);
		--> mutex_lock(&core_lock);
			/* 遍历@bus(这里是i2c_bus_type)的设备列表,并对每个设备调用对应的@fn(这里
             * 是__process_new_driver),传递给它@data (这里是driver)。 如果@start(这里是NULL)
             * 不为NULL,则使用该设备开始迭代。
             * 我们每次都检查@fn的返回。如果它返回的值不是0,我们就跳出并返回那个值。*/
            res = bus_for_each_dev(&i2c_bus_type, NULL, data, fn);
			--> struct klist_iter i;
                struct device *dev;
				klist_iter_init_node(&bus->p->klist_devices, &i,
                             (start ? &start->p->knode_bus : NULL));
                while ((dev = next_device(&i)) && !error)
                    error = fn(dev, data);
                klist_iter_exit(&i);
            mutex_unlock(&core_lock);
--> of_reconfig_notifier_register(&i2c_of_notifier); /* 【注】5 */
		/* 将通知添加到阻塞通知链表 */
    --> return blocking_notifier_chain_register(&of_reconfig_chain, nb);
			/* 此代码在启动期间使用,此时任务切换还不能工作,中断必须保持禁用。在这种情况下,
			 * 我们不能调用down_write()。 */
		--> if (unlikely(system_state == SYSTEM_BOOTING))
                return notifier_chain_register(&nh->head, n);

            down_write(&nh->rwsem);
			/* 通知器链核心程序。下面导出的程序在这些程序之上,并添加了适当的锁。 */
            ret = notifier_chain_register(&nh->head, n);
			--> while ((*nl) != NULL) {
                    if (n->priority > (*nl)->priority)
                        break;
                    nl = &((*nl)->next);
                }
                n->next = *nl;
                rcu_assign_pointer(*nl, n);
            up_write(&nh->rwsem);
i2c_init().【注1/* 该结构表示aliases节点的一个别名属性,作为aliases_lookup链表中的一个节点。 */
struct alias_prop {
    struct list_head link;
    const char *alias;
    struct device_node *np;
    int id;
    char stem[0];
};
struct alias_prop *app;

i2c_init().【注2struct list_head aliases_lookup;
/* aliases_lookup是双向链表,aliases节点都会加入到aliases_lookup */

i2c_init().【注3/* struct subsys_private结构体用来保存bus_type/class结构的驱动程序核心部分的私有部分。 */
struct subsys_private {
	struct kset subsys;    		/* 定义这个子系统的结构kset */
	struct kset *devices_kset; 	/* 子系统的“设备”目录 */
	struct list_head interfaces;/* 与之相关联的子系统接口列表 */
	struct mutex mutex;			/* 互斥量, 保护设备和接口列表 */

	struct kset *drivers_kset;	/* 子系统的“驱动”目录 */
	struct klist klist_devices; /* 迭代遍历@devices_kset的klist */
	struct klist klist_drivers; /* 遍历@drivers_kset的klist */
	struct blocking_notifier_head bus_notifier; /* 用于任何与此总线上有关的事情的总线通知列表 */
	unsigned int drivers_autoprobe:1;
	struct bus_type *bus;		/* 指向该结构关联的结构bus_type的指针 */

	struct kset glue_dirs;		/* 将“glue”目录放在父设备之间,以避免名称空间冲突 */
	struct class *class;		/* 指向与此结构关联的结构类的指针 */
};

/* 总线是处理器和一个或多个设备之间的通道。对于设备模型,所有设备都通过总线连接,即使它是一个
 * 内部的、虚拟的“平台”总线。总线可以互相插接。例如,USB控制器通常是PCI设备。设备模型表示
 * 总线和它们控制的设备之间的实际连接。总线由bus_type结构表示。它包含名称、默认属性、总线方
 * 法、PM操作和驱动程序核心的私有数据。
 */
struct bus_type {
	const char		*name;						/* 总线名称 */
	const char		*dev_name;					/* 用于子系统枚举设备,如(“foo%u”,dev->id) */
	struct device		*dev_root;				/* 作为父设备使用的默认设备 */
	struct device_attribute	*dev_attrs;			/* 总线上设备的默认属性 use dev_groups instead */
	const struct attribute_group **bus_groups;	/* 总线的默认属性 */
	const struct attribute_group **dev_groups;	/* 总线上设备的默认属性 */
	const struct attribute_group **drv_groups;	/* 总线上设备驱动程序的默认属性 */
	
    /* 每当为此总线添加新设备或驱动程序时,调用(可能多次)此函数。如果给定的设备可以被给定的驱动程序处理,
	 * 它应该返回一个非零值。 */
	int (*match)(struct device *dev, struct device_driver *drv);
    
    /* 在添加、删除设备或其他生成uevents来添加环境变量的操作时调用 */
	int (*uevent)(struct device *dev, struct kobj_uevent_env *env);
	
    /* 当一个新的设备或驱动添加到这个总线时调用,并回调特定驱动的探测来初始化匹配的设备 */
    int (*probe)(struct device *dev);
    
    /* 当设备从总线中移除时调用 */
	int (*remove)(struct device *dev);
    
    /* 在关闭时间调用以暂停设备 */
	void (*shutdown)(struct device *dev);
    
	/* 调用以使设备重新联机(在脱机后) */
	int (*online)(struct device *dev);
   
    /* 调用以使设备脱机进行热拆拔。可能会失败 */
	int (*offline)(struct device *dev);
	
    /* 当此总线上的设备要进入睡眠模式时调用 */
	int (*suspend)(struct device *dev, pm_message_t state);
    
    /* 调用来将此总线上的设备从睡眠模式中唤醒 */
	int (*resume)(struct device *dev);
    
	/* 此总线的电源管理操作,回调特定设备驱动程序的pm-ops */
	const struct dev_pm_ops *pm;
    
	/* 此总线的IOMMU特定操作,用于将IOMMU驱动程序实现附加到总线,并允许驱动程序执行特定于总线的设置 */
	const struct iommu_ops *iommu_ops;
	
    /* 驱动核心的私有数据,只有驱动核心可以触摸到它 */
	struct subsys_private *p;
	struct lock_class_key lock_key;
};

struct bus_type i2c_bus_type = {
	.name		= "i2c",
	.match		= i2c_device_match,
	.probe		= i2c_device_probe,
	.remove		= i2c_device_remove,
	.shutdown	= i2c_device_shutdown,
};

i2c_init().【注4static int __process_new_driver(struct device *dev, void *data)
{
	if (dev->type != &i2c_adapter_type)
		return 0;
    /* #define to_i2c_adapter(d) container_of(d, struct i2c_adapter, dev) */
	return i2c_do_add_adapter(data, to_i2c_adapter(dev)); 
    --> /* 检测该总线上能被支持的设备,并实例化它们 */
        i2c_detect(adap, driver);

        /* Let legacy drivers scan this bus for matching devices */
        if (driver->attach_adapter) {
            dev_warn(&adap->dev, "%s: attach_adapter method is deprecated\n",
                 driver->driver.name);
            dev_warn(&adap->dev, "Please use another way to instantiate "
                 "your i2c_client\n");
            /* We ignore the return code; if it fails, too bad */
            driver->attach_adapter(adap);
        }
        return 0;
}

i2c_init().【注5static struct notifier_block i2c_of_notifier = {
	.notifier_call = of_i2c_notify,
};

static int of_i2c_notify(struct notifier_block *nb, unsigned long action,
			 void *arg)
{
	struct of_reconfig_data *rd = arg;
	struct i2c_adapter *adap;
	struct i2c_client *client;
	/* 根据使用的通知返回设备的新状态(status)。在设备从启用(ok)到禁用(disable)时返回0,
	 * 在设备从禁用到启用时返回1,在没有改变时返回-1。 */
	switch (of_reconfig_get_state_change(action, rd)) {
	case OF_RECONFIG_CHANGE_ADD:
		adap = of_find_i2c_adapter_by_node(rd->dn->parent);
            /* 这类似于上文的bus_for_each_dev()函数,但它返回一个指向“找到”设备的指针,以供后续使用,
             * 这是由@match(of_dev_node_match)回调函数确定的。如果设备不匹配,回调函数应该返回0,否
             * 则返回非0。如果回调函数返回非零值,该函数将返回到调用者,并且不再迭代任何其他设备。 */
        --> dev = bus_find_device(&i2c_bus_type, NULL, node, of_dev_node_match);
            /* 当设备类型dev->type与i2c_adapter_type一致时, 即为i2c设备,可通过struct i2c_adapter
             * 结构体中的dev成员找到i2c适配器(i2c_adapter) */
            return i2c_verify_adapter(dev); /* 返回参数为i2c适配器或NULL */
           
		client = of_i2c_register_device(adap, rd->dn);
        --> struct i2c_client *result;
            struct i2c_board_info info = {};
            struct dev_archdata dev_ad = {};
            const __be32 *addr;
            int len;
			/* 根据compatible属性的值, 此程序将尝试为特定设备树节点选择适当的modalias值。它通过剥离在
			 * compatible列表属性中第一个条目中的制造商的前缀(如由','分隔中限制)。*/
            if (of_modalias_node(node, info.type, sizeof(info.type)) < 0) {
                return ERR_PTR(-EINVAL);
            }
			
            /* 从设备节点获取reg属性值并返回 */
            addr = of_get_property(node, "reg", &len);
            if (!addr || (len < sizeof(int))) {
                return ERR_PTR(-EINVAL);
            }

            info.addr = be32_to_cpup(addr);
            if (info.addr > (1 << 10) - 1) {
                return ERR_PTR(-EINVAL);
            }

            info.of_node = of_node_get(node);
            info.archdata = &dev_ad;
			/* 从设备节点获取唤醒源wakeup-source属性 */
            if (of_get_property(node, "wakeup-source", NULL))
                info.flags |= I2C_CLIENT_WAKE;
			/* 实例化一个i2c设备——创建一个i2c设备。绑定通过驱动模型probe()/remove()方法处理。
			 * 当我们从这个函数返回或稍后的时候(例如,可能是热插入会加载驱动程序模块)时,驱动程序可能会
			 * 绑定到这个设备。这个调用不适合于主板初始化逻辑使用,它通常在任何i2c_adapter存在之前很
			 * 久就在arch_initcall()中运行。 */
            result = i2c_new_device(adap, &info);
            --> struct i2c_client *client;
                int status;
				/* 向内核申请分配struct i2c_client结构体指针类型空间 */
                client = kzalloc(sizeof *client, GFP_KERNEL);
				/* 将i2c适配器指针adap赋值给i2c客户端 */
                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));

                /* 这是一个允许的地址有效性检查,除了通用的调用地址,I2C地址映射约束不是故意强制执行的。
                 * 如果是标识的是10位地址则不能超过10位,否则不能超过7位 */
                status = i2c_check_client_addr_validity(client);
                if (status) {
                    goto out_err_silent;
                }

                /* Check for address business */
                status = i2c_check_addr_busy(adap, client->addr);
                if (status)
                    goto out_err;

                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;
				/* 为struct i2c_client结构体成员dev设置名称, 对于10位的客户端,添加任意的偏移量以
				 * 避免冲突 */
                i2c_dev_set_name(adap, client);
            	
            	/* 这只需要两个简单的步骤——初始化设备并将其添加到系统中。这两个步骤可以分开调用,但这是
            	 * 最简单也是最常用的。例如,你应该只在有明确定义的需要使用和refcount设备添加到层次结
            	 * 构之前分别调用这两个辅助方法。
            	 * 
            	 * 有关更多信息,请参阅有关device_initialize()和device_add()的kerneldoc。
            	 * 
            	 * 注意: "永远不要"在调用此函数后直接释放@dev,即使它返回错误!总是使用put_device()
            	 * 来放弃在该函数中初始化的引用。*/
                status = device_register(&client->dev);
                return client;
            if (result == NULL) {
                /* 设备节点的refcount的值减1 */
                of_node_put(node);
                return ERR_PTR(-EINVAL);
            }
		put_device(&adap->dev);
		break;
            
	case OF_RECONFIG_CHANGE_REMOVE:
		/* find our device by node */
		client = of_find_i2c_device_by_node(rd->dn);
		
        /* unregister takes one ref away */
		i2c_unregister_device(client);

		/* and put the reference of the find */
		put_device(&client->dev);
		break;
	}
}

在这里插入图片描述
在这里插入图片描述

  1. 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 of_match_device(drv->of_match_table, dev) != NULL;
	--> return of_match_node(drv->of_match_table, dev->of_node);
		--> match = __of_match_node(drv->of_match_table, dev->of_node);
            --> score = __of_device_is_compatible(dev->of_node, 
                            drv->of_match_table->compatible, 
                            drv->of_match_table->type, 
                            drv->of_match_table->name);
				--> /* 当驱动of_match_table有compatible成员且其值不为空时 */
                    if (drv->of_match_table->compatible && 
                        drv->of_match_table->compatible[0]) {
                        /* 遍历"设备节点"所有的compatible属性值, 直到找到compatible属性 */
                        prop = __of_find_property(dev->of_node, "compatible", NULL);
                        for (cp = of_prop_next_string(prop, NULL); cp;
                             cp = of_prop_next_string(prop, cp), index++) {
                            /* "设备节点"的compatible属性与驱动of_match_table中的
                             * compatible成员比较, 一致时退出比较 */
                            if (of_compat_cmp(cp, drv->of_match_table->compatible, 
                                    strlen(drv->of_match_table->compatible)) == 0) {
                                score = INT_MAX/2 - (index << 2);
                                break;
                            }
                        }
                    }

                    /* 当驱动of_match_table有type成员且其值不为空时 */
                    if (drv->of_match_table->type && drv->of_match_table->type[0]) {
                        /* "设备节点"的type属性与驱动of_match_table中的type成员比较, 
                         * 一致时退出比较 */
                        if (!dev->of_node->type || 
                            of_node_cmp(drv->of_match_table->type, 
                                        dev->of_node->type))
                            return 0;
                        score += 2;
                    }

                    /* 当驱动of_match_table有name成员且其值不为空时 */
                    if (drv->of_match_table->name && drv->of_match_table->name[0]) {
                        /* "设备节点"的name属性与驱动of_match_table中的name成员比较, 
                         * 一致时退出比较 */
                        if (!dev->of_node->name || 
                            of_node_cmp(drv->of_match_table->name, 
                                        dev->of_node->name))
                            return 0;
                        score++;
                    }

/* 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)
    /* 比较i2c_client->name与i2c_device_id->name是否一致 */
	return i2c_match_id(driver->id_table, client) != NULL;

}




3. `i2c_device_probe()` 在新设备或驱动程序添加到i2c总线时调用此函数,并回调特定驱动程序的探测以初始化匹配的设备。
```c
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 && dev->of_node) {
 	int irq = of_irq_get(dev->of_node, 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 (!device_can_wakeup(&client->dev))
 	device_init_wakeup(&client->dev,
 				client->flags & I2C_CLIENT_WAKE);
 dev_dbg(dev, "probe\n");

 status = of_clk_set_defaults(dev->of_node, false);
 if (status < 0)
 	return status;

 status = dev_pm_domain_attach(&client->dev, true);
 if (status != -EPROBE_DEFER) {
 	status = driver->probe(client, i2c_match_id(driver->id_table,
 				client));
 	if (status)
 		dev_pm_domain_detach(&client->dev, true);
 }

 return status;
}

  嵌入式 最新文章
基于高精度单片机开发红外测温仪方案
89C51单片机与DAC0832
基于51单片机宠物自动投料喂食器控制系统仿
《痞子衡嵌入式半月刊》 第 68 期
多思计组实验实验七 简单模型机实验
CSC7720
启明智显分享| ESP32学习笔记参考--PWM(脉冲
STM32初探
STM32 总结
【STM32】CubeMX例程四---定时器中断(附工
上一篇文章      下一篇文章      查看所有文章
加:2022-09-04 01:27:45  更:2022-09-04 01:28:27 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/18 12:06:00-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码