From 22d39faf5fa085b09014e2fa7d98275304e9bba8 Mon Sep 17 00:00:00 2001 From: Phil Howard Date: Tue, 11 May 2021 17:31:38 +0100 Subject: [PATCH] Bringup BME688 and BME680 sensors This changeset brings the BOSCH BME68X Sensor API library in as a submodule and makes it buildable with CMake. A thin wrapper- the BME68X driver- provides simple init, configure, read_forced and read_parallel functions. Two BME688 examples are available for forced-mode and parallel-mode operation. --- .github/workflows/cmake.yml | 4 +- .gitmodules | 3 + drivers/CMakeLists.txt | 1 + drivers/bme68x/CMakeLists.txt | 1 + drivers/bme68x/bme68x.cmake | 17 +++ drivers/bme68x/bme68x.cpp | 119 ++++++++++++++++++ drivers/bme68x/bme68x.hpp | 103 +++++++++++++++ drivers/bme68x/src | 1 + examples/CMakeLists.txt | 1 + examples/breakout_bme688/CMakeLists.txt | 2 + examples/breakout_bme688/bme688_forced.cmake | 12 ++ examples/breakout_bme688/bme688_forced.cpp | 47 +++++++ .../breakout_bme688/bme688_parallel.cmake | 12 ++ examples/breakout_bme688/bme688_parallel.cpp | 66 ++++++++++ 14 files changed, 388 insertions(+), 1 deletion(-) create mode 100644 drivers/bme68x/CMakeLists.txt create mode 100644 drivers/bme68x/bme68x.cmake create mode 100644 drivers/bme68x/bme68x.cpp create mode 100644 drivers/bme68x/bme68x.hpp create mode 160000 drivers/bme68x/src create mode 100644 examples/breakout_bme688/CMakeLists.txt create mode 100644 examples/breakout_bme688/bme688_forced.cmake create mode 100644 examples/breakout_bme688/bme688_forced.cpp create mode 100644 examples/breakout_bme688/bme688_parallel.cmake create mode 100644 examples/breakout_bme688/bme688_parallel.cpp diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 291666b7..faa9b161 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -27,6 +27,8 @@ jobs: steps: - uses: actions/checkout@v2 + with: + submodules: true # Check out the Pico SDK - name: Checkout Pico SDK @@ -62,4 +64,4 @@ jobs: working-directory: ${{runner.workspace}}/build shell: bash run: | - cmake --build . --config $BUILD_TYPE -j 2 \ No newline at end of file + cmake --build . --config $BUILD_TYPE -j 2 diff --git a/.gitmodules b/.gitmodules index ec03a359..9e8742e4 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "micropython/modules/ulab"] path = micropython/modules/ulab url = https://github.com/v923z/micropython-ulab.git +[submodule "drivers/bme68x-sensor-api/src"] + path = drivers/bme68x/src + url = https://github.com/BoschSensortec/BME68x-Sensor-API diff --git a/drivers/CMakeLists.txt b/drivers/CMakeLists.txt index 617c999f..5fc5eb6b 100644 --- a/drivers/CMakeLists.txt +++ b/drivers/CMakeLists.txt @@ -14,5 +14,6 @@ add_subdirectory(fatfs) add_subdirectory(sdcard) add_subdirectory(as7262) add_subdirectory(bh1745) +add_subdirectory(bme68x) add_subdirectory(button) add_subdirectory(rgbled) diff --git a/drivers/bme68x/CMakeLists.txt b/drivers/bme68x/CMakeLists.txt new file mode 100644 index 00000000..ecb79109 --- /dev/null +++ b/drivers/bme68x/CMakeLists.txt @@ -0,0 +1 @@ +include(bme68x.cmake) \ No newline at end of file diff --git a/drivers/bme68x/bme68x.cmake b/drivers/bme68x/bme68x.cmake new file mode 100644 index 00000000..f1805d90 --- /dev/null +++ b/drivers/bme68x/bme68x.cmake @@ -0,0 +1,17 @@ +set(DRIVER_NAME bme68x) +add_library(${DRIVER_NAME} INTERFACE) + +target_sources(${DRIVER_NAME} INTERFACE + ${CMAKE_CURRENT_LIST_DIR}/src/bme68x.c + ${CMAKE_CURRENT_LIST_DIR}/bme68x.cpp +) + +target_include_directories(${DRIVER_NAME} INTERFACE ${CMAKE_CURRENT_LIST_DIR}) +target_include_directories(${DRIVER_NAME} INTERFACE ${CMAKE_CURRENT_LIST_DIR}/src) + +# Pull in pico libraries that we need +target_link_libraries(${DRIVER_NAME} INTERFACE pico_stdlib hardware_i2c pimoroni_i2c) + +# We can't control the uninitialized result variables in the BME68X API +# so demote unitialized to a warning for this target. +target_compile_options(${DRIVER_NAME} INTERFACE -Wno-error=uninitialized) \ No newline at end of file diff --git a/drivers/bme68x/bme68x.cpp b/drivers/bme68x/bme68x.cpp new file mode 100644 index 00000000..d6798826 --- /dev/null +++ b/drivers/bme68x/bme68x.cpp @@ -0,0 +1,119 @@ +#include "bme68x.hpp" +#include "pico/stdlib.h" + +namespace pimoroni { + bool BME68x::init() { + int8_t result = 0; + + if(interrupt != PIN_UNUSED) { + gpio_set_function(interrupt, GPIO_FUNC_SIO); + gpio_set_dir(interrupt, GPIO_IN); + gpio_pull_up(interrupt); + } + + device.intf_ptr = new i2c_intf_ptr{.i2c = i2c, .address = address}; + + device.intf = bme68x_intf::BME68X_I2C_INTF; + device.read = (bme68x_read_fptr_t)&read_bytes; + device.write = (bme68x_write_fptr_t)&write_bytes; + device.delay_us = (bme68x_delay_us_fptr_t)&delay_us; + device.amb_temp = 20; + + result = bme68x_init(&device); + bme68x_check_rslt("bme68x_init", result); + if(result != BME68X_OK) return false; + + result = bme68x_get_conf(&conf, &device); + bme68x_check_rslt("bme68x_get_conf", result); + if(result != BME68X_OK) return false; + + configure(BME68X_FILTER_OFF, BME68X_ODR_NONE, BME68X_OS_16X, BME68X_OS_1X, BME68X_OS_2X); + + return true; + } + + bool BME68x::configure(uint8_t filter, uint8_t odr, uint8_t os_humidity, uint8_t os_pressure, uint8_t os_temp) { + int8_t result; + + conf.filter = filter; + conf.odr = odr; + conf.os_hum = os_humidity; + conf.os_pres = os_pressure; + conf.os_temp = os_temp; + + bme68x_set_conf(&conf, &device); + bme68x_check_rslt("bme68x_set_conf", result); + if(result != BME68X_OK) return false; + + return true; + } + + bool BME68x::read_forced(bme68x_data *data) { + int8_t result = 0; + uint8_t n_fields; + uint32_t delay_period; + + heatr_conf.enable = BME68X_ENABLE; + heatr_conf.heatr_temp = 300; + heatr_conf.heatr_dur = 100; + result = bme68x_set_heatr_conf(BME68X_FORCED_MODE, &heatr_conf, &device); + bme68x_check_rslt("bme68x_set_heatr_conf", result); + if(result != BME68X_OK) return false; + + result = bme68x_set_op_mode(BME68X_FORCED_MODE, &device); + bme68x_check_rslt("bme68x_set_op_mode", result); + if(result != BME68X_OK) return false; + + delay_period = bme68x_get_meas_dur(BME68X_FORCED_MODE, &conf, &device) + (heatr_conf.heatr_dur * 1000); + // Could probably just call sleep_us here directly, I guess the API uses this internally + device.delay_us(delay_period, device.intf_ptr); + + result = bme68x_get_data(BME68X_FORCED_MODE, data, &n_fields, &device); + bme68x_check_rslt("bme68x_get_data", result); + if(result != BME68X_OK) return false; + + return true; + } + + /* + Will read profile_length results with the given temperatures and duration multipliers into the results array. + Blocks until it has a valid result for each temp/duration, and returns the entire set in the given order. + */ + bool BME68x::read_parallel(bme68x_data *results, uint16_t *profile_temps, uint16_t *profile_durations, size_t profile_length) { + int8_t result; + bme68x_data data[3]; // Parallel & Sequential mode read 3 simultaneous fields + uint8_t n_fields; + uint32_t delay_period; + + heatr_conf.enable = BME68X_ENABLE; + heatr_conf.heatr_temp_prof = profile_temps; + heatr_conf.heatr_dur_prof = profile_durations; + heatr_conf.profile_len = profile_length; + heatr_conf.shared_heatr_dur = 140 - (bme68x_get_meas_dur(BME68X_PARALLEL_MODE, &conf, &device) / 1000); + result = bme68x_set_heatr_conf(BME68X_PARALLEL_MODE, &heatr_conf, &device); + bme68x_check_rslt("bme68x_set_heatr_conf", result); + if(result != BME68X_OK) return false; + + result = bme68x_set_op_mode(BME68X_PARALLEL_MODE, &device); + bme68x_check_rslt("bme68x_set_op_mode", result); + if(result != BME68X_OK) return false; + + while (1) { + delay_period = bme68x_get_meas_dur(BME68X_PARALLEL_MODE, &conf, &device) + (heatr_conf.shared_heatr_dur * 1000); + device.delay_us(delay_period, device.intf_ptr); + + result = bme68x_get_data(BME68X_PARALLEL_MODE, data, &n_fields, &device); + if(result == BME68X_W_NO_NEW_DATA) continue; + bme68x_check_rslt("bme68x_get_data", result); + if(result != BME68X_OK) return false; + + for(auto i = 0u; i < n_fields; i++) { + results[data[i].gas_index] = data[i]; + + if(data[i].gas_index == profile_length - 1) return true; + } + } + + return true; + } +} \ No newline at end of file diff --git a/drivers/bme68x/bme68x.hpp b/drivers/bme68x/bme68x.hpp new file mode 100644 index 00000000..cd1d497c --- /dev/null +++ b/drivers/bme68x/bme68x.hpp @@ -0,0 +1,103 @@ +#pragma once + +#include "hardware/i2c.h" +#include "hardware/gpio.h" +#include "bme68x.h" +#include "bme68x_defs.h" +#include "common/pimoroni_i2c.hpp" +#include "stdio.h" + +namespace pimoroni { + class BME68x { + public: + static const uint8_t DEFAULT_I2C_ADDRESS = 0x76; + static const uint8_t ALTERNATE_I2C_ADDRESS = 0x77; + + struct i2c_intf_ptr { + I2C *i2c; + int8_t address; + }; + + bool debug = true; + + bool init(); + bool configure(uint8_t filter, uint8_t odr, uint8_t os_humidity, uint8_t os_pressure, uint8_t os_temp); + bool read_forced(bme68x_data *data); + bool read_parallel(bme68x_data *results, uint16_t *profile_temps, uint16_t *profile_durations, size_t profile_length); + + BME68x() : BME68x(new I2C()) {} + BME68x(uint8_t address, uint interrupt = PIN_UNUSED) : BME68x(new I2C(), address, interrupt) {} + BME68x(I2C *i2c, uint8_t address = DEFAULT_I2C_ADDRESS, uint interrupt = PIN_UNUSED) : i2c(i2c), address(address), interrupt(interrupt) {} + + // Bindings for bme68x_dev + static int write_bytes(uint8_t reg_addr, uint8_t *reg_data, uint32_t length, void *intf_ptr) { + BME68x::i2c_intf_ptr* i2c = (BME68x::i2c_intf_ptr *)intf_ptr; + + uint8_t buffer[length + 1]; + buffer[0] = reg_addr; + for(auto x = 0u; x < length; x++) { + buffer[x + 1] = reg_data[x]; + } + + int result = i2c->i2c->write_blocking(i2c->address, buffer, length + 1, false); + + return result == PICO_ERROR_GENERIC ? 1 : 0; + }; + + static int read_bytes(uint8_t reg_addr, uint8_t *reg_data, uint32_t length, void *intf_ptr) { + BME68x::i2c_intf_ptr* i2c = (BME68x::i2c_intf_ptr *)intf_ptr; + + int result = i2c->i2c->write_blocking(i2c->address, ®_addr, 1, true); + result = i2c->i2c->read_blocking(i2c->address, reg_data, length, false); + + return result == PICO_ERROR_GENERIC ? 1 : 0; + }; + + static void delay_us(uint32_t period, void *intf_ptr) { + sleep_us(period); + } + + /* From BME68X API examples/common/common.c */ + void bme68x_check_rslt(const char api_name[], int8_t rslt) + { + if(!debug) return; + switch (rslt) + { + case BME68X_OK: + /* Do nothing */ + break; + case BME68X_E_NULL_PTR: + printf("%s: Error [%d] : Null pointer\r\n", api_name, rslt); + break; + case BME68X_E_COM_FAIL: + printf("%s: Error [%d] : Communication failure\r\n", api_name, rslt); + break; + case BME68X_E_INVALID_LENGTH: + printf("%s: Error [%d] : Incorrect length parameter\r\n", api_name, rslt); + break; + case BME68X_E_DEV_NOT_FOUND: + printf("%s: Error [%d] : Device not found\r\n", api_name, rslt); + break; + case BME68X_E_SELF_TEST: + printf("%s: Error [%d] : Self test error\r\n", api_name, rslt); + break; + case BME68X_W_NO_NEW_DATA: + printf("%s: Warning [%d] : No new data found\r\n", api_name, rslt); + break; + default: + printf("%s: Error [%d] : Unknown error code\r\n", api_name, rslt); + break; + } + } + + private: + bme68x_dev device; + bme68x_conf conf; + bme68x_heatr_conf heatr_conf; + + I2C *i2c; + + int8_t address = DEFAULT_I2C_ADDRESS; + uint interrupt = I2C_DEFAULT_INT; + }; +} \ No newline at end of file diff --git a/drivers/bme68x/src b/drivers/bme68x/src new file mode 160000 index 00000000..a31906a4 --- /dev/null +++ b/drivers/bme68x/src @@ -0,0 +1 @@ +Subproject commit a31906a455fdde92e3ce5aeaa946ee4a20af5697 diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index cc18a6aa..59226faf 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -13,6 +13,7 @@ add_subdirectory(breakout_trackball) add_subdirectory(breakout_sgp30) add_subdirectory(breakout_colourlcd240x240) add_subdirectory(breakout_msa301) +add_subdirectory(breakout_bme688) add_subdirectory(pico_display) add_subdirectory(pico_unicorn) diff --git a/examples/breakout_bme688/CMakeLists.txt b/examples/breakout_bme688/CMakeLists.txt new file mode 100644 index 00000000..1d949221 --- /dev/null +++ b/examples/breakout_bme688/CMakeLists.txt @@ -0,0 +1,2 @@ +include("${CMAKE_CURRENT_LIST_DIR}/bme688_forced.cmake") +include("${CMAKE_CURRENT_LIST_DIR}/bme688_parallel.cmake") \ No newline at end of file diff --git a/examples/breakout_bme688/bme688_forced.cmake b/examples/breakout_bme688/bme688_forced.cmake new file mode 100644 index 00000000..444ba4a4 --- /dev/null +++ b/examples/breakout_bme688/bme688_forced.cmake @@ -0,0 +1,12 @@ +set(OUTPUT_NAME bme688_forced) + +add_executable( + ${OUTPUT_NAME} + ${OUTPUT_NAME}.cpp +) + +# Pull in pico libraries that we need +target_link_libraries(${OUTPUT_NAME} pico_stdlib bme68x) + +# create map/bin/hex file etc. +pico_add_extra_outputs(${OUTPUT_NAME}) diff --git a/examples/breakout_bme688/bme688_forced.cpp b/examples/breakout_bme688/bme688_forced.cpp new file mode 100644 index 00000000..491a56c8 --- /dev/null +++ b/examples/breakout_bme688/bme688_forced.cpp @@ -0,0 +1,47 @@ +#include +#include +#include "pico/stdlib.h" + +#include "bme68x.hpp" +#include "common/pimoroni_i2c.hpp" + +/* +Read a single reading from the BME688 +*/ + +using namespace pimoroni; + +I2C i2c(BOARD::BREAKOUT_GARDEN); +BME68x bme68x(&i2c); + +int main() { + gpio_init(PICO_DEFAULT_LED_PIN); + gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT); + + stdio_init_all(); + + bme68x.init(); + + while (1) { + sleep_ms(1000); + auto time_abs = get_absolute_time(); + auto time_ms = to_ms_since_boot(time_abs); + + bme68x_data data; + + auto result = bme68x.read_forced(&data); + (void)result; + + printf("%lu, %.2f, %.2f, %.2f, %.2f, 0x%x, %d, %d\n", + (long unsigned int)time_ms, + data.temperature, + data.pressure, + data.humidity, + data.gas_resistance, + data.status, + data.gas_index, + data.meas_index); + } + + return 0; +} diff --git a/examples/breakout_bme688/bme688_parallel.cmake b/examples/breakout_bme688/bme688_parallel.cmake new file mode 100644 index 00000000..58e87871 --- /dev/null +++ b/examples/breakout_bme688/bme688_parallel.cmake @@ -0,0 +1,12 @@ +set(OUTPUT_NAME bme688_parallel) + +add_executable( + ${OUTPUT_NAME} + ${OUTPUT_NAME}.cpp +) + +# Pull in pico libraries that we need +target_link_libraries(${OUTPUT_NAME} pico_stdlib bme68x) + +# create map/bin/hex file etc. +pico_add_extra_outputs(${OUTPUT_NAME}) diff --git a/examples/breakout_bme688/bme688_parallel.cpp b/examples/breakout_bme688/bme688_parallel.cpp new file mode 100644 index 00000000..b0a770b6 --- /dev/null +++ b/examples/breakout_bme688/bme688_parallel.cpp @@ -0,0 +1,66 @@ +#include +#include +#include "pico/stdlib.h" + +#include "bme68x.hpp" +#include "common/pimoroni_i2c.hpp" + +/* +Read a sequence of readings from the BME688 with given heat/duration profiles +Reading the full batch of readings will take some time. This seems to take ~10sec. +*/ + +using namespace pimoroni; + +I2C i2c(BOARD::BREAKOUT_GARDEN); +BME68x bme68x(&i2c); + +constexpr uint16_t profile_length = 10; + +// Space for results +bme68x_data data[profile_length]; + +/* Heater temperature in degree Celsius */ +uint16_t temps[profile_length] = { 320, 100, 100, 100, 200, 200, 200, 320, 320, 320 }; + +/* Multiplier to the shared heater duration */ +uint16_t durations[profile_length] = { 5, 2, 10, 30, 5, 5, 5, 5, 5, 5 }; + + +int main() { + gpio_init(PICO_DEFAULT_LED_PIN); + gpio_set_dir(PICO_DEFAULT_LED_PIN, GPIO_OUT); + + stdio_init_all(); + + bme68x.init(); + + while (1) { + sleep_ms(1000); + auto time_start = get_absolute_time(); + + printf("Fetching %u readings, please wait...\n", profile_length); + + auto result = bme68x.read_parallel(data, temps, durations, profile_length); + (void)result; + + auto time_end = get_absolute_time(); + auto duration = absolute_time_diff_us(time_start, time_end); + auto time_ms = to_ms_since_boot(time_start); + + printf("Done at %lu in %lluus\n", (long unsigned int)time_ms, (long long unsigned int)duration); + + for(auto i = 0u; i < 10u; i++){ + printf("%d, %d: %.2f, %.2f, %.2f, %.2f, 0x%x\n", + data[i].gas_index, + data[i].meas_index, + data[i].temperature, + data[i].pressure, + data[i].humidity, + data[i].gas_resistance, + data[i].status); + } + } + + return 0; +}