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

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

- 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

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õ
ztrong 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.

5. Chọn chế độ đo

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ượng | Nguyên nhân thường gặp | Cá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 I2C | Kiểm tra dây; CSB lên 3V3; chạy chương trình quét I2C |
| Báo chip ID 0x58 dù mua BME280 | Module là BMP280 | Khô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 0xF4 | Dù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 m | p0 = 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 °C | Tự 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ại | Ghi 0xF4 với chế độ normal, hoặc ra lệnh forced mỗi lần đọc |
8. Xem thêm
- Cảm biến độ ẩm: cấu tạo, nguyên lý, các loại
- Đồng hồ và dự báo thời tiết trên OLED với ESP32
- DHT11 và DHT22 với ESP32: đọc nhiệt độ, độ ẩm không cần thư viện
- Deep Sleep trên ESP32: tiết kiệm pin
Bài cùng cụm chủ đề: trạm thời tiết ESP32 đo gió, mưa.
Xem thêm