M5Atom_airqa/ccs811.cpp

575 lines
18 KiB
C++

/*
ccs811.cpp - Library for the CCS811 digital gas sensor for monitoring indoor air quality from ams.
2019 jan 22 v10 Maarten Pennings Added F() on all strings, added get/set_baseline()
2019 jan 15 v9 Maarten Pennings Function set_envdata did not write T; flash now uses PROGMEM array; flash now works without app, better PRINT macros, removed warnings
2018 dec 06 v8 Maarten Pennings Added firmware flash routine
2018 Dec 04 v7 Maarten Pennings Added support for older CCS811's (fw 1100)
2018 Nov 11 v6 Maarten Pennings uint16 -> uint16_t, added cast
2018 Nov 02 v5 Maarten Pennings Added clearing of ERROR_ID
2018 Oct 23 v4 Maarten Pennings Added envdata/i2cdelay
2018 Oct 21 v3 Maarten Pennings Fixed bug in begin(), added hw-version
2018 Oct 21 v2 Maarten Pennings Simplified I2C, added version mngt
2017 Dec 11 v1 Maarten Pennings Created
*/
#include <Arduino.h>
#include <Wire.h>
#include "ccs811.h"
// begin() and flash() prints errors to help diagnose startup problems.
// Change these macro's to empty to suppress those prints.
#define PRINTLN(s) Serial.println(s)
#define PRINT(s) Serial.print(s)
#define PRINTLN2(s,m) Serial.println(s,m)
#define PRINT2(s,m) Serial.print(s,m)
//#define PRINTLN(s)
//#define PRINT(s)
//#define PRINTLN2(s,m)
//#define PRINT2(s,m)
// Timings
#define CCS811_WAIT_AFTER_RESET_US 2000 // The CCS811 needs a wait after reset
#define CCS811_WAIT_AFTER_APPSTART_US 1000 // The CCS811 needs a wait after app start
#define CCS811_WAIT_AFTER_WAKE_US 50 // The CCS811 needs a wait after WAKE signal
#define CCS811_WAIT_AFTER_APPERASE_MS 500 // The CCS811 needs a wait after app erase (300ms from spec not enough)
#define CCS811_WAIT_AFTER_APPVERIFY_MS 70 // The CCS811 needs a wait after app verify
#define CCS811_WAIT_AFTER_APPDATA_MS 50 // The CCS811 needs a wait after writing app data
// Main interface =====================================================================================================
// CCS811 registers/mailboxes, all 1 byte except when stated otherwise
#define CCS811_STATUS 0x00
#define CCS811_MEAS_MODE 0x01
#define CCS811_ALG_RESULT_DATA 0x02 // up to 8 bytes
#define CCS811_RAW_DATA 0x03 // 2 bytes
#define CCS811_ENV_DATA 0x05 // 4 bytes
#define CCS811_THRESHOLDS 0x10 // 5 bytes
#define CCS811_BASELINE 0x11 // 2 bytes
#define CCS811_HW_ID 0x20
#define CCS811_HW_VERSION 0x21
#define CCS811_FW_BOOT_VERSION 0x23 // 2 bytes
#define CCS811_FW_APP_VERSION 0x24 // 2 bytes
#define CCS811_ERROR_ID 0xE0
#define CCS811_APP_ERASE 0xF1 // 4 bytes
#define CCS811_APP_DATA 0xF2 // 9 bytes
#define CCS811_APP_VERIFY 0xF3 // 0 bytes
#define CCS811_APP_START 0xF4 // 0 bytes
#define CCS811_SW_RESET 0xFF // 4 bytes
// Pin number connected to nWAKE (nWAKE can also be bound to GND, then pass -1), slave address (5A or 5B)
CCS811::CCS811(int nwake, int slaveaddr) {
_nwake= nwake;
_slaveaddr= slaveaddr;
_i2cdelay_us= 0;
wake_init();
}
// Reset the CCS811, switch to app mode and check HW_ID. Returns false on problems.
bool CCS811::begin( void ) {
uint8_t sw_reset[]= {0x11,0xE5,0x72,0x8A};
uint8_t app_start[]= {};
uint8_t hw_id;
uint8_t hw_version;
uint8_t app_version[2];
uint8_t status;
bool ok;
// Wakeup CCS811
wake_up();
// Try to ping CCS811 (can we reach CCS811 via I2C?)
ok= i2cwrite(0,0,0);
if( !ok ) ok= i2cwrite(0,0,0); // retry
if( !ok ) {
// Try the other slave address
_slaveaddr= CCS811_SLAVEADDR_0 + CCS811_SLAVEADDR_1 - _slaveaddr; // swap address
ok= i2cwrite(0,0,0);
_slaveaddr= CCS811_SLAVEADDR_0 + CCS811_SLAVEADDR_1 - _slaveaddr; // swap back
if( ok ) {
PRINTLN(F("ccs811: wrong slave address, ping successful on other address"));
} else {
PRINTLN(F("ccs811: ping failed (VDD/GND connected? SDA/SCL connected?)"));
}
goto abort_begin;
}
// Invoke a SW reset (bring CCS811 in a know state)
ok= i2cwrite(CCS811_SW_RESET,4,sw_reset);
if( !ok ) {
PRINTLN(F("ccs811: reset failed"));
goto abort_begin;
}
delayMicroseconds(CCS811_WAIT_AFTER_RESET_US);
// Check that HW_ID is 0x81
ok= i2cread(CCS811_HW_ID,1,&hw_id);
if( !ok ) {
PRINTLN(F("ccs811: HW_ID read failed"));
goto abort_begin;
}
if( hw_id!=0x81 ) {
PRINT(F("ccs811: Wrong HW_ID: "));
PRINTLN2(hw_id,HEX);
goto abort_begin;
}
// Check that HW_VERSION is 0x1X
ok= i2cread(CCS811_HW_VERSION,1,&hw_version);
if( !ok ) {
PRINTLN(F("ccs811: HW_VERSION read failed"));
goto abort_begin;
}
if( (hw_version&0xF0)!=0x10 ) {
PRINT(F("ccs811: Wrong HW_VERSION: "));
PRINTLN2(hw_version,HEX);
goto abort_begin;
}
// Check status (after reset, CCS811 should be in boot mode with valid app)
ok= i2cread(CCS811_STATUS,1,&status);
if( !ok ) {
PRINTLN(F("ccs811: STATUS read (boot mode) failed"));
goto abort_begin;
}
if( status!=0x10 ) {
PRINT(F("ccs811: Not in boot mode, or no valid app: "));
PRINTLN2(status,HEX);
goto abort_begin;
}
// Read the application version
ok= i2cread(CCS811_FW_APP_VERSION,2,app_version);
if( !ok ) {
PRINTLN(F("ccs811: APP_VERSION read failed"));
goto abort_begin;
}
_appversion= app_version[0]*256+app_version[1];
// Switch CCS811 from boot mode into app mode
ok= i2cwrite(CCS811_APP_START,0,app_start);
if( !ok ) {
PRINTLN(F("ccs811: Goto app mode failed"));
goto abort_begin;
}
delayMicroseconds(CCS811_WAIT_AFTER_APPSTART_US);
// Check if the switch was successful
ok= i2cread(CCS811_STATUS,1,&status);
if( !ok ) {
PRINTLN(F("ccs811: STATUS read (app mode) failed"));
goto abort_begin;
}
if( status!=0x90 ) {
PRINT(F("ccs811: Not in app mode, or no valid app: "));
PRINTLN2(status,HEX);
goto abort_begin;
}
// CCS811 back to sleep
wake_down();
// Return success
return true;
abort_begin:
// CCS811 back to sleep
wake_down();
// Return failure
return false;
}
// Switch CCS811 to `mode`, use constants CCS811_MODE_XXX. Returns false on problems.
bool CCS811::start( int mode ) {
uint8_t meas_mode[]= {(uint8_t)(mode<<4)};
wake_up();
bool ok = i2cwrite(CCS811_MEAS_MODE,1,meas_mode);
wake_down();
return ok;
}
// Get measurement results from the CCS811, check status via errstat, e.g. ccs811_errstat(errstat)
void CCS811::read( uint16_t*eco2, uint16_t*etvoc, uint16_t*errstat,uint16_t*raw) {
bool ok;
uint8_t buf[8];
uint8_t stat;
wake_up();
if( _appversion<0x2000 ) {
ok= i2cread(CCS811_STATUS,1,&stat); // CCS811 with pre 2.0.0 firmware has wrong STATUS in CCS811_ALG_RESULT_DATA
if( ok && stat==CCS811_ERRSTAT_OK ) ok= i2cread(CCS811_ALG_RESULT_DATA,8,buf); else buf[5]=0;
buf[4]= stat; // Update STATUS field with correct STATUS
} else {
ok = i2cread(CCS811_ALG_RESULT_DATA,8,buf);
}
wake_down();
// Status and error management
uint16_t combined = buf[5]*256+buf[4];
if( combined & ~(CCS811_ERRSTAT_HWERRORS|CCS811_ERRSTAT_OK) ) ok= false; // Unused bits are 1: I2C transfer error
combined &= CCS811_ERRSTAT_HWERRORS|CCS811_ERRSTAT_OK; // Clear all unused bits
if( !ok ) combined |= CCS811_ERRSTAT_I2CFAIL;
// Clear ERROR_ID if flags are set
if( combined & CCS811_ERRSTAT_HWERRORS ) {
int err = get_errorid();
if( err==-1 ) combined |= CCS811_ERRSTAT_I2CFAIL; // Propagate I2C error
}
// Outputs
if( eco2 ) *eco2 = buf[0]*256+buf[1];
if( etvoc ) *etvoc = buf[2]*256+buf[3];
if( errstat) *errstat= combined;
if( raw ) *raw = buf[6]*256+buf[7];;
}
// Returns a string version of an errstat. Note, each call, this string is updated.
const char * CCS811::errstat_str(uint16_t errstat) {
static char s[17]; // 16 bits plus terminating zero
// First the ERROR_ID flags
s[ 0]='-';
s[ 1]='-';
if( errstat & CCS811_ERRSTAT_HEATER_SUPPLY ) s[ 2]='V'; else s[2]='v';
if( errstat & CCS811_ERRSTAT_HEATER_FAULT ) s[ 3]='H'; else s[3]='h';
if( errstat & CCS811_ERRSTAT_MAX_RESISTANCE ) s[ 4]='X'; else s[4]='x';
if( errstat & CCS811_ERRSTAT_MEASMODE_INVALID ) s[ 5]='M'; else s[5]='m';
if( errstat & CCS811_ERRSTAT_READ_REG_INVALID ) s[ 6]='R'; else s[6]='r';
if( errstat & CCS811_ERRSTAT_WRITE_REG_INVALID) s[ 7]='W'; else s[7]='w';
// Then the STATUS flags
if( errstat & CCS811_ERRSTAT_FW_MODE ) s[ 8]='F'; else s[8]='f';
s[ 9]='-';
s[10]='-';
if( errstat & CCS811_ERRSTAT_APP_VALID ) s[11]='A'; else s[11]='a';
if( errstat & CCS811_ERRSTAT_DATA_READY ) s[12]='D'; else s[12]='d';
s[13]='-';
// Next bit is used by SW to signal I2C transfer error
if( errstat & CCS811_ERRSTAT_I2CFAIL ) s[14]='I'; else s[14]='i';
if( errstat & CCS811_ERRSTAT_ERROR ) s[15]='E'; else s[15]='e';
s[16]='\0';
return s;
}
// Extra interface ========================================================================================
// Gets version of the CCS811 hardware (returns 0 on I2C failure)
int CCS811::hardware_version(void) {
uint8_t buf[1];
wake_up();
bool ok = i2cread(CCS811_HW_VERSION,1,buf);
wake_down();
int version= -1;
if( ok ) version= buf[0];
return version;
}
// Gets version of the CCS811 boot loader (returns 0 on I2C failure)
int CCS811::bootloader_version(void) {
uint8_t buf[2];
wake_up();
bool ok = i2cread(CCS811_FW_BOOT_VERSION,2,buf);
wake_down();
int version= -1;
if( ok ) version= buf[0]*256+buf[1];
return version;
}
// Gets version of the CCS811 application (returns 0 on I2C failure)
int CCS811::application_version(void) {
uint8_t buf[2];
wake_up();
bool ok = i2cread(CCS811_FW_APP_VERSION,2,buf);
wake_down();
int version= -1;
if( ok ) version= buf[0]*256+buf[1];
return version;
}
// Gets the ERROR_ID [same as 'err' part of 'errstat' in 'read'] (returns -1 on I2C failure)
// Note, this actually clears CCS811_ERROR_ID (hardware feature)
int CCS811::get_errorid(void) {
uint8_t buf[1];
wake_up();
bool ok = i2cread(CCS811_ERROR_ID,1,buf);
wake_down();
int version= -1;
if( ok ) version= buf[0];
return version;
}
#define HI(u16) ( (uint8_t)( ((u16)>>8)&0xFF ) )
#define LO(u16) ( (uint8_t)( ((u16)>>0)&0xFF ) )
// Writes t and h to ENV_DATA (see datasheet for format). Returns false on I2C problems.
bool CCS811::set_envdata(uint16_t t, uint16_t h) {
uint8_t envdata[]= { HI(h), LO(h), HI(t), LO(t) };
wake_up();
// Serial.print(" [T="); Serial.print(t); Serial.print(" H="); Serial.print(h); Serial.println("] ");
bool ok = i2cwrite(CCS811_ENV_DATA,4,envdata);
wake_down();
return ok;
}
// Writes t and h (in ENS210 format) to ENV_DATA. Returns false on I2C problems.
bool CCS811::set_envdata210(uint16_t t, uint16_t h) {
// Humidity formats of ENS210 and CCS811 are equal, we only need to map temperature.
// The lowest and highest (raw) ENS210 temperature values the CCS811 can handle
uint16_t lo= 15882; // (273.15-25)*64 = 15881.6 (float to int error is 0.4)
uint16_t hi= 24073; // 65535/8+lo = 24073.875 (24074 would map to 65539, so overflow)
// Check if ENS210 temperature is within CCS811 range, if not clip, if so map
bool ok;
if( t<lo ) ok= set_envdata(0,h);
else if( t>hi ) ok= set_envdata(65535,h);
else ok= set_envdata( (t-lo)*8+3 , h); // error in 'lo' is 0.4; times 8 is 3.2; so we correct 3
// Returns I2C transaction status
return ok;
}
// Reads (encoded) baseline from BASELINE (see datasheet). Returns false on I2C problems.
bool CCS811::get_baseline(uint16_t *baseline) {
uint8_t buf[2];
wake_up();
bool ok = i2cread(CCS811_BASELINE,2,buf);
wake_down();
*baseline= (buf[0]<<8) + buf[1];
return ok;
}
// Writes (encoded) baseline to BASELINE (see datasheet). Returns false on I2C problems.
bool CCS811::set_baseline(uint16_t baseline) {
uint8_t buf[]= { HI(baseline), LO(baseline) };
wake_up();
bool ok = i2cwrite(CCS811_BASELINE,2,buf);
wake_down();
return ok;
}
// Flashes the firmware of the CCS811 with size bytes from image - image _must_ be in PROGMEM
bool CCS811::flash(const uint8_t * image, int size) {
uint8_t sw_reset[]= {0x11,0xE5,0x72,0x8A};
uint8_t app_erase[]= {0xE7,0xA7,0xE6,0x09};
uint8_t app_verify[]= {};
uint8_t status;
int count;
bool ok;
wake_up();
// Try to ping CCS811 (can we reach CCS811 via I2C?)
PRINT(F("ccs811: ping "));
ok= i2cwrite(0,0,0);
if( !ok ) {
PRINTLN(F("FAILED"));
goto abort_begin;
}
PRINTLN(F("ok"));
// Invoke a SW reset (bring CCS811 in a know state)
PRINT(F("ccs811: reset "));
ok= i2cwrite(CCS811_SW_RESET,4,sw_reset);
if( !ok ) {
PRINTLN(F("FAILED"));
goto abort_begin;
}
delayMicroseconds(CCS811_WAIT_AFTER_RESET_US);
PRINTLN(F("ok"));
// Check status (after reset, CCS811 should be in boot mode with or without valid app)
PRINT(F("ccs811: status (reset1) "));
ok= i2cread(CCS811_STATUS,1,&status);
if( !ok ) {
PRINTLN(F("FAILED"));
goto abort_begin;
}
PRINT2(status,HEX);
PRINT(F(" "));
if( status!=0x00 && status!=0x10 ) {
PRINTLN(F("ERROR - ignoring")); // Seems to happens when there is no valid app
} else {
PRINTLN(F("ok"));
}
// Invoke app erase
PRINT(F("ccs811: app-erase "));
ok= i2cwrite(CCS811_APP_ERASE,4,app_erase);
if( !ok ) {
PRINTLN(F("FAILED"));
goto abort_begin;
}
delay(CCS811_WAIT_AFTER_APPERASE_MS);
PRINTLN(F("ok"));
// Check status (CCS811 should be in boot mode without valid app, with erase completed)
PRINT(F("ccs811: status (app-erase) "));
ok= i2cread(CCS811_STATUS,1,&status);
if( !ok ) {
PRINTLN(F("FAILED"));
goto abort_begin;
}
PRINT2(status,HEX);
PRINT(F(" "));
if( status!=0x40 ) {
PRINTLN(F("ERROR"));
goto abort_begin;
}
PRINTLN(F("ok"));
// Write all blocks
count= 0;
while( size>0 ) {
if( count%64==0 ) { PRINT(F("ccs811: writing ")); PRINT(size); PRINT(F(" ")); }
int len= size<8 ? size : 8;
// Copy PROGMEM to RAM
uint8_t ram[8];
memcpy_P(ram, image, len);
// Send 8 bytes from RAM to CCS811
ok= i2cwrite(CCS811_APP_DATA,len, ram);
if( !ok ) {
PRINTLN(F("ccs811: app data failed"));
goto abort_begin;
}
PRINT(F("."));
delay(CCS811_WAIT_AFTER_APPDATA_MS);
image+= len;
size-= len;
count++;
if( count%64==0 ) { PRINT(F(" ")); PRINTLN(size); }
}
if( count%64!=0 ) { PRINT(F(" ")); PRINTLN(size); }
// Invoke app verify
PRINT(F("ccs811: app-verify "));
ok= i2cwrite(CCS811_APP_VERIFY,0,app_verify);
if( !ok ) {
PRINTLN(F("FAILED"));
goto abort_begin;
}
delay(CCS811_WAIT_AFTER_APPVERIFY_MS);
PRINTLN(F("ok"));
// Check status (CCS811 should be in boot mode with valid app, and erased and verified)
PRINT(F("ccs811: status (app-verify) "));
ok= i2cread(CCS811_STATUS,1,&status);
if( !ok ) {
PRINTLN(F("FAILED"));
goto abort_begin;
}
PRINT2(status,HEX);
PRINT(F(" "));
if( status!=0x30 ) {
PRINTLN(F("ERROR"));
goto abort_begin;
}
PRINTLN(F("ok"));
// Invoke a second SW reset (clear flashing flags)
PRINT(F("ccs811: reset2 "));
ok= i2cwrite(CCS811_SW_RESET,4,sw_reset);
if( !ok ) {
PRINTLN(F("FAILED"));
goto abort_begin;
}
delayMicroseconds(CCS811_WAIT_AFTER_RESET_US);
PRINTLN(F("ok"));
// Check status (after reset, CCS811 should be in boot mode with valid app)
PRINT(F("ccs811: status (reset2) "));
ok= i2cread(CCS811_STATUS,1,&status);
if( !ok ) {
PRINTLN(F("FAILED"));
goto abort_begin;
}
PRINT2(status,HEX);
PRINT(F(" "));
if( status!=0x10 ) {
PRINTLN(F("ERROR"));
goto abort_begin;
}
PRINTLN(F("ok"));
// CCS811 back to sleep
wake_down();
// Return success
return true;
abort_begin:
// CCS811 back to sleep
wake_down();
// Return failure
return false;
}
// Advanced interface: i2cdelay ========================================================================================
// Delay before a repeated start - needed for e.g. ESP8266 because it doesn't handle I2C clock stretch correctly
void CCS811::set_i2cdelay(int us) {
if( us<0 ) us= 0;
_i2cdelay_us= us;
}
// Get current delay
int CCS811::get_i2cdelay(void) {
return _i2cdelay_us;
}
// Helper interface: nwake pin ========================================================================================
// _nwake<0 means nWAKE is not connected to a pin of the host, so no action needed
void CCS811::wake_init( void ) {
if( _nwake>=0 ) pinMode(_nwake, OUTPUT);
}
void CCS811::wake_up( void) {
if( _nwake>=0 ) { digitalWrite(_nwake, LOW); delayMicroseconds(CCS811_WAIT_AFTER_WAKE_US); }
}
void CCS811::wake_down( void) {
if( _nwake>=0 ) digitalWrite(_nwake, HIGH);
}
// Helper interface: i2c wrapper ======================================================================================
// Writes `count` from `buf` to register at address `regaddr` in the CCS811. Returns false on I2C problems.
bool CCS811::i2cwrite(int regaddr, int count, const uint8_t * buf) {
Wire.beginTransmission(_slaveaddr); // START, SLAVEADDR
Wire.write(regaddr); // Register address
for( int i=0; i<count; i++) Wire.write(buf[i]); // Write bytes
int r= Wire.endTransmission(true); // STOP
return r==0;
}
// Reads 'count` bytes from register at address `regaddr`, and stores them in `buf`. Returns false on I2C problems.
bool CCS811::i2cread(int regaddr, int count, uint8_t * buf) {
Wire.beginTransmission(_slaveaddr); // START, SLAVEADDR
Wire.write(regaddr); // Register address
int wres= Wire.endTransmission(false); // Repeated START
delayMicroseconds(_i2cdelay_us); // Wait
int rres=Wire.requestFrom(_slaveaddr,count); // From CCS811, read bytes, STOP
for( int i=0; i<count; i++ ) buf[i]=Wire.read();
return (wres==0) && (rres==count);
}