前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Linux驱动之I2C子系统剖析

Linux驱动之I2C子系统剖析

作者头像
菜菜cc
发布2022-11-15 21:29:20
6.3K0
发布2022-11-15 21:29:20
举报

I2C是广泛应用于计算机中的串行总线,用于处理器和其外设之间的通信。

I2C硬件基本概念

  • I2C总线由两根传递数据的双向信号线与一根地线组成,半双工、主从方式通信。
代码语言:txt
复制
- Serial Clock Line (SCL)
- Serial Data Address (SDA)每个设备都有一个唯一设备地址,一次传输8bit,高位在前,低位在后。一次完整的I2C通信需要经历一个完整的时序,I2C总线通信完整时序如下图。一般在驱动中无需关心具体时序,只需操作SoC中的I2C控制器即可,只有在裸机下需要用GPIO模拟I2C通信时才需用到,所以笔者在本文不阐述I2C时序(其实就是懒 O__O “…)。
  • 总线速度有三种模式
代码语言:txt
复制
- 标准模式 100kbps
- 快速模式 400kbps
- 高速模式 3.4Mbps

I2C子系统框架

  • I2C设备驱动层:drivers/i2c/i2c-dev.c (通用型) 或者为特定设备定制的设备驱动(比如E2PROM驱动)
  • I2C核心层: drivers/i2c/i2c-coere.c
  • I2C总线驱动层(主机控制器驱动层):drivers/i2c/busses/i2c-s3c2410.c

I2C设备驱动层

  • 是I2C从机的驱动程序
  • 给用户提供调用接口
  • 内核提供两种方式来实现设备驱动:
    • 第一种是内核默认实现的通用型的I2C设备驱动,位于drivers/i2c/i2c-dev.c中。 这种方式仅仅只是封装了I2C的基本操作,相当于只是封装了I2C的基本时序,向应用层只提供了I2C基本操作的接口,该接口通用于所有的I2C设备。具体设备相关的操作,需要开发者在应用层根据硬件特性来完成对设备的操作。该方式的优点就是通用,而缺点也很明显,封装的不够彻底,需要应用开发人员对硬件有一定程度的了解
    • 第二种是根据特定设备来编写的特定的I2C设备驱动, 该方式彻底封装了硬件的操作,提供给应用层的接口彻底屏蔽I2C的通信细节。该方式的优点就是应用开发人员无需关心硬件

I2C核心层

  • 注册I2C总线
  • 由内核开发人员编写的,不涉及具体硬件
  • 给驱动编程人员提供编程接口

I2C总线驱动层

  • 是I2C主机适配器的驱动程序
  • 初始化I2C适配器(控制器)
  • 实现操作方法:根据I2C操作时序进行操作I2C控制器实现收发数据

源码分析

源码中会涉及到一部分SMBus相关内容,SMBus是Intel在I2C的基础上开发的类似I2C的总线,本文不探讨SMBus相关内容(其实说白了,还是懒QAQ)。笔者会大体上对I2C子系统的源码进行分析,如若分析的有出入,还望指出。

I2C核心层

I2C核心层的实现位于drivers/i2c/i2c-core.c中,笔者从i2c_init函数开始分析。

代码语言:javascript
复制
static int __init i2c_init(void)
{
    int retval;

    retval = bus_register(&i2c_bus_type);     // 注册I2C总线 
    if (retval)
        return retval;
#ifdef CONFIG_I2C_COMPAT
    i2c_adapter_compat_class = class_compat_register("i2c-adapter");
    if (!i2c_adapter_compat_class) {
        retval = -ENOMEM;
        goto bus_err;
    }
#endif
    retval = i2c_add_driver(&dummy_driver);    // 注册了一个虚假的I2C驱动
    if (retval)
        goto class_err;
    return 0;

class_err:
#ifdef CONFIG_I2C_COMPAT
    class_compat_unregister(i2c_adapter_compat_class);
bus_err:
#endif
    bus_unregister(&i2c_bus_type);
    return retval;
}

该函数先是调用了bus_register函数注册了I2C总线,随后调用i2c_add_driver函数来注册了一个虚假的I2C驱动。

先对注册的I2C总线i2c_bus_type进行分析

代码语言:javascript
复制
struct bus_type i2c_bus_type = {
    .name       = "i2c",
    .match      = i2c_device_match,
    .probe      = i2c_device_probe,
    .remove     = i2c_device_remove,
    .shutdown   = i2c_device_shutdown,
    .pm     = &i2c_device_pm_ops,
};

根据Linux设备驱动模型的原理,I2C总线下会挂载两条链表,分别为设备链和驱动链,只要其中一个链表有结点插入,即会通过i2c_device_match函数来遍历另一条链表去匹配设备与驱动,一旦匹配上则会调用i2c_device_probe函数,而i2c_device_probe函数又会调用i2c_driver的probe函数。进到i2c_device_matchi2c_device_probe进行分析。

代码语言:javascript
复制
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;

    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_device_match函数调用的是i2c_match_id函数来进行匹配。从源码中可见,需要注意的是I2C总线匹配方式不同于Platform总线,I2C总线只匹配**id_table**中的name,并不会去匹配driver中的name

代码语言:javascript
复制
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;

    driver = to_i2c_driver(dev->driver);
    if (!driver->probe || !driver->id_table)
        return -ENODEV;
    client->driver = driver;
    if (!device_can_wakeup(&client->dev))
        device_init_wakeup(&client->dev,
                    client->flags & I2C_CLIENT_WAKE);
    dev_dbg(dev, "probe\n");

    /* 调用driver中的probe函数 */
    status = driver->probe(client, i2c_match_id(driver->id_table, client));
    if (status) {
        client->driver = NULL;
        i2c_set_clientdata(client, NULL);
    }
    return status;
}

可以看到,的确是调用driver->probe来进行真正的probe。需要注意的是**if (!driver->probe || !driver->id_table) return -ENODEV;**中对**id_table**进行了非空判断,所以如果采用设备树方式进行匹配也需要对**.id_table**进行有效赋值,否则会出现match上了但probe函数不会调用的奇怪现象,个人感觉这应该是个bug,毕竟这个核心层在设备树出现之前就已经存在了。

回到i2c_init函数,然后注册了一个空的名为dummy的i2c_driver。

代码语言:javascript
复制
static int dummy_probe(struct i2c_client *client,
               const struct i2c_device_id *id)
{
    return 0;
}

static int dummy_remove(struct i2c_client *client)
{
    return 0;
}

static struct i2c_driver dummy_driver = {
    .driver.name    = "dummy",
    .probe      = dummy_probe,
    .remove     = dummy_remove,
    .id_table   = dummy_id,
};

可以看到这是一个完全空的虚假驱动,而I2C核心层为何要注册一个假的驱动不得而知,笔者查阅了网上资料也没法得知,但是/sys/bus/i2c/drivers/dummy确实存在,所以笔者猜测应该纯粹是开发该层次调试用的。

核心层还提供了一系列函数接口供驱动开发者注册和注销驱动:

  • i2c_add_adapter 注册I2C主机适配器驱动 (动态分配总线号)
  • i2c_add_numbered_adapter 注册I2C主机适配器驱动 (静态指定总线号)
  • i2c_del_adapter 注销I2C主机适配器驱动
  • i2c_add_driver 注册I2C从机设备驱动
  • i2c_del_driver 注销I2C从机设备驱动

其他函数暂不分析,在分析其他层的时候调用时再进行分析。

I2C设备驱动层

笔者先从内核提供的通用驱动开始分析,最后在文末给出特定驱动的分析。内核提供了一个通用于所有设备的I2C设备驱动,用户可以在应用层实现对I2C的驱动,其实现位于drivers/i2c/i2c-dev.c中。同样从init函数开始,笔者从i2c_dev_init函数开始分析。

代码语言:javascript
复制
static int __init i2c_dev_init(void)
{
    int res;

    printk(KERN_INFO "i2c /dev entries driver\n");

    /* 将通用驱动注册为字符设备驱动,并提供file_operations 操作方法 */
    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从机设备驱动 */
    res = i2c_add_driver(&i2cdev_driver);
    if (res)
        goto out_unreg_class;

    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;
}

i2c_dev_init函数先是调用了register_chrdev函数注册了一个字符设备驱动,并提供了一个file_operations。由此可见,是将通用驱动实现为字符设备驱动,并由其file_operations结构体的方法为应用层提供通用接口。然后调用class_create创建了一个类,但是可以看到并没有调用device_create在该类下创建设备,所以注意在这里并没有生成设备节点。最后调用i2c_add_driver注册了一个I2C从机设备驱动i2cdev_driveri2cdev_driver定义如下。

代码语言:javascript
复制
static struct i2c_driver i2cdev_driver = {
    .driver = {
        .name   = "dev_driver",
    },
    .attach_adapter = i2cdev_attach_adapter,
    .detach_adapter = i2cdev_detach_adapter,
};

从上可以看到并没有对id_table进行赋值,从上文在I2C核心层分析可知,I2C总线是根据id_table进行匹配,所以这里并不会按照常规的Linux驱动模型进行match后probe,况且这个驱动里也没有probe方法。所以这到底是什么情况?别慌,虽然没有id_table和probe,但是它单独提供了两个方法attach_adapterdetach_adapter。这里先埋个伏笔,不做分析,到I2C总线驱动层分析后自然会柳暗花明。

I2C总线驱动层

笔者使用的SoC是S5PV210,其控制器跟S3C2410基本一致,所以三星的驱动开发者并没有再去写一份S5PV210的主机适配器驱动,而是使用了S3C2410的主机适配器驱动,其位于drivers/i2c/busses/i2c-s3c2410.c中。

i2c_adap_s3c_init函数开始分析。

代码语言:javascript
复制
static int __init i2c_adap_s3c_init(void)
{
    return platform_driver_register(&s3c24xx_i2c_driver);
}

可以看到其作为平台设备驱动而实现,注册了s3c24xx_i2c_driver驱动。

代码语言:javascript
复制
static struct platform_device_id s3c24xx_driver_ids[] = {
    {
        .name       = "s3c2410-i2c",
        .driver_data    = TYPE_S3C2410,
    }, {
        .name       = "s3c2440-i2c",
        .driver_data    = TYPE_S3C2440,
    }, { },
};
MODULE_DEVICE_TABLE(platform, s3c24xx_driver_ids);

static struct platform_driver s3c24xx_i2c_driver = {
    .probe      = s3c24xx_i2c_probe,
    .remove     = s3c24xx_i2c_remove,
    .id_table   = s3c24xx_driver_ids,
    .driver     = {
        .owner  = THIS_MODULE,
        .name   = "s3c-i2c",
        .pm = S3C24XX_DEV_PM_OPS,
    },
};

根据平台总线的原理,很容易得知在arch/arm/mach-s5pv210/mach-x210.c中对其驱动对应的设备进行了注册,其注册的设备定义位于dev-i2c0.c,这是I2C的资源文件。其定义的资源如下。

代码语言:javascript
复制
static struct resource s3c_i2c_resource[] = {
    [0] = {
        .start = S3C_PA_IIC,
        .end   = S3C_PA_IIC + SZ_4K - 1,
        .flags = IORESOURCE_MEM,
    },
    [1] = {
        .start = IRQ_IIC,
        .end   = IRQ_IIC,
        .flags = IORESOURCE_IRQ,
    },
};

struct platform_device s3c_device_i2c0 = {
    .name         = "s3c2410-i2c",
    .id       = 0,
    .num_resources    = ARRAY_SIZE(s3c_i2c_resource),
    .resource     = s3c_i2c_resource,
};

由name可知,与s3c24xx_i2c_driver是匹配的。除此之外,还定义了平台数据default_i2c_data0default_i2c_data0函数。其相关的调用还是在arch/arm/mach-s5pv210/mach-x210.c中进行的,在mach-x210.c中的smdkc110_machine_init函数中进行了如下调用

代码语言:javascript
复制
/* i2c */
// 设置I2C平台数据       NULL表示设置默认的平台数据
s3c_i2c0_set_platdata(NULL);
s3c_i2c1_set_platdata(NULL);
s3c_i2c2_set_platdata(NULL);

现在进到s3c_i2c0_set_platdata函数进行分析。

代码语言:javascript
复制
static struct s3c2410_platform_i2c default_i2c_data0 __initdata = {
    .flags      = 0,
    .slave_addr = 0x10,          // I2C控制器作为从设备时使用的地址
    .frequency  = 400*1000,      // 400kbps
    .sda_delay  = S3C2410_IICLC_SDA_DELAY15 | S3C2410_IICLC_FILTER_ON,   // 间隔时间
};

void __init s3c_i2c0_set_platdata(struct s3c2410_platform_i2c *pd)
{
    struct s3c2410_platform_i2c *npd;

    if (!pd)   // 参数为NULL则使用该函数上面定义的默认的平台数据
        pd = &default_i2c_data0;

    npd = kmemdup(pd, sizeof(struct s3c2410_platform_i2c), GFP_KERNEL);
    if (!npd)
        printk(KERN_ERR "%s: no memory for platform data\n", __func__);
    else if (!npd->cfg_gpio)
        npd->cfg_gpio = s3c_i2c0_cfg_gpio;  // GPIO初始化方法

    // 设置为平台数据
    s3c_device_i2c0.dev.platform_data = npd;
}

可以看到传递NULL则使用了默认的平台数据, 将s3c_i2c0_cfg_gpio函数设置到了平台数据cfg_gpio方法中,最后将平台数据挂接到s3c_device_i2c0这个设备上。

代码语言:javascript
复制
void s3c_i2c0_cfg_gpio(struct platform_device *dev)
{
    s3c_gpio_cfgpin(S5PV210_GPD1(0), S3C_GPIO_SFN(2));      // 设置控制寄存器为I2C0_SDA模式
    s3c_gpio_setpull(S5PV210_GPD1(0), S3C_GPIO_PULL_NONE);     
    s3c_gpio_cfgpin(S5PV210_GPD1(1), S3C_GPIO_SFN(2));      // 设置控制寄存器为I2C0_SCL模式
    s3c_gpio_setpull(S5PV210_GPD1(1), S3C_GPIO_PULL_NONE);  
}

可以看到s3c_i2c0_cfg_gpio函数只是对I2C控制器两根通信线的GPIO初始化。

接下去回到I2C总线驱动层i2c-s3c2410.c中, 进入到s3c24xx_i2c_probe函数进行分析。 probe函数的代码比较多,分段进行分析。

代码语言:javascript
复制
struct s3c24xx_i2c *i2c;
struct s3c2410_platform_i2c *pdata;
struct resource *res;
int ret;

// 获取I2C平台数据
pdata = pdev->dev.platform_data;
if (!pdata) {
    dev_err(&pdev->dev, "no platform data\n");
    return -EINVAL;
}

i2c = kzalloc(sizeof(struct s3c24xx_i2c), GFP_KERNEL);
if (!i2c) {
    dev_err(&pdev->dev, "no memory for state\n");
    return -ENOMEM;
}

strlcpy(i2c->adap.name, "s3c2410-i2c", sizeof(i2c->adap.name));
i2c->adap.owner   = THIS_MODULE;
i2c->adap.algo    = &s3c24xx_i2c_algorithm;      // I2C主机控制器的操作方法
i2c->adap.retries = 2;
i2c->adap.class   = I2C_CLASS_HWMON | I2C_CLASS_SPD;
i2c->tx_setup     = 50;

三星采用struct s3c24xx_i2c结构体来对SoC的控制器进行抽象,该结构体继承于struct i2c_adapter。该段代码先是从device中获取了平台数据,该平台数据即是上文调用s3c_i2c0_set_platdata函数时设置的。然后对i2c->adap进行了相关赋值,关键部分是i2c->adap.algo = &s3c24xx_i2c_algorithm;adap.algo表示I2C主机控制器的操作方法,将该SoC的操作方法挂接到了适配器上。s3c24xx_i2c_algorithm定义了两个操作方法,主要是master_xfer方法,用来发送消息。代码如下。

代码语言:javascript
复制
static const struct i2c_algorithm s3c24xx_i2c_algorithm = {
    .master_xfer        = s3c24xx_i2c_xfer,
    .functionality      = s3c24xx_i2c_func,
};

s3c24xx_i2c_xfer涉及到对具体控制器的操作,不进行展开,但是注意的是其内部调用的是s3c24xx_i2c_doxfer,在s3c24xx_i2c_doxfer函数内部发送完数据后,调用wait_event_timeout函数来进行睡眠等待从机响应。因此可知内核中I2C的等待从机的ACK信号是通过中断实现的,即主机发送完数据后进入睡眠等待从机,从机响应后通过中断通知主机后唤醒。

probe函数接着做了获取时钟和使能时钟,相关代码如下。

代码语言:javascript
复制
// 获取时钟
    i2c->clk = clk_get(&pdev->dev, "i2c");

    if (IS_ERR(i2c->clk)) {
        dev_err(&pdev->dev, "cannot get clock\n");
        ret = -ENOENT;
        goto err_noclk;
    }

    dev_dbg(&pdev->dev, "clock source %p\n", i2c->clk);

    // 使能时钟
    clk_enable(i2c->clk);

紧接着对具体IO和IRQ进行操作。

代码语言:javascript
复制
// 获取I2C平台资源(IO内存地址、IRQ)
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (res == NULL) {
    dev_err(&pdev->dev, "cannot find IO resource\n");
    ret = -ENOENT;
    goto err_clk;
}

i2c->ioarea = request_mem_region(res->start, resource_size(res),
                 pdev->name);

if (i2c->ioarea == NULL) {
    dev_err(&pdev->dev, "cannot request IO\n");
    ret = -ENXIO;
    goto err_clk;
}

// 将物理地址映射为虚拟地址
i2c->regs = ioremap(res->start, resource_size(res));

if (i2c->regs == NULL) {
    dev_err(&pdev->dev, "cannot map IO\n");
    ret = -ENXIO;
    goto err_ioarea;
}

dev_dbg(&pdev->dev, "registers %p (%p, %p)\n",
    i2c->regs, i2c->ioarea, res);

/* setup info block for the i2c core */

i2c->adap.algo_data = i2c;
i2c->adap.dev.parent = &pdev->dev;

/* initialise the i2c controller */

// 初始化I2C控制器
ret = s3c24xx_i2c_init(i2c);    
if (ret != 0)
    goto err_iomap;

// 获取IRQ资源
i2c->irq = ret = platform_get_irq(pdev, 0);   
if (ret <= 0) {
    dev_err(&pdev->dev, "cannot find IRQ\n");
    goto err_iomap;
}

// 申请IRQ (裸机一般使用查询法来判断从机的响应,而内核一般采用中断方式等待从机响应)
ret = request_irq(i2c->irq, s3c24xx_i2c_irq, IRQF_DISABLED,
          dev_name(&pdev->dev), i2c);

把关注点放在初始化I2C控制器的s3c24xx_i2c_init函数和申请IRQ上。

代码语言:javascript
复制
static int s3c24xx_i2c_init(struct s3c24xx_i2c *i2c)
{
    unsigned long iicon = S3C2410_IICCON_IRQEN | S3C2410_IICCON_ACKEN;
    struct s3c2410_platform_i2c *pdata;
    unsigned int freq;

    /* get the plafrom data */

    pdata = i2c->dev->platform_data;

    /* inititalise the gpio */

    if (pdata->cfg_gpio)
        pdata->cfg_gpio(to_platform_device(i2c->dev));      // 设置I2C对应的管脚

    /* write slave address */
    // 设置I2C控制器作为从设备时的地址
    writeb(pdata->slave_addr, i2c->regs + S3C2410_IICADD);  

    dev_dbg(i2c->dev, "slave address 0x%02x\n", pdata->slave_addr);

    writel(iicon, i2c->regs + S3C2410_IICCON);        // 使能 Tx/Rx Interrupt 和 ACK信号

    /* we need to work out the divisors for the clock... */

    // 配置I2C的时钟频率
    if (s3c24xx_i2c_clockrate(i2c, &freq) != 0) {
        writel(0, i2c->regs + S3C2410_IICCON);
        dev_err(i2c->dev, "cannot meet bus frequency required\n");
        return -EINVAL;
    }

    /* todo - check that the i2c lines aren't being dragged anywhere */

    dev_dbg(i2c->dev, "bus frequency set to %d KHz\n", freq);
    dev_dbg(i2c->dev, "S3C2410_IICCON=0x%02lx\n", iicon);

    dev_dbg(i2c->dev, "S3C2440_IICLC=%08x\n", pdata->sda_delay);
    writel(pdata->sda_delay, i2c->regs + S3C2440_IICLC);

    return 0;
}

可以看到设置I2C对应的管脚是调用平台数据中的cfg_gpio,其实看到这里如果还有印象的话就能反应出来这是在调用s3c_i2c0_set_platdata中设置的。该函数还设置了I2C控制器的从地址,该地址用来在控制器作为从地址时使用,但是这种情况的出现微乎其微。除此之外使能Tx/Rx Interrupt和ACK信号,配置了I2C的时钟频率。

注意从前一段分析中得知,内核中I2C采用中断方式等待从机响应,所以probe函数这一段代码中申请了IRQ并绑定了中断处理函数s3c24xx_i2c_irq

代码语言:javascript
复制
static irqreturn_t s3c24xx_i2c_irq(int irqno, void *dev_id)
{
    struct s3c24xx_i2c *i2c = dev_id;
    unsigned long status;
    unsigned long tmp;

    // 获取I2CSTAT寄存器的值
    status = readl(i2c->regs + S3C2410_IICSTAT);

    if (status & S3C2410_IICSTAT_ARBITR) {   // I2C总线仲裁失败
        /* deal with arbitration loss */
        dev_err(i2c->dev, "deal with arbitration loss\n");
    }

    if (i2c->state == STATE_IDLE) {
        dev_dbg(i2c->dev, "IRQ: error i2c->state == IDLE\n");

        tmp = readl(i2c->regs + S3C2410_IICCON);
        tmp &= ~S3C2410_IICCON_IRQPEND;
        writel(tmp, i2c->regs +  S3C2410_IICCON);
        goto out;
    }

    /* pretty much this leaves us with the fact that we've
     * transmitted or received whatever byte we last sent */

    // 处理I2C的收发数据
    i2c_s3c_irq_nextbyte(i2c, status);

 out:
    return IRQ_HANDLED;
}

具体也不展开分析了,但是要注意的是有这么一条线:该中断处理函数调用了i2c_s3c_irq_nextbyte,然后内部调用了s3c24xx_i2c_stop,再内部调用了s3c24xx_i2c_master_complete,最后再内部执行了一个关键代码wake_up(&i2c->wait);,这就是通过中断方式唤醒之前在发送数据时进行的睡眠等待。

回到probe函数,最后分析重头戏。

代码语言:javascript
复制
ret = i2c_add_numbered_adapter(&i2c->adap);
if (ret < 0) {
    dev_err(&pdev->dev, "failed to add bus to i2c core\n");
    goto err_cpufreq;
}

该代码将I2C适配器注册到了内核中。i2c_add_numbered_adapter函数由核心层提供,其定义位于I2C核心层drivers/i2c/i2c-core.c中,用来注册I2C适配器。其实在内核中提供了两个adapter注册接口,分别为i2c_add_adapteri2c_add_numbered_adapter由于在系统中可能存在多个adapter, 所以将每一条I2C总线(控制器)对应一个编号,这个总线号(可以称这个编号为总线号码)与PCI中的总线号不同。它和硬件无关, 只是软件上便于区分而已。对于i2c_add_adapter而言, 它使用的是动态总线号, 即由系统给其分配一个总线号, 而i2c_add_numbered_adapter则是自己指定总线号, 如果这个总线号非法或者是被占用, 就会注册失败。不管哪个注册接口,其核心都是调用i2c_register_adapter函数来进行真正的注册。取出i2c_register_adapter函数的关键部分进行分析。

代码语言:javascript
复制
res = device_register(&adap->dev);

if (adap->nr < __i2c_first_dynamic_bus_num)
    i2c_scan_static_board_info(adap);

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

device_register(&adap->dev);表示主机适配器adapter的注册。

i2c_scan_static_board_info(adap);内部先遍历__i2c_board_list取出板卡信息(描述的是板子上的I2C外设的信息,即I2C从机的信息),该链表的生成是在arch/arm/mach-s5pv210/mach-x210.c中进行的,在mach-x210.c中的smdkc110_machine_init函数中进行了除之前分析的调用s3c_i2c0_set_platdata外,还调用了i2c_register_board_info对板卡信息进行了注册。

代码语言:javascript
复制
int __init
i2c_register_board_info(int busnum,
    struct i2c_board_info const *info, unsigned len)
{
    int status;

    down_write(&__i2c_board_lock);

    /* dynamic bus numbers will be assigned after the last static one */
    // __i2c_first_dynamic_bus_num为全局未显式初始化变量,所以第一次进到这个函数,值为0
    if (busnum >= __i2c_first_dynamic_bus_num)
        __i2c_first_dynamic_bus_num = busnum + 1;

    for (status = 0; len; len--, info++) {
        struct i2c_devinfo  *devinfo;

        devinfo = kzalloc(sizeof(*devinfo), GFP_KERNEL);
        if (!devinfo) {
            pr_debug("i2c-core: can't register boardinfo!\n");
            status = -ENOMEM;
            break;
        }

        devinfo->busnum = busnum;
        devinfo->board_info = *info;
        list_add_tail(&devinfo->list, &__i2c_board_list);    // 将board_info用链表管理起来 
    }

    up_write(&__i2c_board_lock);

    return status;
}

板卡信息的描述,主要对其设备名和从地址进行赋值,示例如下

代码语言:javascript
复制
#define I2C_BOARD_INFO(dev_type, dev_addr) \
    .type = dev_type, .addr = (dev_addr)

#ifdef CONFIG_TOUCHSCREEN_GSLX680
    {
        I2C_BOARD_INFO("gslX680", 0x40),  // 主要对其设备名和从地址进行赋值
    },
#endif

然后在i2c_scan_static_board_info内部利用板卡信息作为原料调用i2c_new_device来创建了client,表示从机设备,并将adapter挂接到了client结构体内部的指针上。i2c_scan_static_board_info代码如下。

代码语言:javascript
复制
static void i2c_scan_static_board_info(struct i2c_adapter *adapter)
{
    struct i2c_devinfo  *devinfo;

    down_read(&__i2c_board_lock);
    // __i2c_board_list在调用i2c_register_board_info时链接起来的
    list_for_each_entry(devinfo, &__i2c_board_list, list) {
        if (devinfo->busnum == adapter->nr
                && !i2c_new_device(adapter,
                        &devinfo->board_info))
            dev_err(&adapter->dev,
                "Can't create device at 0x%02x\n",
                devinfo->board_info.addr);
    }
    up_read(&__i2c_board_lock);
}

创建完client后,回到i2c_register_adapter函数,最后执行了dummy = bus_for_each_drv(&i2c_bus_type, NULL, adap, __process_new_adapter);该函数是遍历在I2C总线上已经注册的driver,通过回调**__process_new_adapter**函数的方式,遍历到i2c-dev这个通用驱动后就会用其**i2cdev_attach_adapter**方法来挂接到在i2c-dev中注册的字符设备驱动,并使用这个字符设备驱动的主设备号和adapter中的总线号(作为次设备号)来创建名为i2c-x的设备节点,应用层访问这个设备节点后即可调用在i2c-dev中注册的file_operations中的操作方法,从操作方法源码知,最终读写调用的是adapter中的读写方法(即在本平台中为i2c-s3c2410.c中定义的方法)。下面对其进行验证。

__process_new_adapter展开如下

代码语言:javascript
复制
static int i2c_do_add_adapter(struct i2c_driver *driver,
                  struct i2c_adapter *adap)
{
    /* Detect supported devices on that bus, and instantiate them */
    i2c_detect(adap, driver);

    /* Let legacy drivers scan this bus for matching devices */
    if (driver->attach_adapter) {
        /* We ignore the return code; if it fails, too bad */
        driver->attach_adapter(adap);   // 调用i2c-dev中的i2cdev_attach_adapter方法
    }
    return 0;
}

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

可以看到driver->attach_adapter(adap);,的确是调用I2C总线下的驱动中的attach_adapter方法,到了这里在I2C设备驱动层埋下的悬念终于要水落石出了(不容易啊啊啊啊啊啊),穿越回到I2C设备驱动层进行分析,进入drivers/i2c/i2c-dev.c分析i2cdev_attach_adapter方法。

代码语言:javascript
复制
static int i2cdev_attach_adapter(struct i2c_adapter *adap)
{
    struct i2c_dev *i2c_dev;
    int res;

    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 */
    /* 使用主设备号和adapter中的总线号(作为次设备号)来创建名为i2c-x的设备节点 */
    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;
    }
    res = device_create_file(i2c_dev->dev, &dev_attr_name);
    if (res)
        goto error_destroy;

    pr_debug("i2c-dev: adapter [%s] registered as minor %d\n",
         adap->name, adap->nr);
    return 0;
error_destroy:
    device_destroy(i2c_dev_class, MKDEV(I2C_MAJOR, adap->nr));
error:
    return_i2c_dev(i2c_dev);
    return res;
}

i2c_dev->dev = device_create(i2c_dev_class, &adap->dev, MKDEV(I2C_MAJOR, adap->nr), NULL, "i2c-%d", adap->nr);使用主设备号和adapter中的总线号(作为次设备号)来创建名为i2c-x的设备节点。

代码语言:javascript
复制
static ssize_t i2cdev_write(struct file *file, const char __user *buf,
        size_t count, loff_t *offset)
{
    int ret;
    char *tmp;
    // 取出i2c_client
    struct i2c_client *client = file->private_data;

    if (count > 8192)
        count = 8192;

    tmp = kmalloc(count, GFP_KERNEL);
    if (tmp == NULL)
        return -ENOMEM;
    // 拷贝用户数据到内核空间
    if (copy_from_user(tmp, buf, count)) {
        kfree(tmp);
        return -EFAULT;
    }

    pr_debug("i2c-dev: i2c-%d writing %zu bytes.\n",
        iminor(file->f_path.dentry->d_inode), count);

    // 发送I2C数据
    ret = i2c_master_send(client, tmp, count);
    kfree(tmp);
    return ret;
}

以write函数为例,可以看到写数据通过ret = i2c_master_send(client, tmp, count);完成的。

代码语言:javascript
复制
int i2c_master_send(struct i2c_client *client, const char *buf, int count)
{
    int ret;
    // 获取I2C适配器
    struct i2c_adapter *adap = client->adapter;
    struct i2c_msg msg;

    // 封装I2C数据包
    msg.addr = client->addr;
    msg.flags = client->flags & I2C_M_TEN;   // 发送标志位
    msg.len = count;
    msg.buf = (char *)buf;

    // 发送I2C数据包
    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;
}

可以看到,经过I2C数据包的封装后,真正的最终写数据通过ret = i2c_transfer(adap, &msg, 1);完成的。进入到i2c_transfer函数,截取关键部分。

代码语言:javascript
复制
for (ret = 0, try = 0; try <= adap->retries; try++) {
    // 调用具体的SoC的I2C总线驱动的发送方法
    ret = adap->algo->master_xfer(adap, msgs, num);
    if (ret != -EAGAIN)
        break;
    if (time_after(jiffies, orig_jiffies + adap->timeout))
        break;
}

山回路转不见君,雪上空留马行处。

adap->algo->master_xfer(adap, msgs, num);终于回到了原点见到了I2C总线驱动层中定义的操作方法。

可以看到过程的确如上文所说,表现为从I2C总线驱动层自底向上后又由自顶向下的调用流程,简直一跃千里后又倾泻而下。

I2C特定设备驱动分析

笔者以S5PV210的E2PROM驱动为例讲解, 源码见github链接

代码语言:javascript
复制
struct e2prom_device {
    struct i2c_client *at24c02_client;   /* I2C client(从设备) */
    /* class和device用来自动创建设备节点 */
    struct class      *at24c02_class;
    struct device     *at24c02_device;
};

struct e2prom_device *e2prom_dev;

封装一个e2prom_device结构体表示对E2PROM的抽象,其中包含I2C client(用来表示I2C从设备)以及class和device(这两者单纯是用来自动创建设备节点的)。

代码语言:javascript
复制
struct i2c_device_id e2prom_table[] = {
    [0] = {
        .name         = "24c02",
        .driver_data  = 0,
    },
    [1] = {
        .name         = "24c08",
        .driver_data  = 0,
    },
};

/* I2C设备驱动 */
struct i2c_driver e2prom_driver = {
    .probe     =  e2prom_probe,
    .remove    =  e2prom_remove,
    .id_table  =  e2prom_table,
    .driver    = {
        .name = "e2prom",
    },
};

static int __init e2prom_init(void)
{
    return i2c_add_driver(&e2prom_driver);   /* 注册I2C设备驱动 */
}

先是调用i2c_add_driver注册I2C设备驱动。根据上文在I2C核心层的源码分析可知,会通过在核心层中注册的i2c_bus_type下的i2c_device_match函数来匹配设备与驱动,一旦匹配上则会调用其i2c_device_probe函数,而i2c_device_probe函数又会调用i2c_driver的probe函数。注意如上文分析所知,client生成的原料为board_info,所以要使这个驱动成功匹配,需要在arch/arm/mach-s5pv210/mach-x210.c中使用i2c_register_board_info来注册board_info。接下去直奔prob函数进行分析。

代码语言:javascript
复制
struct file_operations e2prom_fops = {
    .owner = THIS_MODULE,
    .open  = e2prom_open,
    .write = e2prom_write,
    .read =  e2prom_read,
};

static int e2prom_probe(struct i2c_client *client, const struct i2c_device_id *id)
{
    int ret;

    printk(KERN_INFO "e2prom probe!\n");
    e2prom_dev = kmalloc(sizeof(struct e2prom_device), GFP_KERNEL);
    if (!e2prom_dev) {
        printk(KERN_ERR "malloc failed!\n");
        return -ENOMEM;
    }

    e2prom_dev->at24c02_client = client;

    /* 注册为字符设备驱动 */
    ret = register_chrdev(E2PROM_MAJOR, "e2prom_module", &e2prom_fops);
    if (ret < 0) {
        printk(KERN_ERR "malloc failed\n");
        ret = -ENOMEM;
        goto err0;
    }

    /* 创建类  */
    e2prom_dev->at24c02_class = class_create(THIS_MODULE, "e2prom_class");
    if (IS_ERR(e2prom_dev->at24c02_class)) {
        printk(KERN_ERR "class create failed!\n");
        ret = PTR_ERR(e2prom_dev->at24c02_class);
        goto err1;
    }

    /* 在类下创建设备 */
    e2prom_dev->at24c02_device = device_create(e2prom_dev->at24c02_class, NULL, MKDEV(E2PROM_MAJOR, 0), NULL, "at24c08");
    if (IS_ERR(e2prom_dev->at24c02_device)) {
        printk(KERN_ERR "class create failed!\n");
        ret = PTR_ERR(e2prom_dev->at24c02_device);
        goto err1;
    }

    return 0;
err1:
    unregister_chrdev(E2PROM_MAJOR, "e2prom_module");
err0:
    kfree(e2prom_dev);
    ret

在probe函数中调用register_chrdev函数来将E2PROM驱动注册为了字符设备驱动,并绑定了fops。然后调用class_createdevice_create自动生成设备节点。

代码语言:javascript
复制
static int e2prom_open(struct inode *inode, struct file *file)
{
    return 0;
}

open方法为空,以write方法为例讲解具体的操作,read方法类似。

代码语言:javascript
复制
static ssize_t e2prom_write(struct file *file, const char __user *buf,
        size_t size, loff_t *offset)
{
    int ret = 0;
    char *tmp;
    tmp = kmalloc(size, GFP_KERNEL);
    if (tmp == NULL) {
        printk(KERN_ERR "mallo failed!\n");
        return -ENOMEM;
    }

    /* 将用户空间数据拷贝到内核空间 */
    ret = copy_from_user(tmp, buf, size);
    if (ret) {
        printk("copy data faile!\n");
        goto err0;
    }

    /* I2C write */
    ret = i2c_write_byte(tmp, size);
    if (ret) {
        printk(KERN_ERR "wrtie byte failed!\n");
        goto err0;
    }

    kfree(tmp);
    return size;

err0:
    kfree(tmp);
    return -EINVAL;
}

可以看到真正的操作I2C在i2c_write_byte函数。

代码语言:javascript
复制
static int i2c_write_byte(char *buf, int count)
{
    int ret = 0;
    struct i2c_msg msg;

    /* 封装I2C数据包 */
    msg.addr   = e2prom_dev->at24c02_client->addr; /* I2C从设备地址 */
    msg.flags  = 0;                                /* write flag */
    msg.len    = count;                            /* 数据长度 */
    msg.buf    = buf;                              /* 写入的数据 */

    /* 调用I2C核心层提供的传输函数,其本质还是调用的I2C总线驱动(主机控制器驱动)层下实现的algo->master_xfe方法 */
    ret = i2c_transfer(e2prom_dev->at24c02_client->adapter, &msg, 1);
    if (ret < 0) {
        printk(KERN_ERR "i2c transfer failed!\n");
        return -EINVAL;
    }
    return ret;
}

可以看到是调用在I2C核心层提供的传输函数,其本质还是在传输函数内部调用了跟具体SoC相关的I2C主机控制器操作方法中的传输方法。该函数接口需要提供一个i2c_msg,所以对其进行了创建并填充,注意msg.flags = 0;中0表示写,1表示读。

终了,撒花!!!✿✿✿ ~

本文作者: Ifan Tsai  (菜菜)

本文链接: https://cloud.tencent.com/developer/article/2164591

版权声明: 本文采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-06-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • I2C硬件基本概念
  • I2C子系统框架
    • I2C设备驱动层
      • I2C核心层
        • I2C总线驱动层
        • 源码分析
          • I2C核心层
            • I2C设备驱动层
              • I2C总线驱动层
              • I2C特定设备驱动分析
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档