mirror of
https://github.com/RIOT-OS/RIOT.git
synced 2024-12-29 04:50:03 +01:00
drivers: add support for SHT2x (I2C temp and humidity sensor)
Several parts were inspired by a driver written by: George Psimenos (gp7g14@soton.ac.uk) Steffen Robertz (steffen.robertz@rwth-aachen.de)
This commit is contained in:
parent
c04adf6b1c
commit
50ec5130d6
@ -408,6 +408,11 @@ ifneq (,$(filter sht1%,$(USEMODULE)))
|
||||
USEMODULE += xtimer
|
||||
endif
|
||||
|
||||
ifneq (,$(filter sht2x,$(USEMODULE)))
|
||||
USEMODULE += xtimer
|
||||
FEATURES_REQUIRED += periph_i2c
|
||||
endif
|
||||
|
||||
ifneq (,$(filter sht3x,$(USEMODULE)))
|
||||
USEMODULE += xtimer
|
||||
FEATURES_REQUIRED += periph_i2c
|
||||
|
@ -218,6 +218,10 @@ ifneq (,$(filter sdcard_spi,$(USEMODULE)))
|
||||
USEMODULE_INCLUDES += $(RIOTBASE)/drivers/sdcard_spi/include
|
||||
endif
|
||||
|
||||
ifneq (,$(filter sht2x,$(USEMODULE)))
|
||||
USEMODULE_INCLUDES += $(RIOTBASE)/drivers/sht2x/include
|
||||
endif
|
||||
|
||||
ifneq (,$(filter sht3x,$(USEMODULE)))
|
||||
USEMODULE_INCLUDES += $(RIOTBASE)/drivers/sht3x/include
|
||||
endif
|
||||
|
197
drivers/include/sht2x.h
Normal file
197
drivers/include/sht2x.h
Normal file
@ -0,0 +1,197 @@
|
||||
/*
|
||||
* Copyright (C) 2016,2017,2018 Kees Bakker, SODAQ
|
||||
* Copyright (C) 2017 George Psimenos
|
||||
* Copyright (C) 2018 Steffen Robertz
|
||||
*
|
||||
* 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_sht2x SHT2X
|
||||
* @ingroup drivers_sensors
|
||||
* @brief Device driver interface for the SHT2X sensor
|
||||
* @{
|
||||
*
|
||||
* @file
|
||||
* @brief Device driver implementation for the SHT2x temperature and
|
||||
* humidity sensor.
|
||||
*
|
||||
* @author Kees Bakker <kees@sodaq.com>
|
||||
* @author George Psimenos <gp7g14@soton.ac.uk>
|
||||
* @author Steffen Robertz <steffen.robertz@rwth-aachen.de>
|
||||
*/
|
||||
|
||||
#ifndef SHT2X_H
|
||||
#define SHT2X_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "periph/i2c.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Status and error return codes
|
||||
*/
|
||||
enum {
|
||||
SHT2X_OK = 0, /**< everything was fine */
|
||||
SHT2X_ERR_I2C = -1, /**< error initializing the I2C bus */
|
||||
SHT2X_ERR_NODEV = -2, /**< did not detect SHT2x */
|
||||
SHT2X_ERR_NOCAL = -3, /**< could not read calibration data */
|
||||
SHT2X_ERR_I2C_READ = -4, /**< I2C read error */
|
||||
SHT2X_ERR_TIMEDOUT = -5, /**< timed out */
|
||||
SHT2X_ERR_CRC = -6, /**< CRC error */
|
||||
SHT2X_ERR_USERREG = -7, /**< cannot write User Reg */
|
||||
SHT2X_ERR_RES = -8, /**< invalid resolution */
|
||||
SHT2X_ERR_OTHER = -999, /**< fatal error */
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Available resolutions
|
||||
*
|
||||
* @details The values represent bits 7 and 0 in the User Register
|
||||
*/
|
||||
typedef enum {
|
||||
SHT2X_RES_12_14BIT = 0x00, /**< RH=12bit, T=14bit */
|
||||
SHT2X_RES_8_12BIT = 0x01, /**< RH= 8bit, T=12bit */
|
||||
SHT2X_RES_10_13BIT = 0x80, /**< RH=10bit, T=13bit */
|
||||
SHT2X_RES_11_11BIT = 0x81, /**< RH=11bit, T=11bit */
|
||||
} sht2x_res_t;
|
||||
|
||||
/**
|
||||
* @brief Available Measuring modes
|
||||
*/
|
||||
typedef enum {
|
||||
SHT2X_MEASURE_MODE_HOLD = 0, /**< trigger measurement, hold master */
|
||||
SHT2X_MEASURE_MODE_NO_HOLD = 1 /**< trigger measurement, no hold master (i.e. poll) */
|
||||
} sht2x_measure_mode_t;
|
||||
|
||||
/**
|
||||
* @brief User register masks
|
||||
* @details Notice that the values of @c sht2x_res_t must only have the
|
||||
* bits in @c SHT2X_USER_RESOLUTION_MASK.
|
||||
* @{
|
||||
*/
|
||||
#define SHT2X_USER_RESOLUTION_MASK 0x81
|
||||
#define SHT2X_USER_EOB_MASK 0x40
|
||||
#define SHT2X_USER_HEATER_MASK 0x04
|
||||
#define SHT2X_USER_RESERVED_MASK 0x38
|
||||
#define SHT2X_USER_OTP_MASK 0x02
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* @brief Device initialization parameters
|
||||
*/
|
||||
typedef struct {
|
||||
i2c_t i2c_dev; /**< I2C device */
|
||||
uint8_t i2c_addr; /**< I2C address */
|
||||
sht2x_res_t resolution; /**< resolution bits RH/temp */
|
||||
sht2x_measure_mode_t measure_mode; /**< measurement mode used */
|
||||
bool is_crc_enabled; /**< do CRC or not */
|
||||
} sht2x_params_t;
|
||||
|
||||
/**
|
||||
* @brief Device descriptor for the SHT2X sensor
|
||||
*/
|
||||
typedef struct {
|
||||
sht2x_params_t params; /**< Device Parameters */
|
||||
} sht2x_t;
|
||||
|
||||
/**
|
||||
* @brief Initialize the given SHT2X device
|
||||
*
|
||||
* @param[out] dev Initialized device descriptor of SHT2X device
|
||||
* @param[in] params The parameters for the SHT2x device
|
||||
*
|
||||
* @return SHT2X_OK on success
|
||||
* @return SHT2X_ERR_I2C if given I2C is not enabled in board config
|
||||
* @return SHT2X_ERR_RES invalid resolution
|
||||
* @return SHT2X_ERR_USERREG error writing User Register
|
||||
* @return errors returned by sht2x_write_userreg or sht2x_read_userreg
|
||||
*/
|
||||
int sht2x_init(sht2x_t* dev, const sht2x_params_t* params);
|
||||
|
||||
/**
|
||||
* @brief Reset the SHT2X device
|
||||
*
|
||||
* @param[in] dev Device descriptor of SHT2X device to read from
|
||||
*
|
||||
* @return SHT2X_OK on success
|
||||
* @return SHT2X_ERR_I2C if given I2C is not enabled in board config
|
||||
*/
|
||||
int sht2x_reset(sht2x_t* dev);
|
||||
|
||||
/**
|
||||
* @brief Read temperature value from the given SHT2X device, returned in
|
||||
* centi °C
|
||||
*
|
||||
* @details Notice that this function will sleep (max 66 ms) when the
|
||||
* @a measure_mode is set to @a SHT2X_MEASURE_MODE_NO_HOLD.
|
||||
*
|
||||
* @param[in] dev Device descriptor of SHT2X device to read from
|
||||
*
|
||||
* @returns The temperature in centi Celsius. In case of an error
|
||||
* it returns INT16_MIN.
|
||||
*/
|
||||
int16_t sht2x_read_temperature(const sht2x_t* dev);
|
||||
|
||||
/**
|
||||
* @brief Read humidity value from the given SHT2X device, returned in
|
||||
* centi %RH
|
||||
*
|
||||
* @details Notice that this function will sleep (max 22 ms) when the
|
||||
* @a measure_mode is set to @a SHT2X_MEASURE_MODE_NO_HOLD.
|
||||
*
|
||||
* @param[in] dev Device descriptor of SHT2X device to read from
|
||||
*
|
||||
* @returns Humidity in centi %RH (i.e. the percentage times 100).
|
||||
* In case of an error it returns 0 (i.e. an unrealistic value).
|
||||
*/
|
||||
uint16_t sht2x_read_humidity(const sht2x_t *dev);
|
||||
|
||||
/**
|
||||
* @brief Read identification code from the given SHT2X device
|
||||
*
|
||||
* @param[in] dev Device descriptor of SHT2X device to read from
|
||||
* @param[out] buffer Buffer to write the identification code to
|
||||
* @param[in] buflen The size of @p buffer
|
||||
*
|
||||
* @return >= 0 on success, number of bytes read
|
||||
* @return SHT2X_ERR_CRC in case of a CRC mismatch
|
||||
* @return <0 errors returned by i2c_read_regs
|
||||
*/
|
||||
int sht2x_read_ident(const sht2x_t *dev, uint8_t *buffer, size_t buflen);
|
||||
|
||||
/**
|
||||
* @brief Read User Register from the given SHT2X device
|
||||
*
|
||||
* @param[in] dev Device descriptor of SHT2X device to read from
|
||||
* @param[out] userreg The value of User Register
|
||||
*
|
||||
* @return SHT2X_OK on success
|
||||
* @return SHT2X_ERR_I2C if given I2C is not enabled in board config
|
||||
* @return SHT2X_ERR_OTHER if NULL pointer was given for @p buffer
|
||||
*/
|
||||
int sht2x_read_userreg(const sht2x_t *dev, uint8_t *userreg);
|
||||
|
||||
/**
|
||||
* @brief Write User Register to the given SHT2X device
|
||||
*
|
||||
* @param[in] dev Device descriptor of SHT2X device to read from
|
||||
* @param[in] userreg The value of User Register
|
||||
*
|
||||
* @return SHT2X_OK on success
|
||||
* @return SHT2X_ERR_I2C if given I2C is not enabled in board config
|
||||
*/
|
||||
int sht2x_write_userreg(const sht2x_t *dev, uint8_t userreg);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SHT2X_H */
|
||||
/** @} */
|
1
drivers/sht2x/Makefile
Normal file
1
drivers/sht2x/Makefile
Normal file
@ -0,0 +1 @@
|
||||
include $(RIOTBASE)/Makefile.base
|
99
drivers/sht2x/include/sht2x_params.h
Normal file
99
drivers/sht2x/include/sht2x_params.h
Normal file
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright (C) 2016,2017,2018 Kees Bakker, SODAQ
|
||||
* Copyright (C) 2017 George Psimenos
|
||||
* Copyright (C) 2018 Steffen Robertz
|
||||
*
|
||||
* 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_sht2x
|
||||
*
|
||||
* @{
|
||||
* @file
|
||||
* @brief Default configuration for SHT2X
|
||||
*
|
||||
* @author Kees Bakker <kees@sodaq.com>
|
||||
* @author George Psimenos <gp7g14@soton.ac.uk>
|
||||
* @author Steffen Robertz <steffen.robertz@rwth-aachen.de>
|
||||
*/
|
||||
|
||||
#ifndef SHT2X_PARAMS_H
|
||||
#define SHT2X_PARAMS_H
|
||||
|
||||
#include "sht2x.h"
|
||||
#include "saul_reg.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @brief Set default configuration parameters for the SHT2X
|
||||
* @{
|
||||
*/
|
||||
#ifndef SHT2X_PARAM_I2C_DEV
|
||||
#define SHT2X_PARAM_I2C_DEV (I2C_DEV(0))
|
||||
#endif
|
||||
#ifndef SHT2X_PARAM_I2C_ADDR
|
||||
#define SHT2X_PARAM_I2C_ADDR (0x40)
|
||||
#endif
|
||||
#ifndef SHT2X_PARAM_RESOLUTION
|
||||
#define SHT2X_PARAM_RESOLUTION (SHT2X_RES_12_14BIT)
|
||||
#endif
|
||||
#ifndef SHT2X_PARAM_MEASURE_MODE
|
||||
#define SHT2X_PARAM_MEASURE_MODE (SHT2X_MEASURE_MODE_HOLD)
|
||||
#endif
|
||||
#ifndef SHT2X_PARAM_CRC_MODE
|
||||
#define SHT2X_PARAM_CRC_MODE (1)
|
||||
#endif
|
||||
|
||||
#define SHT2X_PARAMS_DEFAULT {.i2c_dev = SHT2X_PARAM_I2C_DEV, \
|
||||
.i2c_addr = SHT2X_PARAM_I2C_ADDR, \
|
||||
.resolution = SHT2X_PARAM_RESOLUTION, \
|
||||
.measure_mode = SHT2X_PARAM_MEASURE_MODE, \
|
||||
.is_crc_enabled = SHT2X_PARAM_CRC_MODE, \
|
||||
}
|
||||
|
||||
#ifndef SHT2X_SAUL_INFO
|
||||
#define SHT2X_SAUL_INFO { .name = "sht2x" }
|
||||
#endif
|
||||
/**@}*/
|
||||
|
||||
/**
|
||||
* @brief Configure SHT2X
|
||||
*/
|
||||
static const sht2x_params_t sht2x_params[] =
|
||||
{
|
||||
#ifdef SHT2X_PARAMS_BOARD
|
||||
SHT2X_PARAMS_BOARD,
|
||||
#else
|
||||
SHT2X_PARAMS_DEFAULT,
|
||||
#endif
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Get the number of configured SHT2X devices
|
||||
*/
|
||||
#define SHT2X_NUMOF (sizeof(sht2x_params) / sizeof(sht2x_params[0]))
|
||||
|
||||
/**
|
||||
* @brief Configuration details of SAUL registry entries
|
||||
*
|
||||
* This array contains static details of the sensors
|
||||
* for each device. Please be aware that the indexes are used in
|
||||
* auto_init_sht2x, so make sure the indexes match.
|
||||
*/
|
||||
static const saul_reg_info_t sht2x_saul_reg_info[SHT2X_NUMOF] =
|
||||
{
|
||||
SHT2X_SAUL_INFO
|
||||
};
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SHT2X_PARAMS_H */
|
||||
/** @} */
|
475
drivers/sht2x/sht2x.c
Normal file
475
drivers/sht2x/sht2x.c
Normal file
@ -0,0 +1,475 @@
|
||||
/*
|
||||
* Copyright (C) 2016,2017,2018 Kees Bakker, SODAQ
|
||||
* Copyright (C) 2017 George Psimenos
|
||||
* Copyright (C) 2018 Steffen Robertz
|
||||
*
|
||||
* 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_sht2x
|
||||
* @{
|
||||
*
|
||||
* @file
|
||||
* @brief Device driver implementation for the SHT2x temperature and
|
||||
* humidity sensor.
|
||||
*
|
||||
* @author Kees Bakker <kees@sodaq.com>
|
||||
* @author George Psimenos <gp7g14@soton.ac.uk>
|
||||
* @author Steffen Robertz <steffen.robertz@rwth-aachen.de>
|
||||
*
|
||||
* @}
|
||||
*/
|
||||
|
||||
#include <math.h>
|
||||
|
||||
#include "log.h"
|
||||
#include "sht2x.h"
|
||||
#include "sht2x_params.h"
|
||||
#include "periph/i2c.h"
|
||||
#include "xtimer.h"
|
||||
|
||||
#define ENABLE_DEBUG (0)
|
||||
#include "debug.h"
|
||||
|
||||
/**
|
||||
* @brief The number of retries when doing a polled measurement
|
||||
*/
|
||||
#define MAX_RETRIES 20
|
||||
|
||||
/**
|
||||
* @brief A few helper macros
|
||||
*/
|
||||
#define _BUS (dev->params.i2c_dev)
|
||||
#define _ADDR (dev->params.i2c_addr)
|
||||
|
||||
typedef enum {
|
||||
temp_hold_cmd = 0xE3, /**< trigger temp measurement, hold master */
|
||||
hum_hold_cmd = 0xE5, /**< trigger humidity measurement, hold master */
|
||||
write_user_cmd = 0xE6, /**< write user register */
|
||||
read_user_cmd = 0xE7, /**< read user register */
|
||||
temp_no_hold_cmd = 0xF3, /**< trigger temp measurement, no hold master (poll) */
|
||||
hum_no_hold_cmd = 0xF5, /**< trigger humidity measurement, no hold master (poll) */
|
||||
soft_reset_cmd = 0xFE, /**< soft reset */
|
||||
} cmd_t;
|
||||
|
||||
typedef enum {
|
||||
SHT2X_MEASURE_TEMP,
|
||||
SHT2X_MEASURE_RH,
|
||||
} measure_type_t;
|
||||
|
||||
/**
|
||||
* @brief Register addresses to read SHT2x Identification Code.
|
||||
*/
|
||||
static const uint16_t first_mem_addr = 0x0FFA;
|
||||
static const uint16_t second_mem_addr = 0xC9FC;
|
||||
|
||||
static int read_sensor(const sht2x_t* dev, cmd_t command, uint16_t *val);
|
||||
static int read_sensor_poll(const sht2x_t* dev, cmd_t command, uint16_t *val);
|
||||
static uint8_t sht2x_checkcrc(uint8_t data[], uint8_t nbrOfBytes, uint8_t checksum);
|
||||
static void sleep_during_temp_measurement(sht2x_res_t res);
|
||||
static void sleep_during_hum_measurement(sht2x_res_t resolution);
|
||||
|
||||
/*---------------------------------------------------------------------------*
|
||||
* SHT2x Core API *
|
||||
*---------------------------------------------------------------------------*/
|
||||
|
||||
int sht2x_init(sht2x_t* dev, const sht2x_params_t* params)
|
||||
{
|
||||
int i2c_result;
|
||||
|
||||
dev->params = *params;
|
||||
if (dev->params.resolution != SHT2X_RES_12_14BIT &&
|
||||
dev->params.resolution != SHT2X_RES_8_12BIT &&
|
||||
dev->params.resolution != SHT2X_RES_10_13BIT &&
|
||||
dev->params.resolution != SHT2X_RES_11_11BIT) {
|
||||
return SHT2X_ERR_RES;
|
||||
}
|
||||
|
||||
i2c_result = sht2x_reset(dev);
|
||||
if (i2c_result != SHT2X_OK) {
|
||||
return SHT2X_ERR_I2C;
|
||||
}
|
||||
/* wait 15 ms for device to reset */
|
||||
xtimer_usleep(15 * US_PER_MS);
|
||||
|
||||
uint8_t userreg;
|
||||
uint8_t userreg2;
|
||||
i2c_result = sht2x_read_userreg(dev, &userreg);
|
||||
if (i2c_result != SHT2X_OK) {
|
||||
return i2c_result;
|
||||
}
|
||||
DEBUG("[SHT2x] User Register=%02x\n", userreg);
|
||||
|
||||
if ((userreg & SHT2X_USER_RESOLUTION_MASK) != (uint8_t)dev->params.resolution) {
|
||||
userreg &= ~SHT2X_USER_RESOLUTION_MASK;
|
||||
userreg |= (uint8_t)dev->params.resolution;
|
||||
i2c_result = sht2x_write_userreg(dev, userreg);
|
||||
if (i2c_result != SHT2X_OK) {
|
||||
return i2c_result;
|
||||
}
|
||||
i2c_result = sht2x_read_userreg(dev, &userreg2);
|
||||
if (i2c_result != SHT2X_OK) {
|
||||
return i2c_result;
|
||||
}
|
||||
DEBUG("[SHT2x] New User Register=%02x\n", userreg2);
|
||||
if (userreg != userreg2) {
|
||||
return SHT2X_ERR_USERREG;
|
||||
}
|
||||
}
|
||||
|
||||
return SHT2X_OK;
|
||||
}
|
||||
|
||||
int sht2x_reset(sht2x_t* dev)
|
||||
{
|
||||
int i2c_result;
|
||||
cmd_t command = soft_reset_cmd;
|
||||
|
||||
/* Acquire exclusive access */
|
||||
i2c_acquire(_BUS);
|
||||
|
||||
DEBUG("[SHT2x] write command: addr=%02x cmd=%02x\n", _ADDR, (uint8_t)command);
|
||||
i2c_result = i2c_write_byte(_BUS, _ADDR, (uint8_t)command, 0);
|
||||
|
||||
i2c_release(_BUS);
|
||||
|
||||
if (i2c_result != 0) {
|
||||
return SHT2X_ERR_I2C;
|
||||
}
|
||||
|
||||
return SHT2X_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns temperature in centi DegC.
|
||||
*/
|
||||
int16_t sht2x_read_temperature(const sht2x_t* dev)
|
||||
{
|
||||
uint16_t raw_value;
|
||||
int i2c_result;
|
||||
if (dev->params.measure_mode == SHT2X_MEASURE_MODE_NO_HOLD) {
|
||||
i2c_result = read_sensor_poll(dev, temp_no_hold_cmd, &raw_value);
|
||||
} else {
|
||||
i2c_result = read_sensor(dev, temp_hold_cmd, &raw_value);
|
||||
}
|
||||
if (i2c_result != SHT2X_OK) {
|
||||
return INT16_MIN;
|
||||
}
|
||||
return (-46.85 + 175.72 / 65536.0 * raw_value) * 100;
|
||||
}
|
||||
|
||||
/*
|
||||
* Returns humidity in centi %RH (i.e. the percentage times 100).
|
||||
*/
|
||||
uint16_t sht2x_read_humidity(const sht2x_t *dev)
|
||||
{
|
||||
uint16_t raw_value;
|
||||
int i2c_result;
|
||||
if (dev->params.measure_mode == SHT2X_MEASURE_MODE_NO_HOLD) {
|
||||
i2c_result = read_sensor_poll(dev, hum_no_hold_cmd, &raw_value);
|
||||
} else {
|
||||
i2c_result = read_sensor(dev, hum_hold_cmd, &raw_value);
|
||||
}
|
||||
if (i2c_result != SHT2X_OK) {
|
||||
return 0;
|
||||
}
|
||||
return 100 * (-6.0 + 125.0 / 65536.0 * raw_value);
|
||||
}
|
||||
|
||||
static size_t _sht2x_add_ident_byte(uint8_t * buffer, size_t buflen, uint8_t b, size_t ix)
|
||||
{
|
||||
if (ix < buflen) {
|
||||
buffer[ix++] = b;
|
||||
}
|
||||
return ix;
|
||||
}
|
||||
|
||||
int sht2x_read_ident(const sht2x_t *dev, uint8_t * buffer, size_t buflen)
|
||||
{
|
||||
uint8_t data1[8]; /* SNB_3, CRC, SNB_2, CRC, SNB_1, CRC, SNB_0, CRC */
|
||||
uint8_t data2[6]; /* SNC_1, SNC_0, CRC, SNA_1, SNA_0, CRC */
|
||||
size_t ix;
|
||||
int res;
|
||||
|
||||
i2c_acquire(_BUS);
|
||||
res = i2c_read_regs(_BUS, _ADDR,
|
||||
first_mem_addr, data1, sizeof(data1), I2C_REG16);
|
||||
i2c_release(_BUS);
|
||||
if (res < 0) {
|
||||
return res;
|
||||
}
|
||||
|
||||
DEBUG("[SHT2x] ident (1): %02x %02x %02x %02x\n", data1[0], data1[1], data1[2], data1[3]);
|
||||
DEBUG("[SHT2x] ident (1): %02x %02x %02x %02x\n", data1[4], data1[5], data1[6], data1[7]);
|
||||
for (size_t ix = 0; ix < sizeof(data1); ix += 2) {
|
||||
if (sht2x_checkcrc(&data1[ix], 1, data1[ix + 1]) != 0) {
|
||||
DEBUG("[SHT2x] checksum error first (ix=%d)\n", ix);
|
||||
return SHT2X_ERR_CRC;
|
||||
}
|
||||
}
|
||||
|
||||
i2c_acquire(_BUS);
|
||||
res = i2c_read_regs(_BUS, _ADDR,
|
||||
second_mem_addr, data2, sizeof(data2), I2C_REG16);
|
||||
i2c_release(_BUS);
|
||||
if (res < 0) {
|
||||
return res;
|
||||
}
|
||||
DEBUG("[SHT2x] ident (2): %02x %02x %02x\n", data2[0], data2[1], data2[2]);
|
||||
DEBUG("[SHT2x] ident (2): %02x %02x %02x\n", data2[3], data2[4], data2[5]);
|
||||
for (size_t ix = 0; ix < sizeof(data2); ix += 3) {
|
||||
if (sht2x_checkcrc(&data2[ix], 2, data2[ix + 2]) != 0) {
|
||||
DEBUG("[SHT2x] checksum error, second (ix=%d)\n", ix);
|
||||
return SHT2X_ERR_CRC;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* See Sensirion document Electronic_Identification_Code_SHT2x_V1-1_C2
|
||||
*
|
||||
* first memory address:
|
||||
* SNB_3, CRC, SNB_2, CRC, SNB_1, CRC, SNB_0, CRC,
|
||||
* Second memory address:
|
||||
* SNC_1, SNC_0, CRC, SNA_1, SNA_0, CRC
|
||||
*
|
||||
* To assemble the Identification code:
|
||||
* SNA_1, SNA_0, SNB_3, SNB_2, SNB_1, SNB_0, SNC_1, SNC_0
|
||||
*/
|
||||
if (buffer == NULL) {
|
||||
return 0;
|
||||
}
|
||||
ix = 0;
|
||||
ix = _sht2x_add_ident_byte(buffer, buflen, data2[3], ix);
|
||||
ix = _sht2x_add_ident_byte(buffer, buflen, data2[4], ix);
|
||||
ix = _sht2x_add_ident_byte(buffer, buflen, data1[0], ix);
|
||||
ix = _sht2x_add_ident_byte(buffer, buflen, data1[2], ix);
|
||||
ix = _sht2x_add_ident_byte(buffer, buflen, data1[4], ix);
|
||||
ix = _sht2x_add_ident_byte(buffer, buflen, data1[6], ix);
|
||||
ix = _sht2x_add_ident_byte(buffer, buflen, data2[0], ix);
|
||||
ix = _sht2x_add_ident_byte(buffer, buflen, data2[1], ix);
|
||||
return ix;
|
||||
}
|
||||
|
||||
int sht2x_read_userreg(const sht2x_t *dev, uint8_t * userreg)
|
||||
{
|
||||
cmd_t command = read_user_cmd;
|
||||
|
||||
if (userreg) {
|
||||
int i2c_result;
|
||||
DEBUG("[SHT2x] read command: addr=%02x cmd=%02x\n", _ADDR, (uint8_t)command);
|
||||
i2c_acquire(_BUS);
|
||||
i2c_result = i2c_read_reg(_BUS, _ADDR, (uint8_t)command, userreg, 0);
|
||||
i2c_release(_BUS);
|
||||
if (i2c_result != 0) {
|
||||
return SHT2X_ERR_I2C_READ;
|
||||
}
|
||||
} else {
|
||||
return SHT2X_ERR_OTHER;
|
||||
}
|
||||
|
||||
return SHT2X_OK;
|
||||
}
|
||||
|
||||
int sht2x_write_userreg(const sht2x_t *dev, uint8_t userreg)
|
||||
{
|
||||
cmd_t command = write_user_cmd;
|
||||
int i2c_result;
|
||||
DEBUG("[SHT2x] write command: addr=%02x cmd=%02x\n", _ADDR, (uint8_t)command);
|
||||
|
||||
i2c_acquire(_BUS);
|
||||
i2c_result = i2c_write_reg(_BUS, _ADDR, (uint8_t)command, userreg, 0);
|
||||
i2c_release(_BUS);
|
||||
if (i2c_result != 0) {
|
||||
return SHT2X_ERR_I2C;
|
||||
}
|
||||
|
||||
return SHT2X_OK;
|
||||
}
|
||||
|
||||
/******************************************************************************
|
||||
* Local Functions
|
||||
******************************************************************************/
|
||||
|
||||
/**
|
||||
* @brief Read a sensor value from the given SHT2X device
|
||||
*
|
||||
* @param[in] dev Device descriptor of SHT2X device to read from
|
||||
* @param[in] cmd The SHT2x command (hold mode only)
|
||||
* @param[out] val The raw sensor value (only valid if no error)
|
||||
*
|
||||
* @return SHT2X_OK value is returned in @p val
|
||||
* @return SHT2X_NODEV if sensor communication failed
|
||||
* @return SHT2X_ERR_OTHER if parameters are invalid
|
||||
* @return SHT2X_ERR_TIMEDOUT if sensor times out
|
||||
* @return SHT2X_ERR_CRC if the checksum is wrong
|
||||
*/
|
||||
static int read_sensor(const sht2x_t* dev, cmd_t command, uint16_t *val)
|
||||
{
|
||||
uint8_t buffer[3];
|
||||
int i2c_result;
|
||||
|
||||
/* Acquire exclusive access */
|
||||
i2c_acquire(_BUS);
|
||||
|
||||
DEBUG("[SHT2x] write command: addr=%02x cmd=%02x\n", _ADDR, (uint8_t)command);
|
||||
(void)i2c_write_byte(_BUS, _ADDR, (uint8_t)command, 0);
|
||||
i2c_result = i2c_read_bytes(_BUS, _ADDR, buffer, sizeof(buffer), 0);
|
||||
i2c_release(_BUS);
|
||||
if (i2c_result != 0) {
|
||||
DEBUG("[Error] Cannot read SHT2x sensor data.\n");
|
||||
return SHT2X_ERR_I2C_READ;
|
||||
}
|
||||
|
||||
DEBUG("[SHT2x] read: %02x %02x %02x\n", buffer[0], buffer[1], buffer[2]);
|
||||
if (val) {
|
||||
*val = (buffer[0] << 8) | buffer[1];
|
||||
*val &= ~0x0003; /* clear two low bits (status bits) */
|
||||
}
|
||||
|
||||
if (dev->params.is_crc_enabled) {
|
||||
/* byte #3 is the checksum */
|
||||
if (sht2x_checkcrc(buffer, 2, buffer[2]) != 0) {
|
||||
return SHT2X_ERR_CRC;
|
||||
}
|
||||
}
|
||||
|
||||
return SHT2X_OK;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Read a sensor value from the given SHT2X device, polling mode
|
||||
*
|
||||
* @param[in] dev Device descriptor of SHT2X device to read from
|
||||
* @param[in] cmd The SHT2x command (hold mode only)
|
||||
* @param[out] val The raw sensor value (only valid if no error)
|
||||
*
|
||||
* @return SHT2X_OK value is returned in @p val
|
||||
* @return SHT2X_NODEV if sensor communication failed
|
||||
* @return SHT2X_ERR_OTHER if parameters are invalid
|
||||
* @return SHT2X_ERR_TIMEDOUT if sensor times out
|
||||
* @return SHT2X_ERR_CRC if the checksum is wrong
|
||||
*/
|
||||
static int read_sensor_poll(const sht2x_t* dev, cmd_t command, uint16_t *val)
|
||||
{
|
||||
uint8_t buffer[3];
|
||||
int i2c_result;
|
||||
|
||||
/* Acquire exclusive access */
|
||||
i2c_acquire(_BUS);
|
||||
|
||||
DEBUG("[SHT2x] write command: addr=%02x cmd=%02x\n", _ADDR, (uint8_t)command);
|
||||
(void)i2c_write_byte(_BUS, _ADDR, (uint8_t)command, 0);
|
||||
if (command == temp_no_hold_cmd) {
|
||||
sleep_during_temp_measurement(dev->params.resolution);
|
||||
} else {
|
||||
sleep_during_hum_measurement(dev->params.resolution);
|
||||
}
|
||||
|
||||
uint8_t ix = 0;
|
||||
for (; ix < MAX_RETRIES; ix++) {
|
||||
i2c_result = i2c_read_bytes(_BUS, _ADDR, buffer, sizeof(buffer), 0);
|
||||
if (i2c_result == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
i2c_release(_BUS);
|
||||
if (i2c_result != 0) {
|
||||
DEBUG("[Error] Cannot read SHT2x sensor data.\n");
|
||||
return SHT2X_ERR_I2C_READ;
|
||||
}
|
||||
|
||||
DEBUG("[SHT2x] read: %02x %02x %02x\n", buffer[0], buffer[1], buffer[2]);
|
||||
if (val) {
|
||||
*val = (buffer[0] << 8) | buffer[1];
|
||||
*val &= ~0x0003; /* clear two low bits (status bits) */
|
||||
}
|
||||
|
||||
/* byte #3 is the checksum */
|
||||
if (dev->params.is_crc_enabled) {
|
||||
if (sht2x_checkcrc(buffer, 2, buffer[2]) != 0) {
|
||||
return SHT2X_ERR_CRC;
|
||||
}
|
||||
}
|
||||
|
||||
return SHT2X_OK;
|
||||
}
|
||||
|
||||
static const uint16_t POLYNOMIAL = 0x131; /* P(x)=x^8+x^5+x^4+1 = 100110001 */
|
||||
/**
|
||||
* @brief Calculate 8-Bit checksum with given polynomial
|
||||
*/
|
||||
static uint8_t sht2x_checkcrc(uint8_t data[], uint8_t nbrOfBytes, uint8_t checksum)
|
||||
{
|
||||
uint8_t crc = 0;
|
||||
uint8_t byteCtr;
|
||||
for (byteCtr = 0; byteCtr < nbrOfBytes; ++byteCtr)
|
||||
{
|
||||
crc ^= (data[byteCtr]);
|
||||
for (uint8_t bit = 8; bit > 0; --bit)
|
||||
{
|
||||
if ((crc & 0x80) != 0)
|
||||
crc = (crc << 1) ^ POLYNOMIAL;
|
||||
else
|
||||
crc = (crc << 1);
|
||||
}
|
||||
}
|
||||
if (crc != checksum)
|
||||
return 1;
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Initialize the given SHT2X device
|
||||
*
|
||||
* @param[in] res The resolution bits in the User Register
|
||||
*
|
||||
* @details Sleep for the typical time it takes to complete the measurement
|
||||
* this depends on the resolution and is taken from the datasheet.
|
||||
* Measurement time differs for temperature and humidity.
|
||||
*/
|
||||
static void sleep_during_temp_measurement(sht2x_res_t res)
|
||||
{
|
||||
uint32_t amount_ms = 0;
|
||||
|
||||
switch (res) {
|
||||
case SHT2X_RES_12_14BIT:
|
||||
amount_ms = 66;
|
||||
break;
|
||||
case SHT2X_RES_8_12BIT:
|
||||
amount_ms = 17;
|
||||
break;
|
||||
case SHT2X_RES_10_13BIT:
|
||||
amount_ms = 33;
|
||||
break;
|
||||
case SHT2X_RES_11_11BIT:
|
||||
amount_ms = 9;
|
||||
break;
|
||||
}
|
||||
xtimer_usleep(amount_ms * US_PER_MS);
|
||||
}
|
||||
|
||||
static void sleep_during_hum_measurement(sht2x_res_t resolution)
|
||||
{
|
||||
uint32_t amount_ms = 0;
|
||||
|
||||
switch (resolution) {
|
||||
case SHT2X_RES_12_14BIT:
|
||||
amount_ms = 22;
|
||||
break;
|
||||
case SHT2X_RES_8_12BIT:
|
||||
amount_ms = 3;
|
||||
break;
|
||||
case SHT2X_RES_10_13BIT:
|
||||
amount_ms = 7;
|
||||
break;
|
||||
case SHT2X_RES_11_11BIT:
|
||||
amount_ms = 12;
|
||||
break;
|
||||
}
|
||||
xtimer_usleep(amount_ms * US_PER_MS);
|
||||
}
|
53
drivers/sht2x/sht2x_saul.c
Normal file
53
drivers/sht2x/sht2x_saul.c
Normal file
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Kees Bakker, SODAQ
|
||||
*
|
||||
* 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_sht2x
|
||||
* @{
|
||||
*
|
||||
* @file
|
||||
* @brief SAUL adaption for Sensirion SHT20/SHT21/SHT25 devices
|
||||
*
|
||||
* @author Kees Bakker <kees@sodaq.com>
|
||||
*
|
||||
* @}
|
||||
*/
|
||||
|
||||
#include "saul.h"
|
||||
|
||||
#include "sht2x.h"
|
||||
|
||||
static int read_temperature(const void *dev, phydat_t *res)
|
||||
{
|
||||
res->val[0] = sht2x_read_temperature((const sht2x_t *)dev);
|
||||
res->unit = UNIT_TEMP_C;
|
||||
res->scale = -2;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int read_relative_humidity(const void *dev, phydat_t *res)
|
||||
{
|
||||
res->val[0] = sht2x_read_humidity((const sht2x_t *)dev);
|
||||
res->unit = UNIT_PERCENT;
|
||||
res->scale = -2;
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
const saul_driver_t sht2x_temperature_saul_driver = {
|
||||
.read = read_temperature,
|
||||
.write = saul_notsup,
|
||||
.type = SAUL_SENSE_TEMP,
|
||||
};
|
||||
|
||||
const saul_driver_t sht2x_relative_humidity_saul_driver = {
|
||||
.read = read_relative_humidity,
|
||||
.write = saul_notsup,
|
||||
.type = SAUL_SENSE_HUM,
|
||||
};
|
@ -445,6 +445,10 @@ void auto_init(void)
|
||||
extern void auto_init_pulse_counter(void);
|
||||
auto_init_pulse_counter();
|
||||
#endif
|
||||
#ifdef MODULE_SHT2X
|
||||
extern void auto_init_sht2x(void);
|
||||
auto_init_sht2x();
|
||||
#endif
|
||||
#ifdef MODULE_SHT3X
|
||||
extern void auto_init_sht3x(void);
|
||||
auto_init_sht3x();
|
||||
|
77
sys/auto_init/saul/auto_init_sht2x.c
Normal file
77
sys/auto_init/saul/auto_init_sht2x.c
Normal file
@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright (C) 2019 Kees Bakker, SODAQ
|
||||
*
|
||||
* 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 of SHT2X driver.
|
||||
*
|
||||
* @author Kees Bakker <kees@sodaq.com>
|
||||
*
|
||||
* @}
|
||||
*/
|
||||
|
||||
#if defined(MODULE_SHT2X)
|
||||
|
||||
#include "log.h"
|
||||
#include "saul_reg.h"
|
||||
|
||||
#include "sht2x_params.h"
|
||||
#include "sht2x.h"
|
||||
|
||||
/**
|
||||
* @brief Allocation of memory for device descriptors
|
||||
*/
|
||||
static sht2x_t sht2x_devs[SHT2X_NUMOF];
|
||||
|
||||
/**
|
||||
* @brief Reference the driver structs.
|
||||
* @{
|
||||
*/
|
||||
extern const saul_driver_t sht2x_temperature_saul_driver;
|
||||
extern const saul_driver_t sht2x_relative_humidity_saul_driver;
|
||||
/** @} */
|
||||
|
||||
/**
|
||||
* @brief Memory for the SAUL registry entries
|
||||
*/
|
||||
#define SENSORS_NUMOF 2
|
||||
static saul_reg_t saul_entries[SHT2X_NUMOF * SENSORS_NUMOF];
|
||||
|
||||
void auto_init_sht2x(void)
|
||||
{
|
||||
size_t se_ix = 0;
|
||||
for (size_t i = 0; i < SHT2X_NUMOF; i++) {
|
||||
LOG_DEBUG("[auto_init_saul] initializing SHT2X #%u\n", i);
|
||||
int res = sht2x_init(&sht2x_devs[i], &sht2x_params[i]);
|
||||
if (res < 0) {
|
||||
LOG_ERROR("[auto_init_saul] error initializing SHT2X #%u\n", i);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* temperature */
|
||||
saul_entries[se_ix].dev = &sht2x_devs[i];
|
||||
saul_entries[se_ix].name = sht2x_saul_reg_info[i].name;
|
||||
saul_entries[se_ix].driver = &sht2x_temperature_saul_driver;
|
||||
saul_reg_add(&saul_entries[se_ix]);
|
||||
se_ix++;
|
||||
|
||||
/* relative humidity */
|
||||
saul_entries[se_ix].dev = &sht2x_devs[i];
|
||||
saul_entries[se_ix].name = sht2x_saul_reg_info[i].name;
|
||||
saul_entries[se_ix].driver = &sht2x_relative_humidity_saul_driver;
|
||||
saul_reg_add(&saul_entries[se_ix]);
|
||||
se_ix++;
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
typedef int dont_be_pedantic;
|
||||
#endif /* MODULE_SHT2X */
|
10
tests/driver_sht2x/Makefile
Normal file
10
tests/driver_sht2x/Makefile
Normal file
@ -0,0 +1,10 @@
|
||||
APPLICATION = driver_sht2x
|
||||
include ../Makefile.tests_common
|
||||
|
||||
FEATURES_REQUIRED = periph_i2c
|
||||
|
||||
USEMODULE += sht2x
|
||||
USEMODULE += xtimer
|
||||
USEMODULE += printf_float
|
||||
|
||||
include $(RIOTBASE)/Makefile.include
|
24
tests/driver_sht2x/README.md
Executable file
24
tests/driver_sht2x/README.md
Executable file
@ -0,0 +1,24 @@
|
||||
# Sensirion SHT20/SHT21/SHT25 Humidity and Temperature Sensors
|
||||
|
||||
## About
|
||||
This is a test application for the SHT2x humidity/temperature sensor
|
||||
driver.
|
||||
|
||||
## Usage
|
||||
This test application will initialize the SHT2x sensor with the following
|
||||
parameters:
|
||||
- default device type: SHT21
|
||||
- 14-bit temperature resolution
|
||||
- 12-bit humidity resolution
|
||||
|
||||
To change these parameters, you can override them by setting their
|
||||
corresponding defines in your build environment, e.g.
|
||||
```bash
|
||||
CFLAGS=-DSHT2X_PARAM_RESOLUTION=SHT2x_RES_8_12BIT make ...
|
||||
```
|
||||
See `drivers/sht2x_params.h` for the default configuration.
|
||||
|
||||
After initialization, the sensor reads the temperature and humidity values
|
||||
every one second and prints them to STDOUT. The chip-heater of the SHT2x
|
||||
will be turned on in the beginning, so the temperature should rise over
|
||||
time and the humidity should decrease.
|
120
tests/driver_sht2x/main.c
Normal file
120
tests/driver_sht2x/main.c
Normal file
@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (C) 2016 Kees Bakker, SODAQ
|
||||
*
|
||||
* 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 SHT2x temperature and humidity sensor
|
||||
*
|
||||
* @author Kees Bakker <kees@sodaq.com>
|
||||
*
|
||||
* @}
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <inttypes.h>
|
||||
|
||||
#include "sht2x_params.h"
|
||||
#include "sht2x.h"
|
||||
#include "xtimer.h"
|
||||
|
||||
#define SLEEP_2S (2 * 1000 * 1000u) /* 2 seconds delay between printf */
|
||||
|
||||
static void dump_buffer(const char* txt, uint8_t* buffer, size_t len);
|
||||
|
||||
int main(void)
|
||||
{
|
||||
sht2x_t dev;
|
||||
uint8_t ident[8];
|
||||
int result;
|
||||
int ident_size;
|
||||
|
||||
memset(ident, 0, sizeof(ident));
|
||||
|
||||
puts("SHT2X test application\n");
|
||||
|
||||
printf("+------------Initializing------------+\n");
|
||||
/* Use the first default parameter set */
|
||||
result = sht2x_init(&dev, &sht2x_params[0]);
|
||||
if (result != SHT2X_OK) {
|
||||
puts("[Error] The given i2c is not enabled");
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
printf("Initialization successful\n\n");
|
||||
}
|
||||
|
||||
printf("\n+--------Identifier--------+\n");
|
||||
ident_size = sht2x_read_ident(&dev, ident, sizeof(ident));
|
||||
if (ident_size > 0) {
|
||||
dump_buffer("Identifier Code:", ident, ident_size);
|
||||
}
|
||||
|
||||
printf("\n+--------Starting Measurements--------+\n");
|
||||
while (1) {
|
||||
int16_t temperature;
|
||||
|
||||
{
|
||||
/* Print the identification every so often */
|
||||
static int count = 0;
|
||||
if (++count >= 100) {
|
||||
ident_size = sht2x_read_ident(&dev, ident, sizeof(ident));
|
||||
if (ident_size > 0) {
|
||||
dump_buffer("Identifier Code:", ident, ident_size);
|
||||
}
|
||||
count = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Get temperature in centi degrees celsius */
|
||||
temperature = sht2x_read_temperature(&dev);
|
||||
if (temperature == INT16_MIN) {
|
||||
printf("\n+--------Soft Reset--------+\n");
|
||||
sht2x_reset(&dev);
|
||||
} else {
|
||||
uint16_t humidity;
|
||||
|
||||
/* Get humidity in %RH */
|
||||
humidity = sht2x_read_humidity(&dev);
|
||||
|
||||
printf("Temperature [°C]: %d.%d\n"
|
||||
"Humidity [%%RH]: %u.%02u\n"
|
||||
"\n+-------------------------------------+\n",
|
||||
temperature / 100, (temperature % 100) / 10,
|
||||
(unsigned int)(humidity / 100), (unsigned int)(humidity % 100)
|
||||
);
|
||||
}
|
||||
|
||||
xtimer_usleep(SLEEP_2S);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void dump_buffer(const char* txt, uint8_t* buffer, size_t len)
|
||||
{
|
||||
size_t ix;
|
||||
printf("%s\n", txt);
|
||||
for (ix = 0; ix < len; ++ix) {
|
||||
if (ix > 0) {
|
||||
if ((ix % 8) == 0) {
|
||||
printf("\n");
|
||||
}
|
||||
else {
|
||||
printf(" ");
|
||||
}
|
||||
}
|
||||
printf("%02x", buffer[ix]);
|
||||
}
|
||||
if (len > 0) {
|
||||
printf("\n");
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user