博客專欄

        EEPW首頁 > 博客 > 老宇哥帶你玩轉(zhuǎn) ESP32:07 I2C協(xié)議,看這一篇就夠了

        老宇哥帶你玩轉(zhuǎn) ESP32:07 I2C協(xié)議,看這一篇就夠了

        發(fā)布人:芯片之大家 時間:2023-07-04 來源:工程師 發(fā)布文章

        今天我們來玩兒I2C

        I2C概述

        I2C全稱是Inter-Integrated Circuit,是飛利浦半導(dǎo)體公司(06年遷移到NXP了)在1982年發(fā)明的,是使用非常廣泛的一種通信協(xié)議,很多傳感器、存儲芯片、OLED等,都是在使用I2C。標(biāo)準(zhǔn)輸出模式下能達到100kbps的傳輸速率,快速模式下能達到400kbps的傳輸速率,高速模式下能達到3.4Mbps,超高速下最快能達到5Mbps

        與UART一樣,IIC僅用兩條線在設(shè)備間通信:

        image.png


        SCL -- 時鐘信號

        SDA -- 數(shù)據(jù)信號

        I2C主機與從機之間共享時鐘信號,時鐘始終由主機控制,總線下面可以掛多個設(shè)備,是一種同步,多主,多從,半雙工的通信協(xié)議,下面我們簡單介紹一下通信原理:

        image.png


        默認情況下,兩條線都被上拉,SCL=1,SDA=1。

        啟動與停止信號:

        通信開始,要先發(fā)開啟動信號,結(jié)束的時候,要發(fā)送結(jié)束信號。

        開始信號由主設(shè)備發(fā)出啟動,具體為在SCL高電平期間,SDA從高電平切換到低電平

        停止信號由主設(shè)備發(fā)出結(jié)束,具體為在SCL高電平期間,SDA從低電平切換到高電平

        image.png


        當(dāng)然,在傳輸過程中,有時候需要更改數(shù)據(jù)方向,重新傳輸?shù)龋覀儧]必要發(fā)停止信號,直接重新發(fā)啟動信號啟動即可。

        image.png


        地址字節(jié)

        我們的總線上可能掛很多從設(shè)備,在我們主設(shè)備發(fā)送了啟動信號之后,總線上的從設(shè)備就都被“喚醒”了,等著主設(shè)備發(fā)送地址寵幸。所以這里有一個從機地址的概念,從機地址以8位字節(jié)發(fā)送的,MSB在前,最后一位表示接下來讀或?qū)懀?span style="box-sizing: border-box; margin: 0px; padding: 0px; border: 0px; color: rgb(239, 112, 96); --tt-darkmode-color: #EF7060;">高7位構(gòu)成了從機地址,也可以看出,同一個總線上,可以尋址128個從設(shè)備。

        一旦從設(shè)備的地址匹配,就繼續(xù)讀取最后一位,低電平代表寫入,高電平代表讀取。其它從設(shè)備就忽略后面的數(shù)據(jù)。

        ACK與NACK

        在每個字節(jié)傳輸之后,接收設(shè)備發(fā)送一個應(yīng)答信號,確認或者不確認,接收設(shè)備通過在SCL高電平期間,將SDA拉低生成一個確認信號ACK拉高生成一個不確認信號NACK,這里ACK主要用于表示字節(jié)正確傳輸了,NACK表示數(shù)據(jù)傳輸有錯誤,需要從新發(fā)送。應(yīng)答信號主設(shè)備,從設(shè)備都可以產(chǎn)生,比如,主設(shè)備從從設(shè)備讀取最后一個字節(jié)的數(shù)據(jù)后,就要發(fā)送NACK結(jié)束傳輸。

        image.png


        數(shù)據(jù)信號

        數(shù)據(jù)以8位字節(jié)格式傳輸,高字節(jié)在前,傳輸?shù)淖止?jié)數(shù)量沒有限制,但是每個字節(jié)后面必須要有一個數(shù)據(jù)接收方產(chǎn)生的應(yīng)答信號。傳輸過程中,SCL為低的時候,SDA數(shù)據(jù)可以改變,SCL為高的時候,SDA的數(shù)據(jù)必須穩(wěn)定

        image.png


        命令字節(jié)

        當(dāng)寫入或讀取從設(shè)備中特定寄存器時,主機首先要向已尋址的從機寫入寄存器地址,其實也是一個數(shù)據(jù)字節(jié),我們這里稱之為命令字節(jié)。

        寫入設(shè)備

        主設(shè)備在發(fā)出啟動信號之后,緊著著發(fā)送要操作從設(shè)備的地址,最后一位為低電平表示接下來寫入數(shù)據(jù),然后在時鐘信號下一位一位的寫入數(shù)據(jù),在從設(shè)備發(fā)出ACK應(yīng)答之后,發(fā)送結(jié)束信號結(jié)束通信。

        image.png


        讀取數(shù)據(jù)

        主設(shè)備在發(fā)出啟動信號之后,緊著著發(fā)送要操作從設(shè)備的地址,最后一位為高電平表示接下來讀取數(shù)據(jù),然后接管SDA數(shù)據(jù)線并在時鐘的控制下向主設(shè)備發(fā)送數(shù)據(jù),主設(shè)備同樣要在每個字節(jié)接收完畢的時候發(fā)送ACK響應(yīng),當(dāng)主設(shè)備不想接收的時候,就在最后一個字節(jié)接收后發(fā)送NACK響應(yīng),然后恢復(fù)對總線的控制并發(fā)送結(jié)束信號。

        SCL的控制權(quán)始終在主機這里。

        image.png


        當(dāng)然,實際還要很多組合傳輸協(xié)議,這里由于篇幅問題就不展開說了,基本上大同小異,我們根據(jù)不同設(shè)備的數(shù)據(jù)手冊來傳輸就可以啦。I2C還有很多特性,快速命令,仲裁,多主控等等,普通的應(yīng)用接觸不到,感興趣的小伙伴自行研究下。

        硬件

        ESP32有2個硬件I2C總線接口,接口可以配置為主機或從機模式,支持如下特性:

        • 標(biāo)準(zhǔn)模式 (100 Kbit/s)

        • 快速模式 (400 Kbit/s)

        • 高達 5 MHz,但受 SDA 上拉強度的限制

        • 7位/10位尋址模式

        • 雙尋址模式,用戶可以通過編程命令寄存器來控制 I2C 接口,讓他們有更大的靈活性

        SDA與SCL是低電平有效的,所以我們應(yīng)該在兩根數(shù)據(jù)線上用電阻上拉,IO內(nèi)部也是開漏輸出的,一般5V系統(tǒng)接4.7K上拉,3.3V系統(tǒng)接2.4K上拉即可。ESP32上,SDA默認連接GPIO21,SCL默認連接GPIO22,當(dāng)然,我們可以在代碼中配置到任何引腳

        image.png


        軟件

        啟動I2C

        啟動Wire庫并作為主機或者從機加入總線,這個函數(shù)調(diào)用一次即可,參數(shù)為7位從機地址,不帶參數(shù)就以主機的形式加入總線。

        Wire.begin();Wire.begin(address)

        主設(shè)備從從設(shè)備請求字節(jié)

        由主設(shè)備向從設(shè)備請求字節(jié),之后用available()和read()函數(shù)讀取字節(jié),第三個參數(shù)位為stop,在請求后會發(fā)送停止消息,釋放I2C總線,否則總線就不會被釋放。

        Wire.requestFrom(address, quantity);Wire.requestFrom(address, quantity, stop);

        給指定地址的從設(shè)備傳輸數(shù)據(jù)

        給指定地址的從設(shè)備傳輸數(shù)據(jù),之后調(diào)用write()函數(shù)排隊傳輸字節(jié),要通過endTransmission()結(jié)束傳輸。

        Wire.beginTransmission(address)

        endTransmission()有以下幾個返回結(jié)果:

        • 0:成功

        • 1:數(shù)據(jù)太長,無法放入發(fā)送緩沖區(qū)

        • 2:在發(fā)送地址時收到 NACK

        • 3:在發(fā)送數(shù)據(jù)時收到 NACK

        • 4:其他錯誤

        寫數(shù)據(jù)

        向從設(shè)備寫入數(shù)據(jù),在調(diào)用 beginTransmission() 和 endTransmission() 之間。

        Wire.write(value)
        Wire.write(string)
        Wire.write(data, length)

        舉個例子

        #include <Wire.h>byte val = 0;void setup(){
          Wire.begin(); // join i2c bus}void loop(){
          Wire.beginTransmission(44); // transmit to device #44 (0x2c)
                                      // device address is specified in datasheet
          Wire.write(val);             // sends value byte  
          Wire.endTransmission();     // stop transmitting
        
          val++;        // increment value
          if(val == 64) // if reached 64th position (max)
          {
            val = 0;    // start over from lowest value
          }
          delay(500);
        }

        讀數(shù)據(jù)

        調(diào)用requestFrom()后從從設(shè)備讀取數(shù)據(jù)。

        Wire.read()

        舉個例子

        #include <Wire.h>void setup(){
          Wire.begin();        // join i2c bus (address optional for master)
          Serial.begin(9600);  // start serial for output}void loop(){
          Wire.requestFrom(2, 6);    // request 6 bytes from slave device #2
        
          while(Wire.available())    // slave may send less than requested
          {    char c = Wire.read();    // receive a byte as character
            Serial.print(c);         // print the character
          }
        
          delay(500);
        }

        還有其它一些函數(shù),例如修改時鐘頻率等等,大家用到的時候自行了解一下。

        完整程序

        這里我們用一個例子來演示一下,I2C啟動之后,我們開始掃描總線上存在的設(shè)備,并通過串口打印結(jié)果出來,我在I2C下面接了一個OLED的設(shè)備。

        #include "Wire.h"void setup(){
          Serial.begin(115200); 
          Serial.println();
          Serial.println("Scanning for I2C Devices ...");
          Serial.print("\r\n");  int I2CDevices = 0;  byte address;
        
          Wire.begin();  
          for (address = 1; address < 127; address++)
          {
            Wire.beginTransmission(address);    if (Wire.endTransmission() == 0)
            {
              Serial.print("Found I2C Device: ");
              Serial.print(" (0x");      if (address < 16)
              {
                Serial.print("0");
              }
              Serial.print(address, HEX);
              Serial.println(")");
              I2CDevices++;
            }
          }  if (I2CDevices == 0)
          {
            Serial.println("沒有發(fā)現(xiàn)I2C設(shè)備!\n");
          }  else
          {   
            Serial.print("發(fā)現(xiàn)了");
            Serial.print(I2CDevices);
            Serial.println("個I2C設(shè)備!\n");  
          }  
        }
        
        void loop(){
        }

        Wire.endTransmission()返回0,代表這個地址通信成功,我們就認為總線上存在這個地址的設(shè)備。

        image.png


        I2C OLED

        I2C只是個通信協(xié)議,具體的還是要結(jié)合實物來演示,比如一些傳感器或者屏幕,這里我們用I2C協(xié)議的0.96寸OLED屏幕來演示下:

        image.png


        OLED使用SSD1306控制芯片,所以我們需要下載一個庫SSD1306,另外還需要配合圖形庫GFX操作,代碼中,我們先包含對應(yīng)頭文件,然后創(chuàng)建一個Adafruit_SSD1306對象,第三個參數(shù)是用的I2C對象。

        Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

        初始化時候用display.begin(SSD1306_SWITCHCAPVCC, 0x3C)初始化顯示對象,傳入地址,然后就可以自由簡單的顯示我們想要顯示的數(shù)據(jù)了。

        關(guān)于Adafruit_GFX庫,非常強大的一個圖形庫,我們后面單獨講解具體的原理,這里先了解一下即可。

        完整程序

        #include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h>#define SCREEN_WIDTH 128 // OLED display width, in pixels#define SCREEN_HEIGHT 64 // OLED display height, in pixelsAdafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);void setup() {
          Serial.begin(115200);  if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
            Serial.println(F("SSD1306 allocation failed"));    for(;;);
          }  
          delay(1000);
          display.display();
          
          display.clearDisplay();
          display.setTextColor(WHITE);
          display.setTextSize(1);
          display.setCursor(0,0);
          display.print("CHIPHOME");
          display.display();
          display.setCursor(0,8);
          display.print("12345678");
          display.display();
          delay(1000);
        }void loop() {
        
        }

        SSD1306示例代碼演示:

        image.png


        *博客內(nèi)容為網(wǎng)友個人發(fā)布,僅代表博主個人觀點,如有侵權(quán)請聯(lián)系工作人員刪除。



        關(guān)鍵詞: 協(xié)議

        相關(guān)推薦

        技術(shù)專區(qū)

        關(guān)閉
        主站蜘蛛池模板: 广河县| 潜山县| 聊城市| 八宿县| 都匀市| 永善县| 中江县| 朝阳区| 沿河| 诸暨市| 伊吾县| 宁武县| 长乐市| 吴堡县| 怀安县| 双鸭山市| 金寨县| 会同县| 福鼎市| 五莲县| 平和县| 徐州市| 梅河口市| 石家庄市| 华池县| 慈利县| 兴国县| 喀喇沁旗| 南投县| 北碚区| 新民市| 南木林县| 湖南省| 文登市| 玉树县| 东港市| 新丰县| 德保县| 岫岩| 友谊县| 仙桃市|