#include "BME68x_manager.h" #include "driver/i2c.h" #include "driver/gpio.h" #include "esp_log.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" #include "config.h" #include static const char *TAG = "BME68x_Manager"; // BME68x calibration coefficients structure typedef struct { uint16_t par_t1; int16_t par_t2; int8_t par_t3; uint16_t par_p1; int16_t par_p2; int8_t par_p3; int16_t par_p4; int16_t par_p5; int8_t par_p6; int8_t par_p7; int16_t par_p8; int16_t par_p9; uint8_t par_p10; uint16_t par_h1; uint16_t par_h2; int8_t par_h3; int8_t par_h4; int8_t par_h5; uint8_t par_h6; int8_t par_h7; int8_t par_gh1; int16_t par_gh2; int8_t par_gh3; uint8_t res_heat_range; int8_t res_heat_val; int8_t range_sw_err; int32_t t_fine; } BME68x_calib_data_t; // Global calibration data and current I2C address static BME68x_calib_data_t calib_data; static uint8_t current_i2c_addr = BME68x_I2C_ADDR_PRIMARY; // Static function declarations static esp_err_t BME68x_register_read(uint8_t reg_addr, uint8_t *data, size_t len); static esp_err_t BME68x_register_write_byte(uint8_t reg_addr, uint8_t data); static esp_err_t BME68x_get_calib_data(void); static float BME68x_calc_temperature(uint32_t temp_adc); static float BME68x_calc_pressure(uint32_t pres_adc); static float BME68x_calc_humidity(uint16_t hum_adc); static float BME68x_calc_gas_resistance(uint16_t gas_res_adc, uint8_t gas_range); static uint8_t BME68x_calc_heater_res(uint16_t temp); static uint8_t BME68x_calc_air_quality_score(float gas_resistance, float humidity); /** * @brief i2c master initialization */ esp_err_t bme68x_i2c_init(void) { int i2c_master_port = I2C_MASTER_NUM; i2c_config_t conf = { .mode = I2C_MODE_MASTER, .sda_io_num = I2C_MASTER_SDA_IO, .scl_io_num = I2C_MASTER_SCL_IO, .sda_pullup_en = GPIO_PULLUP_ENABLE, .scl_pullup_en = GPIO_PULLUP_ENABLE, .master.clk_speed = I2C_MASTER_FREQ_HZ, }; esp_err_t ret = i2c_param_config(i2c_master_port, &conf); if (ret != ESP_OK) { ESP_LOGE(TAG, "I2C param config failed: %s", esp_err_to_name(ret)); return ret; } ret = i2c_driver_install(i2c_master_port, conf.mode, I2C_MASTER_RX_BUF_DISABLE, I2C_MASTER_TX_BUF_DISABLE, 0); if (ret != ESP_OK) { ESP_LOGE(TAG, "I2C driver install failed: %s", esp_err_to_name(ret)); } return ret; } /** * @brief Read a sequence of bytes from a BME68x sensor registers */ static esp_err_t BME68x_register_read(uint8_t reg_addr, uint8_t *data, size_t len) { return i2c_master_write_read_device(I2C_MASTER_NUM, current_i2c_addr, ®_addr, 1, data, len, I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS); } /** * @brief Write a byte to a BME68x sensor register */ static esp_err_t BME68x_register_write_byte(uint8_t reg_addr, uint8_t data) { uint8_t write_buf[2] = {reg_addr, data}; return i2c_master_write_to_device(I2C_MASTER_NUM, current_i2c_addr, write_buf, sizeof(write_buf), I2C_MASTER_TIMEOUT_MS / portTICK_PERIOD_MS); } /** * @brief Perform simple I2C bus recovery */ esp_err_t bme68x_i2c_recovery(void) { ESP_LOGI(TAG, "Performing I2C bus recovery..."); // Simple approach: just reset the pins briefly gpio_reset_pin(I2C_MASTER_SDA_IO); gpio_reset_pin(I2C_MASTER_SCL_IO); // Configure pins as outputs temporarily gpio_set_direction(I2C_MASTER_SCL_IO, GPIO_MODE_OUTPUT); gpio_set_direction(I2C_MASTER_SDA_IO, GPIO_MODE_OUTPUT); gpio_set_pull_mode(I2C_MASTER_SCL_IO, GPIO_PULLUP_ONLY); gpio_set_pull_mode(I2C_MASTER_SDA_IO, GPIO_PULLUP_ONLY); // Set both pins high gpio_set_level(I2C_MASTER_SCL_IO, 1); gpio_set_level(I2C_MASTER_SDA_IO, 1); vTaskDelay(pdMS_TO_TICKS(10)); // Send some clock pulses for (int i = 0; i < 9; i++) { gpio_set_level(I2C_MASTER_SCL_IO, 0); vTaskDelay(pdMS_TO_TICKS(1)); gpio_set_level(I2C_MASTER_SCL_IO, 1); vTaskDelay(pdMS_TO_TICKS(1)); } // Reset pins back to default gpio_reset_pin(I2C_MASTER_SDA_IO); gpio_reset_pin(I2C_MASTER_SCL_IO); vTaskDelay(pdMS_TO_TICKS(50)); ESP_LOGI(TAG, "I2C bus recovery completed"); return ESP_OK; } /** * @brief Scan I2C bus for devices */ void bme68x_i2c_scan(void) { ESP_LOGI(TAG, "Scanning I2C bus..."); printf("Scanning I2C bus for devices:\n"); int devices_found = 0; // Scan only common I2C addresses to avoid null address errors uint8_t common_addresses[] = {0x76, 0x77, 0x3C, 0x3D, 0x48, 0x49, 0x4A, 0x4B, 0x68, 0x69, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75}; int num_addresses = sizeof(common_addresses) / sizeof(common_addresses[0]); for (int i = 0; i < num_addresses; i++) { uint8_t addr = common_addresses[i]; esp_err_t ret = i2c_master_write_to_device(I2C_MASTER_NUM, addr, NULL, 0, 100 / portTICK_PERIOD_MS); if (ret == ESP_OK) { printf(" Device found at address 0x%02X\n", addr); ESP_LOGI(TAG, "I2C device found at address 0x%02X", addr); devices_found++; } } if (devices_found == 0) { printf(" No I2C devices found at common addresses!\n"); ESP_LOGW(TAG, "No I2C devices found at common addresses"); printf(" (Scanned: 0x76, 0x77, 0x3C, 0x3D, 0x48-0x4B, 0x68-0x75)\n"); } else { printf(" Found %d I2C device(s)\n", devices_found); ESP_LOGI(TAG, "Found %d I2C device(s) on bus", devices_found); } } /** * @brief Software reset BME68x sensor */ esp_err_t bme68x_soft_reset(void) { ESP_LOGI(TAG, "Performing BME68x soft reset..."); // BME68x soft reset command esp_err_t ret = BME68x_register_write_byte(0xE0, 0xB6); // Reset register with reset command if (ret == ESP_OK) { vTaskDelay(pdMS_TO_TICKS(10)); // Wait for reset to complete ESP_LOGI(TAG, "BME68x soft reset completed"); } else { ESP_LOGW(TAG, "BME68x soft reset failed, but continuing..."); } return ret; } /** * @brief Test BME68x sensor connection and read chip ID with recovery attempts */ esp_err_t bme68x_test_connection(void) { uint8_t chip_id; esp_err_t ret; int retry_count = 0; const int max_retries = 3; while (retry_count < max_retries) { ESP_LOGI(TAG, "BME68x connection attempt %d/%d", retry_count + 1, max_retries); // Try primary address first current_i2c_addr = BME68x_I2C_ADDR_PRIMARY; ret = BME68x_register_read(BME68x_REG_CHIP_ID, &chip_id, 1); if (ret != ESP_OK) { ESP_LOGI(TAG, "Failed to read from primary address 0x%02X, trying secondary address...", BME68x_I2C_ADDR_PRIMARY); // Try secondary address current_i2c_addr = BME68x_I2C_ADDR_SECONDARY; ret = BME68x_register_read(BME68x_REG_CHIP_ID, &chip_id, 1); } if (ret == ESP_OK) { if (chip_id == BME68x_CHIP_ID) { ESP_LOGI(TAG, "BME68x sensor found! Chip ID: 0x%02X (Address: 0x%02X)", chip_id, current_i2c_addr); return ESP_OK; } else { ESP_LOGW(TAG, "Unexpected chip ID: 0x%02X (expected 0x%02X)", chip_id, BME68x_CHIP_ID); ret = ESP_FAIL; } } else { ESP_LOGE(TAG, "Failed to communicate with BME68x sensor (attempt %d)", retry_count + 1); } // If communication failed, try recovery if (ret != ESP_OK && retry_count < max_retries - 1) { ESP_LOGI(TAG, "Attempting I2C bus recovery..."); bme68x_i2c_recovery(); vTaskDelay(pdMS_TO_TICKS(100)); // Wait after recovery } retry_count++; } ESP_LOGE(TAG, "Failed to communicate with BME68x sensor after %d attempts", max_retries); return ret; } /** * @brief Read calibration coefficients from BME68x */ static esp_err_t BME68x_get_calib_data(void) { uint8_t coeff_array[41]; esp_err_t ret; // Read first set of coefficients (0x89 to 0xA1) ret = BME68x_register_read(BME68x_COEFF_ADDR1, coeff_array, 25); if (ret != ESP_OK) return ret; // Read second set of coefficients (0xE1 to 0xF0) ret = BME68x_register_read(BME68x_COEFF_ADDR2, &coeff_array[25], 16); if (ret != ESP_OK) return ret; // Temperature coefficients calib_data.par_t1 = (uint16_t)(((uint16_t)coeff_array[34] << 8) | coeff_array[33]); calib_data.par_t2 = (int16_t)(((uint16_t)coeff_array[2] << 8) | coeff_array[1]); calib_data.par_t3 = (int8_t)coeff_array[3]; // Pressure coefficients calib_data.par_p1 = (uint16_t)(((uint16_t)coeff_array[6] << 8) | coeff_array[5]); calib_data.par_p2 = (int16_t)(((uint16_t)coeff_array[8] << 8) | coeff_array[7]); calib_data.par_p3 = (int8_t)coeff_array[9]; calib_data.par_p4 = (int16_t)(((uint16_t)coeff_array[12] << 8) | coeff_array[11]); calib_data.par_p5 = (int16_t)(((uint16_t)coeff_array[14] << 8) | coeff_array[13]); calib_data.par_p6 = (int8_t)coeff_array[16]; calib_data.par_p7 = (int8_t)coeff_array[15]; calib_data.par_p8 = (int16_t)(((uint16_t)coeff_array[20] << 8) | coeff_array[19]); calib_data.par_p9 = (int16_t)(((uint16_t)coeff_array[22] << 8) | coeff_array[21]); calib_data.par_p10 = (uint8_t)coeff_array[23]; // Humidity coefficients calib_data.par_h1 = (uint16_t)(((uint16_t)coeff_array[27] << 4) | (coeff_array[26] & 0x0F)); calib_data.par_h2 = (uint16_t)(((uint16_t)coeff_array[25] << 4) | (coeff_array[26] >> 4)); calib_data.par_h3 = (int8_t)coeff_array[28]; calib_data.par_h4 = (int8_t)coeff_array[29]; calib_data.par_h5 = (int8_t)coeff_array[30]; calib_data.par_h6 = (uint8_t)coeff_array[31]; calib_data.par_h7 = (int8_t)coeff_array[32]; // Gas sensor coefficients calib_data.par_gh1 = (int8_t)coeff_array[37]; calib_data.par_gh2 = (int16_t)(((uint16_t)coeff_array[36] << 8) | coeff_array[35]); calib_data.par_gh3 = (int8_t)coeff_array[38]; // Other coefficients for gas sensor uint8_t temp_var; ret = BME68x_register_read(0x02, &temp_var, 1); if (ret != ESP_OK) return ret; calib_data.res_heat_range = ((temp_var & 0x30) >> 4); ret = BME68x_register_read(0x00, &temp_var, 1); if (ret != ESP_OK) return ret; calib_data.res_heat_val = (int8_t)temp_var; ret = BME68x_register_read(0x04, &temp_var, 1); if (ret != ESP_OK) return ret; calib_data.range_sw_err = ((int8_t)temp_var & 0xF0) >> 4; return ESP_OK; } /** * @brief Calculate temperature from raw ADC value */ static float BME68x_calc_temperature(uint32_t temp_adc) { int64_t var1, var2, var3; float calc_temp; var1 = ((int32_t)temp_adc >> 3) - ((int32_t)calib_data.par_t1 << 1); var2 = (var1 * (int32_t)calib_data.par_t2) >> 11; var3 = ((var1 >> 1) * (var1 >> 1)) >> 12; var3 = ((var3) * ((int32_t)calib_data.par_t3 << 4)) >> 14; calib_data.t_fine = (int32_t)(var2 + var3); calc_temp = (((float)calib_data.t_fine * 5) + 128) / 256; return calc_temp / 100.0f; } /** * @brief Calculate pressure from raw ADC value */ static float BME68x_calc_pressure(uint32_t pres_adc) { int32_t var1, var2, var3; uint32_t calc_pres; var1 = (((int32_t)calib_data.t_fine) >> 1) - 64000; var2 = ((((var1 >> 2) * (var1 >> 2)) >> 11) * (int32_t)calib_data.par_p6) >> 2; var2 = var2 + ((var1 * (int32_t)calib_data.par_p5) << 1); var2 = (var2 >> 2) + ((int32_t)calib_data.par_p4 << 16); var1 = (((((var1 >> 2) * (var1 >> 2)) >> 13) * ((int32_t)calib_data.par_p3 << 5)) >> 3) + (((int32_t)calib_data.par_p2 * var1) >> 1); var1 = var1 >> 18; var1 = ((32768 + var1) * (int32_t)calib_data.par_p1) >> 15; calc_pres = 1048576 - pres_adc; calc_pres = ((calc_pres - (var2 >> 12)) * ((uint32_t)3125)); if (calc_pres >= (1 << 30)) calc_pres = (calc_pres / var1) * 2; else calc_pres = (calc_pres * 2) / var1; var1 = ((int32_t)calib_data.par_p9 * (int32_t)(((calc_pres >> 3) * (calc_pres >> 3)) >> 13)) >> 12; var2 = ((int32_t)(calc_pres >> 2) * (int32_t)calib_data.par_p8) >> 13; var3 = ((int32_t)(calc_pres >> 8) * (int32_t)(calc_pres >> 8) * (int32_t)(calc_pres >> 8) * (int32_t)calib_data.par_p10) >> 17; calc_pres = (calc_pres) + ((var1 + var2 + var3 + ((int32_t)calib_data.par_p7 << 7)) >> 4); return (float)calc_pres / 100.0f; // Convert Pa to hPa } /** * @brief Calculate humidity from raw ADC value */ static float BME68x_calc_humidity(uint16_t hum_adc) { int32_t var1, var2, var3, var4, var5, var6, temp_scaled, calc_hum; temp_scaled = (((int32_t)calib_data.t_fine * 5) + 128) >> 8; var1 = (int32_t)(hum_adc - ((int32_t)((int32_t)calib_data.par_h1 * 16))) - (((temp_scaled * (int32_t)calib_data.par_h3) / ((int32_t)100)) >> 1); var2 = ((int32_t)calib_data.par_h2 * (((temp_scaled * (int32_t)calib_data.par_h4) / ((int32_t)100)) + (((temp_scaled * ((temp_scaled * (int32_t)calib_data.par_h5) / ((int32_t)100))) >> 6) / ((int32_t)100)) + (int32_t)(1 << 14))) >> 10; var3 = var1 * var2; var4 = (int32_t)calib_data.par_h6 << 7; var4 = ((var4) + ((temp_scaled * (int32_t)calib_data.par_h7) / ((int32_t)100))) >> 4; var5 = ((var3 >> 14) * (var3 >> 14)) >> 10; var6 = (var4 * var5) >> 1; calc_hum = (((var3 + var6) >> 10) * ((int32_t)1000)) >> 12; if (calc_hum > 100000) /* Cap at 100%rH */ calc_hum = 100000; else if (calc_hum < 0) calc_hum = 0; return calc_hum / 1000.0f; } /** * @brief Calculate gas resistance from raw ADC value */ static float BME68x_calc_gas_resistance(uint16_t gas_res_adc, uint8_t gas_range) { int64_t var1; uint64_t var2; int64_t var3; uint32_t calc_gas_res; uint32_t lookupTable1[16] = {UINT32_C(2147483647), UINT32_C(2147483647), UINT32_C(2147483647), UINT32_C(2147483647), UINT32_C(2147483647), UINT32_C(2126008810), UINT32_C(2147483647), UINT32_C(2130303777), UINT32_C(2147483647), UINT32_C(2147483647), UINT32_C(2143188679), UINT32_C(2136746228), UINT32_C(2147483647), UINT32_C(2126008810), UINT32_C(2147483647), UINT32_C(2147483647)}; uint32_t lookupTable2[16] = {UINT32_C(4096000000), UINT32_C(2048000000), UINT32_C(1024000000), UINT32_C(512000000), UINT32_C(255744255), UINT32_C(127110228), UINT32_C(64000000), UINT32_C(32258064), UINT32_C(16016016), UINT32_C(8000000), UINT32_C(4000000), UINT32_C(2000000), UINT32_C(1000000), UINT32_C(500000), UINT32_C(250000), UINT32_C(125000)}; var1 = (int64_t)((1340 + (5 * (int64_t)calib_data.range_sw_err)) * ((int64_t)lookupTable1[gas_range])) >> 16; var2 = (((int64_t)((int64_t)gas_res_adc << 15) - (int64_t)(16777216)) + var1); var3 = (((int64_t)lookupTable2[gas_range] * (int64_t)var1) >> 9); calc_gas_res = (uint32_t)((var3 + ((int64_t)var2 >> 1)) / (int64_t)var2); return (float)calc_gas_res; } /** * @brief Calculate heater resistance for target temperature */ static uint8_t BME68x_calc_heater_res(uint16_t temp) { float var1, var2, var3, var4, var5; uint8_t res_heat; if (temp < 200) /* Cap temperature */ temp = 200; else if (temp > 400) temp = 400; var1 = (((float)calib_data.par_gh1 / (16.0f)) + 49.0f); var2 = ((((float)calib_data.par_gh2 / (32768.0f)) * (0.0005f)) + 0.00235f); var3 = ((float)calib_data.par_gh3 / (1024.0f)); var4 = (var1 * (1.0f + (var2 * (float)temp))); var5 = (var4 + (var3 * (float)25.0f)); res_heat = (uint8_t)(3.4f * ((var5 * (4 / (4 + (float)calib_data.res_heat_range)) * (1 / (1 + ((float)calib_data.res_heat_val * 0.002f)))) - 25)); return res_heat; } /** * @brief Calculate air quality score based on gas resistance */ static uint8_t BME68x_calc_air_quality_score(float gas_resistance, float humidity) { float hum_baseline = 40.0f, hum_weighting = 0.15f; float gas_baseline = 80000.0f, gas_weighting = 0.85f; // Calculate humidity contribution to IAQ index float hum_score = 0.0f; if (humidity >= 38 && humidity <= 42) hum_score = hum_weighting * 100; // Humidity +/-5% around optimum else { if (humidity < 38) hum_score = (hum_weighting / hum_baseline) * humidity * 100; else hum_score = hum_weighting * 100 * (1.0f - ((humidity - 42) / (100 - 42))); if (hum_score < 0) hum_score = 0; // Don't go negative } // Calculate gas contribution to IAQ index float gas_score = (gas_weighting / gas_baseline) * gas_resistance * 100; // Combine results for the final IAQ index value (0-100% where 100% is good quality air) float air_quality_score = hum_score + gas_score; if (air_quality_score > 100) air_quality_score = 100; if (air_quality_score < 0) air_quality_score = 0; return (uint8_t)air_quality_score; } /** * @brief Initialize BME68x sensor */ esp_err_t bme68x_init(void) { esp_err_t ret; uint8_t reg_data; // Read calibration data ret = BME68x_get_calib_data(); if (ret != ESP_OK) return ret; // Set humidity oversampling to 2x ret = BME68x_register_write_byte(BME68x_REG_CTRL_HUM, BME68x_OS_2X << BME68x_OSH_SEL); if (ret != ESP_OK) return ret; // Set temperature oversampling to 8x and pressure oversampling to 4x reg_data = (BME68x_OS_8X << BME68x_OST_SEL) | (BME68x_OS_4X << BME68x_OSP_SEL); ret = BME68x_register_write_byte(BME68x_REG_CTRL_MEAS, reg_data); if (ret != ESP_OK) return ret; // Set IIR filter to 3 ret = BME68x_register_write_byte(BME68x_REG_CONFIG, BME68x_FILTER_SIZE_3 << BME68x_FILTER_SEL); if (ret != ESP_OK) return ret; // Enable gas sensor uint8_t heater_res = BME68x_calc_heater_res(320); // Set heater to 320°C ret = BME68x_register_write_byte(BME68x_REG_RES_HEAT_0, heater_res); if (ret != ESP_OK) return ret; // Set gas wait time (approx 150ms) ret = BME68x_register_write_byte(BME68x_REG_GAS_WAIT_0, 0x59); if (ret != ESP_OK) return ret; // Enable gas measurements reg_data = (BME68x_ENABLE_GAS_MEAS << BME68x_RUN_GAS_SEL) | (0 << BME68x_NB_CONV_SEL); ret = BME68x_register_write_byte(BME68x_REG_CTRL_GAS_1, reg_data); if (ret != ESP_OK) return ret; return ESP_OK; } /** * @brief Trigger measurement and read sensor data */ esp_err_t bme68x_read_data(BME68x_data_t *data) { esp_err_t ret; uint8_t reg_data; uint8_t buff[8]; uint32_t adc_temp, adc_pres; uint16_t adc_hum; // Trigger forced mode measurement ret = BME68x_register_read(BME68x_REG_CTRL_MEAS, ®_data, 1); if (ret != ESP_OK) return ret; reg_data = (reg_data & 0xFC) | 0x01; // Set mode to forced ret = BME68x_register_write_byte(BME68x_REG_CTRL_MEAS, reg_data); if (ret != ESP_OK) return ret; // Wait for measurement to complete do { vTaskDelay(pdMS_TO_TICKS(10)); ret = BME68x_register_read(BME68x_REG_MEAS_STATUS_0, ®_data, 1); if (ret != ESP_OK) return ret; } while (reg_data & 0x80); // Wait for measuring bit to clear // Read measurement data ret = BME68x_register_read(BME68x_REG_PRESS_MSB, buff, 8); if (ret != ESP_OK) return ret; // Extract ADC values adc_pres = (uint32_t)buff[0] << 12 | (uint32_t)buff[1] << 4 | (uint32_t)buff[2] >> 4; adc_temp = (uint32_t)buff[3] << 12 | (uint32_t)buff[4] << 4 | (uint32_t)buff[5] >> 4; adc_hum = (uint16_t)buff[6] << 8 | (uint16_t)buff[7]; // Read gas sensor data uint8_t gas_buff[2]; uint16_t adc_gas; uint8_t gas_range; ret = BME68x_register_read(BME68x_REG_GAS_R_MSB, gas_buff, 2); if (ret != ESP_OK) return ret; adc_gas = (uint16_t)gas_buff[0] << 2 | (uint16_t)gas_buff[1] >> 6; gas_range = gas_buff[1] & 0x0F; // Check gas measurement validity data->gas_valid = (gas_buff[1] & 0x20) >> 5; data->heat_stab = (gas_buff[1] & 0x10) >> 4; // Calculate actual values data->temperature = BME68x_calc_temperature(adc_temp) + TEMP_OFFSET; data->pressure = BME68x_calc_pressure(adc_pres); data->humidity = BME68x_calc_humidity(adc_hum); data->gas_resistance = BME68x_calc_gas_resistance(adc_gas, gas_range); // Calculate air quality score data->air_quality_score = BME68x_calc_air_quality_score(data->gas_resistance, data->humidity); return ESP_OK; } /** * @brief Get current I2C address being used */ uint8_t bme68x_get_current_address(void) { return current_i2c_addr; }