Linux驱动之I2C子系统剖析

2022-11-15 21:29:20 浏览数 (2)

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, "proben");

    /* 调用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 drivern");

    /* 将通用驱动注册为字符设备驱动,并提供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 failedn", __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 datan", __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 datan");
    return -EINVAL;
}

i2c = kzalloc(sizeof(struct s3c24xx_i2c), GFP_KERNEL);
if (!i2c) {
    dev_err(&pdev->dev, "no memory for staten");
    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 clockn");
        ret = -ENOENT;
        goto err_noclk;
    }

    dev_dbg(&pdev->dev, "clock source %pn", 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 resourcen");
    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 IOn");
    ret = -ENXIO;
    goto err_clk;
}

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

if (i2c->regs == NULL) {
    dev_err(&pdev->dev, "cannot map IOn");
    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 IRQn");
    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 0xxn", 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 requiredn");
        return -EINVAL;
    }

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

    dev_dbg(i2c->dev, "bus frequency set to %d KHzn", freq);
    dev_dbg(i2c->dev, "S3C2410_IICCON=0xlxn", iicon);

    dev_dbg(i2c->dev, "S3C2440_IICLC=xn", 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 lossn");
    }

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

        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 coren");
    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 0xxn",
                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 %dn",
         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 failedn");
        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 国际许可协议 进行许可。转载请注明出处!

0 人点赞