
前言:兩條線,接一堆感測器
I2C 是 1982 年 Philips(現在的 NXP)發明的協定,目標只有一個:用最少的引腳完成裝置間通訊。它做到了 —— 兩條線(SDA + SCL)就能掛一整排感測器。
但它的規則比 UART、SPI 都細:Start 與 Stop 定義在「SCL 高電位時 SDA 的變化」,資料採樣在上升緣、變化在下降緣,而且每一個 byte 後面都要跟一個 ACK。搞懂這幾件事,你的 I2C 就不會再出現「掃不到裝置」的鬼故事。
兩條線與五個階段
| 訊號線 | 全名 | 說明 |
|---|---|---|
| SDA | Serial Data | 雙向資料線 |
| SCL | Serial Clock | 時脈線,由 Master 產生 |
一次完整的通訊固定由這五個階段組成:

其中最容易搞混的是 Start 與 Stop:它們不是特定的電位,而是「SCL 為高時 SDA 的邊緣變化」。這也是為什麼示波器上看到的 Start 就是 SDA 那一根下降的線。
什麼時候該用 I2C,什麼時候換 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。
一次交易的細節:位址、ACK、資料
每次通訊都從「送出從機位址 + 讀寫位元」開始。以 BME280 為例:

而讀暫存器的動作比想像中複雜一點 —— 因為要先告訴它「我要讀哪一個」:

這個「先寫位址、再 Repeat Start 後讀取」的流程是所有 I2C 感測器的通用模式,看懂這個之後,看任何 datasheet 的讀取時序都不會再茫然。
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 |
它的 I2C 位址有兩個:0x76(SDO 接 GND,預設)與 0x77(SDO 接 VCC)。多數現成模組預設都是 0x76,如果掃不到,第一個要試的就是換成 0x77。
硬體接線:那顆不能省的上拉電阻
| ESP32 | BME280 | 說明 |
|---|---|---|
| 3.3V | VCC | 電源 |
| GND | GND | 接地 |
| GPIO21 | SDA | I2C 資料線 |
| GPIO22 | SCL | I2C 時脈線 |
I2C 匯流排一定要有上拉電阻,SDA 與 SCL 各自需要一顆 4.7 kΩ 接到 3.3 V。如果你買的是 GY-BME280 這類模組,模組上已經內建,直接接線就好;但在麵包板上自己拉線時,少了它 I2C 就是不會動。
而且這個值不是固定的:

ESP32 程式碼(Adafruit 函式庫)
先用程式庫管理員安裝 Adafruit Unified Sensor 與 Adafruit BME280 Library,然後這段程式就能讀出三種數據:
#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
函式庫把底層都包好了,但知道它在做什麼,除錯時會快很多。溫度與濕度是這樣放的:
| 暫存器 | 位址 | 說明 |
|---|---|---|
| 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 的校正參數,這也是為什麼多數人直接用函式庫 —— 它已經把那些數學處理完了。
三步偵錯法

第一步永遠是跑一次掃描,看有沒有人回應:
#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);
}
掃不到裝置時,原因的分布其實非常集中:

掃到裝置但讀值不對,就要往邏輯分析儀與三用電表走:示波器看 Start、SCL 脈波與 ACK,電表量空閒電壓是否為 3.3 V。
三個症狀與它們的真正原因

這三個都不是「I2C 壞了」:一個是感測器還在睡眠模式、一個是沒做海拔校正、一個是位址撞號。把它們記起來,可以省下不少換模組的錢。
總結
I2C 值得學的原因不是它快,而是它便宜又省腳。五個重點:
- Start/Stop 是邊緣事件:SCL 為高時 SDA 的變化,不是特定電位
- 資料在上升緣採樣、下降緣變化:這條規則決定了所有波形長相
- 每 8 bits 一個 ACK:第 9 個時脈由接收方拉低表示收到
- 上拉電阻不能省:4.7 kΩ 是起點,裝置越多要越小
- 除錯從掃描開始:I2C Scanner 是第一步,邏輯分析儀是第二步
把這五點記住,之後接任何 I2C 感測器,你都只需要查它的位址與暫存器表。
文章評論