1. I2C总线的硬件特性:两线式串行总线.用于连接CPU和外设之间的通信接口需要2根信号线,时钟控制线SCL和数据传输信号线SDA.串行:CPU和外设之间传输是一个周期传输一个BIT位, 如果需要写入0X55,需要两个时钟周期才能完成.CPU又称master,外设又称slave.
“一个时钟周期传输一个bit”:CPU和外设之间传输一个bit位,必须要通过时钟控制信号来实现双方的数据收和发!比如CPU在时钟高电平像数据线写入数据,设备在同一个周期低电平从数据线上接收数据.
“总线”:两根信号线上可以挂接很多外设,也可以挂接很多CPU.一般来说总线上只有一个CPU,如果有多个CPU,I2C总线具有仲裁机制来实现同步访问.
SCL和SDA分别会接上一个上拉电阻,这两根信号线的默认状态为高电平状态!一般如果CU或者外设配置GPIO为输出口,就等于CPU或者外设控制了GPIO获取控制权;如果配置成输入口,就等于释放控制权.
2. 问:CPU如何通过两根线找到需要访问的具体设备?CPU如果找到某个具有的外设,那么CPU和外设是如何通过两根信号新完成数据交互? SDA和SCL如何搭配的?
以上答案在I2C总线协议中(芯片手册)
3. 总线协议相关内容:
(1) START信号,起始信号:CPU如果要访问总线,必须CU首先向总线上发送一个START起始信号;此信号由CPU发起;SCL为高电平。SDL由高电平向低电平跳变,产生START信号‘
(2) 停止信号,结束信号:CPU结束访问总线,需要向总线发送一个STOP信号;此信号由CPU发起;SCL为高电平,SDA由低电平向高电平跳变,产生STOP信号;
(3) 设备地址:用于标识外设在I2C总线上的唯一性!同一个I2C总线上外设,每一个外设都有唯一的设备地址;如果CPU要访问某个外设,CPU只需要在总线上发送某个外设的设备地址即可,发送完毕,如果外设存在于总线上,外设会给CPU一个反馈信号,就可以进行后续的数据访问;
外设的设备地址的确定:一般有芯片厂家和原理图共同决定,以三个外设为例,电可檫除存储器AT24C02、温度传感器LM77,背光灯控制芯片ADP8860。
AT24C02,EEPROM的设备地址:1010A2A1A0R/W:1010:高4bit,芯片厂家定义;A2A1A0:原理图上都接GNDèA2A1A01=000 R=1,表示CPU读取外设,W=0,表示CPU写外设。
WP:写保护
通过以上信息,得到读设备地址:10100001=>0Xa1 写设备地址:10100000=>0Xa0 设备地址(7位,不算读写位,地址右移1位,高位补0)=》最终设备地址=01010000=》0X50
ADP8860背光灯控制芯片设备地址0101010X:读设备地址:01010101=>0X55 写设备地址:01010100=>0X54
设备地址:00101010=>0X2A
LM77设备地址100100A1AA1:如果A1A0都接地:A1A0=00 读设备地址:10010001=>0X91 写设备地址:10010000=>0X90 设备地址:01001000=>0X48。
(4)ACK信号:反馈应答信号。如果CPU发送完设备地址,,并且外设进行响应,此时给CPU发送一个ACK应答信号,告诉CPU,外设存在于总线上;如果在数据读写过程中,也可以通过ACK信号指示数据的读写过程是否正常!有效的ACK信号为低电平(数据线),无效的ACK信号为高电平!
4.CPU与外设的数据交互:I2C的数据传输,从高位开始发送,一次传输一个字节!
1.LM77温度传感器的数据传输
(1)CPU发送START信号;(2)COU发送设备地址 3.CPU发送读写位4.设备如果正常存在总线上,设备给CPU发送一个ACK信号 5.根据芯片手册进行数据的读写操作,其中涉及到ACK信号,这个ACK与4步骤ACK信号意义不太一样!6.CPU发送STOP信号,结束此次数据交互。
2.ADP8860背光灯芯片:芯片内部有一组寄存器,但是这些寄存器CPU不能像访问GPIO一样直接去访问寄存器地址,原因ADP8860并没有直接连接到CPU的4G地址空间中,需要间接的利用I2C总线访问芯片内部的寄存器地址;例如把数据0XAA写入芯片内部寄存器0X34这个地址。
(1)。CPU发送START信号 (2)CPU发送设备地址 3.CPU发送读写位,如果是写,这个bit为0 4.ADP8860如果正常存在于总线上,设备返回一个ACK信号给CPU,低电平有效。 5.CPU发送访问的寄存器以后,设备给CPU发送一个ACK信号,告诉CPU可以继续访问; 7.CPU发送要写入的数据0XAA;8.设备将数据0XAA写入到内部寄存器0X34,然后设备给CPU一个ACK信号,告诉CPU写入成功; 9.CPU发送STOP信号,结束此次的寄存器写入操作
3.电可檫除存储器EEPROM(at24c02)访问过程:
AT24C01的容量为256字节,地址编址:0—255 MSN:高位LSB:低位。
将数据0XAA 写入到内部储存地址0X55,CPU要访问的内部地址的操作过程和ADP8860相似!
(1)CPU发送START信号 (2)CPU发送设备地址 (3)CPU发送写:0 (4)设备存在总线上,那么设备给CPU发送一个ACK应答信号,告诉CPU,我在总线上! (4)CPU发送要操作的地址:0X55 (5)设备接收到这个要操作的地址,设备给CPU发送一个ACK信号,告诉CPU,可以访问这个地址!(6)CPU发送要写入的数据0XAA (7)设备接收这个要写入的数据,并将数据写入到对应的地址0X55中,设备给CPU发送一个ACK信号,告诉CPU写入数据成功! (8)CPU发送一个STOP信号,停止数据的此次访问。
随机读:就是读任何一个地址里的数据即可,例如读取0X55地址空间中的数据信息。
SDA和SCL如何搭配使用? 如果CPU或者设备向数据线上写入数据,应该在SCL为低电平写入数据;如果CPU或者设备从数据线上获取数据,应该在同周期的高电平去读数据!
I2C外设的具体如何操作,关键看芯片手册即可!
4. AT24C02的硬件特性:EEPROM,电可檫除存储器; 容量:2K ,256字节 传输速度:100khz’,400KHZ
写周期:5ms 分页:1页为8字节,如果按页写,最多一次只能写8字节,如果写9字节,第9字节数据会把第一字节的数据进行覆盖; 地址编码:0—255 设备地址:0X50
5.案例:存储软件和硬件版本号到EEPROM中:软件版本号:SYYMMDDXY->S14101700 硬件版本号:HYYMMDDXY->H14101700EEROM存储器地址规划:软件版本号的地址范围:0X0—0X9硬件版本哈的地址范围:0X10—0X19
驱动设计:1.采用GPIO模拟I2C时序来实现I2C总线硬件操作!2.采用操作I2C控制器来实现I2C总线硬件操作! 3.采用linux内核I2C驱动框架来实现I2C硬件操作
5.1 Linux内核的I2C驱动框架:
App:open,read,write,ioctl
Eeprom.ADDR = 0;设备地址 Eeprom.data = ‘s’;//数据
Ioctl(fd,I2C_WRITE_CMD,&eeprom)
I2C设备驱动:eeprom_ioctl, eeprom_read,………;
1. 只关注硬件外设; 2.只关注硬件外设操作的数据信息; 设备地址、设备片内地址 设备的数据
2. I2C设备驱动利用内核提供的相关操作方法,将以上数据信息发送给I2C的总线驱动,由I2C总线驱动来实现硬件上面的数据传输!
内核提供的操作接口:1.i2c_transfer();//老式接口 SMBUS接口;//新式接口,兼容老式接口
作用:上一层的I2C设备驱动利用这些接口函数,将I2C设备驱动要访问的数据信息(设备地址、片内地址、数据)丢给I2C总线驱动来实现硬件的总线传输!
I2C总线驱动:1.管理的设备对象仅仅I2C控制器 I2C硬件控制器集成在CPU的内容,访问I2C控制器就通过寄存器来进行,类似串口控制器,nandflash控制器,由硬件帮你发时序!
3. I2C总线驱动启动硬件的时序,然后根据I2C设备驱动发来的数据信息(设备地址、片内地址、数据)最终完成I2C的硬件传输。
利用I2C实现I2C外设的驱动重点涉及两个驱动:I2C总线驱动和I2C设备驱动。
1. I2C总线驱动:管理的对象是I2C控制器,只负责硬件的传输数据,这个驱动一般都是芯片公司在提供的linux内核源码中,只需配置内核添加I2C总线驱动即可:
DEVICE DRIVERàI2C SupportàI2C hardware bus support-><*>s3c2410 i2c drivers
I2C总线驱动:drivers/i2c/buses/i2c-s3c.c
2. i2C设备驱动:实际开发只需关注I2C外设的驱动,就是I2C设备驱动,只需关注外设的操作的数据信息(设备地址、片内地址、数据)
5.2利用I2C驱动框架实现I2C设备驱动。
I2C设备驱动实现利用了内核分离的思想,采用虚拟总线的形式来管理I2C设备驱动,具体如下:
(1)首先linux内核已经帮你定义好了一个I2C的虚拟总线(i2c_bus_type),在这个总线上维护者两个链表:dev链表和drv链表(2)dev链表存放硬件信息,每一个节点的对应的数据类型是struct i2c_client,用这个结构体来装载硬件信息。每当向内核添加一个硬件节点时,内核帮你遍历drv链表,取出drv链表每一个软件节点,根据硬件节点的name和软件节点的id_table中的name进行匹配,如果匹配成功,说明硬件找到了软件,然后调用软件节点的probe函数,然后将匹配成功的硬件节点的首地址传递给probe函数,供probe函数获取硬件信息;(3)drv链表存放软件信息,每一个节点的对应的数据类型是struct_i2c_driver,用这个结构体来装载软件信息(fops)。每当向内核添加一个软件节点时,内核会帮你遍历dev链表,取出dev链表每一个硬件节点,根据硬件节点的name和软件节点的id_table中的name进行匹配,如果匹配成功,说明软件找到了硬件,然后调用软件节点的probe函数,然后将匹配成功的硬件节点首地址传递给probe函数,供probe函数获取硬件信息;(4)i2c硬件的信息:关键是i2c设备地址,因为I2C设备地址有可能由原理图决定。
总结;实现一个I2C设备驱动关键围绕着i2c_client和i2c_driver。
[c] view plain copy
struct i2c_client {
unsigned short flags;/* div., see below 读写标志 */
unsigned short addr;/* chip address - NOTE: 7bit 设备地址7bit*//* addresses are
char name[I2C_NAME_SIZE];//用于匹配
struct i2c_adapter *adapter; /* the adapter we sit on适配器,4个总线*/
struct i2c_driver *driver; /* and our access routines软件节点*/
struct device dev;/* the device structure platform_data来存自己定义的硬件信息 */
int irq;/* irq issued by device 中断号 */
struct list_head detected; //链表
这个结构体不会像platform_device显示的需要自己去分配,初始化和注册,这个工作linux内核已经帮你实现,甚至注册的时候进行匹配,都是linux内核来帮你实现!
问:如果linux内核帮你实现分配初始化i2c_client,内核如何知道我要的操作的设备地址,自己的私有硬件信息? Linux内核提供了另外一个结构体struct i2c_board_info,程序员根据这个结构体来进行对I2C外设的硬件分配初始化和注册,内核会根据i2c_board_info的信息来实现对i2c_client的一系列操作。
问:驱动如何使用i2c_board_info呢?
Struct i2c_board_info{
Char type[I2C_NAME_SIZE];//用于匹配,最终会赋值给i2c_client的name
Unsigned short flags; //读写标志
Unsigned short addrd; //设备地址。最终赋值给i2c_client的addr
Void *platform_data; //装载自己定义的硬件信息,最终赋值给i2c_client的dev.platform_data
Int irq; //中断号,最终赋值给i2c_client的irq
}
明确:struct i2c_board_info的分配,初始化和注册三个步骤必须在平台代码中完成,不能以模块加载的形式来实现! 对struct i2c_board_info的操作本质就是间接的在操作i2c_client。
实现在平台代码中初始化分配i2c_board_info
1.Vimarch/arm/mach-s5pv210/mach-cw21.c 在头文件的后面添加分配i2c_board_info的分配初始化。
StaTIc strucTI2c_board_info eeprom[] = {I2C_BOARD_INFO(“at24c02”,0x50)};
说明:I2C_BOARD_INFO:用于初始化i2c_board_info的type和addrat24c02:用于匹配,跟i2c_driver的id_table的name匹配,最终会赋值给i2c_client的name。 0x50:设备地址,最终会赋值给i2c_client.addr。
2.同样在平台代码的初始化函数中(.init_machine =smdkc110_machine_init),所在函数smdkc110_machine_init中调用I2c_register_board_info注册i2c_board_info信息到内核中!i2c_regster_board_info(int busnum, struct i2c_board_info const*info, unsigned n)
函数功能:注册分配初始化好的i2c_board_info对象到内核中,内核根据这个信息初始化注册i2c_client;
参数:busnum:i2c外设所在的I2C总线编号,cw210开发板的at’24c02通过原理图可知连接到CPU的第一个I2C总线上,所以这个参数指定为0,info:执行分配初始化的i2_board_info对象数组(=eeprom)
N:对象数组的个数RRAY_SIZE(eeprom)
注意:一旦向内核注册I2c_board_info设备信息,内核在初始化时会根据此信息帮你分配初始化和注册一个i2c_client.
2177i2c_register_board_info(0, i2c-devs0, ARRAY_SIZE(i2c_devs0));
2178i2c_register_board_info(1, i2c-devs1, ARRAY_SIZE(i2c_devs1));
2179i2c_register_board_info(2, i2c-devs2, ARRAY_SIZE(i2c_devs2));
2180i2c_register_board_info(5, i2c-devs3, ARRAY_SIZE(i2c_devs5));
2181 //注册atc24c02的硬件对象i2c_board_info
2182i2c_register_board_info(0, eeprom, ARRAY_SIZE(eeprom));
Struct i2c_driver怎样使用?
1.分配初始化struct i2c_driver
Struct i2c_driver eeprom_drv = {
.driver = {
.name= “tarena”//不重要
},
.probe = at24c02_probe, //匹配成功调用
.remove = at24c02, //卸载软件节点调用
.id_table = 其中的name用于匹配
}
2.调用i2c_add_driver注册 3.调用i2c_del_driver卸载。
案例:SMBUS接口作用:I2C设备驱动利用SMBUS相关的函数,能够将I2C设备驱动涉及的数据信息丢给I2C总线驱动,然后I2C硬件传输!本质上就是I2C设备驱动和I2C总线驱动的一个数据交互的桥梁!
SMBUS接口说明文档:内核源码\DocumentaTIon\i2c\smbus-protocol找到对应的SMBUS接口函数。
SMBUS接口函数的使用。
1.找到打开smbus-protol说明文档 2.打开芯片的时序图 3.根据时序图在smbus-protol文档中找到对应的操作函数 4.想尽一切办法看这个函数的说明或者参考别人的代码。
//写数据到EEPROM中
// addr,data
App:ioctl---àaddr,data
I2C设备驱动:at24c02_i2c_write---à设备地址,addr,data
内核:SMBUS--à设备地址,addr,data
总线驱动--àSTART> 设备地址写ACK addr ACK data ACK STOP
1.使用SMBUS接口将数据(地址、数据、设备地址(G_CLIENT->addr)),丢给I2C总线驱动,启动I2C总线驱动的硬件传输。
1.1 打开SMUBS文档:内核源码\DocumentaTIon\i2c\smbus-protocol找到对应的SMBUS接口函数。
1.2打开芯片操作时序图
1.3根据时序图找到对应的SMBUS操作函数
1.4将ADDR,data和匹配成功的i2c_client通过函数丢给I2C总线驱动然后启动I2C总线的硬件传输
i2c_smbus_write_byte_data(g_client, addr, data);
从EEPROM读取数据
// addr
// app:ioctlà data
I2C设备驱动:at24c02_i2c_readà data 设备地组织:addr
内核:SMBUE—》data设备地址,addr
总线驱动-àSTART设备地址写ACK addr ACK START 设备地址 读ACK返回数据data
NOACK STOP -àdata