forked from Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
574 lines
14 KiB
574 lines
14 KiB
// SPDX-License-Identifier: GPL-2.0-only |
|
/* |
|
* ccs811.c - Support for AMS CCS811 VOC Sensor |
|
* |
|
* Copyright (C) 2017 Narcisa Vasile <[email protected]> |
|
* |
|
* Datasheet: ams.com/content/download/951091/2269479/CCS811_DS000459_3-00.pdf |
|
* |
|
* IIO driver for AMS CCS811 (I2C address 0x5A/0x5B set by ADDR Low/High) |
|
* |
|
* TODO: |
|
* 1. Make the drive mode selectable form userspace |
|
* 2. Add support for interrupts |
|
* 3. Adjust time to wait for data to be ready based on selected operation mode |
|
* 4. Read error register and put the information in logs |
|
*/ |
|
|
|
#include <linux/delay.h> |
|
#include <linux/gpio/consumer.h> |
|
#include <linux/i2c.h> |
|
#include <linux/iio/iio.h> |
|
#include <linux/iio/buffer.h> |
|
#include <linux/iio/trigger.h> |
|
#include <linux/iio/triggered_buffer.h> |
|
#include <linux/iio/trigger_consumer.h> |
|
#include <linux/module.h> |
|
|
|
#define CCS811_STATUS 0x00 |
|
#define CCS811_MEAS_MODE 0x01 |
|
#define CCS811_ALG_RESULT_DATA 0x02 |
|
#define CCS811_RAW_DATA 0x03 |
|
#define CCS811_HW_ID 0x20 |
|
#define CCS811_HW_ID_VALUE 0x81 |
|
#define CCS811_HW_VERSION 0x21 |
|
#define CCS811_HW_VERSION_VALUE 0x10 |
|
#define CCS811_HW_VERSION_MASK 0xF0 |
|
#define CCS811_ERR 0xE0 |
|
/* Used to transition from boot to application mode */ |
|
#define CCS811_APP_START 0xF4 |
|
#define CCS811_SW_RESET 0xFF |
|
|
|
/* Status register flags */ |
|
#define CCS811_STATUS_ERROR BIT(0) |
|
#define CCS811_STATUS_DATA_READY BIT(3) |
|
#define CCS811_STATUS_APP_VALID_MASK BIT(4) |
|
#define CCS811_STATUS_APP_VALID_LOADED BIT(4) |
|
/* |
|
* Value of FW_MODE bit of STATUS register describes the sensor's state: |
|
* 0: Firmware is in boot mode, this allows new firmware to be loaded |
|
* 1: Firmware is in application mode. CCS811 is ready to take ADC measurements |
|
*/ |
|
#define CCS811_STATUS_FW_MODE_MASK BIT(7) |
|
#define CCS811_STATUS_FW_MODE_APPLICATION BIT(7) |
|
|
|
/* Measurement modes */ |
|
#define CCS811_MODE_IDLE 0x00 |
|
#define CCS811_MODE_IAQ_1SEC 0x10 |
|
#define CCS811_MODE_IAQ_10SEC 0x20 |
|
#define CCS811_MODE_IAQ_60SEC 0x30 |
|
#define CCS811_MODE_RAW_DATA 0x40 |
|
|
|
#define CCS811_MEAS_MODE_INTERRUPT BIT(3) |
|
|
|
#define CCS811_VOLTAGE_MASK 0x3FF |
|
|
|
struct ccs811_reading { |
|
__be16 co2; |
|
__be16 voc; |
|
u8 status; |
|
u8 error; |
|
__be16 raw_data; |
|
} __attribute__((__packed__)); |
|
|
|
struct ccs811_data { |
|
struct i2c_client *client; |
|
struct mutex lock; /* Protect readings */ |
|
struct ccs811_reading buffer; |
|
struct iio_trigger *drdy_trig; |
|
struct gpio_desc *wakeup_gpio; |
|
bool drdy_trig_on; |
|
/* Ensures correct alignment of timestamp if present */ |
|
struct { |
|
s16 channels[2]; |
|
s64 ts __aligned(8); |
|
} scan; |
|
}; |
|
|
|
static const struct iio_chan_spec ccs811_channels[] = { |
|
{ |
|
.type = IIO_CURRENT, |
|
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | |
|
BIT(IIO_CHAN_INFO_SCALE), |
|
.scan_index = -1, |
|
}, { |
|
.type = IIO_VOLTAGE, |
|
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | |
|
BIT(IIO_CHAN_INFO_SCALE), |
|
.scan_index = -1, |
|
}, { |
|
.type = IIO_CONCENTRATION, |
|
.channel2 = IIO_MOD_CO2, |
|
.modified = 1, |
|
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | |
|
BIT(IIO_CHAN_INFO_SCALE), |
|
.scan_index = 0, |
|
.scan_type = { |
|
.sign = 'u', |
|
.realbits = 16, |
|
.storagebits = 16, |
|
.endianness = IIO_BE, |
|
}, |
|
}, { |
|
.type = IIO_CONCENTRATION, |
|
.channel2 = IIO_MOD_VOC, |
|
.modified = 1, |
|
.info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | |
|
BIT(IIO_CHAN_INFO_SCALE), |
|
.scan_index = 1, |
|
.scan_type = { |
|
.sign = 'u', |
|
.realbits = 16, |
|
.storagebits = 16, |
|
.endianness = IIO_BE, |
|
}, |
|
}, |
|
IIO_CHAN_SOFT_TIMESTAMP(2), |
|
}; |
|
|
|
/* |
|
* The CCS811 powers-up in boot mode. A setup write to CCS811_APP_START will |
|
* transition the sensor to application mode. |
|
*/ |
|
static int ccs811_start_sensor_application(struct i2c_client *client) |
|
{ |
|
int ret; |
|
|
|
ret = i2c_smbus_read_byte_data(client, CCS811_STATUS); |
|
if (ret < 0) |
|
return ret; |
|
|
|
if ((ret & CCS811_STATUS_FW_MODE_APPLICATION)) |
|
return 0; |
|
|
|
if ((ret & CCS811_STATUS_APP_VALID_MASK) != |
|
CCS811_STATUS_APP_VALID_LOADED) |
|
return -EIO; |
|
|
|
ret = i2c_smbus_write_byte(client, CCS811_APP_START); |
|
if (ret < 0) |
|
return ret; |
|
|
|
ret = i2c_smbus_read_byte_data(client, CCS811_STATUS); |
|
if (ret < 0) |
|
return ret; |
|
|
|
if ((ret & CCS811_STATUS_FW_MODE_MASK) != |
|
CCS811_STATUS_FW_MODE_APPLICATION) { |
|
dev_err(&client->dev, "Application failed to start. Sensor is still in boot mode.\n"); |
|
return -EIO; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
static int ccs811_setup(struct i2c_client *client) |
|
{ |
|
int ret; |
|
|
|
ret = ccs811_start_sensor_application(client); |
|
if (ret < 0) |
|
return ret; |
|
|
|
return i2c_smbus_write_byte_data(client, CCS811_MEAS_MODE, |
|
CCS811_MODE_IAQ_1SEC); |
|
} |
|
|
|
static void ccs811_set_wakeup(struct ccs811_data *data, bool enable) |
|
{ |
|
if (!data->wakeup_gpio) |
|
return; |
|
|
|
gpiod_set_value(data->wakeup_gpio, enable); |
|
|
|
if (enable) |
|
usleep_range(50, 60); |
|
else |
|
usleep_range(20, 30); |
|
} |
|
|
|
static int ccs811_get_measurement(struct ccs811_data *data) |
|
{ |
|
int ret, tries = 11; |
|
|
|
ccs811_set_wakeup(data, true); |
|
|
|
/* Maximum waiting time: 1s, as measurements are made every second */ |
|
while (tries-- > 0) { |
|
ret = i2c_smbus_read_byte_data(data->client, CCS811_STATUS); |
|
if (ret < 0) |
|
return ret; |
|
|
|
if ((ret & CCS811_STATUS_DATA_READY) || tries == 0) |
|
break; |
|
msleep(100); |
|
} |
|
if (!(ret & CCS811_STATUS_DATA_READY)) |
|
return -EIO; |
|
|
|
ret = i2c_smbus_read_i2c_block_data(data->client, |
|
CCS811_ALG_RESULT_DATA, 8, |
|
(char *)&data->buffer); |
|
ccs811_set_wakeup(data, false); |
|
|
|
return ret; |
|
} |
|
|
|
static int ccs811_read_raw(struct iio_dev *indio_dev, |
|
struct iio_chan_spec const *chan, |
|
int *val, int *val2, long mask) |
|
{ |
|
struct ccs811_data *data = iio_priv(indio_dev); |
|
int ret; |
|
|
|
switch (mask) { |
|
case IIO_CHAN_INFO_RAW: |
|
ret = iio_device_claim_direct_mode(indio_dev); |
|
if (ret) |
|
return ret; |
|
mutex_lock(&data->lock); |
|
ret = ccs811_get_measurement(data); |
|
if (ret < 0) { |
|
mutex_unlock(&data->lock); |
|
iio_device_release_direct_mode(indio_dev); |
|
return ret; |
|
} |
|
|
|
switch (chan->type) { |
|
case IIO_VOLTAGE: |
|
*val = be16_to_cpu(data->buffer.raw_data) & |
|
CCS811_VOLTAGE_MASK; |
|
ret = IIO_VAL_INT; |
|
break; |
|
case IIO_CURRENT: |
|
*val = be16_to_cpu(data->buffer.raw_data) >> 10; |
|
ret = IIO_VAL_INT; |
|
break; |
|
case IIO_CONCENTRATION: |
|
switch (chan->channel2) { |
|
case IIO_MOD_CO2: |
|
*val = be16_to_cpu(data->buffer.co2); |
|
ret = IIO_VAL_INT; |
|
break; |
|
case IIO_MOD_VOC: |
|
*val = be16_to_cpu(data->buffer.voc); |
|
ret = IIO_VAL_INT; |
|
break; |
|
default: |
|
ret = -EINVAL; |
|
} |
|
break; |
|
default: |
|
ret = -EINVAL; |
|
} |
|
mutex_unlock(&data->lock); |
|
iio_device_release_direct_mode(indio_dev); |
|
|
|
return ret; |
|
|
|
case IIO_CHAN_INFO_SCALE: |
|
switch (chan->type) { |
|
case IIO_VOLTAGE: |
|
*val = 1; |
|
*val2 = 612903; |
|
return IIO_VAL_INT_PLUS_MICRO; |
|
case IIO_CURRENT: |
|
*val = 0; |
|
*val2 = 1000; |
|
return IIO_VAL_INT_PLUS_MICRO; |
|
case IIO_CONCENTRATION: |
|
switch (chan->channel2) { |
|
case IIO_MOD_CO2: |
|
*val = 0; |
|
*val2 = 100; |
|
return IIO_VAL_INT_PLUS_MICRO; |
|
case IIO_MOD_VOC: |
|
*val = 0; |
|
*val2 = 100; |
|
return IIO_VAL_INT_PLUS_NANO; |
|
default: |
|
return -EINVAL; |
|
} |
|
default: |
|
return -EINVAL; |
|
} |
|
default: |
|
return -EINVAL; |
|
} |
|
} |
|
|
|
static const struct iio_info ccs811_info = { |
|
.read_raw = ccs811_read_raw, |
|
}; |
|
|
|
static int ccs811_set_trigger_state(struct iio_trigger *trig, |
|
bool state) |
|
{ |
|
struct iio_dev *indio_dev = iio_trigger_get_drvdata(trig); |
|
struct ccs811_data *data = iio_priv(indio_dev); |
|
int ret; |
|
|
|
ret = i2c_smbus_read_byte_data(data->client, CCS811_MEAS_MODE); |
|
if (ret < 0) |
|
return ret; |
|
|
|
if (state) |
|
ret |= CCS811_MEAS_MODE_INTERRUPT; |
|
else |
|
ret &= ~CCS811_MEAS_MODE_INTERRUPT; |
|
|
|
data->drdy_trig_on = state; |
|
|
|
return i2c_smbus_write_byte_data(data->client, CCS811_MEAS_MODE, ret); |
|
} |
|
|
|
static const struct iio_trigger_ops ccs811_trigger_ops = { |
|
.set_trigger_state = ccs811_set_trigger_state, |
|
}; |
|
|
|
static irqreturn_t ccs811_trigger_handler(int irq, void *p) |
|
{ |
|
struct iio_poll_func *pf = p; |
|
struct iio_dev *indio_dev = pf->indio_dev; |
|
struct ccs811_data *data = iio_priv(indio_dev); |
|
struct i2c_client *client = data->client; |
|
int ret; |
|
|
|
ret = i2c_smbus_read_i2c_block_data(client, CCS811_ALG_RESULT_DATA, |
|
sizeof(data->scan.channels), |
|
(u8 *)data->scan.channels); |
|
if (ret != 4) { |
|
dev_err(&client->dev, "cannot read sensor data\n"); |
|
goto err; |
|
} |
|
|
|
iio_push_to_buffers_with_timestamp(indio_dev, &data->scan, |
|
iio_get_time_ns(indio_dev)); |
|
|
|
err: |
|
iio_trigger_notify_done(indio_dev->trig); |
|
|
|
return IRQ_HANDLED; |
|
} |
|
|
|
static irqreturn_t ccs811_data_rdy_trigger_poll(int irq, void *private) |
|
{ |
|
struct iio_dev *indio_dev = private; |
|
struct ccs811_data *data = iio_priv(indio_dev); |
|
|
|
if (data->drdy_trig_on) |
|
iio_trigger_poll(data->drdy_trig); |
|
|
|
return IRQ_HANDLED; |
|
} |
|
|
|
static int ccs811_reset(struct i2c_client *client) |
|
{ |
|
struct gpio_desc *reset_gpio; |
|
int ret; |
|
|
|
reset_gpio = devm_gpiod_get_optional(&client->dev, "reset", |
|
GPIOD_OUT_LOW); |
|
if (IS_ERR(reset_gpio)) |
|
return PTR_ERR(reset_gpio); |
|
|
|
/* Try to reset using nRESET pin if available else do SW reset */ |
|
if (reset_gpio) { |
|
gpiod_set_value(reset_gpio, 1); |
|
usleep_range(20, 30); |
|
gpiod_set_value(reset_gpio, 0); |
|
} else { |
|
/* |
|
* As per the datasheet, this sequence of values needs to be |
|
* written to the SW_RESET register for triggering the soft |
|
* reset in the device and placing it in boot mode. |
|
*/ |
|
static const u8 reset_seq[] = { |
|
0x11, 0xE5, 0x72, 0x8A, |
|
}; |
|
|
|
ret = i2c_smbus_write_i2c_block_data(client, CCS811_SW_RESET, |
|
sizeof(reset_seq), reset_seq); |
|
if (ret < 0) { |
|
dev_err(&client->dev, "Failed to reset sensor\n"); |
|
return ret; |
|
} |
|
} |
|
|
|
/* tSTART delay required after reset */ |
|
usleep_range(1000, 2000); |
|
|
|
return 0; |
|
} |
|
|
|
static int ccs811_probe(struct i2c_client *client, |
|
const struct i2c_device_id *id) |
|
{ |
|
struct iio_dev *indio_dev; |
|
struct ccs811_data *data; |
|
int ret; |
|
|
|
if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_WRITE_BYTE |
|
| I2C_FUNC_SMBUS_BYTE_DATA |
|
| I2C_FUNC_SMBUS_READ_I2C_BLOCK)) |
|
return -EOPNOTSUPP; |
|
|
|
indio_dev = devm_iio_device_alloc(&client->dev, sizeof(*data)); |
|
if (!indio_dev) |
|
return -ENOMEM; |
|
|
|
data = iio_priv(indio_dev); |
|
i2c_set_clientdata(client, indio_dev); |
|
data->client = client; |
|
|
|
data->wakeup_gpio = devm_gpiod_get_optional(&client->dev, "wakeup", |
|
GPIOD_OUT_HIGH); |
|
if (IS_ERR(data->wakeup_gpio)) |
|
return PTR_ERR(data->wakeup_gpio); |
|
|
|
ccs811_set_wakeup(data, true); |
|
|
|
ret = ccs811_reset(client); |
|
if (ret) { |
|
ccs811_set_wakeup(data, false); |
|
return ret; |
|
} |
|
|
|
/* Check hardware id (should be 0x81 for this family of devices) */ |
|
ret = i2c_smbus_read_byte_data(client, CCS811_HW_ID); |
|
if (ret < 0) { |
|
ccs811_set_wakeup(data, false); |
|
return ret; |
|
} |
|
|
|
if (ret != CCS811_HW_ID_VALUE) { |
|
dev_err(&client->dev, "hardware id doesn't match CCS81x\n"); |
|
ccs811_set_wakeup(data, false); |
|
return -ENODEV; |
|
} |
|
|
|
ret = i2c_smbus_read_byte_data(client, CCS811_HW_VERSION); |
|
if (ret < 0) { |
|
ccs811_set_wakeup(data, false); |
|
return ret; |
|
} |
|
|
|
if ((ret & CCS811_HW_VERSION_MASK) != CCS811_HW_VERSION_VALUE) { |
|
dev_err(&client->dev, "no CCS811 sensor\n"); |
|
ccs811_set_wakeup(data, false); |
|
return -ENODEV; |
|
} |
|
|
|
ret = ccs811_setup(client); |
|
if (ret < 0) { |
|
ccs811_set_wakeup(data, false); |
|
return ret; |
|
} |
|
|
|
ccs811_set_wakeup(data, false); |
|
|
|
mutex_init(&data->lock); |
|
|
|
indio_dev->name = id->name; |
|
indio_dev->info = &ccs811_info; |
|
indio_dev->modes = INDIO_DIRECT_MODE; |
|
|
|
indio_dev->channels = ccs811_channels; |
|
indio_dev->num_channels = ARRAY_SIZE(ccs811_channels); |
|
|
|
if (client->irq > 0) { |
|
ret = devm_request_threaded_irq(&client->dev, client->irq, |
|
ccs811_data_rdy_trigger_poll, |
|
NULL, |
|
IRQF_TRIGGER_FALLING | |
|
IRQF_ONESHOT, |
|
"ccs811_irq", indio_dev); |
|
if (ret) { |
|
dev_err(&client->dev, "irq request error %d\n", -ret); |
|
goto err_poweroff; |
|
} |
|
|
|
data->drdy_trig = devm_iio_trigger_alloc(&client->dev, |
|
"%s-dev%d", |
|
indio_dev->name, |
|
iio_device_id(indio_dev)); |
|
if (!data->drdy_trig) { |
|
ret = -ENOMEM; |
|
goto err_poweroff; |
|
} |
|
|
|
data->drdy_trig->ops = &ccs811_trigger_ops; |
|
iio_trigger_set_drvdata(data->drdy_trig, indio_dev); |
|
indio_dev->trig = data->drdy_trig; |
|
iio_trigger_get(indio_dev->trig); |
|
ret = iio_trigger_register(data->drdy_trig); |
|
if (ret) |
|
goto err_poweroff; |
|
} |
|
|
|
ret = iio_triggered_buffer_setup(indio_dev, NULL, |
|
ccs811_trigger_handler, NULL); |
|
|
|
if (ret < 0) { |
|
dev_err(&client->dev, "triggered buffer setup failed\n"); |
|
goto err_trigger_unregister; |
|
} |
|
|
|
ret = iio_device_register(indio_dev); |
|
if (ret < 0) { |
|
dev_err(&client->dev, "unable to register iio device\n"); |
|
goto err_buffer_cleanup; |
|
} |
|
return 0; |
|
|
|
err_buffer_cleanup: |
|
iio_triggered_buffer_cleanup(indio_dev); |
|
err_trigger_unregister: |
|
if (data->drdy_trig) |
|
iio_trigger_unregister(data->drdy_trig); |
|
err_poweroff: |
|
i2c_smbus_write_byte_data(client, CCS811_MEAS_MODE, CCS811_MODE_IDLE); |
|
|
|
return ret; |
|
} |
|
|
|
static int ccs811_remove(struct i2c_client *client) |
|
{ |
|
struct iio_dev *indio_dev = i2c_get_clientdata(client); |
|
struct ccs811_data *data = iio_priv(indio_dev); |
|
|
|
iio_device_unregister(indio_dev); |
|
iio_triggered_buffer_cleanup(indio_dev); |
|
if (data->drdy_trig) |
|
iio_trigger_unregister(data->drdy_trig); |
|
|
|
return i2c_smbus_write_byte_data(client, CCS811_MEAS_MODE, |
|
CCS811_MODE_IDLE); |
|
} |
|
|
|
static const struct i2c_device_id ccs811_id[] = { |
|
{"ccs811", 0}, |
|
{ } |
|
}; |
|
MODULE_DEVICE_TABLE(i2c, ccs811_id); |
|
|
|
static const struct of_device_id ccs811_dt_ids[] = { |
|
{ .compatible = "ams,ccs811" }, |
|
{ } |
|
}; |
|
MODULE_DEVICE_TABLE(of, ccs811_dt_ids); |
|
|
|
static struct i2c_driver ccs811_driver = { |
|
.driver = { |
|
.name = "ccs811", |
|
.of_match_table = ccs811_dt_ids, |
|
}, |
|
.probe = ccs811_probe, |
|
.remove = ccs811_remove, |
|
.id_table = ccs811_id, |
|
}; |
|
module_i2c_driver(ccs811_driver); |
|
|
|
MODULE_AUTHOR("Narcisa Vasile <[email protected]>"); |
|
MODULE_DESCRIPTION("CCS811 volatile organic compounds sensor"); |
|
MODULE_LICENSE("GPL v2");
|
|
|