CCS811 driver added (#479)

This commit is contained in:
Gunar Schorcht 2017-12-11 04:00:53 +01:00 committed by Ruslan V. Uss
parent 2f62c6e9df
commit 61f23d0cf4
11 changed files with 1989 additions and 0 deletions

57
examples/ccs811/README.md Normal file
View file

@ -0,0 +1,57 @@
# CCS811 Driver Examples
These examples demonstrate the usage of the CCS811 driver with only one sensors.
## Hardware setup
Most examples use only one CCS811 sensor. Following figure shows the hardware configuration if no interrupt is used.
```
+------------------------+ +--------+
| ESP8266 Bus 0 | | CCS811 |
| GPIO 5 (SCL) >----> SCL |
| GPIO 4 (SDA) ------ SDA |
| GND -----> /WAKE |
+------------------------+ +--------+
```
If *nINT* interrupt is used to fetch new data, additionally the interrupt pin has to be connected to a GPIO pin.
```
+------------------------+ +--------+
| ESP8266 Bus 0 | | CCS811 |
| GPIO 5 (SCL) >----> SCL |
| GPIO 4 (SDA) ------ SDA |
| GPIO 2 <----- /nINT |
| GND -----> /WAKE |
+------------------------+ +--------+
```
In examples where CCS811 sensor is used in conjunction with a SHT3x sensor, the hardware configuration looks like following:
```
+------------------------+ +--------+
| ESP8266 Bus 0 | | CCS811 |
| GPIO 5 (SCL) >--+----> SCL |
| GPIO 4 (SDA) ---|-+--- SDA |
| GND ---|-|--> /WAKE |
| | | | +--------+
| | | | | SHT3x |
| | +----> SCL |
| | +--- SDA |
+------------------------+ +--------+
```
## Example description
__*ccs811_one_sensor*__
Simple example with one CCS811 sensor connected to I2C bus 0. It demonstrates the different approaches to fetch the data. Either the interrupt *nINT* is used when new data are available or exceed defined thresholds or the new data are fetched periodically. Which approach is used is defined by the constants ```INT_DATA_RDY_USED``` and ```INT_THRESHOLD_USED```.
__*ccs811_plus_sht3x*__
Simple example with one CCS811 sensor connected to I2C bus 0 and one SHT3x sensor to determine ambient temperature. New data are fetched peridically every 2 seconds.
__*ccs811_temperature*__
Simple example with one CCS811 sensor connected to I2C bus 0. It demonstrates how to use CCS811 with an external NTC resistor to determine ambient temperature.

View file

@ -0,0 +1,4 @@
PROGRAM=CCS811_One_Sensor
EXTRA_COMPONENTS = extras/i2c extras/ccs811
include ../../../common.mk

View file

@ -0,0 +1,159 @@
/**
* Simple example with one sensor connected to I2C bus 0. It demonstrates the
* different approaches to fetch the data. Either the interrupt *nINT* is used
* whenever new data are available or exceed defined thresholds or the new
* data are fetched periodically.
*
* Harware configuration:
*
* +------------------------+ +--------+
* | ESP8266 Bus 0 | | CCS811 |
* | GPIO 5 (SCL) >----> SCL |
* | GPIO 4 (SDA) ------ SDA |
* | GPIO 2 <----- /nINT |
* | GND -----> /WAKE |
* +------------------------+ +--------+
*/
// use following constants to define the demo mode
// #define INT_DATA_RDY_USED
// #define INT_THRESHOLD_USED
#include "espressif/esp_common.h"
#include "esp/uart.h"
#include "i2c/i2c.h"
#include "FreeRTOS.h"
#include <task.h>
// include CCS811 driver
#include "ccs811/ccs811.h"
// define I2C interfaces at which CCS811 sensors can be connected
#define I2C_BUS 0
#define I2C_SCL_PIN 5
#define I2C_SDA_PIN 4
// define GPIO for interrupt
#define INT_GPIO 2
static ccs811_sensor_t* sensor;
#if defined(INT_DATA_RDY_USED) || defined(INT_THRESHOLD_USED)
/**
* In this example, the interrupt *nINT* is used. It is triggered every time
* new data are available (INT_DATA_RDY_USED) or exceed defined thresholds
* (INT_THRESHOLD_USED). In this case, the user has to define an interrupt
* handler that fetches the data directly or triggers a task, that is waiting
* to fetch the data. In this example, a task is defined which suspends itself
* in each cycle to wait for fetching the data. The task is resumed by the
* the interrupt handler.
*/
TaskHandle_t nINT_task;
// User task that fetches the sensor values.
void user_task_interrupt (void *pvParameters)
{
uint16_t tvoc;
uint16_t eco2;
while (1)
{
// task suspends itself and waits to be resumed by interrupt handler
vTaskSuspend (NULL);
// after resume get the results and do something with them
if (ccs811_get_results (sensor, &tvoc, &eco2, 0, 0))
printf("%.3f CCS811 Sensor interrupt: TVOC %d ppb, eCO2 %d ppm\n",
(double)sdk_system_get_time()*1e-3, tvoc, eco2);
}
}
// Interrupt handler which resumes user_task_interrupt on interrupt
void nINT_handler (uint8_t gpio)
{
xTaskResumeFromISR (nINT_task);
}
#else
/*
* In this example, user task fetches the sensor values every seconds.
*/
void user_task_periodic(void *pvParameters)
{
uint16_t tvoc;
uint16_t eco2;
TickType_t last_wakeup = xTaskGetTickCount();
while (1)
{
// get environmental data from another sensor and set them
// ccs811_set_environmental_data (sensor, 25.3, 47.8);
// get the results and do something with them
if (ccs811_get_results (sensor, &tvoc, &eco2, 0, 0))
printf("%.3f CCS811 Sensor periodic: TVOC %d ppb, eCO2 %d ppm\n",
(double)sdk_system_get_time()*1e-3, tvoc, eco2);
// passive waiting until 1 second is over
vTaskDelayUntil(&last_wakeup, 1000 / portTICK_PERIOD_MS);
}
}
#endif
void user_init(void)
{
// set UART Parameter
uart_set_baud(0, 115200);
// give the UART some time to settle
sdk_os_delay_us(500);
/** -- MANDATORY PART -- */
// init all I2C bus interfaces at which CCS811 sensors are connected
i2c_init (I2C_BUS, I2C_SCL_PIN, I2C_SDA_PIN, I2C_FREQ_100K);
// longer clock stretching is required for CCS811
i2c_set_clock_stretch (I2C_BUS, CCS811_I2C_CLOCK_STRETCH);
// init the sensor with slave address CCS811_I2C_ADDRESS_1 connected I2C_BUS.
sensor = ccs811_init_sensor (I2C_BUS, CCS811_I2C_ADDRESS_1);
if (sensor)
{
#if defined(INT_DATA_RDY_USED) || defined(INT_THRESHOLD_USED)
// create a task that is resumed by interrupt handler to use the sensor
xTaskCreate(user_task_interrupt, "user_task_interrupt", 256, NULL, 2, &nINT_task);
// activate the interrupt for INT_GPIO and set the interrupt handler
gpio_set_interrupt(INT_GPIO, GPIO_INTTYPE_EDGE_NEG, nINT_handler);
#ifdef INT_DATA_RDY_USED
// enable the data ready interrupt
ccs811_enable_interrupt (sensor, true);
#else
// set threshold parameters and enable threshold interrupt mode
ccs811_set_eco2_thresholds (sensor, 600, 1100, 40);
#endif
#else
// create a periodic task that uses the sensor
xTaskCreate(user_task_periodic, "user_task_periodic", 256, NULL, 2, NULL);
#endif
// start periodic measurement with one measurement per second
ccs811_set_mode (sensor, ccs811_mode_1s);
}
}

View file

@ -0,0 +1,3 @@
PROGRAM=CCS811_Plus_SHT3x
EXTRA_COMPONENTS = extras/i2c extras/ccs811 extras/sht3x
include ../../../common.mk

View file

@ -0,0 +1,106 @@
/**
* Simple example with one CCS811 sensor connected to I2C bus 0 and one SHT3x
* sensor to determine ambient temperature. New data are fetched peridically.
*
* Harware configuration:
*
* +------------------------+ +--------+
* | ESP8266 Bus 0 | | CCS811 |
* | GPIO 5 (SCL) >--+----> SCL |
* | GPIO 4 (SDA) ---|-+--- SDA |
* | GND ---|-|--> /WAKE |
* | | | | +--------+
* | | | | | SHT3x |
* | | +----> SCL |
* | | +--- SDA |
* +------------------------+ +--------+
*/
#include "espressif/esp_common.h"
#include "esp/uart.h"
#include "i2c/i2c.h"
#include "FreeRTOS.h"
#include <task.h>
// include CCS811 driver
#include "ccs811/ccs811.h"
// include SHT3x driver
#include "sht3x/sht3x.h"
// define I2C interfaces at which CCS811 and SHT3x sensors are connected
#define I2C_BUS 0
#define I2C_SCL_PIN 5
#define I2C_SDA_PIN 4
static ccs811_sensor_t* ccs811; // CCS811 device data structure
static sht3x_sensor_t* sht3x; // SHT3x device data structure
/*
* User task that fetches the sensor values every 2 seconds.
*/
void user_task(void *pvParameters)
{
uint16_t tvoc;
uint16_t eco2;
float temperature;
float humidity;
// start periodic measurement with 1 measurement per second
ccs811_set_mode (ccs811, ccs811_mode_1s);
// start periodic measurements with 1 measurement per second
sht3x_start_measurement (sht3x, sht3x_periodic_1mps, sht3x_high);
// passive waiting until measurement results are available
vTaskDelay (sht3x_get_measurement_duration (sht3x_high));
TickType_t last_wakeup = xTaskGetTickCount();
while (1)
{
// get the results from CCS811 and do something with them
if (ccs811_get_results (ccs811, &tvoc, &eco2, 0, 0))
printf("%.3f CCS811 Sensor periodic: TVOC %d ppb, eCO2 %d ppm\n",
(double)sdk_system_get_time()*1e-3, tvoc, eco2);
// get the values from SHT3x and do something with them
if (sht3x_get_results (sht3x, &temperature, &humidity))
{
printf("%.3f SHT3x Sensor: %.2f °C, %.2f %%\n",
(double)sdk_system_get_time()*1e-3, temperature, humidity);
// set CCS811 environmental data with values fetched from SHT3x
ccs811_set_environmental_data (ccs811, temperature, humidity);
}
// passive waiting until 2 seconds is over
vTaskDelayUntil(&last_wakeup, 2000 / portTICK_PERIOD_MS);
}
}
void user_init(void)
{
// set UART Parameter
uart_set_baud(0, 115200);
// give the UART some time to settle
sdk_os_delay_us(500);
/** -- MANDATORY PART -- */
// init all I2C bus interfaces at which CCS811 sensors are connected
i2c_init(I2C_BUS, I2C_SCL_PIN, I2C_SDA_PIN, I2C_FREQ_100K);
// longer clock stretching is required for CCS811
i2c_set_clock_stretch (I2C_BUS, CCS811_I2C_CLOCK_STRETCH);
// init the sensors
ccs811 = ccs811_init_sensor (I2C_BUS, CCS811_I2C_ADDRESS_1);
sht3x = sht3x_init_sensor (I2C_BUS, SHT3x_ADDR_2);
if (ccs811 && sht3x)
// create a task that uses the sensor
xTaskCreate(user_task, "user_task", 256, NULL, 2, NULL);
}

View file

@ -0,0 +1,4 @@
PROGRAM=CCS811_Temperature
EXTRA_COMPONENTS = extras/i2c extras/ccs811
LIBS ?= gcc hal m
include ../../../common.mk

View file

@ -0,0 +1,131 @@
/**
* Simple example with one sensor connected to I2C bus 0. It demonstrates
* how to use CCS811 with an external NTC thermistor to determine ambient
* temperature.
*
* Harware configuration:
*
* +------------------------+ +--------+
* | ESP8266 Bus 0 | | CCS811 |
* | GPIO 5 (SCL) >----> SCL |
* | GPIO 4 (SDA) ------ SDA |
* | GND -----> /WAKE |
* +------------------------+ +--------+
*/
#include "espressif/esp_common.h"
#include "esp/uart.h"
#include "i2c/i2c.h"
#include "FreeRTOS.h"
#include <task.h>
#include <math.h>
// include CCS811 driver
#include "ccs811/ccs811.h"
// define I2C interfaces at which CCS811 sensors can be connected
#define I2C_BUS 0
#define I2C_SCL_PIN 5
#define I2C_SDA_PIN 4
static ccs811_sensor_t* sensor;
/*
* In this example, user task fetches the sensor values every seconds.
*/
// parameters of the Adafruit CCS811 Air Quality Sensor Breakout
#define CCS811_R_REF 100000
#define CCS811_R_NTC 10000
#define CCS811_R_NTC_TEMP 25
#define CCS811_BCONSTANT 3380
void user_task_periodic(void *pvParameters)
{
uint16_t tvoc;
uint16_t eco2;
TickType_t last_wakeup = xTaskGetTickCount();
while (1)
{
// get environmental data from another sensor and set them
// ccs811_set_environmental_data (sensor, 25.3, 47.8);
// get the results and do something with them
if (ccs811_get_results (sensor, &tvoc, &eco2, 0, 0))
printf("%.3f CCS811 Sensor periodic: TVOC %d ppb, eCO2 %d ppm\n",
(double)sdk_system_get_time()*1e-3, tvoc, eco2);
// get NTC resistance
uint32_t r_ntc = ccs811_get_ntc_resistance (sensor, CCS811_R_REF);
// calculation of temperature from application note ams AN000372
double ntc_temp;
ntc_temp = log((double)r_ntc / CCS811_R_NTC); // 1
ntc_temp /= CCS811_BCONSTANT; // 2
ntc_temp += 1.0 / (CCS811_R_NTC_TEMP + 273.15); // 3
ntc_temp = 1.0 / ntc_temp; // 4
ntc_temp -= 273.15; // 5
printf("%.3f CCS811 Sensor temperature: R_NTC %u Ohm, T %f °C\n",
(double)sdk_system_get_time()*1e-3, r_ntc, ntc_temp);
// passive waiting until 1 second is over
vTaskDelayUntil(&last_wakeup, 1100 / portTICK_PERIOD_MS);
}
}
void user_init(void)
{
// set UART Parameter
uart_set_baud(0, 115200);
// give the UART some time to settle
sdk_os_delay_us(500);
/** -- MANDATORY PART -- */
// init all I2C bus interfaces at which CCS811 sensors are connected
i2c_init(I2C_BUS, I2C_SCL_PIN, I2C_SDA_PIN, I2C_FREQ_100K);
// longer clock stretching is required for CCS811
i2c_set_clock_stretch (I2C_BUS, CCS811_I2C_CLOCK_STRETCH);
// init the sensor with slave address CCS811_I2C_ADDRESS_1 connected I2C_BUS.
sensor = ccs811_init_sensor (I2C_BUS, CCS811_I2C_ADDRESS_1);
if (sensor)
{
#if defined(INT_DATA_RDY_USED) || defined(INT_THRESHOLD_USED)
// create a task that is resumed by interrupt handler to use the sensor
xTaskCreate(user_task_interrupt, "user_task_interrupt", 256, NULL, 2, &nINT_task);
// set the GPIO and interrupt handler for *nINT* interrupt
gpio_set_interrupt(INT_GPIO, GPIO_INTTYPE_EDGE_NEG, nINT_handler);
#ifdef INT_DATA_RDY_USED
// enable the data ready interrupt
ccs811_enable_interrupt (sensor, true);
#else
// set threshold parameters and enable threshold interrupt mode
ccs811_set_eco2_thresholds (sensor, 600, 1100, 40);
#endif
#else
// create a periodic task that uses the sensor
xTaskCreate(user_task_periodic, "user_task_periodic", 256, NULL, 2, NULL);
#endif
// start periodic measurement with one measurement per second
ccs811_set_mode (sensor, ccs811_mode_1s);
}
}

549
extras/ccs811/README.md Normal file
View file

@ -0,0 +1,549 @@
# Driver for the ams CCS811 digital gas sensor for monitoring indoor air quality.
This driver is written for usage with the ESP8266 and FreeRTOS using the I2C interface driver.
## About the sensor
The CCS811 is an ultra-low power digital sensor which detects **Volatile Organic Compounds (VOC)** for Indoor Air Quality (IAQ) monitoring that. The sensor allows to
- convert raw sensor data to Total Volatile Organic Compound (TVOC) and equivalent CO2 (eCO2),
- compensate gas readings due to temperature and humidity using an external sensor,
- trigger interrupts when new measurement results are available or eCO2 value exceeds thresholds,
- correct baseline automatically or manually
- connect a NTC thermistor to provide means of calculating the local ambient temperature.
The sensor uses an I2C interface and supports clock stretching. See the notes on clock stretching during I2C interface intialization.
## Measurement Process
### Sensor modes
The CCS811 can operate in 5 different modes:
Mode | Driver symbol | Period | RAW data | IAQ values
---- | ------------- | ------ |:--------:|:----------:
Idle, Low Current Mode | ``` ccs811_mode_idle ``` | - | - | -
Constant Power Mode | ``` ccs811_mode_1s ``` | 1 s | X | X
Pulse Heating Mode | ``` ccs811_mode_10s ``` | 10 s | X | X
Low Power Pulse Heating Mode | ``` ccs811_mode_60s ``` | 60 s | X | X
Constant Power Mode | ``` ccs811_mode_250ms ``` | 250 ms | X | -
After power up, the sensor starts automatically in *Idle, Low Current Mode* (```mode_idle```). To start periodic measurements, the mode of the sensor has to be changed to any measurement mode. Measurement modes with with different rates of periodic measurements are available, see table above.
**Please note:** In *Constant Power Mode* with measurements every 250 ms (```mode_250ms```) only raw data are available. In all other measurement modes, the Indoor Air Quality (IAQ) values are available additionally. The *Constant Power Mode* with measurements every 250 ms (```mode_250ms```) is only intended for systems where an external host system wants to run an algorithm with raw data.
Once the is initialized with function ```ccs811_init_sensor```, function ```ccs811_set_mode``` can be used to start periodic measurements with a given period.
```
static ccs811_sensor_t* sensor;
...
if ((sensor = ccs811_init_sensor (I2C_BUS, CCS811_I2C_ADDRESS_1)))
{
...
// start periodic measurement with one measurement per second
ccs811_set_mode (sensor, ccs811_mode_1s);
}
...
```
**Please note:**
1. After setting the mode, the sensor is in conditioning period that needs up to 20 minutes, before accurate readings are generated, see the data sheet for more details.
2. During the early-live (burn-in) period, the CCS811 sensor should run for 48 hours in the selected mode of operation to ensure sensor performance is stable, see the data sheet for more details.
3. When the sensor operating mode is changed to a new mode with a lower sample rate, e.g., from *Pulse Heating Mode* (```mode_10s```) to *Low Power Pulse Heating Mode* (```mode_60s```), it should be placed in *Idle, Low Current Mode* (```mode_idle```) for at least 10 minutes before enabling the new mode.
When a sensor operating mode is changed to a new mode with a higher sample rate, e.g., from *Low Power Pulse Heating Mode* (```mode_60s```) to *Pulse Heating Mode* (```mode_10s```), there is no requirement to wait before enabling the new mode.
## Measurement results
Once the measurement mode is set, the user task can use function ```ccs811_get_results``` with same rate as the measurement rate to fetch the results. The function returns **raw data** as well as **Indoor Air Quality (IAQ)** values.
While raw data represents simply the current through the sensor and the voltage across the sensor with the selected current, IAQ values are the results of the processing these raw data by the sensor. IAQ values consist of the **equivalent CO2 (eCO2)** with a range from 400 ppm to 8192 ppm and **Total Volatile Organic Compound (TVOC)** with a range from 0 ppb to 1187 ppb.
```
uint16_t iaq_tvoc;
uint16_t iaq_eco2;
uint8_t raw_i;
uint16_t raw_v;
...
// get the results and do something with them
if (ccs811_get_results (sensor, &tvoc, &eco2, &raw_i, &raw_v))
{
...
}
...
```
If some of the results are not needed, the corresponding pointer parameters can be set to NULL.
If the function ```ccs811_get_results``` is called and no new data are available, e.g., due to the sensor mode time tolerance of 2%, the function still returns successfully. In this case, the results of the last measurement are returned and the error code CCS811_DRV_NO_NEW_DATA is set.
**Please note:**
1. In *Constant Power Mode* with measurements every 250 ms (```mode_250ms```) only raw data are available.
2. The rate of fetching data must not be greater than the rate of measurement. Due to the sensor mode timing tolerance of 2 %, the rate of fetching data should be lower than the measurement rate.
3. If the function is called and no new data are available, the results of the latest measurement are returned and error_code CCS811_DRV_NO_NEW_DATA is set.
### Compensation
If information about the environment like temperature and humidity are available from another sensor, they can be used by CCS811 to compensate gas readings due to temperature and humidity changes. Function ```ccs811_set_environmental_data``` can be used to set these environmental data.
```
float temperature;
float humidity;
...
if (sht3x_get_results (sht3x, &temperature, &humidity))
// set CCS811 environmental data with values fetched from SHT3x
ccs811_set_environmental_data (ccs811, temperature, humidity);
...
```
### NTC
CCS811 supports an external interface for connecting a negative thermal coefficient thermistor (R_NTC) to provide a cost effective and power efficient means of calculating the local ambient temperature. The sensor measures the voltage V_NTC across R_NTC as well as the voltage V_REF across a connected reference resistor (R_REF). Function ```ccs811_get_ntc_resistance``` can be used to fetch the current resistance of R_NTC. It uses the resistance of R_REF and measured voltages V_REF and V_NTV with the following equation:
R_NTC = R_REF / V_REF * V_NTC
Using the data sheet of the NTC, the ambient temperature can be calculated. See application note ams AN000372 for more details. For example, with Adafruit CCS811 Air Quality Sensor Breakout the ambienttemperature can be determined as following:
```
...
#define CCS811_R_REF 100000 // resistance of the reference resistor
#define CCS811_R_NTC 10000 // resistance of NTC at a reference temperature
#define CCS811_R_NTC_TEMP 25 // reference temperature for NTC
#define CCS811_BCONSTANT 3380 // B constant
// get NTC resistance
uint32_t r_ntc = ccs811_get_ntc_resistance (sensor, CCS811_R_REF);
// calculation of temperature from application note ams AN000372
double ntc_temp;
ntc_temp = log((double)r_ntc / CCS811_R_NTC); // 1
ntc_temp /= CCS811_BCONSTANT; // 2
ntc_temp += 1.0 / (CCS811_R_NTC_TEMP + 273.15); // 3
ntc_temp = 1.0 / ntc_temp; // 4
ntc_temp -= 273.15; // 5
....
```
### Interrupts
CCS811 supports two types of interrupts that can be used to fetch data:
- data ready interrupt (INT_DATA_RDY)
- threshold interrupt (INT_THRESHOLD)
#### Data ready interrupt
At the end of each measurement cycle (every 250 ms, 1 second, 10 seconds, or 60 seconds), CCS811 can optionally trigger an interrupt. The signal *nINT* is driven low as soon as new sensor values are ready to read. It will stop being driven low when sensor data are read with function ```ccs811_get_results```.
The interrupt is disabled by default. It can be enabled with function ```ccs811_enable_interrupt```.
```
...
// enable the data ready interrupt
ccs811_enable_interrupt (sensor, true);
...
```
#### Threshold interrupt
The user task can choose that the data ready interrupt is not generated every time when new sensor values become ready but only if the eCO2 value moves from the current range (LOW, MEDIUM, or HIGH) into another range by more than a hysteresis value. Hysteresis is used to prevent multiple interrupts close to a threshold.
The interrupt is disabled by default and can be enabled with function ```ccs811_set_eco2_thresholds```. The ranges are defined by parameters *low* and *high* as following
LOW below parameter value *low*
MEDIUM between parameter values *low* and *high*
HIGH above parameter value *high* is range HIGH.
If all parameters have valid values, the function sets the thresholds and enables the data ready interrupt. Using 0 for all parameters disables the interrupt.
```
...
// set threshold parameters and enable threshold interrupt mode
ccs811_set_eco2_thresholds (sensor, 600, 1100, 40);
...
```
### Baseline
CCS81 supports automatic baseline correction over a minimum time of 24 hours. Using function ```ccs811_get_baseline```, the current baseline value can be saved before the sensor is powered down. This baseline can then be restored with function ```ccs811_set_baseline``` after sensor is powered up again to continue the automatic baseline process.
## Error Handling
Most driver functions return a simple boolean value to indicate whether its execution was successful or an error happened. In the latter case, the member ```error_code``` of the sensor device data structure is set which indicates what error happened.
There are two different error levels that are ORed into one single *error_code*, errors in the I2C communication and errors of the CCS811 sensor itself. To test for a certain error, first you can AND the *error_code* with one of the error masks, ```CCS811_I2C_ERROR_MASK``` for I2C errors and ```CCS811_DRV_ERROR_MASK``` for other errors. Then you can test the result for a certain error code.
For example, error handling for ```ccs811_get_results``` could look like:
```
if (ccs811_get_results (sensor, &tvoc, &eco2, &raw_i, &raw_v))
{
// no error happened
...
}
else
{
// error happened
switch (sensor->error_code & CCS811_I2C_ERROR_MASK)
{
case CCS811_I2C_BUSY: ...
case CCS811_I2C_READ_FAILED: ...
...
}
switch (sensor->error_code & CCS811_DRV_ERROR_MASK)
{
case CCS811_WRONG_MODE: ...
case CCS811_DRV_NO_IAQ_DATA: ...
...
}
}
```
## Usage
First, the hardware configuration has to be established.
### Hardware configurations
Following figure shows the hardware configuration if no interrupt is used.
```
+------------------------+ +--------+
| ESP8266 Bus 0 | | CCS811 |
| GPIO 5 (SCL) >----> SCL |
| GPIO 4 (SDA) ------ SDA |
| GND -----> /WAKE |
+------------------------+ +--------+
```
If interrupt signal *nINT* is used to fetch new data, additionally the interrupt pin has to be connected to a GPIO pin.
```
+------------------------+ +--------+
| ESP8266 Bus 0 | | CCS811 |
| GPIO 5 (SCL) >----> SCL |
| GPIO 4 (SDA) ------ SDA |
| GPIO 2 <----- /nINT |
| GND -----> /WAKE |
+------------------------+ +--------+
```
If CCS811 sensor is used in conjunction with another sensor, e.g., a SHT3x sensor, the hardware configuration looks like following:
```
+------------------------+ +--------+
| ESP8266 Bus 0 | | CCS811 |
| GPIO 5 (SCL) >--+----> SCL |
| GPIO 4 (SDA) ---|-+--- SDA |
| GND ---|-|--> /WAKE |
| | | | +--------+
| | | | | SHT3x |
| | +----> SCL |
| | +--- SDA |
+------------------------+ +--------+
```
### Communication interface settings
Dependent on the hardware configuration, the communication interface settings have to be defined.
```
// define I2C interfaces at which CCS811 sensors can be connected
#define I2C_BUS 0
#define I2C_SCL_PIN 5
#define I2C_SDA_PIN 4
// define GPIO for interrupt
#define INT_GPIO 2
#include "sht3x/sht3x.h"
```
### Main program
Before using the CCS811 driver, function ```i2c_init``` needs to be called for each I2C interface to setup them.
**Please note:** CCS811 uses clock streching that can be longer than the default I2C clock stretching. Therefore the clock stretching parameter of I2C has to be set to at least ```CCS811_I2C_CLOCK_STRETCH```.
```
...
i2c_init(I2C_BUS, I2C_SCL_PIN, I2C_SDA_PIN, I2C_FREQ_100K));
i2c_set_clock_stretch (I2C_BUS, CCS811_I2C_CLOCK_STRETCH);
...
```
Once I2C interfaces to be used are initialized, function ```ccs811_init_sensor``` has to be called for each CCS811 sensor to initialize the sensor and to check its availability as well as its error state. The parameters specify the I2C bus to which it is connected and its I2C slave address.
```
static ccs811_sensor_t* sensor; // pointer to sensor device data structure
...
if ((sensor = ccs811_init_sensor (I2C_BUS, CCS811_I2C_ADDRESS_1)))
{
...
}
...
```
Function ```ccs811_init_sensor``` returns a pointer to the sensor device data structure or NULL in case of error.
If initialization of the sensor was successful, the sensor mode has be set to start periodic measurement. The sensor mode can be changed anytime later.
```
...
// start periodic measurement with one measurement per second
ccs811_set_mode (sensor, ccs811_mode_1s);
...
```
Last, the user task that uses the sensor has to be created.
```
xTaskCreate(user_task, "user_task", 256, NULL, 2, 0);
```
The user task can use different approaches to fetch new data. Either new data are fetched periodically or the interrupt signal *nINT* is used when new data are available or eCO2 value exceeds defined thresholds.
If new data are fetched **periodically** the implementation of the user task is quite simply and could look like following.
```
void user_task(void *pvParameters)
{
uint16_t tvoc;
uint16_t eco2;
TickType_t last_wakeup = xTaskGetTickCount();
while (1)
{
// get the results and do something with them
if (ccs811_get_results (sensor, &tvoc, &eco2, 0, 0))
...
// passive waiting until 1 second is over
vTaskDelayUntil(&last_wakeup, 1000 / portTICK_PERIOD_MS);
}
}
...
```
The user task simply fetches new data with the same rate as the measurements are performed.
**Please note:** The rate of fetching the measurement results must be not greater than the rate of periodic measurements of the sensor, however, it *should be less* to avoid conflicts caused by the timing tolerance of the sensor.
A different approach is to use the **interrupt** *nINT*. This interrupt signal is either triggered every time when new data are available (INT_DATA_RDY) or only whenever eCO2 value exceeds defined thresholds (INT_THRESHOLD). In both cases, the user has to implement an interrupt handler that either fetches the data directly or triggers a task, that is waiting to fetch the data.
```
...
TaskHandle_t nINT_task;
// Interrupt handler which resumes user_task_interrupt on interrupt
void nINT_handler (uint8_t gpio)
{
xTaskResumeFromISR (nINT_task);
}
// User task that fetches the sensor values.
void user_task_interrupt (void *pvParameters)
{
uint16_t tvoc;
uint16_t eco2;
while (1)
{
// task suspends itself and waits to be resumed by interrupt handler
vTaskSuspend (NULL);
// after resume get the results and do something with them
if (ccs811_get_results (sensor, &tvoc, &eco2, 0, 0))
...
}
}
...
xTaskCreate(user_task_interrupt, "user_task_interrupt", 256, NULL, 2, &nINT_task);
...
```
In this example, a task is defined which suspends itself in each cycle to wait for fetching the data. The task is resumed by the interrupt handler.
Finally, the interrupt handler has to be activated for the GPIO which is connected to the interrupt signal. Furthermore, the interrupt has to be enabled in the CCS811 sensor.
Function ```ccs811_enable_interrupt``` enables the interrupt that is triggered whenever new data are available (INT_DATA_RDY).
```
...
// activate the interrupt for INT_GPIO and set the interrupt handler
gpio_set_interrupt(INT_GPIO, GPIO_INTTYPE_EDGE_NEG, nINT_handler);
// enable the data ready interrupt INT_DATA_RDY
ccs811_enable_interrupt (sensor, true);
...
````
Function ```ccs811_set_eco2_thresholds``` enables the interrupt that is triggered whenever eCO2 value exceeds the thresholds (INT_THRESHOLD) defined by parameters.
```
...
// activate the interrupt for INT_GPIO and set the interrupt handler
gpio_set_interrupt(INT_GPIO, GPIO_INTTYPE_EDGE_NEG, nINT_handler);
// set threshold parameters and enable threshold interrupt mode INT_THRESHOLD
ccs811_set_eco2_thresholds (sensor, 600, 1100, 40);
...
````
## Full Example
```
// use following constants to define the demo mode
// #define INT_DATA_RDY_USED
// #define INT_THRESHOLD_USED
#include "espressif/esp_common.h"
#include "esp/uart.h"
#include "i2c/i2c.h"
#include "FreeRTOS.h"
#include <task.h>
// include CCS811 driver
#include "ccs811/ccs811.h"
// define I2C interfaces at which CCS811 sensors can be connected
#define I2C_BUS 0
#define I2C_SCL_PIN 5
#define I2C_SDA_PIN 4
// define GPIO for interrupt
#define INT_GPIO 2
static ccs811_sensor_t* sensor;
#if defined(INT_DATA_RDY_USED) || defined(INT_THRESHOLD_USED)
/**
* In this example, the interrupt *nINT* is used. It is triggered every time
* new data are available (INT_DATA_RDY_USED) or exceed defined thresholds
* (INT_THRESHOLD_USED). In this case, the user has to define an interrupt
* handler that fetches the data directly or triggers a task, that is waiting
* to fetch the data. In this example, a task is defined which suspends itself
* in each cycle to wait for fetching the data. The task is resumed by the
* the interrupt handler.
*/
TaskHandle_t nINT_task;
// User task that fetches the sensor values.
void user_task_interrupt (void *pvParameters)
{
uint16_t tvoc;
uint16_t eco2;
while (1)
{
// task suspends itself and waits to be resumed by interrupt handler
vTaskSuspend (NULL);
// after resume get the results and do something with them
if (ccs811_get_results (sensor, &tvoc, &eco2, 0, 0))
printf("%.3f CCS811 Sensor interrupt: TVOC %d ppb, eCO2 %d ppm\n",
(double)sdk_system_get_time()*1e-3, tvoc, eco2);
}
}
// Interrupt handler which resumes user_task_interrupt on interrupt
void nINT_handler (uint8_t gpio)
{
xTaskResumeFromISR (nINT_task);
}
#else
/*
* In this example, user task fetches the sensor values every seconds.
*/
void user_task_periodic(void *pvParameters)
{
uint16_t tvoc;
uint16_t eco2;
TickType_t last_wakeup = xTaskGetTickCount();
while (1)
{
// get environmental data from another sensor and set them
// ccs811_set_environmental_data (sensor, 25.3, 47.8);
// get the results and do something with them
if (ccs811_get_results (sensor, &tvoc, &eco2, 0, 0))
printf("%.3f CCS811 Sensor periodic: TVOC %d ppb, eCO2 %d ppm\n",
(double)sdk_system_get_time()*1e-3, tvoc, eco2);
// passive waiting until 1 second is over
vTaskDelayUntil(&last_wakeup, 1000 / portTICK_PERIOD_MS);
}
}
#endif
void user_init(void)
{
// set UART Parameter
uart_set_baud(0, 115200);
// give the UART some time to settle
sdk_os_delay_us(500);
/** -- MANDATORY PART -- */
// init all I2C bus interfaces at which CCS811 sensors are connected
i2c_init(I2C_BUS, I2C_SCL_PIN, I2C_SDA_PIN, I2C_FREQ_100K);
// longer clock stretching is required for CCS811
i2c_set_clock_stretch (I2C_BUS, CCS811_I2C_CLOCK_STRETCH);
// init the sensor with slave address CCS811_I2C_ADDRESS_1 connected I2C_BUS.
sensor = ccs811_init_sensor (I2C_BUS, CCS811_I2C_ADDRESS_1);
if (sensor)
{
#if defined(INT_DATA_RDY_USED) || defined(INT_THRESHOLD_USED)
// create a task that is resumed by interrupt handler to use the sensor
xTaskCreate(user_task_interrupt, "user_task_interrupt", 256, NULL, 2, &nINT_task);
// activate the interrupt for INT_GPIO and set the interrupt handler
gpio_set_interrupt(INT_GPIO, GPIO_INTTYPE_EDGE_NEG, nINT_handler);
#ifdef INT_DATA_RDY_USED
// enable the data ready interrupt
ccs811_enable_interrupt (sensor, true);
#else
// set threshold parameters and enable threshold interrupt mode
ccs811_set_eco2_thresholds (sensor, 600, 1100, 40);
#endif
#else
// create a periodic task that uses the sensor
xTaskCreate(user_task_periodic, "user_task_periodic", 256, NULL, 2, NULL);
#endif
// start periodic measurement with one measurement per second
ccs811_set_mode (sensor, ccs811_mode_1s);
}
}
```
## Further Examples
See also the example in the examples directory [examples directory](../../examples/ccs811/README.md).

636
extras/ccs811/ccs811.c Normal file
View file

@ -0,0 +1,636 @@
/*
* Driver for ams CCS811 digital temperature, humity, pressure and
* gas sensor connected to I2C or SPI
*
* Part of esp-open-rtos [https://github.com/SuperHouse/esp-open-rtos]
*
* ---------------------------------------------------------------------------
*
* The BSD License (3-clause license)
*
* Copyright (c) 2017 Gunar Schorcht (https://github.com/gschorcht]
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#include <string.h>
#include "espressif/esp_common.h"
#include "espressif/sdk_private.h"
#include "FreeRTOS.h"
#include "task.h"
#include "i2c/i2c.h"
#include "ccs811.h"
#if defined(CCS811_DEBUG_LEVEL_2)
#define debug(s, f, ...) printf("%s %s: " s "\n", "CCS811", f, ## __VA_ARGS__)
#define debug_dev(s, f, d, ...) printf("%s %s: bus %d, addr %02x - " s "\n", "CCS811", f, d->bus, d->addr, ## __VA_ARGS__)
#else
#define debug(s, f, ...)
#define debug_dev(s, f, d, ...)
#endif
#if defined(CCS811_DEBUG_LEVEL_1) || defined(CCS811_DEBUG_LEVEL_2)
#define error(s, f, ...) printf("%s %s: " s "\n", "CCS811", f, ## __VA_ARGS__)
#define error_dev(s, f, d, ...) printf("%s %s: bus %d, addr %02x - " s "\n", "CCS811", f, d->bus, d->addr, ## __VA_ARGS__)
#else
#define error(s, f, ...)
#define error_dev(s, f, d, ...)
#endif
/* CCS811 register addresses */
#define CCS811_REG_STATUS 0x00
#define CCS811_REG_MEAS_MODE 0x01
#define CCS811_REG_ALG_RESULT_DATA 0x02
#define CCS811_REG_RAW_DATA 0x03
#define CCS811_REG_ENV_DATA 0x05
#define CCS811_REG_NTC 0x06
#define CCS811_REG_THRESHOLDS 0x10
#define CCS811_REG_BASELINE 0x11
#define CCS811_REG_HW_ID 0x20
#define CCS811_REG_HW_VER 0x21
#define CCS811_REG_FW_BOOT_VER 0x23
#define CCS811_REG_FW_APP_VER 0x24
#define CCS811_REG_ERROR_ID 0xe0
#define CCS811_REG_APP_ERASE 0xf1
#define CCS811_REG_APP_DATA 0xf2
#define CCS811_REG_APP_VERIFY 0xf3
#define CCS811_REG_APP_START 0xf4
#define CCS811_REG_SW_RESET 0xff
// status register bits
#define CCS811_STATUS_ERROR 0x01 // error, details in CCS811_REG_ERROR
#define CCS811_STATUS_DATA_RDY 0x08 // new data sample in ALG_RESULT_DATA
#define CCS811_STATUS_APP_VALID 0x10 // valid application firmware loaded
#define CCS811_STATUS_FW_MODE 0x80 // firmware is in application mode
// error register bits
#define CCS811_ERR_WRITE_REG_INV 0x01 // invalid register address on write
#define CCS811_ERR_READ_REG_INV 0x02 // invalid register address on read
#define CCS811_ERR_MEASMODE_INV 0x04 // invalid requested measurement mode
#define CCS811_ERR_MAX_RESISTANCE 0x08 // maximum sensor resistance exceeded
#define CCS811_ERR_HEATER_FAULT 0x10 // heater current not in range
#define CCS811_ERR_HEATER_SUPPLY 0x20 // heater voltage not applied correctly
/**
* Type declarations
*/
typedef struct
{
uint8_t reserved_1 :2;
uint8_t int_thresh :1; // interrupt if new ALG_RESULT_DAT crosses on of the thresholds
uint8_t int_datardy:1; // interrupt if new sample is ready in ALG_RESULT_DAT
uint8_t drive_mode :3; // mode number binary coded
} ccs811_meas_mode_reg_t;
/**
* forward declaration of functions for internal use only
*/
static bool ccs811_reg_read(ccs811_sensor_t* dev, uint8_t reg, uint8_t *data, uint32_t len);
static bool ccs811_reg_write(ccs811_sensor_t* dev, uint8_t reg, uint8_t *data, uint32_t len);
static bool ccs811_check_error_status (ccs811_sensor_t* dev);
static bool ccs811_enable_threshold (ccs811_sensor_t* dev, bool enabled);
static bool ccs811_is_available (ccs811_sensor_t* dev);
ccs811_sensor_t* ccs811_init_sensor (uint8_t bus, uint8_t addr)
{
ccs811_sensor_t* dev;
if ((dev = malloc (sizeof(ccs811_sensor_t))) == NULL)
return NULL;
// init sensor data structure
dev->bus = bus;
dev->addr = addr;
dev->mode = ccs811_mode_idle;
dev->error_code = CCS811_OK;
// check whether sensor is available including the check of the hardware
// id and the error state
if (!ccs811_is_available(dev))
{
error_dev("Sensor is not available.", __FUNCTION__, dev);
free (dev);
return NULL;
}
const static uint8_t sw_reset[4] = { 0x11, 0xe5, 0x72, 0x8a };
// doing a software reset first
if (!ccs811_reg_write(dev, CCS811_REG_SW_RESET, (uint8_t*)sw_reset, 4))
{
error_dev("Could not reset the sensor.", __FUNCTION__, dev);
free (dev);
return NULL;
}
uint8_t status;
// wait 100 ms after the reset
vTaskDelay(100/portTICK_PERIOD_MS);
// get the status to check whether sensor is in bootloader mode
if (!ccs811_reg_read(dev, CCS811_REG_STATUS, &status, 1))
{
error_dev("Could not read status register %02x.", __FUNCTION__, dev, CCS811_REG_STATUS);
free (dev);
return NULL;
}
// if sensor is in bootloader mode (FW_MODE == 0), it has to switch
// to the application mode first
if (!(status & CCS811_STATUS_FW_MODE))
{
// check whether valid application firmware is loaded
if (!(status & CCS811_STATUS_APP_VALID))
{
error_dev("Sensor is in boot mode, but has no valid application.",
__FUNCTION__, dev);
free (dev);
return NULL;
}
// swtich to application mode
if (!ccs811_reg_write(dev, CCS811_REG_APP_START, 0, 0))
{
error_dev("Could not start application", __FUNCTION__, dev);
free (dev);
return NULL;
}
// wait 100 ms after starting the app
vTaskDelay(100/portTICK_PERIOD_MS);
// get the status to check whether sensor switched to application mode
if (!ccs811_reg_read(dev, CCS811_REG_STATUS, &status, 1) ||
!(status & CCS811_STATUS_FW_MODE))
{
error_dev("Could not start application.", __FUNCTION__, dev);
free (dev);
return NULL;
}
}
// try to set default measurement mode to *ccs811_mode_1s*
if (!ccs811_set_mode(dev, ccs811_mode_1s))
{
free (dev);
return NULL;
}
return dev;
}
bool ccs811_set_mode (ccs811_sensor_t* dev, ccs811_mode_t mode)
{
ccs811_meas_mode_reg_t reg;
if (!dev) return false;
dev->error_code = CCS811_OK;
// read measurement mode register value
if (!ccs811_reg_read(dev, CCS811_REG_MEAS_MODE, (uint8_t*)&reg, 1))
return false;
reg.drive_mode = mode;
// write back measurement mode register
if (!ccs811_reg_write(dev, CCS811_REG_MEAS_MODE, (uint8_t*)&reg, 1))
{
error_dev ("Could not set measurement mode.", __FUNCTION__, dev);
return false;
}
// check whether setting measurement mode were succesfull
if (!ccs811_reg_read(dev, CCS811_REG_MEAS_MODE, (uint8_t*)&reg, 1) ||
!reg.drive_mode == mode)
{
error_dev ("Could not set measurement mode to %d", __FUNCTION__, dev, mode);
return ccs811_check_error_status (dev);
}
dev->mode = mode;
return true;
}
#define CCS811_ALG_DATA_ECO2_HB 0
#define CCS811_ALG_DATA_ECO2_LB 1
#define CCS811_ALG_DATA_TVOC_HB 2
#define CCS811_ALG_DATA_TVOC_LB 3
#define CCS811_ALG_DATA_STATUS 4
#define CCS811_ALG_DATA_ERROR_ID 5
#define CCS811_ALG_DATA_RAW_HB 6
#define CCS811_ALG_DATA_RAW_LB 7
bool ccs811_get_results (ccs811_sensor_t* dev,
uint16_t* iaq_tvoc,
uint16_t* iaq_eco2,
uint8_t* raw_i,
uint16_t* raw_v)
{
if (!dev) return false;
dev->error_code = CCS811_OK;
if (dev->mode == ccs811_mode_idle)
{
error_dev ("Sensor is in idle mode and not performing measurements.",
__FUNCTION__, dev);
dev->error_code = CCS811_DRV_WRONG_MODE;
return false;
}
if (dev->mode == ccs811_mode_250ms && (iaq_tvoc || iaq_eco2))
{
error_dev ("Sensor is in constant power mode, only raw data "
"are available every 250ms",
__FUNCTION__, dev);
dev->error_code = CCS811_DRV_NO_IAQ_DATA;
return false;
}
uint8_t data[8];
// read IAQ sensor values and RAW sensor data including status and error id
if (!ccs811_reg_read(dev, CCS811_REG_ALG_RESULT_DATA, data, 8))
{
error_dev ("Could not read sensor data.", __FUNCTION__, dev);
dev->error_code |= CCS811_DRV_RD_DATA_FAILED;
return false;
}
// check for errors
if (data[CCS811_ALG_DATA_STATUS] & CCS811_STATUS_ERROR)
{
return ccs811_check_error_status (dev);
}
// check whether new data are ready, if not, latest values are read from sensor
// and error_code is set
if (!(data[CCS811_ALG_DATA_STATUS] & CCS811_STATUS_DATA_RDY))
{
debug_dev ("No new data.", __FUNCTION__, dev);
dev->error_code = CCS811_DRV_NO_NEW_DATA;
}
// if *iaq* is not NULL return IAQ sensor values
if (iaq_tvoc) *iaq_tvoc = data[CCS811_ALG_DATA_TVOC_HB] << 8 | data[CCS811_ALG_DATA_TVOC_LB];
if (iaq_eco2) *iaq_eco2 = data[CCS811_ALG_DATA_ECO2_HB] << 8 | data[CCS811_ALG_DATA_ECO2_LB];
// if *raw* is not NULL return RAW sensor data
if (raw_i) *raw_i = data[CCS811_ALG_DATA_RAW_HB] >> 2;
if (raw_v) *raw_v = (data[CCS811_ALG_DATA_RAW_HB] & 0x03) << 8 | data[CCS811_ALG_DATA_RAW_LB];
return true;
}
uint32_t ccs811_get_ntc_resistance (ccs811_sensor_t* dev, uint32_t r_ref)
{
if (!dev) return 0;
uint8_t data[4];
// read baseline register
if (!ccs811_reg_read(dev, CCS811_REG_NTC, data, 4))
return 0;
// calculation from application note ams AN000372
uint16_t v_ref = (uint16_t)(data[0]) << 8 | data[1];
uint16_t v_ntc = (uint16_t)(data[2]) << 8 | data[3];
return (v_ntc * r_ref / v_ref);
}
bool ccs811_set_environmental_data (ccs811_sensor_t* dev,
float temperature,
float humidity)
{
if (!dev) return false;
uint16_t temp = (temperature + 25) * 512; // -25 °C maps to 0
uint16_t hum = humidity * 512;
// fill environmental data
uint8_t data[4] = { temp >> 8, temp & 0xff,
hum >> 8, hum & 0xff };
// send environmental data to the sensor
if (!ccs811_reg_write(dev, CCS811_REG_ENV_DATA, data, 4))
{
error_dev ("Could not write environmental data to sensor.", __FUNCTION__, dev);
return false;
}
return true;
}
bool ccs811_set_eco2_thresholds (ccs811_sensor_t* dev,
uint16_t low, uint16_t high, uint8_t hysteresis)
{
if (!dev) return false;
dev->error_code = CCS811_OK;
// check whether interrupt has to be disabled
if (!low && !high && !hysteresis)
return ccs811_enable_threshold (dev, false);
// check parameters
if (low < CCS_ECO2_RANGE_MIN || high > CCS_ECO2_RANGE_MAX || low > high || !hysteresis)
{
error_dev ("Wrong threshold parameters", __FUNCTION__, dev);
dev->error_code = CCS811_DRV_WRONG_PARAMS;
return ccs811_enable_threshold (dev, false);
}
// fill the threshold data
uint8_t data[5] = { low >> 8, low & 0xff,
high >> 8, high & 0xff,
hysteresis };
// write threshold data to the sensor
if (!ccs811_reg_write(dev, CCS811_REG_THRESHOLDS, data, 5))
{
error_dev ("Could not write threshold interrupt data to sensor.", __FUNCTION__, dev);
return ccs811_enable_threshold (dev, false);
}
// finally enable the threshold interrupt mode
return ccs811_enable_threshold (dev, true);
}
bool ccs811_enable_interrupt (ccs811_sensor_t* dev, bool enabled)
{
if (!dev) return false;
ccs811_meas_mode_reg_t reg;
// read measurement mode register value
if (!ccs811_reg_read(dev, CCS811_REG_MEAS_MODE, (uint8_t*)&reg, 1))
return false;
reg.int_datardy = enabled;
reg.int_thresh = false; // threshold mode must not enabled
// write back measurement mode register
if (!ccs811_reg_write(dev, CCS811_REG_MEAS_MODE, (uint8_t*)&reg, 1))
{
error_dev ("Could not set measurment mode register.", __FUNCTION__, dev);
return false;
}
return true;
}
uint16_t ccs811_get_baseline (ccs811_sensor_t* dev)
{
if (!dev) return 0;
uint8_t data[2];
// read baseline register
if (!ccs811_reg_read(dev, CCS811_REG_BASELINE, data, 2))
return 0;
return (uint16_t)(data[0]) << 8 | data[1];
}
bool ccs811_set_baseline (ccs811_sensor_t* dev, uint16_t baseline)
{
if (!dev) return false;
uint8_t data[2] = { baseline >> 8, baseline & 0xff };
// write baseline register
if (!ccs811_reg_write(dev, CCS811_REG_BASELINE, data, 2))
return false;
return true;
}
/**
* FUNCTIONS FOR INTERNAL USE ONLY
*/
static bool ccs811_enable_threshold (ccs811_sensor_t* dev, bool enabled)
{
if (!dev) return false;
ccs811_meas_mode_reg_t reg;
// first, enable/disable the data ready interrupt
if (!ccs811_enable_interrupt (dev, enabled))
return false;
// read measurement mode register value
if (!ccs811_reg_read(dev, CCS811_REG_MEAS_MODE, (uint8_t*)&reg, 1))
return false;
// second, enable/disable the threshold interrupt mode
reg.int_thresh = enabled;
// write back measurement mode register
if (!ccs811_reg_write(dev, CCS811_REG_MEAS_MODE, (uint8_t*)&reg, 1))
{
error_dev ("Could not set measurement mode register.", __FUNCTION__, dev);
return false;
}
return true;
}
static bool ccs811_reg_read(ccs811_sensor_t* dev, uint8_t reg, uint8_t *data, uint32_t len)
{
if (!dev || !data) return false;
debug_dev ("Read %d byte from i2c slave starting at reg addr %02x.", __FUNCTION__, dev, len, reg);
int result = i2c_slave_read(dev->bus, dev->addr, &reg, data, len);
if (result)
{
dev->error_code |= (result == -EBUSY) ? CCS811_I2C_BUSY : CCS811_I2C_READ_FAILED;
error_dev ("Error %d on read %d byte from I2C slave reg addr %02x.",
__FUNCTION__, dev, result, len, reg);
return false;
}
# ifdef CCS811_DEBUG_LEVEL_2
printf("CCS811 %s: Read following bytes: ", __FUNCTION__);
printf("%0x: ", reg);
for (int i=0; i < len; i++)
printf("%0x ", data[i]);
printf("\n");
# endif
return true;
}
static bool ccs811_reg_write(ccs811_sensor_t* dev, uint8_t reg, uint8_t *data, uint32_t len)
{
if (!dev) return false;
debug_dev ("Write %d bytes to i2c slave starting at reg addr %02x", __FUNCTION__, dev, len, reg);
# ifdef CCS811_DEBUG_LEVEL_2
if (data && len)
{
printf("CCS811 %s: Write following bytes starting at reg addr %02x: ", __FUNCTION__, reg);
for (int i=0; i < len; i++)
printf("%02x ", data[i]);
printf("\n");
}
# endif
int result = i2c_slave_write(dev->bus, dev->addr, &reg, data, len);
if (result)
{
dev->error_code |= (result == -EBUSY) ? CCS811_I2C_BUSY : CCS811_I2C_WRITE_FAILED;
error_dev ("Error %d on write %d byte to i2c slave register %02x.",
__FUNCTION__, dev, result, len, reg);
return false;
}
return true;
}
static bool ccs811_check_error_status (ccs811_sensor_t* dev)
{
if (!dev) return false;
dev->error_code = CCS811_OK;
uint8_t status;
uint8_t err_reg;
// check status register
if (!ccs811_reg_read(dev, CCS811_REG_STATUS, &status, 1))
return false;
if (!status & CCS811_STATUS_ERROR)
// everything is fine
return true;
// Check the error register
if (!ccs811_reg_read(dev, CCS811_REG_ERROR_ID, &err_reg, 1))
return false;
if (err_reg & CCS811_ERR_WRITE_REG_INV)
{
error_dev ("Received an invalid register for write.", __FUNCTION__, dev);
dev->error_code = CCS811_DRV_WR_REG_INV;
return false;
}
if (err_reg & CCS811_ERR_READ_REG_INV)
{
error_dev ("Received an invalid register for read.", __FUNCTION__, dev);
dev->error_code = CCS811_DRV_RD_REG_INV;
return false;
}
if (err_reg & CCS811_ERR_MEASMODE_INV)
{
error_dev ("Received an invalid measurement mode request.", __FUNCTION__, dev);
dev->error_code = CCS811_DRV_MM_INV;
return false;
}
if (err_reg & CCS811_ERR_MAX_RESISTANCE)
{
error_dev ("Sensor resistance measurement has reached"
" or exceeded the maximum range.", __FUNCTION__, dev);
dev->error_code = CCS811_DRV_MAX_RESIST;
return false;
}
if (err_reg & CCS811_ERR_HEATER_FAULT)
{
error_dev ("Heater current not in range.", __FUNCTION__, dev);
dev->error_code = CCS811_DRV_HEAT_FAULT;
return false;
}
if (err_reg & CCS811_ERR_HEATER_SUPPLY)
{
error_dev ("Heater voltage is not being applied correctly.", __FUNCTION__, dev);
dev->error_code = CCS811_DRV_HEAT_SUPPLY;
return false;
}
return true;
}
static bool ccs811_is_available (ccs811_sensor_t* dev)
{
if (!dev) return false;
uint8_t reg_data[5];
// check hardware id (register 0x20) and hardware version (register 0x21)
if (!ccs811_reg_read(dev, CCS811_REG_HW_ID, reg_data, 5))
return false;
if (reg_data[0] != 0x81)
{
error_dev ("Wrong hardware ID %02x, should be 0x81", __FUNCTION__, dev, reg_data[0]);
dev->error_code = CCS811_DRV_HW_ID;
return false;
}
debug_dev ("hardware version: %02x", __FUNCTION__, dev, reg_data[1]);
debug_dev ("firmware boot version: %02x", __FUNCTION__, dev, reg_data[3]);
debug_dev ("firmware app version: %02x", __FUNCTION__, dev, reg_data[4]);
return ccs811_check_error_status (dev);
}

331
extras/ccs811/ccs811.h Normal file
View file

@ -0,0 +1,331 @@
/*
* Driver for AMS CCS811 digital gas sensor connected to I2C
*
* Part of esp-open-rtos [https://github.com/SuperHouse/esp-open-rtos]
*
* ---------------------------------------------------------------------------
*
* The BSD License (3-clause license)
*
* Copyright (c) 2017 Gunar Schorcht (https://github.com/gschorcht]
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its
* contributors may be used to endorse or promote products derived from this
* software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef CCS811_DRV_H_
#define CCS811_DRV_H_
#include "stdint.h"
#include "stdbool.h"
// Uncomment one of the following defines to enable debug output
// #define CCS811_DEBUG_LEVEL_1 // only error messages
// #define CCS811_DEBUG_LEVEL_2 // debug and error messages
// CCS811 I2C addresses
#define CCS811_I2C_ADDRESS_1 0x5A // default
#define CCS811_I2C_ADDRESS_2 0x5B
// CCS811 clock streching counter
#define CCS811_I2C_CLOCK_STRETCH 200
// Definition of error codes
#define CCS811_OK 0
#define CCS811_NOK -1
#define CCS811_INT_ERROR_MASK 0x000f
#define CCS811_DRV_ERROR_MASK 0xfff0
// Error codes for the I2C interface ORed with CCS811 driver error codes
#define CCS811_I2C_READ_FAILED 1
#define CCS811_I2C_WRITE_FAILED 2
#define CCS811_I2C_BUSY 3
// CCS811 driver error codes ORed with error codes for I2C the interface
#define CCS811_DRV_BOOT_MODE (1 << 8) // firmware is in boot mode
#define CCS811_DRV_NO_APP (2 << 8) // no application firmware loaded
#define CCS811_DRV_NO_NEW_DATA (3 << 8) // no new data samples are ready
#define CCS811_DRV_NO_IAQ_DATA (4 << 8) // no new data samples are ready
#define CCS811_DRV_HW_ID (5 << 8) // wrong hardware ID
#define CCS811_DRV_INV_SENS (6 << 8) // invalid sensor ID
#define CCS811_DRV_WR_REG_INV (7 << 8) // invalid register addr on write
#define CCS811_DRV_RD_REG_INV (8 << 8) // invalid register addr on read
#define CCS811_DRV_MM_INV (9 << 8) // invalid measurement mode
#define CCS811_DRV_MAX_RESIST (10 << 8) // max sensor resistance reached
#define CCS811_DRV_HEAT_FAULT (11 << 8) // heater current not in range
#define CCS811_DRV_HEAT_SUPPLY (12 << 8) // heater voltage not correct
#define CCS811_DRV_WRONG_MODE (13 << 8) // wrong measurement mode
#define CCS811_DRV_RD_STAT_FAILED (14 << 8) // read status register failed
#define CCS811_DRV_RD_DATA_FAILED (15 << 8) // read sensor data failed
#define CCS811_DRV_APP_START_FAIL (16 << 8) // sensor app start failure
#define CCS811_DRV_WRONG_PARAMS (17 << 8) // wrong parameters used
// ranges
#define CCS_ECO2_RANGE_MIN 400
#define CCS_ECO2_RANGE_MAX 8192
#define CCS_TVOC_RANGE_MIN 0
#define CCS_TVOC_RANGE_MAX 1187
#ifdef __cplusplus
extern "C"
{
#endif
/**
* @brief CCS811 operation modes
*/
typedef enum {
ccs811_mode_idle = 0, // Idle, low current mode
ccs811_mode_1s = 1, // Constant Power mode, IAQ values every 1 s
ccs811_mode_10s = 2, // Pulse Heating mode, IAQ values every 10 s
ccs811_mode_60s = 3, // Low Power Pulse Heating, IAQ values every 60 s
ccs811_mode_250ms = 4 // Constant Power mode, RAW data every 250 ms
} ccs811_mode_t;
/**
* @brief CCS811 sensor device data structure
*/
typedef struct {
int error_code; // contains the error code of last operation
uint8_t bus; // I2C bus
uint8_t addr; // I2C slave address
ccs811_mode_t mode; // operation mode
} ccs811_sensor_t;
/**
* @brief Initialize a CCS811 sensor
*
* The function initializes the CCS811 sensor and checks its availability.
*
* @param bus I2C bus at which CCS811 sensor is connected
* @param addr I2C slave address of the CCS811 sensor
*
* @return pointer to sensor data structure, or NULL on error
*/
ccs811_sensor_t* ccs811_init_sensor (uint8_t bus, uint8_t addr);
/**
* @brief Set the operation mode of the sensor
*
* The function sets the operating mode of the sensor. If the parameter
* *mode* is either *ccs811_mode_1s*, *ccs811_mode_10s*, *ccs811_mode_60s*
* or *ccs811_mode_250ms*, the sensor starts a periodic measurement with
* the specified period. Function *ccs811_get_results* can then be used at
* the same rate to get the results.
*
* In *ccs811_mode_1s*, *ccs811_mode_10s* and *ccs811_mode_60s*, raw sensor
* data as well as IAQ values calculated by the sensor values are available.
* In *ccs811_mode_250ms*, only raw data are available.
*
* In case, parameter mode is *ccs811_mode_idle*, the sensor does not perform
* any measurements.
*
* Please note: Mode timings are subject to typical 2% tolerance due
* to accuracy of internal sensor clock.
*
* Please note: After setting the sensor mode, the sensor needs up to
* 20 minutes, before accurate readings are generated.
*
* Please note: When a sensor operating mode is changed to a new mode with
* a lower sample rate, e.g., from *ccs811_mode_60s* to *ccs811_mode_1s, it
* should be placed in *mode_idle* for at least 10 minutes before enabling
* the new mode.
*
* @param sensor pointer to the sensor device data structure
* @param period measurement period in ms (default 1000 ms)
*
* @return true on success, false on error
*/
bool ccs811_set_mode (ccs811_sensor_t* sensor, ccs811_mode_t mode);
/**
* @brief Get latest IAQ sensor values and/or RAW sensor data
*
* The function reads the IAQ sensor values (TVOC and eCO2) and/or the raw
* sensor data. If some of the results are not needed, the corresponding
* pointer parameters can be set to NULL.
*
* Please note: If the function is called and no new data are available,
* e.g., due to the sensor mode time tolerance of 2%, the function still
* returns successfully. In this case, the results of the last measurement
* are returned and the error code CCS811_DRV_NO_NEW_DATA is set.
*
* Please note: In *ccs811_mode_250ms*, only RAW data are available. In
* that case, the function fails with error_code CCS811_DRV_NO_IAQ_DATA
* if parameters *iaq_tvoc* and *iaq_eco2* are not NULL.
* @param sensor pointer to the sensor device data structure
* @param iaq_tvoc TVOC total volatile organic compound (0 - 1187 ppb)
* @param iaq_eco2 eCO2 equivalent CO2 (400 - 8192 ppm)
* @param raw_i current through the sensor used for measuring (0 - 63 uA)
* @param raw_v voltage across the sensor measured (0 - 1023 = 1.65 V)
*
* @return true on success, false on error
*/
bool ccs811_get_results (ccs811_sensor_t* sensor,
uint16_t* iaq_tvoc,
uint16_t* iaq_eco2,
uint8_t* raw_i,
uint16_t* raw_v);
/**
* brief Get the resistance of connected NTC thermistor
*
* CCS811 supports an external interface for connecting a negative thermal
* coefficient thermistor (R_NTC) to provide a cost effective and power
* efficient means of calculating the local ambient temperature. The sensor
* measures the voltage V_NTC across the R_NTC as well as the voltage V_REF
* across a connected reference resistor (R_REF).
*
* The function returns the current resistance of R_NTC using the equation
*
* R_NTC = R_REF / V_REF * V_NTC
*
* Using the data sheet of the NTC, the ambient temperature can be calculated.
*
* @param sensor pointer to the sensor device data structure
* @param reference resistance of R_REF in Ohm
* @return resistance of R_NTC in Ohm, or 0 on error
*/
uint32_t ccs811_get_ntc_resistance (ccs811_sensor_t* sensor, uint32_t r_ref);
/*
* @brief Set environmental data
*
* If information about the environment are available from another sensor,
* they can be used by CCS811 to compensate gas readings due to
* temperature and humidity changes.
*
* @param sensor pointer to the sensor device data structure
* @param temperature measured temperature in degree Celsius
* @param humidity measured relative humidity in percent
*
* @return true on success, false on error
*/
bool ccs811_set_environmental_data (ccs811_sensor_t* sensor,
float temperature, float humidity);
/**
* @brief Enable or disable data ready interrupt signal *nINT*
*
* At the end of each measurement cycle (250ms, 1s, 10s, 60s), CCS811 can
* optionally trigger an interrupt. The signal *nINT* is driven low as soon
* as new sensor values are ready to read. It will stop being driven low
* when sensor data are read with function *ccs811_get_results*.
*
* The interrupt is disabled by default.
*
* @param sensor pointer to the sensor device data structure
* @param enabled if true, the interrupt is enabled, or disabled otherwise
*
* @return true on success, false on error
*/
bool ccs811_enable_interrupt (ccs811_sensor_t* sensor, bool enabled);
/*
* @brief Set eCO2 threshold mode for data ready interrupts
*
* The user task can choose that the data ready interrupt is not generated
* every time when new sensor values become ready but only if the eCO2 value
* moves from the current range (LOW, MEDIUM, or HIGH) into another range by
* more than a hysteresis value. Hysteresis is used to prevent multiple
* interrupts close to a threshold.
*
* LOW below parameter value *low*
* MEDIUM between parameter values *low* and *high*
* HIGH above parameter value *high* is range HIGH.
*
* If all parameters have valid values, the function sets the thresholds and
* enables the data ready interrupt. Using 0 for all parameters disables the
* interrupt.
*
* The interrupt is disabled by default.
*
* @param sensor pointer to the sensor device data structure
* @param low threshold LOW to MEDIUM (> 400, default 1500)
* @param high threshold MEDIUM to HIGH (< 8192, default 2500)
* @param hysteresis hysteresis value (default 50)
*
* @return true on success, false on error
*/
bool ccs811_set_eco2_thresholds (ccs811_sensor_t* sensor,
uint16_t low,
uint16_t high,
uint8_t hysteresis);
/*
* @brief Get the current baseline value from sensor
*
* The sensor supports automatic baseline correction over a minimum time of
* 24 hours. Using this function, the current baseline value can be saved
* before the sensor is powered down. This baseline can then be restored after
* sensor is powered up again to continue the automatic baseline process.
*
* @param sensor pointer to the sensor device data structure
* @return current baseline value on success, or 0 on error
*/
uint16_t ccs811_get_baseline (ccs811_sensor_t* sensor);
/*
* @brief Write a previously stored baseline value to the sensor
*
* The sensor supports automatic baseline correction over a minimum time of
* 24 hours. Using this function, a previously saved baseline value be
* restored after the sensor is powered up to continue the automatic baseline
* process.
*
* Please note: The baseline must be written after the conditioning period
* of 20 min after power up.
*
* @param sensor pointer to the sensor device data structure
* @param basline baseline to be set
* @return true on success, false on error
*/
bool ccs811_set_baseline (ccs811_sensor_t* sensor, uint16_t baseline);
#ifdef __cplusplus
}
#endif /* End of CPP guard */
#endif /* CCS811_DRV_H_ */

View file

@ -0,0 +1,9 @@
# Component makefile for extras/ccs811
# expected anyone using ccs811 driver includes it as 'ccs811/ccs811.h'
INC_DIRS += $(ccs811_ROOT)..
# args for passing into compile rule generation
ccs811_SRC_DIR = $(ccs811_ROOT)
$(eval $(call component_compile_rules,ccs811))