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