二十二、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    //讀取時鐘如日曆
    可以看到如下圖,正確設置了時間與讀取時間。

   

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章