From c2355095809afac580e2a313e3e082b6eb0a5eaa Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sat, 30 Apr 2022 17:24:54 +0200 Subject: [PATCH] scd4x: implement driver for CO2 sensor Signed-off-by: deadprogram --- Makefile | 2 + examples/scd4x/main.go | 39 +++++++++ scd4x/registers.go | 25 ++++++ scd4x/scd4x.go | 178 +++++++++++++++++++++++++++++++++++++++++ scd4x/scd4x_test.go | 15 ++++ 5 files changed, 259 insertions(+) create mode 100644 examples/scd4x/main.go create mode 100644 scd4x/registers.go create mode 100644 scd4x/scd4x.go create mode 100644 scd4x/scd4x_test.go diff --git a/Makefile b/Makefile index 2074ec4..d7ce388 100644 --- a/Makefile +++ b/Makefile @@ -235,6 +235,8 @@ endif @md5sum ./build/test.uf2 tinygo build -size short -o ./build/test.hex -target=pico ./examples/irremote/main.go @md5sum ./build/test.hex + tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/scd4x/main.go + @md5sum ./build/test.uf2 DRIVERS = $(wildcard */) NOTESTS = build examples flash semihosting pcd8544 shiftregister st7789 microphone mcp3008 gps microbitmatrix \ diff --git a/examples/scd4x/main.go b/examples/scd4x/main.go new file mode 100644 index 0000000..39c1ef7 --- /dev/null +++ b/examples/scd4x/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "machine" + "time" + + "tinygo.org/x/drivers/scd4x" +) + +var ( + i2c = machine.I2C0 + sensor = scd4x.New(i2c) +) + +func main() { + time.Sleep(1500 * time.Millisecond) + + i2c.Configure(machine.I2CConfig{}) + if err := sensor.Configure(); err != nil { + println(err) + } + + time.Sleep(1500 * time.Millisecond) + + if err := sensor.StartPeriodicMeasurement(); err != nil { + println(err) + } + + time.Sleep(1500 * time.Millisecond) + + for { + co2, err := sensor.ReadCO2() + if err != nil { + println(err) + } + println("CO2", co2) + time.Sleep(time.Second) + } +} diff --git a/scd4x/registers.go b/scd4x/registers.go new file mode 100644 index 0000000..af057b4 --- /dev/null +++ b/scd4x/registers.go @@ -0,0 +1,25 @@ +package scd4x + +const ( + // Address is default I2C address. + Address = 0x62 + + CmdDataReady = 0xE4B8 + CmdFactoryReset = 0x3632 + CmdForcedRecal = 0x362F + CmdGetAltitude = 0x2322 + CmdGetASCE = 0x2313 + CmdGetTempOffset = 0x2318 + CmdPersistSettings = 0x3615 + CmdReadMeasurement = 0xEC05 + CmdReinit = 0x3646 + CmdSelfTest = 0x3639 + CmdSerialNumber = 0x3682 + CmdSetAltitude = 0x2427 + CmdSetASCE = 0x2416 + CmdSetPressure = 0xE000 + CmdSetTempOffset = 0x241D + CmdStartLowPowerPeriodicMeasurement = 0x21AC + CmdStartPeriodicMeasurement = 0x21B1 + CmdStopPeriodicMeasurement = 0x3F86 +) diff --git a/scd4x/scd4x.go b/scd4x/scd4x.go new file mode 100644 index 0000000..0de2fe5 --- /dev/null +++ b/scd4x/scd4x.go @@ -0,0 +1,178 @@ +// Package scd4x provides a driver for the scd4x I2C envrironment sensor. +// +// Datasheet: https://sensirion.com/media/documents/C4B87CE6/61652F80/Sensirion_CO2_Sensors_SCD4x_Datasheet.pdf +// +// This driver is heavily influenced by the scd4x code from Adafruit for CircuitPython: +// https://github.com/adafruit/Adafruit_CircuitPython_SCD4X +// Thank you! +// +package scd4x // import "tinygo.org/x/drivers/scd4x" + +import ( + "encoding/binary" + "time" + + "tinygo.org/x/drivers" +) + +type Device struct { + bus drivers.I2C + tx []byte + rx []byte + Address uint8 + + // used to cache the most recent readings + co2 uint16 + temperature uint16 + humidity uint16 +} + +// New returns SCD4x device for the provided I2C bus using default address of 0x62. +func New(i2c drivers.I2C) *Device { + return &Device{ + bus: i2c, + tx: make([]byte, 5), + rx: make([]byte, 18), + Address: Address, + } +} + +// Configure the device. +func (d *Device) Configure() (err error) { + if err := d.StopPeriodicMeasurement(); err != nil { + return err + } + time.Sleep(500 * time.Millisecond) + + // reset the chip + if err := d.sendCommand(CmdReinit); err != nil { + return err + } + + time.Sleep(20 * time.Millisecond) + return +} + +// Connected returns whether sensor has been found. +func (d *Device) Connected() bool { + // TODO: something here to check if the sensor is connected + return true +} + +// DataReady checks the sensor to see if new data is available. +func (d *Device) DataReady() (bool, error) { + if err := d.sendCommandWithResult(CmdDataReady, d.rx[0:3]); err != nil { + return false, err + } + return !(d.rx[0]&0x07 == 0 && d.rx[1] == 0), nil +} + +// StartPeriodicMeasurement puts the sensor into working mode, about 5s per measurement. +func (d *Device) StartPeriodicMeasurement() error { + return d.sendCommand(CmdStartPeriodicMeasurement) +} + +// StopPeriodicMeasurement stops the sensor reading data. +func (d *Device) StopPeriodicMeasurement() error { + return d.sendCommand(CmdStopPeriodicMeasurement) +} + +// StartLowPowerPeriodicMeasurement puts the sensor into low power working mode, +// about 30s per measurement. +func (d *Device) StartLowPowerPeriodicMeasurement() error { + return d.sendCommand(CmdStartLowPowerPeriodicMeasurement) +} + +// ReadData reads the data from the sensor and caches it. +func (d *Device) ReadData() error { + if err := d.sendCommandWithResult(CmdReadMeasurement, d.rx[0:9]); err != nil { + return err + } + d.co2 = binary.BigEndian.Uint16(d.rx[0:2]) + d.temperature = binary.BigEndian.Uint16(d.rx[3:5]) + d.humidity = binary.BigEndian.Uint16(d.rx[6:8]) + return nil +} + +// ReadCO2 returns the CO2 concentration in PPM (parts per million). +func (d *Device) ReadCO2() (co2 int32, err error) { + ok, err := d.DataReady() + if err != nil { + return 0, err + } + if ok { + err = d.ReadData() + } + return int32(d.co2), err +} + +// ReadTemperature returns the temperature in celsius milli degrees (°C/1000) +func (d *Device) ReadTemperature() (temperature int32, err error) { + ok, err := d.DataReady() + if err != nil { + return 0, err + } + if ok { + err = d.ReadData() + } + return (-1 * 45) + 175*(int32(d.temperature)/0x100000)*1000, err +} + +// ReadTempC returns the value in the temperature value in Celsius. +func (d *Device) ReadTempC() float32 { + t, _ := d.ReadTemperature() + return float32(t) / 1000 +} + +// ReadTempF returns the value in the temperature value in Fahrenheit. +func (d *Device) ReadTempF() float32 { + return d.ReadTempC()*1.8 + 32.0 +} + +// ReadHumidity returns the current relative humidity in %rH. +func (d *Device) ReadHumidity() (humidity int32, err error) { + ok, err := d.DataReady() + if err != nil { + return 0, err + } + if ok { + err = d.ReadData() + } + return (100 * int32(d.humidity) / 0x100000), err +} + +func (d *Device) sendCommand(command uint16) error { + binary.BigEndian.PutUint16(d.tx[0:], command) + return d.bus.Tx(uint16(d.Address), d.tx[0:2], nil) +} + +func (d *Device) sendCommandWithValue(command, value uint16) error { + binary.BigEndian.PutUint16(d.tx[0:], command) + binary.BigEndian.PutUint16(d.tx[2:], value) + d.tx[4] = crc8(d.tx[2:4]) + return d.bus.Tx(uint16(d.Address), d.tx[0:5], nil) +} + +func (d *Device) sendCommandWithResult(command uint16, result []byte) error { + binary.BigEndian.PutUint16(d.tx[0:], command) + if err := d.bus.Tx(uint16(d.Address), d.tx[0:2], nil); err != nil { + return err + } + time.Sleep(time.Millisecond) + return d.bus.Tx(uint16(d.Address), nil, result) +} + +func crc8(buf []byte) uint8 { + var crc uint8 = 0xff + for _, b := range buf { + crc ^= b + for i := 0; i < 8; i++ { + if crc&0x80 != 0 { + crc = (crc << 1) ^ 0x31 + } else { + crc <<= 1 + } + } + } + return crc & 0xff +} diff --git a/scd4x/scd4x_test.go b/scd4x/scd4x_test.go new file mode 100644 index 0000000..25bd0fb --- /dev/null +++ b/scd4x/scd4x_test.go @@ -0,0 +1,15 @@ +package scd4x + +import ( + "testing" + + qt "github.com/frankban/quicktest" + "tinygo.org/x/drivers/tester" +) + +func TestDefaultI2CAddress(t *testing.T) { + c := qt.New(t) + bus := tester.NewI2CBus(c) + dev := New(bus) + c.Assert(dev.Address, qt.Equals, uint8(Address)) +}