Skip to content

Commit 6791c13

Browse files
NetForcesdeadprogram
authored andcommitted
scd30: add CO2 sensor driver
1 parent 119afb9 commit 6791c13

5 files changed

Lines changed: 650 additions & 0 deletions

File tree

examples/scd30/main.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package main
2+
3+
import (
4+
"machine"
5+
"time"
6+
7+
"tinygo.org/x/drivers"
8+
"tinygo.org/x/drivers/scd30"
9+
)
10+
11+
var sensor = scd30.New(machine.I2C0)
12+
13+
func main() {
14+
// The SCD30 requires clock stretching and supports I2C speeds up to 100kHz.
15+
if err := machine.I2C0.Configure(machine.I2CConfig{Frequency: 50 * machine.KHz}); err != nil {
16+
println("could not configure I2C:", err.Error())
17+
return
18+
}
19+
20+
if !sensor.Connected() {
21+
println("SCD30 not detected")
22+
return
23+
}
24+
if err := sensor.Configure(scd30.DefaultConfig); err != nil {
25+
println("could not configure SCD30:", err.Error())
26+
return
27+
}
28+
if err := sensor.StartContinuousMeasurement(0); err != nil {
29+
println("could not start SCD30:", err.Error())
30+
return
31+
}
32+
33+
for {
34+
ready, err := sensor.DataReady()
35+
if err != nil {
36+
println("could not read SCD30 status:", err.Error())
37+
time.Sleep(time.Second)
38+
continue
39+
}
40+
if !ready {
41+
time.Sleep(250 * time.Millisecond)
42+
continue
43+
}
44+
45+
if err := sensor.Update(drivers.AllMeasurements); err != nil {
46+
println("could not read SCD30 measurement:", err.Error())
47+
time.Sleep(time.Second)
48+
continue
49+
}
50+
51+
println("CO2 (ppm):", sensor.CO2())
52+
println("temperature (mC):", sensor.Temperature())
53+
println("humidity (0.01%):", sensor.Humidity())
54+
}
55+
}

scd30/registers.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package scd30
2+
3+
const (
4+
// Address is the default and only I2C address of the SCD30.
5+
Address uint16 = 0x61
6+
7+
commandStartContinuousMeasurement = 0x0010
8+
commandStopContinuousMeasurement = 0x0104
9+
commandDataReady = 0x0202
10+
commandReadMeasurement = 0x0300
11+
commandSetMeasurementInterval = 0x4600
12+
commandSetAutoCalibration = 0x5306
13+
)
14+
15+
const (
16+
minimumMeasurementInterval = 2
17+
maximumMeasurementInterval = 1800
18+
minimumAmbientPressure = 700
19+
maximumAmbientPressure = 1400
20+
)

scd30/scd30.go

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
// Package scd30 provides a driver for the Sensirion SCD30 CO2, temperature,
2+
// and humidity sensor.
3+
//
4+
// Datasheet: https://sensirion.com/media/documents/D7CEEF4A/6165372F/Sensirion_CO2_Sensors_SCD30_Interface_Description.pdf
5+
package scd30 // import "tinygo.org/x/drivers/scd30"
6+
7+
import (
8+
"encoding/binary"
9+
"errors"
10+
"math"
11+
"time"
12+
13+
"tinygo.org/x/drivers"
14+
)
15+
16+
const readDelay = 4 * time.Millisecond
17+
18+
var (
19+
ErrCRC = errors.New("scd30: invalid CRC")
20+
21+
ErrInvalidInterval = errors.New("scd30: measurement interval must be between 2 and 1800 seconds")
22+
23+
ErrInvalidAmbientPressure = errors.New("scd30: ambient pressure must be zero or between 700 and 1400 mbar")
24+
)
25+
26+
// Config contains the SCD30 continuous measurement configuration.
27+
type Config struct {
28+
// MeasurementInterval is the interval between measurements in seconds and
29+
// must be between 2 and 1800.
30+
MeasurementInterval uint16
31+
32+
// AutomaticSelfCalibration enables or disables automatic self-calibration.
33+
AutomaticSelfCalibration bool
34+
}
35+
36+
// DefaultConfig contains the power-on defaults documented for the SCD30.
37+
var DefaultConfig = Config{
38+
MeasurementInterval: 2,
39+
AutomaticSelfCalibration: false,
40+
}
41+
42+
// Device is a Sensirion SCD30 sensor connected over I2C.
43+
type Device struct {
44+
bus drivers.I2C
45+
tx [5]byte
46+
rx [18]byte
47+
48+
co2 int32
49+
temperature int32
50+
humidity int32
51+
}
52+
53+
var _ drivers.Sensor = (*Device)(nil)
54+
55+
// New returns a new SCD30 driver. It performs no I/O.
56+
func New(bus drivers.I2C) *Device {
57+
return &Device{bus: bus}
58+
}
59+
60+
// Configure applies the continuous measurement interval and automatic
61+
// self-calibration settings. It does not start continuous measurement.
62+
func (d *Device) Configure(config Config) error {
63+
if err := d.SetMeasurementInterval(config.MeasurementInterval); err != nil {
64+
return err
65+
}
66+
return d.SetAutomaticSelfCalibration(config.AutomaticSelfCalibration)
67+
}
68+
69+
// Connected reports whether an SCD30 responds with a valid data-ready status.
70+
func (d *Device) Connected() bool {
71+
_, err := d.DataReady()
72+
return err == nil
73+
}
74+
75+
// SetMeasurementInterval sets the continuous measurement interval in seconds.
76+
func (d *Device) SetMeasurementInterval(seconds uint16) error {
77+
if seconds < minimumMeasurementInterval || seconds > maximumMeasurementInterval {
78+
return ErrInvalidInterval
79+
}
80+
return d.writeCommandWithArgument(commandSetMeasurementInterval, seconds)
81+
}
82+
83+
// SetAutomaticSelfCalibration enables or disables automatic self-calibration.
84+
func (d *Device) SetAutomaticSelfCalibration(enabled bool) error {
85+
var value uint16
86+
if enabled {
87+
value = 1
88+
}
89+
return d.writeCommandWithArgument(commandSetAutoCalibration, value)
90+
}
91+
92+
// StartContinuousMeasurement begins periodic measurements. Ambient pressure
93+
// must be zero to disable pressure compensation, or between 700 and 1400 mbar.
94+
func (d *Device) StartContinuousMeasurement(ambientPressure uint16) error {
95+
if ambientPressure != 0 && (ambientPressure < minimumAmbientPressure || ambientPressure > maximumAmbientPressure) {
96+
return ErrInvalidAmbientPressure
97+
}
98+
return d.writeCommandWithArgument(commandStartContinuousMeasurement, ambientPressure)
99+
}
100+
101+
// StopContinuousMeasurement stops periodic measurements.
102+
func (d *Device) StopContinuousMeasurement() error {
103+
return d.writeCommand(commandStopContinuousMeasurement)
104+
}
105+
106+
// DataReady reports whether a new measurement can be read.
107+
func (d *Device) DataReady() (bool, error) {
108+
if err := d.readCommand(commandDataReady, d.rx[:3]); err != nil {
109+
return false, err
110+
}
111+
value, err := decodeWord(d.rx[:3])
112+
if err != nil {
113+
return false, err
114+
}
115+
return value != 0, nil
116+
}
117+
118+
// ReadMeasurement reads and caches the latest CO2, temperature, and humidity
119+
// measurement. Use DataReady before calling ReadMeasurement.
120+
func (d *Device) ReadMeasurement() error {
121+
if err := d.readCommand(commandReadMeasurement, d.rx[:18]); err != nil {
122+
return err
123+
}
124+
125+
var data [12]byte
126+
for source, destination := 0, 0; source < 18; source, destination = source+3, destination+2 {
127+
value, err := decodeWord(d.rx[source : source+3])
128+
if err != nil {
129+
return err
130+
}
131+
binary.BigEndian.PutUint16(data[destination:destination+2], value)
132+
}
133+
134+
co2 := decodeFloat32(data[0:4])
135+
temperature := decodeFloat32(data[4:8])
136+
humidity := decodeFloat32(data[8:12])
137+
138+
d.co2 = roundFixed(co2, 1)
139+
d.temperature = roundFixed(temperature, 1000)
140+
d.humidity = roundFixed(humidity, 100)
141+
return nil
142+
}
143+
144+
// Update reads and caches all measurements if any supported measurement was
145+
// requested. The SCD30 provides all three values in a single transaction.
146+
func (d *Device) Update(which drivers.Measurement) error {
147+
if which&(drivers.Concentration|drivers.Temperature|drivers.Humidity) == 0 {
148+
return nil
149+
}
150+
return d.ReadMeasurement()
151+
}
152+
153+
// CO2 returns the last read CO2 concentration in parts per million.
154+
func (d *Device) CO2() int32 {
155+
return d.co2
156+
}
157+
158+
// Temperature returns the last read temperature in millidegrees Celsius.
159+
func (d *Device) Temperature() int32 {
160+
return d.temperature
161+
}
162+
163+
// Humidity returns the last read relative humidity in hundredths of a percent.
164+
func (d *Device) Humidity() int32 {
165+
return d.humidity
166+
}
167+
168+
func (d *Device) readCommand(command uint16, response []byte) error {
169+
if err := d.writeCommand(command); err != nil {
170+
return err
171+
}
172+
// The datasheet requires a delay greater than 3ms before reading.
173+
time.Sleep(readDelay)
174+
return d.bus.Tx(Address, nil, response)
175+
}
176+
177+
func (d *Device) writeCommand(command uint16) error {
178+
binary.BigEndian.PutUint16(d.tx[:2], command)
179+
return d.bus.Tx(Address, d.tx[:2], nil)
180+
}
181+
182+
func (d *Device) writeCommandWithArgument(command, argument uint16) error {
183+
binary.BigEndian.PutUint16(d.tx[:2], command)
184+
binary.BigEndian.PutUint16(d.tx[2:4], argument)
185+
d.tx[4] = crc8(d.tx[2:4])
186+
return d.bus.Tx(Address, d.tx[:5], nil)
187+
}
188+
189+
func decodeWord(data []byte) (uint16, error) {
190+
if len(data) != 3 || crc8(data[:2]) != data[2] {
191+
return 0, ErrCRC
192+
}
193+
return binary.BigEndian.Uint16(data[:2]), nil
194+
}
195+
196+
func decodeFloat32(data []byte) float32 {
197+
return math.Float32frombits(binary.BigEndian.Uint32(data))
198+
}
199+
200+
func roundFixed(value float32, scale int32) int32 {
201+
scaled := value * float32(scale)
202+
if scaled < 0 {
203+
return int32(scaled - 0.5)
204+
}
205+
return int32(scaled + 0.5)
206+
}
207+
208+
func crc8(data []byte) byte {
209+
value := byte(0xff)
210+
for _, current := range data {
211+
value ^= current
212+
for bit := 0; bit < 8; bit++ {
213+
if value&0x80 != 0 {
214+
value = value<<1 ^ 0x31
215+
} else {
216+
value <<= 1
217+
}
218+
}
219+
}
220+
return value
221+
}

0 commit comments

Comments
 (0)