【.NET 與樹莓派】i2c(IIC)通訊

東邪獨孤發表於2021-01-31

i2c(或IIC)協議使用兩根線進行通訊(不包括電源正負極),它們分別為:

1、SDA:資料線,IIC 協議允許在單根資料線上進行雙向通訊——這條線既可以傳送資料,也可以接收資料。

2、SCL:時鐘線,注意了,這個時鐘線跟我們平時所說的時鐘沒什麼關係,不要以為這根線是用來接手錶的。其實,這裡所說的“時鐘”,更像是我們看音樂會的時候,站在前面最中央處的那個指揮者,或者說節拍器。它的作用就是協調硬體之間的傳輸節奏,做到步伐一致,不然資料就會亂了。比如,IIC通訊裡面,當時鍾線的電平拉高後,資料線的內容就不能改變,也就是說,SCL高電平時,不能寫資料,但可以讀。當SCL下降為低電平後,才能向資料線(SDA)寫入資料。

IIC 通訊以 Start 訊號開始,以 Stop 訊號結束。

傳送開始訊號的方法:拉高SCL和SDA的電平,在SCL處於高電平的情況下把SDA的電平拉低。

傳送結束訊號的方法:拉高SCL的電平,在SCL處於高電平的情況下,把SDA的電平拉高。

這其中,你會發現規律:無論是開始訊號還是結束訊號,SCL 都處於高電平,前文提過,時鐘線拉高就是固定資料線上的內容,顯然,在開始和結束訊號中,是不能傳資料的。在SDA上,開始訊號和結束訊號剛好相反,Start 時電平拉低,Stop 時電平拉高。下面這張圖是從 IIC 的協議手冊上盜來的。

【.NET 與樹莓派】i2c(IIC)通訊

 

 

寫入資料時,主機先把時鐘線SCL拉低,然後寫入一個二進位制位(高電平為1,低電平為0),然後把SCL拉高,此時從機讀取這個二進位制位。接著第二個二進位制位也是這樣,主機拉低SCL,寫SDA,再拉高SCL,從機讀……當傳送完 8 個二進位制(一個位元組)後,在第九個時鐘週期,主機把SDA拉高(有時候需要切換為輸入模式),再拉高SCL,等待從機寫應答;如果主機從SDA上讀到低電平,表示從機有應答(你的紅包我收到了),要是讀到高電平,表示無應答(你啥時候發的紅包?我都沒看到)。

從機向主機傳送資料的過程也一樣,SCL仍然由主機操控,SCL拉低後向SDA寫資料,SCL拉高後就不能寫了,此時主機讀SDA上的資料。通常主機在接收完最後一個位元組後可以不應答(讓SCL和SDA同時高電平),或直接傳送 Stop 訊號終止通訊(畢竟主機權力大,生死予奪都是主機說了算)。

上面的東東看得好像很亂,剛接觸時就是這樣的,見多了就熟悉了。可以大概地總結一下:

1、SCL低電平時,傳送方寫SDA;

2、SCL高電平鎖定SDA,傳送方不能寫,接收方讀;

3、應答訊號:SCL高 + SDA低---> 有應答;SCL高 + SDA高---> 無應答。

 

其實,我們實際開發中,不瞭解協議時序也沒關係,我們也很少手動去模擬 IIC 通訊過程。尤其是像樹莓派這種帶作業系統的開發板,更不應該手動去模擬,而是直接用現成的庫(或者API)。不管你什麼語言,你都是先向系統傳送指令,然後系統去控制硬體,效率上都無法保證。而且,IIC 協議都是標準化的協議,你每次寫程式都去手動模擬通訊,浪費時間,意義也不大。這好比我們在 Socket 程式設計時一樣,你不可能總去自己寫個協議再來通訊吧。一般都會直接用 TCP 或 UDP 協議。

所以,對於IIC協議也是如此,我們瞭解一下就行了。老週上面在介紹時也是簡略化的,所以你可能看得有點暈,若想深入理解,可以看資料手冊。畢竟老周不可能把手冊上的內容複製過來的,那就是抄襲了。

好,繼續。

IIC 匯流排可以掛多個從機,從機不會主動發起通訊,都是由主機發起通訊的。因此,主機必須知道要跟哪個從機通訊,故掛到匯流排上的從機必須擁有唯一的地址——這就是所謂的器件地址。就像一個內網中的 N 臺電腦一樣,每臺電腦都要給它分配唯一的 IP 地址,這樣你才能知道你正在跟誰說話。哪怕是 UDP 廣播,也是有廣播地址,192.168.1.255。

IIC 器件地址,7位地址最常見,當然也有 10 位的(老周買的各種模組中都沒見到),這個【位】是二進位制位,常用的 7 位就是7個二進位制位。7 位地址格式如下:

 

 低位在右邊,從右到左,我們看到第 1 位是 R/W,表示讀寫位,就是用來告訴從機,我要讀資料還是寫資料。“W”頭頂上有個橫線,表示低電平,即 0 表示寫,1 表示讀。從第二位到第八位就是從機的地址了。所以,現在你知道為啥地址是7位的原因了吧,就是要留一位來確定讀還是寫。

假如某品牌的自動鏟屎機使用 IIC 通訊協議,標籤上告訴你它的從機地址是 0x47,先把它弄成二進位制。

0100 0111

第八位是0,所以有效的值是第一位到第七位,屬7位地址。當主機要向鏟屎機發起通訊時,需要把地址左移一位,變成:

1000 1110

左移後,第二到第七位表示器件地址,就能空出第一位用來放讀寫標誌了。如果要寫資料,就向從機發 1000 1110;要讀資料,就向從機發 1000 1111。

注意,我們在呼叫庫的時候,是不需要左移的,比如我們.NET中用的 System.Device.Gpio 庫,內部會自動進行左移。

 

好了,基礎知識就介紹到這兒,相信你對 IIC 協議已經有大概的瞭解,下面我們們來看看 System.Device.Gpio 給我們準備了哪些類。

A、名稱空間:System.Device.I2c

B、I2cConnectionSettings 類,用來配置 IIC 通訊的必要引數。其實就兩個:第一個是匯流排ID,一般系統預設的是 1。第二個引數就是從機的地址(不需要左移)。

C、I2cDevice,核心類,用於讀寫資料。這是個抽象類,內部根據不同的系統有各自的實現版本,但我們在呼叫時不用關心是哪個版本。

D、I2cBus,這個一般可以不用,如果硬體上有多個匯流排,可以使用這個類指定使用哪個匯流排。其實樹莓派有兩路 i2c 匯流排的,我們平時用的是 i2c-1,還有一個 i2c-0 是隱藏的,留給攝像頭用的,可以參考官方文件。

 

        i2c_arm                 Set to "on" to enable the ARM's i2c interface
                                (default "off")

        i2c_vc                  Set to "on" to enable the i2c interface
                                usually reserved for the VideoCore processor
                                (default "off")

        i2c                     An alias for i2c_arm

 

“i2c”和“i2c-arm”是同一個東東,只是名字不同罷了,所以,一塊板子上就有 “i2c-arm”和“i2c-vc” 兩路匯流排,“i2c-vc”分配給攝像頭以及視訊相關的介面使用。當然,你也可以拿“i2c-vc”作為常規匯流排用的,要把視訊相關的介面禁用。如果兩路都拿來用了,那麼樹莓派上就有兩個匯流排ID,一個是 0,一個是 1。

另外,也可以使用軟體模擬 i2c,這樣你就可以弄出幾個匯流排出來了——i2c-2、i2c-3、i2c-150 …… 配置如下:

Name:   i2c-gpio
Info:   Adds support for software i2c controller on gpio pins
Load:   dtoverlay=i2c-gpio,<param>=<val>
Params: i2c_gpio_sda            GPIO used for I2C data (default "23")

        i2c_gpio_scl            GPIO used for I2C clock (default "24")

        i2c_gpio_delay_us       Clock delay in microseconds
                                (default "2" = ~100kHz)

        bus                     Set to a unique, non-zero value if wanting
                                multiple i2c-gpio busses. If set, will be used
                                as the preferred bus number (/dev/i2c-<n>). If
                                not set, the default value is 0, but the bus
                                number will be dynamically assigned - probably
                                3.

這個只是提一下,必要時可以用上,軟體模擬的介面通訊,效能和效率會相對差一點的。

 

樹莓派預設是不開啟 i2c 介面的,所以要在配置中將其開啟。

sudo raspi-config

找到介面選項。

【.NET 與樹莓派】i2c(IIC)通訊

 

 選擇 P5 I2C 條目。

【.NET 與樹莓派】i2c(IIC)通訊

 

 然後選擇“YES”。

【.NET 與樹莓派】i2c(IIC)通訊

 

或者簡單粗暴,修改 /boot/config.txt,加上這一行:

dtparam=i2c_arm=on

儲存退出。

 

這一次的 IIC 演示例項,老周不使用感測器。主要擔心有同學會誤解,因為很多電子模組/感測器都是通過讀寫暫存器的方式來控制的,於是有同學會以為 IIC 是操作寄存來傳遞資訊的。其實不然,跟 TCP 協議一樣,你可以用 IIC 傳遞任何位元組,只要能用二進位制表示的就沒問題了。

本例老周用一塊 Arduino (讀音:阿嘟伊諾,重音在後面,“伊諾”要讀出來,別讀什麼“阿丟諾”)開發板做為 IIC 從機,型號為  Uno R3(讀音:烏諾,義大利語“第一”的意思,表明這是 Arduino 的首套板子)。然後用樹莓派作為主機,來控制 Arduino。

Arduino 上使用 Wire 庫進行 IIC 通訊。首先要包含 Wire.h 標頭檔案。

#include <Wire.h>

在這個標頭檔案中,注意有這麼一行。

extern TwoWire Wire;

其實標頭檔案中宣告的封裝類名為 TowWire,然後在標頭檔案中用這個類宣告瞭一個變數 Wire,加上 extern 關鍵字使得其他程式碼能訪問到它,只要 include 這個標頭檔案就OK了。Wire 變數的賦值程式碼在 Wire.cpp 檔案中(提前給你例項化一個物件了)。

TwoWire Wire = TwoWire();

這樣佈局程式碼的好處在於:包含 Wire.h 檔案後,你馬上就能用了,直接就可以通過 Wire 變數呼叫 TwoWire 的公共成員了。

Arduino 程式碼一般有兩個特定的函式:

setup:初始化一些設定,比如某某引腳設定為輸出模式。此函式會在程式在燒進板子上時執行一次,然後就不會執行,進入 loop 函式死迴圈。但是,如果你按了復位按鈕,或者斷電了重新上電,就會執行 setup 函式。

loop:這個函式被放在一個 die 迴圈裡,它會無限期地被呼叫,只要程式被燒進開發板上就會永遠地迴圈。

有同學會問:C/C++不是有入口點嗎,main 函式滾哪裡去了?main 函式在 main.cpp 檔案中,編譯時由 Arduino 編譯器自動連結。

int main(void)
{
    ……
    
    setup();
    
    for (;;) {
        loop();
        if (serialEventRun) serialEventRun();
    }
        
    return 0;
}

從入口點函式的邏輯中也看到,setup 函式只呼叫了一次,然後 loop 函式死迴圈。

好了,題外話結束,下面我們們回到 Arduino 的專案中,在setup函式中呼叫 Wire.begin 方法,開始 IIC 通訊。

void setup()
{
    // 該從機的地址是 0x15
    Wire.begin(0x15);
    // 註冊函式,當收到主機資料時呼叫
    Wire.onReceive(onRecData);
    // 註冊函式,當主機請求資料時呼叫
    Wire.onRequest(onRequestData);
}

如果 Arduino 作為 IIC 主機,呼叫 begin 方法時不需要指定地址;此例中 Arduino 充當從機,所以要指定從機地址 0x15(你可以改為其他地址,一般用7位)。樹莓派上的應用會使用地址 0x15 來找到這塊 Uno 板子。

注意這兩行:

    Wire.onReceive(onRecData);
    Wire.onRequest(onRequestData);

這兩個方法的引數都是指向一個函式的指標,傳遞時直接寫函式名即可。onRecieve 方法註冊一個函式,當收到主機發來的資料時呼叫這個函式;onRepuest 方法註冊一個函式,當主機希望從機傳送資料時呼叫這個函式。

onRecData 和 onRequestData 函式定義如下:

void onRecData(int count)
{
    if (Wire.available())
    {
        // 讀一個位元組
        readData = Wire.read();
    }
}

void onRequestData(void)
{
    // 向主機發資料
    Wire.write(sendData);
}

在這個示例中,主機只向從機發一個位元組,所以引數 count 可以忽略,直接呼叫 Wire.read 讀一個位元組,並儲存在變數 readData 中;傳送資料時呼叫 Wire.write 方法將 sendData 中的內容傳送給主機。在loop迴圈中,根據readData的值生成sendData的內容——根據主機發的命令生成回覆訊息。

void loop()
{
    // 根據主機傳來的資料設定要發給主機的資料
    switch (readData)
    {
    case 1:
        strcpy(sendData, "SB");
        break;
    case 2:
        strcpy(sendData, "NB");
        break;
    case 3:
        strcpy(sendData, "XB");
        break;
    default:
        strcpy(sendData, "SB");
        break;
    }
}

完整程式碼結構如下;

#include <Wire.h>

// 預宣告函式
void onRecData(int);
void onRequestData(void);

// 從主機讀到的資料
uint8_t readData = 0;

// 要發給主機的資料
// 兩個字元 + \0,所以是3位元組
// 但這裡不需要 \0
char sendData[2] = { };

void setup()
{
    // 該從機的地址是 0x15
    Wire.begin(0x15);
    // 註冊函式,當收到主機資料時呼叫
    Wire.onReceive(onRecData);
    // 註冊函式,當主機請求資料時呼叫
    Wire.onRequest(onRequestData);
}

void loop()
{
    ……
}

void onRecData(int count)
{
    if (Wire.available())
    {
        // 讀一個位元組
        readData = Wire.read();
    }
}

void onRequestData(void)
{
    // 向主機發資料
    Wire.write(sendData);
}

 

接下來編寫樹莓派上的應用。

dotnet new console -n Myapp -o .

上面命令建立新的控制檯專案,名為Myapp,存放在當前目錄下。

新增 System.Device.Gpio 包的引用。

dotnet add package System.Device.Gpio

前文提到過,預設啟用的 IIC 匯流排是 i2c-1,所以例項化 I2cConnectionSettings 時,Bus ID 是1,從機地址是 0x15。

    I2cConnectionSettings settings = new(1, 0x15);

隨後獲取 I2cDevice 物件。

    I2cDevice device = I2cDevice.Create(settings);

本例的邏輯為:由使用者從鍵盤輸入數字(1、2、3),然後把這個數字發給從機(Arduino 板子),然後讀取從機回覆的資料。

            byte input = 0; //讀取鍵盤輸入
            Console.WriteLine("現在開始,輸入 end 可退出");
            while (true)
            {
                Console.Write("請輸入:");
                string sl = Console.ReadLine();
                if (sl.Equals("end", StringComparison.InvariantCultureIgnoreCase))
                {
                    break;
                }
                // 將輸入內容轉為byte
                if (!byte.TryParse(sl, out input))
                {
                    input = 0;
                }
                /*
                //傳送資料
                device.WriteByte(input);
                Thread.Sleep(3);
                // 接收從機發來的資料
                Span<byte> buffer = stackalloc byte[3];
                device.Read(buffer);
                */
                // 可以一步到位,寫完就讀
                byte[] sendBuf = new byte[] { input };
                byte[] recvBuf = new byte[2];
                device.WriteRead(sendBuf, recvBuf);
                string sr = Encoding.Default.GetString(recvBuf);
                Console.WriteLine("接收到的資料:{0}", sr);
            }
            device.Dispose();

可以呼叫 WriteXXX 類似方法寫入要傳送的資料,呼叫 ReadXXX 類似的方法讀入接收到的資料。也可以用 WriteRead 方法,寫入資料後接收資料,一步完成。

 

接線方法:樹莓派預設的 IIC 引腳為 GPIO 2和3,即板子上的3、5腳;Arduino 的 SDA 引腳為 A4,SCL引腳為 A5(A4和A5為模擬量讀入口,可重用為 IIC 介面),其實 Arduino 還有一路 IIC 介面,位於數字引腳 D13 、GND、AREF後面,就是這裡:

【.NET 與樹莓派】i2c(IIC)通訊

【.NET 與樹莓派】i2c(IIC)通訊

 

 

 所以,接線圖如下:

【.NET 與樹莓派】i2c(IIC)通訊

也就是,樹莓派的 GPIO 2 接 Arduino 的 A4,樹莓派的 GPIO 3 接 Arduino 的 A5。另外,還要把兩個板子的 GND 連起來(共地),雖然不共地也能通訊,但可能存在被干擾的情況,共地後使用低電平的“0V”有了統一的參考標準,這樣傳遞訊號準確更高。

如果 Arduino 開發板沒有獨立供電,可以把樹莓派的 5V 與 Arduino 的 VIN 連線起來,用樹莓派給 Arduino 供電(VIN的輸入電壓不能高於 5.5V,因為這個引腳沒有保護措施,過壓會炸板子)。

【.NET 與樹莓派】i2c(IIC)通訊

 

編譯 .NET 應用並上傳到樹莓派,然後執行,輸入不同數字,Arduino 會回覆對應的訊息。

【.NET 與樹莓派】i2c(IIC)通訊

 

好了,完工,示例程式碼請點選這裡下載。

有人會問,樹莓派有沒有山寨版?有,比如橙子派什麼的,某寶上還有荔枝派。這些板子大多數不貴,但是不太敢買,還是買原裝的好一些。 Arduino 是開源板子,版本也很多(也有山寨的),像 DFRobot 好像也可以,還有很多十幾塊的沒名字的,所以也叫不出什麼版本,只能說山寨了。不過說實話,還是原裝的執行穩定,儘管貴一些。老周當初也是買了幾塊那種十幾塊的,上傳程式經常出錯,裝驅動也頭疼。原版的穩定,起碼用到現在也出過錯,也不用找驅動,Windows 能識別。

所以說嘛,一分價錢一分貨,後來老周乾脆發點血買原裝版本的。

 

相關文章