commit e940c767a6924db6bc41dd219e3fc53eca668a7f Author: Julien Lazarewicz Date: Sun Jul 5 23:27:50 2026 +0200 Add bh1750, aht20, sgp40 drivers unified on native i2c_master API Forked from managed_components (espressif/bh1750, espressif/aht20, esp-idf-lib/sgp40) and rewrote aht20/sgp40 internals to use driver/i2c_master.h directly instead of the i2c_bus and i2cdev wrapper components, so all three sensors share one create/delete handle pattern over a single i2c_master_bus_handle_t. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0868b1a --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# espidf_i2c + +Forked/rewritten ESP-IDF I2C sensor drivers, unified onto the native +`driver/i2c_master.h` API (ESP-IDF >= 5.3) so every sensor shares the same +handle lifecycle instead of each vendor bringing its own I2C abstraction +(`i2c_bus`, `i2cdev`, etc.). + +## Common API convention + +Every driver in this repo follows the same shape: + +```c +typedef void *xxx_handle_t; + +esp_err_t xxx_create(i2c_master_bus_handle_t bus_handle, uint8_t dev_addr, xxx_handle_t *handle_ret); +esp_err_t xxx_delete(xxx_handle_t handle); +// ... device-specific read/measure functions taking xxx_handle_t +``` + +Callers create one `i2c_master_bus_handle_t` via `i2c_new_master_bus()` and +pass it to each sensor's `_create()`, which internally calls +`i2c_master_bus_add_device()`. No sensor wraps the bus a second time. + +## Components + +- `bh1750/` — ambient light sensor. Already used the native API upstream; + relocated here unchanged. +- `aht20/` — temperature/humidity sensor. Rewritten from Espressif's + `i2c_bus` component onto `i2c_master_transmit`/`i2c_master_receive`. +- `sgp40/` — VOC index sensor. Rewritten from esp-idf-lib's `i2cdev` + component onto `i2c_master_transmit`/`i2c_master_receive`; the Sensirion + VOC algorithm files are carried over unmodified (no I2C dependency). + +## Using from a project + +```yaml +dependencies: + laz/bh1750: + git: https://git.jeese.fr/laz/espidf_i2c.git + path: bh1750 + laz/aht20: + git: https://git.jeese.fr/laz/espidf_i2c.git + path: aht20 + laz/sgp40: + git: https://git.jeese.fr/laz/espidf_i2c.git + path: sgp40 +``` diff --git a/aht20/CMakeLists.txt b/aht20/CMakeLists.txt new file mode 100644 index 0000000..a856a79 --- /dev/null +++ b/aht20/CMakeLists.txt @@ -0,0 +1,6 @@ +idf_component_register( + SRCS "aht20.c" + INCLUDE_DIRS "include" + PRIV_INCLUDE_DIRS "priv_include" + REQUIRES "esp_driver_i2c" +) diff --git a/aht20/LICENSE b/aht20/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/aht20/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/aht20/aht20.c b/aht20/aht20.c new file mode 100644 index 0000000..2e00787 --- /dev/null +++ b/aht20/aht20.c @@ -0,0 +1,134 @@ +/* + * SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "aht20.h" +#include "esp_log.h" +#include "esp_check.h" + +#include "aht20_reg.h" + +#define I2C_CLK_SPEED 100000 + +const static char *TAG = "AHT20"; + +static esp_err_t aht20_write_reg(aht20_handle_t handle, uint8_t reg_addr, uint8_t *data, uint8_t len) +{ + i2c_master_dev_handle_t dev_handle = (i2c_master_dev_handle_t)handle; + uint8_t buf[1 + len]; + buf[0] = reg_addr; + memcpy(&buf[1], data, len); + return i2c_master_transmit(dev_handle, buf, sizeof(buf), pdMS_TO_TICKS(1000)); +} + +static esp_err_t aht20_read_reg(aht20_handle_t handle, uint8_t *data, size_t len) +{ + i2c_master_dev_handle_t dev_handle = (i2c_master_dev_handle_t)handle; + return i2c_master_receive(dev_handle, data, len, pdMS_TO_TICKS(1000)); +} + +static uint8_t aht20_calc_crc(uint8_t *data, uint8_t len) +{ + uint8_t i; + uint8_t byte; + uint8_t crc = 0xFF; + + for (byte = 0; byte < len; byte++) { + crc ^= data[byte]; + for (i = 8; i > 0; --i) { + if ((crc & 0x80) != 0) { + crc = (crc << 1) ^ 0x31; + } else { + crc = crc << 1; + } + } + } + + return crc; +} + +esp_err_t aht20_read_temperature_humidity(aht20_handle_t handle, + uint32_t *temperature_raw, float *temperature, + uint32_t *humidity_raw, float *humidity) +{ + uint8_t status; + uint8_t buf[7]; + uint32_t raw_data; + + ESP_RETURN_ON_FALSE(handle, ESP_ERR_INVALID_ARG, TAG, "invalid device handle pointer"); + + buf[0] = 0x33; + buf[1] = 0x00; + ESP_RETURN_ON_ERROR(aht20_write_reg(handle, AHT20_START_MEASURMENT_CMD, buf, 2), TAG, "I2C write error"); + + vTaskDelay(pdMS_TO_TICKS(100)); + + ESP_RETURN_ON_ERROR(aht20_read_reg(handle, &status, 1), TAG, "I2C read error"); + + if ((status & BIT(AT581X_STATUS_Calibration_Enable)) && + (status & BIT(AT581X_STATUS_CRC_FLAG)) && + ((status & BIT(AT581X_STATUS_BUSY_INDICATION)) == 0)) { + ESP_RETURN_ON_ERROR(aht20_read_reg(handle, buf, 7), TAG, "I2C read error"); + ESP_RETURN_ON_ERROR((aht20_calc_crc(buf, 6) != buf[6]), TAG, "crc is error"); + + raw_data = buf[1]; + raw_data = raw_data << 8; + raw_data += buf[2]; + raw_data = raw_data << 8; + raw_data += buf[3]; + raw_data = raw_data >> 4; + *humidity_raw = raw_data; + *humidity = (float)raw_data * 100 / 1048576; + + raw_data = buf[3] & 0x0F; + raw_data = raw_data << 8; + raw_data += buf[4]; + raw_data = raw_data << 8; + raw_data += buf[5]; + *temperature_raw = raw_data; + *temperature = (float)raw_data * 200 / 1048576 - 50; + return ESP_OK; + } else { + ESP_LOGI(TAG, "data is not ready"); + return ESP_ERR_NOT_FINISHED; + } +} + +esp_err_t aht20_create(i2c_master_bus_handle_t bus_handle, const uint8_t dev_addr, aht20_handle_t *handle_ret) +{ + ESP_LOGI(TAG, "%-15s: %d.%d.%d", CHIP_NAME, AHT20_VER_MAJOR, AHT20_VER_MINOR, AHT20_VER_PATCH); + ESP_LOGI(TAG, "%-15s: %1.1f - %1.1fV", "SUPPLY_VOLTAGE", SUPPLY_VOLTAGE_MIN, SUPPLY_VOLTAGE_MAX); + ESP_LOGI(TAG, "%-15s: %.2f - %.2f degC", "TEMPERATURE", TEMPERATURE_MIN, TEMPERATURE_MAX); + + ESP_RETURN_ON_FALSE(bus_handle, ESP_ERR_INVALID_ARG, TAG, "invalid bus handle"); + ESP_RETURN_ON_FALSE(handle_ret, ESP_ERR_INVALID_ARG, TAG, "invalid device handle pointer"); + + const i2c_device_config_t dev_cfg = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = dev_addr, + .scl_speed_hz = I2C_CLK_SPEED, + }; + + i2c_master_dev_handle_t dev_handle; + esp_err_t ret = i2c_master_bus_add_device(bus_handle, &dev_cfg, &dev_handle); + if (ret != ESP_OK) { + ESP_LOGE(TAG, "i2c_master_bus_add_device failed"); + return ret; + } + + *handle_ret = (aht20_handle_t)dev_handle; + return ESP_OK; +} + +esp_err_t aht20_delete(aht20_handle_t handle) +{ + ESP_RETURN_ON_FALSE(handle, ESP_ERR_INVALID_ARG, TAG, "invalid device handle pointer"); + return i2c_master_bus_rm_device((i2c_master_dev_handle_t)handle); +} diff --git a/aht20/idf_component.yml b/aht20/idf_component.yml new file mode 100644 index 0000000..9ef8d27 --- /dev/null +++ b/aht20/idf_component.yml @@ -0,0 +1,4 @@ +dependencies: + idf: '>=5.3' +description: I2C driver for AHT20 temperature/humidity sensor (native driver/i2c_master.h API) +version: 2.0.0 diff --git a/aht20/include/aht20.h b/aht20/include/aht20.h new file mode 100644 index 0000000..fb11957 --- /dev/null +++ b/aht20/include/aht20.h @@ -0,0 +1,70 @@ +/* + * SPDX-FileCopyrightText: 2023-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include "esp_types.h" +#include "esp_err.h" +#include "driver/i2c_master.h" + +/* AHT20 address: CE pin low - 0x38, CE pin high - 0x39 */ +#define AHT20_ADDRRES_0 (0x38) +#define AHT20_ADDRESS_1 (0x39) + +/** + * @brief Type of AHT20 device handle + */ +typedef void *aht20_handle_t; + +/** + * @brief Create and init sensor object and return a sensor handle + * + * @param[in] bus_handle I2C bus handle. Obtained from i2c_new_master_bus(). + * @param[in] dev_addr I2C address of sensor. Use AHT20_ADDRRES_0 for default address. + * @param[out] handle_ret Handle to created AHT20 driver object. + * + * @return + * - ESP_OK Success + * - ESP_ERR_NO_MEM Not enough memory for the driver + * - Others Error from underlying I2C driver + */ +esp_err_t aht20_create(i2c_master_bus_handle_t bus_handle, const uint8_t dev_addr, aht20_handle_t *handle_ret); + +/** + * @brief Delete and release a sensor object + * + * @param sensor object handle of aht20 + * + * @return + * - ESP_OK Success + * - ESP_FAIL Fail + */ +esp_err_t aht20_delete(aht20_handle_t sensor); + +/** + * @brief read the temperature and humidity data + * + * @param[in] sensor object handle of aht20 + * @param[out] temperature_raw points to a raw temperature buffer + * @param[out] temperature points to a converted temperature buffer + * @param[out] humidity_raw points to a raw humidity buffer + * @param[out] humidity points to a converted humidity buffer + * + * @return + * - ESP_OK Success + * - ESP_ERR_NOT_FINISHED Measurement not ready yet + * - ESP_FAIL Fail + */ +esp_err_t aht20_read_temperature_humidity(aht20_handle_t sensor, + uint32_t *temperature_raw, float *temperature, + uint32_t *humidity_raw, float *humidity); +#ifdef __cplusplus +} +#endif diff --git a/aht20/priv_include/aht20_reg.h b/aht20/priv_include/aht20_reg.h new file mode 100644 index 0000000..478c514 --- /dev/null +++ b/aht20/priv_include/aht20_reg.h @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: 2023 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +/** + * @brief chip information definition + */ +#define CHIP_NAME "ASAIR AHT20" /**< chip name */ +#define SUPPLY_VOLTAGE_MIN (2.2f) /**< chip min supply voltage */ +#define SUPPLY_VOLTAGE_MAX (5.5f) /**< chip max supply voltage */ +#define TEMPERATURE_MIN (-40.0f) /**< chip min operating temperature */ +#define TEMPERATURE_MAX (125.0f) /**< chip max operating temperature */ + +#define AHT20_START_MEASURMENT_CMD 0xAC /* start measurement command */ + +#define AT581X_STATUS_CMP_INT (2) /* 1 --Out threshold range; 0 --In threshold range */ +#define AT581X_STATUS_Calibration_Enable (3) /* 1 --Calibration enable; 0 --Calibration disable */ +#define AT581X_STATUS_CRC_FLAG (4) /* 1 --CRC ok; 0 --CRC failed */ +#define AT581X_STATUS_MODE_STATUS (5) /* 00 -NOR mode; 01 -CYC mode; 1x --CMD mode */ +#define AT581X_STATUS_BUSY_INDICATION (7) /* 1 --Equipment is busy; 0 --Equipment is idle */ diff --git a/bh1750/CMakeLists.txt b/bh1750/CMakeLists.txt new file mode 100644 index 0000000..bf52635 --- /dev/null +++ b/bh1750/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "bh1750.c" + INCLUDE_DIRS "include" + REQUIRES "esp_driver_i2c" +) diff --git a/bh1750/LICENSE b/bh1750/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/bh1750/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/bh1750/bh1750.c b/bh1750/bh1750.c new file mode 100644 index 0000000..3c31773 --- /dev/null +++ b/bh1750/bh1750.c @@ -0,0 +1,116 @@ +/* + * SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "bh1750.h" +#include "driver/i2c_master.h" +#include "freertos/FreeRTOS.h" +#include "freertos/projdefs.h" // for pdMS_TO_TICKS + +#define BH_1750_MEASUREMENT_ACCURACY \ + 1.2 /*!< the typical measurement accuracy of BH1750 sensor \ + */ + +#define BH1750_POWER_DOWN 0x00 /*!< Command to set Power Down*/ +#define BH1750_POWER_ON 0x01 /*!< Command to set Power On*/ +#define I2C_CLK_SPEED 400000 + + +typedef struct +{ + i2c_master_dev_handle_t i2c_handle; +} bh1750_dev_t; + +static esp_err_t bh1750_write_byte(const bh1750_dev_t* const sens, const uint8_t byte) +{ return i2c_master_transmit(sens->i2c_handle, &byte, 1, pdMS_TO_TICKS(1000)); } + +esp_err_t bh1750_create(i2c_master_bus_handle_t i2c_bus, + const uint8_t dev_addr, + bh1750_handle_t* handle_ret) +{ + esp_err_t ret = ESP_OK; + bh1750_dev_t* sensor = (bh1750_dev_t*) calloc(1, sizeof(bh1750_dev_t)); + if (!sensor) + { + return ESP_ERR_NO_MEM; + } + + // Add new I2C device + const i2c_device_config_t i2c_dev_cfg = { + .device_address = dev_addr, + .scl_speed_hz = I2C_CLK_SPEED, + }; + ret = i2c_master_bus_add_device(i2c_bus, &i2c_dev_cfg, &sensor->i2c_handle); + if (ret != ESP_OK) + { + free(sensor); + return ret; + } + + assert(sensor->i2c_handle); + *handle_ret = sensor; + return ret; +} + +esp_err_t bh1750_delete(bh1750_handle_t sensor) +{ + bh1750_dev_t* sens = (bh1750_dev_t*) sensor; + if (sens->i2c_handle) + { + i2c_master_bus_rm_device(sens->i2c_handle); + } + free(sens); + return ESP_OK; +} + +esp_err_t bh1750_power_down(bh1750_handle_t sensor) +{ + bh1750_dev_t* sens = (bh1750_dev_t*) sensor; + return bh1750_write_byte(sens, BH1750_POWER_DOWN); +} + +esp_err_t bh1750_power_on(bh1750_handle_t sensor) +{ + bh1750_dev_t* sens = (bh1750_dev_t*) sensor; + return bh1750_write_byte(sens, BH1750_POWER_ON); +} + +esp_err_t bh1750_set_measure_time(bh1750_handle_t sensor, const uint8_t measure_time) +{ + bh1750_dev_t* sens = (bh1750_dev_t*) sensor; + uint32_t i = 0; + uint8_t buf[2] = { 0x40, 0x60 }; // constant part of the the MTreg + buf[0] |= measure_time >> 5; + buf[1] |= measure_time & 0x1F; + for (i = 0; i < 2; i++) + { + esp_err_t ret = bh1750_write_byte(sens, buf[i]); + if (ESP_OK != ret) + { + return ret; + } + } + return ESP_OK; +} + +esp_err_t bh1750_set_measure_mode(bh1750_handle_t sensor, const bh1750_measure_mode_t cmd_measure) +{ + bh1750_dev_t* sens = (bh1750_dev_t*) sensor; + return bh1750_write_byte(sens, (uint8_t) cmd_measure); +} + +esp_err_t bh1750_get_data(bh1750_handle_t sensor, float* const data) +{ + bh1750_dev_t* sens = (bh1750_dev_t*) sensor; + uint8_t read_buffer[2]; + esp_err_t ret = i2c_master_receive( + sens->i2c_handle, read_buffer, sizeof(read_buffer), pdMS_TO_TICKS(1000)); + if (ESP_OK != ret) + { + return ret; + } + *data = ((read_buffer[0] << 8 | read_buffer[1]) / BH_1750_MEASUREMENT_ACCURACY); + return ESP_OK; +} diff --git a/bh1750/idf_component.yml b/bh1750/idf_component.yml new file mode 100644 index 0000000..2bb2d63 --- /dev/null +++ b/bh1750/idf_component.yml @@ -0,0 +1,4 @@ +dependencies: + idf: '>=5.3' +description: I2C driver for BH1750 light sensor (native driver/i2c_master.h API) +version: 2.0.0 diff --git a/bh1750/include/bh1750.h b/bh1750/include/bh1750.h new file mode 100644 index 0000000..062f8e8 --- /dev/null +++ b/bh1750/include/bh1750.h @@ -0,0 +1,136 @@ +/* + * SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief BH1750 driver + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include "driver/i2c_types.h" +#include "esp_err.h" + +typedef enum { + BH1750_CONTINUE_1LX_RES = 0x10, /*!< Command to set measure mode as Continuously H-Resolution mode*/ + BH1750_CONTINUE_HALFLX_RES = 0x11, /*!< Command to set measure mode as Continuously H-Resolution mode2*/ + BH1750_CONTINUE_4LX_RES = 0x13, /*!< Command to set measure mode as Continuously L-Resolution mode*/ + BH1750_ONETIME_1LX_RES = 0x20, /*!< Command to set measure mode as One Time H-Resolution mode*/ + BH1750_ONETIME_HALFLX_RES = 0x21, /*!< Command to set measure mode as One Time H-Resolution mode2*/ + BH1750_ONETIME_4LX_RES = 0x23, /*!< Command to set measure mode as One Time L-Resolution mode*/ +} bh1750_measure_mode_t; + +#define BH1750_I2C_ADDRESS_DEFAULT (0x23) +typedef void *bh1750_handle_t; + +/** + * @brief Set bh1750 as power down mode (low current) + * + * @param sensor object handle of bh1750 + * + * @return + * - ESP_OK Success + * - ESP_FAIL Fail + */ +esp_err_t bh1750_power_down(bh1750_handle_t sensor); + +/** + * @brief Set bh1750 as power on mode + * + * @param sensor object handle of bh1750 + * + * @return + * - ESP_OK Success + * - ESP_FAIL Fail + */ +esp_err_t bh1750_power_on(bh1750_handle_t sensor); + +/** + * @brief Get light intensity from bh1750 + * + * @param sensor object handle of bh1750 + * @param[in] cmd_measure the instruction to set measurement mode + * + * @note + * You should call this funtion to set measurement mode before call bh1750_get_data() to acquire data. + * If you set onetime mode, you just can get one measurement result. + * If you set continuous mode, you can call bh1750_get_data() to acquire data repeatedly. + * + * @return + * - ESP_OK Success + * - ESP_FAIL Fail + */ +esp_err_t bh1750_set_measure_mode(bh1750_handle_t sensor, const bh1750_measure_mode_t cmd_measure); + +/** + * @brief Get light intensity from BH1750 + * + * Returns light intensity in [lx] corrected by typical BH1750 Measurement Accuracy (= 1.2). + * + * @see BH1750 datasheet Rev. D page 2 + * + * @note + * You should acquire data from the sensor after the measurement time is over, + * so take care of measurement time in different modes. + * + * @param sensor object handle of bh1750 + * @param[out] data light intensity value got from bh1750 in [lx] + * + * @return + * - ESP_OK Success + * - ESP_FAIL Fail + */ +esp_err_t bh1750_get_data(bh1750_handle_t sensor, float *const data); + +/** + * @brief Set measurement time + * + * This function is used to adjust BH1750 sensitivity, i.e. compensating influence from optical window. + * + * @see BH1750 datasheet Rev. D page 11 + * + * @param sensor object handle of bh1750 + * @param[in] measure_time measurement time + * + * @return + * - ESP_OK Success + * - ESP_FAIL Fail + */ +esp_err_t bh1750_set_measure_time(bh1750_handle_t sensor, const uint8_t measure_time); + +/** + * @brief Create and init sensor object and return a sensor handle + * + * @param[in] i2c_bus I2C bus handle. Obtained from i2c_new_master_bus().s + * @param[in] dev_addr I2C device address of sensor. Use BH1750_I2C_ADDRESS_DEFAULT for default address. + * @param[out] handle_ret Handle to created BH1750 driver object. + * + * @return + * - ESP_OK Success + * - ESP_ERR_NO_MEM Not enough memory for the driver + * - ESP_ERR_NOT_FOUND Sensor not found on the I2C bus + * - Others Error from underlying I2C driver + */ +esp_err_t bh1750_create(i2c_master_bus_handle_t i2c_bus, const uint8_t dev_addr, bh1750_handle_t *handle_ret); + +/** + * @brief Delete and release a sensor object + * + * @param sensor object handle of bh1750 + * + * @return + * - ESP_OK Success + * - ESP_FAIL Fail + */ +esp_err_t bh1750_delete(bh1750_handle_t sensor); + +#ifdef __cplusplus +} +#endif diff --git a/sgp40/CMakeLists.txt b/sgp40/CMakeLists.txt new file mode 100644 index 0000000..8564c69 --- /dev/null +++ b/sgp40/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "sgp40.c" "sensirion_voc_algorithm.c" + INCLUDE_DIRS "include" + REQUIRES "esp_driver_i2c" +) diff --git a/sgp40/LICENSE b/sgp40/LICENSE new file mode 100644 index 0000000..57d0b16 --- /dev/null +++ b/sgp40/LICENSE @@ -0,0 +1,27 @@ +Copyright 2020 Sensirion AG +Copyright 2020 Ruslan V. Uss + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, +this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation +and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/sgp40/idf_component.yml b/sgp40/idf_component.yml new file mode 100644 index 0000000..83dc6f1 --- /dev/null +++ b/sgp40/idf_component.yml @@ -0,0 +1,4 @@ +dependencies: + idf: '>=5.3' +description: I2C driver for SGP40 VOC sensor (native driver/i2c_master.h API) +version: 2.0.0 diff --git a/sgp40/include/sensirion_voc_algorithm.h b/sgp40/include/sensirion_voc_algorithm.h new file mode 100644 index 0000000..dfc41d4 --- /dev/null +++ b/sgp40/include/sensirion_voc_algorithm.h @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2020, Sensirion AG + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of Sensirion AG nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @file sensirion_voc_algorithm.h + * @defgroup sgp40 sgp40 + * @{ + * + * Library for calculating concentration of volatile organic compounds + */ + +#ifndef VOCALGORITHM_H_ +#define VOCALGORITHM_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* The fixed point arithmetic parts of this code were originally created by + * https://github.com/PetteriAimonen/libfixmath + */ + +typedef int32_t fix16_t; + +#define F16(x) \ + ((fix16_t)(((x) >= 0) ? ((x)*65536.0 + 0.5) : ((x)*65536.0 - 0.5))) + +#define VocAlgorithm_SAMPLING_INTERVAL (1.) +#define VocAlgorithm_INITIAL_BLACKOUT (45.) +#define VocAlgorithm_VOC_INDEX_GAIN (230.) +#define VocAlgorithm_SRAW_STD_INITIAL (50.) +#define VocAlgorithm_SRAW_STD_BONUS (220.) +#define VocAlgorithm_TAU_MEAN_VARIANCE_HOURS (12.) +#define VocAlgorithm_TAU_INITIAL_MEAN (20.) +#define VocAlgorithm_INIT_DURATION_MEAN ((3600. * 0.75)) +#define VocAlgorithm_INIT_TRANSITION_MEAN (0.01) +#define VocAlgorithm_TAU_INITIAL_VARIANCE (2500.) +#define VocAlgorithm_INIT_DURATION_VARIANCE ((3600. * 1.45)) +#define VocAlgorithm_INIT_TRANSITION_VARIANCE (0.01) +#define VocAlgorithm_GATING_THRESHOLD (340.) +#define VocAlgorithm_GATING_THRESHOLD_INITIAL (510.) +#define VocAlgorithm_GATING_THRESHOLD_TRANSITION (0.09) +#define VocAlgorithm_GATING_MAX_DURATION_MINUTES ((60. * 3.)) +#define VocAlgorithm_GATING_MAX_RATIO (0.3) +#define VocAlgorithm_SIGMOID_L (500.) +#define VocAlgorithm_SIGMOID_K (-0.0065) +#define VocAlgorithm_SIGMOID_X0 (213.) +#define VocAlgorithm_VOC_INDEX_OFFSET_DEFAULT (100.) +#define VocAlgorithm_LP_TAU_FAST (20.0) +#define VocAlgorithm_LP_TAU_SLOW (500.0) +#define VocAlgorithm_LP_ALPHA (-0.2) +#define VocAlgorithm_PERSISTENCE_UPTIME_GAMMA ((3. * 3600.)) +#define VocAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING (64.) +#define VocAlgorithm_MEAN_VARIANCE_ESTIMATOR__FIX16_MAX (32767.) + +/** + * Struct to hold all the states of the VOC algorithm. + */ +typedef struct +{ + fix16_t mVoc_Index_Offset; + fix16_t mTau_Mean_Variance_Hours; + fix16_t mGating_Max_Duration_Minutes; + fix16_t mSraw_Std_Initial; + fix16_t mUptime; + fix16_t mSraw; + fix16_t mVoc_Index; + fix16_t m_Mean_Variance_Estimator__Gating_Max_Duration_Minutes; + bool m_Mean_Variance_Estimator___Initialized; + fix16_t m_Mean_Variance_Estimator___Mean; + fix16_t m_Mean_Variance_Estimator___Sraw_Offset; + fix16_t m_Mean_Variance_Estimator___Std; + fix16_t m_Mean_Variance_Estimator___Gamma; + fix16_t m_Mean_Variance_Estimator___Gamma_Initial_Mean; + fix16_t m_Mean_Variance_Estimator___Gamma_Initial_Variance; + fix16_t m_Mean_Variance_Estimator__Gamma_Mean; + fix16_t m_Mean_Variance_Estimator__Gamma_Variance; + fix16_t m_Mean_Variance_Estimator___Uptime_Gamma; + fix16_t m_Mean_Variance_Estimator___Uptime_Gating; + fix16_t m_Mean_Variance_Estimator___Gating_Duration_Minutes; + fix16_t m_Mean_Variance_Estimator___Sigmoid__L; + fix16_t m_Mean_Variance_Estimator___Sigmoid__K; + fix16_t m_Mean_Variance_Estimator___Sigmoid__X0; + fix16_t m_Mox_Model__Sraw_Std; + fix16_t m_Mox_Model__Sraw_Mean; + fix16_t m_Sigmoid_Scaled__Offset; + fix16_t m_Adaptive_Lowpass__A1; + fix16_t m_Adaptive_Lowpass__A2; + bool m_Adaptive_Lowpass___Initialized; + fix16_t m_Adaptive_Lowpass___X1; + fix16_t m_Adaptive_Lowpass___X2; + fix16_t m_Adaptive_Lowpass___X3; +} VocAlgorithmParams; + +/** + * Initialize the VOC algorithm parameters. Call this once at the beginning or + * whenever the sensor stopped measurements. + * @param params Pointer to the VocAlgorithmParams struct + */ +void VocAlgorithm_init(VocAlgorithmParams *params); + +/** + * Get current algorithm states. Retrieved values can be used in + * VocAlgorithm_set_states() to resume operation after a short interruption, + * skipping initial learning phase. This feature can only be used after at least + * 3 hours of continuous operation. + * @param params Pointer to the VocAlgorithmParams struct + * @param state0 State0 to be stored + * @param state1 State1 to be stored + */ +void VocAlgorithm_get_states(VocAlgorithmParams *params, int32_t *state0, + int32_t *state1); + +/** + * Set previously retrieved algorithm states to resume operation after a short + * interruption, skipping initial learning phase. This feature should not be + * used after inerruptions of more than 10 minutes. Call this once after + * VocAlgorithm_init() and the optional VocAlgorithm_set_tuning_parameters(), if + * desired. Otherwise, the algorithm will start with initial learning phase. + * @param params Pointer to the VocAlgorithmParams struct + * @param state0 State0 to be restored + * @param state1 State1 to be restored + */ +void VocAlgorithm_set_states(VocAlgorithmParams *params, int32_t state0, + int32_t state1); + +/** + * Set parameters to customize the VOC algorithm. Call this once after + * VocAlgorithm_init(), if desired. Otherwise, the default values will be used. + * + * @param params Pointer to the VocAlgorithmParams struct + * @param voc_index_offset VOC index representing typical (average) + * conditions. Range 1..250, default 100 + * @param learning_time_hours Time constant of long-term estimator. + * Past events will be forgotten after about + * twice the learning time. + * Range 1..72 [hours], default 12 [hours] + * @param gating_max_duration_minutes Maximum duration of gating (freeze of + * estimator during high VOC index signal). + * 0 (no gating) or range 1..720 [minutes], + * default 180 [minutes] + * @param std_initial Initial estimate for standard deviation. + * Lower value boosts events during initial + * learning period, but may result in larger + * device-to-device variations. + * Range 10..500, default 50 + */ +void VocAlgorithm_set_tuning_parameters(VocAlgorithmParams *params, + int32_t voc_index_offset, + int32_t learning_time_hours, + int32_t gating_max_duration_minutes, + int32_t std_initial); + +/** + * Calculate the VOC index value from the raw sensor value. + * + * @param params Pointer to the VocAlgorithmParams struct + * @param sraw Raw value from the SGP40 sensor + * @param voc_index Calculated VOC index value from the raw sensor value. Zero + * during initial blackout period and 1..500 afterwards + */ +void VocAlgorithm_process(VocAlgorithmParams *params, int32_t sraw, + int32_t *voc_index); + +#ifdef __cplusplus +} +#endif + +/**@}*/ + +#endif /* VOCALGORITHM_H_ */ diff --git a/sgp40/include/sgp40.h b/sgp40/include/sgp40.h new file mode 100644 index 0000000..4fc1adc --- /dev/null +++ b/sgp40/include/sgp40.h @@ -0,0 +1,106 @@ +/** + * @file sgp40.h + * @defgroup sgp40 sgp40 + * @{ + * + * ESP-IDF driver for SGP40 Indoor Air Quality Sensor for VOC Measurements + * + * Copyright (c) 2020 Ruslan V. Uss + * + * BSD Licensed as described in the file LICENSE + */ +#ifndef __SGP40_H__ +#define __SGP40_H__ + +#include +#include +#include "driver/i2c_master.h" +#include "sensirion_voc_algorithm.h" + +#ifdef __cplusplus +extern "C" { +#endif + +#define SGP40_ADDR 0x59 //!< I2C address + +/** + * @brief Type of SGP40 device handle + */ +typedef void *sgp40_handle_t; + +/** + * @brief Create and init sensor object, read device information, initialize the VOC algorithm + * + * @param[in] bus_handle I2C bus handle. Obtained from i2c_new_master_bus(). + * @param[in] dev_addr I2C address of sensor. Use SGP40_ADDR for default address. + * @param[out] handle_ret Handle to created SGP40 driver object. + * @return `ESP_OK` on success + */ +esp_err_t sgp40_create(i2c_master_bus_handle_t bus_handle, const uint8_t dev_addr, sgp40_handle_t *handle_ret); + +/** + * @brief Delete and release a sensor object + * + * @param sensor object handle of sgp40 + * @return `ESP_OK` on success + */ +esp_err_t sgp40_delete(sgp40_handle_t sensor); + +/** + * @brief Reset device, than put it to idle mode + * + * @param sensor object handle of sgp40 + * @return `ESP_OK` on success + */ +esp_err_t sgp40_soft_reset(sgp40_handle_t sensor); + +/** + * @brief Perform a self-test + * + * @param sensor object handle of sgp40 + * @return `ESP_OK` on success + */ +esp_err_t sgp40_self_test(sgp40_handle_t sensor); + +/** + * @brief Turn hotplate off, stop measurement and put device to idle mode + * + * @param sensor object handle of sgp40 + * @return `ESP_OK` on success + */ +esp_err_t sgp40_heater_off(sgp40_handle_t sensor); + +/** + * @brief Perform a measurement + * + * @param sensor object handle of sgp40 + * @param humidity Relative humidity, percents. Use NaN if + * you want uncompensated measurement + * @param temperature Temperature, degrees Celsius. Use NaN if + * you want uncompensated measurement + * @param[out] raw Raw value, proportional to the logarithm + * of the resistance of the sensing element + * @return `ESP_OK` on success + */ +esp_err_t sgp40_measure_raw(sgp40_handle_t sensor, float humidity, float temperature, uint16_t *raw); + +/** + * @brief Perform a measurement and update VOC index + * + * @param sensor object handle of sgp40 + * @param humidity Relative humidity, percents. Use NaN if + * you want uncompensated measurement + * @param temperature Temperature, degrees Celsius. Use NaN if + * you want uncompensated measurement + * @param[out] voc_index Calculated VOC index + * @return `ESP_OK` on success + */ +esp_err_t sgp40_measure_voc(sgp40_handle_t sensor, float humidity, float temperature, int32_t *voc_index); + +#ifdef __cplusplus +} +#endif + +/**@}*/ + +#endif /* __SGP40_H__ */ diff --git a/sgp40/sensirion_voc_algorithm.c b/sgp40/sensirion_voc_algorithm.c new file mode 100644 index 0000000..fedcc45 --- /dev/null +++ b/sgp40/sensirion_voc_algorithm.c @@ -0,0 +1,898 @@ +/* + * Copyright (c) 2020, Sensirion AG + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * * Neither the name of Sensirion AG nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + */ + +#include "sensirion_voc_algorithm.h" + +/* The fixed point arithmetic parts of this code were originally created by + * https://github.com/PetteriAimonen/libfixmath + */ + +/*!< the maximum value of fix16_t */ +#define FIX16_MAXIMUM 0x7FFFFFFF +/*!< the minimum value of fix16_t */ +#define FIX16_MINIMUM 0x80000000 +/*!< the value used to indicate overflows when FIXMATH_NO_OVERFLOW is not + * specified */ +#define FIX16_OVERFLOW 0x80000000 +/*!< fix16_t value of 1 */ +#define FIX16_ONE 0x00010000 + +inline fix16_t fix16_from_int(int32_t a) +{ + return a * FIX16_ONE; +} + +inline int32_t fix16_cast_to_int(fix16_t a) +{ + return (a >> 16); +} + +/*! Multiplies the two given fix16_t's and returns the result. */ +static fix16_t fix16_mul(fix16_t inArg0, fix16_t inArg1); + +/*! Divides the first given fix16_t by the second and returns the result. */ +static fix16_t fix16_div(fix16_t inArg0, fix16_t inArg1); + +/*! Returns the square root of the given fix16_t. */ +static fix16_t fix16_sqrt(fix16_t inValue); + +/*! Returns the exponent (e^) of the given fix16_t. */ +static fix16_t fix16_exp(fix16_t inValue); + +static fix16_t fix16_mul(fix16_t inArg0, fix16_t inArg1) +{ + // Each argument is divided to 16-bit parts. + // AB + // * CD + // ----------- + // BD 16 * 16 -> 32 bit products + // CB + // AD + // AC + // |----| 64 bit product + int32_t A = (inArg0 >> 16), C = (inArg1 >> 16); + uint32_t B = (inArg0 & 0xFFFF), D = (inArg1 & 0xFFFF); + + int32_t AC = A * C; + int32_t AD_CB = A * D + C * B; + uint32_t BD = B * D; + + int32_t product_hi = AC + (AD_CB >> 16); + + // Handle carry from lower 32 bits to upper part of result. + uint32_t ad_cb_temp = AD_CB << 16; + uint32_t product_lo = BD + ad_cb_temp; + if (product_lo < BD) + product_hi++; + +#ifndef FIXMATH_NO_OVERFLOW + // The upper 17 bits should all be the same (the sign). + if (product_hi >> 31 != product_hi >> 15) + return FIX16_OVERFLOW; +#endif + +#ifdef FIXMATH_NO_ROUNDING + return (product_hi << 16) | (product_lo >> 16); +#else + // Subtracting 0x8000 (= 0.5) and then using signed right shift + // achieves proper rounding to result-1, except in the corner + // case of negative numbers and lowest word = 0x8000. + // To handle that, we also have to subtract 1 for negative numbers. + uint32_t product_lo_tmp = product_lo; + product_lo -= 0x8000; + product_lo -= (uint32_t)product_hi >> 31; + if (product_lo > product_lo_tmp) + product_hi--; + + // Discard the lowest 16 bits. Note that this is not exactly the same + // as dividing by 0x10000. For example if product = -1, result will + // also be -1 and not 0. This is compensated by adding +1 to the result + // and compensating this in turn in the rounding above. + fix16_t result = (product_hi << 16) | (product_lo >> 16); + result += 1; + return result; +#endif +} + +static fix16_t fix16_div(fix16_t a, fix16_t b) +{ + // This uses the basic binary restoring division algorithm. + // It appears to be faster to do the whole division manually than + // trying to compose a 64-bit divide out of 32-bit divisions on + // platforms without hardware divide. + + if (b == 0) + return FIX16_MINIMUM; + + uint32_t remainder = (a >= 0) ? a : (-a); + uint32_t divider = (b >= 0) ? b : (-b); + + uint32_t quotient = 0; + uint32_t bit = 0x10000; + + /* The algorithm requires D >= R */ + while (divider < remainder) + { + divider <<= 1; + bit <<= 1; + } + +#ifndef FIXMATH_NO_OVERFLOW + if (!bit) + return FIX16_OVERFLOW; +#endif + + if (divider & 0x80000000) + { + // Perform one step manually to avoid overflows later. + // We know that divider's bottom bit is 0 here. + if (remainder >= divider) + { + quotient |= bit; + remainder -= divider; + } + divider >>= 1; + bit >>= 1; + } + + /* Main division loop */ + while (bit && remainder) + { + if (remainder >= divider) + { + quotient |= bit; + remainder -= divider; + } + + remainder <<= 1; + bit >>= 1; + } + +#ifndef FIXMATH_NO_ROUNDING + if (remainder >= divider) + { + quotient++; + } +#endif + + fix16_t result = quotient; + + /* Figure out the sign of result */ + if ((a ^ b) & 0x80000000) + { +#ifndef FIXMATH_NO_OVERFLOW + if (result == FIX16_MINIMUM) + return FIX16_OVERFLOW; +#endif + + result = -result; + } + + return result; +} + +static fix16_t fix16_sqrt(fix16_t x) +{ + // It is assumed that x is not negative + + uint32_t num = x; + uint32_t result = 0; + uint32_t bit; + uint8_t n; + + bit = (uint32_t)1 << 30; + while (bit > num) + bit >>= 2; + + // The main part is executed twice, in order to avoid + // using 64 bit values in computations. + for (n = 0; n < 2; n++) + { + // First we get the top 24 bits of the answer. + while (bit) + { + if (num >= result + bit) + { + num -= result + bit; + result = (result >> 1) + bit; + } + else + { + result = (result >> 1); + } + bit >>= 2; + } + + if (n == 0) + { + // Then process it again to get the lowest 8 bits. + if (num > 65535) + { + // The remainder 'num' is too large to be shifted left + // by 16, so we have to add 1 to result manually and + // adjust 'num' accordingly. + // num = a - (result + 0.5)^2 + // = num + result^2 - (result + 0.5)^2 + // = num - result - 0.5 + num -= result; + num = (num << 16) - 0x8000; + result = (result << 16) + 0x8000; + } + else + { + num <<= 16; + result <<= 16; + } + + bit = 1 << 14; + } + } + +#ifndef FIXMATH_NO_ROUNDING + // Finally, if next bit would have been 1, round the result upwards. + if (num > result) + { + result++; + } +#endif + + return (fix16_t)result; +} + +static fix16_t fix16_exp(fix16_t x) +{ +// Function to approximate exp(); optimized more for code size than speed + +// exp(x) for x = +/- {1, 1/8, 1/64, 1/512} +#define NUM_EXP_VALUES 4 + static const fix16_t exp_pos_values[NUM_EXP_VALUES] = + { + F16(2.7182818), F16(1.1331485), F16(1.0157477), F16(1.0019550) + }; + static const fix16_t exp_neg_values[NUM_EXP_VALUES] = + { + F16(0.3678794), F16(0.8824969), F16(0.9844964), F16(0.9980488) + }; + const fix16_t* exp_values; + + fix16_t res, arg; + uint16_t i; + + if (x >= F16(10.3972)) + return FIX16_MAXIMUM; + if (x <= F16(-11.7835)) + return 0; + + if (x < 0) + { + x = -x; + exp_values = exp_neg_values; + } + else + { + exp_values = exp_pos_values; + } + + res = FIX16_ONE; + arg = FIX16_ONE; + for (i = 0; i < NUM_EXP_VALUES; i++) + { + while (x >= arg) + { + res = fix16_mul(res, exp_values[i]); + x -= arg; + } + arg >>= 3; + } + return res; +} + +static void VocAlgorithm__init_instances(VocAlgorithmParams* params); +static void +VocAlgorithm__mean_variance_estimator__init(VocAlgorithmParams* params); +static void VocAlgorithm__mean_variance_estimator___init_instances( + VocAlgorithmParams* params); +static void VocAlgorithm__mean_variance_estimator__set_parameters( + VocAlgorithmParams* params, fix16_t std_initial, + fix16_t tau_mean_variance_hours, fix16_t gating_max_duration_minutes); +static void +VocAlgorithm__mean_variance_estimator__set_states(VocAlgorithmParams* params, + fix16_t mean, fix16_t std, + fix16_t uptime_gamma); +static fix16_t +VocAlgorithm__mean_variance_estimator__get_std(VocAlgorithmParams* params); +static fix16_t +VocAlgorithm__mean_variance_estimator__get_mean(VocAlgorithmParams* params); +static void VocAlgorithm__mean_variance_estimator___calculate_gamma( + VocAlgorithmParams* params, fix16_t voc_index_from_prior); +static void VocAlgorithm__mean_variance_estimator__process( + VocAlgorithmParams* params, fix16_t sraw, fix16_t voc_index_from_prior); +static void VocAlgorithm__mean_variance_estimator___sigmoid__init( + VocAlgorithmParams* params); +static void VocAlgorithm__mean_variance_estimator___sigmoid__set_parameters( + VocAlgorithmParams* params, fix16_t L, fix16_t X0, fix16_t K); +static fix16_t VocAlgorithm__mean_variance_estimator___sigmoid__process( + VocAlgorithmParams* params, fix16_t sample); +static void VocAlgorithm__mox_model__init(VocAlgorithmParams* params); +static void VocAlgorithm__mox_model__set_parameters(VocAlgorithmParams* params, + fix16_t SRAW_STD, + fix16_t SRAW_MEAN); +static fix16_t VocAlgorithm__mox_model__process(VocAlgorithmParams* params, + fix16_t sraw); +static void VocAlgorithm__sigmoid_scaled__init(VocAlgorithmParams* params); +static void +VocAlgorithm__sigmoid_scaled__set_parameters(VocAlgorithmParams* params, + fix16_t offset); +static fix16_t VocAlgorithm__sigmoid_scaled__process(VocAlgorithmParams* params, + fix16_t sample); +static void VocAlgorithm__adaptive_lowpass__init(VocAlgorithmParams* params); +static void +VocAlgorithm__adaptive_lowpass__set_parameters(VocAlgorithmParams* params); +static fix16_t +VocAlgorithm__adaptive_lowpass__process(VocAlgorithmParams* params, + fix16_t sample); + +void VocAlgorithm_init(VocAlgorithmParams* params) +{ + + params->mVoc_Index_Offset = F16(VocAlgorithm_VOC_INDEX_OFFSET_DEFAULT); + params->mTau_Mean_Variance_Hours = + F16(VocAlgorithm_TAU_MEAN_VARIANCE_HOURS); + params->mGating_Max_Duration_Minutes = + F16(VocAlgorithm_GATING_MAX_DURATION_MINUTES); + params->mSraw_Std_Initial = F16(VocAlgorithm_SRAW_STD_INITIAL); + params->mUptime = F16(0.); + params->mSraw = F16(0.); + params->mVoc_Index = 0; + VocAlgorithm__init_instances(params); +} + +static void VocAlgorithm__init_instances(VocAlgorithmParams* params) +{ + + VocAlgorithm__mean_variance_estimator__init(params); + VocAlgorithm__mean_variance_estimator__set_parameters( + params, params->mSraw_Std_Initial, params->mTau_Mean_Variance_Hours, + params->mGating_Max_Duration_Minutes); + VocAlgorithm__mox_model__init(params); + VocAlgorithm__mox_model__set_parameters( + params, VocAlgorithm__mean_variance_estimator__get_std(params), + VocAlgorithm__mean_variance_estimator__get_mean(params)); + VocAlgorithm__sigmoid_scaled__init(params); + VocAlgorithm__sigmoid_scaled__set_parameters(params, + params->mVoc_Index_Offset); + VocAlgorithm__adaptive_lowpass__init(params); + VocAlgorithm__adaptive_lowpass__set_parameters(params); +} + +void VocAlgorithm_get_states(VocAlgorithmParams* params, int32_t* state0, + int32_t* state1) +{ + + *state0 = VocAlgorithm__mean_variance_estimator__get_mean(params); + *state1 = VocAlgorithm__mean_variance_estimator__get_std(params); + return; +} + +void VocAlgorithm_set_states(VocAlgorithmParams* params, int32_t state0, + int32_t state1) +{ + + VocAlgorithm__mean_variance_estimator__set_states( + params, state0, state1, F16(VocAlgorithm_PERSISTENCE_UPTIME_GAMMA)); + params->mSraw = state0; +} + +void VocAlgorithm_set_tuning_parameters(VocAlgorithmParams* params, + int32_t voc_index_offset, + int32_t learning_time_hours, + int32_t gating_max_duration_minutes, + int32_t std_initial) +{ + + params->mVoc_Index_Offset = (fix16_from_int(voc_index_offset)); + params->mTau_Mean_Variance_Hours = (fix16_from_int(learning_time_hours)); + params->mGating_Max_Duration_Minutes = + (fix16_from_int(gating_max_duration_minutes)); + params->mSraw_Std_Initial = (fix16_from_int(std_initial)); + VocAlgorithm__init_instances(params); +} + +void VocAlgorithm_process(VocAlgorithmParams* params, int32_t sraw, + int32_t* voc_index) +{ + + if ((params->mUptime <= F16(VocAlgorithm_INITIAL_BLACKOUT))) + { + params->mUptime = + (params->mUptime + F16(VocAlgorithm_SAMPLING_INTERVAL)); + } + else + { + if (((sraw > 0) && (sraw < 65000))) + { + if ((sraw < 20001)) + { + sraw = 20001; + } + else if ((sraw > 52767)) + { + sraw = 52767; + } + params->mSraw = (fix16_from_int((sraw - 20000))); + } + params->mVoc_Index = + VocAlgorithm__mox_model__process(params, params->mSraw); + params->mVoc_Index = + VocAlgorithm__sigmoid_scaled__process(params, params->mVoc_Index); + params->mVoc_Index = + VocAlgorithm__adaptive_lowpass__process(params, params->mVoc_Index); + if ((params->mVoc_Index < F16(0.5))) + { + params->mVoc_Index = F16(0.5); + } + if ((params->mSraw > F16(0.))) + { + VocAlgorithm__mean_variance_estimator__process( + params, params->mSraw, params->mVoc_Index); + VocAlgorithm__mox_model__set_parameters( + params, VocAlgorithm__mean_variance_estimator__get_std(params), + VocAlgorithm__mean_variance_estimator__get_mean(params)); + } + } + *voc_index = (fix16_cast_to_int((params->mVoc_Index + F16(0.5)))); + return; +} + +static void +VocAlgorithm__mean_variance_estimator__init(VocAlgorithmParams* params) +{ + + VocAlgorithm__mean_variance_estimator__set_parameters(params, F16(0.), + F16(0.), F16(0.)); + VocAlgorithm__mean_variance_estimator___init_instances(params); +} + +static void VocAlgorithm__mean_variance_estimator___init_instances( + VocAlgorithmParams* params) +{ + + VocAlgorithm__mean_variance_estimator___sigmoid__init(params); +} + +static void VocAlgorithm__mean_variance_estimator__set_parameters( + VocAlgorithmParams* params, fix16_t std_initial, + fix16_t tau_mean_variance_hours, fix16_t gating_max_duration_minutes) +{ + + params->m_Mean_Variance_Estimator__Gating_Max_Duration_Minutes = + gating_max_duration_minutes; + params->m_Mean_Variance_Estimator___Initialized = false; + params->m_Mean_Variance_Estimator___Mean = F16(0.); + params->m_Mean_Variance_Estimator___Sraw_Offset = F16(0.); + params->m_Mean_Variance_Estimator___Std = std_initial; + params->m_Mean_Variance_Estimator___Gamma = + (fix16_div(F16((VocAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING * + (VocAlgorithm_SAMPLING_INTERVAL / 3600.))), + (tau_mean_variance_hours + + F16((VocAlgorithm_SAMPLING_INTERVAL / 3600.))))); + params->m_Mean_Variance_Estimator___Gamma_Initial_Mean = + F16(((VocAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING * + VocAlgorithm_SAMPLING_INTERVAL) / + (VocAlgorithm_TAU_INITIAL_MEAN + VocAlgorithm_SAMPLING_INTERVAL))); + params->m_Mean_Variance_Estimator___Gamma_Initial_Variance = F16( + ((VocAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING * + VocAlgorithm_SAMPLING_INTERVAL) / + (VocAlgorithm_TAU_INITIAL_VARIANCE + VocAlgorithm_SAMPLING_INTERVAL))); + params->m_Mean_Variance_Estimator__Gamma_Mean = F16(0.); + params->m_Mean_Variance_Estimator__Gamma_Variance = F16(0.); + params->m_Mean_Variance_Estimator___Uptime_Gamma = F16(0.); + params->m_Mean_Variance_Estimator___Uptime_Gating = F16(0.); + params->m_Mean_Variance_Estimator___Gating_Duration_Minutes = F16(0.); +} + +static void +VocAlgorithm__mean_variance_estimator__set_states(VocAlgorithmParams* params, + fix16_t mean, fix16_t std, + fix16_t uptime_gamma) +{ + + params->m_Mean_Variance_Estimator___Mean = mean; + params->m_Mean_Variance_Estimator___Std = std; + params->m_Mean_Variance_Estimator___Uptime_Gamma = uptime_gamma; + params->m_Mean_Variance_Estimator___Initialized = true; +} + +static fix16_t +VocAlgorithm__mean_variance_estimator__get_std(VocAlgorithmParams* params) +{ + + return params->m_Mean_Variance_Estimator___Std; +} + +static fix16_t +VocAlgorithm__mean_variance_estimator__get_mean(VocAlgorithmParams* params) +{ + + return (params->m_Mean_Variance_Estimator___Mean + + params->m_Mean_Variance_Estimator___Sraw_Offset); +} + +static void VocAlgorithm__mean_variance_estimator___calculate_gamma( + VocAlgorithmParams* params, fix16_t voc_index_from_prior) +{ + + fix16_t uptime_limit; + fix16_t sigmoid_gamma_mean; + fix16_t gamma_mean; + fix16_t gating_threshold_mean; + fix16_t sigmoid_gating_mean; + fix16_t sigmoid_gamma_variance; + fix16_t gamma_variance; + fix16_t gating_threshold_variance; + fix16_t sigmoid_gating_variance; + + uptime_limit = F16((VocAlgorithm_MEAN_VARIANCE_ESTIMATOR__FIX16_MAX - + VocAlgorithm_SAMPLING_INTERVAL)); + if ((params->m_Mean_Variance_Estimator___Uptime_Gamma < uptime_limit)) + { + params->m_Mean_Variance_Estimator___Uptime_Gamma = + (params->m_Mean_Variance_Estimator___Uptime_Gamma + + F16(VocAlgorithm_SAMPLING_INTERVAL)); + } + if ((params->m_Mean_Variance_Estimator___Uptime_Gating < uptime_limit)) + { + params->m_Mean_Variance_Estimator___Uptime_Gating = + (params->m_Mean_Variance_Estimator___Uptime_Gating + + F16(VocAlgorithm_SAMPLING_INTERVAL)); + } + VocAlgorithm__mean_variance_estimator___sigmoid__set_parameters( + params, F16(1.), F16(VocAlgorithm_INIT_DURATION_MEAN), + F16(VocAlgorithm_INIT_TRANSITION_MEAN)); + sigmoid_gamma_mean = + VocAlgorithm__mean_variance_estimator___sigmoid__process( + params, params->m_Mean_Variance_Estimator___Uptime_Gamma); + gamma_mean = + (params->m_Mean_Variance_Estimator___Gamma + + (fix16_mul((params->m_Mean_Variance_Estimator___Gamma_Initial_Mean - + params->m_Mean_Variance_Estimator___Gamma), + sigmoid_gamma_mean))); + gating_threshold_mean = + (F16(VocAlgorithm_GATING_THRESHOLD) + + (fix16_mul( + F16((VocAlgorithm_GATING_THRESHOLD_INITIAL - + VocAlgorithm_GATING_THRESHOLD)), + VocAlgorithm__mean_variance_estimator___sigmoid__process( + params, params->m_Mean_Variance_Estimator___Uptime_Gating)))); + VocAlgorithm__mean_variance_estimator___sigmoid__set_parameters( + params, F16(1.), gating_threshold_mean, + F16(VocAlgorithm_GATING_THRESHOLD_TRANSITION)); + sigmoid_gating_mean = + VocAlgorithm__mean_variance_estimator___sigmoid__process( + params, voc_index_from_prior); + params->m_Mean_Variance_Estimator__Gamma_Mean = + (fix16_mul(sigmoid_gating_mean, gamma_mean)); + VocAlgorithm__mean_variance_estimator___sigmoid__set_parameters( + params, F16(1.), F16(VocAlgorithm_INIT_DURATION_VARIANCE), + F16(VocAlgorithm_INIT_TRANSITION_VARIANCE)); + sigmoid_gamma_variance = + VocAlgorithm__mean_variance_estimator___sigmoid__process( + params, params->m_Mean_Variance_Estimator___Uptime_Gamma); + gamma_variance = + (params->m_Mean_Variance_Estimator___Gamma + + (fix16_mul( + (params->m_Mean_Variance_Estimator___Gamma_Initial_Variance - + params->m_Mean_Variance_Estimator___Gamma), + (sigmoid_gamma_variance - sigmoid_gamma_mean)))); + gating_threshold_variance = + (F16(VocAlgorithm_GATING_THRESHOLD) + + (fix16_mul( + F16((VocAlgorithm_GATING_THRESHOLD_INITIAL - + VocAlgorithm_GATING_THRESHOLD)), + VocAlgorithm__mean_variance_estimator___sigmoid__process( + params, params->m_Mean_Variance_Estimator___Uptime_Gating)))); + VocAlgorithm__mean_variance_estimator___sigmoid__set_parameters( + params, F16(1.), gating_threshold_variance, + F16(VocAlgorithm_GATING_THRESHOLD_TRANSITION)); + sigmoid_gating_variance = + VocAlgorithm__mean_variance_estimator___sigmoid__process( + params, voc_index_from_prior); + params->m_Mean_Variance_Estimator__Gamma_Variance = + (fix16_mul(sigmoid_gating_variance, gamma_variance)); + params->m_Mean_Variance_Estimator___Gating_Duration_Minutes = + (params->m_Mean_Variance_Estimator___Gating_Duration_Minutes + + (fix16_mul(F16((VocAlgorithm_SAMPLING_INTERVAL / 60.)), + ((fix16_mul((F16(1.) - sigmoid_gating_mean), + F16((1. + VocAlgorithm_GATING_MAX_RATIO)))) - + F16(VocAlgorithm_GATING_MAX_RATIO))))); + if ((params->m_Mean_Variance_Estimator___Gating_Duration_Minutes < + F16(0.))) + { + params->m_Mean_Variance_Estimator___Gating_Duration_Minutes = F16(0.); + } + if ((params->m_Mean_Variance_Estimator___Gating_Duration_Minutes > + params->m_Mean_Variance_Estimator__Gating_Max_Duration_Minutes)) + { + params->m_Mean_Variance_Estimator___Uptime_Gating = F16(0.); + } +} + +static void VocAlgorithm__mean_variance_estimator__process( + VocAlgorithmParams* params, fix16_t sraw, fix16_t voc_index_from_prior) +{ + + fix16_t delta_sgp; + fix16_t c; + fix16_t additional_scaling; + + if ((params->m_Mean_Variance_Estimator___Initialized == false)) + { + params->m_Mean_Variance_Estimator___Initialized = true; + params->m_Mean_Variance_Estimator___Sraw_Offset = sraw; + params->m_Mean_Variance_Estimator___Mean = F16(0.); + } + else + { + if (((params->m_Mean_Variance_Estimator___Mean >= F16(100.)) || + (params->m_Mean_Variance_Estimator___Mean <= F16(-100.)))) + { + params->m_Mean_Variance_Estimator___Sraw_Offset = + (params->m_Mean_Variance_Estimator___Sraw_Offset + + params->m_Mean_Variance_Estimator___Mean); + params->m_Mean_Variance_Estimator___Mean = F16(0.); + } + sraw = (sraw - params->m_Mean_Variance_Estimator___Sraw_Offset); + VocAlgorithm__mean_variance_estimator___calculate_gamma( + params, voc_index_from_prior); + delta_sgp = (fix16_div( + (sraw - params->m_Mean_Variance_Estimator___Mean), + F16(VocAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING))); + if ((delta_sgp < F16(0.))) + { + c = (params->m_Mean_Variance_Estimator___Std - delta_sgp); + } + else + { + c = (params->m_Mean_Variance_Estimator___Std + delta_sgp); + } + additional_scaling = F16(1.); + if ((c > F16(1440.))) + { + additional_scaling = F16(4.); + } + params->m_Mean_Variance_Estimator___Std = (fix16_mul( + fix16_sqrt((fix16_mul( + additional_scaling, + (F16(VocAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING) - + params->m_Mean_Variance_Estimator__Gamma_Variance)))), + fix16_sqrt(( + (fix16_mul( + params->m_Mean_Variance_Estimator___Std, + (fix16_div( + params->m_Mean_Variance_Estimator___Std, + (fix16_mul( + F16(VocAlgorithm_MEAN_VARIANCE_ESTIMATOR__GAMMA_SCALING), + additional_scaling)))))) + + (fix16_mul( + (fix16_div( + (fix16_mul( + params->m_Mean_Variance_Estimator__Gamma_Variance, + delta_sgp)), + additional_scaling)), + delta_sgp)))))); + params->m_Mean_Variance_Estimator___Mean = + (params->m_Mean_Variance_Estimator___Mean + + (fix16_mul(params->m_Mean_Variance_Estimator__Gamma_Mean, + delta_sgp))); + } +} + +static void VocAlgorithm__mean_variance_estimator___sigmoid__init( + VocAlgorithmParams* params) +{ + + VocAlgorithm__mean_variance_estimator___sigmoid__set_parameters( + params, F16(0.), F16(0.), F16(0.)); +} + +static void VocAlgorithm__mean_variance_estimator___sigmoid__set_parameters( + VocAlgorithmParams* params, fix16_t L, fix16_t X0, fix16_t K) +{ + + params->m_Mean_Variance_Estimator___Sigmoid__L = L; + params->m_Mean_Variance_Estimator___Sigmoid__K = K; + params->m_Mean_Variance_Estimator___Sigmoid__X0 = X0; +} + +static fix16_t VocAlgorithm__mean_variance_estimator___sigmoid__process( + VocAlgorithmParams* params, fix16_t sample) +{ + + fix16_t x; + + x = (fix16_mul(params->m_Mean_Variance_Estimator___Sigmoid__K, + (sample - params->m_Mean_Variance_Estimator___Sigmoid__X0))); + if ((x < F16(-50.))) + { + return params->m_Mean_Variance_Estimator___Sigmoid__L; + } + else if ((x > F16(50.))) + { + return F16(0.); + } + else + { + return (fix16_div(params->m_Mean_Variance_Estimator___Sigmoid__L, + (F16(1.) + fix16_exp(x)))); + } +} + +static void VocAlgorithm__mox_model__init(VocAlgorithmParams* params) +{ + + VocAlgorithm__mox_model__set_parameters(params, F16(1.), F16(0.)); +} + +static void VocAlgorithm__mox_model__set_parameters(VocAlgorithmParams* params, + fix16_t SRAW_STD, + fix16_t SRAW_MEAN) +{ + + params->m_Mox_Model__Sraw_Std = SRAW_STD; + params->m_Mox_Model__Sraw_Mean = SRAW_MEAN; +} + +static fix16_t VocAlgorithm__mox_model__process(VocAlgorithmParams* params, + fix16_t sraw) +{ + + return (fix16_mul((fix16_div((sraw - params->m_Mox_Model__Sraw_Mean), + (-(params->m_Mox_Model__Sraw_Std + + F16(VocAlgorithm_SRAW_STD_BONUS))))), + F16(VocAlgorithm_VOC_INDEX_GAIN))); +} + +static void VocAlgorithm__sigmoid_scaled__init(VocAlgorithmParams* params) +{ + + VocAlgorithm__sigmoid_scaled__set_parameters(params, F16(0.)); +} + +static void +VocAlgorithm__sigmoid_scaled__set_parameters(VocAlgorithmParams* params, + fix16_t offset) +{ + + params->m_Sigmoid_Scaled__Offset = offset; +} + +static fix16_t VocAlgorithm__sigmoid_scaled__process(VocAlgorithmParams* params, + fix16_t sample) +{ + + fix16_t x; + fix16_t shift; + + x = (fix16_mul(F16(VocAlgorithm_SIGMOID_K), + (sample - F16(VocAlgorithm_SIGMOID_X0)))); + if ((x < F16(-50.))) + { + return F16(VocAlgorithm_SIGMOID_L); + } + else if ((x > F16(50.))) + { + return F16(0.); + } + else + { + if ((sample >= F16(0.))) + { + shift = (fix16_div( + (F16(VocAlgorithm_SIGMOID_L) - + (fix16_mul(F16(5.), params->m_Sigmoid_Scaled__Offset))), + F16(4.))); + return ((fix16_div((F16(VocAlgorithm_SIGMOID_L) + shift), + (F16(1.) + fix16_exp(x)))) - + shift); + } + else + { + return (fix16_mul( + (fix16_div(params->m_Sigmoid_Scaled__Offset, + F16(VocAlgorithm_VOC_INDEX_OFFSET_DEFAULT))), + (fix16_div(F16(VocAlgorithm_SIGMOID_L), + (F16(1.) + fix16_exp(x)))))); + } + } +} + +static void VocAlgorithm__adaptive_lowpass__init(VocAlgorithmParams* params) +{ + + VocAlgorithm__adaptive_lowpass__set_parameters(params); +} + +static void +VocAlgorithm__adaptive_lowpass__set_parameters(VocAlgorithmParams* params) +{ + + params->m_Adaptive_Lowpass__A1 = + F16((VocAlgorithm_SAMPLING_INTERVAL / + (VocAlgorithm_LP_TAU_FAST + VocAlgorithm_SAMPLING_INTERVAL))); + params->m_Adaptive_Lowpass__A2 = + F16((VocAlgorithm_SAMPLING_INTERVAL / + (VocAlgorithm_LP_TAU_SLOW + VocAlgorithm_SAMPLING_INTERVAL))); + params->m_Adaptive_Lowpass___Initialized = false; +} + +static fix16_t +VocAlgorithm__adaptive_lowpass__process(VocAlgorithmParams* params, + fix16_t sample) +{ + + fix16_t abs_delta; + fix16_t F1; + fix16_t tau_a; + fix16_t a3; + + if ((params->m_Adaptive_Lowpass___Initialized == false)) + { + params->m_Adaptive_Lowpass___X1 = sample; + params->m_Adaptive_Lowpass___X2 = sample; + params->m_Adaptive_Lowpass___X3 = sample; + params->m_Adaptive_Lowpass___Initialized = true; + } + params->m_Adaptive_Lowpass___X1 = + ((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass__A1), + params->m_Adaptive_Lowpass___X1)) + + (fix16_mul(params->m_Adaptive_Lowpass__A1, sample))); + params->m_Adaptive_Lowpass___X2 = + ((fix16_mul((F16(1.) - params->m_Adaptive_Lowpass__A2), + params->m_Adaptive_Lowpass___X2)) + + (fix16_mul(params->m_Adaptive_Lowpass__A2, sample))); + abs_delta = + (params->m_Adaptive_Lowpass___X1 - params->m_Adaptive_Lowpass___X2); + if ((abs_delta < F16(0.))) + { + abs_delta = (-abs_delta); + } + F1 = fix16_exp((fix16_mul(F16(VocAlgorithm_LP_ALPHA), abs_delta))); + tau_a = + ((fix16_mul(F16((VocAlgorithm_LP_TAU_SLOW - VocAlgorithm_LP_TAU_FAST)), + F1)) + + F16(VocAlgorithm_LP_TAU_FAST)); + a3 = (fix16_div(F16(VocAlgorithm_SAMPLING_INTERVAL), + (F16(VocAlgorithm_SAMPLING_INTERVAL) + tau_a))); + params->m_Adaptive_Lowpass___X3 = + ((fix16_mul((F16(1.) - a3), params->m_Adaptive_Lowpass___X3)) + + (fix16_mul(a3, sample))); + return params->m_Adaptive_Lowpass___X3; +} diff --git a/sgp40/sgp40.c b/sgp40/sgp40.c new file mode 100644 index 0000000..6a01e76 --- /dev/null +++ b/sgp40/sgp40.c @@ -0,0 +1,248 @@ +/** + * @file sgp40.c + * + * ESP-IDF driver for SGP40 Indoor Air Quality Sensor for VOC Measurements + * + * Copyright (c) 2020 Ruslan V. Uss + * + * BSD Licensed as described in the file LICENSE + */ +#include +#include +#include +#include +#include "sgp40.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +#define I2C_FREQ_HZ 400000 + +static const char *TAG = "sgp40"; + +#define CMD_SOFT_RESET 0x0006 +#define CMD_FEATURESET 0x202f +#define CMD_MEASURE_RAW 0x260f +#define CMD_SELF_TEST 0x280e +#define CMD_SERIAL 0x3682 +#define CMD_HEATER_OFF 0x3615 + +#define TIME_SOFT_RESET 10 +#define TIME_FEATURESET 10 +#define TIME_MEASURE_RAW 30 +#define TIME_SELF_TEST 250 +#define TIME_HEATER_OFF 10 +#define TIME_SERIAL 10 + +#define SELF_TEST_OK 0xd400 + +#define CHECK(x) do { esp_err_t __; if ((__ = x) != ESP_OK) return __; } while (0) +#define CHECK_ARG(ARG) do { if (!(ARG)) return ESP_ERR_INVALID_ARG; } while (0) + +typedef struct +{ + i2c_master_dev_handle_t i2c_handle; + uint16_t serial[3]; + uint16_t featureset; + VocAlgorithmParams voc; +} sgp40_dev_t; + +static uint8_t crc8(const uint8_t *data, size_t count) +{ + uint8_t res = 0xff; + + for (size_t i = 0; i < count; ++i) + { + res ^= data[i]; + for (uint8_t bit = 8; bit > 0; --bit) + { + if (res & 0x80) + res = (res << 1) ^ 0x31; + else + res = (res << 1); + } + } + return res; +} + +static inline uint16_t swap(uint16_t v) +{ + return (v << 8) | (v >> 8); +} + +static esp_err_t send_cmd(i2c_master_dev_handle_t dev_handle, uint16_t cmd, uint16_t *data, size_t words) +{ + uint8_t buf[2 + words * 3]; + // add command + *(uint16_t *)buf = swap(cmd); + if (data && words) + // add arguments + for (size_t i = 0; i < words; i++) + { + uint8_t *p = buf + 2 + i * 3; + *(uint16_t *)p = swap(data[i]); + *(p + 2) = crc8(p, 2); + } + + ESP_LOGV(TAG, "Sending buffer:"); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, buf, sizeof(buf), ESP_LOG_VERBOSE); + + return i2c_master_transmit(dev_handle, buf, sizeof(buf), pdMS_TO_TICKS(1000)); +} + +static esp_err_t read_resp(i2c_master_dev_handle_t dev_handle, uint16_t *data, size_t words) +{ + uint8_t buf[words * 3]; + CHECK(i2c_master_receive(dev_handle, buf, sizeof(buf), pdMS_TO_TICKS(1000))); + + ESP_LOGV(TAG, "Received buffer:"); + ESP_LOG_BUFFER_HEX_LEVEL(TAG, buf, sizeof(buf), ESP_LOG_VERBOSE); + + for (size_t i = 0; i < words; i++) + { + uint8_t *p = buf + i * 3; + uint8_t crc = crc8(p, 2); + if (crc != *(p + 2)) + { + ESP_LOGE(TAG, "Invalid CRC 0x%02x, expected 0x%02x", crc, *(p + 2)); + return ESP_ERR_INVALID_CRC; + } + data[i] = swap(*(uint16_t *)p); + } + return ESP_OK; +} + +static esp_err_t execute_cmd(sgp40_dev_t *dev, uint16_t cmd, uint32_t timeout_ms, + uint16_t *out_data, size_t out_words, uint16_t *in_data, size_t in_words) +{ + CHECK_ARG(dev); + + CHECK(send_cmd(dev->i2c_handle, cmd, out_data, out_words)); + + if (timeout_ms) + vTaskDelay(pdMS_TO_TICKS(timeout_ms)); + + if (in_data && in_words) + CHECK(read_resp(dev->i2c_handle, in_data, in_words)); + + return ESP_OK; +} + +//////////////////////////////////////////////////////////////////////////////// + +esp_err_t sgp40_create(i2c_master_bus_handle_t bus_handle, const uint8_t dev_addr, sgp40_handle_t *handle_ret) +{ + CHECK_ARG(bus_handle && handle_ret); + + sgp40_dev_t *dev = calloc(1, sizeof(sgp40_dev_t)); + if (!dev) + return ESP_ERR_NO_MEM; + + const i2c_device_config_t dev_cfg = { + .dev_addr_length = I2C_ADDR_BIT_LEN_7, + .device_address = dev_addr, + .scl_speed_hz = I2C_FREQ_HZ, + }; + + esp_err_t ret = i2c_master_bus_add_device(bus_handle, &dev_cfg, &dev->i2c_handle); + if (ret != ESP_OK) + { + free(dev); + return ret; + } + + ret = execute_cmd(dev, CMD_SERIAL, TIME_SERIAL, NULL, 0, dev->serial, 3); + if (ret == ESP_OK) + ret = execute_cmd(dev, CMD_FEATURESET, TIME_FEATURESET, NULL, 0, &dev->featureset, 1); + + if (ret != ESP_OK) + { + i2c_master_bus_rm_device(dev->i2c_handle); + free(dev); + return ret; + } + + ESP_LOGD(TAG, "Device found. S/N: 0x%04x%04x%04x, featureset 0x%04x", + dev->serial[0], dev->serial[1], dev->serial[2], dev->featureset); + + VocAlgorithm_init(&dev->voc); + + *handle_ret = (sgp40_handle_t)dev; + return ESP_OK; +} + +esp_err_t sgp40_delete(sgp40_handle_t sensor) +{ + CHECK_ARG(sensor); + + sgp40_dev_t *dev = (sgp40_dev_t *)sensor; + esp_err_t ret = i2c_master_bus_rm_device(dev->i2c_handle); + free(dev); + return ret; +} + +esp_err_t sgp40_soft_reset(sgp40_handle_t sensor) +{ + CHECK_ARG(sensor); + + return execute_cmd((sgp40_dev_t *)sensor, CMD_SOFT_RESET, TIME_SOFT_RESET, NULL, 0, NULL, 0); +} + +esp_err_t sgp40_self_test(sgp40_handle_t sensor) +{ + CHECK_ARG(sensor); + + uint16_t res; + CHECK(execute_cmd((sgp40_dev_t *)sensor, CMD_SELF_TEST, TIME_SELF_TEST, NULL, 0, &res, 1)); + + return res == SELF_TEST_OK ? ESP_OK : ESP_FAIL; +} + +esp_err_t sgp40_heater_off(sgp40_handle_t sensor) +{ + CHECK_ARG(sensor); + + return execute_cmd((sgp40_dev_t *)sensor, CMD_HEATER_OFF, TIME_HEATER_OFF, NULL, 0, NULL, 0); +} + +esp_err_t sgp40_measure_raw(sgp40_handle_t sensor, float humidity, float temperature, uint16_t *raw) +{ + CHECK_ARG(sensor && raw); + + uint16_t params[2]; + if (isnan(humidity) || isnan(temperature)) + { + params[0] = 0x8000; + params[1] = 0x6666; + ESP_LOGW(TAG, "Uncompensated measurement"); + } + else + { + if (humidity < 0) + humidity = 0; + else if (humidity > 100) + humidity = 100; + + if (temperature < -45) + temperature = -45; + else if (temperature > 129.76) + temperature = 129.76; + + params[0] = (uint16_t)(humidity / 100.0 * 65536); + params[1] = (uint16_t)((temperature + 45) / 175.0 * 65535); + } + + return execute_cmd((sgp40_dev_t *)sensor, CMD_MEASURE_RAW, TIME_MEASURE_RAW, params, 2, raw, 1); +} + +esp_err_t sgp40_measure_voc(sgp40_handle_t sensor, float humidity, float temperature, int32_t *voc_index) +{ + CHECK_ARG(sensor && voc_index); + + sgp40_dev_t *dev = (sgp40_dev_t *)sensor; + + uint16_t raw; + CHECK(sgp40_measure_raw(sensor, humidity, temperature, &raw)); + VocAlgorithm_process(&dev->voc, raw, voc_index); + + return ESP_OK; +}