0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 自動化技巧
  3. 正文

ESP32 I2C 通訊實戰:連接 BME280 溫濕度氣壓感測器

2026 年 5 月 21 日 725點熱度 0人點贊 0條評論

ESP32 I2C 連接 BME280 完整教學封面

前言:兩條線,接一堆感測器

I2C 是 1982 年 Philips(現在的 NXP)發明的協定,目標只有一個:用最少的引腳完成裝置間通訊。它做到了 —— 兩條線(SDA + SCL)就能掛一整排感測器。

但它的規則比 UART、SPI 都細:Start 與 Stop 定義在「SCL 高電位時 SDA 的變化」,資料採樣在上升緣、變化在下降緣,而且每一個 byte 後面都要跟一個 ACK。搞懂這幾件事,你的 I2C 就不會再出現「掃不到裝置」的鬼故事。

兩條線與五個階段

訊號線 全名 說明
SDA Serial Data 雙向資料線
SCL Serial Clock 時脈線,由 Master 產生

一次完整的通訊固定由這五個階段組成:

I2C 通訊的五個階段

其中最容易搞混的是 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 為例:

I2C 從機位址的組成

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

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 的校正參數,這也是為什麼多數人直接用函式庫 —— 它已經把那些數學處理完了。

三步偵錯法

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);
}

掃不到裝置時,原因的分布其實非常集中:

掃描不到裝置的原因與比例

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

三個症狀與它們的真正原因

BME280 三個常見症狀與解法

這三個都不是「I2C 壞了」:一個是感測器還在睡眠模式、一個是沒做海拔校正、一個是位址撞號。把它們記起來,可以省下不少換模組的錢。

總結

I2C 值得學的原因不是它快,而是它便宜又省腳。五個重點:

  • Start/Stop 是邊緣事件:SCL 為高時 SDA 的變化,不是特定電位
  • 資料在上升緣採樣、下降緣變化:這條規則決定了所有波形長相
  • 每 8 bits 一個 ACK:第 9 個時脈由接收方拉低表示收到
  • 上拉電阻不能省:4.7 kΩ 是起點,裝置越多要越小
  • 除錯從掃描開始:I2C Scanner 是第一步,邏輯分析儀是第二步

把這五點記住,之後接任何 I2C 感測器,你都只需要查它的位址與暫存器表。

標籤: BME280 ESP32 I2C 感測器 教學 氣壓 溫濕度
最後更新:2026 年 9 月 30 日

shi6a

這個人很懶,什麼都沒留下

點贊
< 上一篇
下一篇 >

文章評論

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回覆

COPYRIGHT © 2026 0x6A Logbook. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang