BME280 và BMP280 với ESP32: phân biệt chip, đo nhiệt độ, áp suất, độ ẩm, tính độ cao không thư viện

BME280 và BMP280: phân biệt bằng chip ID 0x60, 0x58; nối I2C với ESP32; đọc nhiệt độ, áp suất, độ ẩm theo công thức bù trừ của Bosch không cần thư viện; tính độ cao tuyệt đối, tương đối.

BME280 và BMP280 là hai cảm biến của Bosch rất hay dùng với ESP32, Arduino: BMP280 đo nhiệt độ và áp suất khí quyển, BME280 đo thêm độ ẩm. Từ áp suất tính được độ cao, đủ nhạy để nhận ra cảm biến được nâng lên vài chục cm. Bài này giúp phân biệt hai chip (module bán ngoài thị trường hay ghi lẫn tên), nối với ESP32 qua I2C, đọc dữ liệu không cần thư viện theo đúng công thức trong datasheet, tính độ cao tuyệt đối và tương đối, chọn chế độ đo theo việc và xử lý các lỗi hay gặp.

1. BME280 hay BMP280: phân biệt thế nào

Bảng phân biệt: BME280 đo nhiệt độ, áp suất, độ ẩm, chip ID 0x60, chip 2,5 x 2,5 mm; BMP280 không đo độ ẩm, chip ID 0x58, chip 2,0 x 2,5 mm; cùng địa chỉ I2C 0x76 hoặc 0x77, cùng dải 300 đến 1100 hPa
Hai chip cùng chân, khác chip ID.

Hai chip giống nhau về chân, địa chỉ I2C và cách đọc nhiệt độ, áp suất; module nhìn gần như nhau. Cách chắc chắn nhất là đọc thanh ghi chip ID ở địa chỉ 0xD0: 0x60 là BME280, 0x58 là BMP280. Chương trình ở mục 3 làm việc này ngay khi khởi động. Nhìn bằng mắt: chip BME280 gần vuông (2,5 × 2,5 mm), BMP280 hình chữ nhật (2,0 × 2,5 mm), cả hai có lỗ thông khí nhỏ trên vỏ kim loại. Mua module ghi "BME280" mà chương trình báo chip ID 0x58 thì đó là BMP280, không đo được độ ẩm.

BMP180 là chip đời cũ (chip ID 0x55), thanh ghi và công thức khác hẳn; chương trình trong bài không đọc được BMP180.

2. Nối với ESP32

Sơ đồ nối: ESP32 3V3 vào VCC, GND vào GND, D22 vào SCL, D21 vào SDA; chân CSB để trống hoặc nối 3V3 để chọn I2C; SDO nối GND là địa chỉ 0x76, nối 3V3 là 0x77
I2C dùng chân D21, D22.
  • VCC vào 3V3, GND vào GND, SDA vào D21, SCL vào D22 của ESP32 DevKit V1.
  • Module 6 chân có thêm CSB (mức cao hoặc để trống: chọn I2C) và SDO (nối GND: địa chỉ 0x76, nối 3V3: 0x77). Module 4 chân đã nối sẵn hai chân này trên mạch, đa số ở địa chỉ 0x76.
  • Chip chịu tối đa khoảng 3,6 V. Module có ổn áp và điện trở kéo lên mới cấp 5 V được; không chắc thì cấp 3V3.

3. Chương trình đọc không cần thư viện

Thư viện của Adafruit, SparkFun đọc BME280 rất tiện, nhưng tự đọc giúp hiểu cảm biến làm việc thế nào và tự nhận ra BME280 hay BMP280. Mỗi chip được hiệu chuẩn riêng ở nhà máy: các hệ số hiệu chuẩn nằm ở thanh ghi 0x88–0xA1 và 0xE1–0xE7. Số đo thô ở 0xF7–0xFE phải qua các công thức bù trừ trong datasheet mới ra °C, Pa, %RH. Chương trình dùng đúng bản công thức số nguyên của Bosch.

// PYWORLD.VN - Đọc BME280 hoặc BMP280 qua I2C không cần thư viện: tự nhận loại chip (0x60 / 0x58), nhiệt độ, áp suất, độ ẩm
// PYWORLD.VN - Bù trừ theo công thức số nguyên trong datasheet Bosch; tính độ cao tuyệt đối và độ cao so với lúc bật nguồn
// PYWORLD.VN - Nối: VCC 3V3, GND, SDA D21, SCL D22; gõ z trong Serial Monitor để đặt lại mốc độ cao tương đối

#include <Wire.h>
#include <math.h>

const float AP_SUAT_MAT_BIEN_HPA = 1013.25f;   // áp suất mực nước biển tiêu chuẩn; thay bằng số của đài khí tượng nếu có

uint8_t diaChi = 0;
uint8_t maChip = 0;
bool coDoAm = false;

uint16_t digT1, digP1;
int16_t digT2, digT3, digP2, digP3, digP4, digP5, digP6, digP7, digP8, digP9;
uint8_t digH1, digH3;
int16_t digH2, digH4, digH5;
int8_t digH6;
int32_t tFine;
float apSuatMoc = 0;                           // hPa, mốc cho độ cao tương đối

uint8_t doc8(uint8_t thanhGhi) {
  Wire.beginTransmission(diaChi);
  Wire.write(thanhGhi);
  Wire.endTransmission(false);
  Wire.requestFrom(diaChi, (uint8_t)1);
  return Wire.read();
}

void docNhieu(uint8_t thanhGhi, uint8_t *dem, uint8_t soByte) {
  Wire.beginTransmission(diaChi);
  Wire.write(thanhGhi);
  Wire.endTransmission(false);
  Wire.requestFrom(diaChi, soByte);
  for (uint8_t i = 0; i < soByte; i++) dem[i] = Wire.read();
}

void ghi8(uint8_t thanhGhi, uint8_t giaTri) {
  Wire.beginTransmission(diaChi);
  Wire.write(thanhGhi);
  Wire.write(giaTri);
  Wire.endTransmission();
}

bool timCamBien() {
  const uint8_t thu[] = {0x76, 0x77};          // SDO nối GND: 0x76; nối VCC: 0x77
  for (uint8_t a : thu) {
    Wire.beginTransmission(a);
    if (Wire.endTransmission() == 0) {
      diaChi = a;
      maChip = doc8(0xD0);                     // thanh ghi chip ID
      return true;
    }
  }
  return false;
}

void docHeSo() {
  uint8_t d[26];
  docNhieu(0x88, d, 24);                       // 0x88..0x9F: hệ số nhiệt độ và áp suất (byte thấp trước)
  digT1 = d[0] | (d[1] << 8);
  digT2 = d[2] | (d[3] << 8);
  digT3 = d[4] | (d[5] << 8);
  digP1 = d[6] | (d[7] << 8);
  digP2 = d[8] | (d[9] << 8);
  digP3 = d[10] | (d[11] << 8);
  digP4 = d[12] | (d[13] << 8);
  digP5 = d[14] | (d[15] << 8);
  digP6 = d[16] | (d[17] << 8);
  digP7 = d[18] | (d[19] << 8);
  digP8 = d[20] | (d[21] << 8);
  digP9 = d[22] | (d[23] << 8);
  if (coDoAm) {
    digH1 = doc8(0xA1);
    docNhieu(0xE1, d, 7);                      // 0xE1..0xE7: hệ số độ ẩm, digH4 và digH5 dùng chung byte 0xE5
    digH2 = d[0] | (d[1] << 8);
    digH3 = d[2];
    digH4 = (int16_t)((int8_t)d[3] * 16) | (d[4] & 0x0F);
    digH5 = (int16_t)((int8_t)d[5] * 16) | (d[4] >> 4);
    digH6 = (int8_t)d[6];
  }
}

// Ba hàm bù trừ theo datasheet Bosch (bản số nguyên)
int32_t buNhietDo(int32_t adcT) {            // trả về 0,01 °C
  int32_t var1 = ((((adcT >> 3) - ((int32_t)digT1 << 1))) * ((int32_t)digT2)) >> 11;
  int32_t var2 = (((((adcT >> 4) - ((int32_t)digT1)) * ((adcT >> 4) - ((int32_t)digT1))) >> 12) * ((int32_t)digT3)) >> 14;
  tFine = var1 + var2;
  return (tFine * 5 + 128) >> 8;
}

uint32_t buApSuat(int32_t adcP) {            // trả về Pa x 256
  int64_t var1 = ((int64_t)tFine) - 128000;
  int64_t var2 = var1 * var1 * (int64_t)digP6;
  var2 = var2 + ((var1 * (int64_t)digP5) << 17);
  var2 = var2 + (((int64_t)digP4) << 35);
  var1 = ((var1 * var1 * (int64_t)digP3) >> 8) + ((var1 * (int64_t)digP2) << 12);
  var1 = (((((int64_t)1) << 47) + var1)) * ((int64_t)digP1) >> 33;
  if (var1 == 0) return 0;
  int64_t p = 1048576 - adcP;
  p = (((p << 31) - var2) * 3125) / var1;
  var1 = (((int64_t)digP9) * (p >> 13) * (p >> 13)) >> 25;
  var2 = (((int64_t)digP8) * p) >> 19;
  p = ((p + var1 + var2) >> 8) + (((int64_t)digP7) << 4);
  return (uint32_t)p;
}

uint32_t buDoAm(int32_t adcH) {              // trả về %RH x 1024
  int32_t v = tFine - ((int32_t)76800);
  v = (((((adcH << 14) - (((int32_t)digH4) << 20) - (((int32_t)digH5) * v)) + ((int32_t)16384)) >> 15) *
       (((((((v * ((int32_t)digH6)) >> 10) * (((v * ((int32_t)digH3)) >> 11) + ((int32_t)32768))) >> 10) +
          ((int32_t)2097152)) * ((int32_t)digH2) + 8192) >> 14));
  v = v - (((((v >> 15) * (v >> 15)) >> 7) * ((int32_t)digH1)) >> 4);
  v = v < 0 ? 0 : v;
  v = v > 419430400 ? 419430400 : v;
  return (uint32_t)(v >> 12);
}

bool docDo(float &nhietDo, float &apSuatHpa, float &doAm) {
  uint8_t d[8];
  docNhieu(0xF7, d, coDoAm ? 8 : 6);           // áp suất 0xF7..F9, nhiệt độ 0xFA..FC, độ ẩm 0xFD..FE
  int32_t adcP = ((int32_t)d[0] << 12) | ((int32_t)d[1] << 4) | (d[2] >> 4);
  int32_t adcT = ((int32_t)d[3] << 12) | ((int32_t)d[4] << 4) | (d[5] >> 4);
  if (adcT == 0x80000) return false;           // chưa có số đo đầu tiên
  nhietDo = buNhietDo(adcT) / 100.0f;
  apSuatHpa = buApSuat(adcP) / 25600.0f;
  doAm = coDoAm ? buDoAm(((int32_t)d[6] << 8) | d[7]) / 1024.0f : NAN;
  return true;
}

float doCao(float apSuatHpa, float apSuatGoc) {
  return 44330.0f * (1.0f - powf(apSuatHpa / apSuatGoc, 0.1903f));
}

void setup() {
  Serial.begin(115200);
  Wire.begin(21, 22);
  delay(100);
  if (!timCamBien()) {
    Serial.println("Khong thay cam bien o 0x76, 0x77: kiem tra day SDA, SCL, nguon 3V3");
    while (true) delay(1000);
  }
  if (maChip == 0x60) {
    coDoAm = true;
    Serial.printf("Thay BME280 o 0x%02X (chip ID 0x60): nhiet do, ap suat, do am\n", diaChi);
  } else if (maChip == 0x58) {
    Serial.printf("Thay BMP280 o 0x%02X (chip ID 0x58): nhiet do, ap suat, khong do do am\n", diaChi);
  } else {
    Serial.printf("Chip ID 0x%02X o 0x%02X: khong phai BME280/BMP280\n", maChip, diaChi);
    while (true) delay(1000);
  }
  ghi8(0xE0, 0xB6);                            // khởi động lại cảm biến
  delay(10);
  docHeSo();
  if (coDoAm) ghi8(0xF2, 0x01);                // độ ẩm x1: phải ghi trước 0xF4 mới có tác dụng
  ghi8(0xF5, 0x10);                            // chờ 0,5 ms giữa hai lần đo, bộ lọc IIR hệ số 16
  ghi8(0xF4, 0x57);                            // nhiệt độ x2, áp suất x16, chế độ normal
  delay(500);
  float t, p, h;
  if (docDo(t, p, h)) apSuatMoc = p;
}

void loop() {
  if (Serial.available()) {
    char c = Serial.read();
    float t, p, h;
    if ((c == 'z' || c == 'Z') && docDo(t, p, h)) {
      apSuatMoc = p;
      Serial.println("Da dat lai moc do cao tuong doi");
    }
  }
  static uint32_t lanIn = 0;
  if (millis() - lanIn >= 1000) {
    lanIn = millis();
    float t, p, h;
    if (!docDo(t, p, h)) return;
    Serial.printf("T %.2f C | P %.2f hPa | ", t, p);
    if (coDoAm) Serial.printf("RH %.1f %% | ", h);
    Serial.printf("do cao %.1f m | so voi moc %+.2f m\n", doCao(p, AP_SUAT_MAT_BIEN_HPA), doCao(p, apSuatMoc));
  }
}
  • timCamBien() thử địa chỉ 0x76 rồi 0x77, đọc chip ID và chọn có đọc độ ẩm hay không.
  • docHeSo() đọc hệ số hiệu chuẩn. Hai hệ số độ ẩm digH4, digH5 dùng chung một byte ở 0xE5: mỗi hệ số lấy 4 bit của byte đó. Viết sai chỗ này là lỗi rất hay gặp khi tự viết, độ ẩm ra số vô lý.
  • Cấu hình: nhiệt độ lấy mẫu 2 lần, áp suất 16 lần, bộ lọc IIR hệ số 16, chế độ đo liên tục. Đây là cấu hình Bosch gợi ý cho đo độ cao trong nhà, ít nhiễu áp suất nhất.
  • Thanh ghi điều khiển độ ẩm 0xF2 phải ghi trước 0xF4 mới có tác dụng.

Cách hoạt động của bus I2C, quét địa chỉ, đổi chân SDA, SCL có ở bài giao tiếp I2C trên ESP32.

4. Tính độ cao từ áp suất

Đồ thị độ cao theo áp suất với mực nước biển 1013,25 hPa: 1001 hPa khoảng 102 m, 950 hPa khoảng 540 m, 900 hPa khoảng 989 m; gần mực nước biển 1 hPa ứng với khoảng 8,3 m
Công thức khí quyển chuẩn.

Không khí càng lên cao càng loãng, áp suất càng giảm. Công thức khí quyển chuẩn: h = 44330 × (1 − (p / p0)0,1903), với p0 là áp suất ở mực nước biển. Gần mặt đất, áp suất giảm 1 hPa ứng với cao thêm khoảng 8,3 m.

  • Độ cao tuyệt đối dùng p0 = 1013,25 hPa (khí quyển chuẩn). Áp suất mực nước biển thật đổi theo thời tiết, chênh vài hPa là sai vài chục mét. Muốn đúng hơn, thay bằng áp suất quy về mực nước biển của trạm khí tượng gần nhất.
  • Độ cao tương đối lấy p0 là áp suất lúc bắt đầu: ra độ chênh so với chỗ đặt mốc, ví dụ lên mấy tầng nhà, flycam bay cao bao nhiêu so với chỗ cất cánh. Sai số tương đối của áp suất là ±0,12 hPa, khoảng ±1 m; nhiễu ngắn hạn với bộ lọc thì nhỏ hơn nhiều, đủ thấy được chênh vài chục cm. Gõ z trong Serial Monitor để đặt lại mốc.
  • Áp suất trong phòng thay đổi khi mở cửa, bật điều hòa, có gió: độ cao tương đối trôi dần theo thời gian, cần đặt lại mốc trước mỗi lần đo.
Serial Monitor: tìm thấy BMP280 ở 0x76 chip ID 0x58; T 25.08 C, P 1006.53 hPa, độ cao 56,1 m, so với mốc khoảng 0; sau khi nâng lên khoảng 1 m áp suất 1006.42 hPa, so với mốc khoảng +1 m
Số minh họa tính từ bộ hệ số ví dụ trong datasheet.

5. Chọn chế độ đo

Bảng chế độ đo theo gợi ý Bosch: trạm thời tiết forced 1 lần mỗi phút lấy mẫu x1 không lọc; đo độ ẩm forced 1 lần mỗi giây; đo độ cao trong nhà normal lấy mẫu x2 x16 x1 lọc 16; thiết bị chơi game normal x1 x4 lọc 16
Theo cấu hình gợi ý trong datasheet.

Cảm biến có ba chế độ: ngủ, forced (đo một lần rồi ngủ, chương trình ra lệnh mỗi khi cần) và normal (tự đo liên tục). Trạm thời tiết chỉ cần đo mỗi phút một lần ở chế độ forced: tốn rất ít điện, hợp chạy pin với ngủ sâu, và cảm biến hầu như không tự nóng. Đo độ cao cần normal, lấy mẫu nhiều và bật bộ lọc.

6. Nhiệt độ, độ ẩm cao hơn thực tế

Nhiệt độ của BME280 là nhiệt độ của chính chip, dùng chủ yếu để bù trừ áp suất. Module đặt sát ESP32 (chip Wi-Fi nóng lên khi phát), đo liên tục ở chế độ normal, hoặc nằm trong hộp kín sẽ đọc cao hơn nhiệt độ phòng vài độ; độ ẩm tương đối theo đó đọc thấp hơn. Cách giảm: đặt cảm biến cách xa ESP32 và nguồn bằng một đoạn dây, khoét lỗ thông gió cho hộp, dùng chế độ forced đo thưa. Cần đo nhiệt độ, độ ẩm chính xác cho phòng, nhà kính, so sánh các loại cảm biến ở bài cảm biến độ ẩm: cấu tạo, nguyên lý.

7. Lỗi hay gặp

Hiện tượngNguyên nhân thường gặpCách xử lý
Không thấy cảm biến ở 0x76, 0x77Đảo SDA, SCL; thiếu nguồn; module SPI chưa chọn I2CKiểm tra dây; CSB lên 3V3; chạy chương trình quét I2C
Báo chip ID 0x58 dù mua BME280Module là BMP280Không có độ ẩm; đổi module nếu cần đo độ ẩm
Độ ẩm luôn 0 % hoặc 100 %Đọc sai hệ số H4, H5; chưa ghi 0xF2 trước 0xF4Dùng đúng cách ghép byte 0xE5 và thứ tự ghi như chương trình
Áp suất đúng nhưng độ cao lệch vài chục mp0 = 1013,25 hPa khác áp suất mực nước biển hôm đóDùng áp suất quy về mực nước biển của trạm khí tượng, hoặc dùng độ cao tương đối
Nhiệt độ cao hơn phòng 2–5 °CTự nóng, đặt sát ESP32, hộp kínĐặt xa, thông gió, đo chế độ forced
Số đọc đứng im không đổiĐang ở chế độ ngủ hoặc forced mà không ra lệnh đo lạiGhi 0xF4 với chế độ normal, hoặc ra lệnh forced mỗi lần đọc

8. Xem thêm

Bài cùng cụm chủ đề: trạm thời tiết ESP32 đo gió, mưa.


Tin tức liên quan

Tay điều khiển flycam và FPV: các kênh, tín hiệu PPM, SBUS, CRSF/ELRS, failsafe và băng tần
Tay điều khiển flycam và FPV: các kênh, tín hiệu PPM, SBUS, CRSF/ELRS, failsafe và băng tần

Tay điều khiển flycam và FPV: cần Mode 2, thứ tự kênh AETR, tín hiệu PWM, PPM, khung SBUS 25 byte, IBUS, CRSF/ELRS, failsafe khi mất sóng, kính FPV 5,8 GHz và dùng ESP32 làm bộ phát.

Sạc pin Li-ion 1S ở dòng 1 A: chọn dung lượng pin, thời gian sạc và an toàn
Sạc pin Li-ion 1S ở dòng 1 A: chọn dung lượng pin, thời gian sạc và an toàn

Dòng sạc 1 A hợp với pin 18650 và pin Li-po từ khoảng 2000 mAh, quá lớn với pin vài trăm mAh. Bài hướng dẫn chọn dung lượng pin, ước tính thời gian sạc, cách mắc pin và quy tắc an toàn.

Lực kế chuẩn theo ISO 376: trình tự hiệu chuẩn, chỉ tiêu tái lập, lặp lại, nội suy và phân cấp
Lực kế chuẩn theo ISO 376: trình tự hiệu chuẩn, chỉ tiêu tái lập, lặp lại, nội suy và phân cấp

Hiệu chuẩn lực kế chuẩn theo ISO 376: chuẩn bị, trình tự sáu loạt đo xoay 0°, 120°, 240°, công thức b, b', fc, v, bảng tiêu chí cấp 00, 0,5, 1, 2 và ví dụ lực kế 100 kN.

Cách đấu jack Type-C tích hợp mạch sạc với pin 18650, Li-po: sơ đồ, đèn báo và lỗi thường gặp
Cách đấu jack Type-C tích hợp mạch sạc với pin 18650, Li-po: sơ đồ, đèn báo và lỗi thường gặp

Hướng dẫn đấu jack Type-C tích hợp mạch sạc với pin 18650 hoặc Li-po một cell: dây đỏ, dây đen nối vào đâu, tải nối thế nào, khi nào cần mạch bảo vệ, cách đọc đèn đỏ xanh và bảng tra lỗi thường gặp.

Hiệu chuẩn transmitter áp suất 4–20 mA: sơ đồ vòng, sai số theo khoảng đo, lúc nhận và sau chỉnh
Hiệu chuẩn transmitter áp suất 4–20 mA: sơ đồ vòng, sai số theo khoảng đo, lúc nhận và sau chỉnh

Hiệu chuẩn transmitter áp suất 4–20 mA: sơ đồ vòng dòng điện, công thức dòng và sai số theo phần trăm khoảng đo, trình tự lúc nhận và sau chỉnh, ví dụ 0–16 bar, lưu ý về HART và điểm 0.

Module relay 1, 2, 4 kênh: kích mức thấp hay cao, opto cách ly, jumper JD-VCC, chọn 5 V hay 12 V
Module relay 1, 2, 4 kênh: kích mức thấp hay cao, opto cách ly, jumper JD-VCC, chọn 5 V hay 12 V

Module relay 1, 2, 4 kênh: bên trong một kênh có opto, kích mức thấp hay mức cao, jumper JD-VCC để cách ly với ESP32, chọn bản 5 V hay 12 V và đấu tải 220 V qua COM, NO an toàn.

Hiệu chuẩn load cell tại hiện trường: tải thật, mô phỏng mV/V, thế chỗ, ISO 376 và ASTM E74
Hiệu chuẩn load cell tại hiện trường: tải thật, mô phỏng mV/V, thế chỗ, ISO 376 và ASTM E74

Hiệu chuẩn hệ load cell tại hiện trường: chuỗi đo và nguồn sai số, tải thật so với mô phỏng mV/V, thế chỗ cho cân bồn, lưu ý lắp đặt, so sánh ISO 376 và ASTM E74.

Jack Type-C tích hợp mạch sạc pin 1S: cấu tạo, thông số và khi nào nên dùng
Jack Type-C tích hợp mạch sạc pin 1S: cấu tạo, thông số và khi nào nên dùng

Jack Type-C tích hợp mạch sạc pin 1S nhận 5 V, sạc pin Li-ion hoặc Li-po một cell đến 4,2 V ở dòng 1 A, có đèn báo đỏ và xanh. Bài nêu cấu tạo, thông số, điểm khác với jack Type-C thường và các trường hợp nên dùng.


Bình luận
Đã thêm vào giỏ hàng