From 0b8b615350884c17b25a3e3ad1f80bf74f6a1e03 Mon Sep 17 00:00:00 2001 From: CDFER Date: Thu, 13 Apr 2023 10:08:51 +1200 Subject: [PATCH 1/2] I2C interface bugfix, added calibration mode function, added & improved comments --- .vscode/settings.json | 8 +++++ library.json | 2 +- scd4x.cpp | 69 ++++++++++++++++++++++++++++++++------ scd4x.h | 78 ++++++++++++++++++++++++++++++++++++++----- 4 files changed, 137 insertions(+), 20 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bb63124 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "cSpell.words": [ + "datasheet", + "EEPROM", + "LOGI", + "Sensirion" + ] +} \ No newline at end of file diff --git a/library.json b/library.json index c3edba3..0960059 100644 --- a/library.json +++ b/library.json @@ -1,6 +1,6 @@ { "name": "scd4x-CO2", -"version": "1.0.0", +"version": "1.1.0", "description": "A library to interface esp chips with the SCD4x CO2 sensors in the Arduino (c++) Framework.", "keywords": "co2, scd40, scd41, scd4x, i2c", "repository": { diff --git a/scd4x.cpp b/scd4x.cpp index c972226..21a5428 100644 --- a/scd4x.cpp +++ b/scd4x.cpp @@ -1,5 +1,6 @@ /* * Copyright (c) 2021, Sensirion AG + * Copyright (c) 2023, Chris Dirks * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -39,6 +40,8 @@ uint8_t SCD4X::begin(TwoWire& port, uint8_t addr) { } bool SCD4X::isConnected(TwoWire& port, Stream* stream, uint8_t addr) { + const int bytesRequested = 3; + _i2cPort = &port; _address = addr; @@ -47,6 +50,11 @@ bool SCD4X::isConnected(TwoWire& port, Stream* stream, uint8_t addr) { _i2cPort->beginTransmission(_address); _error = _i2cPort->endTransmission(true); + SCD4X::stopPeriodicMeasurement(); + vTaskDelay(500 / portTICK_PERIOD_MS); // wait for SCD4x to stop as per datasheet + + SCD4X::factoryReset(); + char addrCheck[32]; if (_error != 0) { _debug_output_stream->printf("SCD4x returned endTransmission error %i\r\n", _error); @@ -58,11 +66,11 @@ bool SCD4X::isConnected(TwoWire& port, Stream* stream, uint8_t addr) { _i2cPort->write(0x39); _i2cPort->endTransmission(true); - vTaskDelay(10000 / portTICK_PERIOD_MS); + vTaskDelay(10000 / portTICK_PERIOD_MS); // wait for SCD4x to do a self test as per datasheet - uint8_t temp[3]; - if (_i2cPort->requestFrom(_address, (uint8_t)3)) { - Wire.readBytes(temp, 3); + uint8_t temp[bytesRequested]; + if (_i2cPort->requestFrom(_address, bytesRequested)) { + _i2cPort->readBytes(temp, bytesRequested); } if (temp[0] != 0 || temp[1] != 0 || temp[2] != 0x81) { @@ -74,6 +82,13 @@ bool SCD4X::isConnected(TwoWire& port, Stream* stream, uint8_t addr) { return true; } +uint8_t SCD4X::stopPeriodicMeasurement() { + _i2cPort->beginTransmission(_address); + _i2cPort->write(0x3f); + _i2cPort->write(0x86); + return _i2cPort->endTransmission(); +} + uint8_t SCD4X::startPeriodicMeasurement() { _i2cPort->beginTransmission(_address); _i2cPort->write(0x21); @@ -94,10 +109,10 @@ uint8_t SCD4X::readMeasurement(double& co2, double& temperature, double& humidit // 2 bytes sensor status, 1 byte CRC // stop reading after bytesRequested (12 bytes) - uint8_t bytesReceived = Wire.requestFrom(_address, bytesRequested); + uint8_t bytesReceived = _i2cPort->requestFrom(_address, bytesRequested); if (bytesReceived == bytesRequested) { // If received requested amount of bytes uint8_t data[bytesReceived]; - Wire.readBytes(data, bytesReceived); + _i2cPort->readBytes(data, bytesReceived); // floating point conversion co2 = (double)((uint16_t)data[0] << 8 | data[1]); @@ -111,11 +126,12 @@ uint8_t SCD4X::readMeasurement(double& co2, double& temperature, double& humidit return false; } else { ESP_LOGE("measurement", "out of range"); + Serial.printf("%4.0f,%2.1f,%1.0f\n", co2, temperature, humidity); return true; } } else { - //ESP_LOGE("Wire.requestFrom", "bytesReceived(%i) != bytesRequested(%i)", bytesReceived, bytesRequested); + // ESP_LOGE("Wire.requestFrom", "bytesReceived(%i) != bytesRequested(%i)", bytesReceived, bytesRequested); return true; } } else { @@ -132,10 +148,10 @@ bool SCD4X::isDataReady() { _i2cPort->write(0xb8); if (_i2cPort->endTransmission(true) == 0) { - uint8_t bytesReceived = Wire.requestFrom(_address, bytesRequested); + uint8_t bytesReceived = _i2cPort->requestFrom(_address, bytesRequested); if (bytesReceived == bytesRequested) { // If received requested amount of bytes uint8_t data[bytesReceived]; - Wire.readBytes(data, bytesReceived); + _i2cPort->readBytes(data, bytesReceived); return data[1] != (0x00); // check if last 8 bits are not 0 } else { @@ -148,4 +164,37 @@ bool SCD4X::isDataReady() { ESP_LOGE("Wire.endTransmission", "Returned ERROR"); return false; } -} \ No newline at end of file +} + +uint8_t SCD4X::setSelfCalibrationMode(bool enableSelfCalibration) { + SCD4X::stopPeriodicMeasurement(); + vTaskDelay(500 / portTICK_PERIOD_MS); // wait for SCD4x to stop as per datasheet + + SCD4X::factoryReset(); + + _i2cPort->beginTransmission(_address); + _i2cPort->write(0x24); + _i2cPort->write(0x16); + if (enableSelfCalibration) { + _i2cPort->write(0x01); + } else { + _i2cPort->write(0x00); + } + + _error = _i2cPort->endTransmission(); + if (_error != 0) { + return _error; + } else { + return SCD4X::saveSettings(); + } +} + +uint8_t SCD4X::saveSettings() { + _i2cPort->beginTransmission(_address); + _i2cPort->write(0x36); + _i2cPort->write(0x15); + _error = _i2cPort->endTransmission(); + ESP_LOGI("Settings Saved to EEPROM", ""); + vTaskDelay(800 / portTICK_PERIOD_MS); // wait for SCD4x to saveSettings as per datasheet + return _error; +} diff --git a/scd4x.h b/scd4x.h index cfd282a..2d54100 100644 --- a/scd4x.h +++ b/scd4x.h @@ -41,6 +41,12 @@ class SCD4X { * * @param i2cBus Arduino stream object to use for communication. * + * @retval 0 success + * @retval 1 i2c data too long to fit in transmit buffer + * @retval 2 i2c received NACK on transmit of address + * @retval 3 i2c received NACK on transmit of data + * @retval 4 i2c other error + * @retval 5 i2c timeout */ uint8_t begin(TwoWire& port = Wire, uint8_t addr = SCD4X_I2C_ADDRESS); @@ -49,21 +55,37 @@ class SCD4X { * * @param port Wire instance (e.g Wire or Wire1) * @param stream debug output pointer (e.g. &Serial) - * @param addr i2c address of sensor + * @param addr i2c address of sensor (0x62 by default) * @returns true if device correctly connected, otherwise false */ bool isConnected(TwoWire& port = Wire, Stream* stream = &Serial, uint8_t addr = SCD4X_I2C_ADDRESS); /** - * startPeriodicMeasurement() - start periodic measurement, signal update - * interval is 5 seconds. + * startPeriodicMeasurement() - start periodic measurement, new data available + * in ~5 seconds. * - * @note This command is only available in idle mode. - * - * @return 0 on success, an error code otherwise + * @retval 0 success + * @retval 1 i2c data too long to fit in transmit buffer + * @retval 2 i2c received NACK on transmit of address + * @retval 3 i2c received NACK on transmit of data + * @retval 4 i2c other error + * @retval 5 i2c timeout */ uint8_t startPeriodicMeasurement(); + /** + * stopPeriodicMeasurement() - stop periodic measurement + * @note wait atleast 500ms before sending further commands + * + * @retval 0 success + * @retval 1 i2c data too long to fit in transmit buffer + * @retval 2 i2c received NACK on transmit of address + * @retval 3 i2c received NACK on transmit of data + * @retval 4 i2c other error + * @retval 5 i2c timeout + */ + uint8_t stopPeriodicMeasurement(); + /** * readMeasurement() - read sensor output. The measurement data can * only be read out once per signal update interval as the buffer is emptied @@ -82,7 +104,12 @@ class SCD4X { * * @param humidity Relative humidity in %RH * - * @return 0 on success, an error code otherwise + * @retval 0 success + * @retval 1 i2c data too long to fit in transmit buffer + * @retval 2 i2c received NACK on transmit of address + * @retval 3 i2c received NACK on transmit of data + * @retval 4 i2c other error + * @retval 5 i2c timeout */ uint8_t readMeasurement(double& co2, double& temperature, double& humidity); @@ -93,12 +120,45 @@ class SCD4X { * * @param dataReadyFlag True if valid data is available, false otherwise. * - * @return 0 on success, an error code otherwise + * @return 0 on success, an i2c error code otherwise */ bool isDataReady(); + /** + * setSelfCalibrationMode() - a blocking call to set the calibration mode and store it in the EEPROM of the SCD4x + * + * @note The automatic self calibration algorithm assumes that the sensor is exposed to the atmospheric CO2 + * concentration of 400 ppm at least once per week. + * + * @note To avoid unnecessary wear of the EEPROM, the setSelfCalibrationMode command should only be used sparingly. + * + * @param turn on or off self calibration + * + * @retval 0 success + * @retval 1 i2c data too long to fit in transmit buffer + * @retval 2 i2c received NACK on transmit of address + * @retval 3 i2c received NACK on transmit of data + * @retval 4 i2c other error + * @retval 5 i2c timeout + */ + uint8_t setSelfCalibrationMode(bool enableSelfCalibration); + + /** + * saveSettings() - store settings in the EEPROM of the SCD4x, wait atleast 800ms before sending further commands + * @note To avoid unnecessary wear of the EEPROM, the saveSettings command should only be used sparingly. + * EEPROM is guaranteed to endure at least 2000 write cycles before failure. + * + * @retval 0 success + * @retval 1 i2c data too long to fit in transmit buffer + * @retval 2 i2c received NACK on transmit of address + * @retval 3 i2c received NACK on transmit of data + * @retval 4 i2c other error + * @retval 5 i2c timeout + */ + uint8_t saveSettings(); + private: - uint8_t _error; + uint8_t _error = 0; uint8_t _isValid = false; int _address; bool _init = false; From 0788972ef22ce318dde1bc7065f5eda352738821 Mon Sep 17 00:00:00 2001 From: CDFER Date: Tue, 25 Apr 2023 11:27:21 +1200 Subject: [PATCH 2/2] refactor to use I2C Sequences add self calibration modes --- README.md | 20 +++++++++--- scd4x.cpp | 98 ++++++++++++++++--------------------------------------- scd4x.h | 66 ++++++++++++++++++++++++++++++++++--- 3 files changed, 107 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index 08123f8..3ad9e9e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,20 @@ This is a library to interface with the Sensirion SCD4x true CO2 sensors in Arduino using the I2C protocol. +## Warning +These sensors by default have an auto-calibrate mode that takes the lowest CO2 level from the last week and assumes it is 400ppm. This can cause the sensor to read hundreds of ppm off if it is in a room that doesn't get to 400ppm of CO2 in a week. It's important to be aware of this issue, as some scientific papers are even using this sensor and haven't noticed the problem. Here is the code that you need to permanently set them to not auto calibrate. To avoid unnecessary wear of the EEPROM, the saveSettings command should only be used sparingly. The EEPROM is guaranteed to endure at least 2000 write cycles before failure. + +```c++ +#include "scd4x.h" + +Wire.begin(); +co2.begin(Wire); +co2.setCalibrationMode(false); +co2.saveSettings(); +``` + +Despite this issue with the auto-calibration mode, I still believe that the Sensirion SCD4x CO2 Sensors are a great choice for measuring indoor air quality. In my experience, they have proven to be much more accurate than other popular sensors such as eCO2 sensors. It's important to be aware of this particular limitation and take the necessary steps to disable the auto-calibration mode, but overall, these sensors are a reliable and effective tool for monitoring CO2 levels. + ## Features - use multiple I2C Busses -> scd4x.begin(Wire1); - no extra dependencies @@ -12,7 +26,6 @@ This is a library to interface with the Sensirion SCD4x true CO2 sensors in Ardu - not all functions are implemented - not compatible with other SCD4x Arduino libraries - only tested with the esp32 -- under active development ### Setup ```c++ @@ -20,10 +33,9 @@ This is a library to interface with the Sensirion SCD4x true CO2 sensors in Ardu SCD4X co2; double CO2 = 0, temperature = 0, humidity = 0; - Wire.begin(); -scd4x.begin(Wire); -scd4x.startPeriodicMeasurement(); +co2.begin(Wire); +co2.startPeriodicMeasurement(); ``` ### Loop ```c++ diff --git a/scd4x.cpp b/scd4x.cpp index 21a5428..a027e57 100644 --- a/scd4x.cpp +++ b/scd4x.cpp @@ -36,38 +36,29 @@ uint8_t SCD4X::begin(TwoWire& port, uint8_t addr) { _i2cPort = &port; _address = addr; _i2cPort->beginTransmission(_address); - return _i2cPort->endTransmission(); + _error = _i2cPort->endTransmission(); + return _error; } bool SCD4X::isConnected(TwoWire& port, Stream* stream, uint8_t addr) { const int bytesRequested = 3; - _i2cPort = &port; - _address = addr; - _debug_output_stream = stream; - _i2cPort->beginTransmission(_address); - _error = _i2cPort->endTransmission(true); + SCD4X::begin(port, addr); SCD4X::stopPeriodicMeasurement(); vTaskDelay(500 / portTICK_PERIOD_MS); // wait for SCD4x to stop as per datasheet - SCD4X::factoryReset(); - - char addrCheck[32]; if (_error != 0) { _debug_output_stream->printf("SCD4x returned endTransmission error %i\r\n", _error); return false; } - _i2cPort->beginTransmission(_address); - _i2cPort->write(0x36); - _i2cPort->write(0x39); - _i2cPort->endTransmission(true); + _commandSequence(0x3639); vTaskDelay(10000 / portTICK_PERIOD_MS); // wait for SCD4x to do a self test as per datasheet - + uint8_t temp[bytesRequested]; if (_i2cPort->requestFrom(_address, bytesRequested)) { _i2cPort->readBytes(temp, bytesRequested); @@ -83,27 +74,24 @@ bool SCD4X::isConnected(TwoWire& port, Stream* stream, uint8_t addr) { } uint8_t SCD4X::stopPeriodicMeasurement() { - _i2cPort->beginTransmission(_address); - _i2cPort->write(0x3f); - _i2cPort->write(0x86); - return _i2cPort->endTransmission(); + _commandSequence(0x3f86); + return _error; } uint8_t SCD4X::startPeriodicMeasurement() { - _i2cPort->beginTransmission(_address); - _i2cPort->write(0x21); - _i2cPort->write(0xb1); - return _i2cPort->endTransmission(); + _commandSequence(0x21b1); + return _error; } uint8_t SCD4X::readMeasurement(double& co2, double& temperature, double& humidity) { - const int bytesRequested = 12; + const int bytesRequested = 9; _i2cPort->beginTransmission(_address); _i2cPort->write(0xec); _i2cPort->write(0x05); + _error = _i2cPort->endTransmission(false); // no stop bit - if (_i2cPort->endTransmission(true) == 0) { + if (_error == 0) { // read measurement data: 2 bytes co2, 1 byte CRC, // 2 bytes T, 1 byte CRC, 2 bytes RH, 1 byte CRC, // 2 bytes sensor status, 1 byte CRC @@ -123,77 +111,49 @@ uint8_t SCD4X::readMeasurement(double& co2, double& temperature, double& humidit if (inRange(co2, 40000, 0) && inRange(temperature, 60, -10) && inRange(humidity, 100, 0)) { - return false; + return 0; } else { ESP_LOGE("measurement", "out of range"); Serial.printf("%4.0f,%2.1f,%1.0f\n", co2, temperature, humidity); - return true; + _error = 7; } } else { // ESP_LOGE("Wire.requestFrom", "bytesReceived(%i) != bytesRequested(%i)", bytesReceived, bytesRequested); - return true; + _error = 6; } - } else { - ESP_LOGE("Wire.endTransmission", "Returned ERROR"); - return true; } + return _error; } bool SCD4X::isDataReady() { - const int bytesRequested = 3; - - _i2cPort->beginTransmission(_address); - _i2cPort->write(0xe4); - _i2cPort->write(0xb8); - - if (_i2cPort->endTransmission(true) == 0) { - uint8_t bytesReceived = _i2cPort->requestFrom(_address, bytesRequested); - if (bytesReceived == bytesRequested) { // If received requested amount of bytes - uint8_t data[bytesReceived]; - _i2cPort->readBytes(data, bytesReceived); - return data[1] != (0x00); // check if last 8 bits are not 0 - - } else { - ESP_LOGE("Wire.requestFrom", - "bytesReceived(%i) != bytesRequested(%i)", bytesReceived, - bytesRequested); - return false; - } - } else { - ESP_LOGE("Wire.endTransmission", "Returned ERROR"); + if ((_readSequence(0xe4b8) & 0x07ff) == 0x0000) { // lower 11 bits == 0 -> data not ready return false; + } else { + return true; } } -uint8_t SCD4X::setSelfCalibrationMode(bool enableSelfCalibration) { +bool SCD4X::getCalibrationMode() { + return (bool)_readSequence(0x2313); +} + +uint8_t SCD4X::setCalibrationMode(bool enableSelfCalibration) { SCD4X::stopPeriodicMeasurement(); vTaskDelay(500 / portTICK_PERIOD_MS); // wait for SCD4x to stop as per datasheet - SCD4X::factoryReset(); - - _i2cPort->beginTransmission(_address); - _i2cPort->write(0x24); - _i2cPort->write(0x16); if (enableSelfCalibration) { - _i2cPort->write(0x01); + SCD4X::_writeSequence(0x2416, 0x0001, 0xB0); + } else { - _i2cPort->write(0x00); + SCD4X::_writeSequence(0x2416, 0x0000, 0x81); } - _error = _i2cPort->endTransmission(); - if (_error != 0) { - return _error; - } else { - return SCD4X::saveSettings(); - } + return _error; } uint8_t SCD4X::saveSettings() { - _i2cPort->beginTransmission(_address); - _i2cPort->write(0x36); - _i2cPort->write(0x15); - _error = _i2cPort->endTransmission(); + _commandSequence(0x3615); ESP_LOGI("Settings Saved to EEPROM", ""); vTaskDelay(800 / portTICK_PERIOD_MS); // wait for SCD4x to saveSettings as per datasheet return _error; diff --git a/scd4x.h b/scd4x.h index 2d54100..e1fede3 100644 --- a/scd4x.h +++ b/scd4x.h @@ -1,5 +1,6 @@ /* * Copyright (c) 2021, Sensirion AG + * Copyright (c) 2023, Chris Dirks * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -37,7 +38,8 @@ class SCD4X { public: /** - * begin() - Initializes the scd4x_Class class. + * begin() - Initializes this library. Must be called before any other library functions + * @note this function does not start the i2c bus, you must do that before calling this command * * @param i2cBus Arduino stream object to use for communication. * @@ -110,6 +112,8 @@ class SCD4X { * @retval 3 i2c received NACK on transmit of data * @retval 4 i2c other error * @retval 5 i2c timeout + * @retval 6 bytesReceived(%i) != bytesRequested(%i) + * @retval 7 measurement out of range */ uint8_t readMeasurement(double& co2, double& temperature, double& humidity); @@ -141,7 +145,14 @@ class SCD4X { * @retval 4 i2c other error * @retval 5 i2c timeout */ - uint8_t setSelfCalibrationMode(bool enableSelfCalibration); + uint8_t setCalibrationMode(bool enableAutoCalibration); + + /** + * getCalibrationMode() + * + * @return is auto calibration enabled + */ + bool getCalibrationMode(); /** * saveSettings() - store settings in the EEPROM of the SCD4x, wait atleast 800ms before sending further commands @@ -160,12 +171,59 @@ class SCD4X { private: uint8_t _error = 0; uint8_t _isValid = false; - int _address; + int _address = SCD4X_I2C_ADDRESS; bool _init = false; - TwoWire* _i2cPort; + TwoWire* _i2cPort = &Wire; Stream* _debug_output_stream = &Serial; bool inRange(double value, double max, double min) { return !(value <= min) && (value <= max); } + + void _commandSequence(uint16_t registerAddress) { + _i2cPort->beginTransmission(_address); + _i2cPort->write(highByte(registerAddress)); + _i2cPort->write(lowByte(registerAddress)); + _error = _i2cPort->endTransmission(true); // send stop bit + } + + uint16_t _readSequence(uint16_t registerAddress) { + const int bytesRequested = 3; // check bit at end + + _i2cPort->beginTransmission(_address); + _i2cPort->write(highByte(registerAddress)); + _i2cPort->write(lowByte(registerAddress)); + _error = _i2cPort->endTransmission(false); // no stop bit + + if (_error == 0) { + uint8_t bytesReceived = _i2cPort->requestFrom(_address, bytesRequested); + if (bytesReceived == bytesRequested) { // If received requested amount of bytes + uint8_t data[bytesReceived]; + _i2cPort->readBytes(data, bytesReceived); + return ((uint16_t)data[0] << 8 | data[1]); + + } else { + ESP_LOGE("Wire.requestFrom", + "bytesReceived(%i) != bytesRequested(%i)", bytesReceived, + bytesRequested); + return 0; + } + } else { + ESP_LOGE("Wire.endTransmission", "Returned ERROR"); + return 0; + } + } + + void _writeSequence(uint16_t registerAddress, uint16_t value, uint8_t checkSum) { + _i2cPort->beginTransmission(_address); + _i2cPort->write(highByte(registerAddress)); + _i2cPort->write(lowByte(registerAddress)); + + _i2cPort->write(highByte(value)); + _i2cPort->write(lowByte(value)); + _i2cPort->write(checkSum); // Checksum + + _error = _i2cPort->endTransmission(true); // stop bit + } + };