/* This is a library written for the SCD30 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/14751 Written by Nathan Seidle @ SparkFun Electronics, May 22nd, 2018 Updated February 1st 2021 to include some of the features of paulvha's version of the library (while maintaining backward-compatibility): https://github.com/paulvha/scd30 Thank you Paul! The SCD30 measures CO2 with accuracy of +/- 30ppm. This library handles the initialization of the SCD30 and outputs CO2 levels, relative humidty, and temperature. https://github.com/sparkfun/SparkFun_SCD30_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 "SparkFun_SCD30_Arduino_Library.h" SCD30::SCD30(void) { // Constructor } //Initialize the Serial port #ifdef USE_TEENSY3_I2C_LIB bool SCD30::begin(i2c_t3 &wirePort, bool autoCalibrate, bool measBegin) #else bool SCD30::begin(TwoWire &wirePort, bool autoCalibrate, bool measBegin) #endif { _i2cPort = &wirePort; //Grab which port the user wants us to use /* Especially during obtaining the ACK BIT after a byte sent the SCD30 is using clock stretching (but NOT only there)! * The need for clock stretching is described in the Sensirion_CO2_Sensors_SCD30_Interface_Description.pdf * * The default clock stretch (maximum wait time) on the ESP8266-library (2.4.2) is 230us which is set during _i2cPort->begin(); * In the current implementation of the ESP8266 I2C driver there is NO error message when this time expired, while * the clock stretch is still happening, causing uncontrolled behaviour of the hardware combination. * * To set ClockStretchlimit() a check for ESP8266 boards has been added in the driver. * * With setting to 20000, we set a max timeout of 20mS (> 20x the maximum measured) basically disabling the time-out * and now wait for clock stretch to be controlled by the client. */ #if defined(ARDUINO_ARCH_ESP8266) _i2cPort->setClockStretchLimit(200000); #endif uint16_t fwVer; if (getFirmwareVersion(&fwVer) == false) // Read the firmware version. Return false if the CRC check fails. return (false); if (_printDebug == true) { _debugPort->print(F("SCD30 begin: got firmware version 0x")); _debugPort->println(fwVer, HEX); } if (measBegin == false) // Exit now if measBegin is false return (true); //Check for device to respond correctly if (beginMeasuring() == true) //Start continuous measurements { setMeasurementInterval(2); //2 seconds between measurements setAutoSelfCalibration(autoCalibrate); //Enable auto-self-calibration return (true); } return (false); //Something went wrong } //Calling this function with nothing sets the debug port to Serial //You can also call it with other streams like Serial1, SerialUSB, etc. void SCD30::enableDebugging(Stream &debugPort) { _debugPort = &debugPort; _printDebug = true; } //Returns the latest available CO2 level //If the current level has already been reported, trigger a new read uint16_t SCD30::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 SCD30::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 SCD30::getTemperature(void) { if (temperatureHasBeenReported == true) //Trigger a new read readMeasurement(); //Pull in new co2, humidity, and temp into global vars temperatureHasBeenReported = true; return temperature; } //Enables or disables the ASC bool SCD30::setAutoSelfCalibration(bool enable) { if (enable) return sendCommand(COMMAND_AUTOMATIC_SELF_CALIBRATION, 1); //Activate continuous ASC else return sendCommand(COMMAND_AUTOMATIC_SELF_CALIBRATION, 0); //Deactivate continuous ASC } //Set the forced recalibration factor. See 1.3.7. //The reference CO2 concentration has to be within the range 400 ppm ≤ cref(CO2) ≤ 2000 ppm. bool SCD30::setForcedRecalibrationFactor(uint16_t concentration) { if (concentration < 400 || concentration > 2000) { return false; //Error check. } return sendCommand(COMMAND_SET_FORCED_RECALIBRATION_FACTOR, concentration); } //Get the temperature offset. See 1.3.8. float SCD30::getTemperatureOffset(void) { uint16_t response = readRegister(COMMAND_SET_TEMPERATURE_OFFSET); return (((float)response) / 100.0); } //Set the temperature offset. See 1.3.8. bool SCD30::setTemperatureOffset(float tempOffset) { union { int16_t signed16; uint16_t unsigned16; } signedUnsigned; // Avoid any ambiguity casting int16_t to uint16_t signedUnsigned.signed16 = tempOffset * 100; return sendCommand(COMMAND_SET_TEMPERATURE_OFFSET, signedUnsigned.unsigned16); } //Get the altitude compenstation. See 1.3.9. uint16_t SCD30::getAltitudeCompensation(void) { return readRegister(COMMAND_SET_ALTITUDE_COMPENSATION); } //Set the altitude compenstation. See 1.3.9. bool SCD30::setAltitudeCompensation(uint16_t altitude) { return sendCommand(COMMAND_SET_ALTITUDE_COMPENSATION, altitude); } //Set the pressure compenstation. This is passed during measurement startup. //mbar can be 700 to 1200 bool SCD30::setAmbientPressure(uint16_t pressure_mbar) { if (pressure_mbar < 700 || pressure_mbar > 1200) { return false; } return sendCommand(COMMAND_CONTINUOUS_MEASUREMENT, pressure_mbar); } // SCD30 soft reset void SCD30::reset() { sendCommand(COMMAND_RESET); } // Get the current ASC setting bool SCD30::getAutoSelfCalibration() { uint16_t response = readRegister(COMMAND_AUTOMATIC_SELF_CALIBRATION); if (response == 1) { return true; } else { return false; } } //Begins continuous measurements //Continuous measurement status is saved in non-volatile memory. When the sensor //is powered down while continuous measurement mode is active SCD30 will measure //continuously after repowering without sending the measurement command. //Returns true if successful bool SCD30::beginMeasuring(uint16_t pressureOffset) { return (sendCommand(COMMAND_CONTINUOUS_MEASUREMENT, pressureOffset)); } //Overload - no pressureOffset bool SCD30::beginMeasuring(void) { return (beginMeasuring(0)); } // Stop continuous measurement bool SCD30::StopMeasurement(void) { return(sendCommand(COMMAND_STOP_MEAS)); } //Sets interval between measurements //2 seconds to 1800 seconds (30 minutes) bool SCD30::setMeasurementInterval(uint16_t interval) { return sendCommand(COMMAND_SET_MEASUREMENT_INTERVAL, interval); } //Returns true when data is available bool SCD30::dataAvailable() { uint16_t response = readRegister(COMMAND_GET_DATA_READY); if (response == 1) return (true); return (false); } //Get 18 bytes from SCD30 //Updates global variables with floats //Returns true if success bool SCD30::readMeasurement() { //Verify we have data from the sensor if (dataAvailable() == false) return (false); ByteToFl tempCO2; tempCO2.value = 0; ByteToFl tempHumidity; tempHumidity.value = 0; ByteToFl tempTemperature; tempTemperature.value = 0; _i2cPort->beginTransmission(SCD30_ADDRESS); _i2cPort->write(COMMAND_READ_MEASUREMENT >> 8); //MSB _i2cPort->write(COMMAND_READ_MEASUREMENT & 0xFF); //LSB if (_i2cPort->endTransmission() != 0) return (0); //Sensor did not ACK const uint8_t receivedBytes = _i2cPort->requestFrom((uint8_t)SCD30_ADDRESS, (uint8_t)18); bool error = false; if (_i2cPort->available()) { byte bytesToCrc[2]; for (byte x = 0; x < 18; x++) { byte incoming = _i2cPort->read(); switch (x) { case 0: case 1: case 3: case 4: tempCO2.array[x < 3 ? 3-x : 4-x] = incoming; bytesToCrc[x % 3] = incoming; break; case 6: case 7: case 9: case 10: tempTemperature.array[x < 9 ? 9-x : 10-x] = incoming; bytesToCrc[x % 3] = incoming; break; case 12: case 13: case 15: case 16: tempHumidity.array[x < 15 ? 15-x : 16-x] = incoming; bytesToCrc[x % 3] = incoming; break; default: //Validate CRC uint8_t foundCrc = computeCRC8(bytesToCrc, 2); if (foundCrc != incoming) { if (_printDebug == true) { _debugPort->print(F("readMeasurement: found CRC in byte ")); _debugPort->print(x); _debugPort->print(F(", expected 0x")); _debugPort->print(foundCrc, HEX); _debugPort->print(F(", got 0x")); _debugPort->println(incoming, HEX); } error = true; } break; } } } else { if (_printDebug == true) { _debugPort->print(F("readMeasurement: no SCD30 data found from I2C, i2c claims we should receive ")); _debugPort->print(receivedBytes); _debugPort->println(F(" bytes")); } return false; } if (error) { if (_printDebug == true) _debugPort->println(F("readMeasurement: encountered error reading SCD30 data.")); return false; } //Now copy the uint32s into their associated floats co2 = tempCO2.value; temperature = tempTemperature.value; humidity = tempHumidity.value; //Mark our global variables as fresh co2HasBeenReported = false; humidityHasBeenReported = false; temperatureHasBeenReported = false; return (true); //Success! New data available in globals. } //Gets a setting by reading the appropriate register. //Returns true if the CRC is valid. bool SCD30::getSettingValue(uint16_t registerAddress, uint16_t *val) { _i2cPort->beginTransmission(SCD30_ADDRESS); _i2cPort->write(registerAddress >> 8); //MSB _i2cPort->write(registerAddress & 0xFF); //LSB if (_i2cPort->endTransmission() != 0) return (false); //Sensor did not ACK _i2cPort->requestFrom((uint8_t)SCD30_ADDRESS, (uint8_t)3); // Request data and CRC if (_i2cPort->available()) { uint8_t data[2]; data[0] = _i2cPort->read(); data[1] = _i2cPort->read(); uint8_t crc = _i2cPort->read(); *val = (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); if (_printDebug == true) { _debugPort->print(F("getSettingValue: CRC fail: expected 0x")); _debugPort->print(expectedCRC, HEX); _debugPort->print(F(", got 0x")); _debugPort->println(crc, HEX); } } return (false); } //Gets two bytes from SCD30 uint16_t SCD30::readRegister(uint16_t registerAddress) { _i2cPort->beginTransmission(SCD30_ADDRESS); _i2cPort->write(registerAddress >> 8); //MSB _i2cPort->write(registerAddress & 0xFF); //LSB if (_i2cPort->endTransmission() != 0) return (0); //Sensor did not ACK _i2cPort->requestFrom((uint8_t)SCD30_ADDRESS, (uint8_t)2); if (_i2cPort->available()) { uint8_t msb = _i2cPort->read(); uint8_t lsb = _i2cPort->read(); return ((uint16_t)msb << 8 | lsb); } return (0); //Sensor did not respond } //Sends a command along with arguments and CRC bool SCD30::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 _i2cPort->beginTransmission(SCD30_ADDRESS); _i2cPort->write(command >> 8); //MSB _i2cPort->write(command & 0xFF); //LSB _i2cPort->write(arguments >> 8); //MSB _i2cPort->write(arguments & 0xFF); //LSB _i2cPort->write(crc); if (_i2cPort->endTransmission() != 0) return (false); //Sensor did not ACK return (true); } //Sends just a command, no arguments, no CRC bool SCD30::sendCommand(uint16_t command) { _i2cPort->beginTransmission(SCD30_ADDRESS); _i2cPort->write(command >> 8); //MSB _i2cPort->write(command & 0xFF); //LSB if (_i2cPort->endTransmission() != 0) return (false); //Sensor did not ACK return (true); } //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 SCD30::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 }