Linux I2C核心、总线与设备驱动

版权声明:转载请声明 https://blog.csdn.net/qq_40732350/article/details/83242265

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_driveri2c_client,我们需要根据具体设备实现其中的成员函数。

I2C体系结构在Linux中的实现复杂。

当工程师拿到实际的电路板时,面对复杂的Linux I2C子系统,应该如何下手写驱动呢?究竟有哪些是需要亲自做的,哪些是内核已经提供
的呢?理清这个问题非常有意义,可以使我们在面对具体问题时迅速抓住重点。

  1. 适配器驱动可能是Linux内核本身还不包含的;
  2. 挂接在适配器上的具体设备驱动可能也是Linux内核还不包含的

因此,工程师要实现的主要工作如下。

  1. 提供
  2. I2C适配器的硬件驱动,
  3. 探测、初始化I2C适配器(如申请I2C的I/O地址和中断号)、
  4. 驱动CPU控制的I2C适配器从硬件上产生各种信号以及处理I2C中断等。
  5. 提供
  6. 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和设备地址组成。

注意事项:

  1. 地址的定义
  2. i2c client数据结构是描述1IC设备的“模板” ,驱动程序的设备结构体中应该包含该结构。
  3. adapter指向设备连接的总线适配器,系统中可能有多个总线适配器。内核中静态指针数组adapters记录所有已经注册的总线适配器设备。
  4. 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总线层 和设备层的关系 @@@@@@@@@@@

总线层

  1. struct i2c_adapter            I2C适配器(主机CPU)
  2. struct i2c_algorithm            I2C算法(时序控制)

设备层

  1. struct i2c_client            I2C(从机)设备信息
  2. 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)
  1. 参数idp是之前通过idr init初始化的idr指针,或者DEFINE IDR宏定义的IDR的指针。
  2. 参数ptr是和ID号相关联的指针。
  3. 参数id由内核自动分配的ID号。
  4. 参数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.

猜你喜欢

转载自blog.csdn.net/qq_40732350/article/details/83242265