mirror of https://github.com/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.
431 lines
10 KiB
431 lines
10 KiB
// SPDX-License-Identifier: GPL-2.0 |
|
/* |
|
* Sensirion SPS30 particulate matter sensor serial driver |
|
* |
|
* Copyright (c) 2021 Tomasz Duszynski <[email protected]> |
|
*/ |
|
#include <linux/completion.h> |
|
#include <linux/device.h> |
|
#include <linux/errno.h> |
|
#include <linux/iio/iio.h> |
|
#include <linux/minmax.h> |
|
#include <linux/mod_devicetable.h> |
|
#include <linux/module.h> |
|
#include <linux/serdev.h> |
|
#include <linux/types.h> |
|
|
|
#include "sps30.h" |
|
|
|
#define SPS30_SERIAL_DEV_NAME "sps30" |
|
|
|
#define SPS30_SERIAL_SOF_EOF 0x7e |
|
#define SPS30_SERIAL_TIMEOUT msecs_to_jiffies(20) |
|
#define SPS30_SERIAL_MAX_BUF_SIZE 263 |
|
#define SPS30_SERIAL_ESCAPE_CHAR 0x7d |
|
|
|
#define SPS30_SERIAL_FRAME_MIN_SIZE 7 |
|
#define SPS30_SERIAL_FRAME_ADR_OFFSET 1 |
|
#define SPS30_SERIAL_FRAME_CMD_OFFSET 2 |
|
#define SPS30_SERIAL_FRAME_MOSI_LEN_OFFSET 3 |
|
#define SPS30_SERIAL_FRAME_MISO_STATE_OFFSET 3 |
|
#define SPS30_SERIAL_FRAME_MISO_LEN_OFFSET 4 |
|
#define SPS30_SERIAL_FRAME_MISO_DATA_OFFSET 5 |
|
|
|
#define SPS30_SERIAL_START_MEAS 0x00 |
|
#define SPS30_SERIAL_STOP_MEAS 0x01 |
|
#define SPS30_SERIAL_READ_MEAS 0x03 |
|
#define SPS30_SERIAL_RESET 0xd3 |
|
#define SPS30_SERIAL_CLEAN_FAN 0x56 |
|
#define SPS30_SERIAL_PERIOD 0x80 |
|
#define SPS30_SERIAL_DEV_INFO 0xd0 |
|
#define SPS30_SERIAL_READ_VERSION 0xd1 |
|
|
|
struct sps30_serial_priv { |
|
struct completion new_frame; |
|
unsigned char buf[SPS30_SERIAL_MAX_BUF_SIZE]; |
|
size_t num; |
|
bool escaped; |
|
bool done; |
|
}; |
|
|
|
static int sps30_serial_xfer(struct sps30_state *state, const unsigned char *buf, size_t size) |
|
{ |
|
struct serdev_device *serdev = to_serdev_device(state->dev); |
|
struct sps30_serial_priv *priv = state->priv; |
|
int ret; |
|
|
|
priv->num = 0; |
|
priv->escaped = false; |
|
priv->done = false; |
|
|
|
ret = serdev_device_write(serdev, buf, size, SPS30_SERIAL_TIMEOUT); |
|
if (ret < 0) |
|
return ret; |
|
if (ret != size) |
|
return -EIO; |
|
|
|
ret = wait_for_completion_interruptible_timeout(&priv->new_frame, SPS30_SERIAL_TIMEOUT); |
|
if (ret < 0) |
|
return ret; |
|
if (!ret) |
|
return -ETIMEDOUT; |
|
|
|
return 0; |
|
} |
|
|
|
static const struct { |
|
unsigned char byte; |
|
unsigned char byte2; |
|
} sps30_serial_bytes[] = { |
|
{ 0x11, 0x31 }, |
|
{ 0x13, 0x33 }, |
|
{ 0x7e, 0x5e }, |
|
{ 0x7d, 0x5d }, |
|
}; |
|
|
|
static int sps30_serial_put_byte(unsigned char *buf, unsigned char byte) |
|
{ |
|
int i; |
|
|
|
for (i = 0; i < ARRAY_SIZE(sps30_serial_bytes); i++) { |
|
if (sps30_serial_bytes[i].byte != byte) |
|
continue; |
|
|
|
buf[0] = SPS30_SERIAL_ESCAPE_CHAR; |
|
buf[1] = sps30_serial_bytes[i].byte2; |
|
|
|
return 2; |
|
} |
|
|
|
buf[0] = byte; |
|
|
|
return 1; |
|
} |
|
|
|
static char sps30_serial_get_byte(bool escaped, unsigned char byte2) |
|
{ |
|
int i; |
|
|
|
if (!escaped) |
|
return byte2; |
|
|
|
for (i = 0; i < ARRAY_SIZE(sps30_serial_bytes); i++) { |
|
if (sps30_serial_bytes[i].byte2 != byte2) |
|
continue; |
|
|
|
return sps30_serial_bytes[i].byte; |
|
} |
|
|
|
return 0; |
|
} |
|
|
|
static unsigned char sps30_serial_calc_chksum(const unsigned char *buf, size_t num) |
|
{ |
|
unsigned int chksum = 0; |
|
size_t i; |
|
|
|
for (i = 0; i < num; i++) |
|
chksum += buf[i]; |
|
|
|
return ~chksum; |
|
} |
|
|
|
static int sps30_serial_prep_frame(unsigned char *buf, unsigned char cmd, |
|
const unsigned char *arg, size_t arg_size) |
|
{ |
|
unsigned char chksum; |
|
int num = 0; |
|
size_t i; |
|
|
|
buf[num++] = SPS30_SERIAL_SOF_EOF; |
|
buf[num++] = 0; |
|
num += sps30_serial_put_byte(buf + num, cmd); |
|
num += sps30_serial_put_byte(buf + num, arg_size); |
|
|
|
for (i = 0; i < arg_size; i++) |
|
num += sps30_serial_put_byte(buf + num, arg[i]); |
|
|
|
/* SOF isn't checksummed */ |
|
chksum = sps30_serial_calc_chksum(buf + 1, num - 1); |
|
num += sps30_serial_put_byte(buf + num, chksum); |
|
buf[num++] = SPS30_SERIAL_SOF_EOF; |
|
|
|
return num; |
|
} |
|
|
|
static bool sps30_serial_frame_valid(struct sps30_state *state, const unsigned char *buf) |
|
{ |
|
struct sps30_serial_priv *priv = state->priv; |
|
unsigned char chksum; |
|
|
|
if ((priv->num < SPS30_SERIAL_FRAME_MIN_SIZE) || |
|
(priv->num != SPS30_SERIAL_FRAME_MIN_SIZE + |
|
priv->buf[SPS30_SERIAL_FRAME_MISO_LEN_OFFSET])) { |
|
dev_err(state->dev, "frame has invalid number of bytes\n"); |
|
return false; |
|
} |
|
|
|
if ((priv->buf[SPS30_SERIAL_FRAME_ADR_OFFSET] != buf[SPS30_SERIAL_FRAME_ADR_OFFSET]) || |
|
(priv->buf[SPS30_SERIAL_FRAME_CMD_OFFSET] != buf[SPS30_SERIAL_FRAME_CMD_OFFSET])) { |
|
dev_err(state->dev, "frame has wrong ADR and CMD bytes\n"); |
|
return false; |
|
} |
|
|
|
if (priv->buf[SPS30_SERIAL_FRAME_MISO_STATE_OFFSET]) { |
|
dev_err(state->dev, "frame with non-zero state received (0x%02x)\n", |
|
priv->buf[SPS30_SERIAL_FRAME_MISO_STATE_OFFSET]); |
|
return false; |
|
} |
|
|
|
/* SOF, checksum and EOF are not checksummed */ |
|
chksum = sps30_serial_calc_chksum(priv->buf + 1, priv->num - 3); |
|
if (priv->buf[priv->num - 2] != chksum) { |
|
dev_err(state->dev, "frame integrity check failed\n"); |
|
return false; |
|
} |
|
|
|
return true; |
|
} |
|
|
|
static int sps30_serial_command(struct sps30_state *state, unsigned char cmd, |
|
const void *arg, size_t arg_size, void *rsp, size_t rsp_size) |
|
{ |
|
struct sps30_serial_priv *priv = state->priv; |
|
unsigned char buf[SPS30_SERIAL_MAX_BUF_SIZE]; |
|
int ret, size; |
|
|
|
size = sps30_serial_prep_frame(buf, cmd, arg, arg_size); |
|
ret = sps30_serial_xfer(state, buf, size); |
|
if (ret) |
|
return ret; |
|
|
|
if (!sps30_serial_frame_valid(state, buf)) |
|
return -EIO; |
|
|
|
if (rsp) { |
|
rsp_size = min_t(size_t, priv->buf[SPS30_SERIAL_FRAME_MISO_LEN_OFFSET], rsp_size); |
|
memcpy(rsp, &priv->buf[SPS30_SERIAL_FRAME_MISO_DATA_OFFSET], rsp_size); |
|
} |
|
|
|
return rsp_size; |
|
} |
|
|
|
static int sps30_serial_receive_buf(struct serdev_device *serdev, |
|
const unsigned char *buf, size_t size) |
|
{ |
|
struct iio_dev *indio_dev = dev_get_drvdata(&serdev->dev); |
|
struct sps30_serial_priv *priv; |
|
struct sps30_state *state; |
|
unsigned char byte; |
|
size_t i; |
|
|
|
if (!indio_dev) |
|
return 0; |
|
|
|
state = iio_priv(indio_dev); |
|
priv = state->priv; |
|
|
|
/* just in case device put some unexpected data on the bus */ |
|
if (priv->done) |
|
return size; |
|
|
|
/* wait for the start of frame */ |
|
if (!priv->num && size && buf[0] != SPS30_SERIAL_SOF_EOF) |
|
return 1; |
|
|
|
if (priv->num + size >= ARRAY_SIZE(priv->buf)) |
|
size = ARRAY_SIZE(priv->buf) - priv->num; |
|
|
|
for (i = 0; i < size; i++) { |
|
byte = buf[i]; |
|
/* remove stuffed bytes on-the-fly */ |
|
if (byte == SPS30_SERIAL_ESCAPE_CHAR) { |
|
priv->escaped = true; |
|
continue; |
|
} |
|
|
|
byte = sps30_serial_get_byte(priv->escaped, byte); |
|
if (priv->escaped && !byte) |
|
dev_warn(state->dev, "unrecognized escaped char (0x%02x)\n", byte); |
|
|
|
priv->buf[priv->num++] = byte; |
|
|
|
/* EOF received */ |
|
if (!priv->escaped && byte == SPS30_SERIAL_SOF_EOF) { |
|
if (priv->num < SPS30_SERIAL_FRAME_MIN_SIZE) |
|
continue; |
|
|
|
priv->done = true; |
|
complete(&priv->new_frame); |
|
i++; |
|
break; |
|
} |
|
|
|
priv->escaped = false; |
|
} |
|
|
|
return i; |
|
} |
|
|
|
static const struct serdev_device_ops sps30_serial_device_ops = { |
|
.receive_buf = sps30_serial_receive_buf, |
|
.write_wakeup = serdev_device_write_wakeup, |
|
}; |
|
|
|
static int sps30_serial_start_meas(struct sps30_state *state) |
|
{ |
|
/* request BE IEEE754 formatted data */ |
|
unsigned char buf[] = { 0x01, 0x03 }; |
|
|
|
return sps30_serial_command(state, SPS30_SERIAL_START_MEAS, buf, sizeof(buf), NULL, 0); |
|
} |
|
|
|
static int sps30_serial_stop_meas(struct sps30_state *state) |
|
{ |
|
return sps30_serial_command(state, SPS30_SERIAL_STOP_MEAS, NULL, 0, NULL, 0); |
|
} |
|
|
|
static int sps30_serial_reset(struct sps30_state *state) |
|
{ |
|
int ret; |
|
|
|
ret = sps30_serial_command(state, SPS30_SERIAL_RESET, NULL, 0, NULL, 0); |
|
msleep(500); |
|
|
|
return ret; |
|
} |
|
|
|
static int sps30_serial_read_meas(struct sps30_state *state, __be32 *meas, size_t num) |
|
{ |
|
int ret; |
|
|
|
/* measurements are ready within a second */ |
|
if (msleep_interruptible(1000)) |
|
return -EINTR; |
|
|
|
ret = sps30_serial_command(state, SPS30_SERIAL_READ_MEAS, NULL, 0, meas, num * sizeof(num)); |
|
if (ret < 0) |
|
return ret; |
|
/* if measurements aren't ready sensor returns empty frame */ |
|
if (ret == SPS30_SERIAL_FRAME_MIN_SIZE) |
|
return -ETIMEDOUT; |
|
if (ret != num * sizeof(*meas)) |
|
return -EIO; |
|
|
|
return 0; |
|
} |
|
|
|
static int sps30_serial_clean_fan(struct sps30_state *state) |
|
{ |
|
return sps30_serial_command(state, SPS30_SERIAL_CLEAN_FAN, NULL, 0, NULL, 0); |
|
} |
|
|
|
static int sps30_serial_read_cleaning_period(struct sps30_state *state, __be32 *period) |
|
{ |
|
unsigned char buf[] = { 0x00 }; |
|
int ret; |
|
|
|
ret = sps30_serial_command(state, SPS30_SERIAL_PERIOD, buf, sizeof(buf), |
|
period, sizeof(*period)); |
|
if (ret < 0) |
|
return ret; |
|
if (ret != sizeof(*period)) |
|
return -EIO; |
|
|
|
return 0; |
|
} |
|
|
|
static int sps30_serial_write_cleaning_period(struct sps30_state *state, __be32 period) |
|
{ |
|
unsigned char buf[5] = { 0x00 }; |
|
|
|
memcpy(buf + 1, &period, sizeof(period)); |
|
|
|
return sps30_serial_command(state, SPS30_SERIAL_PERIOD, buf, sizeof(buf), NULL, 0); |
|
} |
|
|
|
static int sps30_serial_show_info(struct sps30_state *state) |
|
{ |
|
/* |
|
* tell device do return serial number and add extra nul byte just in case |
|
* serial number isn't a valid string |
|
*/ |
|
unsigned char buf[32 + 1] = { 0x03 }; |
|
struct device *dev = state->dev; |
|
int ret; |
|
|
|
ret = sps30_serial_command(state, SPS30_SERIAL_DEV_INFO, buf, 1, buf, sizeof(buf) - 1); |
|
if (ret < 0) |
|
return ret; |
|
if (ret != sizeof(buf) - 1) |
|
return -EIO; |
|
|
|
dev_info(dev, "serial number: %s\n", buf); |
|
|
|
ret = sps30_serial_command(state, SPS30_SERIAL_READ_VERSION, NULL, 0, buf, sizeof(buf) - 1); |
|
if (ret < 0) |
|
return ret; |
|
if (ret < 2) |
|
return -EIO; |
|
|
|
dev_info(dev, "fw version: %u.%u\n", buf[0], buf[1]); |
|
|
|
return 0; |
|
} |
|
|
|
static const struct sps30_ops sps30_serial_ops = { |
|
.start_meas = sps30_serial_start_meas, |
|
.stop_meas = sps30_serial_stop_meas, |
|
.read_meas = sps30_serial_read_meas, |
|
.reset = sps30_serial_reset, |
|
.clean_fan = sps30_serial_clean_fan, |
|
.read_cleaning_period = sps30_serial_read_cleaning_period, |
|
.write_cleaning_period = sps30_serial_write_cleaning_period, |
|
.show_info = sps30_serial_show_info, |
|
}; |
|
|
|
static int sps30_serial_probe(struct serdev_device *serdev) |
|
{ |
|
struct device *dev = &serdev->dev; |
|
struct sps30_serial_priv *priv; |
|
int ret; |
|
|
|
priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); |
|
if (!priv) |
|
return -ENOMEM; |
|
|
|
init_completion(&priv->new_frame); |
|
serdev_device_set_client_ops(serdev, &sps30_serial_device_ops); |
|
|
|
ret = devm_serdev_device_open(dev, serdev); |
|
if (ret) |
|
return ret; |
|
|
|
serdev_device_set_baudrate(serdev, 115200); |
|
serdev_device_set_flow_control(serdev, false); |
|
|
|
ret = serdev_device_set_parity(serdev, SERDEV_PARITY_NONE); |
|
if (ret) |
|
return ret; |
|
|
|
return sps30_probe(dev, SPS30_SERIAL_DEV_NAME, priv, &sps30_serial_ops); |
|
} |
|
|
|
static const struct of_device_id sps30_serial_of_match[] = { |
|
{ .compatible = "sensirion,sps30" }, |
|
{ } |
|
}; |
|
MODULE_DEVICE_TABLE(of, sps30_serial_of_match); |
|
|
|
static struct serdev_device_driver sps30_serial_driver = { |
|
.driver = { |
|
.name = KBUILD_MODNAME, |
|
.of_match_table = sps30_serial_of_match, |
|
}, |
|
.probe = sps30_serial_probe, |
|
}; |
|
module_serdev_device_driver(sps30_serial_driver); |
|
|
|
MODULE_AUTHOR("Tomasz Duszynski <[email protected]>"); |
|
MODULE_DESCRIPTION("Sensirion SPS30 particulate matter sensor serial driver"); |
|
MODULE_LICENSE("GPL v2");
|
|
|