/******************************************************************************
SparkFunCCS811.cpp
CCS811 Arduino library
Marshall Taylor @ SparkFun Electronics
Nathan Seidle @ SparkFun Electronics
April 4, 2017
https://github.com/sparkfun/CCS811_Air_Quality_Breakout
https://github.com/sparkfun/SparkFun_CCS811_Arduino_Library
Resources:
Uses Wire.h for i2c operation
Development environment specifics:
Arduino IDE 1.8.1
This code is released under the [MIT License](http://opensource.org/licenses/MIT).
Please review the LICENSE.md file included with this example. If you have any questions
or concerns with licensing, please contact techsupport@sparkfun.com.
Distributed as-is; no warranty is given.
******************************************************************************/
//See SparkFunCCS811.h for additional topology notes.
#include "SparkFunCCS811.h"
#include "stdint.h"
#include <Arduino.h>
#include "Wire.h"
#include <math.h>
//****************************************************************************//
//
// CCS811Core functions
//
// Default <address> is 0x5B.
//
//****************************************************************************//
CCS811Core::CCS811Core(uint8_t inputArg) : I2CAddress(inputArg)
{
}
CCS811Core::CCS811_Status_e CCS811Core::beginCore(TwoWire &wirePort)
{
CCS811Core::CCS811_Status_e returnError = CCS811_Stat_SUCCESS;
_i2cPort = &wirePort; //Pull in user's choice of I2C hardware
//Wire.begin(); //See issue 13 https://github.com/sparkfun/SparkFun_CCS811_Arduino_Library/issues/13
#ifdef __AVR__
#endif
#ifdef __MK20DX256__
#endif
#if defined(ARDUINO_ARCH_ESP8266)
_i2cPort->setClockStretchLimit(200000); // was default 230 uS, now 200ms
#endif
//Spin for a few ms
volatile uint8_t temp = 0;
for (uint16_t i = 0; i < 10000; i++)
{
temp++;
}
//Check the ID register to determine if the operation was a success.
uint8_t readCheck;
readCheck = 0;
returnError = readRegister(CSS811_HW_ID, &readCheck);
if (returnError != CCS811_Stat_SUCCESS)
return returnError;
if (readCheck != 0x81)
{
returnError = CCS811_Stat_ID_ERROR;
}
return returnError;
}
//****************************************************************************//
//
// ReadRegister
//
// Parameters:
// offset -- register to read
// *outputPointer -- Pass &variable (address of) to save read data to
//
//****************************************************************************//
CCS811Core::CCS811_Status_e CCS811Core::readRegister(uint8_t offset, uint8_t *outputPointer)
{
//Return value
uint8_t numBytes = 1;
CCS811Core::CCS811_Status_e returnError = CCS811_Stat_SUCCESS;
_i2cPort->beginTransmission(I2CAddress);
_i2cPort->write(offset);
if (_i2cPort->endTransmission() != 0)
{
returnError = CCS811_Stat_I2C_ERROR;
}
_i2cPort->requestFrom(I2CAddress, numBytes);
*outputPointer = _i2cPort->read(); // receive a byte as a proper uint8_t
return returnError;
}
//****************************************************************************//
//
// multiReadRegister
//
// Parameters:
// offset -- register to read
// *outputPointer -- Pass &variable (base address of) to save read data to
// length -- number of bytes to read
//
// Note: Does not know if the target memory space is an array or not, or
// if there is the array is big enough. if the variable passed is only
// two bytes long and 3 bytes are requested, this will over-write some
// other memory!
//
//****************************************************************************//
CCS811Core::CCS811_Status_e CCS811Core::multiReadRegister(uint8_t offset, uint8_t *outputPointer, uint8_t length)
{
CCS811Core::CCS811_Status_e returnError = CCS811_Stat_SUCCESS;
//define pointer that will point to the external space
uint8_t i = 0;
uint8_t c = 0;
//Set the address
_i2cPort->beginTransmission(I2CAddress);
_i2cPort->write(offset);
if (_i2cPort->endTransmission() != 0)
{
returnError = CCS811_Stat_I2C_ERROR;
}
else //OK, all worked, keep going
{
// request 6 bytes from slave device
_i2cPort->requestFrom(I2CAddress, length);
while ((_i2cPort->available()) && (i < length)) // slave may send less than requested
{
c = _i2cPort->read(); // receive a byte as character
*outputPointer = c;
outputPointer++;
i++;
}
}
return returnError;
}
//****************************************************************************//
//
// writeRegister
//
// Parameters:
// offset -- register to write
// dataToWrite -- 8 bit data to write to register
//
//****************************************************************************//
CCS811Core::CCS811_Status_e CCS811Core::writeRegister(uint8_t offset, uint8_t dataToWrite)
{
CCS811Core::CCS811_Status_e returnError = CCS811_Stat_SUCCESS;
_i2cPort->beginTransmission(I2CAddress);
_i2cPort->write(offset);
_i2cPort->write(dataToWrite);
if (_i2cPort->endTransmission() != 0)
{
returnError = CCS811_Stat_I2C_ERROR;
}
return returnError;
}
//****************************************************************************//
//
// multiReadRegister
//
// Parameters:
// offset -- register to read
// *inputPointer -- Pass &variable (base address of) to save read data to
// length -- number of bytes to read
//
// Note: Does not know if the target memory space is an array or not, or
// if there is the array is big enough. if the variable passed is only
// two bytes long and 3 bytes are requested, this will over-write some
// other memory!
//
//****************************************************************************//
CCS811Core::CCS811_Status_e CCS811Core::multiWriteRegister(uint8_t offset, uint8_t *inputPointer, uint8_t length)
{
CCS811Core::CCS811_Status_e returnError = CCS811_Stat_SUCCESS;
//define pointer that will point to the external space
uint8_t i = 0;
//Set the address
_i2cPort->beginTransmission(I2CAddress);
_i2cPort->write(offset);
while (i < length) // send data bytes
{
_i2cPort->write(*inputPointer); // receive a byte as character
inputPointer++;
i++;
}
if (_i2cPort->endTransmission() != 0)
{
returnError = CCS811_Stat_I2C_ERROR;
}
return returnError;
}
//****************************************************************************//
//
// Main user class -- wrapper for the core class + maths
//
// Construct with same rules as the core ( uint8_t busType, uint8_t inputArg )
//
//****************************************************************************//
CCS811::CCS811(uint8_t inputArg) : CCS811Core(inputArg)
{
refResistance = 10000; //Unsupported feature.
resistance = 0; //Unsupported feature.
temperature = 0;
tVOC = 0;
CO2 = 0;
}
CCS811::CCS811() : CCS811(0){}
//****************************************************************************//
//
// Begin
//
// This starts the lower level begin, then applies settings
//
//****************************************************************************//
bool CCS811::begin(TwoWire &wirePort)
{
if (beginWithStatus(wirePort) == CCS811_Stat_SUCCESS)
return true;
return false;
}
//****************************************************************************//
//
// Begin
//
// This starts the lower level begin, then applies settings
//
//****************************************************************************//
CCS811Core::CCS811_Status_e CCS811::beginWithStatus(TwoWire &wirePort)
{
uint8_t data[4] = {0x11, 0xE5, 0x72, 0x8A}; //Reset key
CCS811Core::CCS811_Status_e returnError = CCS811_Stat_SUCCESS; //Default error state
//restart the core
returnError = beginCore(wirePort);
if (returnError != CCS811_Stat_SUCCESS)
return returnError;
//Reset the device
multiWriteRegister(CSS811_SW_RESET, data, 4);
//Tclk = 1/16MHz = 0x0000000625
//0.001 s / tclk = 16000 counts
volatile uint8_t temp = 0;
#ifdef ARDUINO_ARCH_ESP32
for (uint32_t i = 0; i < 80000; i++) //This waits > 1ms @ 80MHz clock
{
temp++;
}
#elif __AVR__
for (uint16_t i = 0; i < 16000; i++) //This waits > 1ms @ 16MHz clock
{
temp++;
}
#else
for (uint32_t i = 0; i < 200000; i++) //Spin for a good while
{
temp++;
}
#endif
if (checkForStatusError() == true)
return CCS811_Stat_INTERNAL_ERROR;
if (appValid() == false)
return CCS811_Stat_INTERNAL_ERROR;
//Write 0 bytes to this register to start app
_i2cPort->beginTransmission(I2CAddress);
_i2cPort->write(CSS811_APP_START);
if (_i2cPort->endTransmission() != 0)
{
return CCS811_Stat_I2C_ERROR;
}
//Added from issue 6
// Without a delay here, the CCS811 and I2C can be put in a bad state.
// Seems to work with 50us delay, but make a bit longer to be sure.
#if defined(ARDUINO_ARCH_ESP32) || defined(ARDUINO_ARCH_ESP8266)
delayMicroseconds(100);
#endif
returnError = setDriveMode(1); //Read every second
return returnError;
}
//****************************************************************************//
//
// Sensor functions
//
//****************************************************************************//
//Updates the total voltatile organic compounds (TVOC) in parts per billion (PPB)
//and the CO2 value
//Returns nothing
CCS811Core::CCS811_Status_e CCS811::readAlgorithmResults(void)
{
uint8_t data[4];
CCS811Core::CCS811_Status_e returnError = multiReadRegister(CSS811_ALG_RESULT_DATA, data, 4);
if (returnError != CCS811_Stat_SUCCESS)
return returnError;
// Data ordered:
// co2MSB, co2LSB, tvocMSB, tvocLSB
CO2 = ((uint16_t)data[0] << 8) | data[1];
tVOC = ((uint16_t)data[2] << 8) | data[3];
return CCS811_Stat_SUCCESS;
}
//Checks to see if error bit is set
bool CCS811::checkForStatusError(void)
{
uint8_t value;
//return the status bit
readRegister(CSS811_STATUS, &value);
return (value & 1 << 0);
}
//Checks to see if DATA_READ flag is set in the status register
bool CCS811::dataAvailable(void)
{
uint8_t value;
CCS811Core::CCS811_Status_e returnError = readRegister(CSS811_STATUS, &value);
if (returnError != CCS811_Stat_SUCCESS)
{
return 0;
}
else
{
return (value & 1 << 3);
}
}
//Checks to see if APP_VALID flag is set in the status register
bool CCS811::appValid(void)
{
uint8_t value;
CCS811Core::CCS811_Status_e returnError = readRegister(CSS811_STATUS, &value);
if (returnError != CCS811_Stat_SUCCESS)
{
return 0;
}
else
{
return (value & 1 << 4);
}
}
uint8_t CCS811::getErrorRegister(void)
{
uint8_t value;
CCS811Core::CCS811_Status_e returnError = readRegister(CSS811_ERROR_ID, &value);
if (returnError != CCS811_Stat_SUCCESS)
{
return 0xFF;
}
else
{
return value; //Send all errors in the event of communication error
}
}
//Returns the baseline value
//Used for telling sensor what 'clean' air is
//You must put the sensor in clean air and record this value
uint16_t CCS811::getBaseline(void)
{
uint8_t data[2];
CCS811Core::CCS811_Status_e returnError = multiReadRegister(CSS811_BASELINE, data, 2);
unsigned int baseline = ((uint16_t)data[0] << 8) | data[1];
if (returnError != CCS811_Stat_SUCCESS)
{
return 0;
}
else
{
return (baseline);
}
}
CCS811Core::CCS811_Status_e CCS811::setBaseline(uint16_t input)
{
uint8_t data[2];
data[0] = (input >> 8) & 0x00FF;
data[1] = input & 0x00FF;
CCS811Core::CCS811_Status_e returnError = multiWriteRegister(CSS811_BASELINE, data, 2);
return returnError;
}
//Enable the nINT signal
CCS811Core::CCS811_Status_e CCS811::enableInterrupts(void)
{
uint8_t value;
CCS811Core::CCS811_Status_e returnError = readRegister(CSS811_MEAS_MODE, &value); //Read what's currently there
if (returnError != CCS811_Stat_SUCCESS)
return returnError;
value |= (1 << 3); //Set INTERRUPT bit
writeRegister(CSS811_MEAS_MODE, value);
return returnError;
}
//Disable the nINT signal
CCS811Core::CCS811_Status_e CCS811::disableInterrupts(void)
{
uint8_t value;
CCS811Core::CCS811_Status_e returnError = readRegister(CSS811_MEAS_MODE, &value); //Read what's currently there
if (returnError != CCS811_Stat_SUCCESS)
return returnError;
value &= ~(1 << 3); //Clear INTERRUPT bit
returnError = writeRegister(CSS811_MEAS_MODE, value);
return returnError;
}
//Mode 0 = Idle
//Mode 1 = read every 1s
//Mode 2 = every 10s
//Mode 3 = every 60s
//Mode 4 = RAW mode
CCS811Core::CCS811_Status_e CCS811::setDriveMode(uint8_t mode)
{
if (mode > 4)
mode = 4; //sanitize input
uint8_t value;
CCS811Core::CCS811_Status_e returnError = readRegister(CSS811_MEAS_MODE, &value); //Read what's currently there
if (returnError != CCS811_Stat_SUCCESS)
return returnError;
value &= ~(0b00000111 << 4); //Clear DRIVE_MODE bits
value |= (mode << 4); //Mask in mode
returnError = writeRegister(CSS811_MEAS_MODE, value);
return returnError;
}
//Given a temp and humidity, write this data to the CSS811 for better compensation
//This function expects the humidity and temp to come in as floats
CCS811Core::CCS811_Status_e CCS811::setEnvironmentalData(float relativeHumidity, float temperature)
{
//Check for invalid temperatures
if ((temperature < -25) || (temperature > 50))
return CCS811_Stat_GENERIC_ERROR;
//Check for invalid humidity
if ((relativeHumidity < 0) || (relativeHumidity > 100))
return CCS811_Stat_GENERIC_ERROR;
uint32_t rH = relativeHumidity * 1000; //42.348 becomes 42348
uint32_t temp = temperature * 1000; //23.2 becomes 23200
byte envData[4];
//Split value into 7-bit integer and 9-bit fractional
//Incorrect way from datasheet.
//envData[0] = ((rH % 1000) / 100) > 7 ? (rH / 1000 + 1) << 1 : (rH / 1000) << 1;
//envData[1] = 0; //CCS811 only supports increments of 0.5 so bits 7-0 will always be zero
//if (((rH % 1000) / 100) > 2 && (((rH % 1000) / 100) < 8))
//{
// envData[0] |= 1; //Set 9th bit of fractional to indicate 0.5%
//}
//Correct rounding. See issue 8: https://github.com/sparkfun/Qwiic_BME280_CCS811_Combo/issues/8
envData[0] = (rH + 250) / 500;
envData[1] = 0; //CCS811 only supports increments of 0.5 so bits 7-0 will always be zero
temp += 25000; //Add the 25C offset
//Split value into 7-bit integer and 9-bit fractional
//envData[2] = ((temp % 1000) / 100) > 7 ? (temp / 1000 + 1) << 1 : (temp / 1000) << 1;
//envData[3] = 0;
//if (((temp % 1000) / 100) > 2 && (((temp % 1000) / 100) < 8))
//{
// envData[2] |= 1; //Set 9th bit of fractional to indicate 0.5C
//}
//Correct rounding
envData[2] = (temp + 250) / 500;
envData[3] = 0;
CCS811Core::CCS811_Status_e returnError = multiWriteRegister(CSS811_ENV_DATA, envData, 4);
return returnError;
}
uint16_t CCS811::getTVOC(void)
{
return tVOC;
}
uint16_t CCS811::getCO2(void)
{
return CO2;
}
//****************************************************************************//
//
// The CCS811 no longer supports temperature compensation from an NTC thermistor.
// NTC thermistor compensation will only work on boards purchased in 2017.
// List of unsupported functions:
// setRefResistance();
// readNTC();
// getResistance();
// getTemperature();
//
//****************************************************************************//
void CCS811::setRefResistance(float input)
{
refResistance = input;
}
CCS811Core::CCS811_Status_e CCS811::readNTC(void)
{
uint8_t data[4];
CCS811Core::CCS811_Status_e returnError = multiReadRegister(CSS811_NTC, data, 4);
vrefCounts = ((uint16_t)data[0] << 8) | data[1];
//Serial.print("vrefCounts: ");
//Serial.println(vrefCounts);
ntcCounts = ((uint16_t)data[2] << 8) | data[3];
//Serial.print("ntcCounts: ");
//Serial.println(ntcCounts);
//Serial.print("sum: ");
//Serial.println(ntcCounts + vrefCounts);
resistance = ((float)ntcCounts * refResistance / (float)vrefCounts);
//Code from Milan Malesevic and Zoran Stupic, 2011,
//Modified by Max Mayfield,
temperature = log((long)resistance);
temperature = 1 / (0.001129148 + (0.000234125 * temperature) + (0.0000000876741 * temperature * temperature * temperature));
temperature = temperature - 273.15; // Convert Kelvin to Celsius
return returnError;
}
float CCS811::getResistance(void)
{
return resistance;
}
float CCS811::getTemperature(void)
{
return temperature;
}
const char *CCS811::statusString(CCS811_Status_e stat)
{
CCS811_Status_e val;
if (stat == CCS811_Stat_NUM)
{
val = stat;
}
else
{
val = stat;
}
switch (val)
{
case CCS811_Stat_SUCCESS:
return "All is well.";
break;
case CCS811_Stat_ID_ERROR:
return "ID Error";
break;
case CCS811_Stat_I2C_ERROR:
return "I2C Error";
break;
case CCS811_Stat_INTERNAL_ERROR:
return "Internal Error";
break;
case CCS811_Stat_GENERIC_ERROR:
return "Generic Error";
break;
default:
return "Unknown Status";
break;
}
return "None";
}