| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Sensirion SCD4X carbon dioxide sensor i2c driver |
| * |
| * Copyright (C) 2021 Protonic Holland |
| * Author: Roan van Dijk <roan@protonic.nl> |
| * |
| * I2C slave address: 0x62 |
| * |
| * Datasheets: |
| * https://www.sensirion.com/file/datasheet_scd4x |
| */ |
| |
| #include <asm/unaligned.h> |
| #include <linux/crc8.h> |
| #include <linux/delay.h> |
| #include <linux/device.h> |
| #include <linux/i2c.h> |
| #include <linux/iio/buffer.h> |
| #include <linux/iio/iio.h> |
| #include <linux/iio/sysfs.h> |
| #include <linux/iio/trigger.h> |
| #include <linux/iio/trigger_consumer.h> |
| #include <linux/iio/triggered_buffer.h> |
| #include <linux/iio/types.h> |
| #include <linux/kernel.h> |
| #include <linux/mutex.h> |
| #include <linux/string.h> |
| #include <linux/sysfs.h> |
| #include <linux/types.h> |
| |
| #define SCD4X_CRC8_POLYNOMIAL 0x31 |
| #define SCD4X_TIMEOUT_ERR 1000 |
| #define SCD4X_READ_BUF_SIZE 9 |
| #define SCD4X_COMMAND_BUF_SIZE 2 |
| #define SCD4X_WRITE_BUF_SIZE 5 |
| #define SCD4X_FRC_MIN_PPM 0 |
| #define SCD4X_FRC_MAX_PPM 2000 |
| #define SCD4X_READY_MASK 0x01 |
| |
| /*Commands SCD4X*/ |
| enum scd4x_cmd { |
| CMD_START_MEAS = 0x21b1, |
| CMD_READ_MEAS = 0xec05, |
| CMD_STOP_MEAS = 0x3f86, |
| CMD_SET_TEMP_OFFSET = 0x241d, |
| CMD_GET_TEMP_OFFSET = 0x2318, |
| CMD_FRC = 0x362f, |
| CMD_SET_ASC = 0x2416, |
| CMD_GET_ASC = 0x2313, |
| CMD_GET_DATA_READY = 0xe4b8, |
| }; |
| |
| enum scd4x_channel_idx { |
| SCD4X_CO2, |
| SCD4X_TEMP, |
| SCD4X_HR, |
| }; |
| |
| struct scd4x_state { |
| struct i2c_client *client; |
| /* maintain access to device, to prevent concurrent reads/writes */ |
| struct mutex lock; |
| struct regulator *vdd; |
| }; |
| |
| DECLARE_CRC8_TABLE(scd4x_crc8_table); |
| |
| static int scd4x_i2c_xfer(struct scd4x_state *state, char *txbuf, int txsize, |
| char *rxbuf, int rxsize) |
| { |
| struct i2c_client *client = state->client; |
| int ret; |
| |
| ret = i2c_master_send(client, txbuf, txsize); |
| |
| if (ret < 0) |
| return ret; |
| if (ret != txsize) |
| return -EIO; |
| |
| if (rxsize == 0) |
| return 0; |
| |
| ret = i2c_master_recv(client, rxbuf, rxsize); |
| if (ret < 0) |
| return ret; |
| if (ret != rxsize) |
| return -EIO; |
| |
| return 0; |
| } |
| |
| static int scd4x_send_command(struct scd4x_state *state, enum scd4x_cmd cmd) |
| { |
| char buf[SCD4X_COMMAND_BUF_SIZE]; |
| int ret; |
| |
| /* |
| * Measurement needs to be stopped before sending commands. |
| * Except stop and start command. |
| */ |
| if ((cmd != CMD_STOP_MEAS) && (cmd != CMD_START_MEAS)) { |
| |
| ret = scd4x_send_command(state, CMD_STOP_MEAS); |
| if (ret) |
| return ret; |
| |
| /* execution time for stopping measurement */ |
| msleep_interruptible(500); |
| } |
| |
| put_unaligned_be16(cmd, buf); |
| ret = scd4x_i2c_xfer(state, buf, 2, buf, 0); |
| if (ret) |
| return ret; |
| |
| if ((cmd != CMD_STOP_MEAS) && (cmd != CMD_START_MEAS)) { |
| ret = scd4x_send_command(state, CMD_START_MEAS); |
| if (ret) |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static int scd4x_read(struct scd4x_state *state, enum scd4x_cmd cmd, |
| void *response, int response_sz) |
| { |
| struct i2c_client *client = state->client; |
| char buf[SCD4X_READ_BUF_SIZE]; |
| char *rsp = response; |
| int i, ret; |
| char crc; |
| |
| /* |
| * Measurement needs to be stopped before sending commands. |
| * Except for reading measurement and data ready command. |
| */ |
| if ((cmd != CMD_GET_DATA_READY) && (cmd != CMD_READ_MEAS)) { |
| ret = scd4x_send_command(state, CMD_STOP_MEAS); |
| if (ret) |
| return ret; |
| |
| /* execution time for stopping measurement */ |
| msleep_interruptible(500); |
| } |
| |
| /* CRC byte for every 2 bytes of data */ |
| response_sz += response_sz / 2; |
| |
| put_unaligned_be16(cmd, buf); |
| ret = scd4x_i2c_xfer(state, buf, 2, buf, response_sz); |
| if (ret) |
| return ret; |
| |
| for (i = 0; i < response_sz; i += 3) { |
| crc = crc8(scd4x_crc8_table, buf + i, 2, CRC8_INIT_VALUE); |
| if (crc != buf[i + 2]) { |
| dev_err(&client->dev, "CRC error\n"); |
| return -EIO; |
| } |
| |
| *rsp++ = buf[i]; |
| *rsp++ = buf[i + 1]; |
| } |
| |
| /* start measurement */ |
| if ((cmd != CMD_GET_DATA_READY) && (cmd != CMD_READ_MEAS)) { |
| ret = scd4x_send_command(state, CMD_START_MEAS); |
| if (ret) |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static int scd4x_write(struct scd4x_state *state, enum scd4x_cmd cmd, uint16_t arg) |
| { |
| char buf[SCD4X_WRITE_BUF_SIZE]; |
| int ret; |
| char crc; |
| |
| put_unaligned_be16(cmd, buf); |
| put_unaligned_be16(arg, buf + 2); |
| |
| crc = crc8(scd4x_crc8_table, buf + 2, 2, CRC8_INIT_VALUE); |
| buf[4] = crc; |
| |
| /* measurement needs to be stopped before sending commands */ |
| ret = scd4x_send_command(state, CMD_STOP_MEAS); |
| if (ret) |
| return ret; |
| |
| /* execution time */ |
| msleep_interruptible(500); |
| |
| ret = scd4x_i2c_xfer(state, buf, SCD4X_WRITE_BUF_SIZE, buf, 0); |
| if (ret) |
| return ret; |
| |
| /* start measurement, except for forced calibration command */ |
| if (cmd != CMD_FRC) { |
| ret = scd4x_send_command(state, CMD_START_MEAS); |
| if (ret) |
| return ret; |
| } |
| |
| return 0; |
| } |
| |
| static int scd4x_write_and_fetch(struct scd4x_state *state, enum scd4x_cmd cmd, |
| uint16_t arg, void *response, int response_sz) |
| { |
| struct i2c_client *client = state->client; |
| char buf[SCD4X_READ_BUF_SIZE]; |
| char *rsp = response; |
| int i, ret; |
| char crc; |
| |
| ret = scd4x_write(state, CMD_FRC, arg); |
| if (ret) |
| goto err; |
| |
| /* execution time */ |
| msleep_interruptible(400); |
| |
| /* CRC byte for every 2 bytes of data */ |
| response_sz += response_sz / 2; |
| |
| ret = i2c_master_recv(client, buf, response_sz); |
| if (ret < 0) |
| goto err; |
| if (ret != response_sz) { |
| ret = -EIO; |
| goto err; |
| } |
| |
| for (i = 0; i < response_sz; i += 3) { |
| crc = crc8(scd4x_crc8_table, buf + i, 2, CRC8_INIT_VALUE); |
| if (crc != buf[i + 2]) { |
| dev_err(&client->dev, "CRC error\n"); |
| ret = -EIO; |
| goto err; |
| } |
| |
| *rsp++ = buf[i]; |
| *rsp++ = buf[i + 1]; |
| } |
| |
| return scd4x_send_command(state, CMD_START_MEAS); |
| |
| err: |
| /* |
| * on error try to start the measurement, |
| * puts sensor back into continuous measurement |
| */ |
| scd4x_send_command(state, CMD_START_MEAS); |
| |
| return ret; |
| } |
| |
| static int scd4x_read_meas(struct scd4x_state *state, uint16_t *meas) |
| { |
| int i, ret; |
| __be16 buf[3]; |
| |
| ret = scd4x_read(state, CMD_READ_MEAS, buf, sizeof(buf)); |
| if (ret) |
| return ret; |
| |
| for (i = 0; i < ARRAY_SIZE(buf); i++) |
| meas[i] = be16_to_cpu(buf[i]); |
| |
| return 0; |
| } |
| |
| static int scd4x_wait_meas_poll(struct scd4x_state *state) |
| { |
| struct i2c_client *client = state->client; |
| int tries = 6; |
| int ret; |
| |
| do { |
| __be16 bval; |
| uint16_t val; |
| |
| ret = scd4x_read(state, CMD_GET_DATA_READY, &bval, sizeof(bval)); |
| if (ret) |
| return -EIO; |
| val = be16_to_cpu(bval); |
| |
| /* new measurement available */ |
| if (val & 0x7FF) |
| return 0; |
| |
| msleep_interruptible(1000); |
| } while (--tries); |
| |
| /* try to start sensor on timeout */ |
| ret = scd4x_send_command(state, CMD_START_MEAS); |
| if (ret) |
| dev_err(&client->dev, "failed to start measurement: %d\n", ret); |
| |
| return -ETIMEDOUT; |
| } |
| |
| static int scd4x_read_poll(struct scd4x_state *state, uint16_t *buf) |
| { |
| int ret; |
| |
| ret = scd4x_wait_meas_poll(state); |
| if (ret) |
| return ret; |
| |
| return scd4x_read_meas(state, buf); |
| } |
| |
| static int scd4x_read_channel(struct scd4x_state *state, int chan) |
| { |
| int ret; |
| uint16_t buf[3]; |
| |
| ret = scd4x_read_poll(state, buf); |
| if (ret) |
| return ret; |
| |
| return buf[chan]; |
| } |
| |
| static int scd4x_read_raw(struct iio_dev *indio_dev, |
| struct iio_chan_spec const *chan, int *val, |
| int *val2, long mask) |
| { |
| struct scd4x_state *state = iio_priv(indio_dev); |
| int ret; |
| __be16 tmp; |
| |
| switch (mask) { |
| case IIO_CHAN_INFO_RAW: |
| ret = iio_device_claim_direct_mode(indio_dev); |
| if (ret) |
| return ret; |
| |
| mutex_lock(&state->lock); |
| ret = scd4x_read_channel(state, chan->address); |
| mutex_unlock(&state->lock); |
| |
| iio_device_release_direct_mode(indio_dev); |
| if (ret < 0) |
| return ret; |
| |
| *val = ret; |
| return IIO_VAL_INT; |
| case IIO_CHAN_INFO_SCALE: |
| if (chan->type == IIO_CONCENTRATION) { |
| *val = 0; |
| *val2 = 100; |
| return IIO_VAL_INT_PLUS_MICRO; |
| } else if (chan->type == IIO_TEMP) { |
| *val = 175000; |
| *val2 = 65536; |
| return IIO_VAL_FRACTIONAL; |
| } else if (chan->type == IIO_HUMIDITYRELATIVE) { |
| *val = 100000; |
| *val2 = 65536; |
| return IIO_VAL_FRACTIONAL; |
| } |
| return -EINVAL; |
| case IIO_CHAN_INFO_OFFSET: |
| *val = -16852; |
| *val2 = 114286; |
| return IIO_VAL_INT_PLUS_MICRO; |
| case IIO_CHAN_INFO_CALIBBIAS: |
| mutex_lock(&state->lock); |
| ret = scd4x_read(state, CMD_GET_TEMP_OFFSET, &tmp, sizeof(tmp)); |
| mutex_unlock(&state->lock); |
| if (ret) |
| return ret; |
| |
| *val = be16_to_cpu(tmp); |
| |
| return IIO_VAL_INT; |
| default: |
| return -EINVAL; |
| } |
| } |
| |
| static int scd4x_write_raw(struct iio_dev *indio_dev, struct iio_chan_spec const *chan, |
| int val, int val2, long mask) |
| { |
| struct scd4x_state *state = iio_priv(indio_dev); |
| int ret = 0; |
| |
| switch (mask) { |
| case IIO_CHAN_INFO_CALIBBIAS: |
| mutex_lock(&state->lock); |
| ret = scd4x_write(state, CMD_SET_TEMP_OFFSET, val); |
| mutex_unlock(&state->lock); |
| |
| return ret; |
| default: |
| return -EINVAL; |
| } |
| } |
| |
| static ssize_t calibration_auto_enable_show(struct device *dev, |
| struct device_attribute *attr, char *buf) |
| { |
| struct iio_dev *indio_dev = dev_to_iio_dev(dev); |
| struct scd4x_state *state = iio_priv(indio_dev); |
| int ret; |
| __be16 bval; |
| u16 val; |
| |
| mutex_lock(&state->lock); |
| ret = scd4x_read(state, CMD_GET_ASC, &bval, sizeof(bval)); |
| mutex_unlock(&state->lock); |
| if (ret) { |
| dev_err(dev, "failed to read automatic calibration"); |
| return ret; |
| } |
| |
| val = (be16_to_cpu(bval) & SCD4X_READY_MASK) ? 1 : 0; |
| |
| return sprintf(buf, "%d\n", val); |
| } |
| |
| static ssize_t calibration_auto_enable_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t len) |
| { |
| struct iio_dev *indio_dev = dev_to_iio_dev(dev); |
| struct scd4x_state *state = iio_priv(indio_dev); |
| bool val; |
| int ret; |
| uint16_t value; |
| |
| ret = kstrtobool(buf, &val); |
| if (ret) |
| return ret; |
| |
| value = val; |
| |
| mutex_lock(&state->lock); |
| ret = scd4x_write(state, CMD_SET_ASC, value); |
| mutex_unlock(&state->lock); |
| if (ret) |
| dev_err(dev, "failed to set automatic calibration"); |
| |
| return ret ?: len; |
| } |
| |
| static ssize_t calibration_forced_value_store(struct device *dev, |
| struct device_attribute *attr, |
| const char *buf, size_t len) |
| { |
| struct iio_dev *indio_dev = dev_to_iio_dev(dev); |
| struct scd4x_state *state = iio_priv(indio_dev); |
| uint16_t val, arg; |
| int ret; |
| |
| ret = kstrtou16(buf, 0, &arg); |
| if (ret) |
| return ret; |
| |
| if (arg < SCD4X_FRC_MIN_PPM || arg > SCD4X_FRC_MAX_PPM) |
| return -EINVAL; |
| |
| mutex_lock(&state->lock); |
| ret = scd4x_write_and_fetch(state, CMD_FRC, arg, &val, sizeof(val)); |
| mutex_unlock(&state->lock); |
| |
| if (val == 0xff) { |
| dev_err(dev, "forced calibration has failed"); |
| return -EINVAL; |
| } |
| |
| return ret ?: len; |
| } |
| |
| static IIO_DEVICE_ATTR_RW(calibration_auto_enable, 0); |
| static IIO_DEVICE_ATTR_WO(calibration_forced_value, 0); |
| |
| static IIO_CONST_ATTR(calibration_forced_value_available, |
| __stringify([SCD4X_FRC_MIN_PPM 1 SCD4X_FRC_MAX_PPM])); |
| |
| static struct attribute *scd4x_attrs[] = { |
| &iio_dev_attr_calibration_auto_enable.dev_attr.attr, |
| &iio_dev_attr_calibration_forced_value.dev_attr.attr, |
| &iio_const_attr_calibration_forced_value_available.dev_attr.attr, |
| NULL |
| }; |
| |
| static const struct attribute_group scd4x_attr_group = { |
| .attrs = scd4x_attrs, |
| }; |
| |
| static const struct iio_info scd4x_info = { |
| .attrs = &scd4x_attr_group, |
| .read_raw = scd4x_read_raw, |
| .write_raw = scd4x_write_raw, |
| }; |
| |
| static const struct iio_chan_spec scd4x_channels[] = { |
| { |
| .type = IIO_CONCENTRATION, |
| .channel2 = IIO_MOD_CO2, |
| .modified = 1, |
| .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | |
| BIT(IIO_CHAN_INFO_SCALE), |
| .address = SCD4X_CO2, |
| .scan_index = SCD4X_CO2, |
| .scan_type = { |
| .sign = 'u', |
| .realbits = 16, |
| .storagebits = 16, |
| .endianness = IIO_BE, |
| }, |
| }, |
| { |
| .type = IIO_TEMP, |
| .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | |
| BIT(IIO_CHAN_INFO_SCALE) | |
| BIT(IIO_CHAN_INFO_OFFSET) | |
| BIT(IIO_CHAN_INFO_CALIBBIAS), |
| .address = SCD4X_TEMP, |
| .scan_index = SCD4X_TEMP, |
| .scan_type = { |
| .sign = 'u', |
| .realbits = 16, |
| .storagebits = 16, |
| .endianness = IIO_BE, |
| }, |
| }, |
| { |
| .type = IIO_HUMIDITYRELATIVE, |
| .info_mask_separate = BIT(IIO_CHAN_INFO_RAW) | |
| BIT(IIO_CHAN_INFO_SCALE), |
| .address = SCD4X_HR, |
| .scan_index = SCD4X_HR, |
| .scan_type = { |
| .sign = 'u', |
| .realbits = 16, |
| .storagebits = 16, |
| .endianness = IIO_BE, |
| }, |
| }, |
| }; |
| |
| static int __maybe_unused scd4x_suspend(struct device *dev) |
| { |
| struct iio_dev *indio_dev = dev_get_drvdata(dev); |
| struct scd4x_state *state = iio_priv(indio_dev); |
| int ret; |
| |
| ret = scd4x_send_command(state, CMD_STOP_MEAS); |
| if (ret) |
| return ret; |
| |
| return regulator_disable(state->vdd); |
| } |
| |
| static int __maybe_unused scd4x_resume(struct device *dev) |
| { |
| struct iio_dev *indio_dev = dev_get_drvdata(dev); |
| struct scd4x_state *state = iio_priv(indio_dev); |
| int ret; |
| |
| ret = regulator_enable(state->vdd); |
| if (ret) |
| return ret; |
| |
| return scd4x_send_command(state, CMD_START_MEAS); |
| } |
| |
| static __maybe_unused SIMPLE_DEV_PM_OPS(scd4x_pm_ops, scd4x_suspend, scd4x_resume); |
| |
| static void scd4x_stop_meas(void *state) |
| { |
| scd4x_send_command(state, CMD_STOP_MEAS); |
| } |
| |
| static void scd4x_disable_regulator(void *data) |
| { |
| struct scd4x_state *state = data; |
| |
| regulator_disable(state->vdd); |
| } |
| |
| static irqreturn_t scd4x_trigger_handler(int irq, void *p) |
| { |
| struct iio_poll_func *pf = p; |
| struct iio_dev *indio_dev = pf->indio_dev; |
| struct scd4x_state *state = iio_priv(indio_dev); |
| struct { |
| uint16_t data[3]; |
| int64_t ts __aligned(8); |
| } scan; |
| int ret; |
| |
| memset(&scan, 0, sizeof(scan)); |
| mutex_lock(&state->lock); |
| ret = scd4x_read_poll(state, scan.data); |
| mutex_unlock(&state->lock); |
| if (ret) |
| goto out; |
| |
| iio_push_to_buffers_with_timestamp(indio_dev, &scan, iio_get_time_ns(indio_dev)); |
| out: |
| iio_trigger_notify_done(indio_dev->trig); |
| return IRQ_HANDLED; |
| } |
| |
| static int scd4x_probe(struct i2c_client *client, const struct i2c_device_id *id) |
| { |
| static const unsigned long scd4x_scan_masks[] = { 0x07, 0x00 }; |
| struct device *dev = &client->dev; |
| struct iio_dev *indio_dev; |
| struct scd4x_state *state; |
| int ret; |
| |
| indio_dev = devm_iio_device_alloc(dev, sizeof(*state)); |
| if (!indio_dev) |
| return -ENOMEM; |
| |
| state = iio_priv(indio_dev); |
| mutex_init(&state->lock); |
| state->client = client; |
| crc8_populate_msb(scd4x_crc8_table, SCD4X_CRC8_POLYNOMIAL); |
| |
| indio_dev->info = &scd4x_info; |
| indio_dev->name = client->name; |
| indio_dev->channels = scd4x_channels; |
| indio_dev->num_channels = ARRAY_SIZE(scd4x_channels); |
| indio_dev->modes = INDIO_DIRECT_MODE; |
| indio_dev->available_scan_masks = scd4x_scan_masks; |
| |
| state->vdd = devm_regulator_get(dev, "vdd"); |
| if (IS_ERR(state->vdd)) |
| return dev_err_probe(dev, PTR_ERR(state->vdd), "failed to get regulator\n"); |
| |
| ret = regulator_enable(state->vdd); |
| if (ret) |
| return ret; |
| |
| ret = devm_add_action_or_reset(dev, scd4x_disable_regulator, state); |
| if (ret) |
| return ret; |
| |
| ret = scd4x_send_command(state, CMD_STOP_MEAS); |
| if (ret) { |
| dev_err(dev, "failed to stop measurement: %d\n", ret); |
| return ret; |
| } |
| |
| /* execution time */ |
| msleep_interruptible(500); |
| |
| ret = devm_iio_triggered_buffer_setup(dev, indio_dev, NULL, scd4x_trigger_handler, NULL); |
| if (ret) |
| return ret; |
| |
| ret = scd4x_send_command(state, CMD_START_MEAS); |
| if (ret) { |
| dev_err(dev, "failed to start measurement: %d\n", ret); |
| return ret; |
| } |
| |
| ret = devm_add_action_or_reset(dev, scd4x_stop_meas, state); |
| if (ret) |
| return ret; |
| |
| return devm_iio_device_register(dev, indio_dev); |
| } |
| |
| static const struct of_device_id scd4x_dt_ids[] = { |
| { .compatible = "sensirion,scd40" }, |
| { .compatible = "sensirion,scd41" }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(of, scd4x_dt_ids); |
| |
| static struct i2c_driver scd4x_i2c_driver = { |
| .driver = { |
| .name = KBUILD_MODNAME, |
| .of_match_table = scd4x_dt_ids, |
| .pm = &scd4x_pm_ops |
| }, |
| .probe = scd4x_probe, |
| }; |
| module_i2c_driver(scd4x_i2c_driver); |
| |
| MODULE_AUTHOR("Roan van Dijk <roan@protonic.nl>"); |
| MODULE_DESCRIPTION("Sensirion SCD4X carbon dioxide sensor core driver"); |
| MODULE_LICENSE("GPL v2"); |