iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 6
1
IoT

Modern Maker : 從那些 Maker 的大小事看 Linux 核心系列 第 6

Day 6:I2C (Part 1) - 簡介與環境設置

  • 分享至 

  • xImage
  •  

關於 Linux 的 I2C 子系統,在 2020 年的 ELCE 上有一個 Tutorial: Introduction to I2C and SPI: Both In-kernel and In-userspace 的演講,裡面大致介紹了 I2C 的協定,以及核心當中 I2C 子系統的架構。而這個演講中的 I2C 部分,跟 2018 年 Southern California Linux Expo 中,一個名為 Linux, I2C, and me 的演講中幾乎一樣 (至少投影片根本一樣)。雖然音質頗差,但內容比前者更詳細。

為了展示 Linux 在使用 I2C 時發生了什麼事,接下來的實驗將使用一個 Raspberry Pi 作為 master,以及一個 Arduino UNO 作為 slave,並且從 Raspberry Pi 以 I2C 傳輸資料給 Arduino UNO 時,觀察使用了哪些函數,並且搭配核心的文件進行參考。

I2C Protocol

I2C 是 Inter-Intergrated Circuit 的縮寫,最早是 Philips 在 1982 年開發的,而現在他則是 NXP 的一部分。所以正式的規格可以在 NXP 的 I2C-bus specification and user manual 文件上找到。

物理層簡介

I2C 需要兩條線路傳輸,一個是 SDA (傳輸資料),另外一個是 SCL (提供時脈)。支援 nulti-master multi-slave (不過大部分的狀況下都是只有一個 master),並且最多可以連接 127 個裝置。在一開始的規格中,每個裝置都會有一個 7 位元的「位址」。

除了 I2C 之外,根據 Tutorial: Introduction to I2C and SPI: Both In-kernel and In-userspace 中提到:另外一個類似的協定叫做 SMBus,是一個 I2C 協定的子集,所以也常常在 I2C 的子系統中出現。

傳輸協定

(來源:維基百科)

I2C 當中,開始傳輸前,SDA 與 SCL 一開始都處於高電位。要開始傳輸時:

  1. 進入 start condition (S):master 把 SDA 拉低。如果 SDA 拉低時,SCL 仍然持平在高電位,這個狀態稱作 start condition (S)。隨後 SCL 跟著拉低,並且準備開始傳輸。

  2. SDA 傳輸資料。資料的傳輸則是跟著 SCL 的時脈走:當 SCL 為高電位時,傳輸 SDA 當時的高低狀態。而這傳輸的資料中,又大致上分成兩個階段:

    1. 傳送 7 位元位址:master 在 start condition 之後,會傳一個 7 位元的資訊。這 7 個位元表示要讀或寫哪個 slave (也就是 slave 的位址)。
    2. 傳送第 8 位元表示讀或寫:7 個位元的位址之後,接著傳輸地 8 個位元。這個位元決定的是 master 要「讀」或是「寫」。
    3. Slave ACK 一次確認:傳送玩 8 個位元之後,收到的 Slave 就要 舉手喊有 ACK 一次。ACK 的方法是 Slave 要把 SDA 拉低。
    4. 傳輸資料:之後就開始依照時脈傳輸資料。如果是 master 要寫,那就是 slave 依照時脈讀讀取 SDA; 如果是 master 要讀,那就是 slave 透過 SDA 搭配時脈讀取 SDA 的高低電壓。除此之外,每 8 個位元會傳輸一個 ACK。如果是 master 寫,那麼就是接收資料的 slave 要 ACK 說有收到; 如果是 master 從 slave 讀東西,那就是 master 要 ACT 說有收到。
    5. 資料結束:如果是 master 寫,那麼其實 slave 沒有辦法選擇自己要不要停,只能逆來順受被 master 餵資料,直到最後傳完 ACK 才能停; 而如果是 master 讀,當 master 不需要資料時,傳一個 NA 給 slave 知會一聲就可以了。
  3. 進入 stop condition:SCL 先拉高,隨後 SDL 跟著拉高。

所以,如果用 [] 表示從 Slave 傳輸的資料,那麼 master 寫時,大概像這樣:

S Addr W [A] Data0 [A] Data1 [A] ... [A] dataN [A] P

相反地,若是 master 讀的時候,狀態順序大概像下面這樣:

S Addr R [A] [Data0] A [Data1] A ... A [dataN] NA P

Step 1:Arduino 上的 I2C (硬體)

前面知道:I2C 需要 SDA 與 SCL 兩條線來傳輸資訊,而根據 Arduino 官方的文件 可以查到如下資料:

Board	I2C / TWI pins
Uno, Ethernet	A4 (SDA), A5 (SCL)
Mega2560	    20 (SDA), 21 (SCL)
Leonardo	    2 (SDA), 3 (SCL)
Due	            20 (SDA), 21 (SCL), SDA1, SCL1

因此,就 Arduino UNO 而言,若想要使用 I2C,需要使用 A4 為 SDA,A5 為 SCL。

Step 2:Arduino 上的 I2C (軟體)

程式方面,Arduino IDE 內建的 Wire.h 函式庫中,可以使用 I2C。用法可以參考官方網站的 Master Writer/Slave Receiver 例子略知一二。以下程式修改自其中:

#include <Wire.h>
#define ARDUINO_ADDR 0x8
char buf[128];

void setup()
{
    Wire.begin(ARDUINO_ADDR);
    Wire.onReceive(receiveEvent);
    Serial.begin(9600);
}

void loop()
{
    delay(100);
}

void receiveEvent(int nbyte)
{
    buf[nbyte] = 0;
    for (int i = 0; Wire.available(); i++)
        buf[i] = Wire.read();
    Serial.println(buf);
}

Step 3:Raspberry Pi 上的 I2C (硬體)

pinout.xyz 可以知道 Raspberry Pi 中,GPIO2 可以作為 SDA; GPIO3 可以作為 SCL。所以,把 GPIO2 跟 Arduino 的 A4 連接; 把 GPIO3 跟 Arduino 的 A5 連接。最後,再把地線接在一起,就完成硬體的設置了。

(來源:pinout.xyz)

Step 4:啟動 Raspberry Pi 上的 I2C

首先,要先用 raspi-config 來啟動 I2C。這個在 Interfacing Options 中的 I2C 選項中:

Step 5:Raspberry Pi 上的 I2C (軟體)

就安裝 Raspberry Pi OS 而言,畢竟是個 Linux,所以基本的 I2C 資訊就可以從核心文件中的 I2C/SMBus Subsystem 來得到。比如說在 Implementing I2C device drivers in userspace 的章節中,就有出現如何在 User Space 去使用 I2C。

而除了這個之外,也可以在 python 使用 smbus 這個套件。可以用以下方式安裝:

$ sudo apt-get install python3-smbus

安裝這個東西會自帶 i2c-tools,比如說可以用 i2cdetect 這個工具去看看哪一條 bus 上面的哪個位置有 i2c 裝置:

$ i2cdetect -y 1

在什麼都沒有時,就會像下面這個樣子:

$ i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --   

接下來就開始寫 Raspberry Pi 端的 master 吧:

from smbus import *

ARDUINO_ADDR = 0x8
I2C_BUS_NO = 1

i2c_bus = SMBus(I2C_BUS_NO)

while 1:
    try:
        message = input("Message to be send: ")
    except:
        break
    for a in [ord(c) for c in message]:
        i2c_bus.write_byte(ARDUINO_ADDR, a)

Step 5:連接裝置

將程式上傳到 Arduino 中,並且將 Raspberry Pi 的 A4 接到 GPIO2A5 接到 GPIO3,並且連接。最後把傳輸線插到電腦並打開序列埠,觀察 I2C 的輸出。

在連接完之後,使用 i2cdetect

$ i2cdetect -y 1

就可以發現剛剛指定的 Arduino I2C 的位置 (0x8) 出現在輸出上:

$ i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- 08 -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --   

順帶一提一件有趣的事情是:雖然這邊的連接都是 I2C,但可以找到的範例程式幾乎都是 SMBus 的例子,比如說使用 python3-smbus 這個函式庫。這個或許可以從核心文件的 Implementing I2C device driversSending and receiving 章節中找到為什麼:

If you can choose between plain I2C communication and SMBus level communication, please use the latter. All adapters understand SMBus level commands, but only some of them understand plain I2C!


上一篇
Day 5:ply 語法簡介
下一篇
Day 7:I2C (Part 2) - ftrace 爽颯登場!
系列文
Modern Maker : 從那些 Maker 的大小事看 Linux 核心30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言