linux下I2C驱动体系结构及在FL2440开发板上的具体实现


作者:郭志超 来源:凌云物网智科实验室 时间:2017-05-11

最近一段时间,我在网上看了一些关于linux下i2c的文档,对i2c有了一些较浅层次了解。写这篇博客,主要是对现在已经掌握知识的巩固。

Linux下I2C驱动体系结构

Linux下I2C驱动体系结构由 三大部分 构成:

1. I2C核心

I2C核心提供了I2C总线驱动和设备驱动的注册、注销方法。它也提供了I2C通信方法(algorithm)上层的、与具体适配器无关的代码 i2c_transfer() 函数,并通过这个函数将总线驱动、用户层串起来。

2.I2C总线驱动

I2C总线驱动就是linux内核中I2C适配器的软件实现,它告诉I2C适配器该怎么工作,从而实现I2C适配器与从设备的通信功能。

我们可以通过这个驱动,使用i2c总线去操控挂接在总线上的设备。

I2C总线驱动由 i2c_adapteri2c_algorithm 这两个结构体来描述。

3.I2C设备驱动

I2C设备驱动是对I2C从设备的软件实现,包括对设备的读、写等操作。

一个具体的I2C设备驱动包括两个部分:

    1.  ①设备驱动代码。用于将设备注册到内核中,并提供了对设备的读写操作。
              ②设备硬件信息i2c_client。

我们也可以通过设备驱动,跳过i2c总线,直接对设备进行操控。

I2C设备驱动由 i2c_driveri2c_client 这两个结构体来描述。

从上面我们知道了,对于i2c总线上的设备,我们可以通过两种不同的方式去控制。一种是利用i2c设备驱动,另一种是利用i2c总线驱动

所有的I2C驱动代码位于drivers/i2c目录下:

(1) i2c-core.c

这个文件实现了I2C核心的功能以及/proc/bus/i2c*接口。

(2)i2c-dev.c

通用i2c设备驱动。就是这个文件实现了用户层利用i2c总线驱动去控制设备的功能,我们会在后面着重分析这个文件。

(3)chips文件夹

这个目录中包含了一些特定的I2C设备驱动,如Dallas公司的DS1337实时钟芯片、EPSON公司的RTC8564实时钟芯片和I2C接口的EEPROM驱动等。

(4)busses文件夹

这个文件中包含了一些I2C总线的驱动,如S3C2410的I2C控制器驱动为i2c-s3c2410.c

(5)algos文件夹

实现了一些I2C总线适配器的algorithm。

I2C核心(i2c-core.c)

初始化模块

static int __init i2c_init(void)/*i2c初始化*/
 {
     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;
 }

我们可知i2c_init()函数主要完成的就是i2c总线驱动和i2c设备驱动的注册。

i2c传输,发送和接收

/*用户空间调用write()时,内核调用此函数,此函数最终调用i2c_transfer()*/  
int i2c_master_send(const struct i2c_client *client, constchar *buf, int count)

/*用户空间调用read()时,内核调用此函数,此函数最终调用i2c_transfer()*/ 
int i2c_master_recv(const struct i2c_client *client, char*buf, int count)  

/*用于传输数据,此函数是i2c适配器工作的基石,我们放在总线驱动中分析*/    
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)

由此我们i2c核心具体完成的就是:

  1. 实现了I2C总线驱动和设备驱动的注册;
  2. 提供接口函数i2c_master_send(),i2c_master_recv(),i2c_transfer()实现了用户界面通过适配器去控制i2c设备的功能。

我们接下来看看I2C设备驱动,也就是第一种操控i2c设备的方法。

I2C设备驱动

要想让内核中的i2c设备驱动使能,我们就应该去内核中的make menuconfig中添加设备的支持,但是这个设备在i2c总线上,所以我们必须要先去内核中添加i2c总线的支持,在添加设备支持。我们这里就只拿eeprom设备作为例子去分析。

这里写图片描述
这里写图片描述
这里写图片描述

在添加完i2c总线的支持后,我们再去添加eeprom,这时候,在eeprom下就会出现I2C EEPROM这个选项。

这里写图片描述
这里写图片描述
这里写图片描述

添加完i2c总线和eeprom设备支持后,在sys/devices/platform/s3c2440-i2c/i2c-0中就会出现相对应的设备。
这里写图片描述

这里写图片描述

那接下来,我们就来看看i2c设备驱动吧。

从前面我们知道i2c设备驱动是由i2c_driver和i2c_client这两个结构体描述的。

我们首先看一下i2c_driver这个结构体

struct i2c_driver 
{  
  unsignedint class;  

  int(*attach_adapter)(struct i2c_adapter *) __deprecated; /*依附i2c适配器函数指针*/  
  int(*detach_adapter)(struct i2c_adapter *) __deprecated;/*脱离i2c适配器函数指针*/  

  int (*probe)(struct i2c_client*, const struct i2c_device_id *);  
  int (*remove)(struct i2c_client*);  

  int(*suspend)(struct i2c_client *, pm_message_t mesg);  
  int(*resume)(struct i2c_client *);  
  void(*alert)(struct i2c_client *, 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表 */  

 /*Device detection callback for automatic device creation */  
  int(*detect)(struct i2c_client *, struct i2c_board_info *);  
  constunsigned short *address_list;  
  structlist_head clients;  
};  

我们可以看出i2c_driver结构对应一套具体的驱动方法。

那么i2c_driver这个结构体是怎么实现的呢,drivers/misc/eeprom/at24.c 这个文件就是驱动代码的实现,并且支持大多数I2C接口的eeprom。

i2c_driver的实现

static struct i2c_driver at24_driver = {
     .driver = {
         .name = "at24",       /*设备名字*/
         .owner = THIS_MODULE,
    },
     .probe = at24_probe,     /*at24_probe函数*/
     .remove = __devexit_p(at24_remove),
     .id_table = at24_ids,    /*设备id表*/
 };

在这个结构体中,我们对i2c_driver结构体中的driver,probe,remove,id_table这些成员进行了赋值。

i2c初始化函数

static int __init at24_init(void)
 {
    if (!io_limit) {
        pr_err("at24: io_limit must not be 0!\n");
        return -EINVAL;
     }

    io_limit = rounddown_pow_of_two(io_limit);
    return i2c_add_driver(&at24_driver);
 }

在前面的代码中,我们通过构建了at24_driver这个结构体,完成了对i2c_driver结构体的赋值,在初始化函数中,我们调用核心层函数,注册i2c_driver结构体。这样,我们的这个I2C设备层驱动就被作为模块加载到内核中了。

i2c_client

在linux系统下,我们习惯于将驱动代码与硬件信息分隔开,这样可以使我们的驱动代码具有可移植性。因此在驱动i2c_driver的构建之后,我们需要去完成硬件信息的构造,即i2c_client。

我们先看一下i2c_client 结构体

struct i2c_client {  
unsigned short flags;                 /*I2C_CLIENT_TEN:使用10位从地址,I2C_CLIENT_PEC:使用SMBus包错误检测*/  
         unsignedshort addr;                 /* chipaddress - NOTE: 7bit    */  
         charname[I2C_NAME_SIZE];  
         struct i2c_adapter *adapter; /* 依附的i2c_adapter   */  
         struct i2c_driver *driver;  /* 依附的i2c_driver*/  
         structdevice dev;             /* the devicestructure             */  
         intirq;                         /* irq issuedby device               */  
         structlist_head detected;  
};

我们可以看出结构体这个结构体对应于真实的物理设备,其中包含了芯片地址,设备名称,设备所依附的适配器,设备所依附的驱动,设备使用的中断号等内容。

i2c_client在开发板文件中的实现

At24c不依赖于具体的CPU和I2C控制器硬件特性,因此如果电路板包含该外设,只需要添加对应的i2c_board_info,下面是at24c02 i2c_client在板文件中的实现:

修改文件: linux/arch/arm/mach-s3c2440/mach-smdk2440.c

#include <linux/i2c/at24.h>  
static struct at24_platform_data at24c02 = {  
    .byte_len   = SZ_2K / 8,        /* eeprom的存储大小,单位Byte */
    .page_size  = 8,               /* 页大小 Byte */ 
    .flags      = 0,  
};  

static struct i2c_board_info __initdata smdk_i2c_devices[] = {  
    /* more devices can be added using expansion connectors */  
  {  
   I2C_BOARD_INFO("24c02", 0x50),  /* 24c02设备名,0x50设备地址 */ 
   .platform_data = &at24c02,  
  },  
};

在smdk2440_machine_init函数中增加如下:

i2c_register_board_info(0, smdk_i2c_devices,            ARRAY_SIZE(smdk_i2c_devices));  

我们调用i2c_register_board_info()函数会把I2C从设备硬件特性信息注册到全局链表__i2c_board_list,但是还没有构建出一个i2c_client结构体,也没有注册进I2C总线。

当我们调用i2c_add_adapter()函数时,会遍历__i2c_board_list获得从设备信息以此来构造i2c_client。

具体过程:i2c_register_adapter()->i2c_scan_static_board_info()->i2c_new_device()->device_register()。

at24_ids结构体

在驱动和硬件信息完成之后,系统是怎么把两部分联系到一块呢?其实就是通过驱动和硬件信息里的名字进行匹配。

这里我们就会有一个疑问,在驱动i2c_driver里我们设置”name=at24”,但是在i2c_client构建时,我们给的设备名是”at24c02”,那么他们两个是怎么匹配成功的呢,其实就是根据i2c_driver里的”id_table” 成员,我们是让”id_table= at24_ids”,那么我们来看看at24_ids这个结构体:

static const struct i2c_device_id at24_ids[] = {
     /* needs 8 addresses as A0-A2 are ignored */
     { "24c00", AT24_DEVICE_MAGIC(128 / 8, AT24_FLAG_TAKE8ADDR) },
   /* old variants can't be handled with this generic entry! */
   { "24c01", AT24_DEVICE_MAGIC(1024 / 8, 0) },
   { "24c02", AT24_DEVICE_MAGIC(2048 / 8, 0) },
   /* spd is a 24c02 in memory DIMMs */
   { "spd", AT24_DEVICE_MAGIC(2048 / 8,AT24_FLAG_READONLY | AT24_FLAG_IRUGO) },
   { "24c04", AT24_DEVICE_MAGIC(4096 / 8, 0) },
   /* 24rf08 quirk is handled at i2c-core */
   { "24c08", AT24_DEVICE_MAGIC(8192 / 8, 0) },
   { "24c16", AT24_DEVICE_MAGIC(16384 / 8, 0) },
   { "24c32", AT24_DEVICE_MAGIC(32768 / 8, AT24_FLAG_ADDR16) },
   { "24c64", AT24_DEVICE_MAGIC(65536 / 8, AT24_FLAG_ADDR16) },
   { "24c128", AT24_DEVICE_MAGIC(131072 / 8, AT24_FLAG_ADDR16) },
   { "24c256", AT24_DEVICE_MAGIC(262144 / 8, AT24_FLAG_ADDR16) },
   { "24c512", AT24_DEVICE_MAGIC(524288 / 8, AT24_FLAG_ADDR16) },
   { "24c1024", AT24_DEVICE_MAGIC(1048576 / 8, AT24_FLAG_ADDR16) },
   { "at24", 0 },
   { /* END OF LIST */ }

我们可以看出at24_ids结构体将所有的at24cx系列芯片做了一个集合,我们在匹配信息时会调用i2c bus中注册的i2c_device_match()->i2c_match_id()函数通过i2c_driver->id_table->name和client->name来匹配。

probe()函数

在驱动和硬件信息匹配成功后,系统会执行probe()函数:

static int at24_probe(struct i2c_client*client, const struct i2c_device_id *id)  
{  
         ……  

         /* 
          * Export the EEPROM bytes through sysfs, sincethat's convenient. 
          * By default, only root should see the data(maybe passwords etc) 
          */  
         sysfs_bin_attr_init(&at24->bin);  
         at24->bin.attr.name= "eeprom";  
         at24->bin.attr.mode= chip.flags & AT24_FLAG_IRUGO ? S_IRUGO : S_IRUSR;  
         at24->bin.read= at24_bin_read;  
         at24->bin.size= chip.byte_len;  

         at24->macc.read= at24_macc_read;  
         writable = !(chip.flags &AT24_FLAG_READONLY);  
         if(writable) 
         {  
          if(!use_smbus || i2c_check_functionality(client->adapter,I2C_FUNC_SMBUS_WRITE_I2C_BLOCK))
          {  
           unsignedwrite_max = chip.page_size;  
           at24->macc.write= at24_macc_write;  
           at24->bin.write= at24_bin_write; 
           at24->bin.attr.mode|= S_IWUSR;                                                             
                            ……  
         } 
……  

         err = sysfs_create_bin_file(&client->dev.kobj,&at24->bin);  
         if(err)  
             gotoerr_clients;  

         i2c_set_clientdata(client,at24);  
         ……  
}

probe()函数主要的工作是在sys目录下创建bin节点文件,用户可以同此节点文件来操作eeprom。

综述

其实 drivers/misc/eeprom/at24.c 这个文件,就是at24cx芯片的一个驱动文件,这个文件包括了:

  • 向内核注册驱动
  • 匹配驱动与硬件信息
  • 提供操作at24cx芯片的方法(读,写),
  • 生成一个节点文件

当我们看完i2c设备驱动时,就会发现,i2c设备eeprom就是一个普通的 字符设备 (driver,device“client”,probe)。我们在前面说到,有两种方式去控制i2c设备。在这里我们就提供一个完全可以跳过i2c总线,只通过设备驱动,直接对eeprom进行操作的代码。

具体代码如下:

/*********************************************************************************
 *      Copyright:  (C) 2017 00
 *                  All rights reserved.
 *
 *       Filename:  i2c_1.c
 *    Description:  This file 
 *                 
 *        Version:  1.0.0(2017年04月26日)
 *         Author:  PainEver <781189738@qq.com>
 *      ChangeLog:  1, Release initial version on "2017年04月26日 17时12分38秒"
 *                 
 ********************************************************************************/

#include<stdio.h>  
#include<string.h>  
#include<unistd.h>  
#include<sys/ioctl.h>  
#include<stdlib.h>  
#include<fcntl.h>  
#include<sys/io.h>  
#include<errno.h>  


#define LEN 20 
#define PRINT_ERROR printf("error: %d:%s",errno,strerror(errno))  


/********************************************************************************  
 *  Description:  
 *   Input Args:  
 *  Output Args:  
 * Return Value:  
 ********************************************************************************/  
int main (int argc, char **argv)  
{  
   int ret,fd,i,j;  
   char read_data[LEN];  
   char write_data[LEN] ="The lifeline\n";  
   char offset[LEN];  

   memset(offset,0,sizeof(offset));  
   fd = open("/sys/devices/platform/s3c2440-i2c/i2c-0/0-0050/eeprom",O_RDWR);  /*打开的这个节点文件,就是probe函数生成的*/
   if(fd < 0)  
   {  
       printf("Open AT24C02 fail.\n");   
        return -1;  
   }
   ret = read(fd, offset,LEN);  
   if(ret < 0)  
   {  
       printf("Read AT24C02 fail.\n");   
        return -1;  
   }  

   printf("%s\n",offset);  //读出当前EEPROM中的数据  

   ret = lseek(fd, 0, SEEK_SET);//调整偏移量,从开始位置写入  

   if(-1 == ret)  
   {  
        PRINT_ERROR;   
        return -1;  
   }  
   ret = write(fd ,write_data, sizeof(write_data));  

   if(ret < 0)  
   {  
    printf("Write error\n");  
    return -1;  
   }//将指定数据写入当前EEPROM中  

   lseek(fd, 0, SEEK_SET);//重新设置偏移量从起始位置读取  

   ret = read(fd , read_data, LEN);  
    if(ret < 0)  
    {  
        printf("Read error\n");   
        return -1;  
    }  
    else if(ret < 20)  
    {  
        printf("Incomplete read\n");  
        return -1;  
    }  
    printf("%s\n",read_data);  
    return 0;  
}

从上述代码中,可以看出,我们完全的跳过了i2c总线,而是直接调用设备本身的驱动去完成读写操作。

好了,设备驱动我们就分析到这里,接下来我们分析总线驱动,也就是另一种操控i2c设备的方法。

I2C总线驱动

一般来说,总线驱动内核已经帮我们完成了,我们分析总线驱动时,我们就主要对一下三部分进行分析:

  • 总线驱动的作用
  • 总线驱动的通信方法
  • 总线驱动的注册

最后呢我们会对如何使用总线驱动控制i2c设备做重点分析。

前面说到I2C总线驱动由i2c_adapter和i2c_algorithm这两个结构体来描述,那么我们先来看看这两个结构体:

I2c_adapter结构体代表I2C总线控制器

struct i2c_adapter {  
   struct module *owner;  
   unsigned int class;       /*classes to allow probing for */  
    const struct i2c_algorithm*algo; /* 总线上数据传输的方式*/  
   void *algo_data;              /* algorithm 数据 */  

   int timeout;            /* 超时时间设置 */  
   int retries;             /* 重试次数 */  
    struct device dev;      /* the adapter device */  

   int nr;                  /*IDR机制中ID号,有时也作次设备号*/
   char name[48];                 /* 适配器名字 */  
   struct completion dev_released;   /* 用于同步 */  
};

I2c_algorithm对应一套通信方法

struct i2c_algorithm {  
    int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, intnum); /*指向 I2C总线通信协议的函数*/
   int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,  
            unsigned short flags, charread_write,  
            u8 command, int size, unioni2c_smbus_data *data); /*实现SMBUS总线通信协议的函数,一般置为NULL*/
    u32 (*functionality) (structi2c_adapter *);  /*确定适配器支持的类型*/
};

总线驱动的作用

一直说总线驱动,总感觉很高端的样子,那我们就来看看总线驱动到底是干什么的:

I2C总线驱动是I2C适配器的软件实现,提供I2C适配器与从设备间完成数据通信的能力

通俗说,就是总线驱动告诉适配器什么时候产生开始信号,什么时候产生结束信号,什么时候产生应答信号,然后适配器就通过自己的硬件去完成这些工作。

既然说到了适配器的硬件信息,那我们就来看看适配器的描述:

S3c2440处理器内部集成了一个I2C控制器,通过四个寄存器来进行控制:

IICCON I2C控制寄存器

IICSTAT I2C状态寄存器

IICDS I2C收发数据移位寄存器

IICADD I2C地址寄存器

通过IICCON,IICDS,IICADD寄存器操作,可在I2C总线上产生开始位、停止位、数据和地址,而传输的状态则通过IICSTAT寄存器来获取。

在弄懂总线驱动是干嘛的之后,我们就知道了,我们完全可以通过操控适配器去控制挂接在i2c总线上的设备,这就是我们所说的通过i2c总线驱动去控制设备。在分析完总线驱动之后,我们会给出一个代码,去实现这种操作。

总线驱动的通信方法

从前面我们知道了I2c_algorithm这个结构体对应一套通信方法,所以我们在分析总线驱动的通信方法事,主要就是对这个结构体进行分析。

struct i2c_algorithm {  
    int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs, intnum); /*指向 I2C总线通信协议的函数*/
    ...
}

我们可以看到master_xfer()这个函数就是总线通信协议的函数,那么这个函数成员是由谁给它赋值的呢?

我们在i2c-s3c2410.c这个文件中发现:

static const struct i2c_algorithm s3c24xx_i2c_algorithm = {
     .master_xfer        = s3c24xx_i2c_xfer,
     .functionality      = s3c24xx_i2c_func,
  };

所以就是s3c24xx_i2c_xfer(),这个函数去完成的适配器与设备的通信,我们再来深追一下,打开s3c24xx_i2c_xfer():

static int s3c24xx_i2c_xfer(struct i2c_adapter *adap,
              struct i2c_msg *msgs, int num)
  {
      struct s3c24xx_i2c *i2c = (struct s3c24xx_i2c *)adap->algo_data;
      int retry;
      int ret;

      clk_enable(i2c->clk);

      for (retry = 0; retry < adap->retries; retry++) {

          ret = s3c24xx_i2c_doxfer(i2c, msgs, num);/*传输到I2C设备的具体函数*/

          if (ret != -EAGAIN) {
              clk_disable(i2c->clk);
              return ret;
          }
       ....
  }

我们发现s3c24xx_i2c_xfer()函数中又是调用了s3c24xx_i2c_doxfer()函数去实现的通信,我们再打开s3c24xx_i2c_doxfer():

static int s3c24xx_i2c_doxfer(struct s3c24xx_i2c *i2c,
 481                   struct i2c_msg *msgs, int num)
 482 {
 483     unsigned long iicstat, timeout;/*传输超时*/
 484     int spins = 20;
 485     int ret;/*返回传输的消息个数*/
 486 
 487     if (i2c->suspended)/*如果适配器处于挂起状态,则返回*/
 488         return -EIO;
 489 
 490     ret = s3c24xx_i2c_set_master(i2c);/*将适配器设置为主机发送状态*/
 491     if (ret != 0) {
 492         dev_err(i2c->dev, "cannot get bus (error %d)\n", ret);
 493         ret = -EAGAIN;
 494         goto out;
 495     }
 496 
 497     spin_lock_irq(&i2c->lock);
 498 
 499     i2c->msg     = msgs;
 500     i2c->msg_num = num;
 501     i2c->msg_ptr = 0;
 502     i2c->msg_idx = 0;
 503     i2c->state   = STATE_START;
 504
 505     s3c24xx_i2c_enable_irq(i2c);/*启动适配器的中断号,允许适配器发出中断*/
 506     s3c24xx_i2c_message_start(i2c, msgs);/*启动适配器的消息传输*/
 507     spin_unlock_irq(&i2c->lock);
 508 
 509     timeout = wait_event_timeout(i2c->wait, i2c->msg_num == 0, HZ * 5);/*设置等待队列,直到i2c->msg_num == 0为真或5ms到来才被唤醒*/
 510 
 511     ret = i2c->msg_idx;
 512 
 513     /* having these next two as dev_err() makes life very
 514      * noisy when doing an i2cdetect */
 515 
 516     if (timeout == 0)
 517         dev_dbg(i2c->dev, "timeout\n");
 518     else if (ret != num)
 519         dev_dbg(i2c->dev, "incomplete xfer (%d)\n", ret);
 520 
 521     /* ensure the stop has been through the bus */
 522 
 523     dev_dbg(i2c->dev, "waiting for bus idle\n");
 524 
 525     /* first, try busy waiting briefly */
 526     do {
 527         iicstat = readl(i2c->regs + S3C2410_IICSTAT);
 528     } while ((iicstat & S3C2410_IICSTAT_START) && --spins);
 529
 530     /* if that timed out sleep */
 531     if (!spins) {
 532         msleep(1);
 533         iicstat = readl(i2c->regs + S3C2410_IICSTAT);
 534     }
 535 
 536     if (iicstat & S3C2410_IICSTAT_START)
 537         dev_warn(i2c->dev, "timeout waiting for bus idle\n");
 538 
 539  out:
 540     return ret;
 541 }

我们可以从注释里看到s3c24xx_i2c_doxfer()主要做了如下这些事:第一,将适配器设置为主机发送状态。第二,设置为中断传输方式。第三,发送启动信号,传输第一个字节。第四,等待超时或者其他函数在i2c->msg_num == 0时唤醒这里的等待队列。

从中我们可以知道s3c24xx_i2c_doxfer()这个函数就是传输到I2C设备的具体函数,其实我对于这个函数的具体是怎么操作的也不是很明白,在这里只是给大家看一下I2c_algorithm这个结构体到底是调用那些函数去完成适配器与设备的通信工作的。有兴趣的朋友可以去深入了解一下。

我们总结一下,I2c_algorithm结构体对应了一套通信方式,具体是其内部成员master_xfer()函数也就是s3c24xx_i2c_xfer()函数通过调用s3c24xx_i2c_doxfer()去实现的通信。

在i2c_adapter 结构体中:

struct i2c_adapter {  
   struct module *owner;  
   ... 
    const struct i2c_algorithm *algo; /* 总线上数据传输的方式*/  
    ...
 }

包含了i2c_algorithm 这个结构体指针成员,指向了总线上数据传输的方式,所以当i2c_algorithm 这个结构体被填充完毕之后,整个i2c总线控制器,也就是适配器就可以工作了。具体怎么填充,我们在后面分析i2c-dev.c的时候,再做讲解。

总线驱动的注册

总线驱动是基于platform来实现的。

platform_driver

在drivers/i2c/busses/i2c-s3c2410.c下

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,  
                   .of_match_table= s3c24xx_i2c_match,  
         },  
};

将platform_driver注册到内核中

static int __init i2c_adap_s3c_init(void)  
{  
         returnplatform_driver_register(&s3c24xx_i2c_driver);  
}

platform_device

在arch/arm/plat-samsung/dev-i2c0.c下

tatic 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",   /* 设备名 */  
#ifdef CONFIG_S3C_DEV_I2C1  
         .id               = 0,  
#else  
         .id               = -1,  
#endif  
         .num_resources         =ARRAY_SIZE(s3c_i2c_resource),  
         .resource   =s3c_i2c_resource,  
};  

struct s3c2410_platform_i2c default_i2c_data __initdata = {  
         .flags                  = 0,  
         .slave_addr      = 0x10,  /* I2C适配器的地址 */  
         .frequency        = 100*1000,  /* 总线频率 */  
         .sda_delay        = 100,   /* SDA边沿延迟时间ns */  
};  

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

         if(!pd)  
                   pd= &default_i2c_data;  

         npd= s3c_set_platdata(pd, sizeof(struct s3c2410_platform_i2c),  
                                   &s3c_device_i2c0);  

         if(!npd->cfg_gpio)  
                   npd->cfg_gpio= s3c_i2c0_cfg_gpio;  
} 

在板文件中把platform_device注册进内核:

static struct platform_device *mini2440_devices[] __initdata = {  
         ……  
         &s3c_device_i2c0,  
……  
};  

probe()函数

在驱动与硬件找到彼此之后,内核就执行probe()函数,来初始适配器硬件。

static int s3c24xx_i2c_probe(struct platform_device *pdev)  
{  
         ……  
         /*初始化适配器信息 */  
         strlcpy(i2c->adap.name,"s3c2410-i2c", sizeof(i2c->adap.name));  
         i2c->adap.owner   = THIS_MODULE;  
         i2c->adap.algo    = &s3c24xx_i2c_algorithm;  
         i2c->adap.retries= 2;  
         i2c->adap.class   = I2C_CLASS_HWMON | I2C_CLASS_SPD;  
         i2c->tx_setup     = 50;  

         /*初始化自旋锁和等待队列头 */  
         spin_lock_init(&i2c->lock);  
         init_waitqueue_head(&i2c->wait);  

         /*映射寄存器 */  
         res= platform_get_resource(pdev, IORESOURCE_MEM, 0);  
         i2c->ioarea= request_mem_region(res->start, resource_size(res),  
                                                pdev->name);  
         i2c->regs= ioremap(res->start, resource_size(res));  

         /*设置I2C核心需要的信息 */  
         i2c->adap.algo_data= i2c;  
         i2c->adap.dev.parent= &pdev->dev;  

         /*初始化I2C控制器 */  
         ret= s3c24xx_i2c_init(i2c);  

         /*申请中断 */  
         i2c->irq= ret = platform_get_irq(pdev, 0);  
         ret= request_irq(i2c->irq, s3c24xx_i2c_irq, 0,  
                              dev_name(&pdev->dev), i2c);  

   /* 注册I2C适配器 */  
         ret= i2c_add_numbered_adapter(&i2c->adap);  
         ……  
}

probe()函数主要工作是时能硬件并申请I2C适配器使用的IO地址,中断号等,然后向I2C核心添加这个适配器。

I2c_adapter注册过程i2c_add_numbered_adapter->i2c_register_adapter

注意,就是在调用i2c_register_adapter这个函数的时候,生成了我们前面所说的i2c_client结构体

这样总线驱动就注册成功了。

在我们前面添加完内核对i2c总线的支持后,开发板中就会出现s3c2410-i2c这个适配器。
这里写图片描述

总线驱动就介绍到这里,那我们如何使用总线驱动去控制设备呢?

我们在前面说到过一个文件 :

i2c-dev.c

i2c-dev.c中针对每个适配器生成一个主设备号为89的设备节点,实现了文件操作接口,用户空间可以通过i2c设备节点访问i2c适配器。适配器的编号从0开始,和适配器的设备节点的次设备号相同。

i2c适配器的设备节点是/dev/i2c-x,其中x是数字,代表适配器的编号。由于适配器编号是动态分配的(和注册次序有关),所以想了解哪一个适配器对应什么编号,可以查看/sys/class/i2c-dev/目录下的文件内容。比如我们在总线驱动的注册中注册了s3c2410-i2c这个适配器,那我们来看看这个适配器是什么编号:

这里写图片描述

我们发现在/sys/class/i2c-dev/目录下只有一个适配器,我们看一下这个适配器的名字是不是我们之前注册的s3c2410-i2c呢:

这里写图片描述
确实就是我们之前注册的s3c2410-i2c适配器。

i2c-dev.c并没有针对特定的设备而设计,它不但提供了适配器的设备节点,而且还提供了通用的read()、write()和ioctl()等接口,应用层可以通过 打开对应的设备节点使用这些通用接口 访问挂接在适配器上的I2C设备,并操控它们。

这就实现了用户层利用i2c总线驱动去控制设备的功能。

我们在内核中添加i2c总线的支持之后,内核就会编译i2c_dev.c,然后在/dev目录下就自动生成了主设备号为89,次设备号为0的设备节点。

这里写图片描述

说到这里,我们就明白了i2c-dev.c就是一个针对i2c设备的一个通用驱动代码,我们有了驱动代码,但是硬件信息从哪里来呢?

在这里,我之前就被迷惑了,我一直以为总线驱动和设备驱动共用我们之前在设备驱动中添加的i2c_client(硬件信息),所以,我就一直把总线驱动和设备驱动联系到了一起,其实是错的。

我们在使用总线驱动控制设备,也就是使用i2c_dev.c这个文件的时候,会打开适配器对应的设备节点open(“/dev/i2c-0”,…),就在这时i2c-dev为打开的线程建立一个i2c_client,但是这个i2c_client并不加到i2c_adapter的client链表当中。当用户关闭设备节点时,它自动被释放。这个i2c_client才是我们在这里用到的硬件信息。

好了,i2c总线驱动的代码,硬件信息都有了,那我么如何使用总线驱动去控制设备呢?

我们之前说过,完成i2c总线驱动也就是完成对I2c_algorithm这个结构体的填充,i2c总线驱动要想工作,必须得有一套通信方式,要不然总线哪知道自己要干什么。所以我们用户空间使用2c-dev.c去控制设备时,就是完成对I2c_algorithm这个结构体的填充。

而在决定了I2c_algorithm这个结构体通信方式的又是master_xfer()这个函数,那我们就会想,用户空间里怎么做才能和master_xfer()这个函数完成对接呢?

其实,在我们介绍i2c_core.c的时候,我们讲了i2c_core.c是把用户空间和i2c总线驱动层串起来的。怎么串起来呢,就是通过i2c_core.c中的i2c_transfer()这个函数。为什么是它呢?我们来看一下这个函数:

 int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
1302 {
1303     unsigned long orig_jiffies;
1304     int ret, try;

1323     if (adap->algo->master_xfer) /*如果master_xfer和这个函数存在*/
        {
1324 #ifdef DEBUG
1325         for (ret = 0; ret < num; ret++) {
1326             dev_dbg(&adap->dev, "master_xfer[%d] %c, addr=0x%02x, "
1327                 "len=%d%s\n", ret, (msgs[ret].flags & I2C_M_RD)
1328                 ? 'R' : 'W', msgs[ret].addr,
msgs[ret].len,
1329                 (msgs[ret].flags & I2C_M_RECV_LEN) ? "+" : "");
1330         }
               ...
}

可以看出i2c_transfer()就是去通过adap->algo->master_xfer与master_xfer()对接。

所以呢,这个函数与i2c总线驱动完成了对接,那我们再来看看这个函数是怎么和用户空间对接的呢?

我们所说的用户空间在使用i2c总线驱动时,都是通过i2c-dev.c提供的函数接口,也就是read(),write(),ioctl()。我们先看一下ioctl吧。打开i2c-dev.c,我们先看i2cdev_fops :

 static const struct file_operations i2cdev_fops = {
512     .owner      = THIS_MODULE,
513     .llseek     = no_llseek,
514     .read       = i2cdev_read,
515     .write      = i2cdev_write,
516     .unlocked_ioctl = i2cdev_ioctl,/*用户层的ioctl()对应的函数*/
517     .open       = i2cdev_open,
518     .release    = i2cdev_release,
519 };

我们再看看看i2cdev_ioctl():

 static long i2cdev_ioctl(struct file *file, unsigned int cmd, unsigned long arg)  
{  
    struct i2c_client *client = file->private_data;  
    unsigned long funcs;  

    dev_dbg(&client->adapter->dev, "ioctl, cmd=0x%02x, arg=0x%02lx\n",  
        cmd, arg);  

    switch (cmd) {  
    case I2C_SLAVE:  
    case I2C_SLAVE_FORCE:  
        /* NOTE:  devices set up to work with "new style" drivers 
         * can't use I2C_SLAVE, even when the device node is not 
         * bound to a driver.  Only I2C_SLAVE_FORCE will work. 
         * 
         * Setting the PEC flag here won't affect kernel drivers, 
         * which will be using the i2c_client node registered with 
         * the driver model core.  Likewise, when that client has 
         * the PEC flag already set, the i2c-dev driver won't see 
         * (or use) this setting. 
         */  
        if ((arg > 0x3ff) ||  
            (((client->flags & I2C_M_TEN) == 0) && arg > 0x7f))  
            return -EINVAL;  
        if (cmd == I2C_SLAVE && i2cdev_check_addr(client->adapter, arg))  
                        return -EBUSY;  
        /* REVISIT: address could become busy later */  
        client->addr = arg; //设置addr  
        return 0;  
    case I2C_TENBIT://设置10 bit地址模式  
        if (arg)  
            client->flags |= I2C_M_TEN;  
        else  
            client->flags &= ~I2C_M_TEN;  
        return 0;  
    case I2C_PEC://设置传输后增加PEC标志  
        if (arg)  
            client->flags |= I2C_CLIENT_PEC;  
        else  
            client->flags &= ~I2C_CLIENT_PEC;  
        return 0;  
    case I2C_FUNCS://获取函数支持  
        funcs = i2c_get_functionality(client->adapter);  
        return put_user(funcs, (unsigned long __user *)arg);  

    case I2C_RDWR://读取和发送数据  
        return i2cdev_ioctl_rdrw(client, arg);  

    case I2C_SMBUS: //SMBUS协议数据传输  
        return i2cdev_ioctl_smbus(client, arg);  

    case I2C_RETRIES://设置重试次数  
        client->adapter->retries = arg;  
        break;  
    case I2C_TIMEOUT://设置超时时间  
        /* For historical reasons, user-space sets the timeout 
         * value in units of 10 ms. 
         */  
        client->adapter->timeout = msecs_to_jiffies(arg * 10);  
        break;  
    default:  
        /* NOTE:  returning a fault code here could cause trouble 
         * in buggy userspace code.  Some old kernel bugs returned 
         * zero in this case, and userspace code might accidentally 
         * have depended on that bug. 
         */  
        return -ENOTTY;  
    }  
    return 0;  
}

对于我们操作i2c总线上的设备时,我们一般都是进行读写操作,所以当我们ioctl(…, I2C_RDWR)时,我们看一下i2cdev_ioctl()做了什么:

 case I2C_RDWR://读取和发送数据  
        return i2cdev_ioctl_rdrw(client, arg);

i2cdev_ioctl()返回到i2cdev_ioctl_rdrw()这个函数,那么我们再来看看这个函数:

static noinline int i2cdev_ioctl_rdrw(struct i2c_client *client,
237         unsigned long arg)
238 {
239     struct i2c_rdwr_ioctl_data rdwr_arg;
240     struct i2c_msg *rdwr_pa;
241     u8 __user **data_ptrs;
242     int i, res;
        ...

295     res = i2c_transfer(client->adapter, rdwr_pa, rdwr_arg.nmsgs);/*调用i2c_transfer()函数*/
296     while (i-- > 0) {
297         if (res >= 0 && (rdwr_pa[i].flags & I2C_M_RD)) {
298             if (copy_to_user(data_ptrs[i], rdwr_pa[i].buf,
299                      rdwr_pa[i].len))
300                 res = -EFAULT;
301         }
302         kfree(rdwr_pa[i].buf);
303     }
304     kfree(data_ptrs);
305     kfree(rdwr_pa);
306     return res;
307 }

所以到这里我们就明白了,正是i2c_core.c中的i2c_transfer()函数将用户空间和i2c总线驱动层串了起来。

read(),write()函数也是一样的,他们分别会调用i2c_core.c中的i2c_master_recv()和i2c_master_send(),而这连个函数又会去调用i2c_transfer()。这里我就不对这两个函数进行分析了,有兴趣的可以自己去看看。

我们明白了i2c_transfer()函数是我们控制i2c总线驱动需要去填充的目标之后,我们就看一下我们需要填充些什么参数。int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)第一个参数是不需要我们填充的,内核会自动填充,我们需要填充的就是 msgs, num这两个参数。而这两个参数呢,就是用户空间i2c_rdwr_ioctl_data这个结构体的两个成员。

struct i2c_rdwr_ioctl_data {    
         struct i2c_msg __user *msgs; /*Msgs 表示单个开始信号传递的数据;*/          
         __u32 nmsgs; /*Nmsgs  表示有多少个msgs*/                       
};    

我们再看一下struct i2c_msg这个结构体:

struct i2c_msg {    
         __u16 addr;     /*设备的地址*/ 
         __u16 flags;    /* 默认为写入 0 */    
         __u16 len;      /*需要写入的数据的长度*/                 
         __u8 *buf;      /*指向需要写入的数据*/    
};

综述

通过上面的分析,我们明白了,想要通过i2c总线驱动,也就是适配器去控制设备,就需要去使用i2c-dev.c函数提供的适配器节点和通用函数接口,并且需要去填充struct i2c_rdwr_ioctl_data {}。那我们在下面就给出一个具体实现通过适配器向eeprom写数据的代码:

/*********************************************************************************
 *      Copyright:  (C) 2017 00
 *                  All rights reserved.
 *
 *       Filename:  i2c_dev.c
 *    Description:  This file 
 *                 
 *        Version:  1.0.0(2017年05月11日)
 *         Author:  PainEver <781189738@qq.com>
 *      ChangeLog:  1, Release initial version on "2017年05月11日 10时36分26秒"
 *                 
 ********************************************************************************/

#include<stdio.h>  
#include<stdlib.h>  
#include<linux/i2c.h>  
#include<linux/i2c-dev.h>  
#include<string.h>  
#include<sys/types.h>  
#include<unistd.h>  
#include<fcntl.h>  
#include<errno.h>  
/******************************************************************************** 
 *  Description: 
 *   Input Args: 
 *  Output Args: 
 * Return Value: 
 ********************************************************************************/  
int main(int argc, char **argv)  
{  
    int     i;
    int     fd;
    int     ret;
    int     length;  
    unsigned char rdwr_addr = 0x00;  //eeprom 读写地址  
    unsigned char device_addr = 0x50; //eeprom 设备地址  
    unsigned char data[] = "the lifeline";    

    struct i2c_rdwr_ioctl_data e2prom_data;  

    e2prom_data.nmsgs = 1;  

    printf("open i2c device...\n");  

    fd = open("/dev/i2c-0",O_RDWR);  
    if (fd < 0)  
     {  
         printf("open faild");  
         return -1;  
     }  

    e2prom_data.msgs = (struct i2c_msg *)malloc(e2prom_data.nmsgs * sizeof(struct i2c_msg));  

    if (e2prom_data.msgs == NULL)  
     {  
         printf("malloc error");  
         return -1;  
     }  

    ioctl(fd, I2C_TIMEOUT, 1);// 设置超时  
    ioctl(fd, I2C_RETRIES, 2);// 设置重试次数  

     /*  向e2prom中写入数据 */  
     length = sizeof(data);    
     e2prom_data.msgs[0].len = length;  
     e2prom_data.msgs[0].addr = device_addr;  

     e2prom_data.msgs[0].buf =(unsigned char *)malloc(length);  
     e2prom_data.msgs[0].buf[0] = rdwr_addr;  
     for(i = 0;i<length; i++)  
     e2prom_data.msgs[0].buf[1+i] = data[i]; /*  write data */  

     ret = ioctl(fd, I2C_RDWR, (unsigned long)&e2prom_data);  
     if(ret <0)  
        {  
             perror("write data error");  
             return -1;  
        }  

     printf("write data: %s to address %#x\n", data, rdwr_addr);  

     return 0;  
}

好了,对于整个I2C我们就分析到这里,文档是很多地方都是我个人的理解,如果有不对的地方,欢迎大家指出。

最后附上一张通过两种方式控制i2c总线上的设备的架构图:

这里写图片描述

左边是第一种,通过设备驱动控制设备;
右边是第二种,通过总线驱动控制设备。


References:

http://blog.csdn.net/paul_liao/article/details/6978318/

http://blog.csdn.net/u010944778/article/details/46807737

在线咨询
微信号
13554373241
联系方式
135-5437-3241
邮箱
guowenxue@aliyun.com
返回顶部