0 引言
由于I2C總線的通用性,Linux作為一款優(yōu)秀的嵌入式操作系統(tǒng),也必須要對其要有很好的支持。在Linux內(nèi)核源碼中對I2C總線的驅(qū)動是基于總線設(shè)備驅(qū)動模型的,其驅(qū)動程序用到了特殊的幾個(gè)數(shù)據(jù)結(jié)構(gòu),對I2C總線協(xié)議進(jìn)行了更抽象更通用的定義,極大的增加了設(shè)備驅(qū)動的可移植性。要編寫出自己的I2C 設(shè)備驅(qū)動程序,必須對這種內(nèi)核I2C總線驅(qū)動的架構(gòu)有深刻的理解。
1 I2C總線的硬件構(gòu)成
I2C 總線協(xié)議只有兩條總線線路,一條是串行數(shù)據(jù)線(SDA),一條是串行時(shí)鐘線(SCL)。SDA 負(fù)責(zé)數(shù)據(jù)的傳輸,SCL 負(fù)責(zé)數(shù)據(jù)傳輸?shù)臅r(shí)鐘同步。I2C 設(shè)備通過這兩條總線連接到處理器的I2C總線控制器上,不同設(shè)備之間通過7 位地址來區(qū)別,而且數(shù)據(jù)的傳輸是雙向的,方向的確定由1位二進(jìn)制數(shù)確定,地址位加方向位是操作I2C 設(shè)備的惟一標(biāo)示,I2C 設(shè)備與CPU 的連接如圖1所示。
I2C 總線上有3 種類型的信號,分別是:開始信號,結(jié)束信號和應(yīng)答信號。這些信號都是由SDA和SCL上的電平變化來表示的。
開始信號(S):當(dāng)SCL為高電平時(shí),SDA由高電平向低電平跳變,表示開始傳輸數(shù)據(jù)。
結(jié)束信號(P):當(dāng)SCL為高電平時(shí),SDAY由低電平向高電平跳變,表示結(jié)束傳輸數(shù)據(jù)。
相應(yīng)信號(ACK):從機(jī)接收到8位數(shù)據(jù)后,在第9個(gè)時(shí)鐘周期,拉低SDA電平,表示已經(jīng)接收到數(shù)據(jù)。
當(dāng)總線空閑時(shí),SDA 和SCL 都處于高電平,主機(jī)檢測到總線空閑就可以向從機(jī)發(fā)送數(shù)據(jù)。主機(jī)首先發(fā)送開始信號S,接著發(fā)出8位數(shù)據(jù)(包括前7位的從機(jī)地址和1 為的方向位),然后等待從機(jī)發(fā)回確認(rèn)信號ACK.
當(dāng)?shù)?位為0時(shí),表示向從機(jī)傳輸數(shù)據(jù),主機(jī)收到確認(rèn)信號后就可以連續(xù)的向從機(jī)寫入8 位數(shù)據(jù);當(dāng)?shù)? 位為1時(shí),表示向從讀取數(shù)據(jù),這時(shí)主機(jī)就可以接收來自從機(jī)的一系列數(shù)據(jù)。最后當(dāng)總個(gè)數(shù)據(jù)傳輸過程完成后,由主機(jī)發(fā)送結(jié)束信號P,表示本次的數(shù)據(jù)傳輸完成。
2 Linux 的I2C設(shè)備驅(qū)動程序的層次結(jié)構(gòu)
因?yàn)镮2C設(shè)備的種類繁多,如果為每一款I(lǐng)2C設(shè)備都編寫一個(gè)驅(qū)動程序,顯然不太現(xiàn)實(shí)也不太可能做到。所以,Linux中是對I2C 設(shè)備驅(qū)動采取了層次化處理,分為總線層和設(shè)備層。將I2C設(shè)備驅(qū)動的一些共同屬性抽象起來歸結(jié)起來作為總線層,而將具體I2C設(shè)備特殊操作作為設(shè)備層。在Linux中I2C設(shè)備驅(qū)動中用到的數(shù)據(jù)結(jié)構(gòu)[4,7-8]的關(guān)系如圖2 所示。關(guān)于這部分代碼位于Linux內(nèi)核源碼樹的/driver/i2c中。
理解這層次結(jié)構(gòu)重點(diǎn)是要理解4個(gè)數(shù)據(jù)結(jié)構(gòu),分別是屬于設(shè)備層的i2c_driver 與i2c_client,屬于總線層的i2c_adapter與i2c_algorithm.下面分別對這四個(gè)數(shù)據(jù)結(jié)構(gòu)做簡要的說明。
struct i2c_driver:具體的每一個(gè)I2C設(shè)備都應(yīng)該對應(yīng)著的一個(gè)驅(qū)動,這個(gè)結(jié)構(gòu)體里面定義了Linux設(shè)備模型中用于I2C 總線管理的一系列函數(shù)指針和I2C 設(shè)備的信息。其中最重要的兩個(gè)成員是適配器檢測函數(shù)指針at-tach_adapter,和設(shè)備ID表id_table.
struct i2c_client:一個(gè)連接在SDA 和SCL 總線上的具體設(shè)備是由i2c_client結(jié)構(gòu)體描述的,定義了兩個(gè)成員變量表示這個(gè)具體設(shè)備所對應(yīng)的適配器和驅(qū)動。
struct i2c_adapter:此結(jié)構(gòu)體表示CPU 里面具體的I2C控制器,本質(zhì)上也是對應(yīng)著一個(gè)物理設(shè)備,其中最要的成員變量是指向適配器驅(qū)動的程序的algo 結(jié)構(gòu)體指針。
struct i2c_algorithm:里面定義了具體適配器驅(qū)動程序的函數(shù)指針。特別是master_xfer函數(shù)指針,這個(gè)函數(shù)實(shí)現(xiàn)了適配器最底層的操作方法,也是I2C設(shè)備驅(qū)動中總線層里面要編寫的重要函數(shù)。
i2c_dev 里面定義了讀寫I2C 設(shè)備應(yīng)用層的讀寫接口,但由于其缺少通用性,一般很少用到所以并不做詳細(xì)的介紹。
i2c_core在驅(qū)動框架中起到了承上啟下的作用,里面定義了許多重要的函數(shù)。例如:adapter注冊/注銷函數(shù),增加/刪除設(shè)備驅(qū)動函數(shù),增加/刪除I2C設(shè)備的函數(shù),I2C傳輸,發(fā)送和接收函數(shù)。這些函數(shù)都是在編寫I2C設(shè)備驅(qū)動程序中必須要用到的接口函數(shù),正是由于這些通用的接口函數(shù)才使得代碼具有很強(qiáng)的可移植性和重用性。
3 編寫I2C設(shè)備驅(qū)動的思路
在了解Linux中I2C設(shè)備驅(qū)動的基本框架后,要編寫自己的設(shè)備驅(qū)動首先要弄清楚的一個(gè)問題是到底內(nèi)核已經(jīng)實(shí)現(xiàn)了那部分,需要實(shí)現(xiàn)的又是那部分。因?yàn)镮2C設(shè)備驅(qū)動是基于總線設(shè)備驅(qū)動模型的,一般而言在移植Linux操作系統(tǒng)中,Linux內(nèi)核已經(jīng)對總線部分已經(jīng)有了很好的實(shí)現(xiàn),所以總線部分的驅(qū)動一般可以不必關(guān)心。
在此需要實(shí)現(xiàn)的是設(shè)備層的i2c_driver與i2c_client結(jié)構(gòu)體,并利用I2C 子系統(tǒng)提供的接口函數(shù)掛接到I2C 總線上。
每一個(gè)I2C設(shè)備驅(qū)動,必須首先創(chuàng)造一個(gè)i2c_driver結(jié)構(gòu)體對象,該結(jié)構(gòu)體包含了I2C設(shè)備探測和注銷的一些基本方法和信息。其中包括設(shè)備驅(qū)動的名字,適配器的掛接/取消函數(shù)指針等。一個(gè)例子如下所示,name字段標(biāo)識本驅(qū)動的名稱(不要超過31 個(gè)字符),at-tach_adapter和detach_client字段為函數(shù)指針,這兩個(gè)函數(shù)在I2C設(shè)備注冊的時(shí)候會自動調(diào)用,需要自己實(shí)現(xiàn)這兩個(gè)函數(shù)。
上面定義的i2c_driver對象,抽象為一個(gè)I2C的驅(qū)動模型,提供對I2C設(shè)備的探測和注銷方法,接下來就是要定義i2c_client 結(jié)構(gòu)體,其代表著一個(gè)具體的I2C 設(shè)備,該結(jié)構(gòu)體有一個(gè)data指針,可以指向任何私有的設(shè)備數(shù)據(jù),在復(fù)雜點(diǎn)的驅(qū)動中可能會用到。
每一個(gè)I2C設(shè)備芯片,都通過硬件連接設(shè)定好了該設(shè)備的I2C設(shè)備地址。因此,I2C設(shè)備的探測一般是靠設(shè)備地址來完成的。那么,首先要在驅(qū)動代碼中聲明你要探測的I2C設(shè)備地址列表以及一個(gè)宏,示例如下:
有了i2c_client結(jié)構(gòu)體代表了具體的設(shè)備和設(shè)備ID后就可以實(shí)現(xiàn)attach_adapter 和detach_client 函數(shù)。這兩個(gè)函數(shù)是系統(tǒng)自動調(diào)用的,它的實(shí)現(xiàn)是有一定的框架的,可以在linux內(nèi)核源碼的驅(qū)動例子中找到,由于代碼過長這里不做具體的分析。針對不同的設(shè)備函數(shù)的實(shí)現(xiàn)會略有不同,一般attach_adapte需要完成的工作是對i2c_client結(jié)構(gòu)體成員賦值和調(diào)用接口函i2c_attach_cli-ent把設(shè)備掛接到適配其中。而detach_client 函數(shù)則是完成相反的工作。
最后的一步是編寫模塊的初始化與退出函數(shù)把驅(qū)動加進(jìn)I2C驅(qū)動子系統(tǒng)中,示例可以是:
至此,I2C設(shè)備的驅(qū)動已經(jīng)完成了,但是到了這一步本驅(qū)動并沒有實(shí)際的用處,它僅僅提供的是一個(gè)設(shè)備驅(qū)動程序的管理框架,所以必須還要進(jìn)行兩方面的補(bǔ)充。
第一方面是,利用I2C總線讀寫外部芯片的控制/狀態(tài)寄存器;第二方面是,向應(yīng)用層提供I2C設(shè)備的讀寫接口,令應(yīng)用程序可以對設(shè)備節(jié)點(diǎn)的讀寫實(shí)現(xiàn)對I2C具體物理設(shè)備的讀寫。為了實(shí)現(xiàn)I2C 設(shè)備寄存器的讀寫操作,必須要用到Linux的I2C子系統(tǒng)提供的讀寫接口函數(shù):
利用這兩個(gè)函數(shù)根據(jù)芯片的讀寫時(shí)序進(jìn)行封裝,就可以讀寫芯片內(nèi)部的寄存器,以寫芯片寄存器為例,必須寫往總線上寫寄存器的地址,然后寫入要往寄存器里寫入的數(shù)據(jù),示例代碼如下所示。讀寄存器的時(shí)序則是則是先寫入要讀寄存器的地址,然后接受總線上的數(shù)據(jù),區(qū)別不大,不做示例。
要想向應(yīng)用層提供讀寫接口,則必須再對I2C設(shè)備驅(qū)動進(jìn)行一次簡單字符設(shè)備驅(qū)動的封裝,將I2C設(shè)備作為一個(gè)簡單字符設(shè)備,依次實(shí)現(xiàn)字符設(shè)備文件操作函數(shù)結(jié)構(gòu)體file_operation 里面的函數(shù)指針?biāo)鶎?yīng)的接口函數(shù),這里只給出了大體的框架,具體的實(shí)現(xiàn)對于不同的芯片有很大的不同。
定義一個(gè)字符設(shè)備結(jié)構(gòu)體cdev,將I2C 設(shè)備當(dāng)做一個(gè)普通的字符設(shè)備處理。
定義一個(gè)文件操作函數(shù)結(jié)構(gòu)體,填寫里面函數(shù)指針,指出設(shè)備操作所對應(yīng)的具體函數(shù),一般的例子是:
接著就是編寫file_operations所對應(yīng)的具體函數(shù)。
最后一步是在模塊的初始化和退出函數(shù)中增加對簡單字符設(shè)備的注冊和注銷操作,包括設(shè)備號申請與注銷,設(shè)備注冊與注銷兩方面。
至此,將編譯好的模塊加載進(jìn)內(nèi)核后就可以在用戶空間利用文件系統(tǒng)的API對設(shè)備文件進(jìn)行各種操作。
4 結(jié)語
I2C總線在電子系統(tǒng)設(shè)計(jì)中是十分普遍的一種接口技術(shù),而Linux又是十分流行的嵌入式操作系統(tǒng)。編寫嵌入式Linux下I2C總線設(shè)備的驅(qū)動驅(qū)動程序是嵌入式開發(fā)中十分重要的一項(xiàng)技術(shù),不容忽視。本文首先講述了Linux系統(tǒng)I2C設(shè)備驅(qū)動程序的總體框架,然后給出了編寫I2C設(shè)備驅(qū)動的總體思路與框架,希望給讀者理清思路,加深對編寫I2C設(shè)備驅(qū)動的理解。總而言之,利用I2C總線通信可以達(dá)到很高的速率,而且總線上可以掛接多個(gè)節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)都由惟一的地址確定。
評論