@[TOC]
12 I2C編程應(yīng)用開發(fā)
? I2C(Inter-Integrated Circuit BUS)是I2C BUS簡稱,中文為集成電路總線,是目前應(yīng)用最廣泛的總線之一。和IMX6ULL有些相關(guān)的是,剛好該總線是NXP前身的PHILIPS設(shè)計。
12.1 I2C協(xié)議
12.1.1 概述
? I2C是一種串行通信總線,使用多主從架構(gòu),最初設(shè)計目的為了讓主板、嵌入式系統(tǒng)或手機用來連接低速周邊設(shè)備。多用于小數(shù)據(jù)量的場合,有傳輸距離短,任意時刻只能有一個主機等特性。嚴格意義上講,I2C應(yīng)該是軟硬件結(jié)合體,所以我們將分物理層和協(xié)議層來介紹該總線。
? I2C總線結(jié)構(gòu)如下圖:
? 傳輸數(shù)據(jù)時,我們需要發(fā)數(shù)據(jù),從主設(shè)備發(fā)送到從設(shè)備上去;也需要把數(shù)據(jù)從從設(shè)備傳送到主設(shè)備上去,數(shù)據(jù)涉及到雙向傳輸。
? 對于I2C通信的過程,下面使用一個形象的生活例子進行類比。
? 體育老師:可以把球發(fā)給學(xué)生,也可以把球從學(xué)生中接過來。
① 發(fā)球:
- a. 老師說:注意了(start);
- b. 老師對A學(xué)生說,我要球發(fā)給你(A就是地址);
- c. 老師就把球發(fā)出去了(傳輸);
- d. A收到球之后,應(yīng)該告訴老師一聲(回應(yīng));
- e. 老師說下課(停止)。
② 接球:
-
a. 老師說注意了(start);
-
b. 老師說:B把球發(fā)給我(B是地址);
-
c. B就把球發(fā)給老師(傳輸);
-
d. 老師收到球之后,給B說一聲,表示收到球了(回應(yīng));
- e. 老師說下課(停止)。
我們就使用這個簡單的例子,來解釋一下I2C的傳輸協(xié)議:
① 老師說注意了,表示開始信號(start)
② 老師告訴某個學(xué)生,表示發(fā)送地址(address)
③ 老師發(fā)球/接球,表示數(shù)據(jù)的傳輸
④ 老師/學(xué)生收到球,回應(yīng)表示:回應(yīng)信號(ACK)
⑤ 老師說下課,表示I2C傳輸接受(P)
12.2.2 物理層
1) 特性1:半雙工(非全雙工)
? I2C總線中只使用兩條線路:SDA、SCL。
① SDA(串行數(shù)據(jù)線):
? 主芯片通過一根SDA線既可以把數(shù)據(jù)發(fā)給從設(shè)備,也可以從SDA上讀取數(shù)據(jù)。在I2C設(shè)備內(nèi)部有兩個引腳(發(fā)送引腳/接受引腳),它們都連接到外部的SDA線上,具體可以參考下圖device端里面的I2Cn_SDA(output/input)。
② SCL(串行時鐘線):
? I2C主設(shè)備發(fā)出時鐘,從設(shè)備接收時鐘。
? SDA和SCL引腳的內(nèi)部電路結(jié)構(gòu)一致,引腳的輸出驅(qū)動與輸入緩沖連在一起。其中輸出為漏極開路的場效應(yīng)管、輸入緩沖為一只高輸入阻抗的同相器。這樣結(jié)構(gòu)有如下特性:
a. 由于 SDA、SCL 為漏極開路結(jié)構(gòu),借助于外部的上拉電阻實現(xiàn)了信號的“線與”邏輯;
b. 引腳在輸出信號的同時還作用輸入信號供內(nèi)部進行檢測,當輸出與輸入不一致時,就表示有問題發(fā)生了。這為 “時鐘同步”和“總線仲裁”提供硬件基礎(chǔ)。
? SDA和CLK連接線上連有兩個上拉電阻,當總線空閑時,兩根線均為高電平。連到總線上的任一器件輸出的低電平,都將使總線的信號變低。
? 物理層連接如下圖所示:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-kyu89PWq-1639020220827)(http://photos.100ask.net/NewHomeSite/IIC_Image003.png)]
2) 特性2:地址和角色可配置
? 每個連接到總線的器件都可以通過唯一的地址和其它器件通信,主機/從機角色和地址可配置,主機可以作為主機發(fā)送器和主機接收器。
3) 特性3:多主機
? I2C是真正的多主機總線,I2C設(shè)備可以在通訊過程轉(zhuǎn)變成主機。如果兩個或更多的主機同時請求總線,可以通過沖突檢測和仲裁防止總線數(shù)據(jù)被破壞。
4) 特性4:傳輸速率
? 傳輸速率在標準模式下可以達到100kb/s,快速模式下可以達到400kb/s。
5) 特性5:負載和距離
? 節(jié)點的最大數(shù)量受限于地址空間以及總線電容決定,另外總電容也限制了實際通信距離只有幾米。
12.2.3 協(xié)議層
1) 數(shù)據(jù)有效性
? I2C協(xié)議的數(shù)據(jù)有效性是靠時鐘來保證的,在時鐘的高電平周期內(nèi),SDA線上的數(shù)據(jù)必須保持穩(wěn)定。數(shù)據(jù)線僅可以在時鐘SCL為低電平時改變。
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-WQhqSPjW-1639020220828)(http://photos.100ask.net/NewHomeSite/IIC_Image004.png)]
2) 起始和結(jié)束條件
起始條件:當SCL為高電平的時候,SDA線上由高到低的跳變被定義為起始條件。
結(jié)束條件:當SCL為高電平的時候,SDA線上由低到高的跳變被定義為停止條件。
? 要注意起始和終止信號都是由主機發(fā)出的,連接到I2C總線上的器件,若具有I2C總線的硬件接口,則很容易檢測到起始和終止信號。
? 總線在起始條件之后,視為忙狀態(tài),在停止條件之后被視為空閑狀態(tài)。
3) 應(yīng)答
? 每當主機向從機發(fā)送完一個字節(jié)的數(shù)據(jù),主機總是需要等待從機給出一個應(yīng)答信號,以確認從機是否成功接收到了數(shù)據(jù),從機應(yīng)答主機所需要的時鐘仍是主機提供的,應(yīng)答出現(xiàn)在每一次主機完成8個數(shù)據(jù)位傳輸后緊跟著的時鐘周期,低電平0表示應(yīng)答,1表示非應(yīng)答。
4) 數(shù)據(jù)幀格式
? SDA線上每個字節(jié)必須是8位長,在每個傳輸(transfer)中所傳輸字節(jié)數(shù)沒有限制,每個字節(jié)后面必須跟一個ACK。8位數(shù)據(jù)中,先傳輸最高有效位(MSB)傳輸。
12.2 在linux系統(tǒng)下操作I2C總線的外設(shè)
12.2.1 概述
? 下圖是在linux系統(tǒng)環(huán)境里操作i2c總線上的外設(shè)流程框圖。我們按照從下向上的順序研究一下該流程中各個角色的功能。
? 在硬件層中,I2C硬件總線只有兩條線路,上面可以掛載多個I2C-device,這些I2C-device有的在I2C總線里充當主機的角色,一般情況該主機為板子上的主cpu中的I2C控制器,比如我們用的100ask_imx6UL板子,這個I2C主機就是imx6中的I2C控制器模塊;其他的I2C-device在I2C總線里充當從機的角色,通常這些從機是板子上完成特定功能的傳感器外設(shè),只不過該外設(shè)與主控cpu的通信方式是只需要兩條線路的I2C總線,比如在我們的100ask_imx6UL板子中就有eeprom和AP3216兩個外設(shè),它們在I2C總線中充當?shù)亩际荌2C從機的角色,它們和主控芯片imx6中的I2C控制器1都是以并聯(lián)的方式掛在這個I2C總線上。
? 在內(nèi)核中,驅(qū)動程序?qū)ο乱瓿蒊2C總線上的I2C通信協(xié)議,收集硬件傳感器的I2C數(shù)據(jù)并封裝成標準的linux操作接口供用戶空間的應(yīng)用程序操作。對上要實現(xiàn)可以通過linux程序把數(shù)據(jù)流組織成I2C協(xié)議下發(fā)到硬件層的相應(yīng)的外設(shè)傳感器中。
? 在用戶空間的應(yīng)用程序中,應(yīng)用工程師完全可以不必理會I2C協(xié)議的詳細規(guī)定。只需要按照驅(qū)動層提供給我們的操作I2C外設(shè)的操作接口函數(shù)就可以像操作linux中其他普通設(shè)備文件那樣輕松的操作I2C外設(shè)了。
12.2.2 簡述I2C的linux驅(qū)動
? I2C在linux內(nèi)核層的驅(qū)動框架主要由三部分組成:
1) I2C核心層:
? I2C核心提供了I2C總線驅(qū)動和設(shè)備驅(qū)動的注冊、注銷方法,I2C通信方法(algorithm)的上層部分,并且還提供了一系列與具體硬件平臺無關(guān)的接口函數(shù)以及探測設(shè)備,檢測設(shè)備地址的上層代碼等。它位于內(nèi)核源碼目錄下的drivers/i2c/i2c-core.c文件中,是I2C總線驅(qū)動和設(shè)備驅(qū)動之間依賴于I2C核心作為紐帶。
? I2C核心中的主要函數(shù)包括:
? 增加/刪除i2c_adapter
int i2c_add_adapter(struct i2c_adapter *adap);
int i2c_del_adapter(struct i2c_adapter *adap);
? 增加/刪除i2c_driver
int i2c_register_driver(struct module *owner, struct i2c_driver *driver);
int i2c_del_driver(struct i2c_driver *driver);
inline int i2c_add_driver(struct i2c_driver *driver);
? i2c_client依附/脫離
int i2c_attach_client(struct i2c_client *client);
int i2c_detach_client(struct i2c_client *client);
? i2c傳輸、發(fā)送和接收
int i2c_transfer(struct i2c_adapter * adap, struct i2c_msg *msgs, int num);
? 用于進行I2C適配器和I2C設(shè)備之間的一組消息交互。其本身不具備驅(qū)動適配器物理硬件完成消息交互的能力,它只是尋找到i2c_adapter對應(yīng)的i2c_algorithm,并使用i2c_algorithm的master_xfer()函數(shù)真正驅(qū)動硬件流程。
int i2c_master_send(struct i2c_client *client,const char *buf ,int count);
int i2c_master_recv(struct i2c_client *client, char *buf ,int count);
? i2c_master_send()和i2c_master_recv()函數(shù)內(nèi)部會調(diào)用i2c_transfer()函數(shù)分別完成一條寫消息和一條讀消息。
a) I2C控制命令分派
? 下面函數(shù)有助于將發(fā)給I2C適配器設(shè)備文件ioctl的命令分派給對應(yīng)適配器的algorithm的algo_control()函數(shù)或i2c_driver的command()函數(shù):
int i2c_control(struct i2c_client *client, unsigned int cmd, unsigned long arg);
void i2c_clients_command(struct i2c_adapter *adap, unsigned int cmd, void *arg);
2) I2C總線驅(qū)動層:
? I2C總線驅(qū)動是對I2C硬件體系結(jié)構(gòu)中適配器端的實現(xiàn),適配器可由CPU控制,甚至可以直接集成在CPU內(nèi)部。
? 它主要完成的功能有:
a) 初始化I2C適配器所使用的硬件資源,申請I/O地址、中斷號等。
b) 通過i2c_add_adapter()添加i2c_adapter的數(shù)據(jù)結(jié)構(gòu),當然這個i2c_adapter數(shù)據(jù)結(jié)構(gòu)的成員已經(jīng)被xxx適配器的相應(yīng)函數(shù)指針所初始化。
c) 釋放I2C適配器所使用的硬件資源,釋放I/O地址、中斷號等。
d) 通過i2c_del_adapter()刪除i2c_adapter的數(shù)據(jù)結(jié)構(gòu)。
3) I2C總線驅(qū)動層:
? I2C設(shè)備驅(qū)動(也稱為客戶驅(qū)動)是對I2C硬件體系結(jié)構(gòu)中設(shè)備端的實現(xiàn),設(shè)備一般掛接在受CPU控制的I2C適配器上,通過I2C適配器與CPU交換數(shù)據(jù)。I2C設(shè)備驅(qū)動模塊加載函數(shù)通用的方法是在I2C設(shè)備驅(qū)動模塊加載函數(shù)中完成兩件事:通過register_chrdev()函數(shù)將I2C設(shè)備注冊為一個字符設(shè)備。通過I2C核心的i2c_add_driver()函數(shù)添加i2c_driver。
12.3 在linux應(yīng)用層使用I2C
? 前面我們講解了I2C的協(xié)議及在linux驅(qū)動框架,那么當你拿到開發(fā)板或者是從公司的硬件同事拿到一個帶有I2C外設(shè)的板子,我們應(yīng)該如何最快速的使用起來這個I2C設(shè)備呢?既然我們總是說這個I2C總線在嵌入式開發(fā)中被廣泛的使用,那么是否有現(xiàn)成的測試工具幫我們完成這個快速使用板子的I2C設(shè)備呢?答案是有的,而且這個測試工具的代碼還是開源的,它被廣泛的應(yīng)用在linux應(yīng)用層來快速驗證I2C外設(shè)是否可用,為我們測試I2C設(shè)備提供了很好的捷徑。
12.3.1 如何使用I2C tools測試I2C外設(shè)
1) I2C tools概述:
? I2C tools包含一套用于Linux應(yīng)用層測試各種各樣I2C功能的工具。它的主要功能包括:總線探測工具、SMBus訪問幫助程序、EEPROM解碼腳本、EEPROM編程工具和用于SMBus訪問的python模塊。只要你所使用的內(nèi)核中包含I2C設(shè)備驅(qū)動,那么就可以在你的板子中正常使用這個測試工具。
2) 下載I2C tools源碼:
? 前面我們已經(jīng)說過了這個I2C tools工具是開源的,那么這個源碼在哪里可以找到呢?
? 下載方法一:直接在內(nèi)核的網(wǎng)站https://mirrors.edge.kernel.org/pub/software/utils/i2c-tools/下載I2C tools代碼的壓縮包。
? 下載方法二:利用git管理工具下載這個I2C tools的源代碼,命令為git clone git://git.kernel.org/pub/scm/utils/i2c-tools/i2c-tools.git強烈建議讀者采用第二種方法下載這個代碼,因為你可以通過git快速地了解這個開源代碼的不同版本的功能改進及bug修復(fù),而且使用git開發(fā)也是作為一名優(yōu)秀的開發(fā)人員必備的一項技能。
3) 編譯I2C tools源碼:
? 進入剛才利用git下載好的iic-tools源碼目錄,修改編譯工具為你當前使用的交叉編譯工具:
26 CC ?= arm-linux-gnueabihf-gcc
27 AR ?= arm-linux-gnueabihf-ar
? 編譯源碼:如果你想編譯靜態(tài)版本,你可以輸入命令:make USE_STATIC_LIB=1;如果使用動態(tài)庫的話,可以直接輸入make進行編譯。安裝命令為:make install,如果你想要讓最后生成的二進制文件最小的話,可以在“make install”之前運行“make strip”。但是,這將不能生成任何調(diào)試庫,也就不能嘗試進一步調(diào)試。然后將tools目錄下的5個可執(zhí)行文件i2cdetect,i2cdump,i2cget,i2cset和i2ctransfer復(fù)制到板子的/usr/sbin/中;將lib目錄下的libi2c.so.0.1.1文件復(fù)制到板子的/usr/lib/libi2c.so.0。之后別忘了將上面的文件修改為可執(zhí)行的權(quán)限。
4) 介紹I2C tools各功能之—i2cdetect
? i2cdetect的主要功能就是I2C設(shè)備查詢,它用于掃描I2C總線上的設(shè)備。它輸出一個表,其中包含指定總線上檢測到的設(shè)備的列表。
? 該命令的常用格式為:i2cdetect [-y] [-a] [-q|-r] i2cbus [first last]。具體參數(shù)的含義如下:
-y | 取消交互模式。默認情況下,i2cdetect將等待用戶的確認,<br> 當使用此標志時,它將直接執(zhí)行操作。 |
---|---|
-a | 強制掃描非規(guī)則地址。一般不推薦。 |
-q | 使用SMBus“快速寫入”命令進行探測。一般不推薦。 |
-r | 使用SMBus“接收字節(jié)”命令進行探測。一般不推薦。 |
-F | 顯示適配器實現(xiàn)的功能列表并退出。 |
-V | 顯示I2C工具的版本并推出。 |
-l | 顯示已經(jīng)在系統(tǒng)中使用的I2C總線。 |
i2cbus | 表示要掃描的I2C總線的編號或名稱。 |
first last | 表示要掃描的從設(shè)備地址范圍。 |
? 該功能的常用方式:
? 第一,先通過i2cdetect -l查看當前系統(tǒng)中的I2C的總線情況:
? 第二,若總線上掛載I2C從設(shè)備,可通過i2cdetect掃描某個I2C總線上的所有設(shè)備??赏ㄟ^控制臺輸入i2cdetect -y 1:(其中"--"表示地址被探測到了,但沒有芯片應(yīng)答; "UU"因為這個地址目前正在被一個驅(qū)動程序使用,探測被省略;而16進制的地址號60,1e和50則表示發(fā)現(xiàn)了一個外部片選從地址為0x60,0x1e(AP3216)和0x50(eeprom)的外設(shè)芯片。
? 第三,查詢I2C總線1 (I2C -1)的功能,命令為i2cdetect -F 1:
[外鏈圖片轉(zhuǎn)存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-pCijYzNc-1639020220832)(http://photos.100ask.net/NewHomeSite/IIC_Image011.png)]
5) 介紹I2C tools各功能之—i2cget
? i2cget的主要功能是獲取I2C外設(shè)某一寄存器的內(nèi)容。該命令的常用格式為:
? i2cget [-f] [-y] [-a] i2cbus chip-address [data-address [mode]]。具體參數(shù)的含義如下:
-f | 強制訪問設(shè)備,即使它已經(jīng)很忙。 默認情況下,i2cget將拒絕訪問<br> 已經(jīng)在內(nèi)核驅(qū)動程序控制下的設(shè)備。 |
---|---|
-y | 取消交互模式。默認情況下,i2cdetect將等待用戶的確認,當使用此<br/> 標志時,它將直接執(zhí)行操作。 |
-a | 允許在0x00 - 0x07和0x78 - 0x7f之間使用地址。一般不推薦。 |
i2cbus | 表示要掃描的I2C總線的編號或名稱。這個數(shù)字應(yīng)該與i2cdetect -l列出<br/> 的總線之一相對應(yīng)。 |
chip-address | 要操作的外設(shè)從地址。 |
data-address | 被查看外設(shè)的寄存器地址。 |
mode | 顯示數(shù)據(jù)的方式: b (read byte data, default) w (read word data) <br/> c (write byte/read byte) |
? 下面是完成讀取0總線上從地址為0x50的外設(shè)的0x10寄存器的數(shù)據(jù),命令為:
? i2cget -y -f 0 0x50 0x10
6) 介紹I2C tools各功能之—i2cdump
? i2cdump的主要功能查看I2C從設(shè)備器件所有寄存器的值。 該命令的常用格式為:i2cdump [-f] [-r first-last] [-y] [-a] i2cbus address [mode [bank [bankreg]]]。具體參數(shù)的含義如下:
-f | 強制訪問設(shè)備,即使它已經(jīng)很忙。 默認情況下,i2cget將拒絕訪問已經(jīng)在<br/>內(nèi)核驅(qū)動程序控制下的設(shè)備。 |
---|---|
-r | 限制正在訪問的寄存器范圍。 此選項僅在模式b,w,c和W中可用。對于<br/>模式W,first必須是偶數(shù),last必須是奇數(shù)。 |
-y | 取消交互模式。默認情況下,i2cdetect將等待用戶的確認,當使用此標志<br/>時,它將直接執(zhí)行操作。 |
-V | 顯示I2C工具的版本并推出。 |
i2cbus | 表示要掃描的I2C總線的編號或名稱。這個數(shù)字應(yīng)該對應(yīng)于i2cdetect -l列<br/>出的總線之一。 |
first last | 表示要掃描的從設(shè)備地址范圍。 |
mode | b: 單個字節(jié) w:16位字 s:SMBus模塊 i:I2C模塊的讀取大小 c: 連續(xù)讀<br/>取所有字節(jié),對于具有地址自動遞增功能的芯片(如EEPROM)非常有用。<br/>W與 w類似,只是讀命令只能在偶數(shù)寄存器地址上發(fā)出;這也是主要用于EEPROM的。 |
? 下面是完成讀取0總線上從地址為0x50的eeprom的數(shù)據(jù),命令為:
? i2cdump -f -y 0 0x50
7) 介紹I2C tools各功能之—i2cset
? i2cset的主要功能是通過I2C總線設(shè)置設(shè)備中某寄存器的值。該命令的常用格式為:
? i2cset [-f] [-y] [-m mask] [-r] i2cbus chip-address data-address [value] ...[mode]
具體參數(shù)的含義如下:
-f | 強制訪問設(shè)備,即使它已經(jīng)很忙。 默認情況下,i2cget將拒絕訪問已<br/>經(jīng)在內(nèi)核驅(qū)動程序控制下的設(shè)備。 |
---|---|
-r | 在寫入值之后立即讀取它,并將結(jié)果與寫入的值進行比較。 |
-y | 取消交互模式。默認情況下,i2cdetect將等待用戶的確認,當使用此標<br/>志時,它將直接執(zhí)行操作。 |
-V | 顯示I2C工具的版本并推出。 |
i2cbus | 表示要掃描的I2C總線的編號或名稱。這個數(shù)字應(yīng)該對應(yīng)于i2cdetect -l列<br/>出的總線之一。 |
-m mask | 如果指定mask參數(shù),那么描述哪些value位將是實際寫入data-addres的。<br/>掩碼中設(shè)置為1的位將從值中取出,而設(shè)置為0的位將從數(shù)據(jù)地址中讀取,從<br/>而由操作保存。 |
mode | b: 單個字節(jié) w:16位字 s:SMBus模塊 i:I2C模塊的讀取大小 c: 連續(xù)讀<br/>取所有字節(jié),對于具有地址自動遞增功能的芯片(如EEPROM)非常有用。<br/> W與 w類似,只是讀命令只能在偶數(shù)寄存器地址上發(fā)出;這也是主要用于<br/>EEPROM的。 |
? 下面是完成向0總線上從地址為0x50的eeprom的0x10寄存器寫入0x55,命令為:
? i2cset -y -f 0 0x50 0x10 0x55
? 然后用i2cget讀取0總線上從地址為0x50的eeprom的0x10寄存器的數(shù)據(jù),命令為:i2cget -y -f 0 0x50 0x10
8) 介紹I2C tools各功能之—i2ctransfer
? i2ctransfer的主要功能是在一次傳輸中發(fā)送用戶定義的I2C消息。i2ctransfer是一個創(chuàng)建I2C消息并將其合并為一個傳輸發(fā)送的程序。對于讀消息,接收緩沖區(qū)的內(nèi)容被打印到stdout,每個讀消息一行。
? 該命令的常用格式為:i2ctransfer [-f] [-y] [-v] [-a] i2cbus desc [data] [desc [data]]
? 具體參數(shù)的含義如下:
-f | 強制訪問設(shè)備,即使它已經(jīng)很忙。 默認情況下,i2cget將拒絕訪問已<br/>經(jīng)在內(nèi)核驅(qū)動程序控制下的設(shè)備。 |
---|---|
-y | 取消交互模式。默認情況下,i2cdetect將等待用戶的確認,當使用此<br/>標志時,它將直接執(zhí)行操作。 |
-v | 啟用詳細輸出。它將打印所有信息發(fā)送,即不僅為讀消息,也為寫消息。 |
-V | 顯示I2C工具的版本并推出。 |
-a | 允許在0x00 - 0x02和0x78 - 0x7f之間使用地址。一般不推薦。 |
i2cbus | 表示要掃描的I2C總線的編號或名稱。這個數(shù)字應(yīng)該對應(yīng)于i2cdetect -l<br/>列出的總線之一。 |
? 下面是完成向0總線上從地址為0x50的eeprom的0x20開始的4個寄存器寫入0x01,0x02,0x03,0x04命令為:i2ctransfer -f -y 0 w5@0x50 0x20 0x01 0x02 0x03 0x04然后再通過命令i2ctransfer -f -y 0 w1@0x50 0x20 r4將0x20地址的4個寄存器數(shù)據(jù)讀出來,見下圖:
12.3.2 在linux應(yīng)用程序中讀寫I2C外設(shè)
? 首先通過前面的介紹,我們已經(jīng)知道站在cpu的角度來看,操作I2C外設(shè)實際上就是通過控制cpu中掛載該I2C外設(shè)的I2C控制器,而這個I2C控制器在linux系統(tǒng)中被稱為“I2C適配器”,這個已經(jīng)在驅(qū)動簡介中介紹過了。而且眾所周知,在linux系統(tǒng)中,每一個設(shè)備都是以文件的形式存在的,所以在linux中操作I2C外設(shè)就變成了操作I2C適配器設(shè)備文件。Linux系統(tǒng)(也就是內(nèi)核)為每個I2C適配器生成了一個主設(shè)備號為89的設(shè)備節(jié)點(次設(shè)備號為0-255),它并沒有針對特定的I2C外設(shè)而設(shè)計,只是提供了通用的read(),write(),和ioctl()等文件操作接口,在用戶空間的應(yīng)用層就可以借用這些接口訪問掛接在適配器上的I2C設(shè)備的存儲空間或寄存器,并控制I2C設(shè)備的工作方式。
? 操作流程:
1) 確定I2C適配器的設(shè)備文件節(jié)點
? i2c適配器的設(shè)備節(jié)點是/dev/i2c-x,其中x是數(shù)字。由于適配器編號是動態(tài)分配的(和注冊次序有關(guān)),所以想了解哪一個適配器對應(yīng)什么編號,可以查看/sys/class/i2c-dev/目錄下的文件內(nèi)容(在這里筆者強烈建議讀者好好利用好sys文件系統(tǒng)):
cat /sys/class/i2c-dev/i2c-0/name
cat /sys/class/i2c-dev/i2c-1/name
? 然后查看硬件原理圖中eeprom是掛在cpu的i2c1控制器中了,然后查看IMX6UL芯片手冊中I2C1的寄存器地址為21A_0000。
? 比對后,我們就很容易知道eeprom外設(shè)對應(yīng)的I2C控制器的設(shè)備節(jié)點為:/dev/i2c-0。
2) 打開適配器對應(yīng)的設(shè)備節(jié)點
? 當用戶打開適配器設(shè)備節(jié)點的時候,Kernel中的i2c-dev代碼為其建立一個i2c_client,但是這個i2c_client并不加到i2c_adapter的client鏈表當中。當用戶關(guān)閉設(shè)備節(jié)點時,它自動被釋放。
3) IOCTL控制
? 這個可以參考內(nèi)核源碼中的include/linux/i2c-dev.h文件。下面舉例說明主要的IOCTL命令:
I2C_SLAVE_FORCE | 設(shè)置I2C從設(shè)備地址(只有在該地址空閑的情況下成功) |
---|---|
I2C_SLAVE_FORCE | 強制設(shè)置I2C從設(shè)備地址(無論內(nèi)核中是否已有驅(qū)動在使用<br/>這個地址都會成功) |
I2C_TENBIT | 選擇地址位長: 0 表示是7bit地址 ; 不等于0 就是10 bit的<br/>地址。只有適配器支持I2C_FUNC_10BIT_ADDR,這個請求才是有效的。 |
I2C_FUNCS | 獲取適配器支持的功能,詳細的可以參考文件include/linux/i2c.h |
I2C_RDWR | 設(shè)置為可讀寫 |
I2C_RETRIES | 設(shè)置收不到ACK時的重試次數(shù) |
I2C_TIMEOUT | 設(shè)置超時的時限 |
4) 使用I2C協(xié)議和設(shè)備進行通信
? 代碼為:ioctl(file,I2C_RDWR,(struct i2c_rdwr_ioctl_data )msgset); 它可以進行連續(xù)的讀寫,中間沒有間歇。只有當適配器支持I2C_FUNC_I2C此命令才有效。參數(shù)msgset是一個指針,指向一個i2c_rdwr_ioctl_data類型的結(jié)構(gòu)體,該結(jié)構(gòu)體的功能就是讓應(yīng)用程序可以向內(nèi)核傳遞消息,其成員包括:struct i2c_msg __ user msgs; 和表示i2c_msgs 個數(shù)的 __u32 nmsgs,它也決定了在硬件I2C總線的硬件通信中有多少個開始信號。由于I2C適配器與外設(shè)通信是以消息為單位的,所以struct i2c_msg對我們來說是非常重要的,它可以包含多條消息,而一條消息有可能包含多個數(shù)據(jù),比如對于eeprom頁寫就包含多個數(shù)據(jù)。下面就介紹一下這個結(jié)構(gòu)體的內(nèi)容:
__u16 addr; | 從設(shè)備地址 |
---|---|
__u16 flags; | 標志(讀/寫) |
I2C_M_TEN | 這是一個10位芯片地址 |
I2C_M_RD | 從設(shè)備到適配器讀數(shù)據(jù) |
I2C_M_NOSTART | 不發(fā)送起始位 |
I2C_M_REV_DIR_ADDR | 翻轉(zhuǎn)讀寫標志 |
I2C_M_IGNORE_NAK | 忽略I2C的NACK信號 |
I2C_M_NO_RD_ACK | 讀操作的時候不發(fā)ACK信號 |
I2C_M_RECV_LEN | 第一次接收數(shù)據(jù)的長度 |
__u16 len; | 寫入或者讀出數(shù)據(jù)的個數(shù)(字節(jié)) |
__u8 *buf; | 寫入或者讀出數(shù)據(jù)的地址 buf[0]。 注意:千萬不要忘記給 2c_rdwr_ioctl_data結(jié)構(gòu)體中的最重要的結(jié)構(gòu)i2c_msg中的buf分配內(nèi)存。 |
5) 用read和write讀寫I2C設(shè)備
? 當然你可以使用read()/write()來與I2C設(shè)備進行通信,代碼如下(以eeprom為例簡要概述操作過程):
? 第一,打開I2C控制器文件節(jié)點: fd =open(“/dev/i2c-0”, O_RDWR);
? 第二,設(shè)置eeprom的設(shè)備地址:ioctl(fd,I2C_SLAVE, 0x50);
? 第三,向eeprom寫數(shù)據(jù):
首先將要操作的eeprom的第一個寄存器地址賦給寫buf的第0個元素wr_buf[0] = 0x10;
然后把要寫入的數(shù)據(jù)寫入到后面的buf中for(i=1;i<13;i++) wr_buf[i]=i;
最后通過write函數(shù)完成向eeprom寫數(shù)據(jù)的功能:write(fd, wr_buf, 13);
? 最后延遲1秒,讓后面的操作與上面的寫操作分開。
? 第四,從eeprom讀數(shù)據(jù):
首先和寫操作一樣,將要操作的寄存器首地址0x10發(fā)給eeprom:write(fd, wr_buf, 1);
從0x10寄存器地址處讀取12個字節(jié)的數(shù)據(jù):ret=read(fd, rd_buf, 12);
? 你會發(fā)現(xiàn),用read和write一次只能進行一個方向的傳輸:或者是讀外設(shè)操作,或者就是寫操作傳輸。
? 代碼如下:
01 #include <stdio.h>
02 #include <sys/ioctl.h>
03 #include <unistd.h>
04 #include <fcntl.h>
05 #include <linux/i2c-dev.h>
06 #include <linux/i2c.h>
07
08 /* eeprom所對應(yīng)的I2C控制器的設(shè)備節(jié)點 */
09 #define EEPROM_DEVICE "/dev/i2c-0"
10
11 /* eeprom的I2C設(shè)備地址 */
12 #define EEPROM_ADDR 0x50
13
14
15 int main()
16 {
17 int fd,i,ret=0;
18 unsigned char w_add=0x10;
19
20 /* 將要讀取的數(shù)據(jù)buf*/
21 unsigned char rd_buf[13] = {0x10};
22
23 /* 要寫的數(shù)據(jù)buf,第0個元素是要操作eeprom的寄存器地址*/
24 unsigned char wr_buf[13] = {0};
25
26 printf("hello,this is read_write i2c test
");
27
28 /* 打開eeprom對應(yīng)的I2C控制器文件 */
29 fd =open(EEPROM_DEVICE, O_RDWR);
30 if (fd< 0)
31 {
32 printf("open"EEPROM_DEVICE"failed
");
33 }
34
35 /*設(shè)置eeprom的I2C設(shè)備地址*/
36 if (ioctl(fd,I2C_SLAVE_FORCE, EEPROM_ADDR) < 0)
37 {
38 printf("set slave address failed
");
39 }
40
41 /* 將要操作的寄存器首地址賦給wr_buf[0] */
42 wr_buf[0] = w_add;
43
44 /* 把要寫入的數(shù)據(jù)寫入到后面的buf中 */
45 for(i=1;i<13;i++)
46 wr_buf[i]=i;
47
48 /* 通過write函數(shù)完成向eeprom寫數(shù)據(jù)的功能 */
49 write(fd, wr_buf, 13);
50
51 /* 延遲一段時間 */
52 sleep(1);
53
54 /*重新開始下一個操作,先寫寄存器的首地址*/
55 write(fd, wr_buf, 1);
56
57 /* 從wr_buf[0] = w_add的寄存器地址開始讀取12個字節(jié)的數(shù)據(jù) */
58 ret=read(fd, rd_buf, 12);
59 printf("ret is %d
",ret);
60
61 for(i=0;i<12;i++)
62 {
63 printf("rd_buf is :%d
",rd_buf[i]);
64 }
65
66 /* 完成操作后,關(guān)閉eeprom對應(yīng)的I2C控制器的設(shè)備文件 */
67 close(fd);
68
69 return 0;
70 }
6) 用數(shù)據(jù)包的方式操作I2C設(shè)備
? 構(gòu)建數(shù)據(jù)包結(jié)構(gòu)體:
? 首先是struct i2c_rdwr_ioctl_data data; 應(yīng)用程序通過該結(jié)構(gòu)體來給內(nèi)核傳遞消息。該結(jié)構(gòu)體包含兩個成員struct i2c_msg user * msgs;和 u32 nmsgs;其中msgs指向表示通信方法傳輸為消息的結(jié)構(gòu)體。而nmsgs則決定了該數(shù)據(jù)包有多少個這樣的通信消息,在I2C通信協(xié)議上來看就代表了有多少個開始信號。
? 接著就是struct i2c_msg; 它可以包含多條消息,而一條消息有可能包含多個數(shù)據(jù)。其成員包括:“代表I2C設(shè)備從地址的 u16 addr; 表示本次消息的標志位的 u16 flags; 表示數(shù)據(jù)長度的 u16 len; 表示數(shù)據(jù)緩沖區(qū)的指針 u8 buf”
? 然后把要和I2C從設(shè)備通信的數(shù)據(jù)與上面兩個結(jié)構(gòu)體建立起相應(yīng)的聯(lián)系。
? 最后調(diào)用I2C_RDWR進入驅(qū)動程序執(zhí)行讀寫組合的I2C數(shù)據(jù)傳輸。
? 代碼如下:
01 #include <stdio.h>
02 #include <string.h>
03 #include <sys/ioctl.h>
04 #include <unistd.h>
05 #include <fcntl.h>
06 #include <linux/i2c-dev.h>
07 #include <linux/i2c.h>
08
09 /* eeprom所對應(yīng)的I2C控制器的設(shè)備節(jié)點 */
10 #define EEPROM_DEVICE "/dev/i2c-0"
11
12 /* eeprom的I2C設(shè)備地址 */
13 #define EEPROM_ADDR 0x50
14
15 /*函數(shù)名:eeprom_write
16 **功能:向eeprom寫數(shù)據(jù)
17 **參數(shù):fd:eeprom對應(yīng)I2C控制器設(shè)備節(jié)點的文件名
18 ** dev_addr:eeprom的I2C從設(shè)備地址
19 ** reg_addr:eeprom的寄存器地址
20 ** data_buf:要向eeprom寫數(shù)據(jù)的數(shù)據(jù)buf
21 ** len:要寫多少個字節(jié)。本例中當前最大支持為8個字節(jié)
22 **返回值:負數(shù)表示操作失敗,其他為成功
23 */
24 int eeprom_write(int fd, unsigned char dev_addr, unsigned char reg_addr, unsigned char * data_buf,int len)
25 {
26 int ret;
27
28 unsigned char msg_buf[9];
29 struct i2c_rdwr_ioctl_data data;
30
31 struct i2c_msg messages;
32
33
34 /* 1. 構(gòu)建msg_buf*/
35 /* 1.1. 將要操作的寄存器首地址賦給要進行I2C數(shù)據(jù)通信的首字節(jié)數(shù)據(jù) */
36 msg_buf[0] = reg_addr;
37
38 /* 1.2. 將要向eeprom寫數(shù)據(jù)的數(shù)據(jù)buf賦在I2C數(shù)據(jù)通信中eeprom寄存器的后面 */
39 if (len < 9) { /* 本demo最大支持一次向eeprom寫一頁大小的8個字節(jié)數(shù)據(jù) */
40 memcpy((void *) &msg_buf[1], data_buf, len); //第1位之后是數(shù)據(jù)
41 } else {
42 printf("This function supports up to 8 bytes at a time !!!
");
43 return -1;
44 }
45
46 /* 2. 構(gòu)建 struct i2c_msg messages */
47 /* 2.1. 賦值eeprom的I2C從設(shè)備地址 */
48 messages.addr = dev_addr;
49
50 /* 2.2. 賦值flags為本次I2C通信完成寫功能 */
51 messages.flags = 0;
52
53 /* 2.3. 賦值len為數(shù)據(jù)buf的長度 + eeprom寄存器地址的數(shù)據(jù)長度 */
54 messages.len = len+1;
55
56 /* 2.4. 構(gòu)建消息包的數(shù)據(jù)buf*/
57 messages.buf = msg_buf;
58
59 /* 3. 構(gòu)建struct i2c_rdwr_ioctl_data data */
60 /* 3.1. 將準備好的消息包賦值給i2c_rdwr_ioctl_data中的msgs消息*/
61 data.msgs = &messages;
62
63 /* 3.2. 由于本次I2C通信只有寫動作,所以消息數(shù)為1次 */
64 data.nmsgs = 1;
65
66 /* 4. 調(diào)用驅(qū)動層的讀寫組合的I2C數(shù)據(jù)傳輸 */
67 if(ioctl(fd, I2C_RDWR, &data) < 0)
68 {
69 printf("I2C_RDWR err
");
70 return -1;
71 }
72
73 /* 5. 等待I2C總線寫入完成 */
74 sleep(1);
75
76 return 0;
77 }
78
79 /*函數(shù)名:eeprom_read
80 **功能:從eeprom讀數(shù)據(jù)
81 **參數(shù):fd:eeprom對應(yīng)I2C控制器設(shè)備節(jié)點的文件名
82 ** dev_addr:eeprom的I2C從設(shè)備地址
83 ** reg_addr:eeprom的寄存器地址
84 ** data_buf:存放從eeprom讀數(shù)據(jù)的buf
85 ** len:要讀多少個字節(jié)。
86 **返回值:負數(shù)表示操作失敗,其他為成功
87 */
88 int eeprom_read(int fd, unsigned char dev_addr, unsigned char reg_addr, unsigned char * data_buf,int len)
89 {
90 int ret;
91
92 unsigned char msg_buf[9];
93 struct i2c_rdwr_ioctl_data data;
94
95 struct i2c_msg messages[2];
96
97 /* 1. 構(gòu)建 struct i2c_msg messages */
98 /* 1.1. 構(gòu)建第一條消息 messages[0] */
99 /* 1.1.1. 賦值eeprom的I2C從設(shè)備地址 */
100 messages[0].addr = dev_addr;
101
102 /* 1.1.2. 賦值flags為本次I2C通信完成寫動作 */
103 messages[0].flags = 0;
104
105 /* 1.1.3. 賦值len為eeprom寄存器地址的數(shù)據(jù)長度是1 */
106 messages[0].len = 1;
107
108 /* 1.1.4. 本次寫動作的數(shù)據(jù)是要讀取eeprom的寄存器首地址*/
109 messages[0].buf = ®_addr;
110
111 /* 1.2. 構(gòu)建第二條消息 messages[1] */
112 /* 1.2.1. 賦值eeprom的I2C從設(shè)備地址 */
113 messages[1].addr = dev_addr;
114
115 /* 1.1.2. 賦值flags為本次I2C通信完成讀動作 */
116 messages[1].flags = I2C_M_RD;
117
118 /* 1.1.3. 賦值len為要讀取eeprom寄存器數(shù)據(jù)長度len */
119 messages[1].len = len;
120
121 /* 1.1.4. 本次讀動作的數(shù)據(jù)要存放的buf位置*/
122 messages[1].buf = data_buf;
123
124 /* 2. 構(gòu)建struct i2c_rdwr_ioctl_data data */
125 /* 2.1. 將準備好的消息包賦值給i2c_rdwr_ioctl_data中的msgs消息*/
126 data.msgs = messages;
127
128 /* 2.2. 由于本次I2C通信既有寫動作也有讀動作,所以消息數(shù)為2次 */
129 data.nmsgs = 2;
130
131 /* 3. 調(diào)用驅(qū)動層的讀寫組合的I2C數(shù)據(jù)傳輸 */
132 if(ioctl(fd, I2C_RDWR, &data) < 0)
133 {
134 printf("I2C_RDWR err
");
135 return -1;
136 }
137
138 /* 4. 等待I2C總線讀取完成 */
139 sleep(1);
140
141 return 0;
142 }
143
144 int main()
145 {
146 int fd,i,ret=0;
147 unsigned char w_add=0x10;
148
149 /* 將要讀取的數(shù)據(jù)buf*/
150 unsigned char rd_buf[8] = {0};
151
152 /* 要寫的數(shù)據(jù)buf*/
153 unsigned char wr_buf[8] = {0};
154
155 printf("hello,this is I2C_RDWR i2c test
");
156
157 /* 打開eeprom對應(yīng)的I2C控制器文件 */
158 fd =open(EEPROM_DEVICE, O_RDWR);
159 if (fd< 0)
160 {
161 printf("open"EEPROM_DEVICE"failed
");
162 }
163
164 /* 把要寫入的數(shù)據(jù)寫入到后面的buf中 */
165 for(i=0;i<8;i++)
166 wr_buf[i]=i;
167
168 /* 通過I2C_RDWR完成向eeprom讀數(shù)據(jù)的功能 */
169 eeprom_write(fd,EEPROM_ADDR,w_add,wr_buf,8);
170
171
172 /* 通過I2C_RDWR完成向eeprom寫數(shù)據(jù)的功能 */
173 eeprom_read(fd,EEPROM_ADDR,w_add,rd_buf,8);
174
175 for(i=0;i<8;i++)
176 {
177 printf("rd_buf is :%d
",rd_buf[i]);
178 }
179
180 /* 完成操作后,關(guān)閉eeprom對應(yīng)的I2C控制器的設(shè)備文件 */
181 close(fd);
182
183 return 0;
184 }
185
186
12.3.3 簡介I2C的調(diào)試方式
1) 概述I2C通信中完成正常通信的常見元素:
? 第一,先檢查I2C總線上的所有設(shè)備是否都經(jīng)上拉電阻到電源,并檢查供電是否穩(wěn)定。
? 第二,數(shù)據(jù)線和時鐘信號線是否有接反的情況。
? 第三,I2C的通信速率是否超過了設(shè)備所支持的最高速度。
? 第四,檢查外部I2C設(shè)備與操作的I2C控制器是否掛在了同一條I2C總線上。
? 第五,檢查操作的I2C外設(shè)地址是否正確。
? 第六,檢查I2C總線上是否有多個相同設(shè)備地址的從機設(shè)備,導(dǎo)致通信沖突。
? 第七,操作的I2C外設(shè)是否處于寫保護狀態(tài),寫保護狀態(tài)是無法寫入數(shù)據(jù)的。
? 第八,檢查I2C通信時序是否滿足I2C通信協(xié)議。
? 第九,檢查在沒有開始運行I2C通信程序的時候,I2C總線上的電平信號是否干凈穩(wěn)定的保持高電平,是否出現(xiàn)過主機誤把SDA拉低的情況,導(dǎo)致I2C總線出現(xiàn)“忙碌”狀態(tài)。
? 第十,檢查I2C通信過程中是否出現(xiàn)SDA或者SCL被長時間一直拉低的狀態(tài)。比如I2C外設(shè)從機由于異常在發(fā)送完ACK信號后沒有釋放SDA。另一種情況是cpu在做從機的時候,沒有及時完成將讀取的主機數(shù)據(jù)進行處理,導(dǎo)致長時間將SCL拉低,破壞了I2C通信流程,因此我們在寫I2C通信的時候最好盡快在I2C接收數(shù)據(jù)中斷服務(wù)函數(shù)中完成數(shù)據(jù)處理工作并授權(quán)I2C控制器讓其正常工作。
? 由于I2C總線的協(xié)議特性,如果總線上有任何一個I2C設(shè)備將SCL或者SDA的信號拉低,其他的I2C設(shè)備都將看到這個低電平,并且都無法拉高他們。這也就是說,如果有設(shè)備不釋放總線,一直把總線的電平拉低,那么整個I2C總線將會出現(xiàn)暫停掛死的狀態(tài),將無法按照I2C協(xié)議進行正常通信。
? 如果負責(zé)I2C總線主機cpu的I2C控制器出現(xiàn)上述長時間拉低I2C總線的電平,理論上我們可以通過調(diào)試代碼找出I2C總線死機的原因,并修改代碼重新初始化該I2C控制器來復(fù)位它,讓其重新進行I2C通信。如果通過調(diào)試發(fā)現(xiàn)導(dǎo)致I2C總線死機的原因是由I2C外設(shè)導(dǎo)致的,那么我們可以復(fù)位該外設(shè)芯片。但是在實際的項目開發(fā)中,可能復(fù)位I2C總線上的元件也無法恢復(fù)正常的I2C通信,這個時候就要設(shè)計I2C總線的主機程序?qū)2C控制器引腳設(shè)置為GPIO功能并模擬I2C協(xié)議完成一次完整的I2C通信,再將I2C控制器設(shè)置設(shè)置為I2C功能。
12.4 總結(jié)I2C在嵌入式項目開發(fā)的應(yīng)用優(yōu)缺點
? 優(yōu)點:只使用兩根線,支持多個主控制器和多個從設(shè)備,I2C具有非常廣泛使用的協(xié)議。
? 缺點:數(shù)據(jù)傳輸速率比SPI慢,數(shù)據(jù)幀的大小限制為8位,實現(xiàn)比SPI更復(fù)雜的硬件。而且I2C通信需要注意下面的使用問題:
1) I2C時鐘信號(SCL)的同步問題
? 在I2C總線上傳送信息時的時鐘同步信號是由掛接在SCL線上的所有器件的邏輯“與”完成的。SCL線上由高電平到低電平的跳變將影響到這些器件,一旦某個器件的時鐘信號下跳為低電平,將使SCL線一直保持低電平,使SCL線上的所有器件開始低電平期。此時,低電平周期短的器件的時鐘由低至高的跳變并不能影響SCL線的狀態(tài),于是這些器件將進入高電平等待的狀態(tài)。當所有器件的時鐘信號都上跳為高電平時,低電平期結(jié)束,SCL線被釋放返回高電平,即所有的器件都同時開始它們的高電平期。其后,第一個結(jié)束高電平期的器件又將SCL線拉成低電平。這樣就在SCL線上產(chǎn)生一個同步時鐘。可見,時鐘低電平時間由時鐘低電平期最長的器件確定,而時鐘高電平時間由時鐘高電平期最短的器件確定。
2) 總線驅(qū)動能力
? 上拉電阻和負載電容決定了總線在某一速率下的穩(wěn)定性。當輸出為高時,電流通過上拉電阻對負載電容充電。上拉越大,電容越大,所需要的時間就越長,如果超過了通信周期的10%,那么這個上升沿就太緩了,相應(yīng)的建立時間會受到影響,I2C規(guī)范的最大負載電容是400pF,快速模式下是100pF。如果輸出為低,電流通過上拉電阻被I2C master器件吸取,(注意根據(jù)I2C規(guī)范,最小只有3毫安的吸取電流)那么這個吸取電流在上拉電阻上的壓降就決定了輸出低電平能達到的范圍,如果不能達到0.3VDD以下,就會有誤采樣。有人說加大上拉電阻是不妥當?shù)?,要具體分析吸取電流、負載電容、上拉電平和通信速率才能決定(普通模式和快速模式是不一樣的)。
? 雖然速度不是特別快,但是信號線上如果有加電容的話,切記不要加大的,一定要小,否則信號還沒到從設(shè)備呢,就被電容吃了。
本文摘自 :https://blog.51cto.com/w