drivers: add driver for Sensirion SPS30 particulate matter sensor

This commit is contained in:
Michel Rottleuthner 2020-02-17 11:02:37 +01:00
parent 410542df90
commit c4093c29a4
6 changed files with 642 additions and 0 deletions

View File

@ -596,6 +596,11 @@ ifneq (,$(filter soft_spi,$(USEMODULE)))
USEMODULE += xtimer
endif
ifneq (,$(filter sps30,$(USEMODULE)))
FEATURES_REQUIRED += periph_i2c
USEMODULE += checksum
endif
ifneq (,$(filter srf02,$(USEMODULE)))
FEATURES_REQUIRED += periph_i2c
USEMODULE += xtimer

View File

@ -308,6 +308,10 @@ ifneq (,$(filter soft_spi,$(USEMODULE)))
USEMODULE_INCLUDES += $(RIOTBASE)/drivers/soft_spi/include
endif
ifneq (,$(filter sps30,$(USEMODULE)))
USEMODULE_INCLUDES += $(RIOTBASE)/drivers/sps30/include
endif
ifneq (,$(filter srf04,$(USEMODULE)))
USEMODULE_INCLUDES += $(RIOTBASE)/drivers/srf04/include
endif

286
drivers/include/sps30.h Normal file
View File

@ -0,0 +1,286 @@
/*
* Copyright (C) 2020 HAW Hamburg
*
* This file is subject to the terms and conditions of the GNU Lesser General
* Public License v2.1. See the file LICENSE in the top level directory for more
* details.
*/
/**
* @defgroup drivers_sps30 Sensirion SPS30 Particulate Matter Sensor
* @ingroup drivers_sensors
*
* About
* =====
*
* This driver provides an interface for the Sensirion SPS30 Sensor.
* The Datasheet can be found [here](https://1n.pm/oqluM).
* For now only I2C mode is supported.
* I2C speed must be set to standard mode (100 kbit/s)
*
* Wiring
* ======
*
* In ASCII-land the connector side of the sensor would look like this:
*
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* +------------------------------------------------------------------------+
* | ____________________ |
* | __| |__ |
* | |__ (1) (2) (3) (4) (5) __| |
* | | | |
* | |____________________| |
* | |
* +------------[#]------------[#]------------[#]------------[#]------------+
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
*
* The numbers refer to following pin names:
*
* | Pin Nr. | SPS30 Signal Name | Connect to | Notes |
* |---------|-------------------|------------|-------------------------------|
* | Pin 1 | VDD | 5 V | should be within +-10 % |
* | Pin 2 | I2C_SDA / UART_RX | SDA* | config by SPS30_PARAM_I2C_DEV |
* | Pin 3 | I2C_SCL / UART_TR | SCL* | config by SPS30_PARAM_I2C_DEV |
* | Pin 4 | SEL | GND | |
* | Pin 5 | GND | GND | |
*
* *The SCL and SDA pins of the SPS30 sensor are open drain so they must be
* pulled high. Consider that internal pull resistors might be too weak.
* So using external 10 kOhm resistors is recommended for that.
*
* @{
*
* @file
* @brief Driver for the Sensirion SPS30 Particulate Matter Sensor
*
* @author Michel Rottleuthner <michel.rottleuthner@haw-hamburg.de>
*/
#ifndef SPS30_H
#define SPS30_H
#include <stdbool.h>
#include "periph/gpio.h"
#include "periph/i2c.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief SPS30 device parameters
*/
typedef struct {
i2c_t i2c_dev; /**< I2C dev the sensor is connected to */
} sps30_params_t;
/**
* @brief SPS30 device instance
*/
typedef struct {
sps30_params_t p; /**< parameters of the sensor device */
} sps30_t;
/**
* @brief Set of measured particulate matter values
*
* @warning Do not change the member order, as it will break the code that
* populates the values in #sps30_read_measurement()
*
*/
typedef struct {
float mc_pm1; /**< Mass concentration of PM 1.0 [µg/m^3] */
float mc_pm2_5; /**< Mass concentration of PM 2.5 [µg/m^3] */
float mc_pm4; /**< Mass concentration of PM 4.0 [µg/m^3] */
float mc_pm10; /**< Mass concentration of PM 10 [µg/m^3] */
float nc_pm0_5; /**< Number concentration of PM 0.5 [µg/m^3] */
float nc_pm1; /**< Number concentration of PM 1.0 [µg/m^3] */
float nc_pm2_5; /**< Number concentration of PM 2.5 [µg/m^3] */
float nc_pm4; /**< Number concentration of PM 4.0 [µg/m^3] */
float nc_pm10; /**< Number concentration of PM 10 [µg/m^3] */
float ps; /**< Typical particle size [µm] */
} sps30_data_t;
/**
* @brief SPS30 error codes (returned as negative values)
*/
typedef enum {
SPS30_OK = 0, /**< Everything went fine */
SPS30_CRC_ERROR, /**< The CRC check of received data failed */
SPS30_I2C_ERROR /**< Some I2C operation failed */
} sps30_error_code_t;
/**
* @brief Seconds the fan cleaning process takes in seconds
*/
#define SPS30_FAN_CLEAN_S (10U)
/**
* @brief Length of serial and article code string
*/
#define SPS30_SER_ART_LEN (32U)
/**
* @brief Default fan auto-clean interval in seconds (1 week)
*/
#define SPS30_DEFAULT_ACI_S (604800UL)
/**
* @brief Maximum number of automatic retries on communication errors
*
* @details If no delays happen between individual requests to the sensor, it
* may happen that the sensor is not yet ready to serve data.
* Handling this within the driver simplifies application code by
* omitting sleep handling or retries there.
* This value may be overwritten to 0 if more fine-grained feedback
* is required or even increased if the device is connected over
* suboptimal wiring.
*
*/
#ifndef SPS30_ERROR_RETRY
#define SPS30_ERROR_RETRY (500U)
#endif
/**
* @brief Initialize SPS30 sensor driver.
* On success the measurement mode will be active after calling.
*
* @param[out] dev Pointer to an SPS30 device handle
* @param[in] params Parameters for device initialization
*
* @return #SPS30_OK on success, negative #sps30_error_code_t on error
*/
int sps30_init(sps30_t *dev, const sps30_params_t *params);
/**
* @brief Enable the measurement action.
*
* @details Starting the measurement activates the fan of the sensor and
* increases the power consumption from below 8 mA to an average
* of 60 mA. When the fan is starting up the consumption can reach
* up to 80 mA for around 200 ms.
* The measurement mode will stay active until either the power is
* turned off, a stop is requested (@ref sps30_stop_measurement),
* or the device is reset (#sps30_reset).
*
* @param[in] dev Pointer to an SPS30 device handle
*
* @return #SPS30_OK on success, negative #sps30_error_code_t on error
*/
int sps30_start_measurement(const sps30_t *dev);
/**
* @brief Stops the measurement action.
*
* @details Stopping the measurement sets the device back to idle mode.
*
* @param[in] dev Pointer to an SPS30 device handle
*
* @return #SPS30_OK on success, negative #sps30_error_code_t on error
*/
int sps30_stop_measurement(const sps30_t *dev);
/**
* @brief Ask the device if a measurement is ready for reading.
*
* @param[in] dev Pointer to an SPS30 device handle
* @param[out] error Pre-allocated memory to return #sps30_error_code_t
* or NULL if not interested
*
* @return true if a new measurement is available
* @return false if no new measurement is available
*/
bool sps30_data_ready(const sps30_t *dev, int *error);
/**
* @brief Read a set of particulate matter measurements.
*
* @param[in] dev Pointer to an SPS30 device handle
* @param[out] data Pre-allocated memory to return measurements
*
* @return #SPS30_OK on success, negative #sps30_error_code_t on error
*/
int sps30_read_measurement(const sps30_t *dev, sps30_data_t *data);
/**
* @brief Read the fan auto-clean interval.
*
* @details The default value is 604800 seconds (1 week).
* See also @ref sps30_start_fan_clean.
*
* @param[in] dev Pointer to an SPS30 device handle
* @param[out] seconds Pre-allocated memory for returning the interval,
* 0 stands for disabled auto-clean
*
* @return #SPS30_OK on success, negative #sps30_error_code_t on error
*/
int sps30_read_ac_interval(const sps30_t *dev, uint32_t *seconds);
/**
* @brief Write the fan auto-clean interval.
*
* @details The new value will be effective immediately after writing but
* reading the updated value is only possible after resetting the
* sensor.
* This setting is persistent across resets and powerdowns. But if
* the sensor is powered off, the active time counter starts from
* zero again. If this is expected to happen, a fan cleaning cycle
* should be triggered manually at least once a week.
* See also @ref sps30_start_fan_clean.
*
* @param[in] dev Pointer to an SPS30 device handle
* @param[out] seconds The new interval in seconds, 0 for disable
*
* @return #SPS30_OK on success, negative #sps30_error_code_t on error
*/
int sps30_write_ac_interval(const sps30_t *dev, uint32_t seconds);
/**
* @brief Run a fan cleaning cycle manually.
*
* @details This will spin up the fan to maximum speed to blow out dust for
* for 10 seconds. No new measurement values are available until
* the cleaning process is finished.
*
* @param[in] dev Pointer to an SPS30 device handle
*
* @return #SPS30_OK on success, negative #sps30_error_code_t on error
*/
int sps30_start_fan_clean(const sps30_t *dev);
/**
* @brief Read the article code from the sensor as string.
*
* @param[in] dev Pointer to an SPS30 device handle
* @param[out] str Pre-allocated memory for returning the article code
* @param[in] len Length of the \p str buffer, must be 32
*
* @return #SPS30_OK on success, negative #sps30_error_code_t on error
*/
int sps30_read_article_code(const sps30_t *dev, char *str, size_t len);
/**
* @brief Read the serial number from the sensor as string.
*
* @param[in] dev Pointer to an SPS30 device handle
* @param[out] str Pre-allocated memory for returning the serial number
* @param[in] len Length of the \p str buffer, must be 32
*
* @return #SPS30_OK on success, negative #sps30_error_code_t on error
*/
int sps30_read_serial_number(const sps30_t *dev, char *str, size_t len);
/**
* @brief Reset the sensor.
*
* @param[in] dev Pointer to an SPS30 device handle
*
* @return #SPS30_OK on success, negative #sps30_error_code_t on error
*/
int sps30_reset(const sps30_t *dev);
#ifdef __cplusplus
}
#endif
#endif /* SPS30_H */
/** @} */

1
drivers/sps30/Makefile Normal file
View File

@ -0,0 +1 @@
include $(RIOTBASE)/Makefile.base

View File

@ -0,0 +1,61 @@
/*
* Copyright (C) 2020 HAW Hamburg
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup drivers_sps30
* @brief Default configuration for Sensirion SPS30 sensors devices
* @author Michel Rottleuthner <michel.rottleuthner@haw-hamburg.de>
* @file
* @{
*/
#ifndef SPS30_PARAMS_H
#define SPS30_PARAMS_H
#include "board.h"
#include "sps30.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @name SPS30 default configuration parameters
* @{
*/
#ifndef SPS30_PARAM_I2C_DEV
#define SPS30_PARAM_I2C_DEV (I2C_DEV(0))
#endif
#ifndef SPS30_PARAMS
#define SPS30_PARAMS { .i2c_dev = SPS30_PARAM_I2C_DEV }
#endif
#ifndef SPS30_SAUL_INFO
#define SPS30_SAUL_INFO { .name = "sps30" }
#endif
/**@}*/
/**
* @brief SPS30 configuration
*/
static const sps30_params_t sps30_params[] =
{
SPS30_PARAMS
};
/**
* @brief Define the number of configured sensors
*/
#define SPS30_NUM ARRAY_SIZE(sps30_params)
#ifdef __cplusplus
}
#endif
#endif /* SPS30_PARAMS_H */
/** @} */

285
drivers/sps30/sps30.c Normal file
View File

@ -0,0 +1,285 @@
/*
* Copyright (C) 2020 Michel Rottleuthner
*
* This file is subject to the terms and conditions of the GNU Lesser
* General Public License v2.1. See the file LICENSE in the top level
* directory for more details.
*/
/**
* @ingroup drivers_sps30
* @brief Device driver for the Sensirion SPS30 particulate matter sensor
* @author Michel Rottleuthner <michel.rottleuthner@haw-hamburg.de>
* @file
*/
#define LOG_LEVEL LOG_DEBUG
#include "log.h"
#include <errno.h>
#include <string.h>
#include "checksum/crc8.h"
#include "sps30.h"
#include "xtimer.h"
#include "byteorder.h"
#include "kernel_defines.h"
/**
* @name SPS30 literal definitions
* @{
*/
#define SPS30_I2C_ADDR (0x69) /**< Fixed I2C address for the Sensor */
#define SPS30_CRC_POLY (0x31) /**< Polynomial for the CRC calculation */
#define SPS30_CRC_INIT (0xFF) /**< Init value for the CRC calculation */
#define SPS30_MESAURE_MODE (0x03) /**< Fixed mode byte for start cmd */
#define SPS30_DUMMY_BYTE (0x00) /**< Fixed dummy byte for unused bytes */
#define SPS30_PTR_LEN (2) /**< Pointer address length in bytes */
/** @} */
/**
* @name Address pointer values for all SPS30 I2C commands
* @{
*/
typedef enum {
SPS30_CMD_START_MEASURE = 0x0010, /**< Start measurement mode */
SPS30_CMD_STOP_MEASURE = 0x0104, /**< Stop measurement mode */
SPS30_CMD_RD_DATA_READY = 0x0202, /**< Read data-ready flag */
SPS30_CMD_RD_MEASUREMENTS = 0x0300, /**< Read measured values */
SPS30_CMD_RW_AUTOCLEAN = 0x8004, /**< Read/write autoclean interval */
SPS30_CMD_START_FAN_CLEAN = 0x5607, /**< Start fan cleaning */
SPS30_CMD_RD_ARTICLE = 0xD025, /**< Read article code */
SPS30_CMD_RD_SERIAL = 0xD033, /**< Read serial number */
SPS30_CMD_RESET = 0xD304, /**< Reset */
} sps30_cmd_t;
/** @} */
/**
* @brief Combine payload and CRC into an SDS30 I2C data frame.
*
* @details The CRC for `data` is calculated and written to `crcd_data`
* together with the payload itself.
* The format of `data` must be just the pure payload:
* { data[0], data[1], data[2], ..., data[n]}
* The format of `crcd_data` same as it shall be sent to the SPS30.
* It consists of payload byte-pairs followed by a single CRC byte:
* { data[0], data[1], csum[0],.., data[n-1], data[n], csum[n-1] }
*
* @param[in] data Source buffer containing just the raw payload
* @param[in] len Size of the 'data' buffer
* @param[out] crcd_data Destination buffer for combined payload and CRCs
*
* @pre sizeof(crcd_data) must be equal to 1.5 * len
*
*/
static inline void _cpy_add_crc(uint8_t *data, size_t len, uint8_t *crcd_data)
{
for (size_t elem = 0; elem < len / 2; elem++) {
int idx = (elem << 1);
crcd_data[idx + elem] = data[idx];
crcd_data[idx + elem + 1] = data[idx + 1];
crcd_data[idx + elem + 2] = crc8(&data[idx], 2, SPS30_CRC_POLY,
SPS30_CRC_INIT);
}
}
/**
* @brief Check the CRC of an SDS30 I2C data frame.
*
* @details The CRC contained in the `crcd_data` is checked and the payload is
* copied to `data`.
* The format of `data` will be just the pure payload:
* { data[0], data[1], data[2], ..., data[n]}
* The format of `crcd_data` is same as it is read from the SPS30.
* It consists of payload byte-pairs followed by a single CRC byte:
* { data[0], data[1], csum[0],.., data[n-1], data[n], csum[n-1] }
*
* @pre sizeof(crcd_data) must be equal to 1.5 * len
*
* @param[out] data Destination buffer for just the raw payload
* @param[in] len Size of the 'data' buffer
* @param[in] crcd_data Source buffer containing combined payload and CRCs
*
* @return true if all CRCs are valid
* @return false if at least one CRC is invalid
*/
static inline bool _cpy_check_crc(uint8_t *data, size_t len, uint8_t *crcd_data)
{
for (size_t elem = 0; elem < len / 2; elem++) {
int idx = (elem << 1);
data[idx] = crcd_data[idx + elem];
data[idx + 1] = crcd_data[idx + elem + 1];
if (crc8(&data[idx], 2, SPS30_CRC_POLY, SPS30_CRC_INIT) !=
(crcd_data[idx + elem + 2])) {
return false;
}
}
return true;
}
/**
* @brief Communicates with an SPS30 device by reading or writing data.
*
* @details This performs all three data transfer types supported by SPS30.
* (1) `Set Pointer`: writes a 16 bit pointer address to the device
* (2) `Set Pointer & Read Data`: (1) followed by separate data-read
* (3) `Set Pointer & Write Data`: (1) combined with a data-write
*
* @param[in] dev Pointer to SPS30 device handle
* @param[in] ptr_addr 16 bit pointer address used as command
* @param[in/out] data Pre-allocated memory pointing to either the data
* that will be sent to the device or to memory that
* will hold the response. For type (1) transfers
* this parameter will be ignored.
* @param[in] len Length of `data` buffer, set to 0 for type (1)
* @param[in] read set to true for reading or false for writing
*
* @return SPS30_OK if everything went fine
* @return -SPS30_CRC_ERROR if the CRC check failed
* @return -SPS30_I2C_ERROR if the I2C communication failed
*/
static int _rx_tx_data(const sps30_t *dev, uint16_t ptr_addr,
uint8_t *data, size_t len, bool read)
{
int res = 0;
unsigned retr = SPS30_ERROR_RETRY;
if (i2c_acquire(dev->p.i2c_dev) != 0) {
LOG_ERROR("could not acquire I2C bus %d\n", dev->p.i2c_dev);
return -SPS30_I2C_ERROR;
}
do {
size_t addr_data_crc_len = SPS30_PTR_LEN + len + len / 2;
uint8_t frame_data[addr_data_crc_len];
frame_data[0] = ptr_addr >> 8;
frame_data[1] = ptr_addr & 0xFF;
/* Both transfer types, `Set Pointer` and `Set Pointer & Read Data`
require writing a pointer address to the device in a separate write
transaction */
if (len == 0 || read) {
res = i2c_write_bytes(dev->p.i2c_dev, SPS30_I2C_ADDR,
&frame_data[0], SPS30_PTR_LEN, 0);
}
if (res == 0 && read) {
/* The `Set Pointer & Read Data` transfer type requires a separate
read transaction to actually read the data */
res = i2c_read_bytes(dev->p.i2c_dev, SPS30_I2C_ADDR,
&frame_data[SPS30_PTR_LEN],
addr_data_crc_len - SPS30_PTR_LEN, 0);
if (!_cpy_check_crc(data, len, &frame_data[SPS30_PTR_LEN])) {
res = -SPS30_CRC_ERROR;
}
} else {
/* For the `Set Pointer & Write Data` transfer type the full frame
is transmitted as one single chunk */
_cpy_add_crc(data, len, &frame_data[SPS30_PTR_LEN]);
res = i2c_write_bytes(dev->p.i2c_dev, SPS30_I2C_ADDR,
&frame_data[0], addr_data_crc_len, 0);
}
} while (res != 0 && retr--);
i2c_release(dev->p.i2c_dev);
return res == 0 ? SPS30_OK : -SPS30_I2C_ERROR;
}
int sps30_init(sps30_t *dev, const sps30_params_t *params)
{
assert(dev && params);
dev->p = *params;
return sps30_start_measurement(dev);
}
int sps30_start_measurement(const sps30_t *dev)
{
assert(dev);
uint8_t data[] = {SPS30_MESAURE_MODE, SPS30_DUMMY_BYTE};
return _rx_tx_data(dev, SPS30_CMD_START_MEASURE, (uint8_t*)data,
sizeof(data), false);
}
int sps30_stop_measurement(const sps30_t *dev)
{
assert(dev);
return _rx_tx_data(dev, SPS30_CMD_STOP_MEASURE, NULL, 0, false);
}
bool sps30_data_ready(const sps30_t *dev, int *error)
{
assert(dev);
uint8_t data[2];
int res = _rx_tx_data(dev, SPS30_CMD_RD_DATA_READY, data, sizeof(data),
true);
if (*error) {
*error = res;
}
return (res == SPS30_OK) && data[1];
}
int sps30_read_measurement(const sps30_t *dev, sps30_data_t *data)
{
/* This compile time check is needed to ensure the below method used for
endianness conversion will work as expected */
BUILD_BUG_ON(sizeof(sps30_data_t) != (sizeof(float) * 10));
assert(dev && data);
/* The target buffer is also used for storing the raw data temporarily */
int res = _rx_tx_data(dev, SPS30_CMD_RD_MEASUREMENTS, (uint8_t*)data,
sizeof(sps30_data_t), true);
/* The sps30_data_t consists only of floats, so it is safe to treat it as
an array of 32 bit values for swapping to correct endianness */
uint32_t *values = (uint32_t*)data;
/* swap to the endianness of this platform */
for (unsigned i = 0; i < (sizeof(sps30_data_t) / sizeof(uint32_t)); i++) {
values[i] = ntohl(values[i]);
}
return res;
}
int sps30_read_ac_interval(const sps30_t *dev, uint32_t *seconds) {
assert(dev);
int res = _rx_tx_data(dev, SPS30_CMD_RW_AUTOCLEAN, (uint8_t*)seconds,
sizeof(uint32_t), true);
*seconds = ntohl(*seconds);
return res;
}
int sps30_write_ac_interval(const sps30_t *dev, uint32_t seconds)
{
assert(dev);
seconds = htonl(seconds);
int res = _rx_tx_data(dev, SPS30_CMD_RW_AUTOCLEAN, (uint8_t*)&seconds,
sizeof(uint32_t), false);
return res;
}
int sps30_start_fan_clean(const sps30_t *dev)
{
assert(dev);
return _rx_tx_data(dev, SPS30_CMD_START_FAN_CLEAN, NULL, 0, false);
}
int sps30_read_article_code(const sps30_t *dev, char *str, size_t len)
{
assert(dev && str && (len == SPS30_SER_ART_LEN));
return _rx_tx_data(dev, SPS30_CMD_RD_ARTICLE, (uint8_t*)str, len, true);
}
int sps30_read_serial_number(const sps30_t *dev, char *str, size_t len)
{
assert(dev && str && (len == SPS30_SER_ART_LEN));
return _rx_tx_data(dev, SPS30_CMD_RD_SERIAL, (uint8_t*)str, len, true);
}
int sps30_reset(const sps30_t *dev)
{
assert(dev);
return _rx_tx_data(dev, SPS30_CMD_RESET, NULL, 0, false);
}