ccs811: new device driver (#385)

The ccs811 is a volatile Organic Compounds sensor.
pull/1/head
DeziderMesko 7 years ago committed by M-A
parent afa0c2bedd
commit c52d26920b

@ -0,0 +1,423 @@
// Copyright 2019 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
package ccs811
import (
"fmt"
"periph.io/x/periph/conn/physic"
"periph.io/x/periph/conn"
"periph.io/x/periph/conn/i2c"
)
// MeasurementMode represents different ways how data is read
type MeasurementMode byte
// Different measurement mode constants:
//
// - Mode 0: Idle, low current mode.
//
// - Mode 1: Constant power mode, IAQ measurement every second.
//
// - Mode 2: Pulse heating mode IAQ measurement every 10 seconds.
//
// - Mode 3: Low power pulse heating mode IAQ measurement every 60 seconds.
//
// - Mode 4: Constant power mode, sensor measurement every 250ms.
const (
MeasurementModeIdle MeasurementMode = 0
MeasurementModeConstant1000 MeasurementMode = 1
MeasurementModePulse MeasurementMode = 2
MeasurementModeLowPower MeasurementMode = 3
MeasurementModeConstant250 MeasurementMode = 4
)
// NeededData represents set of data read from the sensor.
type NeededData byte
// What data should be read from the sensor.
const (
ReadCO2 NeededData = 2
ReadCO2VOC NeededData = 4
ReadCO2VOCStatus NeededData = 5
ReadAll NeededData = 8
)
// SensorErrorID represents error reported by the sensor.
type SensorErrorID byte
// Error constants, applicable if status registers signals error.
const (
//The CCS811 received an I2C write request addressed to this station but with invalid register address ID.
writeRegInvalid SensorErrorID = 0x1
//The CCS811 received an I2C read request to a mailbox ID that is invalid.
readRegInvalid SensorErrorID = 0x2
//The CCS811 received an I2C request to write an unsupported mode to MEAS_MODE.
measModeInvalid SensorErrorID = 0x4
//The sensor resistance measurement has reached or exceeded the maximum range.
maxResistance SensorErrorID = 0x8
//The Heater current in the CCS811 is not in range.
heaterFault SensorErrorID = 0x10
//The Heater voltage is not being applied correctly.
heaterSupply SensorErrorID = 0x20
)
func (d *Dev) errorCodeToError(errorCode SensorErrorID) error {
if errorCode == 0 {
return nil
}
errorText := ""
switch errorCode {
case writeRegInvalid:
errorText = "WRITE_REG_INVALID: The CCS811 received an I2C write request addressed to this station but with invalid register address ID."
case readRegInvalid:
errorText = "READ_REG_INVALID: The CCS811 received an I2C read request to a mailbox ID that is invalid."
case measModeInvalid:
errorText = "MEASMODE_INVALID: The CCS811 received an I2C request to write an unsupported mode to MEAS_MODE."
case maxResistance:
errorText = "MAX_RESISTANCE: The sensor resistance measurement has reached or exceeded the maximum range."
case heaterFault:
errorText = "HEATER_FAULT: The Heater current in the CCS811 is not in range."
case heaterSupply:
errorText = "HEATER_SUPPLY: The Heater voltage is not being applied correctly."
default:
errorText = fmt.Sprintf("Uknwown error, code: %d", errorCode)
}
return fmt.Errorf("Sensor error: %s", errorText)
}
// Opts holds the configuration options. The address must be 0x5A or 0x5B.
type Opts struct {
Addr uint16
MeasurementMode MeasurementMode
InterruptWhenReady bool
UseThreshold bool
}
// DefaultOpts are the safe default options.
var DefaultOpts = Opts{
Addr: 0x5A,
MeasurementMode: MeasurementModeConstant1000,
InterruptWhenReady: false,
UseThreshold: false,
}
// New creates a new driver for CCS811 VOC sensor.
func New(bus i2c.Bus, opts *Opts) (*Dev, error) {
if opts.Addr != 0x5A && opts.Addr != 0x5B {
return nil, fmt.Errorf("Invalid device address, only 0x5A or 0x5B are allowed")
}
if opts.MeasurementMode > MeasurementModeConstant250 {
return nil, fmt.Errorf("Invalid measurement mode")
}
dev := &Dev{
c: &i2c.Dev{Bus: bus, Addr: opts.Addr},
opts: *opts,
}
// From boot mode to measurement mode.
if err := dev.StartSensorApp(); err != nil {
return nil, fmt.Errorf("Error transitioning from boot do app mode: %v", err)
}
mmp := &MeasurementModeParams{MeasurementMode: opts.MeasurementMode,
GenerateInterrupt: opts.InterruptWhenReady,
UseThreshold: opts.UseThreshold}
if err := dev.SetMeasurementModeRegister(*mmp); err != nil {
return nil, fmt.Errorf("Error setting measurement mode: %v", err)
}
return dev, nil
}
// Dev is an handle to an CCS811 sensor.
type Dev struct {
c conn.Conn
opts Opts
}
const ( //Sensor's registers.
statusReg byte = 0x00
measurementModeReg byte = 0x01
algoResultsReg byte = 0x02
rawDataReg byte = 0x03
environmentReg byte = 0x05
baselineReg byte = 0x11
resetReg byte = 0xFF
)
func (d *Dev) String() string {
return "CCS811"
}
// StartSensorApp initializes sensor to application mode.
func (d *Dev) StartSensorApp() error {
return d.c.Tx([]byte{0xf4}, nil)
}
// SetMeasurementModeRegister sets one of the 5 measurement modes, interrupt generation
// and interrupt threshold.
func (d *Dev) SetMeasurementModeRegister(mmp MeasurementModeParams) error {
mesModeValue := (mmp.MeasurementMode << 4)
if mmp.GenerateInterrupt {
mesModeValue = mesModeValue | (0x1 << 3)
}
if mmp.UseThreshold {
mesModeValue = mesModeValue | (0x1 << 2)
}
return d.c.Tx([]byte{measurementModeReg, byte(mesModeValue)}, nil)
}
// MeasurementModeParams is a structure representing Measuremode register of the sensor.
type MeasurementModeParams struct {
MeasurementMode MeasurementMode
GenerateInterrupt bool // True if sensor should generate interrupts on new measurement.
UseThreshold bool // True if sensor should use thresholds from threshold register.
}
// GetMeasurementModeRegister returns current measurement mode of the sensor.
func (d *Dev) GetMeasurementModeRegister() (MeasurementModeParams, error) {
r := make([]byte, 1)
if err := d.c.Tx([]byte{measurementModeReg}, r); err != nil {
return MeasurementModeParams{}, err
}
mode := MeasurementMode(r[0] >> 4)
threshold := (r[0]&4 == 4)
interrupt := (r[0]&8 == 8)
return MeasurementModeParams{MeasurementMode: mode, GenerateInterrupt: interrupt, UseThreshold: threshold}, nil
}
// Reset sets device into the BOOT mode.
func (d *Dev) Reset() error {
if err := d.c.Tx([]byte{resetReg, 0x11, 0xE5, 0x72, 0x8A}, nil); err != nil {
return err
}
return nil
}
// ReadStatus returns value of status register.
func (d *Dev) ReadStatus() (byte, error) {
r := make([]byte, 1)
if err := d.c.Tx([]byte{statusReg}, r); err != nil {
return 0, err
}
return r[0], nil
}
// ReadRawData provides current and voltage on the sensor.
// Current is in range of 0-63uA. Voltage is in range 0-1.65V.
func (d *Dev) ReadRawData() (physic.ElectricCurrent, physic.ElectricPotential, error) {
r := make([]byte, 2)
if err := d.c.Tx([]byte{rawDataReg}, r); err != nil {
return 0, 0, err
}
current, voltage := valuesFromRawData(r)
return current, voltage, nil
}
// SetEnvironmentData allows to provide temperature and humidity so
// sensor can compensate it's measurement.
func (d *Dev) SetEnvironmentData(temp, humidity float32) error {
rawTemp := uint16((temp + 25) * 512)
rawHum := uint16(humidity * 512)
w := []byte{environmentReg,
byte(rawHum >> 8),
byte(rawHum),
byte(rawTemp >> 8),
byte(rawTemp)}
if err := d.c.Tx(w, nil); err != nil {
return err
}
return nil
}
// GetBaseline provides current baseline used by internal measurement alogrithm.
// For better understanding how to use this value, check the SetBaseline and documentation.
func (d *Dev) GetBaseline() ([]byte, error) {
r := make([]byte, 2)
if err := d.c.Tx([]byte{baselineReg}, r); err != nil {
return nil, err
}
return r, nil
}
// SetBaseline sets current baseline for internal measurement algorithm.
// For more details check sensor's specification.
//
// Manual Baseline Correction.
//
// There is a mechanism within CCS811 to manually save and restore a previously
// saved baseline value using the BASELINE register. The correct time to save
// the baseline will depend on the customer use-case and application.
//
// For devices which are powered for >24 hours at a time:
//
// - During the first 500 hours save the baseline every 24-48 hours.
//
// - After the first 500 hours save the baseline every 5-7 days.
//
// For devices which are powered <24 hours at a time:
//
// - If the device is run in, save the baseline before power down.
//
// - If multiple operating modes are used, a separate baseline should be stored for each.
//
// - The baseline should only be restored when the resistance is stable
// (typically 20-30 minutes).
//
// - If changing from a low to high power mode (without spending at least
// 10 minutes in idle), the sensor resistance should be allowed to settle again
// before restoring the baseline.
//
// Note(s):
//
// 1) If a value is written to the BASELINE register while the sensor
// is stabilising, the output of the TVOC and eCO2 calculations may be higher
// than expected.
//
// 2) The baseline must be written after the conditioning period
func (d *Dev) SetBaseline(baseline []byte) error {
w := []byte{baselineReg, baseline[0], baseline[1]}
if err := d.c.Tx(w, nil); err != nil {
return err
}
return nil
}
// SensorValues represents data read from the sensor.
// Data are populated based on NeededData parameter.
//
// Sensor provides eCO2 measurement in range: 400ppm to 8192ppm,
// and VOC measurement in range: 0ppb to 1187ppb.
//
// Sensing resistor's current is between 0-63uA, and voltage 0-1.65V.
//
// Status represents sensor's status register.
// 1001 0110
// |||||||||
// ||||||| \- 1 = There is an error.
// |||||| \- Reserved.
// ||||| \- Reserved.
// |||| \- 1 = Data ready.
// ||| \- 1 = Valid application firmware loaded.
// || \- Reserved.
// | \- Reserved.
// \- 0 = Firmware in boot mode, 1 Firmware in application mode.
//
// Error represents error state of the sensor if available, otherwise is nil.
type SensorValues struct {
ECO2 int
VOC int
Status byte
Error error
RawDataCurrent physic.ElectricCurrent
RawDataVoltage physic.ElectricPotential
}
// Sense provides data from the sensor.
// This function read all 8 available bytes including error, raw data etc.
// If you want just eCO2 and/or VOC, use SensePartial.
func (d *Dev) Sense(values *SensorValues) error {
return d.SensePartial(ReadAll, values)
}
// SensePartial provides marginaly more efficient reading from the sensor.
// You can specify what subset of data you want through NeededData constants.
func (d *Dev) SensePartial(requested NeededData, values *SensorValues) error {
read := make([]byte, requested)
if err := d.c.Tx([]byte{algoResultsReg}, read); err != nil {
return err
}
if requested >= ReadCO2 {
// Exptected range: 400ppm to 8192ppm.
// 0x3F is used to erase randomly set top bits,
// causing value out of range given by specs.
values.ECO2 = int(uint32(read[0]&0x3F)<<8 | uint32(read[1]))
}
if requested >= ReadCO2VOC {
// Expected range: 0ppb to 1187ppb.
// 0x7 is used to erase randomly set top bits
// causing value out of range given by specs.
values.VOC = int(uint32(read[2]&0x7)<<8 | uint32(read[3]))
}
if requested >= ReadCO2VOCStatus {
values.Status = read[4]
}
if requested == ReadAll {
values.Error = d.errorCodeToError(SensorErrorID(read[5]))
values.RawDataCurrent, values.RawDataVoltage = valuesFromRawData(read[6:])
}
return nil
}
// Parse current and voltage from raw data.
func valuesFromRawData(data []byte) (physic.ElectricCurrent, physic.ElectricPotential) {
c := physic.ElectricCurrent(int64(data[0]>>2) * 1000)
sensorsVoltageUnits := int64((uint16(data[0]&0x03) << 8) | uint16(data[1]))
// 1.65V = 1023
// sensorsVoltageUnits is converted to V, and after that to nV.
// 165 is used instead of 1.65 to prevent types truncation.
p := physic.ElectricPotential((sensorsVoltageUnits * 165 * (1000 * 1000 * 1000) / 102300))
return c, p
}
// FwVersions is a strcutre which aggregates all different versions of sensors features.
//
// HWIdentifier - for family of CCS81x should be 0x81.
//
// HWVersion - hardware major and minor version: 0x1X.
//
// BootVersion - version of firmware bootloader in form Major.Minor.Trivial.
//
// ApplicationVersion - version of firmware application in form Major.Minor.Trivial.
type FwVersions struct {
HWIdentifier byte
HWVersion byte
BootVersion string
ApplicationVersion string
}
// GetFirmwareData populates FwVersions structure with data.
func (d *Dev) GetFirmwareData() (*FwVersions, error) {
version := &FwVersions{}
buffer1 := make([]byte, 1)
if err := d.c.Tx([]byte{0x20}, buffer1); err != nil {
return version, err
}
version.HWIdentifier = buffer1[0]
if err := d.c.Tx([]byte{0x21}, buffer1); err != nil {
return version, err
}
version.HWVersion = buffer1[0]
buffer2 := make([]byte, 2)
if err := d.c.Tx([]byte{0x23}, buffer2); err != nil {
return version, err
}
minor := buffer2[0] & 0x0F
major := (buffer2[0] & 0xF0) >> 4
trivial := buffer2[1]
version.BootVersion = fmt.Sprintf("%d.%d.%d", major, minor, trivial)
if err := d.c.Tx([]byte{0x24}, buffer2); err != nil {
return version, err
}
minor = buffer2[0] & 0x0F
major = (buffer2[0] & 0xF0) >> 4
trivial = buffer2[1]
version.ApplicationVersion = fmt.Sprintf("%d.%d.%d", major, minor, trivial)
return version, nil
}

@ -0,0 +1,214 @@
// Copyright 2019 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
package ccs811
import (
"fmt"
"testing"
"periph.io/x/periph/conn/i2c/i2ctest"
"periph.io/x/periph/conn/physic"
)
func TestBasicInitialisationAndDataRead(t *testing.T) {
bus := i2ctest.Playback{
Ops: []i2ctest.IO{
{Addr: 0x5A, W: []byte{0xf4}, R: nil},
{Addr: 0x5A, W: []byte{measurementModeReg, 0x1C}, R: nil},
{Addr: 0x5A, W: []byte{algoResultsReg}, R: []byte{0x1, 0x2, 0x2, 0x3, 0xF, 0x8, 0xF, 0xF}},
},
DontPanic: true,
}
opts := DefaultOpts
opts.InterruptWhenReady = true
opts.UseThreshold = true
dev, err := New(&bus, &opts)
if err != nil {
t.Fatal(err)
}
data := &SensorValues{}
if err := dev.Sense(data); err == nil {
var vExpected physic.ElectricPotential
vExpected.Set("1.65V") // 682 units
var cExpected physic.ElectricCurrent
cExpected.Set("63uA")
if data.ECO2 != 0x102 &&
data.VOC != 0x203 &&
data.Status != 0xF &&
data.Error != fmt.Errorf("Sensor error: %s", "HEATER_FAULT: The Heater current in the CCS811 is not in range.") &&
data.RawDataCurrent != cExpected &&
data.RawDataVoltage != vExpected {
t.Fatalf("Data parsed incorrectly, got %v", data)
}
} else {
t.Fatal(err)
}
}
func TestMeasurementModeRegisterRead(t *testing.T) {
bus := i2ctest.Playback{
Ops: []i2ctest.IO{
{Addr: 0x5A, W: []byte{0xf4}, R: nil},
{Addr: 0x5A, W: []byte{measurementModeReg, 0x4C}, R: nil},
{Addr: 0x5A, W: []byte{measurementModeReg}, R: []byte{0x4C}},
},
DontPanic: true,
}
opts := DefaultOpts
opts.MeasurementMode = MeasurementModeConstant250
opts.InterruptWhenReady = true
opts.UseThreshold = true
dev, err := New(&bus, &opts)
if err != nil {
t.Fatal(err)
}
mode, err := dev.GetMeasurementModeRegister()
if err != nil ||
mode.GenerateInterrupt != true ||
mode.UseThreshold != true ||
mode.MeasurementMode != MeasurementModeConstant250 {
t.Fatalf("Parsing of Measurement Mode register failed. Got: %+v", mode)
}
}
func TestGetFirmwareData(t *testing.T) {
bus := i2ctest.Playback{
Ops: []i2ctest.IO{
{Addr: 0x5A, W: []byte{0xf4}, R: nil},
{Addr: 0x5A, W: []byte{measurementModeReg, 0x4C}, R: nil},
{Addr: 0x5A, W: []byte{0x20}, R: []byte{0x81}},
{Addr: 0x5A, W: []byte{0x21}, R: []byte{0x15}},
{Addr: 0x5A, W: []byte{0x23}, R: []byte{0x12, 0x03}},
{Addr: 0x5A, W: []byte{0x24}, R: []byte{0x89, 0x20}},
},
DontPanic: true,
}
opts := DefaultOpts
opts.MeasurementMode = MeasurementModeConstant250
opts.InterruptWhenReady = true
opts.UseThreshold = true
dev, err := New(&bus, &opts)
if err != nil {
t.Fatal(err)
}
versions, err := dev.GetFirmwareData()
if err != nil ||
versions.HWIdentifier != 0x81 ||
versions.HWVersion != 0x15 ||
versions.BootVersion != "1.2.3" ||
versions.ApplicationVersion != "8.9.32" {
t.Fatalf("Parsing of firmware version data failed. Got: %+v", versions)
}
}
func TestInvalidSensorAddress(t *testing.T) {
bus := i2ctest.Playback{
Ops: []i2ctest.IO{
{Addr: 0x0, W: nil, R: nil},
},
}
if dev, err := New(&bus, &Opts{Addr: 0xFF, MeasurementMode: MeasurementModeConstant1000}); dev != nil || err == nil {
t.Fatal("New should have failed")
}
}
func TestSetEnvironmentData(t *testing.T) {
bus := i2ctest.Playback{
Ops: []i2ctest.IO{
{Addr: 0x5A, W: []byte{0xf4}, R: nil},
{Addr: 0x5A, W: []byte{measurementModeReg, 0x10}, R: nil},
{Addr: 0x5A, W: []byte{environmentReg, 0x61, 0x00, 0x64, 0x00}, R: nil},
{Addr: 0x5A, W: []byte{environmentReg, 0x64, 0x00, 0x61, 0x00}, R: nil},
},
}
dev, err := New(&bus, &DefaultOpts)
if err != nil {
t.Fatal(err)
}
dev.SetEnvironmentData(25, 48.5)
dev.SetEnvironmentData(23.5, 50)
}
func TestBaseline(t *testing.T) {
bus := i2ctest.Playback{
Ops: []i2ctest.IO{
{Addr: 0x5A, W: []byte{0xf4}, R: nil},
{Addr: 0x5A, W: []byte{measurementModeReg, 0x10}, R: nil},
{Addr: 0x5A, W: []byte{baselineReg}, R: []byte{0xAA, 0xDD}},
{Addr: 0x5A, W: []byte{baselineReg, 0xAA, 0xDD}, R: nil},
},
}
dev, err := New(&bus, &DefaultOpts)
if err != nil {
t.Fatal(err)
}
base, err := dev.GetBaseline()
if err != nil {
t.Fatal(err)
}
dev.SetBaseline(base)
}
func TestReadRawData(t *testing.T) {
bus := i2ctest.Playback{
Ops: []i2ctest.IO{
{Addr: 0x5A, W: []byte{0xf4}, R: nil},
{Addr: 0x5A, W: []byte{measurementModeReg, 0x10}, R: nil},
{Addr: 0x5A, W: []byte{rawDataReg}, R: []byte{0x96, 0xAA}},
},
}
dev, err := New(&bus, &DefaultOpts)
if err != nil {
t.Fatal(err)
}
cur, vol, err := dev.ReadRawData()
if err != nil {
t.Fatal(err)
}
var vExpected physic.ElectricPotential
vExpected.Set("1.1V") // 682 units
var cExpected physic.ElectricCurrent
cExpected.Set("37uA")
if cur != cExpected || vol != vExpected {
t.Fatalf("Raw data reading failed got values: %d, %d", cur, vol)
}
}
func TestRawDataParsing(t *testing.T) {
var vExpected physic.ElectricPotential
vExpected.Set("0.825806451V") // 512 units
var cExpected physic.ElectricCurrent
cExpected.Set("62uA")
c, v := valuesFromRawData([]byte{0xFA, 0x0})
if c != cExpected && v != vExpected {
t.Fatal("current and/or voltage data parsed incorrectly")
}
}
func TestReset(t *testing.T) {
bus := i2ctest.Playback{
Ops: []i2ctest.IO{
{Addr: 0x5B, W: []byte{0xf4}, R: nil},
{Addr: 0x5B, W: []byte{measurementModeReg, 0x10}, R: nil},
{Addr: 0x5B, W: []byte{resetReg, 0x11, 0xE5, 0x72, 0x8A}, R: nil},
},
}
opts := &DefaultOpts
opts.Addr = 0x5B
dev, err := New(&bus, opts)
if err != nil {
t.Fatal(err)
}
dev.Reset()
}

@ -0,0 +1,11 @@
// Copyright 2019 The Periph Authors. All rights reserved.
// Use of this source code is governed under the Apache License, Version 2.0
// that can be found in the LICENSE file.
// Package ccs811 controls CCS811 Volatile Organic Compounds sensor via
// I²C interface.
//
// Datasheet
//
// https://ams.com/documents/20143/36005/CCS811_DS000459_6-00.pdf
package ccs811
Loading…
Cancel
Save