/** * @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; }