bmx280.tc¶
BMx280 / BME68x Temperature, Pressure, Humidity & Gas Sensor Driver
// BMx280 / BME68x Temperature, Pressure, Humidity & Gas Sensor Driver
// Auto-detects:
// BMP280 (chip id 0x58) — temp + pressure
// BME280 (chip id 0x60) — temp + pressure + humidity
// BME680 (chip id 0x61, variant 0) — + gas (VOC) resistance
// BME688 (chip id 0x61, variant 1) — + gas; BME690 is register-compatible (variant 1)
// I2C addresses: 0x76 (SDO=GND) or 0x77 (SDO=VCC). Scans both I2C buses.
// BMP/BME280 run in normal mode; BME68x use forced mode with a gas-heater cycle
// (trigger one measurement per second, read the previous one next tick — no delay()).
// Reads every second, displays on web UI + JSON teleperiod, with 6h chart history.
#define BMX_ADDR1 0x76
#define BMX_ADDR2 0x77
#define BMP_ID 0x58
#define BME_ID 0x60
#define BME680_ID 0x61
// Measurement results
float bmx_temp = 0.0;
float bmx_humi = 0.0;
float bmx_pres = 0.0;
float bmx_dewp = 0.0;
float bmx_absh = 0.0;
float bmx_gas = 0.0; // gas resistance, kOhm (BME68x only; higher = cleaner air)
int bmx_ok = 0;
int bmx_addr = 0;
int bmx_bus = 0;
int bmx_has_humi = 0; // 1 = BME280 / BME68x
int bmx_is_680 = 0; // 1 = BME680 / BME688 / BME690 family
int bmx_has_gas = 0; // 1 = gas sensor present (BME68x)
int bmx_variant = 0; // 0 = BME680, 1 = BME688 / BME690
int bmx_gas_valid = 0;
// I2C data buffer
char bmx_buf[26];
// ── BMP280 / BME280 calibration (registers 0x88..0x9F + humidity block) ──
int dig_T1; int dig_T2; int dig_T3;
int dig_P1; int dig_P2; int dig_P3; int dig_P4; int dig_P5;
int dig_P6; int dig_P7; int dig_P8; int dig_P9;
int dig_H1; int dig_H2; int dig_H3; int dig_H4; int dig_H5; int dig_H6;
int t_fine; // shared between BME280 temp & pressure/humidity comp
// ── BME68x calibration (par_*, spread across 0x8A..0xA0 and 0xE1..0xEE) ──
int par_t1; int par_t2; int par_t3;
int par_p1; int par_p2; int par_p3; int par_p4; int par_p5;
int par_p6; int par_p7; int par_p8; int par_p9; int par_p10;
int par_h1; int par_h2; int par_h3; int par_h4; int par_h5; int par_h6; int par_h7;
int par_g1; int par_g2; int par_g3;
int res_heat_range; int res_heat_val; int range_sw_err;
float bme_tfine; // BME68x t_fine (float)
#ifdef USE_CHARTS
// Chart history (6h at 1 sample/min = 360 points)
#define CHART_LEN 360
float hist_temp[CHART_LEN];
float hist_humi[CHART_LEN];
float hist_pres[CHART_LEN];
float hist_gas[CHART_LEN];
int hist_pos;
int hist_tick;
#endif
// Chip name for display
char bmx_name[8];
char bmx_lbl[32];
int sign16(int val) {
if (val >= 32768) return val - 65536;
return val;
}
int sign8(int val) {
if (val >= 128) return val - 256;
return val;
}
// ════════════════════════════════════════════════════════════════════
// BMP280 / BME280 calibration
// ════════════════════════════════════════════════════════════════════
int bme280_read_calib() {
// Read 24 bytes from 0x88..0x9F (temp + pressure)
if (!i2cRead(bmx_addr, 0x88, bmx_buf, 24, bmx_bus)) return 0;
dig_T1 = bmx_buf[0] | (bmx_buf[1] << 8);
dig_T2 = sign16(bmx_buf[2] | (bmx_buf[3] << 8));
dig_T3 = sign16(bmx_buf[4] | (bmx_buf[5] << 8));
dig_P1 = bmx_buf[6] | (bmx_buf[7] << 8);
dig_P2 = sign16(bmx_buf[8] | (bmx_buf[9] << 8));
dig_P3 = sign16(bmx_buf[10] | (bmx_buf[11] << 8));
dig_P4 = sign16(bmx_buf[12] | (bmx_buf[13] << 8));
dig_P5 = sign16(bmx_buf[14] | (bmx_buf[15] << 8));
dig_P6 = sign16(bmx_buf[16] | (bmx_buf[17] << 8));
dig_P7 = sign16(bmx_buf[18] | (bmx_buf[19] << 8));
dig_P8 = sign16(bmx_buf[20] | (bmx_buf[21] << 8));
dig_P9 = sign16(bmx_buf[22] | (bmx_buf[23] << 8));
if (bmx_has_humi) {
dig_H1 = i2cRead8(bmx_addr, 0xA1, bmx_bus);
if (!i2cRead(bmx_addr, 0xE1, bmx_buf, 7, bmx_bus)) return 0;
dig_H2 = sign16(bmx_buf[0] | (bmx_buf[1] << 8));
dig_H3 = bmx_buf[2];
dig_H4 = sign16((bmx_buf[3] << 4) | (bmx_buf[4] & 0x0F));
dig_H5 = sign16((bmx_buf[5] << 4) | ((bmx_buf[4] >> 4) & 0x0F));
dig_H6 = sign8(bmx_buf[6]);
}
return 1;
}
// ════════════════════════════════════════════════════════════════════
// BME68x calibration (different layout than BME280)
// Block A: 0x8A..0xA0 (23 bytes), Block B: 0xE1..0xEE (14 bytes),
// plus res_heat_val(0x00), res_heat_range(0x02), range_sw_err(0x04).
// ════════════════════════════════════════════════════════════════════
int bme680_read_calib() {
if (!i2cRead(bmx_addr, 0x8A, bmx_buf, 23, bmx_bus)) return 0;
par_t2 = sign16(bmx_buf[0] | (bmx_buf[1] << 8));
par_t3 = sign8(bmx_buf[2]);
par_p1 = bmx_buf[4] | (bmx_buf[5] << 8);
par_p2 = sign16(bmx_buf[6] | (bmx_buf[7] << 8));
par_p3 = sign8(bmx_buf[8]);
par_p4 = sign16(bmx_buf[10] | (bmx_buf[11] << 8));
par_p5 = sign16(bmx_buf[12] | (bmx_buf[13] << 8));
par_p7 = sign8(bmx_buf[14]);
par_p6 = sign8(bmx_buf[15]);
par_p8 = sign16(bmx_buf[18] | (bmx_buf[19] << 8));
par_p9 = sign16(bmx_buf[20] | (bmx_buf[21] << 8));
par_p10 = bmx_buf[22];
if (!i2cRead(bmx_addr, 0xE1, bmx_buf, 14, bmx_bus)) return 0;
par_h2 = (bmx_buf[0] << 4) | (bmx_buf[1] >> 4);
par_h1 = (bmx_buf[2] << 4) | (bmx_buf[1] & 0x0F);
par_h3 = sign8(bmx_buf[3]);
par_h4 = sign8(bmx_buf[4]);
par_h5 = sign8(bmx_buf[5]);
par_h6 = bmx_buf[6];
par_h7 = sign8(bmx_buf[7]);
par_t1 = bmx_buf[8] | (bmx_buf[9] << 8);
par_g2 = sign16(bmx_buf[10] | (bmx_buf[11] << 8));
par_g1 = sign8(bmx_buf[12]);
par_g3 = sign8(bmx_buf[13]);
res_heat_val = sign8(i2cRead8(bmx_addr, 0x00, bmx_bus));
res_heat_range = (i2cRead8(bmx_addr, 0x02, bmx_bus) >> 4) & 0x03;
range_sw_err = sign8(i2cRead8(bmx_addr, 0x04, bmx_bus) & 0xF0) / 16;
return 1;
}
// Heater resistance byte for a target temperature (°C), given ambient (°C)
int bme680_calc_res_heat(int target, float amb) {
float var1 = ((float)par_g1 / 16.0) + 49.0;
float var2 = (((float)par_g2 / 32768.0) * 0.0005) + 0.00235;
float var3 = (float)par_g3 / 1024.0;
float var4 = var1 * (1.0 + (var2 * (float)target));
float var5 = var4 + (var3 * amb);
float rh = 3.4 * ((var5 * (4.0 / (4.0 + (float)res_heat_range)) *
(1.0 / (1.0 + ((float)res_heat_val * 0.002)))) - 25.0);
int r = (int)rh;
if (r < 0) r = 0;
if (r > 255) r = 255;
return r;
}
// ════════════════════════════════════════════════════════════════════
// Configure
// ════════════════════════════════════════════════════════════════════
int bme280_configure() {
if (bmx_has_humi) {
if (!i2cWrite8(bmx_addr, 0xF2, 0x01, bmx_bus)) return 0; // ctrl_hum os x1
}
if (!i2cWrite8(bmx_addr, 0xF5, 0xA0, bmx_bus)) return 0; // config: standby 1s, filter off
if (!i2cWrite8(bmx_addr, 0xF4, 0x27, bmx_bus)) return 0; // ctrl_meas: T/P x1, normal mode
return 1;
}
int bme680_configure() {
if (!i2cWrite8(bmx_addr, 0x72, 0x01, bmx_bus)) return 0; // ctrl_hum: os_h x1
i2cWrite8(bmx_addr, 0x75, 0x00, bmx_bus); // config: filter off
// Heater profile 0: ~300 °C target, ~100 ms heat
int rh = bme680_calc_res_heat(300, 25.0);
i2cWrite8(bmx_addr, 0x5A, rh, bmx_bus); // res_heat_0
i2cWrite8(bmx_addr, 0x64, 0x59, bmx_bus); // gas_wait_0 (~100 ms)
i2cWrite8(bmx_addr, 0x71, 0x10, bmx_bus); // ctrl_gas_1: run_gas=1, nb_conv=0
// ctrl_meas: os_t x1, os_p x1, forced mode (01) — kicks off the first measurement
i2cWrite8(bmx_addr, 0x74, 0x25, bmx_bus);
return 1;
}
int bmx_read_calib() {
if (bmx_is_680) return bme680_read_calib();
return bme280_read_calib();
}
int bmx_configure() {
if (bmx_is_680) return bme680_configure();
return bme280_configure();
}
// Scan both buses and addresses, auto-detect chip type
int bmx_scan() {
int bus = 0;
while (bus < 2) {
int addr = BMX_ADDR1;
while (addr <= BMX_ADDR2) {
if (i2cSetDevice(addr, bus)) {
int id = i2cRead8(addr, 0xD0, bus);
if (id == BME_ID || id == BMP_ID || id == BME680_ID) {
bmx_addr = addr;
bmx_bus = bus;
bmx_is_680 = 0;
bmx_has_gas = 0;
if (id == BME680_ID) {
bmx_is_680 = 1;
bmx_has_humi = 1;
bmx_has_gas = 1;
bmx_variant = i2cRead8(addr, 0xF0, bus); // 0 = BME680, 1 = BME688/690
if (bmx_variant == 0) {
strcpy(bmx_name, "BME680");
} else {
strcpy(bmx_name, "BME688");
}
} else if (id == BME_ID) {
bmx_has_humi = 1;
strcpy(bmx_name, "BME280");
} else {
bmx_has_humi = 0;
strcpy(bmx_name, "BMP280");
}
i2cSetActiveFound(bmx_addr, "BMx280", bmx_bus);
return 1;
}
}
addr++;
}
bus++;
}
return 0;
}
// ════════════════════════════════════════════════════════════════════
// BMP280 / BME280 compensation
// ════════════════════════════════════════════════════════════════════
int bme280_comp_temp(int adc_T) {
int var1 = ((((adc_T >> 3) - (dig_T1 << 1))) * dig_T2) >> 11;
int var2 = (((((adc_T >> 4) - dig_T1) * ((adc_T >> 4) - dig_T1)) >> 12) * dig_T3) >> 14;
t_fine = var1 + var2;
return (t_fine * 5 + 128) >> 8;
}
float bme280_comp_pres(int adc_P) {
float var1 = (float)t_fine / 2.0 - 64000.0;
float var2 = var1 * var1 * (float)dig_P6 / 32768.0;
var2 = var2 + var1 * (float)dig_P5 * 2.0;
var2 = var2 / 4.0 + (float)dig_P4 * 65536.0;
var1 = ((float)dig_P3 * var1 * var1 / 524288.0 + (float)dig_P2 * var1) / 524288.0;
var1 = (1.0 + var1 / 32768.0) * (float)dig_P1;
if (var1 == 0.0) return 0.0;
float p = 1048576.0 - (float)adc_P;
p = (p - var2 / 4096.0) * 6250.0 / var1;
var1 = (float)dig_P9 * p * p / 2147483648.0;
var2 = p * (float)dig_P8 / 32768.0;
p = p + (var1 + var2 + (float)dig_P7) / 16.0;
return p;
}
float bme280_comp_humi(int adc_H) {
float h = (float)t_fine - 76800.0;
if (h == 0.0) return 0.0;
h = ((float)adc_H - ((float)dig_H4 * 64.0 + (float)dig_H5 / 16384.0 * h)) *
((float)dig_H2 / 65536.0 * (1.0 + (float)dig_H6 / 67108864.0 * h *
(1.0 + (float)dig_H3 / 67108864.0 * h)));
h = h * (1.0 - (float)dig_H1 * h / 524288.0);
if (h > 100.0) h = 100.0;
if (h < 0.0) h = 0.0;
return h;
}
// ════════════════════════════════════════════════════════════════════
// BME68x compensation (Bosch floating-point reference formulas)
// ════════════════════════════════════════════════════════════════════
float bme680_comp_temp(int adc_T) {
float v1 = ((float)adc_T / 16384.0 - (float)par_t1 / 1024.0) * (float)par_t2;
float d = (float)adc_T / 131072.0 - (float)par_t1 / 8192.0;
float v2 = d * d * (float)par_t3 * 16.0;
bme_tfine = v1 + v2;
return bme_tfine / 5120.0;
}
float bme680_comp_pres(int adc_P) {
float v1 = (bme_tfine / 2.0) - 64000.0;
float v2 = v1 * v1 * ((float)par_p6 / 131072.0);
v2 = v2 + (v1 * (float)par_p5 * 2.0);
v2 = (v2 / 4.0) + ((float)par_p4 * 65536.0);
v1 = (((float)par_p3 * v1 * v1 / 16384.0) + ((float)par_p2 * v1)) / 524288.0;
v1 = (1.0 + (v1 / 32768.0)) * (float)par_p1;
if (v1 == 0.0) return 0.0;
float p = 1048576.0 - (float)adc_P;
p = ((p - (v2 / 4096.0)) * 6250.0) / v1;
v1 = ((float)par_p9 * p * p) / 2147483648.0;
v2 = p * ((float)par_p8 / 32768.0);
float v3 = (p / 256.0) * (p / 256.0) * (p / 256.0) * ((float)par_p10 / 131072.0);
p = p + (v1 + v2 + v3 + ((float)par_p7 * 128.0)) / 16.0;
return p;
}
float bme680_comp_humi(int adc_H) {
float tc = bme_tfine / 5120.0;
float v1 = (float)adc_H - (((float)par_h1 * 16.0) + (((float)par_h3 / 2.0) * tc));
float v2 = v1 * (((float)par_h2 / 262144.0) * (1.0 + (((float)par_h4 / 16384.0) * tc) +
(((float)par_h5 / 1048576.0) * tc * tc)));
float v3 = (float)par_h6 / 16384.0;
float v4 = (float)par_h7 / 2097152.0;
float h = v2 + ((v3 + (v4 * tc)) * v2 * v2);
if (h > 100.0) h = 100.0;
if (h < 0.0) h = 0.0;
return h;
}
// Gas-range lookup constants (BME680 variant 0 only)
float bme680_lk1(int r) {
if (r == 5) return -1.0;
if (r == 7) return -0.8;
if (r == 10) return -0.2;
if (r == 11) return -0.5;
if (r == 13) return -1.0;
return 0.0;
}
float bme680_lk2(int r) {
if (r == 4) return 0.1;
if (r == 5) return 0.7;
if (r == 7) return -0.8;
if (r == 8) return -0.1;
return 0.0;
}
// Gas resistance in Ohm
float bme680_comp_gas(int gas_adc, int gas_range) {
if (bmx_variant != 0) {
// BME688 / BME690 (variant 1)
int iv1 = 262144 >> gas_range;
float v2 = ((float)gas_adc - 512.0) * 3.0 + 4096.0;
return 1000000.0 * (float)iv1 / v2;
}
// BME680 (variant 0)
float v1 = 1340.0 + 5.0 * (float)range_sw_err;
float v2 = v1 * (1.0 + bme680_lk1(gas_range) / 100.0);
float v3 = 1.0 + bme680_lk2(gas_range) / 100.0;
return 1.0 / (v3 * 0.000000125 * (float)(1 << gas_range) *
((((float)gas_adc - 512.0) / v2) + 1.0));
}
// Dewpoint (Magnus formula), returns °C
float bmx_calc_dewpoint(float t, float h) {
if (h <= 0.0) return 0.0;
float gamma = (17.271 * t) / (237.7 + t) + log(h / 100.0);
return (237.7 * gamma) / (17.271 - gamma);
}
// Absolute humidity in g/m³
float bmx_calc_abshumi(float t, float h) {
float ah = 6.112 * exp((17.67 * t) / (t + 243.5)) * h * 2.1674;
return ah / (273.15 + t);
}
// ════════════════════════════════════════════════════════════════════
// BME68x forced-mode service: read the completed measurement, retrigger
// ════════════════════════════════════════════════════════════════════
void bme680_service() {
int st = i2cRead8(bmx_addr, 0x1D, bmx_bus); // meas_status_0
if (st & 0x80) { // new_data_0
// 15 bytes from 0x1D..0x2B
if (i2cRead(bmx_addr, 0x1D, bmx_buf, 15, bmx_bus)) {
int adc_P = (bmx_buf[2] << 12) | (bmx_buf[3] << 4) | (bmx_buf[4] >> 4);
int adc_T = (bmx_buf[5] << 12) | (bmx_buf[6] << 4) | (bmx_buf[7] >> 4);
int adc_H = (bmx_buf[8] << 8) | bmx_buf[9];
int gas_adc = (bmx_buf[13] << 2) | (bmx_buf[14] >> 6);
int gas_range = bmx_buf[14] & 0x0F;
bmx_gas_valid = (bmx_buf[14] >> 5) & 1;
bmx_temp = bme680_comp_temp(adc_T);
bmx_pres = bme680_comp_pres(adc_P) / 100.0;
bmx_humi = bme680_comp_humi(adc_H);
bmx_dewp = bmx_calc_dewpoint(bmx_temp, bmx_humi);
bmx_absh = bmx_calc_abshumi(bmx_temp, bmx_humi);
if (bmx_gas_valid) bmx_gas = bme680_comp_gas(gas_adc, gas_range) / 1000.0;
bmx_ok = 1;
}
}
// Kick off the next forced measurement (os_t x1, os_p x1, forced)
i2cWrite8(bmx_addr, 0x74, 0x25, bmx_bus);
}
void EverySecond() {
if (!bmx_addr) {
if (!bmx_scan()) { bmx_ok = 0; return; }
if (!bmx_read_calib()) { bmx_ok = 0; bmx_addr = 0; return; }
if (!bmx_configure()) { bmx_ok = 0; bmx_addr = 0; return; }
}
if (bmx_is_680) {
bme680_service();
} else {
int rlen = 6;
if (bmx_has_humi) rlen = 8;
if (!i2cRead(bmx_addr, 0xF7, bmx_buf, rlen, bmx_bus)) {
bmx_ok = 0;
bmx_addr = 0;
return;
}
int adc_P = (bmx_buf[0] << 12) | (bmx_buf[1] << 4) | (bmx_buf[2] >> 4);
int adc_T = (bmx_buf[3] << 12) | (bmx_buf[4] << 4) | (bmx_buf[5] >> 4);
int T100 = bme280_comp_temp(adc_T);
bmx_temp = (float)T100 / 100.0;
bmx_pres = bme280_comp_pres(adc_P) / 100.0;
if (bmx_has_humi) {
int adc_H = (bmx_buf[6] << 8) | bmx_buf[7];
bmx_humi = bme280_comp_humi(adc_H);
bmx_dewp = bmx_calc_dewpoint(bmx_temp, bmx_humi);
bmx_absh = bmx_calc_abshumi(bmx_temp, bmx_humi);
}
bmx_ok = 1;
}
#ifdef USE_CHARTS
hist_tick++;
if (hist_tick >= 60) {
hist_tick = 0;
hist_temp[hist_pos % CHART_LEN] = bmx_temp;
hist_pres[hist_pos % CHART_LEN] = bmx_pres;
if (bmx_has_humi) hist_humi[hist_pos % CHART_LEN] = bmx_humi;
if (bmx_has_gas) hist_gas[hist_pos % CHART_LEN] = bmx_gas;
hist_pos++;
}
#endif
}
#ifdef USE_CHARTS
void WebPage() {
int n = hist_pos;
if (n > CHART_LEN) n = CHART_LEN;
if (n > 0) {
webSend("<div style='margin-left:-30px'>");
WebChart('l', "Temperature", "°C", 0xe74c3c, hist_pos, CHART_LEN, hist_temp, 1, 0, 0, 0);
WebChart('l', "Pressure", "hPa", 0x27ae60, hist_pos, CHART_LEN, hist_pres, 1, 0, 0, 0);
if (bmx_has_humi) {
WebChart('l', "Humidity", "%RH", 0x3498db, hist_pos, CHART_LEN, hist_humi, 1, 0, 0, 0);
}
if (bmx_has_gas) {
WebChart('l', "Gas", "kOhm", 0xe67e22, hist_pos, CHART_LEN, hist_gas, 1, 0, 0, 0);
}
webSend("</div>");
}
}
#endif
void bmx_web_label(int idx) {
char vt[32];
LGetString(idx, bmx_lbl);
sprintf(vt, "{s}%s %s{m}", bmx_name, bmx_lbl);
webSend(vt);
}
void WebCall() {
char vt[32];
if (bmx_ok) {
bmx_web_label(0);
sprintf(vt, "%.1f °C{e}", bmx_temp);
webSend(vt);
bmx_web_label(2);
sprintf(vt, "%.1f hPa{e}", bmx_pres);
webSend(vt);
if (bmx_has_humi) {
bmx_web_label(1);
sprintf(vt, "%.1f %{e}", bmx_humi);
webSend(vt);
bmx_web_label(3);
sprintf(vt, "%.1f °C{e}", bmx_dewp);
webSend(vt);
bmx_web_label(20);
sprintf(vt, "%.1f g/m³{e}", bmx_absh);
webSend(vt);
}
if (bmx_has_gas) {
sprintf(vt, "{s}%s Gas{m}", bmx_name);
webSend(vt);
sprintf(vt, "%.1f kΩ{e}", bmx_gas);
webSend(vt);
}
} else {
webSend("{s}BMx280{m}not found{e}");
}
}
void JsonCall() {
if (!bmx_ok) return;
char buf[96];
sprintf(buf, ",\"%s\":{", bmx_name);
responseAppend(buf);
sprintf(buf, "\"Temperature\":%.1f", bmx_temp);
responseAppend(buf);
sprintf(buf, ",\"Pressure\":%.1f", bmx_pres);
responseAppend(buf);
if (bmx_has_humi) {
sprintf(buf, ",\"Humidity\":%.1f", bmx_humi);
responseAppend(buf);
sprintf(buf, ",\"DewPoint\":%.1f", bmx_dewp);
responseAppend(buf);
sprintf(buf, ",\"AbsHumidity\":%.1f", bmx_absh);
responseAppend(buf);
}
if (bmx_has_gas) {
sprintf(buf, ",\"Gas\":%.1f", bmx_gas); // kOhm
responseAppend(buf);
}
responseAppend("}");
}
// Called before VM stops — release I2C address so driver can restart
void OnExit() {
if (bmx_addr) {
I2cResetActive(bmx_addr, bmx_bus);
bmx_addr = 0;
}
}
int main() {
char buf[64];
bmx_ok = 0;
bmx_addr = 0;
#ifdef USE_CHARTS
hist_pos = 0;
hist_tick = 0;
#endif
if (bmx_scan()) {
strcpy(buf, bmx_name);
sprintfAppend(buf, " found at 0x%x on bus %d", bmx_addr, bmx_bus);
addLog(buf);
if (bmx_read_calib() && bmx_configure()) {
addLog("Calibration loaded, sensor active");
} else {
addLog("Calibration/config failed");
bmx_addr = 0;
}
} else {
addLog("BMx280/BME68x not found on any bus");
}
return 0;
}