From d4a38a8974d9cab888ea018444d9aee3ba0c7e8b Mon Sep 17 00:00:00 2001 From: Jussi Kivilinna Date: Wed, 9 Jan 2019 13:35:44 +0000 Subject: [PATCH] Merged in jussi_kivilinna/nuttx/sensirion_drivers (pull request #809) Drivers for Sensirion SCD30, SGP30 and SPS30 sensors * drivers/sensors: add driver for Sensirion SCD30 CO2 sensor * drivers/sensors: add driver for Sensirion SGP30 gas sensor * drivers/sensors: add driver for Sensirion SPS30 particulate matter sensor Approved-by: GregoryN --- drivers/sensors/Kconfig | 84 +++ drivers/sensors/Make.defs | 12 + drivers/sensors/scd30.c | 1112 ++++++++++++++++++++++++++++++++ drivers/sensors/sgp30.c | 1015 ++++++++++++++++++++++++++++++ drivers/sensors/sps30.c | 1116 +++++++++++++++++++++++++++++++++ include/nuttx/sensors/ioctl.h | 32 + include/nuttx/sensors/scd30.h | 90 +++ include/nuttx/sensors/sgp30.h | 99 +++ include/nuttx/sensors/sps30.h | 107 ++++ 9 files changed, 3667 insertions(+) create mode 100644 drivers/sensors/scd30.c create mode 100644 drivers/sensors/sgp30.c create mode 100644 drivers/sensors/sps30.c create mode 100644 include/nuttx/sensors/scd30.h create mode 100644 include/nuttx/sensors/sgp30.h create mode 100644 include/nuttx/sensors/sps30.h diff --git a/drivers/sensors/Kconfig b/drivers/sensors/Kconfig index 6576c8b924..4282d57c99 100644 --- a/drivers/sensors/Kconfig +++ b/drivers/sensors/Kconfig @@ -513,6 +513,59 @@ config LM92_I2C_FREQUENCY range 1 400000 depends on LM92 +config SENSORS_SCD30 + bool "Sensirion SCD30 CO2, humidity and temperature sensor" + default n + ---help--- + Enable driver support for the Sensirion SCD30 CO₂, humidity and + temperature sensor. + +if SENSORS_SCD30 + +config SCD30_I2C + bool "Sensirion SCD30 I2C mode" + default y + select I2C + +config SCD30_I2C_FREQUENCY + int "SCD30 I2C frequency" + default 100000 + range 1 100000 + depends on SCD30_I2C + ---help--- + I2C frequency for SCD30. Note, maximum supported frequency for + this sensor is 100kHz. + +config SCD30_DEBUG + bool "Debug support for the SCD30" + default n + ---help--- + Enables debug features for the SCD30 + +endif # SENSORS_SCD30 + +config SENSORS_SGP30 + bool "Sensirion SGP30 Gas Platform sensor" + default n + select I2C + ---help--- + Enable driver support for the Sensirion SCD30 CO₂ and TVOC gas sensor. + +if SENSORS_SGP30 + +config SGP30_I2C_FREQUENCY + int "SGP30 I2C frequency" + default 400000 + range 1 400000 + +config SGP30_DEBUG + bool "Debug support for the SGP30" + default n + ---help--- + Enables debug features for the SGP30 + +endif # SENSORS_SGP30 + config SENSORS_SHT21 bool "Sensirion SHT21 temperature and humidity sensor" default n @@ -537,6 +590,37 @@ config SHT21_DEBUG endif # SENSORS_SHT21 +config SENSORS_SPS30 + bool "Sensirion SPS30 particulate matter sensor" + default n + ---help--- + Enable driver support for the Sensirion SPS30 particulate matter sensor + sensor. + +if SENSORS_SPS30 + +config SPS30_I2C + bool "Sensirion SPS30 I2C mode" + default y + select I2C + +config SPS30_I2C_FREQUENCY + int "SPS30 I2C frequency" + default 100000 + range 1 100000 + depends on SPS30_I2C + ---help--- + I2C frequency for SPS30. Note, maximum supported frequency for + this sensor is 100kHz. + +config SPS30_DEBUG + bool "Debug support for the SPS30" + default n + ---help--- + Enables debug features for the SPS30 + +endif # SENSORS_SPS30 + config SENSORS_T67XX bool "Telair T6713 carbon dioxide sensor" default n diff --git a/drivers/sensors/Make.defs b/drivers/sensors/Make.defs index 060b1e6676..b1f0968ef9 100644 --- a/drivers/sensors/Make.defs +++ b/drivers/sensors/Make.defs @@ -157,10 +157,22 @@ ifeq ($(CONFIG_SENSORS_INA3221),y) CSRCS += ina3221.c endif +ifeq ($(CONFIG_SENSORS_SCD30),y) + CSRCS += scd30.c +endif + +ifeq ($(CONFIG_SENSORS_SGP30),y) + CSRCS += sgp30.c +endif + ifeq ($(CONFIG_SENSORS_SHT21),y) CSRCS += sht21.c endif +ifeq ($(CONFIG_SENSORS_SPS30),y) + CSRCS += sps30.c +endif + ifeq ($(CONFIG_SENSORS_T67XX),y) CSRCS += t67xx.c endif diff --git a/drivers/sensors/scd30.c b/drivers/sensors/scd30.c new file mode 100644 index 0000000000..e7148af3ad --- /dev/null +++ b/drivers/sensors/scd30.c @@ -0,0 +1,1112 @@ +/**************************************************************************** + * drivers/sensors/scd30.c + * Driver for the Sensirion SCD30 CO₂, temperature and humidity sensor + * + * Copyright (C) 2019 Haltian Ltd. All rights reserved. + * Author: Jussi Kivilinna + * + * 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 NuttX 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 OWNER 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. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#if defined(CONFIG_SENSORS_SCD30) + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#ifdef CONFIG_SCD30_DEBUG +# define scd30_dbg(x, ...) _info(x, ##__VA_ARGS__) +#else +# define scd30_dbg(x, ...) sninfo(x, ##__VA_ARGS__) +#endif + +#ifndef CONFIG_SCD30_I2C_FREQUENCY +# define CONFIG_SCD30_I2C_FREQUENCY 100000 +#endif + +#define SCD30_I2C_RETRIES 3 + +/* SCD30 command words */ + +#define SCD30_CMD_START_MEASUREMENT 0x0010 +#define SCD30_CMD_STOP_MEASUREMENT 0x0104 +#define SCD30_CMD_SET_INTERVAL 0x4600 +#define SCD30_CMD_GET_DATA_READY 0x0202 +#define SCD30_CMD_READ_MEASUREMENT 0x0300 +#define SCD30_CMD_SET_ASC 0x5306 +#define SCD30_CMD_SET_FRC 0x5204 +#define SCD30_CMD_SET_TEMP_OFFSET 0x5403 +#define SCD30_CMD_SET_ALT_COMPENSATION 0x5102 +#define SCD30_CMD_SOFT_RESET 0xD304 + +#define SCD30_DEFAULT_MEASUREMENT_INTERVAL 2 /* seconds */ +#define SCD30_DEFAULT_PRESSURE_COMPENSATION 0 +#define SCD30_DEFAULT_ALTITUDE_COMPENSATION 0 +#define SCD30_DEFAULT_TEMPERATURE_OFFSET 0 + +/**************************************************************************** + * Private + ****************************************************************************/ + +struct scd30_dev_s +{ +#ifdef CONFIG_SCD30_I2C + FAR struct i2c_master_s *i2c; /* I2C interface */ + uint8_t addr; /* I2C address */ +#endif + bool valid; /* If cached readings are valid */ + bool started; /* If continuous measurement is enabled */ +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS + bool unlinked; /* True, driver has been unlinked */ +#endif + struct timespec last_update; /* Last time when sensor was read */ + float co2; /* Cached CO₂ (PPM) */ + float temperature; /* Cached temperature (°C) */ + float humidity; /* Cached humidity (RH%) */ +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS + int16_t crefs; /* Number of open references */ +#endif + sem_t devsem; + uint16_t pressure_comp; /* Pressure compensation in mbar (non-zero + * value overrides altitude compensation). */ + uint16_t altitude_comp; /* Altitude compensation in meters */ + uint16_t interval; /* Background measurement interval in + * seconds (2 to 1800). */ + uint16_t temperature_offset; /* Temperature offset in 0.01 Kelvin. */ +}; + +struct scd30_word_s +{ + uint8_t data[2]; + uint8_t crc; +}; + +/**************************************************************************** + * Private Function Prototypes + ****************************************************************************/ +/* IO Helpers */ + +#ifdef CONFIG_SCD30_I2C +static int scd30_do_transfer(FAR struct i2c_master_s *i2c, + FAR struct i2c_msg_s *msgv, + size_t nmsg); +#endif +static int scd30_write_cmd(FAR struct scd30_dev_s *priv, uint16_t cmd, + FAR struct scd30_word_s *params, + unsigned int num_params); +static int scd30_read_words(FAR struct scd30_dev_s *priv, + FAR struct scd30_word_s *words, + unsigned int num_words); + +/* Data conversion */ + +static uint8_t scd30_crc_word(uint16_t word); +static void scd30_set_command_param(FAR struct scd30_word_s *param, + uint16_t value); +static int scd30_check_data_crc(FAR const struct scd30_word_s *words, + unsigned int num_words); +static uint16_t scd30_data_word_to_uint16(FAR const struct scd30_word_s *word); +static float scd30_data_words_to_float(FAR const struct scd30_word_s words[2]); + +/* Driver features */ + +static int scd30_read_values(FAR struct scd30_dev_s *priv, FAR float *temp, + FAR float *rh, FAR float *co2, bool wait); +static int scd30_configure(FAR struct scd30_dev_s *priv, bool start); + +/* Character driver methods */ + +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS +static int scd30_open(FAR struct file *filep); +static int scd30_close(FAR struct file *filep); +#endif +static ssize_t scd30_read(FAR struct file *filep, FAR char *buffer, + size_t buflen); +static ssize_t scd30_write(FAR struct file *filep, FAR const char *buffer, + size_t buflen); +static int scd30_ioctl(FAR struct file *filep, int cmd, + unsigned long arg); +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS +static int scd30_unlink(FAR struct inode *inode); +#endif + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +static const struct file_operations g_scd30fops = +{ +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS + scd30_open, /* open */ + scd30_close, /* close */ +#else + NULL, /* open */ + NULL, /* close */ +#endif + scd30_read, /* read */ + scd30_write, /* write */ + NULL, /* seek */ + scd30_ioctl /* ioctl */ +#ifndef CONFIG_DISABLE_POLL + , NULL /* poll */ +#endif +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS + , scd30_unlink /* unlink */ +#endif +}; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: scd30_do_transfer + ****************************************************************************/ + +#ifdef CONFIG_SCD30_I2C +static int scd30_do_transfer(FAR struct i2c_master_s *i2c, + FAR struct i2c_msg_s *msgv, + size_t nmsg) +{ + int ret = -EIO; + int retries; + + for (retries = 0; retries < SCD30_I2C_RETRIES; retries++) + { + ret = I2C_TRANSFER(i2c, msgv, nmsg); + if (ret >= 0) + { + return 0; + } + else + { + /* Some error. Try to reset I2C bus and keep trying. */ +#ifdef CONFIG_I2C_RESET + if (retries == SCD30_I2C_RETRIES - 1) + { + break; + } + + ret = I2C_RESET(i2c); + if (ret < 0) + { + scd30_dbg("I2C_RESET failed: %d\n", ret); + return ret; + } +#endif + } + } + + scd30_dbg("xfer failed: %d\n", ret); + return ret; +} +#endif + +/**************************************************************************** + * Name: scd30_write_cmd + ****************************************************************************/ + +static int scd30_write_cmd(FAR struct scd30_dev_s *priv, uint16_t cmd, + FAR struct scd30_word_s *params, + unsigned int num_params) +{ +#ifdef CONFIG_SCD30_I2C + struct i2c_msg_s msg[2]; + uint8_t cmd_buf[2]; + int ret; + + cmd_buf[0] = cmd >> 8; + cmd_buf[1] = cmd >> 0; + + msg[0].frequency = CONFIG_SCD30_I2C_FREQUENCY; + msg[0].addr = priv->addr; + msg[0].flags = 0; + msg[0].buffer = cmd_buf; + msg[0].length = 2; + + if (num_params) + { + msg[1].frequency = CONFIG_SCD30_I2C_FREQUENCY; + msg[1].addr = priv->addr; + msg[1].flags = I2C_M_NOSTART; + msg[1].buffer = (FAR uint8_t *)params; + msg[1].length = num_params * sizeof(*params); + } + + ret = scd30_do_transfer(priv->i2c, msg, (num_params) ? 2 : 1); + + scd30_dbg("cmd: 0x%04X num_params: %d ret: %d\n", + cmd, num_params, ret); + return (ret >= 0) ? OK : ret; +#else + /* UART mode not implemented yet. */ + return -ENODEV; +#endif +} + +/**************************************************************************** + * Name: scd30_read_words + ****************************************************************************/ + +static int scd30_read_words(FAR struct scd30_dev_s *priv, + FAR struct scd30_word_s *words, + unsigned int num_words) +{ +#ifdef CONFIG_SCD30_I2C + struct i2c_msg_s msg[1]; + int ret; + + msg[0].frequency = CONFIG_SCD30_I2C_FREQUENCY; + msg[0].addr = priv->addr; + msg[0].flags = I2C_M_READ; + msg[0].buffer = (FAR uint8_t *)words; + msg[0].length = num_words * sizeof(*words); + + ret = scd30_do_transfer(priv->i2c, msg, 1); + + scd30_dbg("num_words: %d ret: %d\n", num_words, ret); + return (ret >= 0) ? OK : ret; +#else + /* UART mode not implemented yet. */ + return -ENODEV; +#endif +} + +/**************************************************************************** + * Name: scd30_crc_word + ****************************************************************************/ + +static uint8_t scd30_crc_word(uint16_t word) +{ + static const uint8_t crc_table[16] = + { + 0x00, 0x31, 0x62, 0x53, 0xc4, 0xf5, 0xa6, 0x97, + 0xb9, 0x88, 0xdb, 0xea, 0x7d, 0x4c, 0x1f, 0x2e + }; + uint8_t crc = 0xFF; + + crc ^= word >> 8; + crc = (crc << 4) ^ crc_table[crc >> 4]; + crc = (crc << 4) ^ crc_table[crc >> 4]; + crc ^= word >> 0; + crc = (crc << 4) ^ crc_table[crc >> 4]; + crc = (crc << 4) ^ crc_table[crc >> 4]; + + return crc; +} + +/**************************************************************************** + * Name: scd30_set_command_param + ****************************************************************************/ + +static void scd30_set_command_param(FAR struct scd30_word_s *param, + uint16_t value) +{ + param->data[0] = value >> 8; + param->data[1] = value >> 0; + param->crc = scd30_crc_word(value); +} + +/**************************************************************************** + * Name: scd30_data_words_to_float + ****************************************************************************/ + +static float scd30_data_words_to_float(FAR const struct scd30_word_s words[2]) +{ + uint8_t data[4]; + float value; + + data[3] = words[0].data[0]; + data[2] = words[0].data[1]; + data[1] = words[1].data[0]; + data[0] = words[1].data[1]; + memcpy(&value, data, 4); + return value; +} + +/**************************************************************************** + * Name: scd30_data_word_to_uint16 + ****************************************************************************/ + +static uint16_t scd30_data_word_to_uint16(FAR const struct scd30_word_s *word) +{ + return (word[0].data[0] << 8) | (word[0].data[1]); +} + +/**************************************************************************** + * Name: scd30_crc_word + ****************************************************************************/ + +static int scd30_check_data_crc(FAR const struct scd30_word_s *words, + unsigned int num_words) +{ + while (num_words) + { + if (scd30_crc_word(scd30_data_word_to_uint16(words)) != words->crc) + return -1; + + num_words--; + words++; + } + + return 0; +} + +/**************************************************************************** + * Name: scd30_softreset + * + * Description: + * Reset the SCD30 sensor. This takes less than 2000 ms. + * + ****************************************************************************/ + +static int scd30_softreset(FAR struct scd30_dev_s *priv) +{ + int ret; + + ret = scd30_write_cmd(priv, SCD30_CMD_SOFT_RESET, NULL, 0); + if (ret < 0) + return ret; + + return 0; +} + +/**************************************************************************** + * Name: has_time_passed + * + * Description: + * Return true if curr >= start + secs_since_start + * + ****************************************************************************/ + +static bool has_time_passed(struct timespec curr, + struct timespec start, + unsigned int secs_since_start) +{ + if ((long)((start.tv_sec + secs_since_start) - curr.tv_sec) == 0) + { + return start.tv_nsec <= curr.tv_nsec; + } + + return (long)((start.tv_sec + secs_since_start) - curr.tv_sec) <= 0; +} + +/**************************************************************************** + * Name: scd30_read_values + ****************************************************************************/ + +static int scd30_read_values(FAR struct scd30_dev_s *priv, FAR float *temp, + FAR float *rh, FAR float *co2, bool wait) +{ + struct scd30_word_s data[6]; + struct timespec ts; + int ret; + + clock_gettime(CLOCK_REALTIME, &ts); + + if (wait || !priv->valid || + has_time_passed(ts, priv->last_update, + SCD30_DEFAULT_MEASUREMENT_INTERVAL)) + { + while (1) + { + /* Wait data to be ready. */ + + ret = scd30_write_cmd(priv, SCD30_CMD_GET_DATA_READY, NULL, 0); + if (ret < 0) + { + scd30_dbg("ERROR: scd30_write_cmd failed: %d\n", ret); + return ret; + } + + ret = scd30_read_words(priv, data, 1); + if (ret < 0) + { + scd30_dbg("ERROR: scd30_read_words failed: %d\n", ret); + return ret; + } + + if (scd30_check_data_crc(data, 1) < 0) + { + scd30_dbg("ERROR: scd30_read_words crc failed\n"); + ret = -EIO; + return ret; + } + + if (scd30_data_word_to_uint16(data) != 0x0001) + { + if (!wait) + { + scd30_dbg("ERROR: data not ready\n"); + ret = -EAGAIN; + return ret; + } + + ret = nxsig_usleep(500 * 1000); + if (ret == -EINTR) + { + return ret; + } + } + else + { + break; + } + } + + /* Read the raw data */ + + ret = scd30_write_cmd(priv, SCD30_CMD_READ_MEASUREMENT, NULL, 0); + if (ret < 0) + { + scd30_dbg("ERROR: scd30_write_cmd failed: %d\n", ret); + return ret; + } + + ret = scd30_read_words(priv, data, 6); + if (ret < 0) + { + scd30_dbg("ERROR: scd30_read_words failed: %d\n", ret); + return ret; + } + + if (scd30_check_data_crc(data, 6) < 0) + { + scd30_dbg("ERROR: scd30_read_words crc failed\n"); + ret = -EIO; + return ret; + } + + add_sensor_randomness(((data[0].crc ^ data[1].crc) << 0) ^ + ((data[2].crc ^ data[3].crc) << 8) ^ + ((data[4].crc ^ data[5].crc) << 16)); + + priv->co2 = scd30_data_words_to_float(data + 0); + priv->temperature = scd30_data_words_to_float(data + 2); + priv->humidity = scd30_data_words_to_float(data + 4); + priv->last_update = ts; + priv->valid = true; + } + + *temp = priv->temperature; + *rh = priv->humidity; + *co2 = priv->co2; + return OK; +} + +/**************************************************************************** + * Name: scd30_configure + ****************************************************************************/ + +static int scd30_configure(FAR struct scd30_dev_s *priv, bool start) +{ + struct scd30_word_s param; + int ret; + + /* Set measurement interval. */ + + scd30_set_command_param(¶m, priv->interval); + (void)scd30_write_cmd(priv, SCD30_CMD_SET_INTERVAL, ¶m, 1); + + /* Set altitude compensation. */ + + scd30_set_command_param(¶m, priv->altitude_comp); + (void)scd30_write_cmd(priv, SCD30_CMD_SET_ALT_COMPENSATION, ¶m, 1); + + /* Set temperature offset. */ + + scd30_set_command_param(¶m, priv->temperature_offset); + (void)scd30_write_cmd(priv, SCD30_CMD_SET_TEMP_OFFSET, ¶m, 1); + + if (!start) + { + /* Stop measurements. */ + + ret = scd30_write_cmd(priv, SCD30_CMD_STOP_MEASUREMENT, ¶m, 1); + if (ret >= 0) + { + priv->started = false; + } + } + else + { + /* Start measurements (and set pressure compensation). */ + + scd30_set_command_param(¶m, priv->pressure_comp); + ret = scd30_write_cmd(priv, SCD30_CMD_START_MEASUREMENT, ¶m, 1); + if (ret >= 0) + { + priv->started = true; + } + } + + return ret; +} + +/**************************************************************************** + * Name: scd30_open + * + * Description: + * This function is called whenever the SCD30x device is opened. + * + ****************************************************************************/ + +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS +static int scd30_open(FAR struct file *filep) +{ + FAR struct inode *inode = filep->f_inode; + FAR struct scd30_dev_s *priv = inode->i_private; + int ret; + + /* Get exclusive access */ + + do + { + ret = nxsem_wait(&priv->devsem); + + /* The only case that an error should occur here is if the wait was + * awakened by a signal. + */ + + DEBUGASSERT(ret == OK || ret == -EINTR); + } + while (ret == -EINTR); + + /* Increment the count of open references on the driver */ + + priv->crefs++; + DEBUGASSERT(priv->crefs > 0); + + if (priv->crefs == 1) + { + ret = scd30_configure(priv, true); + } + + if (ret < 0) + { + priv->crefs--; + } + + nxsem_post(&priv->devsem); + return ret; +} +#endif + +/**************************************************************************** + * Name: scd30_close + * + * Description: + * This routine is called when the SCD30 device is closed. + * + ****************************************************************************/ + +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS +static int scd30_close(FAR struct file *filep) +{ + FAR struct inode *inode = filep->f_inode; + FAR struct scd30_dev_s *priv = inode->i_private; + int ret; + + /* Get exclusive access */ + + do + { + ret = nxsem_wait(&priv->devsem); + + /* The only case that an error should occur here is if the wait was + * awakened by a signal. + */ + + DEBUGASSERT(ret == OK || ret == -EINTR); + } + while (ret == -EINTR); + + /* Decrement the count of open references on the driver */ + + DEBUGASSERT(priv->crefs > 0); + priv->crefs--; + + /* If the count has decremented to zero and the driver has been unlinked, + * then free memory now. + */ + + if (priv->crefs <= 0 && priv->unlinked) + { + nxsem_destroy(&priv->devsem); + kmm_free(priv); + return OK; + } + + nxsem_post(&priv->devsem); + return OK; +} +#endif + +/**************************************************************************** + * Name: scd30_read + ****************************************************************************/ + +static ssize_t scd30_read(FAR struct file *filep, FAR char *buffer, + size_t buflen) +{ + FAR struct inode *inode = filep->f_inode; + FAR struct scd30_dev_s *priv = inode->i_private; + ssize_t length = 0; + float temp; + float rh; + float co2; + unsigned int temp100; + unsigned int rh100; + unsigned int co2_100; + int ret; + + /* Get exclusive access */ + + do + { + ret = nxsem_wait(&priv->devsem); + + /* The only case that an error should occur here is if the wait was + * awakened by a signal. + */ + + DEBUGASSERT(ret == OK || ret == -EINTR); + } + while (ret == -EINTR); + +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS + if (priv->unlinked) + { + /* Do not allow operations on unlinked sensors. This allows + * sensor use on hot swappable I2C bus. + */ + + nxsem_post(&priv->devsem); + return -ENODEV; + } +#endif + + if (!priv->started) + { + return -ENODATA; + } + + ret = scd30_read_values(priv, &temp, &rh, &co2, + !(filep->f_oflags & O_NONBLOCK)); + if (ret < 0) + { + scd30_dbg("cannot read data: %d\n", ret); + } + else + { + /* This interface is mainly intended for easy debugging in nsh. */ + + temp100 = abs(temp * 100); + rh100 = abs(rh * 100); + co2_100 = abs(co2 * 100); + + length = snprintf(buffer, buflen, "%s%u.%02u %s%u.%02u %s%u.%02u\n", + co2 < 0 ? "-" : "", co2_100 / 100, co2_100 % 100, + temp < 0 ? "-" : "", temp100 / 100, temp100 % 100, + rh < 0 ? "-" : "", rh100 / 100, rh100 % 100); + if (length > buflen) + { + length = buflen; + } + } + + nxsem_post(&priv->devsem); + return length; +} + +/**************************************************************************** + * Name: scd30_write + ****************************************************************************/ + +static ssize_t scd30_write(FAR struct file *filep, FAR const char *buffer, + size_t buflen) +{ + return -ENOSYS; +} + +/**************************************************************************** + * Name: scd30_ioctl + ****************************************************************************/ + +static int scd30_ioctl(FAR struct file *filep, int cmd, unsigned long arg) +{ + FAR struct inode *inode = filep->f_inode; + FAR struct scd30_dev_s *priv = inode->i_private; + struct scd30_word_s param; + int ret; + + /* Get exclusive access */ + + do + { + ret = nxsem_wait(&priv->devsem); + + /* The only case that an error should occur here is if the wait was + * awakened by a signal. + */ + + DEBUGASSERT(ret == OK || ret == -EINTR); + } + while (ret == -EINTR); + +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS + if (priv->unlinked) + { + /* Do not allow operations on unlinked sensors. This allows + * sensor use on hot swappable I2C bus. + */ + + nxsem_post(&priv->devsem); + return -ENODEV; + } +#endif + + switch (cmd) + { + /* Soft reset the SCD30, Arg: None */ + + case SNIOC_RESET: + { + priv->interval = SCD30_DEFAULT_MEASUREMENT_INTERVAL; + priv->pressure_comp = SCD30_DEFAULT_PRESSURE_COMPENSATION; + priv->altitude_comp = SCD30_DEFAULT_ALTITUDE_COMPENSATION; + priv->temperature_offset = SCD30_DEFAULT_TEMPERATURE_OFFSET; + + ret = scd30_softreset(priv); + scd30_dbg("softreset ret: %d\n", ret); + + scd30_configure(priv, priv->started); + } + break; + + /* Start background measurement, Arg: None */ + + case SNIOC_START: + { + /* Start measurements (and set pressure compensation). */ + + scd30_set_command_param(¶m, priv->pressure_comp); + ret = scd30_write_cmd(priv, SCD30_CMD_START_MEASUREMENT, ¶m, 1); + if (ret >= 0) + { + priv->started = true; + } + } + break; + + /* Stop background measurement, Arg: None */ + + case SNIOC_STOP: + { + /* Stop measurements. */ + + ret = scd30_write_cmd(priv, SCD30_CMD_STOP_MEASUREMENT, NULL, 0); + if (ret >= 0) + { + priv->started = false; + } + } + break; + + /* Set background measurement interval, Arg: uint16_t */ + + case SNIOC_SET_INTERVAL: + { + if (arg < 2 && arg > 1800) + { + ret = -EINVAL; + break; + } + + priv->interval = arg; + scd30_set_command_param(¶m, priv->interval); + ret = scd30_write_cmd(priv, SCD30_CMD_SET_INTERVAL, ¶m, 1); + } + break; + + /* Set temperature offset value, Arg: uint16_t */ + + case SNIOC_SET_TEMP_OFFSET: + { + if (arg < 0 && arg > UINT16_MAX) + { + ret = -EINVAL; + break; + } + + priv->temperature_offset = arg; + scd30_set_command_param(¶m, priv->temperature_offset); + ret = scd30_write_cmd(priv, SCD30_CMD_SET_TEMP_OFFSET, ¶m, 1); + } + break; + + /* Set pressure compensation value, Arg: uint16_t */ + + case SNIOC_SET_PRESSURE_COMP: + { + if (arg != 0 && arg < 700 && arg > 1200) + { + ret = -EINVAL; + break; + } + + priv->pressure_comp = arg; + if (priv->started) + { + scd30_set_command_param(¶m, priv->pressure_comp); + ret = scd30_write_cmd(priv, SCD30_CMD_START_MEASUREMENT, + ¶m, 1); + } + else + { + ret = 0; + } + } + break; + + /* Set altitude compensation value, Arg: uint16_t */ + + case SNIOC_SET_ALTITUDE_COMP: + { + if (arg < 0 && arg > UINT16_MAX) + { + ret = -EINVAL; + break; + } + + priv->altitude_comp = arg; + scd30_set_command_param(¶m, priv->altitude_comp); + ret = scd30_write_cmd(priv, SCD30_CMD_SET_ALT_COMPENSATION, + ¶m, 1); + } + break; + + /* Set Forced Recalibration value (FRC), Arg: uint16_t */ + + case SNIOC_SET_FRC: + { + if (arg < 0 && arg > UINT16_MAX) + { + ret = -EINVAL; + break; + } + + scd30_set_command_param(¶m, arg); + ret = scd30_write_cmd(priv, SCD30_CMD_SET_FRC, ¶m, 1); + } + break; + + /* (De-)Activate Automatic Self-Calibration (ASC), Arg: bool */ + + case SNIOC_ENABLE_ASC: + { + if (arg != !!arg) /* 0 or 1 */ + { + ret = -EINVAL; + break; + } + + scd30_set_command_param(¶m, arg); + ret = scd30_write_cmd(priv, SCD30_CMD_SET_ASC, ¶m, 1); + } + break; + + /* Read sensor data, Arg: struct scd30_conv_data_s *data */ + + case SNIOC_READ_CONVERT_DATA: + { + float temp; + float rh; + float co2; + + ret = scd30_read_values(priv, &temp, &rh, &co2, false); + if (ret < 0) + { + scd30_dbg("cannot read data: %d\n", ret); + } + else + { + FAR struct scd30_conv_data_s *data = + (FAR struct scd30_conv_data_s *)arg; + + data->temperature = temp; + data->humidity = rh; + data->co2 = co2; + } + } + break; + + default: + scd30_dbg("Unrecognized cmd: %d\n", cmd); + ret = -ENOTTY; + break; + } + + nxsem_post(&priv->devsem); + return ret; +} + +/**************************************************************************** + * Name: scd30_unlink + ****************************************************************************/ + +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS +static int scd30_unlink(FAR struct inode *inode) +{ + FAR struct scd30_dev_s *priv; + int ret; + + DEBUGASSERT(inode != NULL && inode->i_private != NULL); + priv = (FAR struct scd30_dev_s *)inode->i_private; + + /* Get exclusive access */ + + do + { + ret = nxsem_wait(&priv->devsem); + + /* The only case that an error should occur here is if the wait was + * awakened by a signal. + */ + + DEBUGASSERT(ret == OK || ret == -EINTR); + } + while (ret == -EINTR); + + /* Are there open references to the driver data structure? */ + + if (priv->crefs <= 0) + { + nxsem_destroy(&priv->devsem); + kmm_free(priv); + return OK; + } + + /* No... just mark the driver as unlinked and free the resources when + * the last client closes their reference to the driver. + */ + + priv->unlinked = true; + nxsem_post(&priv->devsem); + return ret; +} +#endif + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +#ifdef CONFIG_SCD30_I2C +/**************************************************************************** + * Name: scd30_register_i2c + * + * Description: + * Register the SCD30 character device as 'devpath' + * + * Input Parameters: + * devpath - The full path to the driver to register. E.g., "/dev/co2_0" + * i2c - An instance of the I2C interface to use to communicate with + * the SCD30 + * addr - The I2C address of the SCD30. The I2C address of SCD30 is + * always 0x61. + * + * Returned Value: + * Zero (OK) on success; a negated errno value on failure. + * + ****************************************************************************/ + +int scd30_register_i2c(FAR const char *devpath, FAR struct i2c_master_s *i2c, + uint8_t addr) +{ + FAR struct scd30_dev_s *priv; + int ret; + + DEBUGASSERT(i2c != NULL); + DEBUGASSERT(addr == CONFIG_SCD30_ADDR); + DEBUGASSERT(scd30_crc_word(0xBEEF) == 0x92); + + /* Initialize the device structure */ + + priv = (FAR struct scd30_dev_s *)kmm_zalloc(sizeof(struct scd30_dev_s)); + if (priv == NULL) + { + scd30_dbg("ERROR: Failed to allocate instance\n"); + return -ENOMEM; + } + + priv->i2c = i2c; + priv->addr = addr; + priv->started = false; + priv->interval = SCD30_DEFAULT_MEASUREMENT_INTERVAL; + priv->pressure_comp = SCD30_DEFAULT_PRESSURE_COMPENSATION; + priv->altitude_comp = SCD30_DEFAULT_ALTITUDE_COMPENSATION; + priv->temperature_offset = SCD30_DEFAULT_TEMPERATURE_OFFSET; + + nxsem_init(&priv->devsem, 0, 1); + + /* Register the character driver */ + + ret = register_driver(devpath, &g_scd30fops, 0666, priv); + if (ret < 0) + { + scd30_dbg("ERROR: Failed to register driver: %d\n", ret); + kmm_free(priv); + } + + return ret; +} +#endif /* CONFIG_SCD30_I2C */ + +#endif /* CONFIG_SENSORS_SCD30 */ diff --git a/drivers/sensors/sgp30.c b/drivers/sensors/sgp30.c new file mode 100644 index 0000000000..85108b95ca --- /dev/null +++ b/drivers/sensors/sgp30.c @@ -0,0 +1,1015 @@ +/**************************************************************************** + * drivers/sensors/sgp30.c + * Driver for the Sensirion SGP30 Gas Platform sensor + * + * Copyright (C) 2019 Haltian Ltd. All rights reserved. + * Author: Jussi Kivilinna + * + * 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 NuttX 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 OWNER 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. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#if defined(CONFIG_I2C) && defined(CONFIG_SENSORS_SGP30) + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#ifdef CONFIG_SGP30_DEBUG +# define sgp30_dbg(x, ...) _info(x, ##__VA_ARGS__) +#else +# define sgp30_dbg(x, ...) sninfo(x, ##__VA_ARGS__) +#endif + +#ifndef CONFIG_SGP30_I2C_FREQUENCY +# define CONFIG_SGP30_I2C_FREQUENCY 100000 +#endif + +#define SGP30_I2C_RETRIES 3 + +/**************************************************************************** + * Private + ****************************************************************************/ + +struct sgp30_dev_s +{ + FAR struct i2c_master_s *i2c; /* I2C interface */ + uint8_t addr; /* I2C address */ +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS + bool unlinked; /* True, driver has been unlinked */ +#endif +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS + int16_t crefs; /* Number of open references */ +#endif + struct timespec last_update; + sem_t devsem; +}; + +struct sgp30_word_s +{ + uint8_t data[2]; + uint8_t crc; +}; + +struct sgp30_cmd_s +{ + uint16_t address; + unsigned int read_delay_usec; +}; + +/**************************************************************************** + * Private Function Prototypes + ****************************************************************************/ +/* IO Helpers */ + +static int sgp30_do_transfer(FAR struct i2c_master_s *i2c, + FAR struct i2c_msg_s *msgv, + size_t nmsg); +static int sgp30_write_cmd(FAR struct sgp30_dev_s *priv, struct sgp30_cmd_s cmd, + FAR struct sgp30_word_s *params, + unsigned int num_params); +static int sgp30_read_cmd(FAR struct sgp30_dev_s *priv, struct sgp30_cmd_s cmd, + FAR struct sgp30_word_s *words, + unsigned int num_words); + +/* Data conversion */ + +static uint8_t sgp30_crc_word(uint16_t word); +static void sgp30_set_command_param(FAR struct sgp30_word_s *param, + uint16_t value); +static int sgp30_check_data_crc(FAR const struct sgp30_word_s *words, + unsigned int num_words); +static uint16_t sgp30_data_word_to_uint16(FAR const struct sgp30_word_s *word); + +/* Driver features */ + +static int sgp30_measure_airq(FAR struct sgp30_dev_s *priv, + FAR struct sgp30_conv_data_s *data); +static int sgp30_measure_raw(FAR struct sgp30_dev_s *priv, + FAR struct sgp30_raw_data_s *data); + +/* Character driver methods */ + +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS +static int sgp30_open(FAR struct file *filep); +static int sgp30_close(FAR struct file *filep); +#endif +static ssize_t sgp30_read(FAR struct file *filep, FAR char *buffer, + size_t buflen); +static ssize_t sgp30_write(FAR struct file *filep, FAR const char *buffer, + size_t buflen); +static int sgp30_ioctl(FAR struct file *filep, int cmd, + unsigned long arg); +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS +static int sgp30_unlink(FAR struct inode *inode); +#endif + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +static const struct file_operations g_sgp30fops = +{ +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS + sgp30_open, /* open */ + sgp30_close, /* close */ +#else + NULL, /* open */ + NULL, /* close */ +#endif + sgp30_read, /* read */ + sgp30_write, /* write */ + NULL, /* seek */ + sgp30_ioctl /* ioctl */ +#ifndef CONFIG_DISABLE_POLL + , NULL /* poll */ +#endif +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS + , sgp30_unlink /* unlink */ +#endif +}; + +/* SGP30 command words */ + +static const struct sgp30_cmd_s SGP30_CMD_INIT_AIR_QUALITY = +{ + 0x2003, 0 +}; + +static const struct sgp30_cmd_s SGP30_CMD_MEASURE_AIR_QUALITY = +{ + 0x2008, 12000 +}; + +static const struct sgp30_cmd_s SGP30_CMD_GET_BASELINE = +{ + 0x2015, 10000 +}; + +static const struct sgp30_cmd_s SGP30_CMD_SET_BASELINE = +{ + 0x201e, 10000 +}; + +static const struct sgp30_cmd_s SGP30_CMD_SET_HUMIDITY = +{ + 0x2061, 10000 +}; + +static const struct sgp30_cmd_s SGP30_CMD_MEASURE_TEST = +{ + 0x2032, 220000 +}; + +static const struct sgp30_cmd_s SGP30_CMD_GET_FEATURE_SET_VERSION = +{ + 0x202f, 2000 +}; + +static const struct sgp30_cmd_s SGP30_CMD_MEASURE_RAW_SIGNALS = +{ + 0x2050, 25000 +}; + +static const struct sgp30_cmd_s SGP30_CMD_GET_SERIAL_ID = +{ + 0x3682, 500 +}; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: sgp30_do_transfer + ****************************************************************************/ + +static int sgp30_do_transfer(FAR struct i2c_master_s *i2c, + FAR struct i2c_msg_s *msgv, + size_t nmsg) +{ + int ret = -EIO; + int retries; + + for (retries = 0; retries < SGP30_I2C_RETRIES; retries++) + { + ret = I2C_TRANSFER(i2c, msgv, nmsg); + if (ret >= 0) + { + return 0; + } + else + { + /* Some error. Try to reset I2C bus and keep trying. */ +#ifdef CONFIG_I2C_RESET + if (retries == SGP30_I2C_RETRIES - 1) + { + break; + } + + ret = I2C_RESET(i2c); + if (ret < 0) + { + sgp30_dbg("I2C_RESET failed: %d\n", ret); + return ret; + } +#endif + } + } + + sgp30_dbg("xfer failed: %d\n", ret); + return ret; +} + +/**************************************************************************** + * Name: sgp30_write_cmd + ****************************************************************************/ + +static int sgp30_write_cmd(FAR struct sgp30_dev_s *priv, struct sgp30_cmd_s cmd, + FAR struct sgp30_word_s *params, + unsigned int num_params) +{ + struct i2c_msg_s msg[2]; + uint8_t addr_buf[2]; + int ret; + + addr_buf[0] = cmd.address >> 8; + addr_buf[1] = cmd.address >> 0; + + msg[0].frequency = CONFIG_SGP30_I2C_FREQUENCY; + msg[0].addr = priv->addr; + msg[0].flags = 0; + msg[0].buffer = addr_buf; + msg[0].length = 2; + + if (num_params) + { + msg[1].frequency = CONFIG_SGP30_I2C_FREQUENCY; + msg[1].addr = priv->addr; + msg[1].flags = I2C_M_NOSTART; + msg[1].buffer = (FAR uint8_t *)params; + msg[1].length = num_params * sizeof(*params); + } + + ret = sgp30_do_transfer(priv->i2c, msg, (num_params) ? 2 : 1); + + sgp30_dbg("cmd: 0x%04X num_params: %d ret: %d\n", + cmd.address, num_params, ret); + return (ret >= 0) ? OK : ret; +} + +/**************************************************************************** + * Name: sgp30_read_cmd + ****************************************************************************/ + +static int sgp30_read_cmd(FAR struct sgp30_dev_s *priv, struct sgp30_cmd_s cmd, + FAR struct sgp30_word_s *words, + unsigned int num_words) +{ + struct i2c_msg_s msg[1]; + uint8_t addr_buf[2]; + int ret; + + addr_buf[0] = cmd.address >> 8; + addr_buf[1] = cmd.address >> 0; + + msg[0].frequency = CONFIG_SGP30_I2C_FREQUENCY; + msg[0].addr = priv->addr; + msg[0].flags = 0; + msg[0].buffer = addr_buf; + msg[0].length = 2; + + ret = sgp30_do_transfer(priv->i2c, msg, 1); + sgp30_dbg("cmd: 0x%04X delay: %uus ret: %d\n", cmd.address, + cmd.read_delay_usec, ret); + if (ret < 0) + return ret; + + up_udelay(cmd.read_delay_usec + 100); + + msg[0].frequency = CONFIG_SGP30_I2C_FREQUENCY; + msg[0].addr = priv->addr; + msg[0].flags = I2C_M_READ; + msg[0].buffer = (FAR uint8_t *)words; + msg[0].length = num_words * sizeof(*words); + + ret = sgp30_do_transfer(priv->i2c, msg, 1); + + sgp30_dbg("read cmd: 0x%04X num_params: %d ret: %d\n", + cmd.address, num_words, ret); + return (ret >= 0) ? OK : ret; +} + +/**************************************************************************** + * Name: sgp30_crc_word + ****************************************************************************/ + +static uint8_t sgp30_crc_word(uint16_t word) +{ + static const uint8_t crc_table[16] = + { + 0x00, 0x31, 0x62, 0x53, 0xc4, 0xf5, 0xa6, 0x97, + 0xb9, 0x88, 0xdb, 0xea, 0x7d, 0x4c, 0x1f, 0x2e + }; + uint8_t crc = 0xFF; + + crc ^= word >> 8; + crc = (crc << 4) ^ crc_table[crc >> 4]; + crc = (crc << 4) ^ crc_table[crc >> 4]; + crc ^= word >> 0; + crc = (crc << 4) ^ crc_table[crc >> 4]; + crc = (crc << 4) ^ crc_table[crc >> 4]; + + return crc; +} + +/**************************************************************************** + * Name: sgp30_set_command_param + ****************************************************************************/ + +static void sgp30_set_command_param(FAR struct sgp30_word_s *param, + uint16_t value) +{ + param->data[0] = value >> 8; + param->data[1] = value >> 0; + param->crc = sgp30_crc_word(value); +} + +/**************************************************************************** + * Name: sgp30_data_word_to_uint16 + ****************************************************************************/ + +static uint16_t sgp30_data_word_to_uint16(FAR const struct sgp30_word_s *word) +{ + return (word[0].data[0] << 8) | (word[0].data[1]); +} + +/**************************************************************************** + * Name: sgp30_crc_word + ****************************************************************************/ + +static int sgp30_check_data_crc(FAR const struct sgp30_word_s *words, + unsigned int num_words) +{ + while (num_words) + { + if (sgp30_crc_word(sgp30_data_word_to_uint16(words)) != words->crc) + return -1; + + num_words--; + words++; + } + + return 0; +} + +/**************************************************************************** + * Name: has_time_passed + * + * Description: + * Return true if curr >= start + secs_since_start + * + ****************************************************************************/ + +static bool has_time_passed(struct timespec curr, + struct timespec start, + unsigned int secs_since_start) +{ + if ((long)((start.tv_sec + secs_since_start) - curr.tv_sec) == 0) + { + return start.tv_nsec <= curr.tv_nsec; + } + + return (long)((start.tv_sec + secs_since_start) - curr.tv_sec) <= 0; +} + +/**************************************************************************** + * Name: sgp30_measure_airq + ****************************************************************************/ + +static int sgp30_measure_airq(FAR struct sgp30_dev_s *priv, + FAR struct sgp30_conv_data_s *data) +{ + struct sgp30_word_s words[2]; + int ret; + + /* Read the data */ + + ret = sgp30_read_cmd(priv, SGP30_CMD_MEASURE_AIR_QUALITY, words, 2); + if (ret < 0) + { + sgp30_dbg("ERROR: sgp30_write_cmd failed: %d\n", ret); + return ret; + } + + if (sgp30_check_data_crc(words, 2) < 0) + { + sgp30_dbg("ERROR: sgp30_read_words crc failed\n"); + ret = -EIO; + return ret; + } + + add_sensor_randomness(words[0].crc ^ (words[1].crc << 8)); + data->co2eq_ppm = sgp30_data_word_to_uint16(words + 0); + data->tvoc_ppb = sgp30_data_word_to_uint16(words + 1); + + return OK; +} + +/**************************************************************************** + * Name: sgp30_measure_raw + ****************************************************************************/ + +static int sgp30_measure_raw(FAR struct sgp30_dev_s *priv, + FAR struct sgp30_raw_data_s *data) +{ + struct sgp30_word_s words[2]; + int ret; + + /* Read the data */ + + ret = sgp30_read_cmd(priv, SGP30_CMD_MEASURE_RAW_SIGNALS, words, 2); + if (ret < 0) + { + sgp30_dbg("ERROR: sgp30_write_cmd failed: %d\n", ret); + return ret; + } + + if (sgp30_check_data_crc(words, 2) < 0) + { + sgp30_dbg("ERROR: sgp30_read_words crc failed\n"); + ret = -EIO; + return ret; + } + + add_sensor_randomness(words[0].crc ^ (words[1].crc << 8)); + data->h2_signal = sgp30_data_word_to_uint16(words + 0); + data->ethanol_signal = sgp30_data_word_to_uint16(words + 1); + + return OK; +} + +/**************************************************************************** + * Name: sgp30_open + * + * Description: + * This function is called whenever the SGP30x device is opened. + * + ****************************************************************************/ + +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS +static int sgp30_open(FAR struct file *filep) +{ + FAR struct inode *inode = filep->f_inode; + FAR struct sgp30_dev_s *priv = inode->i_private; + int ret; + + /* Get exclusive access */ + + do + { + ret = nxsem_wait(&priv->devsem); + + /* The only case that an error should occur here is if the wait was + * awakened by a signal. + */ + + DEBUGASSERT(ret == OK || ret == -EINTR); + } + while (ret == -EINTR); + + /* Increment the count of open references on the driver */ + + priv->crefs++; + DEBUGASSERT(priv->crefs > 0); + + if (priv->crefs == 1) + { + struct sgp30_word_s serial[3]; + struct sgp30_word_s buf[1]; + + if (sgp30_read_cmd(priv, SGP30_CMD_GET_SERIAL_ID, serial, 3) >= 0 + && sgp30_check_data_crc(serial, 3) >= 0 && + sgp30_read_cmd(priv, SGP30_CMD_GET_FEATURE_SET_VERSION, buf, 1) >= 0 + && sgp30_check_data_crc(buf, 1) >= 0) + { + sgp30_dbg("serial id: %04x-%04x-%04x\n", + sgp30_data_word_to_uint16(serial + 0), + sgp30_data_word_to_uint16(serial + 1), + sgp30_data_word_to_uint16(serial + 2)); + sgp30_dbg("feature set version: %d\n", + sgp30_data_word_to_uint16(buf)); + + add_sensor_randomness((buf[0].crc << 24) ^ (serial[0].crc << 16) ^ + (serial[1].crc << 8) ^ (serial[2].crc << 0)); + + ret = sgp30_write_cmd(priv, SGP30_CMD_INIT_AIR_QUALITY, NULL, 0); + if (ret < 0) + { + sgp30_dbg("sgp30_write_cmd(SGP30_CMD_INIT_AIR_QUALITY) failed, %d\n", ret); + } + } + else + { + ret = -ENODEV; + } + + if (ret < 0) + { + ret = -ENODEV; + priv->crefs--; + } + } + + nxsem_post(&priv->devsem); + return ret; +} +#endif + +/**************************************************************************** + * Name: sgp30_close + * + * Description: + * This routine is called when the SGP30 device is closed. + * + ****************************************************************************/ + +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS +static int sgp30_close(FAR struct file *filep) +{ + FAR struct inode *inode = filep->f_inode; + FAR struct sgp30_dev_s *priv = inode->i_private; + int ret; + + /* Get exclusive access */ + + do + { + ret = nxsem_wait(&priv->devsem); + + /* The only case that an error should occur here is if the wait was + * awakened by a signal. + */ + + DEBUGASSERT(ret == OK || ret == -EINTR); + } + while (ret == -EINTR); + + /* Decrement the count of open references on the driver */ + + DEBUGASSERT(priv->crefs > 0); + priv->crefs--; + + /* If the count has decremented to zero and the driver has been unlinked, + * then free memory now. + */ + + if (priv->crefs <= 0 && priv->unlinked) + { + nxsem_destroy(&priv->devsem); + kmm_free(priv); + return OK; + } + + nxsem_post(&priv->devsem); + return OK; +} +#endif + +/**************************************************************************** + * Name: sgp30_read + ****************************************************************************/ + +static ssize_t sgp30_read(FAR struct file *filep, FAR char *buffer, + size_t buflen) +{ + FAR struct inode *inode = filep->f_inode; + FAR struct sgp30_dev_s *priv = inode->i_private; + ssize_t length = 0; + struct timespec ts, ts_sleep; + struct sgp30_conv_data_s data; + int ret; + + /* Get exclusive access */ + + do + { + ret = nxsem_wait(&priv->devsem); + + /* The only case that an error should occur here is if the wait was + * awakened by a signal. + */ + + DEBUGASSERT(ret == OK || ret == -EINTR); + } + while (ret == -EINTR); + +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS + if (priv->unlinked) + { + /* Do not allow operations on unlinked sensors. This allows + * sensor use on hot swappable I2C bus. + */ + + nxsem_post(&priv->devsem); + return -ENODEV; + } +#endif + + /* Note: For correct operation, SGP30 documentation tells host + * to run measurement command every 1 second. + */ + + clock_gettime(CLOCK_REALTIME, &ts); + + while (!has_time_passed(ts, priv->last_update, 1)) + { + if (filep->f_oflags & O_NONBLOCK) + { + nxsem_post(&priv->devsem); + return -EAGAIN; + } + + ts_sleep.tv_sec = priv->last_update.tv_sec + 1 - ts.tv_sec; + ts_sleep.tv_nsec = priv->last_update.tv_nsec - ts.tv_nsec; + while (ts_sleep.tv_nsec < 0) + { + ts_sleep.tv_sec--; + ts_sleep.tv_nsec += NSEC_PER_SEC; + } + + if ((long)ts_sleep.tv_sec >= 0) + { + ret = nxsig_nanosleep(&ts_sleep, NULL); + if (ret == -EINTR) + { + nxsem_post(&priv->devsem); + return -EINTR; + } + } + + clock_gettime(CLOCK_REALTIME, &ts); + } + + ret = sgp30_measure_airq(priv, &data); + if (ret < 0) + { + sgp30_dbg("cannot read data: %d\n", ret); + } + else + { + /* This interface is mainly intended for easy debugging in nsh. */ + + length = snprintf(buffer, buflen, "%u %u\n", + data.co2eq_ppm, data.tvoc_ppb); + if (length > buflen) + { + length = buflen; + } + + priv->last_update = ts; + } + + nxsem_post(&priv->devsem); + return length; +} + +/**************************************************************************** + * Name: sgp30_write + ****************************************************************************/ + +static ssize_t sgp30_write(FAR struct file *filep, FAR const char *buffer, + size_t buflen) +{ + return -ENOSYS; +} + +/**************************************************************************** + * Name: sgp30_ioctl + ****************************************************************************/ + +static int sgp30_ioctl(FAR struct file *filep, int cmd, unsigned long arg) +{ + FAR struct inode *inode = filep->f_inode; + FAR struct sgp30_dev_s *priv = inode->i_private; + struct sgp30_word_s buf[2]; + int ret; + + /* Get exclusive access */ + + do + { + ret = nxsem_wait(&priv->devsem); + + /* The only case that an error should occur here is if the wait was + * awakened by a signal. + */ + + DEBUGASSERT(ret == OK || ret == -EINTR); + } + while (ret == -EINTR); + +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS + if (priv->unlinked) + { + /* Do not allow operations on unlinked sensors. This allows + * sensor use on hot swappable I2C bus. + */ + + nxsem_post(&priv->devsem); + return -ENODEV; + } +#endif + + switch (cmd) + { + /* Soft reset the SGP30, Arg: None */ + + case SNIOC_RESET: + { + /* Note: After INIT_AIR_QUALITY, host should write baseline + * values (SNIOC_SET_BASELINE) from non-volatile memory if + * stored values are less than week old. Without baseline + * values, sensor enters 12hr long early operation phase. + */ + + ret = sgp30_write_cmd(priv, SGP30_CMD_INIT_AIR_QUALITY, NULL, 0); + sgp30_dbg("reset ret: %d\n", ret); + } + break; + + /* Get sensor baseline, Arg: struct sgp30_baseline_s * */ + + case SNIOC_GET_BASELINE: + { + FAR struct sgp30_baseline_s *baseline = + (FAR struct sgp30_baseline_s *)arg; + + /* Note: SGP30 documentation tells host should read baseline values + * every 1 hour and save to non-volatile memory along with + * timestamp. + */ + + ret = sgp30_read_cmd(priv, SGP30_CMD_GET_BASELINE, buf, 2); + if (ret >= 0) + { + if (sgp30_check_data_crc(buf, 2) < 0) + { + ret = -EIO; + break; + } + + baseline->co2eq_baseline = sgp30_data_word_to_uint16(buf + 0); + baseline->tvoc_baseline = sgp30_data_word_to_uint16(buf + 1); + } + } + break; + + /* Set sensor baseline, Arg: struct sgp30_baseline_s * */ + + case SNIOC_SET_BASELINE: + { + FAR const struct sgp30_baseline_s *baseline = + (FAR const struct sgp30_baseline_s *)arg; + + sgp30_set_command_param(buf + 0, baseline->co2eq_baseline); + sgp30_set_command_param(buf + 1, baseline->tvoc_baseline); + ret = sgp30_write_cmd(priv, SGP30_CMD_SET_BASELINE, buf, 2); + } + break; + + /* Set humidity compensation value, Arg: uint16_t */ + + case SNIOC_SET_HUMIDITY: + { + /* Input is absolute humidity in unit "mg/m^3". */ + + if (arg < 0 || arg >= 256000) + { + ret = -EINVAL; + break; + } + + arg = arg * 256 / 1000; /* scale to range 0..65535 */ + + sgp30_set_command_param(buf, arg); + ret = sgp30_write_cmd(priv, SGP30_CMD_SET_HUMIDITY, buf, 1); + } + break; + + /* Read sensor data, Arg: struct sgp30_conv_data_s *data */ + + case SNIOC_READ_CONVERT_DATA: + { + FAR struct sgp30_conv_data_s *data = + (FAR struct sgp30_conv_data_s *)arg; + + /* Note: For correct operation, SGP30 documentation tells host + * to run measurement command every 1 second. + */ + + ret = sgp30_measure_airq(priv, data); + if (ret < 0) + { + sgp30_dbg("cannot read data: %d\n", ret); + } + } + break; + + /* Read raw data, Arg: struct sgp30_raw_data_s *data */ + + case SNIOC_READ_RAW_DATA: + { + FAR struct sgp30_raw_data_s *data = + (FAR struct sgp30_raw_data_s *)arg; + + ret = sgp30_measure_raw(priv, data); + if (ret < 0) + { + sgp30_dbg("cannot read data: %d\n", ret); + } + } + break; + + /* Run selftest, Arg: None. */ + + case SNIOC_START_SELFTEST: + { + ret = sgp30_write_cmd(priv, SGP30_CMD_MEASURE_TEST, NULL, 0); + if (ret >= 0) + { + if (sgp30_check_data_crc(buf, 1) < 0) + { + sgp30_dbg("crc error\n"); + ret = -EIO; + } + else if (sgp30_data_word_to_uint16(buf) != 0xD400) + { + sgp30_dbg("self-test failed, 0x%04x\n", + sgp30_data_word_to_uint16(buf)); + ret = -EFAULT; + } + } + } + break; + + default: + sgp30_dbg("Unrecognized cmd: %d\n", cmd); + ret = -ENOTTY; + break; + } + + nxsem_post(&priv->devsem); + return ret; +} + +/**************************************************************************** + * Name: sgp30_unlink + ****************************************************************************/ + +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS +static int sgp30_unlink(FAR struct inode *inode) +{ + FAR struct sgp30_dev_s *priv; + int ret; + + DEBUGASSERT(inode != NULL && inode->i_private != NULL); + priv = (FAR struct sgp30_dev_s *)inode->i_private; + + /* Get exclusive access */ + + do + { + ret = nxsem_wait(&priv->devsem); + + /* The only case that an error should occur here is if the wait was + * awakened by a signal. + */ + + DEBUGASSERT(ret == OK || ret == -EINTR); + } + while (ret == -EINTR); + + /* Are there open references to the driver data structure? */ + + if (priv->crefs <= 0) + { + nxsem_destroy(&priv->devsem); + kmm_free(priv); + return OK; + } + + /* No... just mark the driver as unlinked and free the resources when + * the last client closes their reference to the driver. + */ + + priv->unlinked = true; + nxsem_post(&priv->devsem); + return ret; +} +#endif + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: sgp30_register + * + * Description: + * Register the SGP30 character device as 'devpath' + * + * Input Parameters: + * devpath - The full path to the driver to register. E.g., "/dev/gas0" + * i2c - An instance of the I2C interface to use to communicate with + * the SGP30 + * addr - The I2C address of the SGP30. The I2C address of SGP30 is + * always 0x58. + * + * Returned Value: + * Zero (OK) on success; a negated errno value on failure. + * + ****************************************************************************/ + +int sgp30_register(FAR const char *devpath, FAR struct i2c_master_s *i2c, + uint8_t addr) +{ + FAR struct sgp30_dev_s *priv; + int ret; + + DEBUGASSERT(i2c != NULL); + DEBUGASSERT(addr == CONFIG_SGP30_ADDR); + DEBUGASSERT(sgp30_crc_word(0xBEEF) == 0x92); + + /* Initialize the device structure */ + + priv = (FAR struct sgp30_dev_s *)kmm_zalloc(sizeof(struct sgp30_dev_s)); + if (priv == NULL) + { + sgp30_dbg("ERROR: Failed to allocate instance\n"); + return -ENOMEM; + } + + priv->i2c = i2c; + priv->addr = addr; + + nxsem_init(&priv->devsem, 0, 1); + + /* Register the character driver */ + + ret = register_driver(devpath, &g_sgp30fops, 0666, priv); + if (ret < 0) + { + sgp30_dbg("ERROR: Failed to register driver: %d\n", ret); + kmm_free(priv); + } + + return ret; +} +#endif /* CONFIG_I2C && CONFIG_SENSORS_SGP30 */ diff --git a/drivers/sensors/sps30.c b/drivers/sensors/sps30.c new file mode 100644 index 0000000000..11f77fba78 --- /dev/null +++ b/drivers/sensors/sps30.c @@ -0,0 +1,1116 @@ +/**************************************************************************** + * drivers/sensors/sps30.c + * Driver for the Sensirion SPS30 particulate matter sensor + * + * Copyright (C) 2019 Haltian Ltd. All rights reserved. + * Author: Jussi Kivilinna + * + * 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 NuttX 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 OWNER 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. + * + ****************************************************************************/ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#if defined(CONFIG_SENSORS_SPS30) + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#ifdef CONFIG_SPS30_DEBUG +# define sps30_dbg(x, ...) _info(x, ##__VA_ARGS__) +#else +# define sps30_dbg(x, ...) sninfo(x, ##__VA_ARGS__) +#endif + +#ifndef CONFIG_SPS30_I2C_FREQUENCY +# define CONFIG_SPS30_I2C_FREQUENCY 100000 +#endif + +#define SPS30_MEASUREMENT_INTERVAL 1 /* one second, fixed in hw */ +#define SPS30_MEASUREMENT_MODE 0x0300 + +#define SPS30_I2C_RETRIES 3 + +/* SPS30 command words */ + +#define SPS30_CMD_START_MEASUREMENT 0x0010 +#define SPS30_CMD_STOP_MEASUREMENT 0x0104 +#define SPS30_CMD_GET_DATA_READY 0x0202 +#define SPS30_CMD_READ_MEASUREMENT 0x0300 +#define SPS30_CMD_SET_AUTO_CLEANING_INTERVAL 0x8004 +#define SPS30_CMD_START_FAN_CLEANING 0x5607 +#define SPS30_CMD_READ_ARTICLE_CODE 0xD025 +#define SPS30_CMD_READ_SERIAL_NUMBER 0xD033 +#define SPS30_CMD_SOFT_RESET 0xD304 + +/**************************************************************************** + * Private + ****************************************************************************/ + +struct sps30_dev_s +{ +#ifdef CONFIG_SPS30_I2C + FAR struct i2c_master_s *i2c; /* I2C interface */ + uint8_t addr; /* I2C address */ +#endif + bool valid; /* If cached readings are valid */ + bool started; /* If continuous measurement is enabled */ +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS + bool unlinked; /* True, driver has been unlinked */ +#endif + struct timespec last_update; /* Last time when sensor was read */ +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS + int16_t crefs; /* Number of open references */ +#endif + sem_t devsem; + + /* Cached sensor values */ + + struct sps30_conv_data_s data; +}; + +struct sps30_word_s +{ + uint8_t data[2]; + uint8_t crc; +}; + +/**************************************************************************** + * Private Function Prototypes + ****************************************************************************/ +/* IO Helpers */ + +#ifdef CONFIG_SPS30_I2C +static int sps30_do_transfer(FAR struct i2c_master_s *i2c, + FAR struct i2c_msg_s *msgv, + size_t nmsg); +#endif +static int sps30_write_cmd(FAR struct sps30_dev_s *priv, uint16_t cmd, + FAR struct sps30_word_s *params, + unsigned int num_params); +static int sps30_read_cmd(FAR struct sps30_dev_s *priv, uint16_t cmd, + FAR struct sps30_word_s *words, + unsigned int num_words); + +/* Data conversion */ + +static uint8_t sps30_crc_word(uint16_t word); +static void sps30_set_command_param(FAR struct sps30_word_s *param, + uint16_t value); +static int sps30_check_data_crc(FAR const struct sps30_word_s *words, + unsigned int num_words); +static uint16_t sps30_data_word_to_uint16(FAR const struct sps30_word_s *word); +static float sps30_data_words_to_float(FAR const struct sps30_word_s words[2]); + +/* Driver features */ + +static int sps30_read_values(FAR struct sps30_dev_s *priv, + FAR struct sps30_conv_data_s *out, bool wait); +static int sps30_configure(FAR struct sps30_dev_s *priv, bool start); +static int sps30_read_dev_info(FAR struct sps30_dev_s *priv, uint16_t cmd, + char *out, size_t outlen); + +/* Character driver methods */ + +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS +static int sps30_open(FAR struct file *filep); +static int sps30_close(FAR struct file *filep); +#endif +static ssize_t sps30_read(FAR struct file *filep, FAR char *buffer, + size_t buflen); +static ssize_t sps30_write(FAR struct file *filep, FAR const char *buffer, + size_t buflen); +static int sps30_ioctl(FAR struct file *filep, int cmd, + unsigned long arg); +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS +static int sps30_unlink(FAR struct inode *inode); +#endif + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +static const struct file_operations g_sps30fops = +{ +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS + sps30_open, /* open */ + sps30_close, /* close */ +#else + NULL, /* open */ + NULL, /* close */ +#endif + sps30_read, /* read */ + sps30_write, /* write */ + NULL, /* seek */ + sps30_ioctl /* ioctl */ +#ifndef CONFIG_DISABLE_POLL + , NULL /* poll */ +#endif +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS + , sps30_unlink /* unlink */ +#endif +}; + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: sps30_do_transfer + ****************************************************************************/ + +#ifdef CONFIG_SPS30_I2C +static int sps30_do_transfer(FAR struct i2c_master_s *i2c, + FAR struct i2c_msg_s *msgv, + size_t nmsg) +{ + int ret = -EIO; + int retries; + + for (retries = 0; retries < SPS30_I2C_RETRIES; retries++) + { + ret = I2C_TRANSFER(i2c, msgv, nmsg); + if (ret >= 0) + { + return 0; + } + else + { + /* Some error. Try to reset I2C bus and keep trying. */ +#ifdef CONFIG_I2C_RESET + if (retries == SPS30_I2C_RETRIES - 1) + { + break; + } + + ret = I2C_RESET(i2c); + if (ret < 0) + { + sps30_dbg("I2C_RESET failed: %d\n", ret); + return ret; + } +#endif + } + } + + sps30_dbg("xfer failed: %d\n", ret); + return ret; +} +#endif + +/**************************************************************************** + * Name: sps30_write_cmd + ****************************************************************************/ + +static int sps30_write_cmd(FAR struct sps30_dev_s *priv, uint16_t cmd, + FAR struct sps30_word_s *params, + unsigned int num_params) +{ +#ifdef CONFIG_SPS30_I2C + struct i2c_msg_s msg[2]; + uint8_t cmd_buf[2]; + int ret; + + cmd_buf[0] = cmd >> 8; + cmd_buf[1] = cmd >> 0; + + msg[0].frequency = CONFIG_SPS30_I2C_FREQUENCY; + msg[0].addr = priv->addr; + msg[0].flags = 0; + msg[0].buffer = cmd_buf; + msg[0].length = 2; + + if (num_params) + { + msg[1].frequency = CONFIG_SPS30_I2C_FREQUENCY; + msg[1].addr = priv->addr; + msg[1].flags = I2C_M_NOSTART; + msg[1].buffer = (FAR uint8_t *)params; + msg[1].length = num_params * sizeof(*params); + } + + ret = sps30_do_transfer(priv->i2c, msg, (num_params) ? 2 : 1); + + sps30_dbg("cmd: 0x%04X num_params: %d ret: %d\n", + cmd, num_params, ret); + return (ret >= 0) ? OK : ret; +#else + /* UART mode not implemented yet. */ + return -ENODEV; +#endif +} + +/**************************************************************************** + * Name: sps30_read_words + ****************************************************************************/ + +static int sps30_read_cmd(FAR struct sps30_dev_s *priv, uint16_t cmd, + FAR struct sps30_word_s *words, + unsigned int num_words) +{ +#ifdef CONFIG_SPS30_I2C + struct i2c_msg_s msg[1]; + uint8_t addr_buf[2]; + int ret; + + addr_buf[0] = cmd >> 8; + addr_buf[1] = cmd >> 0; + + msg[0].frequency = CONFIG_SPS30_I2C_FREQUENCY; + msg[0].addr = priv->addr; + msg[0].flags = 0; + msg[0].buffer = addr_buf; + msg[0].length = 2; + + ret = sps30_do_transfer(priv->i2c, msg, 1); + sps30_dbg("cmd: 0x%04X ret: %d\n", cmd, ret); + if (ret < 0) + return ret; + + msg[0].frequency = CONFIG_SPS30_I2C_FREQUENCY; + msg[0].addr = priv->addr; + msg[0].flags = I2C_M_READ; + msg[0].buffer = (FAR uint8_t *)words; + msg[0].length = num_words * sizeof(*words); + + ret = sps30_do_transfer(priv->i2c, msg, 1); + + sps30_dbg("read cmd: 0x%04X num_params: %d ret: %d\n", + cmd, num_words, ret); + return (ret >= 0) ? OK : ret; +#else + /* UART mode not implemented yet. */ + return -ENODEV; +#endif +} + +/**************************************************************************** + * Name: sps30_crc_word + ****************************************************************************/ + +static uint8_t sps30_crc_word(uint16_t word) +{ + static const uint8_t crc_table[16] = + { + 0x00, 0x31, 0x62, 0x53, 0xc4, 0xf5, 0xa6, 0x97, + 0xb9, 0x88, 0xdb, 0xea, 0x7d, 0x4c, 0x1f, 0x2e + }; + uint8_t crc = 0xFF; + + crc ^= word >> 8; + crc = (crc << 4) ^ crc_table[crc >> 4]; + crc = (crc << 4) ^ crc_table[crc >> 4]; + crc ^= word >> 0; + crc = (crc << 4) ^ crc_table[crc >> 4]; + crc = (crc << 4) ^ crc_table[crc >> 4]; + + return crc; +} + +/**************************************************************************** + * Name: sps30_set_command_param + ****************************************************************************/ + +static void sps30_set_command_param(FAR struct sps30_word_s *param, + uint16_t value) +{ + param->data[0] = value >> 8; + param->data[1] = value >> 0; + param->crc = sps30_crc_word(value); +} + +/**************************************************************************** + * Name: sps30_data_words_to_float + ****************************************************************************/ + +static float sps30_data_words_to_float(FAR const struct sps30_word_s words[2]) +{ + uint8_t data[4]; + float value; + + data[3] = words[0].data[0]; + data[2] = words[0].data[1]; + data[1] = words[1].data[0]; + data[0] = words[1].data[1]; + memcpy(&value, data, 4); + return value; +} + +/**************************************************************************** + * Name: sps30_data_word_to_uint16 + ****************************************************************************/ + +static uint16_t sps30_data_word_to_uint16(FAR const struct sps30_word_s *word) +{ + return (word[0].data[0] << 8) | (word[0].data[1]); +} + +/**************************************************************************** + * Name: sps30_crc_word + ****************************************************************************/ + +static int sps30_check_data_crc(FAR const struct sps30_word_s *words, + unsigned int num_words) +{ + while (num_words) + { + if (sps30_crc_word(sps30_data_word_to_uint16(words)) != words->crc) + return -1; + + num_words--; + words++; + } + + return 0; +} + +/**************************************************************************** + * Name: sps30_softreset + * + * Description: + * Reset the SPS30 sensor. This takes less than 2000 ms. + * + ****************************************************************************/ + +static int sps30_softreset(FAR struct sps30_dev_s *priv) +{ + int ret; + + ret = sps30_write_cmd(priv, SPS30_CMD_SOFT_RESET, NULL, 0); + if (ret < 0) + return ret; + + return 0; +} + +/**************************************************************************** + * Name: has_time_passed + * + * Description: + * Return true if curr >= start + secs_since_start + * + ****************************************************************************/ + +static bool has_time_passed(struct timespec curr, + struct timespec start, + unsigned int secs_since_start) +{ + if ((long)((start.tv_sec + secs_since_start) - curr.tv_sec) == 0) + { + return start.tv_nsec <= curr.tv_nsec; + } + + return (long)((start.tv_sec + secs_since_start) - curr.tv_sec) <= 0; +} + +/**************************************************************************** + * Name: sps30_read_values + ****************************************************************************/ + +static int sps30_read_values(FAR struct sps30_dev_s *priv, + FAR struct sps30_conv_data_s *out, bool wait) +{ + struct sps30_word_s data[20]; + struct timespec ts; + int ret; + + clock_gettime(CLOCK_REALTIME, &ts); + + if (wait || !priv->valid || + has_time_passed(ts, priv->last_update, SPS30_MEASUREMENT_INTERVAL)) + { + while (1) + { + /* Wait data to be ready. */ + + ret = sps30_read_cmd(priv, SPS30_CMD_GET_DATA_READY, data, 1); + if (ret < 0) + { + sps30_dbg("ERROR: sps30_read_cmd failed: %d\n", ret); + return ret; + } + + if (sps30_check_data_crc(data, 1) < 0) + { + sps30_dbg("ERROR: sps30_read_words crc failed\n"); + ret = -EIO; + return ret; + } + + if (sps30_data_word_to_uint16(data) != 0x0001) + { + if (!wait) + { + sps30_dbg("ERROR: data not ready\n"); + ret = -EAGAIN; + return ret; + } + + ret = nxsig_usleep(500 * 1000); + if (ret == -EINTR) + { + return ret; + } + } + else + { + break; + } + } + + /* Read the raw data */ + + ret = sps30_read_cmd(priv, SPS30_CMD_READ_MEASUREMENT, data, 20); + if (ret < 0) + { + sps30_dbg("ERROR: sps30_read_cmd failed: %d\n", ret); + return ret; + } + + if (sps30_check_data_crc(data, 20) < 0) + { + sps30_dbg("ERROR: sps30_read_words crc failed\n"); + ret = -EIO; + return ret; + } + + add_sensor_randomness(((data[0].crc ^ data[1].crc) << 0) ^ + ((data[2].crc ^ data[3].crc) << 8) ^ + ((data[4].crc ^ data[5].crc) << 16) ^ + ((data[6].crc ^ data[7].crc) << 24) ^ + ((data[8].crc ^ data[9].crc) << 0) ^ + ((data[10].crc ^ data[11].crc) << 8) ^ + ((data[12].crc ^ data[13].crc) << 16) ^ + ((data[14].crc ^ data[15].crc) << 24) ^ + ((data[16].crc ^ data[17].crc) << 0) ^ + ((data[18].crc ^ data[19].crc) << 8)); + + priv->data.mass_concenration_pm1_0 = + sps30_data_words_to_float(data + 0); + priv->data.mass_concenration_pm2_5 = + sps30_data_words_to_float(data + 2); + priv->data.mass_concenration_pm4_0 = + sps30_data_words_to_float(data + 4); + priv->data.mass_concenration_pm10 = + sps30_data_words_to_float(data + 6); + priv->data.number_concenration_pm0_5 = + sps30_data_words_to_float(data + 8); + priv->data.number_concenration_pm1_0 = + sps30_data_words_to_float(data + 10); + priv->data.number_concenration_pm2_5 = + sps30_data_words_to_float(data + 12); + priv->data.number_concenration_pm4_0 = + sps30_data_words_to_float(data + 14); + priv->data.number_concenration_pm10 = + sps30_data_words_to_float(data + 16); + priv->data.typical_particle_size = + sps30_data_words_to_float(data + 18); + priv->last_update = ts; + priv->valid = true; + + /* Wait data to be ready. */ + + ret = sps30_read_cmd(priv, SPS30_CMD_GET_DATA_READY, data, 1); + if (ret < 0) + { + sps30_dbg("ERROR: sps30_read_cmd failed: %d\n", ret); + } + } + + *out = priv->data; + return OK; +} + +/**************************************************************************** + * Name: sps30_read_dev_info + ****************************************************************************/ + +static int sps30_read_dev_info(FAR struct sps30_dev_s *priv, uint16_t cmd, + char *out, size_t outlen) +{ + struct sps30_word_s buf[16]; + int ret; + + ret = sps30_read_cmd(priv, cmd, buf, 16); + if (ret < 0) + { + sps30_dbg("ERROR: sps30_read_cmd failed: %d\n", ret); + return ret; + } + + if (sps30_check_data_crc(buf, 16) < 0) + { + sps30_dbg("ERROR: sps30_read_words crc failed\n"); + ret = -EIO; + return ret; + } + + ret = 0; + while (outlen && ret < 32) + { + *out = buf[ret / 2].data[ret % 2]; + ret++; + out++; + outlen--; + } + + if (outlen) + *out = '\0'; + + return ret; +} + +/**************************************************************************** + * Name: sps30_configure + ****************************************************************************/ + +static int sps30_configure(FAR struct sps30_dev_s *priv, bool start) +{ + struct sps30_word_s param; + int ret; + + if (!start) + { + /* Stop measurements. */ + + ret = sps30_write_cmd(priv, SPS30_CMD_STOP_MEASUREMENT, ¶m, 1); + if (ret >= 0) + { + priv->started = false; + } + } + else + { + /* Start measurements (and set pressure compensation). */ + + sps30_set_command_param(¶m, SPS30_MEASUREMENT_MODE); + ret = sps30_write_cmd(priv, SPS30_CMD_START_MEASUREMENT, ¶m, 1); + if (ret >= 0) + { + priv->started = true; + } + } + + return ret; +} + +/**************************************************************************** + * Name: sps30_open + * + * Description: + * This function is called whenever the SPS30x device is opened. + * + ****************************************************************************/ + +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS +static int sps30_open(FAR struct file *filep) +{ + FAR struct inode *inode = filep->f_inode; + FAR struct sps30_dev_s *priv = inode->i_private; + union + { + uint32_t u32[8]; + char c[32]; + } code, sn; + int ret; + + /* Get exclusive access */ + + do + { + ret = nxsem_wait(&priv->devsem); + + /* The only case that an error should occur here is if the wait was + * awakened by a signal. + */ + + DEBUGASSERT(ret == OK || ret == -EINTR); + } + while (ret == -EINTR); + + /* Increment the count of open references on the driver */ + + priv->crefs++; + DEBUGASSERT(priv->crefs > 0); + + if (priv->crefs == 1) + { + /* Read device information. */ + + ret = sps30_read_dev_info(priv, SPS30_CMD_READ_ARTICLE_CODE, code.c, + sizeof(code.c)); + if (ret >= 0) + { + ret = sps30_read_dev_info(priv, SPS30_CMD_READ_SERIAL_NUMBER, sn.c, + sizeof(sn.c)); + if (ret >= 0) + { + static int once; + + if (!once) + { + once = 1; + up_rngaddentropy(RND_SRC_SENSOR, code.u32, + (strlen(code.c) + 3) / 4); + up_rngaddentropy(RND_SRC_SENSOR, sn.u32, + (strlen(sn.c) + 3) / 4); + } + + sps30_dbg("article code: \"%.*s\"\n", 32, code.c); + sps30_dbg("serial number: \"%.*s\"\n", 32, sn.c); + + /* Start sensor. */ + + ret = sps30_configure(priv, true); + } + } + } + + if (ret < 0) + { + priv->crefs--; + } + + nxsem_post(&priv->devsem); + return ret; +} +#endif + +/**************************************************************************** + * Name: sps30_close + * + * Description: + * This routine is called when the SPS30 device is closed. + * + ****************************************************************************/ + +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS +static int sps30_close(FAR struct file *filep) +{ + FAR struct inode *inode = filep->f_inode; + FAR struct sps30_dev_s *priv = inode->i_private; + int ret; + + /* Get exclusive access */ + + do + { + ret = nxsem_wait(&priv->devsem); + + /* The only case that an error should occur here is if the wait was + * awakened by a signal. + */ + + DEBUGASSERT(ret == OK || ret == -EINTR); + } + while (ret == -EINTR); + + /* Decrement the count of open references on the driver */ + + DEBUGASSERT(priv->crefs > 0); + priv->crefs--; + + /* If the count has decremented to zero and the driver has been unlinked, + * then free memory now. + */ + + if (priv->crefs <= 0 && priv->unlinked) + { + nxsem_destroy(&priv->devsem); + kmm_free(priv); + return OK; + } + + nxsem_post(&priv->devsem); + return OK; +} +#endif + +/**************************************************************************** + * Name: sps30_read + ****************************************************************************/ + +static ssize_t sps30_read(FAR struct file *filep, FAR char *buffer, + size_t buflen) +{ + FAR struct inode *inode = filep->f_inode; + FAR struct sps30_dev_s *priv = inode->i_private; + ssize_t length = 0; + struct sps30_conv_data_s data; + unsigned int data100[10]; + int ret; + + /* Get exclusive access */ + + do + { + ret = nxsem_wait(&priv->devsem); + + /* The only case that an error should occur here is if the wait was + * awakened by a signal. + */ + + DEBUGASSERT(ret == OK || ret == -EINTR); + } + while (ret == -EINTR); + +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS + if (priv->unlinked) + { + /* Do not allow operations on unlinked sensors. This allows + * sensor use on hot swappable I2C bus. + */ + + nxsem_post(&priv->devsem); + return -ENODEV; + } +#endif + + if (!priv->started) + { + return -ENODATA; + } + + ret = sps30_read_values(priv, &data, !(filep->f_oflags & O_NONBLOCK)); + if (ret < 0) + { + sps30_dbg("cannot read data: %d\n", ret); + } + else + { + /* This interface is mainly intended for easy debugging in nsh. */ + + data100[0] = abs(data.mass_concenration_pm1_0 * 100); + data100[1] = abs(data.mass_concenration_pm2_5 * 100); + data100[2] = abs(data.mass_concenration_pm4_0 * 100); + data100[3] = abs(data.mass_concenration_pm10 * 100); + data100[4] = abs(data.number_concenration_pm0_5 * 100); + data100[5] = abs(data.number_concenration_pm1_0 * 100); + data100[6] = abs(data.number_concenration_pm2_5 * 100); + data100[7] = abs(data.number_concenration_pm4_0 * 100); + data100[8] = abs(data.number_concenration_pm10 * 100); + data100[9] = abs(data.typical_particle_size * 100); + + length = snprintf(buffer, buflen, + "%u.%02u %u.%02u %u.%02u %u.%02u %u.%02u " + "%u.%02u %u.%02u %u.%02u %u.%02u %u.%02u\n", + data100[0] / 100, data100[0] % 100, + data100[1] / 100, data100[1] % 100, + data100[2] / 100, data100[2] % 100, + data100[3] / 100, data100[3] % 100, + data100[4] / 100, data100[4] % 100, + data100[5] / 100, data100[5] % 100, + data100[6] / 100, data100[6] % 100, + data100[7] / 100, data100[7] % 100, + data100[8] / 100, data100[8] % 100, + data100[9] / 100, data100[9] % 100); + if (length > buflen) + { + length = buflen; + } + } + + nxsem_post(&priv->devsem); + return length; +} + +/**************************************************************************** + * Name: sps30_write + ****************************************************************************/ + +static ssize_t sps30_write(FAR struct file *filep, FAR const char *buffer, + size_t buflen) +{ + return -ENOSYS; +} + +/**************************************************************************** + * Name: sps30_ioctl + ****************************************************************************/ + +static int sps30_ioctl(FAR struct file *filep, int cmd, unsigned long arg) +{ + FAR struct inode *inode = filep->f_inode; + FAR struct sps30_dev_s *priv = inode->i_private; + struct sps30_word_s param; + int ret; + + /* Get exclusive access */ + + do + { + ret = nxsem_wait(&priv->devsem); + + /* The only case that an error should occur here is if the wait was + * awakened by a signal. + */ + + DEBUGASSERT(ret == OK || ret == -EINTR); + } + while (ret == -EINTR); + +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS + if (priv->unlinked) + { + /* Do not allow operations on unlinked sensors. This allows + * sensor use on hot swappable I2C bus. + */ + + nxsem_post(&priv->devsem); + return -ENODEV; + } +#endif + + switch (cmd) + { + /* Soft reset the SPS30, Arg: None */ + + case SNIOC_RESET: + { + ret = sps30_softreset(priv); + sps30_dbg("softreset ret: %d\n", ret); + + sps30_configure(priv, priv->started); + } + break; + + /* Start background measurement, Arg: None */ + + case SNIOC_START: + { + /* Start measurements (and set pressure compensation). */ + + sps30_set_command_param(¶m, SPS30_MEASUREMENT_MODE); + ret = sps30_write_cmd(priv, SPS30_CMD_START_MEASUREMENT, ¶m, 1); + if (ret >= 0) + { + priv->started = true; + } + } + break; + + /* Stop background measurement, Arg: None */ + + case SNIOC_STOP: + { + /* Stop measurements. */ + + ret = sps30_write_cmd(priv, SPS30_CMD_STOP_MEASUREMENT, NULL, 0); + if (ret >= 0) + { + priv->started = false; + } + } + break; + + /* Set fan auto cleaning interval measurement interval, Arg: uint32_t */ + + case SNIOC_SET_CLEAN_INTERVAL: + { + if (arg != (uint32_t)arg) + { + ret = -EINVAL; + break; + } + + if (arg > 0 && arg < 15) + arg = 15; + + sps30_set_command_param(¶m, arg); + ret = sps30_write_cmd(priv, SPS30_CMD_SET_AUTO_CLEANING_INTERVAL, + ¶m, 1); + } + break; + + /* Start fan cleaning, Arg: None */ + + case SNIOC_START_FAN_CLEANING: + { + if (!priv->started) + { + ret = -EBUSY; + break; + } + + ret = sps30_write_cmd(priv, SPS30_CMD_START_FAN_CLEANING, NULL, 0); + } + break; + + /* Read sensor data, Arg: struct sps30_conv_data_s *data */ + + case SNIOC_READ_CONVERT_DATA: + { + FAR struct sps30_conv_data_s *data = + (FAR struct sps30_conv_data_s *)arg; + + ret = sps30_read_values(priv, data, false); + if (ret < 0) + { + sps30_dbg("cannot read data: %d\n", ret); + } + } + break; + + default: + sps30_dbg("Unrecognized cmd: %d\n", cmd); + ret = -ENOTTY; + break; + } + + nxsem_post(&priv->devsem); + return ret; +} + +/**************************************************************************** + * Name: sps30_unlink + ****************************************************************************/ + +#ifndef CONFIG_DISABLE_PSEUDOFS_OPERATIONS +static int sps30_unlink(FAR struct inode *inode) +{ + FAR struct sps30_dev_s *priv; + int ret; + + DEBUGASSERT(inode != NULL && inode->i_private != NULL); + priv = (FAR struct sps30_dev_s *)inode->i_private; + + /* Get exclusive access */ + + do + { + ret = nxsem_wait(&priv->devsem); + + /* The only case that an error should occur here is if the wait was + * awakened by a signal. + */ + + DEBUGASSERT(ret == OK || ret == -EINTR); + } + while (ret == -EINTR); + + /* Are there open references to the driver data structure? */ + + if (priv->crefs <= 0) + { + nxsem_destroy(&priv->devsem); + kmm_free(priv); + return OK; + } + + /* No... just mark the driver as unlinked and free the resources when + * the last client closes their reference to the driver. + */ + + priv->unlinked = true; + nxsem_post(&priv->devsem); + return ret; +} +#endif + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +#ifdef CONFIG_SPS30_I2C +/**************************************************************************** + * Name: sps30_register_i2c + * + * Description: + * Register the SPS30 character device as 'devpath' + * + * Input Parameters: + * devpath - The full path to the driver to register. E.g., "/dev/particle0" + * i2c - An instance of the I2C interface to use to communicate with + * the SPS30 + * addr - The I2C address of the SPS30. The I2C address of SPS30 is + * always 0x69. + * + * Returned Value: + * Zero (OK) on success; a negated errno value on failure. + * + ****************************************************************************/ + +int sps30_register_i2c(FAR const char *devpath, FAR struct i2c_master_s *i2c, + uint8_t addr) +{ + FAR struct sps30_dev_s *priv; + int ret; + + DEBUGASSERT(i2c != NULL); + DEBUGASSERT(addr == CONFIG_SPS30_ADDR); + DEBUGASSERT(sps30_crc_word(0xBEEF) == 0x92); + + /* Initialize the device structure */ + + priv = (FAR struct sps30_dev_s *)kmm_zalloc(sizeof(struct sps30_dev_s)); + if (priv == NULL) + { + sps30_dbg("ERROR: Failed to allocate instance\n"); + return -ENOMEM; + } + + priv->i2c = i2c; + priv->addr = addr; + priv->started = false; + + nxsem_init(&priv->devsem, 0, 1); + + /* Register the character driver */ + + ret = register_driver(devpath, &g_sps30fops, 0666, priv); + if (ret < 0) + { + sps30_dbg("ERROR: Failed to register driver: %d\n", ret); + kmm_free(priv); + } + + return ret; +} +#endif /* CONFIG_SPS30_I2C */ + +#endif /* CONFIG_SENSORS_SPS30 */ diff --git a/include/nuttx/sensors/ioctl.h b/include/nuttx/sensors/ioctl.h index aa44308ae1..3c0a2d6d59 100644 --- a/include/nuttx/sensors/ioctl.h +++ b/include/nuttx/sensors/ioctl.h @@ -178,4 +178,36 @@ #define SNIOC_CHANGE_SMBUSADDR _SNIOC(0x0053) /* Arg: uint8_t value */ +/* IOCTL commands unique to the SCD30 */ + +/* SNIOC_RESET */ /* Arg: None */ +/* SNIOC_START */ /* Arg: None */ +/* SNIOC_STOP */ /* Arg: None */ +/* SNIOC_READ_CONVERT_DATA */ /* Arg: struct scd30_conv_data_s* */ +#define SNIOC_SET_INTERVAL _SNIOC(0x0054) /* Arg: uint16_t value (seconds) */ +#define SNIOC_SET_TEMP_OFFSET _SNIOC(0x0055) /* Arg: uint16_t value (0.01 Kelvin) */ +#define SNIOC_SET_PRESSURE_COMP _SNIOC(0x0056) /* Arg: uint16_t value (mbar) */ +#define SNIOC_SET_ALTITUDE_COMP _SNIOC(0x0057) /* Arg: uint16_t value (meters) */ +#define SNIOC_SET_FRC _SNIOC(0x0058) /* Arg: uint16_t value (CO₂ ppm) */ +#define SNIOC_ENABLE_ASC _SNIOC(0x0059) /* Arg: bool value */ + +/* IOCTL commands unique to the SGP30 */ + +/* SNIOC_RESET */ /* Arg: None */ +/* SNIOC_START_SELFTEST */ /* Arg: None */ +/* SNIOC_READ_CONVERT_DATA */ /* Arg: struct sgp30_conv_data_s* */ +/* SNIOC_READ_RAW_DATA */ /* Arg: struct sgp30_raw_data_s* */ +#define SNIOC_GET_BASELINE _SNIOC(0x005a) /* Arg: struct sgp30_baseline_s* */ +#define SNIOC_SET_BASELINE _SNIOC(0x005b) /* Arg: const struct sgp30_baseline_s* */ +#define SNIOC_SET_HUMIDITY _SNIOC(0x005c) /* Arg: uint32_t value (mg/m³) */ + +/* IOCTL commands unique to the SPS30 */ + +/* SNIOC_RESET */ /* Arg: None */ +/* SNIOC_START */ /* Arg: None */ +/* SNIOC_STOP */ /* Arg: None */ +/* SNIOC_READ_CONVERT_DATA */ /* Arg: struct sps30_conv_data_s* */ +#define SNIOC_SET_CLEAN_INTERVAL _SNIOC(0x005d) /* Arg: uint32_t value (seconds) */ +#define SNIOC_START_FAN_CLEANING _SNIOC(0x005e) /* Arg: None */ + #endif /* __INCLUDE_NUTTX_SENSORS_IOCTL_H */ diff --git a/include/nuttx/sensors/scd30.h b/include/nuttx/sensors/scd30.h new file mode 100644 index 0000000000..c92a1c0f51 --- /dev/null +++ b/include/nuttx/sensors/scd30.h @@ -0,0 +1,90 @@ +/**************************************************************************** + * include/nuttx/sensors/scd30.h + * + * Copyright (C) 2019 Haltian Ltd. All rights reserved. + * + * 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 NuttX 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 OWNER 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. + * + ****************************************************************************/ + +#ifndef __INCLUDE_NUTT_SENSORS_SCD30_H +#define __INCLUDE_NUTT_SENSORS_SCD30_H + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define CONFIG_SCD30_ADDR 0x61 + +/**************************************************************************** + * Public Types + ****************************************************************************/ + +struct i2c_master_s; /* Forward reference */ + +struct scd30_conv_data_s +{ + float temperature; /* Celcius */ + float humidity; /* RH-% */ + float co2; /* CO₂ PPM */ +}; + +/**************************************************************************** + * Public Function Prototypes + ****************************************************************************/ + +#ifdef CONFIG_SCD30_I2C +/**************************************************************************** + * Name: scd30_register_i2c + * + * Description: + * Register the SCD30 character device as 'devpath' + * + * Input Parameters: + * devpath - The full path to the driver to register. E.g., "/dev/co2_0" + * i2c - An instance of the I2C interface to use to communicate with + * the SCD30 + * addr - The I2C address of the SCD30. The I2C address of SCD30 + * is always 0x61. + * + * Returned Value: + * Zero (OK) on success; a negated errno value on failure. + * + ****************************************************************************/ + +int scd30_register_i2c(FAR const char *devpath, FAR struct i2c_master_s *i2c, + uint8_t addr); +#endif + +#endif /* __INCLUDE_NUTT_SENSORS_SHT21_H */ diff --git a/include/nuttx/sensors/sgp30.h b/include/nuttx/sensors/sgp30.h new file mode 100644 index 0000000000..36e790701f --- /dev/null +++ b/include/nuttx/sensors/sgp30.h @@ -0,0 +1,99 @@ +/**************************************************************************** + * include/nuttx/sensors/sgp30.h + * + * Copyright (C) 2019 Haltian Ltd. All rights reserved. + * + * 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 NuttX 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 OWNER 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. + * + ****************************************************************************/ + +#ifndef __INCLUDE_NUTT_SENSORS_SGP30_H +#define __INCLUDE_NUTT_SENSORS_SGP30_H + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define CONFIG_SGP30_ADDR 0x58 + +/**************************************************************************** + * Public Types + ****************************************************************************/ + +struct i2c_master_s; /* Forward reference */ + +struct sgp30_conv_data_s +{ + uint16_t co2eq_ppm; + uint16_t tvoc_ppb; +}; + +struct sgp30_raw_data_s +{ + uint16_t h2_signal; + uint16_t ethanol_signal; +}; + +struct sgp30_baseline_s +{ + uint16_t co2eq_baseline; + uint16_t tvoc_baseline; +}; + +/**************************************************************************** + * Public Function Prototypes + ****************************************************************************/ + +/**************************************************************************** + * Name: sgp30_register + * + * Description: + * Register the SGP30 character device as 'devpath' + * + * Input Parameters: + * devpath - The full path to the driver to register. E.g., "/dev/gas0" + * i2c - An instance of the I2C interface to use to communicate with + * the SGP30 + * addr - The I2C address of the SGP30. The I2C address of SGP30 + * is always 0x58. + * + * Returned Value: + * Zero (OK) on success; a negated errno value on failure. + * + ****************************************************************************/ + +int sgp30_register(FAR const char *devpath, FAR struct i2c_master_s *i2c, + uint8_t addr); + +#endif /* __INCLUDE_NUTT_SENSORS_SHT21_H */ diff --git a/include/nuttx/sensors/sps30.h b/include/nuttx/sensors/sps30.h new file mode 100644 index 0000000000..2ee6d5a8a6 --- /dev/null +++ b/include/nuttx/sensors/sps30.h @@ -0,0 +1,107 @@ +/**************************************************************************** + * include/nuttx/sensors/sps30.h + * + * Copyright (C) 2019 Haltian Ltd. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the followirng 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 NuttX 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 OWNER 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. + * + ****************************************************************************/ + +#ifndef __INCLUDE_NUTT_SENSORS_SPS30_H +#define __INCLUDE_NUTT_SENSORS_SPS30_H + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define CONFIG_SPS30_ADDR 0x69 + +/**************************************************************************** + * Public Types + ****************************************************************************/ + +struct i2c_master_s; /* Forward reference */ + +struct sps30_conv_data_s +{ + /* Mass Concentrations for particle ranges PM1.0, PM2.5, PM4.0, PM10. + * Unit is [µg/m³] (microgram per cubicmeter). + */ + float mass_concenration_pm1_0; + float mass_concenration_pm2_5; + float mass_concenration_pm4_0; + float mass_concenration_pm10; + + /* Number Concentrations for particle ranges PM1.0, PM2.5, PM4.0, PM10. + * Unit is [#/cm³] (number per cubic-centimeter). + */ + + float number_concenration_pm0_5; + float number_concenration_pm1_0; + float number_concenration_pm2_5; + float number_concenration_pm4_0; + float number_concenration_pm10; + + /* Typical particle size. Unit is [µm] (micrometer). */ + float typical_particle_size; +}; + +/**************************************************************************** + * Public Function Prototypes + ****************************************************************************/ + +#ifdef CONFIG_SPS30_I2C +/**************************************************************************** + * Name: sps30_register_i2c + * + * Description: + * Register the SPS30 character device as 'devpath' + * + * Input Parameters: + * devpath - The full path to the driver to register. E.g., "/dev/particle0" + * i2c - An instance of the I2C interface to use to communicate with + * the SPS30 + * addr - The I2C address of the SPS30. The I2C address of SPS30 + * is always 0x69. + * + * Returned Value: + * Zero (OK) on success; a negated errno value on failure. + * + ****************************************************************************/ + +int sps30_register_i2c(FAR const char *devpath, FAR struct i2c_master_s *i2c, + uint8_t addr); +#endif + +#endif /* __INCLUDE_NUTT_SENSORS_SHT21_H */