二十二、Linux驱动之IIC驱动(基于linux2.6.22.6内核)

1. 基本概念

本文默认读者掌握裸机下的I2C操作,该部分只做简单介绍, 主要内容是对linux-2.6.22.6系统下I2C驱动的分析。

    由数据线SDA和时钟SCL构成的串行总线,可发送和接收数据,是一个多主机的半双工通信方式,每个挂接在总线上的器件都有个唯一的地址。位速在标准模式下可达100kbit/s,在快速模式下可达400kbit/s,在高速模式下可待3.4Mbit/s
    I2C总线仅仅使用SCLSDA这两根信号线就实现了设备之间的数据交互, 极大地简化了对硬件资源和PCB板布线空间的占用。 因此, I2C总线非常广泛地应用在EEPROM、 实时钟、 小型LCD等设备与CPU的接口中。

2. I2C结构

2.1 文件结构

    在linux-2.6.22.6内核的driver/i2c目录下内有如下文件:

   

2.1.1 algos文件夹

    里面保存I2C的通信方面的算法(algorithms)

2.1.2 busses文件夹

    里面保存I2C总线驱动相关的文件,比如i2c-omap.c、 i2c-versatile.c、 i2c-s3c2410.c等。

2.1.3 chips文件夹

    里面保存I2C设备驱动相关的文件,比如ds1374,就是I2C时钟芯片驱动。在具体的I2C设别驱动中,调用的都是I2C核心提供的API,因此,这使得具体的I2C设备驱动不依赖于CPU类型和I2C适配器的硬件特性。

2.1.4 i2c-core.c文件

    这个文件实现了I2C核心的功能(I2C总线的初始化、注册和适配器添加和注销等相关工作)以及/proc/bus/i2c*接口。

2.1.5 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()等接口,应用程序可以借用这些接口访问适配器上的I2C设别的存储空间或寄存器,并控制I2C设别的工作方式。

2.2 I2C体系架构

    I2C体系架构图如下:
   
    主要分为3个部分,I2C核心、I2C总线驱动、I2C设备驱动。

2.2.1 I2C核心

    是Linux内核用来维护和管理的I2C的核心部分,其中维护了两个静态的List,分别记录系统中的I2C driver结构和I2C adapter结构。I2C core提供接口函数,允许一个I2C adatper、I2C driverI2C client初始化时在I2C core中进行注册,以及退出时进行注销。同时还提供了I2C总线读写访问的一般接口。

2.2.2 I2C总线驱动

    I2C总线驱动是对I2C硬件体系结构中适配器端的实现,适配器可由CPU控制,甚至可以直接集成在CPU内部。I2C总线驱动主要包含了I2C适配器数据结构i2c_adapter、I2C适配器的算法i2c_algorithm和控制I2C适配器产生通信信号的函数。经由I2C总线驱动的代码,我们可以控制I2C适配器以主控方式产生开始位、停止位、读写周期,以及以从设备方式被读写、产生ACK等。

2.2.3 I2C设备驱动

    I2C设备驱动是对I2C硬件体系结构中设备端的实现,设备一般挂接在受CPU控制的I2C适配器上,通过I2C适配器与CPU交换数据。I2C设备驱动主要包含了数据结构i2c_driveri2c_client,我们需要根据具体设备实现其中的成员函数。

2.2.4 为什么构建这么复杂的I2C体系架构?

    在单片机中使用I2C只需要简单地配置几个I2C寄存器,构建包含开始位、停止位、读写周期、ACK等简单概念函数即可,为什么在Linux上需要构建这么复杂的I2C体系架构呢?为了通用性及为了符合Linux内核驱动模式。
    对于linux系统来说,支持各式各样的CPU类型,并且还想要满足各种I2C芯片硬件特性,就必须将一些稳定性好的部分抽离出来,这样就抽象出了i2c_client(描述具体的I2C设备)、i2c_driver(描述操作I2C设备方法的接口)、i2c_adpter(描述CPU如何控制I2C总线)、i2c_algorithm(I2C通信算法)等。前两个就是抽象出的I2C设备驱动,后两个为I2C总线驱动。
    在一个SoC上可能有多条I2C总线,一条总线对应一个I2C总线驱动(I2C适配器),每一条总线上又可以接多个I2C设备。假如一个SoC上有3个I2C适配器,外接3个I2C设备,想要3个适配器分别都能驱动3个I2C设备,我们只需写3个适配器代码,3个I2C设备代码即可。

3. 分析内核(linux-2.6.22.6)

    下面开始深入内核分析I2C驱动。

3.1 重要数据结构

    内核的I2C驱动围绕着以下几个数据结构进行盘根交错的展开。

3.1.1 struct i2c_adapter(I2C适配器)

    struct i2c_adapter是用来描述一个I2C适配器,在SoC中的指的就是内部外设I2C控制器,当向I2C核心层注册一个I2C适配器时就需要提供这样的一个结构体变量。

struct i2c_adapter {
	struct module *owner;    //所有者
	unsigned int id;
	unsigned int class;    //该适配器支持的从设备的类型
	const struct i2c_algorithm *algo;     //该适配器与从设备的通信算法
	void *algo_data;

	/* --- administration stuff. */
	int (*client_register)(struct i2c_client *);
	int (*client_unregister)(struct i2c_client *);

	/* data fields that are valid for all devices	*/
	u8 level; 			/* nesting level for lockdep */
	struct mutex bus_lock;
	struct mutex clist_lock;

	int timeout;          //超时时间
	int retries;          //重试次数
	struct device dev;    //该适配器设备对应的device

	int nr;                       //适配器的编号
	struct list_head clients;     //clients链表
	struct list_head list;        //适配器链表
	char name[48];                //适配器的名字
	struct completion dev_released;    //用来挂接与适配器匹配成功的从设备i2c_client的一个链表头
};

3.1.2 struct i2c_algorithm(I2C算法)

    struct i2c_algorithm结构体代表的是适配器的通信算法,在构建i2c_adapter结构体变量的时候会去填充这个元素。

struct i2c_algorithm {
	/* If an adapter algorithm can't do I2C-level access, set master_xfer
	   to NULL. If an adapter algorithm can do SMBus access, set
	   smbus_xfer. If set to NULL, the SMBus protocol is simulated
	   using common I2C messages */
	/* master_xfer should return the number of messages successfully
	   processed, or a negative value on error */
	int (*master_xfer)(struct i2c_adapter *adap,struct i2c_msg *msgs,
	                   int num);
	int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,
	                   unsigned short flags, char read_write,
	                   u8 command, int size, union i2c_smbus_data * data);

	/* --- ioctl like call to set div. parameters. */
	int (*algo_control)(struct i2c_adapter *, unsigned int, unsigned long);

	/* To determine what the adapter supports */
	u32 (*functionality) (struct i2c_adapter *);
};

3.1.3 struct i2c_client(I2C次设备)

    struct i2c_client描述一个I2C次设备。

struct i2c_client {  
    unsigned short flags;        //标志    
    unsigned short addr;         //该I2C设备的设备地址,存放地址高7位  
    char name[I2C_NAME_SIZE];    //设备名字
    struct i2c_adapter *adapter; //依附的i2c_adapter,表示该I2C设备支持哪个适配器  
    struct i2c_driver *driver;   //依附的i2c_driver ,表示该I2C设备的驱动是哪个
    struct device dev;           //设备结构体    
    int irq;                     //设备所使用的结构体    
    struct list_head detected;   //链表头  
 };

3.1.4 struct i2c_driver(I2C设备驱动)

    struct i2c_driver描述一个I2C设备驱动。

struct i2c_driver {
	int id;        //驱动标识
	unsigned int class;    //所属类

	int (*attach_adapter)(struct i2c_adapter *);    //匹配适配器函数指针
	int (*detach_adapter)(struct i2c_adapter *);    //卸载适配器函数指针

	int (*detach_client)(struct i2c_client *);      //通知驱动程序一个client即将被删除,驱动程序之前动态分配的资源必须在这里被释放

	int (*probe)(struct i2c_client *);    //设备驱动层的probe函数
	int (*remove)(struct i2c_client *);   //设备驱动层卸载函数

	/* driver model interfaces that don't relate to enumeration  */
	void (*shutdown)(struct i2c_client *);
	int (*suspend)(struct i2c_client *, pm_message_t mesg);
	int (*resume)(struct i2c_client *);

	int (*command)(struct i2c_client *client,unsigned int cmd, void *arg);    //类似于ioctl的命令接口函数

	struct device_driver driver;    //该i2c设备驱动所对应的device_driver
	struct list_head list;    //用来挂接与该i2c_driver匹配成功的i2c_client(次设备)的一个链表头
};

3.2 分析I2C总线驱动

    内核是最好的学习工具。想要写一个linux驱动,最好的方法就是分析内核自带的驱动程序。对于I2C总线驱动我们可以分析内核源码drivers/i2c/busses/i2c-s3c2410.c文件。

3.2.1 入口函数

static struct platform_driver s3c2440_i2c_driver = {
	.probe		= s3c24xx_i2c_probe,
	.remove		= s3c24xx_i2c_remove,
	.resume		= s3c24xx_i2c_resume,
	.driver		= {
		.owner	= THIS_MODULE,
		.name	= "s3c2440-i2c",
	},
};

static int __init i2c_adap_s3c_init(void)
{
	int ret;

	ret = platform_driver_register(&s3c2410_i2c_driver);
	if (ret == 0) {
		ret = platform_driver_register(&s3c2440_i2c_driver);
		if (ret)
			platform_driver_unregister(&s3c2410_i2c_driver);
	}

	return ret;
}

    首先在platform总线上注册一个platform_driver结构platform总线上有同名字的platform_device被注册或已经注册时调用s3c2440_i2c_driver->probe函数。也就是s3c24xx_i2c_probe()函数。

3.2.2 s3c24xx_i2c_probe()函数

struct i2c_adapter  adap;

static int s3c24xx_i2c_probe(struct platform_device *pdev)
{
    struct s3c24xx_i2c *i2c = &s3c24xx_i2c;
       ... ...

       /*获取,使能I2C时钟*/
       i2c->clk = clk_get(&pdev->dev, "i2c");               //获取i2c时钟
       clk_enable(i2c->clk);                                //使能i2c时钟

       ... ....
       /*获取资源*/
       res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
       i2c->regs = ioremap(res->start, (res->end-res->start)+1);

       ... ....

       /*设置i2c_adapter适配器结构体, 将i2c结构体设为adap的私有数据成员*/
    i2c->adap.algo_data = i2c;          //i2c_adapter适配器指向s3c24xx_i2c;
       i2c->adap.dev.parent = &pdev->dev;

 
    /* initialise the i2c controller */
       /*初始化2440的I2C相关的寄存器*/
       ret = s3c24xx_i2c_init(i2c);
       if (ret != 0)
              goto err_iomap;

       ... ...
       /*注册中断服务函数*/
       ret = request_irq(res->start, s3c24xx_i2c_irq, IRQF_DISABLED,pdev->name, i2c);
       ... ...

       /*注册i2c_adapter适配器结构体*/
       ret = i2c_add_adapter(&i2c->adap);
       ... ...
}

    该函数作用如下:
      1. 设置i2c_adapter适配器结构体。
      2. 初始化2440的I2C相关的寄存器。
      3. 注册中断服务函数。
      4. 注册i2c_adapter适配器结构体。
      
    最后调用i2c_add_adapter()函数添加适配器,i2c_add_adapter()函数实际调用了i2c_register_adapter()函数,函数如下:

static int i2c_register_adapter(struct i2c_adapter *adap)
{
       struct list_head  *item;               //链表头,用来存放i2c_driver结构体的表头
       struct i2c_driver *driver;                     //i2c_driver,用来描述一个IIC设备驱动
        list_add_tail(&adap->list, &adapters);       //添加到内核的adapter链表中
        ... ...
       list_for_each(item,&drivers) {        //for循环,从drivers链表里找到i2c_driver结构体的表头
              driver = list_entry(item, struct i2c_driver, list); //通过list_head表头,找到i2c_driver结构体
              if (driver->attach_adapter)  
                     /* We ignore the return code; if it fails, too bad */
                     driver->attach_adapter(adap);    
                //调用i2c_driver的attach_adapter函数来看看,这个新注册的设配器是否支持i2c_driver
       }
}

    该函数作用如下:
      1. 将i2c_adapter放入i2c_bus_type的adapter链表。
     
2. 遍历drivers链表(该drivers链表的成员是i2c_add_driver()添加设备驱动函数里进行添加的)里的i2c_driver,调用i2c_driver->attach_adapter成员函数进行匹配。
    其中i2c_adapter结构体是s3c24xx_i2c结构体的成员,i2c-s3c2410.c中定义了这么一个全局变量:

static const struct i2c_algorithm s3c24xx_i2c_algorithm = {            
       .master_xfer          = s3c24xx_i2c_xfer,  //主机传输
       .functionality          = s3c24xx_i2c_func,    //协议支持函数                 
};

static struct s3c24xx_i2c s3c24xx_i2c = {
       .lock              = __SPIN_LOCK_UNLOCKED(s3c24xx_i2c.lock),
       .wait              = __WAIT_QUEUE_HEAD_INITIALIZER(s3c24xx_i2c.wait),
       .tx_setup = 50,                        //用来延时,等待SCL被释放
       .adap             = {    // i2c_adapter适配器结构体
              .name                   = "s3c2410-i2c",
              .owner                  = THIS_MODULE,
              .algo                     = &s3c24xx_i2c_algorithm,           //存放i2c_algorithm算法结构体
              .retries           = 2,                                       //重试次数
              .class                    = I2C_CLASS_HWMON,
       },
};

    i2c_adapter结构体的名称等于"s3c2410-i2c",它的通信方式就是s3c24xx_i2c_algorithm结构,retries表示重试次数等于2。s3c24xx_i2c_algorithm中的关键函数master_xfer()调用过程:
    1. 传输数据时,调用s3c24xx_i2c_algorithm结构体中的数据传输函数s3c24xx_i2c_xfer()。
   
2. s3c24xx_i2c_xfer()中会调用s3c24xx_i2c_doxfer()进行数据的传输。
   
3. s3c24xx_i2c_doxfer()中向总线 发送IIC设备地址和开始信号S后,便会调用wati_event_timeout()函数进入等待状态。
   
4. 将数据准备好发送时,将产生中断,并调用实现注册的中断处理函数s3c24xx_i2c_irq()。
   
5. s3c24xx_i2c_irq()调用下一个字节传输函数i2s_s3c_irq_nextbyte()来传输数据。
   
6. 当数据传输完成后,会调用 s3c24xx_i2c_stop()。
   
7. 最后调用wake_up()唤醒等待队列,完成数据的传输过程。

    在后面要讲的I2C设备驱动进行读写I2C设备时,最终就会调用到master_xfer

3.2.3 总结I2C总线驱动

    i2c-s3c2410.c文件为我们实现了对2440I2C控制器的设置、I2C通信协议相关代码等,这些就组成了一个适配器adapter(I2C总线驱动),使用这个适配器内核就知道如何与挂在I2C总线上的I2C设备通信了。

3.3 分析I2C设备驱动

    I2C总线驱动让内核知道了怎么发数据,那么I2C设备驱动就是让内核知道什么时候发数据和发什么数据。
    内核自带的I2C设备驱动有很多,框架都是一样的,这里以linux-2.6.22.6/driver/i2c/chips/ds1374.c为例进行分析。

3.3.1 入口函数

    首先进入ds1374.c驱动的入口ds1374_init()函数:

static struct i2c_driver ds1374_driver = {
	.driver = {
		.name	= DS1374_DRV_NAME,    //名称
	},
	.id = I2C_DRIVERID_DS1374,    //IIC设备标识ID
	.attach_adapter = ds1374_attach,    //用来与总线上的adapter链表上的adapter适配器匹配,匹配成功添加该i2c_driver到适配器adapter中
	.detach_client = ds1374_detach,    //与总线上的adapter适配器解绑,分离这个IIC从设备
};
... ...
static int __init ds1374_init(void)
{
	return i2c_add_driver(&ds1374_driver);    //向内核注册一个i2c_driver结构体
}

    看看i2c_add_driver()函数做了什么,代码如下:

static inline int i2c_add_driver(struct i2c_driver *driver)
{
	return i2c_register_driver(THIS_MODULE, driver);
}

    直接调用i2c_register_driver()函数:

int i2c_register_driver(struct module *owner, struct i2c_driver *driver)
{
	int res;
        ... ...

	driver->driver.owner = owner;
	driver->driver.bus = &i2c_bus_type;    //将i2c_driver放在i2c_bus_type链表中

	res = driver_register(&driver->driver);    //注册driver
        ... ...

	list_add_tail(&driver->list,&drivers);    //将该i2c_driver加入i2c_driver链表

	if (driver->attach_adapter) {
		struct i2c_adapter *adapter;

                /*遍历adapters链表上的i2c_adapter适配器匹配该i2c_driver,
                匹配函数为该i2c_driver的attach_adapter成员函数*/
		list_for_each_entry(adapter, &adapters, list) {
			driver->attach_adapter(adapter);    
		}
	}
        ... ...
	return 0;
}

    该函数作用如下:
     
1. i2c_driver添加到i2c_bus_type链表中。
      2. 取出adapters链表中所有的i2c_adapter,然后调用i2c_driver->attach_adapter()函数。
    如下图可以看出,无论是先注册i2c_adapter适配器还是先注册i2c_driver,都会调用到i2c_driver->attach_adapter()函数进行适配器与驱动的匹配。
   

3.3.2 attach_adapter函数

    下面看看attach_adapter函数也就是ds1374_attach()函数:

static int ds1374_attach(struct i2c_adapter *adap)
{
	return i2c_probe(adap, &addr_data, ds1374_probe);
}

    只是调用了i2c_probe()函数,传进来3个参数:
      1. i2c_adapter适配器。
      2. addr_data变量,里面存放了I2C设备地址的信息。
      3. 具体的设备探测回调函数ds1374_probe
    addr_data变量是一个struct i2c_client_address_data结构体,该结构体原型如下:

struct i2c_client_address_data {
       unsigned short *normal_i2c;     //存放正常的设备地址,适配器会去检验该设备地址是否存在总线上
       unsigned short *probe;          //存放探测的设备地址,适配器会去检验该设备地址是否存在总线上
       unsigned short *ignore;         //可以存放I2C_CLIENT_END这个宏
       unsigned short **forces;        //存放强制的设备地址,适配器不检验该设备地址是否存在总线上
};

    当上面结构体的数组成员以I2C_CLIENT_END结尾,则表示地址已结束。看这个结构体如何定义的:

static unsigned short ignore[] = { I2C_CLIENT_END };
static unsigned short normal_addr[] = { 0x68, I2C_CLIENT_END };

static struct i2c_client_address_data addr_data = {
	.normal_i2c = normal_addr,
	.probe = ignore,
	.ignore = ignore,
};

    i2c_probe()函数通过判断addr_data结构体成员,以不同的参数调用i2c_probe_address()函数,主要有两种效果
      1. 适配器会去检测I2C总线上是否确实挂接有addr_data(probe成员或者normal_i2c成员)里指定的I2C设备地址的设备。通过判断*normal_i2c或者*probe是否有非I2C_CLIENT_END
      2. 强制认为I2C总线上存在addr_data(forces成员里指定的I2C设备地址的设备。
   
i2c_probe()函数部分代码如下:

int i2c_probe(struct i2c_adapter *adapter,
	      struct i2c_client_address_data *address_data,
	      int (*found_proc) (struct i2c_adapter *, int, int))
{
        ... ...

        /*如果address_data指定了强制条目forces*/
        /*调用i2c_probe_address函数,第3个参数大于等于0*/
	if (address_data->forces) {
	    unsigned short **forces = address_data->forces;
	    int kind;
            ... ...
	    err = i2c_probe_address(adapter,forces[kind][i + 1],kind, found_proc);
	    ... ...
	}

        ... ...
        
        /*如果address_data指定了检测条目probe*/
        /*调用i2c_probe_address函数,第3个参数为-1*/
	for (i = 0; address_data->probe[i] != I2C_CLIENT_END; i += 2) {
            ... ...
	    err = i2c_probe_address(adapter,address_data->probe[i + 1],-1, found_proc);
            ... ...
	}

        /*如果address_data指定了正常条目normal_i2c*/
        /*调用i2c_probe_address函数,第3个参数为-1*/
	for (i = 0; address_data->normal_i2c[i] != I2C_CLIENT_END; i += 1) {
            ... ...
	    err = i2c_probe_address(adapter, address_data->normal_i2c[i],-1, found_proc);
            ... ...
	}

	return 0;
}

    看看i2c_probe_address()函数里具体做了什么:

static int i2c_probe_address(struct i2c_adapter *adapter, int addr, int kind,
			     int (*found_proc) (struct i2c_adapter *, int, int))
{
        ... ...

	/* 确保有addr中的设备地址的芯片接在I2C总线上(发送该设备地址能收到ACK) */
	if (kind < 0) {
		if (i2c_smbus_xfer(adapter, addr, 0, 0, 0,
				   I2C_SMBUS_QUICK, NULL) < 0)
			return 0;
                ... ...
	}
        ... ...

	/* 调用i2c_probe中传入的第三个参数(检测函数) */
        /* 到这里表示I2C总线上确实存在该设备地址的I2C设备,但是可能有些芯片的设备地址是一样的,在该函数里进行区分具体的芯片 */
	err = found_proc(adapter, addr, kind);
	if (err == -ENODEV)
		err = 0;
        ... ...
	return err;
}

    该函数作用如下:
      1. 调用i2c_smbus_xfer()函数检测是否确实有addr中的设备地址的芯片接在I2C总线上发送设备地址是否能收到ACK)
     
2. 调用i2c_probe()中传入的第三个参数,也就是到这里表示I2C总线上确实存在该设备地址的I2C设备,但是可能有些芯片的设备地址是一样的,在该函数里进行区分具体的芯片)。

3.3.3 i2c_smbus_xfer()函数

    接下来看看i2c_smbus_xfer()函数是如何检验i2c_probe()函数传入的addr_data中的设备地址的,i2c_smbus_xfer()函数部分代码如下:

s32 i2c_smbus_xfer(struct i2c_adapter * adapter, u16 addr, unsigned short flags,char read_write, u8 command, int size,union i2c_smbus_data * data)
{
       s32 res;

       flags &= I2C_M_TEN | I2C_CLIENT_PEC;

       if (adapter->algo->smbus_xfer) {   //如果adapter适配器有smbus_xfer这个函数
              mutex_lock(&adapter->bus_lock);                            //加互斥锁
              res = adapter->algo->smbus_xfer(adapter,addr,flags,read_write,command,size,data);  
                                            //调用adapter适配器里的传输函数
              mutex_unlock(&adapter->bus_lock);                  //解互斥锁
       } else                          //否则使用默认函数传输设备地址
              res = i2c_smbus_xfer_emulated(adapter,addr,flags,read_write,command,size,data);
       return res;
}

    该函数首先判断i2c_adapter适配器成员algo->smbus_xfer函数,可以回顾一下i2c-s3c2410.c中的i2c_adapter适配器是如何定义的:

static const struct i2c_algorithm s3c24xx_i2c_algorithm = {            
       .master_xfer          = s3c24xx_i2c_xfer,  //主机传输
       .functionality          = s3c24xx_i2c_func,    //协议支持函数                 
};

static struct s3c24xx_i2c s3c24xx_i2c = {
       .lock              = __SPIN_LOCK_UNLOCKED(s3c24xx_i2c.lock),
       .wait              = __WAIT_QUEUE_HEAD_INITIALIZER(s3c24xx_i2c.wait),
       .tx_setup = 50,                        //用来延时,等待SCL被释放
       .adap             = {    // i2c_adapter适配器结构体
              .name                   = "s3c2410-i2c",
              .owner                  = THIS_MODULE,
              .algo                     = &s3c24xx_i2c_algorithm,           //存放i2c_algorithm算法结构体
              .retries           = 2,                                       //重试次数
              .class                    = I2C_CLASS_HWMON,
       },
};

    可见适配器并没有algo->smbus_xfer函数,所以i2c_smbus_xfer()函数实际是使用i2c_smbus_xfer_emulated()函数进行发送数据的,i2c_smbus_xfer_emulated()函数部分代码如下:

static s32 i2c_smbus_xfer_emulated(struct i2c_adapter * adapter, u16 addr,unsigned short flags,char read_write, u8 command, int size, union i2c_smbus_data * data)
{
       unsigned char msgbuf0[I2C_SMBUS_BLOCK_MAX+3];              //属于 msg[0]的buf成员
       unsigned char msgbuf1[I2C_SMBUS_BLOCK_MAX+2];              //属于 msg[1]的buf成员
       int num = read_write == I2C_SMBUS_READ?2:1;              //如果为读命令,就等于2,表示要执行两次数据传输
       struct i2c_msg msg[2] = { { addr, flags, 1, msgbuf0 },
                    { addr, flags | I2C_M_RD, 0, msgbuf1 }};           //定义两个i2c_msg结构体,


       msgbuf0[0] = command;             //IIC设备地址最低位为读写命令
       ... ...

       if (i2c_transfer(adapter, msg, num) < 0)
              return -1;

       /*设置i2c_msg结构体成员*/
       if (read_write == I2C_SMBUS_READ)
       switch(size) {
       ... ...
       /*读操作需要两个i2c_msg,写操作需要一个i2c_msg*/
       case I2C_SMBUS_BYTE_DATA:              
          if (read_write == I2C_SMBUS_READ)    //如果是读字节
              msg[1].len = 1;
          else {
                   msg[0].len = 2;
                   msgbuf0[1] = data->byte;
          }
          break;
          ... ...
       }
       ... ...

       if (i2c_transfer(adapter, msg, num) < 0)    //将i2c_msg结构体的内容发送给I2C设备
              return -1;    //返回值不等于0表示没有收到ACK
       ... ...
}

    该函数主要作用如下:
      1. 根据发送类型构造i2c_msg结构体
      2. 使用i2c_transfer()函数发送i2c_msg
   
其中i2c_msg结构体的结构如下所示:

struct i2c_msg {
       __u16 addr;          //I2C从机的设备地址
       __u16 flags;           //当flags=0表示写, flags= I2C_M_RD表示读
       __u16 len;              //传输的数据长度,等于buf数组里的字节数
       __u8 *buf;              //存放数据的数组
};

    i2c_transfer()函数部分代码如下:

int i2c_transfer(struct i2c_adapter * adap, struct i2c_msg *msgs, int num)
{
	int ret;

	if (adap->algo->master_xfer) {
        ... ...
	ret = adap->algo->master_xfer(adap,msgs,num);
        ... ...
	return ret;
	} 
        ... ...
}

    最终是调用到了I2C总线驱动里注册的adapter适配器的算法函数master_xfer发送i2c_msg。再次回顾一下3.2.2中分析的master_xfer()调用过程:
    1. 传输数据时,调用s3c24xx_i2c_algorithm结构体中的数据传输函数s3c24xx_i2c_xfer()。
   
2. s3c24xx_i2c_xfer()中会调用s3c24xx_i2c_doxfer()进行数据的传输。
   
3. s3c24xx_i2c_doxfer()中向总线发送IIC设备地址和开始信号S后,便会调用wati_event_timeout()函数进入等待状态。
   
4. 将数据准备好发送时,将产生中断,并调用实现注册的中断处理函数s3c24xx_i2c_irq()。
   
5. s3c24xx_i2c_irq()调用下一个字节传输函数i2s_s3c_irq_nextbyte()来传输数据。
   
6. 当数据传输完成后,会调用 s3c24xx_i2c_stop()。
   
7. 最后调用wake_up()唤醒等待队列,完成数据的传输过程。

    其中第5点调用i2s_s3c_irq_nextbyte()函数中有如下代码:

if (iicstat  & S3C2410_IICSTAT_LASTBIT &&
    !(i2c->msg->flags & I2C_M_IGNORE_NAK)) {
	/* 如果没有收到ACK */

	dev_dbg(i2c->dev, "ack was not received\n");
	s3c24xx_i2c_stop(i2c, -EREMOTEIO);
	goto out_ack;
}

    当使用适配器发送设备地址没有收到ACK会有如下调用:
      s3c24xx_i2c_stop(i2c, -EREMOTEIO);
          s3c24xx_i2c_master_complete(i2c, ret);   
// ret=-EREMOTEIO
                  if (ret)
                      i2c->msg_idx = ret;   
// i2c->msg_idx=-EREMOTEIO
   
i2c_smbus_xfer()又通过以下调用得到返回值-EREMOTEIO
      i2c_smbus_xfer()
          i2c_transfer()
              s3c24xx_i2c_xfer()
                  s3c24xx_i2c_doxfer
                      ret = i2c->msg_idx;   
// ret=-EREMOTEIO
                          return ret;
  
 
i2c_smbus_xfer()返回值为0表示正确收到ACK,返回值-EREMOTEIO表示没有收到ACK。也就是说当i2c_smbus_xfer()函数的返回值为0表示I2C总线上确实存在该设备地址。存在的话i2c_probe_address()函数就继续往下执行,调用i2c_probe()中传入的第三个参数(回调函数)。对于ds1374.c这个设备驱动,回调函数即ds1374_probe()

3.3.4 ds1374_probe()函数

    ds1374_probe()函数部分代码如下:

static int ds1374_probe(struct i2c_adapter *adap, int addr, int kind)
{
	struct i2c_client *client;    //定义一个i2c_client结构体局部变量
	int rc;

	client = kzalloc(sizeof(struct i2c_client), GFP_KERNEL);    //分配内存
	if (!client)
		return -ENOMEM;

        /*设置i2c_client结构体*/
	strncpy(client->name, DS1374_DRV_NAME, I2C_NAME_SIZE);    //设置名字
	client->addr = addr;    //设置设备地址
	client->adapter = adap;    //设置适配器i2c_adapter
	client->driver = &ds1374_driver;    //设置i2c_driver

	ds1374_workqueue = create_singlethread_workqueue("ds1374");    //创建单线程队列
	if (!ds1374_workqueue) {
		kfree(client);
		return -ENOMEM;	/* most expected reason */
	}
        
        /*注册i2c_client*/
	if ((rc = i2c_attach_client(client)) != 0) {
		kfree(client);    //注册失败,便释放i2c_client这个全局变量
		return rc;
	}

	save_client = client;

	ds1374_check_rtc_status();    //检验ds1374芯片状态

	return 0;
}

    该函数主要内容如下:
      1. 定义一个i2c_client结构体
     
2. 分配相关内存
     
3. 设置i2c_client结构体
     
4. 注册i2c_client
    当注册了i2c_client从设备后,便可以使用i2c_transfer()函数(实际调用了s3c24xx_i2c_xfer()函数)来实现与I2C设备传输数据了。

3.3.5 总结I2C设备驱动

    使用i2c_add_driver()注册i2c_driver,在该函数里面匹配并使用适配器(通信协议)发送设备地址检测该设备是否真实存在(使用强制地址则不用检测),然后注册i2c_client(用来描述具体的I2C设备信息),并将对应的适配器与该i2c_client关联起来,接在便可以使用i2c_transfer()函数读写I2C设备了。(想要在应用程序里读写设备,可以在注册完i2c_client后创建一个字符设备进行读写芯片

3.4 I2C驱动调用框图

    I2C总线驱动与I2C设备驱动的调用框图如下:

4. 编写代码

    本节内容不进行I2C总线驱动的编写,只编写I2C设备驱动部分代码。由于使用的JZ2440开发板上没有接I2C设备,所以通过杜邦线外接一个DS3231时钟芯片,后面将会编写一个I2C设备驱动来使用DS3231。

4.1 DS3231介绍

    DS3231是低成本、高精度I2C实时时钟RTCRTC保存秒、分、时、星期、日期、月和年信息。少于31天的月份,将自动调整月末的日期,包括闰年的修正。时钟的工作格式可以是24小时或带/AM/PM指示的12小时格式。提供两个可设置的日历闹钟和一个可设置的方波输出。地址与数据通过I2C双向总线串行传输。详细芯片资料参考:https://html.alldatasheet.com/html-pdf/112132/DALLAS/DS3231/2425/11/DS3231.html

4.1.1 原理图

    原理图如下(DS3231时钟芯片通过杜邦线连接到开发板):

4.1.2 相关寄存器

    ds3231其中的一组时钟和日历相关寄存器如下图所示:

通过芯片手册可得以下信息:  
    1. 通过读写00h~06h这7个寄存器地址就能读取与设置时钟和日历了。
    2. 从设备地址为7位ds3231地址1101000

4.2 编写代码

    设备驱动ds3231.c完整代码如下:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/interrupt.h>
#include <linux/i2c.h>
#include <linux/rtc.h>
#include <linux/bcd.h>
#include <linux/mutex.h>
#include <linux/workqueue.h>

#define DS3231_REG_SEC		0x00	//秒
#define DS3231_REG_MIN		0x01	//分
#define DS3231_REG_HOURS	0x02	//时
#define DS3231_REG_DAY		0x03	//星期
#define DS3231_REG_DATE		0x04	//日
#define DS3231_REG_MOUNTH	0x05	//月
#define DS3231_REG_YEAR		0x06	//年

#define	DS3231_DRV_ADDR		0x68	//ds3231从设备地址
#define	DS3231_DRV_NAME		"ds3231"

static struct i2c_driver ds3231_driver;
static struct i2c_client *ds3231_client;

int major;	//主设备号
static struct cdev ds3231_cdev;
static struct class *ds3231_class;

static unsigned short ignore[] = { I2C_CLIENT_END };
static unsigned short normal_addr[] = { DS3231_DRV_ADDR, I2C_CLIENT_END };
//static unsigned short force_addr[] = {ANY_I2C_BUS, DS3231_DRV_ADDR, I2C_CLIENT_END};
//static unsigned short * forces[] = {force_addr, NULL};

static struct i2c_client_address_data addr_data = {
	.normal_i2c = normal_addr,
	.probe 		= ignore,
	.ignore 	= ignore,
	//.forces 	= forces,	/* 强制认为存在这个设备 */
};

static ssize_t ds3231_read(struct file *file, char __user *buf, size_t size, loff_t * offset)
{
	unsigned char time[7];
	int reg = DS3231_REG_YEAR;
	int i=sizeof(time);
	
	if (size != sizeof(time))
		return -EINVAL;

	/* 实际调用了i2c_smbus_xfer函数发送数据
	 * 循环读取时钟与日历寄存器数据,读出来的数据是BCD码,
	 * 先转换成16进制并拷贝给应用程序
	 */
	for (reg = DS3231_REG_YEAR; reg >= DS3231_REG_SEC; reg--){
		int tmp;
		if ((tmp = i2c_smbus_read_byte_data(ds3231_client, reg)) < 0) {
			dev_warn(&ds3231_client->dev,
				 "can't read from ds3231 chip\n");
			return -EIO;
		}
		time[--i] = BCD2BIN(tmp) & 0xff;
	}
	
	return copy_to_user(buf, &time, sizeof(time));
}

static ssize_t ds3231_write(struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
	unsigned char time[7];
	int reg;
	int i=0;
	
	if (size != sizeof(time))
		return -EINVAL;

	/* 拷贝要写入的7个16进制时钟与日历值 */
	if(copy_from_user(time, buf, sizeof(time)))
		return -EINVAL;

	/* 实际调用了i2c_smbus_xfer函数发送数据
	 * 循环写入转换成BCD码的时钟与日历值
	 */
	for (reg = DS3231_REG_SEC; reg <= DS3231_REG_YEAR; reg++) {
		if (i2c_smbus_write_byte_data(ds3231_client, reg, BIN2BCD(time[i]))< 0) {
			dev_warn(&ds3231_client->dev,
				 "can't write to ds3231 chip\n");
			return -EINVAL;
		}
		i++;
	}
	return sizeof(time);
}

static struct file_operations ds3231_fops = {
    .owner  =   THIS_MODULE,
    .write  =   ds3231_write,     
	.read	=	ds3231_read,	   
};

static int ds3231_probe(struct i2c_adapter *adap, int addr, int kind)
{
	int err;
	dev_t devid;

	ds3231_client = kzalloc(sizeof(struct i2c_client), GFP_KERNEL);	// 分配内存
	if (!ds3231_client)
		return -ENOMEM;

	/* 设置i2c_client结构体 */
	strcpy(ds3231_client->name, DS3231_DRV_NAME);	//设置名字
	ds3231_client->addr = addr;	//设置从设备地址
	ds3231_client->adapter = adap;	//设置适配器i2c_adapter
	ds3231_client->driver = &ds3231_driver;	//设置i2c_driver

	/* 注册i2c_client结构体 */
	if ((err = i2c_attach_client(ds3231_client)) != 0) {
		kfree(ds3231_client);
		return err;
	}

	/* 创建字符设备 */	
	devid = MKDEV(major, 0);	//从主设备号major,次设备号0得到dev_t类型
	if (major) 
	{
		err=register_chrdev_region(devid, 1, "ds3231");	//注册字符设备
	} 
	else 
	{
		err=alloc_chrdev_region(&devid, 0, 1, "ds3231");	//注册字符设备
		major = MAJOR(devid);	//从dev_t类型得到主设备
	}
	if(err < 0)
		return err;
	
	cdev_init(&ds3231_cdev, &ds3231_fops);
	cdev_add(&ds3231_cdev, devid, 1);
	
	ds3231_class = class_create(THIS_MODULE, "ds3231");
	class_device_create(ds3231_class, NULL, MKDEV(major, 0), NULL, "ds3231"); /* /dev/ds3231 */

	return err;
}

/*回调函数*/
static int ds3231_attach(struct i2c_adapter *adap)
{
	return i2c_probe(adap, &addr_data, ds3231_probe);
}

/*注销函数*/
static int ds3231_detach(struct i2c_client *client)
{
	int rc;

	printk("ds3231_detach !\n");
	class_device_destroy(ds3231_class, MKDEV(major, 0));
	class_destroy(ds3231_class);
	cdev_del(&ds3231_cdev);
	unregister_chrdev_region(MKDEV(major, 0), 1);
	
	if ((rc = i2c_detach_client(client)) == 0) {
		kfree(i2c_get_clientdata(client));
	}
	return rc;
}

static struct i2c_driver ds3231_driver = {
	.driver = {
		.name	= DS3231_DRV_NAME,
	},
	.attach_adapter = ds3231_attach,	//回调函数
	.detach_client = ds3231_detach,		//注销函数
};

static int ds3231_init(void)
{
	return i2c_add_driver(&ds3231_driver);
}

static void ds3231_exit(void)
{
	i2c_del_driver(&ds3231_driver);
}

module_init(ds3231_init);
module_exit(ds3231_exit);

MODULE_AUTHOR("LVZHENHAI");
MODULE_LICENSE("GPL");

    测试程序ds3231_test.c完整代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

/* ./ds3231_test r    //读时钟与日历格式
 * ./ds3231_test w Year Mounth Date Day Hour Min Sec    //设置时钟与日历格式
 */

void print_usage(char *file)
{
	printf("%s r\n", file);
	printf("%s w Year Mounth Date Day Hour Min Sec\n", file);
}

int main(int argc, char **argv)
{
	int fd;
	unsigned char buf[7];
	int i;
	
	if ((argc != 2) && (argc != 9))
	{
		print_usage(argv[0]);
		return -1;
	}

	fd = open("/dev/ds3231", O_RDWR);
	if (fd < 0)
	{
		printf("can't open /dev/ds3231\n");
		return -1;
	}

	if (strcmp(argv[1], "r") == 0)	//读取时钟与日历
	{
		read(fd, buf, sizeof(buf));
		printf("Now Time= Year:%02d, Mounth:%02d, Date:%02d, Day:%d, Hour:%02d, Min:%02d, Sec:%02d\n", \
			 buf[6], buf[5],buf[4], buf[3], buf[2],buf[1],buf[0]);
	}
	else if (strcmp(argv[1], "w") == 0)	//写时钟与日历
	{
		for(i=0;i<7;i++)
		{
			buf[6-i] = strtoul(argv[i+2], NULL, 0);	//将字符转换成数值
		}
		printf("Set Time Year:%02d, Mounth:%02d, Date:%02d, Day:%d, Hour:%02d, Min:%02d, Sec:%02d\n", \
		buf[6], buf[5],buf[4], buf[3], buf[2],buf[1],buf[0]);
		write(fd, buf, sizeof(buf));
	}
	else
	{
		print_usage(argv[0]);
		return -1;
	}
	
	return 0;
}

    Makefile代码如下:

KERN_DIR = /work/system/linux-2.6.22.6    //内核目录
 
all:
	make -C $(KERN_DIR) M=`pwd` modules 
 
clean:
	make -C $(KERN_DIR) M=`pwd` modules clean
	rm -rf modules.order
 
obj-m	+= ds3231.o

5. 测试

内核:linux-2.6.22.6
编译器:arm-linux-gcc-3.4.5
环境:ubuntu9.10

    将ds3231.c、ds3231_text.c、Makefile三个文件放入网络文件系统内,在ubuntu该目录下执行:
      make
      arm-linux-gcc -o ds3231_text ds3231_text.c

    在挂载了网络文件系统的开发板上进入相同目录,执行“ls”查看:
   
    执行如下命令装载驱动与测试:
      insmod ds3231.ko    //装载驱动
      ./ds3231_test w 18 12 31 1 23 59 55    //设置时间为:18年12月31日,星期一,23点59分55秒
      ./ds3231_test r    //读取时钟如日历
    可以看到如下图,正确设置了时间与读取时间。

   

  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值