1. 基本概念
本文默認讀者掌握裸機下的I2C操作,該部分只做簡單介紹, 主要內容是對linux-2.6.22.6系統下I2C驅動的分析。
由數據線SDA和時鐘SCL構成的串行總線,可發送和接收數據,是一個多主機的半雙工通信方式,每個掛接在總線上的器件都有個唯一的地址。位速在標準模式下可達100kbit/s,在快速模式下可達400kbit/s,在高速模式下可待3.4Mbit/s。
I2C總線僅僅使用SCL、 SDA這兩根信號線就實現了設備之間的數據交互, 極大地簡化了對硬件資源和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 driver和I2C 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_driver和i2c_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文件爲我們實現了對2440上I2C控制器的設置、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實時時鐘RTC,RTC保存秒、分、時、星期、日期、月和年信息。少於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 //讀取時鐘如日曆
可以看到如下圖,正確設置了時間與讀取時間。