blob: 29a1af54c95a27defa06d17d29050bce9fb81c31 [file] [log] [blame]
/*
* Copyright (c) 2018 Peter Bigot Consulting, LLC
* Copyright (c) 2018 Linaro Ltd.
*
* SPDX-License-Identifier: Apache-2.0
*/
#define DT_DRV_COMPAT ams_ccs811
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/drivers/i2c.h>
#include <zephyr/kernel.h>
#include <zephyr/sys/byteorder.h>
#include <zephyr/sys/util.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/sys/__assert.h>
#include <zephyr/logging/log.h>
#include "ccs811.h"
#define WAKE_PIN DT_INST_GPIO_PIN(0, wake_gpios)
#define RESET_PIN DT_INST_GPIO_PIN(0, reset_gpios)
LOG_MODULE_REGISTER(CCS811, CONFIG_SENSOR_LOG_LEVEL);
#if DT_INST_NODE_HAS_PROP(0, wake_gpios)
static void set_wake(const struct device *dev, bool enable)
{
struct ccs811_data *drv_data = dev->data;
gpio_pin_set(drv_data->wake_gpio, WAKE_PIN, enable);
if (enable) {
k_busy_wait(50); /* t_WAKE = 50 us */
} else {
k_busy_wait(20); /* t_DWAKE = 20 us */
}
}
#else
#define set_wake(...)
#endif
/* Get STATUS register in low 8 bits, and if ERROR is set put ERROR_ID
* in bits 8..15. These registers are available in both boot and
* application mode.
*/
static int fetch_status(const struct device *dev)
{
struct ccs811_data *drv_data = dev->data;
uint8_t status;
int rv;
if (i2c_reg_read_byte(drv_data->i2c, DT_INST_REG_ADDR(0),
CCS811_REG_STATUS, &status) < 0) {
LOG_ERR("Failed to read Status register");
return -EIO;
}
rv = status;
if (status & CCS811_STATUS_ERROR) {
uint8_t error_id;
if (i2c_reg_read_byte(drv_data->i2c, DT_INST_REG_ADDR(0),
CCS811_REG_ERROR_ID, &error_id) < 0) {
LOG_ERR("Failed to read ERROR_ID register");
return -EIO;
}
rv |= (error_id << 8);
}
return rv;
}
static inline uint8_t error_from_status(int status)
{
return status >> 8;
}
const struct ccs811_result_type *ccs811_result(const struct device *dev)
{
struct ccs811_data *drv_data = dev->data;
return &drv_data->result;
}
int ccs811_configver_fetch(const struct device *dev,
struct ccs811_configver_type *ptr)
{
struct ccs811_data *drv_data = dev->data;
uint8_t cmd;
int rc;
if (!ptr) {
return -EINVAL;
}
set_wake(dev, true);
cmd = CCS811_REG_HW_VERSION;
rc = i2c_write_read(drv_data->i2c, DT_INST_REG_ADDR(0),
&cmd, sizeof(cmd),
&ptr->hw_version, sizeof(ptr->hw_version));
if (rc == 0) {
cmd = CCS811_REG_FW_BOOT_VERSION;
rc = i2c_write_read(drv_data->i2c, DT_INST_REG_ADDR(0),
&cmd, sizeof(cmd),
(uint8_t *)&ptr->fw_boot_version,
sizeof(ptr->fw_boot_version));
ptr->fw_boot_version = sys_be16_to_cpu(ptr->fw_boot_version);
}
if (rc == 0) {
cmd = CCS811_REG_FW_APP_VERSION;
rc = i2c_write_read(drv_data->i2c, DT_INST_REG_ADDR(0),
&cmd, sizeof(cmd),
(uint8_t *)&ptr->fw_app_version,
sizeof(ptr->fw_app_version));
ptr->fw_app_version = sys_be16_to_cpu(ptr->fw_app_version);
}
if (rc == 0) {
LOG_INF("HW %x FW %x APP %x",
ptr->hw_version, ptr->fw_boot_version,
ptr->fw_app_version);
}
set_wake(dev, false);
ptr->mode = drv_data->mode & CCS811_MODE_MSK;
return rc;
}
int ccs811_baseline_fetch(const struct device *dev)
{
const uint8_t cmd = CCS811_REG_BASELINE;
struct ccs811_data *drv_data = dev->data;
int rc;
uint16_t baseline;
set_wake(dev, true);
rc = i2c_write_read(drv_data->i2c, DT_INST_REG_ADDR(0),
&cmd, sizeof(cmd),
(uint8_t *)&baseline, sizeof(baseline));
set_wake(dev, false);
if (rc <= 0) {
rc = baseline;
}
return rc;
}
int ccs811_baseline_update(const struct device *dev,
uint16_t baseline)
{
struct ccs811_data *drv_data = dev->data;
uint8_t buf[1 + sizeof(baseline)];
int rc;
buf[0] = CCS811_REG_BASELINE;
memcpy(buf + 1, &baseline, sizeof(baseline));
set_wake(dev, true);
rc = i2c_write(drv_data->i2c, buf, sizeof(buf), DT_INST_REG_ADDR(0));
set_wake(dev, false);
return rc;
}
int ccs811_envdata_update(const struct device *dev,
const struct sensor_value *temperature,
const struct sensor_value *humidity)
{
struct ccs811_data *drv_data = dev->data;
int rc;
uint8_t buf[5] = { CCS811_REG_ENV_DATA };
/*
* Environment data are represented in a broken whole/fraction
* system that specified a 9-bit fractional part to represent
* milli-units. Since 1000 is greater than 512, the device
* actually only pays attention to the top bit, treating it as
* indicating 0.5. So we only write the first octet (7-bit
* while plus 1-bit half).
*
* Humidity is simple: scale it by two and round to the
* nearest half. Assume the fractional part is not
* negative.
*/
if (humidity) {
int value = 2 * humidity->val1;
value += (250000 + humidity->val2) / 500000;
if (value < 0) {
value = 0;
} else if (value > (2 * 100)) {
value = 2 * 100;
}
LOG_DBG("HUM %d.%06d becomes %d",
humidity->val1, humidity->val2, value);
buf[1] = value;
} else {
buf[1] = 2 * 50;
}
/*
* Temperature is offset from -25 Cel. Values below minimum
* store as zero. Default is 25 Cel. Again we round to the
* nearest half, complicated by Zephyr's signed representation
* of the fractional part.
*/
if (temperature) {
int value = 2 * temperature->val1;
if (temperature->val2 < 0) {
value += (250000 + temperature->val2) / 500000;
} else {
value += (-250000 + temperature->val2) / 500000;
}
if (value < (2 * -25)) {
value = 0;
} else {
value += 2 * 25;
}
LOG_DBG("TEMP %d.%06d becomes %d",
temperature->val1, temperature->val2, value);
buf[3] = value;
} else {
buf[3] = 2 * (25 + 25);
}
set_wake(dev, true);
rc = i2c_write(drv_data->i2c, buf, sizeof(buf), DT_INST_REG_ADDR(0));
set_wake(dev, false);
return rc;
}
static int ccs811_sample_fetch(const struct device *dev,
enum sensor_channel chan)
{
struct ccs811_data *drv_data = dev->data;
struct ccs811_result_type *rp = &drv_data->result;
const uint8_t cmd = CCS811_REG_ALG_RESULT_DATA;
int rc;
uint16_t buf[4] = { 0 };
unsigned int status;
set_wake(dev, true);
rc = i2c_write_read(drv_data->i2c, DT_INST_REG_ADDR(0),
&cmd, sizeof(cmd),
(uint8_t *)buf, sizeof(buf));
set_wake(dev, false);
if (rc < 0) {
return -EIO;
}
rp->co2 = sys_be16_to_cpu(buf[0]);
rp->voc = sys_be16_to_cpu(buf[1]);
status = sys_le16_to_cpu(buf[2]); /* sic */
rp->status = status;
rp->error = error_from_status(status);
rp->raw = sys_be16_to_cpu(buf[3]);
/* APP FW 1.1 does not set DATA_READY, but it does set CO2 to
* zero while it's starting up. Assume a non-zero CO2 with
* old firmware is valid for the purposes of claiming the
* fetch was fresh.
*/
if ((drv_data->app_fw_ver <= 0x11)
&& (rp->co2 != 0)) {
status |= CCS811_STATUS_DATA_READY;
}
return (status & CCS811_STATUS_DATA_READY) ? 0 : -EAGAIN;
}
static int ccs811_channel_get(const struct device *dev,
enum sensor_channel chan,
struct sensor_value *val)
{
struct ccs811_data *drv_data = dev->data;
const struct ccs811_result_type *rp = &drv_data->result;
uint32_t uval;
switch (chan) {
case SENSOR_CHAN_CO2:
val->val1 = rp->co2;
val->val2 = 0;
break;
case SENSOR_CHAN_VOC:
val->val1 = rp->voc;
val->val2 = 0;
break;
case SENSOR_CHAN_VOLTAGE:
/*
* Raw ADC readings are contained in least significant 10 bits
*/
uval = ((rp->raw & CCS811_RAW_VOLTAGE_MSK)
>> CCS811_RAW_VOLTAGE_POS) * CCS811_RAW_VOLTAGE_SCALE;
val->val1 = uval / 1000000U;
val->val2 = uval % 1000000;
break;
case SENSOR_CHAN_CURRENT:
/*
* Current readings are contained in most
* significant 6 bits in microAmps
*/
uval = ((rp->raw & CCS811_RAW_CURRENT_MSK)
>> CCS811_RAW_CURRENT_POS) * CCS811_RAW_CURRENT_SCALE;
val->val1 = uval / 1000000U;
val->val2 = uval % 1000000;
break;
default:
return -ENOTSUP;
}
return 0;
}
static const struct sensor_driver_api ccs811_driver_api = {
#ifdef CONFIG_CCS811_TRIGGER
.attr_set = ccs811_attr_set,
.trigger_set = ccs811_trigger_set,
#endif
.sample_fetch = ccs811_sample_fetch,
.channel_get = ccs811_channel_get,
};
static int switch_to_app_mode(const struct device *dev)
{
struct ccs811_data *drv_data = dev->data;
uint8_t buf;
int status;
LOG_DBG("Switching to Application mode...");
status = fetch_status(dev);
if (status < 0) {
return -EIO;
}
/* Check for the application firmware */
if (!(status & CCS811_STATUS_APP_VALID)) {
LOG_ERR("No Application firmware loaded");
return -EINVAL;
}
/* Check if already in application mode */
if (status & CCS811_STATUS_FW_MODE) {
LOG_DBG("CCS811 Already in application mode");
return 0;
}
buf = CCS811_REG_APP_START;
/* Set the device to application mode */
if (i2c_write(drv_data->i2c, &buf, 1, DT_INST_REG_ADDR(0)) < 0) {
LOG_ERR("Failed to set Application mode");
return -EIO;
}
k_msleep(1); /* t_APP_START */
status = fetch_status(dev);
if (status < 0) {
return -EIO;
}
/* Check for application mode */
if (!(status & CCS811_STATUS_FW_MODE)) {
LOG_ERR("Failed to start Application firmware");
return -EINVAL;
}
LOG_DBG("CCS811 Application firmware started!");
return 0;
}
#ifdef CONFIG_CCS811_TRIGGER
int ccs811_mutate_meas_mode(const struct device *dev,
uint8_t set,
uint8_t clear)
{
struct ccs811_data *drv_data = dev->data;
int rc = 0;
uint8_t mode = set | (drv_data->mode & ~clear);
/*
* Changing drive mode of a running system has preconditions.
* Only allow changing the interrupt generation.
*/
if ((set | clear) & ~(CCS811_MODE_DATARDY | CCS811_MODE_THRESH)) {
return -EINVAL;
}
if (mode != drv_data->mode) {
set_wake(dev, true);
rc = i2c_reg_write_byte(drv_data->i2c, DT_INST_REG_ADDR(0),
CCS811_REG_MEAS_MODE,
mode);
LOG_DBG("CCS811 meas mode change %02x to %02x got %d",
drv_data->mode, mode, rc);
if (rc < 0) {
LOG_ERR("Failed to set mode");
rc = -EIO;
} else {
drv_data->mode = mode;
rc = 0;
}
set_wake(dev, false);
}
return rc;
}
int ccs811_set_thresholds(const struct device *dev)
{
struct ccs811_data *drv_data = dev->data;
const uint8_t buf[5] = {
CCS811_REG_THRESHOLDS,
drv_data->co2_l2m >> 8,
drv_data->co2_l2m,
drv_data->co2_m2h >> 8,
drv_data->co2_m2h,
};
int rc;
set_wake(dev, true);
rc = i2c_write(drv_data->i2c, buf, sizeof(buf), DT_INST_REG_ADDR(0));
set_wake(dev, false);
return rc;
}
#endif /* CONFIG_CCS811_TRIGGER */
static int ccs811_init(const struct device *dev)
{
struct ccs811_data *drv_data = dev->data;
int ret = 0;
int status;
uint16_t fw_ver;
uint8_t cmd;
uint8_t hw_id;
*drv_data = (struct ccs811_data){ 0 };
drv_data->i2c = device_get_binding(DT_INST_BUS_LABEL(0));
if (drv_data->i2c == NULL) {
LOG_ERR("Failed to get pointer to %s device!",
DT_INST_BUS_LABEL(0));
return -EINVAL;
}
#if DT_INST_NODE_HAS_PROP(0, wake_gpios)
drv_data->wake_gpio = device_get_binding(DT_INST_GPIO_LABEL(0, wake_gpios));
if (drv_data->wake_gpio == NULL) {
LOG_ERR("Failed to get pointer to WAKE device: %s",
DT_INST_GPIO_LABEL(0, wake_gpios));
return -EINVAL;
}
/*
* Wakeup pin should be pulled low before initiating
* any I2C transfer. If it has been tied to GND by
* default, skip this part.
*/
gpio_pin_configure(drv_data->wake_gpio, WAKE_PIN,
GPIO_OUTPUT_INACTIVE
| DT_INST_GPIO_FLAGS(0, wake_gpios));
set_wake(dev, true);
k_msleep(1);
#endif
#if DT_INST_NODE_HAS_PROP(0, reset_gpios)
drv_data->reset_gpio = device_get_binding(DT_INST_GPIO_LABEL(0, reset_gpios));
if (drv_data->reset_gpio == NULL) {
LOG_ERR("Failed to get pointer to RESET device: %s",
DT_INST_GPIO_LABEL(0, reset_gpios));
return -EINVAL;
}
gpio_pin_configure(drv_data->reset_gpio, RESET_PIN,
GPIO_OUTPUT_ACTIVE
| DT_INST_GPIO_FLAGS(0, reset_gpios));
k_msleep(1);
#endif
#if DT_INST_NODE_HAS_PROP(0, irq_gpios)
drv_data->irq_gpio = device_get_binding(DT_INST_GPIO_LABEL(0, irq_gpios));
if (drv_data->irq_gpio == NULL) {
LOG_ERR("Failed to get pointer to INT device: %s",
DT_INST_GPIO_LABEL(0, irq_gpios));
return -EINVAL;
}
#endif
k_msleep(20); /* t_START assuming recent power-on */
/* Reset the device. This saves having to deal with detecting
* and validating any errors or configuration inconsistencies
* after a reset that left the device running.
*/
#if DT_INST_NODE_HAS_PROP(0, reset_gpios)
gpio_pin_set(drv_data->reset_gpio, RESET_PIN, 1);
k_busy_wait(15); /* t_RESET */
gpio_pin_set(drv_data->reset_gpio, RESET_PIN, 0);
#else
{
static uint8_t const reset_seq[] = {
0xFF, 0x11, 0xE5, 0x72, 0x8A,
};
if (i2c_write(drv_data->i2c, reset_seq, sizeof(reset_seq),
DT_INST_REG_ADDR(0)) < 0) {
LOG_ERR("Failed to issue SW reset");
ret = -EIO;
goto out;
}
}
#endif
k_msleep(2); /* t_START after reset */
/* Switch device to application mode */
ret = switch_to_app_mode(dev);
if (ret) {
goto out;
}
/* Check Hardware ID */
if (i2c_reg_read_byte(drv_data->i2c, DT_INST_REG_ADDR(0),
CCS811_REG_HW_ID, &hw_id) < 0) {
LOG_ERR("Failed to read Hardware ID register");
ret = -EIO;
goto out;
}
if (hw_id != CCS881_HW_ID) {
LOG_ERR("Hardware ID mismatch!");
ret = -EINVAL;
goto out;
}
/* Check application firmware version (first byte) */
cmd = CCS811_REG_FW_APP_VERSION;
if (i2c_write_read(drv_data->i2c, DT_INST_REG_ADDR(0),
&cmd, sizeof(cmd),
&fw_ver, sizeof(fw_ver)) < 0) {
LOG_ERR("Failed to read App Firmware Version register");
ret = -EIO;
goto out;
}
fw_ver = sys_be16_to_cpu(fw_ver);
LOG_INF("App FW %04x", fw_ver);
drv_data->app_fw_ver = fw_ver >> 8U;
/* Configure measurement mode */
uint8_t meas_mode = CCS811_MODE_IDLE;
#ifdef CONFIG_CCS811_DRIVE_MODE_1
meas_mode = CCS811_MODE_IAQ_1SEC;
#elif defined(CONFIG_CCS811_DRIVE_MODE_2)
meas_mode = CCS811_MODE_IAQ_10SEC;
#elif defined(CONFIG_CCS811_DRIVE_MODE_3)
meas_mode = CCS811_MODE_IAQ_60SEC;
#elif defined(CONFIG_CCS811_DRIVE_MODE_4)
meas_mode = CCS811_MODE_IAQ_250MSEC;
#endif
if (i2c_reg_write_byte(drv_data->i2c, DT_INST_REG_ADDR(0),
CCS811_REG_MEAS_MODE,
meas_mode) < 0) {
LOG_ERR("Failed to set Measurement mode");
ret = -EIO;
goto out;
}
drv_data->mode = meas_mode;
/* Check for error */
status = fetch_status(dev);
if (status < 0) {
ret = -EIO;
goto out;
}
if (status & CCS811_STATUS_ERROR) {
LOG_ERR("CCS811 Error %02x during sensor configuration",
error_from_status(status));
ret = -EINVAL;
goto out;
}
#ifdef CONFIG_CCS811_TRIGGER
ret = ccs811_init_interrupt(dev);
LOG_DBG("CCS811 interrupt init got %d", ret);
#endif
out:
set_wake(dev, false);
return ret;
}
static struct ccs811_data ccs811_driver;
DEVICE_DT_INST_DEFINE(0, ccs811_init, NULL,
&ccs811_driver, NULL,
POST_KERNEL, CONFIG_SENSOR_INIT_PRIORITY,
&ccs811_driver_api);