// SPDX-License-Identifier: GPL-2.0 /* * Sensirion SCD4X carbon dioxide sensor i2c driver * * Copyright (C) 2021 Protonic Holland * Author: Roan van Dijk * * I2C slave address: 0x62 * * Datasheets: * https://www.sensirion.com/file/datasheet_scd4x */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 sysfs_emit(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 (ret) return ret; if (val == 0xff) { dev_err(dev, "forced calibration has failed"); return -EINVAL; } return 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 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 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 DEFINE_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 = pm_sleep_ptr(&scd4x_pm_ops), }, .probe = scd4x_probe, }; module_i2c_driver(scd4x_i2c_driver); MODULE_AUTHOR("Roan van Dijk "); MODULE_DESCRIPTION("Sensirion SCD4X carbon dioxide sensor core driver"); MODULE_LICENSE("GPL v2");