Commit e3166508 authored by Gustavo Silva's avatar Gustavo Silva Committed by Jonathan Cameron

iio: chemical: add driver for ENS160 sensor

ScioSense ENS160 is a digital metal oxide multi-gas sensor, designed
for indoor air quality monitoring. The driver supports readings of
CO2 and VOC, and can be accessed via both SPI and I2C.

Datasheet: https://www.sciosense.com/wp-content/uploads/2023/12/ENS160-Datasheet.pdfSigned-off-by: default avatarGustavo Silva <gustavograzs@gmail.com>
Link: https://lore.kernel.org/r/20240604225747.7212-4-gustavograzs@gmail.comSigned-off-by: default avatarJonathan Cameron <Jonathan.Cameron@huawei.com>
parent ec6c5657
...@@ -76,6 +76,26 @@ config CCS811 ...@@ -76,6 +76,26 @@ config CCS811
Say Y here to build I2C interface support for the AMS Say Y here to build I2C interface support for the AMS
CCS811 VOC (Volatile Organic Compounds) sensor CCS811 VOC (Volatile Organic Compounds) sensor
config ENS160
tristate "ScioSense ENS160 sensor driver"
depends on (I2C || SPI)
select REGMAP
select ENS160_I2C if I2C
select ENS160_SPI if SPI
help
Say yes here to build support for ScioSense ENS160 multi-gas sensor.
This driver can also be built as a module. If so, the module for I2C
would be called ens160_i2c and ens160_spi for SPI support.
config ENS160_I2C
tristate
select REGMAP_I2C
config ENS160_SPI
tristate
select REGMAP_SPI
config IAQCORE config IAQCORE
tristate "AMS iAQ-Core VOC sensors" tristate "AMS iAQ-Core VOC sensors"
depends on I2C depends on I2C
......
...@@ -11,6 +11,9 @@ obj-$(CONFIG_BME680) += bme680_core.o ...@@ -11,6 +11,9 @@ obj-$(CONFIG_BME680) += bme680_core.o
obj-$(CONFIG_BME680_I2C) += bme680_i2c.o obj-$(CONFIG_BME680_I2C) += bme680_i2c.o
obj-$(CONFIG_BME680_SPI) += bme680_spi.o obj-$(CONFIG_BME680_SPI) += bme680_spi.o
obj-$(CONFIG_CCS811) += ccs811.o obj-$(CONFIG_CCS811) += ccs811.o
obj-$(CONFIG_ENS160) += ens160_core.o
obj-$(CONFIG_ENS160_I2C) += ens160_i2c.o
obj-$(CONFIG_ENS160_SPI) += ens160_spi.o
obj-$(CONFIG_IAQCORE) += ams-iaq-core.o obj-$(CONFIG_IAQCORE) += ams-iaq-core.o
obj-$(CONFIG_PMS7003) += pms7003.o obj-$(CONFIG_PMS7003) += pms7003.o
obj-$(CONFIG_SCD30_CORE) += scd30_core.o obj-$(CONFIG_SCD30_CORE) += scd30_core.o
......
/* SPDX-License-Identifier: GPL-2.0 */
#ifndef ENS160_H_
#define ENS160_H_
int devm_ens160_core_probe(struct device *dev, struct regmap *regmap,
const char *name);
#endif
// SPDX-License-Identifier: GPL-2.0
/*
* ScioSense ENS160 multi-gas sensor driver
*
* Copyright (c) 2024 Gustavo Silva <gustavograzs@gmail.com>
*
* Datasheet:
* https://www.sciosense.com/wp-content/uploads/2023/12/ENS160-Datasheet.pdf
*/
#include <linux/bitfield.h>
#include <linux/iio/iio.h>
#include <linux/module.h>
#include <linux/regmap.h>
#include "ens160.h"
#define ENS160_PART_ID 0x160
#define ENS160_BOOTING_TIME_MS 10U
#define ENS160_REG_PART_ID 0x00
#define ENS160_REG_OPMODE 0x10
#define ENS160_REG_MODE_DEEP_SLEEP 0x00
#define ENS160_REG_MODE_IDLE 0x01
#define ENS160_REG_MODE_STANDARD 0x02
#define ENS160_REG_MODE_RESET 0xF0
#define ENS160_REG_COMMAND 0x12
#define ENS160_REG_COMMAND_GET_APPVER 0x0E
#define ENS160_REG_COMMAND_CLRGPR 0xCC
#define ENS160_REG_TEMP_IN 0x13
#define ENS160_REG_RH_IN 0x15
#define ENS160_REG_DEVICE_STATUS 0x20
#define ENS160_REG_DATA_AQI 0x21
#define ENS160_REG_DATA_TVOC 0x22
#define ENS160_REG_DATA_ECO2 0x24
#define ENS160_REG_DATA_T 0x30
#define ENS160_REG_DATA_RH 0x32
#define ENS160_REG_GPR_READ4 0x4C
#define ENS160_STATUS_VALIDITY_FLAG GENMASK(3, 2)
#define ENS160_STATUS_NORMAL 0x00
struct ens160_data {
struct regmap *regmap;
u8 fw_version[3] __aligned(IIO_DMA_MINALIGN);
__le16 buf;
};
static const struct iio_chan_spec ens160_channels[] = {
{
.type = IIO_CONCENTRATION,
.channel2 = IIO_MOD_VOC,
.modified = 1,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
BIT(IIO_CHAN_INFO_SCALE),
.address = ENS160_REG_DATA_TVOC,
},
{
.type = IIO_CONCENTRATION,
.channel2 = IIO_MOD_CO2,
.modified = 1,
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) |
BIT(IIO_CHAN_INFO_SCALE),
.address = ENS160_REG_DATA_ECO2,
},
};
static int ens160_read_raw(struct iio_dev *indio_dev,
struct iio_chan_spec const *chan,
int *val, int *val2, long mask)
{
struct ens160_data *data = iio_priv(indio_dev);
int ret;
switch (mask) {
case IIO_CHAN_INFO_RAW:
ret = regmap_bulk_read(data->regmap, chan->address,
&data->buf, sizeof(data->buf));
if (ret)
return ret;
*val = le16_to_cpu(data->buf);
return IIO_VAL_INT;
case IIO_CHAN_INFO_SCALE:
switch (chan->channel2) {
case IIO_MOD_CO2:
/* The sensor reads CO2 data as ppm */
*val = 0;
*val2 = 100;
return IIO_VAL_INT_PLUS_MICRO;
case IIO_MOD_VOC:
/* The sensor reads VOC data as ppb */
*val = 0;
*val2 = 100;
return IIO_VAL_INT_PLUS_NANO;
default:
return -EINVAL;
}
default:
return -EINVAL;
}
}
static int ens160_set_mode(struct ens160_data *data, u8 mode)
{
int ret;
ret = regmap_write(data->regmap, ENS160_REG_OPMODE, mode);
if (ret)
return ret;
msleep(ENS160_BOOTING_TIME_MS);
return 0;
}
static void ens160_set_idle(void *data)
{
ens160_set_mode(data, ENS160_REG_MODE_IDLE);
}
static int ens160_chip_init(struct ens160_data *data)
{
struct device *dev = regmap_get_device(data->regmap);
unsigned int status;
int ret;
ret = ens160_set_mode(data, ENS160_REG_MODE_RESET);
if (ret)
return ret;
ret = regmap_bulk_read(data->regmap, ENS160_REG_PART_ID, &data->buf,
sizeof(data->buf));
if (ret)
return ret;
if (le16_to_cpu(data->buf) != ENS160_PART_ID)
return -ENODEV;
ret = ens160_set_mode(data, ENS160_REG_MODE_IDLE);
if (ret)
return ret;
ret = regmap_write(data->regmap, ENS160_REG_COMMAND,
ENS160_REG_COMMAND_CLRGPR);
if (ret)
return ret;
ret = regmap_write(data->regmap, ENS160_REG_COMMAND,
ENS160_REG_COMMAND_GET_APPVER);
if (ret)
return ret;
ret = regmap_bulk_read(data->regmap, ENS160_REG_GPR_READ4,
data->fw_version, sizeof(data->fw_version));
if (ret)
return ret;
dev_info(dev, "firmware version: %u.%u.%u\n", data->fw_version[2],
data->fw_version[1], data->fw_version[0]);
ret = ens160_set_mode(data, ENS160_REG_MODE_STANDARD);
if (ret)
return ret;
ret = devm_add_action_or_reset(dev, ens160_set_idle, data);
if (ret)
return ret;
ret = regmap_read(data->regmap, ENS160_REG_DEVICE_STATUS, &status);
if (ret)
return ret;
if (FIELD_GET(ENS160_STATUS_VALIDITY_FLAG, status)
!= ENS160_STATUS_NORMAL)
return -EINVAL;
return 0;
}
static const struct iio_info ens160_info = {
.read_raw = ens160_read_raw,
};
int devm_ens160_core_probe(struct device *dev, struct regmap *regmap,
const char *name)
{
struct ens160_data *data;
struct iio_dev *indio_dev;
int ret;
indio_dev = devm_iio_device_alloc(dev, sizeof(*data));
if (!indio_dev)
return -ENOMEM;
data = iio_priv(indio_dev);
data->regmap = regmap;
indio_dev->name = name;
indio_dev->info = &ens160_info;
indio_dev->modes = INDIO_DIRECT_MODE;
indio_dev->channels = ens160_channels;
indio_dev->num_channels = ARRAY_SIZE(ens160_channels);
ret = ens160_chip_init(data);
if (ret)
return dev_err_probe(dev, ret, "chip initialization failed\n");
return devm_iio_device_register(dev, indio_dev);
}
EXPORT_SYMBOL_NS(devm_ens160_core_probe, IIO_ENS160);
MODULE_AUTHOR("Gustavo Silva <gustavograzs@gmail.com>");
MODULE_DESCRIPTION("ScioSense ENS160 driver");
MODULE_LICENSE("GPL v2");
// SPDX-License-Identifier: GPL-2.0
/*
* ScioSense ENS160 multi-gas sensor I2C driver
*
* Copyright (c) 2024 Gustavo Silva <gustavograzs@gmail.com>
*
* 7-Bit I2C slave address is:
* - 0x52 if ADDR pin LOW
* - 0x53 if ADDR pin HIGH
*/
#include <linux/i2c.h>
#include <linux/module.h>
#include <linux/regmap.h>
#include "ens160.h"
static const struct regmap_config ens160_regmap_i2c_conf = {
.reg_bits = 8,
.val_bits = 8,
};
static int ens160_i2c_probe(struct i2c_client *client)
{
struct regmap *regmap;
regmap = devm_regmap_init_i2c(client, &ens160_regmap_i2c_conf);
if (IS_ERR(regmap))
return dev_err_probe(&client->dev, PTR_ERR(regmap),
"Failed to register i2c regmap\n");
return devm_ens160_core_probe(&client->dev, regmap, "ens160");
}
static const struct i2c_device_id ens160_i2c_id[] = {
{ "ens160" },
{ }
};
MODULE_DEVICE_TABLE(i2c, ens160_i2c_id);
static const struct of_device_id ens160_of_i2c_match[] = {
{ .compatible = "sciosense,ens160" },
{ }
};
MODULE_DEVICE_TABLE(of, ens160_of_i2c_match);
static struct i2c_driver ens160_i2c_driver = {
.driver = {
.name = "ens160",
.of_match_table = ens160_of_i2c_match,
},
.probe = ens160_i2c_probe,
.id_table = ens160_i2c_id,
};
module_i2c_driver(ens160_i2c_driver);
MODULE_AUTHOR("Gustavo Silva <gustavograzs@gmail.com>");
MODULE_DESCRIPTION("ScioSense ENS160 I2C driver");
MODULE_LICENSE("GPL v2");
MODULE_IMPORT_NS(IIO_ENS160);
// SPDX-License-Identifier: GPL-2.0
/*
* ScioSense ENS160 multi-gas sensor SPI driver
*
* Copyright (c) 2024 Gustavo Silva <gustavograzs@gmail.com>
*/
#include <linux/module.h>
#include <linux/regmap.h>
#include <linux/spi/spi.h>
#include "ens160.h"
#define ENS160_SPI_READ BIT(0)
static const struct regmap_config ens160_regmap_spi_conf = {
.reg_bits = 8,
.val_bits = 8,
.reg_shift = -1,
.read_flag_mask = ENS160_SPI_READ,
};
static int ens160_spi_probe(struct spi_device *spi)
{
struct regmap *regmap;
regmap = devm_regmap_init_spi(spi, &ens160_regmap_spi_conf);
if (IS_ERR(regmap))
return dev_err_probe(&spi->dev, PTR_ERR(regmap),
"Failed to register spi regmap\n");
return devm_ens160_core_probe(&spi->dev, regmap, "ens160");
}
static const struct of_device_id ens160_spi_of_match[] = {
{ .compatible = "sciosense,ens160" },
{ }
};
MODULE_DEVICE_TABLE(of, ens160_spi_of_match);
static const struct spi_device_id ens160_spi_id[] = {
{ "ens160" },
{ }
};
MODULE_DEVICE_TABLE(spi, ens160_spi_id);
static struct spi_driver ens160_spi_driver = {
.driver = {
.name = "ens160",
.of_match_table = ens160_spi_of_match,
},
.probe = ens160_spi_probe,
.id_table = ens160_spi_id,
};
module_spi_driver(ens160_spi_driver);
MODULE_AUTHOR("Gustavo Silva <gustavograzs@gmail.com>");
MODULE_DESCRIPTION("ScioSense ENS160 SPI driver");
MODULE_LICENSE("GPL v2");
MODULE_IMPORT_NS(IIO_ENS160);
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment