CCS811 Indoor Air Quality Sensor Driver in Rust

   

We spend an enormous amount of time indoors. The indoor air quality is often overlooked but it is actually an important factor in our health, comfort and even productivity.

There are lots of things that contribute to the degradation of the indoor air quality over time. Some of them are trivial to guess like breathing, chimneys, second-hand tobacco smoke, mold, etc. There are others that you may not have heard of like volatile organic compounds (VOC).
Remember that smell new things have? well sorry but that can give you cancer.

You can build your own indoor air quality monitor with an AMS/ScioSense CCS811 sensor and some Rust using the driver I wrote.

The device

The CCS811 is an ultra-low power digital gas sensor solution which integrates a metal oxide (MOX) gas sensor to detect a wide range of Volatile Organic Compounds (VOCs) for indoor air quality monitoring with a microcontroller unit (MCU), which includes an Analog-to-Digital converter (ADC), and an I²C interface.

CCS811 supports intelligent algorithms to process raw sensor measurements to output equivalent total VOC (eTVOC) and equivalent CO2 (eCO2) values, where the main cause of VOCs is from humans.

CCS811 supports multiple measurement modes that have been optimized for low-power consumption during an active sensor measurement and idle mode extending battery life in portable applications.

Firmware update

Depending on where you bought your device, it might be that the firmware application version is too old. I have observed the older version hangs or returns errors quite often.

You can update the firmware application with a Raspberry Pi (it does not matter which one).
First wire the device like this:

1
2
3
4
5
6
7
RPi   <-> CCS811
GND <-> GND
3.3V <-> VCC
Pin 5 <-> SCL
Pin 3 <-> SDA
GND <-> nWAKE
3.3V <-> RST

Next inside your Raspberry Pi download the driver repository somewhere, then run the flashing program without arguments to print the current firmware version:

1
2
3
git clone https://github.com/eldruin/embedded-ccs811-rs
cd embedded-ccs811-rs
cargo run --example flash-firmware

You will see something like this:

1
2
3
4
Hardware ID: 129, hardware version: (1, 2)
Firmware boot version: (1, 0, 0)
Firmware application version: (1, 1, 0)
Has valid firmware application: true

And then an error because no firmware file was provided.

If the firmware application is smaller than (2, 0, 0), you can update it as follows.

Download the new version of the firmware application from here. Then place it inside the embedded-ccs811-rs folder and call the flashing program again now providing the path to the new firmware file:

1
cargo run --example flash-firmware CCS811_SW000246_1-00.bin

You should see an output similar to this:

1
2
3
4
5
6
7
8
9
10
11
Hardware ID: 129, hardware version: (1, 2)
Firmware boot version: (1, 0, 0)
Firmware application version: (1, 1, 0)
Has valid firmware application: true
Starting update process: Reset, erase, download, verify...
Update was successful!
Status:
Hardware ID: 129, hardware version: (1, 2)
Firmware boot version: (1, 0, 0)
Firmware application version: (2, 0, 0)
Has valid firmware application: true

Done!

Using the driver

To use the device from Rust, you have to add the embedded-ccs811 crate to your project as well as a concrete implementation of the embedded-hal traits. For example if you are using the Raspberry Pi running Linux:

1
2
3
4
5
6
# Cargo.toml
...
[dependencies]
embedded-ccs811 = "0.2"
linux-embedded-hal = "0.3"
nb = "1"

Here is an example program which will start the application and print the measurements (source):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
use embedded_ccs811::{prelude::*, Ccs811Awake, MeasurementMode, ModeChangeError, SlaveAddr};
use linux_embedded_hal::I2cdev;
use nb::block;

fn main() {
let dev = I2cdev::new("/dev/i2c-1").unwrap();
let address = SlaveAddr::default();
let sensor = Ccs811Awake::new(dev, address);
match sensor.start_application() {
Err(ModeChangeError { dev: _, error }) => {
println!("Error during application start: {:?}", error);
}
Ok(mut sensor) => {
sensor.set_mode(MeasurementMode::ConstantPower1s).unwrap();
loop {
let data = block!(sensor.data()).unwrap();
println!("eCO2: {}, eTVOC: {}", data.eco2, data.etvoc);
}
}
}
}

Furthermore, this device must be provided with the ambient temperature and humidity in order to compensate the readings. You can see an example doing this here.

I also created a bare-metal example program that runs on the STM32F1 “blue-pill” board which continuously reads the measurement and prints it on an OLED display. You can find the source code of that program here.

In the driver-examples repository you can find further bare-metal examples which you can adapt to do other things with this device.

Some measurements

Measurements in a closed room with one person

I bought my device on AliExpress and sadly it does not seem to make accurate readings. For example if you see these readings of the device in a closed room with one person. It seems the measurements match the humidity, rather than the CO2 concentration. I made other measurements with an iAQ-Core-C sensor I got from MOUSER and these did resemble what one would expect. I will publish those here soon.

Possibly my CCS811 is simply worn off. If you are interested in doing something useful with this device I would rather recommend buying it from a reputable retailer like Adafruit, Sparkfun, or so.

Raspberry Pi configuration

This device uses clock stretching which leads to communication problems with the Raspberry Pi.
For the firmware update it worked repeatedly fine for me but for measurement reading you may encounter errors like this:

1
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Device(DeviceErrors { invalid_register_write: true, invalid_register_read: true, invalid_measurement: true, max_resistance: true, heater_fault: true, heater_supply: true })', src/libcore/result.rs:1165:5

You may also see this:

1
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: I2C(Nix(Sys(EREMOTEIO)))', src/libcore/result.rs:1165:5

A trick for the Raspberry Pi is to reduce the I2C bus speed.
You can do that by editing the file /boot/config.txt:

1
2
dtparam=i2c_arm=on # Enable I2C
dtparam=i2c_arm_baudrate=10000 # 10kHz speed

Then reboot your Raspberry Pi. Done!

Even when correctly configured the errors can happen on occasion. See here for more info.

Where to go from here?

There is more information and example programs in the crate documentation.
If you encounter any issues, please report them in the issue tracker.
Feedback, suggestions and improvements are gladly welcome.

What’s next?

I also have a pretty finished driver for a similar device: iAQ-Core-C/P air quality sensor. I took some measurements which looked much better. I will announce it here soon.

Otherwise I have been writing many other platform-agnostic Rust drivers although I am slow to finish them up and announce them here.

To see what I am currently working on you can follow me on github.

Thanks for reading and stay tuned!

Links: Source code - Crate - Documentation

Share