first commit

This commit is contained in:
stuce-bot 2025-06-30 20:47:33 +02:00
commit 5893b00dd2
1669 changed files with 1982740 additions and 0 deletions

View file

@ -0,0 +1,237 @@
#include "BMP280.h"
bool BMP280::begin(TwoWire* wire, uint8_t addr, uint8_t sda, uint8_t scl,
long freq) {
_i2c.begin(wire, sda, scl, freq);
_addr = addr;
if (!_i2c.exist(_addr)) {
return false;
}
readCoefficients();
setSampling();
return true;
}
bool BMP280::update() {
readTemperature();
readPressure();
readAltitude();
return true;
}
float BMP280::readTemperature() {
int32_t var1, var2;
int32_t adc_T = read24(BMP280_REGISTER_TEMPDATA);
adc_T >>= 4;
var1 = ((((adc_T >> 3) - ((int32_t)_bmp280_calib.dig_T1 << 1))) *
((int32_t)_bmp280_calib.dig_T2)) >>
11;
var2 = (((((adc_T >> 4) - ((int32_t)_bmp280_calib.dig_T1)) *
((adc_T >> 4) - ((int32_t)_bmp280_calib.dig_T1))) >>
12) *
((int32_t)_bmp280_calib.dig_T3)) >>
14;
t_fine = var1 + var2;
float T = (t_fine * 5 + 128) >> 8;
cTemp = T / 100;
return cTemp;
}
float BMP280::readPressure() {
int64_t var1, var2, p;
// Must be done first to get the t_fine variable set up
int32_t adc_P = read24(BMP280_REGISTER_PRESSUREDATA);
adc_P >>= 4;
var1 = ((int64_t)t_fine) - 128000;
var2 = var1 * var1 * (int64_t)_bmp280_calib.dig_P6;
var2 = var2 + ((var1 * (int64_t)_bmp280_calib.dig_P5) << 17);
var2 = var2 + (((int64_t)_bmp280_calib.dig_P4) << 35);
var1 = ((var1 * var1 * (int64_t)_bmp280_calib.dig_P3) >> 8) +
((var1 * (int64_t)_bmp280_calib.dig_P2) << 12);
var1 =
(((((int64_t)1) << 47) + var1)) * ((int64_t)_bmp280_calib.dig_P1) >> 33;
if (var1 == 0) {
return 0; // avoid exception caused by division by zero
}
p = 1048576 - adc_P;
p = (((p << 31) - var2) * 3125) / var1;
var1 = (((int64_t)_bmp280_calib.dig_P9) * (p >> 13) * (p >> 13)) >> 25;
var2 = (((int64_t)_bmp280_calib.dig_P8) * p) >> 19;
p = ((p + var1 + var2) >> 8) + (((int64_t)_bmp280_calib.dig_P7) << 4);
pressure = p / 256;
return pressure;
}
/*!
* @brief Calculates the approximate altitude using barometric pressure and the
* supplied sea level hPa as a reference.
* @param seaLevelhPa
* The current hPa at sea level.
* @return The approximate altitude above sea level in meters.
*/
float BMP280::readAltitude(float seaLevelhPa) {
float pressure = readPressure(); // in Si units for Pascal
pressure /= 100;
altitude = 44330 * (1.0 - pow(pressure / seaLevelhPa, 0.1903));
return altitude;
}
void BMP280::setSampling(sensor_mode mode, sensor_sampling tempSampling,
sensor_sampling pressSampling, sensor_filter filter,
standby_duration duration) {
_measReg.mode = mode;
_measReg.osrs_t = tempSampling;
_measReg.osrs_p = pressSampling;
_configReg.filter = filter;
_configReg.t_sb = duration;
write8(BMP280_REGISTER_CONFIG, _configReg.get());
write8(BMP280_REGISTER_CONTROL, _measReg.get());
}
void BMP280::readCoefficients() {
_bmp280_calib.dig_T1 = read16_LE(BMP280_REGISTER_DIG_T1);
_bmp280_calib.dig_T2 = readS16_LE(BMP280_REGISTER_DIG_T2);
_bmp280_calib.dig_T3 = readS16_LE(BMP280_REGISTER_DIG_T3);
_bmp280_calib.dig_P1 = read16_LE(BMP280_REGISTER_DIG_P1);
_bmp280_calib.dig_P2 = readS16_LE(BMP280_REGISTER_DIG_P2);
_bmp280_calib.dig_P3 = readS16_LE(BMP280_REGISTER_DIG_P3);
_bmp280_calib.dig_P4 = readS16_LE(BMP280_REGISTER_DIG_P4);
_bmp280_calib.dig_P5 = readS16_LE(BMP280_REGISTER_DIG_P5);
_bmp280_calib.dig_P6 = readS16_LE(BMP280_REGISTER_DIG_P6);
_bmp280_calib.dig_P7 = readS16_LE(BMP280_REGISTER_DIG_P7);
_bmp280_calib.dig_P8 = readS16_LE(BMP280_REGISTER_DIG_P8);
_bmp280_calib.dig_P9 = readS16_LE(BMP280_REGISTER_DIG_P9);
}
/*!
* Calculates the pressure at sea level (QNH) from the specified altitude,
* and atmospheric pressure (QFE).
* @param altitude Altitude in m
* @param atmospheric Atmospheric pressure in hPa
* @return The approximate pressure in hPa
*/
float BMP280::seaLevelForAltitude(float altitude, float atmospheric) {
// Equation taken from BMP180 datasheet (page 17):
// http://www.adafruit.com/datasheets/BST-BMP180-DS000-09.pdf
// Note that using the equation from wikipedia can give bad results
// at high altitude. See this thread for more information:
// http://forums.adafruit.com/viewtopic.php?f=22&t=58064
return atmospheric / pow(1.0 - (altitude / 44330.0), 5.255);
}
/*!
@brief calculates the boiling point of water by a given pressure
@param pressure pressure in hPa
@return temperature in °C
*/
float BMP280::waterBoilingPoint(float pressure) {
// Magnusformular for calculation of the boiling point of water at a given
// pressure
return (234.175 * log(pressure / 6.1078)) /
(17.08085 - log(pressure / 6.1078));
}
/*!
@brief Take a new measurement (only possible in forced mode)
@return true if successful, otherwise false
*/
bool BMP280::takeForcedMeasurement() {
// If we are in forced mode, the BME sensor goes back to sleep after each
// measurement and we need to set it to forced mode once at this point, so
// it will take the next measurement and then return to sleep again.
// In normal mode simply does new measurements periodically.
if (_measReg.mode == MODE_FORCED) {
// set to forced mode, i.e. "take next measurement"
write8(BMP280_REGISTER_CONTROL, _measReg.get());
// wait until measurement has been completed, otherwise we would read
// the values from the last measurement
while (read8(BMP280_REGISTER_STATUS) & 0x08) delay(1);
return true;
}
return false;
}
/*!
* @brief Resets the chip via soft reset
*/
void BMP280::reset(void) {
write8(BMP280_REGISTER_SOFTRESET, MODE_SOFT_RESET_CODE);
}
/*!
@brief Gets the most recent sensor event from the hardware status register.
@return Sensor status as a byte.
*/
uint8_t BMP280::getStatus(void) {
return read8(BMP280_REGISTER_STATUS);
}
/**************************************************************************/
/*!
@brief Writes an 8 bit value over I2C/SPI
*/
/**************************************************************************/
void BMP280::write8(byte reg, byte value) {
_i2c.writeByte(_addr, reg, value);
}
/*!
* @brief Reads an 8 bit value over I2C/SPI
* @param reg
* selected register
* @return value from selected register
*/
uint8_t BMP280::read8(byte reg) {
return _i2c.readByte(_addr, reg);
}
/*!
* @brief Reads a 16 bit value over I2C/SPI
*/
uint16_t BMP280::read16(byte reg) {
uint8_t buffer[2];
_i2c.readBytes(_addr, reg, buffer, 2);
return uint16_t(buffer[0]) << 8 | uint16_t(buffer[1]);
}
uint16_t BMP280::read16_LE(byte reg) {
uint16_t temp = read16(reg);
return (temp >> 8) | (temp << 8);
}
/*!
* @brief Reads a signed 16 bit value over I2C/SPI
*/
int16_t BMP280::readS16(byte reg) {
return (int16_t)read16(reg);
}
int16_t BMP280::readS16_LE(byte reg) {
return (int16_t)read16_LE(reg);
}
/*!
* @brief Reads a 24 bit value over I2C/SPI
*/
uint32_t BMP280::read24(byte reg) {
uint8_t buffer[3];
_i2c.readBytes(_addr, reg, buffer, 3);
return uint32_t(buffer[0]) << 16 | uint32_t(buffer[1]) << 8 |
uint32_t(buffer[2]);
}

View file

@ -0,0 +1,199 @@
#ifndef _BMP280_H
#define _BMP280_H
#include "Arduino.h"
#include "Wire.h"
#include "I2C_Class.h"
#define BMP280_I2C_ADDR 0x76
enum {
BMP280_REGISTER_DIG_T1 = 0x88,
BMP280_REGISTER_DIG_T2 = 0x8A,
BMP280_REGISTER_DIG_T3 = 0x8C,
BMP280_REGISTER_DIG_P1 = 0x8E,
BMP280_REGISTER_DIG_P2 = 0x90,
BMP280_REGISTER_DIG_P3 = 0x92,
BMP280_REGISTER_DIG_P4 = 0x94,
BMP280_REGISTER_DIG_P5 = 0x96,
BMP280_REGISTER_DIG_P6 = 0x98,
BMP280_REGISTER_DIG_P7 = 0x9A,
BMP280_REGISTER_DIG_P8 = 0x9C,
BMP280_REGISTER_DIG_P9 = 0x9E,
BMP280_REGISTER_CHIPID = 0xD0,
BMP280_REGISTER_VERSION = 0xD1,
BMP280_REGISTER_SOFTRESET = 0xE0,
BMP280_REGISTER_CAL26 = 0xE1, /**< R calibration = 0xE1-0xF0 */
BMP280_REGISTER_STATUS = 0xF3,
BMP280_REGISTER_CONTROL = 0xF4,
BMP280_REGISTER_CONFIG = 0xF5,
BMP280_REGISTER_PRESSUREDATA = 0xF7,
BMP280_REGISTER_TEMPDATA = 0xFA,
};
typedef struct {
uint16_t dig_T1; /**< dig_T1 cal register. */
int16_t dig_T2; /**< dig_T2 cal register. */
int16_t dig_T3; /**< dig_T3 cal register. */
uint16_t dig_P1; /**< dig_P1 cal register. */
int16_t dig_P2; /**< dig_P2 cal register. */
int16_t dig_P3; /**< dig_P3 cal register. */
int16_t dig_P4; /**< dig_P4 cal register. */
int16_t dig_P5; /**< dig_P5 cal register. */
int16_t dig_P6; /**< dig_P6 cal register. */
int16_t dig_P7; /**< dig_P7 cal register. */
int16_t dig_P8; /**< dig_P8 cal register. */
int16_t dig_P9; /**< dig_P9 cal register. */
} bmp280_calib_data;
class BMP280 {
public:
/** Oversampling rate for the sensor. */
enum sensor_sampling {
/** No over-sampling. */
SAMPLING_NONE = 0x00,
/** 1x over-sampling. */
SAMPLING_X1 = 0x01,
/** 2x over-sampling. */
SAMPLING_X2 = 0x02,
/** 4x over-sampling. */
SAMPLING_X4 = 0x03,
/** 8x over-sampling. */
SAMPLING_X8 = 0x04,
/** 16x over-sampling. */
SAMPLING_X16 = 0x05
};
/** Operating mode for the sensor. */
enum sensor_mode {
/** Sleep mode. */
MODE_SLEEP = 0x00,
/** Forced mode. */
MODE_FORCED = 0x01,
/** Normal mode. */
MODE_NORMAL = 0x03,
/** Software reset. */
MODE_SOFT_RESET_CODE = 0xB6
};
/** Filtering level for sensor data. */
enum sensor_filter {
/** No filtering. */
FILTER_OFF = 0x00,
/** 2x filtering. */
FILTER_X2 = 0x01,
/** 4x filtering. */
FILTER_X4 = 0x02,
/** 8x filtering. */
FILTER_X8 = 0x03,
/** 16x filtering. */
FILTER_X16 = 0x04
};
/** Standby duration in ms */
enum standby_duration {
/** 1 ms standby. */
STANDBY_MS_1 = 0x00,
/** 62.5 ms standby. */
STANDBY_MS_63 = 0x01,
/** 125 ms standby. */
STANDBY_MS_125 = 0x02,
/** 250 ms standby. */
STANDBY_MS_250 = 0x03,
/** 500 ms standby. */
STANDBY_MS_500 = 0x04,
/** 1000 ms standby. */
STANDBY_MS_1000 = 0x05,
/** 2000 ms standby. */
STANDBY_MS_2000 = 0x06,
/** 4000 ms standby. */
STANDBY_MS_4000 = 0x07
};
public:
bool begin(TwoWire *wire = &Wire, uint8_t addr = BMP280_I2C_ADDR,
uint8_t sda = 21, uint8_t scl = 22, long freq = 400000U);
bool update();
float pressure = 0;
float cTemp = 0;
float altitude = 0;
void reset(void);
uint8_t getStatus(void);
float readTemperature();
float readPressure(void);
float readAltitude(float seaLevelhPa = 1013.25);
float seaLevelForAltitude(float altitude, float atmospheric);
float waterBoilingPoint(float pressure);
bool takeForcedMeasurement();
void setSampling(sensor_mode mode = MODE_NORMAL,
sensor_sampling tempSampling = SAMPLING_X16,
sensor_sampling pressSampling = SAMPLING_X16,
sensor_filter filter = FILTER_OFF,
standby_duration duration = STANDBY_MS_1);
private:
/** Encapsulates the config register */
struct config {
/** Initialize to power-on-reset state */
config()
: t_sb(STANDBY_MS_1), filter(FILTER_OFF), none(0), spi3w_en(0) {
}
/** Inactive duration (standby time) in normal mode */
unsigned int t_sb : 3;
/** Filter settings */
unsigned int filter : 3;
/** Unused - don't set */
unsigned int none : 1;
/** Enables 3-wire SPI */
unsigned int spi3w_en : 1;
/** Used to retrieve the assembled config register's byte value. */
unsigned int get() {
return (t_sb << 5) | (filter << 2) | spi3w_en;
}
};
/** Encapsulates trhe ctrl_meas register */
struct ctrl_meas {
/** Initialize to power-on-reset state */
ctrl_meas()
: osrs_t(SAMPLING_NONE), osrs_p(SAMPLING_NONE), mode(MODE_SLEEP) {
}
/** Temperature oversampling. */
unsigned int osrs_t : 3;
/** Pressure oversampling. */
unsigned int osrs_p : 3;
/** Device mode */
unsigned int mode : 2;
/** Used to retrieve the assembled ctrl_meas register's byte value. */
unsigned int get() {
return (osrs_t << 5) | (osrs_p << 2) | mode;
}
};
void readCoefficients(void);
uint8_t spixfer(uint8_t x);
void write8(byte reg, byte value);
uint8_t read8(byte reg);
uint16_t read16(byte reg);
uint32_t read24(byte reg);
int16_t readS16(byte reg);
uint16_t read16_LE(byte reg);
int16_t readS16_LE(byte reg);
int32_t t_fine;
bmp280_calib_data _bmp280_calib;
config _configReg;
ctrl_meas _measReg;
private:
uint8_t _addr;
I2C_Class _i2c;
};
#endif

View file

@ -0,0 +1,23 @@
#include "DHT12.h"
bool DHT12::begin(TwoWire* wire, uint8_t addr, uint8_t sda, uint8_t scl,
long freq) {
_i2c.begin(wire, sda, scl, freq);
_addr = addr;
return _i2c.exist(_addr);
}
bool DHT12::update() {
uint8_t datos[5] = {0};
if (_i2c.readBytes(_addr, 0, datos, 5)) {
if (datos[4] != (datos[0] + datos[1] + datos[2] + datos[3])) {
return false;
}
cTemp = (datos[2] + (float)datos[3] / 10);
fTemp = ((datos[2] + (float)datos[3] / 10) * 1.8 + 32);
kTemp = (datos[2] + (float)datos[3] / 10) + 273.15;
humidity = (datos[0] + (float)datos[1] / 10);
return true;
}
return false;
}

View file

@ -0,0 +1,26 @@
#ifndef _DHT12_H
#define _DHT12_H
#include "Arduino.h"
#include "Wire.h"
#include "I2C_Class.h"
#define DHT12_I2C_ADDR 0x5c
class DHT12 {
public:
bool begin(TwoWire* wire = &Wire, uint8_t addr = DHT12_I2C_ADDR,
uint8_t sda = 21, uint8_t scl = 22, long freq = 400000U);
bool update();
float cTemp = 0;
float fTemp = 0;
float kTemp = 0;
float humidity = 0;
private:
uint8_t _addr;
I2C_Class _i2c;
};
#endif

View file

@ -0,0 +1,93 @@
#include "I2C_Class.h"
void I2C_Class::begin(TwoWire *wire, uint8_t sda, uint8_t scl, long freq) {
_wire = wire;
_sda = sda;
_scl = scl;
_freq = freq;
_wire->end();
_wire->begin(static_cast<int>(_sda), _scl, freq);
}
bool I2C_Class::exist(uint8_t addr) {
int error;
_wire->beginTransmission(addr);
error = _wire->endTransmission();
if (error == 0) {
return true;
}
return false;
}
bool I2C_Class::writeBytes(uint8_t addr, uint8_t reg, uint8_t *buffer,
uint8_t length) {
_wire->beginTransmission(addr);
_wire->write(reg);
_wire->write(buffer, length);
if (_wire->endTransmission() == 0) return true;
return false;
}
bool I2C_Class::readBytes(uint8_t addr, uint8_t reg, uint8_t *buffer,
uint8_t length) {
uint8_t index = 0;
_wire->beginTransmission(addr);
_wire->write(reg);
_wire->endTransmission();
if (_wire->requestFrom(addr, length)) {
for (uint8_t i = 0; i < length; i++) {
buffer[index++] = _wire->read();
}
return true;
}
return false;
}
bool I2C_Class::readU16(uint8_t addr, uint8_t reg_addr, uint16_t *value) {
uint8_t read_buf[2] = {0x00, 0x00};
bool result = readBytes(addr, reg_addr, read_buf, 2);
*value = (read_buf[0] << 8) | read_buf[1];
return result;
}
bool I2C_Class::writeU16(uint8_t addr, uint8_t reg_addr, uint16_t value) {
uint8_t write_buf[2];
write_buf[0] = value >> 8;
write_buf[1] = value & 0xff;
return writeBytes(addr, reg_addr, write_buf, 2);
}
bool I2C_Class::writeByte(uint8_t addr, uint8_t reg, uint8_t data) {
_wire->beginTransmission(addr);
_wire->write(reg);
_wire->write(data);
if (_wire->endTransmission() == 0) return true;
return false;
}
uint8_t I2C_Class::readByte(uint8_t addr, uint8_t reg) {
_wire->beginTransmission(addr);
_wire->write(reg);
_wire->endTransmission();
if (_wire->requestFrom(addr, 1)) {
return _wire->read();
}
return 0;
}
bool I2C_Class::writeBitOn(uint8_t addr, uint8_t reg, uint8_t data) {
uint8_t temp;
uint8_t write_back;
temp = readByte(addr, reg);
write_back = (temp | data);
return (writeByte(addr, reg, write_back));
}
bool I2C_Class::writeBitOff(uint8_t addr, uint8_t reg, uint8_t data) {
uint8_t temp;
uint8_t write_back;
temp = readByte(addr, reg);
write_back = (temp & (~data));
return (writeByte(addr, reg, write_back));
}

View file

@ -0,0 +1,30 @@
#ifndef _I2C_DEVICE_BUS_
#define _I2C_DEVICE_BUS_
#include "Arduino.h"
#include "Wire.h"
class I2C_Class {
private:
TwoWire* _wire;
uint8_t _scl;
uint8_t _sda;
long _freq;
public:
void begin(TwoWire* wire, uint8_t sda, uint8_t scl, long freq = 100000);
bool exist(uint8_t addr);
bool writeBytes(uint8_t addr, uint8_t reg, uint8_t* buffer, uint8_t length);
bool readBytes(uint8_t addr, uint8_t reg, uint8_t* buffer, uint8_t length);
bool readU16(uint8_t addr, uint8_t reg_addr, uint16_t* value);
bool writeU16(uint8_t addr, uint8_t reg_addr, uint16_t value);
bool writeByte(uint8_t addr, uint8_t reg, uint8_t data);
uint8_t readByte(uint8_t addr, uint8_t reg);
bool writeBitOn(uint8_t addr, uint8_t reg, uint8_t data);
bool writeBitOff(uint8_t addr, uint8_t reg, uint8_t data);
};
#endif

View file

@ -0,0 +1,16 @@
#if defined(M5_UNIT_UNIFIED_ENV_HPP)
#error "DO NOT USE it at the same time as M5UnitUnified libraries"
#endif
#ifndef _M5_UNIT_ENV_H_
#define _M5_UNIT_ENV_H_
#include "Arduino.h"
#include "DHT12.h"
#include "QMP6988.h"
#include "SHT3X.h"
#include "SHT4X.h"
#include "BMP280.h"
#include "SCD4X.h"
#endif

View file

@ -0,0 +1,17 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file M5UnitUnifiedENV.h
@brief Main header of M5UnitENV using M5UnitUnfied
*/
#ifndef M5_UNIT_UNIFIED_ENV_H
#define M5_UNIT_UNIFIED_ENV_H
#ifdef __cplusplus
#include "M5UnitUnifiedENV.hpp"
#else
#error M5UnitUnifiedENV requires a C++ compiler, please change file extension to .cc or .cpp
#endif
#endif

View file

@ -0,0 +1,55 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file M5UnitUnifiedENV.hpp
@brief Main header of M5UnitENV using M5UnitUnified
@mainpage M5Unit-ENV
Library for UnitENV using M5UnitUnified.
*/
#if defined(_M5_UNIT_ENV_H_)
#error "DO NOT USE it at the same time as conventional libraries"
#endif
#ifndef M5_UNIT_UNIFIED_ENV_HPP
#define M5_UNIT_UNIFIED_ENV_HPP
// CO2
#include "unit/unit_SCD40.hpp"
#include "unit/unit_SCD41.hpp"
// ENVIII
#include "unit/unit_SHT30.hpp"
#include "unit/unit_QMP6988.hpp"
#include "unit/unit_ENV3.hpp"
// ENVPro
#include "unit/unit_BME688.hpp"
// TVOC
#include "unit/unit_SGP30.hpp"
// ENVIV
#include "unit/unit_SHT40.hpp"
#include "unit/unit_BMP280.hpp"
#include "unit/unit_ENV4.hpp"
/*!
@namespace m5
@brief Top level namespace of M5stack
*/
namespace m5 {
/*!
@namespace unit
@brief Unit-related namespace
*/
namespace unit {
using UnitCO2 = m5::unit::UnitSCD40;
using UnitCO2L = m5::unit::UnitSCD41;
using UnitENVPro = m5::unit::UnitBME688;
using UnitTVOC = m5::unit::UnitSGP30;
} // namespace unit
} // namespace m5
#endif

View file

@ -0,0 +1,353 @@
#include <math.h>
#include "stdint.h"
#include "stdio.h"
#include "QMP6988.h"
// DISABLE LOG
#define QMP6988_LOG(format...)
#define QMP6988_ERR(format...)
// ENABLE LOG
// #define QMP6988_LOG Serial.printf
// #define QMP6988_ERR Serial.printf
int QMP6988::getCalibrationData() {
int status = 0;
// BITFIELDS temp_COE;
uint8_t a_data_uint8_tr[QMP6988_CALIBRATION_DATA_LENGTH] = {0};
int len;
for (len = 0; len < QMP6988_CALIBRATION_DATA_LENGTH; len += 1) {
status = _i2c.readBytes(_addr, QMP6988_CALIBRATION_DATA_START + len,
&a_data_uint8_tr[len], 1);
if (status == 0) {
QMP6988_LOG("qmp6988 read 0xA0 error!");
return status;
}
}
qmp6988.qmp6988_cali.COE_a0 =
(QMP6988_S32_t)(((a_data_uint8_tr[18] << SHIFT_LEFT_12_POSITION) |
(a_data_uint8_tr[19] << SHIFT_LEFT_4_POSITION) |
(a_data_uint8_tr[24] & 0x0f))
<< 12);
qmp6988.qmp6988_cali.COE_a0 = qmp6988.qmp6988_cali.COE_a0 >> 12;
qmp6988.qmp6988_cali.COE_a1 =
(QMP6988_S16_t)(((a_data_uint8_tr[20]) << SHIFT_LEFT_8_POSITION) |
a_data_uint8_tr[21]);
qmp6988.qmp6988_cali.COE_a2 =
(QMP6988_S16_t)(((a_data_uint8_tr[22]) << SHIFT_LEFT_8_POSITION) |
a_data_uint8_tr[23]);
qmp6988.qmp6988_cali.COE_b00 =
(QMP6988_S32_t)(((a_data_uint8_tr[0] << SHIFT_LEFT_12_POSITION) |
(a_data_uint8_tr[1] << SHIFT_LEFT_4_POSITION) |
((a_data_uint8_tr[24] & 0xf0) >>
SHIFT_RIGHT_4_POSITION))
<< 12);
qmp6988.qmp6988_cali.COE_b00 = qmp6988.qmp6988_cali.COE_b00 >> 12;
qmp6988.qmp6988_cali.COE_bt1 =
(QMP6988_S16_t)(((a_data_uint8_tr[2]) << SHIFT_LEFT_8_POSITION) |
a_data_uint8_tr[3]);
qmp6988.qmp6988_cali.COE_bt2 =
(QMP6988_S16_t)(((a_data_uint8_tr[4]) << SHIFT_LEFT_8_POSITION) |
a_data_uint8_tr[5]);
qmp6988.qmp6988_cali.COE_bp1 =
(QMP6988_S16_t)(((a_data_uint8_tr[6]) << SHIFT_LEFT_8_POSITION) |
a_data_uint8_tr[7]);
qmp6988.qmp6988_cali.COE_b11 =
(QMP6988_S16_t)(((a_data_uint8_tr[8]) << SHIFT_LEFT_8_POSITION) |
a_data_uint8_tr[9]);
qmp6988.qmp6988_cali.COE_bp2 =
(QMP6988_S16_t)(((a_data_uint8_tr[10]) << SHIFT_LEFT_8_POSITION) |
a_data_uint8_tr[11]);
qmp6988.qmp6988_cali.COE_b12 =
(QMP6988_S16_t)(((a_data_uint8_tr[12]) << SHIFT_LEFT_8_POSITION) |
a_data_uint8_tr[13]);
qmp6988.qmp6988_cali.COE_b21 =
(QMP6988_S16_t)(((a_data_uint8_tr[14]) << SHIFT_LEFT_8_POSITION) |
a_data_uint8_tr[15]);
qmp6988.qmp6988_cali.COE_bp3 =
(QMP6988_S16_t)(((a_data_uint8_tr[16]) << SHIFT_LEFT_8_POSITION) |
a_data_uint8_tr[17]);
QMP6988_LOG("<-----------calibration data-------------->\r\n");
QMP6988_LOG("COE_a0[%d] COE_a1[%d] COE_a2[%d] COE_b00[%d]\r\n",
qmp6988.qmp6988_cali.COE_a0, qmp6988.qmp6988_cali.COE_a1,
qmp6988.qmp6988_cali.COE_a2, qmp6988.qmp6988_cali.COE_b00);
QMP6988_LOG("COE_bt1[%d] COE_bt2[%d] COE_bp1[%d] COE_b11[%d]\r\n",
qmp6988.qmp6988_cali.COE_bt1, qmp6988.qmp6988_cali.COE_bt2,
qmp6988.qmp6988_cali.COE_bp1, qmp6988.qmp6988_cali.COE_b11);
QMP6988_LOG("COE_bp2[%d] COE_b12[%d] COE_b21[%d] COE_bp3[%d]\r\n",
qmp6988.qmp6988_cali.COE_bp2, qmp6988.qmp6988_cali.COE_b12,
qmp6988.qmp6988_cali.COE_b21, qmp6988.qmp6988_cali.COE_bp3);
QMP6988_LOG("<-----------calibration data-------------->\r\n");
qmp6988.ik.a0 = qmp6988.qmp6988_cali.COE_a0; // 20Q4
qmp6988.ik.b00 = qmp6988.qmp6988_cali.COE_b00; // 20Q4
qmp6988.ik.a1 = 3608L * (QMP6988_S32_t)qmp6988.qmp6988_cali.COE_a1 -
1731677965L; // 31Q23
qmp6988.ik.a2 = 16889L * (QMP6988_S32_t)qmp6988.qmp6988_cali.COE_a2 -
87619360L; // 30Q47
qmp6988.ik.bt1 = 2982L * (QMP6988_S64_t)qmp6988.qmp6988_cali.COE_bt1 +
107370906L; // 28Q15
qmp6988.ik.bt2 = 329854L * (QMP6988_S64_t)qmp6988.qmp6988_cali.COE_bt2 +
108083093L; // 34Q38
qmp6988.ik.bp1 = 19923L * (QMP6988_S64_t)qmp6988.qmp6988_cali.COE_bp1 +
1133836764L; // 31Q20
qmp6988.ik.b11 = 2406L * (QMP6988_S64_t)qmp6988.qmp6988_cali.COE_b11 +
118215883L; // 28Q34
qmp6988.ik.bp2 = 3079L * (QMP6988_S64_t)qmp6988.qmp6988_cali.COE_bp2 -
181579595L; // 29Q43
qmp6988.ik.b12 = 6846L * (QMP6988_S64_t)qmp6988.qmp6988_cali.COE_b12 +
85590281L; // 29Q53
qmp6988.ik.b21 = 13836L * (QMP6988_S64_t)qmp6988.qmp6988_cali.COE_b21 +
79333336L; // 29Q60
qmp6988.ik.bp3 = 2915L * (QMP6988_S64_t)qmp6988.qmp6988_cali.COE_bp3 +
157155561L; // 28Q65
QMP6988_LOG("<----------- int calibration data -------------->\r\n");
QMP6988_LOG("a0[%d] a1[%d] a2[%d] b00[%d]\r\n", qmp6988.ik.a0,
qmp6988.ik.a1, qmp6988.ik.a2, qmp6988.ik.b00);
QMP6988_LOG("bt1[%lld] bt2[%lld] bp1[%lld] b11[%lld]\r\n",
qmp6988.ik.bt1, qmp6988.ik.bt2, qmp6988.ik.bp1, qmp6988.ik.b11);
QMP6988_LOG("bp2[%lld] b12[%lld] b21[%lld] bp3[%lld]\r\n",
qmp6988.ik.bp2, qmp6988.ik.b12, qmp6988.ik.b21, qmp6988.ik.bp3);
QMP6988_LOG("<----------- int calibration data -------------->\r\n");
return 1;
}
QMP6988_S16_t QMP6988::convTx02e(qmp6988_ik_data_t* ik, QMP6988_S32_t dt) {
QMP6988_S16_t ret;
QMP6988_S64_t wk1, wk2;
// wk1: 60Q4 // bit size
wk1 = ((QMP6988_S64_t)ik->a1 * (QMP6988_S64_t)dt); // 31Q23+24-1=54 (54Q23)
wk2 = ((QMP6988_S64_t)ik->a2 * (QMP6988_S64_t)dt) >>
14; // 30Q47+24-1=53 (39Q33)
wk2 = (wk2 * (QMP6988_S64_t)dt) >> 10; // 39Q33+24-1=62 (52Q23)
wk2 = ((wk1 + wk2) / 32767) >> 19; // 54,52->55Q23 (20Q04)
ret = (QMP6988_S16_t)((ik->a0 + wk2) >> 4); // 21Q4 -> 17Q0
return ret;
}
QMP6988_S32_t QMP6988::getPressure02e(qmp6988_ik_data_t* ik, QMP6988_S32_t dp,
QMP6988_S16_t tx) {
QMP6988_S32_t ret;
QMP6988_S64_t wk1, wk2, wk3;
// wk1 = 48Q16 // bit size
wk1 =
((QMP6988_S64_t)ik->bt1 * (QMP6988_S64_t)tx); // 28Q15+16-1=43 (43Q15)
wk2 = ((QMP6988_S64_t)ik->bp1 * (QMP6988_S64_t)dp) >>
5; // 31Q20+24-1=54 (49Q15)
wk1 += wk2; // 43,49->50Q15
wk2 = ((QMP6988_S64_t)ik->bt2 * (QMP6988_S64_t)tx) >>
1; // 34Q38+16-1=49 (48Q37)
wk2 = (wk2 * (QMP6988_S64_t)tx) >> 8; // 48Q37+16-1=63 (55Q29)
wk3 = wk2; // 55Q29
wk2 = ((QMP6988_S64_t)ik->b11 * (QMP6988_S64_t)tx) >>
4; // 28Q34+16-1=43 (39Q30)
wk2 = (wk2 * (QMP6988_S64_t)dp) >> 1; // 39Q30+24-1=62 (61Q29)
wk3 += wk2; // 55,61->62Q29
wk2 = ((QMP6988_S64_t)ik->bp2 * (QMP6988_S64_t)dp) >>
13; // 29Q43+24-1=52 (39Q30)
wk2 = (wk2 * (QMP6988_S64_t)dp) >> 1; // 39Q30+24-1=62 (61Q29)
wk3 += wk2; // 62,61->63Q29
wk1 += wk3 >> 14; // Q29 >> 14 -> Q15
wk2 =
((QMP6988_S64_t)ik->b12 * (QMP6988_S64_t)tx); // 29Q53+16-1=45 (45Q53)
wk2 = (wk2 * (QMP6988_S64_t)tx) >> 22; // 45Q53+16-1=61 (39Q31)
wk2 = (wk2 * (QMP6988_S64_t)dp) >> 1; // 39Q31+24-1=62 (61Q30)
wk3 = wk2; // 61Q30
wk2 = ((QMP6988_S64_t)ik->b21 * (QMP6988_S64_t)tx) >>
6; // 29Q60+16-1=45 (39Q54)
wk2 = (wk2 * (QMP6988_S64_t)dp) >> 23; // 39Q54+24-1=62 (39Q31)
wk2 = (wk2 * (QMP6988_S64_t)dp) >> 1; // 39Q31+24-1=62 (61Q20)
wk3 += wk2; // 61,61->62Q30
wk2 = ((QMP6988_S64_t)ik->bp3 * (QMP6988_S64_t)dp) >>
12; // 28Q65+24-1=51 (39Q53)
wk2 = (wk2 * (QMP6988_S64_t)dp) >> 23; // 39Q53+24-1=62 (39Q30)
wk2 = (wk2 * (QMP6988_S64_t)dp); // 39Q30+24-1=62 (62Q30)
wk3 += wk2; // 62,62->63Q30
wk1 += wk3 >> 15; // Q30 >> 15 = Q15
wk1 /= 32767L;
wk1 >>= 11; // Q15 >> 7 = Q4
wk1 += ik->b00; // Q4 + 20Q4
// wk1 >>= 4; // 28Q4 -> 24Q0
ret = (QMP6988_S32_t)wk1;
return ret;
}
void QMP6988::reset() {
uint8_t ret = 0;
ret = _i2c.writeByte(_addr, QMP6988_RESET_REG, 0xe6);
if (ret == 0) {
QMP6988_LOG("reset fail!!! \r\n");
}
delay(20);
ret = _i2c.writeByte(_addr, QMP6988_RESET_REG, 0x00);
}
void QMP6988::setpPowermode(int power_mode) {
uint8_t data;
QMP6988_LOG("qmp_set_powermode %d \r\n", power_mode);
qmp6988.power_mode = power_mode;
_i2c.readBytes(_addr, QMP6988_CTRLMEAS_REG, &data, 1);
data = data & 0xfc;
if (power_mode == QMP6988_SLEEP_MODE) {
data |= 0x00;
} else if (power_mode == QMP6988_FORCED_MODE) {
data |= 0x01;
} else if (power_mode == QMP6988_NORMAL_MODE) {
data |= 0x03;
}
_i2c.writeByte(_addr, QMP6988_CTRLMEAS_REG, data);
QMP6988_LOG("qmp_set_powermode 0xf4=0x%x \r\n", data);
delay(20);
}
void QMP6988::setFilter(unsigned char filter) {
uint8_t data;
data = (filter & 0x03);
_i2c.writeByte(_addr, QMP6988_CONFIG_REG, data);
delay(20);
}
void QMP6988::setOversamplingP(unsigned char oversampling_p) {
uint8_t data;
_i2c.readBytes(_addr, QMP6988_CTRLMEAS_REG, &data, 1);
data &= 0xe3;
data |= (oversampling_p << 2);
_i2c.writeByte(_addr, QMP6988_CTRLMEAS_REG, data);
delay(20);
}
void QMP6988::setOversamplingT(unsigned char oversampling_t) {
uint8_t data;
_i2c.readBytes(_addr, QMP6988_CTRLMEAS_REG, &data, 1);
data &= 0x1f;
data |= (oversampling_t << 5);
_i2c.writeByte(_addr, QMP6988_CTRLMEAS_REG, data);
delay(20);
}
float QMP6988::calcAltitude(float pressure, float temp) {
float altitude;
altitude =
(pow((101325 / pressure), 1 / 5.257) - 1) * (temp + 273.15) / 0.0065;
QMP6988_LOG("altitude = %f\r\n", altitude);
return altitude;
}
float QMP6988::calcPressure() {
uint8_t err = 0;
QMP6988_U32_t P_read, T_read;
QMP6988_S32_t P_raw, T_raw;
uint8_t a_data_uint8_tr[6] = {0};
QMP6988_S32_t T_int, P_int;
// press
err = _i2c.readBytes(_addr, QMP6988_PRESSURE_MSB_REG, a_data_uint8_tr, 6);
if (err == 0) {
QMP6988_LOG("qmp6988 read press raw error! \r\n");
return 0.0f;
}
P_read = (QMP6988_U32_t)((((QMP6988_U32_t)(a_data_uint8_tr[0]))
<< SHIFT_LEFT_16_POSITION) |
(((QMP6988_U16_t)(a_data_uint8_tr[1]))
<< SHIFT_LEFT_8_POSITION) |
(a_data_uint8_tr[2]));
P_raw = (QMP6988_S32_t)(P_read - SUBTRACTOR);
T_read = (QMP6988_U32_t)((((QMP6988_U32_t)(a_data_uint8_tr[3]))
<< SHIFT_LEFT_16_POSITION) |
(((QMP6988_U16_t)(a_data_uint8_tr[4]))
<< SHIFT_LEFT_8_POSITION) |
(a_data_uint8_tr[5]));
T_raw = (QMP6988_S32_t)(T_read - SUBTRACTOR);
T_int = convTx02e(&(qmp6988.ik), T_raw);
P_int = getPressure02e(&(qmp6988.ik), P_raw, T_int);
qmp6988.temperature = (float)T_int / 256.0f;
qmp6988.pressure = (float)P_int / 16.0f;
return qmp6988.pressure;
}
float QMP6988::calcTemperature() {
uint8_t err = 0;
QMP6988_U32_t P_read, T_read;
QMP6988_S32_t P_raw, T_raw;
uint8_t a_data_uint8_tr[6] = {0};
QMP6988_S32_t T_int, P_int;
// press
err = _i2c.readBytes(_addr, QMP6988_PRESSURE_MSB_REG, a_data_uint8_tr, 6);
if (err == 0) {
QMP6988_LOG("qmp6988 read press raw error! \r\n");
return 0.0f;
}
P_read = (QMP6988_U32_t)((((QMP6988_U32_t)(a_data_uint8_tr[0]))
<< SHIFT_LEFT_16_POSITION) |
(((QMP6988_U16_t)(a_data_uint8_tr[1]))
<< SHIFT_LEFT_8_POSITION) |
(a_data_uint8_tr[2]));
P_raw = (QMP6988_S32_t)(P_read - SUBTRACTOR);
// temp
err =
_i2c.readBytes(_addr, QMP6988_TEMPERATURE_MSB_REG, a_data_uint8_tr, 3);
if (err == 0) {
QMP6988_LOG("qmp6988 read temp raw error! \n");
}
T_read = (QMP6988_U32_t)((((QMP6988_U32_t)(a_data_uint8_tr[3]))
<< SHIFT_LEFT_16_POSITION) |
(((QMP6988_U16_t)(a_data_uint8_tr[4]))
<< SHIFT_LEFT_8_POSITION) |
(a_data_uint8_tr[5]));
T_raw = (QMP6988_S32_t)(T_read - SUBTRACTOR);
T_int = convTx02e(&(qmp6988.ik), T_raw);
P_int = getPressure02e(&(qmp6988.ik), P_raw, T_int);
qmp6988.temperature = (float)T_int / 256.0f;
qmp6988.pressure = (float)P_int / 16.0f;
return qmp6988.temperature;
}
bool QMP6988::begin(TwoWire* wire, uint8_t addr, uint8_t sda, uint8_t scl,
long freq) {
_i2c.begin(wire, sda, scl, freq);
_addr = addr;
if (!_i2c.exist(_addr)) {
return false;
}
reset();
getCalibrationData();
setpPowermode(QMP6988_NORMAL_MODE);
setFilter(QMP6988_FILTERCOEFF_4);
setOversamplingP(QMP6988_OVERSAMPLING_8X);
setOversamplingT(QMP6988_OVERSAMPLING_1X);
return true;
}
bool QMP6988::update() {
pressure = calcPressure();
cTemp = calcTemperature();
altitude = calcAltitude(pressure, cTemp);
return true;
}

View file

@ -0,0 +1,153 @@
#ifndef __QMP6988_H
#define __QMP6988_H
#include "Arduino.h"
#include "Wire.h"
#include "I2C_Class.h"
#define QMP6988_SLAVE_ADDRESS_L (0x70)
#define QMP6988_SLAVE_ADDRESS_H (0x56)
#define QMP6988_U16_t unsigned short
#define QMP6988_S16_t short
#define QMP6988_U32_t unsigned int
#define QMP6988_S32_t int
#define QMP6988_U64_t unsigned long long
#define QMP6988_S64_t long long
#define QMP6988_CHIP_ID 0x5C
#define QMP6988_CHIP_ID_REG 0xD1
#define QMP6988_RESET_REG 0xE0 /* Device reset register */
#define QMP6988_DEVICE_STAT_REG 0xF3 /* Device state register */
#define QMP6988_CTRLMEAS_REG 0xF4 /* Measurement Condition Control Register */
/* data */
#define QMP6988_PRESSURE_MSB_REG 0xF7 /* Pressure MSB Register */
#define QMP6988_TEMPERATURE_MSB_REG 0xFA /* Temperature MSB Reg */
/* compensation calculation */
#define QMP6988_CALIBRATION_DATA_START \
0xA0 /* QMP6988 compensation coefficients */
#define QMP6988_CALIBRATION_DATA_LENGTH 25
#define SHIFT_RIGHT_4_POSITION 4
#define SHIFT_LEFT_2_POSITION 2
#define SHIFT_LEFT_4_POSITION 4
#define SHIFT_LEFT_5_POSITION 5
#define SHIFT_LEFT_8_POSITION 8
#define SHIFT_LEFT_12_POSITION 12
#define SHIFT_LEFT_16_POSITION 16
/* power mode */
#define QMP6988_SLEEP_MODE 0x00
#define QMP6988_FORCED_MODE 0x01
#define QMP6988_NORMAL_MODE 0x03
#define QMP6988_CTRLMEAS_REG_MODE__POS 0
#define QMP6988_CTRLMEAS_REG_MODE__MSK 0x03
#define QMP6988_CTRLMEAS_REG_MODE__LEN 2
/* oversampling */
#define QMP6988_OVERSAMPLING_SKIPPED 0x00
#define QMP6988_OVERSAMPLING_1X 0x01
#define QMP6988_OVERSAMPLING_2X 0x02
#define QMP6988_OVERSAMPLING_4X 0x03
#define QMP6988_OVERSAMPLING_8X 0x04
#define QMP6988_OVERSAMPLING_16X 0x05
#define QMP6988_OVERSAMPLING_32X 0x06
#define QMP6988_OVERSAMPLING_64X 0x07
#define QMP6988_CTRLMEAS_REG_OSRST__POS 5
#define QMP6988_CTRLMEAS_REG_OSRST__MSK 0xE0
#define QMP6988_CTRLMEAS_REG_OSRST__LEN 3
#define QMP6988_CTRLMEAS_REG_OSRSP__POS 2
#define QMP6988_CTRLMEAS_REG_OSRSP__MSK 0x1C
#define QMP6988_CTRLMEAS_REG_OSRSP__LEN 3
/* filter */
#define QMP6988_FILTERCOEFF_OFF 0x00
#define QMP6988_FILTERCOEFF_2 0x01
#define QMP6988_FILTERCOEFF_4 0x02
#define QMP6988_FILTERCOEFF_8 0x03
#define QMP6988_FILTERCOEFF_16 0x04
#define QMP6988_FILTERCOEFF_32 0x05
#define QMP6988_CONFIG_REG 0xF1 /*IIR filter co-efficient setting Register*/
#define QMP6988_CONFIG_REG_FILTER__POS 0
#define QMP6988_CONFIG_REG_FILTER__MSK 0x07
#define QMP6988_CONFIG_REG_FILTER__LEN 3
#define SUBTRACTOR 8388608
typedef struct _qmp6988_cali_data {
QMP6988_S32_t COE_a0;
QMP6988_S16_t COE_a1;
QMP6988_S16_t COE_a2;
QMP6988_S32_t COE_b00;
QMP6988_S16_t COE_bt1;
QMP6988_S16_t COE_bt2;
QMP6988_S16_t COE_bp1;
QMP6988_S16_t COE_b11;
QMP6988_S16_t COE_bp2;
QMP6988_S16_t COE_b12;
QMP6988_S16_t COE_b21;
QMP6988_S16_t COE_bp3;
} qmp6988_cali_data_t;
typedef struct _qmp6988_fk_data {
float a0, b00;
float a1, a2, bt1, bt2, bp1, b11, bp2, b12, b21, bp3;
} qmp6988_fk_data_t;
typedef struct _qmp6988_ik_data {
QMP6988_S32_t a0, b00;
QMP6988_S32_t a1, a2;
QMP6988_S64_t bt1, bt2, bp1, b11, bp2, b12, b21, bp3;
} qmp6988_ik_data_t;
typedef struct _qmp6988_data {
uint8_t slave;
uint8_t chip_id;
uint8_t power_mode;
float temperature;
float pressure;
float altitude;
qmp6988_cali_data_t qmp6988_cali;
qmp6988_ik_data_t ik;
} qmp6988_data_t;
class QMP6988 {
private:
qmp6988_data_t qmp6988;
uint8_t _addr;
I2C_Class _i2c;
// read calibration data from otp
int getCalibrationData();
QMP6988_S32_t getPressure02e(qmp6988_ik_data_t* ik, QMP6988_S32_t dp,
QMP6988_S16_t tx);
QMP6988_S16_t convTx02e(qmp6988_ik_data_t* ik, QMP6988_S32_t dt);
void reset();
public:
bool begin(TwoWire* wire = &Wire, uint8_t addr = QMP6988_SLAVE_ADDRESS_H,
uint8_t sda = 21, uint8_t scl = 22, long freq = 400000U);
bool update();
float pressure = 0;
float cTemp = 0;
float altitude = 0;
float calcAltitude(float pressure, float temp);
float calcPressure();
float calcTemperature();
void setpPowermode(int power_mode);
void setFilter(unsigned char filter);
void setOversamplingP(unsigned char oversampling_p);
void setOversamplingT(unsigned char oversampling_t);
};
#endif

View file

@ -0,0 +1,793 @@
/*
This is a library written for the SCD4X family of CO2 sensors
SparkFun sells these at its website: www.sparkfun.com
Do you like this library? Help support SparkFun. Buy a board!
https://www.sparkfun.com/products/18365
Written by Paul Clark @ SparkFun Electronics, June 2nd, 2021
The SCD41 measures CO2 from 400ppm to 5000ppm with an accuracy of +/- 40ppm +
5% of reading
This library handles the initialization of the SCD4X and outputs
CO2 levels, relative humidty, and temperature.
https://github.com/sparkfun/SparkFun_SCD4x_Arduino_Library
Development environment specifics:
Arduino IDE 1.8.13
SparkFun code, firmware, and software is released under the MIT License.
Please see LICENSE.md for more details.
*/
#include "SCD4X.h"
SCD4X::SCD4X(scd4x_sensor_type_e sensorType) {
// Constructor
_sensorType = sensorType;
}
bool SCD4X::begin(TwoWire *wire, uint8_t addr, uint8_t sda, uint8_t scl,
long freq, bool measBegin, bool autoCalibrate,
bool skipStopPeriodicMeasurements,
bool pollAndSetDeviceType) {
_i2c.begin(wire, sda, scl, freq);
_addr = addr;
_wire = wire;
if (!_i2c.exist(_addr)) {
return false;
}
bool success = true;
// If periodic measurements are already running, getSerialNumber will
// fail... To be safe, let's stop period measurements before we do anything
// else The user can override this by setting skipStopPeriodicMeasurements
// to true
if (skipStopPeriodicMeasurements == false)
success &= stopPeriodicMeasurement(); // Delays for 500ms...
char serialNumber[13]; // Serial number is 12 digits plus trailing NULL
success &= getSerialNumber(serialNumber); // Read the serial number. Return
// false if the CRC check fails.
if (pollAndSetDeviceType == true) {
scd4x_sensor_type_e sensorType;
success &= getFeatureSetVersion(&sensorType);
setSensorType(sensorType);
}
if (autoCalibrate ==
true) // Must be done before periodic measurements are started
{
success &= setAutomaticSelfCalibrationEnabled(true);
success &= (getAutomaticSelfCalibrationEnabled() == true);
} else {
success &= setAutomaticSelfCalibrationEnabled(false);
success &= (getAutomaticSelfCalibrationEnabled() == false);
}
if (measBegin == true) {
success &= startPeriodicMeasurement();
}
return (success);
}
bool SCD4X::update() {
return readMeasurement();
}
// Start periodic measurements. See 3.5.1
// signal update interval is 5 seconds.
bool SCD4X::startPeriodicMeasurement(void) {
if (periodicMeasurementsAreRunning) {
return (true); // Maybe this should be false?
}
bool success = sendCommand(SCD4x_COMMAND_START_PERIODIC_MEASUREMENT);
if (success) periodicMeasurementsAreRunning = true;
return (success);
}
// Stop periodic measurements. See 3.5.3
// Stop periodic measurement to change the sensor configuration or to save
// power. Note that the sensor will only respond to other commands after waiting
// 500 ms after issuing the stop_periodic_measurement command.
bool SCD4X::stopPeriodicMeasurement(uint16_t delayMillis, TwoWire &wirePort)
{
uint8_t i2cResult;
if (_wire != NULL) // If the sensor has been begun (_wire is not
// NULL) then _wire is used
{
_wire->beginTransmission(_addr);
_wire->write(SCD4x_COMMAND_STOP_PERIODIC_MEASUREMENT >> 8); // MSB
_wire->write(SCD4x_COMMAND_STOP_PERIODIC_MEASUREMENT & 0xFF); // LSB
i2cResult = _wire->endTransmission();
} else {
// If the sensor has not been begun (_wire is NULL) then wirePort
// is used (which will default to Wire)
wirePort.beginTransmission(_addr);
wirePort.write(SCD4x_COMMAND_STOP_PERIODIC_MEASUREMENT >> 8); // MSB
wirePort.write(SCD4x_COMMAND_STOP_PERIODIC_MEASUREMENT & 0xFF); // LSB
i2cResult = wirePort.endTransmission();
}
if (i2cResult == 0) {
periodicMeasurementsAreRunning = false;
if (delayMillis > 0) delay(delayMillis);
return (true);
}
return (false);
}
// Get 9 bytes from SCD4X. See 3.5.2
// Updates global variables with floats
// Returns true if data is read successfully
// Read sensor output. The measurement data can only be read out once per
// signal update interval as the buffer is emptied upon read-out. If no data
// is available in the buffer, the sensor returns a NACK. To avoid a NACK
// response, the get_data_ready_status can be issued to check data status
// (see chapter 3.8.2 for further details).
bool SCD4X::readMeasurement(void) {
// Verify we have data from the sensor
if (getDataReadyStatus() == false) return (false);
scd4x_unsigned16Bytes_t tempCO2;
tempCO2.unsigned16 = 0;
scd4x_unsigned16Bytes_t tempHumidity;
tempHumidity.unsigned16 = 0;
scd4x_unsigned16Bytes_t tempTemperature;
tempTemperature.unsigned16 = 0;
_wire->beginTransmission(_addr);
_wire->write(SCD4x_COMMAND_READ_MEASUREMENT >> 8); // MSB
_wire->write(SCD4x_COMMAND_READ_MEASUREMENT & 0xFF); // LSB
if (_wire->endTransmission() != 0) return (false); // Sensor did not ACK
delay(1); // Datasheet specifies this
_wire->requestFrom((uint8_t)_addr, (uint8_t)9);
bool error = false;
if (_wire->available()) {
byte bytesToCrc[2];
for (byte x = 0; x < 9; x++) {
byte incoming = _wire->read();
switch (x) {
case 0:
case 1:
tempCO2.bytes[x == 0 ? 1 : 0] =
incoming; // Store the two CO2 bytes in
// little-endian format
bytesToCrc[x] =
incoming; // Calculate the CRC on the two CO2 bytes
// in the order they arrive
break;
case 3:
case 4:
tempTemperature.bytes[x == 3 ? 1 : 0] =
incoming; // Store the two T bytes in little-endian
// format
bytesToCrc[x % 3] =
incoming; // Calculate the CRC on the two T bytes
// in the order they arrive
break;
case 6:
case 7:
tempHumidity.bytes[x == 6 ? 1 : 0] =
incoming; // Store the two RH bytes in
// little-endian format
bytesToCrc[x % 3] =
incoming; // Calculate the CRC on the two RH bytes
// in the order they arrive
break;
default: // x == 2, 5, 8
// Validate CRC
uint8_t foundCrc = computeCRC8(
bytesToCrc, 2); // Calculate what the CRC should be
// for these two bytes
if (foundCrc != incoming) // Does this match the CRC
// byte from the sensor?
{
error = true;
}
break;
}
}
} else {
return (false);
}
if (error) {
return (false);
}
// Now copy the int16s into their associated floats
co2 = (float)tempCO2.unsigned16;
temperature = -45 + (((float)tempTemperature.unsigned16) * 175 / 65536);
humidity = ((float)tempHumidity.unsigned16) * 100 / 65536;
// Mark our global variables as fresh
co2HasBeenReported = false;
humidityHasBeenReported = false;
temperatureHasBeenReported = false;
return (true); // Success! New data available in globals.
}
// Returns the latest available CO2 level
// If the current level has already been reported, trigger a new read
uint16_t SCD4X::getCO2(void) {
if (co2HasBeenReported == true) // Trigger a new read
readMeasurement(); // Pull in new co2, humidity, and temp into
// global vars
co2HasBeenReported = true;
return (uint16_t)co2; // Cut off decimal as co2 is 0 to 10,000
}
// Returns the latest available humidity
// If the current level has already been reported, trigger a new read
float SCD4X::getHumidity(void) {
if (humidityHasBeenReported == true) // Trigger a new read
readMeasurement(); // Pull in new co2, humidity, and temp into
// global vars
humidityHasBeenReported = true;
return humidity;
}
// Returns the latest available temperature
// If the current level has already been reported, trigger a new read
float SCD4X::getTemperature(void) {
if (temperatureHasBeenReported == true) // Trigger a new read
readMeasurement(); // Pull in new co2, humidity, and temp into
// global vars
temperatureHasBeenReported = true;
return temperature;
}
// Set the temperature offset (C). See 3.6.1
// Max command duration: 1ms
// The user can set delayMillis to zero f they want the function to return
// immediately. The temperature offset has no influence on the SCD4X CO2
// accuracy. Setting the temperature offset of the SCD4X inside the customer
// device correctly allows the user to leverage the RH and T output signal.
bool SCD4X::setTemperatureOffset(float offset, uint16_t delayMillis) {
if (periodicMeasurementsAreRunning) {
return (false);
}
if (offset < 0) {
return (false);
}
if (offset >= 175) {
return (false);
}
uint16_t offsetWord =
(uint16_t)(offset * 65536 / 175); // Toffset [°C] * 2^16 / 175
bool success =
sendCommand(SCD4x_COMMAND_SET_TEMPERATURE_OFFSET, offsetWord);
if (delayMillis > 0) delay(delayMillis);
return (success);
}
// Get the temperature offset. See 3.6.2
float SCD4X::getTemperatureOffset(void) {
if (periodicMeasurementsAreRunning) {
return (0.0);
}
float offset;
getTemperatureOffset(&offset);
return (offset);
}
// Get the temperature offset. See 3.6.2
bool SCD4X::getTemperatureOffset(float *offset) {
if (periodicMeasurementsAreRunning) {
return (false);
}
uint16_t offsetWord = 0; // offset will be zero if readRegister fails
bool success =
readRegister(SCD4x_COMMAND_GET_TEMPERATURE_OFFSET, &offsetWord, 1);
*offset = ((float)offsetWord) * 175.0 / 65536.0;
return (success);
}
// Set the sensor altitude (metres above sea level). See 3.6.3
// Max command duration: 1ms
// The user can set delayMillis to zero if they want the function to return
// immediately. Reading and writing of the sensor altitude must be done
// while the SCD4X is in idle mode. Typically, the sensor altitude is set
// once after device installation. To save the setting to the EEPROM, the
// persist setting (see chapter 3.9.1) command must be issued. Per default,
// the sensor altitude is set to 0 meter above sea-level.
bool SCD4X::setSensorAltitude(uint16_t altitude, uint16_t delayMillis) {
if (periodicMeasurementsAreRunning) {
return (false);
}
bool success = sendCommand(SCD4x_COMMAND_SET_SENSOR_ALTITUDE, altitude);
if (delayMillis > 0) delay(delayMillis);
return (success);
}
// Get the sensor altitude. See 3.6.4
uint16_t SCD4X::getSensorAltitude(void) {
if (periodicMeasurementsAreRunning) {
return (0);
}
uint16_t altitude = 0;
getSensorAltitude(&altitude);
return (altitude);
}
// Get the sensor altitude. See 3.6.4
bool SCD4X::getSensorAltitude(uint16_t *altitude) {
if (periodicMeasurementsAreRunning) {
return (false);
}
return (readRegister(SCD4x_COMMAND_GET_SENSOR_ALTITUDE, altitude, 1));
}
// Set the ambient pressure (Pa). See 3.6.5
// Max command duration: 1ms
// The user can set delayMillis to zero if they want the function to return
// immediately. The set_ambient_pressure command can be sent during periodic
// measurements to enable continuous pressure compensation.
// setAmbientPressure overrides setSensorAltitude
bool SCD4X::setAmbientPressure(float pressure, uint16_t delayMillis) {
if (pressure < 0) {
return (false);
}
if (pressure > 6553500) {
return (false);
}
uint16_t pressureWord = (uint16_t)(pressure / 100);
bool success =
sendCommand(SCD4x_COMMAND_SET_AMBIENT_PRESSURE, pressureWord);
if (delayMillis > 0) delay(delayMillis);
return (success);
}
// Perform forced recalibration. See 3.7.1
float SCD4X::performForcedRecalibration(uint16_t concentration) {
if (periodicMeasurementsAreRunning) {
return (0.0);
}
float correction = 0.0;
performForcedRecalibration(concentration, &correction);
return (correction);
}
// Perform forced recalibration. See 3.7.1
// To successfully conduct an accurate forced recalibration, the following
// steps need to be carried out:
// 1. Operate the SCD4X in the operation mode later used in normal sensor
// operation (periodic measurement,
// low power periodic measurement or single shot) for > 3 minutes in an
// environment with homogenous and constant CO2 concentration.
// 2. Issue stop_periodic_measurement. Wait 500 ms for the stop command to
// complete.
// 3. Subsequently issue the perform_forced_recalibration command and
// optionally read out the FRC correction
// (i.e. the magnitude of the correction) after waiting for 400 ms for
// the command to complete.
// A return value of 0xffff indicates that the forced recalibration has
// failed.
bool SCD4X::performForcedRecalibration(uint16_t concentration,
float *correction) {
if (periodicMeasurementsAreRunning) {
return (false);
}
uint16_t correctionWord;
bool success =
sendCommand(SCD4x_COMMAND_PERFORM_FORCED_CALIBRATION, concentration);
if (success == false) return (false);
delay(400); // Datasheet specifies this
_wire->requestFrom((uint8_t)_addr, (uint8_t)3);
bool error = false;
if (_wire->available()) {
byte bytesToCrc[2];
bytesToCrc[0] = _wire->read();
correctionWord = ((uint16_t)bytesToCrc[0]) << 8;
bytesToCrc[1] = _wire->read();
correctionWord |= (uint16_t)bytesToCrc[1];
byte incomingCrc = _wire->read();
uint8_t foundCrc = computeCRC8(bytesToCrc, 2);
if (foundCrc != incomingCrc) {
error = true;
}
} else {
return (false);
}
if (error) {
return (false);
}
*correction = ((float)correctionWord) -
32768; // FRC correction [ppm CO2] = word[0] 0x8000
if (correctionWord == 0xffff) // A return value of 0xffff indicates that
// the forced recalibration has failed
return (false);
return (true);
}
// Enable/disable automatic self calibration. See 3.7.2
// Set the current state (enabled / disabled) of the automatic
// self-calibration. By default, ASC is enabled. To save the setting to the
// EEPROM, the persist_setting (see chapter 3.9.1) command must be issued.
bool SCD4X::setAutomaticSelfCalibrationEnabled(bool enabled,
uint16_t delayMillis) {
if (periodicMeasurementsAreRunning) {
return (false);
}
uint16_t enabledWord = enabled == true ? 0x0001 : 0x0000;
bool success = sendCommand(
SCD4x_COMMAND_SET_AUTOMATIC_SELF_CALIBRATION_ENABLED, enabledWord);
if (delayMillis > 0) delay(delayMillis);
return (success);
}
// Check if automatic self calibration is enabled. See 3.7.3
bool SCD4X::getAutomaticSelfCalibrationEnabled(void) {
if (periodicMeasurementsAreRunning) {
return (false);
}
uint16_t enabled;
bool success = getAutomaticSelfCalibrationEnabled(&enabled);
if (success == false) {
return (false);
}
return (enabled == 0x0001);
}
// Check if automatic self calibration is enabled. See 3.7.3
bool SCD4X::getAutomaticSelfCalibrationEnabled(uint16_t *enabled) {
if (periodicMeasurementsAreRunning) {
return (false);
}
return (readRegister(SCD4x_COMMAND_GET_AUTOMATIC_SELF_CALIBRATION_ENABLED,
enabled, 1));
}
// Start low power periodic measurements. See 3.8.1
// Signal update interval will be 30 seconds instead of 5
bool SCD4X::startLowPowerPeriodicMeasurement(void) {
if (periodicMeasurementsAreRunning) {
return (false);
}
bool success =
sendCommand(SCD4x_COMMAND_START_LOW_POWER_PERIODIC_MEASUREMENT);
if (success) periodicMeasurementsAreRunning = true;
return (success);
}
// Returns true when data is available. See 3.8.2
bool SCD4X::getDataReadyStatus(void) {
uint16_t response;
bool success =
readRegister(SCD4x_COMMAND_GET_DATA_READY_STATUS, &response, 1);
if (success == false) return (false);
// If the least significant 11 bits of word[0] are 0 → data not ready
// else → data ready for read-out
if ((response & 0x07ff) == 0x0000) return (false);
return (true);
}
// Persist settings: copy settings (e.g. temperature offset) from RAM to
// EEPROM. See 3.9.1 Configuration settings such as the temperature offset,
// sensor altitude and the ASC enabled/disabled parameter are by default
// stored in the volatile memory (RAM) only and will be lost after a
// power-cycle. The persist_settings command stores the current
// configuration in the EEPROM of the SCD4X, making them persistent across
// power-cycling. To avoid unnecessary wear of the EEPROM, the
// persist_settings command should only be sent when persistence is required
// and if actual changes to the configuration have been made. The EEPROM is
// guaranteed to endure at least 2000 write cycles before failure.
bool SCD4X::persistSettings(uint16_t delayMillis) {
if (periodicMeasurementsAreRunning) {
return (false);
}
bool success = sendCommand(SCD4x_COMMAND_PERSIST_SETTINGS);
if (delayMillis > 0) delay(delayMillis);
return (success);
}
// Get 9 bytes from SCD4X. Convert 48-bit serial number to ASCII chars.
// See 3.9.2 Returns true if serial number is read successfully Reading out
// the serial number can be used to identify the chip and to verify the
// presence of the sensor.
bool SCD4X::getSerialNumber(char *serialNumber) {
if (periodicMeasurementsAreRunning) {
return (false);
}
_wire->beginTransmission(_addr);
_wire->write(SCD4x_COMMAND_GET_SERIAL_NUMBER >> 8); // MSB
_wire->write(SCD4x_COMMAND_GET_SERIAL_NUMBER & 0xFF); // LSB
if (_wire->endTransmission() != 0) return (false); // Sensor did not ACK
delay(1); // Datasheet specifies this
_wire->requestFrom((uint8_t)_addr, (uint8_t)9);
bool error = false;
if (_wire->available()) {
byte bytesToCrc[2];
int digit = 0;
for (byte x = 0; x < 9; x++) {
byte incoming = _wire->read();
switch (x) {
case 0: // The serial number arrives as: two bytes, CRC,
// two bytes, CRC, two bytes, CRC
case 1:
case 3:
case 4:
case 6:
case 7:
serialNumber[digit++] = convertHexToASCII(
incoming >> 4); // Convert each nibble to ASCII
serialNumber[digit++] = convertHexToASCII(incoming & 0x0F);
bytesToCrc[x % 3] = incoming;
break;
default: // x == 2, 5, 8
// Validate CRC
uint8_t foundCrc = computeCRC8(
bytesToCrc, 2); // Calculate what the CRC should be
// for these two bytes
if (foundCrc != incoming) // Does this match the CRC
// byte from the sensor?
{
error = true;
}
break;
}
serialNumber[digit] = 0; // NULL-terminate the string
}
} else {
return (false);
}
if (error) {
return (false);
}
return (true); // Success!
}
// PRIVATE: Convert serial number digit to ASCII
char SCD4X::convertHexToASCII(uint8_t digit) {
if (digit <= 9)
return (char(digit + 0x30));
else
return (char(digit + 0x41 - 10)); // Use upper case for A-F
}
// Perform self test. Takes 10 seconds to complete. See 3.9.3
// The perform_self_test feature can be used as an end-of-line test to check
// sensor functionality and the customer power supply to the sensor.
bool SCD4X::performSelfTest(void) {
if (periodicMeasurementsAreRunning) {
return (false);
}
uint16_t response;
bool success =
readRegister(SCD4x_COMMAND_PERFORM_SELF_TEST, &response, 10000);
return (success &&
(response == 0x0000)); // word[0] = 0 → no malfunction detected
}
// Peform factory reset. See 3.9.4
// The perform_factory_reset command resets all configuration settings
// stored in the EEPROM and erases the FRC and ASC algorithm history.
bool SCD4X::performFactoryReset(uint16_t delayMillis) {
if (periodicMeasurementsAreRunning) {
return (false);
}
bool success = sendCommand(SCD4x_COMMAND_PERFORM_FACTORY_RESET);
if (delayMillis > 0) delay(delayMillis);
return (success);
}
// Reinit. See 3.9.5
// The reinit command reinitializes the sensor by reloading user settings
// from EEPROM. Before sending the reinit command, the stop measurement
// command must be issued. If the reinit command does not trigger the
// desired re-initialization, a power-cycle should be applied to the SCD4X.
bool SCD4X::reInit(uint16_t delayMillis) {
if (periodicMeasurementsAreRunning) {
return (false);
}
bool success = sendCommand(SCD4x_COMMAND_REINIT);
if (delayMillis > 0) delay(delayMillis);
return (success);
}
// Low Power Single Shot. See 3.10.1
// In addition to periodic measurement modes, the SCD41 features a single
// shot measurement mode, i.e. allows for on-demand measurements. The
// typical communication sequence is as follows:
// 1. The sensor is powered up.
// 2. The I2C master sends a single shot command and waits for the indicated
// max. command duration time.
// 3. The I2C master reads out data with the read measurement sequence
// (chapter 3.5.2).
// 4. Steps 2-3 are repeated as required by the application.
bool SCD4X::measureSingleShot(void) {
if (_sensorType != SCD4x_SENSOR_SCD41) {
return (false);
}
if (periodicMeasurementsAreRunning) {
return (false);
}
bool success = sendCommand(SCD4x_COMMAND_MEASURE_SINGLE_SHOT);
return (success);
}
// On-demand measurement of relative humidity and temperature only.
// The sensor output is read using the read_measurement command
// (chapter 3.5.2). CO2 output is returned as 0 ppm.
bool SCD4X::measureSingleShotRHTOnly(void) {
if (_sensorType != SCD4x_SENSOR_SCD41) {
return (false);
}
if (periodicMeasurementsAreRunning) {
return (false);
}
bool success = sendCommand(SCD4x_COMMAND_MEASURE_SINGLE_SHOT_RHT_ONLY);
return (success);
}
scd4x_sensor_type_e SCD4X::getSensorType(void) {
return _sensorType;
}
void SCD4X::setSensorType(scd4x_sensor_type_e sensorType) {
_sensorType = sensorType;
}
bool SCD4X::getFeatureSetVersion(scd4x_sensor_type_e *sensorType) {
if (periodicMeasurementsAreRunning) {
return (false);
}
uint16_t featureSet;
bool success =
readRegister(SCD4x_COMMAND_GET_FEATURE_SET_VERSION, &featureSet, 1);
uint8_t typeOfSensor = ((featureSet & 0x1000) >> 12);
if (typeOfSensor == 0) {
*sensorType = SCD4x_SENSOR_SCD40;
} else if (typeOfSensor == 1) {
*sensorType = SCD4x_SENSOR_SCD41;
} else {
*sensorType = SCD4x_SENSOR_INVALID;
success = false;
}
return (success);
}
// Sends a command along with arguments and CRC
bool SCD4X::sendCommand(uint16_t command, uint16_t arguments) {
uint8_t data[2];
data[0] = arguments >> 8;
data[1] = arguments & 0xFF;
uint8_t crc = computeCRC8(
data, 2); // Calc CRC on the arguments only, not the command
_wire->beginTransmission(_addr);
_wire->write(command >> 8); // MSB
_wire->write(command & 0xFF); // LSB
_wire->write(arguments >> 8); // MSB
_wire->write(arguments & 0xFF); // LSB
_wire->write(crc);
if (_wire->endTransmission() != 0) return (false); // Sensor did not ACK
return (true);
}
// Sends just a command, no arguments, no CRC
bool SCD4X::sendCommand(uint16_t command) {
_wire->beginTransmission(_addr);
_wire->write(command >> 8); // MSB
_wire->write(command & 0xFF); // LSB
if (_wire->endTransmission() != 0) return (false); // Sensor did not ACK
return (true);
}
// Gets two bytes from SCD4X plus CRC.
// Returns true if endTransmission returns zero _and_ the CRC check is valid
bool SCD4X::readRegister(uint16_t registerAddress, uint16_t *response,
uint16_t delayMillis) {
_wire->beginTransmission(_addr);
_wire->write(registerAddress >> 8); // MSB
_wire->write(registerAddress & 0xFF); // LSB
if (_wire->endTransmission() != 0) return (false); // Sensor did not ACK
delay(delayMillis);
_wire->requestFrom((uint8_t)_addr,
(uint8_t)3); // Request data and CRC
if (_wire->available()) {
uint8_t data[2];
data[0] = _wire->read();
data[1] = _wire->read();
uint8_t crc = _wire->read();
*response = (uint16_t)data[0] << 8 | data[1];
uint8_t expectedCRC = computeCRC8(data, 2);
if (crc == expectedCRC) // Return true if CRC check is OK
return (true);
}
return (false);
}
// Given an array and a number of bytes, this calculate CRC8 for those bytes
// CRC is only calc'd on the data portion (two bytes) of the four bytes
// being sent From:
// http://www.sunshine2k.de/articles/coding/crc/understanding_crc.html
// Tested with: http://www.sunshine2k.de/coding/javascript/crc/crc_js.html
// x^8+x^5+x^4+1 = 0x31
uint8_t SCD4X::computeCRC8(uint8_t data[], uint8_t len) {
uint8_t crc = 0xFF; // Init with 0xFF
for (uint8_t x = 0; x < len; x++) {
crc ^= data[x]; // XOR-in the next input byte
for (uint8_t i = 0; i < 8; i++) {
if ((crc & 0x80) != 0)
crc = (uint8_t)((crc << 1) ^ 0x31);
else
crc <<= 1;
}
}
return crc; // No output reflection
}

View file

@ -0,0 +1,233 @@
/*
This is a library written for the SCD4X family of CO2 sensors
SparkFun sells these at its website: www.sparkfun.com
Do you like this library? Help support SparkFun. Buy a board!
https://www.sparkfun.com/products/18365
Written by Paul Clark @ SparkFun Electronics, June 2nd, 2021
Revision by Alex Brudner @ SparkFun Electronics
The SCD41 measures CO2 from 400ppm to 5000ppm with an accuracy of +/- 40ppm +
5% of reading
This library handles the initialization of the SCD4X and outputs
CO2 levels, relative humidty, and temperature.
https://github.com/sparkfun/SparkFun_SCD4x_Arduino_Library
Development environment specifics:
Arduino IDE 1.8.13 and 2.1.0
SparkFun code, firmware, and software is released under the MIT License.
Please see LICENSE.md for more details.
*/
#ifndef __SCD4X_H__
#define __SCD4X_H__
// Uncomment the next #define if using an Teensy >= 3 or Teensy LC and want to
// use the dedicated I2C-Library for it Then you also have to include <i2c_t3.h>
// on your application instead of <Wire.h>
// #define USE_TEENSY3_I2C_LIB
// Uncomment the next #define to EXclude any debug logging from the code, by
// default debug logging code will be included
// #define SCD4x_ENABLE_DEBUGLOG 0 // OFF/disabled/excluded on demand
#include "Arduino.h"
#include "I2C_Class.h"
// The default I2C address for the SCD4X is 0x62.
#define SCD4X_I2C_ADDR 0x62
// Available commands
// Basic Commands
#define SCD4x_COMMAND_START_PERIODIC_MEASUREMENT 0x21b1
#define SCD4x_COMMAND_READ_MEASUREMENT 0xec05 // execution time: 1ms
#define SCD4x_COMMAND_STOP_PERIODIC_MEASUREMENT 0x3f86 // execution time: 500ms
// On-chip output signal compensation
#define SCD4x_COMMAND_SET_TEMPERATURE_OFFSET 0x241d // execution time: 1ms
#define SCD4x_COMMAND_GET_TEMPERATURE_OFFSET 0x2318 // execution time: 1ms
#define SCD4x_COMMAND_SET_SENSOR_ALTITUDE 0x2427 // execution time: 1ms
#define SCD4x_COMMAND_GET_SENSOR_ALTITUDE 0x2322 // execution time: 1ms
#define SCD4x_COMMAND_SET_AMBIENT_PRESSURE 0xe000 // execution time: 1ms
// Field calibration
#define SCD4x_COMMAND_PERFORM_FORCED_CALIBRATION \
0x362f // execution time: 400ms
#define SCD4x_COMMAND_SET_AUTOMATIC_SELF_CALIBRATION_ENABLED \
0x2416 // execution time: 1ms
#define SCD4x_COMMAND_GET_AUTOMATIC_SELF_CALIBRATION_ENABLED \
0x2313 // execution time: 1ms
// Low power
#define SCD4x_COMMAND_START_LOW_POWER_PERIODIC_MEASUREMENT 0x21ac
#define SCD4x_COMMAND_GET_DATA_READY_STATUS 0xe4b8 // execution time: 1ms
// Advanced features
#define SCD4x_COMMAND_PERSIST_SETTINGS 0x3615 // execution time: 800ms
#define SCD4x_COMMAND_GET_SERIAL_NUMBER 0x3682 // execution time: 1ms
#define SCD4x_COMMAND_PERFORM_SELF_TEST 0x3639 // execution time: 10000ms
#define SCD4x_COMMAND_PERFORM_FACTORY_RESET 0x3632 // execution time: 1200ms
#define SCD4x_COMMAND_REINIT 0x3646 // execution time: 20ms
#define SCD4x_COMMAND_GET_FEATURE_SET_VERSION 0x202F // execution time: 1ms
// Low power single shot - SCD41 only
#define SCD4x_COMMAND_MEASURE_SINGLE_SHOT 0x219d // execution time: 5000ms
#define SCD4x_COMMAND_MEASURE_SINGLE_SHOT_RHT_ONLY \
0x2196 // execution time: 50ms
typedef union {
int16_t signed16;
uint16_t unsigned16;
} scd4x_signedUnsigned16_t; // Avoid any ambiguity casting int16_t to uint16_t
typedef union {
uint16_t unsigned16;
uint8_t bytes[2];
} scd4x_unsigned16Bytes_t; // Make it easy to convert 2 x uint8_t to uint16_t
typedef enum {
SCD4x_SENSOR_SCD40 = 0,
SCD4x_SENSOR_SCD41,
SCD4x_SENSOR_INVALID
} scd4x_sensor_type_e;
class SCD4X {
private:
TwoWire *_wire;
uint8_t _addr;
I2C_Class _i2c;
public:
SCD4X(scd4x_sensor_type_e sensorType = SCD4x_SENSOR_SCD40);
bool begin(TwoWire *wire = &Wire, uint8_t addr = SCD4X_I2C_ADDR,
uint8_t sda = 21, uint8_t scl = 22, long freq = 400000U,
bool measBegin = true, bool autoCalibrate = true,
bool skipStopPeriodicMeasurements = false,
bool pollAndSetDeviceType = true
);
bool update(void);
bool startPeriodicMeasurement(void); // Signal update interval is 5 seconds
// stopPeriodicMeasurement can be called before .begin if required
// If the sensor has been begun (_i2cPort is not NULL) then _i2cPort is used
// If the sensor has not been begun (_i2cPort is NULL) then wirePort and
// address are used (which will default to Wire) Note that the sensor will
// only respond to other commands after waiting 500 ms after issuing the
// stop_periodic_measurement command.
bool stopPeriodicMeasurement(uint16_t delayMillis = 500,
TwoWire &wirePort = Wire);
bool readMeasurement(void); // Check for fresh data; store it. Returns true
// if fresh data is available
uint16_t getCO2(void); // Return the CO2 PPM. Automatically request fresh
// data is the data is 'stale'
float getHumidity(void); // Return the RH. Automatically request fresh data
// is the data is 'stale'
float getTemperature(void); // Return the temperature. Automatically
// request fresh data is the data is 'stale'
// Define how warm the sensor is compared to ambient, so RH and T are
// temperature compensated. Has no effect on the CO2 reading Default offset
// is 4C
bool setTemperatureOffset(
float offset,
uint16_t delayMillis = 1); // Returns true if I2C transfer was OK
float getTemperatureOffset(void); // Will return zero if offset is invalid
bool getTemperatureOffset(
float *offset); // Returns true if offset is valid
// Define the sensor altitude in metres above sea level, so RH and CO2 are
// compensated for atmospheric pressure Default altitude is 0m
bool setSensorAltitude(uint16_t altitude, uint16_t delayMillis = 1);
uint16_t getSensorAltitude(
void); // Will return zero if altitude is invalid
bool getSensorAltitude(
uint16_t *altitude); // Returns true if altitude is valid
// Define the ambient pressure in Pascals, so RH and CO2 are compensated for
// atmospheric pressure setAmbientPressure overrides setSensorAltitude
bool setAmbientPressure(float pressure, uint16_t delayMillis = 1);
float performForcedRecalibration(
uint16_t concentration); // Returns the FRC correction value
bool performForcedRecalibration(
uint16_t concentration,
float *correction); // Returns true if FRC is successful
bool setAutomaticSelfCalibrationEnabled(bool enabled = true,
uint16_t delayMillis = 1);
bool getAutomaticSelfCalibrationEnabled(void);
bool getAutomaticSelfCalibrationEnabled(uint16_t *enabled);
bool startLowPowerPeriodicMeasurement(
void); // Start low power measurements - receive data every 30 seconds
bool getDataReadyStatus(void); // Returns true if fresh data is available
bool persistSettings(
uint16_t delayMillis = 800); // Copy sensor settings from RAM to EEPROM
bool getSerialNumber(
char *serialNumber); // Returns true if serial number is read correctly
bool performSelfTest(void); // Takes 10 seconds to complete. Returns true
// if the test is successful
bool performFactoryReset(
uint16_t delayMillis =
1200); // Reset all settings to the factory values
bool reInit(uint16_t delayMillis =
20); // Re-initialize the sensor, load settings from EEPROM
bool measureSingleShot(void); // SCD41 only. Request a single measurement.
// Data will be ready in 5 seconds
bool measureSingleShotRHTOnly(void); // SCD41 only. Request RH and T data
// only. Data will be ready in 50ms
bool sendCommand(uint16_t command, uint16_t arguments);
bool sendCommand(uint16_t command);
bool readRegister(uint16_t registerAddress, uint16_t *response,
uint16_t delayMillis = 1);
uint8_t computeCRC8(uint8_t data[], uint8_t len);
bool getFeatureSetVersion(scd4x_sensor_type_e *sensorType);
scd4x_sensor_type_e getSensorType(
void); // Get the sensor type stored in the struct.
void setSensorType(
scd4x_sensor_type_e sensorType); // Set the sensor type for the device.
private:
// Sensor type
scd4x_sensor_type_e _sensorType;
// Global main datums
float co2 = 0;
float temperature = 0;
float humidity = 0;
// These track the staleness of the current data
// This allows us to avoid calling readMeasurement() every time individual
// datums are requested
bool co2HasBeenReported = true;
bool humidityHasBeenReported = true;
bool temperatureHasBeenReported = true;
// Keep track of whether periodic measurements are in progress
bool periodicMeasurementsAreRunning = false;
// Convert serial number digit to ASCII
char convertHexToASCII(uint8_t digit);
};
#endif

View file

@ -0,0 +1,43 @@
#include "SHT3X.h"
bool SHT3X::begin(TwoWire* wire, uint8_t addr, uint8_t sda, uint8_t scl,
long freq) {
_i2c.begin(wire, sda, scl, freq);
_addr = addr;
_wire = wire;
return _i2c.exist(_addr);
}
bool SHT3X::update() {
unsigned int data[6];
// Start I2C Transmission
_wire->beginTransmission(_addr);
// Send measurement command
_wire->write(0x2C);
_wire->write(0x06);
// Stop I2C transmission
if (_wire->endTransmission() != 0) return false;
delay(200);
// Request 6 bytes of data
_wire->requestFrom(_addr, (uint8_t)6);
// Read 6 bytes of data
// cTemp msb, cTemp lsb, cTemp crc, humidity msb, humidity lsb, humidity crc
for (int i = 0; i < 6; i++) {
data[i] = _wire->read();
};
delay(50);
if (_wire->available() != 0) return false;
// Convert the data
cTemp = ((((data[0] * 256.0) + data[1]) * 175) / 65535.0) - 45;
fTemp = (cTemp * 1.8) + 32;
humidity = ((((data[3] * 256.0) + data[4]) * 100) / 65535.0);
return true;
}

View file

@ -0,0 +1,25 @@
#ifndef __SHT3X_H_
#define __SHT3X_H_
#include "Arduino.h"
#include "I2C_Class.h"
#include "Wire.h"
#define SHT3X_I2C_ADDR 0x44
class SHT3X {
public:
bool begin(TwoWire* wire = &Wire, uint8_t addr = SHT3X_I2C_ADDR,
uint8_t sda = 21, uint8_t scl = 22, long freq = 400000U);
bool update(void);
float cTemp = 0;
float fTemp = 0;
float humidity = 0;
private:
TwoWire* _wire;
uint8_t _addr;
I2C_Class _i2c;
};
#endif

View file

@ -0,0 +1,96 @@
#include "SHT4X.h"
bool SHT4X::begin(TwoWire* wire, uint8_t addr, uint8_t sda, uint8_t scl,
long freq) {
_i2c.begin(wire, sda, scl, freq);
_addr = addr;
_wire = wire;
return _i2c.exist(_addr);
}
bool SHT4X::update() {
uint8_t readbuffer[6];
uint8_t cmd = SHT4x_NOHEAT_HIGHPRECISION;
uint16_t duration = 10;
if (_heater == SHT4X_NO_HEATER) {
if (_precision == SHT4X_HIGH_PRECISION) {
cmd = SHT4x_NOHEAT_HIGHPRECISION;
duration = 10;
}
if (_precision == SHT4X_MED_PRECISION) {
cmd = SHT4x_NOHEAT_MEDPRECISION;
duration = 5;
}
if (_precision == SHT4X_LOW_PRECISION) {
cmd = SHT4x_NOHEAT_LOWPRECISION;
duration = 2;
}
}
if (_heater == SHT4X_HIGH_HEATER_1S) {
cmd = SHT4x_HIGHHEAT_1S;
duration = 1100;
}
if (_heater == SHT4X_HIGH_HEATER_100MS) {
cmd = SHT4x_HIGHHEAT_100MS;
duration = 110;
}
if (_heater == SHT4X_MED_HEATER_1S) {
cmd = SHT4x_MEDHEAT_1S;
duration = 1100;
}
if (_heater == SHT4X_MED_HEATER_100MS) {
cmd = SHT4x_MEDHEAT_100MS;
duration = 110;
}
if (_heater == SHT4X_LOW_HEATER_1S) {
cmd = SHT4x_LOWHEAT_1S;
duration = 1100;
}
if (_heater == SHT4X_LOW_HEATER_100MS) {
cmd = SHT4x_LOWHEAT_100MS;
duration = 110;
}
_i2c.writeByte(_addr, cmd, 1);
delay(duration);
_wire->requestFrom(_addr, (uint8_t)6);
for (uint16_t i = 0; i < 6; i++) {
readbuffer[i] = _wire->read();
}
if (readbuffer[2] != crc8(readbuffer, 2) ||
readbuffer[5] != crc8(readbuffer + 3, 2)) {
return false;
}
float t_ticks = (uint16_t)readbuffer[0] * 256 + (uint16_t)readbuffer[1];
float rh_ticks = (uint16_t)readbuffer[3] * 256 + (uint16_t)readbuffer[4];
cTemp = -45 + 175 * t_ticks / 65535;
humidity = -6 + 125 * rh_ticks / 65535;
humidity = min(max(humidity, (float)0.0), (float)100.0);
return true;
}
void SHT4X::setPrecision(sht4x_precision_t prec) {
_precision = prec;
}
sht4x_precision_t SHT4X::getPrecision(void) {
return _precision;
}
void SHT4X::setHeater(sht4x_heater_t heat) {
_heater = heat;
}
sht4x_heater_t SHT4X::getHeater(void) {
return _heater;
}

View file

@ -0,0 +1,80 @@
#ifndef __SHT4X_H_
#define __SHT4X_H_
#include "Arduino.h"
#include "I2C_Class.h"
#include "Wire.h"
#include "utility.h"
#define SHT40_I2C_ADDR_44 0x44
#define SHT40_I2C_ADDR_45 0x45
#define SHT41_I2C_ADDR_44 0x44
#define SHT41_I2C_ADDR_45 0x45
#define SHT45_I2C_ADDR_44 0x44
#define SHT45_I2C_ADDR_45 0x45
#define SHT4x_DEFAULT_ADDR 0x44 /**< SHT4x I2C Address */
#define SHT4x_NOHEAT_HIGHPRECISION \
0xFD /**< High precision measurement, no heater */
#define SHT4x_NOHEAT_MEDPRECISION \
0xF6 /**< Medium precision measurement, no heater */
#define SHT4x_NOHEAT_LOWPRECISION \
0xE0 /**< Low precision measurement, no heater */
#define SHT4x_HIGHHEAT_1S \
0x39 /**< High precision measurement, high heat for 1 sec */
#define SHT4x_HIGHHEAT_100MS \
0x32 /**< High precision measurement, high heat for 0.1 sec */
#define SHT4x_MEDHEAT_1S \
0x2F /**< High precision measurement, med heat for 1 sec */
#define SHT4x_MEDHEAT_100MS \
0x24 /**< High precision measurement, med heat for 0.1 sec */
#define SHT4x_LOWHEAT_1S \
0x1E /**< High precision measurement, low heat for 1 sec */
#define SHT4x_LOWHEAT_100MS \
0x15 /**< High precision measurement, low heat for 0.1 sec */
#define SHT4x_READSERIAL 0x89 /**< Read Out of Serial Register */
#define SHT4x_SOFTRESET 0x94 /**< Soft Reset */
typedef enum {
SHT4X_HIGH_PRECISION,
SHT4X_MED_PRECISION,
SHT4X_LOW_PRECISION,
} sht4x_precision_t;
/** Optional pre-heater configuration setting */
typedef enum {
SHT4X_NO_HEATER,
SHT4X_HIGH_HEATER_1S,
SHT4X_HIGH_HEATER_100MS,
SHT4X_MED_HEATER_1S,
SHT4X_MED_HEATER_100MS,
SHT4X_LOW_HEATER_1S,
SHT4X_LOW_HEATER_100MS,
} sht4x_heater_t;
class SHT4X {
public:
bool begin(TwoWire* wire = &Wire, uint8_t addr = SHT40_I2C_ADDR_44,
uint8_t sda = 21, uint8_t scl = 22, long freq = 400000U);
bool update(void);
float cTemp = 0;
float humidity = 0;
void setPrecision(sht4x_precision_t prec);
sht4x_precision_t getPrecision(void);
void setHeater(sht4x_heater_t heat);
sht4x_heater_t getHeater(void);
private:
TwoWire* _wire;
uint8_t _addr;
I2C_Class _i2c;
sht4x_precision_t _precision = SHT4X_HIGH_PRECISION;
sht4x_heater_t _heater = SHT4X_NO_HEATER;
};
#endif

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,594 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_BMP280.cpp
@brief BMP280 Unit for M5UnitUnified
*/
#include "unit_BMP280.hpp"
#include <M5Utility.hpp>
#include <limits> // NaN
#include <array>
using namespace m5::utility::mmh3;
using namespace m5::unit::types;
using namespace m5::unit::bmp280;
using namespace m5::unit::bmp280::command;
namespace {
constexpr uint8_t CHIP_IDENTIFIER{0x58};
constexpr uint8_t RESET_VALUE{0xB6};
constexpr uint32_t NOT_MEASURED{0x800000};
constexpr PowerMode mode_table[] = {PowerMode::Sleep, PowerMode::Forced, PowerMode::Forced, // duplicated
PowerMode::Normal
};
constexpr Oversampling osrs_table[] = {
Oversampling::Skipped, Oversampling::X1, Oversampling::X2, Oversampling::X4, Oversampling::X8, Oversampling::X16,
Oversampling::X16, // duplicated
Oversampling::X16, // duplicated
};
constexpr Oversampling osrss_table[][2] = {
// Pressure, Temperature
{Oversampling::X1, Oversampling::X1}, {Oversampling::X2, Oversampling::X1}, {Oversampling::X4, Oversampling::X1},
{Oversampling::X8, Oversampling::X1}, {Oversampling::X16, Oversampling::X2},
};
constexpr Standby standby_table[] = {
Standby::Time0_5ms, Standby::Time62_5ms, Standby::Time125ms, Standby::Time250ms,
Standby::Time500ms, Standby::Time1sec, Standby::Time2sec, Standby::Time4sec,
};
constexpr uint32_t interval_table[] = {0, 62, 125, 250, 500, 1000, 2000, 4000};
constexpr Filter filter_table[] = {
Filter::Off, Filter::Coeff2, Filter::Coeff4, Filter::Coeff8, Filter::Coeff16,
};
struct UseCaseSetting {
OversamplingSetting osrss;
Filter filter;
Standby st;
};
constexpr UseCaseSetting uc_table[] = {
{OversamplingSetting::UltraHighResolution, Filter::Coeff4, Standby::Time62_5ms},
{OversamplingSetting::StandardResolution, Filter::Coeff16, Standby::Time0_5ms},
{OversamplingSetting::UltraLowPower, Filter::Off, Standby::Time4sec},
{OversamplingSetting::StandardResolution, Filter::Coeff4, Standby::Time125ms},
{OversamplingSetting::LowPower, Filter::Off, Standby::Time0_5ms},
{OversamplingSetting::UltraHighResolution, Filter::Coeff16, Standby::Time0_5ms},
};
struct CtrlMeas {
//
inline Oversampling osrs_p() const
{
return osrs_table[(value >> 2) & 0x07];
}
inline Oversampling osrs_t() const
{
return osrs_table[(value >> 5) & 0x07];
}
inline PowerMode mode() const
{
return mode_table[value & 0x03];
}
//
inline void osrs_p(const Oversampling os)
{
value = (value & ~(0x07 << 2)) | ((m5::stl::to_underlying(os) & 0x07) << 2);
}
inline void osrs_t(const Oversampling os)
{
value = (value & ~(0x07 << 5)) | ((m5::stl::to_underlying(os) & 0x07) << 5);
}
inline void mode(const PowerMode m)
{
value = (value & ~0x03) | (m5::stl::to_underlying(m) & 0x03);
}
uint8_t value{};
};
struct Config {
//
inline Standby standby() const
{
return standby_table[(value >> 5) & 0x07];
}
inline Filter filter() const
{
return filter_table[(value >> 2) & 0x07];
}
//
inline void standby(const Standby s)
{
value = (value & ~(0x07 << 5)) | ((m5::stl::to_underlying(s) & 0x07) << 5);
}
inline void filter(const Filter f)
{
value = (value & ~(0x07 << 2)) | ((m5::stl::to_underlying(f) & 0x07) << 2);
}
uint8_t value{};
};
struct Calculator {
inline float temperature(const int32_t adc_P, const int32_t adc_T, const Trimming* t)
{
return t ? compensate_temperature_f(adc_T, *t) : std::numeric_limits<float>::quiet_NaN();
}
inline float pressure(const int32_t adc_P, const int32_t adc_T, const Trimming* t)
{
if (!t) {
return std::numeric_limits<float>::quiet_NaN();
}
if (t_fine == 0) {
(void)compensate_temperature_f(adc_T, *t); // For t_fine
}
return compensate_pressure_f(adc_P, *t);
}
private:
float compensate_temperature(const int32_t adc_T, const Trimming& trim)
{
int32_t var1{}, var2{};
var1 = ((((adc_T >> 3) - ((int32_t)trim.dig_T1 << 1))) * ((int32_t)trim.dig_T2)) >> 11;
var2 = (((((adc_T >> 4) - ((int32_t)trim.dig_T1)) * ((adc_T >> 4) - ((int32_t)trim.dig_T1))) >> 12) *
((int32_t)trim.dig_T3)) >>
14;
t_fine = var1 + var2; // [*1]
float T = (t_fine * 5 + 128) >> 8;
return T * 0.01f;
}
float compensate_pressure(const int32_t adc_P, const Trimming& trim)
{
int64_t var1{}, var2{}, p{};
var1 = ((int64_t)t_fine) - 128000; // (*1) using it!
var2 = var1 * var1 * (int64_t)trim.dig_P6;
var2 = var2 + ((var1 * (int64_t)trim.dig_P5) << 17);
var2 = var2 + (((int64_t)trim.dig_P4) << 35);
var1 = ((var1 * var1 * (int64_t)trim.dig_P3) >> 8) + ((var1 * (int64_t)trim.dig_P2) << 12);
var1 = (((((int64_t)1) << 47) + var1)) * ((int64_t)trim.dig_P1) >> 33;
if (var1) {
p = 1048576 - adc_P;
p = (((p << 31) - var2) * 3125) / var1;
var1 = (((int64_t)trim.dig_P9) * (p >> 13) * (p >> 13)) >> 25;
var2 = (((int64_t)trim.dig_P8) * p) >> 19;
p = ((p + var1 + var2) >> 8) + (((int64_t)trim.dig_P7) << 4);
return p / 256.f;
}
return 0.0f;
}
float compensate_temperature_f(const int32_t adc_T, const Trimming& trim)
{
float var1{}, var2{}, T{};
var1 = (((float)adc_T) / 16384.0f - ((float)trim.dig_T1) / 1024.0f) * ((float)trim.dig_T2);
var2 = ((((float)adc_T) / 131072.0f - ((float)trim.dig_T1) / 8192.0f) *
(((float)adc_T) / 131072.0f - ((float)trim.dig_T1) / 8192.0f)) *
((float)trim.dig_T3);
t_fine = (int32_t)(var1 + var2); // [*2]
T = (var1 + var2) / 5120.0f;
return T;
}
float compensate_pressure_f(const int32_t adc_P, const Trimming& trim)
{
float var1{}, var2{}, P{};
var1 = ((float)t_fine / 2.0f) - 64000.0f; // (*2)
var2 = var1 * var1 * ((float)trim.dig_P6) / 32768.0f;
var2 = var2 + var1 * ((float)trim.dig_P5) * 2.0f;
var2 = (var2 / 4.0f) + (((float)trim.dig_P4) * 65536.0f);
var1 = (((float)trim.dig_P3) * var1 * var1 / 524288.0f + ((float)trim.dig_P2) * var1) / 524288.0f;
var1 = (1.0f + var1 / 32768.0f) * ((float)trim.dig_P1);
if (var1 == 0.0f) {
return 0;
}
P = 1048576.0f - (float)adc_P;
P = (P - (var2 / 4096.0f)) * 6250.0f / var1;
var1 = ((float)trim.dig_P9) * P * P / 2147483648.0f;
var2 = P * ((float)trim.dig_P8) / 32768.0f;
P = P + (var1 + var2 + ((float)trim.dig_P7)) / 16.0f;
return P;
}
////
int32_t t_fine{};
};
} // namespace
namespace m5 {
namespace unit {
namespace bmp280 {
float Data::celsius() const
{
int32_t adc_P = (int32_t)(((uint32_t)raw[0] << 16) | ((uint32_t)raw[1] << 8) | ((uint32_t)raw[2]));
int32_t adc_T = (int32_t)(((uint32_t)raw[3] << 16) | ((uint32_t)raw[4] << 8) | ((uint32_t)raw[5]));
Calculator c{};
// adc_T is NOT_MEASURED if orsr Skipped (Not measured)
return (adc_T != NOT_MEASURED) ? c.temperature(adc_P >> 4, adc_T >> 4, trimming)
: std::numeric_limits<float>::quiet_NaN();
}
float Data::fahrenheit() const
{
return celsius() * 9.0f / 5.0f + 32.f;
}
float Data::pressure() const
{
int32_t adc_P = (int32_t)(((uint32_t)raw[0] << 16) | ((uint32_t)raw[1] << 8) | ((uint32_t)raw[2]));
int32_t adc_T = (int32_t)(((uint32_t)raw[3] << 16) | ((uint32_t)raw[4] << 8) | ((uint32_t)raw[5]));
Calculator c{};
// adc_T/P is NOT_MEASURED if orsr Skipped (Not measured)
return (adc_T != NOT_MEASURED && adc_P != NOT_MEASURED) ? c.pressure(adc_P >> 4, adc_T >> 4, trimming)
: std::numeric_limits<float>::quiet_NaN();
}
} // namespace bmp280
const char UnitBMP280::name[] = "UnitBMP280";
const types::uid_t UnitBMP280::uid{"UnitBMP280"_mmh3};
const types::attr_t UnitBMP280::attr{attribute::AccessI2C};
bool UnitBMP280::begin()
{
auto ssize = stored_size();
assert(ssize && "stored_size must be greater than zero");
if (ssize != _data->capacity()) {
_data.reset(new m5::container::CircularBuffer<Data>(ssize));
if (!_data) {
M5_LIB_LOGE("Failed to allocate");
return false;
}
}
uint8_t id{};
if (!softReset() || !readRegister8(CHIP_ID, id, 0) || id != CHIP_IDENTIFIER) {
M5_LIB_LOGE("Can not detect BMP280 %02X", id);
return false;
}
if (!read_trimming(_trimming)) {
M5_LIB_LOGE("Failed to read trimming");
return false;
}
M5_LIB_LOGV(
"Trimming\n"
"T:%u/%d/%d\n"
"P:%u/%d/%d/%d/%d/%d/%d/%d/%d",
// T
_trimming.dig_T1, _trimming.dig_T2, _trimming.dig_T3,
// P
_trimming.dig_P1, _trimming.dig_P2, _trimming.dig_P3, _trimming.dig_P4, _trimming.dig_P5, _trimming.dig_P6,
_trimming.dig_P7, _trimming.dig_P8, _trimming.dig_P9);
return _cfg.start_periodic
? startPeriodicMeasurement(_cfg.osrs_pressure, _cfg.osrs_temperature, _cfg.filter, _cfg.standby)
: true;
}
void UnitBMP280::update(const bool force)
{
_updated = false;
if (inPeriodic()) {
elapsed_time_t at{m5::utility::millis()};
if (force || !_latest || at >= _latest + _interval) {
Data d{};
// _updated = is_data_ready() && read_measurement(d);
_updated = read_measurement(d);
if (_updated) {
// auto dur = at - _latest;
// M5_LIB_LOGW(">DUR:%ld\n", dur);
_latest = at;
_data->push_back(d);
}
}
}
}
bool UnitBMP280::start_periodic_measurement(const bmp280::Oversampling osrsPressure,
const bmp280::Oversampling osrsTemperature, const bmp280::Filter filter,
const bmp280::Standby st)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
Config c{};
c.standby(st);
c.filter(filter);
CtrlMeas cm{};
cm.osrs_p(osrsPressure);
cm.osrs_t(osrsTemperature);
return writeRegister8(CONFIG, c.value) && writeRegister8(CONTROL_MEASUREMENT, cm.value) &&
start_periodic_measurement();
}
bool UnitBMP280::start_periodic_measurement()
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
Config c{};
_periodic = readRegister8(CONFIG, c.value, 0) && writePowerMode(PowerMode::Normal);
if (_periodic) {
_latest = 0;
_interval = interval_table[m5::stl::to_underlying(c.standby())];
}
return _periodic;
}
bool UnitBMP280::stop_periodic_measurement()
{
if (inPeriodic() && writePowerMode(PowerMode::Sleep)) {
_periodic = false;
return true;
}
return false;
}
bool UnitBMP280::measureSingleshot(bmp280::Data& d, const bmp280::Oversampling osrsPressure,
const bmp280::Oversampling osrsTemperature, const bmp280::Filter filter)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (osrsTemperature == Oversampling::Skipped) {
return false;
}
Config c{};
c.filter(filter);
CtrlMeas cm{};
cm.osrs_p(osrsPressure);
cm.osrs_t(osrsTemperature);
return writeRegister8(CONFIG, c.value) && writeRegister8(CONTROL_MEASUREMENT, cm.value) && measure_singleshot(d);
}
bool UnitBMP280::measure_singleshot(bmp280::Data& d)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (writePowerMode(PowerMode::Forced)) {
auto start_at = m5::utility::millis();
auto timeout_at = start_at + 2 * 1000; // 2sec
bool done{};
do {
PowerMode pm{};
done = readPowerMode(pm) && (pm == PowerMode::Sleep) && is_data_ready();
if (done) {
break;
}
m5::utility::delay(1);
} while (!done && m5::utility::millis() <= timeout_at);
return done && read_measurement(d);
}
return false;
}
bool UnitBMP280::readOversampling(Oversampling& osrsPressure, Oversampling& osrsTemperature)
{
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
osrsPressure = cm.osrs_p();
osrsTemperature = cm.osrs_t();
return true;
}
return false;
}
bool UnitBMP280::writeOversampling(const Oversampling osrsPressure, const Oversampling osrsTemperature)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
cm.osrs_p(osrsPressure);
cm.osrs_t(osrsTemperature);
return writeRegister8(CONTROL_MEASUREMENT, cm.value);
}
return false;
}
bool UnitBMP280::writeOversamplingPressure(const Oversampling osrsPressure)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
cm.osrs_p(osrsPressure);
return writeRegister8(CONTROL_MEASUREMENT, cm.value);
}
return false;
}
bool UnitBMP280::writeOversamplingTemperature(const Oversampling osrsTemperature)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
cm.osrs_t(osrsTemperature);
return writeRegister8(CONTROL_MEASUREMENT, cm.value);
}
return false;
}
bool UnitBMP280::writeOversampling(const bmp280::OversamplingSetting osrss)
{
auto idx = m5::stl::to_underlying(osrss);
return writeOversampling(osrss_table[idx][0], osrss_table[idx][1]);
}
bool UnitBMP280::readPowerMode(PowerMode& m)
{
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
m = cm.mode();
return true;
}
return false;
}
bool UnitBMP280::writePowerMode(const PowerMode m)
{
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
cm.mode(m);
// Datasheet says
// If the device is currently performing ameasurement,
// execution of mode switching commands is delayed until the end of the currentlyrunning measurement period
bool can{};
auto timeout_at = m5::utility::millis() + 1000;
do {
can = is_data_ready();
if (can) {
break;
}
m5::utility::delay(1);
} while (!can && m5::utility::millis() <= timeout_at);
return can && writeRegister8(CONTROL_MEASUREMENT, cm.value);
}
return false;
}
bool UnitBMP280::readFilter(Filter& f)
{
Config c{};
if (readRegister8(CONFIG, c.value, 0)) {
f = c.filter();
return true;
}
return false;
}
bool UnitBMP280::writeFilter(const Filter& f)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
PowerMode pm{};
if (!readPowerMode(pm) || pm != PowerMode::Sleep) {
// Datasheet says
// Writes to the config register in normal mode may be ignored. In sleep mode writes are not ignored
M5_LIB_LOGE("Invalid power mode %02X", pm);
return false;
}
Config c{};
if (readRegister8(CONFIG, c.value, 0)) {
c.filter(f);
return writeRegister8(CONFIG, c.value);
}
return false;
}
bool UnitBMP280::readStandbyTime(Standby& s)
{
Config c{};
if (readRegister8(CONFIG, c.value, 0)) {
s = c.standby();
return true;
}
return false;
}
bool UnitBMP280::writeStandbyTime(const Standby s)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
Config c{};
if (readRegister8(CONFIG, c.value, 0)) {
c.standby(s);
return writeRegister8(CONFIG, c.value);
}
return false;
}
bool UnitBMP280::writeUseCaseSetting(const bmp280::UseCase uc)
{
const auto& tbl = uc_table[m5::stl::to_underlying(uc)];
return writeOversampling(tbl.osrss) && writeFilter(tbl.filter) && writeStandbyTime(tbl.st);
}
bool UnitBMP280::softReset()
{
if (writeRegister8(SOFT_RESET, RESET_VALUE)) {
auto timeout_at = m5::utility::millis() + 100; // 100ms
uint8_t s{0xFF};
do {
if (readRegister8(GET_STATUS, s, 0) && (s & 0x01 /* im update */) == 0x00) {
_periodic = false;
return true;
}
} while ((s & 0x01) && m5::utility::millis() < timeout_at);
return false;
}
return false;
}
//
bool UnitBMP280::read_trimming(Trimming& t)
{
return readRegister(TRIMMING_DIG, t.value, m5::stl::size(t.value), 0);
}
bool UnitBMP280::is_data_ready()
{
uint8_t s{0xFF};
return readRegister8(GET_STATUS, s, 0) && ((s & 0x09 /* Measuring, im update */) == 0x00);
}
bool UnitBMP280::read_measurement(bmp280::Data& d)
{
d.trimming = nullptr;
// Datasheet says
// Shadowing will only work if all data registers are read in a single burst read.
// Therefore, the user must use burst reads if he does not synchronize data readout with themeasurement cycle
if (readRegister(GET_MEASUREMENT, d.raw.data(), d.raw.size(), 0)) {
d.trimming = &_trimming;
return true;
}
return false;
}
} // namespace unit
} // namespace m5

View file

@ -0,0 +1,434 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_BMP280.hpp
@brief BMP280 Unit for M5UnitUnified
*/
#ifndef M5_UNIT_ENV_UNIT_BNP280_HPP
#define M5_UNIT_ENV_UNIT_BNP280_HPP
#include <M5UnitComponent.hpp>
#include <m5_utility/container/circular_buffer.hpp>
#include <limits> // NaN
namespace m5 {
namespace unit {
/*!
@namespace bmp280
@brief For BMP280
*/
namespace bmp280 {
/*!
@enum PowerMode
@brief Operation mode
*/
enum class PowerMode : uint8_t {
Sleep, //!< No measurements are performed
Forced, //!< Single measurements are performed
Normal = 0x03, //!< Periodic measurements are performed
};
/*!
@enum Oversampling
@brief Oversampling factor
*/
enum class Oversampling : uint8_t {
Skipped, //!< Skipped (No measurements are performed)
X1, //!< x1
X2, //!< x2
X4, //!< x4
X8, //!< x8
X16, //!< x16
};
/*!
@enum OversamplingSetting
@brief Oversampling Settings
*/
enum class OversamplingSetting : uint8_t {
UltraLowPower, //!< 16 bit / 2.62 Pa, 16 bit / 0.0050 C
LowPower, //!< 17 bit / 1.31 Pa, 16 bit / 0.0050 C
StandardResolution, //!< 18 bit / 0.66 Pa, 16 bit / 0.0050 C
HighResolution, //!< 19 bit / 0.33 Pa, 16 bit / 0.0050 C
UltraHighResolution, //!< 20 bit / 0.16 Pa, 17 bit / 0.0025 C
};
/*!
@enum Filter
@brief Filter setting
*/
enum class Filter : uint8_t {
Off, //!< Off filter
Coeff2, //!< co-efficient 2
Coeff4, //!< co-efficient 4
Coeff8, //!< co-efficient 8
Coeff16, //!< co-efficient 16
};
/*!
@enum Standby
@brief Measurement standby time for power mode Normal
*/
enum class Standby : uint8_t {
Time0_5ms, //!< 0.5 ms
Time62_5ms, //!< 62.5 ms
Time125ms, //!< 125 ms
Time250ms, //!< 250 ms
Time500ms, //!< 500 ms
Time1sec, //!< 1 second
Time2sec, //!< 2 seconds
Time4sec, //!< 4 seconds
};
/*!
@enum UseCase
@brief Preset settings
*/
enum class UseCase : uint8_t {
LowPower, //!< Handheld device low-power
Dynamic, //!< Handheld device dynamic
Weather, //!< Weather monitoring
Elevator, //!< Elevator / floor change detection
Drop, //!< Drop detection
Indoor, //!< Indoor navigation
};
/*!
@union Trimmming
@brief Trimming parameter
*/
union Trimming {
uint8_t value[12 * 2]{};
struct {
//
uint16_t dig_T1;
int16_t dig_T2;
int16_t dig_T3;
//
uint16_t dig_P1;
int16_t dig_P2;
int16_t dig_P3;
int16_t dig_P4;
int16_t dig_P5;
int16_t dig_P6;
int16_t dig_P7;
int16_t dig_P8;
int16_t dig_P9;
// uint16_t reserved;
} __attribute__((packed));
};
/*!
@struct Data
@brief Measurement data group
*/
struct Data {
std::array<uint8_t, 6> raw{}; //!< RAW data [0,1,2]:pressure [3,4,5]:temperature
const Trimming* trimming{}; //!< For calculate
//! temperature (Celsius)
inline float temperature() const
{
return celsius();
}
float celsius() const; //!< temperature (Celsius)
float fahrenheit() const; //!< temperature (Fahrenheit)
float pressure() const; //!< pressure (Pa)
};
} // namespace bmp280
/*!
@class UnitBMP280
@brief Pressure and temperature sensor unit
*/
class UnitBMP280 : public Component, public PeriodicMeasurementAdapter<UnitBMP280, bmp280::Data> {
M5_UNIT_COMPONENT_HPP_BUILDER(UnitBMP280, 0x76);
public:
/*!
@struct config_t
@brief Settings for begin
*/
struct config_t {
//! Start periodic measurement on begin?
bool start_periodic{true};
//! Pressure oversampling if start on begin
bmp280::Oversampling osrs_pressure{bmp280::Oversampling::X16};
//! Temperature oversampling if start on begin
bmp280::Oversampling osrs_temperature{bmp280::Oversampling::X2};
//! Filter if start on begin
bmp280::Filter filter{bmp280::Filter::Coeff16};
//! Standby time if start on begin
bmp280::Standby standby{bmp280::Standby::Time1sec};
};
explicit UnitBMP280(const uint8_t addr = DEFAULT_ADDRESS)
: Component(addr), _data{new m5::container::CircularBuffer<bmp280::Data>(1)}
{
auto ccfg = component_config();
ccfg.clock = 400 * 1000U;
component_config(ccfg);
}
virtual ~UnitBMP280()
{
}
virtual bool begin() override;
virtual void update(const bool force = false) override;
///@name Settings for begin
///@{
/*! @brief Gets the configration */
inline config_t config()
{
return _cfg;
}
//! @brief Set the configration
inline void config(const config_t& cfg)
{
_cfg = cfg;
}
///@}
///@name Measurement data by periodic
///@{
//! @brief Oldest measured temperature (Celsius)
inline float temperature() const
{
return !empty() ? oldest().temperature() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured temperature (Celsius)
inline float celsius() const
{
return !empty() ? oldest().celsius() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured temperature (Fahrenheit)
inline float fahrenheit() const
{
return !empty() ? oldest().fahrenheit() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured pressure (Pa)
inline float pressure() const
{
return !empty() ? oldest().pressure() : std::numeric_limits<float>::quiet_NaN();
}
///@}
///@name Periodic measurement
///@{
/*!
@brief Start periodic measurement
@param osrsPressure Oversampling factor for pressure
@param osrsTemperature Oversampling factor for temperature
@param filter Filter coeff
@param st Standby time
@return True if successful
@warning Measuring pressure requires measuring temperature
*/
inline bool startPeriodicMeasurement(const bmp280::Oversampling osrsPressure,
const bmp280::Oversampling osrsTemperature, const bmp280::Filter filter,
const bmp280::Standby st)
{
return PeriodicMeasurementAdapter<UnitBMP280, bmp280::Data>::startPeriodicMeasurement(
osrsPressure, osrsTemperature, filter, st);
}
//! @brief Start periodic measurement using current settings
inline bool startPeriodicMeasurement()
{
return PeriodicMeasurementAdapter<UnitBMP280, bmp280::Data>::startPeriodicMeasurement();
}
/*!
@brief Stop periodic measurement
@return True if successful
*/
inline bool stopPeriodicMeasurement()
{
return PeriodicMeasurementAdapter<UnitBMP280, bmp280::Data>::stopPeriodicMeasurement();
}
///@}
///@name Single shot measurement
///@{
/*!
@brief Measurement single shot
@param[out] data Measuerd data
@param osrsPressure Oversampling factor for pressure
@param osrsTemperature Oversampling factor for temperature
@param filter Filter coeff
@return True if successful
@warning During periodic detection runs, an error is returned
@warning Measuring pressure requires measuring temperature
@warning Each setting is overwritten
*/
bool measureSingleshot(bmp280::Data& d, const bmp280::Oversampling osrsPressure,
const bmp280::Oversampling osrsTemperature, const bmp280::Filter filter);
//! @brief Measurement single shot using current settings
inline bool measureSingleshot(bmp280::Data& d)
{
return measure_singleshot(d);
}
///@}
///@name Settings
///@{
/*!
@brief Read the oversampling conditions
@param[out] osrsPressure Oversampling for pressure
@param[out] osrsTemperature Oversampling for temperature
@return True if successful
*/
bool readOversampling(bmp280::Oversampling& osrsPressure, bmp280::Oversampling& osrsTemperature);
/*!
@brief Write the oversampling conditions
@param osrsPressure Oversampling for pressure
@param osrsTemperature Oversampling for temperature
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeOversampling(const bmp280::Oversampling osrsPressure, const bmp280::Oversampling osrsTemperature);
/*!
@brief Write the oversampling conditions for pressure
@param osrsPressure Oversampling for pressure
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeOversamplingPressure(const bmp280::Oversampling osrsPressure);
/*!
@brief Write the oversampling conditions for temperature
@param osrsTemperature Oversampling for temperature
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeOversamplingTemperature(const bmp280::Oversampling osrsTemperature);
/*!
@brief Write the oversampling by OversamplingSetting
@param osrss OversamplingSetting
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeOversampling(const bmp280::OversamplingSetting osrss);
/*!
@brief Read the IIR filter co-efficient
@param[out] f filter
@return True if successful
*/
bool readFilter(bmp280::Filter& f);
/*!
@brief Write the IIR filter co-efficient
@param f filter
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeFilter(const bmp280::Filter& f);
/*!
@brief Read the standby time
@param[out] s standby time
@return True if successful
*/
bool readStandbyTime(bmp280::Standby& s);
/*!
@brief Write the standby time
@param s standby time
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeStandbyTime(const bmp280::Standby s);
/*!
@brief Read the power mode
@param[out] m Power mode
@return True if successful
*/
bool readPowerMode(bmp280::PowerMode& m);
/*!
@brief Write the power mode
@param m Power mode
@return True if successful
@warning Note that the measurement mode is changed
@warning It is recommended to use start/stopPeriodicMeasurement or similar to change the measurement mode
*/
bool writePowerMode(const bmp280::PowerMode m);
/*!
@brief Write the settings based on use cases
@param uc UseCase
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeUseCaseSetting(const bmp280::UseCase uc);
///@}
/*!
@brief Soft reset
@return True if successful
*/
bool softReset();
protected:
bool start_periodic_measurement(const bmp280::Oversampling osrsPressure, const bmp280::Oversampling osrsTemperature,
const bmp280::Filter filter, const bmp280::Standby st);
bool start_periodic_measurement();
bool stop_periodic_measurement();
bool read_measurement(bmp280::Data& d);
bool measure_singleshot(bmp280::Data& d);
bool read_trimming(bmp280::Trimming& t);
bool is_data_ready();
M5_UNIT_COMPONENT_PERIODIC_MEASUREMENT_ADAPTER_HPP_BUILDER(UnitBMP280, bmp280::Data);
protected:
std::unique_ptr<m5::container::CircularBuffer<bmp280::Data>> _data{};
config_t _cfg{};
bmp280::Trimming _trimming{};
};
///@cond
namespace bmp280 {
namespace command {
constexpr uint8_t CHIP_ID{0xD0};
// constexpr uint8_t CHIP_VERSION{0xD1};
constexpr uint8_t SOFT_RESET{0xE0};
constexpr uint8_t GET_STATUS{0xF3};
constexpr uint8_t CONTROL_MEASUREMENT{0xF4};
constexpr uint8_t CONFIG{0xF5};
constexpr uint8_t GET_MEASUREMENT{0XF7}; // 6bytes
constexpr uint8_t GET_PRESSURE{0xF7}; // 3byts
constexpr uint8_t GET_PRESSURE_MSB{0xF7}; // 7:0
constexpr uint8_t GET_PRESSURE_LSB{0xF8}; // 7:0
constexpr uint8_t GET_PRESSURE_XLSB{0xF9}; // 7:4
constexpr uint8_t GET_TEMPERATURE{0XFA}; // 3 bytes
constexpr uint8_t GET_TEMPERATURE_MSB{0XFA}; // 7:0
constexpr uint8_t GET_TEMPERATURE_LSB{0XFB}; // 7:0
constexpr uint8_t GET_TEMPERATURE_XLSB{0XFC}; // 7:4
constexpr uint8_t TRIMMING_DIG{0x88}; // 12 bytes
constexpr uint8_t TRIMMING_DIG_T1{0x88};
constexpr uint8_t TRIMMING_DIG_T2{0x8A};
constexpr uint8_t TRIMMING_DIG_T3{0x8C};
constexpr uint8_t TRIMMING_DIG_P1{0x8E};
constexpr uint8_t TRIMMING_DIG_P2{0x90};
constexpr uint8_t TRIMMING_DIG_P3{0x92};
constexpr uint8_t TRIMMING_DIG_P4{0x94};
constexpr uint8_t TRIMMING_DIG_P5{0x96};
constexpr uint8_t TRIMMING_DIG_P6{0x98};
constexpr uint8_t TRIMMING_DIG_P7{0x9A};
constexpr uint8_t TRIMMING_DIG_P8{0x9C};
constexpr uint8_t TRIMMING_DIG_P9{0x9A};
constexpr uint8_t TRIMMING_DIG_RESERVED{0xA0};
} // namespace command
} // namespace bmp280
///@endcond
} // namespace unit
} // namespace m5
#endif

View file

@ -0,0 +1,48 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_ENV3.cpp
@brief ENV III Unit for M5UnitUnified
*/
#include "unit_ENV3.hpp"
#include <M5Utility.hpp>
namespace m5 {
namespace unit {
using namespace m5::utility::mmh3;
using namespace m5::unit::types;
const char UnitENV3::name[] = "UnitENV3";
const types::uid_t UnitENV3::uid{"UnitENV3"_mmh3};
const types::attr_t UnitENV3::attr{attribute::AccessI2C};
UnitENV3::UnitENV3(const uint8_t addr) : Component(addr)
{
// Form a parent-child relationship
auto cfg = component_config();
cfg.max_children = 2;
component_config(cfg);
_valid = add(sht30, 0) && add(qmp6988, 1);
}
std::shared_ptr<Adapter> UnitENV3::ensure_adapter(const uint8_t ch)
{
if (ch > 2) {
M5_LIB_LOGE("Invalid channel %u", ch);
return std::make_shared<Adapter>(); // Empty adapter
}
auto unit = child(ch);
if (!unit) {
M5_LIB_LOGE("Not exists unit %u", ch);
return std::make_shared<Adapter>(); // Empty adapter
}
auto ad = asAdapter<AdapterI2C>(Adapter::Type::I2C);
return ad ? std::shared_ptr<Adapter>(ad->duplicate(unit->address())) : std::make_shared<Adapter>();
}
} // namespace unit
} // namespace m5

View file

@ -0,0 +1,53 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_ENV3.hpp
@brief ENV III Unit for M5UnitUnified
*/
#ifndef M5_UNIT_ENV_UNIT_ENV3_HPP
#define M5_UNIT_ENV_UNIT_ENV3_HPP
#include <M5UnitComponent.hpp>
#include "unit_SHT30.hpp"
#include "unit_QMP6988.hpp"
namespace m5 {
namespace unit {
/*!
@class UnitENV3
@brief ENV III is an environmental sensor that integrates SHT30 and QMP6988
@details This unit itself has no I/O, but holds SHT30 and QMP6988 instance
*/
class UnitENV3 : public Component {
// Must not be 0x00 for ensure and assign adapter to children
M5_UNIT_COMPONENT_HPP_BUILDER(UnitENV3, 0xFF /* Dummy address */);
public:
UnitSHT30 sht30; //!< @brief SHT30 instance
UnitQMP6988 qmp6988; //!< @brief QMP6988 instance
explicit UnitENV3(const uint8_t addr = DEFAULT_ADDRESS);
virtual ~UnitENV3()
{
}
virtual bool begin() override
{
return _valid;
}
protected:
virtual std::shared_ptr<Adapter> ensure_adapter(const uint8_t ch);
private:
bool _valid{}; // Did the constructor correctly add the child unit?
Component* _children[2]{&sht30, &qmp6988};
};
} // namespace unit
} // namespace m5
#endif

View file

@ -0,0 +1,48 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_ENV4.cpp
@brief ENV 4 Unit for M5UnitUnified
*/
#include "unit_ENV4.hpp"
#include <M5Utility.hpp>
namespace m5 {
namespace unit {
using namespace m5::utility::mmh3;
using namespace m5::unit::types;
const char UnitENV4::name[] = "UnitENV4";
const types::uid_t UnitENV4::uid{"UnitENV4"_mmh3};
const types::attr_t UnitENV4::attr{attribute::AccessI2C};
UnitENV4::UnitENV4(const uint8_t addr) : Component(addr)
{
// Form a parent-child relationship
auto cfg = component_config();
cfg.max_children = 2;
component_config(cfg);
_valid = add(sht40, 0) && add(bmp280, 1);
}
std::shared_ptr<Adapter> UnitENV4::ensure_adapter(const uint8_t ch)
{
if (ch > 2) {
M5_LIB_LOGE("Invalid channel %u", ch);
return std::make_shared<Adapter>(); // Empty adapter
}
auto unit = child(ch);
if (!unit) {
M5_LIB_LOGE("Not exists unit %u", ch);
return std::make_shared<Adapter>(); // Empty adapter
}
auto ad = asAdapter<AdapterI2C>(Adapter::Type::I2C);
return ad ? std::shared_ptr<Adapter>(ad->duplicate(unit->address())) : std::make_shared<Adapter>();
}
} // namespace unit
} // namespace m5

View file

@ -0,0 +1,54 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_ENV4.hpp
@brief ENV IV Unit for M5UnitUnified
*/
#ifndef M5_UNIT_ENV_UNIT_ENV4_HPP
#define M5_UNIT_ENV_UNIT_ENV4_HPP
#include <M5UnitComponent.hpp>
#include <array>
#include "unit_SHT40.hpp"
#include "unit_BMP280.hpp"
namespace m5 {
namespace unit {
/*!
@class UnitENV4
@brief ENV IV is an environmental sensor that integrates SHT40 and BMP280
@details This unit itself has no I/O, but holds SHT40 and BMP280 instance
*/
class UnitENV4 : public Component {
// Must not be 0x00 for ensure and assign adapter to children
M5_UNIT_COMPONENT_HPP_BUILDER(UnitENV4, 0xFF /* Dummy address */);
public:
UnitSHT40 sht40; //!< @brief SHT40 instance
UnitBMP280 bmp280; //!< @brief BMP280 instance
explicit UnitENV4(const uint8_t addr = DEFAULT_ADDRESS);
virtual ~UnitENV4()
{
}
virtual bool begin() override
{
return _valid;
}
protected:
virtual std::shared_ptr<Adapter> ensure_adapter(const uint8_t ch);
private:
bool _valid{}; // Did the constructor correctly add the child unit?
Component* _children[2]{&sht40, &bmp280};
};
} // namespace unit
} // namespace m5
#endif

View file

@ -0,0 +1,594 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_QMP6988.cpp
@brief QMP6988 Unit for M5UnitUnified
*/
#include "unit_QMP6988.hpp"
#include <M5Utility.hpp>
#include <limits> // NaN
#include <cmath>
#include <thread>
using namespace m5::utility::mmh3;
using namespace m5::unit::types;
using namespace m5::unit::qmp6988;
using namespace m5::unit::qmp6988::command;
namespace {
constexpr uint8_t chip_id{0x5C};
constexpr size_t calibration_length{25};
constexpr uint32_t sub_raw{8388608}; // 2^23
constexpr Oversampling osrss_table[][2] = {
// Pressure, Temperature
{Oversampling::X2, Oversampling::X1}, {Oversampling::X4, Oversampling::X1}, {Oversampling::X8, Oversampling::X1},
{Oversampling::X16, Oversampling::X2}, {Oversampling::X32, Oversampling::X4},
};
constexpr PowerMode mode_table[] = {
PowerMode::Sleep,
PowerMode::Forced,
PowerMode::Forced, // duplicated
PowerMode::Normal,
};
constexpr Filter filter_table[] = {
Filter::Off, Filter::Coeff2, Filter::Coeff4, Filter::Coeff8, Filter::Coeff16, Filter::Coeff32,
Filter::Coeff32, // duplicated
Filter::Coeff32, // duplicated
};
struct UseCaseSetting {
OversamplingSetting osrss;
Filter filter;
};
constexpr UseCaseSetting uc_table[] = {
{OversamplingSetting::HighSpeed, Filter::Off},
{OversamplingSetting::LowPower, Filter::Off},
{OversamplingSetting::Standard, Filter::Coeff4},
{OversamplingSetting::HighAccuracy, Filter::Coeff8},
{OversamplingSetting::UltraHightAccuracy, Filter::Coeff32},
};
#if 1
constexpr elapsed_time_t standby_time_table[] = {
5, 5, 50, 250, 500, 1000, 2000, 4000,
};
constexpr float ostb{4.4933f};
constexpr float oversampling_temp_time_table[] = {
0.0f, ostb * 1, ostb * 2, ostb * 4, ostb * 8, ostb * 16, ostb * 32, ostb * 64,
};
constexpr float ospb{0.5032f};
constexpr float oversampling_pressure_time_table[] = {
0.0f, ospb * 1, ospb * 2, ospb * 4, ospb * 8, ospb * 16, ospb * 32, ospb * 64,
};
constexpr float filter_time_table[] = {
0.0f, 0.3f, 0.6f, 1.2f, 2.4f, 4.8f, 9.6f, 9.6f, 9.6f,
};
#endif
constexpr uint32_t interval_table[] = {1, 5, 50, 250, 500, 1000, 2000, 4000};
int16_t convert_temperature256(const int32_t dt, const m5::unit::qmp6988::Calibration& c)
{
int64_t wk1, wk2;
int16_t temp256{};
// wk1: 60Q4 // bit size
wk1 = ((int64_t)c.a1 * (int64_t)dt); // 31Q23+24-1=54 (54Q23)
wk2 = ((int64_t)c.a2 * (int64_t)dt) >> 14; // 30Q47+24-1=53 (39Q33)
wk2 = (wk2 * (int64_t)dt) >> 10; // 39Q33+24-1=62 (52Q23)
wk2 = ((wk1 + wk2) / 32767) >> 19; // 54,52->55Q23 (20Q04)
temp256 = (int16_t)((c.a0 + wk2) >> 4); // 21Q4 -> 17Q0
return temp256;
}
int32_t convert_pressure16(const int32_t dp, const int16_t tx, const Calibration& c)
{
int64_t wk1, wk2, wk3;
// wk1 = 48Q16 // bit size
wk1 = ((int64_t)c.bt1 * (int64_t)tx); // 28Q15+16-1=43 (43Q15)
wk2 = ((int64_t)c.bp1 * (int64_t)dp) >> 5; // 31Q20+24-1=54 (49Q15)
wk1 += wk2; // 43,49->50Q15
wk2 = ((int64_t)c.bt2 * (int64_t)tx) >> 1; // 34Q38+16-1=49 (48Q37)
wk2 = (wk2 * (int64_t)tx) >> 8; // 48Q37+16-1=63 (55Q29)
wk3 = wk2; // 55Q29
wk2 = ((int64_t)c.b11 * (int64_t)tx) >> 4; // 28Q34+16-1=43 (39Q30)
wk2 = (wk2 * (int64_t)dp) >> 1; // 39Q30+24-1=62 (61Q29)
wk3 += wk2; // 55,61->62Q29
wk2 = ((int64_t)c.bp2 * (int64_t)dp) >> 13; // 29Q43+24-1=52 (39Q30)
wk2 = (wk2 * (int64_t)dp) >> 1; // 39Q30+24-1=62 (61Q29)
wk3 += wk2; // 62,61->63Q29
wk1 += wk3 >> 14; // Q29 >> 14 -> Q15
wk2 = ((int64_t)c.b12 * (int64_t)tx); // 29Q53+16-1=45 (45Q53)
wk2 = (wk2 * (int64_t)tx) >> 22; // 45Q53+16-1=61 (39Q31)
wk2 = (wk2 * (int64_t)dp) >> 1; // 39Q31+24-1=62 (61Q30)
wk3 = wk2; // 61Q30
wk2 = ((int64_t)c.b21 * (int64_t)tx) >> 6; // 29Q60+16-1=45 (39Q54)
wk2 = (wk2 * (int64_t)dp) >> 23; // 39Q54+24-1=62 (39Q31)
wk2 = (wk2 * (int64_t)dp) >> 1; // 39Q31+24-1=62 (61Q20)
wk3 += wk2; // 61,61->62Q30
wk2 = ((int64_t)c.bp3 * (int64_t)dp) >> 12; // 28Q65+24-1=51 (39Q53)
wk2 = (wk2 * (int64_t)dp) >> 23; // 39Q53+24-1=62 (39Q30)
wk2 = (wk2 * (int64_t)dp); // 39Q30+24-1=62 (62Q30)
wk3 += wk2; // 62,62->63Q30
wk1 += wk3 >> 15; // Q30 >> 15 = Q15
wk1 /= 32767L;
wk1 >>= 11; // Q15 >> 7 = Q4
wk1 += c.b00; // Q4 + 20Q4
// Not shifted to set output at 16 Pa
// wk1 >>= 4; // 28Q4 -> 24Q0
int32_t p16 = (int32_t)wk1;
return p16;
}
} // namespace
struct CtrlMeas {
Oversampling osrs_t() const
{
return static_cast<Oversampling>((value >> 5) & 0x07);
}
Oversampling osrs_p() const
{
return static_cast<Oversampling>((value >> 2) & 0x07);
}
PowerMode mode() const
{
return mode_table[value & 0x03];
}
void osrs_t(const Oversampling os)
{
value = (value & ~(0x07 << 5)) | ((m5::stl::to_underlying(os) & 0x07) << 5);
}
void osrs_p(const Oversampling os)
{
value = (value & ~(0x07 << 2)) | ((m5::stl::to_underlying(os) & 0x07) << 2);
}
void mode(const PowerMode m)
{
value = (value & ~0x03) | (m5::stl::to_underlying(m) & 0x03);
}
uint8_t value{};
};
struct IOSetup {
Standby standby() const
{
return static_cast<Standby>((value >> 5) & 0x07);
}
void standby(const Standby s)
{
value = (value & ~(0x07 << 5)) | ((m5::stl::to_underlying(s) & 0x07) << 5);
}
uint8_t value{};
};
namespace m5 {
namespace unit {
namespace qmp6988 {
float Data::celsius() const
{
uint32_t rt = (((uint32_t)raw[3]) << 16) | (((uint32_t)raw[4]) << 8) | ((uint32_t)raw[5]);
if (calib && rt) {
int32_t dt = (int32_t)(rt - sub_raw);
int16_t t256 = convert_temperature256(dt, *calib);
return (float)t256 / 256.f;
}
return std::numeric_limits<float>::quiet_NaN();
}
float Data::fahrenheit() const
{
return celsius() * 9.0f / 5.0f + 32.f;
}
float Data::pressure() const
{
uint32_t rt = (((uint32_t)raw[3]) << 16) | (((uint32_t)raw[4]) << 8) | ((uint32_t)raw[5]);
uint32_t rp = (((uint32_t)raw[0]) << 16) | (((uint32_t)raw[1]) << 8) | ((uint32_t)raw[2]);
if (calib && rt && rp) {
int32_t dt = (int32_t)(rt - sub_raw);
int16_t t256 = convert_temperature256(dt, *calib);
int32_t dp = (int32_t)(rp - sub_raw);
int32_t p16 = convert_pressure16(dp, t256, *calib);
return (float)p16 / 16.0f;
}
return std::numeric_limits<float>::quiet_NaN();
}
}; // namespace qmp6988
//
const char UnitQMP6988::name[] = "UnitQMP6988";
const types::uid_t UnitQMP6988::uid{"UnitQMP6988"_mmh3};
const types::attr_t UnitQMP6988::attr{attribute::AccessI2C};
types::elapsed_time_t calculatInterval(const Standby st, const Oversampling ost, const Oversampling osp, const Filter f)
{
// M5_LIB_LOGV("ST:%u OST:%u OSP:%u F:%u", st, ost, osp, f);
// M5_LIB_LOGV(
// "Value ST:%u OST:%u OSP:%u F:%u",
// standby_time_table[m5::stl::to_underlying(st)],
// (elapsed_time_t)std::ceil(
// oversampling_temp_time_table[m5::stl::to_underlying(ost)]),
// (elapsed_time_t)std::ceil(
// oversampling_pressure_time_table[m5::stl::to_underlying(osp)]),
// (elapsed_time_t)std::ceil(
// filter_time_table[m5::stl::to_underlying(f)]));
elapsed_time_t itv = standby_time_table[m5::stl::to_underlying(st)] +
(elapsed_time_t)std::ceil(oversampling_temp_time_table[m5::stl::to_underlying(ost)]) +
(elapsed_time_t)std::ceil(oversampling_pressure_time_table[m5::stl::to_underlying(osp)]) +
(elapsed_time_t)std::ceil(filter_time_table[m5::stl::to_underlying(f)]);
return itv;
}
bool UnitQMP6988::begin()
{
auto ssize = stored_size();
assert(ssize && "stored_size must be greater than zero");
if (ssize != _data->capacity()) {
_data.reset(new m5::container::CircularBuffer<Data>(ssize));
if (!_data) {
M5_LIB_LOGE("Failed to allocate");
return false;
}
}
uint8_t id{};
if (!readRegister8(CHIP_ID, id, 0) || id != chip_id) {
M5_LIB_LOGE("This unit is NOT QMP6988 %x", id);
return false;
}
if (!softReset()) {
M5_LIB_LOGE("Failed to reset");
return false;
}
if (!read_calibration(_calibration)) {
M5_LIB_LOGE("Failed to read_calibration");
return false;
}
return _cfg.start_periodic
? startPeriodicMeasurement(_cfg.osrs_pressure, _cfg.osrs_temperature, _cfg.filter, _cfg.standby)
: true;
}
void UnitQMP6988::update(const bool force)
{
_updated = false;
if (inPeriodic()) {
elapsed_time_t at{m5::utility::millis()};
if (force || !_latest || at >= _latest + _interval) {
Data d{};
//_updated = is_data_ready() && read_measurement(d);
_updated = read_measurement(d, _only_temperature);
if (_updated) {
// auto dur = at - _latest;
// M5_LIB_LOGW(">DUR:%ld", dur);
_latest = at;
_data->push_back(d);
}
}
}
}
bool UnitQMP6988::start_periodic_measurement(const qmp6988::Oversampling osrsPressure,
const qmp6988::Oversampling osrsTemperature, const qmp6988::Filter f,
const Standby st)
{
if (inPeriodic()) {
return false;
}
// Need temperature for measure pressure (Only temperature measurement is acceptable)
if (osrsTemperature == Oversampling::Skipped) {
return false;
}
_only_temperature = (osrsPressure == Oversampling::Skipped);
return writeOversampling(osrsPressure, osrsTemperature) && writeFilter(f) && writeStandbyTime(st) &&
start_periodic_measurement();
}
bool UnitQMP6988::start_periodic_measurement()
{
if (inPeriodic()) {
return false;
}
IOSetup is{};
_periodic = readRegister8(IO_SETUP, is.value, 0) && writePowerMode(PowerMode::Normal);
if (_periodic) {
_latest = 0;
_interval = interval_table[m5::stl::to_underlying(is.standby())];
}
return _periodic;
}
bool UnitQMP6988::stop_periodic_measurement()
{
if (inPeriodic() && writePowerMode(PowerMode::Sleep)) {
_periodic = false;
return true;
}
return false;
}
bool UnitQMP6988::measureSingleshot(qmp6988::Data& d, const qmp6988::Oversampling osrsPressure,
const qmp6988::Oversampling osrsTemperature, const qmp6988::Filter f)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
// Need temperature for measure pressure (Only temperature measurement is acceptable)
if (osrsTemperature == Oversampling::Skipped) {
return false;
}
return writeOversampling(osrsPressure, osrsTemperature) && writeFilter(f) && measureSingleshot(d);
}
bool UnitQMP6988::measureSingleshot(qmp6988::Data& d)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0) && writePowerMode(qmp6988::PowerMode::Forced)) {
auto timeout_at = m5::utility::millis() + 1 * 1000;
bool done{};
do {
done = is_data_ready();
if (done) {
break;
}
std::this_thread::yield();
// m5::utility::delay(1);
} while (!done && m5::utility::millis() <= timeout_at);
return done && read_measurement(d, cm.osrs_p() == Oversampling::Skipped);
}
return false;
}
bool UnitQMP6988::readOversampling(qmp6988::Oversampling& osrsPressure, qmp6988::Oversampling& osrsTemperature)
{
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
osrsPressure = cm.osrs_p();
osrsTemperature = cm.osrs_t();
return true;
}
return false;
}
bool UnitQMP6988::writeOversampling(const qmp6988::Oversampling osrsPressure,
const qmp6988::Oversampling osrsTemperature)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
cm.osrs_p(osrsPressure);
cm.osrs_t(osrsTemperature);
return writeRegister8(CONTROL_MEASUREMENT, cm.value);
}
return false;
}
bool UnitQMP6988::writeOversamplingPressure(const qmp6988::Oversampling osrsPressure)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
cm.osrs_p(osrsPressure);
return writeRegister8(CONTROL_MEASUREMENT, cm.value);
}
return false;
}
bool UnitQMP6988::writeOversamplingTemperature(const qmp6988::Oversampling osrsTemperature)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
cm.osrs_t(osrsTemperature);
return writeRegister8(CONTROL_MEASUREMENT, cm.value);
}
return false;
}
bool UnitQMP6988::writeOversampling(const qmp6988::OversamplingSetting osrss)
{
auto idx = m5::stl::to_underlying(osrss);
return writeOversampling(osrss_table[idx][0], osrss_table[idx][1]);
}
bool UnitQMP6988::readPowerMode(qmp6988::PowerMode& m)
{
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
m = cm.mode();
return true;
}
return false;
}
bool UnitQMP6988::writePowerMode(const qmp6988::PowerMode m)
{
CtrlMeas cm{};
if (readRegister8(CONTROL_MEASUREMENT, cm.value, 0)) {
cm.mode(m);
// Changing mode during measurement may result in erratic data the next time
auto timeout_at = m5::utility::millis() + 1000;
bool can{};
do {
can = is_data_ready();
if (can) {
break;
}
m5::utility::delay(1);
} while (!can && m5::utility::millis() <= timeout_at);
return can && writeRegister8(CONTROL_MEASUREMENT, cm.value);
}
return false;
}
bool UnitQMP6988::readFilter(qmp6988::Filter& f)
{
uint8_t v{};
if (readRegister8(IIR_FILTER, v, 0)) {
f = filter_table[v & 0x07];
return true;
}
return false;
}
bool UnitQMP6988::writeFilter(const qmp6988::Filter f)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
return writeRegister8(IIR_FILTER, m5::stl::to_underlying(f));
}
bool UnitQMP6988::readStandbyTime(qmp6988::Standby& st)
{
IOSetup is{};
if (readRegister8(IO_SETUP, is.value, 0)) {
st = is.standby();
return true;
}
return false;
}
bool UnitQMP6988::writeStandbyTime(const qmp6988::Standby st)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
IOSetup is{};
if (readRegister8(IO_SETUP, is.value, 0)) {
is.standby(st);
return writeRegister8(IO_SETUP, is.value);
}
return false;
}
bool UnitQMP6988::writeUseCaseSetting(const qmp6988::UseCase uc)
{
const auto& tbl = uc_table[m5::stl::to_underlying(uc)];
return writeOversampling(tbl.osrss) && writeFilter(tbl.filter);
}
bool UnitQMP6988::softReset()
{
constexpr uint8_t v{0xE6}; // When inputting "E6h", a soft-reset will be occurred
auto ret = writeRegister8(SOFT_RESET, v);
M5_LIB_LOGD("Reset causes a NO ACK or timeout error, but ignore it");
(void)ret;
m5::utility::delay(10); // Need delay
if (writeRegister(SOFT_RESET, 0x00)) {
_periodic = false;
return true;
}
return false;
}
bool UnitQMP6988::is_data_ready()
{
uint8_t v{};
return readRegister8(GET_STATUS, v, 0) && ((v & 0x08 /* measure */) == 0);
}
bool UnitQMP6988::read_measurement(Data& d, const bool only_temperature)
{
if (readRegister(READ_PRESSURE, d.raw.data(), d.raw.size(), 0)) {
// If osrs_p is Skipped, but the previous pressure data is still there, so it is deleted
if (only_temperature) {
d.raw[0] = d.raw[1] = d.raw[2] = 0;
}
d.calib = &_calibration;
// M5_DUMPI(d.raw.data(), d.raw.size());
return true;
}
return false;
}
bool UnitQMP6988::read_calibration(qmp6988::Calibration& c)
{
using namespace m5::utility; // unsigned_to_signed
using namespace m5::types; // big_uint16_t
uint8_t rbuf[calibration_length]{};
if (!readRegister(READ_COMPENSATION_COEFFICIENT, rbuf, sizeof(rbuf), 0)) {
return false;
}
uint32_t b00 = ((uint32_t)(big_uint16_t(rbuf[0], rbuf[1]).get()) << 4) | ((rbuf[24] >> 4) & 0x0F);
c.b00 = unsigned_to_signed<20>(b00); // 20Q4
c.bt1 = 2982L * (int64_t)unsigned_to_signed<16>(big_uint16_t(rbuf[2], rbuf[3]).get()) + 107370906L; // 28Q15
c.bt2 = 329854L * (int64_t)unsigned_to_signed<16>(big_uint16_t(rbuf[4], rbuf[5]).get()) + +108083093L; // 34Q38
c.bp1 = 19923L * (int64_t)unsigned_to_signed<16>(big_uint16_t(rbuf[6], rbuf[7]).get()) + 1133836764L; // 31Q20
c.b11 = 2406L * (int64_t)unsigned_to_signed<16>(big_uint16_t(rbuf[8], rbuf[9]).get()) + 118215883L; // 28Q34
c.bp2 = 3079L * (int64_t)unsigned_to_signed<16>(big_uint16_t(rbuf[10], rbuf[11]).get()) - 181579595L; // 29Q43
c.b12 = 6846L * (int64_t)unsigned_to_signed<16>(big_uint16_t(rbuf[12], rbuf[13]).get()) + 85590281L; // 29Q53
c.b21 = 13836L * (int64_t)unsigned_to_signed<16>(big_uint16_t(rbuf[14], rbuf[15]).get()) + 79333336L; // 29Q60
c.bp3 = 2915L * (int64_t)unsigned_to_signed<16>(big_uint16_t(rbuf[16], rbuf[17]).get()) + 157155561L; // 28Q65
uint32_t a0 = ((uint32_t)big_uint16_t(rbuf[18], rbuf[19]).get() << 4) | (rbuf[24] & 0x0F);
c.a0 = unsigned_to_signed<20>(a0); // 20Q4
c.a1 = 3608L * (int32_t)unsigned_to_signed<16>(big_uint16_t(rbuf[20], rbuf[21]).get()) - 1731677965L; // 31Q23
c.a2 = 16889L * (int32_t)unsigned_to_signed<16>(big_uint16_t(rbuf[22], rbuf[23]).get()) - 87619360L; // 31Q47
#if 0
M5_LIB_LOGI(
"\n"
"b00:%d\n"
"bt1:%d\n"
"bt2:%lld\n"
"bp1:%d\n"
"b11:%d\n"
"bp2:%d\n"
"b12:%d\n"
"b21:%d\n"
"bp3:%d\n"
"a0:%d\n"
"a1:%d\n"
"a2:%d",
c.b00, c.bt1, c.bt2, c.bp1, c.b11, c.bp2, c.b12, c.b21, c.bp3, c.a0,
c.a1, c.a2);
#endif
return true;
}
} // namespace unit
} // namespace m5

View file

@ -0,0 +1,393 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_QMP6988.hpp
@brief QMP6988 Unit for M5UnitUnified
*/
#ifndef M5_UNIT_ENV_UNIT_QMP6988_HPP
#define M5_UNIT_ENV_UNIT_QMP6988_HPP
#include <M5UnitComponent.hpp>
#include <m5_utility/stl/extension.hpp>
#include <m5_utility/container/circular_buffer.hpp>
#include <limits> // NaN
namespace m5 {
namespace unit {
namespace qmp6988 {
/*!
@enum PowerMode
@brief Operation mode
*/
enum class PowerMode : uint8_t {
Sleep, //!< No measurements are performed
Forced, //!< Single measurements are performed
Normal = 0x03, //!< Periodic measurements are performed
};
/*!
@enum Oversampling
@brief Oversampling value
@warning
*/
enum class Oversampling : uint8_t {
Skipped, //!< Skipped (No measurements are performed)
X1, //!< x1
X2, //!< x2
X4, //!< x4
X8, //!< x8
X16, //!< x16
X32, //!< x32
X64, //!< x64
};
/*!
@enum OversamplingSetting
@brief Oversampling Settings
*/
enum class OversamplingSetting : uint8_t {
HighSpeed, //!< osrsP:X2 osrsT:X1
LowPower, //!< osrsP:X4 osrsT:X1
Standard, //!< osrsP:X8 osrsT:X1
HighAccuracy, //!< osrsP:X16 osrsT:X2
UltraHightAccuracy, //!< osrsP:X32 osrsT:X4
};
/*!
@enum Filter
@brief Filtter setting
*/
enum class Filter : uint8_t {
Off, //!< Off filter
Coeff2, //!< co-efficient 2
Coeff4, //!< co-efficient 4
Coeff8, //!< co-efficient 8
Coeff16, //!< co-efficient 16
Coeff32, //!< co-efficient 32
};
/*!
@enum Standby
@brief Measurement standby time for power mode Normal
*/
enum class Standby : uint8_t {
Time1ms, //!< 1 ms
Time5ms, //!< 5 ms
Time50ms, //!< 50 ms
Time250ms, //!< 250 ms
Time500ms, //!< 500 ms
Time1sec, //!< 1 seconds
Time2sec, //!< 2 seconds
Time4sec, //!< 4 seconds
};
/*!
@enum UseCase
@brief Preset settings
*/
enum class UseCase : uint8_t {
Weather, //!< Weather monitoring
Drop, //!< Drop detection
Elevator, //!< Elevator / floor change detection
Stair, //!< Stair detection
Indoor, //!< Indoor navigation
};
///@cond
struct Calibration {
int32_t b00{}, bt1{}, bp1{};
int64_t bt2{};
int32_t b11{}, bp2{}, b12{}, b21{}, bp3{}, a0{}, a1{}, a2{};
};
///@endcond
/*!
@struct Data
@brief Measurement data group
*/
struct Data {
std::array<uint8_t, 6> raw{}; //!< RAW data
//! temperature (Celsius)
inline float temperature() const
{
return celsius();
}
float celsius() const; //!< temperature (Celsius)
float fahrenheit() const; //!< temperature (Fahrenheit)
float pressure() const; //!< pressure (Pa)
const Calibration* calib{};
};
}; // namespace qmp6988
/*!
@class UnitQMP6988
@brief Barometric pressure sensor to measure atmospheric pressure and altitude estimation
*/
class UnitQMP6988 : public Component, public PeriodicMeasurementAdapter<UnitQMP6988, qmp6988::Data> {
M5_UNIT_COMPONENT_HPP_BUILDER(UnitQMP6988, 0x70);
public:
/*!
@struct config_t
@brief Settings for begin
*/
struct config_t {
//! Start periodic measurement on begin?
bool start_periodic{true};
//! pressure oversampling if start on begin
qmp6988::Oversampling osrs_pressure{qmp6988::Oversampling::X8};
//! temperature oversampling if start on begin
qmp6988::Oversampling osrs_temperature{qmp6988::Oversampling::X1};
//! Filter if start on begin
qmp6988::Filter filter{qmp6988::Filter::Coeff4};
//! Standby time if start on begin
qmp6988::Standby standby{qmp6988::Standby::Time1sec};
};
explicit UnitQMP6988(const uint8_t addr = DEFAULT_ADDRESS)
: Component(addr), _data{new m5::container::CircularBuffer<qmp6988::Data>(1)}
{
auto ccfg = component_config();
ccfg.clock = 400 * 1000U;
component_config(ccfg);
}
virtual ~UnitQMP6988()
{
}
virtual bool begin() override;
virtual void update(const bool force = false) override;
///@name Settings for begin
///@{
/*! @brief Gets the configration */
inline config_t config()
{
return _cfg;
}
//! @brief Set the configration
inline void config(const config_t& cfg)
{
_cfg = cfg;
}
///@}
///@name Measurement data by periodic
///@{
//! @brief Oldest measured temperature (Celsius)
inline float temperature() const
{
return !empty() ? oldest().temperature() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured temperature (Celsius)
inline float celsius() const
{
return !empty() ? oldest().celsius() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured temperature (Fahrenheit)
inline float fahrenheit() const
{
return !empty() ? oldest().fahrenheit() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured pressure (Pa)
inline float pressure() const
{
return !empty() ? oldest().pressure() : std::numeric_limits<float>::quiet_NaN();
}
///@}
///@name Periodic measurement
///@{
/*!
@brief Start periodic measurement
@param osrsPressure Oversampling factor for pressure
@param osrsTemperature Oversampling factor for temperature
@param filter Filter coeff
@param st Standby time
@return True if successful
@warning Measuring pressure requires measuring temperature
*/
inline bool startPeriodicMeasurement(const qmp6988::Oversampling osrsPressure,
const qmp6988::Oversampling osrsTemperature, const qmp6988::Filter f,
const qmp6988::Standby st)
{
return PeriodicMeasurementAdapter<UnitQMP6988, qmp6988::Data>::startPeriodicMeasurement(osrsPressure,
osrsTemperature, f, st);
}
//! @brief Start periodic measurement using current settings
inline bool startPeriodicMeasurement()
{
return PeriodicMeasurementAdapter<UnitQMP6988, qmp6988::Data>::startPeriodicMeasurement();
}
/*!
@brief Stop periodic measurement
@return True if successful
*/
inline bool stopPeriodicMeasurement()
{
return PeriodicMeasurementAdapter<UnitQMP6988, qmp6988::Data>::stopPeriodicMeasurement();
}
///@}
///@name Single shot measurement
///@{
/*!
@brief Measurement single shot
@param[out] data Measuerd data
@param osrsPressure Oversampling factor for pressure
@param osrsTemperature Oversampling factor for temperature
@param filter Filter coeff
@return True if successful
@warning During periodic detection runs, an error is returned
@warning Measuring pressure requires measuring temperature
@warning Each setting is overwritten
*/
bool measureSingleshot(qmp6988::Data& d, const qmp6988::Oversampling osrsPressure,
const qmp6988::Oversampling osrsTemperature, const qmp6988::Filter f);
//! @brief Measurement single shot using current settings
bool measureSingleshot(qmp6988::Data& d);
///@}
///@name Settings
///@{
/*!
@brief Read the oversampling conditions
@param[out] osrsPressure Oversampling for pressure
@param[out] osrsTemperature Oversampling for temperature
@return True if successful
*/
bool readOversampling(qmp6988::Oversampling& osrsPressure, qmp6988::Oversampling& osrsTemperature);
/*!
@brief Write the oversampling conditions
@param osrsPressure Oversampling for pressure
@param osrsTemperature Oversampling for temperature
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeOversampling(const qmp6988::Oversampling osrsPressure, const qmp6988::Oversampling osrsTemperature);
/*!
@brief Write the oversampling conditions for pressure
@param osrsPressure Oversampling for pressure
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeOversamplingPressure(const qmp6988::Oversampling osrsPressure);
/*!
@brief Write the oversampling conditions for temperature
@param osrsTemperature Oversampling for temperature
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeOversamplingTemperature(const qmp6988::Oversampling osrsTemperature);
/*!
@brief Write the oversampling by OversamplingSetting
@param osrss OversamplingSetting
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeOversampling(const qmp6988::OversamplingSetting osrss);
/*!
@brief Read the IIR filter co-efficient
@param[out] f filter
@return True if successful
*/
bool readFilter(qmp6988::Filter& f);
/*!
@brief Write the IIR filter co-efficient
@param f filter
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeFilter(const qmp6988::Filter f);
/*!
@brief Read the standby time
@param[out] st standby time
@return True if successful
*/
bool readStandbyTime(qmp6988::Standby& st);
/*!
@brief Write the standby time
@param st standby time
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeStandbyTime(const qmp6988::Standby st);
/*!
@brief Read the power mode
@param[out] mode PowerMode
@return True if successful
*/
bool readPowerMode(qmp6988::PowerMode& mode);
/*!
@brief Write the power mode
@param m Power mode
@return True if successful
@warning Note that the measurement mode is changed
@warning It is recommended to use start/stopPeriodicMeasurement or similar to change the measurement mode
*/
bool writePowerMode(const qmp6988::PowerMode mode);
/*!
@brief Write the settings based on use cases
@param uc UseCase
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeUseCaseSetting(const qmp6988::UseCase uc);
///@}
/*!
@brief Soft reset
@return True if successful
*/
bool softReset();
protected:
bool start_periodic_measurement();
bool start_periodic_measurement(const qmp6988::Oversampling ost, const qmp6988::Oversampling osp,
const qmp6988::Filter f, const qmp6988::Standby st);
bool stop_periodic_measurement();
bool read_calibration(qmp6988::Calibration& c);
bool read_measurement(qmp6988::Data& d, const bool only_temperature = false);
bool is_data_ready();
M5_UNIT_COMPONENT_PERIODIC_MEASUREMENT_ADAPTER_HPP_BUILDER(UnitQMP6988, qmp6988::Data);
protected:
std::unique_ptr<m5::container::CircularBuffer<qmp6988::Data>> _data{};
qmp6988::Calibration _calibration{};
config_t _cfg{};
bool _only_temperature{};
};
///@cond
namespace qmp6988 {
namespace command {
constexpr uint8_t CHIP_ID{0xD1};
constexpr uint8_t READ_PRESSURE{0xF7}; // ~ F9 3bytes
constexpr uint8_t READ_TEMPERATURE{0xFA}; // ~ FC 3bytes
constexpr uint8_t IO_SETUP{0xF5};
constexpr uint8_t CONTROL_MEASUREMENT{0xF4};
constexpr uint8_t GET_STATUS{0xF3};
constexpr uint8_t IIR_FILTER{0xF1};
constexpr uint8_t SOFT_RESET{0xE0};
constexpr uint8_t READ_COMPENSATION_COEFFICIENT{0xA0}; // ~ 0xB8 25 bytes
} // namespace command
} // namespace qmp6988
///@endcond
} // namespace unit
} // namespace m5
#endif

View file

@ -0,0 +1,530 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_SCD40.cpp
@brief SCD40 Unit for M5UnitUnified
*/
#include "unit_SCD40.hpp"
#include <M5Utility.hpp>
#include <array>
using namespace m5::utility::mmh3;
using namespace m5::unit::types;
using namespace m5::unit::scd4x;
using namespace m5::unit::scd4x::command;
namespace {
struct Temperature {
constexpr static float toFloat(const uint16_t u16)
{
return u16 * 175.f / 65536.f;
}
constexpr static uint16_t toUint16(const float f)
{
return f * 65536 / 175;
}
constexpr static float OFFSET_MIN{0.0f};
constexpr static float OFFSET_MAX{175.0f};
};
constexpr uint16_t mode_reg_table[] = {
START_PERIODIC_MEASUREMENT,
START_LOW_POWER_PERIODIC_MEASUREMENT,
};
constexpr uint32_t interval_table[] = {
5000U, // 5 Sec.
30 * 1000U, // 30 Sec.
};
const uint8_t VARIANT_VALUE[2]{0x04, 0x40}; // SCD40
} // namespace
namespace m5 {
namespace unit {
namespace scd4x {
uint16_t Data::co2() const
{
return m5::types::big_uint16_t(raw[0], raw[1]).get();
}
float Data::celsius() const
{
return -45 + Temperature::toFloat(m5::types::big_uint16_t(raw[3], raw[4]).get());
}
float Data::fahrenheit() const
{
return celsius() * 9.0f / 5.0f + 32.f;
}
float Data::humidity() const
{
return 100.f * m5::types::big_uint16_t(raw[6], raw[7]).get() / 65536.f;
}
} // namespace scd4x
// class UnitSCD40
const char UnitSCD40::name[] = "UnitSCD40";
const types::uid_t UnitSCD40::uid{"UnitSCD40"_mmh3};
const types::attr_t UnitSCD40::attr{attribute::AccessI2C};
bool UnitSCD40::begin()
{
auto ssize = stored_size();
assert(ssize && "stored_size must be greater than zero");
if (ssize != _data->capacity()) {
_data.reset(new m5::container::CircularBuffer<Data>(ssize));
if (!_data) {
M5_LIB_LOGE("Failed to allocate");
return false;
}
}
// Stop (to idle mode)
if (!writeRegister(STOP_PERIODIC_MEASUREMENT)) {
M5_LIB_LOGE("Failed to stop");
return false;
}
m5::utility::delay(STOP_PERIODIC_MEASUREMENT_DURATION);
if (!is_valid_chip()) {
return false;
}
if (!writeAutomaticSelfCalibrationEnabled(_cfg.calibration)) {
M5_LIB_LOGE("Failed to writeAutomaticSelfCalibrationEnabled");
return false;
}
// Stop
return _cfg.start_periodic ? startPeriodicMeasurement(_cfg.mode) : true;
}
bool UnitSCD40::is_valid_chip()
{
uint8_t var[2]{};
if (!read_register(GET_SENSOR_VARIANT, var, 2) || memcmp(var, VARIANT_VALUE, 2) != 0) {
M5_LIB_LOGE("Not SCD40 %02X:%02X", var[0], var[1]);
return false;
}
return true;
}
void UnitSCD40::update(const bool force)
{
_updated = false;
if (inPeriodic()) {
auto at = m5::utility::millis();
if (force || !_latest || at >= _latest + _interval) {
Data d{};
_updated = read_measurement(d);
if (_updated) {
_latest = m5::utility::millis(); // Data acquisition takes time, so acquire again
_data->push_back(d);
}
}
}
}
bool UnitSCD40::start_periodic_measurement(const Mode mode)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
auto m = m5::stl::to_underlying(mode);
_periodic = writeRegister(mode_reg_table[m]);
if (_periodic) {
_interval = interval_table[m];
_latest = 0;
}
return _periodic;
}
bool UnitSCD40::stop_periodic_measurement(const uint32_t duration)
{
if (inPeriodic()) {
if (writeRegister(STOP_PERIODIC_MEASUREMENT)) {
_periodic = false;
m5::utility::delay(duration);
return true;
}
}
return false;
}
bool UnitSCD40::writeTemperatureOffset(const float offset, const uint32_t duration)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (offset < Temperature::OFFSET_MIN || offset >= Temperature::OFFSET_MAX) {
M5_LIB_LOGE("offset is not a valid scope %f", offset);
return false;
}
uint8_t wbuf[2]{};
uint16_t tmp16 = Temperature::toUint16(offset);
wbuf[0] = tmp16 >> 8;
wbuf[1] = tmp16 & 0xFF;
return write_register(SET_TEMPERATURE_OFFSET, wbuf, sizeof(wbuf)) && delay_true(duration);
}
bool UnitSCD40::readTemperatureOffset(float& offset)
{
offset = 0.0f;
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16{};
if (read_register(GET_TEMPERATURE_OFFSET, u16.data(), u16.size(), GET_TEMPERATURE_OFFSET_DURATION)) {
offset = Temperature::toFloat(u16.get());
return true;
}
return false;
}
bool UnitSCD40::writeSensorAltitude(const uint16_t altitude, const uint32_t duration)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16(altitude);
return write_register(SET_SENSOR_ALTITUDE, u16.data(), u16.size()) && delay_true(duration);
}
bool UnitSCD40::readSensorAltitude(uint16_t& altitude)
{
altitude = 0;
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16{};
if (read_register(GET_SENSOR_ALTITUDE, u16.data(), u16.size(), GET_SENSOR_ALTITUDE_DURATION)) {
altitude = u16.get();
return true;
}
return false;
}
bool UnitSCD40::writeAmbientPressure(const uint16_t pressure, const uint32_t duration)
{
constexpr uint32_t PRESSURE_MIN{700};
constexpr uint32_t PRESSURE_MAX{1200};
#if 0
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
#endif
if (pressure < PRESSURE_MIN || pressure > PRESSURE_MAX) {
M5_LIB_LOGE("pressure is not a valid scope (%u - %u) %u", PRESSURE_MIN, PRESSURE_MAX, pressure);
return false;
}
m5::types::big_uint16_t u16(pressure);
return write_register(AMBIENT_PRESSURE, u16.data(), u16.size()) && delay_true(duration);
}
bool UnitSCD40::readAmbientPressure(uint16_t& pressure)
{
pressure = 0;
#if 0
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
#endif
m5::types::big_uint16_t u16{};
if (read_register(AMBIENT_PRESSURE, u16.data(), u16.size(), GET_AMBIENT_PRESSURE_DURATION)) {
pressure = u16.get();
return true;
}
return false;
}
bool UnitSCD40::performForcedRecalibration(const uint16_t concentration, int16_t& correction)
{
// 1. Operate the SCD4x in the operation mode later used in normal sensor
// operation (periodic measurement, low power periodic measurement or single
// shot) for > 3 minutes in an environment with homogenous and constant CO2
// concentration.
// 2. Issue stop_periodic_measurement. Wait 500 ms for the stop command to
// complete.
correction = 0;
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16(concentration);
if (!write_register(PERFORM_FORCED_CALIBRATION, u16.data(), u16.size())) {
return false;
}
#if 0
// 3. Subsequently issue the perform_forced_recalibration command and
// optionally read out the FRC correction (i.e. the magnitude of the
// correction) after waiting for 400 ms for the command to complete.
m5::utility::delay(PERFORM_FORCED_CALIBRATION_DURATION);
std::array<uint8_t, 3> rbuf{};
if (readWithTransaction(rbuf.data(), rbuf.size()) == m5::hal::error::error_t::OK) {
m5::types::big_uint16_t u16{rbuf[0], rbuf[1]};
m5::utility::CRC8_Checksum crc{};
if (rbuf[2] == crc.range(u16.data(), u16.size()) && u16.get() != 0xFFFF) {
correction = (int16_t)(u16.get() - 0x8000);
return true;
}
}
return false;
#else
// 3. Subsequently issue the perform_forced_recalibration command and
// optionally read out the FRC correction (i.e. the magnitude of the
// correction) after waiting for 400 ms for the command to complete.
m5::utility::delay(PERFORM_FORCED_CALIBRATION_DURATION);
if (read_register(PERFORM_FORCED_CALIBRATION, u16.data(), u16.size()) && u16.get() != 0xFFFF) {
correction = (int16_t)(u16.get() - 0x8000);
return true;
}
return false;
#endif
}
bool UnitSCD40::writeAutomaticSelfCalibrationEnabled(const bool enabled, const uint32_t duration)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16(enabled ? 0x0001 : 0x0000);
return write_register(SET_AUTOMATIC_SELF_CALIBRATION_ENABLED, u16.data(), u16.size()) && delay_true(duration);
}
bool UnitSCD40::readAutomaticSelfCalibrationEnabled(bool& enabled)
{
enabled = false;
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16{};
if (read_register(GET_AUTOMATIC_SELF_CALIBRATION_ENABLED, u16.data(), u16.size(),
GET_AUTOMATIC_SELF_CALIBRATION_ENABLED_DURATION)) {
enabled = (u16.get() == 0x0001);
return true;
}
return false;
}
bool UnitSCD40::writeAutomaticSelfCalibrationTarget(const uint16_t ppm, const uint32_t duration)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16{ppm};
return write_register(SET_AUTOMATIC_SELF_CALIBRATION_TARGET, u16.data(), u16.size()) && delay_true(duration);
}
bool UnitSCD40::readAutomaticSelfCalibrationTarget(uint16_t& ppm)
{
ppm = 0;
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16{};
if (read_register(GET_AUTOMATIC_SELF_CALIBRATION_TARGET, u16.data(), u16.size(),
GET_AUTOMATIC_SELF_CALIBRATION_TARGET_DURATION)) {
ppm = u16.get();
return true;
}
return false;
}
bool UnitSCD40::writePersistSettings(const uint32_t duration)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (writeRegister(PERSIST_SETTINGS)) {
m5::utility::delay(duration);
return true;
}
return false;
}
bool UnitSCD40::readSerialNumber(char* serialNumber)
{
if (!serialNumber) {
return false;
}
*serialNumber = '\0';
uint64_t sno{};
if (readSerialNumber(sno)) {
uint_fast8_t i{12};
while (i--) {
*serialNumber++ = m5::utility::uintToHexChar((sno >> (i * 4)) & 0x0F);
}
*serialNumber = '\0';
return true;
}
return false;
}
bool UnitSCD40::readSerialNumber(uint64_t& serialNumber)
{
std::array<uint8_t, 9> rbuf;
serialNumber = 0;
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::utility::CRC8_Checksum crc{};
if (readRegister(GET_SERIAL_NUMBER, rbuf.data(), rbuf.size(), GET_SERIAL_NUMBER_DURATION)) {
m5::types::big_uint16_t u16[3]{{rbuf[0], rbuf[1]}, {rbuf[3], rbuf[4]}, {rbuf[6], rbuf[7]}};
if (crc.range(u16[0].data(), u16[0].size()) == rbuf[2] && crc.range(u16[1].data(), u16[1].size()) == rbuf[5] &&
crc.range(u16[2].data(), u16[2].size()) == rbuf[8]) {
serialNumber = ((uint64_t)u16[0].get()) << 32 | ((uint64_t)u16[1].get()) << 16 | ((uint64_t)u16[2].get());
return true;
}
}
return false;
}
bool UnitSCD40::performSelfTest(bool& malfunction)
{
malfunction = true;
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16{};
if (read_register(PERFORM_SELF_TEST, u16.data(), u16.size(), PERFORM_SELF_TEST_DURATION)) {
malfunction = (u16.get() != 0);
return true;
}
return false;
}
bool UnitSCD40::performFactoryReset(const uint32_t duration)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (writeRegister(PERFORM_FACTORY_RESET)) {
m5::utility::delay(duration);
return true;
}
return false;
}
bool UnitSCD40::reInit(const uint32_t duration)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (writeRegister(REINIT)) {
m5::utility::delay(duration);
return true;
}
return false;
}
bool UnitSCD40::read_data_ready_status()
{
uint16_t res{};
return readRegister16BE(GET_DATA_READY_STATUS, res, GET_DATA_READY_STATUS_DURATION) ? (res & 0x07FF) != 0 : false;
}
// TH only if all is false
bool UnitSCD40::read_measurement(Data& d, const bool all)
{
if (!read_data_ready_status()) {
M5_LIB_LOGV("Not ready");
return false;
}
if (!readRegister(READ_MEASUREMENT, d.raw.data(), d.raw.size(), READ_MEASUREMENT_DURATION)) {
return false;
}
// For RHT only, previous Co2 data may be obtained and should be dismissed
if (!all) {
d.raw[0] = d.raw[1] = d.raw[2] = 0;
}
// Check CRC
m5::utility::CRC8_Checksum crc{};
for (uint_fast8_t i = all ? 0 : 1; i < 3; ++i) {
if (crc.range(d.raw.data() + i * 3, 2U) != d.raw[i * 3 + 2]) {
return false;
}
}
return true;
}
bool UnitSCD40::read_register(const uint16_t reg, uint8_t* rbuf, const uint32_t rlen, const uint32_t duration)
{
uint8_t tmp[rlen + 1]{};
if (!rbuf || !rlen || !readRegister(reg, tmp, sizeof(tmp), duration)) {
return false;
}
m5::utility::CRC8_Checksum crc{};
auto crc8 = crc.range(tmp, rlen);
if (crc8 != tmp[rlen]) {
M5_LIB_LOGE("CRC8 Error:%02X, %02X", tmp[rlen], crc8);
return false;
}
memcpy(rbuf, tmp, rlen);
return true;
}
bool UnitSCD40::write_register(const uint16_t reg, uint8_t* wbuf, const uint32_t wlen)
{
uint8_t buf[wlen + 1]{};
if (!wbuf || !wlen) {
return false;
}
memcpy(buf, wbuf, wlen);
m5::utility::CRC8_Checksum crc{};
auto crc8 = crc.range(wbuf, wlen);
buf[wlen] = crc8;
return writeRegister(reg, buf, sizeof(buf));
}
bool UnitSCD40::delay_true(const uint32_t duration)
{
m5::utility::delay(duration);
return true; // Always true
}
} // namespace unit
} // namespace m5

View file

@ -0,0 +1,390 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_SCD40.hpp
@brief SCD40 Unit for M5UnitUnified
*/
#ifndef M5_UNIT_ENV_UNIT_SCD40_HPP
#define M5_UNIT_ENV_UNIT_SCD40_HPP
#include <M5UnitComponent.hpp>
#include <m5_utility/container/circular_buffer.hpp>
#include <limits> // NaN
namespace m5 {
namespace unit {
/*!
@namespace scd4x
@brief For SCD40/41
*/
namespace scd4x {
/*!
@enum Mode
@brief Mode of periodic measurement
*/
enum class Mode : uint8_t {
Normal, //!< Normal (Receive data every 5 seconds)
LowPower, //!< Low power (Receive data every 30 seconds)
};
/*!
@struct Data
@brief Measurement data group
*/
struct Data {
std::array<uint8_t, 9> raw{}; //!< @brief RAW data
uint16_t co2() const; //!< @brief CO2 concentration (ppm)
//! @brief temperature (Celsius)
inline float temperature() const
{
return celsius();
}
float celsius() const; //!< @brief temperature (Celsius)
float fahrenheit() const; //!< @brief temperature (Fahrenheit)
float humidity() const; //!< @brief humidity (RH)
};
///@cond
// Max command duration(ms)
// For SCD40/41
constexpr uint16_t READ_MEASUREMENT_DURATION{1};
constexpr uint16_t STOP_PERIODIC_MEASUREMENT_DURATION{500};
constexpr uint16_t SET_TEMPERATURE_OFFSET_DURATION{1};
constexpr uint16_t GET_TEMPERATURE_OFFSET_DURATION{1};
constexpr uint16_t SET_SENSOR_ALTITUDE_DURATION{1};
constexpr uint16_t GET_SENSOR_ALTITUDE_DURATION{1};
constexpr uint16_t SET_AMBIENT_PRESSURE_DURATION{1};
constexpr uint16_t GET_AMBIENT_PRESSURE_DURATION{1};
constexpr uint16_t PERFORM_FORCED_CALIBRATION_DURATION{400};
constexpr uint16_t SET_AUTOMATIC_SELF_CALIBRATION_ENABLED_DURATION{1};
constexpr uint16_t GET_AUTOMATIC_SELF_CALIBRATION_ENABLED_DURATION{1};
constexpr uint16_t SET_AUTOMATIC_SELF_CALIBRATION_TARGET_DURATION{1};
constexpr uint16_t GET_AUTOMATIC_SELF_CALIBRATION_TARGET_DURATION{1};
constexpr uint16_t GET_DATA_READY_STATUS_DURATION{1};
constexpr uint16_t PERSIST_SETTINGS_DURATION{800};
constexpr uint16_t GET_SERIAL_NUMBER_DURATION{1};
constexpr uint16_t PERFORM_SELF_TEST_DURATION{10000};
constexpr uint16_t PERFORM_FACTORY_RESET_DURATION{1200};
constexpr uint16_t REINIT_DURATION{20};
/// @endcond
} // namespace scd4x
/*!
@class m5::unit::UnitSCD40
@brief SCD40 unit component
*/
class UnitSCD40 : public Component, public PeriodicMeasurementAdapter<UnitSCD40, scd4x::Data> {
M5_UNIT_COMPONENT_HPP_BUILDER(UnitSCD40, 0x62);
public:
/*!
@struct config_t
@brief Settings for begin
*/
struct config_t {
//! Start periodic measurement on begin?
bool start_periodic{true};
//! Mode of periodic measurement if start on begin?
scd4x::Mode mode{scd4x::Mode::Normal};
//! Enable ASC on begin?
bool calibration{true};
};
explicit UnitSCD40(const uint8_t addr = DEFAULT_ADDRESS)
: Component(addr), _data{new m5::container::CircularBuffer<scd4x::Data>(1)}
{
auto ccfg = component_config();
ccfg.clock = 400 * 1000U;
component_config(ccfg);
}
virtual ~UnitSCD40()
{
}
virtual bool begin() override;
virtual void update(const bool force = false) override;
///@name Settings for begin
///@{
/*! @brief Gets the configration */
inline config_t config() const
{
return _cfg;
}
//! @brief Set the configration
inline void config(const config_t &cfg)
{
_cfg = cfg;
}
///@}
///@name Measurement data by periodic
///@{
//! @brief Oldest measured CO2 concentration (ppm)
inline uint16_t co2() const
{
return !empty() ? oldest().co2() : 0;
}
//! @brief Oldest measured temperature (Celsius)
inline float temperature() const
{
return !empty() ? oldest().temperature() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured temperature (Celsius)
inline float celsius() const
{
return !empty() ? oldest().celsius() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured temperature (Fahrenheit)
inline float fahrenheit() const
{
return !empty() ? oldest().fahrenheit() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured humidity (RH)
inline float humidity() const
{
return !empty() ? oldest().humidity() : std::numeric_limits<float>::quiet_NaN();
}
///@}
///@name Periodic measurement
///@{
/*!
@brief Start periodic measurement
@param mode Measurement mode
@return True if successful
*/
inline bool startPeriodicMeasurement(const scd4x::Mode mode = scd4x::Mode::Normal)
{
return PeriodicMeasurementAdapter<UnitSCD40, scd4x::Data>::startPeriodicMeasurement(mode);
}
/*!
@brief Start low power periodic measurement
@return True if successful
*/
inline bool startLowPowerPeriodicMeasurement()
{
return startPeriodicMeasurement(scd4x::Mode::LowPower);
}
/*!
@brief Stop periodic measurement
@param duration Max command duration(ms)
@return True if successful
*/
inline bool stopPeriodicMeasurement(const uint32_t duration = scd4x::STOP_PERIODIC_MEASUREMENT_DURATION)
{
return PeriodicMeasurementAdapter<UnitSCD40, scd4x::Data>::stopPeriodicMeasurement(duration);
}
///@}
///@name On-Chip Output Signal Compensation
///@{
/*!
@brief Write the temperature offset
@details Define how warm the sensor is compared to ambient, so RH and T
are temperature compensated. Has no effect on the CO2 reading Default offsetis 4C
@param offset (0 <= offset < 175)
@param duration Max command duration(ms)
@return True if successful
@note Recommended temperature offset values are between 0 and 20
@warning During periodic detection runs, an error is returned
*/
bool writeTemperatureOffset(const float offset, const uint32_t duration = scd4x::SET_TEMPERATURE_OFFSET_DURATION);
/*!
@brief Read the temperature offset
@param[out] offset Offset value
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool readTemperatureOffset(float &offset);
/*!
@brief Write the sensor altitude
@details Define the sensor altitude in metres above sea level, so RH and CO2 arecompensated for atmospheric
pressure Default altitude is 0m
@param altitude Sensor altitude [m]
@param duration Max command duration(ms)
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeSensorAltitude(const uint16_t altitude, const uint32_t duration = scd4x::SET_SENSOR_ALTITUDE_DURATION);
/*!
@brief Read the sensor altitude
@param[out] altitude Sensor altitude [m]
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool readSensorAltitude(uint16_t &altitude);
/*!
@brief Write the ambient pressure
@details Define the ambient pressure in Pascals, so RH and CO2 are compensated for atmospheric pressure
setAmbientPressure overrides setSensorAltitude
@param pressure Ambient pressure [hPa]
@param duration Max command duration(ms)
@return True if successful
@warning Valid Valid input values are between 700 1200 hPa
*/
bool writeAmbientPressure(const uint16_t pressure, const uint32_t duration = scd4x::SET_AMBIENT_PRESSURE_DURATION);
/*!
@brief Read the ambient pressure
@param[out] presure Ambient pressure [hPa]
@return True if successful
*/
bool readAmbientPressure(uint16_t &pressure);
///@}
///@name Field Calibration
///@{
/*!
@brief Perform forced recalibration
@param concentration Unit:ppm
@param[out] correction The FRC correction value
@return True if successful
@warning During periodic detection runs, an error is returned
@warning Blocked until the process is completed (about 400ms)
*/
bool performForcedRecalibration(const uint16_t concentration, int16_t &correction);
/*!
@brief Enable/disable automatic self calibration
@param enabled Enable automatic self calibration if true
@param duration Max command duration(ms)
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeAutomaticSelfCalibrationEnabled(
const bool enabled = true, const uint32_t duration = scd4x::SET_AUTOMATIC_SELF_CALIBRATION_ENABLED_DURATION);
/*!
@brief Check if automatic self calibration is enabled
@param[out] enabled True if automatic self calibration is enabled
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool readAutomaticSelfCalibrationEnabled(bool &enabled);
/*!
@brief Write the value of the ASC baseline target
@param ppm ASC target ppm
@param duration Max command duration(ms)
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writeAutomaticSelfCalibrationTarget(
const uint16_t ppm, const uint32_t duration = scd4x::SET_AUTOMATIC_SELF_CALIBRATION_TARGET_DURATION);
/*!
@brief Read the value of the ASC baseline target
@param[out] ppm ASC target ppm
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool readAutomaticSelfCalibrationTarget(uint16_t &ppm);
///@}
///@name Advanced Features
///@{
/*!
@brief Write sensor settings from RAM to EEPROM
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool writePersistSettings(const uint32_t duration = scd4x::PERSIST_SETTINGS_DURATION);
/*!
@brief Read the serial number string
@param[out] serialNumber Output buffer
@return True if successful
@warning Size must be at least 13 bytes
@warning During periodic detection runs, an error is returned
*/
bool readSerialNumber(char *serialNumber);
/*!
@brief Read the serial number value
@param[out] serialNumber serial number value
@return True if successful
@note The serial number is 48 bit
@warning During periodic detection runs, an error is returned
*/
bool readSerialNumber(uint64_t &serialNumber);
/*!
@brief Perform self test
@param[out] True if malfunction detected
@return True if successful
@note Takes 10 seconds to complete
@warning During periodic detection runs, an error is returned
*/
bool performSelfTest(bool &malfunction);
/*!
@brief Peform factory reset
@details Reset all settings to the factory values
@return True if successful
@warning During periodic detection runs, an error is returned
@warning Measurement duration max 1200 ms
*/
bool performFactoryReset(const uint32_t duration = scd4x::PERFORM_FACTORY_RESET_DURATION);
/*!
@brief Re-initialize the sensor, load settings from EEPROM
@return True if successful
@warning During periodic detection runs, an error is returned
@warning Measurement duration max 20 ms
*/
bool reInit(const uint32_t duration = scd4x::REINIT_DURATION);
///@}
protected:
bool read_register(const uint16_t reg, uint8_t *rbuf, const uint32_t rlen, const uint32_t duration = 1);
bool write_register(const uint16_t reg, uint8_t *wbuf, const uint32_t wlen);
bool start_periodic_measurement(const scd4x::Mode mode = scd4x::Mode::Normal);
bool stop_periodic_measurement(const uint32_t duration = scd4x::STOP_PERIODIC_MEASUREMENT_DURATION);
bool read_data_ready_status();
bool read_measurement(scd4x::Data &d, const bool all = true);
virtual bool is_valid_chip();
bool delay_true(const uint32_t duration);
M5_UNIT_COMPONENT_PERIODIC_MEASUREMENT_ADAPTER_HPP_BUILDER(UnitSCD40, scd4x::Data);
protected:
std::unique_ptr<m5::container::CircularBuffer<scd4x::Data>> _data{};
config_t _cfg{};
};
///@cond
namespace scd4x {
namespace command {
// Basic Commands
constexpr uint16_t START_PERIODIC_MEASUREMENT{0x21b1};
constexpr uint16_t READ_MEASUREMENT{0xec05};
constexpr uint16_t STOP_PERIODIC_MEASUREMENT{0x3f86};
// On-chip output signal compensation
constexpr uint16_t SET_TEMPERATURE_OFFSET{0x241d};
constexpr uint16_t GET_TEMPERATURE_OFFSET{0x2318};
constexpr uint16_t SET_SENSOR_ALTITUDE{0x2427};
constexpr uint16_t GET_SENSOR_ALTITUDE{0x2322};
constexpr uint16_t AMBIENT_PRESSURE{0xe000};
// Field calibration
constexpr uint16_t PERFORM_FORCED_CALIBRATION{0x362f};
constexpr uint16_t SET_AUTOMATIC_SELF_CALIBRATION_ENABLED{0x2416};
constexpr uint16_t GET_AUTOMATIC_SELF_CALIBRATION_ENABLED{0x2313};
constexpr uint16_t SET_AUTOMATIC_SELF_CALIBRATION_TARGET{0x243a};
constexpr uint16_t GET_AUTOMATIC_SELF_CALIBRATION_TARGET{0x233f};
// Low power
constexpr uint16_t START_LOW_POWER_PERIODIC_MEASUREMENT{0x21ac};
constexpr uint16_t GET_DATA_READY_STATUS{0xe4b8};
// Advanced features
constexpr uint16_t PERSIST_SETTINGS{0x3615};
constexpr uint16_t GET_SERIAL_NUMBER{0x3682};
constexpr uint16_t PERFORM_SELF_TEST{0x3639};
constexpr uint16_t PERFORM_FACTORY_RESET{0x3632};
constexpr uint16_t REINIT{0x3646};
//
constexpr uint16_t GET_SENSOR_VARIANT{0x202f};
} // namespace command
} // namespace scd4x
///@endcond
} // namespace unit
} // namespace m5
#endif

View file

@ -0,0 +1,178 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_SCD41.cpp
@brief SCD41 Unit for M5UnitUnified
*/
#include "unit_SCD41.hpp"
#include <M5Utility.hpp>
#include <array>
using namespace m5::utility::mmh3;
using namespace m5::unit::types;
using namespace m5::unit::scd4x;
using namespace m5::unit::scd4x::command;
using namespace m5::unit::scd41;
using namespace m5::unit::scd41::command;
namespace {
// Max command duration(ms)
constexpr uint16_t MEASURE_SINGLE_SHOT_DURATION{5000};
constexpr uint16_t MEASURE_SINGLE_SHOT_RHT_ONLY_DURATION{50};
const uint8_t VARIANT_VALUE[2]{0x14, 0x40}; // SCD41
} // namespace
namespace m5 {
namespace unit {
// class UnitSCD41
const char UnitSCD41::name[] = "UnitSCD41";
const types::uid_t UnitSCD41::uid{"UnitSCD41"_mmh3};
const types::attr_t UnitSCD41::attr{attribute::AccessI2C};
bool UnitSCD41::is_valid_chip()
{
uint8_t var[2]{};
if (!read_register(GET_SENSOR_VARIANT, var, 2) || memcmp(var, VARIANT_VALUE, 2) != 0) {
M5_LIB_LOGE("Not SCD41 %02X:%02X", var[0], var[1]);
return false;
}
return true;
}
bool UnitSCD41::measureSingleshot(Data& d)
{
d = Data{};
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (writeRegister(MEASURE_SINGLE_SHOT)) {
m5::utility::delay(MEASURE_SINGLE_SHOT_DURATION);
return read_measurement(d);
}
return false;
}
bool UnitSCD41::measureSingleshotRHT(Data& d)
{
d = Data{};
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (writeRegister(MEASURE_SINGLE_SHOT_RHT_ONLY)) {
m5::utility::delay(MEASURE_SINGLE_SHOT_RHT_ONLY_DURATION);
return read_measurement(d, false);
}
return false;
}
bool UnitSCD41::powerDown(const uint32_t duration)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (writeRegister(POWER_DOWN, nullptr, 0)) {
m5::utility::delay(duration);
return true;
}
return false;
}
bool UnitSCD41::wakeup()
{
constexpr uint32_t WAKE_UP_DURATION{30 + 5};
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
// Note that the SCD4x does not acknowledge the wake_up command
writeRegister(WAKE_UP, nullptr, 0);
m5::utility::delay(WAKE_UP_DURATION);
// The sensors idle state after wake up can be verified by reading out the serial numbe
auto timeout_at = m5::utility::millis() + 1000;
do {
uint64_t sn{};
if (readSerialNumber(sn)) {
return true;
}
m5::utility::delay(10);
} while (m5::utility::millis() <= timeout_at);
return false;
}
bool UnitSCD41::writeAutomaticSelfCalibrationInitialPeriod(const uint16_t hours, const uint32_t duration)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (hours % 4) {
M5_LIB_LOGW("Arguments are modified to multiples of 4");
}
uint16_t h = (hours >> 2) << 2;
m5::types::big_uint16_t u16{h};
return write_register(SET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD, u16.data(), u16.size()) &&
delay_true(duration);
}
bool UnitSCD41::readAutomaticSelfCalibrationInitialPeriod(uint16_t& hours)
{
hours = 0;
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16{};
if (read_register(GET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD, u16.data(), u16.size(),
GET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD_DURATION)) {
hours = u16.get();
return true;
}
return false;
}
bool UnitSCD41::writeAutomaticSelfCalibrationStandardPeriod(const uint16_t hours, const uint32_t duration)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (hours % 4) {
M5_LIB_LOGW("Arguments are modified to multiples of 4");
}
uint16_t h = (hours >> 2) << 2;
m5::types::big_uint16_t u16{h};
return write_register(SET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD, u16.data(), u16.size()) &&
delay_true(duration);
}
bool UnitSCD41::readAutomaticSelfCalibrationStandardPeriod(uint16_t& hours)
{
hours = 0;
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
m5::types::big_uint16_t u16{};
if (read_register(GET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD, u16.data(), u16.size(),
GET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD_DURATION)) {
hours = u16.get();
return true;
}
return false;
}
} // namespace unit
} // namespace m5

View file

@ -0,0 +1,157 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_SCD41.hpp
@brief SCD41 Unit for M5UnitUnified
*/
#ifndef M5_UNIT_ENV_UNIT_SCD41_HPP
#define M5_UNIT_ENV_UNIT_SCD41_HPP
#include "unit_SCD40.hpp"
namespace m5 {
namespace unit {
/*!
@namespace scd41
@brief For SCD41
*/
namespace scd41 {
///@cond
// Max command duration(ms)
// For SCD40/41
constexpr uint32_t POWER_DOWN_DURATION{1};
constexpr uint32_t GET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD_DURATION{1};
constexpr uint32_t SET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD_DURATION{1};
constexpr uint32_t GET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD_DURATION{1};
constexpr uint32_t SET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD_DURATION{1};
///@endcond
} // namespace scd41
/*!
@class m5::unit::UnitSCD41
@brief SCD41 unit component
*/
class UnitSCD41 : public UnitSCD40 {
M5_UNIT_COMPONENT_HPP_BUILDER(UnitSCD41, 0x62);
public:
explicit UnitSCD41(const uint8_t addr = DEFAULT_ADDRESS) : UnitSCD40(addr)
{
auto ccfg = component_config();
ccfg.clock = 400 * 1000U;
component_config(ccfg);
}
virtual ~UnitSCD41()
{
}
///@name Single Shot Measurement Mode
///@{
/*!
@brief Request a single measurement
@param[out] d Measurement data
@return True if successful
@note Blocked until measurement results are acquired (5000 ms)
@warning During periodic detection runs, an error is returned
*/
bool measureSingleshot(scd4x::Data& d);
/*!
@brief Request a single measurement temperature and humidity
@param[out] d Measurement data
@return True if successful
@note Values are updated at 50 ms interval
@note Blocked until measurement results are acquired (50 ms)
@warning Information on CO2 is invalid.
@warning During periodic detection runs, an error is returned
*/
bool measureSingleshotRHT(scd4x::Data& d);
///@}
///@name Power mode
///@{
/*!
@brief Power down
@details The sensor into sleep mode
@param duration Max command duration(ms)
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool powerDown(const uint32_t duration = scd41::POWER_DOWN_DURATION);
/*!
@brief Wake up
@details The sensor from sleep mode into idle mode
@param duration Max command duration(ms)
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool wakeup();
///@}
///@name For ASC(Auto Self-Calibration)
///@{
/*!
@brief Write the duration of the initial period for ASC correction
@param hours ASC initial period
@param duration Max command duration(ms)
@return True if successful
@warning During periodic detection runs, an error is returned
@warning Allowed values are integer multiples of 4 hours
*/
bool writeAutomaticSelfCalibrationInitialPeriod(
const uint16_t hours, const uint32_t duration = scd41::SET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD_DURATION);
/*!
@brief Read the duration of the initial period for ASC correction
@param[out] hours ASC initial period
@param duration Max command duration(ms)
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool readAutomaticSelfCalibrationInitialPeriod(uint16_t& hours);
/*!
@brief Write the standard period for ASC correction
@param hours ASC standard period
@param duration Max command duration(ms)
@return True if successful
@warning During periodic detection runs, an error is returned
@warning Allowed values are integer multiples of 4 hours
*/
bool writeAutomaticSelfCalibrationStandardPeriod(
const uint16_t hours, const uint32_t duration = scd41::SET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD_DURATION);
/*!
@brief Red the standard period for ASC correction
@param[iut] hours ASC standard period
@return True if successful
@warning During periodic detection runs, an error is returned
@warning Allowed values are integer multiples of 4 hours
*/
bool readAutomaticSelfCalibrationStandardPeriod(uint16_t& hours);
///@}
protected:
virtual bool is_valid_chip() override;
};
namespace scd41 {
///@cond
namespace command {
// Low power single shot - SCD41 only
constexpr uint16_t MEASURE_SINGLE_SHOT{0x219d};
constexpr uint16_t MEASURE_SINGLE_SHOT_RHT_ONLY{0x2196};
constexpr uint16_t POWER_DOWN{0x36e0};
constexpr uint16_t WAKE_UP{0x36f6};
constexpr uint16_t SET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD{0x2445};
constexpr uint16_t GET_AUTOMATIC_SELF_CALIBRATION_INITIAL_PERIOD{0x2340};
constexpr uint16_t SET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD{0x244e};
constexpr uint16_t GET_AUTOMATIC_SELF_CALIBRATION_STANDARD_PERIOD{0x234b};
} // namespace command
///@endcond
} // namespace scd41
} // namespace unit
} // namespace m5
#endif

View file

@ -0,0 +1,358 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_SGP30.cpp
@brief SGP30 Unit for M5UnitUnified
*/
#include "unit_SGP30.hpp"
#include <M5Utility.hpp>
#include <array>
#include <cmath>
#include <limits>
using namespace m5::utility::mmh3;
using namespace m5::unit::types;
using namespace m5::unit::sgp30;
using namespace m5::unit::sgp30::command;
namespace {
// Supported lower limit version
constexpr uint8_t lower_limit_version{0x20};
// constexpr elapsed_time_t BASELINE_INTERVAL{1000 * 60 * 60}; // 1 hour (ms)
inline bool delayMeasurementDuration(const uint16_t ms)
{
m5::utility::delay(ms);
return true;
}
} // namespace
namespace m5 {
namespace unit {
namespace sgp30 {
uint16_t Data::co2eq() const
{
// M5_LIB_LOGE(">>> %x:%x => %u", raw[0], raw[1], m5::types::big_uint16_t(raw[0], raw[1]).get());
return m5::types::big_uint16_t(raw[0], raw[1]).get();
}
uint16_t Data::tvoc() const
{
return m5::types::big_uint16_t(raw[3], raw[4]).get();
}
} // namespace sgp30
// class UnitSGP30
const char UnitSGP30::name[] = "UnitSGP30";
const types::uid_t UnitSGP30::uid{"UnitSGP30"_mmh3};
const types::attr_t UnitSGP30::attr{attribute::AccessI2C};
bool UnitSGP30::begin()
{
auto ssize = stored_size();
assert(ssize && "stored_size must be greater than zero");
if (ssize != _data->capacity()) {
_data.reset(new m5::container::CircularBuffer<Data>(ssize));
if (!_data) {
M5_LIB_LOGE("Failed to allocate");
return false;
}
}
m5::utility::delay(1);
Feature f{};
if (!readFeatureSet(f)) {
M5_LIB_LOGE("Failed to read feature");
return false;
}
if (f.productType() != 0) {
// May be SGPC3 gas sensor if value is 1
M5_LIB_LOGE("This unit is NOT SGP30");
return false;
}
_version = f.productVersion();
if (_version < lower_limit_version) {
M5_LIB_LOGE("Not enough the product version %x", _version);
return false;
}
return _cfg.start_periodic
? startPeriodicMeasurement(_cfg.baseline_co2eq, _cfg.baseline_tvoc, _cfg.humidity, _cfg.interval)
: true;
}
void UnitSGP30::update(const bool force)
{
_updated = false;
if (_periodic) {
elapsed_time_t at{m5::utility::millis()};
if (_waiting) {
_waiting = (at < _can_measure_time);
return;
}
if (force || !_latest || at >= _latest + _interval) {
Data d{};
_updated = read_measurement(d);
if (_updated) {
_latest = at;
_data->push_back(d);
}
}
}
}
bool UnitSGP30::start_periodic_measurement(const uint16_t co2eq, const uint16_t tvoc, const uint16_t humidity,
const uint32_t interval, const uint32_t duration)
{
// Baseline and absolute humidity restoration must take place during
// this 15-second period
return start_periodic_measurement(interval, duration) && write_iaq_baseline(co2eq, tvoc) &&
writeAbsoluteHumidity(humidity);
}
bool UnitSGP30::start_periodic_measurement(const uint32_t interval, const uint32_t duration)
{
if (inPeriodic()) {
return false;
}
if (interval < sgp30::MEASURE_IAQ_DURATION) {
M5_LIB_LOGE("Interval too short %u. Must ne greater equal %u", interval, sgp30::MEASURE_IAQ_DURATION);
return false;
}
if (writeRegister(IAQ_INIT)) {
// For the first 15s after the “sgp30_iaq_init” command the sensor
// is an initialization phase during which a “sgp30_measure_iaq”
// command returns fixed values of 400 ppm CO2eq and 0 ppb TVOC.A
// new “sgp30_iaq_init” command has to be sent after every power-up
// or soft reset.
// Baseline and absolute humidity restoration must take place during
// this 15-second period
_can_measure_time = m5::utility::millis() + 15 * 1000;
_periodic = true;
_latest = 0;
_waiting = true;
_interval = interval;
m5::utility::delay(duration);
}
return _periodic;
}
bool UnitSGP30::stop_periodic_measurement()
{
_periodic = false;
return true;
}
bool UnitSGP30::readRaw(uint16_t& h2, uint16_t& ethanol)
{
std::array<uint8_t, 6> rbuf{};
h2 = ethanol = 0;
if (readRegister(MEASURE_RAW, rbuf.data(), rbuf.size(), MEASURE_RAW_DURATION)) {
m5::utility::CRC8_Checksum crc{};
if (crc.range(rbuf.data(), 2) == rbuf[2] && crc.range(rbuf.data() + 3, 2) == rbuf[5]) {
h2 = m5::types::big_uint16_t(rbuf[0], rbuf[1]).get();
ethanol = m5::types::big_uint16_t(rbuf[3], rbuf[4]).get();
}
return true;
}
return false;
}
bool UnitSGP30::readRaw(float& h2, float& ethanol)
{
uint16_t hh{}, et{};
h2 = ethanol = std::numeric_limits<float>::quiet_NaN();
if (!readRaw(hh, et)) {
return false;
}
h2 = 0.5f * std::exp((13119 - hh) / 512.f);
ethanol = 0.4f * std::exp((18472 - et) / 512.f);
return true;
}
bool UnitSGP30::readIaqBaseline(uint16_t& co2eq, uint16_t& tvoc)
{
std::array<uint8_t, 6> rbuf{};
co2eq = tvoc = 0;
if (readRegister(GET_IAQ_BASELINE, rbuf.data(), rbuf.size(), GET_IAQ_BASELINE_DURATION)) {
m5::utility::CRC8_Checksum crc{};
if (crc.range(rbuf.data(), 2) == rbuf[2] && crc.range(rbuf.data() + 3, 2) == rbuf[5]) {
co2eq = m5::types::big_uint16_t(rbuf[0], rbuf[1]).get();
tvoc = m5::types::big_uint16_t(rbuf[3], rbuf[4]).get();
return true;
}
}
return false;
}
bool UnitSGP30::writeAbsoluteHumidity(const uint16_t raw, const uint32_t duration)
{
m5::utility::CRC8_Checksum crc;
std::array<uint8_t, 3> buf{};
m5::types::big_uint16_t rr(raw);
std::memcpy(buf.data(), rr.data(), 2);
buf[2] = crc.range(rr.data(), 2);
return writeRegister(SET_ABSOLUTE_HUMIDITY, buf.data(), buf.size()) && delayMeasurementDuration(duration);
}
bool UnitSGP30::writeAbsoluteHumidity(const float gm3, const uint32_t duration)
{
int32_t tmp = static_cast<int32_t>(std::round(gm3 * 256.f));
if (tmp > 32767 || tmp < -32768) {
M5_LIB_LOGE("Over/underflow: %f / %d", gm3, tmp);
return false;
}
return writeAbsoluteHumidity(static_cast<uint16_t>(static_cast<int16_t>(tmp)), duration);
}
bool UnitSGP30::measureTest(uint16_t& result)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
std::array<uint8_t, 3> rbuf{};
if (readRegister(MEASURE_TEST, rbuf.data(), rbuf.size(), MEASURE_TEST_DURATION)) {
m5::utility::CRC8_Checksum crc;
if (crc.range(rbuf.data(), 2) == rbuf[2]) {
result = m5::types::big_uint16_t(rbuf[0], rbuf[1]).get();
return true;
}
}
return false;
}
#if 0
bool UnitSGP30::readTvocInceptiveBaseline(uint16_t& inceptive_tvoc) {
if (_version < 0x21) {
M5_LIB_LOGE("Not enough the product version %x", _version);
return false;
}
std::array<uint8_t, 3> rbuf{};
if (readRegister(GET_TVOC_INCEPTIVE_BASELINE, rbuf.data(), rbuf.size(), GET_TVOC_INCEPTIVE_BASELINE_DURATION)) {
m5::utility::CRC8_Checksum crc;
if (crc.range(rbuf.data(), 2) == rbuf[2]) {
inceptive_tvoc = m5::types::big_uint16_t(rbuf[0], rbuf[1]).get();
return true;
}
}
return false;
}
bool UnitSGP30::writeTvocInceptiveBaseline(const uint16_t inceptive_tvoc, const uint32_t duration) {
if (_version < 0x21) {
M5_LIB_LOGE("Not enough the product version %x", _version);
return false;
}
M5_LIB_LOGW(">>>>>> %u", inceptive_tvoc);
m5::utility::CRC8_Checksum crc;
std::array<uint8_t, 3> buf{};
m5::types::big_uint16_t tt(inceptive_tvoc);
std::memcpy(buf.data(), tt.data(), 2);
buf[2] = crc.range(tt.data(), 2);
return writeRegister(SET_TVOC_INCEPTIVE_BASELINE, buf.data(), buf.size()) && delayMeasurementDuration(duration);
}
#endif
bool UnitSGP30::generalReset()
{
uint8_t cmd{0x06};
if (generalCall(&cmd, 1)) {
_periodic = false;
m5::utility::delay(10);
return true;
}
return false;
}
bool UnitSGP30::readFeatureSet(sgp30::Feature& feature)
{
std::array<uint8_t, 3> rbuf{};
if (readRegister(GET_FEATURE_SET, rbuf.data(), rbuf.size(), GET_FEATURE_SET_DURATION)) {
m5::utility::CRC8_Checksum crc;
if (crc.range(rbuf.data(), 2) == rbuf[2]) {
feature.value = m5::types::big_uint16_t(rbuf[0], rbuf[1]).get();
return true;
}
}
return false;
}
bool UnitSGP30::readSerialNumber(uint64_t& number)
{
std::array<uint8_t, 9> rbuf{};
number = 0;
if (readRegister(GET_SERIAL_ID, rbuf.data(), rbuf.size(), GET_SERIAL_ID_DURATION)) {
m5::utility::CRC8_Checksum crc;
for (uint_fast8_t i = 0; i < 3; i++) {
if (crc.range(rbuf.data() + i * 3, 2) != rbuf[i * 3 + 2]) {
return false;
}
}
for (uint_fast8_t i = 0; i < 3; ++i) {
number |= ((uint64_t)(m5::types::big_uint16_t(rbuf[i * 3], rbuf[i * 3 + 1]).get())) << (16U * (2 - i));
}
return true;
}
return false;
}
bool UnitSGP30::readSerialNumber(char* number)
{
if (!number) {
return false;
}
*number = '\0';
uint64_t sno{};
if (readSerialNumber(sno)) {
uint_fast8_t i{12};
while (i--) {
*number++ = m5::utility::uintToHexChar((sno >> (i * 4)) & 0x0F);
}
*number = '\0';
return true;
}
return false;
}
//
bool UnitSGP30::write_iaq_baseline(const uint16_t co2eq, const uint16_t tvoc)
{
m5::utility::CRC8_Checksum crc{};
m5::types::big_uint16_t cc(co2eq);
m5::types::big_uint16_t tt(tvoc);
std::array<uint8_t, (2 + 1) * 2> buf{};
// Note that the order is different for get and set
std::memcpy(buf.data() + 0, tt.data(), 2);
buf[2] = crc.range(tt.data(), 2);
std::memcpy(buf.data() + 3, cc.data(), 2);
buf[5] = crc.range(cc.data(), 2);
return writeRegister(SET_IAQ_BASELINE, buf.data(), buf.size());
}
bool UnitSGP30::read_measurement(Data& d)
{
if (readRegister(MEASURE_IAQ, d.raw.data(), d.raw.size(), MEASURE_IAQ_DURATION)) {
m5::utility::CRC8_Checksum crc{};
return crc.range(d.raw.data(), 2) == d.raw[2] && crc.range(d.raw.data() + 3, 2) == d.raw[5];
}
return false;
}
} // namespace unit
} // namespace m5

View file

@ -0,0 +1,360 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_SGP30.hpp
@brief SGP30 Unit for M5UnitUnified
*/
#ifndef M5_UNIT_TVOC_UNIT_SGP30_HPP
#define M5_UNIT_TVOC_UNIT_SGP30_HPP
#include <M5UnitComponent.hpp>
#include <m5_utility/container/circular_buffer.hpp>
#include <array>
namespace m5 {
namespace unit {
/*!
@namespace sgp30
@brief For SGP30
*/
namespace sgp30 {
///@cond
// Max command duration(ms)
// Max measurement duration (ms)
constexpr uint16_t IAQ_INIT_DURATION{10};
constexpr uint16_t MEASURE_IAQ_DURATION{12};
constexpr uint16_t GET_IAQ_BASELINE_DURATION{10};
constexpr uint16_t SET_IAQ_BASELINE_DURATION{10};
constexpr uint16_t SET_ABSOLUTE_HUMIDITY_DURATION{10};
constexpr uint16_t MEASURE_TEST_DURATION{220};
constexpr uint16_t GET_FEATURE_SET_DURATION{10};
constexpr uint16_t MEASURE_RAW_DURATION{25};
constexpr uint16_t GET_TVOC_INCEPTIVE_BASELINE_DURATION{10};
constexpr uint16_t SET_TVOC_INCEPTIVE_BASELINE_DURATION{10};
constexpr uint16_t GET_SERIAL_ID_DURATION{10};
///@endcond
/*!
@struct Feature
@brief Structure of the SGP feature set number
*/
struct Feature {
//! @brief product type (SGP30: 0)
uint8_t productType() const
{
return (value >> 12) & 0x0F;
}
/*!
@brief product version
@note Please note that the last 5 bits of the productversion (bits 12-16
of the LSB) are subject to change
@note This is used to track new features added tothe SGP multi-pixel
platform
*/
uint8_t productVersion() const
{
return value & 0xFF;
}
uint16_t value{};
};
/*!
@struct Data
@brief Measurement data group
*/
struct Data {
std::array<uint8_t, 6> raw{}; //!< RAW data
uint16_t co2eq() const; //!< Co2Eq (ppm)
uint16_t tvoc() const; //!< TVOC (pbb)
};
} // namespace sgp30
/*!
@class UnitSGP30
@brief SGP30 unit
*/
class UnitSGP30 : public Component, public PeriodicMeasurementAdapter<UnitSGP30, sgp30::Data> {
M5_UNIT_COMPONENT_HPP_BUILDER(UnitSGP30, 0x58);
public:
/*!
@struct config_t
@brief Settings for begin
*/
struct config_t {
//! Start periodic measurement on begin()?
bool start_periodic{true};
//! Baseline CO2eq initial value if start on begin
uint16_t baseline_co2eq{};
//! Baseline TVOC initial value if start on begin
uint16_t baseline_tvoc{};
//! Absolute humidity initiali value if start on begin
uint16_t humidity{};
/*!
Inceptive Baseline for TVOC measurements initial value if start on begin
@warning The application of this feature is solely limited to the very
first start-up period of an SGP sensor
*/
uint16_t inceptive_tvoc{};
//! Periodic measurement interval if start on begin
int32_t interval{1000};
};
explicit UnitSGP30(const uint8_t addr = DEFAULT_ADDRESS)
: Component(addr), _data{new m5::container::CircularBuffer<sgp30::Data>(1)}
{
auto ccfg = component_config();
ccfg.clock = 400 * 1000U;
component_config(ccfg);
}
virtual ~UnitSGP30()
{
}
virtual bool begin() override;
virtual void update(const bool force = false) override;
///@name Settings for begin
///@{
/*! @brief Gets the configration */
inline config_t config() const
{
return _cfg;
}
//! @brief Set the configration
inline void config(const config_t& cfg)
{
_cfg = cfg;
}
///@}
///@name Properties
///@{
/*!
@brief Gets the product version
@warning Calling after the call of begin()
*/
inline uint8_t productVersion() const
{
return _version;
}
/*!
@brief Can it be measured?
@return True if it can
@warning After the start of a periodic measurement, it is necessary to wait 15 seconds to obtain a valid
measurement value
*/
inline bool canMeasurePeriodic() const
{
return inPeriodic() && !_waiting;
}
///@}
///@name Measurement data by periodic
///@{
//! @brief Oldest CO2eq (ppm)
inline uint16_t co2eq() const
{
return !empty() ? oldest().co2eq() : 0xFFFF;
}
//! @brief Oldest TVOC (ppb)
inline uint16_t tvoc() const
{
return !empty() ? oldest().tvoc() : 0xFFFF;
}
///@}
///@name Periodic measurement
///@{
/*!
@brief Start periodic measurement
@details Specify settings and measure
@param co2eq iaq baseline for CO2eq
@param tvoc iaq baseline for TVOC
@param humidity absolute humidity (disable if zero)
@param interval Measurement Interval(ms)
@param duration Max command duration(ms)
@return True if successful
@note 15 seconds wait is required before a valid measurement can be initiated (warmup)
@note In update(), waiting are taken into account
*/
inline bool startPeriodicMeasurement(const uint16_t co2eq, const uint16_t tvoc, const uint16_t humidity,
const uint32_t interval = 1000U,
const uint32_t duration = sgp30::IAQ_INIT_DURATION)
{
return PeriodicMeasurementAdapter<UnitSGP30, sgp30::Data>::startPeriodicMeasurement(co2eq, tvoc, humidity,
interval, duration);
}
/*!
@brief Start periodic measurement
@details Measuring in the current settings
@param interval Measurement Interval(ms)
@param duration Max command duration(ms)
@return True if successful
@note 15 seconds wait is required before a valid measurement can be
initiated.
@note In update(), waiting are taken into account
*/
inline bool startPeriodicMeasurement(const uint32_t interval = 1000U,
const uint32_t duration = sgp30::IAQ_INIT_DURATION)
{
return PeriodicMeasurementAdapter<UnitSGP30, sgp30::Data>::startPeriodicMeasurement(interval, duration);
}
/*!
@brief Stop periodic measurement
@return True if successful
*/
inline bool stopPeriodicMeasurement()
{
return PeriodicMeasurementAdapter<UnitSGP30, sgp30::Data>::stopPeriodicMeasurement();
}
///@}
///@name Correction
///@{
/*!
@brief Read the IAQ baseline
@param[out] co2eq iaq baseline for CO2
@param[out] tvoc iaq baseline for TVOC
@return True if successful
*/
bool readIaqBaseline(uint16_t& co2eq, uint16_t& tvoc);
/*!
@brief Write the absolute humidity
@param raw absolute humidity (disable if zero)
@param duration Max command duration(ms)
@return True if successful
@note Value is a fixed-point 8.8bit number
*/
bool writeAbsoluteHumidity(const uint16_t raw, const uint32_t duration = sgp30::SET_ABSOLUTE_HUMIDITY_DURATION);
/*!
@brief Write the absolute humidity
@param gm3 absolute humidity (g/m^3)
@param duration Max command duration(ms)
@return True if successful
*/
bool writeAbsoluteHumidity(const float gm3, const uint32_t duration = sgp30::SET_ABSOLUTE_HUMIDITY_DURATION);
#if 0
/*!
@brief Read the inceptive Basebine for TVOC
@param[out] inceptive_tvoc Inceptive baseline
@return True if successful
@warning Only available if product version is 0x21 or higher
*/
bool readTvocInceptiveBaseline(uint16_t& inceptive_tvoc);
/*!
@brief Write the inceptive Basebine for TVOC
@param inceptive_tvoc Inceptive baseline
@param duration Max command duration(ms)
@return True if successful
@warning The application of this feature is solely limited to the very
first start-up period of an SGP sensor
@warning Only available if product version is 0x21 or higher
*/
bool writeTvocInceptiveBaseline(const uint16_t inceptive_tvoc,
const uint32_t duration = sgp30::SET_TVOC_INCEPTIVE_BASELINE_DURATION);
#endif
///@}
/*!
@brief Read sensor raw signals
@param[out] h2 H2 concentration(raw)
@param[out] ethanol Ethanol concentration(raw)
@return True if successful
*/
bool readRaw(uint16_t& h2, uint16_t& ethanol);
/*!
@brief Read H2/Ethanol concentration
@param[out] h2 H2 concentration(ppm)
@param[out] ethanol Ethanol concentration(rppm
@return True if successful
@note Outputs the value calculated from the output of readRaw
*/
bool readRaw(float& h2, float& ethanol);
/*!
@brief Run the on-chip self-test
@param[out] result self-test return code
@return True if successful
@note If the test is OK, the result will be 0xd400
@warning Must not be executed after startPeriodicMeasurement
*/
bool measureTest(uint16_t& result);
/*!
@brief General reset
@details Reset using I2C general call
@warning This is a reset by General command, the command is also
sent to all devices with I2C connections
@return True if successful
*/
bool generalReset();
/*!
@brief Read the feature set
@param feature
@return True if successful
*/
bool readFeatureSet(sgp30::Feature& feature);
/*!
@brief Read the serial number
@return True if successful
@note Serial number is 48bits
*/
bool readSerialNumber(uint64_t& number);
/*!
@brief Read the serial number string
@param[out] number Output buffer
@return True if successful
@warning number must be at least 13 bytes
*/
bool readSerialNumber(char* number);
protected:
bool start_periodic_measurement(const uint16_t co2eq, const uint16_t tvoc, const uint16_t humidity,
const uint32_t interval, const uint32_t duration = sgp30::IAQ_INIT_DURATION);
bool start_periodic_measurement(const uint32_t interval, const uint32_t duration = sgp30::IAQ_INIT_DURATION);
bool stop_periodic_measurement();
bool write_iaq_baseline(const uint16_t co2eq, const uint16_t tvoc);
bool read_measurement(sgp30::Data& d);
M5_UNIT_COMPONENT_PERIODIC_MEASUREMENT_ADAPTER_HPP_BUILDER(UnitSGP30, sgp30::Data);
protected:
uint8_t _version{}; // Chip version
bool _waiting{};
types::elapsed_time_t _can_measure_time{};
std::unique_ptr<m5::container::CircularBuffer<sgp30::Data>> _data{};
config_t _cfg{};
};
namespace sgp30 {
namespace command {
///@cond
constexpr uint16_t IAQ_INIT{0x2003};
constexpr uint16_t MEASURE_IAQ{0x2008};
constexpr uint16_t GET_IAQ_BASELINE{0x2015};
constexpr uint16_t SET_IAQ_BASELINE{0x201E};
constexpr uint16_t SET_ABSOLUTE_HUMIDITY{0x2061};
constexpr uint16_t MEASURE_TEST{0x2032};
constexpr uint16_t GET_FEATURE_SET{0x202F};
constexpr uint16_t MEASURE_RAW{0x2050};
constexpr uint16_t GET_TVOC_INCEPTIVE_BASELINE{0x20B3};
constexpr uint16_t SET_TVOC_INCEPTIVE_BASELINE{0x2077};
constexpr uint16_t GET_SERIAL_ID{0x3682};
///@endcond
} // namespace command
} // namespace sgp30
} // namespace unit
} // namespace m5
#endif

View file

@ -0,0 +1,350 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_SHT30.cpp
@brief SHT30 Unit for M5UnitUnified
*/
#include "unit_SHT30.hpp"
#include <M5Utility.hpp>
#include <limits> // NaN
#include <array>
using namespace m5::utility::mmh3;
using namespace m5::unit::types;
using namespace m5::unit::sht30;
using namespace m5::unit::sht30::command;
namespace {
struct Temperature {
constexpr static float toFloat(const uint16_t u16)
{
return -45 + u16 * 175 / 65535.f; // -45 + 175 * S / (2^16 - 1)
}
};
// After sending a command to the sensor a minimalwaiting time of 1ms is needed
// before another commandcan be received by the sensor.
bool delay1()
{
m5::utility::delay(1);
return true;
}
constexpr uint16_t periodic_cmd[] = {
// 0.5 mps
START_PERIODIC_MPS_HALF_HIGH,
START_PERIODIC_MPS_HALF_MEDIUM,
START_PERIODIC_MPS_HALF_LOW,
// 1 mps
START_PERIODIC_MPS_1_HIGH,
START_PERIODIC_MPS_1_MEDIUM,
START_PERIODIC_MPS_1_LOW,
// 2 mps
START_PERIODIC_MPS_2_HIGH,
START_PERIODIC_MPS_2_MEDIUM,
START_PERIODIC_MPS_2_LOW,
// 4 mps
START_PERIODIC_MPS_4_HIGH,
START_PERIODIC_MPS_4_MEDIUM,
START_PERIODIC_MPS_4_LOW,
// 10 mps
START_PERIODIC_MPS_10_HIGH,
START_PERIODIC_MPS_10_MEDIUM,
START_PERIODIC_MPS_10_LOW,
};
constexpr elapsed_time_t interval_table[] = {
2000, // 0.5
1000, // 1
500, // 2
250, // 4
100, // 10
};
} // namespace
namespace m5 {
namespace unit {
namespace sht30 {
float Data::celsius() const
{
return Temperature::toFloat(m5::types::big_uint16_t(raw[0], raw[1]).get());
}
float Data::fahrenheit() const
{
return celsius() * 9.0f / 5.0f + 32.f;
}
float Data::humidity() const
{
return 100.f * m5::types::big_uint16_t(raw[3], raw[4]).get() / 65536.f;
}
} // namespace sht30
const char UnitSHT30::name[] = "UnitSHT30";
const types::uid_t UnitSHT30::uid{"UnitSHT30"_mmh3};
const types::attr_t UnitSHT30::attr{attribute::AccessI2C};
bool UnitSHT30::begin()
{
auto ssize = stored_size();
assert(ssize && "stored_size must be greater than zero");
if (ssize != _data->capacity()) {
_data.reset(new m5::container::CircularBuffer<Data>(ssize));
if (!_data) {
M5_LIB_LOGE("Failed to allocate");
return false;
}
}
if (!stopPeriodicMeasurement()) {
M5_LIB_LOGE("Failed to stop");
return false;
}
if (!softReset()) {
M5_LIB_LOGE("Failed to reset");
return false;
}
uint32_t sn{};
if (!readSerialNumber(sn)) {
M5_LIB_LOGE("Failed to readSerialNumber %x", sn);
return false;
}
auto r = _cfg.start_heater ? startHeater() : stopHeater();
if (!r) {
M5_LIB_LOGE("Failed to heater %d", _cfg.start_heater);
return false;
}
return _cfg.start_periodic ? startPeriodicMeasurement(_cfg.mps, _cfg.repeatability) : true;
}
void UnitSHT30::update(const bool force)
{
_updated = false;
if (inPeriodic()) {
elapsed_time_t at{m5::utility::millis()};
if (force || !_latest || at >= _latest + _interval) {
if (writeRegister(READ_MEASUREMENT)) {
Data d{};
_updated = read_measurement(d);
if (_updated) {
_latest = at;
_data->push_back(d);
}
}
}
}
}
bool UnitSHT30::measureSingleshot(Data& d, const sht30::Repeatability rep, const bool stretch)
{
constexpr uint16_t cmd[] = {
// Enable clock stretching
SINGLE_SHOT_ENABLE_STRETCH_HIGH,
SINGLE_SHOT_ENABLE_STRETCH_MEDIUM,
SINGLE_SHOT_ENABLE_STRETCH_LOW,
// Disable clock stretching
SINGLE_SHOT_DISABLE_STRETCH_HIGH,
SINGLE_SHOT_DISABLE_STRETCH_MEDIUM,
SINGLE_SHOT_DISABLE_STRETCH_LOW,
};
// Latency when clock stretching is disabled
constexpr elapsed_time_t ms[] = {
15,
6,
4,
};
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
uint32_t idx = m5::stl::to_underlying(rep) + (stretch ? 0 : 3);
if (idx >= m5::stl::size(cmd)) {
M5_LIB_LOGE("Invalid arg : %u", (int)rep);
return false;
}
if (writeRegister(cmd[idx])) {
m5::utility::delay(stretch ? 1 : ms[m5::stl::to_underlying(rep)]);
return read_measurement(d);
}
return false;
}
bool UnitSHT30::start_periodic_measurement(const sht30::MPS mps, const sht30::Repeatability rep)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
_periodic = writeRegister(periodic_cmd[m5::stl::to_underlying(mps) * 3 + m5::stl::to_underlying(rep)]);
if (_periodic) {
_interval = interval_table[m5::stl::to_underlying(mps)];
m5::utility::delay(16);
return true;
}
return _periodic;
}
bool UnitSHT30::stop_periodic_measurement()
{
if (writeRegister(STOP_PERIODIC_MEASUREMENT)) {
_periodic = false;
_latest = 0;
// Upon reception of the break command the sensor will abort the
// ongoing measurement and enter the single shot mode. This takes
// 1ms
return delay1();
}
return false;
}
bool UnitSHT30::writeModeAccelerateResponseTime()
{
if (!inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are NOT running");
return false;
}
if (writeRegister(ACCELERATED_RESPONSE_TIME)) {
_interval = 1000 / 4; // 4mps
m5::utility::delay(16);
return true;
}
return false;
}
bool UnitSHT30::readStatus(sht30::Status& s)
{
std::array<uint8_t, 3> rbuf{};
if (readRegister(READ_STATUS, rbuf.data(), rbuf.size(), 0) &&
m5::utility::CRC8_Checksum().range(rbuf.data(), 2) == rbuf[2]) {
s.value = m5::types::big_uint16_t(rbuf[0], rbuf[1]).get();
return true;
}
return false;
}
bool UnitSHT30::clearStatus()
{
return writeRegister(CLEAR_STATUS) && delay1();
}
bool UnitSHT30::softReset()
{
if (inPeriodic()) {
M5_LIB_LOGE("Periodic measurements are running");
return false;
}
if (writeRegister(SOFT_RESET)) {
// Max 1.5 ms
// Time between ACK of soft reset command and sensor entering idle
// state
m5::utility::delay(2);
return true;
}
return false;
}
bool UnitSHT30::generalReset()
{
uint8_t cmd{0x06}; // reset command
if (!clearStatus()) {
return false;
}
// Reset does not return ACK, which is an error, but should be ignored
generalCall(&cmd, 1);
m5::utility::delay(1);
auto timeout_at = m5::utility::millis() + 10;
bool done{};
do {
Status s{};
// The ALERT pin will also become active (high) after powerup and
// after resets
if (readStatus(s) && (s.reset() || s.alertPending())) {
done = true;
break;
}
m5::utility::delay(1);
} while (!done && m5::utility::millis() <= timeout_at);
return done;
}
bool UnitSHT30::startHeater()
{
return writeRegister(START_HEATER) && delay1();
}
bool UnitSHT30::stopHeater()
{
return writeRegister(STOP_HEATER) && delay1();
}
bool UnitSHT30::readSerialNumber(uint32_t& serialNumber)
{
serialNumber = 0;
if (inPeriodic()) {
M5_LIB_LOGE("Periodic measurements are running");
return false;
}
std::array<uint8_t, 6> rbuf;
if (readRegister(GET_SERIAL_NUMBER_ENABLE_STRETCH, rbuf.data(), rbuf.size(), 0)) {
m5::types::big_uint16_t u16[2]{{rbuf[0], rbuf[1]}, {rbuf[3], rbuf[4]}};
m5::utility::CRC8_Checksum crc{};
if (crc.range(u16[0].data(), u16[0].size()) == rbuf[2] && crc.range(u16[1].data(), u16[1].size()) == rbuf[5]) {
serialNumber = ((uint32_t)u16[0].get()) << 16 | ((uint32_t)u16[1].get());
return true;
}
}
return false;
}
bool UnitSHT30::readSerialNumber(char* serialNumber)
{
if (!serialNumber) {
return false;
}
*serialNumber = '\0';
uint32_t sno{};
if (readSerialNumber(sno)) {
uint_fast8_t i{8};
while (i--) {
*serialNumber++ = m5::utility::uintToHexChar((sno >> (i * 4)) & 0x0F);
}
*serialNumber = '\0';
return true;
}
return false;
}
bool UnitSHT30::read_measurement(Data& d)
{
if (readWithTransaction(d.raw.data(), d.raw.size()) == m5::hal::error::error_t::OK) {
m5::utility::CRC8_Checksum crc{};
for (uint_fast8_t i = 0; i < 2; ++i) {
if (crc.range(d.raw.data() + i * 3, 2U) != d.raw[i * 3 + 2]) {
return false;
}
}
return true;
}
return false;
}
} // namespace unit
} // namespace m5

View file

@ -0,0 +1,360 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_SHT30.hpp
@brief SHT30 Unit for M5UnitUnified
*/
#ifndef M5_UNIT_ENV_UNIT_SHT30_HPP
#define M5_UNIT_ENV_UNIT_SHT30_HPP
#include <M5UnitComponent.hpp>
#include <m5_utility/container/circular_buffer.hpp>
#include <limits> // NaN
namespace m5 {
namespace unit {
namespace sht30 {
/*!
@enum Repeatability
@brief Repeatability accuracy level
*/
enum class Repeatability : uint8_t {
High, //!< @brief High repeatability
Medium, //!< @brief Medium repeatability
Low //!< @brief Low repeatability
};
/*!
@enum MPS
@brief Measuring frequency
*/
enum class MPS : uint8_t {
Half, //!< @brief 0.5 measurement per second
One, //!< @brief 1 measurement per second
Two, //!< @brief 2 measurement per second
Four, //!< @brief 4 measurement per second
Ten, //!< @brief 10 measurement per second
};
/*!
@struct Status
@brief Accessor for Status
@note The order of the bit fields cannot be controlled, so bitwise
operations are used to obtain each value.
@note Items marked with (*) are subjects to clear status
*/
struct Status {
//! @brief Alert pending status (*)
inline bool alertPending() const
{
return value & (1U << 15);
}
//! @brief Heater status
inline bool heater() const
{
return value & (1U << 13);
}
//! @brief RH tracking alert (*)
inline bool trackingAlertRH() const
{
return value & (1U << 11);
}
//! @brief Tracking alert (*)
inline bool trackingAlert() const
{
return value & (1U << 10);
}
//! @brief System reset detected (*)
inline bool reset() const
{
return value & (1U << 4);
}
//! @brief Command staus
inline bool command() const
{
return value & (1U << 1);
}
//! @brief Write data checksum status
inline bool checksum() const
{
return value & (1U << 0);
}
uint16_t value{};
};
/*!
@struct Data
@brief Measurement data group
*/
struct Data {
std::array<uint8_t, 6> raw{}; //!< RAW data
//! temperature (Celsius)
inline float temperature() const
{
return celsius();
}
float celsius() const; //!< temperature (Celsius)
float fahrenheit() const; //!< temperature (Fahrenheit)
float humidity() const; //!< humidity (RH)
};
} // namespace sht30
/*!
@class UnitSHT30
@brief Temperature and humidity, sensor unit
*/
class UnitSHT30 : public Component, public PeriodicMeasurementAdapter<UnitSHT30, sht30::Data> {
M5_UNIT_COMPONENT_HPP_BUILDER(UnitSHT30, 0x44);
public:
/*!
@struct config_t
@brief Settings for begin
*/
struct config_t {
//! Start periodic measurement on begin?
bool start_periodic{true};
//! Measuring frequency if start on begin
sht30::MPS mps{sht30::MPS::One};
//! Repeatability accuracy level if start on begin
sht30::Repeatability repeatability{sht30::Repeatability::High};
//! start heater on begin?
bool start_heater{false};
};
explicit UnitSHT30(const uint8_t addr = DEFAULT_ADDRESS)
: Component(addr), _data{new m5::container::CircularBuffer<sht30::Data>(1)}
{
auto ccfg = component_config();
ccfg.clock = 400 * 1000U;
component_config(ccfg);
}
virtual ~UnitSHT30()
{
}
virtual bool begin() override;
virtual void update(const bool force = false) override;
///@name Settings for begin
///@{
/*! @brief Gets the configration */
inline config_t config()
{
return _cfg;
}
//! @brief Set the configration
inline void config(const config_t& cfg)
{
_cfg = cfg;
}
///@}
///@name Measurement data by periodic
///@{
//! @brief Oldest measured temperature (Celsius)
inline float temperature() const
{
return !empty() ? oldest().temperature() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured temperature (Celsius)
inline float celsius() const
{
return !empty() ? oldest().celsius() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured temperature (Fahrenheit)
inline float fahrenheit() const
{
return !empty() ? oldest().fahrenheit() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured humidity (RH)
inline float humidity() const
{
return !empty() ? oldest().humidity() : std::numeric_limits<float>::quiet_NaN();
}
///@}
///@name Periodic measurement
///@{
/*!
@brief Start periodic measurement
@param mps Measuring frequency
@param rep Repeatability accuracy level
@return True if successful
*/
inline bool startPeriodicMeasurement(const sht30::MPS mps, const sht30::Repeatability rep)
{
return PeriodicMeasurementAdapter<UnitSHT30, sht30::Data>::startPeriodicMeasurement(mps, rep);
}
/*!
@brief Stop periodic measurement
@return True if successful
*/
inline bool stopPeriodicMeasurement()
{
return PeriodicMeasurementAdapter<UnitSHT30, sht30::Data>::stopPeriodicMeasurement();
}
///@}
///@name Single shot measurement
///@{
/*!
@brief Measurement single shot
@param[out] data Measuerd data
@param rep Repeatability accuracy level
@param stretch Enable clock stretching if true
@return True if successful
@warning During periodic detection runs, an error is returned
@warning After sending a command to the sensor a minimal waiting time of 1ms is needed before another command can
be received by the sensor
*/
bool measureSingleshot(sht30::Data& d, const sht30::Repeatability rep = sht30::Repeatability::High,
const bool stretch = true);
///@}
/*!
@brief Write the mode to ART
@details After issuing the ART command the sensor will start acquiring data with a frequency of 4Hz
@return True if successful
@warning Only available during periodic measurements
*/
bool writeModeAccelerateResponseTime();
///@name Reset
///@{
/*!
@brief Soft reset
@details The sensor to reset its system controller and reloads calibration
data from the memory.
@return True if successful
@warning During periodic detection runs, an error is returned
*/
bool softReset();
/*!
@brief General reset
@details Reset using I2C general call
@return True if successful
@warning This is a reset by General command, the command is also sent to all devices with I2C connections
*/
bool generalReset();
///@}
///@name Heater
///@{
/*!
@brief Start heater
@return True if successful
*/
bool startHeater();
/*!
@brief Stop heater
@return True if successful
*/
bool stopHeater();
///@}
///@name Status
///@{
/*!
@brief Read status
@param[out] s Status
@return True if successful
*/
bool readStatus(sht30::Status& s);
/*!
@brief Clear status
@sa sht30::Status
@return True if successful
*/
bool clearStatus();
///@}
///@name Serial number
///@{
/*!
@brief Read the serial number value
@param[out] serialNumber serial number value
@return True if successful
@note The serial number is 32 bit
@warning During periodic detection runs, an error is returned
*/
bool readSerialNumber(uint32_t& serialNumber);
/*!
@brief Read the serial number string
@param[out] serialNumber Output buffer
@return True if successful
@warning serialNumber must be at least 9 bytes
@warning During periodic detection runs, an error is returned
*/
bool readSerialNumber(char* serialNumber);
///@}
protected:
bool start_periodic_measurement(const sht30::MPS mps, const sht30::Repeatability rep);
bool stop_periodic_measurement();
bool read_measurement(sht30::Data& d);
M5_UNIT_COMPONENT_PERIODIC_MEASUREMENT_ADAPTER_HPP_BUILDER(UnitSHT30, sht30::Data);
protected:
std::unique_ptr<m5::container::CircularBuffer<sht30::Data>> _data{};
config_t _cfg{};
};
///@cond
namespace sht30 {
namespace command {
// Measurement Commands for Single Shot Data Acquisition Mode
constexpr uint16_t SINGLE_SHOT_ENABLE_STRETCH_HIGH{0x2C06};
constexpr uint16_t SINGLE_SHOT_ENABLE_STRETCH_MEDIUM{0x2C0D};
constexpr uint16_t SINGLE_SHOT_ENABLE_STRETCH_LOW{0x2C10};
constexpr uint16_t SINGLE_SHOT_DISABLE_STRETCH_HIGH{0x2400};
constexpr uint16_t SINGLE_SHOT_DISABLE_STRETCH_MEDIUM{0x240B};
constexpr uint16_t SINGLE_SHOT_DISABLE_STRETCH_LOW{0x2416};
// Measurement Commands for Periodic Data Acquisition Mode
constexpr uint16_t START_PERIODIC_MPS_HALF_HIGH{0x2032};
constexpr uint16_t START_PERIODIC_MPS_HALF_MEDIUM{0x2024};
constexpr uint16_t START_PERIODIC_MPS_HALF_LOW{0x202f};
constexpr uint16_t START_PERIODIC_MPS_1_HIGH{0x2130};
constexpr uint16_t START_PERIODIC_MPS_1_MEDIUM{0x2126};
constexpr uint16_t START_PERIODIC_MPS_1_LOW{0x212D};
constexpr uint16_t START_PERIODIC_MPS_2_HIGH{0x2236};
constexpr uint16_t START_PERIODIC_MPS_2_MEDIUM{0x2220};
constexpr uint16_t START_PERIODIC_MPS_2_LOW{0x222B};
constexpr uint16_t START_PERIODIC_MPS_4_HIGH{0x2334};
constexpr uint16_t START_PERIODIC_MPS_4_MEDIUM{0x2322};
constexpr uint16_t START_PERIODIC_MPS_4_LOW{0x2329};
constexpr uint16_t START_PERIODIC_MPS_10_HIGH{0x2737};
constexpr uint16_t START_PERIODIC_MPS_10_MEDIUM{0x2721};
constexpr uint16_t START_PERIODIC_MPS_10_LOW{0x272A};
constexpr uint16_t STOP_PERIODIC_MEASUREMENT{0x3093};
constexpr uint16_t ACCELERATED_RESPONSE_TIME{0x2B32};
constexpr uint16_t READ_MEASUREMENT{0xE000};
// Reset
constexpr uint16_t SOFT_RESET{0x30A2};
// Heater
constexpr uint16_t START_HEATER{0x306D};
constexpr uint16_t STOP_HEATER{0x3066};
// Status
constexpr uint16_t READ_STATUS{0xF32D};
constexpr uint16_t CLEAR_STATUS{0x3041};
// Serial
constexpr uint16_t GET_SERIAL_NUMBER_ENABLE_STRETCH{0x3780};
constexpr uint16_t GET_SERIAL_NUMBER_DISABLE_STRETCH{0x3682};
} // namespace command
} // namespace sht30
///@endcond
} // namespace unit
} // namespace m5
#endif

View file

@ -0,0 +1,296 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_SHT40.cpp
@brief SHT40 Unit for M5UnitUnified
*/
#include "unit_SHT40.hpp"
#include <M5Utility.hpp>
#include <limits> // NaN
#include <array>
using namespace m5::utility::mmh3;
using namespace m5::unit::types;
using namespace m5::unit::sht40;
using namespace m5::unit::sht40::command;
namespace {
constexpr uint8_t periodic_cmd[] = {
// HIGH
MEASURE_HIGH_HEATER_1S,
MEASURE_HIGH_HEATER_100MS,
MEASURE_HIGH,
// MEDIUM
MEASURE_MEDIUM_HEATER_1S,
MEASURE_MEDIUM_HEATER_100MS,
MEASURE_MEDIUM,
// LOW
MEASURE_LOW_HEATER_1S,
MEASURE_LOW_HEATER_100MS,
MEASURE_LOW,
};
constexpr elapsed_time_t interval_table[] = {
// HIGH
1100, 110,
9, // 8.2
// MEDIUM
1100, 110,
5, // 4.5
// LOW
1100, 110,
2, // 1.7
};
constexpr float MAX_HEATER_DUTY{0.05f};
} // namespace
namespace m5 {
namespace unit {
namespace sht40 {
float Data::celsius() const
{
return -45 + 175 * m5::types::big_uint16_t(raw[0], raw[1]).get() / 65535.f;
}
float Data::fahrenheit() const
{
return -49 + 315 * m5::types::big_uint16_t(raw[0], raw[1]).get() / 65535.f;
// return celsius() * 9.0f / 5.0f + 32.f;
}
float Data::humidity() const
{
return -6 + 125 * m5::types::big_uint16_t(raw[3], raw[4]).get() / 65535.f;
}
} // namespace sht40
const char UnitSHT40::name[] = "UnitSHT40";
const types::uid_t UnitSHT40::uid{"UnitSHT40"_mmh3};
const types::attr_t UnitSHT40::attr{attribute::AccessI2C};
bool UnitSHT40::begin()
{
auto ssize = stored_size();
assert(ssize && "stored_size must be greater than zero");
if (ssize != _data->capacity()) {
_data.reset(new m5::container::CircularBuffer<Data>(ssize));
if (!_data) {
M5_LIB_LOGE("Failed to allocate");
return false;
}
}
if (!softReset()) {
M5_LIB_LOGE("Failed to reset");
return false;
}
uint32_t sn{};
if (!readSerialNumber(sn)) {
M5_LIB_LOGE("Failed to readSerialNumber %x", sn);
return false;
}
return _cfg.start_periodic ? startPeriodicMeasurement(_cfg.precision, _cfg.heater, _cfg.heater_duty) : true;
}
void UnitSHT40::update(const bool force)
{
_updated = false;
if (inPeriodic()) {
elapsed_time_t at{m5::utility::millis()};
if (force || !_latest || at >= _latest + _interval) {
Data d{};
_updated = read_measurement(d);
if (_updated) {
_latest = at;
d.heater = (_interval != _duration_heater);
_data->push_back(d);
uint8_t cmd{};
if (at >= _latest_heater + _interval_heater) {
cmd = _cmd;
_latest_heater = at;
_interval = _duration_heater;
} else {
cmd = _measureCmd;
_interval = _duration_measure;
}
if (!writeRegister(cmd)) {
M5_LIB_LOGE("Failed to write, stop periodic measurement");
_periodic = false;
}
}
}
}
}
bool UnitSHT40::start_periodic_measurement(const sht40::Precision precision, const sht40::Heater heater,
const float duty)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
if (duty <= 0.0f || duty > MAX_HEATER_DUTY) {
M5_LIB_LOGW("duty range is invalid %f. duty (0.0, 0.05]");
return false;
}
_cmd = periodic_cmd[m5::stl::to_underlying(precision) * 3 + m5::stl::to_underlying(heater)];
_measureCmd = periodic_cmd[m5::stl::to_underlying(precision) * 3 + m5::stl::to_underlying(Heater::None)];
_periodic = writeRegister(_cmd);
if (_periodic) {
_duration_heater = interval_table[m5::stl::to_underlying(precision) * 3 + m5::stl::to_underlying(heater)];
_duration_measure =
interval_table[m5::stl::to_underlying(precision) * 3 + m5::stl::to_underlying(Heater::None)];
_interval_heater = _duration_heater / duty;
_interval = _duration_heater;
_latest_heater = m5::utility::millis();
m5::utility::delay(_interval); // For first read_measurement in update
return true;
}
return _periodic;
}
bool UnitSHT40::stop_periodic_measurement()
{
if (inPeriodic()) {
// Dismissal of data to be read
Data discard{};
int64_t wait = (int64_t)(_latest + _interval) - (int64_t)m5::utility::millis();
if (wait > 0) {
m5::utility::delay(wait);
read_measurement(discard);
}
_periodic = false;
return true;
}
return false;
}
bool UnitSHT40::measureSingleshot(sht40::Data& d, const sht40::Precision precision, const sht40::Heater heater)
{
if (inPeriodic()) {
M5_LIB_LOGD("Periodic measurements are running");
return false;
}
uint8_t cmd = periodic_cmd[m5::stl::to_underlying(precision) * 3 + m5::stl::to_underlying(heater)];
auto ms = interval_table[m5::stl::to_underlying(precision) * 3 + m5::stl::to_underlying(heater)];
if (writeRegister(cmd)) {
m5::utility::delay(ms);
if (read_measurement(d)) {
d.heater = (heater != Heater::None);
return true;
}
}
return false;
}
bool UnitSHT40::read_measurement(sht40::Data& d)
{
if (readWithTransaction(d.raw.data(), d.raw.size()) == m5::hal::error::error_t::OK) {
m5::utility::CRC8_Checksum crc{};
for (uint_fast8_t i = 0; i < 2; ++i) {
if (crc.range(d.raw.data() + i * 3, 2U) != d.raw[i * 3 + 2]) {
return false;
}
}
return true;
}
return false;
}
bool UnitSHT40::softReset()
{
if (inPeriodic()) {
M5_LIB_LOGE("Periodic measurements are running");
return false;
}
return soft_reset();
}
bool UnitSHT40::soft_reset()
{
if (writeRegister(SOFT_RESET)) {
// Max 1 ms
// Time between ACK of soft reset command and sensor entering idle state
m5::utility::delay(1);
reset_status();
return true;
}
return false;
}
bool UnitSHT40::generalReset()
{
uint8_t cmd{0x06}; // reset command
// Reset does not return ACK, which is an error, but should be ignored
generalCall(&cmd, 1);
m5::utility::delay(1);
reset_status();
return true;
}
bool UnitSHT40::readSerialNumber(uint32_t& serialNumber)
{
serialNumber = 0;
if (inPeriodic()) {
M5_LIB_LOGE("Periodic measurements are running");
return false;
}
std::array<uint8_t, 6> rbuf;
if (readRegister(GET_SERIAL_NUMBER, rbuf.data(), rbuf.size(), 1)) {
m5::types::big_uint16_t u16[2]{{rbuf[0], rbuf[1]}, {rbuf[3], rbuf[4]}};
m5::utility::CRC8_Checksum crc{};
if (crc.range(u16[0].data(), u16[0].size()) == rbuf[2] && crc.range(u16[1].data(), u16[1].size()) == rbuf[5]) {
serialNumber = ((uint32_t)u16[0].get()) << 16 | ((uint32_t)u16[1].get());
return true;
}
}
return false;
}
bool UnitSHT40::readSerialNumber(char* serialNumber)
{
if (!serialNumber) {
return false;
}
*serialNumber = '\0';
uint32_t sno{};
if (readSerialNumber(sno)) {
uint_fast8_t i{8};
while (i--) {
*serialNumber++ = m5::utility::uintToHexChar((sno >> (i * 4)) & 0x0F);
}
*serialNumber = '\0';
return true;
}
return false;
}
void UnitSHT40::reset_status()
{
_interval = _latest = _interval_heater = _latest_heater = 0;
_duration_measure = _duration_heater = 0;
_cmd = _measureCmd = 0;
_periodic = false;
}
} // namespace unit
} // namespace m5

View file

@ -0,0 +1,260 @@
/*
* SPDX-FileCopyrightText: 2024 M5Stack Technology CO LTD
*
* SPDX-License-Identifier: MIT
*/
/*!
@file unit_SHT40.hpp
@brief SHT40 Unit for M5UnitUnified
*/
#ifndef M5_UNIT_ENV_UNIT_SHT40_HPP
#define M5_UNIT_ENV_UNIT_SHT40_HPP
#include <M5UnitComponent.hpp>
#include <m5_utility/container/circular_buffer.hpp>
#include <limits> // NaN
namespace m5 {
namespace unit {
/*!
@namespace sht40
@brief For SHT40
*/
namespace sht40 {
/*!
@enum Precision
@brief precision level
*/
enum class Precision : uint8_t {
High, //!< High precision (high repeatability)
Medium, //!< Medium precision (medium repeatability)
Low //!< Lowest precision (low repeatability)
};
/*!
@enum Heater
@brief Heater behavior
*/
enum class Heater : uint8_t {
Long, //!< Activate heater for 1s
Short, //!< Activate heater for 0.1s
None //!< Not activate heater
};
/*!
@struct Data
@brief Measurement data group
*/
struct Data {
std::array<uint8_t, 6> raw{}; //!< RAW data
bool heater{}; //!< Measured data after heater is activated if true
//! temperature (Celsius)
inline float temperature() const
{
return celsius();
}
float celsius() const; //!< temperature (Celsius)
float fahrenheit() const; //!< temperature (Fahrenheit)
float humidity() const; //!< humidity (RH)
};
} // namespace sht40
/*!
@class UnitSHT40
@brief Temperature and humidity, sensor unit
*/
class UnitSHT40 : public Component, public PeriodicMeasurementAdapter<UnitSHT40, sht40::Data> {
M5_UNIT_COMPONENT_HPP_BUILDER(UnitSHT40, 0x44);
public:
/*!
@struct config_t
@brief Settings for begin
*/
struct config_t {
//! Start periodic measurement on begin?
bool start_periodic{true};
//! Precision level if start on begin
sht40::Precision precision{sht40::Precision::High};
//! Heater behavior if start on begin
sht40::Heater heater{sht40::Heater::None};
//! Heater duty cycle if start on begin [~ 0.05f]
float heater_duty{0.05f};
};
explicit UnitSHT40(const uint8_t addr = DEFAULT_ADDRESS)
: Component(addr), _data{new m5::container::CircularBuffer<sht40::Data>(1)}
{
auto ccfg = component_config();
ccfg.clock = 400 * 1000U;
component_config(ccfg);
}
virtual ~UnitSHT40()
{
}
virtual bool begin() override;
virtual void update(const bool force = false) override;
///@name Settings for begin
///@{
/*! @brief Gets the configration */
inline config_t config()
{
return _cfg;
}
//! @brief Set the configration
inline void config(const config_t& cfg)
{
_cfg = cfg;
}
///@}
///@name Measurement data by periodic
///@{
//! @brief Oldest measured temperature (Celsius)
inline float temperature() const
{
return !empty() ? oldest().temperature() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured temperature (Celsius)
inline float celsius() const
{
return !empty() ? oldest().celsius() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured temperature (Fahrenheit)
inline float fahrenheit() const
{
return !empty() ? oldest().fahrenheit() : std::numeric_limits<float>::quiet_NaN();
}
//! @brief Oldest measured humidity (RH)
inline float humidity() const
{
return !empty() ? oldest().humidity() : std::numeric_limits<float>::quiet_NaN();
}
///@}
///@name Periodic measurement
///@{
/*!
@brief Start periodic measurement
@param precision Sensor precision
@param heater Heater behavior
@param duty Duty for activate heater
@return True if successful
@note If the heater is Long or SHort, the heater will be active periodically within the specified duty
@warning Datasheet says "keepingin mind that the heater is designed for a maximal duty cycle of less than 5%"
*/
inline bool startPeriodicMeasurement(const sht40::Precision precision, const sht40::Heater heater,
const float duty = 0.05f)
{
return PeriodicMeasurementAdapter<UnitSHT40, sht40::Data>::startPeriodicMeasurement(precision, heater, duty);
}
/*!
@brief Stop periodic measurement
@return True if successful
*/
inline bool stopPeriodicMeasurement()
{
return PeriodicMeasurementAdapter<UnitSHT40, sht40::Data>::stopPeriodicMeasurement();
}
///@}
///@name Single shot measurement
///@{
/*!
@brief Measurement single shot
@param[out] data Measuerd data
@param precision Sensor precision
@param heater Heater behavior
@return True if successful
@note Blocking until the process is complete
@warning During periodic detection runs, an error is returned
@warning If heater is activated, the accuracy of the returned value is not guaranteed
@sa UnitSHT40::startPeriodicMeasurement
*/
bool measureSingleshot(sht40::Data& d, const sht40::Precision precision = sht40::Precision::High,
const sht40::Heater heater = sht40::Heater::None);
///@}
///@name Reset
///@{
/*!
@brief Soft reset
@return True if successful
*/
bool softReset();
/*!
@brief General reset
@details Reset using I2C general call
@return True if successful
@warning This is a reset by General command, the command is also sent to all devices with I2C connections
*/
bool generalReset();
///@}
///@name Serial number
///@{
/*!
@brief Read the serial number value
@param[out] serialNumber serial number value
@return True if successful
@note The serial number is 32 bit
@warning During periodic detection runs, an error is returned
*/
bool readSerialNumber(uint32_t& serialNumber);
/*!
@brief Read the serial number string
@param[out] serialNumber Output buffer
@return True if successful
@warning serialNumber must be at least 9 bytes
@warning During periodic detection runs, an error is returned
*/
bool readSerialNumber(char* serialNumber);
///@}
protected:
bool start_periodic_measurement(const sht40::Precision precision, const sht40::Heater heater, const float duty);
bool stop_periodic_measurement();
bool read_measurement(sht40::Data& d);
void reset_status();
bool soft_reset();
M5_UNIT_COMPONENT_PERIODIC_MEASUREMENT_ADAPTER_HPP_BUILDER(UnitSHT40, sht40::Data);
protected:
std::unique_ptr<m5::container::CircularBuffer<sht40::Data>> _data{};
config_t _cfg{};
uint8_t _cmd{}, _measureCmd{};
types::elapsed_time_t _latest_heater{}, _interval_heater{};
uint32_t _duration_measure{}, _duration_heater{};
};
///@cond
namespace sht40 {
namespace command {
constexpr uint8_t MEASURE_HIGH_HEATER_1S{0x39};
constexpr uint8_t MEASURE_HIGH_HEATER_100MS{0x32};
constexpr uint8_t MEASURE_HIGH{0xFD};
constexpr uint8_t MEASURE_MEDIUM_HEATER_1S{0x2F};
constexpr uint8_t MEASURE_MEDIUM_HEATER_100MS{0x24};
constexpr uint8_t MEASURE_MEDIUM{0xF6};
constexpr uint8_t MEASURE_LOW_HEATER_1S{0x1E};
constexpr uint8_t MEASURE_LOW_HEATER_100MS{0x15};
constexpr uint8_t MEASURE_LOW{0xE0};
constexpr uint8_t GET_SERIAL_NUMBER{0x89};
constexpr uint8_t SOFT_RESET{0x94};
} // namespace command
} // namespace sht40
///@endcond
} // namespace unit
} // namespace m5
#endif

View file

@ -0,0 +1,27 @@
#include "utility.h"
uint8_t crc8(const uint8_t *data, int len) {
/*
*
* CRC-8 formula from page 14 of SHT spec pdf
*
* Test data 0xBE, 0xEF should yield 0x92
*
* Initialization data 0xFF
* Polynomial 0x31 (x8 + x5 +x4 +1)
* Final XOR 0x00
*/
const uint8_t POLYNOMIAL(0x31);
uint8_t crc(0xFF);
for (int j = len; j; --j) {
crc ^= *data++;
for (int i = 8; i; --i) {
crc = (crc & 0x80) ? (crc << 1) ^ POLYNOMIAL : (crc << 1);
}
}
return crc;
}

View file

@ -0,0 +1,8 @@
#ifndef __M5_UNIT_ENV_UTILITY_H_
#define __M5_UNIT_ENV_UTILITY_H_
#include "Arduino.h"
uint8_t crc8(const uint8_t *data, int len);
#endif