National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 72 24°C PM2.5 13
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 09

I2C 感測器

I2C 位址、匯流排與讀取溫度感測器
Interaction 25 minute(s)

Learning Objectives

  • 說明 I2C 用兩條線接多個裝置的方式,以及位址的用途
  • 啟用 I2C,看懂 i2cdetect 掃描出來的位址表
  • 用 smbus2 從暫存器讀出位元組,以位元移位組成 TMP102 的溫度
  • 用二補數處理負溫度,並在位址錯誤時接住 OSError

兩條線,接一整排感測器

上一課的 MCP3008 用 SPI,要四條訊號線,每多一顆晶片還要多一條片選線。 感測器一多,樹莓派的腳位很快就不夠用。

I2C(Inter-Integrated Circuit,常唸作 I-squared-C 或 I-two-C)只要兩條線,就能在同一條匯流排上接很多個裝置: 溫度感測器、顯示器、加速度計可以全部並聯在同樣的兩條線上。這一課要回答:

  1. 大家都接在同樣的兩條線上,樹莓派怎麼知道在跟誰說話?
  2. 感測器回傳的是一串位元組,怎麼變成「25.125°C」?
  3. 位址寫錯、感測器沒接好時,程式會遇到什麼?

SDA、SCL 與位址

I2C 的兩條線:

線 意思 樹莓派腳位(BCM)
SDA 資料(雙向) GPIO2(實體第 3 腳)
SCL 時脈 GPIO3(實體第 5 腳)

再加上 3.3V 與 GND,感測器模組就接好了。I2C 的線平常要靠上拉電阻拉到高電位,樹莓派在 GPIO2、GPIO3 上已經內建 1.8 kΩ 的上拉電阻,所以這兩支腳不適合拿來當一般按鈕輸入。

匯流排上由樹莓派發起每一次通訊,稱為控制端;感測器等著被點名,稱為目標裝置。每個目標裝置有一個 7 位元的位址, 通訊一開始先送出位址,只有位址相符的裝置會回應。常見的例子:

裝置 常見位址 說明
TMP102 溫度感測器 0x48 ADD0 腳接 GND;接 3.3V、SDA、SCL 分別變成 0x49、0x4A、0x4B
LCD 1602 的 I2C 轉接板(PCF8574) 0x27 使用 PCF8574A 晶片的版本是 0x3F
SSD1306 OLED 顯示器 0x3C 有些模組是 0x3D

同一條匯流排上位址不能重複;兩顆一樣的感測器要接在一起時,用 ADD0 這類腳位把其中一顆換成別的位址。

位址習慣用十六進位寫:0x48 就是十進位的 72。Python 裡寫 0x48 或 72 都一樣。

啟用 I2C 與掃描位址

和 SPI 一樣,樹莓派的 I2C 預設關閉。執行 sudo raspi-config,選 3 Interface Options → I5 I2C,回答要啟用 (或在桌面的 Control Centre 的 Interfaces 分頁打開)。接好感測器後,用 i2cdetect 掃描匯流排 1:

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

列是十位數、行是個位數(都是十六進位):第 20 列第 7 行出現 27,第 40 列第 8 行出現 48,表示匯流排上有 0x27 與 0x48 兩個裝置回應。 接線後先掃描,看到位址再寫程式,可以省下很多除錯時間:掃不到就是接線、電源或位址的問題,不是程式的問題。

在 Python 裡用 smbus2 套件操作 I2C:SMBus(1) 打開匯流排 1,再用位址與暫存器編號讀寫。

暫存器與 TMP102 的溫度格式

感測器內部有幾個小格子叫暫存器,每一格有編號。讀感測器通常是:「位址 0x48 的裝置,請給我暫存器 0x00 開始的 2 個位元組」。

data = bus.read_i2c_block_data(0x48, 0x00, 2)   # 例如 [0x18, 0x80]

TMP102 的暫存器 0x00 是溫度,用 12 個位元表示,每一單位是 0.0625°C(1/16 度)。12 個位元分在兩個位元組裡: 第一個位元組是高的 8 位元,第二個位元組的前 4 位元是剩下的低位元。組起來的方法:

data[0] = 0x18 = 0001 1000          data[0] << 4  = 0001 1000 0000
data[1] = 0x80 = 1000 0000          data[1] >> 4  =           1000
                                     兩者 OR       = 0001 1000 1000 = 0x188 = 392
392 × 0.0625 = 24.5°C

<< 是往左移(後面補 0),>> 是往右移(丟掉最右邊的位元),| 把兩邊的 1 合在一起。

逐步看:讀 TMP102

模擬的溫度在 2.5 秒內從 24.5°C 升到 26°C。看每一輪讀到的兩個位元組,怎麼一步步變成溫度。

零下怎麼表示:二補數

TMP102 也量得到零下的溫度。12 個位元裡,最高的那一位如果是 1,代表這是負數,用二補數表示: 把整數減掉 2¹²(4096)就是真正的值。

raw = (data[0] << 4) | (data[1] >> 4)
if raw >= 2048:          # 最高位元是 1
    raw = raw - 4096

忘了處理的話,冷凍庫裡的 -5.25°C 會被讀成 250.75°C。下一個範例兩種算法並列。

逐步看:冷凍庫裡的溫度

感測器放在 -5.25°C 的地方。注意第 5 行的判斷,以及沒有處理負數時印出的荒謬數字。

逐步看:位元組的順序

smbus2 還有一個 read_word_data,一次讀 2 個位元組組成一個整數。可是 SMBus 規定先收到的是低位元組, TMP102 卻是先送高位元組,所以直接用會把兩個位元組放反。比較兩種讀法的結果。

位址錯了、線鬆了:OSError

對一個沒有裝置回應的位址讀寫,smbus2 會丟出例外,Linux 上常見的訊息是:

OSError: [Errno 121] Remote I/O error

位址寫錯、SDA 與 SCL 接反、電源沒接、杜邦線鬆脫,都是同一個錯誤。長時間執行的程式(例如每分鐘記錄一次溫度) 不應該因為一次讀取失敗就整個停掉,可以用 try/except 接住,記下錯誤、等一下再試:

try:
    data = bus.read_i2c_block_data(0x48, 0x00, 2)
except OSError as e:
    print("讀取失敗:", e)    # 記下來,下一輪再試

讀取經常失敗時,先用 i2cdetect -y 1 確認位址還在,再檢查接線;不要只在程式裡一直重試。

互動練習:溫度太高就開風扇

TMP102 接在 I2C(0x48),GPIO23 接一顆代表風扇的指示燈。「輸入設定」的滑桿決定模擬期間溫度從幾度變到幾度。依序完成任務:

  1. 直接執行起始程式,看輸出的溫度。
  2. 改程式:溫度高於 28°C 就打開風扇指示燈,否則關掉。
  3. 把溫度改成從 35°C 降到 20°C,再執行一次:風扇要先開、後關。
  4. 回答一個問題。

小測驗:位址與接線

1.

樹莓派的 I2C 匯流排 1 用哪兩支腳位?

2.

i2cdetect 的表格在第 40 列、第 8 行出現 48,程式裡要用哪個位址?

3.

同一條 I2C 匯流排上接兩顆 ADD0 都接地的 TMP102,會怎樣?

第 1 次作答

小測驗:位元組與溫度

1.

下面的程式會印出什麼?

data = [0x19, 0x20]
raw = (data[0] << 4) | (data[1] >> 4)
print(raw * 0.0625)
2.

下面的程式會印出什麼?

raw = 4032
if raw >= 2048:
    raw = raw - 4096
print(raw * 0.0625)
3.

用 smbus2 讀一個沒有接裝置的位址,常見的錯誤是什麼?

第 1 次作答

重點整理

  • I2C 用 SDA(GPIO2)與 SCL(GPIO3)兩條線,同一條匯流排可以接多個裝置,靠 7 位元位址分辨;位址不能重複。
  • 先用 raspi-config 啟用 I2C,接線後用 sudo i2cdetect -y 1 掃描,確認位址再寫程式。
  • smbus2:SMBus(1) 打開匯流排,read_i2c_block_data(位址, 暫存器, 個數) 讀出位元組。
  • TMP102:暫存器 0x00 是 12 位元溫度,(data[0] << 4) | (data[1] >> 4) 組成整數,乘 0.0625°C;最高位元是 1 時減 4096(二補數)。
  • 位址錯誤或接線鬆脫會得到 OSError(errno 121),用 try/except 接住,讓程式不會整個停掉。

下一課把讀到的數字顯示出來:同樣接在 I2C 上的 16x2 LCD。

這一課完成了!
Next lesson:顯示器:LCD 與 OLED
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。