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.
249 lines
6.4 KiB
C
249 lines
6.4 KiB
C
/**
|
|
* @file sgp40.c
|
|
*
|
|
* ESP-IDF driver for SGP40 Indoor Air Quality Sensor for VOC Measurements
|
|
*
|
|
* Copyright (c) 2020 Ruslan V. Uss <unclerus@gmail.com>
|
|
*
|
|
* BSD Licensed as described in the file LICENSE
|
|
*/
|
|
#include <stdlib.h>
|
|
#include <math.h>
|
|
#include <esp_err.h>
|
|
#include <esp_log.h>
|
|
#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;
|
|
}
|