Linux内核i2c驱动详解

一. 内核框架

Linux系统定义了I2C驱动体系结构。在Linux系统中,I2C驱动由三部分组成,即I2C核心、I2C总线驱动和I2C设备驱动。这三部分相互协作,形成了非常通用、可适应性很强的I2C框架。

Linux内核i2c驱动详解_第1张图片

I2C核心 I2C核心提供了I2C总线驱动和设备驱动的注册、注销方法,I2C通信方法(即“Algorithm”)上层的、与具体适配器无关的代码以及探测设备、检测设备地址的上层代码等。

 

I2C总线驱动 I2C总线驱动是对I2C硬件体系结构中适配器端的实现,适配器可由CPU控制,甚至可以直接集成在CPU内部。

I2C总线驱动主要包含了I2C适配器数据结构i2c_adapter、I2C适配器的Algorithm数据结构i2c_algorithm和控制I2C适配器产生通道信号的函数。

经由I2C总线驱动的代码,可以控制I2C适配器以主控方式产生开始位、停止位、读写周期,以及以从设备方式被读写、产生ACK等。

I2C设备驱动 I2C设备驱动(也称为客户驱动)是对I2C硬件体系结构中设备端的实现。设备一般挂接在受CPU控制的I2C适配器上,通过I2C适配器与CPU交换数据。

I2C设备驱动主要包含了数据结构i2c_driver和i2c_client,需要根据具体设备实现其中的成员函数。

在Linux内核中,所有的I2C设备都在sysfs文件系统中显示,存于/sys/bus/i2c/目录,以适配器地址和芯片地址的形式列出。例如:

$ tree /sys/bus/i2c/
​
|—— devices
​
|         |—— 0-0058 -> ../../../devices/platform/PHYT0003:00/i2c-0/0-0058
​
|         |—— 0-0059 -> ../../../devices/platform/PHYT0003:00/i2c-0/0-0059
​
|         |—— 0-005a -> ../../../devices/platform/PHYT0003:00/i2c-0/0-005a
​
|         |—— i2c-0 -> ../../../devices/platform/PHYT0003:00/i2c-0
​
|         |—— i2c-1 -> ../../../devices/platform/PHYT0003:01/i2c-1
​
……
​
|—— drivers
​
|         |—— 88PM860x
​
|         |         |—— bind
​
|         |         |—— uevent
​
|         |         |—— unbind
​
|         |—— ab3100
​
|         |         |—— uevent
​
|         |—— at24
​
|         |         |—— bind
​
|         |         |—— i2c-INT0002:00 -> ../../../../devices/platform/PHYT0003:00/i2c-0/i2c-INT0002:00
​
|         |         |—— module -> ../../../../module/at24
​
|         |         |—— uevent
​
|         |         |—— unbind
​
……
​
|—— drivers_autoprobe
​
|—— drivers_probe
​
|—— uevent

I2C内核源码 在Linux内核源代码中的drivers目录下有一个i2c目录,在i2c目录下又包含如下文件和文件夹:

(1)i2c-core.c 实现了I2C核心功能。

注:i2c-core.c在2017年5月31日被拆分成了

i2c-core-base.c 是 I2C 核心代码的基础部分,提供了 I2C 总线的核心功能,包括设备的注册、卸载和访问等。

i2c-core-slave.c是 I2C 从设备的核心代码,实现了作为 I2C 从设备的功能,包括接收和发送数据等。

i2c-core-smbus.c 实现了 I2C 的 SMBus 功能,SMBus 是一种在 I2C 总线上运行的简化版本协议,用于支持一些特殊的功能,如设备的配置和控制等。

i2c-core-of.c 实现了 I2C 的设备树(Device Tree)支持,设备树是一种描述硬件设备连接关系和属性的数据结构,用于设备的自动识别和配置。

i2c-core-acpi.c实现了 I2C 的 ACPI(Advanced Configuration and Power Interface)支持,ACPI 是一种用于系统配置和电源管理的标准,用于设备的自动识别和配置。

(2)i2c-dev.c 实现了I2C适配器设备文件的功能,每一个I2C适配器都被分配一个设备。通过适配器访问设备文件时的主设备号都为89,次设备号为0~255。应用程序通过“i2c-%d”(i2c-0、i2c-1、……)文件名并使用文件操作接口open、write、read、ioctl和close等来访问这个设备。

i2c-dev.c并不是针对特定的设备而设计的,只是提供了通用的read、write和ioctl等接口,应用层可以借用这些接口访问挂接在适配器上的I2C存储空间或寄存器,并控制I2C设备的工作方式。

(3)busses文件夹 这个文件包含了一些I2C主机控制器的驱动,如i2c-tegra.c、i2c-omap.c、i2c-versatile.c、i2c-s3c2410.c等。

(4)algos文件夹 实现了一些I2C总线适配器的通信方法。

二.分析i2c-dev.c

分析一个驱动先从init看起

1.init

static int __init i2c_dev_init(void)
{
    int res;
    printk(KERN_INFO "i2c /dev entries driver\n");
     // 注册字符设备区域,指定主设备号、次设备号范围和设备名
    res = register_chrdev_region(MKDEV(I2C_MAJOR, 0), I2C_MINORS, "i2c");
    if (res)
        goto out;
    // 创建 i2c-dev 类
    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;
    /*在Linux内核中,注册总线通知器(bus notifier)是一种机制,用于跟踪和处理系统中总线适配器的添加和移除事件。总线适配器是连接外部设备(如传感器、网络设备等)与主机系统的接口。通过注册总线通知器,内核可以在总线适配器被添加或移除时执行相应的操作。
​
当一个总线适配器被添加到系统中时,内核会触发一个总线通知事件。这时,已注册的总线通知器将被调用,并可以执行一些操作,如初始化设备、分配资源等。
​
同样地,当一个总线适配器从系统中移除时,内核也会触发总线通知事件。此时,已注册的总线通知器也会被调用,并可以执行一些清理操作,如释放资源、停止设备等。*/
    // 注册总线通知器,用于跟踪适配器的添加和移除
    res = bus_register_notifier(&i2c_bus_type, &i2cdev_notifier);
    if (res)
        goto out_unreg_class;
     // 立即绑定已经存在的适配器
    i2c_for_each_dev(NULL, i2cdev_attach_adapter);
    return 0;
out_unreg_class:
    class_destroy(i2c_dev_class);
out_unreg_chrdev:
    unregister_chrdev_region(MKDEV(I2C_MAJOR, 0), I2C_MINORS);
out:
    printk(KERN_ERR "%s: Driver Initialisation failed\n", __FILE__);
    return res;
}

2.分析bus_register_notifier

在 /注册总线通知器代码中,bus_register_notifier 函数用于注册一个总线通知器(notifier)到指定的总线类型(bus_type)。总之,这行代码的作用是将自定义的总线通知器注册到指定的 I2C 总线类型上,以便在 I2C 总线上发生适配器添加或移除事件时,触发相应的回调函数进行处理,下面是关于这两个参数的解释:

  1. &i2c_bus_type:这是一个指向 struct bus_type 结构体的指针,表示要注册通知器的总线类型。在这个例子中,i2c_bus_type 是 I2C 总线类型的结构体,用于表示 I2C 总线。通过使用 & 操作符,获取了 i2c_bus_type 的地址,以便将其作为参数传递给 bus_register_notifier 函数。

    • 让我们逐个解释`struct bus_type`结构体中的各个成员的含义:
      ​
      - `const char *name`:总线的名称,用于标识该总线的类型。
      ​
      - `const char *dev_name`:总线上设备的名称前缀。每个设备在总线上都有一个唯一的名称,这个名称由`dev_name`加上设备的编号组成。
      ​
      - `struct device *dev_root`:指向总线上根设备的指针。根设备是总线上的顶级设备,通常表示整个总线的控制器。
      ​
      - `const struct attribute_group **bus_groups`:指向总线属性组的指针数组。属性组是一组相关的设备属性,用于在/sys/bus目录下显示设备的信息。
      ​
      - `const struct attribute_group **dev_groups`:指向设备属性组的指针数组。设备属性组包含了设备特定的属性,用于在/sys/devices目录下显示设备的信息。
      ​
      - `const struct attribute_group **drv_groups`:指向驱动程序属性组的指针数组。驱动程序属性组包含了与驱动程序相关的属性,用于在/sys/bus/driver目录下显示驱动程序的信息。
      ​
      - `int (*match)(struct device *dev, struct device_driver *drv)`:指向设备匹配函数的指针。这个函数用于判断给定的设备是否与驱动程序匹配。
      ​
      - `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)`:指向设备恢复函数的指针。这个函数在设备从挂起状态恢复时被调用。
      ​
      - `int (*num_vf)(struct device *dev)`:指向获取虚拟功能数量函数的指针。这个函数用于获取设备支持的虚拟功能数量。
      ​
      - `int (*dma_configure)(struct device *dev)`:指向DMA配置函数的指针。这个函数用于配置设备的DMA引擎。
      ​
      - `const struct dev_pm_ops *pm`:指向设备的电源管理操作结构体的指针。这个结构体包含了设备的电源管理相关函数。
      ​
      - `const struct iommu_ops *iommu_ops`:指向设备的IOMMU操作结构体的指针。这个结构体包含了设备的IOMMU相关函数。
      ​
      - `struct subsys_private *p`:指向与总线相关的私有数据的指针。
      ​
      - `struct lock_class_key lock_key`:用于锁定总线的锁类别。
      ​
      - `bool need_parent_lock`:表示总线是否需要父锁。如果为真,则总线上的设备在访问父设备时需要获取父设备的锁。
      ​
       

      通过源码我们可以看到Linux内核i2c驱动详解_第2张图片

       

    • 其添加了总线的名称,指向设备匹配函数的指针,指向初始化函数的指针,指向卸载函数的指针,指向关机函数的指针。

  2. &i2cdev_notifier:这是一个指向 struct notifier_block 结构体的指针,表示要注册的总线通知器。struct notifier_block 是一个通用的通知器结构体,用于注册和处理通知事件。i2cdev_notifier 是一个自定义的总线通知器,其中包含了通知事件发生时需要执行的回调函数。通过使用 & 操作符,获取了 i2cdev_notifier 的地址,以便将其作为参数传递给 bus_register_notifier 函数。

struct notifier_block 是一个用于注册和管理通知回调函数的结构体。它包含以下几个成员:

notifier_call:通知回调函数的指针。在特定事件发生时,内核会调用该函数来通知相关的处理逻辑。通常,该函数具有以下原型:

int (*notifier_call)(struct notifier_block *self, unsigned long event, void *data);

这个函数接收三个参数:self 表示指向当前 notifier_block 结构体的指针,event 表示事件类型,data 表示与事件相关的数据。

  1. next:指向下一个 notifier_block 结构体的指针。在注册多个通知回调函数时,内核会将它们以链表的形式连接起来,通过 next 指针进行遍历。

  2. priority:通知回调函数的优先级。当多个通知回调函数注册到同一个事件上时,内核会按照优先级的顺序调用它们。优先级较高的回调函数会先于优先级较低的回调函数被调用。

这个结构体的作用是为内核提供一种通用的机制,用于在特定事件发生时,通知注册的回调函数执行相应的处理逻辑。通过注册和取消注册 notifier_block 结构体,可以动态地管理回调函数的执行。

通过内核我们知道了注册进去了一个i2cdev_notifier_call

 

这里我们可以看到struct notifier_block的结构体。

struct notifier_block {
    notifier_fn_t notifier_call; //这是一个函数指针,它指向一个具有特定签名的函数。在使用`struct notifier_block`结构体时,可以将一个函数赋值给`notifier_call`,当触发某个事件时,系统将调用该函数。
    struct notifier_block __rcu *next; //这是一个指向下一个`struct notifier_block`结构体的指针。通过将多个`struct notifier_block`结构体链接在一起,可以形成一个链表,系统可以按照一定的顺序调用它们。
    int priority; //这是一个整数值,表示当前`struct notifier_block`结构体的优先级。当多个`struct notifier_block`结构体被链接在一起时,系统将按照优先级的顺序调用它们。
};

通过使用struct notifier_block结构体,可以实现事件通知机制,当某个事件发生时,系统可以调用相应的函数进行处理。

3.分析bus_register_notifier的第一个参数i2c_bus_type

Linux内核i2c驱动详解_第3张图片

 

我们可以看到它添加了总线的名称,指向设备匹配函数的指针,指向初始化设备,指向卸载函数的指针,指向关机函数的指针。

3.1设备匹配函数的指针
static int i2c_device_match(struct device *dev, struct device_driver *drv)
{
    struct i2c_client   *client = i2c_verify_client(dev); // 从设备结构体获取I2C客户端结构体指针
    struct i2c_driver   *driver;                          // I2C驱动结构体指针
    // 尝试使用OF风格的匹配方式
    if (i2c_of_match_device(drv->of_match_table, client))
        return 1;// 匹配成功,返回1
    // 尝试使用ACPI风格的匹配方式
    if (acpi_driver_match_device(dev, drv))
        return 1;
    // 将设备驱动结构体指针转换为I2C驱动结构体指针
    driver = to_i2c_driver(drv);
    // 尝试使用I2C风格的匹配方式
    if (i2c_match_id(driver->id_table, client))
        return 1;
    return 0;
}

该函数用于判断给定的设备和设备驱动是否匹配。首先尝试使用OF(Open Firmware)风格的匹配方式,然后尝试使用ACPI(Advanced Configuration and Power Interface)风格的匹配方式,最后尝试使用I2C风格的匹配方式。如果匹配成功,则返回1,表示匹配;否则返回0,表示不匹配。

注释:

OF(Open Firmware)风格的匹配方式是一种在Linux内核中用于设备和驱动程序匹配的方法。它使用设备树(Device Tree)来描述硬件设备的信息,并通过设备树节点中的属性与驱动程序进行匹配。设备树是一种描述硬件设备的静态数据结构,它通过树形结构的方式表示设备之间的层次关系和属性。设备树文件通常以.dts或.dtb为扩展名,在Linux启动时由引导加载程序加载到内存中。在OF风格的匹配方式中,驱动程序通过查找设备树中的设备节点,根据节点的属性进行匹配。设备节点的属性可以包含设备的厂商ID、设备ID、设备类型等信息。驱动程序会将自己支持的设备属性与设备节点的属性进行比较,如果匹配成功,则将该驱动程序与设备关联起来。

ACPI(Advanced Configuration and Power Interface)风格的匹配方式是一种在操作系统中用于设备和驱动程序匹配的方法。ACPI是一种开放标准,用于定义系统硬件的配置和电源管理接口。在ACPI风格的匹配方式中,驱动程序通过与系统的ACPI表进行交互来获取设备的信息,并根据设备的特征进行匹配。ACPI表包含了系统中各个设备的描述信息,包括设备的厂商ID、设备ID、设备类型等。驱动程序通过解析ACPI表获取设备的信息,并与自身支持的设备特征进行匹配。如果匹配成功,则将该驱动程序与设备关联起来,并进行初始化和配置。ACPI风格的匹配方式相对于OF风格的匹配方式更加灵活,可以适应更多不同类型的硬件设备。它在现代操作系统中广泛使用,特别是在基于Intel架构的计算机系统中。ACPI不仅提供了设备匹配的功能,还提供了电源管理、设备配置和操作等功能,使得系统的配置和管理更加方便和灵活。

在I2C风格的匹配方式中,驱动程序通过与I2C总线进行交互来获取设备的信息,并根据设备的地址和其他特征进行匹配。每个I2C设备在总线上都有一个唯一的7位或10位地址,驱动程序通过与设备的地址进行比较来确定匹配。Linux内核中提供了一些函数和数据结构来支持I2C设备和驱动程序的匹配。其中一个重要的数据结构是struct i2c_device_id,它用于描述驱动程序支持的I2C设备的ID信息,包括设备的名称和地址等。驱动程序可以使用MODULE_DEVICE_TABLE(i2c, ...)宏来定义struct i2c_device_id数组,将其与驱动程序关联起来。内核在加载驱动程序时,会根据设备的地址和其他特征在数组中查找匹配项,并将驱动程序与设备进行关联。此外,驱动程序还需要实现probe()函数来处理设备的初始化和配置。当驱动程序与设备匹配成功后,内核会调用probe()函数来初始化设备并进行必要的设置。总之,I2C风格的匹配方式通过设备的地址和其他特征来进行匹配,并通过驱动程序的probe()函数来初始化和配置设备。这种方式在Linux内核中广泛应用于I2C设备的驱动程序。

我们这里还看到了两个结构体

struct i2c_client {
    unsigned short flags;//标志位,用于存储一些标志信息,例如是否启用Packet Error Checking、是否使用10位地址、是否是从设备等。
#define I2C_CLIENT_PEC      0x04    /* 表示设备支持数据包错误检测 (PEC) 功能  */
#define I2C_CLIENT_TEN      0x10    /* 表示设备使用10位的芯片地址,而不是标准的7位地址 */
#define I2C_CLIENT_SLAVE    0x20    /* 表示设备是I2C总线上的从设备 */
#define I2C_CLIENT_HOST_NOTIFY  0x40    /* 表示设备希望使用I2C主机通知机制来接收通知 */
#define I2C_CLIENT_WAKE     0x80    /*用于board_info,表示设备是否具有唤醒功能 */
#define I2C_CLIENT_SCCB     0x9000  /*表示设备使用Omnivision SCCB协议进行通信 */
    unsigned short addr;    //  I2C设备的地址,注意只存储了7位地址
    char name[I2C_NAME_SIZE];  // I2C设备的名称,用于在系统中唯一标识该设备
    struct i2c_adapter *adapter;    /* 指向I2C适配器的指针,表示该设备所连接的I2C总线适配器    */
    struct device dev;      /*  Linux设备模型中的设备结构体,用于表示该I2C设备在系统中的设备节点    */
    int init_irq;           /* 初始化时设置的中断号。*/
    int irq;            /* 设备触发的中断号*/
    struct list_head detected; //一个链表头,用于将该设备添加到已检测到的设备列表中
#if IS_ENABLED(CONFIG_I2C_SLAVE)
    i2c_slave_cb_t slave_cb;    /*  仅在启用了I2C从设备模式时可用,用于指定从设备的回调函数   */
#endif
};

这个结构体用于表示一个I2C设备的客户端,包含了设备的各种信息,例如地址、名称、所连接的适配器等。在使用Linux的I2C子系统时,可以使用这个结构体来表示一个I2C设备,并通过相应的函数来操作和控制该设备。

struct i2c_driver {
    unsigned int class;                        /* 设备类别 */
    int (*probe)(struct i2c_client *client, const struct i2c_device_id *id);    /* 设备探测函数 */
    int (*remove)(struct i2c_client *client);                                   /* 设备移除函数 */
    int (*probe_new)(struct i2c_client *client);                                /* 新的设备探测函数 */
    void (*shutdown)(struct i2c_client *client);                                /* 设备关闭函数 */
      /*警报回调,例如SMBus警报协议。
      *数据值的格式和含义取决于协议。
      *对于SMBus警报协议,只传递一位数据
      *作为警报响应的低位(“事件标志”)。
      *对于SMBus Host Notify协议,数据对应于
      *由充当主设备的从设备报告的16比特有效载荷数据。 
      */
    void (*alert)(struct i2c_client *client, enum i2c_alert_protocol protocol,
              unsigned int data);                                            /* 警报回调函数 */
    int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);     /* 设备特定功能的命令函数 */
    struct device_driver driver;                          /* 设备驱动结构体 */
    const struct i2c_device_id *id_table;                 /* 设备ID表 */
    int (*detect)(struct i2c_client *client, struct i2c_board_info *info);       /* 自动设备检测函数 */
    const unsigned short *address_list;                                         /* 设备地址列表 */
    struct list_head clients;                                                   /* I2C客户端链表 */
    bool disable_i2c_core_irq_mapping;                                          /* 是否禁用I2C核心的中断映射 */
};

struct i2c_driver结构体定义了I2C设备驱动的接口和属性。

3.2初始化设备
static int i2c_device_probe(struct device *dev)
{
    struct i2c_client   *client = i2c_verify_client(dev); // 验证设备是否为i2c_client类型
    struct i2c_driver   *driver; // I2C设备驱动结构体指针
    int status;
    if (!client) // 如果设备不是i2c_client类型,则返回0
        return 0;
    driver = to_i2c_driver(dev->driver); // 将设备的驱动结构体指针转换为i2c_driver类型
    client->irq = client->init_irq;      // 初始化设备的中断号
    if (!client->irq && !driver->disable_i2c_core_irq_mapping) { // 如果设备的中断号为空,并且驱动没有禁用I2C核心的中断映射
        int irq = -ENOENT;
        if (client->flags & I2C_CLIENT_HOST_NOTIFY) {    // 如果设备设置了I2C_CLIENT_HOST_NOTIFY标志
            dev_dbg(dev, "Using Host Notify IRQ\n");
            pm_runtime_get_sync(&client->adapter->dev);   // 获取设备所属的适配器,并使其处于活动状态
            irq = i2c_smbus_host_notify_to_irq(client);   // 将I2C SMBus Host Notify转换为中断号
        } else 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)) {                  // 如果设备有ACPI伙伴
            irq = i2c_acpi_get_irq(client);                // 获取ACPI中断号
        }
        if (irq == -EPROBE_DEFER) // 如果中断号延迟探测
            return irq;
        if (irq < 0)
            irq = 0;
        client->irq = irq; // 设置设备的中断号
    }
    if (!driver->id_table &&
        !i2c_acpi_match_device(dev->driver->acpi_match_table, client) &&
        !i2c_of_match_device(dev->driver->of_match_table, client)) // 如果设备的驱动没有ID表,并且无法匹配ACPI或设备树的ID表
        return -ENODEV;
    if (client->flags & I2C_CLIENT_WAKE) {                    // 如果设备设置了I2C_CLIENT_WAKE标志
        int wakeirq;
        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) // 如果唤醒中断号大于0且不等于设备的中断号
            status = dev_pm_set_dedicated_wake_irq(dev, wakeirq); // 设置设备的专用唤醒中断号
        else if (client->irq > 0)
            status = dev_pm_set_wake_irq(dev, client->irq); // 设置设备的唤醒中断号
        else
            status = 0;
        if (status)
            dev_warn(&client->dev, "failed to set up wakeup irq\n"); // 如果设置唤醒中断号失败,则打印警告信息
    }
    dev_dbg(dev, "probe\n");
    status = of_clk_set_defaults(dev->of_node, false); // 设置设备节点的时钟参数,默认为false
    if (status < 0)
        goto err_clear_wakeup_irq;
    status = dev_pm_domain_attach(&client->dev, true); // 将设备与电源管理域关联起来
    if (status)
        goto err_clear_wakeup_irq;
    if (driver->probe_new)                  // 如果驱动有probe_new函数
        status = driver->probe_new(client); // 调用probe_new函数
    else if (driver->probe)                 // 如果驱动有probe函数
        status = driver->probe(client,
                       i2c_match_id(driver->id_table, client)); // 调用probe函数,并传入设备的ID表和client参数
    /*在调用probe函数时,会传入两个参数:client和i2c_match_id(driver->id_table, client)。其中client是当前的I2C设备客户端结构体指针,而i2c_match_id函数则用于在设备ID表中查找与该设备匹配的ID。这个ID通常用于驱动程序在支持多个设备时进行区分和配置。*/
    else
        status = -EINVAL;
    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);   // 初始化设备的唤醒属性为false
    return status;
}

该函数是用于处理I2C设备的探测过程。函数首先验证设备是否为i2c_client类型,然后获取设备的驱动结构体指针。接下来,函数会设置设备的中断号,如果没有设置且驱动没有禁用I2C核心的中断映射,则尝试获取中断号。如果设备设置了I2C_CLIENT_HOST_NOTIFY标志,则通过适当的操作获取中断号;如果设备有设备树节点,则尝试通过节点获取中断号;如果设备有ACPI伙伴,则尝试获取ACPI中断号。然后,函数会检查设备的驱动是否有ID表,如果没有ID表且无法匹配ACPI或设备树的ID表,则返回错误。接下来,函数会处理设备的唤醒属性,如果设备设置了I2C_CLIENT_WAKE标志,则尝试获取设备的唤醒中断号,并设置设备的唤醒中断号。然后,函数会设置设备节点的时钟参数,并将设备与电源管理域关联起来。最后,函数会根据驱动的probe_new(新的驱动程序模型接口,有助于无缝删除当前probe()的,更常用的是未使用的第二个参数)或probe函数来执行设备的探测操作,并返回相应的状态。如果探测失败,则会分离设备与电源管理域的关联,并清除设备的唤醒中断号。

以上便是注册,卸载这里就不加分析

4.分析bus_register_notifier的第二个参数i2cdev_notifier`

 

我们在上面已经看过了 notifier_block结构体接下来我们看i2cdev_notifier_call的具体实现

static int i2cdev_notifier_call(struct notifier_block *nb, unsigned long action, void *data)
{
    // 将传入的void指针转换为struct device类型
    struct device *dev = data;
    // 根据action的值选择不同的操作
    switch (action) {
        // 当总线通知有设备添加时
        case BUS_NOTIFY_ADD_DEVICE:
            // 调用i2cdev_attach_adapter函数来附加设备到适配器上
            return i2cdev_attach_adapter(dev, NULL);
        // 当总线通知有设备移除时
        case BUS_NOTIFY_DEL_DEVICE:
            // 调用i2cdev_detach_adapter函数来从适配器上移除设备
            return i2cdev_detach_adapter(dev, NULL);
    }
    // 如果action的值不是上述两种情况,则返回0
    return 0;
}

由上可知我们要看i2cdev_attach_adapter函数

static int i2cdev_attach_adapter(struct device *dev, void *dummy)
{
    struct i2c_adapter *adap;
    struct i2c_dev *i2c_dev;
    int res;
    // 检查设备类型是否为i2c_adapter_type,如果不是则返回0
    if (dev->type != &i2c_adapter_type)
        return 0;
    // 将设备结构体转换为i2c_adapter结构体
    adap = to_i2c_adapter(dev);
    // 获取一个空闲的i2c_dev设备
    i2c_dev = get_free_i2c_dev(adap);
    if (IS_ERR(i2c_dev))
        return PTR_ERR(i2c_dev);
    // 初始化i2c_dev设备的字符设备结构体
    cdev_init(&i2c_dev->cdev, &i2cdev_fops);
    i2c_dev->cdev.owner = THIS_MODULE;
    // 将i2c_dev设备的字符设备结构体添加到系统中
    res = cdev_add(&i2c_dev->cdev, MKDEV(I2C_MAJOR, adap->nr), 1);
    if (res)
        goto error_cdev;
    // 使用适配器的信息创建一个i2c_dev设备,并将其注册到驱动核心中
    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;
    }
    // 打印调试信息,表示成功注册i2c_dev设备
    pr_debug("i2c-dev: adapter [%s] registered as minor %d\n",
             adap->name, adap->nr);
    return 0;
error:
    // 注册失败,删除字符设备结构体
    cdev_del(&i2c_dev->cdev);
error_cdev:
    // 释放i2c_dev设备
    put_i2c_dev(i2c_dev);
    return res;
}

这里我们看到两个结构体struct i2c_adapter ,struct i2c_dev

struct i2c_adapter {
    struct module *owner; // 指向拥有该I2C适配器的内核模块的指针
    unsigned int class;      //允许进行探测的I2C设备的类别
    const struct i2c_algorithm *algo; //用于访问总线的I2C算法。
    void *algo_data;  // I2C算法的私有数据
    const struct i2c_lock_operations *lock_ops; //I2C总线锁操作的指针
    struct rt_mutex bus_lock;  // I2C总线的互斥锁
    struct rt_mutex mux_lock;  //I2C多路复用器的互斥锁
    int timeout;        // I2C传输的超时时间(以jiffies为单位)
    int retries;        // I2C传输失败时的重试次数
    struct device dev;       //I2C适配器设备
    unsigned long locked_flags;  //被I2C核心拥有的标志位
#define I2C_ALF_IS_SUSPENDED        0
#define I2C_ALF_SUSPEND_REPORTED    1
    int nr;          //I2C适配器的编号
    char name[48];  //I2C适配器的名称
    struct completion dev_released;  // I2C设备释放完成的完成量
    struct mutex userspace_clients_lock;  //用户空间客户端的互斥锁
    struct list_head userspace_clients;  //用户空间客户端的链表头
    struct i2c_bus_recovery_info *bus_recovery_info;  // I2C总线恢复信息
    const struct i2c_adapter_quirks *quirks;   // I2C适配器的特殊属性
    struct irq_domain *host_notify_domain;
}
struct i2c_dev {
    struct list_head list;          // 用于将i2c_dev结构体连接到全局链表中的链表头
    struct i2c_adapter *adap;       // 指向i2c_adapter结构体的指针,表示I2C适配器
    struct device *dev;             // 指向device结构体的指针,表示I2C设备
    struct cdev cdev;               // 指向cdev结构体的指针,表示字符设备
};
4.1初始化i2c_dev设备的字符设备结构体

cdev_init(&i2c_dev->cdev, &i2cdev_fops);

/**
 * cdev_init - 初始化字符设备结构体
 * @cdev: 要初始化的字符设备结构体指针
 * @fops: 字符设备所支持的操作函数结构体指针
 *
 * 该函数用于初始化一个字符设备结构体。
 * 具体的初始化步骤如下:
 * 1. 使用memset函数将cdev结构体的内存空间清零,确保结构体的所有成员变量都被初始化为0。
 * 2. 使用INIT_LIST_HEAD宏初始化cdev结构体中的list成员变量,该成员变量是一个双向链表的头节点,用于管理与该字符设备相关的文件。
 * 3. 使用kobject_init函数初始化cdev结构体中的kobj成员变量,该成员变量是一个内核对象,用于表示该字符设备在内核中的对象。
 * 4. 将cdev结构体中的ops成员变量指向传入的file_operations结构体指针fops,该结构体定义了字符设备所支持的操作。
 *
 * 通过以上步骤,cdev结构体就被成功初始化,可以用于表示一个字符设备并进行后续的操作。
 */
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
{
    memset(cdev, 0, sizeof *cdev);
    INIT_LIST_HEAD(&cdev->list);
    kobject_init(&cdev->kobj, &ktype_cdev_default);
    cdev->ops = fops;
}
4.2使用适配器的信息创建一个i2c_dev设备
struct device *device_create(struct class *class, struct device *parent,
                 dev_t devt, void *drvdata, const char *fmt, ...)
{
    va_list vargs;  // 定义一个变长参数列表
    struct device *dev;  // 定义一个指向设备结构体的指针
​
    va_start(vargs, fmt);  // 初始化变长参数列表,fmt是最后一个固定参数的前一个参数
    dev = device_create_vargs(class, parent, devt, drvdata, fmt, vargs);  // 调用device_create_vargs函数创建设备
    va_end(vargs);  // 结束变长参数列表的使用
    return dev;  // 返回创建的设备结构体指针
}

这里便创建了i2c-x的节点。

5.绑定已经存在的适配器i2c_for_each_dev(NULL, i2cdev_attach_adapter);

这段代码使用了i2c_for_each_dev函数来遍历系统中的每个i2c设备,并调用i2cdev_attach_adapter函数来将i2c适配器与i2c设备连接起来。

具体解释如下:

  • i2c_for_each_dev是一个宏定义,用于遍历系统中的每个i2c设备。它会遍历一个全局的i2c设备链表,对每个i2c设备执行指定的操作。

  • NULL表示不传递任何参数给遍历的回调函数,即i2cdev_attach_adapter

  • i2cdev_attach_adapter是一个回调函数,用于将i2c适配器与i2c设备连接起来。它会接收两个参数,第一个参数是当前遍历到的i2c设备的指针,第二个参数是NULL,表示没有额外的用户数据传递给回调函数。

总的来说,这段代码的作用是遍历系统中的每个i2c设备,并调用i2cdev_attach_adapter函数将i2c适配器与i2c设备连接起来。传递给i2c_for_each_dev的参数是回调函数i2cdev_attach_adapter需要用到的参数,其中第一个参数是当前遍历到的i2c设备的指针,第二个参数是NULL

5.1i2c_for_each_dev的回调函数i2cdev_attach_adapter

我们来看第二个回调函数i2cdev_attach_adapter

static int i2cdev_attach_adapter(struct device *dev, void *dummy)
{
    struct i2c_adapter *adap;
    struct i2c_dev *i2c_dev;
    int res;
    // 检查设备类型是否为i2c_adapter_type,如果不是则返回0
    if (dev->type != &i2c_adapter_type)
        return 0;
    adap = to_i2c_adapter(dev);
    // 获取一个空闲的i2c_dev结构体
    i2c_dev = get_free_i2c_dev(adap);
    if (IS_ERR(i2c_dev))
        return PTR_ERR(i2c_dev);
    // 初始化cdev结构体,并设置文件操作函数为i2cdev_fops
    cdev_init(&i2c_dev->cdev, &i2cdev_fops);
    i2c_dev->cdev.owner = THIS_MODULE;
    // 将cdev结构体添加到内核的字符设备列表中
    res = cdev_add(&i2c_dev->cdev, MKDEV(I2C_MAJOR, adap->nr), 1);
    if (res)
        goto error_cdev;
    // 使用i2c_dev_class创建一个i2c设备,并将其与适配器关联起来
    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:
    // 如果发生错误,删除cdev结构体
    cdev_del(&i2c_dev->cdev);
error_cdev:
    // 释放i2c_dev结构体
    put_i2c_dev(i2c_dev);
    return res;
}
  • 该函数用于将一个i2c适配器与i2c-dev设备关联起来。

  • 首先,检查传入的设备类型是否为i2c_adapter_type,若不是则返回0。

  • 然后,从设备指针中获取i2c_adapter结构体。

  • 调用get_free_i2c_dev函数获取一个空闲的i2c_dev结构体。

  • 初始化i2c_dev结构体中的cdev成员,并设置文件操作函数为i2cdev_fops。

  • 将cdev结构体添加到内核的字符设备列表中,使用适配器的主设备号和次设备号。

  • 使用i2c_dev_class创建一个i2c设备,并将其与适配器关联起来。

  • 如果出现错误,删除cdev结构体,并释放i2c_dev结构体。

  • 最后,打印调试信息,显示注册成功的适配器的名称和次设备号。

我们可以看到文件操作结构体为i2cdev_fops。

具体实现如下

static const struct file_operations i2cdev_fops = {
    .owner      = THIS_MODULE,
    .llseek     = no_llseek,
    .read       = i2cdev_read,
    .write      = i2cdev_write,
    .unlocked_ioctl = i2cdev_ioctl,
    .compat_ioctl   = compat_i2cdev_ioctl,
    .open       = i2cdev_open,
    .release    = i2cdev_release,
};

之后的具体操作不在进行细致分析。

以上便是内核i2c设备驱动的大致框架。

如有错误欢迎指正。

你可能感兴趣的:(驱动开发)