// SPDX-License-Identifier: GPL-2.0 /* * Sensirion SPS30 particulate matter sensor i2c driver * * Copyright (c) 2020 Tomasz Duszynski * * I2C slave address: 0x69 */ #include #include #include #include #include #include #include #include #include #include "sps30.h" #define SPS30_I2C_CRC8_POLYNOMIAL 0x31 /* max number of bytes needed to store PM measurements or serial string */ #define SPS30_I2C_MAX_BUF_SIZE 48 DECLARE_CRC8_TABLE(sps30_i2c_crc8_table); #define SPS30_I2C_START_MEAS 0x0010 #define SPS30_I2C_STOP_MEAS 0x0104 #define SPS30_I2C_READ_MEAS 0x0300 #define SPS30_I2C_MEAS_READY 0x0202 #define SPS30_I2C_RESET 0xd304 #define SPS30_I2C_CLEAN_FAN 0x5607 #define SPS30_I2C_PERIOD 0x8004 #define SPS30_I2C_READ_SERIAL 0xd033 #define SPS30_I2C_READ_VERSION 0xd100 static int sps30_i2c_xfer(struct sps30_state *state, unsigned char *txbuf, size_t txsize, unsigned char *rxbuf, size_t rxsize) { struct i2c_client *client = to_i2c_client(state->dev); int ret; /* * Sensor does not support repeated start so instead of * sending two i2c messages in a row we just send one by one. */ ret = i2c_master_send(client, txbuf, txsize); if (ret < 0) return ret; if (ret != txsize) return -EIO; if (!rxsize) return 0; ret = i2c_master_recv(client, rxbuf, rxsize); if (ret < 0) return ret; if (ret != rxsize) return -EIO; return 0; } static int sps30_i2c_command(struct sps30_state *state, u16 cmd, void *arg, size_t arg_size, void *rsp, size_t rsp_size) { /* * Internally sensor stores measurements in a following manner: * * PM1: upper two bytes, crc8, lower two bytes, crc8 * PM2P5: upper two bytes, crc8, lower two bytes, crc8 * PM4: upper two bytes, crc8, lower two bytes, crc8 * PM10: upper two bytes, crc8, lower two bytes, crc8 * * What follows next are number concentration measurements and * typical particle size measurement which we omit. */ unsigned char buf[SPS30_I2C_MAX_BUF_SIZE]; unsigned char *tmp; unsigned char crc; size_t i; int ret; put_unaligned_be16(cmd, buf); i = 2; if (rsp) { /* each two bytes are followed by a crc8 */ rsp_size += rsp_size / 2; } else { tmp = arg; while (arg_size) { buf[i] = *tmp++; buf[i + 1] = *tmp++; buf[i + 2] = crc8(sps30_i2c_crc8_table, buf + i, 2, CRC8_INIT_VALUE); arg_size -= 2; i += 3; } } ret = sps30_i2c_xfer(state, buf, i, buf, rsp_size); if (ret) return ret; /* validate received data and strip off crc bytes */ tmp = rsp; for (i = 0; i < rsp_size; i += 3) { crc = crc8(sps30_i2c_crc8_table, buf + i, 2, CRC8_INIT_VALUE); if (crc != buf[i + 2]) { dev_err(state->dev, "data integrity check failed\n"); return -EIO; } *tmp++ = buf[i]; *tmp++ = buf[i + 1]; } return 0; } static int sps30_i2c_start_meas(struct sps30_state *state) { /* request BE IEEE754 formatted data */ unsigned char buf[] = { 0x03, 0x00 }; return sps30_i2c_command(state, SPS30_I2C_START_MEAS, buf, sizeof(buf), NULL, 0); } static int sps30_i2c_stop_meas(struct sps30_state *state) { return sps30_i2c_command(state, SPS30_I2C_STOP_MEAS, NULL, 0, NULL, 0); } static int sps30_i2c_reset(struct sps30_state *state) { int ret; ret = sps30_i2c_command(state, SPS30_I2C_RESET, NULL, 0, NULL, 0); msleep(500); /* * Power-on-reset causes sensor to produce some glitch on i2c bus and * some controllers end up in error state. Recover simply by placing * some data on the bus, for example STOP_MEAS command, which * is NOP in this case. */ sps30_i2c_stop_meas(state); return ret; } static bool sps30_i2c_meas_ready(struct sps30_state *state) { unsigned char buf[2]; int ret; ret = sps30_i2c_command(state, SPS30_I2C_MEAS_READY, NULL, 0, buf, sizeof(buf)); if (ret) return false; return buf[1]; } static int sps30_i2c_read_meas(struct sps30_state *state, __be32 *meas, size_t num) { /* measurements are ready within a second */ if (msleep_interruptible(1000)) return -EINTR; if (!sps30_i2c_meas_ready(state)) return -ETIMEDOUT; return sps30_i2c_command(state, SPS30_I2C_READ_MEAS, NULL, 0, meas, sizeof(num) * num); } static int sps30_i2c_clean_fan(struct sps30_state *state) { return sps30_i2c_command(state, SPS30_I2C_CLEAN_FAN, NULL, 0, NULL, 0); } static int sps30_i2c_read_cleaning_period(struct sps30_state *state, __be32 *period) { return sps30_i2c_command(state, SPS30_I2C_PERIOD, NULL, 0, period, sizeof(*period)); } static int sps30_i2c_write_cleaning_period(struct sps30_state *state, __be32 period) { return sps30_i2c_command(state, SPS30_I2C_PERIOD, &period, sizeof(period), NULL, 0); } static int sps30_i2c_show_info(struct sps30_state *state) { /* extra nul just in case */ unsigned char buf[32 + 1] = { 0x00 }; int ret; ret = sps30_i2c_command(state, SPS30_I2C_READ_SERIAL, NULL, 0, buf, sizeof(buf) - 1); if (ret) return ret; dev_info(state->dev, "serial number: %s\n", buf); ret = sps30_i2c_command(state, SPS30_I2C_READ_VERSION, NULL, 0, buf, 2); if (ret) return ret; dev_info(state->dev, "fw version: %u.%u\n", buf[0], buf[1]); return 0; } static const struct sps30_ops sps30_i2c_ops = { .start_meas = sps30_i2c_start_meas, .stop_meas = sps30_i2c_stop_meas, .read_meas = sps30_i2c_read_meas, .reset = sps30_i2c_reset, .clean_fan = sps30_i2c_clean_fan, .read_cleaning_period = sps30_i2c_read_cleaning_period, .write_cleaning_period = sps30_i2c_write_cleaning_period, .show_info = sps30_i2c_show_info, }; static int sps30_i2c_probe(struct i2c_client *client) { if (!i2c_check_functionality(client->adapter, I2C_FUNC_I2C)) return -EOPNOTSUPP; crc8_populate_msb(sps30_i2c_crc8_table, SPS30_I2C_CRC8_POLYNOMIAL); return sps30_probe(&client->dev, client->name, NULL, &sps30_i2c_ops); } static const struct i2c_device_id sps30_i2c_id[] = { { "sps30" }, { } }; MODULE_DEVICE_TABLE(i2c, sps30_i2c_id); static const struct of_device_id sps30_i2c_of_match[] = { { .compatible = "sensirion,sps30" }, { } }; MODULE_DEVICE_TABLE(of, sps30_i2c_of_match); static struct i2c_driver sps30_i2c_driver = { .driver = { .name = KBUILD_MODNAME, .of_match_table = sps30_i2c_of_match, }, .id_table = sps30_i2c_id, .probe_new = sps30_i2c_probe, }; module_i2c_driver(sps30_i2c_driver); MODULE_AUTHOR("Tomasz Duszynski "); MODULE_DESCRIPTION("Sensirion SPS30 particulate matter sensor i2c driver"); MODULE_LICENSE("GPL v2"); MODULE_IMPORT_NS(IIO_SPS30);