I2C总线及Linux下的I2C体系结构

一.I2C总线

I2C(内置集成电路)总线是由Philips开发的两线式串行总线,支持多主控模式,任何能够进行发送和接收的设备都可以成为主设备。
I2C是philips提出的外设总线。I2C只有两条线,一条串行数据线SDA,一条是时钟线SCL ,使用SCL,SDA这两根信号线就实现了设备之间的数据交互,它方便了工程师的布线。因此,I2C总线被非常广泛地应用在EEPROM,实时钟,小型LCD等设备与CPU的接口中。
总线空闲时,上拉电阻使SDA和SCL线都保持高电平。I2C设备上的串行数据线SDA接口电路是双向的,输出电路用于向总线上发送数据,输入电路用于接收总线上的数据。同样的,串行时钟线SCL也是双向的,作为控制总线数据传送的主机要通过SCL输出电路发送时钟信号,并检测总线上SCL上的电平以决定什么时候发送下一个时钟脉冲电平;作为接收主机命令的从设备需按总线上SCL的信号发送或接收SDA上的信号,它也可以向SCL线发送低电平信号以延长总线时钟信号周期。
起始信号:当SCL为高期间,SDA由高到低的跳变;启动信号是一种电平跳变时序信号,而不是一个电平信号。
停止信号:当SCL为高期间,SDA由低到高的跳变;停止信号也是一种电平跳变时序信号,而不是一个电平信号。
I2C总线及Linux下的I2C体系结构_第1张图片
在开始信号之后i2c主设备便会开始发送数据,在发送数据期间,SCL为高时SDA必须保持稳定无变化,因为SCL为高时会采样数据。若此时SDA变化,可能导致误发结束信号而产生终止。也就是说SCL高,SDA保持稳定,则数据有效,SDA的改变只能发生在SCL的低电平期间。当然接收器每接收一个字节数据都会产生一个ACK回应信号,表示已经收到数据。
开始位和停止位都由I2C主设备产生。在选择从设备时,如果从设备是采用7位地址,则主设备在发起传输过程前,需先发送1字节的地址信息,前7位为设备地址,最后一位为读写标志。之后,每次传输的数据也是一个字节,从MSB位开始传输。每个字节传输完后,在SCL的第9个上升沿到来之前,接收方应该发出一个ACK位。SCL上的时钟脉冲由I2C主控方发出,在第8个时钟周期之后,主控方应该释放SDA,I2C总线的时序如下:
I2C总线及Linux下的I2C体系结构_第2张图片
I2C总线及Linux下的I2C体系结构_第3张图片

二、Linux下的I2C驱动体系结构

在linux系统中,I2C驱动由以下三部分组成:

1.I2C核心:
提供了I2C总线驱动和I2C设备驱动的注册和注销方法,I2C通信方法上层的、与具体适配器无关的代码以及探测设备、检测设备地址的上层代码等。
增加/删除i2c_adapter
int i2c_add_adapter(struct i2c_adapter *adapter)
int i2c_del_adapter(struct i2c_adapter *adap)
int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
void i2c_del_driver(struct i2c_driver *driver)
i2c_client依附/脱离
int i2c_attach_client(struct i2c_client *client)
int i2c_detach_client(struct i2c_client *client)
I2C传输,发送和接收
int i2c_master_send(struct i2c_client *client,const char *buf ,int count)
int i2c_master_recv(struct i2c_client *client, char *buf ,int count)
int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
2.I2C总线驱动:
I2C总线驱动是对I2C硬件体系结构中适配器端的实现,适配器可由CPU控制,甚至可以集成在CPU内部。I2C总线驱动主要包括i2c_adapter数据结构和i2c_algorithm数据结构以及控制i2c适配器产生通信信号的函数。经由I2C总线驱动的代码,可以以主控方式产生开始位、停止位、读写周期,以及以从设备方式被读写、产生ACK等。
一个总线驱动用于支持一条特定的I2C总线的读写。一个总线驱动通常需要两个模块,struct i2c_adapter和struct i2c_algorithm来描述
I2C_adapter:构造一个对I2C core层接口的数据结构,并通过接口函数向I2C core注册一个控制器adapter。
i2c_algorithm:主要实现对I2C总线访问通信的具体算法。

3.I2C设备驱动:
I2C设备驱动是对I2C硬件体系结构中设备端的实现,I2C设备挂接在受CPU控制的I2C适配器上,通过I2C适配器与CPU交换数据。I2C设备驱动主要包括数据结构i2c_driver和i2c_client,这需要具体设备来实现其中的成员函数。

i2c_driver结构对应一套具体的驱动方法,例如:probe、remove、suspend等,需要自己申明。i2c_client数据结构由内核根据具体的设备注册信息自动生成,设备驱动根据硬件具体情况填充。具体使用下面介绍。
I2C 设备驱动具体实现放在在/drivers/i2c目录下chips文件夹。
i2c-dev.c:提供了用户层对I2C设备的访问,包括open,read,write,ioctl,release等常规文件操作,我们可以通过open函数打开 I2C的设备文件,通过ioctl函数设定要访问从设备的地址,然后可以通过 read和write或者ioctl函数完成对I2C设备的读写操作。
I2C_client:对应于真实的物理设备,结构体中包含了芯片地址,设备名称,设备使用的中断号,设备所依附的控制器,设备所依附的驱动等内容。

I2C总线及Linux下的I2C体系结构_第4张图片
架构层次分类  
第一层:提供i2c adapter的硬件驱动,探测、初始化i2c_adapter(如申请i2c的io地址和中断号),驱动soc控制的i2c_adapter在硬件上产生信号(start、stop、ack)以及处理i2c中断。覆盖图中的硬件实现层。
第二层:提供i2c adapter的algorithm,用具体适配器的xxx_xfer()函数来填充i2c_algorithm的master_xfer函数指针,并把赋值后的i2c_algorithm再赋值给i2c_adapter的algo指针。覆盖图中的访问抽象层、i2c核心层。
第三层:实现i2c设备驱动中的i2c_driver接口,用具体的i2c device设备的attach_adapter()、detach_adapter()方法赋值给i2c_driver的成员函数指针。实现设备与总线(或者叫adapter)的挂接。覆盖图中的driver驱动层。
第四层:实现i2c设备所对应的具体device的驱动,i2c_driver只是实现设备与总线的挂接,而挂接在总线上的设备则是千差万别的,所以要实现具体设备device的write()、read()、ioctl()等方法,赋值给file_operations,然后注册字符设备(多数是字符设备)。覆盖图中的driver驱动层。
第一层和第二层又叫i2c总线驱动(bus),第三层和第四层属于i2c设备驱动(device_driver)。

linux下I2C驱动体系结构文件介绍:
i2c-core.c这个文件实现了I2C核心的功能以及/proc/bus/i2c*接口。
i2c-dev.c实现了I2C适配器设备文件的功能,每一个I2C适配器都被分配一个设备。通过适配器访问设备时的主设备号都为89,次设备号为0-255。I2c-dev.c并没有针对特定的设备而设计,只是提供了通用的read(),write(),和ioctl()等接口,应用层可以借用这些接口访问挂接在适配器上的I2C设备的存储空间或寄存器,并控制I2C设备的工作方式。
busses文件夹这个文件中包含了一些I2C总线的驱动,如针对S3C2410,S3C2440,S3C6410等处理器的I2C控制器驱动为i2c-s3c2410.c。
algos文件夹实现了一些I2C总线适配器的algorithm。

i2c_adapter与i2c_algorithm  
i2c_adapter对应与物理上的一个适配器,而i2c_algorithm对应一套通信方法,一个i2c适配器需要i2c_algorithm中提供的(i2c_algorithm中的又是更下层与硬件相关的代码提供)通信函数来控制适配器上产生特定的访问周期。缺少i2c_algorithm的i2c_adapter什么也做不了,因此i2c_adapter中包含其使用的i2c_algorithm的指针。

i2c_driver和i2c_client  
i2c_driver对应一套驱动方法,其主要函数是attach_adapter()和detach_adapter()
i2c_client对应真实的i2c物理设备,每个i2c设备都需要一个i2c_client来描述
i2c_driver与i2c_client的关系是一对多。一个i2c_driver上可以支持多个同等类型的i2c_client
.
i2c_adapter和i2c_client  
i2c_adapter和i2c_client的关系与i2c硬件体系中适配器和设备的关系一致,即i2c_client依附于i2c_adapter,由于一个适配器上可以连接多个i2c设备,所以i2c_adapter中包含依附于它的i2c_client的链表。
从i2c驱动架构图中可以看出,linux内核对i2c架构抽象了一个叫核心层core的中间件,它分离了设备驱动device driver和硬件控制的实现细节(如操作i2c的寄存器),core层不但为上面的设备驱动提供封装后的内核注册函数,而且还为下面的硬件事件提供注册接口(也就是i2c总线注册接口),可以说core层起到了承上启下的作用。

i2c-core对应的源文件为i2c-core.c,位于内核目录/driver/i2c/i2c-core.c,i2c-core.c中主要的函数有以下几个:
i2c_transfer()函数:i2c_transfer()函数本身并不具备驱动适配器物理硬件完成消息交互的能力,它只是寻找到i2c_adapter对应的i2c_algorithm,并使用i2c_algorithm的master_xfer()函数真正的驱动硬件流程。

int i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
{
    unsigned long orig_jiffies;
    int ret, try;
    
    if (adap->algo->master_xfer) {
#ifdef DEBUG
        for (ret = 0; ret < num; ret++) {
            dev_dbg(&adap->dev, "master_xfer[%d] %c, addr=0x%02x, "
                "len=%d%s\n", ret, (msgs[ret].flags & I2C_M_RD)
                ? 'R' : 'W', msgs[ret].addr, msgs[ret].len,
                (msgs[ret].flags & I2C_M_RECV_LEN) ? "+" : "");
        }
#endif

        if (in_atomic() || irqs_disabled()) {
            ret = i2c_trylock_adapter(adap);
            if (!ret)
                /* I2C activity is ongoing. */
                return -EAGAIN;
        } else {
            i2c_lock_adapter(adap);
        }
        /* Retry automatically on arbitration loss */
        orig_jiffies = jiffies;
        for (ret = 0, try = 0; try <= adap->retries; try++) {
            ret = adap->algo->master_xfer(adap, msgs, num);
            if (ret != -EAGAIN)
                break;
            if (time_after(jiffies, orig_jiffies + adap->timeout))
                break;
        }
        i2c_unlock_adapter(adap);
        return ret;
    } else {
        dev_dbg(&adap->dev, "I2C level transfers not supported\n");
        return -EOPNOTSUPP;
    }
}
EXPORT_SYMBOL(i2c_transfer);

i2c_add_adapter函数和i2c_del_adapter函数则用于在i2c_core.c 中向下层的硬件事件提供注册和注销接口。
i2c_register_driver和i2c_del_driver提供设备驱动的注册和注销。

buses文件夹下的i2c_s3c2410.c的与硬件相关的总线驱动代码。I2C适配器控制硬件发送接收信号的i2c_algorithm结构体中的函数指针在与硬件相关的代码中被赋值。

/* i2c bus registration info */
static const struct i2c_algorithm s3c24xx_i2c_algorithm = {
    .master_xfer        = s3c24xx_i2c_xfer,    //
    .functionality      = s3c24xx_i2c_func,
};

/*实现i2c数据的发送和接收的处理过程,也实现i2c适配器和i2c core的连接*/
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;
        }
        dev_dbg(i2c->dev, "Retrying transmission (%d)\n", retry);
        udelay(100);
    }
    clk_disable(i2c->clk);
    return -EREMOTEIO;
}

编写具体的I2C驱动时,工程师需要处理的主要工作如下:
  1).提供I2C适配器的硬件驱动,探测,初始化I2C适配器(如申请I2C的I/O地址和中断号),驱动CPU控制的I2C适配器从硬件上产生。
  2).提供I2C控制的algorithm, 用具体适配器的xxx_xfer()函数填充i2c_algorithm的master_xfer指针,并把i2c_algorithm指针赋给i2c_adapter的algo指针。
  3).实现I2C设备驱动中的i2c_driver接口,用具体yyy的yyy_probe(),yyy_remove(),yyy_suspend(),yyy_resume()函数指针和i2c_device_id设备ID表赋给i2c_driver的probe,remove,suspend,resume和id_table指针。
  4).实现I2C设备所对应类型的具体驱动,i2c_driver只是实现设备与总线的挂接。
  上面的工作中前两个属于I2C总线驱动,后面两个属于I2C设备驱动。

s3c2440中设备调用总线驱动的入口就是i2c_core.c 中的i2c_transfer函数。我们从设备调用总线驱动的入口处开始分析:
在i2c-core.c中的i2c_transfer函数中,会有语句:ret = adap->algo->master_xfer(adap, msgs, num);来实现数据传递,实际此处就是I2C总线驱动执行的入口。在s3c2440.c的总线驱动中,i2c_algorithm结构体中的master_xfer函数指针则指向了 s3c24xx_i2c_xfer。在s3c24xx_i2c_xfer函数中,调用上才s3c244xx_i2c_doxfer函数,s3c24xx_i2c_doxfer函数完成的功能有:
1.将s3c24xx的I2C适配器设置位I2C主设备。
2.初始化s3c24xx_i2c结构体。
3.使能i2c中断。
4.启动i2c消息的传输。

s3c24xx_i2c_doxfer函数调用s3c24xx_i2c_message_start()函数启动了I2C消息数组的传输周期,并没有实现完整的master_transfer传输流程,实现完整的传输流程需要借助i2c适配器上的中断来步步推进。主要包括函数s3c24xx_i2c_irq()和其依赖的i2c_s3c_irq_nextbyte(i2c, status)。
那么,总线驱动是在何时调用i2c适配器的中断处理函数s3c24xx_i2c_irq呢?
在总线驱动的s3c24xx-i2c-probe函数中,会调用该中断处理函数,而调用s3c24xx-i2c-probe的前提则是当一个适合的i2c设备被探测到时,便会调用该函数。
那么s3c24xx_i2c_probe函数会在何时被调用呢?
该函数被赋值给s3c24xx_i2c_driver中的probe函数指针,由以下代码可知,当初始化一个adapter或exit一个适配器时,就会调用s3c24xx_i2c_driver结构体,并根据该结构体中的函数指针执行不同的函数。

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

i2c_dev.c文件
i2c-dev.c文件完全可以看作一个i2c设备驱动,不过,它实现的一个i2c_client是虚拟、临时的,随着设备文件的打开而产生,并随设备文件的关闭而撤销,并没有被添加i2c_addapter的client链表中。i2cdev_read()、i2cdev_write()函数来对应用户空间要使用的read()和write()文件操作接口,这两个函数会调用i2c核心的i2c_master_recv()和i2c_master_send函数来构造一个i2c消息并引发适配器algorithm通信函数的调用,完成消息的传输。
需要注意的是,大多数I2C设备的读写流程并不对应于一条消息,往往需要两条甚至更多的消息来进行一次读写周期,这种情况下,在应用层仍然调用read、write来读写I2C设备,将不能正确的读写。
因此,i2c-dev.c中的i2cdev_read()和i2cdev_write()函数并不具备太强的通用性,只能适用于非RepStart模式的情况。对于两条以上消息组成的读写,在用户空间需要组织i2c_msg消息数组并调用I2C_RDWR_IOCTL命令。
在linux下的I2C驱动体系结构中,通过i2c适配器来控制i2c从设备,而在内核中消息的传递则会涉及到内核中相关的结构体,并且必须遵循一定的读写时序:
我们在调用ioctl之前,需要组织好i2c_msg消息结构体和nmsgs成员。在i2c_msg消息结构体成员包括:从设备地址、读写标志(默认为写入)、消息长度(其单位为字节)、指向消息内容的指针。 这些成员我们看完之后会发现它大致符合先给设备地址,然后给写信号以及数据的时序。其实但我们写代码的时候并不一定是addr非得定义在flags前面,因为内核会自动帮助我们完成这些具体的时序操作。但有一点,我们必须要填充好nmsgs以及i2c_msg中的成员。

/* This is the structure as used in the I2C_RDWR ioctl call */
struct i2c_rdwr_ioctl_data {
    struct i2c_msg __user *msgs;    /* pointers to i2c_msgs */
    __u32 nmsgs;            /* number of i2c_msgs */
};  


struct i2c_msg {
    __u16 addr; /* slave address            */
    __u16 flags; /*默认为写入*/
    __u16 len;      /* msg length               */
    __u8 *buf;      /* pointer to msg data          */
};

可以这样理解,当我们在用户空间调用ioctl函数时,会在i2c_dev.c文件中找相应的ioctl函数,我们再来看下i2c_dev.c中的ioctl函数是如何实现消息的交换的。

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

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:
    
    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;
        return 0;
    case I2C_TENBIT:
        if (arg)
            client->flags |= I2C_M_TEN;
        else
            client->flags &= ~I2C_M_TEN;
        return 0;
     case I2C_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:
        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_dev.c 中的ioctl函数中有一个switch,case语句,会根据用户空间的ioctl函数指定的参数来选择执行不同的动作,当在用户空间指定I2C_RDWR时,就会调用i2cdev_ioctl函数中的i2cdev_ioctl_rdrw函数,继续追踪该函数,它会调用copy_from_user或者copy_to_user以及i2c_transfer函数,如果继续深入了解,i2c_transfer函数是在i2c_core.c中实现的函数,i2c_core.c则是连接了i2c设备驱动和总线驱动,i2c_transfer中的函数调用adap->algo->master_xfer则是i2c总线驱动的入口。而在i2c总线驱动中,例如i2c-s3c2410.c中,则将master_xfer函数指针指向s3c24xx_i2c_xfer函数,s3c24xx_i2c_xfer函数则会继续调用s3c24xx_i2c_doxfer函数继续传输消息的推进。
linux i2c驱动体系结构的图解:
I2C总线及Linux下的I2C体系结构_第5张图片
AT24C02
在这里插入图片描述
I2C总线及Linux下的I2C体系结构_第6张图片
AT24C02的存储容量为2K bit,内容分成32页,每页8Byte,共256Byte,操作时有两种寻址方式:芯片寻址和片内子地址寻址。
(1)芯片寻址:AT24C02的芯片地址为1010,其地址控制字格式为1010A2A1A0R/W。其中A2,A1,A0可编程地址选择位。A2,A1,A0引脚接高、低电平后得到确定的三位编码,与1010形成7位编码,即为该器件的地址码。R/W为芯片读写控制位,该位为0,表示芯片进行写操作。
(2)片内子地址寻址:芯片寻址可对内部256B中的任一个进行读/写操作,其寻址范围为00~FF,共256个寻址单位。
at24c02: AT24C02是一个2K位串行CMOS E2PROM, 内部含有256个8位字节,AT24C02有一个8字节页写缓冲器。该器件通过IIC总线接口进行操作,有一个专门的写保护功能。
根据AT24C02的datasheet:
I2C总线及Linux下的I2C体系结构_第7张图片
I2C总线及Linux下的I2C体系结构_第8张图片

由于E2PROM的半导体工艺特性,对E2PROM的写入时间要5~10ms,但AT24CXX系列串行E2PROM芯片内部设置了一个具有SRAM性质的输入缓冲器,称为页写缓冲器。CPU对该芯片写操作时,AT24CXX系列芯片先将CPU输入的数据暂存页写缓冲器内,然后,慢慢写入E2PROM中。因此,CPU对AT24CXX系列E2PROM一次写入的数据,受到该芯片页写缓冲器容量的限制。页写缓冲器的容量:AT24C01A/02为8B,AT24C04/08/16为16B,AT24C32/64为32B。

参考博客:https://blog.csdn.net/wangpengqi/article/details/17711165
https://blog.csdn.net/u010944778/article/details/46807737

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