Skip to content

Commit c235509

Browse files
committed
scd4x: implement driver for CO2 sensor
Signed-off-by: deadprogram <[email protected]>
1 parent dbff576 commit c235509

File tree

5 files changed

+259
-0
lines changed

5 files changed

+259
-0
lines changed

Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ endif
235235
@md5sum ./build/test.uf2
236236
tinygo build -size short -o ./build/test.hex -target=pico ./examples/irremote/main.go
237237
@md5sum ./build/test.hex
238+
tinygo build -size short -o ./build/test.uf2 -target=pico ./examples/scd4x/main.go
239+
@md5sum ./build/test.uf2
238240

239241
DRIVERS = $(wildcard */)
240242
NOTESTS = build examples flash semihosting pcd8544 shiftregister st7789 microphone mcp3008 gps microbitmatrix \

examples/scd4x/main.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package main
2+
3+
import (
4+
"machine"
5+
"time"
6+
7+
"tinygo.org/x/drivers/scd4x"
8+
)
9+
10+
var (
11+
i2c = machine.I2C0
12+
sensor = scd4x.New(i2c)
13+
)
14+
15+
func main() {
16+
time.Sleep(1500 * time.Millisecond)
17+
18+
i2c.Configure(machine.I2CConfig{})
19+
if err := sensor.Configure(); err != nil {
20+
println(err)
21+
}
22+
23+
time.Sleep(1500 * time.Millisecond)
24+
25+
if err := sensor.StartPeriodicMeasurement(); err != nil {
26+
println(err)
27+
}
28+
29+
time.Sleep(1500 * time.Millisecond)
30+
31+
for {
32+
co2, err := sensor.ReadCO2()
33+
if err != nil {
34+
println(err)
35+
}
36+
println("CO2", co2)
37+
time.Sleep(time.Second)
38+
}
39+
}

scd4x/registers.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package scd4x
2+
3+
const (
4+
// Address is default I2C address.
5+
Address = 0x62
6+
7+
CmdDataReady = 0xE4B8
8+
CmdFactoryReset = 0x3632
9+
CmdForcedRecal = 0x362F
10+
CmdGetAltitude = 0x2322
11+
CmdGetASCE = 0x2313
12+
CmdGetTempOffset = 0x2318
13+
CmdPersistSettings = 0x3615
14+
CmdReadMeasurement = 0xEC05
15+
CmdReinit = 0x3646
16+
CmdSelfTest = 0x3639
17+
CmdSerialNumber = 0x3682
18+
CmdSetAltitude = 0x2427
19+
CmdSetASCE = 0x2416
20+
CmdSetPressure = 0xE000
21+
CmdSetTempOffset = 0x241D
22+
CmdStartLowPowerPeriodicMeasurement = 0x21AC
23+
CmdStartPeriodicMeasurement = 0x21B1
24+
CmdStopPeriodicMeasurement = 0x3F86
25+
)

scd4x/scd4x.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// Package scd4x provides a driver for the scd4x I2C envrironment sensor.
2+
//
3+
// Datasheet: https://sensirion.com/media/documents/C4B87CE6/61652F80/Sensirion_CO2_Sensors_SCD4x_Datasheet.pdf
4+
//
5+
// This driver is heavily influenced by the scd4x code from Adafruit for CircuitPython:
6+
// https://github.com/adafruit/Adafruit_CircuitPython_SCD4X
7+
// Thank you!
8+
//
9+
package scd4x // import "tinygo.org/x/drivers/scd4x"
10+
11+
import (
12+
"encoding/binary"
13+
"time"
14+
15+
"tinygo.org/x/drivers"
16+
)
17+
18+
type Device struct {
19+
bus drivers.I2C
20+
tx []byte
21+
rx []byte
22+
Address uint8
23+
24+
// used to cache the most recent readings
25+
co2 uint16
26+
temperature uint16
27+
humidity uint16
28+
}
29+
30+
// New returns SCD4x device for the provided I2C bus using default address of 0x62.
31+
func New(i2c drivers.I2C) *Device {
32+
return &Device{
33+
bus: i2c,
34+
tx: make([]byte, 5),
35+
rx: make([]byte, 18),
36+
Address: Address,
37+
}
38+
}
39+
40+
// Configure the device.
41+
func (d *Device) Configure() (err error) {
42+
if err := d.StopPeriodicMeasurement(); err != nil {
43+
return err
44+
}
45+
time.Sleep(500 * time.Millisecond)
46+
47+
// reset the chip
48+
if err := d.sendCommand(CmdReinit); err != nil {
49+
return err
50+
}
51+
52+
time.Sleep(20 * time.Millisecond)
53+
return
54+
}
55+
56+
// Connected returns whether sensor has been found.
57+
func (d *Device) Connected() bool {
58+
// TODO: something here to check if the sensor is connected
59+
return true
60+
}
61+
62+
// DataReady checks the sensor to see if new data is available.
63+
func (d *Device) DataReady() (bool, error) {
64+
if err := d.sendCommandWithResult(CmdDataReady, d.rx[0:3]); err != nil {
65+
return false, err
66+
}
67+
return !(d.rx[0]&0x07 == 0 && d.rx[1] == 0), nil
68+
}
69+
70+
// StartPeriodicMeasurement puts the sensor into working mode, about 5s per measurement.
71+
func (d *Device) StartPeriodicMeasurement() error {
72+
return d.sendCommand(CmdStartPeriodicMeasurement)
73+
}
74+
75+
// StopPeriodicMeasurement stops the sensor reading data.
76+
func (d *Device) StopPeriodicMeasurement() error {
77+
return d.sendCommand(CmdStopPeriodicMeasurement)
78+
}
79+
80+
// StartLowPowerPeriodicMeasurement puts the sensor into low power working mode,
81+
// about 30s per measurement.
82+
func (d *Device) StartLowPowerPeriodicMeasurement() error {
83+
return d.sendCommand(CmdStartLowPowerPeriodicMeasurement)
84+
}
85+
86+
// ReadData reads the data from the sensor and caches it.
87+
func (d *Device) ReadData() error {
88+
if err := d.sendCommandWithResult(CmdReadMeasurement, d.rx[0:9]); err != nil {
89+
return err
90+
}
91+
d.co2 = binary.BigEndian.Uint16(d.rx[0:2])
92+
d.temperature = binary.BigEndian.Uint16(d.rx[3:5])
93+
d.humidity = binary.BigEndian.Uint16(d.rx[6:8])
94+
return nil
95+
}
96+
97+
// ReadCO2 returns the CO2 concentration in PPM (parts per million).
98+
func (d *Device) ReadCO2() (co2 int32, err error) {
99+
ok, err := d.DataReady()
100+
if err != nil {
101+
return 0, err
102+
}
103+
if ok {
104+
err = d.ReadData()
105+
}
106+
return int32(d.co2), err
107+
}
108+
109+
// ReadTemperature returns the temperature in celsius milli degrees (°C/1000)
110+
func (d *Device) ReadTemperature() (temperature int32, err error) {
111+
ok, err := d.DataReady()
112+
if err != nil {
113+
return 0, err
114+
}
115+
if ok {
116+
err = d.ReadData()
117+
}
118+
return (-1 * 45) + 175*(int32(d.temperature)/0x100000)*1000, err
119+
}
120+
121+
// ReadTempC returns the value in the temperature value in Celsius.
122+
func (d *Device) ReadTempC() float32 {
123+
t, _ := d.ReadTemperature()
124+
return float32(t) / 1000
125+
}
126+
127+
// ReadTempF returns the value in the temperature value in Fahrenheit.
128+
func (d *Device) ReadTempF() float32 {
129+
return d.ReadTempC()*1.8 + 32.0
130+
}
131+
132+
// ReadHumidity returns the current relative humidity in %rH.
133+
func (d *Device) ReadHumidity() (humidity int32, err error) {
134+
ok, err := d.DataReady()
135+
if err != nil {
136+
return 0, err
137+
}
138+
if ok {
139+
err = d.ReadData()
140+
}
141+
return (100 * int32(d.humidity) / 0x100000), err
142+
}
143+
144+
func (d *Device) sendCommand(command uint16) error {
145+
binary.BigEndian.PutUint16(d.tx[0:], command)
146+
return d.bus.Tx(uint16(d.Address), d.tx[0:2], nil)
147+
}
148+
149+
func (d *Device) sendCommandWithValue(command, value uint16) error {
150+
binary.BigEndian.PutUint16(d.tx[0:], command)
151+
binary.BigEndian.PutUint16(d.tx[2:], value)
152+
d.tx[4] = crc8(d.tx[2:4])
153+
return d.bus.Tx(uint16(d.Address), d.tx[0:5], nil)
154+
}
155+
156+
func (d *Device) sendCommandWithResult(command uint16, result []byte) error {
157+
binary.BigEndian.PutUint16(d.tx[0:], command)
158+
if err := d.bus.Tx(uint16(d.Address), d.tx[0:2], nil); err != nil {
159+
return err
160+
}
161+
time.Sleep(time.Millisecond)
162+
return d.bus.Tx(uint16(d.Address), nil, result)
163+
}
164+
165+
func crc8(buf []byte) uint8 {
166+
var crc uint8 = 0xff
167+
for _, b := range buf {
168+
crc ^= b
169+
for i := 0; i < 8; i++ {
170+
if crc&0x80 != 0 {
171+
crc = (crc << 1) ^ 0x31
172+
} else {
173+
crc <<= 1
174+
}
175+
}
176+
}
177+
return crc & 0xff
178+
}

scd4x/scd4x_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package scd4x
2+
3+
import (
4+
"testing"
5+
6+
qt "github.com/frankban/quicktest"
7+
"tinygo.org/x/drivers/tester"
8+
)
9+
10+
func TestDefaultI2CAddress(t *testing.T) {
11+
c := qt.New(t)
12+
bus := tester.NewI2CBus(c)
13+
dev := New(bus)
14+
c.Assert(dev.Address, qt.Equals, uint8(Address))
15+
}

0 commit comments

Comments
 (0)