Linux中IIC總線驅動分析


雖然I2C硬件體系結構比較簡單,但是I2C體系結構在Linux中的實現卻相當復雜.通過闡述Linux系統中I2C總線體系結構,在此基礎上完成嵌入式Linux系統中I2C總線驅動的開發.

1. 嵌入式Linux中I2C驅動程序分析

I2C(Inter2IntegratedCircuit)總線是一種由PHILIPS公司開發的兩線式串行總線,用於連接微控制器及其外圍設備.嵌入式系統中,微控制器通過I2C總線可隨時可對各個系統中的組件進行設置和查詢,以管理系統的配置或掌握組件的功能狀態來控制外圍設備.I2C總線因為協議成熟,引腳簡單,傳輸速率高,支持的芯片多,並且有利於實現電路的標准化和模塊化,得到了包括Linux在內的很多操作系統的支持,受到開發者的青睞.在Linux環境下使用I2C總線協議,需要理解Linux的I2C總線驅動的體系結構,在此基礎上來進行嵌入式驅動程序和應用程序的開發.

1.1 Linux的I2C驅動框架

Linux內核的I2C總線驅動程序框架如圖1所示:

Linux的I2C體系結構分為3個組成部分:

I2C核心:I2C核心提供了I2C總線驅動和設備驅動的注冊,注銷方法,I2C通信方法(即"al2gorithm")上層的,與具體適配器無關的代碼以及探測設備,檢測設備地址的上層代碼等.這部分是與平台無關的.與其對應的是Linux內核源代碼中的drivers目錄下的i2c2core.c.

I2C總線驅動:I2C總線驅動是對I2C硬件體系結構中適配器端的實現.I2C總線驅動主要包含了I2C適配器數據結構i2c_adapter,I2C適配器的algorithm數據結構i2c_algorithm和控制I2C適配器產生通信信號的函數,經由I2C總線驅動的代碼,我們可以控制I2C適配器以主控方式產生開始位,停止位,讀寫周期,以及以從設備方式被讀寫,產生ACK等.不同的CPU平台對應着不同的I2C總線驅動,比如下文要提到的S3C2410的總線驅動i2c2s3c2410.c,它位於Linux內核源代碼中的drivers目錄下busses文件夾.

I2C設備驅動:I2C設備驅動是對I2C硬件

體系結構中設備端的實現.設備一般掛接在受CPU控制的I2C適配器上,通過I2C適配器與CPU交換數據.I2C設備驅動主要包含了數據結構i2c_driver和i2c_client,我們需要根據具體設備實現其中的成員函數.在Linux內核源代碼中的drivers目錄下的i2c2dev.c文件,實現了I2C適配器設備文件的功能,應用程序通過"i2c2%d"文件名並使用文件操作接口open(),write(),read(),ioctl()和close()等來訪問這個設備.應用層可以借用這些接口訪問掛接在適配器上的I2C設備的存儲空間或寄存器並控制I2C設備的工作方式.

1.2 Linux的I2C驅動框架中的主要數據結構及其關系

Linux的I2C驅動框架中的主要數據結構包括:i2c_driver,i2c_client,i2c_adapter和i2c_algo2rithm.它們的定義在內核中的i2c.h頭文件中.i2c_adapter對應於物理上的一個適配器,這個適配器是基於不同的平台的,一個I2C適配器需要i2c_algorithm中提供的通信函數來控制適配器,因此i2c_adapter中包含其使用的i2c_algorithm的指針.i2c_algorithm中的關鍵函數master_xfer()以i2c_msg為單位產生I2C訪問需要的信號.不同的平台所對應的master_xfer()是不同的,開發人員需要根據所用平台的硬件特性實現自己的xxx_xfer()方法以填充i2c_algorithm的master_xfer指針.

i2c_driver對應一套驅動方法,不對應於任何的物理實體.i2c_client對應於真實的物理設備,每個I2C設備都需要一個i2c_client來描述.i2c_client依附於i2c_adpater,這與I2C硬件體系中適配器和設備的關系一致.i2c_driver提供了i2c2cli2ent與i2c2adapter產生聯系的函數.當attach_a2dapter()函數探測物理設備時,如果確定存在一個client,則把該client使用的i2c_client數據結構的adapter指針指向對應的i2c_adapter,driver指針指向該i2c_driver,並調用i2c_adapter的client_register()函數來注冊此設備.相反的過程發生在i2c_driver的detach_client()函數被調用的時候.

1.3 Linux的I2C體系結構中三個組成部分的作用

I2C核心提供了一組不依賴於硬件平台的接口函數,I2C總線驅動和設備驅動之間依賴於I2C核心作為紐帶.I2C核心提供了i2c_adapter的增加和刪除函數,i2c_driver的增加和刪除函數,i2c_client的依附和脫離函數以及i2c傳輸,發送和接收函數,i2c傳輸函數i2c_transfer()用於進行I2C適配器和I2C設備之間的一組消息交互i2c_mas2ter_send()函數和i2c_master_recv()函數內部會調用i2c_transfer()函數分別完成一條寫消息和一條讀消息.

I2C總線驅動包括I2C適配器驅動加載與卸載以及I2C總線通信方法.其中I2C適配器驅動加載(與卸載)要完成初始化(釋放)I2C適配器所使用的硬件資源,申請I/O地址,中斷號,通過i2c_add_adapter()添加i2c_adapter的數據結構(通過i2c_del_adapter()刪除i2c_adapter的數據結構)的工作.I2C總線通信方法主要對特定的I2C適配器實現i2c_algorithm的master_xfer()方法來實現i2c_msg的傳輸.不同的適配器對應的master_xfer()方法由其處理器的硬件特性決定.I2C設備驅動主要用於I2C設備驅動模塊加載與卸載以及提供I2C設備驅動文件操作接口.I2C設備驅動的模塊加載通用的方法遵循以下流程:首先通過register_chrdev()將I2C設備注冊為一個字符設備,然后利用I2C核心中的i2c_add_a2dapter()添加i2c_driver.調用i2c_add_adapter()過程中會引發i2c_driver結構體中的yyy_attach_adapter()的執行,它通過調用I2C核心的i2c_probe()實現物理設備的探測.i2c_probe()會引發yyy_detect()的調用.yyy_detect()中會初始化i2c_client,然后調用內核的i2c_attach_client()通知I2C核心此時系統中包含了一個新的I2C設備.之后會引發I2C設備驅動中yyy_init_client()來初始化設備.卸載過程執行相反的操作.

I2C設備驅動模塊加載與卸載的流程如圖2所示:

綜上所述,對於特定的嵌入式Linux操作系統,由於I2C核心是不依賴硬件平台的,所以開發的主要工作在於特定平台的總線驅動的開發以及特定設備的驅動開發.

2. 實例:基於S3C2410的I2C設備驅動開發與應用

2.1 S3C2410I2C總線驅動設計

S3C2410處理器內部集成了一個I2C控制器,通過控制寄存器IICCON,IICDS和IICADD可在I2C總線線產生開始位,停止位,數據和地址,而傳輸的狀態則通過IICSTAT寄存器獲取.S3C2410處理器內部集成的I2C控制器可支持主,從兩種模式,我們主要使用主模式.

一般來說,I2C總線驅動通常需要兩個模塊來描述,即一個由具體平台決定的適配器模塊和這個適配器相對應的I2C總線通信方法.根據2.1中對I2C設備驅動的描述,針對S3C2410的I2C總線驅動設計主要要實現的工作包括設計S3C2410適配器的模塊加載卸載函數i2c_adap_s3c_init(),i2c_adap_s3c_exit()以及S3C2410適配器的通信方法函數s3c24xx_i2c_xfer().

I2C適配器驅動作為一個單獨的模塊被加載進內核,這個過程只需要注冊一個device_driver結構體,device_driver結構體包含了具體適配器的probe()函數,remove()函數,resume()函數指針等信息,它需要被定義和賦值.當通過device_regis2ter()函數注冊device_driver結構體時,probe指針指向的s3c24xx2i2c_probe()函數將被調用,以初始化適配器硬件.然后通過I2C核心提供的i2c_add_adapter()函數添加這個適配器.下面進行詳細的說明:device_driver結構體的定義如下:

 1 static struct device_driver s3c2410_i2c_driver={
 2     .name=“s3c2410-i2c”,
 3 
 4     .bus=&platform_bus_type,
 5 
 6     .probe=s3c24xx_i2c_probe,
 7 
 8     .remove=s3c24xx_i2c_remove,
 9 
10     .resume=s3c24xx_i2c_resume,
11 };

其中s3c24xx_i2c_probe()用來使能硬件並申請I2C適配器使用的I/O地址,中斷號等,在這些工作都完成無誤后,通過I2C核心提供的i2c_add_a2dapter()函數添加這個適配器.與s3c24xx_i2c_probe()函數完成相反功能的函數是s3c24xx_i2c_remove()函數,它在適配器模塊卸載函數調用de2vice_unregister()函數時通過device_driver的re2move指針被調用.

在上述過程中用到了s3c24xx_i2c結構體進行適配器所有信息的封裝.它類似於私有信息結構體,下面代碼是一個由s3c2410總線驅動模塊定義的一個s3c24xx_i2c結構體全局實例:

 1 static struct s3c24xx_i2c s3c24xx_i2c={
 2 
 3     .lock=SPIN_LOCK_UNLOCKED,/*自旋鎖未鎖定*/
 4 
 5     .wait=__WAIT_QUEUE_HEAD_INITIALIZER
 6 
 7     (
 8 
 9         s3c24xx_i2c.wait
10 
11     ),/*等待隊列初始化*/
12 
13     .adap={
14 
15     .name=“s3c2410-i2c”,
16 
17     .owner=THIS_MODULE,
18 
19     .algo=&s3c24xx_i2c_algorithm,
20 
21     .retries=2,
22 
23     .class=I2C_CLASS_HWMON,
24 
25     },
26 
27 };

上述代碼指定了S3C2410I2C總線通信傳輸函數s3c24xx_i2c_xfer(),它依賴於s3c24xx_i2c_doxfer()函數和s3c24xx_i2c_message_start()函數,上述三個函數結合s3c24xx_i2c_irq()和i2s_s3c_irq_nextbyte()函數共同完成algorithm中的master_xfer功能.具體的,master_xfer指針指向的s3c24xx_i2c_xfer()函數調用s3c24xx_i2c_doxfer()函數傳輸I2C消息.s3c24xx_i2c_doxfer()首先將S3C2410的I2C適配器設置為I2C主設備,其后初始化s3c24xx_i2c結構體,使能I2C中斷並調用s3c24xx_i2c_message_start()函數啟動I2C消息傳輸,s3c24xx_i2c_message_start()函數寫S3C2410適配器對應的控制寄存器向I2C從設備傳遞開始位和從設備地址.具體通信過程可參照S3C2410數據手冊的I2C總線部分.此外,master_xfer()功能的完整實現需要借助I2C適配器上的中斷函s3c24xx_i2c_irq()以及i2s_s3c_irq_nextbyte()函數來步步推進.通過上述流程,可實現i2c_msg消息數組的傳輸.由上述討論可以看出,在進行I2C設備讀寫時是以i2c_msg為單位進行的,i2c_msg結構體在I2C總線體系中是一個十分重要的結構,下面給出其定義:

1 struct i2c_msg{
2     __u16addr;/*設備地址*/
3 
4     __u16flags;/*標志*/
5 
6     __u16len;/*消息長度*/
7 
8     __u83buf;/*消息數據*/
9 };

2.2 S3C2410I2C設備驅動設計

一個具體的I2C設備驅動需要實現兩個方面的接口.一個是對I2Ccore層的接口,用於掛載I2Cadapter層來實現對I2C總線及I2C設備具體的訪問方法,包括要實現attach_adapter,detach_a2dapter,detach_client,command等接口函數.另一個是用戶應用層的接口,提供用戶程序訪問I2C設備的接口,包括實現open, release,read,write以及ioctl等標准文件操作接口函數.

對I2Ccore層的接口函數的具體功能解釋如下:

attach_adapter:I2C驅動在調用I2C_add_driver()注冊時,對發現的每一個I2Cadapter(對應一條I2C總線)都要調用該函數,檢查該I2Ca2dapter是否符合I2Cdriver的特定條件,如果符合條件則連接此I2Cadapter,並通過I2Cadapter來實現對I2C總線及I2C設備的訪問。detach_a2dapter實現相反的功能.

detach_client:I2Cdriver在刪除一個I2C device時調用該函數,清除描述這個I2Cdevice的數據結構,這樣以后就不能訪問該設備了.

command:針對設備的特點,實現一系列的子功能,是用戶接口中的ioctl功能的底層實現.

在驅動中必須實現一個structi2c_driver的數據結構,並在驅動模塊初始化時向I2Ccore注冊一個I2C驅動,並完成對I2Cadapter的相關操作.其代碼為:

 1 static struct i2c_driver i2cdev_driver={
 2     .owner=THIS_MODULE,
 3 
 4     .name=“dev_driver”,
 5 
 6     .id=I2C_DRIVERID_I2CDEV,
 7 
 8     .flags=I2C_DF_NOTIFY,
 9 
10     .attach_adapter=i2cdev_attach_adapter,
11 
12     .detach_adapter=i2cdev_detach_adapter,
13 
14     .detach_client=i2cdev_detach_client,
15 
16     .command=i2cdev_command,
17 };

以上幾個接口函數使設備驅動程序實現了對I2C總線及I2Cadpater的掛接,因此就可以通過I2Ccore提供的對I2C總線讀寫訪問的通用接口來實現設備驅動程序對用戶應用層的接口函數.

為實現對用戶應用層的接口,在I2C設備驅動中需要實現一個structfile_operations的數據結構,並向內核注冊為一個字符類型的設備.

 1 static struct file_operations i2cdev_fops={
 2     .owner=THIS_MODULE,
 3 
 4     .read=i2cdev_read,
 5 
 6     .write=i2cdev_write,
 7 
 8     .ioctl=i2cdev_ioctl,
 9 
10     .open=i2cdev_open,
11 
12     .release=i2cdev_release,
13 };

數據結構i2cdev_fops中的i2cdev_open和i2cdev_release對應file_operations中的open和release,分別用來打開和關閉設備.i2cdev_ioctl對應file_operations中的ioctl,對用戶提供的一系列控制設備的具體命令,如:I2C_SLAVE(設置從設備地址),I2C_RETRIES(沒有收到設備ACK情況下的重試次數,缺省為1),I2C_TIMEOU(超時)以及I2C_RDWR.i2cdev_read對應file_opera2tions中的read,實現從字符設備到用戶空間讀數據.i2cdev_write對應file_operations中的write,實現從用戶空間到字符設備寫數據.實際上由於i2c_dev.c已經實現了I2C設備的文件操作接口,所以開發者只需要實現i2c_driver結構即可.

下面以S3C2410I2C設備的讀過程中的函數調用過程進行圖示(如圖3)來進一步表明在實際的嵌入式Linux系統中I2C驅動體系三個組成部分即I2C核心,I2C總線驅動以及I2C設備驅動之間的關系(寫過程類似).

2.3 S3C2410I2C設備驅動應用

本部分主要涉及ARM端I2C總線驅動及應用程序的開發,來實現ARM與FPGA的數據交換.由於AT24C08的操作時序與項目需求近似,故FPGA從端暫時通過利用AT24C08(8K串行E2PROM)模擬,而FPGA端在后期進行開發.

通過以上對研究目的的描述,這部分的主要工作是利用3.2提到的I2C設備驅動中提供的接口read,write及ioctl來實現對I2C設備的讀寫,從圖3可以看出,i2cdev_read(),i2cdev_write()讀寫函數分別調用I2C核心的i2c_master_recv()和i2c_master_send()函數來構造1條I2C消息並引發適配器algorithm通信函數的調用,完成消息的傳輸,他們的操作時序如圖4所示.

通過上面的時序不難看出這兩個讀寫函數在一個讀寫周期內只實現了1條i2c_msg的傳輸,這樣是無法實現對復雜的I2C從設備的讀寄存器過程的(詳見下文中的AT24C08).目前大多數的I2C設備都要求在一個讀寫周期內至少實現2條以上的i2c_msg的傳輸,即I2C總線協議中的Repstart模式,其操作時序如圖5所示.

由於i2cdev_read()和i2cdev_write()適用於非RepStart模式的情況.對於2條以上消息組成的讀寫,在用戶空間需要組織i2c_msg消息數組並調用I2C_RDWRIOCTL命令來實現.應用程序需要通過i2c_rdwr_ioctl_data結構體來給內核傳遞I2C消息,其定義如下:

1 struct i2c_rdwr_ioctl_data{
2     struct i2c_msg__user3msgs;//pointerstoi2c_msgs
3 
4     __u32 nmsgs;//numberofi2c_msgs
5 };

通過在用戶空間賦值由i2c_rdwr_ioctl_data結構體成員msgs指針指向的I2C消息數組來實現從設備的尋址,讀寫控制以及傳輸的數據.然后調用i2cdev_ioctl()函數的I2C_RDWR命令來完成i2c_msg從用戶空間向內核空間的傳送.由於傳送的是i2c_msg消息數組,所以可以實現多個I2C消息的傳輸,從而實現了Repstart模式.下面就AT24C08的操作時序給出使用S3C2410I2C總線對AT24C08從設備的讀寫過程.

AT24C08是一個1024*8位串行CMOSEE2PROM,內部含有1024個8位字節,它通過I2C總線接口進行操作.通過使用它來模擬FPGA作為I2C從設備獲取I2C總線上的數據並存儲到內部存儲器中.AT24C08的器件地址為0x50,它的寫操作共有以下兩種方式:

字節寫:在此模式下,主器件發送起始命令和從器件地址信息以及讀寫bit給從器件,從器件產生ACK應答后,主器件發送字節地址,當再次收到ACK應答后,主器件發送數據到被尋址的存儲單元.從器件再次應答,並在主器件產生停止信號后開始內部數據的擦寫.

頁寫:此模式下,AT24C08可以一次寫入16個字節的數據.頁寫操作的啟動和字節寫一樣,不同在於傳送了一個字節數據后並不產生停止信號.主器件被允許發送15個額外的字節.每發送一個字節數據后AT24C08產生一個應答位並將字節地址位加1.

At24C08的讀操作共有以下三種方式:

立即地址讀:AT24C08的地址計數器內容為最后操作字節的地址加1.即如果上次讀寫操作地址為N,則立即讀的地址從N+1開始.如果N=1023,則計數器就翻轉到0其繼續輸出數據.AT24C08接收到從器件地址信號后,發送ACK應答,然后發送一個8位字節數據.主器件不需發送ACK,但要產生一個停止信號.

選擇性讀:此模式允許主器件對寄存器的人一字節進行讀操作.主器件首先通過發送起始信號,從器件地址和它想讀取的字節數據的地址執行一個偽寫操作.當AT24C08產生ACK后,主器件重新發送起始信號和從器件地址,此時R/W位置1,AT24C08響應並發送ACK,然后輸出所要求的一個位字節數據.主器件不發送但是要產生一8ACK個停止信號.

連續讀:此模式由立即讀或選擇性讀操作啟動.在AT24C08發送完一個8字節數據后,主器件產生一個應答信號告知AT24C08主器件需要更多的數據.對主機產生的每個ACK,AT24C08都發送一個8位數據字節.當主器件不發送ACK而發送停止位時操作結束.

根據AT24C08I2C的操作時序以及上文對設備讀寫時序的描述,下面給出對AT24C08寫操作時序圖示及實現的部分代碼:

對字節寫和頁寫兩種寫操作,由於只需要一條I2C消息傳輸,所以既可以通過i2cdev_write()函數實現,也可以使用i2cdev_ioctl()函數的I2C_RDWR命令實現.第一種方式主要進行的操作如下:

 1 fd=open(“/dev/i2c/0”,O_RDWR);/*打開i2c設備*/
 2 
 3 ioctl(fd,I2C_SLAVE,0x50);/*設置eeprom地址*/
 4 
 5 ioctl(fd,I2C_TIMEOUT,1);/*設置超時*/
 6 
 7 ioctl(fd,I2C_RETRIES,1);/*設置重試次數*/
 8 
 9 buf[0]=byte_address;/*byte_address賦給buf[0]*/
10 
11 for(i=1;i<=MSG_LEN;i++)
12 
13     buf[i]=data_i;/*將要寫的數據賦給buf[i]*/
14 
15 for(j=0;j<=MSG_LEN+1;j++)
16 
17     write(fd,buf[j],1);/*調用write函數發送數據*/
18 
19 close(fd);/*關閉i2c設備*/

字節寫的情況下MSG_LEN=1,頁寫情況下MSG_LEN=16.第二種方式主要進行的操作如下:

 1 struct i2c_rdwr_ioctl_data msgtable;/*定義結構體*/
 2 
 3 fd=open(“/dev/i2c/0”,O_RDWR);/*打開i2c設備*/
 4 
 5 msgtable.nmsgs=1;/*消息數量*/
 6 
 7 msgtable.msgs[0].addr=slave_address;
 8 
 9 msgtable.msgs[0].buf[0]=byte_address;
10 
11 msgtable.msgs[0].len=DAT_LEN+1;/*byteaddress+data*/
12 
13 msgtable.msgs[0].flags=0;/*write mode*/
14 
15 ioctl(fd,I2C_TIMEOUT,1);/*設置超時*/
16 
17 ioctl(fd,I2C_RETRIES,1);/*設置重試次數*/
18 
19 ioctl(fd,I2C_RDWR,&msgtable);/*使用I2C_RDWR命令傳輸I2C消息*/
20 
21 close(fd);/*關閉i2c設備*/

對讀操作的三種模式,對於立即讀模式,從描述可以看出只需要1條I2C消息傳輸,故可使用i2cdev_read()就可以簡單實現,但是這種操作由於很少使用應用價值不大.對於選擇性讀操作由於要先進行字節尋址,所以讀操作時序中需要首先實現一個寫字節地址的過程,這需要兩條I2C消息實現,即Repstart模式.所以這里必須使用I2C_RDWR命令.下面給出選擇性讀操作的時序及實現的部分代碼:

 1 struct i2c_rdwr_ioctl_data msgtable;/*定義結構體*/
 2 
 3 fd=open(“/dev/i2c/0”,O_RDWR);/*打開i2c設備*/
 4 
 5 msgtable.nmsgs=2;/*消息數量*/
 6 
 7 msgtable.msgs[0].addr=slave_address;
 8 
 9 msgtable.msgs[0].buf[0]=byte_address;
10 
11 msgtable.msgs[0].len=1;
12 
13 msgtable.msgs[0].flags=0;/*write mode*/
14 
15 msgtable.msgs[1].addr=slave_address;
16 
17 msgtable.msgs[1].buf=sbuf;
18 
19 msgtable.msgs[1].len=SBUF_LEN;
20 
21 msgtable.msgs[1].flags=1;/*read mode*/
22 
23 ioctl(fd,I2C_TIMEOUT,1);/*設置超時*/
24 
25 ioctl(fd,I2C_RETRIES,1);/*設置重試次數*/
26 
27 ioctl(fd,I2C_RDWR,&msgtable);/*使用I2C_RDWR命令傳輸I2C消息*/
28 
29 close(fd);/*關閉i2c設備*/

對於基於選擇性讀的連續讀的情況,只需要將第二條I2C消息的buf數組長度設為需要讀出字節的長度即可.通過上述操作,便可以通過S3C2410的I2C總線對EEPROM進行讀寫.下一步只需利用verilog語言對FPGA進行編程來實現I2C從端的功能,模仿上述AT24C08的操作時序並將寄存器內的指令合理的分配給其它設備.

3. 結束語

嵌入式Linux中I2C2core框架提供了統一的,不需要修改的接口.而驅動程序開發者只需要實現特定的I2C總線適配器驅動和I2C設備驅動便可以進行用戶空間的應用程序設計.本文闡述了Linux系統中I2C總線體系結構,並基於項目要求通過具體例子給出了基於S3C2410處理器利用I2C總線與I2C設備通信的用戶空間的實現.

本文轉自:道客巴巴《嵌入式Linux系統中I2C總線驅動的研究與應用》篇


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM