diff --git a/drivers/sensors/CMakeLists.txt b/drivers/sensors/CMakeLists.txt index fc56aca87ea..893ada9b9d3 100644 --- a/drivers/sensors/CMakeLists.txt +++ b/drivers/sensors/CMakeLists.txt @@ -172,6 +172,10 @@ if(CONFIG_SENSORS) list(APPEND SRCS lps25h.c) endif() + if(CONFIG_SENSORS_CCS811) + list(APPEND SRCS ccs811_uorb.c) + endif() + if(CONFIG_ADXL345_I2C) list(APPEND SRCS adxl345_i2c.c) endif() diff --git a/drivers/sensors/Kconfig b/drivers/sensors/Kconfig index 52e2ad178c6..0cf4f6adba4 100644 --- a/drivers/sensors/Kconfig +++ b/drivers/sensors/Kconfig @@ -722,6 +722,23 @@ config SENSORS_BME688_THREAD_STACKSIZE endif # SENSORS_BME688 +config SENSORS_CCS811 + bool "ams CCS811 Gas Sensor" + default n + select I2C + ---help--- + Enable driver for the ams CCS811 digital gas sensor for monitoring + indoor air quality (eCO2 and TVOC). + +if SENSORS_CCS811 + +config CCS811_I2C_FREQUENCY + int "CCS811 I2C frequency" + default 100000 + range 1 400000 + +endif # SENSORS_CCS811 + config SENSORS_DHTXX bool "DHTxx humidity/temperature Sensor support" default n diff --git a/drivers/sensors/Make.defs b/drivers/sensors/Make.defs index 04b34616f08..306d57b9df2 100644 --- a/drivers/sensors/Make.defs +++ b/drivers/sensors/Make.defs @@ -226,6 +226,10 @@ ifeq ($(CONFIG_SENSORS_BME688),y) CSRCS += bme688_uorb.c endif +ifeq ($(CONFIG_SENSORS_CCS811),y) + CSRCS += ccs811_uorb.c +endif + ifeq ($(CONFIG_SENSORS_ISL29023),y) CSRCS += isl29023.c endif diff --git a/drivers/sensors/ccs811_uorb.c b/drivers/sensors/ccs811_uorb.c new file mode 100644 index 00000000000..ff9a15bb3cc --- /dev/null +++ b/drivers/sensors/ccs811_uorb.c @@ -0,0 +1,666 @@ +/**************************************************************************** + * drivers/sensors/ccs811_uorb.c + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you 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. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +/**************************************************************************** + * Pre-Processor Definitions + ****************************************************************************/ + +/* Time to wait after a nWAKE falling edge before the CCS811 is ready for an + * I2C transaction (datasheet t_WAKE is 50us). + */ + +#define CCS811_TWAKE_US (100) + +/* Number of times a single I2C transaction is retried. The first access + * after an idle gap occasionally NAKs while the sensor wakes. + */ + +#define CCS811_XFER_RETRIES (3) + +/* Number of times the whole boot-to-application bring-up is retried. */ + +#define CCS811_START_RETRIES (2) + +/* Registers */ + +#define CCS811_STATUS (0x00) +#define CCS811_MEAS_MODE (0x01) +#define CCS811_ALG_RESULT_DATA (0x02) +#define CCS811_RAW_DATA (0x03) +#define CCS811_ENV_DATA (0x05) +#define CCS811_THRESHOLDS (0x10) +#define CCS811_BASELINE (0x11) +#define CCS811_HW_ID (0x20) +#define CCS811_HW_VERSION (0x21) +#define CCS811_FW_BOOT_VERSION (0x23) +#define CCS811_FW_APP_VERSION (0x24) +#define CCS811_INTERNAL_STATE (0xa0) +#define CCS811_ERROR_ID (0xe0) +#define CCS811_SW_RESET (0xff) +#define CCS811_APP_ERASE (0xf1) +#define CCS811_APP_DATA (0xf2) +#define CCS811_APP_VERIFY (0xf3) +#define CCS811_APP_START (0xf4) + +/* HW_ID value */ + +#define CCS811_DEVID (0x81) + +/* STATUS register bits */ + +#define CCS811_STATUS_ERROR (1 << 0) +#define CCS811_STATUS_DATARDY (1 << 3) +#define CCS811_STATUS_APPVALID (1 << 4) +#define CCS811_STATUS_FWMODE (1 << 7) + +/* MEAS_MODE drive modes */ + +#define CCS811_DRIVE_MODE_IDLE (0 << 4) /* Idle, measurements disabled */ +#define CCS811_DRIVE_MODE_1SEC (1 << 4) /* Constant power, every 1 s */ +#define CCS811_DRIVE_MODE_10SEC (2 << 4) /* Pulse heating, every 10 s */ +#define CCS811_DRIVE_MODE_60SEC (3 << 4) /* Pulse heating, every 60 s */ + +/**************************************************************************** + * Private Types + ****************************************************************************/ + +/* Each measurement channel needs its own lower-half. */ + +struct ccs811_sensor_s +{ + FAR struct sensor_lowerhalf_s lower; /* Lower-half driver */ + FAR struct ccs811_dev_s *dev; /* Parent device */ + bool enabled; /* Channel activated */ +}; + +struct ccs811_dev_s +{ + struct ccs811_sensor_s co2; /* eCO2 lower-half */ + struct ccs811_sensor_s tvoc; /* TVOC lower-half */ + FAR struct i2c_master_s *i2c; /* I2C interface */ + CODE void (*wake)(bool on); /* Board nWAKE control (may be NULL) */ + uint8_t addr; /* I2C address */ + bool wake_held; /* nWAKE kept low across a burst */ + bool valid; /* Cached measurement available */ + uint64_t timestamp; /* Cached measurement timestamp */ + uint16_t eco2; /* Last eCO2 in ppm */ + uint16_t etvoc; /* Last TVOC in ppb */ + unsigned long interval; /* Polling interval in us */ +}; + +/**************************************************************************** + * Private Function Prototypes + ****************************************************************************/ + +static int ccs811_read_regs(FAR struct ccs811_dev_s *priv, uint8_t regaddr, + FAR uint8_t *regvals, int len); +static int ccs811_write(FAR struct ccs811_dev_s *priv, uint8_t regaddr, + FAR const uint8_t *regvals, int len); +static int ccs811_appstart(FAR struct ccs811_dev_s *priv); +static int ccs811_start(FAR struct ccs811_dev_s *priv); +static int ccs811_measure(FAR struct ccs811_dev_s *priv); + +/* Sensor lower-half operations */ + +static int ccs811_activate(FAR struct sensor_lowerhalf_s *lower, + FAR struct file *filep, bool enable); +static int ccs811_set_interval(FAR struct sensor_lowerhalf_s *lower, + FAR struct file *filep, + FAR uint32_t *period_us); +static int ccs811_fetch(FAR struct sensor_lowerhalf_s *lower, + FAR struct file *filep, FAR char *buffer, + size_t buflen); + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +static const struct sensor_ops_s g_ccs811_ops = +{ + .activate = ccs811_activate, + .set_interval = ccs811_set_interval, + .fetch = ccs811_fetch, +}; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: ccs811_wake_hold + * + * Description: + * Assert nWAKE and keep it asserted across a whole sequence of I2C + * transactions (the per-transaction pulse is suppressed while held). The + * boot->application transition must not be interrupted by nWAKE returning + * high, otherwise the sensor aborts the launch and stays in boot mode. + * + ****************************************************************************/ + +static void ccs811_wake_hold(FAR struct ccs811_dev_s *priv, bool hold) +{ + if (priv->wake == NULL) + { + return; + } + + if (hold) + { + priv->wake(true); + up_udelay(CCS811_TWAKE_US); + priv->wake_held = true; + } + else + { + priv->wake_held = false; + priv->wake(false); + } +} + +/**************************************************************************** + * Name: ccs811_read_regs + ****************************************************************************/ + +static int ccs811_read_regs(FAR struct ccs811_dev_s *priv, uint8_t regaddr, + FAR uint8_t *regvals, int len) +{ + struct i2c_config_s config; + int ret = -EIO; + int i; + + config.frequency = CONFIG_CCS811_I2C_FREQUENCY; + config.address = priv->addr; + config.addrlen = 7; + + for (i = 0; i < CCS811_XFER_RETRIES; i++) + { + if (priv->wake != NULL && !priv->wake_held) + { + priv->wake(true); /* nWAKE falling edge */ + up_udelay(CCS811_TWAKE_US); + } + + ret = i2c_writeread(priv->i2c, &config, ®addr, 1, regvals, len); + + if (priv->wake != NULL && !priv->wake_held) + { + priv->wake(false); /* release nWAKE */ + } + + if (ret >= 0) + { + break; + } + + nxsig_usleep(2000); + } + + return ret; +} + +/**************************************************************************** + * Name: ccs811_write + * + * Description: + * Write a command register optionally followed by data bytes. + * + ****************************************************************************/ + +static int ccs811_write(FAR struct ccs811_dev_s *priv, uint8_t regaddr, + FAR const uint8_t *regvals, int len) +{ + struct i2c_config_s config; + uint8_t buf[2]; + int ret = -EIO; + int i; + + config.frequency = CONFIG_CCS811_I2C_FREQUENCY; + config.address = priv->addr; + config.addrlen = 7; + + buf[0] = regaddr; + if (len > 0) + { + memcpy(&buf[1], regvals, len); + } + + for (i = 0; i < CCS811_XFER_RETRIES; i++) + { + if (priv->wake != NULL && !priv->wake_held) + { + priv->wake(true); /* nWAKE falling edge */ + up_udelay(CCS811_TWAKE_US); + } + + ret = i2c_write(priv->i2c, &config, buf, len + 1); + + if (priv->wake != NULL && !priv->wake_held) + { + priv->wake(false); /* release nWAKE */ + } + + if (ret >= 0) + { + break; + } + + nxsig_usleep(2000); + } + + return ret; +} + +/**************************************************************************** + * Name: ccs811_appstart + * + * Description: + * Verify the chip ID, switch to application mode and start periodic + * measurements. A single attempt. + * + ****************************************************************************/ + +static int ccs811_appstart(FAR struct ccs811_dev_s *priv) +{ + uint8_t regval; + int retries; + int ret; + + /* Wait for the sensor to respond and verify the hardware ID. These use + * the normal per-transaction nWAKE pulse so each attempt re-wakes the + * part. + */ + + retries = 20; + do + { + nxsig_usleep(20000); + ret = ccs811_read_regs(priv, CCS811_HW_ID, ®val, 1); + } + while (ret < 0 && --retries); + + if (ret < 0) + { + return ret; + } + + if (regval != CCS811_DEVID) + { + snerr("ERROR: Wrong device ID: 0x%02x\n", regval); + return -ENODEV; + } + + /* Poll STATUS until a valid application is reported. */ + + retries = 20; + do + { + nxsig_usleep(20000); + ret = ccs811_read_regs(priv, CCS811_STATUS, ®val, 1); + if (ret < 0) + { + return ret; + } + } + while ((regval & CCS811_STATUS_APPVALID) == 0 && --retries); + + if ((regval & CCS811_STATUS_APPVALID) == 0) + { + snerr("ERROR: CCS811 has no valid application (status=0x%02x)\n", + regval); + return -ENODEV; + } + + /* Launch the application firmware. nWAKE must stay asserted (low) across + * the whole boot->application transition: the sensor aborts the launch + * and falls back to boot mode if nWAKE returns high (i.e. it is allowed + * to sleep) before the application is running. Hold nWAKE low, issue + * APP_START, then poll STATUS tightly (back-to-back reads keep the part + * awake) until it reports application mode. During the launch STATUS can + * read back as 0xff/0xfd garbage, so only accept a clean value: FWMODE + * set with all reserved bits clear. + */ + + ccs811_wake_hold(priv, true); + + retries = 20; + do + { + ret = ccs811_write(priv, CCS811_APP_START, NULL, 0); + if (ret < 0) + { + goto errout; + } + + up_udelay(2000); + ret = ccs811_read_regs(priv, CCS811_STATUS, ®val, 1); + } + while ((ret < 0 || (regval & 0x66) != 0 || + (regval & CCS811_STATUS_FWMODE) == 0) && --retries); + + if (ret < 0) + { + goto errout; + } + + if ((regval & CCS811_STATUS_FWMODE) == 0) + { + snerr("ERROR: CCS811 failed to enter application mode (0x%02x)\n", + regval); + ret = -EIO; + goto errout; + } + + /* Select constant-power measurement mode, one sample every second. */ + + regval = CCS811_DRIVE_MODE_1SEC; + ret = ccs811_write(priv, CCS811_MEAS_MODE, ®val, 1); + +errout: + ccs811_wake_hold(priv, false); + return ret; +} + +/**************************************************************************** + * Name: ccs811_start + * + * Description: + * Bring the CCS811 into measurement mode. The boot-to-application + * transition is occasionally unreliable after a warm reset, so retry the + * whole sequence a few times before giving up. + * + ****************************************************************************/ + +static int ccs811_start(FAR struct ccs811_dev_s *priv) +{ + int ret = -EIO; + int i; + + for (i = 0; i < CCS811_START_RETRIES; i++) + { + ret = ccs811_appstart(priv); + if (ret >= 0) + { + break; + } + + nxsig_usleep(50000); + } + + return ret; +} + +/**************************************************************************** + * Name: ccs811_measure + * + * Description: + * Refresh the cached eCO2/TVOC values from the algorithm result data. + * A single ALG_RESULT_DATA read returns both values and clears DATARDY, + * so the result is cached and shared by both lower-halves. + * + ****************************************************************************/ + +static int ccs811_measure(FAR struct ccs811_dev_s *priv) +{ + uint8_t data[8]; + uint8_t status = 0; + int ret; + + ret = ccs811_read_regs(priv, CCS811_STATUS, &status, 1); + if (ret < 0) + { + return ret; + } + + /* If the sensor has dropped back into boot mode, re-run the application + * bring-up before trying to read a measurement. + */ + + if ((status & CCS811_STATUS_FWMODE) == 0) + { + ret = ccs811_appstart(priv); + if (ret < 0) + { + return ret; + } + + ret = ccs811_read_regs(priv, CCS811_STATUS, &status, 1); + if (ret < 0) + { + return ret; + } + } + + /* No new sample is ready yet - keep the previously reported value. */ + + if ((status & CCS811_STATUS_DATARDY) == 0) + { + return -EAGAIN; + } + + ret = ccs811_read_regs(priv, CCS811_ALG_RESULT_DATA, data, 8); + if (ret < 0) + { + return ret; + } + + /* data[4] holds the status, data[5] the error id */ + + if (data[4] & CCS811_STATUS_ERROR) + { + snerr("ERROR: CCS811 measurement error: 0x%02x\n", data[5]); + return -EIO; + } + + priv->eco2 = data[0] << 8 | data[1]; + priv->etvoc = data[2] << 8 | data[3]; + priv->timestamp = sensor_get_timestamp(); + priv->valid = true; + + return OK; +} + +/**************************************************************************** + * Name: ccs811_activate + ****************************************************************************/ + +static int ccs811_activate(FAR struct sensor_lowerhalf_s *lower, + FAR struct file *filep, bool enable) +{ + FAR struct ccs811_sensor_s *priv = + container_of(lower, FAR struct ccs811_sensor_s, lower); + + priv->enabled = enable; + return OK; +} + +/**************************************************************************** + * Name: ccs811_set_interval + ****************************************************************************/ + +static int ccs811_set_interval(FAR struct sensor_lowerhalf_s *lower, + FAR struct file *filep, + FAR uint32_t *period_us) +{ + FAR struct ccs811_sensor_s *priv = + container_of(lower, FAR struct ccs811_sensor_s, lower); + + priv->dev->interval = *period_us; + return OK; +} + +/**************************************************************************** + * Name: ccs811_fetch + ****************************************************************************/ + +static int ccs811_fetch(FAR struct sensor_lowerhalf_s *lower, + FAR struct file *filep, FAR char *buffer, + size_t buflen) +{ + FAR struct ccs811_sensor_s *priv = + container_of(lower, FAR struct ccs811_sensor_s, lower); + FAR struct ccs811_dev_s *dev = priv->dev; + int ret; + + ret = ccs811_measure(dev); + if (ret < 0 && ret != -EAGAIN) + { + return ret; + } + + if (!dev->valid) + { + return -EAGAIN; + } + + if (lower->type == SENSOR_TYPE_CO2) + { + struct sensor_co2 co2; + + if (buflen != sizeof(co2)) + { + return -EINVAL; + } + + co2.timestamp = dev->timestamp; + co2.co2 = (float)dev->eco2; + memcpy(buffer, &co2, sizeof(co2)); + return sizeof(co2); + } + else + { + struct sensor_tvoc tvoc; + + if (buflen != sizeof(tvoc)) + { + return -EINVAL; + } + + tvoc.timestamp = dev->timestamp; + tvoc.tvoc = (float)dev->etvoc / 1000.0f; + memcpy(buffer, &tvoc, sizeof(tvoc)); + return sizeof(tvoc); + } +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: ccs811_register_uorb + * + * Description: + * Register the CCS811 eCO2 and TVOC sensors as UORB devices. + * + * Input Parameters: + * devno - The device number, used to build the device path. + * i2c - The I2C bus driver instance. + * addr - The I2C address of the CCS811. + * config - Board-specific configuration (may be NULL). + * + * Returned Value: + * Zero (OK) on success; a negated errno value on failure. + * + ****************************************************************************/ + +int ccs811_register_uorb(int devno, FAR struct i2c_master_s *i2c, + uint8_t addr, + FAR const struct ccs811_config_s *config) +{ + FAR struct ccs811_dev_s *priv; + int ret; + + DEBUGASSERT(i2c != NULL); + + priv = kmm_zalloc(sizeof(struct ccs811_dev_s)); + if (priv == NULL) + { + snerr("ERROR: Failed to allocate CCS811 instance\n"); + return -ENOMEM; + } + + priv->i2c = i2c; + priv->addr = addr; + priv->interval = 1000000; + + if (config != NULL) + { + priv->wake = config->wake; + } + + ret = ccs811_start(priv); + if (ret < 0) + { + snerr("ERROR: Failed to start CCS811: %d\n", ret); + goto errout; + } + + /* Register the eCO2 lower-half */ + + priv->co2.dev = priv; + priv->co2.lower.ops = &g_ccs811_ops; + priv->co2.lower.type = SENSOR_TYPE_CO2; + + ret = sensor_register(&priv->co2.lower, devno); + if (ret < 0) + { + snerr("ERROR: Failed to register CCS811 eCO2: %d\n", ret); + goto errout; + } + + /* Register the TVOC lower-half */ + + priv->tvoc.dev = priv; + priv->tvoc.lower.ops = &g_ccs811_ops; + priv->tvoc.lower.type = SENSOR_TYPE_TVOC; + + ret = sensor_register(&priv->tvoc.lower, devno); + if (ret < 0) + { + snerr("ERROR: Failed to register CCS811 TVOC: %d\n", ret); + sensor_unregister(&priv->co2.lower, devno); + goto errout; + } + + sninfo("CCS811 registered\n"); + return OK; + +errout: + kmm_free(priv); + return ret; +} diff --git a/include/nuttx/sensors/ccs811.h b/include/nuttx/sensors/ccs811.h new file mode 100644 index 00000000000..6a1372d690d --- /dev/null +++ b/include/nuttx/sensors/ccs811.h @@ -0,0 +1,82 @@ +/**************************************************************************** + * include/nuttx/sensors/ccs811.h + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you 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. + * + ****************************************************************************/ + +#ifndef __INCLUDE_NUTTX_SENSORS_CCS811_H +#define __INCLUDE_NUTTX_SENSORS_CCS811_H + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include + +#include + +/**************************************************************************** + * Public Types + ****************************************************************************/ + +struct ccs811_config_s +{ + /* Optional board callback to control the CCS811 nWAKE line around each + * I2C transaction (on=true asserts nWAKE, on=false releases it). Leave + * NULL when nWAKE is tied low in hardware. + */ + + CODE void (*wake)(bool on); +}; + +/**************************************************************************** + * Public Function Prototypes + ****************************************************************************/ + +#ifdef __cplusplus +extern "C" +{ +#endif + +/**************************************************************************** + * Name: ccs811_register_uorb + * + * Description: + * Register the CCS811 eCO2 and TVOC sensors as UORB devices. + * + * Input Parameters: + * devno - The device number, used to build the device path. + * i2c - The I2C bus driver instance. + * addr - The I2C address of the CCS811. + * config - Board-specific configuration (may be NULL). + * + * Returned Value: + * Zero (OK) on success; a negated errno value on failure. + * + ****************************************************************************/ + +int ccs811_register_uorb(int devno, FAR struct i2c_master_s *i2c, + uint8_t addr, + FAR const struct ccs811_config_s *config); + +#ifdef __cplusplus +} +#endif + +#endif /* __INCLUDE_NUTTX_SENSORS_CCS811_H */