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主设备便会开始发送数据,在发送数据期间,SCL为高时SDA必须保持稳定无变化,因为SCL为高时会采样数据。若此时SDA变化,可能导致误发结束信号而产生终止。也就是说SCL高,SDA保持稳定,则数据有效,SDA的改变只能发生在SCL的低电平期间。当然接收器每接收一个字节数据都会产生一个ACK回应信号,表示已经收到数据。
开始位和停止位都由I2C主设备产生。在选择从设备时,如果从设备是采用7位地址,则主设备在发起传输过程前,需先发送1字节的地址信息,前7位为设备地址,最后一位为读写标志。之后,每次传输的数据也是一个字节,从MSB位开始传输。每个字节传输完后,在SCL的第9个上升沿到来之前,接收方应该发出一个ACK位。SCL上的时钟脉冲由I2C主控方发出,在第8个时钟周期之后,主控方应该释放SDA,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 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驱动体系结构的图解:
AT24C02
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:
由于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