【linux iic子系统】i2c整体框图【精髓部分】(五)

9 篇文章 5 订阅

经过之前文章介绍,读者可能还未对i2c子系统有整体的认识,没关系,这篇文章可以帮助你。

前言

我们知道,注册一个字符设备驱动可以向上提供字符设备节点,比如/dev/xxx节点,我们对这个节点进行write/read操作,最终就会调用到字符驱动提供的write/read函数,以完成我们想要实现的功能。同样i2c设备与驱动匹配之后,也是注册设备节点供用户操作,注意的是,这里说的设备节点包含字符设备和块设备节点,本文以字符设备为例进行讲解。

如果对字符设备驱动没有概念的读者,请先查阅相关文章学习。对于接触过字符设备驱动的读者,也可参阅以下文章:

https://blog.csdn.net/zz2633105/article/details/116898695?spm=1001.2014.3001.5501

i2c总线设备模型

Linux内核启动过程中会注册一条虚拟的i2c总线,以便i2c设备与i2c驱动挂靠,注意,这里说的总线是总线设备模型中的总线,并不对应实物上的总线,为了与后面实际总线进行区分,后续将这条虚拟的i2c总线称为i2c_bus_type。

我们所说的i2c设备对应的软件结构i2c_client与i2c驱动对应的软件结构i2c_driver均挂在i2c_bus_type上,除此之外,i2c控制器(i2c_adapter)也是作为设备挂在i2c_bus_type上的,但它并不是i2c设备类型!i2c设备与i2c驱动挂接简图如下图所示:

根据前面《i2c设备与驱动匹配过程》文章我们知道,设备与驱动匹配成功后最终会执行i2c驱动的probe函数,那么,我们在probe函数中注册字符设备节点,不就提供了与字符设备驱动一样的接口了吗?事实上,部分i2c设备的驱动就是这样做的,比如温度传感器tmp75,其驱动内部实现tmp75温度读取函数,再由标准字符设备驱动接口ioctl调用读函数向上层返回温度数据,后续文章将会提供示例代码。

i2c整体框图

话不多说直接上图(内核版本linux4.18.0)。

简要讲解一下:

  • i2c_adapter与i2c_client通过内部dev成员挂接在i2c_bus_type上,而I2c_driver通过内部driver成员挂接在i2c_bus_type上。
  • 一个i2c_driver可以匹配多个i2c_client,但一个i2c_client只能挂在一个i2c_driver下。
  • 每一个i2c_adapter对应一条实际i2c总线,在总线上挂着i2c设备实物,每个i2c设备对应一个i2c_client(i2c_client0对应i2c设备0),而i2c_driver是纯软件概念,没有对应实物。
  • 一般来说,i2c_driver会给每个i2c设备注册设备节点(以字符设备节点为例),向用户层提供标准操作接口,如write/read/ioctl,执行这些接口最终会调用到i2c驱动中的对应接口,完成想要功能。
  • 一般来说,i2c_driver是通过调用i2c_adapter提供的通信方法,完成操作i2c设备,比如tmp75的驱动控制器i2c适配器发出满足tmp75手册要求的读时序,完成读取tmp75的温度数据。

使用单片机读写过I2c设备的人都知道,通过GPIO引脚模拟i2c时序即可读写i2c设备,既然i2c_adapter已经提供了通信方法,那直接操作i2c_adapter不就可以读写i2c设备了吗?的确是的,i2c子系统也为每个i2c适配器注册了字符设备节点,如上图右上方所示,我们可以操作i2c-0节点来完成读写i2c设备0,如通过i2c-0提供的ioctl接口+I2C_SLAVE命令字即可设置i2c设备地址,再通过ioctl接口+I2C_RDWR命令字即可读数据,关于直接使用i2c-0来操作i2c设备的具体方法不在这详细讲解,读者可自行搜索关键字:linux i2c 用户态读写。

谁为i2c_adapter提供的操作接口和注册设备节点的?

在i2c核心有一个文件叫i2c-dev.c,文件中向系统注册了一个模块。

代码图1

723行占用了字符驱动设备号(89),但并未实际注册字符驱动设备。735行向系统中注册了通知链,去监测i2c总线上设备注册!当i2c总线上设备注册/删除并呼唤通知链时,则会调用i2cdev_notifier结构体中的.notifier_call指向的函数!

代码图2

如果总线上是添加设备,则调用i2cdev_attach_adapter函数,如果是删除设备则调用i2cdev_detach_adapter函数。

代码图3

如果总线上是添加设备是i2c适配器(638行),则使用之前占用的设备号向系统中注册字符设备驱动,并提供fops操作方法集。653行创建了名为i2c-x的设备节点。

通过上面所述可以知道,当i2c适配器注册到系统中并呼唤通知链后会触发i2cdev_attach_adapter函数执行,进而为i2c适配器注册一个字符驱动,那么这里就有一个疑问了,如果i2c适配器在i2c-dev.c模块注册之前提前注册到了系统怎么办?

回到代码图1截图740行,模块注册时会调用i2c_for_each_dev!这个函数会去遍历i2c总线上所有设备,执行i2cdev_attach_adapter函数为提前注册到i2c总线上的i2c适配器注册字符驱动!

其中fn函数就是i2cdev_attach_adapter函数。

上面代码图1的解释中提到“735行向系统中注册了通知链,去监测i2c总线上设备注册”,所以我们再看看i2c适配器注册会发生什么?

i2c适配器注册会发生什么?

i2c适配器注册有两个函数,i2c_add_numbered_adapter与i2c_add_adapter,它们最终都会调用i2c_register_adapter函数,其代码简化后如下:

  static int i2c_register_adapter(struct i2c_adapter *adap)
    {
        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 (adap->nr < __i2c_first_dynamic_bus_num)
            i2c_scan_static_board_info(adap);

        mutex_lock(&core_lock);
        bus_for_each_drv(&i2c_bus_type, NULL, adap, __process_new_adapter);
        mutex_unlock(&core_lock);
    }

首先设置设备为i2c_adapter_type类型,然后调用device_register函数将i2c适配注册到i2c总线上并呼唤通知链,接着如果是静态注册的话,调用i2c_scan_static_board_info函数将__i2c_board_list链表上对应总线号的设备实例化注册进i2c总线中,最后调用bus_for_each_drv函数遍历i2c总线上所有i2c驱动,使用它们的i2c设备地址组中地址探测该i2c适配器上是否有响应设备,有的话便实例化后注册进i2c总线。我们先看看device_register(直接调用device_add)函数,其代码简化后如下:

    int device_add(struct device *dev)
    {
        error = device_add_attrs(dev);//创建sys目录下设备其他属性文件  
        error = bus_add_device(dev);//将设备添加到对应总线,重要

        device_pm_add(dev);//添加设备到激活设备列表中,用于电源管理

        if (dev->bus)//呼唤通知链表,通知注册监听该总线的设备,有新设备加入
            blocking_notifier_call_chain(&dev->bus->p->bus_notifier,
                             BUS_NOTIFY_ADD_DEVICE, dev);
        bus_probe_device(dev);//在总线上寻找对应的driver
    }

首先将i2c适配注册到I2c总线上,然后呼唤通知链,事件类型为BUS_NOTIFY_ADD_DEVICE(对应左侧i2c-dev模块监测类型),最后调用bus_probe_device设备匹配驱动函数,这个函数是用于i2c设备匹配i2c总线上驱动的,所以虽然i2c适配器也会调用这个函数,但在函数内部调用到i2c总线上的match函数后,因为其类型i2c_adapter_type是而不是i2c_client_type,因此直接结束。i2c_scan_static_board_info函数,用途在设备添加方法中简要介绍了一下,具体情况可参阅《i2c设备的添加方法》文章。接下来看看bus_for_each_drv函数做了什么:   

 bus_for_each_drv(&i2c_bus_type, NULL, adap, __process_new_adapter);

    int bus_for_each_drv(struct bus_type *bus, struct device_driver *start,
             void *data, int (*fn)(struct device_driver *, void *))
    {
        klist_iter_init_node(&bus->p->klist_drivers, &i,
                     start ? &start->p->knode_bus : NULL);
        while ((drv = next_driver(&i)) && !error)
            error = fn(drv, data);
        klist_iter_exit(&i);
    }

    static int __process_new_adapter(struct device_driver *d, void *data)
    {
        return i2c_do_add_adapter(to_i2c_driver(d), data);
    }

    static int i2c_do_add_adapter(struct i2c_driver *driver, struct i2c_adapter *adap)
    {
        i2c_detect(adap, driver);
    }

    static int i2c_detect(struct i2c_adapter *adapter, struct i2c_driver *driver)
    {
        if (!driver->detect || !address_list)//驱动链表不为空
            return 0;
        if (!(adapter->class & driver->class))//i2c驱动支持该i2c适配器类型
            return 0;

        err = i2c_detect_address(temp_client, driver);
    }

    static int i2c_detect_address(struct i2c_client *temp_client, struct i2c_driver *driver)
    {
        err = i2c_check_7bit_addr_validity_strict(addr);//检查是否为7位有效地址
        if (i2c_check_addr_busy(adapter, addr))//跳过已经使用的i2c设备
        if (!i2c_default_probe(adapter, addr))//检查这个地址是否有回应
        info.addr = addr;//执行到这,则i2c设备存在,调用i2c驱动的detect函数
        err = driver->detect(temp_client, &info);//函数中最少填充i2c设备名
        client = i2c_new_device(adapter, &info);//实例化i2c设备并注册待i2c总线
        if (client)//不为空的话,将i2c设备挂载到i2c驱动链表上
            list_add_tail(&client->detected, &driver->clients);
    }

首先检查有效性、是否有设备回应、是否被使用,之后初始化了i2c_board_info结构,注意只初始化了地址(实例化设备必须还要名字),然后调用了i2c驱动中的detect函数,如果成功则调用 i2c_new_device函数真正实例化i2c设备,并且将i2c设备挂在i2c驱动的链表上!i2c驱动注册时也会调用函数i2c_do_add_adapter函数,具体情况可参阅《i2c设备的添加方法》文章。

 

读者到这有没有什么疑问?是谁向i2c总线驱动模型中注册了i2c_adatper结构,并提供通信方法(algorithm)的呢?

看下图,简要解释一下你就明白了。

platform_bus_type是虚拟的平台总线,pci_bus_type是虚拟的pci总线,注意了,pci_bus_type是总线驱动模型中的概念,与实际的pci总线是两回事。

i2c_adapter的一般通过两种方法注册(当然,你也可以写一个模块暴力注册i2c_adapter,只要你开心就好)。

  • 一种做法是为i2c适配器创建一个platform设备注册到platform_bus_type总线上与i2c适配器的platform驱动匹配,在驱动的probe函数中向i2c_bus_type注册i2c_adapter和提供algorithm通信方法!
  • 另一种做法是i2c适配器作为pci设备注册到pci_bus_type总线上与i2c适配器的pci驱动匹配,在驱动的probe函数中向i2c_bus_type注册i2c_adapter和提供algorithm通信方法!需要注意的是,pci_device是系统自行创建的,这部分知识将在pci专栏中介绍。

 

这里简单提了一下i2c_adapter注册方法,想要详细了解的可以看下一篇文章《i2c-designware框架分析》。

  • 7
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LinuxI2C子系统是用于在Linux内核中管理和操作I2C总线的子系统。它提供了一组API和驱动程序,允许用户空间应用程序与连接到I2C总线上的设备进行通信。 在Linux中,I2C子系统由以下几个主要组件组成: 1. I2C核心驱动程序:这是I2C子系统的核心部分,负责管理I2C总线和设备的注册、协议处理等功能。它提供了一组API供其他驱动程序或用户空间应用程序使用。 2. I2C适配器驱动程序:这些驱动程序用于支持特定的硬件I2C适配器,如FPGA、SOC等。它们与I2C核心驱动程序紧密配合,负责将硬件特定的操作转换为通用的I2C操作。 3. I2C设备驱动程序:这些驱动程序用于支持连接到I2C总线上的具体设备。每个I2C设备都有一个对应的设备驱动程序,负责处理设备的初始化、通信协议等。在Linux中,这些设备驱动程序通常作为内核模块存在。 4. I2C工具和库:除了内核驱动程序外,Linux还提供了一些用户空间工具和库,用于与I2C设备进行交互。例如,`i2cdetect`工具用于检测I2C总线上的设备地址,`i2cget`和`i2cset`工具用于读取和写入I2C设备的寄存器值。 用户空间应用程序可以使用I2C子系统提供的API和工具来访问和控制连接到I2C总线上的设备。通过打开适当的设备节点文件,并使用相应的读写操作,可以向设备发送命令和数据,以及从设备读取响应和数据。 总而言之,LinuxI2C子系统提供了一套完整的解决方案,使用户能够方便地在Linux环境中操作和管理I2C设备。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值