diff --git a/drivers/Makefile.dep b/drivers/Makefile.dep index fc00e9f0b1a24b4efc95e5ffac8a7dcb822573e3..3f6df3479e472910d88029f86e018f0243236cd9 100644 --- a/drivers/Makefile.dep +++ b/drivers/Makefile.dep @@ -394,6 +394,10 @@ ifneq (,$(filter sdcard_spi,$(USEMODULE))) USEMODULE += xtimer endif +ifneq (,$(filter sds011,$(USEMODULE))) + FEATURES_REQUIRED += periph_uart +endif + ifneq (,$(filter servo,$(USEMODULE))) FEATURES_REQUIRED += periph_pwm endif diff --git a/drivers/Makefile.include b/drivers/Makefile.include index d4ab792e104fecc4b30ff98eb0a87bf37e49ba7b..936ca29a31e50ed24d615bed677abdf238cd9a3f 100644 --- a/drivers/Makefile.include +++ b/drivers/Makefile.include @@ -285,3 +285,7 @@ endif ifneq (,$(filter xbee,$(USEMODULE))) USEMODULE_INCLUDES += $(RIOTBASE)/drivers/xbee/include endif + +ifneq (,$(filter sds011,$(USEMODULE))) + USEMODULE_INCLUDES += $(RIOTBASE)/drivers/sds011/include +endif diff --git a/drivers/include/saul.h b/drivers/include/saul.h index 7acb21046a56d11211293aedb2e3d97ca473e5ca..17da75990ec16e9f32338f309804db0822619cfe 100644 --- a/drivers/include/saul.h +++ b/drivers/include/saul.h @@ -101,6 +101,7 @@ enum { SAUL_SENSE_RSSI = 0x93, /**< sensor: RSSI */ SAUL_SENSE_CHARGE = 0x94, /**< sensor: coulomb counter */ SAUL_SENSE_CURRENT = 0x95, /**< sensor: ammeter */ + SAUL_SENSE_PM = 0x96, /**< sensor: particulate matter */ SAUL_CLASS_ANY = 0xff /**< any device - wildcard */ /* extend this list as needed... */ }; diff --git a/drivers/include/sds011.h b/drivers/include/sds011.h new file mode 100644 index 0000000000000000000000000000000000000000..2aca1a57a8bfec8a68638b1a63e2359b9ced5569 --- /dev/null +++ b/drivers/include/sds011.h @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2018 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_sds011 SDS011 Laser Dust Sensor + * @ingroup drivers_sensors + * @brief Driver SDS011 Laser Dust Sensor + * @{ + * + * @file + * @brief Interface for controlling SDS011 Laser Dust Sensor + * + * @author Michel Rottleuthner <michel.rottleuthner@haw-hamburg.de> + */ + +#ifndef SDS011_H +#define SDS011_H + +#include <stdbool.h> + +#include "periph/gpio.h" +#include "periph/uart.h" +#include "mutex.h" +#include "sds011_internal.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief SDS011 wildcard address to address all devices + */ +#define SDS011_DEVID_WILDCARD (0xFFFF) + +/** + * @brief Named return values + */ +enum { + SDS011_OK, /**< all good */ + SDS011_INVALID_RESPONSE, /**< invalid response */ + SDS011_INVALID_CHKSUM, /**< invalid checksum */ + SDS011_ERROR, /**< internal error */ +}; + +/** + * @brief Report mode of the SDS011 sensor + */ +typedef enum sds011_reporting_mode { + SDS011_RMODE_ACTIVE = 0, /**< continuously reporting values */ + SDS011_RMODE_QUERY = 1, /**< sensor needs to be queried */ +} sds011_reporting_mode_t; + +/** + * @brief Work/sleep mode of the SDS011 sensor + */ +typedef enum sds011_working_mode { + SDS011_WMODE_SLEEP = 0, /**< laser & fan are disabled */ + SDS011_WMODE_WORK = 1, /**< laser & fan are enabled */ +} sds011_working_mode_t; + +/** + * @brief Configuration parameters for SDS011 Laser Dust Sensor + */ +typedef struct { + uart_t uart; /**< UART device the sensor is connected to */ + gpio_t pwr_pin; /**< GPIO pin for disabling supply voltage for the sensor */ + uint16_t dev_id; /**< Unique sensor device ID */ + bool pwr_ah; /**< Logic level of the power pin (true for active high) */ +} sds011_params_t; + +/** + * @brief Data type for storing SDS011 sensor readings + */ +typedef struct { + uint16_t pm_2_5; /**< Particulate Matter 2.5 concentration [0.1µg/m^3] */ + uint16_t pm_10; /**< Particulate Matter 10 concentration [0.1µg/m^3] */ +} sds011_data_t; + +/** + * @brief callback for measurements actively reported by the SDS011 sensor + */ +typedef void (*sds011_callback_t)(sds011_data_t *data, void *ctx); + +/** + * @brief Device descriptor definition for SDS011 Laser Dust Sensor + */ +typedef struct { + sds011_params_t params; /**< parameters for SDS011 device */ + mutex_t dev_lock; /**< mutex to synchronize device access */ + mutex_t cb_lock; /**< mutex to synchronize callbacks */ + sds011_callback_t cb; /**< callback deliver values async */ + void *cbctx; /**< user context for the callback */ + uint16_t checksum; /**< iteratively calculated checksum */ + uint8_t rx_mem[SDS011_FRAME_RECV_LEN]; /**< receive buffer */ + uint8_t pos; /**< receive buffer position counter */ +} sds011_t; + +/** + * @brief Initialize SDS011 Laser Dust Sensor + * + * @param[out] dev device descriptor + * @param[in] params device configuration + * + * @pre @p dev != NULL + * @pre @p params != NULL + * + * @return SDS011_OK on success + * @return SDS011_ERROR on error + */ +int sds011_init(sds011_t *dev, const sds011_params_t *params); + +/** + * @brief Enable power supply of SDS011 laser dust sensor + * + * @param[in] dev device descriptor + * + * @pre @p dev != NULL + */ +void sds011_power_on(const sds011_t *dev); + +/** + * @brief Disable power supply of SDS011 laser dust sensor + * + * @param[in] dev device descriptor + * + * @pre @p dev != NULL + */ +void sds011_power_off(const sds011_t *dev); + +/** + * @brief Read measurement values from SDS011 laser dust sensor + * + * @param[in] dev device descriptor + * @param[out] data pointer for storing the values + * + * @return SDS011_OK on success + * @return SDS011_INVALID_RESPONSE when response doesn't match the request + * @return SDS011_INVALID_CHKSUM when response checksum is invalid + * @return SDS011_ERROR when other error occured + * + * @pre @p dev != NULL + * @pre @p data != NULL + */ +int sds011_read(sds011_t *dev, sds011_data_t *data); + +/** + * @brief Register measurement callback + * + * The registered callback is executed when new measurements were + * received by the sensor. This function should be used together with + * active reporting mode of the sensor that automatically sends new + * measurements periodically (factory default setting of the sensor). + * + * @param[in] dev device descriptor + * @param[in] cb function to be called for new values (NULL for disable) + * @param[in] ctx context pointer that will be handed to the callback + * + * @return SDS011_OK on success + * @return SDS011_ERROR when error occured + * + * @pre @p dev != NULL + */ +int sds011_register_callback(sds011_t *dev, sds011_callback_t cb, void *ctx); + +/** + * @brief Get the current reporting mode of the sensor + * + * @param[in] dev device descriptor + * @param[out] mode SDS011_RMODE_ACTIVE: continuously report new values + * SDS011_RMODE_QUERY: new values need to be requested + * + * @return SDS011_OK on success + * @return SDS011_INVALID_RESPONSE when response doesn't match the request + * @return SDS011_INVALID_CHKSUM when response checksum is invalid + * @return SDS011_ERROR when other error occured + * + * @pre @p dev != NULL + */ +int sds011_get_reporting_mode(sds011_t *dev, sds011_reporting_mode_t *mode); + +/** + * @brief Set the reporting mode of the sensor + * + * @param[in] dev device descriptor + * @param[in] mode SDS011_RMODE_ACTIVE: continuously report new values + * SDS011_RMODE_QUERY: new values need to be requested + * + * @note This setting is persistent even after a full power-cycle! + * Factory default is SDS011_RMODE_ACTIVE + * + * @return SDS011_OK on success + * @return SDS011_INVALID_RESPONSE when response doesn't match the request + * @return SDS011_INVALID_CHKSUM when response checksum is invalid + * @return SDS011_ERROR when other error occured + * + * @pre @p dev != NULL + */ +int sds011_set_reporting_mode(sds011_t *dev, sds011_reporting_mode_t mode); + +/** + * @brief Get current working mode of the sensor + * + * @param[in] dev device descriptor + * @param[out] mode SDS011_WMODE_SLEEP: sensor is in sleep mode (~3 mA) + * SDS011_WMODE_WORK: sensor is in working mode (~65 mA) + * + * @return SDS011_OK on success + * @return SDS011_INVALID_RESPONSE when response doesn't match the request + * @return SDS011_INVALID_CHKSUM when response checksum is invalid + * @return SDS011_ERROR when other error occured + * + * @pre @p dev != NULL + */ +int sds011_get_working_mode(sds011_t *dev, sds011_working_mode_t *mode); + +/** + * @brief Set working mode of the sensor + * + * @param[in] dev device descriptor + * @param[in] mode SDS011_WMODE_SLEEP: put to sleep mode (~3 mA) + * SDS011_WMODE_WORK: put to working mode (~65 mA) + * + * @return SDS011_OK on success + * @return SDS011_INVALID_RESPONSE when response doesn't match the request + * @return SDS011_INVALID_CHKSUM when response checksum is invalid + * @return SDS011_ERROR when other error occured + * + * @pre @p dev != NULL + */ +int sds011_set_working_mode(sds011_t *dev, sds011_working_mode_t mode); + +/** + * @brief Get current working period of the sensor + * + * @param[in] dev device descriptor + * @param[out] minutes working period of the sensor in minutes + * + * @return SDS011_OK on success + * @return SDS011_INVALID_RESPONSE when response doesn't match the request + * @return SDS011_INVALID_CHKSUM when response checksum is invalid + * @return SDS011_ERROR when other error occured + * + * @pre @p dev != NULL + */ +int sds011_get_working_period(sds011_t *dev, uint8_t *minutes); + +/** + * @brief Set working period of the sensor + * + * @param[in] dev device descriptor + * @param[in] minutes 0 - 30 new working period of the sensor in minutes + * 0 for coninuous reporting mode + * 1 - 30 for a period of @p minutes + * + * @note For values greater than 0, the active duration (fan, laser enabled) + * is always fixed to 30 seconds, while the sleep duration is adjusted + * to give an overall period of @p minutes. + * + * @return SDS011_OK on success + * @return SDS011_INVALID_RESPONSE when response doesn't match the request + * @return SDS011_INVALID_CHKSUM when response checksum is invalid + * @return SDS011_ERROR when other error occured + * + * @pre @p dev != NULL + */ +int sds011_set_working_period(sds011_t *dev, uint8_t minutes); + +/** + * @brief Get firmware version of the sensor + * + * @param[in] dev device descriptor + * @param[out] year year of the firmware version + * @param[out] mon month of the firmware version + * @param[out] day day of the firmware version + * + * @return SDS011_OK on success + * @return SDS011_INVALID_RESPONSE when response doesn't match the request + * @return SDS011_INVALID_CHKSUM when response checksum is invalid + * @return SDS011_ERROR when other error occured + * + * @pre @p dev != NULL + * @pre @p year != NULL + * @pre @p mon != NULL + * @pre @p day != NULL + */ +int sds011_get_fw_version(sds011_t *dev, uint8_t *year, uint8_t *mon, uint8_t *day); + +/** + * @brief Set device ID of the sensor + * + * @param[in] dev device descriptor + * @param[in] sens_dev_id ID as one number (ID byte 1 MSB, ID byte 2 LSB) + * + * @note This setting is persistent even after a full power-cycle! + * Factory default is an individual ID which is printed next to the + * serial number barcode. For the number xxxx-abab the ID is 0xabab. + * + * @return SDS011_OK on success + * @return SDS011_INVALID_RESPONSE when response doesn't match the request + * @return SDS011_INVALID_CHKSUM when response checksum is invalid + * @return SDS011_ERROR when other error occured + * + * @pre @p dev != NULL + */ +int sds011_set_dev_id(sds011_t *dev, uint16_t sens_dev_id); + +#ifdef __cplusplus +} +#endif + +#endif /* SDS011_H */ +/** @} */ diff --git a/drivers/saul/saul_str.c b/drivers/saul/saul_str.c index c8e1e85b46d310a76df863273251a0a3e9552035..dbbb487985092b7e193750bd120e28f445b05bae 100644 --- a/drivers/saul/saul_str.c +++ b/drivers/saul/saul_str.c @@ -57,8 +57,9 @@ const char *saul_class_to_str(const uint8_t class_id) case SAUL_SENSE_RSSI: return "SENSE_RSSI"; case SAUL_SENSE_CHARGE: return "SENSE_CHARGE"; case SAUL_SENSE_CURRENT: return "SENSE_CURRENT"; - case SAUL_CLASS_ANY: return "CLASS_ANY"; case SAUL_SENSE_OCCUP: return "SENSE_OCCUP"; + case SAUL_SENSE_PM: return "SENSE_PM"; + case SAUL_CLASS_ANY: return "CLASS_ANY"; default: return "CLASS_UNKNOWN"; } } diff --git a/drivers/sds011/Makefile b/drivers/sds011/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..48422e909a47d7cd428d10fa73825060ccc8d8c2 --- /dev/null +++ b/drivers/sds011/Makefile @@ -0,0 +1 @@ +include $(RIOTBASE)/Makefile.base diff --git a/drivers/sds011/include/sds011_internal.h b/drivers/sds011/include/sds011_internal.h new file mode 100644 index 0000000000000000000000000000000000000000..e41fca0f1fa978ac04080e3bbafbf83e2720016c --- /dev/null +++ b/drivers/sds011/include/sds011_internal.h @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2018 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_sds011 + * @{ + * + * @file + * @brief Internal constants etc. for the SDS011 laser dust sensor + * + * @author Michel Rottleuthner <michel.rottleuthner@haw-hamburg.de> + * @} + */ +#ifndef SDS011_INTERNAL_H +#define SDS011_INTERNAL_H + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief SDS011 baud rate + */ +#define SDS011_UART_BAUDRATE (9600U) + + /** + * @name SDS011 frame lengths + * @{ + */ +#define SDS011_FRAME_SEND_LEN (19U) +#define SDS011_FRAME_RECV_LEN (10U) +/** @} */ + +/** + * @name SDS011 command values + * @{ + */ +#define SDS011_CMD_DB1_SET_DR_MODE (2U) +#define SDS011_CMD_DB1_QUERY_DATA (4U) +#define SDS011_CMD_DB1_SET_DEV_ID (5U) +#define SDS011_CMD_DB1_SET_SLEEP_WORK (6U) +#define SDS011_CMD_DB1_CHECK_FIRMWARE (7U) +#define SDS011_CMD_DB1_SET_WORK_PERIOD (8U) +/** @} */ + +/** + * @name SDS011 command option values + * @{ + */ +#define SDS011_CMD_OPT_QUERY (0U) +#define SDS011_CMD_OPT_SET (1U) +#define SDS011_CMD_OPT_REPORT_ACTIVE (0U) +#define SDS011_CMD_OPT_REPORT_QUERY (1U) +#define SDS011_CMD_OPT_SLEEP (0U) +#define SDS011_CMD_OPT_WORK (1U) +/** @} */ + +/** + * @name SDS011 frame constants + * @{ + */ +#define SDS011_CMDID_QUERY (0xB4) +#define SDS011_RCMDID_REPLY (0xC5) +#define SDS011_RCMDID_DATA (0xC0) +#define SDS011_FRAME_TAIL (0xAB) +#define SDS011_FRAME_HEAD (0xAA) +#define SDS011_FRAME_CSUM_MSK (0xFF) +/** @} */ + +/** + * @name SDS011 frame value indexes + * @{ + */ +#define SDS011_FRAME_HEAD_IDX (0U) +#define SDS011_CMDID_IDX (1U) +#define SDS011_DB1_IDX (2U) +#define SDS011_DB2_IDX (3U) +#define SDS011_DB3_IDX (4U) +#define SDS011_DB4_IDX (5U) +#define SDS011_DB5_IDX (6U) +#define SDS011_DB6_IDX (7U) +#define SDS011_DEVID1_IDX (15U) +#define SDS011_DEVID2_IDX (16U) +#define SDS011_FRAME_SEND_TAIL_IDX (SDS011_FRAME_SEND_LEN - 1) +#define SDS011_FRAME_RECV_TAIL_IDX (SDS011_FRAME_RECV_LEN - 1) +#define SDS011_FRAME_SEND_CSUM_IDX (SDS011_FRAME_SEND_LEN - 2) +#define SDS011_FRAME_RECV_CSUM_IDX (SDS011_FRAME_RECV_LEN - 2) +/** @} */ + +#ifdef __cplusplus +} +#endif + +#endif /* SDS011_INTERNAL_H */ +/** @} */ diff --git a/drivers/sds011/include/sds011_params.h b/drivers/sds011/include/sds011_params.h new file mode 100644 index 0000000000000000000000000000000000000000..c8ea97a5e7fb537714908ad3665d558934308aff --- /dev/null +++ b/drivers/sds011/include/sds011_params.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2018 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_sds011 + * @{ + * + * @file + * @brief SDS011 sensor specific configuration + * + * @author Michel Rottleuthner <michel.rottleuthner@haw-hamburg.de> + */ + +#ifndef SDS011_PARAMS_H +#define SDS011_PARAMS_H + +#include "board.h" +#include "periph/uart.h" +#include "saul_reg.h" +#include "sds011.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @name Set default configuration parameters for the SDS011 driver + * @{ + */ +#ifndef SDS011_PARAM_UART_DEV +#define SDS011_PARAM_UART_DEV (UART_DEV(1)) +#endif +#ifndef SDS011_PARAM_PWR_PIN +#define SDS011_PARAM_PWR_PIN (GPIO_PIN(0, 0)) +#endif +#ifndef SDS011_PARAM_PWR_PIN_AH +#define SDS011_PARAM_PWR_PIN_AH (true) +#endif + +#ifndef SDS011_PARAMS +#define SDS011_PARAMS { .uart = SDS011_PARAM_UART_DEV, \ + .pwr_pin = SDS011_PARAM_PWR_PIN, \ + .pwr_ah = SDS011_PARAM_PWR_PIN_AH, \ + .dev_id = SDS011_DEVID_WILDCARD } +#endif + +#ifndef SDS011_SAUL_INFO +#define SDS011_SAUL_INFO { .name = "SDS011" } +#endif +/** @} */ + +/** + * @brief SDS011 configuration + */ +static const sds011_params_t sds011_params[] = +{ + SDS011_PARAMS +}; + +/** + * @brief Allocate and configure entries to the SAUL registry + */ +saul_reg_info_t sds011_saul_info[] = +{ + SDS011_SAUL_INFO +}; + +#ifdef __cplusplus +} +#endif + +#endif /* SDS011_PARAMS_H */ +/** @} */ diff --git a/drivers/sds011/sds011.c b/drivers/sds011/sds011.c new file mode 100644 index 0000000000000000000000000000000000000000..06da9d6f6c316abbc8199332aee0616ac4222574 --- /dev/null +++ b/drivers/sds011/sds011.c @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2018 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_sds011 + * @{ + * + * @file + * @brief SDS011 Laser Dust Sensor driver implementation + * + * @author Michel Rottleuthner <michel.rottleuthner@haw-hamburg.de> + * + * @} + */ + +#include <string.h> + +#include "assert.h" +#include "sds011.h" +#include "periph/uart.h" + +/** + * @brief UART receive callback + * + * @param[in] arg Context value previously handed to the uart_init call + * @param[in] data single byte received over UART + */ +static void _rx_cb(void *arg, uint8_t data) +{ + sds011_t *dev = (sds011_t*)arg; + + /* frame MUST start with HEAD byte and the buffer must be cleared + before writing to it again */ + if (((dev->pos == 0) && (data != SDS011_FRAME_HEAD)) || + (dev->pos == SDS011_FRAME_RECV_LEN)) { + return; + } + + dev->rx_mem[dev->pos] = data; + + if ((dev->pos >= SDS011_DB1_IDX) && + (dev->pos < SDS011_FRAME_RECV_CSUM_IDX)) { + dev->checksum += data; + } + else if ((dev->pos == SDS011_FRAME_RECV_LEN - 1) && + (dev->rx_mem[SDS011_FRAME_HEAD_IDX] == SDS011_FRAME_HEAD) && + (dev->rx_mem[SDS011_FRAME_RECV_TAIL_IDX] == SDS011_FRAME_TAIL)) { + + dev->checksum &= SDS011_FRAME_CSUM_MSK; + if (dev->rx_mem[SDS011_FRAME_RECV_CSUM_IDX] == dev->checksum) { + + if ((dev->cb != NULL) && + (dev->rx_mem[SDS011_CMDID_IDX] == SDS011_RCMDID_DATA)) { + sds011_data_t measure; + measure.pm_2_5 = dev->rx_mem[SDS011_DB1_IDX] | + (dev->rx_mem[SDS011_DB2_IDX] << 8); + measure.pm_10 = dev->rx_mem[SDS011_DB3_IDX] | + (dev->rx_mem[SDS011_DB4_IDX] << 8); + dev->cb(&measure, dev->cbctx); + dev->pos = -1; + } + } + else { + dev->pos = -1; + } + + dev->checksum = 0; + + /* unlock the mutex for the calling function */ + mutex_unlock(&dev->cb_lock); + } + + dev->pos++; +} + +/** + * @brief send command and wait for first replied message + * + * @param[in] dev SDS011 device the command is sent to + * @param[in] data_bytes data bytes to send within the command + * @param[in] len number of data bytes + * @param[out] recv_frm pointer where the received frame will be stored + * must at least provide SDS011_FRAME_RECV_LEN bytes + */ +int _send_recv_cmd(sds011_t *dev, uint8_t *data_bytes, size_t len, uint8_t *recv_frm) +{ + uint8_t cmd[SDS011_FRAME_SEND_LEN] = {0}; + int checksum = 0; + int res = SDS011_ERROR; + + cmd[SDS011_FRAME_HEAD_IDX] = SDS011_FRAME_HEAD; + cmd[SDS011_CMDID_IDX] = SDS011_CMDID_QUERY; + + for (unsigned i = 0; i < len; i++) { + cmd[SDS011_DB1_IDX + i] = data_bytes[i]; + checksum += data_bytes[i]; + } + + cmd[SDS011_DEVID1_IDX] = (dev->params.dev_id >> 8) & 0xFF; + checksum += cmd[SDS011_DEVID1_IDX]; + cmd[SDS011_DEVID2_IDX] = dev->params.dev_id & 0xFF; + checksum += cmd[SDS011_DEVID2_IDX]; + + cmd[SDS011_FRAME_SEND_LEN - 2] = checksum & SDS011_FRAME_CSUM_MSK; + cmd[SDS011_FRAME_SEND_TAIL_IDX] = SDS011_FRAME_TAIL; + + mutex_lock(&dev->dev_lock); + + dev->pos = 0; + dev->checksum = 0; + + mutex_lock(&dev->cb_lock); + + /* if no active reporting callback is registered, UART must be enabled first */ + if((dev->cb == NULL) && + (uart_init(dev->params.uart, SDS011_UART_BAUDRATE, _rx_cb, dev) != 0)) { + mutex_unlock(&dev->cb_lock); + mutex_unlock(&dev->dev_lock); + return SDS011_ERROR; + } + + uart_write(dev->params.uart, cmd, SDS011_FRAME_SEND_LEN); + + /* wait for the isr callback to unlock the mutex */ + mutex_lock(&dev->cb_lock); + + /* only copy data when checksum was valid */ + if (dev->pos != 0) { + memcpy(recv_frm, dev->rx_mem, SDS011_FRAME_RECV_LEN); + /* mark the recv buffer as free */ + dev->pos = 0; + dev->checksum = 0; + + /* check if we received a valid response for the cmd sent*/ + if(((recv_frm[SDS011_CMDID_IDX] == SDS011_RCMDID_REPLY) && + (cmd[SDS011_DB1_IDX] == recv_frm[SDS011_DB1_IDX])) + || ((recv_frm[SDS011_CMDID_IDX] == SDS011_RCMDID_DATA) + && (cmd[SDS011_DB1_IDX] == SDS011_CMD_DB1_QUERY_DATA))) { + res = SDS011_OK; + } + else { + res = SDS011_INVALID_RESPONSE; + } + } + else { + res = SDS011_INVALID_CHKSUM; + } + + /* reset mutex state */ + mutex_unlock(&dev->cb_lock); + + /* if no active reporting callback is registered, UART can be disabled */ + if((dev->cb == NULL) && + (uart_init(dev->params.uart, SDS011_UART_BAUDRATE, NULL, NULL) != 0)) { + res = SDS011_ERROR; + } + + /* release device */ + mutex_unlock(&dev->dev_lock); + + return res; +} + +/** + * @brief shorthand to get a single byte property with _send_recv_cmd + * + * @param[in] dev SDS011 device the command is sent to + * @param[in] data_bytes data bytes to send within the command + * @param[in] len number of data bytes + * @param[out] p pointer for storing single data byte of the reply + * @param[out] p_idx index of data byte we want to read + */ +static int _get_property(sds011_t *dev, uint8_t *data_bytes, size_t len, + uint8_t *p, uint8_t p_idx) +{ + uint8_t recv[SDS011_FRAME_RECV_LEN]; + int res = _send_recv_cmd(dev, data_bytes, len, recv); + + if (res == SDS011_OK) { + *p = recv[p_idx]; + } + + return res; +} + +int sds011_init(sds011_t *dev, const sds011_params_t *params) +{ + assert((dev != NULL) && (params != NULL) && (params->uart < UART_NUMOF)); + + if ((params->pwr_pin != GPIO_UNDEF) && + (gpio_init(params->pwr_pin, GPIO_OUT) != 0)) { + return SDS011_ERROR; + } + + memcpy(&dev->params, params, sizeof(sds011_params_t)); + + mutex_init(&dev->dev_lock); + mutex_init(&dev->cb_lock); + + dev->cb = NULL; + + sds011_power_on(dev); + + return SDS011_OK; +} + +int sds011_register_callback(sds011_t *dev, sds011_callback_t cb, void *ctx) +{ + assert(dev != NULL); + mutex_lock(&dev->dev_lock); + dev->cbctx = ctx; + dev->cb = cb; + + /* either register un unregister the uart callback */ + if (uart_init(dev->params.uart, SDS011_UART_BAUDRATE, + cb == NULL ? NULL : _rx_cb, + cb == NULL ? NULL : dev) != 0) { + mutex_unlock(&dev->dev_lock); + return SDS011_ERROR; + } + mutex_unlock(&dev->dev_lock); + return SDS011_OK; +} + +void sds011_power_on(const sds011_t *dev) +{ + assert(dev != NULL); + if(dev->params.pwr_pin != GPIO_UNDEF) { + gpio_write(dev->params.pwr_pin, dev->params.pwr_ah); + } +} + +void sds011_power_off(const sds011_t *dev) +{ + assert(dev != NULL); + if(dev->params.pwr_pin != GPIO_UNDEF) { + gpio_write(dev->params.pwr_pin, !dev->params.pwr_ah); + } +} + +int sds011_get_reporting_mode(sds011_t *dev, sds011_reporting_mode_t *mode) +{ + assert(dev != NULL); + uint8_t cmd[] = {SDS011_CMD_DB1_SET_DR_MODE, SDS011_CMD_OPT_QUERY}; + uint8_t prop = 0; + int res = _get_property(dev, cmd, sizeof(cmd), &prop, SDS011_DB3_IDX); + *mode = ((prop == 0) ? SDS011_RMODE_ACTIVE : SDS011_RMODE_QUERY); + return res; +} + +int sds011_set_reporting_mode(sds011_t *dev, sds011_reporting_mode_t mode) +{ + assert(dev != NULL); + uint8_t cmd[] = {SDS011_CMD_DB1_SET_DR_MODE, SDS011_CMD_OPT_SET, mode}; + + uint8_t recv[SDS011_FRAME_RECV_LEN]; + int res = _send_recv_cmd(dev, cmd, sizeof(cmd), recv); + + if (res == SDS011_OK) { + if ((recv[SDS011_DB2_IDX] == SDS011_CMD_OPT_SET) && + (recv[SDS011_DB3_IDX] == mode)) { + return SDS011_OK; + } + + return SDS011_ERROR; + } + + return res; +} + +int sds011_read(sds011_t *dev, sds011_data_t *data) +{ + assert((dev != NULL) && (data != NULL)); + uint8_t cmd[] = {SDS011_CMD_DB1_QUERY_DATA}; + + uint8_t recv[SDS011_FRAME_RECV_LEN]; + int res = _send_recv_cmd(dev, cmd, sizeof(cmd), recv); + + if (res == SDS011_OK) { + data->pm_2_5 = recv[SDS011_DB1_IDX] | (recv[SDS011_DB2_IDX] << 8); + data->pm_10 = recv[SDS011_DB3_IDX] | (recv[SDS011_DB4_IDX] << 8); + } + + return res; +} + +int sds011_set_dev_id(sds011_t *dev, uint16_t sens_dev_id) +{ + assert(dev != NULL); + uint8_t cmd[13] = {0}; + cmd[0] = SDS011_CMD_DB1_SET_DEV_ID; + cmd[11] = (sens_dev_id >> 8) & 0xFF; + cmd[12] = sens_dev_id & 0xFF; + + uint8_t recv[SDS011_FRAME_RECV_LEN]; + int res = _send_recv_cmd(dev, cmd, sizeof(cmd), recv); + + if ((res == SDS011_OK) && + (recv[SDS011_DB5_IDX] == cmd[11]) && + (recv[SDS011_DB6_IDX] == cmd[12])) { + return SDS011_OK; + } + + return SDS011_ERROR; +} + +int sds011_get_working_mode(sds011_t *dev, sds011_working_mode_t *mode) +{ + assert(dev != NULL); + uint8_t cmd[] = {SDS011_CMD_DB1_SET_SLEEP_WORK, SDS011_CMD_OPT_QUERY}; + uint8_t prop = 0; + int res = _get_property(dev, cmd, sizeof(cmd), &prop, SDS011_DB3_IDX); + *mode = ((prop == 0) ? SDS011_WMODE_SLEEP : SDS011_WMODE_WORK); + return res; +} + +int sds011_set_working_mode(sds011_t *dev, sds011_working_mode_t mode) +{ + assert(dev != NULL); + uint8_t cmd[] = {SDS011_CMD_DB1_SET_SLEEP_WORK, SDS011_CMD_OPT_SET, mode}; + + uint8_t recv[SDS011_FRAME_RECV_LEN]; + int res = _send_recv_cmd(dev, cmd, sizeof(cmd), recv); + + if (res == SDS011_OK) { + if ((recv[SDS011_DB2_IDX] == SDS011_CMD_OPT_SET) && + (recv[SDS011_DB3_IDX] == mode)) { + return SDS011_OK; + } + + return SDS011_ERROR; + } + + return res; +} + +int sds011_get_working_period(sds011_t *dev, uint8_t *minutes) +{ + assert(dev != NULL); + uint8_t cmd[] = {SDS011_CMD_DB1_SET_WORK_PERIOD, SDS011_CMD_OPT_QUERY}; + return _get_property(dev, cmd, sizeof(cmd), minutes, SDS011_DB3_IDX); +} + +int sds011_set_working_period(sds011_t *dev, uint8_t minutes) +{ + assert(dev != NULL); + uint8_t cmd[] = {SDS011_CMD_DB1_SET_WORK_PERIOD, SDS011_CMD_OPT_SET, minutes}; + + uint8_t recv[SDS011_FRAME_RECV_LEN]; + int res = _send_recv_cmd(dev, cmd, sizeof(cmd), recv); + + if (res == SDS011_OK) { + if ((recv[SDS011_DB2_IDX] == SDS011_CMD_OPT_SET) && + (recv[SDS011_DB3_IDX] == minutes)) { + return SDS011_OK; + } + + return SDS011_ERROR; + } + + return res; +} + +int sds011_get_fw_version(sds011_t *dev, uint8_t *year, uint8_t *mon, uint8_t *day) +{ + assert((dev != NULL) && (year != NULL) && (mon != NULL) && (day != NULL)); + uint8_t cmd[] = {SDS011_CMD_DB1_CHECK_FIRMWARE}; + + uint8_t recv[SDS011_FRAME_RECV_LEN]; + int res = _send_recv_cmd(dev, cmd, sizeof(cmd), recv); + + if (res == SDS011_OK) { + *year = recv[SDS011_DB2_IDX]; + *mon = recv[SDS011_DB3_IDX]; + *day = recv[SDS011_DB4_IDX]; + } + + return res; +} diff --git a/drivers/sds011/sds011_saul.c b/drivers/sds011/sds011_saul.c new file mode 100644 index 0000000000000000000000000000000000000000..d57e125f9e4c415b1d64e9911f6f630381210745 --- /dev/null +++ b/drivers/sds011/sds011_saul.c @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2018 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_sds011 + * @{ + * + * @file + * @brief SAUL adaption for SDS011 sensor + * + * @author Michel Rottleuthner <michel.rottleuthner@haw-hamburg.de> + * + * @} + */ + +#include <string.h> + +#include "saul.h" +#include "sds011.h" +#include "xtimer.h" + +static int _read(const void *dev, phydat_t *res) +{ + sds011_data_t data; + + if (sds011_read((sds011_t *)dev, &data) == SDS011_OK) { + res->val[0] = data.pm_2_5; + res->val[1] = data.pm_10; + res->unit = UNIT_GPM3; + res->scale = -7; + return 2; + } + return ECANCELED; +} + +const saul_driver_t sds011_saul_driver = { + .read = _read, + .write = saul_notsup, + .type = SAUL_SENSE_PM +}; diff --git a/sys/auto_init/auto_init.c b/sys/auto_init/auto_init.c index a5c45badece582ba03cec0bcd87e6e1c1f0a01bc..b4e389d95d8dc741338b79459e41fa062a4044ac 100644 --- a/sys/auto_init/auto_init.c +++ b/sys/auto_init/auto_init.c @@ -449,6 +449,10 @@ void auto_init(void) extern void auto_init_sht3x(void); auto_init_sht3x(); #endif +#ifdef MODULE_SDS011 + extern void auto_init_sds011(void); + auto_init_sds011(); +#endif #ifdef MODULE_SI114X extern void auto_init_si114x(void); auto_init_si114x(); diff --git a/sys/auto_init/saul/auto_init_sds011.c b/sys/auto_init/saul/auto_init_sds011.c new file mode 100644 index 0000000000000000000000000000000000000000..f5874b6f491b2c9058fd6692f8a3405eded7da2e --- /dev/null +++ b/sys/auto_init/saul/auto_init_sds011.c @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2018 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 sys_auto_init_saul + * @{ + * + * @file + * @brief Auto initialization for SDS011 particulate matter sensor + * + * @author Michel Rottleuthner <michel.rottleuthner@haw-hamburg.de> + * + * @} + */ + +#ifdef MODULE_SDS011 + +#include "assert.h" +#include "log.h" +#include "saul_reg.h" +#include "sds011.h" +#include "sds011_params.h" + +/** + * @brief Define the number of configured sensors + */ +#define SDS011_NUM (sizeof(sds011_params) / sizeof(sds011_params[0])) + +/** + * @brief Allocate memory for the device descriptors + */ +static sds011_t sds011_devs[SDS011_NUM]; + +/** + * @brief Memory for the SAUL registry entries + */ +static saul_reg_t saul_entries[SDS011_NUM]; + +/** + * @brief Define the number of saul info + */ +#define SDS011_INFO_NUM (sizeof(sds011_saul_info) / sizeof(sds011_saul_info[0])) + +/** + * @name Import SAUL endpoint + * @{ + */ +extern const saul_driver_t sds011_saul_driver; +/** @} */ + +void auto_init_sds011(void) +{ + assert(SDS011_INFO_NUM == SDS011_NUM); + + for (unsigned int i = 0; i < SDS011_NUM; i++) { + LOG_DEBUG("[auto_init_saul] initializing sds011 #%u\n", i); + + if (sds011_init(&sds011_devs[i], &sds011_params[i]) != SDS011_OK) { + LOG_ERROR("[auto_init_saul] error initializing sds011 #%u\n", i); + continue; + } + + int retries = 0; + + /* sensor must be set to query mode for manual reading by saul */ + while (sds011_set_reporting_mode(&sds011_devs[i], SDS011_RMODE_QUERY) != SDS011_OK) { + if (retries++ >= 3) { + LOG_ERROR("[auto_init_saul] error setting sds011 to query mode #%u\n", i); + continue; + } + } + + saul_entries[i].dev = &(sds011_devs[i]); + saul_entries[i].name = sds011_saul_info[i].name; + saul_entries[i].driver = &sds011_saul_driver; + saul_reg_add(&saul_entries[i]); + } +} + +#else +typedef int dont_be_pedantic; +#endif /* MODULE_SDS011 */ diff --git a/sys/include/phydat.h b/sys/include/phydat.h index 8a4e161f180d49e6c53f8bdde37683975ffe8711..e57a517c5436596b62e0d7a8e42242d1855282f6 100644 --- a/sys/include/phydat.h +++ b/sys/include/phydat.h @@ -110,7 +110,9 @@ enum { UNIT_PPB, /**< part per billion */ /* aggregate values */ UNIT_TIME, /**< the three dimensions contain sec, min, and hours */ - UNIT_DATE /**< the 3 dimensions contain days, months and years */ + UNIT_DATE, /**< the 3 dimensions contain days, months and years */ + /* mass concentration */ + UNIT_GPM3 /**< grams per cubic meters */ /* extend this list as needed */ }; diff --git a/sys/phydat/phydat_str.c b/sys/phydat/phydat_str.c index 27916b31bea712c47591fc1bf23e60fba1d28bbd..f450c9aa65938d9944ee41c23b2d2938073cb3e2 100644 --- a/sys/phydat/phydat_str.c +++ b/sys/phydat/phydat_str.c @@ -102,6 +102,7 @@ const char *phydat_unit_to_str(uint8_t unit) case UNIT_PERCENT: return "%"; case UNIT_CTS: return "cts"; case UNIT_COULOMB: return "C"; + case UNIT_GPM3: return "g/m^3"; default: return ""; } } diff --git a/tests/driver_sds011/Makefile b/tests/driver_sds011/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..03a1d7827e662dc19d3a1006cbd6ff3cfcfe891e --- /dev/null +++ b/tests/driver_sds011/Makefile @@ -0,0 +1,6 @@ +include ../Makefile.tests_common + +USEMODULE += sds011 +USEMODULE += xtimer + +include $(RIOTBASE)/Makefile.include diff --git a/tests/driver_sds011/main.c b/tests/driver_sds011/main.c new file mode 100644 index 0000000000000000000000000000000000000000..7d5b83c12373cc736ab77aaa0b56d77600a08c04 --- /dev/null +++ b/tests/driver_sds011/main.c @@ -0,0 +1,236 @@ +/** + * Copyright (C) 2018 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 tests + * @{ + * + * @file + * @brief Test application for the SDS011 Laser Dust Sensor driver + * + * @author Michel Rottleuthner <michel.rottleuthner@haw-hamburg.de> + * + * @} + */ +#include <string.h> +#include <stdio.h> +#include <stdint.h> + +#include "xtimer.h" + +#include "sds011.h" +#include "sds011_params.h" +#include "msg.h" +#include "thread.h" + +#define ACTIVE_REPORTING_TEST_CNT (20U) +#define PUT_TO_QUERY_MODE_RETRIES (3U) +#define PUT_TO_QUERY_MODE_RETRY_TIMEOUT_MS (100U) +#define MANUAL_QUERY_CNT (10U) +#define WORKING_PERIOD (0U) + +/** + * @brief Allocate the device descriptor + */ +static sds011_t dev; + +static char* _rmode_str(sds011_reporting_mode_t rmode) +{ + switch (rmode) { + case SDS011_RMODE_ACTIVE: + return "ACTIVE"; + case SDS011_RMODE_QUERY: + return "QUERY"; + default: + return "INVALID"; + } +} + +static char* _wmode_str(sds011_working_mode_t wmode) +{ + switch (wmode) { + case SDS011_WMODE_WORK: + return "WORK"; + case SDS011_WMODE_SLEEP: + return "SLEEP"; + default: + return "INVALID"; + } +} + +static void _print_measurement(sds011_data_t *data) +{ + uint16_t pm10_ug_int = data->pm_10 / 10; + uint16_t pm10_ug_dec = data->pm_10 - 10 * pm10_ug_int; + uint16_t pm2_5_ug_int = data->pm_2_5 / 10; + uint16_t pm2_5_ug_dec = data->pm_2_5 - 10 * pm2_5_ug_int; + printf("==> PM2.5: %d.%0d ug/m^3 | PM10: %d.%0d ug/m^3\n", + pm2_5_ug_int, pm2_5_ug_dec, pm10_ug_int, pm10_ug_dec); +} + +void measure_cb(sds011_data_t *data, void *ctx) +{ + msg_t msg = { .content.value = (((uint32_t)data->pm_10) << 16 | data->pm_2_5) }; + kernel_pid_t target_pid = (int)ctx; + msg_send(&msg, target_pid); +} + +int main(void) +{ + unsigned retry_cnt = 0; + uint8_t year; + uint8_t month; + uint8_t day; + sds011_reporting_mode_t rmode; + sds011_working_mode_t wmode; + uint8_t minutes; + sds011_data_t data; + + puts("SDS011 test application"); + + /* initialize the driver */ + if (sds011_init(&dev, &sds011_params[0]) == SDS011_OK) { + puts("init [OK]"); + } + else { + puts("initalization [ERROR]"); + return -1; + } + + printf("setting reporting mode to '%s'...\n", _rmode_str(SDS011_RMODE_QUERY)); + + /* set the sensor to query mode to disable active reporting messages + -> to work correctly, this step may need to be repeated if the automatic + output is incoming while the reply is expected */ + while (sds011_set_reporting_mode(&dev, SDS011_RMODE_QUERY) != SDS011_OK) { + if (retry_cnt++ >= PUT_TO_QUERY_MODE_RETRIES) { + puts("[ERROR]"); + return -1; + } + xtimer_usleep(PUT_TO_QUERY_MODE_RETRY_TIMEOUT_MS * 1000); + puts("[RETRY]"); + } + + puts("[OK]"); + puts("getting reporting mode from device..."); + + if (sds011_get_reporting_mode(&dev, &rmode) == SDS011_OK) { + printf("[OK] => mode: %s\n", _rmode_str(rmode)); + if (rmode != SDS011_RMODE_QUERY) { + puts("mismatch! [ERROR]"); + return -1; + } + } + else { + puts("[ERROR]"); + return -1; + } + + puts("getting firmware version..."); + + if (sds011_get_fw_version(&dev, &year, &month, &day) == SDS011_OK) { + printf("[OK] => %d.%d.%d\n", year, month, day); + } + else { + puts("[ERROR]"); + return -1; + } + + printf("setting working mode to '%s'...\n", _wmode_str(SDS011_WMODE_WORK)); + + if (sds011_set_working_mode(&dev, SDS011_WMODE_WORK) == SDS011_OK) { + puts("[OK]"); + } + else { + puts("[ERROR]"); + return -1; + } + + puts("getting working mode from device..."); + + if (sds011_get_working_mode(&dev, &wmode) == SDS011_OK) { + printf("[OK] => mode: %s\n", _wmode_str(wmode)); + if (wmode != SDS011_WMODE_WORK) { + puts("mismatch! [ERROR]"); + return -1; + } + } + else { + puts("[ERROR]"); + return -1; + } + + printf("setting working period to %u...\n", WORKING_PERIOD); + + if (sds011_set_working_period(&dev, WORKING_PERIOD) == SDS011_OK) { + puts("[OK]"); + } + else { + puts("[ERROR]"); + return 1; + } + + if (sds011_get_working_period(&dev, &minutes) == SDS011_OK) { + printf("[OK] => working period: %u\n", minutes); + if (minutes != WORKING_PERIOD) { + puts("mismatch! [ERROR]"); + return -1; + } + } + else { + puts("[ERROR]"); + return -1; + } + + for (unsigned i = 0; i < MANUAL_QUERY_CNT; i++) { + if (sds011_read(&dev, &data) == SDS011_OK) { + printf("manual query %2u/%u [OK]: ", i + 1, MANUAL_QUERY_CNT); + _print_measurement(&data); + } + } + + sds011_register_callback(&dev, measure_cb, (void*)(int)thread_getpid()); + + printf("switching to active reporting mode for %u measurements...\n", + ACTIVE_REPORTING_TEST_CNT); + + if (sds011_set_reporting_mode(&dev, SDS011_RMODE_ACTIVE) == SDS011_OK) { + puts("[OK]"); + } + else { + puts("[ERROR]"); + return -1; + } + + /* wait a little bit so the callback gets executed a few times */ + msg_t msg; + for(unsigned msg_cnt = 0; msg_cnt < ACTIVE_REPORTING_TEST_CNT; msg_cnt++){ + msg_receive(&msg); + sds011_data_t data; + data.pm_10 = msg.content.value >> 16; + data.pm_2_5 = msg.content.value & 0xFFFF; + printf("msg from callback: "); + _print_measurement(&data); + } + + /* unregister callback */ + sds011_register_callback(&dev, NULL, NULL); + + puts("switching to sleep mode..."); + + if (sds011_set_working_mode(&dev, SDS011_WMODE_SLEEP) == SDS011_OK) { + puts("[OK]"); + } + else { + puts("[ERROR]"); + return -1; + } + + puts("[SUCCESS]"); + return 0; +}