ESP32 I2C 通訊是嵌入式開發中最實用的技能之一。I2C(Inter-Integrated Circuit)只需兩條線(SDA、SCL)就能連接多個感測器,BME280 感測器教學就是最經典的入門範例——它可以同時讀取溫度、濕度和氣壓資料。本文從 I2C 時序圖解讀開始,帶你一步步實作。
本篇以 ESP32 + BME280(溫濕度氣壓感測器)為例,從 I2C 協定原理、時序圖解讀、到完整程式碼實作,帶你一次搞懂 I2C 通訊。
目錄
- 一、I2C 協定基礎
- 二、I2C 時序圖完全解讀
- 三、BME280 感測器介紹
- 四、硬體接線
- 五、ESP32 程式碼(Adafruit 函式庫)
- 六、程式碼解讀:從原始暫存器操作到高階 API
- 七、I2C 偵錯工具
- 八、常見問題與排錯
一、I2C 協定基礎
I2C 由 Philips(現 NXP)在 1982 年發明,設計目標是用最少的引腳完成裝置間通訊:
| 訊號線 | 全名 | 說明 |
|---|---|---|
| SDA | Serial Data | 雙向資料線 |
| SCL | Serial Clock | 時脈線,由 Master 產生 |
I2C vs SPI 對比
| 特性 | I2C | SPI |
|---|---|---|
| 引腳數 | 2(SDA + SCL) | 4+(SCLK + MOSI + MISO + 每裝置一條 CS) |
| 傳輸模式 | 半雙工 | 全雙工 |
| 速度 | 標準 100KHz,快速 400KHz,高速 3.4MHz | 最高數十 MHz |
| 多裝置 | 匯流排架構,最多 127 個裝置 | 每個裝置需要獨立 CS |
| 通訊方式 | 裝置有 7-bit 位址,Master 定址 | 透過 CS 片選 |
| 硬體需求 | 需要上拉電阻 | 不需要上拉電阻 |
什麼時候用 I2C? 當你需要接多個感測器、引腳有限、速度要求不高的時候。反之,如果你需要高速傳輸大量資料(如相機、顯示器、Flash 記憶體),SPI 更適合。
二、I2C 時序圖完全解讀
以下是用 Python + WaveDrom 產生的 I2C 基本時序圖:
I2C 通訊有五個關鍵階段:
1. Start Condition(起始條件)
當 SCL 為高電位時,SDA 從高電位切換到低電位。這個邊緣變化告訴所有匯流排上的裝置:「準備開始通訊」。所有 I2C 指令都以 Start Condition 開頭。
2. 傳送從機位址(7-bit + R/W)
Master 送出 7-bit 從機位址 + 1 bit 讀寫標誌(0=寫,1=讀)。例如 BME280 的位址是 0x76(7-bit),加上寫標誌變成 0xEC。每個時脈週期傳送一個 bit,高位元(MSB)先傳。
3. ACK/NACK(應答)
第 9 個時脈週期由接收方控制。如果接收方將 SDA 拉低,表示 ACK(收到);如果 SDA 維持高電位,表示 NACK(未收到或結束)。
4. 資料傳輸
每 8 個 bit 資料後跟著 1 個 ACK bit。資料在 SCL 上升緣時被採樣,在下降緣時變化。Master 負責產生所有時脈訊號。
5. Stop Condition(停止條件)
當 SCL 為高電位時,SDA 從低電位切換到高電位,表示通訊結束。
三、BME280 感測器介紹
BME280 是 Bosch 生產的整合式環境感測器,一顆晶片同時測量三種數據:
| 參數 | 範圍 | 精度 | 解析度 |
|---|---|---|---|
| 溫度 | -40°C ~ +85°C | ±0.5°C | 0.01°C |
| 濕度 | 0% ~ 100% | ±3% | 0.008% |
| 氣壓 | 300 ~ 1100 hPa | ±1 hPa | 0.18 Pa |
BME280 的 I2C 位址有兩種:
- 0x76(預設)— 當 SDO 接 GND 時
- 0x77 — 當 SDO 接 VCC 時
大部分模組預設是 0x76,如果你買的是 breakout 板,通常不需要改。
四、硬體接線
ESP32 的 I2C 引腳可以任意指定(使用 Wire.begin() 時設定),但慣例上:
| ESP32 | BME280 | 說明 |
|---|---|---|
| 3.3V | VCC | 電源 |
| GND | GND | 接地 |
| GPIO21 | SDA | I2C 資料線 |
| GPIO22 | SCL | I2C 時脈線 |
⚠️ 關鍵:I2C 匯流排必須有上拉電阻!
SDA 和 SCL 都需要分別接到 VCC(3.3V)的上拉電阻,典型值 4.7kΩ。如果你買的是 BME280 模組(如 GY-BME280),模組上已經內建了上拉電阻,直接接線即可。
如果你是在麵包板上自己搭建,記得加上 4.7kΩ 電阻,否則 I2C 無法正常通訊。
五、ESP32 程式碼(Adafruit 函式庫)
在 Arduino IDE 中,先安裝以下函式庫(程式庫管理員搜尋):
- Adafruit Unified Sensor
- Adafruit BME280 Library
以下完整程式碼,ESP32 讀取 BME280 資料並透過 Serial 輸出:
#include <Wire.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#define SEALEVELPRESSURE_HPA (1013.25)
Adafruit_BME280 bme;
unsigned long delayTime;
void setup() {
Serial.begin(115200);
while (!Serial) delay(10);
Serial.println(F("BME280 測試開始"));
// 初始化 I2C(指定引腳)
Wire.begin(21, 22); // SDA=GPIO21, SCL=GPIO22
// 初始化 BME280(位址 0x76)
if (!bme.begin(0x76, &Wire)) {
Serial.println(F("找不到 BME280 感測器!"));
Serial.println(F("請檢查:"));
Serial.println(F(" 1. 接線是否正確"));
Serial.println(F(" 2. 上拉電阻是否安裝"));
Serial.println(F(" 3. I2C 位址是否為 0x76"));
while (1) delay(10);
}
Serial.println(F("BME280 初始化成功"));
Serial.println();
delayTime = 1000;
}
void loop() {
printValues();
delay(delayTime);
}
void printValues() {
// 讀取三種資料
float temperature = bme.readTemperature();
float humidity = bme.readHumidity();
float pressure = bme.readPressure() / 100.0F;
float altitude = bme.readAltitude(SEALEVELPRESSURE_HPA);
// 顯示
Serial.print("溫度 = ");
Serial.print(temperature, 2);
Serial.println(" °C");
Serial.print("濕度 = ");
Serial.print(humidity, 2);
Serial.println(" %");
Serial.print("氣壓 = ");
Serial.print(pressure, 2);
Serial.println(" hPa");
Serial.print("海拔 ≈ ");
Serial.print(altitude, 2);
Serial.println(" m");
Serial.println("-------------------");
}
六、程式碼解讀:從原始暫存器到高階 API
Adafruit BME280 函式庫封裝了所有底層操作,但了解底層有助於除錯。BME280 的溫度資料存放在以下暫存器:
| 暫存器 | 位址 | 說明 |
|---|---|---|
| hum_lsb | 0xFE | 濕度低位元組 |
| hum_msb | 0xFD | 濕度高位元組 |
| temp_xlsb | 0xFC | 溫度最低位元組 |
| temp_lsb | 0xFB | 溫度低位元組 |
| temp_msb | 0xFA | 溫度高位元組 |
| press_xlsb | 0xF9 | 氣壓最低位元組 |
| press_lsb | 0xF8 | 氣壓低位元組 |
| press_msb | 0xF7 | 氣壓高位元組 |
| config | 0xF5 | 配置暫存器 |
| ctrl_meas | 0xF4 | 控制測量暫存器 |
| hum_ctrl | 0xF2 | 濕度控制暫存器 |
| id | 0xD0 | 晶片 ID(應為 0x60) |
| calib00..calib41 | 0x88..0xB0 | 校正參數(非常重要!) |
讀取流程如下:
如果要用原始 I2C 操作讀取溫度,程式碼會像這樣:
#include <Wire.h>
#define BME280_ADDR 0x76
#define TEMP_MSB 0xFA
void readBME280() {
Wire.beginTransmission(BME280_ADDR);
Wire.write(TEMP_MSB); // 指定要讀取的暫存器
Wire.endTransmission(false); // 不發送 Stop(Repeated Start)
Wire.requestFrom(BME280_ADDR, 3); // 讀 3 個 bytes
if (Wire.available() >= 3) {
uint32_t msb = Wire.read();
uint32_t lsb = Wire.read();
uint32_t xlsb = Wire.read();
// 組合 20-bit 原始值
uint32_t raw_temp = (msb << 12) | (lsb << 4) | (xlsb >> 4);
Serial.print("原始溫度值 = ");
Serial.println(raw_temp);
}
}
不過實際轉換成攝氏需要套用 BME280 的校正公式(需要讀取 calib00~calib41 共 42 bytes 的校正參數),這就是為什麼使用 Adafruit 函式庫比自己從頭寫方便很多——它已經幫你處理好所有數學。
七、I2C 偵錯工具
當 I2C 通訊有問題時,以下是有效的偵錯方法:
1. I2C 掃描(I2C Scanner)
這是最基本的診斷工具——掃描匯流排上所有 I2C 裝置:
#include <Wire.h>
void setup() {
Serial.begin(115200);
Wire.begin(21, 22);
Serial.println("I2C Scanner 啟動");
}
void loop() {
byte error, address;
int nDevices = 0;
Serial.println("掃描 I2C 匯流排...");
for (address = 1; address < 127; address++) {
Wire.beginTransmission(address);
error = Wire.endTransmission();
if (error == 0) {
Serial.printf("發現裝置,位址: 0x%02X\r\n", address);
nDevices++;
}
}
if (nDevices == 0) {
Serial.println("找不到任何 I2C 裝置!");
}
delay(5000);
}
2. 用邏輯分析儀抓波形
如果掃描不到裝置,接上邏輯分析儀(如 Saleae)看 SDA/SCL 波形:
- Start Condition 有沒有出現?(SCL 高時 SDA 下降)
- SCL 有沒有脈波? 沒有脈波可能是程式沒跑到 I2C 初始化
- ACK 有沒有拉低? 沒有 ACK 表示裝置沒回應
- SDA/SCL 波形是否乾淨? 上升緣太緩慢表示上拉電阻太大
3. 用三用電表量測
- 量 SDA 對 GND 電壓:空閒時應為 3.3V
- 量 SCL 對 GND 電壓:空閒時應為 3.3V
- 如果接近 0V,表示沒有上拉電阻或 GPIO 被拉低
八、常見問題與排錯
Q1:I2C 掃描不到 BME280
最常見的原因依序是:
- 缺少上拉電阻(約占 60% 的案例)— SDA 和 SCL 必須有 4.7kΩ 上拉到 3.3V
- 位址錯誤(約 20%)— 試試 0x76 和 0x77 兩個位址
- 接線錯誤(約 15%)— SDA 接 SDA,SCL 接 SCL,不要交叉
- 感測器損壞(約 5%)— 換一顆試試
Q2:讀到的溫度固定不變
BME280 的測量模式可能設為「睡眠模式」。確認 ctrl_meas 暫存器(0xF4)的值為 0x27(正常模式,IIR 濾波關閉)。Adafruit 函式庫在 begin() 時會自動設定,但如果你用原始 I2C 操作,記得要設定。
Q3:氣壓值偏高或偏低
氣壓讀數需要根據當地海拔校正。BME280 回傳的是絕對氣壓,如果你住在高海拔地區,數值會偏低。可以用 bme.readAltitude(SEALEVELPRESSURE_HPA) 來計算海拔,但需要傳入正確的海平面氣壓參考值(可從氣象局查詢)。
Q4:SDA 或 SCL 波形上升緣很緩慢
這表示上拉電阻阻抗太高。4.7kΩ 是標準值,如果你的 I2C 匯流排上有很多裝置(增加寄生電容),可能需要降到 2.2kΩ 甚至 1kΩ。反之如果上升緣太快(過衝),則需要增加電阻值。
Q5:多個 I2C 裝置衝突
確認所有裝置的 I2C 位址不同。BME280 有 0x76 和 0x77 兩個選項。如果兩個裝置位址相同,可以買 I2C 多工器(如 TCA9548A)來解決。
總結
I2C 是嵌入式開發的必備技能,兩條線就能搞定多感測器通訊:
- I2C 只需要 SDA + SCL 兩條線,加上 4.7kΩ 上拉電阻
- BME280 是目前 C/P 值最高的環境感測器,一顆測三種資料
- 使用 Adafruit 函式庫可用幾行程式完成讀取
- I2C Scanner 是除錯的第一步,邏輯分析儀是第二步
- 90% 的通訊問題來自上拉電阻或接線錯誤
文章評論