I2C总线基本知识
I2C总线物理拓扑结构
I2C总线在物理连接上非常简单,分别有SDA(串行数据线)和SCL(串行时钟线)及上拉电阻组成。通信原理是通过对SCL和SDA线高低电平时序的控制,来产生I2C总线协议所需要的信号进行数据的传递。在总线空闲状态时,这两个线一般被上面所接的上拉电阻拉高,保持着高电平。
I2C总线特征
I2C总线上的每一个设备都可以作为主设备或者从设备,而且每一个设备都会对应一个唯一的地址(我们可以从I2C器件的数据手册得知),主从设备之间就通过这个地址来确定与哪个器件进行通信,在通常的应用中,我们把CPU带I2C总线接口的模块作为主设备,把挂载在总线上的其他设备都作为从设备。
I2C总线上可挂载的设备数量受总线上的最大电容600pF限制,如果所挂接的是相同型号的器件,则还受器件地址位的限制。
I2C总线数据传输速率在标准模式下可达100kbit/s,快速模式下可达400kbit/s,高速模块下可达3.4Mbit/s。一般通过I2C总线接口可编程时钟来实现传输速率的调整,同时也跟所接的上拉电阻的阻值有关。
I2C总线的主设备与从设备之间以字节(8位)为单位进行双向的数据传输。
I2C总线协议
I2C协议规定,总线上数据的传输必须以一个起始信号作为开始条件,一个结束信号作为传输的停止条件。起始和结束信号总是由主设备产生。总线在空闲状态时,SCL和SDA头保持着高电平,当SCL为高电平而SDA由高到低的跳变,表示产生一个起始条件;当SCL为高而SDA由低到高的跳变,表示产生一个停止条件。在起始条件产生后,总线处于忙状态,由本次数据传输的主设备独占,其他I2C器件无法访问总线。而在停止条件产生后,本次数据传输的主从设备将释放总线,总线再次处于空闲状态。如果所示:
在了解起始条件和停止条件后,我们再来看看在这个过程中数据的传输是如何进行的。前面我们已经提到过,数据传输以字节为单位。主设备在SCL线上产生每个时钟脉冲的过程中将在SDA线上传输一个数据位。当一个字节按数据从高位到低位的顺序传输完后,紧接着从设备将拉低SDA线,回传给主设备一个应答位。此时才认为一个字节真正的被传输完成。当然,并不是所有的字节传输都必须有一个应答位,比如:当从设备不能再接收主设备发送数据时,从设备将回传一个否定应答位(即只有主设备读从设备数据中才会参数否定应答位)。数据传输的过程如图所示:
在前面我们还提到过,I2C总线上的每一个设备都对应一个唯一的地址,主从设备之间的数据传输是建立在地址的基础上,也就是说,主设备在传输有效数据之前要先指定从设备的地址,地址指定的过程和上面数据传输的过程一样,只不过大多数从设备的地址是7位的,然后协议规定再给地址添加一个最低位用来表示接下来数据传输的方向。0表示主设备向从设备写数据,1表示主设备向从设备读数据。如果所示:
I2C总线操作
对I2C总线的操作实际就是主从设备之间的读写操作。大致可分为以下三种情况:
第一、主设备往从设备中写数据。数据传输格式如下:
第二、主设备从从设备中读数据。数据传输格式如下:
第三、主设备往从设备中写数据,然后重启起始条件,紧接着从从设备中读取数据;或者是主设备从从设备中读数据,然后重启起始条件,紧接着主设备往从设备中写数据。数据传输格式如下:
第三种操作在单个主设备系统中,重复的开启起始条件机制要比用STOP终止传输后载次开启总线更有效率。
Linux I2C子系统框架
通过上面的了解,要完成一个I2C通信必须有以下三部分构成:I2C总线(SCL和SDA)、主设备和从设备。而主设备就是我们可以通过软件来控制I2C总线(SCL和SDA)上的时序,从设备就是我们实际要去操作的设备,如时间芯片、EEPROM、电容屏等。在Linux内核I2C子系统中将主设备称为I2C控制器或称I2C适配器,由于tcc893x是ARM CPU,可以存在多个内置的I2C控制器,所以Linux I2C子系统利用结构体i2c_adapter来管理每一个I2C控制器。每一个I2C控制器都可以挂载多个I2C器件,此时我们就必须知道I2C器件具体挂载在哪个I2C控制器上。
那么Linux子系统需要实现哪些呢?
1、我们单片机中I2C器件是直接通过SDA和SCL线与CPU相连,而ARM中是通过I2C控制器来控制SDA和SCL时序的,也就是我们要驱使I2C工作起来。
Linux I2C子系统中将I2C控制器设备驱动和I2C控制器设备资源采用platfrom平台驱动方式,来驱动I2C控制器工作起来
2、I2C控制器工作起来了,那么I2C控制器与I2C器件(从设备)如何通信呢?
Linux I2C子系统利用结构体i2c_algorithm来管理I2C控制器与I2C从设备的通信方法,利用结构体i2c_msg来管理每次通信的信息(包括是写还是读、器件地址、数据、数据长度等)
3、存在多个I2C控制器(I2C适配器),需要专门管理这些I2C控制器(I2C适配器)。
Linux I2C子系统利用结构体i2c_adapter来管理每一个I2C控制器,并且将每一个I2C控制器注册到I2C总线(i2c_bus)上去。注意:此处的I2C总线不是指SDA和SCL
4、最后就可以去实现具体I2C从设备驱动了,并指出此I2C从设备挂载在哪个I2C控制器上,从机地址为多少?
Linux I2C子系统利用结构体i2c_driver和i2c_client来管理每个I2C从机设备
Linux I2C子系统相关的文件夹或文件——kernel/driver/i2c目录
(1)、i2c_core.c
这个文件实现了I2C核心的功能以及/proc/bus/i2c*接口,比如i2c注册、i2c撤销、i2c通信等等函数都在此文件中
(2)、i2c-dev.c
实现了I2C控制器设备文件,每一个I2C控制器都被分配一个设备。通过适配器访问设备时的主设备号都为89,此设备号为0~255。应用程序通过"i2c-%d"(i2c-0、i2c-1、i2c-2....),文件名并使用文件操作接口open()、write()、read()、ioctl()和close等来访问这个设备。i2c-dev.c并没有针对特定的设备而设计,只是提供了通用的read()、write()和ioctl()等接口,应用层可以借助这些接口访问挂接在适配器上的I2C设备的存储空间或寄存器,并控制I2C设备的工作方式。
(3)、chips文件夹
这个目录中包含了一些特定的I2C设备驱动,如Dallas公司的DS1337时钟芯片,EPSON公司的RTC8564实时钟芯片和I2C接口的EEPROM驱动等。在具体的I2C设备驱动中,调用的都是I2C核心提供的API,因此,这使得具体有I2C设备驱动不依赖CPU的类型和I2C适配器的硬件特性。
(4)、busses文件夹
这个文件中包含了一些I2C控制器驱动,如针对S3C2410、S3C2440、S3C6410等处理器的I2C控制器驱动i2c_s3c2410.c
(5)、algos文件夹
实现了一些I2C控制器的algorithm
I2C控制器驱动分析
I2C控制器设备驱动文件——kernel/drivers/i2c/busses/i2c-tcc.c
I2C控制器设备资源文件——kernel/arch/arm/mach-tcc893x/devices.c
我们可以通过搜索"tcc-i2c",不难找到资源文件在kernel/arch/arm/mach-tcc893x/devices.c中
所有的I2C控制器(适配器)设备资源都是像I2C控制0一样定义在此文件中,他们的platform_device中的name成员都为"tcc-i2c",即它们共用一个I2C控制器设备驱动。并且tcc893x中所有的platform平台设备资源都是一起注册到platform平台总线上的。先将所有platform平台设备资源结构体放到kernel/arch/arm/mach-tcc893x/board-tcc8930.c文件中的platform_devices结构体体数组指针(名称为:*tcc8930_devices[])中,如:
并在kernel/arch/arm/mach-tcc893x/board-tcc8930.c文件中的tcc8930_init_machine函数中最末尾处调用platform_add_advices函数,如:
其实在tcc8930_init_machine函数中调用platform_add_advices函数之前,调用了platform_add_platfrom_data函数将每个I2C控制器的平台数据添加到对应的I2C控制器设备资源里,而每个I2C控制器的平台数据包含内核速率和SCL速率等信息。如:
#if defined(CONFIG_I2C_TCC_CORE0)
platform_device_add_data(&tcc8930_i2c_core0_device, &tcc8930_core0_platform_data, sizeof(struct tcc_i2c_platform_data));
#endif
int platform_device_add_data(struct platform_device *pdev, const void *data,size_t size)
{
void *d = NULL;
if (data) {
d = kmemdup(data, size, GFP_KERNEL);
if (!d)
return -ENOMEM;
}
kfree(pdev->dev.platform_data);
pdev->dev.platform_data = d;//将<span style="font-family: Arial, Helvetica, sans-serif;">tcc8930_core0_platform_data赋值给dev中的platform_data成员</span>
return 0;
}
而platform_add_advices函数就是循环调用platform_device_register函数将所有platform平台设备资源注册到platform平台总线上,如:
现在I2C控制器设备驱动和I2C控制器设备资源都注册到platform平台总线上了,如果I2C控制器设备驱动和I2C控制器设备资源匹配成功则进入I2C控制器设备驱动文件中的probe函数,即我们这进入kernel/drivers/i2c/busses/i2c-tcc.c文件中的probe函数
static int tcc_i2c_probe(struct platform_device *pdev)
{
int i, ret = 0;
struct tcc_i2c *i2c;
struct tcc_i2c_platform_data *tcc_platform_data = pdev->dev.platform_data;//获取平台的其他资源,主要是时钟
struct resource *resource[I2C_NUM_OF_CH];//I2C_NUM_OF_CH=1
tcc_i2c_in_suspend = 0;
i2c = kzalloc(sizeof(struct tcc_i2c) * I2C_NUM_OF_CH, GFP_KERNEL);
if (!i2c) {
ret = -ENOMEM;
goto err_nockl;
}
for (i = 0; i < I2C_NUM_OF_CH; i++) {
resource[i] = platform_get_resource(pdev, IORESOURCE_IO, i);//获取对应I2Cx资源中IO资源
if (!resource[i]) {
printk("i2c%d: no io resource? (ch%d)\n", pdev->id, i);
dev_err(&pdev->dev, "no io resource? (ch%d)\n", i);
ret = -ENODEV;
goto err_io;
}
i2c[i].adap.owner = THIS_MODULE;
i2c[i].adap.algo = &tcc_i2c_algorithm;//总线通信方法结构体指针,这样我们的I2C适配器就具有总线通信了
i2c[i].adap.retries = 2;
sprintf(i2c[i].adap.name, "%s-ch%d",pdev->name, i);
spin_lock_init(&i2c[i].lock);
init_waitqueue_head(&i2c[i].wait);
/*pdev->id就是tcc8930_i2c_corex_device中的id成员,而i2c结构体中的core成员用于下面tcc_i2c_init函数中的tca_i2c_setgpio函数中
*而在tca_i2c_setgpio函数中通过core的值进入switch中的不同case进行对I2C控制器上的引脚进行引脚配置*/
i2c[i].core = pdev->id;
i2c[i].ch = i; //--->i = 0
i2c[i].regs = (volatile struct tcc_i2c_regs *) IO_ADDRESS(resource[i]->start);
i2c[i].IRQSTR = resource[i]->end;
i2c[i].smu_i2c_flag = tcc_platform_data->smu_i2c_flag;
i2c[i].i2c_clk_rate = tcc_platform_data->i2c_ch_clk_rate[i];
i2c[i].core_clk_rate = tcc_platform_data->core_clk_rate;
memcpy(i2c[i].core_clk_name, tcc_platform_data->core_clk_name, sizeof(tcc_platform_data->core_clk_name));
/* Set GPIO and Enable Clock */此函数完成/* I2C clock Enable *//* Set i2c core clock to 4MHz *//* I2C GPIO setting */
if(tcc_i2c_init(i2c + i) < 0) {//此函数最终调用了tca_i2c_setgpio函数对应i2c引脚进行配置,设置对应引脚为i2c功能
goto err_io;
}
i2c[i].adap.algo_data = i2c + i;
i2c[i].adap.dev.parent = &pdev->dev;
i2c[i].adap.class = I2C_CLASS_HWMON | I2C_CLASS_SPD;
i2c[i].adap.nr = pdev->id * I2C_NUM_OF_CH + i;// nr = id*1 + 0 = id
/*注册i2c_adapter通过调用i2c_add_numbered_adpter函数,每注册一个就会搜索i2c_board_list链表,每搜索就会构建一个i2c_client,i2c_client的数据是从i2c_register_board_info中得到,而每构建一个i2c_client就会注册到设备链表中去。*/
ret = i2c_add_numbered_adapter(&i2c[i].adap);
if (ret < 0) {
printk("%s: Failed to add bus\n", i2c[i].adap.name);
for (i--; i >= 0; i--)
i2c_del_adapter(&i2c[i].adap);
goto err_clk;
}
}
platform_set_drvdata(pdev, i2c);
return 0;
}
<pre name="code" class="cpp">static const struct i2c_algorithm tcc_i2c_algorithm = {
.master_xfer = tcc_i2c_xfer,
.functionality = tcc_i2c_func,
};//kernel/drivers/i2c/busses/i2c-tcc.c
i2c_add_numbered_adapter(&i2c[i].adap);//kernel/drivers/i2c/i2c-core.c
status = i2c_register_adapter(adap);//添加I2C适配器,即添加I2C适配器到I2C总线上
/*总线通信方法结构体指针,master_xfer()函数(用于在I2C适配器上完成传递给它的i2c_msg数组中的每个I2C消息)和functionality()函数(用于返回algorithm所支持的通信协议,如I2C_FUNC_I2C、I2C_FUNC_10BIT_ADDR、I2C_FUNC_SMBUS_READ_BYTE、I2C_FUNC_SMBUS_WRITE_BYTE等)*/
static u32 tcc_i2c_func(struct i2c_adapter *adap)
{
return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL;
}
static int tcc_i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num)
ret = tcc_i2c_doxfer(i2c, msgs, num);
static int tcc_i2c_doxfer(struct tcc_i2c *i2c, struct i2c_msg *msgs, int num)
{
int ret, i;
for (i = 0; i < num; i++) {
spin_lock_irq(&i2c->lock);
i2c->msg = &msgs[i];
i2c->msg->flags = msgs[i].flags;
i2c->msg_num = num;
i2c->msg_ptr = 0;
i2c->msg_idx = 0;
i2c->state = STATE_START;
spin_unlock_irq(&i2c->lock);
if (i2c->msg->flags & I2C_M_RD) {
ret = recv_i2c(i2c);
if (ret != 1)
printk("recv_i2c failed! - addr(0x%02X)\n", i2c->msg->addr);
} else {
ret = send_i2c(i2c);
if (ret != 1)
printk("send_i2c failed! - addr(0x%02X)\n", i2c->msg->addr);
}
}
return ret;
}