??? Linux I2C驅動是嵌入式Linux驅動開發人員經常需要編寫的一種驅動,因為凡是系統中使用到的I2C設備,幾乎都需要編寫相應的I2C驅動去配置和控制它,例如 RTC實時時鐘芯片、音視頻采集芯片、音視頻輸出芯片、EEROM芯片、AD/DA轉換芯片等等。
??? Linux I2C驅動涉及的知識點還是挺多的,主要分為Linux I2C的總線驅動(I2C BUS Driver)和設備驅動(I2C Clients Driver),本文主要關注如何快速地完成一個具體的I2C設備驅動(I2C Clients Driver)。關于Linux I2C驅動的整體架構、核心原理等可以在網上搜索其他相關文章學習。
? ? 注意:本系列文章的I2C設備驅動是基于Linux 2.6.18內核。
??? 本文主要參考了Linux內核源碼目錄下的 ./Documentation/i2c/writing-clients 文檔。以手頭的一款視頻采集芯片TVP5158為驅動目標,編寫Linux I2C設備驅動。
1.???i2c_driver結構體對象?????
??? 每一個I2C設備驅動,必須首先創造一個i2c_driver結構體對象,該結構體包含了I2C設備探測和注銷的一些基本方法和信息,示例如下:
- static?struct?i2c_driver?tvp5158_i2c_driver?=?{ ?
- ????????.driver?=?{ ?
- ????????????.name?=?"tvp5158_i2c_driver", ?
- ????????}, ??
- ????????.attach_adapter?=?&tvp5158_attach_adapter, ?
- ????????.detach_client??=?&tvp5158_detach_client, ?
- ????????.command????????=?NULL, ?
- }; ?
??? 其中,name字段標識本驅動的名稱(不要超過31個字符),attach_adapter和detach_client字段為函數指針,這兩個函數在I2C設備注冊的時候會自動調用,需要自己實現這兩個函數,后面將詳細講述。
2.?? i2c_client 結構體對象
??? 上面定義的i2c_driver對象,抽象為一個i2c的驅動模型,提供對i2C設備的探測和注銷方法,而i2c_client結構體則是代表著一個具體的i2c設備,該結構體有一個data指針,可以指向任何私有的設備數據,在復雜點的驅動中可能會用到。示例如下:???
- struct?tvp5158_obj{?????? ?
- ????struct?i2c_client?client;?????? ?
- ????int?users;?//?how?many?users?using?the?driver?? ?
- }; ?
- ?
- struct?tvp5158_obj*?g_tvp5158_obj;??
??? 其中,users為示例,用戶可以自己在tvp5158_obj這個結構體里面添加感興趣的字段,但是i2c_client字段不可少。具體用法后面再詳細講。
3.?? 設備注冊及探測功能
??? 這一步很關鍵,按照標準的要求來寫,則Linux系統會自動調用相關的代碼去探測你的I2C設備,并且添加到系統的I2C設備列表中以供后面訪問。
??? 我們知道,每一個I2C設備芯片,都通過硬件連接設定好了該設備的I2C設備地址。因此,I2C設備的探測一般是靠設備地址來完成的。那么,首先要在驅動代碼中聲明你要探測的I2C設備地址列表,以及一個宏。示例如下:
- static?unsigned?short?normal_i2c[]?=?{ ?
- ????????0xbc?>>?1, ?
- ????????0xbe?>>?1, ?
- ????????I2C_CLIENT_END ?
- }; ?
- I2C_CLIENT_INSMOD; ?
??? normal_i2c 數組包含了你需要探測的I2C設備地址列表,并且必須以I2C_CLIENT_END作為結尾,注意,上述代碼中的0xbc和0xbe是我在硬件上為我的tvp5158分配的地址,硬件上我支持通過跳線將該地址設置為 0xbc 或者 0xbe,所以把這兩個地址均寫入到探測列表中,讓系統進行探測。如果你的I2C設備的地址是固定的,那么,這里可以只寫你自己的I2C設備地址,注意必須向右移位1。
??? 宏 I2C_CLIENT_INSMOD 的作用網上有許多文章進行了詳細的講解,這里我就不詳細描述了,記得加上就行,我們重點關注實現。
??? 下一步就應該編寫第1步中的兩個回調函數,一個用于注冊設備,一個用于注銷設備。探測函數示例如下:
- static?int?tvp5158_attach_adapter(struct?i2c_adapter?*adapter) ?
- { ?
- ????return?i2c_probe(adapter,?&addr_data,?&tvp5158_detect_client); ?
- }?
??? 這個回調函數系統會自動調用,我們只需要按照上述代碼形式寫好就行,這里調用了系統的I2C設備探測函數,i2c_probe(),第三個參數為具體的設備探測回調函數,系統會在探測設備的時候調用這個函數,需要自己實現。示例如下:
- static?int?tvp5158_detect_client(struct?i2c_adapter?*adapter,int?address,int?kind) ?
- { ?
- ????struct?tvp5158_obj?*pObj; ?
- ????int?err?=?0; ?
- ?
- ????printk(KERN_INFO?"I2C:?tvp5158_detect_client?at?address?%x?...\n",?address); ?
- ?
- ????if(?g_tvp5158_obj?!=?NULL??)?{ ?
- ????????//already?allocated,inc?user?count,?and?return?the?allocated?handle ?
- ????????g_tvp5158_obj->users++; ?
- ????????return?0; ?
- ????} ?
- ?
- ????/*?alloc?obj?*/?
- ????pObj?=?kmalloc(sizeof(struct?tvp5158_obj),?GFP_KERNEL); ?
- ????if?(pObj==0){ ?
- ????????return?-ENOMEM; ?
- ????} ?
- ????memset(pObj,?0,?sizeof(struct?tvp5158_obj)); ?
- ????pObj->client.addr????=?address; ?
- ????pObj->client.adapter?=?adapter; ?
- ????pObj->client.driver??=?&tvp5158_i2c_driver; ?
- ????pObj->client.flags???=?I2C_CLIENT_ALLOW_USE; ?
- ????pObj->users++; ?
- ?
- ????/*?attach?i2c?client?to?sys?i2c?clients?list?*/?
- ????if((err?=?i2c_attach_client(&pObj->client))){ ?
- ????????printk(?KERN_ERR?"I2C:?ERROR:?i2c_attach_client?fail!?address=%x\n",address); ?
- ????????return?err; ?
- ????} ?
- ?
- ????//?store?the?pObj ?
- ????g_tvp5158_obj?=?pObj; ?
- ?
- ????printk(?KERN_ERR?"I2C:?i2c_attach_client?ok!?address=%x\n",address); ?
- ?
- ????return?0; ?
- }?
??? 到此為止,探測并且注冊設備的代碼已經完成,以后對該??I2C?設備的訪問均可以通過 g_tvp5158_obj 這個全局的指針進行了。
4.??? 注銷I2C設備?
??? 同理,設備注銷的回調函數也會自動被系統調用,只需要按照模板寫好設備注銷代碼,示例如下:????
- static?int?tvp5158_detach_client(struct?i2c_client?*client) ?
- { ?
- ????int?err; ?
- ?
- ????if(?!?client->adapter?){ ?
- ????????return?-ENODEV; ?
- ????} ?
- ?
- ????if(?(err?=?i2c_detach_client(client))?)?{ ?
- ????????printk(?KERN_ERR?"Client?deregistration?failed?(address=%x),?client?not?detached.\n",?client->addr); ?
- ????????return?err; ?
- ????} ?
- ?
- ????client->adapter?=?NULL; ?
- ?
- ????if(?g_tvp5158_obj?){ ?
- ????????kfree(g_tvp5158_obj); ?
- ????} ?
- ?
- ????return?0; ?
- }?
??? 到此為止,設備的注冊和注銷代碼已經全部完成,下面要做的就是提供讀寫I2C設備的方法。
?5.?? I2C設備的讀寫??????
??? 對I2C設備的讀寫,Linux系統提供了多種接口,可以在內核的 i2c.h 中找到,這里簡單介紹其中的兩種接口。
?? 【接口一】:
- extern?int?i2c_master_send(struct?i2c_client?*,const?char*?,int); ?
- ?
- extern?int?i2c_master_recv(struct?i2c_client?*,char*?,int); ?
??? 第一個參數是 i2c_client 對象指針,第二個參數是要傳輸的數據buffer指針,第三個參數為buffer的大小。
?? 【接口二】:
- extern?int?i2c_transfer(struct?i2c_adapter?*adap,?struct?i2c_msg?*msg,?int?num);?
??? 這個接口支持一次向I2C設備發送多個消息,每一個消息可以是讀也可以是寫,讀或者寫以及讀寫的目標地址(寄存器地址)均包含在msg消息參數里面。
??? 這些接口僅僅是最底層的讀寫方法,關于具體怎么與I2C設備交互,比如具體怎么讀芯片的某個特定寄存器的值,這需要看具體的芯片手冊,每個I2C芯片都會有具體的I2C寄存器讀寫時序圖。因此,為了在驅動中提供更好的訪問接口,還需要根據具體的時序要求對這些讀寫函數進行進一步封裝,這些內容將在后面的文章中講述。
6. ?模塊初始化及其他
? ? 下一步就是整個模塊的初始化代碼和逆初始化代碼,以及模塊聲明了。? ??
- static?int?__init?tvp5158_i2c_init(void)?
- {?
- ????g_tvp5158_obj?=?NULL;?
- ?????
- ????return?i2c_add_driver(&tvp5158_i2c_driver);?
- }?
- ?
- static?void?__exit?tvp5158_i2c_exit(void)?
- {?
- ????i2c_del_driver(&tvp5158_i2c_driver);?
- }?
- ?
- module_init(tvp5158_i2c_init);?
- module_exit(tvp5158_i2c_exit);?
- ?
- MODULE_DESCRIPTION("TVP5158?i2c?driver");?
- MODULE_AUTHOR("Lujun?@hust");?
- MODULE_LICENSE("GPL");?
? ? 在初始化的代碼里面,添加本模塊的 i2c driver 對象,在逆初始化代碼里面,刪除本模塊的 i2c driver 對象。
7.?? 總結
??? 到此為止,算是從應用的角度把編寫一個I2C的設備驅動代碼講完了,很多原理性的東西我都沒有具體分析(其實我也了解的不深),以后會慢慢更深入地學習和了解,文中有什么講述不正確的地方,歡迎留言或者來信lujun.hust@gmail.com交流。
??? 讀到最后,大家可能還有一個疑問,這個驅動寫完了怎么在用戶空間(應用層)去使用它呢?由于本文不想把代碼弄得太多太復雜,怕提高理解的難度,所以就沒有講,其實要想在用戶空間使用該I2C設備驅動,則還需要借助字符設備驅動來完成,即為這個I2C設備驅動封裝一層字符設備驅動,這樣,用戶空間就可以通過對字符設備驅動的訪問來訪問I2C設備,這個方法我會在后面的文章中講述。