I2C总线仅仅使用SCL、 SDA这两根信号线就实现了设备之间的数据交互,极大地简化了对硬件资源和PCB板布线空间的占用。因此, I2C总线非常广泛地应用在EEPROM、实时钟、小型LCD等设备与CPU的接口中
Linux的I2C体系结构分为3个组成部分。
(1) I2C核心
I2C核心提供了I2C总线驱动和设备驱动的注册、注销方法, I2C通信方法(即Algorithm)上层的与具体适配器无关的代码以及探测设备、检测设备地址的上层代码等,如图15.1所示。
(2) I2C总线驱动
I2C总线驱动是对I2C硬件体系结构中适配器端的实现,适配器可由CPU控制,甚至可以直接集成在CPU内部。I2C总线驱动主要包含I2C适配器数据结构i2c_adapter、 I2C适配器的Algorithm数据结构i2c_algorithm和控制I2C适配器产生通信信号的函数。
经由I2C总线驱动的代码,我们可以控制I2C适配器以主控方式产生开始位、停止位、读写周期,以及以从设备方式被读写、产生ACK等。
(3) I2C设备驱动
I2C设备驱动(也称为客户驱动)是对I2C硬件体系结构中设备端的实现,设备一般挂接在受CPU控制的I2C适配器上,通过I2C适配器与CPU交换数据。I2C设备驱动主要包含数据结构i2c_driver和i2c_client,我们需要根据具体设备实现其中的成员函数。
I2C体系结构在Linux中的实现复杂。
当工程师拿到实际的电路板时,面对复杂的Linux I2C子系统,应该如何下手写驱动呢?究竟有哪些是需要亲自做的,哪些是内核已经提供
的呢?理清这个问题非常有意义,可以使我们在面对具体问题时迅速抓住重点。
- 适配器驱动可能是Linux内核本身还不包含的;
- 挂接在适配器上的具体设备驱动可能也是Linux内核还不包含的
因此,工程师要实现的主要工作如下。
- 提供
- I2C适配器的硬件驱动,
- 探测、初始化I2C适配器(如申请I2C的I/O地址和中断号)、
- 驱动CPU控制的I2C适配器从硬件上产生各种信号以及处理I2C中断等。
- 提供
- I2C适配器的Algorithm,用具体适配器的xxx_xfer()函数填充i2c_algorithm的master_xfer指针,并把i2c_algorithm指针赋值给i2c_adapter的algo指针。
1 重要结构体 ###############
1.1 设备层 @@@@@@@@@@@
1.1.1 IIC设备(i2c_client)---------------
由IIC总线规范可知, IIC总线由两条物理线路组成,这两条物理线路是SDA和SCL。只要连接到SDA和SCL总线上的设备都可以叫做IIC设备。一个IIC设备由i2c client数据结构进行描述。
struct i2c_client {
unsigned short flags; /* div., see below */
unsigned short addr; /* chip address - NOTE: 7bit */
/* addresses are stored in the */
/* _LOWER_ 7 bits */
char name[I2C_NAME_SIZE];
struct i2c_adapter *adapter; /* the adapter we sit on */
struct i2c_driver *driver; /* and our access routines */
struct device dev; /* the device structure */
int irq; /* irq issued by device */
struct list_head detected;
};
设备地址(注意每种设备的设备地址定义可能不同)
i2c_client中的addr的低8位表示设备地址。设备地址由读写位、器件类型和自定义地址组成,如图所示。
以下是AT24C08芯片的定义
第7位是R/w位,"0”表示写,“1”表示读(通常读写信号中写上面有一横线,表示低电平) ,所以I2C设备通常有两个地址,即读地址和写地址。
类型器件由中间4位组成,这是由半导公司生产时就已固化的了,也就是说这4位已是固定的。
自定义地址码由低3位组成。这是由用户自己设置的,通常的作法如EEPROM这些器件是由外部1芯片的3个引脚所组合电平决定的(用常用的名字如A0,Al、 A2) 。A0,A1,A2就是自定义地址码。自定义地址码只能表示8个地址,所以同一IIC总线上同一型号的芯片最多只能挂接8个。AT24C08的自定义地址码如图所示,A0, Al和A2接低电平,所以自定义地址码为0.
如果在两条不同的IIC总线上挂接了两块类型和地址相同的芯片,那么这两块芯片的,地址相同。这显然是地址冲突的,解决的办法是为总线适配器指定一个ID号,那么新的芯片地址就由总线适配器的ID和设备地址组成。
注意事项:
- 地址的定义
- i2c client数据结构是描述1IC设备的“模板” ,驱动程序的设备结构体中应该包含该结构。
- adapter指向设备连接的总线适配器,系统中可能有多个总线适配器。内核中静态指针数组adapters记录所有已经注册的总线适配器设备。
- driver是指向设备对应的驱动,这个驱动程序是在系统检测到设备存在时赋值的。
1.1.2 IIC设备驱动(i2c_driver)----------------
每一个IIC设备都应该对应一个驱动,也就是每一个i2c client结构都应该对应一个 i2c driver结构。它们之间通过指针相互连接。i2c driver结构体的代码如下:
struct i2c_driver {
int id; //驱动id
unsigned int class; //驱动类型
int (*attach_adapter)(struct i2c_adapter *);//当检测到适配器调用的函数
int (*detach_adapter)(struct i2c_adapter *); //卸载适配器的函数
/* 以下是新类型驱动需要的函数 */
int (*probe)(struct i2c_client *, const struct i2c_device_id *);//新型设备的探测函数
int (*remove)(struct i2c_client *);//移除函数
/* driver model interfaces that don't relate to enumeration */
void (*shutdown)(struct i2c_client *); //关闭IIC设备
int (*suspend)(struct i2c_client *, pm_message_t mesg);//挂起IIC设备
int (*resume)(struct i2c_client *);//恢复设备
void (*alert)(struct i2c_client *, unsigned int data);
//使用命令使设备完成特殊的功能,类似ioctl函数
int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);
struct device_driver driver; //设备驱动结构体
const struct i2c_device_id *id_table; //设备ID表
/* 设备检测回调,用于自动创建设备 */
int (*detect)(struct i2c_client *, struct i2c_board_info *);//自动探测设备的回调函数
const unsigned short *address_list;
struct list_head clients;//指向支持的设备
};
第08~12行,定义了新类型的驱动程序函数,这些函数支持IIC设备的动态插入和拔出。如果IIC设备不可以动态插入和拔出,那么就应该实现第04~06行的驱动函数。注意要么只定义04~06行的传统函数,要么只定义08~12行的新类型设备的驱动函数。如果同时定义这些函数,将出现"i2c-core: driver driver-name is confused"的警告。
第14行,类似于字符设备的ioctl()函数,用来控制设备的状态。
第15行,是IIC设备内嵌的设备驱动结果体。
第16行,是一个设备ID表,表示这个设备驱动程序支持哪些设备。
第17行, detect()是自动探测设备的回调函数,这个函数一般不会执行。
第18行,表示设备映射到虚拟内存的地址范围。
第19行,使用一个list head类型的clients链表连接这个驱动支持的所有IIC设备。
1.1.3 设备和驱动的关系
1.2 IIC总线层 @@@@@@@@@@@
1.2.1 IIC总线适配器(i2c_adapter)-----------
struct i2c_adapter {
struct module *owner; //模块计数
unsigned int id; //alogorithm的类型,定义与i2c_id.h中
unsigned int class; /* 允许探测的驱动的类型 */
const struct i2c_algorithm *algo; /* 指向适配器的驱动程序 */
void *algo_data; //指向适配器的私有数据,根据不同情况使用的方法不同
/* data fields that are valid for all devices */
struct rt_mutex bus_lock;
int timeout; /* 超时 */
int retries; //重试次数
struct device dev; /* 指向适配器设备结构体 */
int nr;
char name[48]; //名字
struct completion dev_released; //用于同步的完成量
struct list_head userspace_clients;//链接总线上设备的链表
};
第05行,定义了一个i2c algorithm结构体。一个1IC适配器上的1IC总线通信方法由其驱动程序i2c algorithm结构体描述,该结构体由algo指针指向。对于不同的适配器有不同的i2c algorithm结构体。
第12、13行, timeout和retries用于超时重传,总线传输数据并不是每次都成功,所以需要超时重传机制。
第16行的clients连接该适配器上的所有IIC设备(i2c client) 。
clist lock互斥锁用于实现对1IC总线的互斥访问:在访问1IC总线上的任一设备期间当前进程必须首先获得该信号量。
第18行,定义了一个完成量用于表示适配器是否在被其他进程使用。
1.2.2 IIC总线驱动程序(i2c_algorithm)-----------
struct i2c_algorithm {
//传输函数
int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,
int num);
//smbus方式传输函数指针
int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,
unsigned short flags, char read_write,
u8 command, int size, union i2c_smbus_data *data);
/* To determine what the adapter supports */
u32 (*functionality) (struct i2c_adapter *); //返回适配器支持的功能
};
第03行,定义了一个master xfer()函数,其指向实现IIC总线通信协议的函数。
第05行,定义了一个smbus xfer()函数,其指向实现SMBus总线通信协议的函数。SMBus协议基于1IC协议的原理,也是由2条总线组成(1个时钟线,1个数据线)。SMBus和1IC之间可以通过软件方式兼容,所以这里提供了一个函数,但是一般都赋值为NULL,没有使用,这里可以忽略。
第08行定义了一个functionality ()函数,主要用来确定适配器支持哪些传输类型。
1.3 IIC总线层 和设备层的关系 @@@@@@@@@@@
总线层
- struct i2c_adapter I2C适配器(主机CPU)
- struct i2c_algorithm I2C算法(时序控制)
设备层
- struct i2c_client I2C(从机)设备信息
- struct i2c_driver I2C(从机)设备驱动
1.4 IIC设备驱动开发步骤 @@@@@@@@@@@
2 IIC子系统的初始化 ###############
在启动系统时,需要对1IC子系统进行初始化。这些初始化函数包含在i2c-core.c文件中。该文件中包含1IC子系统中的公用代码,驱动开发人员只需要用它,而不需要修改它。下面对这些公用代码的主要部分进行介绍。
IIC初始化:
static int __init i2c_init(void)
{
int retval;
retval = bus_register(&i2c_bus_type); //注册一条“i2c”总线
if (retval)
return retval;
retval = i2c_add_driver(&dummy_driver);//将一个空驱动注册到IIC总线中
if (retval)
goto class_err;
return 0;
class_err:
bus_unregister(&i2c_bus_type); //总线销毁
return retval;
}
第04行,调用设备模型中的bus register()函数在系统中注册一条新的总线,该总线的名称是i2c。适配器设备、IIC设备和IIC设备驱动程序都会连接到这条总线上。
第10行,调用i2c add driver()函数向i2c总线注册一个空的IIC设备驱动程序,用于特殊用途。驱动开发人员不用关心该空驱动程序。
第14-17行,用来错误处理。
IIC退出:
static void __exit i2c_exit(void)
{
i2c_del_driver(&dummy_driver);//去掉注销iic设备驱动程序
bus_unregister(&i2c_bus_type);//注销IIC总线
}
5 适配器驱动程序 ###################
适配器驱动程序是1IC设备驱动程序需要实现的主要驱动程序,这个驱动程序需要根据具体的适配器硬件来编写,本节将对适配器驱动程序进行详细的讲解。
1 适配器结构体 @@@@@@@@@
i2c adapter结构体为描述各种1IC适配器提供了通用“模板” ,它定义了注册总线上所有设备的clients链表、指向具体1IC适配器的总线通信方法i2c algorithm的algo指针、实现i2c总线操作原子性的lock信号量。但i2c adapter结构体只是所有适配器的共有属性,并不能代表所有类型的适配器。
struct s3c24xx_i2c {
spinlock_t lock;
wait_queue_head_t wait;
unsigned int suspended:1; //表示设备是否挂起,只有一位确定
struct i2c_msg *msg;
unsigned int msg_num;
unsigned int msg_idx;
unsigned int msg_ptr;
unsigned int tx_setup;
unsigned int irq; //设配器的中断号
enum s3c24xx_i2c_state state;
unsigned long clkrate;
void __iomem *regs;
struct clk *clk; //对应的时钟
struct device *dev; //适配器对应的设备结构体
struct resource *ioarea; //适配器对应的资源
struct i2c_adapter adap; //适配器主体结构体
};
第02行的lock自旋锁。
第03行的wait表示等待队列头。由于IIC设备是低速设备,所以可以采用“阻寒,一中断”的驱动模型,即读写i2c设备的用户进程在1IC设备操作期间进入阻塞状,态,待1IC操作完成后总线适配器将引发中断,再相应地在中断处理程序中唤醒受,阻的用户进程。所以在s3c24xx i2c结构体中设计了等待队列首部wait成员,用来将阻塞的进程放入等待队列中。
第05行的i2c msg表示从适配器到设备一次传输的单位,用这个结构体将数据包装起来便于操作,在后面的内容中将详细说明。
第06行的msg num表示消息的个数。
第07行的msg idx表示第几个消息。当完成一个消息后,该值增加。
第08行的msg-ptr总是指向当前交互中要传送、接收的下一个字节,在i2c msg.buf中的偏移位置。
第09行,表示写IIC设备寄存器的一个时间,这里被设置成50毫秒。
第10行,代表IIC设备申请的中断号。
第11行,表示1IC设备目前的状态。这个状态结构体s3c24xx i2c state将在下文详细讲述。
第12行,表示时钟速率。
第13行,表示IIC设备的寄存器地址。
第14行,表示IIC设备对应的时钟。
第16行, ioarea指针指向了适配器申请的资源。
第17行, adap表示内嵌的适配器结构体。
IIC消息
s3c24xx i2c适配器结构体中有一个i2c msg的消息指针。该结构体是从适配器到1C设备传输数据的基本单位,代码如下:
struct i2c_msg {
__u16 addr; /* slave address */
__u16 flags;
#define I2C_M_TEN 0x0010 /* this is a ten bit chip address */
#define I2C_M_RD 0x0001 /* 从从机到主机读数据 */
#define I2C_M_NOSTART 0x4000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_REV_DIR_ADDR 0x2000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_IGNORE_NAK 0x1000 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_NO_RD_ACK 0x0800 /* if I2C_FUNC_PROTOCOL_MANGLING */
#define I2C_M_RECV_LEN 0x0400 /* 第一次接收的字节长度 */
__u16 len; /* 消息字节长度 */
__u8 *buf; /* 指向消息字节缓冲区 */
};
其中addr为IIC设备的地址。这个字段说明一个适配器在获得总线控制权后,可以与多个1IC设备进行交互。
buf指向与IC设备交互的数据缓冲区,其长度为len.
flags中的,标志位描述该消息的属性,这些属性由一系列12C M *宏表示。
2 适配器加载函数 @@@@@@@@@
当驱动开发人员拿到一块新的电路板,并研究了响应的IIC适配器之后,就应该使用内核提供的框架函数向1IC子系统中添加一个新的适配器。
这个过程如下所示:
(1)分配一个1IC适配器,并初始化相应的变量。
(2)使用i2c add adapter()函数向IIC子系统添加适配器结构体i2c adapter,这个结构体已经在第一步初始化了。
i2c add adapter()函数的代码如下:
int i2c_add_adapter(struct i2c_adapter *adapter)
{
int id, res = 0;
retry:
if (idr_pre_get(&i2c_adapter_idr, GFP_KERNEL) == 0)
return -ENOMEM;//分配内存失败
mutex_lock(&core_lock);//上锁
/* "above" here means "above or equal to", sigh */
res = idr_get_new_above(&i2c_adapter_idr, adapter,
__i2c_first_dynamic_bus_num, &id);//id分配
mutex_unlock(&core_lock);//解锁
if (res < 0) {
if (res == -EAGAIN)
goto retry;
return res;
}
adapter->nr = id;
return i2c_register_adapter(adapter);//注册适配器设备
}
该代码的第17行是向内核注册一个适配器设备。第03~16行涉及一个陌生的IDR机制,由于IDR机制较为复杂,下面单独列一节内容加以说明。当了解了IDR机制后,将对 i2c-add -adapte()函数进行进一步解释。
3 IDR机制 @@@@@@@@@
IDR机制在Linux内核中指的就是整数ID管理机制。从实质上来讲,这就是一种将一个整数ID号和一个指针关联在一起的机制。
3.1 IDR机制原理 ------------
IDR机制适用在那些需要把某个整数和特定指针关联在一起的地方。例如,在1IC总线中,每个设备都有自己的地址,要想在总线上找到特定的设备,就必须要先发送该设备的地址。当适配器要访问总线上的IIC设备时,首先要知道它们的ID号,同时要在内核中建立一个用于描述该设备的结构体和驱动程序。
怎么才能将该设备的ID号和它的设备结构体联系起来呢?
数组:进行索引,但如果ID号的范围很大(比如32位的ID号) ,则用数组索引会占据大量的内存空间,这显然不可能
链表:但如果总线中实际存在的设备较多,则链表的查询效率会很低。
这种情况下,就可以采用IDR机制,该机制内部采用红黑树(radix,类似于二分数)实现,可以很方便地将整数和指针关联起来,并且具有很高的搜索效率。IDR机制的主要代码在/include/linux/idr.h实现,下面对其主要函数进行说明。
3.2 结构体
struct idr {
struct idr_layer *top;
struct idr_layer *id_free;
int layers; /* only valid without concurrent changes */
int id_free_cnt;
spinlock_t lock;
};
#define IDR_INIT(name) \
{ \
.top = NULL, \
.id_free = NULL, \
.layers = 0, \
.id_free_cnt = 0, \
.lock = __SPIN_LOCK_UNLOCKED(name.lock), \
}
#define DEFINE_IDR(name) struct idr name = IDR_INIT(name) //定义一个idr结构体
3.3 初始化
void idr_init(struct idr *idp)
{
memset(idp, 0, sizeof(struct idr)); //初始化为0
spin_lock_init(&idp->lock); //初始化自旋锁
}
3.4 分配内存存放ID号的内存
每次通过IDR获得ID号之前,需要为ID号先分配内存。分配内存的函数是, idr_preget()。
成功1 ,错误0
int idr_pre_get(struct idr *idp, gfp_t gfp_mask)
{
while (idp->id_free_cnt < IDR_FREE_MAX) {
struct idr_layer *new;
new = kmem_cache_zalloc(idr_layer_cache, gfp_mask);
if (new == NULL)
return (0);
move_to_free_list(idp, new);
}
return 1;
}
该函数的第一个参数是指向IDR结构体的指针:第二个参数是内存分配标志,与 kmalloc()函数的标志相同。
3.5 分配ID号并将ID号和指针关联
int idr_get_new(struct idr *idp, void *ptr, int *id)
int idr_get_new_above(struct idr *idp, void *ptr, int starting_id, int *id)
- 参数idp是之前通过idr init初始化的idr指针,或者DEFINE IDR宏定义的IDR的指针。
- 参数ptr是和ID号相关联的指针。
- 参数id由内核自动分配的ID号。
- 参数start id是起始ID号。
内核在分配ID号时,会从start id开始。函数调用成功时返回0,如果没有ID可以分配,则返回负数,
3.6 通过ID号查询对应的指针
如果知道了ID号,需要查询对应的指针,可以使用idr find()函数。
void *idr_find(struct idr *idp, int id)
参数idp是之前通过idr init初始化的IDR指针,或者DEFINE IDR宏定义IDR的指针。
参数id是要查询的ID号。如果成功返回,则给定ID相关联的指针,如果没有,则返回NULL.
3.7 删除ID
void idr_remove(struct idr *idp, int id) //删除一个
void idr_remove_all(struct idr *idp) //删除所有
3.8 通过ID获得适配器指针
struct i2c_adapter *i2c_get_adapter(int id)
{
struct i2c_adapter *adapter;//适配器指针
mutex_lock(&core_lock);
adapter = idr_find(&i2c_adapter_idr, id);//查询适配器
if (adapter && !try_module_get(adapter->owner))
adapter = NULL;//适配器模块引用加一
mutex_unlock(&core_lock);
return adapter;
}
3.9 实例代码
retry:
if (idr_pre_get(&i2c_adapter_idr, GFP_KERNEL) == 0)
return -ENOMEM;//分配内存失败
mutex_lock(&core_lock);//上锁
/* 为适配器分配ID号,__i2c_first_dynamic_bus_num是动态分配的最小值 */
res = idr_get_new_above(&i2c_adapter_idr, adapter,
__i2c_first_dynamic_bus_num, &id);//id分配
mutex_unlock(&core_lock);//解锁
if (res < 0) {
if (res == -EAGAIN)
goto retry;
return res;
}
4 适配器卸载函数 @@@@@@@@@
i2c del adapter()函数用于注销适配器的数据结构,删除其总线上所有设备的i2c client数据结构和对应的i2c driver驱动程序,并减少其代表总线上所有设:备的相应驱动程序数据结构的引用计数(如果到达0,则卸载设备驱动程序)。该函数的原型如下:
int i2c_del_adapter(struct i2c_adapter *adap)
5 IIC总线通信方法s3c24xx_i2c_algorithm @@@@@@@@@
static const struct i2c_algorithm s3c24xx_i2c_algorithm = {
.master_xfer = s3c24xx_i2c_xfer,
.functionality = s3c24xx_i2c_func,
};
.2.