| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * Sensirion SCD30 carbon dioxide sensor serial driver |
| * |
| * Copyright (c) 2020 Tomasz Duszynski <tomasz.duszynski@octakon.com> |
| */ |
| #include <linux/crc16.h> |
| #include <linux/device.h> |
| #include <linux/errno.h> |
| #include <linux/iio/iio.h> |
| #include <linux/jiffies.h> |
| #include <linux/mod_devicetable.h> |
| #include <linux/module.h> |
| #include <linux/property.h> |
| #include <linux/serdev.h> |
| #include <linux/string.h> |
| #include <linux/types.h> |
| #include <asm/unaligned.h> |
| |
| #include "scd30.h" |
| |
| #define SCD30_SERDEV_ADDR 0x61 |
| #define SCD30_SERDEV_WRITE 0x06 |
| #define SCD30_SERDEV_READ 0x03 |
| #define SCD30_SERDEV_MAX_BUF_SIZE 17 |
| #define SCD30_SERDEV_RX_HEADER_SIZE 3 |
| #define SCD30_SERDEV_CRC_SIZE 2 |
| #define SCD30_SERDEV_TIMEOUT msecs_to_jiffies(200) |
| |
| struct scd30_serdev_priv { |
| struct completion meas_ready; |
| char *buf; |
| int num_expected; |
| int num; |
| }; |
| |
| static u16 scd30_serdev_cmd_lookup_tbl[] = { |
| [CMD_START_MEAS] = 0x0036, |
| [CMD_STOP_MEAS] = 0x0037, |
| [CMD_MEAS_INTERVAL] = 0x0025, |
| [CMD_MEAS_READY] = 0x0027, |
| [CMD_READ_MEAS] = 0x0028, |
| [CMD_ASC] = 0x003a, |
| [CMD_FRC] = 0x0039, |
| [CMD_TEMP_OFFSET] = 0x003b, |
| [CMD_FW_VERSION] = 0x0020, |
| [CMD_RESET] = 0x0034, |
| }; |
| |
| static u16 scd30_serdev_calc_crc(const char *buf, int size) |
| { |
| return crc16(0xffff, buf, size); |
| } |
| |
| static int scd30_serdev_xfer(struct scd30_state *state, char *txbuf, int txsize, |
| char *rxbuf, int rxsize) |
| { |
| struct serdev_device *serdev = to_serdev_device(state->dev); |
| struct scd30_serdev_priv *priv = state->priv; |
| int ret; |
| |
| priv->buf = rxbuf; |
| priv->num_expected = rxsize; |
| priv->num = 0; |
| |
| ret = serdev_device_write(serdev, txbuf, txsize, SCD30_SERDEV_TIMEOUT); |
| if (ret < 0) |
| return ret; |
| if (ret != txsize) |
| return -EIO; |
| |
| ret = wait_for_completion_interruptible_timeout(&priv->meas_ready, SCD30_SERDEV_TIMEOUT); |
| if (ret < 0) |
| return ret; |
| if (!ret) |
| return -ETIMEDOUT; |
| |
| return 0; |
| } |
| |
| static int scd30_serdev_command(struct scd30_state *state, enum scd30_cmd cmd, u16 arg, |
| void *response, int size) |
| { |
| /* |
| * Communication over serial line is based on modbus protocol (or rather |
| * its variation called modbus over serial to be precise). Upon |
| * receiving a request device should reply with response. |
| * |
| * Frame below represents a request message. Each field takes |
| * exactly one byte. |
| * |
| * +------+------+-----+-----+-------+-------+-----+-----+ |
| * | dev | op | reg | reg | byte1 | byte0 | crc | crc | |
| * | addr | code | msb | lsb | | | lsb | msb | |
| * +------+------+-----+-----+-------+-------+-----+-----+ |
| * |
| * The message device replies with depends on the 'op code' field from |
| * the request. In case it was set to SCD30_SERDEV_WRITE sensor should |
| * reply with unchanged request. Otherwise 'op code' was set to |
| * SCD30_SERDEV_READ and response looks like the one below. As with |
| * request, each field takes one byte. |
| * |
| * +------+------+--------+-------+-----+-------+-----+-----+ |
| * | dev | op | num of | byte0 | ... | byteN | crc | crc | |
| * | addr | code | bytes | | | | lsb | msb | |
| * +------+------+--------+-------+-----+-------+-----+-----+ |
| */ |
| char txbuf[SCD30_SERDEV_MAX_BUF_SIZE] = { SCD30_SERDEV_ADDR }, |
| rxbuf[SCD30_SERDEV_MAX_BUF_SIZE]; |
| int ret, rxsize, txsize = 2; |
| char *rsp = response; |
| u16 crc; |
| |
| put_unaligned_be16(scd30_serdev_cmd_lookup_tbl[cmd], txbuf + txsize); |
| txsize += 2; |
| |
| if (rsp) { |
| txbuf[1] = SCD30_SERDEV_READ; |
| if (cmd == CMD_READ_MEAS) |
| /* number of u16 words to read */ |
| put_unaligned_be16(size / 2, txbuf + txsize); |
| else |
| put_unaligned_be16(0x0001, txbuf + txsize); |
| txsize += 2; |
| crc = scd30_serdev_calc_crc(txbuf, txsize); |
| put_unaligned_le16(crc, txbuf + txsize); |
| txsize += 2; |
| rxsize = SCD30_SERDEV_RX_HEADER_SIZE + size + SCD30_SERDEV_CRC_SIZE; |
| } else { |
| if ((cmd == CMD_STOP_MEAS) || (cmd == CMD_RESET)) |
| arg = 0x0001; |
| |
| txbuf[1] = SCD30_SERDEV_WRITE; |
| put_unaligned_be16(arg, txbuf + txsize); |
| txsize += 2; |
| crc = scd30_serdev_calc_crc(txbuf, txsize); |
| put_unaligned_le16(crc, txbuf + txsize); |
| txsize += 2; |
| rxsize = txsize; |
| } |
| |
| ret = scd30_serdev_xfer(state, txbuf, txsize, rxbuf, rxsize); |
| if (ret) |
| return ret; |
| |
| switch (txbuf[1]) { |
| case SCD30_SERDEV_WRITE: |
| if (memcmp(txbuf, rxbuf, txsize)) { |
| dev_err(state->dev, "wrong message received\n"); |
| return -EIO; |
| } |
| break; |
| case SCD30_SERDEV_READ: |
| if (rxbuf[2] != (rxsize - SCD30_SERDEV_RX_HEADER_SIZE - SCD30_SERDEV_CRC_SIZE)) { |
| dev_err(state->dev, "received data size does not match header\n"); |
| return -EIO; |
| } |
| |
| rxsize -= SCD30_SERDEV_CRC_SIZE; |
| crc = get_unaligned_le16(rxbuf + rxsize); |
| if (crc != scd30_serdev_calc_crc(rxbuf, rxsize)) { |
| dev_err(state->dev, "data integrity check failed\n"); |
| return -EIO; |
| } |
| |
| rxsize -= SCD30_SERDEV_RX_HEADER_SIZE; |
| memcpy(rsp, rxbuf + SCD30_SERDEV_RX_HEADER_SIZE, rxsize); |
| break; |
| default: |
| dev_err(state->dev, "received unknown op code\n"); |
| return -EIO; |
| } |
| |
| return 0; |
| } |
| |
| static int scd30_serdev_receive_buf(struct serdev_device *serdev, |
| const unsigned char *buf, size_t size) |
| { |
| struct iio_dev *indio_dev = serdev_device_get_drvdata(serdev); |
| struct scd30_serdev_priv *priv; |
| struct scd30_state *state; |
| int num; |
| |
| if (!indio_dev) |
| return 0; |
| |
| state = iio_priv(indio_dev); |
| priv = state->priv; |
| |
| /* just in case sensor puts some unexpected bytes on the bus */ |
| if (!priv->buf) |
| return 0; |
| |
| if (priv->num + size >= priv->num_expected) |
| num = priv->num_expected - priv->num; |
| else |
| num = size; |
| |
| memcpy(priv->buf + priv->num, buf, num); |
| priv->num += num; |
| |
| if (priv->num == priv->num_expected) { |
| priv->buf = NULL; |
| complete(&priv->meas_ready); |
| } |
| |
| return num; |
| } |
| |
| static const struct serdev_device_ops scd30_serdev_ops = { |
| .receive_buf = scd30_serdev_receive_buf, |
| .write_wakeup = serdev_device_write_wakeup, |
| }; |
| |
| static int scd30_serdev_probe(struct serdev_device *serdev) |
| { |
| struct device *dev = &serdev->dev; |
| struct scd30_serdev_priv *priv; |
| int irq, ret; |
| |
| priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); |
| if (!priv) |
| return -ENOMEM; |
| |
| init_completion(&priv->meas_ready); |
| serdev_device_set_client_ops(serdev, &scd30_serdev_ops); |
| |
| ret = devm_serdev_device_open(dev, serdev); |
| if (ret) |
| return ret; |
| |
| serdev_device_set_baudrate(serdev, 19200); |
| serdev_device_set_flow_control(serdev, false); |
| |
| ret = serdev_device_set_parity(serdev, SERDEV_PARITY_NONE); |
| if (ret) |
| return ret; |
| |
| irq = fwnode_irq_get(dev_fwnode(dev), 0); |
| |
| return scd30_probe(dev, irq, KBUILD_MODNAME, priv, scd30_serdev_command); |
| } |
| |
| static const struct of_device_id scd30_serdev_of_match[] = { |
| { .compatible = "sensirion,scd30" }, |
| { } |
| }; |
| MODULE_DEVICE_TABLE(of, scd30_serdev_of_match); |
| |
| static struct serdev_device_driver scd30_serdev_driver = { |
| .driver = { |
| .name = KBUILD_MODNAME, |
| .of_match_table = scd30_serdev_of_match, |
| .pm = pm_sleep_ptr(&scd30_pm_ops), |
| }, |
| .probe = scd30_serdev_probe, |
| }; |
| module_serdev_device_driver(scd30_serdev_driver); |
| |
| MODULE_AUTHOR("Tomasz Duszynski <tomasz.duszynski@octakon.com>"); |
| MODULE_DESCRIPTION("Sensirion SCD30 carbon dioxide sensor serial driver"); |
| MODULE_LICENSE("GPL v2"); |
| MODULE_IMPORT_NS(IIO_SCD30); |